From be34b72a0ba9678349a207252d8546edf6d650fd Mon Sep 17 00:00:00 2001 From: Devin R Date: Sat, 11 Apr 2020 13:15:36 -0400 Subject: [PATCH 01/18] state_store: WIP trait layout and default json impl --- src/lib.rs | 2 ++ src/state/mod.rs | 67 ++++++++++++++++++++++++++++++++++++++++ src/state/state_store.rs | 12 +++++++ 3 files changed, 81 insertions(+) create mode 100644 src/state/mod.rs create mode 100644 src/state/state_store.rs diff --git a/src/lib.rs b/src/lib.rs index 28bf9375..f7dfe7ce 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,6 +39,7 @@ mod error; mod event_emitter; mod models; mod session; +mod state; #[cfg(test)] pub mod test_builder; @@ -50,5 +51,6 @@ pub use async_client::{AsyncClient, AsyncClientConfig, SyncSettings}; pub use base_client::Client; pub use event_emitter::EventEmitter; pub use models::Room; +pub use state::{StateStore, JsonStore}; pub(crate) const VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/src/state/mod.rs b/src/state/mod.rs new file mode 100644 index 00000000..f315bc25 --- /dev/null +++ b/src/state/mod.rs @@ -0,0 +1,67 @@ +// Copyright 2020 Damir Jelić +// Copyright 2020 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. + +pub mod state_store; +pub use state_store::JsonStore; + +use crate::api; +use crate::events; +use api::r0::message::create_message_event; +use api::r0::session::login; +use api::r0::sync::sync_events; +use events::collections::all::{Event as NonRoomEvent, RoomEvent, StateEvent}; + +use std::collections::HashMap; +use std::convert::{TryFrom, TryInto}; +use std::result::Result as StdResult; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use uuid::Uuid; + +use futures::future::Future; +use tokio::sync::RwLock; +use tokio::time::delay_for as sleep; +#[cfg(feature = "encryption")] +use tracing::debug; +use tracing::{info, instrument, trace}; + +use http::Method as HttpMethod; +use http::Response as HttpResponse; +use reqwest::header::{HeaderValue, InvalidHeaderValue}; +use url::Url; + +use ruma_api::{Endpoint, Outgoing}; +use ruma_events::room::message::MessageEventContent; +use ruma_events::EventResult; +pub use ruma_events::EventType; +use ruma_identifiers::RoomId; + +use crate::base_client::Client as BaseClient; +use crate::models::Room; +use crate::session::Session; +use crate::VERSION; +use crate::{Error, EventEmitter, Result}; +/// Abstraction around the data store to avoid unnecessary request on client initialization. +/// +pub trait StateStore { + fn load_state(&self) -> sync_events::IncomingResponse; + + fn save_state_events(&mut self, events: Vec) -> Result<()>; + fn save_room_events(&mut self, events: Vec) -> Result<()>; + fn save_non_room_events(&mut self, events: Vec) -> Result<()>; + + +} diff --git a/src/state/state_store.rs b/src/state/state_store.rs new file mode 100644 index 00000000..4f1f4d02 --- /dev/null +++ b/src/state/state_store.rs @@ -0,0 +1,12 @@ +use super::StateStore; + +/// A default `StateStore` implementation that serializes state as json +/// and saves it to disk. +pub struct JsonStore { + +} + +impl StateStore for JsonStore { + +} + From 9c73947e21af5b60b3b319919070f37ac8aa2d87 Mon Sep 17 00:00:00 2001 From: Devin R Date: Tue, 14 Apr 2020 16:23:55 -0400 Subject: [PATCH 02/18] design-doc: add more state store info --- design.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/design.md b/design.md index 8b974294..7b7dae99 100644 --- a/design.md +++ b/design.md @@ -3,7 +3,7 @@ ## Design and Layout #### Async Client -The highest level structure that ties the other pieces of functionality together. The client is responsible for the Request/Response cycle. It can be thought of as a thin layer atop the `BaseClient` passing requests along for the `BaseClient` to handle. A user should be able to write their own `AsyncClient` using the `BaseClient`. It knows how to +The highest level structure that ties the other pieces of functionality together. The client is responsible for the Request/Response cycle. It can be thought of as a thin layer atop the `BaseClient` passing requests along for the `BaseClient` to handle. A user should be able to write their own `AsyncClient` using the `BaseClient`. It knows how to - login - send messages - encryption ... @@ -92,6 +92,8 @@ The `BaseClient` also has access to a `dyn StateStore` this is an abstraction ar - store/save - update ?? +The state store will restore our client state in the `BaseClient` and client authors can just get the latest state that they want to present from the client object. No need to ask the state store for it, this may change if custom setups request this. `StateStore`'s base is load/store and internally to the crate update the `BaseClient`. + #### Event Emitter The consumer of this crate can implement the `EventEmitter` trait for full control over how incoming events are handled by their client. If that isn't enough it is possible to receive every incoming response with the `AsyncClient::sync_forever` callback. - list the methods for `EventEmitter`? From 7889da2b302cb43f9fa845e7b28ae3e2d809047a Mon Sep 17 00:00:00 2001 From: Devin R Date: Thu, 16 Apr 2020 10:02:59 -0400 Subject: [PATCH 03/18] state_store: very rough draft of json store --- Cargo.toml | 8 ++- design.md | 4 +- src/error.rs | 9 +++ src/models/room.rs | 8 +-- src/models/room_member.rs | 19 +++++- src/session.rs | 4 +- src/state/mod.rs | 139 +++++++++++++++++++++++++++----------- src/state/state_store.rs | 139 ++++++++++++++++++++++++++++++++++++-- 8 files changed, 273 insertions(+), 57 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0d76e75a..c160a20d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,15 +12,18 @@ version = "0.1.0" [features] default = [] -encryption = ["olm-rs", "serde/derive", "serde_json", "cjson", "zeroize"] +encryption = ["olm-rs", "serde/derive", "cjson", "zeroize"] sqlite-cryptostore = ["sqlx", "zeroize"] [dependencies] +dirs = "2.0.2" futures = "0.3.4" reqwest = "0.10.4" http = "0.2.1" url = "2.1.1" async-trait = "0.1.30" +serde = "1.0.106" +serde_json = "1.0.51" # Ruma dependencies js_int = "0.1.4" @@ -32,8 +35,6 @@ uuid = { version = "0.8.1", features = ["v4"] } # Dependencies for the encryption support olm-rs = { git = "https://gitlab.gnome.org/poljar/olm-rs", optional = true, features = ["serde"]} -serde = { version = "1.0.106", optional = true, features = ["derive"] } -serde_json = { version = "1.0.51", optional = true } cjson = { version = "0.1.0", optional = true } zeroize = { version = "1.1.0", optional = true, features = ["zeroize_derive"] } @@ -65,3 +66,4 @@ serde_json = "1.0.51" tracing-subscriber = "0.2.4" tempfile = "3.1.0" mockito = "0.25.1" +lazy_static = "1.4.0" diff --git a/design.md b/design.md index 7b7dae99..43e9d7d8 100644 --- a/design.md +++ b/design.md @@ -11,7 +11,7 @@ The highest level structure that ties the other pieces of functionality together - make raw Http requests #### Base Client/Client State Machine -In addition to Http the `AsyncClient` passes along methods from the `BaseClient` that deal with `Room`s and `RoomMember`s. This allows the client to keep track of more complicated information that needs to be calculated in some way. +In addition to Http, the `AsyncClient` passes along methods from the `BaseClient` that deal with `Room`s and `RoomMember`s. This allows the client to keep track of more complicated information that needs to be calculated in some way. - human readable room names - power level? - ignored list? @@ -87,7 +87,7 @@ pub struct RoomMember { ``` #### State Store -The `BaseClient` also has access to a `dyn StateStore` this is an abstraction around a "database" to keep client state without requesting a full sync from the server on start up. A default implementation that serializes/deserializes json to files in a specified directory can be used. The user can also implement `StateStore` to fit any storage solution they choose. +The `BaseClient` also has access to a `dyn StateStore` this is an abstraction around a "database" to keep the client state without requesting a full sync from the server on startup. A default implementation that serializes/deserializes JSON to files in a specified directory can be used. The user can also implement `StateStore` to fit any storage solution they choose. - load - store/save - update ?? diff --git a/src/error.rs b/src/error.rs index 817baae9..33e119a6 100644 --- a/src/error.rs +++ b/src/error.rs @@ -15,10 +15,13 @@ //! Error conditions. +use std::io::Error as IoError; + use reqwest::Error as ReqwestError; use ruma_api::error::FromHttpResponseError as RumaResponseError; use ruma_api::error::IntoHttpError as RumaIntoHttpError; use ruma_client_api::Error as RumaClientError; +use serde_json::Error as JsonError; use thiserror::Error; use url::ParseError; @@ -46,6 +49,12 @@ pub enum Error { /// An error converting between ruma_client_api types and Hyper types. #[error("can't convert between ruma_client_api and hyper types.")] IntoHttp(RumaIntoHttpError), + /// An error de/serializing type for the `StateStore` + #[error(transparent)] + SerdeJson(#[from] JsonError), + /// An error de/serializing type for the `StateStore` + #[error(transparent)] + IoError(#[from] IoError), #[cfg(feature = "encryption")] /// An error occured durring a E2EE operation. #[error(transparent)] diff --git a/src/models/room.rs b/src/models/room.rs index 9701d04a..21ce143b 100644 --- a/src/models/room.rs +++ b/src/models/room.rs @@ -32,8 +32,8 @@ use crate::events::EventType; use crate::identifiers::{RoomAliasId, RoomId, UserId}; use js_int::{Int, UInt}; - -#[derive(Debug, Default)] +use serde::{Deserialize, Serialize}; +#[derive(Debug, Default, PartialEq, Serialize, Deserialize)] /// `RoomName` allows the calculation of a text room name. pub struct RoomName { /// The displayed name of the room. @@ -44,7 +44,7 @@ pub struct RoomName { aliases: Vec, } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct PowerLevels { /// The level required to ban a user. pub ban: Int, @@ -70,7 +70,7 @@ pub struct PowerLevels { pub notifications: Int, } -#[derive(Debug)] +#[derive(Debug, PartialEq, Serialize, Deserialize)] /// A Matrix rooom. pub struct Room { /// The unique id of the room. diff --git a/src/models/room_member.rs b/src/models/room_member.rs index e1211112..fb784653 100644 --- a/src/models/room_member.rs +++ b/src/models/room_member.rs @@ -24,10 +24,10 @@ use crate::events::room::{ use crate::identifiers::UserId; use js_int::{Int, UInt}; - +use serde::{Deserialize, Serialize}; // Notes: if Alice invites Bob into a room we will get an event with the sender as Alice and the state key as Bob. -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize)] /// A Matrix room member. /// pub struct RoomMember { @@ -58,11 +58,26 @@ pub struct RoomMember { /// The human readable name of this room member. pub name: String, /// The events that created the state of this room member. + #[serde(skip)] pub events: Vec, /// The `PresenceEvent`s connected to this user. + #[serde(skip)] pub presence_events: Vec, } +impl PartialEq for RoomMember { + fn eq(&self, other: &RoomMember) -> bool { + // TODO check everything but events and presence_events they dont impl PartialEq + self.room_id == other.room_id + && self.user_id == other.user_id + && self.name == other.name + && self.display_name == other.display_name + && self.avatar_url == other.avatar_url + && self.last_active_ago == other.last_active_ago + && self.membership == other.membership + } +} + impl RoomMember { pub fn new(event: &MemberEvent) -> Self { Self { diff --git a/src/session.rs b/src/session.rs index 6df86eff..eeee864f 100644 --- a/src/session.rs +++ b/src/session.rs @@ -16,9 +16,9 @@ //! User sessions. use ruma_identifiers::UserId; - +use serde::{Deserialize, Serialize}; /// A user session, containing an access token and information about the associated user account. -#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] pub struct Session { /// The access token used for this session. pub access_token: String, diff --git a/src/state/mod.rs b/src/state/mod.rs index 0a95bb88..ae81fc82 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -16,53 +16,112 @@ pub mod state_store; pub use state_store::JsonStore; -use crate::api; -use crate::events; -use api::r0::message::create_message_event; -use api::r0::session::login; -use api::r0::sync::sync_events; -use events::collections::all::{Event as NonRoomEvent, RoomEvent, StateEvent}; +use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::convert::{TryFrom, TryInto}; -use std::result::Result as StdResult; -use std::sync::Arc; -use std::time::{Duration, Instant}; - -use uuid::Uuid; - -use futures::future::Future; -use tokio::sync::RwLock; -use tokio::time::delay_for as sleep; -#[cfg(feature = "encryption")] -use tracing::debug; -use tracing::{info, instrument, trace}; - -use http::Method as HttpMethod; -use http::Response as HttpResponse; -use reqwest::header::{HeaderValue, InvalidHeaderValue}; -use url::Url; - -use ruma_api::{Endpoint, Outgoing}; -use ruma_events::room::message::MessageEventContent; -use ruma_events::EventResult; -pub use ruma_events::EventType; -use ruma_identifiers::RoomId; - -use crate::base_client::Client as BaseClient; +use crate::events::push_rules::Ruleset; +use crate::identifiers::{RoomId, UserId}; use crate::models::Room; use crate::session::Session; -use crate::VERSION; -use crate::{Error, EventEmitter, Result}; +use crate::{base_client::Token, Result}; + +#[derive(Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct ClientState { + /// The current client session containing our user id, device id and access + /// token. + pub session: Option, + /// The current sync token that should be used for the next sync call. + pub sync_token: Option, + /// A list of ignored users. + pub ignored_users: Vec, + /// The push ruleset for the logged in user. + pub push_ruleset: Option, +} + /// Abstraction around the data store to avoid unnecessary request on client initialization. -/// pub trait StateStore { /// - fn load_state(&self) -> sync_events::IncomingResponse; + fn load_client_state(&self) -> Result; /// - fn save_state_events(&mut self, events: Vec) -> Result<()>; + fn load_room_state(&self, room_id: &RoomId) -> Result; /// - fn save_room_events(&mut self, events: Vec) -> Result<()>; + fn store_client_state(&self, _: ClientState) -> Result<()>; /// - fn save_non_room_events(&mut self, events: Vec) -> Result<()>; + fn store_room_state(&self, _: &Room) -> Result<()>; +} + +#[cfg(test)] +mod test { + use super::*; + + use std::collections::HashMap; + use std::convert::TryFrom; + + use crate::identifiers::{RoomId, UserId}; + + #[test] + fn serialize() { + let id = RoomId::try_from("!roomid:example.com").unwrap(); + let user = UserId::try_from("@example:example.com").unwrap(); + + let room = Room::new(&id, &user); + + let state = ClientState { + session: None, + sync_token: Some("hello".into()), + ignored_users: vec![user], + push_ruleset: None, + }; + assert_eq!( + r#"{"session":null,"sync_token":"hello","ignored_users":["@example:example.com"],"push_ruleset":null}"#, + serde_json::to_string(&state).unwrap() + ); + + let mut joined_rooms = HashMap::new(); + joined_rooms.insert(id, room); + assert_eq!( + r#"{ + "!roomid:example.com": { + "room_id": "!roomid:example.com", + "room_name": { + "name": null, + "canonical_alias": null, + "aliases": [] + }, + "own_user_id": "@example:example.com", + "creator": null, + "members": {}, + "typing_users": [], + "power_levels": null, + "encrypted": false, + "unread_highlight": null, + "unread_notifications": null + } +}"#, + serde_json::to_string_pretty(&joined_rooms).unwrap() + ); + } + + #[test] + fn deserialize() { + let id = RoomId::try_from("!roomid:example.com").unwrap(); + let user = UserId::try_from("@example:example.com").unwrap(); + + let room = Room::new(&id, &user); + + let state = ClientState { + session: None, + sync_token: Some("hello".into()), + ignored_users: vec![user], + push_ruleset: None, + }; + let json = serde_json::to_string(&state).unwrap(); + + assert_eq!(state, serde_json::from_str(&json).unwrap()); + + let mut joined_rooms = HashMap::new(); + joined_rooms.insert(id, room); + let json = serde_json::to_string(&joined_rooms).unwrap(); + + assert_eq!(joined_rooms, serde_json::from_str(&json).unwrap()); + } } diff --git a/src/state/state_store.rs b/src/state/state_store.rs index 2ebb4052..ea78cee1 100644 --- a/src/state/state_store.rs +++ b/src/state/state_store.rs @@ -1,9 +1,140 @@ -use super::StateStore; +use std::fs::OpenOptions; +use std::io::{BufReader, BufWriter, Write}; +use std::path::Path; +use super::{ClientState, StateStore}; +use crate::identifiers::RoomId; +use crate::{Error, Result, Room}; /// A default `StateStore` implementation that serializes state as json /// and saves it to disk. -pub struct JsonStore {} +pub struct JsonStore; -// impl StateStore for JsonStore { +impl StateStore for JsonStore { + fn load_client_state(&self) -> Result { + if let Some(mut path) = dirs::home_dir() { + path.push(".matrix_store/client.json"); + let file = OpenOptions::new().read(true).open(path)?; + let reader = BufReader::new(file); + serde_json::from_reader(reader).map_err(Error::from) + } else { + todo!("Error maybe") + } + } -// } + fn load_room_state(&self, room_id: &RoomId) -> Result { + if let Some(mut path) = dirs::home_dir() { + path.push(&format!(".matrix_store/rooms/{}.json", room_id)); + + let file = OpenOptions::new().read(true).open(path)?; + let reader = BufReader::new(file); + serde_json::from_reader(reader).map_err(Error::from) + } else { + todo!("Error maybe") + } + } + + fn store_client_state(&self, state: ClientState) -> Result<()> { + if let Some(mut path) = dirs::home_dir() { + path.push(".matrix_store/client.json"); + + if !Path::new(&path).exists() { + let mut dir = path.clone(); + dir.pop(); + std::fs::create_dir_all(dir)?; + } + + let json = serde_json::to_string(&state).map_err(Error::from)?; + + let file = OpenOptions::new().write(true).create(true).open(path)?; + let mut writer = BufWriter::new(file); + writer.write_all(json.as_bytes())?; + + Ok(()) + } else { + todo!("Error maybe") + } + } + + fn store_room_state(&self, room: &Room) -> Result<()> { + if let Some(mut path) = dirs::home_dir() { + path.push(&format!(".matrix_store/rooms/{}.json", room.room_id)); + + if !Path::new(&path).exists() { + let mut dir = path.clone(); + dir.pop(); + std::fs::create_dir_all(dir)?; + } + + let json = serde_json::to_string(&room).map_err(Error::from)?; + + let file = OpenOptions::new().write(true).create(true).open(path)?; + let mut writer = BufWriter::new(file); + writer.write_all(json.as_bytes())?; + + Ok(()) + } else { + todo!("Error maybe") + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + use std::convert::TryFrom; + use std::fs; + use std::sync::Mutex; + + use lazy_static::lazy_static; + + use crate::identifiers::{RoomId, UserId}; + + lazy_static! { + /// Limit io tests to one thread at a time. + pub static ref MTX: Mutex<()> = Mutex::new(()); + } + + fn run_and_cleanup(test: fn()) { + let _lock = MTX.lock(); + + test(); + + let mut path = dirs::home_dir().unwrap(); + path.push(".matrix_store"); + + if path.exists() { + fs::remove_dir_all(path).unwrap(); + } + } + + fn test_store_client_state() { + let store = JsonStore; + let state = ClientState::default(); + store.store_client_state(state).unwrap(); + let loaded = store.load_client_state().unwrap(); + assert_eq!(loaded, ClientState::default()); + } + + #[test] + fn store_client_state() { + run_and_cleanup(test_store_client_state); + } + + fn test_store_room_state() { + let store = JsonStore; + + let id = RoomId::try_from("!roomid:example.com").unwrap(); + let user = UserId::try_from("@example:example.com").unwrap(); + + let room = Room::new(&id, &user); + store.store_room_state(&room).unwrap(); + let loaded = store.load_room_state(&id).unwrap(); + assert_eq!(loaded, Room::new(&id, &user)); + } + + #[test] + fn store_room_state() { + run_and_cleanup(test_store_room_state); + } +} From 9fb4bd9d190a4c2dce3452d9d8e0131a193e2656 Mon Sep 17 00:00:00 2001 From: Devin R Date: Sat, 18 Apr 2020 18:06:30 -0400 Subject: [PATCH 04/18] state_store: add associated store and error type, custom deserialization for ruma event types --- examples/command_bot.rs | 7 +++-- examples/login.rs | 3 +- src/async_client.rs | 22 +++++++-------- src/base_client.rs | 12 +++++--- src/event_emitter/mod.rs | 2 +- src/models/event_deser.rs | 56 +++++++++++++++++++++++++++++++++++++ src/models/mod.rs | 1 + src/models/room.rs | 2 +- src/models/room_member.rs | 6 ++-- src/request_builder.rs | 8 +++--- src/state/mod.rs | 28 ++++++++++++++----- src/state/state_store.rs | 9 ++++++ src/test_builder.rs | 30 +++++++++++--------- tests/async_client_tests.rs | 6 ++-- 14 files changed, 142 insertions(+), 50 deletions(-) create mode 100644 src/models/event_deser.rs diff --git a/examples/command_bot.rs b/examples/command_bot.rs index 220b0ea1..0cf607ba 100644 --- a/examples/command_bot.rs +++ b/examples/command_bot.rs @@ -12,11 +12,14 @@ use url::Url; struct CommandBot { /// This clone of the `AsyncClient` will send requests to the server, /// while the other keeps us in sync with the server using `sync_forever`. - client: AsyncClient, + /// + /// The two type parameters are for the `StateStore` trait and specify the `Store` + /// type and `IoError` type to use, here we don't care. + client: AsyncClient<(), ()>, } impl CommandBot { - pub fn new(client: AsyncClient) -> Self { + pub fn new(client: AsyncClient<(), ()>) -> Self { Self { client } } } diff --git a/examples/login.rs b/examples/login.rs index 150684c7..4521332c 100644 --- a/examples/login.rs +++ b/examples/login.rs @@ -45,7 +45,8 @@ async fn login( .proxy("http://localhost:8080")? .disable_ssl_verification(); let homeserver_url = Url::parse(&homeserver_url)?; - let mut client = AsyncClient::new_with_config(homeserver_url, None, client_config).unwrap(); + let mut client = + AsyncClient::<(), ()>::new_with_config(homeserver_url, None, client_config).unwrap(); client.add_event_emitter(Box::new(EventCallback)).await; diff --git a/src/async_client.rs b/src/async_client.rs index f206b9e9..cdd12870 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -55,16 +55,16 @@ const DEFAULT_SYNC_TIMEOUT: Duration = Duration::from_secs(30); /// An async/await enabled Matrix client. /// /// All of the state is held in an `Arc` so the `AsyncClient` can be cloned freely. -pub struct AsyncClient { +pub struct AsyncClient { /// The URL of the homeserver to connect to. homeserver: Url, /// The underlying HTTP client. http_client: reqwest::Client, /// User session data. - pub(crate) base_client: Arc>, + pub(crate) base_client: Arc>>, } -impl std::fmt::Debug for AsyncClient { +impl std::fmt::Debug for AsyncClient { fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> StdResult<(), std::fmt::Error> { write!(fmt, "AsyncClient {{ homeserver: {} }}", self.homeserver) } @@ -197,7 +197,7 @@ use api::r0::room::create_room; use api::r0::session::login; use api::r0::sync::sync_events; -impl AsyncClient { +impl AsyncClient { /// Creates a new client for making HTTP requests to the given homeserver. /// /// # Arguments @@ -485,7 +485,7 @@ impl AsyncClient { /// .name("name") /// .room_version("v1.0"); /// - /// let mut cli = AsyncClient::new(homeserver, None).unwrap(); + /// let mut cli = AsyncClient::<(), ()>::new(homeserver, None).unwrap(); /// # use futures::executor::block_on; /// # block_on(async { /// assert!(cli.create_room(builder).await.is_ok()); @@ -529,7 +529,7 @@ impl AsyncClient { /// .direction(Direction::Backward) /// .limit(UInt::new(10).unwrap()); /// - /// let mut cli = AsyncClient::new(homeserver, None).unwrap(); + /// let mut cli = AsyncClient::<(), ()>::new(homeserver, None).unwrap(); /// # use futures::executor::block_on; /// # block_on(async { /// assert!(cli.room_messages(builder).await.is_ok()); @@ -673,7 +673,7 @@ impl AsyncClient { /// # use futures::executor::block_on; /// # block_on(async { /// # let homeserver = Url::parse("http://localhost:8080").unwrap(); - /// # let mut client = AsyncClient::new(homeserver, None).unwrap(); + /// # let mut client = AsyncClient::<(), ()>::new(homeserver, None).unwrap(); /// /// use async_std::sync::channel; /// @@ -863,7 +863,7 @@ impl AsyncClient { /// use matrix_sdk::events::room::message::{MessageEventContent, TextMessageEventContent}; /// # block_on(async { /// # let homeserver = Url::parse("http://localhost:8080").unwrap(); - /// # let mut client = AsyncClient::new(homeserver, None).unwrap(); + /// # let mut client = AsyncClient::<(), ()>::new(homeserver, None).unwrap(); /// # let room_id = RoomId::try_from("!test:localhost").unwrap(); /// use uuid::Uuid; /// @@ -1120,7 +1120,7 @@ mod test { device_id: "DEVICEID".to_owned(), }; let homeserver = url::Url::parse(&mockito::server_url()).unwrap(); - let client = AsyncClient::new(homeserver, Some(session)).unwrap(); + let client = AsyncClient::<(), ()>::new(homeserver, Some(session)).unwrap(); let rid = RoomId::try_from("!roomid:room.com").unwrap(); let uid = UserId::try_from("@example:localhost").unwrap(); @@ -1152,7 +1152,7 @@ mod test { }; let homeserver = url::Url::parse(&mockito::server_url()).unwrap(); - let client = AsyncClient::new(homeserver, Some(session)).unwrap(); + let client = AsyncClient::<(), ()>::new(homeserver, Some(session)).unwrap(); let mut bld = EventBuilder::default() .add_room_event_from_file("./tests/data/events/member.json", RoomEvent::RoomMember) @@ -1182,7 +1182,7 @@ mod test { .with_body_from_file("tests/data/login_response_error.json") .create(); - let client = AsyncClient::new(homeserver, None).unwrap(); + let client = AsyncClient::<(), ()>::new(homeserver, None).unwrap(); if let Err(err) = client.login("example", "wordpass", None, None).await { if let crate::Error::RumaResponse(ruma_api::error::FromHttpResponseError::Http( diff --git a/src/base_client.rs b/src/base_client.rs index 30cefc79..1655fe86 100644 --- a/src/base_client.rs +++ b/src/base_client.rs @@ -34,6 +34,7 @@ use crate::events::EventResult; use crate::identifiers::{RoomId, UserId}; use crate::models::Room; use crate::session::Session; +use crate::state::StateStore; use crate::EventEmitter; #[cfg(feature = "encryption")] @@ -60,7 +61,7 @@ pub type Token = String; /// /// This Client is a state machine that receives responses and events and /// accordingly updates it's state. -pub struct Client { +pub struct Client { /// The current client session containing our user id, device id and access /// token. pub session: Option, @@ -75,12 +76,14 @@ pub struct Client { /// Any implementor of EventEmitter will act as the callbacks for various /// events. pub event_emitter: Option>, + /// + pub state_store: Option>>, #[cfg(feature = "encryption")] olm: Arc>>, } -impl fmt::Debug for Client { +impl fmt::Debug for Client { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Client") .field("session", &self.session) @@ -93,7 +96,7 @@ impl fmt::Debug for Client { } } -impl Client { +impl Client { /// Create a new client. /// /// # Arguments @@ -114,6 +117,7 @@ impl Client { ignored_users: Vec::new(), push_ruleset: None, event_emitter: None, + state_store: None, #[cfg(feature = "encryption")] olm: Arc::new(Mutex::new(olm)), }) @@ -811,7 +815,7 @@ mod test { .with_body_from_file("tests/data/sync.json") .create(); - let client = AsyncClient::new(homeserver, Some(session)).unwrap(); + let client = AsyncClient::<(), ()>::new(homeserver, Some(session)).unwrap(); let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); diff --git a/src/event_emitter/mod.rs b/src/event_emitter/mod.rs index 2d675521..369f73be 100644 --- a/src/event_emitter/mod.rs +++ b/src/event_emitter/mod.rs @@ -238,7 +238,7 @@ mod test { let vec = Arc::new(Mutex::new(Vec::new())); let test_vec = Arc::clone(&vec); let emitter = Box::new(EvEmitterTest(vec)) as Box<(dyn EventEmitter)>; - let mut client = AsyncClient::new(homeserver, Some(session)).unwrap(); + let mut client = AsyncClient::<(), ()>::new(homeserver, Some(session)).unwrap(); client.add_event_emitter(emitter).await; let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); diff --git a/src/models/event_deser.rs b/src/models/event_deser.rs new file mode 100644 index 00000000..7c0a6252 --- /dev/null +++ b/src/models/event_deser.rs @@ -0,0 +1,56 @@ +//! De-/serialization functions to and from json strings, allows the type to be used as a query string. + +use serde::de::{Deserialize, Deserializer, Error as _}; + +use crate::events::collections::all::Event; +use crate::events::presence::PresenceEvent; +use crate::events::EventResult; + +pub fn deserialize_events<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let mut events = vec![]; + let ev = Vec::>::deserialize(deserializer)?; + for event in ev { + events.push(event.into_result().map_err(D::Error::custom)?); + } + + Ok(events) +} + +pub fn deserialize_presence<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let mut events = vec![]; + let ev = Vec::>::deserialize(deserializer)?; + for event in ev { + events.push(event.into_result().map_err(D::Error::custom)?); + } + + Ok(events) +} + +#[cfg(test)] +mod test { + use std::fs; + + use crate::events::room::member::MemberEvent; + use crate::events::EventResult; + use crate::models::RoomMember; + + #[test] + fn events_and_presence_deserialization() { + let ev_json = fs::read_to_string("./tests/data/events/member.json").unwrap(); + let ev = serde_json::from_str::>(&ev_json) + .unwrap() + .into_result() + .unwrap(); + let member = RoomMember::new(&ev); + + let member_json = serde_json::to_string(&member).unwrap(); + let mem = serde_json::from_str::(&member_json).unwrap(); + assert_eq!(member, mem); + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs index 07fcb00e..5e461a32 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,3 +1,4 @@ +mod event_deser; mod room; mod room_member; diff --git a/src/models/room.rs b/src/models/room.rs index f6353583..a87bb792 100644 --- a/src/models/room.rs +++ b/src/models/room.rs @@ -439,7 +439,7 @@ mod test { .with_body_from_file("tests/data/sync.json") .create(); - let client = AsyncClient::new(homeserver, Some(session)).unwrap(); + let client = AsyncClient::<(), ()>::new(homeserver, Some(session)).unwrap(); let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); diff --git a/src/models/room_member.rs b/src/models/room_member.rs index fb784653..b53d660c 100644 --- a/src/models/room_member.rs +++ b/src/models/room_member.rs @@ -58,16 +58,16 @@ pub struct RoomMember { /// The human readable name of this room member. pub name: String, /// The events that created the state of this room member. - #[serde(skip)] + #[serde(deserialize_with = "super::event_deser::deserialize_events")] pub events: Vec, /// The `PresenceEvent`s connected to this user. - #[serde(skip)] + #[serde(deserialize_with = "super::event_deser::deserialize_presence")] pub presence_events: Vec, } impl PartialEq for RoomMember { fn eq(&self, other: &RoomMember) -> bool { - // TODO check everything but events and presence_events they dont impl PartialEq + // TODO check everything but events and presence_events they don;t impl PartialEq self.room_id == other.room_id && self.user_id == other.user_id && self.name == other.name diff --git a/src/request_builder.rs b/src/request_builder.rs index 795e8534..d76a5179 100644 --- a/src/request_builder.rs +++ b/src/request_builder.rs @@ -29,7 +29,7 @@ use js_int::UInt; /// .visibility(Visibility::Public) /// .name("name") /// .room_version("v1.0"); -/// let mut cli = AsyncClient::new(homeserver, None).unwrap(); +/// let mut cli = AsyncClient::<(), ()>::new(homeserver, None).unwrap(); /// cli.create_room(builder).await; /// # }) /// ``` @@ -186,7 +186,7 @@ impl Into for RoomBuilder { /// # rt.block_on(async { /// # let room_id = RoomId::new(homeserver.as_str()).unwrap(); /// # let last_sync_token = "".to_string();; -/// let mut cli = AsyncClient::new(homeserver, None).unwrap(); +/// let mut cli = AsyncClient::<(), ()>::new(homeserver, None).unwrap(); /// /// let mut builder = MessagesRequestBuilder::new(); /// builder.room_id(room_id) @@ -341,7 +341,7 @@ mod test { .room_alias_name("room_alias") .topic("room topic") .visibility(Visibility::Private); - let cli = AsyncClient::new(homeserver, Some(session)).unwrap(); + let cli = AsyncClient::<(), ()>::new(homeserver, Some(session)).unwrap(); assert!(cli.create_room(builder).await.is_ok()); } @@ -373,7 +373,7 @@ mod test { // TODO this makes ruma error `Err(IntoHttp(IntoHttpError(Query(Custom("unsupported value")))))`?? // .filter(RoomEventFilter::default()); - let cli = AsyncClient::new(homeserver, Some(session)).unwrap(); + let cli = AsyncClient::<(), ()>::new(homeserver, Some(session)).unwrap(); assert!(cli.room_messages(builder).await.is_ok()); } } diff --git a/src/state/mod.rs b/src/state/mod.rs index ae81fc82..0df00c12 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -13,16 +13,18 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::path::Path; + pub mod state_store; pub use state_store::JsonStore; use serde::{Deserialize, Serialize}; +use crate::base_client::Token; use crate::events::push_rules::Ruleset; use crate::identifiers::{RoomId, UserId}; use crate::models::Room; use crate::session::Session; -use crate::{base_client::Token, Result}; #[derive(Debug, Default, PartialEq, Serialize, Deserialize)] pub struct ClientState { @@ -38,15 +40,24 @@ pub struct ClientState { } /// Abstraction around the data store to avoid unnecessary request on client initialization. -pub trait StateStore { +pub trait StateStore: Send + Sync { + /// The type of store to create. The default `JsonStore` uses `ClientState` as the store + /// to serialize and deserialize state to JSON files. + type Store; + + /// The error type to return. + type IoError; + + /// Set up connections or open files to load/save state. + fn open(&self, path: &Path) -> Result<(), Self::IoError>; /// - fn load_client_state(&self) -> Result; + fn load_client_state(&self) -> Result; /// - fn load_room_state(&self, room_id: &RoomId) -> Result; + fn load_room_state(&self, room_id: &RoomId) -> Result; /// - fn store_client_state(&self, _: ClientState) -> Result<()>; + fn store_client_state(&self, _: Self::Store) -> Result<(), Self::IoError>; /// - fn store_room_state(&self, _: &Room) -> Result<()>; + fn store_room_state(&self, _: &Room) -> Result<(), Self::IoError>; } #[cfg(test)] @@ -85,7 +96,10 @@ mod test { "room_name": { "name": null, "canonical_alias": null, - "aliases": [] + "aliases": [], + "heroes": [], + "joined_member_count": null, + "invited_member_count": null }, "own_user_id": "@example:example.com", "creator": null, diff --git a/src/state/state_store.rs b/src/state/state_store.rs index ea78cee1..65e3b7ee 100644 --- a/src/state/state_store.rs +++ b/src/state/state_store.rs @@ -10,6 +10,15 @@ use crate::{Error, Result, Room}; pub struct JsonStore; impl StateStore for JsonStore { + type IoError = Error; + type Store = ClientState; + + fn open(&self, path: &Path) -> Result<()> { + if !path.exists() { + std::fs::create_dir_all(path)?; + } + Ok(()) + } fn load_client_state(&self) -> Result { if let Some(mut path) = dirs::home_dir() { path.push(".matrix_store/client.json"); diff --git a/src/test_builder.rs b/src/test_builder.rs index 4849cfb1..edcff255 100644 --- a/src/test_builder.rs +++ b/src/test_builder.rs @@ -49,9 +49,9 @@ pub struct RoomTestRunner { state_events: Vec, } -pub struct ClientTestRunner { +pub struct ClientTestRunner { /// Used when testing the whole client - client: Option, + client: Option>, /// RoomId and UserId to use for the events. /// /// The RoomId must match the RoomId of the events to track. @@ -69,9 +69,9 @@ pub struct ClientTestRunner { } #[allow(dead_code)] -pub struct MockTestRunner { +pub struct MockTestRunner { /// Used when testing the whole client - client: Option, + client: Option>, /// The ephemeral room events that determine the state of a `Room`. ephemeral: Vec, /// The account data events that determine the state of a `Room`. @@ -169,11 +169,11 @@ impl EventBuilder { /// /// The `TestRunner` streams the events to the client and holds methods to make assertions /// about the state of the client. - pub fn build_mock_runner>( + pub fn build_mock_runner>( mut self, method: &str, path: P, - ) -> MockTestRunner { + ) -> MockTestRunner { let body = serde_json::json! { { "device_one_time_keys_count": {}, @@ -238,7 +238,11 @@ impl EventBuilder { /// /// The `TestRunner` streams the events to the `AsyncClient` and holds methods to make assertions /// about the state of the `AsyncClient`. - pub fn build_client_runner(self, room_id: RoomId, user_id: UserId) -> ClientTestRunner { + pub fn build_client_runner( + self, + room_id: RoomId, + user_id: UserId, + ) -> ClientTestRunner { ClientTestRunner { client: None, room_user_id: (room_id, user_id), @@ -313,8 +317,8 @@ impl RoomTestRunner { } } -impl ClientTestRunner { - pub fn set_client(&mut self, client: AsyncClient) -> &mut Self { +impl ClientTestRunner { + pub fn set_client(&mut self, client: AsyncClient) -> &mut Self { self.client = Some(client); self } @@ -355,14 +359,14 @@ impl ClientTestRunner { } } - pub async fn to_client(&mut self) -> &mut AsyncClient { + pub async fn to_client(&mut self) -> &mut AsyncClient { self.stream_client_events().await; self.client.as_mut().unwrap() } } -impl MockTestRunner { - pub fn set_client(&mut self, client: AsyncClient) -> &mut Self { +impl MockTestRunner { + pub fn set_client(&mut self, client: AsyncClient) -> &mut Self { self.client = Some(client); self } @@ -372,7 +376,7 @@ impl MockTestRunner { self } - pub async fn to_client(&mut self) -> Result<&mut AsyncClient, crate::Error> { + pub async fn to_client(&mut self) -> Result<&mut AsyncClient, crate::Error> { self.client .as_mut() .unwrap() diff --git a/tests/async_client_tests.rs b/tests/async_client_tests.rs index 3c9dacdc..fe68f9ae 100644 --- a/tests/async_client_tests.rs +++ b/tests/async_client_tests.rs @@ -17,7 +17,7 @@ async fn login() { .with_body_from_file("tests/data/login_response.json") .create(); - let client = AsyncClient::new(homeserver, None).unwrap(); + let client = AsyncClient::<(), ()>::new(homeserver, None).unwrap(); client .login("example", "wordpass", None, None) @@ -46,7 +46,7 @@ async fn sync() { .with_body_from_file("tests/data/sync.json") .create(); - let client = AsyncClient::new(homeserver, Some(session)).unwrap(); + let client = AsyncClient::<(), ()>::new(homeserver, Some(session)).unwrap(); let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); @@ -75,7 +75,7 @@ async fn room_names() { .with_body_from_file("tests/data/sync.json") .create(); - let client = AsyncClient::new(homeserver, Some(session)).unwrap(); + let client = AsyncClient::<(), ()>::new(homeserver, Some(session)).unwrap(); let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); From 8f89e8335ce0536af297b37aaa32a10fa14a1f79 Mon Sep 17 00:00:00 2001 From: Devin R Date: Sun, 19 Apr 2020 15:25:25 -0400 Subject: [PATCH 05/18] state_store: add load_all_rooms, fix clippy warnings --- src/models/room.rs | 6 ++--- src/state/mod.rs | 11 +++++++--- src/state/state_store.rs | 47 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 57 insertions(+), 7 deletions(-) diff --git a/src/models/room.rs b/src/models/room.rs index a87bb792..c8d99ebd 100644 --- a/src/models/room.rs +++ b/src/models/room.rs @@ -257,8 +257,8 @@ impl Room { invited_member_count, } = summary; self.room_name.heroes = heroes.clone(); - self.room_name.invited_member_count = invited_member_count.clone(); - self.room_name.joined_member_count = joined_member_count.clone(); + self.room_name.invited_member_count = *invited_member_count; + self.room_name.joined_member_count = *joined_member_count; } /// Handle a room.member updating the room state if necessary. @@ -453,7 +453,7 @@ mod test { .await; assert_eq!(2, room.members.len()); - for (_id, member) in &room.members { + for member in room.members.values() { assert_eq!(MembershipState::Join, member.membership); } diff --git a/src/state/mod.rs b/src/state/mod.rs index 0df00c12..087fc76b 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -13,6 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::collections::HashMap; use std::path::Path; pub mod state_store; @@ -50,13 +51,17 @@ pub trait StateStore: Send + Sync { /// Set up connections or open files to load/save state. fn open(&self, path: &Path) -> Result<(), Self::IoError>; - /// + /// Loads the state of `BaseClient` through `StateStore::Store` type. fn load_client_state(&self) -> Result; - /// + /// Load the state of a single `Room` by `RoomId`. fn load_room_state(&self, room_id: &RoomId) -> Result; + /// Load the state of all `Room`s. /// + /// This will be mapped over in the client in order to store `Room`s in an async safe way. + fn load_all_rooms(&self) -> Result, Self::IoError>; + /// Save the current state of the `BaseClient` using the `StateStore::Store` type. fn store_client_state(&self, _: Self::Store) -> Result<(), Self::IoError>; - /// + /// Save the state a single `Room`. fn store_room_state(&self, _: &Room) -> Result<(), Self::IoError>; } diff --git a/src/state/state_store.rs b/src/state/state_store.rs index 65e3b7ee..8e9fda09 100644 --- a/src/state/state_store.rs +++ b/src/state/state_store.rs @@ -1,4 +1,5 @@ -use std::fs::OpenOptions; +use std::collections::HashMap; +use std::fs::{self, OpenOptions}; use std::io::{BufReader, BufWriter, Write}; use std::path::Path; @@ -42,6 +43,33 @@ impl StateStore for JsonStore { } } + fn load_all_rooms(&self) -> Result> { + if let Some(mut path) = dirs::home_dir() { + path.push(".matrix_store/rooms/"); + + let mut rooms_map = HashMap::new(); + for file in fs::read_dir(&path)? { + let file = file?.path(); + + if file.is_dir() { + continue; + } + + let f_hdl = OpenOptions::new().read(true).open(&file)?; + let reader = BufReader::new(f_hdl); + + let room = serde_json::from_reader::<_, Room>(reader).map_err(Error::from)?; + let room_id = room.room_id.clone(); + + rooms_map.insert(room_id, room); + } + + Ok(rooms_map) + } else { + todo!("Error maybe") + } + } + fn store_client_state(&self, state: ClientState) -> Result<()> { if let Some(mut path) = dirs::home_dir() { path.push(".matrix_store/client.json"); @@ -146,4 +174,21 @@ mod test { fn store_room_state() { run_and_cleanup(test_store_room_state); } + + fn test_load_rooms() { + let store = JsonStore; + + let id = RoomId::try_from("!roomid:example.com").unwrap(); + let user = UserId::try_from("@example:example.com").unwrap(); + + let room = Room::new(&id, &user); + store.store_room_state(&room).unwrap(); + let loaded = store.load_all_rooms().unwrap(); + println!("{:?}", loaded); + } + + #[test] + fn load_rooms() { + run_and_cleanup(test_load_rooms); + } } From 267453de4d9ee1cc31113f7162142b529d2ff2e8 Mon Sep 17 00:00:00 2001 From: Devin R Date: Tue, 21 Apr 2020 09:36:59 -0400 Subject: [PATCH 06/18] state_store: add Path to StateStore methods, remove associated Error type --- examples/command_bot.rs | 8 +- examples/login.rs | 2 +- src/async_client.rs | 14 +-- src/base_client.rs | 10 +-- src/event_emitter/mod.rs | 2 +- src/models/room.rs | 2 +- src/models/room_member.rs | 2 +- src/request_builder.rs | 4 +- src/state/mod.rs | 17 ++-- src/state/state_store.rs | 164 +++++++++++++++++------------------- src/test_builder.rs | 30 +++---- tests/async_client_tests.rs | 6 +- 12 files changed, 123 insertions(+), 138 deletions(-) diff --git a/examples/command_bot.rs b/examples/command_bot.rs index 0cf607ba..40aed458 100644 --- a/examples/command_bot.rs +++ b/examples/command_bot.rs @@ -13,13 +13,13 @@ struct CommandBot { /// This clone of the `AsyncClient` will send requests to the server, /// while the other keeps us in sync with the server using `sync_forever`. /// - /// The two type parameters are for the `StateStore` trait and specify the `Store` - /// type and `IoError` type to use, here we don't care. - client: AsyncClient<(), ()>, + /// The type parameter is for the `StateStore` trait specifying the `Store` + /// type for state storage, here we don't care. + client: AsyncClient<()>, } impl CommandBot { - pub fn new(client: AsyncClient<(), ()>) -> Self { + pub fn new(client: AsyncClient<()>) -> Self { Self { client } } } diff --git a/examples/login.rs b/examples/login.rs index 4521332c..9fbe4e16 100644 --- a/examples/login.rs +++ b/examples/login.rs @@ -46,7 +46,7 @@ async fn login( .disable_ssl_verification(); let homeserver_url = Url::parse(&homeserver_url)?; let mut client = - AsyncClient::<(), ()>::new_with_config(homeserver_url, None, client_config).unwrap(); + AsyncClient::<()>::new_with_config(homeserver_url, None, client_config).unwrap(); client.add_event_emitter(Box::new(EventCallback)).await; diff --git a/src/async_client.rs b/src/async_client.rs index fae92458..7a0f1455 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -55,16 +55,16 @@ const DEFAULT_SYNC_TIMEOUT: Duration = Duration::from_secs(30); /// An async/await enabled Matrix client. /// /// All of the state is held in an `Arc` so the `AsyncClient` can be cloned freely. -pub struct AsyncClient { +pub struct AsyncClient { /// The URL of the homeserver to connect to. homeserver: Url, /// The underlying HTTP client. http_client: reqwest::Client, /// User session data. - pub(crate) base_client: Arc>>, + pub(crate) base_client: Arc>>, } -impl std::fmt::Debug for AsyncClient { +impl std::fmt::Debug for AsyncClient { fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> StdResult<(), std::fmt::Error> { write!(fmt, "AsyncClient {{ homeserver: {} }}", self.homeserver) } @@ -197,7 +197,7 @@ use api::r0::room::create_room; use api::r0::session::login; use api::r0::sync::sync_events; -impl AsyncClient { +impl AsyncClient { /// Creates a new client for making HTTP requests to the given homeserver. /// /// # Arguments @@ -1119,7 +1119,7 @@ mod test { device_id: "DEVICEID".to_owned(), }; let homeserver = url::Url::parse(&mockito::server_url()).unwrap(); - let client = AsyncClient::<(), ()>::new(homeserver, Some(session)).unwrap(); + let client = AsyncClient::<()>::new(homeserver, Some(session)).unwrap(); let rid = RoomId::try_from("!roomid:room.com").unwrap(); let uid = UserId::try_from("@example:localhost").unwrap(); @@ -1151,7 +1151,7 @@ mod test { }; let homeserver = url::Url::parse(&mockito::server_url()).unwrap(); - let client = AsyncClient::<(), ()>::new(homeserver, Some(session)).unwrap(); + let client = AsyncClient::<()>::new(homeserver, Some(session)).unwrap(); let mut bld = EventBuilder::default() .add_room_event_from_file("./tests/data/events/member.json", RoomEvent::RoomMember) @@ -1181,7 +1181,7 @@ mod test { .with_body_from_file("tests/data/login_response_error.json") .create(); - let client = AsyncClient::<(), ()>::new(homeserver, None).unwrap(); + let client = AsyncClient::<()>::new(homeserver, None).unwrap(); if let Err(err) = client.login("example", "wordpass", None, None).await { if let crate::Error::RumaResponse(ruma_api::error::FromHttpResponseError::Http( diff --git a/src/base_client.rs b/src/base_client.rs index 1655fe86..5fce01fd 100644 --- a/src/base_client.rs +++ b/src/base_client.rs @@ -61,7 +61,7 @@ pub type Token = String; /// /// This Client is a state machine that receives responses and events and /// accordingly updates it's state. -pub struct Client { +pub struct Client { /// The current client session containing our user id, device id and access /// token. pub session: Option, @@ -77,13 +77,13 @@ pub struct Client { /// events. pub event_emitter: Option>, /// - pub state_store: Option>>, + pub state_store: Option>>, #[cfg(feature = "encryption")] olm: Arc>>, } -impl fmt::Debug for Client { +impl fmt::Debug for Client { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Client") .field("session", &self.session) @@ -96,7 +96,7 @@ impl fmt::Debug for Client { } } -impl Client { +impl Client { /// Create a new client. /// /// # Arguments @@ -815,7 +815,7 @@ mod test { .with_body_from_file("tests/data/sync.json") .create(); - let client = AsyncClient::<(), ()>::new(homeserver, Some(session)).unwrap(); + let client = AsyncClient::<()>::new(homeserver, Some(session)).unwrap(); let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); diff --git a/src/event_emitter/mod.rs b/src/event_emitter/mod.rs index 369f73be..4ff8b265 100644 --- a/src/event_emitter/mod.rs +++ b/src/event_emitter/mod.rs @@ -238,7 +238,7 @@ mod test { let vec = Arc::new(Mutex::new(Vec::new())); let test_vec = Arc::clone(&vec); let emitter = Box::new(EvEmitterTest(vec)) as Box<(dyn EventEmitter)>; - let mut client = AsyncClient::<(), ()>::new(homeserver, Some(session)).unwrap(); + let mut client = AsyncClient::<()>::new(homeserver, Some(session)).unwrap(); client.add_event_emitter(emitter).await; let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); diff --git a/src/models/room.rs b/src/models/room.rs index c8d99ebd..b5979e5a 100644 --- a/src/models/room.rs +++ b/src/models/room.rs @@ -439,7 +439,7 @@ mod test { .with_body_from_file("tests/data/sync.json") .create(); - let client = AsyncClient::<(), ()>::new(homeserver, Some(session)).unwrap(); + let client = AsyncClient::<()>::new(homeserver, Some(session)).unwrap(); let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); diff --git a/src/models/room_member.rs b/src/models/room_member.rs index b53d660c..4f9db724 100644 --- a/src/models/room_member.rs +++ b/src/models/room_member.rs @@ -67,7 +67,7 @@ pub struct RoomMember { impl PartialEq for RoomMember { fn eq(&self, other: &RoomMember) -> bool { - // TODO check everything but events and presence_events they don;t impl PartialEq + // TODO check everything but events and presence_events they don't impl PartialEq self.room_id == other.room_id && self.user_id == other.user_id && self.name == other.name diff --git a/src/request_builder.rs b/src/request_builder.rs index d76a5179..15ed1ee1 100644 --- a/src/request_builder.rs +++ b/src/request_builder.rs @@ -341,7 +341,7 @@ mod test { .room_alias_name("room_alias") .topic("room topic") .visibility(Visibility::Private); - let cli = AsyncClient::<(), ()>::new(homeserver, Some(session)).unwrap(); + let cli = AsyncClient::<()>::new(homeserver, Some(session)).unwrap(); assert!(cli.create_room(builder).await.is_ok()); } @@ -373,7 +373,7 @@ mod test { // TODO this makes ruma error `Err(IntoHttp(IntoHttpError(Query(Custom("unsupported value")))))`?? // .filter(RoomEventFilter::default()); - let cli = AsyncClient::<(), ()>::new(homeserver, Some(session)).unwrap(); + let cli = AsyncClient::<()>::new(homeserver, Some(session)).unwrap(); assert!(cli.room_messages(builder).await.is_ok()); } } diff --git a/src/state/mod.rs b/src/state/mod.rs index 087fc76b..18b07e01 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -26,7 +26,7 @@ use crate::events::push_rules::Ruleset; use crate::identifiers::{RoomId, UserId}; use crate::models::Room; use crate::session::Session; - +use crate::Result; #[derive(Debug, Default, PartialEq, Serialize, Deserialize)] pub struct ClientState { /// The current client session containing our user id, device id and access @@ -46,23 +46,20 @@ pub trait StateStore: Send + Sync { /// to serialize and deserialize state to JSON files. type Store; - /// The error type to return. - type IoError; - /// Set up connections or open files to load/save state. - fn open(&self, path: &Path) -> Result<(), Self::IoError>; + fn open(&self, path: &Path) -> Result<()>; /// Loads the state of `BaseClient` through `StateStore::Store` type. - fn load_client_state(&self) -> Result; + fn load_client_state(&self, path: &Path) -> Result; /// Load the state of a single `Room` by `RoomId`. - fn load_room_state(&self, room_id: &RoomId) -> Result; + fn load_room_state(&self, path: &Path, room_id: &RoomId) -> Result; /// Load the state of all `Room`s. /// /// This will be mapped over in the client in order to store `Room`s in an async safe way. - fn load_all_rooms(&self) -> Result, Self::IoError>; + fn load_all_rooms(&self, path: &Path) -> Result>; /// Save the current state of the `BaseClient` using the `StateStore::Store` type. - fn store_client_state(&self, _: Self::Store) -> Result<(), Self::IoError>; + fn store_client_state(&self, path: &Path, _: Self::Store) -> Result<()>; /// Save the state a single `Room`. - fn store_room_state(&self, _: &Room) -> Result<(), Self::IoError>; + fn store_room_state(&self, path: &Path, _: &Room) -> Result<()>; } #[cfg(test)] diff --git a/src/state/state_store.rs b/src/state/state_store.rs index 8e9fda09..b5ef6617 100644 --- a/src/state/state_store.rs +++ b/src/state/state_store.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::fs::{self, OpenOptions}; use std::io::{BufReader, BufWriter, Write}; -use std::path::Path; +use std::path::{Path, PathBuf}; use super::{ClientState, StateStore}; use crate::identifiers::RoomId; @@ -11,7 +11,6 @@ use crate::{Error, Result, Room}; pub struct JsonStore; impl StateStore for JsonStore { - type IoError = Error; type Store = ClientState; fn open(&self, path: &Path) -> Result<()> { @@ -20,98 +19,84 @@ impl StateStore for JsonStore { } Ok(()) } - fn load_client_state(&self) -> Result { - if let Some(mut path) = dirs::home_dir() { - path.push(".matrix_store/client.json"); - let file = OpenOptions::new().read(true).open(path)?; - let reader = BufReader::new(file); - serde_json::from_reader(reader).map_err(Error::from) - } else { - todo!("Error maybe") - } + fn load_client_state(&self, path: &Path) -> Result { + let mut path = path.to_path_buf(); + path.push("client.json"); + + let file = OpenOptions::new().read(true).open(path)?; + let reader = BufReader::new(file); + serde_json::from_reader(reader).map_err(Error::from) } - fn load_room_state(&self, room_id: &RoomId) -> Result { - if let Some(mut path) = dirs::home_dir() { - path.push(&format!(".matrix_store/rooms/{}.json", room_id)); + fn load_room_state(&self, path: &Path, room_id: &RoomId) -> Result { + let mut path = path.to_path_buf(); + path.push(&format!("rooms/{}.json", room_id)); - let file = OpenOptions::new().read(true).open(path)?; - let reader = BufReader::new(file); - serde_json::from_reader(reader).map_err(Error::from) - } else { - todo!("Error maybe") - } + let file = OpenOptions::new().read(true).open(path)?; + let reader = BufReader::new(file); + serde_json::from_reader(reader).map_err(Error::from) } - fn load_all_rooms(&self) -> Result> { - if let Some(mut path) = dirs::home_dir() { - path.push(".matrix_store/rooms/"); + fn load_all_rooms(&self, path: &Path) -> Result> { + let mut path = path.to_path_buf(); + path.push("rooms"); - let mut rooms_map = HashMap::new(); - for file in fs::read_dir(&path)? { - let file = file?.path(); + let mut rooms_map = HashMap::new(); + for file in fs::read_dir(&path)? { + let file = file?.path(); - if file.is_dir() { - continue; - } - - let f_hdl = OpenOptions::new().read(true).open(&file)?; - let reader = BufReader::new(f_hdl); - - let room = serde_json::from_reader::<_, Room>(reader).map_err(Error::from)?; - let room_id = room.room_id.clone(); - - rooms_map.insert(room_id, room); + if file.is_dir() { + continue; } - Ok(rooms_map) - } else { - todo!("Error maybe") + let f_hdl = OpenOptions::new().read(true).open(&file)?; + let reader = BufReader::new(f_hdl); + + let room = serde_json::from_reader::<_, Room>(reader).map_err(Error::from)?; + let room_id = room.room_id.clone(); + + rooms_map.insert(room_id, room); } + + Ok(rooms_map) } - fn store_client_state(&self, state: ClientState) -> Result<()> { - if let Some(mut path) = dirs::home_dir() { - path.push(".matrix_store/client.json"); + fn store_client_state(&self, path: &Path, state: ClientState) -> Result<()> { + let mut path = path.to_path_buf(); + path.push("client.json"); - if !Path::new(&path).exists() { - let mut dir = path.clone(); - dir.pop(); - std::fs::create_dir_all(dir)?; - } - - let json = serde_json::to_string(&state).map_err(Error::from)?; - - let file = OpenOptions::new().write(true).create(true).open(path)?; - let mut writer = BufWriter::new(file); - writer.write_all(json.as_bytes())?; - - Ok(()) - } else { - todo!("Error maybe") + if !Path::new(&path).exists() { + let mut dir = path.clone(); + dir.pop(); + std::fs::create_dir_all(dir)?; } + + let json = serde_json::to_string(&state).map_err(Error::from)?; + + let file = OpenOptions::new().write(true).create(true).open(path)?; + let mut writer = BufWriter::new(file); + writer.write_all(json.as_bytes())?; + + Ok(()) } - fn store_room_state(&self, room: &Room) -> Result<()> { - if let Some(mut path) = dirs::home_dir() { - path.push(&format!(".matrix_store/rooms/{}.json", room.room_id)); + fn store_room_state(&self, path: &Path, room: &Room) -> Result<()> { + let mut path = path.to_path_buf(); + path.push(&format!("rooms/{}.json", room.room_id)); - if !Path::new(&path).exists() { - let mut dir = path.clone(); - dir.pop(); - std::fs::create_dir_all(dir)?; - } - - let json = serde_json::to_string(&room).map_err(Error::from)?; - - let file = OpenOptions::new().write(true).create(true).open(path)?; - let mut writer = BufWriter::new(file); - writer.write_all(json.as_bytes())?; - - Ok(()) - } else { - todo!("Error maybe") + if !Path::new(&path).exists() { + let mut dir = path.clone(); + dir.pop(); + std::fs::create_dir_all(dir)?; } + + let json = serde_json::to_string(&room).map_err(Error::from)?; + + let file = OpenOptions::new().write(true).create(true).open(path)?; + let mut writer = BufWriter::new(file); + writer.write_all(json.as_bytes())?; + + Ok(()) } } @@ -132,15 +117,22 @@ mod test { pub static ref MTX: Mutex<()> = Mutex::new(()); } + lazy_static! { + /// Limit io tests to one thread at a time. + pub static ref PATH: PathBuf = { + let mut path = dirs::home_dir().unwrap(); + path.push(".matrix_store"); + path + }; + } + fn run_and_cleanup(test: fn()) { let _lock = MTX.lock(); test(); - let mut path = dirs::home_dir().unwrap(); - path.push(".matrix_store"); - - if path.exists() { + if PATH.exists() { + let path: &Path = &PATH; fs::remove_dir_all(path).unwrap(); } } @@ -148,8 +140,8 @@ mod test { fn test_store_client_state() { let store = JsonStore; let state = ClientState::default(); - store.store_client_state(state).unwrap(); - let loaded = store.load_client_state().unwrap(); + store.store_client_state(&PATH, state).unwrap(); + let loaded = store.load_client_state(&PATH).unwrap(); assert_eq!(loaded, ClientState::default()); } @@ -165,8 +157,8 @@ mod test { let user = UserId::try_from("@example:example.com").unwrap(); let room = Room::new(&id, &user); - store.store_room_state(&room).unwrap(); - let loaded = store.load_room_state(&id).unwrap(); + store.store_room_state(&PATH, &room).unwrap(); + let loaded = store.load_room_state(&PATH, &id).unwrap(); assert_eq!(loaded, Room::new(&id, &user)); } @@ -182,8 +174,8 @@ mod test { let user = UserId::try_from("@example:example.com").unwrap(); let room = Room::new(&id, &user); - store.store_room_state(&room).unwrap(); - let loaded = store.load_all_rooms().unwrap(); + store.store_room_state(&PATH, &room).unwrap(); + let loaded = store.load_all_rooms(&PATH).unwrap(); println!("{:?}", loaded); } diff --git a/src/test_builder.rs b/src/test_builder.rs index edcff255..b6ef8cbd 100644 --- a/src/test_builder.rs +++ b/src/test_builder.rs @@ -49,9 +49,9 @@ pub struct RoomTestRunner { state_events: Vec, } -pub struct ClientTestRunner { +pub struct ClientTestRunner { /// Used when testing the whole client - client: Option>, + client: Option>, /// RoomId and UserId to use for the events. /// /// The RoomId must match the RoomId of the events to track. @@ -69,9 +69,9 @@ pub struct ClientTestRunner { } #[allow(dead_code)] -pub struct MockTestRunner { +pub struct MockTestRunner { /// Used when testing the whole client - client: Option>, + client: Option>, /// The ephemeral room events that determine the state of a `Room`. ephemeral: Vec, /// The account data events that determine the state of a `Room`. @@ -169,11 +169,11 @@ impl EventBuilder { /// /// The `TestRunner` streams the events to the client and holds methods to make assertions /// about the state of the client. - pub fn build_mock_runner>( + pub fn build_mock_runner>( mut self, method: &str, path: P, - ) -> MockTestRunner { + ) -> MockTestRunner { let body = serde_json::json! { { "device_one_time_keys_count": {}, @@ -238,11 +238,7 @@ impl EventBuilder { /// /// The `TestRunner` streams the events to the `AsyncClient` and holds methods to make assertions /// about the state of the `AsyncClient`. - pub fn build_client_runner( - self, - room_id: RoomId, - user_id: UserId, - ) -> ClientTestRunner { + pub fn build_client_runner(self, room_id: RoomId, user_id: UserId) -> ClientTestRunner { ClientTestRunner { client: None, room_user_id: (room_id, user_id), @@ -317,8 +313,8 @@ impl RoomTestRunner { } } -impl ClientTestRunner { - pub fn set_client(&mut self, client: AsyncClient) -> &mut Self { +impl ClientTestRunner { + pub fn set_client(&mut self, client: AsyncClient) -> &mut Self { self.client = Some(client); self } @@ -359,14 +355,14 @@ impl ClientTestRunner { } } - pub async fn to_client(&mut self) -> &mut AsyncClient { + pub async fn to_client(&mut self) -> &mut AsyncClient { self.stream_client_events().await; self.client.as_mut().unwrap() } } -impl MockTestRunner { - pub fn set_client(&mut self, client: AsyncClient) -> &mut Self { +impl MockTestRunner { + pub fn set_client(&mut self, client: AsyncClient) -> &mut Self { self.client = Some(client); self } @@ -376,7 +372,7 @@ impl MockTestRunner { self } - pub async fn to_client(&mut self) -> Result<&mut AsyncClient, crate::Error> { + pub async fn to_client(&mut self) -> Result<&mut AsyncClient, crate::Error> { self.client .as_mut() .unwrap() diff --git a/tests/async_client_tests.rs b/tests/async_client_tests.rs index fe68f9ae..9e27beda 100644 --- a/tests/async_client_tests.rs +++ b/tests/async_client_tests.rs @@ -17,7 +17,7 @@ async fn login() { .with_body_from_file("tests/data/login_response.json") .create(); - let client = AsyncClient::<(), ()>::new(homeserver, None).unwrap(); + let client = AsyncClient::<()>::new(homeserver, None).unwrap(); client .login("example", "wordpass", None, None) @@ -46,7 +46,7 @@ async fn sync() { .with_body_from_file("tests/data/sync.json") .create(); - let client = AsyncClient::<(), ()>::new(homeserver, Some(session)).unwrap(); + let client = AsyncClient::<()>::new(homeserver, Some(session)).unwrap(); let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); @@ -75,7 +75,7 @@ async fn room_names() { .with_body_from_file("tests/data/sync.json") .create(); - let client = AsyncClient::<(), ()>::new(homeserver, Some(session)).unwrap(); + let client = AsyncClient::<()>::new(homeserver, Some(session)).unwrap(); let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); From ad7a18d50a3044af788abe3f515ec18e1f33cefc Mon Sep 17 00:00:00 2001 From: Devin R Date: Tue, 21 Apr 2020 09:47:36 -0400 Subject: [PATCH 07/18] state_store: remove associated Store type --- examples/command_bot.rs | 4 ++-- examples/login.rs | 3 +-- src/async_client.rs | 14 +++++++------- src/base_client.rs | 10 +++++----- src/event_emitter/mod.rs | 2 +- src/models/room.rs | 2 +- src/request_builder.rs | 4 ++-- src/state/mod.rs | 8 ++------ src/state/state_store.rs | 2 -- src/test_builder.rs | 26 +++++++++++++------------- tests/async_client_tests.rs | 6 +++--- 11 files changed, 37 insertions(+), 44 deletions(-) diff --git a/examples/command_bot.rs b/examples/command_bot.rs index 40aed458..a2d58fb0 100644 --- a/examples/command_bot.rs +++ b/examples/command_bot.rs @@ -15,11 +15,11 @@ struct CommandBot { /// /// The type parameter is for the `StateStore` trait specifying the `Store` /// type for state storage, here we don't care. - client: AsyncClient<()>, + client: AsyncClient, } impl CommandBot { - pub fn new(client: AsyncClient<()>) -> Self { + pub fn new(client: AsyncClient) -> Self { Self { client } } } diff --git a/examples/login.rs b/examples/login.rs index 9fbe4e16..150684c7 100644 --- a/examples/login.rs +++ b/examples/login.rs @@ -45,8 +45,7 @@ async fn login( .proxy("http://localhost:8080")? .disable_ssl_verification(); let homeserver_url = Url::parse(&homeserver_url)?; - let mut client = - AsyncClient::<()>::new_with_config(homeserver_url, None, client_config).unwrap(); + let mut client = AsyncClient::new_with_config(homeserver_url, None, client_config).unwrap(); client.add_event_emitter(Box::new(EventCallback)).await; diff --git a/src/async_client.rs b/src/async_client.rs index 7a0f1455..ffbc0f61 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -55,16 +55,16 @@ const DEFAULT_SYNC_TIMEOUT: Duration = Duration::from_secs(30); /// An async/await enabled Matrix client. /// /// All of the state is held in an `Arc` so the `AsyncClient` can be cloned freely. -pub struct AsyncClient { +pub struct AsyncClient { /// The URL of the homeserver to connect to. homeserver: Url, /// The underlying HTTP client. http_client: reqwest::Client, /// User session data. - pub(crate) base_client: Arc>>, + pub(crate) base_client: Arc>, } -impl std::fmt::Debug for AsyncClient { +impl std::fmt::Debug for AsyncClient { fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> StdResult<(), std::fmt::Error> { write!(fmt, "AsyncClient {{ homeserver: {} }}", self.homeserver) } @@ -197,7 +197,7 @@ use api::r0::room::create_room; use api::r0::session::login; use api::r0::sync::sync_events; -impl AsyncClient { +impl AsyncClient { /// Creates a new client for making HTTP requests to the given homeserver. /// /// # Arguments @@ -1119,7 +1119,7 @@ mod test { device_id: "DEVICEID".to_owned(), }; let homeserver = url::Url::parse(&mockito::server_url()).unwrap(); - let client = AsyncClient::<()>::new(homeserver, Some(session)).unwrap(); + let client = AsyncClient::new(homeserver, Some(session)).unwrap(); let rid = RoomId::try_from("!roomid:room.com").unwrap(); let uid = UserId::try_from("@example:localhost").unwrap(); @@ -1151,7 +1151,7 @@ mod test { }; let homeserver = url::Url::parse(&mockito::server_url()).unwrap(); - let client = AsyncClient::<()>::new(homeserver, Some(session)).unwrap(); + let client = AsyncClient::new(homeserver, Some(session)).unwrap(); let mut bld = EventBuilder::default() .add_room_event_from_file("./tests/data/events/member.json", RoomEvent::RoomMember) @@ -1181,7 +1181,7 @@ mod test { .with_body_from_file("tests/data/login_response_error.json") .create(); - let client = AsyncClient::<()>::new(homeserver, None).unwrap(); + let client = AsyncClient::new(homeserver, None).unwrap(); if let Err(err) = client.login("example", "wordpass", None, None).await { if let crate::Error::RumaResponse(ruma_api::error::FromHttpResponseError::Http( diff --git a/src/base_client.rs b/src/base_client.rs index 5fce01fd..0fd8e3bd 100644 --- a/src/base_client.rs +++ b/src/base_client.rs @@ -61,7 +61,7 @@ pub type Token = String; /// /// This Client is a state machine that receives responses and events and /// accordingly updates it's state. -pub struct Client { +pub struct Client { /// The current client session containing our user id, device id and access /// token. pub session: Option, @@ -77,13 +77,13 @@ pub struct Client { /// events. pub event_emitter: Option>, /// - pub state_store: Option>>, + pub state_store: Option>, #[cfg(feature = "encryption")] olm: Arc>>, } -impl fmt::Debug for Client { +impl fmt::Debug for Client { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Client") .field("session", &self.session) @@ -96,7 +96,7 @@ impl fmt::Debug for Client { } } -impl Client { +impl Client { /// Create a new client. /// /// # Arguments @@ -815,7 +815,7 @@ mod test { .with_body_from_file("tests/data/sync.json") .create(); - let client = AsyncClient::<()>::new(homeserver, Some(session)).unwrap(); + let client = AsyncClient::new(homeserver, Some(session)).unwrap(); let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); diff --git a/src/event_emitter/mod.rs b/src/event_emitter/mod.rs index 4ff8b265..2d675521 100644 --- a/src/event_emitter/mod.rs +++ b/src/event_emitter/mod.rs @@ -238,7 +238,7 @@ mod test { let vec = Arc::new(Mutex::new(Vec::new())); let test_vec = Arc::clone(&vec); let emitter = Box::new(EvEmitterTest(vec)) as Box<(dyn EventEmitter)>; - let mut client = AsyncClient::<()>::new(homeserver, Some(session)).unwrap(); + let mut client = AsyncClient::new(homeserver, Some(session)).unwrap(); client.add_event_emitter(emitter).await; let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); diff --git a/src/models/room.rs b/src/models/room.rs index b5979e5a..0eed93fa 100644 --- a/src/models/room.rs +++ b/src/models/room.rs @@ -439,7 +439,7 @@ mod test { .with_body_from_file("tests/data/sync.json") .create(); - let client = AsyncClient::<()>::new(homeserver, Some(session)).unwrap(); + let client = AsyncClient::new(homeserver, Some(session)).unwrap(); let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); diff --git a/src/request_builder.rs b/src/request_builder.rs index 15ed1ee1..e891f492 100644 --- a/src/request_builder.rs +++ b/src/request_builder.rs @@ -341,7 +341,7 @@ mod test { .room_alias_name("room_alias") .topic("room topic") .visibility(Visibility::Private); - let cli = AsyncClient::<()>::new(homeserver, Some(session)).unwrap(); + let cli = AsyncClient::new(homeserver, Some(session)).unwrap(); assert!(cli.create_room(builder).await.is_ok()); } @@ -373,7 +373,7 @@ mod test { // TODO this makes ruma error `Err(IntoHttp(IntoHttpError(Query(Custom("unsupported value")))))`?? // .filter(RoomEventFilter::default()); - let cli = AsyncClient::<()>::new(homeserver, Some(session)).unwrap(); + let cli = AsyncClient::new(homeserver, Some(session)).unwrap(); assert!(cli.room_messages(builder).await.is_ok()); } } diff --git a/src/state/mod.rs b/src/state/mod.rs index 18b07e01..b9a5c2d0 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -42,14 +42,10 @@ pub struct ClientState { /// Abstraction around the data store to avoid unnecessary request on client initialization. pub trait StateStore: Send + Sync { - /// The type of store to create. The default `JsonStore` uses `ClientState` as the store - /// to serialize and deserialize state to JSON files. - type Store; - /// Set up connections or open files to load/save state. fn open(&self, path: &Path) -> Result<()>; /// Loads the state of `BaseClient` through `StateStore::Store` type. - fn load_client_state(&self, path: &Path) -> Result; + fn load_client_state(&self, path: &Path) -> Result; /// Load the state of a single `Room` by `RoomId`. fn load_room_state(&self, path: &Path, room_id: &RoomId) -> Result; /// Load the state of all `Room`s. @@ -57,7 +53,7 @@ pub trait StateStore: Send + Sync { /// This will be mapped over in the client in order to store `Room`s in an async safe way. fn load_all_rooms(&self, path: &Path) -> Result>; /// Save the current state of the `BaseClient` using the `StateStore::Store` type. - fn store_client_state(&self, path: &Path, _: Self::Store) -> Result<()>; + fn store_client_state(&self, path: &Path, _: ClientState) -> Result<()>; /// Save the state a single `Room`. fn store_room_state(&self, path: &Path, _: &Room) -> Result<()>; } diff --git a/src/state/state_store.rs b/src/state/state_store.rs index b5ef6617..6f57b0d9 100644 --- a/src/state/state_store.rs +++ b/src/state/state_store.rs @@ -11,8 +11,6 @@ use crate::{Error, Result, Room}; pub struct JsonStore; impl StateStore for JsonStore { - type Store = ClientState; - fn open(&self, path: &Path) -> Result<()> { if !path.exists() { std::fs::create_dir_all(path)?; diff --git a/src/test_builder.rs b/src/test_builder.rs index b6ef8cbd..4849cfb1 100644 --- a/src/test_builder.rs +++ b/src/test_builder.rs @@ -49,9 +49,9 @@ pub struct RoomTestRunner { state_events: Vec, } -pub struct ClientTestRunner { +pub struct ClientTestRunner { /// Used when testing the whole client - client: Option>, + client: Option, /// RoomId and UserId to use for the events. /// /// The RoomId must match the RoomId of the events to track. @@ -69,9 +69,9 @@ pub struct ClientTestRunner { } #[allow(dead_code)] -pub struct MockTestRunner { +pub struct MockTestRunner { /// Used when testing the whole client - client: Option>, + client: Option, /// The ephemeral room events that determine the state of a `Room`. ephemeral: Vec, /// The account data events that determine the state of a `Room`. @@ -169,11 +169,11 @@ impl EventBuilder { /// /// The `TestRunner` streams the events to the client and holds methods to make assertions /// about the state of the client. - pub fn build_mock_runner>( + pub fn build_mock_runner>( mut self, method: &str, path: P, - ) -> MockTestRunner { + ) -> MockTestRunner { let body = serde_json::json! { { "device_one_time_keys_count": {}, @@ -238,7 +238,7 @@ impl EventBuilder { /// /// The `TestRunner` streams the events to the `AsyncClient` and holds methods to make assertions /// about the state of the `AsyncClient`. - pub fn build_client_runner(self, room_id: RoomId, user_id: UserId) -> ClientTestRunner { + pub fn build_client_runner(self, room_id: RoomId, user_id: UserId) -> ClientTestRunner { ClientTestRunner { client: None, room_user_id: (room_id, user_id), @@ -313,8 +313,8 @@ impl RoomTestRunner { } } -impl ClientTestRunner { - pub fn set_client(&mut self, client: AsyncClient) -> &mut Self { +impl ClientTestRunner { + pub fn set_client(&mut self, client: AsyncClient) -> &mut Self { self.client = Some(client); self } @@ -355,14 +355,14 @@ impl ClientTestRunner { } } - pub async fn to_client(&mut self) -> &mut AsyncClient { + pub async fn to_client(&mut self) -> &mut AsyncClient { self.stream_client_events().await; self.client.as_mut().unwrap() } } -impl MockTestRunner { - pub fn set_client(&mut self, client: AsyncClient) -> &mut Self { +impl MockTestRunner { + pub fn set_client(&mut self, client: AsyncClient) -> &mut Self { self.client = Some(client); self } @@ -372,7 +372,7 @@ impl MockTestRunner { self } - pub async fn to_client(&mut self) -> Result<&mut AsyncClient, crate::Error> { + pub async fn to_client(&mut self) -> Result<&mut AsyncClient, crate::Error> { self.client .as_mut() .unwrap() diff --git a/tests/async_client_tests.rs b/tests/async_client_tests.rs index 9e27beda..3c9dacdc 100644 --- a/tests/async_client_tests.rs +++ b/tests/async_client_tests.rs @@ -17,7 +17,7 @@ async fn login() { .with_body_from_file("tests/data/login_response.json") .create(); - let client = AsyncClient::<()>::new(homeserver, None).unwrap(); + let client = AsyncClient::new(homeserver, None).unwrap(); client .login("example", "wordpass", None, None) @@ -46,7 +46,7 @@ async fn sync() { .with_body_from_file("tests/data/sync.json") .create(); - let client = AsyncClient::<()>::new(homeserver, Some(session)).unwrap(); + let client = AsyncClient::new(homeserver, Some(session)).unwrap(); let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); @@ -75,7 +75,7 @@ async fn room_names() { .with_body_from_file("tests/data/sync.json") .create(); - let client = AsyncClient::<()>::new(homeserver, Some(session)).unwrap(); + let client = AsyncClient::new(homeserver, Some(session)).unwrap(); let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); From 5491838228ce4a70771adc9eae59c8e1b132587a Mon Sep 17 00:00:00 2001 From: Devin R Date: Wed, 22 Apr 2020 17:39:57 -0400 Subject: [PATCH 08/18] state_store: fix docs, test client with default state store --- design.md | 13 ++-- src/async_client.rs | 113 ++++++++++++++++++++++++++++---- src/base_client.rs | 32 ++++++++- src/request_builder.rs | 4 +- src/state/mod.rs | 33 ++++++++-- src/state/state_store.rs | 136 ++++++++++++++++++++++++++++++--------- src/test_builder.rs | 16 ++--- 7 files changed, 279 insertions(+), 68 deletions(-) diff --git a/design.md b/design.md index 43e9d7d8..1e2f2c26 100644 --- a/design.md +++ b/design.md @@ -12,14 +12,14 @@ The highest level structure that ties the other pieces of functionality together #### Base Client/Client State Machine In addition to Http, the `AsyncClient` passes along methods from the `BaseClient` that deal with `Room`s and `RoomMember`s. This allows the client to keep track of more complicated information that needs to be calculated in some way. - - human readable room names + - human-readable room names - power level? - ignored list? - push rulesset? - more? #### Crypto State Machine -Given a Matrix response the crypto machine will update it's internal state, along with encryption information this means keeping track of when to encrypt. It has knowledge of when encryption needs to happen and can be asked from the `BaseClient`. The crypto state machine is given responses that relate to encryption and can create encrypted request bodies for encryption related requests. Basically it tells the `BaseClient` to send a to-device messages out and the `BaseClient` is responsible for notifying the crypto state machine when it sent the message so crypto can update state. +Given a Matrix response the crypto machine will update it's internal state, along with encryption information this means keeping track of when to encrypt. It knows when encryption needs to happen based on signals from the `BaseClient`. The crypto state machine is given responses that relate to encryption and can create encrypted request bodies for encryption-related requests. Basically it tells the `BaseClient` to send to-device messages out, and the `BaseClient` is responsible for notifying the crypto state machine when it sent the message so crypto can update state. #### Client State/Room and RoomMember The `BaseClient` is responsible for keeping state in sync through the `IncomingResponse`s of `AsyncClient` or querying the `StateStore`. By processing and then delegating incoming `RoomEvent`s, `StateEvent`s, `PresenceEvent`, `IncomingAccountData` and `EphemeralEvent`s to the correct `Room` in the base clients `HashMap` or further to `Room`'s `RoomMember` via the members `HashMap`. The `BaseClient` is also responsible for emitting the incoming events to the `EventEmitter` trait. @@ -87,12 +87,13 @@ pub struct RoomMember { ``` #### State Store -The `BaseClient` also has access to a `dyn StateStore` this is an abstraction around a "database" to keep the client state without requesting a full sync from the server on startup. A default implementation that serializes/deserializes JSON to files in a specified directory can be used. The user can also implement `StateStore` to fit any storage solution they choose. - - load - - store/save +The `BaseClient` also has access to a `dyn StateStore` this is an abstraction around a "database" to keep the client state without requesting a full sync from the server on startup. A default implementation that serializes/deserializes JSON to files in a specified directory can be used. The user can also implement `StateStore` to fit any storage solution they choose. The base client handles the storage automatically. There "may be/are TODO" ways for the user to interact directly. The room event handling methods signal if the state was modified; if so, we check if some room state file needs to be overwritten. + - open + - load client/room or rooms + - store client/room - update ?? -The state store will restore our client state in the `BaseClient` and client authors can just get the latest state that they want to present from the client object. No need to ask the state store for it, this may change if custom setups request this. `StateStore`'s base is load/store and internally to the crate update the `BaseClient`. +The state store will restore our client state in the `BaseClient` and client authors can just get the latest state that they want to present from the client object. No need to ask the state store for it, this may change if custom setups request this. `StateStore`'s main purpose is to provide load/store functionality and, internally to the crate, update the `BaseClient`. #### Event Emitter The consumer of this crate can implement the `EventEmitter` trait for full control over how incoming events are handled by their client. If that isn't enough it is possible to receive every incoming response with the `AsyncClient::sync_forever` callback. diff --git a/src/async_client.rs b/src/async_client.rs index ffbc0f61..5c3b33c9 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -15,6 +15,8 @@ use std::collections::HashMap; use std::convert::{TryFrom, TryInto}; +use std::ops::Deref; +use std::path::{Path, PathBuf}; use std::result::Result as StdResult; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -46,6 +48,7 @@ use crate::api; use crate::base_client::Client as BaseClient; use crate::models::Room; use crate::session::Session; +use crate::state::{ClientState, JsonStore, StateStore}; use crate::VERSION; use crate::{Error, EventEmitter, Result}; @@ -62,6 +65,8 @@ pub struct AsyncClient { http_client: reqwest::Client, /// User session data. pub(crate) base_client: Arc>, + /// The path to the default state store. + state_store_path: Option, } impl std::fmt::Debug for AsyncClient { @@ -70,7 +75,7 @@ impl std::fmt::Debug for AsyncClient { } } -#[derive(Default, Debug)] +#[derive(Default)] /// Configuration for the creation of the `AsyncClient`. /// /// # Example @@ -88,6 +93,19 @@ pub struct AsyncClientConfig { proxy: Option, user_agent: Option, disable_ssl_verification: bool, + store_path: Option, + state_store: Option>, +} + +impl std::fmt::Debug for AsyncClientConfig { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> StdResult<(), std::fmt::Error> { + fmt.debug_struct("AsyncClientConfig") + .field("proxy", &self.proxy) + .field("user_agent", &self.user_agent) + .field("disable_ssl_verification", &self.disable_ssl_verification) + .field("store_path", &self.store_path) + .finish() + } } impl AsyncClientConfig { @@ -129,6 +147,23 @@ impl AsyncClientConfig { self.user_agent = Some(HeaderValue::from_str(user_agent)?); Ok(self) } + + /// Set the path for the default `StateStore`. + /// + /// When the path is set `AsyncClient` will set the state store + /// to `JsonStore`. + pub fn state_store_path>(mut self, path: P) -> Self { + self.store_path = Some(path.as_ref().to_owned()); + self + } + + /// Set a custom implementation of a `StateStore`. + /// + /// The state store should be "connected" before being set. + pub fn state_store(mut self, store: Box) -> Self { + self.state_store = Some(store); + self + } } #[derive(Debug, Default, Clone)] @@ -253,10 +288,20 @@ impl AsyncClient { let http_client = http_client.default_headers(headers).build()?; + let mut base_client = BaseClient::new(session)?; + if let Some(path) = config.store_path.as_ref() { + let store = JsonStore; + store.open(path)?; + base_client.state_store = Some(Box::new(store)); + } else if let Some(store) = config.state_store { + base_client.state_store = Some(store); + }; + Ok(Self { homeserver, http_client, - base_client: Arc::new(RwLock::new(BaseClient::new(session)?)), + base_client: Arc::new(RwLock::new(base_client)), + state_store_path: config.store_path, }) } @@ -337,7 +382,15 @@ impl AsyncClient { let response = self.send(request).await?; let mut client = self.base_client.write().await; - client.receive_login_response(&response).await?; + // TODO avoid allocation somehow? + let path = self.state_store_path.as_ref().map(|p| { + let mut path = PathBuf::from(p); + path.push(response.user_id.to_string()); + path + }); + client + .receive_login_response(&response, path.as_ref()) + .await?; Ok(response) } @@ -485,7 +538,7 @@ impl AsyncClient { /// .name("name") /// .room_version("v1.0"); /// - /// let mut cli = AsyncClient::<(), ()>::new(homeserver, None).unwrap(); + /// let mut cli = AsyncClient::new(homeserver, None).unwrap(); /// # use futures::executor::block_on; /// # block_on(async { /// assert!(cli.create_room(builder).await.is_ok()); @@ -529,7 +582,7 @@ impl AsyncClient { /// .direction(Direction::Backward) /// .limit(UInt::new(10).unwrap()); /// - /// let mut cli = AsyncClient::<(), ()>::new(homeserver, None).unwrap(); + /// let mut cli = AsyncClient::new(homeserver, None).unwrap(); /// # use futures::executor::block_on; /// # block_on(async { /// assert!(cli.room_messages(builder).await.is_ok()); @@ -560,12 +613,15 @@ impl AsyncClient { let mut response = self.send(request).await?; + let mut updated = false; for (room_id, room) in &mut response.rooms.join { let matrix_room = { let mut client = self.base_client.write().await; for event in &room.state.events { if let EventResult::Ok(e) = event { - client.receive_joined_state_event(&room_id, &e).await; + if client.receive_joined_state_event(&room_id, &e).await { + updated = true; + } } } @@ -586,9 +642,14 @@ impl AsyncClient { for mut event in &mut room.timeline.events { let decrypted_event = { let mut client = self.base_client.write().await; - client - .receive_joined_timeline_event(room_id, &mut event) - .await + let mut timeline_update = false; + let decrypt_ev = client + .receive_joined_timeline_event(room_id, &mut event, &mut timeline_update) + .await; + if timeline_update { + updated = true; + }; + decrypt_ev }; if let Some(e) = decrypted_event { @@ -606,7 +667,9 @@ impl AsyncClient { { if let EventResult::Ok(e) = account_data { let mut client = self.base_client.write().await; - client.receive_account_data_event(&room_id, e).await; + if client.receive_account_data_event(&room_id, e).await { + updated = true; + } client.emit_account_data_event(room_id, e).await; } } @@ -619,7 +682,9 @@ impl AsyncClient { { if let EventResult::Ok(e) = presence { let mut client = self.base_client.write().await; - client.receive_presence_event(&room_id, e).await; + if client.receive_presence_event(&room_id, e).await { + updated = true; + } client.emit_presence_event(room_id, e).await; } @@ -630,17 +695,37 @@ impl AsyncClient { { if let EventResult::Ok(e) = ephemeral { let mut client = self.base_client.write().await; - client.receive_ephemeral_event(&room_id, e).await; + if client.receive_ephemeral_event(&room_id, e).await { + updated = true; + } client.emit_ephemeral_event(room_id, e).await; } } } + + if updated { + if let Some(store) = self.base_client.read().await.state_store.as_ref() { + if let Some(path) = self.state_store_path.as_ref() { + store + .store_room_state(&path, matrix_room.read().await.deref()) + .await?; + }; + } + } } let mut client = self.base_client.write().await; client.receive_sync_response(&mut response).await; + if updated { + if let Some(store) = client.state_store.as_ref() { + if let Some(path) = self.state_store_path.as_ref() { + let state = ClientState::from_base_client(&client); + store.store_client_state(&path, state).await?; + }; + } + } Ok(response) } @@ -673,7 +758,7 @@ impl AsyncClient { /// # use futures::executor::block_on; /// # block_on(async { /// # let homeserver = Url::parse("http://localhost:8080").unwrap(); - /// # let mut client = AsyncClient::<(), ()>::new(homeserver, None).unwrap(); + /// # let mut client = AsyncClient::new(homeserver, None).unwrap(); /// /// use async_std::sync::channel; /// @@ -862,7 +947,7 @@ impl AsyncClient { /// use matrix_sdk::events::room::message::{MessageEventContent, TextMessageEventContent}; /// # block_on(async { /// # let homeserver = Url::parse("http://localhost:8080").unwrap(); - /// # let mut client = AsyncClient::<(), ()>::new(homeserver, None).unwrap(); + /// # let mut client = AsyncClient::new(homeserver, None).unwrap(); /// # let room_id = RoomId::try_from("!test:localhost").unwrap(); /// use uuid::Uuid; /// diff --git a/src/base_client.rs b/src/base_client.rs index 0fd8e3bd..92c191f6 100644 --- a/src/base_client.rs +++ b/src/base_client.rs @@ -19,6 +19,7 @@ use std::collections::HashSet; use std::fmt; use std::sync::Arc; +use std::path::PathBuf; #[cfg(feature = "encryption")] use std::result::Result as StdResult; @@ -34,7 +35,7 @@ use crate::events::EventResult; use crate::identifiers::{RoomId, UserId}; use crate::models::Room; use crate::session::Session; -use crate::state::StateStore; +use crate::state::{ClientState, StateStore}; use crate::EventEmitter; #[cfg(feature = "encryption")] @@ -144,6 +145,7 @@ impl Client { pub async fn receive_login_response( &mut self, response: &api::session::login::Response, + store_path: Option<&PathBuf>, ) -> Result<()> { let session = Session { access_token: response.access_token.clone(), @@ -158,6 +160,27 @@ impl Client { *olm = Some(OlmMachine::new(&response.user_id, &response.device_id)?); } + if let Some(path) = store_path { + if let Some(store) = self.state_store.as_ref() { + let ClientState { + session, + sync_token, + ignored_users, + push_ruleset, + } = store.load_client_state(&path).await?; + let mut rooms = store.load_all_rooms(&path).await?; + + self.joined_rooms = rooms + .drain() + .map(|(k, room)| (k, Arc::new(RwLock::new(room)))) + .collect(); + self.session = session; + self.sync_token = sync_token; + self.ignored_users = ignored_users; + self.push_ruleset = push_ruleset; + } + } + Ok(()) } @@ -235,10 +258,14 @@ impl Client { /// * `room_id` - The unique id of the room the event belongs to. /// /// * `event` - The event that should be handled by the client. + /// + /// * `did_update` - This is used internally to confirm when the state has + /// been updated. pub async fn receive_joined_timeline_event( &mut self, room_id: &RoomId, event: &mut EventResult, + did_update: &mut bool, ) -> Option> { match event { EventResult::Ok(e) => { @@ -263,7 +290,8 @@ impl Client { } let mut room = self.get_or_create_room(&room_id).write().await; - room.receive_timeline_event(e); + // Not sure what the best way to do this is ?? + *did_update = room.receive_timeline_event(e); decrypted_event } _ => None, diff --git a/src/request_builder.rs b/src/request_builder.rs index e891f492..795e8534 100644 --- a/src/request_builder.rs +++ b/src/request_builder.rs @@ -29,7 +29,7 @@ use js_int::UInt; /// .visibility(Visibility::Public) /// .name("name") /// .room_version("v1.0"); -/// let mut cli = AsyncClient::<(), ()>::new(homeserver, None).unwrap(); +/// let mut cli = AsyncClient::new(homeserver, None).unwrap(); /// cli.create_room(builder).await; /// # }) /// ``` @@ -186,7 +186,7 @@ impl Into for RoomBuilder { /// # rt.block_on(async { /// # let room_id = RoomId::new(homeserver.as_str()).unwrap(); /// # let last_sync_token = "".to_string();; -/// let mut cli = AsyncClient::<(), ()>::new(homeserver, None).unwrap(); +/// let mut cli = AsyncClient::new(homeserver, None).unwrap(); /// /// let mut builder = MessagesRequestBuilder::new(); /// builder.room_id(room_id) diff --git a/src/state/mod.rs b/src/state/mod.rs index b9a5c2d0..e6513e98 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -21,7 +21,7 @@ pub use state_store::JsonStore; use serde::{Deserialize, Serialize}; -use crate::base_client::Token; +use crate::base_client::{Client as BaseClient, Token}; use crate::events::push_rules::Ruleset; use crate::identifiers::{RoomId, UserId}; use crate::models::Room; @@ -40,22 +40,41 @@ pub struct ClientState { pub push_ruleset: Option, } +impl ClientState { + pub fn from_base_client(client: &BaseClient) -> ClientState { + let BaseClient { + session, + sync_token, + ignored_users, + push_ruleset, + .. + } = client; + Self { + session: session.clone(), + sync_token: sync_token.clone(), + ignored_users: ignored_users.clone(), + push_ruleset: push_ruleset.clone(), + } + } +} + /// Abstraction around the data store to avoid unnecessary request on client initialization. +#[async_trait::async_trait] pub trait StateStore: Send + Sync { - /// Set up connections or open files to load/save state. + /// Set up connections or check files exist to load/save state. fn open(&self, path: &Path) -> Result<()>; /// Loads the state of `BaseClient` through `StateStore::Store` type. - fn load_client_state(&self, path: &Path) -> Result; + async fn load_client_state(&self, path: &Path) -> Result; /// Load the state of a single `Room` by `RoomId`. - fn load_room_state(&self, path: &Path, room_id: &RoomId) -> Result; + async fn load_room_state(&self, path: &Path, room_id: &RoomId) -> Result; /// Load the state of all `Room`s. /// /// This will be mapped over in the client in order to store `Room`s in an async safe way. - fn load_all_rooms(&self, path: &Path) -> Result>; + async fn load_all_rooms(&self, path: &Path) -> Result>; /// Save the current state of the `BaseClient` using the `StateStore::Store` type. - fn store_client_state(&self, path: &Path, _: ClientState) -> Result<()>; + async fn store_client_state(&self, path: &Path, _: ClientState) -> Result<()>; /// Save the state a single `Room`. - fn store_room_state(&self, path: &Path, _: &Room) -> Result<()>; + async fn store_room_state(&self, path: &Path, _: &Room) -> Result<()>; } #[cfg(test)] diff --git a/src/state/state_store.rs b/src/state/state_store.rs index 6f57b0d9..83ea4e63 100644 --- a/src/state/state_store.rs +++ b/src/state/state_store.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::fs::{self, OpenOptions}; use std::io::{BufReader, BufWriter, Write}; -use std::path::{Path, PathBuf}; +use std::path::Path; use super::{ClientState, StateStore}; use crate::identifiers::RoomId; @@ -10,6 +10,7 @@ use crate::{Error, Result, Room}; /// and saves it to disk. pub struct JsonStore; +#[async_trait::async_trait] impl StateStore for JsonStore { fn open(&self, path: &Path) -> Result<()> { if !path.exists() { @@ -17,7 +18,8 @@ impl StateStore for JsonStore { } Ok(()) } - fn load_client_state(&self, path: &Path) -> Result { + + async fn load_client_state(&self, path: &Path) -> Result { let mut path = path.to_path_buf(); path.push("client.json"); @@ -26,7 +28,7 @@ impl StateStore for JsonStore { serde_json::from_reader(reader).map_err(Error::from) } - fn load_room_state(&self, path: &Path, room_id: &RoomId) -> Result { + async fn load_room_state(&self, path: &Path, room_id: &RoomId) -> Result { let mut path = path.to_path_buf(); path.push(&format!("rooms/{}.json", room_id)); @@ -35,7 +37,7 @@ impl StateStore for JsonStore { serde_json::from_reader(reader).map_err(Error::from) } - fn load_all_rooms(&self, path: &Path) -> Result> { + async fn load_all_rooms(&self, path: &Path) -> Result> { let mut path = path.to_path_buf(); path.push("rooms"); @@ -59,7 +61,7 @@ impl StateStore for JsonStore { Ok(rooms_map) } - fn store_client_state(&self, path: &Path, state: ClientState) -> Result<()> { + async fn store_client_state(&self, path: &Path, state: ClientState) -> Result<()> { let mut path = path.to_path_buf(); path.push("client.json"); @@ -71,14 +73,18 @@ impl StateStore for JsonStore { let json = serde_json::to_string(&state).map_err(Error::from)?; - let file = OpenOptions::new().write(true).create(true).open(path)?; + let file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(path)?; let mut writer = BufWriter::new(file); writer.write_all(json.as_bytes())?; Ok(()) } - fn store_room_state(&self, path: &Path, room: &Room) -> Result<()> { + async fn store_room_state(&self, path: &Path, room: &Room) -> Result<()> { let mut path = path.to_path_buf(); path.push(&format!("rooms/{}.json", room.room_id)); @@ -90,7 +96,11 @@ impl StateStore for JsonStore { let json = serde_json::to_string(&room).map_err(Error::from)?; - let file = OpenOptions::new().write(true).create(true).open(path)?; + let file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(path)?; let mut writer = BufWriter::new(file); writer.write_all(json.as_bytes())?; @@ -104,11 +114,15 @@ mod test { use std::convert::TryFrom; use std::fs; + use std::path::PathBuf; + use std::str::FromStr; use std::sync::Mutex; use lazy_static::lazy_static; + use mockito::{mock, Matcher}; use crate::identifiers::{RoomId, UserId}; + use crate::{AsyncClient, AsyncClientConfig, Session, SyncSettings}; lazy_static! { /// Limit io tests to one thread at a time. @@ -124,10 +138,13 @@ mod test { }; } - fn run_and_cleanup(test: fn()) { + async fn run_and_cleanup(test: fn() -> Fut) + where + Fut: std::future::Future, + { let _lock = MTX.lock(); - test(); + test().await; if PATH.exists() { let path: &Path = &PATH; @@ -135,50 +152,111 @@ mod test { } } - fn test_store_client_state() { + async fn test_store_client_state() { let store = JsonStore; let state = ClientState::default(); - store.store_client_state(&PATH, state).unwrap(); - let loaded = store.load_client_state(&PATH).unwrap(); + store.store_client_state(&PATH, state).await.unwrap(); + let loaded = store.load_client_state(&PATH).await.unwrap(); assert_eq!(loaded, ClientState::default()); } - #[test] - fn store_client_state() { - run_and_cleanup(test_store_client_state); + #[tokio::test] + async fn store_client_state() { + run_and_cleanup(test_store_client_state).await; } - fn test_store_room_state() { + async fn test_store_room_state() { let store = JsonStore; let id = RoomId::try_from("!roomid:example.com").unwrap(); let user = UserId::try_from("@example:example.com").unwrap(); let room = Room::new(&id, &user); - store.store_room_state(&PATH, &room).unwrap(); - let loaded = store.load_room_state(&PATH, &id).unwrap(); + store.store_room_state(&PATH, &room).await.unwrap(); + let loaded = store.load_room_state(&PATH, &id).await.unwrap(); assert_eq!(loaded, Room::new(&id, &user)); } - #[test] - fn store_room_state() { - run_and_cleanup(test_store_room_state); + #[tokio::test] + async fn store_room_state() { + run_and_cleanup(test_store_room_state).await; } - fn test_load_rooms() { + async fn test_load_rooms() { let store = JsonStore; let id = RoomId::try_from("!roomid:example.com").unwrap(); let user = UserId::try_from("@example:example.com").unwrap(); let room = Room::new(&id, &user); - store.store_room_state(&PATH, &room).unwrap(); - let loaded = store.load_all_rooms(&PATH).unwrap(); - println!("{:?}", loaded); + store.store_room_state(&PATH, &room).await.unwrap(); + let loaded = store.load_all_rooms(&PATH).await.unwrap(); + assert_eq!(&room, loaded.get(&id).unwrap()); } - #[test] - fn load_rooms() { - run_and_cleanup(test_load_rooms); + #[tokio::test] + async fn load_rooms() { + run_and_cleanup(test_load_rooms).await; + } + + async fn test_client_sync_store() { + let homeserver = url::Url::from_str(&mockito::server_url()).unwrap(); + + let session = Session { + access_token: "1234".to_owned(), + user_id: UserId::try_from("@cheeky_monkey:matrix.org").unwrap(), + device_id: "DEVICEID".to_owned(), + }; + + let _m = mock( + "GET", + Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_string()), + ) + .with_status(200) + .with_body_from_file("tests/data/sync.json") + .create(); + + let _m = mock("POST", "/_matrix/client/r0/login") + .with_status(200) + .with_body_from_file("tests/data/login_response.json") + .create(); + + let mut path = PATH.clone(); + path.push(session.user_id.to_string()); + // a sync response to populate our JSON store with user_id added to path + let config = AsyncClientConfig::default().state_store_path(&path); + let client = + AsyncClient::new_with_config(homeserver.clone(), Some(session.clone()), config) + .unwrap(); + let sync_settings = SyncSettings::new().timeout(std::time::Duration::from_millis(3000)); + let _ = client.sync(sync_settings).await.unwrap(); + + // remove user_id as login will set this + path.pop(); + // once logged in without syncing the client is updated from the state store + let config = AsyncClientConfig::default().state_store_path(&path); + let client = AsyncClient::new_with_config(homeserver, None, config).unwrap(); + client + .login("example", "wordpass", None, None) + .await + .unwrap(); + + let base_client = client.base_client.read().await; + + // assert the synced client and the logged in client are equal + assert_eq!(base_client.session, Some(session)); + assert_eq!( + base_client.sync_token, + Some("s526_47314_0_7_1_1_1_11444_1".to_string()) + ); + assert_eq!( + base_client.ignored_users, + vec![UserId::try_from("@someone:example.org").unwrap()] + ); + } + + #[tokio::test] + async fn client_sync_store() { + run_and_cleanup(test_client_sync_store).await; } } diff --git a/src/test_builder.rs b/src/test_builder.rs index 4849cfb1..6b1c8b6c 100644 --- a/src/test_builder.rs +++ b/src/test_builder.rs @@ -15,7 +15,7 @@ use crate::events::{ use crate::identifiers::{RoomId, UserId}; use crate::AsyncClient; -use mockito::{self, mock, Mock}; +use mockito::{self, mock, Matcher, Mock}; use crate::models::Room; @@ -169,11 +169,7 @@ impl EventBuilder { /// /// The `TestRunner` streams the events to the client and holds methods to make assertions /// about the state of the client. - pub fn build_mock_runner>( - mut self, - method: &str, - path: P, - ) -> MockTestRunner { + pub fn build_mock_runner>(mut self, method: &str, path: P) -> MockTestRunner { let body = serde_json::json! { { "device_one_time_keys_count": {}, @@ -344,8 +340,12 @@ impl ClientTestRunner { } for event in &self.room_events { - cli.receive_joined_timeline_event(room_id, &mut EventResult::Ok(event.clone())) - .await; + cli.receive_joined_timeline_event( + room_id, + &mut EventResult::Ok(event.clone()), + &mut false, + ) + .await; } for event in &self.presence_events { cli.receive_presence_event(room_id, event).await; From fd7d3db32b5bb8c292744b7e00d1e5f1b0c9bfc5 Mon Sep 17 00:00:00 2001 From: Devin R Date: Thu, 23 Apr 2020 16:40:23 -0400 Subject: [PATCH 09/18] state_store: move path into JsonStore, store must be "connected" before adding to AsyncClientConfig --- examples/command_bot.rs | 3 -- src/async_client.rs | 49 +++++------------------- src/base_client.rs | 36 ++++++++---------- src/state/mod.rs | 13 +++---- src/state/state_store.rs | 82 +++++++++++++++++++++++----------------- 5 files changed, 78 insertions(+), 105 deletions(-) diff --git a/examples/command_bot.rs b/examples/command_bot.rs index a2d58fb0..220b0ea1 100644 --- a/examples/command_bot.rs +++ b/examples/command_bot.rs @@ -12,9 +12,6 @@ use url::Url; struct CommandBot { /// This clone of the `AsyncClient` will send requests to the server, /// while the other keeps us in sync with the server using `sync_forever`. - /// - /// The type parameter is for the `StateStore` trait specifying the `Store` - /// type for state storage, here we don't care. client: AsyncClient, } diff --git a/src/async_client.rs b/src/async_client.rs index 5c3b33c9..aae5f5d8 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -16,7 +16,6 @@ use std::collections::HashMap; use std::convert::{TryFrom, TryInto}; use std::ops::Deref; -use std::path::{Path, PathBuf}; use std::result::Result as StdResult; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -48,7 +47,7 @@ use crate::api; use crate::base_client::Client as BaseClient; use crate::models::Room; use crate::session::Session; -use crate::state::{ClientState, JsonStore, StateStore}; +use crate::state::{ClientState, StateStore}; use crate::VERSION; use crate::{Error, EventEmitter, Result}; @@ -65,8 +64,6 @@ pub struct AsyncClient { http_client: reqwest::Client, /// User session data. pub(crate) base_client: Arc>, - /// The path to the default state store. - state_store_path: Option, } impl std::fmt::Debug for AsyncClient { @@ -93,7 +90,6 @@ pub struct AsyncClientConfig { proxy: Option, user_agent: Option, disable_ssl_verification: bool, - store_path: Option, state_store: Option>, } @@ -103,7 +99,6 @@ impl std::fmt::Debug for AsyncClientConfig { .field("proxy", &self.proxy) .field("user_agent", &self.user_agent) .field("disable_ssl_verification", &self.disable_ssl_verification) - .field("store_path", &self.store_path) .finish() } } @@ -148,15 +143,6 @@ impl AsyncClientConfig { Ok(self) } - /// Set the path for the default `StateStore`. - /// - /// When the path is set `AsyncClient` will set the state store - /// to `JsonStore`. - pub fn state_store_path>(mut self, path: P) -> Self { - self.store_path = Some(path.as_ref().to_owned()); - self - } - /// Set a custom implementation of a `StateStore`. /// /// The state store should be "connected" before being set. @@ -289,11 +275,8 @@ impl AsyncClient { let http_client = http_client.default_headers(headers).build()?; let mut base_client = BaseClient::new(session)?; - if let Some(path) = config.store_path.as_ref() { - let store = JsonStore; - store.open(path)?; - base_client.state_store = Some(Box::new(store)); - } else if let Some(store) = config.state_store { + + if let Some(store) = config.state_store { base_client.state_store = Some(store); }; @@ -301,7 +284,6 @@ impl AsyncClient { homeserver, http_client, base_client: Arc::new(RwLock::new(base_client)), - state_store_path: config.store_path, }) } @@ -382,15 +364,8 @@ impl AsyncClient { let response = self.send(request).await?; let mut client = self.base_client.write().await; - // TODO avoid allocation somehow? - let path = self.state_store_path.as_ref().map(|p| { - let mut path = PathBuf::from(p); - path.push(response.user_id.to_string()); - path - }); - client - .receive_login_response(&response, path.as_ref()) - .await?; + + client.receive_login_response(&response).await?; Ok(response) } @@ -706,11 +681,9 @@ impl AsyncClient { if updated { if let Some(store) = self.base_client.read().await.state_store.as_ref() { - if let Some(path) = self.state_store_path.as_ref() { - store - .store_room_state(&path, matrix_room.read().await.deref()) - .await?; - }; + store + .store_room_state(matrix_room.read().await.deref()) + .await?; } } } @@ -720,10 +693,8 @@ impl AsyncClient { if updated { if let Some(store) = client.state_store.as_ref() { - if let Some(path) = self.state_store_path.as_ref() { - let state = ClientState::from_base_client(&client); - store.store_client_state(&path, state).await?; - }; + let state = ClientState::from_base_client(&client); + store.store_client_state(state).await?; } } Ok(response) diff --git a/src/base_client.rs b/src/base_client.rs index 92c191f6..f1d04759 100644 --- a/src/base_client.rs +++ b/src/base_client.rs @@ -19,7 +19,6 @@ use std::collections::HashSet; use std::fmt; use std::sync::Arc; -use std::path::PathBuf; #[cfg(feature = "encryption")] use std::result::Result as StdResult; @@ -145,7 +144,6 @@ impl Client { pub async fn receive_login_response( &mut self, response: &api::session::login::Response, - store_path: Option<&PathBuf>, ) -> Result<()> { let session = Session { access_token: response.access_token.clone(), @@ -160,25 +158,23 @@ impl Client { *olm = Some(OlmMachine::new(&response.user_id, &response.device_id)?); } - if let Some(path) = store_path { - if let Some(store) = self.state_store.as_ref() { - let ClientState { - session, - sync_token, - ignored_users, - push_ruleset, - } = store.load_client_state(&path).await?; - let mut rooms = store.load_all_rooms(&path).await?; + if let Some(store) = self.state_store.as_ref() { + let ClientState { + session, + sync_token, + ignored_users, + push_ruleset, + } = store.load_client_state().await?; + let mut rooms = store.load_all_rooms().await?; - self.joined_rooms = rooms - .drain() - .map(|(k, room)| (k, Arc::new(RwLock::new(room)))) - .collect(); - self.session = session; - self.sync_token = sync_token; - self.ignored_users = ignored_users; - self.push_ruleset = push_ruleset; - } + self.joined_rooms = rooms + .drain() + .map(|(k, room)| (k, Arc::new(RwLock::new(room)))) + .collect(); + self.session = session; + self.sync_token = sync_token; + self.ignored_users = ignored_users; + self.push_ruleset = push_ruleset; } Ok(()) diff --git a/src/state/mod.rs b/src/state/mod.rs index e6513e98..f643570e 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -14,7 +14,6 @@ // limitations under the License. use std::collections::HashMap; -use std::path::Path; pub mod state_store; pub use state_store::JsonStore; @@ -61,20 +60,18 @@ impl ClientState { /// Abstraction around the data store to avoid unnecessary request on client initialization. #[async_trait::async_trait] pub trait StateStore: Send + Sync { - /// Set up connections or check files exist to load/save state. - fn open(&self, path: &Path) -> Result<()>; /// Loads the state of `BaseClient` through `StateStore::Store` type. - async fn load_client_state(&self, path: &Path) -> Result; + async fn load_client_state(&self) -> Result; /// Load the state of a single `Room` by `RoomId`. - async fn load_room_state(&self, path: &Path, room_id: &RoomId) -> Result; + async fn load_room_state(&self, room_id: &RoomId) -> Result; /// Load the state of all `Room`s. /// /// This will be mapped over in the client in order to store `Room`s in an async safe way. - async fn load_all_rooms(&self, path: &Path) -> Result>; + async fn load_all_rooms(&self) -> Result>; /// Save the current state of the `BaseClient` using the `StateStore::Store` type. - async fn store_client_state(&self, path: &Path, _: ClientState) -> Result<()>; + async fn store_client_state(&self, _: ClientState) -> Result<()>; /// Save the state a single `Room`. - async fn store_room_state(&self, path: &Path, _: &Room) -> Result<()>; + async fn store_room_state(&self, _: &Room) -> Result<()>; } #[cfg(test)] diff --git a/src/state/state_store.rs b/src/state/state_store.rs index 83ea4e63..98d2f7c3 100644 --- a/src/state/state_store.rs +++ b/src/state/state_store.rs @@ -1,26 +1,36 @@ use std::collections::HashMap; use std::fs::{self, OpenOptions}; use std::io::{BufReader, BufWriter, Write}; -use std::path::Path; +use std::path::{Path, PathBuf}; use super::{ClientState, StateStore}; use crate::identifiers::RoomId; use crate::{Error, Result, Room}; /// A default `StateStore` implementation that serializes state as json /// and saves it to disk. -pub struct JsonStore; +pub struct JsonStore { + path: PathBuf, +} + +impl JsonStore { + /// Create a `JsonStore` to store the client and room state. + /// + /// Checks if the provided path exists and creates the directories if not. + pub fn open>(path: P) -> Result { + let p = path.as_ref(); + if !p.exists() { + std::fs::create_dir_all(p)?; + } + Ok(Self { + path: p.to_path_buf(), + }) + } +} #[async_trait::async_trait] impl StateStore for JsonStore { - fn open(&self, path: &Path) -> Result<()> { - if !path.exists() { - std::fs::create_dir_all(path)?; - } - Ok(()) - } - - async fn load_client_state(&self, path: &Path) -> Result { - let mut path = path.to_path_buf(); + async fn load_client_state(&self) -> Result { + let mut path = self.path.clone(); path.push("client.json"); let file = OpenOptions::new().read(true).open(path)?; @@ -28,8 +38,8 @@ impl StateStore for JsonStore { serde_json::from_reader(reader).map_err(Error::from) } - async fn load_room_state(&self, path: &Path, room_id: &RoomId) -> Result { - let mut path = path.to_path_buf(); + async fn load_room_state(&self, room_id: &RoomId) -> Result { + let mut path = self.path.clone(); path.push(&format!("rooms/{}.json", room_id)); let file = OpenOptions::new().read(true).open(path)?; @@ -37,8 +47,8 @@ impl StateStore for JsonStore { serde_json::from_reader(reader).map_err(Error::from) } - async fn load_all_rooms(&self, path: &Path) -> Result> { - let mut path = path.to_path_buf(); + async fn load_all_rooms(&self) -> Result> { + let mut path = self.path.clone(); path.push("rooms"); let mut rooms_map = HashMap::new(); @@ -61,8 +71,8 @@ impl StateStore for JsonStore { Ok(rooms_map) } - async fn store_client_state(&self, path: &Path, state: ClientState) -> Result<()> { - let mut path = path.to_path_buf(); + async fn store_client_state(&self, state: ClientState) -> Result<()> { + let mut path = self.path.clone(); path.push("client.json"); if !Path::new(&path).exists() { @@ -84,8 +94,8 @@ impl StateStore for JsonStore { Ok(()) } - async fn store_room_state(&self, path: &Path, room: &Room) -> Result<()> { - let mut path = path.to_path_buf(); + async fn store_room_state(&self, room: &Room) -> Result<()> { + let mut path = self.path.clone(); path.push(&format!("rooms/{}.json", room.room_id)); if !Path::new(&path).exists() { @@ -153,10 +163,11 @@ mod test { } async fn test_store_client_state() { - let store = JsonStore; + let path: &Path = &PATH; + let store = JsonStore::open(path).unwrap(); let state = ClientState::default(); - store.store_client_state(&PATH, state).await.unwrap(); - let loaded = store.load_client_state(&PATH).await.unwrap(); + store.store_client_state(state).await.unwrap(); + let loaded = store.load_client_state().await.unwrap(); assert_eq!(loaded, ClientState::default()); } @@ -166,14 +177,15 @@ mod test { } async fn test_store_room_state() { - let store = JsonStore; + let path: &Path = &PATH; + let store = JsonStore::open(path).unwrap(); let id = RoomId::try_from("!roomid:example.com").unwrap(); let user = UserId::try_from("@example:example.com").unwrap(); let room = Room::new(&id, &user); - store.store_room_state(&PATH, &room).await.unwrap(); - let loaded = store.load_room_state(&PATH, &id).await.unwrap(); + store.store_room_state(&room).await.unwrap(); + let loaded = store.load_room_state(&id).await.unwrap(); assert_eq!(loaded, Room::new(&id, &user)); } @@ -183,14 +195,15 @@ mod test { } async fn test_load_rooms() { - let store = JsonStore; + let path: &Path = &PATH; + let store = JsonStore::open(path).unwrap(); let id = RoomId::try_from("!roomid:example.com").unwrap(); let user = UserId::try_from("@example:example.com").unwrap(); let room = Room::new(&id, &user); - store.store_room_state(&PATH, &room).await.unwrap(); - let loaded = store.load_all_rooms(&PATH).await.unwrap(); + store.store_room_state(&room).await.unwrap(); + let loaded = store.load_all_rooms().await.unwrap(); assert_eq!(&room, loaded.get(&id).unwrap()); } @@ -221,20 +234,19 @@ mod test { .with_body_from_file("tests/data/login_response.json") .create(); - let mut path = PATH.clone(); - path.push(session.user_id.to_string()); - // a sync response to populate our JSON store with user_id added to path - let config = AsyncClientConfig::default().state_store_path(&path); + let path: &Path = &PATH; + // a sync response to populate our JSON store + let config = + AsyncClientConfig::default().state_store(Box::new(JsonStore::open(path).unwrap())); let client = AsyncClient::new_with_config(homeserver.clone(), Some(session.clone()), config) .unwrap(); let sync_settings = SyncSettings::new().timeout(std::time::Duration::from_millis(3000)); let _ = client.sync(sync_settings).await.unwrap(); - // remove user_id as login will set this - path.pop(); // once logged in without syncing the client is updated from the state store - let config = AsyncClientConfig::default().state_store_path(&path); + let config = + AsyncClientConfig::default().state_store(Box::new(JsonStore::open(path).unwrap())); let client = AsyncClient::new_with_config(homeserver, None, config).unwrap(); client .login("example", "wordpass", None, None) From 030aa7975043b2915f1989316c1ee6349dc2754a Mon Sep 17 00:00:00 2001 From: Devin R Date: Thu, 23 Apr 2020 19:37:27 -0400 Subject: [PATCH 10/18] state_store: add method to manually sync state store, `AsyncClient::sync` now initially syncs with state store --- src/async_client.rs | 45 ++++++++++++++++++++++++++++++-- src/base_client.rs | 55 ++++++++++++++++++++++++++-------------- src/state/state_store.rs | 16 +++++++----- 3 files changed, 88 insertions(+), 28 deletions(-) diff --git a/src/async_client.rs b/src/async_client.rs index aae5f5d8..9321bfcc 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -16,6 +16,7 @@ use std::collections::HashMap; use std::convert::{TryFrom, TryInto}; use std::ops::Deref; +use std::path::Path; use std::result::Result as StdResult; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -75,6 +76,9 @@ impl std::fmt::Debug for AsyncClient { #[derive(Default)] /// Configuration for the creation of the `AsyncClient`. /// +/// When setting the `StateStore` it is up to the user to open/connect +/// the storage backend before client creation. +/// /// # Example /// /// ``` @@ -86,6 +90,14 @@ impl std::fmt::Debug for AsyncClient { /// .unwrap() /// .disable_ssl_verification(); /// ``` +/// add the default `JsonStore` to the `AsyncClient` +/// ```no_run +/// # use matrix_sdk::{AsyncClientConfig, JsonStore}; +/// +/// let store = JsonStore::open("path/to/json").unwrap(); +/// let client_config = AsyncClientConfig::new() +/// . state_store(Box::new(store)); +/// ``` pub struct AsyncClientConfig { proxy: Option, user_agent: Option, @@ -145,7 +157,7 @@ impl AsyncClientConfig { /// Set a custom implementation of a `StateStore`. /// - /// The state store should be "connected" before being set. + /// The state store should be "connected/opened" before being set. pub fn state_store(mut self, store: Box) -> Self { self.state_store = Some(store); self @@ -331,6 +343,18 @@ impl AsyncClient { self.base_client.read().await.joined_rooms.clone() } + /// This allows `AsyncClient` to manually sync state with the provided `StateStore`. + /// + /// Returns true when a successful `StateStore` sync has completed. + /// # Examples + /// + /// ``` + /// // TODO + /// ``` + pub async fn sync_with_state_store(&self) -> Result { + self.base_client.write().await.sync_with_state_store().await + } + /// Login to the server. /// /// # Arguments @@ -573,11 +597,28 @@ impl AsyncClient { /// Synchronize the client's state with the latest state on the server. /// + /// If a `StateStore` is provided and this is the initial sync state will + /// be loaded from the state store. + /// /// # Arguments /// /// * `sync_settings` - Settings for the sync call. #[instrument] - pub async fn sync(&self, sync_settings: SyncSettings) -> Result { + pub async fn sync( + &self, + mut sync_settings: SyncSettings, + ) -> Result { + { + if self.base_client.read().await.is_state_store_synced() { + if let Ok(synced) = self.sync_with_state_store().await { + if synced { + // once synced, update the sync token to the last known state from `StateStore`. + sync_settings.token = self.sync_token().await; + } + } + } + } + let request = sync_events::Request { filter: None, since: sync_settings.token, diff --git a/src/base_client.rs b/src/base_client.rs index f1d04759..b10a5601 100644 --- a/src/base_client.rs +++ b/src/base_client.rs @@ -78,6 +78,8 @@ pub struct Client { pub event_emitter: Option>, /// pub state_store: Option>, + /// Does the `Client` need to sync with the state store. + needs_state_store_sync: bool, #[cfg(feature = "encryption")] olm: Arc>>, @@ -118,6 +120,7 @@ impl Client { push_ruleset: None, event_emitter: None, state_store: None, + needs_state_store_sync: true, #[cfg(feature = "encryption")] olm: Arc::new(Mutex::new(olm)), }) @@ -135,6 +138,39 @@ impl Client { self.event_emitter = 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 + } + + /// When a client is provided the state store will load state from the `StateStore`. + /// + /// Returns `true` when a sync has successfully completed. + pub(crate) async fn sync_with_state_store(&mut self) -> Result { + if let Some(store) = self.state_store.as_ref() { + let ClientState { + session, + sync_token, + ignored_users, + push_ruleset, + } = store.load_client_state().await?; + let mut rooms = store.load_all_rooms().await?; + + self.joined_rooms = rooms + .drain() + .map(|(k, room)| (k, Arc::new(RwLock::new(room)))) + .collect(); + self.session = session; + self.sync_token = sync_token; + self.ignored_users = ignored_users; + self.push_ruleset = push_ruleset; + + self.needs_state_store_sync = false; + } + + Ok(!self.needs_state_store_sync) + } + /// Receive a login response and update the session of the client. /// /// # Arguments @@ -158,25 +194,6 @@ impl Client { *olm = Some(OlmMachine::new(&response.user_id, &response.device_id)?); } - if let Some(store) = self.state_store.as_ref() { - let ClientState { - session, - sync_token, - ignored_users, - push_ruleset, - } = store.load_client_state().await?; - let mut rooms = store.load_all_rooms().await?; - - self.joined_rooms = rooms - .drain() - .map(|(k, room)| (k, Arc::new(RwLock::new(room)))) - .collect(); - self.session = session; - self.sync_token = sync_token; - self.ignored_users = ignored_users; - self.push_ruleset = push_ruleset; - } - Ok(()) } diff --git a/src/state/state_store.rs b/src/state/state_store.rs index 98d2f7c3..964c4e84 100644 --- a/src/state/state_store.rs +++ b/src/state/state_store.rs @@ -242,16 +242,18 @@ mod test { AsyncClient::new_with_config(homeserver.clone(), Some(session.clone()), config) .unwrap(); let sync_settings = SyncSettings::new().timeout(std::time::Duration::from_millis(3000)); - let _ = client.sync(sync_settings).await.unwrap(); + // fake a sync to skip the load with the state store, this will fail as the files won't exist + // but the `AsyncClient::sync` will skip `StateStore::load_*` + assert!(client.sync_with_state_store().await.is_err()); + // gather state to save to the db + let _ = client.sync(sync_settings.clone()).await.unwrap(); - // once logged in without syncing the client is updated from the state store + // now syncing the client will update from the state store let config = AsyncClientConfig::default().state_store(Box::new(JsonStore::open(path).unwrap())); - let client = AsyncClient::new_with_config(homeserver, None, config).unwrap(); - client - .login("example", "wordpass", None, None) - .await - .unwrap(); + let client = + AsyncClient::new_with_config(homeserver, Some(session.clone()), config).unwrap(); + client.sync(sync_settings).await.unwrap(); let base_client = client.base_client.read().await; From 316295bb774d03d21caa6cdb916ed5f8c7cd0beb Mon Sep 17 00:00:00 2001 From: Devin R Date: Thu, 23 Apr 2020 21:57:54 -0400 Subject: [PATCH 11/18] state_store: add initial_use to let client know its safe to load state --- src/async_client.rs | 4 +++- src/base_client.rs | 5 +++++ src/state/mod.rs | 4 ++++ src/state/state_store.rs | 12 ++++++++---- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/async_client.rs b/src/async_client.rs index 66251e17..280768f6 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -610,7 +610,9 @@ impl AsyncClient { #[instrument] pub async fn sync(&self, mut sync_settings: SyncSettings) -> Result { { - if self.base_client.read().await.is_state_store_synced() { + // if the client hasn't been synced from the state store don't sync again + if !self.base_client.read().await.is_state_store_synced() { + // this will bail out returning false if the store has not been set up if let Ok(synced) = self.sync_with_state_store().await { if synced { // once synced, update the sync token to the last known state from `StateStore`. diff --git a/src/base_client.rs b/src/base_client.rs index 3f150cec..0b9d5f88 100644 --- a/src/base_client.rs +++ b/src/base_client.rs @@ -148,6 +148,11 @@ impl Client { /// Returns `true` when a sync has successfully completed. pub(crate) async fn sync_with_state_store(&mut self) -> Result { if let Some(store) = self.state_store.as_ref() { + // return false and continues with a sync request then saves the state and creates + // and populates the files during the sync + if !store.initial_use().await? { + return Ok(false); + } let ClientState { session, sync_token, diff --git a/src/state/mod.rs b/src/state/mod.rs index f643570e..19b1460b 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -60,6 +60,10 @@ impl ClientState { /// Abstraction around the data store to avoid unnecessary request on client initialization. #[async_trait::async_trait] pub trait StateStore: Send + Sync { + /// Signals to the `AsyncClient` if this is the first time a StateStore` has been used. + /// + /// Returns true if `StateStore` has been set up and ready to be loaded from. + async fn initial_use(&self) -> Result; /// Loads the state of `BaseClient` through `StateStore::Store` type. async fn load_client_state(&self) -> Result; /// Load the state of a single `Room` by `RoomId`. diff --git a/src/state/state_store.rs b/src/state/state_store.rs index 964c4e84..99f84797 100644 --- a/src/state/state_store.rs +++ b/src/state/state_store.rs @@ -29,6 +29,12 @@ impl JsonStore { #[async_trait::async_trait] impl StateStore for JsonStore { + async fn initial_use(&self) -> Result { + let mut path = self.path.clone(); + path.push("client.json"); + Ok(fs::read_to_string(path).map_or(false, |s| !s.is_empty())) + } + async fn load_client_state(&self) -> Result { let mut path = self.path.clone(); path.push("client.json"); @@ -242,10 +248,8 @@ mod test { AsyncClient::new_with_config(homeserver.clone(), Some(session.clone()), config) .unwrap(); let sync_settings = SyncSettings::new().timeout(std::time::Duration::from_millis(3000)); - // fake a sync to skip the load with the state store, this will fail as the files won't exist - // but the `AsyncClient::sync` will skip `StateStore::load_*` - assert!(client.sync_with_state_store().await.is_err()); - // gather state to save to the db + + // gather state to save to the db, the first time through loading will be skipped let _ = client.sync(sync_settings.clone()).await.unwrap(); // now syncing the client will update from the state store From 23755b10ab79b014949ae1bd996381ae274f70be Mon Sep 17 00:00:00 2001 From: Devin R Date: Fri, 24 Apr 2020 19:57:51 -0400 Subject: [PATCH 12/18] state_store: add user_id localpart to state store path from session --- Cargo.toml | 2 +- src/async_client.rs | 5 +-- src/base_client.rs | 38 +++++++++++------- src/request_builder.rs | 11 +++-- src/state/mod.rs | 36 ++++++++--------- src/state/state_store.rs | 86 ++++++++++++++++++++++++++-------------- 6 files changed, 108 insertions(+), 70 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b5322703..ceaa9d53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,7 @@ features = ["std", "std-future"] [dependencies.tokio] version = "0.2.16" default-features = false -features = ["sync", "time"] +features = ["sync", "time", "fs"] [dependencies.sqlx] version = "0.3.3" diff --git a/src/async_client.rs b/src/async_client.rs index 280768f6..36c8dfe3 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -18,7 +18,6 @@ use std::collections::BTreeMap; use std::collections::HashMap; use std::convert::{TryFrom, TryInto}; use std::ops::Deref; -use std::path::Path; use std::result::Result as StdResult; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -159,7 +158,7 @@ impl AsyncClientConfig { /// Set a custom implementation of a `StateStore`. /// - /// The state store should be "connected/opened" before being set. + /// The state store should be opened before being set. pub fn state_store(mut self, store: Box) -> Self { self.state_store = Some(store); self @@ -610,7 +609,7 @@ impl AsyncClient { #[instrument] pub async fn sync(&self, mut sync_settings: SyncSettings) -> Result { { - // if the client hasn't been synced from the state store don't sync again + // if the client has been synced from the state store don't sync again if !self.base_client.read().await.is_state_store_synced() { // this will bail out returning false if the store has not been set up if let Ok(synced) = self.sync_with_state_store().await { diff --git a/src/base_client.rs b/src/base_client.rs index 0b9d5f88..7f618894 100644 --- a/src/base_client.rs +++ b/src/base_client.rs @@ -148,27 +148,37 @@ impl Client { /// Returns `true` when a sync has successfully completed. pub(crate) async fn sync_with_state_store(&mut self) -> Result { if let Some(store) = self.state_store.as_ref() { - // return false and continues with a sync request then saves the state and creates - // and populates the files during the sync - if !store.initial_use().await? { + if let Some(client_state) = store.load_client_state().await? { + let ClientState { + user_id, + device_id, + sync_token, + ignored_users, + push_ruleset, + } = client_state; + + if let Some(sess) = self.session.as_mut() { + if let Some(device) = device_id { + sess.device_id = device; + } + if let Some(user) = user_id { + sess.user_id = user; + } + } + self.sync_token = sync_token; + self.ignored_users = ignored_users; + self.push_ruleset = 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 ClientState { - session, - sync_token, - ignored_users, - push_ruleset, - } = store.load_client_state().await?; - let mut rooms = store.load_all_rooms().await?; + let mut rooms = store.load_all_rooms().await?; self.joined_rooms = rooms .drain() .map(|(k, room)| (k, Arc::new(RwLock::new(room)))) .collect(); - self.session = session; - self.sync_token = sync_token; - self.ignored_users = ignored_users; - self.push_ruleset = push_ruleset; self.needs_state_store_sync = false; } diff --git a/src/request_builder.rs b/src/request_builder.rs index f9d58d14..a9893321 100644 --- a/src/request_builder.rs +++ b/src/request_builder.rs @@ -295,6 +295,7 @@ mod test { use super::*; use crate::events::room::power_levels::NotificationPowerLevels; use crate::{identifiers::RoomId, AsyncClient, Session}; + use api::r0::filter::{LazyLoadOptions, RoomEventFilter}; use js_int::Int; use mockito::{mock, Matcher}; @@ -371,9 +372,13 @@ mod test { .from("t47429-4392820_219380_26003_2265".to_string()) .to("t4357353_219380_26003_2265".to_string()) .direction(Direction::Backward) - .limit(UInt::new(10).unwrap()); - // TODO this makes ruma error `Err(IntoHttp(IntoHttpError(Query(Custom("unsupported value")))))`?? - // .filter(RoomEventFilter::default()); + .limit(UInt::new(10).unwrap()) + .filter(RoomEventFilter { + lazy_load_options: LazyLoadOptions::Enabled { + include_redundant_members: false, + }, + ..Default::default() + }); let cli = AsyncClient::new(homeserver, Some(session)).unwrap(); assert!(cli.room_messages(builder).await.is_ok()); diff --git a/src/state/mod.rs b/src/state/mod.rs index 19b1460b..c58d4d82 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -22,15 +22,15 @@ use serde::{Deserialize, Serialize}; use crate::base_client::{Client as BaseClient, Token}; use crate::events::push_rules::Ruleset; -use crate::identifiers::{RoomId, UserId}; +use crate::identifiers::{DeviceId, RoomId, UserId}; use crate::models::Room; -use crate::session::Session; use crate::Result; -#[derive(Debug, Default, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct ClientState { - /// The current client session containing our user id, device id and access - /// token. - pub session: Option, + /// The `UserId` for the current logged in user. + pub user_id: Option, + /// The `DeviceId` of the current logged in user. + pub device_id: Option, /// The current sync token that should be used for the next sync call. pub sync_token: Option, /// A list of ignored users. @@ -49,7 +49,8 @@ impl ClientState { .. } = client; Self { - session: session.clone(), + user_id: session.as_ref().map(|s| s.user_id.clone()), + device_id: session.as_ref().map(|s| s.device_id.clone()), sync_token: sync_token.clone(), ignored_users: ignored_users.clone(), push_ruleset: push_ruleset.clone(), @@ -60,14 +61,11 @@ impl ClientState { /// Abstraction around the data store to avoid unnecessary request on client initialization. #[async_trait::async_trait] pub trait StateStore: Send + Sync { - /// Signals to the `AsyncClient` if this is the first time a StateStore` has been used. + /// Loads the state of `BaseClient` through `ClientState` type. /// - /// Returns true if `StateStore` has been set up and ready to be loaded from. - async fn initial_use(&self) -> Result; - /// Loads the state of `BaseClient` through `StateStore::Store` type. - async fn load_client_state(&self) -> Result; - /// Load the state of a single `Room` by `RoomId`. - async fn load_room_state(&self, room_id: &RoomId) -> Result; + /// An `Option::None` should be returned only if the `StateStore` tries to + /// load but no state has been stored. + async fn load_client_state(&self) -> Result>; /// Load the state of all `Room`s. /// /// This will be mapped over in the client in order to store `Room`s in an async safe way. @@ -85,8 +83,6 @@ mod test { use std::collections::HashMap; use std::convert::TryFrom; - use crate::identifiers::{RoomId, UserId}; - #[test] fn serialize() { let id = RoomId::try_from("!roomid:example.com").unwrap(); @@ -95,13 +91,14 @@ mod test { let room = Room::new(&id, &user); let state = ClientState { - session: None, + user_id: Some(user.clone()), + device_id: None, sync_token: Some("hello".into()), ignored_users: vec![user], push_ruleset: None, }; assert_eq!( - r#"{"session":null,"sync_token":"hello","ignored_users":["@example:example.com"],"push_ruleset":null}"#, + r#"{"user_id":"@example:example.com","device_id":null,"sync_token":"hello","ignored_users":["@example:example.com"],"push_ruleset":null}"#, serde_json::to_string(&state).unwrap() ); @@ -141,7 +138,8 @@ mod test { let room = Room::new(&id, &user); let state = ClientState { - session: None, + user_id: Some(user.clone()), + device_id: None, sync_token: Some("hello".into()), ignored_users: vec![user], push_ruleset: None, diff --git a/src/state/state_store.rs b/src/state/state_store.rs index 99f84797..87bb474c 100644 --- a/src/state/state_store.rs +++ b/src/state/state_store.rs @@ -2,14 +2,25 @@ use std::collections::HashMap; use std::fs::{self, OpenOptions}; use std::io::{BufReader, BufWriter, Write}; use std::path::{Path, PathBuf}; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; + +use tokio::fs as async_fs; +use tokio::sync::RwLock; use super::{ClientState, StateStore}; use crate::identifiers::RoomId; use crate::{Error, Result, Room}; /// A default `StateStore` implementation that serializes state as json /// and saves it to disk. +/// +/// When logged in the `JsonStore` appends the user_id to it's folder path, +/// so all files are saved in `my_client/user_id/*`. pub struct JsonStore { - path: PathBuf, + path: Arc>, + user_path_set: AtomicBool, } impl JsonStore { @@ -22,39 +33,30 @@ impl JsonStore { std::fs::create_dir_all(p)?; } Ok(Self { - path: p.to_path_buf(), + path: Arc::new(RwLock::new(p.to_path_buf())), + user_path_set: AtomicBool::new(false), }) } } #[async_trait::async_trait] impl StateStore for JsonStore { - async fn initial_use(&self) -> Result { - let mut path = self.path.clone(); - path.push("client.json"); - Ok(fs::read_to_string(path).map_or(false, |s| !s.is_empty())) - } - - async fn load_client_state(&self) -> Result { - let mut path = self.path.clone(); + async fn load_client_state(&self) -> Result> { + let mut path = self.path.read().await.clone(); path.push("client.json"); - let file = OpenOptions::new().read(true).open(path)?; - let reader = BufReader::new(file); - serde_json::from_reader(reader).map_err(Error::from) - } - - async fn load_room_state(&self, room_id: &RoomId) -> Result { - let mut path = self.path.clone(); - path.push(&format!("rooms/{}.json", room_id)); - - let file = OpenOptions::new().read(true).open(path)?; - let reader = BufReader::new(file); - serde_json::from_reader(reader).map_err(Error::from) + let json = async_fs::read_to_string(path) + .await + .map_or(String::default(), |s| s); + if json.is_empty() { + Ok(None) + } else { + serde_json::from_str(&json).map(Some).map_err(Error::from) + } } async fn load_all_rooms(&self) -> Result> { - let mut path = self.path.clone(); + let mut path = self.path.read().await.clone(); path.push("rooms"); let mut rooms_map = HashMap::new(); @@ -78,7 +80,16 @@ impl StateStore for JsonStore { } async fn store_client_state(&self, state: ClientState) -> Result<()> { - let mut path = self.path.clone(); + if !self.user_path_set.load(Ordering::SeqCst) { + if let Some(user) = &state.user_id { + self.user_path_set.swap(true, Ordering::SeqCst); + self.path + .write() + .await + .push(format!("{}", user.localpart())) + } + } + let mut path = self.path.read().await.clone(); path.push("client.json"); if !Path::new(&path).exists() { @@ -101,7 +112,11 @@ impl StateStore for JsonStore { } async fn store_room_state(&self, room: &Room) -> Result<()> { - let mut path = self.path.clone(); + if !self.user_path_set.load(Ordering::SeqCst) { + // TODO Error here, should the load methods also error? + } + + let mut path = self.path.read().await.clone(); path.push(&format!("rooms/{}.json", room.room_id)); if !Path::new(&path).exists() { @@ -170,11 +185,22 @@ mod test { async fn test_store_client_state() { let path: &Path = &PATH; + + let user = UserId::try_from("@example:example.com").unwrap(); + let store = JsonStore::open(path).unwrap(); - let state = ClientState::default(); - store.store_client_state(state).await.unwrap(); + + let state = ClientState { + user_id: Some(user.clone()), + device_id: None, + sync_token: Some("hello".into()), + ignored_users: vec![user], + push_ruleset: None, + }; + + store.store_client_state(state.clone()).await.unwrap(); let loaded = store.load_client_state().await.unwrap(); - assert_eq!(loaded, ClientState::default()); + assert_eq!(loaded, Some(state)); } #[tokio::test] @@ -191,8 +217,8 @@ mod test { let room = Room::new(&id, &user); store.store_room_state(&room).await.unwrap(); - let loaded = store.load_room_state(&id).await.unwrap(); - assert_eq!(loaded, Room::new(&id, &user)); + let loaded = store.load_all_rooms().await.unwrap(); + assert_eq!(loaded.get(&id), Some(&Room::new(&id, &user))); } #[tokio::test] From 8f57f73b666b8c35dc17374e8967469a1b167593 Mon Sep 17 00:00:00 2001 From: Devin R Date: Sat, 25 Apr 2020 06:26:35 -0400 Subject: [PATCH 13/18] command_bot: use JsonStore in example --- examples/command_bot.rs | 9 +++++++-- src/state/mod.rs | 7 +++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/examples/command_bot.rs b/examples/command_bot.rs index fded958e..2b27bfe3 100644 --- a/examples/command_bot.rs +++ b/examples/command_bot.rs @@ -4,7 +4,7 @@ use std::{env, process::exit}; use matrix_sdk::{ self, events::room::message::{MessageEvent, MessageEventContent, TextMessageEventContent}, - AsyncClient, AsyncClientConfig, EventEmitter, Room, SyncSettings, + AsyncClient, AsyncClientConfig, EventEmitter, JsonStore, Room, SyncSettings, }; use tokio::sync::RwLock; use url::Url; @@ -63,9 +63,14 @@ async fn login_and_sync( username: String, password: String, ) -> Result<(), matrix_sdk::Error> { + let mut home = dirs::home_dir().expect("no home directory found"); + home.push("party_bot"); + + let store = JsonStore::open(&home)?; let client_config = AsyncClientConfig::new() .proxy("http://localhost:8080")? - .disable_ssl_verification(); + .disable_ssl_verification() + .state_store(Box::new(store)); let homeserver_url = Url::parse(&homeserver_url)?; // create a new AsyncClient with the given homeserver url and config diff --git a/src/state/mod.rs b/src/state/mod.rs index 1e9906eb..723bdb07 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -25,6 +25,13 @@ use crate::events::push_rules::Ruleset; use crate::identifiers::{DeviceId, RoomId, UserId}; use crate::models::Room; use crate::Result; + +/// `ClientState` holds all the information to restore a `BaseClient` +/// except the `access_token` as the default store is not secure. +/// +/// When implementing `StateStore` for something other than the filesystem +/// implement `From for YourDbType` this allows for easy conversion +/// when needed in `StateStore::load/store_client_state` #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct ClientState { /// The `UserId` for the current logged in user. From bb2d5315250da7303c325c757abc0c92f42e4a33 Mon Sep 17 00:00:00 2001 From: Devin R Date: Sun, 26 Apr 2020 17:13:55 -0400 Subject: [PATCH 14/18] state_store: clean up and add examples to docs --- design.md | 6 +++--- src/async_client.rs | 23 +++++++++++++++++++---- src/session.rs | 3 +-- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/design.md b/design.md index 1e2f2c26..fa084fa9 100644 --- a/design.md +++ b/design.md @@ -19,7 +19,7 @@ In addition to Http, the `AsyncClient` passes along methods from the `BaseClient - more? #### Crypto State Machine -Given a Matrix response the crypto machine will update it's internal state, along with encryption information this means keeping track of when to encrypt. It knows when encryption needs to happen based on signals from the `BaseClient`. The crypto state machine is given responses that relate to encryption and can create encrypted request bodies for encryption-related requests. Basically it tells the `BaseClient` to send to-device messages out, and the `BaseClient` is responsible for notifying the crypto state machine when it sent the message so crypto can update state. +Given a Matrix response the crypto machine will update its own internal state, along with encryption information. `BaseClient` and the crypto machine together keep track of when to encrypt. It knows when encryption needs to happen based on signals from the `BaseClient`. The crypto state machine is given responses that relate to encryption and can create encrypted request bodies for encryption-related requests. Basically it tells the `BaseClient` to send to-device messages out, and the `BaseClient` is responsible for notifying the crypto state machine when it sent the message so crypto can update state. #### Client State/Room and RoomMember The `BaseClient` is responsible for keeping state in sync through the `IncomingResponse`s of `AsyncClient` or querying the `StateStore`. By processing and then delegating incoming `RoomEvent`s, `StateEvent`s, `PresenceEvent`, `IncomingAccountData` and `EphemeralEvent`s to the correct `Room` in the base clients `HashMap` or further to `Room`'s `RoomMember` via the members `HashMap`. The `BaseClient` is also responsible for emitting the incoming events to the `EventEmitter` trait. @@ -89,12 +89,12 @@ pub struct RoomMember { #### State Store The `BaseClient` also has access to a `dyn StateStore` this is an abstraction around a "database" to keep the client state without requesting a full sync from the server on startup. A default implementation that serializes/deserializes JSON to files in a specified directory can be used. The user can also implement `StateStore` to fit any storage solution they choose. The base client handles the storage automatically. There "may be/are TODO" ways for the user to interact directly. The room event handling methods signal if the state was modified; if so, we check if some room state file needs to be overwritten. - open - - load client/room or rooms + - load client/rooms - store client/room - update ?? The state store will restore our client state in the `BaseClient` and client authors can just get the latest state that they want to present from the client object. No need to ask the state store for it, this may change if custom setups request this. `StateStore`'s main purpose is to provide load/store functionality and, internally to the crate, update the `BaseClient`. #### Event Emitter -The consumer of this crate can implement the `EventEmitter` trait for full control over how incoming events are handled by their client. If that isn't enough it is possible to receive every incoming response with the `AsyncClient::sync_forever` callback. +The consumer of this crate can implement the `EventEmitter` trait for full control over how incoming events are handled by their client. If that isn't enough, it is possible to receive every incoming response with the `AsyncClient::sync_forever` callback. - list the methods for `EventEmitter`? diff --git a/src/async_client.rs b/src/async_client.rs index 36c8dfe3..670d5af2 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -91,13 +91,13 @@ impl std::fmt::Debug for AsyncClient { /// .unwrap() /// .disable_ssl_verification(); /// ``` -/// add the default `JsonStore` to the `AsyncClient` +/// An example of adding a default `JsonStore` to the `AsyncClient`. /// ```no_run /// # use matrix_sdk::{AsyncClientConfig, JsonStore}; /// /// let store = JsonStore::open("path/to/json").unwrap(); /// let client_config = AsyncClientConfig::new() -/// . state_store(Box::new(store)); +/// .state_store(Box::new(store)); /// ``` pub struct AsyncClientConfig { proxy: Option, @@ -349,8 +349,23 @@ impl AsyncClient { /// Returns true when a successful `StateStore` sync has completed. /// # Examples /// - /// ``` - /// // TODO + /// ```no_run + /// use matrix_sdk::{AsyncClient, AsyncClientConfig, 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 = AsyncClientConfig::new().state_store(Box::new(store)); + /// let mut cli = AsyncClient::new(homeserver, None).unwrap(); + /// # use futures::executor::block_on; + /// # block_on(async { + /// let _ = cli.login("name", "password", None, None).await.unwrap(); + /// // returns true when a state store sync is successful + /// assert!(cli.sync_with_state_store().await.unwrap()); + /// // now state is restored without a request to the server + /// assert_eq!(vec!["room".to_string(), "names".to_string()], cli.get_room_names().await) + /// # }); /// ``` pub async fn sync_with_state_store(&self) -> Result { self.base_client.write().await.sync_with_state_store().await diff --git a/src/session.rs b/src/session.rs index eeee864f..f091d16a 100644 --- a/src/session.rs +++ b/src/session.rs @@ -16,9 +16,8 @@ //! User sessions. use ruma_identifiers::UserId; -use serde::{Deserialize, Serialize}; /// A user session, containing an access token and information about the associated user account. -#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct Session { /// The access token used for this session. pub access_token: String, From 55de913e083b45e341c4fae2555bb29549f4c0b7 Mon Sep 17 00:00:00 2001 From: Devin R Date: Sun, 26 Apr 2020 17:27:06 -0400 Subject: [PATCH 15/18] state_store: use as many async fs functions as possible --- examples/command_bot.rs | 5 ++++- src/state/state_store.rs | 11 ++++------- src/test_builder.rs | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/examples/command_bot.rs b/examples/command_bot.rs index 2b27bfe3..a238dac5 100644 --- a/examples/command_bot.rs +++ b/examples/command_bot.rs @@ -63,6 +63,7 @@ async fn login_and_sync( username: String, password: String, ) -> Result<(), matrix_sdk::Error> { + // the location for `JsonStore` to save files to let mut home = dirs::home_dir().expect("no home directory found"); home.push("party_bot"); @@ -87,7 +88,9 @@ async fn login_and_sync( println!("logged in as {}", username); - // initial sync to set up state and so our bot doesn't respond to old messages + // An initial sync to set up state and so our bot doesn't respond to old messages. + // If the `StateStore` finds saved state in the location given the initial sync will + // be skipped in favor of loading state from the store client.sync(SyncSettings::default()).await.unwrap(); // add our CommandBot to be notified of incoming messages, we do this after the initial // sync to avoid responding to messages before the bot was running. diff --git a/src/state/state_store.rs b/src/state/state_store.rs index 87bb474c..a30dfaa2 100644 --- a/src/state/state_store.rs +++ b/src/state/state_store.rs @@ -30,7 +30,7 @@ impl JsonStore { pub fn open>(path: P) -> Result { let p = path.as_ref(); if !p.exists() { - std::fs::create_dir_all(p)?; + fs::create_dir_all(p)?; } Ok(Self { path: Arc::new(RwLock::new(p.to_path_buf())), @@ -83,10 +83,7 @@ impl StateStore for JsonStore { if !self.user_path_set.load(Ordering::SeqCst) { if let Some(user) = &state.user_id { self.user_path_set.swap(true, Ordering::SeqCst); - self.path - .write() - .await - .push(format!("{}", user.localpart())) + self.path.write().await.push(user.localpart()) } } let mut path = self.path.read().await.clone(); @@ -95,7 +92,7 @@ impl StateStore for JsonStore { if !Path::new(&path).exists() { let mut dir = path.clone(); dir.pop(); - std::fs::create_dir_all(dir)?; + async_fs::create_dir_all(dir).await?; } let json = serde_json::to_string(&state).map_err(Error::from)?; @@ -122,7 +119,7 @@ impl StateStore for JsonStore { if !Path::new(&path).exists() { let mut dir = path.clone(); dir.pop(); - std::fs::create_dir_all(dir)?; + async_fs::create_dir_all(dir).await?; } let json = serde_json::to_string(&room).map_err(Error::from)?; diff --git a/src/test_builder.rs b/src/test_builder.rs index 6149f4f6..db0f616a 100644 --- a/src/test_builder.rs +++ b/src/test_builder.rs @@ -13,7 +13,7 @@ use crate::events::{ EventJson, TryFromRaw, }; use crate::identifiers::{RoomId, UserId}; -use crate::AsyncClient; +use crate::{AsyncClient, Error, SyncSettings}; use mockito::{self, mock, Matcher, Mock}; @@ -399,11 +399,11 @@ impl MockTestRunner { self } - pub async fn to_client(&mut self) -> Result<&mut AsyncClient, crate::Error> { + pub async fn to_client(&mut self) -> Result<&mut AsyncClient, Error> { self.client .as_mut() .unwrap() - .sync(crate::SyncSettings::default()) + .sync(SyncSettings::default()) .await?; Ok(self.client.as_mut().unwrap()) From 199fb59a764f971e61b75271d34904f4ed5e7525 Mon Sep 17 00:00:00 2001 From: Devin R Date: Mon, 27 Apr 2020 07:23:59 -0400 Subject: [PATCH 16/18] state_store: use tokio Mutex in state store tests --- src/state/state_store.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/state/state_store.rs b/src/state/state_store.rs index a30dfaa2..e7f5e3ac 100644 --- a/src/state/state_store.rs +++ b/src/state/state_store.rs @@ -142,12 +142,13 @@ mod test { use std::convert::TryFrom; use std::fs; + use std::future::Future; use std::path::PathBuf; use std::str::FromStr; - use std::sync::Mutex; use lazy_static::lazy_static; use mockito::{mock, Matcher}; + use tokio::sync::Mutex; use crate::identifiers::{RoomId, UserId}; use crate::{AsyncClient, AsyncClientConfig, Session, SyncSettings}; @@ -168,9 +169,9 @@ mod test { async fn run_and_cleanup(test: fn() -> Fut) where - Fut: std::future::Future, + Fut: Future, { - let _lock = MTX.lock(); + let _lock = MTX.lock().await; test().await; From 8db05ace1e36e7c603e3170a02c7ac0f90d72199 Mon Sep 17 00:00:00 2001 From: Devin R Date: Mon, 27 Apr 2020 08:10:43 -0400 Subject: [PATCH 17/18] sync with state store after login to set path otherwise it makes 2x the folders --- src/base_client.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/base_client.rs b/src/base_client.rs index 83c76585..d4504f80 100644 --- a/src/base_client.rs +++ b/src/base_client.rs @@ -209,6 +209,12 @@ impl Client { *olm = Some(OlmMachine::new(&response.user_id, &response.device_id)?); } + if let Some(store) = self.state_store.as_ref() { + store + .store_client_state(ClientState::from_base_client(self)) + .await?; + } + Ok(()) } From 631ad261ae9b8b99c356c8170c0bd43347e4e913 Mon Sep 17 00:00:00 2001 From: Devin R Date: Mon, 27 Apr 2020 16:55:40 -0400 Subject: [PATCH 18/18] state_store: append user name to path when `store_room_state` is called --- src/base_client.rs | 6 ------ src/state/state_store.rs | 3 ++- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/base_client.rs b/src/base_client.rs index 1e09c887..c3372376 100644 --- a/src/base_client.rs +++ b/src/base_client.rs @@ -209,12 +209,6 @@ impl Client { *olm = Some(OlmMachine::new(&response.user_id, &response.device_id)?); } - if let Some(store) = self.state_store.as_ref() { - store - .store_client_state(ClientState::from_base_client(self)) - .await?; - } - Ok(()) } diff --git a/src/state/state_store.rs b/src/state/state_store.rs index e7f5e3ac..ef3eab1d 100644 --- a/src/state/state_store.rs +++ b/src/state/state_store.rs @@ -110,7 +110,8 @@ impl StateStore for JsonStore { async fn store_room_state(&self, room: &Room) -> Result<()> { if !self.user_path_set.load(Ordering::SeqCst) { - // TODO Error here, should the load methods also error? + self.user_path_set.swap(true, Ordering::SeqCst); + self.path.write().await.push(room.own_user_id.localpart()) } let mut path = self.path.read().await.clone();