1use std::{
16 collections::{BTreeMap, BTreeSet, HashSet},
17 sync::{Arc, atomic::AtomicBool},
18};
19
20use as_variant::as_variant;
21use bitflags::bitflags;
22use eyeball::Subscriber;
23use matrix_sdk_common::{ROOM_VERSION_FALLBACK, ROOM_VERSION_RULES_FALLBACK};
24use ruma::{
25 EventId, MxcUri, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId,
26 RoomAliasId, RoomId, RoomVersionId,
27 api::client::sync::sync_events::v3::RoomSummary as RumaSummary,
28 events::{
29 AnyPossiblyRedactedStateEventContent, AnyStrippedStateEvent, AnySyncStateEvent,
30 AnySyncTimelineEvent, StateEventType,
31 call::member::{
32 CallMemberStateKey, MembershipData, PossiblyRedactedCallMemberEventContent,
33 },
34 direct::OwnedDirectUserIdentifier,
35 member_hints::PossiblyRedactedMemberHintsEventContent,
36 room::{
37 avatar::{self, PossiblyRedactedRoomAvatarEventContent},
38 canonical_alias::PossiblyRedactedRoomCanonicalAliasEventContent,
39 encryption::PossiblyRedactedRoomEncryptionEventContent,
40 guest_access::{GuestAccess, PossiblyRedactedRoomGuestAccessEventContent},
41 history_visibility::{
42 HistoryVisibility, PossiblyRedactedRoomHistoryVisibilityEventContent,
43 },
44 join_rules::{JoinRule, PossiblyRedactedRoomJoinRulesEventContent},
45 name::PossiblyRedactedRoomNameEventContent,
46 pinned_events::{
47 PossiblyRedactedRoomPinnedEventsEventContent, RoomPinnedEventsEventContent,
48 },
49 redaction::SyncRoomRedactionEvent,
50 retention::RoomRetentionEventContent,
51 tombstone::PossiblyRedactedRoomTombstoneEventContent,
52 topic::PossiblyRedactedRoomTopicEventContent,
53 },
54 rtc::notification::CallIntent,
55 tag::{TagEventContent, TagName, Tags},
56 },
57 room::RoomType,
58 room_version_rules::{RedactionRules, RoomVersionRules},
59 serde::Raw,
60};
61use serde::{Deserialize, Serialize};
62use tokio::sync::MutexGuard;
63use tracing::{field::debug, info, instrument, warn};
64
65use super::{
66 AccountDataSource, EncryptionState, Room, RoomCreateWithCreatorEventContent, RoomDisplayName,
67 RoomHero, RoomNotableTags, RoomState, RoomSummary,
68};
69use crate::{
70 MinimalStateEvent, StateChanges, StoreError,
71 deserialized_responses::RawSyncOrStrippedState,
72 latest_event::LatestEventValue,
73 notification_settings::RoomNotificationMode,
74 read_receipts::ReadReceipts,
75 room::call::CallIntentConsensus,
76 store::{IncorrectMutexGuardError, SaveLockedStateStore, StateStoreExt},
77 sync::UnreadNotificationsCount,
78 utils::{AnyStateEventEnum, RawStateEventWithKeys},
79};
80
81const DEFAULT_MAX_POWER_LEVEL: i64 = 100;
83
84impl Room {
85 pub fn subscribe_info(&self) -> Subscriber<RoomInfo> {
87 self.info.subscribe()
88 }
89
90 pub fn clone_info(&self) -> RoomInfo {
92 self.info.get()
93 }
94
95 pub async fn update_room_info<F>(&self, f: F)
99 where
100 F: FnOnce(RoomInfo) -> (RoomInfo, RoomInfoNotableUpdateReasons),
101 {
102 self.update_room_info_with_store_guard(&self.store.lock().lock().await, f)
103 .expect("should have correct mutex!")
104 }
105
106 pub fn update_room_info_with_store_guard<F>(
113 &self,
114 guard: &MutexGuard<'_, ()>,
115 f: F,
116 ) -> Result<(), IncorrectMutexGuardError>
117 where
118 F: FnOnce(RoomInfo) -> (RoomInfo, RoomInfoNotableUpdateReasons),
119 {
120 if !std::ptr::eq(MutexGuard::mutex(guard), self.store.lock()) {
121 return Err(IncorrectMutexGuardError);
122 }
123
124 let (info, mut reasons) = f(self.clone_info());
125 self.info.set(info);
126
127 if reasons.is_empty() {
128 reasons = RoomInfoNotableUpdateReasons::NONE;
132 }
133 let _ = self
134 .room_info_notable_update_sender
135 .send(RoomInfoNotableUpdate { room_id: self.room_id.clone(), reasons });
136
137 Ok(())
138 }
139
140 pub async fn update_and_save_room_info<F>(&self, f: F) -> Result<(), StoreError>
143 where
144 F: FnOnce(RoomInfo) -> (RoomInfo, RoomInfoNotableUpdateReasons),
145 {
146 self.update_and_save_room_info_with_store_guard(&self.store.lock().lock().await, f).await
147 }
148
149 pub async fn update_and_save_room_info_with_store_guard<F>(
156 &self,
157 guard: &MutexGuard<'_, ()>,
158 f: F,
159 ) -> Result<(), StoreError>
160 where
161 F: FnOnce(RoomInfo) -> (RoomInfo, RoomInfoNotableUpdateReasons),
162 {
163 let (info, reasons) = f(self.clone_info());
164 let mut changes = StateChanges::default();
165 changes.add_room(info.clone());
166 self.store.save_changes_with_guard(guard, &changes).await?;
167 self.update_room_info_with_store_guard(guard, |_| (info, reasons))?;
168 Ok(())
169 }
170}
171
172#[derive(Clone, Debug, Serialize, Deserialize)]
176pub struct BaseRoomInfo {
177 pub(crate) avatar: Option<MinimalStateEvent<PossiblyRedactedRoomAvatarEventContent>>,
179 pub(crate) canonical_alias:
181 Option<MinimalStateEvent<PossiblyRedactedRoomCanonicalAliasEventContent>>,
182 pub(crate) create: Option<MinimalStateEvent<RoomCreateWithCreatorEventContent>>,
184 pub(crate) dm_targets: HashSet<OwnedDirectUserIdentifier>,
187 pub(crate) encryption: Option<PossiblyRedactedRoomEncryptionEventContent>,
189 pub(crate) guest_access: Option<MinimalStateEvent<PossiblyRedactedRoomGuestAccessEventContent>>,
191 pub(crate) history_visibility:
193 Option<MinimalStateEvent<PossiblyRedactedRoomHistoryVisibilityEventContent>>,
194 pub(crate) join_rules: Option<MinimalStateEvent<PossiblyRedactedRoomJoinRulesEventContent>>,
196 pub(crate) max_power_level: i64,
198 pub(crate) member_hints: Option<MinimalStateEvent<PossiblyRedactedMemberHintsEventContent>>,
201 pub(crate) name: Option<MinimalStateEvent<PossiblyRedactedRoomNameEventContent>>,
203 pub(crate) retention: Option<MinimalStateEvent<RoomRetentionEventContent>>,
205 pub(crate) tombstone: Option<MinimalStateEvent<PossiblyRedactedRoomTombstoneEventContent>>,
207 pub(crate) topic: Option<MinimalStateEvent<PossiblyRedactedRoomTopicEventContent>>,
209 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
212 pub(crate) rtc_member_events:
213 BTreeMap<CallMemberStateKey, MinimalStateEvent<PossiblyRedactedCallMemberEventContent>>,
214 #[serde(default)]
216 pub(crate) is_marked_unread: bool,
217 #[serde(default)]
219 pub(crate) is_marked_unread_source: AccountDataSource,
220 #[serde(skip_serializing_if = "RoomNotableTags::is_empty", default)]
225 pub(crate) notable_tags: RoomNotableTags,
226 #[serde(skip_serializing_if = "Option::is_none", default)]
228 pub(crate) fully_read_event_id: Option<OwnedEventId>,
229 pub(crate) pinned_events: Option<PossiblyRedactedRoomPinnedEventsEventContent>,
231}
232
233impl BaseRoomInfo {
234 pub fn new() -> Self {
236 Self::default()
237 }
238
239 pub fn room_version(&self) -> Option<&RoomVersionId> {
244 Some(&self.create.as_ref()?.content.room_version)
245 }
246
247 pub fn handle_state_event<T: AnyStateEventEnum>(
251 &mut self,
252 raw_event: &mut RawStateEventWithKeys<T>,
253 ) -> bool {
254 match (&raw_event.event_type, raw_event.state_key.as_str()) {
255 (StateEventType::RoomEncryption, "") => {
256 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
261 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomEncryption)
262 }) && event.content.algorithm.is_some()
263 {
264 self.encryption = Some(event.content);
265 true
266 } else {
267 false
268 }
269 }
270 (StateEventType::RoomAvatar, "") => {
271 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
272 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomAvatar)
273 }) {
274 self.avatar = Some(event);
275 true
276 } else {
277 self.avatar.take().is_some()
280 }
281 }
282 (StateEventType::RoomName, "") => {
283 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
284 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomName)
285 }) {
286 self.name = Some(event);
287 true
288 } else {
289 self.name.take().is_some()
292 }
293 }
294 (StateEventType::RoomCreate, "") if self.create.is_none() => {
296 if let Some(any_event) = raw_event.deserialize()
297 && let Some(content) = as_variant!(
298 any_event.get_content(),
299 AnyPossiblyRedactedStateEventContent::RoomCreate
300 )
301 {
302 self.create = Some(MinimalStateEvent {
303 content: RoomCreateWithCreatorEventContent::from_event_content(
304 content,
305 any_event.get_sender().to_owned(),
306 ),
307 event_id: any_event.get_event_id().map(ToOwned::to_owned),
308 });
309 true
310 } else {
311 false
312 }
313 }
314 (StateEventType::RoomHistoryVisibility, "") => {
315 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
316 as_variant!(
317 any_event,
318 AnyPossiblyRedactedStateEventContent::RoomHistoryVisibility
319 )
320 }) {
321 self.history_visibility = Some(event);
322 true
323 } else {
324 self.history_visibility.take().is_some()
327 }
328 }
329 (StateEventType::RoomRetention, "") => {
330 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
331 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomRetention)
332 }) {
333 self.retention = Some(event);
334 true
335 } else {
336 self.retention.take().is_some()
339 }
340 }
341 (StateEventType::RoomGuestAccess, "") => {
342 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
343 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomGuestAccess)
344 }) {
345 self.guest_access = Some(event);
346 true
347 } else {
348 self.guest_access.take().is_some()
351 }
352 }
353 (StateEventType::MemberHints, "") => {
354 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
355 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::MemberHints)
356 }) {
357 self.member_hints = Some(event);
358 true
359 } else {
360 self.member_hints.take().is_some()
363 }
364 }
365 (StateEventType::RoomJoinRules, "") => {
366 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
367 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomJoinRules)
368 }) {
369 match &event.content.join_rule {
370 JoinRule::Invite
371 | JoinRule::Knock
372 | JoinRule::Private
373 | JoinRule::Restricted(_)
374 | JoinRule::KnockRestricted(_)
375 | JoinRule::Public => {
376 self.join_rules = Some(event);
377 true
378 }
379 r => {
380 warn!(join_rule = ?r.as_str(), "Encountered a custom join rule, skipping");
381 self.join_rules.take().is_some()
384 }
385 }
386 } else {
387 self.join_rules.take().is_some()
390 }
391 }
392 (StateEventType::RoomCanonicalAlias, "") => {
393 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
394 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomCanonicalAlias)
395 }) {
396 self.canonical_alias = Some(event);
397 true
398 } else {
399 self.canonical_alias.take().is_some()
402 }
403 }
404 (StateEventType::RoomTopic, "") => {
405 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
406 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomTopic)
407 }) {
408 self.topic = Some(event);
409 true
410 } else {
411 self.topic.take().is_some()
414 }
415 }
416 (StateEventType::RoomTombstone, "") => {
417 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
418 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomTombstone)
419 }) {
420 self.tombstone = Some(event);
421 true
422 } else {
423 self.tombstone.take().is_some()
426 }
427 }
428 (StateEventType::RoomPowerLevels, "") => {
429 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
430 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomPowerLevels)
431 }) {
432 let new_max = i64::from(
433 event
434 .content
435 .users
436 .values()
437 .fold(event.content.users_default, |max_pl, user_pl| {
438 max_pl.max(*user_pl)
439 }),
440 );
441
442 if self.max_power_level != new_max {
443 self.max_power_level = new_max;
444 true
445 } else {
446 false
447 }
448 } else if self.max_power_level != DEFAULT_MAX_POWER_LEVEL {
449 self.max_power_level = DEFAULT_MAX_POWER_LEVEL;
451 true
452 } else {
453 false
454 }
455 }
456 (StateEventType::CallMember, _) => {
457 if let Ok(call_member_key) = raw_event.state_key.parse::<CallMemberStateKey>() {
458 if let Some(any_event) = raw_event.deserialize()
459 && let Some(content) = as_variant!(
460 any_event.get_content(),
461 AnyPossiblyRedactedStateEventContent::CallMember
462 )
463 {
464 let mut event = MinimalStateEvent {
465 content,
466 event_id: any_event.get_event_id().map(ToOwned::to_owned),
467 };
468
469 if let Some(origin_server_ts) = any_event.get_origin_server_ts() {
470 event.content.set_created_ts_if_none(origin_server_ts);
471 }
472
473 self.rtc_member_events.insert(call_member_key, event);
475
476 self.rtc_member_events
479 .retain(|_, ev| !ev.content.active_memberships(None).is_empty());
480
481 true
482 } else {
483 self.rtc_member_events.remove(&call_member_key).is_some()
486 }
487 } else {
488 false
489 }
490 }
491 (StateEventType::RoomPinnedEvents, "") => {
492 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
493 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomPinnedEvents)
494 }) {
495 self.pinned_events = Some(event.content);
496 true
497 } else {
498 self.pinned_events.take().is_some()
501 }
502 }
503 _ => false,
504 }
505 }
506
507 pub(super) fn handle_redaction(&mut self, redacts: &EventId) {
508 let redaction_rules = self
509 .room_version()
510 .and_then(|room_version| room_version.rules())
511 .unwrap_or(ROOM_VERSION_RULES_FALLBACK)
512 .redaction;
513
514 if let Some(ev) = &mut self.avatar
515 && ev.event_id.as_deref() == Some(redacts)
516 {
517 ev.redact(&redaction_rules);
518 } else if let Some(ev) = &mut self.canonical_alias
519 && ev.event_id.as_deref() == Some(redacts)
520 {
521 ev.redact(&redaction_rules);
522 } else if let Some(ev) = &mut self.create
523 && ev.event_id.as_deref() == Some(redacts)
524 {
525 ev.redact(&redaction_rules);
526 } else if let Some(ev) = &mut self.guest_access
527 && ev.event_id.as_deref() == Some(redacts)
528 {
529 ev.redact(&redaction_rules);
530 } else if let Some(ev) = &mut self.history_visibility
531 && ev.event_id.as_deref() == Some(redacts)
532 {
533 ev.redact(&redaction_rules);
534 } else if let Some(ev) = &mut self.join_rules
535 && ev.event_id.as_deref() == Some(redacts)
536 {
537 ev.redact(&redaction_rules);
538 } else if let Some(ev) = &mut self.name
539 && ev.event_id.as_deref() == Some(redacts)
540 {
541 ev.redact(&redaction_rules);
542 } else if let Some(ev) = &mut self.retention
543 && ev.event_id.as_deref() == Some(redacts)
544 {
545 ev.redact(&redaction_rules);
546 } else if let Some(ev) = &mut self.tombstone
547 && ev.event_id.as_deref() == Some(redacts)
548 {
549 ev.redact(&redaction_rules);
550 } else if let Some(ev) = &mut self.topic
551 && ev.event_id.as_deref() == Some(redacts)
552 {
553 ev.redact(&redaction_rules);
554 } else {
555 self.rtc_member_events
556 .retain(|_, member_event| member_event.event_id.as_deref() != Some(redacts));
557 }
558 }
559
560 pub fn handle_notable_tags(&mut self, tags: &Tags) {
561 let mut notable_tags = RoomNotableTags::empty();
562
563 if tags.contains_key(&TagName::Favorite) {
564 notable_tags.insert(RoomNotableTags::FAVOURITE);
565 }
566
567 if tags.contains_key(&TagName::LowPriority) {
568 notable_tags.insert(RoomNotableTags::LOW_PRIORITY);
569 }
570
571 self.notable_tags = notable_tags;
572 }
573}
574
575impl Default for BaseRoomInfo {
576 fn default() -> Self {
577 Self {
578 avatar: None,
579 canonical_alias: None,
580 create: None,
581 dm_targets: Default::default(),
582 member_hints: None,
583 encryption: None,
584 guest_access: None,
585 history_visibility: None,
586 join_rules: None,
587 max_power_level: DEFAULT_MAX_POWER_LEVEL,
588 name: None,
589 retention: None,
590 tombstone: None,
591 topic: None,
592 rtc_member_events: BTreeMap::new(),
593 is_marked_unread: false,
594 is_marked_unread_source: AccountDataSource::Unstable,
595 notable_tags: RoomNotableTags::empty(),
596 fully_read_event_id: None,
597 pinned_events: None,
598 }
599 }
600}
601
602#[derive(Clone, Debug, Serialize, Deserialize)]
606pub struct RoomInfo {
607 #[serde(default, alias = "version")]
610 pub(crate) data_format_version: u8,
611
612 pub(crate) room_id: OwnedRoomId,
614
615 pub(crate) room_state: RoomState,
617
618 pub(crate) notification_counts: UnreadNotificationsCount,
623
624 pub(crate) summary: RoomSummary,
626
627 pub(crate) members_synced: bool,
629
630 pub(crate) last_prev_batch: Option<String>,
632
633 pub(crate) sync_info: SyncInfo,
635
636 pub(crate) encryption_state_synced: bool,
638
639 #[serde(default)]
641 pub(crate) latest_event_value: LatestEventValue,
642
643 #[serde(default)]
645 pub(crate) read_receipts: ReadReceipts,
646
647 pub(crate) base_info: Box<BaseRoomInfo>,
650
651 #[serde(skip)]
655 pub(crate) warned_about_unknown_room_version_rules: Arc<AtomicBool>,
656
657 #[serde(default, skip_serializing_if = "Option::is_none")]
662 pub(crate) cached_display_name: Option<RoomDisplayName>,
663
664 #[serde(default, skip_serializing_if = "Option::is_none")]
666 pub(crate) cached_user_defined_notification_mode: Option<RoomNotificationMode>,
667
668 #[serde(default)]
685 pub(crate) recency_stamp: Option<RoomRecencyStamp>,
686}
687
688impl RoomInfo {
689 #[doc(hidden)] pub fn new(room_id: &RoomId, room_state: RoomState) -> Self {
691 Self {
692 data_format_version: 1,
693 room_id: room_id.into(),
694 room_state,
695 notification_counts: Default::default(),
696 summary: Default::default(),
697 members_synced: false,
698 last_prev_batch: None,
699 sync_info: SyncInfo::NoState,
700 encryption_state_synced: false,
701 latest_event_value: LatestEventValue::default(),
702 read_receipts: Default::default(),
703 base_info: Box::new(BaseRoomInfo::new()),
704 warned_about_unknown_room_version_rules: Arc::new(false.into()),
705 cached_display_name: None,
706 cached_user_defined_notification_mode: None,
707 recency_stamp: None,
708 }
709 }
710
711 pub fn mark_as_joined(&mut self) {
713 self.set_state(RoomState::Joined);
714 }
715
716 pub fn mark_as_left(&mut self) {
718 self.set_state(RoomState::Left);
719 }
720
721 pub fn mark_as_invited(&mut self) {
723 self.set_state(RoomState::Invited);
724 }
725
726 pub fn mark_as_knocked(&mut self) {
728 self.set_state(RoomState::Knocked);
729 }
730
731 pub fn mark_as_banned(&mut self) {
733 self.set_state(RoomState::Banned);
734 }
735
736 pub fn set_state(&mut self, room_state: RoomState) {
738 self.room_state = room_state;
739 }
740
741 pub fn mark_members_synced(&mut self) {
743 self.members_synced = true;
744 }
745
746 pub fn mark_members_missing(&mut self) {
748 self.members_synced = false;
749 }
750
751 pub fn are_members_synced(&self) -> bool {
753 self.members_synced
754 }
755
756 pub fn mark_state_partially_synced(&mut self) {
758 self.sync_info = SyncInfo::PartiallySynced;
759 }
760
761 pub fn mark_state_fully_synced(&mut self) {
763 self.sync_info = SyncInfo::FullySynced;
764 }
765
766 pub fn mark_state_not_synced(&mut self) {
768 self.sync_info = SyncInfo::NoState;
769 }
770
771 pub fn mark_encryption_state_synced(&mut self) {
773 self.encryption_state_synced = true;
774 }
775
776 pub fn mark_encryption_state_missing(&mut self) {
778 self.encryption_state_synced = false;
779 }
780
781 pub fn set_prev_batch(&mut self, prev_batch: Option<&str>) -> bool {
785 if self.last_prev_batch.as_deref() != prev_batch {
786 self.last_prev_batch = prev_batch.map(|p| p.to_owned());
787 true
788 } else {
789 false
790 }
791 }
792
793 pub fn state(&self) -> RoomState {
795 self.room_state
796 }
797
798 #[cfg(not(feature = "experimental-encrypted-state-events"))]
800 pub fn encryption_state(&self) -> EncryptionState {
801 if !self.encryption_state_synced {
802 EncryptionState::Unknown
803 } else if self.base_info.encryption.is_some() {
804 EncryptionState::Encrypted
805 } else {
806 EncryptionState::NotEncrypted
807 }
808 }
809
810 #[cfg(feature = "experimental-encrypted-state-events")]
812 pub fn encryption_state(&self) -> EncryptionState {
813 if !self.encryption_state_synced {
814 EncryptionState::Unknown
815 } else {
816 self.base_info
817 .encryption
818 .as_ref()
819 .map(|state| {
820 if state.encrypt_state_events {
821 EncryptionState::StateEncrypted
822 } else {
823 EncryptionState::Encrypted
824 }
825 })
826 .unwrap_or(EncryptionState::NotEncrypted)
827 }
828 }
829
830 pub fn set_encryption_event(
832 &mut self,
833 event: Option<PossiblyRedactedRoomEncryptionEventContent>,
834 ) {
835 self.base_info.encryption = event;
836 }
837
838 pub fn handle_encryption_state(
840 &mut self,
841 requested_required_states: &[(StateEventType, String)],
842 ) {
843 if requested_required_states
844 .iter()
845 .any(|(state_event, _)| state_event == &StateEventType::RoomEncryption)
846 {
847 self.mark_encryption_state_synced();
854 }
855 }
856
857 pub fn handle_state_event(
861 &mut self,
862 raw_event: &mut RawStateEventWithKeys<AnySyncStateEvent>,
863 ) -> bool {
864 if raw_event.event_type == StateEventType::MemberHints
866 && let Some(AnySyncStateEvent::MemberHints(new_hints)) = raw_event.deserialize()
867 && let (Some(current_hints), Some(new)) =
869 (&self.base_info.member_hints, new_hints.as_original())
870 && current_hints
872 .content
873 .service_members
874 .as_ref()
875 .is_some_and(|current_members| *current_members != new.content.service_members)
876 {
877 self.summary.active_service_members = None;
879 }
880
881 let base_info_has_been_modified = self.base_info.handle_state_event(raw_event);
883
884 if raw_event.event_type == StateEventType::RoomEncryption && raw_event.state_key.is_empty()
885 {
886 self.mark_encryption_state_synced();
892 }
893
894 base_info_has_been_modified
895 }
896
897 pub fn handle_stripped_state_event(
901 &mut self,
902 raw_event: &mut RawStateEventWithKeys<AnyStrippedStateEvent>,
903 ) -> bool {
904 self.base_info.handle_state_event(raw_event)
905 }
906
907 #[instrument(skip_all, fields(redacts))]
909 pub fn handle_redaction(
910 &mut self,
911 event: &SyncRoomRedactionEvent,
912 _raw: &Raw<SyncRoomRedactionEvent>,
913 ) {
914 let redaction_rules = self.room_version_rules_or_default().redaction;
915
916 let Some(redacts) = event.redacts(&redaction_rules) else {
917 info!("Can't apply redaction, redacts field is missing");
918 return;
919 };
920 tracing::Span::current().record("redacts", debug(redacts));
921
922 self.base_info.handle_redaction(redacts);
923 }
924
925 pub fn avatar_url(&self) -> Option<&MxcUri> {
927 self.base_info.avatar.as_ref().and_then(|e| e.content.url.as_deref())
928 }
929
930 pub fn update_avatar(&mut self, url: Option<OwnedMxcUri>) {
932 self.base_info.avatar = url.map(|url| {
933 let mut content = PossiblyRedactedRoomAvatarEventContent::new();
934 content.url = Some(url);
935
936 MinimalStateEvent { content, event_id: None }
937 });
938 }
939
940 pub fn avatar_info(&self) -> Option<&avatar::ImageInfo> {
942 self.base_info.avatar.as_ref().and_then(|e| e.content.info.as_deref())
943 }
944
945 pub fn update_notification_count(&mut self, notification_counts: UnreadNotificationsCount) {
947 self.notification_counts = notification_counts;
948 }
949
950 pub fn update_from_ruma_summary(&mut self, summary: &RumaSummary) -> bool {
954 let mut changed = false;
955
956 if !summary.is_empty() {
957 if !summary.heroes.is_empty() {
958 self.summary.room_heroes = summary
959 .heroes
960 .iter()
961 .map(|hero_id| RoomHero {
962 user_id: hero_id.to_owned(),
963 display_name: None,
964 avatar_url: None,
965 })
966 .collect();
967
968 changed = true;
969 }
970
971 if let Some(joined) = summary.joined_member_count {
972 self.summary.joined_member_count = joined.into();
973 changed = true;
974 }
975
976 if let Some(invited) = summary.invited_member_count {
977 self.summary.invited_member_count = invited.into();
978 changed = true;
979 }
980 }
981
982 if changed {
983 self.summary.active_service_members = None;
984 }
985
986 changed
987 }
988
989 pub(crate) fn update_joined_member_count(&mut self, count: u64) {
991 self.summary.joined_member_count = count;
992 }
993
994 pub(crate) fn update_invited_member_count(&mut self, count: u64) {
996 self.summary.invited_member_count = count;
997 }
998
999 pub(crate) fn update_heroes(&mut self, heroes: Vec<RoomHero>) {
1001 self.summary.room_heroes = heroes;
1002 }
1003
1004 pub fn heroes(&self) -> &[RoomHero] {
1006 &self.summary.room_heroes
1007 }
1008
1009 pub fn active_members_count(&self) -> u64 {
1013 self.summary.joined_member_count.saturating_add(self.summary.invited_member_count)
1014 }
1015
1016 pub fn invited_members_count(&self) -> u64 {
1018 self.summary.invited_member_count
1019 }
1020
1021 pub fn joined_members_count(&self) -> u64 {
1023 self.summary.joined_member_count
1024 }
1025
1026 pub fn canonical_alias(&self) -> Option<&RoomAliasId> {
1028 self.base_info.canonical_alias.as_ref()?.content.alias.as_deref()
1029 }
1030
1031 pub fn alt_aliases(&self) -> &[OwnedRoomAliasId] {
1033 self.base_info
1034 .canonical_alias
1035 .as_ref()
1036 .map(|ev| ev.content.alt_aliases.as_ref())
1037 .unwrap_or_default()
1038 }
1039
1040 pub fn room_id(&self) -> &RoomId {
1042 &self.room_id
1043 }
1044
1045 pub fn room_version(&self) -> Option<&RoomVersionId> {
1047 self.base_info.room_version()
1048 }
1049
1050 pub fn room_version_rules_or_default(&self) -> RoomVersionRules {
1055 use std::sync::atomic::Ordering;
1056
1057 self.base_info.room_version().and_then(|room_version| room_version.rules()).unwrap_or_else(
1058 || {
1059 if self
1060 .warned_about_unknown_room_version_rules
1061 .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
1062 .is_ok()
1063 {
1064 warn!("Unable to get the room version rules, defaulting to rules for room version {ROOM_VERSION_FALLBACK}");
1065 }
1066
1067 ROOM_VERSION_RULES_FALLBACK
1068 },
1069 )
1070 }
1071
1072 pub fn room_type(&self) -> Option<&RoomType> {
1074 self.base_info.create.as_ref()?.content.room_type.as_ref()
1075 }
1076
1077 pub fn creators(&self) -> Option<Vec<OwnedUserId>> {
1079 Some(self.base_info.create.as_ref()?.content.creators())
1080 }
1081
1082 pub(super) fn guest_access(&self) -> &GuestAccess {
1083 self.base_info
1084 .guest_access
1085 .as_ref()
1086 .and_then(|event| event.content.guest_access.as_ref())
1087 .unwrap_or(&GuestAccess::Forbidden)
1088 }
1089
1090 pub fn history_visibility(&self) -> Option<&HistoryVisibility> {
1094 Some(&self.base_info.history_visibility.as_ref()?.content.history_visibility)
1095 }
1096
1097 pub fn history_visibility_or_default(&self) -> &HistoryVisibility {
1104 self.history_visibility().unwrap_or(&HistoryVisibility::Shared)
1105 }
1106
1107 pub fn retention(&self) -> Option<&RoomRetentionEventContent> {
1111 self.base_info.retention.as_ref().map(|e| &e.content)
1112 }
1113
1114 pub fn join_rule(&self) -> Option<&JoinRule> {
1117 Some(&self.base_info.join_rules.as_ref()?.content.join_rule)
1118 }
1119
1120 pub fn service_members(&self) -> Option<&BTreeSet<OwnedUserId>> {
1123 self.base_info.member_hints.as_ref()?.content.service_members.as_ref()
1124 }
1125
1126 pub fn name(&self) -> Option<&str> {
1128 self.base_info.name.as_ref()?.content.name.as_deref().filter(|name| !name.is_empty())
1129 }
1130
1131 pub fn create(&self) -> Option<&RoomCreateWithCreatorEventContent> {
1133 Some(&self.base_info.create.as_ref()?.content)
1134 }
1135
1136 pub fn tombstone(&self) -> Option<&PossiblyRedactedRoomTombstoneEventContent> {
1138 Some(&self.base_info.tombstone.as_ref()?.content)
1139 }
1140
1141 pub fn topic(&self) -> Option<&str> {
1143 self.base_info.topic.as_ref()?.content.topic.as_deref()
1144 }
1145
1146 fn active_matrix_rtc_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
1151 let mut v = self
1152 .base_info
1153 .rtc_member_events
1154 .iter()
1155 .flat_map(|(state_key, ev)| {
1156 ev.content.active_memberships(None).into_iter().map(move |m| (state_key.clone(), m))
1157 })
1158 .collect::<Vec<_>>();
1159 v.sort_by_key(|(_, m)| m.created_ts());
1160 v
1161 }
1162
1163 fn active_room_call_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
1169 self.active_matrix_rtc_memberships()
1170 .into_iter()
1171 .filter(|(_user_id, m)| m.is_room_call())
1172 .collect()
1173 }
1174
1175 pub fn has_active_room_call(&self) -> bool {
1178 !self.active_room_call_memberships().is_empty()
1179 }
1180
1181 pub fn is_device_in_active_room_call(
1188 &self,
1189 user_id: &ruma::UserId,
1190 device_id: &ruma::DeviceId,
1191 ) -> bool {
1192 self.active_room_call_memberships().iter().any(|(state_key, membership)| {
1193 state_key.user_id() == user_id && membership.device_id() == device_id
1194 })
1195 }
1196
1197 pub fn active_room_call_consensus_intent(&self) -> CallIntentConsensus {
1214 let memberships = self.active_room_call_memberships();
1215 let total_count: u64 = memberships.len() as u64;
1216
1217 if total_count == 0 {
1218 return CallIntentConsensus::None;
1219 }
1220
1221 let mut consensus_intent: Option<CallIntent> = None;
1223 let mut agreeing_count: u64 = 0;
1224
1225 for (_, data) in memberships.iter() {
1226 if let Some(intent) = data.call_intent() {
1227 match &consensus_intent {
1228 None => {
1230 consensus_intent = Some(intent.clone());
1231 agreeing_count = 1;
1232 }
1233 Some(current) if current == intent => {
1235 agreeing_count += 1;
1236 }
1237 Some(_) => return CallIntentConsensus::None,
1239 }
1240 }
1241 }
1242
1243 match consensus_intent {
1245 None => CallIntentConsensus::None,
1246 Some(intent) if agreeing_count == total_count => {
1247 CallIntentConsensus::Full(intent)
1249 }
1250 Some(intent) => {
1251 CallIntentConsensus::Partial { intent, agreeing_count, total_count }
1253 }
1254 }
1255 }
1256
1257 pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
1266 self.active_room_call_memberships()
1267 .iter()
1268 .map(|(call_member_state_key, _)| call_member_state_key.user_id().to_owned())
1269 .collect()
1270 }
1271
1272 pub fn set_latest_event(&mut self, new_value: LatestEventValue) {
1274 self.latest_event_value = new_value;
1275 }
1276
1277 pub fn update_recency_stamp(&mut self, stamp: RoomRecencyStamp) {
1281 self.recency_stamp = Some(stamp);
1282 }
1283
1284 pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
1286 self.base_info.pinned_events.clone().and_then(|c| c.pinned)
1287 }
1288
1289 pub fn fully_read_event_id(&self) -> Option<&EventId> {
1292 self.base_info.fully_read_event_id.as_deref()
1293 }
1294
1295 pub fn is_pinned_event(&self, event_id: &EventId) -> bool {
1300 self.base_info
1301 .pinned_events
1302 .as_ref()
1303 .and_then(|content| content.pinned.as_deref())
1304 .is_some_and(|pinned| pinned.contains(&event_id.to_owned()))
1305 }
1306
1307 pub fn read_receipts(&self) -> &ReadReceipts {
1309 &self.read_receipts
1310 }
1311
1312 pub fn set_read_receipts(&mut self, read_receipts: ReadReceipts) {
1314 self.read_receipts = read_receipts;
1315 }
1316
1317 #[instrument(skip_all, fields(room_id = ?self.room_id))]
1325 pub(crate) async fn apply_migrations(&mut self, store: SaveLockedStateStore) -> bool {
1326 let mut migrated = false;
1327
1328 if self.data_format_version < 1 {
1329 info!("Migrating room info to version 1");
1330
1331 match store.get_room_account_data_event_static::<TagEventContent>(&self.room_id).await {
1333 Ok(Some(raw_event)) => match raw_event.deserialize() {
1335 Ok(event) => {
1336 self.base_info.handle_notable_tags(&event.content.tags);
1337 }
1338 Err(error) => {
1339 warn!("Failed to deserialize room tags: {error}");
1340 }
1341 },
1342 Ok(_) => {
1343 }
1345 Err(error) => {
1346 warn!("Failed to load room tags: {error}");
1347 }
1348 }
1349
1350 match store.get_state_event_static::<RoomPinnedEventsEventContent>(&self.room_id).await
1352 {
1353 Ok(Some(RawSyncOrStrippedState::Sync(raw_event))) => {
1355 if let Some(mut raw_event) =
1356 RawStateEventWithKeys::try_from_raw_state_event(raw_event.cast())
1357 {
1358 self.handle_state_event(&mut raw_event);
1359 }
1360 }
1361 Ok(_) => {
1362 }
1364 Err(error) => {
1365 warn!("Failed to load room pinned events: {error}");
1366 }
1367 }
1368
1369 self.data_format_version = 1;
1370 migrated = true;
1371 }
1372
1373 migrated
1374 }
1375
1376 pub fn active_service_member_count(&self) -> Option<u64> {
1379 self.summary.active_service_members
1380 }
1381
1382 pub fn update_active_service_member_count(&mut self, count: Option<u64>) {
1385 self.summary.active_service_members = count;
1386 }
1387}
1388
1389#[repr(transparent)]
1391#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
1392#[serde(transparent)]
1393pub struct RoomRecencyStamp(u64);
1394
1395impl From<u64> for RoomRecencyStamp {
1396 fn from(value: u64) -> Self {
1397 Self(value)
1398 }
1399}
1400
1401impl From<RoomRecencyStamp> for u64 {
1402 fn from(value: RoomRecencyStamp) -> Self {
1403 value.0
1404 }
1405}
1406
1407#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1408pub(crate) enum SyncInfo {
1409 NoState,
1415
1416 PartiallySynced,
1419
1420 FullySynced,
1422}
1423
1424pub fn apply_redaction(
1427 event: &Raw<AnySyncTimelineEvent>,
1428 raw_redaction: &Raw<SyncRoomRedactionEvent>,
1429 rules: &RedactionRules,
1430) -> Option<Raw<AnySyncTimelineEvent>> {
1431 use ruma::canonical_json::{RedactedBecause, redact_in_place};
1432
1433 let mut event_json = match event.deserialize_as() {
1434 Ok(json) => json,
1435 Err(e) => {
1436 warn!("Failed to deserialize latest event: {e}");
1437 return None;
1438 }
1439 };
1440
1441 let redacted_because = match RedactedBecause::from_raw_event(raw_redaction) {
1442 Ok(rb) => rb,
1443 Err(e) => {
1444 warn!("Redaction event is not valid canonical JSON: {e}");
1445 return None;
1446 }
1447 };
1448
1449 let redact_result = redact_in_place(&mut event_json, rules, Some(redacted_because));
1450
1451 if let Err(e) = redact_result {
1452 warn!("Failed to redact event: {e}");
1453 return None;
1454 }
1455
1456 let raw = Raw::new(&event_json).expect("CanonicalJsonObject must be serializable");
1457 Some(raw.cast_unchecked())
1458}
1459
1460#[derive(Debug, Clone)]
1470pub struct RoomInfoNotableUpdate {
1471 pub room_id: OwnedRoomId,
1473
1474 pub reasons: RoomInfoNotableUpdateReasons,
1476}
1477
1478bitflags! {
1479 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1481 pub struct RoomInfoNotableUpdateReasons: u16 {
1482 const RECENCY_STAMP = 0b0000_0000_0000_0001;
1484
1485 const LATEST_EVENT = 0b0000_0000_0000_0010;
1487
1488 const READ_RECEIPT = 0b0000_0000_0000_0100;
1490
1491 const UNREAD_MARKER = 0b0000_0000_0000_1000;
1493
1494 const MEMBERSHIP = 0b0000_0000_0001_0000;
1496
1497 const DISPLAY_NAME = 0b0000_0000_0010_0000;
1499
1500 const ACTIVE_SERVICE_MEMBERS = 0b0000_0000_0100_0000;
1502
1503 const NONE = 0b0000_0000_1000_0000;
1515
1516 const FULLY_READ = 0b0000_0001_0000_0000;
1518
1519 const HEROES = 0b0000_0010_0000_0000;
1521 }
1522}
1523
1524impl Default for RoomInfoNotableUpdateReasons {
1525 fn default() -> Self {
1526 Self::empty()
1527 }
1528}
1529
1530#[cfg(test)]
1531mod tests {
1532 use std::{collections::BTreeSet, str::FromStr, sync::Arc, time::Duration};
1533
1534 use assert_matches::assert_matches;
1535 use futures_util::future::{self, Either};
1536 #[cfg(all(target_family = "wasm", target_os = "unknown"))]
1537 use gloo_timers::future::sleep;
1538 use matrix_sdk_common::executor::spawn;
1539 use matrix_sdk_test::{async_test, event_factory::EventFactory};
1540 use ruma::{
1541 assign,
1542 events::{
1543 AnyRoomAccountDataEvent,
1544 room::{
1545 pinned_events::RoomPinnedEventsEventContent, retention::RoomRetentionEventContent,
1546 },
1547 tag::{TagInfo, TagName, Tags, UserTagName},
1548 },
1549 owned_event_id, owned_mxc_uri, owned_user_id, room_id,
1550 serde::Raw,
1551 user_id,
1552 };
1553 use serde_json::json;
1554 use similar_asserts::assert_eq;
1555 use tokio::sync::Mutex;
1556 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
1557 use tokio::time::sleep;
1558
1559 use super::{BaseRoomInfo, LatestEventValue, RoomInfo, SyncInfo};
1560 use crate::{
1561 RawStateEventWithKeys, Room, RoomDisplayName, RoomHero, RoomInfoNotableUpdateReasons,
1562 RoomState, StateChanges, StateStore,
1563 notification_settings::RoomNotificationMode,
1564 room::{RoomNotableTags, RoomSummary},
1565 store::{IntoStateStore, MemoryStore, RoomLoadSettings, SaveLockedStateStore},
1566 sync::UnreadNotificationsCount,
1567 };
1568
1569 #[test]
1570 fn test_room_info_serialization() {
1571 let info = RoomInfo {
1575 data_format_version: 1,
1576 room_id: room_id!("!gda78o:server.tld").into(),
1577 room_state: RoomState::Invited,
1578 notification_counts: UnreadNotificationsCount {
1579 highlight_count: 1,
1580 notification_count: 2,
1581 },
1582 summary: RoomSummary {
1583 room_heroes: vec![RoomHero {
1584 user_id: owned_user_id!("@somebody:example.org"),
1585 display_name: None,
1586 avatar_url: None,
1587 }],
1588 joined_member_count: 5,
1589 invited_member_count: 0,
1590 active_service_members: None,
1591 },
1592 members_synced: true,
1593 last_prev_batch: Some("pb".to_owned()),
1594 sync_info: SyncInfo::FullySynced,
1595 encryption_state_synced: true,
1596 latest_event_value: LatestEventValue::None,
1597 base_info: Box::new(
1598 assign!(BaseRoomInfo::new(), { pinned_events: Some(RoomPinnedEventsEventContent::new(vec![owned_event_id!("$a")]).into()) }),
1599 ),
1600 read_receipts: Default::default(),
1601 warned_about_unknown_room_version_rules: Arc::new(false.into()),
1602 cached_display_name: None,
1603 cached_user_defined_notification_mode: None,
1604 recency_stamp: Some(42.into()),
1605 };
1606
1607 let info_json = json!({
1608 "data_format_version": 1,
1609 "room_id": "!gda78o:server.tld",
1610 "room_state": "Invited",
1611 "notification_counts": {
1612 "highlight_count": 1,
1613 "notification_count": 2,
1614 },
1615 "summary": {
1616 "room_heroes": [{
1617 "user_id": "@somebody:example.org",
1618 "display_name": null,
1619 "avatar_url": null
1620 }],
1621 "joined_member_count": 5,
1622 "invited_member_count": 0,
1623 },
1624 "members_synced": true,
1625 "last_prev_batch": "pb",
1626 "sync_info": "FullySynced",
1627 "encryption_state_synced": true,
1628 "latest_event_value": "None",
1629 "base_info": {
1630 "avatar": null,
1631 "canonical_alias": null,
1632 "create": null,
1633 "dm_targets": [],
1634 "encryption": null,
1635 "guest_access": null,
1636 "history_visibility": null,
1637 "is_marked_unread": false,
1638 "is_marked_unread_source": "Unstable",
1639 "join_rules": null,
1640 "max_power_level": 100,
1641 "member_hints": null,
1642 "name": null,
1643 "retention": null,
1644 "tombstone": null,
1645 "topic": null,
1646 "pinned_events": {
1647 "pinned": ["$a"]
1648 },
1649 },
1650 "read_receipts": {
1651 "num_unread": 0,
1652 "num_mentions": 0,
1653 "num_notifications": 0,
1654 "latest_active": null,
1655 "pending": {
1656 "items": [],
1657 "capacity": 10,
1658 },
1659 },
1660 "recency_stamp": 42,
1661 });
1662
1663 assert_eq!(serde_json::to_value(info).unwrap(), info_json);
1664 }
1665
1666 #[async_test]
1667 async fn test_room_info_migration_v1() {
1668 let store = SaveLockedStateStore::new(MemoryStore::new().into_state_store());
1669
1670 let room_info_json = json!({
1671 "room_id": "!gda78o:server.tld",
1672 "room_state": "Joined",
1673 "notification_counts": {
1674 "highlight_count": 1,
1675 "notification_count": 2,
1676 },
1677 "summary": {
1678 "room_heroes": [{
1679 "user_id": "@somebody:example.org",
1680 "display_name": null,
1681 "avatar_url": null
1682 }],
1683 "joined_member_count": 5,
1684 "invited_member_count": 0,
1685 },
1686 "members_synced": true,
1687 "last_prev_batch": "pb",
1688 "sync_info": "FullySynced",
1689 "encryption_state_synced": true,
1690 "latest_event": {
1691 "event": {
1692 "encryption_info": null,
1693 "event": {
1694 "sender": "@u:i.uk",
1695 },
1696 },
1697 },
1698 "base_info": {
1699 "avatar": null,
1700 "canonical_alias": null,
1701 "create": null,
1702 "dm_targets": [],
1703 "encryption": null,
1704 "guest_access": null,
1705 "history_visibility": null,
1706 "join_rules": null,
1707 "max_power_level": 100,
1708 "name": null,
1709 "tombstone": null,
1710 "topic": null,
1711 },
1712 "read_receipts": {
1713 "num_unread": 0,
1714 "num_mentions": 0,
1715 "num_notifications": 0,
1716 "latest_active": null,
1717 "pending": []
1718 },
1719 "recency_stamp": 42,
1720 });
1721 let mut room_info: RoomInfo = serde_json::from_value(room_info_json).unwrap();
1722
1723 assert_eq!(room_info.data_format_version, 0);
1724 assert!(room_info.base_info.notable_tags.is_empty());
1725 assert!(room_info.base_info.pinned_events.is_none());
1726
1727 assert!(room_info.apply_migrations(store.clone()).await);
1729
1730 assert_eq!(room_info.data_format_version, 1);
1731 assert!(room_info.base_info.notable_tags.is_empty());
1732 assert!(room_info.base_info.pinned_events.is_none());
1733
1734 assert!(!room_info.apply_migrations(store.clone()).await);
1736
1737 assert_eq!(room_info.data_format_version, 1);
1738 assert!(room_info.base_info.notable_tags.is_empty());
1739 assert!(room_info.base_info.pinned_events.is_none());
1740
1741 let mut changes = StateChanges::default();
1743
1744 let f = EventFactory::new().room(&room_info.room_id).sender(user_id!("@example:localhost"));
1745 let mut tags = Tags::new();
1746 tags.insert(TagName::Favorite, TagInfo::new());
1747 tags.insert(TagName::User(UserTagName::from_str("u.work").unwrap()), TagInfo::new());
1748 let raw_tag_event: Raw<AnyRoomAccountDataEvent> = f.tag(tags).into();
1749 let tag_event = raw_tag_event.deserialize().unwrap();
1750 changes.add_room_account_data(&room_info.room_id, tag_event, raw_tag_event);
1751
1752 let raw_pinned_events_event: Raw<_> = f
1753 .room_pinned_events(vec![owned_event_id!("$a"), owned_event_id!("$b")])
1754 .into_raw_sync_state();
1755 let pinned_events_event = raw_pinned_events_event.deserialize().unwrap();
1756 changes.add_state_event(&room_info.room_id, pinned_events_event, raw_pinned_events_event);
1757
1758 store.save_changes(&changes).await.unwrap();
1759
1760 room_info.data_format_version = 0;
1762 assert!(room_info.apply_migrations(store.clone()).await);
1763
1764 assert_eq!(room_info.data_format_version, 1);
1765 assert!(room_info.base_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
1766 assert!(room_info.base_info.pinned_events.is_some());
1767
1768 let new_room_info = RoomInfo::new(room_id!("!new_room:localhost"), RoomState::Joined);
1770 assert_eq!(new_room_info.data_format_version, 1);
1771 }
1772
1773 #[test]
1774 fn test_room_info_deserialization() {
1775 let info_json = json!({
1776 "room_id": "!gda78o:server.tld",
1777 "room_state": "Joined",
1778 "notification_counts": {
1779 "highlight_count": 1,
1780 "notification_count": 2,
1781 },
1782 "summary": {
1783 "room_heroes": [{
1784 "user_id": "@somebody:example.org",
1785 "display_name": "Somebody",
1786 "avatar_url": "mxc://example.org/abc"
1787 }],
1788 "joined_member_count": 5,
1789 "invited_member_count": 0,
1790 },
1791 "members_synced": true,
1792 "last_prev_batch": "pb",
1793 "sync_info": "FullySynced",
1794 "encryption_state_synced": true,
1795 "base_info": {
1796 "avatar": null,
1797 "canonical_alias": null,
1798 "create": null,
1799 "dm_targets": [],
1800 "encryption": null,
1801 "guest_access": null,
1802 "history_visibility": null,
1803 "join_rules": null,
1804 "max_power_level": 100,
1805 "member_hints": null,
1806 "name": null,
1807 "tombstone": null,
1808 "topic": null,
1809 },
1810 "cached_display_name": { "Calculated": "lol" },
1811 "cached_user_defined_notification_mode": "Mute",
1812 "recency_stamp": 42,
1813 });
1814
1815 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1816
1817 assert_eq!(info.room_id, "!gda78o:server.tld");
1818 assert_eq!(info.room_state, RoomState::Joined);
1819 assert_eq!(info.notification_counts.highlight_count, 1);
1820 assert_eq!(info.notification_counts.notification_count, 2);
1821 assert_eq!(
1822 info.summary.room_heroes,
1823 vec![RoomHero {
1824 user_id: owned_user_id!("@somebody:example.org"),
1825 display_name: Some("Somebody".to_owned()),
1826 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1827 }]
1828 );
1829 assert_eq!(info.summary.joined_member_count, 5);
1830 assert_eq!(info.summary.invited_member_count, 0);
1831 assert!(info.members_synced);
1832 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1833 assert_eq!(info.sync_info, SyncInfo::FullySynced);
1834 assert!(info.encryption_state_synced);
1835 assert_matches!(info.latest_event_value, LatestEventValue::None);
1836 assert!(info.base_info.avatar.is_none());
1837 assert!(info.base_info.canonical_alias.is_none());
1838 assert!(info.base_info.create.is_none());
1839 assert_eq!(info.base_info.dm_targets.len(), 0);
1840 assert!(info.base_info.encryption.is_none());
1841 assert!(info.base_info.guest_access.is_none());
1842 assert!(info.base_info.history_visibility.is_none());
1843 assert!(info.base_info.join_rules.is_none());
1844 assert_eq!(info.base_info.max_power_level, 100);
1845 assert!(info.base_info.member_hints.is_none());
1846 assert!(info.base_info.name.is_none());
1847 assert!(info.base_info.tombstone.is_none());
1848 assert!(info.base_info.topic.is_none());
1849
1850 assert_eq!(
1851 info.cached_display_name.as_ref(),
1852 Some(&RoomDisplayName::Calculated("lol".to_owned())),
1853 );
1854 assert_eq!(
1855 info.cached_user_defined_notification_mode.as_ref(),
1856 Some(&RoomNotificationMode::Mute)
1857 );
1858 assert_eq!(info.recency_stamp.as_ref(), Some(&42.into()));
1859 }
1860
1861 #[test]
1868 fn test_room_info_deserialization_without_optional_items() {
1869 let info_json = json!({
1872 "room_id": "!gda78o:server.tld",
1873 "room_state": "Invited",
1874 "notification_counts": {
1875 "highlight_count": 1,
1876 "notification_count": 2,
1877 },
1878 "summary": {
1879 "room_heroes": [{
1880 "user_id": "@somebody:example.org",
1881 "display_name": "Somebody",
1882 "avatar_url": "mxc://example.org/abc"
1883 }],
1884 "joined_member_count": 5,
1885 "invited_member_count": 0,
1886 },
1887 "members_synced": true,
1888 "last_prev_batch": "pb",
1889 "sync_info": "FullySynced",
1890 "encryption_state_synced": true,
1891 "base_info": {
1892 "avatar": null,
1893 "canonical_alias": null,
1894 "create": null,
1895 "dm_targets": [],
1896 "encryption": null,
1897 "guest_access": null,
1898 "history_visibility": null,
1899 "join_rules": null,
1900 "max_power_level": 100,
1901 "name": null,
1902 "tombstone": null,
1903 "topic": null,
1904 },
1905 });
1906
1907 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1908
1909 assert_eq!(info.room_id, "!gda78o:server.tld");
1910 assert_eq!(info.room_state, RoomState::Invited);
1911 assert_eq!(info.notification_counts.highlight_count, 1);
1912 assert_eq!(info.notification_counts.notification_count, 2);
1913 assert_eq!(
1914 info.summary.room_heroes,
1915 vec![RoomHero {
1916 user_id: owned_user_id!("@somebody:example.org"),
1917 display_name: Some("Somebody".to_owned()),
1918 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1919 }]
1920 );
1921 assert_eq!(info.summary.joined_member_count, 5);
1922 assert_eq!(info.summary.invited_member_count, 0);
1923 assert!(info.members_synced);
1924 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1925 assert_eq!(info.sync_info, SyncInfo::FullySynced);
1926 assert!(info.encryption_state_synced);
1927 assert!(info.base_info.avatar.is_none());
1928 assert!(info.base_info.canonical_alias.is_none());
1929 assert!(info.base_info.create.is_none());
1930 assert_eq!(info.base_info.dm_targets.len(), 0);
1931 assert!(info.base_info.encryption.is_none());
1932 assert!(info.base_info.guest_access.is_none());
1933 assert!(info.base_info.history_visibility.is_none());
1934 assert!(info.base_info.join_rules.is_none());
1935 assert_eq!(info.base_info.max_power_level, 100);
1936 assert!(info.base_info.name.is_none());
1937 assert!(info.base_info.tombstone.is_none());
1938 assert!(info.base_info.topic.is_none());
1939 }
1940
1941 #[test]
1942 fn test_member_hints_with_different_contents_reset_computed_value() {
1943 let expected = BTreeSet::from_iter([
1944 owned_user_id!("@alice:example.org"),
1945 owned_user_id!("@bob:example.org"),
1946 ]);
1947
1948 let info_json = json!({
1949 "room_id": "!gda78o:server.tld",
1950 "room_state": "Invited",
1951 "notification_counts": {
1952 "highlight_count": 1,
1953 "notification_count": 2,
1954 },
1955 "summary": {
1956 "room_heroes": [{
1957 "user_id": "@somebody:example.org",
1958 "display_name": "Somebody",
1959 "avatar_url": "mxc://example.org/abc"
1960 }],
1961 "joined_member_count": 5,
1962 "invited_member_count": 0,
1963 "active_service_members": 2,
1964 },
1965 "members_synced": true,
1966 "last_prev_batch": "pb",
1967 "sync_info": "FullySynced",
1968 "encryption_state_synced": true,
1969 "base_info": {
1970 "avatar": null,
1971 "canonical_alias": null,
1972 "create": null,
1973 "dm_targets": [],
1974 "encryption": null,
1975 "guest_access": null,
1976 "history_visibility": null,
1977 "join_rules": null,
1978 "max_power_level": 100,
1979 "member_hints": {
1980 "Original": {
1981 "content": {
1982 "service_members": ["@alice:example.org", "@bob:example.org"]
1983 }
1984 }
1985 },
1986 "name": null,
1987 "tombstone": null,
1988 "topic": null,
1989 },
1990 });
1991
1992 let info: RoomInfo = serde_json::from_value(info_json.clone()).unwrap();
1993 assert_eq!(info.base_info.member_hints.unwrap().content.service_members.unwrap(), expected);
1994 assert_eq!(info.summary.active_service_members, Some(2));
1995
1996 let mut info: RoomInfo = serde_json::from_value(info_json.clone()).unwrap();
1998 let mut raw_state_event_with_keys = RawStateEventWithKeys::try_from_raw_state_event(
1999 EventFactory::new()
2000 .sender(user_id!("@alice:example.org"))
2001 .member_hints(expected.clone())
2002 .into_raw_sync_state(),
2003 )
2004 .expect("Expected member hints event is created");
2005
2006 info.handle_state_event(&mut raw_state_event_with_keys);
2007
2008 assert_eq!(info.base_info.member_hints.unwrap().content.service_members.unwrap(), expected);
2010 assert_eq!(info.summary.active_service_members, Some(2));
2012
2013 let mut info: RoomInfo = serde_json::from_value(info_json).unwrap();
2015 let new_member_hints = BTreeSet::from_iter([owned_user_id!("@alice:example.org")]);
2016 let mut raw_state_event_with_keys = RawStateEventWithKeys::try_from_raw_state_event(
2017 EventFactory::new()
2018 .sender(user_id!("@alice:example.org"))
2019 .member_hints(new_member_hints.clone())
2020 .into_raw_sync_state(),
2021 )
2022 .expect("New member hints event is created");
2023
2024 info.handle_state_event(&mut raw_state_event_with_keys);
2025
2026 assert_eq!(
2028 info.base_info.member_hints.unwrap().content.service_members.unwrap(),
2029 new_member_hints
2030 );
2031 assert!(info.summary.active_service_members.is_none());
2033 }
2034
2035 fn make_room_and_state_store(room_state: RoomState) -> (Room, SaveLockedStateStore) {
2036 let state_store = SaveLockedStateStore::new(MemoryStore::new().into_state_store());
2037 let user_id = user_id!("@user:localhost");
2038 let room_id = room_id!("!room:localhost");
2039 let (sender, _) = tokio::sync::broadcast::channel(1);
2040 let room = Room::new(user_id, state_store.clone(), room_id, room_state, sender);
2041 (room, state_store)
2042 }
2043
2044 #[async_test]
2045 async fn test_update_room_info_only_updates_in_memory_room_info() {
2046 let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2047
2048 let before = room.clone_info();
2049 assert_eq!(before.state(), RoomState::Joined);
2050 room.update_room_info(|mut info| {
2051 info.mark_as_banned();
2052 (info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
2053 })
2054 .await;
2055 let after = room.clone_info();
2056 assert_eq!(after.state(), RoomState::Banned);
2057
2058 let infos = state_store
2059 .get_room_infos(&RoomLoadSettings::One(room.room_id.clone()))
2060 .await
2061 .expect("get room info");
2062 assert!(infos.is_empty());
2063 }
2064
2065 #[async_test]
2066 async fn test_update_room_info_with_store_guard_only_updates_in_memory_room_info() {
2067 let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2068
2069 let before = room.clone_info();
2070 assert_eq!(before.state(), RoomState::Joined);
2071 room.update_room_info_with_store_guard(&state_store.lock().lock().await, |mut info| {
2072 info.mark_as_banned();
2073 (info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
2074 })
2075 .expect("update room info");
2076 let after = room.clone_info();
2077 assert_eq!(after.state(), RoomState::Banned);
2078
2079 let infos = state_store
2080 .get_room_infos(&RoomLoadSettings::One(room.room_id.clone()))
2081 .await
2082 .expect("get room info");
2083 assert!(infos.is_empty());
2084 }
2085
2086 #[async_test]
2087 async fn test_update_room_info_only_accepts_guard_for_underlying_mutex() {
2088 let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2089
2090 room.update_room_info_with_store_guard(&state_store.lock().lock().await, |info| {
2091 (info, RoomInfoNotableUpdateReasons::NONE)
2092 })
2093 .expect("room accepts guard for underlying mutex");
2094
2095 let mutex = Mutex::new(());
2096 room.update_room_info_with_store_guard(&mutex.lock().await, |info| {
2097 (info, RoomInfoNotableUpdateReasons::NONE)
2098 })
2099 .expect_err("room does not accept guard for unknown mutex");
2100 }
2101
2102 #[async_test]
2103 async fn test_update_and_save_room_info_updates_room_info_in_memory_and_store() {
2104 let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2105
2106 let before = room.clone_info();
2107 assert_eq!(before.state(), RoomState::Joined);
2108 room.update_and_save_room_info(|mut info| {
2109 info.mark_as_banned();
2110 (info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
2111 })
2112 .await
2113 .expect("update and save room info");
2114 let after = room.clone_info();
2115 assert_eq!(after.state(), RoomState::Banned);
2116
2117 let infos = state_store
2118 .get_room_infos(&RoomLoadSettings::One(room.room_id.clone()))
2119 .await
2120 .expect("get room info");
2121 assert_eq!(infos.len(), 1);
2122 assert_matches!(infos.first(), Some(info) => {
2123 info.state() == RoomState::Banned
2124 });
2125 }
2126
2127 #[async_test]
2128 async fn test_update_and_save_room_info_with_store_guard_updates_room_info_in_memory_and_store()
2129 {
2130 let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2131
2132 let before = room.clone_info();
2133 assert_eq!(before.state(), RoomState::Joined);
2134 room.update_and_save_room_info_with_store_guard(
2135 &state_store.lock().lock().await,
2136 |mut info| {
2137 info.mark_as_banned();
2138 (info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
2139 },
2140 )
2141 .await
2142 .expect("update and save room info");
2143 let after = room.clone_info();
2144 assert_eq!(after.state(), RoomState::Banned);
2145
2146 let infos = state_store
2147 .get_room_infos(&RoomLoadSettings::One(room.room_id.clone()))
2148 .await
2149 .expect("get room info");
2150 assert_eq!(infos.len(), 1);
2151 assert_matches!(infos.first(), Some(info) => {
2152 info.state() == RoomState::Banned
2153 });
2154 }
2155
2156 #[async_test]
2157 async fn test_update_and_save_room_info_only_accepts_guard_for_underlying_mutex() {
2158 let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2159
2160 room.update_and_save_room_info_with_store_guard(&state_store.lock().lock().await, |info| {
2161 (info, RoomInfoNotableUpdateReasons::NONE)
2162 })
2163 .await
2164 .expect("room accepts guard for underlying mutex");
2165
2166 let mutex = Mutex::new(());
2167 room.update_and_save_room_info_with_store_guard(&mutex.lock().await, |info| {
2168 (info, RoomInfoNotableUpdateReasons::NONE)
2169 })
2170 .await
2171 .expect_err("room does not accept guard for unknown mutex");
2172 }
2173
2174 #[derive(Debug)]
2175 struct Elapsed;
2176
2177 async fn timeout<F: Future + Unpin>(duration: Duration, f: F) -> Result<F::Output, Elapsed> {
2178 #[cfg(all(target_family = "wasm", target_os = "unknown"))]
2179 {
2180 match future::select(sleep(duration), f).await {
2181 Either::Left(_) => return Err(Elapsed),
2182 Either::Right((output, _)) => Ok(output),
2183 }
2184 }
2185 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
2186 {
2187 tokio::time::timeout(duration, f).await.map_err(|_| Elapsed)
2188 }
2189 }
2190
2191 #[async_test]
2192 async fn test_update_room_info_waits_to_acquire_lock_before_updating_room_info() {
2193 let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2194
2195 let lock_task = spawn({
2197 let state_store = state_store.clone();
2198 async move {
2199 let lock = state_store.lock();
2200 let _guard = lock.lock().await;
2201 sleep(Duration::from_secs(5)).await;
2202 }
2203 });
2204
2205 let save_task = spawn(async move {
2207 room.update_room_info(|info| (info, RoomInfoNotableUpdateReasons::NONE)).await
2208 });
2209
2210 assert_matches!(future::select(lock_task, save_task).await, Either::Left((_, save_task)) => {
2213 timeout(Duration::from_millis(100), save_task)
2214 .await
2215 .expect("task completes before timeout")
2216 .expect("task completes successfully")
2217 });
2218 }
2219
2220 #[async_test]
2221 async fn test_update_and_save_room_info_waits_to_acquire_lock_before_updating_room_info() {
2222 let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2223
2224 let lock_task = spawn({
2226 let state_store = state_store.clone();
2227 async move {
2228 let lock = state_store.lock();
2229 let _guard = lock.lock().await;
2230 sleep(Duration::from_secs(5)).await;
2231 }
2232 });
2233
2234 let save_task = spawn(async move {
2236 room.update_and_save_room_info(|info| (info, RoomInfoNotableUpdateReasons::NONE)).await
2237 });
2238
2239 assert_matches!(future::select(lock_task, save_task).await, Either::Left((_, save_task)) => {
2242 timeout(Duration::from_millis(100), save_task)
2243 .await
2244 .expect("task completes before timeout")
2245 .expect("task completes successfully")
2246 .expect("update and save room info");
2247 });
2248 }
2249
2250 #[test]
2251 fn test_retention_stored_on_handle_state_event() {
2252 let mut info = RoomInfo::new(room_id!("!gda78o:server.tld"), RoomState::Joined);
2253 assert!(info.retention().is_none(), "retention should be absent before any event");
2254
2255 let max_lifetime = Duration::from_secs(86_400); let content = RoomRetentionEventContent::new().at_most(max_lifetime).unwrap();
2257
2258 let mut raw = RawStateEventWithKeys::try_from_raw_state_event(
2259 EventFactory::new()
2260 .sender(user_id!("@alice:example.org"))
2261 .event(content)
2262 .state_key("")
2263 .into_raw_sync_state(),
2264 )
2265 .expect("retention state event should be constructable");
2266
2267 info.handle_state_event(&mut raw);
2268
2269 let retention = info.retention().expect("retention should be set after event");
2270 assert_eq!(retention.max_lifetime(), Some(max_lifetime));
2271 assert!(retention.min_lifetime().is_none());
2272 }
2273}