Write on the margins of the internet. Powered by the AT Protocol.
margin.at
extension
web
atproto
comments
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}