Skip to main content

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, OwnedMxcUri, OwnedRoomId, OwnedUserId,
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    /// Collection of avatar changes that room member events trigger.
223    pub avatar_changes: Option<BTreeMap<OwnedUserId, Option<OwnedMxcUri>>>,
224}
225
226#[cfg(not(tarpaulin_include))]
227impl fmt::Debug for JoinedRoomUpdate {
228    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229        f.debug_struct("JoinedRoomUpdate")
230            .field("unread_notifications", &self.unread_notifications)
231            .field("timeline", &self.timeline)
232            .field("state", &self.state)
233            .field("account_data", &DebugListOfRawEventsNoId(&self.account_data))
234            .field("ephemeral", &self.ephemeral)
235            .field("ambiguity_changes", &self.ambiguity_changes)
236            .finish()
237    }
238}
239
240impl JoinedRoomUpdate {
241    pub(crate) fn new(
242        timeline: Timeline,
243        state: State,
244        account_data: Vec<Raw<AnyRoomAccountDataEvent>>,
245        ephemeral: Vec<Raw<AnySyncEphemeralRoomEvent>>,
246        unread_notifications: UnreadNotificationsCount,
247        ambiguity_changes: BTreeMap<OwnedEventId, AmbiguityChange>,
248        avatar_changes: Option<BTreeMap<OwnedUserId, Option<OwnedMxcUri>>>,
249    ) -> Self {
250        Self {
251            unread_notifications,
252            timeline,
253            state,
254            account_data,
255            ephemeral,
256            ambiguity_changes,
257            avatar_changes,
258        }
259    }
260}
261
262/// Counts of unread notifications for a room.
263#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
264pub struct UnreadNotificationsCount {
265    /// The number of unread notifications for this room with the highlight flag
266    /// set.
267    pub highlight_count: u64,
268    /// The total number of unread notifications for this room.
269    pub notification_count: u64,
270}
271
272impl From<RumaUnreadNotificationsCount> for UnreadNotificationsCount {
273    fn from(notifications: RumaUnreadNotificationsCount) -> Self {
274        Self {
275            highlight_count: notifications.highlight_count.map(|c| c.into()).unwrap_or(0),
276            notification_count: notifications.notification_count.map(|c| c.into()).unwrap_or(0),
277        }
278    }
279}
280
281/// Updates to left rooms.
282#[derive(Clone, Default)]
283pub struct LeftRoomUpdate {
284    /// The timeline of messages and state changes in the room up to the point
285    /// when the user left.
286    pub timeline: Timeline,
287    /// Updates to the state.
288    ///
289    /// If `since` is missing or `full_state` is true, the start point of the
290    /// update is the beginning of the timeline. Otherwise, the start point
291    /// is the time specified in `since`.
292    ///
293    /// If `state_after` was used, the end point of the update is the end of the
294    /// `timeline`. Otherwise, the end point of these updates is the start of
295    /// the `timeline`, and to calculate room state we must scan the `timeline`
296    /// for state events as well as using this information in this property.
297    pub state: State,
298    /// The private data that this user has attached to this room.
299    pub account_data: Vec<Raw<AnyRoomAccountDataEvent>>,
300    /// Collection of ambiguity changes that room member events trigger.
301    ///
302    /// This is a map of event ID of the `m.room.member` event to the
303    /// details of the ambiguity change.
304    pub ambiguity_changes: BTreeMap<OwnedEventId, AmbiguityChange>,
305}
306
307impl LeftRoomUpdate {
308    pub(crate) fn new(
309        timeline: Timeline,
310        state: State,
311        account_data: Vec<Raw<AnyRoomAccountDataEvent>>,
312        ambiguity_changes: BTreeMap<OwnedEventId, AmbiguityChange>,
313    ) -> Self {
314        Self { timeline, state, account_data, ambiguity_changes }
315    }
316}
317
318#[cfg(not(tarpaulin_include))]
319impl fmt::Debug for LeftRoomUpdate {
320    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
321        f.debug_struct("LeftRoomUpdate")
322            .field("timeline", &self.timeline)
323            .field("state", &self.state)
324            .field("account_data", &DebugListOfRawEventsNoId(&self.account_data))
325            .field("ambiguity_changes", &self.ambiguity_changes)
326            .finish()
327    }
328}
329
330/// Events in the room.
331#[derive(Clone, Debug, Default)]
332pub struct Timeline {
333    /// True if the number of events returned was limited by the `limit` on the
334    /// filter.
335    pub limited: bool,
336
337    /// A token that can be supplied to to the `from` parameter of the
338    /// `/rooms/{roomId}/messages` endpoint.
339    pub prev_batch: Option<String>,
340
341    /// A list of events.
342    pub events: Vec<TimelineEvent>,
343}
344
345impl Timeline {
346    pub(crate) fn new(limited: bool, prev_batch: Option<String>) -> Self {
347        Self { limited, prev_batch, ..Default::default() }
348    }
349}
350
351/// State changes in the room.
352#[derive(Clone)]
353pub enum State {
354    /// The state changes between the previous sync and the start of the
355    /// timeline.
356    ///
357    /// To get the full list of state changes since the previous sync, the state
358    /// events in [`Timeline`] must be added to these events to update the local
359    /// state.
360    Before(Vec<Raw<AnySyncStateEvent>>),
361
362    /// The state changes between the previous sync and the end of the timeline.
363    ///
364    /// This contains the full list of state changes since the previous sync.
365    /// State events in [`Timeline`] must be ignored to update the local state.
366    After(Vec<Raw<AnySyncStateEvent>>),
367}
368
369impl Default for State {
370    fn default() -> Self {
371        Self::Before(vec![])
372    }
373}
374
375#[cfg(not(tarpaulin_include))]
376impl fmt::Debug for State {
377    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
378        match self {
379            Self::Before(events) => {
380                f.debug_tuple("Before").field(&DebugListOfRawEvents(events)).finish()
381            }
382            Self::After(events) => {
383                f.debug_tuple("After").field(&DebugListOfRawEvents(events)).finish()
384            }
385        }
386    }
387}
388
389struct DebugInvitedRoomUpdates<'a>(&'a BTreeMap<OwnedRoomId, InvitedRoomUpdate>);
390
391#[cfg(not(tarpaulin_include))]
392impl fmt::Debug for DebugInvitedRoomUpdates<'_> {
393    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
394        f.debug_map().entries(self.0.iter().map(|(k, v)| (k, DebugInvitedRoom(v)))).finish()
395    }
396}
397
398struct DebugKnockedRoomUpdates<'a>(&'a BTreeMap<OwnedRoomId, KnockedRoomUpdate>);
399
400#[cfg(not(tarpaulin_include))]
401impl fmt::Debug for DebugKnockedRoomUpdates<'_> {
402    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
403        f.debug_map().entries(self.0.iter().map(|(k, v)| (k, DebugKnockedRoom(v)))).finish()
404    }
405}
406
407/// A notification triggered by a sync response.
408#[derive(Clone)]
409pub struct Notification {
410    /// The actions to perform when the conditions for this rule are met.
411    pub actions: Vec<Action>,
412
413    /// The event that triggered the notification.
414    pub event: RawAnySyncOrStrippedTimelineEvent,
415}
416
417#[cfg(not(tarpaulin_include))]
418impl fmt::Debug for Notification {
419    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
420        let event_debug = match &self.event {
421            RawAnySyncOrStrippedTimelineEvent::Sync(ev) => DebugRawEvent(ev),
422            RawAnySyncOrStrippedTimelineEvent::Stripped(ev) => {
423                DebugRawEvent(ev.cast_ref_unchecked())
424            }
425        };
426
427        f.debug_struct("Notification")
428            .field("actions", &self.actions)
429            .field("event", &event_debug)
430            .finish()
431    }
432}