···9696 return nil
9797 }
98989999- // look for any post that contains the #golang hashtag
9999+ // this is where logic goes for what posts you wish to store for a feed but for this example
100100+ // just look for any post that contains the #golang hashtag
100101 if !strings.Contains(strings.ToLower(bskyPost.Text), "#golang") {
101102 return nil
102103 }
-54
handlers.go
···88 "net/http"
99 "net/url"
1010 "strconv"
1111- "strings"
1212-1313- "github.com/bluesky-social/indigo/atproto/identity"
1414- "github.com/bluesky-social/indigo/atproto/syntax"
1515- "github.com/golang-jwt/jwt/v5"
1611)
17121813const (
···3631// HandleGetFeedSkeleton is the handler that will build up and return a feed response
3732func (s *Server) HandleGetFeedSkeleton(w http.ResponseWriter, r *http.Request) {
3833 slog.Debug("got request for feed skeleton", "host", r.RemoteAddr)
3939-4040- // if you need to get a feed based on the user making the request you can use this to get the callers DID
4141- // _, err = getRequestUserDID(r)
4242- // if err != nil {
4343- // slog.Error("validate users auth", "error", err)
4444- // http.Error(w, "validate auth", http.StatusUnauthorized)
4545- // return
4646- // }
47344835 params := r.URL.Query()
4936···166153 return 0, fmt.Errorf("parsing limit param: %w", err)
167154 }
168155 return limit, nil
169169-}
170170-171171-func getRequestUserDID(r *http.Request) (string, error) {
172172- headerValues := r.Header["Authorization"]
173173-174174- if len(headerValues) != 1 {
175175- return "", fmt.Errorf("missing authorization header")
176176- }
177177- token := strings.TrimSpace(strings.Replace(headerValues[0], "Bearer ", "", 1))
178178-179179- keyfunc := func(token *jwt.Token) (any, error) {
180180- did := syntax.DID(token.Claims.(jwt.MapClaims)["iss"].(string))
181181- identity, err := identity.DefaultDirectory().LookupDID(r.Context(), did)
182182- if err != nil {
183183- return nil, fmt.Errorf("unable to resolve did %s: %s", did, err)
184184- }
185185- key, err := identity.PublicKey()
186186- if err != nil {
187187- return nil, fmt.Errorf("signing key not found for did %s: %s", did, err)
188188- }
189189- return key, nil
190190- }
191191-192192- validMethods := jwt.WithValidMethods([]string{ES256, ES256K})
193193-194194- parsedToken, err := jwt.ParseWithClaims(token, jwt.MapClaims{}, keyfunc, validMethods)
195195- if err != nil {
196196- return "", fmt.Errorf("invalid token: %s", err)
197197- }
198198-199199- claims, ok := parsedToken.Claims.(jwt.MapClaims)
200200- if !ok {
201201- return "", fmt.Errorf("token contained no claims")
202202- }
203203-204204- issVal, ok := claims["iss"].(string)
205205- if !ok {
206206- return "", fmt.Errorf("iss claim missing")
207207- }
208208-209209- return string(syntax.DID(issVal)), nil
210156}
211157212158func (s *Server) getFeed(ctx context.Context, feed, cursor string, limit int) (FeedSkeletonReponse, error) {
+38
readme.md
···11+## ATProto feed demo
22+33+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)
44+55+### What is a feed generator?
66+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.
77+88+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.
99+1010+### Running the app
1111+1212+There are 2 parts to this repo:
1313+1414+1: The feed generator server
1515+2: A small cli tool that is used to register the feed.
1616+1717+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.
1818+1919+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.
2020+2121+* BSKY_HANDLE - Your own handle which will allow the feed register script to authenticate and register the feed for you
2222+* BSKY_PASS - A password to authenticate - app passwords are recomended here!
2323+* 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)
2424+* 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
2525+* FEED_DISPLAY_NAME - This is the name you will give your feed that users will be able to see
2626+* FEED_DESCRIPTION - This is a description of your feed that users will be able to see
2727+* 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")
2828+2929+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`
3030+3131+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`
3232+3333+This should print out some JSON and part of that will be a field `validationStatus` which should have the value `valid` if successful.
3434+3535+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.
3636+3737+### Contributing
3838+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.