From 7cfa3be0c69c9a63658bd6fa56c8c6c4d9fd4c71 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 12 Sep 2024 09:12:49 +0200 Subject: [PATCH] client-api: Implement MSC4186. (#1907) * client-api: Derive `Default` for `v4::SyncList`. * client-api: Implement MSC4186. --- crates/ruma-client-api/Cargo.toml | 1 + crates/ruma-client-api/src/error.rs | 4 +- .../ruma-client-api/src/error/kind_serde.rs | 4 +- .../ruma-client-api/src/sync/sync_events.rs | 3 + .../src/sync/sync_events/v4.rs | 139 ++- .../src/sync/sync_events/v5.rs | 856 ++++++++++++++++++ crates/ruma/Cargo.toml | 2 + 7 files changed, 1003 insertions(+), 6 deletions(-) create mode 100644 crates/ruma-client-api/src/sync/sync_events/v5.rs diff --git a/crates/ruma-client-api/Cargo.toml b/crates/ruma-client-api/Cargo.toml index 64f19f66..4b7d30a4 100644 --- a/crates/ruma-client-api/Cargo.toml +++ b/crates/ruma-client-api/Cargo.toml @@ -51,6 +51,7 @@ unstable-msc3983 = [] unstable-msc4108 = [] unstable-msc4121 = [] unstable-msc4140 = [] +unstable-msc4186 = [] [dependencies] as_variant = { workspace = true } diff --git a/crates/ruma-client-api/src/error.rs b/crates/ruma-client-api/src/error.rs index 9d34ba01..7d2ddeb6 100644 --- a/crates/ruma-client-api/src/error.rs +++ b/crates/ruma-client-api/src/error.rs @@ -173,7 +173,7 @@ pub enum ErrorKind { CannotOverwriteMedia, /// M_UNKNOWN_POS for sliding sync - #[cfg(feature = "unstable-msc3575")] + #[cfg(any(feature = "unstable-msc3575", feature = "unstable-msc4186"))] UnknownPos, /// M_URL_NOT_SET @@ -271,7 +271,7 @@ impl AsRef for ErrorKind { Self::DuplicateAnnotation => "M_DUPLICATE_ANNOTATION", Self::NotYetUploaded => "M_NOT_YET_UPLOADED", Self::CannotOverwriteMedia => "M_CANNOT_OVERWRITE_MEDIA", - #[cfg(feature = "unstable-msc3575")] + #[cfg(any(feature = "unstable-msc3575", feature = "unstable-msc4186"))] Self::UnknownPos => "M_UNKNOWN_POS", Self::UrlNotSet => "M_URL_NOT_SET", Self::BadStatus { .. } => "M_BAD_STATUS", diff --git a/crates/ruma-client-api/src/error/kind_serde.rs b/crates/ruma-client-api/src/error/kind_serde.rs index f6c5cd4c..5121b2f5 100644 --- a/crates/ruma-client-api/src/error/kind_serde.rs +++ b/crates/ruma-client-api/src/error/kind_serde.rs @@ -228,7 +228,7 @@ impl<'de> Visitor<'de> for ErrorKindVisitor { ErrCode::DuplicateAnnotation => ErrorKind::DuplicateAnnotation, ErrCode::NotYetUploaded => ErrorKind::NotYetUploaded, ErrCode::CannotOverwriteMedia => ErrorKind::CannotOverwriteMedia, - #[cfg(feature = "unstable-msc3575")] + #[cfg(any(feature = "unstable-msc3575", feature = "unstable-msc4186"))] ErrCode::UnknownPos => ErrorKind::UnknownPos, ErrCode::UrlNotSet => ErrorKind::UrlNotSet, ErrCode::BadStatus => ErrorKind::BadStatus { @@ -301,7 +301,7 @@ enum ErrCode { NotYetUploaded, #[ruma_enum(alias = "FI.MAU.MSC2246_CANNOT_OVERWRITE_MEDIA")] CannotOverwriteMedia, - #[cfg(feature = "unstable-msc3575")] + #[cfg(any(feature = "unstable-msc3575", feature = "unstable-msc4186"))] UnknownPos, UrlNotSet, BadStatus, diff --git a/crates/ruma-client-api/src/sync/sync_events.rs b/crates/ruma-client-api/src/sync/sync_events.rs index 0e522aeb..84e2141b 100644 --- a/crates/ruma-client-api/src/sync/sync_events.rs +++ b/crates/ruma-client-api/src/sync/sync_events.rs @@ -11,6 +11,9 @@ pub mod v3; #[cfg(feature = "unstable-msc3575")] pub mod v4; +#[cfg(feature = "unstable-msc4186")] +pub mod v5; + /// Unread notifications count. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] diff --git a/crates/ruma-client-api/src/sync/sync_events/v4.rs b/crates/ruma-client-api/src/sync/sync_events/v4.rs index 1f9768ff..03cea5ef 100644 --- a/crates/ruma-client-api/src/sync/sync_events/v4.rs +++ b/crates/ruma-client-api/src/sync/sync_events/v4.rs @@ -22,7 +22,7 @@ use ruma_events::{ }; use serde::{de::Error as _, Deserialize, Serialize}; -use super::{DeviceLists, UnreadNotificationsCount}; +use super::{v5, DeviceLists, UnreadNotificationsCount}; const METADATA: Metadata = metadata! { method: POST, @@ -393,7 +393,7 @@ pub enum SlidingOp { } /// Updates to joined rooms. -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct SyncList { /// The sync operation to apply, if any. @@ -927,6 +927,141 @@ impl Typing { } } +impl From for Request { + fn from(value: v5::Request) -> Self { + Self { + pos: value.pos, + conn_id: value.conn_id, + txn_id: value.txn_id, + timeout: value.timeout, + lists: value + .lists + .into_iter() + .map(|(list_name, list)| (list_name, list.into())) + .collect(), + room_subscriptions: value + .room_subscriptions + .into_iter() + .map(|(room_id, room_subscription)| (room_id, room_subscription.into())) + .collect(), + extensions: value.extensions.into(), + + ..Default::default() + } + } +} + +impl From for SyncRequestList { + fn from(value: v5::request::List) -> Self { + Self { + ranges: value.ranges, + room_details: value.room_details.into(), + include_heroes: value.include_heroes, + filters: value.filters.map(Into::into), + + // Defaults from MSC4186. + sort: vec!["by_recency".to_owned(), "by_name".to_owned()], + bump_event_types: vec![ + TimelineEventType::RoomMessage, + TimelineEventType::RoomEncrypted, + TimelineEventType::RoomCreate, + TimelineEventType::Sticker, + ], + + ..Default::default() + } + } +} + +impl From for RoomDetailsConfig { + fn from(value: v5::request::RoomDetails) -> Self { + Self { required_state: value.required_state, timeline_limit: value.timeline_limit } + } +} + +impl From for SyncRequestListFilters { + fn from(value: v5::request::ListFilters) -> Self { + Self { + is_invite: value.is_invite, + not_room_types: value.not_room_types, + ..Default::default() + } + } +} + +impl From for RoomSubscription { + fn from(value: v5::request::RoomSubscription) -> Self { + Self { + required_state: value.required_state, + timeline_limit: value.timeline_limit, + include_heroes: value.include_heroes, + } + } +} + +impl From for ExtensionsConfig { + fn from(value: v5::request::Extensions) -> Self { + Self { + to_device: value.to_device.into(), + e2ee: value.e2ee.into(), + account_data: value.account_data.into(), + receipts: value.receipts.into(), + typing: value.typing.into(), + + ..Default::default() + } + } +} + +impl From for ToDeviceConfig { + fn from(value: v5::request::ToDevice) -> Self { + Self { + enabled: value.enabled, + limit: value.limit, + since: value.since, + lists: value.lists, + rooms: value.rooms, + } + } +} + +impl From for E2EEConfig { + fn from(value: v5::request::E2EE) -> Self { + Self { enabled: value.enabled } + } +} + +impl From for AccountDataConfig { + fn from(value: v5::request::AccountData) -> Self { + Self { enabled: value.enabled, lists: value.lists, rooms: value.rooms } + } +} + +impl From for ReceiptsConfig { + fn from(value: v5::request::Receipts) -> Self { + Self { + enabled: value.enabled, + lists: value.lists, + rooms: value.rooms.map(|rooms| rooms.into_iter().map(Into::into).collect()), + } + } +} + +impl From for RoomReceiptConfig { + fn from(value: v5::request::ReceiptsRoom) -> Self { + match value { + v5::request::ReceiptsRoom::Room(room_id) => Self::Room(room_id), + _ => Self::AllSubscribed, + } + } +} + +impl From for TypingConfig { + fn from(value: v5::request::Typing) -> Self { + Self { enabled: value.enabled, lists: value.lists, rooms: value.rooms } + } +} + #[cfg(test)] mod tests { use ruma_common::owned_room_id; diff --git a/crates/ruma-client-api/src/sync/sync_events/v5.rs b/crates/ruma-client-api/src/sync/sync_events/v5.rs new file mode 100644 index 00000000..24e7f1bd --- /dev/null +++ b/crates/ruma-client-api/src/sync/sync_events/v5.rs @@ -0,0 +1,856 @@ +//! `POST /_matrix/client/unstable/org.matrix.simplified_msc3575/sync` ([MSC4186]) +//! +//! A simplified version of sliding sync ([MSC3575]). +//! +//! Get all new events in a sliding window of rooms since the last sync or a given point in time. +//! +//! [MSC3575]: https://github.com/matrix-org/matrix-spec-proposals/pull/3575 +//! [MSC4186]: https://github.com/matrix-org/matrix-spec-proposals/pull/4186 + +use std::{collections::BTreeMap, time::Duration}; + +use js_int::UInt; +use js_option::JsOption; +use ruma_common::{ + api::{request, response, Metadata}, + metadata, + serde::{duration::opt_ms, Raw}, + OwnedMxcUri, OwnedRoomId, OwnedUserId, +}; +use ruma_events::{AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, StateEventType}; +use serde::{Deserialize, Serialize}; + +use super::{v4, UnreadNotificationsCount}; + +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/unstable/org.matrix.simplified_msc3575/sync", + // 1.4 => "/_matrix/client/v5/sync", + } +}; + +/// Request type for the `/sync` endpoint. +#[request(error = crate::Error)] +#[derive(Default)] +pub struct Request { + /// A point in time to continue a sync from. + /// + /// This is an opaque value taken from the `pos` field of a previous `/sync` + /// response. A `None` value asks the server to start a new _session_ (mind + /// it can be costly) + #[serde(skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub pos: Option, + + /// A unique string identifier for this connection to the server. + /// + /// If this is missing, only one sliding sync connection can be made to + /// the server at any one time. Clients need to set this to allow more + /// than one connection concurrently, so the server can distinguish between + /// connections. This must be provided with every request, if your client + /// needs more than one concurrent connection. + /// + /// Limitation: it must not contain more than 16 chars, due to it being + /// required with every request. + #[serde(skip_serializing_if = "Option::is_none")] + pub conn_id: Option, + + /// Allows clients to know what request params reached the server, + /// functionally similar to txn IDs on `/send` for events. + #[serde(skip_serializing_if = "Option::is_none")] + pub txn_id: Option, + + /// The maximum time to poll before responding to this request. + /// + /// `None` means no timeout, so virtually an infinite wait from the server. + #[serde(with = "opt_ms", default, skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub timeout: Option, + + /// Lists of rooms we are interested by, represented by ranges. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub lists: BTreeMap, + + /// Specific rooms we are interested by. + /// + /// It is useful to receive updates from rooms that are possibly + /// out-of-range of all the lists (see [`Self::lists`]). + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub room_subscriptions: BTreeMap, + + /// Extensions. + #[serde(default, skip_serializing_if = "request::Extensions::is_empty")] + pub extensions: request::Extensions, +} + +impl Request { + /// Creates an empty `Request`. + pub fn new() -> Self { + Default::default() + } +} + +/// HTTP types related to a [`Request`]. +pub mod request { + use ruma_common::{directory::RoomTypeFilter, serde::deserialize_cow_str, RoomId}; + use serde::de::Error as _; + + use super::{BTreeMap, Deserialize, OwnedRoomId, Serialize, StateEventType, UInt}; + + /// A sliding sync list request (see [`super::Request::lists`]). + #[derive(Clone, Debug, Default, Serialize, Deserialize)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct List { + /// The ranges of rooms we're interested in. + pub ranges: Vec<(UInt, UInt)>, + + /// The details to be included per room. + #[serde(flatten)] + pub room_details: RoomDetails, + + /// Request a stripped variant of membership events for the users used + /// to calculate the room name. + #[serde(skip_serializing_if = "Option::is_none")] + pub include_heroes: Option, + + /// Filters to apply to the list before sorting. + #[serde(skip_serializing_if = "Option::is_none")] + pub filters: Option, + } + + /// A sliding sync list request filters (see [`List::filters`]). + /// + /// All fields are applied with _AND_ operators. The absence of fields + /// implies no filter on that criteria: it does NOT imply `false`. + #[derive(Clone, Debug, Default, Serialize, Deserialize)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct ListFilters { + /// Whether to return invited rooms, only joined rooms or both. + /// + /// Flag which only returns rooms the user is currently invited to. + /// If unset, both invited and joined rooms are returned. If false, + /// no invited rooms are returned. If true, only invited rooms are + /// returned. + #[serde(skip_serializing_if = "Option::is_none")] + pub is_invite: Option, + + /// Only list rooms that are not of these create-types, or all. + /// + /// This can be used to filter out spaces from the room list. + #[serde(default, skip_serializing_if = "<[_]>::is_empty")] + pub not_room_types: Vec, + } + + /// Sliding sync request room subscription (see [`super::Request::room_subscriptions`]). + #[derive(Clone, Debug, Default, Serialize, Deserialize)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct RoomSubscription { + /// Required state for each returned room. An array of event type and + /// state key tuples. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub required_state: Vec<(StateEventType, String)>, + + /// The maximum number of timeline events to return per room. + #[serde(skip_serializing_if = "Option::is_none")] + pub timeline_limit: Option, + + /// Include the room heroes. + #[serde(skip_serializing_if = "Option::is_none")] + pub include_heroes: Option, + } + + /// Sliding sync request room details (see [`List::room_details`]). + #[derive(Clone, Debug, Default, Serialize, Deserialize)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct RoomDetails { + /// Required state for each returned room. An array of event type and state key tuples. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub required_state: Vec<(StateEventType, String)>, + + /// The maximum number of timeline events to return per room. + #[serde(skip_serializing_if = "Option::is_none")] + pub timeline_limit: Option, + } + + /// Sliding sync request extensions (see [`super::Request::extensions`]). + #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct Extensions { + /// Configure the to-device extension. + #[serde(default, skip_serializing_if = "ToDevice::is_empty")] + pub to_device: ToDevice, + + /// Configure the E2EE extension. + #[serde(default, skip_serializing_if = "E2EE::is_empty")] + pub e2ee: E2EE, + + /// Configure the account data extension. + #[serde(default, skip_serializing_if = "AccountData::is_empty")] + pub account_data: AccountData, + + /// Configure the receipts extension. + #[serde(default, skip_serializing_if = "Receipts::is_empty")] + pub receipts: Receipts, + + /// Configure the typing extension. + #[serde(default, skip_serializing_if = "Typing::is_empty")] + pub typing: Typing, + + /// Extensions may add further fields to the list. + #[serde(flatten)] + other: BTreeMap, + } + + impl Extensions { + /// Whether all fields are empty or `None`. + pub fn is_empty(&self) -> bool { + self.to_device.is_empty() + && self.e2ee.is_empty() + && self.account_data.is_empty() + && self.receipts.is_empty() + && self.typing.is_empty() + && self.other.is_empty() + } + } + + /// To-device messages extension. + /// + /// According to [MSC3885](https://github.com/matrix-org/matrix-spec-proposals/pull/3885). + #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct ToDevice { + /// Activate or deactivate this extension. + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled: Option, + + /// Maximum number of to-device messages per response. + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + + /// Give messages since this token only. + #[serde(skip_serializing_if = "Option::is_none")] + pub since: Option, + + /// List of list names for which to-device events should be enabled. + /// + /// If not defined, will be enabled for *all* the lists appearing in the + /// request. If defined and empty, will be disabled for all the lists. + #[serde(skip_serializing_if = "Option::is_none")] + pub lists: Option>, + + /// List of room names for which to-device events should be enabled. + /// + /// If not defined, will be enabled for *all* the rooms appearing in the + /// room subscriptions. If defined and empty, will be disabled for all + /// the rooms. + #[serde(skip_serializing_if = "Option::is_none")] + pub rooms: Option>, + } + + impl ToDevice { + /// Whether all fields are empty or `None`. + pub fn is_empty(&self) -> bool { + self.enabled.is_none() && self.limit.is_none() && self.since.is_none() + } + } + + /// E2EE extension configuration. + /// + /// According to [MSC3884](https://github.com/matrix-org/matrix-spec-proposals/pull/3884). + #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct E2EE { + /// Activate or deactivate this extension. + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled: Option, + } + + impl E2EE { + /// Whether all fields are empty or `None`. + pub fn is_empty(&self) -> bool { + self.enabled.is_none() + } + } + + /// Account-data extension . + /// + /// Not yet part of the spec proposal. Taken from the reference implementation + /// + #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct AccountData { + /// Activate or deactivate this extension. + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled: Option, + + /// List of list names for which account data should be enabled. + /// + /// This is specific to room account data (e.g. user-defined room tags). + /// + /// If not defined, will be enabled for *all* the lists appearing in the + /// request. If defined and empty, will be disabled for all the lists. + #[serde(skip_serializing_if = "Option::is_none")] + pub lists: Option>, + + /// List of room names for which account data should be enabled. + /// + /// This is specific to room account data (e.g. user-defined room tags). + /// + /// If not defined, will be enabled for *all* the rooms appearing in the + /// room subscriptions. If defined and empty, will be disabled for all + /// the rooms. + #[serde(skip_serializing_if = "Option::is_none")] + pub rooms: Option>, + } + + impl AccountData { + /// Whether all fields are empty or `None`. + pub fn is_empty(&self) -> bool { + self.enabled.is_none() + } + } + + /// Receipt extension. + /// + /// According to [MSC3960](https://github.com/matrix-org/matrix-spec-proposals/pull/3960) + #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct Receipts { + /// Activate or deactivate this extension. + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled: Option, + + /// List of list names for which receipts should be enabled. + /// + /// If not defined, will be enabled for *all* the lists appearing in the + /// request. If defined and empty, will be disabled for all the lists. + #[serde(skip_serializing_if = "Option::is_none")] + pub lists: Option>, + + /// List of room names for which receipts should be enabled. + /// + /// If not defined, will be enabled for *all* the rooms appearing in the + /// room subscriptions. If defined and empty, will be disabled for all + /// the rooms. + #[serde(skip_serializing_if = "Option::is_none")] + pub rooms: Option>, + } + + impl Receipts { + /// Whether all fields are empty or `None`. + pub fn is_empty(&self) -> bool { + self.enabled.is_none() + } + } + + /// Single entry for a room-related read receipt configuration in + /// [`Receipts`]. + #[derive(Clone, Debug, PartialEq)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub enum ReceiptsRoom { + /// Get read receipts for all the subscribed rooms. + AllSubscribed, + + /// Get read receipts for this particular room. + Room(OwnedRoomId), + } + + impl Serialize for ReceiptsRoom { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Self::AllSubscribed => serializer.serialize_str("*"), + Self::Room(r) => r.serialize(serializer), + } + } + } + + impl<'de> Deserialize<'de> for ReceiptsRoom { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + match deserialize_cow_str(deserializer)?.as_ref() { + "*" => Ok(Self::AllSubscribed), + other => Ok(Self::Room(RoomId::parse(other).map_err(D::Error::custom)?.to_owned())), + } + } + } + + /// Typing extension configuration. + /// + /// Not yet part of the spec proposal. Taken from the reference implementation + /// + #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct Typing { + /// Activate or deactivate this extension. + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled: Option, + + /// List of list names for which typing notifications should be enabled. + /// + /// If not defined, will be enabled for *all* the lists appearing in the + /// request. If defined and empty, will be disabled for all the lists. + #[serde(skip_serializing_if = "Option::is_none")] + pub lists: Option>, + + /// List of room names for which typing notifications should be enabled. + /// + /// If not defined, will be enabled for *all* the rooms appearing in the + /// room subscriptions. If defined and empty, will be disabled for all + /// the rooms. + #[serde(skip_serializing_if = "Option::is_none")] + pub rooms: Option>, + } + + impl Typing { + /// Whether all fields are empty or `None`. + pub fn is_empty(&self) -> bool { + self.enabled.is_none() + } + } +} + +/// Response type for the `/sync` endpoint. +#[response(error = crate::Error)] +pub struct Response { + /// Whether this response describes an initial sync. + #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")] + pub initial: bool, + + /// Matches the `txn_id` sent by the request (see [`Request::txn_id`]). + #[serde(skip_serializing_if = "Option::is_none")] + pub txn_id: Option, + + /// The token to supply in the `pos` parameter of the next `/sync` request + /// (see [`Request::pos`]). + pub pos: String, + + /// Resulting details of the lists. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub lists: BTreeMap, + + /// The updated rooms. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub rooms: BTreeMap, + + /// Extensions. + #[serde(default, skip_serializing_if = "response::Extensions::is_empty")] + pub extensions: response::Extensions, +} + +impl Response { + /// Creates a new `Response` with the given `pos`. + pub fn new(pos: String) -> Self { + Self { + initial: Default::default(), + txn_id: None, + pos, + lists: Default::default(), + rooms: Default::default(), + extensions: Default::default(), + } + } +} + +/// HTTP types related to a [`Response`]. +pub mod response { + use ruma_common::DeviceKeyAlgorithm; + use ruma_events::{ + receipt::SyncReceiptEvent, typing::SyncTypingEvent, AnyGlobalAccountDataEvent, + AnyRoomAccountDataEvent, AnyToDeviceEvent, + }; + + use super::{ + super::DeviceLists, AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, + BTreeMap, Deserialize, JsOption, OwnedMxcUri, OwnedRoomId, OwnedUserId, Raw, Serialize, + UInt, UnreadNotificationsCount, + }; + + /// A sliding sync response updates to joiend rooms (see + /// [`super::Response::lists`]). + #[derive(Clone, Debug, Default, Deserialize, Serialize)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct List { + /// The total number of rooms found for this list. + pub count: UInt, + } + + /// A slising sync response updated room (see [`super::Response::rooms`]). + #[derive(Clone, Debug, Default, Deserialize, Serialize)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct Room { + /// The name as calculated by the server. + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + + /// The avatar. + #[serde(default, skip_serializing_if = "JsOption::is_undefined")] + pub avatar: JsOption, + + /// Whether it is an initial response. + #[serde(skip_serializing_if = "Option::is_none")] + pub initial: Option, + + /// Whether it is a direct room. + #[serde(skip_serializing_if = "Option::is_none")] + pub is_dm: Option, + + /// If this is `Some(_)`, this is a not-yet-accepted invite containing + /// the given stripped state events. + #[serde(skip_serializing_if = "Option::is_none")] + pub invite_state: Option>>, + + /// Number of unread notifications. + #[serde(flatten, default, skip_serializing_if = "UnreadNotificationsCount::is_empty")] + pub unread_notifications: UnreadNotificationsCount, + + /// Message-like events and live state events. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub timeline: Vec>, + + /// State events as configured by the request. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub required_state: Vec>, + + /// The `prev_batch` allowing you to paginate through the messages + /// before the given ones. + #[serde(skip_serializing_if = "Option::is_none")] + pub prev_batch: Option, + + /// True if the number of events returned was limited by the limit on + /// the filter. + #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")] + pub limited: bool, + + /// The number of users with membership of `join`, including the + /// client’s own user ID. + #[serde(skip_serializing_if = "Option::is_none")] + pub joined_count: Option, + + /// The number of users with membership of `invite`. + #[serde(skip_serializing_if = "Option::is_none")] + pub invited_count: Option, + + /// The number of timeline events which have just occurred and are not + /// historical. + #[serde(skip_serializing_if = "Option::is_none")] + pub num_live: Option, + + /// The bump stamp of the room. + /// + /// It can be interpreted as a “recency stamp” or “streaming order + /// index”. For example, consider `roomA` with `bump_stamp = 2`, `roomB` + /// with `bump_stamp = 1` and `roomC` with `bump_stamp = 0`. If `roomC` + /// receives an update, its `bump_stamp` will be 3. + #[serde(skip_serializing_if = "Option::is_none")] + pub bump_stamp: Option, + + /// Heroes of the room, if requested. + #[serde(skip_serializing_if = "Option::is_none")] + pub heroes: Option>, + } + + impl Room { + /// Creates an empty `Room`. + pub fn new() -> Self { + Default::default() + } + } + + /// A sliding sync response room hero (see [`Room::heroes`]). + #[derive(Clone, Debug, Deserialize, Serialize)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct Hero { + /// The user ID. + pub user_id: OwnedUserId, + + /// The name. + #[serde(rename = "displayname", skip_serializing_if = "Option::is_none")] + pub name: Option, + + /// The avatar. + #[serde(rename = "avatar_url", skip_serializing_if = "Option::is_none")] + pub avatar: Option, + } + + impl Hero { + /// Creates a new `Hero` with the given user ID. + pub fn new(user_id: OwnedUserId) -> Self { + Self { user_id, name: None, avatar: None } + } + } + + /// Extensions responses. + #[derive(Clone, Debug, Default, Serialize, Deserialize)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct Extensions { + /// To-device extension response. + #[serde(skip_serializing_if = "Option::is_none")] + pub to_device: Option, + + /// E2EE extension response. + #[serde(default, skip_serializing_if = "E2EE::is_empty")] + pub e2ee: E2EE, + + /// Account data extension response. + #[serde(default, skip_serializing_if = "AccountData::is_empty")] + pub account_data: AccountData, + + /// Receipts extension response. + #[serde(default, skip_serializing_if = "Receipts::is_empty")] + pub receipts: Receipts, + + /// Typing extension response. + #[serde(default, skip_serializing_if = "Typing::is_empty")] + pub typing: Typing, + } + + impl Extensions { + /// Whether the extension data is empty. + /// + /// True if neither to-device, e2ee nor account data are to be found. + pub fn is_empty(&self) -> bool { + self.to_device.is_none() + && self.e2ee.is_empty() + && self.account_data.is_empty() + && self.receipts.is_empty() + && self.typing.is_empty() + } + } + + /// To-device extension response. + /// + /// According to [MSC3885](https://github.com/matrix-org/matrix-spec-proposals/pull/3885). + #[derive(Clone, Debug, Default, Serialize, Deserialize)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct ToDevice { + /// Fetch the next batch from this entry. + pub next_batch: String, + + /// The to-device events. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub events: Vec>, + } + + /// E2EE extension response. + /// + /// According to [MSC3884](https://github.com/matrix-org/matrix-spec-proposals/pull/3884). + #[derive(Clone, Debug, Default, Serialize, Deserialize)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct E2EE { + /// Information on E2EE device updates. + #[serde(default, skip_serializing_if = "DeviceLists::is_empty")] + pub device_lists: DeviceLists, + + /// For each key algorithm, the number of unclaimed one-time keys + /// currently held on the server for a device. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub device_one_time_keys_count: BTreeMap, + + /// For each key algorithm, the number of unclaimed one-time keys + /// currently held on the server for a device. + /// + /// The presence of this field indicates that the server supports + /// fallback keys. + #[serde(skip_serializing_if = "Option::is_none")] + pub device_unused_fallback_key_types: Option>, + } + + impl E2EE { + /// Whether all fields are empty or `None`. + pub fn is_empty(&self) -> bool { + self.device_lists.is_empty() + && self.device_one_time_keys_count.is_empty() + && self.device_unused_fallback_key_types.is_none() + } + } + + /// Account-data extension response . + /// + /// Not yet part of the spec proposal. Taken from the reference implementation + /// + #[derive(Clone, Debug, Default, Serialize, Deserialize)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct AccountData { + /// The global private data created by this user. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub global: Vec>, + + /// The private data that this user has attached to each room. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub rooms: BTreeMap>>, + } + + impl AccountData { + /// Whether all fields are empty or `None`. + pub fn is_empty(&self) -> bool { + self.global.is_empty() && self.rooms.is_empty() + } + } + + /// Receipt extension response. + /// + /// According to [MSC3960](https://github.com/matrix-org/matrix-spec-proposals/pull/3960) + #[derive(Clone, Debug, Default, Serialize, Deserialize)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct Receipts { + /// The ephemeral receipt room event for each room. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub rooms: BTreeMap>, + } + + impl Receipts { + /// Whether all fields are empty or `None`. + pub fn is_empty(&self) -> bool { + self.rooms.is_empty() + } + } + + /// Typing extension response. + /// + /// Not yet part of the spec proposal. Taken from the reference implementation + /// + #[derive(Clone, Debug, Default, Serialize, Deserialize)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct Typing { + /// The ephemeral typing event for each room. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub rooms: BTreeMap>, + } + + impl Typing { + /// Whether all fields are empty or `None`. + pub fn is_empty(&self) -> bool { + self.rooms.is_empty() + } + } +} + +impl From for Response { + fn from(value: v4::Response) -> Self { + Self { + pos: value.pos, + initial: value.initial, + txn_id: value.txn_id, + lists: value.lists.into_iter().map(|(room_id, list)| (room_id, list.into())).collect(), + rooms: value.rooms.into_iter().map(|(room_id, room)| (room_id, room.into())).collect(), + extensions: value.extensions.into(), + } + } +} + +impl From for response::List { + fn from(value: v4::SyncList) -> Self { + Self { count: value.count } + } +} + +impl From for response::Room { + fn from(value: v4::SlidingSyncRoom) -> Self { + Self { + name: value.name, + avatar: value.avatar, + initial: value.initial, + is_dm: value.is_dm, + invite_state: value.invite_state, + unread_notifications: value.unread_notifications, + timeline: value.timeline, + required_state: value.required_state, + prev_batch: value.prev_batch, + limited: value.limited, + joined_count: value.joined_count, + invited_count: value.invited_count, + num_live: value.num_live, + bump_stamp: value.timestamp.map(|t| t.0), + heroes: value.heroes.map(|heroes| heroes.into_iter().map(Into::into).collect()), + } + } +} + +impl From for response::Hero { + fn from(value: v4::SlidingSyncRoomHero) -> Self { + Self { user_id: value.user_id, name: value.name, avatar: value.avatar } + } +} + +impl From for response::Extensions { + fn from(value: v4::Extensions) -> Self { + Self { + to_device: value.to_device.map(Into::into), + e2ee: value.e2ee.into(), + account_data: value.account_data.into(), + receipts: value.receipts.into(), + typing: value.typing.into(), + } + } +} + +impl From for response::ToDevice { + fn from(value: v4::ToDevice) -> Self { + Self { next_batch: value.next_batch, events: value.events } + } +} + +impl From for response::E2EE { + fn from(value: v4::E2EE) -> Self { + Self { + device_lists: value.device_lists, + device_one_time_keys_count: value.device_one_time_keys_count, + device_unused_fallback_key_types: value.device_unused_fallback_key_types, + } + } +} + +impl From for response::AccountData { + fn from(value: v4::AccountData) -> Self { + Self { global: value.global, rooms: value.rooms } + } +} + +impl From for response::Receipts { + fn from(value: v4::Receipts) -> Self { + Self { rooms: value.rooms } + } +} + +impl From for response::Typing { + fn from(value: v4::Typing) -> Self { + Self { rooms: value.rooms } + } +} + +#[cfg(test)] +mod tests { + use ruma_common::owned_room_id; + + use super::request::ReceiptsRoom; + + #[test] + fn serialize_request_receipts_room() { + let entry = ReceiptsRoom::AllSubscribed; + assert_eq!(serde_json::to_string(&entry).unwrap().as_str(), r#""*""#); + + let entry = ReceiptsRoom::Room(owned_room_id!("!foo:bar.baz")); + assert_eq!(serde_json::to_string(&entry).unwrap().as_str(), r#""!foo:bar.baz""#); + } + + #[test] + fn deserialize_request_receipts_room() { + assert_eq!( + serde_json::from_str::(r#""*""#).unwrap(), + ReceiptsRoom::AllSubscribed + ); + + assert_eq!( + serde_json::from_str::(r#""!foo:bar.baz""#).unwrap(), + ReceiptsRoom::Room(owned_room_id!("!foo:bar.baz")) + ); + } +} diff --git a/crates/ruma/Cargo.toml b/crates/ruma/Cargo.toml index 63c6c275..33c9759b 100644 --- a/crates/ruma/Cargo.toml +++ b/crates/ruma/Cargo.toml @@ -228,6 +228,7 @@ unstable-msc4108 = ["ruma-client-api?/unstable-msc4108"] unstable-msc4121 = ["ruma-client-api?/unstable-msc4121"] unstable-msc4125 = ["ruma-federation-api?/unstable-msc4125"] unstable-msc4140 = ["ruma-client-api?/unstable-msc4140"] +unstable-msc4186 = ["ruma-client-api?/unstable-msc4186"] unstable-pdu = ["ruma-events?/unstable-pdu"] unstable-unspecified = [ "ruma-common/unstable-unspecified", @@ -279,6 +280,7 @@ __unstable-mscs = [ "unstable-msc4121", "unstable-msc4125", "unstable-msc4140", + "unstable-msc4186", ] __ci = [ "full",