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