This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / appview / bsky / client.go
4.8 kB 199 lines
1package bsky 2 3import ( 4 "context" 5 "encoding/json" 6 "log" 7 "net/http" 8 "time" 9 10 "github.com/bluesky-social/indigo/atproto/syntax" 11 "github.com/bluesky-social/indigo/xrpc" 12 "tangled.org/core/appview/models" 13 "tangled.org/core/consts" 14) 15 16type rawFeedOutput struct { 17 Cursor *string `json:"cursor,omitempty"` 18 Feed []rawFeedItem `json:"feed"` 19} 20 21type rawFeedItem struct { 22 Post rawPostView `json:"post"` 23} 24 25type rawPostView struct { 26 URI string `json:"uri"` 27 Author struct { 28 DID string `json:"did"` 29 } `json:"author"` 30 Record rawRecord `json:"record"` 31 Embed json.RawMessage `json:"embed,omitempty"` 32 LikeCount *int64 `json:"likeCount,omitempty"` 33 ReplyCount *int64 `json:"replyCount,omitempty"` 34 RepostCount *int64 `json:"repostCount,omitempty"` 35 QuoteCount *int64 `json:"quoteCount,omitempty"` 36} 37 38type rawRecord struct { 39 Text string `json:"text"` 40 CreatedAt string `json:"createdAt"` 41 Langs []string `json:"langs,omitempty"` 42 Tags []string `json:"tags,omitempty"` 43 Facets json.RawMessage `json:"facets,omitempty"` 44} 45 46type rawEmbedType struct { 47 Type string `json:"$type"` 48} 49 50func FetchPosts(ctx context.Context, c *xrpc.Client, limit int, cursor string) ([]models.BskyPost, string, error) { 51 var out rawFeedOutput 52 53 params := map[string]any{ 54 "actor": consts.TangledDid, 55 "filter": "posts_no_replies", 56 "limit": int64(limit), 57 } 58 if cursor != "" { 59 params["cursor"] = cursor 60 } 61 if err := c.Do(ctx, http.MethodGet, "", "app.bsky.feed.getAuthorFeed", params, nil, &out); err != nil { 62 return nil, "", err 63 } 64 65 var posts []models.BskyPost 66 for _, feedItem := range out.Feed { 67 raw := feedItem.Post 68 69 if len(raw.Embed) > 0 { 70 var t rawEmbedType 71 json.Unmarshal(raw.Embed, &t) 72 if t.Type == "app.bsky.embed.record#view" { 73 continue 74 } 75 } 76 77 atUri, err := syntax.ParseATURI(raw.URI) 78 if err != nil { 79 log.Println("bsky: parse AT URI:", err) 80 continue 81 } 82 83 createdAt, err := time.Parse(time.RFC3339, raw.Record.CreatedAt) 84 if err != nil { 85 log.Println("bsky: parse createdAt:", err) 86 continue 87 } 88 89 post := models.BskyPost{ 90 AuthorDid: raw.Author.DID, 91 Rkey: atUri.RecordKey().String(), 92 Text: raw.Record.Text, 93 CreatedAt: createdAt, 94 Langs: raw.Record.Langs, 95 Tags: raw.Record.Tags, 96 Facets: raw.Record.Facets, 97 } 98 99 if raw.LikeCount != nil { 100 post.LikeCount = *raw.LikeCount 101 } 102 if raw.ReplyCount != nil { 103 post.ReplyCount = *raw.ReplyCount 104 } 105 if raw.RepostCount != nil { 106 post.RepostCount = *raw.RepostCount 107 } 108 if raw.QuoteCount != nil { 109 post.QuoteCount = *raw.QuoteCount 110 } 111 112 if len(raw.Embed) > 0 { 113 embed, err := parseEmbed(raw.Embed) 114 if err != nil { 115 log.Println("bsky: parse embed:", err) 116 } 117 post.Embed = embed 118 } 119 120 posts = append(posts, post) 121 } 122 123 nextCursor := "" 124 if out.Cursor != nil { 125 nextCursor = *out.Cursor 126 } 127 128 return posts, nextCursor, nil 129} 130 131func parseEmbed(raw json.RawMessage) (*models.PostEmbed, error) { 132 var t rawEmbedType 133 if err := json.Unmarshal(raw, &t); err != nil { 134 return nil, err 135 } 136 137 switch t.Type { 138 case "app.bsky.embed.images#view": 139 var v struct { 140 Images []models.PostImage `json:"images"` 141 } 142 if err := json.Unmarshal(raw, &v); err != nil { 143 return nil, err 144 } 145 return &models.PostEmbed{Images: v.Images}, nil 146 147 case "app.bsky.embed.gallery#view": 148 // gallery items use "thumbnail" instead of "thumb" 149 var v struct { 150 Items []struct { 151 Fullsize string `json:"fullsize"` 152 Thumbnail string `json:"thumbnail"` 153 Alt string `json:"alt"` 154 AspectRatio *models.AspectRatio `json:"aspectRatio,omitempty"` 155 } `json:"items"` 156 } 157 if err := json.Unmarshal(raw, &v); err != nil { 158 return nil, err 159 } 160 embed := &models.PostEmbed{} 161 for _, item := range v.Items { 162 embed.Images = append(embed.Images, models.PostImage{ 163 Fullsize: item.Fullsize, 164 Thumb: item.Thumbnail, 165 Alt: item.Alt, 166 AspectRatio: item.AspectRatio, 167 }) 168 } 169 return embed, nil 170 171 case "app.bsky.embed.external#view": 172 var v struct { 173 External models.PostExternal `json:"external"` 174 } 175 if err := json.Unmarshal(raw, &v); err != nil { 176 return nil, err 177 } 178 return &models.PostEmbed{External: &v.External}, nil 179 180 case "app.bsky.embed.video#view": 181 var v models.PostVideo 182 if err := json.Unmarshal(raw, &v); err != nil { 183 return nil, err 184 } 185 return &models.PostEmbed{Video: &v}, nil 186 187 case "app.bsky.embed.recordWithMedia#view": 188 var v struct { 189 Media json.RawMessage `json:"media"` 190 } 191 if err := json.Unmarshal(raw, &v); err != nil { 192 return nil, err 193 } 194 return parseEmbed(v.Media) 195 196 default: 197 return nil, nil 198 } 199}