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