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")]
277 const MAX_ENCRYPTED_EVENTS: std::num::NonZeroUsize =
278 unsafe { std::num::NonZeroUsize::new_unchecked(10) };
279
280 pub(crate) fn new(
281 own_user_id: &UserId,
282 store: Arc<DynStateStore>,
283 room_id: &RoomId,
284 room_state: RoomState,
285 room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
286 ) -> Self {
287 let room_info = RoomInfo::new(room_id, room_state);
288 Self::restore(own_user_id, store, room_info, room_info_notable_update_sender)
289 }
290
291 pub(crate) fn restore(
292 own_user_id: &UserId,
293 store: Arc<DynStateStore>,
294 room_info: RoomInfo,
295 room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
296 ) -> Self {
297 let (room_member_updates_sender, _) = broadcast::channel(10);
298 Self {
299 own_user_id: own_user_id.into(),
300 room_id: room_info.room_id.clone(),
301 store,
302 inner: SharedObservable::new(room_info),
303 #[cfg(feature = "e2e-encryption")]
304 latest_encrypted_events: Arc::new(SyncRwLock::new(RingBuffer::new(
305 Self::MAX_ENCRYPTED_EVENTS,
306 ))),
307 room_info_notable_update_sender,
308 seen_knock_request_ids_map: SharedObservable::new_async(None),
309 room_member_updates_sender,
310 }
311 }
312
313 pub fn room_id(&self) -> &RoomId {
315 &self.room_id
316 }
317
318 pub fn creator(&self) -> Option<OwnedUserId> {
320 self.inner.read().creator().map(ToOwned::to_owned)
321 }
322
323 pub fn own_user_id(&self) -> &UserId {
325 &self.own_user_id
326 }
327
328 pub fn state(&self) -> RoomState {
330 self.inner.read().room_state
331 }
332
333 pub fn prev_state(&self) -> Option<RoomState> {
335 self.inner.read().prev_room_state
336 }
337
338 pub fn is_space(&self) -> bool {
340 self.inner.read().room_type().is_some_and(|t| *t == RoomType::Space)
341 }
342
343 pub fn room_type(&self) -> Option<RoomType> {
346 self.inner.read().room_type().map(ToOwned::to_owned)
347 }
348
349 pub fn unread_notification_counts(&self) -> UnreadNotificationsCount {
351 self.inner.read().notification_counts
352 }
353
354 pub fn num_unread_messages(&self) -> u64 {
359 self.inner.read().read_receipts.num_unread
360 }
361
362 pub fn read_receipts(&self) -> RoomReadReceipts {
364 self.inner.read().read_receipts.clone()
365 }
366
367 pub fn num_unread_notifications(&self) -> u64 {
372 self.inner.read().read_receipts.num_notifications
373 }
374
375 pub fn num_unread_mentions(&self) -> u64 {
381 self.inner.read().read_receipts.num_mentions
382 }
383
384 pub fn are_members_synced(&self) -> bool {
391 self.inner.read().members_synced
392 }
393
394 #[cfg(feature = "testing")]
399 pub fn mark_members_synced(&self) {
400 self.inner.update(|info| {
401 info.members_synced = true;
402 });
403 }
404
405 pub fn mark_members_missing(&self) {
407 self.inner.update_if(|info| {
408 mem::replace(&mut info.members_synced, false)
410 })
411 }
412
413 pub fn is_state_fully_synced(&self) -> bool {
421 self.inner.read().sync_info == SyncInfo::FullySynced
422 }
423
424 pub fn is_state_partially_or_fully_synced(&self) -> bool {
428 self.inner.read().sync_info != SyncInfo::NoState
429 }
430
431 pub fn is_encryption_state_synced(&self) -> bool {
438 self.inner.read().encryption_state_synced
439 }
440
441 pub fn last_prev_batch(&self) -> Option<String> {
444 self.inner.read().last_prev_batch.clone()
445 }
446
447 pub fn avatar_url(&self) -> Option<OwnedMxcUri> {
449 self.inner.read().avatar_url().map(ToOwned::to_owned)
450 }
451
452 pub fn avatar_info(&self) -> Option<avatar::ImageInfo> {
454 self.inner.read().avatar_info().map(ToOwned::to_owned)
455 }
456
457 pub fn canonical_alias(&self) -> Option<OwnedRoomAliasId> {
459 self.inner.read().canonical_alias().map(ToOwned::to_owned)
460 }
461
462 pub fn alt_aliases(&self) -> Vec<OwnedRoomAliasId> {
464 self.inner.read().alt_aliases().to_owned()
465 }
466
467 pub fn create_content(&self) -> Option<RoomCreateWithCreatorEventContent> {
477 match self.inner.read().base_info.create.as_ref()? {
478 MinimalStateEvent::Original(ev) => Some(ev.content.clone()),
479 MinimalStateEvent::Redacted(ev) => Some(ev.content.clone()),
480 }
481 }
482
483 #[instrument(skip_all, fields(room_id = ?self.room_id))]
487 pub async fn is_direct(&self) -> StoreResult<bool> {
488 match self.state() {
489 RoomState::Joined | RoomState::Left | RoomState::Banned => {
490 Ok(!self.inner.read().base_info.dm_targets.is_empty())
491 }
492
493 RoomState::Invited => {
494 let member = self.get_member(self.own_user_id()).await?;
495
496 match member {
497 None => {
498 info!("RoomMember not found for the user's own id");
499 Ok(false)
500 }
501 Some(member) => match member.event.as_ref() {
502 MemberEvent::Sync(_) => {
503 warn!("Got MemberEvent::Sync in an invited room");
504 Ok(false)
505 }
506 MemberEvent::Stripped(event) => {
507 Ok(event.content.is_direct.unwrap_or(false))
508 }
509 },
510 }
511 }
512
513 RoomState::Knocked => Ok(false),
515 }
516 }
517
518 pub fn direct_targets(&self) -> HashSet<OwnedDirectUserIdentifier> {
527 self.inner.read().base_info.dm_targets.clone()
528 }
529
530 pub fn direct_targets_length(&self) -> usize {
533 self.inner.read().base_info.dm_targets.len()
534 }
535
536 pub fn is_encrypted(&self) -> bool {
538 self.inner.read().is_encrypted()
539 }
540
541 pub fn encryption_settings(&self) -> Option<RoomEncryptionEventContent> {
544 self.inner.read().base_info.encryption.clone()
545 }
546
547 pub fn guest_access(&self) -> GuestAccess {
549 self.inner.read().guest_access().clone()
550 }
551
552 pub fn history_visibility(&self) -> Option<HistoryVisibility> {
554 self.inner.read().history_visibility().cloned()
555 }
556
557 pub fn history_visibility_or_default(&self) -> HistoryVisibility {
560 self.inner.read().history_visibility_or_default().clone()
561 }
562
563 pub fn is_public(&self) -> bool {
565 matches!(self.join_rule(), JoinRule::Public)
566 }
567
568 pub fn join_rule(&self) -> JoinRule {
570 self.inner.read().join_rule().clone()
571 }
572
573 pub fn max_power_level(&self) -> i64 {
578 self.inner.read().base_info.max_power_level
579 }
580
581 pub async fn power_levels(&self) -> Result<RoomPowerLevels, Error> {
583 Ok(self
584 .store
585 .get_state_event_static::<RoomPowerLevelsEventContent>(self.room_id())
586 .await?
587 .ok_or(Error::InsufficientData)?
588 .deserialize()?
589 .power_levels())
590 }
591
592 pub fn name(&self) -> Option<String> {
597 self.inner.read().name().map(ToOwned::to_owned)
598 }
599
600 pub fn is_tombstoned(&self) -> bool {
602 self.inner.read().base_info.tombstone.is_some()
603 }
604
605 pub fn tombstone(&self) -> Option<RoomTombstoneEventContent> {
607 self.inner.read().tombstone().cloned()
608 }
609
610 pub fn topic(&self) -> Option<String> {
612 self.inner.read().topic().map(ToOwned::to_owned)
613 }
614
615 pub fn has_active_room_call(&self) -> bool {
618 self.inner.read().has_active_room_call()
619 }
620
621 pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
630 self.inner.read().active_room_call_participants()
631 }
632
633 pub async fn display_name(&self) -> StoreResult<RoomDisplayName> {
648 if let Some(name) = self.cached_display_name() {
649 Ok(name)
650 } else {
651 self.compute_display_name().await
652 }
653 }
654
655 pub(crate) async fn compute_display_name(&self) -> StoreResult<RoomDisplayName> {
667 enum DisplayNameOrSummary {
668 Summary(RoomSummary),
669 DisplayName(RoomDisplayName),
670 }
671
672 let display_name_or_summary = {
673 let inner = self.inner.read();
674
675 match (inner.name(), inner.canonical_alias()) {
676 (Some(name), _) => {
677 let name = RoomDisplayName::Named(name.trim().to_owned());
678 DisplayNameOrSummary::DisplayName(name)
679 }
680 (None, Some(alias)) => {
681 let name = RoomDisplayName::Aliased(alias.alias().trim().to_owned());
682 DisplayNameOrSummary::DisplayName(name)
683 }
684 (None, None) => DisplayNameOrSummary::Summary(inner.summary.clone()),
689 }
690 };
691
692 let display_name = match display_name_or_summary {
693 DisplayNameOrSummary::Summary(summary) => {
694 self.compute_display_name_from_summary(summary).await?
695 }
696 DisplayNameOrSummary::DisplayName(display_name) => display_name,
697 };
698
699 self.inner.update_if(|info| {
701 if info.cached_display_name.as_ref() != Some(&display_name) {
702 info.cached_display_name = Some(display_name.clone());
703 true
704 } else {
705 false
706 }
707 });
708
709 Ok(display_name)
710 }
711
712 async fn compute_display_name_from_summary(
714 &self,
715 summary: RoomSummary,
716 ) -> StoreResult<RoomDisplayName> {
717 let computed_summary = if !summary.room_heroes.is_empty() {
718 self.extract_and_augment_summary(&summary).await?
719 } else {
720 self.compute_summary().await?
721 };
722
723 let ComputedSummary { heroes, num_service_members, num_joined_invited_guess } =
724 computed_summary;
725
726 let summary_member_count = (summary.joined_member_count + summary.invited_member_count)
727 .saturating_sub(num_service_members);
728
729 let num_joined_invited = if self.state() == RoomState::Invited {
730 heroes.len() as u64 + 1
733 } else if summary_member_count == 0 {
734 num_joined_invited_guess
735 } else {
736 summary_member_count
737 };
738
739 debug!(
740 room_id = ?self.room_id(),
741 own_user = ?self.own_user_id,
742 num_joined_invited,
743 heroes = ?heroes,
744 "Calculating name for a room based on heroes",
745 );
746
747 let display_name = compute_display_name_from_heroes(
748 num_joined_invited,
749 heroes.iter().map(|hero| hero.as_str()).collect(),
750 );
751
752 Ok(display_name)
753 }
754
755 async fn extract_and_augment_summary(
764 &self,
765 summary: &RoomSummary,
766 ) -> StoreResult<ComputedSummary> {
767 let heroes = &summary.room_heroes;
768
769 let mut names = Vec::with_capacity(heroes.len());
770 let own_user_id = self.own_user_id();
771 let member_hints = self.get_member_hints().await?;
772
773 let num_service_members = heroes
778 .iter()
779 .filter(|hero| member_hints.service_members.contains(&hero.user_id))
780 .count() as u64;
781
782 let heroes_filter = heroes_filter(own_user_id, &member_hints);
785 let heroes_filter = |hero: &&RoomHero| heroes_filter(&hero.user_id);
786
787 for hero in heroes.iter().filter(heroes_filter) {
788 if let Some(display_name) = &hero.display_name {
789 names.push(display_name.clone());
790 } else {
791 match self.get_member(&hero.user_id).await {
792 Ok(Some(member)) => {
793 names.push(member.name().to_owned());
794 }
795 Ok(None) => {
796 warn!("Ignoring hero, no member info for {}", hero.user_id);
797 }
798 Err(error) => {
799 warn!("Ignoring hero, error getting member: {}", error);
800 }
801 }
802 }
803 }
804
805 let num_joined_invited_guess = summary.joined_member_count + summary.invited_member_count;
806
807 let num_joined_invited_guess = if num_joined_invited_guess == 0 {
810 let guess = self
811 .store
812 .get_user_ids(self.room_id(), RoomMemberships::JOIN | RoomMemberships::INVITE)
813 .await?
814 .len() as u64;
815
816 guess.saturating_sub(num_service_members)
817 } else {
818 num_joined_invited_guess
820 };
821
822 Ok(ComputedSummary { heroes: names, num_service_members, num_joined_invited_guess })
823 }
824
825 async fn compute_summary(&self) -> StoreResult<ComputedSummary> {
831 let member_hints = self.get_member_hints().await?;
832
833 let heroes_filter = heroes_filter(&self.own_user_id, &member_hints);
836 let heroes_filter = |u: &RoomMember| heroes_filter(u.user_id());
837
838 let mut members = self.members(RoomMemberships::JOIN | RoomMemberships::INVITE).await?;
839
840 let num_service_members = members
844 .iter()
845 .filter(|member| member_hints.service_members.contains(member.user_id()))
846 .count();
847
848 let num_joined_invited = members.len() - num_service_members;
855
856 if num_joined_invited == 0
857 || (num_joined_invited == 1 && members[0].user_id() == self.own_user_id)
858 {
859 members = self.members(RoomMemberships::LEAVE | RoomMemberships::BAN).await?;
861 }
862
863 members.sort_unstable_by(|lhs, rhs| lhs.name().cmp(rhs.name()));
865
866 let heroes = members
867 .into_iter()
868 .filter(heroes_filter)
869 .take(NUM_HEROES)
870 .map(|u| u.name().to_owned())
871 .collect();
872
873 trace!(
874 ?heroes,
875 num_joined_invited,
876 num_service_members,
877 "Computed a room summary since we didn't receive one."
878 );
879
880 let num_service_members = num_service_members as u64;
881 let num_joined_invited_guess = num_joined_invited as u64;
882
883 Ok(ComputedSummary { heroes, num_service_members, num_joined_invited_guess })
884 }
885
886 async fn get_member_hints(&self) -> StoreResult<MemberHintsEventContent> {
887 Ok(self
888 .store
889 .get_state_event_static::<MemberHintsEventContent>(self.room_id())
890 .await?
891 .and_then(|event| {
892 event
893 .deserialize()
894 .inspect_err(|e| warn!("Couldn't deserialize the member hints event: {e}"))
895 .ok()
896 })
897 .and_then(|event| as_variant!(event, SyncOrStrippedState::Sync(SyncStateEvent::Original(e)) => e.content))
898 .unwrap_or_default())
899 }
900
901 pub fn cached_display_name(&self) -> Option<RoomDisplayName> {
905 self.inner.read().cached_display_name.clone()
906 }
907
908 pub fn update_cached_user_defined_notification_mode(&self, mode: RoomNotificationMode) {
914 self.inner.update_if(|info| {
915 if info.cached_user_defined_notification_mode.as_ref() != Some(&mode) {
916 info.cached_user_defined_notification_mode = Some(mode);
917
918 true
919 } else {
920 false
921 }
922 });
923 }
924
925 pub fn cached_user_defined_notification_mode(&self) -> Option<RoomNotificationMode> {
930 self.inner.read().cached_user_defined_notification_mode
931 }
932
933 pub fn latest_event(&self) -> Option<LatestEvent> {
936 self.inner.read().latest_event.as_deref().cloned()
937 }
938
939 #[cfg(feature = "e2e-encryption")]
944 pub(crate) fn latest_encrypted_events(&self) -> Vec<Raw<AnySyncTimelineEvent>> {
945 self.latest_encrypted_events.read().unwrap().iter().cloned().collect()
946 }
947
948 #[cfg(feature = "e2e-encryption")]
959 pub(crate) fn on_latest_event_decrypted(
960 &self,
961 latest_event: Box<LatestEvent>,
962 index: usize,
963 changes: &mut crate::StateChanges,
964 room_info_notable_updates: &mut BTreeMap<OwnedRoomId, RoomInfoNotableUpdateReasons>,
965 ) {
966 self.latest_encrypted_events.write().unwrap().drain(0..=index);
967
968 let room_info = changes
969 .room_infos
970 .entry(self.room_id().to_owned())
971 .or_insert_with(|| self.clone_info());
972
973 room_info.latest_event = Some(latest_event);
974
975 room_info_notable_updates
976 .entry(self.room_id().to_owned())
977 .or_default()
978 .insert(RoomInfoNotableUpdateReasons::LATEST_EVENT);
979 }
980
981 pub async fn joined_user_ids(&self) -> StoreResult<Vec<OwnedUserId>> {
984 self.store.get_user_ids(self.room_id(), RoomMemberships::JOIN).await
985 }
986
987 pub async fn members(&self, memberships: RoomMemberships) -> StoreResult<Vec<RoomMember>> {
990 let user_ids = self.store.get_user_ids(self.room_id(), memberships).await?;
991
992 if user_ids.is_empty() {
993 return Ok(Vec::new());
994 }
995
996 let member_events = self
997 .store
998 .get_state_events_for_keys_static::<RoomMemberEventContent, _, _>(
999 self.room_id(),
1000 &user_ids,
1001 )
1002 .await?
1003 .into_iter()
1004 .map(|raw_event| raw_event.deserialize())
1005 .collect::<Result<Vec<_>, _>>()?;
1006
1007 let mut profiles = self.store.get_profiles(self.room_id(), &user_ids).await?;
1008
1009 let mut presences = self
1010 .store
1011 .get_presence_events(&user_ids)
1012 .await?
1013 .into_iter()
1014 .filter_map(|e| {
1015 e.deserialize().ok().map(|presence| (presence.sender.clone(), presence))
1016 })
1017 .collect::<BTreeMap<_, _>>();
1018
1019 let display_names = member_events.iter().map(|e| e.display_name()).collect::<Vec<_>>();
1020 let room_info = self.member_room_info(&display_names).await?;
1021
1022 let mut members = Vec::new();
1023
1024 for event in member_events {
1025 let profile = profiles.remove(event.user_id());
1026 let presence = presences.remove(event.user_id());
1027 members.push(RoomMember::from_parts(event, profile, presence, &room_info))
1028 }
1029
1030 Ok(members)
1031 }
1032
1033 pub fn heroes(&self) -> Vec<RoomHero> {
1035 self.inner.read().heroes().to_vec()
1036 }
1037
1038 pub fn active_members_count(&self) -> u64 {
1041 self.inner.read().active_members_count()
1042 }
1043
1044 pub fn invited_members_count(&self) -> u64 {
1046 self.inner.read().invited_members_count()
1047 }
1048
1049 pub fn joined_members_count(&self) -> u64 {
1051 self.inner.read().joined_members_count()
1052 }
1053
1054 pub fn subscribe_info(&self) -> Subscriber<RoomInfo> {
1056 self.inner.subscribe()
1057 }
1058
1059 pub fn clone_info(&self) -> RoomInfo {
1061 self.inner.get()
1062 }
1063
1064 pub fn set_room_info(
1066 &self,
1067 room_info: RoomInfo,
1068 room_info_notable_update_reasons: RoomInfoNotableUpdateReasons,
1069 ) {
1070 self.inner.set(room_info);
1071
1072 let _ = self.room_info_notable_update_sender.send(RoomInfoNotableUpdate {
1074 room_id: self.room_id.clone(),
1075 reasons: room_info_notable_update_reasons,
1076 });
1077 }
1078
1079 pub async fn get_member(&self, user_id: &UserId) -> StoreResult<Option<RoomMember>> {
1087 let Some(raw_event) = self.store.get_member_event(self.room_id(), user_id).await? else {
1088 debug!(%user_id, "Member event not found in state store");
1089 return Ok(None);
1090 };
1091
1092 let event = raw_event.deserialize()?;
1093
1094 let presence =
1095 self.store.get_presence_event(user_id).await?.and_then(|e| e.deserialize().ok());
1096
1097 let profile = self.store.get_profile(self.room_id(), user_id).await?;
1098
1099 let display_names = [event.display_name()];
1100 let room_info = self.member_room_info(&display_names).await?;
1101
1102 Ok(Some(RoomMember::from_parts(event, profile, presence, &room_info)))
1103 }
1104
1105 async fn member_room_info<'a>(
1109 &self,
1110 display_names: &'a [DisplayName],
1111 ) -> StoreResult<MemberRoomInfo<'a>> {
1112 let max_power_level = self.max_power_level();
1113 let room_creator = self.inner.read().creator().map(ToOwned::to_owned);
1114
1115 let power_levels = self
1116 .store
1117 .get_state_event_static(self.room_id())
1118 .await?
1119 .and_then(|e| e.deserialize().ok());
1120
1121 let users_display_names =
1122 self.store.get_users_with_display_names(self.room_id(), display_names).await?;
1123
1124 let ignored_users = self
1125 .store
1126 .get_account_data_event_static::<IgnoredUserListEventContent>()
1127 .await?
1128 .map(|c| c.deserialize())
1129 .transpose()?
1130 .map(|e| e.content.ignored_users.into_keys().collect());
1131
1132 Ok(MemberRoomInfo {
1133 power_levels: power_levels.into(),
1134 max_power_level,
1135 room_creator,
1136 users_display_names,
1137 ignored_users,
1138 })
1139 }
1140
1141 pub async fn tags(&self) -> StoreResult<Option<Tags>> {
1143 if let Some(AnyRoomAccountDataEvent::Tag(event)) = self
1144 .store
1145 .get_room_account_data_event(self.room_id(), RoomAccountDataEventType::Tag)
1146 .await?
1147 .and_then(|r| r.deserialize().ok())
1148 {
1149 Ok(Some(event.content.tags))
1150 } else {
1151 Ok(None)
1152 }
1153 }
1154
1155 pub fn is_favourite(&self) -> bool {
1159 self.inner.read().base_info.notable_tags.contains(RoomNotableTags::FAVOURITE)
1160 }
1161
1162 pub fn is_low_priority(&self) -> bool {
1167 self.inner.read().base_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY)
1168 }
1169
1170 pub async fn load_user_receipt(
1173 &self,
1174 receipt_type: ReceiptType,
1175 thread: ReceiptThread,
1176 user_id: &UserId,
1177 ) -> StoreResult<Option<(OwnedEventId, Receipt)>> {
1178 self.store.get_user_room_receipt_event(self.room_id(), receipt_type, thread, user_id).await
1179 }
1180
1181 pub async fn load_event_receipts(
1185 &self,
1186 receipt_type: ReceiptType,
1187 thread: ReceiptThread,
1188 event_id: &EventId,
1189 ) -> StoreResult<Vec<(OwnedUserId, Receipt)>> {
1190 self.store
1191 .get_event_room_receipt_events(self.room_id(), receipt_type, thread, event_id)
1192 .await
1193 }
1194
1195 pub fn is_marked_unread(&self) -> bool {
1198 self.inner.read().base_info.is_marked_unread
1199 }
1200
1201 pub fn recency_stamp(&self) -> Option<u64> {
1205 self.inner.read().recency_stamp
1206 }
1207
1208 pub fn pinned_event_ids_stream(&self) -> impl Stream<Item = Vec<OwnedEventId>> {
1211 self.inner
1212 .subscribe()
1213 .map(|i| i.base_info.pinned_events.map(|c| c.pinned).unwrap_or_default())
1214 }
1215
1216 pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
1218 self.inner.read().pinned_event_ids()
1219 }
1220
1221 pub async fn mark_knock_requests_as_seen(&self, user_ids: &[OwnedUserId]) -> StoreResult<()> {
1224 let raw_user_ids: Vec<&str> = user_ids.iter().map(|id| id.as_str()).collect();
1225 let member_raw_events = self
1226 .store
1227 .get_state_events_for_keys(self.room_id(), StateEventType::RoomMember, &raw_user_ids)
1228 .await?;
1229 let mut event_to_user_ids = Vec::with_capacity(member_raw_events.len());
1230
1231 for raw_event in member_raw_events {
1234 let event = raw_event.cast::<RoomMemberEventContent>().deserialize()?;
1235 match event {
1236 SyncOrStrippedState::Sync(SyncStateEvent::Original(event)) => {
1237 if event.content.membership == MembershipState::Knock {
1238 event_to_user_ids.push((event.event_id, event.state_key))
1239 } else {
1240 warn!("Could not mark knock event as seen: event {} for user {} is not in Knock membership state.", event.event_id, event.state_key);
1241 }
1242 }
1243 _ => warn!(
1244 "Could not mark knock event as seen: event for user {} is not valid.",
1245 event.state_key()
1246 ),
1247 }
1248 }
1249
1250 let current_seen_events_guard = self.get_write_guarded_current_knock_request_ids().await?;
1251 let mut current_seen_events = current_seen_events_guard.clone().unwrap_or_default();
1252
1253 current_seen_events.extend(event_to_user_ids);
1254
1255 self.update_seen_knock_request_ids(current_seen_events_guard, current_seen_events).await?;
1256
1257 Ok(())
1258 }
1259
1260 pub async fn remove_outdated_seen_knock_requests_ids(&self) -> StoreResult<()> {
1263 let current_seen_events_guard = self.get_write_guarded_current_knock_request_ids().await?;
1264 let mut current_seen_events = current_seen_events_guard.clone().unwrap_or_default();
1265
1266 let keys: Vec<OwnedUserId> = current_seen_events.values().map(|id| id.to_owned()).collect();
1268 let raw_member_events: Vec<RawMemberEvent> =
1269 self.store.get_state_events_for_keys_static(self.room_id(), &keys).await?;
1270 let member_events = raw_member_events
1271 .into_iter()
1272 .map(|raw| raw.deserialize())
1273 .collect::<Result<Vec<MemberEvent>, _>>()?;
1274
1275 let mut ids_to_remove = Vec::new();
1276
1277 for (event_id, user_id) in current_seen_events.iter() {
1278 let matching_member = member_events.iter().find(|event| event.user_id() == user_id);
1281
1282 if let Some(member) = matching_member {
1283 let member_event_id = member.event_id();
1284 if *member.membership() != MembershipState::Knock
1286 || member_event_id.is_some_and(|id| id != event_id)
1287 {
1288 ids_to_remove.push(event_id.to_owned());
1289 }
1290 } else {
1291 ids_to_remove.push(event_id.to_owned());
1292 }
1293 }
1294
1295 if ids_to_remove.is_empty() {
1297 return Ok(());
1298 }
1299
1300 for event_id in ids_to_remove {
1301 current_seen_events.remove(&event_id);
1302 }
1303
1304 self.update_seen_knock_request_ids(current_seen_events_guard, current_seen_events).await?;
1305
1306 Ok(())
1307 }
1308
1309 pub async fn get_seen_knock_request_ids(
1311 &self,
1312 ) -> Result<BTreeMap<OwnedEventId, OwnedUserId>, StoreError> {
1313 Ok(self.get_write_guarded_current_knock_request_ids().await?.clone().unwrap_or_default())
1314 }
1315
1316 async fn get_write_guarded_current_knock_request_ids(
1317 &self,
1318 ) -> StoreResult<ObservableWriteGuard<'_, Option<BTreeMap<OwnedEventId, OwnedUserId>>, AsyncLock>>
1319 {
1320 let mut guard = self.seen_knock_request_ids_map.write().await;
1321 if guard.is_none() {
1323 let updated_seen_ids = self
1325 .store
1326 .get_kv_data(StateStoreDataKey::SeenKnockRequests(self.room_id()))
1327 .await?
1328 .and_then(|v| v.into_seen_knock_requests())
1329 .unwrap_or_default();
1330
1331 ObservableWriteGuard::set(&mut guard, Some(updated_seen_ids));
1332 }
1333 Ok(guard)
1334 }
1335
1336 async fn update_seen_knock_request_ids(
1337 &self,
1338 mut guard: ObservableWriteGuard<'_, Option<BTreeMap<OwnedEventId, OwnedUserId>>, AsyncLock>,
1339 new_value: BTreeMap<OwnedEventId, OwnedUserId>,
1340 ) -> StoreResult<()> {
1341 ObservableWriteGuard::set(&mut guard, Some(new_value.clone()));
1343
1344 self.store
1346 .set_kv_data(
1347 StateStoreDataKey::SeenKnockRequests(self.room_id()),
1348 StateStoreDataValue::SeenKnockRequests(new_value),
1349 )
1350 .await?;
1351
1352 Ok(())
1353 }
1354}
1355
1356#[cfg(not(feature = "test-send-sync"))]
1358unsafe impl Send for Room {}
1359
1360#[cfg(not(feature = "test-send-sync"))]
1362unsafe impl Sync for Room {}
1363
1364#[cfg(feature = "test-send-sync")]
1365#[test]
1366fn test_send_sync_for_room() {
1368 fn assert_send_sync<T: Send + Sync>() {}
1369
1370 assert_send_sync::<Room>();
1371}
1372
1373#[derive(Clone, Debug, Serialize, Deserialize)]
1377pub struct RoomInfo {
1378 #[serde(default)]
1380 pub(crate) version: u8,
1381
1382 pub(crate) room_id: OwnedRoomId,
1384
1385 pub(crate) room_state: RoomState,
1387
1388 pub(crate) prev_room_state: Option<RoomState>,
1390
1391 pub(crate) notification_counts: UnreadNotificationsCount,
1396
1397 pub(crate) summary: RoomSummary,
1399
1400 pub(crate) members_synced: bool,
1402
1403 pub(crate) last_prev_batch: Option<String>,
1405
1406 pub(crate) sync_info: SyncInfo,
1408
1409 pub(crate) encryption_state_synced: bool,
1411
1412 pub(crate) latest_event: Option<Box<LatestEvent>>,
1414
1415 #[serde(default)]
1417 pub(crate) read_receipts: RoomReadReceipts,
1418
1419 pub(crate) base_info: Box<BaseRoomInfo>,
1422
1423 #[serde(skip)]
1427 pub(crate) warned_about_unknown_room_version: Arc<AtomicBool>,
1428
1429 #[serde(default, skip_serializing_if = "Option::is_none")]
1434 pub(crate) cached_display_name: Option<RoomDisplayName>,
1435
1436 #[serde(default, skip_serializing_if = "Option::is_none")]
1438 pub(crate) cached_user_defined_notification_mode: Option<RoomNotificationMode>,
1439
1440 #[serde(default)]
1447 pub(crate) recency_stamp: Option<u64>,
1448}
1449
1450#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1451pub(crate) enum SyncInfo {
1452 NoState,
1458
1459 PartiallySynced,
1462
1463 FullySynced,
1465}
1466
1467impl RoomInfo {
1468 #[doc(hidden)] pub fn new(room_id: &RoomId, room_state: RoomState) -> Self {
1470 Self {
1471 version: 1,
1472 room_id: room_id.into(),
1473 room_state,
1474 prev_room_state: None,
1475 notification_counts: Default::default(),
1476 summary: Default::default(),
1477 members_synced: false,
1478 last_prev_batch: None,
1479 sync_info: SyncInfo::NoState,
1480 encryption_state_synced: false,
1481 latest_event: None,
1482 read_receipts: Default::default(),
1483 base_info: Box::new(BaseRoomInfo::new()),
1484 warned_about_unknown_room_version: Arc::new(false.into()),
1485 cached_display_name: None,
1486 cached_user_defined_notification_mode: None,
1487 recency_stamp: None,
1488 }
1489 }
1490
1491 pub fn mark_as_joined(&mut self) {
1493 self.set_state(RoomState::Joined);
1494 }
1495
1496 pub fn mark_as_left(&mut self) {
1498 self.set_state(RoomState::Left);
1499 }
1500
1501 pub fn mark_as_invited(&mut self) {
1503 self.set_state(RoomState::Invited);
1504 }
1505
1506 pub fn mark_as_knocked(&mut self) {
1508 self.set_state(RoomState::Knocked);
1509 }
1510
1511 pub fn mark_as_banned(&mut self) {
1513 self.set_state(RoomState::Banned);
1514 }
1515
1516 pub fn set_state(&mut self, room_state: RoomState) {
1518 if room_state != self.room_state {
1519 self.prev_room_state = Some(self.room_state);
1520 self.room_state = room_state;
1521 }
1522 }
1523
1524 pub fn mark_members_synced(&mut self) {
1526 self.members_synced = true;
1527 }
1528
1529 pub fn mark_members_missing(&mut self) {
1531 self.members_synced = false;
1532 }
1533
1534 pub fn are_members_synced(&self) -> bool {
1536 self.members_synced
1537 }
1538
1539 pub fn mark_state_partially_synced(&mut self) {
1541 self.sync_info = SyncInfo::PartiallySynced;
1542 }
1543
1544 pub fn mark_state_fully_synced(&mut self) {
1546 self.sync_info = SyncInfo::FullySynced;
1547 }
1548
1549 pub fn mark_state_not_synced(&mut self) {
1551 self.sync_info = SyncInfo::NoState;
1552 }
1553
1554 pub fn mark_encryption_state_synced(&mut self) {
1556 self.encryption_state_synced = true;
1557 }
1558
1559 pub fn mark_encryption_state_missing(&mut self) {
1561 self.encryption_state_synced = false;
1562 }
1563
1564 pub fn set_prev_batch(&mut self, prev_batch: Option<&str>) -> bool {
1568 if self.last_prev_batch.as_deref() != prev_batch {
1569 self.last_prev_batch = prev_batch.map(|p| p.to_owned());
1570 true
1571 } else {
1572 false
1573 }
1574 }
1575
1576 pub fn state(&self) -> RoomState {
1578 self.room_state
1579 }
1580
1581 pub fn is_encrypted(&self) -> bool {
1583 self.base_info.encryption.is_some()
1584 }
1585
1586 pub fn set_encryption_event(&mut self, event: Option<RoomEncryptionEventContent>) {
1588 self.base_info.encryption = event;
1589 }
1590
1591 pub fn handle_state_event(&mut self, event: &AnySyncStateEvent) -> bool {
1595 let ret = self.base_info.handle_state_event(event);
1596
1597 if let AnySyncStateEvent::RoomEncryption(_) = event {
1601 if self.is_encrypted() {
1602 self.mark_encryption_state_synced();
1603 }
1604 }
1605
1606 ret
1607 }
1608
1609 pub fn handle_stripped_state_event(&mut self, event: &AnyStrippedStateEvent) -> bool {
1613 self.base_info.handle_stripped_state_event(event)
1614 }
1615
1616 #[instrument(skip_all, fields(redacts))]
1618 pub fn handle_redaction(
1619 &mut self,
1620 event: &SyncRoomRedactionEvent,
1621 _raw: &Raw<SyncRoomRedactionEvent>,
1622 ) {
1623 let room_version = self.base_info.room_version().unwrap_or(&RoomVersionId::V1);
1624
1625 let Some(redacts) = event.redacts(room_version) else {
1626 info!("Can't apply redaction, redacts field is missing");
1627 return;
1628 };
1629 tracing::Span::current().record("redacts", debug(redacts));
1630
1631 if let Some(latest_event) = &mut self.latest_event {
1632 tracing::trace!("Checking if redaction applies to latest event");
1633 if latest_event.event_id().as_deref() == Some(redacts) {
1634 match apply_redaction(latest_event.event().raw(), _raw, room_version) {
1635 Some(redacted) => {
1636 latest_event.event_mut().kind =
1639 TimelineEventKind::PlainText { event: redacted };
1640 debug!("Redacted latest event");
1641 }
1642 None => {
1643 self.latest_event = None;
1644 debug!("Removed latest event");
1645 }
1646 }
1647 }
1648 }
1649
1650 self.base_info.handle_redaction(redacts);
1651 }
1652
1653 pub fn avatar_url(&self) -> Option<&MxcUri> {
1655 self.base_info
1656 .avatar
1657 .as_ref()
1658 .and_then(|e| e.as_original().and_then(|e| e.content.url.as_deref()))
1659 }
1660
1661 pub fn update_avatar(&mut self, url: Option<OwnedMxcUri>) {
1663 self.base_info.avatar = url.map(|url| {
1664 let mut content = RoomAvatarEventContent::new();
1665 content.url = Some(url);
1666
1667 MinimalStateEvent::Original(OriginalMinimalStateEvent { content, event_id: None })
1668 });
1669 }
1670
1671 pub fn avatar_info(&self) -> Option<&avatar::ImageInfo> {
1673 self.base_info
1674 .avatar
1675 .as_ref()
1676 .and_then(|e| e.as_original().and_then(|e| e.content.info.as_deref()))
1677 }
1678
1679 pub fn update_notification_count(&mut self, notification_counts: UnreadNotificationsCount) {
1681 self.notification_counts = notification_counts;
1682 }
1683
1684 pub fn update_from_ruma_summary(&mut self, summary: &RumaSummary) -> bool {
1688 let mut changed = false;
1689
1690 if !summary.is_empty() {
1691 if !summary.heroes.is_empty() {
1692 self.summary.room_heroes = summary
1693 .heroes
1694 .iter()
1695 .map(|hero_id| RoomHero {
1696 user_id: hero_id.to_owned(),
1697 display_name: None,
1698 avatar_url: None,
1699 })
1700 .collect();
1701
1702 changed = true;
1703 }
1704
1705 if let Some(joined) = summary.joined_member_count {
1706 self.summary.joined_member_count = joined.into();
1707 changed = true;
1708 }
1709
1710 if let Some(invited) = summary.invited_member_count {
1711 self.summary.invited_member_count = invited.into();
1712 changed = true;
1713 }
1714 }
1715
1716 changed
1717 }
1718
1719 pub(crate) fn update_joined_member_count(&mut self, count: u64) {
1721 self.summary.joined_member_count = count;
1722 }
1723
1724 pub(crate) fn update_invited_member_count(&mut self, count: u64) {
1726 self.summary.invited_member_count = count;
1727 }
1728
1729 pub(crate) fn update_heroes(&mut self, heroes: Vec<RoomHero>) {
1731 self.summary.room_heroes = heroes;
1732 }
1733
1734 pub fn heroes(&self) -> &[RoomHero] {
1736 &self.summary.room_heroes
1737 }
1738
1739 pub fn active_members_count(&self) -> u64 {
1743 self.summary.joined_member_count.saturating_add(self.summary.invited_member_count)
1744 }
1745
1746 pub fn invited_members_count(&self) -> u64 {
1748 self.summary.invited_member_count
1749 }
1750
1751 pub fn joined_members_count(&self) -> u64 {
1753 self.summary.joined_member_count
1754 }
1755
1756 pub fn canonical_alias(&self) -> Option<&RoomAliasId> {
1758 self.base_info.canonical_alias.as_ref()?.as_original()?.content.alias.as_deref()
1759 }
1760
1761 pub fn alt_aliases(&self) -> &[OwnedRoomAliasId] {
1763 self.base_info
1764 .canonical_alias
1765 .as_ref()
1766 .and_then(|ev| ev.as_original())
1767 .map(|ev| ev.content.alt_aliases.as_ref())
1768 .unwrap_or_default()
1769 }
1770
1771 pub fn room_id(&self) -> &RoomId {
1773 &self.room_id
1774 }
1775
1776 pub fn room_version(&self) -> Option<&RoomVersionId> {
1778 self.base_info.room_version()
1779 }
1780
1781 pub fn room_version_or_default(&self) -> RoomVersionId {
1786 use std::sync::atomic::Ordering;
1787
1788 self.base_info.room_version().cloned().unwrap_or_else(|| {
1789 if self
1790 .warned_about_unknown_room_version
1791 .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
1792 .is_ok()
1793 {
1794 warn!("Unknown room version, falling back to v10");
1795 }
1796
1797 RoomVersionId::V10
1798 })
1799 }
1800
1801 pub fn room_type(&self) -> Option<&RoomType> {
1803 match self.base_info.create.as_ref()? {
1804 MinimalStateEvent::Original(ev) => ev.content.room_type.as_ref(),
1805 MinimalStateEvent::Redacted(ev) => ev.content.room_type.as_ref(),
1806 }
1807 }
1808
1809 pub fn creator(&self) -> Option<&UserId> {
1811 match self.base_info.create.as_ref()? {
1812 MinimalStateEvent::Original(ev) => Some(&ev.content.creator),
1813 MinimalStateEvent::Redacted(ev) => Some(&ev.content.creator),
1814 }
1815 }
1816
1817 fn guest_access(&self) -> &GuestAccess {
1818 match &self.base_info.guest_access {
1819 Some(MinimalStateEvent::Original(ev)) => &ev.content.guest_access,
1820 _ => &GuestAccess::Forbidden,
1821 }
1822 }
1823
1824 pub fn history_visibility(&self) -> Option<&HistoryVisibility> {
1828 match &self.base_info.history_visibility {
1829 Some(MinimalStateEvent::Original(ev)) => Some(&ev.content.history_visibility),
1830 _ => None,
1831 }
1832 }
1833
1834 pub fn history_visibility_or_default(&self) -> &HistoryVisibility {
1841 match &self.base_info.history_visibility {
1842 Some(MinimalStateEvent::Original(ev)) => &ev.content.history_visibility,
1843 _ => &HistoryVisibility::Shared,
1844 }
1845 }
1846
1847 pub fn join_rule(&self) -> &JoinRule {
1851 match &self.base_info.join_rules {
1852 Some(MinimalStateEvent::Original(ev)) => &ev.content.join_rule,
1853 _ => &JoinRule::Public,
1854 }
1855 }
1856
1857 pub fn name(&self) -> Option<&str> {
1859 let name = &self.base_info.name.as_ref()?.as_original()?.content.name;
1860 (!name.is_empty()).then_some(name)
1861 }
1862
1863 fn tombstone(&self) -> Option<&RoomTombstoneEventContent> {
1864 Some(&self.base_info.tombstone.as_ref()?.as_original()?.content)
1865 }
1866
1867 pub fn topic(&self) -> Option<&str> {
1869 Some(&self.base_info.topic.as_ref()?.as_original()?.content.topic)
1870 }
1871
1872 fn active_matrix_rtc_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
1877 let mut v = self
1878 .base_info
1879 .rtc_member_events
1880 .iter()
1881 .filter_map(|(user_id, ev)| {
1882 ev.as_original().map(|ev| {
1883 ev.content
1884 .active_memberships(None)
1885 .into_iter()
1886 .map(move |m| (user_id.clone(), m))
1887 })
1888 })
1889 .flatten()
1890 .collect::<Vec<_>>();
1891 v.sort_by_key(|(_, m)| m.created_ts());
1892 v
1893 }
1894
1895 fn active_room_call_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
1901 self.active_matrix_rtc_memberships()
1902 .into_iter()
1903 .filter(|(_user_id, m)| m.is_room_call())
1904 .collect()
1905 }
1906
1907 pub fn has_active_room_call(&self) -> bool {
1910 !self.active_room_call_memberships().is_empty()
1911 }
1912
1913 pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
1922 self.active_room_call_memberships()
1923 .iter()
1924 .map(|(call_member_state_key, _)| call_member_state_key.user_id().to_owned())
1925 .collect()
1926 }
1927
1928 pub fn latest_event(&self) -> Option<&LatestEvent> {
1930 self.latest_event.as_deref()
1931 }
1932
1933 pub(crate) fn update_recency_stamp(&mut self, stamp: u64) {
1937 self.recency_stamp = Some(stamp);
1938 }
1939
1940 pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
1942 self.base_info.pinned_events.clone().map(|c| c.pinned)
1943 }
1944
1945 pub fn is_pinned_event(&self, event_id: &EventId) -> bool {
1951 self.base_info
1952 .pinned_events
1953 .as_ref()
1954 .map(|p| p.pinned.contains(&event_id.to_owned()))
1955 .unwrap_or_default()
1956 }
1957
1958 #[instrument(skip_all, fields(room_id = ?self.room_id))]
1966 pub(crate) async fn apply_migrations(&mut self, store: Arc<DynStateStore>) -> bool {
1967 let mut migrated = false;
1968
1969 if self.version < 1 {
1970 info!("Migrating room info to version 1");
1971
1972 match store.get_room_account_data_event_static::<TagEventContent>(&self.room_id).await {
1974 Ok(Some(raw_event)) => match raw_event.deserialize() {
1976 Ok(event) => {
1977 self.base_info.handle_notable_tags(&event.content.tags);
1978 }
1979 Err(error) => {
1980 warn!("Failed to deserialize room tags: {error}");
1981 }
1982 },
1983 Ok(_) => {
1984 }
1986 Err(error) => {
1987 warn!("Failed to load room tags: {error}");
1988 }
1989 }
1990
1991 match store.get_state_event_static::<RoomPinnedEventsEventContent>(&self.room_id).await
1993 {
1994 Ok(Some(RawSyncOrStrippedState::Sync(raw_event))) => {
1996 match raw_event.deserialize() {
1997 Ok(event) => {
1998 self.handle_state_event(&event.into());
1999 }
2000 Err(error) => {
2001 warn!("Failed to deserialize room pinned events: {error}");
2002 }
2003 }
2004 }
2005 Ok(_) => {
2006 }
2008 Err(error) => {
2009 warn!("Failed to load room pinned events: {error}");
2010 }
2011 }
2012
2013 self.version = 1;
2014 migrated = true;
2015 }
2016
2017 migrated
2018 }
2019}
2020
2021pub fn apply_redaction(
2024 event: &Raw<AnySyncTimelineEvent>,
2025 raw_redaction: &Raw<SyncRoomRedactionEvent>,
2026 room_version: &RoomVersionId,
2027) -> Option<Raw<AnySyncTimelineEvent>> {
2028 use ruma::canonical_json::{redact_in_place, RedactedBecause};
2029
2030 let mut event_json = match event.deserialize_as() {
2031 Ok(json) => json,
2032 Err(e) => {
2033 warn!("Failed to deserialize latest event: {e}");
2034 return None;
2035 }
2036 };
2037
2038 let redacted_because = match RedactedBecause::from_raw_event(raw_redaction) {
2039 Ok(rb) => rb,
2040 Err(e) => {
2041 warn!("Redaction event is not valid canonical JSON: {e}");
2042 return None;
2043 }
2044 };
2045
2046 let redact_result = redact_in_place(&mut event_json, room_version, Some(redacted_because));
2047
2048 if let Err(e) = redact_result {
2049 warn!("Failed to redact event: {e}");
2050 return None;
2051 }
2052
2053 let raw = Raw::new(&event_json).expect("CanonicalJsonObject must be serializable");
2054 Some(raw.cast())
2055}
2056
2057bitflags! {
2058 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
2063 pub struct RoomStateFilter: u16 {
2064 const JOINED = 0b00000001;
2066 const INVITED = 0b00000010;
2068 const LEFT = 0b00000100;
2070 const KNOCKED = 0b00001000;
2072 const BANNED = 0b00010000;
2074 }
2075}
2076
2077impl RoomStateFilter {
2078 pub fn matches(&self, state: RoomState) -> bool {
2080 if self.is_empty() {
2081 return true;
2082 }
2083
2084 let bit_state = match state {
2085 RoomState::Joined => Self::JOINED,
2086 RoomState::Left => Self::LEFT,
2087 RoomState::Invited => Self::INVITED,
2088 RoomState::Knocked => Self::KNOCKED,
2089 RoomState::Banned => Self::BANNED,
2090 };
2091
2092 self.contains(bit_state)
2093 }
2094
2095 pub fn as_vec(&self) -> Vec<RoomState> {
2097 let mut states = Vec::new();
2098
2099 if self.contains(Self::JOINED) {
2100 states.push(RoomState::Joined);
2101 }
2102 if self.contains(Self::LEFT) {
2103 states.push(RoomState::Left);
2104 }
2105 if self.contains(Self::INVITED) {
2106 states.push(RoomState::Invited);
2107 }
2108 if self.contains(Self::KNOCKED) {
2109 states.push(RoomState::Knocked);
2110 }
2111 if self.contains(Self::BANNED) {
2112 states.push(RoomState::Banned);
2113 }
2114
2115 states
2116 }
2117}
2118
2119fn compute_display_name_from_heroes(
2123 num_joined_invited: u64,
2124 mut heroes: Vec<&str>,
2125) -> RoomDisplayName {
2126 let num_heroes = heroes.len() as u64;
2127 let num_joined_invited_except_self = num_joined_invited.saturating_sub(1);
2128
2129 heroes.sort_unstable();
2131
2132 let names = if num_heroes == 0 && num_joined_invited > 1 {
2133 format!("{} people", num_joined_invited)
2134 } else if num_heroes >= num_joined_invited_except_self {
2135 heroes.join(", ")
2136 } else if num_heroes < num_joined_invited_except_self && num_joined_invited > 1 {
2137 format!("{}, and {} others", heroes.join(", "), (num_joined_invited - num_heroes))
2140 } else {
2141 "".to_owned()
2142 };
2143
2144 if num_joined_invited <= 1 {
2146 if names.is_empty() {
2147 RoomDisplayName::Empty
2148 } else {
2149 RoomDisplayName::EmptyWas(names)
2150 }
2151 } else {
2152 RoomDisplayName::Calculated(names)
2153 }
2154}
2155
2156#[cfg(test)]
2157mod tests {
2158 use std::{
2159 collections::BTreeSet,
2160 ops::{Not, Sub},
2161 str::FromStr,
2162 sync::Arc,
2163 time::Duration,
2164 };
2165
2166 use assign::assign;
2167 use matrix_sdk_common::deserialized_responses::TimelineEvent;
2168 use matrix_sdk_test::{
2169 async_test,
2170 event_factory::EventFactory,
2171 test_json::{sync_events::PINNED_EVENTS, TAG},
2172 ALICE, BOB, CAROL,
2173 };
2174 use ruma::{
2175 api::client::sync::sync_events::v3::RoomSummary as RumaSummary,
2176 device_id, event_id,
2177 events::{
2178 call::member::{
2179 ActiveFocus, ActiveLivekitFocus, Application, CallApplicationContent,
2180 CallMemberEventContent, CallMemberStateKey, Focus, LegacyMembershipData,
2181 LegacyMembershipDataInit, LivekitFocus, OriginalSyncCallMemberEvent,
2182 },
2183 room::{
2184 canonical_alias::RoomCanonicalAliasEventContent,
2185 encryption::{OriginalSyncRoomEncryptionEvent, RoomEncryptionEventContent},
2186 member::{MembershipState, RoomMemberEventContent, StrippedRoomMemberEvent},
2187 name::RoomNameEventContent,
2188 pinned_events::RoomPinnedEventsEventContent,
2189 },
2190 AnySyncStateEvent, EmptyStateKey, StateEventType, StateUnsigned, SyncStateEvent,
2191 },
2192 owned_event_id, owned_room_id, owned_user_id, room_alias_id, room_id,
2193 serde::Raw,
2194 time::SystemTime,
2195 user_id, DeviceId, EventEncryptionAlgorithm, EventId, MilliSecondsSinceUnixEpoch,
2196 OwnedEventId, OwnedUserId, UserId,
2197 };
2198 use serde_json::json;
2199 use similar_asserts::assert_eq;
2200 use stream_assert::{assert_pending, assert_ready};
2201
2202 use super::{compute_display_name_from_heroes, Room, RoomHero, RoomInfo, RoomState, SyncInfo};
2203 use crate::{
2204 latest_event::LatestEvent,
2205 rooms::RoomNotableTags,
2206 store::{IntoStateStore, MemoryStore, StateChanges, StateStore, StoreConfig},
2207 test_utils::logged_in_base_client,
2208 BaseClient, MinimalStateEvent, OriginalMinimalStateEvent, RoomDisplayName,
2209 RoomInfoNotableUpdateReasons, RoomStateFilter, SessionMeta,
2210 };
2211
2212 #[test]
2213 fn test_room_info_serialization() {
2214 use ruma::owned_user_id;
2218
2219 use super::RoomSummary;
2220 use crate::{rooms::BaseRoomInfo, sync::UnreadNotificationsCount};
2221
2222 let info = RoomInfo {
2223 version: 1,
2224 room_id: room_id!("!gda78o:server.tld").into(),
2225 room_state: RoomState::Invited,
2226 prev_room_state: None,
2227 notification_counts: UnreadNotificationsCount {
2228 highlight_count: 1,
2229 notification_count: 2,
2230 },
2231 summary: RoomSummary {
2232 room_heroes: vec![RoomHero {
2233 user_id: owned_user_id!("@somebody:example.org"),
2234 display_name: None,
2235 avatar_url: None,
2236 }],
2237 joined_member_count: 5,
2238 invited_member_count: 0,
2239 },
2240 members_synced: true,
2241 last_prev_batch: Some("pb".to_owned()),
2242 sync_info: SyncInfo::FullySynced,
2243 encryption_state_synced: true,
2244 latest_event: Some(Box::new(LatestEvent::new(TimelineEvent::new(
2245 Raw::from_json_string(json!({"sender": "@u:i.uk"}).to_string()).unwrap(),
2246 )))),
2247 base_info: Box::new(
2248 assign!(BaseRoomInfo::new(), { pinned_events: Some(RoomPinnedEventsEventContent::new(vec![owned_event_id!("$a")])) }),
2249 ),
2250 read_receipts: Default::default(),
2251 warned_about_unknown_room_version: Arc::new(false.into()),
2252 cached_display_name: None,
2253 cached_user_defined_notification_mode: None,
2254 recency_stamp: Some(42),
2255 };
2256
2257 let info_json = json!({
2258 "version": 1,
2259 "room_id": "!gda78o:server.tld",
2260 "room_state": "Invited",
2261 "prev_room_state": null,
2262 "notification_counts": {
2263 "highlight_count": 1,
2264 "notification_count": 2,
2265 },
2266 "summary": {
2267 "room_heroes": [{
2268 "user_id": "@somebody:example.org",
2269 "display_name": null,
2270 "avatar_url": null
2271 }],
2272 "joined_member_count": 5,
2273 "invited_member_count": 0,
2274 },
2275 "members_synced": true,
2276 "last_prev_batch": "pb",
2277 "sync_info": "FullySynced",
2278 "encryption_state_synced": true,
2279 "latest_event": {
2280 "event": {
2281 "kind": {"PlainText": {"event": {"sender": "@u:i.uk"}}},
2282 },
2283 },
2284 "base_info": {
2285 "avatar": null,
2286 "canonical_alias": null,
2287 "create": null,
2288 "dm_targets": [],
2289 "encryption": null,
2290 "guest_access": null,
2291 "history_visibility": null,
2292 "is_marked_unread": false,
2293 "join_rules": null,
2294 "max_power_level": 100,
2295 "name": null,
2296 "tombstone": null,
2297 "topic": null,
2298 "pinned_events": {
2299 "pinned": ["$a"]
2300 },
2301 },
2302 "read_receipts": {
2303 "num_unread": 0,
2304 "num_mentions": 0,
2305 "num_notifications": 0,
2306 "latest_active": null,
2307 "pending": []
2308 },
2309 "recency_stamp": 42,
2310 });
2311
2312 assert_eq!(serde_json::to_value(info).unwrap(), info_json);
2313 }
2314
2315 #[test]
2322 fn test_room_info_deserialization_without_optional_items() {
2323 use ruma::{owned_mxc_uri, owned_user_id};
2324
2325 let info_json = json!({
2328 "room_id": "!gda78o:server.tld",
2329 "room_state": "Invited",
2330 "prev_room_state": null,
2331 "notification_counts": {
2332 "highlight_count": 1,
2333 "notification_count": 2,
2334 },
2335 "summary": {
2336 "room_heroes": [{
2337 "user_id": "@somebody:example.org",
2338 "display_name": "Somebody",
2339 "avatar_url": "mxc://example.org/abc"
2340 }],
2341 "joined_member_count": 5,
2342 "invited_member_count": 0,
2343 },
2344 "members_synced": true,
2345 "last_prev_batch": "pb",
2346 "sync_info": "FullySynced",
2347 "encryption_state_synced": true,
2348 "base_info": {
2349 "avatar": null,
2350 "canonical_alias": null,
2351 "create": null,
2352 "dm_targets": [],
2353 "encryption": null,
2354 "guest_access": null,
2355 "history_visibility": null,
2356 "join_rules": null,
2357 "max_power_level": 100,
2358 "name": null,
2359 "tombstone": null,
2360 "topic": null,
2361 },
2362 });
2363
2364 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
2365
2366 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
2367 assert_eq!(info.room_state, RoomState::Invited);
2368 assert_eq!(info.notification_counts.highlight_count, 1);
2369 assert_eq!(info.notification_counts.notification_count, 2);
2370 assert_eq!(
2371 info.summary.room_heroes,
2372 vec![RoomHero {
2373 user_id: owned_user_id!("@somebody:example.org"),
2374 display_name: Some("Somebody".to_owned()),
2375 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
2376 }]
2377 );
2378 assert_eq!(info.summary.joined_member_count, 5);
2379 assert_eq!(info.summary.invited_member_count, 0);
2380 assert!(info.members_synced);
2381 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
2382 assert_eq!(info.sync_info, SyncInfo::FullySynced);
2383 assert!(info.encryption_state_synced);
2384 assert!(info.base_info.avatar.is_none());
2385 assert!(info.base_info.canonical_alias.is_none());
2386 assert!(info.base_info.create.is_none());
2387 assert_eq!(info.base_info.dm_targets.len(), 0);
2388 assert!(info.base_info.encryption.is_none());
2389 assert!(info.base_info.guest_access.is_none());
2390 assert!(info.base_info.history_visibility.is_none());
2391 assert!(info.base_info.join_rules.is_none());
2392 assert_eq!(info.base_info.max_power_level, 100);
2393 assert!(info.base_info.name.is_none());
2394 assert!(info.base_info.tombstone.is_none());
2395 assert!(info.base_info.topic.is_none());
2396 }
2397
2398 #[test]
2399 fn test_room_info_deserialization() {
2400 use ruma::{owned_mxc_uri, owned_user_id};
2401
2402 use crate::notification_settings::RoomNotificationMode;
2403
2404 let info_json = json!({
2405 "room_id": "!gda78o:server.tld",
2406 "room_state": "Joined",
2407 "prev_room_state": "Invited",
2408 "notification_counts": {
2409 "highlight_count": 1,
2410 "notification_count": 2,
2411 },
2412 "summary": {
2413 "room_heroes": [{
2414 "user_id": "@somebody:example.org",
2415 "display_name": "Somebody",
2416 "avatar_url": "mxc://example.org/abc"
2417 }],
2418 "joined_member_count": 5,
2419 "invited_member_count": 0,
2420 },
2421 "members_synced": true,
2422 "last_prev_batch": "pb",
2423 "sync_info": "FullySynced",
2424 "encryption_state_synced": true,
2425 "base_info": {
2426 "avatar": null,
2427 "canonical_alias": null,
2428 "create": null,
2429 "dm_targets": [],
2430 "encryption": null,
2431 "guest_access": null,
2432 "history_visibility": null,
2433 "join_rules": null,
2434 "max_power_level": 100,
2435 "name": null,
2436 "tombstone": null,
2437 "topic": null,
2438 },
2439 "cached_display_name": { "Calculated": "lol" },
2440 "cached_user_defined_notification_mode": "Mute",
2441 "recency_stamp": 42,
2442 });
2443
2444 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
2445
2446 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
2447 assert_eq!(info.room_state, RoomState::Joined);
2448 assert_eq!(info.prev_room_state, Some(RoomState::Invited));
2449 assert_eq!(info.notification_counts.highlight_count, 1);
2450 assert_eq!(info.notification_counts.notification_count, 2);
2451 assert_eq!(
2452 info.summary.room_heroes,
2453 vec![RoomHero {
2454 user_id: owned_user_id!("@somebody:example.org"),
2455 display_name: Some("Somebody".to_owned()),
2456 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
2457 }]
2458 );
2459 assert_eq!(info.summary.joined_member_count, 5);
2460 assert_eq!(info.summary.invited_member_count, 0);
2461 assert!(info.members_synced);
2462 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
2463 assert_eq!(info.sync_info, SyncInfo::FullySynced);
2464 assert!(info.encryption_state_synced);
2465 assert!(info.latest_event.is_none());
2466 assert!(info.base_info.avatar.is_none());
2467 assert!(info.base_info.canonical_alias.is_none());
2468 assert!(info.base_info.create.is_none());
2469 assert_eq!(info.base_info.dm_targets.len(), 0);
2470 assert!(info.base_info.encryption.is_none());
2471 assert!(info.base_info.guest_access.is_none());
2472 assert!(info.base_info.history_visibility.is_none());
2473 assert!(info.base_info.join_rules.is_none());
2474 assert_eq!(info.base_info.max_power_level, 100);
2475 assert!(info.base_info.name.is_none());
2476 assert!(info.base_info.tombstone.is_none());
2477 assert!(info.base_info.topic.is_none());
2478
2479 assert_eq!(
2480 info.cached_display_name.as_ref(),
2481 Some(&RoomDisplayName::Calculated("lol".to_owned())),
2482 );
2483 assert_eq!(
2484 info.cached_user_defined_notification_mode.as_ref(),
2485 Some(&RoomNotificationMode::Mute)
2486 );
2487 assert_eq!(info.recency_stamp.as_ref(), Some(&42));
2488 }
2489
2490 #[async_test]
2491 async fn test_is_favourite() {
2492 let client = BaseClient::with_store_config(StoreConfig::new(
2494 "cross-process-store-locks-holder-name".to_owned(),
2495 ));
2496
2497 client
2498 .set_session_meta(
2499 SessionMeta {
2500 user_id: user_id!("@alice:example.org").into(),
2501 device_id: ruma::device_id!("AYEAYEAYE").into(),
2502 },
2503 #[cfg(feature = "e2e-encryption")]
2504 None,
2505 )
2506 .await
2507 .unwrap();
2508
2509 let room_id = room_id!("!test:localhost");
2510 let room = client.get_or_create_room(room_id, RoomState::Joined);
2511
2512 assert!(room.is_favourite().not());
2514
2515 let mut room_info_subscriber = room.subscribe_info();
2517
2518 assert_pending!(room_info_subscriber);
2519
2520 let tag_raw = Raw::new(&json!({
2522 "content": {
2523 "tags": {
2524 "m.favourite": {
2525 "order": 0.0
2526 },
2527 },
2528 },
2529 "type": "m.tag",
2530 }))
2531 .unwrap()
2532 .cast();
2533
2534 let mut changes = StateChanges::default();
2536 client
2537 .handle_room_account_data(room_id, &[tag_raw], &mut changes, &mut Default::default())
2538 .await;
2539 client.apply_changes(&changes, Default::default());
2540
2541 assert_ready!(room_info_subscriber);
2543 assert_pending!(room_info_subscriber);
2544
2545 assert!(room.is_favourite());
2547
2548 let tag_raw = Raw::new(&json!({
2550 "content": {
2551 "tags": {},
2552 },
2553 "type": "m.tag"
2554 }))
2555 .unwrap()
2556 .cast();
2557 client
2558 .handle_room_account_data(room_id, &[tag_raw], &mut changes, &mut Default::default())
2559 .await;
2560 client.apply_changes(&changes, Default::default());
2561
2562 assert_ready!(room_info_subscriber);
2564 assert_pending!(room_info_subscriber);
2565
2566 assert!(room.is_favourite().not());
2568 }
2569
2570 #[async_test]
2571 async fn test_is_low_priority() {
2572 let client = BaseClient::with_store_config(StoreConfig::new(
2574 "cross-process-store-locks-holder-name".to_owned(),
2575 ));
2576
2577 client
2578 .set_session_meta(
2579 SessionMeta {
2580 user_id: user_id!("@alice:example.org").into(),
2581 device_id: ruma::device_id!("AYEAYEAYE").into(),
2582 },
2583 #[cfg(feature = "e2e-encryption")]
2584 None,
2585 )
2586 .await
2587 .unwrap();
2588
2589 let room_id = room_id!("!test:localhost");
2590 let room = client.get_or_create_room(room_id, RoomState::Joined);
2591
2592 assert!(!room.is_low_priority());
2594
2595 let mut room_info_subscriber = room.subscribe_info();
2597
2598 assert_pending!(room_info_subscriber);
2599
2600 let tag_raw = Raw::new(&json!({
2602 "content": {
2603 "tags": {
2604 "m.lowpriority": {
2605 "order": 0.0
2606 },
2607 }
2608 },
2609 "type": "m.tag"
2610 }))
2611 .unwrap()
2612 .cast();
2613
2614 let mut changes = StateChanges::default();
2616 client
2617 .handle_room_account_data(room_id, &[tag_raw], &mut changes, &mut Default::default())
2618 .await;
2619 client.apply_changes(&changes, Default::default());
2620
2621 assert_ready!(room_info_subscriber);
2623 assert_pending!(room_info_subscriber);
2624
2625 assert!(room.is_low_priority());
2627
2628 let tag_raw = Raw::new(&json!({
2630 "content": {
2631 "tags": {},
2632 },
2633 "type": "m.tag"
2634 }))
2635 .unwrap()
2636 .cast();
2637 client
2638 .handle_room_account_data(room_id, &[tag_raw], &mut changes, &mut Default::default())
2639 .await;
2640 client.apply_changes(&changes, Default::default());
2641
2642 assert_ready!(room_info_subscriber);
2644 assert_pending!(room_info_subscriber);
2645
2646 assert!(room.is_low_priority().not());
2648 }
2649
2650 fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
2651 let store = Arc::new(MemoryStore::new());
2652 let user_id = user_id!("@me:example.org");
2653 let room_id = room_id!("!test:localhost");
2654 let (sender, _receiver) = tokio::sync::broadcast::channel(1);
2655
2656 (store.clone(), Room::new(user_id, store, room_id, room_type, sender))
2657 }
2658
2659 fn make_stripped_member_event(user_id: &UserId, name: &str) -> Raw<StrippedRoomMemberEvent> {
2660 let ev_json = json!({
2661 "type": "m.room.member",
2662 "content": assign!(RoomMemberEventContent::new(MembershipState::Join), {
2663 displayname: Some(name.to_owned())
2664 }),
2665 "sender": user_id,
2666 "state_key": user_id,
2667 });
2668
2669 Raw::new(&ev_json).unwrap().cast()
2670 }
2671
2672 #[async_test]
2673 async fn test_display_name_for_joined_room_is_empty_if_no_info() {
2674 let (_, room) = make_room_test_helper(RoomState::Joined);
2675 assert_eq!(room.compute_display_name().await.unwrap(), RoomDisplayName::Empty);
2676 }
2677
2678 #[async_test]
2679 async fn test_display_name_for_joined_room_uses_canonical_alias_if_available() {
2680 let (_, room) = make_room_test_helper(RoomState::Joined);
2681 room.inner
2682 .update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
2683 assert_eq!(
2684 room.compute_display_name().await.unwrap(),
2685 RoomDisplayName::Aliased("test".to_owned())
2686 );
2687 }
2688
2689 #[async_test]
2690 async fn test_display_name_for_joined_room_prefers_name_over_alias() {
2691 let (_, room) = make_room_test_helper(RoomState::Joined);
2692 room.inner
2693 .update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
2694 assert_eq!(
2695 room.compute_display_name().await.unwrap(),
2696 RoomDisplayName::Aliased("test".to_owned())
2697 );
2698 room.inner.update(|info| info.base_info.name = Some(make_name_event()));
2699 assert_eq!(
2701 room.compute_display_name().await.unwrap(),
2702 RoomDisplayName::Named("Test Room".to_owned())
2703 );
2704 }
2705
2706 #[async_test]
2707 async fn test_display_name_for_invited_room_is_empty_if_no_info() {
2708 let (_, room) = make_room_test_helper(RoomState::Invited);
2709 assert_eq!(room.compute_display_name().await.unwrap(), RoomDisplayName::Empty);
2710 }
2711
2712 #[async_test]
2713 async fn test_display_name_for_invited_room_is_empty_if_room_name_empty() {
2714 let (_, room) = make_room_test_helper(RoomState::Invited);
2715
2716 let room_name = MinimalStateEvent::Original(OriginalMinimalStateEvent {
2717 content: RoomNameEventContent::new(String::new()),
2718 event_id: None,
2719 });
2720 room.inner.update(|info| info.base_info.name = Some(room_name));
2721
2722 assert_eq!(room.compute_display_name().await.unwrap(), RoomDisplayName::Empty);
2723 }
2724
2725 #[async_test]
2726 async fn test_display_name_for_invited_room_uses_canonical_alias_if_available() {
2727 let (_, room) = make_room_test_helper(RoomState::Invited);
2728 room.inner
2729 .update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
2730 assert_eq!(
2731 room.compute_display_name().await.unwrap(),
2732 RoomDisplayName::Aliased("test".to_owned())
2733 );
2734 }
2735
2736 #[async_test]
2737 async fn test_display_name_for_invited_room_prefers_name_over_alias() {
2738 let (_, room) = make_room_test_helper(RoomState::Invited);
2739 room.inner
2740 .update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
2741 assert_eq!(
2742 room.compute_display_name().await.unwrap(),
2743 RoomDisplayName::Aliased("test".to_owned())
2744 );
2745 room.inner.update(|info| info.base_info.name = Some(make_name_event()));
2746 assert_eq!(
2748 room.compute_display_name().await.unwrap(),
2749 RoomDisplayName::Named("Test Room".to_owned())
2750 );
2751 }
2752
2753 fn make_canonical_alias_event() -> MinimalStateEvent<RoomCanonicalAliasEventContent> {
2754 MinimalStateEvent::Original(OriginalMinimalStateEvent {
2755 content: assign!(RoomCanonicalAliasEventContent::new(), {
2756 alias: Some(room_alias_id!("#test:example.com").to_owned()),
2757 }),
2758 event_id: None,
2759 })
2760 }
2761
2762 fn make_name_event() -> MinimalStateEvent<RoomNameEventContent> {
2763 MinimalStateEvent::Original(OriginalMinimalStateEvent {
2764 content: RoomNameEventContent::new("Test Room".to_owned()),
2765 event_id: None,
2766 })
2767 }
2768
2769 #[async_test]
2770 async fn test_display_name_dm_invited() {
2771 let (store, room) = make_room_test_helper(RoomState::Invited);
2772 let room_id = room_id!("!test:localhost");
2773 let matthew = user_id!("@matthew:example.org");
2774 let me = user_id!("@me:example.org");
2775 let mut changes = StateChanges::new("".to_owned());
2776 let summary = assign!(RumaSummary::new(), {
2777 heroes: vec![me.to_owned(), matthew.to_owned()],
2778 });
2779
2780 changes.add_stripped_member(
2781 room_id,
2782 matthew,
2783 make_stripped_member_event(matthew, "Matthew"),
2784 );
2785 changes.add_stripped_member(room_id, me, make_stripped_member_event(me, "Me"));
2786 store.save_changes(&changes).await.unwrap();
2787
2788 room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
2789 assert_eq!(
2790 room.compute_display_name().await.unwrap(),
2791 RoomDisplayName::Calculated("Matthew".to_owned())
2792 );
2793 }
2794
2795 #[async_test]
2796 async fn test_display_name_dm_invited_no_heroes() {
2797 let (store, room) = make_room_test_helper(RoomState::Invited);
2798 let room_id = room_id!("!test:localhost");
2799 let matthew = user_id!("@matthew:example.org");
2800 let me = user_id!("@me:example.org");
2801 let mut changes = StateChanges::new("".to_owned());
2802
2803 changes.add_stripped_member(
2804 room_id,
2805 matthew,
2806 make_stripped_member_event(matthew, "Matthew"),
2807 );
2808 changes.add_stripped_member(room_id, me, make_stripped_member_event(me, "Me"));
2809 store.save_changes(&changes).await.unwrap();
2810
2811 assert_eq!(
2812 room.compute_display_name().await.unwrap(),
2813 RoomDisplayName::Calculated("Matthew".to_owned())
2814 );
2815 }
2816
2817 #[async_test]
2818 async fn test_display_name_dm_joined() {
2819 let (store, room) = make_room_test_helper(RoomState::Joined);
2820 let room_id = room_id!("!test:localhost");
2821 let matthew = user_id!("@matthew:example.org");
2822 let me = user_id!("@me:example.org");
2823
2824 let mut changes = StateChanges::new("".to_owned());
2825 let summary = assign!(RumaSummary::new(), {
2826 joined_member_count: Some(2u32.into()),
2827 heroes: vec![me.to_owned(), matthew.to_owned()],
2828 });
2829
2830 let f = EventFactory::new().room(room_id!("!test:localhost"));
2831
2832 let members = changes
2833 .state
2834 .entry(room_id.to_owned())
2835 .or_default()
2836 .entry(StateEventType::RoomMember)
2837 .or_default();
2838 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
2839 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
2840
2841 store.save_changes(&changes).await.unwrap();
2842
2843 room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
2844 assert_eq!(
2845 room.compute_display_name().await.unwrap(),
2846 RoomDisplayName::Calculated("Matthew".to_owned())
2847 );
2848 }
2849
2850 #[async_test]
2851 async fn test_display_name_dm_joined_service_members() {
2852 let (store, room) = make_room_test_helper(RoomState::Joined);
2853 let room_id = room_id!("!test:localhost");
2854
2855 let matthew = user_id!("@sahasrhala:example.org");
2856 let me = user_id!("@me:example.org");
2857 let bot = user_id!("@bot:example.org");
2858
2859 let mut changes = StateChanges::new("".to_owned());
2860 let summary = assign!(RumaSummary::new(), {
2861 joined_member_count: Some(3u32.into()),
2862 heroes: vec![me.to_owned(), matthew.to_owned(), bot.to_owned()],
2863 });
2864
2865 let f = EventFactory::new().room(room_id!("!test:localhost"));
2866
2867 let members = changes
2868 .state
2869 .entry(room_id.to_owned())
2870 .or_default()
2871 .entry(StateEventType::RoomMember)
2872 .or_default();
2873 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
2874 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
2875 members.insert(bot.into(), f.member(bot).display_name("Bot").into_raw());
2876
2877 let member_hints_content =
2878 f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into_raw();
2879 changes
2880 .state
2881 .entry(room_id.to_owned())
2882 .or_default()
2883 .entry(StateEventType::MemberHints)
2884 .or_default()
2885 .insert("".to_owned(), member_hints_content);
2886
2887 store.save_changes(&changes).await.unwrap();
2888
2889 room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
2890 assert_eq!(
2892 room.compute_display_name().await.unwrap(),
2893 RoomDisplayName::Calculated("Matthew".to_owned())
2894 );
2895 }
2896
2897 #[async_test]
2898 async fn test_display_name_dm_joined_alone_with_service_members() {
2899 let (store, room) = make_room_test_helper(RoomState::Joined);
2900 let room_id = room_id!("!test:localhost");
2901
2902 let me = user_id!("@me:example.org");
2903 let bot = user_id!("@bot:example.org");
2904
2905 let mut changes = StateChanges::new("".to_owned());
2906 let summary = assign!(RumaSummary::new(), {
2907 joined_member_count: Some(2u32.into()),
2908 heroes: vec![me.to_owned(), bot.to_owned()],
2909 });
2910
2911 let f = EventFactory::new().room(room_id!("!test:localhost"));
2912
2913 let members = changes
2914 .state
2915 .entry(room_id.to_owned())
2916 .or_default()
2917 .entry(StateEventType::RoomMember)
2918 .or_default();
2919 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
2920 members.insert(bot.into(), f.member(bot).display_name("Bot").into_raw());
2921
2922 let member_hints_content =
2923 f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into_raw();
2924 changes
2925 .state
2926 .entry(room_id.to_owned())
2927 .or_default()
2928 .entry(StateEventType::MemberHints)
2929 .or_default()
2930 .insert("".to_owned(), member_hints_content);
2931
2932 store.save_changes(&changes).await.unwrap();
2933
2934 room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
2935 assert_eq!(room.compute_display_name().await.unwrap(), RoomDisplayName::Empty);
2937 }
2938
2939 #[async_test]
2940 async fn test_display_name_dm_joined_no_heroes() {
2941 let (store, room) = make_room_test_helper(RoomState::Joined);
2942 let room_id = room_id!("!test:localhost");
2943 let matthew = user_id!("@matthew:example.org");
2944 let me = user_id!("@me:example.org");
2945 let mut changes = StateChanges::new("".to_owned());
2946
2947 let f = EventFactory::new().room(room_id!("!test:localhost"));
2948
2949 let members = changes
2950 .state
2951 .entry(room_id.to_owned())
2952 .or_default()
2953 .entry(StateEventType::RoomMember)
2954 .or_default();
2955 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
2956 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
2957
2958 store.save_changes(&changes).await.unwrap();
2959
2960 assert_eq!(
2961 room.compute_display_name().await.unwrap(),
2962 RoomDisplayName::Calculated("Matthew".to_owned())
2963 );
2964 }
2965
2966 #[async_test]
2967 async fn test_display_name_dm_joined_no_heroes_service_members() {
2968 let (store, room) = make_room_test_helper(RoomState::Joined);
2969 let room_id = room_id!("!test:localhost");
2970
2971 let matthew = user_id!("@matthew:example.org");
2972 let me = user_id!("@me:example.org");
2973 let bot = user_id!("@bot:example.org");
2974
2975 let mut changes = StateChanges::new("".to_owned());
2976
2977 let f = EventFactory::new().room(room_id!("!test:localhost"));
2978
2979 let members = changes
2980 .state
2981 .entry(room_id.to_owned())
2982 .or_default()
2983 .entry(StateEventType::RoomMember)
2984 .or_default();
2985 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
2986 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
2987 members.insert(bot.into(), f.member(bot).display_name("Bot").into_raw());
2988
2989 let member_hints_content =
2990 f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into_raw();
2991 changes
2992 .state
2993 .entry(room_id.to_owned())
2994 .or_default()
2995 .entry(StateEventType::MemberHints)
2996 .or_default()
2997 .insert("".to_owned(), member_hints_content);
2998
2999 store.save_changes(&changes).await.unwrap();
3000
3001 assert_eq!(
3002 room.compute_display_name().await.unwrap(),
3003 RoomDisplayName::Calculated("Matthew".to_owned())
3004 );
3005 }
3006
3007 #[async_test]
3008 async fn test_display_name_deterministic() {
3009 let (store, room) = make_room_test_helper(RoomState::Joined);
3010
3011 let alice = user_id!("@alice:example.org");
3012 let bob = user_id!("@bob:example.org");
3013 let carol = user_id!("@carol:example.org");
3014 let denis = user_id!("@denis:example.org");
3015 let erica = user_id!("@erica:example.org");
3016 let fred = user_id!("@fred:example.org");
3017 let me = user_id!("@me:example.org");
3018
3019 let mut changes = StateChanges::new("".to_owned());
3020
3021 let f = EventFactory::new().room(room_id!("!test:localhost"));
3022
3023 {
3026 let members = changes
3027 .state
3028 .entry(room.room_id().to_owned())
3029 .or_default()
3030 .entry(StateEventType::RoomMember)
3031 .or_default();
3032 members.insert(carol.into(), f.member(carol).display_name("Carol").into_raw());
3033 members.insert(bob.into(), f.member(bob).display_name("Bob").into_raw());
3034 members.insert(fred.into(), f.member(fred).display_name("Fred").into_raw());
3035 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
3036 store.save_changes(&changes).await.unwrap();
3037 }
3038
3039 {
3040 let members = changes
3041 .state
3042 .entry(room.room_id().to_owned())
3043 .or_default()
3044 .entry(StateEventType::RoomMember)
3045 .or_default();
3046 members.insert(alice.into(), f.member(alice).display_name("Alice").into_raw());
3047 members.insert(erica.into(), f.member(erica).display_name("Erica").into_raw());
3048 members.insert(denis.into(), f.member(denis).display_name("Denis").into_raw());
3049 store.save_changes(&changes).await.unwrap();
3050 }
3051
3052 let summary = assign!(RumaSummary::new(), {
3053 joined_member_count: Some(7u32.into()),
3054 heroes: vec![denis.to_owned(), carol.to_owned(), bob.to_owned(), erica.to_owned()],
3055 });
3056 room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
3057
3058 assert_eq!(
3059 room.compute_display_name().await.unwrap(),
3060 RoomDisplayName::Calculated("Bob, Carol, Denis, Erica, and 3 others".to_owned())
3061 );
3062 }
3063
3064 #[async_test]
3065 async fn test_display_name_deterministic_no_heroes() {
3066 let (store, room) = make_room_test_helper(RoomState::Joined);
3067
3068 let alice = user_id!("@alice:example.org");
3069 let bob = user_id!("@bob:example.org");
3070 let carol = user_id!("@carol:example.org");
3071 let denis = user_id!("@denis:example.org");
3072 let erica = user_id!("@erica:example.org");
3073 let fred = user_id!("@fred:example.org");
3074 let me = user_id!("@me:example.org");
3075
3076 let f = EventFactory::new().room(room_id!("!test:localhost"));
3077
3078 let mut changes = StateChanges::new("".to_owned());
3079
3080 {
3083 let members = changes
3084 .state
3085 .entry(room.room_id().to_owned())
3086 .or_default()
3087 .entry(StateEventType::RoomMember)
3088 .or_default();
3089 members.insert(carol.into(), f.member(carol).display_name("Carol").into_raw());
3090 members.insert(bob.into(), f.member(bob).display_name("Bob").into_raw());
3091 members.insert(fred.into(), f.member(fred).display_name("Fred").into_raw());
3092 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
3093
3094 store.save_changes(&changes).await.unwrap();
3095 }
3096
3097 {
3098 let members = changes
3099 .state
3100 .entry(room.room_id().to_owned())
3101 .or_default()
3102 .entry(StateEventType::RoomMember)
3103 .or_default();
3104 members.insert(alice.into(), f.member(alice).display_name("Alice").into_raw());
3105 members.insert(erica.into(), f.member(erica).display_name("Erica").into_raw());
3106 members.insert(denis.into(), f.member(denis).display_name("Denis").into_raw());
3107 store.save_changes(&changes).await.unwrap();
3108 }
3109
3110 assert_eq!(
3111 room.compute_display_name().await.unwrap(),
3112 RoomDisplayName::Calculated("Alice, Bob, Carol, Denis, Erica, and 2 others".to_owned())
3113 );
3114 }
3115
3116 #[async_test]
3117 async fn test_display_name_dm_alone() {
3118 let (store, room) = make_room_test_helper(RoomState::Joined);
3119 let room_id = room_id!("!test:localhost");
3120 let matthew = user_id!("@matthew:example.org");
3121 let me = user_id!("@me:example.org");
3122 let mut changes = StateChanges::new("".to_owned());
3123 let summary = assign!(RumaSummary::new(), {
3124 joined_member_count: Some(1u32.into()),
3125 heroes: vec![me.to_owned(), matthew.to_owned()],
3126 });
3127
3128 let f = EventFactory::new().room(room_id!("!test:localhost"));
3129
3130 let members = changes
3131 .state
3132 .entry(room_id.to_owned())
3133 .or_default()
3134 .entry(StateEventType::RoomMember)
3135 .or_default();
3136 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
3137 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
3138
3139 store.save_changes(&changes).await.unwrap();
3140
3141 room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
3142 assert_eq!(
3143 room.compute_display_name().await.unwrap(),
3144 RoomDisplayName::EmptyWas("Matthew".to_owned())
3145 );
3146 }
3147
3148 #[cfg(feature = "e2e-encryption")]
3149 #[async_test]
3150 async fn test_setting_the_latest_event_doesnt_cause_a_room_info_notable_update() {
3151 use std::collections::BTreeMap;
3152
3153 use assert_matches::assert_matches;
3154
3155 use crate::{RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons};
3156
3157 let client = BaseClient::with_store_config(StoreConfig::new(
3159 "cross-process-store-locks-holder-name".to_owned(),
3160 ));
3161
3162 client
3163 .set_session_meta(
3164 SessionMeta {
3165 user_id: user_id!("@alice:example.org").into(),
3166 device_id: ruma::device_id!("AYEAYEAYE").into(),
3167 },
3168 None,
3169 )
3170 .await
3171 .unwrap();
3172
3173 let room_id = room_id!("!test:localhost");
3174 let room = client.get_or_create_room(room_id, RoomState::Joined);
3175
3176 add_encrypted_event(&room, "$A");
3178 assert!(room.latest_event().is_none());
3180
3181 let mut room_info_notable_update = client.room_info_notable_update_receiver();
3183
3184 let event = make_latest_event("$A");
3186
3187 let mut changes = StateChanges::default();
3188 let mut room_info_notable_updates = BTreeMap::new();
3189 room.on_latest_event_decrypted(
3190 event.clone(),
3191 0,
3192 &mut changes,
3193 &mut room_info_notable_updates,
3194 );
3195
3196 assert!(room_info_notable_updates.contains_key(room_id));
3197
3198 assert!(room_info_notable_update.try_recv().is_err());
3200
3201 client.apply_changes(&changes, room_info_notable_updates);
3203 assert_eq!(room.latest_event().unwrap().event_id(), event.event_id());
3204
3205 assert_matches!(
3207 room_info_notable_update.recv().await,
3208 Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons }) => {
3209 assert_eq!(received_room_id, room_id);
3210 assert!(reasons.contains(RoomInfoNotableUpdateReasons::LATEST_EVENT));
3211 }
3212 );
3213 }
3214
3215 #[cfg(feature = "e2e-encryption")]
3216 #[async_test]
3217 async fn test_when_we_provide_a_newly_decrypted_event_it_replaces_latest_event() {
3218 use std::collections::BTreeMap;
3219
3220 let (_store, room) = make_room_test_helper(RoomState::Joined);
3222 add_encrypted_event(&room, "$A");
3223 assert!(room.latest_event().is_none());
3225
3226 let event = make_latest_event("$A");
3228 let mut changes = StateChanges::default();
3229 let mut room_info_notable_updates = BTreeMap::new();
3230 room.on_latest_event_decrypted(
3231 event.clone(),
3232 0,
3233 &mut changes,
3234 &mut room_info_notable_updates,
3235 );
3236 room.set_room_info(
3237 changes.room_infos.get(room.room_id()).cloned().unwrap(),
3238 room_info_notable_updates.get(room.room_id()).copied().unwrap(),
3239 );
3240
3241 assert_eq!(room.latest_event().unwrap().event_id(), event.event_id());
3243 }
3244
3245 #[cfg(feature = "e2e-encryption")]
3246 #[async_test]
3247 async fn test_when_a_newly_decrypted_event_appears_we_delete_all_older_encrypted_events() {
3248 use std::collections::BTreeMap;
3249
3250 let (_store, room) = make_room_test_helper(RoomState::Joined);
3252 room.inner.update(|info| info.latest_event = Some(make_latest_event("$A")));
3253 add_encrypted_event(&room, "$0");
3254 add_encrypted_event(&room, "$1");
3255 add_encrypted_event(&room, "$2");
3256 add_encrypted_event(&room, "$3");
3257
3258 let new_event = make_latest_event("$1");
3260 let new_event_index = 1;
3261 let mut changes = StateChanges::default();
3262 let mut room_info_notable_updates = BTreeMap::new();
3263 room.on_latest_event_decrypted(
3264 new_event.clone(),
3265 new_event_index,
3266 &mut changes,
3267 &mut room_info_notable_updates,
3268 );
3269 room.set_room_info(
3270 changes.room_infos.get(room.room_id()).cloned().unwrap(),
3271 room_info_notable_updates.get(room.room_id()).copied().unwrap(),
3272 );
3273
3274 let enc_evs = room.latest_encrypted_events();
3276 assert_eq!(enc_evs.len(), 2);
3277 assert_eq!(enc_evs[0].get_field::<&str>("event_id").unwrap().unwrap(), "$2");
3278 assert_eq!(enc_evs[1].get_field::<&str>("event_id").unwrap().unwrap(), "$3");
3279
3280 assert_eq!(room.latest_event().unwrap().event_id(), new_event.event_id());
3282 }
3283
3284 #[cfg(feature = "e2e-encryption")]
3285 #[async_test]
3286 async fn test_replacing_the_newest_event_leaves_none_left() {
3287 use std::collections::BTreeMap;
3288
3289 let (_store, room) = make_room_test_helper(RoomState::Joined);
3291 add_encrypted_event(&room, "$0");
3292 add_encrypted_event(&room, "$1");
3293 add_encrypted_event(&room, "$2");
3294 add_encrypted_event(&room, "$3");
3295
3296 let new_event = make_latest_event("$3");
3298 let new_event_index = 3;
3299 let mut changes = StateChanges::default();
3300 let mut room_info_notable_updates = BTreeMap::new();
3301 room.on_latest_event_decrypted(
3302 new_event,
3303 new_event_index,
3304 &mut changes,
3305 &mut room_info_notable_updates,
3306 );
3307 room.set_room_info(
3308 changes.room_infos.get(room.room_id()).cloned().unwrap(),
3309 room_info_notable_updates.get(room.room_id()).copied().unwrap(),
3310 );
3311
3312 let enc_evs = room.latest_encrypted_events();
3314 assert_eq!(enc_evs.len(), 0);
3315 }
3316
3317 #[cfg(feature = "e2e-encryption")]
3318 fn add_encrypted_event(room: &Room, event_id: &str) {
3319 room.latest_encrypted_events
3320 .write()
3321 .unwrap()
3322 .push(Raw::from_json_string(json!({ "event_id": event_id }).to_string()).unwrap());
3323 }
3324
3325 #[cfg(feature = "e2e-encryption")]
3326 fn make_latest_event(event_id: &str) -> Box<LatestEvent> {
3327 Box::new(LatestEvent::new(TimelineEvent::new(
3328 Raw::from_json_string(json!({ "event_id": event_id }).to_string()).unwrap(),
3329 )))
3330 }
3331
3332 fn timestamp(minutes_ago: u32) -> MilliSecondsSinceUnixEpoch {
3333 MilliSecondsSinceUnixEpoch::from_system_time(
3334 SystemTime::now().sub(Duration::from_secs((60 * minutes_ago).into())),
3335 )
3336 .expect("date out of range")
3337 }
3338
3339 fn legacy_membership_for_my_call(
3340 device_id: &DeviceId,
3341 membership_id: &str,
3342 minutes_ago: u32,
3343 ) -> LegacyMembershipData {
3344 let (application, foci) = foci_and_application();
3345 assign!(
3346 LegacyMembershipData::from(LegacyMembershipDataInit {
3347 application,
3348 device_id: device_id.to_owned(),
3349 expires: Duration::from_millis(3_600_000),
3350 foci_active: foci,
3351 membership_id: membership_id.to_owned(),
3352 }),
3353 { created_ts: Some(timestamp(minutes_ago)) }
3354 )
3355 }
3356
3357 fn legacy_member_state_event(
3358 memberships: Vec<LegacyMembershipData>,
3359 ev_id: &EventId,
3360 user_id: &UserId,
3361 ) -> AnySyncStateEvent {
3362 let content = CallMemberEventContent::new_legacy(memberships);
3363
3364 AnySyncStateEvent::CallMember(SyncStateEvent::Original(OriginalSyncCallMemberEvent {
3365 content,
3366 event_id: ev_id.to_owned(),
3367 sender: user_id.to_owned(),
3368 origin_server_ts: timestamp(0),
3371 state_key: CallMemberStateKey::new(user_id.to_owned(), None, false),
3372 unsigned: StateUnsigned::new(),
3373 }))
3374 }
3375
3376 struct InitData<'a> {
3377 device_id: &'a DeviceId,
3378 minutes_ago: u32,
3379 }
3380
3381 fn session_member_state_event(
3382 ev_id: &EventId,
3383 user_id: &UserId,
3384 init_data: Option<InitData<'_>>,
3385 ) -> AnySyncStateEvent {
3386 let application = Application::Call(CallApplicationContent::new(
3387 "my_call_id_1".to_owned(),
3388 ruma::events::call::member::CallScope::Room,
3389 ));
3390 let foci_preferred = vec![Focus::Livekit(LivekitFocus::new(
3391 "my_call_foci_alias".to_owned(),
3392 "https://lk.org".to_owned(),
3393 ))];
3394 let focus_active = ActiveFocus::Livekit(ActiveLivekitFocus::new());
3395 let (content, state_key) = match init_data {
3396 Some(InitData { device_id, minutes_ago }) => (
3397 CallMemberEventContent::new(
3398 application,
3399 device_id.to_owned(),
3400 focus_active,
3401 foci_preferred,
3402 Some(timestamp(minutes_ago)),
3403 ),
3404 CallMemberStateKey::new(user_id.to_owned(), Some(device_id.to_owned()), false),
3405 ),
3406 None => (
3407 CallMemberEventContent::new_empty(None),
3408 CallMemberStateKey::new(user_id.to_owned(), None, false),
3409 ),
3410 };
3411
3412 AnySyncStateEvent::CallMember(SyncStateEvent::Original(OriginalSyncCallMemberEvent {
3413 content,
3414 event_id: ev_id.to_owned(),
3415 sender: user_id.to_owned(),
3416 origin_server_ts: timestamp(0),
3419 state_key,
3420 unsigned: StateUnsigned::new(),
3421 }))
3422 }
3423
3424 fn foci_and_application() -> (Application, Vec<Focus>) {
3425 (
3426 Application::Call(CallApplicationContent::new(
3427 "my_call_id_1".to_owned(),
3428 ruma::events::call::member::CallScope::Room,
3429 )),
3430 vec![Focus::Livekit(LivekitFocus::new(
3431 "my_call_foci_alias".to_owned(),
3432 "https://lk.org".to_owned(),
3433 ))],
3434 )
3435 }
3436
3437 fn receive_state_events(room: &Room, events: Vec<&AnySyncStateEvent>) {
3438 room.inner.update_if(|info| {
3439 let mut res = false;
3440 for ev in events {
3441 res |= info.handle_state_event(ev);
3442 }
3443 res
3444 });
3445 }
3446
3447 fn legacy_create_call_with_member_events_for_user(a: &UserId, b: &UserId, c: &UserId) -> Room {
3451 let (_, room) = make_room_test_helper(RoomState::Joined);
3452
3453 let a_empty = legacy_member_state_event(Vec::new(), event_id!("$1234"), a);
3454
3455 let m_init_b = legacy_membership_for_my_call(device_id!("DEVICE_0"), "0", 1);
3457 let b_one = legacy_member_state_event(vec![m_init_b], event_id!("$12345"), b);
3458
3459 let m_init_c1 = legacy_membership_for_my_call(device_id!("DEVICE_0"), "0", 10);
3461 let m_init_c2 = legacy_membership_for_my_call(device_id!("DEVICE_1"), "0", 20);
3463 let c_two = legacy_member_state_event(vec![m_init_c1, m_init_c2], event_id!("$123456"), c);
3464
3465 receive_state_events(&room, vec![&c_two, &a_empty, &b_one]);
3467
3468 room
3469 }
3470
3471 fn session_create_call_with_member_events_for_user(a: &UserId, b: &UserId, c: &UserId) -> Room {
3475 let (_, room) = make_room_test_helper(RoomState::Joined);
3476
3477 let a_empty = session_member_state_event(event_id!("$1234"), a, None);
3478
3479 let b_one = session_member_state_event(
3481 event_id!("$12345"),
3482 b,
3483 Some(InitData { device_id: "DEVICE_0".into(), minutes_ago: 1 }),
3484 );
3485
3486 let m_c1 = session_member_state_event(
3487 event_id!("$123456_0"),
3488 c,
3489 Some(InitData { device_id: "DEVICE_0".into(), minutes_ago: 10 }),
3490 );
3491 let m_c2 = session_member_state_event(
3492 event_id!("$123456_1"),
3493 c,
3494 Some(InitData { device_id: "DEVICE_1".into(), minutes_ago: 20 }),
3495 );
3496 receive_state_events(&room, vec![&m_c1, &m_c2, &a_empty, &b_one]);
3498
3499 room
3500 }
3501
3502 #[test]
3503 fn test_show_correct_active_call_state() {
3504 let room_legacy = legacy_create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL);
3505
3506 assert_eq!(
3510 vec![CAROL.to_owned(), CAROL.to_owned(), BOB.to_owned()],
3511 room_legacy.active_room_call_participants()
3512 );
3513 assert!(room_legacy.has_active_room_call());
3514
3515 let room_session = session_create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL);
3516 assert_eq!(
3517 vec![CAROL.to_owned(), CAROL.to_owned(), BOB.to_owned()],
3518 room_session.active_room_call_participants()
3519 );
3520 assert!(room_session.has_active_room_call());
3521 }
3522
3523 #[test]
3524 fn test_active_call_is_false_when_everyone_left() {
3525 let room = legacy_create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL);
3526
3527 let b_empty_membership = legacy_member_state_event(Vec::new(), event_id!("$1234_1"), &BOB);
3528 let c_empty_membership =
3529 legacy_member_state_event(Vec::new(), event_id!("$12345_1"), &CAROL);
3530
3531 receive_state_events(&room, vec![&b_empty_membership, &c_empty_membership]);
3532
3533 assert_eq!(Vec::<OwnedUserId>::new(), room.active_room_call_participants());
3535 assert!(!room.has_active_room_call());
3536 }
3537
3538 #[test]
3539 fn test_calculate_room_name() {
3540 let mut actual = compute_display_name_from_heroes(2, vec!["a"]);
3541 assert_eq!(RoomDisplayName::Calculated("a".to_owned()), actual);
3542
3543 actual = compute_display_name_from_heroes(3, vec!["a", "b"]);
3544 assert_eq!(RoomDisplayName::Calculated("a, b".to_owned()), actual);
3545
3546 actual = compute_display_name_from_heroes(4, vec!["a", "b", "c"]);
3547 assert_eq!(RoomDisplayName::Calculated("a, b, c".to_owned()), actual);
3548
3549 actual = compute_display_name_from_heroes(5, vec!["a", "b", "c"]);
3550 assert_eq!(RoomDisplayName::Calculated("a, b, c, and 2 others".to_owned()), actual);
3551
3552 actual = compute_display_name_from_heroes(5, vec![]);
3553 assert_eq!(RoomDisplayName::Calculated("5 people".to_owned()), actual);
3554
3555 actual = compute_display_name_from_heroes(0, vec![]);
3556 assert_eq!(RoomDisplayName::Empty, actual);
3557
3558 actual = compute_display_name_from_heroes(1, vec![]);
3559 assert_eq!(RoomDisplayName::Empty, actual);
3560
3561 actual = compute_display_name_from_heroes(1, vec!["a"]);
3562 assert_eq!(RoomDisplayName::EmptyWas("a".to_owned()), actual);
3563
3564 actual = compute_display_name_from_heroes(1, vec!["a", "b"]);
3565 assert_eq!(RoomDisplayName::EmptyWas("a, b".to_owned()), actual);
3566
3567 actual = compute_display_name_from_heroes(1, vec!["a", "b", "c"]);
3568 assert_eq!(RoomDisplayName::EmptyWas("a, b, c".to_owned()), actual);
3569 }
3570
3571 #[test]
3572 fn test_encryption_is_set_when_encryption_event_is_received() {
3573 let (_store, room) = make_room_test_helper(RoomState::Joined);
3574
3575 assert!(room.is_encryption_state_synced().not());
3576 assert!(room.is_encrypted().not());
3577
3578 let encryption_content =
3579 RoomEncryptionEventContent::new(EventEncryptionAlgorithm::MegolmV1AesSha2);
3580 let encryption_event = AnySyncStateEvent::RoomEncryption(SyncStateEvent::Original(
3581 OriginalSyncRoomEncryptionEvent {
3582 content: encryption_content,
3583 event_id: OwnedEventId::from_str("$1234_1").unwrap(),
3584 sender: ALICE.to_owned(),
3585 origin_server_ts: timestamp(0),
3588 state_key: EmptyStateKey,
3589 unsigned: StateUnsigned::new(),
3590 },
3591 ));
3592 receive_state_events(&room, vec![&encryption_event]);
3593
3594 assert!(room.is_encryption_state_synced());
3595 assert!(room.is_encrypted());
3596 }
3597
3598 #[async_test]
3599 async fn test_room_info_migration_v1() {
3600 let store = MemoryStore::new().into_state_store();
3601
3602 let room_info_json = json!({
3603 "room_id": "!gda78o:server.tld",
3604 "room_state": "Joined",
3605 "notification_counts": {
3606 "highlight_count": 1,
3607 "notification_count": 2,
3608 },
3609 "summary": {
3610 "room_heroes": [{
3611 "user_id": "@somebody:example.org",
3612 "display_name": null,
3613 "avatar_url": null
3614 }],
3615 "joined_member_count": 5,
3616 "invited_member_count": 0,
3617 },
3618 "members_synced": true,
3619 "last_prev_batch": "pb",
3620 "sync_info": "FullySynced",
3621 "encryption_state_synced": true,
3622 "latest_event": {
3623 "event": {
3624 "encryption_info": null,
3625 "event": {
3626 "sender": "@u:i.uk",
3627 },
3628 },
3629 },
3630 "base_info": {
3631 "avatar": null,
3632 "canonical_alias": null,
3633 "create": null,
3634 "dm_targets": [],
3635 "encryption": null,
3636 "guest_access": null,
3637 "history_visibility": null,
3638 "join_rules": null,
3639 "max_power_level": 100,
3640 "name": null,
3641 "tombstone": null,
3642 "topic": null,
3643 },
3644 "read_receipts": {
3645 "num_unread": 0,
3646 "num_mentions": 0,
3647 "num_notifications": 0,
3648 "latest_active": null,
3649 "pending": []
3650 },
3651 "recency_stamp": 42,
3652 });
3653 let mut room_info: RoomInfo = serde_json::from_value(room_info_json).unwrap();
3654
3655 assert_eq!(room_info.version, 0);
3656 assert!(room_info.base_info.notable_tags.is_empty());
3657 assert!(room_info.base_info.pinned_events.is_none());
3658
3659 assert!(room_info.apply_migrations(store.clone()).await);
3661
3662 assert_eq!(room_info.version, 1);
3663 assert!(room_info.base_info.notable_tags.is_empty());
3664 assert!(room_info.base_info.pinned_events.is_none());
3665
3666 assert!(!room_info.apply_migrations(store.clone()).await);
3668
3669 assert_eq!(room_info.version, 1);
3670 assert!(room_info.base_info.notable_tags.is_empty());
3671 assert!(room_info.base_info.pinned_events.is_none());
3672
3673 let mut changes = StateChanges::default();
3675
3676 let raw_tag_event = Raw::new(&*TAG).unwrap().cast();
3677 let tag_event = raw_tag_event.deserialize().unwrap();
3678 changes.add_room_account_data(&room_info.room_id, tag_event, raw_tag_event);
3679
3680 let raw_pinned_events_event = Raw::new(&*PINNED_EVENTS).unwrap().cast();
3681 let pinned_events_event = raw_pinned_events_event.deserialize().unwrap();
3682 changes.add_state_event(&room_info.room_id, pinned_events_event, raw_pinned_events_event);
3683
3684 store.save_changes(&changes).await.unwrap();
3685
3686 room_info.version = 0;
3688 assert!(room_info.apply_migrations(store.clone()).await);
3689
3690 assert_eq!(room_info.version, 1);
3691 assert!(room_info.base_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
3692 assert!(room_info.base_info.pinned_events.is_some());
3693
3694 let new_room_info = RoomInfo::new(room_id!("!new_room:localhost"), RoomState::Joined);
3696 assert_eq!(new_room_info.version, 1);
3697 }
3698
3699 #[async_test]
3700 async fn test_prev_room_state_is_updated() {
3701 let (_store, room) = make_room_test_helper(RoomState::Invited);
3702 assert_eq!(room.prev_state(), None);
3703 assert_eq!(room.state(), RoomState::Invited);
3704
3705 let mut room_info = room.clone_info();
3707 room_info.mark_as_joined();
3708 room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP);
3709 assert_eq!(room.prev_state(), Some(RoomState::Invited));
3710 assert_eq!(room.state(), RoomState::Joined);
3711
3712 let mut room_info = room.clone_info();
3714 room_info.mark_as_joined();
3715 room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP);
3716 assert_eq!(room.prev_state(), Some(RoomState::Invited));
3717 assert_eq!(room.state(), RoomState::Joined);
3718
3719 let mut room_info = room.clone_info();
3721 room_info.mark_as_left();
3722 room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP);
3723 assert_eq!(room.prev_state(), Some(RoomState::Joined));
3724 assert_eq!(room.state(), RoomState::Left);
3725
3726 let mut room_info = room.clone_info();
3728 room_info.mark_as_banned();
3729 room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP);
3730 assert_eq!(room.prev_state(), Some(RoomState::Left));
3731 assert_eq!(room.state(), RoomState::Banned);
3732 }
3733
3734 #[async_test]
3735 async fn test_room_state_filters() {
3736 let client = logged_in_base_client(None).await;
3737
3738 let joined_room_id = owned_room_id!("!joined:example.org");
3739 client.get_or_create_room(&joined_room_id, RoomState::Joined);
3740
3741 let invited_room_id = owned_room_id!("!invited:example.org");
3742 client.get_or_create_room(&invited_room_id, RoomState::Invited);
3743
3744 let left_room_id = owned_room_id!("!left:example.org");
3745 client.get_or_create_room(&left_room_id, RoomState::Left);
3746
3747 let knocked_room_id = owned_room_id!("!knocked:example.org");
3748 client.get_or_create_room(&knocked_room_id, RoomState::Knocked);
3749
3750 let banned_room_id = owned_room_id!("!banned:example.org");
3751 client.get_or_create_room(&banned_room_id, RoomState::Banned);
3752
3753 let joined_rooms = client.rooms_filtered(RoomStateFilter::JOINED);
3754 assert_eq!(joined_rooms.len(), 1);
3755 assert_eq!(joined_rooms[0].state(), RoomState::Joined);
3756 assert_eq!(joined_rooms[0].room_id, joined_room_id);
3757
3758 let invited_rooms = client.rooms_filtered(RoomStateFilter::INVITED);
3759 assert_eq!(invited_rooms.len(), 1);
3760 assert_eq!(invited_rooms[0].state(), RoomState::Invited);
3761 assert_eq!(invited_rooms[0].room_id, invited_room_id);
3762
3763 let left_rooms = client.rooms_filtered(RoomStateFilter::LEFT);
3764 assert_eq!(left_rooms.len(), 1);
3765 assert_eq!(left_rooms[0].state(), RoomState::Left);
3766 assert_eq!(left_rooms[0].room_id, left_room_id);
3767
3768 let knocked_rooms = client.rooms_filtered(RoomStateFilter::KNOCKED);
3769 assert_eq!(knocked_rooms.len(), 1);
3770 assert_eq!(knocked_rooms[0].state(), RoomState::Knocked);
3771 assert_eq!(knocked_rooms[0].room_id, knocked_room_id);
3772
3773 let banned_rooms = client.rooms_filtered(RoomStateFilter::BANNED);
3774 assert_eq!(banned_rooms.len(), 1);
3775 assert_eq!(banned_rooms[0].state(), RoomState::Banned);
3776 assert_eq!(banned_rooms[0].room_id, banned_room_id);
3777 }
3778
3779 #[test]
3780 fn test_room_state_filters_as_vec() {
3781 assert_eq!(RoomStateFilter::JOINED.as_vec(), vec![RoomState::Joined]);
3782 assert_eq!(RoomStateFilter::LEFT.as_vec(), vec![RoomState::Left]);
3783 assert_eq!(RoomStateFilter::INVITED.as_vec(), vec![RoomState::Invited]);
3784 assert_eq!(RoomStateFilter::KNOCKED.as_vec(), vec![RoomState::Knocked]);
3785 assert_eq!(RoomStateFilter::BANNED.as_vec(), vec![RoomState::Banned]);
3786
3787 assert_eq!(
3789 RoomStateFilter::all().as_vec(),
3790 vec![
3791 RoomState::Joined,
3792 RoomState::Left,
3793 RoomState::Invited,
3794 RoomState::Knocked,
3795 RoomState::Banned
3796 ]
3797 );
3798 }
3799}