From 23a608788b196ed3000397d45b9b3a9aead185b8 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Wed, 17 Aug 2022 16:16:02 +0200 Subject: [PATCH] client-api: Add sliding-sync endpoint Co-authored-by: Matthew Hodgson --- .gitignore | 1 + crates/ruma-client-api/CHANGELOG.md | 1 + crates/ruma-client-api/Cargo.toml | 1 + .../ruma-client-api/src/sync/sync_events.rs | 773 +----------------- .../src/sync/sync_events/v3.rs | 756 +++++++++++++++++ .../src/sync/sync_events/v4.rs | 335 ++++++++ crates/ruma/Cargo.toml | 2 + 7 files changed, 1119 insertions(+), 750 deletions(-) create mode 100644 crates/ruma-client-api/src/sync/sync_events/v3.rs create mode 100644 crates/ruma-client-api/src/sync/sync_events/v4.rs diff --git a/.gitignore b/.gitignore index 8094f6ea..0e7eed5e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .vscode target Cargo.lock +.DS_Store diff --git a/crates/ruma-client-api/CHANGELOG.md b/crates/ruma-client-api/CHANGELOG.md index ee8f07ac..56324b79 100644 --- a/crates/ruma-client-api/CHANGELOG.md +++ b/crates/ruma-client-api/CHANGELOG.md @@ -2,6 +2,7 @@ Breaking changes: +* `UnreadNotificationsCount` has moved from `sync::sync_events::v3` to `sync::sync_events` * Remove `PartialEq` implementations for a number of types * If the lack of such an `impl` causes problems, please open a GitHub issue * Split `uiaa::UserIdentifier::ThirdParty` into two separate variants diff --git a/crates/ruma-client-api/Cargo.toml b/crates/ruma-client-api/Cargo.toml index ebdd0578..059dc503 100644 --- a/crates/ruma-client-api/Cargo.toml +++ b/crates/ruma-client-api/Cargo.toml @@ -29,6 +29,7 @@ unstable-msc2965 = [] unstable-msc2967 = [] unstable-msc3440 = [] unstable-msc3488 = [] +unstable-msc3575 = [] client = [] server = [] diff --git a/crates/ruma-client-api/src/sync/sync_events.rs b/crates/ruma-client-api/src/sync/sync_events.rs index 37b99ba3..f8b803ce 100644 --- a/crates/ruma-client-api/src/sync/sync_events.rs +++ b/crates/ruma-client-api/src/sync/sync_events.rs @@ -1,761 +1,34 @@ //! `GET /_matrix/client/*/sync` -pub mod v3 { - //! `/v3/` ([spec]) - //! - //! [spec]: https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3sync +use js_int::UInt; +use serde::{self, Deserialize, Serialize}; - use std::{collections::BTreeMap, time::Duration}; +pub mod v3; - use js_int::UInt; - use ruma_common::{ - api::ruma_api, - events::{ - presence::PresenceEvent, AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, - AnyStrippedStateEvent, AnySyncEphemeralRoomEvent, AnySyncRoomEvent, AnySyncStateEvent, - AnyToDeviceEvent, - }, - presence::PresenceState, - serde::{Incoming, Raw}, - DeviceKeyAlgorithm, OwnedRoomId, OwnedUserId, - }; - use serde::{Deserialize, Serialize}; +#[cfg(feature = "unstable-msc3575")] +pub mod v4; - use crate::filter::{FilterDefinition, IncomingFilterDefinition}; +/// Unread notifications count. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct UnreadNotificationsCount { + /// The number of unread notifications for this room with the highlight flag set. + #[serde(skip_serializing_if = "Option::is_none")] + pub highlight_count: Option, - ruma_api! { - metadata: { - description: "Get all new events from all rooms since the last sync or a given point of time.", - method: GET, - name: "sync", - r0_path: "/_matrix/client/r0/sync", - stable_path: "/_matrix/client/v3/sync", - rate_limited: false, - authentication: AccessToken, - added: 1.0, - } + /// The total number of unread notifications for this room. + #[serde(skip_serializing_if = "Option::is_none")] + pub notification_count: Option, +} - #[derive(Default)] - request: { - /// A filter represented either as its full JSON definition or the ID of a saved filter. - #[serde(skip_serializing_if = "Option::is_none")] - #[ruma_api(query)] - pub filter: Option<&'a Filter<'a>>, - - /// A point in time to continue a sync from. - /// - /// Should be a token from the `next_batch` field of a previous `/sync` - /// request. - #[serde(skip_serializing_if = "Option::is_none")] - #[ruma_api(query)] - pub since: Option<&'a str>, - - /// Controls whether to include the full state for all rooms the user is a member of. - #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")] - #[ruma_api(query)] - pub full_state: bool, - - /// Controls whether the client is automatically marked as online by polling this API. - /// - /// Defaults to `PresenceState::Online`. - #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")] - #[ruma_api(query)] - pub set_presence: &'a PresenceState, - - /// The maximum time to poll in milliseconds before returning this request. - #[serde( - with = "ruma_common::serde::duration::opt_ms", - default, - skip_serializing_if = "Option::is_none", - )] - #[ruma_api(query)] - pub timeout: Option, - } - - response: { - /// The batch token to supply in the `since` param of the next `/sync` request. - pub next_batch: String, - - /// Updates to rooms. - #[serde(default, skip_serializing_if = "Rooms::is_empty")] - pub rooms: Rooms, - - /// Updates to the presence status of other users. - #[serde(default, skip_serializing_if = "Presence::is_empty")] - pub presence: Presence, - - /// The global private data created by this user. - #[serde(default, skip_serializing_if = "GlobalAccountData::is_empty")] - pub account_data: GlobalAccountData, - - /// Messages sent directly between devices. - #[serde(default, skip_serializing_if = "ToDevice::is_empty")] - pub to_device: ToDevice, - - /// Information on E2E device updates. - /// - /// Only present on an incremental sync. - #[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. - pub device_unused_fallback_key_types: Option>, - } - - error: crate::Error +impl UnreadNotificationsCount { + /// Creates an empty `UnreadNotificationsCount`. + pub fn new() -> Self { + Default::default() } - impl Request<'_> { - /// Creates an empty `Request`. - pub fn new() -> Self { - Default::default() - } - } - - impl Response { - /// Creates a new `Response` with the given batch token. - pub fn new(next_batch: String) -> Self { - Self { - next_batch, - rooms: Default::default(), - presence: Default::default(), - account_data: Default::default(), - to_device: Default::default(), - device_lists: Default::default(), - device_one_time_keys_count: BTreeMap::new(), - device_unused_fallback_key_types: None, - } - } - } - - /// A filter represented either as its full JSON definition or the ID of a saved filter. - #[derive(Clone, Debug, Incoming, Serialize)] - #[allow(clippy::large_enum_variant)] - #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] - #[serde(untagged)] - pub enum Filter<'a> { - // The filter definition needs to be (de)serialized twice because it is a URL-encoded JSON - // string. Since #[ruma_api(query)] only does the latter and this is a very uncommon - // setup, we implement it through custom serde logic for this specific enum variant rather - // than adding another ruma_api attribute. - // - // On the deserialization side, because this is an enum with #[serde(untagged)], serde - // will try the variants in order (https://serde.rs/enum-representations.html). That means because - // FilterDefinition is the first variant, JSON decoding is attempted first which is almost - // functionally equivalent to looking at whether the first symbol is a '{' as the spec - // says. (there are probably some corner cases like leading whitespace) - /// A complete filter definition serialized to JSON. - #[serde(with = "ruma_common::serde::json_string")] - FilterDefinition(FilterDefinition<'a>), - - /// The ID of a filter saved on the server. - FilterId(&'a str), - } - - impl<'a> From> for Filter<'a> { - fn from(def: FilterDefinition<'a>) -> Self { - Self::FilterDefinition(def) - } - } - - impl<'a> From<&'a str> for Filter<'a> { - fn from(id: &'a str) -> Self { - Self::FilterId(id) - } - } - - /// Updates to rooms. - #[derive(Clone, Debug, Default, Deserialize, Serialize)] - #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] - pub struct Rooms { - /// The rooms that the user has left or been banned from. - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub leave: BTreeMap, - - /// The rooms that the user has joined. - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub join: BTreeMap, - - /// The rooms that the user has been invited to. - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub invite: BTreeMap, - - /// The rooms that the user has knocked on. - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub knock: BTreeMap, - } - - impl Rooms { - /// Creates an empty `Rooms`. - pub fn new() -> Self { - Default::default() - } - - /// Returns true if there is no update in any room. - pub fn is_empty(&self) -> bool { - self.leave.is_empty() && self.join.is_empty() && self.invite.is_empty() - } - } - - /// Historical updates to left rooms. - #[derive(Clone, Debug, Default, Deserialize, Serialize)] - #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] - pub struct LeftRoom { - /// The timeline of messages and state changes in the room up to the point when the user - /// left. - #[serde(default, skip_serializing_if = "Timeline::is_empty")] - pub timeline: Timeline, - - /// The state updates for the room up to the start of the timeline. - #[serde(default, skip_serializing_if = "State::is_empty")] - pub state: State, - - /// The private data that this user has attached to this room. - #[serde(default, skip_serializing_if = "RoomAccountData::is_empty")] - pub account_data: RoomAccountData, - } - - impl LeftRoom { - /// Creates an empty `LeftRoom`. - pub fn new() -> Self { - Default::default() - } - - /// Returns true if there are updates in the room. - pub fn is_empty(&self) -> bool { - self.timeline.is_empty() && self.state.is_empty() && self.account_data.is_empty() - } - } - - /// Updates to joined rooms. - #[derive(Clone, Debug, Default, Deserialize, Serialize)] - #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] - pub struct JoinedRoom { - /// Information about the room which clients may need to correctly render it - /// to users. - #[serde(default, skip_serializing_if = "RoomSummary::is_empty")] - pub summary: RoomSummary, - - /// Counts of unread notifications for this room. - #[serde(default, skip_serializing_if = "UnreadNotificationsCount::is_empty")] - pub unread_notifications: UnreadNotificationsCount, - - /// The timeline of messages and state changes in the room. - #[serde(default, skip_serializing_if = "Timeline::is_empty")] - pub timeline: Timeline, - - /// Updates to the state, between the time indicated by the `since` parameter, and the - /// start of the `timeline` (or all state up to the start of the `timeline`, if - /// `since` is not given, or `full_state` is true). - #[serde(default, skip_serializing_if = "State::is_empty")] - pub state: State, - - /// The private data that this user has attached to this room. - #[serde(default, skip_serializing_if = "RoomAccountData::is_empty")] - pub account_data: RoomAccountData, - - /// The ephemeral events in the room that aren't recorded in the timeline or state of the - /// room. - #[serde(default, skip_serializing_if = "Ephemeral::is_empty")] - pub ephemeral: Ephemeral, - - /// The number of unread events since the latest read receipt. - /// - /// This uses the unstable prefix in [MSC2654]. - /// - /// [MSC2654]: https://github.com/matrix-org/matrix-spec-proposals/pull/2654 - #[cfg(feature = "unstable-msc2654")] - #[serde( - rename = "org.matrix.msc2654.unread_count", - alias = "unread_count", - skip_serializing_if = "Option::is_none" - )] - pub unread_count: Option, - } - - impl JoinedRoom { - /// Creates an empty `JoinedRoom`. - pub fn new() -> Self { - Default::default() - } - - /// Returns true if there are no updates in the room. - pub fn is_empty(&self) -> bool { - let is_empty = self.summary.is_empty() - && self.unread_notifications.is_empty() - && self.timeline.is_empty() - && self.state.is_empty() - && self.account_data.is_empty() - && self.ephemeral.is_empty(); - - #[cfg(not(feature = "unstable-msc2654"))] - return is_empty; - - #[cfg(feature = "unstable-msc2654")] - return is_empty && self.unread_count.is_none(); - } - } - - /// Updates to knocked rooms. - #[derive(Clone, Debug, Default, Deserialize, Serialize)] - #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] - pub struct KnockedRoom { - /// The knock state. - pub knock_state: KnockState, - } - - /// A mapping from a key `events` to a list of `StrippedStateEvent`. - #[derive(Clone, Debug, Default, Deserialize, Serialize)] - #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] - pub struct KnockState { - /// The list of events. - pub events: Vec>, - } - - /// Unread notifications count. - #[derive(Clone, Debug, Default, Deserialize, Serialize)] - #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] - pub struct UnreadNotificationsCount { - /// The number of unread notifications for this room with the highlight flag set. - #[serde(skip_serializing_if = "Option::is_none")] - pub highlight_count: Option, - - /// The total number of unread notifications for this room. - #[serde(skip_serializing_if = "Option::is_none")] - pub notification_count: Option, - } - - impl UnreadNotificationsCount { - /// Creates an empty `UnreadNotificationsCount`. - pub fn new() -> Self { - Default::default() - } - - /// Returns true if there are no notification count updates. - pub fn is_empty(&self) -> bool { - self.highlight_count.is_none() && self.notification_count.is_none() - } - } - - /// Events in the room. - #[derive(Clone, Debug, Default, Deserialize, Serialize)] - #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] - pub struct Timeline { - /// True if the number of events returned was limited by the `limit` on the filter. - /// - /// Default to `false`. - #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")] - pub limited: bool, - - /// A token that can be supplied to to the `from` parameter of the - /// `/rooms/{roomId}/messages` endpoint. - #[serde(skip_serializing_if = "Option::is_none")] - pub prev_batch: Option, - - /// A list of events. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub events: Vec>, - } - - impl Timeline { - /// Creates an empty `Timeline`. - pub fn new() -> Self { - Default::default() - } - - /// Returns true if there are no timeline updates. - pub fn is_empty(&self) -> bool { - self.events.is_empty() - } - } - - /// State events in the room. - #[derive(Clone, Debug, Default, Deserialize, Serialize)] - #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] - pub struct State { - /// A list of state events. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub events: Vec>, - } - - impl State { - /// Creates an empty `State`. - pub fn new() -> Self { - Default::default() - } - - /// Returns true if there are no state updates. - pub fn is_empty(&self) -> bool { - self.events.is_empty() - } - } - - /// The global private data created by this user. - #[derive(Clone, Debug, Default, Deserialize, Serialize)] - #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] - pub struct GlobalAccountData { - /// A list of events. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub events: Vec>, - } - - impl GlobalAccountData { - /// Creates an empty `GlobalAccountData`. - pub fn new() -> Self { - Default::default() - } - - /// Returns true if there are no global account data updates. - pub fn is_empty(&self) -> bool { - self.events.is_empty() - } - } - - /// The private data that this user has attached to this room. - #[derive(Clone, Debug, Default, Deserialize, Serialize)] - #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] - pub struct RoomAccountData { - /// A list of events. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub events: Vec>, - } - - impl RoomAccountData { - /// Creates an empty `RoomAccountData`. - pub fn new() -> Self { - Default::default() - } - - /// Returns true if there are no room account data updates. - pub fn is_empty(&self) -> bool { - self.events.is_empty() - } - } - - /// Ephemeral events not recorded in the timeline or state of the room. - #[derive(Clone, Debug, Default, Deserialize, Serialize)] - #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] - pub struct Ephemeral { - /// A list of events. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub events: Vec>, - } - - impl Ephemeral { - /// Creates an empty `Ephemeral`. - pub fn new() -> Self { - Default::default() - } - - /// Returns true if there are no ephemeral event updates. - pub fn is_empty(&self) -> bool { - self.events.is_empty() - } - } - - /// Information about room for rendering to clients. - #[derive(Clone, Debug, Default, Deserialize, Serialize)] - #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] - pub struct RoomSummary { - /// Users which can be used to generate a room name if the room does not have one. - /// - /// Required if room name or canonical aliases are not set or empty. - #[serde(rename = "m.heroes", default, skip_serializing_if = "Vec::is_empty")] - pub heroes: Vec, - - /// Number of users whose membership status is `join`. - /// Required if field has changed since last sync; otherwise, it may be - /// omitted. - #[serde(rename = "m.joined_member_count", skip_serializing_if = "Option::is_none")] - pub joined_member_count: Option, - - /// Number of users whose membership status is `invite`. - /// Required if field has changed since last sync; otherwise, it may be - /// omitted. - #[serde(rename = "m.invited_member_count", skip_serializing_if = "Option::is_none")] - pub invited_member_count: Option, - } - - impl RoomSummary { - /// Creates an empty `RoomSummary`. - pub fn new() -> Self { - Default::default() - } - - /// Returns true if there are no room summary updates. - pub fn is_empty(&self) -> bool { - self.heroes.is_empty() - && self.joined_member_count.is_none() - && self.invited_member_count.is_none() - } - } - - /// Updates to the rooms that the user has been invited to. - #[derive(Clone, Debug, Default, Deserialize, Serialize)] - #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] - pub struct InvitedRoom { - /// The state of a room that the user has been invited to. - #[serde(default, skip_serializing_if = "InviteState::is_empty")] - pub invite_state: InviteState, - } - - impl InvitedRoom { - /// Creates an empty `InvitedRoom`. - pub fn new() -> Self { - Default::default() - } - - /// Returns true if there are no updates to this room. - pub fn is_empty(&self) -> bool { - self.invite_state.is_empty() - } - } - - /// The state of a room that the user has been invited to. - #[derive(Clone, Debug, Default, Deserialize, Serialize)] - #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] - pub struct InviteState { - /// A list of state events. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub events: Vec>, - } - - impl InviteState { - /// Creates an empty `InviteState`. - pub fn new() -> Self { - Default::default() - } - - /// Returns true if there are no state updates. - pub fn is_empty(&self) -> bool { - self.events.is_empty() - } - } - - /// Updates to the presence status of other users. - #[derive(Clone, Debug, Default, Deserialize, Serialize)] - #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] - pub struct Presence { - /// A list of events. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub events: Vec>, - } - - impl Presence { - /// Creates an empty `Presence`. - pub fn new() -> Self { - Default::default() - } - - /// Returns true if there are no presence updates. - pub fn is_empty(&self) -> bool { - self.events.is_empty() - } - } - - /// Messages sent directly between devices. - #[derive(Clone, Debug, Default, Deserialize, Serialize)] - #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] - pub struct ToDevice { - /// A list of to-device events. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub events: Vec>, - } - - impl ToDevice { - /// Creates an empty `ToDevice`. - pub fn new() -> Self { - Default::default() - } - - /// Returns true if there are no to-device events. - pub fn is_empty(&self) -> bool { - self.events.is_empty() - } - } - - /// Information on E2E device updates. - #[derive(Clone, Debug, Default, Deserialize, Serialize)] - #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] - pub struct DeviceLists { - /// List of users who have updated their device identity keys or who now - /// share an encrypted room with the client since the previous sync - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub changed: Vec, - - /// List of users who no longer share encrypted rooms since the previous sync - /// response. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub left: Vec, - } - - impl DeviceLists { - /// Creates an empty `DeviceLists`. - pub fn new() -> Self { - Default::default() - } - - /// Returns true if there are no device list updates. - pub fn is_empty(&self) -> bool { - self.changed.is_empty() && self.left.is_empty() - } - } - - #[cfg(test)] - mod tests { - use assign::assign; - use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; - - use super::Timeline; - - #[test] - fn timeline_serde() { - let timeline = assign!(Timeline::new(), { limited: true }); - let timeline_serialized = json!({ "limited": true }); - assert_eq!(to_json_value(timeline).unwrap(), timeline_serialized); - - let timeline_deserialized = from_json_value::(timeline_serialized).unwrap(); - assert!(timeline_deserialized.limited); - - let timeline_default = Timeline::default(); - assert_eq!(to_json_value(timeline_default).unwrap(), json!({})); - - let timeline_default_deserialized = from_json_value::(json!({})).unwrap(); - assert!(!timeline_default_deserialized.limited); - } - } - - #[cfg(all(test, feature = "client"))] - mod client_tests { - use std::time::Duration; - - use ruma_common::api::{MatrixVersion, OutgoingRequest as _, SendAccessToken}; - - use super::{Filter, PresenceState, Request}; - - #[test] - fn serialize_all_params() { - let req: http::Request> = Request { - filter: Some(&Filter::FilterId("66696p746572")), - since: Some("s72594_4483_1934"), - full_state: true, - set_presence: &PresenceState::Offline, - timeout: Some(Duration::from_millis(30000)), - } - .try_into_http_request( - "https://homeserver.tld", - SendAccessToken::IfRequired("auth_tok"), - &[MatrixVersion::V1_1], - ) - .unwrap(); - - let uri = req.uri(); - let query = uri.query().unwrap(); - - assert_eq!(uri.path(), "/_matrix/client/v3/sync"); - assert!(query.contains("filter=66696p746572")); - assert!(query.contains("since=s72594_4483_1934")); - assert!(query.contains("full_state=true")); - assert!(query.contains("set_presence=offline")); - assert!(query.contains("timeout=30000")); - } - } - - #[cfg(all(test, feature = "server"))] - mod server_tests { - use std::time::Duration; - - use assert_matches::assert_matches; - use ruma_common::{api::IncomingRequest as _, presence::PresenceState}; - - use super::{IncomingFilter, IncomingRequest}; - - #[test] - fn deserialize_all_query_params() { - let uri = http::Uri::builder() - .scheme("https") - .authority("matrix.org") - .path_and_query( - "/_matrix/client/r0/sync\ - ?filter=myfilter\ - &since=myts\ - &full_state=false\ - &set_presence=offline\ - &timeout=5000", - ) - .build() - .unwrap(); - - let req = IncomingRequest::try_from_http_request( - http::Request::builder().uri(uri).body(&[] as &[u8]).unwrap(), - &[] as &[String], - ) - .unwrap(); - - let id = assert_matches!(req.filter, Some(IncomingFilter::FilterId(id)) => id); - assert_eq!(id, "myfilter"); - assert_eq!(req.since.as_deref(), Some("myts")); - assert!(!req.full_state); - assert_eq!(req.set_presence, PresenceState::Offline); - assert_eq!(req.timeout, Some(Duration::from_millis(5000))); - } - - #[test] - fn deserialize_no_query_params() { - let uri = http::Uri::builder() - .scheme("https") - .authority("matrix.org") - .path_and_query("/_matrix/client/r0/sync") - .build() - .unwrap(); - - let req = IncomingRequest::try_from_http_request( - http::Request::builder().uri(uri).body(&[] as &[u8]).unwrap(), - &[] as &[String], - ) - .unwrap(); - - assert_matches!(req.filter, None); - assert_eq!(req.since, None); - assert!(!req.full_state); - assert_eq!(req.set_presence, PresenceState::Online); - assert_eq!(req.timeout, None); - } - - #[test] - fn deserialize_some_query_params() { - let uri = http::Uri::builder() - .scheme("https") - .authority("matrix.org") - .path_and_query( - "/_matrix/client/r0/sync\ - ?filter=EOKFFmdZYF\ - &timeout=0", - ) - .build() - .unwrap(); - - let req = IncomingRequest::try_from_http_request( - http::Request::builder().uri(uri).body(&[] as &[u8]).unwrap(), - &[] as &[String], - ) - .unwrap(); - - let id = assert_matches!(req.filter, Some(IncomingFilter::FilterId(id)) => id); - assert_eq!(id, "EOKFFmdZYF"); - assert_eq!(req.since, None); - assert!(!req.full_state); - assert_eq!(req.set_presence, PresenceState::Online); - assert_eq!(req.timeout, Some(Duration::from_millis(0))); - } + /// Returns true if there are no notification count updates. + pub fn is_empty(&self) -> bool { + self.highlight_count.is_none() && self.notification_count.is_none() } } diff --git a/crates/ruma-client-api/src/sync/sync_events/v3.rs b/crates/ruma-client-api/src/sync/sync_events/v3.rs new file mode 100644 index 00000000..77ccf4ec --- /dev/null +++ b/crates/ruma-client-api/src/sync/sync_events/v3.rs @@ -0,0 +1,756 @@ +//! `/v3/` ([spec]) +//! +//! [spec]: https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3sync + +use std::{collections::BTreeMap, time::Duration}; + +use super::UnreadNotificationsCount; +use js_int::UInt; +use ruma_common::{ + api::ruma_api, + events::{ + presence::PresenceEvent, AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, + AnyStrippedStateEvent, AnySyncEphemeralRoomEvent, AnySyncRoomEvent, AnySyncStateEvent, + AnyToDeviceEvent, + }, + presence::PresenceState, + serde::{Incoming, Raw}, + DeviceKeyAlgorithm, OwnedRoomId, OwnedUserId, +}; +use serde::{Deserialize, Serialize}; + +use crate::filter::{FilterDefinition, IncomingFilterDefinition}; + +ruma_api! { + metadata: { + description: "Get all new events from all rooms since the last sync or a given point of time.", + method: GET, + name: "sync", + r0_path: "/_matrix/client/r0/sync", + stable_path: "/_matrix/client/v3/sync", + rate_limited: false, + authentication: AccessToken, + added: 1.0, + } + + #[derive(Default)] + request: { + /// A filter represented either as its full JSON definition or the ID of a saved filter. + #[serde(skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub filter: Option<&'a Filter<'a>>, + + /// A point in time to continue a sync from. + /// + /// Should be a token from the `next_batch` field of a previous `/sync` + /// request. + #[serde(skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub since: Option<&'a str>, + + /// Controls whether to include the full state for all rooms the user is a member of. + #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")] + #[ruma_api(query)] + pub full_state: bool, + + /// Controls whether the client is automatically marked as online by polling this API. + /// + /// Defaults to `PresenceState::Online`. + #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")] + #[ruma_api(query)] + pub set_presence: &'a PresenceState, + + /// The maximum time to poll in milliseconds before returning this request. + #[serde( + with = "ruma_common::serde::duration::opt_ms", + default, + skip_serializing_if = "Option::is_none", + )] + #[ruma_api(query)] + pub timeout: Option, + } + + response: { + /// The batch token to supply in the `since` param of the next `/sync` request. + pub next_batch: String, + + /// Updates to rooms. + #[serde(default, skip_serializing_if = "Rooms::is_empty")] + pub rooms: Rooms, + + /// Updates to the presence status of other users. + #[serde(default, skip_serializing_if = "Presence::is_empty")] + pub presence: Presence, + + /// The global private data created by this user. + #[serde(default, skip_serializing_if = "GlobalAccountData::is_empty")] + pub account_data: GlobalAccountData, + + /// Messages sent directly between devices. + #[serde(default, skip_serializing_if = "ToDevice::is_empty")] + pub to_device: ToDevice, + + /// Information on E2E device updates. + /// + /// Only present on an incremental sync. + #[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. + pub device_unused_fallback_key_types: Option>, + } + + error: crate::Error +} + +impl Request<'_> { + /// Creates an empty `Request`. + pub fn new() -> Self { + Default::default() + } +} + +impl Response { + /// Creates a new `Response` with the given batch token. + pub fn new(next_batch: String) -> Self { + Self { + next_batch, + rooms: Default::default(), + presence: Default::default(), + account_data: Default::default(), + to_device: Default::default(), + device_lists: Default::default(), + device_one_time_keys_count: BTreeMap::new(), + device_unused_fallback_key_types: None, + } + } +} + +/// A filter represented either as its full JSON definition or the ID of a saved filter. +#[derive(Clone, Debug, Incoming, Serialize)] +#[allow(clippy::large_enum_variant)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +#[serde(untagged)] +pub enum Filter<'a> { + // The filter definition needs to be (de)serialized twice because it is a URL-encoded JSON + // string. Since #[ruma_api(query)] only does the latter and this is a very uncommon + // setup, we implement it through custom serde logic for this specific enum variant rather + // than adding another ruma_api attribute. + // + // On the deserialization side, because this is an enum with #[serde(untagged)], serde + // will try the variants in order (https://serde.rs/enum-representations.html). That means because + // FilterDefinition is the first variant, JSON decoding is attempted first which is almost + // functionally equivalent to looking at whether the first symbol is a '{' as the spec + // says. (there are probably some corner cases like leading whitespace) + /// A complete filter definition serialized to JSON. + #[serde(with = "ruma_common::serde::json_string")] + FilterDefinition(FilterDefinition<'a>), + + /// The ID of a filter saved on the server. + FilterId(&'a str), +} + +impl<'a> From> for Filter<'a> { + fn from(def: FilterDefinition<'a>) -> Self { + Self::FilterDefinition(def) + } +} + +impl<'a> From<&'a str> for Filter<'a> { + fn from(id: &'a str) -> Self { + Self::FilterId(id) + } +} + +/// Updates to rooms. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct Rooms { + /// The rooms that the user has left or been banned from. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub leave: BTreeMap, + + /// The rooms that the user has joined. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub join: BTreeMap, + + /// The rooms that the user has been invited to. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub invite: BTreeMap, + + /// The rooms that the user has knocked on. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub knock: BTreeMap, +} + +impl Rooms { + /// Creates an empty `Rooms`. + pub fn new() -> Self { + Default::default() + } + + /// Returns true if there is no update in any room. + pub fn is_empty(&self) -> bool { + self.leave.is_empty() && self.join.is_empty() && self.invite.is_empty() + } +} + +/// Historical updates to left rooms. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct LeftRoom { + /// The timeline of messages and state changes in the room up to the point when the user + /// left. + #[serde(default, skip_serializing_if = "Timeline::is_empty")] + pub timeline: Timeline, + + /// The state updates for the room up to the start of the timeline. + #[serde(default, skip_serializing_if = "State::is_empty")] + pub state: State, + + /// The private data that this user has attached to this room. + #[serde(default, skip_serializing_if = "RoomAccountData::is_empty")] + pub account_data: RoomAccountData, +} + +impl LeftRoom { + /// Creates an empty `LeftRoom`. + pub fn new() -> Self { + Default::default() + } + + /// Returns true if there are updates in the room. + pub fn is_empty(&self) -> bool { + self.timeline.is_empty() && self.state.is_empty() && self.account_data.is_empty() + } +} + +/// Updates to joined rooms. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct JoinedRoom { + /// Information about the room which clients may need to correctly render it + /// to users. + #[serde(default, skip_serializing_if = "RoomSummary::is_empty")] + pub summary: RoomSummary, + + /// Counts of unread notifications for this room. + #[serde(default, skip_serializing_if = "UnreadNotificationsCount::is_empty")] + pub unread_notifications: UnreadNotificationsCount, + + /// The timeline of messages and state changes in the room. + #[serde(default, skip_serializing_if = "Timeline::is_empty")] + pub timeline: Timeline, + + /// Updates to the state, between the time indicated by the `since` parameter, and the + /// start of the `timeline` (or all state up to the start of the `timeline`, if + /// `since` is not given, or `full_state` is true). + #[serde(default, skip_serializing_if = "State::is_empty")] + pub state: State, + + /// The private data that this user has attached to this room. + #[serde(default, skip_serializing_if = "RoomAccountData::is_empty")] + pub account_data: RoomAccountData, + + /// The ephemeral events in the room that aren't recorded in the timeline or state of the + /// room. + #[serde(default, skip_serializing_if = "Ephemeral::is_empty")] + pub ephemeral: Ephemeral, + + /// The number of unread events since the latest read receipt. + /// + /// This uses the unstable prefix in [MSC2654]. + /// + /// [MSC2654]: https://github.com/matrix-org/matrix-spec-proposals/pull/2654 + #[cfg(feature = "unstable-msc2654")] + #[serde( + rename = "org.matrix.msc2654.unread_count", + alias = "unread_count", + skip_serializing_if = "Option::is_none" + )] + pub unread_count: Option, +} + +impl JoinedRoom { + /// Creates an empty `JoinedRoom`. + pub fn new() -> Self { + Default::default() + } + + /// Returns true if there are no updates in the room. + pub fn is_empty(&self) -> bool { + let is_empty = self.summary.is_empty() + && self.unread_notifications.is_empty() + && self.timeline.is_empty() + && self.state.is_empty() + && self.account_data.is_empty() + && self.ephemeral.is_empty(); + + #[cfg(not(feature = "unstable-msc2654"))] + return is_empty; + + #[cfg(feature = "unstable-msc2654")] + return is_empty && self.unread_count.is_none(); + } +} + +/// Updates to knocked rooms. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct KnockedRoom { + /// The knock state. + pub knock_state: KnockState, +} + +/// A mapping from a key `events` to a list of `StrippedStateEvent`. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct KnockState { + /// The list of events. + pub events: Vec>, +} + +/// Events in the room. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct Timeline { + /// True if the number of events returned was limited by the `limit` on the filter. + /// + /// Default to `false`. + #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")] + pub limited: bool, + + /// A token that can be supplied to to the `from` parameter of the + /// `/rooms/{roomId}/messages` endpoint. + #[serde(skip_serializing_if = "Option::is_none")] + pub prev_batch: Option, + + /// A list of events. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub events: Vec>, +} + +impl Timeline { + /// Creates an empty `Timeline`. + pub fn new() -> Self { + Default::default() + } + + /// Returns true if there are no timeline updates. + pub fn is_empty(&self) -> bool { + self.events.is_empty() + } +} + +/// State events in the room. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct State { + /// A list of state events. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub events: Vec>, +} + +impl State { + /// Creates an empty `State`. + pub fn new() -> Self { + Default::default() + } + + /// Returns true if there are no state updates. + pub fn is_empty(&self) -> bool { + self.events.is_empty() + } + + /// Creates a `State` with events + pub fn with_events(events: Vec>) -> Self { + State { events, ..Default::default() } + } +} + +impl From>> for State { + fn from(events: Vec>) -> Self { + State::with_events(events) + } +} + +/// The global private data created by this user. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct GlobalAccountData { + /// A list of events. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub events: Vec>, +} + +impl GlobalAccountData { + /// Creates an empty `GlobalAccountData`. + pub fn new() -> Self { + Default::default() + } + + /// Returns true if there are no global account data updates. + pub fn is_empty(&self) -> bool { + self.events.is_empty() + } +} + +/// The private data that this user has attached to this room. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct RoomAccountData { + /// A list of events. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub events: Vec>, +} + +impl RoomAccountData { + /// Creates an empty `RoomAccountData`. + pub fn new() -> Self { + Default::default() + } + + /// Returns true if there are no room account data updates. + pub fn is_empty(&self) -> bool { + self.events.is_empty() + } +} + +/// Ephemeral events not recorded in the timeline or state of the room. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct Ephemeral { + /// A list of events. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub events: Vec>, +} + +impl Ephemeral { + /// Creates an empty `Ephemeral`. + pub fn new() -> Self { + Default::default() + } + + /// Returns true if there are no ephemeral event updates. + pub fn is_empty(&self) -> bool { + self.events.is_empty() + } +} + +/// Information about room for rendering to clients. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct RoomSummary { + /// Users which can be used to generate a room name if the room does not have one. + /// + /// Required if room name or canonical aliases are not set or empty. + #[serde(rename = "m.heroes", default, skip_serializing_if = "Vec::is_empty")] + pub heroes: Vec, + + /// Number of users whose membership status is `join`. + /// Required if field has changed since last sync; otherwise, it may be + /// omitted. + #[serde(rename = "m.joined_member_count", skip_serializing_if = "Option::is_none")] + pub joined_member_count: Option, + + /// Number of users whose membership status is `invite`. + /// Required if field has changed since last sync; otherwise, it may be + /// omitted. + #[serde(rename = "m.invited_member_count", skip_serializing_if = "Option::is_none")] + pub invited_member_count: Option, +} + +impl RoomSummary { + /// Creates an empty `RoomSummary`. + pub fn new() -> Self { + Default::default() + } + + /// Returns true if there are no room summary updates. + pub fn is_empty(&self) -> bool { + self.heroes.is_empty() + && self.joined_member_count.is_none() + && self.invited_member_count.is_none() + } +} + +/// Updates to the rooms that the user has been invited to. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct InvitedRoom { + /// The state of a room that the user has been invited to. + #[serde(default, skip_serializing_if = "InviteState::is_empty")] + pub invite_state: InviteState, +} + +impl InvitedRoom { + /// Creates an empty `InvitedRoom`. + pub fn new() -> Self { + Default::default() + } + + /// Returns true if there are no updates to this room. + pub fn is_empty(&self) -> bool { + self.invite_state.is_empty() + } +} + +impl From for InvitedRoom { + fn from(invite_state: InviteState) -> Self { + InvitedRoom { invite_state, ..Default::default() } + } +} + +/// The state of a room that the user has been invited to. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct InviteState { + /// A list of state events. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub events: Vec>, +} + +impl InviteState { + /// Creates an empty `InviteState`. + pub fn new() -> Self { + Default::default() + } + + /// Returns true if there are no state updates. + pub fn is_empty(&self) -> bool { + self.events.is_empty() + } +} + +impl From>> for InviteState { + fn from(events: Vec>) -> Self { + InviteState { events, ..Default::default() } + } +} + +/// Updates to the presence status of other users. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct Presence { + /// A list of events. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub events: Vec>, +} + +impl Presence { + /// Creates an empty `Presence`. + pub fn new() -> Self { + Default::default() + } + + /// Returns true if there are no presence updates. + pub fn is_empty(&self) -> bool { + self.events.is_empty() + } +} + +/// Messages sent directly between devices. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct ToDevice { + /// A list of to-device events. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub events: Vec>, +} + +impl ToDevice { + /// Creates an empty `ToDevice`. + pub fn new() -> Self { + Default::default() + } + + /// Returns true if there are no to-device events. + pub fn is_empty(&self) -> bool { + self.events.is_empty() + } +} + +/// Information on E2E device updates. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct DeviceLists { + /// List of users who have updated their device identity keys or who now + /// share an encrypted room with the client since the previous sync + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub changed: Vec, + + /// List of users who no longer share encrypted rooms since the previous sync + /// response. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub left: Vec, +} + +impl DeviceLists { + /// Creates an empty `DeviceLists`. + pub fn new() -> Self { + Default::default() + } + + /// Returns true if there are no device list updates. + pub fn is_empty(&self) -> bool { + self.changed.is_empty() && self.left.is_empty() + } +} + +#[cfg(test)] +mod tests { + use assign::assign; + use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; + + use super::Timeline; + + #[test] + fn timeline_serde() { + let timeline = assign!(Timeline::new(), { limited: true }); + let timeline_serialized = json!({ "limited": true }); + assert_eq!(to_json_value(timeline).unwrap(), timeline_serialized); + + let timeline_deserialized = from_json_value::(timeline_serialized).unwrap(); + assert!(timeline_deserialized.limited); + + let timeline_default = Timeline::default(); + assert_eq!(to_json_value(timeline_default).unwrap(), json!({})); + + let timeline_default_deserialized = from_json_value::(json!({})).unwrap(); + assert!(!timeline_default_deserialized.limited); + } +} + +#[cfg(all(test, feature = "client"))] +mod client_tests { + use std::time::Duration; + + use ruma_common::api::{MatrixVersion, OutgoingRequest as _, SendAccessToken}; + + use super::{Filter, PresenceState, Request}; + + #[test] + fn serialize_all_params() { + let req: http::Request> = Request { + filter: Some(&Filter::FilterId("66696p746572")), + since: Some("s72594_4483_1934"), + full_state: true, + set_presence: &PresenceState::Offline, + timeout: Some(Duration::from_millis(30000)), + } + .try_into_http_request( + "https://homeserver.tld", + SendAccessToken::IfRequired("auth_tok"), + &[MatrixVersion::V1_1], + ) + .unwrap(); + + let uri = req.uri(); + let query = uri.query().unwrap(); + + assert_eq!(uri.path(), "/_matrix/client/v3/sync"); + assert!(query.contains("filter=66696p746572")); + assert!(query.contains("since=s72594_4483_1934")); + assert!(query.contains("full_state=true")); + assert!(query.contains("set_presence=offline")); + assert!(query.contains("timeout=30000")); + } +} + +#[cfg(all(test, feature = "server"))] +mod server_tests { + use std::time::Duration; + + use assert_matches::assert_matches; + use ruma_common::{api::IncomingRequest as _, presence::PresenceState}; + + use super::{IncomingFilter, IncomingRequest}; + + #[test] + fn deserialize_all_query_params() { + let uri = http::Uri::builder() + .scheme("https") + .authority("matrix.org") + .path_and_query( + "/_matrix/client/r0/sync\ + ?filter=myfilter\ + &since=myts\ + &full_state=false\ + &set_presence=offline\ + &timeout=5000", + ) + .build() + .unwrap(); + + let req = IncomingRequest::try_from_http_request( + http::Request::builder().uri(uri).body(&[] as &[u8]).unwrap(), + &[] as &[String], + ) + .unwrap(); + + let id = assert_matches!(req.filter, Some(IncomingFilter::FilterId(id)) => id); + assert_eq!(id, "myfilter"); + assert_eq!(req.since.as_deref(), Some("myts")); + assert!(!req.full_state); + assert_eq!(req.set_presence, PresenceState::Offline); + assert_eq!(req.timeout, Some(Duration::from_millis(5000))); + } + + #[test] + fn deserialize_no_query_params() { + let uri = http::Uri::builder() + .scheme("https") + .authority("matrix.org") + .path_and_query("/_matrix/client/r0/sync") + .build() + .unwrap(); + + let req = IncomingRequest::try_from_http_request( + http::Request::builder().uri(uri).body(&[] as &[u8]).unwrap(), + &[] as &[String], + ) + .unwrap(); + + assert_matches!(req.filter, None); + assert_eq!(req.since, None); + assert!(!req.full_state); + assert_eq!(req.set_presence, PresenceState::Online); + assert_eq!(req.timeout, None); + } + + #[test] + fn deserialize_some_query_params() { + let uri = http::Uri::builder() + .scheme("https") + .authority("matrix.org") + .path_and_query( + "/_matrix/client/r0/sync\ + ?filter=EOKFFmdZYF\ + &timeout=0", + ) + .build() + .unwrap(); + + let req = IncomingRequest::try_from_http_request( + http::Request::builder().uri(uri).body(&[] as &[u8]).unwrap(), + &[] as &[String], + ) + .unwrap(); + + let id = assert_matches!(req.filter, Some(IncomingFilter::FilterId(id)) => id); + assert_eq!(id, "EOKFFmdZYF"); + assert_eq!(req.since, None); + assert!(!req.full_state); + assert_eq!(req.set_presence, PresenceState::Online); + assert_eq!(req.timeout, Some(Duration::from_millis(0))); + } +} diff --git a/crates/ruma-client-api/src/sync/sync_events/v4.rs b/crates/ruma-client-api/src/sync/sync_events/v4.rs new file mode 100644 index 00000000..fc0f6648 --- /dev/null +++ b/crates/ruma-client-api/src/sync/sync_events/v4.rs @@ -0,0 +1,335 @@ +//! [POST /_matrix/client/unstable/org.matrix.msc3575/sync](https://github.com/matrix-org/matrix-doc/blob/kegan/sync-v3/proposals/3575-sync.md) + +use std::{collections::BTreeMap, time::Duration}; + +use super::UnreadNotificationsCount; +use js_int::UInt; +use ruma_common::{ + api::ruma_api, + events::{AnyStrippedStateEvent, AnySyncRoomEvent, AnySyncStateEvent, RoomEventType}, + serde::{duration::opt_ms, Raw}, + OwnedRoomId, +}; +use serde::{Deserialize, Serialize}; + +ruma_api! { + metadata: { + description: "Get all new events in a sliding window of rooms since the last sync or a given point of time.", + method: POST, + name: "sync", + // added: 1.4, + // stable_path: "/_matrix/client/v4/sync", + unstable_path: "/_matrix/client/unstable/org.matrix.msc3575/sync", + rate_limited: false, + authentication: AccessToken, + } + + #[derive(Default)] + request: { + /// A point in time to continue a sync from. + /// + /// Should be a token from the `pos` field of a previous `/sync` + /// response. + #[serde(skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub pos: Option<&'a str>, + + /// 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<&'a str>, + + /// The maximum time to poll before responding to this request. + #[serde( + with = "opt_ms", + default, + skip_serializing_if = "Option::is_none", + )] + #[ruma_api(query)] + pub timeout: Option, + + /// The lists of rooms we're interested in. + pub lists: &'a [SyncRequestList], + + /// Specific rooms and event types that we want to receive events from. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub room_subscriptions: BTreeMap, + + /// Specific rooms we no longer want to receive events from. + #[serde(default, skip_serializing_if = "<[_]>::is_empty")] + pub unsubscribe_rooms: &'a [OwnedRoomId], + + /// Extensions API. + #[serde(skip_serializing_if = "BTreeMap::is_empty")] + pub extensions: BTreeMap, + } + + response: { + /// Whether this response describes an initial sync (i.e. after the `pos` token has been + /// discard by the server?). + #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")] + pub initial: bool, + + /// The token to supply in the `pos` param of the next `/sync` request. + pub pos: String, + + /// Updates to the sliding room list. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub lists: Vec, + + /// The updates on rooms. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub rooms: BTreeMap, + } + + error: crate::Error +} + +impl Request<'_> { + /// Creates an empty `Request`. + pub fn new() -> Self { + Default::default() + } +} + +impl Response { + /// Creates a new `Response` with the given pos. + pub fn new(pos: String) -> Self { + Self { + initial: Default::default(), + pos, + lists: Default::default(), + rooms: Default::default(), + } + } +} +/// Filter for a sliding sync list, set at request. +/// +/// All fields are applied with AND operators, hence if `is_dm` is `true` and `is_encrypted` is +/// `true` then only encrypted DM rooms will be returned. The absence of fields implies no filter +/// on that criteria: it does NOT imply `false`. +/// +/// Filters are considered _sticky_, meaning that the filter only has to be provided once and their +/// parameters 'sticks' for future requests until a new filter overwrites them. +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct SyncRequestListFilters { + /// Whether to return DMs, non-DM rooms or both. + /// + /// Flag which only returns rooms present (or not) in the DM section of account data. + /// If unset, both DM rooms and non-DM rooms are returned. If false, only non-DM rooms + /// are returned. If true, only DM rooms are returned. + #[serde(skip_serializing_if = "Option::is_none")] + pub is_dm: Option, + + /// Only list rooms that are spaces of these or all. + /// + /// A list of spaces which target rooms must be a part of. For every invited/joined + /// room for this user, ensure that there is a parent space event which is in this list. If + /// unset, all rooms are included. Servers MUST NOT navigate subspaces. It is up to the + /// client to give a complete list of spaces to navigate. Only rooms directly in these + /// spaces will be returned. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub spaces: Vec, + + /// Whether to return encrypted, non-encrypted rooms or both. + /// + /// Flag which only returns rooms which have an `m.room.encryption` state event. If + /// unset, both encrypted and unencrypted rooms are returned. If false, only unencrypted + /// rooms are returned. If true, only encrypted rooms are returned. + #[serde(skip_serializing_if = "Option::is_none")] + pub is_encrypted: Option, + + /// 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, + + /// Whether to return Rooms with tombstones, only rooms without tombstones or both. + /// + /// Flag which only returns rooms which have an `m.room.tombstone` state event. If unset, + /// both tombstoned and un-tombstoned rooms are returned. If false, only un-tombstoned rooms + /// are returned. If true, only tombstoned rooms are returned. + #[serde(skip_serializing_if = "Option::is_none")] + pub is_tombstoned: Option, + + /// Only list rooms of given create-types or all. + /// + /// If specified, only rooms where the `m.room.create` event has a `type` matching one + /// of the strings in this array will be returned. If this field is unset, all rooms are + /// returned regardless of type. This can be used to get the initial set of spaces for an + /// account. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub room_types: Vec, + + /// Only list rooms that are not of these create-types, or all. + /// + /// Same as "room_types" but inverted. This can be used to filter out spaces from the room + /// list. + #[serde(default, skip_serializing_if = "<[_]>::is_empty")] + pub not_room_types: Vec, + + /// Only list rooms matching the given string, or all. + /// + /// Filter the room name. Case-insensitive partial matching e.g 'foo' matches 'abFooab'. + /// The term 'like' is inspired by SQL 'LIKE', and the text here is similar to '%foo%'. + pub room_name_like: Option, + + /// Extensions may add further fields to the filters. + #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")] + pub extensions: BTreeMap, +} + +/// Sliding Sync Request for each list. +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct SyncRequestList { + /// Put this list into the all-rooms-mode. + /// + /// Settings this to true will inform the server that, no matter how slow + /// that might be, the clients wants all rooms the filters apply to. When operating + /// in this mode, `ranges` and `sort` will be ignored there will be no movement operations + /// (`DELETE` followed by `INSERT`) as the client has the entire list and can work out whatever + /// sort order they wish. There will still be `DELETE` and `INSERT` operations when rooms are + /// left or joined respectively. In addition, there will be an initial `SYNC` operation to let + /// the client know which rooms in the rooms object were from this list. + #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")] + pub slow_get_all_rooms: bool, + + /// The ranges of rooms we're interested in. + pub ranges: Vec<(UInt, UInt)>, + + /// The sort ordering applied to this list of rooms. Sticky. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub sort: Vec, + + /// Required state for each room returned. An array of event type and state key tuples. + /// Note that elements of this array are NOT sticky so they must be specified in full when they + /// are changed. Sticky. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub required_state: Vec<(RoomEventType, String)>, + + /// The maximum number of timeline events to return per room. Sticky. + #[serde(skip_serializing_if = "Option::is_none")] + pub timeline_limit: Option, + + /// Filters to apply to the list before sorting. Sticky. + #[serde(skip_serializing_if = "Option::is_none")] + pub filters: Option, +} + +/// The RoomSubscriptions of the SlidingSync Request +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct RoomSubscription { + /// Required state for each room returned. An array of event type and state key tuples. + /// + /// Note that elements of this array are NOT sticky so they must be specified in full when they + /// are changed. Sticky. + + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub required_state: Vec<(RoomEventType, String)>, + + /// The maximum number of timeline events to return per room. Sticky. + #[serde(skip_serializing_if = "Option::is_none")] + pub timeline_limit: Option, +} + +/// Operation applied to the specific SlidingSyncList +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "UPPERCASE")] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub enum SlidingOp { + /// Full reset of the given window. + Sync, + /// Insert an item at the given point, moves all following entry by + /// one to the next Empty or Invalid field. + Insert, + /// Drop this entry, moves all following entry up by one. + Delete, + /// Mark these as invaldiated. + Invalidate, +} + +/// Updates to joined rooms. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct SyncList { + /// The sync operation to apply, if any. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub ops: Vec, + + /// The total number of rooms found for this filter. + pub count: UInt, +} + +/// Updates to joined rooms. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct SyncOp { + /// The sync operation to apply. + pub op: SlidingOp, + + /// The range this list update applies to. + pub range: Option<(UInt, UInt)>, + + /// Or the specific index the update applies to. + pub index: Option, + + /// The list of room_ids updates to apply. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub room_ids: Vec, + + /// On insert and delete we are only receiving exactly one room_id. + pub room_id: Option, +} + +/// Updates to joined rooms. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct SlidingSyncRoom { + /// The name of the room as calculated by the server. + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + + /// Was this an initial response. + #[serde(skip_serializing_if = "Option::is_none")] + pub initial: Option, + + /// This is a direct message. + #[serde(skip_serializing_if = "Option::is_none")] + pub is_dm: Option, + + /// This is not-yet-accepted invite, with the following sync state events + /// the room must be considered in invite state as long as the Option is not None + /// even if there are no state events. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub invite_state: Vec>, + + /// Counts of unread notifications for this room. + #[serde(flatten, default, skip_serializing_if = "UnreadNotificationsCount::is_empty")] + pub unread_notifications: UnreadNotificationsCount, + + /// The timeline of messages and state changes in the room. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub timeline: Vec>, + + /// Updates to the state at the beginning of the `timeline`. + /// A list of state events. + #[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, +} + +impl SlidingSyncRoom { + /// Creates an empty `Room`. + pub fn new() -> Self { + Default::default() + } +} diff --git a/crates/ruma/Cargo.toml b/crates/ruma/Cargo.toml index 0e01ac4d..077ac84e 100644 --- a/crates/ruma/Cargo.toml +++ b/crates/ruma/Cargo.toml @@ -164,6 +164,7 @@ unstable-msc3551 = ["ruma-common/unstable-msc3551"] unstable-msc3552 = ["ruma-common/unstable-msc3552"] unstable-msc3553 = ["ruma-common/unstable-msc3553"] unstable-msc3554 = ["ruma-common/unstable-msc3554"] +unstable-msc3575 = ["ruma-client-api?/unstable-msc3575"] unstable-msc3618 = ["ruma-federation-api?/unstable-msc3618"] unstable-msc3723 = ["ruma-federation-api?/unstable-msc3723"] unstable-msc3786 = ["ruma-common/unstable-msc3786"] @@ -194,6 +195,7 @@ __ci = [ "unstable-msc3552", "unstable-msc3553", "unstable-msc3554", + "unstable-msc3575", "unstable-msc3618", "unstable-msc3723", "unstable-msc3786",