diff options
| -rw-r--r-- | backend/fetchers/app.bsky.go | 20 | ||||
| -rw-r--r-- | backend/fetchers/cache.go | 77 | ||||
| -rw-r--r-- | backend/main.go | 7 |
3 files changed, 94 insertions, 10 deletions
diff --git a/backend/fetchers/app.bsky.go b/backend/fetchers/app.bsky.go index b6a133b..6b25f0b 100644 --- a/backend/fetchers/app.bsky.go +++ b/backend/fetchers/app.bsky.go @@ -5,6 +5,7 @@ import ( "fmt" "llostre/backend/auth" "llostre/backend/notes" + "log/slog" "net/http" ) @@ -12,22 +13,22 @@ type ListRecordsResponse struct { Records []notes.AppBskyFeedPostRecord `json:"records"` } -func FetchAppBskyFeedPosts(did, pdsURL, accessToken string, keypair *auth.DPoPKeyPair) ([]notes.Note, error) { +func FetchAppBskyFeedPosts(logger *slog.Logger, did, pdsURL, accessToken string, keypair *auth.DPoPKeyPair, notesCache *Cache) ([]notes.Note, error) { url := fmt.Sprintf("%s/xrpc/com.atproto.repo.listRecords?repo=%s&collection=app.bsky.feed.post", pdsURL, did) - fmt.Printf("Generating DPoP proof for: %s\n", url) + logger.Debug("generating DPoP proof", "url", url) dpopProof, err := keypair.GenerateDPoPProof("GET", url, "") if err != nil { - fmt.Printf("DPoP generation failed: %v\n", err) + logger.Error("DPoP generation failed", "error", err) return nil, err } - fmt.Printf("Making request to PDS\n") + logger.Debug("making request to PDS") req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } - req.Header.Set("Authorization", "Bearer "+accessToken) + req.Header.Set("Authorization", "Bearer " + accessToken) req.Header.Set("DPoP", dpopProof) resp, err := http.DefaultClient.Do(req) @@ -43,18 +44,19 @@ func FetchAppBskyFeedPosts(did, pdsURL, accessToken string, keypair *auth.DPoPKe profile, err := FetchProfile(did, pdsURL) if err != nil { - fmt.Printf("Profile fetch failed for %s: %v\n", did, err) + logger.Error("profile fetch failed", "did", did, "error", err) } else { - fmt.Printf("Fetched profile: %+v\n", profile) + logger.Debug("fetched profile") } - var posts []notes.Note + posts := make([]notes.Note, 0, len(response.Records)) for _, record := range response.Records { bp, err := notes.AppBskyFeedPostFromRecord(record, profile) if err != nil { - fmt.Printf("skipping malformed record %s: %v\n", record.URI, err) + logger.Error("failed to convert record", "error", err, "uri", record.URI) continue } + notesCache.Set(bp.URI, bp) posts = append(posts, *bp) } diff --git a/backend/fetchers/cache.go b/backend/fetchers/cache.go new file mode 100644 index 0000000..12c68a8 --- /dev/null +++ b/backend/fetchers/cache.go @@ -0,0 +1,77 @@ +package fetchers + +import ( + "llostre/backend/notes" + "sync" + "time" +) + +type CacheEntry struct { + Note *notes.Note + ExpiresAt time.Time +} + +type Cache struct { + posts map[string]*CacheEntry + mu sync.RWMutex +} + +func NewCache() *Cache { + return &Cache{ + posts: make(map[string]*CacheEntry), + } +} + +func (c *Cache) Get(uri string) (*notes.Note, bool) { + c.mu.Lock() + defer c.mu.Unlock() + + entry, exists := c.posts[uri] + + if !exists { + return nil, false + } + + if time.Now().After(entry.ExpiresAt) { + delete(c.posts, uri) + return nil, false + } + + return entry.Note, true +} + +func (c *Cache) Set(uri string, note *notes.Note) { + c.mu.Lock() + defer c.mu.Unlock() + + ttl := time.Now().Add(15 * time.Minute) + + c.posts[uri] = &CacheEntry{ + Note: note, + ExpiresAt: ttl, + } +} + +func (c *Cache) GetMany(uris []string) map[string]*notes.Note { + c.mu.Lock() + defer c.mu.Unlock() + + result := make(map[string]*notes.Note) + now := time.Now() + + for _, uri := range uris { + entry, exists := c.posts[uri] + if !exists { + continue + } + + if now.After(entry.ExpiresAt) { + delete(c.posts, uri) + continue + } + + result[uri] = entry.Note + } + + return result +} diff --git a/backend/main.go b/backend/main.go index d5fbb91..f6851da 100644 --- a/backend/main.go +++ b/backend/main.go @@ -21,6 +21,7 @@ var ( frontendURL string sessionsMutex sync.RWMutex sessions = make(map[string]*auth.OAuthState) + notesCache *fetchers.Cache ) func init() { @@ -51,6 +52,8 @@ func init() { logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ Level: logLevel, })) + + notesCache = fetchers.NewCache() } func loginHandler(w http.ResponseWriter, r *http.Request) { @@ -266,6 +269,7 @@ func postsHandler(w http.ResponseWriter, r *http.Request) { logger.Debug("session public key", "key", session.KeyPair.PublicKey) } + // Check for access token refresh needsRefresh := time.Now().After(session.ExpiresAt) pds := session.PDS refreshToken := session.RefreshToken @@ -289,7 +293,8 @@ func postsHandler(w http.ResponseWriter, r *http.Request) { sessionsMutex.Unlock() } - posts, err := fetchers.FetchAppBskyFeedPosts(session.DID, session.PDS, session.AccessToken, session.KeyPair) + // Fetch app.bsky.feed.post records + posts, err := fetchers.FetchAppBskyFeedPosts(logger, session.DID, session.PDS, session.AccessToken, session.KeyPair, notesCache) if err != nil { logger.Error("fetching app.bsky.feed.posts failed", "error", err) http.Error(w, err.Error(), http.StatusInternalServerError) |
