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 processors::e2ee::to_device::Output { processed_to_device_events: to_device } =
615 processors::e2ee::to_device::from_sync_v2(
616 &response,
617 olm_machine.as_ref(),
618 &self.decryption_settings,
619 )
620 .await?;
621
622 #[cfg(not(feature = "e2e-encryption"))]
623 let to_device = response
624 .to_device
625 .events
626 .into_iter()
627 .map(|raw| {
628 use matrix_sdk_common::deserialized_responses::{
629 ProcessedToDeviceEvent, ToDeviceUnableToDecryptInfo,
630 ToDeviceUnableToDecryptReason,
631 };
632
633 if let Ok(Some(event_type)) = raw.get_field::<String>("type") {
634 if event_type == "m.room.encrypted" {
635 ProcessedToDeviceEvent::UnableToDecrypt {
636 encrypted_event: raw,
637 utd_info: ToDeviceUnableToDecryptInfo {
638 reason: ToDeviceUnableToDecryptReason::EncryptionIsDisabled,
639 },
640 }
641 } else {
642 ProcessedToDeviceEvent::PlainText(raw)
643 }
644 } else {
645 ProcessedToDeviceEvent::Invalid(raw)
647 }
648 })
649 .collect();
650
651 let mut ambiguity_cache = AmbiguityCache::new(self.state_store.inner.clone());
652
653 let global_account_data_processor =
654 processors::account_data::global(&response.account_data.events);
655
656 let push_rules = self.get_push_rules(&global_account_data_processor).await?;
657
658 let mut room_updates = RoomUpdates::default();
659 let mut notifications = Default::default();
660
661 let mut updated_members_in_room: BTreeMap<OwnedRoomId, BTreeSet<OwnedUserId>> =
662 BTreeMap::new();
663
664 for (room_id, joined_room) in response.rooms.join {
665 let joined_room_update = processors::room::sync_v2::update_joined_room(
666 &mut context,
667 processors::room::RoomCreationData::new(
668 &room_id,
669 self.room_info_notable_update_sender.clone(),
670 requested_required_states,
671 &mut ambiguity_cache,
672 ),
673 joined_room,
674 &mut updated_members_in_room,
675 processors::notification::Notification::new(
676 &push_rules,
677 &mut notifications,
678 &self.state_store,
679 ),
680 #[cfg(feature = "e2e-encryption")]
681 processors::e2ee::E2EE::new(
682 olm_machine.as_ref(),
683 &self.decryption_settings,
684 self.handle_verification_events,
685 ),
686 )
687 .await?;
688
689 room_updates.joined.insert(room_id, joined_room_update);
690 }
691
692 for (room_id, left_room) in response.rooms.leave {
693 let left_room_update = processors::room::sync_v2::update_left_room(
694 &mut context,
695 processors::room::RoomCreationData::new(
696 &room_id,
697 self.room_info_notable_update_sender.clone(),
698 requested_required_states,
699 &mut ambiguity_cache,
700 ),
701 left_room,
702 processors::notification::Notification::new(
703 &push_rules,
704 &mut notifications,
705 &self.state_store,
706 ),
707 #[cfg(feature = "e2e-encryption")]
708 processors::e2ee::E2EE::new(
709 olm_machine.as_ref(),
710 &self.decryption_settings,
711 self.handle_verification_events,
712 ),
713 )
714 .await?;
715
716 room_updates.left.insert(room_id, left_room_update);
717 }
718
719 for (room_id, invited_room) in response.rooms.invite {
720 let invited_room_update = processors::room::sync_v2::update_invited_room(
721 &mut context,
722 &room_id,
723 invited_room,
724 self.room_info_notable_update_sender.clone(),
725 processors::notification::Notification::new(
726 &push_rules,
727 &mut notifications,
728 &self.state_store,
729 ),
730 )
731 .await?;
732
733 room_updates.invited.insert(room_id, invited_room_update);
734 }
735
736 for (room_id, knocked_room) in response.rooms.knock {
737 let knocked_room_update = processors::room::sync_v2::update_knocked_room(
738 &mut context,
739 &room_id,
740 knocked_room,
741 self.room_info_notable_update_sender.clone(),
742 processors::notification::Notification::new(
743 &push_rules,
744 &mut notifications,
745 &self.state_store,
746 ),
747 )
748 .await?;
749
750 room_updates.knocked.insert(room_id, knocked_room_update);
751 }
752
753 global_account_data_processor.apply(&mut context, &self.state_store).await;
754
755 context.state_changes.presence = response
756 .presence
757 .events
758 .iter()
759 .filter_map(|e| {
760 let event = e.deserialize().ok()?;
761 Some((event.sender, e.clone()))
762 })
763 .collect();
764
765 context.state_changes.ambiguity_maps = ambiguity_cache.cache;
766
767 {
768 let _state_store_lock = self.state_store_lock().lock().await;
769
770 processors::changes::save_and_apply(
771 context,
772 &self.state_store,
773 &self.ignore_user_list_changes,
774 Some(response.next_batch.clone()),
775 )
776 .await?;
777 }
778
779 let mut context = Context::default();
780
781 processors::room::display_name::update_for_rooms(
784 &mut context,
785 &room_updates,
786 &self.state_store,
787 )
788 .await;
789
790 {
792 let _state_store_lock = self.state_store_lock().lock().await;
793
794 processors::changes::save_only(context, &self.state_store).await?;
795 }
796
797 for (room_id, member_ids) in updated_members_in_room {
798 if let Some(room) = self.get_room(&room_id) {
799 let _ =
800 room.room_member_updates_sender.send(RoomMembersUpdate::Partial(member_ids));
801 }
802 }
803
804 if enabled!(Level::INFO) {
805 info!("Processed a sync response in {:?}", now.map(|now| now.elapsed()));
806 }
807
808 let response = SyncResponse {
809 rooms: room_updates,
810 presence: response.presence.events,
811 account_data: response.account_data.events,
812 to_device,
813 notifications,
814 };
815
816 Ok(response)
817 }
818
819 #[instrument(skip_all, fields(?room_id))]
831 pub async fn receive_all_members(
832 &self,
833 room_id: &RoomId,
834 request: &api::membership::get_member_events::v3::Request,
835 response: &api::membership::get_member_events::v3::Response,
836 ) -> Result<()> {
837 if request.membership.is_some() || request.not_membership.is_some() || request.at.is_some()
838 {
839 return Err(Error::InvalidReceiveMembersParameters);
843 }
844
845 let Some(room) = self.state_store.room(room_id) else {
846 return Ok(());
848 };
849
850 let mut chunk = Vec::with_capacity(response.chunk.len());
851 let mut context = Context::default();
852
853 #[cfg(feature = "e2e-encryption")]
854 let mut user_ids = BTreeSet::new();
855
856 let mut ambiguity_map: HashMap<DisplayName, BTreeSet<OwnedUserId>> = Default::default();
857
858 for raw_event in &response.chunk {
859 let member = match raw_event.deserialize() {
860 Ok(ev) => ev,
861 Err(e) => {
862 let event_id: Option<String> = raw_event.get_field("event_id").ok().flatten();
863 debug!(event_id, "Failed to deserialize member event: {e}");
864 continue;
865 }
866 };
867
868 #[cfg(feature = "e2e-encryption")]
878 match member.membership() {
879 MembershipState::Join | MembershipState::Invite => {
880 user_ids.insert(member.state_key().to_owned());
881 }
882 _ => (),
883 }
884
885 if let StateEvent::Original(e) = &member
886 && let Some(d) = &e.content.displayname
887 {
888 let display_name = DisplayName::new(d);
889 ambiguity_map.entry(display_name).or_default().insert(member.state_key().clone());
890 }
891
892 let sync_member: SyncRoomMemberEvent = member.clone().into();
893 processors::profiles::upsert_or_delete(&mut context, room_id, &sync_member);
894
895 context
896 .state_changes
897 .state
898 .entry(room_id.to_owned())
899 .or_default()
900 .entry(member.event_type())
901 .or_default()
902 .insert(member.state_key().to_string(), raw_event.clone().cast());
903 chunk.push(member);
904 }
905
906 #[cfg(feature = "e2e-encryption")]
907 processors::e2ee::tracked_users::update(
908 self.olm_machine().await.as_ref(),
909 room.encryption_state(),
910 &user_ids,
911 )
912 .await?;
913
914 context.state_changes.ambiguity_maps.insert(room_id.to_owned(), ambiguity_map);
915
916 {
917 let _state_store_lock = self.state_store_lock().lock().await;
918
919 let mut room_info = room.clone_info();
920 room_info.mark_members_synced();
921 context.state_changes.add_room(room_info);
922
923 processors::changes::save_and_apply(
924 context,
925 &self.state_store,
926 &self.ignore_user_list_changes,
927 None,
928 )
929 .await?;
930 }
931
932 let _ = room.room_member_updates_sender.send(RoomMembersUpdate::FullReload);
933
934 Ok(())
935 }
936
937 pub async fn receive_filter_upload(
953 &self,
954 filter_name: &str,
955 response: &api::filter::create_filter::v3::Response,
956 ) -> Result<()> {
957 Ok(self
958 .state_store
959 .set_kv_data(
960 StateStoreDataKey::Filter(filter_name),
961 StateStoreDataValue::Filter(response.filter_id.clone()),
962 )
963 .await?)
964 }
965
966 pub async fn get_filter(&self, filter_name: &str) -> StoreResult<Option<String>> {
978 let filter = self
979 .state_store
980 .get_kv_data(StateStoreDataKey::Filter(filter_name))
981 .await?
982 .map(|d| d.into_filter().expect("State store data not a filter"));
983
984 Ok(filter)
985 }
986
987 #[cfg(feature = "e2e-encryption")]
989 pub async fn share_room_key(&self, room_id: &RoomId) -> Result<Vec<Arc<ToDeviceRequest>>> {
990 match self.olm_machine().await.as_ref() {
991 Some(o) => {
992 let Some(room) = self.get_room(room_id) else {
993 return Err(Error::InsufficientData);
994 };
995
996 let history_visibility = room.history_visibility_or_default();
997 let Some(room_encryption_event) = room.encryption_settings() else {
998 return Err(Error::EncryptionNotEnabled);
999 };
1000
1001 let filter = if history_visibility == HistoryVisibility::Joined {
1004 RoomMemberships::JOIN
1005 } else {
1006 RoomMemberships::ACTIVE
1007 };
1008
1009 let members = self.state_store.get_user_ids(room_id, filter).await?;
1010
1011 let settings = EncryptionSettings::new(
1012 room_encryption_event,
1013 history_visibility,
1014 self.room_key_recipient_strategy.clone(),
1015 );
1016
1017 Ok(o.share_room_key(room_id, members.iter().map(Deref::deref), settings).await?)
1018 }
1019 None => panic!("Olm machine wasn't started"),
1020 }
1021 }
1022
1023 pub fn get_room(&self, room_id: &RoomId) -> Option<Room> {
1029 self.state_store.room(room_id)
1030 }
1031
1032 pub async fn forget_room(&self, room_id: &RoomId) -> Result<()> {
1040 self.state_store.forget_room(room_id).await?;
1042
1043 match self.event_cache_store().lock().await? {
1045 EventCacheStoreLockState::Clean(guard) | EventCacheStoreLockState::Dirty(guard) => {
1050 guard.remove_room(room_id).await?
1051 }
1052 }
1053
1054 Ok(())
1055 }
1056
1057 #[cfg(feature = "e2e-encryption")]
1059 pub async fn olm_machine(&self) -> RwLockReadGuard<'_, Option<OlmMachine>> {
1060 self.olm_machine.read().await
1061 }
1062
1063 pub(crate) async fn get_push_rules(
1069 &self,
1070 global_account_data_processor: &processors::account_data::Global,
1071 ) -> Result<Ruleset> {
1072 let _timer = timer!(Level::TRACE, "get_push_rules");
1073 if let Some(event) = global_account_data_processor
1074 .push_rules()
1075 .and_then(|ev| ev.deserialize_as_unchecked::<PushRulesEvent>().ok())
1076 {
1077 Ok(event.content.global)
1078 } else if let Some(event) = self
1079 .state_store
1080 .get_account_data_event_static::<PushRulesEventContent>()
1081 .await?
1082 .and_then(|ev| ev.deserialize().ok())
1083 {
1084 Ok(event.content.global)
1085 } else if let Some(session_meta) = self.state_store.session_meta() {
1086 Ok(Ruleset::server_default(&session_meta.user_id))
1087 } else {
1088 Ok(Ruleset::new())
1089 }
1090 }
1091
1092 pub fn subscribe_to_ignore_user_list_changes(&self) -> Subscriber<Vec<String>> {
1095 self.ignore_user_list_changes.subscribe()
1096 }
1097
1098 pub fn room_info_notable_update_receiver(&self) -> broadcast::Receiver<RoomInfoNotableUpdate> {
1102 self.room_info_notable_update_sender.subscribe()
1103 }
1104
1105 pub async fn is_user_ignored(&self, user_id: &UserId) -> bool {
1107 match self.state_store.get_account_data_event_static::<IgnoredUserListEventContent>().await
1108 {
1109 Ok(Some(raw_ignored_user_list)) => match raw_ignored_user_list.deserialize() {
1110 Ok(current_ignored_user_list) => {
1111 current_ignored_user_list.content.ignored_users.contains_key(user_id)
1112 }
1113 Err(error) => {
1114 warn!(?error, "Failed to deserialize the ignored user list event");
1115 false
1116 }
1117 },
1118 Ok(None) => false,
1119 Err(error) => {
1120 warn!(?error, "Could not get the ignored user list from the state store");
1121 false
1122 }
1123 }
1124 }
1125}
1126
1127#[derive(Debug, Default)]
1139pub struct RequestedRequiredStates {
1140 default: Vec<(StateEventType, String)>,
1141 for_rooms: HashMap<OwnedRoomId, Vec<(StateEventType, String)>>,
1142}
1143
1144impl RequestedRequiredStates {
1145 pub fn new(
1150 default: Vec<(StateEventType, String)>,
1151 for_rooms: HashMap<OwnedRoomId, Vec<(StateEventType, String)>>,
1152 ) -> Self {
1153 Self { default, for_rooms }
1154 }
1155
1156 pub fn for_room(&self, room_id: &RoomId) -> &[(StateEventType, String)] {
1158 self.for_rooms.get(room_id).unwrap_or(&self.default)
1159 }
1160}
1161
1162impl From<&v5::Request> for RequestedRequiredStates {
1163 fn from(request: &v5::Request) -> Self {
1164 let mut default = BTreeSet::new();
1171
1172 for list in request.lists.values() {
1173 default.extend(BTreeSet::from_iter(list.room_details.required_state.iter().cloned()));
1174 }
1175
1176 for room_subscription in request.room_subscriptions.values() {
1177 default.extend(BTreeSet::from_iter(room_subscription.required_state.iter().cloned()));
1178 }
1179
1180 Self { default: default.into_iter().collect(), for_rooms: HashMap::new() }
1181 }
1182}
1183
1184#[cfg(test)]
1185mod tests {
1186 use std::collections::HashMap;
1187
1188 use assert_matches2::{assert_let, assert_matches};
1189 use futures_util::FutureExt as _;
1190 use matrix_sdk_test::{
1191 BOB, InvitedRoomBuilder, LeftRoomBuilder, StateTestEvent, StrippedStateTestEvent,
1192 SyncResponseBuilder, async_test, event_factory::EventFactory, ruma_response_from_json,
1193 };
1194 use ruma::{
1195 api::client::{self as api, sync::sync_events::v5},
1196 event_id,
1197 events::{StateEventType, room::member::MembershipState},
1198 room_id,
1199 serde::Raw,
1200 user_id,
1201 };
1202 use serde_json::{json, value::to_raw_value};
1203
1204 use super::{BaseClient, RequestedRequiredStates};
1205 use crate::{
1206 RoomDisplayName, RoomState, SessionMeta,
1207 client::ThreadingSupport,
1208 store::{RoomLoadSettings, StateStoreExt, StoreConfig},
1209 test_utils::logged_in_base_client,
1210 };
1211
1212 #[test]
1213 fn test_requested_required_states() {
1214 let room_id_0 = room_id!("!r0");
1215 let room_id_1 = room_id!("!r1");
1216
1217 let requested_required_states = RequestedRequiredStates::new(
1218 vec![(StateEventType::RoomAvatar, "".to_owned())],
1219 HashMap::from([(
1220 room_id_0.to_owned(),
1221 vec![
1222 (StateEventType::RoomMember, "foo".to_owned()),
1223 (StateEventType::RoomEncryption, "".to_owned()),
1224 ],
1225 )]),
1226 );
1227
1228 assert_eq!(
1230 requested_required_states.for_room(room_id_0),
1231 &[
1232 (StateEventType::RoomMember, "foo".to_owned()),
1233 (StateEventType::RoomEncryption, "".to_owned()),
1234 ]
1235 );
1236
1237 assert_eq!(
1239 requested_required_states.for_room(room_id_1),
1240 &[(StateEventType::RoomAvatar, "".to_owned()),]
1241 );
1242 }
1243
1244 #[test]
1245 fn test_requested_required_states_from_sync_v5_request() {
1246 let room_id_0 = room_id!("!r0");
1247 let room_id_1 = room_id!("!r1");
1248
1249 let mut request = v5::Request::new();
1251
1252 {
1253 let requested_required_states = RequestedRequiredStates::from(&request);
1254
1255 assert!(requested_required_states.default.is_empty());
1256 assert!(requested_required_states.for_rooms.is_empty());
1257 }
1258
1259 request.lists.insert("foo".to_owned(), {
1261 let mut list = v5::request::List::default();
1262 list.room_details.required_state = vec![
1263 (StateEventType::RoomAvatar, "".to_owned()),
1264 (StateEventType::RoomEncryption, "".to_owned()),
1265 ];
1266
1267 list
1268 });
1269
1270 {
1271 let requested_required_states = RequestedRequiredStates::from(&request);
1272
1273 assert_eq!(
1274 requested_required_states.default,
1275 &[
1276 (StateEventType::RoomAvatar, "".to_owned()),
1277 (StateEventType::RoomEncryption, "".to_owned())
1278 ]
1279 );
1280 assert!(requested_required_states.for_rooms.is_empty());
1281 }
1282
1283 request.lists.insert("bar".to_owned(), {
1285 let mut list = v5::request::List::default();
1286 list.room_details.required_state = vec![
1287 (StateEventType::RoomEncryption, "".to_owned()),
1288 (StateEventType::RoomName, "".to_owned()),
1289 ];
1290
1291 list
1292 });
1293
1294 {
1295 let requested_required_states = RequestedRequiredStates::from(&request);
1296
1297 assert_eq!(
1299 requested_required_states.default,
1300 &[
1301 (StateEventType::RoomAvatar, "".to_owned()),
1302 (StateEventType::RoomEncryption, "".to_owned()),
1303 (StateEventType::RoomName, "".to_owned()),
1304 ]
1305 );
1306 assert!(requested_required_states.for_rooms.is_empty());
1307 }
1308
1309 request.room_subscriptions.insert(room_id_0.to_owned(), {
1311 let mut room_subscription = v5::request::RoomSubscription::default();
1312
1313 room_subscription.required_state = vec![
1314 (StateEventType::RoomJoinRules, "".to_owned()),
1315 (StateEventType::RoomEncryption, "".to_owned()),
1316 ];
1317
1318 room_subscription
1319 });
1320
1321 {
1322 let requested_required_states = RequestedRequiredStates::from(&request);
1323
1324 assert_eq!(
1326 requested_required_states.default,
1327 &[
1328 (StateEventType::RoomAvatar, "".to_owned()),
1329 (StateEventType::RoomEncryption, "".to_owned()),
1330 (StateEventType::RoomJoinRules, "".to_owned()),
1331 (StateEventType::RoomName, "".to_owned()),
1332 ]
1333 );
1334 assert!(requested_required_states.for_rooms.is_empty());
1335 }
1336
1337 request.room_subscriptions.insert(room_id_1.to_owned(), {
1339 let mut room_subscription = v5::request::RoomSubscription::default();
1340
1341 room_subscription.required_state = vec![
1342 (StateEventType::RoomName, "".to_owned()),
1343 (StateEventType::RoomTopic, "".to_owned()),
1344 ];
1345
1346 room_subscription
1347 });
1348
1349 {
1350 let requested_required_states = RequestedRequiredStates::from(&request);
1351
1352 assert_eq!(
1354 requested_required_states.default,
1355 &[
1356 (StateEventType::RoomAvatar, "".to_owned()),
1357 (StateEventType::RoomEncryption, "".to_owned()),
1358 (StateEventType::RoomJoinRules, "".to_owned()),
1359 (StateEventType::RoomName, "".to_owned()),
1360 (StateEventType::RoomTopic, "".to_owned()),
1361 ]
1362 );
1363 }
1364 }
1365
1366 #[async_test]
1367 async fn test_invite_after_leaving() {
1368 let user_id = user_id!("@alice:example.org");
1369 let room_id = room_id!("!test:example.org");
1370
1371 let client = logged_in_base_client(Some(user_id)).await;
1372
1373 let mut sync_builder = SyncResponseBuilder::new();
1374
1375 let response = sync_builder
1376 .add_left_room(
1377 LeftRoomBuilder::new(room_id).add_timeline_event(
1378 EventFactory::new()
1379 .member(user_id)
1380 .membership(MembershipState::Leave)
1381 .display_name("Alice")
1382 .event_id(event_id!("$994173582443PhrSn:example.org")),
1383 ),
1384 )
1385 .build_sync_response();
1386 client.receive_sync_response(response).await.unwrap();
1387 assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left);
1388
1389 let response = sync_builder
1390 .add_invited_room(InvitedRoomBuilder::new(room_id).add_state_event(
1391 StrippedStateTestEvent::Custom(json!({
1392 "content": {
1393 "displayname": "Alice",
1394 "membership": "invite",
1395 },
1396 "event_id": "$143273582443PhrSn:example.org",
1397 "origin_server_ts": 1432735824653u64,
1398 "sender": "@example:example.org",
1399 "state_key": user_id,
1400 "type": "m.room.member",
1401 })),
1402 ))
1403 .build_sync_response();
1404 client.receive_sync_response(response).await.unwrap();
1405 assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Invited);
1406 }
1407
1408 #[async_test]
1409 async fn test_invite_displayname() {
1410 let user_id = user_id!("@alice:example.org");
1411 let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
1412
1413 let client = logged_in_base_client(Some(user_id)).await;
1414
1415 let response = ruma_response_from_json(&json!({
1416 "next_batch": "asdkl;fjasdkl;fj;asdkl;f",
1417 "device_one_time_keys_count": {
1418 "signed_curve25519": 50u64
1419 },
1420 "device_unused_fallback_key_types": [
1421 "signed_curve25519"
1422 ],
1423 "rooms": {
1424 "invite": {
1425 "!ithpyNKDtmhneaTQja:example.org": {
1426 "invite_state": {
1427 "events": [
1428 {
1429 "content": {
1430 "creator": "@test:example.org",
1431 "room_version": "9"
1432 },
1433 "sender": "@test:example.org",
1434 "state_key": "",
1435 "type": "m.room.create"
1436 },
1437 {
1438 "content": {
1439 "join_rule": "invite"
1440 },
1441 "sender": "@test:example.org",
1442 "state_key": "",
1443 "type": "m.room.join_rules"
1444 },
1445 {
1446 "content": {
1447 "algorithm": "m.megolm.v1.aes-sha2"
1448 },
1449 "sender": "@test:example.org",
1450 "state_key": "",
1451 "type": "m.room.encryption"
1452 },
1453 {
1454 "content": {
1455 "avatar_url": "mxc://example.org/dcBBDwuWEUrjfrOchvkirUST",
1456 "displayname": "Kyra",
1457 "membership": "join"
1458 },
1459 "sender": "@test:example.org",
1460 "state_key": "@test:example.org",
1461 "type": "m.room.member"
1462 },
1463 {
1464 "content": {
1465 "avatar_url": "mxc://example.org/ABFEXSDrESxovWwEnCYdNcHT",
1466 "displayname": "alice",
1467 "is_direct": true,
1468 "membership": "invite"
1469 },
1470 "origin_server_ts": 1650878657984u64,
1471 "sender": "@test:example.org",
1472 "state_key": "@alice:example.org",
1473 "type": "m.room.member",
1474 "unsigned": {
1475 "age": 14u64
1476 },
1477 "event_id": "$fLDqltg9Puj-kWItLSFVHPGN4YkgpYQf2qImPzdmgrE"
1478 }
1479 ]
1480 }
1481 }
1482 }
1483 }
1484 }));
1485
1486 client.receive_sync_response(response).await.unwrap();
1487
1488 let room = client.get_room(room_id).expect("Room not found");
1489 assert_eq!(room.state(), RoomState::Invited);
1490 assert_eq!(
1491 room.compute_display_name().await.expect("fetching display name failed").into_inner(),
1492 RoomDisplayName::Calculated("Kyra".to_owned())
1493 );
1494 }
1495
1496 #[async_test]
1497 async fn test_deserialization_failure() {
1498 let user_id = user_id!("@alice:example.org");
1499 let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
1500
1501 let client = BaseClient::new(
1502 StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
1503 ThreadingSupport::Disabled,
1504 );
1505 client
1506 .activate(
1507 SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
1508 RoomLoadSettings::default(),
1509 #[cfg(feature = "e2e-encryption")]
1510 None,
1511 )
1512 .await
1513 .unwrap();
1514
1515 let response = ruma_response_from_json(&json!({
1516 "next_batch": "asdkl;fjasdkl;fj;asdkl;f",
1517 "rooms": {
1518 "join": {
1519 "!ithpyNKDtmhneaTQja:example.org": {
1520 "state": {
1521 "events": [
1522 {
1523 "invalid": "invalid",
1524 },
1525 {
1526 "content": {
1527 "name": "The room name"
1528 },
1529 "event_id": "$143273582443PhrSn:example.org",
1530 "origin_server_ts": 1432735824653u64,
1531 "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
1532 "sender": "@example:example.org",
1533 "state_key": "",
1534 "type": "m.room.name",
1535 "unsigned": {
1536 "age": 1234
1537 }
1538 },
1539 ]
1540 }
1541 }
1542 }
1543 }
1544 }));
1545
1546 client.receive_sync_response(response).await.unwrap();
1547 client
1548 .state_store()
1549 .get_state_event_static::<ruma::events::room::name::RoomNameEventContent>(room_id)
1550 .await
1551 .expect("Failed to fetch state event")
1552 .expect("State event not found")
1553 .deserialize()
1554 .expect("Failed to deserialize state event");
1555 }
1556
1557 #[async_test]
1558 async fn test_invited_members_arent_ignored() {
1559 let user_id = user_id!("@alice:example.org");
1560 let inviter_user_id = user_id!("@bob:example.org");
1561 let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
1562
1563 let client = BaseClient::new(
1564 StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
1565 ThreadingSupport::Disabled,
1566 );
1567 client
1568 .activate(
1569 SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
1570 RoomLoadSettings::default(),
1571 #[cfg(feature = "e2e-encryption")]
1572 None,
1573 )
1574 .await
1575 .unwrap();
1576
1577 let mut sync_builder = SyncResponseBuilder::new();
1579 let response = sync_builder
1580 .add_joined_room(matrix_sdk_test::JoinedRoomBuilder::new(room_id))
1581 .build_sync_response();
1582 client.receive_sync_response(response).await.unwrap();
1583
1584 let request = api::membership::get_member_events::v3::Request::new(room_id.to_owned());
1587
1588 let raw_member_event = json!({
1589 "content": {
1590 "avatar_url": "mxc://localhost/fewjilfewjil42",
1591 "displayname": "Invited Alice",
1592 "membership": "invite"
1593 },
1594 "event_id": "$151800140517rfvjc:localhost",
1595 "origin_server_ts": 151800140,
1596 "room_id": room_id,
1597 "sender": inviter_user_id,
1598 "state_key": user_id,
1599 "type": "m.room.member",
1600 "unsigned": {
1601 "age": 13374242,
1602 }
1603 });
1604 let response = api::membership::get_member_events::v3::Response::new(vec![Raw::from_json(
1605 to_raw_value(&raw_member_event).unwrap(),
1606 )]);
1607
1608 client.receive_all_members(room_id, &request, &response).await.unwrap();
1610
1611 let room = client.get_room(room_id).unwrap();
1612
1613 let member = room.get_member(user_id).await.expect("ok").expect("exists");
1615
1616 assert_eq!(member.user_id(), user_id);
1617 assert_eq!(member.display_name().unwrap(), "Invited Alice");
1618 assert_eq!(member.avatar_url().unwrap().to_string(), "mxc://localhost/fewjilfewjil42");
1619 }
1620
1621 #[async_test]
1622 async fn test_reinvited_members_get_a_display_name() {
1623 let user_id = user_id!("@alice:example.org");
1624 let inviter_user_id = user_id!("@bob:example.org");
1625 let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
1626
1627 let client = BaseClient::new(
1628 StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
1629 ThreadingSupport::Disabled,
1630 );
1631 client
1632 .activate(
1633 SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
1634 RoomLoadSettings::default(),
1635 #[cfg(feature = "e2e-encryption")]
1636 None,
1637 )
1638 .await
1639 .unwrap();
1640
1641 let mut sync_builder = SyncResponseBuilder::new();
1643 let response = sync_builder
1644 .add_joined_room(matrix_sdk_test::JoinedRoomBuilder::new(room_id).add_state_event(
1645 StateTestEvent::Custom(json!({
1646 "content": {
1647 "avatar_url": null,
1648 "displayname": null,
1649 "membership": "leave"
1650 },
1651 "event_id": "$151803140217rkvjc:localhost",
1652 "origin_server_ts": 151800139,
1653 "room_id": room_id,
1654 "sender": user_id,
1655 "state_key": user_id,
1656 "type": "m.room.member",
1657 })),
1658 ))
1659 .build_sync_response();
1660 client.receive_sync_response(response).await.unwrap();
1661
1662 let request = api::membership::get_member_events::v3::Request::new(room_id.to_owned());
1664
1665 let raw_member_event = json!({
1666 "content": {
1667 "avatar_url": "mxc://localhost/fewjilfewjil42",
1668 "displayname": "Invited Alice",
1669 "membership": "invite"
1670 },
1671 "event_id": "$151800140517rfvjc:localhost",
1672 "origin_server_ts": 151800140,
1673 "room_id": room_id,
1674 "sender": inviter_user_id,
1675 "state_key": user_id,
1676 "type": "m.room.member",
1677 "unsigned": {
1678 "age": 13374242,
1679 }
1680 });
1681 let response = api::membership::get_member_events::v3::Response::new(vec![Raw::from_json(
1682 to_raw_value(&raw_member_event).unwrap(),
1683 )]);
1684
1685 client.receive_all_members(room_id, &request, &response).await.unwrap();
1687
1688 let room = client.get_room(room_id).unwrap();
1689
1690 let member = room.get_member(user_id).await.expect("ok").expect("exists");
1692
1693 assert_eq!(member.user_id(), user_id);
1694 assert_eq!(member.display_name().unwrap(), "Invited Alice");
1695 assert_eq!(member.avatar_url().unwrap().to_string(), "mxc://localhost/fewjilfewjil42");
1696 }
1697
1698 #[async_test]
1699 async fn test_ignored_user_list_changes() {
1700 let user_id = user_id!("@alice:example.org");
1701 let client = BaseClient::new(
1702 StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
1703 ThreadingSupport::Disabled,
1704 );
1705
1706 client
1707 .activate(
1708 SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
1709 RoomLoadSettings::default(),
1710 #[cfg(feature = "e2e-encryption")]
1711 None,
1712 )
1713 .await
1714 .unwrap();
1715
1716 let mut subscriber = client.subscribe_to_ignore_user_list_changes();
1717 assert!(subscriber.next().now_or_never().is_none());
1718
1719 let f = EventFactory::new();
1720 let mut sync_builder = SyncResponseBuilder::new();
1721 let response = sync_builder
1722 .add_global_account_data(f.ignored_user_list([(*BOB).into()]))
1723 .build_sync_response();
1724 client.receive_sync_response(response).await.unwrap();
1725
1726 assert_let!(Some(ignored) = subscriber.next().await);
1727 assert_eq!(ignored, [BOB.to_string()]);
1728
1729 let response = sync_builder
1731 .add_global_account_data(f.ignored_user_list([(*BOB).into()]))
1732 .build_sync_response();
1733 client.receive_sync_response(response).await.unwrap();
1734
1735 assert!(subscriber.next().now_or_never().is_none());
1737
1738 let response =
1740 sync_builder.add_global_account_data(f.ignored_user_list([])).build_sync_response();
1741 client.receive_sync_response(response).await.unwrap();
1742
1743 assert_let!(Some(ignored) = subscriber.next().await);
1744 assert!(ignored.is_empty());
1745 }
1746
1747 #[async_test]
1748 async fn test_is_user_ignored() {
1749 let ignored_user_id = user_id!("@alice:example.org");
1750 let client = logged_in_base_client(None).await;
1751
1752 let mut sync_builder = SyncResponseBuilder::new();
1753 let f = EventFactory::new();
1754 let response = sync_builder
1755 .add_global_account_data(f.ignored_user_list([ignored_user_id.to_owned()]))
1756 .build_sync_response();
1757 client.receive_sync_response(response).await.unwrap();
1758
1759 assert!(client.is_user_ignored(ignored_user_id).await);
1760 }
1761
1762 #[async_test]
1763 async fn test_invite_details_are_set() {
1764 let user_id = user_id!("@alice:localhost");
1765 let client = logged_in_base_client(Some(user_id)).await;
1766 let invited_room_id = room_id!("!invited:localhost");
1767 let unknown_room_id = room_id!("!unknown:localhost");
1768
1769 let mut sync_builder = SyncResponseBuilder::new();
1770 let response = sync_builder
1771 .add_invited_room(InvitedRoomBuilder::new(invited_room_id))
1772 .build_sync_response();
1773 client.receive_sync_response(response).await.unwrap();
1774
1775 let invited_room = client
1778 .get_room(invited_room_id)
1779 .expect("The sync should have created a room in the invited state");
1780
1781 assert_eq!(invited_room.state(), RoomState::Invited);
1782 assert!(invited_room.invite_acceptance_details().is_none());
1783
1784 let joined_room = client
1786 .room_joined(invited_room_id, Some(user_id.to_owned()))
1787 .await
1788 .expect("We should be able to mark a room as joined");
1789
1790 assert_eq!(joined_room.state(), RoomState::Joined);
1792 assert_matches!(joined_room.invite_acceptance_details(), Some(details));
1793 assert_eq!(details.inviter, user_id);
1794
1795 assert!(client.get_room(unknown_room_id).is_none());
1798 let unknown_room = client
1799 .room_joined(unknown_room_id, Some(user_id.to_owned()))
1800 .await
1801 .expect("We should be able to mark a room as joined");
1802
1803 assert_eq!(unknown_room.state(), RoomState::Joined);
1804 assert!(unknown_room.invite_acceptance_details().is_none());
1805
1806 sync_builder.clear();
1807 let response =
1808 sync_builder.add_left_room(LeftRoomBuilder::new(invited_room_id)).build_sync_response();
1809 client.receive_sync_response(response).await.unwrap();
1810
1811 let left_room = client
1813 .get_room(invited_room_id)
1814 .expect("The sync should have created a room in the invited state");
1815
1816 assert_eq!(left_room.state(), RoomState::Left);
1817 assert!(left_room.invite_acceptance_details().is_none());
1818 }
1819}