// Copyright 2021 The Matrix.org Foundation C.I.C. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! The state store holds the overall state for rooms, users and their //! profiles and their timelines. It is an overall cache for faster access //! and convenience- accessible through `Store`. //! //! Implementing the `StateStore` trait, you can plug any storage backend //! into the store for the actual storage. By default this brings an in-memory //! store. use std::{ borrow::Borrow, collections::{BTreeMap, BTreeSet}, ops::Deref, pin::Pin, result::Result as StdResult, sync::Arc, }; use futures_signals::signal::{Mutable, ReadOnlyMutable}; use once_cell::sync::OnceCell; #[cfg(any(test, feature = "testing"))] #[macro_use] pub mod integration_tests; use async_trait::async_trait; use dashmap::DashMap; use matrix_sdk_common::{locks::RwLock, AsyncTraitDeps}; #[cfg(feature = "e2e-encryption")] use matrix_sdk_crypto::store::{CryptoStore, IntoCryptoStore}; use ruma::{ api::client::push::get_notifications::v3::Notification, events::{ presence::PresenceEvent, receipt::{Receipt, ReceiptEventContent, ReceiptType}, room::{ member::{StrippedRoomMemberEvent, SyncRoomMemberEvent}, redaction::OriginalSyncRoomRedactionEvent, }, AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent, EmptyStateKey, GlobalAccountDataEvent, GlobalAccountDataEventContent, GlobalAccountDataEventType, RedactContent, RedactedStateEventContent, RoomAccountDataEvent, RoomAccountDataEventContent, RoomAccountDataEventType, StateEventContent, StateEventType, StaticEventContent, SyncStateEvent, }, serde::Raw, EventId, MxcUri, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId, }; use serde::de::DeserializeOwned; /// BoxStream of owned Types pub type BoxStream = Pin + Send>>; #[cfg(feature = "experimental-timeline")] use crate::deserialized_responses::{SyncTimelineEvent, TimelineSlice}; use crate::{ deserialized_responses::MemberEvent, media::MediaRequest, rooms::{RoomInfo, RoomType}, MinimalRoomMemberEvent, Room, Session, SessionMeta, SessionTokens, }; pub(crate) mod ambiguity_map; mod memory_store; pub use self::memory_store::MemoryStore; /// State store specific error type. #[derive(Debug, thiserror::Error)] pub enum StoreError { /// An error happened in the underlying database backend. #[error(transparent)] Backend(Box), /// An error happened while serializing or deserializing some data. #[error(transparent)] Json(#[from] serde_json::Error), /// An error happened while deserializing a Matrix identifier, e.g. an user /// id. #[error(transparent)] Identifier(#[from] ruma::IdParseError), /// The store is locked with a passphrase and an incorrect passphrase was /// given. #[error("The store failed to be unlocked")] StoreLocked, /// An unencrypted store was tried to be unlocked with a passphrase. #[error("The store is not encrypted but was tried to be opened with a passphrase")] UnencryptedStore, /// The store failed to encrypt or decrypt some data. #[error("Error encrypting or decrypting data from the store: {0}")] Encryption(String), /// The store failed to encode or decode some data. #[error("Error encoding or decoding data from the store: {0}")] Codec(String), /// The database format has changed in a backwards incompatible way. #[error( "The database format changed in an incompatible way, current \ version: {0}, latest version: {1}" )] UnsupportedDatabaseVersion(usize, usize), /// Redacting an event in the store has failed. /// /// This should never happen. #[error("Redaction failed: {0}")] Redaction(#[source] ruma::canonical_json::RedactionError), } impl StoreError { /// Create a new [`Backend`][Self::Backend] error. /// /// Shorthand for `StoreError::Backend(Box::new(error))`. #[inline] pub fn backend(error: E) -> Self where E: std::error::Error + Send + Sync + 'static, { Self::Backend(Box::new(error)) } } /// A `StateStore` specific result type. pub type Result = std::result::Result; /// An abstract state store trait that can be used to implement different stores /// for the SDK. #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait StateStore: AsyncTraitDeps { /// Save the given filter id under the given name. /// /// # Arguments /// /// * `filter_name` - The name that should be used to store the filter id. /// /// * `filter_id` - The filter id that should be stored in the state store. async fn save_filter(&self, filter_name: &str, filter_id: &str) -> Result<()>; /// Save the set of state changes in the store. async fn save_changes(&self, changes: &StateChanges) -> Result<()>; /// Get the filter id that was stored under the given filter name. /// /// # Arguments /// /// * `filter_name` - The name that was used to store the filter id. async fn get_filter(&self, filter_name: &str) -> Result>; /// Get the last stored sync token. async fn get_sync_token(&self) -> Result>; /// Get the stored presence event for the given user. /// /// # Arguments /// /// * `user_id` - The id of the user for which we wish to fetch the presence /// event for. async fn get_presence_event(&self, user_id: &UserId) -> Result>>; /// Get a state event out of the state store. /// /// # Arguments /// /// * `room_id` - The id of the room the state event was received for. /// /// * `event_type` - The event type of the state event. async fn get_state_event( &self, room_id: &RoomId, event_type: StateEventType, state_key: &str, ) -> Result>>; /// Get a list of state events for a given room and `StateEventType`. /// /// # Arguments /// /// * `room_id` - The id of the room to find events for. /// /// * `event_type` - The event type. async fn get_state_events( &self, room_id: &RoomId, event_type: StateEventType, ) -> Result>>; /// Get the current profile for the given user in the given room. /// /// # Arguments /// /// * `room_id` - The room id the profile is used in. /// /// * `user_id` - The id of the user the profile belongs to. async fn get_profile( &self, room_id: &RoomId, user_id: &UserId, ) -> Result>; /// Get the `MemberEvent` for the given state key in the given room id. /// /// # Arguments /// /// * `room_id` - The room id the member event belongs to. /// /// * `state_key` - The user id that the member event defines the state for. async fn get_member_event( &self, room_id: &RoomId, state_key: &UserId, ) -> Result>; /// Get all the user ids of members for a given room, for stripped and /// regular rooms alike. async fn get_user_ids(&self, room_id: &RoomId) -> Result>; /// Get all the user ids of members that are in the invited state for a /// given room, for stripped and regular rooms alike. async fn get_invited_user_ids(&self, room_id: &RoomId) -> Result>; /// Get all the user ids of members that are in the joined state for a /// given room, for stripped and regular rooms alike. async fn get_joined_user_ids(&self, room_id: &RoomId) -> Result>; /// Get all the pure `RoomInfo`s the store knows about. async fn get_room_infos(&self) -> Result>; /// Get all the pure `RoomInfo`s the store knows about. async fn get_stripped_room_infos(&self) -> Result>; /// Get all the users that use the given display name in the given room. /// /// # Arguments /// /// * `room_id` - The id of the room for which the display name users should /// be fetched for. /// /// * `display_name` - The display name that the users use. async fn get_users_with_display_name( &self, room_id: &RoomId, display_name: &str, ) -> Result>; /// Get an event out of the account data store. /// /// # Arguments /// /// * `event_type` - The event type of the account data event. async fn get_account_data_event( &self, event_type: GlobalAccountDataEventType, ) -> Result>>; /// Get an event out of the room account data store. /// /// # Arguments /// /// * `room_id` - The id of the room for which the room account data event /// should /// be fetched. /// /// * `event_type` - The event type of the room account data event. async fn get_room_account_data_event( &self, room_id: &RoomId, event_type: RoomAccountDataEventType, ) -> Result>>; /// Get an event out of the user room receipt store. /// /// # Arguments /// /// * `room_id` - The id of the room for which the receipt should be /// fetched. /// /// * `receipt_type` - The type of the receipt. /// /// * `user_id` - The id of the user for who the receipt should be fetched. async fn get_user_room_receipt_event( &self, room_id: &RoomId, receipt_type: ReceiptType, user_id: &UserId, ) -> Result>; /// Get events out of the event room receipt store. /// /// # Arguments /// /// * `room_id` - The id of the room for which the receipts should be /// fetched. /// /// * `receipt_type` - The type of the receipts. /// /// * `event_id` - The id of the event for which the receipts should be /// fetched. async fn get_event_room_receipt_events( &self, room_id: &RoomId, receipt_type: ReceiptType, event_id: &EventId, ) -> Result>; /// Get arbitrary data from the custom store /// /// # Arguments /// /// * `key` - The key to fetch data for async fn get_custom_value(&self, key: &[u8]) -> Result>>; /// Put arbitrary data into the custom store /// /// # Arguments /// /// * `key` - The key to insert data into /// /// * `value` - The value to insert async fn set_custom_value(&self, key: &[u8], value: Vec) -> Result>>; /// Add a media file's content in the media store. /// /// # Arguments /// /// * `request` - The `MediaRequest` of the file. /// /// * `content` - The content of the file. async fn add_media_content(&self, request: &MediaRequest, content: Vec) -> Result<()>; /// Get a media file's content out of the media store. /// /// # Arguments /// /// * `request` - The `MediaRequest` of the file. async fn get_media_content(&self, request: &MediaRequest) -> Result>>; /// Removes a media file's content from the media store. /// /// # Arguments /// /// * `request` - The `MediaRequest` of the file. async fn remove_media_content(&self, request: &MediaRequest) -> Result<()>; /// Removes all the media files' content associated to an `MxcUri` from the /// media store. /// /// # Arguments /// /// * `uri` - The `MxcUri` of the media files. async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()>; /// Removes a room and all elements associated from the state store. /// /// # Arguments /// /// * `room_id` - The `RoomId` of the room to delete. async fn remove_room(&self, room_id: &RoomId) -> Result<()>; /// Get a stream of the stored timeline /// /// # Arguments /// /// * `room_id` - The `RoomId` of the room to delete. /// /// Returns a stream of events and a token that can be used to request /// previous events. #[cfg(feature = "experimental-timeline")] async fn room_timeline( &self, room_id: &RoomId, ) -> Result>, Option)>>; } /// Convenience functionality for state stores. #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait StateStoreExt: StateStore { /// Get a specific state event of statically-known type. /// /// # Arguments /// /// * `room_id` - The id of the room the state event was received for. async fn get_state_event_static( &self, room_id: &RoomId, ) -> Result>>> where C: StaticEventContent + StateEventContent + RedactContent, C::Redacted: RedactedStateEventContent + DeserializeOwned, { Ok(self.get_state_event(room_id, C::TYPE.into(), "").await?.map(Raw::cast)) } /// Get a specific state event of statically-known type. /// /// # Arguments /// /// * `room_id` - The id of the room the state event was received for. async fn get_state_event_static_for_key( &self, room_id: &RoomId, state_key: &K, ) -> Result>>> where C: StaticEventContent + StateEventContent + RedactContent, C::StateKey: Borrow, C::Redacted: RedactedStateEventContent, K: AsRef + ?Sized + Sync, { Ok(self.get_state_event(room_id, C::TYPE.into(), state_key.as_ref()).await?.map(Raw::cast)) } /// Get a list of state events of a statically-known type for a given room. /// /// # Arguments /// /// * `room_id` - The id of the room to find events for. async fn get_state_events_static( &self, room_id: &RoomId, ) -> Result>>> where C: StaticEventContent + StateEventContent + RedactContent, C::Redacted: RedactedStateEventContent, { // FIXME: Could be more efficient, if we had streaming store accessor functions Ok(self .get_state_events(room_id, C::TYPE.into()) .await? .into_iter() .map(Raw::cast) .collect()) } /// Get an event of a statically-known type from the account data store. async fn get_account_data_event_static( &self, ) -> Result>>> where C: StaticEventContent + GlobalAccountDataEventContent, { Ok(self.get_account_data_event(C::TYPE.into()).await?.map(Raw::cast)) } /// Get an event of a statically-known type from the room account data /// store. /// /// # Arguments /// /// * `room_id` - The id of the room for which the room account data event /// should be fetched. async fn get_room_account_data_event_static( &self, room_id: &RoomId, ) -> Result>>> where C: StaticEventContent + RoomAccountDataEventContent, { Ok(self.get_room_account_data_event(room_id, C::TYPE.into()).await?.map(Raw::cast)) } } #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl StateStoreExt for T {} /// A type that can be type-erased into `Arc`. /// /// This trait is not meant to be implemented directly outside /// `matrix-sdk-crypto`, but it is automatically implemented for everything that /// implements `StateStore`. pub trait IntoStateStore { #[doc(hidden)] fn into_state_store(self) -> Arc; } impl IntoStateStore for T where T: StateStore + Sized + 'static, { fn into_state_store(self) -> Arc { Arc::new(self) } } impl IntoStateStore for Arc where T: StateStore + 'static, { fn into_state_store(self) -> Arc { self } } /// A state store wrapper for the SDK. /// /// This adds additional higher level store functionality on top of a /// `StateStore` implementation. #[derive(Debug, Clone)] pub(crate) struct Store { pub(super) inner: Arc, session_meta: Arc>, pub(super) session_tokens: Mutable>, /// The current sync token that should be used for the next sync call. pub(super) sync_token: Arc>>, rooms: Arc>, stripped_rooms: Arc>, } impl Store { /// Create a new Store with the default `MemoryStore` pub fn open_memory_store() -> Self { let inner = Arc::new(MemoryStore::new()); Self::new(inner) } } impl Store { /// Create a new store, wrappning the given `StateStore` pub fn new(inner: Arc) -> Self { Self { inner, session_meta: Default::default(), session_tokens: Default::default(), sync_token: Default::default(), rooms: Default::default(), stripped_rooms: Default::default(), } } /// Restore the access to the Store from the given `Session`, overwrites any /// previously existing access to the Store. pub async fn restore_session(&self, session: Session) -> Result<()> { for info in self.inner.get_room_infos().await? { let room = Room::restore(&session.user_id, self.inner.clone(), info); self.rooms.insert(room.room_id().to_owned(), room); } for info in self.inner.get_stripped_room_infos().await? { let room = Room::restore(&session.user_id, self.inner.clone(), info); self.stripped_rooms.insert(room.room_id().to_owned(), room); } let token = self.get_sync_token().await?; *self.sync_token.write().await = token; let (session_meta, session_tokens) = session.into_parts(); self.session_meta.set(session_meta).expect("Session IDs were already set"); self.session_tokens.set(Some(session_tokens)); Ok(()) } /// The current [`SessionMeta`] containing our user ID and device ID. pub fn session_meta(&self) -> Option<&SessionMeta> { self.session_meta.get() } /// The current [`SessionTokens`] containing our access token and optional /// refresh token. pub fn session_tokens(&self) -> ReadOnlyMutable> { self.session_tokens.read_only() } /// Set the current [`SessionTokens`]. pub fn set_session_tokens(&self, tokens: SessionTokens) { self.session_tokens.set(Some(tokens)); } /// The current [`Session`] containing our user id, device ID, access /// token and optional refresh token. pub fn session(&self) -> Option { let meta = self.session_meta.get()?; let tokens = self.session_tokens.get_cloned()?; Some(Session::from_parts(meta.to_owned(), tokens)) } /// Get all the rooms this store knows about. pub fn get_rooms(&self) -> Vec { self.rooms.iter().filter_map(|r| self.get_room(r.key())).collect() } /// Get the room with the given room id. pub fn get_room(&self, room_id: &RoomId) -> Option { self.rooms .get(room_id) .and_then(|r| match r.room_type() { RoomType::Joined => Some(r.clone()), RoomType::Left => Some(r.clone()), RoomType::Invited => self.get_stripped_room(room_id), }) .or_else(|| self.get_stripped_room(room_id)) } /// Get all the rooms this store knows about. pub fn get_stripped_rooms(&self) -> Vec { self.stripped_rooms.iter().filter_map(|r| self.get_stripped_room(r.key())).collect() } /// Get the stripped room with the given room id. pub fn get_stripped_room(&self, room_id: &RoomId) -> Option { self.stripped_rooms.get(room_id).map(|r| r.clone()) } /// Lookup the stripped Room for the given RoomId, or create one, if it /// didn't exist yet in the store pub async fn get_or_create_stripped_room(&self, room_id: &RoomId) -> Room { let user_id = &self.session_meta.get().expect("Creating room while not being logged in").user_id; // Remove the respective room from non-stripped rooms. self.rooms.remove(room_id); self.stripped_rooms .entry(room_id.to_owned()) .or_insert_with(|| Room::new(user_id, self.inner.clone(), room_id, RoomType::Invited)) .clone() } /// Lookup the Room for the given RoomId, or create one, if it didn't exist /// yet in the store pub async fn get_or_create_room(&self, room_id: &RoomId, room_type: RoomType) -> Room { if room_type == RoomType::Invited { return self.get_or_create_stripped_room(room_id).await; } let user_id = &self.session_meta.get().expect("Creating room while not being logged in").user_id; // Remove the respective room from stripped rooms. self.stripped_rooms.remove(room_id); self.rooms .entry(room_id.to_owned()) .or_insert_with(|| Room::new(user_id, self.inner.clone(), room_id, room_type)) .clone() } } impl Deref for Store { type Target = dyn StateStore; fn deref(&self) -> &Self::Target { self.inner.deref() } } /// Store state changes and pass them to the StateStore. #[derive(Debug, Default)] pub struct StateChanges { /// The sync token that relates to this update. pub sync_token: Option, /// A user session, containing an access token and information about the /// associated user account. pub session: Option, /// A mapping of event type string to `AnyBasicEvent`. pub account_data: BTreeMap>, /// A mapping of `UserId` to `PresenceEvent`. pub presence: BTreeMap>, /// A mapping of `RoomId` to a map of users and their `SyncRoomMemberEvent`. pub members: BTreeMap>, /// A mapping of `RoomId` to a map of users and their /// `MinimalRoomMemberEvent`. pub profiles: BTreeMap>, /// A mapping of `RoomId` to a map of event type string to a state key and /// `AnySyncStateEvent`. pub state: BTreeMap>>>, /// A mapping of `RoomId` to a map of event type string to `AnyBasicEvent`. pub room_account_data: BTreeMap>>, /// A map of `RoomId` to `RoomInfo`. pub room_infos: BTreeMap, /// A map of `RoomId` to `ReceiptEventContent`. pub receipts: BTreeMap, /// A map of `RoomId` to maps of `OwnedEventId` to be redacted by /// `SyncRoomRedactionEvent`. pub redactions: BTreeMap>, /// A mapping of `RoomId` to a map of event type to a map of state key to /// `AnyStrippedStateEvent`. pub stripped_state: BTreeMap< OwnedRoomId, BTreeMap>>, >, /// A mapping of `RoomId` to a map of users and their /// `StrippedRoomMemberEvent`. pub stripped_members: BTreeMap>, /// A map of `RoomId` to `RoomInfo` for stripped rooms (e.g. for invites or /// while knocking) pub stripped_room_infos: BTreeMap, /// A map from room id to a map of a display name and a set of user ids that /// share that display name in the given room. pub ambiguity_maps: BTreeMap>>, /// A map of `RoomId` to a vector of `Notification`s pub notifications: BTreeMap>, /// A mapping of `RoomId` to a `TimelineSlice` #[cfg(feature = "experimental-timeline")] pub timeline: BTreeMap, } impl StateChanges { /// Create a new `StateChanges` struct with the given sync_token. pub fn new(sync_token: String) -> Self { Self { sync_token: Some(sync_token), ..Default::default() } } /// Update the `StateChanges` struct with the given `PresenceEvent`. pub fn add_presence_event(&mut self, event: PresenceEvent, raw_event: Raw) { self.presence.insert(event.sender, raw_event); } /// Update the `StateChanges` struct with the given `RoomInfo`. pub fn add_room(&mut self, room: RoomInfo) { self.room_infos.insert(room.room_id.as_ref().to_owned(), room); } /// Update the `StateChanges` struct with the given `RoomInfo`. pub fn add_stripped_room(&mut self, room: RoomInfo) { self.stripped_room_infos.insert(room.room_id.as_ref().to_owned(), room); } /// Update the `StateChanges` struct with the given `AnyBasicEvent`. pub fn add_account_data( &mut self, event: AnyGlobalAccountDataEvent, raw_event: Raw, ) { self.account_data.insert(event.event_type(), raw_event); } /// Update the `StateChanges` struct with the given room with a new /// `AnyBasicEvent`. pub fn add_room_account_data( &mut self, room_id: &RoomId, event: AnyRoomAccountDataEvent, raw_event: Raw, ) { self.room_account_data .entry(room_id.to_owned()) .or_default() .insert(event.event_type(), raw_event); } /// Update the `StateChanges` struct with the given room with a new /// `StrippedMemberEvent`. pub fn add_stripped_member(&mut self, room_id: &RoomId, event: StrippedRoomMemberEvent) { let user_id = event.state_key.clone(); self.stripped_members.entry(room_id.to_owned()).or_default().insert(user_id, event); } /// Update the `StateChanges` struct with the given room with a new /// `AnySyncStateEvent`. pub fn add_state_event( &mut self, room_id: &RoomId, event: AnySyncStateEvent, raw_event: Raw, ) { self.state .entry(room_id.to_owned()) .or_default() .entry(event.event_type()) .or_default() .insert(event.state_key().to_owned(), raw_event); } /// Redact an event in the room pub fn add_redaction(&mut self, room_id: &RoomId, redaction: OriginalSyncRoomRedactionEvent) { self.redactions .entry(room_id.to_owned()) .or_default() .insert(redaction.redacts.clone(), redaction); } /// Update the `StateChanges` struct with the given room with a new /// `Notification`. pub fn add_notification(&mut self, room_id: &RoomId, notification: Notification) { self.notifications.entry(room_id.to_owned()).or_default().push(notification); } /// Update the `StateChanges` struct with the given room with a new /// `Receipts`. pub fn add_receipts(&mut self, room_id: &RoomId, event: ReceiptEventContent) { self.receipts.insert(room_id.to_owned(), event); } /// Update the `StateChanges` struct with the given room with a new /// `TimelineSlice`. #[cfg(feature = "experimental-timeline")] pub fn add_timeline(&mut self, room_id: &RoomId, timeline: TimelineSlice) { self.timeline.insert(room_id.to_owned(), timeline); } } /// Configuration for the state store and, when `encryption` is enabled, for the /// crypto store. /// /// # Example /// /// ``` /// # use matrix_sdk_base::store::StoreConfig; /// /// let store_config = StoreConfig::new(); /// ``` #[derive(Clone, Default)] pub struct StoreConfig { #[cfg(feature = "e2e-encryption")] pub(crate) crypto_store: Option>, pub(crate) state_store: Option>, } #[cfg(not(tarpaulin_include))] impl std::fmt::Debug for StoreConfig { fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> StdResult<(), std::fmt::Error> { fmt.debug_struct("StoreConfig").finish() } } impl StoreConfig { /// Create a new default `StoreConfig`. #[must_use] pub fn new() -> Self { Default::default() } /// Set a custom implementation of a `CryptoStore`. /// /// The crypto store must be opened before being set. #[cfg(feature = "e2e-encryption")] pub fn crypto_store(mut self, store: impl IntoCryptoStore) -> Self { self.crypto_store = Some(store.into_crypto_store()); self } /// Set a custom implementation of a `StateStore`. pub fn state_store(mut self, store: impl IntoStateStore) -> Self { self.state_store = Some(store.into_state_store()); self } }