···1010 "github.com/bluesky-social/indigo/atproto/syntax"
1111)
12121313+// Interface for auth implementations which can be used with [APIClient].
1314type AuthMethod interface {
1414- // endpoint parameter is included for auth methods which need to include the NSID in authorization tokens
1515+ // Endpoint parameter is included for auth methods which need to include the NSID in authorization tokens
1516 DoWithAuth(c *http.Client, req *http.Request, endpoint syntax.NSID) (*http.Response, error)
1617}
17181919+// General purpose client for atproto "XRPC" API endpoints.
1820type APIClient struct {
1919- // inner HTTP client
2121+ // Inner HTTP client. May be customized after the overall [APIClient] struct is created; for example to set a default request timeout.
2022 Client *http.Client
21232222- // host URL prefix: scheme, hostname, and port. This field is required.
2424+ // Host URL prefix: scheme, hostname, and port. This field is required.
2325 Host string
24262525- // optional auth client "middleware"
2727+ // Optional auth client "middleware".
2628 Auth AuthMethod
27292828- // 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.
3030+ // 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.
2931 Headers http.Header
30323131- // optional authenticated account DID for this client. Does not change client behavior; this is included as a convenience for calling code, logging, etc.
3333+ // optional authenticated account DID for this client. Does not change client behavior; this field is included as a convenience for calling code, logging, etc.
3234 AccountDID *syntax.DID
3335}
34363537// 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.
3638//
3737-// Uses the default stdlib http.Client, and sets a default User-Agent.
3939+// Uses [http.DefaultClient], and sets a default User-Agent.
3840func NewAPIClient(host string) *APIClient {
3941 return &APIClient{
4042 Client: http.DefaultClient,
···47494850// High-level helper for simple JSON "Query" API calls.
4951//
5050-// Does not work with all API endpoints. For more control, use the Do() method with APIRequest.
5252+// This method automatically parses non-successful responses to [APIError].
5353+//
5454+// For Query endpoints which return non-JSON data, or other situations needing complete configuration of the request and response, use the [APIClient.Do] method.
5155func (c *APIClient) Get(ctx context.Context, endpoint syntax.NSID, params map[string]any, out any) error {
52565357 req := NewAPIRequest(http.MethodGet, endpoint, nil)
···87918892// High-level helper for simple JSON-to-JSON "Procedure" API calls, with no query params.
8993//
9090-// Does not work with all possible atproto API endpoints. For more control, use the Do() method with APIRequest.
9494+// This method automatically parses non-successful responses to [APIError].
9595+//
9696+// For Query endpoints which expect non-JSON request bodies; return non-JSON responses; direct use of [io.Reader] for the request body; or other situations needing complete configuration of the request and response, use the [APIClient.Do] method.
9197func (c *APIClient) Post(ctx context.Context, endpoint syntax.NSID, body any, out any) error {
9298 bodyJSON, err := json.Marshal(body)
9399 if err != nil {
···124130125131// Full-featured method for atproto API requests.
126132//
127127-// NOTE: this does not currently parse error response JSON body, thought it might in the future.
133133+// TODO: this does not currently parse API error response JSON body to [APIError], thought it might in the future.
128134func (c *APIClient) Do(ctx context.Context, req *APIRequest) (*http.Response, error) {
129135130136 if c.Client == nil {
···150156151157// Returns a shallow copy of the APIClient with the provided service ref configured as a proxy header.
152158//
153153-// To configure service proxying without creating a copy, simply set the "Atproto-Proxy" header.
159159+// To configure service proxying without creating a copy, simply set the 'Atproto-Proxy' header.
154160func (c *APIClient) WithService(ref string) *APIClient {
155161 hdr := c.Headers.Clone()
156162 hdr.Set("Atproto-Proxy", ref)
···164170 return &out
165171}
166172167167-// Configures labeler header (Atproto-Accept-Labelers) with the indicated "redact" level labelers, and regular labelers.
173173+// Configures labeler header ('Atproto-Accept-Labelers') with the indicated "redact" level labelers, and regular labelers.
174174+//
175175+// Overwrites any existing client-level header value.
168176func (c *APIClient) SetLabelers(redact, other []syntax.DID) {
169177 c.Headers.Set("Atproto-Accept-Labelers", encodeLabelerHeader(redact, other))
170178}
+6-5
atproto/client/apirequest.go
···1111)
12121313var (
1414- // atproto API "Query" Lexicon method, which is HTTP GET. Not to be confused with proposed "HTTP QUERY" method.
1414+ // atproto API "Query" Lexicon method, which is HTTP GET. Not to be confused with the IETF draft "HTTP QUERY" method.
1515 MethodQuery = http.MethodGet
16161717 // atproto API "Procedure" Lexicon method, which is HTTP POST.
···2828 // Optional request body (may be nil). If this is provided, then 'Content-Type' header should be specified
2929 Body io.Reader
30303131- // 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.
3131+ // 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.
3232 GetBody func() (io.ReadCloser, error)
33333434 // Optional query parameters (field may be nil). These will be encoded as provided.
···40404141// Initializes a new request struct. Initializes Headers and QueryParams so they can be manipulated immediately.
4242//
4343-// 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).
4343+// 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]).
4444func NewAPIRequest(method string, endpoint syntax.NSID, body io.Reader) *APIRequest {
4545 req := APIRequest{
4646 Method: method,
···6666 return &req
6767}
68686969-// Creates an `http.Request` for this API request.
6969+// Creates an [http.Request] for this API request.
7070//
7171// `host` parameter should be a URL prefix: schema, hostname, port (required)
7272-// `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)
7272+//
7373+// `clientHeaders`, if provided, is 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)
7374func (r *APIRequest) HTTPRequest(ctx context.Context, host string, clientHeaders http.Header) (*http.Request, error) {
7475 u, err := url.Parse(host)
7576 if err != nil {
+21
atproto/client/doc.go
···11+/*
22+General-purpose client for atproto "XRPC" HTTP API endpoints.
33+44+[APIClient] wraps an [http.Client] and provides an ergonomic atproto-specific (but not Lexicon-specific) interface for "Query" (GET) and "Procedure" (POST) endpoints. It does not support "Event Stream" (WebSocket) endpoints. The client is expected to be used with a single host at a time, though it does have special support ([APIClient.WithService]) for proxied service requests when connected to a PDS host. The client does not authenticate requests by default, but supports pluggable authentication methods (see below). The [APIReponse] struct represents a generic API request, and helps with conversion to an [http.Request].
55+66+The [APIError] struct can represent a generic API error response (eg, an HTTP response with a 4xx or 5xx response code), including the 'error' and 'message' JSON response fields expected with atproto. It is intended to be used with [errors.Is] in error handling, or to provide helpful error messages.
77+88+The [AuthMethod] interface allows [APIClient] to work with multiple forms of authentication in atproto. It is expected that more complex auth systems (eg, those using signed JWTs) will be implemented in separate packages, but this package does include two simple auth methods:
99+1010+- [PasswordAuth] is the original PDS user auth method, using access and refresh tokens.
1111+- [AdminAuth] is simple HTTP Basic authentication for administrative requests, as implemented by many atproto services (Relay, Ozone, PDS, etc).
1212+1313+## Design Notes
1414+1515+Several [AuthMethod] implementations are expected to require retrying entire request at unexpected times. For example, unexpected OAuth DPoP nonce changes, or unexpected password session token refreshes. The auth method may also need to make requests to other servers as part of the refresh process (eg, OAuth when working with a PDS/entryway split). This means that requests should be "retryable" as often as possible. This is mostly a concern for Procedures (HTTP POST) with a non-empty body. The [http.Client] will attempt to "unclose" some common [io.ReadCloser] types (like [bytes.Buffer]), but others may need special handling, using the [APIRequest.GetBody] method. This package will try to make types implementing [io.Seeker] tryable; this helps with things like passing in a open file descriptor for file uploads.
1616+1717+In theory, the [http.RoundTripper] interface could have been used instead of [AuthMethod]; or auth methods could have been injected in to [http.Client] instances directly. This package avoids this pattern for a few reasons. The first is that wrangling layered stacks of [http.RoundTripper] can become cumbersome. Calling code may want to use [http.Client] variants which add observability, retries, circuit-breaking, or other non-auth customization. Secondly, some atproto auth methods will require requests to other servers or endpoints, and having a common [http.Client] to re-use for these requests makes sense. Finally, several atproto auth methods need to know the target endpoint as an NSID; while this could be re-parsed from the request URL, it is simpler and more reliable to pass it as an argument.
1818+1919+This package tries to use minimal dependencies beyond the Go standard library, to make it easy to reference as a dependency. It does require the [github.com/bluesky-social/indigo/atproto/syntax] and [github.com/bluesky-social/indigo/atproto/identity] sibling packages. In particular, this package does not include any auth methods requiring JWTs, to avoid adding any specific JWT implementation as a dependency.
2020+*/
2121+package client
+1-1
atproto/client/lexclient.go
···1010 "github.com/bluesky-social/indigo/atproto/syntax"
1111)
12121313-// Implements the `LexClient` interface, for use with code-generated API helpers.
1313+// Implements the [github.com/bluesky-social/indigo/lex/util.LexClient] interface, for use with code-generated API helpers.
1414func (c *APIClient) LexDo(ctx context.Context, kind string, inpenc string, method string, params map[string]any, bodyobj any, out any) error {
1515 // some of the code here is copied from indigo:xrpc/xrpc.go
1616
+15-10
atproto/client/password_auth.go
···14141515type RefreshCallback = func(ctx context.Context, data PasswordSessionData)
16161717+// Implementation of [AuthMethod] for password-based auth sessions with atproto PDS hosts. Automatically refreshes "access token" using a "refresh token" when needed.
1818+//
1919+// It is safe to use this auth method concurrently from multiple goroutines.
1720type PasswordAuth struct {
1821 Session PasswordSessionData
1922···2629 lk sync.RWMutex
2730}
28313232+// Data about a PDS password auth session which can be persisted and then used to resume the session later.
2933type PasswordSessionData struct {
3034 AccessToken string `json:"access_token"`
3135 RefreshToken string `json:"refresh_token"`
···3337 Host string `json:"host"`
3438}
35394040+// Creates a deep copy of the session data.
3641func (sd *PasswordSessionData) Clone() PasswordSessionData {
3742 return PasswordSessionData{
3843 AccessToken: sd.AccessToken,
···114119 if err != nil {
115120 return nil, err
116121 }
117117- // TODO: could handle auth failure as special error type here
122122+ // NOTE: could handle auth failure as special error type here
118123 return retryResp, err
119124}
120125···128133// Refreshes auth tokens (takes a write-lock on session data).
129134//
130135// `priorRefreshToken` argument is used to check if a concurrent refresh already took place.
131131-//
132132-// TODO: need a "Logout" method as well? which takes the refresh token (not access token)
133136func (a *PasswordAuth) Refresh(ctx context.Context, c *http.Client, priorRefreshToken string) error {
134137135138 a.lk.Lock()
···162165 if err := json.NewDecoder(resp.Body).Decode(&eb); err != nil {
163166 return &APIError{StatusCode: resp.StatusCode}
164167 }
165165- // TODO: indicate in this error that it was from refresh process, not original request?
168168+ // TODO: indicate in the error that it was from refresh process, not original request?
166169 return eb.APIError(resp.StatusCode)
167170 }
168171···212215 return nil
213216}
214217215215-// Creates a new APIClient with PasswordAuth for the provided user. The provided identity directory is used to resolve the PDS host for the account.
218218+// Creates a new [APIClient] with [PasswordAuth] for the provided user. The provided identity directory is used to resolve the PDS host for the account.
216219//
217220// `authToken` is optional; is used when multi-factor authentication is enabled for the account.
218218-// `cb` is an optional callback which will be called with updated session data after any token refresh, in a goroutine.
221221+//
222222+// `cb` is an optional callback which will be called with updated session data after any token refresh.
219223func LoginWithPassword(ctx context.Context, dir identity.Directory, username syntax.AtIdentifier, password, authToken string, cb RefreshCallback) (*APIClient, error) {
220224221225 ident, err := dir.Lookup(ctx, username)
···240244 return c, nil
241245}
242246243243-// Creates a new APIClient with PasswordAuth, based on a login to the provided host. Note that with some PDS implementations, 'username' could be an email address. This login method also works in situations where an account's network identity does not resolve to this specific host.
247247+// Creates a new [APIClient] with [PasswordAuth], based on a login to the provided host. Note that with some PDS implementations, 'username' could be an email address. This login method also works in situations where an account's network identity does not resolve to this specific host.
244248//
245249// `authToken` is optional; is used when multi-factor authentication is enabled for the account.
246246-// `cb` is an optional callback which will be called with updated session data after any token refresh, in a goroutine.
250250+//
251251+// `cb` is an optional callback which will be called with updated session data after any token refresh.
247252func LoginWithPasswordHost(ctx context.Context, host, username, password, authToken string, cb RefreshCallback) (*APIClient, error) {
248253249254 c := NewAPIClient(host)
···283288 return c, nil
284289}
285290286286-// Creates an APIClient using PasswordAuth, based on existing session data.
291291+// Creates an [APIClient] using [PasswordAuth], based on existing session data.
287292//
288288-// `cb` is an optional callback which will be called with updated session data after any token refresh, in a goroutine.
293293+// `cb` is an optional callback which will be called with updated session data after any token refresh.
289294func ResumePasswordSession(data PasswordSessionData, cb RefreshCallback) *APIClient {
290295 c := NewAPIClient(data.Host)
291296 ra := PasswordAuth{