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;
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, MxcUri, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId,
50 RoomAliasId, RoomId, RoomVersionId, UserId,
51};
52use serde::{Deserialize, Serialize};
53use tracing::{debug, 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
69impl Room {
70 pub fn subscribe_info(&self) -> Subscriber<RoomInfo> {
72 self.inner.subscribe()
73 }
74
75 pub fn clone_info(&self) -> RoomInfo {
77 self.inner.get()
78 }
79
80 pub fn set_room_info(
82 &self,
83 room_info: RoomInfo,
84 room_info_notable_update_reasons: RoomInfoNotableUpdateReasons,
85 ) {
86 self.inner.set(room_info);
87
88 if !room_info_notable_update_reasons.is_empty() {
89 let _ = self.room_info_notable_update_sender.send(RoomInfoNotableUpdate {
91 room_id: self.room_id.clone(),
92 reasons: room_info_notable_update_reasons,
93 });
94 } else {
95 let _ = self.room_info_notable_update_sender.send(RoomInfoNotableUpdate {
99 room_id: self.room_id.clone(),
100 reasons: RoomInfoNotableUpdateReasons::NONE,
101 });
102 }
103 }
104}
105
106#[derive(Clone, Debug, Serialize, Deserialize)]
110pub struct BaseRoomInfo {
111 pub(crate) avatar: Option<MinimalStateEvent<RoomAvatarEventContent>>,
113 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
115 pub(crate) beacons: BTreeMap<OwnedUserId, MinimalStateEvent<BeaconInfoEventContent>>,
116 pub(crate) canonical_alias: Option<MinimalStateEvent<RoomCanonicalAliasEventContent>>,
118 pub(crate) create: Option<MinimalStateEvent<RoomCreateWithCreatorEventContent>>,
120 pub(crate) dm_targets: HashSet<OwnedDirectUserIdentifier>,
123 pub(crate) encryption: Option<RoomEncryptionEventContent>,
125 pub(crate) guest_access: Option<MinimalStateEvent<RoomGuestAccessEventContent>>,
127 pub(crate) history_visibility: Option<MinimalStateEvent<RoomHistoryVisibilityEventContent>>,
129 pub(crate) join_rules: Option<MinimalStateEvent<RoomJoinRulesEventContent>>,
131 pub(crate) max_power_level: i64,
133 pub(crate) name: Option<MinimalStateEvent<RoomNameEventContent>>,
135 pub(crate) tombstone: Option<MinimalStateEvent<RoomTombstoneEventContent>>,
137 pub(crate) topic: Option<MinimalStateEvent<RoomTopicEventContent>>,
139 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
142 pub(crate) rtc_member_events:
143 BTreeMap<CallMemberStateKey, MinimalStateEvent<CallMemberEventContent>>,
144 #[serde(default)]
146 pub(crate) is_marked_unread: bool,
147 #[serde(default)]
149 pub(crate) is_marked_unread_source: AccountDataSource,
150 #[serde(skip_serializing_if = "RoomNotableTags::is_empty", default)]
155 pub(crate) notable_tags: RoomNotableTags,
156 pub(crate) pinned_events: Option<RoomPinnedEventsEventContent>,
158}
159
160impl BaseRoomInfo {
161 pub fn new() -> Self {
163 Self::default()
164 }
165
166 pub fn room_version(&self) -> Option<&RoomVersionId> {
171 match self.create.as_ref()? {
172 MinimalStateEvent::Original(ev) => Some(&ev.content.room_version),
173 MinimalStateEvent::Redacted(ev) => Some(&ev.content.room_version),
174 }
175 }
176
177 pub fn handle_state_event(&mut self, ev: &AnySyncStateEvent) -> bool {
181 match ev {
182 AnySyncStateEvent::BeaconInfo(b) => {
183 self.beacons.insert(b.state_key().clone(), b.into());
184 }
185 AnySyncStateEvent::RoomEncryption(SyncStateEvent::Original(encryption)) => {
187 self.encryption = Some(encryption.content.clone());
188 }
189 AnySyncStateEvent::RoomAvatar(a) => {
190 self.avatar = Some(a.into());
191 }
192 AnySyncStateEvent::RoomName(n) => {
193 self.name = Some(n.into());
194 }
195 AnySyncStateEvent::RoomCreate(c) if self.create.is_none() => {
196 self.create = Some(c.into());
197 }
198 AnySyncStateEvent::RoomHistoryVisibility(h) => {
199 self.history_visibility = Some(h.into());
200 }
201 AnySyncStateEvent::RoomGuestAccess(g) => {
202 self.guest_access = Some(g.into());
203 }
204 AnySyncStateEvent::RoomJoinRules(c) => {
205 self.join_rules = Some(c.into());
206 }
207 AnySyncStateEvent::RoomCanonicalAlias(a) => {
208 self.canonical_alias = Some(a.into());
209 }
210 AnySyncStateEvent::RoomTopic(t) => {
211 self.topic = Some(t.into());
212 }
213 AnySyncStateEvent::RoomTombstone(t) => {
214 self.tombstone = Some(t.into());
215 }
216 AnySyncStateEvent::RoomPowerLevels(p) => {
217 self.max_power_level = p.power_levels().max().into();
218 }
219 AnySyncStateEvent::CallMember(m) => {
220 let Some(o_ev) = m.as_original() else {
221 return false;
222 };
223
224 let mut o_ev = o_ev.clone();
227 o_ev.content.set_created_ts_if_none(o_ev.origin_server_ts);
228
229 self.rtc_member_events
231 .insert(m.state_key().clone(), SyncStateEvent::Original(o_ev).into());
232
233 self.rtc_member_events.retain(|_, ev| {
235 ev.as_original().is_some_and(|o| !o.content.active_memberships(None).is_empty())
236 });
237 }
238 AnySyncStateEvent::RoomPinnedEvents(p) => {
239 self.pinned_events = p.as_original().map(|p| p.content.clone());
240 }
241 _ => return false,
242 }
243
244 true
245 }
246
247 pub fn handle_stripped_state_event(&mut self, ev: &AnyStrippedStateEvent) -> bool {
252 match ev {
253 AnyStrippedStateEvent::RoomEncryption(encryption) => {
254 if let Some(algorithm) = &encryption.content.algorithm {
255 let content = assign!(RoomEncryptionEventContent::new(algorithm.clone()), {
256 rotation_period_ms: encryption.content.rotation_period_ms,
257 rotation_period_msgs: encryption.content.rotation_period_msgs,
258 });
259 self.encryption = Some(content);
260 }
261 }
265 AnyStrippedStateEvent::RoomAvatar(a) => {
266 self.avatar = Some(a.into());
267 }
268 AnyStrippedStateEvent::RoomName(n) => {
269 self.name = Some(n.into());
270 }
271 AnyStrippedStateEvent::RoomCreate(c) if self.create.is_none() => {
272 self.create = Some(c.into());
273 }
274 AnyStrippedStateEvent::RoomHistoryVisibility(h) => {
275 self.history_visibility = Some(h.into());
276 }
277 AnyStrippedStateEvent::RoomGuestAccess(g) => {
278 self.guest_access = Some(g.into());
279 }
280 AnyStrippedStateEvent::RoomJoinRules(c) => {
281 self.join_rules = Some(c.into());
282 }
283 AnyStrippedStateEvent::RoomCanonicalAlias(a) => {
284 self.canonical_alias = Some(a.into());
285 }
286 AnyStrippedStateEvent::RoomTopic(t) => {
287 self.topic = Some(t.into());
288 }
289 AnyStrippedStateEvent::RoomTombstone(t) => {
290 self.tombstone = Some(t.into());
291 }
292 AnyStrippedStateEvent::RoomPowerLevels(p) => {
293 self.max_power_level = p.power_levels().max().into();
294 }
295 AnyStrippedStateEvent::CallMember(_) => {
296 return false;
299 }
300 AnyStrippedStateEvent::RoomPinnedEvents(p) => {
301 if let Some(pinned) = p.content.pinned.clone() {
302 self.pinned_events = Some(RoomPinnedEventsEventContent::new(pinned));
303 }
304 }
305 _ => return false,
306 }
307
308 true
309 }
310
311 pub(super) fn handle_redaction(&mut self, redacts: &EventId) {
312 let room_version = self.room_version().unwrap_or(&RoomVersionId::V1).to_owned();
313
314 if self.avatar.has_event_id(redacts) {
316 self.avatar.as_mut().unwrap().redact(&room_version);
317 } else if self.canonical_alias.has_event_id(redacts) {
318 self.canonical_alias.as_mut().unwrap().redact(&room_version);
319 } else if self.create.has_event_id(redacts) {
320 self.create.as_mut().unwrap().redact(&room_version);
321 } else if self.guest_access.has_event_id(redacts) {
322 self.guest_access.as_mut().unwrap().redact(&room_version);
323 } else if self.history_visibility.has_event_id(redacts) {
324 self.history_visibility.as_mut().unwrap().redact(&room_version);
325 } else if self.join_rules.has_event_id(redacts) {
326 self.join_rules.as_mut().unwrap().redact(&room_version);
327 } else if self.name.has_event_id(redacts) {
328 self.name.as_mut().unwrap().redact(&room_version);
329 } else if self.tombstone.has_event_id(redacts) {
330 self.tombstone.as_mut().unwrap().redact(&room_version);
331 } else if self.topic.has_event_id(redacts) {
332 self.topic.as_mut().unwrap().redact(&room_version);
333 } else {
334 self.rtc_member_events
335 .retain(|_, member_event| member_event.event_id() != Some(redacts));
336 }
337 }
338
339 pub fn handle_notable_tags(&mut self, tags: &Tags) {
340 let mut notable_tags = RoomNotableTags::empty();
341
342 if tags.contains_key(&TagName::Favorite) {
343 notable_tags.insert(RoomNotableTags::FAVOURITE);
344 }
345
346 if tags.contains_key(&TagName::LowPriority) {
347 notable_tags.insert(RoomNotableTags::LOW_PRIORITY);
348 }
349
350 self.notable_tags = notable_tags;
351 }
352}
353
354impl Default for BaseRoomInfo {
355 fn default() -> Self {
356 Self {
357 avatar: None,
358 beacons: BTreeMap::new(),
359 canonical_alias: None,
360 create: None,
361 dm_targets: Default::default(),
362 encryption: None,
363 guest_access: None,
364 history_visibility: None,
365 join_rules: None,
366 max_power_level: 100,
367 name: None,
368 tombstone: None,
369 topic: None,
370 rtc_member_events: BTreeMap::new(),
371 is_marked_unread: false,
372 is_marked_unread_source: AccountDataSource::Unstable,
373 notable_tags: RoomNotableTags::empty(),
374 pinned_events: None,
375 }
376 }
377}
378
379trait OptionExt {
380 fn has_event_id(&self, ev_id: &EventId) -> bool;
381}
382
383impl<C> OptionExt for Option<MinimalStateEvent<C>>
384where
385 C: StaticStateEventContent + RedactContent,
386 C::Redacted: RedactedStateEventContent,
387{
388 fn has_event_id(&self, ev_id: &EventId) -> bool {
389 self.as_ref().is_some_and(|ev| ev.event_id() == Some(ev_id))
390 }
391}
392
393#[derive(Clone, Debug, Serialize, Deserialize)]
397pub struct RoomInfo {
398 #[serde(default, alias = "version")]
401 pub(crate) data_format_version: u8,
402
403 pub(crate) room_id: OwnedRoomId,
405
406 pub(crate) room_state: RoomState,
408
409 pub(crate) notification_counts: UnreadNotificationsCount,
414
415 pub(crate) summary: RoomSummary,
417
418 pub(crate) members_synced: bool,
420
421 pub(crate) last_prev_batch: Option<String>,
423
424 pub(crate) sync_info: SyncInfo,
426
427 pub(crate) encryption_state_synced: bool,
429
430 pub(crate) latest_event: Option<Box<LatestEvent>>,
432
433 #[serde(default)]
435 pub(crate) read_receipts: RoomReadReceipts,
436
437 pub(crate) base_info: Box<BaseRoomInfo>,
440
441 #[serde(skip)]
445 pub(crate) warned_about_unknown_room_version: Arc<AtomicBool>,
446
447 #[serde(default, skip_serializing_if = "Option::is_none")]
452 pub(crate) cached_display_name: Option<RoomDisplayName>,
453
454 #[serde(default, skip_serializing_if = "Option::is_none")]
456 pub(crate) cached_user_defined_notification_mode: Option<RoomNotificationMode>,
457
458 #[serde(default)]
465 pub(crate) recency_stamp: Option<u64>,
466}
467
468impl RoomInfo {
469 #[doc(hidden)] pub fn new(room_id: &RoomId, room_state: RoomState) -> Self {
471 Self {
472 data_format_version: 1,
473 room_id: room_id.into(),
474 room_state,
475 notification_counts: Default::default(),
476 summary: Default::default(),
477 members_synced: false,
478 last_prev_batch: None,
479 sync_info: SyncInfo::NoState,
480 encryption_state_synced: false,
481 latest_event: None,
482 read_receipts: Default::default(),
483 base_info: Box::new(BaseRoomInfo::new()),
484 warned_about_unknown_room_version: Arc::new(false.into()),
485 cached_display_name: None,
486 cached_user_defined_notification_mode: None,
487 recency_stamp: None,
488 }
489 }
490
491 pub fn mark_as_joined(&mut self) {
493 self.set_state(RoomState::Joined);
494 }
495
496 pub fn mark_as_left(&mut self) {
498 self.set_state(RoomState::Left);
499 }
500
501 pub fn mark_as_invited(&mut self) {
503 self.set_state(RoomState::Invited);
504 }
505
506 pub fn mark_as_knocked(&mut self) {
508 self.set_state(RoomState::Knocked);
509 }
510
511 pub fn mark_as_banned(&mut self) {
513 self.set_state(RoomState::Banned);
514 }
515
516 pub fn set_state(&mut self, room_state: RoomState) {
518 self.room_state = room_state;
519 }
520
521 pub fn mark_members_synced(&mut self) {
523 self.members_synced = true;
524 }
525
526 pub fn mark_members_missing(&mut self) {
528 self.members_synced = false;
529 }
530
531 pub fn are_members_synced(&self) -> bool {
533 self.members_synced
534 }
535
536 pub fn mark_state_partially_synced(&mut self) {
538 self.sync_info = SyncInfo::PartiallySynced;
539 }
540
541 pub fn mark_state_fully_synced(&mut self) {
543 self.sync_info = SyncInfo::FullySynced;
544 }
545
546 pub fn mark_state_not_synced(&mut self) {
548 self.sync_info = SyncInfo::NoState;
549 }
550
551 pub fn mark_encryption_state_synced(&mut self) {
553 self.encryption_state_synced = true;
554 }
555
556 pub fn mark_encryption_state_missing(&mut self) {
558 self.encryption_state_synced = false;
559 }
560
561 pub fn set_prev_batch(&mut self, prev_batch: Option<&str>) -> bool {
565 if self.last_prev_batch.as_deref() != prev_batch {
566 self.last_prev_batch = prev_batch.map(|p| p.to_owned());
567 true
568 } else {
569 false
570 }
571 }
572
573 pub fn state(&self) -> RoomState {
575 self.room_state
576 }
577
578 pub fn encryption_state(&self) -> EncryptionState {
580 if !self.encryption_state_synced {
581 EncryptionState::Unknown
582 } else if self.base_info.encryption.is_some() {
583 EncryptionState::Encrypted
584 } else {
585 EncryptionState::NotEncrypted
586 }
587 }
588
589 pub fn set_encryption_event(&mut self, event: Option<RoomEncryptionEventContent>) {
591 self.base_info.encryption = event;
592 }
593
594 pub fn handle_encryption_state(
596 &mut self,
597 requested_required_states: &[(StateEventType, String)],
598 ) {
599 if requested_required_states
600 .iter()
601 .any(|(state_event, _)| state_event == &StateEventType::RoomEncryption)
602 {
603 self.mark_encryption_state_synced();
609 }
610 }
611
612 pub fn handle_state_event(&mut self, event: &AnySyncStateEvent) -> bool {
616 let base_info_has_been_modified = self.base_info.handle_state_event(event);
618
619 if let AnySyncStateEvent::RoomEncryption(_) = event {
620 self.mark_encryption_state_synced();
626 }
627
628 base_info_has_been_modified
629 }
630
631 pub fn handle_stripped_state_event(&mut self, event: &AnyStrippedStateEvent) -> bool {
635 self.base_info.handle_stripped_state_event(event)
636 }
637
638 #[instrument(skip_all, fields(redacts))]
640 pub fn handle_redaction(
641 &mut self,
642 event: &SyncRoomRedactionEvent,
643 _raw: &Raw<SyncRoomRedactionEvent>,
644 ) {
645 let room_version = self.base_info.room_version().unwrap_or(&RoomVersionId::V1);
646
647 let Some(redacts) = event.redacts(room_version) else {
648 info!("Can't apply redaction, redacts field is missing");
649 return;
650 };
651 tracing::Span::current().record("redacts", debug(redacts));
652
653 if let Some(latest_event) = &mut self.latest_event {
654 tracing::trace!("Checking if redaction applies to latest event");
655 if latest_event.event_id().as_deref() == Some(redacts) {
656 match apply_redaction(latest_event.event().raw(), _raw, room_version) {
657 Some(redacted) => {
658 latest_event.event_mut().kind =
661 TimelineEventKind::PlainText { event: redacted };
662 debug!("Redacted latest event");
663 }
664 None => {
665 self.latest_event = None;
666 debug!("Removed latest event");
667 }
668 }
669 }
670 }
671
672 self.base_info.handle_redaction(redacts);
673 }
674
675 pub fn avatar_url(&self) -> Option<&MxcUri> {
677 self.base_info
678 .avatar
679 .as_ref()
680 .and_then(|e| e.as_original().and_then(|e| e.content.url.as_deref()))
681 }
682
683 pub fn update_avatar(&mut self, url: Option<OwnedMxcUri>) {
685 self.base_info.avatar = url.map(|url| {
686 let mut content = RoomAvatarEventContent::new();
687 content.url = Some(url);
688
689 MinimalStateEvent::Original(OriginalMinimalStateEvent { content, event_id: None })
690 });
691 }
692
693 pub fn avatar_info(&self) -> Option<&avatar::ImageInfo> {
695 self.base_info
696 .avatar
697 .as_ref()
698 .and_then(|e| e.as_original().and_then(|e| e.content.info.as_deref()))
699 }
700
701 pub fn update_notification_count(&mut self, notification_counts: UnreadNotificationsCount) {
703 self.notification_counts = notification_counts;
704 }
705
706 pub fn update_from_ruma_summary(&mut self, summary: &RumaSummary) -> bool {
710 let mut changed = false;
711
712 if !summary.is_empty() {
713 if !summary.heroes.is_empty() {
714 self.summary.room_heroes = summary
715 .heroes
716 .iter()
717 .map(|hero_id| RoomHero {
718 user_id: hero_id.to_owned(),
719 display_name: None,
720 avatar_url: None,
721 })
722 .collect();
723
724 changed = true;
725 }
726
727 if let Some(joined) = summary.joined_member_count {
728 self.summary.joined_member_count = joined.into();
729 changed = true;
730 }
731
732 if let Some(invited) = summary.invited_member_count {
733 self.summary.invited_member_count = invited.into();
734 changed = true;
735 }
736 }
737
738 changed
739 }
740
741 pub(crate) fn update_joined_member_count(&mut self, count: u64) {
743 self.summary.joined_member_count = count;
744 }
745
746 pub(crate) fn update_invited_member_count(&mut self, count: u64) {
748 self.summary.invited_member_count = count;
749 }
750
751 pub(crate) fn update_heroes(&mut self, heroes: Vec<RoomHero>) {
753 self.summary.room_heroes = heroes;
754 }
755
756 pub fn heroes(&self) -> &[RoomHero] {
758 &self.summary.room_heroes
759 }
760
761 pub fn active_members_count(&self) -> u64 {
765 self.summary.joined_member_count.saturating_add(self.summary.invited_member_count)
766 }
767
768 pub fn invited_members_count(&self) -> u64 {
770 self.summary.invited_member_count
771 }
772
773 pub fn joined_members_count(&self) -> u64 {
775 self.summary.joined_member_count
776 }
777
778 pub fn canonical_alias(&self) -> Option<&RoomAliasId> {
780 self.base_info.canonical_alias.as_ref()?.as_original()?.content.alias.as_deref()
781 }
782
783 pub fn alt_aliases(&self) -> &[OwnedRoomAliasId] {
785 self.base_info
786 .canonical_alias
787 .as_ref()
788 .and_then(|ev| ev.as_original())
789 .map(|ev| ev.content.alt_aliases.as_ref())
790 .unwrap_or_default()
791 }
792
793 pub fn room_id(&self) -> &RoomId {
795 &self.room_id
796 }
797
798 pub fn room_version(&self) -> Option<&RoomVersionId> {
800 self.base_info.room_version()
801 }
802
803 pub fn room_version_or_default(&self) -> RoomVersionId {
808 use std::sync::atomic::Ordering;
809
810 self.base_info.room_version().cloned().unwrap_or_else(|| {
811 if self
812 .warned_about_unknown_room_version
813 .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
814 .is_ok()
815 {
816 warn!("Unknown room version, falling back to v10");
817 }
818
819 RoomVersionId::V10
820 })
821 }
822
823 pub fn room_type(&self) -> Option<&RoomType> {
825 match self.base_info.create.as_ref()? {
826 MinimalStateEvent::Original(ev) => ev.content.room_type.as_ref(),
827 MinimalStateEvent::Redacted(ev) => ev.content.room_type.as_ref(),
828 }
829 }
830
831 pub fn creator(&self) -> Option<&UserId> {
833 match self.base_info.create.as_ref()? {
834 MinimalStateEvent::Original(ev) => Some(&ev.content.creator),
835 MinimalStateEvent::Redacted(ev) => Some(&ev.content.creator),
836 }
837 }
838
839 pub(super) fn guest_access(&self) -> &GuestAccess {
840 match &self.base_info.guest_access {
841 Some(MinimalStateEvent::Original(ev)) => &ev.content.guest_access,
842 _ => &GuestAccess::Forbidden,
843 }
844 }
845
846 pub fn history_visibility(&self) -> Option<&HistoryVisibility> {
850 match &self.base_info.history_visibility {
851 Some(MinimalStateEvent::Original(ev)) => Some(&ev.content.history_visibility),
852 _ => None,
853 }
854 }
855
856 pub fn history_visibility_or_default(&self) -> &HistoryVisibility {
863 match &self.base_info.history_visibility {
864 Some(MinimalStateEvent::Original(ev)) => &ev.content.history_visibility,
865 _ => &HistoryVisibility::Shared,
866 }
867 }
868
869 pub fn join_rule(&self) -> &JoinRule {
873 match &self.base_info.join_rules {
874 Some(MinimalStateEvent::Original(ev)) => &ev.content.join_rule,
875 _ => &JoinRule::Public,
876 }
877 }
878
879 pub fn name(&self) -> Option<&str> {
881 let name = &self.base_info.name.as_ref()?.as_original()?.content.name;
882 (!name.is_empty()).then_some(name)
883 }
884
885 pub(super) fn tombstone(&self) -> Option<&RoomTombstoneEventContent> {
886 Some(&self.base_info.tombstone.as_ref()?.as_original()?.content)
887 }
888
889 pub fn topic(&self) -> Option<&str> {
891 Some(&self.base_info.topic.as_ref()?.as_original()?.content.topic)
892 }
893
894 fn active_matrix_rtc_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
899 let mut v = self
900 .base_info
901 .rtc_member_events
902 .iter()
903 .filter_map(|(user_id, ev)| {
904 ev.as_original().map(|ev| {
905 ev.content
906 .active_memberships(None)
907 .into_iter()
908 .map(move |m| (user_id.clone(), m))
909 })
910 })
911 .flatten()
912 .collect::<Vec<_>>();
913 v.sort_by_key(|(_, m)| m.created_ts());
914 v
915 }
916
917 fn active_room_call_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
923 self.active_matrix_rtc_memberships()
924 .into_iter()
925 .filter(|(_user_id, m)| m.is_room_call())
926 .collect()
927 }
928
929 pub fn has_active_room_call(&self) -> bool {
932 !self.active_room_call_memberships().is_empty()
933 }
934
935 pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
944 self.active_room_call_memberships()
945 .iter()
946 .map(|(call_member_state_key, _)| call_member_state_key.user_id().to_owned())
947 .collect()
948 }
949
950 pub fn latest_event(&self) -> Option<&LatestEvent> {
952 self.latest_event.as_deref()
953 }
954
955 pub(crate) fn update_recency_stamp(&mut self, stamp: u64) {
959 self.recency_stamp = Some(stamp);
960 }
961
962 pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
964 self.base_info.pinned_events.clone().map(|c| c.pinned)
965 }
966
967 pub fn is_pinned_event(&self, event_id: &EventId) -> bool {
973 self.base_info
974 .pinned_events
975 .as_ref()
976 .map(|p| p.pinned.contains(&event_id.to_owned()))
977 .unwrap_or_default()
978 }
979
980 #[instrument(skip_all, fields(room_id = ?self.room_id))]
988 pub(crate) async fn apply_migrations(&mut self, store: Arc<DynStateStore>) -> bool {
989 let mut migrated = false;
990
991 if self.data_format_version < 1 {
992 info!("Migrating room info to version 1");
993
994 match store.get_room_account_data_event_static::<TagEventContent>(&self.room_id).await {
996 Ok(Some(raw_event)) => match raw_event.deserialize() {
998 Ok(event) => {
999 self.base_info.handle_notable_tags(&event.content.tags);
1000 }
1001 Err(error) => {
1002 warn!("Failed to deserialize room tags: {error}");
1003 }
1004 },
1005 Ok(_) => {
1006 }
1008 Err(error) => {
1009 warn!("Failed to load room tags: {error}");
1010 }
1011 }
1012
1013 match store.get_state_event_static::<RoomPinnedEventsEventContent>(&self.room_id).await
1015 {
1016 Ok(Some(RawSyncOrStrippedState::Sync(raw_event))) => {
1018 match raw_event.deserialize() {
1019 Ok(event) => {
1020 self.handle_state_event(&event.into());
1021 }
1022 Err(error) => {
1023 warn!("Failed to deserialize room pinned events: {error}");
1024 }
1025 }
1026 }
1027 Ok(_) => {
1028 }
1030 Err(error) => {
1031 warn!("Failed to load room pinned events: {error}");
1032 }
1033 }
1034
1035 self.data_format_version = 1;
1036 migrated = true;
1037 }
1038
1039 migrated
1040 }
1041}
1042
1043#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1044pub(crate) enum SyncInfo {
1045 NoState,
1051
1052 PartiallySynced,
1055
1056 FullySynced,
1058}
1059
1060pub fn apply_redaction(
1063 event: &Raw<AnySyncTimelineEvent>,
1064 raw_redaction: &Raw<SyncRoomRedactionEvent>,
1065 room_version: &RoomVersionId,
1066) -> Option<Raw<AnySyncTimelineEvent>> {
1067 use ruma::canonical_json::{redact_in_place, RedactedBecause};
1068
1069 let mut event_json = match event.deserialize_as() {
1070 Ok(json) => json,
1071 Err(e) => {
1072 warn!("Failed to deserialize latest event: {e}");
1073 return None;
1074 }
1075 };
1076
1077 let redacted_because = match RedactedBecause::from_raw_event(raw_redaction) {
1078 Ok(rb) => rb,
1079 Err(e) => {
1080 warn!("Redaction event is not valid canonical JSON: {e}");
1081 return None;
1082 }
1083 };
1084
1085 let redact_result = redact_in_place(&mut event_json, room_version, Some(redacted_because));
1086
1087 if let Err(e) = redact_result {
1088 warn!("Failed to redact event: {e}");
1089 return None;
1090 }
1091
1092 let raw = Raw::new(&event_json).expect("CanonicalJsonObject must be serializable");
1093 Some(raw.cast())
1094}
1095
1096#[derive(Debug, Clone)]
1106pub struct RoomInfoNotableUpdate {
1107 pub room_id: OwnedRoomId,
1109
1110 pub reasons: RoomInfoNotableUpdateReasons,
1112}
1113
1114bitflags! {
1115 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1117 pub struct RoomInfoNotableUpdateReasons: u8 {
1118 const RECENCY_STAMP = 0b0000_0001;
1120
1121 const LATEST_EVENT = 0b0000_0010;
1123
1124 const READ_RECEIPT = 0b0000_0100;
1126
1127 const UNREAD_MARKER = 0b0000_1000;
1129
1130 const MEMBERSHIP = 0b0001_0000;
1132
1133 const DISPLAY_NAME = 0b0010_0000;
1135
1136 const NONE = 0b1000_0000;
1147 }
1148}
1149
1150impl Default for RoomInfoNotableUpdateReasons {
1151 fn default() -> Self {
1152 Self::empty()
1153 }
1154}
1155
1156#[cfg(test)]
1157mod tests {
1158 use std::sync::Arc;
1159
1160 use matrix_sdk_common::deserialized_responses::TimelineEvent;
1161 use matrix_sdk_test::{
1162 async_test,
1163 test_json::{sync_events::PINNED_EVENTS, TAG},
1164 };
1165 use ruma::{
1166 assign, events::room::pinned_events::RoomPinnedEventsEventContent, owned_event_id,
1167 owned_mxc_uri, owned_user_id, room_id, serde::Raw,
1168 };
1169 use serde_json::json;
1170
1171 use super::{BaseRoomInfo, RoomInfo, SyncInfo};
1172 use crate::{
1173 latest_event::LatestEvent,
1174 notification_settings::RoomNotificationMode,
1175 room::{RoomNotableTags, RoomSummary},
1176 store::{IntoStateStore, MemoryStore},
1177 sync::UnreadNotificationsCount,
1178 RoomDisplayName, RoomHero, RoomState, StateChanges,
1179 };
1180
1181 #[test]
1182 fn test_room_info_serialization() {
1183 let info = RoomInfo {
1187 data_format_version: 1,
1188 room_id: room_id!("!gda78o:server.tld").into(),
1189 room_state: RoomState::Invited,
1190 notification_counts: UnreadNotificationsCount {
1191 highlight_count: 1,
1192 notification_count: 2,
1193 },
1194 summary: RoomSummary {
1195 room_heroes: vec![RoomHero {
1196 user_id: owned_user_id!("@somebody:example.org"),
1197 display_name: None,
1198 avatar_url: None,
1199 }],
1200 joined_member_count: 5,
1201 invited_member_count: 0,
1202 },
1203 members_synced: true,
1204 last_prev_batch: Some("pb".to_owned()),
1205 sync_info: SyncInfo::FullySynced,
1206 encryption_state_synced: true,
1207 latest_event: Some(Box::new(LatestEvent::new(TimelineEvent::new(
1208 Raw::from_json_string(json!({"sender": "@u:i.uk"}).to_string()).unwrap(),
1209 )))),
1210 base_info: Box::new(
1211 assign!(BaseRoomInfo::new(), { pinned_events: Some(RoomPinnedEventsEventContent::new(vec![owned_event_id!("$a")])) }),
1212 ),
1213 read_receipts: Default::default(),
1214 warned_about_unknown_room_version: Arc::new(false.into()),
1215 cached_display_name: None,
1216 cached_user_defined_notification_mode: None,
1217 recency_stamp: Some(42),
1218 };
1219
1220 let info_json = json!({
1221 "data_format_version": 1,
1222 "room_id": "!gda78o:server.tld",
1223 "room_state": "Invited",
1224 "notification_counts": {
1225 "highlight_count": 1,
1226 "notification_count": 2,
1227 },
1228 "summary": {
1229 "room_heroes": [{
1230 "user_id": "@somebody:example.org",
1231 "display_name": null,
1232 "avatar_url": null
1233 }],
1234 "joined_member_count": 5,
1235 "invited_member_count": 0,
1236 },
1237 "members_synced": true,
1238 "last_prev_batch": "pb",
1239 "sync_info": "FullySynced",
1240 "encryption_state_synced": true,
1241 "latest_event": {
1242 "event": {
1243 "kind": {"PlainText": {"event": {"sender": "@u:i.uk"}}},
1244 },
1245 },
1246 "base_info": {
1247 "avatar": null,
1248 "canonical_alias": null,
1249 "create": null,
1250 "dm_targets": [],
1251 "encryption": null,
1252 "guest_access": null,
1253 "history_visibility": null,
1254 "is_marked_unread": false,
1255 "is_marked_unread_source": "Unstable",
1256 "join_rules": null,
1257 "max_power_level": 100,
1258 "name": null,
1259 "tombstone": null,
1260 "topic": null,
1261 "pinned_events": {
1262 "pinned": ["$a"]
1263 },
1264 },
1265 "read_receipts": {
1266 "num_unread": 0,
1267 "num_mentions": 0,
1268 "num_notifications": 0,
1269 "latest_active": null,
1270 "pending": []
1271 },
1272 "recency_stamp": 42,
1273 });
1274
1275 assert_eq!(serde_json::to_value(info).unwrap(), info_json);
1276 }
1277
1278 #[async_test]
1279 async fn test_room_info_migration_v1() {
1280 let store = MemoryStore::new().into_state_store();
1281
1282 let room_info_json = json!({
1283 "room_id": "!gda78o:server.tld",
1284 "room_state": "Joined",
1285 "notification_counts": {
1286 "highlight_count": 1,
1287 "notification_count": 2,
1288 },
1289 "summary": {
1290 "room_heroes": [{
1291 "user_id": "@somebody:example.org",
1292 "display_name": null,
1293 "avatar_url": null
1294 }],
1295 "joined_member_count": 5,
1296 "invited_member_count": 0,
1297 },
1298 "members_synced": true,
1299 "last_prev_batch": "pb",
1300 "sync_info": "FullySynced",
1301 "encryption_state_synced": true,
1302 "latest_event": {
1303 "event": {
1304 "encryption_info": null,
1305 "event": {
1306 "sender": "@u:i.uk",
1307 },
1308 },
1309 },
1310 "base_info": {
1311 "avatar": null,
1312 "canonical_alias": null,
1313 "create": null,
1314 "dm_targets": [],
1315 "encryption": null,
1316 "guest_access": null,
1317 "history_visibility": null,
1318 "join_rules": null,
1319 "max_power_level": 100,
1320 "name": null,
1321 "tombstone": null,
1322 "topic": null,
1323 },
1324 "read_receipts": {
1325 "num_unread": 0,
1326 "num_mentions": 0,
1327 "num_notifications": 0,
1328 "latest_active": null,
1329 "pending": []
1330 },
1331 "recency_stamp": 42,
1332 });
1333 let mut room_info: RoomInfo = serde_json::from_value(room_info_json).unwrap();
1334
1335 assert_eq!(room_info.data_format_version, 0);
1336 assert!(room_info.base_info.notable_tags.is_empty());
1337 assert!(room_info.base_info.pinned_events.is_none());
1338
1339 assert!(room_info.apply_migrations(store.clone()).await);
1341
1342 assert_eq!(room_info.data_format_version, 1);
1343 assert!(room_info.base_info.notable_tags.is_empty());
1344 assert!(room_info.base_info.pinned_events.is_none());
1345
1346 assert!(!room_info.apply_migrations(store.clone()).await);
1348
1349 assert_eq!(room_info.data_format_version, 1);
1350 assert!(room_info.base_info.notable_tags.is_empty());
1351 assert!(room_info.base_info.pinned_events.is_none());
1352
1353 let mut changes = StateChanges::default();
1355
1356 let raw_tag_event = Raw::new(&*TAG).unwrap().cast();
1357 let tag_event = raw_tag_event.deserialize().unwrap();
1358 changes.add_room_account_data(&room_info.room_id, tag_event, raw_tag_event);
1359
1360 let raw_pinned_events_event = Raw::new(&*PINNED_EVENTS).unwrap().cast();
1361 let pinned_events_event = raw_pinned_events_event.deserialize().unwrap();
1362 changes.add_state_event(&room_info.room_id, pinned_events_event, raw_pinned_events_event);
1363
1364 store.save_changes(&changes).await.unwrap();
1365
1366 room_info.data_format_version = 0;
1368 assert!(room_info.apply_migrations(store.clone()).await);
1369
1370 assert_eq!(room_info.data_format_version, 1);
1371 assert!(room_info.base_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
1372 assert!(room_info.base_info.pinned_events.is_some());
1373
1374 let new_room_info = RoomInfo::new(room_id!("!new_room:localhost"), RoomState::Joined);
1376 assert_eq!(new_room_info.data_format_version, 1);
1377 }
1378
1379 #[test]
1380 fn test_room_info_deserialization() {
1381 let info_json = json!({
1382 "room_id": "!gda78o:server.tld",
1383 "room_state": "Joined",
1384 "notification_counts": {
1385 "highlight_count": 1,
1386 "notification_count": 2,
1387 },
1388 "summary": {
1389 "room_heroes": [{
1390 "user_id": "@somebody:example.org",
1391 "display_name": "Somebody",
1392 "avatar_url": "mxc://example.org/abc"
1393 }],
1394 "joined_member_count": 5,
1395 "invited_member_count": 0,
1396 },
1397 "members_synced": true,
1398 "last_prev_batch": "pb",
1399 "sync_info": "FullySynced",
1400 "encryption_state_synced": true,
1401 "base_info": {
1402 "avatar": null,
1403 "canonical_alias": null,
1404 "create": null,
1405 "dm_targets": [],
1406 "encryption": null,
1407 "guest_access": null,
1408 "history_visibility": null,
1409 "join_rules": null,
1410 "max_power_level": 100,
1411 "name": null,
1412 "tombstone": null,
1413 "topic": null,
1414 },
1415 "cached_display_name": { "Calculated": "lol" },
1416 "cached_user_defined_notification_mode": "Mute",
1417 "recency_stamp": 42,
1418 });
1419
1420 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1421
1422 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1423 assert_eq!(info.room_state, RoomState::Joined);
1424 assert_eq!(info.notification_counts.highlight_count, 1);
1425 assert_eq!(info.notification_counts.notification_count, 2);
1426 assert_eq!(
1427 info.summary.room_heroes,
1428 vec![RoomHero {
1429 user_id: owned_user_id!("@somebody:example.org"),
1430 display_name: Some("Somebody".to_owned()),
1431 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1432 }]
1433 );
1434 assert_eq!(info.summary.joined_member_count, 5);
1435 assert_eq!(info.summary.invited_member_count, 0);
1436 assert!(info.members_synced);
1437 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1438 assert_eq!(info.sync_info, SyncInfo::FullySynced);
1439 assert!(info.encryption_state_synced);
1440 assert!(info.latest_event.is_none());
1441 assert!(info.base_info.avatar.is_none());
1442 assert!(info.base_info.canonical_alias.is_none());
1443 assert!(info.base_info.create.is_none());
1444 assert_eq!(info.base_info.dm_targets.len(), 0);
1445 assert!(info.base_info.encryption.is_none());
1446 assert!(info.base_info.guest_access.is_none());
1447 assert!(info.base_info.history_visibility.is_none());
1448 assert!(info.base_info.join_rules.is_none());
1449 assert_eq!(info.base_info.max_power_level, 100);
1450 assert!(info.base_info.name.is_none());
1451 assert!(info.base_info.tombstone.is_none());
1452 assert!(info.base_info.topic.is_none());
1453
1454 assert_eq!(
1455 info.cached_display_name.as_ref(),
1456 Some(&RoomDisplayName::Calculated("lol".to_owned())),
1457 );
1458 assert_eq!(
1459 info.cached_user_defined_notification_mode.as_ref(),
1460 Some(&RoomNotificationMode::Mute)
1461 );
1462 assert_eq!(info.recency_stamp.as_ref(), Some(&42));
1463 }
1464
1465 #[test]
1472 fn test_room_info_deserialization_without_optional_items() {
1473 let info_json = json!({
1476 "room_id": "!gda78o:server.tld",
1477 "room_state": "Invited",
1478 "notification_counts": {
1479 "highlight_count": 1,
1480 "notification_count": 2,
1481 },
1482 "summary": {
1483 "room_heroes": [{
1484 "user_id": "@somebody:example.org",
1485 "display_name": "Somebody",
1486 "avatar_url": "mxc://example.org/abc"
1487 }],
1488 "joined_member_count": 5,
1489 "invited_member_count": 0,
1490 },
1491 "members_synced": true,
1492 "last_prev_batch": "pb",
1493 "sync_info": "FullySynced",
1494 "encryption_state_synced": true,
1495 "base_info": {
1496 "avatar": null,
1497 "canonical_alias": null,
1498 "create": null,
1499 "dm_targets": [],
1500 "encryption": null,
1501 "guest_access": null,
1502 "history_visibility": null,
1503 "join_rules": null,
1504 "max_power_level": 100,
1505 "name": null,
1506 "tombstone": null,
1507 "topic": null,
1508 },
1509 });
1510
1511 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1512
1513 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1514 assert_eq!(info.room_state, RoomState::Invited);
1515 assert_eq!(info.notification_counts.highlight_count, 1);
1516 assert_eq!(info.notification_counts.notification_count, 2);
1517 assert_eq!(
1518 info.summary.room_heroes,
1519 vec![RoomHero {
1520 user_id: owned_user_id!("@somebody:example.org"),
1521 display_name: Some("Somebody".to_owned()),
1522 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1523 }]
1524 );
1525 assert_eq!(info.summary.joined_member_count, 5);
1526 assert_eq!(info.summary.invited_member_count, 0);
1527 assert!(info.members_synced);
1528 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1529 assert_eq!(info.sync_info, SyncInfo::FullySynced);
1530 assert!(info.encryption_state_synced);
1531 assert!(info.base_info.avatar.is_none());
1532 assert!(info.base_info.canonical_alias.is_none());
1533 assert!(info.base_info.create.is_none());
1534 assert_eq!(info.base_info.dm_targets.len(), 0);
1535 assert!(info.base_info.encryption.is_none());
1536 assert!(info.base_info.guest_access.is_none());
1537 assert!(info.base_info.history_visibility.is_none());
1538 assert!(info.base_info.join_rules.is_none());
1539 assert_eq!(info.base_info.max_power_level, 100);
1540 assert!(info.base_info.name.is_none());
1541 assert!(info.base_info.tombstone.is_none());
1542 assert!(info.base_info.topic.is_none());
1543 }
1544}