-
Notifications
You must be signed in to change notification settings - Fork 38
/
client.go
195 lines (175 loc) · 6.31 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
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
package sync2
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"time"
"github.com/matrix-org/gomatrixserverlib"
"github.com/tidwall/gjson"
)
const AccountDataGlobalRoom = ""
var ProxyVersion = ""
var HTTP401 error = fmt.Errorf("HTTP 401")
type Client interface {
// WhoAmI asks the homeserver to lookup the access token using the CSAPI /whoami
// endpoint. The response must contain a device ID (meaning that we assume the
// homeserver supports Matrix >= 1.1.)
WhoAmI(accessToken string) (userID, deviceID string, err error)
DoSyncV2(ctx context.Context, accessToken, since string, isFirst bool, toDeviceOnly bool) (*SyncResponse, int, error)
}
// HTTPClient represents a Sync v2 Client.
// One client can be shared among many users.
type HTTPClient struct {
Client *http.Client
DestinationServer string
}
// Return sync2.HTTP401 if this request returns 401
func (v *HTTPClient) WhoAmI(accessToken string) (string, string, error) {
req, err := http.NewRequest("GET", v.DestinationServer+"/_matrix/client/r0/account/whoami", nil)
if err != nil {
return "", "", err
}
req.Header.Set("User-Agent", "sync-v3-proxy-"+ProxyVersion)
req.Header.Set("Authorization", "Bearer "+accessToken)
res, err := v.Client.Do(req)
if err != nil {
return "", "", err
}
if res.StatusCode != 200 {
if res.StatusCode == 401 {
return "", "", HTTP401
}
return "", "", fmt.Errorf("/whoami returned HTTP %d", res.StatusCode)
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", "", err
}
response := gjson.ParseBytes(body)
return response.Get("user_id").Str, response.Get("device_id").Str, nil
}
// DoSyncV2 performs a sync v2 request. Returns the sync response and the response status code
// or an error. Set isFirst=true on the first sync to force a timeout=0 sync to ensure snapiness.
func (v *HTTPClient) DoSyncV2(ctx context.Context, accessToken, since string, isFirst, toDeviceOnly bool) (*SyncResponse, int, error) {
syncURL := v.createSyncURL(since, isFirst, toDeviceOnly)
req, err := http.NewRequest("GET", syncURL, nil)
req.Header.Set("User-Agent", "sync-v3-proxy-"+ProxyVersion)
req.Header.Set("Authorization", "Bearer "+accessToken)
if err != nil {
return nil, 0, fmt.Errorf("DoSyncV2: NewRequest failed: %w", err)
}
var res *http.Response
if isFirst {
longTimeoutClient := &http.Client{
Timeout: 30 * time.Minute,
}
res, err = longTimeoutClient.Do(req)
} else {
res, err = v.Client.Do(req)
}
if err != nil {
return nil, 0, fmt.Errorf("DoSyncV2: request failed: %w", err)
}
switch res.StatusCode {
case 200:
var svr SyncResponse
if err := json.NewDecoder(res.Body).Decode(&svr); err != nil {
return nil, 0, fmt.Errorf("DoSyncV2: response body decode JSON failed: %w", err)
}
return &svr, 200, nil
default:
return nil, res.StatusCode, fmt.Errorf("DoSyncV2: response returned %s", res.Status)
}
}
func (v *HTTPClient) createSyncURL(since string, isFirst, toDeviceOnly bool) string {
qps := "?"
if isFirst { // first time polling for v2-sync in this process
qps += "timeout=0"
} else {
qps += "timeout=30000"
}
if since != "" {
qps += "&since=" + since
}
// To reduce the likelihood of a gappy v2 sync, ask for a large timeline by default.
// Synapse's default is 10; 50 is the maximum allowed, by my reading of
// https://github.com/matrix-org/synapse/blob/89a71e73905ffa1c97ae8be27d521cd2ef3f3a0c/synapse/handlers/sync.py#L576-L577
// NB: this is a stopgap to reduce the likelihood of hitting
// https://github.com/matrix-org/sliding-sync/issues/18
timelineLimit := 50
if since == "" {
// First time the poller has sync v2-ed for this user
timelineLimit = 1
}
room := map[string]interface{}{}
room["timeline"] = map[string]interface{}{"limit": timelineLimit}
if toDeviceOnly {
// no rooms match this filter, so we get everything but room data
room["rooms"] = []string{}
}
filter := map[string]interface{}{
"room": room,
}
filterJSON, _ := json.Marshal(filter)
qps += "&filter=" + url.QueryEscape(string(filterJSON))
return v.DestinationServer + "/_matrix/client/r0/sync" + qps
}
type SyncResponse struct {
NextBatch string `json:"next_batch"`
AccountData EventsResponse `json:"account_data"`
Presence struct {
Events []gomatrixserverlib.ClientEvent `json:"events,omitempty"`
} `json:"presence"`
Rooms SyncRoomsResponse `json:"rooms"`
ToDevice EventsResponse `json:"to_device"`
DeviceLists struct {
Changed []string `json:"changed,omitempty"`
Left []string `json:"left,omitempty"`
} `json:"device_lists"`
DeviceListsOTKCount map[string]int `json:"device_one_time_keys_count,omitempty"`
DeviceUnusedFallbackKeyTypes []string `json:"device_unused_fallback_key_types,omitempty"`
}
type SyncRoomsResponse struct {
Join map[string]SyncV2JoinResponse `json:"join"`
Invite map[string]SyncV2InviteResponse `json:"invite"`
Leave map[string]SyncV2LeaveResponse `json:"leave"`
}
// JoinResponse represents a /sync response for a room which is under the 'join' or 'peek' key.
type SyncV2JoinResponse struct {
State EventsResponse `json:"state"`
Timeline TimelineResponse `json:"timeline"`
Ephemeral EventsResponse `json:"ephemeral"`
AccountData EventsResponse `json:"account_data"`
UnreadNotifications UnreadNotifications `json:"unread_notifications"`
}
type UnreadNotifications struct {
HighlightCount *int `json:"highlight_count,omitempty"`
NotificationCount *int `json:"notification_count,omitempty"`
}
type TimelineResponse struct {
Events []json.RawMessage `json:"events"`
Limited bool `json:"limited"`
PrevBatch string `json:"prev_batch,omitempty"`
}
type EventsResponse struct {
Events []json.RawMessage `json:"events"`
}
// InviteResponse represents a /sync response for a room which is under the 'invite' key.
type SyncV2InviteResponse struct {
InviteState EventsResponse `json:"invite_state"`
}
// LeaveResponse represents a /sync response for a room which is under the 'leave' key.
type SyncV2LeaveResponse struct {
State struct {
Events []json.RawMessage `json:"events"`
} `json:"state"`
Timeline struct {
Events []json.RawMessage `json:"events"`
Limited bool `json:"limited"`
PrevBatch string `json:"prev_batch,omitempty"`
} `json:"timeline"`
}