From f8de6c378945495f304efebad86be05d5623290a Mon Sep 17 00:00:00 2001 From: Guillermo Serrahima Date: Sat, 11 Apr 2026 07:30:08 +0200 Subject: some refactor for personal comfort + lint --- backend/fetchers/app.bsky.actor.go | 19 ++++--- backend/fetchers/app.bsky.feed.go | 107 +++++++++++++++++++------------------ backend/fetchers/cache.go | 3 +- backend/fetchers/config.go | 10 ++++ backend/main.go | 73 ++++++++++++------------- 5 files changed, 111 insertions(+), 101 deletions(-) create mode 100644 backend/fetchers/config.go diff --git a/backend/fetchers/app.bsky.actor.go b/backend/fetchers/app.bsky.actor.go index a56fe0b..41a9c17 100644 --- a/backend/fetchers/app.bsky.actor.go +++ b/backend/fetchers/app.bsky.actor.go @@ -7,7 +7,6 @@ import ( "llostre/backend/auth" "llostre/backend/general" "llostre/backend/notes" - "log/slog" "net" "net/http" "strings" @@ -132,34 +131,34 @@ func FetchHandleAndPDS(did string) (string, string, error) { return handle, pds, nil } -func FetchAppBskyActorProfile(logger *slog.Logger, did, pdsURL, slingshotURL string, session *auth.OAuthState, cache *Cache) (*notes.Author, error) { +func FetchAppBskyActorProfile(conf *Config, did, pdsURL string, session *auth.OAuthState) (*notes.Author, error) { // construct profile record URI uri := fmt.Sprintf("at://%s/app.bsky.actor.profile/self", did) // try slingshot first - if slingshotURL != "" && general.SlingshotAvailable() { - rawRecord, err := general.FetchRawRecordByURI(logger, uri, slingshotURL, session) + if conf.SlingshotURL != "" && general.SlingshotAvailable() { + rawRecord, err := general.FetchRawRecordByURI(conf.Logger, uri, conf.SlingshotURL, session) if err != nil { - logger.Warn("slingshot profile fetch failed", "did", did, "error", err) + conf.Logger.Warn("slingshot profile fetch failed", "did", did, "error", err) general.SetSlingshotBackoff(5 * time.Minute) } else { - return parseAppBskyActorProfile(did, pdsURL, rawRecord, cache) + return parseAppBskyActorProfile(did, pdsURL, rawRecord, conf.Cache) } } // fallback to PDS - rawRecord, err := general.FetchRawRecordByURI(logger, uri, pdsURL, session) + rawRecord, err := general.FetchRawRecordByURI(conf.Logger, uri, pdsURL, session) if err != nil { return nil, err } - return parseAppBskyActorProfile(did, pdsURL, rawRecord, cache) + return parseAppBskyActorProfile(did, pdsURL, rawRecord, conf.Cache) } -func FetchAppBskyActorProfiles(logger *slog.Logger, dids []string, pdsURL, slingshotURL string, session *auth.OAuthState, cache *Cache) map[string]notes.Author { +func FetchAppBskyActorProfiles(conf *Config, dids []string, pdsURL string, session *auth.OAuthState) map[string]notes.Author { authors := make(map[string]notes.Author) for _, did := range dids { - author, err := FetchAppBskyActorProfile(logger, did, pdsURL, slingshotURL, session, cache) + author, err := FetchAppBskyActorProfile(conf, did, pdsURL, session) if err == nil { authors[did] = *author } diff --git a/backend/fetchers/app.bsky.feed.go b/backend/fetchers/app.bsky.feed.go index f490efb..8562a9c 100644 --- a/backend/fetchers/app.bsky.feed.go +++ b/backend/fetchers/app.bsky.feed.go @@ -6,78 +6,77 @@ import ( "llostre/backend/auth" "llostre/backend/general" "llostre/backend/notes" - "log/slog" "time" ) -func FetchAppBskyFeedPosts(logger *slog.Logger, did, pdsURL, slingshotURL string, session *auth.OAuthState, cache *Cache) ([]notes.Note, error) { +func FetchAppBskyFeedPosts(conf *Config, did, pdsURL string, session *auth.OAuthState) ([]notes.Note, error) { // get author of the note - author, found := cache.GetAuthor(did) + author, found := conf.Cache.GetAuthor(did) var err error if !found { - author, err = FetchAppBskyActorProfile(logger, did, pdsURL, slingshotURL, session, cache) + author, err = FetchAppBskyActorProfile(conf, did, pdsURL, session) } if err != nil { return nil, err } // try slingshot first if given - if slingshotURL != "" && general.SlingshotAvailable() { - records, err := general.FetchRawRecordCollection(logger, did, "app.bsky.feed.post", slingshotURL, session) + if conf.SlingshotURL != "" && general.SlingshotAvailable() { + records, err := general.FetchRawRecordCollection(conf.Logger, did, "app.bsky.feed.post", conf.SlingshotURL, session) if err != nil { - logger.Error("fetching from slingshot failed, falling back to PDS") + conf.Logger.Error("fetching from slingshot failed, falling back to PDS") general.SetSlingshotBackoff(5 * time.Minute) } else { - return parseAppBskyFeedPostCollection(logger, slingshotURL, session, records, author, cache) + return parseAppBskyFeedPostCollection(conf, session, records, author) } } - records, err := general.FetchRawRecordCollection(logger, did, "app.bsky.feed.post", pdsURL, session) + records, err := general.FetchRawRecordCollection(conf.Logger, did, "app.bsky.feed.post", pdsURL, session) if err != nil { return nil, err } - return parseAppBskyFeedPostCollection(logger, slingshotURL, session, records, author, cache) + return parseAppBskyFeedPostCollection(conf, session, records, author) } -func FetchAppBskyFeedPostsWithCursor(logger *slog.Logger, did, pdsURL, slingshotURL string, session *auth.OAuthState, cache *Cache, cursor string, limit int) ([]notes.Note, error) { - author, found := cache.GetAuthor(did) +func FetchAppBskyFeedPostsWithCursor(conf *Config, did, pdsURL string, session *auth.OAuthState, cursor string) ([]notes.Note, error) { + author, found := conf.Cache.GetAuthor(did) var err error if !found { - author, err = FetchAppBskyActorProfile(logger, did, pdsURL, slingshotURL, session, cache) + author, err = FetchAppBskyActorProfile(conf, did, pdsURL, session) } if err != nil { return nil, err } - if slingshotURL != "" && general.SlingshotAvailable() { - records, nextCursor, err := general.FetchRawRecordCollectionWithCursor(logger, did, "app.bsky.feed.post", slingshotURL, session, cursor, limit) + if conf.SlingshotURL != "" && general.SlingshotAvailable() { + records, nextCursor, err := general.FetchRawRecordCollectionWithCursor(conf.Logger, did, "app.bsky.feed.post", conf.SlingshotURL, session, cursor, conf.PageSize) if err != nil { - logger.Error("fetching from slingshot failed, falling back to PDS") + conf.Logger.Error("fetching from slingshot failed, falling back to PDS") general.SetSlingshotBackoff(5 * time.Minute) } else { - notes, err := parseAppBskyFeedPostCollection(logger, slingshotURL, session, records, author, cache) - cache.SetCursor(did, nextCursor) + notes, err := parseAppBskyFeedPostCollection(conf, session, records, author) + conf.Cache.SetCursor(did, nextCursor) return notes, err } } - records, nextCursor, err := general.FetchRawRecordCollectionWithCursor(logger, did, "app.bsky.feed.post", pdsURL, session, cursor, limit) + records, nextCursor, err := general.FetchRawRecordCollectionWithCursor(conf.Logger, did, "app.bsky.feed.post", pdsURL, session, cursor, conf.PageSize) if err != nil { return nil, err } - notes, err := parseAppBskyFeedPostCollection(logger, pdsURL, session, records, author, cache) + notes, err := parseAppBskyFeedPostCollection(conf, session, records, author) - cache.SetCursor(did, nextCursor) + conf.Cache.SetCursor(did, nextCursor) return notes, err } -func GetAppBskyFeedPostByURI(logger *slog.Logger, uri, slingshotURL string, session *auth.OAuthState, cache *Cache) (*notes.Note, error) { +func GetAppBskyFeedPostByURI(conf *Config, uri string, session *auth.OAuthState) (*notes.Note, error) { // check cache, return if it was cached without auth or if it was cached with auth and we have auth - logger.Debug("Getting app.bsky.feed.post", "uri", uri) - if cached, found := cache.Get(uri); found { - logger.Debug("Found in the cache", "public", !cached.AuthorNoUnauth) + conf.Logger.Debug("Getting app.bsky.feed.post", "uri", uri) + if cached, found := conf.Cache.Get(uri); found { + conf.Logger.Debug("Found in the cache", "public", !cached.AuthorNoUnauth) if !cached.AuthorNoUnauth { return cached, nil } @@ -89,10 +88,10 @@ func GetAppBskyFeedPostByURI(logger *slog.Logger, uri, slingshotURL string, sess } } - return FetchAppBskyFeedPostByURI(logger, uri, slingshotURL, session, cache) + return FetchAppBskyFeedPostByURI(conf, uri, session) } -func FetchAppBskyFeedPostByURI(logger *slog.Logger, uri, slingshotURL string, session *auth.OAuthState, cache *Cache) (*notes.Note, error) { +func FetchAppBskyFeedPostByURI(conf *Config, uri string, session *auth.OAuthState) (*notes.Note, error) { repo, _, _, err := general.ParseURI(uri) // need to resolve DID to PDS @@ -100,38 +99,38 @@ func FetchAppBskyFeedPostByURI(logger *slog.Logger, uri, slingshotURL string, se if err != nil { return nil, err } - logger.Debug("fetched origin PDS", "did", repo, "pdsURL", pdsURL) + conf.Logger.Debug("fetched origin PDS", "did", repo, "pdsURL", pdsURL) // get author of the note - author, found := cache.GetAuthor(repo) + author, found := conf.Cache.GetAuthor(repo) if !found { - author, err = FetchAppBskyActorProfile(logger, repo, pdsURL, slingshotURL, session, cache) + author, err = FetchAppBskyActorProfile(conf, repo, pdsURL, session) } if err != nil { return nil, err } // try slingshot first if given - if slingshotURL != "" && general.SlingshotAvailable() { - rawRecord, err := general.FetchRawRecordByURI(logger, uri, slingshotURL, session) + if conf.SlingshotURL != "" && general.SlingshotAvailable() { + rawRecord, err := general.FetchRawRecordByURI(conf.Logger, uri, conf.SlingshotURL, session) if err != nil { - logger.Error("fetching from slingshot failed", "error", err) + conf.Logger.Error("fetching from slingshot failed", "error", err) general.SetSlingshotBackoff(5 * time.Minute) } else { - return parseAppBskyFeedPostRecord(rawRecord, author, cache) + return parseAppBskyFeedPostRecord(rawRecord, author, conf) } } - rawRecord, err := general.FetchRawRecordByURI(logger, uri, pdsURL, session) + rawRecord, err := general.FetchRawRecordByURI(conf.Logger, uri, pdsURL, session) if err != nil { - logger.Error("Fetching from PDS failed") + conf.Logger.Error("Fetching from PDS failed") return nil, err } - return parseAppBskyFeedPostRecord(rawRecord, author, cache) + return parseAppBskyFeedPostRecord(rawRecord, author, conf) } -func parseAppBskyFeedPostRecord(rawRecord *general.RawRecord, author *notes.Author, cache *Cache) (*notes.Note, error) { +func parseAppBskyFeedPostRecord(rawRecord *general.RawRecord, author *notes.Author, conf *Config) (*notes.Note, error) { if rawRecord == nil { return nil, fmt.Errorf("unable to parse nil record") } @@ -150,57 +149,59 @@ func parseAppBskyFeedPostRecord(rawRecord *general.RawRecord, author *notes.Auth } // cache it - cache.Set(note.URI, note) + conf.Cache.Set(note.URI, note) + + conf.Logger.Debug("parsed note", "uri", note.URI, "media_count", len(note.Media), "embed", record.Value.Embed) return note, nil } -func parseAppBskyFeedPostCollection(logger *slog.Logger, slingshotURL string, session *auth.OAuthState, records []general.RawRecord, author *notes.Author, cache *Cache) ([]notes.Note, error) { +func parseAppBskyFeedPostCollection(conf *Config, session *auth.OAuthState, records []general.RawRecord, author *notes.Author) ([]notes.Note, error) { posts := make([]notes.Note, 0, len(records)) for _, record := range records { - note, err := parseAppBskyFeedPostRecord(&record, author, cache) + note, err := parseAppBskyFeedPostRecord(&record, author, conf) if err != nil { - logger.Error("failed to parse post record", "error", err) + conf.Logger.Error("failed to parse post record", "error", err) continue } if note.ReplyTo != nil { - parents, err := FetchParents(logger, *note.ReplyTo, slingshotURL, session, cache, 10) + parents, err := FetchParents(conf, *note.ReplyTo, session, 10) if err != nil { - logger.Warn("failed to fetch parent chain", "uri", note.URI, "error", err) + conf.Logger.Warn("failed to fetch parent chain", "uri", note.URI, "error", err) } note.Parents = parents } - cache.Set(note.URI, note) + conf.Cache.Set(note.URI, note) posts = append(posts, *note) } return notes.DeduplicateThreads(posts), nil } -func FetchParents(logger *slog.Logger, replyToURI string, slingshotURL string, session *auth.OAuthState, cache *Cache, maxDepth int) ([]notes.Note, error) { +func FetchParents(conf *Config, replyToURI string, session *auth.OAuthState, maxDepth int) ([]notes.Note, error) { parents := []notes.Note{} currentURI := replyToURI for currentURI != "" { - note, err := GetAppBskyFeedPostByURI(logger, currentURI, slingshotURL, session, cache) + note, err := GetAppBskyFeedPostByURI(conf, currentURI, session) if err != nil { - logger.Warn("failed to fetch parent", "uri", currentURI, "error", err) + conf.Logger.Warn("failed to fetch parent", "uri", currentURI, "error", err) note = notes.MissingNote(currentURI) - cache.Set(currentURI, note) + conf.Cache.Set(currentURI, note) parents = append(parents, *note) break } // preemptively fetch posts from author to avoid hammering servers if there's a back and forth - if _, found := cache.GetAuthor(note.AuthorDID); !found { - logger.Debug("caching posts from thread participant", "did", note.AuthorDID) + if _, found := conf.Cache.GetAuthor(note.AuthorDID); !found { + conf.Logger.Debug("caching posts from thread participant", "did", note.AuthorDID) authorPDS, err := FetchPDS(note.AuthorDID) if err != nil { - logger.Warn("failed to fetch parent", "uri", currentURI, "error", err) + conf.Logger.Warn("failed to fetch parent", "uri", currentURI, "error", err) break } - go FetchAppBskyFeedPosts(logger, note.AuthorDID, authorPDS, slingshotURL, session, cache) + go FetchAppBskyFeedPostsWithCursor(conf, note.AuthorDID, authorPDS, session, "") } parents = append(parents, *note) diff --git a/backend/fetchers/cache.go b/backend/fetchers/cache.go index 26be070..ac91259 100644 --- a/backend/fetchers/cache.go +++ b/backend/fetchers/cache.go @@ -4,11 +4,10 @@ import ( "context" "encoding/json" "fmt" + "llostre/backend/notes" "sort" "time" - "llostre/backend/notes" - "github.com/redis/go-redis/v9" ) diff --git a/backend/fetchers/config.go b/backend/fetchers/config.go new file mode 100644 index 0000000..090eb09 --- /dev/null +++ b/backend/fetchers/config.go @@ -0,0 +1,10 @@ +package fetchers + +import "log/slog" + +type Config struct { + SlingshotURL string + PageSize int + Logger *slog.Logger + Cache *Cache +} diff --git a/backend/main.go b/backend/main.go index 27767b0..6585e56 100644 --- a/backend/main.go +++ b/backend/main.go @@ -19,14 +19,11 @@ import ( ) var ( - logger *slog.Logger backendURL string frontendURL string - slingshotURL string sessionsMutex sync.RWMutex sessions = make(map[string]*auth.OAuthState) - cache *fetchers.Cache - pageSize int + config *fetchers.Config ) func init() { @@ -54,11 +51,11 @@ func init() { logLevel = slog.LevelError } - logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ Level: logLevel, })) - slingshotURL = os.Getenv("SLINGSHOT_URL") + slingshotURL := os.Getenv("SLINGSHOT_URL") ttlMin := os.Getenv("TTL_MIN") var expiryMinutes int @@ -75,12 +72,16 @@ func init() { redisPassword := os.Getenv("REDIS_PASSWORD") postCache, err := fetchers.NewCache(redisAddr, redisPassword, expiryMinutes) if err != nil { - logger.Error("failed to connect to redis", "error", err) + config.Logger.Error("failed to connect to redis", "error", err) panic(err) } - pageSize = 50 - cache = postCache + config = &fetchers.Config{ + SlingshotURL: slingshotURL, + PageSize: 50, + Logger: logger, + Cache: postCache, + } } func loginHandler(w http.ResponseWriter, r *http.Request) { @@ -101,7 +102,7 @@ func loginHandler(w http.ResponseWriter, r *http.Request) { http.Error(w, fmt.Sprintf("failed to fetch PDS: %v", err), http.StatusInternalServerError) return } - logger.Debug("Resolved DID and PDS", "did", did, "pds", pds) + config.Logger.Debug("Resolved DID and PDS", "did", did, "pds", pds) // fetch oauth metadata from PDS metadata, err := auth.FetchOAuthMetadata(pds) @@ -222,7 +223,7 @@ func callbackHandler(w http.ResponseWriter, r *http.Request) { delete(sessions, state) // remove the partial oauth state from login sessionsMutex.Unlock() - logger.Debug("About to set cookie", "token", sessionToken) + config.Logger.Debug("About to set cookie", "token", sessionToken) // set session cookie http.SetCookie(w, &http.Cookie{ @@ -235,7 +236,7 @@ func callbackHandler(w http.ResponseWriter, r *http.Request) { MaxAge: 86400 * 30, }) - logger.Debug("Cookie set, redirecting to frontend", "destination", frontendURL+"/") + config.Logger.Debug("Cookie set, redirecting to frontend", "destination", frontendURL+"/") // redirect to main app http.Redirect(w, r, frontendURL, http.StatusFound) @@ -281,9 +282,9 @@ func getSession(cookieValue string) (*auth.OAuthState, error) { sessionRead := session.Copy() sessionsMutex.RUnlock() - logger.Debug("session found", "did", session.DID, "pds", session.PDS, "access token length", len(session.AccessToken), "session keypair", session.KeyPair) + config.Logger.Debug("session found", "did", session.DID, "pds", session.PDS, "access token length", len(session.AccessToken), "session keypair", session.KeyPair) if session.KeyPair != nil { - logger.Debug("session public key", "key", session.KeyPair.PublicKey) + config.Logger.Debug("session public key", "key", session.KeyPair.PublicKey) } needsRefresh := time.Now().After(sessionRead.ExpiresAt) @@ -294,7 +295,7 @@ func getSession(cookieValue string) (*auth.OAuthState, error) { sessionsMutex.Lock() delete(sessions, cookieValue) sessionsMutex.Unlock() - logger.Error("no authorization servers found for token refresh") + config.Logger.Error("no authorization servers found for token refresh") return nil, fmt.Errorf("no authorization servers found") } @@ -310,16 +311,16 @@ func getSession(cookieValue string) (*auth.OAuthState, error) { } func postsHandler(w http.ResponseWriter, r *http.Request) { - logger.Debug("posts handler start") + config.Logger.Debug("posts handler start") cookie, err := r.Cookie("session_token") if err != nil { - logger.Error("cookie not received", "error", err) + config.Logger.Error("cookie not received", "error", err) http.Error(w, "unauthorized", http.StatusUnauthorized) return } - logger.Debug("cookie received", "value", cookie.Value) + config.Logger.Debug("cookie received", "value", cookie.Value) session, err := getSession(cookie.Value) if err != nil { @@ -330,42 +331,42 @@ func postsHandler(w http.ResponseWriter, r *http.Request) { // Fetch app.bsky.feed.post records var posts []notes.Note - cached, err := cache.GetPostsByDID(session.DID) - logger.Debug("cache read", "count", len(cached), "err", err) + cached, err := config.Cache.GetPostsByDID(session.DID) + config.Logger.Debug("cache read", "count", len(cached), "err", err) if err != nil { - logger.Warn("cache read failed, falling back to fetch", "error", err) + config.Logger.Warn("cache read failed, falling back to fetch", "error", err) } if len(cached) > 0 { posts = notes.DeduplicateThreads(cached) } else { - posts, err = fetchers.FetchAppBskyFeedPostsWithCursor(logger, session.DID, session.PDS, slingshotURL, session, cache, "", pageSize) + posts, err = fetchers.FetchAppBskyFeedPostsWithCursor(config, session.DID, session.PDS, session, "") if err != nil { - logger.Error("fetching app.bsky.feed.posts failed", "error", err) + config.Logger.Error("fetching app.bsky.feed.posts failed", "error", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } } - logger.Debug("posts ready", "count", len(posts)) + config.Logger.Debug("posts ready", "count", len(posts)) // background refetch if cache is running low - oldestSent, hasOldest := cache.GetOldestSent(session.DID) + oldestSent, hasOldest := config.Cache.GetOldestSent(session.DID) if !hasOldest || len(posts) > 0 { oldest := posts[len(posts)-1].CreatedAt - count, _ := cache.CountPostsOlderThan(session.DID, oldest) - cursor, hasCursor := cache.GetCursor(session.DID) - logger.Debug("refetch check", "count", count, "pageSize", pageSize, "hasCursor", hasCursor, "oldest", oldest) - if count < pageSize && hasCursor { + count, _ := config.Cache.CountPostsOlderThan(session.DID, oldest) + cursor, hasCursor := config.Cache.GetCursor(session.DID) + config.Logger.Debug("refetch check", "count", count, "pageSize", config.PageSize, "hasCursor", hasCursor, "oldest", oldest) + if count < config.PageSize && hasCursor { sessionCopy := session.Copy() - go fetchers.FetchAppBskyFeedPostsWithCursor(logger, sessionCopy.DID, sessionCopy.PDS, slingshotURL, sessionCopy, cache, cursor, pageSize) + go fetchers.FetchAppBskyFeedPostsWithCursor(config, sessionCopy.DID, sessionCopy.PDS, sessionCopy, cursor) } if !hasOldest || oldest.Before(oldestSent) { - cache.SetOldestSent(session.DID, oldest) + config.Cache.SetOldestSent(session.DID, oldest) } } // return posts - logger.Debug("successfully fetched posts", "number", len(posts)) + config.Logger.Debug("successfully fetched posts", "number", len(posts)) w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Content-Type", "application/json") @@ -389,18 +390,18 @@ func postHandler(w http.ResponseWriter, r *http.Request) { var currentSession *auth.OAuthState cookie, err := r.Cookie("session_token") if err != nil { - logger.Warn("unauthorized") + config.Logger.Warn("unauthorized") } else { session, err := getSession(cookie.Value) if err != nil { - logger.Warn("invalid session", "error", err) + config.Logger.Warn("invalid session", "error", err) } else { currentSession = session } } - note, err := fetchers.GetAppBskyFeedPostByURI(logger, uri, slingshotURL, currentSession, cache) + note, err := fetchers.GetAppBskyFeedPostByURI(config, uri, currentSession) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -411,7 +412,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) { } func main() { - defer cache.Close() + defer config.Cache.Close() // serve static files fs := http.FileServer(http.Dir("../frontend/dist")) -- cgit v1.2.3