···
"github.com/bluesky-social/indigo/api/bsky"
11
+
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/bluesky-social/indigo/xrpc"
"github.com/bluesky-social/jetstream/pkg/client"
"github.com/bluesky-social/jetstream/pkg/models"
···
const ListenTypeFollows = "follows"
type SubscriberData struct {
28
-
ListenTo Set[string]
29
+
ListenTo Set[syntax.DID]
30
+
follows map[syntax.RecordKey]bsky.GraphFollow
33
+
type ListeneeData struct {
34
+
targets *hashmap.Map[syntax.DID, *SubscriberData]
35
+
likes map[syntax.RecordKey]bsky.FeedLike
type NotificationMessage struct {
33
-
Liked bool `json:"liked"`
34
-
ByDid string `json:"did"`
35
-
RepostURI string `json:"repost_uri"`
39
+
Liked bool `json:"liked"`
40
+
ByDid syntax.DID `json:"did"`
41
+
RepostURI syntax.ATURI `json:"repost_uri"`
type SubscriberMessage struct {
···
type SubscriberUpdateListenTo struct {
44
-
ListenTo []string `json:"listen_to"`
50
+
ListenTo []syntax.DID `json:"listen_to"`
// storing the subscriber data in both Should Be Fine
// we dont modify subscriber data at the same time in two places
50
-
subscribers = hashmap.New[string, *SubscriberData]()
51
-
listeningTo = hashmap.New[string, *hashmap.Map[string, *SubscriberData]]()
56
+
subscribers = hashmap.New[syntax.DID, *SubscriberData]()
57
+
listeningTo = hashmap.New[syntax.DID, *ListeneeData]()
likeStream *client.Client
subscriberStream *client.Client
···
func getSubscriberDids() []string {
dids := make([]string, 0, subscribers.Len())
67
-
subscribers.Range(func(s string, sd *SubscriberData) bool {
68
-
dids = append(dids, s)
73
+
subscribers.Range(func(s syntax.DID, sd *SubscriberData) bool {
74
+
dids = append(dids, string(s))
74
-
func listenTo(sd *SubscriberData, did string) {
75
-
targetDids, _ := listeningTo.GetOrInsert(did, hashmap.New[string, *SubscriberData]())
76
-
targetDids.Insert(sd.DID, sd)
80
+
func startListeningTo(sd *SubscriberData, did syntax.DID) {
81
+
ld, _ := listeningTo.GetOrInsert(did, &ListeneeData{
82
+
targets: hashmap.New[syntax.DID, *SubscriberData](),
83
+
likes: make(map[syntax.RecordKey]bsky.FeedLike),
85
+
ld.targets.Insert(sd.DID, sd)
79
-
func stopListeningTo(subscriberDid, did string) {
80
-
if targetDids, exists := listeningTo.Get(did); exists {
81
-
targetDids.Del(subscriberDid)
88
+
func stopListeningTo(subscriberDid, did syntax.DID) {
89
+
if ld, exists := listeningTo.Get(did); exists {
90
+
ld.targets.Del(subscriberDid)
···
func handleSubscribe(w http.ResponseWriter, r *http.Request) {
111
+
did, err := syntax.ParseDID(vars["did"])
113
+
http.Error(w, "not a valid did", http.StatusBadRequest)
105
-
// "follows", everything else is considered as "none"
listenType := query.Get("listenTo")
if len(listenType) == 0 {
listenType = ListenTypeFollows
111
-
logger = logger.With("did", did)
123
+
logger := logger.With("did", did)
conn, err := upgrader.Upgrade(w, r, nil)
···
133
-
var subbedTo Set[string]
145
+
sd := &SubscriberData{
148
+
ListenType: listenType,
···
logger.Info("fetched follows")
159
+
sd.follows = follows
160
+
sd.ListenTo = make(Set[syntax.DID])
161
+
for _, follow := range follows {
162
+
sd.ListenTo[syntax.DID(follow.Subject)] = struct{}{}
145
-
subbedTo = make(Set[string])
165
+
sd.ListenTo = make(Set[syntax.DID])
147
-
logger.Error("invalid listen type", "requestedType", listenType)
151
-
reposts, err := fetchReposts(r.Context(), xrpcClient, did)
153
-
logger.Error("error fetching reposts", "error", err)
167
+
http.Error(w, "invalid listen type", http.StatusBadRequest)
156
-
logger.Info("fetched reposts")
158
-
sd := &SubscriberData{
161
-
ListenType: listenType,
162
-
ListenTo: subbedTo,
subscribers.Set(sd.DID, sd)
for listenDid := range sd.ListenTo {
168
-
listenTo(sd, listenDid)
173
+
startListeningTo(sd, listenDid)
updateSubscriberStreamOpts()
172
-
updateLikeStreamOpts()
// delete subscriber after we are done
for listenDid := range sd.ListenTo {
stopListeningTo(sd.DID, listenDid)
updateSubscriberStreamOpts()
181
-
updateLikeStreamOpts()
logger.Info("serving subscriber")
···
for _, listenDid := range innerMsg.ListenTo {
sd.ListenTo[listenDid] = struct{}{}
212
-
listenTo(sd, listenDid)
213
+
startListeningTo(sd, listenDid)
···
func getLikeStreamOpts() models.SubscriberOptionsUpdatePayload {
return models.SubscriberOptionsUpdatePayload{
WantedCollections: []string{"app.bsky.feed.like"},
221
-
// WantedDIDs: getFollowsDids(),
···
232
-
func updateLikeStreamOpts() {
233
-
opts := getLikeStreamOpts()
234
-
err := likeStream.SendOptionsUpdate(opts)
236
-
logger.Error("couldnt update like stream opts", "error", err)
239
-
logger.Info("updated like stream opts", "requestedDids", len(opts.WantedDIDs))
func updateSubscriberStreamOpts() {
opts := getSubscriberStreamOpts()
err := subscriberStream.SendOptionsUpdate(opts)
···
func HandleLikeEvent(ctx context.Context, event *models.Event) error {
253
-
if event == nil || event.Commit == nil || len(event.Commit.Record) == 0 {
243
+
if event == nil || event.Commit == nil {
247
+
byDid := syntax.DID(event.Did)
// skip handling event if its not from a source we are listening to
258
-
targets, exists := listeningTo.Get(event.Did)
249
+
ld, exists := listeningTo.Get(byDid)
254
+
deleted := event.Commit.Operation == models.CommitOperationDelete
255
+
rkey := syntax.RecordKey(event.Commit.RKey)
264
-
if err := json.Unmarshal(event.Commit.Record, &like); err != nil {
265
-
logger.Error("failed to unmarshal like", "error", err)
259
+
if l, exists := ld.likes[rkey]; exists {
261
+
defer delete(ld.likes, rkey)
263
+
logger.Error("like record not found", "rkey", rkey)
267
+
if err := json.Unmarshal(event.Commit.Record, &like); err != nil {
268
+
logger.Error("failed to unmarshal like", "error", err)
273
+
// if there is no via it means its not a repost anyway
274
+
if like.Via == nil {
269
-
targets.Range(func(s string, sd *SubscriberData) bool {
270
-
for repostURI, _ := range sd.Reposts {
271
-
// (un)liked a post the subscriber reposted
272
-
if like.Subject.Uri == repostURI {
273
-
notification := NotificationMessage{
274
-
Liked: event.Commit.Operation != models.CommitOperationDelete,
276
-
RepostURI: repostURI,
278
+
// store for later when it gets deleted so we can fetch the record
280
+
ld.likes[rkey] = like
279
-
if err := sd.Conn.WriteJSON(notification); err != nil {
280
-
logger.Error("failed to send notification", "subscriber", sd.DID, "error", err)
283
+
repostURI := syntax.ATURI(like.Via.Uri)
284
+
// if not a repost we dont care
285
+
if repostURI.Collection() != "app.bsky.feed.repost" {
288
+
reposterDID, err := repostURI.Authority().AsDID()
292
+
if sd, exists := ld.targets.Get(reposterDID); exists {
293
+
notification := NotificationMessage{
296
+
RepostURI: repostURI,
299
+
if err := sd.Conn.WriteJSON(notification); err != nil {
300
+
logger.Error("failed to send notification", "subscriber", sd.DID, "error", err)
···
312
+
byDid := syntax.DID(event.Did)
313
+
sd, exists := subscribers.Get(byDid)
318
+
deleted := event.Commit.Operation == models.CommitOperationDelete
319
+
rkey := syntax.RecordKey(event.Commit.RKey)
switch event.Commit.Collection {
296
-
case "app.bsky.feed.repost":
297
-
modifySubscribersWithEvent(
299
-
func(s *SubscriberData, r bsky.FeedRepost, deleted bool) {
301
-
delete(s.Reposts, r.Subject.Uri)
303
-
s.Reposts[r.Subject.Uri] = struct{}{}
case "app.bsky.graph.follow":
308
-
modifySubscribersWithEvent(
310
-
func(s *SubscriberData, r bsky.GraphFollow, deleted bool) {
311
-
// if we arent managing then we dont need to update anything
312
-
if s.ListenType != ListenTypeFollows {
316
-
stopListeningTo(s.DID, r.Subject)
317
-
delete(s.ListenTo, r.Subject)
319
-
s.ListenTo[r.Subject] = struct{}{}
320
-
listenTo(s, r.Subject)
323
+
// if we arent managing then we dont need to update anything
324
+
if sd.ListenType != ListenTypeFollows {
327
+
var r bsky.GraphFollow
329
+
if f, exists := sd.follows[rkey]; exists {
332
+
logger.Error("follow record not found", "rkey", rkey)
335
+
subjectDid := syntax.DID(r.Subject)
336
+
stopListeningTo(sd.DID, subjectDid)
337
+
delete(sd.ListenTo, subjectDid)
338
+
delete(sd.follows, rkey)
340
+
if err := unmarshalEvent(event, &r); err != nil {
343
+
subjectDid := syntax.DID(r.Subject)
344
+
sd.ListenTo[subjectDid] = struct{}{}
345
+
sd.follows[rkey] = r
346
+
startListeningTo(sd, subjectDid)
329
-
type ModifyFunc[v any] func(*SubscriberData, v, bool)
331
-
func modifySubscribersWithEvent[v any](event *models.Event, handle ModifyFunc[v]) error {
332
-
if len(event.Commit.Record) == 0 {
353
+
func unmarshalEvent[v any](event *models.Event, val *v) error {
354
+
if err := json.Unmarshal(event.Commit.Record, val); err != nil {
355
+
logger.Error("failed to unmarshal", "error", err, "raw", event.Commit.Record)
337
-
if err := json.Unmarshal(event.Commit.Record, &data); err != nil {
338
-
logger.Error("failed to unmarshal repost", "error", err, "raw", event.Commit.Record)
342
-
if subscriber, exists := subscribers.Get(event.Did); exists {
343
-
handle(subscriber, data, event.Commit.Operation == models.CommitOperationDelete)