From 585ca9fdf71a7d24c280b64a2bccb3a7b32052c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Sun, 31 Jan 2021 18:09:03 +0100 Subject: [PATCH 01/39] matrix-sdk: Split out the http errors into a sub-enum --- matrix_sdk/src/client.rs | 19 +++++---- matrix_sdk/src/error.rs | 77 ++++++++++++++++++----------------- matrix_sdk/src/http_client.rs | 33 +++++++++------ matrix_sdk/src/lib.rs | 2 +- 4 files changed, 70 insertions(+), 61 deletions(-) diff --git a/matrix_sdk/src/client.rs b/matrix_sdk/src/client.rs index 2ea72ff1..6cdb7000 100644 --- a/matrix_sdk/src/client.rs +++ b/matrix_sdk/src/client.rs @@ -118,6 +118,7 @@ use matrix_sdk_common::{ }; use crate::{ + error::HttpError, http_client::{client_with_config, HttpClient, HttpSend}, Error, OutgoingRequest, Result, }; @@ -1451,7 +1452,7 @@ impl Client { content_type: Some(content_type.essence_str()), }); - self.http_client.upload(request).await + Ok(self.http_client.upload(request).await?) } /// Send an arbitrary request to the server, without updating client state. @@ -1494,9 +1495,9 @@ impl Client { pub async fn send(&self, request: Request) -> Result where Request: OutgoingRequest + Debug, - Error: From>, + HttpError: From>, { - self.http_client.send(request).await + Ok(self.http_client.send(request).await?) } #[cfg(feature = "encryption")] @@ -2276,7 +2277,7 @@ impl Client { #[cfg(test)] mod test { - use crate::ClientConfig; + use crate::{ClientConfig, HttpError}; use super::{ get_public_rooms, get_public_rooms_filtered, register::RegistrationKind, Client, @@ -2471,12 +2472,12 @@ mod test { .create(); if let Err(err) = client.login("example", "wordpass", None, None).await { - if let crate::Error::RumaResponse(crate::FromHttpResponseError::Http( - crate::ServerError::Known(crate::api::Error { + if let crate::Error::Http(HttpError::FromHttpResponse( + crate::FromHttpResponseError::Http(crate::ServerError::Known(crate::api::Error { kind, message, status_code, - }), + })), )) = err { if let crate::api::error::ErrorKind::Forbidden = kind { @@ -2517,10 +2518,10 @@ mod test { }); if let Err(err) = client.register(user).await { - if let crate::Error::UiaaError(crate::FromHttpResponseError::Http( + if let crate::Error::Http(HttpError::UiaaError(crate::FromHttpResponseError::Http( // TODO this should be a UiaaError need to investigate crate::ServerError::Unknown(e), - )) = err + ))) = err { assert!(e.to_string().starts_with("EOF while parsing")) } else { diff --git a/matrix_sdk/src/error.rs b/matrix_sdk/src/error.rs index 29b0d9b4..e4611628 100644 --- a/matrix_sdk/src/error.rs +++ b/matrix_sdk/src/error.rs @@ -20,7 +20,7 @@ use matrix_sdk_common::{ r0::uiaa::{UiaaInfo, UiaaResponse as UiaaError}, Error as RumaClientError, }, - FromHttpResponseError as RumaResponseError, IntoHttpError as RumaIntoHttpError, ServerError, + FromHttpResponseError, IntoHttpError, ServerError, }; use reqwest::Error as ReqwestError; use serde_json::Error as JsonError; @@ -33,9 +33,14 @@ use matrix_sdk_base::crypto::store::CryptoStoreError; /// Result type of the rust-sdk. pub type Result = std::result::Result; -/// Internal representation of errors. +/// An HTTP error, representing either a connection error or an error while +/// converting the raw HTTP response into a Matrix response. #[derive(Error, Debug)] -pub enum Error { +pub enum HttpError { + /// An error at the HTTP layer. + #[error(transparent)] + Reqwest(#[from] ReqwestError), + /// Queried endpoint requires authentication but was called on an anonymous client. #[error("the queried endpoint requires authentication but was called before logging in")] AuthenticationRequired, @@ -44,9 +49,33 @@ pub enum Error { #[error("the queried endpoint is not meant for clients")] NotClientRequest, - /// An error at the HTTP layer. + /// An error converting between ruma_client_api types and Hyper types. #[error(transparent)] - Reqwest(#[from] ReqwestError), + FromHttpResponse(#[from] FromHttpResponseError), + + /// An error converting between ruma_client_api types and Hyper types. + #[error(transparent)] + IntoHttp(#[from] IntoHttpError), + + /// An error occurred while authenticating. + /// + /// When registering or authenticating the Matrix server can send a `UiaaResponse` + /// as the error type, this is a User-Interactive Authentication API response. This + /// represents an error with information about how to authenticate the user. + #[error(transparent)] + UiaaError(#[from] FromHttpResponseError), +} + +/// Internal representation of errors. +#[derive(Error, Debug)] +pub enum Error { + /// Error doing an HTTP request. + #[error(transparent)] + Http(#[from] HttpError), + + /// Queried endpoint requires authentication but was called on an anonymous client. + #[error("the queried endpoint requires authentication but was called before logging in")] + AuthenticationRequired, /// An error de/serializing type for the `StateStore` #[error(transparent)] @@ -56,14 +85,6 @@ pub enum Error { #[error(transparent)] IO(#[from] IoError), - /// An error converting between ruma_client_api types and Hyper types. - #[error("can't parse the JSON response as a Matrix response")] - RumaResponse(RumaResponseError), - - /// 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 occurred in the Matrix client library. #[error(transparent)] MatrixError(#[from] MatrixError), @@ -76,14 +97,6 @@ pub enum Error { /// An error occured in the state store. #[error(transparent)] StateStore(#[from] StoreError), - - /// An error occurred while authenticating. - /// - /// When registering or authenticating the Matrix server can send a `UiaaResponse` - /// as the error type, this is a User-Interactive Authentication API response. This - /// represents an error with information about how to authenticate the user. - #[error("User-Interactive Authentication required.")] - UiaaError(RumaResponseError), } impl Error { @@ -99,9 +112,9 @@ impl Error { /// This method is an convenience method to get to the info the server /// returned on the first, failed request. pub fn uiaa_response(&self) -> Option<&UiaaInfo> { - if let Error::UiaaError(RumaResponseError::Http(ServerError::Known( + if let Error::Http(HttpError::UiaaError(FromHttpResponseError::Http(ServerError::Known( UiaaError::AuthResponse(i), - ))) = self + )))) = self { Some(i) } else { @@ -110,20 +123,8 @@ impl Error { } } -impl From> for Error { - fn from(error: RumaResponseError) -> Self { - Self::UiaaError(error) - } -} - -impl From> for Error { - fn from(error: RumaResponseError) -> Self { - Self::RumaResponse(error) - } -} - -impl From for Error { - fn from(error: RumaIntoHttpError) -> Self { - Self::IntoHttp(error) +impl From for Error { + fn from(e: ReqwestError) -> Self { + Error::Http(HttpError::Reqwest(e)) } } diff --git a/matrix_sdk/src/http_client.rs b/matrix_sdk/src/http_client.rs index fd84a424..64f83cc2 100644 --- a/matrix_sdk/src/http_client.rs +++ b/matrix_sdk/src/http_client.rs @@ -24,7 +24,7 @@ use matrix_sdk_common::{ FromHttpResponseError, }; -use crate::{ClientConfig, Error, OutgoingRequest, Result, Session}; +use crate::{error::HttpError, ClientConfig, OutgoingRequest, Session}; /// Abstraction around the http layer. The allows implementors to use different /// http libraries. @@ -74,7 +74,7 @@ pub trait HttpSend: AsyncTraitDeps { async fn send_request( &self, request: http::Request>, - ) -> Result>>; + ) -> Result>, HttpError>; } #[derive(Clone, Debug)] @@ -90,7 +90,7 @@ impl HttpClient { request: Request, session: Arc>>, content_type: Option, - ) -> Result>> { + ) -> Result>, HttpError> { let mut request = { let read_guard; let access_token = match Request::METADATA.authentication { @@ -100,11 +100,11 @@ impl HttpClient { if let Some(session) = read_guard.as_ref() { Some(session.access_token.as_str()) } else { - return Err(Error::AuthenticationRequired); + return Err(HttpError::AuthenticationRequired); } } AuthScheme::None => None, - _ => return Err(Error::NotClientRequest), + _ => return Err(HttpError::NotClientRequest), }; request.try_into_http_request(&self.homeserver.to_string(), access_token)? @@ -124,17 +124,20 @@ impl HttpClient { pub async fn upload( &self, request: create_content::Request<'_>, - ) -> Result { + ) -> Result { let response = self .send_request(request, self.session.clone(), None) .await?; Ok(create_content::Response::try_from(response)?) } - pub async fn send(&self, request: Request) -> Result + pub async fn send( + &self, + request: Request, + ) -> Result where - Request: OutgoingRequest, - Error: From>, + Request: OutgoingRequest + Debug, + HttpError: From>, { let content_type = HeaderValue::from_static("application/json"); let response = self @@ -143,12 +146,14 @@ impl HttpClient { trace!("Got response: {:?}", response); - Ok(Request::IncomingResponse::try_from(response)?) + let response = Request::IncomingResponse::try_from(response)?; + + Ok(response) } } /// Build a client with the specified configuration. -pub(crate) fn client_with_config(config: &ClientConfig) -> Result { +pub(crate) fn client_with_config(config: &ClientConfig) -> Result { let http_client = reqwest::Client::builder(); #[cfg(not(target_arch = "wasm32"))] @@ -188,7 +193,9 @@ pub(crate) fn client_with_config(config: &ClientConfig) -> Result { Ok(http_client.build()?) } -async fn response_to_http_response(mut response: Response) -> Result>> { +async fn response_to_http_response( + mut response: Response, +) -> Result>, reqwest::Error> { let status = response.status(); let mut http_builder = HttpResponse::builder().status(status); @@ -211,7 +218,7 @@ impl HttpSend for Client { async fn send_request( &self, request: http::Request>, - ) -> Result>> { + ) -> Result>, HttpError> { Ok( response_to_http_response(self.execute(reqwest::Request::try_from(request)?).await?) .await?, diff --git a/matrix_sdk/src/lib.rs b/matrix_sdk/src/lib.rs index 8528e920..23e14f8f 100644 --- a/matrix_sdk/src/lib.rs +++ b/matrix_sdk/src/lib.rs @@ -90,7 +90,7 @@ pub use client::{Client, ClientConfig, LoopCtrl, SyncSettings}; #[cfg(feature = "encryption")] #[cfg_attr(feature = "docs", doc(cfg(encryption)))] pub use device::Device; -pub use error::{Error, Result}; +pub use error::{Error, HttpError, Result}; pub use http_client::HttpSend; #[cfg(feature = "encryption")] #[cfg_attr(feature = "docs", doc(cfg(encryption)))] From 42ec456abf3415724e05df00d4d33e3f5d3de65a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Sun, 31 Jan 2021 21:10:30 +0100 Subject: [PATCH 02/39] matrix-sdk: Add initial support for request retrying --- matrix_sdk/Cargo.toml | 4 +++ matrix_sdk/src/error.rs | 9 +++++ matrix_sdk/src/http_client.rs | 62 +++++++++++++++++++++++++++++++---- 3 files changed, 68 insertions(+), 7 deletions(-) diff --git a/matrix_sdk/Cargo.toml b/matrix_sdk/Cargo.toml index 648e358c..d9e8d358 100644 --- a/matrix_sdk/Cargo.toml +++ b/matrix_sdk/Cargo.toml @@ -50,6 +50,10 @@ default_features = false version = "0.11.0" default_features = false +[dependencies.backoff] +git = "https://github.com/ihrwein/backoff" +features = ["tokio"] + [dependencies.tracing-futures] version = "0.2.4" default-features = false diff --git a/matrix_sdk/src/error.rs b/matrix_sdk/src/error.rs index e4611628..1c2f9e57 100644 --- a/matrix_sdk/src/error.rs +++ b/matrix_sdk/src/error.rs @@ -14,6 +14,7 @@ //! Error conditions. +use http::StatusCode; use matrix_sdk_base::{Error as MatrixError, StoreError}; use matrix_sdk_common::{ api::{ @@ -64,6 +65,14 @@ pub enum HttpError { /// represents an error with information about how to authenticate the user. #[error(transparent)] UiaaError(#[from] FromHttpResponseError), + + /// The server returned a status code that should be retried. + #[error("Server returned an error {0}")] + Server(StatusCode), + + /// The given request can't be cloned and thus can't be retried. + #[error("The request cannot be cloned")] + UnableToCloneRequest, } /// Internal representation of errors. diff --git a/matrix_sdk/src/http_client.rs b/matrix_sdk/src/http_client.rs index 64f83cc2..a13e2989 100644 --- a/matrix_sdk/src/http_client.rs +++ b/matrix_sdk/src/http_client.rs @@ -14,6 +14,10 @@ use std::{convert::TryFrom, fmt::Debug, sync::Arc}; +#[cfg(not(test))] +use backoff::{tokio::retry, Error as RetryError, ExponentialBackoff}; +#[cfg(not(test))] +use http::StatusCode; use http::{HeaderValue, Method as HttpMethod, Response as HttpResponse}; use reqwest::{Client, Response}; use tracing::trace; @@ -43,7 +47,7 @@ pub trait HttpSend: AsyncTraitDeps { /// /// ``` /// use std::convert::TryFrom; - /// use matrix_sdk::{HttpSend, Result, async_trait}; + /// use matrix_sdk::{HttpSend, async_trait, HttpError}; /// /// #[derive(Debug)] /// struct Client(reqwest::Client); @@ -52,7 +56,7 @@ pub trait HttpSend: AsyncTraitDeps { /// async fn response_to_http_response( /// &self, /// mut response: reqwest::Response, - /// ) -> Result>> { + /// ) -> Result>, HttpError> { /// // Convert the reqwest response to a http one. /// todo!() /// } @@ -60,7 +64,7 @@ pub trait HttpSend: AsyncTraitDeps { /// /// #[async_trait] /// impl HttpSend for Client { - /// async fn send_request(&self, request: http::Request>) -> Result>> { + /// async fn send_request(&self, request: http::Request>) -> Result>, HttpError> { /// Ok(self /// .response_to_http_response( /// self.0 @@ -212,6 +216,53 @@ async fn response_to_http_response( Ok(http_builder.body(body).unwrap()) } +#[cfg(test)] +async fn send_request( + client: &Client, + request: http::Request>, +) -> Result>, HttpError> { + let request = reqwest::Request::try_from(request)?; + let response = client.execute(request).await?; + + Ok(response_to_http_response(response).await?) +} + +#[cfg(not(test))] +async fn send_request( + client: &Client, + request: http::Request>, +) -> Result>, HttpError> { + let backoff = ExponentialBackoff::default(); + // TODO set a sensible timeout for the request here. + let request = &reqwest::Request::try_from(request)?; + + let request = || async move { + let request = request.try_clone().ok_or(HttpError::UnableToCloneRequest)?; + + let response = client + .execute(request) + .await + .map_err(|e| RetryError::Transient(HttpError::Reqwest(e)))?; + + let status_code = response.status(); + // TODO TOO_MANY_REQUESTS will have a retry timeout which we should + // use. + if status_code.is_server_error() || response.status() == StatusCode::TOO_MANY_REQUESTS { + return Err(RetryError::Transient(HttpError::Server(status_code))); + } + + let response = response_to_http_response(response) + .await + .map_err(|e| RetryError::Permanent(HttpError::Reqwest(e)))?; + + Ok(response) + }; + + let response = retry(backoff, request).await?; + + Ok(response) +} + #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl HttpSend for Client { @@ -219,9 +270,6 @@ impl HttpSend for Client { &self, request: http::Request>, ) -> Result>, HttpError> { - Ok( - response_to_http_response(self.execute(reqwest::Request::try_from(request)?).await?) - .await?, - ) + send_request(&self, request).await } } From 6a4ac8f3611f0b535623e13ca9fe782273d05bcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Sun, 31 Jan 2021 21:12:00 +0100 Subject: [PATCH 03/39] matrix-sdk: Replace some unwraps with expects. --- matrix_sdk/src/http_client.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/matrix_sdk/src/http_client.rs b/matrix_sdk/src/http_client.rs index a13e2989..0b692856 100644 --- a/matrix_sdk/src/http_client.rs +++ b/matrix_sdk/src/http_client.rs @@ -182,7 +182,8 @@ pub(crate) fn client_with_config(config: &ClientConfig) -> Result a.clone(), - None => HeaderValue::from_str(&format!("matrix-rust-sdk {}", crate::VERSION)).unwrap(), + None => HeaderValue::from_str(&format!("matrix-rust-sdk {}", crate::VERSION)) + .expect("Can't construct the version header"), }; headers.insert(reqwest::header::USER_AGENT, user_agent); @@ -203,7 +204,9 @@ async fn response_to_http_response( let status = response.status(); let mut http_builder = HttpResponse::builder().status(status); - let headers = http_builder.headers_mut().unwrap(); + let headers = http_builder + .headers_mut() + .expect("Can't get the response builder headers"); for (k, v) in response.headers_mut().drain() { if let Some(key) = k { @@ -213,7 +216,9 @@ async fn response_to_http_response( let body = response.bytes().await?.as_ref().to_owned(); - Ok(http_builder.body(body).unwrap()) + Ok(http_builder + .body(body) + .expect("Can't construct a response using the given body")) } #[cfg(test)] From a551ae2beedf2049755b3ac939cef8eb555bb98d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 1 Feb 2021 17:15:29 +0100 Subject: [PATCH 04/39] matrix-sdk: Add sensible connection and request timeouts This sets the default * connection timeout to 5s * request timeout to 10s * request timeout for syncs to the sync timeout + 10s * request timeout for uploads to be based on 1Mbps upload speed expectations --- matrix_sdk/examples/get_profiles.rs | 2 +- matrix_sdk/src/client.rs | 96 +++++++++++++++++------------ matrix_sdk/src/http_client.rs | 37 ++++++++--- matrix_sdk/src/sas.rs | 2 +- 4 files changed, 86 insertions(+), 51 deletions(-) diff --git a/matrix_sdk/examples/get_profiles.rs b/matrix_sdk/examples/get_profiles.rs index c75d25b4..4d20bb76 100644 --- a/matrix_sdk/examples/get_profiles.rs +++ b/matrix_sdk/examples/get_profiles.rs @@ -19,7 +19,7 @@ async fn get_profile(client: Client, mxid: &UserId) -> MatrixResult let request = profile::get_profile::Request::new(mxid); // Start the request using matrix_sdk::Client::send - let resp = client.send(request).await?; + let resp = client.send(request, None).await?; // Use the response and construct a UserProfile struct. // See https://docs.rs/ruma-client-api/0.9.0/ruma_client_api/r0/profile/get_profile/struct.Response.html diff --git a/matrix_sdk/src/client.rs b/matrix_sdk/src/client.rs index 6cdb7000..0d4abfe2 100644 --- a/matrix_sdk/src/client.rs +++ b/matrix_sdk/src/client.rs @@ -132,6 +132,10 @@ use crate::{ }; const DEFAULT_SYNC_TIMEOUT: Duration = Duration::from_secs(30); +/// Give the sync a bit more time than the default request timeout does. +const SYNC_REQUEST_TIMEOUT: Duration = Duration::from_secs(15); +/// A conservative upload speed of 1Mbps +const DEFAULT_UPLOAD_SPEED: u64 = 125_000; /// An async/await enabled Matrix client. /// @@ -452,7 +456,7 @@ impl Client { pub async fn display_name(&self) -> Result> { let user_id = self.user_id().await.ok_or(Error::AuthenticationRequired)?; let request = get_display_name::Request::new(&user_id); - let response = self.send(request).await?; + let response = self.send(request, None).await?; Ok(response.displayname) } @@ -475,7 +479,7 @@ impl Client { pub async fn set_display_name(&self, name: Option<&str>) -> Result<()> { let user_id = self.user_id().await.ok_or(Error::AuthenticationRequired)?; let request = set_display_name::Request::new(&user_id, name); - self.send(request).await?; + self.send(request, None).await?; Ok(()) } @@ -500,7 +504,7 @@ impl Client { pub async fn avatar_url(&self) -> Result> { let user_id = self.user_id().await.ok_or(Error::AuthenticationRequired)?; let request = get_avatar_url::Request::new(&user_id); - let response = self.send(request).await?; + let response = self.send(request, None).await?; Ok(response.avatar_url) } @@ -513,7 +517,7 @@ impl Client { pub async fn set_avatar_url(&self, url: Option<&str>) -> Result<()> { let user_id = self.user_id().await.ok_or(Error::AuthenticationRequired)?; let request = set_avatar_url::Request::new(&user_id, url); - self.send(request).await?; + self.send(request, None).await?; Ok(()) } @@ -672,7 +676,7 @@ impl Client { } ); - let response = self.send(request).await?; + let response = self.send(request, None).await?; self.base_client.receive_login_response(&response).await?; Ok(response) @@ -734,7 +738,7 @@ impl Client { info!("Registering to {}", self.homeserver); let request = registration.into(); - self.send(request).await + self.send(request, None).await } /// Get or upload a sync filter. @@ -748,7 +752,7 @@ impl Client { } else { let user_id = self.user_id().await.ok_or(Error::AuthenticationRequired)?; let request = FilterUploadRequest::new(&user_id, definition); - let response = self.send(request).await?; + let response = self.send(request, None).await?; self.base_client .receive_filter_upload(filter_name, &response) @@ -768,7 +772,7 @@ impl Client { /// * `room_id` - The `RoomId` of the room to be joined. pub async fn join_room_by_id(&self, room_id: &RoomId) -> Result { let request = join_room_by_id::Request::new(room_id); - self.send(request).await + self.send(request, None).await } /// Join a room by `RoomId`. @@ -788,7 +792,7 @@ impl Client { let request = assign!(join_room_by_id_or_alias::Request::new(alias), { server_name: server_names, }); - self.send(request).await + self.send(request, None).await } /// Forget a room by `RoomId`. @@ -800,7 +804,7 @@ impl Client { /// * `room_id` - The `RoomId` of the room to be forget. pub async fn forget_room_by_id(&self, room_id: &RoomId) -> Result { let request = forget_room::Request::new(room_id); - self.send(request).await + self.send(request, None).await } /// Ban a user from a room by `RoomId` and `UserId`. @@ -821,7 +825,7 @@ impl Client { reason: Option<&str>, ) -> Result { let request = assign!(ban_user::Request::new(room_id, user_id), { reason }); - self.send(request).await + self.send(request, None).await } /// Kick a user out of the specified room. @@ -842,7 +846,7 @@ impl Client { reason: Option<&str>, ) -> Result { let request = assign!(kick_user::Request::new(room_id, user_id), { reason }); - self.send(request).await + self.send(request, None).await } /// Leave the specified room. @@ -854,7 +858,7 @@ impl Client { /// * `room_id` - The `RoomId` of the room to leave. pub async fn leave_room(&self, room_id: &RoomId) -> Result { let request = leave_room::Request::new(room_id); - self.send(request).await + self.send(request, None).await } /// Invite the specified user by `UserId` to the given room. @@ -874,7 +878,7 @@ impl Client { let recipient = InvitationRecipient::UserId { user_id }; let request = invite_user::Request::new(room_id, recipient); - self.send(request).await + self.send(request, None).await } /// Invite the specified user by third party id to the given room. @@ -893,7 +897,7 @@ impl Client { ) -> Result { let recipient = InvitationRecipient::ThirdPartyId(invite_id); let request = invite_user::Request::new(room_id, recipient); - self.send(request).await + self.send(request, None).await } /// Search the homeserver's directory of public rooms. @@ -939,7 +943,7 @@ impl Client { since, server, }); - self.send(request).await + self.send(request, None).await } /// Search the homeserver's directory of public rooms with a filter. @@ -977,7 +981,7 @@ impl Client { room_search: impl Into>, ) -> Result { let request = room_search.into(); - self.send(request).await + self.send(request, None).await } /// Create a room using the `RoomBuilder` and send the request. @@ -1009,7 +1013,7 @@ impl Client { room: impl Into>, ) -> Result { let request = room.into(); - self.send(request).await + self.send(request, None).await } /// Sends a request to `/_matrix/client/r0/rooms/{room_id}/messages` and returns @@ -1044,8 +1048,8 @@ impl Client { &self, request: impl Into>, ) -> Result { - let req = request.into(); - self.send(req).await + let request = request.into(); + self.send(request, None).await } /// Send a request to notify the room of a user typing. @@ -1088,7 +1092,7 @@ impl Client { let user_id = self.user_id().await.ok_or(Error::AuthenticationRequired)?; let request = TypingRequest::new(&user_id, room_id, typing.into()); - self.send(request).await + self.send(request, None).await } /// Send a request to notify the room the user has read specific event. @@ -1107,7 +1111,7 @@ impl Client { ) -> Result { let request = create_receipt::Request::new(room_id, create_receipt::ReceiptType::Read, event_id); - self.send(request).await + self.send(request, None).await } /// Send a request to notify the room user has read up to specific event. @@ -1130,7 +1134,7 @@ impl Client { let request = assign!(set_read_marker::Request::new(room_id, fully_read), { read_receipt }); - self.send(request).await + self.send(request, None).await } /// Share a group session for the given room. @@ -1261,7 +1265,7 @@ impl Client { let txn_id = txn_id.unwrap_or_else(Uuid::new_v4).to_string(); let request = send_message_event::Request::new(&room_id, &txn_id, &content); - let response = self.send(request).await?; + let response = self.send(request, None).await?; Ok(response) } @@ -1448,11 +1452,13 @@ impl Client { let mut data = Vec::new(); reader.read_to_end(&mut data)?; + let timeout = Duration::from_secs(data.len() as u64 / DEFAULT_UPLOAD_SPEED); + let request = assign!(create_content::Request::new(data), { content_type: Some(content_type.essence_str()), }); - Ok(self.http_client.upload(request).await?) + Ok(self.http_client.upload(request, Some(timeout)).await?) } /// Send an arbitrary request to the server, without updating client state. @@ -1466,6 +1472,9 @@ impl Client { /// /// * `request` - A filled out and valid request for the endpoint to be hit /// + /// * `timeout` - An optional request timeout setting, this overrides the + /// default request setting if one was set. + /// /// # Example /// /// ```no_run @@ -1486,18 +1495,22 @@ impl Client { /// let request = profile::get_profile::Request::new(&user_id); /// /// // Start the request using Client::send() - /// let response = client.send(request).await.unwrap(); + /// let response = client.send(request, None).await.unwrap(); /// /// // Check the corresponding Response struct to find out what types are /// // returned /// # }) /// ``` - pub async fn send(&self, request: Request) -> Result + pub async fn send( + &self, + request: Request, + timeout: Option, + ) -> Result where Request: OutgoingRequest + Debug, HttpError: From>, { - Ok(self.http_client.send(request).await?) + Ok(self.http_client.send(request, timeout).await?) } #[cfg(feature = "encryption")] @@ -1512,7 +1525,7 @@ impl Client { request.messages.clone(), ); - self.send(request).await + self.send(request, None).await } /// Get information of all our own devices. @@ -1541,7 +1554,7 @@ impl Client { pub async fn devices(&self) -> Result { let request = get_devices::Request::new(); - self.send(request).await + self.send(request, None).await } /// Delete the given devices from the server. @@ -1606,13 +1619,13 @@ impl Client { let mut request = delete_devices::Request::new(devices); request.auth = auth_data; - self.send(request).await + self.send(request, None).await } /// Get the room members for the given room. pub async fn room_members(&self, room_id: &RoomId) -> Result { let request = get_member_events::Request::new(room_id); - let response = self.send(request).await?; + let response = self.send(request, None).await?; Ok(self.base_client.receive_members(room_id, &response).await?) } @@ -1638,7 +1651,12 @@ impl Client { timeout: sync_settings.timeout, }); - let response = self.send(request).await?; + let timeout = sync_settings + .timeout + .unwrap_or_else(|| Duration::from_secs(0)) + + SYNC_REQUEST_TIMEOUT; + + let response = self.send(request, Some(timeout)).await?; Ok(self.base_client.receive_sync_response(response).await?) } @@ -1779,7 +1797,7 @@ impl Client { } OutgoingRequests::SignatureUpload(request) => { // TODO remove this unwrap. - if let Ok(resp) = self.send(request.clone()).await { + if let Ok(resp) = self.send(request.clone(), None).await { self.base_client .mark_request_as_sent(&r.request_id(), &resp) .await @@ -1839,7 +1857,7 @@ impl Client { let _lock = self.key_claim_lock.lock().await; if let Some((request_id, request)) = self.base_client.get_missing_sessions(users).await? { - let response = self.send(request).await?; + let response = self.send(request, None).await?; self.base_client .mark_request_as_sent(&request_id, &response) .await?; @@ -1898,7 +1916,7 @@ impl Client { request.one_time_keys.as_ref().map_or(0, |k| k.len()) ); - let response = self.send(request.clone()).await?; + let response = self.send(request.clone(), None).await?; self.base_client .mark_request_as_sent(request_id, &response) .await?; @@ -1927,7 +1945,7 @@ impl Client { ) -> Result { let request = assign!(get_keys::Request::new(), { device_keys }); - let response = self.send(request).await?; + let response = self.send(request, None).await?; self.base_client .mark_request_as_sent(request_id, &response) .await?; @@ -2080,8 +2098,8 @@ impl Client { user_signing_key: request.user_signing_key, }); - self.send(request).await?; - self.send(signature_request).await?; + self.send(request, None).await?; + self.send(signature_request, None).await?; Ok(()) } diff --git a/matrix_sdk/src/http_client.rs b/matrix_sdk/src/http_client.rs index 0b692856..d995e1eb 100644 --- a/matrix_sdk/src/http_client.rs +++ b/matrix_sdk/src/http_client.rs @@ -24,12 +24,15 @@ use tracing::trace; use url::Url; use matrix_sdk_common::{ - api::r0::media::create_content, async_trait, locks::RwLock, AsyncTraitDeps, AuthScheme, - FromHttpResponseError, + api::r0::media::create_content, async_trait, instant::Duration, locks::RwLock, AsyncTraitDeps, + AuthScheme, FromHttpResponseError, }; use crate::{error::HttpError, ClientConfig, OutgoingRequest, Session}; +const DEFAULT_CONNECTION_TIMEOUT: Duration = Duration::from_secs(5); +const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(10); + /// Abstraction around the http layer. The allows implementors to use different /// http libraries. #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] @@ -78,6 +81,7 @@ pub trait HttpSend: AsyncTraitDeps { async fn send_request( &self, request: http::Request>, + timeout: Option, ) -> Result>, HttpError>; } @@ -94,6 +98,7 @@ impl HttpClient { request: Request, session: Arc>>, content_type: Option, + timeout: Option, ) -> Result>, HttpError> { let mut request = { let read_guard; @@ -122,15 +127,16 @@ impl HttpClient { } } - self.inner.send_request(request).await + self.inner.send_request(request, timeout).await } pub async fn upload( &self, request: create_content::Request<'_>, + timeout: Option, ) -> Result { let response = self - .send_request(request, self.session.clone(), None) + .send_request(request, self.session.clone(), None, timeout) .await?; Ok(create_content::Response::try_from(response)?) } @@ -138,6 +144,7 @@ impl HttpClient { pub async fn send( &self, request: Request, + timeout: Option, ) -> Result where Request: OutgoingRequest + Debug, @@ -145,7 +152,7 @@ impl HttpClient { { let content_type = HeaderValue::from_static("application/json"); let response = self - .send_request(request, self.session.clone(), Some(content_type)) + .send_request(request, self.session.clone(), Some(content_type), timeout) .await?; trace!("Got response: {:?}", response); @@ -164,7 +171,7 @@ pub(crate) fn client_with_config(config: &ClientConfig) -> Result http_client.timeout(x), - None => http_client, + None => http_client.timeout(DEFAULT_REQUEST_TIMEOUT), }; let http_client = if config.disable_ssl_verification { @@ -188,7 +195,9 @@ pub(crate) fn client_with_config(config: &ClientConfig) -> Result>, + _: Option, ) -> Result>, HttpError> { let request = reqwest::Request::try_from(request)?; let response = client.execute(request).await?; @@ -236,10 +246,16 @@ async fn send_request( async fn send_request( client: &Client, request: http::Request>, + timeout: Option, ) -> Result>, HttpError> { let backoff = ExponentialBackoff::default(); - // TODO set a sensible timeout for the request here. - let request = &reqwest::Request::try_from(request)?; + let mut request = reqwest::Request::try_from(request)?; + + if let Some(timeout) = timeout { + *request.timeout_mut() = Some(timeout); + } + + let request = &request; let request = || async move { let request = request.try_clone().ok_or(HttpError::UnableToCloneRequest)?; @@ -274,7 +290,8 @@ impl HttpSend for Client { async fn send_request( &self, request: http::Request>, + timeout: Option, ) -> Result>, HttpError> { - send_request(&self, request).await + send_request(&self, request, timeout).await } } diff --git a/matrix_sdk/src/sas.rs b/matrix_sdk/src/sas.rs index ccbc4224..389633f7 100644 --- a/matrix_sdk/src/sas.rs +++ b/matrix_sdk/src/sas.rs @@ -54,7 +54,7 @@ impl Sas { } if let Some(s) = signature { - self.client.send(s).await?; + self.client.send(s, None).await?; } Ok(()) From 2e2d9b33a4d0d866e37d7374a248edd726c0141e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 1 Feb 2021 17:30:43 +0100 Subject: [PATCH 05/39] contrib: Add a mitmproxy script which can be used to test out request retrying --- contrib/mitmproxy/failures.py | 47 +++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 contrib/mitmproxy/failures.py diff --git a/contrib/mitmproxy/failures.py b/contrib/mitmproxy/failures.py new file mode 100644 index 00000000..10dc1898 --- /dev/null +++ b/contrib/mitmproxy/failures.py @@ -0,0 +1,47 @@ +""" +A mitmproxy script that introduces certain request failures in a deterministic +way. + +Used mainly for Matrix style requests. + +To run execute it with mitmproxy: + + >>> mitmproxy -s failures.py` + +""" +import time +import json + +from mitmproxy import http +from mitmproxy.script import concurrent + +REQUEST_COUNT = 0 + + +@concurrent +def request(flow): + global REQUEST_COUNT + + REQUEST_COUNT += 1 + + if REQUEST_COUNT % 2 == 0: + return + elif REQUEST_COUNT % 3 == 0: + flow.response = http.HTTPResponse.make( + 500, + b"Gateway error", + ) + elif REQUEST_COUNT % 7 == 0: + if "sync" in flow.request.pretty_url: + time.sleep(60) + else: + time.sleep(30) + else: + flow.response = http.HTTPResponse.make( + 429, + json.dumps({ + "errcode": "M_LIMIT_EXCEEDED", + "error": "Too many requests", + "retry_after_ms": 2000 + }) + ) From 19e98849639273b66cc292e0c1375f255cbb541d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 1 Feb 2021 17:58:03 +0100 Subject: [PATCH 06/39] matrix-sdk: Update for the latest backoff changes --- matrix_sdk/Cargo.toml | 1 + matrix_sdk/src/http_client.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/matrix_sdk/Cargo.toml b/matrix_sdk/Cargo.toml index d9e8d358..bca1f66b 100644 --- a/matrix_sdk/Cargo.toml +++ b/matrix_sdk/Cargo.toml @@ -53,6 +53,7 @@ default_features = false [dependencies.backoff] git = "https://github.com/ihrwein/backoff" features = ["tokio"] +rev = "fa3fb91431729ce871d29c62b93425b8aec740f4" [dependencies.tracing-futures] version = "0.2.4" diff --git a/matrix_sdk/src/http_client.rs b/matrix_sdk/src/http_client.rs index d995e1eb..1a66203e 100644 --- a/matrix_sdk/src/http_client.rs +++ b/matrix_sdk/src/http_client.rs @@ -15,7 +15,7 @@ use std::{convert::TryFrom, fmt::Debug, sync::Arc}; #[cfg(not(test))] -use backoff::{tokio::retry, Error as RetryError, ExponentialBackoff}; +use backoff::{future::retry, Error as RetryError, ExponentialBackoff}; #[cfg(not(test))] use http::StatusCode; use http::{HeaderValue, Method as HttpMethod, Response as HttpResponse}; From f3d4f6aab460bf990c6e1a11f4b1efd750143244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 1 Feb 2021 19:24:29 +0100 Subject: [PATCH 07/39] matrix-sdk: Fix our HttpClient trait implementation example --- matrix_sdk/src/http_client.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/matrix_sdk/src/http_client.rs b/matrix_sdk/src/http_client.rs index 1a66203e..60a89e97 100644 --- a/matrix_sdk/src/http_client.rs +++ b/matrix_sdk/src/http_client.rs @@ -51,6 +51,7 @@ pub trait HttpSend: AsyncTraitDeps { /// ``` /// use std::convert::TryFrom; /// use matrix_sdk::{HttpSend, async_trait, HttpError}; + /// # use std::time::Duration; /// /// #[derive(Debug)] /// struct Client(reqwest::Client); @@ -67,7 +68,11 @@ pub trait HttpSend: AsyncTraitDeps { /// /// #[async_trait] /// impl HttpSend for Client { - /// async fn send_request(&self, request: http::Request>) -> Result>, HttpError> { + /// async fn send_request( + /// &self, + /// request: http::Request>, + /// timeout: Option, + /// ) -> Result>, HttpError> { /// Ok(self /// .response_to_http_response( /// self.0 From ca7117af2b34540b75e204e864ef3a458c9e17cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 1 Feb 2021 21:56:15 +0100 Subject: [PATCH 08/39] matrix-sdk: Clamp the request timeout for uploads to a sensible value --- matrix_sdk/src/client.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/matrix_sdk/src/client.rs b/matrix_sdk/src/client.rs index 0d4abfe2..016da325 100644 --- a/matrix_sdk/src/client.rs +++ b/matrix_sdk/src/client.rs @@ -136,6 +136,8 @@ const DEFAULT_SYNC_TIMEOUT: Duration = Duration::from_secs(30); const SYNC_REQUEST_TIMEOUT: Duration = Duration::from_secs(15); /// A conservative upload speed of 1Mbps const DEFAULT_UPLOAD_SPEED: u64 = 125_000; +/// 5 min minimal upload request timeout, used to clamp the request timeout. +const MIN_UPLOAD_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 5); /// An async/await enabled Matrix client. /// @@ -1452,7 +1454,10 @@ impl Client { let mut data = Vec::new(); reader.read_to_end(&mut data)?; - let timeout = Duration::from_secs(data.len() as u64 / DEFAULT_UPLOAD_SPEED); + let timeout = std::cmp::max( + Duration::from_secs(data.len() as u64 / DEFAULT_UPLOAD_SPEED), + MIN_UPLOAD_REQUEST_TIMEOUT, + ); let request = assign!(create_content::Request::new(data), { content_type: Some(content_type.essence_str()), From fcd1c877659cd7c7971722d9a788bf648caf223c Mon Sep 17 00:00:00 2001 From: Devin Ragotzy Date: Thu, 4 Feb 2021 15:54:20 -0500 Subject: [PATCH 09/39] matrix_sdk: export CustomEvent and StateChanges add docs to StateChanges --- matrix_sdk/src/lib.rs | 4 ++-- matrix_sdk_base/src/lib.rs | 4 ++-- matrix_sdk_base/src/store/mod.rs | 25 ++++++++++++++++++++++++- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/matrix_sdk/src/lib.rs b/matrix_sdk/src/lib.rs index 8528e920..d831d541 100644 --- a/matrix_sdk/src/lib.rs +++ b/matrix_sdk/src/lib.rs @@ -68,8 +68,8 @@ compile_error!("only one of 'native-tls' or 'rustls-tls' features can be enabled #[cfg_attr(feature = "docs", doc(cfg(encryption)))] pub use matrix_sdk_base::crypto::LocalTrust; pub use matrix_sdk_base::{ - Error as BaseError, EventEmitter, InvitedRoom, JoinedRoom, LeftRoom, RoomInfo, RoomMember, - RoomState, Session, StoreError, + CustomEvent, Error as BaseError, EventEmitter, InvitedRoom, JoinedRoom, LeftRoom, RoomInfo, + RoomMember, RoomState, Session, StateChanges, StoreError, }; pub use matrix_sdk_common::*; diff --git a/matrix_sdk_base/src/lib.rs b/matrix_sdk_base/src/lib.rs index 15731b1a..32cffeaa 100644 --- a/matrix_sdk_base/src/lib.rs +++ b/matrix_sdk_base/src/lib.rs @@ -51,12 +51,12 @@ mod rooms; mod session; mod store; -pub use event_emitter::EventEmitter; +pub use event_emitter::{CustomEvent, EventEmitter}; pub use rooms::{ InvitedRoom, JoinedRoom, LeftRoom, Room, RoomInfo, RoomMember, RoomState, StrippedRoom, StrippedRoomInfo, }; -pub use store::{StateStore, Store, StoreError}; +pub use store::{StateChanges, StateStore, Store, StoreError}; pub use client::{BaseClient, BaseClientConfig, RoomStateType}; diff --git a/matrix_sdk_base/src/store/mod.rs b/matrix_sdk_base/src/store/mod.rs index de87554b..698efa13 100644 --- a/matrix_sdk_base/src/store/mod.rs +++ b/matrix_sdk_base/src/store/mod.rs @@ -355,26 +355,41 @@ impl Deref for Store { } } +/// Store state changes and pass them to the StateStore. #[derive(Debug, Default)] pub struct StateChanges { + /// The sync token that relates to this update. pub sync_token: Option, + /// A user session, containing an access token and information about the associated user account. pub session: Option, + /// A mapping of event type string to `AnyBasicEvent`. pub account_data: BTreeMap, + /// A mapping of `UserId` to `PresenceEvent`. pub presence: BTreeMap, + /// A mapping of `RoomId` to a map of users and their `MemberEvent`. pub members: BTreeMap>, + /// A mapping of `RoomId` to a map of users and their `MemberEventContent`. pub profiles: BTreeMap>, - pub ambiguity_maps: BTreeMap>>, + + pub(crate) ambiguity_maps: BTreeMap>>, + /// A mapping of `RoomId` to a map of event type string to a state key and `AnySyncStateEvent`. pub state: BTreeMap>>, + /// A mapping of `RoomId` to a map of event type string to `AnyBasicEvent`. pub room_account_data: BTreeMap>, + /// A map of `RoomId` to `RoomInfo`. pub room_infos: BTreeMap, + /// A mapping of `RoomId` to a map of event type to a map of state key to `AnyStrippedStateEvent`. pub stripped_state: BTreeMap>>, + /// A mapping of `RoomId` to a map of users and their `StrippedMemberEvent`. pub stripped_members: BTreeMap>, + /// A map of `RoomId` to `StrippedRoomInfo`. pub invited_room_info: BTreeMap, } impl StateChanges { + /// Create a new `StateChanges` struct with the given sync_token. pub fn new(sync_token: String) -> Self { Self { sync_token: Some(sync_token), @@ -382,25 +397,30 @@ impl StateChanges { } } + /// Update the `StateChanges` struct with the given `PresenceEvent`. pub fn add_presence_event(&mut self, event: PresenceEvent) { self.presence.insert(event.sender.clone(), event); } + /// Update the `StateChanges` struct with the given `RoomInfo`. pub fn add_room(&mut self, room: RoomInfo) { self.room_infos .insert(room.room_id.as_ref().to_owned(), room); } + /// Update the `StateChanges` struct with the given `StrippedRoomInfo`. pub fn add_stripped_room(&mut self, room: StrippedRoomInfo) { self.invited_room_info .insert(room.room_id.as_ref().to_owned(), room); } + /// Update the `StateChanges` struct with the given `AnyBasicEvent`. pub fn add_account_data(&mut self, event: AnyBasicEvent) { self.account_data .insert(event.content().event_type().to_owned(), event); } + /// Update the `StateChanges` struct with the given room with a new `AnyBasicEvent`. pub fn add_room_account_data(&mut self, room_id: &RoomId, event: AnyBasicEvent) { self.room_account_data .entry(room_id.to_owned()) @@ -408,6 +428,7 @@ impl StateChanges { .insert(event.content().event_type().to_owned(), event); } + /// Update the `StateChanges` struct with the given room with a new `AnyStrippedStateEvent`. pub fn add_stripped_state_event(&mut self, room_id: &RoomId, event: AnyStrippedStateEvent) { self.stripped_state .entry(room_id.to_owned()) @@ -417,6 +438,7 @@ impl StateChanges { .insert(event.state_key().to_string(), event); } + /// Update the `StateChanges` struct with the given room with a new `StrippedMemberEvent`. pub fn add_stripped_member(&mut self, room_id: &RoomId, event: StrippedMemberEvent) { let user_id = event.state_key.clone(); @@ -426,6 +448,7 @@ impl StateChanges { .insert(user_id, event); } + /// Update the `StateChanges` struct with the given room with a new `AnySyncStateEvent`. pub fn add_state_event(&mut self, room_id: &RoomId, event: AnySyncStateEvent) { self.state .entry(room_id.to_owned()) From 36e3039d73b51e64e39e247c98d316883e37beb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Sun, 7 Feb 2021 12:53:06 +0100 Subject: [PATCH 10/39] matrix-sdk: Disable request retrying for wasm for now Backoff supports the retry method for futures only for non-wasm targets for now, thus we're going to disable it until that changes. --- matrix_sdk/Cargo.toml | 2 +- matrix_sdk/src/http_client.rs | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/matrix_sdk/Cargo.toml b/matrix_sdk/Cargo.toml index bca1f66b..95ffc18b 100644 --- a/matrix_sdk/Cargo.toml +++ b/matrix_sdk/Cargo.toml @@ -50,7 +50,7 @@ default_features = false version = "0.11.0" default_features = false -[dependencies.backoff] +[target.'cfg(not(target_arch = "wasm32"))'.dependencies.backoff] git = "https://github.com/ihrwein/backoff" features = ["tokio"] rev = "fa3fb91431729ce871d29c62b93425b8aec740f4" diff --git a/matrix_sdk/src/http_client.rs b/matrix_sdk/src/http_client.rs index 60a89e97..cd7d6265 100644 --- a/matrix_sdk/src/http_client.rs +++ b/matrix_sdk/src/http_client.rs @@ -14,9 +14,9 @@ use std::{convert::TryFrom, fmt::Debug, sync::Arc}; -#[cfg(not(test))] +#[cfg(all(not(test), not(target_arch = "wasm32")))] use backoff::{future::retry, Error as RetryError, ExponentialBackoff}; -#[cfg(not(test))] +#[cfg(all(not(test), not(target_arch = "wasm32")))] use http::StatusCode; use http::{HeaderValue, Method as HttpMethod, Response as HttpResponse}; use reqwest::{Client, Response}; @@ -30,7 +30,9 @@ use matrix_sdk_common::{ use crate::{error::HttpError, ClientConfig, OutgoingRequest, Session}; +#[cfg(not(target_arch = "wasm32"))] const DEFAULT_CONNECTION_TIMEOUT: Duration = Duration::from_secs(5); +#[cfg(not(target_arch = "wasm32"))] const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(10); /// Abstraction around the http layer. The allows implementors to use different @@ -235,7 +237,7 @@ async fn response_to_http_response( .expect("Can't construct a response using the given body")) } -#[cfg(test)] +#[cfg(any(test, target_arch = "wasm32"))] async fn send_request( client: &Client, request: http::Request>, @@ -247,7 +249,7 @@ async fn send_request( Ok(response_to_http_response(response).await?) } -#[cfg(not(test))] +#[cfg(all(not(test), not(target_arch = "wasm32")))] async fn send_request( client: &Client, request: http::Request>, From e7e43a8bf0c0195444abefd0dafd3d5432065b72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Sun, 7 Feb 2021 17:21:50 +0100 Subject: [PATCH 11/39] matrix-sdk: Use a released version of backoff --- matrix_sdk/Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/matrix_sdk/Cargo.toml b/matrix_sdk/Cargo.toml index 95ffc18b..62452356 100644 --- a/matrix_sdk/Cargo.toml +++ b/matrix_sdk/Cargo.toml @@ -51,9 +51,8 @@ version = "0.11.0" default_features = false [target.'cfg(not(target_arch = "wasm32"))'.dependencies.backoff] -git = "https://github.com/ihrwein/backoff" +version = "0.3.0" features = ["tokio"] -rev = "fa3fb91431729ce871d29c62b93425b8aec740f4" [dependencies.tracing-futures] version = "0.2.4" From 155f97526209c22b993ab658b3d8cb547ee7f5eb Mon Sep 17 00:00:00 2001 From: Julian Sparber Date: Fri, 5 Feb 2021 15:18:11 +0100 Subject: [PATCH 12/39] Update ruma to rev d6aa37c848b7f682a98c25b346899e284ffc6df7 This enables the `compat` feature of ruma to increase compatipility. --- matrix_sdk_common/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix_sdk_common/Cargo.toml b/matrix_sdk_common/Cargo.toml index 444d397e..5d9262b5 100644 --- a/matrix_sdk_common/Cargo.toml +++ b/matrix_sdk_common/Cargo.toml @@ -22,8 +22,8 @@ async-trait = "0.1.42" [dependencies.ruma] version = "0.0.2" git = "https://github.com/ruma/ruma" -rev = "8c109d3c0a7ec66b352dc82677d30db7cb0723eb" -features = ["client-api", "unstable-pre-spec"] +rev = "d6aa37c848b7f682a98c25b346899e284ffc6df7" +features = ["client-api", "compat", "unstable-pre-spec"] [target.'cfg(not(target_arch = "wasm32"))'.dependencies] uuid = { version = "0.8.2", default-features = false, features = ["v4", "serde"] } From 19b78be93f5be4d5f06163c3ac3606318f472f1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 10 Feb 2021 09:15:25 +0100 Subject: [PATCH 13/39] base: Fix a typo --- matrix_sdk_base/src/store/ambiguity_map.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix_sdk_base/src/store/ambiguity_map.rs b/matrix_sdk_base/src/store/ambiguity_map.rs index 2f7f2b91..4c84cdd2 100644 --- a/matrix_sdk_base/src/store/ambiguity_map.rs +++ b/matrix_sdk_base/src/store/ambiguity_map.rs @@ -195,7 +195,7 @@ impl AmbiguityCache { let old_display_name = if let Some(event) = old_event { if matches!(event.content.membership, Join | Invite) { - let dispaly_name = if let Some(d) = changes + let display_name = if let Some(d) = changes .profiles .get(room_id) .and_then(|p| p.get(&member_event.state_key)) @@ -213,7 +213,7 @@ impl AmbiguityCache { event.content.displayname.clone() }; - Some(dispaly_name.unwrap_or_else(|| event.state_key.localpart().to_string())) + Some(display_name.unwrap_or_else(|| event.state_key.localpart().to_string())) } else { None } From e3d1de8e6c44fe8ab0dc228895bdefe7b885e989 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 10 Feb 2021 09:51:14 +0100 Subject: [PATCH 14/39] client: Fix the sync_with_callback example --- matrix_sdk/Cargo.toml | 1 - matrix_sdk/src/client.rs | 11 ++++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/matrix_sdk/Cargo.toml b/matrix_sdk/Cargo.toml index 62452356..b15d84d7 100644 --- a/matrix_sdk/Cargo.toml +++ b/matrix_sdk/Cargo.toml @@ -72,7 +72,6 @@ version = "3.0.2" features = ["wasm-bindgen"] [dev-dependencies] -async-std = { version = "1.9.0", features = ["unstable"] } dirs = "3.0.1" matrix-sdk-test = { version = "0.2.0", path = "../matrix_sdk_test" } tokio = { version = "1.1.0", default-features = false, features = ["rt-multi-thread", "macros"] } diff --git a/matrix_sdk/src/client.rs b/matrix_sdk/src/client.rs index 016da325..b52f4bea 100644 --- a/matrix_sdk/src/client.rs +++ b/matrix_sdk/src/client.rs @@ -1702,11 +1702,10 @@ impl Client { /// the interesting events through a mpsc channel to another thread e.g. a /// UI thread. /// - /// ```compile_fail,E0658 + /// ```no_run /// # use matrix_sdk::events::{ /// # room::message::{MessageEvent, MessageEventContent, TextMessageEventContent}, /// # }; - /// # use matrix_sdk::Room; /// # use std::sync::{Arc, RwLock}; /// # use std::time::Duration; /// # use matrix_sdk::{Client, SyncSettings, LoopCtrl}; @@ -1716,7 +1715,7 @@ impl Client { /// # let homeserver = Url::parse("http://localhost:8080").unwrap(); /// # let mut client = Client::new(homeserver).unwrap(); /// - /// use async_std::sync::channel; + /// use tokio::sync::mpsc::channel; /// /// let (tx, rx) = channel(100); /// @@ -1725,14 +1724,12 @@ impl Client { /// .timeout(Duration::from_secs(30)); /// /// client - /// .sync_with_callback(sync_settings, async move |response| { + /// .sync_with_callback(sync_settings, |response| async move { /// let channel = sync_channel; /// /// for (room_id, room) in response.rooms.join { /// for event in room.timeline.events { - /// if let Ok(e) = event.deserialize() { - /// channel.send(e).await; - /// } + /// channel.send(event).await.unwrap(); /// } /// } /// From c34f69f8a3996d6b90d30ee3e1d1493492aa3354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 10 Feb 2021 15:42:55 +0100 Subject: [PATCH 15/39] crypto: Don't receive the whole sync response, only what we need. This makes it clearer what the crypto layer is doing, this also makes it clearer for people that will use the crypto layer over FFI that they don't need to go through a serialize/deserialize cycle for the whole sync response. --- matrix_sdk_base/src/client.rs | 7 ++++++- matrix_sdk_crypto/src/machine.rs | 31 +++++++++++++++++++++---------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/matrix_sdk_base/src/client.rs b/matrix_sdk_base/src/client.rs index 81b339c8..7e7d5f15 100644 --- a/matrix_sdk_base/src/client.rs +++ b/matrix_sdk_base/src/client.rs @@ -728,7 +728,12 @@ impl BaseClient { // decryptes to-device events, but leaves room events alone. // This makes sure that we have the deryption keys for the room // events at hand. - o.receive_sync_response(&response).await? + o.receive_sync_changes( + &response.to_device, + &response.device_lists, + &response.device_one_time_keys_count, + ) + .await? } else { response .to_device diff --git a/matrix_sdk_crypto/src/machine.rs b/matrix_sdk_crypto/src/machine.rs index b8405aa4..f54ce932 100644 --- a/matrix_sdk_crypto/src/machine.rs +++ b/matrix_sdk_crypto/src/machine.rs @@ -27,7 +27,7 @@ use matrix_sdk_common::{ upload_keys, upload_signatures::Request as UploadSignaturesRequest, }, - sync::sync_events::Response as SyncResponse, + sync::sync_events::{DeviceLists, ToDevice as RumaToDevice}, }, assign, deserialized_responses::ToDevice, @@ -763,19 +763,31 @@ impl OlmMachine { self.account.update_uploaded_key_count(key_count).await; } - /// Handle a sync response and update the internal state of the Olm machine. + /// Handle a to-device and one-time key counts from a sync response. /// - /// This will decrypt to-device events but will not touch events in the room - /// timeline. + /// This will decrypt and handle to-device events returning the decrypted + /// versions of them. /// /// To decrypt an event from the room timeline call [`decrypt_room_event`]. /// /// # Arguments /// - /// * `response` - The sync latest sync response. + /// * `to_device_events` - The to-device events of the current sync + /// response. + /// + /// * `changed_devices` - The list of devices that changed in this sync + /// resopnse. + /// + /// * `one_time_keys_count` - The current one-time keys counts that the sync + /// response returned. /// /// [`decrypt_room_event`]: #method.decrypt_room_event - pub async fn receive_sync_response(&self, response: &SyncResponse) -> OlmResult { + pub async fn receive_sync_changes( + &self, + to_device_events: &RumaToDevice, + changed_devices: &DeviceLists, + one_time_keys_counts: &BTreeMap, + ) -> OlmResult { // Remove verification objects that have expired or are done. self.verification_machine.garbage_collect(); @@ -786,10 +798,9 @@ impl OlmMachine { ..Default::default() }; - self.update_one_time_key_count(&response.device_one_time_keys_count) - .await; + self.update_one_time_key_count(one_time_keys_counts).await; - for user_id in &response.device_lists.changed { + for user_id in &changed_devices.changed { if let Err(e) = self.identity_manager.mark_user_as_changed(&user_id).await { error!("Error marking a tracked user as changed {:?}", e); } @@ -797,7 +808,7 @@ impl OlmMachine { let mut events = Vec::new(); - for event_result in &response.to_device.events { + for event_result in &to_device_events.events { let mut event = if let Ok(e) = event_result.deserialize() { e } else { From b7fda1deb7be10cdd8d60f126cb01411638a891f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 10 Feb 2021 20:57:26 +0100 Subject: [PATCH 16/39] base: Fix a typo in the room members --- matrix_sdk_base/src/rooms/members.rs | 4 ++-- matrix_sdk_base/src/rooms/normal.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/matrix_sdk_base/src/rooms/members.rs b/matrix_sdk_base/src/rooms/members.rs index 7363b947..c722566f 100644 --- a/matrix_sdk_base/src/rooms/members.rs +++ b/matrix_sdk_base/src/rooms/members.rs @@ -31,7 +31,7 @@ pub struct RoomMember { pub(crate) event: Arc, pub(crate) profile: Arc>, pub(crate) presence: Arc>, - pub(crate) power_levles: Arc>>, + pub(crate) power_levels: Arc>>, pub(crate) max_power_level: i64, pub(crate) is_room_creator: bool, pub(crate) display_name_ambiguous: bool, @@ -86,7 +86,7 @@ impl RoomMember { /// Get the power level of this member. pub fn power_level(&self) -> i64 { - self.power_levles + self.power_levels .as_ref() .as_ref() .map(|e| { diff --git a/matrix_sdk_base/src/rooms/normal.rs b/matrix_sdk_base/src/rooms/normal.rs index 2d546100..da7777fb 100644 --- a/matrix_sdk_base/src/rooms/normal.rs +++ b/matrix_sdk_base/src/rooms/normal.rs @@ -420,7 +420,7 @@ impl Room { event: member_event.into(), profile: profile.into(), presence: presence.into(), - power_levles: power.into(), + power_levels: power.into(), max_power_level, is_room_creator, display_name_ambiguous: ambiguous, From e8571721700f8f6c6d98855349db0cbd2d52f0c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 10 Feb 2021 21:32:33 +0100 Subject: [PATCH 17/39] base: Fix a couple of typos --- matrix_sdk_base/src/rooms/members.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix_sdk_base/src/rooms/members.rs b/matrix_sdk_base/src/rooms/members.rs index c722566f..800079b2 100644 --- a/matrix_sdk_base/src/rooms/members.rs +++ b/matrix_sdk_base/src/rooms/members.rs @@ -43,7 +43,7 @@ impl RoomMember { &self.event.state_key } - /// Get the display name of the member if ther is one. + /// Get the display name of the member if there is one. pub fn display_name(&self) -> Option<&str> { if let Some(p) = self.profile.as_ref() { p.displayname.as_deref() @@ -64,7 +64,7 @@ impl RoomMember { } } - /// Get the avatar url of the member, if ther is one. + /// Get the avatar url of the member, if there is one. pub fn avatar_url(&self) -> Option<&str> { match self.profile.as_ref() { Some(p) => p.avatar_url.as_deref(), From 2e7f862f9c3028d9d2e6d72b7e2432344851d1a4 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 12 Feb 2021 12:29:50 +0100 Subject: [PATCH 18/39] Delete .travis.yml CI has been moved to GitHub actions a while ago. --- .travis.yml | 89 ----------------------------------------------------- 1 file changed, 89 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a7f20867..00000000 --- a/.travis.yml +++ /dev/null @@ -1,89 +0,0 @@ -language: rust -rust: stable -addons: - apt: - packages: - - libssl-dev - -jobs: - allow_failures: - - os: osx - name: macOS 10.15 - - include: - - stage: Format - os: linux - before_script: - - rustup component add rustfmt - script: - - cargo fmt --all -- --check - - - stage: Clippy - os: linux - before_script: - - rustup component add clippy - script: - - cargo clippy --all-targets -- -D warnings - - - stage: Test - os: linux - - - os: windows - script: - - cd matrix_sdk - - cargo test --no-default-features --features "messages, native-tls" - - cd ../matrix_sdk_base - - cargo test --no-default-features --features "messages" - - - os: osx - - - os: linux - name: native-tls build - script: - - cd matrix_sdk - - cargo build --no-default-features --features "native-tls" - - - os: linux - name: rustls-tls build - script: - - cd matrix_sdk - - cargo build --no-default-features --features "rustls-tls" - - - os: osx - name: macOS 10.15 - osx_image: xcode12 - - - os: linux - name: Coverage - before_script: - - cargo install cargo-tarpaulin - script: - - cargo tarpaulin --ignore-config --exclude-files "matrix_sdk/examples/*,matrix_sdk_common,matrix_sdk_test" --out Xml - after_success: - - bash <(curl -s https://codecov.io/bash) - - - os: linux - name: wasm32-unknown-unknown - before_script: - - | - set -e - cargo install wasm-bindgen-cli - rustup target add wasm32-unknown-unknown - wget https://github.com/emscripten-core/emsdk/archive/master.zip - unzip master.zip - ./emsdk-master/emsdk install latest - ./emsdk-master/emsdk activate latest - script: - - | - set -e - source emsdk-master/emsdk_env.sh - cd matrix_sdk/examples/wasm_command_bot - cargo build --target wasm32-unknown-unknown - cd - - - cd matrix_sdk_base - cargo test --target wasm32-unknown-unknown --no-default-features - -script: - - cargo build - - cargo test From 2811c490a0956df7a5d148b53430fb54f9c8b850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 12 Feb 2021 12:59:53 +0100 Subject: [PATCH 19/39] matrix-sdk: Fix some new clippy warnings --- matrix_sdk_base/examples/state_inspector.rs | 6 ++--- matrix_sdk_base/src/client.rs | 25 ++++++++----------- .../src/verification/requests.rs | 1 + 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/matrix_sdk_base/examples/state_inspector.rs b/matrix_sdk_base/examples/state_inspector.rs index b881629a..9b3ba08c 100644 --- a/matrix_sdk_base/examples/state_inspector.rs +++ b/matrix_sdk_base/examples/state_inspector.rs @@ -1,4 +1,4 @@ -use std::{convert::TryFrom, fmt::Debug, io, sync::Arc}; +use std::{convert::TryFrom, fmt::Debug, sync::Arc}; use futures::executor::block_on; use serde::Serialize; @@ -388,7 +388,7 @@ impl Inspector { } } -fn main() -> io::Result<()> { +fn main() { let argparse = Argparse::new("state-inspector") .global_setting(ArgParseSettings::DisableVersion) .global_setting(ArgParseSettings::VersionlessSubcommands) @@ -430,6 +430,4 @@ fn main() -> io::Result<()> { } else { block_on(inspector.run(matches)); } - - Ok(()) } diff --git a/matrix_sdk_base/src/client.rs b/matrix_sdk_base/src/client.rs index 7e7d5f15..0e0da616 100644 --- a/matrix_sdk_base/src/client.rs +++ b/matrix_sdk_base/src/client.rs @@ -495,20 +495,17 @@ impl BaseClient { }, #[cfg(feature = "encryption")] - AnySyncRoomEvent::Message(message) => { - if let AnySyncMessageEvent::RoomEncrypted(encrypted) = message { - if let Some(olm) = self.olm_machine().await { - if let Ok(decrypted) = - olm.decrypt_room_event(encrypted, room_id).await - { - match decrypted.deserialize() { - Ok(decrypted) => e = decrypted, - Err(e) => { - warn!( - "Error deserializing a decrypted event {:?} ", - e - ) - } + AnySyncRoomEvent::Message(AnySyncMessageEvent::RoomEncrypted( + encrypted, + )) => { + if let Some(olm) = self.olm_machine().await { + if let Ok(decrypted) = + olm.decrypt_room_event(encrypted, room_id).await + { + match decrypted.deserialize() { + Ok(decrypted) => e = decrypted, + Err(e) => { + warn!("Error deserializing a decrypted event {:?} ", e) } } } diff --git a/matrix_sdk_crypto/src/verification/requests.rs b/matrix_sdk_crypto/src/verification/requests.rs index 51098de0..18abfac4 100644 --- a/matrix_sdk_crypto/src/verification/requests.rs +++ b/matrix_sdk_crypto/src/verification/requests.rs @@ -134,6 +134,7 @@ impl VerificationRequest { self.inner.lock().unwrap().accept() } + #[allow(clippy::unnecessary_wraps)] pub(crate) fn receive_ready( &self, sender: &UserId, From e3e48148f02201e15ac22557945198ce9b35aa27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Barreteau?= Date: Sat, 13 Feb 2021 10:43:42 +0100 Subject: [PATCH 20/39] Rename `add_event_emitter` to `set_event_emitter` Closes #145. --- matrix_sdk/examples/autojoin.rs | 2 +- matrix_sdk/examples/command_bot.rs | 2 +- matrix_sdk/examples/image_bot.rs | 2 +- matrix_sdk/examples/login.rs | 2 +- matrix_sdk/src/client.rs | 4 ++-- matrix_sdk_base/src/client.rs | 2 +- matrix_sdk_base/src/event_emitter/mod.rs | 10 +++++----- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/matrix_sdk/examples/autojoin.rs b/matrix_sdk/examples/autojoin.rs index c8f376ee..0e0a0509 100644 --- a/matrix_sdk/examples/autojoin.rs +++ b/matrix_sdk/examples/autojoin.rs @@ -78,7 +78,7 @@ async fn login_and_sync( println!("logged in as {}", username); client - .add_event_emitter(Box::new(AutoJoinBot::new(client.clone()))) + .set_event_emitter(Box::new(AutoJoinBot::new(client.clone()))) .await; client.sync(SyncSettings::default()).await; diff --git a/matrix_sdk/examples/command_bot.rs b/matrix_sdk/examples/command_bot.rs index 0afca9b9..d4dbfa43 100644 --- a/matrix_sdk/examples/command_bot.rs +++ b/matrix_sdk/examples/command_bot.rs @@ -88,7 +88,7 @@ async fn login_and_sync( // 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. client - .add_event_emitter(Box::new(CommandBot::new(client.clone()))) + .set_event_emitter(Box::new(CommandBot::new(client.clone()))) .await; // since we called `sync_once` before we entered our sync loop we must pass diff --git a/matrix_sdk/examples/image_bot.rs b/matrix_sdk/examples/image_bot.rs index 344495df..bd81a033 100644 --- a/matrix_sdk/examples/image_bot.rs +++ b/matrix_sdk/examples/image_bot.rs @@ -86,7 +86,7 @@ async fn login_and_sync( client.sync_once(SyncSettings::default()).await.unwrap(); client - .add_event_emitter(Box::new(ImageBot::new(client.clone(), image))) + .set_event_emitter(Box::new(ImageBot::new(client.clone(), image))) .await; let settings = SyncSettings::default().token(client.sync_token().await.unwrap()); diff --git a/matrix_sdk/examples/login.rs b/matrix_sdk/examples/login.rs index 37440ac2..4b253df8 100644 --- a/matrix_sdk/examples/login.rs +++ b/matrix_sdk/examples/login.rs @@ -44,7 +44,7 @@ async fn login( let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL"); let client = Client::new(homeserver_url).unwrap(); - client.add_event_emitter(Box::new(EventCallback)).await; + client.set_event_emitter(Box::new(EventCallback)).await; client .login(username, password, None, Some("rust-sdk")) diff --git a/matrix_sdk/src/client.rs b/matrix_sdk/src/client.rs index b52f4bea..463e8269 100644 --- a/matrix_sdk/src/client.rs +++ b/matrix_sdk/src/client.rs @@ -556,8 +556,8 @@ impl Client { /// Add `EventEmitter` to `Client`. /// /// The methods of `EventEmitter` are called when the respective `RoomEvents` occur. - pub async fn add_event_emitter(&self, emitter: Box) { - self.base_client.add_event_emitter(emitter).await; + pub async fn set_event_emitter(&self, emitter: Box) { + self.base_client.set_event_emitter(emitter).await; } /// Returns the joined rooms this client knows about. diff --git a/matrix_sdk_base/src/client.rs b/matrix_sdk_base/src/client.rs index 0e0da616..0885bd6d 100644 --- a/matrix_sdk_base/src/client.rs +++ b/matrix_sdk_base/src/client.rs @@ -430,7 +430,7 @@ impl BaseClient { /// Add `EventEmitter` to `Client`. /// /// The methods of `EventEmitter` are called when the respective `RoomEvents` occur. - pub async fn add_event_emitter(&self, emitter: Box) { + pub async fn set_event_emitter(&self, emitter: Box) { let emitter = Emitter { inner: emitter, store: self.store.clone(), diff --git a/matrix_sdk_base/src/event_emitter/mod.rs b/matrix_sdk_base/src/event_emitter/mod.rs index db5f4e58..1bf7830d 100644 --- a/matrix_sdk_base/src/event_emitter/mod.rs +++ b/matrix_sdk_base/src/event_emitter/mod.rs @@ -746,7 +746,7 @@ mod test { let emitter = Box::new(EvEmitterTest(vec)); let client = get_client().await; - client.add_event_emitter(emitter).await; + client.set_event_emitter(emitter).await; let response = sync_response(SyncResponseFile::Default); client.receive_sync_response(response).await.unwrap(); @@ -778,7 +778,7 @@ mod test { let emitter = Box::new(EvEmitterTest(vec)); let client = get_client().await; - client.add_event_emitter(emitter).await; + client.set_event_emitter(emitter).await; let response = sync_response(SyncResponseFile::Invite); client.receive_sync_response(response).await.unwrap(); @@ -801,7 +801,7 @@ mod test { let emitter = Box::new(EvEmitterTest(vec)); let client = get_client().await; - client.add_event_emitter(emitter).await; + client.set_event_emitter(emitter).await; let response = sync_response(SyncResponseFile::Leave); client.receive_sync_response(response).await.unwrap(); @@ -831,7 +831,7 @@ mod test { let emitter = Box::new(EvEmitterTest(vec)); let client = get_client().await; - client.add_event_emitter(emitter).await; + client.set_event_emitter(emitter).await; let response = sync_response(SyncResponseFile::All); client.receive_sync_response(response).await.unwrap(); @@ -856,7 +856,7 @@ mod test { let emitter = Box::new(EvEmitterTest(vec)); let client = get_client().await; - client.add_event_emitter(emitter).await; + client.set_event_emitter(emitter).await; let response = sync_response(SyncResponseFile::Voip); client.receive_sync_response(response).await.unwrap(); From b6f2c43330f55b1286be3bd2fa52eab0ab48e084 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Barreteau?= Date: Sat, 13 Feb 2021 11:01:31 +0100 Subject: [PATCH 21/39] Rename `EventEmitter` to `EventHandler` --- design.md | 8 +- matrix_sdk/examples/autojoin.rs | 6 +- matrix_sdk/examples/command_bot.rs | 8 +- matrix_sdk/examples/image_bot.rs | 6 +- matrix_sdk/examples/login.rs | 6 +- matrix_sdk/src/client.rs | 10 +-- matrix_sdk/src/lib.rs | 2 +- matrix_sdk_base/src/client.rs | 28 +++---- .../{event_emitter => event_handler}/mod.rs | 82 +++++++++---------- matrix_sdk_base/src/lib.rs | 4 +- 10 files changed, 80 insertions(+), 80 deletions(-) rename matrix_sdk_base/src/{event_emitter => event_handler}/mod.rs (93%) diff --git a/design.md b/design.md index fa084fa9..21d12d90 100644 --- a/design.md +++ b/design.md @@ -22,7 +22,7 @@ In addition to Http, the `AsyncClient` passes along methods from the `BaseClient 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. +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 forwarding the incoming events to the `EventHandler` trait. ```rust /// A Matrix room. @@ -95,6 +95,6 @@ The `BaseClient` also has access to a `dyn StateStore` this is an abstraction ar 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. - - list the methods for `EventEmitter`? +#### Event Handler +The consumer of this crate can implement the `EventHandler` 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 `EventHandler`? diff --git a/matrix_sdk/examples/autojoin.rs b/matrix_sdk/examples/autojoin.rs index 0e0a0509..b28f33a0 100644 --- a/matrix_sdk/examples/autojoin.rs +++ b/matrix_sdk/examples/autojoin.rs @@ -4,7 +4,7 @@ use tokio::time::{sleep, Duration}; use matrix_sdk::{ self, async_trait, events::{room::member::MemberEventContent, StrippedStateEvent}, - Client, ClientConfig, EventEmitter, RoomState, SyncSettings, + Client, ClientConfig, EventHandler, RoomState, SyncSettings, }; use url::Url; @@ -19,7 +19,7 @@ impl AutoJoinBot { } #[async_trait] -impl EventEmitter for AutoJoinBot { +impl EventHandler for AutoJoinBot { async fn on_stripped_state_member( &self, room: RoomState, @@ -78,7 +78,7 @@ async fn login_and_sync( println!("logged in as {}", username); client - .set_event_emitter(Box::new(AutoJoinBot::new(client.clone()))) + .set_event_handler(Box::new(AutoJoinBot::new(client.clone()))) .await; client.sync(SyncSettings::default()).await; diff --git a/matrix_sdk/examples/command_bot.rs b/matrix_sdk/examples/command_bot.rs index d4dbfa43..9f568a87 100644 --- a/matrix_sdk/examples/command_bot.rs +++ b/matrix_sdk/examples/command_bot.rs @@ -6,7 +6,7 @@ use matrix_sdk::{ room::message::{MessageEventContent, TextMessageEventContent}, AnyMessageEventContent, SyncMessageEvent, }, - Client, ClientConfig, EventEmitter, RoomState, SyncSettings, + Client, ClientConfig, EventHandler, RoomState, SyncSettings, }; use url::Url; @@ -23,7 +23,7 @@ impl CommandBot { } #[async_trait] -impl EventEmitter for CommandBot { +impl EventHandler for CommandBot { async fn on_room_message( &self, room: RoomState, @@ -88,13 +88,13 @@ async fn login_and_sync( // 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. client - .set_event_emitter(Box::new(CommandBot::new(client.clone()))) + .set_event_handler(Box::new(CommandBot::new(client.clone()))) .await; // since we called `sync_once` before we entered our sync loop we must pass // that sync token to `sync` let settings = SyncSettings::default().token(client.sync_token().await.unwrap()); - // this keeps state from the server streaming in to CommandBot via the EventEmitter trait + // this keeps state from the server streaming in to CommandBot via the EventHandler trait client.sync(settings).await; Ok(()) diff --git a/matrix_sdk/examples/image_bot.rs b/matrix_sdk/examples/image_bot.rs index bd81a033..68de1fcd 100644 --- a/matrix_sdk/examples/image_bot.rs +++ b/matrix_sdk/examples/image_bot.rs @@ -14,7 +14,7 @@ use matrix_sdk::{ room::message::{MessageEventContent, TextMessageEventContent}, SyncMessageEvent, }, - Client, EventEmitter, RoomState, SyncSettings, + Client, EventHandler, RoomState, SyncSettings, }; use url::Url; @@ -31,7 +31,7 @@ impl ImageBot { } #[async_trait] -impl EventEmitter for ImageBot { +impl EventHandler for ImageBot { async fn on_room_message( &self, room: RoomState, @@ -86,7 +86,7 @@ async fn login_and_sync( client.sync_once(SyncSettings::default()).await.unwrap(); client - .set_event_emitter(Box::new(ImageBot::new(client.clone(), image))) + .set_event_handler(Box::new(ImageBot::new(client.clone(), image))) .await; let settings = SyncSettings::default().token(client.sync_token().await.unwrap()); diff --git a/matrix_sdk/examples/login.rs b/matrix_sdk/examples/login.rs index 4b253df8..3a998426 100644 --- a/matrix_sdk/examples/login.rs +++ b/matrix_sdk/examples/login.rs @@ -7,13 +7,13 @@ use matrix_sdk::{ room::message::{MessageEventContent, TextMessageEventContent}, SyncMessageEvent, }, - Client, EventEmitter, RoomState, SyncSettings, + Client, EventHandler, RoomState, SyncSettings, }; struct EventCallback; #[async_trait] -impl EventEmitter for EventCallback { +impl EventHandler for EventCallback { async fn on_room_message( &self, room: RoomState, @@ -44,7 +44,7 @@ async fn login( let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL"); let client = Client::new(homeserver_url).unwrap(); - client.set_event_emitter(Box::new(EventCallback)).await; + client.set_event_handler(Box::new(EventCallback)).await; client .login(username, password, None, Some("rust-sdk")) diff --git a/matrix_sdk/src/client.rs b/matrix_sdk/src/client.rs index 463e8269..d7f111a9 100644 --- a/matrix_sdk/src/client.rs +++ b/matrix_sdk/src/client.rs @@ -41,7 +41,7 @@ use tracing::{error, info, instrument}; use matrix_sdk_base::{ deserialized_responses::{MembersResponse, SyncResponse}, - BaseClient, BaseClientConfig, EventEmitter, InvitedRoom, JoinedRoom, LeftRoom, Session, Store, + BaseClient, BaseClientConfig, EventHandler, InvitedRoom, JoinedRoom, LeftRoom, Session, Store, }; #[cfg(feature = "encryption")] @@ -553,11 +553,11 @@ impl Client { Ok(()) } - /// Add `EventEmitter` to `Client`. + /// Add `EventHandler` to `Client`. /// - /// The methods of `EventEmitter` are called when the respective `RoomEvents` occur. - pub async fn set_event_emitter(&self, emitter: Box) { - self.base_client.set_event_emitter(emitter).await; + /// The methods of `EventHandler` are called when the respective `RoomEvents` occur. + pub async fn set_event_handler(&self, handler: Box) { + self.base_client.set_event_handler(handler).await; } /// Returns the joined rooms this client knows about. diff --git a/matrix_sdk/src/lib.rs b/matrix_sdk/src/lib.rs index 98a56188..7b265886 100644 --- a/matrix_sdk/src/lib.rs +++ b/matrix_sdk/src/lib.rs @@ -68,7 +68,7 @@ compile_error!("only one of 'native-tls' or 'rustls-tls' features can be enabled #[cfg_attr(feature = "docs", doc(cfg(encryption)))] pub use matrix_sdk_base::crypto::LocalTrust; pub use matrix_sdk_base::{ - CustomEvent, Error as BaseError, EventEmitter, InvitedRoom, JoinedRoom, LeftRoom, RoomInfo, + CustomEvent, Error as BaseError, EventHandler, InvitedRoom, JoinedRoom, LeftRoom, RoomInfo, RoomMember, RoomState, Session, StateChanges, StoreError, }; diff --git a/matrix_sdk_base/src/client.rs b/matrix_sdk_base/src/client.rs index 0885bd6d..43c7c796 100644 --- a/matrix_sdk_base/src/client.rs +++ b/matrix_sdk_base/src/client.rs @@ -59,11 +59,11 @@ use zeroize::Zeroizing; use crate::{ error::Result, - event_emitter::Emitter, + event_handler::Handler, rooms::{RoomInfo, RoomType, StrippedRoomInfo}, session::Session, store::{ambiguity_map::AmbiguityCache, Result as StoreResult, StateChanges, Store}, - EventEmitter, RoomState, + EventHandler, RoomState, }; pub type Token = String; @@ -150,7 +150,7 @@ fn hoist_room_event_prev_content( Ok(ev) } -/// Signals to the `BaseClient` which `RoomState` to send to `EventEmitter`. +/// Signals to the `BaseClient` which `RoomState` to send to `EventHandler`. #[derive(Debug)] pub enum RoomStateType { /// Represents a joined room, the `joined_rooms` HashMap will be used. @@ -180,9 +180,9 @@ pub struct BaseClient { cryptostore: Arc>>>, store_path: Arc>, store_passphrase: Arc>>, - /// Any implementor of EventEmitter will act as the callbacks for various + /// Any implementor of EventHandler will act as the callbacks for various /// events. - event_emitter: Arc>>, + event_handler: Arc>>, } #[cfg(not(tarpaulin_include))] @@ -328,7 +328,7 @@ impl BaseClient { cryptostore: Mutex::new(crypto_store).into(), store_path: config.store_path.into(), store_passphrase: config.passphrase.into(), - event_emitter: RwLock::new(None).into(), + event_handler: RwLock::new(None).into(), }) } @@ -427,15 +427,15 @@ impl BaseClient { self.sync_token.read().await.clone() } - /// Add `EventEmitter` to `Client`. + /// Add `EventHandler` to `Client`. /// - /// The methods of `EventEmitter` are called when the respective `RoomEvents` occur. - pub async fn set_event_emitter(&self, emitter: Box) { - let emitter = Emitter { - inner: emitter, + /// The methods of `EventHandler` are called when the respective `RoomEvents` occur. + pub async fn set_event_handler(&self, handler: Box) { + let handler = Handler { + inner: handler, store: self.store.clone(), }; - *self.event_emitter.write().await = Some(emitter); + *self.event_handler.write().await = Some(handler); } async fn handle_timeline( @@ -942,8 +942,8 @@ impl BaseClient { }, }; - if let Some(emitter) = self.event_emitter.read().await.as_ref() { - emitter.emit_sync(&response).await; + if let Some(handler) = self.event_handler.read().await.as_ref() { + handler.handle_sync(&response).await; } Ok(response) diff --git a/matrix_sdk_base/src/event_emitter/mod.rs b/matrix_sdk_base/src/event_handler/mod.rs similarity index 93% rename from matrix_sdk_base/src/event_emitter/mod.rs rename to matrix_sdk_base/src/event_handler/mod.rs index 1bf7830d..6ec1d8a6 100644 --- a/matrix_sdk_base/src/event_emitter/mod.rs +++ b/matrix_sdk_base/src/event_handler/mod.rs @@ -52,41 +52,41 @@ use crate::{ }; use matrix_sdk_common::async_trait; -pub(crate) struct Emitter { - pub(crate) inner: Box, +pub(crate) struct Handler { + pub(crate) inner: Box, pub(crate) store: Store, } -impl Deref for Emitter { - type Target = dyn EventEmitter; +impl Deref for Handler { + type Target = dyn EventHandler; fn deref(&self) -> &Self::Target { &*self.inner } } -impl Emitter { +impl Handler { fn get_room(&self, room_id: &RoomId) -> Option { self.store.get_room(room_id) } - pub(crate) async fn emit_sync(&self, response: &SyncResponse) { + pub(crate) async fn handle_sync(&self, response: &SyncResponse) { for (room_id, room_info) in &response.rooms.join { if let Some(room) = self.get_room(room_id) { for event in &room_info.ephemeral.events { - self.emit_ephemeral_event(room.clone(), event).await; + self.handle_ephemeral_event(room.clone(), event).await; } for event in &room_info.account_data.events { - self.emit_account_data_event(room.clone(), event).await; + self.handle_account_data_event(room.clone(), event).await; } for event in &room_info.state.events { - self.emit_state_event(room.clone(), event).await; + self.handle_state_event(room.clone(), event).await; } for event in &room_info.timeline.events { - self.emit_timeline_event(room.clone(), event).await; + self.handle_timeline_event(room.clone(), event).await; } } } @@ -94,15 +94,15 @@ impl Emitter { for (room_id, room_info) in &response.rooms.leave { if let Some(room) = self.get_room(room_id) { for event in &room_info.account_data.events { - self.emit_account_data_event(room.clone(), event).await; + self.handle_account_data_event(room.clone(), event).await; } for event in &room_info.state.events { - self.emit_state_event(room.clone(), event).await; + self.handle_state_event(room.clone(), event).await; } for event in &room_info.timeline.events { - self.emit_timeline_event(room.clone(), event).await; + self.handle_timeline_event(room.clone(), event).await; } } } @@ -110,7 +110,7 @@ impl Emitter { for (room_id, room_info) in &response.rooms.invite { if let Some(room) = self.get_room(room_id) { for event in &room_info.invite_state.events { - self.emit_stripped_state_event(room.clone(), event).await; + self.handle_stripped_state_event(room.clone(), event).await; } } } @@ -120,7 +120,7 @@ impl Emitter { } } - async fn emit_timeline_event(&self, room: RoomState, event: &AnySyncRoomEvent) { + async fn handle_timeline_event(&self, room: RoomState, event: &AnySyncRoomEvent) { match event { AnySyncRoomEvent::State(event) => match event { AnySyncStateEvent::RoomMember(e) => self.on_room_member(room, e).await, @@ -160,7 +160,7 @@ impl Emitter { } } - async fn emit_state_event(&self, room: RoomState, event: &AnySyncStateEvent) { + async fn handle_state_event(&self, room: RoomState, event: &AnySyncStateEvent) { match event { AnySyncStateEvent::RoomMember(member) => self.on_state_member(room, &member).await, AnySyncStateEvent::RoomName(name) => self.on_state_name(room, &name).await, @@ -185,9 +185,9 @@ impl Emitter { } } - pub(crate) async fn emit_stripped_state_event( + pub(crate) async fn handle_stripped_state_event( &self, - // TODO these events are only emitted in invited rooms. + // TODO these events are only handleted in invited rooms. room: RoomState, event: &AnyStrippedStateEvent, ) { @@ -216,7 +216,7 @@ impl Emitter { } } - pub(crate) async fn emit_account_data_event(&self, room: RoomState, event: &AnyBasicEvent) { + pub(crate) async fn handle_account_data_event(&self, room: RoomState, event: &AnyBasicEvent) { match event { AnyBasicEvent::Presence(presence) => self.on_non_room_presence(room, &presence).await, AnyBasicEvent::IgnoredUserList(ignored) => { @@ -227,7 +227,7 @@ impl Emitter { } } - pub(crate) async fn emit_ephemeral_event( + pub(crate) async fn handle_ephemeral_event( &self, room: RoomState, event: &AnySyncEphemeralRoomEvent, @@ -262,7 +262,7 @@ pub enum CustomEvent<'c> { StrippedState(&'c StrippedStateEvent), } -/// This trait allows any type implementing `EventEmitter` to specify event callbacks for each event. +/// This trait allows any type implementing `EventHandler` to specify event callbacks for each event. /// The `Client` calls each method when the corresponding event is received. /// /// # Examples @@ -276,14 +276,14 @@ pub enum CustomEvent<'c> { /// # room::message::{MessageEventContent, TextMessageEventContent}, /// # SyncMessageEvent /// # }, -/// # EventEmitter, RoomState +/// # EventHandler, RoomState /// # }; /// # use matrix_sdk_common::{async_trait, locks::RwLock}; /// /// struct EventCallback; /// /// #[async_trait] -/// impl EventEmitter for EventCallback { +/// impl EventHandler for EventCallback { /// async fn on_room_message(&self, room: RoomState, event: &SyncMessageEvent) { /// if let RoomState::Joined(room) = room { /// if let SyncMessageEvent { @@ -304,7 +304,7 @@ pub enum CustomEvent<'c> { /// ``` #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] -pub trait EventEmitter: Send + Sync { +pub trait EventHandler: Send + Sync { // ROOM EVENTS from `IncomingTimeline` /// Fires when `Client` receives a `RoomEvent::RoomMember` event. async fn on_room_member(&self, _: RoomState, _: &SyncStateEvent) {} @@ -496,11 +496,11 @@ mod test { pub use wasm_bindgen_test::*; #[derive(Clone)] - pub struct EvEmitterTest(Arc>>); + pub struct EvHandlerTest(Arc>>); #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] - impl EventEmitter for EvEmitterTest { + impl EventHandler for EvHandlerTest { async fn on_room_member(&self, _: RoomState, _: &SyncStateEvent) { self.0.lock().await.push("member".to_string()) } @@ -740,13 +740,13 @@ mod test { } #[async_test] - async fn event_emitter_joined() { + async fn event_handler_joined() { let vec = Arc::new(Mutex::new(Vec::new())); let test_vec = Arc::clone(&vec); - let emitter = Box::new(EvEmitterTest(vec)); + let handler = Box::new(EvHandlerTest(vec)); let client = get_client().await; - client.set_event_emitter(emitter).await; + client.set_event_handler(handler).await; let response = sync_response(SyncResponseFile::Default); client.receive_sync_response(response).await.unwrap(); @@ -772,13 +772,13 @@ mod test { } #[async_test] - async fn event_emitter_invite() { + async fn event_handler_invite() { let vec = Arc::new(Mutex::new(Vec::new())); let test_vec = Arc::clone(&vec); - let emitter = Box::new(EvEmitterTest(vec)); + let handler = Box::new(EvHandlerTest(vec)); let client = get_client().await; - client.set_event_emitter(emitter).await; + client.set_event_handler(handler).await; let response = sync_response(SyncResponseFile::Invite); client.receive_sync_response(response).await.unwrap(); @@ -795,13 +795,13 @@ mod test { } #[async_test] - async fn event_emitter_leave() { + async fn event_handler_leave() { let vec = Arc::new(Mutex::new(Vec::new())); let test_vec = Arc::clone(&vec); - let emitter = Box::new(EvEmitterTest(vec)); + let handler = Box::new(EvHandlerTest(vec)); let client = get_client().await; - client.set_event_emitter(emitter).await; + client.set_event_handler(handler).await; let response = sync_response(SyncResponseFile::Leave); client.receive_sync_response(response).await.unwrap(); @@ -825,13 +825,13 @@ mod test { } #[async_test] - async fn event_emitter_more_events() { + async fn event_handler_more_events() { let vec = Arc::new(Mutex::new(Vec::new())); let test_vec = Arc::clone(&vec); - let emitter = Box::new(EvEmitterTest(vec)); + let handler = Box::new(EvHandlerTest(vec)); let client = get_client().await; - client.set_event_emitter(emitter).await; + client.set_event_handler(handler).await; let response = sync_response(SyncResponseFile::All); client.receive_sync_response(response).await.unwrap(); @@ -850,13 +850,13 @@ mod test { } #[async_test] - async fn event_emitter_voip() { + async fn event_handler_voip() { let vec = Arc::new(Mutex::new(Vec::new())); let test_vec = Arc::clone(&vec); - let emitter = Box::new(EvEmitterTest(vec)); + let handler = Box::new(EvHandlerTest(vec)); let client = get_client().await; - client.set_event_emitter(emitter).await; + client.set_event_handler(handler).await; let response = sync_response(SyncResponseFile::Voip); client.receive_sync_response(response).await.unwrap(); diff --git a/matrix_sdk_base/src/lib.rs b/matrix_sdk_base/src/lib.rs index 32cffeaa..197e007f 100644 --- a/matrix_sdk_base/src/lib.rs +++ b/matrix_sdk_base/src/lib.rs @@ -46,12 +46,12 @@ pub use matrix_sdk_common::*; mod client; mod error; -mod event_emitter; +mod event_handler; mod rooms; mod session; mod store; -pub use event_emitter::{CustomEvent, EventEmitter}; +pub use event_handler::{CustomEvent, EventHandler}; pub use rooms::{ InvitedRoom, JoinedRoom, LeftRoom, Room, RoomInfo, RoomMember, RoomState, StrippedRoom, StrippedRoomInfo, From fe11ad7e3ea63216f85de218b383b450efe6dd8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 15 Feb 2021 09:44:10 +0100 Subject: [PATCH 22/39] matrix-sdk: Remove the design doc for now It's outdated and somewhat misleading, so remove it for now until we have a new one with pictures and stuff. Closes: #139 --- design.md | 100 ------------------------------------------------------ 1 file changed, 100 deletions(-) delete mode 100644 design.md diff --git a/design.md b/design.md deleted file mode 100644 index 21d12d90..00000000 --- a/design.md +++ /dev/null @@ -1,100 +0,0 @@ -# Matrix Rust SDK - -## 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 - - login - - send messages - - encryption ... - - sync client state with the server - - 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. - - human-readable room names - - power level? - - ignored list? - - push rulesset? - - more? - -#### Crypto State Machine -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 forwarding the incoming events to the `EventHandler` trait. - -```rust -/// A Matrix room. -pub struct Room { - /// The unique id of the room. - pub room_id: RoomId, - /// The name of the room, clients use this to represent a room. - pub room_name: RoomName, - /// The mxid of our own user. - pub own_user_id: UserId, - /// The mxid of the room creator. - pub creator: Option, - /// The map of room members. - pub members: HashMap, - /// A list of users that are currently typing. - pub typing_users: Vec, - /// The power level requirements for specific actions in this room - pub power_levels: Option, - // TODO when encryption events are handled we store algorithm used and rotation time. - /// A flag indicating if the room is encrypted. - pub encrypted: bool, - /// Number of unread notifications with highlight flag set. - pub unread_highlight: Option, - /// Number of unread notifications. - pub unread_notifications: Option, -} -``` - -```rust -pub struct RoomMember { - /// The unique mxid of the user. - pub user_id: UserId, - /// The human readable name of the user. - pub display_name: Option, - /// The matrix url of the users avatar. - pub avatar_url: Option, - /// The time, in ms, since the user interacted with the server. - pub last_active_ago: Option, - /// If the user should be considered active. - pub currently_active: Option, - /// The unique id of the room. - pub room_id: Option, - /// If the member is typing. - pub typing: Option, - /// The presence of the user, if found. - pub presence: Option, - /// The presence status message, if found. - pub status_msg: Option, - /// The users power level. - pub power_level: Option, - /// The normalized power level of this `RoomMember` (0-100). - pub power_level_norm: Option, - /// The `MembershipState` of this `RoomMember`. - pub membership: MembershipState, - /// The human readable name of this room member. - pub name: String, - /// The events that created the state of this room member. - pub events: Vec, - /// The `PresenceEvent`s connected to this user. - pub presence_events: Vec, -} -``` - -#### 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/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 Handler -The consumer of this crate can implement the `EventHandler` 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 `EventHandler`? From c39fa6543fe1310a007962876f472fe1994c05ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 15 Feb 2021 15:19:48 +0100 Subject: [PATCH 23/39] crypto: Expose the EncryptionInfo struct publicly --- matrix_sdk_crypto/src/file_encryption/attachments.rs | 6 ++++++ matrix_sdk_crypto/src/file_encryption/mod.rs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/matrix_sdk_crypto/src/file_encryption/attachments.rs b/matrix_sdk_crypto/src/file_encryption/attachments.rs index b578d71c..1ea0469e 100644 --- a/matrix_sdk_crypto/src/file_encryption/attachments.rs +++ b/matrix_sdk_crypto/src/file_encryption/attachments.rs @@ -257,12 +257,18 @@ impl<'a, R: Read + 'a> AttachmentEncryptor<'a, R> { } } +/// Struct holding all the information that is needed to decrypt an encrypted +/// file. #[derive(Debug, Serialize, Deserialize)] pub struct EncryptionInfo { #[serde(rename = "v")] + /// The version of the encryption scheme. pub version: String, + /// The web key that was used to encrypt the file. pub web_key: JsonWebKey, + /// The initialization vector that was used to encrypt the file. pub iv: String, + /// The hashes that can be used to check the validity of the file. pub hashes: BTreeMap, } diff --git a/matrix_sdk_crypto/src/file_encryption/mod.rs b/matrix_sdk_crypto/src/file_encryption/mod.rs index 41e0b045..481659dd 100644 --- a/matrix_sdk_crypto/src/file_encryption/mod.rs +++ b/matrix_sdk_crypto/src/file_encryption/mod.rs @@ -1,5 +1,5 @@ mod attachments; mod key_export; -pub use attachments::{AttachmentDecryptor, AttachmentEncryptor, DecryptorError}; +pub use attachments::{AttachmentDecryptor, AttachmentEncryptor, DecryptorError, EncryptionInfo}; pub use key_export::{decrypt_key_export, encrypt_key_export}; From 1db89741bc56103d9733fecef9befece2d3eb1cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 16 Feb 2021 09:42:23 +0100 Subject: [PATCH 24/39] matrix-sdk: Re-export the EncryptionInfo struct --- matrix_sdk/src/lib.rs | 2 +- matrix_sdk_crypto/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix_sdk/src/lib.rs b/matrix_sdk/src/lib.rs index 7b265886..29719837 100644 --- a/matrix_sdk/src/lib.rs +++ b/matrix_sdk/src/lib.rs @@ -66,7 +66,7 @@ compile_error!("only one of 'native-tls' or 'rustls-tls' features can be enabled #[cfg(feature = "encryption")] #[cfg_attr(feature = "docs", doc(cfg(encryption)))] -pub use matrix_sdk_base::crypto::LocalTrust; +pub use matrix_sdk_base::crypto::{EncryptionInfo, LocalTrust}; pub use matrix_sdk_base::{ CustomEvent, Error as BaseError, EventHandler, InvitedRoom, JoinedRoom, LeftRoom, RoomInfo, RoomMember, RoomState, Session, StateChanges, StoreError, diff --git a/matrix_sdk_crypto/src/lib.rs b/matrix_sdk_crypto/src/lib.rs index e4262e3c..ce262a2c 100644 --- a/matrix_sdk_crypto/src/lib.rs +++ b/matrix_sdk_crypto/src/lib.rs @@ -42,7 +42,7 @@ mod verification; pub use error::{MegolmError, OlmError}; pub use file_encryption::{ decrypt_key_export, encrypt_key_export, AttachmentDecryptor, AttachmentEncryptor, - DecryptorError, + DecryptorError, EncryptionInfo, }; pub use identities::{ Device, LocalTrust, OwnUserIdentity, ReadOnlyDevice, UserDevices, UserIdentities, UserIdentity, From ef5d7ca5794fa28c2ebc0dc5c35ed6e8b7a6141e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 16 Feb 2021 10:29:10 +0100 Subject: [PATCH 25/39] crypto: Add missing flush calls to the sled crypto store --- matrix_sdk_crypto/src/store/sled.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/matrix_sdk_crypto/src/store/sled.rs b/matrix_sdk_crypto/src/store/sled.rs index 6ad3b920..bf39fc34 100644 --- a/matrix_sdk_crypto/src/store/sled.rs +++ b/matrix_sdk_crypto/src/store/sled.rs @@ -426,11 +426,9 @@ impl CryptoStore for SledStore { } async fn save_account(&self, account: ReadOnlyAccount) -> Result<()> { - let pickle = account.pickle(self.get_pickle_mode()).await; - self.account - .insert("account".encode(), serde_json::to_vec(&pickle)?)?; - - Ok(()) + let mut changes = Changes::default(); + changes.account = Some(account); + self.save_changes(changes).await } async fn save_changes(&self, changes: Changes) -> Result<()> { @@ -569,6 +567,7 @@ impl CryptoStore for SledStore { async fn save_value(&self, key: String, value: String) -> Result<()> { self.values.insert(key.as_str().encode(), value.as_str())?; + self.inner.flush_async().await?; Ok(()) } From 544881f11ca10994e4b80bb9cd49769d66346f25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 16 Feb 2021 10:52:19 +0100 Subject: [PATCH 26/39] crypto: Fix a clippy warning --- matrix_sdk_crypto/src/store/sled.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/matrix_sdk_crypto/src/store/sled.rs b/matrix_sdk_crypto/src/store/sled.rs index bf39fc34..7dcb0226 100644 --- a/matrix_sdk_crypto/src/store/sled.rs +++ b/matrix_sdk_crypto/src/store/sled.rs @@ -426,8 +426,11 @@ impl CryptoStore for SledStore { } async fn save_account(&self, account: ReadOnlyAccount) -> Result<()> { - let mut changes = Changes::default(); - changes.account = Some(account); + let changes = Changes { + account: Some(account), + ..Default::default() + }; + self.save_changes(changes).await } From 6cc03d1c199fd07e46bb7551daa0f4b25e07df20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 17 Feb 2021 15:23:26 +0100 Subject: [PATCH 27/39] crypto: Improve the logging for deserialization failures --- matrix_sdk_crypto/src/machine.rs | 21 ++++++++++++++------- matrix_sdk_crypto/src/olm/account.rs | 2 +- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/matrix_sdk_crypto/src/machine.rs b/matrix_sdk_crypto/src/machine.rs index f54ce932..c580f745 100644 --- a/matrix_sdk_crypto/src/machine.rs +++ b/matrix_sdk_crypto/src/machine.rs @@ -809,12 +809,16 @@ impl OlmMachine { let mut events = Vec::new(); for event_result in &to_device_events.events { - let mut event = if let Ok(e) = event_result.deserialize() { - e - } else { - // Skip invalid events. - warn!("Received an invalid to-device event {:?}", event_result); - continue; + let mut event = match event_result.deserialize() { + Ok(e) => e, + Err(e) => { + // Skip invalid events. + warn!( + "Received an invalid to-device event {:?} {:?}", + e, event_result + ); + continue; + } }; info!("Received a to-device event {:?}", event); @@ -931,7 +935,10 @@ impl OlmMachine { // TODO check if this is from a verified device. let (decrypted_event, _) = session.decrypt(event).await?; - trace!("Successfully decrypted Megolm event {:?}", decrypted_event); + trace!( + "Successfully decrypted a Megolm event {:?}", + decrypted_event + ); // TODO set the encryption info on the event (is it verified, was it // decrypted, sender key...) diff --git a/matrix_sdk_crypto/src/olm/account.rs b/matrix_sdk_crypto/src/olm/account.rs index a5ed77e0..1da6f01c 100644 --- a/matrix_sdk_crypto/src/olm/account.rs +++ b/matrix_sdk_crypto/src/olm/account.rs @@ -349,7 +349,7 @@ impl Account { (SessionType::New(session), plaintext) }; - trace!("Successfully decrypted a Olm message: {}", plaintext); + trace!("Successfully decrypted an Olm message: {}", plaintext); let (event, signing_key) = match self.parse_decrypted_to_device_event(sender, &plaintext) { Ok(r) => r, From 5ca40b9893f14b5f6d5cfa10738dc1a522ee0588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 17 Feb 2021 15:24:46 +0100 Subject: [PATCH 28/39] crypto: Be more forgiving when updating one-time key counts --- matrix_sdk_crypto/src/olm/account.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/matrix_sdk_crypto/src/olm/account.rs b/matrix_sdk_crypto/src/olm/account.rs index 1da6f01c..6a9cf42c 100644 --- a/matrix_sdk_crypto/src/olm/account.rs +++ b/matrix_sdk_crypto/src/olm/account.rs @@ -191,10 +191,10 @@ impl Account { } pub async fn update_uploaded_key_count(&self, key_count: &BTreeMap) { - let one_time_key_count = key_count.get(&DeviceKeyAlgorithm::SignedCurve25519); - - let count: u64 = one_time_key_count.map_or(0, |c| (*c).into()); - self.inner.update_uploaded_key_count(count); + if let Some(count) = key_count.get(&DeviceKeyAlgorithm::SignedCurve25519) { + let count: u64 = (*count).into(); + self.inner.update_uploaded_key_count(count); + } } pub async fn receive_keys_upload_response( From 2a09e588f3fd0bfea83d269560276feb7b576677 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 17 Feb 2021 16:01:51 +0100 Subject: [PATCH 29/39] crypto: Log when we receive room keys --- matrix_sdk_crypto/src/key_request.rs | 9 +++++++++ matrix_sdk_crypto/src/machine.rs | 7 +++++++ 2 files changed, 16 insertions(+) diff --git a/matrix_sdk_crypto/src/key_request.rs b/matrix_sdk_crypto/src/key_request.rs index c2be3528..c77b8af9 100644 --- a/matrix_sdk_crypto/src/key_request.rs +++ b/matrix_sdk_crypto/src/key_request.rs @@ -664,6 +664,15 @@ impl KeyRequestMachine { Some(session) }; + if let Some(s) = &session { + info!( + "Received a forwarded room key from {} for room {} with session id {}", + event.sender, + s.room_id(), + s.session_id() + ); + } + Ok(( Some(AnyToDeviceEvent::ForwardedRoomKey(event.clone())), session, diff --git a/matrix_sdk_crypto/src/machine.rs b/matrix_sdk_crypto/src/machine.rs index c580f745..dca6603b 100644 --- a/matrix_sdk_crypto/src/machine.rs +++ b/matrix_sdk_crypto/src/machine.rs @@ -598,6 +598,13 @@ impl OlmMachine { &event.content.room_id, session_key, )?; + + info!( + "Received a new room key from {} for room {} with session id {}", + event.sender, + event.content.room_id, + session.session_id() + ); let event = AnyToDeviceEvent::RoomKey(event.clone()); Ok((Some(event), Some(session))) } From 6e168051b62f70f141a4a602b46c51853c31c164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 26 Feb 2021 15:59:27 +0100 Subject: [PATCH 30/39] crypto: Chunk out key query requests. --- matrix_sdk_crypto/src/identities/manager.rs | 22 +++++++++++---------- matrix_sdk_crypto/src/machine.rs | 19 +++++++++--------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/matrix_sdk_crypto/src/identities/manager.rs b/matrix_sdk_crypto/src/identities/manager.rs index 1333fadd..5351d638 100644 --- a/matrix_sdk_crypto/src/identities/manager.rs +++ b/matrix_sdk_crypto/src/identities/manager.rs @@ -43,6 +43,8 @@ pub(crate) struct IdentityManager { } impl IdentityManager { + const MAX_KEY_QUERY_USERS: usize = 250; + pub fn new(user_id: Arc, device_id: Arc, store: Store) -> Self { IdentityManager { user_id, @@ -298,19 +300,19 @@ impl IdentityManager { /// /// [`OlmMachine`]: struct.OlmMachine.html /// [`receive_keys_query_response`]: #method.receive_keys_query_response - pub async fn users_for_key_query(&self) -> Option { - let mut users = self.store.users_for_key_query(); + pub async fn users_for_key_query(&self) -> Vec { + let users = self.store.users_for_key_query(); if users.is_empty() { - None + Vec::new() } else { - let mut device_keys: BTreeMap>> = BTreeMap::new(); + let users: Vec = users.into_iter().collect(); - for user in users.drain() { - device_keys.insert(user, Vec::new()); - } - - Some(KeysQueryRequest::new(device_keys)) + users + .chunks(Self::MAX_KEY_QUERY_USERS) + .map(|u| u.iter().map(|u| (u.clone(), Vec::new())).collect()) + .map(KeysQueryRequest::new) + .collect() } } @@ -566,7 +568,7 @@ pub(crate) mod test { #[async_test] async fn test_manager_creation() { let manager = manager(); - assert!(manager.users_for_key_query().await.is_none()) + assert!(manager.users_for_key_query().await.is_empty()) } #[async_test] diff --git a/matrix_sdk_crypto/src/machine.rs b/matrix_sdk_crypto/src/machine.rs index dca6603b..6ace47b5 100644 --- a/matrix_sdk_crypto/src/machine.rs +++ b/matrix_sdk_crypto/src/machine.rs @@ -304,16 +304,17 @@ impl OlmMachine { requests.push(r); } - if let Some(r) = - self.identity_manager - .users_for_key_query() - .await - .map(|r| OutgoingRequest { - request_id: Uuid::new_v4(), - request: Arc::new(r.into()), - }) + for request in self + .identity_manager + .users_for_key_query() + .await + .into_iter() + .map(|r| OutgoingRequest { + request_id: Uuid::new_v4(), + request: Arc::new(r.into()), + }) { - requests.push(r); + requests.push(request); } requests.append(&mut self.outgoing_to_device_requests()); From c64567ba9b832aac38700a67b6e4ee48ac9ecf9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Sat, 27 Feb 2021 16:54:06 +0100 Subject: [PATCH 31/39] crypto: Add a bench for the key claiming process --- matrix_sdk_crypto/benches/crypto_bench.rs | 59 ++++++++++++++++++++--- matrix_sdk_crypto/benches/keys_claim.json | 1 + 2 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 matrix_sdk_crypto/benches/keys_claim.json diff --git a/matrix_sdk_crypto/benches/crypto_bench.rs b/matrix_sdk_crypto/benches/crypto_bench.rs index 18e4ab54..1a9e1501 100644 --- a/matrix_sdk_crypto/benches/crypto_bench.rs +++ b/matrix_sdk_crypto/benches/crypto_bench.rs @@ -1,12 +1,13 @@ use std::convert::TryFrom; use criterion::{ - async_executor::FuturesExecutor, criterion_group, criterion_main, BenchmarkId, Criterion, - Throughput, + async_executor::FuturesExecutor, criterion_group, criterion_main, BatchSize, BenchmarkId, + Criterion, Throughput, }; +use futures::executor::block_on; use matrix_sdk_common::{ - api::r0::keys::get_keys, + api::r0::keys::{claim_keys, get_keys}, identifiers::{user_id, DeviceIdBox, UserId}, uuid::Uuid, }; @@ -29,7 +30,14 @@ fn keys_query_response() -> get_keys::Response { get_keys::Response::try_from(data).expect("Can't parse the keys upload response") } -pub fn receive_keys_query(c: &mut Criterion) { +fn keys_claim_response() -> claim_keys::Response { + let data = include_bytes!("./keys_claim.json"); + let data: Value = serde_json::from_slice(data).unwrap(); + let data = response_from_file(&data); + claim_keys::Response::try_from(data).expect("Can't parse the keys upload response") +} + +pub fn keys_query(c: &mut Criterion) { let machine = OlmMachine::new(&alice_id(), &alice_device_id()); let response = keys_query_response(); let uuid = Uuid::new_v4(); @@ -42,11 +50,14 @@ pub fn receive_keys_query(c: &mut Criterion) { + response.self_signing_keys.len() + response.user_signing_keys.len(); - let mut group = c.benchmark_group("key query throughput"); + let mut group = c.benchmark_group("Keys querying"); group.throughput(Throughput::Elements(count as u64)); group.bench_with_input( - BenchmarkId::new("key_query", "150 devices key query response parsing"), + BenchmarkId::new( + "Keys querying", + "150 device keys parsing and signature checking", + ), &response, |b, response| { b.to_async(FuturesExecutor) @@ -56,5 +67,39 @@ pub fn receive_keys_query(c: &mut Criterion) { group.finish() } -criterion_group!(benches, receive_keys_query); +pub fn keys_claiming(c: &mut Criterion) { + let keys_query_response = keys_query_response(); + let uuid = Uuid::new_v4(); + + let response = keys_claim_response(); + + let count = response + .one_time_keys + .values() + .fold(0, |acc, d| acc + d.len()); + + let mut group = c.benchmark_group("Keys claiming throughput"); + group.throughput(Throughput::Elements(count as u64)); + + let name = format!("{} one-time keys claiming and session creation", count); + + group.bench_with_input( + BenchmarkId::new("One-time keys claiming", &name), + &response, + |b, response| { + b.iter_batched( + || { + let machine = OlmMachine::new(&alice_id(), &alice_device_id()); + block_on(machine.mark_request_as_sent(&uuid, &keys_query_response)).unwrap(); + machine + }, + move |machine| block_on(machine.mark_request_as_sent(&uuid, response)).unwrap(), + BatchSize::SmallInput, + ) + }, + ); + group.finish() +} + +criterion_group!(benches, keys_query, keys_claiming); criterion_main!(benches); diff --git a/matrix_sdk_crypto/benches/keys_claim.json b/matrix_sdk_crypto/benches/keys_claim.json new file mode 100644 index 00000000..58aa4669 --- /dev/null +++ b/matrix_sdk_crypto/benches/keys_claim.json @@ -0,0 +1 @@ +{"one_time_keys":{"@example2:localhost":{"NMMBNBUSNR":{"signed_curve25519:AAAADg":{"key":"Tib0yzXb6gAdrZsb74DQ80N3VEx7rw/AM5pO3vyBOWc","signatures":{"@example2:localhost":{"ed25519:NMMBNBUSNR":"UnK5GSQE7Oj4aG56ftAK0qBzzylwE/r3gHLFJqsKKRgZO13D3BlfQG8nGJ2vH+dU8jGfjWxMreHWoiK7JUasAw"}}}},"SKISMLNIMH":{"signed_curve25519:AAAAVQ":{"key":"nuNElix5zk/v2YaKh6YW6tD9tXGlJ283ixm1pLa0NiI","signatures":{"@example2:localhost":{"ed25519:SKISMLNIMH":"rC8Mn+ac0ClLhsBZ59+iWoD6hxq3L3CM17qi+Pw60ybOt/W5TvU8PPViDoyfPQB26jG9z9Q2rbvxorbwxZiQCQ"}}}}},"@example:localhost":{"AFGUOBTZWM":{"signed_curve25519:AAAABQ":{"key":"9IGouMnkB6c6HOd4xUsNv4i3Dulb4IS96TzDordzOws","signatures":{"@example:localhost":{"ed25519:AFGUOBTZWM":"2bvUbbmJegrV0eVP/vcJKuIWC3kud+V8+C0dZtg4dVovOSJdTP/iF36tQn2bh5+rb9xLlSeztXBdhy4c+LiOAg"}}}},"ARCIQGAOXH":{"signed_curve25519:AAAABQ":{"key":"hb2zAbIYFQHQxfVnDXMpaOOtIw7Sm+/IM2xNsh4tfkA","signatures":{"@example:localhost":{"ed25519:ARCIQGAOXH":"FyO4OmRuzCPZHWyhYEoAOTg6Tu03CUS3h+1c0zFc3HFvJ7oI4TVmbUP20KenX8/xnTO5CQaUCXVyH8QN6s+oDQ"}}}},"AYUJPHPSAC":{"signed_curve25519:AAAABQ":{"key":"se2bMxnTTbxCZHI6bMPijc7ereFfUZ0ML8W3BcU/vGU","signatures":{"@example:localhost":{"ed25519:AYUJPHPSAC":"7bKgbXDAfOgqgUdlIL2ladoZK4xbisMHoIv1kG84j788U/bzoGYY3ETqjf5m99DTroBd5+7W1u7XBf0vMksGBA"}}}},"AZWYEFDSDX":{"signed_curve25519:AAAABQ":{"key":"xsoUhL7yH3i1anuXeVzCjgzjP1JKouki7kAlF96kmSo","signatures":{"@example:localhost":{"ed25519:AZWYEFDSDX":"ABDNOM7lDXqXCCvCTx+ruWle5BPJWEp7J86zC5vIzWgo6uMs+AnxTHeRcdzArZyDgo31hWHgExj/mCbcGWQpDg"}}}},"BBGXWJCGEV":{"signed_curve25519:AAAABQ":{"key":"x8GEvcwcs08Jx6KJ6DzAPwo+2YuD1pXZ0mxPfy0U10U","signatures":{"@example:localhost":{"ed25519:BBGXWJCGEV":"9ZpEJxOMkjIQh34UQ1ZFpgU/92h1kk/f46dCNaRAPfOYiQLhBTnDx+yHT2ys6IeJ/RjtWe5nrFJRPgAFf+t/Bg"}}}},"BNPGUGDFAE":{"signed_curve25519:AAAABQ":{"key":"QSiaJfFPKo7YelLnezHpNwl7+54NnJyX2aPuqR2/Y2o","signatures":{"@example:localhost":{"ed25519:BNPGUGDFAE":"EzemA6kDlDrNJy6DXcTAY7MZMK/14FqNFr8PrjZUAFTGdiDvNvTrsr9kNMssJQkJr05YSh9SGzsVhzRXFEtrDg"}}}},"BZKAGPQUAB":{"signed_curve25519:AAAABQ":{"key":"Ng1qyxiWyTz1h8i7VOX/oNvtOtklFvzdCzrjhP9Gczg","signatures":{"@example:localhost":{"ed25519:BZKAGPQUAB":"vHTUl/MQC5lMQgslTFXdyZ3p+68df2Oa2VIB2hUu2cB/QILZcQh7CepsvK2wkU8elQNehUaB699f7yI62A3QDA"}}}},"CCECSKBEDH":{"signed_curve25519:AAAABQ":{"key":"6wdkkNbRv5KOCQEAJpx6Adx7Pp8Baa3vtm3Qx0iRN3c","signatures":{"@example:localhost":{"ed25519:CCECSKBEDH":"Q8t6n0Ch1WNzc0azZZf8pGmvI6DZbBfmwOUyAQY5ZBNdY3d6sJZXunw4HroDcQpxAGdI09Ju8Kx8mBVg31P9BQ"}}}},"CEVBXZPTZI":{"signed_curve25519:AAAABQ":{"key":"JgEjoYPflqUDz+mZ6kWZuGfv59PgFXQalWUMBDXvQEA","signatures":{"@example:localhost":{"ed25519:CEVBXZPTZI":"plsxT+REpfILkbV2soWSdJKaBO82pX0q+HJHWXNJePmogBH+JavxZvfCRQXnRcqNVqvVZMbd7LzdE9MGs1ePDw"}}}},"CEVLRMVPBD":{"signed_curve25519:AAAABQ":{"key":"yuM+176WD2MwzgTCNKNgU5hTreRd6kTB2NQzrApp+GY","signatures":{"@example:localhost":{"ed25519:CEVLRMVPBD":"7ngNlt5SUEQ8k2pdmrYeiF2y6iTZzHScVfZEL7Uaf3uGkxu3i2qhdWmaZhlUCGhAJWg/u9Gmz/dOHM8GqCELDg"}}}},"CHWHHLVNRU":{"signed_curve25519:AAAABQ":{"key":"UM3FRT6cFTvwZ7XFDI59rG1lBIfvbYx6l2sRlYteuhA","signatures":{"@example:localhost":{"ed25519:CHWHHLVNRU":"8iQU5ROmvy+weXD7riod4cvHvTYrxvin/YnumOl/eFZFUqYf+fJtqDxTUVpcrpMO2Mn1vlZ6IUFPQoeTllozCQ"}}}},"COTJRBEJRK":{"signed_curve25519:AAAABQ":{"key":"TjdvLmI/S1ZfDXlxPpR8SEHLrWllkOiEWPsOYrSFGCc","signatures":{"@example:localhost":{"ed25519:COTJRBEJRK":"CnCUBx9yRBJGc2iPrMAWdJIMftkGn4wgGW8i9l6VkV8ORAH07q0gO08IEI8XNiveLzuC2vq4hjqGxYWJFYMYCA"}}}},"COZKBAJDSL":{"signed_curve25519:AAAABQ":{"key":"diq2cZEb46nCLvMj9SsY1WXSNLo0LkKwhGnZYbtwCVU","signatures":{"@example:localhost":{"ed25519:COZKBAJDSL":"Tu3byXcnF59k75UO2aRrVZHrxq63FwZ/7qG2jz1IhyslttMpExhLv+BCYmPaFZHR4pkb/aNwTLBAmugLZ5gtAQ"}}}},"CPLENDMCJZ":{"signed_curve25519:AAAABQ":{"key":"Yv8Q7EuPA7dpxxJaFl9Mjo9S0CSGKCgsufSXrr0dRRY","signatures":{"@example:localhost":{"ed25519:CPLENDMCJZ":"SVIpMqBbqLMyP2mfHd6fqGjDCUPV1BqV+Hrw54xIAVI0FIu2EipCtU64OIl55+dHOBFuhGwzAtlVfnJJa5k5Cg"}}}},"CQSGOHRFJE":{"signed_curve25519:AAAABQ":{"key":"EW4AYmZ65GGUGy0mNla7gjybMDvqyB0s4r9AgxMKQFo","signatures":{"@example:localhost":{"ed25519:CQSGOHRFJE":"TmZDCRzSqu8EC/xg2Fob6U5DhGeQ6HRgR02TWqcUR44GnwcgibANQemWvV/agu6LnJjAkREggXZC47xceZExDQ"}}}},"CRYYTUURSR":{"signed_curve25519:AAAABQ":{"key":"0u8F+/8lHiIxgrdHrBnSFbWOyJ5+4k9GGF7nAmdF00g","signatures":{"@example:localhost":{"ed25519:CRYYTUURSR":"+UK3EP4BJqXPCfhKMtl7/iWgXbIM7zUYmvWEh2vT1oQnboOIOM5vsRltarjv7RIfDiP5N2m7TeEBvNjrf3e0Cg"}}}},"CXLMFOVYUX":{"signed_curve25519:AAAABQ":{"key":"Y8QbX9D7p+3AIYT8uxOzzaT2AqefrffptfQlkljjpk8","signatures":{"@example:localhost":{"ed25519:CXLMFOVYUX":"gHN8yzyDURb5XiaCr/gKYfIIVkYOhLFNqVJ7C39YSOVWC7QALMKY1Aiy63b0ldNVofMCoCqogbTxyfs6t7DtDw"}}}},"DDMJJSUUSH":{"signed_curve25519:AAAABQ":{"key":"qcJDtyiLcu7li19tCmaVfh1cnYFLTxM7YTISLGuFsS0","signatures":{"@example:localhost":{"ed25519:DDMJJSUUSH":"m6moRaWZkCYueh6nvbR5kIQbZmTvZFRMLQte2b8Z9NSM6Boa+/SR4Ds0YA4I8OVe5CEBA9tdsS68sH4NubRBAA"}}}},"DEFPMZXZCM":{"signed_curve25519:AAAABQ":{"key":"T3kZS/F3tDWy2P0kPP8NcugNF1rf4TaYEeX2loLmhAI","signatures":{"@example:localhost":{"ed25519:DEFPMZXZCM":"R/lm/iL4xedB+fldEqFSMgYbga1KdhVxHa42vRPb9ucD34pKAoB2So11nKG4pxBujzO258VofWPXToz4AmRkAA"}}}},"DHJPFSVBXZ":{"signed_curve25519:AAAABQ":{"key":"8qdZCJXi9jUJHaBNvW4xC1Z45+djh0sJOakuJknSTEg","signatures":{"@example:localhost":{"ed25519:DHJPFSVBXZ":"igHPMqdtAsVwBfmWoQz62Hp1HnPDQ/YrTIFLKcIFzMX/Mat9uJRj/C/4WYdJ+AWq+A+cEMS3/mlnZERpTqS2Bw"}}}},"DKJOMBOOJT":{"signed_curve25519:AAAABQ":{"key":"OKQJW/h8MPLO3fTaFFhlu0eu8JpfiIwVVKWUITjZBC0","signatures":{"@example:localhost":{"ed25519:DKJOMBOOJT":"aqV5z0vDMj5IgozTtr45L8kXTlL9m7o8UFLy5/RLfLn3RGOlm4VoImrhJLMLCe/Unn0qiyesq2wLS5KTYXxRCg"}}}},"DKTFRSRHCO":{"signed_curve25519:AAAABQ":{"key":"TpPJMlL6XY6tV3J7+0IxSVVLwO73wyC648Zk0xBELCI","signatures":{"@example:localhost":{"ed25519:DKTFRSRHCO":"0nlR1AtN+dFhjtP6ODOSkcfYVg6A3bIZ41378tXTsl3ZlOsMHitfo8mEyo+uKzj6ManK6rxdGA5+WpqDnBO9AQ"}}}},"DQCYLQFCGB":{"signed_curve25519:AAAAAw":{"key":"TmtqlsSp8X8HUrGsCBwOXAPGaC0UaDpS4vXRcljNyEk","signatures":{"@example:localhost":{"ed25519:DQCYLQFCGB":"1LAyTQYZ+T+YDOC3kmf7fWY0ZB9xCmi8yWi1yc7zH1TVzC3cvDHF4sUgqCqJvlHlG/oo7m0dfsdo43cX3FWiDQ"}}}},"DTFJXETAZD":{"signed_curve25519:AAAABQ":{"key":"/vV7pIZFVsMm7ny2LA0vwuGrSXox/Oh9dSr0hHb+xFQ","signatures":{"@example:localhost":{"ed25519:DTFJXETAZD":"AnKflgTS+JrNrNKTRLfkUCH+uVH7WAfRT+DVRNVHanGIVeJyWbLC0HAetTE3O5B2DQlw3FLCQTFTCIIsp+00CA"}}}},"EDECYDJGSA":{"signed_curve25519:AAAABQ":{"key":"5jo6mb1OdQ0xXLAaG5M+w/W8eZASHjwQJU3s/XNYRDU","signatures":{"@example:localhost":{"ed25519:EDECYDJGSA":"ULBYHkRRFutZ1cbJVV93f/2llA1kWQ0WkrhqiJuIg+wt9CVFocUYPFa0KwdOQ9y3zcSbUL7QJTRxwb084SzODw"}}}},"EFZNCETFCJ":{"signed_curve25519:AAAABQ":{"key":"XdfKhyXK1z4+h3li3qRo9S5Guw6IqniXhuVvSbYiDRQ","signatures":{"@example:localhost":{"ed25519:EFZNCETFCJ":"YNCHbhPgQl0ChOUmd+nD22Fx16imrLGwEvqeLooKqltqhB9rvRl65ta85FsnoZleRnLPGhQwmum0Vcj9eE5ACw"}}}},"EVMLDDWANA":{"signed_curve25519:AAAABQ":{"key":"f9XvwIZgXRPRG5RvgQD58ek/quiA1vi9qUohromPjSU","signatures":{"@example:localhost":{"ed25519:EVMLDDWANA":"6g1HsO60iXjHMeQnzUDKsaEIgA2Wkvm4R88Mlm+LkCjX3agFRCLmWQbDbCNyhfAuKp5RtGVztwLP+w/7qr5OBw"}}}},"FAQIGNOZEI":{"signed_curve25519:AAAABQ":{"key":"W3nw8o5bls1FakhZcXXm7bu754GSDZhDuy1h5Z2qlF8","signatures":{"@example:localhost":{"ed25519:FAQIGNOZEI":"s0ytdVbNqoibCBIg/H8eoDapHnK30x3f8LgYQelGAK1JeYaHV2jsXErB08IqMX+ExveuZJjhC2UruMotjM38DA"}}}},"FEEBAUWTGH":{"signed_curve25519:AAAABQ":{"key":"nmMKZNw2GNnX5IrC+mLxs7VygFGe4Rr8Luzir/wlZgY","signatures":{"@example:localhost":{"ed25519:FEEBAUWTGH":"3jDEjcUS1zh1Ls79HcgIU9s4Hbkp7X2+31O4bLwfuZmHX4qgfhEJdHAqw4rnv8d5sGurRvij9SrrsrurX0kSAA"}}}},"FIFUHVFZPB":{"signed_curve25519:AAAAAw":{"key":"7XG2DKzDDUhmOaYAuBnVfmvzg+CYOiwBJHh38xpGjW0","signatures":{"@example:localhost":{"ed25519:FIFUHVFZPB":"WolHVbPLD/GnbXr0Sat78bV6YEVf3tWK3NQV+59VGGUA09ktj2Ks8UgPNpbCQSRs/w10PMITcA0wqDOdCzO2CA"}}}},"FLAIGGZDUI":{"signed_curve25519:AAAABQ":{"key":"C24KE70E2Q8oGhntRH9A8ecJQ4c+NqcT/Sr7U++2K3M","signatures":{"@example:localhost":{"ed25519:FLAIGGZDUI":"+zbDEA7vbM++Ykg+l1KucVnM4wAnlJhw+ynAWG2pXwooPz0Dwss9Svv3o+TtbRxoVj9mSMafN0qjoiOrJWQIBA"}}}},"FPPORJYJWZ":{"signed_curve25519:AAAABQ":{"key":"7Qeko4af6cyac1/UH0RtvcjhIwVbTH9XjPn22a3jiHo","signatures":{"@example:localhost":{"ed25519:FPPORJYJWZ":"/4huQ0h2v24kRvqCIiZsjtucgMoc2/0ak7tQ52ACWma1iEFkjFkJhs5QKf81qfb73jkwjcnSHfUtcLMago/eBA"}}}},"FRZAXXLFNO":{"signed_curve25519:AAAABQ":{"key":"m+LMjhyRoutQij2tLLynQpOpWmYLrNilywl8QkFe8AU","signatures":{"@example:localhost":{"ed25519:FRZAXXLFNO":"so0lZOlknaNZopmQ6hWAnZSo822V4dYDRAZtD/FL9UPj8WLNUCoArkDzaU05mi95Eu5fAmr15hs3FJXamBFhAg"}}}},"GGBGEQJNSJ":{"signed_curve25519:AAAABQ":{"key":"B0b2TKhfX9aaGllKtqRpczjBkD33C5Vo+GFZok9qC38","signatures":{"@example:localhost":{"ed25519:GGBGEQJNSJ":"EWeAsi4wsssk6Y62h8mMLQi1UZNj+atYQLsyoGmcmGRZ9IYAuXz/qLkGGTErvMzlA6OxeJPBUWSEj9tgq5V/AQ"}}}},"GIOUUFLOLZ":{"signed_curve25519:AAAABQ":{"key":"4gaX6tSP2CH3SuFhHbJewTJnTv1ATfmLyvqGySFKvTM","signatures":{"@example:localhost":{"ed25519:GIOUUFLOLZ":"hoiLlIRhzEG74Jp69ckkIlhtuVRqRIf7GHOYrSW5UFaxzxwdcqVWUeXHL4cq9KUAhk+Sq7WIDojavU8Ia99dCQ"}}}},"GOAKBDPUKJ":{"signed_curve25519:AAAABQ":{"key":"UDn9ExvHIO2cddBqPas4KXePMaX1vRJ6zmUdQq5GPVs","signatures":{"@example:localhost":{"ed25519:GOAKBDPUKJ":"kELcOheftQVdcgouGW+uAbiA0Z74qgql0ucoWAWeh6GW5z5xiKpGkPgUDa2QLyV760nJY24dSFVbk5XBcHCpDA"}}}},"GOVWXTWHIQ":{"signed_curve25519:AAAABQ":{"key":"LQiTE14erDyDGZWJGRgbzbTOdPqZNUqUUZmGRU8wnQ4","signatures":{"@example:localhost":{"ed25519:GOVWXTWHIQ":"mabFSjzBrFkgRYNQcizR5U0GwAazZVzXIU6tEjG1cHx5dp6j/ZaXMUBQqYIsdzaO+yNYRvn4RUoD0JhCDnQjBw"}}}},"GSKQTOKLNQ":{"signed_curve25519:AAAABQ":{"key":"Mmy/vwjtOj5kMBrj1fijI5H7oswIOELDczyto74hZGM","signatures":{"@example:localhost":{"ed25519:GSKQTOKLNQ":"5N69Kp3isctfVDdVnoaBIc0/d3nRSiTHT04eHdMNXTDKkxbeZpVqCgl42W6IMYFcGrDMeK2WJVEfYxGstXTGCw"}}}},"HHQFHSMEYG":{"signed_curve25519:AAAABQ":{"key":"CDF79o3JSPwt5pLoP+2QUUi/EL7N4hlbdXzkBCTr8Ts","signatures":{"@example:localhost":{"ed25519:HHQFHSMEYG":"NTitNxtT3qbi2mbTsqpW/DueRJBHdwHe7MzWH7PaS50skqcNpGm+9nWmMMXeyHtMCgGKISOy1gNo+/FGPo2DDQ"}}}},"HVAILYUHDN":{"signed_curve25519:AAAABQ":{"key":"zfV6yLaZ5Z3u7Hk1UWtfYy55FMl1+fsi+dgIk+L6j3o","signatures":{"@example:localhost":{"ed25519:HVAILYUHDN":"JYfDBJSpMmGZiENM0uXRHHEZrh2cBQNnTLzsDwA+LFcPmpAgBXbN7xxRLuV/K5ndfsNu66JlzgU8a99p1lR4Cw"}}}},"ICPHTICSZV":{"signed_curve25519:AAAABQ":{"key":"fQbah5Bz/1fmGZ6eK/vfbUBIgeoz/yknAILK2FrNSV4","signatures":{"@example:localhost":{"ed25519:ICPHTICSZV":"sT2vwmRj1gBR0cPJKXEEkndti9T6eubuKZogHS9AYAuKSL6rjmE3E4vsBKQQNgKZROim2oUc4DqUYDkHm+LBAA"}}}},"IDZMWJANDA":{"signed_curve25519:AAAABQ":{"key":"FOrahT+LcAIUXPLJwF8kxhB/q9VhOy0TMLndwTHZH3w","signatures":{"@example:localhost":{"ed25519:IDZMWJANDA":"NXVD65HL34YhcdfayixruoSVW4UmVqqtcCsNdCLvcjQrjgRCeTQ20U5g2qLde9MhpGmPZEI07ovVTFh0dWShDg"}}}},"IFLWAVYNOG":{"signed_curve25519:AAAABQ":{"key":"d9f8PJdxzTwjhbiL+KSsQp7O0AP564wAoKO0YPlTUBY","signatures":{"@example:localhost":{"ed25519:IFLWAVYNOG":"wkdJG9IbrjkZFiSuAu05gjv0rZRU5t9qiU/Xetlreo2pZjUfRUttvpxqlti9ms5IVFRjf2yqIWjhOjajq2pgCA"}}}},"IKHFOAKJYY":{"signed_curve25519:AAAABQ":{"key":"3wEwFj17WW2A7X/LEQche7nUSwtyKD0qYf7kyUlVOF0","signatures":{"@example:localhost":{"ed25519:IKHFOAKJYY":"3o972gJH+KZZ+41P03rPbfCPiRQJR2lwPFyH6RLRqW9yZCF5io1mT/cnhQFT55bc7fdLp9eH1hOntkp88rsjDw"}}}},"IKJVWFOXIF":{"signed_curve25519:AAAABQ":{"key":"zHDptdgkl/sZI2zcmal15TR7fvgpbIC7tjOStACZUjc","signatures":{"@example:localhost":{"ed25519:IKJVWFOXIF":"TeukT6OLHwq3+VVSLbuLsYr515fp369hzUCiC7e024e8nRXvT4it1wPT+3dd/XFtkZK9gdQaNrOHO+dnJUeCAw"}}}},"IUQJOWHVID":{"signed_curve25519:AAAAAw":{"key":"GCFQ3E66D0tW8yV78UJvZ0LQxesyxlgxyqba4X4ywX0","signatures":{"@example:localhost":{"ed25519:IUQJOWHVID":"Ot9fkViT03WSXkya0+szJ6tbAte+6hQhwvQe+vY7juQpeW9X0/gYXz07P13yVoG1mjLyQEn691U4sG31q9aMAw"}}}},"IZKJKHXKYI":{"signed_curve25519:AAAABA":{"key":"cyuP5zHIU8y4Di8IIwT1O3pK1Y6PUo5X1MO8KInsWhc","signatures":{"@example:localhost":{"ed25519:IZKJKHXKYI":"aOhRAXBCWdNai6VtUSb+lAa1/jjXb65ZySTE061CwilmXp5y3nmpXtu3cU3+ENRuGVbBiIBU1GlDCALtOWTMDA"}}}},"JDKSTVQUQM":{"signed_curve25519:AAAAAw":{"key":"vwgDSMn59usFwH4zT3H3QPWWtBfWHX0ibX0d4aE4xU8","signatures":{"@example:localhost":{"ed25519:JDKSTVQUQM":"ZcXGAn9J1/EIrFKrPWceC5oDMKHeBvXxkXz5Y3jzl9mwDRy7xKcfM9tBMgb7Nt873wdvIIz9M4L6Rw4h/pG2AQ"}}}},"JHCACATEDC":{"signed_curve25519:AAAABQ":{"key":"WgUdgP1x6sDs0aRje145nVn+OASZCNChSR0yg1Gq63A","signatures":{"@example:localhost":{"ed25519:JHCACATEDC":"apswiRhdR4E5waOxCpORduI/AyH/2oZvWf7WKECDE/iDhw7mc0NFzuO7PZZbMGQ/cZBXZHl379mhr/MK9zpxBQ"}}}},"JKEXZVRELA":{"signed_curve25519:AAAABQ":{"key":"vT8xV+yDhZXazduqUYXy2PUOBcy9xNal4LZd9JUEYxE","signatures":{"@example:localhost":{"ed25519:JKEXZVRELA":"hd4WVbRyt9ZMPzdDybJwloBMpvOWhYufVtNI+Ff/tiIVDfan9Fsh1GylweKqhcfLOWpgxlZGe92P1FGkVk+vDg"}}}},"JSJZHEGJHQ":{"signed_curve25519:AAAABQ":{"key":"Pm3bmSAt4N9aBsglBl7I3Mse9Mkm/o/VTMkkoNmnny0","signatures":{"@example:localhost":{"ed25519:JSJZHEGJHQ":"zeC0n5IO9n9RnBNEU5Bzwkhg++xxHYw9Hc+XcuDreu0bg/TmPgKFvJFSOIoYc2ETlNrusu2Srl1TRWq0zmsECg"}}}},"JSXCDTJQTQ":{"signed_curve25519:AAAABQ":{"key":"7Sg6EctVD6ohjNFKJWH19oMYuSEkI95SOFirJIxnaRM","signatures":{"@example:localhost":{"ed25519:JSXCDTJQTQ":"qvytL/sppF6UyGTYbjqSCZVtKtQyD9Yh3gr1gA5oLqUibrWAsuLyFJVT5QJp/boLN8WrNbcu1YZMFtOd7mWnDg"}}}},"JVUSWHJVVM":{"signed_curve25519:AAAABQ":{"key":"Q9B+aCB8MbsGR+HzFAr3rRhKt4NmVT3laRPJtc5ckXY","signatures":{"@example:localhost":{"ed25519:JVUSWHJVVM":"z+qdE54OxTt7+2d7S2ogA6IevyLJVXcYGy0maqvbhRl/ItVp5zI/QkgYR6r0C2oDp7TkA9AwfP8fdZcxD3RyBA"}}}},"JXESGIWWOX":{"signed_curve25519:AAAABQ":{"key":"9Syd/W/GDKVtFA9vqRq2AxiQXpze5B/tdnmYt1DzxUc","signatures":{"@example:localhost":{"ed25519:JXESGIWWOX":"3GKCdNYurZyinprjXYpL25eCTwd9ZZVyvluJ6bBLp6Cr0Ho/Pa+IN7ddEIvv+XeM7xh14L2OTkCm4V+TcpXrDQ"}}}},"KAJPYAZKWG":{"signed_curve25519:AAAAAw":{"key":"5kcrA5y+i7Q6NT9o4k9pXUHeBeDa/Iz4yfyiIkVvWgU","signatures":{"@example:localhost":{"ed25519:KAJPYAZKWG":"20ie3Apsm2iNz25Gaqeb2ubU1GzOVAKwKJnvMoCbbgcXM3DgdZIIRylWYyKU4FKt7Fqhh/8Z7YClBRSY7M/GCA"}}}},"KIAYGCDGBV":{"signed_curve25519:AAAAAw":{"key":"c84WaZULk36GTbbVJru7a2wg0tTai7T8dxpZvN64mgQ","signatures":{"@example:localhost":{"ed25519:KIAYGCDGBV":"d6wwo19qdQgbTjivp+2vTH9799ftbBeeTcqFobTDZLVxxruteCr5DcTg07q8+12AngBrhUZoL9ArUit4stPPDA"}}}},"KISRZEPQWM":{"signed_curve25519:AAAABQ":{"key":"Y0WAaJIRREfjrloJklpbWynqPBro+32vBEDtQ4O8AFw","signatures":{"@example:localhost":{"ed25519:KISRZEPQWM":"fGXr6nqQztJMfSFGm15QztdKOTNFEYzRuCLftS3B6gShUiGsJxUpUuyf0/woko1kkYbeOvb9LJK7pVONMCEpDw"}}}},"KKCOTIXYTM":{"signed_curve25519:AAAABQ":{"key":"7s2XFE+l1wzwuv7R6l2HtIM9wjOPeNLPDr5T322zCVc","signatures":{"@example:localhost":{"ed25519:KKCOTIXYTM":"ZYOZAa2SEa2hPNyc41AeRC7bZmNmaRfBR5rATNI4EB+nSE3N5SQ2FCkryHlNKFn4NCa/GuJ05F6WlfX9veAlDA"}}}},"KKZJWRYGSG":{"signed_curve25519:AAAABQ":{"key":"opFUiO9oFOaTVxp9mcgCOfiFVKSBc4rgyp1DiRj8rxg","signatures":{"@example:localhost":{"ed25519:KKZJWRYGSG":"rbzItmBcHF3E1gg1c51W2JfUcdY7LIzdvqi4K/TbTwqonk5Gc6DhQtPIqLSxHXWspGD+9bR29Tlz3bViTzlIBQ"}}}},"KMKRBJXMDJ":{"signed_curve25519:AAAABQ":{"key":"XybOfpwgQdki0smx6fwjqRKVuqepXCb50DnVTAVCux0","signatures":{"@example:localhost":{"ed25519:KMKRBJXMDJ":"ztiwAH+beqnRaEuA+GqRWeBC+tml1+fOLLH+mQ5fIzmaQev6w3bQhBPjJmDkIugMFvscu2IQoGN7QazUXg71CA"}}}},"KTZXWCPEZU":{"signed_curve25519:AAAABQ":{"key":"ujpucE+5OJxupcNllB658jL32g9GCUtGCS15okUKqmA","signatures":{"@example:localhost":{"ed25519:KTZXWCPEZU":"9hTNg4KrzOS4gxKNX4ElI11CYOGrnNv6/AtiAZ9evZbuS+EGIR2im336JwHoqAQbspPwdQgPk6A1Dks3X1GPBw"}}}},"LMAUZPCZJE":{"signed_curve25519:AAAACA":{"key":"PQa8Id/xkNPqU+xK3l0hVs2gtk6feWm2wncWHYf3DBk","signatures":{"@example:localhost":{"ed25519:LMAUZPCZJE":"UgkfOr4R2gLaxP4DYVHxqFWcssNaDqYovUm5IRMvoUl+b1hH5Iv4XP9to3XAERbV4l2G34UhpGJ0ZMzC6ehrCg"}}}},"LTPSXLWMGZ":{"signed_curve25519:AAAABQ":{"key":"mvkq27g4aGic71BotAtFZqPgd+2fbr18VsGbjOCA800","signatures":{"@example:localhost":{"ed25519:LTPSXLWMGZ":"38/8fbSvrO3Qz3m03hWUb/Ef5jDKapbd0pztSeAMILv0MewdQcF9zxSFkIUNOtczWZvsUeSj15vv+UfYIWkXCA"}}}},"LVWOVGOXME":{"signed_curve25519:AAAAsQ":{"key":"H2sqFeswPO1HiMNlWUByDWYuk2S00fXlDwcDjCw9Y28","signatures":{"@example:localhost":{"ed25519:LVWOVGOXME":"IMIkKYDkbnGLpfiOmv4gGZkhUy400+gpsqmNJxwYUt5f2TfhiDvoh7tX7kSSIx56hAqNQV6A5hSPlp8Qjh2fAg"}}}},"MJUNZFVNEB":{"signed_curve25519:AAAABQ":{"key":"l7/ezZMwt6TuNzijz1E2gr6sxUQQUH/0AO5XFNMeYzc","signatures":{"@example:localhost":{"ed25519:MJUNZFVNEB":"lJUYlp9/kGXN4R+uEU/JG0DsQPzgA5APcSbULvUPoSzDye3Exg4FdDE7ro0/yd4632DzcDWJk12UttK/r9ZLBA"}}}},"MOGWGGXXBZ":{"signed_curve25519:AAAABQ":{"key":"D4NvD7OchpyaCWaZj4v00CIvCFUQvfo+8kFYpv3n1WM","signatures":{"@example:localhost":{"ed25519:MOGWGGXXBZ":"Yx/cWF3XJVFwHik1Mt9sEnZ0g6hz4uCDQZ8lqzY7Pg1RcQAjVCWMpP8UgrcqAkZcswg/MpH4o9iBl3huT2daBQ"}}}},"MPLCYWDUMV":{"signed_curve25519:AAAABQ":{"key":"runHg1CANPy3vGq+nFBSOUSiGZzDgYZ6ZcpN8BT6mCo","signatures":{"@example:localhost":{"ed25519:MPLCYWDUMV":"8BadaF/wscICNyno+fsMkQQHJTUFp51U2TV9JD8o4X+OeCts+Al+Q3pB1hoAQwXlBKVqVVAUVpCyPpGaR7x5Dw"}}}},"MWFXPINOAO":{"signed_curve25519:AAAABQ":{"key":"7547iJG8yD7rsQioFeR/6VNrePgagKuamNoTCiA8PQw","signatures":{"@example:localhost":{"ed25519:MWFXPINOAO":"xUVkHAeLyHrLyYnUltJmCAJZUJmdGfpdUwXZZFN0dVlbP0TbB33bIq91pQcjsbUFA+9zGBtqj1wV/Z2rvyr2CA"}}}},"MWVTUXDNNM":{"signed_curve25519:AAAABQ":{"key":"lwfLjFqwJN1Z8xFzb830030NVKXKQjXy46HQnn8WWUA","signatures":{"@example:localhost":{"ed25519:MWVTUXDNNM":"Em6DNEOvM0Ym1wDT8Dtmcnl/4ArBghW0H8g6rzfvfFIy/fBuMTp54JrJVIm/zCqdmH/ygfsn7jUi5IieXaPWBg"}}}},"MYTNISNDKK":{"signed_curve25519:AAAABQ":{"key":"S4/rTuzq2XxbfoACFgbVPPOVEa2biEvpoZWaOsG/c08","signatures":{"@example:localhost":{"ed25519:MYTNISNDKK":"GFuyakFesGnYwk0pIlPQeQgCrST7M4qgMSuGe8SD9cS/owb4GhI633Uya6jYvI3NitzF+50fOq5ScvsR1x6NAw"}}}},"MZECYDWJFY":{"signed_curve25519:AAAABQ":{"key":"fjopoO0I90USyQskjdORu9iEtEJVcNRUF7DBCsEcpAc","signatures":{"@example:localhost":{"ed25519:MZECYDWJFY":"2wFJsn/+K3/92A6SDQUYQCeRcW7oxJbkv2rOJRIfKnDGqg7t1sVtRcJwrIoqxhQ+zn/N9pT/DjTgcv2tM5eFCQ"}}}},"NATWAGSISG":{"signed_curve25519:AAAABQ":{"key":"3wItCw+Ps1x4A4Hx88MM6s4yLXJYT5WTwEOiOQLRzQo","signatures":{"@example:localhost":{"ed25519:NATWAGSISG":"wUFCL+lBn3dKqt7DomiNefz88cznNfvUM+1ou7QDWCmhtV3OugnM1vOldQC6wcL+Sni6mOyxh07nqTYnxFAMAA"}}}},"NFOYODETZN":{"signed_curve25519:AAAABQ":{"key":"o58N6afcIfD+vLIj/NEKG16xDGBBm0bInEltzGmk/AA","signatures":{"@example:localhost":{"ed25519:NFOYODETZN":"VYA8+P5axweIHMPETW8hds6AYdozfs6byHwV/g04UmmJayViJu8MBPdmrW5E93Zfhxtgrf78y8BrxS2WW0+UDw"}}}},"NKYXFHUYWE":{"signed_curve25519:AAAABQ":{"key":"XAj2MOH7RIOlfgqkjPsNWS9CxUIh49yUC1bNl59oXxU","signatures":{"@example:localhost":{"ed25519:NKYXFHUYWE":"vMVRtXO20nmd7sK4OEpbAk/fNqxPy1YBpZpaxEDSBtszAcXrEC3b89U237FGJvSQCYgdcYdP1+pRGPcey0TjCg"}}}},"NLWICQEGVA":{"signed_curve25519:AAAABQ":{"key":"igMZya3UYfUWW7UD3ywXhiYlY7YucTxK/EuubQ8wvnQ","signatures":{"@example:localhost":{"ed25519:NLWICQEGVA":"t+vgNIrhCcl1O6H8oJy47pDwzQtqQq4yoy9+n7N9DdL6e9SlhfFXeaev9anjLUBpsJbHOrakw0jKgtPFEWApCg"}}}},"NMEOKLUWNV":{"signed_curve25519:AAAABQ":{"key":"+tWr41BirmesfifQiN/aLTibUyYuWyLCxh94IUMqk2E","signatures":{"@example:localhost":{"ed25519:NMEOKLUWNV":"W6Xw3qZGTQkWPmbkantW12DaF/xERhgdLgIgKpPP/AyoMSMb9QMYbOVPu1k1HnC8ne6usI6OVArB+G3a57bnAg"}}}},"NXGUGXMPQX":{"signed_curve25519:AAAABQ":{"key":"KjO8TWg0SeWQK6EP3PU+lNt/VGpeLOrsdBdMY0SWYxI","signatures":{"@example:localhost":{"ed25519:NXGUGXMPQX":"A58uyovkFugwM0jlth1baDnQlAcADpXgctRedd6dYnuJRbdEsBohizrneHcwCJl03ZP7dgge2d1VDeTwhAspBw"}}}},"OAVYDZNHPK":{"signed_curve25519:AAAABQ":{"key":"XxtK/9qnhuI4EdB5NX8jbf1py0olHhU8R67IzaKR2X0","signatures":{"@example:localhost":{"ed25519:OAVYDZNHPK":"STg4ahB+5Kaxqh/LWr3RcrNJjo6RV8chRrkrQEnCk6euHXPMrH6FzpZZfqSYZWJTjk1qvtKEiYsE1OG8wyWyCg"}}}},"OAXXWDJTWT":{"signed_curve25519:AAAABQ":{"key":"GgNmjyBlfwB2zSi7zbO0xYX5YGstCA3kGeD7xvdBjnA","signatures":{"@example:localhost":{"ed25519:OAXXWDJTWT":"5eOMyEVvJZrJdgARL295Pvg/KG3cPqRf5x3o8gFLrZZ5FI6XzHYRAhmJX/JgdktovhtvfhBerBcl/VDOZR1ODQ"}}}},"OBFHLVDNMU":{"signed_curve25519:AAAABQ":{"key":"1fdMPzmpAjAwGCJkSJQm6cHueLOWVB1FP7KyCgNnj28","signatures":{"@example:localhost":{"ed25519:OBFHLVDNMU":"LcuZBb+pkoM2gOZ6FeVXBaIAk4SHHCiti+BBjcnXwBYIz8ujqiFUN46jIGyPgMeqc3PTB+S4YxpQoAmE3bd2Bg"}}}},"OJBSRLPEIR":{"signed_curve25519:AAAABQ":{"key":"mi6gyEO1RGT1LW4ncMsEulRJjADvl862tyFDBGiLQ0I","signatures":{"@example:localhost":{"ed25519:OJBSRLPEIR":"CB3t9RAHaC3cx2HorunazS6qIQ4iW6/xUoRk+MCOGDfpzCi5B/yd52AQQzKriLbAAag/676JgeQQa9trjP6TDw"}}}},"OOIGEAIKXR":{"signed_curve25519:AAAABQ":{"key":"PbTR9BL/uzEdBa2a+FSAukPe0DbhaQ1B79q6NSuh6GU","signatures":{"@example:localhost":{"ed25519:OOIGEAIKXR":"/KVD7+PxY0CXUA9ME24PNRLFRIumS/uBVhpz6QACxUGee7w2ErDSRotOoqsLf45BvE8cufOmoLfnTDeDPVxwBQ"}}}},"OOJRSPOHZE":{"signed_curve25519:AAAABQ":{"key":"0RhHNv1NpFtifL/jgFTwGQLHdeWLWTThh7K2l9/3Iik","signatures":{"@example:localhost":{"ed25519:OOJRSPOHZE":"lZd7by0GVrFqQIwoVg3LeC95Cc5e9Jeq6wzHojyRNdlYqrggk6ZdADeoJIQLE/S4+zddiXtOY2cbuouCX4jqAg"}}}},"OOWGKJKCAL":{"signed_curve25519:AAAABQ":{"key":"Tuqx8OYyF2zovDEtXL3Vs76MqKk53YRPwc25Rt/0JHI","signatures":{"@example:localhost":{"ed25519:OOWGKJKCAL":"r/WJJTnqWgch4qXIszqKINKXg8Nt8FNrVD65zNaMBBMc0Ijyr1PI9uQ/l1ob5CSLFq6AghGM2BQ6Ii/8eJ/FAA"}}}},"OQUMZGQBZC":{"signed_curve25519:AAAABQ":{"key":"3SC3CYb6asP/0/uMT6IDAmzJfAgnpsbaZAmEQBsJ+Uc","signatures":{"@example:localhost":{"ed25519:OQUMZGQBZC":"hYfJVAwPzSBRONyTK7N+nuwrKzfFYGbkOCKkp4XeEpLfpp8QLpbpuyXtX3ZnixZ579veMsBBSmneIpbYQdI7Ag"}}}},"PCWYNGUAAB":{"signed_curve25519:AAAABQ":{"key":"dGzuxckv84idCdXr6pPrVmpDfEt7YQfssvp4GKiGUnY","signatures":{"@example:localhost":{"ed25519:PCWYNGUAAB":"4hj9MxeXRGcF7I941CWY+8dawPaXLYA57lwMJ5TV62jeQrxGgm/7yQHSdenwdbYIhuvs9oD1wbP+t8n6HRuCCg"}}}},"PFXZAUEJSR":{"signed_curve25519:AAAAAw":{"key":"C6nKhK1Wr7IxWjsbs7akxAGBvcH/Rmx547xnP5+zGCQ","signatures":{"@example:localhost":{"ed25519:PFXZAUEJSR":"lSwwfnCBCLC2bA8Q05uqJq2k03NFrCu2fIik1DEKRhG0BdJmAZOvCpKDpWANN7BkU9YPLsgPZzF5V8alihDFDg"}}}},"PIRRYJJPEE":{"signed_curve25519:AAAABQ":{"key":"getZl6nYQJy4eUwyX3Mpdj0GtZUNpRH1IVqxBjYj0Sc","signatures":{"@example:localhost":{"ed25519:PIRRYJJPEE":"lMiOsoLCjtVgiIZPL6FtjA+ZUcohXaJVXXBDosLCzRzt8GiNtrVKgfBSWLp6+nBFMDHAJDgNRi2g9wyAoOjHDA"}}}},"PITUXWBZKW":{"signed_curve25519:AAAABQ":{"key":"Vy66LQ4mdNqnRGS9PfbUEZBBwMFZXaQROkY+7G1DNh8","signatures":{"@example:localhost":{"ed25519:PITUXWBZKW":"LzEde1/0bQbmMc35jxeuWsVR54RenM/QagnRm7yDVZwwwjDWiK8N+w+8ZOBhp2hbebl0vCuja2k2sv6wGaR0Dw"}}}},"PLKUNRVDEA":{"signed_curve25519:AAAABQ":{"key":"rvviVyDH4f/oWyGA6gxNjq04aw0wcKYwBNboL/youAA","signatures":{"@example:localhost":{"ed25519:PLKUNRVDEA":"pQqvm2m4DAetw0pQO6zh9Eml6zv3Hb+uketW6rag0DXVHaCc26jOsa9w5QyzIAoThs1VRzIShrV7HTtITh+YBg"}}}},"PMTFYTFGTD":{"signed_curve25519:AAAAAw":{"key":"EWTpwhcQmMHfyJhg+nlU7NLYrGrcxZ2U5AHVFfepg2A","signatures":{"@example:localhost":{"ed25519:PMTFYTFGTD":"IY6r6cm1FouDPzcIK/FGs3ZJRTldPyE9vTgss3TPb1enVQDObwaoeci/YXdhBLD/3UmFTlpfmrDFPwZOSvGcDw"}}}},"POEMMCBEMR":{"signed_curve25519:AAAABQ":{"key":"OIzB2zd1+agcQJvRS6KUsndzBi4pEj5pnxpzYPEcVlg","signatures":{"@example:localhost":{"ed25519:POEMMCBEMR":"LPCe18RY0NOqocPDDIOW8npLsinH9V31zwqnevGOjAZwUd4DI95xbqFQ2oyf0zdCBZT2hA6A0md9m6vj8wKiAQ"}}}},"PPHATICKBQ":{"signed_curve25519:AAAABQ":{"key":"GwDTTkGb+5lA/nIuQFSLP/DhcCIqZGKVW+ivVcTdYz4","signatures":{"@example:localhost":{"ed25519:PPHATICKBQ":"swjm0Q5aLiKmOdXfOHQV03v64UVurX1cxWEQx6pkIgN4GcrM0uj2OkJ8KlIokDBTKUrDFiNvkVKSgeQGDSpfDg"}}}},"PTJUYANNPX":{"signed_curve25519:AAAABQ":{"key":"mKOGTrZ2/ByL5F3YkF5T2ilLTeELdQqwY6nKRYkjiH8","signatures":{"@example:localhost":{"ed25519:PTJUYANNPX":"YsVgrAv6OrO4ju5tJOHbS6Zi6Syyn2AklnqYTQ7hqdE1V7rJIEMIn79vJnNfYm9IWzZ3cgVlRs6+q3DybymNAA"}}}},"PVSCUNMZPC":{"signed_curve25519:AAAABQ":{"key":"4cfqHlyFJrustJMMLMoMIXFFzppvkJj7IOb7p7skfiY","signatures":{"@example:localhost":{"ed25519:PVSCUNMZPC":"SrCmBSBZxyEViE3qDiYIpie/k40N9PJiDolwW05rjQYKrETTBPCJq4rHBgu8lJuOsuaDnoTOT+lpyd/v6uWbDQ"}}}},"PYKAGIYQNL":{"signed_curve25519:AAAABQ":{"key":"mqekWemvWR5RT+o2uLy91HYw8qqtpDBzfHWaHiH1PDA","signatures":{"@example:localhost":{"ed25519:PYKAGIYQNL":"NR5NCIMoPuXSEhlUDjsP9UnBCh/FJ+LeDPPE0aOaoM+1mBYFezaCWtDVfi6OftSHBECAM3R6KaYH70fhCxcbAA"}}}},"QEVCPMBWQX":{"signed_curve25519:AAAABQ":{"key":"NOLqrPubBokQcWYtzI/lc3SsQzr/6FP6zFUo2/cAQGk","signatures":{"@example:localhost":{"ed25519:QEVCPMBWQX":"QAHKj1grBgcI0cCwgrXlLbZSndibdGI7UkVRWBa5UzfzFEYeWKTYYwkSMRGZNecRSjLmVTHDDGEN3PeKHMO8Cw"}}}},"QGBSWWBGRL":{"signed_curve25519:AAAABQ":{"key":"zC89+w+d2IMeDVpuDBBvxSbbitEX+bn0wWkx2xDH2XU","signatures":{"@example:localhost":{"ed25519:QGBSWWBGRL":"KFVht65sBVukCeh0/vGxZ7HbrudbsXWXUDNeIQr/b9DmXd1w6cAhVtc4WGJXLYSp8pcYjxkL1BrqXBIHNfgLDg"}}}},"QWHZAEXBJB":{"signed_curve25519:AAAABQ":{"key":"pd+Hg1O3fnsQWpl3/G7fNBBopO1ldYsNR5PJjc7tbmE","signatures":{"@example:localhost":{"ed25519:QWHZAEXBJB":"dnSodBcZiIas0b/hxWKQMgMSdMsORZLmcx2a6dzKp1zxtGAab6Jn1cOx7G7j1P0QG7ZJIMcjH4cFV+5g2AO+BA"}}}},"RDRBMAITSM":{"signed_curve25519:AAAABQ":{"key":"wg3kZ2R8X50VEl5U4sCXeMA4pgBK3Zzhj0Gy14hgAQs","signatures":{"@example:localhost":{"ed25519:RDRBMAITSM":"BEeEnEN/IPUPF7Om+sKVNqHqJgUiU0t0n70FpDUuPNL2asFTuGawIRDrUGsNGJBhQK7dI3hRFTC+q9Seem7nDQ"}}}},"REGDZMINHE":{"signed_curve25519:AAAABQ":{"key":"spP/9TVeRUiwsx7srT/X8cAzxGOhn3lhJBmwtyPfc1Y","signatures":{"@example:localhost":{"ed25519:REGDZMINHE":"tpN7Efa+8KzrXLYT0wpOdIXoxXe5KpgElWz99mxSYIC7zf5Hivez60VUWGwQ9nqXWlao7vsgJBjv1dkarIPMAg"}}}},"RKMGIKJYLD":{"signed_curve25519:AAAABQ":{"key":"WXZacNrPQ3anivmmVkMgO4NXqvuTCrHKGJa+L3GIyCM","signatures":{"@example:localhost":{"ed25519:RKMGIKJYLD":"abljyBMvI5vZhkAAwd6u3IKfTbT05/GZddLjufpL12ScybIyCOneVAm8bD3qqUzSHsXkKP12VEJRBovyCmb1Aw"}}}},"RMFBNJEHOU":{"signed_curve25519:AAAABQ":{"key":"DMGtNTLxGtBG5h39lAJD07Crawq5QQMHSSj5YDp3zXk","signatures":{"@example:localhost":{"ed25519:RMFBNJEHOU":"2SKPf2tlbAWWo/7dybPP/xaYjyVsIvaNgJyzqlWOANqOkL+2nanfrhdyIaAg53SOGcNnTz5YWYmrmVy0VbCWBw"}}}},"RNCCPGZUFA":{"signed_curve25519:AAAAAw":{"key":"IhVaRjse0bZG/+6n1yPY6ncz8Uc0CHib72Ls9hrxBFo","signatures":{"@example:localhost":{"ed25519:RNCCPGZUFA":"rGwEDQixUhQdAEXFx34D6YoOK0zL0lHoTkYxory+cW563jj38+Gy+HuCQ/7xGNg081+hgb/sCMy3gvjmA85QCA"}}}},"RQIKNNRLWT":{"signed_curve25519:AAAABQ":{"key":"+C1abhNI53RuyewxlKnmeQ5LGINpjN0zzpGQKwsR/n0","signatures":{"@example:localhost":{"ed25519:RQIKNNRLWT":"K0S3Hb+HtxhZAKn/mNN+unfx5z6yhOJlmNCFAADCxoip3gEnv1ZKNfye0a1OdeW5m6EwYgFS7Gk0PjpxcuKsDA"}}}},"SFJLADLVLN":{"signed_curve25519:AAAABQ":{"key":"mLPBefUN5dtdwxTSX/RZQ6L7cWLhrQ9JbILoQb/8xVo","signatures":{"@example:localhost":{"ed25519:SFJLADLVLN":"1a5ENQ08Y3tzwo0bY0tihY7q09gFICuTLjfIcN7C/3v6x+zT2QEMymlAMzLm+cjJaIfs01jLyP1vnSQn5mJaBw"}}}},"SFKYKWBWOS":{"signed_curve25519:AAAABQ":{"key":"xtGqXvZJCky4TOW77p4nYeBAmcmCdXwyGrzkQDTSRmw","signatures":{"@example:localhost":{"ed25519:SFKYKWBWOS":"mduwl0FThUlKhT7oW0shPDH8YRDGL3ORSStp0nT3azzb/RtV5Ndj4be7jvOnGxCPCsmiSQ8fe3igaUUE7UHCBQ"}}}},"SFQMEHNZZS":{"signed_curve25519:AAAABQ":{"key":"X3YZD0f3Hh/VsLoQBN7q/D3VmmeZhn/Ctpgi9XWpj1U","signatures":{"@example:localhost":{"ed25519:SFQMEHNZZS":"Od+MLIeXuTPev6tLuK80BVQaIzN84Xm8GrmOb7qJlc04xXdPxrGed6L3RKHEeHADLbcZUvnoOO3+5DOq4ofBAw"}}}},"SJJONGRGGH":{"signed_curve25519:AAAABQ":{"key":"nBLmKUOx9eVq3QnP20B+4dAM74Y8ZYwC+CXGMC0hayw","signatures":{"@example:localhost":{"ed25519:SJJONGRGGH":"Q1a7j8Uvio+RALpVyHLa1ajC28NekDETHGymIgZGH44eK+UBxdXO6SBH4qDTE80zVuG2vEBdpW6+A6+mwdzfAw"}}}},"SJMFYPCVTT":{"signed_curve25519:AAAABQ":{"key":"IS1G2+ma34zs5WsUch2xpLw1I07fzlfxTzqpvwt3Llc","signatures":{"@example:localhost":{"ed25519:SJMFYPCVTT":"UGua/56SkG3whnGUw1cEnoPylmVhBWJVNSY/pVGegxFkYuF3joblIiH1JIkUHr5Yh2aDr3j1EzYnp18eH1BxDQ"}}}},"SLMEYJPPRU":{"signed_curve25519:AAAABQ":{"key":"EXzN4RHRgQiINQCsBP1SOv+VolPL+rHBaftuBsJw93A","signatures":{"@example:localhost":{"ed25519:SLMEYJPPRU":"zDCeiWUvy1gG8lyAb6IpI2+ipj+uBU5Y8Bb8KKorw9aFigKMGvwYTK4OhvSeNrsa+WQeqjZAXh0nNtTwKdKmCA"}}}},"SPQBMGZYIV":{"signed_curve25519:AAAABQ":{"key":"AQyiIz6RGeftx2bKckeeqZjKmPBWPRta1BEb38Y38GU","signatures":{"@example:localhost":{"ed25519:SPQBMGZYIV":"ukScLq/GQrjYwKytzwI4YcZ+fVUEf0I/rvy//VP7dP/cWIVOMktOCrm4JB3fpn8lCmf7yxbkJu5i1er4ebt3Dw"}}}},"SRLQGLPZOH":{"signed_curve25519:AAAABQ":{"key":"LquIiMAZnx4TkQwf76iDrkdtsJ62QPnhwKxWmteslTs","signatures":{"@example:localhost":{"ed25519:SRLQGLPZOH":"xkt69nBhv4M1/NLrbOWiSW6q6rnT/N+Yxk04O3F1d43yulpJh30aA/HIRQNSo4nD2IIg6fulvpNF88149aVJAQ"}}}},"SRWZNGFQQK":{"signed_curve25519:AAAABQ":{"key":"RppHoXv8KOEg0pMsHN2fYnfZHjLFB1HfU/Vr9TJFrW8","signatures":{"@example:localhost":{"ed25519:SRWZNGFQQK":"QfUY6mIZAPH1WuG4FXjOcK3nb5E4H2BGLp71SyqAyeSIxhW9X7+6UnvPdAJIKrzqsS0zPiT7GtNPmh7gW49TBQ"}}}},"SUQWYILHRZ":{"signed_curve25519:AAAAAw":{"key":"R6lNl5jRxhioTdrNJnx5clkR2k51ldV3B0h5jX+5JC4","signatures":{"@example:localhost":{"ed25519:SUQWYILHRZ":"aOLQ0jPUvjTmD0X7tsRPlNBBpnf3KednMC6O1ILHFKAe7npstBbmVthtrc3Pa9jPJ6d/cimEj7Xupj/DvaJGBg"}}}},"SWBGZLVCMH":{"signed_curve25519:AAAAAw":{"key":"tNo9xIeKKbOQVVsCML7sbW3qeIHzSIxjZxBWtUmhbhs","signatures":{"@example:localhost":{"ed25519:SWBGZLVCMH":"Uvwx9OVpB6w0D37u69vK+MOtVy00OzfhTgcYHPHhioHZIEBCWWrmKOXcKV6C66m33JvqUMRBUfsJkQ8yMYlYCA"}}}},"SZVTBGJGBL":{"signed_curve25519:AAAAAw":{"key":"FUT/CqpFfwrHVGS5ruqH0c+PNGqD5POv9s+394gSWmU","signatures":{"@example:localhost":{"ed25519:SZVTBGJGBL":"GI1t0IPwanIEwfj0qK+RxgARB3fY4MJRlIJxkMA4YM91RxKT1wqVKpem1HCRMvqbxlFFBAuLI6tZF7YD9mZ7AQ"}}}},"TCOYAZRGZK":{"signed_curve25519:AAAABQ":{"key":"KOz9ucpAnKBA++zPswsXkohuh0m7z/BaEVKUuJw1Vno","signatures":{"@example:localhost":{"ed25519:TCOYAZRGZK":"uq0CFVJerfkBzE4beMUpsXpeim8CmDlu1zNvPDkGFJvlUYIpq3TPqj/HI/fNfviEs9oYwlZ5nN5/9Ey06kubDA"}}}},"TDQKXBKSKG":{"signed_curve25519:AAAABA":{"key":"4FM24OsPJgaZF6iLW61bZxT9/9ykfEyYrFEfJm15Am8","signatures":{"@example:localhost":{"ed25519:TDQKXBKSKG":"an66kGmbzaJmwze+enZbGQwYRrQUgwjAQW/as6SaOI4qZ/+0PViLyo/3SX9ulIZQdtdZuRSPV1V0ogI+/wGGDg"}}}},"TXOCPFNJLZ":{"signed_curve25519:AAAABQ":{"key":"KN+JqOuGBrYlYvzEbbVacFqk4LIknUrMRpZynXpLekY","signatures":{"@example:localhost":{"ed25519:TXOCPFNJLZ":"QB5PNrI5+gnjEHWxtHC9Na4+0SHGNVYZPiNnXcmKhlQ9XQpGWRq1i0K6/axcmLxeIl70hjZ9ieCOnrbYzqaYBQ"}}}},"TXWZDGLUCI":{"signed_curve25519:AAAABQ":{"key":"QSJuspIfKbxt135IFRs8NZzy4X/LWHolIMrAzFM9B3s","signatures":{"@example:localhost":{"ed25519:TXWZDGLUCI":"a338MAD59U2FPzuLiSXcb/Q7B1dE8b4TL53DZUXrNehJjlUkTiqmDWfGDLOAq7PnsDCFXtjy6Y0XSJfrfwz1Aw"}}}},"TYVVHABYWC":{"signed_curve25519:AAAABQ":{"key":"ayDn1US9YUCrtLPDhQraPdu/Dqum0wD/OrhRukvxEig","signatures":{"@example:localhost":{"ed25519:TYVVHABYWC":"7amVLtIyaRWbo2MZoPlQY8iiiHP7SLA3TaWcxfvA4BwFOlGckNEcLsLMCFWXOh8eW9q/syOztO0UIoPiR2pxDg"}}}},"TYVXUYCJYC":{"signed_curve25519:AAAABQ":{"key":"RBQ2YXiSVaDLS6AdXcUsdvSf2Kl6S+c+6WoJHLSKHRY","signatures":{"@example:localhost":{"ed25519:TYVXUYCJYC":"eUb6G1dDN65gl1HFzyIVrjQrwvaTixJWdKb0fBXovdTnQiiEKs2PuPyRCH8LU3UngB6e24q2L9uA7JF5Dt7TCw"}}}},"UJFFDLXBFG":{"signed_curve25519:AAAABQ":{"key":"KHyV9f0VFN2U39NpKD0AAfgf+5JPWH0rguIpHeLWQTA","signatures":{"@example:localhost":{"ed25519:UJFFDLXBFG":"Od/8j/CHvOAkSzNWDv3vERk0BQYvPloHDyojC+ohVvsafhKSJEuM+jIU7lxQHtqo0Rkp1AsmO0P2vDcbnQx4AA"}}}},"ULJMVHVAOG":{"signed_curve25519:AAAABQ":{"key":"y3dwcVbLHeVa5Rd43nPqfKlqHQEF4KMAThSDgM/YDSA","signatures":{"@example:localhost":{"ed25519:ULJMVHVAOG":"n6NA3X/WHtMPyArLcpJjnl+t0TXvq7B845/9pJFE3kZQjZ9thIwQVV1qUIs3EvdjdqoU5BsMPxFFD4jxWqi0Ag"}}}},"ULRVAILNPW":{"signed_curve25519:AAAABQ":{"key":"zwy6uSf6UWD0cXXmzktB0dO2f24Cq55DfPStS8I2CVQ","signatures":{"@example:localhost":{"ed25519:ULRVAILNPW":"gs/OnCJx8EBQrOxzi/nGpKWd8wPss8lbAyVifEYI79SkZEUieIqRcOdo9OOg1yM89mPFJRG9uwO2uD9lEe88AQ"}}}},"UNOULJXOXX":{"signed_curve25519:AAAABQ":{"key":"oMvdhZCoHj6Dxe67uY9gIm2mnbVAAMi6NNF5yfPEM3Q","signatures":{"@example:localhost":{"ed25519:UNOULJXOXX":"J4QUjCgohmDVlCwf39U//aqeBF1Z89SPpajZkhKWG3bU8Neuuj6cSRNEjxVsREw58VJyTZbOq3EK6BZ9gMNsCQ"}}}},"UPMCUPQRDH":{"signed_curve25519:AAAABQ":{"key":"PWgUmtktUgIUmSNRdfvMKTdonTZSeiIyoEOpwlioWA4","signatures":{"@example:localhost":{"ed25519:UPMCUPQRDH":"xuSDWBaKrpXNvP2PQ1FVHG2pHjjAG1FqltitvUWLcJVPLvjDAlEhL4XQudYjx3I8zbvVIYtPSue63lLZGlxhCQ"}}}},"UPSICJSJXA":{"signed_curve25519:AAAABQ":{"key":"uArB2L11//UuHLvFxMeDrHEjZPQih8HA5LX3LdeaWkE","signatures":{"@example:localhost":{"ed25519:UPSICJSJXA":"nvgdFFWR0P+yTjtW8afOqURMpEst3NfNX16jl2vdsir9pyhXvvyGOjmclDYxvlFQp5fKO1kWnUSb1qXgYDMYDQ"}}}},"UTVPFWOSFH":{"signed_curve25519:AAAABQ":{"key":"KBJi8THRGfIge2/DpB/CCPzcykDYE8ICaEEFmjxaQHM","signatures":{"@example:localhost":{"ed25519:UTVPFWOSFH":"im50KkQkf1vt5XWAPoqnAbhY4gvHSeTJiJgwCzL/OM4Di/YYmdGbrfSW4g4lz4oERLlQrQLs4QCN97DYArXZAA"}}}},"UWKQEPYTKT":{"signed_curve25519:AAAABQ":{"key":"HYdlZiD4ezrg6J9R3jyQTSun+7g6g0wUUQH6N3iNYkc","signatures":{"@example:localhost":{"ed25519:UWKQEPYTKT":"BTR/ZpJh8J25+oP84Q/0FfhMGJaZysuU+y9f2QOest2GIr8X5K0ePiLgBPFeHEBd99aYbtpzMJgVU7YHowS7DQ"}}}},"UYIIXHSTHI":{"signed_curve25519:AAAABQ":{"key":"RB8Rg94m/XOw7aajfhplTJbjGqIMG3vovnWVl7zUjGY","signatures":{"@example:localhost":{"ed25519:UYIIXHSTHI":"+bnWYVUL5xVqtPYGcCYKvkHae2K9njyOXy+IJA7i/TlQ2f//LdE2tClM0jeVeOZEsunBBc8OBj6JV2h2BBpmDA"}}}},"UYIUDLVJWP":{"signed_curve25519:AAAABQ":{"key":"btIdnh5VcFLXEn02Rercxhu++Zs3s1dPLXmGy1gj5zQ","signatures":{"@example:localhost":{"ed25519:UYIUDLVJWP":"lksUHpn87mQYJ5a94LKvHTkV7smfrg82MnlwuCozf3nLuxO46qwOQtEkmZPAabcJHbRQ5rdi8T/H38ov8BzMAg"}}}},"VEGPLVOEVA":{"signed_curve25519:AAAABQ":{"key":"F5Ftz9qLxmZmWDEdD7r352Kn9R4NL+acuch7soAIdjI","signatures":{"@example:localhost":{"ed25519:VEGPLVOEVA":"o5fxPEKcFiqd4fhhri7fr2xy6kLQmPgHnXoiS7Get2wZSxxrDTU96wVo3McJagD2/u1gd+NoqVpL8EUCV88UAw"}}}},"VJLILRHZUW":{"signed_curve25519:AAAABQ":{"key":"2IujXgo0ok0G508s4ToK5tZ6JuwVBGRTZHUPrT5hDkk","signatures":{"@example:localhost":{"ed25519:VJLILRHZUW":"9a2sK2tLY1MvobVp2yrBPhd0miKMu5AFwOYfsZogBO73c7dy+JHZQ/JGegzBp6K9iHPA6dL66wYm2UVtAANGAQ"}}}},"VKUYMBNZQQ":{"signed_curve25519:AAAABQ":{"key":"fdO42BEwP0Xzn2PIin+vuldPwMheySw9iK7tjHLMFyY","signatures":{"@example:localhost":{"ed25519:VKUYMBNZQQ":"z29JbKgyztcsxwhMKw9nYj77k9zY3QqyWQT0Pys75AC015qtEICyn/sbbhGCxfPyqsKm7+Ltvm1OqSybQrU6Ag"}}}},"VRSCQXSSNQ":{"signed_curve25519:AAAABQ":{"key":"djXakT2j688707Od7RoIHjqEZI2FneCr8or2rRYj5FQ","signatures":{"@example:localhost":{"ed25519:VRSCQXSSNQ":"GtSYeL+tkOGPipShCyVNQY9tmg1HJ1BIvpUqhF+M6p6xbWJWQSv6N7qKM0mTrU8nvbly8CiYG7fcMrq29S0NCw"}}}},"VXTRVBKMJB":{"signed_curve25519:AAAABQ":{"key":"kuXok8lJ2zwa4kSi89FT/7Pooa3t4qTrub8Ordy4uTA","signatures":{"@example:localhost":{"ed25519:VXTRVBKMJB":"jY9/JnJ5cv1YWvyhYt2IgifreS9MBLcw74/juVwo7K1C7P2vuseotHV9f6JdPouzZSc3iKZak+gigkvJAPyHAA"}}}},"WBXWCWRWFI":{"signed_curve25519:AAAABQ":{"key":"FLneOCkNi6NST/O5jlF5Fn3dWgZ9pnWr3HC++Z1t+ig","signatures":{"@example:localhost":{"ed25519:WBXWCWRWFI":"kvFyMgpkq9aevA2vlDOmMPEBJ3p6mJFOhq3oI4CPqQoJ3ulVb7XV8r/5qHclnCpIJPK4mZJ8HlA942vW9N/MDQ"}}}},"WINACUTKQK":{"signed_curve25519:AAAABQ":{"key":"YY1iW2zaqy22FnuQnDXiHl25bpLk45fkv2A9tHzjzFg","signatures":{"@example:localhost":{"ed25519:WINACUTKQK":"Hge/EXnK6N7WHaQS7fKI7Sl9GRWwv8ZQBWPd36qH5MeSIc0Q94eUhusok6ta+62OBf4nzJkhEtDUqwDV0iWgDA"}}}},"WNHNFCTQDF":{"signed_curve25519:AAAABQ":{"key":"30NcSoAJE2tAVu4JPf2a98KpRyWJ4E423ho/ZV5klBg","signatures":{"@example:localhost":{"ed25519:WNHNFCTQDF":"L3MXSX9/aud3uU7jWGlyuuyHabXWyM/pO0vsO0ITO0KlKHP6irA6oM2YE+Tfz7tjPpu8AH+R7Q384Z//2JRoDA"}}}},"WUFSXGMGDC":{"signed_curve25519:AAAABQ":{"key":"kNtjFE8rBDZc8DodzBqtsBtES5wfyNCq1BeMGSJ8hSw","signatures":{"@example:localhost":{"ed25519:WUFSXGMGDC":"RTqYH/awDSocgz0IcapT4MN1BafNrbOhs/eWgiK/jGk8NcABaHwBRmiinJPEAm8r5hymI2r2OZbDziNsjMjQDg"}}}},"WWFJHBNUVS":{"signed_curve25519:AAAABQ":{"key":"pnHgn0Jaq6DvR28Vivs9kMXCD7xtlgFA4NYvnEgo80E","signatures":{"@example:localhost":{"ed25519:WWFJHBNUVS":"dbwjnSEr3kPvqMQKtmitxzmTUNoDkG88OeNta3uoKOngNKRbpeccoDe8pVwUJcnVf06W1XZyGBWfc/4IhFEoCw"}}}},"WYEWNGNQLY":{"signed_curve25519:AAAABQ":{"key":"m+LxWZViRuTGO1sP9YoeMf9H0qaIHMQmg0bnhxu23Dw","signatures":{"@example:localhost":{"ed25519:WYEWNGNQLY":"H5R8rmv1JQ4xownu0jO1dgBzFG3mrTc77p8Sqfdd0driKs2yiZQxcOmUjjQLgV5CTkjNQaQD13ooIv/dmFy1Ag"}}}},"WZAEFAVVYB":{"signed_curve25519:AAAABQ":{"key":"u+wyAfIvMOOy50g3KK2Vj3edH7SrBB9lVPZBHLWII38","signatures":{"@example:localhost":{"ed25519:WZAEFAVVYB":"9DRXWbZ8AkkgfHmEB+PKhgZiUrZQWKu6Sud+z8X1TWWuoLXUMq5z7fe8M7y2XGt5b1hu2zEGc54nTvnVEqT9Dw"}}}},"XDGOZRFUAX":{"signed_curve25519:AAAABQ":{"key":"y68SYUbWvJnyUi546cr8eun3YYSvumv/GM7swY203CE","signatures":{"@example:localhost":{"ed25519:XDGOZRFUAX":"pKq+SXQkE+8uvL1fXUCUU/Ipz1VDZry60H1U1mJAywTVSc0TA6G4toZd/nVeU1cmYLDqrUgKIxyWeb9t3Jh+DQ"}}}},"XJNSUWGBNH":{"signed_curve25519:AAAABQ":{"key":"1CH8UzLh/spbZAmrm6hZ87VGvlyoLfrsvI2+q1AgbCw","signatures":{"@example:localhost":{"ed25519:XJNSUWGBNH":"U7vgPxdDNsNm8CXmGKuLWH0xAowa7G6W44md75xXFCj0LCz2Mzw/+VA67b6AcXloKI3NUeypgNJOcvhbPa2PBw"}}}},"XOWLHHFSWM":{"signed_curve25519:AAAAOg":{"key":"nL33vdK1f9IhVipN9oUUzjUhIL0+rGwBOA+dCkknoic","signatures":{"@example:localhost":{"ed25519:XOWLHHFSWM":"V5PMnT3zgkvHLxo+P1wH1XFwaiCt37GvE1VyltYkIsv9xbrihH24gYTgQG+oktZgH9PZArg5x0/VGcdA6qqhCg"}}}},"XVGSJAYERR":{"signed_curve25519:AAAABQ":{"key":"xVW9NhaaLKbdO23WZCtuyBrIpKJqC91Z70xeBDSSJWY","signatures":{"@example:localhost":{"ed25519:XVGSJAYERR":"z+BGFh+zfyjDnOg3yEmUe05PxicRrtEQPcf6WxA8EP7XvLLcbhROyGKftvxhQ3xupAbFNPz2I6ErgIUHhR3ZCA"}}}},"XYMAAFPCXM":{"signed_curve25519:AAAABQ":{"key":"kR+9jLf0UfGemfpEsebR4pZbJxg3uwrbYgOaotjWhh0","signatures":{"@example:localhost":{"ed25519:XYMAAFPCXM":"9tgu3/EDmsbCZJRPng95Ew4cou1aeuYgo4sKXmHHvH9RRgxRoBG2Qxp42LQqUf5JbRoaLgexjDvbrNQaPOEGDA"}}}},"YEETQDERLH":{"signed_curve25519:AAAABQ":{"key":"bNgXd1nPdHv36xBLJz2kSWtmQfOKo3J4YmGO9ZCejwg","signatures":{"@example:localhost":{"ed25519:YEETQDERLH":"RS8sI3k+g1uwu/91KRiG1dvaq1YFQWYPQGAZtqtJL0Zg/wbTFV1N8exTGEo4OxCi45uTJ3r3yK2/har0yodhBw"}}}},"YGMINGOBMN":{"signed_curve25519:AAAABQ":{"key":"eoQwGbDbtzH9guNz5kt+P4y0wD2DG61I7eW3ukYQpkw","signatures":{"@example:localhost":{"ed25519:YGMINGOBMN":"jirRu8NPtHG96cAM29hMwrUZUh9mi0un9Fm8vMTyALm0FDTsJYx8hJiY2rYGtH3gF48W4zcLWwJa9KZ/1aSeBw"}}}},"YLSFRBNBIQ":{"signed_curve25519:AAAABQ":{"key":"wq8ScYQ33p868VizuV3BR3PHtUwaPB8YGi3jzGCgdSY","signatures":{"@example:localhost":{"ed25519:YLSFRBNBIQ":"cT5BnUKOJzD95jIjXGq1n2Qt3xKhRjlJeUe4cUNkduQc7+2+t14cBsiGIsZJRZzogE3Ut/8O6WZP9f1EvBgPAg"}}}},"YMSRVAICJW":{"signed_curve25519:AAAABQ":{"key":"C8vbpciQBiM9jh5x4nY10hc3MY4WyLLJ6da/d0KYnBQ","signatures":{"@example:localhost":{"ed25519:YMSRVAICJW":"8LyCHwYxlhIRf+0Iilvbmu9NgkvkvO7dHpLO+ipBOjtRTgpkDYw+L0x6L+T+tieiqr/51+Y/vDI7OYWtxUv7BQ"}}}},"YPRSATMMOG":{"signed_curve25519:AAAABQ":{"key":"59oiAPEIGNCd6rJVLrtD21K+7y9jRj3XFJK9yjG9sWE","signatures":{"@example:localhost":{"ed25519:YPRSATMMOG":"rNYSNNIlagNBoSAujk0i2o8YdLoSMw5evu+FAE9YFmnJoV9OK9dKC7CXKdHFOnm8NM+hmMloPFgyVkFvB1iOBA"}}}},"YPSHAVNOAF":{"signed_curve25519:AAAABQ":{"key":"7KCLKyTLrOcjDJ2Q73VPipZLo4zLnpoKfN5GY9eh+mw","signatures":{"@example:localhost":{"ed25519:YPSHAVNOAF":"AJ4A5GOM3yD4GHfegaFMecObjjS78TEaO/S3WfJTjT0zCcomy8LYtdjPLmg/jB4Ph9LLQuEB+UOmYexO3CRoCg"}}}},"ZCKUKLKSTF":{"signed_curve25519:AAAABQ":{"key":"xRN+INH//ipFSVS7Nbb1Q/qQ12Ty8O4v1wG8UrAQo2I","signatures":{"@example:localhost":{"ed25519:ZCKUKLKSTF":"ZYyjyN4CIOSp+t21oBoED85UB2sM3+xxNDQ0MrJZnwvh4nHCQJk3v0VBGQ8XWtquU1/nJR1vl4++C1Omoa1lBg"}}}},"ZCOMPAXRXZ":{"signed_curve25519:AAAABQ":{"key":"VcdhjR4vbU43zUpYE9rNRO2lm8tb0H2ezboxvRF3iHU","signatures":{"@example:localhost":{"ed25519:ZCOMPAXRXZ":"7TYpEa+iUwH4dHFEHU6/qc0G8fFVv3D4/fRKo57O8e9yo+CDunutMC20azFJiXijjMUnexqvhlSu3lCINXFnCw"}}}},"ZDHFMSACDV":{"signed_curve25519:AAAABQ":{"key":"0aFaVokTl1WnqVrlbkFU7T1Zx9yw2zv9q75RZgoR8SI","signatures":{"@example:localhost":{"ed25519:ZDHFMSACDV":"VcERdTNlYwwZfCGi02cnNUfB2KE9immPa8HNX9nuwd1tdRsAusxL2tgI8nJQTMaDEUiuu3E1KseRc1wMSGiIDQ"}}}},"ZLSGHJACJW":{"signed_curve25519:AAAABQ":{"key":"Nfv0q06oElGow107VfASK2F9cqxGisYB8Xw9qCbSfBk","signatures":{"@example:localhost":{"ed25519:ZLSGHJACJW":"dRacsgbd1A1ppOkQHWSJgR5fCPnnd3qDJdb6Wv4yplg3N0D4ii//8nePmxT9/MrKSDV/VCMLPrDEMdossOlCCg"}}}},"ZMKDSVIKPT":{"signed_curve25519:AAAABQ":{"key":"tiFh7Ogyw8NzpR5RfMI7mhLUcNMVACgS5BGc/fouJhA","signatures":{"@example:localhost":{"ed25519:ZMKDSVIKPT":"dO4tYN0DzyRmAujbvo7HjrwNvH8nQEGwRz6jnhFb+xKErohppTk3R7A0l3Wong1upfAtkcMkmQzxpkJsX6PxBQ"}}}}}},"failures":{}} From fc6ff4288e6576daa60e9cf7f08a0c84c94afe92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Sat, 27 Feb 2021 18:34:46 +0100 Subject: [PATCH 32/39] benches: Add support to generate flamegraphs when we profile our benchmarks --- matrix_sdk_crypto/Cargo.toml | 1 + matrix_sdk_crypto/benches/crypto_bench.rs | 17 +++-- matrix_sdk_crypto/benches/perf.rs | 78 +++++++++++++++++++++++ 3 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 matrix_sdk_crypto/benches/perf.rs diff --git a/matrix_sdk_crypto/Cargo.toml b/matrix_sdk_crypto/Cargo.toml index a14cf0f6..99d35532 100644 --- a/matrix_sdk_crypto/Cargo.toml +++ b/matrix_sdk_crypto/Cargo.toml @@ -52,6 +52,7 @@ http = "0.2.3" matrix-sdk-test = { version = "0.2.0", path = "../matrix_sdk_test" } indoc = "1.0.3" criterion = { version = "0.3.4", features = ["async", "async_futures", "html_reports"] } +pprof = { version = "0.4.2", features = ["flamegraph"] } [[bench]] name = "crypto_bench" diff --git a/matrix_sdk_crypto/benches/crypto_bench.rs b/matrix_sdk_crypto/benches/crypto_bench.rs index 1a9e1501..e715dd92 100644 --- a/matrix_sdk_crypto/benches/crypto_bench.rs +++ b/matrix_sdk_crypto/benches/crypto_bench.rs @@ -1,9 +1,8 @@ +mod perf; + use std::convert::TryFrom; -use criterion::{ - async_executor::FuturesExecutor, criterion_group, criterion_main, BatchSize, BenchmarkId, - Criterion, Throughput, -}; +use criterion::{async_executor::FuturesExecutor, *}; use futures::executor::block_on; use matrix_sdk_common::{ @@ -101,5 +100,13 @@ pub fn keys_claiming(c: &mut Criterion) { group.finish() } -criterion_group!(benches, keys_query, keys_claiming); +fn criterion() -> Criterion { + Criterion::default().with_profiler(perf::FlamegraphProfiler::new(100)) +} + +criterion_group! { + name = benches; + config = criterion(); + targets = keys_query, keys_claiming +} criterion_main!(benches); diff --git a/matrix_sdk_crypto/benches/perf.rs b/matrix_sdk_crypto/benches/perf.rs new file mode 100644 index 00000000..a23bf872 --- /dev/null +++ b/matrix_sdk_crypto/benches/perf.rs @@ -0,0 +1,78 @@ +//! This is a simple Criterion Profiler implementation using pprof. +//! +//! It's mostly a direct copy from here: https://www.jibbow.com/posts/criterion-flamegraphs/ +use std::{fs::File, os::raw::c_int, path::Path}; + +use criterion::profiler::Profiler; +use pprof::ProfilerGuard; + +/// Small custom profiler that can be used with Criterion to create a flamegraph for benchmarks. +/// Also see [the Criterion documentation on this][custom-profiler]. +/// +/// ## Example on how to enable the custom profiler: +/// +/// ``` +/// mod perf; +/// use perf::FlamegraphProfiler; +/// +/// fn fibonacci_profiled(criterion: &mut Criterion) { +/// // Use the criterion struct as normal here. +/// } +/// +/// fn custom() -> Criterion { +/// Criterion::default().with_profiler(FlamegraphProfiler::new()) +/// } +/// +/// criterion_group! { +/// name = benches; +/// config = custom(); +/// targets = fibonacci_profiled +/// } +/// ``` +/// +/// The neat thing about this is that it will sample _only_ the benchmark, and not other stuff like +/// the setup process. +/// +/// Further, it will only kick in if `--profile-time