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 }