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"` }