1use crate::error::MessengerError;
13use crate::ids::{RoomId, UserId};
14use crate::wire::events::{BasicEvent, RawEvent, StrippedStateEvent, ToDeviceEvent};
15use serde::{Deserialize, Serialize};
16use std::collections::BTreeMap;
17
18pub fn parse_sync_response(body: &[u8]) -> Result<SyncResponse, MessengerError> {
20 Ok(serde_json::from_slice(body)?)
21}
22
23#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
26pub struct RoomSummary {
27 #[serde(default, rename = "m.heroes")]
30 pub heroes: Vec<UserId>,
31 #[serde(default, rename = "m.joined_member_count", skip_serializing_if = "Option::is_none")]
35 pub joined_member_count: Option<u64>,
36 #[serde(default, rename = "m.invited_member_count", skip_serializing_if = "Option::is_none")]
39 pub invited_member_count: Option<u64>,
40}
41
42#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
46pub struct StateSection {
47 #[serde(default)]
49 pub events: Vec<RawEvent>,
50}
51
52#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
54pub struct TimelineSection {
55 #[serde(default)]
57 pub events: Vec<RawEvent>,
58 #[serde(default)]
63 pub limited: bool,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub prev_batch: Option<String>,
69}
70
71#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
74pub struct EphemeralSection {
75 #[serde(default)]
77 pub events: Vec<BasicEvent>,
78}
79
80#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
83pub struct AccountDataSection {
84 #[serde(default)]
86 pub events: Vec<BasicEvent>,
87}
88
89#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
95pub struct UnreadNotifications {
96 #[serde(default)]
98 pub notification_count: u64,
99 #[serde(default)]
102 pub highlight_count: u64,
103}
104
105#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
107pub struct JoinedRoom {
108 #[serde(default)]
110 pub summary: RoomSummary,
111 #[serde(default)]
113 pub state: StateSection,
114 #[serde(default)]
116 pub timeline: TimelineSection,
117 #[serde(default)]
119 pub ephemeral: EphemeralSection,
120 #[serde(default)]
122 pub account_data: AccountDataSection,
123 #[serde(default)]
125 pub unread_notifications: UnreadNotifications,
126}
127
128#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
130pub struct InviteStateSection {
131 #[serde(default)]
133 pub events: Vec<StrippedStateEvent>,
134}
135
136#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
138pub struct InvitedRoom {
139 #[serde(default)]
142 pub invite_state: InviteStateSection,
143}
144
145#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
148pub struct LeftRoom {
149 #[serde(default)]
152 pub state: StateSection,
153 #[serde(default)]
155 pub timeline: TimelineSection,
156 #[serde(default)]
158 pub account_data: AccountDataSection,
159}
160
161#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
164pub struct RoomsSection {
165 #[serde(default)]
167 pub join: BTreeMap<RoomId, JoinedRoom>,
168 #[serde(default)]
170 pub invite: BTreeMap<RoomId, InvitedRoom>,
171 #[serde(default)]
173 pub leave: BTreeMap<RoomId, LeftRoom>,
174}
175
176#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
179pub struct ToDeviceSection {
180 #[serde(default)]
182 pub events: Vec<ToDeviceEvent>,
183}
184
185#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
188pub struct DeviceListsSection {
189 #[serde(default)]
191 pub changed: Vec<UserId>,
192 #[serde(default)]
195 pub left: Vec<UserId>,
196}
197
198#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
201pub struct SyncResponse {
202 pub next_batch: String,
204 #[serde(default)]
206 pub rooms: RoomsSection,
207 #[serde(default)]
209 pub account_data: AccountDataSection,
210 #[serde(default)]
212 pub to_device: ToDeviceSection,
213 #[serde(default)]
216 pub device_lists: DeviceListsSection,
217 #[serde(default)]
220 pub device_one_time_keys_count: BTreeMap<String, u64>,
221 #[serde(default)]
225 pub device_unused_fallback_key_types: Vec<String>,
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use crate::ids::EventId;
232 use crate::wire::events::RoomKeyContent;
233
234 #[test]
235 fn parses_incremental_sync_with_only_next_batch() {
236 let body = br#"{"next_batch":"s72595_4483_1934"}"#;
237 let response = parse_sync_response(body).expect("sparse sync response parses");
238 assert_eq!(response.next_batch, "s72595_4483_1934");
239 assert!(response.rooms.join.is_empty());
240 assert!(response.rooms.invite.is_empty());
241 assert!(response.rooms.leave.is_empty());
242 assert!(response.to_device.events.is_empty());
243 assert!(response.device_lists.changed.is_empty());
244 assert!(response.device_one_time_keys_count.is_empty());
245 assert!(response.device_unused_fallback_key_types.is_empty());
246 }
247
248 #[test]
249 fn parses_sync_response_with_limited_timeline() {
250 let body = serde_json::json!({
251 "next_batch": "s999",
252 "rooms": {
253 "join": {
254 "!room:example.org": {
255 "summary": {
256 "m.heroes": ["@bob:example.org"],
257 "m.joined_member_count": 3,
258 "m.invited_member_count": 0
259 },
260 "state": { "events": [] },
261 "timeline": {
262 "events": [
263 {
264 "event_id": "$1:example.org",
265 "type": "m.room.message",
266 "sender": "@alice:example.org",
267 "origin_server_ts": 1_700_000_000_000i64,
268 "content": { "msgtype": "m.text", "body": "hi" }
269 }
270 ],
271 "limited": true,
272 "prev_batch": "t111_222"
273 },
274 "ephemeral": { "events": [] },
275 "account_data": { "events": [] },
276 "unread_notifications": { "notification_count": 2, "highlight_count": 0 }
277 }
278 }
279 }
280 })
281 .to_string();
282
283 let response = parse_sync_response(body.as_bytes()).expect("valid sync response");
284 assert_eq!(response.next_batch, "s999");
285 let room_id = RoomId::parse("!room:example.org").expect("valid room id");
286 let room = response.rooms.join.get(&room_id).expect("room present");
287 assert!(room.timeline.limited);
288 assert_eq!(room.timeline.prev_batch.as_deref(), Some("t111_222"));
289 assert_eq!(room.timeline.events.len(), 1);
290 assert_eq!(room.timeline.events[0].event_type, "m.room.message");
291 assert_eq!(room.summary.joined_member_count, Some(3));
292 assert_eq!(room.unread_notifications.notification_count, 2);
293 assert_eq!(room.unread_notifications.highlight_count, 0);
294 }
295
296 #[test]
297 fn parses_sync_response_with_to_device_events() {
298 let body = serde_json::json!({
299 "next_batch": "s2",
300 "to_device": {
301 "events": [
302 {
303 "sender": "@bob:example.org",
304 "type": "m.room_key",
305 "content": {
306 "algorithm": "m.megolm.v1.aes-sha2",
307 "room_id": "!r:example.org",
308 "session_id": "session-abc",
309 "session_key": "session-key-b64"
310 }
311 }
312 ]
313 },
314 "device_one_time_keys_count": { "signed_curve25519": 42 },
315 "device_unused_fallback_key_types": []
316 })
317 .to_string();
318
319 let response = parse_sync_response(body.as_bytes()).expect("valid sync response");
320 assert_eq!(response.to_device.events.len(), 1);
321 let event = &response.to_device.events[0];
322 assert_eq!(event.event_type, "m.room_key");
323 let content: RoomKeyContent = serde_json::from_value(event.content.clone()).expect("valid room_key content");
324 assert_eq!(content.session_id, "session-abc");
325 assert_eq!(content.room_id, RoomId::parse("!r:example.org").expect("valid room id"));
326 assert_eq!(response.device_one_time_keys_count.get("signed_curve25519"), Some(&42));
327 assert!(response.device_unused_fallback_key_types.is_empty());
328 }
329
330 #[test]
331 fn parses_invite_and_leave_room_sections() {
332 let body = serde_json::json!({
333 "next_batch": "s3",
334 "rooms": {
335 "invite": {
336 "!invited:example.org": {
337 "invite_state": {
338 "events": [
339 {
340 "type": "m.room.member",
341 "state_key": "@me:example.org",
342 "sender": "@bob:example.org",
343 "content": { "membership": "invite" }
344 }
345 ]
346 }
347 }
348 },
349 "leave": {
350 "!left:example.org": {
351 "timeline": { "events": [] },
352 "state": { "events": [] }
353 }
354 }
355 }
356 })
357 .to_string();
358
359 let response = parse_sync_response(body.as_bytes()).expect("valid sync response");
360 let invited_id = RoomId::parse("!invited:example.org").expect("valid room id");
361 let invited = response.rooms.invite.get(&invited_id).expect("invited room present");
362 assert_eq!(invited.invite_state.events.len(), 1);
363 assert_eq!(invited.invite_state.events[0].state_key, "@me:example.org");
364
365 let left_id = RoomId::parse("!left:example.org").expect("valid room id");
366 assert!(response.rooms.leave.contains_key(&left_id));
367 }
368
369 #[test]
370 fn ephemeral_and_account_data_events_use_the_basic_shape() {
371 let body = serde_json::json!({
372 "next_batch": "s4",
373 "rooms": {
374 "join": {
375 "!room:example.org": {
376 "ephemeral": {
377 "events": [
378 { "type": "m.typing", "content": { "user_ids": ["@alice:example.org"] } }
379 ]
380 },
381 "account_data": {
382 "events": [
383 { "type": "m.fully_read", "content": { "event_id": "$last:example.org" } }
384 ]
385 }
386 }
387 }
388 }
389 })
390 .to_string();
391
392 let response = parse_sync_response(body.as_bytes()).expect("valid sync response");
393 let room_id = RoomId::parse("!room:example.org").expect("valid room id");
394 let room = response.rooms.join.get(&room_id).expect("room present");
395 assert_eq!(room.ephemeral.events[0].event_type, "m.typing");
396 let fully_read: crate::wire::events::FullyReadContent =
397 serde_json::from_value(room.account_data.events[0].content.clone()).expect("valid fully_read content");
398 assert_eq!(fully_read.event_id, EventId::parse("$last:example.org").expect("valid event id"));
399 }
400}