state_store: very rough draft of json store

master
Devin R 2020-04-16 10:02:59 -04:00
parent 5fa6b2fc06
commit 7889da2b30
8 changed files with 273 additions and 57 deletions

View File

@ -12,15 +12,18 @@ version = "0.1.0"
[features] [features]
default = [] default = []
encryption = ["olm-rs", "serde/derive", "serde_json", "cjson", "zeroize"] encryption = ["olm-rs", "serde/derive", "cjson", "zeroize"]
sqlite-cryptostore = ["sqlx", "zeroize"] sqlite-cryptostore = ["sqlx", "zeroize"]
[dependencies] [dependencies]
dirs = "2.0.2"
futures = "0.3.4" futures = "0.3.4"
reqwest = "0.10.4" reqwest = "0.10.4"
http = "0.2.1" http = "0.2.1"
url = "2.1.1" url = "2.1.1"
async-trait = "0.1.30" async-trait = "0.1.30"
serde = "1.0.106"
serde_json = "1.0.51"
# Ruma dependencies # Ruma dependencies
js_int = "0.1.4" js_int = "0.1.4"
@ -32,8 +35,6 @@ uuid = { version = "0.8.1", features = ["v4"] }
# Dependencies for the encryption support # Dependencies for the encryption support
olm-rs = { git = "https://gitlab.gnome.org/poljar/olm-rs", optional = true, features = ["serde"]} 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 } cjson = { version = "0.1.0", optional = true }
zeroize = { version = "1.1.0", optional = true, features = ["zeroize_derive"] } zeroize = { version = "1.1.0", optional = true, features = ["zeroize_derive"] }
@ -65,3 +66,4 @@ serde_json = "1.0.51"
tracing-subscriber = "0.2.4" tracing-subscriber = "0.2.4"
tempfile = "3.1.0" tempfile = "3.1.0"
mockito = "0.25.1" mockito = "0.25.1"
lazy_static = "1.4.0"

View File

