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 tombstone::PossiblyRedactedRoomTombstoneEventContent,
51 topic::PossiblyRedactedRoomTopicEventContent,
52 },
53 rtc::notification::CallIntent,
54 tag::{TagEventContent, TagName, Tags},
55 },
56 room::RoomType,
57 room_version_rules::{RedactionRules, RoomVersionRules},
58 serde::Raw,
59};
60use serde::{Deserialize, Serialize};
61use tokio::sync::MutexGuard;
62use tracing::{field::debug, info, instrument, warn};
63
64use super::{
65 AccountDataSource, EncryptionState, Room, RoomCreateWithCreatorEventContent, RoomDisplayName,
66 RoomHero, RoomNotableTags, RoomState, RoomSummary,
67};
68use crate::{
69 MinimalStateEvent, StateChanges, StoreError,
70 deserialized_responses::RawSyncOrStrippedState,
71 latest_event::LatestEventValue,
72 notification_settings::RoomNotificationMode,
73 read_receipts::RoomReadReceipts,
74 room::call::CallIntentConsensus,
75 store::{IncorrectMutexGuardError, SaveLockedStateStore, StateStoreExt},
76 sync::UnreadNotificationsCount,
77 utils::{AnyStateEventEnum, RawStateEventWithKeys},
78};
79
80const DEFAULT_MAX_POWER_LEVEL: i64 = 100;
82
83impl Room {
84 pub fn subscribe_info(&self) -> Subscriber<RoomInfo> {
86 self.info.subscribe()
87 }
88
89 pub fn clone_info(&self) -> RoomInfo {
91 self.info.get()
92 }
93
94 pub async fn update_room_info<F>(&self, f: F)
98 where
99 F: FnOnce(RoomInfo) -> (RoomInfo, RoomInfoNotableUpdateReasons),
100 {
101 self.update_room_info_with_store_guard(&self.store.lock().lock().await, f)
102 .expect("should have correct mutex!")
103 }
104
105 pub fn update_room_info_with_store_guard<F>(
112 &self,
113 guard: &MutexGuard<'_, ()>,
114 f: F,
115 ) -> Result<(), IncorrectMutexGuardError>
116 where
117 F: FnOnce(RoomInfo) -> (RoomInfo, RoomInfoNotableUpdateReasons),
118 {
119 if !std::ptr::eq(MutexGuard::mutex(guard), self.store.lock()) {
120 return Err(IncorrectMutexGuardError);
121 }
122
123 let (info, mut reasons) = f(self.clone_info());
124 self.info.set(info);
125
126 if reasons.is_empty() {
127 reasons = RoomInfoNotableUpdateReasons::NONE;
131 }
132 let _ = self
133 .room_info_notable_update_sender
134 .send(RoomInfoNotableUpdate { room_id: self.room_id.clone(), reasons });
135
136 Ok(())
137 }
138
139 pub async fn update_and_save_room_info<F>(&self, f: F) -> Result<(), StoreError>
142 where
143 F: FnOnce(RoomInfo) -> (RoomInfo, RoomInfoNotableUpdateReasons),
144 {
145 self.update_and_save_room_info_with_store_guard(&self.store.lock().lock().await, f).await
146 }
147
148 pub async fn update_and_save_room_info_with_store_guard<F>(
155 &self,
156 guard: &MutexGuard<'_, ()>,
157 f: F,
158 ) -> Result<(), StoreError>
159 where
160 F: FnOnce(RoomInfo) -> (RoomInfo, RoomInfoNotableUpdateReasons),
161 {
162 let (info, reasons) = f(self.clone_info());
163 let mut changes = StateChanges::default();
164 changes.add_room(info.clone());
165 self.store.save_changes_with_guard(guard, &changes).await?;
166 self.update_room_info_with_store_guard(guard, |_| (info, reasons))?;
167 Ok(())
168 }
169}
170
171#[derive(Clone, Debug, Serialize, Deserialize)]
175pub struct BaseRoomInfo {
176 pub(crate) avatar: Option<MinimalStateEvent<PossiblyRedactedRoomAvatarEventContent>>,
178 pub(crate) canonical_alias:
180 Option<MinimalStateEvent<PossiblyRedactedRoomCanonicalAliasEventContent>>,
181 pub(crate) create: Option<MinimalStateEvent<RoomCreateWithCreatorEventContent>>,
183 pub(crate) dm_targets: HashSet<OwnedDirectUserIdentifier>,
186 pub(crate) encryption: Option<PossiblyRedactedRoomEncryptionEventContent>,
188 pub(crate) guest_access: Option<MinimalStateEvent<PossiblyRedactedRoomGuestAccessEventContent>>,
190 pub(crate) history_visibility:
192 Option<MinimalStateEvent<PossiblyRedactedRoomHistoryVisibilityEventContent>>,
193 pub(crate) join_rules: Option<MinimalStateEvent<PossiblyRedactedRoomJoinRulesEventContent>>,
195 pub(crate) max_power_level: i64,
197 pub(crate) member_hints: Option<MinimalStateEvent<PossiblyRedactedMemberHintsEventContent>>,
200 pub(crate) name: Option<MinimalStateEvent<PossiblyRedactedRoomNameEventContent>>,
202 pub(crate) tombstone: Option<MinimalStateEvent<PossiblyRedactedRoomTombstoneEventContent>>,
204 pub(crate) topic: Option<MinimalStateEvent<PossiblyRedactedRoomTopicEventContent>>,
206 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
209 pub(crate) rtc_member_events:
210 BTreeMap<CallMemberStateKey, MinimalStateEvent<PossiblyRedactedCallMemberEventContent>>,
211 #[serde(default)]
213 pub(crate) is_marked_unread: bool,
214 #[serde(default)]
216 pub(crate) is_marked_unread_source: AccountDataSource,
217 #[serde(skip_serializing_if = "RoomNotableTags::is_empty", default)]
222 pub(crate) notable_tags: RoomNotableTags,
223 #[serde(skip_serializing_if = "Option::is_none", default)]
225 pub(crate) fully_read_event_id: Option<OwnedEventId>,
226 pub(crate) pinned_events: Option<PossiblyRedactedRoomPinnedEventsEventContent>,
228}
229
230impl BaseRoomInfo {
231 pub fn new() -> Self {
233 Self::default()
234 }
235
236 pub fn room_version(&self) -> Option<&RoomVersionId> {
241 Some(&self.create.as_ref()?.content.room_version)
242 }
243
244 pub fn handle_state_event<T: AnyStateEventEnum>(
248 &mut self,
249 raw_event: &mut RawStateEventWithKeys<T>,
250 ) -> bool {
251 match (&raw_event.event_type, raw_event.state_key.as_str()) {
252 (StateEventType::RoomEncryption, "") => {
253 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
257 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomEncryption)
258 }) && event.content.algorithm.is_some()
259 {
260 self.encryption = Some(event.content);
261 true
262 } else {
263 false
264 }
265 }
266 (StateEventType::RoomAvatar, "") => {
267 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
268 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomAvatar)
269 }) {
270 self.avatar = Some(event);
271 true
272 } else {
273 self.avatar.take().is_some()
275 }
276 }
277 (StateEventType::RoomName, "") => {
278 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
279 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomName)
280 }) {
281 self.name = Some(event);
282 true
283 } else {
284 self.name.take().is_some()
286 }
287 }
288 (StateEventType::RoomCreate, "") if self.create.is_none() => {
290 if let Some(any_event) = raw_event.deserialize()
291 && let Some(content) = as_variant!(
292 any_event.get_content(),
293 AnyPossiblyRedactedStateEventContent::RoomCreate
294 )
295 {
296 self.create = Some(MinimalStateEvent {
297 content: RoomCreateWithCreatorEventContent::from_event_content(
298 content,
299 any_event.get_sender().to_owned(),
300 ),
301 event_id: any_event.get_event_id().map(ToOwned::to_owned),
302 });
303 true
304 } else {
305 false
306 }
307 }
308 (StateEventType::RoomHistoryVisibility, "") => {
309 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
310 as_variant!(
311 any_event,
312 AnyPossiblyRedactedStateEventContent::RoomHistoryVisibility
313 )
314 }) {
315 self.history_visibility = Some(event);
316 true
317 } else {
318 self.history_visibility.take().is_some()
320 }
321 }
322 (StateEventType::RoomGuestAccess, "") => {
323 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
324 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomGuestAccess)
325 }) {
326 self.guest_access = Some(event);
327 true
328 } else {
329 self.guest_access.take().is_some()
331 }
332 }
333 (StateEventType::MemberHints, "") => {
334 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
335 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::MemberHints)
336 }) {
337 self.member_hints = Some(event);
338 true
339 } else {
340 self.member_hints.take().is_some()
342 }
343 }
344 (StateEventType::RoomJoinRules, "") => {
345 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
346 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomJoinRules)
347 }) {
348 match &event.content.join_rule {
349 JoinRule::Invite
350 | JoinRule::Knock
351 | JoinRule::Private
352 | JoinRule::Restricted(_)
353 | JoinRule::KnockRestricted(_)
354 | JoinRule::Public => {
355 self.join_rules = Some(event);
356 true
357 }
358 r => {
359 warn!(join_rule = ?r.as_str(), "Encountered a custom join rule, skipping");
360 self.join_rules.take().is_some()
362 }
363 }
364 } else {
365 self.join_rules.take().is_some()
367 }
368 }
369 (StateEventType::RoomCanonicalAlias, "") => {
370 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
371 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomCanonicalAlias)
372 }) {
373 self.canonical_alias = Some(event);
374 true
375 } else {
376 self.canonical_alias.take().is_some()
378 }
379 }
380 (StateEventType::RoomTopic, "") => {
381 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
382 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomTopic)
383 }) {
384 self.topic = Some(event);
385 true
386 } else {
387 self.topic.take().is_some()
389 }
390 }
391 (StateEventType::RoomTombstone, "") => {
392 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
393 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomTombstone)
394 }) {
395 self.tombstone = Some(event);
396 true
397 } else {
398 self.tombstone.take().is_some()
400 }
401 }
402 (StateEventType::RoomPowerLevels, "") => {
403 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
404 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomPowerLevels)
405 }) {
406 let new_max = i64::from(
407 event
408 .content
409 .users
410 .values()
411 .fold(event.content.users_default, |max_pl, user_pl| {
412 max_pl.max(*user_pl)
413 }),
414 );
415
416 if self.max_power_level != new_max {
417 self.max_power_level = new_max;
418 true
419 } else {
420 false
421 }
422 } else if self.max_power_level != DEFAULT_MAX_POWER_LEVEL {
423 self.max_power_level = DEFAULT_MAX_POWER_LEVEL;
425 true
426 } else {
427 false
428 }
429 }
430 (StateEventType::CallMember, _) => {
431 if let Ok(call_member_key) = raw_event.state_key.parse::<CallMemberStateKey>() {
432 if let Some(any_event) = raw_event.deserialize()
433 && let Some(content) = as_variant!(
434 any_event.get_content(),
435 AnyPossiblyRedactedStateEventContent::CallMember
436 )
437 {
438 let mut event = MinimalStateEvent {
439 content,
440 event_id: any_event.get_event_id().map(ToOwned::to_owned),
441 };
442
443 if let Some(origin_server_ts) = any_event.get_origin_server_ts() {
444 event.content.set_created_ts_if_none(origin_server_ts);
445 }
446
447 self.rtc_member_events.insert(call_member_key, event);
449
450 self.rtc_member_events
452 .retain(|_, ev| !ev.content.active_memberships(None).is_empty());
453
454 true
455 } else {
456 self.rtc_member_events.remove(&call_member_key).is_some()
459 }
460 } else {
461 false
462 }
463 }
464 (StateEventType::RoomPinnedEvents, "") => {
465 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
466 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomPinnedEvents)
467 }) {
468 self.pinned_events = Some(event.content);
469 true
470 } else {
471 self.pinned_events.take().is_some()
473 }
474 }
475 _ => false,
476 }
477 }
478
479 pub(super) fn handle_redaction(&mut self, redacts: &EventId) {
480 let redaction_rules = self
481 .room_version()
482 .and_then(|room_version| room_version.rules())
483 .unwrap_or(ROOM_VERSION_RULES_FALLBACK)
484 .redaction;
485
486 if let Some(ev) = &mut self.avatar
487 && ev.event_id.as_deref() == Some(redacts)
488 {
489 ev.redact(&redaction_rules);
490 } else if let Some(ev) = &mut self.canonical_alias
491 && ev.event_id.as_deref() == Some(redacts)
492 {
493 ev.redact(&redaction_rules);
494 } else if let Some(ev) = &mut self.create
495 && ev.event_id.as_deref() == Some(redacts)
496 {
497 ev.redact(&redaction_rules);
498 } else if let Some(ev) = &mut self.guest_access
499 && ev.event_id.as_deref() == Some(redacts)
500 {
501 ev.redact(&redaction_rules);
502 } else if let Some(ev) = &mut self.history_visibility
503 && ev.event_id.as_deref() == Some(redacts)
504 {
505 ev.redact(&redaction_rules);
506 } else if let Some(ev) = &mut self.join_rules
507 && ev.event_id.as_deref() == Some(redacts)
508 {
509 ev.redact(&redaction_rules);
510 } else if let Some(ev) = &mut self.name
511 && ev.event_id.as_deref() == Some(redacts)
512 {
513 ev.redact(&redaction_rules);
514 } else if let Some(ev) = &mut self.tombstone
515 && ev.event_id.as_deref() == Some(redacts)
516 {
517 ev.redact(&redaction_rules);
518 } else if let Some(ev) = &mut self.topic
519 && ev.event_id.as_deref() == Some(redacts)
520 {
521 ev.redact(&redaction_rules);
522 } else {
523 self.rtc_member_events
524 .retain(|_, member_event| member_event.event_id.as_deref() != Some(redacts));
525 }
526 }
527
528 pub fn handle_notable_tags(&mut self, tags: &Tags) {
529 let mut notable_tags = RoomNotableTags::empty();
530
531 if tags.contains_key(&TagName::Favorite) {
532 notable_tags.insert(RoomNotableTags::FAVOURITE);
533 }
534
535 if tags.contains_key(&TagName::LowPriority) {
536 notable_tags.insert(RoomNotableTags::LOW_PRIORITY);
537 }
538
539 self.notable_tags = notable_tags;
540 }
541}
542
543impl Default for BaseRoomInfo {
544 fn default() -> Self {
545 Self {
546 avatar: None,
547 canonical_alias: None,
548 create: None,
549 dm_targets: Default::default(),
550 member_hints: None,
551 encryption: None,
552 guest_access: None,
553 history_visibility: None,
554 join_rules: None,
555 max_power_level: DEFAULT_MAX_POWER_LEVEL,
556 name: None,
557 tombstone: None,
558 topic: None,
559 rtc_member_events: BTreeMap::new(),
560 is_marked_unread: false,
561 is_marked_unread_source: AccountDataSource::Unstable,
562 notable_tags: RoomNotableTags::empty(),
563 fully_read_event_id: None,
564 pinned_events: None,
565 }
566 }
567}
568
569#[derive(Clone, Debug, Serialize, Deserialize)]
573pub struct RoomInfo {
574 #[serde(default, alias = "version")]
577 pub(crate) data_format_version: u8,
578
579 pub(crate) room_id: OwnedRoomId,
581
582 pub(crate) room_state: RoomState,
584
585 pub(crate) notification_counts: UnreadNotificationsCount,
590
591 pub(crate) summary: RoomSummary,
593
594 pub(crate) members_synced: bool,
596
597 pub(crate) last_prev_batch: Option<String>,
599
600 pub(crate) sync_info: SyncInfo,
602
603 pub(crate) encryption_state_synced: bool,
605
606 #[serde(default)]
608 pub(crate) latest_event_value: LatestEventValue,
609
610 #[serde(default)]
612 pub(crate) read_receipts: RoomReadReceipts,
613
614 pub(crate) base_info: Box<BaseRoomInfo>,
617
618 #[serde(skip)]
622 pub(crate) warned_about_unknown_room_version_rules: Arc<AtomicBool>,
623
624 #[serde(default, skip_serializing_if = "Option::is_none")]
629 pub(crate) cached_display_name: Option<RoomDisplayName>,
630
631 #[serde(default, skip_serializing_if = "Option::is_none")]
633 pub(crate) cached_user_defined_notification_mode: Option<RoomNotificationMode>,
634
635 #[serde(default)]
652 pub(crate) recency_stamp: Option<RoomRecencyStamp>,
653}
654
655impl RoomInfo {
656 #[doc(hidden)] pub fn new(room_id: &RoomId, room_state: RoomState) -> Self {
658 Self {
659 data_format_version: 1,
660 room_id: room_id.into(),
661 room_state,
662 notification_counts: Default::default(),
663 summary: Default::default(),
664 members_synced: false,
665 last_prev_batch: None,
666 sync_info: SyncInfo::NoState,
667 encryption_state_synced: false,
668 latest_event_value: LatestEventValue::default(),
669 read_receipts: Default::default(),
670 base_info: Box::new(BaseRoomInfo::new()),
671 warned_about_unknown_room_version_rules: Arc::new(false.into()),
672 cached_display_name: None,
673 cached_user_defined_notification_mode: None,
674 recency_stamp: None,
675 }
676 }
677
678 pub fn mark_as_joined(&mut self) {
680 self.set_state(RoomState::Joined);
681 }
682
683 pub fn mark_as_left(&mut self) {
685 self.set_state(RoomState::Left);
686 }
687
688 pub fn mark_as_invited(&mut self) {
690 self.set_state(RoomState::Invited);
691 }
692
693 pub fn mark_as_knocked(&mut self) {
695 self.set_state(RoomState::Knocked);
696 }
697
698 pub fn mark_as_banned(&mut self) {
700 self.set_state(RoomState::Banned);
701 }
702
703 pub fn set_state(&mut self, room_state: RoomState) {
705 self.room_state = room_state;
706 }
707
708 pub fn mark_members_synced(&mut self) {
710 self.members_synced = true;
711 }
712
713 pub fn mark_members_missing(&mut self) {
715 self.members_synced = false;
716 }
717
718 pub fn are_members_synced(&self) -> bool {
720 self.members_synced
721 }
722
723 pub fn mark_state_partially_synced(&mut self) {
725 self.sync_info = SyncInfo::PartiallySynced;
726 }
727
728 pub fn mark_state_fully_synced(&mut self) {
730 self.sync_info = SyncInfo::FullySynced;
731 }
732
733 pub fn mark_state_not_synced(&mut self) {
735 self.sync_info = SyncInfo::NoState;
736 }
737
738 pub fn mark_encryption_state_synced(&mut self) {
740 self.encryption_state_synced = true;
741 }
742
743 pub fn mark_encryption_state_missing(&mut self) {
745 self.encryption_state_synced = false;
746 }
747
748 pub fn set_prev_batch(&mut self, prev_batch: Option<&str>) -> bool {
752 if self.last_prev_batch.as_deref() != prev_batch {
753 self.last_prev_batch = prev_batch.map(|p| p.to_owned());
754 true
755 } else {
756 false
757 }
758 }
759
760 pub fn state(&self) -> RoomState {
762 self.room_state
763 }
764
765 #[cfg(not(feature = "experimental-encrypted-state-events"))]
767 pub fn encryption_state(&self) -> EncryptionState {
768 if !self.encryption_state_synced {
769 EncryptionState::Unknown
770 } else if self.base_info.encryption.is_some() {
771 EncryptionState::Encrypted
772 } else {
773 EncryptionState::NotEncrypted
774 }
775 }
776
777 #[cfg(feature = "experimental-encrypted-state-events")]
779 pub fn encryption_state(&self) -> EncryptionState {
780 if !self.encryption_state_synced {
781 EncryptionState::Unknown
782 } else {
783 self.base_info
784 .encryption
785 .as_ref()
786 .map(|state| {
787 if state.encrypt_state_events {
788 EncryptionState::StateEncrypted
789 } else {
790 EncryptionState::Encrypted
791 }
792 })
793 .unwrap_or(EncryptionState::NotEncrypted)
794 }
795 }
796
797 pub fn set_encryption_event(
799 &mut self,
800 event: Option<PossiblyRedactedRoomEncryptionEventContent>,
801 ) {
802 self.base_info.encryption = event;
803 }
804
805 pub fn handle_encryption_state(
807 &mut self,
808 requested_required_states: &[(StateEventType, String)],
809 ) {
810 if requested_required_states
811 .iter()
812 .any(|(state_event, _)| state_event == &StateEventType::RoomEncryption)
813 {
814 self.mark_encryption_state_synced();
820 }
821 }
822
823 pub fn handle_state_event(
827 &mut self,
828 raw_event: &mut RawStateEventWithKeys<AnySyncStateEvent>,
829 ) -> bool {
830 if raw_event.event_type == StateEventType::MemberHints
832 && let Some(AnySyncStateEvent::MemberHints(new_hints)) = raw_event.deserialize()
833 && let (Some(current_hints), Some(new)) =
835 (&self.base_info.member_hints, new_hints.as_original())
836 && current_hints
838 .content
839 .service_members
840 .as_ref()
841 .is_some_and(|current_members| *current_members != new.content.service_members)
842 {
843 self.summary.active_service_members = None;
845 }
846
847 let base_info_has_been_modified = self.base_info.handle_state_event(raw_event);
849
850 if raw_event.event_type == StateEventType::RoomEncryption && raw_event.state_key.is_empty()
851 {
852 self.mark_encryption_state_synced();
858 }
859
860 base_info_has_been_modified
861 }
862
863 pub fn handle_stripped_state_event(
867 &mut self,
868 raw_event: &mut RawStateEventWithKeys<AnyStrippedStateEvent>,
869 ) -> bool {
870 self.base_info.handle_state_event(raw_event)
871 }
872
873 #[instrument(skip_all, fields(redacts))]
875 pub fn handle_redaction(
876 &mut self,
877 event: &SyncRoomRedactionEvent,
878 _raw: &Raw<SyncRoomRedactionEvent>,
879 ) {
880 let redaction_rules = self.room_version_rules_or_default().redaction;
881
882 let Some(redacts) = event.redacts(&redaction_rules) else {
883 info!("Can't apply redaction, redacts field is missing");
884 return;
885 };
886 tracing::Span::current().record("redacts", debug(redacts));
887
888 self.base_info.handle_redaction(redacts);
889 }
890
891 pub fn avatar_url(&self) -> Option<&MxcUri> {
893 self.base_info.avatar.as_ref().and_then(|e| e.content.url.as_deref())
894 }
895
896 pub fn update_avatar(&mut self, url: Option<OwnedMxcUri>) {
898 self.base_info.avatar = url.map(|url| {
899 let mut content = PossiblyRedactedRoomAvatarEventContent::new();
900 content.url = Some(url);
901
902 MinimalStateEvent { content, event_id: None }
903 });
904 }
905
906 pub fn avatar_info(&self) -> Option<&avatar::ImageInfo> {
908 self.base_info.avatar.as_ref().and_then(|e| e.content.info.as_deref())
909 }
910
911 pub fn update_notification_count(&mut self, notification_counts: UnreadNotificationsCount) {
913 self.notification_counts = notification_counts;
914 }
915
916 pub fn update_from_ruma_summary(&mut self, summary: &RumaSummary) -> bool {
920 let mut changed = false;
921
922 if !summary.is_empty() {
923 if !summary.heroes.is_empty() {
924 self.summary.room_heroes = summary
925 .heroes
926 .iter()
927 .map(|hero_id| RoomHero {
928 user_id: hero_id.to_owned(),
929 display_name: None,
930 avatar_url: None,
931 })
932 .collect();
933
934 changed = true;
935 }
936
937 if let Some(joined) = summary.joined_member_count {
938 self.summary.joined_member_count = joined.into();
939 changed = true;
940 }
941
942 if let Some(invited) = summary.invited_member_count {
943 self.summary.invited_member_count = invited.into();
944 changed = true;
945 }
946 }
947
948 if changed {
949 self.summary.active_service_members = None;
950 }
951
952 changed
953 }
954
955 pub(crate) fn update_joined_member_count(&mut self, count: u64) {
957 self.summary.joined_member_count = count;
958 }
959
960 pub(crate) fn update_invited_member_count(&mut self, count: u64) {
962 self.summary.invited_member_count = count;
963 }
964
965 pub(crate) fn update_heroes(&mut self, heroes: Vec<RoomHero>) {
967 self.summary.room_heroes = heroes;
968 }
969
970 pub fn heroes(&self) -> &[RoomHero] {
972 &self.summary.room_heroes
973 }
974
975 pub fn active_members_count(&self) -> u64 {
979 self.summary.joined_member_count.saturating_add(self.summary.invited_member_count)
980 }
981
982 pub fn invited_members_count(&self) -> u64 {
984 self.summary.invited_member_count
985 }
986
987 pub fn joined_members_count(&self) -> u64 {
989 self.summary.joined_member_count
990 }
991
992 pub fn canonical_alias(&self) -> Option<&RoomAliasId> {
994 self.base_info.canonical_alias.as_ref()?.content.alias.as_deref()
995 }
996
997 pub fn alt_aliases(&self) -> &[OwnedRoomAliasId] {
999 self.base_info
1000 .canonical_alias
1001 .as_ref()
1002 .map(|ev| ev.content.alt_aliases.as_ref())
1003 .unwrap_or_default()
1004 }
1005
1006 pub fn room_id(&self) -> &RoomId {
1008 &self.room_id
1009 }
1010
1011 pub fn room_version(&self) -> Option<&RoomVersionId> {
1013 self.base_info.room_version()
1014 }
1015
1016 pub fn room_version_rules_or_default(&self) -> RoomVersionRules {
1021 use std::sync::atomic::Ordering;
1022
1023 self.base_info.room_version().and_then(|room_version| room_version.rules()).unwrap_or_else(
1024 || {
1025 if self
1026 .warned_about_unknown_room_version_rules
1027 .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
1028 .is_ok()
1029 {
1030 warn!("Unable to get the room version rules, defaulting to rules for room version {ROOM_VERSION_FALLBACK}");
1031 }
1032
1033 ROOM_VERSION_RULES_FALLBACK
1034 },
1035 )
1036 }
1037
1038 pub fn room_type(&self) -> Option<&RoomType> {
1040 self.base_info.create.as_ref()?.content.room_type.as_ref()
1041 }
1042
1043 pub fn creators(&self) -> Option<Vec<OwnedUserId>> {
1045 Some(self.base_info.create.as_ref()?.content.creators())
1046 }
1047
1048 pub(super) fn guest_access(&self) -> &GuestAccess {
1049 self.base_info
1050 .guest_access
1051 .as_ref()
1052 .and_then(|event| event.content.guest_access.as_ref())
1053 .unwrap_or(&GuestAccess::Forbidden)
1054 }
1055
1056 pub fn history_visibility(&self) -> Option<&HistoryVisibility> {
1060 Some(&self.base_info.history_visibility.as_ref()?.content.history_visibility)
1061 }
1062
1063 pub fn history_visibility_or_default(&self) -> &HistoryVisibility {
1070 self.history_visibility().unwrap_or(&HistoryVisibility::Shared)
1071 }
1072
1073 pub fn join_rule(&self) -> Option<&JoinRule> {
1076 Some(&self.base_info.join_rules.as_ref()?.content.join_rule)
1077 }
1078
1079 pub fn service_members(&self) -> Option<&BTreeSet<OwnedUserId>> {
1082 self.base_info.member_hints.as_ref()?.content.service_members.as_ref()
1083 }
1084
1085 pub fn name(&self) -> Option<&str> {
1087 self.base_info.name.as_ref()?.content.name.as_deref().filter(|name| !name.is_empty())
1088 }
1089
1090 pub fn create(&self) -> Option<&RoomCreateWithCreatorEventContent> {
1092 Some(&self.base_info.create.as_ref()?.content)
1093 }
1094
1095 pub fn tombstone(&self) -> Option<&PossiblyRedactedRoomTombstoneEventContent> {
1097 Some(&self.base_info.tombstone.as_ref()?.content)
1098 }
1099
1100 pub fn topic(&self) -> Option<&str> {
1102 self.base_info.topic.as_ref()?.content.topic.as_deref()
1103 }
1104
1105 fn active_matrix_rtc_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
1110 let mut v = self
1111 .base_info
1112 .rtc_member_events
1113 .iter()
1114 .flat_map(|(state_key, ev)| {
1115 ev.content.active_memberships(None).into_iter().map(move |m| (state_key.clone(), m))
1116 })
1117 .collect::<Vec<_>>();
1118 v.sort_by_key(|(_, m)| m.created_ts());
1119 v
1120 }
1121
1122 fn active_room_call_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
1128 self.active_matrix_rtc_memberships()
1129 .into_iter()
1130 .filter(|(_user_id, m)| m.is_room_call())
1131 .collect()
1132 }
1133
1134 pub fn has_active_room_call(&self) -> bool {
1137 !self.active_room_call_memberships().is_empty()
1138 }
1139
1140 pub fn is_device_in_active_room_call(
1147 &self,
1148 user_id: &ruma::UserId,
1149 device_id: &ruma::DeviceId,
1150 ) -> bool {
1151 self.active_room_call_memberships().iter().any(|(state_key, membership)| {
1152 state_key.user_id() == user_id && membership.device_id() == device_id
1153 })
1154 }
1155
1156 pub fn active_room_call_consensus_intent(&self) -> CallIntentConsensus {
1173 let memberships = self.active_room_call_memberships();
1174 let total_count: u64 = memberships.len() as u64;
1175
1176 if total_count == 0 {
1177 return CallIntentConsensus::None;
1178 }
1179
1180 let mut consensus_intent: Option<CallIntent> = None;
1182 let mut agreeing_count: u64 = 0;
1183
1184 for (_, data) in memberships.iter() {
1185 if let Some(intent) = data.call_intent() {
1186 match &consensus_intent {
1187 None => {
1189 consensus_intent = Some(intent.clone());
1190 agreeing_count = 1;
1191 }
1192 Some(current) if current == intent => {
1194 agreeing_count += 1;
1195 }
1196 Some(_) => return CallIntentConsensus::None,
1198 }
1199 }
1200 }
1201
1202 match consensus_intent {
1204 None => CallIntentConsensus::None,
1205 Some(intent) if agreeing_count == total_count => {
1206 CallIntentConsensus::Full(intent)
1208 }
1209 Some(intent) => {
1210 CallIntentConsensus::Partial { intent, agreeing_count, total_count }
1212 }
1213 }
1214 }
1215
1216 pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
1225 self.active_room_call_memberships()
1226 .iter()
1227 .map(|(call_member_state_key, _)| call_member_state_key.user_id().to_owned())
1228 .collect()
1229 }
1230
1231 pub fn set_latest_event(&mut self, new_value: LatestEventValue) {
1233 self.latest_event_value = new_value;
1234 }
1235
1236 pub fn update_recency_stamp(&mut self, stamp: RoomRecencyStamp) {
1240 self.recency_stamp = Some(stamp);
1241 }
1242
1243 pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
1245 self.base_info.pinned_events.clone().and_then(|c| c.pinned)
1246 }
1247
1248 pub fn fully_read_event_id(&self) -> Option<&EventId> {
1251 self.base_info.fully_read_event_id.as_deref()
1252 }
1253
1254 pub fn is_pinned_event(&self, event_id: &EventId) -> bool {
1260 self.base_info
1261 .pinned_events
1262 .as_ref()
1263 .and_then(|content| content.pinned.as_deref())
1264 .is_some_and(|pinned| pinned.contains(&event_id.to_owned()))
1265 }
1266
1267 pub fn read_receipts(&self) -> &RoomReadReceipts {
1269 &self.read_receipts
1270 }
1271
1272 pub fn set_read_receipts(&mut self, read_receipts: RoomReadReceipts) {
1274 self.read_receipts = read_receipts;
1275 }
1276
1277 #[instrument(skip_all, fields(room_id = ?self.room_id))]
1285 pub(crate) async fn apply_migrations(&mut self, store: SaveLockedStateStore) -> bool {
1286 let mut migrated = false;
1287
1288 if self.data_format_version < 1 {
1289 info!("Migrating room info to version 1");
1290
1291 match store.get_room_account_data_event_static::<TagEventContent>(&self.room_id).await {
1293 Ok(Some(raw_event)) => match raw_event.deserialize() {
1295 Ok(event) => {
1296 self.base_info.handle_notable_tags(&event.content.tags);
1297 }
1298 Err(error) => {
1299 warn!("Failed to deserialize room tags: {error}");
1300 }
1301 },
1302 Ok(_) => {
1303 }
1305 Err(error) => {
1306 warn!("Failed to load room tags: {error}");
1307 }
1308 }
1309
1310 match store.get_state_event_static::<RoomPinnedEventsEventContent>(&self.room_id).await
1312 {
1313 Ok(Some(RawSyncOrStrippedState::Sync(raw_event))) => {
1315 if let Some(mut raw_event) =
1316 RawStateEventWithKeys::try_from_raw_state_event(raw_event.cast())
1317 {
1318 self.handle_state_event(&mut raw_event);
1319 }
1320 }
1321 Ok(_) => {
1322 }
1324 Err(error) => {
1325 warn!("Failed to load room pinned events: {error}");
1326 }
1327 }
1328
1329 self.data_format_version = 1;
1330 migrated = true;
1331 }
1332
1333 migrated
1334 }
1335
1336 pub fn active_service_member_count(&self) -> Option<u64> {
1339 self.summary.active_service_members
1340 }
1341
1342 pub fn update_active_service_member_count(&mut self, count: Option<u64>) {
1345 self.summary.active_service_members = count;
1346 }
1347}
1348
1349#[repr(transparent)]
1351#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
1352#[serde(transparent)]
1353pub struct RoomRecencyStamp(u64);
1354
1355impl From<u64> for RoomRecencyStamp {
1356 fn from(value: u64) -> Self {
1357 Self(value)
1358 }
1359}
1360
1361impl From<RoomRecencyStamp> for u64 {
1362 fn from(value: RoomRecencyStamp) -> Self {
1363 value.0
1364 }
1365}
1366
1367#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1368pub(crate) enum SyncInfo {
1369 NoState,
1375
1376 PartiallySynced,
1379
1380 FullySynced,
1382}
1383
1384pub fn apply_redaction(
1387 event: &Raw<AnySyncTimelineEvent>,
1388 raw_redaction: &Raw<SyncRoomRedactionEvent>,
1389 rules: &RedactionRules,
1390) -> Option<Raw<AnySyncTimelineEvent>> {
1391 use ruma::canonical_json::{RedactedBecause, redact_in_place};
1392
1393 let mut event_json = match event.deserialize_as() {
1394 Ok(json) => json,
1395 Err(e) => {
1396 warn!("Failed to deserialize latest event: {e}");
1397 return None;
1398 }
1399 };
1400
1401 let redacted_because = match RedactedBecause::from_raw_event(raw_redaction) {
1402 Ok(rb) => rb,
1403 Err(e) => {
1404 warn!("Redaction event is not valid canonical JSON: {e}");
1405 return None;
1406 }
1407 };
1408
1409 let redact_result = redact_in_place(&mut event_json, rules, Some(redacted_because));
1410
1411 if let Err(e) = redact_result {
1412 warn!("Failed to redact event: {e}");
1413 return None;
1414 }
1415
1416 let raw = Raw::new(&event_json).expect("CanonicalJsonObject must be serializable");
1417 Some(raw.cast_unchecked())
1418}
1419
1420#[derive(Debug, Clone)]
1430pub struct RoomInfoNotableUpdate {
1431 pub room_id: OwnedRoomId,
1433
1434 pub reasons: RoomInfoNotableUpdateReasons,
1436}
1437
1438bitflags! {
1439 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1441 pub struct RoomInfoNotableUpdateReasons: u16 {
1442 const RECENCY_STAMP = 0b0000_0000_0000_0001;
1444
1445 const LATEST_EVENT = 0b0000_0000_0000_0010;
1447
1448 const READ_RECEIPT = 0b0000_0000_0000_0100;
1450
1451 const UNREAD_MARKER = 0b0000_0000_0000_1000;
1453
1454 const MEMBERSHIP = 0b0000_0000_0001_0000;
1456
1457 const DISPLAY_NAME = 0b0000_0000_0010_0000;
1459
1460 const ACTIVE_SERVICE_MEMBERS = 0b0000_0000_0100_0000;
1462
1463 const NONE = 0b0000_0000_1000_0000;
1474
1475 const FULLY_READ = 0b0000_0001_0000_0000;
1477
1478 const HEROES = 0b0000_0010_0000_0000;
1480 }
1481}
1482
1483impl Default for RoomInfoNotableUpdateReasons {
1484 fn default() -> Self {
1485 Self::empty()
1486 }
1487}
1488
1489#[cfg(test)]
1490mod tests {
1491 use std::{collections::BTreeSet, str::FromStr, sync::Arc, time::Duration};
1492
1493 use assert_matches::assert_matches;
1494 use futures_util::future::{self, Either};
1495 #[cfg(all(target_family = "wasm", target_os = "unknown"))]
1496 use gloo_timers::future::sleep;
1497 use matrix_sdk_common::executor::spawn;
1498 use matrix_sdk_test::{async_test, event_factory::EventFactory};
1499 use ruma::{
1500 assign,
1501 events::{
1502 AnyRoomAccountDataEvent,
1503 room::pinned_events::RoomPinnedEventsEventContent,
1504 tag::{TagInfo, TagName, Tags, UserTagName},
1505 },
1506 owned_event_id, owned_mxc_uri, owned_user_id, room_id,
1507 serde::Raw,
1508 user_id,
1509 };
1510 use serde_json::json;
1511 use similar_asserts::assert_eq;
1512 use tokio::sync::Mutex;
1513 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
1514 use tokio::time::sleep;
1515
1516 use super::{BaseRoomInfo, LatestEventValue, RoomInfo, SyncInfo};
1517 use crate::{
1518 RawStateEventWithKeys, Room, RoomDisplayName, RoomHero, RoomInfoNotableUpdateReasons,
1519 RoomState, StateChanges, StateStore,
1520 notification_settings::RoomNotificationMode,
1521 room::{RoomNotableTags, RoomSummary},
1522 store::{IntoStateStore, MemoryStore, RoomLoadSettings, SaveLockedStateStore},
1523 sync::UnreadNotificationsCount,
1524 };
1525
1526 #[test]
1527 fn test_room_info_serialization() {
1528 let info = RoomInfo {
1532 data_format_version: 1,
1533 room_id: room_id!("!gda78o:server.tld").into(),
1534 room_state: RoomState::Invited,
1535 notification_counts: UnreadNotificationsCount {
1536 highlight_count: 1,
1537 notification_count: 2,
1538 },
1539 summary: RoomSummary {
1540 room_heroes: vec![RoomHero {
1541 user_id: owned_user_id!("@somebody:example.org"),
1542 display_name: None,
1543 avatar_url: None,
1544 }],
1545 joined_member_count: 5,
1546 invited_member_count: 0,
1547 active_service_members: None,
1548 },
1549 members_synced: true,
1550 last_prev_batch: Some("pb".to_owned()),
1551 sync_info: SyncInfo::FullySynced,
1552 encryption_state_synced: true,
1553 latest_event_value: LatestEventValue::None,
1554 base_info: Box::new(
1555 assign!(BaseRoomInfo::new(), { pinned_events: Some(RoomPinnedEventsEventContent::new(vec![owned_event_id!("$a")]).into()) }),
1556 ),
1557 read_receipts: Default::default(),
1558 warned_about_unknown_room_version_rules: Arc::new(false.into()),
1559 cached_display_name: None,
1560 cached_user_defined_notification_mode: None,
1561 recency_stamp: Some(42.into()),
1562 };
1563
1564 let info_json = json!({
1565 "data_format_version": 1,
1566 "room_id": "!gda78o:server.tld",
1567 "room_state": "Invited",
1568 "notification_counts": {
1569 "highlight_count": 1,
1570 "notification_count": 2,
1571 },
1572 "summary": {
1573 "room_heroes": [{
1574 "user_id": "@somebody:example.org",
1575 "display_name": null,
1576 "avatar_url": null
1577 }],
1578 "joined_member_count": 5,
1579 "invited_member_count": 0,
1580 },
1581 "members_synced": true,
1582 "last_prev_batch": "pb",
1583 "sync_info": "FullySynced",
1584 "encryption_state_synced": true,
1585 "latest_event_value": "None",
1586 "base_info": {
1587 "avatar": null,
1588 "canonical_alias": null,
1589 "create": null,
1590 "dm_targets": [],
1591 "encryption": null,
1592 "guest_access": null,
1593 "history_visibility": null,
1594 "is_marked_unread": false,
1595 "is_marked_unread_source": "Unstable",
1596 "join_rules": null,
1597 "max_power_level": 100,
1598 "member_hints": null,
1599 "name": null,
1600 "tombstone": null,
1601 "topic": null,
1602 "pinned_events": {
1603 "pinned": ["$a"]
1604 },
1605 },
1606 "read_receipts": {
1607 "num_unread": 0,
1608 "num_mentions": 0,
1609 "num_notifications": 0,
1610 "latest_active": null,
1611 "pending": {
1612 "items": [],
1613 "capacity": 10,
1614 },
1615 },
1616 "recency_stamp": 42,
1617 });
1618
1619 assert_eq!(serde_json::to_value(info).unwrap(), info_json);
1620 }
1621
1622 #[async_test]
1623 async fn test_room_info_migration_v1() {
1624 let store = SaveLockedStateStore::new(MemoryStore::new().into_state_store());
1625
1626 let room_info_json = json!({
1627 "room_id": "!gda78o:server.tld",
1628 "room_state": "Joined",
1629 "notification_counts": {
1630 "highlight_count": 1,
1631 "notification_count": 2,
1632 },
1633 "summary": {
1634 "room_heroes": [{
1635 "user_id": "@somebody:example.org",
1636 "display_name": null,
1637 "avatar_url": null
1638 }],
1639 "joined_member_count": 5,
1640 "invited_member_count": 0,
1641 },
1642 "members_synced": true,
1643 "last_prev_batch": "pb",
1644 "sync_info": "FullySynced",
1645 "encryption_state_synced": true,
1646 "latest_event": {
1647 "event": {
1648 "encryption_info": null,
1649 "event": {
1650 "sender": "@u:i.uk",
1651 },
1652 },
1653 },
1654 "base_info": {
1655 "avatar": null,
1656 "canonical_alias": null,
1657 "create": null,
1658 "dm_targets": [],
1659 "encryption": null,
1660 "guest_access": null,
1661 "history_visibility": null,
1662 "join_rules": null,
1663 "max_power_level": 100,
1664 "name": null,
1665 "tombstone": null,
1666 "topic": null,
1667 },
1668 "read_receipts": {
1669 "num_unread": 0,
1670 "num_mentions": 0,
1671 "num_notifications": 0,
1672 "latest_active": null,
1673 "pending": []
1674 },
1675 "recency_stamp": 42,
1676 });
1677 let mut room_info: RoomInfo = serde_json::from_value(room_info_json).unwrap();
1678
1679 assert_eq!(room_info.data_format_version, 0);
1680 assert!(room_info.base_info.notable_tags.is_empty());
1681 assert!(room_info.base_info.pinned_events.is_none());
1682
1683 assert!(room_info.apply_migrations(store.clone()).await);
1685
1686 assert_eq!(room_info.data_format_version, 1);
1687 assert!(room_info.base_info.notable_tags.is_empty());
1688 assert!(room_info.base_info.pinned_events.is_none());
1689
1690 assert!(!room_info.apply_migrations(store.clone()).await);
1692
1693 assert_eq!(room_info.data_format_version, 1);
1694 assert!(room_info.base_info.notable_tags.is_empty());
1695 assert!(room_info.base_info.pinned_events.is_none());
1696
1697 let mut changes = StateChanges::default();
1699
1700 let f = EventFactory::new().room(&room_info.room_id).sender(user_id!("@example:localhost"));
1701 let mut tags = Tags::new();
1702 tags.insert(TagName::Favorite, TagInfo::new());
1703 tags.insert(TagName::User(UserTagName::from_str("u.work").unwrap()), TagInfo::new());
1704 let raw_tag_event: Raw<AnyRoomAccountDataEvent> = f.tag(tags).into();
1705 let tag_event = raw_tag_event.deserialize().unwrap();
1706 changes.add_room_account_data(&room_info.room_id, tag_event, raw_tag_event);
1707
1708 let raw_pinned_events_event: Raw<_> = f
1709 .room_pinned_events(vec![owned_event_id!("$a"), owned_event_id!("$b")])
1710 .into_raw_sync_state();
1711 let pinned_events_event = raw_pinned_events_event.deserialize().unwrap();
1712 changes.add_state_event(&room_info.room_id, pinned_events_event, raw_pinned_events_event);
1713
1714 store.save_changes(&changes).await.unwrap();
1715
1716 room_info.data_format_version = 0;
1718 assert!(room_info.apply_migrations(store.clone()).await);
1719
1720 assert_eq!(room_info.data_format_version, 1);
1721 assert!(room_info.base_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
1722 assert!(room_info.base_info.pinned_events.is_some());
1723
1724 let new_room_info = RoomInfo::new(room_id!("!new_room:localhost"), RoomState::Joined);
1726 assert_eq!(new_room_info.data_format_version, 1);
1727 }
1728
1729 #[test]
1730 fn test_room_info_deserialization() {
1731 let info_json = json!({
1732 "room_id": "!gda78o:server.tld",
1733 "room_state": "Joined",
1734 "notification_counts": {
1735 "highlight_count": 1,
1736 "notification_count": 2,
1737 },
1738 "summary": {
1739 "room_heroes": [{
1740 "user_id": "@somebody:example.org",
1741 "display_name": "Somebody",
1742 "avatar_url": "mxc://example.org/abc"
1743 }],
1744 "joined_member_count": 5,
1745 "invited_member_count": 0,
1746 },
1747 "members_synced": true,
1748 "last_prev_batch": "pb",
1749 "sync_info": "FullySynced",
1750 "encryption_state_synced": true,
1751 "base_info": {
1752 "avatar": null,
1753 "canonical_alias": null,
1754 "create": null,
1755 "dm_targets": [],
1756 "encryption": null,
1757 "guest_access": null,
1758 "history_visibility": null,
1759 "join_rules": null,
1760 "max_power_level": 100,
1761 "member_hints": null,
1762 "name": null,
1763 "tombstone": null,
1764 "topic": null,
1765 },
1766 "cached_display_name": { "Calculated": "lol" },
1767 "cached_user_defined_notification_mode": "Mute",
1768 "recency_stamp": 42,
1769 });
1770
1771 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1772
1773 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1774 assert_eq!(info.room_state, RoomState::Joined);
1775 assert_eq!(info.notification_counts.highlight_count, 1);
1776 assert_eq!(info.notification_counts.notification_count, 2);
1777 assert_eq!(
1778 info.summary.room_heroes,
1779 vec![RoomHero {
1780 user_id: owned_user_id!("@somebody:example.org"),
1781 display_name: Some("Somebody".to_owned()),
1782 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1783 }]
1784 );
1785 assert_eq!(info.summary.joined_member_count, 5);
1786 assert_eq!(info.summary.invited_member_count, 0);
1787 assert!(info.members_synced);
1788 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1789 assert_eq!(info.sync_info, SyncInfo::FullySynced);
1790 assert!(info.encryption_state_synced);
1791 assert_matches!(info.latest_event_value, LatestEventValue::None);
1792 assert!(info.base_info.avatar.is_none());
1793 assert!(info.base_info.canonical_alias.is_none());
1794 assert!(info.base_info.create.is_none());
1795 assert_eq!(info.base_info.dm_targets.len(), 0);
1796 assert!(info.base_info.encryption.is_none());
1797 assert!(info.base_info.guest_access.is_none());
1798 assert!(info.base_info.history_visibility.is_none());
1799 assert!(info.base_info.join_rules.is_none());
1800 assert_eq!(info.base_info.max_power_level, 100);
1801 assert!(info.base_info.member_hints.is_none());
1802 assert!(info.base_info.name.is_none());
1803 assert!(info.base_info.tombstone.is_none());
1804 assert!(info.base_info.topic.is_none());
1805
1806 assert_eq!(
1807 info.cached_display_name.as_ref(),
1808 Some(&RoomDisplayName::Calculated("lol".to_owned())),
1809 );
1810 assert_eq!(
1811 info.cached_user_defined_notification_mode.as_ref(),
1812 Some(&RoomNotificationMode::Mute)
1813 );
1814 assert_eq!(info.recency_stamp.as_ref(), Some(&42.into()));
1815 }
1816
1817 #[test]
1824 fn test_room_info_deserialization_without_optional_items() {
1825 let info_json = json!({
1828 "room_id": "!gda78o:server.tld",
1829 "room_state": "Invited",
1830 "notification_counts": {
1831 "highlight_count": 1,
1832 "notification_count": 2,
1833 },
1834 "summary": {
1835 "room_heroes": [{
1836 "user_id": "@somebody:example.org",
1837 "display_name": "Somebody",
1838 "avatar_url": "mxc://example.org/abc"
1839 }],
1840 "joined_member_count": 5,
1841 "invited_member_count": 0,
1842 },
1843 "members_synced": true,
1844 "last_prev_batch": "pb",
1845 "sync_info": "FullySynced",
1846 "encryption_state_synced": true,
1847 "base_info": {
1848 "avatar": null,
1849 "canonical_alias": null,
1850 "create": null,
1851 "dm_targets": [],
1852 "encryption": null,
1853 "guest_access": null,
1854 "history_visibility": null,
1855 "join_rules": null,
1856 "max_power_level": 100,
1857 "name": null,
1858 "tombstone": null,
1859 "topic": null,
1860 },
1861 });
1862
1863 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1864
1865 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1866 assert_eq!(info.room_state, RoomState::Invited);
1867 assert_eq!(info.notification_counts.highlight_count, 1);
1868 assert_eq!(info.notification_counts.notification_count, 2);
1869 assert_eq!(
1870 info.summary.room_heroes,
1871 vec![RoomHero {
1872 user_id: owned_user_id!("@somebody:example.org"),
1873 display_name: Some("Somebody".to_owned()),
1874 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1875 }]
1876 );
1877 assert_eq!(info.summary.joined_member_count, 5);
1878 assert_eq!(info.summary.invited_member_count, 0);
1879 assert!(info.members_synced);
1880 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1881 assert_eq!(info.sync_info, SyncInfo::FullySynced);
1882 assert!(info.encryption_state_synced);
1883 assert!(info.base_info.avatar.is_none());
1884 assert!(info.base_info.canonical_alias.is_none());
1885 assert!(info.base_info.create.is_none());
1886 assert_eq!(info.base_info.dm_targets.len(), 0);
1887 assert!(info.base_info.encryption.is_none());
1888 assert!(info.base_info.guest_access.is_none());
1889 assert!(info.base_info.history_visibility.is_none());
1890 assert!(info.base_info.join_rules.is_none());
1891 assert_eq!(info.base_info.max_power_level, 100);
1892 assert!(info.base_info.name.is_none());
1893 assert!(info.base_info.tombstone.is_none());
1894 assert!(info.base_info.topic.is_none());
1895 }
1896
1897 #[test]
1898 fn test_member_hints_with_different_contents_reset_computed_value() {
1899 let expected = BTreeSet::from_iter([
1900 owned_user_id!("@alice:example.org"),
1901 owned_user_id!("@bob:example.org"),
1902 ]);
1903
1904 let info_json = json!({
1905 "room_id": "!gda78o:server.tld",
1906 "room_state": "Invited",
1907 "notification_counts": {
1908 "highlight_count": 1,
1909 "notification_count": 2,
1910 },
1911 "summary": {
1912 "room_heroes": [{
1913 "user_id": "@somebody:example.org",
1914 "display_name": "Somebody",
1915 "avatar_url": "mxc://example.org/abc"
1916 }],
1917 "joined_member_count": 5,
1918 "invited_member_count": 0,
1919 "active_service_members": 2,
1920 },
1921 "members_synced": true,
1922 "last_prev_batch": "pb",
1923 "sync_info": "FullySynced",
1924 "encryption_state_synced": true,
1925 "base_info": {
1926 "avatar": null,
1927 "canonical_alias": null,
1928 "create": null,
1929 "dm_targets": [],
1930 "encryption": null,
1931 "guest_access": null,
1932 "history_visibility": null,
1933 "join_rules": null,
1934 "max_power_level": 100,
1935 "member_hints": {
1936 "Original": {
1937 "content": {
1938 "service_members": ["@alice:example.org", "@bob:example.org"]
1939 }
1940 }
1941 },
1942 "name": null,
1943 "tombstone": null,
1944 "topic": null,
1945 },
1946 });
1947
1948 let info: RoomInfo = serde_json::from_value(info_json.clone()).unwrap();
1949 assert_eq!(info.base_info.member_hints.unwrap().content.service_members.unwrap(), expected);
1950 assert_eq!(info.summary.active_service_members, Some(2));
1951
1952 let mut info: RoomInfo = serde_json::from_value(info_json.clone()).unwrap();
1954 let mut raw_state_event_with_keys = RawStateEventWithKeys::try_from_raw_state_event(
1955 EventFactory::new()
1956 .sender(user_id!("@alice:example.org"))
1957 .member_hints(expected.clone())
1958 .into_raw_sync_state(),
1959 )
1960 .expect("Expected member hints event is created");
1961
1962 info.handle_state_event(&mut raw_state_event_with_keys);
1963
1964 assert_eq!(info.base_info.member_hints.unwrap().content.service_members.unwrap(), expected);
1966 assert_eq!(info.summary.active_service_members, Some(2));
1968
1969 let mut info: RoomInfo = serde_json::from_value(info_json).unwrap();
1971 let new_member_hints = BTreeSet::from_iter([owned_user_id!("@alice:example.org")]);
1972 let mut raw_state_event_with_keys = RawStateEventWithKeys::try_from_raw_state_event(
1973 EventFactory::new()
1974 .sender(user_id!("@alice:example.org"))
1975 .member_hints(new_member_hints.clone())
1976 .into_raw_sync_state(),
1977 )
1978 .expect("New member hints event is created");
1979
1980 info.handle_state_event(&mut raw_state_event_with_keys);
1981
1982 assert_eq!(
1984 info.base_info.member_hints.unwrap().content.service_members.unwrap(),
1985 new_member_hints
1986 );
1987 assert!(info.summary.active_service_members.is_none());
1989 }
1990
1991 fn make_room_and_state_store(room_state: RoomState) -> (Room, SaveLockedStateStore) {
1992 let state_store = SaveLockedStateStore::new(MemoryStore::new().into_state_store());
1993 let user_id = user_id!("@user:localhost");
1994 let room_id = room_id!("!room:localhost");
1995 let (sender, _) = tokio::sync::broadcast::channel(1);
1996 let room = Room::new(user_id, state_store.clone(), room_id, room_state, sender);
1997 (room, state_store)
1998 }
1999
2000 #[async_test]
2001 async fn test_update_room_info_only_updates_in_memory_room_info() {
2002 let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2003
2004 let before = room.clone_info();
2005 assert_eq!(before.state(), RoomState::Joined);
2006 room.update_room_info(|mut info| {
2007 info.mark_as_banned();
2008 (info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
2009 })
2010 .await;
2011 let after = room.clone_info();
2012 assert_eq!(after.state(), RoomState::Banned);
2013
2014 let infos = state_store
2015 .get_room_infos(&RoomLoadSettings::One(room.room_id.clone()))
2016 .await
2017 .expect("get room info");
2018 assert!(infos.is_empty());
2019 }
2020
2021 #[async_test]
2022 async fn test_update_room_info_with_store_guard_only_updates_in_memory_room_info() {
2023 let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2024
2025 let before = room.clone_info();
2026 assert_eq!(before.state(), RoomState::Joined);
2027 room.update_room_info_with_store_guard(&state_store.lock().lock().await, |mut info| {
2028 info.mark_as_banned();
2029 (info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
2030 })
2031 .expect("update room info");
2032 let after = room.clone_info();
2033 assert_eq!(after.state(), RoomState::Banned);
2034
2035 let infos = state_store
2036 .get_room_infos(&RoomLoadSettings::One(room.room_id.clone()))
2037 .await
2038 .expect("get room info");
2039 assert!(infos.is_empty());
2040 }
2041
2042 #[async_test]
2043 async fn test_update_room_info_only_accepts_guard_for_underlying_mutex() {
2044 let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2045
2046 room.update_room_info_with_store_guard(&state_store.lock().lock().await, |info| {
2047 (info, RoomInfoNotableUpdateReasons::NONE)
2048 })
2049 .expect("room accepts guard for underlying mutex");
2050
2051 let mutex = Mutex::new(());
2052 room.update_room_info_with_store_guard(&mutex.lock().await, |info| {
2053 (info, RoomInfoNotableUpdateReasons::NONE)
2054 })
2055 .expect_err("room does not accept guard for unknown mutex");
2056 }
2057
2058 #[async_test]
2059 async fn test_update_and_save_room_info_updates_room_info_in_memory_and_store() {
2060 let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2061
2062 let before = room.clone_info();
2063 assert_eq!(before.state(), RoomState::Joined);
2064 room.update_and_save_room_info(|mut info| {
2065 info.mark_as_banned();
2066 (info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
2067 })
2068 .await
2069 .expect("update and save room info");
2070 let after = room.clone_info();
2071 assert_eq!(after.state(), RoomState::Banned);
2072
2073 let infos = state_store
2074 .get_room_infos(&RoomLoadSettings::One(room.room_id.clone()))
2075 .await
2076 .expect("get room info");
2077 assert_eq!(infos.len(), 1);
2078 assert_matches!(infos.first(), Some(info) => {
2079 info.state() == RoomState::Banned
2080 });
2081 }
2082
2083 #[async_test]
2084 async fn test_update_and_save_room_info_with_store_guard_updates_room_info_in_memory_and_store()
2085 {
2086 let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2087
2088 let before = room.clone_info();
2089 assert_eq!(before.state(), RoomState::Joined);
2090 room.update_and_save_room_info_with_store_guard(
2091 &state_store.lock().lock().await,
2092 |mut info| {
2093 info.mark_as_banned();
2094 (info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
2095 },
2096 )
2097 .await
2098 .expect("update and save room info");
2099 let after = room.clone_info();
2100 assert_eq!(after.state(), RoomState::Banned);
2101
2102 let infos = state_store
2103 .get_room_infos(&RoomLoadSettings::One(room.room_id.clone()))
2104 .await
2105 .expect("get room info");
2106 assert_eq!(infos.len(), 1);
2107 assert_matches!(infos.first(), Some(info) => {
2108 info.state() == RoomState::Banned
2109 });
2110 }
2111
2112 #[async_test]
2113 async fn test_update_and_save_room_info_only_accepts_guard_for_underlying_mutex() {
2114 let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2115
2116 room.update_and_save_room_info_with_store_guard(&state_store.lock().lock().await, |info| {
2117 (info, RoomInfoNotableUpdateReasons::NONE)
2118 })
2119 .await
2120 .expect("room accepts guard for underlying mutex");
2121
2122 let mutex = Mutex::new(());
2123 room.update_and_save_room_info_with_store_guard(&mutex.lock().await, |info| {
2124 (info, RoomInfoNotableUpdateReasons::NONE)
2125 })
2126 .await
2127 .expect_err("room does not accept guard for unknown mutex");
2128 }
2129
2130 #[derive(Debug)]
2131 struct Elapsed;
2132
2133 async fn timeout<F: Future + Unpin>(duration: Duration, f: F) -> Result<F::Output, Elapsed> {
2134 #[cfg(all(target_family = "wasm", target_os = "unknown"))]
2135 {
2136 match future::select(sleep(duration), f).await {
2137 Either::Left(_) => return Err(Elapsed),
2138 Either::Right((output, _)) => Ok(output),
2139 }
2140 }
2141 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
2142 {
2143 tokio::time::timeout(duration, f).await.map_err(|_| Elapsed)
2144 }
2145 }
2146
2147 #[async_test]
2148 async fn test_update_room_info_waits_to_acquire_lock_before_updating_room_info() {
2149 let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2150
2151 let lock_task = spawn({
2153 let state_store = state_store.clone();
2154 async move {
2155 let lock = state_store.lock();
2156 let _guard = lock.lock().await;
2157 sleep(Duration::from_secs(5)).await;
2158 }
2159 });
2160
2161 let save_task = spawn(async move {
2163 room.update_room_info(|info| (info, RoomInfoNotableUpdateReasons::NONE)).await
2164 });
2165
2166 assert_matches!(future::select(lock_task, save_task).await, Either::Left((_, save_task)) => {
2169 timeout(Duration::from_millis(100), save_task)
2170 .await
2171 .expect("task completes before timeout")
2172 .expect("task completes successfully")
2173 });
2174 }
2175
2176 #[async_test]
2177 async fn test_update_and_save_room_info_waits_to_acquire_lock_before_updating_room_info() {
2178 let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2179
2180 let lock_task = spawn({
2182 let state_store = state_store.clone();
2183 async move {
2184 let lock = state_store.lock();
2185 let _guard = lock.lock().await;
2186 sleep(Duration::from_secs(5)).await;
2187 }
2188 });
2189
2190 let save_task = spawn(async move {
2192 room.update_and_save_room_info(|info| (info, RoomInfoNotableUpdateReasons::NONE)).await
2193 });
2194
2195 assert_matches!(future::select(lock_task, save_task).await, Either::Left((_, save_task)) => {
2198 timeout(Duration::from_millis(100), save_task)
2199 .await
2200 .expect("task completes before timeout")
2201 .expect("task completes successfully")
2202 .expect("update and save room info");
2203 });
2204 }
2205}