this repo has no description
0
fork

Configure Feed

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

sketch API notes

+283 -57
+28
atproto/client/admin_auth.go
··· 1 + package client 2 + 3 + import ( 4 + "context" 5 + "encoding/base64" 6 + "net/http" 7 + 8 + "github.com/bluesky-social/indigo/atproto/syntax" 9 + ) 10 + 11 + type AdminAuth struct { 12 + basicAuthHeader string 13 + } 14 + 15 + func NewAdminAuth(password string) AdminAuth { 16 + header := "Basic" + base64.StdEncoding.EncodeToString([]byte("admin:"+password)) 17 + return AdminAuth{basicAuthHeader: header} 18 + } 19 + 20 + func (a *AdminAuth) DoWithAuth(ctx context.Context, req *http.Request, httpClient *http.Client) (*http.Response, error) { 21 + req.Header.Set("Authorization", a.basicAuthHeader) 22 + return httpClient.Do(req) 23 + } 24 + 25 + // Admin bearer token auth does not involve an account DID 26 + func (a *AdminAuth) AccountDID() syntax.DID { 27 + return "" 28 + }
+65
atproto/client/api_client.go
··· 1 + package client 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "io" 7 + "net/http" 8 + 9 + "github.com/bluesky-social/indigo/atproto/syntax" 10 + ) 11 + 12 + // NOTE: this is an interface so it can be wrapped/extended. eg, a variant with a bunch of retries, or caching, or whatever. maybe that is too complex and we should have simple struct type, more like the existing `indigo/xrpc` package? hrm. 13 + 14 + type APIClient interface { 15 + // Full-power method for making atproto API requests. 16 + Do(ctx context.Context, req *APIRequest) (*http.Response, error) 17 + 18 + // High-level helper for simple JSON "Query" API calls. 19 + // 20 + // Does not work with all API endpoints. For more control, use the Do() method with APIRequest. 21 + Get(ctx context.Context, endpoint syntax.NSID, params map[string]string) (*json.RawMessage, error) 22 + 23 + // High-level helper for simple JSON-to-JSON "Procedure" API calls. 24 + // 25 + // Does not work with all API endpoints. For more control, use the Do() method with APIRequest. 26 + // TODO: what is the right type for body, to indicate it can be marshaled as JSON? 27 + Post(ctx context.Context, endpoint syntax.NSID, body any) (*json.RawMessage, error) 28 + 29 + // Returns the currently-authenticated account DID, or empty string if not available. 30 + AuthDID() syntax.DID 31 + } 32 + 33 + type APIRequest struct { 34 + HTTPVerb string // TODO: type? 35 + Endpoint syntax.NSID 36 + Body io.Reader 37 + QueryParams map[string]string // TODO: better type for this? 38 + Headers map[string]string 39 + } 40 + 41 + func (r *APIRequest) HTTPRequest(ctx context.Context, host string, headers map[string]string) (*http.Request, error) { 42 + // TODO: use 'url' to safely construct the request URL 43 + u := host + "/xrpc/" + r.Endpoint.String() 44 + // XXX: query params 45 + httpReq, err := http.NewRequestWithContext(ctx, r.HTTPVerb, u, r.Body) 46 + if err != nil { 47 + return nil, err 48 + } 49 + 50 + // first set default headers 51 + if headers != nil { 52 + for k, v := range headers { 53 + httpReq.Header.Set(k, v) 54 + } 55 + } 56 + 57 + // then request-specific take priority (overwrite) 58 + if r.Headers != nil { 59 + for k, v := range r.Headers { 60 + httpReq.Header.Set(k, v) 61 + } 62 + } 63 + 64 + return httpReq, nil 65 + }
+13
atproto/client/auth_method.go
··· 1 + package client 2 + 3 + import ( 4 + "context" 5 + "net/http" 6 + 7 + "github.com/bluesky-social/indigo/atproto/syntax" 8 + ) 9 + 10 + type AuthMethod interface { 11 + DoWithAuth(ctx context.Context, httpReq *http.Request, httpClient *http.Client) (*http.Response, error) 12 + AccountDID() syntax.DID 13 + }
+107
atproto/client/base_api_client.go
··· 1 + package client 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "encoding/json" 7 + "fmt" 8 + "net/http" 9 + 10 + "github.com/bluesky-social/indigo/atproto/syntax" 11 + ) 12 + 13 + type BaseAPIClient struct { 14 + HTTPClient *http.Client 15 + Host string 16 + Auth AuthMethod 17 + DefaultHeaders map[string]string 18 + } 19 + 20 + func (c *BaseAPIClient) Get(ctx context.Context, endpoint syntax.NSID, params map[string]string) (*json.RawMessage, error) { 21 + hdr := map[string]string{ 22 + "Accept": "application/json", 23 + } 24 + req := APIRequest{ 25 + HTTPVerb: "GET", 26 + Endpoint: endpoint, 27 + Body: nil, 28 + QueryParams: params, 29 + Headers: hdr, 30 + } 31 + resp, err := c.Do(ctx, req) 32 + if err != nil { 33 + return nil, err 34 + } 35 + 36 + defer resp.Body.Close() 37 + // TODO: duplicate error handling with Post()? 38 + if !(resp.StatusCode >= 200 && resp.StatusCode < 300) { 39 + return nil, fmt.Errorf("non-successful API request status: %d", resp.StatusCode) 40 + } 41 + 42 + var ret json.RawMessage 43 + if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil { 44 + return nil, fmt.Errorf("expected JSON response body: %w", err) 45 + } 46 + return &ret, nil 47 + } 48 + 49 + func (c *BaseAPIClient) Post(ctx context.Context, endpoint syntax.NSID, body any) (*json.RawMessage, error) { 50 + bodyJSON, err := json.Marshal(body) 51 + if err != nil { 52 + return nil, err 53 + } 54 + hdr := map[string]string{ 55 + "Accept": "application/json", 56 + "Content-Type": "application/json", 57 + } 58 + req := APIRequest{ 59 + HTTPVerb: "POST", 60 + Endpoint: endpoint, 61 + Body: bytes.NewReader(bodyJSON), 62 + QueryParams: nil, 63 + Headers: hdr, 64 + } 65 + resp, err := c.Do(ctx, req) 66 + if err != nil { 67 + return nil, err 68 + } 69 + 70 + defer resp.Body.Close() 71 + // TODO: duplicate error handling with Get()? 72 + if !(resp.StatusCode >= 200 && resp.StatusCode < 300) { 73 + return nil, fmt.Errorf("non-successful API request status: %d", resp.StatusCode) 74 + } 75 + 76 + var ret json.RawMessage 77 + if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil { 78 + return nil, fmt.Errorf("expected JSON response body: %w", err) 79 + } 80 + return &ret, nil 81 + } 82 + 83 + func (c *BaseAPIClient) Do(ctx context.Context, req APIRequest) (*http.Response, error) { 84 + httpReq, err := req.HTTPRequest(ctx, c.Host, c.DefaultHeaders) 85 + if err != nil { 86 + return nil, err 87 + } 88 + 89 + var resp *http.Response 90 + if c.Auth != nil { 91 + resp, err = c.Auth.DoWithAuth(ctx, httpReq, c.HTTPClient) 92 + } else { 93 + resp, err = c.HTTPClient.Do(httpReq) 94 + } 95 + if err != nil { 96 + return nil, err 97 + } 98 + // TODO: handle some common response errors: rate-limits, 5xx, auth required, etc 99 + return resp, nil 100 + } 101 + 102 + func (c *BaseAPIClient) AuthDID() syntax.DID { 103 + if c.Auth != nil { 104 + return c.Auth.AccountDID() 105 + } 106 + return "" 107 + }
-57
atproto/client/client.go
··· 1 - package client 2 - 3 - import ( 4 - "context" 5 - "fmt" 6 - "net/http" 7 - 8 - "github.com/bluesky-social/indigo/atproto/syntax" 9 - ) 10 - 11 - type AuthInfo struct { 12 - AccessJwt string `json:"accessJwt"` 13 - RefreshJwt string `json:"refreshJwt"` 14 - AdminToken string `json:"adminToken"` 15 - Handle string `json:"handle"` 16 - Did string `json:"did"` 17 - } 18 - 19 - type Client struct { 20 - Client *http.Client 21 - Auth *AuthInfo 22 - Host string 23 - UserAgent *string 24 - Headers map[string]string 25 - } 26 - 27 - // Context can include additional headers, including "cache busting", metrics collection, etc. 28 - 29 - func (c *Client) Login(ctx context.Context, ident syntax.AtIdentifier, password string) (*AuthInfo, error) { 30 - return nil, fmt.Errorf("XXX: not implemented") 31 - } 32 - 33 - func (c *Client) Get(ctx context.Context, endpoint string, params map[string]interface{}) (map[string]interface{}, error) { 34 - return nil, fmt.Errorf("XXX: not implemented") 35 - } 36 - 37 - func (c *Client) GetBytes(ctx context.Context, endpoint string, params map[string]interface{}) ([]byte, error) { 38 - return nil, fmt.Errorf("XXX: not implemented") 39 - } 40 - 41 - func (c *Client) Post(ctx context.Context, endpoint string, body, params map[string]interface{}) (map[string]interface{}, error) { 42 - return nil, fmt.Errorf("XXX: not implemented") 43 - } 44 - 45 - // TODO: return stdlib HTTP wrapper? 46 - func (c *Client) PostBytes(ctx context.Context, endpoint string, contentType string, body, params map[string]interface{}) ([]byte, error) { 47 - return nil, fmt.Errorf("XXX: not implemented") 48 - } 49 - 50 - type StreamEvent struct { 51 - // header meta 52 - // body bytes 53 - } 54 - 55 - func (c *Client) Subscribe(ctx context.Context, endpoint string, ch <-chan StreamEvent, params map[string]interface{}) error { 56 - return fmt.Errorf("XXX: not implemented") 57 - }
+39
atproto/client/net_client.go
··· 1 + package client 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "errors" 7 + "io" 8 + 9 + "github.com/bluesky-social/indigo/atproto/syntax" 10 + ) 11 + 12 + // API for clients which pull data from the public atproto network. 13 + // 14 + // Implementations of this interface might resolve PDS instances for DIDs, and fetch data from there. Or they might talk to an archival relay or other network mirroring service. 15 + type NetClient interface { 16 + // Fetches record JSON, without verification or validation. A version (CID) can optionally be specified; use empty string to fetch the latest. 17 + // Returns the record as JSON, and the CID indicated by the server. Does not verify that the data (as CBOR) matches the CID, and does not cryptographically verify a "proof chain" to the record. 18 + GetRecordJSON(ctx context.Context, aturi syntax.ATURI, version syntax.CID) (*json.RawMessage, *syntax.CID, error) 19 + 20 + // Fetches the indicated record as CBOR, and authenticates it by checking both the cryptographic signature and Merkle Tree hashes from the current repo revision. A version (CID) can optionally be specified; use empty string to fetch the latest. 21 + // Returns the record as CBOR; the CID of the validated record, and the repo commit revision. 22 + VerifyRecordCBOR(ctx context.Context, aturi syntax.ATURI, version syntax.CID) (*[]byte, *syntax.CID, string, error) 23 + 24 + // Fetches repo export (CAR file). Optionally attempts to fetch only the diff "since" an earlier repo revision. 25 + GetRepoCAR(ctx context.Context, did syntax.DID, since string) (*io.Reader, error) 26 + 27 + // Fetches indicated blob. Does not validate the CID. Returns a reader (which calling code is responsible for closing). 28 + GetBlob(ctx context.Context, did syntax.DID, cid syntax.CID) (*io.Reader, error) 29 + CheckAccountStatus(ctx context.Context, did syntax.DID) (*AccountStatus, error) 30 + } 31 + 32 + // XXX: type alias to codegen? or just copy? this is protocol-level 33 + type AccountStatus struct { 34 + } 35 + 36 + func VerifyBlobCID(blob []byte, cid syntax.CID) error { 37 + // XXX: compute hash, check against provided CID 38 + return errors.New("Not Implemented") 39 + }
+31
atproto/client/refresh_auth.go
··· 1 + package client 2 + 3 + import ( 4 + "context" 5 + "net/http" 6 + 7 + "github.com/bluesky-social/indigo/atproto/syntax" 8 + ) 9 + 10 + type RefreshAuth struct { 11 + AccessToken string 12 + RefreshToken string 13 + DID syntax.DID 14 + // The AuthHost might different from any APIClient host, if there is an entryway involved 15 + AuthHost string 16 + } 17 + 18 + // TODO: 19 + //func NewRefreshAuth(pdsHost, accountIdentifier, password string) (*RefreshAuth, error) { 20 + 21 + func (a *RefreshAuth) DoWithAuth(ctx context.Context, httpReq *http.Request, httpClient *http.Client) (*http.Response, error) { 22 + httpReq.Header.Set("Authorization", "Bearer "+a.AccessToken) 23 + // XXX: check response. if it is 403, because access token is expired, then take a lock and do a refresh 24 + // TODO: when doing a refresh request, copy at least the User-Agent header from httpReq, and re-use httpClient 25 + return httpClient.Do(httpReq) 26 + } 27 + 28 + // Admin bearer token auth does not involve an account DID 29 + func (a *RefreshAuth) AccountDID() syntax.DID { 30 + return a.DID 31 + }