A collection of Custom Bluesky Feeds, including Fresh Feeds, all under one roof
1// Package feedrouter describes the FeedRouter type, which is responsible for generating feeds for a given DID.
2// It also describes the Feed interface, which is implemented by the various feed types.
3package feedrouter
4
5import (
6 "context"
7 "fmt"
8
9 appbsky "github.com/bluesky-social/indigo/api/bsky"
10 did "github.com/whyrusleeping/go-did"
11)
12
13type Feed interface {
14 GetPage(ctx context.Context, feed string, userDID string, limit int64, cursor string) (feedPosts []*appbsky.FeedDefs_SkeletonFeedPost, newCursor *string, err error)
15 Describe(ctx context.Context) ([]appbsky.FeedDescribeFeedGenerator_Feed, error)
16}
17
18type FeedRouter struct {
19 FeedActorDID did.DID // DID of the Repo the Feed is published under
20 ServiceEndpoint string // URL of the FeedRouter service
21 ServiceDID did.DID // DID of the FeedRouter service
22 DIDDocument did.Document // DID Document of the FeedRouter service
23 AcceptableURIPrefixes []string // URIs that the FeedRouter is allowed to generate feeds for
24 FeedMap map[string]Feed // map of FeedName to Feed
25 Feeds []Feed
26}
27
28type NotFoundError struct {
29 error
30}
31
32// NewFeedRouter returns a new FeedRouter
33func NewFeedRouter(
34 ctx context.Context,
35 feedActorDIDString string,
36 serviceDIDString string,
37 acceptableDIDs []string,
38 serviceEndpoint string,
39) (*FeedRouter, error) {
40 acceptableURIPrefixes := []string{}
41 for _, did := range acceptableDIDs {
42 acceptableURIPrefixes = append(acceptableURIPrefixes, "at://"+did+"/app.bsky.feed.generator/")
43 }
44
45 serviceDID, err := did.ParseDID(serviceDIDString)
46 if err != nil {
47 return nil, fmt.Errorf("error parsing serviceDID: %w", err)
48 }
49
50 feedActorDID, err := did.ParseDID(feedActorDIDString)
51 if err != nil {
52 return nil, fmt.Errorf("error parsing feedActorDID: %w", err)
53 }
54
55 serviceID, err := did.ParseDID("#bsky_fg")
56 if err != nil {
57 panic(err)
58 }
59
60 doc := did.Document{
61 Context: []string{did.CtxDIDv1},
62 ID: serviceDID,
63 Service: []did.Service{
64 {
65 ID: serviceID,
66 Type: "BskyFeedGenerator",
67 ServiceEndpoint: serviceEndpoint,
68 },
69 },
70 }
71
72 return &FeedRouter{
73 FeedMap: map[string]Feed{},
74 FeedActorDID: feedActorDID,
75 ServiceDID: serviceDID,
76 DIDDocument: doc,
77 AcceptableURIPrefixes: acceptableURIPrefixes,
78 ServiceEndpoint: serviceEndpoint,
79 }, nil
80}
81
82// AddFeed adds a feed to the FeedRouter
83// Feed precedence for overlapping aliases is determined by the order in which
84// they are added (first added is highest precedence)
85func (fg *FeedRouter) AddFeed(feedAliases []string, feed Feed) {
86 if fg.FeedMap == nil {
87 fg.FeedMap = map[string]Feed{}
88 }
89
90 for _, feedAlias := range feedAliases {
91 // Skip the feed if we already have the alias registered so we don't add it twice
92 // Feed precedence is determined by the order in which they are added
93 if _, ok := fg.FeedMap[feedAlias]; ok {
94 continue
95 }
96
97 fg.FeedMap[feedAlias] = feed
98 }
99
100 fg.Feeds = append(fg.Feeds, feed)
101}