A demo of a Bluesky feed generator in Go

Readme and some more code comments. Also remove the user auth stuff as it doesn't work at the moment. Need to add that back in at a later date.

+40 -55
+2 -1
consumer.go
··· 96 96 return nil 97 97 } 98 98 99 - // look for any post that contains the #golang hashtag 99 + // this is where logic goes for what posts you wish to store for a feed but for this example 100 + // just look for any post that contains the #golang hashtag 100 101 if !strings.Contains(strings.ToLower(bskyPost.Text), "#golang") { 101 102 return nil 102 103 }
-54
handlers.go
··· 8 8 "net/http" 9 9 "net/url" 10 10 "strconv" 11 - "strings" 12 - 13 - "github.com/bluesky-social/indigo/atproto/identity" 14 - "github.com/bluesky-social/indigo/atproto/syntax" 15 - "github.com/golang-jwt/jwt/v5" 16 11 ) 17 12 18 13 const ( ··· 36 31 // HandleGetFeedSkeleton is the handler that will build up and return a feed response 37 32 func (s *Server) HandleGetFeedSkeleton(w http.ResponseWriter, r *http.Request) { 38 33 slog.Debug("got request for feed skeleton", "host", r.RemoteAddr) 39 - 40 - // if you need to get a feed based on the user making the request you can use this to get the callers DID 41 - // _, err = getRequestUserDID(r) 42 - // if err != nil { 43 - // slog.Error("validate users auth", "error", err) 44 - // http.Error(w, "validate auth", http.StatusUnauthorized) 45 - // return 46 - // } 47 34 48 35 params := r.URL.Query() 49 36 ··· 166 153 return 0, fmt.Errorf("parsing limit param: %w", err) 167 154 } 168 155 return limit, nil 169 - } 170 - 171 - func getRequestUserDID(r *http.Request) (string, error) { 172 - headerValues := r.Header["Authorization"] 173 - 174 - if len(headerValues) != 1 { 175 - return "", fmt.Errorf("missing authorization header") 176 - } 177 - token := strings.TrimSpace(strings.Replace(headerValues[0], "Bearer ", "", 1)) 178 - 179 - keyfunc := func(token *jwt.Token) (any, error) { 180 - did := syntax.DID(token.Claims.(jwt.MapClaims)["iss"].(string)) 181 - identity, err := identity.DefaultDirectory().LookupDID(r.Context(), did) 182 - if err != nil { 183 - return nil, fmt.Errorf("unable to resolve did %s: %s", did, err) 184 - } 185 - key, err := identity.PublicKey() 186 - if err != nil { 187 - return nil, fmt.Errorf("signing key not found for did %s: %s", did, err) 188 - } 189 - return key, nil 190 - } 191 - 192 - validMethods := jwt.WithValidMethods([]string{ES256, ES256K}) 193 - 194 - parsedToken, err := jwt.ParseWithClaims(token, jwt.MapClaims{}, keyfunc, validMethods) 195 - if err != nil { 196 - return "", fmt.Errorf("invalid token: %s", err) 197 - } 198 - 199 - claims, ok := parsedToken.Claims.(jwt.MapClaims) 200 - if !ok { 201 - return "", fmt.Errorf("token contained no claims") 202 - } 203 - 204 - issVal, ok := claims["iss"].(string) 205 - if !ok { 206 - return "", fmt.Errorf("iss claim missing") 207 - } 208 - 209 - return string(syntax.DID(issVal)), nil 210 156 } 211 157 212 158 func (s *Server) getFeed(ctx context.Context, feed, cursor string, limit int) (FeedSkeletonReponse, error) {
+38
readme.md
··· 1 + ## ATProto feed demo 2 + 3 + This is a demo example of how to create a Bluesky feed generator written in Go. You can read about how they work from some official documentation [here](https://docs.bsky.app/docs/starter-templates/custom-feeds) 4 + 5 + ### What is a feed generator? 6 + A quick high level overview is that it's a server that consumes post events from a firehose and then stores them in a database if they meet the criteria of a feed. For example, this repo demo will store any post that contains the text #golang. 7 + 8 + The server is then registered as a feed that belongs to a user. Other users can then look at that feed and see the posts that the feed contains. When that happens, a request is sent to the feed server to get a feed. The server will work out what posts to return and its response will be what's called a skeleton; a list of post IDs that can be hydrated for the users by an appview. 9 + 10 + ### Running the app 11 + 12 + There are 2 parts to this repo: 13 + 14 + 1: The feed generator server 15 + 2: A small cli tool that is used to register the feed. 16 + 17 + If doing this in local development on your machine I suggest using something like ngrok to get a public facing URL that Bluesky can use to call your locally running feed server. You will need to expose the port `443` as part of this as that's the port that is used as part of the server. For example `ngrok http http://localhost:443` which will give you a publicly accessable URL. This URL is what you will need to use in your `.env` file detailed below. 18 + 19 + A few environment variables are required to run the app. Use the `example.env` file as a template and store your environment variables in a `.env` file. 20 + 21 + * BSKY_HANDLE - Your own handle which will allow the feed register script to authenticate and register the feed for you 22 + * BSKY_PASS - A password to authenticate - app passwords are recomended here! 23 + * FEED_HOST_NAME - This is the URL of where the feed server is hosted for example "demo-feed.com" (This should not include the protocol) 24 + * FEED_NAME - This is a unique name you are going to give your feed that will be stored as an RKey in your PDS as a record 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 + 31 + Next you need to register the feed which can be done by running from the root of this repo `go run cmd/register-feed/main.go` 32 + 33 + This should print out some JSON and part of that will be a field `validationStatus` which should have the value `valid` if successful. 34 + 35 + You can then head to your profile on Bluesky, go to the feeds section and you should see your feed. There may not be any posts on it as it's not likely someone has posted a post with #golang since you started your server. However if you create a post with #golang you should see it in your feed. 36 + 37 + ### Contributing 38 + This is a demo of how to build and run a simple feed generator in Go. There are lots more things that can be done to create feeds but that can be left to you. I have kept it simple but if you wish to contribute then feel free to fork and PR any improvements you think there can be.