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 tracing::{field::debug, info, instrument, warn};
62
63use super::{
64 AccountDataSource, EncryptionState, Room, RoomCreateWithCreatorEventContent, RoomDisplayName,
65 RoomHero, RoomNotableTags, RoomState, RoomSummary,
66};
67use crate::{
68 MinimalStateEvent,
69 deserialized_responses::RawSyncOrStrippedState,
70 latest_event::LatestEventValue,
71 notification_settings::RoomNotificationMode,
72 read_receipts::RoomReadReceipts,
73 room::call::CallIntentConsensus,
74 store::{DynStateStore, StateStoreExt},
75 sync::UnreadNotificationsCount,
76 utils::{AnyStateEventEnum, RawStateEventWithKeys},
77};
78
79const DEFAULT_MAX_POWER_LEVEL: i64 = 100;
81
82impl Room {
83 pub fn subscribe_info(&self) -> Subscriber<RoomInfo> {
85 self.info.subscribe()
86 }
87
88 pub fn clone_info(&self) -> RoomInfo {
90 self.info.get()
91 }
92
93 pub fn set_room_info(
95 &self,
96 room_info: RoomInfo,
97 room_info_notable_update_reasons: RoomInfoNotableUpdateReasons,
98 ) {
99 self.info.set(room_info);
100
101 if !room_info_notable_update_reasons.is_empty() {
102 let _ = self.room_info_notable_update_sender.send(RoomInfoNotableUpdate {
104 room_id: self.room_id.clone(),
105 reasons: room_info_notable_update_reasons,
106 });
107 } else {
108 let _ = self.room_info_notable_update_sender.send(RoomInfoNotableUpdate {
112 room_id: self.room_id.clone(),
113 reasons: RoomInfoNotableUpdateReasons::NONE,
114 });
115 }
116 }
117}
118
119#[derive(Clone, Debug, Serialize, Deserialize)]
123pub struct BaseRoomInfo {
124 pub(crate) avatar: Option<MinimalStateEvent<PossiblyRedactedRoomAvatarEventContent>>,
126 pub(crate) canonical_alias:
128 Option<MinimalStateEvent<PossiblyRedactedRoomCanonicalAliasEventContent>>,
129 pub(crate) create: Option<MinimalStateEvent<RoomCreateWithCreatorEventContent>>,
131 pub(crate) dm_targets: HashSet<OwnedDirectUserIdentifier>,
134 pub(crate) encryption: Option<PossiblyRedactedRoomEncryptionEventContent>,
136 pub(crate) guest_access: Option<MinimalStateEvent<PossiblyRedactedRoomGuestAccessEventContent>>,
138 pub(crate) history_visibility:
140 Option<MinimalStateEvent<PossiblyRedactedRoomHistoryVisibilityEventContent>>,
141 pub(crate) join_rules: Option<MinimalStateEvent<PossiblyRedactedRoomJoinRulesEventContent>>,
143 pub(crate) max_power_level: i64,
145 pub(crate) member_hints: Option<MinimalStateEvent<PossiblyRedactedMemberHintsEventContent>>,
148 pub(crate) name: Option<MinimalStateEvent<PossiblyRedactedRoomNameEventContent>>,
150 pub(crate) tombstone: Option<MinimalStateEvent<PossiblyRedactedRoomTombstoneEventContent>>,
152 pub(crate) topic: Option<MinimalStateEvent<PossiblyRedactedRoomTopicEventContent>>,
154 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
157 pub(crate) rtc_member_events:
158 BTreeMap<CallMemberStateKey, MinimalStateEvent<PossiblyRedactedCallMemberEventContent>>,
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<PossiblyRedactedRoomPinnedEventsEventContent>,
173}
174
175impl BaseRoomInfo {
176 pub fn new() -> Self {
178 Self::default()
179 }
180
181 pub fn room_version(&self) -> Option<&RoomVersionId> {
186 Some(&self.create.as_ref()?.content.room_version)
187 }
188
189 pub fn handle_state_event<T: AnyStateEventEnum>(
193 &mut self,
194 raw_event: &mut RawStateEventWithKeys<T>,
195 ) -> bool {
196 match (&raw_event.event_type, raw_event.state_key.as_str()) {
197 (StateEventType::RoomEncryption, "") => {
198 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
202 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomEncryption)
203 }) && event.content.algorithm.is_some()
204 {
205 self.encryption = Some(event.content);
206 true
207 } else {
208 false
209 }
210 }
211 (StateEventType::RoomAvatar, "") => {
212 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
213 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomAvatar)
214 }) {
215 self.avatar = Some(event);
216 true
217 } else {
218 self.avatar.take().is_some()
220 }
221 }
222 (StateEventType::RoomName, "") => {
223 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
224 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomName)
225 }) {
226 self.name = Some(event);
227 true
228 } else {
229 self.name.take().is_some()
231 }
232 }
233 (StateEventType::RoomCreate, "") if self.create.is_none() => {
235 if let Some(any_event) = raw_event.deserialize()
236 && let Some(content) = as_variant!(
237 any_event.get_content(),
238 AnyPossiblyRedactedStateEventContent::RoomCreate
239 )
240 {
241 self.create = Some(MinimalStateEvent {
242 content: RoomCreateWithCreatorEventContent::from_event_content(
243 content,
244 any_event.get_sender().to_owned(),
245 ),
246 event_id: any_event.get_event_id().map(ToOwned::to_owned),
247 });
248 true
249 } else {
250 false
251 }
252 }
253 (StateEventType::RoomHistoryVisibility, "") => {
254 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
255 as_variant!(
256 any_event,
257 AnyPossiblyRedactedStateEventContent::RoomHistoryVisibility
258 )
259 }) {
260 self.history_visibility = Some(event);
261 true
262 } else {
263 self.history_visibility.take().is_some()
265 }
266 }
267 (StateEventType::RoomGuestAccess, "") => {
268 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
269 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomGuestAccess)
270 }) {
271 self.guest_access = Some(event);
272 true
273 } else {
274 self.guest_access.take().is_some()
276 }
277 }
278 (StateEventType::MemberHints, "") => {
279 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
280 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::MemberHints)
281 }) {
282 self.member_hints = Some(event);
283 true
284 } else {
285 self.member_hints.take().is_some()
287 }
288 }
289 (StateEventType::RoomJoinRules, "") => {
290 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
291 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomJoinRules)
292 }) {
293 match &event.content.join_rule {
294 JoinRule::Invite
295 | JoinRule::Knock
296 | JoinRule::Private
297 | JoinRule::Restricted(_)
298 | JoinRule::KnockRestricted(_)
299 | JoinRule::Public => {
300 self.join_rules = Some(event);
301 true
302 }
303 r => {
304 warn!(join_rule = ?r.as_str(), "Encountered a custom join rule, skipping");
305 self.join_rules.take().is_some()
307 }
308 }
309 } else {
310 self.join_rules.take().is_some()
312 }
313 }
314 (StateEventType::RoomCanonicalAlias, "") => {
315 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
316 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomCanonicalAlias)
317 }) {
318 self.canonical_alias = Some(event);
319 true
320 } else {
321 self.canonical_alias.take().is_some()
323 }
324 }
325 (StateEventType::RoomTopic, "") => {
326 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
327 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomTopic)
328 }) {
329 self.topic = Some(event);
330 true
331 } else {
332 self.topic.take().is_some()
334 }
335 }
336 (StateEventType::RoomTombstone, "") => {
337 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
338 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomTombstone)
339 }) {
340 self.tombstone = Some(event);
341 true
342 } else {
343 self.tombstone.take().is_some()
345 }
346 }
347 (StateEventType::RoomPowerLevels, "") => {
348 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
349 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomPowerLevels)
350 }) {
351 let new_max = i64::from(
352 event
353 .content
354 .users
355 .values()
356 .fold(event.content.users_default, |max_pl, user_pl| {
357 max_pl.max(*user_pl)
358 }),
359 );
360
361 if self.max_power_level != new_max {
362 self.max_power_level = new_max;
363 true
364 } else {
365 false
366 }
367 } else if self.max_power_level != DEFAULT_MAX_POWER_LEVEL {
368 self.max_power_level = DEFAULT_MAX_POWER_LEVEL;
370 true
371 } else {
372 false
373 }
374 }
375 (StateEventType::CallMember, _) => {
376 if let Ok(call_member_key) = raw_event.state_key.parse::<CallMemberStateKey>() {
377 if let Some(any_event) = raw_event.deserialize()
378 && let Some(content) = as_variant!(
379 any_event.get_content(),
380 AnyPossiblyRedactedStateEventContent::CallMember
381 )
382 {
383 let mut event = MinimalStateEvent {
384 content,
385 event_id: any_event.get_event_id().map(ToOwned::to_owned),
386 };
387
388 if let Some(origin_server_ts) = any_event.get_origin_server_ts() {
389 event.content.set_created_ts_if_none(origin_server_ts);
390 }
391
392 self.rtc_member_events.insert(call_member_key, event);
394
395 self.rtc_member_events
397 .retain(|_, ev| !ev.content.active_memberships(None).is_empty());
398
399 true
400 } else {
401 self.rtc_member_events.remove(&call_member_key).is_some()
404 }
405 } else {
406 false
407 }
408 }
409 (StateEventType::RoomPinnedEvents, "") => {
410 if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
411 as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomPinnedEvents)
412 }) {
413 self.pinned_events = Some(event.content);
414 true
415 } else {
416 self.pinned_events.take().is_some()
418 }
419 }
420 _ => false,
421 }
422 }
423
424 pub(super) fn handle_redaction(&mut self, redacts: &EventId) {
425 let redaction_rules = self
426 .room_version()
427 .and_then(|room_version| room_version.rules())
428 .unwrap_or(ROOM_VERSION_RULES_FALLBACK)
429 .redaction;
430
431 if let Some(ev) = &mut self.avatar
432 && ev.event_id.as_deref() == Some(redacts)
433 {
434 ev.redact(&redaction_rules);
435 } else if let Some(ev) = &mut self.canonical_alias
436 && ev.event_id.as_deref() == Some(redacts)
437 {
438 ev.redact(&redaction_rules);
439 } else if let Some(ev) = &mut self.create
440 && ev.event_id.as_deref() == Some(redacts)
441 {
442 ev.redact(&redaction_rules);
443 } else if let Some(ev) = &mut self.guest_access
444 && ev.event_id.as_deref() == Some(redacts)
445 {
446 ev.redact(&redaction_rules);
447 } else if let Some(ev) = &mut self.history_visibility
448 && ev.event_id.as_deref() == Some(redacts)
449 {
450 ev.redact(&redaction_rules);
451 } else if let Some(ev) = &mut self.join_rules
452 && ev.event_id.as_deref() == Some(redacts)
453 {
454 ev.redact(&redaction_rules);
455 } else if let Some(ev) = &mut self.name
456 && ev.event_id.as_deref() == Some(redacts)
457 {
458 ev.redact(&redaction_rules);
459 } else if let Some(ev) = &mut self.tombstone
460 && ev.event_id.as_deref() == Some(redacts)
461 {
462 ev.redact(&redaction_rules);
463 } else if let Some(ev) = &mut self.topic
464 && ev.event_id.as_deref() == Some(redacts)
465 {
466 ev.redact(&redaction_rules);
467 } else {
468 self.rtc_member_events
469 .retain(|_, member_event| member_event.event_id.as_deref() != Some(redacts));
470 }
471 }
472
473 pub fn handle_notable_tags(&mut self, tags: &Tags) {
474 let mut notable_tags = RoomNotableTags::empty();
475
476 if tags.contains_key(&TagName::Favorite) {
477 notable_tags.insert(RoomNotableTags::FAVOURITE);
478 }
479
480 if tags.contains_key(&TagName::LowPriority) {
481 notable_tags.insert(RoomNotableTags::LOW_PRIORITY);
482 }
483
484 self.notable_tags = notable_tags;
485 }
486}
487
488impl Default for BaseRoomInfo {
489 fn default() -> Self {
490 Self {
491 avatar: None,
492 canonical_alias: None,
493 create: None,
494 dm_targets: Default::default(),
495 member_hints: None,
496 encryption: None,
497 guest_access: None,
498 history_visibility: None,
499 join_rules: None,
500 max_power_level: DEFAULT_MAX_POWER_LEVEL,
501 name: None,
502 tombstone: None,
503 topic: None,
504 rtc_member_events: BTreeMap::new(),
505 is_marked_unread: false,
506 is_marked_unread_source: AccountDataSource::Unstable,
507 notable_tags: RoomNotableTags::empty(),
508 pinned_events: None,
509 }
510 }
511}
512
513#[derive(Clone, Debug, Serialize, Deserialize)]
517pub struct RoomInfo {
518 #[serde(default, alias = "version")]
521 pub(crate) data_format_version: u8,
522
523 pub(crate) room_id: OwnedRoomId,
525
526 pub(crate) room_state: RoomState,
528
529 pub(crate) notification_counts: UnreadNotificationsCount,
534
535 pub(crate) summary: RoomSummary,
537
538 pub(crate) members_synced: bool,
540
541 pub(crate) last_prev_batch: Option<String>,
543
544 pub(crate) sync_info: SyncInfo,
546
547 pub(crate) encryption_state_synced: bool,
549
550 #[serde(default)]
552 pub(crate) latest_event_value: LatestEventValue,
553
554 #[serde(default)]
556 pub(crate) read_receipts: RoomReadReceipts,
557
558 pub(crate) base_info: Box<BaseRoomInfo>,
561
562 #[serde(skip)]
566 pub(crate) warned_about_unknown_room_version_rules: Arc<AtomicBool>,
567
568 #[serde(default, skip_serializing_if = "Option::is_none")]
573 pub(crate) cached_display_name: Option<RoomDisplayName>,
574
575 #[serde(default, skip_serializing_if = "Option::is_none")]
577 pub(crate) cached_user_defined_notification_mode: Option<RoomNotificationMode>,
578
579 #[serde(default)]
596 pub(crate) recency_stamp: Option<RoomRecencyStamp>,
597}
598
599impl RoomInfo {
600 #[doc(hidden)] pub fn new(room_id: &RoomId, room_state: RoomState) -> Self {
602 Self {
603 data_format_version: 1,
604 room_id: room_id.into(),
605 room_state,
606 notification_counts: Default::default(),
607 summary: Default::default(),
608 members_synced: false,
609 last_prev_batch: None,
610 sync_info: SyncInfo::NoState,
611 encryption_state_synced: false,
612 latest_event_value: LatestEventValue::default(),
613 read_receipts: Default::default(),
614 base_info: Box::new(BaseRoomInfo::new()),
615 warned_about_unknown_room_version_rules: Arc::new(false.into()),
616 cached_display_name: None,
617 cached_user_defined_notification_mode: None,
618 recency_stamp: None,
619 }
620 }
621
622 pub fn mark_as_joined(&mut self) {
624 self.set_state(RoomState::Joined);
625 }
626
627 pub fn mark_as_left(&mut self) {
629 self.set_state(RoomState::Left);
630 }
631
632 pub fn mark_as_invited(&mut self) {
634 self.set_state(RoomState::Invited);
635 }
636
637 pub fn mark_as_knocked(&mut self) {
639 self.set_state(RoomState::Knocked);
640 }
641
642 pub fn mark_as_banned(&mut self) {
644 self.set_state(RoomState::Banned);
645 }
646
647 pub fn set_state(&mut self, room_state: RoomState) {
649 self.room_state = room_state;
650 }
651
652 pub fn mark_members_synced(&mut self) {
654 self.members_synced = true;
655 }
656
657 pub fn mark_members_missing(&mut self) {
659 self.members_synced = false;
660 }
661
662 pub fn are_members_synced(&self) -> bool {
664 self.members_synced
665 }
666
667 pub fn mark_state_partially_synced(&mut self) {
669 self.sync_info = SyncInfo::PartiallySynced;
670 }
671
672 pub fn mark_state_fully_synced(&mut self) {
674 self.sync_info = SyncInfo::FullySynced;
675 }
676
677 pub fn mark_state_not_synced(&mut self) {
679 self.sync_info = SyncInfo::NoState;
680 }
681
682 pub fn mark_encryption_state_synced(&mut self) {
684 self.encryption_state_synced = true;
685 }
686
687 pub fn mark_encryption_state_missing(&mut self) {
689 self.encryption_state_synced = false;
690 }
691
692 pub fn set_prev_batch(&mut self, prev_batch: Option<&str>) -> bool {
696 if self.last_prev_batch.as_deref() != prev_batch {
697 self.last_prev_batch = prev_batch.map(|p| p.to_owned());
698 true
699 } else {
700 false
701 }
702 }
703
704 pub fn state(&self) -> RoomState {
706 self.room_state
707 }
708
709 #[cfg(not(feature = "experimental-encrypted-state-events"))]
711 pub fn encryption_state(&self) -> EncryptionState {
712 if !self.encryption_state_synced {
713 EncryptionState::Unknown
714 } else if self.base_info.encryption.is_some() {
715 EncryptionState::Encrypted
716 } else {
717 EncryptionState::NotEncrypted
718 }
719 }
720
721 #[cfg(feature = "experimental-encrypted-state-events")]
723 pub fn encryption_state(&self) -> EncryptionState {
724 if !self.encryption_state_synced {
725 EncryptionState::Unknown
726 } else {
727 self.base_info
728 .encryption
729 .as_ref()
730 .map(|state| {
731 if state.encrypt_state_events {
732 EncryptionState::StateEncrypted
733 } else {
734 EncryptionState::Encrypted
735 }
736 })
737 .unwrap_or(EncryptionState::NotEncrypted)
738 }
739 }
740
741 pub fn set_encryption_event(
743 &mut self,
744 event: Option<PossiblyRedactedRoomEncryptionEventContent>,
745 ) {
746 self.base_info.encryption = event;
747 }
748
749 pub fn handle_encryption_state(
751 &mut self,
752 requested_required_states: &[(StateEventType, String)],
753 ) {
754 if requested_required_states
755 .iter()
756 .any(|(state_event, _)| state_event == &StateEventType::RoomEncryption)
757 {
758 self.mark_encryption_state_synced();
764 }
765 }
766
767 pub fn handle_state_event(
771 &mut self,
772 raw_event: &mut RawStateEventWithKeys<AnySyncStateEvent>,
773 ) -> bool {
774 let base_info_has_been_modified = self.base_info.handle_state_event(raw_event);
776
777 if raw_event.event_type == StateEventType::RoomEncryption && raw_event.state_key.is_empty()
778 {
779 self.mark_encryption_state_synced();
785 }
786
787 base_info_has_been_modified
788 }
789
790 pub fn handle_stripped_state_event(
794 &mut self,
795 raw_event: &mut RawStateEventWithKeys<AnyStrippedStateEvent>,
796 ) -> bool {
797 self.base_info.handle_state_event(raw_event)
798 }
799
800 #[instrument(skip_all, fields(redacts))]
802 pub fn handle_redaction(
803 &mut self,
804 event: &SyncRoomRedactionEvent,
805 _raw: &Raw<SyncRoomRedactionEvent>,
806 ) {
807 let redaction_rules = self.room_version_rules_or_default().redaction;
808
809 let Some(redacts) = event.redacts(&redaction_rules) else {
810 info!("Can't apply redaction, redacts field is missing");
811 return;
812 };
813 tracing::Span::current().record("redacts", debug(redacts));
814
815 self.base_info.handle_redaction(redacts);
816 }
817
818 pub fn avatar_url(&self) -> Option<&MxcUri> {
820 self.base_info.avatar.as_ref().and_then(|e| e.content.url.as_deref())
821 }
822
823 pub fn update_avatar(&mut self, url: Option<OwnedMxcUri>) {
825 self.base_info.avatar = url.map(|url| {
826 let mut content = PossiblyRedactedRoomAvatarEventContent::new();
827 content.url = Some(url);
828
829 MinimalStateEvent { content, event_id: None }
830 });
831 }
832
833 pub fn avatar_info(&self) -> Option<&avatar::ImageInfo> {
835 self.base_info.avatar.as_ref().and_then(|e| e.content.info.as_deref())
836 }
837
838 pub fn update_notification_count(&mut self, notification_counts: UnreadNotificationsCount) {
840 self.notification_counts = notification_counts;
841 }
842
843 pub fn update_from_ruma_summary(&mut self, summary: &RumaSummary) -> bool {
847 let mut changed = false;
848
849 if !summary.is_empty() {
850 if !summary.heroes.is_empty() {
851 self.summary.room_heroes = summary
852 .heroes
853 .iter()
854 .map(|hero_id| RoomHero {
855 user_id: hero_id.to_owned(),
856 display_name: None,
857 avatar_url: None,
858 })
859 .collect();
860
861 changed = true;
862 }
863
864 if let Some(joined) = summary.joined_member_count {
865 self.summary.joined_member_count = joined.into();
866 changed = true;
867 }
868
869 if let Some(invited) = summary.invited_member_count {
870 self.summary.invited_member_count = invited.into();
871 changed = true;
872 }
873 }
874
875 changed
876 }
877
878 pub(crate) fn update_joined_member_count(&mut self, count: u64) {
880 self.summary.joined_member_count = count;
881 }
882
883 pub(crate) fn update_invited_member_count(&mut self, count: u64) {
885 self.summary.invited_member_count = count;
886 }
887
888 pub(crate) fn update_heroes(&mut self, heroes: Vec<RoomHero>) {
890 self.summary.room_heroes = heroes;
891 }
892
893 pub fn heroes(&self) -> &[RoomHero] {
895 &self.summary.room_heroes
896 }
897
898 pub fn active_members_count(&self) -> u64 {
902 self.summary.joined_member_count.saturating_add(self.summary.invited_member_count)
903 }
904
905 pub fn invited_members_count(&self) -> u64 {
907 self.summary.invited_member_count
908 }
909
910 pub fn joined_members_count(&self) -> u64 {
912 self.summary.joined_member_count
913 }
914
915 pub fn canonical_alias(&self) -> Option<&RoomAliasId> {
917 self.base_info.canonical_alias.as_ref()?.content.alias.as_deref()
918 }
919
920 pub fn alt_aliases(&self) -> &[OwnedRoomAliasId] {
922 self.base_info
923 .canonical_alias
924 .as_ref()
925 .map(|ev| ev.content.alt_aliases.as_ref())
926 .unwrap_or_default()
927 }
928
929 pub fn room_id(&self) -> &RoomId {
931 &self.room_id
932 }
933
934 pub fn room_version(&self) -> Option<&RoomVersionId> {
936 self.base_info.room_version()
937 }
938
939 pub fn room_version_rules_or_default(&self) -> RoomVersionRules {
944 use std::sync::atomic::Ordering;
945
946 self.base_info.room_version().and_then(|room_version| room_version.rules()).unwrap_or_else(
947 || {
948 if self
949 .warned_about_unknown_room_version_rules
950 .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
951 .is_ok()
952 {
953 warn!("Unable to get the room version rules, defaulting to rules for room version {ROOM_VERSION_FALLBACK}");
954 }
955
956 ROOM_VERSION_RULES_FALLBACK
957 },
958 )
959 }
960
961 pub fn room_type(&self) -> Option<&RoomType> {
963 self.base_info.create.as_ref()?.content.room_type.as_ref()
964 }
965
966 pub fn creators(&self) -> Option<Vec<OwnedUserId>> {
968 Some(self.base_info.create.as_ref()?.content.creators())
969 }
970
971 pub(super) fn guest_access(&self) -> &GuestAccess {
972 self.base_info
973 .guest_access
974 .as_ref()
975 .and_then(|event| event.content.guest_access.as_ref())
976 .unwrap_or(&GuestAccess::Forbidden)
977 }
978
979 pub fn history_visibility(&self) -> Option<&HistoryVisibility> {
983 Some(&self.base_info.history_visibility.as_ref()?.content.history_visibility)
984 }
985
986 pub fn history_visibility_or_default(&self) -> &HistoryVisibility {
993 self.history_visibility().unwrap_or(&HistoryVisibility::Shared)
994 }
995
996 pub fn join_rule(&self) -> Option<&JoinRule> {
999 Some(&self.base_info.join_rules.as_ref()?.content.join_rule)
1000 }
1001
1002 pub fn service_members(&self) -> Option<&BTreeSet<OwnedUserId>> {
1005 self.base_info.member_hints.as_ref()?.content.service_members.as_ref()
1006 }
1007
1008 pub fn name(&self) -> Option<&str> {
1010 self.base_info.name.as_ref()?.content.name.as_deref().filter(|name| !name.is_empty())
1011 }
1012
1013 pub fn create(&self) -> Option<&RoomCreateWithCreatorEventContent> {
1015 Some(&self.base_info.create.as_ref()?.content)
1016 }
1017
1018 pub fn tombstone(&self) -> Option<&PossiblyRedactedRoomTombstoneEventContent> {
1020 Some(&self.base_info.tombstone.as_ref()?.content)
1021 }
1022
1023 pub fn topic(&self) -> Option<&str> {
1025 self.base_info.topic.as_ref()?.content.topic.as_deref()
1026 }
1027
1028 fn active_matrix_rtc_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
1033 let mut v = self
1034 .base_info
1035 .rtc_member_events
1036 .iter()
1037 .flat_map(|(state_key, ev)| {
1038 ev.content.active_memberships(None).into_iter().map(move |m| (state_key.clone(), m))
1039 })
1040 .collect::<Vec<_>>();
1041 v.sort_by_key(|(_, m)| m.created_ts());
1042 v
1043 }
1044
1045 fn active_room_call_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
1051 self.active_matrix_rtc_memberships()
1052 .into_iter()
1053 .filter(|(_user_id, m)| m.is_room_call())
1054 .collect()
1055 }
1056
1057 pub fn has_active_room_call(&self) -> bool {
1060 !self.active_room_call_memberships().is_empty()
1061 }
1062
1063 pub fn active_room_call_consensus_intent(&self) -> CallIntentConsensus {
1080 let memberships = self.active_room_call_memberships();
1081 let total_count: u64 = memberships.len() as u64;
1082
1083 if total_count == 0 {
1084 return CallIntentConsensus::None;
1085 }
1086
1087 let mut consensus_intent: Option<CallIntent> = None;
1089 let mut agreeing_count: u64 = 0;
1090
1091 for (_, data) in memberships.iter() {
1092 if let Some(intent) = data.call_intent() {
1093 match &consensus_intent {
1094 None => {
1096 consensus_intent = Some(intent.clone());
1097 agreeing_count = 1;
1098 }
1099 Some(current) if current == intent => {
1101 agreeing_count += 1;
1102 }
1103 Some(_) => return CallIntentConsensus::None,
1105 }
1106 }
1107 }
1108
1109 match consensus_intent {
1111 None => CallIntentConsensus::None,
1112 Some(intent) if agreeing_count == total_count => {
1113 CallIntentConsensus::Full(intent)
1115 }
1116 Some(intent) => {
1117 CallIntentConsensus::Partial { intent, agreeing_count, total_count }
1119 }
1120 }
1121 }
1122
1123 pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
1132 self.active_room_call_memberships()
1133 .iter()
1134 .map(|(call_member_state_key, _)| call_member_state_key.user_id().to_owned())
1135 .collect()
1136 }
1137
1138 pub fn set_latest_event(&mut self, new_value: LatestEventValue) {
1140 self.latest_event_value = new_value;
1141 }
1142
1143 pub fn update_recency_stamp(&mut self, stamp: RoomRecencyStamp) {
1147 self.recency_stamp = Some(stamp);
1148 }
1149
1150 pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
1152 self.base_info.pinned_events.clone().and_then(|c| c.pinned)
1153 }
1154
1155 pub fn is_pinned_event(&self, event_id: &EventId) -> bool {
1161 self.base_info
1162 .pinned_events
1163 .as_ref()
1164 .and_then(|content| content.pinned.as_deref())
1165 .is_some_and(|pinned| pinned.contains(&event_id.to_owned()))
1166 }
1167
1168 pub fn read_receipts(&self) -> &RoomReadReceipts {
1170 &self.read_receipts
1171 }
1172
1173 pub fn set_read_receipts(&mut self, read_receipts: RoomReadReceipts) {
1175 self.read_receipts = read_receipts;
1176 }
1177
1178 #[instrument(skip_all, fields(room_id = ?self.room_id))]
1186 pub(crate) async fn apply_migrations(&mut self, store: Arc<DynStateStore>) -> bool {
1187 let mut migrated = false;
1188
1189 if self.data_format_version < 1 {
1190 info!("Migrating room info to version 1");
1191
1192 match store.get_room_account_data_event_static::<TagEventContent>(&self.room_id).await {
1194 Ok(Some(raw_event)) => match raw_event.deserialize() {
1196 Ok(event) => {
1197 self.base_info.handle_notable_tags(&event.content.tags);
1198 }
1199 Err(error) => {
1200 warn!("Failed to deserialize room tags: {error}");
1201 }
1202 },
1203 Ok(_) => {
1204 }
1206 Err(error) => {
1207 warn!("Failed to load room tags: {error}");
1208 }
1209 }
1210
1211 match store.get_state_event_static::<RoomPinnedEventsEventContent>(&self.room_id).await
1213 {
1214 Ok(Some(RawSyncOrStrippedState::Sync(raw_event))) => {
1216 if let Some(mut raw_event) =
1217 RawStateEventWithKeys::try_from_raw_state_event(raw_event.cast())
1218 {
1219 self.handle_state_event(&mut raw_event);
1220 }
1221 }
1222 Ok(_) => {
1223 }
1225 Err(error) => {
1226 warn!("Failed to load room pinned events: {error}");
1227 }
1228 }
1229
1230 self.data_format_version = 1;
1231 migrated = true;
1232 }
1233
1234 migrated
1235 }
1236}
1237
1238#[repr(transparent)]
1240#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
1241#[serde(transparent)]
1242pub struct RoomRecencyStamp(u64);
1243
1244impl From<u64> for RoomRecencyStamp {
1245 fn from(value: u64) -> Self {
1246 Self(value)
1247 }
1248}
1249
1250impl From<RoomRecencyStamp> for u64 {
1251 fn from(value: RoomRecencyStamp) -> Self {
1252 value.0
1253 }
1254}
1255
1256#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1257pub(crate) enum SyncInfo {
1258 NoState,
1264
1265 PartiallySynced,
1268
1269 FullySynced,
1271}
1272
1273pub fn apply_redaction(
1276 event: &Raw<AnySyncTimelineEvent>,
1277 raw_redaction: &Raw<SyncRoomRedactionEvent>,
1278 rules: &RedactionRules,
1279) -> Option<Raw<AnySyncTimelineEvent>> {
1280 use ruma::canonical_json::{RedactedBecause, redact_in_place};
1281
1282 let mut event_json = match event.deserialize_as() {
1283 Ok(json) => json,
1284 Err(e) => {
1285 warn!("Failed to deserialize latest event: {e}");
1286 return None;
1287 }
1288 };
1289
1290 let redacted_because = match RedactedBecause::from_raw_event(raw_redaction) {
1291 Ok(rb) => rb,
1292 Err(e) => {
1293 warn!("Redaction event is not valid canonical JSON: {e}");
1294 return None;
1295 }
1296 };
1297
1298 let redact_result = redact_in_place(&mut event_json, rules, Some(redacted_because));
1299
1300 if let Err(e) = redact_result {
1301 warn!("Failed to redact event: {e}");
1302 return None;
1303 }
1304
1305 let raw = Raw::new(&event_json).expect("CanonicalJsonObject must be serializable");
1306 Some(raw.cast_unchecked())
1307}
1308
1309#[derive(Debug, Clone)]
1319pub struct RoomInfoNotableUpdate {
1320 pub room_id: OwnedRoomId,
1322
1323 pub reasons: RoomInfoNotableUpdateReasons,
1325}
1326
1327bitflags! {
1328 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1330 pub struct RoomInfoNotableUpdateReasons: u8 {
1331 const RECENCY_STAMP = 0b0000_0001;
1333
1334 const LATEST_EVENT = 0b0000_0010;
1336
1337 const READ_RECEIPT = 0b0000_0100;
1339
1340 const UNREAD_MARKER = 0b0000_1000;
1342
1343 const MEMBERSHIP = 0b0001_0000;
1345
1346 const DISPLAY_NAME = 0b0010_0000;
1348
1349 const NONE = 0b1000_0000;
1360 }
1361}
1362
1363impl Default for RoomInfoNotableUpdateReasons {
1364 fn default() -> Self {
1365 Self::empty()
1366 }
1367}
1368
1369#[cfg(test)]
1370mod tests {
1371 use std::{str::FromStr, sync::Arc};
1372
1373 use assert_matches::assert_matches;
1374 use matrix_sdk_test::{async_test, event_factory::EventFactory};
1375 use ruma::{
1376 assign,
1377 events::{
1378 AnyRoomAccountDataEvent,
1379 room::pinned_events::RoomPinnedEventsEventContent,
1380 tag::{TagInfo, TagName, Tags, UserTagName},
1381 },
1382 owned_event_id, owned_mxc_uri, owned_user_id, room_id,
1383 serde::Raw,
1384 user_id,
1385 };
1386 use serde_json::json;
1387 use similar_asserts::assert_eq;
1388
1389 use super::{BaseRoomInfo, LatestEventValue, RoomInfo, SyncInfo};
1390 use crate::{
1391 RoomDisplayName, RoomHero, RoomState, StateChanges,
1392 notification_settings::RoomNotificationMode,
1393 room::{RoomNotableTags, RoomSummary},
1394 store::{IntoStateStore, MemoryStore},
1395 sync::UnreadNotificationsCount,
1396 };
1397
1398 #[test]
1399 fn test_room_info_serialization() {
1400 let info = RoomInfo {
1404 data_format_version: 1,
1405 room_id: room_id!("!gda78o:server.tld").into(),
1406 room_state: RoomState::Invited,
1407 notification_counts: UnreadNotificationsCount {
1408 highlight_count: 1,
1409 notification_count: 2,
1410 },
1411 summary: RoomSummary {
1412 room_heroes: vec![RoomHero {
1413 user_id: owned_user_id!("@somebody:example.org"),
1414 display_name: None,
1415 avatar_url: None,
1416 }],
1417 joined_member_count: 5,
1418 invited_member_count: 0,
1419 },
1420 members_synced: true,
1421 last_prev_batch: Some("pb".to_owned()),
1422 sync_info: SyncInfo::FullySynced,
1423 encryption_state_synced: true,
1424 latest_event_value: LatestEventValue::None,
1425 base_info: Box::new(
1426 assign!(BaseRoomInfo::new(), { pinned_events: Some(RoomPinnedEventsEventContent::new(vec![owned_event_id!("$a")]).into()) }),
1427 ),
1428 read_receipts: Default::default(),
1429 warned_about_unknown_room_version_rules: Arc::new(false.into()),
1430 cached_display_name: None,
1431 cached_user_defined_notification_mode: None,
1432 recency_stamp: Some(42.into()),
1433 };
1434
1435 let info_json = json!({
1436 "data_format_version": 1,
1437 "room_id": "!gda78o:server.tld",
1438 "room_state": "Invited",
1439 "notification_counts": {
1440 "highlight_count": 1,
1441 "notification_count": 2,
1442 },
1443 "summary": {
1444 "room_heroes": [{
1445 "user_id": "@somebody:example.org",
1446 "display_name": null,
1447 "avatar_url": null
1448 }],
1449 "joined_member_count": 5,
1450 "invited_member_count": 0,
1451 },
1452 "members_synced": true,
1453 "last_prev_batch": "pb",
1454 "sync_info": "FullySynced",
1455 "encryption_state_synced": true,
1456 "latest_event_value": "None",
1457 "base_info": {
1458 "avatar": null,
1459 "canonical_alias": null,
1460 "create": null,
1461 "dm_targets": [],
1462 "encryption": null,
1463 "guest_access": null,
1464 "history_visibility": null,
1465 "is_marked_unread": false,
1466 "is_marked_unread_source": "Unstable",
1467 "join_rules": null,
1468 "max_power_level": 100,
1469 "member_hints": null,
1470 "name": null,
1471 "tombstone": null,
1472 "topic": null,
1473 "pinned_events": {
1474 "pinned": ["$a"]
1475 },
1476 },
1477 "read_receipts": {
1478 "num_unread": 0,
1479 "num_mentions": 0,
1480 "num_notifications": 0,
1481 "latest_active": null,
1482 "pending": [],
1483 },
1484 "recency_stamp": 42,
1485 });
1486
1487 assert_eq!(serde_json::to_value(info).unwrap(), info_json);
1488 }
1489
1490 #[async_test]
1491 async fn test_room_info_migration_v1() {
1492 let store = MemoryStore::new().into_state_store();
1493
1494 let room_info_json = json!({
1495 "room_id": "!gda78o:server.tld",
1496 "room_state": "Joined",
1497 "notification_counts": {
1498 "highlight_count": 1,
1499 "notification_count": 2,
1500 },
1501 "summary": {
1502 "room_heroes": [{
1503 "user_id": "@somebody:example.org",
1504 "display_name": null,
1505 "avatar_url": null
1506 }],
1507 "joined_member_count": 5,
1508 "invited_member_count": 0,
1509 },
1510 "members_synced": true,
1511 "last_prev_batch": "pb",
1512 "sync_info": "FullySynced",
1513 "encryption_state_synced": true,
1514 "latest_event": {
1515 "event": {
1516 "encryption_info": null,
1517 "event": {
1518 "sender": "@u:i.uk",
1519 },
1520 },
1521 },
1522 "base_info": {
1523 "avatar": null,
1524 "canonical_alias": null,
1525 "create": null,
1526 "dm_targets": [],
1527 "encryption": null,
1528 "guest_access": null,
1529 "history_visibility": null,
1530 "join_rules": null,
1531 "max_power_level": 100,
1532 "name": null,
1533 "tombstone": null,
1534 "topic": null,
1535 },
1536 "read_receipts": {
1537 "num_unread": 0,
1538 "num_mentions": 0,
1539 "num_notifications": 0,
1540 "latest_active": null,
1541 "pending": []
1542 },
1543 "recency_stamp": 42,
1544 });
1545 let mut room_info: RoomInfo = serde_json::from_value(room_info_json).unwrap();
1546
1547 assert_eq!(room_info.data_format_version, 0);
1548 assert!(room_info.base_info.notable_tags.is_empty());
1549 assert!(room_info.base_info.pinned_events.is_none());
1550
1551 assert!(room_info.apply_migrations(store.clone()).await);
1553
1554 assert_eq!(room_info.data_format_version, 1);
1555 assert!(room_info.base_info.notable_tags.is_empty());
1556 assert!(room_info.base_info.pinned_events.is_none());
1557
1558 assert!(!room_info.apply_migrations(store.clone()).await);
1560
1561 assert_eq!(room_info.data_format_version, 1);
1562 assert!(room_info.base_info.notable_tags.is_empty());
1563 assert!(room_info.base_info.pinned_events.is_none());
1564
1565 let mut changes = StateChanges::default();
1567
1568 let f = EventFactory::new().room(&room_info.room_id).sender(user_id!("@example:localhost"));
1569 let mut tags = Tags::new();
1570 tags.insert(TagName::Favorite, TagInfo::new());
1571 tags.insert(TagName::User(UserTagName::from_str("u.work").unwrap()), TagInfo::new());
1572 let raw_tag_event: Raw<AnyRoomAccountDataEvent> = f.tag(tags).into();
1573 let tag_event = raw_tag_event.deserialize().unwrap();
1574 changes.add_room_account_data(&room_info.room_id, tag_event, raw_tag_event);
1575
1576 let raw_pinned_events_event: Raw<_> = f
1577 .room_pinned_events(vec![owned_event_id!("$a"), owned_event_id!("$b")])
1578 .into_raw_sync_state();
1579 let pinned_events_event = raw_pinned_events_event.deserialize().unwrap();
1580 changes.add_state_event(&room_info.room_id, pinned_events_event, raw_pinned_events_event);
1581
1582 store.save_changes(&changes).await.unwrap();
1583
1584 room_info.data_format_version = 0;
1586 assert!(room_info.apply_migrations(store.clone()).await);
1587
1588 assert_eq!(room_info.data_format_version, 1);
1589 assert!(room_info.base_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
1590 assert!(room_info.base_info.pinned_events.is_some());
1591
1592 let new_room_info = RoomInfo::new(room_id!("!new_room:localhost"), RoomState::Joined);
1594 assert_eq!(new_room_info.data_format_version, 1);
1595 }
1596
1597 #[test]
1598 fn test_room_info_deserialization() {
1599 let info_json = json!({
1600 "room_id": "!gda78o:server.tld",
1601 "room_state": "Joined",
1602 "notification_counts": {
1603 "highlight_count": 1,
1604 "notification_count": 2,
1605 },
1606 "summary": {
1607 "room_heroes": [{
1608 "user_id": "@somebody:example.org",
1609 "display_name": "Somebody",
1610 "avatar_url": "mxc://example.org/abc"
1611 }],
1612 "joined_member_count": 5,
1613 "invited_member_count": 0,
1614 },
1615 "members_synced": true,
1616 "last_prev_batch": "pb",
1617 "sync_info": "FullySynced",
1618 "encryption_state_synced": true,
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 "member_hints": null,
1630 "name": null,
1631 "tombstone": null,
1632 "topic": null,
1633 },
1634 "cached_display_name": { "Calculated": "lol" },
1635 "cached_user_defined_notification_mode": "Mute",
1636 "recency_stamp": 42,
1637 });
1638
1639 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1640
1641 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1642 assert_eq!(info.room_state, RoomState::Joined);
1643 assert_eq!(info.notification_counts.highlight_count, 1);
1644 assert_eq!(info.notification_counts.notification_count, 2);
1645 assert_eq!(
1646 info.summary.room_heroes,
1647 vec![RoomHero {
1648 user_id: owned_user_id!("@somebody:example.org"),
1649 display_name: Some("Somebody".to_owned()),
1650 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1651 }]
1652 );
1653 assert_eq!(info.summary.joined_member_count, 5);
1654 assert_eq!(info.summary.invited_member_count, 0);
1655 assert!(info.members_synced);
1656 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1657 assert_eq!(info.sync_info, SyncInfo::FullySynced);
1658 assert!(info.encryption_state_synced);
1659 assert_matches!(info.latest_event_value, LatestEventValue::None);
1660 assert!(info.base_info.avatar.is_none());
1661 assert!(info.base_info.canonical_alias.is_none());
1662 assert!(info.base_info.create.is_none());
1663 assert_eq!(info.base_info.dm_targets.len(), 0);
1664 assert!(info.base_info.encryption.is_none());
1665 assert!(info.base_info.guest_access.is_none());
1666 assert!(info.base_info.history_visibility.is_none());
1667 assert!(info.base_info.join_rules.is_none());
1668 assert_eq!(info.base_info.max_power_level, 100);
1669 assert!(info.base_info.member_hints.is_none());
1670 assert!(info.base_info.name.is_none());
1671 assert!(info.base_info.tombstone.is_none());
1672 assert!(info.base_info.topic.is_none());
1673
1674 assert_eq!(
1675 info.cached_display_name.as_ref(),
1676 Some(&RoomDisplayName::Calculated("lol".to_owned())),
1677 );
1678 assert_eq!(
1679 info.cached_user_defined_notification_mode.as_ref(),
1680 Some(&RoomNotificationMode::Mute)
1681 );
1682 assert_eq!(info.recency_stamp.as_ref(), Some(&42.into()));
1683 }
1684
1685 #[test]
1692 fn test_room_info_deserialization_without_optional_items() {
1693 let info_json = json!({
1696 "room_id": "!gda78o:server.tld",
1697 "room_state": "Invited",
1698 "notification_counts": {
1699 "highlight_count": 1,
1700 "notification_count": 2,
1701 },
1702 "summary": {
1703 "room_heroes": [{
1704 "user_id": "@somebody:example.org",
1705 "display_name": "Somebody",
1706 "avatar_url": "mxc://example.org/abc"
1707 }],
1708 "joined_member_count": 5,
1709 "invited_member_count": 0,
1710 },
1711 "members_synced": true,
1712 "last_prev_batch": "pb",
1713 "sync_info": "FullySynced",
1714 "encryption_state_synced": true,
1715 "base_info": {
1716 "avatar": null,
1717 "canonical_alias": null,
1718 "create": null,
1719 "dm_targets": [],
1720 "encryption": null,
1721 "guest_access": null,
1722 "history_visibility": null,
1723 "join_rules": null,
1724 "max_power_level": 100,
1725 "name": null,
1726 "tombstone": null,
1727 "topic": null,
1728 },
1729 });
1730
1731 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1732
1733 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1734 assert_eq!(info.room_state, RoomState::Invited);
1735 assert_eq!(info.notification_counts.highlight_count, 1);
1736 assert_eq!(info.notification_counts.notification_count, 2);
1737 assert_eq!(
1738 info.summary.room_heroes,
1739 vec![RoomHero {
1740 user_id: owned_user_id!("@somebody:example.org"),
1741 display_name: Some("Somebody".to_owned()),
1742 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1743 }]
1744 );
1745 assert_eq!(info.summary.joined_member_count, 5);
1746 assert_eq!(info.summary.invited_member_count, 0);
1747 assert!(info.members_synced);
1748 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1749 assert_eq!(info.sync_info, SyncInfo::FullySynced);
1750 assert!(info.encryption_state_synced);
1751 assert!(info.base_info.avatar.is_none());
1752 assert!(info.base_info.canonical_alias.is_none());
1753 assert!(info.base_info.create.is_none());
1754 assert_eq!(info.base_info.dm_targets.len(), 0);
1755 assert!(info.base_info.encryption.is_none());
1756 assert!(info.base_info.guest_access.is_none());
1757 assert!(info.base_info.history_visibility.is_none());
1758 assert!(info.base_info.join_rules.is_none());
1759 assert_eq!(info.base_info.max_power_level, 100);
1760 assert!(info.base_info.name.is_none());
1761 assert!(info.base_info.tombstone.is_none());
1762 assert!(info.base_info.topic.is_none());
1763 }
1764}