Write on the margins of the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
at ui-refactor 179 lines 4.4 kB view raw
1package slingshot 2 3import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "net/http" 8 "net/url" 9 "time" 10) 11 12const ( 13 DefaultBaseURL = "https://slingshot.microcosm.blue" 14 DefaultTimeout = 5 * time.Second 15 UserAgent = "Margin (margin.at)" 16) 17 18type Client struct { 19 baseURL string 20 httpClient *http.Client 21} 22 23func NewClient() *Client { 24 return &Client{ 25 baseURL: DefaultBaseURL, 26 httpClient: &http.Client{ 27 Timeout: DefaultTimeout, 28 }, 29 } 30} 31 32func NewClientWithURL(baseURL string) *Client { 33 return &Client{ 34 baseURL: baseURL, 35 httpClient: &http.Client{ 36 Timeout: DefaultTimeout, 37 }, 38 } 39} 40 41type Identity struct { 42 DID string `json:"did"` 43 Handle string `json:"handle"` 44 PDS string `json:"pds"` 45} 46 47type Record struct { 48 URI string `json:"uri"` 49 CID string `json:"cid"` 50 Value json.RawMessage `json:"value"` 51} 52 53func (c *Client) ResolveIdentity(ctx context.Context, identifier string) (*Identity, error) { 54 endpoint := fmt.Sprintf("%s/identity/%s", c.baseURL, url.PathEscape(identifier)) 55 56 req, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil) 57 if err != nil { 58 return nil, fmt.Errorf("failed to create request: %w", err) 59 } 60 req.Header.Set("User-Agent", UserAgent) 61 62 resp, err := c.httpClient.Do(req) 63 if err != nil { 64 return nil, fmt.Errorf("request failed: %w", err) 65 } 66 defer resp.Body.Close() 67 68 if resp.StatusCode == http.StatusNotFound { 69 return nil, fmt.Errorf("identity not found: %s", identifier) 70 } 71 72 if resp.StatusCode != http.StatusOK { 73 return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) 74 } 75 76 var identity Identity 77 if err := json.NewDecoder(resp.Body).Decode(&identity); err != nil { 78 return nil, fmt.Errorf("failed to decode response: %w", err) 79 } 80 81 return &identity, nil 82} 83 84func (c *Client) GetRecord(ctx context.Context, uri string) (*Record, error) { 85 params := url.Values{} 86 params.Set("uri", uri) 87 88 endpoint := fmt.Sprintf("%s/record?%s", c.baseURL, params.Encode()) 89 90 req, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil) 91 if err != nil { 92 return nil, fmt.Errorf("failed to create request: %w", err) 93 } 94 req.Header.Set("User-Agent", UserAgent) 95 96 resp, err := c.httpClient.Do(req) 97 if err != nil { 98 return nil, fmt.Errorf("request failed: %w", err) 99 } 100 defer resp.Body.Close() 101 102 if resp.StatusCode == http.StatusNotFound { 103 return nil, fmt.Errorf("record not found: %s", uri) 104 } 105 106 if resp.StatusCode != http.StatusOK { 107 return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) 108 } 109 110 var record Record 111 if err := json.NewDecoder(resp.Body).Decode(&record); err != nil { 112 return nil, fmt.Errorf("failed to decode response: %w", err) 113 } 114 115 return &record, nil 116} 117 118func (c *Client) GetRecordByParts(ctx context.Context, repo, collection, rkey string) (*Record, error) { 119 uri := fmt.Sprintf("at://%s/%s/%s", repo, collection, rkey) 120 return c.GetRecord(ctx, uri) 121} 122 123type ListRecordsResponse struct { 124 Records []Record `json:"records"` 125 Cursor string `json:"cursor,omitempty"` 126} 127 128func (c *Client) ListRecords(ctx context.Context, repo, collection string, limit int, cursor string) (*ListRecordsResponse, error) { 129 params := url.Values{} 130 params.Set("repo", repo) 131 params.Set("collection", collection) 132 if limit > 0 { 133 params.Set("limit", fmt.Sprintf("%d", limit)) 134 } 135 if cursor != "" { 136 params.Set("cursor", cursor) 137 } 138 139 endpoint := fmt.Sprintf("%s/records?%s", c.baseURL, params.Encode()) 140 141 req, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil) 142 if err != nil { 143 return nil, fmt.Errorf("failed to create request: %w", err) 144 } 145 req.Header.Set("User-Agent", UserAgent) 146 147 resp, err := c.httpClient.Do(req) 148 if err != nil { 149 return nil, fmt.Errorf("request failed: %w", err) 150 } 151 defer resp.Body.Close() 152 153 if resp.StatusCode != http.StatusOK { 154 return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) 155 } 156 157 var listResp ListRecordsResponse 158 if err := json.NewDecoder(resp.Body).Decode(&listResp); err != nil { 159 return nil, fmt.Errorf("failed to decode response: %w", err) 160 } 161 162 return &listResp, nil 163} 164 165func (c *Client) ResolveDID(ctx context.Context, did string) (string, error) { 166 identity, err := c.ResolveIdentity(ctx, did) 167 if err != nil { 168 return "", err 169 } 170 return identity.PDS, nil 171} 172 173func (c *Client) ResolveHandle(ctx context.Context, handle string) (string, error) { 174 identity, err := c.ResolveIdentity(ctx, handle) 175 if err != nil { 176 return "", err 177 } 178 return identity.DID, nil 179}