-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmonzo.go
More file actions
262 lines (210 loc) · 6.75 KB
/
monzo.go
File metadata and controls
262 lines (210 loc) · 6.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
package monzo
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"net/http"
"net/url"
"strings"
"time"
"golang.org/x/oauth2"
)
const (
// DefaultUserAgent is the user-agent string that will be sent to the server, unless it is overridden on the Client.
DefaultUserAgent = "go-monzo/1.0 (https://github.com/arylatt/go-monzo)"
// BaseURL is the default Monzo API URL.
BaseURL = "https://api.monzo.com"
)
// Client is the Monzo API client.
//
// Client contains modifiable fields: BaseURL for changing where requests are sent, and UserAgent for changing the user-agent string sent to the server.
//
// The various API endpoints are accessed through the different Service fields (e.g. Accounts, Balance, Pots, etc...),
// based on the Monzo API Reference - https://docs.monzo.com/.
type Client struct {
client *http.Client
BaseURL *url.URL
UserAgent string
common service
Accounts *AccountsService
Balance *BalanceService
Pots *PotsService
Transactions *TransactionsService
Feed *FeedService
Attachments *AttachmentsService
Receipts *ReceiptsService
Webhooks *WebhooksService
}
// Internal struct to provide the different API services with the common client.
type service struct {
client *Client
}
// New creates a new Monzo API client based on the parent HTTP client.
//
// The parent HTTP client should contain a transport capable of authorizing requests, either via static access token, or OAuth2.
func New(client *http.Client) (c *Client) {
baseURL, _ := url.Parse(BaseURL)
c = &Client{
client: client,
BaseURL: baseURL,
UserAgent: DefaultUserAgent,
}
c.common.client = c
c.Accounts = (*AccountsService)(&c.common)
c.Balance = (*BalanceService)(&c.common)
c.Pots = (*PotsService)(&c.common)
c.Transactions = (*TransactionsService)(&c.common)
c.Feed = (*FeedService)(&c.common)
c.Attachments = (*AttachmentsService)(&c.common)
c.Receipts = (*ReceiptsService)(&c.common)
c.Webhooks = (*WebhooksService)(&c.common)
return
}
// Do sends a request to the server and attempts to parse the response data for a Monzo API error.
func (c *Client) Do(req *http.Request) (*http.Response, error) {
resp, err := c.client.Do(req)
parsedResp := CheckResponse(resp)
if parsedResp != nil {
return resp, parsedResp
}
return resp, err
}
// Internal helper to encode request body data and provide the appropriate content type string.
func encodeBody(body any) (io.Reader, string, error) {
if body == nil {
return nil, "", nil
}
switch body := body.(type) {
case url.Values:
return strings.NewReader(body.Encode()), "application/x-www-form-urlencoded", nil
default:
buf := &bytes.Buffer{}
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(false)
return buf, "application/json", enc.Encode(body)
}
}
// NewRequest creates a new HTTP request to be sent to the Monzo API with a background context.
func (c *Client) NewRequest(method, url string, body any) (*http.Request, error) {
return c.NewRequestWithContext(context.Background(), method, url, body)
}
// NewRequestWithContext creates a new HTTP request to be sent to the Monzo API with the provided context.
//
// The request body is encoded and attached to the request, as well as the appropriate user-agent, content type, and accept request headers.
func (c *Client) NewRequestWithContext(ctx context.Context, method, urlStr string, body any) (req *http.Request, err error) {
bodyBuf, contentType, err := encodeBody(body)
if err != nil {
return
}
u, err := c.BaseURL.Parse(urlStr)
if err != nil {
return
}
req, err = http.NewRequestWithContext(ctx, method, u.String(), bodyBuf)
if err != nil {
return
}
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
if c.UserAgent != "" {
req.Header.Set("User-Agent", c.UserAgent)
}
req.Header.Set("Accept", "application/json")
return
}
// Internal helper to call NewRequest and Do and return the results.
func (c *Client) doQuick(method, url string, body any) (resp *http.Response, err error) {
req, err := c.NewRequest(method, url, body)
if err != nil {
return
}
return c.Do(req)
}
// Post sends a HTTP POST request.
func (c *Client) Post(url string, body any) (resp *http.Response, err error) {
return c.doQuick(http.MethodPost, url, body)
}
// Put sends a HTTP PUT request.
func (c *Client) Put(url string, body any) (resp *http.Response, err error) {
return c.doQuick(http.MethodPut, url, body)
}
// Patch sends a HTTP PATCH request.
func (c *Client) Patch(url string, body any) (resp *http.Response, err error) {
return c.doQuick(http.MethodPatch, url, body)
}
// Get sends a HTTP GET request.
func (c *Client) Get(url string, body any) (resp *http.Response, err error) {
return c.doQuick(http.MethodGet, url, body)
}
// Delete sends a HTTP DELETE request.
func (c *Client) Delete(url string) (resp *http.Response, err error) {
return c.doQuick(http.MethodDelete, url, nil)
}
// LogOut revokes the access and refresh token. A new OAuth2Client will need to be created.
func (c *Client) LogOut() (err error) {
_, err = c.Post("/oauth2/logout", nil)
return
}
// ParseResponse attempts to decode the HTTP response body into the provided structure.
func ParseResponse(resp *http.Response, errIn error, v any) (err error) {
err = errIn
if err != nil {
return
}
defer resp.Body.Close()
switch v := v.(type) {
case nil:
case io.Writer:
_, err = io.Copy(v, resp.Body)
default:
err = json.NewDecoder(resp.Body).Decode(v)
if err == io.EOF {
err = nil
}
}
return
}
// Pot represents data about the currently authenticated user provided by the Monzo API.
type Whoami struct {
Authenticated bool `json:"authenticated"`
ClientID string `json:"client_id"`
UserID string `json:"user_id"`
}
// Returns information about the current access token.
func (c *Client) Whoami() (who *Whoami, err error) {
who = &Whoami{}
resp, err := c.Get("/ping/whoami", nil)
err = ParseResponse(resp, err, who)
return
}
// Returns the OAuth2 token being currently used.
func (c *Client) Token() (*oauth2.Token, error) {
switch t := c.client.Transport.(type) {
case *oauth2.Transport:
return t.Source.Token()
}
return nil, errors.New("could not access token from transport")
}
// RefreshToken updates the expiry time on the OAuth2 token to be in the past, and then calls Whoami to
// force the OAuth2 transport to refresh the token.
func (c *Client) RefreshToken() (err error) {
err = c.RefreshTokenOnNextRequest()
if err != nil {
return
}
_, err = c.Whoami()
return
}
// RefreshTokenOnNextRequest updates the expiry time on the OAuth2 token to be in the past, so that the next API call will
// force the OAuth2 transport to refresh the token.
func (c *Client) RefreshTokenOnNextRequest() (err error) {
token, err := c.Token()
if err != nil {
return
}
token.Expiry = time.Unix(1, 0)
return
}