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