@ -11,7 +11,7 @@ The highest level structure that ties the other pieces of functionality together
- make raw Http requests - make raw Http requests
#### Base Client/Client State Machine #### 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 - human readable room names
- power level? - power level?
- ignored list? - ignored list?
@ -87,7 +87,7 @@ pub struct RoomMember {
``` ```
#### State Store #### 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 - load
- store/save - store/save
- update ?? - update ??

View File

@ -15,10 +15,13 @@
//! Error conditions. //! Error conditions.
use std::io::Error as IoError;
use reqwest::Error as ReqwestError; use reqwest::Error as ReqwestError;
use ruma_api::error::FromHttpResponseError as RumaResponseError; use ruma_api::error::FromHttpResponseError as RumaResponseError;
use ruma_api::error::IntoHttpError as RumaIntoHttpError; use ruma_api::error::IntoHttpError as RumaIntoHttpError;
use ruma_client_api::Error as RumaClientError; use ruma_client_api::Error as RumaClientError;
use serde_json::Error as JsonError;
use thiserror::Error; use thiserror::Error;
use url::ParseError; use url::ParseError;
@ -46,6 +49,12 @@ pub enum Error {
/// An error converting between ruma_client_api types and Hyper types. /// An error converting between ruma_client_api types and Hyper types.
#[error("can't convert between ruma_client_api and hyper types.")] #[error("can't convert between ruma_client_api and hyper types.")]
IntoHttp(RumaIntoHttpError), 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")] #[cfg(feature = "encryption")]
/// An error occured durring a E2EE operation. /// An error occured durring a E2EE operation.
#[error(transparent)] #[error(transparent)]

View File

@ -32,8 +32,8 @@ use crate::events::EventType;
use crate::identifiers::{RoomAliasId, RoomId, UserId}; use crate::identifiers::{RoomAliasId, RoomId, UserId};
use js_int::{Int, UInt}; use js_int::{Int, UInt};
use serde::{Deserialize, Serialize};
#[derive(Debug, Default)] #[derive(Debug, Default, PartialEq, Serialize, Deserialize)]
/// `RoomName` allows the calculation of a text room name. /// `RoomName` allows the calculation of a text room name.
pub struct RoomName { pub struct RoomName {
/// The displayed name of the room. /// The displayed name of the room.
@ -44,7 +44,7 @@ pub struct RoomName {
aliases: Vec<RoomAliasId>, aliases: Vec<RoomAliasId>,
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PowerLevels { pub struct PowerLevels {
/// The level required to ban a user. /// The level required to ban a user.
pub ban: Int, pub ban: Int,
@ -70,7 +70,7 @@ pub struct PowerLevels {
pub notifications: Int, pub notifications: Int,
} }
#[derive(Debug)] #[derive(Debug, PartialEq, Serialize, Deserialize)]
/// A Matrix rooom. /// A Matrix rooom.
pub struct Room { pub struct Room {
/// The unique id of the room. /// The unique id of the room.

View File

@ -24,10 +24,10 @@ use crate::events::room::{
use crate::identifiers::UserId; use crate::identifiers::UserId;
use js_int::{Int, UInt}; 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. // 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. /// A Matrix room member.
/// ///
pub struct RoomMember { pub struct RoomMember {
@ -58,11 +58,26 @@ pub struct RoomMember {
/// The human readable name of this room member. /// The human readable name of this room member.
pub name: String, pub name: String,
/// The events that created the state of this room member. /// The events that created the state of this room member.
#[serde(skip)]
pub events: Vec<Event>, pub events: Vec<Event>,
/// The `PresenceEvent`s connected to this user. /// The `PresenceEvent`s connected to this user.
#[serde(skip)]
pub presence_events: Vec<PresenceEvent>, pub presence_events: Vec<PresenceEvent>,
} }
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 { impl RoomMember {
pub fn new(event: &MemberEvent) -> Self { pub fn new(event: &MemberEvent) -> Self {
Self { Self {

View File

@ -16,9 +16,9 @@
//! User sessions. //! User sessions.
use ruma_identifiers::UserId; use ruma_identifiers::UserId;
use serde::{Deserialize, Serialize};
/// A user session, containing an access token and information about the associated user account. /// 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 { pub struct Session {
/// The access token used for this session. /// The access token used for this session.
pub access_token: String, pub access_token: String,

View File

@ -16,53 +16,112 @@
pub mod state_store; pub mod state_store;
pub use state_store::JsonStore; pub use state_store::JsonStore;
use crate::api; use serde::{Deserialize, Serialize};
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 crate::events::push_rules::Ruleset;
use std::convert::{TryFrom, TryInto}; use crate::identifiers::{RoomId, UserId};
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::models::Room;
use crate::session::Session; use crate::session::Session;
use crate::VERSION; use crate::{base_client::Token, Result};
use crate::{Error, EventEmitter, 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<Session>,
/// The current sync token that should be used for the next sync call.
pub sync_token: Option<Token>,
/// A list of ignored users.
pub ignored_users: Vec<UserId>,
/// The push ruleset for the logged in user.
pub push_ruleset: Option<Ruleset>,
}
/// Abstraction around the data store to avoid unnecessary request on client initialization. /// Abstraction around the data store to avoid unnecessary request on client initialization.
///
pub trait StateStore { pub trait StateStore {
/// ///
fn load_state(&self) -> sync_events::IncomingResponse; fn load_client_state(&self) -> Result<ClientState>;
/// ///
fn save_state_events(&mut self, events: Vec<StateEvent>) -> Result<()>; fn load_room_state(&self, room_id: &RoomId) -> Result<Room>;
/// ///
fn save_room_events(&mut self, events: Vec<RoomEvent>) -> Result<()>; fn store_client_state(&self, _: ClientState) -> Result<()>;
/// ///
fn save_non_room_events(&mut self, events: Vec<NonRoomEvent>) -> 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());
}
} }

View File

@ -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 /// A default `StateStore` implementation that serializes state as json
/// and saves it to disk. /// 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<ClientState> {
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<Room> {
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);
}
}