use std::{
borrow::Borrow,
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
fmt,
ops::Deref,
result::Result as StdResult,
str::{FromStr, Utf8Error},
sync::{Arc, RwLock as StdRwLock},
};
use eyeball_im::{Vector, VectorDiff};
use futures_util::Stream;
use matrix_sdk_common::ROOM_VERSION_RULES_FALLBACK;
use once_cell::sync::OnceCell;
#[cfg(any(test, feature = "testing"))]
#[macro_use]
pub mod integration_tests;
mod observable_map;
mod traits;
use matrix_sdk_common::locks::Mutex as SyncMutex;
#[cfg(feature = "e2e-encryption")]
use matrix_sdk_crypto::store::{DynCryptoStore, IntoCryptoStore};
pub use matrix_sdk_store_encryption::Error as StoreEncryptionError;
use observable_map::ObservableMap;
use ruma::{
EventId, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
events::{
AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnyStrippedStateEvent,
AnySyncStateEvent, EmptyStateKey, GlobalAccountDataEventType, RedactContent,
RedactedStateEventContent, RoomAccountDataEventType, StateEventType, StaticEventContent,
StaticStateEventContent, StrippedStateEvent, SyncStateEvent,
presence::PresenceEvent,
receipt::ReceiptEventContent,
room::{
create::RoomCreateEventContent,
member::{RoomMemberEventContent, StrippedRoomMemberEvent},
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
redaction::SyncRoomRedactionEvent,
},
},
serde::Raw,
};
use serde::de::DeserializeOwned;
use tokio::sync::{Mutex, RwLock, broadcast};
use tracing::warn;
pub use traits::compare_thread_subscription_bump_stamps;
use crate::{
MinimalRoomMemberEvent, Room, RoomCreateWithCreatorEventContent, RoomStateFilter, SessionMeta,
deserialized_responses::DisplayName,
event_cache::store as event_cache_store,
media::store as media_store,
room::{RoomInfo, RoomInfoNotableUpdate, RoomState},
};
pub(crate) mod ambiguity_map;
mod memory_store;
pub mod migration_helpers;
mod send_queue;
#[cfg(any(test, feature = "testing"))]
pub use self::integration_tests::StateStoreIntegrationTests;
#[cfg(feature = "unstable-msc4274")]
pub use self::send_queue::{AccumulatedSentMediaInfo, FinishGalleryItemInfo};
pub use self::{
memory_store::MemoryStore,
send_queue::{
ChildTransactionId, DependentQueuedRequest, DependentQueuedRequestKind,
FinishUploadThumbnailInfo, QueueWedgeError, QueuedRequest, QueuedRequestKind,
SentMediaInfo, SentRequestKey, SerializableEventContent,
},
traits::{
ComposerDraft, ComposerDraftType, DraftAttachment, DraftAttachmentContent, DraftThumbnail,
DynStateStore, IntoStateStore, StateStore, StateStoreDataKey, StateStoreDataValue,
StateStoreExt, SupportedVersionsResponse, ThreadSubscriptionCatchupToken, TtlStoreValue,
WellKnownResponse,
},
};
#[derive(Debug, thiserror::Error)]
pub enum StoreError {
#[error(transparent)]
Backend(Box<dyn std::error::Error + Send + Sync>),
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error(transparent)]
Identifier(#[from] ruma::IdParseError),
#[error("The store failed to be unlocked")]
StoreLocked,
#[error("The store is not encrypted but was tried to be opened with a passphrase")]
UnencryptedStore,
#[error("Error encrypting or decrypting data from the store: {0}")]
Encryption(#[from] StoreEncryptionError),
#[error("Error encoding or decoding data from the store: {0}")]
Codec(#[from] Utf8Error),
#[error(
"The database format changed in an incompatible way, current \
version: {0}, latest version: {1}"
)]
UnsupportedDatabaseVersion(usize, usize),
#[error("Redaction failed: {0}")]
Redaction(#[source] ruma::canonical_json::RedactionError),
#[error("The store contains invalid data: {details}")]
InvalidData {
details: String,
},
}
impl StoreError {
#[inline]
pub fn backend<E>(error: E) -> Self
where
E: std::error::Error + Send + Sync + 'static,
{
Self::Backend(Box::new(error))
}
}
pub type Result<T, E = StoreError> = std::result::Result<T, E>;
#[derive(Clone)]
pub(crate) struct BaseStateStore {
pub(super) inner: Arc<DynStateStore>,
session_meta: Arc<OnceCell<SessionMeta>>,
room_load_settings: Arc<RwLock<RoomLoadSettings>>,
pub(super) sync_token: Arc<RwLock<Option<String>>>,
rooms: Arc<StdRwLock<ObservableMap<OwnedRoomId, Room>>>,
lock: Arc<Mutex<()>>,
pub(crate) already_logged_missing_room: Arc<SyncMutex<HashSet<OwnedRoomId>>>,
}
impl BaseStateStore {
pub fn new(inner: Arc<DynStateStore>) -> Self {
Self {
inner,
session_meta: Default::default(),
room_load_settings: Default::default(),
sync_token: Default::default(),
rooms: Arc::new(StdRwLock::new(ObservableMap::new())),
lock: Default::default(),
already_logged_missing_room: Default::default(),
}
}
pub fn lock(&self) -> &Mutex<()> {
&self.lock
}
pub(crate) fn set_session_meta(&self, session_meta: SessionMeta) {
self.session_meta.set(session_meta).expect("`SessionMeta` was already set");
}
pub(crate) async fn load_rooms(
&self,
user_id: &UserId,
room_load_settings: RoomLoadSettings,
room_info_notable_update_sender: &broadcast::Sender<RoomInfoNotableUpdate>,
) -> Result<()> {
*self.room_load_settings.write().await = room_load_settings.clone();
let room_infos = self.load_and_migrate_room_infos(room_load_settings).await?;
let mut rooms = self.rooms.write().unwrap();
for room_info in room_infos {
let new_room = Room::restore(
user_id,
self.inner.clone(),
room_info,
room_info_notable_update_sender.clone(),
);
let new_room_id = new_room.room_id().to_owned();
rooms.insert(new_room_id, new_room);
}
Ok(())
}
async fn load_and_migrate_room_infos(
&self,
room_load_settings: RoomLoadSettings,
) -> Result<Vec<RoomInfo>> {
let mut room_infos = self.inner.get_room_infos(&room_load_settings).await?;
let mut migrated_room_infos = Vec::with_capacity(room_infos.len());
for room_info in room_infos.iter_mut() {
if room_info.apply_migrations(self.inner.clone()).await {
migrated_room_infos.push(room_info.clone());
}
}
if !migrated_room_infos.is_empty() {
let changes = StateChanges {
room_infos: migrated_room_infos
.into_iter()
.map(|room_info| (room_info.room_id.clone(), room_info))
.collect(),
..Default::default()
};
if let Err(error) = self.inner.save_changes(&changes).await {
warn!("Failed to save migrated room infos: {error}");
}
}
Ok(room_infos)
}
pub(crate) async fn load_sync_token(&self) -> Result<()> {
let token =
self.get_kv_data(StateStoreDataKey::SyncToken).await?.and_then(|s| s.into_sync_token());
*self.sync_token.write().await = token;
Ok(())
}
#[cfg(any(feature = "e2e-encryption", test))]
pub(crate) async fn derive_from_other(
&self,
other: &Self,
room_info_notable_update_sender: &broadcast::Sender<RoomInfoNotableUpdate>,
) -> Result<()> {
let Some(session_meta) = other.session_meta.get() else {
return Ok(());
};
let room_load_settings = other.room_load_settings.read().await.clone();
self.load_rooms(&session_meta.user_id, room_load_settings, room_info_notable_update_sender)
.await?;
self.load_sync_token().await?;
self.set_session_meta(session_meta.clone());
Ok(())
}
pub fn session_meta(&self) -> Option<&SessionMeta> {
self.session_meta.get()
}
pub fn rooms(&self) -> Vec<Room> {
self.rooms.read().unwrap().iter().cloned().collect()
}
pub fn rooms_filtered(&self, filter: RoomStateFilter) -> Vec<Room> {
self.rooms
.read()
.unwrap()
.iter()
.filter(|room| filter.matches(room.state()))
.cloned()
.collect()
}
pub fn rooms_stream(
&self,
) -> (Vector<Room>, impl Stream<Item = Vec<VectorDiff<Room>>> + use<>) {
self.rooms.read().unwrap().stream()
}
pub fn room(&self, room_id: &RoomId) -> Option<Room> {
self.rooms.read().unwrap().get(room_id).cloned()
}
pub(crate) fn room_exists(&self, room_id: &RoomId) -> bool {
self.rooms.read().unwrap().get(room_id).is_some()
}
pub fn get_or_create_room(
&self,
room_id: &RoomId,
room_state: RoomState,
room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
) -> Room {
let user_id =
&self.session_meta.get().expect("Creating room while not being logged in").user_id;
self.rooms
.write()
.unwrap()
.get_or_create(room_id, || {
Room::new(
user_id,
self.inner.clone(),
room_id,
room_state,
room_info_notable_update_sender,
)
})
.clone()
}
pub(crate) async fn forget_room(&self, room_id: &RoomId) -> Result<()> {
self.inner.remove_room(room_id).await?;
self.rooms.write().unwrap().remove(room_id);
Ok(())
}
}
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for BaseStateStore {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Store")
.field("inner", &self.inner)
.field("session_meta", &self.session_meta)
.field("sync_token", &self.sync_token)
.field("rooms", &self.rooms)
.finish_non_exhaustive()
}
}
impl Deref for BaseStateStore {
type Target = DynStateStore;
fn deref(&self) -> &Self::Target {
self.inner.deref()
}
}
#[derive(Clone, Debug, Default)]
pub enum RoomLoadSettings {
#[default]
All,
One(OwnedRoomId),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ThreadSubscriptionStatus {
Subscribed {
automatic: bool,
},
Unsubscribed,
}
impl FromStr for ThreadSubscriptionStatus {
type Err = ();
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"automatic" => Ok(ThreadSubscriptionStatus::Subscribed { automatic: true }),
"manual" => Ok(ThreadSubscriptionStatus::Subscribed { automatic: false }),
"unsubscribed" => Ok(ThreadSubscriptionStatus::Unsubscribed),
_ => Err(()),
}
}
}
impl ThreadSubscriptionStatus {
pub fn as_str(&self) -> &'static str {
match self {
ThreadSubscriptionStatus::Subscribed { automatic } => {
if *automatic {
"automatic"
} else {
"manual"
}
}
ThreadSubscriptionStatus::Unsubscribed => "unsubscribed",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct StoredThreadSubscription {
pub status: ThreadSubscriptionStatus,
pub bump_stamp: Option<u64>,
}
#[derive(Clone, Debug, Default)]
pub struct StateChanges {
pub sync_token: Option<String>,
pub account_data: BTreeMap<GlobalAccountDataEventType, Raw<AnyGlobalAccountDataEvent>>,
pub presence: BTreeMap<OwnedUserId, Raw<PresenceEvent>>,
pub profiles: BTreeMap<OwnedRoomId, BTreeMap<OwnedUserId, MinimalRoomMemberEvent>>,
pub profiles_to_delete: BTreeMap<OwnedRoomId, Vec<OwnedUserId>>,
pub state:
BTreeMap<OwnedRoomId, BTreeMap<StateEventType, BTreeMap<String, Raw<AnySyncStateEvent>>>>,
pub room_account_data:
BTreeMap<OwnedRoomId, BTreeMap<RoomAccountDataEventType, Raw<AnyRoomAccountDataEvent>>>,
pub room_infos: BTreeMap<OwnedRoomId, RoomInfo>,
pub receipts: BTreeMap<OwnedRoomId, ReceiptEventContent>,
pub redactions: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, Raw<SyncRoomRedactionEvent>>>,
pub stripped_state: BTreeMap<
OwnedRoomId,
BTreeMap<StateEventType, BTreeMap<String, Raw<AnyStrippedStateEvent>>>,
>,
pub ambiguity_maps: BTreeMap<OwnedRoomId, HashMap<DisplayName, BTreeSet<OwnedUserId>>>,
}
impl StateChanges {
pub fn new(sync_token: String) -> Self {
Self { sync_token: Some(sync_token), ..Default::default() }
}
pub fn add_presence_event(&mut self, event: PresenceEvent, raw_event: Raw<PresenceEvent>) {
self.presence.insert(event.sender, raw_event);
}
pub fn add_room(&mut self, room: RoomInfo) {
self.room_infos.insert(room.room_id.clone(), room);
}
pub fn add_room_account_data(
&mut self,
room_id: &RoomId,
event: AnyRoomAccountDataEvent,
raw_event: Raw<AnyRoomAccountDataEvent>,
) {
self.room_account_data
.entry(room_id.to_owned())
.or_default()
.insert(event.event_type(), raw_event);
}
pub fn add_stripped_member(
&mut self,
room_id: &RoomId,
user_id: &UserId,
event: Raw<StrippedRoomMemberEvent>,
) {
self.stripped_state
.entry(room_id.to_owned())
.or_default()
.entry(StateEventType::RoomMember)
.or_default()
.insert(user_id.into(), event.cast());
}
pub fn add_state_event(
&mut self,
room_id: &RoomId,
event: AnySyncStateEvent,
raw_event: Raw<AnySyncStateEvent>,
) {
self.state
.entry(room_id.to_owned())
.or_default()
.entry(event.event_type())
.or_default()
.insert(event.state_key().to_owned(), raw_event);
}
pub fn add_redaction(
&mut self,
room_id: &RoomId,
redacted_event_id: &EventId,
redaction: Raw<SyncRoomRedactionEvent>,
) {
self.redactions
.entry(room_id.to_owned())
.or_default()
.insert(redacted_event_id.to_owned(), redaction);
}
pub fn add_receipts(&mut self, room_id: &RoomId, event: ReceiptEventContent) {
self.receipts.insert(room_id.to_owned(), event);
}
pub(crate) fn state_static_for_key<C, K>(
&self,
room_id: &RoomId,
state_key: &K,
) -> Option<&Raw<SyncStateEvent<C>>>
where
C: StaticEventContent<IsPrefix = ruma::events::False>
+ StaticStateEventContent
+ RedactContent,
C::Redacted: RedactedStateEventContent,
C::StateKey: Borrow<K>,
K: AsRef<str> + ?Sized,
{
self.state
.get(room_id)?
.get(&C::TYPE.into())?
.get(state_key.as_ref())
.map(Raw::cast_ref_unchecked)
}
pub(crate) fn stripped_state_static_for_key<C, K>(
&self,
room_id: &RoomId,
state_key: &K,
) -> Option<&Raw<StrippedStateEvent<C::PossiblyRedacted>>>
where
C: StaticEventContent<IsPrefix = ruma::events::False> + StaticStateEventContent,
C::StateKey: Borrow<K>,
K: AsRef<str> + ?Sized,
{
self.stripped_state
.get(room_id)?
.get(&C::TYPE.into())?
.get(state_key.as_ref())
.map(Raw::cast_ref_unchecked)
}
pub(crate) fn any_state_static_for_key<C, K>(
&self,
room_id: &RoomId,
state_key: &K,
) -> Option<StrippedStateEvent<C::PossiblyRedacted>>
where
C: StaticEventContent<IsPrefix = ruma::events::False>
+ StaticStateEventContent
+ RedactContent,
C::Redacted: RedactedStateEventContent,
C::PossiblyRedacted: StaticEventContent + DeserializeOwned,
C::StateKey: Borrow<K>,
K: AsRef<str> + ?Sized,
{
self.state_static_for_key::<C, K>(room_id, state_key)
.map(Raw::cast_ref)
.or_else(|| self.stripped_state_static_for_key::<C, K>(room_id, state_key))?
.deserialize()
.ok()
}
pub(crate) fn member(
&self,
room_id: &RoomId,
user_id: &UserId,
) -> Option<StrippedRoomMemberEvent> {
self.any_state_static_for_key::<RoomMemberEventContent, _>(room_id, user_id)
}
pub(crate) fn create(&self, room_id: &RoomId) -> Option<RoomCreateWithCreatorEventContent> {
self.any_state_static_for_key::<RoomCreateEventContent, _>(room_id, &EmptyStateKey)
.map(|event| {
RoomCreateWithCreatorEventContent::from_event_content(event.content, event.sender)
})
.or_else(|| self.room_infos.get(room_id)?.create().cloned())
}
pub(crate) fn power_levels(&self, room_id: &RoomId) -> Option<RoomPowerLevels> {
let power_levels_content = self
.any_state_static_for_key::<RoomPowerLevelsEventContent, _>(room_id, &EmptyStateKey)?;
let create_content = self.create(room_id)?;
let rules = create_content.room_version.rules().unwrap_or(ROOM_VERSION_RULES_FALLBACK);
let creators = create_content.creators();
Some(power_levels_content.power_levels(&rules.authorization, creators))
}
}
#[derive(Clone)]
pub struct StoreConfig {
#[cfg(feature = "e2e-encryption")]
pub(crate) crypto_store: Arc<DynCryptoStore>,
pub(crate) state_store: Arc<DynStateStore>,
pub(crate) event_cache_store: event_cache_store::EventCacheStoreLock,
pub(crate) media_store: media_store::MediaStoreLock,
cross_process_store_locks_holder_name: String,
}
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for StoreConfig {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> StdResult<(), fmt::Error> {
fmt.debug_struct("StoreConfig").finish()
}
}
impl StoreConfig {
#[must_use]
pub fn new(cross_process_store_locks_holder_name: String) -> Self {
Self {
#[cfg(feature = "e2e-encryption")]
crypto_store: matrix_sdk_crypto::store::MemoryStore::new().into_crypto_store(),
state_store: Arc::new(MemoryStore::new()),
event_cache_store: event_cache_store::EventCacheStoreLock::new(
event_cache_store::MemoryStore::new(),
cross_process_store_locks_holder_name.clone(),
),
media_store: media_store::MediaStoreLock::new(
media_store::MemoryMediaStore::new(),
cross_process_store_locks_holder_name.clone(),
),
cross_process_store_locks_holder_name,
}
}
#[cfg(feature = "e2e-encryption")]
pub fn crypto_store(mut self, store: impl IntoCryptoStore) -> Self {
self.crypto_store = store.into_crypto_store();
self
}
pub fn state_store(mut self, store: impl IntoStateStore) -> Self {
self.state_store = store.into_state_store();
self
}
pub fn event_cache_store<S>(mut self, event_cache_store: S) -> Self
where
S: event_cache_store::IntoEventCacheStore,
{
self.event_cache_store = event_cache_store::EventCacheStoreLock::new(
event_cache_store,
self.cross_process_store_locks_holder_name.clone(),
);
self
}
pub fn media_store<S>(mut self, media_store: S) -> Self
where
S: media_store::IntoMediaStore,
{
self.media_store = media_store::MediaStoreLock::new(
media_store,
self.cross_process_store_locks_holder_name.clone(),
);
self
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use assert_matches::assert_matches;
use matrix_sdk_test::async_test;
use ruma::{owned_device_id, owned_user_id, room_id, user_id};
use tokio::sync::broadcast;
use super::{BaseStateStore, MemoryStore, RoomLoadSettings};
use crate::{RoomInfo, RoomState, SessionMeta, StateChanges};
#[async_test]
async fn test_set_session_meta() {
let store = BaseStateStore::new(Arc::new(MemoryStore::new()));
let session_meta = SessionMeta {
user_id: owned_user_id!("@mnt_io:matrix.org"),
device_id: owned_device_id!("HELLOYOU"),
};
assert!(store.session_meta.get().is_none());
store.set_session_meta(session_meta.clone());
assert_eq!(store.session_meta.get(), Some(&session_meta));
}
#[async_test]
#[should_panic]
async fn test_set_session_meta_twice() {
let store = BaseStateStore::new(Arc::new(MemoryStore::new()));
let session_meta = SessionMeta {
user_id: owned_user_id!("@mnt_io:matrix.org"),
device_id: owned_device_id!("HELLOYOU"),
};
store.set_session_meta(session_meta.clone());
store.set_session_meta(session_meta);
}
#[async_test]
async fn test_derive_from_other() {
let other = BaseStateStore::new(Arc::new(MemoryStore::new()));
let session_meta = SessionMeta {
user_id: owned_user_id!("@mnt_io:matrix.org"),
device_id: owned_device_id!("HELLOYOU"),
};
let (room_info_notable_update_sender, _) = broadcast::channel(1);
let room_id_0 = room_id!("!r0");
other
.load_rooms(
&session_meta.user_id,
RoomLoadSettings::One(room_id_0.to_owned()),
&room_info_notable_update_sender,
)
.await
.unwrap();
other.set_session_meta(session_meta.clone());
let store = BaseStateStore::new(Arc::new(MemoryStore::new()));
store.derive_from_other(&other, &room_info_notable_update_sender).await.unwrap();
assert_eq!(store.session_meta.get(), Some(&session_meta));
assert_matches!(*store.room_load_settings.read().await, RoomLoadSettings::One(ref room_id) => {
assert_eq!(room_id, room_id_0);
});
}
#[test]
fn test_room_load_settings_default() {
assert_matches!(RoomLoadSettings::default(), RoomLoadSettings::All);
}
#[async_test]
async fn test_load_all_rooms() {
let room_id_0 = room_id!("!r0");
let room_id_1 = room_id!("!r1");
let user_id = user_id!("@mnt_io:matrix.org");
let memory_state_store = Arc::new(MemoryStore::new());
{
let store = BaseStateStore::new(memory_state_store.clone());
let mut changes = StateChanges::default();
changes.add_room(RoomInfo::new(room_id_0, RoomState::Joined));
changes.add_room(RoomInfo::new(room_id_1, RoomState::Joined));
store.inner.save_changes(&changes).await.unwrap();
}
{
let store = BaseStateStore::new(memory_state_store.clone());
let (room_info_notable_update_sender, _) = broadcast::channel(2);
assert_matches!(*store.room_load_settings.read().await, RoomLoadSettings::All);
store
.load_rooms(user_id, RoomLoadSettings::All, &room_info_notable_update_sender)
.await
.unwrap();
assert_matches!(*store.room_load_settings.read().await, RoomLoadSettings::All);
let mut rooms = store.rooms();
rooms.sort_by(|a, b| a.room_id().cmp(b.room_id()));
assert_eq!(rooms.len(), 2);
assert_eq!(rooms[0].room_id(), room_id_0);
assert_eq!(rooms[0].own_user_id(), user_id);
assert_eq!(rooms[1].room_id(), room_id_1);
assert_eq!(rooms[1].own_user_id(), user_id);
}
}
#[async_test]
async fn test_load_one_room() {
let room_id_0 = room_id!("!r0");
let room_id_1 = room_id!("!r1");
let user_id = user_id!("@mnt_io:matrix.org");
let memory_state_store = Arc::new(MemoryStore::new());
{
let store = BaseStateStore::new(memory_state_store.clone());
let mut changes = StateChanges::default();
changes.add_room(RoomInfo::new(room_id_0, RoomState::Joined));
changes.add_room(RoomInfo::new(room_id_1, RoomState::Joined));
store.inner.save_changes(&changes).await.unwrap();
}
{
let store = BaseStateStore::new(memory_state_store.clone());
let (room_info_notable_update_sender, _) = broadcast::channel(2);
assert_matches!(*store.room_load_settings.read().await, RoomLoadSettings::All);
store
.load_rooms(
user_id,
RoomLoadSettings::One(room_id_1.to_owned()),
&room_info_notable_update_sender,
)
.await
.unwrap();
assert_matches!(
*store.room_load_settings.read().await,
RoomLoadSettings::One(ref room_id) => {
assert_eq!(room_id, room_id_1);
}
);
let rooms = store.rooms();
assert_eq!(rooms.len(), 1);
assert_eq!(rooms[0].room_id(), room_id_1);
assert_eq!(rooms[0].own_user_id(), user_id);
}
}
}