package 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) }