From f2714cce888802443ae8675b7cceda19cab0d716 Mon Sep 17 00:00:00 2001 From: oppiliappan Date: Mon, 06 Jul 2026 17:27:36 +0000 Subject: [PATCH] appview/bsky: switch to manual post fetching indigo needs a version bump to support galleries in responses Signed-off-by: oppiliappan --- appview/bsky/client.go | 186 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------- appview/db/bsky.go | 5 +++-- appview/models/bsky.go | 83 ++++++++++++++++++++++++++++------------------------------------------------------- 3 file(s) changed, 202 insertion(s)(+), 72 deletion(s)(-) diff --git a/appview/bsky/client.go b/appview/bsky/client.go --- a/appview/bsky/client.go +++ b/appview/bsky/client.go @@ -2,42 +2,198 @@ import ( "context" + "encoding/json" "log" + "net/http" + "time" - bsky "github.com/bluesky-social/indigo/api/bsky" + "github.com/bluesky-social/indigo/atproto/syntax" "github.com/bluesky-social/indigo/xrpc" "tangled.org/core/appview/models" "tangled.org/core/consts" ) +type rawFeedOutput struct { + Cursor *string `json:"cursor,omitempty"` + Feed []rawFeedItem `json:"feed"` +} + +type rawFeedItem struct { + Post rawPostView `json:"post"` +} + +type rawPostView struct { + URI string `json:"uri"` + Author struct { + DID string `json:"did"` + } `json:"author"` + Record rawRecord `json:"record"` + Embed json.RawMessage `json:"embed,omitempty"` + LikeCount *int64 `json:"likeCount,omitempty"` + ReplyCount *int64 `json:"replyCount,omitempty"` + RepostCount *int64 `json:"repostCount,omitempty"` + QuoteCount *int64 `json:"quoteCount,omitempty"` +} + +type rawRecord struct { + Text string `json:"text"` + CreatedAt string `json:"createdAt"` + Langs []string `json:"langs,omitempty"` + Tags []string `json:"tags,omitempty"` + Facets json.RawMessage `json:"facets,omitempty"` +} + +type rawEmbedType struct { + Type string `json:"$type"` +} + func FetchPosts(ctx context.Context, c *xrpc.Client, limit int, cursor string) ([]models.BskyPost, string, error) { - resp, err := bsky.FeedGetAuthorFeed(ctx, c, consts.TangledDid, cursor, "posts_no_replies", false, int64(limit)) - if err != nil { + var out rawFeedOutput + + params := map[string]any{ + "actor": consts.TangledDid, + "filter": "posts_no_replies", + "limit": int64(limit), + } + if cursor != "" { + params["cursor"] = cursor + } + if err := c.Do(ctx, http.MethodGet, "", "app.bsky.feed.getAuthorFeed", params, nil, &out); err != nil { return nil, "", err } var posts []models.BskyPost - for _, feedViewPost := range resp.Feed { - // skip quote posts - if feedViewPost.Post.Embed != nil && feedViewPost.Post.Embed.EmbedRecord_View != nil { - continue + for _, feedItem := range out.Feed { + raw := feedItem.Post + + if len(raw.Embed) > 0 { + var t rawEmbedType + json.Unmarshal(raw.Embed, &t) + if t.Type == "app.bsky.embed.record#view" { + continue + } } - post, err := models.NewBskyPostFromView(feedViewPost.Post) + atUri, err := syntax.ParseATURI(raw.URI) if err != nil { - log.Println(err) + log.Println("bsky: parse AT URI:", err) continue } - posts = append(posts, *post) + createdAt, err := time.Parse(time.RFC3339, raw.Record.CreatedAt) + if err != nil { + log.Println("bsky: parse createdAt:", err) + continue + } + + post := models.BskyPost{ + AuthorDid: raw.Author.DID, + Rkey: atUri.RecordKey().String(), + Text: raw.Record.Text, + CreatedAt: createdAt, + Langs: raw.Record.Langs, + Tags: raw.Record.Tags, + Facets: raw.Record.Facets, + } + + if raw.LikeCount != nil { + post.LikeCount = *raw.LikeCount + } + if raw.ReplyCount != nil { + post.ReplyCount = *raw.ReplyCount + } + if raw.RepostCount != nil { + post.RepostCount = *raw.RepostCount + } + if raw.QuoteCount != nil { + post.QuoteCount = *raw.QuoteCount + } + + if len(raw.Embed) > 0 { + embed, err := parseEmbed(raw.Embed) + if err != nil { + log.Println("bsky: parse embed:", err) + } + post.Embed = embed + } + + posts = append(posts, post) } - return posts, stringPtr(resp.Cursor), nil + nextCursor := "" + if out.Cursor != nil { + nextCursor = *out.Cursor + } + + return posts, nextCursor, nil } -func stringPtr(s *string) string { - if s == nil { - return "" +func parseEmbed(raw json.RawMessage) (*models.PostEmbed, error) { + var t rawEmbedType + if err := json.Unmarshal(raw, &t); err != nil { + return nil, err } - return *s + + switch t.Type { + case "app.bsky.embed.images#view": + var v struct { + Images []models.PostImage `json:"images"` + } + if err := json.Unmarshal(raw, &v); err != nil { + return nil, err + } + return &models.PostEmbed{Images: v.Images}, nil + + case "app.bsky.embed.gallery#view": + // gallery items use "thumbnail" instead of "thumb" + var v struct { + Items []struct { + Fullsize string `json:"fullsize"` + Thumbnail string `json:"thumbnail"` + Alt string `json:"alt"` + AspectRatio *models.AspectRatio `json:"aspectRatio,omitempty"` + } `json:"items"` + } + if err := json.Unmarshal(raw, &v); err != nil { + return nil, err + } + embed := &models.PostEmbed{} + for _, item := range v.Items { + embed.Images = append(embed.Images, models.PostImage{ + Fullsize: item.Fullsize, + Thumb: item.Thumbnail, + Alt: item.Alt, + AspectRatio: item.AspectRatio, + }) + } + return embed, nil + + case "app.bsky.embed.external#view": + var v struct { + External models.PostExternal `json:"external"` + } + if err := json.Unmarshal(raw, &v); err != nil { + return nil, err + } + return &models.PostEmbed{External: &v.External}, nil + + case "app.bsky.embed.video#view": + var v models.PostVideo + if err := json.Unmarshal(raw, &v); err != nil { + return nil, err + } + return &models.PostEmbed{Video: &v}, nil + + case "app.bsky.embed.recordWithMedia#view": + var v struct { + Media json.RawMessage `json:"media"` + } + if err := json.Unmarshal(raw, &v); err != nil { + return nil, err + } + return parseEmbed(v.Media) + + default: + return nil, nil + } } diff --git a/appview/db/bsky.go b/appview/db/bsky.go --- a/appview/db/bsky.go +++ b/appview/db/bsky.go @@ -29,7 +29,7 @@ langsJSON, _ = json.Marshal(post.Langs) } if len(post.Facets) > 0 { - facetsJSON, _ = json.Marshal(post.Facets) + facetsJSON = post.Facets // already raw JSON bytes } if post.Embed != nil { embedJSON, _ = json.Marshal(post.Embed) @@ -109,7 +109,8 @@ json.Unmarshal([]byte(facets.V), &post.Facets) } if embed.Valid && embed.V != "" { - json.Unmarshal([]byte(embed.V), &post.Embed) + post.Embed = new(models.PostEmbed) + json.Unmarshal([]byte(embed.V), post.Embed) } posts = append(posts, post) diff --git a/appview/models/bsky.go b/appview/models/bsky.go --- a/appview/models/bsky.go +++ b/appview/models/bsky.go @@ -1,10 +1,8 @@ package models import ( + "encoding/json" "time" - - apibsky "github.com/bluesky-social/indigo/api/bsky" - "github.com/bluesky-social/indigo/atproto/syntax" ) type BskyPost struct { @@ -14,66 +12,41 @@ CreatedAt time.Time Langs []string Tags []string - Embed *apibsky.FeedDefs_PostView_Embed - Facets []*apibsky.RichtextFacet - Labels *apibsky.FeedPost_Labels - Reply *apibsky.FeedPost_ReplyRef + Embed *PostEmbed + Facets json.RawMessage LikeCount int64 ReplyCount int64 RepostCount int64 QuoteCount int64 } -func NewBskyPostFromView(postView *apibsky.FeedDefs_PostView) (*BskyPost, error) { - atUri, err := syntax.ParseATURI(postView.Uri) - if err != nil { - return nil, err - } +type PostEmbed struct { + Images []PostImage `json:"images,omitempty"` + External *PostExternal `json:"external,omitempty"` + Video *PostVideo `json:"video,omitempty"` +} - // decode the record to get FeedPost - feedPost, ok := postView.Record.Val.(*apibsky.FeedPost) - if !ok { - return nil, err - } +type AspectRatio struct { + Width int64 `json:"width"` + Height int64 `json:"height"` +} - createdAt, err := time.Parse(time.RFC3339, feedPost.CreatedAt) - if err != nil { - return nil, err - } +type PostImage struct { + Fullsize string `json:"fullsize"` + Thumb string `json:"thumb"` + Alt string `json:"alt"` + AspectRatio *AspectRatio `json:"aspectRatio,omitempty"` +} - var likeCount, replyCount, repostCount, quoteCount int64 - if postView.LikeCount != nil { - likeCount = *postView.LikeCount - } - if postView.ReplyCount != nil { - replyCount = *postView.ReplyCount - } - if postView.RepostCount != nil { - repostCount = *postView.RepostCount - } - if postView.QuoteCount != nil { - quoteCount = *postView.QuoteCount - } +type PostExternal struct { + Uri string `json:"uri"` + Title string `json:"title"` + Description string `json:"description"` + Thumb string `json:"thumb"` +} - post := &BskyPost{ - Rkey: atUri.RecordKey().String(), - Text: feedPost.Text, - CreatedAt: createdAt, - Langs: feedPost.Langs, - Tags: feedPost.Tags, - Embed: postView.Embed, - Facets: feedPost.Facets, - Labels: feedPost.Labels, - Reply: feedPost.Reply, - LikeCount: likeCount, - ReplyCount: replyCount, - RepostCount: repostCount, - QuoteCount: quoteCount, - } - - if author := postView.Author; author != nil { - post.AuthorDid = author.Did - } - - return post, nil +type PostVideo struct { + Playlist string `json:"playlist"` + Thumbnail string `json:"thumbnail"` + Alt string `json:"alt"` } -- tangled.sh