matrix_sdk_base/
sync.rs

1// Copyright 2022 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! The SDK's representation of the result of a `/sync` request.
16
17use std::{collections::BTreeMap, fmt};
18
19use matrix_sdk_common::{
20    debug::DebugRawEvent,
21    deserialized_responses::{ProcessedToDeviceEvent, TimelineEvent},
22};
23pub use ruma::api::client::sync::sync_events::v3::{
24    InvitedRoom as InvitedRoomUpdate, KnockedRoom as KnockedRoomUpdate,
25};
26use ruma::{
27    OwnedEventId, OwnedRoomId,
28    api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount,
29    events::{
30        AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnySyncEphemeralRoomEvent,
31        AnySyncStateEvent, presence::PresenceEvent,
32    },
33    push::Action,
34    serde::Raw,
35};
36use serde::{Deserialize, Serialize};
37
38use crate::{
39    debug::{
40        DebugInvitedRoom, DebugKnockedRoom, DebugListOfProcessedToDeviceEvents,
41        DebugListOfRawEvents, DebugListOfRawEventsNoId,
42    },
43    deserialized_responses::{AmbiguityChange, RawAnySyncOrStrippedTimelineEvent},
44};
45
46/// Generalized representation of a `/sync` response.
47///
48/// This type is intended to be applicable regardless of the endpoint used for
49/// syncing.
50#[derive(Clone, Default)]
51pub struct SyncResponse {
52    /// Updates to rooms.
53    pub rooms: RoomUpdates,
54    /// Updates to the presence status of other users.
55    pub presence: Vec<Raw<PresenceEvent>>,
56    /// The global private data created by this user.
57    pub account_data: Vec<Raw<AnyGlobalAccountDataEvent>>,
58    /// Messages sent directly between devices.
59    pub to_device: Vec<ProcessedToDeviceEvent>,
60    /// New notifications per room.
61    pub notifications: BTreeMap<OwnedRoomId, Vec<Notification>>,
62}
63
64#[cfg(not(tarpaulin_include))]
65impl fmt::Debug for SyncResponse {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        f.debug_struct("SyncResponse")
68            .field("rooms", &self.rooms)
69            .field("account_data", &DebugListOfRawEventsNoId(&self.account_data))
70            .field("to_device", &DebugListOfProcessedToDeviceEvents(&self.to_device))
71            .field("notifications", &self.notifications)
72            .finish_non_exhaustive()
73    }
74}
75
76/// Updates to rooms in a [`SyncResponse`].
77#[derive(Clone, Default)]
78pub struct RoomUpdates {
79    /// The rooms that the user has left or been banned from.
80    pub left: BTreeMap<OwnedRoomId, LeftRoomUpdate>,
81    /// The rooms that the user has joined.
82    pub joined: BTreeMap<OwnedRoomId, JoinedRoomUpdate>,
83    /// The rooms that the user has been invited to.
84    pub invited: BTreeMap<OwnedRoomId, InvitedRoomUpdate>,
85    /// The rooms that the user has knocked on.
86    pub knocked: BTreeMap<OwnedRoomId, KnockedRoomUpdate>,
87}
88
89impl RoomUpdates {
90    /// Iterate over all room IDs, from [`RoomUpdates::left`],
91    /// [`RoomUpdates::joined`], [`RoomUpdates::invited`] and
92    /// [`RoomUpdates::knocked`].
93    pub fn iter_all_room_ids(&self) -> impl Iterator<Item = &OwnedRoomId> {
94        self.left
95            .keys()
96            .chain(self.joined.keys())
97            .chain(self.invited.keys())
98            .chain(self.knocked.keys())
99    }
100
101    /// Returns whether or not this update contains any changes to the list
102    /// of invited, joined, knocked or left rooms.
103    pub fn is_empty(&self) -> bool {
104        self.invited.is_empty()
105            && self.joined.is_empty()
106            && self.knocked.is_empty()
107            && self.left.is_empty()
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use std::collections::BTreeMap;
114
115    use assert_matches::assert_matches;
116    use ruma::room_id;
117
118    use super::{
119        InvitedRoomUpdate, JoinedRoomUpdate, KnockedRoomUpdate, LeftRoomUpdate, RoomUpdates,
120    };
121
122    #[test]
123    fn test_room_updates_iter_all_room_ids() {
124        let room_id_0 = room_id!("!r0");
125        let room_id_1 = room_id!("!r1");
126        let room_id_2 = room_id!("!r2");
127        let room_id_3 = room_id!("!r3");
128        let room_id_4 = room_id!("!r4");
129        let room_id_5 = room_id!("!r5");
130        let room_id_6 = room_id!("!r6");
131        let room_id_7 = room_id!("!r7");
132        let room_updates = RoomUpdates {
133            left: {
134                let mut left = BTreeMap::new();
135                left.insert(room_id_0.to_owned(), LeftRoomUpdate::default());
136                left.insert(room_id_1.to_owned(), LeftRoomUpdate::default());
137                left
138            },
139            joined: {
140                let mut joined = BTreeMap::new();
141                joined.insert(room_id_2.to_owned(), JoinedRoomUpdate::default());
142                joined.insert(room_id_3.to_owned(), JoinedRoomUpdate::default());
143                joined
144            },
145            invited: {
146                let mut invited = BTreeMap::new();
147                invited.insert(room_id_4.to_owned(), InvitedRoomUpdate::default());
148                invited.insert(room_id_5.to_owned(), InvitedRoomUpdate::default());
149                invited
150            },
151            knocked: {
152                let mut knocked = BTreeMap::new();
153                knocked.insert(room_id_6.to_owned(), KnockedRoomUpdate::default());
154                knocked.insert(room_id_7.to_owned(), KnockedRoomUpdate::default());
155                knocked
156            },
157        };
158
159        let mut iter = room_updates.iter_all_room_ids();
160        assert_matches!(iter.next(), Some(room_id) => assert_eq!(room_id, room_id_0));
161        assert_matches!(iter.next(), Some(room_id) => assert_eq!(room_id, room_id_1));
162        assert_matches!(iter.next(), Some(room_id) => assert_eq!(room_id, room_id_2));
163        assert_matches!(iter.next(), Some(room_id) => assert_eq!(room_id, room_id_3));
164        assert_matches!(iter.next(), Some(room_id) => assert_eq!(room_id, room_id_4));
165        assert_matches!(iter.next(), Some(room_id) => assert_eq!(room_id, room_id_5));
166        assert_matches!(iter.next(), Some(room_id) => assert_eq!(room_id, room_id_6));
167        assert_matches!(iter.next(), Some(room_id) => assert_eq!(room_id, room_id_7));
168        assert!(iter.next().is_none());
169    }
170
171    #[test]
172    fn test_empty_room_updates() {
173        let room_updates = RoomUpdates::default();
174
175        let mut iter = room_updates.iter_all_room_ids();
176        assert!(iter.next().is_none());
177
178        assert!(room_updates.is_empty());
179    }
180}
181
182#[cfg(not(tarpaulin_include))]
183impl fmt::Debug for RoomUpdates {
184    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185        f.debug_struct("RoomUpdates")
186            .field("left", &self.left)
187            .field("joined", &self.joined)
188            .field("invited", &DebugInvitedRoomUpdates(&self.invited))
189            .field("knocked", &DebugKnockedRoomUpdates(&self.knocked))
190            .finish()
191    }
192}
193
194/// Updates to joined rooms.
195#[derive(Clone, Default)]
196pub struct JoinedRoomUpdate {
197    /// Counts of unread notifications for this room.
198    pub unread_notifications: UnreadNotificationsCount,
199    /// The timeline of messages and state changes in the room.
200    pub timeline: Timeline,
201    /// Updates to the state.
202    ///
203    /// If `since` is missing or `full_state` is true, the start point of the
204    /// update is the beginning of the timeline. Otherwise, the start point
205    /// is the time specified in `since`.
206    ///
207    /// If `state_after` was used, the end point of the update is the end of the
208    /// `timeline`. Otherwise, the end point of these updates is the start of
209    /// the `timeline`, and to calculate room state we must scan the `timeline`
210    /// for state events as well as using this information in this property.
211    pub state: State,
212    /// The private data that this user has attached to this room.
213    pub account_data: Vec<Raw<AnyRoomAccountDataEvent>>,
214    /// The ephemeral events in the room that aren't recorded in the timeline or
215    /// state of the room. e.g. typing.
216    pub ephemeral: Vec<Raw<AnySyncEphemeralRoomEvent>>,
217    /// Collection of ambiguity changes that room member events trigger.
218    ///
219    /// This is a map of event ID of the `m.room.member` event to the
220    /// details of the ambiguity change.
221    pub ambiguity_changes: BTreeMap<OwnedEventId, AmbiguityChange>,
222}
223
224#[cfg(not(tarpaulin_include))]
225impl fmt::Debug for JoinedRoomUpdate {
226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227        f.debug_struct("JoinedRoomUpdate")
228            .field("unread_notifications", &self.unread_notifications)
229            .field("timeline", &self.timeline)
230            .field("state", &self.state)
231            .field("account_data", &DebugListOfRawEventsNoId(&self.account_data))
232            .field("ephemeral", &self.ephemeral)
233            .field("ambiguity_changes", &self.ambiguity_changes)
234            .finish()
235    }
236}
237
238impl JoinedRoomUpdate {
239    pub(crate) fn new(
240        timeline: Timeline,
241        state: State,
242        account_data: Vec<Raw<AnyRoomAccountDataEvent>>,
243        ephemeral: Vec<Raw<AnySyncEphemeralRoomEvent>>,
244        unread_notifications: UnreadNotificationsCount,
245        ambiguity_changes: BTreeMap<OwnedEventId, AmbiguityChange>,
246    ) -> Self {
247        Self { unread_notifications, timeline, state, account_data, ephemeral, ambiguity_changes }
248    }
249}
250
251/// Counts of unread notifications for a room.
252#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
253pub struct UnreadNotificationsCount {
254    /// The number of unread notifications for this room with the highlight flag
255    /// set.
256    pub highlight_count: u64,
257    /// The total number of unread notifications for this room.
258    pub notification_count: u64,
259}
260
261impl From<RumaUnreadNotificationsCount> for UnreadNotificationsCount {
262    fn from(notifications: RumaUnreadNotificationsCount) -> Self {
263        Self {
264            highlight_count: notifications.highlight_count.map(|c| c.into()).unwrap_or(0),
265            notification_count: notifications.notification_count.map(|c| c.into()).unwrap_or(0),
266        }
267    }
268}
269
270/// Updates to left rooms.
271#[derive(Clone, Default)]
272pub struct LeftRoomUpdate {
273    /// The timeline of messages and state changes in the room up to the point
274    /// when the user left.
275    pub timeline: Timeline,
276    /// Updates to the state.
277    ///
278    /// If `since` is missing or `full_state` is true, the start point of the
279    /// update is the beginning of the timeline. Otherwise, the start point
280    /// is the time specified in `since`.
281    ///
282    /// If `state_after` was used, the end point of the update is the end of the
283    /// `timeline`. Otherwise, the end point of these updates is the start of
284    /// the `timeline`, and to calculate room state we must scan the `timeline`
285    /// for state events as well as using this information in this property.
286    pub state: State,
287    /// The private data that this user has attached to this room.
288    pub account_data: Vec<Raw<AnyRoomAccountDataEvent>>,
289    /// Collection of ambiguity changes that room member events trigger.
290    ///
291    /// This is a map of event ID of the `m.room.member` event to the
292    /// details of the ambiguity change.
293    pub ambiguity_changes: BTreeMap<OwnedEventId, AmbiguityChange>,
294}
295
296impl LeftRoomUpdate {
297    pub(crate) fn new(
298        timeline: Timeline,
299        state: State,
300        account_data: Vec<Raw<AnyRoomAccountDataEvent>>,
301        ambiguity_changes: BTreeMap<OwnedEventId, AmbiguityChange>,
302    ) -> Self {
303        Self { timeline, state, account_data, ambiguity_changes }
304    }
305}
306
307#[cfg(not(tarpaulin_include))]
308impl fmt::Debug for LeftRoomUpdate {
309    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310        f.debug_struct("LeftRoomUpdate")
311            .field("timeline", &self.timeline)
312            .field("state", &self.state)
313            .field("account_data", &DebugListOfRawEventsNoId(&self.account_data))
314            .field("ambiguity_changes", &self.ambiguity_changes)
315            .finish()
316    }
317}
318
319/// Events in the room.
320#[derive(Clone, Debug, Default)]
321pub struct Timeline {
322    /// True if the number of events returned was limited by the `limit` on the
323    /// filter.
324    pub limited: bool,
325
326    /// A token that can be supplied to to the `from` parameter of the
327    /// `/rooms/{roomId}/messages` endpoint.
328    pub prev_batch: Option<String>,
329
330    /// A list of events.
331    pub events: Vec<TimelineEvent>,
332}
333
334impl Timeline {
335    pub(crate) fn new(limited: bool, prev_batch: Option<String>) -> Self {
336        Self { limited, prev_batch, ..Default::default() }
337    }
338}
339
340/// State changes in the room.
341#[derive(Clone)]
342pub enum State {
343    /// The state changes between the previous sync and the start of the
344    /// timeline.
345    ///
346    /// To get the full list of state changes since the previous sync, the state
347    /// events in [`Timeline`] must be added to these events to update the local
348    /// state.
349    Before(Vec<Raw<AnySyncStateEvent>>),
350
351    /// The state changes between the previous sync and the end of the timeline.
352    ///
353    /// This contains the full list of state changes since the previous sync.
354    /// State events in [`Timeline`] must be ignored to update the local state.
355    After(Vec<Raw<AnySyncStateEvent>>),
356}
357
358impl Default for State {
359    fn default() -> Self {
360        Self::Before(vec![])
361    }
362}
363
364#[cfg(not(tarpaulin_include))]
365impl fmt::Debug for State {
366    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
367        match self {
368            Self::Before(events) => {
369                f.debug_tuple("Before").field(&DebugListOfRawEvents(events)).finish()
370            }
371            Self::After(events) => {
372                f.debug_tuple("After").field(&DebugListOfRawEvents(events)).finish()
373            }
374        }
375    }
376}
377
378struct DebugInvitedRoomUpdates<'a>(&'a BTreeMap<OwnedRoomId, InvitedRoomUpdate>);
379
380#[cfg(not(tarpaulin_include))]
381impl fmt::Debug for DebugInvitedRoomUpdates<'_> {
382    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
383        f.debug_map().entries(self.0.iter().map(|(k, v)| (k, DebugInvitedRoom(v)))).finish()
384    }
385}
386
387struct DebugKnockedRoomUpdates<'a>(&'a BTreeMap<OwnedRoomId, KnockedRoomUpdate>);
388
389#[cfg(not(tarpaulin_include))]
390impl fmt::Debug for DebugKnockedRoomUpdates<'_> {
391    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
392        f.debug_map().entries(self.0.iter().map(|(k, v)| (k, DebugKnockedRoom(v)))).finish()
393    }
394}
395
396/// A notification triggered by a sync response.
397#[derive(Clone)]
398pub struct Notification {
399    /// The actions to perform when the conditions for this rule are met.
400    pub actions: Vec<Action>,
401
402    /// The event that triggered the notification.
403    pub event: RawAnySyncOrStrippedTimelineEvent,
404}
405
406#[cfg(not(tarpaulin_include))]
407impl fmt::Debug for Notification {
408    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
409        let event_debug = match &self.event {
410            RawAnySyncOrStrippedTimelineEvent::Sync(ev) => DebugRawEvent(ev),
411            RawAnySyncOrStrippedTimelineEvent::Stripped(ev) => {
412                DebugRawEvent(ev.cast_ref_unchecked())
413            }
414        };
415
416        f.debug_struct("Notification")
417            .field("actions", &self.actions)
418            .field("event", &event_debug)
419            .finish()
420    }
421}