Skip to main content

matrix_sdk_ui/
notification_client.rs

1// Copyright 2023 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 that specific language governing permissions and
13// limitations under the License.
14
15use std::{
16    collections::BTreeMap,
17    ops::Deref,
18    sync::{Arc, Mutex},
19    time::Duration,
20};
21
22use futures_util::{StreamExt as _, pin_mut};
23use itertools::Itertools;
24use matrix_sdk::{
25    Client, ClientBuildError, SlidingSyncList, SlidingSyncMode,
26    room::{PushContext, Room},
27};
28use matrix_sdk_base::{RoomState, StoreError, deserialized_responses::TimelineEvent};
29use matrix_sdk_common::{cross_process_lock::CrossProcessLockConfig, timeout::timeout};
30use ruma::{
31    EventId, OwnedEventId, OwnedRoomId, RoomId, UserId,
32    api::client::sync::sync_events::v5 as http,
33    assign,
34    events::{
35        AnyMessageLikeEventContent, AnyStateEvent, AnyStateEventContentChange,
36        AnySyncMessageLikeEvent, AnySyncTimelineEvent, StateEventContentChange, StateEventType,
37        TimelineEventType,
38        room::{
39            encrypted::OriginalSyncRoomEncryptedEvent,
40            join_rules::JoinRule,
41            member::{MembershipState, StrippedRoomMemberEvent},
42            message::{Relation, SyncRoomMessageEvent},
43        },
44    },
45    html::RemoveReplyFallback,
46    push::Action,
47    serde::Raw,
48    time::Instant,
49    uint,
50};
51use thiserror::Error;
52use tokio::sync::Mutex as AsyncMutex;
53use tracing::{debug, info, instrument, trace, warn};
54
55use crate::{
56    DEFAULT_SANITIZER_MODE,
57    encryption_sync_service::{EncryptionSyncPermit, EncryptionSyncService},
58    sync_service::SyncService,
59};
60
61/// What kind of process setup do we have for this notification client?
62#[derive(Clone)]
63pub enum NotificationProcessSetup {
64    /// The notification client may run on a separate process than the rest of
65    /// the app.
66    ///
67    /// For instance, this is the case on iOS, where notifications are handled
68    /// in a separate process (the Notification Service Extension, aka NSE).
69    ///
70    /// In that case, a cross-process lock will be used to coordinate writes
71    /// into the stores handled by the SDK.
72    MultipleProcesses,
73
74    /// The notification client runs in the same process as the rest of the
75    /// `Client` performing syncs.
76    ///
77    /// For instance, this is the case on Android, where a notification will
78    /// wake up the main app process.
79    ///
80    /// In that case, a smart reference to the [`SyncService`] must be provided.
81    SingleProcess { sync_service: Arc<SyncService> },
82}
83
84/// A client specialized for handling push notifications received over the
85/// network, for an app.
86///
87/// In particular, it takes care of running a full decryption sync, in case the
88/// event in the notification was impossible to decrypt beforehand.
89pub struct NotificationClient {
90    /// SDK client that uses an in-memory state store.
91    client: Client,
92
93    /// SDK client that uses the same state store as the caller's context.
94    parent_client: Client,
95
96    /// Is the notification client running on its own process or not?
97    process_setup: NotificationProcessSetup,
98
99    /// A mutex to serialize requests to the notifications sliding sync.
100    ///
101    /// If several notifications come in at the same time (e.g. network was
102    /// unreachable because of airplane mode or something similar), then we
103    /// need to make sure that repeated calls to `get_notification` won't
104    /// cause multiple requests with the same `conn_id` we're using for
105    /// notifications. This mutex solves this by sequentializing the requests.
106    notification_sync_mutex: AsyncMutex<()>,
107
108    /// A mutex to serialize requests to the encryption sliding sync that's used
109    /// in case we didn't have the keys to decipher an event.
110    ///
111    /// Same reasoning as [`Self::notification_sync_mutex`].
112    encryption_sync_mutex: AsyncMutex<()>,
113}
114
115impl NotificationClient {
116    const CONNECTION_ID: &'static str = "notifications";
117    const LOCK_ID: &'static str = "notifications";
118
119    /// Maximum time to wait for the main encryption sync to receive a
120    /// missing room key, in a [`NotificationProcessSetup::SingleProcess`]
121    /// setup where that sync is already running.
122    ///
123    /// The value matches the time budget of the other path, where no encryption
124    /// sync is running and the notification client runs one itself, using two
125    /// sync iterations of the sync, each of which long-polls for up to
126    /// 3 seconds.
127    const RUNNING_SYNC_DECRYPTION_DEADLINE: Duration = Duration::from_secs(6);
128
129    /// Create a new notification client.
130    pub async fn new(
131        parent_client: Client,
132        process_setup: NotificationProcessSetup,
133    ) -> Result<Self, Error> {
134        // Only create the lock id if cross process lock is needed (multiple processes)
135        let cross_process_store_config = match process_setup {
136            NotificationProcessSetup::MultipleProcesses => {
137                CrossProcessLockConfig::multi_process(Self::LOCK_ID)
138            }
139            NotificationProcessSetup::SingleProcess { .. } => CrossProcessLockConfig::SingleProcess,
140        };
141        let client = parent_client.notification_client(cross_process_store_config).await?;
142
143        Ok(NotificationClient {
144            client,
145            parent_client,
146            notification_sync_mutex: AsyncMutex::new(()),
147            encryption_sync_mutex: AsyncMutex::new(()),
148            process_setup,
149        })
150    }
151
152    /// Fetches a room by its ID using the in-memory state store backed client.
153    /// Useful to retrieve room information after running the limited
154    /// notification client sliding sync loop.
155    pub fn get_room(&self, room_id: &RoomId) -> Option<Room> {
156        self.client.get_room(room_id)
157    }
158
159    /// Fetches the content of a notification.
160    ///
161    /// This will first try to get the notification using a short-lived sliding
162    /// sync, and if the sliding-sync can't find the event, then it'll use a
163    /// `/context` query to find the event with associated member information.
164    ///
165    /// An error result means that we couldn't resolve the notification; in that
166    /// case, a dummy notification may be displayed instead.
167    #[instrument(skip(self))]
168    pub async fn get_notification(
169        &self,
170        room_id: &RoomId,
171        event_id: &EventId,
172    ) -> Result<NotificationStatus, Error> {
173        let status = self.get_notification_with_sliding_sync(room_id, event_id).await?;
174        match status {
175            NotificationStatus::Event(..)
176            | NotificationStatus::EventFilteredOut
177            | NotificationStatus::EventRedacted => Ok(status),
178            NotificationStatus::EventNotFound => {
179                self.get_notification_with_context(room_id, event_id).await
180            }
181        }
182    }
183
184    /// Fetches the content of several notifications.
185    ///
186    /// This will first try to get the notifications using a short-lived sliding
187    /// sync, and if the sliding-sync can't find the events, then it'll use a
188    /// `/context` query to find the events with associated member information.
189    ///
190    /// An error result at the top level means that something failed when trying
191    /// to set up the notification fetching.
192    ///
193    /// For each notification item you can also receive an error, which means
194    /// something failed when trying to fetch that particular notification
195    /// (decryption, fetching push actions, etc.); in that case, a dummy
196    /// notification may be displayed instead.
197    pub async fn get_notifications(
198        &self,
199        requests: &[NotificationItemsRequest],
200    ) -> Result<BatchNotificationFetchingResult, Error> {
201        let mut notifications = self.get_notifications_with_sliding_sync(requests).await?;
202
203        for request in requests {
204            for event_id in &request.event_ids {
205                match notifications.get_mut(event_id) {
206                    // If the notification for a given event wasn't found with sliding sync, try
207                    // with a /context for each event.
208                    Some(Ok(NotificationStatus::EventNotFound)) | None => {
209                        notifications.insert(
210                            event_id.to_owned(),
211                            self.get_notification_with_context(&request.room_id, event_id).await,
212                        );
213                    }
214
215                    _ => {}
216                }
217            }
218        }
219
220        Ok(notifications)
221    }
222
223    /// Run an encryption sync loop, in case an event is still encrypted.
224    ///
225    /// Will return `Ok(Some)` if and only if:
226    /// - the event was encrypted,
227    /// - we successfully ran an encryption sync or waited long enough for an
228    ///   existing encryption sync to decrypt the event.
229    ///
230    /// Otherwise, if the event was not encrypted, or couldn't be decrypted
231    /// (without causing a fatal error), will return `Ok(None)`.
232    #[instrument(skip_all)]
233    async fn retry_decryption(
234        &self,
235        room: &Room,
236        raw_event: &Raw<AnySyncTimelineEvent>,
237    ) -> Result<Option<TimelineEvent>, Error> {
238        let event: AnySyncTimelineEvent =
239            raw_event.deserialize().map_err(|_| Error::InvalidRumaEvent)?;
240
241        if !is_event_encrypted(event.event_type()) {
242            return Ok(None);
243        }
244
245        // Serialize calls to this function.
246        let _guard = self.encryption_sync_mutex.lock().await;
247
248        // The message is still encrypted, and the client is configured to retry
249        // decryption.
250        //
251        // Spawn an `EncryptionSync` that runs two iterations of the sliding sync loop:
252        // - the first iteration allows to get SS events as well as send e2ee requests.
253        // - the second one let the SS homeserver forward events triggered by the
254        //   sending of e2ee requests.
255        //
256        // Keep timeouts small for both, since we might be short on time.
257
258        let push_ctx = room.push_context().await?;
259        let sync_permit_guard = match &self.process_setup {
260            NotificationProcessSetup::MultipleProcesses => {
261                // We're running on our own process, dedicated for notifications. In that case,
262                // create a dummy sync permit; we're guaranteed there's at most one since we've
263                // acquired the `encryption_sync_mutex' lock here.
264                let sync_permit = Arc::new(AsyncMutex::new(EncryptionSyncPermit::new()));
265                sync_permit.lock_owned().await
266            }
267
268            NotificationProcessSetup::SingleProcess { sync_service } => {
269                if let Some(permit_guard) = sync_service.try_get_encryption_sync_permit() {
270                    permit_guard
271                } else {
272                    // There's already a sync service active, thus the encryption sync is already
273                    // running elsewhere, and we must not run a second one. As a matter of fact,
274                    // if the event was encrypted, that means we were racing against the
275                    // encryption sync: wait for it to receive the room key, then decrypt.
276                    debug!("Encryption sync running in background, waiting for the room key");
277                    return self.wait_for_room_key(room, raw_event, push_ctx.as_ref()).await;
278                }
279            }
280        };
281
282        let encryption_sync = EncryptionSyncService::new(
283            self.client.clone(),
284            Some((Duration::from_secs(3), Duration::from_secs(4))),
285        )
286        .await;
287
288        // Just log out errors, but don't have them abort the notification processing:
289        // an undecrypted notification is still better than no
290        // notifications.
291
292        match encryption_sync {
293            Ok(sync) => match sync.run_fixed_iterations(2, sync_permit_guard).await {
294                // Note: We specify the cast type in case the
295                // `experimental-encrypted-state-events` feature is enabled, which provides
296                // multiple cast implementations.
297                Ok(()) => match room.decrypt_event(raw_event.cast_ref_unchecked::<OriginalSyncRoomEncryptedEvent>(), push_ctx.as_ref()).await {
298                    Ok(new_event) => match new_event.kind {
299                        matrix_sdk::deserialized_responses::TimelineEventKind::UnableToDecrypt {
300                            utd_info, ..
301                        } => {
302                            trace!(
303                                "Encryption sync failed to decrypt the event: {:?}",
304                                utd_info.reason
305                            );
306                            Ok(None)
307                        }
308                        _ => {
309                            trace!("Encryption sync managed to decrypt the event.");
310                            Ok(Some(new_event))
311                        }
312                    },
313                    Err(err) => {
314                        trace!("Encryption sync failed to decrypt the event: {err}");
315                        Ok(None)
316                    }
317                },
318                Err(err) => {
319                    warn!("Encryption sync error: {err:#}");
320                    Ok(None)
321                }
322            },
323            Err(err) => {
324                warn!("Encryption sync build error: {err:#}",);
325                Ok(None)
326            }
327        }
328    }
329
330    /// Wait for the main encryption sync to receive the room key needed to
331    /// decrypt `raw_event`, then decrypt it.
332    ///
333    /// This is used in a [`NotificationProcessSetup::SingleProcess`] setup when
334    /// the encryption sync is already running, since the notification client
335    /// must not run a second one.
336    ///
337    /// Returns `Ok(None)` if no key for the room has been received within
338    /// [`Self::RUNNING_SYNC_DECRYPTION_DEADLINE`], or if the event can't be
339    /// decrypted for another reason.
340    async fn wait_for_room_key(
341        &self,
342        room: &Room,
343        raw_event: &Raw<AnySyncTimelineEvent>,
344        push_ctx: Option<&PushContext>,
345    ) -> Result<Option<TimelineEvent>, Error> {
346        // Subscribe before the first decryption attempt, so that a key received in
347        // between can't be missed. The notification client shares its `OlmMachine` with
348        // the parent client, which the running encryption sync belongs to, so keys it
349        // receives are both reported here and usable by `try_decrypt` right away.
350        let Some(room_keys) = self.parent_client.encryption().room_keys_received_stream().await
351        else {
352            // No `OlmMachine`, hence no keys to wait for: a single attempt is all we can
353            // do.
354            return Ok(match try_decrypt(room, raw_event, push_ctx).await? {
355                DecryptionAttempt::Decrypted(event) => Some(event),
356                DecryptionAttempt::MissingRoomKey | DecryptionAttempt::Unrecoverable => None,
357            });
358        };
359        pin_mut!(room_keys);
360
361        let deadline = Instant::now() + Self::RUNNING_SYNC_DECRYPTION_DEADLINE;
362
363        loop {
364            match try_decrypt(room, raw_event, push_ctx).await? {
365                DecryptionAttempt::Decrypted(event) => {
366                    trace!("Waiting succeeded and event could be decrypted!");
367                    return Ok(Some(event));
368                }
369                DecryptionAttempt::Unrecoverable => return Ok(None),
370                DecryptionAttempt::MissingRoomKey => {}
371            }
372
373            // Wait for keys of this room to be received, then try again.
374            loop {
375                let remaining = deadline.saturating_duration_since(Instant::now());
376                if remaining.is_zero() {
377                    debug!("Timeout waiting for the encryption sync to receive the room key.");
378                    return Ok(None);
379                }
380
381                match timeout(room_keys.next(), remaining).await {
382                    Ok(Some(Ok(keys))) => {
383                        if keys.iter().any(|key| &*key.room_id == room.room_id()) {
384                            trace!("Received room keys for the room, retrying decryption");
385                            break;
386                        }
387                        // Keys for other rooms can't help, keep waiting.
388                    }
389                    Ok(Some(Err(_))) => {
390                        // The stream lagged behind, so we may have missed keys for the room:
391                        // retry to be on the safe side.
392                        break;
393                    }
394                    Ok(None) => {
395                        debug!("The room keys stream ended while waiting for the room key.");
396                        return Ok(None);
397                    }
398                    Err(_) => {
399                        debug!("Timeout waiting for the encryption sync to receive the room key.");
400                        return Ok(None);
401                    }
402                }
403            }
404        }
405    }
406
407    /// Try to run a sliding sync (without encryption) to retrieve the events
408    /// from the notification.
409    ///
410    /// An event can either be:
411    /// - an invite event,
412    /// - or a non-invite event.
413    ///
414    /// In case it's a non-invite event, it's rather easy: we'll request
415    /// explicit state that'll be useful for building the
416    /// `NotificationItem`, and subscribe to the room which the notification
417    /// relates to.
418    ///
419    /// In case it's an invite-event, it's trickier because the stripped event
420    /// may not contain the event id, so we can't just match on it. Rather,
421    /// we look at stripped room member events that may be fitting (i.e.
422    /// match the current user and are invites), and if the SDK concludes the
423    /// room was in the invited state, and we didn't find the event by id,
424    /// *then* we'll use that stripped room member event.
425    #[instrument(skip_all)]
426    async fn try_sliding_sync(
427        &self,
428        requests: &[NotificationItemsRequest],
429    ) -> Result<BTreeMap<OwnedEventId, (OwnedRoomId, Option<RawNotificationEvent>)>, Error> {
430        const MAX_SLIDING_SYNC_ATTEMPTS: u64 = 3;
431        // Serialize all the calls to this method by taking a lock at the beginning,
432        // that will be dropped later.
433        let _guard = self.notification_sync_mutex.lock().await;
434
435        // Set up a sliding sync that only subscribes to the room that had the
436        // notification, so we can figure out the full event and associated
437        // information.
438
439        let raw_notifications = Arc::new(Mutex::new(BTreeMap::new()));
440        let handler_raw_notification = raw_notifications.clone();
441
442        let raw_invites = Arc::new(Mutex::new(BTreeMap::new()));
443        let handler_raw_invites = raw_invites.clone();
444
445        let user_id = self.client.user_id().unwrap().to_owned();
446        let room_ids = requests.iter().map(|req| req.room_id.clone()).collect::<Vec<_>>();
447
448        let requests = Arc::new(requests.iter().map(|req| (*req).clone()).collect::<Vec<_>>());
449
450        let timeline_event_handler = self.client.add_event_handler({
451            let requests = requests.clone();
452            move |raw: Raw<AnySyncTimelineEvent>| async move {
453                match &raw.get_field::<OwnedEventId>("event_id") {
454                    Ok(Some(event_id)) => {
455                        let Some(request) =
456                            &requests.iter().find(|request| request.event_ids.contains(event_id))
457                        else {
458                            return;
459                        };
460
461                        let room_id = request.room_id.clone();
462
463                        // found it! There shouldn't be a previous event before, but if
464                        // there is, that should be ok to
465                        // just replace it.
466                        handler_raw_notification.lock().unwrap().insert(
467                            event_id.to_owned(),
468                            (room_id, Some(RawNotificationEvent::Timeline(raw))),
469                        );
470                    }
471                    Ok(None) => {
472                        warn!("a sync event had no event id");
473                    }
474                    Err(err) => {
475                        warn!("failed to deserialize sync event id: {err}");
476                    }
477                }
478            }
479        });
480
481        let handler_raw_notifications = raw_notifications.clone();
482        let stripped_member_handler = self.client.add_event_handler({
483            let requests = requests.clone();
484            let room_ids: Vec<_> = room_ids.clone();
485            move |raw: Raw<StrippedRoomMemberEvent>, room: Room| async move {
486                if !room_ids.contains(&room.room_id().to_owned()) {
487                    return;
488                }
489
490                let deserialized = match raw.deserialize() {
491                    Ok(d) => d,
492                    Err(err) => {
493                        warn!("failed to deserialize raw stripped room member event: {err}");
494                        return;
495                    }
496                };
497
498                trace!("received a stripped room member event");
499
500                // Try to match the event by event_id, as it's the most precise. In theory, we
501                // shouldn't receive it, so that's a first attempt.
502                match &raw.get_field::<OwnedEventId>("event_id") {
503                    Ok(Some(event_id)) => {
504                        let request =
505                            &requests.iter().find(|request| request.event_ids.contains(event_id));
506                        if request.is_none() {
507                            return;
508                        }
509                        let room_id = request.unwrap().room_id.clone();
510
511                        // found it! There shouldn't be a previous event before, but if
512                        // there is, that should be ok to
513                        // just replace it.
514                        handler_raw_notifications.lock().unwrap().insert(
515                            event_id.to_owned(),
516                            (room_id, Some(RawNotificationEvent::Invite(raw))),
517                        );
518                        return;
519                    }
520                    Ok(None) => {
521                        warn!("a room member event had no id");
522                    }
523                    Err(err) => {
524                        warn!("failed to deserialize room member event id: {err}");
525                    }
526                }
527
528                // Try to match the event by membership and state_key for the current user.
529                if deserialized.content.membership == MembershipState::Invite
530                    && deserialized.state_key == user_id
531                {
532                    trace!("found an invite event for the current user");
533                    // This could be it! There might be several of these following each other, so
534                    // assume it's the latest one (in sync ordering), and override a previous one if
535                    // present.
536                    handler_raw_invites
537                        .lock()
538                        .unwrap()
539                        .insert(deserialized.state_key, Some(RawNotificationEvent::Invite(raw)));
540                } else {
541                    trace!("not an invite event, or not for the current user");
542                }
543            }
544        });
545
546        // Room power levels are necessary to build the push context.
547        let required_state = vec![
548            (StateEventType::RoomEncryption, "".to_owned()),
549            (StateEventType::RoomMember, "$LAZY".to_owned()),
550            (StateEventType::RoomMember, "$ME".to_owned()),
551            (StateEventType::RoomCanonicalAlias, "".to_owned()),
552            (StateEventType::RoomName, "".to_owned()),
553            (StateEventType::RoomAvatar, "".to_owned()),
554            (StateEventType::RoomPowerLevels, "".to_owned()),
555            (StateEventType::RoomJoinRules, "".to_owned()),
556            (StateEventType::CallMember, "*".to_owned()),
557            (StateEventType::RoomCreate, "".to_owned()),
558            (StateEventType::MemberHints, "".to_owned()),
559        ];
560
561        let invites = SlidingSyncList::builder("invites")
562            .sync_mode(SlidingSyncMode::new_selective().add_range(0..=16))
563            .timeline_limit(8)
564            .required_state(required_state.clone())
565            .filters(Some(assign!(http::request::ListFilters::default(), {
566                is_invite: Some(true),
567            })));
568
569        let sync = self
570            .client
571            .sliding_sync(Self::CONNECTION_ID)?
572            .poll_timeout(Duration::from_secs(1))
573            .network_timeout(Duration::from_secs(3))
574            .with_account_data_extension(
575                assign!(http::request::AccountData::default(), { enabled: Some(true) }),
576            )
577            .add_list(invites)
578            .build()
579            .await?;
580
581        sync.add_room_subscriptions(
582            &room_ids.iter().map(|id| id.deref()).collect::<Vec<&RoomId>>(),
583            Some(assign!(http::request::RoomSubscription::default(), {
584                required_state,
585                timeline_limit: uint!(16)
586            })),
587            true,
588        );
589
590        let mut remaining_attempts = MAX_SLIDING_SYNC_ATTEMPTS;
591
592        let stream = sync.sync();
593        pin_mut!(stream);
594
595        // Sum the expected event count for each room
596        let expected_event_count = requests.iter().map(|req| req.event_ids.len()).sum::<usize>();
597
598        loop {
599            if stream.next().await.is_none() {
600                // Sliding sync aborted early.
601                break;
602            }
603
604            let event_count = raw_notifications.lock().unwrap().len();
605            let invite_count = raw_invites.lock().unwrap().len();
606
607            let current_attempt = 1 + MAX_SLIDING_SYNC_ATTEMPTS - remaining_attempts;
608            trace!(
609                "Attempt #{current_attempt}: \
610                Found {event_count} notification(s), \
611                {invite_count} invite event(s), \
612                expected {expected_event_count} total",
613            );
614
615            // We can stop looking once we've received the expected number of events from
616            // the sync. Since we can receive only events or invites for rooms but not both,
617            // and we're not taking into account invites from not subscribed rooms, this
618            // check should be accurate.
619            if event_count + invite_count == expected_event_count {
620                // We got the events.
621                break;
622            }
623
624            remaining_attempts -= 1;
625            warn!("There are some missing notifications, remaining attempts: {remaining_attempts}");
626            if remaining_attempts == 0 {
627                // We're out of luck.
628                break;
629            }
630        }
631
632        self.client.remove_event_handler(stripped_member_handler);
633        self.client.remove_event_handler(timeline_event_handler);
634
635        let mut notifications = raw_notifications.clone().lock().unwrap().clone();
636        let mut missing_event_ids = Vec::new();
637
638        // Create the list of missing event ids after the syncs.
639        for request in requests.iter() {
640            for event_id in &request.event_ids {
641                if !notifications.contains_key(event_id) {
642                    missing_event_ids.push((request.room_id.to_owned(), event_id.to_owned()));
643                }
644            }
645        }
646
647        // Try checking if the missing notifications could be invites.
648        for (room_id, missing_event_id) in missing_event_ids {
649            trace!("we didn't have a non-invite event, looking for invited room now");
650            if let Some(room) = self.client.get_room(&room_id) {
651                if room.state() == RoomState::Invited {
652                    if let Some((_, stripped_event)) = raw_invites.lock().unwrap().pop_first() {
653                        notifications
654                            .insert(missing_event_id, (room_id.to_owned(), stripped_event));
655                    }
656                } else {
657                    debug!("the room isn't in the invited state");
658                }
659            } else {
660                warn!(%room_id, "unknown room, can't check for invite events");
661            }
662        }
663
664        let found = if notifications.len() == expected_event_count { "" } else { "not " };
665        trace!("all notification events have{found} been found");
666
667        Ok(notifications)
668    }
669
670    pub async fn get_notification_with_sliding_sync(
671        &self,
672        room_id: &RoomId,
673        event_id: &EventId,
674    ) -> Result<NotificationStatus, Error> {
675        info!("fetching notification event with a sliding sync");
676
677        let request = NotificationItemsRequest {
678            room_id: room_id.to_owned(),
679            event_ids: vec![event_id.to_owned()],
680        };
681
682        let mut get_notifications_result =
683            self.get_notifications_with_sliding_sync(&[request]).await?;
684
685        get_notifications_result.remove(event_id).unwrap_or(Ok(NotificationStatus::EventNotFound))
686    }
687
688    /// Given a (decrypted or not) event, figure out whether it should be
689    /// filtered out for other client-side reasons (such as the sender being
690    /// ignored, for instance), and returns the corresponding
691    /// [`NotificationStatus`].
692    async fn compute_status(
693        &self,
694        room: &Room,
695        push_actions: Option<&[Action]>,
696        raw_event: RawNotificationEvent,
697        state_events: Vec<Raw<AnyStateEvent>>,
698    ) -> Result<NotificationStatus, Error> {
699        if let Some(actions) = push_actions
700            && !actions.iter().any(|a| a.should_notify())
701        {
702            // The event shouldn't notify: return early.
703            return Ok(NotificationStatus::EventFilteredOut);
704        }
705
706        let notification_item =
707            NotificationItem::new(room, raw_event, push_actions, state_events).await?;
708
709        if self.client.is_user_ignored(notification_item.event.sender()).await {
710            Ok(NotificationStatus::EventFilteredOut)
711        } else {
712            Ok(NotificationStatus::Event(Box::new(notification_item)))
713        }
714    }
715
716    /// Get a list of full notifications, given a room id and event ids.
717    ///
718    /// This will run a small sliding sync to retrieve the content of the
719    /// events, along with extra data to form a rich notification context.
720    pub async fn get_notifications_with_sliding_sync(
721        &self,
722        requests: &[NotificationItemsRequest],
723    ) -> Result<BatchNotificationFetchingResult, Error> {
724        let raw_events = self.try_sliding_sync(requests).await?;
725
726        let mut batch_result = BatchNotificationFetchingResult::new();
727
728        for (event_id, (room_id, raw_event)) in raw_events.into_iter() {
729            // At this point it should have been added by the sync, if it's not, give up.
730            let Some(room) = self.client.get_room(&room_id) else { return Err(Error::UnknownRoom) };
731
732            let Some(raw_event) = raw_event else {
733                // The event was not found, so we can't build a notification.
734                batch_result.insert(event_id, Ok(NotificationStatus::EventNotFound));
735                continue;
736            };
737
738            let (raw_event, push_actions) = match &raw_event {
739                RawNotificationEvent::Timeline(timeline_event) => {
740                    // Check if the event is redacted first
741                    let event_for_redaction_check: AnySyncTimelineEvent =
742                        match timeline_event.deserialize() {
743                            Ok(event) => event,
744                            Err(_) => {
745                                batch_result.insert(event_id, Err(Error::InvalidRumaEvent));
746                                continue;
747                            }
748                        };
749
750                    if is_event_redacted(&event_for_redaction_check) {
751                        batch_result.insert(event_id, Ok(NotificationStatus::EventRedacted));
752                        continue;
753                    }
754
755                    // Timeline events may be encrypted, so make sure they get decrypted first.
756                    match self.retry_decryption(&room, timeline_event).await {
757                        Ok(Some(timeline_event)) => {
758                            let push_actions = timeline_event.push_actions().map(ToOwned::to_owned);
759                            (
760                                RawNotificationEvent::Timeline(timeline_event.into_raw()),
761                                push_actions,
762                            )
763                        }
764
765                        Ok(None) => {
766                            // The event was either not encrypted in the first place, or we
767                            // couldn't decrypt it after retrying. Use the raw event as is.
768                            match room.event_push_actions(timeline_event).await {
769                                Ok(push_actions) => (raw_event.clone(), push_actions),
770                                Err(err) => {
771                                    // Could not get push actions.
772                                    batch_result.insert(event_id, Err(err.into()));
773                                    continue;
774                                }
775                            }
776                        }
777
778                        Err(err) => {
779                            batch_result.insert(event_id, Err(err));
780                            continue;
781                        }
782                    }
783                }
784
785                RawNotificationEvent::Invite(invite_event) => {
786                    // Invite events can't be encrypted, so they should be in clear text.
787                    match room.event_push_actions(invite_event).await {
788                        Ok(push_actions) => {
789                            (RawNotificationEvent::Invite(invite_event.clone()), push_actions)
790                        }
791                        Err(err) => {
792                            batch_result.insert(event_id, Err(err.into()));
793                            continue;
794                        }
795                    }
796                }
797            };
798
799            let notification_status_result =
800                self.compute_status(&room, push_actions.as_deref(), raw_event, Vec::new()).await;
801
802            batch_result.insert(event_id, notification_status_result);
803        }
804
805        Ok(batch_result)
806    }
807
808    /// Retrieve a notification using a `/context` query.
809    ///
810    /// This is for clients that are already running other sliding syncs in the
811    /// same process, so that most of the contextual information for the
812    /// notification should already be there. In particular, the room containing
813    /// the event MUST be known (via a sliding sync for invites, or another
814    /// sliding sync).
815    ///
816    /// An error result means that we couldn't resolve the notification; in that
817    /// case, a dummy notification may be displayed instead. A `None` result
818    /// means the notification has been filtered out by the user's push
819    /// rules.
820    pub async fn get_notification_with_context(
821        &self,
822        room_id: &RoomId,
823        event_id: &EventId,
824    ) -> Result<NotificationStatus, Error> {
825        info!("fetching notification event with a /context query");
826
827        // See above comment.
828        let Some(room) = self.parent_client.get_room(room_id) else {
829            return Err(Error::UnknownRoom);
830        };
831
832        let response = room.event_with_context(event_id, true, uint!(0), None).await?;
833
834        let mut timeline_event = response.event.ok_or(Error::ContextMissingEvent)?;
835        let state_events = response.state;
836
837        // Check if the event is redacted
838        let event_for_redaction_check: AnySyncTimelineEvent =
839            timeline_event.raw().deserialize().map_err(|_| Error::InvalidRumaEvent)?;
840
841        if is_event_redacted(&event_for_redaction_check) {
842            return Ok(NotificationStatus::EventRedacted);
843        }
844
845        if let Some(decrypted_event) = self.retry_decryption(&room, timeline_event.raw()).await? {
846            timeline_event = decrypted_event;
847        }
848
849        let push_actions = timeline_event.push_actions().map(ToOwned::to_owned);
850
851        self.compute_status(
852            &room,
853            push_actions.as_deref(),
854            RawNotificationEvent::Timeline(timeline_event.into_raw()),
855            state_events,
856        )
857        .await
858    }
859}
860
861/// The outcome of an attempt at decrypting a notified event.
862enum DecryptionAttempt {
863    /// The event could be decrypted.
864    Decrypted(TimelineEvent),
865
866    /// The event could not be decrypted because the room key is missing; it may
867    /// still arrive.
868    MissingRoomKey,
869
870    /// The event could not be decrypted, and waiting longer is unlikely to
871    /// help.
872    Unrecoverable,
873}
874
875/// Attempt to decrypt an encrypted timeline event of `room`.
876async fn try_decrypt(
877    room: &Room,
878    raw_event: &Raw<AnySyncTimelineEvent>,
879    push_ctx: Option<&PushContext>,
880) -> Result<DecryptionAttempt, matrix_sdk::Error> {
881    // Note: We specify the cast type in case the
882    // `experimental-encrypted-state-events` feature is enabled, which provides
883    // multiple cast implementations.
884    let new_event = room
885        .decrypt_event(raw_event.cast_ref_unchecked::<OriginalSyncRoomEncryptedEvent>(), push_ctx)
886        .await?;
887
888    if let matrix_sdk::deserialized_responses::TimelineEventKind::UnableToDecrypt {
889        utd_info, ..
890    } = &new_event.kind
891    {
892        return Ok(if utd_info.reason.is_missing_room_key() {
893            DecryptionAttempt::MissingRoomKey
894        } else {
895            debug!(
896                "Event could not be decrypted, but waiting longer is unlikely to help: {:?}",
897                utd_info.reason
898            );
899            DecryptionAttempt::Unrecoverable
900        });
901    }
902
903    Ok(DecryptionAttempt::Decrypted(new_event))
904}
905
906fn is_event_encrypted(event_type: TimelineEventType) -> bool {
907    let is_still_encrypted = matches!(event_type, TimelineEventType::RoomEncrypted);
908
909    #[cfg(feature = "unstable-msc3956")]
910    let is_still_encrypted =
911        is_still_encrypted || matches!(event_type, ruma::events::TimelineEventType::Encrypted);
912
913    is_still_encrypted
914}
915
916fn is_event_redacted(event: &AnySyncTimelineEvent) -> bool {
917    // Check if the event is a message-like event but has no original content (i.e.,
918    // redacted)
919    match event {
920        AnySyncTimelineEvent::MessageLike(msg) => msg.is_redacted(),
921        _ => false,
922    }
923}
924
925#[derive(Debug)]
926pub enum NotificationStatus {
927    /// The event has been found and was not filtered out.
928    Event(Box<NotificationItem>),
929    /// The event couldn't be found in the network queries used to find it.
930    EventNotFound,
931    /// The event has been filtered out, either because of the user's push
932    /// rules, or because the user which triggered it is ignored by the
933    /// current user.
934    EventFilteredOut,
935    /// The event has been redacted and has no meaningful content.
936    EventRedacted,
937}
938
939#[derive(Debug, Clone)]
940pub struct NotificationItemsRequest {
941    pub room_id: OwnedRoomId,
942    pub event_ids: Vec<OwnedEventId>,
943}
944
945type BatchNotificationFetchingResult = BTreeMap<OwnedEventId, Result<NotificationStatus, Error>>;
946
947/// The Notification event as it was fetched from remote for the
948/// given `event_id`, represented as Raw but decrypted, thus only
949/// whether it is an invite or regular Timeline event has been
950/// determined.
951#[derive(Debug, Clone)]
952pub enum RawNotificationEvent {
953    /// The raw event for a timeline event
954    Timeline(Raw<AnySyncTimelineEvent>),
955    /// The notification contains an invitation with the given
956    /// StrippedRoomMemberEvent (in raw here)
957    Invite(Raw<StrippedRoomMemberEvent>),
958}
959
960/// The deserialized Event as it was fetched from remote for the
961/// given `event_id` and after decryption (if possible).
962#[derive(Debug)]
963pub enum NotificationEvent {
964    /// The Notification was for a TimelineEvent
965    Timeline(Box<AnySyncTimelineEvent>),
966    /// The Notification is an invite with the given stripped room event data
967    Invite(Box<StrippedRoomMemberEvent>),
968}
969
970impl NotificationEvent {
971    pub fn sender(&self) -> &UserId {
972        match self {
973            NotificationEvent::Timeline(ev) => ev.sender(),
974            NotificationEvent::Invite(ev) => &ev.sender,
975        }
976    }
977
978    /// Returns the root event id of the thread the notification event is in, if
979    /// any.
980    fn thread_id(&self) -> Option<OwnedEventId> {
981        let NotificationEvent::Timeline(sync_timeline_event) = &self else {
982            return None;
983        };
984        let AnySyncTimelineEvent::MessageLike(event) = sync_timeline_event.as_ref() else {
985            return None;
986        };
987        let content = event.original_content()?;
988        match content {
989            AnyMessageLikeEventContent::RoomMessage(content) => match content.relates_to? {
990                Relation::Thread(thread) => Some(thread.event_id),
991                _ => None,
992            },
993            _ => None,
994        }
995    }
996}
997
998/// A notification with its full content.
999#[derive(Debug)]
1000pub struct NotificationItem {
1001    /// Underlying Ruma event.
1002    pub event: NotificationEvent,
1003
1004    /// The raw of the underlying event.
1005    pub raw_event: RawNotificationEvent,
1006
1007    /// Display name of the sender.
1008    pub sender_display_name: Option<String>,
1009    /// Avatar URL of the sender.
1010    pub sender_avatar_url: Option<String>,
1011    /// Is the sender's name ambiguous?
1012    pub is_sender_name_ambiguous: bool,
1013
1014    /// Room computed display name.
1015    pub room_computed_display_name: String,
1016    /// Room avatar URL.
1017    pub room_avatar_url: Option<String>,
1018    /// Room canonical alias.
1019    pub room_canonical_alias: Option<String>,
1020    /// Room topic.
1021    pub room_topic: Option<String>,
1022    /// Room join rule.
1023    ///
1024    /// Set to `None` if the join rule for this room is not available.
1025    pub room_join_rule: Option<JoinRule>,
1026    /// Is this room encrypted?
1027    pub is_room_encrypted: Option<bool>,
1028    /// Is this room considered a direct message?
1029    pub is_direct_message_room: bool,
1030    /// Numbers of members who joined the room.
1031    pub joined_members_count: u64,
1032    /// Number of service members in the room.
1033    pub service_members: Vec<String>,
1034    pub active_service_members_count: u64,
1035    /// Is the room a space?
1036    pub is_space: bool,
1037
1038    /// Is it a noisy notification? (i.e. does any push action contain a sound
1039    /// action)
1040    ///
1041    /// It is set if and only if the push actions could be determined.
1042    pub is_noisy: Option<bool>,
1043    pub has_mention: Option<bool>,
1044    pub thread_id: Option<OwnedEventId>,
1045
1046    /// The push actions for this notification (notify, sound, highlight, etc.).
1047    pub actions: Option<Vec<Action>>,
1048
1049    /// Whether the room this notification is from is a DM or not.
1050    pub room_is_dm: bool,
1051}
1052
1053impl NotificationItem {
1054    async fn new(
1055        room: &Room,
1056        raw_event: RawNotificationEvent,
1057        push_actions: Option<&[Action]>,
1058        state_events: Vec<Raw<AnyStateEvent>>,
1059    ) -> Result<Self, Error> {
1060        let event = match &raw_event {
1061            RawNotificationEvent::Timeline(raw_event) => {
1062                let mut event = raw_event.deserialize().map_err(|_| Error::InvalidRumaEvent)?;
1063                if let AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
1064                    SyncRoomMessageEvent::Original(ev),
1065                )) = &mut event
1066                {
1067                    ev.content.sanitize(DEFAULT_SANITIZER_MODE, RemoveReplyFallback::Yes);
1068                }
1069                NotificationEvent::Timeline(Box::new(event))
1070            }
1071            RawNotificationEvent::Invite(raw_event) => NotificationEvent::Invite(Box::new(
1072                raw_event.deserialize().map_err(|_| Error::InvalidRumaEvent)?,
1073            )),
1074        };
1075
1076        let sender = match room.state() {
1077            RoomState::Invited => room.invite_details().await?.inviter,
1078            _ => room.get_member_no_sync(event.sender()).await?,
1079        };
1080
1081        let (mut sender_display_name, mut sender_avatar_url, is_sender_name_ambiguous) =
1082            match &sender {
1083                Some(sender) => (
1084                    sender.display_name().map(|s| s.to_owned()),
1085                    sender.avatar_url().map(|s| s.to_string()),
1086                    sender.name_ambiguous(),
1087                ),
1088                None => (None, None, false),
1089            };
1090
1091        if sender_display_name.is_none() || sender_avatar_url.is_none() {
1092            let sender_id = event.sender();
1093            for ev in state_events {
1094                let ev = match ev.deserialize() {
1095                    Ok(ev) => ev,
1096                    Err(err) => {
1097                        warn!("Failed to deserialize a state event: {err}");
1098                        continue;
1099                    }
1100                };
1101                if ev.sender() != sender_id {
1102                    continue;
1103                }
1104                if let AnyStateEventContentChange::RoomMember(StateEventContentChange::Original {
1105                    content,
1106                    ..
1107                }) = ev.content_change()
1108                {
1109                    if sender_display_name.is_none() {
1110                        sender_display_name = content.displayname;
1111                    }
1112                    if sender_avatar_url.is_none() {
1113                        sender_avatar_url = content.avatar_url.map(|url| url.to_string());
1114                    }
1115                }
1116            }
1117        }
1118
1119        let is_noisy = push_actions.map(|actions| actions.iter().any(|a| a.sound().is_some()));
1120        let has_mention = push_actions.map(|actions| actions.iter().any(|a| a.is_highlight()));
1121        let thread_id = event.thread_id().clone();
1122        let service_members = room
1123            .service_members()
1124            .unwrap_or_default()
1125            .iter()
1126            .map(ToString::to_string)
1127            .collect_vec();
1128
1129        let active_service_members_count =
1130            room.update_active_service_members().await?.unwrap_or_default().len() as u64;
1131
1132        let item = NotificationItem {
1133            event,
1134            raw_event,
1135            sender_display_name,
1136            sender_avatar_url,
1137            is_sender_name_ambiguous,
1138            room_computed_display_name: room.display_name().await?.to_string(),
1139            room_avatar_url: room.avatar_url().map(|s| s.to_string()),
1140            room_canonical_alias: room.canonical_alias().map(|c| c.to_string()),
1141            room_topic: room.topic(),
1142            room_join_rule: room.join_rule(),
1143            is_direct_message_room: room.is_direct().await?,
1144            is_room_encrypted: room
1145                .latest_encryption_state()
1146                .await
1147                .map(|state| state.is_encrypted())
1148                .ok(),
1149            joined_members_count: room.joined_members_count(),
1150            service_members,
1151            active_service_members_count,
1152            is_space: room.is_space(),
1153            is_noisy,
1154            has_mention,
1155            thread_id,
1156            actions: push_actions.map(|actions| actions.to_vec()),
1157            room_is_dm: room.compute_is_dm().await?,
1158        };
1159
1160        Ok(item)
1161    }
1162
1163    /// Returns whether this room is public or not, based on the join rule.
1164    ///
1165    /// Maybe return `None` if the join rule is not available.
1166    pub fn is_public(&self) -> Option<bool> {
1167        self.room_join_rule.as_ref().map(|rule| matches!(rule, JoinRule::Public))
1168    }
1169}
1170
1171/// An error for the [`NotificationClient`].
1172#[derive(Debug, Error)]
1173pub enum Error {
1174    #[error(transparent)]
1175    BuildingLocalClient(ClientBuildError),
1176
1177    /// The room associated to this event wasn't found.
1178    #[error("unknown room for a notification")]
1179    UnknownRoom,
1180
1181    /// The Ruma event contained within this notification couldn't be parsed.
1182    #[error("invalid ruma event")]
1183    InvalidRumaEvent,
1184
1185    /// When calling `get_notification_with_sliding_sync`, the room was missing
1186    /// in the response.
1187    #[error("the sliding sync response doesn't include the target room")]
1188    SlidingSyncEmptyRoom,
1189
1190    #[error("the event was missing in the `/context` query")]
1191    ContextMissingEvent,
1192
1193    /// An error forwarded from the client.
1194    #[error(transparent)]
1195    SdkError(#[from] matrix_sdk::Error),
1196
1197    /// An error forwarded from the underlying state store.
1198    #[error(transparent)]
1199    StoreError(#[from] StoreError),
1200}
1201
1202#[cfg(test)]
1203mod tests {
1204    use std::collections::BTreeMap;
1205
1206    use assert_matches2::assert_let;
1207    use matrix_sdk::test_utils::mocks::MatrixMockServer;
1208    use matrix_sdk_test::{ALICE, async_test, event_factory::EventFactory};
1209    use ruma::{
1210        api::client::sync::sync_events::v5,
1211        assign, event_id,
1212        events::room::{member::MembershipState, message::RedactedRoomMessageEventContent},
1213        owned_event_id, owned_room_id, room_id, user_id,
1214    };
1215
1216    use crate::notification_client::{
1217        NotificationClient, NotificationItem, NotificationItemsRequest, NotificationProcessSetup,
1218        NotificationStatus, RawNotificationEvent,
1219    };
1220
1221    #[async_test]
1222    async fn test_notification_item_returns_thread_id() {
1223        let server = MatrixMockServer::new().await;
1224        let client = server.client_builder().build().await;
1225
1226        let room_id = room_id!("!a:b.c");
1227        let thread_root_event_id = event_id!("$root:b.c");
1228        let message = EventFactory::new()
1229            .room(room_id)
1230            .sender(user_id!("@sender:b.c"))
1231            .text_msg("Threaded")
1232            .in_thread(thread_root_event_id, event_id!("$prev:b.c"))
1233            .into_raw_sync();
1234        let room = server.sync_joined_room(&client, room_id).await;
1235
1236        let raw_notification_event = RawNotificationEvent::Timeline(message);
1237        let notification_item =
1238            NotificationItem::new(&room, raw_notification_event, None, Vec::new())
1239                .await
1240                .expect("Could not create notification item");
1241
1242        assert_let!(Some(thread_id) = notification_item.thread_id);
1243        assert_eq!(thread_id, thread_root_event_id);
1244    }
1245
1246    #[async_test]
1247    async fn test_try_sliding_sync_ignores_invites_for_non_subscribed_rooms() {
1248        let server = MatrixMockServer::new().await;
1249        let client = server.client_builder().build().await;
1250
1251        let user_id = client.user_id().unwrap();
1252        let room_id = room_id!("!a:b.c");
1253        let invite = EventFactory::new()
1254            .room(room_id)
1255            .member(user_id)
1256            .membership(MembershipState::Invite)
1257            .no_event_id()
1258            .into_raw_sync_state();
1259        let mut room = v5::response::Room::new();
1260        room.invite_state = Some(vec![invite.cast_unchecked()]);
1261        let rooms = BTreeMap::from_iter([(room_id.to_owned(), room)]);
1262        server
1263            .mock_sliding_sync()
1264            .ok(assign!(v5::Response::new("1".to_owned()), {
1265                rooms: rooms,
1266            }))
1267            .mount()
1268            .await;
1269
1270        let notification_client =
1271            NotificationClient::new(client.clone(), NotificationProcessSetup::MultipleProcesses)
1272                .await
1273                .expect("Could not create a notification client");
1274
1275        // Check we don't receive the invite for a different room, even if it was
1276        // included in the sync response
1277        let event_id = owned_event_id!("$a:b.c");
1278        let result = notification_client
1279            .try_sliding_sync(&[NotificationItemsRequest {
1280                room_id: owned_room_id!("!other:b.c"),
1281                event_ids: vec![event_id.clone()],
1282            }])
1283            .await
1284            .expect("Could not run sliding sync");
1285
1286        assert!(result.is_empty());
1287
1288        // Now try fetching the invite for the previously ignored room
1289        let result = notification_client
1290            .try_sliding_sync(&[NotificationItemsRequest {
1291                room_id: room_id.to_owned(),
1292                event_ids: vec![event_id.clone()],
1293            }])
1294            .await
1295            .expect("Could not run sliding sync");
1296
1297        // Check we did receive an event
1298        assert!(!result.is_empty());
1299
1300        // Try to assert it's the same event (since we don't have an event id)
1301        // We can check its room, sender and membership state
1302        let (in_room_id, event) = &result[&event_id];
1303        assert_eq!(room_id, in_room_id);
1304        assert_let!(Some(RawNotificationEvent::Invite(raw_invite)) = event);
1305
1306        let invite = raw_invite.deserialize().expect("Could not deserialize invite event");
1307        assert_eq!(invite.state_key, user_id.to_string());
1308        assert_eq!(invite.content.membership, MembershipState::Invite);
1309    }
1310
1311    #[async_test]
1312    async fn test_redacted_event_returns_event_redacted_status() {
1313        let server = MatrixMockServer::new().await;
1314        let client = server.client_builder().build().await;
1315
1316        let room_id = room_id!("!a:b.c");
1317
1318        // Create a redacted message event (no content)
1319        let event_id = owned_event_id!("$redacted:b.c");
1320        let redacted_event = EventFactory::new()
1321            .room(room_id)
1322            .sender(user_id!("@sender:b.c"))
1323            .redacted(&ALICE, RedactedRoomMessageEventContent::new())
1324            .event_id(&event_id)
1325            .into_raw();
1326        let mut room = v5::response::Room::new();
1327        room.timeline = vec![redacted_event];
1328
1329        let mut rooms = BTreeMap::new();
1330        rooms.insert(room_id.to_owned(), room);
1331
1332        server
1333            .mock_sliding_sync()
1334            .ok(assign!(v5::Response::new("1".to_owned()), {
1335                rooms: rooms,
1336            }))
1337            .mount()
1338            .await;
1339
1340        let notification_client =
1341            NotificationClient::new(client.clone(), NotificationProcessSetup::MultipleProcesses)
1342                .await
1343                .expect("Could not create a notification client");
1344
1345        let result: NotificationStatus = notification_client
1346            .get_notification_with_sliding_sync(room_id, &event_id)
1347            .await
1348            .expect("Could not get notification");
1349
1350        match result {
1351            NotificationStatus::EventRedacted => {
1352                // Success - redacted event was properly detected
1353            }
1354            other => panic!("Expected EventRedacted, got {:?}", other),
1355        }
1356    }
1357}