1package photocopy
2
3import (
4 "bytes"
5 "context"
6 "fmt"
7 "strings"
8 "time"
9
10 "github.com/araddon/dateparse"
11 "github.com/bluesky-social/indigo/api/bsky"
12 "github.com/bluesky-social/indigo/atproto/syntax"
13 "github.com/haileyok/photocopy/models"
14)
15
16func (p *Photocopy) handleCreate(ctx context.Context, recb []byte, indexedAt, rev, did, collection, rkey, cid string) error {
17
18 iat, err := dateparse.ParseAny(indexedAt)
19 if err != nil {
20 return err
21 }
22
23 switch collection {
24 case "app.bsky.feed.post":
25 return p.handleCreatePost(ctx, rev, recb, uriFromParts(did, collection, rkey), did, collection, rkey, cid, iat)
26 case "app.bsky.graph.follow":
27 return p.handleCreateFollow(ctx, recb, uriFromParts(did, collection, rkey), did, rkey, iat)
28 case "app.bsky.feed.like", "app.bsky.feed.repost":
29 return p.handleCreateInteraction(ctx, recb, uriFromParts(did, collection, rkey), did, collection, rkey, iat)
30 default:
31 return nil
32 }
33}
34
35func (p *Photocopy) handleCreatePost(ctx context.Context, rev string, recb []byte, uri, did, collection, rkey, cid string, indexedAt time.Time) error {
36 var rec bsky.FeedPost
37 if err := rec.UnmarshalCBOR(bytes.NewReader(recb)); err != nil {
38 return err
39 }
40
41 cat, err := parseTimeFromRecord(rec, rkey)
42 if err != nil {
43 return err
44 }
45
46 post := models.Post{
47 Uri: uri,
48 Rkey: rkey,
49 CreatedAt: *cat,
50 IndexedAt: indexedAt,
51 Did: did,
52 }
53
54 if rec.Reply != nil {
55 if rec.Reply.Parent != nil {
56 aturi, err := syntax.ParseATURI(rec.Reply.Parent.Uri)
57 if err != nil {
58 return fmt.Errorf("error parsing at-uri: %w", err)
59
60 }
61 post.ParentDid = aturi.Authority().String()
62 post.ParentUri = rec.Reply.Parent.Uri
63 }
64 if rec.Reply.Root != nil {
65 aturi, err := syntax.ParseATURI(rec.Reply.Root.Uri)
66 if err != nil {
67 return fmt.Errorf("error parsing at-uri: %w", err)
68
69 }
70 post.RootDid = aturi.Authority().String()
71 post.RootUri = rec.Reply.Root.Uri
72 }
73 }
74
75 if rec.Embed != nil && rec.Embed.EmbedRecord != nil && rec.Embed.EmbedRecord.Record != nil {
76 aturi, err := syntax.ParseATURI(rec.Embed.EmbedRecord.Record.Uri)
77 if err != nil {
78 return fmt.Errorf("error parsing at-uri: %w", err)
79
80 }
81 post.QuoteDid = aturi.Authority().String()
82 post.QuoteUri = rec.Embed.EmbedRecord.Record.Uri
83 } else if rec.Embed != nil && rec.Embed.EmbedRecordWithMedia != nil && rec.Embed.EmbedRecordWithMedia.Record != nil && rec.Embed.EmbedRecordWithMedia.Record.Record != nil {
84 aturi, err := syntax.ParseATURI(rec.Embed.EmbedRecordWithMedia.Record.Record.Uri)
85 if err != nil {
86 return fmt.Errorf("error parsing at-uri: %w", err)
87
88 }
89 post.QuoteDid = aturi.Authority().String()
90 post.QuoteUri = rec.Embed.EmbedRecordWithMedia.Record.Record.Uri
91 }
92
93 if err := p.inserters.postsInserter.Insert(ctx, post); err != nil {
94 return err
95 }
96
97 return nil
98}
99
100func (p *Photocopy) handleCreateFollow(ctx context.Context, recb []byte, uri, did, rkey string, indexedAt time.Time) error {
101 var rec bsky.GraphFollow
102 if err := rec.UnmarshalCBOR(bytes.NewReader(recb)); err != nil {
103 return err
104 }
105
106 cat, err := parseTimeFromRecord(rec, rkey)
107 if err != nil {
108 return err
109 }
110
111 follow := models.Follow{
112 Uri: uri,
113 Did: did,
114 Rkey: rkey,
115 CreatedAt: *cat,
116 IndexedAt: indexedAt,
117 Subject: rec.Subject,
118 }
119
120 if err := p.inserters.followsInserter.Insert(ctx, follow); err != nil {
121 return err
122 }
123
124 return nil
125}
126
127func (p *Photocopy) handleCreateInteraction(ctx context.Context, recb []byte, uri, did, collection, rkey string, indexedAt time.Time) error {
128 colPts := strings.Split(collection, ".")
129 if len(colPts) < 4 {
130 return fmt.Errorf("invalid collection type %s", collection)
131 }
132
133 interaction := models.Interaction{
134 Uri: uri,
135 Kind: colPts[3],
136 Rkey: rkey,
137 IndexedAt: indexedAt,
138 Did: did,
139 SubjectUri: uri,
140 SubjectDid: did,
141 }
142
143 switch collection {
144 case "app.bsky.feed.like":
145 var rec bsky.FeedLike
146 if err := rec.UnmarshalCBOR(bytes.NewReader(recb)); err != nil {
147 return err
148 }
149
150 cat, err := parseTimeFromRecord(rec, rkey)
151 if err != nil {
152 return err
153 }
154
155 if rec.Subject == nil {
156 return fmt.Errorf("invalid subject in like")
157 }
158
159 aturi, err := syntax.ParseATURI(rec.Subject.Uri)
160 if err != nil {
161 return fmt.Errorf("error parsing at-uri: %w", err)
162
163 }
164
165 interaction.SubjectDid = aturi.Authority().String()
166 interaction.SubjectUri = rec.Subject.Uri
167 interaction.CreatedAt = *cat
168 case "app.bsky.feed.repost":
169 var rec bsky.FeedRepost
170 if err := rec.UnmarshalCBOR(bytes.NewReader(recb)); err != nil {
171 return err
172 }
173
174 cat, err := parseTimeFromRecord(rec, rkey)
175 if err != nil {
176 return err
177 }
178
179 if rec.Subject == nil {
180 return fmt.Errorf("invalid subject in repost")
181 }
182
183 aturi, err := syntax.ParseATURI(rec.Subject.Uri)
184 if err != nil {
185 return fmt.Errorf("error parsing at-uri: %w", err)
186
187 }
188
189 interaction.SubjectDid = aturi.Authority().String()
190 interaction.SubjectUri = rec.Subject.Uri
191 interaction.CreatedAt = *cat
192 }
193
194 if err := p.inserters.interactionsInserter.Insert(ctx, interaction); err != nil {
195 return err
196 }
197
198 return nil
199}
200
201func parseTimeFromRecord(rec any, rkey string) (*time.Time, error) {
202 var rkeyTime time.Time
203 if rkey != "self" {
204 rt, err := syntax.ParseTID(rkey)
205 if err == nil {
206 rkeyTime = rt.Time()
207 }
208 }
209 switch rec := rec.(type) {
210 case *bsky.FeedPost:
211 t, err := dateparse.ParseAny(rec.CreatedAt)
212 if err != nil {
213 return nil, err
214 }
215
216 if inRange(t) {
217 return &t, nil
218 }
219
220 if rkeyTime.IsZero() || !inRange(rkeyTime) {
221 return timePtr(time.Now()), nil
222 }
223
224 return &rkeyTime, nil
225 case *bsky.FeedRepost:
226 t, err := dateparse.ParseAny(rec.CreatedAt)
227 if err != nil {
228 return nil, err
229 }
230
231 if inRange(t) {
232 return timePtr(t), nil
233 }
234
235 if rkeyTime.IsZero() {
236 return nil, fmt.Errorf("failed to get a useful timestamp from record")
237 }
238
239 return &rkeyTime, nil
240 case *bsky.FeedLike:
241 t, err := dateparse.ParseAny(rec.CreatedAt)
242 if err != nil {
243 return nil, err
244 }
245
246 if inRange(t) {
247 return timePtr(t), nil
248 }
249
250 if rkeyTime.IsZero() {
251 return nil, fmt.Errorf("failed to get a useful timestamp from record")
252 }
253
254 return &rkeyTime, nil
255 case *bsky.ActorProfile:
256 // We can't really trust the createdat in the profile record anyway, and its very possible its missing. just use iat for this one
257 return timePtr(time.Now()), nil
258 case *bsky.FeedGenerator:
259 if !rkeyTime.IsZero() && inRange(rkeyTime) {
260 return &rkeyTime, nil
261 }
262 return timePtr(time.Now()), nil
263 default:
264 if !rkeyTime.IsZero() && inRange(rkeyTime) {
265 return &rkeyTime, nil
266 }
267 return timePtr(time.Now()), nil
268 }
269}
270
271func inRange(t time.Time) bool {
272 now := time.Now()
273 if t.Before(now) {
274 return now.Sub(t) <= time.Hour*24*365*5
275 }
276 return t.Sub(now) <= time.Hour*24*200
277}
278
279func timePtr(t time.Time) *time.Time {
280 return &t
281}