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
84pub struct NotificationClient {
90 client: Client,
92
93 parent_client: Client,
95
96 process_setup: NotificationProcessSetup,
98
99 notification_sync_mutex: AsyncMutex<()>,
107
108 encryption_sync_mutex: AsyncMutex<()>,
113}
114
115impl NotificationClient {
116 const CONNECTION_ID: &'static str = "notifications";
117 const LOCK_ID: &'static str = "notifications";
118
119 const RUNNING_SYNC_DECRYPTION_DEADLINE: Duration = Duration::from_secs(6);
128
129 pub async fn new(
131 parent_client: Client,
132 process_setup: NotificationProcessSetup,
133 ) -> Result<Self, Error> {
134 let cross_process_store_config = match process_setup {
136 NotificationProcessSetup::MultipleProcesses => {
137 CrossProcessLockConfig::multi_process(Self::LOCK_ID)
138 }
139 NotificationProcessSetup::SingleProcess { .. } => CrossProcessLockConfig::SingleProcess,
140 };
141 let client = parent_client.notification_client(cross_process_store_config).await?;
142
143 Ok(NotificationClient {
144 client,
145 parent_client,
146 notification_sync_mutex: AsyncMutex::new(()),
147 encryption_sync_mutex: AsyncMutex::new(()),
148 process_setup,
149 })
150 }
151
152 pub fn get_room(&self, room_id: &RoomId) -> Option<Room> {
156 self.client.get_room(room_id)
157 }
158
159 #[instrument(skip(self))]
168 pub async fn get_notification(
169 &self,
170 room_id: &RoomId,
171 event_id: &EventId,
172 ) -> Result<NotificationStatus, Error> {
173 let status = self.get_notification_with_sliding_sync(room_id, event_id).await?;
174 match status {
175 NotificationStatus::Event(..)
176 | NotificationStatus::EventFilteredOut
177 | NotificationStatus::EventRedacted => Ok(status),
178 NotificationStatus::EventNotFound => {
179 self.get_notification_with_context(room_id, event_id).await
180 }
181 }
182 }
183
184 pub async fn get_notifications(
198 &self,
199 requests: &[NotificationItemsRequest],
200 ) -> Result<BatchNotificationFetchingResult, Error> {
201 let mut notifications = self.get_notifications_with_sliding_sync(requests).await?;
202
203 for request in requests {
204 for event_id in &request.event_ids {
205 match notifications.get_mut(event_id) {
206 Some(Ok(NotificationStatus::EventNotFound)) | None => {
209 notifications.insert(
210 event_id.to_owned(),
211 self.get_notification_with_context(&request.room_id, event_id).await,
212 );
213 }
214
215 _ => {}
216 }
217 }
218 }
219
220 Ok(notifications)
221 }
222
223 #[instrument(skip_all)]
233 async fn retry_decryption(
234 &self,
235 room: &Room,
236 raw_event: &Raw<AnySyncTimelineEvent>,
237 ) -> Result<Option<TimelineEvent>, Error> {
238 let event: AnySyncTimelineEvent =
239 raw_event.deserialize().map_err(|_| Error::InvalidRumaEvent)?;
240
241 if !is_event_encrypted(event.event_type()) {
242 return Ok(None);
243 }
244
245 let _guard = self.encryption_sync_mutex.lock().await;
247
248 let push_ctx = room.push_context().await?;
259 let sync_permit_guard = match &self.process_setup {
260 NotificationProcessSetup::MultipleProcesses => {
261 let sync_permit = Arc::new(AsyncMutex::new(EncryptionSyncPermit::new()));
265 sync_permit.lock_owned().await
266 }
267
268 NotificationProcessSetup::SingleProcess { sync_service } => {
269 if let Some(permit_guard) = sync_service.try_get_encryption_sync_permit() {
270 permit_guard
271 } else {
272 debug!("Encryption sync running in background, waiting for the room key");
277 return self.wait_for_room_key(room, raw_event, push_ctx.as_ref()).await;
278 }
279 }
280 };
281
282 let encryption_sync = EncryptionSyncService::new(
283 self.client.clone(),
284 Some((Duration::from_secs(3), Duration::from_secs(4))),
285 )
286 .await;
287
288 match encryption_sync {
293 Ok(sync) => match sync.run_fixed_iterations(2, sync_permit_guard).await {
294 Ok(()) => match room.decrypt_event(raw_event.cast_ref_unchecked::<OriginalSyncRoomEncryptedEvent>(), push_ctx.as_ref()).await {
298 Ok(new_event) => match new_event.kind {
299 matrix_sdk::deserialized_responses::TimelineEventKind::UnableToDecrypt {
300 utd_info, ..
301 } => {
302 trace!(
303 "Encryption sync failed to decrypt the event: {:?}",
304 utd_info.reason
305 );
306 Ok(None)
307 }
308 _ => {
309 trace!("Encryption sync managed to decrypt the event.");
310 Ok(Some(new_event))
311 }
312 },
313 Err(err) => {
314 trace!("Encryption sync failed to decrypt the event: {err}");
315 Ok(None)
316 }
317 },
318 Err(err) => {
319 warn!("Encryption sync error: {err:#}");
320 Ok(None)
321 }
322 },
323 Err(err) => {
324 warn!("Encryption sync build error: {err:#}",);
325 Ok(None)
326 }
327 }
328 }
329
330 async fn wait_for_room_key(
341 &self,
342 room: &Room,
343 raw_event: &Raw<AnySyncTimelineEvent>,
344 push_ctx: Option<&PushContext>,
345 ) -> Result<Option<TimelineEvent>, Error> {
346 let Some(room_keys) = self.parent_client.encryption().room_keys_received_stream().await
351 else {
352 return Ok(match try_decrypt(room, raw_event, push_ctx).await? {
355 DecryptionAttempt::Decrypted(event) => Some(event),
356 DecryptionAttempt::MissingRoomKey | DecryptionAttempt::Unrecoverable => None,
357 });
358 };
359 pin_mut!(room_keys);
360
361 let deadline = Instant::now() + Self::RUNNING_SYNC_DECRYPTION_DEADLINE;
362
363 loop {
364 match try_decrypt(room, raw_event, push_ctx).await? {
365 DecryptionAttempt::Decrypted(event) => {
366 trace!("Waiting succeeded and event could be decrypted!");
367 return Ok(Some(event));
368 }
369 DecryptionAttempt::Unrecoverable => return Ok(None),
370 DecryptionAttempt::MissingRoomKey => {}
371 }
372
373 loop {
375 let remaining = deadline.saturating_duration_since(Instant::now());
376 if remaining.is_zero() {
377 debug!("Timeout waiting for the encryption sync to receive the room key.");
378 return Ok(None);
379 }
380
381 match timeout(room_keys.next(), remaining).await {
382 Ok(Some(Ok(keys))) => {
383 if keys.iter().any(|key| &*key.room_id == room.room_id()) {
384 trace!("Received room keys for the room, retrying decryption");
385 break;
386 }
387 }
389 Ok(Some(Err(_))) => {
390 break;
393 }
394 Ok(None) => {
395 debug!("The room keys stream ended while waiting for the room key.");
396 return Ok(None);
397 }
398 Err(_) => {
399 debug!("Timeout waiting for the encryption sync to receive the room key.");
400 return Ok(None);
401 }
402 }
403 }
404 }
405 }
406
407 #[instrument(skip_all)]
426 async fn try_sliding_sync(
427 &self,
428 requests: &[NotificationItemsRequest],
429 ) -> Result<BTreeMap<OwnedEventId, (OwnedRoomId, Option<RawNotificationEvent>)>, Error> {
430 const MAX_SLIDING_SYNC_ATTEMPTS: u64 = 3;
431 let _guard = self.notification_sync_mutex.lock().await;
434
435 let raw_notifications = Arc::new(Mutex::new(BTreeMap::new()));
440 let handler_raw_notification = raw_notifications.clone();
441
442 let raw_invites = Arc::new(Mutex::new(BTreeMap::new()));
443 let handler_raw_invites = raw_invites.clone();
444
445 let user_id = self.client.user_id().unwrap().to_owned();
446 let room_ids = requests.iter().map(|req| req.room_id.clone()).collect::<Vec<_>>();
447
448 let requests = Arc::new(requests.iter().map(|req| (*req).clone()).collect::<Vec<_>>());
449
450 let timeline_event_handler = self.client.add_event_handler({
451 let requests = requests.clone();
452 move |raw: Raw<AnySyncTimelineEvent>| async move {
453 match &raw.get_field::<OwnedEventId>("event_id") {
454 Ok(Some(event_id)) => {
455 let Some(request) =
456 &requests.iter().find(|request| request.event_ids.contains(event_id))
457 else {
458 return;
459 };
460
461 let room_id = request.room_id.clone();
462
463 handler_raw_notification.lock().unwrap().insert(
467 event_id.to_owned(),
468 (room_id, Some(RawNotificationEvent::Timeline(raw))),
469 );
470 }
471 Ok(None) => {
472 warn!("a sync event had no event id");
473 }
474 Err(err) => {
475 warn!("failed to deserialize sync event id: {err}");
476 }
477 }
478 }
479 });
480
481 let handler_raw_notifications = raw_notifications.clone();
482 let stripped_member_handler = self.client.add_event_handler({
483 let requests = requests.clone();
484 let room_ids: Vec<_> = room_ids.clone();
485 move |raw: Raw<StrippedRoomMemberEvent>, room: Room| async move {
486 if !room_ids.contains(&room.room_id().to_owned()) {
487 return;
488 }
489
490 let deserialized = match raw.deserialize() {
491 Ok(d) => d,
492 Err(err) => {
493 warn!("failed to deserialize raw stripped room member event: {err}");
494 return;
495 }
496 };
497
498 trace!("received a stripped room member event");
499
500 match &raw.get_field::<OwnedEventId>("event_id") {
503 Ok(Some(event_id)) => {
504 let request =
505 &requests.iter().find(|request| request.event_ids.contains(event_id));
506 if request.is_none() {
507 return;
508 }
509 let room_id = request.unwrap().room_id.clone();
510
511 handler_raw_notifications.lock().unwrap().insert(
515 event_id.to_owned(),
516 (room_id, Some(RawNotificationEvent::Invite(raw))),
517 );
518 return;
519 }
520 Ok(None) => {
521 warn!("a room member event had no id");
522 }
523 Err(err) => {
524 warn!("failed to deserialize room member event id: {err}");
525 }
526 }
527
528 if deserialized.content.membership == MembershipState::Invite
530 && deserialized.state_key == user_id
531 {
532 trace!("found an invite event for the current user");
533 handler_raw_invites
537 .lock()
538 .unwrap()
539 .insert(deserialized.state_key, Some(RawNotificationEvent::Invite(raw)));
540 } else {
541 trace!("not an invite event, or not for the current user");
542 }
543 }
544 });
545
546 let required_state = vec![
548 (StateEventType::RoomEncryption, "".to_owned()),
549 (StateEventType::RoomMember, "$LAZY".to_owned()),
550 (StateEventType::RoomMember, "$ME".to_owned()),
551 (StateEventType::RoomCanonicalAlias, "".to_owned()),
552 (StateEventType::RoomName, "".to_owned()),
553 (StateEventType::RoomAvatar, "".to_owned()),
554 (StateEventType::RoomPowerLevels, "".to_owned()),
555 (StateEventType::RoomJoinRules, "".to_owned()),
556 (StateEventType::CallMember, "*".to_owned()),
557 (StateEventType::RoomCreate, "".to_owned()),
558 (StateEventType::MemberHints, "".to_owned()),
559 ];
560
561 let invites = SlidingSyncList::builder("invites")
562 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=16))
563 .timeline_limit(8)
564 .required_state(required_state.clone())
565 .filters(Some(assign!(http::request::ListFilters::default(), {
566 is_invite: Some(true),
567 })));
568
569 let sync = self
570 .client
571 .sliding_sync(Self::CONNECTION_ID)?
572 .poll_timeout(Duration::from_secs(1))
573 .network_timeout(Duration::from_secs(3))
574 .with_account_data_extension(
575 assign!(http::request::AccountData::default(), { enabled: Some(true) }),
576 )
577 .add_list(invites)
578 .build()
579 .await?;
580
581 sync.add_room_subscriptions(
582 &room_ids.iter().map(|id| id.deref()).collect::<Vec<&RoomId>>(),
583 Some(assign!(http::request::RoomSubscription::default(), {
584 required_state,
585 timeline_limit: uint!(16)
586 })),
587 true,
588 );
589
590 let mut remaining_attempts = MAX_SLIDING_SYNC_ATTEMPTS;
591
592 let stream = sync.sync();
593 pin_mut!(stream);
594
595 let expected_event_count = requests.iter().map(|req| req.event_ids.len()).sum::<usize>();
597
598 loop {
599 if stream.next().await.is_none() {
600 break;
602 }
603
604 let event_count = raw_notifications.lock().unwrap().len();
605 let invite_count = raw_invites.lock().unwrap().len();
606
607 let current_attempt = 1 + MAX_SLIDING_SYNC_ATTEMPTS - remaining_attempts;
608 trace!(
609 "Attempt #{current_attempt}: \
610 Found {event_count} notification(s), \
611 {invite_count} invite event(s), \
612 expected {expected_event_count} total",
613 );
614
615 if event_count + invite_count == expected_event_count {
620 break;
622 }
623
624 remaining_attempts -= 1;
625 warn!("There are some missing notifications, remaining attempts: {remaining_attempts}");
626 if remaining_attempts == 0 {
627 break;
629 }
630 }
631
632 self.client.remove_event_handler(stripped_member_handler);
633 self.client.remove_event_handler(timeline_event_handler);
634
635 let mut notifications = raw_notifications.clone().lock().unwrap().clone();
636 let mut missing_event_ids = Vec::new();
637
638 for request in requests.iter() {
640 for event_id in &request.event_ids {
641 if !notifications.contains_key(event_id) {
642 missing_event_ids.push((request.room_id.to_owned(), event_id.to_owned()));
643 }
644 }
645 }
646
647 for (room_id, missing_event_id) in missing_event_ids {
649 trace!("we didn't have a non-invite event, looking for invited room now");
650 if let Some(room) = self.client.get_room(&room_id) {
651 if room.state() == RoomState::Invited {
652 if let Some((_, stripped_event)) = raw_invites.lock().unwrap().pop_first() {
653 notifications
654 .insert(missing_event_id, (room_id.to_owned(), stripped_event));
655 }
656 } else {
657 debug!("the room isn't in the invited state");
658 }
659 } else {
660 warn!(%room_id, "unknown room, can't check for invite events");
661 }
662 }
663
664 let found = if notifications.len() == expected_event_count { "" } else { "not " };
665 trace!("all notification events have{found} been found");
666
667 Ok(notifications)
668 }
669
670 pub async fn get_notification_with_sliding_sync(
671 &self,
672 room_id: &RoomId,
673 event_id: &EventId,
674 ) -> Result<NotificationStatus, Error> {
675 info!("fetching notification event with a sliding sync");
676
677 let request = NotificationItemsRequest {
678 room_id: room_id.to_owned(),
679 event_ids: vec![event_id.to_owned()],
680 };
681
682 let mut get_notifications_result =
683 self.get_notifications_with_sliding_sync(&[request]).await?;
684
685 get_notifications_result.remove(event_id).unwrap_or(Ok(NotificationStatus::EventNotFound))
686 }
687
688 async fn compute_status(
693 &self,
694 room: &Room,
695 push_actions: Option<&[Action]>,
696 raw_event: RawNotificationEvent,
697 state_events: Vec<Raw<AnyStateEvent>>,
698 ) -> Result<NotificationStatus, Error> {
699 if let Some(actions) = push_actions
700 && !actions.iter().any(|a| a.should_notify())
701 {
702 return Ok(NotificationStatus::EventFilteredOut);
704 }
705
706 let notification_item =
707 NotificationItem::new(room, raw_event, push_actions, state_events).await?;
708
709 if self.client.is_user_ignored(notification_item.event.sender()).await {
710 Ok(NotificationStatus::EventFilteredOut)
711 } else {
712 Ok(NotificationStatus::Event(Box::new(notification_item)))
713 }
714 }
715
716 pub async fn get_notifications_with_sliding_sync(
721 &self,
722 requests: &[NotificationItemsRequest],
723 ) -> Result<BatchNotificationFetchingResult, Error> {
724 let raw_events = self.try_sliding_sync(requests).await?;
725
726 let mut batch_result = BatchNotificationFetchingResult::new();
727
728 for (event_id, (room_id, raw_event)) in raw_events.into_iter() {
729 let Some(room) = self.client.get_room(&room_id) else { return Err(Error::UnknownRoom) };
731
732 let Some(raw_event) = raw_event else {
733 batch_result.insert(event_id, Ok(NotificationStatus::EventNotFound));
735 continue;
736 };
737
738 let (raw_event, push_actions) = match &raw_event {
739 RawNotificationEvent::Timeline(timeline_event) => {
740 let event_for_redaction_check: AnySyncTimelineEvent =
742 match timeline_event.deserialize() {
743 Ok(event) => event,
744 Err(_) => {
745 batch_result.insert(event_id, Err(Error::InvalidRumaEvent));
746 continue;
747 }
748 };
749
750 if is_event_redacted(&event_for_redaction_check) {
751 batch_result.insert(event_id, Ok(NotificationStatus::EventRedacted));
752 continue;
753 }
754
755 match self.retry_decryption(&room, timeline_event).await {
757 Ok(Some(timeline_event)) => {
758 let push_actions = timeline_event.push_actions().map(ToOwned::to_owned);
759 (
760 RawNotificationEvent::Timeline(timeline_event.into_raw()),
761 push_actions,
762 )
763 }
764
765 Ok(None) => {
766 match room.event_push_actions(timeline_event).await {
769 Ok(push_actions) => (raw_event.clone(), push_actions),
770 Err(err) => {
771 batch_result.insert(event_id, Err(err.into()));
773 continue;
774 }
775 }
776 }
777
778 Err(err) => {
779 batch_result.insert(event_id, Err(err));
780 continue;
781 }
782 }
783 }
784
785 RawNotificationEvent::Invite(invite_event) => {
786 match room.event_push_actions(invite_event).await {
788 Ok(push_actions) => {
789 (RawNotificationEvent::Invite(invite_event.clone()), push_actions)
790 }
791 Err(err) => {
792 batch_result.insert(event_id, Err(err.into()));
793 continue;
794 }
795 }
796 }
797 };
798
799 let notification_status_result =
800 self.compute_status(&room, push_actions.as_deref(), raw_event, Vec::new()).await;
801
802 batch_result.insert(event_id, notification_status_result);
803 }
804
805 Ok(batch_result)
806 }
807
808 pub async fn get_notification_with_context(
821 &self,
822 room_id: &RoomId,
823 event_id: &EventId,
824 ) -> Result<NotificationStatus, Error> {
825 info!("fetching notification event with a /context query");
826
827 let Some(room) = self.parent_client.get_room(room_id) else {
829 return Err(Error::UnknownRoom);
830 };
831
832 let response = room.event_with_context(event_id, true, uint!(0), None).await?;
833
834 let mut timeline_event = response.event.ok_or(Error::ContextMissingEvent)?;
835 let state_events = response.state;
836
837 let event_for_redaction_check: AnySyncTimelineEvent =
839 timeline_event.raw().deserialize().map_err(|_| Error::InvalidRumaEvent)?;
840
841 if is_event_redacted(&event_for_redaction_check) {
842 return Ok(NotificationStatus::EventRedacted);
843 }
844
845 if let Some(decrypted_event) = self.retry_decryption(&room, timeline_event.raw()).await? {
846 timeline_event = decrypted_event;
847 }
848
849 let push_actions = timeline_event.push_actions().map(ToOwned::to_owned);
850
851 self.compute_status(
852 &room,
853 push_actions.as_deref(),
854 RawNotificationEvent::Timeline(timeline_event.into_raw()),
855 state_events,
856 )
857 .await
858 }
859}
860
861enum DecryptionAttempt {
863 Decrypted(TimelineEvent),
865
866 MissingRoomKey,
869
870 Unrecoverable,
873}
874
875async fn try_decrypt(
877 room: &Room,
878 raw_event: &Raw<AnySyncTimelineEvent>,
879 push_ctx: Option<&PushContext>,
880) -> Result<DecryptionAttempt, matrix_sdk::Error> {
881 let new_event = room
885 .decrypt_event(raw_event.cast_ref_unchecked::<OriginalSyncRoomEncryptedEvent>(), push_ctx)
886 .await?;
887
888 if let matrix_sdk::deserialized_responses::TimelineEventKind::UnableToDecrypt {
889 utd_info, ..
890 } = &new_event.kind
891 {
892 return Ok(if utd_info.reason.is_missing_room_key() {
893 DecryptionAttempt::MissingRoomKey
894 } else {
895 debug!(
896 "Event could not be decrypted, but waiting longer is unlikely to help: {:?}",
897 utd_info.reason
898 );
899 DecryptionAttempt::Unrecoverable
900 });
901 }
902
903 Ok(DecryptionAttempt::Decrypted(new_event))
904}
905
906fn is_event_encrypted(event_type: TimelineEventType) -> bool {
907 let is_still_encrypted = matches!(event_type, TimelineEventType::RoomEncrypted);
908
909 #[cfg(feature = "unstable-msc3956")]
910 let is_still_encrypted =
911 is_still_encrypted || matches!(event_type, ruma::events::TimelineEventType::Encrypted);
912
913 is_still_encrypted
914}
915
916fn is_event_redacted(event: &AnySyncTimelineEvent) -> bool {
917 match event {
920 AnySyncTimelineEvent::MessageLike(msg) => msg.is_redacted(),
921 _ => false,
922 }
923}
924
925#[derive(Debug)]
926pub enum NotificationStatus {
927 Event(Box<NotificationItem>),
929 EventNotFound,
931 EventFilteredOut,
935 EventRedacted,
937}
938
939#[derive(Debug, Clone)]
940pub struct NotificationItemsRequest {
941 pub room_id: OwnedRoomId,
942 pub event_ids: Vec<OwnedEventId>,
943}
944
945type BatchNotificationFetchingResult = BTreeMap<OwnedEventId, Result<NotificationStatus, Error>>;
946
947#[derive(Debug, Clone)]
952pub enum RawNotificationEvent {
953 Timeline(Raw<AnySyncTimelineEvent>),
955 Invite(Raw<StrippedRoomMemberEvent>),
958}
959
960#[derive(Debug)]
963pub enum NotificationEvent {
964 Timeline(Box<AnySyncTimelineEvent>),
966 Invite(Box<StrippedRoomMemberEvent>),
968}
969
970impl NotificationEvent {
971 pub fn sender(&self) -> &UserId {
972 match self {
973 NotificationEvent::Timeline(ev) => ev.sender(),
974 NotificationEvent::Invite(ev) => &ev.sender,
975 }
976 }
977
978 fn thread_id(&self) -> Option<OwnedEventId> {
981 let NotificationEvent::Timeline(sync_timeline_event) = &self else {
982 return None;
983 };
984 let AnySyncTimelineEvent::MessageLike(event) = sync_timeline_event.as_ref() else {
985 return None;
986 };
987 let content = event.original_content()?;
988 match content {
989 AnyMessageLikeEventContent::RoomMessage(content) => match content.relates_to? {
990 Relation::Thread(thread) => Some(thread.event_id),
991 _ => None,
992 },
993 _ => None,
994 }
995 }
996}
997
998#[derive(Debug)]
1000pub struct NotificationItem {
1001 pub event: NotificationEvent,
1003
1004 pub raw_event: RawNotificationEvent,
1006
1007 pub sender_display_name: Option<String>,
1009 pub sender_avatar_url: Option<String>,
1011 pub is_sender_name_ambiguous: bool,
1013
1014 pub room_computed_display_name: String,
1016 pub room_avatar_url: Option<String>,
1018 pub room_canonical_alias: Option<String>,
1020 pub room_topic: Option<String>,
1022 pub room_join_rule: Option<JoinRule>,
1026 pub is_room_encrypted: Option<bool>,
1028 pub is_direct_message_room: bool,
1030 pub joined_members_count: u64,
1032 pub service_members: Vec<String>,
1034 pub active_service_members_count: u64,
1035 pub is_space: bool,
1037
1038 pub is_noisy: Option<bool>,
1043 pub has_mention: Option<bool>,
1044 pub thread_id: Option<OwnedEventId>,
1045
1046 pub actions: Option<Vec<Action>>,
1048
1049 pub room_is_dm: bool,
1051}
1052
1053impl NotificationItem {
1054 async fn new(
1055 room: &Room,
1056 raw_event: RawNotificationEvent,
1057 push_actions: Option<&[Action]>,
1058 state_events: Vec<Raw<AnyStateEvent>>,
1059 ) -> Result<Self, Error> {
1060 let event = match &raw_event {
1061 RawNotificationEvent::Timeline(raw_event) => {
1062 let mut event = raw_event.deserialize().map_err(|_| Error::InvalidRumaEvent)?;
1063 if let AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
1064 SyncRoomMessageEvent::Original(ev),
1065 )) = &mut event
1066 {
1067 ev.content.sanitize(DEFAULT_SANITIZER_MODE, RemoveReplyFallback::Yes);
1068 }
1069 NotificationEvent::Timeline(Box::new(event))
1070 }
1071 RawNotificationEvent::Invite(raw_event) => NotificationEvent::Invite(Box::new(
1072 raw_event.deserialize().map_err(|_| Error::InvalidRumaEvent)?,
1073 )),
1074 };
1075
1076 let sender = match room.state() {
1077 RoomState::Invited => room.invite_details().await?.inviter,
1078 _ => room.get_member_no_sync(event.sender()).await?,
1079 };
1080
1081 let (mut sender_display_name, mut sender_avatar_url, is_sender_name_ambiguous) =
1082 match &sender {
1083 Some(sender) => (
1084 sender.display_name().map(|s| s.to_owned()),
1085 sender.avatar_url().map(|s| s.to_string()),
1086 sender.name_ambiguous(),
1087 ),
1088 None => (None, None, false),
1089 };
1090
1091 if sender_display_name.is_none() || sender_avatar_url.is_none() {
1092 let sender_id = event.sender();
1093 for ev in state_events {
1094 let ev = match ev.deserialize() {
1095 Ok(ev) => ev,
1096 Err(err) => {
1097 warn!("Failed to deserialize a state event: {err}");
1098 continue;
1099 }
1100 };
1101 if ev.sender() != sender_id {
1102 continue;
1103 }
1104 if let AnyStateEventContentChange::RoomMember(StateEventContentChange::Original {
1105 content,
1106 ..
1107 }) = ev.content_change()
1108 {
1109 if sender_display_name.is_none() {
1110 sender_display_name = content.displayname;
1111 }
1112 if sender_avatar_url.is_none() {
1113 sender_avatar_url = content.avatar_url.map(|url| url.to_string());
1114 }
1115 }
1116 }
1117 }
1118
1119 let is_noisy = push_actions.map(|actions| actions.iter().any(|a| a.sound().is_some()));
1120 let has_mention = push_actions.map(|actions| actions.iter().any(|a| a.is_highlight()));
1121 let thread_id = event.thread_id().clone();
1122 let service_members = room
1123 .service_members()
1124 .unwrap_or_default()
1125 .iter()
1126 .map(ToString::to_string)
1127 .collect_vec();
1128
1129 let active_service_members_count =
1130 room.update_active_service_members().await?.unwrap_or_default().len() as u64;
1131
1132 let item = NotificationItem {
1133 event,
1134 raw_event,
1135 sender_display_name,
1136 sender_avatar_url,
1137 is_sender_name_ambiguous,
1138 room_computed_display_name: room.display_name().await?.to_string(),
1139 room_avatar_url: room.avatar_url().map(|s| s.to_string()),
1140 room_canonical_alias: room.canonical_alias().map(|c| c.to_string()),
1141 room_topic: room.topic(),
1142 room_join_rule: room.join_rule(),
1143 is_direct_message_room: room.is_direct().await?,
1144 is_room_encrypted: room
1145 .latest_encryption_state()
1146 .await
1147 .map(|state| state.is_encrypted())
1148 .ok(),
1149 joined_members_count: room.joined_members_count(),
1150 service_members,
1151 active_service_members_count,
1152 is_space: room.is_space(),
1153 is_noisy,
1154 has_mention,
1155 thread_id,
1156 actions: push_actions.map(|actions| actions.to_vec()),
1157 room_is_dm: room.compute_is_dm().await?,
1158 };
1159
1160 Ok(item)
1161 }
1162
1163 pub fn is_public(&self) -> Option<bool> {
1167 self.room_join_rule.as_ref().map(|rule| matches!(rule, JoinRule::Public))
1168 }
1169}
1170
1171#[derive(Debug, Error)]
1173pub enum Error {
1174 #[error(transparent)]
1175 BuildingLocalClient(ClientBuildError),
1176
1177 #[error("unknown room for a notification")]
1179 UnknownRoom,
1180
1181 #[error("invalid ruma event")]
1183 InvalidRumaEvent,
1184
1185 #[error("the sliding sync response doesn't include the target room")]
1188 SlidingSyncEmptyRoom,
1189
1190 #[error("the event was missing in the `/context` query")]
1191 ContextMissingEvent,
1192
1193 #[error(transparent)]
1195 SdkError(#[from] matrix_sdk::Error),
1196
1197 #[error(transparent)]
1199 StoreError(#[from] StoreError),
1200}
1201
1202#[cfg(test)]
1203mod tests {
1204 use std::collections::BTreeMap;
1205
1206 use assert_matches2::assert_let;
1207 use matrix_sdk::test_utils::mocks::MatrixMockServer;
1208 use matrix_sdk_test::{ALICE, async_test, event_factory::EventFactory};
1209 use ruma::{
1210 api::client::sync::sync_events::v5,
1211 assign, event_id,
1212 events::room::{member::MembershipState, message::RedactedRoomMessageEventContent},
1213 owned_event_id, owned_room_id, room_id, user_id,
1214 };
1215
1216 use crate::notification_client::{
1217 NotificationClient, NotificationItem, NotificationItemsRequest, NotificationProcessSetup,
1218 NotificationStatus, RawNotificationEvent,
1219 };
1220
1221 #[async_test]
1222 async fn test_notification_item_returns_thread_id() {
1223 let server = MatrixMockServer::new().await;
1224 let client = server.client_builder().build().await;
1225
1226 let room_id = room_id!("!a:b.c");
1227 let thread_root_event_id = event_id!("$root:b.c");
1228 let message = EventFactory::new()
1229 .room(room_id)
1230 .sender(user_id!("@sender:b.c"))
1231 .text_msg("Threaded")
1232 .in_thread(thread_root_event_id, event_id!("$prev:b.c"))
1233 .into_raw_sync();
1234 let room = server.sync_joined_room(&client, room_id).await;
1235
1236 let raw_notification_event = RawNotificationEvent::Timeline(message);
1237 let notification_item =
1238 NotificationItem::new(&room, raw_notification_event, None, Vec::new())
1239 .await
1240 .expect("Could not create notification item");
1241
1242 assert_let!(Some(thread_id) = notification_item.thread_id);
1243 assert_eq!(thread_id, thread_root_event_id);
1244 }
1245
1246 #[async_test]
1247 async fn test_try_sliding_sync_ignores_invites_for_non_subscribed_rooms() {
1248 let server = MatrixMockServer::new().await;
1249 let client = server.client_builder().build().await;
1250
1251 let user_id = client.user_id().unwrap();
1252 let room_id = room_id!("!a:b.c");
1253 let invite = EventFactory::new()
1254 .room(room_id)
1255 .member(user_id)
1256 .membership(MembershipState::Invite)
1257 .no_event_id()
1258 .into_raw_sync_state();
1259 let mut room = v5::response::Room::new();
1260 room.invite_state = Some(vec![invite.cast_unchecked()]);
1261 let rooms = BTreeMap::from_iter([(room_id.to_owned(), room)]);
1262 server
1263 .mock_sliding_sync()
1264 .ok(assign!(v5::Response::new("1".to_owned()), {
1265 rooms: rooms,
1266 }))
1267 .mount()
1268 .await;
1269
1270 let notification_client =
1271 NotificationClient::new(client.clone(), NotificationProcessSetup::MultipleProcesses)
1272 .await
1273 .expect("Could not create a notification client");
1274
1275 let event_id = owned_event_id!("$a:b.c");
1278 let result = notification_client
1279 .try_sliding_sync(&[NotificationItemsRequest {
1280 room_id: owned_room_id!("!other:b.c"),
1281 event_ids: vec![event_id.clone()],
1282 }])
1283 .await
1284 .expect("Could not run sliding sync");
1285
1286 assert!(result.is_empty());
1287
1288 let result = notification_client
1290 .try_sliding_sync(&[NotificationItemsRequest {
1291 room_id: room_id.to_owned(),
1292 event_ids: vec![event_id.clone()],
1293 }])
1294 .await
1295 .expect("Could not run sliding sync");
1296
1297 assert!(!result.is_empty());
1299
1300 let (in_room_id, event) = &result[&event_id];
1303 assert_eq!(room_id, in_room_id);
1304 assert_let!(Some(RawNotificationEvent::Invite(raw_invite)) = event);
1305
1306 let invite = raw_invite.deserialize().expect("Could not deserialize invite event");
1307 assert_eq!(invite.state_key, user_id.to_string());
1308 assert_eq!(invite.content.membership, MembershipState::Invite);
1309 }
1310
1311 #[async_test]
1312 async fn test_redacted_event_returns_event_redacted_status() {
1313 let server = MatrixMockServer::new().await;
1314 let client = server.client_builder().build().await;
1315
1316 let room_id = room_id!("!a:b.c");
1317
1318 let event_id = owned_event_id!("$redacted:b.c");
1320 let redacted_event = EventFactory::new()
1321 .room(room_id)
1322 .sender(user_id!("@sender:b.c"))
1323 .redacted(&ALICE, RedactedRoomMessageEventContent::new())
1324 .event_id(&event_id)
1325 .into_raw();
1326 let mut room = v5::response::Room::new();
1327 room.timeline = vec![redacted_event];
1328
1329 let mut rooms = BTreeMap::new();
1330 rooms.insert(room_id.to_owned(), room);
1331
1332 server
1333 .mock_sliding_sync()
1334 .ok(assign!(v5::Response::new("1".to_owned()), {
1335 rooms: rooms,
1336 }))
1337 .mount()
1338 .await;
1339
1340 let notification_client =
1341 NotificationClient::new(client.clone(), NotificationProcessSetup::MultipleProcesses)
1342 .await
1343 .expect("Could not create a notification client");
1344
1345 let result: NotificationStatus = notification_client
1346 .get_notification_with_sliding_sync(room_id, &event_id)
1347 .await
1348 .expect("Could not get notification");
1349
1350 match result {
1351 NotificationStatus::EventRedacted => {
1352 }
1354 other => panic!("Expected EventRedacted, got {:?}", other),
1355 }
1356 }
1357}