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 response_processors as processors,
2250 rooms::RoomNotableTags,
2251 store::{
2252 IntoStateStore, MemoryStore, RoomLoadSettings, StateChanges, StateStore, StoreConfig,
2253 },
2254 test_utils::logged_in_base_client,
2255 BaseClient, MinimalStateEvent, OriginalMinimalStateEvent, RoomDisplayName,
2256 RoomInfoNotableUpdateReasons, RoomStateFilter, SessionMeta,
2257 };
2258
2259 #[test]
2260 fn test_room_info_serialization() {
2261 use ruma::owned_user_id;
2265
2266 use super::RoomSummary;
2267 use crate::{rooms::BaseRoomInfo, sync::UnreadNotificationsCount};
2268
2269 let info = RoomInfo {
2270 version: 1,
2271 room_id: room_id!("!gda78o:server.tld").into(),
2272 room_state: RoomState::Invited,
2273 prev_room_state: None,
2274 notification_counts: UnreadNotificationsCount {
2275 highlight_count: 1,
2276 notification_count: 2,
2277 },
2278 summary: RoomSummary {
2279 room_heroes: vec![RoomHero {
2280 user_id: owned_user_id!("@somebody:example.org"),
2281 display_name: None,
2282 avatar_url: None,
2283 }],
2284 joined_member_count: 5,
2285 invited_member_count: 0,
2286 },
2287 members_synced: true,
2288 last_prev_batch: Some("pb".to_owned()),
2289 sync_info: SyncInfo::FullySynced,
2290 encryption_state_synced: true,
2291 latest_event: Some(Box::new(LatestEvent::new(TimelineEvent::new(
2292 Raw::from_json_string(json!({"sender": "@u:i.uk"}).to_string()).unwrap(),
2293 )))),
2294 base_info: Box::new(
2295 assign!(BaseRoomInfo::new(), { pinned_events: Some(RoomPinnedEventsEventContent::new(vec![owned_event_id!("$a")])) }),
2296 ),
2297 read_receipts: Default::default(),
2298 warned_about_unknown_room_version: Arc::new(false.into()),
2299 cached_display_name: None,
2300 cached_user_defined_notification_mode: None,
2301 recency_stamp: Some(42),
2302 };
2303
2304 let info_json = json!({
2305 "version": 1,
2306 "room_id": "!gda78o:server.tld",
2307 "room_state": "Invited",
2308 "prev_room_state": null,
2309 "notification_counts": {
2310 "highlight_count": 1,
2311 "notification_count": 2,
2312 },
2313 "summary": {
2314 "room_heroes": [{
2315 "user_id": "@somebody:example.org",
2316 "display_name": null,
2317 "avatar_url": null
2318 }],
2319 "joined_member_count": 5,
2320 "invited_member_count": 0,
2321 },
2322 "members_synced": true,
2323 "last_prev_batch": "pb",
2324 "sync_info": "FullySynced",
2325 "encryption_state_synced": true,
2326 "latest_event": {
2327 "event": {
2328 "kind": {"PlainText": {"event": {"sender": "@u:i.uk"}}},
2329 },
2330 },
2331 "base_info": {
2332 "avatar": null,
2333 "canonical_alias": null,
2334 "create": null,
2335 "dm_targets": [],
2336 "encryption": null,
2337 "guest_access": null,
2338 "history_visibility": null,
2339 "is_marked_unread": false,
2340 "join_rules": null,
2341 "max_power_level": 100,
2342 "name": null,
2343 "tombstone": null,
2344 "topic": null,
2345 "pinned_events": {
2346 "pinned": ["$a"]
2347 },
2348 },
2349 "read_receipts": {
2350 "num_unread": 0,
2351 "num_mentions": 0,
2352 "num_notifications": 0,
2353 "latest_active": null,
2354 "pending": []
2355 },
2356 "recency_stamp": 42,
2357 });
2358
2359 assert_eq!(serde_json::to_value(info).unwrap(), info_json);
2360 }
2361
2362 #[test]
2369 fn test_room_info_deserialization_without_optional_items() {
2370 use ruma::{owned_mxc_uri, owned_user_id};
2371
2372 let info_json = json!({
2375 "room_id": "!gda78o:server.tld",
2376 "room_state": "Invited",
2377 "prev_room_state": null,
2378 "notification_counts": {
2379 "highlight_count": 1,
2380 "notification_count": 2,
2381 },
2382 "summary": {
2383 "room_heroes": [{
2384 "user_id": "@somebody:example.org",
2385 "display_name": "Somebody",
2386 "avatar_url": "mxc://example.org/abc"
2387 }],
2388 "joined_member_count": 5,
2389 "invited_member_count": 0,
2390 },
2391 "members_synced": true,
2392 "last_prev_batch": "pb",
2393 "sync_info": "FullySynced",
2394 "encryption_state_synced": true,
2395 "base_info": {
2396 "avatar": null,
2397 "canonical_alias": null,
2398 "create": null,
2399 "dm_targets": [],
2400 "encryption": null,
2401 "guest_access": null,
2402 "history_visibility": null,
2403 "join_rules": null,
2404 "max_power_level": 100,
2405 "name": null,
2406 "tombstone": null,
2407 "topic": null,
2408 },
2409 });
2410
2411 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
2412
2413 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
2414 assert_eq!(info.room_state, RoomState::Invited);
2415 assert_eq!(info.notification_counts.highlight_count, 1);
2416 assert_eq!(info.notification_counts.notification_count, 2);
2417 assert_eq!(
2418 info.summary.room_heroes,
2419 vec![RoomHero {
2420 user_id: owned_user_id!("@somebody:example.org"),
2421 display_name: Some("Somebody".to_owned()),
2422 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
2423 }]
2424 );
2425 assert_eq!(info.summary.joined_member_count, 5);
2426 assert_eq!(info.summary.invited_member_count, 0);
2427 assert!(info.members_synced);
2428 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
2429 assert_eq!(info.sync_info, SyncInfo::FullySynced);
2430 assert!(info.encryption_state_synced);
2431 assert!(info.base_info.avatar.is_none());
2432 assert!(info.base_info.canonical_alias.is_none());
2433 assert!(info.base_info.create.is_none());
2434 assert_eq!(info.base_info.dm_targets.len(), 0);
2435 assert!(info.base_info.encryption.is_none());
2436 assert!(info.base_info.guest_access.is_none());
2437 assert!(info.base_info.history_visibility.is_none());
2438 assert!(info.base_info.join_rules.is_none());
2439 assert_eq!(info.base_info.max_power_level, 100);
2440 assert!(info.base_info.name.is_none());
2441 assert!(info.base_info.tombstone.is_none());
2442 assert!(info.base_info.topic.is_none());
2443 }
2444
2445 #[test]
2446 fn test_room_info_deserialization() {
2447 use ruma::{owned_mxc_uri, owned_user_id};
2448
2449 use crate::notification_settings::RoomNotificationMode;
2450
2451 let info_json = json!({
2452 "room_id": "!gda78o:server.tld",
2453 "room_state": "Joined",
2454 "prev_room_state": "Invited",
2455 "notification_counts": {
2456 "highlight_count": 1,
2457 "notification_count": 2,
2458 },
2459 "summary": {
2460 "room_heroes": [{
2461 "user_id": "@somebody:example.org",
2462 "display_name": "Somebody",
2463 "avatar_url": "mxc://example.org/abc"
2464 }],
2465 "joined_member_count": 5,
2466 "invited_member_count": 0,
2467 },
2468 "members_synced": true,
2469 "last_prev_batch": "pb",
2470 "sync_info": "FullySynced",
2471 "encryption_state_synced": true,
2472 "base_info": {
2473 "avatar": null,
2474 "canonical_alias": null,
2475 "create": null,
2476 "dm_targets": [],
2477 "encryption": null,
2478 "guest_access": null,
2479 "history_visibility": null,
2480 "join_rules": null,
2481 "max_power_level": 100,
2482 "name": null,
2483 "tombstone": null,
2484 "topic": null,
2485 },
2486 "cached_display_name": { "Calculated": "lol" },
2487 "cached_user_defined_notification_mode": "Mute",
2488 "recency_stamp": 42,
2489 });
2490
2491 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
2492
2493 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
2494 assert_eq!(info.room_state, RoomState::Joined);
2495 assert_eq!(info.prev_room_state, Some(RoomState::Invited));
2496 assert_eq!(info.notification_counts.highlight_count, 1);
2497 assert_eq!(info.notification_counts.notification_count, 2);
2498 assert_eq!(
2499 info.summary.room_heroes,
2500 vec![RoomHero {
2501 user_id: owned_user_id!("@somebody:example.org"),
2502 display_name: Some("Somebody".to_owned()),
2503 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
2504 }]
2505 );
2506 assert_eq!(info.summary.joined_member_count, 5);
2507 assert_eq!(info.summary.invited_member_count, 0);
2508 assert!(info.members_synced);
2509 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
2510 assert_eq!(info.sync_info, SyncInfo::FullySynced);
2511 assert!(info.encryption_state_synced);
2512 assert!(info.latest_event.is_none());
2513 assert!(info.base_info.avatar.is_none());
2514 assert!(info.base_info.canonical_alias.is_none());
2515 assert!(info.base_info.create.is_none());
2516 assert_eq!(info.base_info.dm_targets.len(), 0);
2517 assert!(info.base_info.encryption.is_none());
2518 assert!(info.base_info.guest_access.is_none());
2519 assert!(info.base_info.history_visibility.is_none());
2520 assert!(info.base_info.join_rules.is_none());
2521 assert_eq!(info.base_info.max_power_level, 100);
2522 assert!(info.base_info.name.is_none());
2523 assert!(info.base_info.tombstone.is_none());
2524 assert!(info.base_info.topic.is_none());
2525
2526 assert_eq!(
2527 info.cached_display_name.as_ref(),
2528 Some(&RoomDisplayName::Calculated("lol".to_owned())),
2529 );
2530 assert_eq!(
2531 info.cached_user_defined_notification_mode.as_ref(),
2532 Some(&RoomNotificationMode::Mute)
2533 );
2534 assert_eq!(info.recency_stamp.as_ref(), Some(&42));
2535 }
2536
2537 #[async_test]
2538 async fn test_is_favourite() {
2539 let client =
2541 BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
2542
2543 client
2544 .activate(
2545 SessionMeta {
2546 user_id: user_id!("@alice:example.org").into(),
2547 device_id: ruma::device_id!("AYEAYEAYE").into(),
2548 },
2549 RoomLoadSettings::default(),
2550 #[cfg(feature = "e2e-encryption")]
2551 None,
2552 )
2553 .await
2554 .unwrap();
2555
2556 let room_id = room_id!("!test:localhost");
2557 let room = client.get_or_create_room(room_id, RoomState::Joined);
2558
2559 assert!(room.is_favourite().not());
2561
2562 let mut room_info_subscriber = room.subscribe_info();
2564
2565 assert_pending!(room_info_subscriber);
2566
2567 let tag_raw = Raw::new(&json!({
2569 "content": {
2570 "tags": {
2571 "m.favourite": {
2572 "order": 0.0
2573 },
2574 },
2575 },
2576 "type": "m.tag",
2577 }))
2578 .unwrap()
2579 .cast();
2580
2581 let mut context = processors::Context::new(StateChanges::default(), Default::default());
2583
2584 processors::account_data::for_room(&mut context, room_id, &[tag_raw], &client.state_store)
2585 .await;
2586
2587 processors::changes::save_and_apply(
2588 context.clone(),
2589 &client.state_store,
2590 &client.ignore_user_list_changes,
2591 None,
2592 )
2593 .await
2594 .unwrap();
2595
2596 assert_ready!(room_info_subscriber);
2598 assert_pending!(room_info_subscriber);
2599
2600 assert!(room.is_favourite());
2602
2603 let tag_raw = Raw::new(&json!({
2605 "content": {
2606 "tags": {},
2607 },
2608 "type": "m.tag"
2609 }))
2610 .unwrap()
2611 .cast();
2612
2613 processors::account_data::for_room(&mut context, room_id, &[tag_raw], &client.state_store)
2614 .await;
2615
2616 processors::changes::save_and_apply(
2617 context,
2618 &client.state_store,
2619 &client.ignore_user_list_changes,
2620 None,
2621 )
2622 .await
2623 .unwrap();
2624
2625 assert_ready!(room_info_subscriber);
2627 assert_pending!(room_info_subscriber);
2628
2629 assert!(room.is_favourite().not());
2631 }
2632
2633 #[async_test]
2634 async fn test_is_low_priority() {
2635 let client =
2637 BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
2638
2639 client
2640 .activate(
2641 SessionMeta {
2642 user_id: user_id!("@alice:example.org").into(),
2643 device_id: ruma::device_id!("AYEAYEAYE").into(),
2644 },
2645 RoomLoadSettings::default(),
2646 #[cfg(feature = "e2e-encryption")]
2647 None,
2648 )
2649 .await
2650 .unwrap();
2651
2652 let room_id = room_id!("!test:localhost");
2653 let room = client.get_or_create_room(room_id, RoomState::Joined);
2654
2655 assert!(!room.is_low_priority());
2657
2658 let mut room_info_subscriber = room.subscribe_info();
2660
2661 assert_pending!(room_info_subscriber);
2662
2663 let tag_raw = Raw::new(&json!({
2665 "content": {
2666 "tags": {
2667 "m.lowpriority": {
2668 "order": 0.0
2669 },
2670 }
2671 },
2672 "type": "m.tag"
2673 }))
2674 .unwrap()
2675 .cast();
2676
2677 let mut context = processors::Context::new(StateChanges::default(), Default::default());
2679
2680 processors::account_data::for_room(&mut context, room_id, &[tag_raw], &client.state_store)
2681 .await;
2682
2683 processors::changes::save_and_apply(
2684 context.clone(),
2685 &client.state_store,
2686 &client.ignore_user_list_changes,
2687 None,
2688 )
2689 .await
2690 .unwrap();
2691
2692 assert_ready!(room_info_subscriber);
2694 assert_pending!(room_info_subscriber);
2695
2696 assert!(room.is_low_priority());
2698
2699 let tag_raw = Raw::new(&json!({
2701 "content": {
2702 "tags": {},
2703 },
2704 "type": "m.tag"
2705 }))
2706 .unwrap()
2707 .cast();
2708
2709 processors::account_data::for_room(&mut context, room_id, &[tag_raw], &client.state_store)
2710 .await;
2711
2712 processors::changes::save_and_apply(
2713 context,
2714 &client.state_store,
2715 &client.ignore_user_list_changes,
2716 None,
2717 )
2718 .await
2719 .unwrap();
2720
2721 assert_ready!(room_info_subscriber);
2723 assert_pending!(room_info_subscriber);
2724
2725 assert!(room.is_low_priority().not());
2727 }
2728
2729 fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
2730 let store = Arc::new(MemoryStore::new());
2731 let user_id = user_id!("@me:example.org");
2732 let room_id = room_id!("!test:localhost");
2733 let (sender, _receiver) = tokio::sync::broadcast::channel(1);
2734
2735 (store.clone(), Room::new(user_id, store, room_id, room_type, sender))
2736 }
2737
2738 fn make_stripped_member_event(user_id: &UserId, name: &str) -> Raw<StrippedRoomMemberEvent> {
2739 let ev_json = json!({
2740 "type": "m.room.member",
2741 "content": assign!(RoomMemberEventContent::new(MembershipState::Join), {
2742 displayname: Some(name.to_owned())
2743 }),
2744 "sender": user_id,
2745 "state_key": user_id,
2746 });
2747
2748 Raw::new(&ev_json).unwrap().cast()
2749 }
2750
2751 #[async_test]
2752 async fn test_display_name_for_joined_room_is_empty_if_no_info() {
2753 let (_, room) = make_room_test_helper(RoomState::Joined);
2754 assert_eq!(room.compute_display_name().await.unwrap(), RoomDisplayName::Empty);
2755 }
2756
2757 #[async_test]
2758 async fn test_display_name_for_joined_room_uses_canonical_alias_if_available() {
2759 let (_, room) = make_room_test_helper(RoomState::Joined);
2760 room.inner
2761 .update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
2762 assert_eq!(
2763 room.compute_display_name().await.unwrap(),
2764 RoomDisplayName::Aliased("test".to_owned())
2765 );
2766 }
2767
2768 #[async_test]
2769 async fn test_display_name_for_joined_room_prefers_name_over_alias() {
2770 let (_, room) = make_room_test_helper(RoomState::Joined);
2771 room.inner
2772 .update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
2773 assert_eq!(
2774 room.compute_display_name().await.unwrap(),
2775 RoomDisplayName::Aliased("test".to_owned())
2776 );
2777 room.inner.update(|info| info.base_info.name = Some(make_name_event()));
2778 assert_eq!(
2780 room.compute_display_name().await.unwrap(),
2781 RoomDisplayName::Named("Test Room".to_owned())
2782 );
2783 }
2784
2785 #[async_test]
2786 async fn test_display_name_for_invited_room_is_empty_if_no_info() {
2787 let (_, room) = make_room_test_helper(RoomState::Invited);
2788 assert_eq!(room.compute_display_name().await.unwrap(), RoomDisplayName::Empty);
2789 }
2790
2791 #[async_test]
2792 async fn test_display_name_for_invited_room_is_empty_if_room_name_empty() {
2793 let (_, room) = make_room_test_helper(RoomState::Invited);
2794
2795 let room_name = MinimalStateEvent::Original(OriginalMinimalStateEvent {
2796 content: RoomNameEventContent::new(String::new()),
2797 event_id: None,
2798 });
2799 room.inner.update(|info| info.base_info.name = Some(room_name));
2800
2801 assert_eq!(room.compute_display_name().await.unwrap(), RoomDisplayName::Empty);
2802 }
2803
2804 #[async_test]
2805 async fn test_display_name_for_invited_room_uses_canonical_alias_if_available() {
2806 let (_, room) = make_room_test_helper(RoomState::Invited);
2807 room.inner
2808 .update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
2809 assert_eq!(
2810 room.compute_display_name().await.unwrap(),
2811 RoomDisplayName::Aliased("test".to_owned())
2812 );
2813 }
2814
2815 #[async_test]
2816 async fn test_display_name_for_invited_room_prefers_name_over_alias() {
2817 let (_, room) = make_room_test_helper(RoomState::Invited);
2818 room.inner
2819 .update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
2820 assert_eq!(
2821 room.compute_display_name().await.unwrap(),
2822 RoomDisplayName::Aliased("test".to_owned())
2823 );
2824 room.inner.update(|info| info.base_info.name = Some(make_name_event()));
2825 assert_eq!(
2827 room.compute_display_name().await.unwrap(),
2828 RoomDisplayName::Named("Test Room".to_owned())
2829 );
2830 }
2831
2832 fn make_canonical_alias_event() -> MinimalStateEvent<RoomCanonicalAliasEventContent> {
2833 MinimalStateEvent::Original(OriginalMinimalStateEvent {
2834 content: assign!(RoomCanonicalAliasEventContent::new(), {
2835 alias: Some(room_alias_id!("#test:example.com").to_owned()),
2836 }),
2837 event_id: None,
2838 })
2839 }
2840
2841 fn make_name_event() -> MinimalStateEvent<RoomNameEventContent> {
2842 MinimalStateEvent::Original(OriginalMinimalStateEvent {
2843 content: RoomNameEventContent::new("Test Room".to_owned()),
2844 event_id: None,
2845 })
2846 }
2847
2848 #[async_test]
2849 async fn test_display_name_dm_invited() {
2850 let (store, room) = make_room_test_helper(RoomState::Invited);
2851 let room_id = room_id!("!test:localhost");
2852 let matthew = user_id!("@matthew:example.org");
2853 let me = user_id!("@me:example.org");
2854 let mut changes = StateChanges::new("".to_owned());
2855 let summary = assign!(RumaSummary::new(), {
2856 heroes: vec![me.to_owned(), matthew.to_owned()],
2857 });
2858
2859 changes.add_stripped_member(
2860 room_id,
2861 matthew,
2862 make_stripped_member_event(matthew, "Matthew"),
2863 );
2864 changes.add_stripped_member(room_id, me, make_stripped_member_event(me, "Me"));
2865 store.save_changes(&changes).await.unwrap();
2866
2867 room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
2868 assert_eq!(
2869 room.compute_display_name().await.unwrap(),
2870 RoomDisplayName::Calculated("Matthew".to_owned())
2871 );
2872 }
2873
2874 #[async_test]
2875 async fn test_display_name_dm_invited_no_heroes() {
2876 let (store, room) = make_room_test_helper(RoomState::Invited);
2877 let room_id = room_id!("!test:localhost");
2878 let matthew = user_id!("@matthew:example.org");
2879 let me = user_id!("@me:example.org");
2880 let mut changes = StateChanges::new("".to_owned());
2881
2882 changes.add_stripped_member(
2883 room_id,
2884 matthew,
2885 make_stripped_member_event(matthew, "Matthew"),
2886 );
2887 changes.add_stripped_member(room_id, me, make_stripped_member_event(me, "Me"));
2888 store.save_changes(&changes).await.unwrap();
2889
2890 assert_eq!(
2891 room.compute_display_name().await.unwrap(),
2892 RoomDisplayName::Calculated("Matthew".to_owned())
2893 );
2894 }
2895
2896 #[async_test]
2897 async fn test_display_name_dm_joined() {
2898 let (store, room) = make_room_test_helper(RoomState::Joined);
2899 let room_id = room_id!("!test:localhost");
2900 let matthew = user_id!("@matthew:example.org");
2901 let me = user_id!("@me:example.org");
2902
2903 let mut changes = StateChanges::new("".to_owned());
2904 let summary = assign!(RumaSummary::new(), {
2905 joined_member_count: Some(2u32.into()),
2906 heroes: vec![me.to_owned(), matthew.to_owned()],
2907 });
2908
2909 let f = EventFactory::new().room(room_id!("!test:localhost"));
2910
2911 let members = changes
2912 .state
2913 .entry(room_id.to_owned())
2914 .or_default()
2915 .entry(StateEventType::RoomMember)
2916 .or_default();
2917 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
2918 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
2919
2920 store.save_changes(&changes).await.unwrap();
2921
2922 room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
2923 assert_eq!(
2924 room.compute_display_name().await.unwrap(),
2925 RoomDisplayName::Calculated("Matthew".to_owned())
2926 );
2927 }
2928
2929 #[async_test]
2930 async fn test_display_name_dm_joined_service_members() {
2931 let (store, room) = make_room_test_helper(RoomState::Joined);
2932 let room_id = room_id!("!test:localhost");
2933
2934 let matthew = user_id!("@sahasrhala:example.org");
2935 let me = user_id!("@me:example.org");
2936 let bot = user_id!("@bot:example.org");
2937
2938 let mut changes = StateChanges::new("".to_owned());
2939 let summary = assign!(RumaSummary::new(), {
2940 joined_member_count: Some(3u32.into()),
2941 heroes: vec![me.to_owned(), matthew.to_owned(), bot.to_owned()],
2942 });
2943
2944 let f = EventFactory::new().room(room_id!("!test:localhost"));
2945
2946 let members = changes
2947 .state
2948 .entry(room_id.to_owned())
2949 .or_default()
2950 .entry(StateEventType::RoomMember)
2951 .or_default();
2952 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
2953 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
2954 members.insert(bot.into(), f.member(bot).display_name("Bot").into_raw());
2955
2956 let member_hints_content =
2957 f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into_raw();
2958 changes
2959 .state
2960 .entry(room_id.to_owned())
2961 .or_default()
2962 .entry(StateEventType::MemberHints)
2963 .or_default()
2964 .insert("".to_owned(), member_hints_content);
2965
2966 store.save_changes(&changes).await.unwrap();
2967
2968 room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
2969 assert_eq!(
2971 room.compute_display_name().await.unwrap(),
2972 RoomDisplayName::Calculated("Matthew".to_owned())
2973 );
2974 }
2975
2976 #[async_test]
2977 async fn test_display_name_dm_joined_alone_with_service_members() {
2978 let (store, room) = make_room_test_helper(RoomState::Joined);
2979 let room_id = room_id!("!test:localhost");
2980
2981 let me = user_id!("@me:example.org");
2982 let bot = user_id!("@bot:example.org");
2983
2984 let mut changes = StateChanges::new("".to_owned());
2985 let summary = assign!(RumaSummary::new(), {
2986 joined_member_count: Some(2u32.into()),
2987 heroes: vec![me.to_owned(), bot.to_owned()],
2988 });
2989
2990 let f = EventFactory::new().room(room_id!("!test:localhost"));
2991
2992 let members = changes
2993 .state
2994 .entry(room_id.to_owned())
2995 .or_default()
2996 .entry(StateEventType::RoomMember)
2997 .or_default();
2998 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
2999 members.insert(bot.into(), f.member(bot).display_name("Bot").into_raw());
3000
3001 let member_hints_content =
3002 f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into_raw();
3003 changes
3004 .state
3005 .entry(room_id.to_owned())
3006 .or_default()
3007 .entry(StateEventType::MemberHints)
3008 .or_default()
3009 .insert("".to_owned(), member_hints_content);
3010
3011 store.save_changes(&changes).await.unwrap();
3012
3013 room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
3014 assert_eq!(room.compute_display_name().await.unwrap(), RoomDisplayName::Empty);
3016 }
3017
3018 #[async_test]
3019 async fn test_display_name_dm_joined_no_heroes() {
3020 let (store, room) = make_room_test_helper(RoomState::Joined);
3021 let room_id = room_id!("!test:localhost");
3022 let matthew = user_id!("@matthew:example.org");
3023 let me = user_id!("@me:example.org");
3024 let mut changes = StateChanges::new("".to_owned());
3025
3026 let f = EventFactory::new().room(room_id!("!test:localhost"));
3027
3028 let members = changes
3029 .state
3030 .entry(room_id.to_owned())
3031 .or_default()
3032 .entry(StateEventType::RoomMember)
3033 .or_default();
3034 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
3035 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
3036
3037 store.save_changes(&changes).await.unwrap();
3038
3039 assert_eq!(
3040 room.compute_display_name().await.unwrap(),
3041 RoomDisplayName::Calculated("Matthew".to_owned())
3042 );
3043 }
3044
3045 #[async_test]
3046 async fn test_display_name_dm_joined_no_heroes_service_members() {
3047 let (store, room) = make_room_test_helper(RoomState::Joined);
3048 let room_id = room_id!("!test:localhost");
3049
3050 let matthew = user_id!("@matthew:example.org");
3051 let me = user_id!("@me:example.org");
3052 let bot = user_id!("@bot:example.org");
3053
3054 let mut changes = StateChanges::new("".to_owned());
3055
3056 let f = EventFactory::new().room(room_id!("!test:localhost"));
3057
3058 let members = changes
3059 .state
3060 .entry(room_id.to_owned())
3061 .or_default()
3062 .entry(StateEventType::RoomMember)
3063 .or_default();
3064 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
3065 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
3066 members.insert(bot.into(), f.member(bot).display_name("Bot").into_raw());
3067
3068 let member_hints_content =
3069 f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into_raw();
3070 changes
3071 .state
3072 .entry(room_id.to_owned())
3073 .or_default()
3074 .entry(StateEventType::MemberHints)
3075 .or_default()
3076 .insert("".to_owned(), member_hints_content);
3077
3078 store.save_changes(&changes).await.unwrap();
3079
3080 assert_eq!(
3081 room.compute_display_name().await.unwrap(),
3082 RoomDisplayName::Calculated("Matthew".to_owned())
3083 );
3084 }
3085
3086 #[async_test]
3087 async fn test_display_name_deterministic() {
3088 let (store, room) = make_room_test_helper(RoomState::Joined);
3089
3090 let alice = user_id!("@alice:example.org");
3091 let bob = user_id!("@bob:example.org");
3092 let carol = user_id!("@carol:example.org");
3093 let denis = user_id!("@denis:example.org");
3094 let erica = user_id!("@erica:example.org");
3095 let fred = user_id!("@fred:example.org");
3096 let me = user_id!("@me:example.org");
3097
3098 let mut changes = StateChanges::new("".to_owned());
3099
3100 let f = EventFactory::new().room(room_id!("!test:localhost"));
3101
3102 {
3105 let members = changes
3106 .state
3107 .entry(room.room_id().to_owned())
3108 .or_default()
3109 .entry(StateEventType::RoomMember)
3110 .or_default();
3111 members.insert(carol.into(), f.member(carol).display_name("Carol").into_raw());
3112 members.insert(bob.into(), f.member(bob).display_name("Bob").into_raw());
3113 members.insert(fred.into(), f.member(fred).display_name("Fred").into_raw());
3114 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
3115 store.save_changes(&changes).await.unwrap();
3116 }
3117
3118 {
3119 let members = changes
3120 .state
3121 .entry(room.room_id().to_owned())
3122 .or_default()
3123 .entry(StateEventType::RoomMember)
3124 .or_default();
3125 members.insert(alice.into(), f.member(alice).display_name("Alice").into_raw());
3126 members.insert(erica.into(), f.member(erica).display_name("Erica").into_raw());
3127 members.insert(denis.into(), f.member(denis).display_name("Denis").into_raw());
3128 store.save_changes(&changes).await.unwrap();
3129 }
3130
3131 let summary = assign!(RumaSummary::new(), {
3132 joined_member_count: Some(7u32.into()),
3133 heroes: vec![denis.to_owned(), carol.to_owned(), bob.to_owned(), erica.to_owned()],
3134 });
3135 room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
3136
3137 assert_eq!(
3138 room.compute_display_name().await.unwrap(),
3139 RoomDisplayName::Calculated("Bob, Carol, Denis, Erica, and 3 others".to_owned())
3140 );
3141 }
3142
3143 #[async_test]
3144 async fn test_display_name_deterministic_no_heroes() {
3145 let (store, room) = make_room_test_helper(RoomState::Joined);
3146
3147 let alice = user_id!("@alice:example.org");
3148 let bob = user_id!("@bob:example.org");
3149 let carol = user_id!("@carol:example.org");
3150 let denis = user_id!("@denis:example.org");
3151 let erica = user_id!("@erica:example.org");
3152 let fred = user_id!("@fred:example.org");
3153 let me = user_id!("@me:example.org");
3154
3155 let f = EventFactory::new().room(room_id!("!test:localhost"));
3156
3157 let mut changes = StateChanges::new("".to_owned());
3158
3159 {
3162 let members = changes
3163 .state
3164 .entry(room.room_id().to_owned())
3165 .or_default()
3166 .entry(StateEventType::RoomMember)
3167 .or_default();
3168 members.insert(carol.into(), f.member(carol).display_name("Carol").into_raw());
3169 members.insert(bob.into(), f.member(bob).display_name("Bob").into_raw());
3170 members.insert(fred.into(), f.member(fred).display_name("Fred").into_raw());
3171 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
3172
3173 store.save_changes(&changes).await.unwrap();
3174 }
3175
3176 {
3177 let members = changes
3178 .state
3179 .entry(room.room_id().to_owned())
3180 .or_default()
3181 .entry(StateEventType::RoomMember)
3182 .or_default();
3183 members.insert(alice.into(), f.member(alice).display_name("Alice").into_raw());
3184 members.insert(erica.into(), f.member(erica).display_name("Erica").into_raw());
3185 members.insert(denis.into(), f.member(denis).display_name("Denis").into_raw());
3186 store.save_changes(&changes).await.unwrap();
3187 }
3188
3189 assert_eq!(
3190 room.compute_display_name().await.unwrap(),
3191 RoomDisplayName::Calculated("Alice, Bob, Carol, Denis, Erica, and 2 others".to_owned())
3192 );
3193 }
3194
3195 #[async_test]
3196 async fn test_display_name_dm_alone() {
3197 let (store, room) = make_room_test_helper(RoomState::Joined);
3198 let room_id = room_id!("!test:localhost");
3199 let matthew = user_id!("@matthew:example.org");
3200 let me = user_id!("@me:example.org");
3201 let mut changes = StateChanges::new("".to_owned());
3202 let summary = assign!(RumaSummary::new(), {
3203 joined_member_count: Some(1u32.into()),
3204 heroes: vec![me.to_owned(), matthew.to_owned()],
3205 });
3206
3207 let f = EventFactory::new().room(room_id!("!test:localhost"));
3208
3209 let members = changes
3210 .state
3211 .entry(room_id.to_owned())
3212 .or_default()
3213 .entry(StateEventType::RoomMember)
3214 .or_default();
3215 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
3216 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
3217
3218 store.save_changes(&changes).await.unwrap();
3219
3220 room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
3221 assert_eq!(
3222 room.compute_display_name().await.unwrap(),
3223 RoomDisplayName::EmptyWas("Matthew".to_owned())
3224 );
3225 }
3226
3227 #[cfg(feature = "e2e-encryption")]
3228 #[async_test]
3229 async fn test_setting_the_latest_event_doesnt_cause_a_room_info_notable_update() {
3230 use assert_matches::assert_matches;
3231
3232 use crate::{RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons};
3233
3234 let client =
3236 BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
3237
3238 client
3239 .activate(
3240 SessionMeta {
3241 user_id: user_id!("@alice:example.org").into(),
3242 device_id: ruma::device_id!("AYEAYEAYE").into(),
3243 },
3244 RoomLoadSettings::default(),
3245 None,
3246 )
3247 .await
3248 .unwrap();
3249
3250 let room_id = room_id!("!test:localhost");
3251 let room = client.get_or_create_room(room_id, RoomState::Joined);
3252
3253 add_encrypted_event(&room, "$A");
3255 assert!(room.latest_event().is_none());
3257
3258 let mut room_info_notable_update = client.room_info_notable_update_receiver();
3260
3261 let event = make_latest_event("$A");
3263
3264 let mut context = processors::Context::new(StateChanges::default(), Default::default());
3265 room.on_latest_event_decrypted(
3266 event.clone(),
3267 0,
3268 &mut context.state_changes,
3269 &mut context.room_info_notable_updates,
3270 );
3271
3272 assert!(context.room_info_notable_updates.contains_key(room_id));
3273
3274 assert!(room_info_notable_update.try_recv().is_err());
3276
3277 processors::changes::save_and_apply(
3279 context,
3280 &client.state_store,
3281 &client.ignore_user_list_changes,
3282 None,
3283 )
3284 .await
3285 .unwrap();
3286
3287 assert_eq!(room.latest_event().unwrap().event_id(), event.event_id());
3288
3289 assert_matches!(
3291 room_info_notable_update.recv().await,
3292 Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons }) => {
3293 assert_eq!(received_room_id, room_id);
3294 assert!(reasons.contains(RoomInfoNotableUpdateReasons::LATEST_EVENT));
3295 }
3296 );
3297 }
3298
3299 #[cfg(feature = "e2e-encryption")]
3300 #[async_test]
3301 async fn test_when_we_provide_a_newly_decrypted_event_it_replaces_latest_event() {
3302 use std::collections::BTreeMap;
3303
3304 let (_store, room) = make_room_test_helper(RoomState::Joined);
3306 add_encrypted_event(&room, "$A");
3307 assert!(room.latest_event().is_none());
3309
3310 let event = make_latest_event("$A");
3312 let mut changes = StateChanges::default();
3313 let mut room_info_notable_updates = BTreeMap::new();
3314 room.on_latest_event_decrypted(
3315 event.clone(),
3316 0,
3317 &mut changes,
3318 &mut room_info_notable_updates,
3319 );
3320 room.set_room_info(
3321 changes.room_infos.get(room.room_id()).cloned().unwrap(),
3322 room_info_notable_updates.get(room.room_id()).copied().unwrap(),
3323 );
3324
3325 assert_eq!(room.latest_event().unwrap().event_id(), event.event_id());
3327 }
3328
3329 #[cfg(feature = "e2e-encryption")]
3330 #[async_test]
3331 async fn test_when_a_newly_decrypted_event_appears_we_delete_all_older_encrypted_events() {
3332 use std::collections::BTreeMap;
3333
3334 let (_store, room) = make_room_test_helper(RoomState::Joined);
3336 room.inner.update(|info| info.latest_event = Some(make_latest_event("$A")));
3337 add_encrypted_event(&room, "$0");
3338 add_encrypted_event(&room, "$1");
3339 add_encrypted_event(&room, "$2");
3340 add_encrypted_event(&room, "$3");
3341
3342 let new_event = make_latest_event("$1");
3344 let new_event_index = 1;
3345 let mut changes = StateChanges::default();
3346 let mut room_info_notable_updates = BTreeMap::new();
3347 room.on_latest_event_decrypted(
3348 new_event.clone(),
3349 new_event_index,
3350 &mut changes,
3351 &mut room_info_notable_updates,
3352 );
3353 room.set_room_info(
3354 changes.room_infos.get(room.room_id()).cloned().unwrap(),
3355 room_info_notable_updates.get(room.room_id()).copied().unwrap(),
3356 );
3357
3358 let enc_evs = room.latest_encrypted_events();
3360 assert_eq!(enc_evs.len(), 2);
3361 assert_eq!(enc_evs[0].get_field::<&str>("event_id").unwrap().unwrap(), "$2");
3362 assert_eq!(enc_evs[1].get_field::<&str>("event_id").unwrap().unwrap(), "$3");
3363
3364 assert_eq!(room.latest_event().unwrap().event_id(), new_event.event_id());
3366 }
3367
3368 #[cfg(feature = "e2e-encryption")]
3369 #[async_test]
3370 async fn test_replacing_the_newest_event_leaves_none_left() {
3371 use std::collections::BTreeMap;
3372
3373 let (_store, room) = make_room_test_helper(RoomState::Joined);
3375 add_encrypted_event(&room, "$0");
3376 add_encrypted_event(&room, "$1");
3377 add_encrypted_event(&room, "$2");
3378 add_encrypted_event(&room, "$3");
3379
3380 let new_event = make_latest_event("$3");
3382 let new_event_index = 3;
3383 let mut changes = StateChanges::default();
3384 let mut room_info_notable_updates = BTreeMap::new();
3385 room.on_latest_event_decrypted(
3386 new_event,
3387 new_event_index,
3388 &mut changes,
3389 &mut room_info_notable_updates,
3390 );
3391 room.set_room_info(
3392 changes.room_infos.get(room.room_id()).cloned().unwrap(),
3393 room_info_notable_updates.get(room.room_id()).copied().unwrap(),
3394 );
3395
3396 let enc_evs = room.latest_encrypted_events();
3398 assert_eq!(enc_evs.len(), 0);
3399 }
3400
3401 #[cfg(feature = "e2e-encryption")]
3402 fn add_encrypted_event(room: &Room, event_id: &str) {
3403 room.latest_encrypted_events
3404 .write()
3405 .unwrap()
3406 .push(Raw::from_json_string(json!({ "event_id": event_id }).to_string()).unwrap());
3407 }
3408
3409 #[cfg(feature = "e2e-encryption")]
3410 fn make_latest_event(event_id: &str) -> Box<LatestEvent> {
3411 Box::new(LatestEvent::new(TimelineEvent::new(
3412 Raw::from_json_string(json!({ "event_id": event_id }).to_string()).unwrap(),
3413 )))
3414 }
3415
3416 fn timestamp(minutes_ago: u32) -> MilliSecondsSinceUnixEpoch {
3417 MilliSecondsSinceUnixEpoch::from_system_time(
3418 SystemTime::now().sub(Duration::from_secs((60 * minutes_ago).into())),
3419 )
3420 .expect("date out of range")
3421 }
3422
3423 fn legacy_membership_for_my_call(
3424 device_id: &DeviceId,
3425 membership_id: &str,
3426 minutes_ago: u32,
3427 ) -> LegacyMembershipData {
3428 let (application, foci) = foci_and_application();
3429 assign!(
3430 LegacyMembershipData::from(LegacyMembershipDataInit {
3431 application,
3432 device_id: device_id.to_owned(),
3433 expires: Duration::from_millis(3_600_000),
3434 foci_active: foci,
3435 membership_id: membership_id.to_owned(),
3436 }),
3437 { created_ts: Some(timestamp(minutes_ago)) }
3438 )
3439 }
3440
3441 fn legacy_member_state_event(
3442 memberships: Vec<LegacyMembershipData>,
3443 ev_id: &EventId,
3444 user_id: &UserId,
3445 ) -> AnySyncStateEvent {
3446 let content = CallMemberEventContent::new_legacy(memberships);
3447
3448 AnySyncStateEvent::CallMember(SyncStateEvent::Original(OriginalSyncCallMemberEvent {
3449 content,
3450 event_id: ev_id.to_owned(),
3451 sender: user_id.to_owned(),
3452 origin_server_ts: timestamp(0),
3455 state_key: CallMemberStateKey::new(user_id.to_owned(), None, false),
3456 unsigned: StateUnsigned::new(),
3457 }))
3458 }
3459
3460 struct InitData<'a> {
3461 device_id: &'a DeviceId,
3462 minutes_ago: u32,
3463 }
3464
3465 fn session_member_state_event(
3466 ev_id: &EventId,
3467 user_id: &UserId,
3468 init_data: Option<InitData<'_>>,
3469 ) -> AnySyncStateEvent {
3470 let application = Application::Call(CallApplicationContent::new(
3471 "my_call_id_1".to_owned(),
3472 ruma::events::call::member::CallScope::Room,
3473 ));
3474 let foci_preferred = vec![Focus::Livekit(LivekitFocus::new(
3475 "my_call_foci_alias".to_owned(),
3476 "https://lk.org".to_owned(),
3477 ))];
3478 let focus_active = ActiveFocus::Livekit(ActiveLivekitFocus::new());
3479 let (content, state_key) = match init_data {
3480 Some(InitData { device_id, minutes_ago }) => (
3481 CallMemberEventContent::new(
3482 application,
3483 device_id.to_owned(),
3484 focus_active,
3485 foci_preferred,
3486 Some(timestamp(minutes_ago)),
3487 ),
3488 CallMemberStateKey::new(user_id.to_owned(), Some(device_id.to_owned()), false),
3489 ),
3490 None => (
3491 CallMemberEventContent::new_empty(None),
3492 CallMemberStateKey::new(user_id.to_owned(), None, false),
3493 ),
3494 };
3495
3496 AnySyncStateEvent::CallMember(SyncStateEvent::Original(OriginalSyncCallMemberEvent {
3497 content,
3498 event_id: ev_id.to_owned(),
3499 sender: user_id.to_owned(),
3500 origin_server_ts: timestamp(0),
3503 state_key,
3504 unsigned: StateUnsigned::new(),
3505 }))
3506 }
3507
3508 fn foci_and_application() -> (Application, Vec<Focus>) {
3509 (
3510 Application::Call(CallApplicationContent::new(
3511 "my_call_id_1".to_owned(),
3512 ruma::events::call::member::CallScope::Room,
3513 )),
3514 vec![Focus::Livekit(LivekitFocus::new(
3515 "my_call_foci_alias".to_owned(),
3516 "https://lk.org".to_owned(),
3517 ))],
3518 )
3519 }
3520
3521 fn receive_state_events(room: &Room, events: Vec<&AnySyncStateEvent>) {
3522 room.inner.update_if(|info| {
3523 let mut res = false;
3524 for ev in events {
3525 res |= info.handle_state_event(ev);
3526 }
3527 res
3528 });
3529 }
3530
3531 fn legacy_create_call_with_member_events_for_user(a: &UserId, b: &UserId, c: &UserId) -> Room {
3535 let (_, room) = make_room_test_helper(RoomState::Joined);
3536
3537 let a_empty = legacy_member_state_event(Vec::new(), event_id!("$1234"), a);
3538
3539 let m_init_b = legacy_membership_for_my_call(device_id!("DEVICE_0"), "0", 1);
3541 let b_one = legacy_member_state_event(vec![m_init_b], event_id!("$12345"), b);
3542
3543 let m_init_c1 = legacy_membership_for_my_call(device_id!("DEVICE_0"), "0", 10);
3545 let m_init_c2 = legacy_membership_for_my_call(device_id!("DEVICE_1"), "0", 20);
3547 let c_two = legacy_member_state_event(vec![m_init_c1, m_init_c2], event_id!("$123456"), c);
3548
3549 receive_state_events(&room, vec![&c_two, &a_empty, &b_one]);
3551
3552 room
3553 }
3554
3555 fn session_create_call_with_member_events_for_user(a: &UserId, b: &UserId, c: &UserId) -> Room {
3559 let (_, room) = make_room_test_helper(RoomState::Joined);
3560
3561 let a_empty = session_member_state_event(event_id!("$1234"), a, None);
3562
3563 let b_one = session_member_state_event(
3565 event_id!("$12345"),
3566 b,
3567 Some(InitData { device_id: "DEVICE_0".into(), minutes_ago: 1 }),
3568 );
3569
3570 let m_c1 = session_member_state_event(
3571 event_id!("$123456_0"),
3572 c,
3573 Some(InitData { device_id: "DEVICE_0".into(), minutes_ago: 10 }),
3574 );
3575 let m_c2 = session_member_state_event(
3576 event_id!("$123456_1"),
3577 c,
3578 Some(InitData { device_id: "DEVICE_1".into(), minutes_ago: 20 }),
3579 );
3580 receive_state_events(&room, vec![&m_c1, &m_c2, &a_empty, &b_one]);
3582
3583 room
3584 }
3585
3586 #[test]
3587 fn test_show_correct_active_call_state() {
3588 let room_legacy = legacy_create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL);
3589
3590 assert_eq!(
3594 vec![CAROL.to_owned(), CAROL.to_owned(), BOB.to_owned()],
3595 room_legacy.active_room_call_participants()
3596 );
3597 assert!(room_legacy.has_active_room_call());
3598
3599 let room_session = session_create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL);
3600 assert_eq!(
3601 vec![CAROL.to_owned(), CAROL.to_owned(), BOB.to_owned()],
3602 room_session.active_room_call_participants()
3603 );
3604 assert!(room_session.has_active_room_call());
3605 }
3606
3607 #[test]
3608 fn test_active_call_is_false_when_everyone_left() {
3609 let room = legacy_create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL);
3610
3611 let b_empty_membership = legacy_member_state_event(Vec::new(), event_id!("$1234_1"), &BOB);
3612 let c_empty_membership =
3613 legacy_member_state_event(Vec::new(), event_id!("$12345_1"), &CAROL);
3614
3615 receive_state_events(&room, vec![&b_empty_membership, &c_empty_membership]);
3616
3617 assert_eq!(Vec::<OwnedUserId>::new(), room.active_room_call_participants());
3619 assert!(!room.has_active_room_call());
3620 }
3621
3622 #[test]
3623 fn test_calculate_room_name() {
3624 let mut actual = compute_display_name_from_heroes(2, vec!["a"]);
3625 assert_eq!(RoomDisplayName::Calculated("a".to_owned()), actual);
3626
3627 actual = compute_display_name_from_heroes(3, vec!["a", "b"]);
3628 assert_eq!(RoomDisplayName::Calculated("a, b".to_owned()), actual);
3629
3630 actual = compute_display_name_from_heroes(4, vec!["a", "b", "c"]);
3631 assert_eq!(RoomDisplayName::Calculated("a, b, c".to_owned()), actual);
3632
3633 actual = compute_display_name_from_heroes(5, vec!["a", "b", "c"]);
3634 assert_eq!(RoomDisplayName::Calculated("a, b, c, and 2 others".to_owned()), actual);
3635
3636 actual = compute_display_name_from_heroes(5, vec![]);
3637 assert_eq!(RoomDisplayName::Calculated("5 people".to_owned()), actual);
3638
3639 actual = compute_display_name_from_heroes(0, vec![]);
3640 assert_eq!(RoomDisplayName::Empty, actual);
3641
3642 actual = compute_display_name_from_heroes(1, vec![]);
3643 assert_eq!(RoomDisplayName::Empty, actual);
3644
3645 actual = compute_display_name_from_heroes(1, vec!["a"]);
3646 assert_eq!(RoomDisplayName::EmptyWas("a".to_owned()), actual);
3647
3648 actual = compute_display_name_from_heroes(1, vec!["a", "b"]);
3649 assert_eq!(RoomDisplayName::EmptyWas("a, b".to_owned()), actual);
3650
3651 actual = compute_display_name_from_heroes(1, vec!["a", "b", "c"]);
3652 assert_eq!(RoomDisplayName::EmptyWas("a, b, c".to_owned()), actual);
3653 }
3654
3655 #[test]
3656 fn test_encryption_is_set_when_encryption_event_is_received_encrypted() {
3657 let (_store, room) = make_room_test_helper(RoomState::Joined);
3658
3659 assert_matches!(room.encryption_state(), EncryptionState::Unknown);
3660
3661 let encryption_content =
3662 RoomEncryptionEventContent::new(EventEncryptionAlgorithm::MegolmV1AesSha2);
3663 let encryption_event = AnySyncStateEvent::RoomEncryption(SyncStateEvent::Original(
3664 OriginalSyncRoomEncryptionEvent {
3665 content: encryption_content,
3666 event_id: OwnedEventId::from_str("$1234_1").unwrap(),
3667 sender: ALICE.to_owned(),
3668 origin_server_ts: timestamp(0),
3671 state_key: EmptyStateKey,
3672 unsigned: StateUnsigned::new(),
3673 },
3674 ));
3675 receive_state_events(&room, vec![&encryption_event]);
3676
3677 assert_matches!(room.encryption_state(), EncryptionState::Encrypted);
3678 }
3679
3680 #[test]
3681 fn test_encryption_is_set_when_encryption_event_is_received_not_encrypted() {
3682 let (_store, room) = make_room_test_helper(RoomState::Joined);
3683
3684 assert_matches!(room.encryption_state(), EncryptionState::Unknown);
3685 room.inner.update_if(|info| {
3686 info.mark_encryption_state_synced();
3687
3688 false
3689 });
3690
3691 assert_matches!(room.encryption_state(), EncryptionState::NotEncrypted);
3692 }
3693
3694 #[test]
3695 fn test_encryption_state() {
3696 assert!(EncryptionState::Unknown.is_unknown());
3697 assert!(EncryptionState::Encrypted.is_unknown().not());
3698 assert!(EncryptionState::NotEncrypted.is_unknown().not());
3699
3700 assert!(EncryptionState::Unknown.is_encrypted().not());
3701 assert!(EncryptionState::Encrypted.is_encrypted());
3702 assert!(EncryptionState::NotEncrypted.is_encrypted().not());
3703 }
3704
3705 #[async_test]
3706 async fn test_room_info_migration_v1() {
3707 let store = MemoryStore::new().into_state_store();
3708
3709 let room_info_json = json!({
3710 "room_id": "!gda78o:server.tld",
3711 "room_state": "Joined",
3712 "notification_counts": {
3713 "highlight_count": 1,
3714 "notification_count": 2,
3715 },
3716 "summary": {
3717 "room_heroes": [{
3718 "user_id": "@somebody:example.org",
3719 "display_name": null,
3720 "avatar_url": null
3721 }],
3722 "joined_member_count": 5,
3723 "invited_member_count": 0,
3724 },
3725 "members_synced": true,
3726 "last_prev_batch": "pb",
3727 "sync_info": "FullySynced",
3728 "encryption_state_synced": true,
3729 "latest_event": {
3730 "event": {
3731 "encryption_info": null,
3732 "event": {
3733 "sender": "@u:i.uk",
3734 },
3735 },
3736 },
3737 "base_info": {
3738 "avatar": null,
3739 "canonical_alias": null,
3740 "create": null,
3741 "dm_targets": [],
3742 "encryption": null,
3743 "guest_access": null,
3744 "history_visibility": null,
3745 "join_rules": null,
3746 "max_power_level": 100,
3747 "name": null,
3748 "tombstone": null,
3749 "topic": null,
3750 },
3751 "read_receipts": {
3752 "num_unread": 0,
3753 "num_mentions": 0,
3754 "num_notifications": 0,
3755 "latest_active": null,
3756 "pending": []
3757 },
3758 "recency_stamp": 42,
3759 });
3760 let mut room_info: RoomInfo = serde_json::from_value(room_info_json).unwrap();
3761
3762 assert_eq!(room_info.version, 0);
3763 assert!(room_info.base_info.notable_tags.is_empty());
3764 assert!(room_info.base_info.pinned_events.is_none());
3765
3766 assert!(room_info.apply_migrations(store.clone()).await);
3768
3769 assert_eq!(room_info.version, 1);
3770 assert!(room_info.base_info.notable_tags.is_empty());
3771 assert!(room_info.base_info.pinned_events.is_none());
3772
3773 assert!(!room_info.apply_migrations(store.clone()).await);
3775
3776 assert_eq!(room_info.version, 1);
3777 assert!(room_info.base_info.notable_tags.is_empty());
3778 assert!(room_info.base_info.pinned_events.is_none());
3779
3780 let mut changes = StateChanges::default();
3782
3783 let raw_tag_event = Raw::new(&*TAG).unwrap().cast();
3784 let tag_event = raw_tag_event.deserialize().unwrap();
3785 changes.add_room_account_data(&room_info.room_id, tag_event, raw_tag_event);
3786
3787 let raw_pinned_events_event = Raw::new(&*PINNED_EVENTS).unwrap().cast();
3788 let pinned_events_event = raw_pinned_events_event.deserialize().unwrap();
3789 changes.add_state_event(&room_info.room_id, pinned_events_event, raw_pinned_events_event);
3790
3791 store.save_changes(&changes).await.unwrap();
3792
3793 room_info.version = 0;
3795 assert!(room_info.apply_migrations(store.clone()).await);
3796
3797 assert_eq!(room_info.version, 1);
3798 assert!(room_info.base_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
3799 assert!(room_info.base_info.pinned_events.is_some());
3800
3801 let new_room_info = RoomInfo::new(room_id!("!new_room:localhost"), RoomState::Joined);
3803 assert_eq!(new_room_info.version, 1);
3804 }
3805
3806 #[async_test]
3807 async fn test_prev_room_state_is_updated() {
3808 let (_store, room) = make_room_test_helper(RoomState::Invited);
3809 assert_eq!(room.prev_state(), None);
3810 assert_eq!(room.state(), RoomState::Invited);
3811
3812 let mut room_info = room.clone_info();
3814 room_info.mark_as_joined();
3815 room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP);
3816 assert_eq!(room.prev_state(), Some(RoomState::Invited));
3817 assert_eq!(room.state(), RoomState::Joined);
3818
3819 let mut room_info = room.clone_info();
3821 room_info.mark_as_joined();
3822 room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP);
3823 assert_eq!(room.prev_state(), Some(RoomState::Invited));
3824 assert_eq!(room.state(), RoomState::Joined);
3825
3826 let mut room_info = room.clone_info();
3828 room_info.mark_as_left();
3829 room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP);
3830 assert_eq!(room.prev_state(), Some(RoomState::Joined));
3831 assert_eq!(room.state(), RoomState::Left);
3832
3833 let mut room_info = room.clone_info();
3835 room_info.mark_as_banned();
3836 room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP);
3837 assert_eq!(room.prev_state(), Some(RoomState::Left));
3838 assert_eq!(room.state(), RoomState::Banned);
3839 }
3840
3841 #[async_test]
3842 async fn test_room_state_filters() {
3843 let client = logged_in_base_client(None).await;
3844
3845 let joined_room_id = owned_room_id!("!joined:example.org");
3846 client.get_or_create_room(&joined_room_id, RoomState::Joined);
3847
3848 let invited_room_id = owned_room_id!("!invited:example.org");
3849 client.get_or_create_room(&invited_room_id, RoomState::Invited);
3850
3851 let left_room_id = owned_room_id!("!left:example.org");
3852 client.get_or_create_room(&left_room_id, RoomState::Left);
3853
3854 let knocked_room_id = owned_room_id!("!knocked:example.org");
3855 client.get_or_create_room(&knocked_room_id, RoomState::Knocked);
3856
3857 let banned_room_id = owned_room_id!("!banned:example.org");
3858 client.get_or_create_room(&banned_room_id, RoomState::Banned);
3859
3860 let joined_rooms = client.rooms_filtered(RoomStateFilter::JOINED);
3861 assert_eq!(joined_rooms.len(), 1);
3862 assert_eq!(joined_rooms[0].state(), RoomState::Joined);
3863 assert_eq!(joined_rooms[0].room_id, joined_room_id);
3864
3865 let invited_rooms = client.rooms_filtered(RoomStateFilter::INVITED);
3866 assert_eq!(invited_rooms.len(), 1);
3867 assert_eq!(invited_rooms[0].state(), RoomState::Invited);
3868 assert_eq!(invited_rooms[0].room_id, invited_room_id);
3869
3870 let left_rooms = client.rooms_filtered(RoomStateFilter::LEFT);
3871 assert_eq!(left_rooms.len(), 1);
3872 assert_eq!(left_rooms[0].state(), RoomState::Left);
3873 assert_eq!(left_rooms[0].room_id, left_room_id);
3874
3875 let knocked_rooms = client.rooms_filtered(RoomStateFilter::KNOCKED);
3876 assert_eq!(knocked_rooms.len(), 1);
3877 assert_eq!(knocked_rooms[0].state(), RoomState::Knocked);
3878 assert_eq!(knocked_rooms[0].room_id, knocked_room_id);
3879
3880 let banned_rooms = client.rooms_filtered(RoomStateFilter::BANNED);
3881 assert_eq!(banned_rooms.len(), 1);
3882 assert_eq!(banned_rooms[0].state(), RoomState::Banned);
3883 assert_eq!(banned_rooms[0].room_id, banned_room_id);
3884 }
3885
3886 #[test]
3887 fn test_room_state_filters_as_vec() {
3888 assert_eq!(RoomStateFilter::JOINED.as_vec(), vec![RoomState::Joined]);
3889 assert_eq!(RoomStateFilter::LEFT.as_vec(), vec![RoomState::Left]);
3890 assert_eq!(RoomStateFilter::INVITED.as_vec(), vec![RoomState::Invited]);
3891 assert_eq!(RoomStateFilter::KNOCKED.as_vec(), vec![RoomState::Knocked]);
3892 assert_eq!(RoomStateFilter::BANNED.as_vec(), vec![RoomState::Banned]);
3893
3894 assert_eq!(
3896 RoomStateFilter::all().as_vec(),
3897 vec![
3898 RoomState::Joined,
3899 RoomState::Left,
3900 RoomState::Invited,
3901 RoomState::Knocked,
3902 RoomState::Banned
3903 ]
3904 );
3905 }
3906}