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(crate) 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
102#[cfg(test)]
103mod tests {
104    use std::collections::BTreeMap;
105
106    use assert_matches::assert_matches;
107    use ruma::room_id;
108
109    use super::{
110        InvitedRoomUpdate, JoinedRoomUpdate, KnockedRoomUpdate, LeftRoomUpdate, RoomUpdates,
111    };
112
113    #[test]
114    fn test_room_updates_iter_all_room_ids() {
115        let room_id_0 = room_id!("!r0");
116        let room_id_1 = room_id!("!r1");
117        let room_id_2 = room_id!("!r2");
118        let room_id_3 = room_id!("!r3");
119        let room_id_4 = room_id!("!r4");
120        let room_id_5 = room_id!("!r5");
121        let room_id_6 = room_id!("!r6");
122        let room_id_7 = room_id!("!r7");
123        let room_updates = RoomUpdates {
124            left: {
125                let mut left = BTreeMap::new();
126                left.insert(room_id_0.to_owned(), LeftRoomUpdate::default());
127                left.insert(room_id_1.to_owned(), LeftRoomUpdate::default());
128                left
129            },
130            joined: {
131                let mut joined = BTreeMap::new();
132                joined.insert(room_id_2.to_owned(), JoinedRoomUpdate::default());
133                joined.insert(room_id_3.to_owned(), JoinedRoomUpdate::default());
134                joined
135            },
136            invited: {
137                let mut invited = BTreeMap::new();
138                invited.insert(room_id_4.to_owned(), InvitedRoomUpdate::default());
139                invited.insert(room_id_5.to_owned(), InvitedRoomUpdate::default());
140                invited
141            },
142            knocked: {
143                let mut knocked = BTreeMap::new();
144                knocked.insert(room_id_6.to_owned(), KnockedRoomUpdate::default());
145                knocked.insert(room_id_7.to_owned(), KnockedRoomUpdate::default());
146                knocked
147            },
148        };
149
150        let mut iter = room_updates.iter_all_room_ids();
151        assert_matches!(iter.next(), Some(room_id) => assert_eq!(room_id, room_id_0));
152        assert_matches!(iter.next(), Some(room_id) => assert_eq!(room_id, room_id_1));
153        assert_matches!(iter.next(), Some(room_id) => assert_eq!(room_id, room_id_2));
154        assert_matches!(iter.next(), Some(room_id) => assert_eq!(room_id, room_id_3));
155        assert_matches!(iter.next(), Some(room_id) => assert_eq!(room_id, room_id_4));
156        assert_matches!(iter.next(), Some(room_id) => assert_eq!(room_id, room_id_5));
157        assert_matches!(iter.next(), Some(room_id) => assert_eq!(room_id, room_id_6));
158        assert_matches!(iter.next(), Some(room_id) => assert_eq!(room_id, room_id_7));
159        assert!(iter.next().is_none());
160    }
161}
162
163#[cfg(not(tarpaulin_include))]
164impl fmt::Debug for RoomUpdates {
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166        f.debug_struct("RoomUpdates")
167            .field("left", &self.left)
168            .field("joined", &self.joined)
169            .field("invited", &DebugInvitedRoomUpdates(&self.invited))
170            .field("knocked", &DebugKnockedRoomUpdates(&self.knocked))
171            .finish()
172    }
173}
174
175/// Updates to joined rooms.
176#[derive(Clone, Default)]
177pub struct JoinedRoomUpdate {
178    /// Counts of unread notifications for this room.
179    pub unread_notifications: UnreadNotificationsCount,
180    /// The timeline of messages and state changes in the room.
181    pub timeline: Timeline,
182    /// Updates to the state.
183    ///
184    /// If `since` is missing or `full_state` is true, the start point of the
185    /// update is the beginning of the timeline. Otherwise, the start point
186    /// is the time specified in `since`.
187    ///
188    /// If `state_after` was used, the end point of the update is the end of the
189    /// `timeline`. Otherwise, the end point of these updates is the start of
190    /// the `timeline`, and to calculate room state we must scan the `timeline`
191    /// for state events as well as using this information in this property.
192    pub state: State,
193    /// The private data that this user has attached to this room.
194    pub account_data: Vec<Raw<AnyRoomAccountDataEvent>>,
195    /// The ephemeral events in the room that aren't recorded in the timeline or
196    /// state of the room. e.g. typing.
197    pub ephemeral: Vec<Raw<AnySyncEphemeralRoomEvent>>,
198    /// Collection of ambiguity changes that room member events trigger.
199    ///
200    /// This is a map of event ID of the `m.room.member` event to the
201    /// details of the ambiguity change.
202    pub ambiguity_changes: BTreeMap<OwnedEventId, AmbiguityChange>,
203}
204
205#[cfg(not(tarpaulin_include))]
206impl fmt::Debug for JoinedRoomUpdate {
207    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208        f.debug_struct("JoinedRoomUpdate")
209            .field("unread_notifications", &self.unread_notifications)
210            .field("timeline", &self.timeline)
211            .field("state", &self.state)
212            .field("account_data", &DebugListOfRawEventsNoId(&self.account_data))
213            .field("ephemeral", &self.ephemeral)
214            .field("ambiguity_changes", &self.ambiguity_changes)
215            .finish()
216    }
217}
218
219impl JoinedRoomUpdate {
220    pub(crate) fn new(
221        timeline: Timeline,
222        state: State,
223        account_data: Vec<Raw<AnyRoomAccountDataEvent>>,
224        ephemeral: Vec<Raw<AnySyncEphemeralRoomEvent>>,
225        unread_notifications: UnreadNotificationsCount,
226        ambiguity_changes: BTreeMap<OwnedEventId, AmbiguityChange>,
227    ) -> Self {
228        Self { unread_notifications, timeline, state, account_data, ephemeral, ambiguity_changes }
229    }
230}
231
232/// Counts of unread notifications for a room.
233#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
234pub struct UnreadNotificationsCount {
235    /// The number of unread notifications for this room with the highlight flag
236    /// set.
237    pub highlight_count: u64,
238    /// The total number of unread notifications for this room.
239    pub notification_count: u64,
240}
241
242impl From<RumaUnreadNotificationsCount> for UnreadNotificationsCount {
243    fn from(notifications: RumaUnreadNotificationsCount) -> Self {
244        Self {
245            highlight_count: notifications.highlight_count.map(|c| c.into()).unwrap_or(0),
246            notification_count: notifications.notification_count.map(|c| c.into()).unwrap_or(0),
247        }
248    }
249}
250
251/// Updates to left rooms.
252#[derive(Clone, Default)]
253pub struct LeftRoomUpdate {
254    /// The timeline of messages and state changes in the room up to the point
255    /// when the user left.
256    pub timeline: Timeline,
257    /// Updates to the state.
258    ///
259    /// If `since` is missing or `full_state` is true, the start point of the
260    /// update is the beginning of the timeline. Otherwise, the start point
261    /// is the time specified in `since`.
262    ///
263    /// If `state_after` was used, the end point of the update is the end of the
264    /// `timeline`. Otherwise, the end point of these updates is the start of
265    /// the `timeline`, and to calculate room state we must scan the `timeline`
266    /// for state events as well as using this information in this property.
267    pub state: State,
268    /// The private data that this user has attached to this room.
269    pub account_data: Vec<Raw<AnyRoomAccountDataEvent>>,
270    /// Collection of ambiguity changes that room member events trigger.
271    ///
272    /// This is a map of event ID of the `m.room.member` event to the
273    /// details of the ambiguity change.
274    pub ambiguity_changes: BTreeMap<OwnedEventId, AmbiguityChange>,
275}
276
277impl LeftRoomUpdate {
278    pub(crate) fn new(
279        timeline: Timeline,
280        state: State,
281        account_data: Vec<Raw<AnyRoomAccountDataEvent>>,
282        ambiguity_changes: BTreeMap<OwnedEventId, AmbiguityChange>,
283    ) -> Self {
284        Self { timeline, state, account_data, ambiguity_changes }
285    }
286}
287
288#[cfg(not(tarpaulin_include))]
289impl fmt::Debug for LeftRoomUpdate {
290    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291        f.debug_struct("LeftRoomUpdate")
292            .field("timeline", &self.timeline)
293            .field("state", &self.state)
294            .field("account_data", &DebugListOfRawEventsNoId(&self.account_data))
295            .field("ambiguity_changes", &self.ambiguity_changes)
296            .finish()
297    }
298}
299
300/// Events in the room.
301#[derive(Clone, Debug, Default)]
302pub struct Timeline {
303    /// True if the number of events returned was limited by the `limit` on the
304    /// filter.
305    pub limited: bool,
306
307    /// A token that can be supplied to to the `from` parameter of the
308    /// `/rooms/{roomId}/messages` endpoint.
309    pub prev_batch: Option<String>,
310
311    /// A list of events.
312    pub events: Vec<TimelineEvent>,
313}
314
315impl Timeline {
316    pub(crate) fn new(limited: bool, prev_batch: Option<String>) -> Self {
317        Self { limited, prev_batch, ..Default::default() }
318    }
319}
320
321/// State changes in the room.
322#[derive(Clone)]
323pub enum State {
324    /// The state changes between the previous sync and the start of the
325    /// timeline.
326    ///
327    /// To get the full list of state changes since the previous sync, the state
328    /// events in [`Timeline`] must be added to these events to update the local
329    /// state.
330    Before(Vec<Raw<AnySyncStateEvent>>),
331
332    /// The state changes between the previous sync and the end of the timeline.
333    ///
334    /// This contains the full list of state changes since the previous sync.
335    /// State events in [`Timeline`] must be ignored to update the local state.
336    After(Vec<Raw<AnySyncStateEvent>>),
337}
338
339impl Default for State {
340    fn default() -> Self {
341        Self::Before(vec![])
342    }
343}
344
345#[cfg(not(tarpaulin_include))]
346impl fmt::Debug for State {
347    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
348        match self {
349            Self::Before(events) => {
350                f.debug_tuple("Before").field(&DebugListOfRawEvents(events)).finish()
351            }
352            Self::After(events) => {
353                f.debug_tuple("After").field(&DebugListOfRawEvents(events)).finish()
354            }
355        }
356    }
357}
358
359struct DebugInvitedRoomUpdates<'a>(&'a BTreeMap<OwnedRoomId, InvitedRoomUpdate>);
360
361#[cfg(not(tarpaulin_include))]
362impl fmt::Debug for DebugInvitedRoomUpdates<'_> {
363    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
364        f.debug_map().entries(self.0.iter().map(|(k, v)| (k, DebugInvitedRoom(v)))).finish()
365    }
366}
367
368struct DebugKnockedRoomUpdates<'a>(&'a BTreeMap<OwnedRoomId, KnockedRoomUpdate>);
369
370#[cfg(not(tarpaulin_include))]
371impl fmt::Debug for DebugKnockedRoomUpdates<'_> {
372    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
373        f.debug_map().entries(self.0.iter().map(|(k, v)| (k, DebugKnockedRoom(v)))).finish()
374    }
375}
376
377/// A notification triggered by a sync response.
378#[derive(Clone)]
379pub struct Notification {
380    /// The actions to perform when the conditions for this rule are met.
381    pub actions: Vec<Action>,
382
383    /// The event that triggered the notification.
384    pub event: RawAnySyncOrStrippedTimelineEvent,
385}
386
387#[cfg(not(tarpaulin_include))]
388impl fmt::Debug for Notification {
389    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390        let event_debug = match &self.event {
391            RawAnySyncOrStrippedTimelineEvent::Sync(ev) => DebugRawEvent(ev),
392            RawAnySyncOrStrippedTimelineEvent::Stripped(ev) => {
393                DebugRawEvent(ev.cast_ref_unchecked())
394            }
395        };
396
397        f.debug_struct("Notification")
398            .field("actions", &self.actions)
399            .field("event", &event_debug)
400            .finish()
401    }
402}