aboutsummaryrefslogtreecommitdiff
path: root/fetchers/app.bsky.actor.go
diff options
context:
space:
mode:
Diffstat (limited to 'fetchers/app.bsky.actor.go')
-rw-r--r--fetchers/app.bsky.actor.go184
1 files changed, 184 insertions, 0 deletions
diff --git a/fetchers/app.bsky.actor.go b/fetchers/app.bsky.actor.go
new file mode 100644
index 0000000..ebac250
--- /dev/null
+++ b/fetchers/app.bsky.actor.go
@@ -0,0 +1,184 @@
+package fetchers
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "llostre/auth"
+ "llostre/general"
+ "llostre/notes"
+ "net"
+ "net/http"
+ "strings"
+ "time"
+)
+
+type DIDDocument struct {
+ AlsoKnownAs []string `json:"alsoKnownAs"`
+ Service []struct {
+ ID string `json:"id"`
+ Type string `json:"type"`
+ ServiceEndpoint string `json:"serviceEndpoint"`
+ } `json:"service"`
+}
+
+func FetchDID(handle string) (string, error) {
+ // return if it's a DID already
+ if strings.HasPrefix(handle, "did:") {
+ return handle, nil
+ }
+
+ // resolve DNS record first
+ txtRecords, err := net.LookupTXT("_atproto." + handle)
+ if err == nil {
+ for _, txt := range txtRecords {
+ if after, ok := strings.CutPrefix(txt, "did="); ok {
+ return after, nil
+ }
+ }
+ }
+
+ // fallback to HTTPS well-known
+ resp, err := http.Get(fmt.Sprintf("https://%s/.well-known/atproto-did", handle))
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return "", err
+ }
+
+ did := strings.TrimSpace(string(body))
+ if !strings.HasPrefix(did, "did:") {
+ return "", fmt.Errorf("invalid DID returned: %s", did)
+ }
+
+ return did, nil
+}
+
+func FetchDidDoc(did string) (*DIDDocument, error) {
+ didDocURL := fmt.Sprintf("https://plc.directory/%s", did)
+
+ resp, err := http.Get(didDocURL)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != 200 {
+ return nil, fmt.Errorf("DID resolution failed: %d", resp.StatusCode)
+ }
+
+ var didDoc DIDDocument
+ if err := json.NewDecoder(resp.Body).Decode(&didDoc); err != nil {
+ return nil, err
+ }
+
+ return &didDoc, nil
+}
+
+func FetchHandle(did string) (string, error) {
+ var didDoc, err = FetchDidDoc(did)
+ if err != nil {
+ return "", err
+ }
+
+ if len(didDoc.AlsoKnownAs) == 0 {
+ return "", fmt.Errorf("no handle found")
+ }
+
+ handle := strings.TrimPrefix(didDoc.AlsoKnownAs[0], "at://")
+ return handle, nil
+}
+
+func FetchPDS(did string) (string, error) {
+ var didDoc, err = FetchDidDoc(did)
+ if err != nil {
+ return "", err
+ }
+
+ for _, svc := range didDoc.Service {
+ if svc.Type == "AtprotoPersonalDataServer" {
+ return svc.ServiceEndpoint, nil
+ }
+ }
+
+ return "", fmt.Errorf("no PDS found for DID %s", did)
+}
+
+func FetchHandleAndPDS(did string) (string, string, error) {
+ var didDoc, err = FetchDidDoc(did)
+ if err != nil {
+ return "", "", err
+ }
+
+ if len(didDoc.AlsoKnownAs) == 0 {
+ return "", "", fmt.Errorf("no handle found")
+ }
+
+ var pds string
+ for _, svc := range didDoc.Service {
+ if svc.Type == "AtprotoPersonalDataServer" {
+ pds = svc.ServiceEndpoint
+ break
+ }
+ }
+
+ handle := strings.TrimPrefix(didDoc.AlsoKnownAs[0], "at://")
+
+ return handle, pds, nil
+}
+
+func FetchAppBskyActorProfile(conf *Config, did, pdsURL string, session *auth.OAuthState) (*notes.Author, error) {
+ // construct profile record URI
+ uri := fmt.Sprintf("at://%s/app.bsky.actor.profile/self", did)
+
+ // try slingshot first
+ if conf.SlingshotURL != "" && general.SlingshotAvailable() {
+ rawRecord, err := general.FetchRawRecordByURI(conf.Logger, uri, conf.SlingshotURL, session)
+ if err != nil {
+ conf.Logger.Warn("slingshot profile fetch failed", "did", did, "error", err)
+ general.SetSlingshotBackoff(5 * time.Minute)
+ } else {
+ return parseAppBskyActorProfile(did, pdsURL, rawRecord, conf.Cache)
+ }
+ }
+
+ // fallback to PDS
+ rawRecord, err := general.FetchRawRecordByURI(conf.Logger, uri, pdsURL, session)
+ if err != nil {
+ return nil, err
+ }
+
+ return parseAppBskyActorProfile(did, pdsURL, rawRecord, conf.Cache)
+}
+
+func FetchAppBskyActorProfiles(conf *Config, dids []string, pdsURL string, session *auth.OAuthState) map[string]notes.Author {
+ authors := make(map[string]notes.Author)
+ for _, did := range dids {
+ author, err := FetchAppBskyActorProfile(conf, did, pdsURL, session)
+ if err == nil {
+ authors[did] = *author
+ }
+ }
+ return authors
+}
+
+func parseAppBskyActorProfile(did, pds string, rawRecord *general.RawRecord, cache *Cache) (*notes.Author, error) {
+ var profile notes.AppBskyActorProfileRecord
+ if err := json.Unmarshal(rawRecord.Value, &profile); err != nil {
+ return nil, err
+ }
+
+ handle, err := FetchHandle(did)
+ if err != nil {
+ handle = "handle.invalid"
+ }
+
+ author, err := notes.AuthorFromAppBskyActorProfileRecord(profile, did, handle, pds)
+
+ cache.SetAuthor(did, author)
+ return author, nil
+}