this repo has no description
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}