1#![allow(clippy::assign_op_pattern)] mod call;
18mod create;
19mod display_name;
20mod encryption;
21mod knock;
22mod latest_event;
23mod members;
24mod room_info;
25mod state;
26mod tags;
27mod tombstone;
28
29use std::collections::{BTreeMap, BTreeSet, HashSet};
30
31pub use call::CallIntentConsensus;
32pub use create::*;
33pub use display_name::{RoomDisplayName, RoomHero, RoomHeroWithProfile};
34pub(crate) use display_name::{RoomSummary, UpdatedRoomDisplayName};
35pub use encryption::EncryptionState;
36use eyeball::{AsyncLock, SharedObservable};
37use futures_util::{Stream, StreamExt};
38pub use members::{RoomMember, RoomMembersUpdate, RoomMemberships};
39pub(crate) use room_info::SyncInfo;
40pub use room_info::{
41 BaseRoomInfo, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomRecencyStamp,
42 apply_redaction,
43};
44#[cfg(feature = "unstable-msc4426")]
45use ruma::profile::{Call, Status};
46use ruma::{
47 EventId, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomId,
48 RoomVersionId, UserId,
49 events::{
50 direct::OwnedDirectUserIdentifier,
51 receipt::{Receipt, ReceiptThread, ReceiptType},
52 room::{
53 avatar,
54 guest_access::GuestAccess,
55 history_visibility::HistoryVisibility,
56 join_rules::JoinRule,
57 member::MembershipState,
58 power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent, RoomPowerLevelsSource},
59 },
60 },
61 room::RoomType,
62};
63use serde::{Deserialize, Serialize};
64pub use state::{RoomState, RoomStateFilter};
65pub(crate) use tags::RoomNotableTags;
66use tokio::sync::broadcast;
67pub use tombstone::{PredecessorRoom, SuccessorRoom};
68use tracing::{info, instrument, trace, warn};
69
70use crate::{
71 DmRoomDefinition, Error, StateStore,
72 deserialized_responses::MemberEvent,
73 notification_settings::RoomNotificationMode,
74 read_receipts::RoomReadReceipts,
75 store::{Result as StoreResult, SaveLockedStateStore, StateStoreExt},
76 sync::UnreadNotificationsCount,
77};
78
79#[derive(Debug, Clone)]
82pub struct Room {
83 pub(super) room_id: OwnedRoomId,
85
86 pub(super) own_user_id: OwnedUserId,
88
89 pub(super) info: SharedObservable<RoomInfo>,
90
91 pub(super) room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
95
96 pub(super) store: SaveLockedStateStore,
98
99 pub seen_knock_request_ids_map:
103 SharedObservable<Option<BTreeMap<OwnedEventId, OwnedUserId>>, AsyncLock>,
104
105 pub room_member_updates_sender: broadcast::Sender<RoomMembersUpdate>,
107}
108
109impl Room {
110 pub(crate) fn new(
111 own_user_id: &UserId,
112 store: SaveLockedStateStore,
113 room_id: &RoomId,
114 room_state: RoomState,
115 room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
116 ) -> Self {
117 let room_info = RoomInfo::new(room_id, room_state);
118 Self::restore(own_user_id, store, room_info, room_info_notable_update_sender)
119 }
120
121 pub(crate) fn restore(
122 own_user_id: &UserId,
123 store: SaveLockedStateStore,
124 room_info: RoomInfo,
125 room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
126 ) -> Self {
127 let (room_member_updates_sender, _) = broadcast::channel(10);
128 Self {
129 own_user_id: own_user_id.into(),
130 room_id: room_info.room_id.clone(),
131 store,
132 info: SharedObservable::new(room_info),
133 room_info_notable_update_sender,
134 seen_knock_request_ids_map: SharedObservable::new_async(None),
135 room_member_updates_sender,
136 }
137 }
138
139 pub fn room_id(&self) -> &RoomId {
141 &self.room_id
142 }
143
144 pub fn creators(&self) -> Option<Vec<OwnedUserId>> {
146 self.info.read().creators()
147 }
148
149 pub fn own_user_id(&self) -> &UserId {
151 &self.own_user_id
152 }
153
154 pub fn is_space(&self) -> bool {
156 self.info.read().room_type().is_some_and(|t| *t == RoomType::Space)
157 }
158
159 pub fn is_call(&self) -> bool {
163 self.info.read().room_type().is_some_and(|t| *t == RoomType::Call)
164 }
165
166 pub fn room_type(&self) -> Option<RoomType> {
169 self.info.read().room_type().map(ToOwned::to_owned)
170 }
171
172 pub fn unread_notification_counts(&self) -> UnreadNotificationsCount {
181 self.info.read().notification_counts
182 }
183
184 pub fn num_unread_messages(&self) -> u64 {
189 self.info.read().read_receipts.num_unread
190 }
191
192 pub fn num_unread_notifications(&self) -> u64 {
197 self.info.read().read_receipts.num_notifications
198 }
199
200 pub fn num_unread_mentions(&self) -> u64 {
206 self.info.read().read_receipts.num_mentions
207 }
208
209 pub fn read_receipts(&self) -> RoomReadReceipts {
211 self.info.read().read_receipts.clone()
212 }
213
214 pub fn is_state_fully_synced(&self) -> bool {
222 self.info.read().sync_info == SyncInfo::FullySynced
223 }
224
225 pub fn is_state_partially_or_fully_synced(&self) -> bool {
229 self.info.read().sync_info != SyncInfo::NoState
230 }
231
232 pub fn last_prev_batch(&self) -> Option<String> {
235 self.info.read().last_prev_batch.clone()
236 }
237
238 pub fn avatar_url(&self) -> Option<OwnedMxcUri> {
240 self.info.read().avatar_url().map(ToOwned::to_owned)
241 }
242
243 pub fn avatar_info(&self) -> Option<avatar::ImageInfo> {
245 self.info.read().avatar_info().map(ToOwned::to_owned)
246 }
247
248 pub fn canonical_alias(&self) -> Option<OwnedRoomAliasId> {
250 self.info.read().canonical_alias().map(ToOwned::to_owned)
251 }
252
253 pub fn alt_aliases(&self) -> Vec<OwnedRoomAliasId> {
255 self.info.read().alt_aliases().to_owned()
256 }
257
258 pub fn create_content(&self) -> Option<RoomCreateWithCreatorEventContent> {
268 Some(self.info.read().base_info.create.as_ref()?.content.clone())
269 }
270
271 #[instrument(skip_all, fields(room_id = ?self.room_id))]
275 pub async fn is_direct(&self) -> StoreResult<bool> {
276 match self.state() {
277 RoomState::Joined | RoomState::Left | RoomState::Banned => {
278 Ok(!self.info.read().base_info.dm_targets.is_empty())
279 }
280
281 RoomState::Invited => {
282 let member = self.get_member(self.own_user_id()).await?;
283
284 match member {
285 None => {
286 info!("RoomMember not found for the user's own id");
287 Ok(false)
288 }
289 Some(member) => match member.event.as_ref() {
290 MemberEvent::Sync(_) => {
291 warn!("Got MemberEvent::Sync in an invited room");
292 Ok(false)
293 }
294 MemberEvent::Stripped(event) => {
295 Ok(event.content.is_direct.unwrap_or(false))
296 }
297 },
298 }
299 }
300
301 RoomState::Knocked => Ok(false),
303 }
304 }
305
306 pub async fn compute_is_dm(&self, dm_room_definition: &DmRoomDefinition) -> StoreResult<bool> {
309 let is_direct = self.is_direct().await?;
310
311 match *dm_room_definition {
312 DmRoomDefinition::MatrixSpec => Ok(is_direct),
313 DmRoomDefinition::TwoMembers => {
314 if !is_direct {
315 return Ok(false);
316 }
317 let active_service_member_count =
318 self.update_active_service_members().await?.unwrap_or_default().len() as u64;
319 let has_at_most_two_members =
320 self.active_members_count().saturating_sub(active_service_member_count) <= 2;
321 Ok(has_at_most_two_members)
322 }
323 }
324 }
325
326 pub fn direct_targets(&self) -> HashSet<OwnedDirectUserIdentifier> {
335 self.info.read().base_info.dm_targets.clone()
336 }
337
338 pub fn direct_targets_length(&self) -> usize {
341 self.info.read().base_info.dm_targets.len()
342 }
343
344 pub fn guest_access(&self) -> GuestAccess {
346 self.info.read().guest_access().clone()
347 }
348
349 pub fn history_visibility(&self) -> Option<HistoryVisibility> {
351 self.info.read().history_visibility().cloned()
352 }
353
354 pub fn history_visibility_or_default(&self) -> HistoryVisibility {
357 self.info.read().history_visibility_or_default().clone()
358 }
359
360 pub fn is_public(&self) -> Option<bool> {
364 self.info.read().join_rule().map(|join_rule| matches!(join_rule, JoinRule::Public))
365 }
366
367 pub fn join_rule(&self) -> Option<JoinRule> {
369 self.info.read().join_rule().cloned()
370 }
371
372 pub fn max_power_level(&self) -> i64 {
377 self.info.read().base_info.max_power_level
378 }
379
380 pub fn service_members(&self) -> Option<BTreeSet<OwnedUserId>> {
382 self.info.read().service_members().cloned()
383 }
384
385 pub async fn power_levels(&self) -> Result<RoomPowerLevels, Error> {
387 let power_levels_content = self
388 .store
389 .get_state_event_static::<RoomPowerLevelsEventContent>(self.room_id())
390 .await?
391 .ok_or(Error::InsufficientData)?
392 .deserialize()?;
393 let creators = self.creators().ok_or(Error::InsufficientData)?;
394 let rules = self.info.read().room_version_rules_or_default();
395
396 Ok(power_levels_content.power_levels(&rules.authorization, creators))
397 }
398
399 pub async fn power_levels_or_default(&self) -> RoomPowerLevels {
402 if let Ok(power_levels) = self.power_levels().await {
403 return power_levels;
404 }
405
406 let rules = self.info.read().room_version_rules_or_default();
408 RoomPowerLevels::new(
409 RoomPowerLevelsSource::None,
410 &rules.authorization,
411 self.creators().into_iter().flatten(),
412 )
413 }
414
415 pub fn name(&self) -> Option<String> {
420 self.info.read().name().map(ToOwned::to_owned)
421 }
422
423 pub fn topic(&self) -> Option<String> {
425 self.info.read().topic().map(ToOwned::to_owned)
426 }
427
428 pub fn update_cached_user_defined_notification_mode(&self, mode: RoomNotificationMode) {
434 self.info.update_if(|info| {
435 if info.cached_user_defined_notification_mode.as_ref() != Some(&mode) {
436 info.cached_user_defined_notification_mode = Some(mode);
437
438 true
439 } else {
440 false
441 }
442 });
443 }
444
445 pub fn cached_user_defined_notification_mode(&self) -> Option<RoomNotificationMode> {
450 self.info.read().cached_user_defined_notification_mode
451 }
452
453 pub fn clear_user_defined_notification_mode(&self) {
456 self.info.update_if(|info| {
457 if info.cached_user_defined_notification_mode.is_some() {
458 info.cached_user_defined_notification_mode = None;
459 true
460 } else {
461 false
462 }
463 })
464 }
465
466 pub async fn joined_user_ids(&self) -> StoreResult<Vec<OwnedUserId>> {
469 self.store.get_user_ids(self.room_id(), RoomMemberships::JOIN).await
470 }
471
472 #[cfg(feature = "unstable-msc4426")]
475 pub(crate) fn hero_user_ids(&self) -> Vec<OwnedUserId> {
476 self.info.read().heroes().iter().map(|hero| hero.user_id.clone()).collect()
477 }
478
479 #[cfg_attr(not(feature = "unstable-msc4426"), allow(clippy::unused_async))]
484 pub async fn heroes(&self) -> Vec<RoomHeroWithProfile> {
485 let heroes: Vec<RoomHero> = {
486 let guard = self.info.read();
487 let heroes = guard.heroes();
488
489 if let Some(service_members) = guard.service_members() {
490 heroes
491 .iter()
492 .filter(|hero| !service_members.contains(&hero.user_id))
493 .cloned()
494 .collect()
495 } else {
496 heroes.to_vec()
497 }
498 };
499
500 if heroes.is_empty() {
502 return Vec::new();
503 }
504
505 #[cfg(not(feature = "unstable-msc4426"))]
507 {
508 heroes.into_iter().map(RoomHeroWithProfile::from).collect()
509 }
510
511 #[cfg(feature = "unstable-msc4426")]
513 {
514 let user_ids = heroes.iter().map(|hero| hero.user_id.clone()).collect::<Vec<_>>();
515
516 let mut global_profiles =
517 self.store.get_global_profiles(&user_ids).await.unwrap_or_else(|error| {
518 tracing::warn!(?error, "Failed to load global profiles for room heroes");
519 Default::default()
520 });
521
522 heroes
523 .into_iter()
524 .map(|hero| {
525 let (status, call) = global_profiles
526 .remove(&*hero.user_id)
527 .map(|profile| {
528 (
529 profile.get_static::<Status>().ok().flatten(),
530 profile.get_static::<Call>().ok().flatten(),
531 )
532 })
533 .unwrap_or_default();
534
535 RoomHeroWithProfile {
536 user_id: hero.user_id,
537 display_name: hero.display_name,
538 avatar_url: hero.avatar_url,
539 status,
540 call,
541 }
542 })
543 .collect()
544 }
545 }
546
547 pub async fn load_user_receipt(
550 &self,
551 receipt_type: ReceiptType,
552 thread: ReceiptThread,
553 user_id: &UserId,
554 ) -> StoreResult<Option<(OwnedEventId, Receipt)>> {
555 self.store.get_user_room_receipt_event(self.room_id(), receipt_type, thread, user_id).await
556 }
557
558 pub async fn load_event_receipts(
562 &self,
563 receipt_type: ReceiptType,
564 thread: ReceiptThread,
565 event_id: &EventId,
566 ) -> StoreResult<Vec<(OwnedUserId, Receipt)>> {
567 self.store
568 .get_event_room_receipt_events(self.room_id(), receipt_type, thread, event_id)
569 .await
570 }
571
572 pub fn is_marked_unread(&self) -> bool {
575 self.info.read().base_info.is_marked_unread
576 }
577
578 pub fn fully_read_event_id(&self) -> Option<OwnedEventId> {
581 self.info.read().fully_read_event_id().map(ToOwned::to_owned)
582 }
583
584 pub fn version(&self) -> Option<RoomVersionId> {
586 self.info.read().room_version().cloned()
587 }
588
589 pub fn recency_stamp(&self) -> Option<RoomRecencyStamp> {
593 self.info.read().recency_stamp
594 }
595
596 pub fn pinned_event_ids_stream(&self) -> impl Stream<Item = Vec<OwnedEventId>> + use<> {
599 self.info
600 .subscribe()
601 .map(|i| i.base_info.pinned_events.and_then(|c| c.pinned).unwrap_or_default())
602 }
603
604 pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
606 self.info.read().pinned_event_ids()
607 }
608
609 pub async fn update_active_service_members(&self) -> StoreResult<Option<Vec<RoomMember>>> {
613 if let Some(service_members) = self.service_members() {
614 let mut found = Vec::new();
615 for user_id in service_members {
616 match self.get_member(&user_id).await {
617 Ok(Some(member)) => {
618 if matches!(
620 member.membership(),
621 MembershipState::Join | MembershipState::Invite
622 ) {
623 found.push(member);
624 }
625 }
626 Ok(None) => (),
627 Err(error) => return Err(error),
628 }
629 }
630
631 trace!("Updating active service members ({}) in room {}", found.len(), self.room_id());
632
633 let new_active_service_member_count = found.len() as u64;
634 let current_active_service_member_count =
635 self.info.read().summary.active_service_members.unwrap_or_default();
636 if new_active_service_member_count != current_active_service_member_count {
637 self.update_and_save_room_info(|mut info| {
638 info.update_active_service_member_count(Some(new_active_service_member_count));
639 (info, RoomInfoNotableUpdateReasons::ACTIVE_SERVICE_MEMBERS)
640 })
641 .await?;
642 }
643
644 Ok(Some(found))
645 } else {
646 if self.info.read().summary.active_service_members.is_some() {
647 self.update_and_save_room_info(|mut info| {
648 info.update_active_service_member_count(None);
649 (info, RoomInfoNotableUpdateReasons::ACTIVE_SERVICE_MEMBERS)
650 })
651 .await?;
652 }
653 Ok(None)
654 }
655 }
656
657 #[instrument(skip_all, fields(room_id = ?self.room_id))]
661 pub async fn compute_joined_service_members(&self) -> StoreResult<Option<Vec<RoomMember>>> {
662 if !self.are_members_synced() {
663 trace!("Tried to compute joined service members in a room that is not synced");
664 return Ok(None);
665 }
666 if let Some(service_member_ids) = self.service_members() {
667 let mut ret = vec![];
668 for user_id in service_member_ids.iter() {
669 if let Some(member) = self.get_member(user_id).await.unwrap()
670 && matches!(member.membership(), MembershipState::Join)
671 {
672 trace!("Found a joined service member ({})", user_id);
673 ret.push(member);
674 } else {
675 trace!("Did not find a joined service member ({})", user_id);
676 }
677 }
678 trace!(
679 "Computed joined service members ({}) for service member count {}",
680 ret.len(),
681 service_member_ids.len()
682 );
683 Ok(Some(ret))
684 } else {
685 trace!("Tried to compute joined service members in a room that has no service members",);
686 Ok(None)
687 }
688 }
689
690 pub fn active_service_members_count(&self) -> Option<u64> {
693 self.info.read().summary.active_service_members
694 }
695}
696
697#[cfg(not(feature = "test-send-sync"))]
699unsafe impl Send for Room {}
700
701#[cfg(not(feature = "test-send-sync"))]
703unsafe impl Sync for Room {}
704
705#[cfg(feature = "test-send-sync")]
706#[test]
707fn test_send_sync_for_room() {
709 fn assert_send_sync<
710 T: matrix_sdk_common::SendOutsideWasm + matrix_sdk_common::SyncOutsideWasm,
711 >() {
712 }
713
714 assert_send_sync::<Room>();
715}
716
717#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
719pub(crate) enum AccountDataSource {
720 Stable,
722
723 #[default]
725 Unstable,
726}
727
728#[cfg(test)]
729mod tests {
730 use matrix_sdk_test::{
731 JoinedRoomBuilder, SyncResponseBuilder, async_test, event_factory::EventFactory,
732 };
733 use ruma::{room_id, user_id};
734 use serde_json::json;
735
736 use super::*;
737 use crate::test_utils::logged_in_base_client;
738
739 #[async_test]
740 async fn test_room_heroes_filters_out_service_members() {
741 let client = logged_in_base_client(None).await;
742 let user_id = &client.session_meta().unwrap().user_id;
743 let service_member_id = user_id!("@service:example.org");
744 let alice_id = user_id!("@alice:example.org");
745 let room_id = room_id!("!room:example.org");
746
747 let room = client.get_or_create_room(room_id, RoomState::Joined);
748
749 let mut sync_builder = SyncResponseBuilder::new();
751 let response = sync_builder
752 .add_joined_room(
753 JoinedRoomBuilder::new(room_id)
754 .set_room_summary(json!({
755 "m.joined_member_count": 3,
756 "m.invited_member_count": 0,
757 "m.heroes": [alice_id.to_owned(), service_member_id.to_owned()],
758 }))
759 .add_state_event(
760 EventFactory::new()
761 .sender(user_id)
762 .member_hints(BTreeSet::from([service_member_id.to_owned()])),
763 ),
764 )
765 .build_sync_response();
766
767 client.receive_sync_response(response).await.unwrap();
768
769 let heroes = room.heroes().await;
771 assert_eq!(heroes.len(), 1);
772 assert_eq!(heroes[0].user_id, alice_id);
773 }
774
775 #[cfg(feature = "unstable-msc4426")]
776 #[async_test]
777 async fn test_room_heroes_carry_global_profile() {
778 use ruma::{
779 SecondsSinceUnixEpoch,
780 profile::{
781 CallProfileField, ProfileFieldValue, StatusProfileField, UserProfileChanges,
782 UserProfileUpdate,
783 },
784 };
785
786 use crate::store::StateChanges;
787
788 let client = logged_in_base_client(None).await;
789 let alice_id = user_id!("@alice:example.org");
790 let room_id = room_id!("!room:example.org");
791
792 let room = client.get_or_create_room(room_id, RoomState::Joined);
793
794 let mut sync_builder = SyncResponseBuilder::new();
795 let response = sync_builder
796 .add_joined_room(JoinedRoomBuilder::new(room_id).set_room_summary(json!({
797 "m.joined_member_count": 2,
798 "m.invited_member_count": 0,
799 "m.heroes": [alice_id.to_owned()],
800 })))
801 .build_sync_response();
802 client.receive_sync_response(response).await.unwrap();
803
804 let heroes = room.heroes().await;
806 assert_eq!(heroes.len(), 1);
807 assert_eq!(heroes[0].user_id, alice_id);
808 assert!(heroes[0].status.is_none());
809 assert!(heroes[0].call.is_none());
810
811 let mut call = CallProfileField::new();
813 call.call_joined_ts = Some(SecondsSinceUnixEpoch(1_700_000_000u32.into()));
814 let mut changes = StateChanges::default();
815 changes.global_profiles.insert(alice_id.to_owned(), {
816 let mut profile_changes = UserProfileChanges::new();
817 profile_changes.insert_updated_value(ProfileFieldValue::Status(
818 StatusProfileField::new("Working".to_owned(), "💻".to_owned()),
819 ));
820 profile_changes.insert_updated_value(ProfileFieldValue::Call(call));
821 UserProfileUpdate::Updated(profile_changes)
822 });
823 client.state_store().save_changes(&changes).await.unwrap();
824
825 let heroes = room.heroes().await;
827 let hero = &heroes[0];
828 let status = hero.status.as_ref().expect("status is set");
829 assert_eq!(status.text, "Working");
830 assert_eq!(status.emoji, "💻");
831 assert_eq!(
832 hero.call.as_ref().expect("call is set").call_joined_ts,
833 Some(SecondsSinceUnixEpoch(1_700_000_000u32.into()))
834 );
835 }
836}