1#[cfg(feature = "e2e-encryption")]
17use std::sync::Arc;
18use std::{
19 collections::{BTreeMap, BTreeSet, HashMap},
20 fmt,
21 ops::Deref,
22};
23
24use eyeball::{SharedObservable, Subscriber};
25use eyeball_im::{Vector, VectorDiff};
26use futures_util::Stream;
27use matrix_sdk_common::{cross_process_lock::CrossProcessLockConfig, timer};
28#[cfg(feature = "e2e-encryption")]
29use matrix_sdk_crypto::{
30 CollectStrategy, DecryptionSettings, EncryptionSettings, OlmError, OlmMachine,
31 TrustRequirement, store::DynCryptoStore, store::types::RoomPendingKeyBundleDetails,
32 types::requests::ToDeviceRequest,
33};
34#[cfg(doc)]
35use ruma::DeviceId;
36#[cfg(feature = "e2e-encryption")]
37use ruma::events::room::{history_visibility::HistoryVisibility, member::MembershipState};
38use ruma::{
39 OwnedRoomId, OwnedUserId, RoomId, UserId,
40 api::client::{self as api, sync::sync_events::v5},
41 events::{
42 StateEvent, StateEventType,
43 ignored_user_list::IgnoredUserListEventContent,
44 push_rules::{PushRulesEvent, PushRulesEventContent},
45 room::member::SyncRoomMemberEvent,
46 },
47 push::Ruleset,
48 time::Instant,
49};
50use tokio::sync::{Mutex, broadcast};
51#[cfg(feature = "e2e-encryption")]
52use tokio::sync::{RwLock, RwLockReadGuard};
53use tracing::{Level, debug, enabled, info, instrument, warn};
54
55#[cfg(feature = "e2e-encryption")]
56use crate::RoomMemberships;
57use crate::{
58 RoomStateFilter, SessionMeta, StateStore,
59 deserialized_responses::DisplayName,
60 error::{Error, Result},
61 event_cache::store::{EventCacheStoreLock, EventCacheStoreLockState},
62 media::store::MediaStoreLock,
63 response_processors::{self as processors, Context},
64 room::{
65 Room, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMembersUpdate, RoomState,
66 },
67 store::{
68 BaseStateStore, DynStateStore, MemoryStore, Result as StoreResult, RoomLoadSettings,
69 StateChanges, StateStoreDataKey, StateStoreDataValue, StateStoreExt, StoreConfig,
70 ambiguity_map::AmbiguityCache,
71 },
72 sync::{RoomUpdates, SyncResponse},
73};
74
75#[derive(Clone)]
96pub struct BaseClient {
97 pub(crate) state_store: BaseStateStore,
99
100 event_cache_store: EventCacheStoreLock,
102
103 media_store: MediaStoreLock,
105
106 #[cfg(feature = "e2e-encryption")]
111 crypto_store: Arc<DynCryptoStore>,
112
113 #[cfg(feature = "e2e-encryption")]
117 olm_machine: Arc<RwLock<Option<OlmMachine>>>,
118
119 pub(crate) ignore_user_list_changes: SharedObservable<Vec<String>>,
121
122 #[cfg(feature = "e2e-encryption")]
125 pub room_key_recipient_strategy: CollectStrategy,
126
127 #[cfg(feature = "e2e-encryption")]
129 pub decryption_settings: DecryptionSettings,
130
131 #[cfg(feature = "e2e-encryption")]
133 pub handle_verification_events: bool,
134
135 pub threading_support: ThreadingSupport,
137
138 pub dm_room_definition: DmRoomDefinition,
140}
141
142#[cfg(not(tarpaulin_include))]
143impl fmt::Debug for BaseClient {
144 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145 f.debug_struct("BaseClient")
146 .field("session_meta", &self.state_store.session_meta())
147 .field("sync_token", &self.state_store.sync_token)
148 .finish_non_exhaustive()
149 }
150}
151
152#[derive(Clone, Copy, Debug)]
164pub enum ThreadingSupport {
165 Enabled {
167 with_subscriptions: bool,
173 },
174 Disabled,
176}
177
178impl BaseClient {
179 pub fn new(
186 config: StoreConfig,
187 threading_support: ThreadingSupport,
188 dm_room_definition: DmRoomDefinition,
189 ) -> Self {
190 let store = BaseStateStore::new(config.state_store);
191
192 BaseClient {
193 state_store: store,
194 event_cache_store: config.event_cache_store,
195 media_store: config.media_store,
196 #[cfg(feature = "e2e-encryption")]
197 crypto_store: config.crypto_store,
198 #[cfg(feature = "e2e-encryption")]
199 olm_machine: Default::default(),
200 ignore_user_list_changes: Default::default(),
201 #[cfg(feature = "e2e-encryption")]
202 room_key_recipient_strategy: Default::default(),
203 #[cfg(feature = "e2e-encryption")]
204 decryption_settings: DecryptionSettings {
205 sender_device_trust_requirement: TrustRequirement::Untrusted,
206 },
207 #[cfg(feature = "e2e-encryption")]
208 handle_verification_events: true,
209 threading_support,
210 dm_room_definition,
211 }
212 }
213
214 #[cfg(feature = "e2e-encryption")]
217 pub async fn clone_with_in_memory_state_store(
218 &self,
219 cross_process_mode: CrossProcessLockConfig,
220 handle_verification_events: bool,
221 ) -> Result<Self> {
222 let config = StoreConfig::new(cross_process_mode).state_store(MemoryStore::new());
223 let config = config.crypto_store(self.crypto_store.clone());
224
225 let copy = Self {
226 state_store: BaseStateStore::new(config.state_store),
227 event_cache_store: config.event_cache_store,
228 media_store: config.media_store,
229 crypto_store: self.crypto_store.clone(),
236 olm_machine: self.olm_machine.clone(),
237 ignore_user_list_changes: Default::default(),
238 room_key_recipient_strategy: self.room_key_recipient_strategy.clone(),
239 decryption_settings: self.decryption_settings.clone(),
240 handle_verification_events,
241 threading_support: self.threading_support,
242 dm_room_definition: self.dm_room_definition.clone(),
243 };
244
245 copy.state_store.derive_from_other(&self.state_store).await?;
246
247 Ok(copy)
248 }
249
250 #[cfg(not(feature = "e2e-encryption"))]
253 #[allow(clippy::unused_async)]
254 pub async fn clone_with_in_memory_state_store(
255 &self,
256 cross_process_store_config: CrossProcessLockConfig,
257 _handle_verification_events: bool,
258 ) -> Result<Self> {
259 let config = StoreConfig::new(cross_process_store_config).state_store(MemoryStore::new());
260 Ok(Self::new(config, ThreadingSupport::Disabled, DmRoomDefinition::default()))
261 }
262
263 pub fn session_meta(&self) -> Option<&SessionMeta> {
269 self.state_store.session_meta()
270 }
271
272 pub fn rooms(&self) -> Vec<Room> {
274 self.state_store.rooms()
275 }
276
277 pub fn rooms_filtered(&self, filter: RoomStateFilter) -> Vec<Room> {
279 self.state_store.rooms_filtered(filter)
280 }
281
282 pub fn rooms_stream(
285 &self,
286 ) -> (Vector<Room>, impl Stream<Item = Vec<VectorDiff<Room>>> + use<>) {
287 self.state_store.rooms_stream()
288 }
289
290 pub fn get_or_create_room(&self, room_id: &RoomId, room_state: RoomState) -> Room {
293 self.state_store.get_or_create_room(room_id, room_state)
294 }
295
296 pub fn state_store(&self) -> &DynStateStore {
298 self.state_store.deref()
299 }
300
301 pub fn event_cache_store(&self) -> &EventCacheStoreLock {
303 &self.event_cache_store
304 }
305
306 pub fn media_store(&self) -> &MediaStoreLock {
308 &self.media_store
309 }
310
311 pub fn is_active(&self) -> bool {
315 self.state_store.session_meta().is_some()
316 }
317
318 pub async fn activate(
350 &self,
351 session_meta: SessionMeta,
352 room_load_settings: RoomLoadSettings,
353 #[cfg(feature = "e2e-encryption")] custom_account: Option<
354 crate::crypto::vodozemac::olm::Account,
355 >,
356 ) -> Result<()> {
357 debug!(user_id = ?session_meta.user_id, device_id = ?session_meta.device_id, "Activating the client");
358
359 self.state_store.load_rooms(&session_meta.user_id, room_load_settings).await?;
360 self.state_store.load_sync_token().await?;
361 self.state_store.set_session_meta(session_meta);
362
363 #[cfg(feature = "e2e-encryption")]
364 self.regenerate_olm(custom_account).await?;
365
366 Ok(())
367 }
368
369 #[cfg(feature = "e2e-encryption")]
373 pub async fn regenerate_olm(
374 &self,
375 custom_account: Option<crate::crypto::vodozemac::olm::Account>,
376 ) -> Result<()> {
377 tracing::debug!("regenerating OlmMachine");
378 let session_meta = self.session_meta().ok_or(Error::OlmError(OlmError::MissingSession))?;
379
380 let olm_machine = OlmMachine::with_store(
383 &session_meta.user_id,
384 &session_meta.device_id,
385 self.crypto_store.clone(),
386 custom_account,
387 )
388 .await
389 .map_err(OlmError::from)?;
390
391 *self.olm_machine.write().await = Some(olm_machine);
392 Ok(())
393 }
394
395 pub async fn sync_token(&self) -> Option<String> {
398 self.state_store.sync_token.read().await.clone()
399 }
400
401 pub async fn room_knocked(&self, room_id: &RoomId) -> Result<Room> {
405 let room = self.state_store.get_or_create_room(room_id, RoomState::Knocked);
406
407 if room.state() != RoomState::Knocked {
408 let store_guard = self.state_store.lock().lock().await;
409
410 #[cfg(feature = "e2e-encryption")]
413 if let Some(olm_machine) = self.olm_machine().await.as_ref() {
414 olm_machine.store().clear_room_pending_key_bundle(room_id).await?
415 }
416
417 room.update_and_save_room_info_with_store_guard(&store_guard, |mut info| {
418 info.mark_as_knocked();
419 info.mark_state_partially_synced();
420 info.mark_members_missing(); (info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
422 })
423 .await?;
424 }
425
426 Ok(room)
427 }
428
429 pub async fn room_joined(
473 &self,
474 room_id: &RoomId,
475 inviter: Option<OwnedUserId>,
476 ) -> Result<Room> {
477 let room = self.state_store.get_or_create_room(room_id, RoomState::Joined);
478
479 if room.state() != RoomState::Joined {
482 let store_guard = self.state_store_lock().lock().await;
483
484 #[cfg(feature = "e2e-encryption")]
485 {
486 let previous_state = room.state();
497 if previous_state == RoomState::Invited
498 && let Some(inviter) = inviter
499 && let Some(olm_machine) = self.olm_machine().await.as_ref()
500 {
501 olm_machine.store().store_room_pending_key_bundle(room_id, &inviter).await?
502 }
503 }
504 #[cfg(not(feature = "e2e-encryption"))]
505 {
506 let _ = inviter;
508 }
509
510 room.update_and_save_room_info_with_store_guard(&store_guard, |mut info| {
511 info.mark_as_joined();
512 info.mark_state_partially_synced();
513 info.mark_members_missing(); (info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
515 })
516 .await?;
517 }
518
519 Ok(room)
520 }
521
522 pub async fn room_left(&self, room_id: &RoomId) -> Result<()> {
526 let room = self.state_store.get_or_create_room(room_id, RoomState::Left);
527
528 if room.state() != RoomState::Left {
529 let store_guard = self.state_store.lock().lock().await;
530
531 #[cfg(feature = "e2e-encryption")]
534 if let Some(olm_machine) = self.olm_machine().await.as_ref() {
535 olm_machine.store().clear_room_pending_key_bundle(room_id).await?
536 }
537
538 room.update_and_save_room_info_with_store_guard(&store_guard, |mut info| {
539 info.mark_as_left();
540 info.mark_state_partially_synced();
541 info.mark_members_missing(); (info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
543 })
544 .await?;
545 }
546
547 Ok(())
548 }
549
550 pub fn state_store_lock(&self) -> &Mutex<()> {
555 self.state_store.lock()
556 }
557
558 #[instrument(skip_all)]
564 pub async fn receive_sync_response(
565 &self,
566 response: api::sync::sync_events::v3::Response,
567 ) -> Result<SyncResponse> {
568 self.receive_sync_response_with_requested_required_states(
569 response,
570 &RequestedRequiredStates::default(),
571 )
572 .await
573 }
574
575 pub async fn receive_sync_response_with_requested_required_states(
583 &self,
584 response: api::sync::sync_events::v3::Response,
585 requested_required_states: &RequestedRequiredStates,
586 ) -> Result<SyncResponse> {
587 if self.state_store.sync_token.read().await.as_ref() == Some(&response.next_batch) {
591 info!("Got the same sync response twice");
592 return Ok(SyncResponse::default());
593 }
594
595 let now = if enabled!(Level::INFO) { Some(Instant::now()) } else { None };
596
597 let user_id = self
598 .session_meta()
599 .expect("Sync shouldn't run without an authenticated user")
600 .user_id
601 .to_owned();
602
603 #[cfg(feature = "e2e-encryption")]
604 let olm_machine = self.olm_machine().await;
605
606 let mut context = Context::new(StateChanges::new(response.next_batch.clone()));
607
608 #[cfg(feature = "e2e-encryption")]
609 let processors::e2ee::to_device::Output { processed_to_device_events: to_device } =
610 processors::e2ee::to_device::from_sync_v2(
611 &response,
612 olm_machine.as_ref(),
613 &self.decryption_settings,
614 )
615 .await?;
616
617 #[cfg(not(feature = "e2e-encryption"))]
618 let to_device = response
619 .to_device
620 .events
621 .into_iter()
622 .map(|raw| {
623 use matrix_sdk_common::deserialized_responses::{
624 ProcessedToDeviceEvent, ToDeviceUnableToDecryptInfo,
625 ToDeviceUnableToDecryptReason,
626 };
627
628 if let Ok(Some(event_type)) = raw.get_field::<String>("type") {
629 if event_type == "m.room.encrypted" {
630 ProcessedToDeviceEvent::UnableToDecrypt {
631 encrypted_event: raw,
632 utd_info: ToDeviceUnableToDecryptInfo {
633 reason: ToDeviceUnableToDecryptReason::EncryptionIsDisabled,
634 },
635 }
636 } else {
637 ProcessedToDeviceEvent::PlainText(raw)
638 }
639 } else {
640 ProcessedToDeviceEvent::Invalid(raw)
642 }
643 })
644 .collect();
645
646 let mut ambiguity_cache = AmbiguityCache::new(self.state_store.inner.clone());
647
648 let global_account_data_processor =
649 processors::account_data::global(&response.account_data.events);
650
651 let push_rules = self.get_push_rules(&global_account_data_processor).await?;
652
653 let mut room_updates = RoomUpdates::default();
654 let mut notifications = Default::default();
655
656 let mut updated_members_in_room: BTreeMap<OwnedRoomId, BTreeSet<OwnedUserId>> =
657 BTreeMap::new();
658
659 #[cfg(feature = "e2e-encryption")]
660 let e2ee_context = processors::e2ee::E2EE::new(
661 olm_machine.as_ref(),
662 &self.decryption_settings,
663 self.handle_verification_events,
664 );
665
666 for (room_id, joined_room) in response.rooms.join {
667 let joined_room_update = processors::room::sync_v2::update_joined_room(
668 &mut context,
669 processors::room::RoomCreationData::new(
670 &room_id,
671 requested_required_states,
672 &mut ambiguity_cache,
673 ),
674 joined_room,
675 &mut updated_members_in_room,
676 processors::notification::Notification::new(
677 &push_rules,
678 &mut notifications,
679 &self.state_store,
680 ),
681 #[cfg(feature = "e2e-encryption")]
682 &e2ee_context,
683 )
684 .await?;
685
686 room_updates.joined.insert(room_id, joined_room_update);
687 }
688
689 for (room_id, left_room) in response.rooms.leave {
690 let left_room_update = processors::room::sync_v2::update_left_room(
691 &mut context,
692 processors::room::RoomCreationData::new(
693 &room_id,
694 requested_required_states,
695 &mut ambiguity_cache,
696 ),
697 left_room,
698 processors::notification::Notification::new(
699 &push_rules,
700 &mut notifications,
701 &self.state_store,
702 ),
703 #[cfg(feature = "e2e-encryption")]
704 &e2ee_context,
705 )
706 .await?;
707
708 room_updates.left.insert(room_id, left_room_update);
709 }
710
711 for (room_id, invited_room) in response.rooms.invite {
712 let invited_room_update = processors::room::sync_v2::update_invited_room(
713 &mut context,
714 &room_id,
715 &user_id,
716 invited_room,
717 processors::notification::Notification::new(
718 &push_rules,
719 &mut notifications,
720 &self.state_store,
721 ),
722 #[cfg(feature = "e2e-encryption")]
723 &e2ee_context,
724 )
725 .await?;
726
727 room_updates.invited.insert(room_id, invited_room_update);
728 }
729
730 for (room_id, knocked_room) in response.rooms.knock {
731 let knocked_room_update = processors::room::sync_v2::update_knocked_room(
732 &mut context,
733 &room_id,
734 &user_id,
735 knocked_room,
736 processors::notification::Notification::new(
737 &push_rules,
738 &mut notifications,
739 &self.state_store,
740 ),
741 #[cfg(feature = "e2e-encryption")]
742 &e2ee_context,
743 )
744 .await?;
745
746 room_updates.knocked.insert(room_id, knocked_room_update);
747 }
748
749 global_account_data_processor.apply(&mut context, &self.state_store).await;
750
751 context.state_changes.presence = response
752 .presence
753 .events
754 .iter()
755 .filter_map(|e| {
756 let event = e.deserialize().ok()?;
757 Some((event.sender, e.clone()))
758 })
759 .collect();
760
761 context.state_changes.ambiguity_maps = ambiguity_cache.cache;
762
763 processors::changes::save_and_apply(
764 context,
765 &self.state_store,
766 &self.state_store_lock().lock().await,
767 &self.ignore_user_list_changes,
768 Some(response.next_batch.clone()),
769 )
770 .await?;
771
772 let mut context = Context::default();
773
774 processors::room::display_name::update_for_rooms(
777 &mut context,
778 &room_updates,
779 &self.state_store,
780 )
781 .await;
782
783 processors::changes::save_only(
785 context,
786 &self.state_store,
787 &self.state_store_lock().lock().await,
788 )
789 .await?;
790
791 for (room_id, member_ids) in updated_members_in_room {
792 if let Some(room) = self.get_room(&room_id) {
793 let _ =
794 room.room_member_updates_sender.send(RoomMembersUpdate::Partial(member_ids));
795 }
796 }
797
798 if enabled!(Level::INFO) {
799 info!("Processed a sync response in {:?}", now.map(|now| now.elapsed()));
800 }
801
802 let response = SyncResponse {
803 rooms: room_updates,
804 presence: response.presence.events,
805 account_data: response.account_data.events,
806 to_device,
807 notifications,
808 };
809
810 Ok(response)
811 }
812
813 #[instrument(skip_all, fields(?room_id))]
825 pub async fn receive_all_members(
826 &self,
827 room_id: &RoomId,
828 request: &api::membership::get_member_events::v3::Request,
829 response: &api::membership::get_member_events::v3::Response,
830 ) -> Result<()> {
831 if request.membership.is_some() || request.not_membership.is_some() || request.at.is_some()
832 {
833 return Err(Error::InvalidReceiveMembersParameters);
837 }
838
839 let Some(room) = self.state_store.room(room_id) else {
840 return Ok(());
842 };
843
844 let mut chunk = Vec::with_capacity(response.chunk.len());
845 let mut context = Context::default();
846
847 #[cfg(feature = "e2e-encryption")]
848 let mut user_ids = BTreeSet::new();
849
850 let mut ambiguity_map: HashMap<DisplayName, BTreeSet<OwnedUserId>> = Default::default();
851
852 for raw_event in &response.chunk {
853 let member = match raw_event.deserialize() {
854 Ok(ev) => ev,
855 Err(e) => {
856 let event_id: Option<String> = raw_event.get_field("event_id").ok().flatten();
857 debug!(event_id, "Failed to deserialize member event: {e}");
858 continue;
859 }
860 };
861
862 #[cfg(feature = "e2e-encryption")]
872 match member.membership() {
873 MembershipState::Join | MembershipState::Invite => {
874 user_ids.insert(member.state_key().to_owned());
875 }
876 _ => (),
877 }
878
879 if let StateEvent::Original(e) = &member
880 && let Some(d) = &e.content.displayname
881 {
882 let display_name = DisplayName::new(d);
883 ambiguity_map.entry(display_name).or_default().insert(member.state_key().clone());
884 }
885
886 let sync_member: SyncRoomMemberEvent = member.clone().into();
887 processors::profiles::upsert_or_delete(&mut context, room_id, &sync_member);
888
889 context
890 .state_changes
891 .state
892 .entry(room_id.to_owned())
893 .or_default()
894 .entry(member.event_type())
895 .or_default()
896 .insert(member.state_key().to_string(), raw_event.clone().cast());
897 chunk.push(member);
898 }
899
900 #[cfg(feature = "e2e-encryption")]
901 processors::e2ee::tracked_users::update(
902 self.olm_machine().await.as_ref(),
903 room.encryption_state(),
904 &user_ids,
905 )
906 .await?;
907
908 context.state_changes.ambiguity_maps.insert(room_id.to_owned(), ambiguity_map);
909
910 {
911 let state_store_guard = self.state_store_lock().lock().await;
912
913 let mut room_info = room.clone_info();
914 room_info.mark_members_synced();
915 context.state_changes.add_room(room_info);
916
917 processors::changes::save_and_apply(
918 context,
919 &self.state_store,
920 &state_store_guard,
921 &self.ignore_user_list_changes,
922 None,
923 )
924 .await?;
925 }
926
927 let _ = room.room_member_updates_sender.send(RoomMembersUpdate::FullReload);
928
929 #[cfg(feature = "e2e-encryption")]
930 if let Some(olm) = self.olm_machine().await.as_ref() {
931 tracing::debug!("Rotating room key due to full member list reload");
936 if let Err(e) = olm.discard_room_key(room_id).await {
937 tracing::warn!("Error discarding room key: {e:?}");
938 }
939 }
940
941 Ok(())
942 }
943
944 pub async fn receive_filter_upload(
960 &self,
961 filter_name: &str,
962 response: &api::filter::create_filter::v3::Response,
963 ) -> Result<()> {
964 Ok(self
965 .state_store
966 .set_kv_data(
967 StateStoreDataKey::Filter(filter_name),
968 StateStoreDataValue::Filter(response.filter_id.clone()),
969 )
970 .await?)
971 }
972
973 pub async fn get_filter(&self, filter_name: &str) -> StoreResult<Option<String>> {
985 let filter = self
986 .state_store
987 .get_kv_data(StateStoreDataKey::Filter(filter_name))
988 .await?
989 .map(|d| d.into_filter().expect("State store data not a filter"));
990
991 Ok(filter)
992 }
993
994 #[cfg(feature = "e2e-encryption")]
996 pub async fn share_room_key(&self, room_id: &RoomId) -> Result<Vec<Arc<ToDeviceRequest>>> {
997 match self.olm_machine().await.as_ref() {
998 Some(o) => {
999 let Some(room) = self.get_room(room_id) else {
1000 return Err(Error::InsufficientData);
1001 };
1002
1003 let history_visibility = room.history_visibility_or_default();
1004 let Some(room_encryption_event) = room.encryption_settings() else {
1005 return Err(Error::EncryptionNotEnabled);
1006 };
1007
1008 let filter = if history_visibility == HistoryVisibility::Joined {
1011 RoomMemberships::JOIN
1012 } else {
1013 RoomMemberships::ACTIVE
1014 };
1015
1016 let members = self.state_store.get_user_ids(room_id, filter).await?;
1017
1018 let Some(settings) = EncryptionSettings::from_possibly_redacted(
1019 room_encryption_event,
1020 history_visibility,
1021 self.room_key_recipient_strategy.clone(),
1022 ) else {
1023 return Err(Error::EncryptionNotEnabled);
1024 };
1025
1026 Ok(o.share_room_key(room_id, members.iter().map(Deref::deref), settings).await?)
1027 }
1028 None => panic!("Olm machine wasn't started"),
1029 }
1030 }
1031
1032 pub fn get_room(&self, room_id: &RoomId) -> Option<Room> {
1038 self.state_store.room(room_id)
1039 }
1040
1041 pub async fn forget_room(&self, room_id: &RoomId) -> Result<()> {
1049 self.state_store.forget_room(room_id).await?;
1051
1052 match self.event_cache_store().lock().await? {
1054 EventCacheStoreLockState::Clean(guard) | EventCacheStoreLockState::Dirty(guard) => {
1059 guard.remove_room(room_id).await?
1060 }
1061 }
1062
1063 Ok(())
1064 }
1065
1066 #[cfg(feature = "e2e-encryption")]
1068 pub async fn olm_machine(&self) -> RwLockReadGuard<'_, Option<OlmMachine>> {
1069 self.olm_machine.read().await
1070 }
1071
1072 pub(crate) async fn get_push_rules(
1078 &self,
1079 global_account_data_processor: &processors::account_data::Global,
1080 ) -> Result<Ruleset> {
1081 let _timer = timer!(Level::TRACE, "get_push_rules");
1082 if let Some(event) = global_account_data_processor
1083 .push_rules()
1084 .and_then(|ev| ev.deserialize_as_unchecked::<PushRulesEvent>().ok())
1085 {
1086 Ok(event.content.global)
1087 } else if let Some(event) = self
1088 .state_store
1089 .get_account_data_event_static::<PushRulesEventContent>()
1090 .await?
1091 .and_then(|ev| ev.deserialize().ok())
1092 {
1093 Ok(event.content.global)
1094 } else if let Some(session_meta) = self.state_store.session_meta() {
1095 Ok(Ruleset::server_default(&session_meta.user_id))
1096 } else {
1097 Ok(Ruleset::new())
1098 }
1099 }
1100
1101 pub fn subscribe_to_ignore_user_list_changes(&self) -> Subscriber<Vec<String>> {
1104 self.ignore_user_list_changes.subscribe()
1105 }
1106
1107 pub fn room_info_notable_update_receiver(&self) -> broadcast::Receiver<RoomInfoNotableUpdate> {
1111 self.state_store.room_info_notable_update_sender.subscribe()
1112 }
1113
1114 pub async fn is_user_ignored(&self, user_id: &UserId) -> bool {
1116 match self.state_store.get_account_data_event_static::<IgnoredUserListEventContent>().await
1117 {
1118 Ok(Some(raw_ignored_user_list)) => match raw_ignored_user_list.deserialize() {
1119 Ok(current_ignored_user_list) => {
1120 current_ignored_user_list.content.ignored_users.contains_key(user_id)
1121 }
1122 Err(error) => {
1123 warn!(?error, "Failed to deserialize the ignored user list event");
1124 false
1125 }
1126 },
1127 Ok(None) => false,
1128 Err(error) => {
1129 warn!(?error, "Could not get the ignored user list from the state store");
1130 false
1131 }
1132 }
1133 }
1134
1135 #[cfg(feature = "e2e-encryption")]
1140 pub async fn get_pending_key_bundle_details_for_room(
1141 &self,
1142 room_id: &RoomId,
1143 ) -> Result<Option<RoomPendingKeyBundleDetails>> {
1144 let result = match self.olm_machine().await.as_ref() {
1145 Some(machine) => {
1146 machine.store().get_pending_key_bundle_details_for_room(room_id).await?
1147 }
1148 None => None,
1149 };
1150 Ok(result)
1151 }
1152}
1153
1154#[derive(Debug, Default)]
1166pub struct RequestedRequiredStates {
1167 default: Vec<(StateEventType, String)>,
1168 for_rooms: HashMap<OwnedRoomId, Vec<(StateEventType, String)>>,
1169}
1170
1171impl RequestedRequiredStates {
1172 pub fn new(
1177 default: Vec<(StateEventType, String)>,
1178 for_rooms: HashMap<OwnedRoomId, Vec<(StateEventType, String)>>,
1179 ) -> Self {
1180 Self { default, for_rooms }
1181 }
1182
1183 pub fn for_room(&self, room_id: &RoomId) -> &[(StateEventType, String)] {
1185 self.for_rooms.get(room_id).unwrap_or(&self.default)
1186 }
1187}
1188
1189impl From<&v5::Request> for RequestedRequiredStates {
1190 fn from(request: &v5::Request) -> Self {
1191 let mut default = BTreeSet::new();
1198
1199 for list in request.lists.values() {
1200 default.extend(BTreeSet::from_iter(list.room_details.required_state.iter().cloned()));
1201 }
1202
1203 for room_subscription in request.room_subscriptions.values() {
1204 default.extend(BTreeSet::from_iter(room_subscription.required_state.iter().cloned()));
1205 }
1206
1207 Self { default: default.into_iter().collect(), for_rooms: HashMap::new() }
1208 }
1209}
1210
1211#[derive(Debug, Clone, Default)]
1213#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1214pub enum DmRoomDefinition {
1215 #[default]
1218 MatrixSpec,
1219 TwoMembers,
1222}
1223
1224#[cfg(test)]
1225mod tests {
1226 use std::collections::HashMap;
1227
1228 use assert_matches2::assert_let;
1229 #[cfg(feature = "e2e-encryption")]
1230 use assert_matches2::assert_matches;
1231 use futures_util::FutureExt as _;
1232 use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;
1233 use matrix_sdk_test::{
1234 BOB, InvitedRoomBuilder, LeftRoomBuilder, SyncResponseBuilder, async_test,
1235 event_factory::EventFactory, ruma_response_from_json,
1236 };
1237 use ruma::{
1238 api::client::{self as api, sync::sync_events::v5},
1239 event_id,
1240 events::{StateEventType, room::member::MembershipState},
1241 room_id,
1242 serde::Raw,
1243 user_id,
1244 };
1245 use serde_json::{json, value::to_raw_value};
1246
1247 use super::{BaseClient, RequestedRequiredStates};
1248 use crate::{
1249 DmRoomDefinition, RoomDisplayName, RoomState, SessionMeta,
1250 client::ThreadingSupport,
1251 store::{RoomLoadSettings, StateStoreExt, StoreConfig},
1252 test_utils::logged_in_base_client,
1253 };
1254
1255 #[test]
1256 fn test_requested_required_states() {
1257 let room_id_0 = room_id!("!r0");
1258 let room_id_1 = room_id!("!r1");
1259
1260 let requested_required_states = RequestedRequiredStates::new(
1261 vec![(StateEventType::RoomAvatar, "".to_owned())],
1262 HashMap::from([(
1263 room_id_0.to_owned(),
1264 vec![
1265 (StateEventType::RoomMember, "foo".to_owned()),
1266 (StateEventType::RoomEncryption, "".to_owned()),
1267 ],
1268 )]),
1269 );
1270
1271 assert_eq!(
1273 requested_required_states.for_room(room_id_0),
1274 &[
1275 (StateEventType::RoomMember, "foo".to_owned()),
1276 (StateEventType::RoomEncryption, "".to_owned()),
1277 ]
1278 );
1279
1280 assert_eq!(
1282 requested_required_states.for_room(room_id_1),
1283 &[(StateEventType::RoomAvatar, "".to_owned()),]
1284 );
1285 }
1286
1287 #[test]
1288 fn test_requested_required_states_from_sync_v5_request() {
1289 let room_id_0 = room_id!("!r0");
1290 let room_id_1 = room_id!("!r1");
1291
1292 let mut request = v5::Request::new();
1294
1295 {
1296 let requested_required_states = RequestedRequiredStates::from(&request);
1297
1298 assert!(requested_required_states.default.is_empty());
1299 assert!(requested_required_states.for_rooms.is_empty());
1300 }
1301
1302 request.lists.insert("foo".to_owned(), {
1304 let mut list = v5::request::List::default();
1305 list.room_details.required_state = vec![
1306 (StateEventType::RoomAvatar, "".to_owned()),
1307 (StateEventType::RoomEncryption, "".to_owned()),
1308 ];
1309
1310 list
1311 });
1312
1313 {
1314 let requested_required_states = RequestedRequiredStates::from(&request);
1315
1316 assert_eq!(
1317 requested_required_states.default,
1318 &[
1319 (StateEventType::RoomAvatar, "".to_owned()),
1320 (StateEventType::RoomEncryption, "".to_owned())
1321 ]
1322 );
1323 assert!(requested_required_states.for_rooms.is_empty());
1324 }
1325
1326 request.lists.insert("bar".to_owned(), {
1328 let mut list = v5::request::List::default();
1329 list.room_details.required_state = vec![
1330 (StateEventType::RoomEncryption, "".to_owned()),
1331 (StateEventType::RoomName, "".to_owned()),
1332 ];
1333
1334 list
1335 });
1336
1337 {
1338 let requested_required_states = RequestedRequiredStates::from(&request);
1339
1340 assert_eq!(
1342 requested_required_states.default,
1343 &[
1344 (StateEventType::RoomAvatar, "".to_owned()),
1345 (StateEventType::RoomEncryption, "".to_owned()),
1346 (StateEventType::RoomName, "".to_owned()),
1347 ]
1348 );
1349 assert!(requested_required_states.for_rooms.is_empty());
1350 }
1351
1352 request.room_subscriptions.insert(room_id_0.to_owned(), {
1354 let mut room_subscription = v5::request::RoomSubscription::default();
1355
1356 room_subscription.required_state = vec![
1357 (StateEventType::RoomJoinRules, "".to_owned()),
1358 (StateEventType::RoomEncryption, "".to_owned()),
1359 ];
1360
1361 room_subscription
1362 });
1363
1364 {
1365 let requested_required_states = RequestedRequiredStates::from(&request);
1366
1367 assert_eq!(
1369 requested_required_states.default,
1370 &[
1371 (StateEventType::RoomAvatar, "".to_owned()),
1372 (StateEventType::RoomEncryption, "".to_owned()),
1373 (StateEventType::RoomJoinRules, "".to_owned()),
1374 (StateEventType::RoomName, "".to_owned()),
1375 ]
1376 );
1377 assert!(requested_required_states.for_rooms.is_empty());
1378 }
1379
1380 request.room_subscriptions.insert(room_id_1.to_owned(), {
1382 let mut room_subscription = v5::request::RoomSubscription::default();
1383
1384 room_subscription.required_state = vec![
1385 (StateEventType::RoomName, "".to_owned()),
1386 (StateEventType::RoomTopic, "".to_owned()),
1387 ];
1388
1389 room_subscription
1390 });
1391
1392 {
1393 let requested_required_states = RequestedRequiredStates::from(&request);
1394
1395 assert_eq!(
1397 requested_required_states.default,
1398 &[
1399 (StateEventType::RoomAvatar, "".to_owned()),
1400 (StateEventType::RoomEncryption, "".to_owned()),
1401 (StateEventType::RoomJoinRules, "".to_owned()),
1402 (StateEventType::RoomName, "".to_owned()),
1403 (StateEventType::RoomTopic, "".to_owned()),
1404 ]
1405 );
1406 }
1407 }
1408
1409 #[async_test]
1410 async fn test_invite_after_leaving() {
1411 let user_id = user_id!("@alice:example.org");
1412 let room_id = room_id!("!test:example.org");
1413
1414 let client = logged_in_base_client(Some(user_id)).await;
1415 let f = EventFactory::new();
1416
1417 let mut sync_builder = SyncResponseBuilder::new();
1418
1419 let response = sync_builder
1420 .add_left_room(
1421 LeftRoomBuilder::new(room_id).add_timeline_event(
1422 EventFactory::new()
1423 .member(user_id)
1424 .membership(MembershipState::Leave)
1425 .display_name("Alice")
1426 .event_id(event_id!("$994173582443PhrSn:example.org")),
1427 ),
1428 )
1429 .build_sync_response();
1430 client.receive_sync_response(response).await.unwrap();
1431 assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left);
1432
1433 let response = sync_builder
1434 .add_invited_room(
1435 InvitedRoomBuilder::new(room_id).add_state_event(
1436 f.member(user_id)
1437 .sender(user_id!("@example:example.org"))
1438 .membership(MembershipState::Invite)
1439 .display_name("Alice"),
1440 ),
1441 )
1442 .build_sync_response();
1443 client.receive_sync_response(response).await.unwrap();
1444 assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Invited);
1445 }
1446
1447 #[async_test]
1448 async fn test_invite_displayname() {
1449 let user_id = user_id!("@alice:example.org");
1450 let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
1451
1452 let client = logged_in_base_client(Some(user_id)).await;
1453
1454 let response = ruma_response_from_json(&json!({
1455 "next_batch": "asdkl;fjasdkl;fj;asdkl;f",
1456 "device_one_time_keys_count": {
1457 "signed_curve25519": 50u64
1458 },
1459 "device_unused_fallback_key_types": [
1460 "signed_curve25519"
1461 ],
1462 "rooms": {
1463 "invite": {
1464 "!ithpyNKDtmhneaTQja:example.org": {
1465 "invite_state": {
1466 "events": [
1467 {
1468 "content": {
1469 "creator": "@test:example.org",
1470 "room_version": "9"
1471 },
1472 "sender": "@test:example.org",
1473 "state_key": "",
1474 "type": "m.room.create"
1475 },
1476 {
1477 "content": {
1478 "join_rule": "invite"
1479 },
1480 "sender": "@test:example.org",
1481 "state_key": "",
1482 "type": "m.room.join_rules"
1483 },
1484 {
1485 "content": {
1486 "algorithm": "m.megolm.v1.aes-sha2"
1487 },
1488 "sender": "@test:example.org",
1489 "state_key": "",
1490 "type": "m.room.encryption"
1491 },
1492 {
1493 "content": {
1494 "avatar_url": "mxc://example.org/dcBBDwuWEUrjfrOchvkirUST",
1495 "displayname": "Kyra",
1496 "membership": "join"
1497 },
1498 "sender": "@test:example.org",
1499 "state_key": "@test:example.org",
1500 "type": "m.room.member"
1501 },
1502 {
1503 "content": {
1504 "avatar_url": "mxc://example.org/ABFEXSDrESxovWwEnCYdNcHT",
1505 "displayname": "alice",
1506 "is_direct": true,
1507 "membership": "invite"
1508 },
1509 "origin_server_ts": 1650878657984u64,
1510 "sender": "@test:example.org",
1511 "state_key": "@alice:example.org",
1512 "type": "m.room.member",
1513 "unsigned": {
1514 "age": 14u64
1515 },
1516 "event_id": "$fLDqltg9Puj-kWItLSFVHPGN4YkgpYQf2qImPzdmgrE"
1517 }
1518 ]
1519 }
1520 }
1521 }
1522 }
1523 }));
1524
1525 client.receive_sync_response(response).await.unwrap();
1526
1527 let room = client.get_room(room_id).expect("Room not found");
1528 assert_eq!(room.state(), RoomState::Invited);
1529 assert_eq!(
1530 room.compute_display_name().await.expect("fetching display name failed").into_inner(),
1531 RoomDisplayName::Calculated("Kyra".to_owned())
1532 );
1533 }
1534
1535 #[async_test]
1536 async fn test_deserialization_failure() {
1537 let user_id = user_id!("@alice:example.org");
1538 let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
1539
1540 let client = BaseClient::new(
1541 StoreConfig::new(CrossProcessLockConfig::SingleProcess),
1542 ThreadingSupport::Disabled,
1543 DmRoomDefinition::default(),
1544 );
1545 client
1546 .activate(
1547 SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
1548 RoomLoadSettings::default(),
1549 #[cfg(feature = "e2e-encryption")]
1550 None,
1551 )
1552 .await
1553 .unwrap();
1554
1555 let response = ruma_response_from_json(&json!({
1556 "next_batch": "asdkl;fjasdkl;fj;asdkl;f",
1557 "rooms": {
1558 "join": {
1559 "!ithpyNKDtmhneaTQja:example.org": {
1560 "state": {
1561 "events": [
1562 {
1563 "invalid": "invalid",
1564 },
1565 {
1566 "content": {
1567 "name": "The room name"
1568 },
1569 "event_id": "$143273582443PhrSn:example.org",
1570 "origin_server_ts": 1432735824653u64,
1571 "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
1572 "sender": "@example:example.org",
1573 "state_key": "",
1574 "type": "m.room.name",
1575 "unsigned": {
1576 "age": 1234
1577 }
1578 },
1579 ]
1580 }
1581 }
1582 }
1583 }
1584 }));
1585
1586 client.receive_sync_response(response).await.unwrap();
1587 client
1588 .state_store()
1589 .get_state_event_static::<ruma::events::room::name::RoomNameEventContent>(room_id)
1590 .await
1591 .expect("Failed to fetch state event")
1592 .expect("State event not found")
1593 .deserialize()
1594 .expect("Failed to deserialize state event");
1595 }
1596
1597 #[async_test]
1598 async fn test_invited_members_arent_ignored() {
1599 let user_id = user_id!("@alice:example.org");
1600 let inviter_user_id = user_id!("@bob:example.org");
1601 let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
1602
1603 let client = BaseClient::new(
1604 StoreConfig::new(CrossProcessLockConfig::SingleProcess),
1605 ThreadingSupport::Disabled,
1606 DmRoomDefinition::default(),
1607 );
1608 client
1609 .activate(
1610 SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
1611 RoomLoadSettings::default(),
1612 #[cfg(feature = "e2e-encryption")]
1613 None,
1614 )
1615 .await
1616 .unwrap();
1617
1618 let mut sync_builder = SyncResponseBuilder::new();
1620 let response = sync_builder
1621 .add_joined_room(matrix_sdk_test::JoinedRoomBuilder::new(room_id))
1622 .build_sync_response();
1623 client.receive_sync_response(response).await.unwrap();
1624
1625 let request = api::membership::get_member_events::v3::Request::new(room_id.to_owned());
1628
1629 let raw_member_event = json!({
1630 "content": {
1631 "avatar_url": "mxc://localhost/fewjilfewjil42",
1632 "displayname": "Invited Alice",
1633 "membership": "invite"
1634 },
1635 "event_id": "$151800140517rfvjc:localhost",
1636 "origin_server_ts": 151800140,
1637 "room_id": room_id,
1638 "sender": inviter_user_id,
1639 "state_key": user_id,
1640 "type": "m.room.member",
1641 "unsigned": {
1642 "age": 13374242,
1643 }
1644 });
1645 let response = api::membership::get_member_events::v3::Response::new(vec![Raw::from_json(
1646 to_raw_value(&raw_member_event).unwrap(),
1647 )]);
1648
1649 client.receive_all_members(room_id, &request, &response).await.unwrap();
1651
1652 let room = client.get_room(room_id).unwrap();
1653
1654 let member = room.get_member(user_id).await.expect("ok").expect("exists");
1656
1657 assert_eq!(member.user_id(), user_id);
1658 assert_eq!(member.display_name().unwrap(), "Invited Alice");
1659 assert_eq!(member.avatar_url().unwrap().to_string(), "mxc://localhost/fewjilfewjil42");
1660 }
1661
1662 #[async_test]
1663 async fn test_reinvited_members_get_a_display_name() {
1664 let user_id = user_id!("@alice:example.org");
1665 let inviter_user_id = user_id!("@bob:example.org");
1666 let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
1667
1668 let client = BaseClient::new(
1669 StoreConfig::new(CrossProcessLockConfig::SingleProcess),
1670 ThreadingSupport::Disabled,
1671 DmRoomDefinition::default(),
1672 );
1673 client
1674 .activate(
1675 SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
1676 RoomLoadSettings::default(),
1677 #[cfg(feature = "e2e-encryption")]
1678 None,
1679 )
1680 .await
1681 .unwrap();
1682
1683 let f = EventFactory::new().sender(user_id);
1685 let mut sync_builder = SyncResponseBuilder::new();
1686 let response = sync_builder
1687 .add_joined_room(
1688 matrix_sdk_test::JoinedRoomBuilder::new(room_id)
1689 .add_state_event(f.member(user_id).leave()),
1690 )
1691 .build_sync_response();
1692 client.receive_sync_response(response).await.unwrap();
1693
1694 let request = api::membership::get_member_events::v3::Request::new(room_id.to_owned());
1696
1697 let raw_member_event = json!({
1698 "content": {
1699 "avatar_url": "mxc://localhost/fewjilfewjil42",
1700 "displayname": "Invited Alice",
1701 "membership": "invite"
1702 },
1703 "event_id": "$151800140517rfvjc:localhost",
1704 "origin_server_ts": 151800140,
1705 "room_id": room_id,
1706 "sender": inviter_user_id,
1707 "state_key": user_id,
1708 "type": "m.room.member",
1709 "unsigned": {
1710 "age": 13374242,
1711 }
1712 });
1713 let response = api::membership::get_member_events::v3::Response::new(vec![Raw::from_json(
1714 to_raw_value(&raw_member_event).unwrap(),
1715 )]);
1716
1717 client.receive_all_members(room_id, &request, &response).await.unwrap();
1719
1720 let room = client.get_room(room_id).unwrap();
1721
1722 let member = room.get_member(user_id).await.expect("ok").expect("exists");
1724
1725 assert_eq!(member.user_id(), user_id);
1726 assert_eq!(member.display_name().unwrap(), "Invited Alice");
1727 assert_eq!(member.avatar_url().unwrap().to_string(), "mxc://localhost/fewjilfewjil42");
1728 }
1729
1730 #[async_test]
1731 async fn test_ignored_user_list_changes() {
1732 let user_id = user_id!("@alice:example.org");
1733 let client = BaseClient::new(
1734 StoreConfig::new(CrossProcessLockConfig::SingleProcess),
1735 ThreadingSupport::Disabled,
1736 DmRoomDefinition::default(),
1737 );
1738
1739 client
1740 .activate(
1741 SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
1742 RoomLoadSettings::default(),
1743 #[cfg(feature = "e2e-encryption")]
1744 None,
1745 )
1746 .await
1747 .unwrap();
1748
1749 let mut subscriber = client.subscribe_to_ignore_user_list_changes();
1750 assert!(subscriber.next().now_or_never().is_none());
1751
1752 let f = EventFactory::new();
1753 let mut sync_builder = SyncResponseBuilder::new();
1754 let response = sync_builder
1755 .add_global_account_data(f.ignored_user_list([(*BOB).into()]))
1756 .build_sync_response();
1757 client.receive_sync_response(response).await.unwrap();
1758
1759 assert_let!(Some(ignored) = subscriber.next().await);
1760 assert_eq!(ignored, [BOB.to_string()]);
1761
1762 let response = sync_builder
1764 .add_global_account_data(f.ignored_user_list([(*BOB).into()]))
1765 .build_sync_response();
1766 client.receive_sync_response(response).await.unwrap();
1767
1768 assert!(subscriber.next().now_or_never().is_none());
1770
1771 let response =
1773 sync_builder.add_global_account_data(f.ignored_user_list([])).build_sync_response();
1774 client.receive_sync_response(response).await.unwrap();
1775
1776 assert_let!(Some(ignored) = subscriber.next().await);
1777 assert!(ignored.is_empty());
1778 }
1779
1780 #[async_test]
1781 async fn test_is_user_ignored() {
1782 let ignored_user_id = user_id!("@alice:example.org");
1783 let client = logged_in_base_client(None).await;
1784
1785 let mut sync_builder = SyncResponseBuilder::new();
1786 let f = EventFactory::new();
1787 let response = sync_builder
1788 .add_global_account_data(f.ignored_user_list([ignored_user_id.to_owned()]))
1789 .build_sync_response();
1790 client.receive_sync_response(response).await.unwrap();
1791
1792 assert!(client.is_user_ignored(ignored_user_id).await);
1793 }
1794
1795 #[cfg(feature = "e2e-encryption")]
1796 #[async_test]
1797 async fn test_invite_details_are_set() {
1798 let user_id = user_id!("@alice:localhost");
1799 let client = logged_in_base_client(Some(user_id)).await;
1800 let known_room_id = room_id!("!invited:localhost");
1801 let unknown_room_id = room_id!("!unknown:localhost");
1802
1803 let mut sync_builder = SyncResponseBuilder::new();
1804 let response = sync_builder
1805 .add_invited_room(InvitedRoomBuilder::new(known_room_id))
1806 .build_sync_response();
1807 client.receive_sync_response(response).await.unwrap();
1808
1809 let invited_room = client
1812 .get_room(known_room_id)
1813 .expect("The sync should have created a room in the invited state");
1814
1815 assert_eq!(invited_room.state(), RoomState::Invited);
1816 assert!(
1817 client.get_pending_key_bundle_details_for_room(known_room_id).await.unwrap().is_none()
1818 );
1819
1820 let joined_room = client
1822 .room_joined(known_room_id, Some(user_id.to_owned()))
1823 .await
1824 .expect("We should be able to mark a room as joined");
1825
1826 assert_eq!(joined_room.state(), RoomState::Joined);
1828 assert_matches!(
1829 client.get_pending_key_bundle_details_for_room(known_room_id).await,
1830 Ok(Some(details))
1831 );
1832 assert_eq!(details.inviter, user_id);
1833
1834 assert!(client.get_room(unknown_room_id).is_none());
1837 let unknown_room = client
1838 .room_joined(unknown_room_id, Some(user_id.to_owned()))
1839 .await
1840 .expect("We should be able to mark a room as joined");
1841
1842 assert_eq!(unknown_room.state(), RoomState::Joined);
1843 assert!(
1844 client
1845 .get_pending_key_bundle_details_for_room(unknown_room_id)
1846 .await
1847 .unwrap()
1848 .is_none()
1849 );
1850
1851 sync_builder.clear();
1852 let response =
1853 sync_builder.add_left_room(LeftRoomBuilder::new(known_room_id)).build_sync_response();
1854 client.receive_sync_response(response).await.unwrap();
1855
1856 let left_room = client
1858 .get_room(known_room_id)
1859 .expect("The sync should have created a room in the invited state");
1860
1861 assert_eq!(left_room.state(), RoomState::Left);
1862 assert!(
1863 client.get_pending_key_bundle_details_for_room(known_room_id).await.unwrap().is_none()
1864 );
1865 }
1866}