Something went wrong. Try again.
Feed generator written in Golang
Something went wrong. Try again.
5.6 kB · 233 lines
Go
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234package main
import ( "encoding/json" "fmt" "log" "net/http" "strconv" "strings" "time"
combskytypes "github.com/bluesky-social/indigo/api/bsky" chi "github.com/go-chi/chi/v5" middleware "github.com/go-chi/chi/v5/middleware")
type HTTPServerSettings struct { PublisherDID string ServiceDID string Hostname string}
type HTTPServer struct { Router chi.Router DatabaseConn *ManagedDatabaseConnection DoneSignal *chan interface{} Settings HTTPServerSettings}
func NewHTTPServer(dbcon *ManagedDatabaseConnection) *HTTPServer { router := chi.NewRouter()
router.Use(middleware.RequestID) router.Use(middleware.Logger) router.Use(middleware.Recoverer) router.Use(middleware.Compress(5)) router.Use(middleware.SetHeader("Content-Type", "application/json"))
env := GetEnvironmentVariables() publisherDid := env["FEEDGEN_PUBLISHER_DID"] hostname := env["FEEDGEN_HOSTNAME"] serviceDid, hasSvcDid := env["FEEDGEN_SERVICE_DID"] if !hasSvcDid { serviceDid = fmt.Sprintf("did:web:%s", hostname) }
done := make(chan interface{})
server := HTTPServer{ router, dbcon, &done, HTTPServerSettings{ publisherDid, serviceDid, hostname, }, }
return &server}
func (httpServer HTTPServer) RegisterRoutes() { httpServer.Router.Get("/", func(w http.ResponseWriter, r *http.Request) { encoded, _ := json.Marshal( map[string]interface{}{ "what": "atproto feed generator, written in from scratch in go", "when": "may 2025", "who": map[string]string{ "did": fmt.Sprintf("at://%s", httpServer.Settings.PublisherDID), "github": "https://github.com/keaysma", "tangled": "https://tangled.org/@keays.io", }, }, )
w.WriteHeader(http.StatusOK) w.Write(encoded) })
httpServer.Router.Get("/.well-known/did.json", func(w http.ResponseWriter, r *http.Request) { if !strings.HasSuffix(httpServer.Settings.ServiceDID, httpServer.Settings.Hostname) { w.WriteHeader(http.StatusNotFound) return }
encoded, _ := json.Marshal( map[string]interface{}{ "@context": []string{"https://www.w3.org/ns/did/v1"}, "id": httpServer.Settings.ServiceDID, "service": []map[string]string{ map[string]string{ "id": "#bsky_fg", "type": "BskyFeedGenerator", "serviceEndpoint": fmt.Sprintf("https://%s", httpServer.Settings.Hostname), }, }, }, ) w.WriteHeader(http.StatusOK) w.Write(encoded) return })
// TBD - Does Indigo provide some kind-of wrapper for this? httpServer.Router.Get("/xrpc/app.bsky.feed.getFeedSkeleton", func(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() feedName := query.Get("feed") cursor := query.Get("cursor") paramLimit := query.Get("limit")
feedNameBase := fmt.Sprintf("at://%s/app.bsky.feed.generator/", httpServer.Settings.PublisherDID) algo, hasFeedNameBase := strings.CutPrefix(feedName, feedNameBase) if !hasFeedNameBase { encoded, _ := json.Marshal( map[string]string{ "error": "UnsupportedAlgorithm", "message": "Unsupported algorithm", }, )
w.WriteHeader(http.StatusBadRequest) w.Write(encoded) return }
switch algo { case "banana-slip": case "coffee": break default: encoded, _ := json.Marshal( map[string]string{ "error": "UnsupportedAlgorithm", "message": "Unsupported algorithm", }, )
w.WriteHeader(http.StatusBadRequest) w.Write(encoded) return }
limit, err := strconv.Atoi(paramLimit) if err != nil || limit < 1 || limit > 100 { limit = 50 }
indexedBefore := time.Now().UTC() if cursor != "" { epoch, err := strconv.ParseInt(cursor, 10, 64) if err != nil { encoded, _ := json.Marshal( map[string]string{ "error": "InvalidRequest", "message": "Bad cursor value", }, )
w.WriteHeader(http.StatusBadRequest) w.Write(encoded) return } indexedBefore = time.Unix(epoch, 0) }
feedData, err := httpServer.DatabaseConn.ReadPostsFromDatabase(algo, indexedBefore, limit) if err != nil { log.Println(fmt.Errorf("failed to read posts from database: ", err)) encoded, _ := json.Marshal( map[string]string{ "error": "internal server error", "message": "Failed to get feed data", }, )
w.WriteHeader(http.StatusInternalServerError) w.Write(encoded) return }
feed := make([]combskytypes.FeedDefs_SkeletonFeedPost, 0) for _, entry := range *feedData { feed = append(feed, combskytypes.FeedDefs_SkeletonFeedPost{ Post: entry.Uri, }) }
lastIndexed := indexedBefore if len(*feedData) > 0 { lastIndexed, err = time.Parse(time.RFC3339, (*feedData)[len(*feedData)-1].IndexedAt) if err != nil { log.Println(fmt.Errorf("failed to read posts from database: ", err)) encoded, _ := json.Marshal( map[string]string{ "error": "internal server error", "message": "Failed to get feed data", }, )
w.WriteHeader(http.StatusInternalServerError) w.Write(encoded) return } }
encoded, _ := json.Marshal( map[string]interface{}{ "cursor": strconv.FormatInt(lastIndexed.UTC().Unix(), 10), "feed": feed, }, )
w.WriteHeader(http.StatusOK) w.Write(encoded) })}
func (httpServer HTTPServer) StartListener() { env := GetEnvironmentVariables() host, hostFound := env["FEEDGEN_LISTENHOST"] if !hostFound { host = "0.0.0.0" }
port, portFound := env["FEEDGEN_PORT"] if !portFound { port = "3123" }
defer close(*httpServer.DoneSignal) http.ListenAndServe(fmt.Sprintf("%s:%s", host, port), httpServer.Router)}