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.AppBskyFeedPostFromRecord(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.AppBskyFeedPostFromRecord(record, profile) if err != nil { return nil, err } // cache it cache.Set(note.URI, note) return note, nil }