1package xrpc
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "log"
8 "net/http"
9 "regexp"
10 "strings"
11 "time"
12
13 "margin.at/internal/config"
14 "margin.at/internal/slingshot"
15)
16
17var SlingshotClient = slingshot.NewClient()
18
19var (
20 didPattern = regexp.MustCompile(`^did:[a-z]+:[a-zA-Z0-9._:%-]+$`)
21 nsidPattern = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9]*(\.[a-zA-Z][a-zA-Z0-9]*)+$`)
22 rkeyPattern = regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)
23)
24
25type ATURI struct {
26 DID string
27 Collection string
28 RKey string
29}
30
31func ParseATURI(uri string) (*ATURI, error) {
32 if !strings.HasPrefix(uri, "at://") {
33 return nil, fmt.Errorf("invalid AT-URI: must start with at://")
34 }
35
36 path := strings.TrimPrefix(uri, "at://")
37 parts := strings.Split(path, "/")
38
39 if len(parts) < 1 || parts[0] == "" {
40 return nil, fmt.Errorf("invalid AT-URI: missing DID authority")
41 }
42
43 did := parts[0]
44 if !didPattern.MatchString(did) {
45 return nil, fmt.Errorf("invalid AT-URI: malformed DID %q", did)
46 }
47
48 result := &ATURI{DID: did}
49
50 if len(parts) >= 2 && parts[1] != "" {
51 collection := parts[1]
52 if !nsidPattern.MatchString(collection) {
53 return nil, fmt.Errorf("invalid AT-URI: malformed collection NSID %q", collection)
54 }
55 result.Collection = collection
56 }
57
58 if len(parts) >= 3 && parts[2] != "" {
59 rkey := parts[2]
60 if !rkeyPattern.MatchString(rkey) || strings.HasPrefix(rkey, ".") || strings.HasSuffix(rkey, ".") {
61 return nil, fmt.Errorf("invalid AT-URI: malformed record key %q", rkey)
62 }
63 if len(rkey) > 512 {
64 return nil, fmt.Errorf("invalid AT-URI: record key too long (max 512)")
65 }
66 result.RKey = rkey
67 }
68
69 if len(parts) > 3 {
70 return nil, fmt.Errorf("invalid AT-URI: too many path segments")
71 }
72
73 return result, nil
74}
75
76func (a *ATURI) String() string {
77 if a.Collection == "" {
78 return fmt.Sprintf("at://%s", a.DID)
79 }
80 if a.RKey == "" {
81 return fmt.Sprintf("at://%s/%s", a.DID, a.Collection)
82 }
83 return fmt.Sprintf("at://%s/%s/%s", a.DID, a.Collection, a.RKey)
84}
85
86func init() {
87 log.Printf("Slingshot client initialized: %s", slingshot.DefaultBaseURL)
88}
89
90func ResolveDIDToPDS(did string) (string, error) {
91 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
92 defer cancel()
93
94 if pds, err := SlingshotClient.ResolveDID(ctx, did); err == nil && pds != "" {
95 return pds, nil
96 }
97
98 return resolveDIDToPDSDirect(did)
99}
100
101func resolveDIDToPDSDirect(did string) (string, error) {
102 var docURL string
103 if strings.HasPrefix(did, "did:plc:") {
104 docURL = config.Get().PLCResolveURL(did)
105 } else if strings.HasPrefix(did, "did:web:") {
106 domain := strings.TrimPrefix(did, "did:web:")
107 docURL = fmt.Sprintf("https://%s/.well-known/did.json", domain)
108 } else {
109 return "", nil
110 }
111
112 client := &http.Client{
113 Timeout: 10 * time.Second,
114 }
115 resp, err := client.Get(docURL)
116 if err != nil {
117 return "", err
118 }
119 defer resp.Body.Close()
120
121 if resp.StatusCode != 200 {
122 return "", fmt.Errorf("failed to fetch DID doc: %d", resp.StatusCode)
123 }
124
125 var doc struct {
126 Service []struct {
127 ID string `json:"id"`
128 Type string `json:"type"`
129 ServiceEndpoint string `json:"serviceEndpoint"`
130 } `json:"service"`
131 }
132 if err := json.NewDecoder(resp.Body).Decode(&doc); err != nil {
133 return "", err
134 }
135
136 for _, svc := range doc.Service {
137 if svc.ID == "#atproto_pds" && svc.Type == "AtprotoPersonalDataServer" {
138 return svc.ServiceEndpoint, nil
139 }
140 }
141 for _, svc := range doc.Service {
142 if svc.Type == "AtprotoPersonalDataServer" {
143 return svc.ServiceEndpoint, nil
144 }
145 }
146 return "", nil
147}
148
149func ResolveHandle(handle string) (string, error) {
150 if strings.HasPrefix(handle, "did:") {
151 return handle, nil
152 }
153
154 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
155 defer cancel()
156
157 if did, err := SlingshotClient.ResolveHandle(ctx, handle); err == nil && did != "" {
158 return did, nil
159 }
160
161 return resolveHandleDirect(handle)
162}
163
164func resolveHandleDirect(handle string) (string, error) {
165 url := config.Get().BskyResolveHandleURL(handle)
166 client := &http.Client{
167 Timeout: 5 * time.Second,
168 }
169 resp, err := client.Get(url)
170 if err != nil {
171 return "", err
172 }
173 defer resp.Body.Close()
174
175 if resp.StatusCode != 200 {
176 return "", fmt.Errorf("failed to resolve handle: %d", resp.StatusCode)
177 }
178
179 var result struct {
180 DID string `json:"did"`
181 }
182 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
183 return "", err
184 }
185 return result.DID, nil
186}