1#[cfg(feature = "e2e-encryption")]
16use std::sync::RwLock as SyncRwLock;
17use std::{
18 collections::{BTreeMap, BTreeSet, HashSet},
19 mem,
20 sync::{atomic::AtomicBool, Arc},
21};
22
23use as_variant::as_variant;
24use bitflags::bitflags;
25use eyeball::{AsyncLock, ObservableWriteGuard, SharedObservable, Subscriber};
26use futures_util::{Stream, StreamExt};
27use matrix_sdk_common::deserialized_responses::TimelineEventKind;
28#[cfg(feature = "e2e-encryption")]
29use matrix_sdk_common::ring_buffer::RingBuffer;
30use ruma::{
31 api::client::sync::sync_events::v3::RoomSummary as RumaSummary,
32 events::{
33 call::member::{CallMemberStateKey, MembershipData},
34 direct::OwnedDirectUserIdentifier,
35 ignored_user_list::IgnoredUserListEventContent,
36 member_hints::MemberHintsEventContent,
37 receipt::{Receipt, ReceiptThread, ReceiptType},
38 room::{
39 avatar::{self, RoomAvatarEventContent},
40 encryption::RoomEncryptionEventContent,
41 guest_access::GuestAccess,
42 history_visibility::HistoryVisibility,
43 join_rules::JoinRule,
44 member::{MembershipState, RoomMemberEventContent},
45 pinned_events::RoomPinnedEventsEventContent,
46 power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
47 redaction::SyncRoomRedactionEvent,
48 tombstone::RoomTombstoneEventContent,
49 },
50 tag::{TagEventContent, Tags},
51 AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent,
52 RoomAccountDataEventType, StateEventType, SyncStateEvent,
53 },
54 room::RoomType,
55 serde::Raw,
56 EventId, MxcUri, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId,
57 RoomAliasId, RoomId, RoomVersionId, UserId,
58};
59use serde::{Deserialize, Serialize};
60use tokio::sync::broadcast;
61use tracing::{debug, field::debug, info, instrument, trace, warn};
62
63use super::{
64 members::MemberRoomInfo, BaseRoomInfo, RoomCreateWithCreatorEventContent, RoomDisplayName,
65 RoomMember, RoomNotableTags,
66};
67use crate::{
68 deserialized_responses::{
69 DisplayName, MemberEvent, RawMemberEvent, RawSyncOrStrippedState, SyncOrStrippedState,
70 },
71 latest_event::LatestEvent,
72 notification_settings::RoomNotificationMode,
73 read_receipts::RoomReadReceipts,
74 store::{DynStateStore, Result as StoreResult, StateStoreExt},
75 sync::UnreadNotificationsCount,
76 Error, MinimalStateEvent, OriginalMinimalStateEvent, RoomMemberships, StateStoreDataKey,
77 StateStoreDataValue, StoreError,
78};
79
80#[derive(Debug, Clone)]
90pub struct RoomInfoNotableUpdate {
91 pub room_id: OwnedRoomId,
93
94 pub reasons: RoomInfoNotableUpdateReasons,
96}
97
98bitflags! {
99 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
101 pub struct RoomInfoNotableUpdateReasons: u8 {
102 const RECENCY_STAMP = 0b0000_0001;
104
105 const LATEST_EVENT = 0b0000_0010;
107
108 const READ_RECEIPT = 0b0000_0100;
110
111 const UNREAD_MARKER = 0b0000_1000;
113
114 const MEMBERSHIP = 0b0001_0000;
116 }
117}
118
119struct ComputedSummary {
126 heroes: Vec<String>,
129 num_service_members: u64,
131 num_joined_invited_guess: u64,
134}
135
136impl Default for RoomInfoNotableUpdateReasons {
137 fn default() -> Self {
138 Self::empty()
139 }
140}
141
142#[derive(Debug, Clone)]
145pub struct Room {
146 room_id: OwnedRoomId,
148
149 own_user_id: OwnedUserId,
151
152 inner: SharedObservable<RoomInfo>,
153 room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
154 store: Arc<DynStateStore>,
155
156 #[cfg(feature = "e2e-encryption")]
166 pub latest_encrypted_events: Arc<SyncRwLock<RingBuffer<Raw<AnySyncTimelineEvent>>>>,
167
168 pub seen_knock_request_ids_map:
172 SharedObservable<Option<BTreeMap<OwnedEventId, OwnedUserId>>, AsyncLock>,
173
174 pub room_member_updates_sender: broadcast::Sender<RoomMembersUpdate>,
176}
177
178#[derive(Clone, Debug, Default, Serialize, Deserialize)]
181pub struct RoomSummary {
182 #[serde(default, skip_serializing_if = "Vec::is_empty")]
190 pub(crate) room_heroes: Vec<RoomHero>,
191 pub(crate) joined_member_count: u64,
193 pub(crate) invited_member_count: u64,
195}
196
197#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
199pub struct RoomHero {
200 pub user_id: OwnedUserId,
202 pub display_name: Option<String>,
204 pub avatar_url: Option<OwnedMxcUri>,
206}
207
208#[cfg(test)]
209impl RoomSummary {
210 pub(crate) fn heroes(&self) -> &[RoomHero] {
211 &self.room_heroes
212 }
213}
214
215#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
218pub enum RoomState {
219 Joined,
221 Left,
223 Invited,
225 Knocked,
227 Banned,
229}
230
231impl From<&MembershipState> for RoomState {
232 fn from(membership_state: &MembershipState) -> Self {
233 match membership_state {
234 MembershipState::Ban => Self::Banned,
235 MembershipState::Invite => Self::Invited,
236 MembershipState::Join => Self::Joined,
237 MembershipState::Knock => Self::Knocked,
238 MembershipState::Leave => Self::Left,
239 _ => panic!("Unexpected MembershipState: {}", membership_state),
240 }
241 }
242}
243
244const NUM_HEROES: usize = 5;
251
252fn heroes_filter<'a>(
258 own_user_id: &'a UserId,
259 member_hints: &'a MemberHintsEventContent,
260) -> impl Fn(&UserId) -> bool + use<'a> {
261 move |user_id| user_id != own_user_id && !member_hints.service_members.contains(user_id)
262}
263
264#[derive(Debug, Clone)]
266pub enum RoomMembersUpdate {
267 FullReload,
269 Partial(BTreeSet<OwnedUserId>),
271}
272
273impl Room {
274 #[cfg(feature = "e2e-encryption")]
276 const MAX_ENCRYPTED_EVENTS: std::num::NonZeroUsize = std::num::NonZeroUsize::new(10).unwrap();
277
278 pub(crate) fn new(
279 own_user_id: &UserId,
280 store: Arc<DynStateStore>,
281 room_id: &RoomId,
282 room_state: RoomState,
283 room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
284 ) -> Self {
285 let room_info = RoomInfo::new(room_id, room_state);
286 Self::restore(own_user_id, store, room_info, room_info_notable_update_sender)
287 }
288
289 pub(crate) fn restore(
290 own_user_id: &UserId,
291 store: Arc<DynStateStore>,
292 room_info: RoomInfo,
293 room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
294 ) -> Self {
295 let (room_member_updates_sender, _) = broadcast::channel(10);
296 Self {
297 own_user_id: own_user_id.into(),
298 room_id: room_info.room_id.clone(),
299 store,
300 inner: SharedObservable::new(room_info),
301 #[cfg(feature = "e2e-encryption")]
302 latest_encrypted_events: Arc::new(SyncRwLock::new(RingBuffer::new(
303 Self::MAX_ENCRYPTED_EVENTS,
304 ))),
305 room_info_notable_update_sender,
306 seen_knock_request_ids_map: SharedObservable::new_async(None),
307 room_member_updates_sender,
308 }
309 }
310
311 pub fn room_id(&self) -> &RoomId {
313 &self.room_id
314 }
315
316 pub fn creator(&self) -> Option<OwnedUserId> {
318 self.inner.read().creator().map(ToOwned::to_owned)
319 }
320
321 pub fn own_user_id(&self) -> &UserId {
323 &self.own_user_id
324 }
325
326 pub fn state(&self) -> RoomState {
328 self.inner.read().room_state
329 }
330
331 pub fn prev_state(&self) -> Option<RoomState> {
333 self.inner.read().prev_room_state
334 }
335
336 pub fn is_space(&self) -> bool {
338 self.inner.read().room_type().is_some_and(|t| *t == RoomType::Space)
339 }
340
341 pub fn room_type(&self) -> Option<RoomType> {
344 self.inner.read().room_type().map(ToOwned::to_owned)
345 }
346
347 pub fn unread_notification_counts(&self) -> UnreadNotificationsCount {
349 self.inner.read().notification_counts
350 }
351
352 pub fn num_unread_messages(&self) -> u64 {
357 self.inner.read().read_receipts.num_unread
358 }
359
360 pub fn read_receipts(&self) -> RoomReadReceipts {
362 self.inner.read().read_receipts.clone()
363 }
364
365 pub fn num_unread_notifications(&self) -> u64 {
370 self.inner.read().read_receipts.num_notifications
371 }
372
373 pub fn num_unread_mentions(&self) -> u64 {
379 self.inner.read().read_receipts.num_mentions
380 }
381
382 pub fn are_members_synced(&self) -> bool {
389 self.inner.read().members_synced
390 }
391
392 #[cfg(feature = "testing")]
397 pub fn mark_members_synced(&self) {
398 self.inner.update(|info| {
399 info.members_synced = true;
400 });
401 }
402
403 pub fn mark_members_missing(&self) {
405 self.inner.update_if(|info| {
406 mem::replace(&mut info.members_synced, false)
408 })
409 }
410
411 pub fn is_state_fully_synced(&self) -> bool {
419 self.inner.read().sync_info == SyncInfo::FullySynced
420 }
421
422 pub fn is_state_partially_or_fully_synced(&self) -> bool {
426 self.inner.read().sync_info != SyncInfo::NoState
427 }
428
429 pub fn is_encryption_state_synced(&self) -> bool {
436 self.inner.read().encryption_state_synced
437 }
438
439 pub fn last_prev_batch(&self) -> Option<String> {
442 self.inner.read().last_prev_batch.clone()
443 }
444
445 pub fn avatar_url(&self) -> Option<OwnedMxcUri> {
447 self.inner.read().avatar_url().map(ToOwned::to_owned)
448 }
449
450 pub fn avatar_info(&self) -> Option<avatar::ImageInfo> {
452 self.inner.read().avatar_info().map(ToOwned::to_owned)
453 }
454
455 pub fn canonical_alias(&self) -> Option<OwnedRoomAliasId> {
457 self.inner.read().canonical_alias().map(ToOwned::to_owned)
458 }
459
460 pub fn alt_aliases(&self) -> Vec<OwnedRoomAliasId> {
462 self.inner.read().alt_aliases().to_owned()
463 }
464
465 pub fn create_content(&self) -> Option<RoomCreateWithCreatorEventContent> {
475 match self.inner.read().base_info.create.as_ref()? {
476 MinimalStateEvent::Original(ev) => Some(ev.content.clone()),
477 MinimalStateEvent::Redacted(ev) => Some(ev.content.clone()),
478 }
479 }
480
481 #[instrument(skip_all, fields(room_id = ?self.room_id))]
485 pub async fn is_direct(&self) -> StoreResult<bool> {
486 match self.state() {
487 RoomState::Joined | RoomState::Left | RoomState::Banned => {
488 Ok(!self.inner.read().base_info.dm_targets.is_empty())
489 }
490
491 RoomState::Invited => {
492 let member = self.get_member(self.own_user_id()).await?;
493
494 match member {
495 None => {
496 info!("RoomMember not found for the user's own id");
497 Ok(false)
498 }
499 Some(member) => match member.event.as_ref() {
500 MemberEvent::Sync(_) => {
501 warn!("Got MemberEvent::Sync in an invited room");
502 Ok(false)
503 }
504 MemberEvent::Stripped(event) => {
505 Ok(event.content.is_direct.unwrap_or(false))
506 }
507 },
508 }
509 }
510
511 RoomState::Knocked => Ok(false),
513 }
514 }
515
516 pub fn direct_targets(&self) -> HashSet<OwnedDirectUserIdentifier> {
525 self.inner.read().base_info.dm_targets.clone()
526 }
527
528 pub fn direct_targets_length(&self) -> usize {
531 self.inner.read().base_info.dm_targets.len()
532 }
533
534 pub fn is_encrypted(&self) -> bool {
536 self.inner.read().is_encrypted()
537 }
538
539 pub fn encryption_settings(&self) -> Option<RoomEncryptionEventContent> {
542 self.inner.read().base_info.encryption.clone()
543 }
544
545 pub fn guest_access(&self) -> GuestAccess {
547 self.inner.read().guest_access().clone()
548 }
549
550 pub fn history_visibility(&self) -> Option<HistoryVisibility> {
552 self.inner.read().history_visibility().cloned()
553 }
554
555 pub fn history_visibility_or_default(&self) -> HistoryVisibility {
558 self.inner.read().history_visibility_or_default().clone()
559 }
560
561 pub fn is_public(&self) -> bool {
563 matches!(self.join_rule(), JoinRule::Public)
564 }
565
566 pub fn join_rule(&self) -> JoinRule {
568 self.inner.read().join_rule().clone()
569 }
570
571 pub fn max_power_level(&self) -> i64 {
576 self.inner.read().base_info.max_power_level
577 }
578
579 pub async fn power_levels(&self) -> Result<RoomPowerLevels, Error> {
581 Ok(self
582 .store
583 .get_state_event_static::<RoomPowerLevelsEventContent>(self.room_id())
584 .await?
585 .ok_or(Error::InsufficientData)?
586 .deserialize()?
587 .power_levels())
588 }
589
590 pub fn name(&self) -> Option<String> {
595 self.inner.read().name().map(ToOwned::to_owned)
596 }
597
598 pub fn is_tombstoned(&self) -> bool {
600 self.inner.read().base_info.tombstone.is_some()
601 }
602
603 pub fn tombstone(&self) -> Option<RoomTombstoneEventContent> {
605 self.inner.read().tombstone().cloned()
606 }
607
608 pub fn topic(&self) -> Option<String> {
610 self.inner.read().topic().map(ToOwned::to_owned)
611 }
612
613 pub fn has_active_room_call(&self) -> bool {
616 self.inner.read().has_active_room_call()
617 }
618
619 pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
628 self.inner.read().active_room_call_participants()
629 }
630
631 pub async fn display_name(&self) -> StoreResult<RoomDisplayName> {
646 if let Some(name) = self.cached_display_name() {
647 Ok(name)
648 } else {
649 self.compute_display_name().await
650 }
651 }
652
653 pub(crate) async fn compute_display_name(&self) -> StoreResult<RoomDisplayName> {
665 enum DisplayNameOrSummary {
666 Summary(RoomSummary),
667 DisplayName(RoomDisplayName),
668 }
669
670 let display_name_or_summary = {
671 let inner = self.inner.read();
672
673 match (inner.name(), inner.canonical_alias()) {
674 (Some(name), _) => {
675 let name = RoomDisplayName::Named(name.trim().to_owned());
676 DisplayNameOrSummary::DisplayName(name)
677 }
678 (None, Some(alias)) => {
679 let name = RoomDisplayName::Aliased(alias.alias().trim().to_owned());
680 DisplayNameOrSummary::DisplayName(name)
681 }
682 (None, None) => DisplayNameOrSummary::Summary(inner.summary.clone()),
687 }
688 };
689
690 let display_name = match display_name_or_summary {
691 DisplayNameOrSummary::Summary(summary) => {
692 self.compute_display_name_from_summary(summary).await?
693 }
694 DisplayNameOrSummary::DisplayName(display_name) => display_name,
695 };
696
697 self.inner.update_if(|info| {
699 if info.cached_display_name.as_ref() != Some(&display_name) {
700 info.cached_display_name = Some(display_name.clone());
701 true
702 } else {
703 false
704 }
705 });
706
707 Ok(display_name)
708 }
709
710 async fn compute_display_name_from_summary(
712 &self,
713 summary: RoomSummary,
714 ) -> StoreResult<RoomDisplayName> {
715 let computed_summary = if !summary.room_heroes.is_empty() {
716 self.extract_and_augment_summary(&summary).await?
717 } else {
718 self.compute_summary().await?
719 };
720
721 let ComputedSummary { heroes, num_service_members, num_joined_invited_guess } =
722 computed_summary;
723
724 let summary_member_count = (summary.joined_member_count + summary.invited_member_count)
725 .saturating_sub(num_service_members);
726
727 let num_joined_invited = if self.state() == RoomState::Invited {
728 heroes.len() as u64 + 1
731 } else if summary_member_count == 0 {
732 num_joined_invited_guess
733 } else {
734 summary_member_count
735 };
736
737 debug!(
738 room_id = ?self.room_id(),
739 own_user = ?self.own_user_id,
740 num_joined_invited,
741 heroes = ?heroes,
742 "Calculating name for a room based on heroes",
743 );
744
745 let display_name = compute_display_name_from_heroes(
746 num_joined_invited,
747 heroes.iter().map(|hero| hero.as_str()).collect(),
748 );
749
750 Ok(display_name)
751 }
752
753 async fn extract_and_augment_summary(
762 &self,
763 summary: &RoomSummary,
764 ) -> StoreResult<ComputedSummary> {
765 let heroes = &summary.room_heroes;
766
767 let mut names = Vec::with_capacity(heroes.len());
768 let own_user_id = self.own_user_id();
769 let member_hints = self.get_member_hints().await?;
770
771 let num_service_members = heroes
776 .iter()
777 .filter(|hero| member_hints.service_members.contains(&hero.user_id))
778 .count() as u64;
779
780 let heroes_filter = heroes_filter(own_user_id, &member_hints);
783 let heroes_filter = |hero: &&RoomHero| heroes_filter(&hero.user_id);
784
785 for hero in heroes.iter().filter(heroes_filter) {
786 if let Some(display_name) = &hero.display_name {
787 names.push(display_name.clone());
788 } else {
789 match self.get_member(&hero.user_id).await {
790 Ok(Some(member)) => {
791 names.push(member.name().to_owned());
792 }
793 Ok(None) => {
794 warn!("Ignoring hero, no member info for {}", hero.user_id);
795 }
796 Err(error) => {
797 warn!("Ignoring hero, error getting member: {}", error);
798 }
799 }
800 }
801 }
802
803 let num_joined_invited_guess = summary.joined_member_count + summary.invited_member_count;
804
805 let num_joined_invited_guess = if num_joined_invited_guess == 0 {
808 let guess = self
809 .store
810 .get_user_ids(self.room_id(), RoomMemberships::JOIN | RoomMemberships::INVITE)
811 .await?
812 .len() as u64;
813
814 guess.saturating_sub(num_service_members)
815 } else {
816 num_joined_invited_guess
818 };
819
820 Ok(ComputedSummary { heroes: names, num_service_members, num_joined_invited_guess })
821 }
822
823 async fn compute_summary(&self) -> StoreResult<ComputedSummary> {
829 let member_hints = self.get_member_hints().await?;
830
831 let heroes_filter = heroes_filter(&self.own_user_id, &member_hints);
834 let heroes_filter = |u: &RoomMember| heroes_filter(u.user_id());
835
836 let mut members = self.members(RoomMemberships::JOIN | RoomMemberships::INVITE).await?;
837
838 let num_service_members = members
842 .iter()
843 .filter(|member| member_hints.service_members.contains(member.user_id()))
844 .count();
845
846 let num_joined_invited = members.len() - num_service_members;
853
854 if num_joined_invited == 0
855 || (num_joined_invited == 1 && members[0].user_id() == self.own_user_id)
856 {
857 members = self.members(RoomMemberships::LEAVE | RoomMemberships::BAN).await?;
859 }
860
861 members.sort_unstable_by(|lhs, rhs| lhs.name().cmp(rhs.name()));
863
864 let heroes = members
865 .into_iter()
866 .filter(heroes_filter)
867 .take(NUM_HEROES)
868 .map(|u| u.name().to_owned())
869 .collect();
870
871 trace!(
872 ?heroes,
873 num_joined_invited,
874 num_service_members,
875 "Computed a room summary since we didn't receive one."
876 );
877
878 let num_service_members = num_service_members as u64;
879 let num_joined_invited_guess = num_joined_invited as u64;
880
881 Ok(ComputedSummary { heroes, num_service_members, num_joined_invited_guess })
882 }
883
884 async fn get_member_hints(&self) -> StoreResult<MemberHintsEventContent> {
885 Ok(self
886 .store
887 .get_state_event_static::<MemberHintsEventContent>(self.room_id())
888 .await?
889 .and_then(|event| {
890 event
891 .deserialize()
892 .inspect_err(|e| warn!("Couldn't deserialize the member hints event: {e}"))
893 .ok()
894 })
895 .and_then(|event| as_variant!(event, SyncOrStrippedState::Sync(SyncStateEvent::Original(e)) => e.content))
896 .unwrap_or_default())
897 }
898
899 pub fn cached_display_name(&self) -> Option<RoomDisplayName> {
903 self.inner.read().cached_display_name.clone()
904 }
905
906 pub fn update_cached_user_defined_notification_mode(&self, mode: RoomNotificationMode) {
912 self.inner.update_if(|info| {
913 if info.cached_user_defined_notification_mode.as_ref() != Some(&mode) {
914 info.cached_user_defined_notification_mode = Some(mode);
915
916 true
917 } else {
918 false
919 }
920 });
921 }
922
923 pub fn cached_user_defined_notification_mode(&self) -> Option<RoomNotificationMode> {
928 self.inner.read().cached_user_defined_notification_mode
929 }
930
931 pub fn latest_event(&self) -> Option<LatestEvent> {
934 self.inner.read().latest_event.as_deref().cloned()
935 }
936
937 #[cfg(feature = "e2e-encryption")]
942 pub(crate) fn latest_encrypted_events(&self) -> Vec<Raw<AnySyncTimelineEvent>> {
943 self.latest_encrypted_events.read().unwrap().iter().cloned().collect()
944 }
945
946 #[cfg(feature = "e2e-encryption")]
957 pub(crate) fn on_latest_event_decrypted(
958 &self,
959 latest_event: Box<LatestEvent>,
960 index: usize,
961 changes: &mut crate::StateChanges,
962 room_info_notable_updates: &mut BTreeMap<OwnedRoomId, RoomInfoNotableUpdateReasons>,
963 ) {
964 self.latest_encrypted_events.write().unwrap().drain(0..=index);
965
966 let room_info = changes
967 .room_infos
968 .entry(self.room_id().to_owned())
969 .or_insert_with(|| self.clone_info());
970
971 room_info.latest_event = Some(latest_event);
972
973 room_info_notable_updates
974 .entry(self.room_id().to_owned())
975 .or_default()
976 .insert(RoomInfoNotableUpdateReasons::LATEST_EVENT);
977 }
978
979 pub async fn joined_user_ids(&self) -> StoreResult<Vec<OwnedUserId>> {
982 self.store.get_user_ids(self.room_id(), RoomMemberships::JOIN).await
983 }
984
985 pub async fn members(&self, memberships: RoomMemberships) -> StoreResult<Vec<RoomMember>> {
988 let user_ids = self.store.get_user_ids(self.room_id(), memberships).await?;
989
990 if user_ids.is_empty() {
991 return Ok(Vec::new());
992 }
993
994 let member_events = self
995 .store
996 .get_state_events_for_keys_static::<RoomMemberEventContent, _, _>(
997 self.room_id(),
998 &user_ids,
999 )
1000 .await?
1001 .into_iter()
1002 .map(|raw_event| raw_event.deserialize())
1003 .collect::<Result<Vec<_>, _>>()?;
1004
1005 let mut profiles = self.store.get_profiles(self.room_id(), &user_ids).await?;
1006
1007 let mut presences = self
1008 .store
1009 .get_presence_events(&user_ids)
1010 .await?
1011 .into_iter()
1012 .filter_map(|e| {
1013 e.deserialize().ok().map(|presence| (presence.sender.clone(), presence))
1014 })
1015 .collect::<BTreeMap<_, _>>();
1016
1017 let display_names = member_events.iter().map(|e| e.display_name()).collect::<Vec<_>>();
1018 let room_info = self.member_room_info(&display_names).await?;
1019
1020 let mut members = Vec::new();
1021
1022 for event in member_events {
1023 let profile = profiles.remove(event.user_id());
1024 let presence = presences.remove(event.user_id());
1025 members.push(RoomMember::from_parts(event, profile, presence, &room_info))
1026 }
1027
1028 Ok(members)
1029 }
1030
1031 pub fn heroes(&self) -> Vec<RoomHero> {
1033 self.inner.read().heroes().to_vec()
1034 }
1035
1036 pub fn active_members_count(&self) -> u64 {
1039 self.inner.read().active_members_count()
1040 }
1041
1042 pub fn invited_members_count(&self) -> u64 {
1044 self.inner.read().invited_members_count()
1045 }
1046
1047 pub fn joined_members_count(&self) -> u64 {
1049 self.inner.read().joined_members_count()
1050 }
1051
1052 pub fn subscribe_info(&self) -> Subscriber<RoomInfo> {
1054 self.inner.subscribe()
1055 }
1056
1057 pub fn clone_info(&self) -> RoomInfo {
1059 self.inner.get()
1060 }
1061
1062 pub fn set_room_info(
1064 &self,
1065 room_info: RoomInfo,
1066 room_info_notable_update_reasons: RoomInfoNotableUpdateReasons,
1067 ) {
1068 self.inner.set(room_info);
1069
1070 let _ = self.room_info_notable_update_sender.send(RoomInfoNotableUpdate {
1072 room_id: self.room_id.clone(),
1073 reasons: room_info_notable_update_reasons,
1074 });
1075 }
1076
1077 pub async fn get_member(&self, user_id: &UserId) -> StoreResult<Option<RoomMember>> {
1085 let Some(raw_event) = self.store.get_member_event(self.room_id(), user_id).await? else {
1086 debug!(%user_id, "Member event not found in state store");
1087 return Ok(None);
1088 };
1089
1090 let event = raw_event.deserialize()?;
1091
1092 let presence =
1093 self.store.get_presence_event(user_id).await?.and_then(|e| e.deserialize().ok());
1094
1095 let profile = self.store.get_profile(self.room_id(), user_id).await?;
1096
1097 let display_names = [event.display_name()];
1098 let room_info = self.member_room_info(&display_names).await?;
1099
1100 Ok(Some(RoomMember::from_parts(event, profile, presence, &room_info)))
1101 }
1102
1103 async fn member_room_info<'a>(
1107 &self,
1108 display_names: &'a [DisplayName],
1109 ) -> StoreResult<MemberRoomInfo<'a>> {
1110 let max_power_level = self.max_power_level();
1111 let room_creator = self.inner.read().creator().map(ToOwned::to_owned);
1112
1113 let power_levels = self
1114 .store
1115 .get_state_event_static(self.room_id())
1116 .await?
1117 .and_then(|e| e.deserialize().ok());
1118
1119 let users_display_names =
1120 self.store.get_users_with_display_names(self.room_id(), display_names).await?;
1121
1122 let ignored_users = self
1123 .store
1124 .get_account_data_event_static::<IgnoredUserListEventContent>()
1125 .await?
1126 .map(|c| c.deserialize())
1127 .transpose()?
1128 .map(|e| e.content.ignored_users.into_keys().collect());
1129
1130 Ok(MemberRoomInfo {
1131 power_levels: power_levels.into(),
1132 max_power_level,
1133 room_creator,
1134 users_display_names,
1135 ignored_users,
1136 })
1137 }
1138
1139 pub async fn tags(&self) -> StoreResult<Option<Tags>> {
1141 if let Some(AnyRoomAccountDataEvent::Tag(event)) = self
1142 .store
1143 .get_room_account_data_event(self.room_id(), RoomAccountDataEventType::Tag)
1144 .await?
1145 .and_then(|r| r.deserialize().ok())
1146 {
1147 Ok(Some(event.content.tags))
1148 } else {
1149 Ok(None)
1150 }
1151 }
1152
1153 pub fn is_favourite(&self) -> bool {
1157 self.inner.read().base_info.notable_tags.contains(RoomNotableTags::FAVOURITE)
1158 }
1159
1160 pub fn is_low_priority(&self) -> bool {
1165 self.inner.read().base_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY)
1166 }
1167
1168 pub async fn load_user_receipt(
1171 &self,
1172 receipt_type: ReceiptType,
1173 thread: ReceiptThread,
1174 user_id: &UserId,
1175 ) -> StoreResult<Option<(OwnedEventId, Receipt)>> {
1176 self.store.get_user_room_receipt_event(self.room_id(), receipt_type, thread, user_id).await
1177 }
1178
1179 pub async fn load_event_receipts(
1183 &self,
1184 receipt_type: ReceiptType,
1185 thread: ReceiptThread,
1186 event_id: &EventId,
1187 ) -> StoreResult<Vec<(OwnedUserId, Receipt)>> {
1188 self.store
1189 .get_event_room_receipt_events(self.room_id(), receipt_type, thread, event_id)
1190 .await
1191 }
1192
1193 pub fn is_marked_unread(&self) -> bool {
1196 self.inner.read().base_info.is_marked_unread
1197 }
1198
1199 pub fn recency_stamp(&self) -> Option<u64> {
1203 self.inner.read().recency_stamp
1204 }
1205
1206 pub fn pinned_event_ids_stream(&self) -> impl Stream<Item = Vec<OwnedEventId>> {
1209 self.inner
1210 .subscribe()
1211 .map(|i| i.base_info.pinned_events.map(|c| c.pinned).unwrap_or_default())
1212 }
1213
1214 pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
1216 self.inner.read().pinned_event_ids()
1217 }
1218
1219 pub async fn mark_knock_requests_as_seen(&self, user_ids: &[OwnedUserId]) -> StoreResult<()> {
1222 let raw_user_ids: Vec<&str> = user_ids.iter().map(|id| id.as_str()).collect();
1223 let member_raw_events = self
1224 .store
1225 .get_state_events_for_keys(self.room_id(), StateEventType::RoomMember, &raw_user_ids)
1226 .await?;
1227 let mut event_to_user_ids = Vec::with_capacity(member_raw_events.len());
1228
1229 for raw_event in member_raw_events {
1232 let event = raw_event.cast::<RoomMemberEventContent>().deserialize()?;
1233 match event {
1234 SyncOrStrippedState::Sync(SyncStateEvent::Original(event)) => {
1235 if event.content.membership == MembershipState::Knock {
1236 event_to_user_ids.push((event.event_id, event.state_key))
1237 } else {
1238 warn!("Could not mark knock event as seen: event {} for user {} is not in Knock membership state.", event.event_id, event.state_key);
1239 }
1240 }
1241 _ => warn!(
1242 "Could not mark knock event as seen: event for user {} is not valid.",
1243 event.state_key()
1244 ),
1245 }
1246 }
1247
1248 let current_seen_events_guard = self.get_write_guarded_current_knock_request_ids().await?;
1249 let mut current_seen_events = current_seen_events_guard.clone().unwrap_or_default();
1250
1251 current_seen_events.extend(event_to_user_ids);
1252
1253 self.update_seen_knock_request_ids(current_seen_events_guard, current_seen_events).await?;
1254
1255 Ok(())
1256 }
1257
1258 pub async fn remove_outdated_seen_knock_requests_ids(&self) -> StoreResult<()> {
1261 let current_seen_events_guard = self.get_write_guarded_current_knock_request_ids().await?;
1262 let mut current_seen_events = current_seen_events_guard.clone().unwrap_or_default();
1263
1264 let keys: Vec<OwnedUserId> = current_seen_events.values().map(|id| id.to_owned()).collect();
1266 let raw_member_events: Vec<RawMemberEvent> =
1267 self.store.get_state_events_for_keys_static(self.room_id(), &keys).await?;
1268 let member_events = raw_member_events
1269 .into_iter()
1270 .map(|raw| raw.deserialize())
1271 .collect::<Result<Vec<MemberEvent>, _>>()?;
1272
1273 let mut ids_to_remove = Vec::new();
1274
1275 for (event_id, user_id) in current_seen_events.iter() {
1276 let matching_member = member_events.iter().find(|event| event.user_id() == user_id);
1279
1280 if let Some(member) = matching_member {
1281 let member_event_id = member.event_id();
1282 if *member.membership() != MembershipState::Knock
1284 || member_event_id.is_some_and(|id| id != event_id)
1285 {
1286 ids_to_remove.push(event_id.to_owned());
1287 }
1288 } else {
1289 ids_to_remove.push(event_id.to_owned());
1290 }
1291 }
1292
1293 if ids_to_remove.is_empty() {
1295 return Ok(());
1296 }
1297
1298 for event_id in ids_to_remove {
1299 current_seen_events.remove(&event_id);
1300 }
1301
1302 self.update_seen_knock_request_ids(current_seen_events_guard, current_seen_events).await?;
1303
1304 Ok(())
1305 }
1306
1307 pub async fn get_seen_knock_request_ids(
1309 &self,
1310 ) -> Result<BTreeMap<OwnedEventId, OwnedUserId>, StoreError> {
1311 Ok(self.get_write_guarded_current_knock_request_ids().await?.clone().unwrap_or_default())
1312 }
1313
1314 async fn get_write_guarded_current_knock_request_ids(
1315 &self,
1316 ) -> StoreResult<ObservableWriteGuard<'_, Option<BTreeMap<OwnedEventId, OwnedUserId>>, AsyncLock>>
1317 {
1318 let mut guard = self.seen_knock_request_ids_map.write().await;
1319 if guard.is_none() {
1321 let updated_seen_ids = self
1323 .store
1324 .get_kv_data(StateStoreDataKey::SeenKnockRequests(self.room_id()))
1325 .await?
1326 .and_then(|v| v.into_seen_knock_requests())
1327 .unwrap_or_default();
1328
1329 ObservableWriteGuard::set(&mut guard, Some(updated_seen_ids));
1330 }
1331 Ok(guard)
1332 }
1333
1334 async fn update_seen_knock_request_ids(
1335 &self,
1336 mut guard: ObservableWriteGuard<'_, Option<BTreeMap<OwnedEventId, OwnedUserId>>, AsyncLock>,
1337 new_value: BTreeMap<OwnedEventId, OwnedUserId>,
1338 ) -> StoreResult<()> {
1339 ObservableWriteGuard::set(&mut guard, Some(new_value.clone()));
1341
1342 self.store
1344 .set_kv_data(
1345 StateStoreDataKey::SeenKnockRequests(self.room_id()),
1346 StateStoreDataValue::SeenKnockRequests(new_value),
1347 )
1348 .await?;
1349
1350 Ok(())
1351 }
1352}
1353
1354#[cfg(not(feature = "test-send-sync"))]
1356unsafe impl Send for Room {}
1357
1358#[cfg(not(feature = "test-send-sync"))]
1360unsafe impl Sync for Room {}
1361
1362#[cfg(feature = "test-send-sync")]
1363#[test]
1364fn test_send_sync_for_room() {
1366 fn assert_send_sync<T: Send + Sync>() {}
1367
1368 assert_send_sync::<Room>();
1369}
1370
1371#[derive(Clone, Debug, Serialize, Deserialize)]
1375pub struct RoomInfo {
1376 #[serde(default)]
1378 pub(crate) version: u8,
1379
1380 pub(crate) room_id: OwnedRoomId,
1382
1383 pub(crate) room_state: RoomState,
1385
1386 pub(crate) prev_room_state: Option<RoomState>,
1388
1389 pub(crate) notification_counts: UnreadNotificationsCount,
1394
1395 pub(crate) summary: RoomSummary,
1397
1398 pub(crate) members_synced: bool,
1400
1401 pub(crate) last_prev_batch: Option<String>,
1403
1404 pub(crate) sync_info: SyncInfo,
1406
1407 pub(crate) encryption_state_synced: bool,
1409
1410 pub(crate) latest_event: Option<Box<LatestEvent>>,
1412
1413 #[serde(default)]
1415 pub(crate) read_receipts: RoomReadReceipts,
1416
1417 pub(crate) base_info: Box<BaseRoomInfo>,
1420
1421 #[serde(skip)]
1425 pub(crate) warned_about_unknown_room_version: Arc<AtomicBool>,
1426
1427 #[serde(default, skip_serializing_if = "Option::is_none")]
1432 pub(crate) cached_display_name: Option<RoomDisplayName>,
1433
1434 #[serde(default, skip_serializing_if = "Option::is_none")]
1436 pub(crate) cached_user_defined_notification_mode: Option<RoomNotificationMode>,
1437
1438 #[serde(default)]
1445 pub(crate) recency_stamp: Option<u64>,
1446}
1447
1448#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1449pub(crate) enum SyncInfo {
1450 NoState,
1456
1457 PartiallySynced,
1460
1461 FullySynced,
1463}
1464
1465impl RoomInfo {
1466 #[doc(hidden)] pub fn new(room_id: &RoomId, room_state: RoomState) -> Self {
1468 Self {
1469 version: 1,
1470 room_id: room_id.into(),
1471 room_state,
1472 prev_room_state: None,
1473 notification_counts: Default::default(),
1474 summary: Default::default(),
1475 members_synced: false,
1476 last_prev_batch: None,
1477 sync_info: SyncInfo::NoState,
1478 encryption_state_synced: false,
1479 latest_event: None,
1480 read_receipts: Default::default(),
1481 base_info: Box::new(BaseRoomInfo::new()),
1482 warned_about_unknown_room_version: Arc::new(false.into()),
1483 cached_display_name: None,
1484 cached_user_defined_notification_mode: None,
1485 recency_stamp: None,
1486 }
1487 }
1488
1489 pub fn mark_as_joined(&mut self) {
1491 self.set_state(RoomState::Joined);
1492 }
1493
1494 pub fn mark_as_left(&mut self) {
1496 self.set_state(RoomState::Left);
1497 }
1498
1499 pub fn mark_as_invited(&mut self) {
1501 self.set_state(RoomState::Invited);
1502 }
1503
1504 pub fn mark_as_knocked(&mut self) {
1506 self.set_state(RoomState::Knocked);
1507 }
1508
1509 pub fn mark_as_banned(&mut self) {
1511 self.set_state(RoomState::Banned);
1512 }
1513
1514 pub fn set_state(&mut self, room_state: RoomState) {
1516 if room_state != self.room_state {
1517 self.prev_room_state = Some(self.room_state);
1518 self.room_state = room_state;
1519 }
1520 }
1521
1522 pub fn mark_members_synced(&mut self) {
1524 self.members_synced = true;
1525 }
1526
1527 pub fn mark_members_missing(&mut self) {
1529 self.members_synced = false;
1530 }
1531
1532 pub fn are_members_synced(&self) -> bool {
1534 self.members_synced
1535 }
1536
1537 pub fn mark_state_partially_synced(&mut self) {
1539 self.sync_info = SyncInfo::PartiallySynced;
1540 }
1541
1542 pub fn mark_state_fully_synced(&mut self) {
1544 self.sync_info = SyncInfo::FullySynced;
1545 }
1546
1547 pub fn mark_state_not_synced(&mut self) {
1549 self.sync_info = SyncInfo::NoState;
1550 }
1551
1552 pub fn mark_encryption_state_synced(&mut self) {
1554 self.encryption_state_synced = true;
1555 }
1556
1557 pub fn mark_encryption_state_missing(&mut self) {
1559 self.encryption_state_synced = false;
1560 }
1561
1562 pub fn set_prev_batch(&mut self, prev_batch: Option<&str>) -> bool {
1566 if self.last_prev_batch.as_deref() != prev_batch {
1567 self.last_prev_batch = prev_batch.map(|p| p.to_owned());
1568 true
1569 } else {
1570 false
1571 }
1572 }
1573
1574 pub fn state(&self) -> RoomState {
1576 self.room_state
1577 }
1578
1579 pub fn is_encrypted(&self) -> bool {
1581 self.base_info.encryption.is_some()
1582 }
1583
1584 pub fn set_encryption_event(&mut self, event: Option<RoomEncryptionEventContent>) {
1586 self.base_info.encryption = event;
1587 }
1588
1589 pub fn handle_state_event(&mut self, event: &AnySyncStateEvent) -> bool {
1593 let ret = self.base_info.handle_state_event(event);
1594
1595 if let AnySyncStateEvent::RoomEncryption(_) = event {
1599 if self.is_encrypted() {
1600 self.mark_encryption_state_synced();
1601 }
1602 }
1603
1604 ret
1605 }
1606
1607 pub fn handle_stripped_state_event(&mut self, event: &AnyStrippedStateEvent) -> bool {
1611 self.base_info.handle_stripped_state_event(event)
1612 }
1613
1614 #[instrument(skip_all, fields(redacts))]
1616 pub fn handle_redaction(
1617 &mut self,
1618 event: &SyncRoomRedactionEvent,
1619 _raw: &Raw<SyncRoomRedactionEvent>,
1620 ) {
1621 let room_version = self.base_info.room_version().unwrap_or(&RoomVersionId::V1);
1622
1623 let Some(redacts) = event.redacts(room_version) else {
1624 info!("Can't apply redaction, redacts field is missing");
1625 return;
1626 };
1627 tracing::Span::current().record("redacts", debug(redacts));
1628
1629 if let Some(latest_event) = &mut self.latest_event {
1630 tracing::trace!("Checking if redaction applies to latest event");
1631 if latest_event.event_id().as_deref() == Some(redacts) {
1632 match apply_redaction(latest_event.event().raw(), _raw, room_version) {
1633 Some(redacted) => {
1634 latest_event.event_mut().kind =
1637 TimelineEventKind::PlainText { event: redacted };
1638 debug!("Redacted latest event");
1639 }
1640 None => {
1641 self.latest_event = None;
1642 debug!("Removed latest event");
1643 }
1644 }
1645 }
1646 }
1647
1648 self.base_info.handle_redaction(redacts);
1649 }
1650
1651 pub fn avatar_url(&self) -> Option<&MxcUri> {
1653 self.base_info
1654 .avatar
1655 .as_ref()
1656 .and_then(|e| e.as_original().and_then(|e| e.content.url.as_deref()))
1657 }
1658
1659 pub fn update_avatar(&mut self, url: Option<OwnedMxcUri>) {
1661 self.base_info.avatar = url.map(|url| {
1662 let mut content = RoomAvatarEventContent::new();
1663 content.url = Some(url);
1664
1665 MinimalStateEvent::Original(OriginalMinimalStateEvent { content, event_id: None })
1666 });
1667 }
1668
1669 pub fn avatar_info(&self) -> Option<&avatar::ImageInfo> {
1671 self.base_info
1672 .avatar
1673 .as_ref()
1674 .and_then(|e| e.as_original().and_then(|e| e.content.info.as_deref()))
1675 }
1676
1677 pub fn update_notification_count(&mut self, notification_counts: UnreadNotificationsCount) {
1679 self.notification_counts = notification_counts;
1680 }
1681
1682 pub fn update_from_ruma_summary(&mut self, summary: &RumaSummary) -> bool {
1686 let mut changed = false;
1687
1688 if !summary.is_empty() {
1689 if !summary.heroes.is_empty() {
1690 self.summary.room_heroes = summary
1691 .heroes
1692 .iter()
1693 .map(|hero_id| RoomHero {
1694 user_id: hero_id.to_owned(),
1695 display_name: None,
1696 avatar_url: None,
1697 })
1698 .collect();
1699
1700 changed = true;
1701 }
1702
1703 if let Some(joined) = summary.joined_member_count {
1704 self.summary.joined_member_count = joined.into();
1705 changed = true;
1706 }
1707
1708 if let Some(invited) = summary.invited_member_count {
1709 self.summary.invited_member_count = invited.into();
1710 changed = true;
1711 }
1712 }
1713
1714 changed
1715 }
1716
1717 pub(crate) fn update_joined_member_count(&mut self, count: u64) {
1719 self.summary.joined_member_count = count;
1720 }
1721
1722 pub(crate) fn update_invited_member_count(&mut self, count: u64) {
1724 self.summary.invited_member_count = count;
1725 }
1726
1727 pub(crate) fn update_heroes(&mut self, heroes: Vec<RoomHero>) {
1729 self.summary.room_heroes = heroes;
1730 }
1731
1732 pub fn heroes(&self) -> &[RoomHero] {
1734 &self.summary.room_heroes
1735 }
1736
1737 pub fn active_members_count(&self) -> u64 {
1741 self.summary.joined_member_count.saturating_add(self.summary.invited_member_count)
1742 }
1743
1744 pub fn invited_members_count(&self) -> u64 {
1746 self.summary.invited_member_count
1747 }
1748
1749 pub fn joined_members_count(&self) -> u64 {
1751 self.summary.joined_member_count
1752 }
1753
1754 pub fn canonical_alias(&self) -> Option<&RoomAliasId> {
1756 self.base_info.canonical_alias.as_ref()?.as_original()?.content.alias.as_deref()
1757 }
1758
1759 pub fn alt_aliases(&self) -> &[OwnedRoomAliasId] {
1761 self.base_info
1762 .canonical_alias
1763 .as_ref()
1764 .and_then(|ev| ev.as_original())
1765 .map(|ev| ev.content.alt_aliases.as_ref())
1766 .unwrap_or_default()
1767 }
1768
1769 pub fn room_id(&self) -> &RoomId {
1771 &self.room_id
1772 }
1773
1774 pub fn room_version(&self) -> Option<&RoomVersionId> {
1776 self.base_info.room_version()
1777 }
1778
1779 pub fn room_version_or_default(&self) -> RoomVersionId {
1784 use std::sync::atomic::Ordering;
1785
1786 self.base_info.room_version().cloned().unwrap_or_else(|| {
1787 if self
1788 .warned_about_unknown_room_version
1789 .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
1790 .is_ok()
1791 {
1792 warn!("Unknown room version, falling back to v10");
1793 }
1794
1795 RoomVersionId::V10
1796 })
1797 }
1798
1799 pub fn room_type(&self) -> Option<&RoomType> {
1801 match self.base_info.create.as_ref()? {
1802 MinimalStateEvent::Original(ev) => ev.content.room_type.as_ref(),
1803 MinimalStateEvent::Redacted(ev) => ev.content.room_type.as_ref(),
1804 }
1805 }
1806
1807 pub fn creator(&self) -> Option<&UserId> {
1809 match self.base_info.create.as_ref()? {
1810 MinimalStateEvent::Original(ev) => Some(&ev.content.creator),
1811 MinimalStateEvent::Redacted(ev) => Some(&ev.content.creator),
1812 }
1813 }
1814
1815 fn guest_access(&self) -> &GuestAccess {
1816 match &self.base_info.guest_access {
1817 Some(MinimalStateEvent::Original(ev)) => &ev.content.guest_access,
1818 _ => &GuestAccess::Forbidden,
1819 }
1820 }
1821
1822 pub fn history_visibility(&self) -> Option<&HistoryVisibility> {
1826 match &self.base_info.history_visibility {
1827 Some(MinimalStateEvent::Original(ev)) => Some(&ev.content.history_visibility),
1828 _ => None,
1829 }
1830 }
1831
1832 pub fn history_visibility_or_default(&self) -> &HistoryVisibility {
1839 match &self.base_info.history_visibility {
1840 Some(MinimalStateEvent::Original(ev)) => &ev.content.history_visibility,
1841 _ => &HistoryVisibility::Shared,
1842 }
1843 }
1844
1845 pub fn join_rule(&self) -> &JoinRule {
1849 match &self.base_info.join_rules {
1850 Some(MinimalStateEvent::Original(ev)) => &ev.content.join_rule,
1851 _ => &JoinRule::Public,
1852 }
1853 }
1854
1855 pub fn name(&self) -> Option<&str> {
1857 let name = &self.base_info.name.as_ref()?.as_original()?.content.name;
1858 (!name.is_empty()).then_some(name)
1859 }
1860
1861 fn tombstone(&self) -> Option<&RoomTombstoneEventContent> {
1862 Some(&self.base_info.tombstone.as_ref()?.as_original()?.content)
1863 }
1864
1865 pub fn topic(&self) -> Option<&str> {
1867 Some(&self.base_info.topic.as_ref()?.as_original()?.content.topic)
1868 }
1869
1870 fn active_matrix_rtc_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
1875 let mut v = self
1876 .base_info
1877 .rtc_member_events
1878 .iter()
1879 .filter_map(|(user_id, ev)| {
1880 ev.as_original().map(|ev| {
1881 ev.content
1882 .active_memberships(None)
1883 .into_iter()
1884 .map(move |m| (user_id.clone(), m))
1885 })
1886 })
1887 .flatten()
1888 .collect::<Vec<_>>();
1889 v.sort_by_key(|(_, m)| m.created_ts());
1890 v
1891 }
1892
1893 fn active_room_call_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
1899 self.active_matrix_rtc_memberships()
1900 .into_iter()
1901 .filter(|(_user_id, m)| m.is_room_call())
1902 .collect()
1903 }
1904
1905 pub fn has_active_room_call(&self) -> bool {
1908 !self.active_room_call_memberships().is_empty()
1909 }
1910
1911 pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
1920 self.active_room_call_memberships()
1921 .iter()
1922 .map(|(call_member_state_key, _)| call_member_state_key.user_id().to_owned())
1923 .collect()
1924 }
1925
1926 pub fn latest_event(&self) -> Option<&LatestEvent> {
1928 self.latest_event.as_deref()
1929 }
1930
1931 pub(crate) fn update_recency_stamp(&mut self, stamp: u64) {
1935 self.recency_stamp = Some(stamp);
1936 }
1937
1938 pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
1940 self.base_info.pinned_events.clone().map(|c| c.pinned)
1941 }
1942
1943 pub fn is_pinned_event(&self, event_id: &EventId) -> bool {
1949 self.base_info
1950 .pinned_events
1951 .as_ref()
1952 .map(|p| p.pinned.contains(&event_id.to_owned()))
1953 .unwrap_or_default()
1954 }
1955
1956 #[instrument(skip_all, fields(room_id = ?self.room_id))]
1964 pub(crate) async fn apply_migrations(&mut self, store: Arc<DynStateStore>) -> bool {
1965 let mut migrated = false;
1966
1967 if self.version < 1 {
1968 info!("Migrating room info to version 1");
1969
1970 match store.get_room_account_data_event_static::<TagEventContent>(&self.room_id).await {
1972 Ok(Some(raw_event)) => match raw_event.deserialize() {
1974 Ok(event) => {
1975 self.base_info.handle_notable_tags(&event.content.tags);
1976 }
1977 Err(error) => {
1978 warn!("Failed to deserialize room tags: {error}");
1979 }
1980 },
1981 Ok(_) => {
1982 }
1984 Err(error) => {
1985 warn!("Failed to load room tags: {error}");
1986 }
1987 }
1988
1989 match store.get_state_event_static::<RoomPinnedEventsEventContent>(&self.room_id).await
1991 {
1992 Ok(Some(RawSyncOrStrippedState::Sync(raw_event))) => {
1994 match raw_event.deserialize() {
1995 Ok(event) => {
1996 self.handle_state_event(&event.into());
1997 }
1998 Err(error) => {
1999 warn!("Failed to deserialize room pinned events: {error}");
2000 }
2001 }
2002 }
2003 Ok(_) => {
2004 }
2006 Err(error) => {
2007 warn!("Failed to load room pinned events: {error}");
2008 }
2009 }
2010
2011 self.version = 1;
2012 migrated = true;
2013 }
2014
2015 migrated
2016 }
2017}
2018
2019pub fn apply_redaction(
2022 event: &Raw<AnySyncTimelineEvent>,
2023 raw_redaction: &Raw<SyncRoomRedactionEvent>,
2024 room_version: &RoomVersionId,
2025) -> Option<Raw<AnySyncTimelineEvent>> {
2026 use ruma::canonical_json::{redact_in_place, RedactedBecause};
2027
2028 let mut event_json = match event.deserialize_as() {
2029 Ok(json) => json,
2030 Err(e) => {
2031 warn!("Failed to deserialize latest event: {e}");
2032 return None;
2033 }
2034 };
2035
2036 let redacted_because = match RedactedBecause::from_raw_event(raw_redaction) {
2037 Ok(rb) => rb,
2038 Err(e) => {
2039 warn!("Redaction event is not valid canonical JSON: {e}");
2040 return None;
2041 }
2042 };
2043
2044 let redact_result = redact_in_place(&mut event_json, room_version, Some(redacted_because));
2045
2046 if let Err(e) = redact_result {
2047 warn!("Failed to redact event: {e}");
2048 return None;
2049 }
2050
2051 let raw = Raw::new(&event_json).expect("CanonicalJsonObject must be serializable");
2052 Some(raw.cast())
2053}
2054
2055bitflags! {
2056 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
2061 pub struct RoomStateFilter: u16 {
2062 const JOINED = 0b00000001;
2064 const INVITED = 0b00000010;
2066 const LEFT = 0b00000100;
2068 const KNOCKED = 0b00001000;
2070 const BANNED = 0b00010000;
2072 }
2073}
2074
2075impl RoomStateFilter {
2076 pub fn matches(&self, state: RoomState) -> bool {
2078 if self.is_empty() {
2079 return true;
2080 }
2081
2082 let bit_state = match state {
2083 RoomState::Joined => Self::JOINED,
2084 RoomState::Left => Self::LEFT,
2085 RoomState::Invited => Self::INVITED,
2086 RoomState::Knocked => Self::KNOCKED,
2087 RoomState::Banned => Self::BANNED,
2088 };
2089
2090 self.contains(bit_state)
2091 }
2092
2093 pub fn as_vec(&self) -> Vec<RoomState> {
2095 let mut states = Vec::new();
2096
2097 if self.contains(Self::JOINED) {
2098 states.push(RoomState::Joined);
2099 }
2100 if self.contains(Self::LEFT) {
2101 states.push(RoomState::Left);
2102 }
2103 if self.contains(Self::INVITED) {
2104 states.push(RoomState::Invited);
2105 }
2106 if self.contains(Self::KNOCKED) {
2107 states.push(RoomState::Knocked);
2108 }
2109 if self.contains(Self::BANNED) {
2110 states.push(RoomState::Banned);
2111 }
2112
2113 states
2114 }
2115}
2116
2117fn compute_display_name_from_heroes(
2121 num_joined_invited: u64,
2122 mut heroes: Vec<&str>,
2123) -> RoomDisplayName {
2124 let num_heroes = heroes.len() as u64;
2125 let num_joined_invited_except_self = num_joined_invited.saturating_sub(1);
2126
2127 heroes.sort_unstable();
2129
2130 let names = if num_heroes == 0 && num_joined_invited > 1 {
2131 format!("{} people", num_joined_invited)
2132 } else if num_heroes >= num_joined_invited_except_self {
2133 heroes.join(", ")
2134 } else if num_heroes < num_joined_invited_except_self && num_joined_invited > 1 {
2135 format!("{}, and {} others", heroes.join(", "), (num_joined_invited - num_heroes))
2138 } else {
2139 "".to_owned()
2140 };
2141
2142 if num_joined_invited <= 1 {
2144 if names.is_empty() {
2145 RoomDisplayName::Empty
2146 } else {
2147 RoomDisplayName::EmptyWas(names)
2148 }
2149 } else {
2150 RoomDisplayName::Calculated(names)
2151 }
2152}
2153
2154#[cfg(test)]
2155mod tests {
2156 use std::{
2157 collections::BTreeSet,
2158 ops::{Not, Sub},
2159 str::FromStr,
2160 sync::Arc,
2161 time::Duration,
2162 };
2163
2164 use assign::assign;
2165 use matrix_sdk_common::deserialized_responses::TimelineEvent;
2166 use matrix_sdk_test::{
2167 async_test,
2168 event_factory::EventFactory,
2169 test_json::{sync_events::PINNED_EVENTS, TAG},
2170 ALICE, BOB, CAROL,
2171 };
2172 use ruma::{
2173 api::client::sync::sync_events::v3::RoomSummary as RumaSummary,
2174 device_id, event_id,
2175 events::{
2176 call::member::{
2177 ActiveFocus, ActiveLivekitFocus, Application, CallApplicationContent,
2178 CallMemberEventContent, CallMemberStateKey, Focus, LegacyMembershipData,
2179 LegacyMembershipDataInit, LivekitFocus, OriginalSyncCallMemberEvent,
2180 },
2181 room::{
2182 canonical_alias::RoomCanonicalAliasEventContent,
2183 encryption::{OriginalSyncRoomEncryptionEvent, RoomEncryptionEventContent},
2184 member::{MembershipState, RoomMemberEventContent, StrippedRoomMemberEvent},
2185 name::RoomNameEventContent,
2186 pinned_events::RoomPinnedEventsEventContent,
2187 },
2188 AnySyncStateEvent, EmptyStateKey, StateEventType, StateUnsigned, SyncStateEvent,
2189 },
2190 owned_event_id, owned_room_id, owned_user_id, room_alias_id, room_id,
2191 serde::Raw,
2192 time::SystemTime,
2193 user_id, DeviceId, EventEncryptionAlgorithm, EventId, MilliSecondsSinceUnixEpoch,
2194 OwnedEventId, OwnedUserId, UserId,
2195 };
2196 use serde_json::json;
2197 use similar_asserts::assert_eq;
2198 use stream_assert::{assert_pending, assert_ready};
2199
2200 use super::{compute_display_name_from_heroes, Room, RoomHero, RoomInfo, RoomState, SyncInfo};
2201 use crate::{
2202 latest_event::LatestEvent,
2203 rooms::RoomNotableTags,
2204 store::{IntoStateStore, MemoryStore, StateChanges, StateStore, StoreConfig},
2205 test_utils::logged_in_base_client,
2206 BaseClient, MinimalStateEvent, OriginalMinimalStateEvent, RoomDisplayName,
2207 RoomInfoNotableUpdateReasons, RoomStateFilter, SessionMeta,
2208 };
2209
2210 #[test]
2211 fn test_room_info_serialization() {
2212 use ruma::owned_user_id;
2216
2217 use super::RoomSummary;
2218 use crate::{rooms::BaseRoomInfo, sync::UnreadNotificationsCount};
2219
2220 let info = RoomInfo {
2221 version: 1,
2222 room_id: room_id!("!gda78o:server.tld").into(),
2223 room_state: RoomState::Invited,
2224 prev_room_state: None,
2225 notification_counts: UnreadNotificationsCount {
2226 highlight_count: 1,
2227 notification_count: 2,
2228 },
2229 summary: RoomSummary {
2230 room_heroes: vec![RoomHero {
2231 user_id: owned_user_id!("@somebody:example.org"),
2232 display_name: None,
2233 avatar_url: None,
2234 }],
2235 joined_member_count: 5,
2236 invited_member_count: 0,
2237 },
2238 members_synced: true,
2239 last_prev_batch: Some("pb".to_owned()),
2240 sync_info: SyncInfo::FullySynced,
2241 encryption_state_synced: true,
2242 latest_event: Some(Box::new(LatestEvent::new(TimelineEvent::new(
2243 Raw::from_json_string(json!({"sender": "@u:i.uk"}).to_string()).unwrap(),
2244 )))),
2245 base_info: Box::new(
2246 assign!(BaseRoomInfo::new(), { pinned_events: Some(RoomPinnedEventsEventContent::new(vec![owned_event_id!("$a")])) }),
2247 ),
2248 read_receipts: Default::default(),
2249 warned_about_unknown_room_version: Arc::new(false.into()),
2250 cached_display_name: None,
2251 cached_user_defined_notification_mode: None,
2252 recency_stamp: Some(42),
2253 };
2254
2255 let info_json = json!({
2256 "version": 1,
2257 "room_id": "!gda78o:server.tld",
2258 "room_state": "Invited",
2259 "prev_room_state": null,
2260 "notification_counts": {
2261 "highlight_count": 1,
2262 "notification_count": 2,
2263 },
2264 "summary": {
2265 "room_heroes": [{
2266 "user_id": "@somebody:example.org",
2267 "display_name": null,
2268 "avatar_url": null
2269 }],
2270 "joined_member_count": 5,
2271 "invited_member_count": 0,
2272 },
2273 "members_synced": true,
2274 "last_prev_batch": "pb",
2275 "sync_info": "FullySynced",
2276 "encryption_state_synced": true,
2277 "latest_event": {
2278 "event": {
2279 "kind": {"PlainText": {"event": {"sender": "@u:i.uk"}}},
2280 },
2281 },
2282 "base_info": {
2283 "avatar": null,
2284 "canonical_alias": null,
2285 "create": null,
2286 "dm_targets": [],
2287 "encryption": null,
2288 "guest_access": null,
2289 "history_visibility": null,
2290 "is_marked_unread": false,
2291 "join_rules": null,
2292 "max_power_level": 100,
2293 "name": null,
2294 "tombstone": null,
2295 "topic": null,
2296 "pinned_events": {
2297 "pinned": ["$a"]
2298 },
2299 },
2300 "read_receipts": {
2301 "num_unread": 0,
2302 "num_mentions": 0,
2303 "num_notifications": 0,
2304 "latest_active": null,
2305 "pending": []
2306 },
2307 "recency_stamp": 42,
2308 });
2309
2310 assert_eq!(serde_json::to_value(info).unwrap(), info_json);
2311 }
2312
2313 #[test]
2320 fn test_room_info_deserialization_without_optional_items() {
2321 use ruma::{owned_mxc_uri, owned_user_id};
2322
2323 let info_json = json!({
2326 "room_id": "!gda78o:server.tld",
2327 "room_state": "Invited",
2328 "prev_room_state": null,
2329 "notification_counts": {
2330 "highlight_count": 1,
2331 "notification_count": 2,
2332 },
2333 "summary": {
2334 "room_heroes": [{
2335 "user_id": "@somebody:example.org",
2336 "display_name": "Somebody",
2337 "avatar_url": "mxc://example.org/abc"
2338 }],
2339 "joined_member_count": 5,
2340 "invited_member_count": 0,
2341 },
2342 "members_synced": true,
2343 "last_prev_batch": "pb",
2344 "sync_info": "FullySynced",
2345 "encryption_state_synced": true,
2346 "base_info": {
2347 "avatar": null,
2348 "canonical_alias": null,
2349 "create": null,
2350 "dm_targets": [],
2351 "encryption": null,
2352 "guest_access": null,
2353 "history_visibility": null,
2354 "join_rules": null,
2355 "max_power_level": 100,
2356 "name": null,
2357 "tombstone": null,
2358 "topic": null,
2359 },
2360 });
2361
2362 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
2363
2364 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
2365 assert_eq!(info.room_state, RoomState::Invited);
2366 assert_eq!(info.notification_counts.highlight_count, 1);
2367 assert_eq!(info.notification_counts.notification_count, 2);
2368 assert_eq!(
2369 info.summary.room_heroes,
2370 vec![RoomHero {
2371 user_id: owned_user_id!("@somebody:example.org"),
2372 display_name: Some("Somebody".to_owned()),
2373 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
2374 }]
2375 );
2376 assert_eq!(info.summary.joined_member_count, 5);
2377 assert_eq!(info.summary.invited_member_count, 0);
2378 assert!(info.members_synced);
2379 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
2380 assert_eq!(info.sync_info, SyncInfo::FullySynced);
2381 assert!(info.encryption_state_synced);
2382 assert!(info.base_info.avatar.is_none());
2383 assert!(info.base_info.canonical_alias.is_none());
2384 assert!(info.base_info.create.is_none());
2385 assert_eq!(info.base_info.dm_targets.len(), 0);
2386 assert!(info.base_info.encryption.is_none());
2387 assert!(info.base_info.guest_access.is_none());
2388 assert!(info.base_info.history_visibility.is_none());
2389 assert!(info.base_info.join_rules.is_none());
2390 assert_eq!(info.base_info.max_power_level, 100);
2391 assert!(info.base_info.name.is_none());
2392 assert!(info.base_info.tombstone.is_none());
2393 assert!(info.base_info.topic.is_none());
2394 }
2395
2396 #[test]
2397 fn test_room_info_deserialization() {
2398 use ruma::{owned_mxc_uri, owned_user_id};
2399
2400 use crate::notification_settings::RoomNotificationMode;
2401
2402 let info_json = json!({
2403 "room_id": "!gda78o:server.tld",
2404 "room_state": "Joined",
2405 "prev_room_state": "Invited",
2406 "notification_counts": {
2407 "highlight_count": 1,
2408 "notification_count": 2,
2409 },
2410 "summary": {
2411 "room_heroes": [{
2412 "user_id": "@somebody:example.org",
2413 "display_name": "Somebody",
2414 "avatar_url": "mxc://example.org/abc"
2415 }],
2416 "joined_member_count": 5,
2417 "invited_member_count": 0,
2418 },
2419 "members_synced": true,
2420 "last_prev_batch": "pb",
2421 "sync_info": "FullySynced",
2422 "encryption_state_synced": true,
2423 "base_info": {
2424 "avatar": null,
2425 "canonical_alias": null,
2426 "create": null,
2427 "dm_targets": [],
2428 "encryption": null,
2429 "guest_access": null,
2430 "history_visibility": null,
2431 "join_rules": null,
2432 "max_power_level": 100,
2433 "name": null,
2434 "tombstone": null,
2435 "topic": null,
2436 },
2437 "cached_display_name": { "Calculated": "lol" },
2438 "cached_user_defined_notification_mode": "Mute",
2439 "recency_stamp": 42,
2440 });
2441
2442 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
2443
2444 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
2445 assert_eq!(info.room_state, RoomState::Joined);
2446 assert_eq!(info.prev_room_state, Some(RoomState::Invited));
2447 assert_eq!(info.notification_counts.highlight_count, 1);
2448 assert_eq!(info.notification_counts.notification_count, 2);
2449 assert_eq!(
2450 info.summary.room_heroes,
2451 vec![RoomHero {
2452 user_id: owned_user_id!("@somebody:example.org"),
2453 display_name: Some("Somebody".to_owned()),
2454 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
2455 }]
2456 );
2457 assert_eq!(info.summary.joined_member_count, 5);
2458 assert_eq!(info.summary.invited_member_count, 0);
2459 assert!(info.members_synced);
2460 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
2461 assert_eq!(info.sync_info, SyncInfo::FullySynced);
2462 assert!(info.encryption_state_synced);
2463 assert!(info.latest_event.is_none());
2464 assert!(info.base_info.avatar.is_none());
2465 assert!(info.base_info.canonical_alias.is_none());
2466 assert!(info.base_info.create.is_none());
2467 assert_eq!(info.base_info.dm_targets.len(), 0);
2468 assert!(info.base_info.encryption.is_none());
2469 assert!(info.base_info.guest_access.is_none());
2470 assert!(info.base_info.history_visibility.is_none());
2471 assert!(info.base_info.join_rules.is_none());
2472 assert_eq!(info.base_info.max_power_level, 100);
2473 assert!(info.base_info.name.is_none());
2474 assert!(info.base_info.tombstone.is_none());
2475 assert!(info.base_info.topic.is_none());
2476
2477 assert_eq!(
2478 info.cached_display_name.as_ref(),
2479 Some(&RoomDisplayName::Calculated("lol".to_owned())),
2480 );
2481 assert_eq!(
2482 info.cached_user_defined_notification_mode.as_ref(),
2483 Some(&RoomNotificationMode::Mute)
2484 );
2485 assert_eq!(info.recency_stamp.as_ref(), Some(&42));
2486 }
2487
2488 #[async_test]
2489 async fn test_is_favourite() {
2490 let client = BaseClient::with_store_config(StoreConfig::new(
2492 "cross-process-store-locks-holder-name".to_owned(),
2493 ));
2494
2495 client
2496 .set_session_meta(
2497 SessionMeta {
2498 user_id: user_id!("@alice:example.org").into(),
2499 device_id: ruma::device_id!("AYEAYEAYE").into(),
2500 },
2501 #[cfg(feature = "e2e-encryption")]
2502 None,
2503 )
2504 .await
2505 .unwrap();
2506
2507 let room_id = room_id!("!test:localhost");
2508 let room = client.get_or_create_room(room_id, RoomState::Joined);
2509
2510 assert!(room.is_favourite().not());
2512
2513 let mut room_info_subscriber = room.subscribe_info();
2515
2516 assert_pending!(room_info_subscriber);
2517
2518 let tag_raw = Raw::new(&json!({
2520 "content": {
2521 "tags": {
2522 "m.favourite": {
2523 "order": 0.0
2524 },
2525 },
2526 },
2527 "type": "m.tag",
2528 }))
2529 .unwrap()
2530 .cast();
2531
2532 let mut changes = StateChanges::default();
2534 client
2535 .handle_room_account_data(room_id, &[tag_raw], &mut changes, &mut Default::default())
2536 .await;
2537 client.apply_changes(&changes, Default::default());
2538
2539 assert_ready!(room_info_subscriber);
2541 assert_pending!(room_info_subscriber);
2542
2543 assert!(room.is_favourite());
2545
2546 let tag_raw = Raw::new(&json!({
2548 "content": {
2549 "tags": {},
2550 },
2551 "type": "m.tag"
2552 }))
2553 .unwrap()
2554 .cast();
2555 client
2556 .handle_room_account_data(room_id, &[tag_raw], &mut changes, &mut Default::default())
2557 .await;
2558 client.apply_changes(&changes, Default::default());
2559
2560 assert_ready!(room_info_subscriber);
2562 assert_pending!(room_info_subscriber);
2563
2564 assert!(room.is_favourite().not());
2566 }
2567
2568 #[async_test]
2569 async fn test_is_low_priority() {
2570 let client = BaseClient::with_store_config(StoreConfig::new(
2572 "cross-process-store-locks-holder-name".to_owned(),
2573 ));
2574
2575 client
2576 .set_session_meta(
2577 SessionMeta {
2578 user_id: user_id!("@alice:example.org").into(),
2579 device_id: ruma::device_id!("AYEAYEAYE").into(),
2580 },
2581 #[cfg(feature = "e2e-encryption")]
2582 None,
2583 )
2584 .await
2585 .unwrap();
2586
2587 let room_id = room_id!("!test:localhost");
2588 let room = client.get_or_create_room(room_id, RoomState::Joined);
2589
2590 assert!(!room.is_low_priority());
2592
2593 let mut room_info_subscriber = room.subscribe_info();
2595
2596 assert_pending!(room_info_subscriber);
2597
2598 let tag_raw = Raw::new(&json!({
2600 "content": {
2601 "tags": {
2602 "m.lowpriority": {
2603 "order": 0.0
2604 },
2605 }
2606 },
2607 "type": "m.tag"
2608 }))
2609 .unwrap()
2610 .cast();
2611
2612 let mut changes = StateChanges::default();
2614 client
2615 .handle_room_account_data(room_id, &[tag_raw], &mut changes, &mut Default::default())
2616 .await;
2617 client.apply_changes(&changes, Default::default());
2618
2619 assert_ready!(room_info_subscriber);
2621 assert_pending!(room_info_subscriber);
2622
2623 assert!(room.is_low_priority());
2625
2626 let tag_raw = Raw::new(&json!({
2628 "content": {
2629 "tags": {},
2630 },
2631 "type": "m.tag"
2632 }))
2633 .unwrap()
2634 .cast();
2635 client
2636 .handle_room_account_data(room_id, &[tag_raw], &mut changes, &mut Default::default())
2637 .await;
2638 client.apply_changes(&changes, Default::default());
2639
2640 assert_ready!(room_info_subscriber);
2642 assert_pending!(room_info_subscriber);
2643
2644 assert!(room.is_low_priority().not());
2646 }
2647
2648 fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
2649 let store = Arc::new(MemoryStore::new());
2650 let user_id = user_id!("@me:example.org");
2651 let room_id = room_id!("!test:localhost");
2652 let (sender, _receiver) = tokio::sync::broadcast::channel(1);
2653
2654 (store.clone(), Room::new(user_id, store, room_id, room_type, sender))
2655 }
2656
2657 fn make_stripped_member_event(user_id: &UserId, name: &str) -> Raw<StrippedRoomMemberEvent> {
2658 let ev_json = json!({
2659 "type": "m.room.member",
2660 "content": assign!(RoomMemberEventContent::new(MembershipState::Join), {
2661 displayname: Some(name.to_owned())
2662 }),
2663 "sender": user_id,
2664 "state_key": user_id,
2665 });
2666
2667 Raw::new(&ev_json).unwrap().cast()
2668 }
2669
2670 #[async_test]
2671 async fn test_display_name_for_joined_room_is_empty_if_no_info() {
2672 let (_, room) = make_room_test_helper(RoomState::Joined);
2673 assert_eq!(room.compute_display_name().await.unwrap(), RoomDisplayName::Empty);
2674 }
2675
2676 #[async_test]
2677 async fn test_display_name_for_joined_room_uses_canonical_alias_if_available() {
2678 let (_, room) = make_room_test_helper(RoomState::Joined);
2679 room.inner
2680 .update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
2681 assert_eq!(
2682 room.compute_display_name().await.unwrap(),
2683 RoomDisplayName::Aliased("test".to_owned())
2684 );
2685 }
2686
2687 #[async_test]
2688 async fn test_display_name_for_joined_room_prefers_name_over_alias() {
2689 let (_, room) = make_room_test_helper(RoomState::Joined);
2690 room.inner
2691 .update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
2692 assert_eq!(
2693 room.compute_display_name().await.unwrap(),
2694 RoomDisplayName::Aliased("test".to_owned())
2695 );
2696 room.inner.update(|info| info.base_info.name = Some(make_name_event()));
2697 assert_eq!(
2699 room.compute_display_name().await.unwrap(),
2700 RoomDisplayName::Named("Test Room".to_owned())
2701 );
2702 }
2703
2704 #[async_test]
2705 async fn test_display_name_for_invited_room_is_empty_if_no_info() {
2706 let (_, room) = make_room_test_helper(RoomState::Invited);
2707 assert_eq!(room.compute_display_name().await.unwrap(), RoomDisplayName::Empty);
2708 }
2709
2710 #[async_test]
2711 async fn test_display_name_for_invited_room_is_empty_if_room_name_empty() {
2712 let (_, room) = make_room_test_helper(RoomState::Invited);
2713
2714 let room_name = MinimalStateEvent::Original(OriginalMinimalStateEvent {
2715 content: RoomNameEventContent::new(String::new()),
2716 event_id: None,
2717 });
2718 room.inner.update(|info| info.base_info.name = Some(room_name));
2719
2720 assert_eq!(room.compute_display_name().await.unwrap(), RoomDisplayName::Empty);
2721 }
2722
2723 #[async_test]
2724 async fn test_display_name_for_invited_room_uses_canonical_alias_if_available() {
2725 let (_, room) = make_room_test_helper(RoomState::Invited);
2726 room.inner
2727 .update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
2728 assert_eq!(
2729 room.compute_display_name().await.unwrap(),
2730 RoomDisplayName::Aliased("test".to_owned())
2731 );
2732 }
2733
2734 #[async_test]
2735 async fn test_display_name_for_invited_room_prefers_name_over_alias() {
2736 let (_, room) = make_room_test_helper(RoomState::Invited);
2737 room.inner
2738 .update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
2739 assert_eq!(
2740 room.compute_display_name().await.unwrap(),
2741 RoomDisplayName::Aliased("test".to_owned())
2742 );
2743 room.inner.update(|info| info.base_info.name = Some(make_name_event()));
2744 assert_eq!(
2746 room.compute_display_name().await.unwrap(),
2747 RoomDisplayName::Named("Test Room".to_owned())
2748 );
2749 }
2750
2751 fn make_canonical_alias_event() -> MinimalStateEvent<RoomCanonicalAliasEventContent> {
2752 MinimalStateEvent::Original(OriginalMinimalStateEvent {
2753 content: assign!(RoomCanonicalAliasEventContent::new(), {
2754 alias: Some(room_alias_id!("#test:example.com").to_owned()),
2755 }),
2756 event_id: None,
2757 })
2758 }
2759
2760 fn make_name_event() -> MinimalStateEvent<RoomNameEventContent> {
2761 MinimalStateEvent::Original(OriginalMinimalStateEvent {
2762 content: RoomNameEventContent::new("Test Room".to_owned()),
2763 event_id: None,
2764 })
2765 }
2766
2767 #[async_test]
2768 async fn test_display_name_dm_invited() {
2769 let (store, room) = make_room_test_helper(RoomState::Invited);
2770 let room_id = room_id!("!test:localhost");
2771 let matthew = user_id!("@matthew:example.org");
2772 let me = user_id!("@me:example.org");
2773 let mut changes = StateChanges::new("".to_owned());
2774 let summary = assign!(RumaSummary::new(), {
2775 heroes: vec![me.to_owned(), matthew.to_owned()],
2776 });
2777
2778 changes.add_stripped_member(
2779 room_id,
2780 matthew,
2781 make_stripped_member_event(matthew, "Matthew"),
2782 );
2783 changes.add_stripped_member(room_id, me, make_stripped_member_event(me, "Me"));
2784 store.save_changes(&changes).await.unwrap();
2785
2786 room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
2787 assert_eq!(
2788 room.compute_display_name().await.unwrap(),
2789 RoomDisplayName::Calculated("Matthew".to_owned())
2790 );
2791 }
2792
2793 #[async_test]
2794 async fn test_display_name_dm_invited_no_heroes() {
2795 let (store, room) = make_room_test_helper(RoomState::Invited);
2796 let room_id = room_id!("!test:localhost");
2797 let matthew = user_id!("@matthew:example.org");
2798 let me = user_id!("@me:example.org");
2799 let mut changes = StateChanges::new("".to_owned());
2800
2801 changes.add_stripped_member(
2802 room_id,
2803 matthew,
2804 make_stripped_member_event(matthew, "Matthew"),
2805 );
2806 changes.add_stripped_member(room_id, me, make_stripped_member_event(me, "Me"));
2807 store.save_changes(&changes).await.unwrap();
2808
2809 assert_eq!(
2810 room.compute_display_name().await.unwrap(),
2811 RoomDisplayName::Calculated("Matthew".to_owned())
2812 );
2813 }
2814
2815 #[async_test]
2816 async fn test_display_name_dm_joined() {
2817 let (store, room) = make_room_test_helper(RoomState::Joined);
2818 let room_id = room_id!("!test:localhost");
2819 let matthew = user_id!("@matthew:example.org");
2820 let me = user_id!("@me:example.org");
2821
2822 let mut changes = StateChanges::new("".to_owned());
2823 let summary = assign!(RumaSummary::new(), {
2824 joined_member_count: Some(2u32.into()),
2825 heroes: vec![me.to_owned(), matthew.to_owned()],
2826 });
2827
2828 let f = EventFactory::new().room(room_id!("!test:localhost"));
2829
2830 let members = changes
2831 .state
2832 .entry(room_id.to_owned())
2833 .or_default()
2834 .entry(StateEventType::RoomMember)
2835 .or_default();
2836 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
2837 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
2838
2839 store.save_changes(&changes).await.unwrap();
2840
2841 room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
2842 assert_eq!(
2843 room.compute_display_name().await.unwrap(),
2844 RoomDisplayName::Calculated("Matthew".to_owned())
2845 );
2846 }
2847
2848 #[async_test]
2849 async fn test_display_name_dm_joined_service_members() {
2850 let (store, room) = make_room_test_helper(RoomState::Joined);
2851 let room_id = room_id!("!test:localhost");
2852
2853 let matthew = user_id!("@sahasrhala:example.org");
2854 let me = user_id!("@me:example.org");
2855 let bot = user_id!("@bot:example.org");
2856
2857 let mut changes = StateChanges::new("".to_owned());
2858 let summary = assign!(RumaSummary::new(), {
2859 joined_member_count: Some(3u32.into()),
2860 heroes: vec![me.to_owned(), matthew.to_owned(), bot.to_owned()],
2861 });
2862
2863 let f = EventFactory::new().room(room_id!("!test:localhost"));
2864
2865 let members = changes
2866 .state
2867 .entry(room_id.to_owned())
2868 .or_default()
2869 .entry(StateEventType::RoomMember)
2870 .or_default();
2871 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
2872 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
2873 members.insert(bot.into(), f.member(bot).display_name("Bot").into_raw());
2874
2875 let member_hints_content =
2876 f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into_raw();
2877 changes
2878 .state
2879 .entry(room_id.to_owned())
2880 .or_default()
2881 .entry(StateEventType::MemberHints)
2882 .or_default()
2883 .insert("".to_owned(), member_hints_content);
2884
2885 store.save_changes(&changes).await.unwrap();
2886
2887 room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
2888 assert_eq!(
2890 room.compute_display_name().await.unwrap(),
2891 RoomDisplayName::Calculated("Matthew".to_owned())
2892 );
2893 }
2894
2895 #[async_test]
2896 async fn test_display_name_dm_joined_alone_with_service_members() {
2897 let (store, room) = make_room_test_helper(RoomState::Joined);
2898 let room_id = room_id!("!test:localhost");
2899
2900 let me = user_id!("@me:example.org");
2901 let bot = user_id!("@bot:example.org");
2902
2903 let mut changes = StateChanges::new("".to_owned());
2904 let summary = assign!(RumaSummary::new(), {
2905 joined_member_count: Some(2u32.into()),
2906 heroes: vec![me.to_owned(), bot.to_owned()],
2907 });
2908
2909 let f = EventFactory::new().room(room_id!("!test:localhost"));
2910
2911 let members = changes
2912 .state
2913 .entry(room_id.to_owned())
2914 .or_default()
2915 .entry(StateEventType::RoomMember)
2916 .or_default();
2917 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
2918 members.insert(bot.into(), f.member(bot).display_name("Bot").into_raw());
2919
2920 let member_hints_content =
2921 f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into_raw();
2922 changes
2923 .state
2924 .entry(room_id.to_owned())
2925 .or_default()
2926 .entry(StateEventType::MemberHints)
2927 .or_default()
2928 .insert("".to_owned(), member_hints_content);
2929
2930 store.save_changes(&changes).await.unwrap();
2931
2932 room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
2933 assert_eq!(room.compute_display_name().await.unwrap(), RoomDisplayName::Empty);
2935 }
2936
2937 #[async_test]
2938 async fn test_display_name_dm_joined_no_heroes() {
2939 let (store, room) = make_room_test_helper(RoomState::Joined);
2940 let room_id = room_id!("!test:localhost");
2941 let matthew = user_id!("@matthew:example.org");
2942 let me = user_id!("@me:example.org");
2943 let mut changes = StateChanges::new("".to_owned());
2944
2945 let f = EventFactory::new().room(room_id!("!test:localhost"));
2946
2947 let members = changes
2948 .state
2949 .entry(room_id.to_owned())
2950 .or_default()
2951 .entry(StateEventType::RoomMember)
2952 .or_default();
2953 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
2954 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
2955
2956 store.save_changes(&changes).await.unwrap();
2957
2958 assert_eq!(
2959 room.compute_display_name().await.unwrap(),
2960 RoomDisplayName::Calculated("Matthew".to_owned())
2961 );
2962 }
2963
2964 #[async_test]
2965 async fn test_display_name_dm_joined_no_heroes_service_members() {
2966 let (store, room) = make_room_test_helper(RoomState::Joined);
2967 let room_id = room_id!("!test:localhost");
2968
2969 let matthew = user_id!("@matthew:example.org");
2970 let me = user_id!("@me:example.org");
2971 let bot = user_id!("@bot:example.org");
2972
2973 let mut changes = StateChanges::new("".to_owned());
2974
2975 let f = EventFactory::new().room(room_id!("!test:localhost"));
2976
2977 let members = changes
2978 .state
2979 .entry(room_id.to_owned())
2980 .or_default()
2981 .entry(StateEventType::RoomMember)
2982 .or_default();
2983 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
2984 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
2985 members.insert(bot.into(), f.member(bot).display_name("Bot").into_raw());
2986
2987 let member_hints_content =
2988 f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into_raw();
2989 changes
2990 .state
2991 .entry(room_id.to_owned())
2992 .or_default()
2993 .entry(StateEventType::MemberHints)
2994 .or_default()
2995 .insert("".to_owned(), member_hints_content);
2996
2997 store.save_changes(&changes).await.unwrap();
2998
2999 assert_eq!(
3000 room.compute_display_name().await.unwrap(),
3001 RoomDisplayName::Calculated("Matthew".to_owned())
3002 );
3003 }
3004
3005 #[async_test]
3006 async fn test_display_name_deterministic() {
3007 let (store, room) = make_room_test_helper(RoomState::Joined);
3008
3009 let alice = user_id!("@alice:example.org");
3010 let bob = user_id!("@bob:example.org");
3011 let carol = user_id!("@carol:example.org");
3012 let denis = user_id!("@denis:example.org");
3013 let erica = user_id!("@erica:example.org");
3014 let fred = user_id!("@fred:example.org");
3015 let me = user_id!("@me:example.org");
3016
3017 let mut changes = StateChanges::new("".to_owned());
3018
3019 let f = EventFactory::new().room(room_id!("!test:localhost"));
3020
3021 {
3024 let members = changes
3025 .state
3026 .entry(room.room_id().to_owned())
3027 .or_default()
3028 .entry(StateEventType::RoomMember)
3029 .or_default();
3030 members.insert(carol.into(), f.member(carol).display_name("Carol").into_raw());
3031 members.insert(bob.into(), f.member(bob).display_name("Bob").into_raw());
3032 members.insert(fred.into(), f.member(fred).display_name("Fred").into_raw());
3033 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
3034 store.save_changes(&changes).await.unwrap();
3035 }
3036
3037 {
3038 let members = changes
3039 .state
3040 .entry(room.room_id().to_owned())
3041 .or_default()
3042 .entry(StateEventType::RoomMember)
3043 .or_default();
3044 members.insert(alice.into(), f.member(alice).display_name("Alice").into_raw());
3045 members.insert(erica.into(), f.member(erica).display_name("Erica").into_raw());
3046 members.insert(denis.into(), f.member(denis).display_name("Denis").into_raw());
3047 store.save_changes(&changes).await.unwrap();
3048 }
3049
3050 let summary = assign!(RumaSummary::new(), {
3051 joined_member_count: Some(7u32.into()),
3052 heroes: vec![denis.to_owned(), carol.to_owned(), bob.to_owned(), erica.to_owned()],
3053 });
3054 room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
3055
3056 assert_eq!(
3057 room.compute_display_name().await.unwrap(),
3058 RoomDisplayName::Calculated("Bob, Carol, Denis, Erica, and 3 others".to_owned())
3059 );
3060 }
3061
3062 #[async_test]
3063 async fn test_display_name_deterministic_no_heroes() {
3064 let (store, room) = make_room_test_helper(RoomState::Joined);
3065
3066 let alice = user_id!("@alice:example.org");
3067 let bob = user_id!("@bob:example.org");
3068 let carol = user_id!("@carol:example.org");
3069 let denis = user_id!("@denis:example.org");
3070 let erica = user_id!("@erica:example.org");
3071 let fred = user_id!("@fred:example.org");
3072 let me = user_id!("@me:example.org");
3073
3074 let f = EventFactory::new().room(room_id!("!test:localhost"));
3075
3076 let mut changes = StateChanges::new("".to_owned());
3077
3078 {
3081 let members = changes
3082 .state
3083 .entry(room.room_id().to_owned())
3084 .or_default()
3085 .entry(StateEventType::RoomMember)
3086 .or_default();
3087 members.insert(carol.into(), f.member(carol).display_name("Carol").into_raw());
3088 members.insert(bob.into(), f.member(bob).display_name("Bob").into_raw());
3089 members.insert(fred.into(), f.member(fred).display_name("Fred").into_raw());
3090 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
3091
3092 store.save_changes(&changes).await.unwrap();
3093 }
3094
3095 {
3096 let members = changes
3097 .state
3098 .entry(room.room_id().to_owned())
3099 .or_default()
3100 .entry(StateEventType::RoomMember)
3101 .or_default();
3102 members.insert(alice.into(), f.member(alice).display_name("Alice").into_raw());
3103 members.insert(erica.into(), f.member(erica).display_name("Erica").into_raw());
3104 members.insert(denis.into(), f.member(denis).display_name("Denis").into_raw());
3105 store.save_changes(&changes).await.unwrap();
3106 }
3107
3108 assert_eq!(
3109 room.compute_display_name().await.unwrap(),
3110 RoomDisplayName::Calculated("Alice, Bob, Carol, Denis, Erica, and 2 others".to_owned())
3111 );
3112 }
3113
3114 #[async_test]
3115 async fn test_display_name_dm_alone() {
3116 let (store, room) = make_room_test_helper(RoomState::Joined);
3117 let room_id = room_id!("!test:localhost");
3118 let matthew = user_id!("@matthew:example.org");
3119 let me = user_id!("@me:example.org");
3120 let mut changes = StateChanges::new("".to_owned());
3121 let summary = assign!(RumaSummary::new(), {
3122 joined_member_count: Some(1u32.into()),
3123 heroes: vec![me.to_owned(), matthew.to_owned()],
3124 });
3125
3126 let f = EventFactory::new().room(room_id!("!test:localhost"));
3127
3128 let members = changes
3129 .state
3130 .entry(room_id.to_owned())
3131 .or_default()
3132 .entry(StateEventType::RoomMember)
3133 .or_default();
3134 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
3135 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
3136
3137 store.save_changes(&changes).await.unwrap();
3138
3139 room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
3140 assert_eq!(
3141 room.compute_display_name().await.unwrap(),
3142 RoomDisplayName::EmptyWas("Matthew".to_owned())
3143 );
3144 }
3145
3146 #[cfg(feature = "e2e-encryption")]
3147 #[async_test]
3148 async fn test_setting_the_latest_event_doesnt_cause_a_room_info_notable_update() {
3149 use std::collections::BTreeMap;
3150
3151 use assert_matches::assert_matches;
3152
3153 use crate::{RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons};
3154
3155 let client = BaseClient::with_store_config(StoreConfig::new(
3157 "cross-process-store-locks-holder-name".to_owned(),
3158 ));
3159
3160 client
3161 .set_session_meta(
3162 SessionMeta {
3163 user_id: user_id!("@alice:example.org").into(),
3164 device_id: ruma::device_id!("AYEAYEAYE").into(),
3165 },
3166 None,
3167 )
3168 .await
3169 .unwrap();
3170
3171 let room_id = room_id!("!test:localhost");
3172 let room = client.get_or_create_room(room_id, RoomState::Joined);
3173
3174 add_encrypted_event(&room, "$A");
3176 assert!(room.latest_event().is_none());
3178
3179 let mut room_info_notable_update = client.room_info_notable_update_receiver();
3181
3182 let event = make_latest_event("$A");
3184
3185 let mut changes = StateChanges::default();
3186 let mut room_info_notable_updates = BTreeMap::new();
3187 room.on_latest_event_decrypted(
3188 event.clone(),
3189 0,
3190 &mut changes,
3191 &mut room_info_notable_updates,
3192 );
3193
3194 assert!(room_info_notable_updates.contains_key(room_id));
3195
3196 assert!(room_info_notable_update.try_recv().is_err());
3198
3199 client.apply_changes(&changes, room_info_notable_updates);
3201 assert_eq!(room.latest_event().unwrap().event_id(), event.event_id());
3202
3203 assert_matches!(
3205 room_info_notable_update.recv().await,
3206 Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons }) => {
3207 assert_eq!(received_room_id, room_id);
3208 assert!(reasons.contains(RoomInfoNotableUpdateReasons::LATEST_EVENT));
3209 }
3210 );
3211 }
3212
3213 #[cfg(feature = "e2e-encryption")]
3214 #[async_test]
3215 async fn test_when_we_provide_a_newly_decrypted_event_it_replaces_latest_event() {
3216 use std::collections::BTreeMap;
3217
3218 let (_store, room) = make_room_test_helper(RoomState::Joined);
3220 add_encrypted_event(&room, "$A");
3221 assert!(room.latest_event().is_none());
3223
3224 let event = make_latest_event("$A");
3226 let mut changes = StateChanges::default();
3227 let mut room_info_notable_updates = BTreeMap::new();
3228 room.on_latest_event_decrypted(
3229 event.clone(),
3230 0,
3231 &mut changes,
3232 &mut room_info_notable_updates,
3233 );
3234 room.set_room_info(
3235 changes.room_infos.get(room.room_id()).cloned().unwrap(),
3236 room_info_notable_updates.get(room.room_id()).copied().unwrap(),
3237 );
3238
3239 assert_eq!(room.latest_event().unwrap().event_id(), event.event_id());
3241 }
3242
3243 #[cfg(feature = "e2e-encryption")]
3244 #[async_test]
3245 async fn test_when_a_newly_decrypted_event_appears_we_delete_all_older_encrypted_events() {
3246 use std::collections::BTreeMap;
3247
3248 let (_store, room) = make_room_test_helper(RoomState::Joined);
3250 room.inner.update(|info| info.latest_event = Some(make_latest_event("$A")));
3251 add_encrypted_event(&room, "$0");
3252 add_encrypted_event(&room, "$1");
3253 add_encrypted_event(&room, "$2");
3254 add_encrypted_event(&room, "$3");
3255
3256 let new_event = make_latest_event("$1");
3258 let new_event_index = 1;
3259 let mut changes = StateChanges::default();
3260 let mut room_info_notable_updates = BTreeMap::new();
3261 room.on_latest_event_decrypted(
3262 new_event.clone(),
3263 new_event_index,
3264 &mut changes,
3265 &mut room_info_notable_updates,
3266 );
3267 room.set_room_info(
3268 changes.room_infos.get(room.room_id()).cloned().unwrap(),
3269 room_info_notable_updates.get(room.room_id()).copied().unwrap(),
3270 );
3271
3272 let enc_evs = room.latest_encrypted_events();
3274 assert_eq!(enc_evs.len(), 2);
3275 assert_eq!(enc_evs[0].get_field::<&str>("event_id").unwrap().unwrap(), "$2");
3276 assert_eq!(enc_evs[1].get_field::<&str>("event_id").unwrap().unwrap(), "$3");
3277
3278 assert_eq!(room.latest_event().unwrap().event_id(), new_event.event_id());
3280 }
3281
3282 #[cfg(feature = "e2e-encryption")]
3283 #[async_test]
3284 async fn test_replacing_the_newest_event_leaves_none_left() {
3285 use std::collections::BTreeMap;
3286
3287 let (_store, room) = make_room_test_helper(RoomState::Joined);
3289 add_encrypted_event(&room, "$0");
3290 add_encrypted_event(&room, "$1");
3291 add_encrypted_event(&room, "$2");
3292 add_encrypted_event(&room, "$3");
3293
3294 let new_event = make_latest_event("$3");
3296 let new_event_index = 3;
3297 let mut changes = StateChanges::default();
3298 let mut room_info_notable_updates = BTreeMap::new();
3299 room.on_latest_event_decrypted(
3300 new_event,
3301 new_event_index,
3302 &mut changes,
3303 &mut room_info_notable_updates,
3304 );
3305 room.set_room_info(
3306 changes.room_infos.get(room.room_id()).cloned().unwrap(),
3307 room_info_notable_updates.get(room.room_id()).copied().unwrap(),
3308 );
3309
3310 let enc_evs = room.latest_encrypted_events();
3312 assert_eq!(enc_evs.len(), 0);
3313 }
3314
3315 #[cfg(feature = "e2e-encryption")]
3316 fn add_encrypted_event(room: &Room, event_id: &str) {
3317 room.latest_encrypted_events
3318 .write()
3319 .unwrap()
3320 .push(Raw::from_json_string(json!({ "event_id": event_id }).to_string()).unwrap());
3321 }
3322
3323 #[cfg(feature = "e2e-encryption")]
3324 fn make_latest_event(event_id: &str) -> Box<LatestEvent> {
3325 Box::new(LatestEvent::new(TimelineEvent::new(
3326 Raw::from_json_string(json!({ "event_id": event_id }).to_string()).unwrap(),
3327 )))
3328 }
3329
3330 fn timestamp(minutes_ago: u32) -> MilliSecondsSinceUnixEpoch {
3331 MilliSecondsSinceUnixEpoch::from_system_time(
3332 SystemTime::now().sub(Duration::from_secs((60 * minutes_ago).into())),
3333 )
3334 .expect("date out of range")
3335 }
3336
3337 fn legacy_membership_for_my_call(
3338 device_id: &DeviceId,
3339 membership_id: &str,
3340 minutes_ago: u32,
3341 ) -> LegacyMembershipData {
3342 let (application, foci) = foci_and_application();
3343 assign!(
3344 LegacyMembershipData::from(LegacyMembershipDataInit {
3345 application,
3346 device_id: device_id.to_owned(),
3347 expires: Duration::from_millis(3_600_000),
3348 foci_active: foci,
3349 membership_id: membership_id.to_owned(),
3350 }),
3351 { created_ts: Some(timestamp(minutes_ago)) }
3352 )
3353 }
3354
3355 fn legacy_member_state_event(
3356 memberships: Vec<LegacyMembershipData>,
3357 ev_id: &EventId,
3358 user_id: &UserId,
3359 ) -> AnySyncStateEvent {
3360 let content = CallMemberEventContent::new_legacy(memberships);
3361
3362 AnySyncStateEvent::CallMember(SyncStateEvent::Original(OriginalSyncCallMemberEvent {
3363 content,
3364 event_id: ev_id.to_owned(),
3365 sender: user_id.to_owned(),
3366 origin_server_ts: timestamp(0),
3369 state_key: CallMemberStateKey::new(user_id.to_owned(), None, false),
3370 unsigned: StateUnsigned::new(),
3371 }))
3372 }
3373
3374 struct InitData<'a> {
3375 device_id: &'a DeviceId,
3376 minutes_ago: u32,
3377 }
3378
3379 fn session_member_state_event(
3380 ev_id: &EventId,
3381 user_id: &UserId,
3382 init_data: Option<InitData<'_>>,
3383 ) -> AnySyncStateEvent {
3384 let application = Application::Call(CallApplicationContent::new(
3385 "my_call_id_1".to_owned(),
3386 ruma::events::call::member::CallScope::Room,
3387 ));
3388 let foci_preferred = vec![Focus::Livekit(LivekitFocus::new(
3389 "my_call_foci_alias".to_owned(),
3390 "https://lk.org".to_owned(),
3391 ))];
3392 let focus_active = ActiveFocus::Livekit(ActiveLivekitFocus::new());
3393 let (content, state_key) = match init_data {
3394 Some(InitData { device_id, minutes_ago }) => (
3395 CallMemberEventContent::new(
3396 application,
3397 device_id.to_owned(),
3398 focus_active,
3399 foci_preferred,
3400 Some(timestamp(minutes_ago)),
3401 ),
3402 CallMemberStateKey::new(user_id.to_owned(), Some(device_id.to_owned()), false),
3403 ),
3404 None => (
3405 CallMemberEventContent::new_empty(None),
3406 CallMemberStateKey::new(user_id.to_owned(), None, false),
3407 ),
3408 };
3409
3410 AnySyncStateEvent::CallMember(SyncStateEvent::Original(OriginalSyncCallMemberEvent {
3411 content,
3412 event_id: ev_id.to_owned(),
3413 sender: user_id.to_owned(),
3414 origin_server_ts: timestamp(0),
3417 state_key,
3418 unsigned: StateUnsigned::new(),
3419 }))
3420 }
3421
3422 fn foci_and_application() -> (Application, Vec<Focus>) {
3423 (
3424 Application::Call(CallApplicationContent::new(
3425 "my_call_id_1".to_owned(),
3426 ruma::events::call::member::CallScope::Room,
3427 )),
3428 vec![Focus::Livekit(LivekitFocus::new(
3429 "my_call_foci_alias".to_owned(),
3430 "https://lk.org".to_owned(),
3431 ))],
3432 )
3433 }
3434
3435 fn receive_state_events(room: &Room, events: Vec<&AnySyncStateEvent>) {
3436 room.inner.update_if(|info| {
3437 let mut res = false;
3438 for ev in events {
3439 res |= info.handle_state_event(ev);
3440 }
3441 res
3442 });
3443 }
3444
3445 fn legacy_create_call_with_member_events_for_user(a: &UserId, b: &UserId, c: &UserId) -> Room {
3449 let (_, room) = make_room_test_helper(RoomState::Joined);
3450
3451 let a_empty = legacy_member_state_event(Vec::new(), event_id!("$1234"), a);
3452
3453 let m_init_b = legacy_membership_for_my_call(device_id!("DEVICE_0"), "0", 1);
3455 let b_one = legacy_member_state_event(vec![m_init_b], event_id!("$12345"), b);
3456
3457 let m_init_c1 = legacy_membership_for_my_call(device_id!("DEVICE_0"), "0", 10);
3459 let m_init_c2 = legacy_membership_for_my_call(device_id!("DEVICE_1"), "0", 20);
3461 let c_two = legacy_member_state_event(vec![m_init_c1, m_init_c2], event_id!("$123456"), c);
3462
3463 receive_state_events(&room, vec![&c_two, &a_empty, &b_one]);
3465
3466 room
3467 }
3468
3469 fn session_create_call_with_member_events_for_user(a: &UserId, b: &UserId, c: &UserId) -> Room {
3473 let (_, room) = make_room_test_helper(RoomState::Joined);
3474
3475 let a_empty = session_member_state_event(event_id!("$1234"), a, None);
3476
3477 let b_one = session_member_state_event(
3479 event_id!("$12345"),
3480 b,
3481 Some(InitData { device_id: "DEVICE_0".into(), minutes_ago: 1 }),
3482 );
3483
3484 let m_c1 = session_member_state_event(
3485 event_id!("$123456_0"),
3486 c,
3487 Some(InitData { device_id: "DEVICE_0".into(), minutes_ago: 10 }),
3488 );
3489 let m_c2 = session_member_state_event(
3490 event_id!("$123456_1"),
3491 c,
3492 Some(InitData { device_id: "DEVICE_1".into(), minutes_ago: 20 }),
3493 );
3494 receive_state_events(&room, vec![&m_c1, &m_c2, &a_empty, &b_one]);
3496
3497 room
3498 }
3499
3500 #[test]
3501 fn test_show_correct_active_call_state() {
3502 let room_legacy = legacy_create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL);
3503
3504 assert_eq!(
3508 vec![CAROL.to_owned(), CAROL.to_owned(), BOB.to_owned()],
3509 room_legacy.active_room_call_participants()
3510 );
3511 assert!(room_legacy.has_active_room_call());
3512
3513 let room_session = session_create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL);
3514 assert_eq!(
3515 vec![CAROL.to_owned(), CAROL.to_owned(), BOB.to_owned()],
3516 room_session.active_room_call_participants()
3517 );
3518 assert!(room_session.has_active_room_call());
3519 }
3520
3521 #[test]
3522 fn test_active_call_is_false_when_everyone_left() {
3523 let room = legacy_create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL);
3524
3525 let b_empty_membership = legacy_member_state_event(Vec::new(), event_id!("$1234_1"), &BOB);
3526 let c_empty_membership =
3527 legacy_member_state_event(Vec::new(), event_id!("$12345_1"), &CAROL);
3528
3529 receive_state_events(&room, vec![&b_empty_membership, &c_empty_membership]);
3530
3531 assert_eq!(Vec::<OwnedUserId>::new(), room.active_room_call_participants());
3533 assert!(!room.has_active_room_call());
3534 }
3535
3536 #[test]
3537 fn test_calculate_room_name() {
3538 let mut actual = compute_display_name_from_heroes(2, vec!["a"]);
3539 assert_eq!(RoomDisplayName::Calculated("a".to_owned()), actual);
3540
3541 actual = compute_display_name_from_heroes(3, vec!["a", "b"]);
3542 assert_eq!(RoomDisplayName::Calculated("a, b".to_owned()), actual);
3543
3544 actual = compute_display_name_from_heroes(4, vec!["a", "b", "c"]);
3545 assert_eq!(RoomDisplayName::Calculated("a, b, c".to_owned()), actual);
3546
3547 actual = compute_display_name_from_heroes(5, vec!["a", "b", "c"]);
3548 assert_eq!(RoomDisplayName::Calculated("a, b, c, and 2 others".to_owned()), actual);
3549
3550 actual = compute_display_name_from_heroes(5, vec![]);
3551 assert_eq!(RoomDisplayName::Calculated("5 people".to_owned()), actual);
3552
3553 actual = compute_display_name_from_heroes(0, vec![]);
3554 assert_eq!(RoomDisplayName::Empty, actual);
3555
3556 actual = compute_display_name_from_heroes(1, vec![]);
3557 assert_eq!(RoomDisplayName::Empty, actual);
3558
3559 actual = compute_display_name_from_heroes(1, vec!["a"]);
3560 assert_eq!(RoomDisplayName::EmptyWas("a".to_owned()), actual);
3561
3562 actual = compute_display_name_from_heroes(1, vec!["a", "b"]);
3563 assert_eq!(RoomDisplayName::EmptyWas("a, b".to_owned()), actual);
3564
3565 actual = compute_display_name_from_heroes(1, vec!["a", "b", "c"]);
3566 assert_eq!(RoomDisplayName::EmptyWas("a, b, c".to_owned()), actual);
3567 }
3568
3569 #[test]
3570 fn test_encryption_is_set_when_encryption_event_is_received() {
3571 let (_store, room) = make_room_test_helper(RoomState::Joined);
3572
3573 assert!(room.is_encryption_state_synced().not());
3574 assert!(room.is_encrypted().not());
3575
3576 let encryption_content =
3577 RoomEncryptionEventContent::new(EventEncryptionAlgorithm::MegolmV1AesSha2);
3578 let encryption_event = AnySyncStateEvent::RoomEncryption(SyncStateEvent::Original(
3579 OriginalSyncRoomEncryptionEvent {
3580 content: encryption_content,
3581 event_id: OwnedEventId::from_str("$1234_1").unwrap(),
3582 sender: ALICE.to_owned(),
3583 origin_server_ts: timestamp(0),
3586 state_key: EmptyStateKey,
3587 unsigned: StateUnsigned::new(),
3588 },
3589 ));
3590 receive_state_events(&room, vec![&encryption_event]);
3591
3592 assert!(room.is_encryption_state_synced());
3593 assert!(room.is_encrypted());
3594 }
3595
3596 #[async_test]
3597 async fn test_room_info_migration_v1() {
3598 let store = MemoryStore::new().into_state_store();
3599
3600 let room_info_json = json!({
3601 "room_id": "!gda78o:server.tld",
3602 "room_state": "Joined",
3603 "notification_counts": {
3604 "highlight_count": 1,
3605 "notification_count": 2,
3606 },
3607 "summary": {
3608 "room_heroes": [{
3609 "user_id": "@somebody:example.org",
3610 "display_name": null,
3611 "avatar_url": null
3612 }],
3613 "joined_member_count": 5,
3614 "invited_member_count": 0,
3615 },
3616 "members_synced": true,
3617 "last_prev_batch": "pb",
3618 "sync_info": "FullySynced",
3619 "encryption_state_synced": true,
3620 "latest_event": {
3621 "event": {
3622 "encryption_info": null,
3623 "event": {
3624 "sender": "@u:i.uk",
3625 },
3626 },
3627 },
3628 "base_info": {
3629 "avatar": null,
3630 "canonical_alias": null,
3631 "create": null,
3632 "dm_targets": [],
3633 "encryption": null,
3634 "guest_access": null,
3635 "history_visibility": null,
3636 "join_rules": null,
3637 "max_power_level": 100,
3638 "name": null,
3639 "tombstone": null,
3640 "topic": null,
3641 },
3642 "read_receipts": {
3643 "num_unread": 0,
3644 "num_mentions": 0,
3645 "num_notifications": 0,
3646 "latest_active": null,
3647 "pending": []
3648 },
3649 "recency_stamp": 42,
3650 });
3651 let mut room_info: RoomInfo = serde_json::from_value(room_info_json).unwrap();
3652
3653 assert_eq!(room_info.version, 0);
3654 assert!(room_info.base_info.notable_tags.is_empty());
3655 assert!(room_info.base_info.pinned_events.is_none());
3656
3657 assert!(room_info.apply_migrations(store.clone()).await);
3659
3660 assert_eq!(room_info.version, 1);
3661 assert!(room_info.base_info.notable_tags.is_empty());
3662 assert!(room_info.base_info.pinned_events.is_none());
3663
3664 assert!(!room_info.apply_migrations(store.clone()).await);
3666
3667 assert_eq!(room_info.version, 1);
3668 assert!(room_info.base_info.notable_tags.is_empty());
3669 assert!(room_info.base_info.pinned_events.is_none());
3670
3671 let mut changes = StateChanges::default();
3673
3674 let raw_tag_event = Raw::new(&*TAG).unwrap().cast();
3675 let tag_event = raw_tag_event.deserialize().unwrap();
3676 changes.add_room_account_data(&room_info.room_id, tag_event, raw_tag_event);
3677
3678 let raw_pinned_events_event = Raw::new(&*PINNED_EVENTS).unwrap().cast();
3679 let pinned_events_event = raw_pinned_events_event.deserialize().unwrap();
3680 changes.add_state_event(&room_info.room_id, pinned_events_event, raw_pinned_events_event);
3681
3682 store.save_changes(&changes).await.unwrap();
3683
3684 room_info.version = 0;
3686 assert!(room_info.apply_migrations(store.clone()).await);
3687
3688 assert_eq!(room_info.version, 1);
3689 assert!(room_info.base_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
3690 assert!(room_info.base_info.pinned_events.is_some());
3691
3692 let new_room_info = RoomInfo::new(room_id!("!new_room:localhost"), RoomState::Joined);
3694 assert_eq!(new_room_info.version, 1);
3695 }
3696
3697 #[async_test]
3698 async fn test_prev_room_state_is_updated() {
3699 let (_store, room) = make_room_test_helper(RoomState::Invited);
3700 assert_eq!(room.prev_state(), None);
3701 assert_eq!(room.state(), RoomState::Invited);
3702
3703 let mut room_info = room.clone_info();
3705 room_info.mark_as_joined();
3706 room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP);
3707 assert_eq!(room.prev_state(), Some(RoomState::Invited));
3708 assert_eq!(room.state(), RoomState::Joined);
3709
3710 let mut room_info = room.clone_info();
3712 room_info.mark_as_joined();
3713 room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP);
3714 assert_eq!(room.prev_state(), Some(RoomState::Invited));
3715 assert_eq!(room.state(), RoomState::Joined);
3716
3717 let mut room_info = room.clone_info();
3719 room_info.mark_as_left();
3720 room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP);
3721 assert_eq!(room.prev_state(), Some(RoomState::Joined));
3722 assert_eq!(room.state(), RoomState::Left);
3723
3724 let mut room_info = room.clone_info();
3726 room_info.mark_as_banned();
3727 room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP);
3728 assert_eq!(room.prev_state(), Some(RoomState::Left));
3729 assert_eq!(room.state(), RoomState::Banned);
3730 }
3731
3732 #[async_test]
3733 async fn test_room_state_filters() {
3734 let client = logged_in_base_client(None).await;
3735
3736 let joined_room_id = owned_room_id!("!joined:example.org");
3737 client.get_or_create_room(&joined_room_id, RoomState::Joined);
3738
3739 let invited_room_id = owned_room_id!("!invited:example.org");
3740 client.get_or_create_room(&invited_room_id, RoomState::Invited);
3741
3742 let left_room_id = owned_room_id!("!left:example.org");
3743 client.get_or_create_room(&left_room_id, RoomState::Left);
3744
3745 let knocked_room_id = owned_room_id!("!knocked:example.org");
3746 client.get_or_create_room(&knocked_room_id, RoomState::Knocked);
3747
3748 let banned_room_id = owned_room_id!("!banned:example.org");
3749 client.get_or_create_room(&banned_room_id, RoomState::Banned);
3750
3751 let joined_rooms = client.rooms_filtered(RoomStateFilter::JOINED);
3752 assert_eq!(joined_rooms.len(), 1);
3753 assert_eq!(joined_rooms[0].state(), RoomState::Joined);
3754 assert_eq!(joined_rooms[0].room_id, joined_room_id);
3755
3756 let invited_rooms = client.rooms_filtered(RoomStateFilter::INVITED);
3757 assert_eq!(invited_rooms.len(), 1);
3758 assert_eq!(invited_rooms[0].state(), RoomState::Invited);
3759 assert_eq!(invited_rooms[0].room_id, invited_room_id);
3760
3761 let left_rooms = client.rooms_filtered(RoomStateFilter::LEFT);
3762 assert_eq!(left_rooms.len(), 1);
3763 assert_eq!(left_rooms[0].state(), RoomState::Left);
3764 assert_eq!(left_rooms[0].room_id, left_room_id);
3765
3766 let knocked_rooms = client.rooms_filtered(RoomStateFilter::KNOCKED);
3767 assert_eq!(knocked_rooms.len(), 1);
3768 assert_eq!(knocked_rooms[0].state(), RoomState::Knocked);
3769 assert_eq!(knocked_rooms[0].room_id, knocked_room_id);
3770
3771 let banned_rooms = client.rooms_filtered(RoomStateFilter::BANNED);
3772 assert_eq!(banned_rooms.len(), 1);
3773 assert_eq!(banned_rooms[0].state(), RoomState::Banned);
3774 assert_eq!(banned_rooms[0].room_id, banned_room_id);
3775 }
3776
3777 #[test]
3778 fn test_room_state_filters_as_vec() {
3779 assert_eq!(RoomStateFilter::JOINED.as_vec(), vec![RoomState::Joined]);
3780 assert_eq!(RoomStateFilter::LEFT.as_vec(), vec![RoomState::Left]);
3781 assert_eq!(RoomStateFilter::INVITED.as_vec(), vec![RoomState::Invited]);
3782 assert_eq!(RoomStateFilter::KNOCKED.as_vec(), vec![RoomState::Knocked]);
3783 assert_eq!(RoomStateFilter::BANNED.as_vec(), vec![RoomState::Banned]);
3784
3785 assert_eq!(
3787 RoomStateFilter::all().as_vec(),
3788 vec![
3789 RoomState::Joined,
3790 RoomState::Left,
3791 RoomState::Invited,
3792 RoomState::Knocked,
3793 RoomState::Banned
3794 ]
3795 );
3796 }
3797}