aboutsummaryrefslogtreecommitdiff
path: root/fetchers/app.bsky.feed.go
diff options
context:
space:
mode:
Diffstat (limited to 'fetchers/app.bsky.feed.go')
-rw-r--r--fetchers/app.bsky.feed.go217
1 files changed, 217 insertions, 0 deletions
diff --git a/fetchers/app.bsky.feed.go b/fetchers/app.bsky.feed.go
new file mode 100644
index 0000000..5eebec9
--- /dev/null
+++ b/fetchers/app.bsky.feed.go
@@ -0,0 +1,217 @@
+package fetchers
+
+import (
+ "encoding/json"
+ "fmt"
+ "llostre/auth"
+ "llostre/general"
+ "llostre/notes"
+ "time"
+)
+
+func FetchAppBskyFeedPosts(conf *Config, did, pdsURL string, session *auth.OAuthState) ([]notes.Note, error) {
+ // get author of the note
+ author, found := conf.Cache.GetAuthor(did)
+ var err error
+ if !found {
+ author, err = FetchAppBskyActorProfile(conf, did, pdsURL, session)
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ // try slingshot first if given
+ if conf.SlingshotURL != "" && general.SlingshotAvailable() {
+ records, err := general.FetchRawRecordCollection(conf.Logger, did, "app.bsky.feed.post", conf.SlingshotURL, session)
+ if err != nil {
+ conf.Logger.Error("fetching from slingshot failed, falling back to PDS")
+ general.SetSlingshotBackoff(5 * time.Minute)
+ } else {
+ return parseAppBskyFeedPostCollection(conf, session, records, author)
+ }
+ }
+
+ records, err := general.FetchRawRecordCollection(conf.Logger, did, "app.bsky.feed.post", pdsURL, session)
+ if err != nil {
+ return nil, err
+ }
+
+ return parseAppBskyFeedPostCollection(conf, session, records, author)
+}
+
+func FetchAppBskyFeedPostsWithCursor(conf *Config, did, pdsURL string, session *auth.OAuthState, cursor string) ([]notes.Note, error) {
+ author, found := conf.Cache.GetAuthor(did)
+ var err error
+ if !found {
+ author, err = FetchAppBskyActorProfile(conf, did, pdsURL, session)
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ if conf.SlingshotURL != "" && general.SlingshotAvailable() {
+ records, nextCursor, err := general.FetchRawRecordCollectionWithCursor(conf.Logger, did, "app.bsky.feed.post", conf.SlingshotURL, session, cursor, conf.PageSize)
+ if err != nil {
+ conf.Logger.Error("fetching from slingshot failed, falling back to PDS")
+ general.SetSlingshotBackoff(5 * time.Minute)
+ } else {
+ notes, err := parseAppBskyFeedPostCollection(conf, session, records, author)
+ conf.Cache.SetCursor(did, nextCursor)
+ return notes, err
+ }
+ }
+
+ records, nextCursor, err := general.FetchRawRecordCollectionWithCursor(conf.Logger, did, "app.bsky.feed.post", pdsURL, session, cursor, conf.PageSize)
+ if err != nil {
+ return nil, err
+ }
+
+ notes, err := parseAppBskyFeedPostCollection(conf, session, records, author)
+
+ conf.Cache.SetCursor(did, nextCursor)
+ return notes, err
+}
+
+func GetAppBskyFeedPostByURI(conf *Config, uri string, session *auth.OAuthState) (*notes.Note, error) {
+ // check cache, return if it was cached without auth or if it was cached with auth and we have auth
+ conf.Logger.Debug("Getting app.bsky.feed.post", "uri", uri)
+ if cached, found := conf.Cache.Get(uri); found {
+ conf.Logger.Debug("Found in the cache", "public", !cached.AuthorNoUnauth)
+ if !cached.AuthorNoUnauth {
+ return cached, nil
+ }
+
+ // author requires auth - if we have a session, serve it
+ // if the token was invalid, trust that it would have been caught by the "refresh token" call in main
+ if session != nil {
+ return cached, nil
+ }
+ }
+
+ return FetchAppBskyFeedPostByURI(conf, uri, session)
+}
+
+func FetchAppBskyFeedPostByURI(conf *Config, uri string, session *auth.OAuthState) (*notes.Note, error) {
+ repo, _, _, err := general.ParseURI(uri)
+
+ // need to resolve DID to PDS
+ pdsURL, err := FetchPDS(repo)
+ if err != nil {
+ return nil, err
+ }
+ conf.Logger.Debug("fetched origin PDS", "did", repo, "pdsURL", pdsURL)
+
+ // get author of the note
+ author, found := conf.Cache.GetAuthor(repo)
+ if !found {
+ author, err = FetchAppBskyActorProfile(conf, repo, pdsURL, session)
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ // try slingshot first if given
+ if conf.SlingshotURL != "" && general.SlingshotAvailable() {
+ rawRecord, err := general.FetchRawRecordByURI(conf.Logger, uri, conf.SlingshotURL, session)
+ if err != nil {
+ conf.Logger.Error("fetching from slingshot failed", "error", err)
+ general.SetSlingshotBackoff(5 * time.Minute)
+ } else {
+ return parseAppBskyFeedPostRecord(rawRecord, author, conf)
+ }
+ }
+
+ rawRecord, err := general.FetchRawRecordByURI(conf.Logger, uri, pdsURL, session)
+ if err != nil {
+ conf.Logger.Error("Fetching from PDS failed")
+ return nil, err
+ }
+
+ return parseAppBskyFeedPostRecord(rawRecord, author, conf)
+}
+
+func parseAppBskyFeedPostRecord(rawRecord *general.RawRecord, author *notes.Author, conf *Config) (*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
+ conf.Cache.Set(note.URI, note)
+
+ conf.Logger.Debug("parsed note", "uri", note.URI, "media_count", len(note.Media), "embed", record.Value.Embed)
+
+ return note, nil
+}
+
+func parseAppBskyFeedPostCollection(conf *Config, session *auth.OAuthState, records []general.RawRecord, author *notes.Author) ([]notes.Note, error) {
+ posts := make([]notes.Note, 0, len(records))
+ for _, record := range records {
+ note, err := parseAppBskyFeedPostRecord(&record, author, conf)
+ if err != nil {
+ conf.Logger.Error("failed to parse post record", "error", err)
+ continue
+ }
+ if note.ReplyTo != nil {
+ parents, err := FetchParents(conf, *note.ReplyTo, session, 10)
+ if err != nil {
+ conf.Logger.Warn("failed to fetch parent chain", "uri", note.URI, "error", err)
+ }
+ note.Parents = parents
+ }
+ conf.Cache.Set(note.URI, note)
+ posts = append(posts, *note)
+ }
+
+ return notes.DeduplicateThreads(posts), nil
+}
+
+func FetchParents(conf *Config, replyToURI string, session *auth.OAuthState, maxDepth int) ([]notes.Note, error) {
+ parents := []notes.Note{}
+ currentURI := replyToURI
+
+ for currentURI != "" {
+ note, err := GetAppBskyFeedPostByURI(conf, currentURI, session)
+ if err != nil {
+ conf.Logger.Warn("failed to fetch parent", "uri", currentURI, "error", err)
+ note = notes.MissingNote(currentURI)
+
+ conf.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 := conf.Cache.GetAuthor(note.AuthorDID); !found {
+ conf.Logger.Debug("caching posts from thread participant", "did", note.AuthorDID)
+ authorPDS, err := FetchPDS(note.AuthorDID)
+ if err != nil {
+ conf.Logger.Warn("failed to fetch parent", "uri", currentURI, "error", err)
+ break
+ }
+ go FetchAppBskyFeedPostsWithCursor(conf, note.AuthorDID, authorPDS, session, "")
+ }
+
+ parents = append(parents, *note)
+
+ if note.ReplyTo == nil {
+ break
+ }
+
+ currentURI = *note.ReplyTo
+ }
+
+ return parents, nil
+}