1use std::{
16 collections::{BTreeMap, 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, MilliSecondsSinceUnixEpoch, MxcUri, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId,
26 OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, RoomVersionId,
27 api::client::sync::sync_events::v3::RoomSummary as RumaSummary,
28 assign,
29 events::{
30 AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, StateEventType,
31 SyncStateEvent,
32 call::member::{CallMemberEventContent, CallMemberStateKey, MembershipData},
33 direct::OwnedDirectUserIdentifier,
34 room::{
35 avatar::{self, RoomAvatarEventContent},
36 canonical_alias::RoomCanonicalAliasEventContent,
37 encryption::RoomEncryptionEventContent,
38 guest_access::{GuestAccess, RoomGuestAccessEventContent},
39 history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
40 join_rules::{JoinRule, RoomJoinRulesEventContent},
41 name::RoomNameEventContent,
42 pinned_events::RoomPinnedEventsEventContent,
43 redaction::SyncRoomRedactionEvent,
44 tombstone::RoomTombstoneEventContent,
45 topic::RoomTopicEventContent,
46 },
47 tag::{TagEventContent, TagName, Tags},
48 },
49 room::RoomType,
50 room_version_rules::{AuthorizationRules, RedactionRules, RoomVersionRules},
51 serde::Raw,
52};
53use serde::{Deserialize, Serialize};
54use tracing::{error, field::debug, info, instrument, warn};
55
56use super::{
57 AccountDataSource, EncryptionState, Room, RoomCreateWithCreatorEventContent, RoomDisplayName,
58 RoomHero, RoomNotableTags, RoomState, RoomSummary,
59};
60use crate::{
61 MinimalStateEvent, OriginalMinimalStateEvent,
62 deserialized_responses::RawSyncOrStrippedState,
63 latest_event::LatestEventValue,
64 notification_settings::RoomNotificationMode,
65 read_receipts::RoomReadReceipts,
66 store::{DynStateStore, StateStoreExt},
67 sync::UnreadNotificationsCount,
68 utils::RawSyncStateEventWithKeys,
69};
70
71const DEFAULT_MAX_POWER_LEVEL: i64 = 100;
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct InviteAcceptanceDetails {
78 pub invite_accepted_at: MilliSecondsSinceUnixEpoch,
81
82 pub inviter: OwnedUserId,
84}
85
86impl Room {
87 pub fn subscribe_info(&self) -> Subscriber<RoomInfo> {
89 self.info.subscribe()
90 }
91
92 pub fn clone_info(&self) -> RoomInfo {
94 self.info.get()
95 }
96
97 pub fn set_room_info(
99 &self,
100 room_info: RoomInfo,
101 room_info_notable_update_reasons: RoomInfoNotableUpdateReasons,
102 ) {
103 self.info.set(room_info);
104
105 if !room_info_notable_update_reasons.is_empty() {
106 let _ = self.room_info_notable_update_sender.send(RoomInfoNotableUpdate {
108 room_id: self.room_id.clone(),
109 reasons: room_info_notable_update_reasons,
110 });
111 } else {
112 let _ = self.room_info_notable_update_sender.send(RoomInfoNotableUpdate {
116 room_id: self.room_id.clone(),
117 reasons: RoomInfoNotableUpdateReasons::NONE,
118 });
119 }
120 }
121}
122
123#[derive(Clone, Debug, Serialize, Deserialize)]
127pub struct BaseRoomInfo {
128 pub(crate) avatar: Option<MinimalStateEvent<RoomAvatarEventContent>>,
130 pub(crate) canonical_alias: Option<MinimalStateEvent<RoomCanonicalAliasEventContent>>,
132 pub(crate) create: Option<MinimalStateEvent<RoomCreateWithCreatorEventContent>>,
134 pub(crate) dm_targets: HashSet<OwnedDirectUserIdentifier>,
137 pub(crate) encryption: Option<RoomEncryptionEventContent>,
139 pub(crate) guest_access: Option<MinimalStateEvent<RoomGuestAccessEventContent>>,
141 pub(crate) history_visibility: Option<MinimalStateEvent<RoomHistoryVisibilityEventContent>>,
143 pub(crate) join_rules: Option<MinimalStateEvent<RoomJoinRulesEventContent>>,
145 pub(crate) max_power_level: i64,
147 pub(crate) name: Option<MinimalStateEvent<RoomNameEventContent>>,
149 pub(crate) tombstone: Option<MinimalStateEvent<RoomTombstoneEventContent>>,
151 pub(crate) topic: Option<MinimalStateEvent<RoomTopicEventContent>>,
153 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
156 pub(crate) rtc_member_events:
157 BTreeMap<CallMemberStateKey, MinimalStateEvent<CallMemberEventContent>>,
158 #[serde(default)]
160 pub(crate) is_marked_unread: bool,
161 #[serde(default)]
163 pub(crate) is_marked_unread_source: AccountDataSource,
164 #[serde(skip_serializing_if = "RoomNotableTags::is_empty", default)]
169 pub(crate) notable_tags: RoomNotableTags,
170 pub(crate) pinned_events: Option<RoomPinnedEventsEventContent>,
172}
173
174impl BaseRoomInfo {
175 pub fn new() -> Self {
177 Self::default()
178 }
179
180 pub fn room_version(&self) -> Option<&RoomVersionId> {
185 match self.create.as_ref()? {
186 MinimalStateEvent::Original(ev) => Some(&ev.content.room_version),
187 MinimalStateEvent::Redacted(ev) => Some(&ev.content.room_version),
188 }
189 }
190
191 pub fn handle_state_event(&mut self, raw_event: &mut RawSyncStateEventWithKeys) -> bool {
195 match (&raw_event.event_type, raw_event.state_key.as_str()) {
196 (StateEventType::RoomEncryption, "") => {
197 if let Some(SyncStateEvent::Original(event)) =
200 raw_event.deserialize_as(|any_event| {
201 as_variant!(any_event, AnySyncStateEvent::RoomEncryption)
202 })
203 {
204 self.encryption = Some(event.content.clone());
205 true
206 } else {
207 false
208 }
209 }
210 (StateEventType::RoomAvatar, "") => {
211 if let Some(event) = raw_event.deserialize_as(|any_event| {
212 as_variant!(any_event, AnySyncStateEvent::RoomAvatar)
213 }) {
214 self.avatar = Some(event.into());
215 true
216 } else {
217 self.avatar.take().is_some()
219 }
220 }
221 (StateEventType::RoomName, "") => {
222 if let Some(event) = raw_event
223 .deserialize_as(|any_event| as_variant!(any_event, AnySyncStateEvent::RoomName))
224 {
225 self.name = Some(event.into());
226 true
227 } else {
228 self.name.take().is_some()
230 }
231 }
232 (StateEventType::RoomCreate, "") if self.create.is_none() => {
234 if let Some(event) = raw_event.deserialize_as(|any_event| {
235 as_variant!(any_event, AnySyncStateEvent::RoomCreate)
236 }) {
237 self.create = Some(event.into());
238 true
239 } else {
240 false
241 }
242 }
243 (StateEventType::RoomHistoryVisibility, "") => {
244 if let Some(event) = raw_event.deserialize_as(|any_event| {
245 as_variant!(any_event, AnySyncStateEvent::RoomHistoryVisibility)
246 }) {
247 self.history_visibility = Some(event.into());
248 true
249 } else {
250 self.history_visibility.take().is_some()
252 }
253 }
254 (StateEventType::RoomGuestAccess, "") => {
255 if let Some(event) = raw_event.deserialize_as(|any_event| {
256 as_variant!(any_event, AnySyncStateEvent::RoomGuestAccess)
257 }) {
258 self.guest_access = Some(event.into());
259 true
260 } else {
261 self.guest_access.take().is_some()
263 }
264 }
265 (StateEventType::RoomJoinRules, "") => {
266 if let Some(event) = raw_event.deserialize_as(|any_event| {
267 as_variant!(any_event, AnySyncStateEvent::RoomJoinRules)
268 }) {
269 match event.join_rule() {
270 JoinRule::Invite
271 | JoinRule::Knock
272 | JoinRule::Private
273 | JoinRule::Restricted(_)
274 | JoinRule::KnockRestricted(_)
275 | JoinRule::Public => {
276 self.join_rules = Some(event.into());
277 true
278 }
279 r => {
280 warn!(join_rule = ?r.as_str(), "Encountered a custom join rule, skipping");
281 self.join_rules.take().is_some()
283 }
284 }
285 } else {
286 self.join_rules.take().is_some()
288 }
289 }
290 (StateEventType::RoomCanonicalAlias, "") => {
291 if let Some(event) = raw_event.deserialize_as(|any_event| {
292 as_variant!(any_event, AnySyncStateEvent::RoomCanonicalAlias)
293 }) {
294 self.canonical_alias = Some(event.into());
295 true
296 } else {
297 self.canonical_alias.take().is_some()
299 }
300 }
301 (StateEventType::RoomTopic, "") => {
302 if let Some(event) = raw_event.deserialize_as(|any_event| {
303 as_variant!(any_event, AnySyncStateEvent::RoomTopic)
304 }) {
305 self.topic = Some(event.into());
306 true
307 } else {
308 self.topic.take().is_some()
310 }
311 }
312 (StateEventType::RoomTombstone, "") => {
313 if let Some(event) = raw_event.deserialize_as(|any_event| {
314 as_variant!(any_event, AnySyncStateEvent::RoomTombstone)
315 }) {
316 self.tombstone = Some(event.into());
317 true
318 } else {
319 self.tombstone.take().is_some()
321 }
322 }
323 (StateEventType::RoomPowerLevels, "") => {
324 if let Some(event) = raw_event.deserialize_as(|any_event| {
325 as_variant!(any_event, AnySyncStateEvent::RoomPowerLevels)
326 }) {
327 self.max_power_level =
329 event.power_levels(&AuthorizationRules::V1, vec![]).max().into();
330 true
331 } else if self.max_power_level != DEFAULT_MAX_POWER_LEVEL {
332 self.max_power_level = DEFAULT_MAX_POWER_LEVEL;
334 true
335 } else {
336 false
337 }
338 }
339 (StateEventType::CallMember, _) => {
340 if let Some(SyncStateEvent::Original(event)) =
341 raw_event.deserialize_as(|any_event| {
342 as_variant!(any_event, AnySyncStateEvent::CallMember)
343 })
344 {
345 let mut event = event.clone();
348 event.content.set_created_ts_if_none(event.origin_server_ts);
349
350 self.rtc_member_events
352 .insert(event.state_key.clone(), SyncStateEvent::Original(event).into());
353
354 self.rtc_member_events.retain(|_, ev| {
356 ev.as_original()
357 .is_some_and(|o| !o.content.active_memberships(None).is_empty())
358 });
359
360 true
361 } else if let Ok(call_member_key) =
362 raw_event.state_key.parse::<CallMemberStateKey>()
363 {
364 self.rtc_member_events.remove(&call_member_key).is_some()
367 } else {
368 false
369 }
370 }
371 (StateEventType::RoomPinnedEvents, "") => {
372 if let Some(SyncStateEvent::Original(event)) =
373 raw_event.deserialize_as(|any_event| {
374 as_variant!(any_event, AnySyncStateEvent::RoomPinnedEvents)
375 })
376 {
377 self.pinned_events = Some(event.content.clone());
378 true
379 } else {
380 self.pinned_events.take().is_some()
382 }
383 }
384 _ => false,
385 }
386 }
387
388 pub fn handle_stripped_state_event(&mut self, ev: &AnyStrippedStateEvent) -> bool {
393 match ev {
394 AnyStrippedStateEvent::RoomEncryption(encryption) => {
395 if let Some(algorithm) = &encryption.content.algorithm {
396 let content = assign!(RoomEncryptionEventContent::new(algorithm.clone()), {
397 rotation_period_ms: encryption.content.rotation_period_ms,
398 rotation_period_msgs: encryption.content.rotation_period_msgs,
399 });
400 self.encryption = Some(content);
401 }
402 }
406 AnyStrippedStateEvent::RoomAvatar(a) => {
407 self.avatar = Some(a.into());
408 }
409 AnyStrippedStateEvent::RoomName(n) => {
410 self.name = Some(n.into());
411 }
412 AnyStrippedStateEvent::RoomCreate(c) if self.create.is_none() => {
413 self.create = Some(c.into());
414 }
415 AnyStrippedStateEvent::RoomHistoryVisibility(h) => {
416 self.history_visibility = Some(h.into());
417 }
418 AnyStrippedStateEvent::RoomGuestAccess(g) => {
419 self.guest_access = Some(g.into());
420 }
421 AnyStrippedStateEvent::RoomJoinRules(c) => match &c.content.join_rule {
422 JoinRule::Invite
423 | JoinRule::Knock
424 | JoinRule::Private
425 | JoinRule::Restricted(_)
426 | JoinRule::KnockRestricted(_)
427 | JoinRule::Public => self.join_rules = Some(c.into()),
428 r => warn!("Encountered a custom join rule {}, skipping", r.as_str()),
429 },
430 AnyStrippedStateEvent::RoomCanonicalAlias(a) => {
431 self.canonical_alias = Some(a.into());
432 }
433 AnyStrippedStateEvent::RoomTopic(t) => {
434 self.topic = Some(t.into());
435 }
436 AnyStrippedStateEvent::RoomTombstone(t) => {
437 self.tombstone = Some(t.into());
438 }
439 AnyStrippedStateEvent::RoomPowerLevels(p) => {
440 self.max_power_level = p.power_levels(&AuthorizationRules::V1, vec![]).max().into();
442 }
443 AnyStrippedStateEvent::CallMember(_) => {
444 return false;
447 }
448 AnyStrippedStateEvent::RoomPinnedEvents(p) => {
449 if let Some(pinned) = p.content.pinned.clone() {
450 self.pinned_events = Some(RoomPinnedEventsEventContent::new(pinned));
451 }
452 }
453 _ => return false,
454 }
455
456 true
457 }
458
459 pub(super) fn handle_redaction(&mut self, redacts: &EventId) {
460 let redaction_rules = self
461 .room_version()
462 .and_then(|room_version| room_version.rules())
463 .unwrap_or(ROOM_VERSION_RULES_FALLBACK)
464 .redaction;
465
466 if let Some(ev) = &mut self.avatar
467 && ev.event_id() == Some(redacts)
468 {
469 ev.redact(&redaction_rules);
470 } else if let Some(ev) = &mut self.canonical_alias
471 && ev.event_id() == Some(redacts)
472 {
473 ev.redact(&redaction_rules);
474 } else if let Some(ev) = &mut self.create
475 && ev.event_id() == Some(redacts)
476 {
477 ev.redact(&redaction_rules);
478 } else if let Some(ev) = &mut self.guest_access
479 && ev.event_id() == Some(redacts)
480 {
481 ev.redact(&redaction_rules);
482 } else if let Some(ev) = &mut self.history_visibility
483 && ev.event_id() == Some(redacts)
484 {
485 ev.redact(&redaction_rules);
486 } else if let Some(ev) = &mut self.join_rules
487 && ev.event_id() == Some(redacts)
488 {
489 ev.redact(&redaction_rules);
490 } else if let Some(ev) = &mut self.name
491 && ev.event_id() == Some(redacts)
492 {
493 ev.redact(&redaction_rules);
494 } else if let Some(ev) = &mut self.tombstone
495 && ev.event_id() == Some(redacts)
496 {
497 ev.redact(&redaction_rules);
498 } else if let Some(ev) = &mut self.topic
499 && ev.event_id() == Some(redacts)
500 {
501 ev.redact(&redaction_rules);
502 } else {
503 self.rtc_member_events
504 .retain(|_, member_event| member_event.event_id() != Some(redacts));
505 }
506 }
507
508 pub fn handle_notable_tags(&mut self, tags: &Tags) {
509 let mut notable_tags = RoomNotableTags::empty();
510
511 if tags.contains_key(&TagName::Favorite) {
512 notable_tags.insert(RoomNotableTags::FAVOURITE);
513 }
514
515 if tags.contains_key(&TagName::LowPriority) {
516 notable_tags.insert(RoomNotableTags::LOW_PRIORITY);
517 }
518
519 self.notable_tags = notable_tags;
520 }
521}
522
523impl Default for BaseRoomInfo {
524 fn default() -> Self {
525 Self {
526 avatar: None,
527 canonical_alias: None,
528 create: None,
529 dm_targets: Default::default(),
530 encryption: None,
531 guest_access: None,
532 history_visibility: None,
533 join_rules: None,
534 max_power_level: DEFAULT_MAX_POWER_LEVEL,
535 name: None,
536 tombstone: None,
537 topic: None,
538 rtc_member_events: BTreeMap::new(),
539 is_marked_unread: false,
540 is_marked_unread_source: AccountDataSource::Unstable,
541 notable_tags: RoomNotableTags::empty(),
542 pinned_events: None,
543 }
544 }
545}
546
547#[derive(Clone, Debug, Serialize, Deserialize)]
551pub struct RoomInfo {
552 #[serde(default, alias = "version")]
555 pub(crate) data_format_version: u8,
556
557 pub(crate) room_id: OwnedRoomId,
559
560 pub(crate) room_state: RoomState,
562
563 pub(crate) notification_counts: UnreadNotificationsCount,
568
569 pub(crate) summary: RoomSummary,
571
572 pub(crate) members_synced: bool,
574
575 pub(crate) last_prev_batch: Option<String>,
577
578 pub(crate) sync_info: SyncInfo,
580
581 pub(crate) encryption_state_synced: bool,
583
584 #[serde(default)]
586 pub(crate) latest_event_value: LatestEventValue,
587
588 #[serde(default)]
590 pub(crate) read_receipts: RoomReadReceipts,
591
592 pub(crate) base_info: Box<BaseRoomInfo>,
595
596 #[serde(skip)]
600 pub(crate) warned_about_unknown_room_version_rules: Arc<AtomicBool>,
601
602 #[serde(default, skip_serializing_if = "Option::is_none")]
607 pub(crate) cached_display_name: Option<RoomDisplayName>,
608
609 #[serde(default, skip_serializing_if = "Option::is_none")]
611 pub(crate) cached_user_defined_notification_mode: Option<RoomNotificationMode>,
612
613 #[serde(default)]
630 pub(crate) recency_stamp: Option<RoomRecencyStamp>,
631
632 #[serde(default, skip_serializing_if = "Option::is_none")]
638 pub(crate) invite_acceptance_details: Option<InviteAcceptanceDetails>,
639}
640
641impl RoomInfo {
642 #[doc(hidden)] pub fn new(room_id: &RoomId, room_state: RoomState) -> Self {
644 Self {
645 data_format_version: 1,
646 room_id: room_id.into(),
647 room_state,
648 notification_counts: Default::default(),
649 summary: Default::default(),
650 members_synced: false,
651 last_prev_batch: None,
652 sync_info: SyncInfo::NoState,
653 encryption_state_synced: false,
654 latest_event_value: LatestEventValue::default(),
655 read_receipts: Default::default(),
656 base_info: Box::new(BaseRoomInfo::new()),
657 warned_about_unknown_room_version_rules: Arc::new(false.into()),
658 cached_display_name: None,
659 cached_user_defined_notification_mode: None,
660 recency_stamp: None,
661 invite_acceptance_details: None,
662 }
663 }
664
665 pub fn mark_as_joined(&mut self) {
667 self.set_state(RoomState::Joined);
668 }
669
670 pub fn mark_as_left(&mut self) {
672 self.set_state(RoomState::Left);
673 }
674
675 pub fn mark_as_invited(&mut self) {
677 self.set_state(RoomState::Invited);
678 }
679
680 pub fn mark_as_knocked(&mut self) {
682 self.set_state(RoomState::Knocked);
683 }
684
685 pub fn mark_as_banned(&mut self) {
687 self.set_state(RoomState::Banned);
688 }
689
690 pub fn set_state(&mut self, room_state: RoomState) {
692 if self.state() != RoomState::Joined && self.invite_acceptance_details.is_some() {
693 error!(room_id = %self.room_id, "The RoomInfo contains invite acceptance details but the room is not in the joined state");
694 }
695 self.invite_acceptance_details = None;
698 self.room_state = room_state;
699 }
700
701 pub fn mark_members_synced(&mut self) {
703 self.members_synced = true;
704 }
705
706 pub fn mark_members_missing(&mut self) {
708 self.members_synced = false;
709 }
710
711 pub fn are_members_synced(&self) -> bool {
713 self.members_synced
714 }
715
716 pub fn mark_state_partially_synced(&mut self) {
718 self.sync_info = SyncInfo::PartiallySynced;
719 }
720
721 pub fn mark_state_fully_synced(&mut self) {
723 self.sync_info = SyncInfo::FullySynced;
724 }
725
726 pub fn mark_state_not_synced(&mut self) {
728 self.sync_info = SyncInfo::NoState;
729 }
730
731 pub fn mark_encryption_state_synced(&mut self) {
733 self.encryption_state_synced = true;
734 }
735
736 pub fn mark_encryption_state_missing(&mut self) {
738 self.encryption_state_synced = false;
739 }
740
741 pub fn set_prev_batch(&mut self, prev_batch: Option<&str>) -> bool {
745 if self.last_prev_batch.as_deref() != prev_batch {
746 self.last_prev_batch = prev_batch.map(|p| p.to_owned());
747 true
748 } else {
749 false
750 }
751 }
752
753 pub fn state(&self) -> RoomState {
755 self.room_state
756 }
757
758 #[cfg(not(feature = "experimental-encrypted-state-events"))]
760 pub fn encryption_state(&self) -> EncryptionState {
761 if !self.encryption_state_synced {
762 EncryptionState::Unknown
763 } else if self.base_info.encryption.is_some() {
764 EncryptionState::Encrypted
765 } else {
766 EncryptionState::NotEncrypted
767 }
768 }
769
770 #[cfg(feature = "experimental-encrypted-state-events")]
772 pub fn encryption_state(&self) -> EncryptionState {
773 if !self.encryption_state_synced {
774 EncryptionState::Unknown
775 } else {
776 self.base_info
777 .encryption
778 .as_ref()
779 .map(|state| {
780 if state.encrypt_state_events {
781 EncryptionState::StateEncrypted
782 } else {
783 EncryptionState::Encrypted
784 }
785 })
786 .unwrap_or(EncryptionState::NotEncrypted)
787 }
788 }
789
790 pub fn set_encryption_event(&mut self, event: Option<RoomEncryptionEventContent>) {
792 self.base_info.encryption = event;
793 }
794
795 pub fn handle_encryption_state(
797 &mut self,
798 requested_required_states: &[(StateEventType, String)],
799 ) {
800 if requested_required_states
801 .iter()
802 .any(|(state_event, _)| state_event == &StateEventType::RoomEncryption)
803 {
804 self.mark_encryption_state_synced();
810 }
811 }
812
813 pub fn handle_state_event(&mut self, raw_event: &mut RawSyncStateEventWithKeys) -> bool {
817 let base_info_has_been_modified = self.base_info.handle_state_event(raw_event);
819
820 if raw_event.event_type == StateEventType::RoomEncryption && raw_event.state_key.is_empty()
821 {
822 self.mark_encryption_state_synced();
828 }
829
830 base_info_has_been_modified
831 }
832
833 pub fn handle_stripped_state_event(&mut self, event: &AnyStrippedStateEvent) -> bool {
837 self.base_info.handle_stripped_state_event(event)
838 }
839
840 #[instrument(skip_all, fields(redacts))]
842 pub fn handle_redaction(
843 &mut self,
844 event: &SyncRoomRedactionEvent,
845 _raw: &Raw<SyncRoomRedactionEvent>,
846 ) {
847 let redaction_rules = self.room_version_rules_or_default().redaction;
848
849 let Some(redacts) = event.redacts(&redaction_rules) else {
850 info!("Can't apply redaction, redacts field is missing");
851 return;
852 };
853 tracing::Span::current().record("redacts", debug(redacts));
854
855 self.base_info.handle_redaction(redacts);
856 }
857
858 pub fn avatar_url(&self) -> Option<&MxcUri> {
860 self.base_info
861 .avatar
862 .as_ref()
863 .and_then(|e| e.as_original().and_then(|e| e.content.url.as_deref()))
864 }
865
866 pub fn update_avatar(&mut self, url: Option<OwnedMxcUri>) {
868 self.base_info.avatar = url.map(|url| {
869 let mut content = RoomAvatarEventContent::new();
870 content.url = Some(url);
871
872 MinimalStateEvent::Original(OriginalMinimalStateEvent { content, event_id: None })
873 });
874 }
875
876 pub fn avatar_info(&self) -> Option<&avatar::ImageInfo> {
878 self.base_info
879 .avatar
880 .as_ref()
881 .and_then(|e| e.as_original().and_then(|e| e.content.info.as_deref()))
882 }
883
884 pub fn update_notification_count(&mut self, notification_counts: UnreadNotificationsCount) {
886 self.notification_counts = notification_counts;
887 }
888
889 pub fn update_from_ruma_summary(&mut self, summary: &RumaSummary) -> bool {
893 let mut changed = false;
894
895 if !summary.is_empty() {
896 if !summary.heroes.is_empty() {
897 self.summary.room_heroes = summary
898 .heroes
899 .iter()
900 .map(|hero_id| RoomHero {
901 user_id: hero_id.to_owned(),
902 display_name: None,
903 avatar_url: None,
904 })
905 .collect();
906
907 changed = true;
908 }
909
910 if let Some(joined) = summary.joined_member_count {
911 self.summary.joined_member_count = joined.into();
912 changed = true;
913 }
914
915 if let Some(invited) = summary.invited_member_count {
916 self.summary.invited_member_count = invited.into();
917 changed = true;
918 }
919 }
920
921 changed
922 }
923
924 pub(crate) fn update_joined_member_count(&mut self, count: u64) {
926 self.summary.joined_member_count = count;
927 }
928
929 pub(crate) fn update_invited_member_count(&mut self, count: u64) {
931 self.summary.invited_member_count = count;
932 }
933
934 pub(crate) fn set_invite_acceptance_details(&mut self, details: InviteAcceptanceDetails) {
935 self.invite_acceptance_details = Some(details);
936 }
937
938 pub fn invite_acceptance_details(&self) -> Option<InviteAcceptanceDetails> {
945 self.invite_acceptance_details.clone()
946 }
947
948 pub(crate) fn update_heroes(&mut self, heroes: Vec<RoomHero>) {
950 self.summary.room_heroes = heroes;
951 }
952
953 pub fn heroes(&self) -> &[RoomHero] {
955 &self.summary.room_heroes
956 }
957
958 pub fn active_members_count(&self) -> u64 {
962 self.summary.joined_member_count.saturating_add(self.summary.invited_member_count)
963 }
964
965 pub fn invited_members_count(&self) -> u64 {
967 self.summary.invited_member_count
968 }
969
970 pub fn joined_members_count(&self) -> u64 {
972 self.summary.joined_member_count
973 }
974
975 pub fn canonical_alias(&self) -> Option<&RoomAliasId> {
977 self.base_info.canonical_alias.as_ref()?.as_original()?.content.alias.as_deref()
978 }
979
980 pub fn alt_aliases(&self) -> &[OwnedRoomAliasId] {
982 self.base_info
983 .canonical_alias
984 .as_ref()
985 .and_then(|ev| ev.as_original())
986 .map(|ev| ev.content.alt_aliases.as_ref())
987 .unwrap_or_default()
988 }
989
990 pub fn room_id(&self) -> &RoomId {
992 &self.room_id
993 }
994
995 pub fn room_version(&self) -> Option<&RoomVersionId> {
997 self.base_info.room_version()
998 }
999
1000 pub fn room_version_rules_or_default(&self) -> RoomVersionRules {
1005 use std::sync::atomic::Ordering;
1006
1007 self.base_info.room_version().and_then(|room_version| room_version.rules()).unwrap_or_else(
1008 || {
1009 if self
1010 .warned_about_unknown_room_version_rules
1011 .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
1012 .is_ok()
1013 {
1014 warn!("Unable to get the room version rules, defaulting to rules for room version {ROOM_VERSION_FALLBACK}");
1015 }
1016
1017 ROOM_VERSION_RULES_FALLBACK
1018 },
1019 )
1020 }
1021
1022 pub fn room_type(&self) -> Option<&RoomType> {
1024 match self.base_info.create.as_ref()? {
1025 MinimalStateEvent::Original(ev) => ev.content.room_type.as_ref(),
1026 MinimalStateEvent::Redacted(ev) => ev.content.room_type.as_ref(),
1027 }
1028 }
1029
1030 pub fn creators(&self) -> Option<Vec<OwnedUserId>> {
1032 match self.base_info.create.as_ref()? {
1033 MinimalStateEvent::Original(ev) => Some(ev.content.creators()),
1034 MinimalStateEvent::Redacted(ev) => Some(ev.content.creators()),
1035 }
1036 }
1037
1038 pub(super) fn guest_access(&self) -> &GuestAccess {
1039 match &self.base_info.guest_access {
1040 Some(MinimalStateEvent::Original(ev)) => &ev.content.guest_access,
1041 _ => &GuestAccess::Forbidden,
1042 }
1043 }
1044
1045 pub fn history_visibility(&self) -> Option<&HistoryVisibility> {
1049 match &self.base_info.history_visibility {
1050 Some(MinimalStateEvent::Original(ev)) => Some(&ev.content.history_visibility),
1051 _ => None,
1052 }
1053 }
1054
1055 pub fn history_visibility_or_default(&self) -> &HistoryVisibility {
1062 match &self.base_info.history_visibility {
1063 Some(MinimalStateEvent::Original(ev)) => &ev.content.history_visibility,
1064 _ => &HistoryVisibility::Shared,
1065 }
1066 }
1067
1068 pub fn join_rule(&self) -> Option<&JoinRule> {
1071 match &self.base_info.join_rules {
1072 Some(MinimalStateEvent::Original(ev)) => Some(&ev.content.join_rule),
1073 _ => None,
1074 }
1075 }
1076
1077 pub fn name(&self) -> Option<&str> {
1079 let name = &self.base_info.name.as_ref()?.as_original()?.content.name;
1080 (!name.is_empty()).then_some(name)
1081 }
1082
1083 pub fn create(&self) -> Option<&RoomCreateWithCreatorEventContent> {
1085 Some(&self.base_info.create.as_ref()?.as_original()?.content)
1086 }
1087
1088 pub fn tombstone(&self) -> Option<&RoomTombstoneEventContent> {
1090 Some(&self.base_info.tombstone.as_ref()?.as_original()?.content)
1091 }
1092
1093 pub fn topic(&self) -> Option<&str> {
1095 Some(&self.base_info.topic.as_ref()?.as_original()?.content.topic)
1096 }
1097
1098 fn active_matrix_rtc_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
1103 let mut v = self
1104 .base_info
1105 .rtc_member_events
1106 .iter()
1107 .filter_map(|(user_id, ev)| {
1108 ev.as_original().map(|ev| {
1109 ev.content
1110 .active_memberships(None)
1111 .into_iter()
1112 .map(move |m| (user_id.clone(), m))
1113 })
1114 })
1115 .flatten()
1116 .collect::<Vec<_>>();
1117 v.sort_by_key(|(_, m)| m.created_ts());
1118 v
1119 }
1120
1121 fn active_room_call_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
1127 self.active_matrix_rtc_memberships()
1128 .into_iter()
1129 .filter(|(_user_id, m)| m.is_room_call())
1130 .collect()
1131 }
1132
1133 pub fn has_active_room_call(&self) -> bool {
1136 !self.active_room_call_memberships().is_empty()
1137 }
1138
1139 pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
1148 self.active_room_call_memberships()
1149 .iter()
1150 .map(|(call_member_state_key, _)| call_member_state_key.user_id().to_owned())
1151 .collect()
1152 }
1153
1154 pub fn set_latest_event(&mut self, new_value: LatestEventValue) {
1156 self.latest_event_value = new_value;
1157 }
1158
1159 pub fn update_recency_stamp(&mut self, stamp: RoomRecencyStamp) {
1163 self.recency_stamp = Some(stamp);
1164 }
1165
1166 pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
1168 self.base_info.pinned_events.clone().map(|c| c.pinned)
1169 }
1170
1171 pub fn is_pinned_event(&self, event_id: &EventId) -> bool {
1177 self.base_info
1178 .pinned_events
1179 .as_ref()
1180 .map(|p| p.pinned.contains(&event_id.to_owned()))
1181 .unwrap_or_default()
1182 }
1183
1184 #[instrument(skip_all, fields(room_id = ?self.room_id))]
1192 pub(crate) async fn apply_migrations(&mut self, store: Arc<DynStateStore>) -> bool {
1193 let mut migrated = false;
1194
1195 if self.data_format_version < 1 {
1196 info!("Migrating room info to version 1");
1197
1198 match store.get_room_account_data_event_static::<TagEventContent>(&self.room_id).await {
1200 Ok(Some(raw_event)) => match raw_event.deserialize() {
1202 Ok(event) => {
1203 self.base_info.handle_notable_tags(&event.content.tags);
1204 }
1205 Err(error) => {
1206 warn!("Failed to deserialize room tags: {error}");
1207 }
1208 },
1209 Ok(_) => {
1210 }
1212 Err(error) => {
1213 warn!("Failed to load room tags: {error}");
1214 }
1215 }
1216
1217 match store.get_state_event_static::<RoomPinnedEventsEventContent>(&self.room_id).await
1219 {
1220 Ok(Some(RawSyncOrStrippedState::Sync(raw_event))) => {
1222 if let Some(mut raw_event) =
1223 RawSyncStateEventWithKeys::try_from_raw_state_event(raw_event.cast())
1224 {
1225 self.handle_state_event(&mut raw_event);
1226 }
1227 }
1228 Ok(_) => {
1229 }
1231 Err(error) => {
1232 warn!("Failed to load room pinned events: {error}");
1233 }
1234 }
1235
1236 self.data_format_version = 1;
1237 migrated = true;
1238 }
1239
1240 migrated
1241 }
1242}
1243
1244#[repr(transparent)]
1246#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
1247#[serde(transparent)]
1248pub struct RoomRecencyStamp(u64);
1249
1250impl From<u64> for RoomRecencyStamp {
1251 fn from(value: u64) -> Self {
1252 Self(value)
1253 }
1254}
1255
1256impl From<RoomRecencyStamp> for u64 {
1257 fn from(value: RoomRecencyStamp) -> Self {
1258 value.0
1259 }
1260}
1261
1262#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1263pub(crate) enum SyncInfo {
1264 NoState,
1270
1271 PartiallySynced,
1274
1275 FullySynced,
1277}
1278
1279pub fn apply_redaction(
1282 event: &Raw<AnySyncTimelineEvent>,
1283 raw_redaction: &Raw<SyncRoomRedactionEvent>,
1284 rules: &RedactionRules,
1285) -> Option<Raw<AnySyncTimelineEvent>> {
1286 use ruma::canonical_json::{RedactedBecause, redact_in_place};
1287
1288 let mut event_json = match event.deserialize_as() {
1289 Ok(json) => json,
1290 Err(e) => {
1291 warn!("Failed to deserialize latest event: {e}");
1292 return None;
1293 }
1294 };
1295
1296 let redacted_because = match RedactedBecause::from_raw_event(raw_redaction) {
1297 Ok(rb) => rb,
1298 Err(e) => {
1299 warn!("Redaction event is not valid canonical JSON: {e}");
1300 return None;
1301 }
1302 };
1303
1304 let redact_result = redact_in_place(&mut event_json, rules, Some(redacted_because));
1305
1306 if let Err(e) = redact_result {
1307 warn!("Failed to redact event: {e}");
1308 return None;
1309 }
1310
1311 let raw = Raw::new(&event_json).expect("CanonicalJsonObject must be serializable");
1312 Some(raw.cast_unchecked())
1313}
1314
1315#[derive(Debug, Clone)]
1325pub struct RoomInfoNotableUpdate {
1326 pub room_id: OwnedRoomId,
1328
1329 pub reasons: RoomInfoNotableUpdateReasons,
1331}
1332
1333bitflags! {
1334 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1336 pub struct RoomInfoNotableUpdateReasons: u8 {
1337 const RECENCY_STAMP = 0b0000_0001;
1339
1340 const LATEST_EVENT = 0b0000_0010;
1342
1343 const READ_RECEIPT = 0b0000_0100;
1345
1346 const UNREAD_MARKER = 0b0000_1000;
1348
1349 const MEMBERSHIP = 0b0001_0000;
1351
1352 const DISPLAY_NAME = 0b0010_0000;
1354
1355 const NONE = 0b1000_0000;
1366 }
1367}
1368
1369impl Default for RoomInfoNotableUpdateReasons {
1370 fn default() -> Self {
1371 Self::empty()
1372 }
1373}
1374
1375#[cfg(test)]
1376mod tests {
1377 use std::sync::Arc;
1378
1379 use assert_matches::assert_matches;
1380 use matrix_sdk_test::{
1381 async_test,
1382 test_json::{TAG, sync_events::PINNED_EVENTS},
1383 };
1384 use ruma::{
1385 assign, events::room::pinned_events::RoomPinnedEventsEventContent, owned_event_id,
1386 owned_mxc_uri, owned_user_id, room_id, serde::Raw,
1387 };
1388 use serde_json::json;
1389 use similar_asserts::assert_eq;
1390
1391 use super::{BaseRoomInfo, LatestEventValue, RoomInfo, SyncInfo};
1392 use crate::{
1393 RoomDisplayName, RoomHero, RoomState, StateChanges,
1394 notification_settings::RoomNotificationMode,
1395 room::{RoomNotableTags, RoomSummary},
1396 store::{IntoStateStore, MemoryStore},
1397 sync::UnreadNotificationsCount,
1398 };
1399
1400 #[test]
1401 fn test_room_info_serialization() {
1402 let info = RoomInfo {
1406 data_format_version: 1,
1407 room_id: room_id!("!gda78o:server.tld").into(),
1408 room_state: RoomState::Invited,
1409 notification_counts: UnreadNotificationsCount {
1410 highlight_count: 1,
1411 notification_count: 2,
1412 },
1413 summary: RoomSummary {
1414 room_heroes: vec![RoomHero {
1415 user_id: owned_user_id!("@somebody:example.org"),
1416 display_name: None,
1417 avatar_url: None,
1418 }],
1419 joined_member_count: 5,
1420 invited_member_count: 0,
1421 },
1422 members_synced: true,
1423 last_prev_batch: Some("pb".to_owned()),
1424 sync_info: SyncInfo::FullySynced,
1425 encryption_state_synced: true,
1426 latest_event_value: LatestEventValue::None,
1427 base_info: Box::new(
1428 assign!(BaseRoomInfo::new(), { pinned_events: Some(RoomPinnedEventsEventContent::new(vec![owned_event_id!("$a")])) }),
1429 ),
1430 read_receipts: Default::default(),
1431 warned_about_unknown_room_version_rules: Arc::new(false.into()),
1432 cached_display_name: None,
1433 cached_user_defined_notification_mode: None,
1434 recency_stamp: Some(42.into()),
1435 invite_acceptance_details: None,
1436 };
1437
1438 let info_json = json!({
1439 "data_format_version": 1,
1440 "room_id": "!gda78o:server.tld",
1441 "room_state": "Invited",
1442 "notification_counts": {
1443 "highlight_count": 1,
1444 "notification_count": 2,
1445 },
1446 "summary": {
1447 "room_heroes": [{
1448 "user_id": "@somebody:example.org",
1449 "display_name": null,
1450 "avatar_url": null
1451 }],
1452 "joined_member_count": 5,
1453 "invited_member_count": 0,
1454 },
1455 "members_synced": true,
1456 "last_prev_batch": "pb",
1457 "sync_info": "FullySynced",
1458 "encryption_state_synced": true,
1459 "latest_event_value": "None",
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 "is_marked_unread": false,
1469 "is_marked_unread_source": "Unstable",
1470 "join_rules": null,
1471 "max_power_level": 100,
1472 "name": null,
1473 "tombstone": null,
1474 "topic": null,
1475 "pinned_events": {
1476 "pinned": ["$a"]
1477 },
1478 },
1479 "read_receipts": {
1480 "num_unread": 0,
1481 "num_mentions": 0,
1482 "num_notifications": 0,
1483 "latest_active": null,
1484 "pending": [],
1485 },
1486 "recency_stamp": 42,
1487 });
1488
1489 assert_eq!(serde_json::to_value(info).unwrap(), info_json);
1490 }
1491
1492 #[async_test]
1493 async fn test_room_info_migration_v1() {
1494 let store = MemoryStore::new().into_state_store();
1495
1496 let room_info_json = json!({
1497 "room_id": "!gda78o:server.tld",
1498 "room_state": "Joined",
1499 "notification_counts": {
1500 "highlight_count": 1,
1501 "notification_count": 2,
1502 },
1503 "summary": {
1504 "room_heroes": [{
1505 "user_id": "@somebody:example.org",
1506 "display_name": null,
1507 "avatar_url": null
1508 }],
1509 "joined_member_count": 5,
1510 "invited_member_count": 0,
1511 },
1512 "members_synced": true,
1513 "last_prev_batch": "pb",
1514 "sync_info": "FullySynced",
1515 "encryption_state_synced": true,
1516 "latest_event": {
1517 "event": {
1518 "encryption_info": null,
1519 "event": {
1520 "sender": "@u:i.uk",
1521 },
1522 },
1523 },
1524 "base_info": {
1525 "avatar": null,
1526 "canonical_alias": null,
1527 "create": null,
1528 "dm_targets": [],
1529 "encryption": null,
1530 "guest_access": null,
1531 "history_visibility": null,
1532 "join_rules": null,
1533 "max_power_level": 100,
1534 "name": null,
1535 "tombstone": null,
1536 "topic": null,
1537 },
1538 "read_receipts": {
1539 "num_unread": 0,
1540 "num_mentions": 0,
1541 "num_notifications": 0,
1542 "latest_active": null,
1543 "pending": []
1544 },
1545 "recency_stamp": 42,
1546 });
1547 let mut room_info: RoomInfo = serde_json::from_value(room_info_json).unwrap();
1548
1549 assert_eq!(room_info.data_format_version, 0);
1550 assert!(room_info.base_info.notable_tags.is_empty());
1551 assert!(room_info.base_info.pinned_events.is_none());
1552
1553 assert!(room_info.apply_migrations(store.clone()).await);
1555
1556 assert_eq!(room_info.data_format_version, 1);
1557 assert!(room_info.base_info.notable_tags.is_empty());
1558 assert!(room_info.base_info.pinned_events.is_none());
1559
1560 assert!(!room_info.apply_migrations(store.clone()).await);
1562
1563 assert_eq!(room_info.data_format_version, 1);
1564 assert!(room_info.base_info.notable_tags.is_empty());
1565 assert!(room_info.base_info.pinned_events.is_none());
1566
1567 let mut changes = StateChanges::default();
1569
1570 let raw_tag_event = Raw::new(&*TAG).unwrap().cast_unchecked();
1571 let tag_event = raw_tag_event.deserialize().unwrap();
1572 changes.add_room_account_data(&room_info.room_id, tag_event, raw_tag_event);
1573
1574 let raw_pinned_events_event = Raw::new(&*PINNED_EVENTS).unwrap().cast_unchecked();
1575 let pinned_events_event = raw_pinned_events_event.deserialize().unwrap();
1576 changes.add_state_event(&room_info.room_id, pinned_events_event, raw_pinned_events_event);
1577
1578 store.save_changes(&changes).await.unwrap();
1579
1580 room_info.data_format_version = 0;
1582 assert!(room_info.apply_migrations(store.clone()).await);
1583
1584 assert_eq!(room_info.data_format_version, 1);
1585 assert!(room_info.base_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
1586 assert!(room_info.base_info.pinned_events.is_some());
1587
1588 let new_room_info = RoomInfo::new(room_id!("!new_room:localhost"), RoomState::Joined);
1590 assert_eq!(new_room_info.data_format_version, 1);
1591 }
1592
1593 #[test]
1594 fn test_room_info_deserialization() {
1595 let info_json = json!({
1596 "room_id": "!gda78o:server.tld",
1597 "room_state": "Joined",
1598 "notification_counts": {
1599 "highlight_count": 1,
1600 "notification_count": 2,
1601 },
1602 "summary": {
1603 "room_heroes": [{
1604 "user_id": "@somebody:example.org",
1605 "display_name": "Somebody",
1606 "avatar_url": "mxc://example.org/abc"
1607 }],
1608 "joined_member_count": 5,
1609 "invited_member_count": 0,
1610 },
1611 "members_synced": true,
1612 "last_prev_batch": "pb",
1613 "sync_info": "FullySynced",
1614 "encryption_state_synced": true,
1615 "base_info": {
1616 "avatar": null,
1617 "canonical_alias": null,
1618 "create": null,
1619 "dm_targets": [],
1620 "encryption": null,
1621 "guest_access": null,
1622 "history_visibility": null,
1623 "join_rules": null,
1624 "max_power_level": 100,
1625 "name": null,
1626 "tombstone": null,
1627 "topic": null,
1628 },
1629 "cached_display_name": { "Calculated": "lol" },
1630 "cached_user_defined_notification_mode": "Mute",
1631 "recency_stamp": 42,
1632 });
1633
1634 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1635
1636 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1637 assert_eq!(info.room_state, RoomState::Joined);
1638 assert_eq!(info.notification_counts.highlight_count, 1);
1639 assert_eq!(info.notification_counts.notification_count, 2);
1640 assert_eq!(
1641 info.summary.room_heroes,
1642 vec![RoomHero {
1643 user_id: owned_user_id!("@somebody:example.org"),
1644 display_name: Some("Somebody".to_owned()),
1645 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1646 }]
1647 );
1648 assert_eq!(info.summary.joined_member_count, 5);
1649 assert_eq!(info.summary.invited_member_count, 0);
1650 assert!(info.members_synced);
1651 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1652 assert_eq!(info.sync_info, SyncInfo::FullySynced);
1653 assert!(info.encryption_state_synced);
1654 assert_matches!(info.latest_event_value, LatestEventValue::None);
1655 assert!(info.base_info.avatar.is_none());
1656 assert!(info.base_info.canonical_alias.is_none());
1657 assert!(info.base_info.create.is_none());
1658 assert_eq!(info.base_info.dm_targets.len(), 0);
1659 assert!(info.base_info.encryption.is_none());
1660 assert!(info.base_info.guest_access.is_none());
1661 assert!(info.base_info.history_visibility.is_none());
1662 assert!(info.base_info.join_rules.is_none());
1663 assert_eq!(info.base_info.max_power_level, 100);
1664 assert!(info.base_info.name.is_none());
1665 assert!(info.base_info.tombstone.is_none());
1666 assert!(info.base_info.topic.is_none());
1667
1668 assert_eq!(
1669 info.cached_display_name.as_ref(),
1670 Some(&RoomDisplayName::Calculated("lol".to_owned())),
1671 );
1672 assert_eq!(
1673 info.cached_user_defined_notification_mode.as_ref(),
1674 Some(&RoomNotificationMode::Mute)
1675 );
1676 assert_eq!(info.recency_stamp.as_ref(), Some(&42.into()));
1677 }
1678
1679 #[test]
1686 fn test_room_info_deserialization_without_optional_items() {
1687 let info_json = json!({
1690 "room_id": "!gda78o:server.tld",
1691 "room_state": "Invited",
1692 "notification_counts": {
1693 "highlight_count": 1,
1694 "notification_count": 2,
1695 },
1696 "summary": {
1697 "room_heroes": [{
1698 "user_id": "@somebody:example.org",
1699 "display_name": "Somebody",
1700 "avatar_url": "mxc://example.org/abc"
1701 }],
1702 "joined_member_count": 5,
1703 "invited_member_count": 0,
1704 },
1705 "members_synced": true,
1706 "last_prev_batch": "pb",
1707 "sync_info": "FullySynced",
1708 "encryption_state_synced": true,
1709 "base_info": {
1710 "avatar": null,
1711 "canonical_alias": null,
1712 "create": null,
1713 "dm_targets": [],
1714 "encryption": null,
1715 "guest_access": null,
1716 "history_visibility": null,
1717 "join_rules": null,
1718 "max_power_level": 100,
1719 "name": null,
1720 "tombstone": null,
1721 "topic": null,
1722 },
1723 });
1724
1725 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1726
1727 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1728 assert_eq!(info.room_state, RoomState::Invited);
1729 assert_eq!(info.notification_counts.highlight_count, 1);
1730 assert_eq!(info.notification_counts.notification_count, 2);
1731 assert_eq!(
1732 info.summary.room_heroes,
1733 vec![RoomHero {
1734 user_id: owned_user_id!("@somebody:example.org"),
1735 display_name: Some("Somebody".to_owned()),
1736 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1737 }]
1738 );
1739 assert_eq!(info.summary.joined_member_count, 5);
1740 assert_eq!(info.summary.invited_member_count, 0);
1741 assert!(info.members_synced);
1742 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1743 assert_eq!(info.sync_info, SyncInfo::FullySynced);
1744 assert!(info.encryption_state_synced);
1745 assert!(info.base_info.avatar.is_none());
1746 assert!(info.base_info.canonical_alias.is_none());
1747 assert!(info.base_info.create.is_none());
1748 assert_eq!(info.base_info.dm_targets.len(), 0);
1749 assert!(info.base_info.encryption.is_none());
1750 assert!(info.base_info.guest_access.is_none());
1751 assert!(info.base_info.history_visibility.is_none());
1752 assert!(info.base_info.join_rules.is_none());
1753 assert_eq!(info.base_info.max_power_level, 100);
1754 assert!(info.base_info.name.is_none());
1755 assert!(info.base_info.tombstone.is_none());
1756 assert!(info.base_info.topic.is_none());
1757 }
1758}