use crate::error::MessengerError;
use crate::ids::{RoomId, UserId};
use crate::wire::events::{BasicEvent, RawEvent, StrippedStateEvent, ToDeviceEvent};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
pub fn parse_sync_response(body: &[u8]) -> Result<SyncResponse, MessengerError> {
Ok(serde_json::from_slice(body)?)
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct RoomSummary {
#[serde(default, rename = "m.heroes")]
pub heroes: Vec<UserId>,
#[serde(default, rename = "m.joined_member_count", skip_serializing_if = "Option::is_none")]
pub joined_member_count: Option<u64>,
#[serde(default, rename = "m.invited_member_count", skip_serializing_if = "Option::is_none")]
pub invited_member_count: Option<u64>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct StateSection {
#[serde(default)]
pub events: Vec<RawEvent>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct TimelineSection {
#[serde(default)]
pub events: Vec<RawEvent>,
#[serde(default)]
pub limited: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prev_batch: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct EphemeralSection {
#[serde(default)]
pub events: Vec<BasicEvent>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct AccountDataSection {
#[serde(default)]
pub events: Vec<BasicEvent>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct UnreadNotifications {
#[serde(default)]
pub notification_count: u64,
#[serde(default)]
pub highlight_count: u64,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct JoinedRoom {
#[serde(default)]
pub summary: RoomSummary,
#[serde(default)]
pub state: StateSection,
#[serde(default)]
pub timeline: TimelineSection,
#[serde(default)]
pub ephemeral: EphemeralSection,
#[serde(default)]
pub account_data: AccountDataSection,
#[serde(default)]
pub unread_notifications: UnreadNotifications,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct InviteStateSection {
#[serde(default)]
pub events: Vec<StrippedStateEvent>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct InvitedRoom {
#[serde(default)]
pub invite_state: InviteStateSection,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct LeftRoom {
#[serde(default)]
pub state: StateSection,
#[serde(default)]
pub timeline: TimelineSection,
#[serde(default)]
pub account_data: AccountDataSection,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct RoomsSection {
#[serde(default)]
pub join: BTreeMap<RoomId, JoinedRoom>,
#[serde(default)]
pub invite: BTreeMap<RoomId, InvitedRoom>,
#[serde(default)]
pub leave: BTreeMap<RoomId, LeftRoom>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct ToDeviceSection {
#[serde(default)]
pub events: Vec<ToDeviceEvent>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct DeviceListsSection {
#[serde(default)]
pub changed: Vec<UserId>,
#[serde(default)]
pub left: Vec<UserId>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct SyncResponse {
pub next_batch: String,
#[serde(default)]
pub rooms: RoomsSection,
#[serde(default)]
pub account_data: AccountDataSection,
#[serde(default)]
pub to_device: ToDeviceSection,
#[serde(default)]
pub device_lists: DeviceListsSection,
#[serde(default)]
pub device_one_time_keys_count: BTreeMap<String, u64>,
#[serde(default)]
pub device_unused_fallback_key_types: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ids::EventId;
use crate::wire::events::RoomKeyContent;
#[test]
fn parses_incremental_sync_with_only_next_batch() {
let body = br#"{"next_batch":"s72595_4483_1934"}"#;
let response = parse_sync_response(body).expect("sparse sync response parses");
assert_eq!(response.next_batch, "s72595_4483_1934");
assert!(response.rooms.join.is_empty());
assert!(response.rooms.invite.is_empty());
assert!(response.rooms.leave.is_empty());
assert!(response.to_device.events.is_empty());
assert!(response.device_lists.changed.is_empty());
assert!(response.device_one_time_keys_count.is_empty());
assert!(response.device_unused_fallback_key_types.is_empty());
}
#[test]
fn parses_sync_response_with_limited_timeline() {
let body = serde_json::json!({
"next_batch": "s999",
"rooms": {
"join": {
"!room:example.org": {
"summary": {
"m.heroes": ["@bob:example.org"],
"m.joined_member_count": 3,
"m.invited_member_count": 0
},
"state": { "events": [] },
"timeline": {
"events": [
{
"event_id": "$1:example.org",
"type": "m.room.message",
"sender": "@alice:example.org",
"origin_server_ts": 1_700_000_000_000i64,
"content": { "msgtype": "m.text", "body": "hi" }
}
],
"limited": true,
"prev_batch": "t111_222"
},
"ephemeral": { "events": [] },
"account_data": { "events": [] },
"unread_notifications": { "notification_count": 2, "highlight_count": 0 }
}
}
}
})
.to_string();
let response = parse_sync_response(body.as_bytes()).expect("valid sync response");
assert_eq!(response.next_batch, "s999");
let room_id = RoomId::parse("!room:example.org").expect("valid room id");
let room = response.rooms.join.get(&room_id).expect("room present");
assert!(room.timeline.limited);
assert_eq!(room.timeline.prev_batch.as_deref(), Some("t111_222"));
assert_eq!(room.timeline.events.len(), 1);
assert_eq!(room.timeline.events[0].event_type, "m.room.message");
assert_eq!(room.summary.joined_member_count, Some(3));
assert_eq!(room.unread_notifications.notification_count, 2);
assert_eq!(room.unread_notifications.highlight_count, 0);
}
#[test]
fn parses_sync_response_with_to_device_events() {
let body = serde_json::json!({
"next_batch": "s2",
"to_device": {
"events": [
{
"sender": "@bob:example.org",
"type": "m.room_key",
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!r:example.org",
"session_id": "session-abc",
"session_key": "session-key-b64"
}
}
]
},
"device_one_time_keys_count": { "signed_curve25519": 42 },
"device_unused_fallback_key_types": []
})
.to_string();
let response = parse_sync_response(body.as_bytes()).expect("valid sync response");
assert_eq!(response.to_device.events.len(), 1);
let event = &response.to_device.events[0];
assert_eq!(event.event_type, "m.room_key");
let content: RoomKeyContent = serde_json::from_value(event.content.clone()).expect("valid room_key content");
assert_eq!(content.session_id, "session-abc");
assert_eq!(content.room_id, RoomId::parse("!r:example.org").expect("valid room id"));
assert_eq!(response.device_one_time_keys_count.get("signed_curve25519"), Some(&42));
assert!(response.device_unused_fallback_key_types.is_empty());
}
#[test]
fn parses_invite_and_leave_room_sections() {
let body = serde_json::json!({
"next_batch": "s3",
"rooms": {
"invite": {
"!invited:example.org": {
"invite_state": {
"events": [
{
"type": "m.room.member",
"state_key": "@me:example.org",
"sender": "@bob:example.org",
"content": { "membership": "invite" }
}
]
}
}
},
"leave": {
"!left:example.org": {
"timeline": { "events": [] },
"state": { "events": [] }
}
}
}
})
.to_string();
let response = parse_sync_response(body.as_bytes()).expect("valid sync response");
let invited_id = RoomId::parse("!invited:example.org").expect("valid room id");
let invited = response.rooms.invite.get(&invited_id).expect("invited room present");
assert_eq!(invited.invite_state.events.len(), 1);
assert_eq!(invited.invite_state.events[0].state_key, "@me:example.org");
let left_id = RoomId::parse("!left:example.org").expect("valid room id");
assert!(response.rooms.leave.contains_key(&left_id));
}
#[test]
fn ephemeral_and_account_data_events_use_the_basic_shape() {
let body = serde_json::json!({
"next_batch": "s4",
"rooms": {
"join": {
"!room:example.org": {
"ephemeral": {
"events": [
{ "type": "m.typing", "content": { "user_ids": ["@alice:example.org"] } }
]
},
"account_data": {
"events": [
{ "type": "m.fully_read", "content": { "event_id": "$last:example.org" } }
]
}
}
}
}
})
.to_string();
let response = parse_sync_response(body.as_bytes()).expect("valid sync response");
let room_id = RoomId::parse("!room:example.org").expect("valid room id");
let room = response.rooms.join.get(&room_id).expect("room present");
assert_eq!(room.ephemeral.events[0].event_type, "m.typing");
let fully_read: crate::wire::events::FullyReadContent =
serde_json::from_value(room.account_data.events[0].content.clone()).expect("valid fully_read content");
assert_eq!(fully_read.event_id, EventId::parse("$last:example.org").expect("valid event id"));
}
}