#![allow(clippy::exhaustive_structs)]
use std::{collections::BTreeMap, time::Duration};
use js_int::UInt;
use ruma_api::ruma_api;
use ruma_common::presence::PresenceState;
use ruma_events::{
presence::PresenceEvent, AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent,
AnyStrippedStateEvent, AnySyncEphemeralRoomEvent, AnySyncRoomEvent, AnySyncStateEvent,
AnyToDeviceEvent,
};
use ruma_identifiers::{DeviceKeyAlgorithm, RoomId, UserId};
use ruma_serde::{Outgoing, Raw};
use serde::{Deserialize, Serialize};
use crate::r0::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",
path: "/_matrix/client/r0/sync",
rate_limited: false,
authentication: AccessToken,
}
#[derive(Default)]
request: {
#[serde(skip_serializing_if = "Option::is_none")]
#[ruma_api(query)]
pub filter: Option<&'a Filter<'a>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[ruma_api(query)]
pub since: Option<&'a str>,
#[serde(default, skip_serializing_if = "ruma_serde::is_default")]
#[ruma_api(query)]
pub full_state: bool,
#[serde(default, skip_serializing_if = "ruma_serde::is_default")]
#[ruma_api(query)]
pub set_presence: &'a PresenceState,
#[serde(
with = "ruma_serde::duration::opt_ms",
default,
skip_serializing_if = "Option::is_none",
)]
#[ruma_api(query)]
pub timeout: Option<Duration>,
}
response: {
pub next_batch: String,
#[serde(default, skip_serializing_if = "Rooms::is_empty")]
pub rooms: Rooms,
#[serde(default, skip_serializing_if = "Presence::is_empty")]
pub presence: Presence,
#[serde(default, skip_serializing_if = "GlobalAccountData::is_empty")]
pub account_data: GlobalAccountData,
#[serde(default, skip_serializing_if = "ToDevice::is_empty")]
pub to_device: ToDevice,
#[serde(default, skip_serializing_if = "DeviceLists::is_empty")]
pub device_lists: DeviceLists,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub device_one_time_keys_count: BTreeMap<DeviceKeyAlgorithm, UInt>,
#[cfg(not(feature = "unstable-exhaustive-types"))]
#[doc(hidden)]
#[serde(skip, default = "crate::private")]
pub __test_exhaustive: crate::Private,
}
error: crate::Error
}
impl Request<'_> {
pub fn new() -> Self {
Default::default()
}
}
impl Response {
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(),
#[cfg(not(feature = "unstable-exhaustive-types"))]
__test_exhaustive: crate::private(),
}
}
}
#[derive(Clone, Debug, Outgoing, Serialize)]
#[allow(clippy::large_enum_variant)]
#[serde(untagged)]
pub enum Filter<'a> {
#[serde(with = "ruma_serde::json_string")]
FilterDefinition(FilterDefinition<'a>),
FilterId(&'a str),
}
impl<'a> From<FilterDefinition<'a>> 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)
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Rooms {
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub leave: BTreeMap<RoomId, LeftRoom>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub join: BTreeMap<RoomId, JoinedRoom>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub invite: BTreeMap<RoomId, InvitedRoom>,
#[cfg(not(feature = "unstable-exhaustive-types"))]
#[doc(hidden)]
#[serde(skip, default = "crate::private")]
pub __test_exhaustive: crate::Private,
}
impl Rooms {
pub fn new() -> Self {
Default::default()
}
pub fn is_empty(&self) -> bool {
self.leave.is_empty() && self.join.is_empty() && self.invite.is_empty()
}
}
impl Default for Rooms {
fn default() -> Self {
Self {
leave: BTreeMap::new(),
join: BTreeMap::new(),
invite: BTreeMap::new(),
#[cfg(not(feature = "unstable-exhaustive-types"))]
__test_exhaustive: crate::private(),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct LeftRoom {
#[serde(default, skip_serializing_if = "Timeline::is_empty")]
pub timeline: Timeline,
#[serde(default, skip_serializing_if = "State::is_empty")]
pub state: State,
#[serde(default, skip_serializing_if = "RoomAccountData::is_empty")]
pub account_data: RoomAccountData,
#[cfg(not(feature = "unstable-exhaustive-types"))]
#[doc(hidden)]
#[serde(skip, default = "crate::private")]
pub __test_exhaustive: crate::Private,
}
impl LeftRoom {
pub fn new() -> Self {
Default::default()
}
pub fn is_empty(&self) -> bool {
self.timeline.is_empty() && self.state.is_empty() && self.account_data.is_empty()
}
}
impl Default for LeftRoom {
fn default() -> Self {
Self {
timeline: Default::default(),
state: Default::default(),
account_data: Default::default(),
#[cfg(not(feature = "unstable-exhaustive-types"))]
__test_exhaustive: crate::private(),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct JoinedRoom {
#[serde(default, skip_serializing_if = "RoomSummary::is_empty")]
pub summary: RoomSummary,
#[serde(default, skip_serializing_if = "UnreadNotificationsCount::is_empty")]
pub unread_notifications: UnreadNotificationsCount,
#[serde(default, skip_serializing_if = "Timeline::is_empty")]
pub timeline: Timeline,
#[serde(default, skip_serializing_if = "State::is_empty")]
pub state: State,
#[serde(default, skip_serializing_if = "RoomAccountData::is_empty")]
pub account_data: RoomAccountData,
#[serde(default, skip_serializing_if = "Ephemeral::is_empty")]
pub ephemeral: Ephemeral,
#[cfg(not(feature = "unstable-exhaustive-types"))]
#[doc(hidden)]
#[serde(skip, default = "crate::private")]
pub __test_exhaustive: crate::Private,
}
impl JoinedRoom {
pub fn new() -> Self {
Default::default()
}
pub fn is_empty(&self) -> bool {
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()
}
}
impl Default for JoinedRoom {
fn default() -> Self {
Self {
summary: Default::default(),
unread_notifications: Default::default(),
timeline: Default::default(),
state: Default::default(),
account_data: Default::default(),
ephemeral: Default::default(),
#[cfg(not(feature = "unstable-exhaustive-types"))]
__test_exhaustive: crate::private(),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct UnreadNotificationsCount {
#[serde(skip_serializing_if = "Option::is_none")]
pub highlight_count: Option<UInt>,
#[serde(skip_serializing_if = "Option::is_none")]
pub notification_count: Option<UInt>,
#[cfg(not(feature = "unstable-exhaustive-types"))]
#[doc(hidden)]
#[serde(skip, default = "crate::private")]
pub __test_exhaustive: crate::Private,
}
impl UnreadNotificationsCount {
pub fn new() -> Self {
Default::default()
}
pub fn is_empty(&self) -> bool {
self.highlight_count.is_none() && self.notification_count.is_none()
}
}
impl Default for UnreadNotificationsCount {
fn default() -> Self {
Self {
highlight_count: None,
notification_count: None,
#[cfg(not(feature = "unstable-exhaustive-types"))]
__test_exhaustive: crate::private(),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Timeline {
#[serde(default, skip_serializing_if = "ruma_serde::is_default")]
pub limited: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub prev_batch: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub events: Vec<Raw<AnySyncRoomEvent>>,
#[cfg(not(feature = "unstable-exhaustive-types"))]
#[doc(hidden)]
#[serde(skip, default = "crate::private")]
pub __test_exhaustive: crate::Private,
}
impl Timeline {
pub fn new() -> Self {
Default::default()
}
pub fn is_empty(&self) -> bool {
self.events.is_empty()
}
}
impl Default for Timeline {
fn default() -> Self {
Self {
limited: false,
prev_batch: None,
events: vec![],
#[cfg(not(feature = "unstable-exhaustive-types"))]
__test_exhaustive: crate::private(),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct State {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub events: Vec<Raw<AnySyncStateEvent>>,
#[cfg(not(feature = "unstable-exhaustive-types"))]
#[doc(hidden)]
#[serde(skip, default = "crate::private")]
pub __test_exhaustive: crate::Private,
}
impl State {
pub fn new() -> Self {
Default::default()
}
pub fn is_empty(&self) -> bool {
self.events.is_empty()
}
}
impl Default for State {
fn default() -> Self {
Self {
events: vec![],
#[cfg(not(feature = "unstable-exhaustive-types"))]
__test_exhaustive: crate::private(),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct GlobalAccountData {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub events: Vec<Raw<AnyGlobalAccountDataEvent>>,
#[cfg(not(feature = "unstable-exhaustive-types"))]
#[doc(hidden)]
#[serde(skip, default = "crate::private")]
pub __test_exhaustive: crate::Private,
}
impl GlobalAccountData {
pub fn new() -> Self {
Default::default()
}
pub fn is_empty(&self) -> bool {
self.events.is_empty()
}
}
impl Default for GlobalAccountData {
fn default() -> Self {
Self {
events: vec![],
#[cfg(not(feature = "unstable-exhaustive-types"))]
__test_exhaustive: crate::private(),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct RoomAccountData {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub events: Vec<Raw<AnyRoomAccountDataEvent>>,
#[cfg(not(feature = "unstable-exhaustive-types"))]
#[doc(hidden)]
#[serde(skip, default = "crate::private")]
pub __test_exhaustive: crate::Private,
}
impl RoomAccountData {
pub fn new() -> Self {
Default::default()
}
pub fn is_empty(&self) -> bool {
self.events.is_empty()
}
}
impl Default for RoomAccountData {
fn default() -> Self {
Self {
events: vec![],
#[cfg(not(feature = "unstable-exhaustive-types"))]
__test_exhaustive: crate::private(),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Ephemeral {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub events: Vec<Raw<AnySyncEphemeralRoomEvent>>,
#[cfg(not(feature = "unstable-exhaustive-types"))]
#[doc(hidden)]
#[serde(skip, default = "crate::private")]
pub __test_exhaustive: crate::Private,
}
impl Ephemeral {
pub fn new() -> Self {
Default::default()
}
pub fn is_empty(&self) -> bool {
self.events.is_empty()
}
}
impl Default for Ephemeral {
fn default() -> Self {
Self {
events: vec![],
#[cfg(not(feature = "unstable-exhaustive-types"))]
__test_exhaustive: crate::private(),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct RoomSummary {
#[serde(rename = "m.heroes", default, skip_serializing_if = "Vec::is_empty")]
pub heroes: Vec<String>,
#[serde(rename = "m.joined_member_count", skip_serializing_if = "Option::is_none")]
pub joined_member_count: Option<UInt>,
#[serde(rename = "m.invited_member_count", skip_serializing_if = "Option::is_none")]
pub invited_member_count: Option<UInt>,
#[cfg(not(feature = "unstable-exhaustive-types"))]
#[doc(hidden)]
#[serde(skip, default = "crate::private")]
pub __test_exhaustive: crate::Private,
}
impl RoomSummary {
pub fn new() -> Self {
Default::default()
}
pub fn is_empty(&self) -> bool {
self.heroes.is_empty()
&& self.joined_member_count.is_none()
&& self.invited_member_count.is_none()
}
}
impl Default for RoomSummary {
fn default() -> Self {
Self {
heroes: vec![],
joined_member_count: None,
invited_member_count: None,
#[cfg(not(feature = "unstable-exhaustive-types"))]
__test_exhaustive: crate::private(),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct InvitedRoom {
#[serde(default, skip_serializing_if = "InviteState::is_empty")]
pub invite_state: InviteState,
#[cfg(not(feature = "unstable-exhaustive-types"))]
#[doc(hidden)]
#[serde(skip, default = "crate::private")]
pub __test_exhaustive: crate::Private,
}
impl InvitedRoom {
pub fn new() -> Self {
Default::default()
}
pub fn is_empty(&self) -> bool {
self.invite_state.is_empty()
}
}
impl Default for InvitedRoom {
fn default() -> Self {
Self {
invite_state: Default::default(),
#[cfg(not(feature = "unstable-exhaustive-types"))]
__test_exhaustive: crate::private(),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct InviteState {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub events: Vec<Raw<AnyStrippedStateEvent>>,
#[cfg(not(feature = "unstable-exhaustive-types"))]
#[doc(hidden)]
#[serde(skip, default = "crate::private")]
pub __test_exhaustive: crate::Private,
}
impl InviteState {
pub fn new() -> Self {
Default::default()
}
pub fn is_empty(&self) -> bool {
self.events.is_empty()
}
}
impl Default for InviteState {
fn default() -> Self {
Self {
events: vec![],
#[cfg(not(feature = "unstable-exhaustive-types"))]
__test_exhaustive: crate::private(),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Presence {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub events: Vec<Raw<PresenceEvent>>,
#[cfg(not(feature = "unstable-exhaustive-types"))]
#[doc(hidden)]
#[serde(skip, default = "crate::private")]
pub __test_exhaustive: crate::Private,
}
impl Presence {
pub fn new() -> Self {
Default::default()
}
pub fn is_empty(&self) -> bool {
self.events.is_empty()
}
}
impl Default for Presence {
fn default() -> Self {
Self {
events: vec![],
#[cfg(not(feature = "unstable-exhaustive-types"))]
__test_exhaustive: crate::private(),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ToDevice {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub events: Vec<Raw<AnyToDeviceEvent>>,
#[cfg(not(feature = "unstable-exhaustive-types"))]
#[doc(hidden)]
#[serde(skip, default = "crate::private")]
pub __test_exhaustive: crate::Private,
}
impl ToDevice {
pub fn new() -> Self {
Default::default()
}
pub fn is_empty(&self) -> bool {
self.events.is_empty()
}
}
impl Default for ToDevice {
fn default() -> Self {
Self {
events: vec![],
#[cfg(not(feature = "unstable-exhaustive-types"))]
__test_exhaustive: crate::private(),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct DeviceLists {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub changed: Vec<UserId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub left: Vec<UserId>,
#[cfg(not(feature = "unstable-exhaustive-types"))]
#[doc(hidden)]
#[serde(skip, default = "crate::private")]
pub __test_exhaustive: crate::Private,
}
impl DeviceLists {
pub fn new() -> Self {
Default::default()
}
pub fn is_empty(&self) -> bool {
self.changed.is_empty() && self.left.is_empty()
}
}
impl Default for DeviceLists {
fn default() -> Self {
Self {
changed: vec![],
left: vec![],
#[cfg(not(feature = "unstable-exhaustive-types"))]
__test_exhaustive: crate::private(),
}
}
}
#[cfg(test)]
mod tests {
use assign::assign;
use matches::assert_matches;
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);
assert_matches!(timeline_deserialized, Ok(Timeline { limited: true, .. }));
let timeline_default = Timeline::default();
assert_eq!(to_json_value(timeline_default).unwrap(), json!({}));
let timeline_default_deserialized = from_json_value(json!({}));
assert_matches!(timeline_default_deserialized, Ok(Timeline { limited: false, .. }));
}
}
#[cfg(all(test, feature = "client"))]
mod client_tests {
use std::time::Duration;
use ruma_api::{OutgoingRequest as _, SendAccessToken};
use super::{Filter, PresenceState, Request};
#[test]
fn serialize_all_params() {
let req: http::Request<Vec<u8>> = 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"))
.unwrap();
let uri = req.uri();
let query = uri.query().unwrap();
assert_eq!(uri.path(), "/_matrix/client/r0/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 matches::assert_matches;
use ruma_api::IncomingRequest as _;
use ruma_common::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(),
)
.unwrap();
assert_matches!(req.filter, Some(IncomingFilter::FilterId(id)) if id == "myfilter");
assert_eq!(req.since, Some("myts".into()));
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(),
)
.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(),
)
.unwrap();
assert_matches!(req.filter, Some(IncomingFilter::FilterId(id)) if 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)));
}
}