this repo has no description
0
fork

Configure Feed

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

progress on APIClient ergonomics

+92 -60
+1 -1
atproto/client/admin_auth.go
··· 14 14 } 15 15 16 16 func NewAdminClient(host, password string) *APIClient { 17 - c := NewPublicClient(host) 17 + c := NewAPIClient(host) 18 18 c.Auth = &AdminAuth{Password: password} 19 19 return c 20 20 }
+1 -1
atproto/client/admin_auth_test.go
··· 32 32 defer srv.Close() 33 33 34 34 { 35 - c := NewPublicClient(srv.URL) 35 + c := NewAPIClient(srv.URL) 36 36 err := c.Get(ctx, syntax.NSID("com.example.get"), nil, nil) 37 37 assert.ErrorAs(err, &apierr) 38 38 }
+22 -36
atproto/client/api_client.go
··· 28 28 AccountDID *syntax.DID 29 29 } 30 30 31 - func NewPublicClient(host string) *APIClient { 31 + // Creates a simple APIClient for the provided host. This is appropriate for use with unauthenticated ("public") atproto API endpoints, or to use as a base client to add authentication. 32 + // 33 + // Uses the default stdlib http.Client, and sets a default User-Agent. 34 + func NewAPIClient(host string) *APIClient { 32 35 return &APIClient{ 33 36 Client: http.DefaultClient, 34 37 Host: host, ··· 42 45 // 43 46 // Does not work with all API endpoints. For more control, use the Do() method with APIRequest. 44 47 func (c *APIClient) Get(ctx context.Context, endpoint syntax.NSID, params url.Values, out any) error { 45 - hdr := map[string][]string{ 46 - "Accept": []string{"application/json"}, 47 - } 48 - req := APIRequest{ 49 - Method: http.MethodGet, 50 - Endpoint: endpoint, 51 - Body: nil, 52 - QueryParams: params, 53 - Headers: hdr, 54 - } 48 + 49 + req := NewAPIRequest(http.MethodGet, endpoint, nil) 50 + req.QueryParams = params 51 + req.Headers.Set("Accept", "application/json") 52 + 55 53 resp, err := c.Do(ctx, req) 56 54 if err != nil { 57 55 return err ··· 84 82 if err != nil { 85 83 return err 86 84 } 87 - hdr := map[string][]string{ 88 - "Accept": []string{"application/json"}, 89 - "Content-Type": []string{"application/json"}, 90 - } 91 - req := APIRequest{ 92 - Method: http.MethodPost, 93 - Endpoint: endpoint, 94 - Body: bytes.NewReader(bodyJSON), 95 - QueryParams: nil, 96 - Headers: hdr, 97 - } 85 + 86 + req := NewAPIRequest(http.MethodPost, endpoint, bytes.NewReader(bodyJSON)) 87 + req.Headers.Set("Accept", "application/json") 88 + req.Headers.Set("Content-Type", "application/json") 89 + 98 90 resp, err := c.Do(ctx, req) 99 91 if err != nil { 100 92 return err ··· 119 111 return nil 120 112 } 121 113 122 - // Full-power method for atproto API requests. 114 + // Full-featured method for atproto API requests. 123 115 // 124 116 // NOTE: this does not currently parse error response JSON body, thought it might in the future. 125 - func (c *APIClient) Do(ctx context.Context, req APIRequest) (*http.Response, error) { 117 + func (c *APIClient) Do(ctx context.Context, req *APIRequest) (*http.Response, error) { 118 + 119 + if c.Client == nil { 120 + c.Client = http.DefaultClient 121 + } 122 + 126 123 httpReq, err := req.HTTPRequest(ctx, c.Host, c.Headers) 127 124 if err != nil { 128 125 return nil, err 129 - } 130 - 131 - // NOTE: this updates the client object itself 132 - if c.Client == nil { 133 - c.Client = http.DefaultClient 134 126 } 135 127 136 128 var resp *http.Response ··· 149 141 // 150 142 // To configure service proxying without creating a copy, simply set the "Atproto-Proxy" header. 151 143 func (c *APIClient) WithService(ref string) *APIClient { 152 - hdr := make(http.Header) 153 - for k := range c.Headers { 154 - for _, v := range c.Headers.Values(k) { 155 - hdr.Add(k, v) 156 - } 157 - } 158 - 144 + hdr := c.Headers.Clone() 159 145 hdr.Set("Atproto-Proxy", ref) 160 146 out := APIClient{ 161 147 Client: c.Client,
+54 -15
atproto/client/api_request.go
··· 1 1 package client 2 2 3 3 import ( 4 + "bytes" 4 5 "context" 5 6 "fmt" 6 7 "io" ··· 19 20 ) 20 21 21 22 type APIRequest struct { 22 - // HTTP method, eg "GET" (required) 23 + // HTTP method as a string (eg "GET") (required) 23 24 Method string 24 25 25 26 // atproto API endpoint, as NSID (required) 26 27 Endpoint syntax.NSID 27 28 28 - // optional request body. if this is provided, then 'Content-Type' header should be specified 29 - Body io.Reader 29 + // Optional request body (may be nil). If this is provided, then 'Content-Type' header should be specified 30 + Body io.ReadCloser 30 31 31 - // XXX: 32 - //GetBody func() (io.ReadCloser, error) 32 + // Optional function to return new reader for request body; used for retries. strongly recommended if Body is defined. Body still needs to be defined, even if this function is provided. 33 + GetBody func() (io.ReadCloser, error) 33 34 34 - // optional query parameters. These will be encoded as provided. 35 + // Optional query parameters (field may be nil). These will be encoded as provided. 35 36 QueryParams url.Values 36 37 37 - // optional HTTP headers. Only the first value will be included for each header key ("Set" behavior). 38 + // Optional HTTP headers (field bay be nil). Only the first value will be included for each header key ("Set" behavior). 38 39 Headers http.Header 39 40 } 40 41 41 - // Turns the API request in to an `http.Request`. 42 + // Initializes a new request struct. Initializes Headers and QueryParams so they can be manipulated immediately. 43 + // 44 + // If body is provided (it can be nil), will try to turn it in to the most retry-able form (and wrap as io.ReadCloser). 45 + func NewAPIRequest(method string, endpoint syntax.NSID, body io.Reader) *APIRequest { 46 + req := APIRequest{ 47 + Method: method, 48 + Endpoint: endpoint, 49 + Headers: map[string][]string{}, 50 + QueryParams: map[string][]string{}, 51 + } 52 + 53 + // logic to turn "whatever io.Reader we are handed" in to something relatively re-tryable (using GetBody) 54 + if body != nil { 55 + switch v := body.(type) { 56 + case *bytes.Buffer: 57 + req.Body = io.NopCloser(v) 58 + req.GetBody = func() (io.ReadCloser, error) { 59 + return io.NopCloser(v), nil 60 + } 61 + case io.Seeker: 62 + req.Body = io.NopCloser(body) 63 + req.GetBody = func() (io.ReadCloser, error) { 64 + v.Seek(0, 0) 65 + return io.NopCloser(body), nil 66 + } 67 + case io.ReadCloser: 68 + req.Body = v 69 + case io.Reader: 70 + req.Body = io.NopCloser(body) 71 + } 72 + } 73 + return &req 74 + } 75 + 76 + // Creates an `http.Request` for this API request. 42 77 // 43 - // `host` parameter should be a URL prefix: schema, hostname, port. 44 - // `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. 45 - func (r *APIRequest) HTTPRequest(ctx context.Context, host string, headers http.Header) (*http.Request, error) { 78 + // `host` parameter should be a URL prefix: schema, hostname, port (required) 79 + // `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. (optional; may be nil) 80 + func (r *APIRequest) HTTPRequest(ctx context.Context, host string, clientHeaders http.Header) (*http.Request, error) { 46 81 u, err := url.Parse(host) 47 82 if err != nil { 48 83 return nil, err ··· 58 93 } 59 94 u.Path = "/xrpc/" + r.Endpoint.String() 60 95 u.RawQuery = "" 61 - if r.QueryParams != nil { 96 + if r.QueryParams != nil && len(r.QueryParams) > 0 { 62 97 u.RawQuery = r.QueryParams.Encode() 63 98 } 64 99 httpReq, err := http.NewRequestWithContext(ctx, r.Method, u.String(), r.Body) ··· 66 101 return nil, err 67 102 } 68 103 104 + if r.GetBody != nil { 105 + httpReq.GetBody = r.GetBody 106 + } 107 + 69 108 // first set default headers... 70 - if headers != nil { 71 - for k := range headers { 72 - httpReq.Header.Set(k, headers.Get(k)) 109 + if clientHeaders != nil { 110 + for k := range clientHeaders { 111 + httpReq.Header.Set(k, clientHeaders.Get(k)) 73 112 } 74 113 } 75 114
+14 -7
atproto/client/password_auth.go
··· 35 35 } 36 36 37 37 // on success, or most errors, just return HTTP response 38 - if resp.StatusCode != 400 || !strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json") { 38 + if resp.StatusCode != http.StatusBadRequest || !strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json") { 39 39 return resp, nil 40 40 } 41 41 ··· 54 54 return nil, err 55 55 } 56 56 57 - // XXX: review "retry" logic (HTTP body, headers, etc) 58 - req.Header.Set("Authorization", "Bearer "+a.Session.AccessToken) 59 - resp, err = c.Do(req) 57 + retry := req.Clone(req.Context()) 58 + if req.GetBody != nil { 59 + retry.Body, err = req.GetBody() 60 + if err != nil { 61 + return nil, fmt.Errorf("API request retry GetBody failed: %w", err) 62 + } 63 + } 64 + 65 + retry.Header.Set("Authorization", "Bearer "+a.Session.AccessToken) 66 + retryResp, err := c.Do(retry) 60 67 if err != nil { 61 68 return nil, err 62 69 } 63 - // XXX: handle auth error here? 64 - return resp, err 70 + // TODO: could handle auth failure as special error type here 71 + return retryResp, err 65 72 } 66 73 67 74 // TODO: need a "Logout" method as well? which takes the refresh token (not access token) ··· 127 134 return nil, fmt.Errorf("account does not have PDS registered") 128 135 } 129 136 130 - c := NewPublicClient(host) 137 + c := NewAPIClient(host) 131 138 reqBody := comatproto.ServerCreateSession_Input{ 132 139 Identifier: ident.DID.String(), 133 140 Password: password,