···6 "encoding/json"
7 "fmt"
8 "net/http"
0910 "github.com/bluesky-social/indigo/atproto/syntax"
11)
1213type APIClient struct {
14- HTTPClient *http.Client
00015 Host string
0016 Auth AuthMethod
17- DefaultHeaders map[string]string
0018}
1920// High-level helper for simple JSON "Query" API calls.
21//
22// Does not work with all API endpoints. For more control, use the Do() method with APIRequest.
23-func (c *APIClient) Get(ctx context.Context, endpoint syntax.NSID, params map[string]string, out any) error {
24- hdr := map[string]string{
25- "Accept": "application/json",
26 }
27 req := APIRequest{
28- HTTPVerb: "GET",
29 Endpoint: endpoint,
30 Body: nil,
31 QueryParams: params,
···35 if err != nil {
36 return err
37 }
38-39 defer resp.Body.Close()
40- // TODO: duplicate error handling with Post()?
41 if !(resp.StatusCode >= 200 && resp.StatusCode < 300) {
42 var eb ErrorBody
43 if err := json.NewDecoder(resp.Body).Decode(&eb); err != nil {
···56 return nil
57}
5859-// High-level helper for simple JSON-to-JSON "Procedure" API calls.
60//
61-// Does not work with all API endpoints. For more control, use the Do() method with APIRequest.
62func (c *APIClient) Post(ctx context.Context, endpoint syntax.NSID, body any, out any) error {
63 bodyJSON, err := json.Marshal(body)
64 if err != nil {
65 return err
66 }
67- hdr := map[string]string{
68- "Accept": "application/json",
69- "Content-Type": "application/json",
70 }
71 req := APIRequest{
72- HTTPVerb: "POST",
73 Endpoint: endpoint,
74 Body: bytes.NewReader(bodyJSON),
75 QueryParams: nil,
···79 if err != nil {
80 return err
81 }
82-83 defer resp.Body.Close()
84- // TODO: duplicate error handling with Get()?
85 if !(resp.StatusCode >= 200 && resp.StatusCode < 300) {
86 var eb ErrorBody
87 if err := json.NewDecoder(resp.Body).Decode(&eb); err != nil {
···101}
102103// Full-power method for atproto API requests.
00104func (c *APIClient) Do(ctx context.Context, req APIRequest) (*http.Response, error) {
105 httpReq, err := req.HTTPRequest(ctx, c.Host, c.DefaultHeaders)
106 if err != nil {
107 return nil, err
108 }
109110- // TODO: thread-safe?
111- if c.HTTPClient == nil {
112- c.HTTPClient = http.DefaultClient
113 }
114115 var resp *http.Response
116 if c.Auth != nil {
117- resp, err = c.Auth.DoWithAuth(c.HTTPClient, httpReq)
118 } else {
119- resp, err = c.HTTPClient.Do(httpReq)
120 }
121 if err != nil {
122 return nil, err
123 }
124- // TODO: handle some common response errors: rate-limits, 5xx, auth required, etc
125 return resp, nil
126}
0000000000
···6 "encoding/json"
7 "fmt"
8 "net/http"
9+ "net/url"
1011 "github.com/bluesky-social/indigo/atproto/syntax"
12)
1314type APIClient struct {
15+ // inner HTTP client
16+ Client *http.Client
17+18+ // host URL prefix: scheme, hostname, and port. This field is required.
19 Host string
20+21+ // optional auth client "middleware"
22 Auth AuthMethod
23+24+ // optional HTTP headers which will be included in all requests. Only a single value per key is included; request-level headers will override any client-level defaults.
25+ DefaultHeaders http.Header
26}
2728// High-level helper for simple JSON "Query" API calls.
29//
30// Does not work with all API endpoints. For more control, use the Do() method with APIRequest.
31+func (c *APIClient) Get(ctx context.Context, endpoint syntax.NSID, params url.Values, out any) error {
32+ hdr := map[string][]string{
33+ "Accept": []string{"application/json"},
34 }
35 req := APIRequest{
36+ Method: http.MethodGet,
37 Endpoint: endpoint,
38 Body: nil,
39 QueryParams: params,
···43 if err != nil {
44 return err
45 }
046 defer resp.Body.Close()
47+48 if !(resp.StatusCode >= 200 && resp.StatusCode < 300) {
49 var eb ErrorBody
50 if err := json.NewDecoder(resp.Body).Decode(&eb); err != nil {
···63 return nil
64}
6566+// High-level helper for simple JSON-to-JSON "Procedure" API calls, with no query params.
67//
68+// Does not work with all possible atproto API endpoints. For more control, use the Do() method with APIRequest.
69func (c *APIClient) Post(ctx context.Context, endpoint syntax.NSID, body any, out any) error {
70 bodyJSON, err := json.Marshal(body)
71 if err != nil {
72 return err
73 }
74+ hdr := map[string][]string{
75+ "Accept": []string{"application/json"},
76+ "Content-Type": []string{"application/json"},
77 }
78 req := APIRequest{
79+ Method: http.MethodPost,
80 Endpoint: endpoint,
81 Body: bytes.NewReader(bodyJSON),
82 QueryParams: nil,
···86 if err != nil {
87 return err
88 }
089 defer resp.Body.Close()
90+91 if !(resp.StatusCode >= 200 && resp.StatusCode < 300) {
92 var eb ErrorBody
93 if err := json.NewDecoder(resp.Body).Decode(&eb); err != nil {
···107}
108109// Full-power method for atproto API requests.
110+//
111+// NOTE: this does not currently parse error response JSON body, thought it might in the future.
112func (c *APIClient) Do(ctx context.Context, req APIRequest) (*http.Response, error) {
113 httpReq, err := req.HTTPRequest(ctx, c.Host, c.DefaultHeaders)
114 if err != nil {
115 return nil, err
116 }
117118+ // NOTE: this updates the client object itself
119+ if c.Client == nil {
120+ c.Client = http.DefaultClient
121 }
122123 var resp *http.Response
124 if c.Auth != nil {
125+ resp, err = c.Auth.DoWithAuth(c.Client, httpReq)
126 } else {
127+ resp, err = c.Client.Do(httpReq)
128 }
129 if err != nil {
130 return nil, err
131 }
0132 return resp, nil
133}
134+135+func NewPublicClient(host string) *APIClient {
136+ return &APIClient{
137+ Client: http.DefaultClient,
138+ Host: host,
139+ DefaultHeaders: map[string][]string{
140+ "User-Agent": []string{"indigo-sdk"},
141+ },
142+ }
143+}
+42-14
atproto/client/api_request.go
···3import (
4 "context"
5 "io"
06 "net/http"
7 "net/url"
89 "github.com/bluesky-social/indigo/atproto/syntax"
10)
110000000012type APIRequest struct {
13- HTTPVerb string // TODO: type?
00014 Endpoint syntax.NSID
0015 Body io.Reader
16- QueryParams map[string]string // TODO: better type for this?
17- Headers map[string]string
000018}
1920-func (r *APIRequest) HTTPRequest(ctx context.Context, host string, headers map[string]string) (*http.Request, error) {
000021 u, err := url.Parse(host)
22 if err != nil {
23 return nil, err
24 }
00000000025 u.Path = "/xrpc/" + r.Endpoint.String()
026 if r.QueryParams != nil {
27- q := u.Query()
28- for k, v := range r.QueryParams {
29- q.Add(k, v)
30- }
31- u.RawQuery = q.Encode()
32 }
33- httpReq, err := http.NewRequestWithContext(ctx, r.HTTPVerb, u.String(), r.Body)
34 if err != nil {
35 return nil, err
36 }
3738 // first set default headers...
39 if headers != nil {
40- for k, v := range headers {
41- httpReq.Header.Set(k, v)
42 }
43 }
4445 // ... then request-specific take priority (overwrite)
46 if r.Headers != nil {
47- for k, v := range r.Headers {
48- httpReq.Header.Set(k, v)
49 }
50 }
51
···3import (
4 "context"
5 "io"
6+ "fmt"
7 "net/http"
8 "net/url"
910 "github.com/bluesky-social/indigo/atproto/syntax"
11)
1213+var (
14+ // atproto API "Query" Lexicon method, which is HTTP GET. Not to be confused with proposed "HTTP QUERY" method.
15+ MethodQuery = http.MethodGet
16+17+ // atproto API "Procedure" Lexicon method, which is HTTP POST.
18+ MethodProcedure = http.MethodPost
19+)
20+21type APIRequest struct {
22+ // HTTP method, eg "GET" (required)
23+ Method string
24+25+ // atproto API endpoint, as NSID (required)
26 Endpoint syntax.NSID
27+28+ // optional request body. if this is provided, then 'Content-Type' header should be specified
29 Body io.Reader
30+31+ // optional query parameters. These will be encoded as provided.
32+ QueryParams url.Values
33+34+ // optional HTTP headers. Only the first value will be included for each header key ("Set" behavior).
35+ Headers http.Header
36}
3738+// Turns the API request in to an `http.Request`.
39+//
40+// `host` parameter should be a URL prefix: schema, hostname, port.
41+// `headers` parameters are treated as client-level defaults. Only a single value is allowed per key ("Set" behavior), and will be clobbered by any request-level header values.
42+func (r *APIRequest) HTTPRequest(ctx context.Context, host string, headers http.Header) (*http.Request, error) {
43 u, err := url.Parse(host)
44 if err != nil {
45 return nil, err
46 }
47+ if u.Host == "" {
48+ return nil, fmt.Errorf("empty hostname in host URL")
49+ }
50+ if u.Scheme == "" {
51+ return nil, fmt.Errorf("empty scheme in host URL")
52+ }
53+ if r.Endpoint == "" {
54+ return nil, fmt.Errorf("empty request endpoint")
55+ }
56 u.Path = "/xrpc/" + r.Endpoint.String()
57+ u.RawQuery = nil
58 if r.QueryParams != nil {
59+ u.RawQuery = r.QueryParams.Encode()
000060 }
61+ httpReq, err := http.NewRequestWithContext(ctx, r.Method, u.String(), r.Body)
62 if err != nil {
63 return nil, err
64 }
6566 // first set default headers...
67 if headers != nil {
68+ for k := range headers {
69+ httpReq.Header.Set(k, headers.Get(k))
70 }
71 }
7273 // ... then request-specific take priority (overwrite)
74 if r.Headers != nil {
75+ for k := range r.Headers {
76+ httpReq.Header.Set(k, headers.Get(k))
77 }
78 }
79