1use std::{
16 collections::{BTreeMap, HashSet},
17 sync::{Arc, atomic::AtomicBool},
18};
19
20use bitflags::bitflags;
21use eyeball::Subscriber;
22use matrix_sdk_common::{
23 ROOM_VERSION_FALLBACK, ROOM_VERSION_RULES_FALLBACK, deserialized_responses::TimelineEventKind,
24};
25use ruma::{
26 EventId, MilliSecondsSinceUnixEpoch, MxcUri, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId,
27 OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, RoomVersionId,
28 api::client::sync::sync_events::v3::RoomSummary as RumaSummary,
29 assign,
30 events::{
31 AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, StateEventType,
32 SyncStateEvent,
33 beacon_info::BeaconInfoEventContent,
34 call::member::{CallMemberEventContent, CallMemberStateKey, MembershipData},
35 direct::OwnedDirectUserIdentifier,
36 room::{
37 avatar::{self, RoomAvatarEventContent},
38 canonical_alias::RoomCanonicalAliasEventContent,
39 encryption::RoomEncryptionEventContent,
40 guest_access::{GuestAccess, RoomGuestAccessEventContent},
41 history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
42 join_rules::{JoinRule, RoomJoinRulesEventContent},
43 name::RoomNameEventContent,
44 pinned_events::RoomPinnedEventsEventContent,
45 redaction::SyncRoomRedactionEvent,
46 tombstone::RoomTombstoneEventContent,
47 topic::RoomTopicEventContent,
48 },
49 tag::{TagEventContent, TagName, Tags},
50 },
51 room::RoomType,
52 room_version_rules::{AuthorizationRules, RedactionRules, RoomVersionRules},
53 serde::Raw,
54};
55use serde::{Deserialize, Serialize};
56use tracing::{debug, error, field::debug, info, instrument, warn};
57
58use super::{
59 AccountDataSource, EncryptionState, Room, RoomCreateWithCreatorEventContent, RoomDisplayName,
60 RoomHero, RoomNotableTags, RoomState, RoomSummary,
61};
62use crate::{
63 MinimalStateEvent, OriginalMinimalStateEvent,
64 deserialized_responses::RawSyncOrStrippedState,
65 latest_event::{LatestEvent, LatestEventValue},
66 notification_settings::RoomNotificationMode,
67 read_receipts::RoomReadReceipts,
68 store::{DynStateStore, StateStoreExt},
69 sync::UnreadNotificationsCount,
70};
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct InviteAcceptanceDetails {
76 pub invite_accepted_at: MilliSecondsSinceUnixEpoch,
79
80 pub inviter: OwnedUserId,
82}
83
84impl Room {
85 pub fn subscribe_info(&self) -> Subscriber<RoomInfo> {
87 self.info.subscribe()
88 }
89
90 pub fn clone_info(&self) -> RoomInfo {
92 self.info.get()
93 }
94
95 pub fn set_room_info(
97 &self,
98 room_info: RoomInfo,
99 room_info_notable_update_reasons: RoomInfoNotableUpdateReasons,
100 ) {
101 self.info.set(room_info);
102
103 if !room_info_notable_update_reasons.is_empty() {
104 let _ = self.room_info_notable_update_sender.send(RoomInfoNotableUpdate {
106 room_id: self.room_id.clone(),
107 reasons: room_info_notable_update_reasons,
108 });
109 } else {
110 let _ = self.room_info_notable_update_sender.send(RoomInfoNotableUpdate {
114 room_id: self.room_id.clone(),
115 reasons: RoomInfoNotableUpdateReasons::NONE,
116 });
117 }
118 }
119}
120
121#[derive(Clone, Debug, Serialize, Deserialize)]
125pub struct BaseRoomInfo {
126 pub(crate) avatar: Option<MinimalStateEvent<RoomAvatarEventContent>>,
128 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
130 pub(crate) beacons: BTreeMap<OwnedUserId, MinimalStateEvent<BeaconInfoEventContent>>,
131 pub(crate) canonical_alias: Option<MinimalStateEvent<RoomCanonicalAliasEventContent>>,
133 pub(crate) create: Option<MinimalStateEvent<RoomCreateWithCreatorEventContent>>,
135 pub(crate) dm_targets: HashSet<OwnedDirectUserIdentifier>,
138 pub(crate) encryption: Option<RoomEncryptionEventContent>,
140 pub(crate) guest_access: Option<MinimalStateEvent<RoomGuestAccessEventContent>>,
142 pub(crate) history_visibility: Option<MinimalStateEvent<RoomHistoryVisibilityEventContent>>,
144 pub(crate) join_rules: Option<MinimalStateEvent<RoomJoinRulesEventContent>>,
146 pub(crate) max_power_level: i64,
148 pub(crate) name: Option<MinimalStateEvent<RoomNameEventContent>>,
150 pub(crate) tombstone: Option<MinimalStateEvent<RoomTombstoneEventContent>>,
152 pub(crate) topic: Option<MinimalStateEvent<RoomTopicEventContent>>,
154 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
157 pub(crate) rtc_member_events:
158 BTreeMap<CallMemberStateKey, MinimalStateEvent<CallMemberEventContent>>,
159 #[serde(default)]
161 pub(crate) is_marked_unread: bool,
162 #[serde(default)]
164 pub(crate) is_marked_unread_source: AccountDataSource,
165 #[serde(skip_serializing_if = "RoomNotableTags::is_empty", default)]
170 pub(crate) notable_tags: RoomNotableTags,
171 pub(crate) pinned_events: Option<RoomPinnedEventsEventContent>,
173}
174
175impl BaseRoomInfo {
176 pub fn new() -> Self {
178 Self::default()
179 }
180
181 pub fn room_version(&self) -> Option<&RoomVersionId> {
186 match self.create.as_ref()? {
187 MinimalStateEvent::Original(ev) => Some(&ev.content.room_version),
188 MinimalStateEvent::Redacted(ev) => Some(&ev.content.room_version),
189 }
190 }
191
192 pub fn handle_state_event(&mut self, ev: &AnySyncStateEvent) -> bool {
196 match ev {
197 AnySyncStateEvent::BeaconInfo(b) => {
198 self.beacons.insert(b.state_key().clone(), b.into());
199 }
200 AnySyncStateEvent::RoomEncryption(SyncStateEvent::Original(encryption)) => {
202 self.encryption = Some(encryption.content.clone());
203 }
204 AnySyncStateEvent::RoomAvatar(a) => {
205 self.avatar = Some(a.into());
206 }
207 AnySyncStateEvent::RoomName(n) => {
208 self.name = Some(n.into());
209 }
210 AnySyncStateEvent::RoomCreate(c) if self.create.is_none() => {
212 self.create = Some(c.into());
213 }
214 AnySyncStateEvent::RoomHistoryVisibility(h) => {
215 self.history_visibility = Some(h.into());
216 }
217 AnySyncStateEvent::RoomGuestAccess(g) => {
218 self.guest_access = Some(g.into());
219 }
220 AnySyncStateEvent::RoomJoinRules(c) => match c.join_rule() {
221 JoinRule::Invite
222 | JoinRule::Knock
223 | JoinRule::Private
224 | JoinRule::Restricted(_)
225 | JoinRule::KnockRestricted(_)
226 | JoinRule::Public => self.join_rules = Some(c.into()),
227 r => warn!("Encountered a custom join rule {}, skipping", r.as_str()),
228 },
229 AnySyncStateEvent::RoomCanonicalAlias(a) => {
230 self.canonical_alias = Some(a.into());
231 }
232 AnySyncStateEvent::RoomTopic(t) => {
233 self.topic = Some(t.into());
234 }
235 AnySyncStateEvent::RoomTombstone(t) => {
236 self.tombstone = Some(t.into());
237 }
238 AnySyncStateEvent::RoomPowerLevels(p) => {
239 self.max_power_level = p.power_levels(&AuthorizationRules::V1, vec![]).max().into();
241 }
242 AnySyncStateEvent::CallMember(m) => {
243 let Some(o_ev) = m.as_original() else {
244 return false;
245 };
246
247 let mut o_ev = o_ev.clone();
250 o_ev.content.set_created_ts_if_none(o_ev.origin_server_ts);
251
252 self.rtc_member_events
254 .insert(m.state_key().clone(), SyncStateEvent::Original(o_ev).into());
255
256 self.rtc_member_events.retain(|_, ev| {
258 ev.as_original().is_some_and(|o| !o.content.active_memberships(None).is_empty())
259 });
260 }
261 AnySyncStateEvent::RoomPinnedEvents(p) => {
262 self.pinned_events = p.as_original().map(|p| p.content.clone());
263 }
264 _ => return false,
265 }
266
267 true
268 }
269
270 pub fn handle_stripped_state_event(&mut self, ev: &AnyStrippedStateEvent) -> bool {
275 match ev {
276 AnyStrippedStateEvent::RoomEncryption(encryption) => {
277 if let Some(algorithm) = &encryption.content.algorithm {
278 let content = assign!(RoomEncryptionEventContent::new(algorithm.clone()), {
279 rotation_period_ms: encryption.content.rotation_period_ms,
280 rotation_period_msgs: encryption.content.rotation_period_msgs,
281 });
282 self.encryption = Some(content);
283 }
284 }
288 AnyStrippedStateEvent::RoomAvatar(a) => {
289 self.avatar = Some(a.into());
290 }
291 AnyStrippedStateEvent::RoomName(n) => {
292 self.name = Some(n.into());
293 }
294 AnyStrippedStateEvent::RoomCreate(c) if self.create.is_none() => {
295 self.create = Some(c.into());
296 }
297 AnyStrippedStateEvent::RoomHistoryVisibility(h) => {
298 self.history_visibility = Some(h.into());
299 }
300 AnyStrippedStateEvent::RoomGuestAccess(g) => {
301 self.guest_access = Some(g.into());
302 }
303 AnyStrippedStateEvent::RoomJoinRules(c) => match &c.content.join_rule {
304 JoinRule::Invite
305 | JoinRule::Knock
306 | JoinRule::Private
307 | JoinRule::Restricted(_)
308 | JoinRule::KnockRestricted(_)
309 | JoinRule::Public => self.join_rules = Some(c.into()),
310 r => warn!("Encountered a custom join rule {}, skipping", r.as_str()),
311 },
312 AnyStrippedStateEvent::RoomCanonicalAlias(a) => {
313 self.canonical_alias = Some(a.into());
314 }
315 AnyStrippedStateEvent::RoomTopic(t) => {
316 self.topic = Some(t.into());
317 }
318 AnyStrippedStateEvent::RoomTombstone(t) => {
319 self.tombstone = Some(t.into());
320 }
321 AnyStrippedStateEvent::RoomPowerLevels(p) => {
322 self.max_power_level = p.power_levels(&AuthorizationRules::V1, vec![]).max().into();
324 }
325 AnyStrippedStateEvent::CallMember(_) => {
326 return false;
329 }
330 AnyStrippedStateEvent::RoomPinnedEvents(p) => {
331 if let Some(pinned) = p.content.pinned.clone() {
332 self.pinned_events = Some(RoomPinnedEventsEventContent::new(pinned));
333 }
334 }
335 _ => return false,
336 }
337
338 true
339 }
340
341 pub(super) fn handle_redaction(&mut self, redacts: &EventId) {
342 let redaction_rules = self
343 .room_version()
344 .and_then(|room_version| room_version.rules())
345 .unwrap_or(ROOM_VERSION_RULES_FALLBACK)
346 .redaction;
347
348 if let Some(ev) = &mut self.avatar
349 && ev.event_id() == Some(redacts)
350 {
351 ev.redact(&redaction_rules);
352 } else if let Some(ev) = &mut self.canonical_alias
353 && ev.event_id() == Some(redacts)
354 {
355 ev.redact(&redaction_rules);
356 } else if let Some(ev) = &mut self.create
357 && ev.event_id() == Some(redacts)
358 {
359 ev.redact(&redaction_rules);
360 } else if let Some(ev) = &mut self.guest_access
361 && ev.event_id() == Some(redacts)
362 {
363 ev.redact(&redaction_rules);
364 } else if let Some(ev) = &mut self.history_visibility
365 && ev.event_id() == Some(redacts)
366 {
367 ev.redact(&redaction_rules);
368 } else if let Some(ev) = &mut self.join_rules
369 && ev.event_id() == Some(redacts)
370 {
371 ev.redact(&redaction_rules);
372 } else if let Some(ev) = &mut self.name
373 && ev.event_id() == Some(redacts)
374 {
375 ev.redact(&redaction_rules);
376 } else if let Some(ev) = &mut self.tombstone
377 && ev.event_id() == Some(redacts)
378 {
379 ev.redact(&redaction_rules);
380 } else if let Some(ev) = &mut self.topic
381 && ev.event_id() == Some(redacts)
382 {
383 ev.redact(&redaction_rules);
384 } else {
385 self.rtc_member_events
386 .retain(|_, member_event| member_event.event_id() != Some(redacts));
387 }
388 }
389
390 pub fn handle_notable_tags(&mut self, tags: &Tags) {
391 let mut notable_tags = RoomNotableTags::empty();
392
393 if tags.contains_key(&TagName::Favorite) {
394 notable_tags.insert(RoomNotableTags::FAVOURITE);
395 }
396
397 if tags.contains_key(&TagName::LowPriority) {
398 notable_tags.insert(RoomNotableTags::LOW_PRIORITY);
399 }
400
401 self.notable_tags = notable_tags;
402 }
403}
404
405impl Default for BaseRoomInfo {
406 fn default() -> Self {
407 Self {
408 avatar: None,
409 beacons: BTreeMap::new(),
410 canonical_alias: None,
411 create: None,
412 dm_targets: Default::default(),
413 encryption: None,
414 guest_access: None,
415 history_visibility: None,
416 join_rules: None,
417 max_power_level: 100,
418 name: None,
419 tombstone: None,
420 topic: None,
421 rtc_member_events: BTreeMap::new(),
422 is_marked_unread: false,
423 is_marked_unread_source: AccountDataSource::Unstable,
424 notable_tags: RoomNotableTags::empty(),
425 pinned_events: None,
426 }
427 }
428}
429
430#[derive(Clone, Debug, Serialize, Deserialize)]
434pub struct RoomInfo {
435 #[serde(default, alias = "version")]
438 pub(crate) data_format_version: u8,
439
440 pub(crate) room_id: OwnedRoomId,
442
443 pub(crate) room_state: RoomState,
445
446 pub(crate) notification_counts: UnreadNotificationsCount,
451
452 pub(crate) summary: RoomSummary,
454
455 pub(crate) members_synced: bool,
457
458 pub(crate) last_prev_batch: Option<String>,
460
461 pub(crate) sync_info: SyncInfo,
463
464 pub(crate) encryption_state_synced: bool,
466
467 pub(crate) latest_event: Option<Box<LatestEvent>>,
471
472 #[serde(default)]
476 pub(crate) new_latest_event: LatestEventValue,
477
478 #[serde(default)]
480 pub(crate) read_receipts: RoomReadReceipts,
481
482 pub(crate) base_info: Box<BaseRoomInfo>,
485
486 #[serde(skip)]
490 pub(crate) warned_about_unknown_room_version_rules: Arc<AtomicBool>,
491
492 #[serde(default, skip_serializing_if = "Option::is_none")]
497 pub(crate) cached_display_name: Option<RoomDisplayName>,
498
499 #[serde(default, skip_serializing_if = "Option::is_none")]
501 pub(crate) cached_user_defined_notification_mode: Option<RoomNotificationMode>,
502
503 #[serde(default)]
520 pub(crate) recency_stamp: Option<RoomRecencyStamp>,
521
522 #[serde(default, skip_serializing_if = "Option::is_none")]
528 pub(crate) invite_acceptance_details: Option<InviteAcceptanceDetails>,
529}
530
531impl RoomInfo {
532 #[doc(hidden)] pub fn new(room_id: &RoomId, room_state: RoomState) -> Self {
534 Self {
535 data_format_version: 1,
536 room_id: room_id.into(),
537 room_state,
538 notification_counts: Default::default(),
539 summary: Default::default(),
540 members_synced: false,
541 last_prev_batch: None,
542 sync_info: SyncInfo::NoState,
543 encryption_state_synced: false,
544 latest_event: None,
545 new_latest_event: LatestEventValue::default(),
546 read_receipts: Default::default(),
547 base_info: Box::new(BaseRoomInfo::new()),
548 warned_about_unknown_room_version_rules: Arc::new(false.into()),
549 cached_display_name: None,
550 cached_user_defined_notification_mode: None,
551 recency_stamp: None,
552 invite_acceptance_details: None,
553 }
554 }
555
556 pub fn mark_as_joined(&mut self) {
558 self.set_state(RoomState::Joined);
559 }
560
561 pub fn mark_as_left(&mut self) {
563 self.set_state(RoomState::Left);
564 }
565
566 pub fn mark_as_invited(&mut self) {
568 self.set_state(RoomState::Invited);
569 }
570
571 pub fn mark_as_knocked(&mut self) {
573 self.set_state(RoomState::Knocked);
574 }
575
576 pub fn mark_as_banned(&mut self) {
578 self.set_state(RoomState::Banned);
579 }
580
581 pub fn set_state(&mut self, room_state: RoomState) {
583 if self.state() != RoomState::Joined && self.invite_acceptance_details.is_some() {
584 error!(room_id = %self.room_id, "The RoomInfo contains invite acceptance details but the room is not in the joined state");
585 }
586 self.invite_acceptance_details = None;
589 self.room_state = room_state;
590 }
591
592 pub fn mark_members_synced(&mut self) {
594 self.members_synced = true;
595 }
596
597 pub fn mark_members_missing(&mut self) {
599 self.members_synced = false;
600 }
601
602 pub fn are_members_synced(&self) -> bool {
604 self.members_synced
605 }
606
607 pub fn mark_state_partially_synced(&mut self) {
609 self.sync_info = SyncInfo::PartiallySynced;
610 }
611
612 pub fn mark_state_fully_synced(&mut self) {
614 self.sync_info = SyncInfo::FullySynced;
615 }
616
617 pub fn mark_state_not_synced(&mut self) {
619 self.sync_info = SyncInfo::NoState;
620 }
621
622 pub fn mark_encryption_state_synced(&mut self) {
624 self.encryption_state_synced = true;
625 }
626
627 pub fn mark_encryption_state_missing(&mut self) {
629 self.encryption_state_synced = false;
630 }
631
632 pub fn set_prev_batch(&mut self, prev_batch: Option<&str>) -> bool {
636 if self.last_prev_batch.as_deref() != prev_batch {
637 self.last_prev_batch = prev_batch.map(|p| p.to_owned());
638 true
639 } else {
640 false
641 }
642 }
643
644 pub fn state(&self) -> RoomState {
646 self.room_state
647 }
648
649 #[cfg(not(feature = "experimental-encrypted-state-events"))]
651 pub fn encryption_state(&self) -> EncryptionState {
652 if !self.encryption_state_synced {
653 EncryptionState::Unknown
654 } else if self.base_info.encryption.is_some() {
655 EncryptionState::Encrypted
656 } else {
657 EncryptionState::NotEncrypted
658 }
659 }
660
661 #[cfg(feature = "experimental-encrypted-state-events")]
663 pub fn encryption_state(&self) -> EncryptionState {
664 if !self.encryption_state_synced {
665 EncryptionState::Unknown
666 } else {
667 self.base_info
668 .encryption
669 .as_ref()
670 .map(|state| {
671 if state.encrypt_state_events {
672 EncryptionState::StateEncrypted
673 } else {
674 EncryptionState::Encrypted
675 }
676 })
677 .unwrap_or(EncryptionState::NotEncrypted)
678 }
679 }
680
681 pub fn set_encryption_event(&mut self, event: Option<RoomEncryptionEventContent>) {
683 self.base_info.encryption = event;
684 }
685
686 pub fn handle_encryption_state(
688 &mut self,
689 requested_required_states: &[(StateEventType, String)],
690 ) {
691 if requested_required_states
692 .iter()
693 .any(|(state_event, _)| state_event == &StateEventType::RoomEncryption)
694 {
695 self.mark_encryption_state_synced();
701 }
702 }
703
704 pub fn handle_state_event(&mut self, event: &AnySyncStateEvent) -> bool {
708 let base_info_has_been_modified = self.base_info.handle_state_event(event);
710
711 if let AnySyncStateEvent::RoomEncryption(_) = event {
712 self.mark_encryption_state_synced();
718 }
719
720 base_info_has_been_modified
721 }
722
723 pub fn handle_stripped_state_event(&mut self, event: &AnyStrippedStateEvent) -> bool {
727 self.base_info.handle_stripped_state_event(event)
728 }
729
730 #[instrument(skip_all, fields(redacts))]
732 pub fn handle_redaction(
733 &mut self,
734 event: &SyncRoomRedactionEvent,
735 _raw: &Raw<SyncRoomRedactionEvent>,
736 ) {
737 let redaction_rules = self.room_version_rules_or_default().redaction;
738
739 let Some(redacts) = event.redacts(&redaction_rules) else {
740 info!("Can't apply redaction, redacts field is missing");
741 return;
742 };
743 tracing::Span::current().record("redacts", debug(redacts));
744
745 if let Some(latest_event) = &mut self.latest_event {
746 tracing::trace!("Checking if redaction applies to latest event");
747 if latest_event.event_id().as_deref() == Some(redacts) {
748 match apply_redaction(latest_event.event().raw(), _raw, &redaction_rules) {
749 Some(redacted) => {
750 latest_event.event_mut().kind =
753 TimelineEventKind::PlainText { event: redacted };
754 debug!("Redacted latest event");
755 }
756 None => {
757 self.latest_event = None;
758 debug!("Removed latest event");
759 }
760 }
761 }
762 }
763
764 self.base_info.handle_redaction(redacts);
765 }
766
767 pub fn avatar_url(&self) -> Option<&MxcUri> {
769 self.base_info
770 .avatar
771 .as_ref()
772 .and_then(|e| e.as_original().and_then(|e| e.content.url.as_deref()))
773 }
774
775 pub fn update_avatar(&mut self, url: Option<OwnedMxcUri>) {
777 self.base_info.avatar = url.map(|url| {
778 let mut content = RoomAvatarEventContent::new();
779 content.url = Some(url);
780
781 MinimalStateEvent::Original(OriginalMinimalStateEvent { content, event_id: None })
782 });
783 }
784
785 pub fn avatar_info(&self) -> Option<&avatar::ImageInfo> {
787 self.base_info
788 .avatar
789 .as_ref()
790 .and_then(|e| e.as_original().and_then(|e| e.content.info.as_deref()))
791 }
792
793 pub fn update_notification_count(&mut self, notification_counts: UnreadNotificationsCount) {
795 self.notification_counts = notification_counts;
796 }
797
798 pub fn update_from_ruma_summary(&mut self, summary: &RumaSummary) -> bool {
802 let mut changed = false;
803
804 if !summary.is_empty() {
805 if !summary.heroes.is_empty() {
806 self.summary.room_heroes = summary
807 .heroes
808 .iter()
809 .map(|hero_id| RoomHero {
810 user_id: hero_id.to_owned(),
811 display_name: None,
812 avatar_url: None,
813 })
814 .collect();
815
816 changed = true;
817 }
818
819 if let Some(joined) = summary.joined_member_count {
820 self.summary.joined_member_count = joined.into();
821 changed = true;
822 }
823
824 if let Some(invited) = summary.invited_member_count {
825 self.summary.invited_member_count = invited.into();
826 changed = true;
827 }
828 }
829
830 changed
831 }
832
833 pub(crate) fn update_joined_member_count(&mut self, count: u64) {
835 self.summary.joined_member_count = count;
836 }
837
838 pub(crate) fn update_invited_member_count(&mut self, count: u64) {
840 self.summary.invited_member_count = count;
841 }
842
843 pub(crate) fn set_invite_acceptance_details(&mut self, details: InviteAcceptanceDetails) {
844 self.invite_acceptance_details = Some(details);
845 }
846
847 pub fn invite_acceptance_details(&self) -> Option<InviteAcceptanceDetails> {
854 self.invite_acceptance_details.clone()
855 }
856
857 pub(crate) fn update_heroes(&mut self, heroes: Vec<RoomHero>) {
859 self.summary.room_heroes = heroes;
860 }
861
862 pub fn heroes(&self) -> &[RoomHero] {
864 &self.summary.room_heroes
865 }
866
867 pub fn active_members_count(&self) -> u64 {
871 self.summary.joined_member_count.saturating_add(self.summary.invited_member_count)
872 }
873
874 pub fn invited_members_count(&self) -> u64 {
876 self.summary.invited_member_count
877 }
878
879 pub fn joined_members_count(&self) -> u64 {
881 self.summary.joined_member_count
882 }
883
884 pub fn canonical_alias(&self) -> Option<&RoomAliasId> {
886 self.base_info.canonical_alias.as_ref()?.as_original()?.content.alias.as_deref()
887 }
888
889 pub fn alt_aliases(&self) -> &[OwnedRoomAliasId] {
891 self.base_info
892 .canonical_alias
893 .as_ref()
894 .and_then(|ev| ev.as_original())
895 .map(|ev| ev.content.alt_aliases.as_ref())
896 .unwrap_or_default()
897 }
898
899 pub fn room_id(&self) -> &RoomId {
901 &self.room_id
902 }
903
904 pub fn room_version(&self) -> Option<&RoomVersionId> {
906 self.base_info.room_version()
907 }
908
909 pub fn room_version_rules_or_default(&self) -> RoomVersionRules {
914 use std::sync::atomic::Ordering;
915
916 self.base_info.room_version().and_then(|room_version| room_version.rules()).unwrap_or_else(
917 || {
918 if self
919 .warned_about_unknown_room_version_rules
920 .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
921 .is_ok()
922 {
923 warn!("Unable to get the room version rules, defaulting to rules for room version {ROOM_VERSION_FALLBACK}");
924 }
925
926 ROOM_VERSION_RULES_FALLBACK
927 },
928 )
929 }
930
931 pub fn room_type(&self) -> Option<&RoomType> {
933 match self.base_info.create.as_ref()? {
934 MinimalStateEvent::Original(ev) => ev.content.room_type.as_ref(),
935 MinimalStateEvent::Redacted(ev) => ev.content.room_type.as_ref(),
936 }
937 }
938
939 pub fn creators(&self) -> Option<Vec<OwnedUserId>> {
941 match self.base_info.create.as_ref()? {
942 MinimalStateEvent::Original(ev) => Some(ev.content.creators()),
943 MinimalStateEvent::Redacted(ev) => Some(ev.content.creators()),
944 }
945 }
946
947 pub(super) fn guest_access(&self) -> &GuestAccess {
948 match &self.base_info.guest_access {
949 Some(MinimalStateEvent::Original(ev)) => &ev.content.guest_access,
950 _ => &GuestAccess::Forbidden,
951 }
952 }
953
954 pub fn history_visibility(&self) -> Option<&HistoryVisibility> {
958 match &self.base_info.history_visibility {
959 Some(MinimalStateEvent::Original(ev)) => Some(&ev.content.history_visibility),
960 _ => None,
961 }
962 }
963
964 pub fn history_visibility_or_default(&self) -> &HistoryVisibility {
971 match &self.base_info.history_visibility {
972 Some(MinimalStateEvent::Original(ev)) => &ev.content.history_visibility,
973 _ => &HistoryVisibility::Shared,
974 }
975 }
976
977 pub fn join_rule(&self) -> Option<&JoinRule> {
980 match &self.base_info.join_rules {
981 Some(MinimalStateEvent::Original(ev)) => Some(&ev.content.join_rule),
982 _ => None,
983 }
984 }
985
986 pub fn name(&self) -> Option<&str> {
988 let name = &self.base_info.name.as_ref()?.as_original()?.content.name;
989 (!name.is_empty()).then_some(name)
990 }
991
992 pub fn create(&self) -> Option<&RoomCreateWithCreatorEventContent> {
994 Some(&self.base_info.create.as_ref()?.as_original()?.content)
995 }
996
997 pub fn tombstone(&self) -> Option<&RoomTombstoneEventContent> {
999 Some(&self.base_info.tombstone.as_ref()?.as_original()?.content)
1000 }
1001
1002 pub fn topic(&self) -> Option<&str> {
1004 Some(&self.base_info.topic.as_ref()?.as_original()?.content.topic)
1005 }
1006
1007 fn active_matrix_rtc_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
1012 let mut v = self
1013 .base_info
1014 .rtc_member_events
1015 .iter()
1016 .filter_map(|(user_id, ev)| {
1017 ev.as_original().map(|ev| {
1018 ev.content
1019 .active_memberships(None)
1020 .into_iter()
1021 .map(move |m| (user_id.clone(), m))
1022 })
1023 })
1024 .flatten()
1025 .collect::<Vec<_>>();
1026 v.sort_by_key(|(_, m)| m.created_ts());
1027 v
1028 }
1029
1030 fn active_room_call_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
1036 self.active_matrix_rtc_memberships()
1037 .into_iter()
1038 .filter(|(_user_id, m)| m.is_room_call())
1039 .collect()
1040 }
1041
1042 pub fn has_active_room_call(&self) -> bool {
1045 !self.active_room_call_memberships().is_empty()
1046 }
1047
1048 pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
1057 self.active_room_call_memberships()
1058 .iter()
1059 .map(|(call_member_state_key, _)| call_member_state_key.user_id().to_owned())
1060 .collect()
1061 }
1062
1063 pub fn latest_event(&self) -> Option<&LatestEvent> {
1065 self.latest_event.as_deref()
1066 }
1067
1068 pub fn set_new_latest_event(&mut self, new_value: LatestEventValue) {
1070 self.new_latest_event = new_value;
1071 }
1072
1073 pub fn update_recency_stamp(&mut self, stamp: RoomRecencyStamp) {
1077 self.recency_stamp = Some(stamp);
1078 }
1079
1080 pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
1082 self.base_info.pinned_events.clone().map(|c| c.pinned)
1083 }
1084
1085 pub fn is_pinned_event(&self, event_id: &EventId) -> bool {
1091 self.base_info
1092 .pinned_events
1093 .as_ref()
1094 .map(|p| p.pinned.contains(&event_id.to_owned()))
1095 .unwrap_or_default()
1096 }
1097
1098 #[instrument(skip_all, fields(room_id = ?self.room_id))]
1106 pub(crate) async fn apply_migrations(&mut self, store: Arc<DynStateStore>) -> bool {
1107 let mut migrated = false;
1108
1109 if self.data_format_version < 1 {
1110 info!("Migrating room info to version 1");
1111
1112 match store.get_room_account_data_event_static::<TagEventContent>(&self.room_id).await {
1114 Ok(Some(raw_event)) => match raw_event.deserialize() {
1116 Ok(event) => {
1117 self.base_info.handle_notable_tags(&event.content.tags);
1118 }
1119 Err(error) => {
1120 warn!("Failed to deserialize room tags: {error}");
1121 }
1122 },
1123 Ok(_) => {
1124 }
1126 Err(error) => {
1127 warn!("Failed to load room tags: {error}");
1128 }
1129 }
1130
1131 match store.get_state_event_static::<RoomPinnedEventsEventContent>(&self.room_id).await
1133 {
1134 Ok(Some(RawSyncOrStrippedState::Sync(raw_event))) => {
1136 match raw_event.deserialize() {
1137 Ok(event) => {
1138 self.handle_state_event(&event.into());
1139 }
1140 Err(error) => {
1141 warn!("Failed to deserialize room pinned events: {error}");
1142 }
1143 }
1144 }
1145 Ok(_) => {
1146 }
1148 Err(error) => {
1149 warn!("Failed to load room pinned events: {error}");
1150 }
1151 }
1152
1153 self.data_format_version = 1;
1154 migrated = true;
1155 }
1156
1157 migrated
1158 }
1159}
1160
1161#[repr(transparent)]
1163#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
1164#[serde(transparent)]
1165pub struct RoomRecencyStamp(u64);
1166
1167impl From<u64> for RoomRecencyStamp {
1168 fn from(value: u64) -> Self {
1169 Self(value)
1170 }
1171}
1172
1173impl From<RoomRecencyStamp> for u64 {
1174 fn from(value: RoomRecencyStamp) -> Self {
1175 value.0
1176 }
1177}
1178
1179#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1180pub(crate) enum SyncInfo {
1181 NoState,
1187
1188 PartiallySynced,
1191
1192 FullySynced,
1194}
1195
1196pub fn apply_redaction(
1199 event: &Raw<AnySyncTimelineEvent>,
1200 raw_redaction: &Raw<SyncRoomRedactionEvent>,
1201 rules: &RedactionRules,
1202) -> Option<Raw<AnySyncTimelineEvent>> {
1203 use ruma::canonical_json::{RedactedBecause, redact_in_place};
1204
1205 let mut event_json = match event.deserialize_as() {
1206 Ok(json) => json,
1207 Err(e) => {
1208 warn!("Failed to deserialize latest event: {e}");
1209 return None;
1210 }
1211 };
1212
1213 let redacted_because = match RedactedBecause::from_raw_event(raw_redaction) {
1214 Ok(rb) => rb,
1215 Err(e) => {
1216 warn!("Redaction event is not valid canonical JSON: {e}");
1217 return None;
1218 }
1219 };
1220
1221 let redact_result = redact_in_place(&mut event_json, rules, Some(redacted_because));
1222
1223 if let Err(e) = redact_result {
1224 warn!("Failed to redact event: {e}");
1225 return None;
1226 }
1227
1228 let raw = Raw::new(&event_json).expect("CanonicalJsonObject must be serializable");
1229 Some(raw.cast_unchecked())
1230}
1231
1232#[derive(Debug, Clone)]
1242pub struct RoomInfoNotableUpdate {
1243 pub room_id: OwnedRoomId,
1245
1246 pub reasons: RoomInfoNotableUpdateReasons,
1248}
1249
1250bitflags! {
1251 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1253 pub struct RoomInfoNotableUpdateReasons: u8 {
1254 const RECENCY_STAMP = 0b0000_0001;
1256
1257 const LATEST_EVENT = 0b0000_0010;
1259
1260 const READ_RECEIPT = 0b0000_0100;
1262
1263 const UNREAD_MARKER = 0b0000_1000;
1265
1266 const MEMBERSHIP = 0b0001_0000;
1268
1269 const DISPLAY_NAME = 0b0010_0000;
1271
1272 const NONE = 0b1000_0000;
1283 }
1284}
1285
1286impl Default for RoomInfoNotableUpdateReasons {
1287 fn default() -> Self {
1288 Self::empty()
1289 }
1290}
1291
1292#[cfg(test)]
1293mod tests {
1294 use std::sync::Arc;
1295
1296 use assert_matches::assert_matches;
1297 use matrix_sdk_common::deserialized_responses::TimelineEvent;
1298 use matrix_sdk_test::{
1299 async_test,
1300 test_json::{TAG, sync_events::PINNED_EVENTS},
1301 };
1302 use ruma::{
1303 assign, events::room::pinned_events::RoomPinnedEventsEventContent, owned_event_id,
1304 owned_mxc_uri, owned_user_id, room_id, serde::Raw,
1305 };
1306 use serde_json::json;
1307 use similar_asserts::assert_eq;
1308
1309 use super::{BaseRoomInfo, LatestEventValue, RoomInfo, SyncInfo};
1310 use crate::{
1311 RoomDisplayName, RoomHero, RoomState, StateChanges,
1312 latest_event::LatestEvent,
1313 notification_settings::RoomNotificationMode,
1314 room::{RoomNotableTags, RoomSummary},
1315 store::{IntoStateStore, MemoryStore},
1316 sync::UnreadNotificationsCount,
1317 };
1318
1319 #[test]
1320 fn test_room_info_serialization() {
1321 let info = RoomInfo {
1325 data_format_version: 1,
1326 room_id: room_id!("!gda78o:server.tld").into(),
1327 room_state: RoomState::Invited,
1328 notification_counts: UnreadNotificationsCount {
1329 highlight_count: 1,
1330 notification_count: 2,
1331 },
1332 summary: RoomSummary {
1333 room_heroes: vec![RoomHero {
1334 user_id: owned_user_id!("@somebody:example.org"),
1335 display_name: None,
1336 avatar_url: None,
1337 }],
1338 joined_member_count: 5,
1339 invited_member_count: 0,
1340 },
1341 members_synced: true,
1342 last_prev_batch: Some("pb".to_owned()),
1343 sync_info: SyncInfo::FullySynced,
1344 encryption_state_synced: true,
1345 latest_event: Some(Box::new(LatestEvent::new(TimelineEvent::from_plaintext(
1346 Raw::from_json_string(json!({"sender": "@u:i.uk"}).to_string()).unwrap(),
1347 )))),
1348 new_latest_event: LatestEventValue::None,
1349 base_info: Box::new(
1350 assign!(BaseRoomInfo::new(), { pinned_events: Some(RoomPinnedEventsEventContent::new(vec![owned_event_id!("$a")])) }),
1351 ),
1352 read_receipts: Default::default(),
1353 warned_about_unknown_room_version_rules: Arc::new(false.into()),
1354 cached_display_name: None,
1355 cached_user_defined_notification_mode: None,
1356 recency_stamp: Some(42.into()),
1357 invite_acceptance_details: None,
1358 };
1359
1360 let info_json = json!({
1361 "data_format_version": 1,
1362 "room_id": "!gda78o:server.tld",
1363 "room_state": "Invited",
1364 "notification_counts": {
1365 "highlight_count": 1,
1366 "notification_count": 2,
1367 },
1368 "summary": {
1369 "room_heroes": [{
1370 "user_id": "@somebody:example.org",
1371 "display_name": null,
1372 "avatar_url": null
1373 }],
1374 "joined_member_count": 5,
1375 "invited_member_count": 0,
1376 },
1377 "members_synced": true,
1378 "last_prev_batch": "pb",
1379 "sync_info": "FullySynced",
1380 "encryption_state_synced": true,
1381 "latest_event": {
1382 "event": {
1383 "kind": {"PlainText": {"event": {"sender": "@u:i.uk"}}},
1384 "thread_summary": "None",
1385 "timestamp": null,
1386 },
1387 },
1388 "new_latest_event": "None",
1389 "base_info": {
1390 "avatar": null,
1391 "canonical_alias": null,
1392 "create": null,
1393 "dm_targets": [],
1394 "encryption": null,
1395 "guest_access": null,
1396 "history_visibility": null,
1397 "is_marked_unread": false,
1398 "is_marked_unread_source": "Unstable",
1399 "join_rules": null,
1400 "max_power_level": 100,
1401 "name": null,
1402 "tombstone": null,
1403 "topic": null,
1404 "pinned_events": {
1405 "pinned": ["$a"]
1406 },
1407 },
1408 "read_receipts": {
1409 "num_unread": 0,
1410 "num_mentions": 0,
1411 "num_notifications": 0,
1412 "latest_active": null,
1413 "pending": [],
1414 },
1415 "recency_stamp": 42,
1416 });
1417
1418 assert_eq!(serde_json::to_value(info).unwrap(), info_json);
1419 }
1420
1421 #[async_test]
1422 async fn test_room_info_migration_v1() {
1423 let store = MemoryStore::new().into_state_store();
1424
1425 let room_info_json = json!({
1426 "room_id": "!gda78o:server.tld",
1427 "room_state": "Joined",
1428 "notification_counts": {
1429 "highlight_count": 1,
1430 "notification_count": 2,
1431 },
1432 "summary": {
1433 "room_heroes": [{
1434 "user_id": "@somebody:example.org",
1435 "display_name": null,
1436 "avatar_url": null
1437 }],
1438 "joined_member_count": 5,
1439 "invited_member_count": 0,
1440 },
1441 "members_synced": true,
1442 "last_prev_batch": "pb",
1443 "sync_info": "FullySynced",
1444 "encryption_state_synced": true,
1445 "latest_event": {
1446 "event": {
1447 "encryption_info": null,
1448 "event": {
1449 "sender": "@u:i.uk",
1450 },
1451 },
1452 },
1453 "base_info": {
1454 "avatar": null,
1455 "canonical_alias": null,
1456 "create": null,
1457 "dm_targets": [],
1458 "encryption": null,
1459 "guest_access": null,
1460 "history_visibility": null,
1461 "join_rules": null,
1462 "max_power_level": 100,
1463 "name": null,
1464 "tombstone": null,
1465 "topic": null,
1466 },
1467 "read_receipts": {
1468 "num_unread": 0,
1469 "num_mentions": 0,
1470 "num_notifications": 0,
1471 "latest_active": null,
1472 "pending": []
1473 },
1474 "recency_stamp": 42,
1475 });
1476 let mut room_info: RoomInfo = serde_json::from_value(room_info_json).unwrap();
1477
1478 assert_eq!(room_info.data_format_version, 0);
1479 assert!(room_info.base_info.notable_tags.is_empty());
1480 assert!(room_info.base_info.pinned_events.is_none());
1481
1482 assert!(room_info.apply_migrations(store.clone()).await);
1484
1485 assert_eq!(room_info.data_format_version, 1);
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 let mut changes = StateChanges::default();
1498
1499 let raw_tag_event = Raw::new(&*TAG).unwrap().cast_unchecked();
1500 let tag_event = raw_tag_event.deserialize().unwrap();
1501 changes.add_room_account_data(&room_info.room_id, tag_event, raw_tag_event);
1502
1503 let raw_pinned_events_event = Raw::new(&*PINNED_EVENTS).unwrap().cast_unchecked();
1504 let pinned_events_event = raw_pinned_events_event.deserialize().unwrap();
1505 changes.add_state_event(&room_info.room_id, pinned_events_event, raw_pinned_events_event);
1506
1507 store.save_changes(&changes).await.unwrap();
1508
1509 room_info.data_format_version = 0;
1511 assert!(room_info.apply_migrations(store.clone()).await);
1512
1513 assert_eq!(room_info.data_format_version, 1);
1514 assert!(room_info.base_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
1515 assert!(room_info.base_info.pinned_events.is_some());
1516
1517 let new_room_info = RoomInfo::new(room_id!("!new_room:localhost"), RoomState::Joined);
1519 assert_eq!(new_room_info.data_format_version, 1);
1520 }
1521
1522 #[test]
1523 fn test_room_info_deserialization() {
1524 let info_json = json!({
1525 "room_id": "!gda78o:server.tld",
1526 "room_state": "Joined",
1527 "notification_counts": {
1528 "highlight_count": 1,
1529 "notification_count": 2,
1530 },
1531 "summary": {
1532 "room_heroes": [{
1533 "user_id": "@somebody:example.org",
1534 "display_name": "Somebody",
1535 "avatar_url": "mxc://example.org/abc"
1536 }],
1537 "joined_member_count": 5,
1538 "invited_member_count": 0,
1539 },
1540 "members_synced": true,
1541 "last_prev_batch": "pb",
1542 "sync_info": "FullySynced",
1543 "encryption_state_synced": true,
1544 "base_info": {
1545 "avatar": null,
1546 "canonical_alias": null,
1547 "create": null,
1548 "dm_targets": [],
1549 "encryption": null,
1550 "guest_access": null,
1551 "history_visibility": null,
1552 "join_rules": null,
1553 "max_power_level": 100,
1554 "name": null,
1555 "tombstone": null,
1556 "topic": null,
1557 },
1558 "cached_display_name": { "Calculated": "lol" },
1559 "cached_user_defined_notification_mode": "Mute",
1560 "recency_stamp": 42,
1561 });
1562
1563 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1564
1565 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1566 assert_eq!(info.room_state, RoomState::Joined);
1567 assert_eq!(info.notification_counts.highlight_count, 1);
1568 assert_eq!(info.notification_counts.notification_count, 2);
1569 assert_eq!(
1570 info.summary.room_heroes,
1571 vec![RoomHero {
1572 user_id: owned_user_id!("@somebody:example.org"),
1573 display_name: Some("Somebody".to_owned()),
1574 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1575 }]
1576 );
1577 assert_eq!(info.summary.joined_member_count, 5);
1578 assert_eq!(info.summary.invited_member_count, 0);
1579 assert!(info.members_synced);
1580 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1581 assert_eq!(info.sync_info, SyncInfo::FullySynced);
1582 assert!(info.encryption_state_synced);
1583 assert!(info.latest_event.is_none());
1584 assert_matches!(info.new_latest_event, LatestEventValue::None);
1585 assert!(info.base_info.avatar.is_none());
1586 assert!(info.base_info.canonical_alias.is_none());
1587 assert!(info.base_info.create.is_none());
1588 assert_eq!(info.base_info.dm_targets.len(), 0);
1589 assert!(info.base_info.encryption.is_none());
1590 assert!(info.base_info.guest_access.is_none());
1591 assert!(info.base_info.history_visibility.is_none());
1592 assert!(info.base_info.join_rules.is_none());
1593 assert_eq!(info.base_info.max_power_level, 100);
1594 assert!(info.base_info.name.is_none());
1595 assert!(info.base_info.tombstone.is_none());
1596 assert!(info.base_info.topic.is_none());
1597
1598 assert_eq!(
1599 info.cached_display_name.as_ref(),
1600 Some(&RoomDisplayName::Calculated("lol".to_owned())),
1601 );
1602 assert_eq!(
1603 info.cached_user_defined_notification_mode.as_ref(),
1604 Some(&RoomNotificationMode::Mute)
1605 );
1606 assert_eq!(info.recency_stamp.as_ref(), Some(&42.into()));
1607 }
1608
1609 #[test]
1616 fn test_room_info_deserialization_without_optional_items() {
1617 let info_json = json!({
1620 "room_id": "!gda78o:server.tld",
1621 "room_state": "Invited",
1622 "notification_counts": {
1623 "highlight_count": 1,
1624 "notification_count": 2,
1625 },
1626 "summary": {
1627 "room_heroes": [{
1628 "user_id": "@somebody:example.org",
1629 "display_name": "Somebody",
1630 "avatar_url": "mxc://example.org/abc"
1631 }],
1632 "joined_member_count": 5,
1633 "invited_member_count": 0,
1634 },
1635 "members_synced": true,
1636 "last_prev_batch": "pb",
1637 "sync_info": "FullySynced",
1638 "encryption_state_synced": true,
1639 "base_info": {
1640 "avatar": null,
1641 "canonical_alias": null,
1642 "create": null,
1643 "dm_targets": [],
1644 "encryption": null,
1645 "guest_access": null,
1646 "history_visibility": null,
1647 "join_rules": null,
1648 "max_power_level": 100,
1649 "name": null,
1650 "tombstone": null,
1651 "topic": null,
1652 },
1653 });
1654
1655 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1656
1657 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1658 assert_eq!(info.room_state, RoomState::Invited);
1659 assert_eq!(info.notification_counts.highlight_count, 1);
1660 assert_eq!(info.notification_counts.notification_count, 2);
1661 assert_eq!(
1662 info.summary.room_heroes,
1663 vec![RoomHero {
1664 user_id: owned_user_id!("@somebody:example.org"),
1665 display_name: Some("Somebody".to_owned()),
1666 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1667 }]
1668 );
1669 assert_eq!(info.summary.joined_member_count, 5);
1670 assert_eq!(info.summary.invited_member_count, 0);
1671 assert!(info.members_synced);
1672 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1673 assert_eq!(info.sync_info, SyncInfo::FullySynced);
1674 assert!(info.encryption_state_synced);
1675 assert!(info.base_info.avatar.is_none());
1676 assert!(info.base_info.canonical_alias.is_none());
1677 assert!(info.base_info.create.is_none());
1678 assert_eq!(info.base_info.dm_targets.len(), 0);
1679 assert!(info.base_info.encryption.is_none());
1680 assert!(info.base_info.guest_access.is_none());
1681 assert!(info.base_info.history_visibility.is_none());
1682 assert!(info.base_info.join_rules.is_none());
1683 assert_eq!(info.base_info.max_power_level, 100);
1684 assert!(info.base_info.name.is_none());
1685 assert!(info.base_info.tombstone.is_none());
1686 assert!(info.base_info.topic.is_none());
1687 }
1688}