This repository has no description
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}