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 pub(crate) pinned_events: Option<PossiblyRedactedRoomPinnedEventsEventContent>,
225}
226
227impl BaseRoomInfo {
228 pub fn new() -> Self {
230 Self::default()
231 }
232
233 pub fn room_version(&self) -> Option<&RoomVersionId> {
238 Some(&self.create.as_ref()?.content.room_version)
239 }
240
241 pub fn handle_state_event<T: AnyStateEventEnum>(
245 &mut self,
246 raw_event: &mut RawStateEventWithKeys<T>,
247 ) -> bool {
248 match (&raw_event.event_type, raw_event.state_key.as_str()) {
249 (StateEventType::RoomEncryption, "") => {
250 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
254 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomEncryption)
255 }) && event.content.algorithm.is_some()
256 {
257 self.encryption = Some(event.content);
258 true
259 } else {
260 false
261 }
262 }
263 (StateEventType::RoomAvatar, "") => {
264 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
265 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomAvatar)
266 }) {
267 self.avatar = Some(event);
268 true
269 } else {
270 self.avatar.take().is_some()
272 }
273 }
274 (StateEventType::RoomName, "") => {
275 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
276 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomName)
277 }) {
278 self.name = Some(event);
279 true
280 } else {
281 self.name.take().is_some()
283 }
284 }
285 (StateEventType::RoomCreate, "") if self.create.is_none() => {
287 if let Some(any_event) = raw_event.deserialize()
288 && let Some(content) = as_variant!(
289 any_event.get_content(),
290 AnyPossiblyRedactedStateEventContent::RoomCreate
291 )
292 {
293 self.create = Some(MinimalStateEvent {
294 content: RoomCreateWithCreatorEventContent::from_event_content(
295 content,
296 any_event.get_sender().to_owned(),
297 ),
298 event_id: any_event.get_event_id().map(ToOwned::to_owned),
299 });
300 true
301 } else {
302 false
303 }
304 }
305 (StateEventType::RoomHistoryVisibility, "") => {
306 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
307 as_variant!(
308 any_event,
309 AnyPossiblyRedactedStateEventContent::RoomHistoryVisibility
310 )
311 }) {
312 self.history_visibility = Some(event);
313 true
314 } else {
315 self.history_visibility.take().is_some()
317 }
318 }
319 (StateEventType::RoomGuestAccess, "") => {
320 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
321 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomGuestAccess)
322 }) {
323 self.guest_access = Some(event);
324 true
325 } else {
326 self.guest_access.take().is_some()
328 }
329 }
330 (StateEventType::MemberHints, "") => {
331 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
332 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::MemberHints)
333 }) {
334 self.member_hints = Some(event);
335 true
336 } else {
337 self.member_hints.take().is_some()
339 }
340 }
341 (StateEventType::RoomJoinRules, "") => {
342 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
343 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomJoinRules)
344 }) {
345 match &event.content.join_rule {
346 JoinRule::Invite
347 | JoinRule::Knock
348 | JoinRule::Private
349 | JoinRule::Restricted(_)
350 | JoinRule::KnockRestricted(_)
351 | JoinRule::Public => {
352 self.join_rules = Some(event);
353 true
354 }
355 r => {
356 warn!(join_rule = ?r.as_str(), "Encountered a custom join rule, skipping");
357 self.join_rules.take().is_some()
359 }
360 }
361 } else {
362 self.join_rules.take().is_some()
364 }
365 }
366 (StateEventType::RoomCanonicalAlias, "") => {
367 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
368 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomCanonicalAlias)
369 }) {
370 self.canonical_alias = Some(event);
371 true
372 } else {
373 self.canonical_alias.take().is_some()
375 }
376 }
377 (StateEventType::RoomTopic, "") => {
378 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
379 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomTopic)
380 }) {
381 self.topic = Some(event);
382 true
383 } else {
384 self.topic.take().is_some()
386 }
387 }
388 (StateEventType::RoomTombstone, "") => {
389 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
390 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomTombstone)
391 }) {
392 self.tombstone = Some(event);
393 true
394 } else {
395 self.tombstone.take().is_some()
397 }
398 }
399 (StateEventType::RoomPowerLevels, "") => {
400 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
401 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomPowerLevels)
402 }) {
403 let new_max = i64::from(
404 event
405 .content
406 .users
407 .values()
408 .fold(event.content.users_default, |max_pl, user_pl| {
409 max_pl.max(*user_pl)
410 }),
411 );
412
413 if self.max_power_level != new_max {
414 self.max_power_level = new_max;
415 true
416 } else {
417 false
418 }
419 } else if self.max_power_level != DEFAULT_MAX_POWER_LEVEL {
420 self.max_power_level = DEFAULT_MAX_POWER_LEVEL;
422 true
423 } else {
424 false
425 }
426 }
427 (StateEventType::CallMember, _) => {
428 if let Ok(call_member_key) = raw_event.state_key.parse::<CallMemberStateKey>() {
429 if let Some(any_event) = raw_event.deserialize()
430 && let Some(content) = as_variant!(
431 any_event.get_content(),
432 AnyPossiblyRedactedStateEventContent::CallMember
433 )
434 {
435 let mut event = MinimalStateEvent {
436 content,
437 event_id: any_event.get_event_id().map(ToOwned::to_owned),
438 };
439
440 if let Some(origin_server_ts) = any_event.get_origin_server_ts() {
441 event.content.set_created_ts_if_none(origin_server_ts);
442 }
443
444 self.rtc_member_events.insert(call_member_key, event);
446
447 self.rtc_member_events
449 .retain(|_, ev| !ev.content.active_memberships(None).is_empty());
450
451 true
452 } else {
453 self.rtc_member_events.remove(&call_member_key).is_some()
456 }
457 } else {
458 false
459 }
460 }
461 (StateEventType::RoomPinnedEvents, "") => {
462 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
463 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomPinnedEvents)
464 }) {
465 self.pinned_events = Some(event.content);
466 true
467 } else {
468 self.pinned_events.take().is_some()
470 }
471 }
472 _ => false,
473 }
474 }
475
476 pub(super) fn handle_redaction(&mut self, redacts: &EventId) {
477 let redaction_rules = self
478 .room_version()
479 .and_then(|room_version| room_version.rules())
480 .unwrap_or(ROOM_VERSION_RULES_FALLBACK)
481 .redaction;
482
483 if let Some(ev) = &mut self.avatar
484 && ev.event_id.as_deref() == Some(redacts)
485 {
486 ev.redact(&redaction_rules);
487 } else if let Some(ev) = &mut self.canonical_alias
488 && ev.event_id.as_deref() == Some(redacts)
489 {
490 ev.redact(&redaction_rules);
491 } else if let Some(ev) = &mut self.create
492 && ev.event_id.as_deref() == Some(redacts)
493 {
494 ev.redact(&redaction_rules);
495 } else if let Some(ev) = &mut self.guest_access
496 && ev.event_id.as_deref() == Some(redacts)
497 {
498 ev.redact(&redaction_rules);
499 } else if let Some(ev) = &mut self.history_visibility
500 && ev.event_id.as_deref() == Some(redacts)
501 {
502 ev.redact(&redaction_rules);
503 } else if let Some(ev) = &mut self.join_rules
504 && ev.event_id.as_deref() == Some(redacts)
505 {
506 ev.redact(&redaction_rules);
507 } else if let Some(ev) = &mut self.name
508 && ev.event_id.as_deref() == Some(redacts)
509 {
510 ev.redact(&redaction_rules);
511 } else if let Some(ev) = &mut self.tombstone
512 && ev.event_id.as_deref() == Some(redacts)
513 {
514 ev.redact(&redaction_rules);
515 } else if let Some(ev) = &mut self.topic
516 && ev.event_id.as_deref() == Some(redacts)
517 {
518 ev.redact(&redaction_rules);
519 } else {
520 self.rtc_member_events
521 .retain(|_, member_event| member_event.event_id.as_deref() != Some(redacts));
522 }
523 }
524
525 pub fn handle_notable_tags(&mut self, tags: &Tags) {
526 let mut notable_tags = RoomNotableTags::empty();
527
528 if tags.contains_key(&TagName::Favorite) {
529 notable_tags.insert(RoomNotableTags::FAVOURITE);
530 }
531
532 if tags.contains_key(&TagName::LowPriority) {
533 notable_tags.insert(RoomNotableTags::LOW_PRIORITY);
534 }
535
536 self.notable_tags = notable_tags;
537 }
538}
539
540impl Default for BaseRoomInfo {
541 fn default() -> Self {
542 Self {
543 avatar: None,
544 canonical_alias: None,
545 create: None,
546 dm_targets: Default::default(),
547 member_hints: None,
548 encryption: None,
549 guest_access: None,
550 history_visibility: None,
551 join_rules: None,
552 max_power_level: DEFAULT_MAX_POWER_LEVEL,
553 name: None,
554 tombstone: None,
555 topic: None,
556 rtc_member_events: BTreeMap::new(),
557 is_marked_unread: false,
558 is_marked_unread_source: AccountDataSource::Unstable,
559 notable_tags: RoomNotableTags::empty(),
560 pinned_events: None,
561 }
562 }
563}
564
565#[derive(Clone, Debug, Serialize, Deserialize)]
569pub struct RoomInfo {
570 #[serde(default, alias = "version")]
573 pub(crate) data_format_version: u8,
574
575 pub(crate) room_id: OwnedRoomId,
577
578 pub(crate) room_state: RoomState,
580
581 pub(crate) notification_counts: UnreadNotificationsCount,
586
587 pub(crate) summary: RoomSummary,
589
590 pub(crate) members_synced: bool,
592
593 pub(crate) last_prev_batch: Option<String>,
595
596 pub(crate) sync_info: SyncInfo,
598
599 pub(crate) encryption_state_synced: bool,
601
602 #[serde(default)]
604 pub(crate) latest_event_value: LatestEventValue,
605
606 #[serde(default)]
608 pub(crate) read_receipts: RoomReadReceipts,
609
610 pub(crate) base_info: Box<BaseRoomInfo>,
613
614 #[serde(skip)]
618 pub(crate) warned_about_unknown_room_version_rules: Arc<AtomicBool>,
619
620 #[serde(default, skip_serializing_if = "Option::is_none")]
625 pub(crate) cached_display_name: Option<RoomDisplayName>,
626
627 #[serde(default, skip_serializing_if = "Option::is_none")]
629 pub(crate) cached_user_defined_notification_mode: Option<RoomNotificationMode>,
630
631 #[serde(default)]
648 pub(crate) recency_stamp: Option<RoomRecencyStamp>,
649}
650
651impl RoomInfo {
652 #[doc(hidden)] pub fn new(room_id: &RoomId, room_state: RoomState) -> Self {
654 Self {
655 data_format_version: 1,
656 room_id: room_id.into(),
657 room_state,
658 notification_counts: Default::default(),
659 summary: Default::default(),
660 members_synced: false,
661 last_prev_batch: None,
662 sync_info: SyncInfo::NoState,
663 encryption_state_synced: false,
664 latest_event_value: LatestEventValue::default(),
665 read_receipts: Default::default(),
666 base_info: Box::new(BaseRoomInfo::new()),
667 warned_about_unknown_room_version_rules: Arc::new(false.into()),
668 cached_display_name: None,
669 cached_user_defined_notification_mode: None,
670 recency_stamp: None,
671 }
672 }
673
674 pub fn mark_as_joined(&mut self) {
676 self.set_state(RoomState::Joined);
677 }
678
679 pub fn mark_as_left(&mut self) {
681 self.set_state(RoomState::Left);
682 }
683
684 pub fn mark_as_invited(&mut self) {
686 self.set_state(RoomState::Invited);
687 }
688
689 pub fn mark_as_knocked(&mut self) {
691 self.set_state(RoomState::Knocked);
692 }
693
694 pub fn mark_as_banned(&mut self) {
696 self.set_state(RoomState::Banned);
697 }
698
699 pub fn set_state(&mut self, room_state: RoomState) {
701 self.room_state = room_state;
702 }
703
704 pub fn mark_members_synced(&mut self) {
706 self.members_synced = true;
707 }
708
709 pub fn mark_members_missing(&mut self) {
711 self.members_synced = false;
712 }
713
714 pub fn are_members_synced(&self) -> bool {
716 self.members_synced
717 }
718
719 pub fn mark_state_partially_synced(&mut self) {
721 self.sync_info = SyncInfo::PartiallySynced;
722 }
723
724 pub fn mark_state_fully_synced(&mut self) {
726 self.sync_info = SyncInfo::FullySynced;
727 }
728
729 pub fn mark_state_not_synced(&mut self) {
731 self.sync_info = SyncInfo::NoState;
732 }
733
734 pub fn mark_encryption_state_synced(&mut self) {
736 self.encryption_state_synced = true;
737 }
738
739 pub fn mark_encryption_state_missing(&mut self) {
741 self.encryption_state_synced = false;
742 }
743
744 pub fn set_prev_batch(&mut self, prev_batch: Option<&str>) -> bool {
748 if self.last_prev_batch.as_deref() != prev_batch {
749 self.last_prev_batch = prev_batch.map(|p| p.to_owned());
750 true
751 } else {
752 false
753 }
754 }
755
756 pub fn state(&self) -> RoomState {
758 self.room_state
759 }
760
761 #[cfg(not(feature = "experimental-encrypted-state-events"))]
763 pub fn encryption_state(&self) -> EncryptionState {
764 if !self.encryption_state_synced {
765 EncryptionState::Unknown
766 } else if self.base_info.encryption.is_some() {
767 EncryptionState::Encrypted
768 } else {
769 EncryptionState::NotEncrypted
770 }
771 }
772
773 #[cfg(feature = "experimental-encrypted-state-events")]
775 pub fn encryption_state(&self) -> EncryptionState {
776 if !self.encryption_state_synced {
777 EncryptionState::Unknown
778 } else {
779 self.base_info
780 .encryption
781 .as_ref()
782 .map(|state| {
783 if state.encrypt_state_events {
784 EncryptionState::StateEncrypted
785 } else {
786 EncryptionState::Encrypted
787 }
788 })
789 .unwrap_or(EncryptionState::NotEncrypted)
790 }
791 }
792
793 pub fn set_encryption_event(
795 &mut self,
796 event: Option<PossiblyRedactedRoomEncryptionEventContent>,
797 ) {
798 self.base_info.encryption = event;
799 }
800
801 pub fn handle_encryption_state(
803 &mut self,
804 requested_required_states: &[(StateEventType, String)],
805 ) {
806 if requested_required_states
807 .iter()
808 .any(|(state_event, _)| state_event == &StateEventType::RoomEncryption)
809 {
810 self.mark_encryption_state_synced();
816 }
817 }
818
819 pub fn handle_state_event(
823 &mut self,
824 raw_event: &mut RawStateEventWithKeys<AnySyncStateEvent>,
825 ) -> bool {
826 if raw_event.event_type == StateEventType::MemberHints
828 && let Some(AnySyncStateEvent::MemberHints(new_hints)) = raw_event.deserialize()
829 && let (Some(current_hints), Some(new)) =
831 (&self.base_info.member_hints, new_hints.as_original())
832 && current_hints
834 .content
835 .service_members
836 .as_ref()
837 .is_some_and(|current_members| *current_members != new.content.service_members)
838 {
839 self.summary.active_service_members = None;
841 }
842
843 let base_info_has_been_modified = self.base_info.handle_state_event(raw_event);
845
846 if raw_event.event_type == StateEventType::RoomEncryption && raw_event.state_key.is_empty()
847 {
848 self.mark_encryption_state_synced();
854 }
855
856 base_info_has_been_modified
857 }
858
859 pub fn handle_stripped_state_event(
863 &mut self,
864 raw_event: &mut RawStateEventWithKeys<AnyStrippedStateEvent>,
865 ) -> bool {
866 self.base_info.handle_state_event(raw_event)
867 }
868
869 #[instrument(skip_all, fields(redacts))]
871 pub fn handle_redaction(
872 &mut self,
873 event: &SyncRoomRedactionEvent,
874 _raw: &Raw<SyncRoomRedactionEvent>,
875 ) {
876 let redaction_rules = self.room_version_rules_or_default().redaction;
877
878 let Some(redacts) = event.redacts(&redaction_rules) else {
879 info!("Can't apply redaction, redacts field is missing");
880 return;
881 };
882 tracing::Span::current().record("redacts", debug(redacts));
883
884 self.base_info.handle_redaction(redacts);
885 }
886
887 pub fn avatar_url(&self) -> Option<&MxcUri> {
889 self.base_info.avatar.as_ref().and_then(|e| e.content.url.as_deref())
890 }
891
892 pub fn update_avatar(&mut self, url: Option<OwnedMxcUri>) {
894 self.base_info.avatar = url.map(|url| {
895 let mut content = PossiblyRedactedRoomAvatarEventContent::new();
896 content.url = Some(url);
897
898 MinimalStateEvent { content, event_id: None }
899 });
900 }
901
902 pub fn avatar_info(&self) -> Option<&avatar::ImageInfo> {
904 self.base_info.avatar.as_ref().and_then(|e| e.content.info.as_deref())
905 }
906
907 pub fn update_notification_count(&mut self, notification_counts: UnreadNotificationsCount) {
909 self.notification_counts = notification_counts;
910 }
911
912 pub fn update_from_ruma_summary(&mut self, summary: &RumaSummary) -> bool {
916 let mut changed = false;
917
918 if !summary.is_empty() {
919 if !summary.heroes.is_empty() {
920 self.summary.room_heroes = summary
921 .heroes
922 .iter()
923 .map(|hero_id| RoomHero {
924 user_id: hero_id.to_owned(),
925 display_name: None,
926 avatar_url: None,
927 })
928 .collect();
929
930 changed = true;
931 }
932
933 if let Some(joined) = summary.joined_member_count {
934 self.summary.joined_member_count = joined.into();
935 changed = true;
936 }
937
938 if let Some(invited) = summary.invited_member_count {
939 self.summary.invited_member_count = invited.into();
940 changed = true;
941 }
942 }
943
944 if changed {
945 self.summary.active_service_members = None;
946 }
947
948 changed
949 }
950
951 pub(crate) fn update_joined_member_count(&mut self, count: u64) {
953 self.summary.joined_member_count = count;
954 }
955
956 pub(crate) fn update_invited_member_count(&mut self, count: u64) {
958 self.summary.invited_member_count = count;
959 }
960
961 pub(crate) fn update_heroes(&mut self, heroes: Vec<RoomHero>) {
963 self.summary.room_heroes = heroes;
964 }
965
966 pub fn heroes(&self) -> &[RoomHero] {
968 &self.summary.room_heroes
969 }
970
971 pub fn active_members_count(&self) -> u64 {
975 self.summary.joined_member_count.saturating_add(self.summary.invited_member_count)
976 }
977
978 pub fn invited_members_count(&self) -> u64 {
980 self.summary.invited_member_count
981 }
982
983 pub fn joined_members_count(&self) -> u64 {
985 self.summary.joined_member_count
986 }
987
988 pub fn canonical_alias(&self) -> Option<&RoomAliasId> {
990 self.base_info.canonical_alias.as_ref()?.content.alias.as_deref()
991 }
992
993 pub fn alt_aliases(&self) -> &[OwnedRoomAliasId] {
995 self.base_info
996 .canonical_alias
997 .as_ref()
998 .map(|ev| ev.content.alt_aliases.as_ref())
999 .unwrap_or_default()
1000 }
1001
1002 pub fn room_id(&self) -> &RoomId {
1004 &self.room_id
1005 }
1006
1007 pub fn room_version(&self) -> Option<&RoomVersionId> {
1009 self.base_info.room_version()
1010 }
1011
1012 pub fn room_version_rules_or_default(&self) -> RoomVersionRules {
1017 use std::sync::atomic::Ordering;
1018
1019 self.base_info.room_version().and_then(|room_version| room_version.rules()).unwrap_or_else(
1020 || {
1021 if self
1022 .warned_about_unknown_room_version_rules
1023 .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
1024 .is_ok()
1025 {
1026 warn!("Unable to get the room version rules, defaulting to rules for room version {ROOM_VERSION_FALLBACK}");
1027 }
1028
1029 ROOM_VERSION_RULES_FALLBACK
1030 },
1031 )
1032 }
1033
1034 pub fn room_type(&self) -> Option<&RoomType> {
1036 self.base_info.create.as_ref()?.content.room_type.as_ref()
1037 }
1038
1039 pub fn creators(&self) -> Option<Vec<OwnedUserId>> {
1041 Some(self.base_info.create.as_ref()?.content.creators())
1042 }
1043
1044 pub(super) fn guest_access(&self) -> &GuestAccess {
1045 self.base_info
1046 .guest_access
1047 .as_ref()
1048 .and_then(|event| event.content.guest_access.as_ref())
1049 .unwrap_or(&GuestAccess::Forbidden)
1050 }
1051
1052 pub fn history_visibility(&self) -> Option<&HistoryVisibility> {
1056 Some(&self.base_info.history_visibility.as_ref()?.content.history_visibility)
1057 }
1058
1059 pub fn history_visibility_or_default(&self) -> &HistoryVisibility {
1066 self.history_visibility().unwrap_or(&HistoryVisibility::Shared)
1067 }
1068
1069 pub fn join_rule(&self) -> Option<&JoinRule> {
1072 Some(&self.base_info.join_rules.as_ref()?.content.join_rule)
1073 }
1074
1075 pub fn service_members(&self) -> Option<&BTreeSet<OwnedUserId>> {
1078 self.base_info.member_hints.as_ref()?.content.service_members.as_ref()
1079 }
1080
1081 pub fn name(&self) -> Option<&str> {
1083 self.base_info.name.as_ref()?.content.name.as_deref().filter(|name| !name.is_empty())
1084 }
1085
1086 pub fn create(&self) -> Option<&RoomCreateWithCreatorEventContent> {
1088 Some(&self.base_info.create.as_ref()?.content)
1089 }
1090
1091 pub fn tombstone(&self) -> Option<&PossiblyRedactedRoomTombstoneEventContent> {
1093 Some(&self.base_info.tombstone.as_ref()?.content)
1094 }
1095
1096 pub fn topic(&self) -> Option<&str> {
1098 self.base_info.topic.as_ref()?.content.topic.as_deref()
1099 }
1100
1101 fn active_matrix_rtc_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
1106 let mut v = self
1107 .base_info
1108 .rtc_member_events
1109 .iter()
1110 .flat_map(|(state_key, ev)| {
1111 ev.content.active_memberships(None).into_iter().map(move |m| (state_key.clone(), m))
1112 })
1113 .collect::<Vec<_>>();
1114 v.sort_by_key(|(_, m)| m.created_ts());
1115 v
1116 }
1117
1118 fn active_room_call_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
1124 self.active_matrix_rtc_memberships()
1125 .into_iter()
1126 .filter(|(_user_id, m)| m.is_room_call())
1127 .collect()
1128 }
1129
1130 pub fn has_active_room_call(&self) -> bool {
1133 !self.active_room_call_memberships().is_empty()
1134 }
1135
1136 pub fn active_room_call_consensus_intent(&self) -> CallIntentConsensus {
1153 let memberships = self.active_room_call_memberships();
1154 let total_count: u64 = memberships.len() as u64;
1155
1156 if total_count == 0 {
1157 return CallIntentConsensus::None;
1158 }
1159
1160 let mut consensus_intent: Option<CallIntent> = None;
1162 let mut agreeing_count: u64 = 0;
1163
1164 for (_, data) in memberships.iter() {
1165 if let Some(intent) = data.call_intent() {
1166 match &consensus_intent {
1167 None => {
1169 consensus_intent = Some(intent.clone());
1170 agreeing_count = 1;
1171 }
1172 Some(current) if current == intent => {
1174 agreeing_count += 1;
1175 }
1176 Some(_) => return CallIntentConsensus::None,
1178 }
1179 }
1180 }
1181
1182 match consensus_intent {
1184 None => CallIntentConsensus::None,
1185 Some(intent) if agreeing_count == total_count => {
1186 CallIntentConsensus::Full(intent)
1188 }
1189 Some(intent) => {
1190 CallIntentConsensus::Partial { intent, agreeing_count, total_count }
1192 }
1193 }
1194 }
1195
1196 pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
1205 self.active_room_call_memberships()
1206 .iter()
1207 .map(|(call_member_state_key, _)| call_member_state_key.user_id().to_owned())
1208 .collect()
1209 }
1210
1211 pub fn set_latest_event(&mut self, new_value: LatestEventValue) {
1213 self.latest_event_value = new_value;
1214 }
1215
1216 pub fn update_recency_stamp(&mut self, stamp: RoomRecencyStamp) {
1220 self.recency_stamp = Some(stamp);
1221 }
1222
1223 pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
1225 self.base_info.pinned_events.clone().and_then(|c| c.pinned)
1226 }
1227
1228 pub fn is_pinned_event(&self, event_id: &EventId) -> bool {
1234 self.base_info
1235 .pinned_events
1236 .as_ref()
1237 .and_then(|content| content.pinned.as_deref())
1238 .is_some_and(|pinned| pinned.contains(&event_id.to_owned()))
1239 }
1240
1241 pub fn read_receipts(&self) -> &RoomReadReceipts {
1243 &self.read_receipts
1244 }
1245
1246 pub fn set_read_receipts(&mut self, read_receipts: RoomReadReceipts) {
1248 self.read_receipts = read_receipts;
1249 }
1250
1251 #[instrument(skip_all, fields(room_id = ?self.room_id))]
1259 pub(crate) async fn apply_migrations(&mut self, store: SaveLockedStateStore) -> bool {
1260 let mut migrated = false;
1261
1262 if self.data_format_version < 1 {
1263 info!("Migrating room info to version 1");
1264
1265 match store.get_room_account_data_event_static::<TagEventContent>(&self.room_id).await {
1267 Ok(Some(raw_event)) => match raw_event.deserialize() {
1269 Ok(event) => {
1270 self.base_info.handle_notable_tags(&event.content.tags);
1271 }
1272 Err(error) => {
1273 warn!("Failed to deserialize room tags: {error}");
1274 }
1275 },
1276 Ok(_) => {
1277 }
1279 Err(error) => {
1280 warn!("Failed to load room tags: {error}");
1281 }
1282 }
1283
1284 match store.get_state_event_static::<RoomPinnedEventsEventContent>(&self.room_id).await
1286 {
1287 Ok(Some(RawSyncOrStrippedState::Sync(raw_event))) => {
1289 if let Some(mut raw_event) =
1290 RawStateEventWithKeys::try_from_raw_state_event(raw_event.cast())
1291 {
1292 self.handle_state_event(&mut raw_event);
1293 }
1294 }
1295 Ok(_) => {
1296 }
1298 Err(error) => {
1299 warn!("Failed to load room pinned events: {error}");
1300 }
1301 }
1302
1303 self.data_format_version = 1;
1304 migrated = true;
1305 }
1306
1307 migrated
1308 }
1309
1310 pub fn active_service_member_count(&self) -> Option<u64> {
1313 self.summary.active_service_members
1314 }
1315
1316 pub fn update_active_service_member_count(&mut self, count: Option<u64>) {
1319 self.summary.active_service_members = count;
1320 }
1321}
1322
1323#[repr(transparent)]
1325#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
1326#[serde(transparent)]
1327pub struct RoomRecencyStamp(u64);
1328
1329impl From<u64> for RoomRecencyStamp {
1330 fn from(value: u64) -> Self {
1331 Self(value)
1332 }
1333}
1334
1335impl From<RoomRecencyStamp> for u64 {
1336 fn from(value: RoomRecencyStamp) -> Self {
1337 value.0
1338 }
1339}
1340
1341#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1342pub(crate) enum SyncInfo {
1343 NoState,
1349
1350 PartiallySynced,
1353
1354 FullySynced,
1356}
1357
1358pub fn apply_redaction(
1361 event: &Raw<AnySyncTimelineEvent>,
1362 raw_redaction: &Raw<SyncRoomRedactionEvent>,
1363 rules: &RedactionRules,
1364) -> Option<Raw<AnySyncTimelineEvent>> {
1365 use ruma::canonical_json::{RedactedBecause, redact_in_place};
1366
1367 let mut event_json = match event.deserialize_as() {
1368 Ok(json) => json,
1369 Err(e) => {
1370 warn!("Failed to deserialize latest event: {e}");
1371 return None;
1372 }
1373 };
1374
1375 let redacted_because = match RedactedBecause::from_raw_event(raw_redaction) {
1376 Ok(rb) => rb,
1377 Err(e) => {
1378 warn!("Redaction event is not valid canonical JSON: {e}");
1379 return None;
1380 }
1381 };
1382
1383 let redact_result = redact_in_place(&mut event_json, rules, Some(redacted_because));
1384
1385 if let Err(e) = redact_result {
1386 warn!("Failed to redact event: {e}");
1387 return None;
1388 }
1389
1390 let raw = Raw::new(&event_json).expect("CanonicalJsonObject must be serializable");
1391 Some(raw.cast_unchecked())
1392}
1393
1394#[derive(Debug, Clone)]
1404pub struct RoomInfoNotableUpdate {
1405 pub room_id: OwnedRoomId,
1407
1408 pub reasons: RoomInfoNotableUpdateReasons,
1410}
1411
1412bitflags! {
1413 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1415 pub struct RoomInfoNotableUpdateReasons: u8 {
1416 const RECENCY_STAMP = 0b0000_0001;
1418
1419 const LATEST_EVENT = 0b0000_0010;
1421
1422 const READ_RECEIPT = 0b0000_0100;
1424
1425 const UNREAD_MARKER = 0b0000_1000;
1427
1428 const MEMBERSHIP = 0b0001_0000;
1430
1431 const DISPLAY_NAME = 0b0010_0000;
1433
1434 const ACTIVE_SERVICE_MEMBERS = 0b0100_0000;
1436
1437 const NONE = 0b1000_0000;
1448 }
1449}
1450
1451impl Default for RoomInfoNotableUpdateReasons {
1452 fn default() -> Self {
1453 Self::empty()
1454 }
1455}
1456
1457#[cfg(test)]
1458mod tests {
1459 use std::{collections::BTreeSet, str::FromStr, sync::Arc, time::Duration};
1460
1461 use assert_matches::assert_matches;
1462 use futures_util::future::{self, Either};
1463 #[cfg(all(target_family = "wasm", target_os = "unknown"))]
1464 use gloo_timers::future::sleep;
1465 use matrix_sdk_common::executor::spawn;
1466 use matrix_sdk_test::{async_test, event_factory::EventFactory};
1467 use ruma::{
1468 assign,
1469 events::{
1470 AnyRoomAccountDataEvent,
1471 room::pinned_events::RoomPinnedEventsEventContent,
1472 tag::{TagInfo, TagName, Tags, UserTagName},
1473 },
1474 owned_event_id, owned_mxc_uri, owned_user_id, room_id,
1475 serde::Raw,
1476 user_id,
1477 };
1478 use serde_json::json;
1479 use similar_asserts::assert_eq;
1480 use tokio::sync::Mutex;
1481 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
1482 use tokio::time::sleep;
1483
1484 use super::{BaseRoomInfo, LatestEventValue, RoomInfo, SyncInfo};
1485 use crate::{
1486 RawStateEventWithKeys, Room, RoomDisplayName, RoomHero, RoomInfoNotableUpdateReasons,
1487 RoomState, StateChanges, StateStore,
1488 notification_settings::RoomNotificationMode,
1489 room::{RoomNotableTags, RoomSummary},
1490 store::{IntoStateStore, MemoryStore, RoomLoadSettings, SaveLockedStateStore},
1491 sync::UnreadNotificationsCount,
1492 };
1493
1494 #[test]
1495 fn test_room_info_serialization() {
1496 let info = RoomInfo {
1500 data_format_version: 1,
1501 room_id: room_id!("!gda78o:server.tld").into(),
1502 room_state: RoomState::Invited,
1503 notification_counts: UnreadNotificationsCount {
1504 highlight_count: 1,
1505 notification_count: 2,
1506 },
1507 summary: RoomSummary {
1508 room_heroes: vec![RoomHero {
1509 user_id: owned_user_id!("@somebody:example.org"),
1510 display_name: None,
1511 avatar_url: None,
1512 }],
1513 joined_member_count: 5,
1514 invited_member_count: 0,
1515 active_service_members: None,
1516 },
1517 members_synced: true,
1518 last_prev_batch: Some("pb".to_owned()),
1519 sync_info: SyncInfo::FullySynced,
1520 encryption_state_synced: true,
1521 latest_event_value: LatestEventValue::None,
1522 base_info: Box::new(
1523 assign!(BaseRoomInfo::new(), { pinned_events: Some(RoomPinnedEventsEventContent::new(vec![owned_event_id!("$a")]).into()) }),
1524 ),
1525 read_receipts: Default::default(),
1526 warned_about_unknown_room_version_rules: Arc::new(false.into()),
1527 cached_display_name: None,
1528 cached_user_defined_notification_mode: None,
1529 recency_stamp: Some(42.into()),
1530 };
1531
1532 let info_json = json!({
1533 "data_format_version": 1,
1534 "room_id": "!gda78o:server.tld",
1535 "room_state": "Invited",
1536 "notification_counts": {
1537 "highlight_count": 1,
1538 "notification_count": 2,
1539 },
1540 "summary": {
1541 "room_heroes": [{
1542 "user_id": "@somebody:example.org",
1543 "display_name": null,
1544 "avatar_url": null
1545 }],
1546 "joined_member_count": 5,
1547 "invited_member_count": 0,
1548 },
1549 "members_synced": true,
1550 "last_prev_batch": "pb",
1551 "sync_info": "FullySynced",
1552 "encryption_state_synced": true,
1553 "latest_event_value": "None",
1554 "base_info": {
1555 "avatar": null,
1556 "canonical_alias": null,
1557 "create": null,
1558 "dm_targets": [],
1559 "encryption": null,
1560 "guest_access": null,
1561 "history_visibility": null,
1562 "is_marked_unread": false,
1563 "is_marked_unread_source": "Unstable",
1564 "join_rules": null,
1565 "max_power_level": 100,
1566 "member_hints": null,
1567 "name": null,
1568 "tombstone": null,
1569 "topic": null,
1570 "pinned_events": {
1571 "pinned": ["$a"]
1572 },
1573 },
1574 "read_receipts": {
1575 "num_unread": 0,
1576 "num_mentions": 0,
1577 "num_notifications": 0,
1578 "latest_active": null,
1579 "pending": [],
1580 },
1581 "recency_stamp": 42,
1582 });
1583
1584 assert_eq!(serde_json::to_value(info).unwrap(), info_json);
1585 }
1586
1587 #[async_test]
1588 async fn test_room_info_migration_v1() {
1589 let store = SaveLockedStateStore::new(MemoryStore::new().into_state_store());
1590
1591 let room_info_json = json!({
1592 "room_id": "!gda78o:server.tld",
1593 "room_state": "Joined",
1594 "notification_counts": {
1595 "highlight_count": 1,
1596 "notification_count": 2,
1597 },
1598 "summary": {
1599 "room_heroes": [{
1600 "user_id": "@somebody:example.org",
1601 "display_name": null,
1602 "avatar_url": null
1603 }],
1604 "joined_member_count": 5,
1605 "invited_member_count": 0,
1606 },
1607 "members_synced": true,
1608 "last_prev_batch": "pb",
1609 "sync_info": "FullySynced",
1610 "encryption_state_synced": true,
1611 "latest_event": {
1612 "event": {
1613 "encryption_info": null,
1614 "event": {
1615 "sender": "@u:i.uk",
1616 },
1617 },
1618 },
1619 "base_info": {
1620 "avatar": null,
1621 "canonical_alias": null,
1622 "create": null,
1623 "dm_targets": [],
1624 "encryption": null,
1625 "guest_access": null,
1626 "history_visibility": null,
1627 "join_rules": null,
1628 "max_power_level": 100,
1629 "name": null,
1630 "tombstone": null,
1631 "topic": null,
1632 },
1633 "read_receipts": {
1634 "num_unread": 0,
1635 "num_mentions": 0,
1636 "num_notifications": 0,
1637 "latest_active": null,
1638 "pending": []
1639 },
1640 "recency_stamp": 42,
1641 });
1642 let mut room_info: RoomInfo = serde_json::from_value(room_info_json).unwrap();
1643
1644 assert_eq!(room_info.data_format_version, 0);
1645 assert!(room_info.base_info.notable_tags.is_empty());
1646 assert!(room_info.base_info.pinned_events.is_none());
1647
1648 assert!(room_info.apply_migrations(store.clone()).await);
1650
1651 assert_eq!(room_info.data_format_version, 1);
1652 assert!(room_info.base_info.notable_tags.is_empty());
1653 assert!(room_info.base_info.pinned_events.is_none());
1654
1655 assert!(!room_info.apply_migrations(store.clone()).await);
1657
1658 assert_eq!(room_info.data_format_version, 1);
1659 assert!(room_info.base_info.notable_tags.is_empty());
1660 assert!(room_info.base_info.pinned_events.is_none());
1661
1662 let mut changes = StateChanges::default();
1664
1665 let f = EventFactory::new().room(&room_info.room_id).sender(user_id!("@example:localhost"));
1666 let mut tags = Tags::new();
1667 tags.insert(TagName::Favorite, TagInfo::new());
1668 tags.insert(TagName::User(UserTagName::from_str("u.work").unwrap()), TagInfo::new());
1669 let raw_tag_event: Raw<AnyRoomAccountDataEvent> = f.tag(tags).into();
1670 let tag_event = raw_tag_event.deserialize().unwrap();
1671 changes.add_room_account_data(&room_info.room_id, tag_event, raw_tag_event);
1672
1673 let raw_pinned_events_event: Raw<_> = f
1674 .room_pinned_events(vec![owned_event_id!("$a"), owned_event_id!("$b")])
1675 .into_raw_sync_state();
1676 let pinned_events_event = raw_pinned_events_event.deserialize().unwrap();
1677 changes.add_state_event(&room_info.room_id, pinned_events_event, raw_pinned_events_event);
1678
1679 store.save_changes(&changes).await.unwrap();
1680
1681 room_info.data_format_version = 0;
1683 assert!(room_info.apply_migrations(store.clone()).await);
1684
1685 assert_eq!(room_info.data_format_version, 1);
1686 assert!(room_info.base_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
1687 assert!(room_info.base_info.pinned_events.is_some());
1688
1689 let new_room_info = RoomInfo::new(room_id!("!new_room:localhost"), RoomState::Joined);
1691 assert_eq!(new_room_info.data_format_version, 1);
1692 }
1693
1694 #[test]
1695 fn test_room_info_deserialization() {
1696 let info_json = json!({
1697 "room_id": "!gda78o:server.tld",
1698 "room_state": "Joined",
1699 "notification_counts": {
1700 "highlight_count": 1,
1701 "notification_count": 2,
1702 },
1703 "summary": {
1704 "room_heroes": [{
1705 "user_id": "@somebody:example.org",
1706 "display_name": "Somebody",
1707 "avatar_url": "mxc://example.org/abc"
1708 }],
1709 "joined_member_count": 5,
1710 "invited_member_count": 0,
1711 },
1712 "members_synced": true,
1713 "last_prev_batch": "pb",
1714 "sync_info": "FullySynced",
1715 "encryption_state_synced": true,
1716 "base_info": {
1717 "avatar": null,
1718 "canonical_alias": null,
1719 "create": null,
1720 "dm_targets": [],
1721 "encryption": null,
1722 "guest_access": null,
1723 "history_visibility": null,
1724 "join_rules": null,
1725 "max_power_level": 100,
1726 "member_hints": null,
1727 "name": null,
1728 "tombstone": null,
1729 "topic": null,
1730 },
1731 "cached_display_name": { "Calculated": "lol" },
1732 "cached_user_defined_notification_mode": "Mute",
1733 "recency_stamp": 42,
1734 });
1735
1736 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1737
1738 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1739 assert_eq!(info.room_state, RoomState::Joined);
1740 assert_eq!(info.notification_counts.highlight_count, 1);
1741 assert_eq!(info.notification_counts.notification_count, 2);
1742 assert_eq!(
1743 info.summary.room_heroes,
1744 vec![RoomHero {
1745 user_id: owned_user_id!("@somebody:example.org"),
1746 display_name: Some("Somebody".to_owned()),
1747 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1748 }]
1749 );
1750 assert_eq!(info.summary.joined_member_count, 5);
1751 assert_eq!(info.summary.invited_member_count, 0);
1752 assert!(info.members_synced);
1753 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1754 assert_eq!(info.sync_info, SyncInfo::FullySynced);
1755 assert!(info.encryption_state_synced);
1756 assert_matches!(info.latest_event_value, LatestEventValue::None);
1757 assert!(info.base_info.avatar.is_none());
1758 assert!(info.base_info.canonical_alias.is_none());
1759 assert!(info.base_info.create.is_none());
1760 assert_eq!(info.base_info.dm_targets.len(), 0);
1761 assert!(info.base_info.encryption.is_none());
1762 assert!(info.base_info.guest_access.is_none());
1763 assert!(info.base_info.history_visibility.is_none());
1764 assert!(info.base_info.join_rules.is_none());
1765 assert_eq!(info.base_info.max_power_level, 100);
1766 assert!(info.base_info.member_hints.is_none());
1767 assert!(info.base_info.name.is_none());
1768 assert!(info.base_info.tombstone.is_none());
1769 assert!(info.base_info.topic.is_none());
1770
1771 assert_eq!(
1772 info.cached_display_name.as_ref(),
1773 Some(&RoomDisplayName::Calculated("lol".to_owned())),
1774 );
1775 assert_eq!(
1776 info.cached_user_defined_notification_mode.as_ref(),
1777 Some(&RoomNotificationMode::Mute)
1778 );
1779 assert_eq!(info.recency_stamp.as_ref(), Some(&42.into()));
1780 }
1781
1782 #[test]
1789 fn test_room_info_deserialization_without_optional_items() {
1790 let info_json = json!({
1793 "room_id": "!gda78o:server.tld",
1794 "room_state": "Invited",
1795 "notification_counts": {
1796 "highlight_count": 1,
1797 "notification_count": 2,
1798 },
1799 "summary": {
1800 "room_heroes": [{
1801 "user_id": "@somebody:example.org",
1802 "display_name": "Somebody",
1803 "avatar_url": "mxc://example.org/abc"
1804 }],
1805 "joined_member_count": 5,
1806 "invited_member_count": 0,
1807 },
1808 "members_synced": true,
1809 "last_prev_batch": "pb",
1810 "sync_info": "FullySynced",
1811 "encryption_state_synced": true,
1812 "base_info": {
1813 "avatar": null,
1814 "canonical_alias": null,
1815 "create": null,
1816 "dm_targets": [],
1817 "encryption": null,
1818 "guest_access": null,
1819 "history_visibility": null,
1820 "join_rules": null,
1821 "max_power_level": 100,
1822 "name": null,
1823 "tombstone": null,
1824 "topic": null,
1825 },
1826 });
1827
1828 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1829
1830 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1831 assert_eq!(info.room_state, RoomState::Invited);
1832 assert_eq!(info.notification_counts.highlight_count, 1);
1833 assert_eq!(info.notification_counts.notification_count, 2);
1834 assert_eq!(
1835 info.summary.room_heroes,
1836 vec![RoomHero {
1837 user_id: owned_user_id!("@somebody:example.org"),
1838 display_name: Some("Somebody".to_owned()),
1839 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1840 }]
1841 );
1842 assert_eq!(info.summary.joined_member_count, 5);
1843 assert_eq!(info.summary.invited_member_count, 0);
1844 assert!(info.members_synced);
1845 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1846 assert_eq!(info.sync_info, SyncInfo::FullySynced);
1847 assert!(info.encryption_state_synced);
1848 assert!(info.base_info.avatar.is_none());
1849 assert!(info.base_info.canonical_alias.is_none());
1850 assert!(info.base_info.create.is_none());
1851 assert_eq!(info.base_info.dm_targets.len(), 0);
1852 assert!(info.base_info.encryption.is_none());
1853 assert!(info.base_info.guest_access.is_none());
1854 assert!(info.base_info.history_visibility.is_none());
1855 assert!(info.base_info.join_rules.is_none());
1856 assert_eq!(info.base_info.max_power_level, 100);
1857 assert!(info.base_info.name.is_none());
1858 assert!(info.base_info.tombstone.is_none());
1859 assert!(info.base_info.topic.is_none());
1860 }
1861
1862 #[test]
1863 fn test_member_hints_with_different_contents_reset_computed_value() {
1864 let expected = BTreeSet::from_iter([
1865 owned_user_id!("@alice:example.org"),
1866 owned_user_id!("@bob:example.org"),
1867 ]);
1868
1869 let info_json = json!({
1870 "room_id": "!gda78o:server.tld",
1871 "room_state": "Invited",
1872 "notification_counts": {
1873 "highlight_count": 1,
1874 "notification_count": 2,
1875 },
1876 "summary": {
1877 "room_heroes": [{
1878 "user_id": "@somebody:example.org",
1879 "display_name": "Somebody",
1880 "avatar_url": "mxc://example.org/abc"
1881 }],
1882 "joined_member_count": 5,
1883 "invited_member_count": 0,
1884 "active_service_members": 2,
1885 },
1886 "members_synced": true,
1887 "last_prev_batch": "pb",
1888 "sync_info": "FullySynced",
1889 "encryption_state_synced": true,
1890 "base_info": {
1891 "avatar": null,
1892 "canonical_alias": null,
1893 "create": null,
1894 "dm_targets": [],
1895 "encryption": null,
1896 "guest_access": null,
1897 "history_visibility": null,
1898 "join_rules": null,
1899 "max_power_level": 100,
1900 "member_hints": {
1901 "Original": {
1902 "content": {
1903 "service_members": ["@alice:example.org", "@bob:example.org"]
1904 }
1905 }
1906 },
1907 "name": null,
1908 "tombstone": null,
1909 "topic": null,
1910 },
1911 });
1912
1913 let info: RoomInfo = serde_json::from_value(info_json.clone()).unwrap();
1914 assert_eq!(info.base_info.member_hints.unwrap().content.service_members.unwrap(), expected);
1915 assert_eq!(info.summary.active_service_members, Some(2));
1916
1917 let mut info: RoomInfo = serde_json::from_value(info_json.clone()).unwrap();
1919 let mut raw_state_event_with_keys = RawStateEventWithKeys::try_from_raw_state_event(
1920 EventFactory::new()
1921 .sender(user_id!("@alice:example.org"))
1922 .member_hints(expected.clone())
1923 .into_raw_sync_state(),
1924 )
1925 .expect("Expected member hints event is created");
1926
1927 info.handle_state_event(&mut raw_state_event_with_keys);
1928
1929 assert_eq!(info.base_info.member_hints.unwrap().content.service_members.unwrap(), expected);
1931 assert_eq!(info.summary.active_service_members, Some(2));
1933
1934 let mut info: RoomInfo = serde_json::from_value(info_json).unwrap();
1936 let new_member_hints = BTreeSet::from_iter([owned_user_id!("@alice:example.org")]);
1937 let mut raw_state_event_with_keys = RawStateEventWithKeys::try_from_raw_state_event(
1938 EventFactory::new()
1939 .sender(user_id!("@alice:example.org"))
1940 .member_hints(new_member_hints.clone())
1941 .into_raw_sync_state(),
1942 )
1943 .expect("New member hints event is created");
1944
1945 info.handle_state_event(&mut raw_state_event_with_keys);
1946
1947 assert_eq!(
1949 info.base_info.member_hints.unwrap().content.service_members.unwrap(),
1950 new_member_hints
1951 );
1952 assert!(info.summary.active_service_members.is_none());
1954 }
1955
1956 fn make_room_and_state_store(room_state: RoomState) -> (Room, SaveLockedStateStore) {
1957 let state_store = SaveLockedStateStore::new(MemoryStore::new().into_state_store());
1958 let user_id = user_id!("@user:localhost");
1959 let room_id = room_id!("!room:localhost");
1960 let (sender, _) = tokio::sync::broadcast::channel(1);
1961 let room = Room::new(user_id, state_store.clone(), room_id, room_state, sender);
1962 (room, state_store)
1963 }
1964
1965 #[async_test]
1966 async fn test_update_room_info_only_updates_in_memory_room_info() {
1967 let (room, state_store) = make_room_and_state_store(RoomState::Joined);
1968
1969 let before = room.clone_info();
1970 assert_eq!(before.state(), RoomState::Joined);
1971 room.update_room_info(|mut info| {
1972 info.mark_as_banned();
1973 (info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
1974 })
1975 .await;
1976 let after = room.clone_info();
1977 assert_eq!(after.state(), RoomState::Banned);
1978
1979 let infos = state_store
1980 .get_room_infos(&RoomLoadSettings::One(room.room_id.clone()))
1981 .await
1982 .expect("get room info");
1983 assert!(infos.is_empty());
1984 }
1985
1986 #[async_test]
1987 async fn test_update_room_info_with_store_guard_only_updates_in_memory_room_info() {
1988 let (room, state_store) = make_room_and_state_store(RoomState::Joined);
1989
1990 let before = room.clone_info();
1991 assert_eq!(before.state(), RoomState::Joined);
1992 room.update_room_info_with_store_guard(&state_store.lock().lock().await, |mut info| {
1993 info.mark_as_banned();
1994 (info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
1995 })
1996 .expect("update room info");
1997 let after = room.clone_info();
1998 assert_eq!(after.state(), RoomState::Banned);
1999
2000 let infos = state_store
2001 .get_room_infos(&RoomLoadSettings::One(room.room_id.clone()))
2002 .await
2003 .expect("get room info");
2004 assert!(infos.is_empty());
2005 }
2006
2007 #[async_test]
2008 async fn test_update_room_info_only_accepts_guard_for_underlying_mutex() {
2009 let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2010
2011 room.update_room_info_with_store_guard(&state_store.lock().lock().await, |info| {
2012 (info, RoomInfoNotableUpdateReasons::NONE)
2013 })
2014 .expect("room accepts guard for underlying mutex");
2015
2016 let mutex = Mutex::new(());
2017 room.update_room_info_with_store_guard(&mutex.lock().await, |info| {
2018 (info, RoomInfoNotableUpdateReasons::NONE)
2019 })
2020 .expect_err("room does not accept guard for unknown mutex");
2021 }
2022
2023 #[async_test]
2024 async fn test_update_and_save_room_info_updates_room_info_in_memory_and_store() {
2025 let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2026
2027 let before = room.clone_info();
2028 assert_eq!(before.state(), RoomState::Joined);
2029 room.update_and_save_room_info(|mut info| {
2030 info.mark_as_banned();
2031 (info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
2032 })
2033 .await
2034 .expect("update and save room info");
2035 let after = room.clone_info();
2036 assert_eq!(after.state(), RoomState::Banned);
2037
2038 let infos = state_store
2039 .get_room_infos(&RoomLoadSettings::One(room.room_id.clone()))
2040 .await
2041 .expect("get room info");
2042 assert_eq!(infos.len(), 1);
2043 assert_matches!(infos.first(), Some(info) => {
2044 info.state() == RoomState::Banned
2045 });
2046 }
2047
2048 #[async_test]
2049 async fn test_update_and_save_room_info_with_store_guard_updates_room_info_in_memory_and_store()
2050 {
2051 let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2052
2053 let before = room.clone_info();
2054 assert_eq!(before.state(), RoomState::Joined);
2055 room.update_and_save_room_info_with_store_guard(
2056 &state_store.lock().lock().await,
2057 |mut info| {
2058 info.mark_as_banned();
2059 (info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
2060 },
2061 )
2062 .await
2063 .expect("update and save room info");
2064 let after = room.clone_info();
2065 assert_eq!(after.state(), RoomState::Banned);
2066
2067 let infos = state_store
2068 .get_room_infos(&RoomLoadSettings::One(room.room_id.clone()))
2069 .await
2070 .expect("get room info");
2071 assert_eq!(infos.len(), 1);
2072 assert_matches!(infos.first(), Some(info) => {
2073 info.state() == RoomState::Banned
2074 });
2075 }
2076
2077 #[async_test]
2078 async fn test_update_and_save_room_info_only_accepts_guard_for_underlying_mutex() {
2079 let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2080
2081 room.update_and_save_room_info_with_store_guard(&state_store.lock().lock().await, |info| {
2082 (info, RoomInfoNotableUpdateReasons::NONE)
2083 })
2084 .await
2085 .expect("room accepts guard for underlying mutex");
2086
2087 let mutex = Mutex::new(());
2088 room.update_and_save_room_info_with_store_guard(&mutex.lock().await, |info| {
2089 (info, RoomInfoNotableUpdateReasons::NONE)
2090 })
2091 .await
2092 .expect_err("room does not accept guard for unknown mutex");
2093 }
2094
2095 #[derive(Debug)]
2096 struct Elapsed;
2097
2098 async fn timeout<F: Future + Unpin>(duration: Duration, f: F) -> Result<F::Output, Elapsed> {
2099 #[cfg(all(target_family = "wasm", target_os = "unknown"))]
2100 {
2101 match future::select(sleep(duration), f).await {
2102 Either::Left(_) => return Err(Elapsed),
2103 Either::Right((output, _)) => Ok(output),
2104 }
2105 }
2106 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
2107 {
2108 tokio::time::timeout(duration, f).await.map_err(|_| Elapsed)
2109 }
2110 }
2111
2112 #[async_test]
2113 async fn test_update_room_info_waits_to_acquire_lock_before_updating_room_info() {
2114 let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2115
2116 let lock_task = spawn({
2118 let state_store = state_store.clone();
2119 async move {
2120 let lock = state_store.lock();
2121 let _guard = lock.lock().await;
2122 sleep(Duration::from_secs(5)).await;
2123 }
2124 });
2125
2126 let save_task = spawn(async move {
2128 room.update_room_info(|info| (info, RoomInfoNotableUpdateReasons::NONE)).await
2129 });
2130
2131 assert_matches!(future::select(lock_task, save_task).await, Either::Left((_, save_task)) => {
2134 timeout(Duration::from_millis(100), save_task)
2135 .await
2136 .expect("task completes before timeout")
2137 .expect("task completes successfully")
2138 });
2139 }
2140
2141 #[async_test]
2142 async fn test_update_and_save_room_info_waits_to_acquire_lock_before_updating_room_info() {
2143 let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2144
2145 let lock_task = spawn({
2147 let state_store = state_store.clone();
2148 async move {
2149 let lock = state_store.lock();
2150 let _guard = lock.lock().await;
2151 sleep(Duration::from_secs(5)).await;
2152 }
2153 });
2154
2155 let save_task = spawn(async move {
2157 room.update_and_save_room_info(|info| (info, RoomInfoNotableUpdateReasons::NONE)).await
2158 });
2159
2160 assert_matches!(future::select(lock_task, save_task).await, Either::Left((_, save_task)) => {
2163 timeout(Duration::from_millis(100), save_task)
2164 .await
2165 .expect("task completes before timeout")
2166 .expect("task completes successfully")
2167 .expect("update and save room info");
2168 });
2169 }
2170}