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 active_room_call_consensus_intent(&self) -> CallIntentConsensus {
1157 let memberships = self.active_room_call_memberships();
1158 let total_count: u64 = memberships.len() as u64;
1159
1160 if total_count == 0 {
1161 return CallIntentConsensus::None;
1162 }
1163
1164 let mut consensus_intent: Option<CallIntent> = None;
1166 let mut agreeing_count: u64 = 0;
1167
1168 for (_, data) in memberships.iter() {
1169 if let Some(intent) = data.call_intent() {
1170 match &consensus_intent {
1171 None => {
1173 consensus_intent = Some(intent.clone());
1174 agreeing_count = 1;
1175 }
1176 Some(current) if current == intent => {
1178 agreeing_count += 1;
1179 }
1180 Some(_) => return CallIntentConsensus::None,
1182 }
1183 }
1184 }
1185
1186 match consensus_intent {
1188 None => CallIntentConsensus::None,
1189 Some(intent) if agreeing_count == total_count => {
1190 CallIntentConsensus::Full(intent)
1192 }
1193 Some(intent) => {
1194 CallIntentConsensus::Partial { intent, agreeing_count, total_count }
1196 }
1197 }
1198 }
1199
1200 pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
1209 self.active_room_call_memberships()
1210 .iter()
1211 .map(|(call_member_state_key, _)| call_member_state_key.user_id().to_owned())
1212 .collect()
1213 }
1214
1215 pub fn set_latest_event(&mut self, new_value: LatestEventValue) {
1217 self.latest_event_value = new_value;
1218 }
1219
1220 pub fn update_recency_stamp(&mut self, stamp: RoomRecencyStamp) {
1224 self.recency_stamp = Some(stamp);
1225 }
1226
1227 pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
1229 self.base_info.pinned_events.clone().and_then(|c| c.pinned)
1230 }
1231
1232 pub fn fully_read_event_id(&self) -> Option<&EventId> {
1235 self.base_info.fully_read_event_id.as_deref()
1236 }
1237
1238 pub fn is_pinned_event(&self, event_id: &EventId) -> bool {
1244 self.base_info
1245 .pinned_events
1246 .as_ref()
1247 .and_then(|content| content.pinned.as_deref())
1248 .is_some_and(|pinned| pinned.contains(&event_id.to_owned()))
1249 }
1250
1251 pub fn read_receipts(&self) -> &RoomReadReceipts {
1253 &self.read_receipts
1254 }
1255
1256 pub fn set_read_receipts(&mut self, read_receipts: RoomReadReceipts) {
1258 self.read_receipts = read_receipts;
1259 }
1260
1261 #[instrument(skip_all, fields(room_id = ?self.room_id))]
1269 pub(crate) async fn apply_migrations(&mut self, store: SaveLockedStateStore) -> bool {
1270 let mut migrated = false;
1271
1272 if self.data_format_version < 1 {
1273 info!("Migrating room info to version 1");
1274
1275 match store.get_room_account_data_event_static::<TagEventContent>(&self.room_id).await {
1277 Ok(Some(raw_event)) => match raw_event.deserialize() {
1279 Ok(event) => {
1280 self.base_info.handle_notable_tags(&event.content.tags);
1281 }
1282 Err(error) => {
1283 warn!("Failed to deserialize room tags: {error}");
1284 }
1285 },
1286 Ok(_) => {
1287 }
1289 Err(error) => {
1290 warn!("Failed to load room tags: {error}");
1291 }
1292 }
1293
1294 match store.get_state_event_static::<RoomPinnedEventsEventContent>(&self.room_id).await
1296 {
1297 Ok(Some(RawSyncOrStrippedState::Sync(raw_event))) => {
1299 if let Some(mut raw_event) =
1300 RawStateEventWithKeys::try_from_raw_state_event(raw_event.cast())
1301 {
1302 self.handle_state_event(&mut raw_event);
1303 }
1304 }
1305 Ok(_) => {
1306 }
1308 Err(error) => {
1309 warn!("Failed to load room pinned events: {error}");
1310 }
1311 }
1312
1313 self.data_format_version = 1;
1314 migrated = true;
1315 }
1316
1317 migrated
1318 }
1319
1320 pub fn active_service_member_count(&self) -> Option<u64> {
1323 self.summary.active_service_members
1324 }
1325
1326 pub fn update_active_service_member_count(&mut self, count: Option<u64>) {
1329 self.summary.active_service_members = count;
1330 }
1331}
1332
1333#[repr(transparent)]
1335#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
1336#[serde(transparent)]
1337pub struct RoomRecencyStamp(u64);
1338
1339impl From<u64> for RoomRecencyStamp {
1340 fn from(value: u64) -> Self {
1341 Self(value)
1342 }
1343}
1344
1345impl From<RoomRecencyStamp> for u64 {
1346 fn from(value: RoomRecencyStamp) -> Self {
1347 value.0
1348 }
1349}
1350
1351#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1352pub(crate) enum SyncInfo {
1353 NoState,
1359
1360 PartiallySynced,
1363
1364 FullySynced,
1366}
1367
1368pub fn apply_redaction(
1371 event: &Raw<AnySyncTimelineEvent>,
1372 raw_redaction: &Raw<SyncRoomRedactionEvent>,
1373 rules: &RedactionRules,
1374) -> Option<Raw<AnySyncTimelineEvent>> {
1375 use ruma::canonical_json::{RedactedBecause, redact_in_place};
1376
1377 let mut event_json = match event.deserialize_as() {
1378 Ok(json) => json,
1379 Err(e) => {
1380 warn!("Failed to deserialize latest event: {e}");
1381 return None;
1382 }
1383 };
1384
1385 let redacted_because = match RedactedBecause::from_raw_event(raw_redaction) {
1386 Ok(rb) => rb,
1387 Err(e) => {
1388 warn!("Redaction event is not valid canonical JSON: {e}");
1389 return None;
1390 }
1391 };
1392
1393 let redact_result = redact_in_place(&mut event_json, rules, Some(redacted_because));
1394
1395 if let Err(e) = redact_result {
1396 warn!("Failed to redact event: {e}");
1397 return None;
1398 }
1399
1400 let raw = Raw::new(&event_json).expect("CanonicalJsonObject must be serializable");
1401 Some(raw.cast_unchecked())
1402}
1403
1404#[derive(Debug, Clone)]
1414pub struct RoomInfoNotableUpdate {
1415 pub room_id: OwnedRoomId,
1417
1418 pub reasons: RoomInfoNotableUpdateReasons,
1420}
1421
1422bitflags! {
1423 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1425 pub struct RoomInfoNotableUpdateReasons: u16 {
1426 const RECENCY_STAMP = 0b0000_0000_0000_0001;
1428
1429 const LATEST_EVENT = 0b0000_0000_0000_0010;
1431
1432 const READ_RECEIPT = 0b0000_0000_0000_0100;
1434
1435 const UNREAD_MARKER = 0b0000_0000_0000_1000;
1437
1438 const MEMBERSHIP = 0b0000_0000_0001_0000;
1440
1441 const DISPLAY_NAME = 0b0000_0000_0010_0000;
1443
1444 const ACTIVE_SERVICE_MEMBERS = 0b0000_0000_0100_0000;
1446
1447 const NONE = 0b0000_0000_1000_0000;
1458
1459 const FULLY_READ = 0b0000_0001_0000_0000;
1461 }
1462}
1463
1464impl Default for RoomInfoNotableUpdateReasons {
1465 fn default() -> Self {
1466 Self::empty()
1467 }
1468}
1469
1470#[cfg(test)]
1471mod tests {
1472 use std::{collections::BTreeSet, str::FromStr, sync::Arc, time::Duration};
1473
1474 use assert_matches::assert_matches;
1475 use futures_util::future::{self, Either};
1476 #[cfg(all(target_family = "wasm", target_os = "unknown"))]
1477 use gloo_timers::future::sleep;
1478 use matrix_sdk_common::executor::spawn;
1479 use matrix_sdk_test::{async_test, event_factory::EventFactory};
1480 use ruma::{
1481 assign,
1482 events::{
1483 AnyRoomAccountDataEvent,
1484 room::pinned_events::RoomPinnedEventsEventContent,
1485 tag::{TagInfo, TagName, Tags, UserTagName},
1486 },
1487 owned_event_id, owned_mxc_uri, owned_user_id, room_id,
1488 serde::Raw,
1489 user_id,
1490 };
1491 use serde_json::json;
1492 use similar_asserts::assert_eq;
1493 use tokio::sync::Mutex;
1494 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
1495 use tokio::time::sleep;
1496
1497 use super::{BaseRoomInfo, LatestEventValue, RoomInfo, SyncInfo};
1498 use crate::{
1499 RawStateEventWithKeys, Room, RoomDisplayName, RoomHero, RoomInfoNotableUpdateReasons,
1500 RoomState, StateChanges, StateStore,
1501 notification_settings::RoomNotificationMode,
1502 room::{RoomNotableTags, RoomSummary},
1503 store::{IntoStateStore, MemoryStore, RoomLoadSettings, SaveLockedStateStore},
1504 sync::UnreadNotificationsCount,
1505 };
1506
1507 #[test]
1508 fn test_room_info_serialization() {
1509 let info = RoomInfo {
1513 data_format_version: 1,
1514 room_id: room_id!("!gda78o:server.tld").into(),
1515 room_state: RoomState::Invited,
1516 notification_counts: UnreadNotificationsCount {
1517 highlight_count: 1,
1518 notification_count: 2,
1519 },
1520 summary: RoomSummary {
1521 room_heroes: vec![RoomHero {
1522 user_id: owned_user_id!("@somebody:example.org"),
1523 display_name: None,
1524 avatar_url: None,
1525 }],
1526 joined_member_count: 5,
1527 invited_member_count: 0,
1528 active_service_members: None,
1529 },
1530 members_synced: true,
1531 last_prev_batch: Some("pb".to_owned()),
1532 sync_info: SyncInfo::FullySynced,
1533 encryption_state_synced: true,
1534 latest_event_value: LatestEventValue::None,
1535 base_info: Box::new(
1536 assign!(BaseRoomInfo::new(), { pinned_events: Some(RoomPinnedEventsEventContent::new(vec![owned_event_id!("$a")]).into()) }),
1537 ),
1538 read_receipts: Default::default(),
1539 warned_about_unknown_room_version_rules: Arc::new(false.into()),
1540 cached_display_name: None,
1541 cached_user_defined_notification_mode: None,
1542 recency_stamp: Some(42.into()),
1543 };
1544
1545 let info_json = json!({
1546 "data_format_version": 1,
1547 "room_id": "!gda78o:server.tld",
1548 "room_state": "Invited",
1549 "notification_counts": {
1550 "highlight_count": 1,
1551 "notification_count": 2,
1552 },
1553 "summary": {
1554 "room_heroes": [{
1555 "user_id": "@somebody:example.org",
1556 "display_name": null,
1557 "avatar_url": null
1558 }],
1559 "joined_member_count": 5,
1560 "invited_member_count": 0,
1561 },
1562 "members_synced": true,
1563 "last_prev_batch": "pb",
1564 "sync_info": "FullySynced",
1565 "encryption_state_synced": true,
1566 "latest_event_value": "None",
1567 "base_info": {
1568 "avatar": null,
1569 "canonical_alias": null,
1570 "create": null,
1571 "dm_targets": [],
1572 "encryption": null,
1573 "guest_access": null,
1574 "history_visibility": null,
1575 "is_marked_unread": false,
1576 "is_marked_unread_source": "Unstable",
1577 "join_rules": null,
1578 "max_power_level": 100,
1579 "member_hints": null,
1580 "name": null,
1581 "tombstone": null,
1582 "topic": null,
1583 "pinned_events": {
1584 "pinned": ["$a"]
1585 },
1586 },
1587 "read_receipts": {
1588 "num_unread": 0,
1589 "num_mentions": 0,
1590 "num_notifications": 0,
1591 "latest_active": null,
1592 "pending": [],
1593 },
1594 "recency_stamp": 42,
1595 });
1596
1597 assert_eq!(serde_json::to_value(info).unwrap(), info_json);
1598 }
1599
1600 #[async_test]
1601 async fn test_room_info_migration_v1() {
1602 let store = SaveLockedStateStore::new(MemoryStore::new().into_state_store());
1603
1604 let room_info_json = json!({
1605 "room_id": "!gda78o:server.tld",
1606 "room_state": "Joined",
1607 "notification_counts": {
1608 "highlight_count": 1,
1609 "notification_count": 2,
1610 },
1611 "summary": {
1612 "room_heroes": [{
1613 "user_id": "@somebody:example.org",
1614 "display_name": null,
1615 "avatar_url": null
1616 }],
1617 "joined_member_count": 5,
1618 "invited_member_count": 0,
1619 },
1620 "members_synced": true,
1621 "last_prev_batch": "pb",
1622 "sync_info": "FullySynced",
1623 "encryption_state_synced": true,
1624 "latest_event": {
1625 "event": {
1626 "encryption_info": null,
1627 "event": {
1628 "sender": "@u:i.uk",
1629 },
1630 },
1631 },
1632 "base_info": {
1633 "avatar": null,
1634 "canonical_alias": null,
1635 "create": null,
1636 "dm_targets": [],
1637 "encryption": null,
1638 "guest_access": null,
1639 "history_visibility": null,
1640 "join_rules": null,
1641 "max_power_level": 100,
1642 "name": null,
1643 "tombstone": null,
1644 "topic": null,
1645 },
1646 "read_receipts": {
1647 "num_unread": 0,
1648 "num_mentions": 0,
1649 "num_notifications": 0,
1650 "latest_active": null,
1651 "pending": []
1652 },
1653 "recency_stamp": 42,
1654 });
1655 let mut room_info: RoomInfo = serde_json::from_value(room_info_json).unwrap();
1656
1657 assert_eq!(room_info.data_format_version, 0);
1658 assert!(room_info.base_info.notable_tags.is_empty());
1659 assert!(room_info.base_info.pinned_events.is_none());
1660
1661 assert!(room_info.apply_migrations(store.clone()).await);
1663
1664 assert_eq!(room_info.data_format_version, 1);
1665 assert!(room_info.base_info.notable_tags.is_empty());
1666 assert!(room_info.base_info.pinned_events.is_none());
1667
1668 assert!(!room_info.apply_migrations(store.clone()).await);
1670
1671 assert_eq!(room_info.data_format_version, 1);
1672 assert!(room_info.base_info.notable_tags.is_empty());
1673 assert!(room_info.base_info.pinned_events.is_none());
1674
1675 let mut changes = StateChanges::default();
1677
1678 let f = EventFactory::new().room(&room_info.room_id).sender(user_id!("@example:localhost"));
1679 let mut tags = Tags::new();
1680 tags.insert(TagName::Favorite, TagInfo::new());
1681 tags.insert(TagName::User(UserTagName::from_str("u.work").unwrap()), TagInfo::new());
1682 let raw_tag_event: Raw<AnyRoomAccountDataEvent> = f.tag(tags).into();
1683 let tag_event = raw_tag_event.deserialize().unwrap();
1684 changes.add_room_account_data(&room_info.room_id, tag_event, raw_tag_event);
1685
1686 let raw_pinned_events_event: Raw<_> = f
1687 .room_pinned_events(vec![owned_event_id!("$a"), owned_event_id!("$b")])
1688 .into_raw_sync_state();
1689 let pinned_events_event = raw_pinned_events_event.deserialize().unwrap();
1690 changes.add_state_event(&room_info.room_id, pinned_events_event, raw_pinned_events_event);
1691
1692 store.save_changes(&changes).await.unwrap();
1693
1694 room_info.data_format_version = 0;
1696 assert!(room_info.apply_migrations(store.clone()).await);
1697
1698 assert_eq!(room_info.data_format_version, 1);
1699 assert!(room_info.base_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
1700 assert!(room_info.base_info.pinned_events.is_some());
1701
1702 let new_room_info = RoomInfo::new(room_id!("!new_room:localhost"), RoomState::Joined);
1704 assert_eq!(new_room_info.data_format_version, 1);
1705 }
1706
1707 #[test]
1708 fn test_room_info_deserialization() {
1709 let info_json = json!({
1710 "room_id": "!gda78o:server.tld",
1711 "room_state": "Joined",
1712 "notification_counts": {
1713 "highlight_count": 1,
1714 "notification_count": 2,
1715 },
1716 "summary": {
1717 "room_heroes": [{
1718 "user_id": "@somebody:example.org",
1719 "display_name": "Somebody",
1720 "avatar_url": "mxc://example.org/abc"
1721 }],
1722 "joined_member_count": 5,
1723 "invited_member_count": 0,
1724 },
1725 "members_synced": true,
1726 "last_prev_batch": "pb",
1727 "sync_info": "FullySynced",
1728 "encryption_state_synced": true,
1729 "base_info": {
1730 "avatar": null,
1731 "canonical_alias": null,
1732 "create": null,
1733 "dm_targets": [],
1734 "encryption": null,
1735 "guest_access": null,
1736 "history_visibility": null,
1737 "join_rules": null,
1738 "max_power_level": 100,
1739 "member_hints": null,
1740 "name": null,
1741 "tombstone": null,
1742 "topic": null,
1743 },
1744 "cached_display_name": { "Calculated": "lol" },
1745 "cached_user_defined_notification_mode": "Mute",
1746 "recency_stamp": 42,
1747 });
1748
1749 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1750
1751 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1752 assert_eq!(info.room_state, RoomState::Joined);
1753 assert_eq!(info.notification_counts.highlight_count, 1);
1754 assert_eq!(info.notification_counts.notification_count, 2);
1755 assert_eq!(
1756 info.summary.room_heroes,
1757 vec![RoomHero {
1758 user_id: owned_user_id!("@somebody:example.org"),
1759 display_name: Some("Somebody".to_owned()),
1760 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1761 }]
1762 );
1763 assert_eq!(info.summary.joined_member_count, 5);
1764 assert_eq!(info.summary.invited_member_count, 0);
1765 assert!(info.members_synced);
1766 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1767 assert_eq!(info.sync_info, SyncInfo::FullySynced);
1768 assert!(info.encryption_state_synced);
1769 assert_matches!(info.latest_event_value, LatestEventValue::None);
1770 assert!(info.base_info.avatar.is_none());
1771 assert!(info.base_info.canonical_alias.is_none());
1772 assert!(info.base_info.create.is_none());
1773 assert_eq!(info.base_info.dm_targets.len(), 0);
1774 assert!(info.base_info.encryption.is_none());
1775 assert!(info.base_info.guest_access.is_none());
1776 assert!(info.base_info.history_visibility.is_none());
1777 assert!(info.base_info.join_rules.is_none());
1778 assert_eq!(info.base_info.max_power_level, 100);
1779 assert!(info.base_info.member_hints.is_none());
1780 assert!(info.base_info.name.is_none());
1781 assert!(info.base_info.tombstone.is_none());
1782 assert!(info.base_info.topic.is_none());
1783
1784 assert_eq!(
1785 info.cached_display_name.as_ref(),
1786 Some(&RoomDisplayName::Calculated("lol".to_owned())),
1787 );
1788 assert_eq!(
1789 info.cached_user_defined_notification_mode.as_ref(),
1790 Some(&RoomNotificationMode::Mute)
1791 );
1792 assert_eq!(info.recency_stamp.as_ref(), Some(&42.into()));
1793 }
1794
1795 #[test]
1802 fn test_room_info_deserialization_without_optional_items() {
1803 let info_json = json!({
1806 "room_id": "!gda78o:server.tld",
1807 "room_state": "Invited",
1808 "notification_counts": {
1809 "highlight_count": 1,
1810 "notification_count": 2,
1811 },
1812 "summary": {
1813 "room_heroes": [{
1814 "user_id": "@somebody:example.org",
1815 "display_name": "Somebody",
1816 "avatar_url": "mxc://example.org/abc"
1817 }],
1818 "joined_member_count": 5,
1819 "invited_member_count": 0,
1820 },
1821 "members_synced": true,
1822 "last_prev_batch": "pb",
1823 "sync_info": "FullySynced",
1824 "encryption_state_synced": true,
1825 "base_info": {
1826 "avatar": null,
1827 "canonical_alias": null,
1828 "create": null,
1829 "dm_targets": [],
1830 "encryption": null,
1831 "guest_access": null,
1832 "history_visibility": null,
1833 "join_rules": null,
1834 "max_power_level": 100,
1835 "name": null,
1836 "tombstone": null,
1837 "topic": null,
1838 },
1839 });
1840
1841 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1842
1843 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1844 assert_eq!(info.room_state, RoomState::Invited);
1845 assert_eq!(info.notification_counts.highlight_count, 1);
1846 assert_eq!(info.notification_counts.notification_count, 2);
1847 assert_eq!(
1848 info.summary.room_heroes,
1849 vec![RoomHero {
1850 user_id: owned_user_id!("@somebody:example.org"),
1851 display_name: Some("Somebody".to_owned()),
1852 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1853 }]
1854 );
1855 assert_eq!(info.summary.joined_member_count, 5);
1856 assert_eq!(info.summary.invited_member_count, 0);
1857 assert!(info.members_synced);
1858 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1859 assert_eq!(info.sync_info, SyncInfo::FullySynced);
1860 assert!(info.encryption_state_synced);
1861 assert!(info.base_info.avatar.is_none());
1862 assert!(info.base_info.canonical_alias.is_none());
1863 assert!(info.base_info.create.is_none());
1864 assert_eq!(info.base_info.dm_targets.len(), 0);
1865 assert!(info.base_info.encryption.is_none());
1866 assert!(info.base_info.guest_access.is_none());
1867 assert!(info.base_info.history_visibility.is_none());
1868 assert!(info.base_info.join_rules.is_none());
1869 assert_eq!(info.base_info.max_power_level, 100);
1870 assert!(info.base_info.name.is_none());
1871 assert!(info.base_info.tombstone.is_none());
1872 assert!(info.base_info.topic.is_none());
1873 }
1874
1875 #[test]
1876 fn test_member_hints_with_different_contents_reset_computed_value() {
1877 let expected = BTreeSet::from_iter([
1878 owned_user_id!("@alice:example.org"),
1879 owned_user_id!("@bob:example.org"),
1880 ]);
1881
1882 let info_json = json!({
1883 "room_id": "!gda78o:server.tld",
1884 "room_state": "Invited",
1885 "notification_counts": {
1886 "highlight_count": 1,
1887 "notification_count": 2,
1888 },
1889 "summary": {
1890 "room_heroes": [{
1891 "user_id": "@somebody:example.org",
1892 "display_name": "Somebody",
1893 "avatar_url": "mxc://example.org/abc"
1894 }],
1895 "joined_member_count": 5,
1896 "invited_member_count": 0,
1897 "active_service_members": 2,
1898 },
1899 "members_synced": true,
1900 "last_prev_batch": "pb",
1901 "sync_info": "FullySynced",
1902 "encryption_state_synced": true,
1903 "base_info": {
1904 "avatar": null,
1905 "canonical_alias": null,
1906 "create": null,
1907 "dm_targets": [],
1908 "encryption": null,
1909 "guest_access": null,
1910 "history_visibility": null,
1911 "join_rules": null,
1912 "max_power_level": 100,
1913 "member_hints": {
1914 "Original": {
1915 "content": {
1916 "service_members": ["@alice:example.org", "@bob:example.org"]
1917 }
1918 }
1919 },
1920 "name": null,
1921 "tombstone": null,
1922 "topic": null,
1923 },
1924 });
1925
1926 let info: RoomInfo = serde_json::from_value(info_json.clone()).unwrap();
1927 assert_eq!(info.base_info.member_hints.unwrap().content.service_members.unwrap(), expected);
1928 assert_eq!(info.summary.active_service_members, Some(2));
1929
1930 let mut info: RoomInfo = serde_json::from_value(info_json.clone()).unwrap();
1932 let mut raw_state_event_with_keys = RawStateEventWithKeys::try_from_raw_state_event(
1933 EventFactory::new()
1934 .sender(user_id!("@alice:example.org"))
1935 .member_hints(expected.clone())
1936 .into_raw_sync_state(),
1937 )
1938 .expect("Expected member hints event is created");
1939
1940 info.handle_state_event(&mut raw_state_event_with_keys);
1941
1942 assert_eq!(info.base_info.member_hints.unwrap().content.service_members.unwrap(), expected);
1944 assert_eq!(info.summary.active_service_members, Some(2));
1946
1947 let mut info: RoomInfo = serde_json::from_value(info_json).unwrap();
1949 let new_member_hints = BTreeSet::from_iter([owned_user_id!("@alice:example.org")]);
1950 let mut raw_state_event_with_keys = RawStateEventWithKeys::try_from_raw_state_event(
1951 EventFactory::new()
1952 .sender(user_id!("@alice:example.org"))
1953 .member_hints(new_member_hints.clone())
1954 .into_raw_sync_state(),
1955 )
1956 .expect("New member hints event is created");
1957
1958 info.handle_state_event(&mut raw_state_event_with_keys);
1959
1960 assert_eq!(
1962 info.base_info.member_hints.unwrap().content.service_members.unwrap(),
1963 new_member_hints
1964 );
1965 assert!(info.summary.active_service_members.is_none());
1967 }
1968
1969 fn make_room_and_state_store(room_state: RoomState) -> (Room, SaveLockedStateStore) {
1970 let state_store = SaveLockedStateStore::new(MemoryStore::new().into_state_store());
1971 let user_id = user_id!("@user:localhost");
1972 let room_id = room_id!("!room:localhost");
1973 let (sender, _) = tokio::sync::broadcast::channel(1);
1974 let room = Room::new(user_id, state_store.clone(), room_id, room_state, sender);
1975 (room, state_store)
1976 }
1977
1978 #[async_test]
1979 async fn test_update_room_info_only_updates_in_memory_room_info() {
1980 let (room, state_store) = make_room_and_state_store(RoomState::Joined);
1981
1982 let before = room.clone_info();
1983 assert_eq!(before.state(), RoomState::Joined);
1984 room.update_room_info(|mut info| {
1985 info.mark_as_banned();
1986 (info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
1987 })
1988 .await;
1989 let after = room.clone_info();
1990 assert_eq!(after.state(), RoomState::Banned);
1991
1992 let infos = state_store
1993 .get_room_infos(&RoomLoadSettings::One(room.room_id.clone()))
1994 .await
1995 .expect("get room info");
1996 assert!(infos.is_empty());
1997 }
1998
1999 #[async_test]
2000 async fn test_update_room_info_with_store_guard_only_updates_in_memory_room_info() {
2001 let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2002
2003 let before = room.clone_info();
2004 assert_eq!(before.state(), RoomState::Joined);
2005 room.update_room_info_with_store_guard(&state_store.lock().lock().await, |mut info| {
2006 info.mark_as_banned();
2007 (info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
2008 })
2009 .expect("update room info");
2010 let after = room.clone_info();
2011 assert_eq!(after.state(), RoomState::Banned);
2012
2013 let infos = state_store
2014 .get_room_infos(&RoomLoadSettings::One(room.room_id.clone()))
2015 .await
2016 .expect("get room info");
2017 assert!(infos.is_empty());
2018 }
2019
2020 #[async_test]
2021 async fn test_update_room_info_only_accepts_guard_for_underlying_mutex() {
2022 let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2023
2024 room.update_room_info_with_store_guard(&state_store.lock().lock().await, |info| {
2025 (info, RoomInfoNotableUpdateReasons::NONE)
2026 })
2027 .expect("room accepts guard for underlying mutex");
2028
2029 let mutex = Mutex::new(());
2030 room.update_room_info_with_store_guard(&mutex.lock().await, |info| {
2031 (info, RoomInfoNotableUpdateReasons::NONE)
2032 })
2033 .expect_err("room does not accept guard for unknown mutex");
2034 }
2035
2036 #[async_test]
2037 async fn test_update_and_save_room_info_updates_room_info_in_memory_and_store() {
2038 let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2039
2040 let before = room.clone_info();
2041 assert_eq!(before.state(), RoomState::Joined);
2042 room.update_and_save_room_info(|mut info| {
2043 info.mark_as_banned();
2044 (info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
2045 })
2046 .await
2047 .expect("update and save room info");
2048 let after = room.clone_info();
2049 assert_eq!(after.state(), RoomState::Banned);
2050
2051 let infos = state_store
2052 .get_room_infos(&RoomLoadSettings::One(room.room_id.clone()))
2053 .await
2054 .expect("get room info");
2055 assert_eq!(infos.len(), 1);
2056 assert_matches!(infos.first(), Some(info) => {
2057 info.state() == RoomState::Banned
2058 });
2059 }
2060
2061 #[async_test]
2062 async fn test_update_and_save_room_info_with_store_guard_updates_room_info_in_memory_and_store()
2063 {
2064 let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2065
2066 let before = room.clone_info();
2067 assert_eq!(before.state(), RoomState::Joined);
2068 room.update_and_save_room_info_with_store_guard(
2069 &state_store.lock().lock().await,
2070 |mut info| {
2071 info.mark_as_banned();
2072 (info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
2073 },
2074 )
2075 .await
2076 .expect("update and save room info");
2077 let after = room.clone_info();
2078 assert_eq!(after.state(), RoomState::Banned);
2079
2080 let infos = state_store
2081 .get_room_infos(&RoomLoadSettings::One(room.room_id.clone()))
2082 .await
2083 .expect("get room info");
2084 assert_eq!(infos.len(), 1);
2085 assert_matches!(infos.first(), Some(info) => {
2086 info.state() == RoomState::Banned
2087 });
2088 }
2089
2090 #[async_test]
2091 async fn test_update_and_save_room_info_only_accepts_guard_for_underlying_mutex() {
2092 let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2093
2094 room.update_and_save_room_info_with_store_guard(&state_store.lock().lock().await, |info| {
2095 (info, RoomInfoNotableUpdateReasons::NONE)
2096 })
2097 .await
2098 .expect("room accepts guard for underlying mutex");
2099
2100 let mutex = Mutex::new(());
2101 room.update_and_save_room_info_with_store_guard(&mutex.lock().await, |info| {
2102 (info, RoomInfoNotableUpdateReasons::NONE)
2103 })
2104 .await
2105 .expect_err("room does not accept guard for unknown mutex");
2106 }
2107
2108 #[derive(Debug)]
2109 struct Elapsed;
2110
2111 async fn timeout<F: Future + Unpin>(duration: Duration, f: F) -> Result<F::Output, Elapsed> {
2112 #[cfg(all(target_family = "wasm", target_os = "unknown"))]
2113 {
2114 match future::select(sleep(duration), f).await {
2115 Either::Left(_) => return Err(Elapsed),
2116 Either::Right((output, _)) => Ok(output),
2117 }
2118 }
2119 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
2120 {
2121 tokio::time::timeout(duration, f).await.map_err(|_| Elapsed)
2122 }
2123 }
2124
2125 #[async_test]
2126 async fn test_update_room_info_waits_to_acquire_lock_before_updating_room_info() {
2127 let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2128
2129 let lock_task = spawn({
2131 let state_store = state_store.clone();
2132 async move {
2133 let lock = state_store.lock();
2134 let _guard = lock.lock().await;
2135 sleep(Duration::from_secs(5)).await;
2136 }
2137 });
2138
2139 let save_task = spawn(async move {
2141 room.update_room_info(|info| (info, RoomInfoNotableUpdateReasons::NONE)).await
2142 });
2143
2144 assert_matches!(future::select(lock_task, save_task).await, Either::Left((_, save_task)) => {
2147 timeout(Duration::from_millis(100), save_task)
2148 .await
2149 .expect("task completes before timeout")
2150 .expect("task completes successfully")
2151 });
2152 }
2153
2154 #[async_test]
2155 async fn test_update_and_save_room_info_waits_to_acquire_lock_before_updating_room_info() {
2156 let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2157
2158 let lock_task = spawn({
2160 let state_store = state_store.clone();
2161 async move {
2162 let lock = state_store.lock();
2163 let _guard = lock.lock().await;
2164 sleep(Duration::from_secs(5)).await;
2165 }
2166 });
2167
2168 let save_task = spawn(async move {
2170 room.update_and_save_room_info(|info| (info, RoomInfoNotableUpdateReasons::NONE)).await
2171 });
2172
2173 assert_matches!(future::select(lock_task, save_task).await, Either::Left((_, save_task)) => {
2176 timeout(Duration::from_millis(100), save_task)
2177 .await
2178 .expect("task completes before timeout")
2179 .expect("task completes successfully")
2180 .expect("update and save room info");
2181 });
2182 }
2183}