+105
-15
atproto/client/api_client.go
+105
-15
atproto/client/api_client.go
···
1
1
package client
2
2
3
3
import (
4
+
"bytes"
4
5
"context"
5
6
"encoding/json"
7
+
"fmt"
6
8
"net/http"
7
9
8
10
"github.com/bluesky-social/indigo/atproto/syntax"
9
11
)
10
12
11
-
// 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
+
type APIClient struct {
14
+
HTTPClient *http.Client
15
+
Host string
16
+
Auth AuthMethod
17
+
DefaultHeaders map[string]string
18
+
}
12
19
13
-
type APIClient interface {
14
-
// Full-power method for making atproto API requests.
15
-
Do(ctx context.Context, req *APIRequest) (*http.Response, error)
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) (*json.RawMessage, 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,
32
+
Headers: hdr,
33
+
}
34
+
resp, err := c.Do(ctx, req)
35
+
if err != nil {
36
+
return nil, err
37
+
}
16
38
17
-
// High-level helper for simple JSON "Query" API calls.
18
-
//
19
-
// Does not work with all API endpoints. For more control, use the Do() method with APIRequest.
20
-
Get(ctx context.Context, endpoint syntax.NSID, params map[string]string) (*json.RawMessage, error)
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 {
44
+
return nil, &APIError{StatusCode: resp.StatusCode}
45
+
}
46
+
return nil, eb.APIError(resp.StatusCode)
47
+
}
21
48
22
-
// High-level helper for simple JSON-to-JSON "Procedure" API calls.
23
-
//
24
-
// Does not work with all API endpoints. For more control, use the Do() method with APIRequest.
25
-
// TODO: what is the right type for body, to indicate it can be marshaled as JSON?
26
-
Post(ctx context.Context, endpoint syntax.NSID, body any) (*json.RawMessage, error)
49
+
var ret json.RawMessage
50
+
if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil {
51
+
return nil, fmt.Errorf("expected JSON response body: %w", err)
52
+
}
53
+
return &ret, nil
54
+
}
27
55
28
-
// Returns the currently-authenticated account DID, or empty string if not available.
29
-
AuthDID() syntax.DID
56
+
// High-level helper for simple JSON-to-JSON "Procedure" API calls.
57
+
//
58
+
// Does not work with all API endpoints. For more control, use the Do() method with APIRequest.
59
+
func (c *APIClient) Post(ctx context.Context, endpoint syntax.NSID, body any) (*json.RawMessage, error) {
60
+
bodyJSON, err := json.Marshal(body)
61
+
if err != nil {
62
+
return nil, err
63
+
}
64
+
hdr := map[string]string{
65
+
"Accept": "application/json",
66
+
"Content-Type": "application/json",
67
+
}
68
+
req := APIRequest{
69
+
HTTPVerb: "POST",
70
+
Endpoint: endpoint,
71
+
Body: bytes.NewReader(bodyJSON),
72
+
QueryParams: nil,
73
+
Headers: hdr,
74
+
}
75
+
resp, err := c.Do(ctx, req)
76
+
if err != nil {
77
+
return nil, err
78
+
}
79
+
80
+
defer resp.Body.Close()
81
+
// TODO: duplicate error handling with Get()?
82
+
if !(resp.StatusCode >= 200 && resp.StatusCode < 300) {
83
+
var eb ErrorBody
84
+
if err := json.NewDecoder(resp.Body).Decode(&eb); err != nil {
85
+
return nil, &APIError{StatusCode: resp.StatusCode}
86
+
}
87
+
return nil, eb.APIError(resp.StatusCode)
88
+
}
89
+
90
+
var ret json.RawMessage
91
+
if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil {
92
+
return nil, fmt.Errorf("expected JSON response body: %w", err)
93
+
}
94
+
return &ret, nil
95
+
}
96
+
97
+
// Full-power method for atproto API requests.
98
+
func (c *APIClient) Do(ctx context.Context, req APIRequest) (*http.Response, error) {
99
+
httpReq, err := req.HTTPRequest(ctx, c.Host, c.DefaultHeaders)
100
+
if err != nil {
101
+
return nil, err
102
+
}
103
+
104
+
// TODO: thread-safe?
105
+
if c.HTTPClient == nil {
106
+
c.HTTPClient = http.DefaultClient
107
+
}
108
+
109
+
var resp *http.Response
110
+
if c.Auth != nil {
111
+
resp, err = c.Auth.DoWithAuth(ctx, httpReq, c.HTTPClient)
112
+
} else {
113
+
resp, err = c.HTTPClient.Do(httpReq)
114
+
}
115
+
if err != nil {
116
+
return nil, err
117
+
}
118
+
// TODO: handle some common response errors: rate-limits, 5xx, auth required, etc
119
+
return resp, nil
30
120
}
-120
atproto/client/base_api_client.go
-120
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
-
var eb ErrorBody
40
-
if err := json.NewDecoder(resp.Body).Decode(&eb); err != nil {
41
-
return nil, &APIError{StatusCode: resp.StatusCode}
42
-
}
43
-
return nil, eb.APIError(resp.StatusCode)
44
-
}
45
-
46
-
var ret json.RawMessage
47
-
if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil {
48
-
return nil, fmt.Errorf("expected JSON response body: %w", err)
49
-
}
50
-
return &ret, nil
51
-
}
52
-
53
-
func (c *BaseAPIClient) Post(ctx context.Context, endpoint syntax.NSID, body any) (*json.RawMessage, error) {
54
-
bodyJSON, err := json.Marshal(body)
55
-
if err != nil {
56
-
return nil, err
57
-
}
58
-
hdr := map[string]string{
59
-
"Accept": "application/json",
60
-
"Content-Type": "application/json",
61
-
}
62
-
req := APIRequest{
63
-
HTTPVerb: "POST",
64
-
Endpoint: endpoint,
65
-
Body: bytes.NewReader(bodyJSON),
66
-
QueryParams: nil,
67
-
Headers: hdr,
68
-
}
69
-
resp, err := c.Do(ctx, req)
70
-
if err != nil {
71
-
return nil, err
72
-
}
73
-
74
-
defer resp.Body.Close()
75
-
// TODO: duplicate error handling with Get()?
76
-
if !(resp.StatusCode >= 200 && resp.StatusCode < 300) {
77
-
var eb ErrorBody
78
-
if err := json.NewDecoder(resp.Body).Decode(&eb); err != nil {
79
-
return nil, &APIError{StatusCode: resp.StatusCode}
80
-
}
81
-
return nil, eb.APIError(resp.StatusCode)
82
-
}
83
-
84
-
var ret json.RawMessage
85
-
if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil {
86
-
return nil, fmt.Errorf("expected JSON response body: %w", err)
87
-
}
88
-
return &ret, nil
89
-
}
90
-
91
-
func (c *BaseAPIClient) Do(ctx context.Context, req APIRequest) (*http.Response, error) {
92
-
httpReq, err := req.HTTPRequest(ctx, c.Host, c.DefaultHeaders)
93
-
if err != nil {
94
-
return nil, err
95
-
}
96
-
97
-
// TODO: thread-safe?
98
-
if c.HTTPClient == nil {
99
-
c.HTTPClient = http.DefaultClient
100
-
}
101
-
102
-
var resp *http.Response
103
-
if c.Auth != nil {
104
-
resp, err = c.Auth.DoWithAuth(ctx, httpReq, c.HTTPClient)
105
-
} else {
106
-
resp, err = c.HTTPClient.Do(httpReq)
107
-
}
108
-
if err != nil {
109
-
return nil, err
110
-
}
111
-
// TODO: handle some common response errors: rate-limits, 5xx, auth required, etc
112
-
return resp, nil
113
-
}
114
-
115
-
func (c *BaseAPIClient) AuthDID() syntax.DID {
116
-
if c.Auth != nil {
117
-
return c.Auth.AccountDID()
118
-
}
119
-
return ""
120
-
}
+1
-1
atproto/client/cmd/atclient/main.go
+1
-1
atproto/client/cmd/atclient/main.go
+21
-26
atproto/client/examples_test.go
+21
-26
atproto/client/examples_test.go
···
2
2
3
3
import (
4
4
"fmt"
5
+
"context"
6
+
"encoding/json"
5
7
6
-
atdata "github.com/bluesky-social/indigo/atproto/data"
8
+
appbsky "github.com/bluesky-social/indigo/api/bsky"
9
+
"github.com/bluesky-social/indigo/atproto/syntax"
7
10
)
8
11
9
-
func ExampleGetRequest() {
12
+
func ExampleGet() {
10
13
11
-
// First load Lexicon schema JSON files from local disk.
12
-
cat := NewBaseCatalog()
13
-
if err := cat.LoadDirectory("testdata/catalog"); err != nil {
14
-
panic("failed to load lexicons")
14
+
ctx := context.Background()
15
+
16
+
c := APIClient{
17
+
Host: "https://public.api.bsky.app",
15
18
}
16
19
17
-
// Parse record JSON data using atproto/data helper
18
-
recordJSON := `{
19
-
"$type": "example.lexicon.record",
20
-
"integer": 123,
21
-
"formats": {
22
-
"did": "did:web:example.com",
23
-
"aturi": "at://handle.example.com/com.example.nsid/asdf123",
24
-
"datetime": "2023-10-30T22:25:23Z",
25
-
"language": "en",
26
-
"tid": "3kznmn7xqxl22"
27
-
}
28
-
}`
29
-
30
-
recordData, err := atdata.UnmarshalJSON([]byte(recordJSON))
20
+
endpoint := syntax.NSID("app.bsky.actor.getProfile")
21
+
params := map[string]string{
22
+
"actor": "atproto.com",
23
+
}
24
+
b, err := c.Get(ctx, endpoint, params)
31
25
if err != nil {
32
-
panic("failed to parse record JSON")
26
+
panic(err)
33
27
}
34
28
35
-
if err := ValidateRecord(&cat, recordData, "example.lexicon.record", 0); err != nil {
36
-
fmt.Printf("Schema validation failed: %v\n", err)
37
-
} else {
38
-
fmt.Println("Success!")
29
+
var profile appbsky.ActorDefs_ProfileViewDetailed
30
+
if err := json.Unmarshal(*b, &profile); err != nil {
31
+
panic(err)
39
32
}
40
-
// Output: Success!
33
+
34
+
fmt.Println(profile.Handle)
35
+
// Output: atproto.com
41
36
}