package fetchers import ( "encoding/json" "fmt" "llostre/backend/auth" "llostre/backend/general" "llostre/backend/notes" "log/slog" ) func FetchAppBskyFeedPosts(logger *slog.Logger, did, pdsURL, slingshotURL string, session *auth.OAuthState, cache *Cache) ([]notes.Note, error) { // get author of the note author, found := cache.GetProfile(did) var err error if !found { author, err = FetchAppBskyActorProfile(logger, did, pdsURL, slingshotURL, session, cache) } if err != nil { return nil, err } // try slingshot first if given if slingshotURL != "" { records, err := general.FetchRawRecordCollection(logger, did, "app.bsky.feed.post", slingshotURL, session) if err != nil { logger.Error("Fetching from slingshot failed") } else { return parseAppBskyFeedPostCollection(logger, slingshotURL, session, records, author, cache) } } records, err := general.FetchRawRecordCollection(logger, did, "app.bsky.feed.post", pdsURL, session) if err != nil { return nil, err } return parseAppBskyFeedPostCollection(logger, slingshotURL, session, records, author, cache) } func GetAppBskyFeedPostByURI(logger *slog.Logger, uri, slingshotURL string, session *auth.OAuthState, cache *Cache) (*notes.Note, error) { // check cache, return if it was cached without auth or if it was cached with auth and we have auth if cached, found := cache.Get(uri); found { if !cached.AuthorNoUnauth { return cached, nil } if general.IsValidSession(session) { return cached, nil } } return FetchAppBskyFeedPostByURI(logger, uri, slingshotURL, session, cache) } func FetchAppBskyFeedPostByURI(logger *slog.Logger, uri, slingshotURL string, session *auth.OAuthState, cache *Cache) (*notes.Note, error) { repo, _, _, err := general.ParseURI(uri) // need to resolve DID to PDS pdsURL, err := FetchPDS(repo) if err != nil { return nil, err } // get author of the note author, found := cache.GetProfile(repo) if !found { author, err = FetchAppBskyActorProfile(logger, repo, pdsURL, slingshotURL, session, cache) } if err != nil { return nil, err } // try slingshot first if given if slingshotURL != "" { rawRecord, err := general.FetchRawRecordByURI(logger, uri, slingshotURL, session) if err != nil { logger.Error("fetching from slingshot failed", "error", err) } else { return parseAppBskyFeedPostRecord(rawRecord, author, cache) } } rawRecord, err := general.FetchRawRecordByURI(logger, uri, pdsURL, session) if err != nil { logger.Error("Fetching from PDS failed") return nil, err } return parseAppBskyFeedPostRecord(rawRecord, author, cache) } func parseAppBskyFeedPostRecord(rawRecord *general.RawRecord, author *notes.Author, cache *Cache) (*notes.Note, error) { if rawRecord == nil { return nil, fmt.Errorf("unable to parse nil record") } record := notes.AppBskyFeedPostRecord{ URI: rawRecord.URI, } err := json.Unmarshal(rawRecord.Value, &record.Value) if err != nil { return nil, err } note, err := notes.NoteFromAppBskyFeedPostRecord(record, author) if err != nil { return nil, err } // cache it cache.Set(note.URI, note) return note, nil } func parseAppBskyFeedPostCollection(logger *slog.Logger, slingshotURL string, session *auth.OAuthState, records []general.RawRecord, author *notes.Author, cache *Cache) ([]notes.Note, error) { posts := make([]notes.Note, 0, len(records)) for _, record := range records { note, err := parseAppBskyFeedPostRecord(&record, author, cache) if err != nil { logger.Error("failed to parse post record", "error", err) continue } if note.ReplyTo != nil { parents, err := FetchParents(logger, *note.ReplyTo, slingshotURL, session, cache, 10) if err != nil { logger.Warn("failed to fetch parent chain", "uri", note.URI, "error", err) } note.Parents = parents } cache.Set(note.URI, note) posts = append(posts, *note) } return notes.DeduplicateThreads(posts), nil } func FetchParents(logger *slog.Logger, replyToURI string, slingshotURL string, session *auth.OAuthState, cache *Cache, maxDepth int) ([]notes.Note, error) { parents := []notes.Note{} currentURI := replyToURI for currentURI != "" { note, err := GetAppBskyFeedPostByURI(logger, currentURI, slingshotURL, session, cache) if err != nil { logger.Warn("failed to fetch parent", "uri", currentURI, "error", err) note = notes.MissingNote(currentURI) cache.Set(currentURI, note) parents = append(parents, *note) break } // preemptively fetch posts from author to avoid hammering servers if there's a back and forth if _, found := cache.GetProfile(note.AuthorDID); !found { logger.Debug("caching posts from thread participant", "did", note.AuthorDID) authorPDS, err := FetchPDS(note.AuthorDID) if err != nil { logger.Warn("failed to fetch parent", "uri", currentURI, "error", err) break } go FetchAppBskyFeedPosts(logger, note.AuthorDID, authorPDS, slingshotURL, session, cache) } parents = append(parents, *note) if note.ReplyTo == nil { break } currentURI = *note.ReplyTo } return parents, nil }