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 string, pdsURL string) (*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) } return &general.Profile{ Handle: handle, DisplayName: response.Value.DisplayName, AvatarURL: avatarURL, BannerURL: bannerURL, Pronouns: response.Value.Pronouns, }, nil } func FetchProfiles(dids []string, pdsURL string) map[string]general.Profile { profiles := make(map[string]general.Profile) for _, did := range dids { profile, err := FetchProfile(did, pdsURL) if err == nil { profiles[did] = *profile } } return profiles }