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)
14
15func ResolveHandle(ctx context.Context, handle string) (string, error) {
16 var did string
17
18 _, err := syntax.ParseHandle(handle)
19 if err != nil {
20 return "", err
21 }
22
23 recs, err := net.LookupTXT(fmt.Sprintf("_atproto.%s", handle))
24 if err == nil {
25 for _, rec := range recs {
26 if strings.HasPrefix(rec, "did=") {
27 did = strings.Split(rec, "did=")[1]
28 break
29 }
30 }
31 } else {
32 fmt.Printf("erorr getting txt records: %v\n", err)
33 }
34
35 if did == "" {
36 req, err := http.NewRequestWithContext(
37 ctx,
38 "GET",
39 fmt.Sprintf("https://%s/.well-known/atproto-did", handle),
40 nil,
41 )
42 if err != nil {
43 return "", nil
44 }
45
46 resp, err := http.DefaultClient.Do(req)
47 if err != nil {
48 return "", nil
49 }
50 defer resp.Body.Close()
51
52 if resp.StatusCode != http.StatusOK {
53 io.Copy(io.Discard, resp.Body)
54 return "", fmt.Errorf("unable to resolve handle")
55 }
56
57 b, err := io.ReadAll(resp.Body)
58 if err != nil {
59 return "", err
60 }
61
62 maybeDid := string(b)
63
64 if _, err := syntax.ParseDID(maybeDid); err != nil {
65 return "", fmt.Errorf("unable to resolve handle")
66 }
67
68 did = maybeDid
69 }
70
71 return did, nil
72}
73
74type DidDoc struct {
75 Context []string `json:"@context"`
76 Id string `json:"id"`
77 AlsoKnownAs []string `json:"alsoKnownAs"`
78 VerificationMethods []DidDocVerificationMethod `json:"verificationMethods"`
79 Service []DidDocService `json:"service"`
80}
81
82type DidDocVerificationMethod struct {
83 Id string `json:"id"`
84 Type string `json:"type"`
85 Controller string `json:"controller"`
86 PublicKeyMultibase string `json:"publicKeyMultibase"`
87}
88
89type DidDocService struct {
90 Id string `json:"id"`
91 Type string `json:"type"`
92 ServiceEndpoint string `json:"serviceEndpoint"`
93}
94
95type DidData struct {
96 Did string `json:"did"`
97 VerificationMethods map[string]string `json:"verificationMethods"`
98 RotationKeys []string `json:"rotationKeys"`
99 AlsoKnownAs []string `json:"alsoKnownAs"`
100 Services map[string]OperationService `json:"services"`
101}
102
103type OperationService struct {
104 Type string `json:"type"`
105 Endpoint string `json:"endpoint"`
106}
107
108type DidLog []DidLogEntry
109
110type DidLogEntry struct {
111 Sig string `json:"sig"`
112 Prev *string `json:"prev"`
113 Type string `json:"string"`
114 Services map[string]OperationService `json:"services"`
115 AlsoKnownAs []string `json:"alsoKnownAs"`
116 RotationKeys []string `json:"rotationKeys"`
117 VerificationMethods map[string]string `json:"verificationMethods"`
118}
119
120type DidAuditEntry struct {
121 Did string `json:"did"`
122 Operation DidLogEntry `json:"operation"`
123 Cid string `json:"cid"`
124 Nullified bool `json:"nullified"`
125 CreatedAt string `json:"createdAt"`
126}
127
128type DidAuditLog []DidAuditEntry
129
130func FetchDidDoc(ctx context.Context, did string) (*DidDoc, error) {
131 var ustr string
132 if strings.HasPrefix(did, "did:plc:") {
133 ustr = fmt.Sprintf("https://plc.directory/%s", did)
134 } else if strings.HasPrefix(did, "did:web:") {
135 ustr = fmt.Sprintf("https://%s/.well-known/did.json", strings.TrimPrefix(did, "did:web:"))
136 } else {
137 return nil, fmt.Errorf("did was not a supported did type")
138 }
139
140 req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil)
141 if err != nil {
142 return nil, err
143 }
144
145 resp, err := http.DefaultClient.Do(req)
146 if err != nil {
147 return nil, err
148 }
149 defer resp.Body.Close()
150
151 if resp.StatusCode != 200 {
152 io.Copy(io.Discard, resp.Body)
153 return nil, fmt.Errorf("could not find identity in plc registry")
154 }
155
156 var diddoc DidDoc
157 if err := json.NewDecoder(resp.Body).Decode(&diddoc); err != nil {
158 return nil, err
159 }
160
161 return &diddoc, nil
162}
163
164func FetchDidData(ctx context.Context, did string) (*DidData, error) {
165 var ustr string
166 ustr = fmt.Sprintf("https://plc.directory/%s/data", did)
167
168 req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil)
169 if err != nil {
170 return nil, err
171 }
172
173 resp, err := http.DefaultClient.Do(req)
174 if err != nil {
175 return nil, err
176 }
177 defer resp.Body.Close()
178
179 if resp.StatusCode != 200 {
180 io.Copy(io.Discard, resp.Body)
181 return nil, fmt.Errorf("could not find identity in plc registry")
182 }
183
184 var diddata DidData
185 if err := json.NewDecoder(resp.Body).Decode(&diddata); err != nil {
186 return nil, err
187 }
188
189 return &diddata, nil
190}
191
192func FetchDidAuditLog(ctx context.Context, did string) (DidAuditLog, error) {
193 var ustr string
194 ustr = fmt.Sprintf("https://plc.directory/%s/log/audit", did)
195
196 req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil)
197 if err != nil {
198 return nil, err
199 }
200
201 resp, err := http.DefaultClient.Do(req)
202 if err != nil {
203 return nil, err
204 }
205 defer resp.Body.Close()
206
207 if resp.StatusCode != 200 {
208 io.Copy(io.Discard, resp.Body)
209 return nil, fmt.Errorf("could not find identity in plc registry")
210 }
211
212 var didlog DidAuditLog
213 if err := json.NewDecoder(resp.Body).Decode(&didlog); err != nil {
214 return nil, err
215 }
216
217 return didlog, nil
218}
219
220func ResolveService(ctx context.Context, did string) (string, error) {
221 diddoc, err := FetchDidDoc(ctx, did)
222 if err != nil {
223 return "", err
224 }
225
226 var service string
227 for _, svc := range diddoc.Service {
228 if svc.Id == "#atproto_pds" {
229 service = svc.ServiceEndpoint
230 }
231 }
232
233 if service == "" {
234 return "", fmt.Errorf("could not find atproto_pds service in identity services")
235 }
236
237 return service, nil
238}