use std::{collections::HashMap, time::Duration};
use js_int::UInt;
use ruma_api::{ruma_api, Outgoing};
use ruma_events::{
collections::{
all::{RoomEvent, StateEvent},
only::Event as NonRoomEvent,
},
presence::PresenceEvent,
stripped::AnyStrippedStateEvent,
to_device::AnyToDeviceEvent,
EventResult,
};
use ruma_identifiers::RoomId;
use serde::{Deserialize, Serialize};
use crate::{
r0::{filter::FilterDefinition, keys::KeyAlgorithm},
serde::is_default,
};
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,
requires_authentication: true,
}
request {
#[serde(skip_serializing_if = "Option::is_none")]
#[ruma_api(query)]
pub filter: Option<Filter>,
#[serde(skip_serializing_if = "Option::is_none")]
#[ruma_api(query)]
pub since: Option<String>,
#[serde(default, skip_serializing_if = "is_default")]
#[ruma_api(query)]
pub full_state: bool,
#[serde(default, skip_serializing_if = "is_default")]
#[ruma_api(query)]
pub set_presence: SetPresence,
#[serde(
with = "crate::serde::duration::opt_ms",
default,
skip_serializing_if = "Option::is_none",
)]
#[ruma_api(query)]
pub timeout: Option<Duration>,
}
response {
pub next_batch: String,
#[wrap_incoming]
pub rooms: Rooms,
#[wrap_incoming]
pub presence: Presence,
#[serde(default, skip_serializing_if = "ToDevice::is_empty")]
#[wrap_incoming]
pub to_device: ToDevice,
#[serde(skip_serializing_if = "Option::is_none")]
pub device_lists: Option<DeviceLists>,
#[serde(skip_serializing_if = "HashMap::is_empty")]
pub device_one_time_keys_count: HashMap<KeyAlgorithm, UInt>,
}
error: crate::Error
}
#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum SetPresence {
Offline,
Online,
Unavailable,
}
impl Default for SetPresence {
fn default() -> Self {
Self::Online
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[allow(clippy::large_enum_variant)]
#[serde(untagged)]
pub enum Filter {
#[serde(with = "filter_def_serde")]
FilterDefinition(FilterDefinition),
FilterId(String),
}
mod filter_def_serde {
use serde::{de::Error as _, ser::Error as _, Deserialize, Deserializer, Serializer};
use crate::r0::filter::FilterDefinition;
pub fn serialize<S>(filter_def: &FilterDefinition, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let string = serde_json::to_string(filter_def).map_err(S::Error::custom)?;
serializer.serialize_str(&string)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<FilterDefinition, D::Error>
where
D: Deserializer<'de>,
{
let filter_str = <&str>::deserialize(deserializer)?;
serde_json::from_str(filter_str).map_err(D::Error::custom)
}
}
#[derive(Clone, Debug, Serialize, Outgoing)]
pub struct Rooms {
#[wrap_incoming(LeftRoom)]
pub leave: HashMap<RoomId, LeftRoom>,
#[wrap_incoming(JoinedRoom)]
pub join: HashMap<RoomId, JoinedRoom>,
#[wrap_incoming(InvitedRoom)]
pub invite: HashMap<RoomId, InvitedRoom>,
}
#[derive(Clone, Debug, Serialize, Outgoing)]
pub struct LeftRoom {
#[wrap_incoming]
pub timeline: Timeline,
#[wrap_incoming]
pub state: State,
#[wrap_incoming]
pub account_data: AccountData,
}
#[derive(Clone, Debug, Serialize, Outgoing)]
pub struct JoinedRoom {
pub summary: RoomSummary,
pub unread_notifications: UnreadNotificationsCount,
#[wrap_incoming]
pub timeline: Timeline,
#[wrap_incoming]
pub state: State,
#[wrap_incoming]
pub account_data: AccountData,
#[wrap_incoming]
pub ephemeral: Ephemeral,
}
#[derive(Clone, Copy, 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>,
}
#[derive(Clone, Debug, Serialize, Outgoing)]
pub struct Timeline {
#[serde(skip_serializing_if = "Option::is_none")]
pub limited: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prev_batch: Option<String>,
#[wrap_incoming(RoomEvent with EventResult)]
pub events: Vec<RoomEvent>,
}
#[derive(Clone, Debug, Serialize, Outgoing)]
pub struct State {
#[wrap_incoming(StateEvent with EventResult)]
pub events: Vec<StateEvent>,
}
#[derive(Clone, Debug, Serialize, Outgoing)]
pub struct AccountData {
#[wrap_incoming(NonRoomEvent with EventResult)]
pub events: Vec<NonRoomEvent>,
}
#[derive(Clone, Debug, Serialize, Outgoing)]
pub struct Ephemeral {
#[wrap_incoming(NonRoomEvent with EventResult)]
pub events: Vec<NonRoomEvent>,
}
#[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")]
pub joined_member_count: Option<UInt>,
#[serde(rename = "m.invited_member_count")]
pub invited_member_count: Option<UInt>,
}
#[derive(Clone, Debug, Serialize, Outgoing)]
pub struct InvitedRoom {
#[wrap_incoming]
pub invite_state: InviteState,
}
#[derive(Clone, Debug, Serialize, Outgoing)]
pub struct InviteState {
#[wrap_incoming(AnyStrippedStateEvent with EventResult)]
pub events: Vec<AnyStrippedStateEvent>,
}
#[derive(Clone, Debug, Serialize, Outgoing)]
pub struct Presence {
#[wrap_incoming(PresenceEvent with EventResult)]
pub events: Vec<PresenceEvent>,
}
#[derive(Clone, Debug, Serialize, Outgoing)]
pub struct ToDevice {
#[wrap_incoming(AnyToDeviceEvent with EventResult)]
pub events: Vec<AnyToDeviceEvent>,
}
impl ToDevice {
fn is_empty(&self) -> bool {
self.events.is_empty()
}
}
impl Default for IncomingToDevice {
fn default() -> Self {
Self { events: Vec::new() }
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct DeviceLists {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub changed: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub left: Vec<String>,
}
#[cfg(test)]
mod tests {
use std::{convert::TryInto, time::Duration};
use super::{Filter, Request, SetPresence};
#[test]
fn serialize_sync_request() {
let req: http::Request<Vec<u8>> = Request {
filter: Some(Filter::FilterId("66696p746572".into())),
since: Some("s72594_4483_1934".into()),
full_state: true,
set_presence: SetPresence::Offline,
timeout: Some(Duration::from_millis(30000)),
}
.try_into()
.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"))
}
#[test]
fn deserialize_sync_request_with_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: Request = http::Request::builder()
.uri(uri)
.body(Vec::<u8>::new())
.unwrap()
.try_into()
.unwrap();
match req.filter {
Some(Filter::FilterId(id)) if id == "myfilter" => {}
_ => {
panic!("Not the expected filter ID.");
}
}
assert_eq!(req.since, Some("myts".into()));
assert_eq!(req.full_state, false);
assert_eq!(req.set_presence, SetPresence::Offline);
assert_eq!(req.timeout, Some(Duration::from_millis(5000)));
}
#[test]
fn deserialize_sync_request_without_query_params() {
let uri = http::Uri::builder()
.scheme("https")
.authority("matrix.org")
.path_and_query("/_matrix/client/r0/sync")
.build()
.unwrap();
let req: Request = http::Request::builder()
.uri(uri)
.body(Vec::<u8>::new())
.unwrap()
.try_into()
.unwrap();
assert!(req.filter.is_none());
assert!(req.since.is_none());
assert_eq!(req.full_state, false);
assert_eq!(req.set_presence, SetPresence::Online);
assert!(req.timeout.is_none());
}
}