porting all github actions from bluesky-social/indigo to tangled CI
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

iterate on APIClient

+81 -36
+39 -22
atproto/client/api_client.go
··· 6 "encoding/json" 7 "fmt" 8 "net/http" 9 10 "github.com/bluesky-social/indigo/atproto/syntax" 11 ) 12 13 type APIClient struct { 14 - HTTPClient *http.Client 15 Host string 16 Auth AuthMethod 17 - DefaultHeaders map[string]string 18 } 19 20 // 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 } 58 59 - // 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. 62 func (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 } 102 103 // Full-power method for atproto API requests. 104 func (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 } 109 110 - // TODO: thread-safe? 111 - if c.HTTPClient == nil { 112 - c.HTTPClient = http.DefaultClient 113 } 114 115 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 }
··· 6 "encoding/json" 7 "fmt" 8 "net/http" 9 + "net/url" 10 11 "github.com/bluesky-social/indigo/atproto/syntax" 12 ) 13 14 type 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 } 27 28 // 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 } 46 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 } 65 66 + // 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. 69 func (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 } 89 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 } 108 109 // 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. 112 func (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 } 117 118 + // NOTE: this updates the client object itself 119 + if c.Client == nil { 120 + c.Client = http.DefaultClient 121 } 122 123 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 } 132 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
··· 3 import ( 4 "context" 5 "io" 6 "net/http" 7 "net/url" 8 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 ) 11 12 type APIRequest struct { 13 - HTTPVerb string // TODO: type? 14 Endpoint syntax.NSID 15 Body io.Reader 16 - QueryParams map[string]string // TODO: better type for this? 17 - Headers map[string]string 18 } 19 20 - func (r *APIRequest) HTTPRequest(ctx context.Context, host string, headers map[string]string) (*http.Request, error) { 21 u, err := url.Parse(host) 22 if err != nil { 23 return nil, err 24 } 25 u.Path = "/xrpc/" + r.Endpoint.String() 26 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 } 37 38 // first set default headers... 39 if headers != nil { 40 - for k, v := range headers { 41 - httpReq.Header.Set(k, v) 42 } 43 } 44 45 // ... 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
··· 3 import ( 4 "context" 5 "io" 6 + "fmt" 7 "net/http" 8 "net/url" 9 10 "github.com/bluesky-social/indigo/atproto/syntax" 11 ) 12 13 + 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 + 21 type 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 } 37 38 + // 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() 60 } 61 + httpReq, err := http.NewRequestWithContext(ctx, r.Method, u.String(), r.Body) 62 if err != nil { 63 return nil, err 64 } 65 66 // first set default headers... 67 if headers != nil { 68 + for k := range headers { 69 + httpReq.Header.Set(k, headers.Get(k)) 70 } 71 } 72 73 // ... 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