forked from chanbakjsd/gotrix
-
Notifications
You must be signed in to change notification settings - Fork 0
/
client.go
138 lines (116 loc) · 3.33 KB
/
client.go
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
package httputil
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httputil"
"time"
"github.com/diamondburned/gotrix/debug"
"github.com/diamondburned/gotrix/matrix"
)
// Client is a HTTP client that is Matrix API-aware.
type Client struct {
ClientDriver
// AccessToken is the token to attach to the request.
AccessToken string
// HomeServer is the host part of the homeserver and is treated as the host
// for all requests.
HomeServer string
// HomeServerScheme is the scheme to talk to homeserver on.
// It is https most of the time.
HomeServerScheme string
ctx context.Context
}
// NewClient creates a new Client that uses the default HTTP client.
func NewClient() Client {
return Client{
ClientDriver: http.DefaultClient,
ctx: context.Background(),
}
}
// NewCustomClient creates a new Client that uses the provided ClientDriver.
func NewCustomClient(d ClientDriver) Client {
return Client{
ClientDriver: d,
ctx: context.Background(),
}
}
// WithContext creates a copy of Client that uses the provided context during request creation.
func (c Client) WithContext(ctx context.Context) Client {
c.ctx = ctx
return c
}
// FullRoute creates the full route from the provided route.
func (c *Client) FullRoute(route string) string {
return c.HomeServerScheme + "://" + c.HomeServer + "/" + route
}
// Request makes the request and returns the result.
//
// It may return any HTTP request errors or a matrix.HTTPError which may possibly
// wrap a matrix.APIError.
func (c *Client) Request(method, route string, to interface{}, mods ...Modifier) error {
// Generate the request.
req, err := http.NewRequestWithContext(c.ctx, method, c.FullRoute(route), nil)
if err != nil {
return err
}
// Apply all the request modifiers.
for _, v := range mods {
v(c, req)
}
if debug.TraceEnabled {
b, err := httputil.DumpRequest(req, true)
if err != nil {
panic(err)
}
debug.Trace("<<<<\n" + string(b))
}
// Make the request.
resp, err := c.Do(req)
if err != nil {
debug.Trace(">>>> N/A. Error: " + err.Error())
return err
}
if debug.TraceEnabled {
b, err := httputil.DumpResponse(resp, true)
if err != nil {
panic(err)
}
debug.Trace(">>>>\n" + string(b))
}
defer func() {
// We honestly don't care about errors closing the body.
_ = resp.Body.Close()
}()
// HTTP OK. Just return the object.
if resp.StatusCode == http.StatusOK {
if to == nil {
return nil
}
return json.NewDecoder(resp.Body).Decode(to)
}
bodyBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
// Try to decode into target just in case it is expecting the error message.
_ = json.NewDecoder(bytes.NewReader(bodyBytes)).Decode(to)
// Try parsing it as an API error.
var apiError matrix.APIError
err = json.NewDecoder(bytes.NewReader(bodyBytes)).Decode(&apiError)
if err != nil {
return matrix.NewHTTPError(resp.StatusCode, err)
}
// If it's a rate-limit, we intercept it and retry after the recommended time.
if apiError.Code == matrix.CodeLimitExceeded {
debug.Debug(
fmt.Sprintf("Being rate-limited by homeserver. Retrying in %dms.", apiError.RetryAfterMillisecond),
)
time.Sleep(time.Duration(apiError.RetryAfterMillisecond) * time.Millisecond)
return c.Request(method, route, to, mods...)
}
return matrix.NewHTTPError(resp.StatusCode, apiError)
}