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::timer;
28#[cfg(feature = "e2e-encryption")]
29use matrix_sdk_crypto::{
30 CollectStrategy, DecryptionSettings, EncryptionSettings, OlmError, OlmMachine,
31 TrustRequirement, store::DynCryptoStore, types::requests::ToDeviceRequest,
32};
33#[cfg(doc)]
34use ruma::DeviceId;
35#[cfg(feature = "e2e-encryption")]
36use ruma::events::room::{history_visibility::HistoryVisibility, member::MembershipState};
37use ruma::{
38 MilliSecondsSinceUnixEpoch, OwnedRoomId, OwnedUserId, RoomId, UserId,
39 api::client::{self as api, sync::sync_events::v5},
40 events::{
41 StateEvent, StateEventType,
42 ignored_user_list::IgnoredUserListEventContent,
43 push_rules::{PushRulesEvent, PushRulesEventContent},
44 room::member::SyncRoomMemberEvent,
45 },
46 push::Ruleset,
47 time::Instant,
48};
49use tokio::sync::{Mutex, broadcast};
50#[cfg(feature = "e2e-encryption")]
51use tokio::sync::{RwLock, RwLockReadGuard};
52use tracing::{Level, debug, enabled, info, instrument, warn};
53
54#[cfg(feature = "e2e-encryption")]
55use crate::RoomMemberships;
56use crate::{
57 InviteAcceptanceDetails, RoomStateFilter, SessionMeta,
58 deserialized_responses::DisplayName,
59 error::{Error, Result},
60 event_cache::store::{EventCacheStoreLock, EventCacheStoreLockState},
61 media::store::MediaStoreLock,
62 response_processors::{self as processors, Context},
63 room::{
64 Room, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMembersUpdate, RoomState,
65 },
66 store::{
67 BaseStateStore, DynStateStore, MemoryStore, Result as StoreResult, RoomLoadSettings,
68 StateChanges, StateStoreDataKey, StateStoreDataValue, StateStoreExt, StoreConfig,
69 ambiguity_map::AmbiguityCache,
70 },
71 sync::{RoomUpdates, SyncResponse},
72};
73
74#[derive(Clone)]
89pub struct BaseClient {
90 pub(crate) state_store: BaseStateStore,
92
93 event_cache_store: EventCacheStoreLock,
95
96 media_store: MediaStoreLock,
98
99 #[cfg(feature = "e2e-encryption")]
104 crypto_store: Arc<DynCryptoStore>,
105
106 #[cfg(feature = "e2e-encryption")]
110 olm_machine: Arc<RwLock<Option<OlmMachine>>>,
111
112 pub(crate) ignore_user_list_changes: SharedObservable<Vec<String>>,
114
115 pub(crate) room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
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 let (room_info_notable_update_sender, _room_info_notable_update_receiver) =
192 broadcast::channel(500);
193
194 BaseClient {
195 state_store: store,
196 event_cache_store: config.event_cache_store,
197 media_store: config.media_store,
198 #[cfg(feature = "e2e-encryption")]
199 crypto_store: config.crypto_store,
200 #[cfg(feature = "e2e-encryption")]
201 olm_machine: Default::default(),
202 ignore_user_list_changes: Default::default(),
203 room_info_notable_update_sender,
204 #[cfg(feature = "e2e-encryption")]
205 room_key_recipient_strategy: Default::default(),
206 #[cfg(feature = "e2e-encryption")]
207 decryption_settings: DecryptionSettings {
208 sender_device_trust_requirement: TrustRequirement::Untrusted,
209 },
210 #[cfg(feature = "e2e-encryption")]
211 handle_verification_events: true,
212 threading_support,
213 }
214 }
215
216 #[cfg(feature = "e2e-encryption")]
219 pub async fn clone_with_in_memory_state_store(
220 &self,
221 cross_process_store_locks_holder_name: &str,
222 handle_verification_events: bool,
223 ) -> Result<Self> {
224 let config = StoreConfig::new(cross_process_store_locks_holder_name.to_owned())
225 .state_store(MemoryStore::new());
226 let config = config.crypto_store(self.crypto_store.clone());
227
228 let copy = Self {
229 state_store: BaseStateStore::new(config.state_store),
230 event_cache_store: config.event_cache_store,
231 media_store: config.media_store,
232 crypto_store: self.crypto_store.clone(),
239 olm_machine: self.olm_machine.clone(),
240 ignore_user_list_changes: Default::default(),
241 room_info_notable_update_sender: self.room_info_notable_update_sender.clone(),
242 room_key_recipient_strategy: self.room_key_recipient_strategy.clone(),
243 decryption_settings: self.decryption_settings.clone(),
244 handle_verification_events,
245 threading_support: self.threading_support,
246 };
247
248 copy.state_store
249 .derive_from_other(&self.state_store, ©.room_info_notable_update_sender)
250 .await?;
251
252 Ok(copy)
253 }
254
255 #[cfg(not(feature = "e2e-encryption"))]
258 #[allow(clippy::unused_async)]
259 pub async fn clone_with_in_memory_state_store(
260 &self,
261 cross_process_store_locks_holder: &str,
262 _handle_verification_events: bool,
263 ) -> Result<Self> {
264 let config = StoreConfig::new(cross_process_store_locks_holder.to_owned())
265 .state_store(MemoryStore::new());
266 Ok(Self::new(config, ThreadingSupport::Disabled))
267 }
268
269 pub fn session_meta(&self) -> Option<&SessionMeta> {
275 self.state_store.session_meta()
276 }
277
278 pub fn rooms(&self) -> Vec<Room> {
280 self.state_store.rooms()
281 }
282
283 pub fn rooms_filtered(&self, filter: RoomStateFilter) -> Vec<Room> {
285 self.state_store.rooms_filtered(filter)
286 }
287
288 pub fn rooms_stream(
291 &self,
292 ) -> (Vector<Room>, impl Stream<Item = Vec<VectorDiff<Room>>> + use<>) {
293 self.state_store.rooms_stream()
294 }
295
296 pub fn get_or_create_room(&self, room_id: &RoomId, room_state: RoomState) -> Room {
299 self.state_store.get_or_create_room(
300 room_id,
301 room_state,
302 self.room_info_notable_update_sender.clone(),
303 )
304 }
305
306 pub fn state_store(&self) -> &DynStateStore {
308 self.state_store.deref()
309 }
310
311 pub fn event_cache_store(&self) -> &EventCacheStoreLock {
313 &self.event_cache_store
314 }
315
316 pub fn media_store(&self) -> &MediaStoreLock {
318 &self.media_store
319 }
320
321 pub fn is_active(&self) -> bool {
325 self.state_store.session_meta().is_some()
326 }
327
328 pub async fn activate(
360 &self,
361 session_meta: SessionMeta,
362 room_load_settings: RoomLoadSettings,
363 #[cfg(feature = "e2e-encryption")] custom_account: Option<
364 crate::crypto::vodozemac::olm::Account,
365 >,
366 ) -> Result<()> {
367 debug!(user_id = ?session_meta.user_id, device_id = ?session_meta.device_id, "Activating the client");
368
369 self.state_store
370 .load_rooms(
371 &session_meta.user_id,
372 room_load_settings,
373 &self.room_info_notable_update_sender,
374 )
375 .await?;
376 self.state_store.load_sync_token().await?;
377 self.state_store.set_session_meta(session_meta);
378
379 #[cfg(feature = "e2e-encryption")]
380 self.regenerate_olm(custom_account).await?;
381
382 Ok(())
383 }
384
385 #[cfg(feature = "e2e-encryption")]
389 pub async fn regenerate_olm(
390 &self,
391 custom_account: Option<crate::crypto::vodozemac::olm::Account>,
392 ) -> Result<()> {
393 tracing::debug!("regenerating OlmMachine");
394 let session_meta = self.session_meta().ok_or(Error::OlmError(OlmError::MissingSession))?;
395
396 let olm_machine = OlmMachine::with_store(
399 &session_meta.user_id,
400 &session_meta.device_id,
401 self.crypto_store.clone(),
402 custom_account,
403 )
404 .await
405 .map_err(OlmError::from)?;
406
407 *self.olm_machine.write().await = Some(olm_machine);
408 Ok(())
409 }
410
411 pub async fn sync_token(&self) -> Option<String> {
414 self.state_store.sync_token.read().await.clone()
415 }
416
417 pub async fn room_knocked(&self, room_id: &RoomId) -> Result<Room> {
421 let room = self.state_store.get_or_create_room(
422 room_id,
423 RoomState::Knocked,
424 self.room_info_notable_update_sender.clone(),
425 );
426
427 if room.state() != RoomState::Knocked {
428 let _state_store_lock = self.state_store_lock().lock().await;
429
430 let mut room_info = room.clone_info();
431 room_info.mark_as_knocked();
432 room_info.mark_state_partially_synced();
433 room_info.mark_members_missing(); let mut changes = StateChanges::default();
435 changes.add_room(room_info.clone());
436 self.state_store.save_changes(&changes).await?; room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP);
438 }
439
440 Ok(room)
441 }
442
443 pub async fn room_joined(
482 &self,
483 room_id: &RoomId,
484 inviter: Option<OwnedUserId>,
485 ) -> Result<Room> {
486 let room = self.state_store.get_or_create_room(
487 room_id,
488 RoomState::Joined,
489 self.room_info_notable_update_sender.clone(),
490 );
491
492 if room.state() != RoomState::Joined {
495 let _state_store_lock = self.state_store_lock().lock().await;
496
497 let mut room_info = room.clone_info();
498 let previous_state = room.state();
499
500 room_info.mark_as_joined();
501 room_info.mark_state_partially_synced();
502 room_info.mark_members_missing(); if previous_state == RoomState::Invited
515 && let Some(inviter) = inviter
516 {
517 let details = InviteAcceptanceDetails {
518 invite_accepted_at: MilliSecondsSinceUnixEpoch::now(),
519 inviter,
520 };
521 room_info.set_invite_acceptance_details(details);
522 }
523
524 let mut changes = StateChanges::default();
525 changes.add_room(room_info.clone());
526
527 self.state_store.save_changes(&changes).await?; room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP);
530 }
531
532 Ok(room)
533 }
534
535 pub async fn room_left(&self, room_id: &RoomId) -> Result<()> {
539 let room = self.state_store.get_or_create_room(
540 room_id,
541 RoomState::Left,
542 self.room_info_notable_update_sender.clone(),
543 );
544
545 if room.state() != RoomState::Left {
546 let _state_store_lock = self.state_store_lock().lock().await;
547
548 let mut room_info = room.clone_info();
549 room_info.mark_as_left();
550 room_info.mark_state_partially_synced();
551 room_info.mark_members_missing(); let mut changes = StateChanges::default();
553 changes.add_room(room_info.clone());
554 self.state_store.save_changes(&changes).await?; room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP);
556 }
557
558 Ok(())
559 }
560
561 pub fn state_store_lock(&self) -> &Mutex<()> {
566 self.state_store.lock()
567 }
568
569 #[instrument(skip_all)]
575 pub async fn receive_sync_response(
576 &self,
577 response: api::sync::sync_events::v3::Response,
578 ) -> Result<SyncResponse> {
579 self.receive_sync_response_with_requested_required_states(
580 response,
581 &RequestedRequiredStates::default(),
582 )
583 .await
584 }
585
586 pub async fn receive_sync_response_with_requested_required_states(
594 &self,
595 response: api::sync::sync_events::v3::Response,
596 requested_required_states: &RequestedRequiredStates,
597 ) -> Result<SyncResponse> {
598 if self.state_store.sync_token.read().await.as_ref() == Some(&response.next_batch) {
602 info!("Got the same sync response twice");
603 return Ok(SyncResponse::default());
604 }
605
606 let now = if enabled!(Level::INFO) { Some(Instant::now()) } else { None };
607
608 #[cfg(feature = "e2e-encryption")]
609 let olm_machine = self.olm_machine().await;
610
611 let mut context = Context::new(StateChanges::new(response.next_batch.clone()));
612
613 #[cfg(feature = "e2e-encryption")]
614 let to_device = {
615 let processors::e2ee::to_device::Output {
616 processed_to_device_events: to_device,
617 room_key_updates,
618 } = processors::e2ee::to_device::from_sync_v2(
619 &response,
620 olm_machine.as_ref(),
621 &self.decryption_settings,
622 )
623 .await?;
624
625 processors::latest_event::decrypt_from_rooms(
626 &mut context,
627 room_key_updates
628 .into_iter()
629 .flatten()
630 .filter_map(|room_key_info| self.get_room(&room_key_info.room_id))
631 .collect(),
632 processors::e2ee::E2EE::new(
633 olm_machine.as_ref(),
634 &self.decryption_settings,
635 self.handle_verification_events,
636 ),
637 )
638 .await?;
639
640 to_device
641 };
642
643 #[cfg(not(feature = "e2e-encryption"))]
644 let to_device = response
645 .to_device
646 .events
647 .into_iter()
648 .map(|raw| {
649 use matrix_sdk_common::deserialized_responses::{
650 ProcessedToDeviceEvent, ToDeviceUnableToDecryptInfo,
651 ToDeviceUnableToDecryptReason,
652 };
653
654 if let Ok(Some(event_type)) = raw.get_field::<String>("type") {
655 if event_type == "m.room.encrypted" {
656 ProcessedToDeviceEvent::UnableToDecrypt {
657 encrypted_event: raw,
658 utd_info: ToDeviceUnableToDecryptInfo {
659 reason: ToDeviceUnableToDecryptReason::EncryptionIsDisabled,
660 },
661 }
662 } else {
663 ProcessedToDeviceEvent::PlainText(raw)
664 }
665 } else {
666 ProcessedToDeviceEvent::Invalid(raw)
668 }
669 })
670 .collect();
671
672 let mut ambiguity_cache = AmbiguityCache::new(self.state_store.inner.clone());
673
674 let global_account_data_processor =
675 processors::account_data::global(&response.account_data.events);
676
677 let push_rules = self.get_push_rules(&global_account_data_processor).await?;
678
679 let mut room_updates = RoomUpdates::default();
680 let mut notifications = Default::default();
681
682 let mut updated_members_in_room: BTreeMap<OwnedRoomId, BTreeSet<OwnedUserId>> =
683 BTreeMap::new();
684
685 for (room_id, joined_room) in response.rooms.join {
686 let joined_room_update = processors::room::sync_v2::update_joined_room(
687 &mut context,
688 processors::room::RoomCreationData::new(
689 &room_id,
690 self.room_info_notable_update_sender.clone(),
691 requested_required_states,
692 &mut ambiguity_cache,
693 ),
694 joined_room,
695 &mut updated_members_in_room,
696 processors::notification::Notification::new(
697 &push_rules,
698 &mut notifications,
699 &self.state_store,
700 ),
701 #[cfg(feature = "e2e-encryption")]
702 processors::e2ee::E2EE::new(
703 olm_machine.as_ref(),
704 &self.decryption_settings,
705 self.handle_verification_events,
706 ),
707 )
708 .await?;
709
710 room_updates.joined.insert(room_id, joined_room_update);
711 }
712
713 for (room_id, left_room) in response.rooms.leave {
714 let left_room_update = processors::room::sync_v2::update_left_room(
715 &mut context,
716 processors::room::RoomCreationData::new(
717 &room_id,
718 self.room_info_notable_update_sender.clone(),
719 requested_required_states,
720 &mut ambiguity_cache,
721 ),
722 left_room,
723 processors::notification::Notification::new(
724 &push_rules,
725 &mut notifications,
726 &self.state_store,
727 ),
728 #[cfg(feature = "e2e-encryption")]
729 processors::e2ee::E2EE::new(
730 olm_machine.as_ref(),
731 &self.decryption_settings,
732 self.handle_verification_events,
733 ),
734 )
735 .await?;
736
737 room_updates.left.insert(room_id, left_room_update);
738 }
739
740 for (room_id, invited_room) in response.rooms.invite {
741 let invited_room_update = processors::room::sync_v2::update_invited_room(
742 &mut context,
743 &room_id,
744 invited_room,
745 self.room_info_notable_update_sender.clone(),
746 processors::notification::Notification::new(
747 &push_rules,
748 &mut notifications,
749 &self.state_store,
750 ),
751 )
752 .await?;
753
754 room_updates.invited.insert(room_id, invited_room_update);
755 }
756
757 for (room_id, knocked_room) in response.rooms.knock {
758 let knocked_room_update = processors::room::sync_v2::update_knocked_room(
759 &mut context,
760 &room_id,
761 knocked_room,
762 self.room_info_notable_update_sender.clone(),
763 processors::notification::Notification::new(
764 &push_rules,
765 &mut notifications,
766 &self.state_store,
767 ),
768 )
769 .await?;
770
771 room_updates.knocked.insert(room_id, knocked_room_update);
772 }
773
774 global_account_data_processor.apply(&mut context, &self.state_store).await;
775
776 context.state_changes.presence = response
777 .presence
778 .events
779 .iter()
780 .filter_map(|e| {
781 let event = e.deserialize().ok()?;
782 Some((event.sender, e.clone()))
783 })
784 .collect();
785
786 context.state_changes.ambiguity_maps = ambiguity_cache.cache;
787
788 {
789 let _state_store_lock = self.state_store_lock().lock().await;
790
791 processors::changes::save_and_apply(
792 context,
793 &self.state_store,
794 &self.ignore_user_list_changes,
795 Some(response.next_batch.clone()),
796 )
797 .await?;
798 }
799
800 let mut context = Context::default();
801
802 processors::room::display_name::update_for_rooms(
805 &mut context,
806 &room_updates,
807 &self.state_store,
808 )
809 .await;
810
811 {
813 let _state_store_lock = self.state_store_lock().lock().await;
814
815 processors::changes::save_only(context, &self.state_store).await?;
816 }
817
818 for (room_id, member_ids) in updated_members_in_room {
819 if let Some(room) = self.get_room(&room_id) {
820 let _ =
821 room.room_member_updates_sender.send(RoomMembersUpdate::Partial(member_ids));
822 }
823 }
824
825 if enabled!(Level::INFO) {
826 info!("Processed a sync response in {:?}", now.map(|now| now.elapsed()));
827 }
828
829 let response = SyncResponse {
830 rooms: room_updates,
831 presence: response.presence.events,
832 account_data: response.account_data.events,
833 to_device,
834 notifications,
835 };
836
837 Ok(response)
838 }
839
840 #[instrument(skip_all, fields(?room_id))]
852 pub async fn receive_all_members(
853 &self,
854 room_id: &RoomId,
855 request: &api::membership::get_member_events::v3::Request,
856 response: &api::membership::get_member_events::v3::Response,
857 ) -> Result<()> {
858 if request.membership.is_some() || request.not_membership.is_some() || request.at.is_some()
859 {
860 return Err(Error::InvalidReceiveMembersParameters);
864 }
865
866 let Some(room) = self.state_store.room(room_id) else {
867 return Ok(());
869 };
870
871 let mut chunk = Vec::with_capacity(response.chunk.len());
872 let mut context = Context::default();
873
874 #[cfg(feature = "e2e-encryption")]
875 let mut user_ids = BTreeSet::new();
876
877 let mut ambiguity_map: HashMap<DisplayName, BTreeSet<OwnedUserId>> = Default::default();
878
879 for raw_event in &response.chunk {
880 let member = match raw_event.deserialize() {
881 Ok(ev) => ev,
882 Err(e) => {
883 let event_id: Option<String> = raw_event.get_field("event_id").ok().flatten();
884 debug!(event_id, "Failed to deserialize member event: {e}");
885 continue;
886 }
887 };
888
889 #[cfg(feature = "e2e-encryption")]
899 match member.membership() {
900 MembershipState::Join | MembershipState::Invite => {
901 user_ids.insert(member.state_key().to_owned());
902 }
903 _ => (),
904 }
905
906 if let StateEvent::Original(e) = &member
907 && let Some(d) = &e.content.displayname
908 {
909 let display_name = DisplayName::new(d);
910 ambiguity_map.entry(display_name).or_default().insert(member.state_key().clone());
911 }
912
913 let sync_member: SyncRoomMemberEvent = member.clone().into();
914 processors::profiles::upsert_or_delete(&mut context, room_id, &sync_member);
915
916 context
917 .state_changes
918 .state
919 .entry(room_id.to_owned())
920 .or_default()
921 .entry(member.event_type())
922 .or_default()
923 .insert(member.state_key().to_string(), raw_event.clone().cast());
924 chunk.push(member);
925 }
926
927 #[cfg(feature = "e2e-encryption")]
928 processors::e2ee::tracked_users::update(
929 self.olm_machine().await.as_ref(),
930 room.encryption_state(),
931 &user_ids,
932 )
933 .await?;
934
935 context.state_changes.ambiguity_maps.insert(room_id.to_owned(), ambiguity_map);
936
937 {
938 let _state_store_lock = self.state_store_lock().lock().await;
939
940 let mut room_info = room.clone_info();
941 room_info.mark_members_synced();
942 context.state_changes.add_room(room_info);
943
944 processors::changes::save_and_apply(
945 context,
946 &self.state_store,
947 &self.ignore_user_list_changes,
948 None,
949 )
950 .await?;
951 }
952
953 let _ = room.room_member_updates_sender.send(RoomMembersUpdate::FullReload);
954
955 Ok(())
956 }
957
958 pub async fn receive_filter_upload(
974 &self,
975 filter_name: &str,
976 response: &api::filter::create_filter::v3::Response,
977 ) -> Result<()> {
978 Ok(self
979 .state_store
980 .set_kv_data(
981 StateStoreDataKey::Filter(filter_name),
982 StateStoreDataValue::Filter(response.filter_id.clone()),
983 )
984 .await?)
985 }
986
987 pub async fn get_filter(&self, filter_name: &str) -> StoreResult<Option<String>> {
999 let filter = self
1000 .state_store
1001 .get_kv_data(StateStoreDataKey::Filter(filter_name))
1002 .await?
1003 .map(|d| d.into_filter().expect("State store data not a filter"));
1004
1005 Ok(filter)
1006 }
1007
1008 #[cfg(feature = "e2e-encryption")]
1010 pub async fn share_room_key(&self, room_id: &RoomId) -> Result<Vec<Arc<ToDeviceRequest>>> {
1011 match self.olm_machine().await.as_ref() {
1012 Some(o) => {
1013 let Some(room) = self.get_room(room_id) else {
1014 return Err(Error::InsufficientData);
1015 };
1016
1017 let history_visibility = room.history_visibility_or_default();
1018 let Some(room_encryption_event) = room.encryption_settings() else {
1019 return Err(Error::EncryptionNotEnabled);
1020 };
1021
1022 let filter = if history_visibility == HistoryVisibility::Joined {
1025 RoomMemberships::JOIN
1026 } else {
1027 RoomMemberships::ACTIVE
1028 };
1029
1030 let members = self.state_store.get_user_ids(room_id, filter).await?;
1031
1032 let settings = EncryptionSettings::new(
1033 room_encryption_event,
1034 history_visibility,
1035 self.room_key_recipient_strategy.clone(),
1036 );
1037
1038 Ok(o.share_room_key(room_id, members.iter().map(Deref::deref), settings).await?)
1039 }
1040 None => panic!("Olm machine wasn't started"),
1041 }
1042 }
1043
1044 pub fn get_room(&self, room_id: &RoomId) -> Option<Room> {
1050 self.state_store.room(room_id)
1051 }
1052
1053 pub async fn forget_room(&self, room_id: &RoomId) -> Result<()> {
1061 self.state_store.forget_room(room_id).await?;
1063
1064 match self.event_cache_store().lock().await? {
1066 EventCacheStoreLockState::Clean(guard) | EventCacheStoreLockState::Dirty(guard) => {
1071 guard.remove_room(room_id).await?
1072 }
1073 }
1074
1075 Ok(())
1076 }
1077
1078 #[cfg(feature = "e2e-encryption")]
1080 pub async fn olm_machine(&self) -> RwLockReadGuard<'_, Option<OlmMachine>> {
1081 self.olm_machine.read().await
1082 }
1083
1084 pub(crate) async fn get_push_rules(
1090 &self,
1091 global_account_data_processor: &processors::account_data::Global,
1092 ) -> Result<Ruleset> {
1093 let _timer = timer!(Level::TRACE, "get_push_rules");
1094 if let Some(event) = global_account_data_processor
1095 .push_rules()
1096 .and_then(|ev| ev.deserialize_as_unchecked::<PushRulesEvent>().ok())
1097 {
1098 Ok(event.content.global)
1099 } else if let Some(event) = self
1100 .state_store
1101 .get_account_data_event_static::<PushRulesEventContent>()
1102 .await?
1103 .and_then(|ev| ev.deserialize().ok())
1104 {
1105 Ok(event.content.global)
1106 } else if let Some(session_meta) = self.state_store.session_meta() {
1107 Ok(Ruleset::server_default(&session_meta.user_id))
1108 } else {
1109 Ok(Ruleset::new())
1110 }
1111 }
1112
1113 pub fn subscribe_to_ignore_user_list_changes(&self) -> Subscriber<Vec<String>> {
1116 self.ignore_user_list_changes.subscribe()
1117 }
1118
1119 pub fn room_info_notable_update_receiver(&self) -> broadcast::Receiver<RoomInfoNotableUpdate> {
1123 self.room_info_notable_update_sender.subscribe()
1124 }
1125
1126 pub async fn is_user_ignored(&self, user_id: &UserId) -> bool {
1128 match self.state_store.get_account_data_event_static::<IgnoredUserListEventContent>().await
1129 {
1130 Ok(Some(raw_ignored_user_list)) => match raw_ignored_user_list.deserialize() {
1131 Ok(current_ignored_user_list) => {
1132 current_ignored_user_list.content.ignored_users.contains_key(user_id)
1133 }
1134 Err(error) => {
1135 warn!(?error, "Failed to deserialize the ignored user list event");
1136 false
1137 }
1138 },
1139 Ok(None) => false,
1140 Err(error) => {
1141 warn!(?error, "Could not get the ignored user list from the state store");
1142 false
1143 }
1144 }
1145 }
1146}
1147
1148#[derive(Debug, Default)]
1160pub struct RequestedRequiredStates {
1161 default: Vec<(StateEventType, String)>,
1162 for_rooms: HashMap<OwnedRoomId, Vec<(StateEventType, String)>>,
1163}
1164
1165impl RequestedRequiredStates {
1166 pub fn new(
1171 default: Vec<(StateEventType, String)>,
1172 for_rooms: HashMap<OwnedRoomId, Vec<(StateEventType, String)>>,
1173 ) -> Self {
1174 Self { default, for_rooms }
1175 }
1176
1177 pub fn for_room(&self, room_id: &RoomId) -> &[(StateEventType, String)] {
1179 self.for_rooms.get(room_id).unwrap_or(&self.default)
1180 }
1181}
1182
1183impl From<&v5::Request> for RequestedRequiredStates {
1184 fn from(request: &v5::Request) -> Self {
1185 let mut default = BTreeSet::new();
1192
1193 for list in request.lists.values() {
1194 default.extend(BTreeSet::from_iter(list.room_details.required_state.iter().cloned()));
1195 }
1196
1197 for room_subscription in request.room_subscriptions.values() {
1198 default.extend(BTreeSet::from_iter(room_subscription.required_state.iter().cloned()));
1199 }
1200
1201 Self { default: default.into_iter().collect(), for_rooms: HashMap::new() }
1202 }
1203}
1204
1205#[cfg(test)]
1206mod tests {
1207 use std::collections::HashMap;
1208
1209 use assert_matches2::{assert_let, assert_matches};
1210 use futures_util::FutureExt as _;
1211 use matrix_sdk_test::{
1212 BOB, InvitedRoomBuilder, LeftRoomBuilder, StateTestEvent, StrippedStateTestEvent,
1213 SyncResponseBuilder, async_test, event_factory::EventFactory, ruma_response_from_json,
1214 };
1215 use ruma::{
1216 api::client::{self as api, sync::sync_events::v5},
1217 event_id,
1218 events::{StateEventType, room::member::MembershipState},
1219 room_id,
1220 serde::Raw,
1221 user_id,
1222 };
1223 use serde_json::{json, value::to_raw_value};
1224
1225 use super::{BaseClient, RequestedRequiredStates};
1226 use crate::{
1227 RoomDisplayName, RoomState, SessionMeta,
1228 client::ThreadingSupport,
1229 store::{RoomLoadSettings, StateStoreExt, StoreConfig},
1230 test_utils::logged_in_base_client,
1231 };
1232
1233 #[test]
1234 fn test_requested_required_states() {
1235 let room_id_0 = room_id!("!r0");
1236 let room_id_1 = room_id!("!r1");
1237
1238 let requested_required_states = RequestedRequiredStates::new(
1239 vec![(StateEventType::RoomAvatar, "".to_owned())],
1240 HashMap::from([(
1241 room_id_0.to_owned(),
1242 vec![
1243 (StateEventType::RoomMember, "foo".to_owned()),
1244 (StateEventType::RoomEncryption, "".to_owned()),
1245 ],
1246 )]),
1247 );
1248
1249 assert_eq!(
1251 requested_required_states.for_room(room_id_0),
1252 &[
1253 (StateEventType::RoomMember, "foo".to_owned()),
1254 (StateEventType::RoomEncryption, "".to_owned()),
1255 ]
1256 );
1257
1258 assert_eq!(
1260 requested_required_states.for_room(room_id_1),
1261 &[(StateEventType::RoomAvatar, "".to_owned()),]
1262 );
1263 }
1264
1265 #[test]
1266 fn test_requested_required_states_from_sync_v5_request() {
1267 let room_id_0 = room_id!("!r0");
1268 let room_id_1 = room_id!("!r1");
1269
1270 let mut request = v5::Request::new();
1272
1273 {
1274 let requested_required_states = RequestedRequiredStates::from(&request);
1275
1276 assert!(requested_required_states.default.is_empty());
1277 assert!(requested_required_states.for_rooms.is_empty());
1278 }
1279
1280 request.lists.insert("foo".to_owned(), {
1282 let mut list = v5::request::List::default();
1283 list.room_details.required_state = vec![
1284 (StateEventType::RoomAvatar, "".to_owned()),
1285 (StateEventType::RoomEncryption, "".to_owned()),
1286 ];
1287
1288 list
1289 });
1290
1291 {
1292 let requested_required_states = RequestedRequiredStates::from(&request);
1293
1294 assert_eq!(
1295 requested_required_states.default,
1296 &[
1297 (StateEventType::RoomAvatar, "".to_owned()),
1298 (StateEventType::RoomEncryption, "".to_owned())
1299 ]
1300 );
1301 assert!(requested_required_states.for_rooms.is_empty());
1302 }
1303
1304 request.lists.insert("bar".to_owned(), {
1306 let mut list = v5::request::List::default();
1307 list.room_details.required_state = vec![
1308 (StateEventType::RoomEncryption, "".to_owned()),
1309 (StateEventType::RoomName, "".to_owned()),
1310 ];
1311
1312 list
1313 });
1314
1315 {
1316 let requested_required_states = RequestedRequiredStates::from(&request);
1317
1318 assert_eq!(
1320 requested_required_states.default,
1321 &[
1322 (StateEventType::RoomAvatar, "".to_owned()),
1323 (StateEventType::RoomEncryption, "".to_owned()),
1324 (StateEventType::RoomName, "".to_owned()),
1325 ]
1326 );
1327 assert!(requested_required_states.for_rooms.is_empty());
1328 }
1329
1330 request.room_subscriptions.insert(room_id_0.to_owned(), {
1332 let mut room_subscription = v5::request::RoomSubscription::default();
1333
1334 room_subscription.required_state = vec![
1335 (StateEventType::RoomJoinRules, "".to_owned()),
1336 (StateEventType::RoomEncryption, "".to_owned()),
1337 ];
1338
1339 room_subscription
1340 });
1341
1342 {
1343 let requested_required_states = RequestedRequiredStates::from(&request);
1344
1345 assert_eq!(
1347 requested_required_states.default,
1348 &[
1349 (StateEventType::RoomAvatar, "".to_owned()),
1350 (StateEventType::RoomEncryption, "".to_owned()),
1351 (StateEventType::RoomJoinRules, "".to_owned()),
1352 (StateEventType::RoomName, "".to_owned()),
1353 ]
1354 );
1355 assert!(requested_required_states.for_rooms.is_empty());
1356 }
1357
1358 request.room_subscriptions.insert(room_id_1.to_owned(), {
1360 let mut room_subscription = v5::request::RoomSubscription::default();
1361
1362 room_subscription.required_state = vec![
1363 (StateEventType::RoomName, "".to_owned()),
1364 (StateEventType::RoomTopic, "".to_owned()),
1365 ];
1366
1367 room_subscription
1368 });
1369
1370 {
1371 let requested_required_states = RequestedRequiredStates::from(&request);
1372
1373 assert_eq!(
1375 requested_required_states.default,
1376 &[
1377 (StateEventType::RoomAvatar, "".to_owned()),
1378 (StateEventType::RoomEncryption, "".to_owned()),
1379 (StateEventType::RoomJoinRules, "".to_owned()),
1380 (StateEventType::RoomName, "".to_owned()),
1381 (StateEventType::RoomTopic, "".to_owned()),
1382 ]
1383 );
1384 }
1385 }
1386
1387 #[async_test]
1388 async fn test_invite_after_leaving() {
1389 let user_id = user_id!("@alice:example.org");
1390 let room_id = room_id!("!test:example.org");
1391
1392 let client = logged_in_base_client(Some(user_id)).await;
1393
1394 let mut sync_builder = SyncResponseBuilder::new();
1395
1396 let response = sync_builder
1397 .add_left_room(
1398 LeftRoomBuilder::new(room_id).add_timeline_event(
1399 EventFactory::new()
1400 .member(user_id)
1401 .membership(MembershipState::Leave)
1402 .display_name("Alice")
1403 .event_id(event_id!("$994173582443PhrSn:example.org")),
1404 ),
1405 )
1406 .build_sync_response();
1407 client.receive_sync_response(response).await.unwrap();
1408 assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left);
1409
1410 let response = sync_builder
1411 .add_invited_room(InvitedRoomBuilder::new(room_id).add_state_event(
1412 StrippedStateTestEvent::Custom(json!({
1413 "content": {
1414 "displayname": "Alice",
1415 "membership": "invite",
1416 },
1417 "event_id": "$143273582443PhrSn:example.org",
1418 "origin_server_ts": 1432735824653u64,
1419 "sender": "@example:example.org",
1420 "state_key": user_id,
1421 "type": "m.room.member",
1422 })),
1423 ))
1424 .build_sync_response();
1425 client.receive_sync_response(response).await.unwrap();
1426 assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Invited);
1427 }
1428
1429 #[async_test]
1430 async fn test_invite_displayname() {
1431 let user_id = user_id!("@alice:example.org");
1432 let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
1433
1434 let client = logged_in_base_client(Some(user_id)).await;
1435
1436 let response = ruma_response_from_json(&json!({
1437 "next_batch": "asdkl;fjasdkl;fj;asdkl;f",
1438 "device_one_time_keys_count": {
1439 "signed_curve25519": 50u64
1440 },
1441 "device_unused_fallback_key_types": [
1442 "signed_curve25519"
1443 ],
1444 "rooms": {
1445 "invite": {
1446 "!ithpyNKDtmhneaTQja:example.org": {
1447 "invite_state": {
1448 "events": [
1449 {
1450 "content": {
1451 "creator": "@test:example.org",
1452 "room_version": "9"
1453 },
1454 "sender": "@test:example.org",
1455 "state_key": "",
1456 "type": "m.room.create"
1457 },
1458 {
1459 "content": {
1460 "join_rule": "invite"
1461 },
1462 "sender": "@test:example.org",
1463 "state_key": "",
1464 "type": "m.room.join_rules"
1465 },
1466 {
1467 "content": {
1468 "algorithm": "m.megolm.v1.aes-sha2"
1469 },
1470 "sender": "@test:example.org",
1471 "state_key": "",
1472 "type": "m.room.encryption"
1473 },
1474 {
1475 "content": {
1476 "avatar_url": "mxc://example.org/dcBBDwuWEUrjfrOchvkirUST",
1477 "displayname": "Kyra",
1478 "membership": "join"
1479 },
1480 "sender": "@test:example.org",
1481 "state_key": "@test:example.org",
1482 "type": "m.room.member"
1483 },
1484 {
1485 "content": {
1486 "avatar_url": "mxc://example.org/ABFEXSDrESxovWwEnCYdNcHT",
1487 "displayname": "alice",
1488 "is_direct": true,
1489 "membership": "invite"
1490 },
1491 "origin_server_ts": 1650878657984u64,
1492 "sender": "@test:example.org",
1493 "state_key": "@alice:example.org",
1494 "type": "m.room.member",
1495 "unsigned": {
1496 "age": 14u64
1497 },
1498 "event_id": "$fLDqltg9Puj-kWItLSFVHPGN4YkgpYQf2qImPzdmgrE"
1499 }
1500 ]
1501 }
1502 }
1503 }
1504 }
1505 }));
1506
1507 client.receive_sync_response(response).await.unwrap();
1508
1509 let room = client.get_room(room_id).expect("Room not found");
1510 assert_eq!(room.state(), RoomState::Invited);
1511 assert_eq!(
1512 room.compute_display_name().await.expect("fetching display name failed").into_inner(),
1513 RoomDisplayName::Calculated("Kyra".to_owned())
1514 );
1515 }
1516
1517 #[async_test]
1518 async fn test_deserialization_failure() {
1519 let user_id = user_id!("@alice:example.org");
1520 let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
1521
1522 let client = BaseClient::new(
1523 StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
1524 ThreadingSupport::Disabled,
1525 );
1526 client
1527 .activate(
1528 SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
1529 RoomLoadSettings::default(),
1530 #[cfg(feature = "e2e-encryption")]
1531 None,
1532 )
1533 .await
1534 .unwrap();
1535
1536 let response = ruma_response_from_json(&json!({
1537 "next_batch": "asdkl;fjasdkl;fj;asdkl;f",
1538 "rooms": {
1539 "join": {
1540 "!ithpyNKDtmhneaTQja:example.org": {
1541 "state": {
1542 "events": [
1543 {
1544 "invalid": "invalid",
1545 },
1546 {
1547 "content": {
1548 "name": "The room name"
1549 },
1550 "event_id": "$143273582443PhrSn:example.org",
1551 "origin_server_ts": 1432735824653u64,
1552 "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
1553 "sender": "@example:example.org",
1554 "state_key": "",
1555 "type": "m.room.name",
1556 "unsigned": {
1557 "age": 1234
1558 }
1559 },
1560 ]
1561 }
1562 }
1563 }
1564 }
1565 }));
1566
1567 client.receive_sync_response(response).await.unwrap();
1568 client
1569 .state_store()
1570 .get_state_event_static::<ruma::events::room::name::RoomNameEventContent>(room_id)
1571 .await
1572 .expect("Failed to fetch state event")
1573 .expect("State event not found")
1574 .deserialize()
1575 .expect("Failed to deserialize state event");
1576 }
1577
1578 #[async_test]
1579 async fn test_invited_members_arent_ignored() {
1580 let user_id = user_id!("@alice:example.org");
1581 let inviter_user_id = user_id!("@bob:example.org");
1582 let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
1583
1584 let client = BaseClient::new(
1585 StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
1586 ThreadingSupport::Disabled,
1587 );
1588 client
1589 .activate(
1590 SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
1591 RoomLoadSettings::default(),
1592 #[cfg(feature = "e2e-encryption")]
1593 None,
1594 )
1595 .await
1596 .unwrap();
1597
1598 let mut sync_builder = SyncResponseBuilder::new();
1600 let response = sync_builder
1601 .add_joined_room(matrix_sdk_test::JoinedRoomBuilder::new(room_id))
1602 .build_sync_response();
1603 client.receive_sync_response(response).await.unwrap();
1604
1605 let request = api::membership::get_member_events::v3::Request::new(room_id.to_owned());
1608
1609 let raw_member_event = json!({
1610 "content": {
1611 "avatar_url": "mxc://localhost/fewjilfewjil42",
1612 "displayname": "Invited Alice",
1613 "membership": "invite"
1614 },
1615 "event_id": "$151800140517rfvjc:localhost",
1616 "origin_server_ts": 151800140,
1617 "room_id": room_id,
1618 "sender": inviter_user_id,
1619 "state_key": user_id,
1620 "type": "m.room.member",
1621 "unsigned": {
1622 "age": 13374242,
1623 }
1624 });
1625 let response = api::membership::get_member_events::v3::Response::new(vec![Raw::from_json(
1626 to_raw_value(&raw_member_event).unwrap(),
1627 )]);
1628
1629 client.receive_all_members(room_id, &request, &response).await.unwrap();
1631
1632 let room = client.get_room(room_id).unwrap();
1633
1634 let member = room.get_member(user_id).await.expect("ok").expect("exists");
1636
1637 assert_eq!(member.user_id(), user_id);
1638 assert_eq!(member.display_name().unwrap(), "Invited Alice");
1639 assert_eq!(member.avatar_url().unwrap().to_string(), "mxc://localhost/fewjilfewjil42");
1640 }
1641
1642 #[async_test]
1643 async fn test_reinvited_members_get_a_display_name() {
1644 let user_id = user_id!("@alice:example.org");
1645 let inviter_user_id = user_id!("@bob:example.org");
1646 let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
1647
1648 let client = BaseClient::new(
1649 StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
1650 ThreadingSupport::Disabled,
1651 );
1652 client
1653 .activate(
1654 SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
1655 RoomLoadSettings::default(),
1656 #[cfg(feature = "e2e-encryption")]
1657 None,
1658 )
1659 .await
1660 .unwrap();
1661
1662 let mut sync_builder = SyncResponseBuilder::new();
1664 let response = sync_builder
1665 .add_joined_room(matrix_sdk_test::JoinedRoomBuilder::new(room_id).add_state_event(
1666 StateTestEvent::Custom(json!({
1667 "content": {
1668 "avatar_url": null,
1669 "displayname": null,
1670 "membership": "leave"
1671 },
1672 "event_id": "$151803140217rkvjc:localhost",
1673 "origin_server_ts": 151800139,
1674 "room_id": room_id,
1675 "sender": user_id,
1676 "state_key": user_id,
1677 "type": "m.room.member",
1678 })),
1679 ))
1680 .build_sync_response();
1681 client.receive_sync_response(response).await.unwrap();
1682
1683 let request = api::membership::get_member_events::v3::Request::new(room_id.to_owned());
1685
1686 let raw_member_event = json!({
1687 "content": {
1688 "avatar_url": "mxc://localhost/fewjilfewjil42",
1689 "displayname": "Invited Alice",
1690 "membership": "invite"
1691 },
1692 "event_id": "$151800140517rfvjc:localhost",
1693 "origin_server_ts": 151800140,
1694 "room_id": room_id,
1695 "sender": inviter_user_id,
1696 "state_key": user_id,
1697 "type": "m.room.member",
1698 "unsigned": {
1699 "age": 13374242,
1700 }
1701 });
1702 let response = api::membership::get_member_events::v3::Response::new(vec![Raw::from_json(
1703 to_raw_value(&raw_member_event).unwrap(),
1704 )]);
1705
1706 client.receive_all_members(room_id, &request, &response).await.unwrap();
1708
1709 let room = client.get_room(room_id).unwrap();
1710
1711 let member = room.get_member(user_id).await.expect("ok").expect("exists");
1713
1714 assert_eq!(member.user_id(), user_id);
1715 assert_eq!(member.display_name().unwrap(), "Invited Alice");
1716 assert_eq!(member.avatar_url().unwrap().to_string(), "mxc://localhost/fewjilfewjil42");
1717 }
1718
1719 #[async_test]
1720 async fn test_ignored_user_list_changes() {
1721 let user_id = user_id!("@alice:example.org");
1722 let client = BaseClient::new(
1723 StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
1724 ThreadingSupport::Disabled,
1725 );
1726
1727 client
1728 .activate(
1729 SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
1730 RoomLoadSettings::default(),
1731 #[cfg(feature = "e2e-encryption")]
1732 None,
1733 )
1734 .await
1735 .unwrap();
1736
1737 let mut subscriber = client.subscribe_to_ignore_user_list_changes();
1738 assert!(subscriber.next().now_or_never().is_none());
1739
1740 let f = EventFactory::new();
1741 let mut sync_builder = SyncResponseBuilder::new();
1742 let response = sync_builder
1743 .add_global_account_data(f.ignored_user_list([(*BOB).into()]))
1744 .build_sync_response();
1745 client.receive_sync_response(response).await.unwrap();
1746
1747 assert_let!(Some(ignored) = subscriber.next().await);
1748 assert_eq!(ignored, [BOB.to_string()]);
1749
1750 let response = sync_builder
1752 .add_global_account_data(f.ignored_user_list([(*BOB).into()]))
1753 .build_sync_response();
1754 client.receive_sync_response(response).await.unwrap();
1755
1756 assert!(subscriber.next().now_or_never().is_none());
1758
1759 let response =
1761 sync_builder.add_global_account_data(f.ignored_user_list([])).build_sync_response();
1762 client.receive_sync_response(response).await.unwrap();
1763
1764 assert_let!(Some(ignored) = subscriber.next().await);
1765 assert!(ignored.is_empty());
1766 }
1767
1768 #[async_test]
1769 async fn test_is_user_ignored() {
1770 let ignored_user_id = user_id!("@alice:example.org");
1771 let client = logged_in_base_client(None).await;
1772
1773 let mut sync_builder = SyncResponseBuilder::new();
1774 let f = EventFactory::new();
1775 let response = sync_builder
1776 .add_global_account_data(f.ignored_user_list([ignored_user_id.to_owned()]))
1777 .build_sync_response();
1778 client.receive_sync_response(response).await.unwrap();
1779
1780 assert!(client.is_user_ignored(ignored_user_id).await);
1781 }
1782
1783 #[async_test]
1784 async fn test_invite_details_are_set() {
1785 let user_id = user_id!("@alice:localhost");
1786 let client = logged_in_base_client(Some(user_id)).await;
1787 let invited_room_id = room_id!("!invited:localhost");
1788 let unknown_room_id = room_id!("!unknown:localhost");
1789
1790 let mut sync_builder = SyncResponseBuilder::new();
1791 let response = sync_builder
1792 .add_invited_room(InvitedRoomBuilder::new(invited_room_id))
1793 .build_sync_response();
1794 client.receive_sync_response(response).await.unwrap();
1795
1796 let invited_room = client
1799 .get_room(invited_room_id)
1800 .expect("The sync should have created a room in the invited state");
1801
1802 assert_eq!(invited_room.state(), RoomState::Invited);
1803 assert!(invited_room.invite_acceptance_details().is_none());
1804
1805 let joined_room = client
1807 .room_joined(invited_room_id, Some(user_id.to_owned()))
1808 .await
1809 .expect("We should be able to mark a room as joined");
1810
1811 assert_eq!(joined_room.state(), RoomState::Joined);
1813 assert_matches!(joined_room.invite_acceptance_details(), Some(details));
1814 assert_eq!(details.inviter, user_id);
1815
1816 assert!(client.get_room(unknown_room_id).is_none());
1819 let unknown_room = client
1820 .room_joined(unknown_room_id, Some(user_id.to_owned()))
1821 .await
1822 .expect("We should be able to mark a room as joined");
1823
1824 assert_eq!(unknown_room.state(), RoomState::Joined);
1825 assert!(unknown_room.invite_acceptance_details().is_none());
1826
1827 sync_builder.clear();
1828 let response =
1829 sync_builder.add_left_room(LeftRoomBuilder::new(invited_room_id)).build_sync_response();
1830 client.receive_sync_response(response).await.unwrap();
1831
1832 let left_room = client
1834 .get_room(invited_room_id)
1835 .expect("The sync should have created a room in the invited state");
1836
1837 assert_eq!(left_room.state(), RoomState::Left);
1838 assert!(left_room.invite_acceptance_details().is_none());
1839 }
1840}