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,
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)]
93pub struct BaseClient {
94 pub(crate) state_store: BaseStateStore,
96
97 event_cache_store: EventCacheStoreLock,
99
100 media_store: MediaStoreLock,
102
103 #[cfg(feature = "e2e-encryption")]
108 crypto_store: Arc<DynCryptoStore>,
109
110 #[cfg(feature = "e2e-encryption")]
114 olm_machine: Arc<RwLock<Option<OlmMachine>>>,
115
116 pub(crate) ignore_user_list_changes: SharedObservable<Vec<String>>,
118
119 #[cfg(feature = "e2e-encryption")]
122 pub room_key_recipient_strategy: CollectStrategy,
123
124 #[cfg(feature = "e2e-encryption")]
126 pub decryption_settings: DecryptionSettings,
127
128 #[cfg(feature = "e2e-encryption")]
130 pub handle_verification_events: bool,
131
132 pub threading_support: ThreadingSupport,
134}
135
136#[cfg(not(tarpaulin_include))]
137impl fmt::Debug for BaseClient {
138 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139 f.debug_struct("BaseClient")
140 .field("session_meta", &self.state_store.session_meta())
141 .field("sync_token", &self.state_store.sync_token)
142 .finish_non_exhaustive()
143 }
144}
145
146#[derive(Clone, Copy, Debug)]
158pub enum ThreadingSupport {
159 Enabled {
161 with_subscriptions: bool,
167 },
168 Disabled,
170}
171
172impl BaseClient {
173 pub fn new(config: StoreConfig, threading_support: ThreadingSupport) -> Self {
180 let store = BaseStateStore::new(config.state_store);
181
182 BaseClient {
183 state_store: store,
184 event_cache_store: config.event_cache_store,
185 media_store: config.media_store,
186 #[cfg(feature = "e2e-encryption")]
187 crypto_store: config.crypto_store,
188 #[cfg(feature = "e2e-encryption")]
189 olm_machine: Default::default(),
190 ignore_user_list_changes: Default::default(),
191 #[cfg(feature = "e2e-encryption")]
192 room_key_recipient_strategy: Default::default(),
193 #[cfg(feature = "e2e-encryption")]
194 decryption_settings: DecryptionSettings {
195 sender_device_trust_requirement: TrustRequirement::Untrusted,
196 },
197 #[cfg(feature = "e2e-encryption")]
198 handle_verification_events: true,
199 threading_support,
200 }
201 }
202
203 #[cfg(feature = "e2e-encryption")]
206 pub async fn clone_with_in_memory_state_store(
207 &self,
208 cross_process_mode: CrossProcessLockConfig,
209 handle_verification_events: bool,
210 ) -> Result<Self> {
211 let config = StoreConfig::new(cross_process_mode).state_store(MemoryStore::new());
212 let config = config.crypto_store(self.crypto_store.clone());
213
214 let copy = Self {
215 state_store: BaseStateStore::new(config.state_store),
216 event_cache_store: config.event_cache_store,
217 media_store: config.media_store,
218 crypto_store: self.crypto_store.clone(),
225 olm_machine: self.olm_machine.clone(),
226 ignore_user_list_changes: Default::default(),
227 room_key_recipient_strategy: self.room_key_recipient_strategy.clone(),
228 decryption_settings: self.decryption_settings.clone(),
229 handle_verification_events,
230 threading_support: self.threading_support,
231 };
232
233 copy.state_store.derive_from_other(&self.state_store).await?;
234
235 Ok(copy)
236 }
237
238 #[cfg(not(feature = "e2e-encryption"))]
241 #[allow(clippy::unused_async)]
242 pub async fn clone_with_in_memory_state_store(
243 &self,
244 cross_process_store_config: CrossProcessLockConfig,
245 _handle_verification_events: bool,
246 ) -> Result<Self> {
247 let config = StoreConfig::new(cross_process_store_config).state_store(MemoryStore::new());
248 Ok(Self::new(config, ThreadingSupport::Disabled))
249 }
250
251 pub fn session_meta(&self) -> Option<&SessionMeta> {
257 self.state_store.session_meta()
258 }
259
260 pub fn rooms(&self) -> Vec<Room> {
262 self.state_store.rooms()
263 }
264
265 pub fn rooms_filtered(&self, filter: RoomStateFilter) -> Vec<Room> {
267 self.state_store.rooms_filtered(filter)
268 }
269
270 pub fn rooms_stream(
273 &self,
274 ) -> (Vector<Room>, impl Stream<Item = Vec<VectorDiff<Room>>> + use<>) {
275 self.state_store.rooms_stream()
276 }
277
278 pub fn get_or_create_room(&self, room_id: &RoomId, room_state: RoomState) -> Room {
281 self.state_store.get_or_create_room(room_id, room_state)
282 }
283
284 pub fn state_store(&self) -> &DynStateStore {
286 self.state_store.deref()
287 }
288
289 pub fn event_cache_store(&self) -> &EventCacheStoreLock {
291 &self.event_cache_store
292 }
293
294 pub fn media_store(&self) -> &MediaStoreLock {
296 &self.media_store
297 }
298
299 pub fn is_active(&self) -> bool {
303 self.state_store.session_meta().is_some()
304 }
305
306 pub async fn activate(
338 &self,
339 session_meta: SessionMeta,
340 room_load_settings: RoomLoadSettings,
341 #[cfg(feature = "e2e-encryption")] custom_account: Option<
342 crate::crypto::vodozemac::olm::Account,
343 >,
344 ) -> Result<()> {
345 debug!(user_id = ?session_meta.user_id, device_id = ?session_meta.device_id, "Activating the client");
346
347 self.state_store.load_rooms(&session_meta.user_id, room_load_settings).await?;
348 self.state_store.load_sync_token().await?;
349 self.state_store.set_session_meta(session_meta);
350
351 #[cfg(feature = "e2e-encryption")]
352 self.regenerate_olm(custom_account).await?;
353
354 Ok(())
355 }
356
357 #[cfg(feature = "e2e-encryption")]
361 pub async fn regenerate_olm(
362 &self,
363 custom_account: Option<crate::crypto::vodozemac::olm::Account>,
364 ) -> Result<()> {
365 tracing::debug!("regenerating OlmMachine");
366 let session_meta = self.session_meta().ok_or(Error::OlmError(OlmError::MissingSession))?;
367
368 let olm_machine = OlmMachine::with_store(
371 &session_meta.user_id,
372 &session_meta.device_id,
373 self.crypto_store.clone(),
374 custom_account,
375 )
376 .await
377 .map_err(OlmError::from)?;
378
379 *self.olm_machine.write().await = Some(olm_machine);
380 Ok(())
381 }
382
383 pub async fn sync_token(&self) -> Option<String> {
386 self.state_store.sync_token.read().await.clone()
387 }
388
389 pub async fn room_knocked(&self, room_id: &RoomId) -> Result<Room> {
393 let room = self.state_store.get_or_create_room(room_id, RoomState::Knocked);
394
395 if room.state() != RoomState::Knocked {
396 let _state_store_lock = self.state_store_lock().lock().await;
397
398 let mut room_info = room.clone_info();
399 room_info.mark_as_knocked();
400 room_info.mark_state_partially_synced();
401 room_info.mark_members_missing(); #[cfg(feature = "e2e-encryption")]
406 if let Some(olm_machine) = self.olm_machine().await.as_ref() {
407 olm_machine.store().clear_room_pending_key_bundle(room_info.room_id()).await?
408 }
409
410 let mut changes = StateChanges::default();
411 changes.add_room(room_info.clone());
412 self.state_store.save_changes(&changes).await?; room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP);
414 }
415
416 Ok(room)
417 }
418
419 pub async fn room_joined(
459 &self,
460 room_id: &RoomId,
461 inviter: Option<OwnedUserId>,
462 ) -> Result<Room> {
463 let room = self.state_store.get_or_create_room(room_id, RoomState::Joined);
464
465 if room.state() != RoomState::Joined {
468 let _state_store_lock = self.state_store_lock().lock().await;
469
470 let mut room_info = room.clone_info();
471
472 room_info.mark_as_joined();
473 room_info.mark_state_partially_synced();
474 room_info.mark_members_missing(); #[cfg(feature = "e2e-encryption")]
477 {
478 let previous_state = room.state();
489 if previous_state == RoomState::Invited
490 && let Some(inviter) = inviter
491 && let Some(olm_machine) = self.olm_machine().await.as_ref()
492 {
493 olm_machine.store().store_room_pending_key_bundle(room_id, &inviter).await?
494 }
495 }
496 #[cfg(not(feature = "e2e-encryption"))]
497 {
498 let _ = inviter;
500 }
501
502 let mut changes = StateChanges::default();
503 changes.add_room(room_info.clone());
504
505 self.state_store.save_changes(&changes).await?; room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP);
508 }
509
510 Ok(room)
511 }
512
513 pub async fn room_left(&self, room_id: &RoomId) -> Result<()> {
517 let room = self.state_store.get_or_create_room(room_id, RoomState::Left);
518
519 if room.state() != RoomState::Left {
520 let _state_store_lock = self.state_store_lock().lock().await;
521
522 let mut room_info = room.clone_info();
523 room_info.mark_as_left();
524 room_info.mark_state_partially_synced();
525 room_info.mark_members_missing(); #[cfg(feature = "e2e-encryption")]
530 if let Some(olm_machine) = self.olm_machine().await.as_ref() {
531 olm_machine.store().clear_room_pending_key_bundle(room_info.room_id()).await?
532 }
533
534 let mut changes = StateChanges::default();
535 changes.add_room(room_info.clone());
536 self.state_store.save_changes(&changes).await?; room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP);
538 }
539
540 Ok(())
541 }
542
543 pub fn state_store_lock(&self) -> &Mutex<()> {
548 self.state_store.lock()
549 }
550
551 #[instrument(skip_all)]
557 pub async fn receive_sync_response(
558 &self,
559 response: api::sync::sync_events::v3::Response,
560 ) -> Result<SyncResponse> {
561 self.receive_sync_response_with_requested_required_states(
562 response,
563 &RequestedRequiredStates::default(),
564 )
565 .await
566 }
567
568 pub async fn receive_sync_response_with_requested_required_states(
576 &self,
577 response: api::sync::sync_events::v3::Response,
578 requested_required_states: &RequestedRequiredStates,
579 ) -> Result<SyncResponse> {
580 if self.state_store.sync_token.read().await.as_ref() == Some(&response.next_batch) {
584 info!("Got the same sync response twice");
585 return Ok(SyncResponse::default());
586 }
587
588 let now = if enabled!(Level::INFO) { Some(Instant::now()) } else { None };
589
590 let user_id = self
591 .session_meta()
592 .expect("Sync shouldn't run without an authenticated user")
593 .user_id
594 .to_owned();
595
596 #[cfg(feature = "e2e-encryption")]
597 let olm_machine = self.olm_machine().await;
598
599 let mut context = Context::new(StateChanges::new(response.next_batch.clone()));
600
601 #[cfg(feature = "e2e-encryption")]
602 let processors::e2ee::to_device::Output { processed_to_device_events: to_device } =
603 processors::e2ee::to_device::from_sync_v2(
604 &response,
605 olm_machine.as_ref(),
606 &self.decryption_settings,
607 )
608 .await?;
609
610 #[cfg(not(feature = "e2e-encryption"))]
611 let to_device = response
612 .to_device
613 .events
614 .into_iter()
615 .map(|raw| {
616 use matrix_sdk_common::deserialized_responses::{
617 ProcessedToDeviceEvent, ToDeviceUnableToDecryptInfo,
618 ToDeviceUnableToDecryptReason,
619 };
620
621 if let Ok(Some(event_type)) = raw.get_field::<String>("type") {
622 if event_type == "m.room.encrypted" {
623 ProcessedToDeviceEvent::UnableToDecrypt {
624 encrypted_event: raw,
625 utd_info: ToDeviceUnableToDecryptInfo {
626 reason: ToDeviceUnableToDecryptReason::EncryptionIsDisabled,
627 },
628 }
629 } else {
630 ProcessedToDeviceEvent::PlainText(raw)
631 }
632 } else {
633 ProcessedToDeviceEvent::Invalid(raw)
635 }
636 })
637 .collect();
638
639 let mut ambiguity_cache = AmbiguityCache::new(self.state_store.inner.clone());
640
641 let global_account_data_processor =
642 processors::account_data::global(&response.account_data.events);
643
644 let push_rules = self.get_push_rules(&global_account_data_processor).await?;
645
646 let mut room_updates = RoomUpdates::default();
647 let mut notifications = Default::default();
648
649 let mut updated_members_in_room: BTreeMap<OwnedRoomId, BTreeSet<OwnedUserId>> =
650 BTreeMap::new();
651
652 #[cfg(feature = "e2e-encryption")]
653 let e2ee_context = processors::e2ee::E2EE::new(
654 olm_machine.as_ref(),
655 &self.decryption_settings,
656 self.handle_verification_events,
657 );
658
659 for (room_id, joined_room) in response.rooms.join {
660 let joined_room_update = processors::room::sync_v2::update_joined_room(
661 &mut context,
662 processors::room::RoomCreationData::new(
663 &room_id,
664 requested_required_states,
665 &mut ambiguity_cache,
666 ),
667 joined_room,
668 &mut updated_members_in_room,
669 processors::notification::Notification::new(
670 &push_rules,
671 &mut notifications,
672 &self.state_store,
673 ),
674 #[cfg(feature = "e2e-encryption")]
675 &e2ee_context,
676 )
677 .await?;
678
679 room_updates.joined.insert(room_id, joined_room_update);
680 }
681
682 for (room_id, left_room) in response.rooms.leave {
683 let left_room_update = processors::room::sync_v2::update_left_room(
684 &mut context,
685 processors::room::RoomCreationData::new(
686 &room_id,
687 requested_required_states,
688 &mut ambiguity_cache,
689 ),
690 left_room,
691 processors::notification::Notification::new(
692 &push_rules,
693 &mut notifications,
694 &self.state_store,
695 ),
696 #[cfg(feature = "e2e-encryption")]
697 &e2ee_context,
698 )
699 .await?;
700
701 room_updates.left.insert(room_id, left_room_update);
702 }
703
704 for (room_id, invited_room) in response.rooms.invite {
705 let invited_room_update = processors::room::sync_v2::update_invited_room(
706 &mut context,
707 &room_id,
708 &user_id,
709 invited_room,
710 processors::notification::Notification::new(
711 &push_rules,
712 &mut notifications,
713 &self.state_store,
714 ),
715 #[cfg(feature = "e2e-encryption")]
716 &e2ee_context,
717 )
718 .await?;
719
720 room_updates.invited.insert(room_id, invited_room_update);
721 }
722
723 for (room_id, knocked_room) in response.rooms.knock {
724 let knocked_room_update = processors::room::sync_v2::update_knocked_room(
725 &mut context,
726 &room_id,
727 &user_id,
728 knocked_room,
729 processors::notification::Notification::new(
730 &push_rules,
731 &mut notifications,
732 &self.state_store,
733 ),
734 #[cfg(feature = "e2e-encryption")]
735 &e2ee_context,
736 )
737 .await?;
738
739 room_updates.knocked.insert(room_id, knocked_room_update);
740 }
741
742 global_account_data_processor.apply(&mut context, &self.state_store).await;
743
744 context.state_changes.presence = response
745 .presence
746 .events
747 .iter()
748 .filter_map(|e| {
749 let event = e.deserialize().ok()?;
750 Some((event.sender, e.clone()))
751 })
752 .collect();
753
754 context.state_changes.ambiguity_maps = ambiguity_cache.cache;
755
756 {
757 let _state_store_lock = self.state_store_lock().lock().await;
758
759 processors::changes::save_and_apply(
760 context,
761 &self.state_store,
762 &self.ignore_user_list_changes,
763 Some(response.next_batch.clone()),
764 )
765 .await?;
766 }
767
768 let mut context = Context::default();
769
770 processors::room::display_name::update_for_rooms(
773 &mut context,
774 &room_updates,
775 &self.state_store,
776 )
777 .await;
778
779 {
781 let _state_store_lock = self.state_store_lock().lock().await;
782
783 processors::changes::save_only(context, &self.state_store).await?;
784 }
785
786 for (room_id, member_ids) in updated_members_in_room {
787 if let Some(room) = self.get_room(&room_id) {
788 let _ =
789 room.room_member_updates_sender.send(RoomMembersUpdate::Partial(member_ids));
790 }
791 }
792
793 if enabled!(Level::INFO) {
794 info!("Processed a sync response in {:?}", now.map(|now| now.elapsed()));
795 }
796
797 let response = SyncResponse {
798 rooms: room_updates,
799 presence: response.presence.events,
800 account_data: response.account_data.events,
801 to_device,
802 notifications,
803 };
804
805 Ok(response)
806 }
807
808 #[instrument(skip_all, fields(?room_id))]
820 pub async fn receive_all_members(
821 &self,
822 room_id: &RoomId,
823 request: &api::membership::get_member_events::v3::Request,
824 response: &api::membership::get_member_events::v3::Response,
825 ) -> Result<()> {
826 if request.membership.is_some() || request.not_membership.is_some() || request.at.is_some()
827 {
828 return Err(Error::InvalidReceiveMembersParameters);
832 }
833
834 let Some(room) = self.state_store.room(room_id) else {
835 return Ok(());
837 };
838
839 let mut chunk = Vec::with_capacity(response.chunk.len());
840 let mut context = Context::default();
841
842 #[cfg(feature = "e2e-encryption")]
843 let mut user_ids = BTreeSet::new();
844
845 let mut ambiguity_map: HashMap<DisplayName, BTreeSet<OwnedUserId>> = Default::default();
846
847 for raw_event in &response.chunk {
848 let member = match raw_event.deserialize() {
849 Ok(ev) => ev,
850 Err(e) => {
851 let event_id: Option<String> = raw_event.get_field("event_id").ok().flatten();
852 debug!(event_id, "Failed to deserialize member event: {e}");
853 continue;
854 }
855 };
856
857 #[cfg(feature = "e2e-encryption")]
867 match member.membership() {
868 MembershipState::Join | MembershipState::Invite => {
869 user_ids.insert(member.state_key().to_owned());
870 }
871 _ => (),
872 }
873
874 if let StateEvent::Original(e) = &member
875 && let Some(d) = &e.content.displayname
876 {
877 let display_name = DisplayName::new(d);
878 ambiguity_map.entry(display_name).or_default().insert(member.state_key().clone());
879 }
880
881 let sync_member: SyncRoomMemberEvent = member.clone().into();
882 processors::profiles::upsert_or_delete(&mut context, room_id, &sync_member);
883
884 context
885 .state_changes
886 .state
887 .entry(room_id.to_owned())
888 .or_default()
889 .entry(member.event_type())
890 .or_default()
891 .insert(member.state_key().to_string(), raw_event.clone().cast());
892 chunk.push(member);
893 }
894
895 #[cfg(feature = "e2e-encryption")]
896 processors::e2ee::tracked_users::update(
897 self.olm_machine().await.as_ref(),
898 room.encryption_state(),
899 &user_ids,
900 )
901 .await?;
902
903 context.state_changes.ambiguity_maps.insert(room_id.to_owned(), ambiguity_map);
904
905 {
906 let _state_store_lock = self.state_store_lock().lock().await;
907
908 let mut room_info = room.clone_info();
909 room_info.mark_members_synced();
910 context.state_changes.add_room(room_info);
911
912 processors::changes::save_and_apply(
913 context,
914 &self.state_store,
915 &self.ignore_user_list_changes,
916 None,
917 )
918 .await?;
919 }
920
921 let _ = room.room_member_updates_sender.send(RoomMembersUpdate::FullReload);
922
923 Ok(())
924 }
925
926 pub async fn receive_filter_upload(
942 &self,
943 filter_name: &str,
944 response: &api::filter::create_filter::v3::Response,
945 ) -> Result<()> {
946 Ok(self
947 .state_store
948 .set_kv_data(
949 StateStoreDataKey::Filter(filter_name),
950 StateStoreDataValue::Filter(response.filter_id.clone()),
951 )
952 .await?)
953 }
954
955 pub async fn get_filter(&self, filter_name: &str) -> StoreResult<Option<String>> {
967 let filter = self
968 .state_store
969 .get_kv_data(StateStoreDataKey::Filter(filter_name))
970 .await?
971 .map(|d| d.into_filter().expect("State store data not a filter"));
972
973 Ok(filter)
974 }
975
976 #[cfg(feature = "e2e-encryption")]
978 pub async fn share_room_key(&self, room_id: &RoomId) -> Result<Vec<Arc<ToDeviceRequest>>> {
979 match self.olm_machine().await.as_ref() {
980 Some(o) => {
981 let Some(room) = self.get_room(room_id) else {
982 return Err(Error::InsufficientData);
983 };
984
985 let history_visibility = room.history_visibility_or_default();
986 let Some(room_encryption_event) = room.encryption_settings() else {
987 return Err(Error::EncryptionNotEnabled);
988 };
989
990 let filter = if history_visibility == HistoryVisibility::Joined {
993 RoomMemberships::JOIN
994 } else {
995 RoomMemberships::ACTIVE
996 };
997
998 let members = self.state_store.get_user_ids(room_id, filter).await?;
999
1000 let Some(settings) = EncryptionSettings::from_possibly_redacted(
1001 room_encryption_event,
1002 history_visibility,
1003 self.room_key_recipient_strategy.clone(),
1004 ) else {
1005 return Err(Error::EncryptionNotEnabled);
1006 };
1007
1008 Ok(o.share_room_key(room_id, members.iter().map(Deref::deref), settings).await?)
1009 }
1010 None => panic!("Olm machine wasn't started"),
1011 }
1012 }
1013
1014 pub fn get_room(&self, room_id: &RoomId) -> Option<Room> {
1020 self.state_store.room(room_id)
1021 }
1022
1023 pub async fn forget_room(&self, room_id: &RoomId) -> Result<()> {
1031 self.state_store.forget_room(room_id).await?;
1033
1034 match self.event_cache_store().lock().await? {
1036 EventCacheStoreLockState::Clean(guard) | EventCacheStoreLockState::Dirty(guard) => {
1041 guard.remove_room(room_id).await?
1042 }
1043 }
1044
1045 Ok(())
1046 }
1047
1048 #[cfg(feature = "e2e-encryption")]
1050 pub async fn olm_machine(&self) -> RwLockReadGuard<'_, Option<OlmMachine>> {
1051 self.olm_machine.read().await
1052 }
1053
1054 pub(crate) async fn get_push_rules(
1060 &self,
1061 global_account_data_processor: &processors::account_data::Global,
1062 ) -> Result<Ruleset> {
1063 let _timer = timer!(Level::TRACE, "get_push_rules");
1064 if let Some(event) = global_account_data_processor
1065 .push_rules()
1066 .and_then(|ev| ev.deserialize_as_unchecked::<PushRulesEvent>().ok())
1067 {
1068 Ok(event.content.global)
1069 } else if let Some(event) = self
1070 .state_store
1071 .get_account_data_event_static::<PushRulesEventContent>()
1072 .await?
1073 .and_then(|ev| ev.deserialize().ok())
1074 {
1075 Ok(event.content.global)
1076 } else if let Some(session_meta) = self.state_store.session_meta() {
1077 Ok(Ruleset::server_default(&session_meta.user_id))
1078 } else {
1079 Ok(Ruleset::new())
1080 }
1081 }
1082
1083 pub fn subscribe_to_ignore_user_list_changes(&self) -> Subscriber<Vec<String>> {
1086 self.ignore_user_list_changes.subscribe()
1087 }
1088
1089 pub fn room_info_notable_update_receiver(&self) -> broadcast::Receiver<RoomInfoNotableUpdate> {
1093 self.state_store.room_info_notable_update_sender.subscribe()
1094 }
1095
1096 pub async fn is_user_ignored(&self, user_id: &UserId) -> bool {
1098 match self.state_store.get_account_data_event_static::<IgnoredUserListEventContent>().await
1099 {
1100 Ok(Some(raw_ignored_user_list)) => match raw_ignored_user_list.deserialize() {
1101 Ok(current_ignored_user_list) => {
1102 current_ignored_user_list.content.ignored_users.contains_key(user_id)
1103 }
1104 Err(error) => {
1105 warn!(?error, "Failed to deserialize the ignored user list event");
1106 false
1107 }
1108 },
1109 Ok(None) => false,
1110 Err(error) => {
1111 warn!(?error, "Could not get the ignored user list from the state store");
1112 false
1113 }
1114 }
1115 }
1116
1117 #[cfg(feature = "e2e-encryption")]
1122 pub async fn get_pending_key_bundle_details_for_room(
1123 &self,
1124 room_id: &RoomId,
1125 ) -> Result<Option<RoomPendingKeyBundleDetails>> {
1126 let result = match self.olm_machine().await.as_ref() {
1127 Some(machine) => {
1128 machine.store().get_pending_key_bundle_details_for_room(room_id).await?
1129 }
1130 None => None,
1131 };
1132 Ok(result)
1133 }
1134}
1135
1136#[derive(Debug, Default)]
1148pub struct RequestedRequiredStates {
1149 default: Vec<(StateEventType, String)>,
1150 for_rooms: HashMap<OwnedRoomId, Vec<(StateEventType, String)>>,
1151}
1152
1153impl RequestedRequiredStates {
1154 pub fn new(
1159 default: Vec<(StateEventType, String)>,
1160 for_rooms: HashMap<OwnedRoomId, Vec<(StateEventType, String)>>,
1161 ) -> Self {
1162 Self { default, for_rooms }
1163 }
1164
1165 pub fn for_room(&self, room_id: &RoomId) -> &[(StateEventType, String)] {
1167 self.for_rooms.get(room_id).unwrap_or(&self.default)
1168 }
1169}
1170
1171impl From<&v5::Request> for RequestedRequiredStates {
1172 fn from(request: &v5::Request) -> Self {
1173 let mut default = BTreeSet::new();
1180
1181 for list in request.lists.values() {
1182 default.extend(BTreeSet::from_iter(list.room_details.required_state.iter().cloned()));
1183 }
1184
1185 for room_subscription in request.room_subscriptions.values() {
1186 default.extend(BTreeSet::from_iter(room_subscription.required_state.iter().cloned()));
1187 }
1188
1189 Self { default: default.into_iter().collect(), for_rooms: HashMap::new() }
1190 }
1191}
1192
1193#[cfg(test)]
1194mod tests {
1195 use std::collections::HashMap;
1196
1197 use assert_matches2::assert_let;
1198 #[cfg(feature = "e2e-encryption")]
1199 use assert_matches2::assert_matches;
1200 use futures_util::FutureExt as _;
1201 use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;
1202 use matrix_sdk_test::{
1203 BOB, InvitedRoomBuilder, LeftRoomBuilder, SyncResponseBuilder, async_test,
1204 event_factory::EventFactory, ruma_response_from_json,
1205 };
1206 use ruma::{
1207 api::client::{self as api, sync::sync_events::v5},
1208 event_id,
1209 events::{StateEventType, room::member::MembershipState},
1210 room_id,
1211 serde::Raw,
1212 user_id,
1213 };
1214 use serde_json::{json, value::to_raw_value};
1215
1216 use super::{BaseClient, RequestedRequiredStates};
1217 use crate::{
1218 RoomDisplayName, RoomState, SessionMeta,
1219 client::ThreadingSupport,
1220 store::{RoomLoadSettings, StateStoreExt, StoreConfig},
1221 test_utils::logged_in_base_client,
1222 };
1223
1224 #[test]
1225 fn test_requested_required_states() {
1226 let room_id_0 = room_id!("!r0");
1227 let room_id_1 = room_id!("!r1");
1228
1229 let requested_required_states = RequestedRequiredStates::new(
1230 vec![(StateEventType::RoomAvatar, "".to_owned())],
1231 HashMap::from([(
1232 room_id_0.to_owned(),
1233 vec![
1234 (StateEventType::RoomMember, "foo".to_owned()),
1235 (StateEventType::RoomEncryption, "".to_owned()),
1236 ],
1237 )]),
1238 );
1239
1240 assert_eq!(
1242 requested_required_states.for_room(room_id_0),
1243 &[
1244 (StateEventType::RoomMember, "foo".to_owned()),
1245 (StateEventType::RoomEncryption, "".to_owned()),
1246 ]
1247 );
1248
1249 assert_eq!(
1251 requested_required_states.for_room(room_id_1),
1252 &[(StateEventType::RoomAvatar, "".to_owned()),]
1253 );
1254 }
1255
1256 #[test]
1257 fn test_requested_required_states_from_sync_v5_request() {
1258 let room_id_0 = room_id!("!r0");
1259 let room_id_1 = room_id!("!r1");
1260
1261 let mut request = v5::Request::new();
1263
1264 {
1265 let requested_required_states = RequestedRequiredStates::from(&request);
1266
1267 assert!(requested_required_states.default.is_empty());
1268 assert!(requested_required_states.for_rooms.is_empty());
1269 }
1270
1271 request.lists.insert("foo".to_owned(), {
1273 let mut list = v5::request::List::default();
1274 list.room_details.required_state = vec![
1275 (StateEventType::RoomAvatar, "".to_owned()),
1276 (StateEventType::RoomEncryption, "".to_owned()),
1277 ];
1278
1279 list
1280 });
1281
1282 {
1283 let requested_required_states = RequestedRequiredStates::from(&request);
1284
1285 assert_eq!(
1286 requested_required_states.default,
1287 &[
1288 (StateEventType::RoomAvatar, "".to_owned()),
1289 (StateEventType::RoomEncryption, "".to_owned())
1290 ]
1291 );
1292 assert!(requested_required_states.for_rooms.is_empty());
1293 }
1294
1295 request.lists.insert("bar".to_owned(), {
1297 let mut list = v5::request::List::default();
1298 list.room_details.required_state = vec![
1299 (StateEventType::RoomEncryption, "".to_owned()),
1300 (StateEventType::RoomName, "".to_owned()),
1301 ];
1302
1303 list
1304 });
1305
1306 {
1307 let requested_required_states = RequestedRequiredStates::from(&request);
1308
1309 assert_eq!(
1311 requested_required_states.default,
1312 &[
1313 (StateEventType::RoomAvatar, "".to_owned()),
1314 (StateEventType::RoomEncryption, "".to_owned()),
1315 (StateEventType::RoomName, "".to_owned()),
1316 ]
1317 );
1318 assert!(requested_required_states.for_rooms.is_empty());
1319 }
1320
1321 request.room_subscriptions.insert(room_id_0.to_owned(), {
1323 let mut room_subscription = v5::request::RoomSubscription::default();
1324
1325 room_subscription.required_state = vec![
1326 (StateEventType::RoomJoinRules, "".to_owned()),
1327 (StateEventType::RoomEncryption, "".to_owned()),
1328 ];
1329
1330 room_subscription
1331 });
1332
1333 {
1334 let requested_required_states = RequestedRequiredStates::from(&request);
1335
1336 assert_eq!(
1338 requested_required_states.default,
1339 &[
1340 (StateEventType::RoomAvatar, "".to_owned()),
1341 (StateEventType::RoomEncryption, "".to_owned()),
1342 (StateEventType::RoomJoinRules, "".to_owned()),
1343 (StateEventType::RoomName, "".to_owned()),
1344 ]
1345 );
1346 assert!(requested_required_states.for_rooms.is_empty());
1347 }
1348
1349 request.room_subscriptions.insert(room_id_1.to_owned(), {
1351 let mut room_subscription = v5::request::RoomSubscription::default();
1352
1353 room_subscription.required_state = vec![
1354 (StateEventType::RoomName, "".to_owned()),
1355 (StateEventType::RoomTopic, "".to_owned()),
1356 ];
1357
1358 room_subscription
1359 });
1360
1361 {
1362 let requested_required_states = RequestedRequiredStates::from(&request);
1363
1364 assert_eq!(
1366 requested_required_states.default,
1367 &[
1368 (StateEventType::RoomAvatar, "".to_owned()),
1369 (StateEventType::RoomEncryption, "".to_owned()),
1370 (StateEventType::RoomJoinRules, "".to_owned()),
1371 (StateEventType::RoomName, "".to_owned()),
1372 (StateEventType::RoomTopic, "".to_owned()),
1373 ]
1374 );
1375 }
1376 }
1377
1378 #[async_test]
1379 async fn test_invite_after_leaving() {
1380 let user_id = user_id!("@alice:example.org");
1381 let room_id = room_id!("!test:example.org");
1382
1383 let client = logged_in_base_client(Some(user_id)).await;
1384 let f = EventFactory::new();
1385
1386 let mut sync_builder = SyncResponseBuilder::new();
1387
1388 let response = sync_builder
1389 .add_left_room(
1390 LeftRoomBuilder::new(room_id).add_timeline_event(
1391 EventFactory::new()
1392 .member(user_id)
1393 .membership(MembershipState::Leave)
1394 .display_name("Alice")
1395 .event_id(event_id!("$994173582443PhrSn:example.org")),
1396 ),
1397 )
1398 .build_sync_response();
1399 client.receive_sync_response(response).await.unwrap();
1400 assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left);
1401
1402 let response = sync_builder
1403 .add_invited_room(
1404 InvitedRoomBuilder::new(room_id).add_state_event(
1405 f.member(user_id)
1406 .sender(user_id!("@example:example.org"))
1407 .membership(MembershipState::Invite)
1408 .display_name("Alice"),
1409 ),
1410 )
1411 .build_sync_response();
1412 client.receive_sync_response(response).await.unwrap();
1413 assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Invited);
1414 }
1415
1416 #[async_test]
1417 async fn test_invite_displayname() {
1418 let user_id = user_id!("@alice:example.org");
1419 let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
1420
1421 let client = logged_in_base_client(Some(user_id)).await;
1422
1423 let response = ruma_response_from_json(&json!({
1424 "next_batch": "asdkl;fjasdkl;fj;asdkl;f",
1425 "device_one_time_keys_count": {
1426 "signed_curve25519": 50u64
1427 },
1428 "device_unused_fallback_key_types": [
1429 "signed_curve25519"
1430 ],
1431 "rooms": {
1432 "invite": {
1433 "!ithpyNKDtmhneaTQja:example.org": {
1434 "invite_state": {
1435 "events": [
1436 {
1437 "content": {
1438 "creator": "@test:example.org",
1439 "room_version": "9"
1440 },
1441 "sender": "@test:example.org",
1442 "state_key": "",
1443 "type": "m.room.create"
1444 },
1445 {
1446 "content": {
1447 "join_rule": "invite"
1448 },
1449 "sender": "@test:example.org",
1450 "state_key": "",
1451 "type": "m.room.join_rules"
1452 },
1453 {
1454 "content": {
1455 "algorithm": "m.megolm.v1.aes-sha2"
1456 },
1457 "sender": "@test:example.org",
1458 "state_key": "",
1459 "type": "m.room.encryption"
1460 },
1461 {
1462 "content": {
1463 "avatar_url": "mxc://example.org/dcBBDwuWEUrjfrOchvkirUST",
1464 "displayname": "Kyra",
1465 "membership": "join"
1466 },
1467 "sender": "@test:example.org",
1468 "state_key": "@test:example.org",
1469 "type": "m.room.member"
1470 },
1471 {
1472 "content": {
1473 "avatar_url": "mxc://example.org/ABFEXSDrESxovWwEnCYdNcHT",
1474 "displayname": "alice",
1475 "is_direct": true,
1476 "membership": "invite"
1477 },
1478 "origin_server_ts": 1650878657984u64,
1479 "sender": "@test:example.org",
1480 "state_key": "@alice:example.org",
1481 "type": "m.room.member",
1482 "unsigned": {
1483 "age": 14u64
1484 },
1485 "event_id": "$fLDqltg9Puj-kWItLSFVHPGN4YkgpYQf2qImPzdmgrE"
1486 }
1487 ]
1488 }
1489 }
1490 }
1491 }
1492 }));
1493
1494 client.receive_sync_response(response).await.unwrap();
1495
1496 let room = client.get_room(room_id).expect("Room not found");
1497 assert_eq!(room.state(), RoomState::Invited);
1498 assert_eq!(
1499 room.compute_display_name().await.expect("fetching display name failed").into_inner(),
1500 RoomDisplayName::Calculated("Kyra".to_owned())
1501 );
1502 }
1503
1504 #[async_test]
1505 async fn test_deserialization_failure() {
1506 let user_id = user_id!("@alice:example.org");
1507 let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
1508
1509 let client = BaseClient::new(
1510 StoreConfig::new(CrossProcessLockConfig::SingleProcess),
1511 ThreadingSupport::Disabled,
1512 );
1513 client
1514 .activate(
1515 SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
1516 RoomLoadSettings::default(),
1517 #[cfg(feature = "e2e-encryption")]
1518 None,
1519 )
1520 .await
1521 .unwrap();
1522
1523 let response = ruma_response_from_json(&json!({
1524 "next_batch": "asdkl;fjasdkl;fj;asdkl;f",
1525 "rooms": {
1526 "join": {
1527 "!ithpyNKDtmhneaTQja:example.org": {
1528 "state": {
1529 "events": [
1530 {
1531 "invalid": "invalid",
1532 },
1533 {
1534 "content": {
1535 "name": "The room name"
1536 },
1537 "event_id": "$143273582443PhrSn:example.org",
1538 "origin_server_ts": 1432735824653u64,
1539 "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
1540 "sender": "@example:example.org",
1541 "state_key": "",
1542 "type": "m.room.name",
1543 "unsigned": {
1544 "age": 1234
1545 }
1546 },
1547 ]
1548 }
1549 }
1550 }
1551 }
1552 }));
1553
1554 client.receive_sync_response(response).await.unwrap();
1555 client
1556 .state_store()
1557 .get_state_event_static::<ruma::events::room::name::RoomNameEventContent>(room_id)
1558 .await
1559 .expect("Failed to fetch state event")
1560 .expect("State event not found")
1561 .deserialize()
1562 .expect("Failed to deserialize state event");
1563 }
1564
1565 #[async_test]
1566 async fn test_invited_members_arent_ignored() {
1567 let user_id = user_id!("@alice:example.org");
1568 let inviter_user_id = user_id!("@bob:example.org");
1569 let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
1570
1571 let client = BaseClient::new(
1572 StoreConfig::new(CrossProcessLockConfig::SingleProcess),
1573 ThreadingSupport::Disabled,
1574 );
1575 client
1576 .activate(
1577 SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
1578 RoomLoadSettings::default(),
1579 #[cfg(feature = "e2e-encryption")]
1580 None,
1581 )
1582 .await
1583 .unwrap();
1584
1585 let mut sync_builder = SyncResponseBuilder::new();
1587 let response = sync_builder
1588 .add_joined_room(matrix_sdk_test::JoinedRoomBuilder::new(room_id))
1589 .build_sync_response();
1590 client.receive_sync_response(response).await.unwrap();
1591
1592 let request = api::membership::get_member_events::v3::Request::new(room_id.to_owned());
1595
1596 let raw_member_event = json!({
1597 "content": {
1598 "avatar_url": "mxc://localhost/fewjilfewjil42",
1599 "displayname": "Invited Alice",
1600 "membership": "invite"
1601 },
1602 "event_id": "$151800140517rfvjc:localhost",
1603 "origin_server_ts": 151800140,
1604 "room_id": room_id,
1605 "sender": inviter_user_id,
1606 "state_key": user_id,
1607 "type": "m.room.member",
1608 "unsigned": {
1609 "age": 13374242,
1610 }
1611 });
1612 let response = api::membership::get_member_events::v3::Response::new(vec![Raw::from_json(
1613 to_raw_value(&raw_member_event).unwrap(),
1614 )]);
1615
1616 client.receive_all_members(room_id, &request, &response).await.unwrap();
1618
1619 let room = client.get_room(room_id).unwrap();
1620
1621 let member = room.get_member(user_id).await.expect("ok").expect("exists");
1623
1624 assert_eq!(member.user_id(), user_id);
1625 assert_eq!(member.display_name().unwrap(), "Invited Alice");
1626 assert_eq!(member.avatar_url().unwrap().to_string(), "mxc://localhost/fewjilfewjil42");
1627 }
1628
1629 #[async_test]
1630 async fn test_reinvited_members_get_a_display_name() {
1631 let user_id = user_id!("@alice:example.org");
1632 let inviter_user_id = user_id!("@bob:example.org");
1633 let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
1634
1635 let client = BaseClient::new(
1636 StoreConfig::new(CrossProcessLockConfig::SingleProcess),
1637 ThreadingSupport::Disabled,
1638 );
1639 client
1640 .activate(
1641 SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
1642 RoomLoadSettings::default(),
1643 #[cfg(feature = "e2e-encryption")]
1644 None,
1645 )
1646 .await
1647 .unwrap();
1648
1649 let f = EventFactory::new().sender(user_id);
1651 let mut sync_builder = SyncResponseBuilder::new();
1652 let response = sync_builder
1653 .add_joined_room(
1654 matrix_sdk_test::JoinedRoomBuilder::new(room_id)
1655 .add_state_event(f.member(user_id).leave()),
1656 )
1657 .build_sync_response();
1658 client.receive_sync_response(response).await.unwrap();
1659
1660 let request = api::membership::get_member_events::v3::Request::new(room_id.to_owned());
1662
1663 let raw_member_event = json!({
1664 "content": {
1665 "avatar_url": "mxc://localhost/fewjilfewjil42",
1666 "displayname": "Invited Alice",
1667 "membership": "invite"
1668 },
1669 "event_id": "$151800140517rfvjc:localhost",
1670 "origin_server_ts": 151800140,
1671 "room_id": room_id,
1672 "sender": inviter_user_id,
1673 "state_key": user_id,
1674 "type": "m.room.member",
1675 "unsigned": {
1676 "age": 13374242,
1677 }
1678 });
1679 let response = api::membership::get_member_events::v3::Response::new(vec![Raw::from_json(
1680 to_raw_value(&raw_member_event).unwrap(),
1681 )]);
1682
1683 client.receive_all_members(room_id, &request, &response).await.unwrap();
1685
1686 let room = client.get_room(room_id).unwrap();
1687
1688 let member = room.get_member(user_id).await.expect("ok").expect("exists");
1690
1691 assert_eq!(member.user_id(), user_id);
1692 assert_eq!(member.display_name().unwrap(), "Invited Alice");
1693 assert_eq!(member.avatar_url().unwrap().to_string(), "mxc://localhost/fewjilfewjil42");
1694 }
1695
1696 #[async_test]
1697 async fn test_ignored_user_list_changes() {
1698 let user_id = user_id!("@alice:example.org");
1699 let client = BaseClient::new(
1700 StoreConfig::new(CrossProcessLockConfig::SingleProcess),
1701 ThreadingSupport::Disabled,
1702 );
1703
1704 client
1705 .activate(
1706 SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
1707 RoomLoadSettings::default(),
1708 #[cfg(feature = "e2e-encryption")]
1709 None,
1710 )
1711 .await
1712 .unwrap();
1713
1714 let mut subscriber = client.subscribe_to_ignore_user_list_changes();
1715 assert!(subscriber.next().now_or_never().is_none());
1716
1717 let f = EventFactory::new();
1718 let mut sync_builder = SyncResponseBuilder::new();
1719 let response = sync_builder
1720 .add_global_account_data(f.ignored_user_list([(*BOB).into()]))
1721 .build_sync_response();
1722 client.receive_sync_response(response).await.unwrap();
1723
1724 assert_let!(Some(ignored) = subscriber.next().await);
1725 assert_eq!(ignored, [BOB.to_string()]);
1726
1727 let response = sync_builder
1729 .add_global_account_data(f.ignored_user_list([(*BOB).into()]))
1730 .build_sync_response();
1731 client.receive_sync_response(response).await.unwrap();
1732
1733 assert!(subscriber.next().now_or_never().is_none());
1735
1736 let response =
1738 sync_builder.add_global_account_data(f.ignored_user_list([])).build_sync_response();
1739 client.receive_sync_response(response).await.unwrap();
1740
1741 assert_let!(Some(ignored) = subscriber.next().await);
1742 assert!(ignored.is_empty());
1743 }
1744
1745 #[async_test]
1746 async fn test_is_user_ignored() {
1747 let ignored_user_id = user_id!("@alice:example.org");
1748 let client = logged_in_base_client(None).await;
1749
1750 let mut sync_builder = SyncResponseBuilder::new();
1751 let f = EventFactory::new();
1752 let response = sync_builder
1753 .add_global_account_data(f.ignored_user_list([ignored_user_id.to_owned()]))
1754 .build_sync_response();
1755 client.receive_sync_response(response).await.unwrap();
1756
1757 assert!(client.is_user_ignored(ignored_user_id).await);
1758 }
1759
1760 #[cfg(feature = "e2e-encryption")]
1761 #[async_test]
1762 async fn test_invite_details_are_set() {
1763 let user_id = user_id!("@alice:localhost");
1764 let client = logged_in_base_client(Some(user_id)).await;
1765 let known_room_id = room_id!("!invited:localhost");
1766 let unknown_room_id = room_id!("!unknown:localhost");
1767
1768 let mut sync_builder = SyncResponseBuilder::new();
1769 let response = sync_builder
1770 .add_invited_room(InvitedRoomBuilder::new(known_room_id))
1771 .build_sync_response();
1772 client.receive_sync_response(response).await.unwrap();
1773
1774 let invited_room = client
1777 .get_room(known_room_id)
1778 .expect("The sync should have created a room in the invited state");
1779
1780 assert_eq!(invited_room.state(), RoomState::Invited);
1781 assert!(
1782 client.get_pending_key_bundle_details_for_room(known_room_id).await.unwrap().is_none()
1783 );
1784
1785 let joined_room = client
1787 .room_joined(known_room_id, Some(user_id.to_owned()))
1788 .await
1789 .expect("We should be able to mark a room as joined");
1790
1791 assert_eq!(joined_room.state(), RoomState::Joined);
1793 assert_matches!(
1794 client.get_pending_key_bundle_details_for_room(known_room_id).await,
1795 Ok(Some(details))
1796 );
1797 assert_eq!(details.inviter, user_id);
1798
1799 assert!(client.get_room(unknown_room_id).is_none());
1802 let unknown_room = client
1803 .room_joined(unknown_room_id, Some(user_id.to_owned()))
1804 .await
1805 .expect("We should be able to mark a room as joined");
1806
1807 assert_eq!(unknown_room.state(), RoomState::Joined);
1808 assert!(
1809 client
1810 .get_pending_key_bundle_details_for_room(unknown_room_id)
1811 .await
1812 .unwrap()
1813 .is_none()
1814 );
1815
1816 sync_builder.clear();
1817 let response =
1818 sync_builder.add_left_room(LeftRoomBuilder::new(known_room_id)).build_sync_response();
1819 client.receive_sync_response(response).await.unwrap();
1820
1821 let left_room = client
1823 .get_room(known_room_id)
1824 .expect("The sync should have created a room in the invited state");
1825
1826 assert_eq!(left_room.state(), RoomState::Left);
1827 assert!(
1828 client.get_pending_key_bundle_details_for_room(known_room_id).await.unwrap().is_none()
1829 );
1830 }
1831}