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