diff options
Diffstat (limited to 'backend/fetchers/app.bsky.actor.go')
| -rw-r--r-- | backend/fetchers/app.bsky.actor.go | 184 |
1 files changed, 184 insertions, 0 deletions
diff --git a/backend/fetchers/app.bsky.actor.go b/backend/fetchers/app.bsky.actor.go new file mode 100644 index 0000000..3c0154a --- /dev/null +++ b/backend/fetchers/app.bsky.actor.go @@ -0,0 +1,184 @@ +package fetchers + +import ( + "encoding/json" + "fmt" + "io" + "llostre/backend/general" + "net" + "net/http" + "strings" +) + +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 FetchProfile(did, pdsURL string, cache *Cache) (*general.Profile, error) { + recordURL := fmt.Sprintf("%s/xrpc/com.atproto.repo.getRecord?repo=%s&collection=app.bsky.actor.profile&rkey=self", pdsURL, did) + + resp, err := http.Get(recordURL) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("profile fetch failed: %d", resp.StatusCode) + } + + handle, err := FetchHandle(did) + if err != nil { + handle = "handle.invalid" + } + + var response general.ProfileRecordResponse + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + return nil, err + } + + var avatarURL, bannerURL string + if response.Value.Avatar != nil { + avatarURL = fmt.Sprintf("https://cdn.bsky.app/img/avatar/plain/%s/%s@jpeg", did, response.Value.Avatar.Ref.Link) + } + if response.Value.Banner != nil { + bannerURL = fmt.Sprintf("https://cdn.bsky.app/img/banner/plain/%s/%s@jpeg", did, response.Value.Banner.Ref.Link) + } + + profile := &general.Profile{ + Handle: handle, + DisplayName: response.Value.DisplayName, + AvatarURL: avatarURL, + BannerURL: bannerURL, + Pronouns: response.Value.Pronouns, + } + + cache.SetProfile(handle, profile) + return profile, nil +} + +func FetchProfiles(dids []string, pdsURL string, cache *Cache) map[string]general.Profile { + profiles := make(map[string]general.Profile) + for _, did := range dids { + profile, err := FetchProfile(did, pdsURL, cache) + if err == nil { + cache.SetProfile(did, profile) + profiles[did] = *profile + } + } + return profiles +} |
