2021-06-09 17:27:54 +01:00
|
|
|
package sync2
|
2021-05-14 16:49:33 +01:00
|
|
|
|
|
|
|
import (
|
2022-12-14 18:53:55 +00:00
|
|
|
"context"
|
2021-05-14 16:49:33 +01:00
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2023-11-16 13:57:51 +00:00
|
|
|
"github.com/matrix-org/sliding-sync/internal"
|
2023-11-13 22:08:17 +00:00
|
|
|
"io"
|
2021-05-14 16:49:33 +01:00
|
|
|
"net/http"
|
2022-07-21 16:20:59 +01:00
|
|
|
"net/url"
|
2023-06-27 20:07:21 +02:00
|
|
|
"time"
|
2021-05-14 16:49:33 +01:00
|
|
|
|
2023-09-19 11:48:49 +02:00
|
|
|
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
|
|
|
|
2021-06-16 18:56:31 +01:00
|
|
|
"github.com/tidwall/gjson"
|
2021-05-14 16:49:33 +01:00
|
|
|
)
|
|
|
|
|
2021-11-09 15:08:08 +00:00
|
|
|
const AccountDataGlobalRoom = ""
|
|
|
|
|
2022-12-14 18:53:55 +00:00
|
|
|
var ProxyVersion = ""
|
2023-03-01 16:40:15 +00:00
|
|
|
var HTTP401 error = fmt.Errorf("HTTP 401")
|
2022-12-14 18:53:55 +00:00
|
|
|
|
2021-07-21 16:35:36 +01:00
|
|
|
type Client interface {
|
2023-09-26 13:24:26 +01:00
|
|
|
// Versions fetches and parses the list of Matrix versions that the homeserver
|
|
|
|
// advertises itself as supporting.
|
|
|
|
Versions(ctx context.Context) (version []string, err error)
|
2023-04-11 22:14:15 +01:00
|
|
|
// 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.)
|
2023-09-13 16:58:44 +02:00
|
|
|
WhoAmI(ctx context.Context, accessToken string) (userID, deviceID string, err error)
|
2023-01-05 18:25:25 +00:00
|
|
|
DoSyncV2(ctx context.Context, accessToken, since string, isFirst bool, toDeviceOnly bool) (*SyncResponse, int, error)
|
2021-07-21 16:35:36 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// HTTPClient represents a Sync v2 Client.
|
2021-06-04 13:02:28 +01:00
|
|
|
// One client can be shared among many users.
|
2021-07-21 16:35:36 +01:00
|
|
|
type HTTPClient struct {
|
2021-05-14 16:49:33 +01:00
|
|
|
Client *http.Client
|
2023-09-19 11:48:49 +02:00
|
|
|
LongTimeoutClient *http.Client
|
2021-05-14 16:49:33 +01:00
|
|
|
DestinationServer string
|
|
|
|
}
|
|
|
|
|
2023-09-19 11:48:49 +02:00
|
|
|
func NewHTTPClient(shortTimeout, longTimeout time.Duration, destHomeServer string) *HTTPClient {
|
|
|
|
return &HTTPClient{
|
2023-11-16 19:31:43 +00:00
|
|
|
LongTimeoutClient: newClient(longTimeout, destHomeServer),
|
|
|
|
Client: newClient(shortTimeout, destHomeServer),
|
|
|
|
DestinationServer: internal.GetBaseURL(destHomeServer),
|
2023-11-13 22:08:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-16 19:31:43 +00:00
|
|
|
func newClient(timeout time.Duration, destHomeServer string) *http.Client {
|
2023-11-13 22:08:17 +00:00
|
|
|
transport := http.DefaultTransport
|
2023-11-16 19:31:43 +00:00
|
|
|
if internal.IsUnixSocket(destHomeServer) {
|
|
|
|
transport = internal.UnixTransport(destHomeServer)
|
2023-11-13 22:08:17 +00:00
|
|
|
}
|
|
|
|
return &http.Client{
|
|
|
|
Timeout: timeout,
|
|
|
|
Transport: otelhttp.NewTransport(transport),
|
2023-09-19 11:48:49 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-26 13:24:26 +01:00
|
|
|
func (v *HTTPClient) Versions(ctx context.Context) ([]string, error) {
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", v.DestinationServer+"/_matrix/client/versions", nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
req.Header.Set("User-Agent", "sync-v3-proxy-"+ProxyVersion)
|
|
|
|
res, err := v.Client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if res.StatusCode != http.StatusOK {
|
|
|
|
return nil, fmt.Errorf("/versions returned HTTP %d", res.StatusCode)
|
|
|
|
}
|
|
|
|
defer res.Body.Close()
|
2023-11-13 22:08:17 +00:00
|
|
|
body, err := io.ReadAll(res.Body)
|
2023-09-26 13:24:26 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
var parsedRes struct {
|
|
|
|
Result []string `json:"versions"`
|
|
|
|
}
|
|
|
|
err = json.Unmarshal(body, &parsedRes)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("could not parse /versions response: %w", err)
|
|
|
|
}
|
|
|
|
return parsedRes.Result, nil
|
|
|
|
}
|
|
|
|
|
2023-03-01 16:40:15 +00:00
|
|
|
// Return sync2.HTTP401 if this request returns 401
|
2023-09-13 16:58:44 +02:00
|
|
|
func (v *HTTPClient) WhoAmI(ctx context.Context, accessToken string) (string, string, error) {
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", v.DestinationServer+"/_matrix/client/r0/account/whoami", nil)
|
2021-06-16 18:56:31 +01:00
|
|
|
if err != nil {
|
2023-04-11 22:14:15 +01:00
|
|
|
return "", "", err
|
2021-06-16 18:56:31 +01:00
|
|
|
}
|
2022-12-14 18:53:55 +00:00
|
|
|
req.Header.Set("User-Agent", "sync-v3-proxy-"+ProxyVersion)
|
2022-07-14 10:48:45 +01:00
|
|
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
2021-06-16 18:56:31 +01:00
|
|
|
res, err := v.Client.Do(req)
|
|
|
|
if err != nil {
|
2023-04-11 22:14:15 +01:00
|
|
|
return "", "", err
|
2021-06-16 18:56:31 +01:00
|
|
|
}
|
|
|
|
if res.StatusCode != 200 {
|
2023-03-01 16:40:15 +00:00
|
|
|
if res.StatusCode == 401 {
|
2023-04-11 22:14:15 +01:00
|
|
|
return "", "", HTTP401
|
2023-03-01 16:40:15 +00:00
|
|
|
}
|
2023-04-11 22:14:15 +01:00
|
|
|
return "", "", fmt.Errorf("/whoami returned HTTP %d", res.StatusCode)
|
2021-06-16 18:56:31 +01:00
|
|
|
}
|
|
|
|
defer res.Body.Close()
|
2023-11-13 22:08:17 +00:00
|
|
|
body, err := io.ReadAll(res.Body)
|
2021-06-16 18:56:31 +01:00
|
|
|
if err != nil {
|
2023-04-11 22:14:15 +01:00
|
|
|
return "", "", err
|
2021-06-16 18:56:31 +01:00
|
|
|
}
|
2023-04-11 22:14:15 +01:00
|
|
|
response := gjson.ParseBytes(body)
|
|
|
|
return response.Get("user_id").Str, response.Get("device_id").Str, nil
|
2021-06-16 18:56:31 +01:00
|
|
|
}
|
|
|
|
|
2021-06-04 13:02:28 +01:00
|
|
|
// DoSyncV2 performs a sync v2 request. Returns the sync response and the response status code
|
2021-10-29 13:15:39 +01:00
|
|
|
// or an error. Set isFirst=true on the first sync to force a timeout=0 sync to ensure snapiness.
|
2023-01-05 18:25:25 +00:00
|
|
|
func (v *HTTPClient) DoSyncV2(ctx context.Context, accessToken, since string, isFirst, toDeviceOnly bool) (*SyncResponse, int, error) {
|
|
|
|
syncURL := v.createSyncURL(since, isFirst, toDeviceOnly)
|
2023-09-13 16:58:44 +02:00
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", syncURL, nil)
|
2022-12-14 18:53:55 +00:00
|
|
|
req.Header.Set("User-Agent", "sync-v3-proxy-"+ProxyVersion)
|
2022-07-14 10:48:45 +01:00
|
|
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
2021-05-14 16:49:33 +01:00
|
|
|
if err != nil {
|
2021-06-04 13:02:28 +01:00
|
|
|
return nil, 0, fmt.Errorf("DoSyncV2: NewRequest failed: %w", err)
|
2021-05-14 16:49:33 +01:00
|
|
|
}
|
2023-06-27 20:07:21 +02:00
|
|
|
var res *http.Response
|
|
|
|
if isFirst {
|
2023-09-19 11:48:49 +02:00
|
|
|
res, err = v.LongTimeoutClient.Do(req)
|
2023-06-27 20:07:21 +02:00
|
|
|
} else {
|
|
|
|
res, err = v.Client.Do(req)
|
|
|
|
}
|
2021-05-14 16:49:33 +01:00
|
|
|
if err != nil {
|
2021-06-04 13:02:28 +01:00
|
|
|
return nil, 0, fmt.Errorf("DoSyncV2: request failed: %w", err)
|
2021-05-14 16:49:33 +01:00
|
|
|
}
|
|
|
|
switch res.StatusCode {
|
|
|
|
case 200:
|
2021-06-04 13:02:28 +01:00
|
|
|
var svr SyncResponse
|
2021-05-14 16:49:33 +01:00
|
|
|
if err := json.NewDecoder(res.Body).Decode(&svr); err != nil {
|
2021-06-04 13:02:28 +01:00
|
|
|
return nil, 0, fmt.Errorf("DoSyncV2: response body decode JSON failed: %w", err)
|
2021-05-14 16:49:33 +01:00
|
|
|
}
|
2021-06-04 13:02:28 +01:00
|
|
|
return &svr, 200, nil
|
2021-05-14 16:49:33 +01:00
|
|
|
default:
|
2021-06-04 13:02:28 +01:00
|
|
|
return nil, res.StatusCode, fmt.Errorf("DoSyncV2: response returned %s", res.Status)
|
2021-05-14 16:49:33 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-05 18:25:25 +00:00
|
|
|
func (v *HTTPClient) createSyncURL(since string, isFirst, toDeviceOnly bool) string {
|
|
|
|
qps := "?"
|
2023-04-14 17:57:04 +01:00
|
|
|
if isFirst { // first time polling for v2-sync in this process
|
2023-01-05 18:25:25 +00:00
|
|
|
qps += "timeout=0"
|
|
|
|
} else {
|
|
|
|
qps += "timeout=30000"
|
|
|
|
}
|
|
|
|
if since != "" {
|
2024-05-20 11:57:24 +02:00
|
|
|
qps += "&since=" + url.QueryEscape(since)
|
2023-01-05 18:25:25 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 11:28:31 +02:00
|
|
|
// Set presence to offline, this potentially reduces CPU load on upstream homeservers
|
|
|
|
qps += "&set_presence=offline"
|
|
|
|
|
2023-04-14 17:57:04 +01:00
|
|
|
// 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
|
2023-01-05 18:25:25 +00:00
|
|
|
}
|
2023-04-14 17:57:04 +01:00
|
|
|
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,
|
2023-09-07 12:05:44 +02:00
|
|
|
// filter out all presence events (remove this once/if the proxy handles presence)
|
|
|
|
"presence": map[string]interface{}{"not_types": []string{"*"}},
|
2023-04-14 17:57:04 +01:00
|
|
|
}
|
|
|
|
filterJSON, _ := json.Marshal(filter)
|
|
|
|
qps += "&filter=" + url.QueryEscape(string(filterJSON))
|
|
|
|
|
2023-01-05 18:25:25 +00:00
|
|
|
return v.DestinationServer + "/_matrix/client/r0/sync" + qps
|
|
|
|
}
|
|
|
|
|
2021-06-04 13:02:28 +01:00
|
|
|
type SyncResponse struct {
|
2021-11-11 12:39:19 +00:00
|
|
|
NextBatch string `json:"next_batch"`
|
|
|
|
AccountData EventsResponse `json:"account_data"`
|
|
|
|
Presence struct {
|
2023-08-31 17:06:44 +01:00
|
|
|
Events []json.RawMessage `json:"events,omitempty"`
|
2021-05-14 16:49:33 +01:00
|
|
|
} `json:"presence"`
|
2021-12-14 11:51:47 +00:00
|
|
|
Rooms SyncRoomsResponse `json:"rooms"`
|
|
|
|
ToDevice EventsResponse `json:"to_device"`
|
2021-05-14 16:49:33 +01:00
|
|
|
DeviceLists struct {
|
|
|
|
Changed []string `json:"changed,omitempty"`
|
|
|
|
Left []string `json:"left,omitempty"`
|
|
|
|
} `json:"device_lists"`
|
2022-08-09 10:05:18 +01:00
|
|
|
DeviceListsOTKCount map[string]int `json:"device_one_time_keys_count,omitempty"`
|
|
|
|
DeviceUnusedFallbackKeyTypes []string `json:"device_unused_fallback_key_types,omitempty"`
|
2021-05-14 16:49:33 +01:00
|
|
|
}
|
|
|
|
|
2021-10-25 18:03:32 +01:00
|
|
|
type SyncRoomsResponse struct {
|
|
|
|
Join map[string]SyncV2JoinResponse `json:"join"`
|
|
|
|
Invite map[string]SyncV2InviteResponse `json:"invite"`
|
|
|
|
Leave map[string]SyncV2LeaveResponse `json:"leave"`
|
|
|
|
}
|
|
|
|
|
2021-05-14 16:49:33 +01:00
|
|
|
// JoinResponse represents a /sync response for a room which is under the 'join' or 'peek' key.
|
|
|
|
type SyncV2JoinResponse struct {
|
2021-11-03 11:07:01 +00:00
|
|
|
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"`
|
2021-05-14 16:49:33 +01:00
|
|
|
}
|
|
|
|
|
2021-10-25 18:03:32 +01:00
|
|
|
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"`
|
|
|
|
}
|
|
|
|
|
2021-05-14 16:49:33 +01:00
|
|
|
// InviteResponse represents a /sync response for a room which is under the 'invite' key.
|
|
|
|
type SyncV2InviteResponse struct {
|
2022-03-25 13:07:12 +00:00
|
|
|
InviteState EventsResponse `json:"invite_state"`
|
2021-05-14 16:49:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// LeaveResponse represents a /sync response for a room which is under the 'leave' key.
|
|
|
|
type SyncV2LeaveResponse struct {
|
|
|
|
State struct {
|
2021-09-30 18:23:52 +01:00
|
|
|
Events []json.RawMessage `json:"events"`
|
2021-05-14 16:49:33 +01:00
|
|
|
} `json:"state"`
|
|
|
|
Timeline struct {
|
2021-09-30 18:23:52 +01:00
|
|
|
Events []json.RawMessage `json:"events"`
|
|
|
|
Limited bool `json:"limited"`
|
|
|
|
PrevBatch string `json:"prev_batch,omitempty"`
|
2021-05-14 16:49:33 +01:00
|
|
|
} `json:"timeline"`
|
|
|
|
}
|