diff options
Diffstat (limited to 'backend/fetchers/app.bsky.feed.go')
| -rw-r--r-- | backend/fetchers/app.bsky.feed.go | 143 |
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 +} |
