aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--backend/fetchers/app.bsky.go83
-rw-r--r--backend/fetchers/cache.go73
-rw-r--r--backend/fetchers/profiles.go14
-rw-r--r--backend/general/utils.go16
-rw-r--r--backend/notes/app.bsky.feed.post.go4
5 files changed, 160 insertions, 30 deletions
diff --git a/backend/fetchers/app.bsky.go b/backend/fetchers/app.bsky.go
index 6b25f0b..069a183 100644
--- a/backend/fetchers/app.bsky.go
+++ b/backend/fetchers/app.bsky.go
@@ -3,7 +3,9 @@ package fetchers
import (
"encoding/json"
"fmt"
+ "io"
"llostre/backend/auth"
+ "llostre/backend/general"
"llostre/backend/notes"
"log/slog"
"net/http"
@@ -28,7 +30,7 @@ func FetchAppBskyFeedPosts(logger *slog.Logger, did, pdsURL, accessToken string,
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)
@@ -42,7 +44,7 @@ func FetchAppBskyFeedPosts(logger *slog.Logger, did, pdsURL, accessToken string,
return nil, err
}
- profile, err := FetchProfile(did, pdsURL)
+ profile, err := FetchProfile(did, pdsURL, notesCache)
if err != nil {
logger.Error("profile fetch failed", "did", did, "error", err)
} else {
@@ -62,3 +64,80 @@ func FetchAppBskyFeedPosts(logger *slog.Logger, did, pdsURL, accessToken string,
return posts, nil
}
+
+func FetchPostByURI(uri, accessToken string, keypair *auth.DPoPKeyPair, cache *Cache) (*notes.Note, error) {
+ // check cache
+ if cached, found := cache.Get(uri); found {
+ return cached, nil
+ }
+
+ repo, collection, rkey, err := general.ParseURI(uri)
+
+ // need to resolve DID to PDS
+ pds, err := FetchPDS(repo)
+ if err != nil {
+ return nil, err
+ }
+
+ url := fmt.Sprintf("%s/xrpc/com.atproto.repo.getRecord?repo=%s&collection=%s&rkey=%s",
+ pds, repo, collection, rkey)
+
+ // fetch with DPoP
+ dpopProof, err := keypair.GenerateDPoPProof("GET", url, "")
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("Authorization", "Bearer "+accessToken)
+ req.Header.Set("DPoP", dpopProof)
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != 200 {
+ body, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("PDS returned status %d: %s", resp.StatusCode, string(body))
+ }
+
+ var result struct {
+ URI string `json:"uri"`
+ Value json.RawMessage `json:"value"`
+ }
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+ return nil, err
+ }
+
+ // convert to AppBskyFeedPostRecord
+ record := notes.AppBskyFeedPostRecord{
+ URI: result.URI,
+ }
+ if err := json.Unmarshal(result.Value, &record.Value); err != nil {
+ return nil, err
+ }
+
+ // get author of the note
+ profile, found := cache.GetProfile(repo)
+ if !found {
+ profile, err = FetchProfile(repo, pds, cache)
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ note, err := notes.AppBskyFeedPostFromRecord(record, profile)
+ if err != nil {
+ return nil, err
+ }
+
+ // cache it
+ cache.Set(note.URI, note)
+
+ return note, nil
+}
diff --git a/backend/fetchers/cache.go b/backend/fetchers/cache.go
index 12c68a8..b863d60 100644
--- a/backend/fetchers/cache.go
+++ b/backend/fetchers/cache.go
@@ -1,54 +1,55 @@
package fetchers
import (
+ "llostre/backend/general"
"llostre/backend/notes"
"sync"
"time"
)
type CacheEntry struct {
- Note *notes.Note
- ExpiresAt time.Time
+ Note *notes.Note
+ ExpiresAt time.Time
}
type Cache struct {
- posts map[string]*CacheEntry
- mu sync.RWMutex
+ posts map[string]*CacheEntry
+ profiles map[string]*ProfileCacheEntry
+ mu sync.RWMutex
}
func NewCache() *Cache {
return &Cache{
- posts: make(map[string]*CacheEntry),
+ posts: make(map[string]*CacheEntry),
+ profiles: make(map[string]*ProfileCacheEntry),
}
}
func (c *Cache) Get(uri string) (*notes.Note, bool) {
- c.mu.Lock()
- defer c.mu.Unlock()
+ c.mu.Lock()
+ defer c.mu.Unlock()
- entry, exists := c.posts[uri]
+ entry, exists := c.posts[uri]
- if !exists {
- return nil, false
- }
+ if !exists {
+ return nil, false
+ }
- if time.Now().After(entry.ExpiresAt) {
- delete(c.posts, uri)
- return nil, false
- }
+ if time.Now().After(entry.ExpiresAt) {
+ delete(c.posts, uri)
+ return nil, false
+ }
- return entry.Note, true
+ 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,
+ Note: note,
+ ExpiresAt: time.Now().Add(15 * time.Minute),
}
}
@@ -75,3 +76,35 @@ func (c *Cache) GetMany(uris []string) map[string]*notes.Note {
return result
}
+
+type ProfileCacheEntry struct {
+ Profile *general.Profile
+ ExpiresAt time.Time
+}
+
+func (c *Cache) GetProfile(did string) (*general.Profile, bool) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ entry, exists := c.profiles[did]
+ if !exists {
+ return nil, false
+ }
+
+ if time.Now().After(entry.ExpiresAt) {
+ delete(c.profiles, did)
+ return nil, false
+ }
+
+ return entry.Profile, true
+}
+
+func (c *Cache) SetProfile(did string, profile *general.Profile) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ c.profiles[did] = &ProfileCacheEntry{
+ Profile: profile,
+ ExpiresAt: time.Now().Add(15 * time.Minute),
+ }
+}
diff --git a/backend/fetchers/profiles.go b/backend/fetchers/profiles.go
index 01a27c0..3c0154a 100644
--- a/backend/fetchers/profiles.go
+++ b/backend/fetchers/profiles.go
@@ -128,7 +128,7 @@ func FetchHandleAndPDS(did string) (string, string, error) {
return handle, pds, nil
}
-func FetchProfile(did string, pdsURL string) (*general.Profile, error) {
+func FetchProfile(did, pdsURL string, cache *Cache) (*general.Profile, error) {
recordURL := fmt.Sprintf("%s/xrpc/com.atproto.repo.getRecord?repo=%s&collection=app.bsky.actor.profile&rkey=self", pdsURL, did)
resp, err := http.Get(recordURL)
@@ -159,20 +159,24 @@ func FetchProfile(did string, pdsURL string) (*general.Profile, error) {
bannerURL = fmt.Sprintf("https://cdn.bsky.app/img/banner/plain/%s/%s@jpeg", did, response.Value.Banner.Ref.Link)
}
- return &general.Profile{
+ profile := &general.Profile{
Handle: handle,
DisplayName: response.Value.DisplayName,
AvatarURL: avatarURL,
BannerURL: bannerURL,
Pronouns: response.Value.Pronouns,
- }, nil
+ }
+
+ cache.SetProfile(handle, profile)
+ return profile, nil
}
-func FetchProfiles(dids []string, pdsURL string) map[string]general.Profile {
+func FetchProfiles(dids []string, pdsURL string, cache *Cache) map[string]general.Profile {
profiles := make(map[string]general.Profile)
for _, did := range dids {
- profile, err := FetchProfile(did, pdsURL)
+ profile, err := FetchProfile(did, pdsURL, cache)
if err == nil {
+ cache.SetProfile(did, profile)
profiles[did] = *profile
}
}
diff --git a/backend/general/utils.go b/backend/general/utils.go
new file mode 100644
index 0000000..fe0854a
--- /dev/null
+++ b/backend/general/utils.go
@@ -0,0 +1,16 @@
+package general
+
+import (
+ "fmt"
+ "strings"
+)
+
+func ParseURI(uri string) (repo, collection, rkey string, err error) {
+ // URI format: 'at://DID/collection/recordkey'
+ parts := strings.Split(strings.TrimPrefix(uri, "at://"), "/")
+ if len(parts) != 3 {
+ return "", "", "", fmt.Errorf("invalid URI format: %s", uri)
+ }
+
+ return parts[0], parts[1], parts[2], nil
+}
diff --git a/backend/notes/app.bsky.feed.post.go b/backend/notes/app.bsky.feed.post.go
index 1c11733..c4311e7 100644
--- a/backend/notes/app.bsky.feed.post.go
+++ b/backend/notes/app.bsky.feed.post.go
@@ -3,7 +3,6 @@ package notes
import (
"fmt"
"llostre/backend/general"
- "strings"
"time"
)
@@ -47,8 +46,7 @@ func AppBskyFeedPostFromRecord(record AppBskyFeedPostRecord, profile *general.Pr
replyTo = &record.Value.Reply.Parent.URI
}
- parts := strings.Split(record.URI, "/") // at://DID/collection/rkey
- did := parts[2]
+ did, _, _, err := general.ParseURI(record.URI)
var facets []Facet
for _, f := range record.Value.Facets {