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 tag::{TagEventContent, TagName, Tags},
54 },
55 room::RoomType,
56 room_version_rules::{RedactionRules, RoomVersionRules},
57 serde::Raw,
58};
59use serde::{Deserialize, Serialize};
60use tracing::{field::debug, info, instrument, warn};
61
62use super::{
63 AccountDataSource, EncryptionState, Room, RoomCreateWithCreatorEventContent, RoomDisplayName,
64 RoomHero, RoomNotableTags, RoomState, RoomSummary,
65};
66use crate::{
67 MinimalStateEvent,
68 deserialized_responses::RawSyncOrStrippedState,
69 latest_event::LatestEventValue,
70 notification_settings::RoomNotificationMode,
71 read_receipts::RoomReadReceipts,
72 store::{DynStateStore, StateStoreExt},
73 sync::UnreadNotificationsCount,
74 utils::{AnyStateEventEnum, RawStateEventWithKeys},
75};
76
77const DEFAULT_MAX_POWER_LEVEL: i64 = 100;
79
80impl Room {
81 pub fn subscribe_info(&self) -> Subscriber<RoomInfo> {
83 self.info.subscribe()
84 }
85
86 pub fn clone_info(&self) -> RoomInfo {
88 self.info.get()
89 }
90
91 pub fn set_room_info(
93 &self,
94 room_info: RoomInfo,
95 room_info_notable_update_reasons: RoomInfoNotableUpdateReasons,
96 ) {
97 self.info.set(room_info);
98
99 if !room_info_notable_update_reasons.is_empty() {
100 let _ = self.room_info_notable_update_sender.send(RoomInfoNotableUpdate {
102 room_id: self.room_id.clone(),
103 reasons: room_info_notable_update_reasons,
104 });
105 } else {
106 let _ = self.room_info_notable_update_sender.send(RoomInfoNotableUpdate {
110 room_id: self.room_id.clone(),
111 reasons: RoomInfoNotableUpdateReasons::NONE,
112 });
113 }
114 }
115}
116
117#[derive(Clone, Debug, Serialize, Deserialize)]
121pub struct BaseRoomInfo {
122 pub(crate) avatar: Option<MinimalStateEvent<PossiblyRedactedRoomAvatarEventContent>>,
124 pub(crate) canonical_alias:
126 Option<MinimalStateEvent<PossiblyRedactedRoomCanonicalAliasEventContent>>,
127 pub(crate) create: Option<MinimalStateEvent<RoomCreateWithCreatorEventContent>>,
129 pub(crate) dm_targets: HashSet<OwnedDirectUserIdentifier>,
132 pub(crate) encryption: Option<PossiblyRedactedRoomEncryptionEventContent>,
134 pub(crate) guest_access: Option<MinimalStateEvent<PossiblyRedactedRoomGuestAccessEventContent>>,
136 pub(crate) history_visibility:
138 Option<MinimalStateEvent<PossiblyRedactedRoomHistoryVisibilityEventContent>>,
139 pub(crate) join_rules: Option<MinimalStateEvent<PossiblyRedactedRoomJoinRulesEventContent>>,
141 pub(crate) max_power_level: i64,
143 pub(crate) member_hints: Option<MinimalStateEvent<PossiblyRedactedMemberHintsEventContent>>,
146 pub(crate) name: Option<MinimalStateEvent<PossiblyRedactedRoomNameEventContent>>,
148 pub(crate) tombstone: Option<MinimalStateEvent<PossiblyRedactedRoomTombstoneEventContent>>,
150 pub(crate) topic: Option<MinimalStateEvent<PossiblyRedactedRoomTopicEventContent>>,
152 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
155 pub(crate) rtc_member_events:
156 BTreeMap<CallMemberStateKey, MinimalStateEvent<PossiblyRedactedCallMemberEventContent>>,
157 #[serde(default)]
159 pub(crate) is_marked_unread: bool,
160 #[serde(default)]
162 pub(crate) is_marked_unread_source: AccountDataSource,
163 #[serde(skip_serializing_if = "RoomNotableTags::is_empty", default)]
168 pub(crate) notable_tags: RoomNotableTags,
169 pub(crate) pinned_events: Option<PossiblyRedactedRoomPinnedEventsEventContent>,
171}
172
173impl BaseRoomInfo {
174 pub fn new() -> Self {
176 Self::default()
177 }
178
179 pub fn room_version(&self) -> Option<&RoomVersionId> {
184 Some(&self.create.as_ref()?.content.room_version)
185 }
186
187 pub fn handle_state_event<T: AnyStateEventEnum>(
191 &mut self,
192 raw_event: &mut RawStateEventWithKeys<T>,
193 ) -> bool {
194 match (&raw_event.event_type, raw_event.state_key.as_str()) {
195 (StateEventType::RoomEncryption, "") => {
196 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
200 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomEncryption)
201 }) && event.content.algorithm.is_some()
202 {
203 self.encryption = Some(event.content);
204 true
205 } else {
206 false
207 }
208 }
209 (StateEventType::RoomAvatar, "") => {
210 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
211 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomAvatar)
212 }) {
213 self.avatar = Some(event);
214 true
215 } else {
216 self.avatar.take().is_some()
218 }
219 }
220 (StateEventType::RoomName, "") => {
221 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
222 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomName)
223 }) {
224 self.name = Some(event);
225 true
226 } else {
227 self.name.take().is_some()
229 }
230 }
231 (StateEventType::RoomCreate, "") if self.create.is_none() => {
233 if let Some(any_event) = raw_event.deserialize()
234 && let Some(content) = as_variant!(
235 any_event.get_content(),
236 AnyPossiblyRedactedStateEventContent::RoomCreate
237 )
238 {
239 self.create = Some(MinimalStateEvent {
240 content: RoomCreateWithCreatorEventContent::from_event_content(
241 content,
242 any_event.get_sender().to_owned(),
243 ),
244 event_id: any_event.get_event_id().map(ToOwned::to_owned),
245 });
246 true
247 } else {
248 false
249 }
250 }
251 (StateEventType::RoomHistoryVisibility, "") => {
252 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
253 as_variant!(
254 any_event,
255 AnyPossiblyRedactedStateEventContent::RoomHistoryVisibility
256 )
257 }) {
258 self.history_visibility = Some(event);
259 true
260 } else {
261 self.history_visibility.take().is_some()
263 }
264 }
265 (StateEventType::RoomGuestAccess, "") => {
266 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
267 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomGuestAccess)
268 }) {
269 self.guest_access = Some(event);
270 true
271 } else {
272 self.guest_access.take().is_some()
274 }
275 }
276 (StateEventType::MemberHints, "") => {
277 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
278 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::MemberHints)
279 }) {
280 self.member_hints = Some(event);
281 true
282 } else {
283 self.member_hints.take().is_some()
285 }
286 }
287 (StateEventType::RoomJoinRules, "") => {
288 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
289 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomJoinRules)
290 }) {
291 match &event.content.join_rule {
292 JoinRule::Invite
293 | JoinRule::Knock
294 | JoinRule::Private
295 | JoinRule::Restricted(_)
296 | JoinRule::KnockRestricted(_)
297 | JoinRule::Public => {
298 self.join_rules = Some(event);
299 true
300 }
301 r => {
302 warn!(join_rule = ?r.as_str(), "Encountered a custom join rule, skipping");
303 self.join_rules.take().is_some()
305 }
306 }
307 } else {
308 self.join_rules.take().is_some()
310 }
311 }
312 (StateEventType::RoomCanonicalAlias, "") => {
313 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
314 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomCanonicalAlias)
315 }) {
316 self.canonical_alias = Some(event);
317 true
318 } else {
319 self.canonical_alias.take().is_some()
321 }
322 }
323 (StateEventType::RoomTopic, "") => {
324 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
325 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomTopic)
326 }) {
327 self.topic = Some(event);
328 true
329 } else {
330 self.topic.take().is_some()
332 }
333 }
334 (StateEventType::RoomTombstone, "") => {
335 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
336 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomTombstone)
337 }) {
338 self.tombstone = Some(event);
339 true
340 } else {
341 self.tombstone.take().is_some()
343 }
344 }
345 (StateEventType::RoomPowerLevels, "") => {
346 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
347 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomPowerLevels)
348 }) {
349 let new_max = i64::from(
350 event
351 .content
352 .users
353 .values()
354 .fold(event.content.users_default, |max_pl, user_pl| {
355 max_pl.max(*user_pl)
356 }),
357 );
358
359 if self.max_power_level != new_max {
360 self.max_power_level = new_max;
361 true
362 } else {
363 false
364 }
365 } else if self.max_power_level != DEFAULT_MAX_POWER_LEVEL {
366 self.max_power_level = DEFAULT_MAX_POWER_LEVEL;
368 true
369 } else {
370 false
371 }
372 }
373 (StateEventType::CallMember, _) => {
374 if let Ok(call_member_key) = raw_event.state_key.parse::<CallMemberStateKey>() {
375 if let Some(any_event) = raw_event.deserialize()
376 && let Some(content) = as_variant!(
377 any_event.get_content(),
378 AnyPossiblyRedactedStateEventContent::CallMember
379 )
380 {
381 let mut event = MinimalStateEvent {
382 content,
383 event_id: any_event.get_event_id().map(ToOwned::to_owned),
384 };
385
386 if let Some(origin_server_ts) = any_event.get_origin_server_ts() {
387 event.content.set_created_ts_if_none(origin_server_ts);
388 }
389
390 self.rtc_member_events.insert(call_member_key, event);
392
393 self.rtc_member_events
395 .retain(|_, ev| !ev.content.active_memberships(None).is_empty());
396
397 true
398 } else {
399 self.rtc_member_events.remove(&call_member_key).is_some()
402 }
403 } else {
404 false
405 }
406 }
407 (StateEventType::RoomPinnedEvents, "") => {
408 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
409 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomPinnedEvents)
410 }) {
411 self.pinned_events = Some(event.content);
412 true
413 } else {
414 self.pinned_events.take().is_some()
416 }
417 }
418 _ => false,
419 }
420 }
421
422 pub(super) fn handle_redaction(&mut self, redacts: &EventId) {
423 let redaction_rules = self
424 .room_version()
425 .and_then(|room_version| room_version.rules())
426 .unwrap_or(ROOM_VERSION_RULES_FALLBACK)
427 .redaction;
428
429 if let Some(ev) = &mut self.avatar
430 && ev.event_id.as_deref() == Some(redacts)
431 {
432 ev.redact(&redaction_rules);
433 } else if let Some(ev) = &mut self.canonical_alias
434 && ev.event_id.as_deref() == Some(redacts)
435 {
436 ev.redact(&redaction_rules);
437 } else if let Some(ev) = &mut self.create
438 && ev.event_id.as_deref() == Some(redacts)
439 {
440 ev.redact(&redaction_rules);
441 } else if let Some(ev) = &mut self.guest_access
442 && ev.event_id.as_deref() == Some(redacts)
443 {
444 ev.redact(&redaction_rules);
445 } else if let Some(ev) = &mut self.history_visibility
446 && ev.event_id.as_deref() == Some(redacts)
447 {
448 ev.redact(&redaction_rules);
449 } else if let Some(ev) = &mut self.join_rules
450 && ev.event_id.as_deref() == Some(redacts)
451 {
452 ev.redact(&redaction_rules);
453 } else if let Some(ev) = &mut self.name
454 && ev.event_id.as_deref() == Some(redacts)
455 {
456 ev.redact(&redaction_rules);
457 } else if let Some(ev) = &mut self.tombstone
458 && ev.event_id.as_deref() == Some(redacts)
459 {
460 ev.redact(&redaction_rules);
461 } else if let Some(ev) = &mut self.topic
462 && ev.event_id.as_deref() == Some(redacts)
463 {
464 ev.redact(&redaction_rules);
465 } else {
466 self.rtc_member_events
467 .retain(|_, member_event| member_event.event_id.as_deref() != Some(redacts));
468 }
469 }
470
471 pub fn handle_notable_tags(&mut self, tags: &Tags) {
472 let mut notable_tags = RoomNotableTags::empty();
473
474 if tags.contains_key(&TagName::Favorite) {
475 notable_tags.insert(RoomNotableTags::FAVOURITE);
476 }
477
478 if tags.contains_key(&TagName::LowPriority) {
479 notable_tags.insert(RoomNotableTags::LOW_PRIORITY);
480 }
481
482 self.notable_tags = notable_tags;
483 }
484}
485
486impl Default for BaseRoomInfo {
487 fn default() -> Self {
488 Self {
489 avatar: None,
490 canonical_alias: None,
491 create: None,
492 dm_targets: Default::default(),
493 member_hints: None,
494 encryption: None,
495 guest_access: None,
496 history_visibility: None,
497 join_rules: None,
498 max_power_level: DEFAULT_MAX_POWER_LEVEL,
499 name: None,
500 tombstone: None,
501 topic: None,
502 rtc_member_events: BTreeMap::new(),
503 is_marked_unread: false,
504 is_marked_unread_source: AccountDataSource::Unstable,
505 notable_tags: RoomNotableTags::empty(),
506 pinned_events: None,
507 }
508 }
509}
510
511#[derive(Clone, Debug, Serialize, Deserialize)]
515pub struct RoomInfo {
516 #[serde(default, alias = "version")]
519 pub(crate) data_format_version: u8,
520
521 pub(crate) room_id: OwnedRoomId,
523
524 pub(crate) room_state: RoomState,
526
527 pub(crate) notification_counts: UnreadNotificationsCount,
532
533 pub(crate) summary: RoomSummary,
535
536 pub(crate) members_synced: bool,
538
539 pub(crate) last_prev_batch: Option<String>,
541
542 pub(crate) sync_info: SyncInfo,
544
545 pub(crate) encryption_state_synced: bool,
547
548 #[serde(default)]
550 pub(crate) latest_event_value: LatestEventValue,
551
552 #[serde(default)]
554 pub(crate) read_receipts: RoomReadReceipts,
555
556 pub(crate) base_info: Box<BaseRoomInfo>,
559
560 #[serde(skip)]
564 pub(crate) warned_about_unknown_room_version_rules: Arc<AtomicBool>,
565
566 #[serde(default, skip_serializing_if = "Option::is_none")]
571 pub(crate) cached_display_name: Option<RoomDisplayName>,
572
573 #[serde(default, skip_serializing_if = "Option::is_none")]
575 pub(crate) cached_user_defined_notification_mode: Option<RoomNotificationMode>,
576
577 #[serde(default)]
594 pub(crate) recency_stamp: Option<RoomRecencyStamp>,
595}
596
597impl RoomInfo {
598 #[doc(hidden)] pub fn new(room_id: &RoomId, room_state: RoomState) -> Self {
600 Self {
601 data_format_version: 1,
602 room_id: room_id.into(),
603 room_state,
604 notification_counts: Default::default(),
605 summary: Default::default(),
606 members_synced: false,
607 last_prev_batch: None,
608 sync_info: SyncInfo::NoState,
609 encryption_state_synced: false,
610 latest_event_value: LatestEventValue::default(),
611 read_receipts: Default::default(),
612 base_info: Box::new(BaseRoomInfo::new()),
613 warned_about_unknown_room_version_rules: Arc::new(false.into()),
614 cached_display_name: None,
615 cached_user_defined_notification_mode: None,
616 recency_stamp: None,
617 }
618 }
619
620 pub fn mark_as_joined(&mut self) {
622 self.set_state(RoomState::Joined);
623 }
624
625 pub fn mark_as_left(&mut self) {
627 self.set_state(RoomState::Left);
628 }
629
630 pub fn mark_as_invited(&mut self) {
632 self.set_state(RoomState::Invited);
633 }
634
635 pub fn mark_as_knocked(&mut self) {
637 self.set_state(RoomState::Knocked);
638 }
639
640 pub fn mark_as_banned(&mut self) {
642 self.set_state(RoomState::Banned);
643 }
644
645 pub fn set_state(&mut self, room_state: RoomState) {
647 self.room_state = room_state;
648 }
649
650 pub fn mark_members_synced(&mut self) {
652 self.members_synced = true;
653 }
654
655 pub fn mark_members_missing(&mut self) {
657 self.members_synced = false;
658 }
659
660 pub fn are_members_synced(&self) -> bool {
662 self.members_synced
663 }
664
665 pub fn mark_state_partially_synced(&mut self) {
667 self.sync_info = SyncInfo::PartiallySynced;
668 }
669
670 pub fn mark_state_fully_synced(&mut self) {
672 self.sync_info = SyncInfo::FullySynced;
673 }
674
675 pub fn mark_state_not_synced(&mut self) {
677 self.sync_info = SyncInfo::NoState;
678 }
679
680 pub fn mark_encryption_state_synced(&mut self) {
682 self.encryption_state_synced = true;
683 }
684
685 pub fn mark_encryption_state_missing(&mut self) {
687 self.encryption_state_synced = false;
688 }
689
690 pub fn set_prev_batch(&mut self, prev_batch: Option<&str>) -> bool {
694 if self.last_prev_batch.as_deref() != prev_batch {
695 self.last_prev_batch = prev_batch.map(|p| p.to_owned());
696 true
697 } else {
698 false
699 }
700 }
701
702 pub fn state(&self) -> RoomState {
704 self.room_state
705 }
706
707 #[cfg(not(feature = "experimental-encrypted-state-events"))]
709 pub fn encryption_state(&self) -> EncryptionState {
710 if !self.encryption_state_synced {
711 EncryptionState::Unknown
712 } else if self.base_info.encryption.is_some() {
713 EncryptionState::Encrypted
714 } else {
715 EncryptionState::NotEncrypted
716 }
717 }
718
719 #[cfg(feature = "experimental-encrypted-state-events")]
721 pub fn encryption_state(&self) -> EncryptionState {
722 if !self.encryption_state_synced {
723 EncryptionState::Unknown
724 } else {
725 self.base_info
726 .encryption
727 .as_ref()
728 .map(|state| {
729 if state.encrypt_state_events {
730 EncryptionState::StateEncrypted
731 } else {
732 EncryptionState::Encrypted
733 }
734 })
735 .unwrap_or(EncryptionState::NotEncrypted)
736 }
737 }
738
739 pub fn set_encryption_event(
741 &mut self,
742 event: Option<PossiblyRedactedRoomEncryptionEventContent>,
743 ) {
744 self.base_info.encryption = event;
745 }
746
747 pub fn handle_encryption_state(
749 &mut self,
750 requested_required_states: &[(StateEventType, String)],
751 ) {
752 if requested_required_states
753 .iter()
754 .any(|(state_event, _)| state_event == &StateEventType::RoomEncryption)
755 {
756 self.mark_encryption_state_synced();
762 }
763 }
764
765 pub fn handle_state_event(
769 &mut self,
770 raw_event: &mut RawStateEventWithKeys<AnySyncStateEvent>,
771 ) -> bool {
772 let base_info_has_been_modified = self.base_info.handle_state_event(raw_event);
774
775 if raw_event.event_type == StateEventType::RoomEncryption && raw_event.state_key.is_empty()
776 {
777 self.mark_encryption_state_synced();
783 }
784
785 base_info_has_been_modified
786 }
787
788 pub fn handle_stripped_state_event(
792 &mut self,
793 raw_event: &mut RawStateEventWithKeys<AnyStrippedStateEvent>,
794 ) -> bool {
795 self.base_info.handle_state_event(raw_event)
796 }
797
798 #[instrument(skip_all, fields(redacts))]
800 pub fn handle_redaction(
801 &mut self,
802 event: &SyncRoomRedactionEvent,
803 _raw: &Raw<SyncRoomRedactionEvent>,
804 ) {
805 let redaction_rules = self.room_version_rules_or_default().redaction;
806
807 let Some(redacts) = event.redacts(&redaction_rules) else {
808 info!("Can't apply redaction, redacts field is missing");
809 return;
810 };
811 tracing::Span::current().record("redacts", debug(redacts));
812
813 self.base_info.handle_redaction(redacts);
814 }
815
816 pub fn avatar_url(&self) -> Option<&MxcUri> {
818 self.base_info.avatar.as_ref().and_then(|e| e.content.url.as_deref())
819 }
820
821 pub fn update_avatar(&mut self, url: Option<OwnedMxcUri>) {
823 self.base_info.avatar = url.map(|url| {
824 let mut content = PossiblyRedactedRoomAvatarEventContent::new();
825 content.url = Some(url);
826
827 MinimalStateEvent { content, event_id: None }
828 });
829 }
830
831 pub fn avatar_info(&self) -> Option<&avatar::ImageInfo> {
833 self.base_info.avatar.as_ref().and_then(|e| e.content.info.as_deref())
834 }
835
836 pub fn update_notification_count(&mut self, notification_counts: UnreadNotificationsCount) {
838 self.notification_counts = notification_counts;
839 }
840
841 pub fn update_from_ruma_summary(&mut self, summary: &RumaSummary) -> bool {
845 let mut changed = false;
846
847 if !summary.is_empty() {
848 if !summary.heroes.is_empty() {
849 self.summary.room_heroes = summary
850 .heroes
851 .iter()
852 .map(|hero_id| RoomHero {
853 user_id: hero_id.to_owned(),
854 display_name: None,
855 avatar_url: None,
856 })
857 .collect();
858
859 changed = true;
860 }
861
862 if let Some(joined) = summary.joined_member_count {
863 self.summary.joined_member_count = joined.into();
864 changed = true;
865 }
866
867 if let Some(invited) = summary.invited_member_count {
868 self.summary.invited_member_count = invited.into();
869 changed = true;
870 }
871 }
872
873 changed
874 }
875
876 pub(crate) fn update_joined_member_count(&mut self, count: u64) {
878 self.summary.joined_member_count = count;
879 }
880
881 pub(crate) fn update_invited_member_count(&mut self, count: u64) {
883 self.summary.invited_member_count = count;
884 }
885
886 pub(crate) fn update_heroes(&mut self, heroes: Vec<RoomHero>) {
888 self.summary.room_heroes = heroes;
889 }
890
891 pub fn heroes(&self) -> &[RoomHero] {
893 &self.summary.room_heroes
894 }
895
896 pub fn active_members_count(&self) -> u64 {
900 self.summary.joined_member_count.saturating_add(self.summary.invited_member_count)
901 }
902
903 pub fn invited_members_count(&self) -> u64 {
905 self.summary.invited_member_count
906 }
907
908 pub fn joined_members_count(&self) -> u64 {
910 self.summary.joined_member_count
911 }
912
913 pub fn canonical_alias(&self) -> Option<&RoomAliasId> {
915 self.base_info.canonical_alias.as_ref()?.content.alias.as_deref()
916 }
917
918 pub fn alt_aliases(&self) -> &[OwnedRoomAliasId] {
920 self.base_info
921 .canonical_alias
922 .as_ref()
923 .map(|ev| ev.content.alt_aliases.as_ref())
924 .unwrap_or_default()
925 }
926
927 pub fn room_id(&self) -> &RoomId {
929 &self.room_id
930 }
931
932 pub fn room_version(&self) -> Option<&RoomVersionId> {
934 self.base_info.room_version()
935 }
936
937 pub fn room_version_rules_or_default(&self) -> RoomVersionRules {
942 use std::sync::atomic::Ordering;
943
944 self.base_info.room_version().and_then(|room_version| room_version.rules()).unwrap_or_else(
945 || {
946 if self
947 .warned_about_unknown_room_version_rules
948 .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
949 .is_ok()
950 {
951 warn!("Unable to get the room version rules, defaulting to rules for room version {ROOM_VERSION_FALLBACK}");
952 }
953
954 ROOM_VERSION_RULES_FALLBACK
955 },
956 )
957 }
958
959 pub fn room_type(&self) -> Option<&RoomType> {
961 self.base_info.create.as_ref()?.content.room_type.as_ref()
962 }
963
964 pub fn creators(&self) -> Option<Vec<OwnedUserId>> {
966 Some(self.base_info.create.as_ref()?.content.creators())
967 }
968
969 pub(super) fn guest_access(&self) -> &GuestAccess {
970 self.base_info
971 .guest_access
972 .as_ref()
973 .and_then(|event| event.content.guest_access.as_ref())
974 .unwrap_or(&GuestAccess::Forbidden)
975 }
976
977 pub fn history_visibility(&self) -> Option<&HistoryVisibility> {
981 Some(&self.base_info.history_visibility.as_ref()?.content.history_visibility)
982 }
983
984 pub fn history_visibility_or_default(&self) -> &HistoryVisibility {
991 self.history_visibility().unwrap_or(&HistoryVisibility::Shared)
992 }
993
994 pub fn join_rule(&self) -> Option<&JoinRule> {
997 Some(&self.base_info.join_rules.as_ref()?.content.join_rule)
998 }
999
1000 pub fn service_members(&self) -> Option<&BTreeSet<OwnedUserId>> {
1003 self.base_info.member_hints.as_ref()?.content.service_members.as_ref()
1004 }
1005
1006 pub fn name(&self) -> Option<&str> {
1008 self.base_info.name.as_ref()?.content.name.as_deref().filter(|name| !name.is_empty())
1009 }
1010
1011 pub fn create(&self) -> Option<&RoomCreateWithCreatorEventContent> {
1013 Some(&self.base_info.create.as_ref()?.content)
1014 }
1015
1016 pub fn tombstone(&self) -> Option<&PossiblyRedactedRoomTombstoneEventContent> {
1018 Some(&self.base_info.tombstone.as_ref()?.content)
1019 }
1020
1021 pub fn topic(&self) -> Option<&str> {
1023 self.base_info.topic.as_ref()?.content.topic.as_deref()
1024 }
1025
1026 fn active_matrix_rtc_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
1031 let mut v = self
1032 .base_info
1033 .rtc_member_events
1034 .iter()
1035 .flat_map(|(state_key, ev)| {
1036 ev.content.active_memberships(None).into_iter().map(move |m| (state_key.clone(), m))
1037 })
1038 .collect::<Vec<_>>();
1039 v.sort_by_key(|(_, m)| m.created_ts());
1040 v
1041 }
1042
1043 fn active_room_call_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
1049 self.active_matrix_rtc_memberships()
1050 .into_iter()
1051 .filter(|(_user_id, m)| m.is_room_call())
1052 .collect()
1053 }
1054
1055 pub fn has_active_room_call(&self) -> bool {
1058 !self.active_room_call_memberships().is_empty()
1059 }
1060
1061 pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
1070 self.active_room_call_memberships()
1071 .iter()
1072 .map(|(call_member_state_key, _)| call_member_state_key.user_id().to_owned())
1073 .collect()
1074 }
1075
1076 pub fn set_latest_event(&mut self, new_value: LatestEventValue) {
1078 self.latest_event_value = new_value;
1079 }
1080
1081 pub fn update_recency_stamp(&mut self, stamp: RoomRecencyStamp) {
1085 self.recency_stamp = Some(stamp);
1086 }
1087
1088 pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
1090 self.base_info.pinned_events.clone().and_then(|c| c.pinned)
1091 }
1092
1093 pub fn is_pinned_event(&self, event_id: &EventId) -> bool {
1099 self.base_info
1100 .pinned_events
1101 .as_ref()
1102 .and_then(|content| content.pinned.as_deref())
1103 .is_some_and(|pinned| pinned.contains(&event_id.to_owned()))
1104 }
1105
1106 pub fn read_receipts(&self) -> &RoomReadReceipts {
1108 &self.read_receipts
1109 }
1110
1111 pub fn set_read_receipts(&mut self, read_receipts: RoomReadReceipts) {
1113 self.read_receipts = read_receipts;
1114 }
1115
1116 #[instrument(skip_all, fields(room_id = ?self.room_id))]
1124 pub(crate) async fn apply_migrations(&mut self, store: Arc<DynStateStore>) -> bool {
1125 let mut migrated = false;
1126
1127 if self.data_format_version < 1 {
1128 info!("Migrating room info to version 1");
1129
1130 match store.get_room_account_data_event_static::<TagEventContent>(&self.room_id).await {
1132 Ok(Some(raw_event)) => match raw_event.deserialize() {
1134 Ok(event) => {
1135 self.base_info.handle_notable_tags(&event.content.tags);
1136 }
1137 Err(error) => {
1138 warn!("Failed to deserialize room tags: {error}");
1139 }
1140 },
1141 Ok(_) => {
1142 }
1144 Err(error) => {
1145 warn!("Failed to load room tags: {error}");
1146 }
1147 }
1148
1149 match store.get_state_event_static::<RoomPinnedEventsEventContent>(&self.room_id).await
1151 {
1152 Ok(Some(RawSyncOrStrippedState::Sync(raw_event))) => {
1154 if let Some(mut raw_event) =
1155 RawStateEventWithKeys::try_from_raw_state_event(raw_event.cast())
1156 {
1157 self.handle_state_event(&mut raw_event);
1158 }
1159 }
1160 Ok(_) => {
1161 }
1163 Err(error) => {
1164 warn!("Failed to load room pinned events: {error}");
1165 }
1166 }
1167
1168 self.data_format_version = 1;
1169 migrated = true;
1170 }
1171
1172 migrated
1173 }
1174}
1175
1176#[repr(transparent)]
1178#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
1179#[serde(transparent)]
1180pub struct RoomRecencyStamp(u64);
1181
1182impl From<u64> for RoomRecencyStamp {
1183 fn from(value: u64) -> Self {
1184 Self(value)
1185 }
1186}
1187
1188impl From<RoomRecencyStamp> for u64 {
1189 fn from(value: RoomRecencyStamp) -> Self {
1190 value.0
1191 }
1192}
1193
1194#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1195pub(crate) enum SyncInfo {
1196 NoState,
1202
1203 PartiallySynced,
1206
1207 FullySynced,
1209}
1210
1211pub fn apply_redaction(
1214 event: &Raw<AnySyncTimelineEvent>,
1215 raw_redaction: &Raw<SyncRoomRedactionEvent>,
1216 rules: &RedactionRules,
1217) -> Option<Raw<AnySyncTimelineEvent>> {
1218 use ruma::canonical_json::{RedactedBecause, redact_in_place};
1219
1220 let mut event_json = match event.deserialize_as() {
1221 Ok(json) => json,
1222 Err(e) => {
1223 warn!("Failed to deserialize latest event: {e}");
1224 return None;
1225 }
1226 };
1227
1228 let redacted_because = match RedactedBecause::from_raw_event(raw_redaction) {
1229 Ok(rb) => rb,
1230 Err(e) => {
1231 warn!("Redaction event is not valid canonical JSON: {e}");
1232 return None;
1233 }
1234 };
1235
1236 let redact_result = redact_in_place(&mut event_json, rules, Some(redacted_because));
1237
1238 if let Err(e) = redact_result {
1239 warn!("Failed to redact event: {e}");
1240 return None;
1241 }
1242
1243 let raw = Raw::new(&event_json).expect("CanonicalJsonObject must be serializable");
1244 Some(raw.cast_unchecked())
1245}
1246
1247#[derive(Debug, Clone)]
1257pub struct RoomInfoNotableUpdate {
1258 pub room_id: OwnedRoomId,
1260
1261 pub reasons: RoomInfoNotableUpdateReasons,
1263}
1264
1265bitflags! {
1266 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1268 pub struct RoomInfoNotableUpdateReasons: u8 {
1269 const RECENCY_STAMP = 0b0000_0001;
1271
1272 const LATEST_EVENT = 0b0000_0010;
1274
1275 const READ_RECEIPT = 0b0000_0100;
1277
1278 const UNREAD_MARKER = 0b0000_1000;
1280
1281 const MEMBERSHIP = 0b0001_0000;
1283
1284 const DISPLAY_NAME = 0b0010_0000;
1286
1287 const NONE = 0b1000_0000;
1298 }
1299}
1300
1301impl Default for RoomInfoNotableUpdateReasons {
1302 fn default() -> Self {
1303 Self::empty()
1304 }
1305}
1306
1307#[cfg(test)]
1308mod tests {
1309 use std::{str::FromStr, sync::Arc};
1310
1311 use assert_matches::assert_matches;
1312 use matrix_sdk_test::{async_test, event_factory::EventFactory};
1313 use ruma::{
1314 assign,
1315 events::{
1316 AnyRoomAccountDataEvent,
1317 room::pinned_events::RoomPinnedEventsEventContent,
1318 tag::{TagInfo, TagName, Tags, UserTagName},
1319 },
1320 owned_event_id, owned_mxc_uri, owned_user_id, room_id,
1321 serde::Raw,
1322 user_id,
1323 };
1324 use serde_json::json;
1325 use similar_asserts::assert_eq;
1326
1327 use super::{BaseRoomInfo, LatestEventValue, RoomInfo, SyncInfo};
1328 use crate::{
1329 RoomDisplayName, RoomHero, RoomState, StateChanges,
1330 notification_settings::RoomNotificationMode,
1331 room::{RoomNotableTags, RoomSummary},
1332 store::{IntoStateStore, MemoryStore},
1333 sync::UnreadNotificationsCount,
1334 };
1335
1336 #[test]
1337 fn test_room_info_serialization() {
1338 let info = RoomInfo {
1342 data_format_version: 1,
1343 room_id: room_id!("!gda78o:server.tld").into(),
1344 room_state: RoomState::Invited,
1345 notification_counts: UnreadNotificationsCount {
1346 highlight_count: 1,
1347 notification_count: 2,
1348 },
1349 summary: RoomSummary {
1350 room_heroes: vec![RoomHero {
1351 user_id: owned_user_id!("@somebody:example.org"),
1352 display_name: None,
1353 avatar_url: None,
1354 }],
1355 joined_member_count: 5,
1356 invited_member_count: 0,
1357 },
1358 members_synced: true,
1359 last_prev_batch: Some("pb".to_owned()),
1360 sync_info: SyncInfo::FullySynced,
1361 encryption_state_synced: true,
1362 latest_event_value: LatestEventValue::None,
1363 base_info: Box::new(
1364 assign!(BaseRoomInfo::new(), { pinned_events: Some(RoomPinnedEventsEventContent::new(vec![owned_event_id!("$a")]).into()) }),
1365 ),
1366 read_receipts: Default::default(),
1367 warned_about_unknown_room_version_rules: Arc::new(false.into()),
1368 cached_display_name: None,
1369 cached_user_defined_notification_mode: None,
1370 recency_stamp: Some(42.into()),
1371 };
1372
1373 let info_json = json!({
1374 "data_format_version": 1,
1375 "room_id": "!gda78o:server.tld",
1376 "room_state": "Invited",
1377 "notification_counts": {
1378 "highlight_count": 1,
1379 "notification_count": 2,
1380 },
1381 "summary": {
1382 "room_heroes": [{
1383 "user_id": "@somebody:example.org",
1384 "display_name": null,
1385 "avatar_url": null
1386 }],
1387 "joined_member_count": 5,
1388 "invited_member_count": 0,
1389 },
1390 "members_synced": true,
1391 "last_prev_batch": "pb",
1392 "sync_info": "FullySynced",
1393 "encryption_state_synced": true,
1394 "latest_event_value": "None",
1395 "base_info": {
1396 "avatar": null,
1397 "canonical_alias": null,
1398 "create": null,
1399 "dm_targets": [],
1400 "encryption": null,
1401 "guest_access": null,
1402 "history_visibility": null,
1403 "is_marked_unread": false,
1404 "is_marked_unread_source": "Unstable",
1405 "join_rules": null,
1406 "max_power_level": 100,
1407 "member_hints": null,
1408 "name": null,
1409 "tombstone": null,
1410 "topic": null,
1411 "pinned_events": {
1412 "pinned": ["$a"]
1413 },
1414 },
1415 "read_receipts": {
1416 "num_unread": 0,
1417 "num_mentions": 0,
1418 "num_notifications": 0,
1419 "latest_active": null,
1420 "pending": [],
1421 },
1422 "recency_stamp": 42,
1423 });
1424
1425 assert_eq!(serde_json::to_value(info).unwrap(), info_json);
1426 }
1427
1428 #[async_test]
1429 async fn test_room_info_migration_v1() {
1430 let store = MemoryStore::new().into_state_store();
1431
1432 let room_info_json = json!({
1433 "room_id": "!gda78o:server.tld",
1434 "room_state": "Joined",
1435 "notification_counts": {
1436 "highlight_count": 1,
1437 "notification_count": 2,
1438 },
1439 "summary": {
1440 "room_heroes": [{
1441 "user_id": "@somebody:example.org",
1442 "display_name": null,
1443 "avatar_url": null
1444 }],
1445 "joined_member_count": 5,
1446 "invited_member_count": 0,
1447 },
1448 "members_synced": true,
1449 "last_prev_batch": "pb",
1450 "sync_info": "FullySynced",
1451 "encryption_state_synced": true,
1452 "latest_event": {
1453 "event": {
1454 "encryption_info": null,
1455 "event": {
1456 "sender": "@u:i.uk",
1457 },
1458 },
1459 },
1460 "base_info": {
1461 "avatar": null,
1462 "canonical_alias": null,
1463 "create": null,
1464 "dm_targets": [],
1465 "encryption": null,
1466 "guest_access": null,
1467 "history_visibility": null,
1468 "join_rules": null,
1469 "max_power_level": 100,
1470 "name": null,
1471 "tombstone": null,
1472 "topic": null,
1473 },
1474 "read_receipts": {
1475 "num_unread": 0,
1476 "num_mentions": 0,
1477 "num_notifications": 0,
1478 "latest_active": null,
1479 "pending": []
1480 },
1481 "recency_stamp": 42,
1482 });
1483 let mut room_info: RoomInfo = serde_json::from_value(room_info_json).unwrap();
1484
1485 assert_eq!(room_info.data_format_version, 0);
1486 assert!(room_info.base_info.notable_tags.is_empty());
1487 assert!(room_info.base_info.pinned_events.is_none());
1488
1489 assert!(room_info.apply_migrations(store.clone()).await);
1491
1492 assert_eq!(room_info.data_format_version, 1);
1493 assert!(room_info.base_info.notable_tags.is_empty());
1494 assert!(room_info.base_info.pinned_events.is_none());
1495
1496 assert!(!room_info.apply_migrations(store.clone()).await);
1498
1499 assert_eq!(room_info.data_format_version, 1);
1500 assert!(room_info.base_info.notable_tags.is_empty());
1501 assert!(room_info.base_info.pinned_events.is_none());
1502
1503 let mut changes = StateChanges::default();
1505
1506 let f = EventFactory::new().room(&room_info.room_id).sender(user_id!("@example:localhost"));
1507 let mut tags = Tags::new();
1508 tags.insert(TagName::Favorite, TagInfo::new());
1509 tags.insert(TagName::User(UserTagName::from_str("u.work").unwrap()), TagInfo::new());
1510 let raw_tag_event: Raw<AnyRoomAccountDataEvent> = f.tag(tags).into();
1511 let tag_event = raw_tag_event.deserialize().unwrap();
1512 changes.add_room_account_data(&room_info.room_id, tag_event, raw_tag_event);
1513
1514 let raw_pinned_events_event: Raw<_> = f
1515 .room_pinned_events(vec![owned_event_id!("$a"), owned_event_id!("$b")])
1516 .into_raw_sync_state();
1517 let pinned_events_event = raw_pinned_events_event.deserialize().unwrap();
1518 changes.add_state_event(&room_info.room_id, pinned_events_event, raw_pinned_events_event);
1519
1520 store.save_changes(&changes).await.unwrap();
1521
1522 room_info.data_format_version = 0;
1524 assert!(room_info.apply_migrations(store.clone()).await);
1525
1526 assert_eq!(room_info.data_format_version, 1);
1527 assert!(room_info.base_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
1528 assert!(room_info.base_info.pinned_events.is_some());
1529
1530 let new_room_info = RoomInfo::new(room_id!("!new_room:localhost"), RoomState::Joined);
1532 assert_eq!(new_room_info.data_format_version, 1);
1533 }
1534
1535 #[test]
1536 fn test_room_info_deserialization() {
1537 let info_json = json!({
1538 "room_id": "!gda78o:server.tld",
1539 "room_state": "Joined",
1540 "notification_counts": {
1541 "highlight_count": 1,
1542 "notification_count": 2,
1543 },
1544 "summary": {
1545 "room_heroes": [{
1546 "user_id": "@somebody:example.org",
1547 "display_name": "Somebody",
1548 "avatar_url": "mxc://example.org/abc"
1549 }],
1550 "joined_member_count": 5,
1551 "invited_member_count": 0,
1552 },
1553 "members_synced": true,
1554 "last_prev_batch": "pb",
1555 "sync_info": "FullySynced",
1556 "encryption_state_synced": true,
1557 "base_info": {
1558 "avatar": null,
1559 "canonical_alias": null,
1560 "create": null,
1561 "dm_targets": [],
1562 "encryption": null,
1563 "guest_access": null,
1564 "history_visibility": null,
1565 "join_rules": null,
1566 "max_power_level": 100,
1567 "member_hints": null,
1568 "name": null,
1569 "tombstone": null,
1570 "topic": null,
1571 },
1572 "cached_display_name": { "Calculated": "lol" },
1573 "cached_user_defined_notification_mode": "Mute",
1574 "recency_stamp": 42,
1575 });
1576
1577 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1578
1579 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1580 assert_eq!(info.room_state, RoomState::Joined);
1581 assert_eq!(info.notification_counts.highlight_count, 1);
1582 assert_eq!(info.notification_counts.notification_count, 2);
1583 assert_eq!(
1584 info.summary.room_heroes,
1585 vec![RoomHero {
1586 user_id: owned_user_id!("@somebody:example.org"),
1587 display_name: Some("Somebody".to_owned()),
1588 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1589 }]
1590 );
1591 assert_eq!(info.summary.joined_member_count, 5);
1592 assert_eq!(info.summary.invited_member_count, 0);
1593 assert!(info.members_synced);
1594 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1595 assert_eq!(info.sync_info, SyncInfo::FullySynced);
1596 assert!(info.encryption_state_synced);
1597 assert_matches!(info.latest_event_value, LatestEventValue::None);
1598 assert!(info.base_info.avatar.is_none());
1599 assert!(info.base_info.canonical_alias.is_none());
1600 assert!(info.base_info.create.is_none());
1601 assert_eq!(info.base_info.dm_targets.len(), 0);
1602 assert!(info.base_info.encryption.is_none());
1603 assert!(info.base_info.guest_access.is_none());
1604 assert!(info.base_info.history_visibility.is_none());
1605 assert!(info.base_info.join_rules.is_none());
1606 assert_eq!(info.base_info.max_power_level, 100);
1607 assert!(info.base_info.member_hints.is_none());
1608 assert!(info.base_info.name.is_none());
1609 assert!(info.base_info.tombstone.is_none());
1610 assert!(info.base_info.topic.is_none());
1611
1612 assert_eq!(
1613 info.cached_display_name.as_ref(),
1614 Some(&RoomDisplayName::Calculated("lol".to_owned())),
1615 );
1616 assert_eq!(
1617 info.cached_user_defined_notification_mode.as_ref(),
1618 Some(&RoomNotificationMode::Mute)
1619 );
1620 assert_eq!(info.recency_stamp.as_ref(), Some(&42.into()));
1621 }
1622
1623 #[test]
1630 fn test_room_info_deserialization_without_optional_items() {
1631 let info_json = json!({
1634 "room_id": "!gda78o:server.tld",
1635 "room_state": "Invited",
1636 "notification_counts": {
1637 "highlight_count": 1,
1638 "notification_count": 2,
1639 },
1640 "summary": {
1641 "room_heroes": [{
1642 "user_id": "@somebody:example.org",
1643 "display_name": "Somebody",
1644 "avatar_url": "mxc://example.org/abc"
1645 }],
1646 "joined_member_count": 5,
1647 "invited_member_count": 0,
1648 },
1649 "members_synced": true,
1650 "last_prev_batch": "pb",
1651 "sync_info": "FullySynced",
1652 "encryption_state_synced": true,
1653 "base_info": {
1654 "avatar": null,
1655 "canonical_alias": null,
1656 "create": null,
1657 "dm_targets": [],
1658 "encryption": null,
1659 "guest_access": null,
1660 "history_visibility": null,
1661 "join_rules": null,
1662 "max_power_level": 100,
1663 "name": null,
1664 "tombstone": null,
1665 "topic": null,
1666 },
1667 });
1668
1669 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1670
1671 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1672 assert_eq!(info.room_state, RoomState::Invited);
1673 assert_eq!(info.notification_counts.highlight_count, 1);
1674 assert_eq!(info.notification_counts.notification_count, 2);
1675 assert_eq!(
1676 info.summary.room_heroes,
1677 vec![RoomHero {
1678 user_id: owned_user_id!("@somebody:example.org"),
1679 display_name: Some("Somebody".to_owned()),
1680 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1681 }]
1682 );
1683 assert_eq!(info.summary.joined_member_count, 5);
1684 assert_eq!(info.summary.invited_member_count, 0);
1685 assert!(info.members_synced);
1686 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1687 assert_eq!(info.sync_info, SyncInfo::FullySynced);
1688 assert!(info.encryption_state_synced);
1689 assert!(info.base_info.avatar.is_none());
1690 assert!(info.base_info.canonical_alias.is_none());
1691 assert!(info.base_info.create.is_none());
1692 assert_eq!(info.base_info.dm_targets.len(), 0);
1693 assert!(info.base_info.encryption.is_none());
1694 assert!(info.base_info.guest_access.is_none());
1695 assert!(info.base_info.history_visibility.is_none());
1696 assert!(info.base_info.join_rules.is_none());
1697 assert_eq!(info.base_info.max_power_level, 100);
1698 assert!(info.base_info.name.is_none());
1699 assert!(info.base_info.tombstone.is_none());
1700 assert!(info.base_info.topic.is_none());
1701 }
1702}