aboutsummaryrefslogtreecommitdiff
path: root/backend/fetchers/app.bsky.feed.go
diff options
context:
space:
mode:
Diffstat (limited to 'backend/fetchers/app.bsky.feed.go')
-rw-r--r--backend/fetchers/app.bsky.feed.go143
1 files changed, 143 insertions, 0 deletions
diff --git a/backend/fetchers/app.bsky.feed.go b/backend/fetchers/app.bsky.feed.go
new file mode 100644
index 0000000..92303fc
--- /dev/null
+++ b/backend/fetchers/app.bsky.feed.go
@@ -0,0 +1,143 @@
+package fetchers
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "llostre/backend/auth"
+ "llostre/backend/general"
+ "llostre/backend/notes"
+ "log/slog"
+ "net/http"
+)
+
+type ListRecordsResponse struct {
+ Records []notes.AppBskyFeedPostRecord `json:"records"`
+}
+
+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)
+
+ logger.Debug("generating DPoP proof", "url", url)
+ dpopProof, err := keypair.GenerateDPoPProof("GET", url, "")
+ if err != nil {
+ logger.Error("DPoP generation failed", "error", err)
+ return nil, err
+ }
+
+ 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("DPoP", dpopProof)
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ var response ListRecordsResponse
+ if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
+ return nil, err
+ }
+
+ profile, err := FetchProfile(did, pdsURL, notesCache)
+ if err != nil {
+ logger.Error("profile fetch failed", "did", did, "error", err)
+ } else {
+ logger.Debug("fetched profile")
+ }
+
+ posts := make([]notes.Note, 0, len(response.Records))
+ for _, record := range response.Records {
+ bp, err := notes.NoteFromAppBskyFeedPostRecord(record, profile)
+ if err != nil {
+ logger.Error("failed to convert record", "error", err, "uri", record.URI)
+ continue
+ }
+ notesCache.Set(bp.URI, bp)
+ posts = append(posts, *bp)
+ }
+
+ 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.NoteFromAppBskyFeedPostRecord(record, profile)
+ if err != nil {
+ return nil, err
+ }
+
+ // cache it
+ cache.Set(note.URI, note)
+
+ return note, nil
+}