1use 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#[derive(Clone)]
63pub enum NotificationProcessSetup {
64 MultipleProcesses,
73
74 SingleProcess { sync_service: Arc<SyncService> },
82}
83
84#[derive(Clone, Copy, Debug, PartialEq, Eq)]
87pub struct NotificationClientTimeouts {
88 pub sync_poll_timeout: Duration,
92
93 pub sync_network_timeout: Duration,
97
98 pub decryption_deadline: Duration,
118
119 pub encryption_sync_poll_timeout: Duration,
127
128 pub encryption_sync_network_timeout: Duration,
132}
133
134impl Default for NotificationClientTimeouts {
135 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 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
154pub struct NotificationClient {
160 client: Client,
162
163 parent_client: Client,
165
166 process_setup: NotificationProcessSetup,
168
169 notification_sync_mutex: AsyncMutex<()>,
177
178 encryption_sync_mutex: AsyncMutex<()>,
183
184 timeouts: NotificationClientTimeouts,
187}
188
189impl NotificationClient {
190 const CONNECTION_ID: &'static str = "notifications";
191 const LOCK_ID: &'static str = "notifications";
192
193 const MIN_DECRYPTION_ITERATIONS: usize = 2;
201
202 pub async fn new(
204 parent_client: Client,
205 process_setup: NotificationProcessSetup,
206 ) -> Result<Self, Error> {
207 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 pub fn with_timeouts(mut self, timeouts: NotificationClientTimeouts) -> Self {
228 self.timeouts = timeouts;
229 self
230 }
231
232 pub fn timeouts(&self) -> &NotificationClientTimeouts {
234 &self.timeouts
235 }
236
237 pub fn get_room(&self, room_id: &RoomId) -> Option<Room> {
241 self.client.get_room(room_id)
242 }
243
244 #[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 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 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 #[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 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 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 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 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 warn!("Encryption sync error, attempting to decrypt one last time: {err:#}");
398 true
399 }
400
401 None => {
402 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 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 let Some(room_keys) = self.parent_client.encryption().room_keys_received_stream().await
458 else {
459 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 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 }
496 Ok(Some(Err(_))) => {
497 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 #[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 let _guard = self.notification_sync_mutex.lock().await;
541
542 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 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 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 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 if deserialized.content.membership == MembershipState::Invite
637 && deserialized.state_key == user_id
638 {
639 trace!("found an invite event for the current user");
640 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 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 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 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 if event_count + invite_count == expected_event_count {
727 break;
729 }
730
731 remaining_attempts -= 1;
732 warn!("There are some missing notifications, remaining attempts: {remaining_attempts}");
733 if remaining_attempts == 0 {
734 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 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 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 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 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 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 let Some(room) = self.client.get_room(&room_id) else { return Err(Error::UnknownRoom) };
838
839 let Some(raw_event) = raw_event else {
840 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 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 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 match room.event_push_actions(timeline_event).await {
876 Ok(push_actions) => (raw_event.clone(), push_actions),
877 Err(err) => {
878 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 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 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 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 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
968enum DecryptionAttempt {
970 Decrypted(TimelineEvent),
972
973 MissingRoomKey,
976
977 Unrecoverable,
980}
981
982async fn try_decrypt(
984 room: &Room,
985 raw_event: &Raw<AnySyncTimelineEvent>,
986 push_ctx: Option<&PushContext>,
987) -> Result<DecryptionAttempt, matrix_sdk::Error> {
988 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 match event {
1027 AnySyncTimelineEvent::MessageLike(msg) => msg.is_redacted(),
1028 _ => false,
1029 }
1030}
1031
1032#[derive(Debug)]
1033pub enum NotificationStatus {
1034 Event(Box<NotificationItem>),
1036 EventNotFound,
1038 EventFilteredOut,
1042 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#[derive(Debug, Clone)]
1059pub enum RawNotificationEvent {
1060 Timeline(Raw<AnySyncTimelineEvent>),
1062 Invite(Raw<StrippedRoomMemberEvent>),
1065}
1066
1067#[derive(Debug)]
1070pub enum NotificationEvent {
1071 Timeline(Box<AnySyncTimelineEvent>),
1073 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 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#[derive(Debug)]
1107pub struct NotificationItem {
1108 pub event: NotificationEvent,
1110
1111 pub raw_event: RawNotificationEvent,
1113
1114 pub sender_display_name: Option<String>,
1116 pub sender_avatar_url: Option<String>,
1118 pub is_sender_name_ambiguous: bool,
1120
1121 pub room_computed_display_name: String,
1123 pub room_avatar_url: Option<String>,
1125 pub room_canonical_alias: Option<String>,
1127 pub room_topic: Option<String>,
1129 pub room_join_rule: Option<JoinRule>,
1133 pub is_room_encrypted: Option<bool>,
1135 pub is_direct_message_room: bool,
1137 pub joined_members_count: u64,
1139 pub service_members: Vec<String>,
1141 pub active_service_members_count: u64,
1142 pub is_space: bool,
1144
1145 pub is_noisy: Option<bool>,
1150 pub has_mention: Option<bool>,
1151 pub thread_id: Option<OwnedEventId>,
1152
1153 pub actions: Option<Vec<Action>>,
1155
1156 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 pub fn is_public(&self) -> Option<bool> {
1274 self.room_join_rule.as_ref().map(|rule| matches!(rule, JoinRule::Public))
1275 }
1276}
1277
1278#[derive(Debug, Error)]
1280pub enum Error {
1281 #[error(transparent)]
1282 BuildingLocalClient(ClientBuildError),
1283
1284 #[error("unknown room for a notification")]
1286 UnknownRoom,
1287
1288 #[error("invalid ruma event")]
1290 InvalidRumaEvent,
1291
1292 #[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 #[error(transparent)]
1302 SdkError(#[from] matrix_sdk::Error),
1303
1304 #[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 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 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 assert!(!result.is_empty());
1406
1407 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 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 }
1461 other => panic!("Expected EventRedacted, got {:?}", other),
1462 }
1463 }
1464}