matrix-sdk: Move most of the configuration to the base client.

master
Damir Jelić 2020-05-25 14:21:04 +02:00
parent 7edb42b75c
commit ba66ee214f
10 changed files with 256 additions and 144 deletions

View File

@ -20,7 +20,7 @@ sqlite-cryptostore = ["matrix-sdk-base/sqlite-cryptostore"]
http = "0.2.1"
reqwest = "0.10.4"
serde_json = "1.0.53"
thiserror = "1.0.18"
thiserror = "1.0.19"
tracing = "0.1.14"
url = "2.1.1"
futures-timer = { version = "3.0.2", features = ["wasm-bindgen"] }

View File

@ -17,6 +17,7 @@
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::convert::{TryFrom, TryInto};
use std::path::Path;
use std::result::Result as StdResult;
use std::sync::Arc;
@ -47,10 +48,7 @@ use crate::api;
#[cfg(not(target_arch = "wasm32"))]
use crate::VERSION;
use crate::{Error, EventEmitter, Result};
use matrix_sdk_base::BaseClient;
use matrix_sdk_base::Room;
use matrix_sdk_base::Session;
use matrix_sdk_base::StateStore;
use matrix_sdk_base::{BaseClient, BaseClientConfig, Room, Session, StateStore};
const DEFAULT_SYNC_TIMEOUT: Duration = Duration::from_secs(30);
@ -104,7 +102,7 @@ pub struct ClientConfig {
proxy: Option<reqwest::Proxy>,
user_agent: Option<HeaderValue>,
disable_ssl_verification: bool,
state_store: Option<Box<dyn StateStore>>,
base_config: BaseClientConfig,
}
#[cfg_attr(tarpaulin, skip)]
@ -166,7 +164,36 @@ impl ClientConfig {
///
/// The state store should be opened before being set.
pub fn state_store(mut self, store: Box<dyn StateStore>) -> Self {
self.state_store = Some(store);
self.base_config = self.base_config.state_store(store);
self
}
/// Set the path for storage.
///
/// # Arguments
///
/// * `path` - The path where the stores should save data in. It is the
/// callers responsibility to make sure that the path exists.
///
/// In the default configuration the client will open default
/// implementations for the crypto store and the state store. It will use
/// the given path to open the stores. If no path is provided no store will
/// be opened
pub fn store_path<P: AsRef<Path>>(mut self, path: P) -> Self {
self.base_config = self.base_config.store_path(path);
self
}
/// Set the passphrase to encrypt the crypto store.
///
/// # Argument
///
/// * `passphrase` - The passphrase that will be used to encrypt the data in
/// the cryptostore.
///
/// This is only used if no custom cryptostore is set.
pub fn passphrase(mut self, passphrase: String) -> Self {
self.base_config = self.base_config.passphrase(passphrase);
self
}
}
@ -293,11 +320,7 @@ impl Client {
let http_client = http_client.build()?;
let base_client = if let Some(store) = config.state_store {
BaseClient::new_with_state_store(store)?
} else {
BaseClient::new()?
};
let base_client = BaseClient::new_with_config(config.base_config)?;
Ok(Self {
homeserver,
@ -371,38 +394,6 @@ impl Client {
self.base_client.get_left_room(room_id).await
}
/// This allows `Client` to manually sync state with the provided `StateStore`.
///
/// Returns true when a successful `StateStore` sync has completed.
///
/// # Examples
///
/// ```no_run
/// use matrix_sdk::{Client, ClientConfig, JsonStore, RoomBuilder};
/// # use matrix_sdk::api::r0::room::Visibility;
/// # use url::Url;
///
/// # let homeserver = Url::parse("http://example.com").unwrap();
/// let store = JsonStore::open("path/to/store").unwrap();
/// let config = ClientConfig::new().state_store(Box::new(store));
/// let mut client = Client::new(homeserver).unwrap();
/// # use futures::executor::block_on;
/// # block_on(async {
/// let _ = client.login("name", "password", None, None).await.unwrap();
/// // returns true when a state store sync is successful
/// assert!(client.sync_with_state_store().await.unwrap());
/// // now state is restored without a request to the server
/// let mut names = vec![];
/// for r in client.joined_rooms().read().await.values() {
/// names.push(r.read().await.display_name());
/// }
/// assert_eq!(vec!["room".to_string(), "names".to_string()], names)
/// # });
/// ```
pub async fn sync_with_state_store(&self) -> Result<bool> {
Ok(self.base_client.sync_with_state_store().await?)
}
/// This allows `Client` to manually store `Room` state with the provided
/// `StateStore`.
///
@ -477,8 +468,6 @@ impl Client {
self.send(request).await
}
// TODO enable this once Ruma supports proper serialization of the query
// string.
/// Join a room by `RoomId`.
///
/// Returns a `join_room_by_id_or_alias::Response` consisting of the
@ -1441,8 +1430,6 @@ mod test {
);
}
// TODO enable this once Ruma supports proper serialization of the query
// string.
#[tokio::test]
async fn join_room_by_id_or_alias() {
let homeserver = Url::from_str(&mockito::server_url()).unwrap();
@ -1798,7 +1785,7 @@ mod test {
.unwrap();
assert_eq!(
EventId::try_from("$h29iv0s8:example.com").ok(),
EventId::try_from("$h29iv0s8:example.com").unwrap(),
response.event_id
)
}

View File

@ -20,12 +20,13 @@ sqlite-cryptostore = ["matrix-sdk-crypto/sqlite-cryptostore"]
async-trait = "0.1.31"
serde = "1.0.110"
serde_json = "1.0.53"
zeroize = "1.1.0"
matrix-sdk-common = { version = "0.1.0", path = "../matrix_sdk_common" }
matrix-sdk-crypto = { version = "0.1.0", path = "../matrix_sdk_crypto", optional = true }
# Misc dependencies
thiserror = "1.0.18"
thiserror = "1.0.19"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio]
version = "0.2.21"

View File

@ -17,10 +17,10 @@ use std::collections::HashMap;
#[cfg(feature = "encryption")]
use std::collections::{BTreeMap, HashSet};
use std::fmt;
use std::sync::atomic::{AtomicBool, Ordering};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use zeroize::Zeroizing;
#[cfg(feature = "encryption")]
use std::result::Result as StdResult;
use crate::api::r0 as api;
@ -55,8 +55,10 @@ use crate::api::r0::to_device::send_event_to_device;
use crate::events::room::{encrypted::EncryptedEventContent, message::MessageEventContent};
#[cfg(feature = "encryption")]
use crate::identifiers::DeviceId;
#[cfg(not(target_arch = "wasm32"))]
use crate::JsonStore;
#[cfg(feature = "encryption")]
use matrix_sdk_crypto::{OlmMachine, OneTimeKeys};
use matrix_sdk_crypto::{CryptoStore, OlmMachine, OneTimeKeys};
pub type Token = String;
@ -115,11 +117,13 @@ pub struct BaseClient {
///
/// There is a default implementation `JsonStore` that saves JSON to disk.
state_store: Arc<RwLock<Option<Box<dyn StateStore>>>>,
/// Does the `Client` need to sync with the state store.
needs_state_store_sync: Arc<AtomicBool>,
#[cfg(feature = "encryption")]
olm: Arc<Mutex<Option<OlmMachine>>>,
#[cfg(feature = "encryption")]
cryptostore: Arc<Mutex<Option<Box<dyn CryptoStore>>>>,
store_path: Arc<Option<PathBuf>>,
store_passphrase: Arc<Zeroizing<String>>,
}
#[cfg_attr(tarpaulin, skip)]
@ -136,31 +140,99 @@ impl fmt::Debug for BaseClient {
}
}
/// Configuration for the creation of the `BaseClient`.
///
/// # Example
///
/// ```
/// # use matrix_sdk_base::BaseClientConfig;
///
/// let client_config = BaseClientConfig::new()
/// .store_path("/home/example/matrix-sdk-client")
/// .passphrase("test-passphrase".to_owned());
/// ```
#[derive(Default)]
pub struct BaseClientConfig {
state_store: Option<Box<dyn StateStore>>,
#[cfg(feature = "encryption")]
crypto_store: Option<Box<dyn CryptoStore>>,
store_path: Option<PathBuf>,
passphrase: Option<Zeroizing<String>>,
}
#[cfg_attr(tarpaulin, skip)]
impl std::fmt::Debug for BaseClientConfig {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> StdResult<(), std::fmt::Error> {
fmt.debug_struct("BaseClientConfig").finish()
}
}
impl BaseClientConfig {
/// Create a new default `BaseClientConfig`.
pub fn new() -> Self {
Default::default()
}
/// Set a custom implementation of a `StateStore`.
///
/// The state store should be opened before being set.
pub fn state_store(mut self, store: Box<dyn StateStore>) -> Self {
self.state_store = Some(store);
self
}
/// Set a custom implementation of a `CryptoStore`.
///
/// The crypto store should be opened before being set.
#[cfg(feature = "encryption")]
pub fn crypto_store(mut self, store: Box<dyn CryptoStore>) -> Self {
self.crypto_store = Some(store);
self
}
/// Set the path for storage.
///
/// # Arguments
///
/// * `path` - The path where the stores should save data in. It is the
/// callers responsibility to make sure that the path exists.
///
/// In the default configuration the client will open default
/// implementations for the crypto store and the state store. It will use
/// the given path to open the stores. If no path is provided no store will
/// be opened
pub fn store_path<P: AsRef<Path>>(mut self, path: P) -> Self {
self.store_path = Some(path.as_ref().into());
self
}
/// Set the passphrase to encrypt the crypto store.
///
/// # Argument
///
/// * `passphrase` - The passphrase that will be used to encrypt the data in
/// the cryptostore.
///
/// This is only used if no custom cryptostore is set.
pub fn passphrase(mut self, passphrase: String) -> Self {
self.passphrase = Some(Zeroizing::new(passphrase));
self
}
}
impl BaseClient {
/// Create a new client.
///
/// # Arguments
///
/// * `session` - An optional session if the user already has one from a
/// previous login call.
/// Create a new default client.
pub fn new() -> Result<Self> {
BaseClient::new_helper(None)
BaseClient::new_with_config(BaseClientConfig::default())
}
/// Create a new client.
///
/// # Arguments
///
/// * `session` - An optional session if the user already has one from a
/// * `config` - An optional session if the user already has one from a
/// previous login call.
///
/// * `store` - An open state store implementation that will be used through
/// the lifetime of the client.
pub fn new_with_state_store(store: Box<dyn StateStore>) -> Result<Self> {
BaseClient::new_helper(Some(store))
}
fn new_helper(store: Option<Box<dyn StateStore>>) -> Result<Self> {
pub fn new_with_config(config: BaseClientConfig) -> Result<Self> {
Ok(BaseClient {
session: Arc::new(RwLock::new(None)),
sync_token: Arc::new(RwLock::new(None)),
@ -170,10 +242,17 @@ impl BaseClient {
ignored_users: Arc::new(RwLock::new(Vec::new())),
push_ruleset: Arc::new(RwLock::new(None)),
event_emitter: Arc::new(RwLock::new(None)),
state_store: Arc::new(RwLock::new(store)),
needs_state_store_sync: Arc::new(AtomicBool::from(true)),
state_store: Arc::new(RwLock::new(config.state_store)),
#[cfg(feature = "encryption")]
olm: Arc::new(Mutex::new(None)),
#[cfg(feature = "encryption")]
cryptostore: Arc::new(Mutex::new(config.crypto_store)),
store_path: Arc::new(config.store_path),
store_passphrase: Arc::new(
config
.passphrase
.unwrap_or_else(|| Zeroizing::new("DEFAULT_PASSPHRASE".to_owned())),
),
})
}
@ -197,55 +276,55 @@ impl BaseClient {
*self.event_emitter.write().await = Some(emitter);
}
/// Returns true if the state store has been loaded into the client.
pub fn is_state_store_synced(&self) -> bool {
!self.needs_state_store_sync.load(Ordering::Relaxed)
}
/// When a client is provided the state store will load state from the `StateStore`.
///
/// Returns `true` when a state store sync has successfully completed.
pub async fn sync_with_state_store(&self) -> Result<bool> {
async fn sync_with_state_store(&self, session: &Session) -> Result<bool> {
let store = self.state_store.read().await;
if let Some(store) = store.as_ref() {
if let Some(sess) = self.session.read().await.as_ref() {
if let Some(client_state) = store.load_client_state(sess).await? {
let ClientState {
sync_token,
ignored_users,
push_ruleset,
} = client_state;
*self.sync_token.write().await = sync_token;
*self.ignored_users.write().await = ignored_users;
*self.push_ruleset.write().await = push_ruleset;
} else {
// return false and continues with a sync request then save the state and create
// and populate the files during the sync
return Ok(false);
}
let AllRooms {
mut joined,
mut invited,
mut left,
} = store.load_all_rooms().await?;
*self.joined_rooms.write().await = joined
.drain()
.map(|(k, room)| (k, Arc::new(RwLock::new(room))))
.collect();
*self.invited_rooms.write().await = invited
.drain()
.map(|(k, room)| (k, Arc::new(RwLock::new(room))))
.collect();
*self.left_rooms.write().await = left
.drain()
.map(|(k, room)| (k, Arc::new(RwLock::new(room))))
.collect();
self.needs_state_store_sync.store(false, Ordering::Relaxed);
let loaded = if let Some(store) = store.as_ref() {
if let Some(client_state) = store.load_client_state(session).await? {
let ClientState {
sync_token,
ignored_users,
push_ruleset,
} = client_state;
*self.sync_token.write().await = sync_token;
*self.ignored_users.write().await = ignored_users;
*self.push_ruleset.write().await = push_ruleset;
} else {
// return false and continues with a sync request then save the state and create
// and populate the files during the sync
return Ok(false);
}
}
Ok(!self.needs_state_store_sync.load(Ordering::Relaxed))
let AllRooms {
mut joined,
mut invited,
mut left,
} = store.load_all_rooms().await?;
*self.joined_rooms.write().await = joined
.drain()
.map(|(k, room)| (k, Arc::new(RwLock::new(room))))
.collect();
*self.invited_rooms.write().await = invited
.drain()
.map(|(k, room)| (k, Arc::new(RwLock::new(room))))
.collect();
*self.left_rooms.write().await = left
.drain()
.map(|(k, room)| (k, Arc::new(RwLock::new(room))))
.collect();
true
} else {
false
};
Ok(loaded)
}
/// When a client is provided the state store will load state from the `StateStore`.
@ -303,9 +382,54 @@ impl BaseClient {
#[cfg(feature = "encryption")]
{
let mut olm = self.olm.lock().await;
*olm = Some(OlmMachine::new(&session.user_id, &session.device_id));
let store = self.cryptostore.lock().await.take();
if let Some(store) = store {
*olm = Some(
OlmMachine::new_with_store(
session.user_id.to_owned(),
session.device_id.to_owned(),
store,
)
.await
.unwrap(),
);
} else if let Some(path) = self.store_path.as_ref() {
#[cfg(feature = "sqlite-cryptostore")]
{
*olm = Some(
OlmMachine::new_with_default_store(
&session.user_id,
&session.device_id,
path,
&self.store_passphrase,
)
.await
.unwrap(),
);
}
#[cfg(not(feature = "sqlite-cryptostore"))]
{
*olm = Some(OlmMachine::new(&session.user_id, &session.device_id));
}
} else {
*olm = Some(OlmMachine::new(&session.user_id, &session.device_id));
}
}
self.sync_with_state_store().await?;
// If there wasn't a state store opened, try to open the default one if
// a store path was provided.
if self.state_store.read().await.is_none() {
#[cfg(not(target_arch = "wasm32"))]
{
if let Some(path) = &*self.store_path {
let store = JsonStore::open(path)?;
*self.state_store.write().await = Some(Box::new(store));
}
}
}
self.sync_with_state_store(&session).await?;
*self.session.write().await = Some(session);
@ -713,8 +837,6 @@ impl BaseClient {
/// # Arguments
///
/// * `response` - The response that we received after a successful sync.
///
/// * `did_update` - Signals to the `StateStore` if the client state needs updating.
pub async fn receive_sync_response(
&self,
response: &mut api::sync::sync_events::Response,

View File

@ -45,7 +45,7 @@ mod models;
mod session;
mod state;
pub use client::{BaseClient, RoomState, RoomStateType};
pub use client::{BaseClient, BaseClientConfig, RoomState, RoomStateType};
pub use event_emitter::{EventEmitter, SyncRoom};
#[cfg(feature = "encryption")]
pub use matrix_sdk_crypto::{Device, TrustState};

View File

@ -205,7 +205,7 @@ mod test {
use crate::api::r0::sync::sync_events::Response as SyncResponse;
use crate::identifiers::{RoomId, UserId};
use crate::{BaseClient, Session};
use crate::{BaseClient, BaseClientConfig, Session};
fn sync_response(file: &str) -> SyncResponse {
let mut file = File::open(file).unwrap();
@ -360,7 +360,8 @@ mod test {
// a sync response to populate our JSON store
let store = Box::new(JsonStore::open(path).unwrap());
let client = BaseClient::new_with_state_store(store).unwrap();
let client =
BaseClient::new_with_config(BaseClientConfig::new().state_store(store)).unwrap();
client.restore_login(session.clone()).await.unwrap();
let mut response = sync_response("../test_data/sync.json");
@ -370,9 +371,9 @@ mod test {
// now syncing the client will update from the state store
let store = Box::new(JsonStore::open(path).unwrap());
let client = BaseClient::new_with_state_store(store).unwrap();
let client =
BaseClient::new_with_config(BaseClientConfig::new().state_store(store)).unwrap();
client.restore_login(session.clone()).await.unwrap();
client.sync_with_state_store().await.unwrap();
// assert the synced client and the logged in client are equal
assert_eq!(*client.session().read().await, Some(session));

View File

@ -13,7 +13,7 @@ version = "0.1.0"
[dependencies]
js_int = "0.1.5"
ruma-api = "0.16.1"
ruma-client-api = "0.8.0"
ruma-client-api = "0.9.0"
ruma-events = "0.21.2"
ruma-identifiers = "0.16.1"
instant = { version = "0.1.4", features = ["wasm-bindgen", "now"] }

View File

@ -27,10 +27,10 @@ zeroize = { version = "1.1.0", features = ["zeroize_derive"] }
url = "2.1.1"
# Misc dependencies
thiserror = "1.0.18"
thiserror = "1.0.19"
tracing = "0.1.14"
atomic = "0.4.5"
dashmap = "3.11.1"
dashmap = "3.11.2"
[dependencies.tracing-futures]
version = "0.2.4"

View File

@ -141,9 +141,9 @@ impl OlmMachine {
/// * `store` - A `Cryptostore` implementation that will be used to store
/// the encryption keys.
pub async fn new_with_store(
user_id: &UserId,
device_id: &str,
mut store: impl CryptoStore + 'static,
user_id: UserId,
device_id: String,
mut store: Box<dyn CryptoStore>,
) -> StoreError<Self> {
let account = match store.load_account().await? {
Some(a) => {
@ -161,7 +161,7 @@ impl OlmMachine {
device_id: device_id.to_owned(),
account,
uploaded_signed_key_count: None,
store: Box::new(store),
store,
outbound_group_sessions: HashMap::new(),
})
}
@ -181,12 +181,12 @@ impl OlmMachine {
user_id: &UserId,
device_id: &str,
path: P,
passphrase: String,
passphrase: &str,
) -> StoreError<Self> {
let store =
SqliteStore::open_with_passphrase(&user_id, device_id, path, passphrase).await?;
OlmMachine::new_with_store(user_id, device_id, store).await
OlmMachine::new_with_store(user_id.to_owned(), device_id.to_owned(), Box::new(store)).await
}
/// The unique user id that owns this identity.

View File

@ -91,9 +91,15 @@ impl SqliteStore {
user_id: &UserId,
device_id: &str,
path: P,
passphrase: String,
passphrase: &str,
) -> Result<SqliteStore> {
SqliteStore::open_helper(user_id, device_id, path, Some(Zeroizing::new(passphrase))).await
SqliteStore::open_helper(
user_id,
device_id,
path,
Some(Zeroizing::new(passphrase.to_owned())),
)
.await
}
fn path_to_url(path: &Path) -> Result<Url> {
@ -801,14 +807,9 @@ mod test {
let user_id = &UserId::try_from(USER_ID).unwrap();
let store = if let Some(passphrase) = passphrase {
SqliteStore::open_with_passphrase(
&user_id,
DEVICE_ID,
tmpdir_path,
passphrase.to_owned(),
)
.await
.expect("Can't create a passphrase protected store")
SqliteStore::open_with_passphrase(&user_id, DEVICE_ID, tmpdir_path, passphrase)
.await
.expect("Can't create a passphrase protected store")
} else {
SqliteStore::open(&user_id, DEVICE_ID, tmpdir_path)
.await