Something went wrong. Try again.
A tool for managing custom bluesky feeds
Something went wrong. Try again.
5.1 kB · 174 lines
Go
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175package server
import ( "encoding/json" "fmt" "log/slog" "net/http" "strconv"
"tangled.org/dhawos.dev/bskyfeedctl/feeds" "tangled.org/dhawos.dev/bskyfeedctl/indexer"
"github.com/bluesky-social/indigo/atproto/syntax")
var statusHealthOK = healthResponse{Status: "ok"}
const ( generatorCollection = "app.bsky.feed.generator" defaultFeedLimit = "30")
type healthResponse struct { Status string `json:"status"`}
type ServerConfig struct { Host string Addr string Port uint}
type BlueskyFeedGeneratorConfig struct { Server ServerConfig}
type BlueskyFeedGenerator struct { serverConfig ServerConfig feedConfig feeds.FeedConfig feeds map[string]feeds.Feed}
func NewBlueskyFeedGenerator(serverConfig ServerConfig, feedConfig feeds.FeedConfig, esConfig indexer.ElasticsearchConfig) (*BlueskyFeedGenerator, error) { elasticClient, err := indexer.NewElasticTypedClient(esConfig) if err != nil { return nil, fmt.Errorf("initialiazing elastic client: %w", err) } return &BlueskyFeedGenerator{ serverConfig: serverConfig, feedConfig: feedConfig, feeds: feeds.RegisterFeeds(elasticClient), }, nil}
func (bfg *BlueskyFeedGenerator) Start() error { xrpc := http.NewServeMux() xrpc.HandleFunc("GET /app.bsky.feed.getFeedSkeleton", bfg.appBskyFeedGetFeedSkeletonHandler)
root := http.NewServeMux() root.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { err := json.NewEncoder(w).Encode(statusHealthOK) if err != nil { slog.Error("error sending health response", slog.Any("err", err)) http.Error(w, "", http.StatusInternalServerError) return } }) root.HandleFunc("/.well-known/did.json", bfg.wellKnownDIDHandler) root.Handle("/xrpc/", http.StripPrefix("/xrpc", xrpc)) loggedRoot := loggingMiddleware(root)
addr := fmt.Sprintf("%s:%d", bfg.serverConfig.Addr, bfg.serverConfig.Port) slog.Info("Starting custom feed server", slog.String("addr", addr)) err := http.ListenAndServe(addr, loggedRoot) if err != nil { return err } return nil}
type wellKnownDIDResponseService struct { ID string `json:"id"` Type string `json:"type"` ServiceEndpoint string `json:"serviceEndpoint"`}
type wellKnownDIDResponse struct { Context []string `json:"@context"` ID string `json:"id"` Service []wellKnownDIDResponseService `json:"service"`}
func (bfg *BlueskyFeedGenerator) wellKnownDIDHandler(w http.ResponseWriter, req *http.Request) { result := wellKnownDIDResponse{ Context: []string{"https://www.w3.org/ns/did/v1"}, ID: bfg.feedConfig.FeedDID, Service: []wellKnownDIDResponseService{ { ID: "#bsky_fg", Type: "BskyFeedGenerator", ServiceEndpoint: fmt.Sprintf("https://%s", bfg.serverConfig.Host), }, }, } w.Header().Set("Content-Type", "application/json") err := json.NewEncoder(w).Encode(result) if err != nil { slog.Error("error sending well known response", slog.Any("err", err)) http.Error(w, "", http.StatusInternalServerError) return }}
func (bfg *BlueskyFeedGenerator) appBskyFeedGetFeedSkeletonHandler(w http.ResponseWriter, req *http.Request) { // Parse request feed := req.URL.Query().Get("feed") if feed == "" { http.Error(w, "", http.StatusBadRequest) return } cursor := req.URL.Query().Get("cursor") limit := req.URL.Query().Get("limit") if limit == "" { limit = defaultFeedLimit } limitInt, err := strconv.ParseInt(limit, 10, 32) limitInt32 := int32(limitInt) if err != nil { slog.Error("could not parse limit", "limit", limit, "err", err) http.Error(w, "", http.StatusBadRequest) return }
atURI, err := syntax.ParseATURI(feed) if err != nil { http.Error(w, "", http.StatusBadRequest) return }
if atURI.Authority() != bfg.feedConfig.OwnerDID.Authority() { slog.Error("invalid atURI authority", "authority", atURI.Authority().String(), "expectedAuthority", bfg.feedConfig.OwnerDID.Authority().String()) http.Error(w, "", http.StatusBadRequest) return } if atURI.Collection().String() != generatorCollection { slog.Error("invalid collection", "collection", atURI.Collection().String(), "expectedCollection", generatorCollection) http.Error(w, "", http.StatusBadRequest) return } algo, exists := bfg.feeds[atURI.RecordKey().String()] if !exists { slog.Error("feed not found", "feed", atURI.RecordKey()) http.Error(w, "", http.StatusNotFound) return } feedRequest := feeds.FeedRequest{ Lang: "fr", Cursor: cursor, Limit: limitInt32, } response, err := algo.SelectionFunc(req.Context(), &feedRequest) if err != nil { slog.Error("unable to generate feed", slog.String("feed", atURI.RecordKey().String()), slog.Any("err", err)) http.Error(w, "unable to generate feed", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(response) if err != nil { slog.Error("unable to encode response", slog.String("feed", atURI.RecordKey().String()), slog.Any("err", err)) http.Error(w, "unable to encode response", http.StatusInternalServerError) return }}