mautrix-go/responses.go
Toni Spets ad20a9218f
Beeper extension for client side media dedup (#267)
The client can send a unique id (like a hash) of a file it intends to
upload in the create request. If the server already has the file it will
return a "completed" response.

The unique id is meant to be opaque for the server. For privacy reasons
it is recommended not to use the raw hash of a file.  The returned MXC
should be stable for the same unique id for the same user but is not
guaranteed.

The room id is used to tie the lifecycle of created media to an existing
room on the homeserver. If a room is purged from the homeserver the
media will be purged along with it.

If the file has been created but not uploaded the response will not have
a completed timestamp which allows the client to retry sending the file.
If the upload has already been completed the upload URL will be empty.

It is possible for multiple clients to send a create request
simultaneously with the same unique id and upload the file at the same
time. It is also possible for the server to forget the unique id and
allow reuploading the same file again returning a new MXC.

This commit also fixes UnusedExpiresAt type in the response which is a
breaking change.
2024-08-21 19:19:03 +03:00

625 lines
21 KiB
Go

package mautrix
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"strconv"
"strings"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
"go.mau.fi/util/jsontime"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
// RespWhoami is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3accountwhoami
type RespWhoami struct {
UserID id.UserID `json:"user_id"`
DeviceID id.DeviceID `json:"device_id"`
}
// RespCreateFilter is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3useruseridfilter
type RespCreateFilter struct {
FilterID string `json:"filter_id"`
}
// RespJoinRoom is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidjoin
type RespJoinRoom struct {
RoomID id.RoomID `json:"room_id"`
}
// RespLeaveRoom is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidleave
type RespLeaveRoom struct{}
// RespForgetRoom is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidforget
type RespForgetRoom struct{}
// RespInviteUser is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidinvite
type RespInviteUser struct{}
// RespKickUser is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidkick
type RespKickUser struct{}
// RespBanUser is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidban
type RespBanUser struct{}
// RespUnbanUser is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidunban
type RespUnbanUser struct{}
// RespTyping is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3roomsroomidtypinguserid
type RespTyping struct{}
// RespPresence is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3presenceuseridstatus
type RespPresence struct {
Presence event.Presence `json:"presence"`
LastActiveAgo int `json:"last_active_ago"`
StatusMsg string `json:"status_msg"`
CurrentlyActive bool `json:"currently_active"`
}
// RespJoinedRooms is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3joined_rooms
type RespJoinedRooms struct {
JoinedRooms []id.RoomID `json:"joined_rooms"`
}
// RespJoinedMembers is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3roomsroomidjoined_members
type RespJoinedMembers struct {
Joined map[id.UserID]JoinedMember `json:"joined"`
}
type JoinedMember struct {
DisplayName string `json:"display_name,omitempty"`
AvatarURL string `json:"avatar_url,omitempty"`
}
// RespMessages is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3roomsroomidmessages
type RespMessages struct {
Start string `json:"start"`
Chunk []*event.Event `json:"chunk"`
State []*event.Event `json:"state"`
End string `json:"end,omitempty"`
}
// RespContext is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3roomsroomidcontexteventid
type RespContext struct {
End string `json:"end"`
Event *event.Event `json:"event"`
EventsAfter []*event.Event `json:"events_after"`
EventsBefore []*event.Event `json:"events_before"`
Start string `json:"start"`
State []*event.Event `json:"state"`
}
// RespSendEvent is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3roomsroomidsendeventtypetxnid
type RespSendEvent struct {
EventID id.EventID `json:"event_id"`
}
// RespMediaConfig is the JSON response for https://spec.matrix.org/v1.4/client-server-api/#get_matrixmediav3config
type RespMediaConfig struct {
UploadSize int64 `json:"m.upload.size,omitempty"`
}
// RespMediaUpload is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixmediav3upload
type RespMediaUpload struct {
ContentURI id.ContentURI `json:"content_uri"`
}
// RespCreateMXC is the JSON response for https://spec.matrix.org/v1.7/client-server-api/#post_matrixmediav1create
type RespCreateMXC struct {
ContentURI id.ContentURI `json:"content_uri"`
UnusedExpiresAt jsontime.UnixMilli `json:"unused_expires_at,omitempty"`
UnstableUploadURL string `json:"com.beeper.msc3870.upload_url,omitempty"`
// Beeper extensions for uploading unique media only once
BeeperUniqueID string `json:"com.beeper.unique_id,omitempty"`
BeeperCompletedAt jsontime.UnixMilli `json:"com.beeper.completed_at,omitempty"`
}
// RespPreviewURL is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixmediav3preview_url
type RespPreviewURL = event.LinkPreview
// RespUserInteractive is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#user-interactive-authentication-api
type RespUserInteractive struct {
Flows []UIAFlow `json:"flows,omitempty"`
Params map[AuthType]interface{} `json:"params,omitempty"`
Session string `json:"session,omitempty"`
Completed []string `json:"completed,omitempty"`
ErrCode string `json:"errcode,omitempty"`
Error string `json:"error,omitempty"`
}
type UIAFlow struct {
Stages []AuthType `json:"stages,omitempty"`
}
// HasSingleStageFlow returns true if there exists at least 1 Flow with a single stage of stageName.
func (r RespUserInteractive) HasSingleStageFlow(stageName AuthType) bool {
for _, f := range r.Flows {
if len(f.Stages) == 1 && f.Stages[0] == stageName {
return true
}
}
return false
}
// RespUserDisplayName is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3profileuseriddisplayname
type RespUserDisplayName struct {
DisplayName string `json:"displayname"`
}
type RespUserProfile struct {
DisplayName string `json:"displayname"`
AvatarURL id.ContentURI `json:"avatar_url"`
}
// RespRegisterAvailable is the JSON response for https://spec.matrix.org/v1.4/client-server-api/#get_matrixclientv3registeravailable
type RespRegisterAvailable struct {
Available bool `json:"available"`
}
// RespRegister is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3register
type RespRegister struct {
AccessToken string `json:"access_token,omitempty"`
DeviceID id.DeviceID `json:"device_id,omitempty"`
UserID id.UserID `json:"user_id"`
RefreshToken string `json:"refresh_token,omitempty"`
ExpiresInMS int64 `json:"expires_in_ms,omitempty"`
// Deprecated: homeserver should be parsed from the user ID
HomeServer string `json:"home_server,omitempty"`
}
type LoginFlow struct {
Type AuthType `json:"type"`
}
// RespLoginFlows is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3login
type RespLoginFlows struct {
Flows []LoginFlow `json:"flows"`
}
func (rlf *RespLoginFlows) FirstFlowOfType(flowTypes ...AuthType) *LoginFlow {
for _, flow := range rlf.Flows {
for _, flowType := range flowTypes {
if flow.Type == flowType {
return &flow
}
}
}
return nil
}
func (rlf *RespLoginFlows) HasFlow(flowType ...AuthType) bool {
return rlf.FirstFlowOfType(flowType...) != nil
}
// RespLogin is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3login
type RespLogin struct {
AccessToken string `json:"access_token"`
DeviceID id.DeviceID `json:"device_id"`
UserID id.UserID `json:"user_id"`
WellKnown *ClientWellKnown `json:"well_known,omitempty"`
}
// RespLogout is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3logout
type RespLogout struct{}
// RespCreateRoom is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3createroom
type RespCreateRoom struct {
RoomID id.RoomID `json:"room_id"`
}
type RespMembers struct {
Chunk []*event.Event `json:"chunk"`
}
type LazyLoadSummary struct {
Heroes []id.UserID `json:"m.heroes,omitempty"`
JoinedMemberCount *int `json:"m.joined_member_count,omitempty"`
InvitedMemberCount *int `json:"m.invited_member_count,omitempty"`
}
type SyncEventsList struct {
Events []*event.Event `json:"events,omitempty"`
}
type SyncTimeline struct {
SyncEventsList
Limited bool `json:"limited,omitempty"`
PrevBatch string `json:"prev_batch,omitempty"`
}
// RespSync is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3sync
type RespSync struct {
NextBatch string `json:"next_batch"`
AccountData SyncEventsList `json:"account_data"`
Presence SyncEventsList `json:"presence"`
ToDevice SyncEventsList `json:"to_device"`
DeviceLists DeviceLists `json:"device_lists"`
DeviceOTKCount OTKCount `json:"device_one_time_keys_count"`
FallbackKeys []id.KeyAlgorithm `json:"device_unused_fallback_key_types"`
Rooms RespSyncRooms `json:"rooms"`
}
type RespSyncRooms struct {
Leave map[id.RoomID]*SyncLeftRoom `json:"leave,omitempty"`
Join map[id.RoomID]*SyncJoinedRoom `json:"join,omitempty"`
Invite map[id.RoomID]*SyncInvitedRoom `json:"invite,omitempty"`
Knock map[id.RoomID]*SyncKnockedRoom `json:"knock,omitempty"`
}
type marshalableRespSync RespSync
var syncPathsToDelete = []string{"account_data", "presence", "to_device", "device_lists", "device_one_time_keys_count", "rooms"}
// marshalAndDeleteEmpty marshals a JSON object, then uses gjson to delete empty objects at the given gjson paths.
func marshalAndDeleteEmpty(marshalable interface{}, paths []string) ([]byte, error) {
data, err := json.Marshal(marshalable)
if err != nil {
return nil, err
}
for _, path := range paths {
res := gjson.GetBytes(data, path)
if res.IsObject() && len(res.Raw) == 2 {
data, err = sjson.DeleteBytes(data, path)
if err != nil {
return nil, fmt.Errorf("failed to delete empty %s: %w", path, err)
}
}
}
return data, nil
}
func (rs *RespSync) MarshalJSON() ([]byte, error) {
return marshalAndDeleteEmpty((*marshalableRespSync)(rs), syncPathsToDelete)
}
type DeviceLists struct {
Changed []id.UserID `json:"changed,omitempty"`
Left []id.UserID `json:"left,omitempty"`
}
type OTKCount struct {
Curve25519 int `json:"curve25519,omitempty"`
SignedCurve25519 int `json:"signed_curve25519,omitempty"`
// For appservice OTK counts only: the user ID in question
UserID id.UserID `json:"-"`
DeviceID id.DeviceID `json:"-"`
}
type SyncLeftRoom struct {
Summary LazyLoadSummary `json:"summary"`
State SyncEventsList `json:"state"`
Timeline SyncTimeline `json:"timeline"`
}
type marshalableSyncLeftRoom SyncLeftRoom
var syncLeftRoomPathsToDelete = []string{"summary", "state", "timeline"}
func (slr SyncLeftRoom) MarshalJSON() ([]byte, error) {
return marshalAndDeleteEmpty((marshalableSyncLeftRoom)(slr), syncLeftRoomPathsToDelete)
}
type BeeperInboxPreviewEvent struct {
EventID id.EventID `json:"event_id"`
Timestamp jsontime.UnixMilli `json:"origin_server_ts"`
Event *event.Event `json:"event,omitempty"`
}
type SyncJoinedRoom struct {
Summary LazyLoadSummary `json:"summary"`
State SyncEventsList `json:"state"`
Timeline SyncTimeline `json:"timeline"`
Ephemeral SyncEventsList `json:"ephemeral"`
AccountData SyncEventsList `json:"account_data"`
UnreadNotifications *UnreadNotificationCounts `json:"unread_notifications,omitempty"`
// https://github.com/matrix-org/matrix-spec-proposals/pull/2654
MSC2654UnreadCount *int `json:"org.matrix.msc2654.unread_count,omitempty"`
// Beeper extension
BeeperInboxPreview *BeeperInboxPreviewEvent `json:"com.beeper.inbox.preview,omitempty"`
}
type UnreadNotificationCounts struct {
HighlightCount int `json:"highlight_count"`
NotificationCount int `json:"notification_count"`
}
type marshalableSyncJoinedRoom SyncJoinedRoom
var syncJoinedRoomPathsToDelete = []string{"summary", "state", "timeline", "ephemeral", "account_data"}
func (sjr SyncJoinedRoom) MarshalJSON() ([]byte, error) {
return marshalAndDeleteEmpty((marshalableSyncJoinedRoom)(sjr), syncJoinedRoomPathsToDelete)
}
type SyncInvitedRoom struct {
Summary LazyLoadSummary `json:"summary"`
State SyncEventsList `json:"invite_state"`
}
type marshalableSyncInvitedRoom SyncInvitedRoom
var syncInvitedRoomPathsToDelete = []string{"summary"}
func (sir SyncInvitedRoom) MarshalJSON() ([]byte, error) {
return marshalAndDeleteEmpty((marshalableSyncInvitedRoom)(sir), syncInvitedRoomPathsToDelete)
}
type SyncKnockedRoom struct {
State SyncEventsList `json:"knock_state"`
}
type RespTurnServer struct {
Username string `json:"username"`
Password string `json:"password"`
TTL int `json:"ttl"`
URIs []string `json:"uris"`
}
type RespAliasCreate struct{}
type RespAliasDelete struct{}
type RespAliasResolve struct {
RoomID id.RoomID `json:"room_id"`
Servers []string `json:"servers"`
}
type RespAliasList struct {
Aliases []id.RoomAlias `json:"aliases"`
}
type RespUploadKeys struct {
OneTimeKeyCounts OTKCount `json:"one_time_key_counts"`
}
type RespQueryKeys struct {
Failures map[string]interface{} `json:"failures,omitempty"`
DeviceKeys map[id.UserID]map[id.DeviceID]DeviceKeys `json:"device_keys"`
MasterKeys map[id.UserID]CrossSigningKeys `json:"master_keys"`
SelfSigningKeys map[id.UserID]CrossSigningKeys `json:"self_signing_keys"`
UserSigningKeys map[id.UserID]CrossSigningKeys `json:"user_signing_keys"`
}
type RespClaimKeys struct {
Failures map[string]interface{} `json:"failures,omitempty"`
OneTimeKeys map[id.UserID]map[id.DeviceID]map[id.KeyID]OneTimeKey `json:"one_time_keys"`
}
type RespUploadSignatures struct {
Failures map[string]interface{} `json:"failures,omitempty"`
}
type RespKeyChanges struct {
Changed []id.UserID `json:"changed"`
Left []id.UserID `json:"left"`
}
type RespSendToDevice struct{}
// RespDevicesInfo is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3devices
type RespDevicesInfo struct {
Devices []RespDeviceInfo `json:"devices"`
}
// RespDeviceInfo is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3devicesdeviceid
type RespDeviceInfo struct {
DeviceID id.DeviceID `json:"device_id"`
DisplayName string `json:"display_name"`
LastSeenIP string `json:"last_seen_ip"`
LastSeenTS int64 `json:"last_seen_ts"`
}
// Deprecated: MSC2716 was abandoned
type RespBatchSend struct {
StateEventIDs []id.EventID `json:"state_event_ids"`
EventIDs []id.EventID `json:"event_ids"`
InsertionEventID id.EventID `json:"insertion_event_id"`
BatchEventID id.EventID `json:"batch_event_id"`
BaseInsertionEventID id.EventID `json:"base_insertion_event_id"`
NextBatchID id.BatchID `json:"next_batch_id"`
}
type RespBeeperBatchSend struct {
EventIDs []id.EventID `json:"event_ids"`
}
// RespCapabilities is the JSON response for https://spec.matrix.org/v1.3/client-server-api/#get_matrixclientv3capabilities
type RespCapabilities struct {
RoomVersions *CapRoomVersions `json:"m.room_versions,omitempty"`
ChangePassword *CapBooleanTrue `json:"m.change_password,omitempty"`
SetDisplayname *CapBooleanTrue `json:"m.set_displayname,omitempty"`
SetAvatarURL *CapBooleanTrue `json:"m.set_avatar_url,omitempty"`
ThreePIDChanges *CapBooleanTrue `json:"m.3pid_changes,omitempty"`
Custom map[string]interface{} `json:"-"`
}
type serializableRespCapabilities RespCapabilities
func (rc *RespCapabilities) UnmarshalJSON(data []byte) error {
res := gjson.GetBytes(data, "capabilities")
if !res.Exists() || !res.IsObject() {
return nil
}
if res.Index > 0 {
data = data[res.Index : res.Index+len(res.Raw)]
} else {
data = []byte(res.Raw)
}
err := json.Unmarshal(data, (*serializableRespCapabilities)(rc))
if err != nil {
return err
}
err = json.Unmarshal(data, &rc.Custom)
if err != nil {
return err
}
// Remove non-custom capabilities from the custom map so that they don't get overridden when serializing back
for _, field := range reflect.VisibleFields(reflect.TypeOf(rc).Elem()) {
jsonTag := strings.Split(field.Tag.Get("json"), ",")[0]
if jsonTag != "-" && jsonTag != "" {
delete(rc.Custom, jsonTag)
}
}
return nil
}
func (rc *RespCapabilities) MarshalJSON() ([]byte, error) {
marshalableCopy := make(map[string]interface{}, len(rc.Custom))
val := reflect.ValueOf(rc).Elem()
for _, field := range reflect.VisibleFields(val.Type()) {
jsonTag := strings.Split(field.Tag.Get("json"), ",")[0]
if jsonTag != "-" && jsonTag != "" {
fieldVal := val.FieldByIndex(field.Index)
if !fieldVal.IsNil() {
marshalableCopy[jsonTag] = fieldVal.Interface()
}
}
}
if rc.Custom != nil {
for key, value := range rc.Custom {
marshalableCopy[key] = value
}
}
var buf bytes.Buffer
buf.WriteString(`{"capabilities":`)
err := json.NewEncoder(&buf).Encode(marshalableCopy)
if err != nil {
return nil, err
}
buf.WriteByte('}')
return buf.Bytes(), nil
}
type CapBoolean struct {
Enabled bool `json:"enabled"`
}
type CapBooleanTrue CapBoolean
// IsEnabled returns true if the capability is either enabled explicitly or not specified (nil)
func (cb *CapBooleanTrue) IsEnabled() bool {
// Default to true when
return cb == nil || cb.Enabled
}
type CapBooleanFalse CapBoolean
// IsEnabled returns true if the capability is enabled explicitly. If it's not specified, this returns false.
func (cb *CapBooleanFalse) IsEnabled() bool {
return cb != nil && cb.Enabled
}
type CapRoomVersionStability string
const (
CapRoomVersionStable CapRoomVersionStability = "stable"
CapRoomVersionUnstable CapRoomVersionStability = "unstable"
)
type CapRoomVersions struct {
Default string `json:"default"`
Available map[string]CapRoomVersionStability `json:"available"`
}
func (vers *CapRoomVersions) IsStable(version string) bool {
if vers == nil || vers.Available == nil {
val, err := strconv.Atoi(version)
return err == nil && val > 0
}
return vers.Available[version] == CapRoomVersionStable
}
func (vers *CapRoomVersions) IsAvailable(version string) bool {
if vers == nil || vers.Available == nil {
return false
}
_, available := vers.Available[version]
return available
}
// RespHierarchy is the JSON response for https://spec.matrix.org/v1.4/client-server-api/#get_matrixclientv1roomsroomidhierarchy
type RespHierarchy struct {
NextBatch string `json:"next_batch,omitempty"`
Rooms []ChildRoomsChunk `json:"rooms"`
}
type ChildRoomsChunk struct {
AvatarURL id.ContentURI `json:"avatar_url,omitempty"`
CanonicalAlias id.RoomAlias `json:"canonical_alias,omitempty"`
ChildrenState []StrippedStateWithTime `json:"children_state"`
GuestCanJoin bool `json:"guest_can_join"`
JoinRule event.JoinRule `json:"join_rule,omitempty"`
Name string `json:"name,omitempty"`
NumJoinedMembers int `json:"num_joined_members"`
RoomID id.RoomID `json:"room_id"`
RoomType event.RoomType `json:"room_type"`
Topic string `json:"topic,omitempty"`
WorldReadble bool `json:"world_readable"`
}
type StrippedStateWithTime struct {
event.StrippedState
Timestamp jsontime.UnixMilli `json:"origin_server_ts"`
}
type RespAppservicePing struct {
DurationMS int64 `json:"duration_ms"`
}
type RespBeeperMergeRoom RespCreateRoom
type RespBeeperSplitRoom struct {
RoomIDs map[string]id.RoomID `json:"room_ids"`
}
type RespTimestampToEvent struct {
EventID id.EventID `json:"event_id"`
Timestamp jsontime.UnixMilli `json:"origin_server_ts"`
}
type RespRoomKeysVersionCreate struct {
Version id.KeyBackupVersion `json:"version"`
}
type RespRoomKeysVersion[A any] struct {
Algorithm id.KeyBackupAlgorithm `json:"algorithm"`
AuthData A `json:"auth_data"`
Count int `json:"count"`
ETag string `json:"etag"`
Version id.KeyBackupVersion `json:"version"`
}
type RespRoomKeys[S any] struct {
Rooms map[id.RoomID]RespRoomKeyBackup[S] `json:"rooms"`
}
type RespRoomKeyBackup[S any] struct {
Sessions map[id.SessionID]RespKeyBackupData[S] `json:"sessions"`
}
type RespKeyBackupData[S any] struct {
FirstMessageIndex int `json:"first_message_index"`
ForwardedCount int `json:"forwarded_count"`
IsVerified bool `json:"is_verified"`
SessionData S `json:"session_data"`
}
type RespRoomKeysUpdate struct {
Count int `json:"count"`
ETag string `json:"etag"`
}