1use std::{
16 collections::{BTreeMap, HashSet},
17 sync::{atomic::AtomicBool, Arc},
18};
19
20use bitflags::bitflags;
21use eyeball::Subscriber;
22use matrix_sdk_common::{deserialized_responses::TimelineEventKind, ROOM_VERSION_FALLBACK};
23use ruma::{
24 api::client::sync::sync_events::v3::RoomSummary as RumaSummary,
25 assign,
26 events::{
27 beacon_info::BeaconInfoEventContent,
28 call::member::{CallMemberEventContent, CallMemberStateKey, MembershipData},
29 direct::OwnedDirectUserIdentifier,
30 room::{
31 avatar::{self, RoomAvatarEventContent},
32 canonical_alias::RoomCanonicalAliasEventContent,
33 encryption::RoomEncryptionEventContent,
34 guest_access::{GuestAccess, RoomGuestAccessEventContent},
35 history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
36 join_rules::{JoinRule, RoomJoinRulesEventContent},
37 name::RoomNameEventContent,
38 pinned_events::RoomPinnedEventsEventContent,
39 redaction::SyncRoomRedactionEvent,
40 tombstone::RoomTombstoneEventContent,
41 topic::RoomTopicEventContent,
42 },
43 tag::{TagEventContent, TagName, Tags},
44 AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, RedactContent,
45 RedactedStateEventContent, StateEventType, StaticStateEventContent, SyncStateEvent,
46 },
47 room::RoomType,
48 serde::Raw,
49 EventId, MilliSecondsSinceUnixEpoch, MxcUri, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId,
50 OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, RoomVersionId, UserId,
51};
52use serde::{Deserialize, Serialize};
53use tracing::{debug, error, field::debug, info, instrument, warn};
54
55use super::{
56 AccountDataSource, EncryptionState, Room, RoomCreateWithCreatorEventContent, RoomDisplayName,
57 RoomHero, RoomNotableTags, RoomState, RoomSummary,
58};
59use crate::{
60 deserialized_responses::RawSyncOrStrippedState,
61 latest_event::LatestEvent,
62 notification_settings::RoomNotificationMode,
63 read_receipts::RoomReadReceipts,
64 store::{DynStateStore, StateStoreExt},
65 sync::UnreadNotificationsCount,
66 MinimalStateEvent, OriginalMinimalStateEvent,
67};
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct InviteAcceptanceDetails {
73 pub invite_accepted_at: MilliSecondsSinceUnixEpoch,
76
77 pub inviter: OwnedUserId,
79}
80
81impl Room {
82 pub fn subscribe_info(&self) -> Subscriber<RoomInfo> {
84 self.inner.subscribe()
85 }
86
87 pub fn clone_info(&self) -> RoomInfo {
89 self.inner.get()
90 }
91
92 pub fn set_room_info(
94 &self,
95 room_info: RoomInfo,
96 room_info_notable_update_reasons: RoomInfoNotableUpdateReasons,
97 ) {
98 self.inner.set(room_info);
99
100 if !room_info_notable_update_reasons.is_empty() {
101 let _ = self.room_info_notable_update_sender.send(RoomInfoNotableUpdate {
103 room_id: self.room_id.clone(),
104 reasons: room_info_notable_update_reasons,
105 });
106 } else {
107 let _ = self.room_info_notable_update_sender.send(RoomInfoNotableUpdate {
111 room_id: self.room_id.clone(),
112 reasons: RoomInfoNotableUpdateReasons::NONE,
113 });
114 }
115 }
116}
117
118#[derive(Clone, Debug, Serialize, Deserialize)]
122pub struct BaseRoomInfo {
123 pub(crate) avatar: Option<MinimalStateEvent<RoomAvatarEventContent>>,
125 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
127 pub(crate) beacons: BTreeMap<OwnedUserId, MinimalStateEvent<BeaconInfoEventContent>>,
128 pub(crate) canonical_alias: Option<MinimalStateEvent<RoomCanonicalAliasEventContent>>,
130 pub(crate) create: Option<MinimalStateEvent<RoomCreateWithCreatorEventContent>>,
132 pub(crate) dm_targets: HashSet<OwnedDirectUserIdentifier>,
135 pub(crate) encryption: Option<RoomEncryptionEventContent>,
137 pub(crate) guest_access: Option<MinimalStateEvent<RoomGuestAccessEventContent>>,
139 pub(crate) history_visibility: Option<MinimalStateEvent<RoomHistoryVisibilityEventContent>>,
141 pub(crate) join_rules: Option<MinimalStateEvent<RoomJoinRulesEventContent>>,
143 pub(crate) max_power_level: i64,
145 pub(crate) name: Option<MinimalStateEvent<RoomNameEventContent>>,
147 pub(crate) tombstone: Option<MinimalStateEvent<RoomTombstoneEventContent>>,
149 pub(crate) topic: Option<MinimalStateEvent<RoomTopicEventContent>>,
151 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
154 pub(crate) rtc_member_events:
155 BTreeMap<CallMemberStateKey, MinimalStateEvent<CallMemberEventContent>>,
156 #[serde(default)]
158 pub(crate) is_marked_unread: bool,
159 #[serde(default)]
161 pub(crate) is_marked_unread_source: AccountDataSource,
162 #[serde(skip_serializing_if = "RoomNotableTags::is_empty", default)]
167 pub(crate) notable_tags: RoomNotableTags,
168 pub(crate) pinned_events: Option<RoomPinnedEventsEventContent>,
170}
171
172impl BaseRoomInfo {
173 pub fn new() -> Self {
175 Self::default()
176 }
177
178 pub fn room_version(&self) -> Option<&RoomVersionId> {
183 match self.create.as_ref()? {
184 MinimalStateEvent::Original(ev) => Some(&ev.content.room_version),
185 MinimalStateEvent::Redacted(ev) => Some(&ev.content.room_version),
186 }
187 }
188
189 pub fn handle_state_event(&mut self, ev: &AnySyncStateEvent) -> bool {
193 match ev {
194 AnySyncStateEvent::BeaconInfo(b) => {
195 self.beacons.insert(b.state_key().clone(), b.into());
196 }
197 AnySyncStateEvent::RoomEncryption(SyncStateEvent::Original(encryption)) => {
199 self.encryption = Some(encryption.content.clone());
200 }
201 AnySyncStateEvent::RoomAvatar(a) => {
202 self.avatar = Some(a.into());
203 }
204 AnySyncStateEvent::RoomName(n) => {
205 self.name = Some(n.into());
206 }
207 AnySyncStateEvent::RoomCreate(c) if self.create.is_none() => {
209 self.create = Some(c.into());
210 }
211 AnySyncStateEvent::RoomHistoryVisibility(h) => {
212 self.history_visibility = Some(h.into());
213 }
214 AnySyncStateEvent::RoomGuestAccess(g) => {
215 self.guest_access = Some(g.into());
216 }
217 AnySyncStateEvent::RoomJoinRules(c) => {
218 self.join_rules = Some(c.into());
219 }
220 AnySyncStateEvent::RoomCanonicalAlias(a) => {
221 self.canonical_alias = Some(a.into());
222 }
223 AnySyncStateEvent::RoomTopic(t) => {
224 self.topic = Some(t.into());
225 }
226 AnySyncStateEvent::RoomTombstone(t) => {
227 self.tombstone = Some(t.into());
228 }
229 AnySyncStateEvent::RoomPowerLevels(p) => {
230 self.max_power_level = p.power_levels().max().into();
231 }
232 AnySyncStateEvent::CallMember(m) => {
233 let Some(o_ev) = m.as_original() else {
234 return false;
235 };
236
237 let mut o_ev = o_ev.clone();
240 o_ev.content.set_created_ts_if_none(o_ev.origin_server_ts);
241
242 self.rtc_member_events
244 .insert(m.state_key().clone(), SyncStateEvent::Original(o_ev).into());
245
246 self.rtc_member_events.retain(|_, ev| {
248 ev.as_original().is_some_and(|o| !o.content.active_memberships(None).is_empty())
249 });
250 }
251 AnySyncStateEvent::RoomPinnedEvents(p) => {
252 self.pinned_events = p.as_original().map(|p| p.content.clone());
253 }
254 _ => return false,
255 }
256
257 true
258 }
259
260 pub fn handle_stripped_state_event(&mut self, ev: &AnyStrippedStateEvent) -> bool {
265 match ev {
266 AnyStrippedStateEvent::RoomEncryption(encryption) => {
267 if let Some(algorithm) = &encryption.content.algorithm {
268 let content = assign!(RoomEncryptionEventContent::new(algorithm.clone()), {
269 rotation_period_ms: encryption.content.rotation_period_ms,
270 rotation_period_msgs: encryption.content.rotation_period_msgs,
271 });
272 self.encryption = Some(content);
273 }
274 }
278 AnyStrippedStateEvent::RoomAvatar(a) => {
279 self.avatar = Some(a.into());
280 }
281 AnyStrippedStateEvent::RoomName(n) => {
282 self.name = Some(n.into());
283 }
284 AnyStrippedStateEvent::RoomCreate(c) if self.create.is_none() => {
285 self.create = Some(c.into());
286 }
287 AnyStrippedStateEvent::RoomHistoryVisibility(h) => {
288 self.history_visibility = Some(h.into());
289 }
290 AnyStrippedStateEvent::RoomGuestAccess(g) => {
291 self.guest_access = Some(g.into());
292 }
293 AnyStrippedStateEvent::RoomJoinRules(c) => {
294 self.join_rules = Some(c.into());
295 }
296 AnyStrippedStateEvent::RoomCanonicalAlias(a) => {
297 self.canonical_alias = Some(a.into());
298 }
299 AnyStrippedStateEvent::RoomTopic(t) => {
300 self.topic = Some(t.into());
301 }
302 AnyStrippedStateEvent::RoomTombstone(t) => {
303 self.tombstone = Some(t.into());
304 }
305 AnyStrippedStateEvent::RoomPowerLevels(p) => {
306 self.max_power_level = p.power_levels().max().into();
307 }
308 AnyStrippedStateEvent::CallMember(_) => {
309 return false;
312 }
313 AnyStrippedStateEvent::RoomPinnedEvents(p) => {
314 if let Some(pinned) = p.content.pinned.clone() {
315 self.pinned_events = Some(RoomPinnedEventsEventContent::new(pinned));
316 }
317 }
318 _ => return false,
319 }
320
321 true
322 }
323
324 pub(super) fn handle_redaction(&mut self, redacts: &EventId) {
325 let room_version = self.room_version().unwrap_or(&ROOM_VERSION_FALLBACK).to_owned();
326
327 if self.avatar.has_event_id(redacts) {
329 self.avatar.as_mut().unwrap().redact(&room_version);
330 } else if self.canonical_alias.has_event_id(redacts) {
331 self.canonical_alias.as_mut().unwrap().redact(&room_version);
332 } else if self.create.has_event_id(redacts) {
333 self.create.as_mut().unwrap().redact(&room_version);
334 } else if self.guest_access.has_event_id(redacts) {
335 self.guest_access.as_mut().unwrap().redact(&room_version);
336 } else if self.history_visibility.has_event_id(redacts) {
337 self.history_visibility.as_mut().unwrap().redact(&room_version);
338 } else if self.join_rules.has_event_id(redacts) {
339 self.join_rules.as_mut().unwrap().redact(&room_version);
340 } else if self.name.has_event_id(redacts) {
341 self.name.as_mut().unwrap().redact(&room_version);
342 } else if self.tombstone.has_event_id(redacts) {
343 self.tombstone.as_mut().unwrap().redact(&room_version);
344 } else if self.topic.has_event_id(redacts) {
345 self.topic.as_mut().unwrap().redact(&room_version);
346 } else {
347 self.rtc_member_events
348 .retain(|_, member_event| member_event.event_id() != Some(redacts));
349 }
350 }
351
352 pub fn handle_notable_tags(&mut self, tags: &Tags) {
353 let mut notable_tags = RoomNotableTags::empty();
354
355 if tags.contains_key(&TagName::Favorite) {
356 notable_tags.insert(RoomNotableTags::FAVOURITE);
357 }
358
359 if tags.contains_key(&TagName::LowPriority) {
360 notable_tags.insert(RoomNotableTags::LOW_PRIORITY);
361 }
362
363 self.notable_tags = notable_tags;
364 }
365}
366
367impl Default for BaseRoomInfo {
368 fn default() -> Self {
369 Self {
370 avatar: None,
371 beacons: BTreeMap::new(),
372 canonical_alias: None,
373 create: None,
374 dm_targets: Default::default(),
375 encryption: None,
376 guest_access: None,
377 history_visibility: None,
378 join_rules: None,
379 max_power_level: 100,
380 name: None,
381 tombstone: None,
382 topic: None,
383 rtc_member_events: BTreeMap::new(),
384 is_marked_unread: false,
385 is_marked_unread_source: AccountDataSource::Unstable,
386 notable_tags: RoomNotableTags::empty(),
387 pinned_events: None,
388 }
389 }
390}
391
392trait OptionExt {
393 fn has_event_id(&self, ev_id: &EventId) -> bool;
394}
395
396impl<C> OptionExt for Option<MinimalStateEvent<C>>
397where
398 C: StaticStateEventContent + RedactContent,
399 C::Redacted: RedactedStateEventContent,
400{
401 fn has_event_id(&self, ev_id: &EventId) -> bool {
402 self.as_ref().is_some_and(|ev| ev.event_id() == Some(ev_id))
403 }
404}
405
406#[derive(Clone, Debug, Serialize, Deserialize)]
410pub struct RoomInfo {
411 #[serde(default, alias = "version")]
414 pub(crate) data_format_version: u8,
415
416 pub(crate) room_id: OwnedRoomId,
418
419 pub(crate) room_state: RoomState,
421
422 pub(crate) notification_counts: UnreadNotificationsCount,
427
428 pub(crate) summary: RoomSummary,
430
431 pub(crate) members_synced: bool,
433
434 pub(crate) last_prev_batch: Option<String>,
436
437 pub(crate) sync_info: SyncInfo,
439
440 pub(crate) encryption_state_synced: bool,
442
443 pub(crate) latest_event: Option<Box<LatestEvent>>,
445
446 #[serde(default)]
448 pub(crate) read_receipts: RoomReadReceipts,
449
450 pub(crate) base_info: Box<BaseRoomInfo>,
453
454 #[serde(skip)]
458 pub(crate) warned_about_unknown_room_version: Arc<AtomicBool>,
459
460 #[serde(default, skip_serializing_if = "Option::is_none")]
465 pub(crate) cached_display_name: Option<RoomDisplayName>,
466
467 #[serde(default, skip_serializing_if = "Option::is_none")]
469 pub(crate) cached_user_defined_notification_mode: Option<RoomNotificationMode>,
470
471 #[serde(default)]
478 pub(crate) recency_stamp: Option<u64>,
479
480 #[serde(default, skip_serializing_if = "Option::is_none")]
486 pub(crate) invite_acceptance_details: Option<InviteAcceptanceDetails>,
487}
488
489impl RoomInfo {
490 #[doc(hidden)] pub fn new(room_id: &RoomId, room_state: RoomState) -> Self {
492 Self {
493 data_format_version: 1,
494 room_id: room_id.into(),
495 room_state,
496 notification_counts: Default::default(),
497 summary: Default::default(),
498 members_synced: false,
499 last_prev_batch: None,
500 sync_info: SyncInfo::NoState,
501 encryption_state_synced: false,
502 latest_event: None,
503 read_receipts: Default::default(),
504 base_info: Box::new(BaseRoomInfo::new()),
505 warned_about_unknown_room_version: Arc::new(false.into()),
506 cached_display_name: None,
507 cached_user_defined_notification_mode: None,
508 recency_stamp: None,
509 invite_acceptance_details: None,
510 }
511 }
512
513 pub fn mark_as_joined(&mut self) {
515 self.set_state(RoomState::Joined);
516 }
517
518 pub fn mark_as_left(&mut self) {
520 self.set_state(RoomState::Left);
521 }
522
523 pub fn mark_as_invited(&mut self) {
525 self.set_state(RoomState::Invited);
526 }
527
528 pub fn mark_as_knocked(&mut self) {
530 self.set_state(RoomState::Knocked);
531 }
532
533 pub fn mark_as_banned(&mut self) {
535 self.set_state(RoomState::Banned);
536 }
537
538 pub fn set_state(&mut self, room_state: RoomState) {
540 if self.state() != RoomState::Joined && self.invite_acceptance_details.is_some() {
541 error!(room_id = %self.room_id, "The RoomInfo contains invite acceptance details but the room is not in the joined state");
542 }
543 self.invite_acceptance_details = None;
546 self.room_state = room_state;
547 }
548
549 pub fn mark_members_synced(&mut self) {
551 self.members_synced = true;
552 }
553
554 pub fn mark_members_missing(&mut self) {
556 self.members_synced = false;
557 }
558
559 pub fn are_members_synced(&self) -> bool {
561 self.members_synced
562 }
563
564 pub fn mark_state_partially_synced(&mut self) {
566 self.sync_info = SyncInfo::PartiallySynced;
567 }
568
569 pub fn mark_state_fully_synced(&mut self) {
571 self.sync_info = SyncInfo::FullySynced;
572 }
573
574 pub fn mark_state_not_synced(&mut self) {
576 self.sync_info = SyncInfo::NoState;
577 }
578
579 pub fn mark_encryption_state_synced(&mut self) {
581 self.encryption_state_synced = true;
582 }
583
584 pub fn mark_encryption_state_missing(&mut self) {
586 self.encryption_state_synced = false;
587 }
588
589 pub fn set_prev_batch(&mut self, prev_batch: Option<&str>) -> bool {
593 if self.last_prev_batch.as_deref() != prev_batch {
594 self.last_prev_batch = prev_batch.map(|p| p.to_owned());
595 true
596 } else {
597 false
598 }
599 }
600
601 pub fn state(&self) -> RoomState {
603 self.room_state
604 }
605
606 pub fn encryption_state(&self) -> EncryptionState {
608 if !self.encryption_state_synced {
609 EncryptionState::Unknown
610 } else if self.base_info.encryption.is_some() {
611 EncryptionState::Encrypted
612 } else {
613 EncryptionState::NotEncrypted
614 }
615 }
616
617 pub fn set_encryption_event(&mut self, event: Option<RoomEncryptionEventContent>) {
619 self.base_info.encryption = event;
620 }
621
622 pub fn handle_encryption_state(
624 &mut self,
625 requested_required_states: &[(StateEventType, String)],
626 ) {
627 if requested_required_states
628 .iter()
629 .any(|(state_event, _)| state_event == &StateEventType::RoomEncryption)
630 {
631 self.mark_encryption_state_synced();
637 }
638 }
639
640 pub fn handle_state_event(&mut self, event: &AnySyncStateEvent) -> bool {
644 let base_info_has_been_modified = self.base_info.handle_state_event(event);
646
647 if let AnySyncStateEvent::RoomEncryption(_) = event {
648 self.mark_encryption_state_synced();
654 }
655
656 base_info_has_been_modified
657 }
658
659 pub fn handle_stripped_state_event(&mut self, event: &AnyStrippedStateEvent) -> bool {
663 self.base_info.handle_stripped_state_event(event)
664 }
665
666 #[instrument(skip_all, fields(redacts))]
668 pub fn handle_redaction(
669 &mut self,
670 event: &SyncRoomRedactionEvent,
671 _raw: &Raw<SyncRoomRedactionEvent>,
672 ) {
673 let room_version = self.room_version_or_default();
674
675 let Some(redacts) = event.redacts(&room_version) else {
676 info!("Can't apply redaction, redacts field is missing");
677 return;
678 };
679 tracing::Span::current().record("redacts", debug(redacts));
680
681 if let Some(latest_event) = &mut self.latest_event {
682 tracing::trace!("Checking if redaction applies to latest event");
683 if latest_event.event_id().as_deref() == Some(redacts) {
684 match apply_redaction(latest_event.event().raw(), _raw, &room_version) {
685 Some(redacted) => {
686 latest_event.event_mut().kind =
689 TimelineEventKind::PlainText { event: redacted };
690 debug!("Redacted latest event");
691 }
692 None => {
693 self.latest_event = None;
694 debug!("Removed latest event");
695 }
696 }
697 }
698 }
699
700 self.base_info.handle_redaction(redacts);
701 }
702
703 pub fn avatar_url(&self) -> Option<&MxcUri> {
705 self.base_info
706 .avatar
707 .as_ref()
708 .and_then(|e| e.as_original().and_then(|e| e.content.url.as_deref()))
709 }
710
711 pub fn update_avatar(&mut self, url: Option<OwnedMxcUri>) {
713 self.base_info.avatar = url.map(|url| {
714 let mut content = RoomAvatarEventContent::new();
715 content.url = Some(url);
716
717 MinimalStateEvent::Original(OriginalMinimalStateEvent { content, event_id: None })
718 });
719 }
720
721 pub fn avatar_info(&self) -> Option<&avatar::ImageInfo> {
723 self.base_info
724 .avatar
725 .as_ref()
726 .and_then(|e| e.as_original().and_then(|e| e.content.info.as_deref()))
727 }
728
729 pub fn update_notification_count(&mut self, notification_counts: UnreadNotificationsCount) {
731 self.notification_counts = notification_counts;
732 }
733
734 pub fn update_from_ruma_summary(&mut self, summary: &RumaSummary) -> bool {
738 let mut changed = false;
739
740 if !summary.is_empty() {
741 if !summary.heroes.is_empty() {
742 self.summary.room_heroes = summary
743 .heroes
744 .iter()
745 .map(|hero_id| RoomHero {
746 user_id: hero_id.to_owned(),
747 display_name: None,
748 avatar_url: None,
749 })
750 .collect();
751
752 changed = true;
753 }
754
755 if let Some(joined) = summary.joined_member_count {
756 self.summary.joined_member_count = joined.into();
757 changed = true;
758 }
759
760 if let Some(invited) = summary.invited_member_count {
761 self.summary.invited_member_count = invited.into();
762 changed = true;
763 }
764 }
765
766 changed
767 }
768
769 pub(crate) fn update_joined_member_count(&mut self, count: u64) {
771 self.summary.joined_member_count = count;
772 }
773
774 pub(crate) fn update_invited_member_count(&mut self, count: u64) {
776 self.summary.invited_member_count = count;
777 }
778
779 pub(crate) fn set_invite_acceptance_details(&mut self, details: InviteAcceptanceDetails) {
780 self.invite_acceptance_details = Some(details);
781 }
782
783 pub fn invite_acceptance_details(&self) -> Option<InviteAcceptanceDetails> {
790 self.invite_acceptance_details.clone()
791 }
792
793 pub(crate) fn update_heroes(&mut self, heroes: Vec<RoomHero>) {
795 self.summary.room_heroes = heroes;
796 }
797
798 pub fn heroes(&self) -> &[RoomHero] {
800 &self.summary.room_heroes
801 }
802
803 pub fn active_members_count(&self) -> u64 {
807 self.summary.joined_member_count.saturating_add(self.summary.invited_member_count)
808 }
809
810 pub fn invited_members_count(&self) -> u64 {
812 self.summary.invited_member_count
813 }
814
815 pub fn joined_members_count(&self) -> u64 {
817 self.summary.joined_member_count
818 }
819
820 pub fn canonical_alias(&self) -> Option<&RoomAliasId> {
822 self.base_info.canonical_alias.as_ref()?.as_original()?.content.alias.as_deref()
823 }
824
825 pub fn alt_aliases(&self) -> &[OwnedRoomAliasId] {
827 self.base_info
828 .canonical_alias
829 .as_ref()
830 .and_then(|ev| ev.as_original())
831 .map(|ev| ev.content.alt_aliases.as_ref())
832 .unwrap_or_default()
833 }
834
835 pub fn room_id(&self) -> &RoomId {
837 &self.room_id
838 }
839
840 pub fn room_version(&self) -> Option<&RoomVersionId> {
842 self.base_info.room_version()
843 }
844
845 pub fn room_version_or_default(&self) -> RoomVersionId {
850 use std::sync::atomic::Ordering;
851
852 self.base_info.room_version().cloned().unwrap_or_else(|| {
853 if self
854 .warned_about_unknown_room_version
855 .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
856 .is_ok()
857 {
858 warn!("Unknown room version, falling back to {ROOM_VERSION_FALLBACK}");
859 }
860
861 ROOM_VERSION_FALLBACK
862 })
863 }
864
865 pub fn room_type(&self) -> Option<&RoomType> {
867 match self.base_info.create.as_ref()? {
868 MinimalStateEvent::Original(ev) => ev.content.room_type.as_ref(),
869 MinimalStateEvent::Redacted(ev) => ev.content.room_type.as_ref(),
870 }
871 }
872
873 pub fn creator(&self) -> Option<&UserId> {
875 match self.base_info.create.as_ref()? {
876 MinimalStateEvent::Original(ev) => Some(&ev.content.creator),
877 MinimalStateEvent::Redacted(ev) => Some(&ev.content.creator),
878 }
879 }
880
881 pub(super) fn guest_access(&self) -> &GuestAccess {
882 match &self.base_info.guest_access {
883 Some(MinimalStateEvent::Original(ev)) => &ev.content.guest_access,
884 _ => &GuestAccess::Forbidden,
885 }
886 }
887
888 pub fn history_visibility(&self) -> Option<&HistoryVisibility> {
892 match &self.base_info.history_visibility {
893 Some(MinimalStateEvent::Original(ev)) => Some(&ev.content.history_visibility),
894 _ => None,
895 }
896 }
897
898 pub fn history_visibility_or_default(&self) -> &HistoryVisibility {
905 match &self.base_info.history_visibility {
906 Some(MinimalStateEvent::Original(ev)) => &ev.content.history_visibility,
907 _ => &HistoryVisibility::Shared,
908 }
909 }
910
911 pub fn join_rule(&self) -> Option<&JoinRule> {
914 match &self.base_info.join_rules {
915 Some(MinimalStateEvent::Original(ev)) => Some(&ev.content.join_rule),
916 _ => None,
917 }
918 }
919
920 pub fn name(&self) -> Option<&str> {
922 let name = &self.base_info.name.as_ref()?.as_original()?.content.name;
923 (!name.is_empty()).then_some(name)
924 }
925
926 pub fn create(&self) -> Option<&RoomCreateWithCreatorEventContent> {
928 Some(&self.base_info.create.as_ref()?.as_original()?.content)
929 }
930
931 pub fn tombstone(&self) -> Option<&RoomTombstoneEventContent> {
933 Some(&self.base_info.tombstone.as_ref()?.as_original()?.content)
934 }
935
936 pub fn topic(&self) -> Option<&str> {
938 Some(&self.base_info.topic.as_ref()?.as_original()?.content.topic)
939 }
940
941 fn active_matrix_rtc_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
946 let mut v = self
947 .base_info
948 .rtc_member_events
949 .iter()
950 .filter_map(|(user_id, ev)| {
951 ev.as_original().map(|ev| {
952 ev.content
953 .active_memberships(None)
954 .into_iter()
955 .map(move |m| (user_id.clone(), m))
956 })
957 })
958 .flatten()
959 .collect::<Vec<_>>();
960 v.sort_by_key(|(_, m)| m.created_ts());
961 v
962 }
963
964 fn active_room_call_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
970 self.active_matrix_rtc_memberships()
971 .into_iter()
972 .filter(|(_user_id, m)| m.is_room_call())
973 .collect()
974 }
975
976 pub fn has_active_room_call(&self) -> bool {
979 !self.active_room_call_memberships().is_empty()
980 }
981
982 pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
991 self.active_room_call_memberships()
992 .iter()
993 .map(|(call_member_state_key, _)| call_member_state_key.user_id().to_owned())
994 .collect()
995 }
996
997 pub fn latest_event(&self) -> Option<&LatestEvent> {
999 self.latest_event.as_deref()
1000 }
1001
1002 pub(crate) fn update_recency_stamp(&mut self, stamp: u64) {
1006 self.recency_stamp = Some(stamp);
1007 }
1008
1009 pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
1011 self.base_info.pinned_events.clone().map(|c| c.pinned)
1012 }
1013
1014 pub fn is_pinned_event(&self, event_id: &EventId) -> bool {
1020 self.base_info
1021 .pinned_events
1022 .as_ref()
1023 .map(|p| p.pinned.contains(&event_id.to_owned()))
1024 .unwrap_or_default()
1025 }
1026
1027 #[instrument(skip_all, fields(room_id = ?self.room_id))]
1035 pub(crate) async fn apply_migrations(&mut self, store: Arc<DynStateStore>) -> bool {
1036 let mut migrated = false;
1037
1038 if self.data_format_version < 1 {
1039 info!("Migrating room info to version 1");
1040
1041 match store.get_room_account_data_event_static::<TagEventContent>(&self.room_id).await {
1043 Ok(Some(raw_event)) => match raw_event.deserialize() {
1045 Ok(event) => {
1046 self.base_info.handle_notable_tags(&event.content.tags);
1047 }
1048 Err(error) => {
1049 warn!("Failed to deserialize room tags: {error}");
1050 }
1051 },
1052 Ok(_) => {
1053 }
1055 Err(error) => {
1056 warn!("Failed to load room tags: {error}");
1057 }
1058 }
1059
1060 match store.get_state_event_static::<RoomPinnedEventsEventContent>(&self.room_id).await
1062 {
1063 Ok(Some(RawSyncOrStrippedState::Sync(raw_event))) => {
1065 match raw_event.deserialize() {
1066 Ok(event) => {
1067 self.handle_state_event(&event.into());
1068 }
1069 Err(error) => {
1070 warn!("Failed to deserialize room pinned events: {error}");
1071 }
1072 }
1073 }
1074 Ok(_) => {
1075 }
1077 Err(error) => {
1078 warn!("Failed to load room pinned events: {error}");
1079 }
1080 }
1081
1082 self.data_format_version = 1;
1083 migrated = true;
1084 }
1085
1086 migrated
1087 }
1088}
1089
1090#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1091pub(crate) enum SyncInfo {
1092 NoState,
1098
1099 PartiallySynced,
1102
1103 FullySynced,
1105}
1106
1107pub fn apply_redaction(
1110 event: &Raw<AnySyncTimelineEvent>,
1111 raw_redaction: &Raw<SyncRoomRedactionEvent>,
1112 room_version: &RoomVersionId,
1113) -> Option<Raw<AnySyncTimelineEvent>> {
1114 use ruma::canonical_json::{redact_in_place, RedactedBecause};
1115
1116 let mut event_json = match event.deserialize_as() {
1117 Ok(json) => json,
1118 Err(e) => {
1119 warn!("Failed to deserialize latest event: {e}");
1120 return None;
1121 }
1122 };
1123
1124 let redacted_because = match RedactedBecause::from_raw_event(raw_redaction) {
1125 Ok(rb) => rb,
1126 Err(e) => {
1127 warn!("Redaction event is not valid canonical JSON: {e}");
1128 return None;
1129 }
1130 };
1131
1132 let redact_result = redact_in_place(&mut event_json, room_version, Some(redacted_because));
1133
1134 if let Err(e) = redact_result {
1135 warn!("Failed to redact event: {e}");
1136 return None;
1137 }
1138
1139 let raw = Raw::new(&event_json).expect("CanonicalJsonObject must be serializable");
1140 Some(raw.cast())
1141}
1142
1143#[derive(Debug, Clone)]
1153pub struct RoomInfoNotableUpdate {
1154 pub room_id: OwnedRoomId,
1156
1157 pub reasons: RoomInfoNotableUpdateReasons,
1159}
1160
1161bitflags! {
1162 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1164 pub struct RoomInfoNotableUpdateReasons: u8 {
1165 const RECENCY_STAMP = 0b0000_0001;
1167
1168 const LATEST_EVENT = 0b0000_0010;
1170
1171 const READ_RECEIPT = 0b0000_0100;
1173
1174 const UNREAD_MARKER = 0b0000_1000;
1176
1177 const MEMBERSHIP = 0b0001_0000;
1179
1180 const DISPLAY_NAME = 0b0010_0000;
1182
1183 const NONE = 0b1000_0000;
1194 }
1195}
1196
1197impl Default for RoomInfoNotableUpdateReasons {
1198 fn default() -> Self {
1199 Self::empty()
1200 }
1201}
1202
1203#[cfg(test)]
1204mod tests {
1205 use std::sync::Arc;
1206
1207 use matrix_sdk_common::deserialized_responses::TimelineEvent;
1208 use matrix_sdk_test::{
1209 async_test,
1210 test_json::{sync_events::PINNED_EVENTS, TAG},
1211 };
1212 use ruma::{
1213 assign, events::room::pinned_events::RoomPinnedEventsEventContent, owned_event_id,
1214 owned_mxc_uri, owned_user_id, room_id, serde::Raw,
1215 };
1216 use serde_json::json;
1217 use similar_asserts::assert_eq;
1218
1219 use super::{BaseRoomInfo, RoomInfo, SyncInfo};
1220 use crate::{
1221 latest_event::LatestEvent,
1222 notification_settings::RoomNotificationMode,
1223 room::{RoomNotableTags, RoomSummary},
1224 store::{IntoStateStore, MemoryStore},
1225 sync::UnreadNotificationsCount,
1226 RoomDisplayName, RoomHero, RoomState, StateChanges,
1227 };
1228
1229 #[test]
1230 fn test_room_info_serialization() {
1231 let info = RoomInfo {
1235 data_format_version: 1,
1236 room_id: room_id!("!gda78o:server.tld").into(),
1237 room_state: RoomState::Invited,
1238 notification_counts: UnreadNotificationsCount {
1239 highlight_count: 1,
1240 notification_count: 2,
1241 },
1242 summary: RoomSummary {
1243 room_heroes: vec![RoomHero {
1244 user_id: owned_user_id!("@somebody:example.org"),
1245 display_name: None,
1246 avatar_url: None,
1247 }],
1248 joined_member_count: 5,
1249 invited_member_count: 0,
1250 },
1251 members_synced: true,
1252 last_prev_batch: Some("pb".to_owned()),
1253 sync_info: SyncInfo::FullySynced,
1254 encryption_state_synced: true,
1255 latest_event: Some(Box::new(LatestEvent::new(TimelineEvent::from_plaintext(
1256 Raw::from_json_string(json!({"sender": "@u:i.uk"}).to_string()).unwrap(),
1257 )))),
1258 base_info: Box::new(
1259 assign!(BaseRoomInfo::new(), { pinned_events: Some(RoomPinnedEventsEventContent::new(vec![owned_event_id!("$a")])) }),
1260 ),
1261 read_receipts: Default::default(),
1262 warned_about_unknown_room_version: Arc::new(false.into()),
1263 cached_display_name: None,
1264 cached_user_defined_notification_mode: None,
1265 recency_stamp: Some(42),
1266 invite_acceptance_details: None,
1267 };
1268
1269 let info_json = json!({
1270 "data_format_version": 1,
1271 "room_id": "!gda78o:server.tld",
1272 "room_state": "Invited",
1273 "notification_counts": {
1274 "highlight_count": 1,
1275 "notification_count": 2,
1276 },
1277 "summary": {
1278 "room_heroes": [{
1279 "user_id": "@somebody:example.org",
1280 "display_name": null,
1281 "avatar_url": null
1282 }],
1283 "joined_member_count": 5,
1284 "invited_member_count": 0,
1285 },
1286 "members_synced": true,
1287 "last_prev_batch": "pb",
1288 "sync_info": "FullySynced",
1289 "encryption_state_synced": true,
1290 "latest_event": {
1291 "event": {
1292 "kind": {"PlainText": {"event": {"sender": "@u:i.uk"}}},
1293 "thread_summary": "None"
1294 },
1295 },
1296 "base_info": {
1297 "avatar": null,
1298 "canonical_alias": null,
1299 "create": null,
1300 "dm_targets": [],
1301 "encryption": null,
1302 "guest_access": null,
1303 "history_visibility": null,
1304 "is_marked_unread": false,
1305 "is_marked_unread_source": "Unstable",
1306 "join_rules": null,
1307 "max_power_level": 100,
1308 "name": null,
1309 "tombstone": null,
1310 "topic": null,
1311 "pinned_events": {
1312 "pinned": ["$a"]
1313 },
1314 },
1315 "read_receipts": {
1316 "num_unread": 0,
1317 "num_mentions": 0,
1318 "num_notifications": 0,
1319 "latest_active": null,
1320 "pending": [],
1321 },
1322 "recency_stamp": 42,
1323 });
1324
1325 assert_eq!(serde_json::to_value(info).unwrap(), info_json);
1326 }
1327
1328 #[async_test]
1329 async fn test_room_info_migration_v1() {
1330 let store = MemoryStore::new().into_state_store();
1331
1332 let room_info_json = json!({
1333 "room_id": "!gda78o:server.tld",
1334 "room_state": "Joined",
1335 "notification_counts": {
1336 "highlight_count": 1,
1337 "notification_count": 2,
1338 },
1339 "summary": {
1340 "room_heroes": [{
1341 "user_id": "@somebody:example.org",
1342 "display_name": null,
1343 "avatar_url": null
1344 }],
1345 "joined_member_count": 5,
1346 "invited_member_count": 0,
1347 },
1348 "members_synced": true,
1349 "last_prev_batch": "pb",
1350 "sync_info": "FullySynced",
1351 "encryption_state_synced": true,
1352 "latest_event": {
1353 "event": {
1354 "encryption_info": null,
1355 "event": {
1356 "sender": "@u:i.uk",
1357 },
1358 },
1359 },
1360 "base_info": {
1361 "avatar": null,
1362 "canonical_alias": null,
1363 "create": null,
1364 "dm_targets": [],
1365 "encryption": null,
1366 "guest_access": null,
1367 "history_visibility": null,
1368 "join_rules": null,
1369 "max_power_level": 100,
1370 "name": null,
1371 "tombstone": null,
1372 "topic": null,
1373 },
1374 "read_receipts": {
1375 "num_unread": 0,
1376 "num_mentions": 0,
1377 "num_notifications": 0,
1378 "latest_active": null,
1379 "pending": []
1380 },
1381 "recency_stamp": 42,
1382 });
1383 let mut room_info: RoomInfo = serde_json::from_value(room_info_json).unwrap();
1384
1385 assert_eq!(room_info.data_format_version, 0);
1386 assert!(room_info.base_info.notable_tags.is_empty());
1387 assert!(room_info.base_info.pinned_events.is_none());
1388
1389 assert!(room_info.apply_migrations(store.clone()).await);
1391
1392 assert_eq!(room_info.data_format_version, 1);
1393 assert!(room_info.base_info.notable_tags.is_empty());
1394 assert!(room_info.base_info.pinned_events.is_none());
1395
1396 assert!(!room_info.apply_migrations(store.clone()).await);
1398
1399 assert_eq!(room_info.data_format_version, 1);
1400 assert!(room_info.base_info.notable_tags.is_empty());
1401 assert!(room_info.base_info.pinned_events.is_none());
1402
1403 let mut changes = StateChanges::default();
1405
1406 let raw_tag_event = Raw::new(&*TAG).unwrap().cast();
1407 let tag_event = raw_tag_event.deserialize().unwrap();
1408 changes.add_room_account_data(&room_info.room_id, tag_event, raw_tag_event);
1409
1410 let raw_pinned_events_event = Raw::new(&*PINNED_EVENTS).unwrap().cast();
1411 let pinned_events_event = raw_pinned_events_event.deserialize().unwrap();
1412 changes.add_state_event(&room_info.room_id, pinned_events_event, raw_pinned_events_event);
1413
1414 store.save_changes(&changes).await.unwrap();
1415
1416 room_info.data_format_version = 0;
1418 assert!(room_info.apply_migrations(store.clone()).await);
1419
1420 assert_eq!(room_info.data_format_version, 1);
1421 assert!(room_info.base_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
1422 assert!(room_info.base_info.pinned_events.is_some());
1423
1424 let new_room_info = RoomInfo::new(room_id!("!new_room:localhost"), RoomState::Joined);
1426 assert_eq!(new_room_info.data_format_version, 1);
1427 }
1428
1429 #[test]
1430 fn test_room_info_deserialization() {
1431 let info_json = json!({
1432 "room_id": "!gda78o:server.tld",
1433 "room_state": "Joined",
1434 "notification_counts": {
1435 "highlight_count": 1,
1436 "notification_count": 2,
1437 },
1438 "summary": {
1439 "room_heroes": [{
1440 "user_id": "@somebody:example.org",
1441 "display_name": "Somebody",
1442 "avatar_url": "mxc://example.org/abc"
1443 }],
1444 "joined_member_count": 5,
1445 "invited_member_count": 0,
1446 },
1447 "members_synced": true,
1448 "last_prev_batch": "pb",
1449 "sync_info": "FullySynced",
1450 "encryption_state_synced": true,
1451 "base_info": {
1452 "avatar": null,
1453 "canonical_alias": null,
1454 "create": null,
1455 "dm_targets": [],
1456 "encryption": null,
1457 "guest_access": null,
1458 "history_visibility": null,
1459 "join_rules": null,
1460 "max_power_level": 100,
1461 "name": null,
1462 "tombstone": null,
1463 "topic": null,
1464 },
1465 "cached_display_name": { "Calculated": "lol" },
1466 "cached_user_defined_notification_mode": "Mute",
1467 "recency_stamp": 42,
1468 });
1469
1470 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1471
1472 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1473 assert_eq!(info.room_state, RoomState::Joined);
1474 assert_eq!(info.notification_counts.highlight_count, 1);
1475 assert_eq!(info.notification_counts.notification_count, 2);
1476 assert_eq!(
1477 info.summary.room_heroes,
1478 vec![RoomHero {
1479 user_id: owned_user_id!("@somebody:example.org"),
1480 display_name: Some("Somebody".to_owned()),
1481 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1482 }]
1483 );
1484 assert_eq!(info.summary.joined_member_count, 5);
1485 assert_eq!(info.summary.invited_member_count, 0);
1486 assert!(info.members_synced);
1487 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1488 assert_eq!(info.sync_info, SyncInfo::FullySynced);
1489 assert!(info.encryption_state_synced);
1490 assert!(info.latest_event.is_none());
1491 assert!(info.base_info.avatar.is_none());
1492 assert!(info.base_info.canonical_alias.is_none());
1493 assert!(info.base_info.create.is_none());
1494 assert_eq!(info.base_info.dm_targets.len(), 0);
1495 assert!(info.base_info.encryption.is_none());
1496 assert!(info.base_info.guest_access.is_none());
1497 assert!(info.base_info.history_visibility.is_none());
1498 assert!(info.base_info.join_rules.is_none());
1499 assert_eq!(info.base_info.max_power_level, 100);
1500 assert!(info.base_info.name.is_none());
1501 assert!(info.base_info.tombstone.is_none());
1502 assert!(info.base_info.topic.is_none());
1503
1504 assert_eq!(
1505 info.cached_display_name.as_ref(),
1506 Some(&RoomDisplayName::Calculated("lol".to_owned())),
1507 );
1508 assert_eq!(
1509 info.cached_user_defined_notification_mode.as_ref(),
1510 Some(&RoomNotificationMode::Mute)
1511 );
1512 assert_eq!(info.recency_stamp.as_ref(), Some(&42));
1513 }
1514
1515 #[test]
1522 fn test_room_info_deserialization_without_optional_items() {
1523 let info_json = json!({
1526 "room_id": "!gda78o:server.tld",
1527 "room_state": "Invited",
1528 "notification_counts": {
1529 "highlight_count": 1,
1530 "notification_count": 2,
1531 },
1532 "summary": {
1533 "room_heroes": [{
1534 "user_id": "@somebody:example.org",
1535 "display_name": "Somebody",
1536 "avatar_url": "mxc://example.org/abc"
1537 }],
1538 "joined_member_count": 5,
1539 "invited_member_count": 0,
1540 },
1541 "members_synced": true,
1542 "last_prev_batch": "pb",
1543 "sync_info": "FullySynced",
1544 "encryption_state_synced": true,
1545 "base_info": {
1546 "avatar": null,
1547 "canonical_alias": null,
1548 "create": null,
1549 "dm_targets": [],
1550 "encryption": null,
1551 "guest_access": null,
1552 "history_visibility": null,
1553 "join_rules": null,
1554 "max_power_level": 100,
1555 "name": null,
1556 "tombstone": null,
1557 "topic": null,
1558 },
1559 });
1560
1561 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1562
1563 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1564 assert_eq!(info.room_state, RoomState::Invited);
1565 assert_eq!(info.notification_counts.highlight_count, 1);
1566 assert_eq!(info.notification_counts.notification_count, 2);
1567 assert_eq!(
1568 info.summary.room_heroes,
1569 vec![RoomHero {
1570 user_id: owned_user_id!("@somebody:example.org"),
1571 display_name: Some("Somebody".to_owned()),
1572 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1573 }]
1574 );
1575 assert_eq!(info.summary.joined_member_count, 5);
1576 assert_eq!(info.summary.invited_member_count, 0);
1577 assert!(info.members_synced);
1578 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1579 assert_eq!(info.sync_info, SyncInfo::FullySynced);
1580 assert!(info.encryption_state_synced);
1581 assert!(info.base_info.avatar.is_none());
1582 assert!(info.base_info.canonical_alias.is_none());
1583 assert!(info.base_info.create.is_none());
1584 assert_eq!(info.base_info.dm_targets.len(), 0);
1585 assert!(info.base_info.encryption.is_none());
1586 assert!(info.base_info.guest_access.is_none());
1587 assert!(info.base_info.history_visibility.is_none());
1588 assert!(info.base_info.join_rules.is_none());
1589 assert_eq!(info.base_info.max_power_level, 100);
1590 assert!(info.base_info.name.is_none());
1591 assert!(info.base_info.tombstone.is_none());
1592 assert!(info.base_info.topic.is_none());
1593 }
1594}