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.inner.subscribe()
88 }
89
90 pub fn clone_info(&self) -> RoomInfo {
92 self.inner.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.inner.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) => {
221 self.join_rules = Some(c.into());
222 }
223 AnySyncStateEvent::RoomCanonicalAlias(a) => {
224 self.canonical_alias = Some(a.into());
225 }
226 AnySyncStateEvent::RoomTopic(t) => {
227 self.topic = Some(t.into());
228 }
229 AnySyncStateEvent::RoomTombstone(t) => {
230 self.tombstone = Some(t.into());
231 }
232 AnySyncStateEvent::RoomPowerLevels(p) => {
233 self.max_power_level = p.power_levels(&AuthorizationRules::V1, vec![]).max().into();
235 }
236 AnySyncStateEvent::CallMember(m) => {
237 let Some(o_ev) = m.as_original() else {
238 return false;
239 };
240
241 let mut o_ev = o_ev.clone();
244 o_ev.content.set_created_ts_if_none(o_ev.origin_server_ts);
245
246 self.rtc_member_events
248 .insert(m.state_key().clone(), SyncStateEvent::Original(o_ev).into());
249
250 self.rtc_member_events.retain(|_, ev| {
252 ev.as_original().is_some_and(|o| !o.content.active_memberships(None).is_empty())
253 });
254 }
255 AnySyncStateEvent::RoomPinnedEvents(p) => {
256 self.pinned_events = p.as_original().map(|p| p.content.clone());
257 }
258 _ => return false,
259 }
260
261 true
262 }
263
264 pub fn handle_stripped_state_event(&mut self, ev: &AnyStrippedStateEvent) -> bool {
269 match ev {
270 AnyStrippedStateEvent::RoomEncryption(encryption) => {
271 if let Some(algorithm) = &encryption.content.algorithm {
272 let content = assign!(RoomEncryptionEventContent::new(algorithm.clone()), {
273 rotation_period_ms: encryption.content.rotation_period_ms,
274 rotation_period_msgs: encryption.content.rotation_period_msgs,
275 });
276 self.encryption = Some(content);
277 }
278 }
282 AnyStrippedStateEvent::RoomAvatar(a) => {
283 self.avatar = Some(a.into());
284 }
285 AnyStrippedStateEvent::RoomName(n) => {
286 self.name = Some(n.into());
287 }
288 AnyStrippedStateEvent::RoomCreate(c) if self.create.is_none() => {
289 self.create = Some(c.into());
290 }
291 AnyStrippedStateEvent::RoomHistoryVisibility(h) => {
292 self.history_visibility = Some(h.into());
293 }
294 AnyStrippedStateEvent::RoomGuestAccess(g) => {
295 self.guest_access = Some(g.into());
296 }
297 AnyStrippedStateEvent::RoomJoinRules(c) => {
298 self.join_rules = Some(c.into());
299 }
300 AnyStrippedStateEvent::RoomCanonicalAlias(a) => {
301 self.canonical_alias = Some(a.into());
302 }
303 AnyStrippedStateEvent::RoomTopic(t) => {
304 self.topic = Some(t.into());
305 }
306 AnyStrippedStateEvent::RoomTombstone(t) => {
307 self.tombstone = Some(t.into());
308 }
309 AnyStrippedStateEvent::RoomPowerLevels(p) => {
310 self.max_power_level = p.power_levels(&AuthorizationRules::V1, vec![]).max().into();
312 }
313 AnyStrippedStateEvent::CallMember(_) => {
314 return false;
317 }
318 AnyStrippedStateEvent::RoomPinnedEvents(p) => {
319 if let Some(pinned) = p.content.pinned.clone() {
320 self.pinned_events = Some(RoomPinnedEventsEventContent::new(pinned));
321 }
322 }
323 _ => return false,
324 }
325
326 true
327 }
328
329 pub(super) fn handle_redaction(&mut self, redacts: &EventId) {
330 let redaction_rules = self
331 .room_version()
332 .and_then(|room_version| room_version.rules())
333 .unwrap_or(ROOM_VERSION_RULES_FALLBACK)
334 .redaction;
335
336 if let Some(ev) = &mut self.avatar
337 && ev.event_id() == Some(redacts)
338 {
339 ev.redact(&redaction_rules);
340 } else if let Some(ev) = &mut self.canonical_alias
341 && ev.event_id() == Some(redacts)
342 {
343 ev.redact(&redaction_rules);
344 } else if let Some(ev) = &mut self.create
345 && ev.event_id() == Some(redacts)
346 {
347 ev.redact(&redaction_rules);
348 } else if let Some(ev) = &mut self.guest_access
349 && ev.event_id() == Some(redacts)
350 {
351 ev.redact(&redaction_rules);
352 } else if let Some(ev) = &mut self.history_visibility
353 && ev.event_id() == Some(redacts)
354 {
355 ev.redact(&redaction_rules);
356 } else if let Some(ev) = &mut self.join_rules
357 && ev.event_id() == Some(redacts)
358 {
359 ev.redact(&redaction_rules);
360 } else if let Some(ev) = &mut self.name
361 && ev.event_id() == Some(redacts)
362 {
363 ev.redact(&redaction_rules);
364 } else if let Some(ev) = &mut self.tombstone
365 && ev.event_id() == Some(redacts)
366 {
367 ev.redact(&redaction_rules);
368 } else if let Some(ev) = &mut self.topic
369 && ev.event_id() == Some(redacts)
370 {
371 ev.redact(&redaction_rules);
372 } else {
373 self.rtc_member_events
374 .retain(|_, member_event| member_event.event_id() != Some(redacts));
375 }
376 }
377
378 pub fn handle_notable_tags(&mut self, tags: &Tags) {
379 let mut notable_tags = RoomNotableTags::empty();
380
381 if tags.contains_key(&TagName::Favorite) {
382 notable_tags.insert(RoomNotableTags::FAVOURITE);
383 }
384
385 if tags.contains_key(&TagName::LowPriority) {
386 notable_tags.insert(RoomNotableTags::LOW_PRIORITY);
387 }
388
389 self.notable_tags = notable_tags;
390 }
391}
392
393impl Default for BaseRoomInfo {
394 fn default() -> Self {
395 Self {
396 avatar: None,
397 beacons: BTreeMap::new(),
398 canonical_alias: None,
399 create: None,
400 dm_targets: Default::default(),
401 encryption: None,
402 guest_access: None,
403 history_visibility: None,
404 join_rules: None,
405 max_power_level: 100,
406 name: None,
407 tombstone: None,
408 topic: None,
409 rtc_member_events: BTreeMap::new(),
410 is_marked_unread: false,
411 is_marked_unread_source: AccountDataSource::Unstable,
412 notable_tags: RoomNotableTags::empty(),
413 pinned_events: None,
414 }
415 }
416}
417
418#[derive(Clone, Debug, Serialize, Deserialize)]
422pub struct RoomInfo {
423 #[serde(default, alias = "version")]
426 pub(crate) data_format_version: u8,
427
428 pub(crate) room_id: OwnedRoomId,
430
431 pub(crate) room_state: RoomState,
433
434 pub(crate) notification_counts: UnreadNotificationsCount,
439
440 pub(crate) summary: RoomSummary,
442
443 pub(crate) members_synced: bool,
445
446 pub(crate) last_prev_batch: Option<String>,
448
449 pub(crate) sync_info: SyncInfo,
451
452 pub(crate) encryption_state_synced: bool,
454
455 pub(crate) latest_event: Option<Box<LatestEvent>>,
459
460 #[serde(default)]
464 pub(crate) new_latest_event: LatestEventValue,
465
466 #[serde(default)]
468 pub(crate) read_receipts: RoomReadReceipts,
469
470 pub(crate) base_info: Box<BaseRoomInfo>,
473
474 #[serde(skip)]
478 pub(crate) warned_about_unknown_room_version_rules: Arc<AtomicBool>,
479
480 #[serde(default, skip_serializing_if = "Option::is_none")]
485 pub(crate) cached_display_name: Option<RoomDisplayName>,
486
487 #[serde(default, skip_serializing_if = "Option::is_none")]
489 pub(crate) cached_user_defined_notification_mode: Option<RoomNotificationMode>,
490
491 #[serde(default)]
498 pub(crate) recency_stamp: Option<u64>,
499
500 #[serde(default, skip_serializing_if = "Option::is_none")]
506 pub(crate) invite_acceptance_details: Option<InviteAcceptanceDetails>,
507}
508
509impl RoomInfo {
510 #[doc(hidden)] pub fn new(room_id: &RoomId, room_state: RoomState) -> Self {
512 Self {
513 data_format_version: 1,
514 room_id: room_id.into(),
515 room_state,
516 notification_counts: Default::default(),
517 summary: Default::default(),
518 members_synced: false,
519 last_prev_batch: None,
520 sync_info: SyncInfo::NoState,
521 encryption_state_synced: false,
522 latest_event: None,
523 new_latest_event: LatestEventValue::default(),
524 read_receipts: Default::default(),
525 base_info: Box::new(BaseRoomInfo::new()),
526 warned_about_unknown_room_version_rules: Arc::new(false.into()),
527 cached_display_name: None,
528 cached_user_defined_notification_mode: None,
529 recency_stamp: None,
530 invite_acceptance_details: None,
531 }
532 }
533
534 pub fn mark_as_joined(&mut self) {
536 self.set_state(RoomState::Joined);
537 }
538
539 pub fn mark_as_left(&mut self) {
541 self.set_state(RoomState::Left);
542 }
543
544 pub fn mark_as_invited(&mut self) {
546 self.set_state(RoomState::Invited);
547 }
548
549 pub fn mark_as_knocked(&mut self) {
551 self.set_state(RoomState::Knocked);
552 }
553
554 pub fn mark_as_banned(&mut self) {
556 self.set_state(RoomState::Banned);
557 }
558
559 pub fn set_state(&mut self, room_state: RoomState) {
561 if self.state() != RoomState::Joined && self.invite_acceptance_details.is_some() {
562 error!(room_id = %self.room_id, "The RoomInfo contains invite acceptance details but the room is not in the joined state");
563 }
564 self.invite_acceptance_details = None;
567 self.room_state = room_state;
568 }
569
570 pub fn mark_members_synced(&mut self) {
572 self.members_synced = true;
573 }
574
575 pub fn mark_members_missing(&mut self) {
577 self.members_synced = false;
578 }
579
580 pub fn are_members_synced(&self) -> bool {
582 self.members_synced
583 }
584
585 pub fn mark_state_partially_synced(&mut self) {
587 self.sync_info = SyncInfo::PartiallySynced;
588 }
589
590 pub fn mark_state_fully_synced(&mut self) {
592 self.sync_info = SyncInfo::FullySynced;
593 }
594
595 pub fn mark_state_not_synced(&mut self) {
597 self.sync_info = SyncInfo::NoState;
598 }
599
600 pub fn mark_encryption_state_synced(&mut self) {
602 self.encryption_state_synced = true;
603 }
604
605 pub fn mark_encryption_state_missing(&mut self) {
607 self.encryption_state_synced = false;
608 }
609
610 pub fn set_prev_batch(&mut self, prev_batch: Option<&str>) -> bool {
614 if self.last_prev_batch.as_deref() != prev_batch {
615 self.last_prev_batch = prev_batch.map(|p| p.to_owned());
616 true
617 } else {
618 false
619 }
620 }
621
622 pub fn state(&self) -> RoomState {
624 self.room_state
625 }
626
627 #[cfg(not(feature = "experimental-encrypted-state-events"))]
629 pub fn encryption_state(&self) -> EncryptionState {
630 if !self.encryption_state_synced {
631 EncryptionState::Unknown
632 } else if self.base_info.encryption.is_some() {
633 EncryptionState::Encrypted
634 } else {
635 EncryptionState::NotEncrypted
636 }
637 }
638
639 #[cfg(feature = "experimental-encrypted-state-events")]
641 pub fn encryption_state(&self) -> EncryptionState {
642 if !self.encryption_state_synced {
643 EncryptionState::Unknown
644 } else {
645 self.base_info
646 .encryption
647 .as_ref()
648 .map(|state| {
649 if state.encrypt_state_events {
650 EncryptionState::StateEncrypted
651 } else {
652 EncryptionState::Encrypted
653 }
654 })
655 .unwrap_or(EncryptionState::NotEncrypted)
656 }
657 }
658
659 pub fn set_encryption_event(&mut self, event: Option<RoomEncryptionEventContent>) {
661 self.base_info.encryption = event;
662 }
663
664 pub fn handle_encryption_state(
666 &mut self,
667 requested_required_states: &[(StateEventType, String)],
668 ) {
669 if requested_required_states
670 .iter()
671 .any(|(state_event, _)| state_event == &StateEventType::RoomEncryption)
672 {
673 self.mark_encryption_state_synced();
679 }
680 }
681
682 pub fn handle_state_event(&mut self, event: &AnySyncStateEvent) -> bool {
686 let base_info_has_been_modified = self.base_info.handle_state_event(event);
688
689 if let AnySyncStateEvent::RoomEncryption(_) = event {
690 self.mark_encryption_state_synced();
696 }
697
698 base_info_has_been_modified
699 }
700
701 pub fn handle_stripped_state_event(&mut self, event: &AnyStrippedStateEvent) -> bool {
705 self.base_info.handle_stripped_state_event(event)
706 }
707
708 #[instrument(skip_all, fields(redacts))]
710 pub fn handle_redaction(
711 &mut self,
712 event: &SyncRoomRedactionEvent,
713 _raw: &Raw<SyncRoomRedactionEvent>,
714 ) {
715 let redaction_rules = self.room_version_rules_or_default().redaction;
716
717 let Some(redacts) = event.redacts(&redaction_rules) else {
718 info!("Can't apply redaction, redacts field is missing");
719 return;
720 };
721 tracing::Span::current().record("redacts", debug(redacts));
722
723 if let Some(latest_event) = &mut self.latest_event {
724 tracing::trace!("Checking if redaction applies to latest event");
725 if latest_event.event_id().as_deref() == Some(redacts) {
726 match apply_redaction(latest_event.event().raw(), _raw, &redaction_rules) {
727 Some(redacted) => {
728 latest_event.event_mut().kind =
731 TimelineEventKind::PlainText { event: redacted };
732 debug!("Redacted latest event");
733 }
734 None => {
735 self.latest_event = None;
736 debug!("Removed latest event");
737 }
738 }
739 }
740 }
741
742 self.base_info.handle_redaction(redacts);
743 }
744
745 pub fn avatar_url(&self) -> Option<&MxcUri> {
747 self.base_info
748 .avatar
749 .as_ref()
750 .and_then(|e| e.as_original().and_then(|e| e.content.url.as_deref()))
751 }
752
753 pub fn update_avatar(&mut self, url: Option<OwnedMxcUri>) {
755 self.base_info.avatar = url.map(|url| {
756 let mut content = RoomAvatarEventContent::new();
757 content.url = Some(url);
758
759 MinimalStateEvent::Original(OriginalMinimalStateEvent { content, event_id: None })
760 });
761 }
762
763 pub fn avatar_info(&self) -> Option<&avatar::ImageInfo> {
765 self.base_info
766 .avatar
767 .as_ref()
768 .and_then(|e| e.as_original().and_then(|e| e.content.info.as_deref()))
769 }
770
771 pub fn update_notification_count(&mut self, notification_counts: UnreadNotificationsCount) {
773 self.notification_counts = notification_counts;
774 }
775
776 pub fn update_from_ruma_summary(&mut self, summary: &RumaSummary) -> bool {
780 let mut changed = false;
781
782 if !summary.is_empty() {
783 if !summary.heroes.is_empty() {
784 self.summary.room_heroes = summary
785 .heroes
786 .iter()
787 .map(|hero_id| RoomHero {
788 user_id: hero_id.to_owned(),
789 display_name: None,
790 avatar_url: None,
791 })
792 .collect();
793
794 changed = true;
795 }
796
797 if let Some(joined) = summary.joined_member_count {
798 self.summary.joined_member_count = joined.into();
799 changed = true;
800 }
801
802 if let Some(invited) = summary.invited_member_count {
803 self.summary.invited_member_count = invited.into();
804 changed = true;
805 }
806 }
807
808 changed
809 }
810
811 pub(crate) fn update_joined_member_count(&mut self, count: u64) {
813 self.summary.joined_member_count = count;
814 }
815
816 pub(crate) fn update_invited_member_count(&mut self, count: u64) {
818 self.summary.invited_member_count = count;
819 }
820
821 pub(crate) fn set_invite_acceptance_details(&mut self, details: InviteAcceptanceDetails) {
822 self.invite_acceptance_details = Some(details);
823 }
824
825 pub fn invite_acceptance_details(&self) -> Option<InviteAcceptanceDetails> {
832 self.invite_acceptance_details.clone()
833 }
834
835 pub(crate) fn update_heroes(&mut self, heroes: Vec<RoomHero>) {
837 self.summary.room_heroes = heroes;
838 }
839
840 pub fn heroes(&self) -> &[RoomHero] {
842 &self.summary.room_heroes
843 }
844
845 pub fn active_members_count(&self) -> u64 {
849 self.summary.joined_member_count.saturating_add(self.summary.invited_member_count)
850 }
851
852 pub fn invited_members_count(&self) -> u64 {
854 self.summary.invited_member_count
855 }
856
857 pub fn joined_members_count(&self) -> u64 {
859 self.summary.joined_member_count
860 }
861
862 pub fn canonical_alias(&self) -> Option<&RoomAliasId> {
864 self.base_info.canonical_alias.as_ref()?.as_original()?.content.alias.as_deref()
865 }
866
867 pub fn alt_aliases(&self) -> &[OwnedRoomAliasId] {
869 self.base_info
870 .canonical_alias
871 .as_ref()
872 .and_then(|ev| ev.as_original())
873 .map(|ev| ev.content.alt_aliases.as_ref())
874 .unwrap_or_default()
875 }
876
877 pub fn room_id(&self) -> &RoomId {
879 &self.room_id
880 }
881
882 pub fn room_version(&self) -> Option<&RoomVersionId> {
884 self.base_info.room_version()
885 }
886
887 pub fn room_version_rules_or_default(&self) -> RoomVersionRules {
892 use std::sync::atomic::Ordering;
893
894 self.base_info.room_version().and_then(|room_version| room_version.rules()).unwrap_or_else(
895 || {
896 if self
897 .warned_about_unknown_room_version_rules
898 .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
899 .is_ok()
900 {
901 warn!("Unable to get the room version rules, defaulting to rules for room version {ROOM_VERSION_FALLBACK}");
902 }
903
904 ROOM_VERSION_RULES_FALLBACK
905 },
906 )
907 }
908
909 pub fn room_type(&self) -> Option<&RoomType> {
911 match self.base_info.create.as_ref()? {
912 MinimalStateEvent::Original(ev) => ev.content.room_type.as_ref(),
913 MinimalStateEvent::Redacted(ev) => ev.content.room_type.as_ref(),
914 }
915 }
916
917 pub fn creators(&self) -> Option<Vec<OwnedUserId>> {
919 match self.base_info.create.as_ref()? {
920 MinimalStateEvent::Original(ev) => Some(ev.content.creators()),
921 MinimalStateEvent::Redacted(ev) => Some(ev.content.creators()),
922 }
923 }
924
925 pub(super) fn guest_access(&self) -> &GuestAccess {
926 match &self.base_info.guest_access {
927 Some(MinimalStateEvent::Original(ev)) => &ev.content.guest_access,
928 _ => &GuestAccess::Forbidden,
929 }
930 }
931
932 pub fn history_visibility(&self) -> Option<&HistoryVisibility> {
936 match &self.base_info.history_visibility {
937 Some(MinimalStateEvent::Original(ev)) => Some(&ev.content.history_visibility),
938 _ => None,
939 }
940 }
941
942 pub fn history_visibility_or_default(&self) -> &HistoryVisibility {
949 match &self.base_info.history_visibility {
950 Some(MinimalStateEvent::Original(ev)) => &ev.content.history_visibility,
951 _ => &HistoryVisibility::Shared,
952 }
953 }
954
955 pub fn join_rule(&self) -> Option<&JoinRule> {
958 match &self.base_info.join_rules {
959 Some(MinimalStateEvent::Original(ev)) => Some(&ev.content.join_rule),
960 _ => None,
961 }
962 }
963
964 pub fn name(&self) -> Option<&str> {
966 let name = &self.base_info.name.as_ref()?.as_original()?.content.name;
967 (!name.is_empty()).then_some(name)
968 }
969
970 pub fn create(&self) -> Option<&RoomCreateWithCreatorEventContent> {
972 Some(&self.base_info.create.as_ref()?.as_original()?.content)
973 }
974
975 pub fn tombstone(&self) -> Option<&RoomTombstoneEventContent> {
977 Some(&self.base_info.tombstone.as_ref()?.as_original()?.content)
978 }
979
980 pub fn topic(&self) -> Option<&str> {
982 Some(&self.base_info.topic.as_ref()?.as_original()?.content.topic)
983 }
984
985 fn active_matrix_rtc_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
990 let mut v = self
991 .base_info
992 .rtc_member_events
993 .iter()
994 .filter_map(|(user_id, ev)| {
995 ev.as_original().map(|ev| {
996 ev.content
997 .active_memberships(None)
998 .into_iter()
999 .map(move |m| (user_id.clone(), m))
1000 })
1001 })
1002 .flatten()
1003 .collect::<Vec<_>>();
1004 v.sort_by_key(|(_, m)| m.created_ts());
1005 v
1006 }
1007
1008 fn active_room_call_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
1014 self.active_matrix_rtc_memberships()
1015 .into_iter()
1016 .filter(|(_user_id, m)| m.is_room_call())
1017 .collect()
1018 }
1019
1020 pub fn has_active_room_call(&self) -> bool {
1023 !self.active_room_call_memberships().is_empty()
1024 }
1025
1026 pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
1035 self.active_room_call_memberships()
1036 .iter()
1037 .map(|(call_member_state_key, _)| call_member_state_key.user_id().to_owned())
1038 .collect()
1039 }
1040
1041 pub fn latest_event(&self) -> Option<&LatestEvent> {
1043 self.latest_event.as_deref()
1044 }
1045
1046 pub fn set_new_latest_event(&mut self, new_value: LatestEventValue) {
1048 self.new_latest_event = new_value;
1049 }
1050
1051 pub(crate) fn update_recency_stamp(&mut self, stamp: u64) {
1055 self.recency_stamp = Some(stamp);
1056 }
1057
1058 pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
1060 self.base_info.pinned_events.clone().map(|c| c.pinned)
1061 }
1062
1063 pub fn is_pinned_event(&self, event_id: &EventId) -> bool {
1069 self.base_info
1070 .pinned_events
1071 .as_ref()
1072 .map(|p| p.pinned.contains(&event_id.to_owned()))
1073 .unwrap_or_default()
1074 }
1075
1076 #[instrument(skip_all, fields(room_id = ?self.room_id))]
1084 pub(crate) async fn apply_migrations(&mut self, store: Arc<DynStateStore>) -> bool {
1085 let mut migrated = false;
1086
1087 if self.data_format_version < 1 {
1088 info!("Migrating room info to version 1");
1089
1090 match store.get_room_account_data_event_static::<TagEventContent>(&self.room_id).await {
1092 Ok(Some(raw_event)) => match raw_event.deserialize() {
1094 Ok(event) => {
1095 self.base_info.handle_notable_tags(&event.content.tags);
1096 }
1097 Err(error) => {
1098 warn!("Failed to deserialize room tags: {error}");
1099 }
1100 },
1101 Ok(_) => {
1102 }
1104 Err(error) => {
1105 warn!("Failed to load room tags: {error}");
1106 }
1107 }
1108
1109 match store.get_state_event_static::<RoomPinnedEventsEventContent>(&self.room_id).await
1111 {
1112 Ok(Some(RawSyncOrStrippedState::Sync(raw_event))) => {
1114 match raw_event.deserialize() {
1115 Ok(event) => {
1116 self.handle_state_event(&event.into());
1117 }
1118 Err(error) => {
1119 warn!("Failed to deserialize room pinned events: {error}");
1120 }
1121 }
1122 }
1123 Ok(_) => {
1124 }
1126 Err(error) => {
1127 warn!("Failed to load room pinned events: {error}");
1128 }
1129 }
1130
1131 self.data_format_version = 1;
1132 migrated = true;
1133 }
1134
1135 migrated
1136 }
1137}
1138
1139#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1140pub(crate) enum SyncInfo {
1141 NoState,
1147
1148 PartiallySynced,
1151
1152 FullySynced,
1154}
1155
1156pub fn apply_redaction(
1159 event: &Raw<AnySyncTimelineEvent>,
1160 raw_redaction: &Raw<SyncRoomRedactionEvent>,
1161 rules: &RedactionRules,
1162) -> Option<Raw<AnySyncTimelineEvent>> {
1163 use ruma::canonical_json::{RedactedBecause, redact_in_place};
1164
1165 let mut event_json = match event.deserialize_as() {
1166 Ok(json) => json,
1167 Err(e) => {
1168 warn!("Failed to deserialize latest event: {e}");
1169 return None;
1170 }
1171 };
1172
1173 let redacted_because = match RedactedBecause::from_raw_event(raw_redaction) {
1174 Ok(rb) => rb,
1175 Err(e) => {
1176 warn!("Redaction event is not valid canonical JSON: {e}");
1177 return None;
1178 }
1179 };
1180
1181 let redact_result = redact_in_place(&mut event_json, rules, Some(redacted_because));
1182
1183 if let Err(e) = redact_result {
1184 warn!("Failed to redact event: {e}");
1185 return None;
1186 }
1187
1188 let raw = Raw::new(&event_json).expect("CanonicalJsonObject must be serializable");
1189 Some(raw.cast_unchecked())
1190}
1191
1192#[derive(Debug, Clone)]
1202pub struct RoomInfoNotableUpdate {
1203 pub room_id: OwnedRoomId,
1205
1206 pub reasons: RoomInfoNotableUpdateReasons,
1208}
1209
1210bitflags! {
1211 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1213 pub struct RoomInfoNotableUpdateReasons: u8 {
1214 const RECENCY_STAMP = 0b0000_0001;
1216
1217 const LATEST_EVENT = 0b0000_0010;
1219
1220 const READ_RECEIPT = 0b0000_0100;
1222
1223 const UNREAD_MARKER = 0b0000_1000;
1225
1226 const MEMBERSHIP = 0b0001_0000;
1228
1229 const DISPLAY_NAME = 0b0010_0000;
1231
1232 const NONE = 0b1000_0000;
1243 }
1244}
1245
1246impl Default for RoomInfoNotableUpdateReasons {
1247 fn default() -> Self {
1248 Self::empty()
1249 }
1250}
1251
1252#[cfg(test)]
1253mod tests {
1254 use std::sync::Arc;
1255
1256 use assert_matches::assert_matches;
1257 use matrix_sdk_common::deserialized_responses::TimelineEvent;
1258 use matrix_sdk_test::{
1259 async_test,
1260 test_json::{TAG, sync_events::PINNED_EVENTS},
1261 };
1262 use ruma::{
1263 assign, events::room::pinned_events::RoomPinnedEventsEventContent, owned_event_id,
1264 owned_mxc_uri, owned_user_id, room_id, serde::Raw,
1265 };
1266 use serde_json::json;
1267 use similar_asserts::assert_eq;
1268
1269 use super::{BaseRoomInfo, LatestEventValue, RoomInfo, SyncInfo};
1270 use crate::{
1271 RoomDisplayName, RoomHero, RoomState, StateChanges,
1272 latest_event::LatestEvent,
1273 notification_settings::RoomNotificationMode,
1274 room::{RoomNotableTags, RoomSummary},
1275 store::{IntoStateStore, MemoryStore},
1276 sync::UnreadNotificationsCount,
1277 };
1278
1279 #[test]
1280 fn test_room_info_serialization() {
1281 let info = RoomInfo {
1285 data_format_version: 1,
1286 room_id: room_id!("!gda78o:server.tld").into(),
1287 room_state: RoomState::Invited,
1288 notification_counts: UnreadNotificationsCount {
1289 highlight_count: 1,
1290 notification_count: 2,
1291 },
1292 summary: RoomSummary {
1293 room_heroes: vec![RoomHero {
1294 user_id: owned_user_id!("@somebody:example.org"),
1295 display_name: None,
1296 avatar_url: None,
1297 }],
1298 joined_member_count: 5,
1299 invited_member_count: 0,
1300 },
1301 members_synced: true,
1302 last_prev_batch: Some("pb".to_owned()),
1303 sync_info: SyncInfo::FullySynced,
1304 encryption_state_synced: true,
1305 latest_event: Some(Box::new(LatestEvent::new(TimelineEvent::from_plaintext(
1306 Raw::from_json_string(json!({"sender": "@u:i.uk"}).to_string()).unwrap(),
1307 )))),
1308 new_latest_event: LatestEventValue::None,
1309 base_info: Box::new(
1310 assign!(BaseRoomInfo::new(), { pinned_events: Some(RoomPinnedEventsEventContent::new(vec![owned_event_id!("$a")])) }),
1311 ),
1312 read_receipts: Default::default(),
1313 warned_about_unknown_room_version_rules: Arc::new(false.into()),
1314 cached_display_name: None,
1315 cached_user_defined_notification_mode: None,
1316 recency_stamp: Some(42),
1317 invite_acceptance_details: None,
1318 };
1319
1320 let info_json = json!({
1321 "data_format_version": 1,
1322 "room_id": "!gda78o:server.tld",
1323 "room_state": "Invited",
1324 "notification_counts": {
1325 "highlight_count": 1,
1326 "notification_count": 2,
1327 },
1328 "summary": {
1329 "room_heroes": [{
1330 "user_id": "@somebody:example.org",
1331 "display_name": null,
1332 "avatar_url": null
1333 }],
1334 "joined_member_count": 5,
1335 "invited_member_count": 0,
1336 },
1337 "members_synced": true,
1338 "last_prev_batch": "pb",
1339 "sync_info": "FullySynced",
1340 "encryption_state_synced": true,
1341 "latest_event": {
1342 "event": {
1343 "kind": {"PlainText": {"event": {"sender": "@u:i.uk"}}},
1344 "thread_summary": "None"
1345 },
1346 },
1347 "new_latest_event": "None",
1348 "base_info": {
1349 "avatar": null,
1350 "canonical_alias": null,
1351 "create": null,
1352 "dm_targets": [],
1353 "encryption": null,
1354 "guest_access": null,
1355 "history_visibility": null,
1356 "is_marked_unread": false,
1357 "is_marked_unread_source": "Unstable",
1358 "join_rules": null,
1359 "max_power_level": 100,
1360 "name": null,
1361 "tombstone": null,
1362 "topic": null,
1363 "pinned_events": {
1364 "pinned": ["$a"]
1365 },
1366 },
1367 "read_receipts": {
1368 "num_unread": 0,
1369 "num_mentions": 0,
1370 "num_notifications": 0,
1371 "latest_active": null,
1372 "pending": [],
1373 },
1374 "recency_stamp": 42,
1375 });
1376
1377 assert_eq!(serde_json::to_value(info).unwrap(), info_json);
1378 }
1379
1380 #[async_test]
1381 async fn test_room_info_migration_v1() {
1382 let store = MemoryStore::new().into_state_store();
1383
1384 let room_info_json = json!({
1385 "room_id": "!gda78o:server.tld",
1386 "room_state": "Joined",
1387 "notification_counts": {
1388 "highlight_count": 1,
1389 "notification_count": 2,
1390 },
1391 "summary": {
1392 "room_heroes": [{
1393 "user_id": "@somebody:example.org",
1394 "display_name": null,
1395 "avatar_url": null
1396 }],
1397 "joined_member_count": 5,
1398 "invited_member_count": 0,
1399 },
1400 "members_synced": true,
1401 "last_prev_batch": "pb",
1402 "sync_info": "FullySynced",
1403 "encryption_state_synced": true,
1404 "latest_event": {
1405 "event": {
1406 "encryption_info": null,
1407 "event": {
1408 "sender": "@u:i.uk",
1409 },
1410 },
1411 },
1412 "base_info": {
1413 "avatar": null,
1414 "canonical_alias": null,
1415 "create": null,
1416 "dm_targets": [],
1417 "encryption": null,
1418 "guest_access": null,
1419 "history_visibility": null,
1420 "join_rules": null,
1421 "max_power_level": 100,
1422 "name": null,
1423 "tombstone": null,
1424 "topic": null,
1425 },
1426 "read_receipts": {
1427 "num_unread": 0,
1428 "num_mentions": 0,
1429 "num_notifications": 0,
1430 "latest_active": null,
1431 "pending": []
1432 },
1433 "recency_stamp": 42,
1434 });
1435 let mut room_info: RoomInfo = serde_json::from_value(room_info_json).unwrap();
1436
1437 assert_eq!(room_info.data_format_version, 0);
1438 assert!(room_info.base_info.notable_tags.is_empty());
1439 assert!(room_info.base_info.pinned_events.is_none());
1440
1441 assert!(room_info.apply_migrations(store.clone()).await);
1443
1444 assert_eq!(room_info.data_format_version, 1);
1445 assert!(room_info.base_info.notable_tags.is_empty());
1446 assert!(room_info.base_info.pinned_events.is_none());
1447
1448 assert!(!room_info.apply_migrations(store.clone()).await);
1450
1451 assert_eq!(room_info.data_format_version, 1);
1452 assert!(room_info.base_info.notable_tags.is_empty());
1453 assert!(room_info.base_info.pinned_events.is_none());
1454
1455 let mut changes = StateChanges::default();
1457
1458 let raw_tag_event = Raw::new(&*TAG).unwrap().cast_unchecked();
1459 let tag_event = raw_tag_event.deserialize().unwrap();
1460 changes.add_room_account_data(&room_info.room_id, tag_event, raw_tag_event);
1461
1462 let raw_pinned_events_event = Raw::new(&*PINNED_EVENTS).unwrap().cast_unchecked();
1463 let pinned_events_event = raw_pinned_events_event.deserialize().unwrap();
1464 changes.add_state_event(&room_info.room_id, pinned_events_event, raw_pinned_events_event);
1465
1466 store.save_changes(&changes).await.unwrap();
1467
1468 room_info.data_format_version = 0;
1470 assert!(room_info.apply_migrations(store.clone()).await);
1471
1472 assert_eq!(room_info.data_format_version, 1);
1473 assert!(room_info.base_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
1474 assert!(room_info.base_info.pinned_events.is_some());
1475
1476 let new_room_info = RoomInfo::new(room_id!("!new_room:localhost"), RoomState::Joined);
1478 assert_eq!(new_room_info.data_format_version, 1);
1479 }
1480
1481 #[test]
1482 fn test_room_info_deserialization() {
1483 let info_json = json!({
1484 "room_id": "!gda78o:server.tld",
1485 "room_state": "Joined",
1486 "notification_counts": {
1487 "highlight_count": 1,
1488 "notification_count": 2,
1489 },
1490 "summary": {
1491 "room_heroes": [{
1492 "user_id": "@somebody:example.org",
1493 "display_name": "Somebody",
1494 "avatar_url": "mxc://example.org/abc"
1495 }],
1496 "joined_member_count": 5,
1497 "invited_member_count": 0,
1498 },
1499 "members_synced": true,
1500 "last_prev_batch": "pb",
1501 "sync_info": "FullySynced",
1502 "encryption_state_synced": true,
1503 "base_info": {
1504 "avatar": null,
1505 "canonical_alias": null,
1506 "create": null,
1507 "dm_targets": [],
1508 "encryption": null,
1509 "guest_access": null,
1510 "history_visibility": null,
1511 "join_rules": null,
1512 "max_power_level": 100,
1513 "name": null,
1514 "tombstone": null,
1515 "topic": null,
1516 },
1517 "cached_display_name": { "Calculated": "lol" },
1518 "cached_user_defined_notification_mode": "Mute",
1519 "recency_stamp": 42,
1520 });
1521
1522 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1523
1524 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1525 assert_eq!(info.room_state, RoomState::Joined);
1526 assert_eq!(info.notification_counts.highlight_count, 1);
1527 assert_eq!(info.notification_counts.notification_count, 2);
1528 assert_eq!(
1529 info.summary.room_heroes,
1530 vec![RoomHero {
1531 user_id: owned_user_id!("@somebody:example.org"),
1532 display_name: Some("Somebody".to_owned()),
1533 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1534 }]
1535 );
1536 assert_eq!(info.summary.joined_member_count, 5);
1537 assert_eq!(info.summary.invited_member_count, 0);
1538 assert!(info.members_synced);
1539 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1540 assert_eq!(info.sync_info, SyncInfo::FullySynced);
1541 assert!(info.encryption_state_synced);
1542 assert!(info.latest_event.is_none());
1543 assert_matches!(info.new_latest_event, LatestEventValue::None);
1544 assert!(info.base_info.avatar.is_none());
1545 assert!(info.base_info.canonical_alias.is_none());
1546 assert!(info.base_info.create.is_none());
1547 assert_eq!(info.base_info.dm_targets.len(), 0);
1548 assert!(info.base_info.encryption.is_none());
1549 assert!(info.base_info.guest_access.is_none());
1550 assert!(info.base_info.history_visibility.is_none());
1551 assert!(info.base_info.join_rules.is_none());
1552 assert_eq!(info.base_info.max_power_level, 100);
1553 assert!(info.base_info.name.is_none());
1554 assert!(info.base_info.tombstone.is_none());
1555 assert!(info.base_info.topic.is_none());
1556
1557 assert_eq!(
1558 info.cached_display_name.as_ref(),
1559 Some(&RoomDisplayName::Calculated("lol".to_owned())),
1560 );
1561 assert_eq!(
1562 info.cached_user_defined_notification_mode.as_ref(),
1563 Some(&RoomNotificationMode::Mute)
1564 );
1565 assert_eq!(info.recency_stamp.as_ref(), Some(&42));
1566 }
1567
1568 #[test]
1575 fn test_room_info_deserialization_without_optional_items() {
1576 let info_json = json!({
1579 "room_id": "!gda78o:server.tld",
1580 "room_state": "Invited",
1581 "notification_counts": {
1582 "highlight_count": 1,
1583 "notification_count": 2,
1584 },
1585 "summary": {
1586 "room_heroes": [{
1587 "user_id": "@somebody:example.org",
1588 "display_name": "Somebody",
1589 "avatar_url": "mxc://example.org/abc"
1590 }],
1591 "joined_member_count": 5,
1592 "invited_member_count": 0,
1593 },
1594 "members_synced": true,
1595 "last_prev_batch": "pb",
1596 "sync_info": "FullySynced",
1597 "encryption_state_synced": true,
1598 "base_info": {
1599 "avatar": null,
1600 "canonical_alias": null,
1601 "create": null,
1602 "dm_targets": [],
1603 "encryption": null,
1604 "guest_access": null,
1605 "history_visibility": null,
1606 "join_rules": null,
1607 "max_power_level": 100,
1608 "name": null,
1609 "tombstone": null,
1610 "topic": null,
1611 },
1612 });
1613
1614 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1615
1616 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1617 assert_eq!(info.room_state, RoomState::Invited);
1618 assert_eq!(info.notification_counts.highlight_count, 1);
1619 assert_eq!(info.notification_counts.notification_count, 2);
1620 assert_eq!(
1621 info.summary.room_heroes,
1622 vec![RoomHero {
1623 user_id: owned_user_id!("@somebody:example.org"),
1624 display_name: Some("Somebody".to_owned()),
1625 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1626 }]
1627 );
1628 assert_eq!(info.summary.joined_member_count, 5);
1629 assert_eq!(info.summary.invited_member_count, 0);
1630 assert!(info.members_synced);
1631 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1632 assert_eq!(info.sync_info, SyncInfo::FullySynced);
1633 assert!(info.encryption_state_synced);
1634 assert!(info.base_info.avatar.is_none());
1635 assert!(info.base_info.canonical_alias.is_none());
1636 assert!(info.base_info.create.is_none());
1637 assert_eq!(info.base_info.dm_targets.len(), 0);
1638 assert!(info.base_info.encryption.is_none());
1639 assert!(info.base_info.guest_access.is_none());
1640 assert!(info.base_info.history_visibility.is_none());
1641 assert!(info.base_info.join_rules.is_none());
1642 assert_eq!(info.base_info.max_power_level, 100);
1643 assert!(info.base_info.name.is_none());
1644 assert!(info.base_info.tombstone.is_none());
1645 assert!(info.base_info.topic.is_none());
1646 }
1647}