its for when you want to get like notifications for your reposts

feat: let users specify if they want to handle specifying what dids to listen to

ptr.pet cde45e87 bbcf2989

verified
Changed files
+95 -44
+95 -44
main.go
···
type Set[T comparable] map[T]struct{}
+
const ListenTypeNone = "none"
+
const ListenTypeFollows = "follows"
+
type SubscriberData struct {
-
DID string
-
Conn *websocket.Conn
-
ListenTo Set[string]
-
Reposts Set[string]
+
DID string
+
Conn *websocket.Conn
+
ListenType string
+
ListenTo Set[string]
+
Reposts Set[string]
}
type NotificationMessage struct {
···
RepostURI string `json:"repost_uri"`
}
+
type SubscriberMessage struct {
+
Type string `json:"type"`
+
Content json.RawMessage `json:"content"`
+
}
+
+
type SubscriberUpdateListenTo struct {
+
ListenTo []string `json:"listen_to"`
+
}
+
var (
// storing the subscriber data in both Should Be Fine
// we dont modify subscriber data at the same time in two places
···
func main() {
logger = slog.Default()
-
go likeStreamLoop(logger)
-
go subscriberStreamLoop(logger)
+
go startJetstreamLoop(logger, &likeStream, "like_tracker", HandleLikeEvent, getLikeStreamOpts)
+
go startJetstreamLoop(logger, &subscriberStream, "subscriber", HandleSubscriberEvent, getSubscriberStreamOpts)
r := mux.NewRouter()
r.HandleFunc("/subscribe/{did}", handleSubscribe).Methods("GET")
-
log.Println("Server starting on :8080")
+
log.Println("server starting on :8080")
if err := http.ListenAndServe(":8080", r); err != nil {
log.Fatalf("error while serving: %s", err)
}
···
vars := mux.Vars(r)
did := vars["did"]
+
query := r.URL.Query()
+
// "follows", everything else is considered as "none"
+
listenType := query.Get("listenTo")
+
if len(listenType) == 0 {
+
listenType = ListenTypeFollows
+
}
+
logger = logger.With("did", did)
conn, err := upgrader.Upgrade(w, r, nil)
···
xrpcClient := &xrpc.Client{
Host: pdsURI,
}
-
// todo: implement skipping fetching follows and allow specifying users to listen to via websocket
-
follows, err := fetchFollows(r.Context(), xrpcClient, did)
-
if err != nil {
-
logger.Error("error fetching follows", "error", err)
+
+
var subbedTo Set[string]
+
+
switch listenType {
+
case ListenTypeFollows:
+
follows, err := fetchFollows(r.Context(), xrpcClient, did)
+
if err != nil {
+
logger.Error("error fetching follows", "error", err)
+
return
+
}
+
logger.Info("fetched follows")
+
subbedTo = follows
+
case ListenTypeNone:
+
subbedTo = make(Set[string])
+
default:
+
logger.Error("invalid listen type", "requestedType", listenType)
return
}
-
logger.Info("fetched follows")
+
reposts, err := fetchReposts(r.Context(), xrpcClient, did)
if err != nil {
logger.Error("error fetching reposts", "error", err)
···
logger.Info("fetched reposts")
sd := &SubscriberData{
-
DID: did,
-
Conn: conn,
-
// use user follows as default listen to
-
ListenTo: follows,
-
Reposts: reposts,
+
DID: did,
+
Conn: conn,
+
ListenType: listenType,
+
ListenTo: subbedTo,
+
Reposts: reposts,
}
subscribers.Set(sd.DID, sd)
···
logger.Info("serving subscriber")
for {
-
_, _, err := conn.ReadMessage()
+
var msg SubscriberMessage
+
err := conn.ReadJSON(&msg)
if err != nil {
logger.Info("WebSocket connection closed", "error", err)
break
}
+
switch msg.Type {
+
case "update_listen_to":
+
// only allow this if we arent managing listen to
+
if sd.ListenType != ListenTypeNone {
+
continue
+
}
+
+
var innerMsg SubscriberUpdateListenTo
+
if err := json.Unmarshal(msg.Content, &innerMsg); err != nil {
+
logger.Info("invalid message", "error", err)
+
break
+
}
+
// remove all current listens and add the ones the user requested
+
for listenDid := range sd.ListenTo {
+
stopListeningTo(sd.DID, listenDid)
+
delete(sd.ListenTo, listenDid)
+
}
+
for _, listenDid := range innerMsg.ListenTo {
+
sd.ListenTo[listenDid] = struct{}{}
+
listenTo(sd, listenDid)
+
}
+
}
}
}
···
return
}
logger.Info("updated subscriber stream opts", "userCount", len(opts.WantedDIDs))
-
}
-
-
func likeStreamLoop(logger *slog.Logger) {
-
startJetstreamLoop(logger, &likeStream, "like_tracker", HandleLikeEvent, getLikeStreamOpts)
-
}
-
-
func subscriberStreamLoop(logger *slog.Logger) {
-
startJetstreamLoop(logger, &subscriberStream, "subscriber", HandleSubscriberEvent, getSubscriberStreamOpts)
}
func HandleLikeEvent(ctx context.Context, event *models.Event) error {
···
case "app.bsky.feed.repost":
modifySubscribersWithEvent(
event,
-
func(s *SubscriberData, r bsky.FeedRepost) { delete(s.Reposts, r.Subject.Uri) },
-
func(s *SubscriberData, r bsky.FeedRepost) {
-
s.Reposts[r.Subject.Uri] = struct{}{}
+
func(s *SubscriberData, r bsky.FeedRepost, deleted bool) {
+
if deleted {
+
delete(s.Reposts, r.Subject.Uri)
+
} else {
+
s.Reposts[r.Subject.Uri] = struct{}{}
+
}
},
)
case "app.bsky.graph.follow":
modifySubscribersWithEvent(
event,
-
func(s *SubscriberData, r bsky.GraphFollow) {
-
delete(s.ListenTo, r.Subject)
-
stopListeningTo(s.DID, r.Subject)
-
},
-
func(s *SubscriberData, r bsky.GraphFollow) {
-
s.ListenTo[r.Subject] = struct{}{}
-
listenTo(s, r.Subject)
+
func(s *SubscriberData, r bsky.GraphFollow, deleted bool) {
+
// if we arent managing then we dont need to update anything
+
if s.ListenType != ListenTypeFollows {
+
return
+
}
+
if deleted {
+
stopListeningTo(s.DID, r.Subject)
+
delete(s.ListenTo, r.Subject)
+
} else {
+
s.ListenTo[r.Subject] = struct{}{}
+
listenTo(s, r.Subject)
+
}
},
)
}
···
return nil
}
-
type ModifyFunc[v any] func(*SubscriberData, v)
+
type ModifyFunc[v any] func(*SubscriberData, v, bool)
-
func modifySubscribersWithEvent[v any](event *models.Event, onDelete ModifyFunc[v], onUpdate ModifyFunc[v]) error {
+
func modifySubscribersWithEvent[v any](event *models.Event, handle ModifyFunc[v]) error {
if len(event.Commit.Record) == 0 {
return nil
}
var data v
if err := json.Unmarshal(event.Commit.Record, &data); err != nil {
-
logger.Error("Failed to unmarshal repost", "error", err, "raw", event.Commit.Record)
+
logger.Error("failed to unmarshal repost", "error", err, "raw", event.Commit.Record)
return nil
}
if subscriber, exists := subscribers.Get(event.Did); exists {
-
if event.Commit.Operation == models.CommitOperationDelete {
-
onDelete(subscriber, data)
-
} else {
-
onUpdate(subscriber, data)
-
}
+
handle(subscriber, data, event.Commit.Operation == models.CommitOperationDelete)
}
return nil