An atproto PDS written in Go
103
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 89013a78a2b75e58225f790f182b3f8b554deea3 222 lines 5.1 kB view raw
1package identity 2 3import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net" 9 "net/http" 10 "strings" 11 12 "github.com/bluesky-social/indigo/atproto/syntax" 13 "github.com/bluesky-social/indigo/util" 14) 15 16func ResolveHandleFromTXT(ctx context.Context, handle string) (string, error) { 17 name := fmt.Sprintf("_atproto.%s", handle) 18 recs, err := net.LookupTXT(name) 19 if err != nil { 20 return "", fmt.Errorf("handle could not be resolved via txt: %w", err) 21 } 22 23 for _, rec := range recs { 24 if strings.HasPrefix(rec, "did=") { 25 maybeDid := strings.Split(rec, "did=")[1] 26 if _, err := syntax.ParseDID(maybeDid); err == nil { 27 return maybeDid, nil 28 } 29 } 30 } 31 32 return "", fmt.Errorf("handle could not be resolved via txt: no record found") 33} 34 35func ResolveHandleFromWellKnown(ctx context.Context, cli *http.Client, handle string) (string, error) { 36 ustr := fmt.Sprintf("https://%s/.well-known/atproto-did", handle) 37 req, err := http.NewRequestWithContext( 38 ctx, 39 "GET", 40 ustr, 41 nil, 42 ) 43 if err != nil { 44 return "", fmt.Errorf("handle could not be resolved via web: %w", err) 45 } 46 47 resp, err := cli.Do(req) 48 if err != nil { 49 return "", fmt.Errorf("handle could not be resolved via web: %w", err) 50 } 51 defer resp.Body.Close() 52 53 b, err := io.ReadAll(resp.Body) 54 if err != nil { 55 return "", fmt.Errorf("handle could not be resolved via web: %w", err) 56 } 57 58 if resp.StatusCode != http.StatusOK { 59 return "", fmt.Errorf("handle could not be resolved via web: invalid status code %d", resp.StatusCode) 60 } 61 62 maybeDid := string(b) 63 64 if _, err := syntax.ParseDID(maybeDid); err != nil { 65 return "", fmt.Errorf("handle could not be resolved via web: invalid did in document") 66 } 67 68 return maybeDid, nil 69} 70 71func ResolveHandle(ctx context.Context, cli *http.Client, handle string) (string, error) { 72 if cli == nil { 73 cli = util.RobustHTTPClient() 74 } 75 76 _, err := syntax.ParseHandle(handle) 77 if err != nil { 78 return "", err 79 } 80 81 if maybeDidFromTxt, err := ResolveHandleFromTXT(ctx, handle); err == nil { 82 return maybeDidFromTxt, nil 83 } 84 85 if maybeDidFromWeb, err := ResolveHandleFromWellKnown(ctx, cli, handle); err == nil { 86 return maybeDidFromWeb, nil 87 } 88 89 return "", fmt.Errorf("handle could not be resolved") 90} 91 92func DidToDocUrl(did string) (string, error) { 93 if strings.HasPrefix(did, "did:plc:") { 94 return fmt.Sprintf("https://plc.directory/%s", did), nil 95 } else if after, ok := strings.CutPrefix(did, "did:web:"); ok { 96 return fmt.Sprintf("https://%s/.well-known/did.json", after), nil 97 } else { 98 return "", fmt.Errorf("did was not a supported did type") 99 } 100} 101 102func FetchDidDoc(ctx context.Context, cli *http.Client, did string) (*DidDoc, error) { 103 if cli == nil { 104 cli = util.RobustHTTPClient() 105 } 106 107 ustr, err := DidToDocUrl(did) 108 if err != nil { 109 return nil, err 110 } 111 112 req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil) 113 if err != nil { 114 return nil, err 115 } 116 117 resp, err := cli.Do(req) 118 if err != nil { 119 return nil, err 120 } 121 defer resp.Body.Close() 122 123 if resp.StatusCode != 200 { 124 io.Copy(io.Discard, resp.Body) 125 return nil, fmt.Errorf("unable to find did doc at url. did: %s. url: %s", did, ustr) 126 } 127 128 var diddoc DidDoc 129 if err := json.NewDecoder(resp.Body).Decode(&diddoc); err != nil { 130 return nil, err 131 } 132 133 return &diddoc, nil 134} 135 136func FetchDidData(ctx context.Context, cli *http.Client, did string) (*DidData, error) { 137 if cli == nil { 138 cli = util.RobustHTTPClient() 139 } 140 141 var ustr string 142 ustr = fmt.Sprintf("https://plc.directory/%s/data", did) 143 144 req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil) 145 if err != nil { 146 return nil, err 147 } 148 149 resp, err := cli.Do(req) 150 if err != nil { 151 return nil, err 152 } 153 defer resp.Body.Close() 154 155 if resp.StatusCode != 200 { 156 io.Copy(io.Discard, resp.Body) 157 return nil, fmt.Errorf("could not find identity in plc registry") 158 } 159 160 var diddata DidData 161 if err := json.NewDecoder(resp.Body).Decode(&diddata); err != nil { 162 return nil, err 163 } 164 165 return &diddata, nil 166} 167 168func FetchDidAuditLog(ctx context.Context, cli *http.Client, did string) (DidAuditLog, error) { 169 if cli == nil { 170 cli = util.RobustHTTPClient() 171 } 172 173 var ustr string 174 ustr = fmt.Sprintf("https://plc.directory/%s/log/audit", did) 175 176 req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil) 177 if err != nil { 178 return nil, err 179 } 180 181 resp, err := http.DefaultClient.Do(req) 182 if err != nil { 183 return nil, err 184 } 185 defer resp.Body.Close() 186 187 if resp.StatusCode != 200 { 188 io.Copy(io.Discard, resp.Body) 189 return nil, fmt.Errorf("could not find identity in plc registry") 190 } 191 192 var didlog DidAuditLog 193 if err := json.NewDecoder(resp.Body).Decode(&didlog); err != nil { 194 return nil, err 195 } 196 197 return didlog, nil 198} 199 200func ResolveService(ctx context.Context, cli *http.Client, did string) (string, error) { 201 if cli == nil { 202 cli = util.RobustHTTPClient() 203 } 204 205 diddoc, err := FetchDidDoc(ctx, cli, did) 206 if err != nil { 207 return "", err 208 } 209 210 var service string 211 for _, svc := range diddoc.Service { 212 if svc.Id == "#atproto_pds" { 213 service = svc.ServiceEndpoint 214 } 215 } 216 217 if service == "" { 218 return "", fmt.Errorf("could not find atproto_pds service in identity services") 219 } 220 221 return service, nil 222}