A demo of a Bluesky feed generator in Go

Compare changes

Choose any two refs to compare.

Changed files
+61 -8
cmd
register-feed
+1
.env-example
··· 5 FEED_DISPLAY_NAME="My demo feed" 6 FEED_DESCRIPTION="This is a demo feed" 7 FEED_DID="did:web:demo-feed.com"
··· 5 FEED_DISPLAY_NAME="My demo feed" 6 FEED_DESCRIPTION="This is a demo feed" 7 FEED_DID="did:web:demo-feed.com" 8 + ACCEPTS_INTERACTIONS="true"
+14 -8
cmd/register-feed/main.go
··· 33 } 34 35 type registerRecord struct { 36 - Did string `json:"did"` 37 - DisplayName string `json:"displayName"` 38 - Description string `json:"description"` 39 - CreatedAt time.Time `json:"createdAt"` 40 } 41 42 func main() { ··· 136 if feedDID == "" { 137 return fmt.Errorf("FEED_DID environment not set") 138 } 139 140 reqData := registerFeedGen{ 141 Repo: auth.Did, 142 Collection: "app.bsky.feed.generator", 143 Rkey: feedName, 144 Record: registerRecord{ 145 - Did: feedDID, 146 - DisplayName: feedDisplayName, 147 - Description: feedDescription, 148 - CreatedAt: time.Now(), 149 }, 150 } 151
··· 33 } 34 35 type registerRecord struct { 36 + Did string `json:"did"` 37 + DisplayName string `json:"displayName"` 38 + Description string `json:"description"` 39 + CreatedAt time.Time `json:"createdAt"` 40 + AcceptsInteractions bool `json:"acceptsInteractions"` 41 } 42 43 func main() { ··· 137 if feedDID == "" { 138 return fmt.Errorf("FEED_DID environment not set") 139 } 140 + acceptsInteractions := false 141 + if os.Getenv("ACCEPTS_INTERACTIONS") == "true" { 142 + acceptsInteractions = true 143 + } 144 145 reqData := registerFeedGen{ 146 Repo: auth.Did, 147 Collection: "app.bsky.feed.generator", 148 Rkey: feedName, 149 Record: registerRecord{ 150 + Did: feedDID, 151 + DisplayName: feedDisplayName, 152 + Description: feedDescription, 153 + CreatedAt: time.Now(), 154 + AcceptsInteractions: acceptsInteractions, 155 }, 156 } 157
+44
handlers.go
··· 4 "context" 5 "encoding/json" 6 "fmt" 7 "log/slog" 8 "net/http" 9 "net/url" ··· 112 _, _ = w.Write(b) 113 } 114 115 // WellKnownResponse is what's returned on a well-known endpoint 116 type WellKnownResponse struct { 117 Context []string `json:"@context"`
··· 4 "context" 5 "encoding/json" 6 "fmt" 7 + "io" 8 "log/slog" 9 "net/http" 10 "net/url" ··· 113 _, _ = w.Write(b) 114 } 115 116 + // FeedInteractions details the interactions that a user had with a feed when they viewed it 117 + type FeedInteractions struct { 118 + Interactions []Interaction `json:"interactions"` 119 + } 120 + 121 + type Interaction struct { 122 + Item string `json:"item"` 123 + Event string `json:"event"` 124 + } 125 + 126 + // HandleFeedInteractions will handle when the client sends back a feed interaction so you can improve 127 + // the feed quality for the user 128 + func (s *Server) HandleFeedInteractions(w http.ResponseWriter, r *http.Request) { 129 + slog.Debug("handle feed interactions") 130 + userDID, err := getRequestUserDID(r) 131 + if err != nil { 132 + slog.Error("validate user auth", "error", err) 133 + http.Error(w, "validate auth", http.StatusUnauthorized) 134 + return 135 + } 136 + 137 + body, err := io.ReadAll(r.Body) 138 + if err != nil { 139 + slog.Error("read feed interactions request body", "error", err) 140 + http.Error(w, "read body", http.StatusBadRequest) 141 + return 142 + } 143 + 144 + var feedInteractions FeedInteractions 145 + err = json.Unmarshal(body, &feedInteractions) 146 + if err != nil { 147 + slog.Error("decode feed interactions request body", "error", err) 148 + http.Error(w, "decode body", http.StatusBadRequest) 149 + return 150 + } 151 + 152 + // here is where you would likely do something with the data that is sent to you such as improving the 153 + // data you store for a users feed 154 + for _, interaction := range feedInteractions.Interactions { 155 + slog.Info("interaction for user", "user", userDID, "item", interaction.Item, "interaction", interaction.Event) 156 + } 157 + } 158 + 159 // WellKnownResponse is what's returned on a well-known endpoint 160 type WellKnownResponse struct { 161 Context []string `json:"@context"`
+1
readme.md
··· 25 * FEED_DISPLAY_NAME - This is the name you will give your feed that users will be able to see 26 * FEED_DESCRIPTION - This is a description of your feed that users will be able to see 27 * FEED_DID - This is the DID that will be used to register the record. Unless you know what you are doing it's best to use `did:web:` + FEED_HOST_NAME (eg "did:web:demo-feed.com") 28 29 First you need to run the feed generator by building the application `go build -o demo-feed-generator ./cmd/feed-generator/main.go` and then running it `./demo-feed-generator` 30
··· 25 * FEED_DISPLAY_NAME - This is the name you will give your feed that users will be able to see 26 * FEED_DESCRIPTION - This is a description of your feed that users will be able to see 27 * FEED_DID - This is the DID that will be used to register the record. Unless you know what you are doing it's best to use `did:web:` + FEED_HOST_NAME (eg "did:web:demo-feed.com") 28 + * ACCEPTS_INTERACTIONS - Set this to be true if you wish your feed to accepts interactions such as "show more" or "show less" 29 30 First you need to run the feed generator by building the application `go build -o demo-feed-generator ./cmd/feed-generator/main.go` and then running it `./demo-feed-generator` 31
+1
server.go
··· 42 mux := http.NewServeMux() 43 mux.HandleFunc("/xrpc/app.bsky.feed.getFeedSkeleton", srv.HandleGetFeedSkeleton) 44 mux.HandleFunc("/xrpc/app.bsky.feed.describeFeedGenerator", srv.HandleDescribeFeedGenerator) 45 mux.HandleFunc("/.well-known/did.json", srv.HandleWellKnown) 46 addr := fmt.Sprintf("0.0.0.0:%d", port) 47
··· 42 mux := http.NewServeMux() 43 mux.HandleFunc("/xrpc/app.bsky.feed.getFeedSkeleton", srv.HandleGetFeedSkeleton) 44 mux.HandleFunc("/xrpc/app.bsky.feed.describeFeedGenerator", srv.HandleDescribeFeedGenerator) 45 + mux.HandleFunc("POST /xrpc/app.bsky.feed.sendInteractions", srv.HandleFeedInteractions) 46 mux.HandleFunc("/.well-known/did.json", srv.HandleWellKnown) 47 addr := fmt.Sprintf("0.0.0.0:%d", port) 48