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