From cc591cce1c60a1ca4c96d3f03239f6b3ccb574b7 Mon Sep 17 00:00:00 2001 From: Johannes Becker Date: Mon, 17 May 2021 11:38:28 +0200 Subject: [PATCH 1/7] appservice: Improve docs --- matrix_sdk/src/lib.rs | 3 +-- matrix_sdk_appservice/src/lib.rs | 5 +++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/matrix_sdk/src/lib.rs b/matrix_sdk/src/lib.rs index 8b5da4eb..044e44b7 100644 --- a/matrix_sdk/src/lib.rs +++ b/matrix_sdk/src/lib.rs @@ -52,8 +52,7 @@ //! synapse configuration `require_auth_for_profile_requests`. Enabled by //! default. //! * `appservice`: Enables low-level appservice functionality. For an -//! high-level API there's the -//! `matrix-sdk-appservice` crate +//! high-level API there's the `matrix-sdk-appservice` crate #![deny( missing_debug_implementations, diff --git a/matrix_sdk_appservice/src/lib.rs b/matrix_sdk_appservice/src/lib.rs index 11c9710d..75e52134 100644 --- a/matrix_sdk_appservice/src/lib.rs +++ b/matrix_sdk_appservice/src/lib.rs @@ -14,8 +14,9 @@ //! Matrix [Application Service] library //! -//! The appservice crate aims to provide a batteries-included experience. That -//! means that we +//! The appservice crate aims to provide a batteries-included experience by +//! being a thin wrapper around the [`matrix_sdk`]. That means that we +//! //! * ship with functionality to configure your webserver crate or simply run //! the webserver for you //! * receive and validate requests from the homeserver correctly From bd5e112a46816b889d0056f752ffbac47b18de8b Mon Sep 17 00:00:00 2001 From: Johannes Becker Date: Mon, 17 May 2021 11:48:53 +0200 Subject: [PATCH 2/7] appservice: Remove outdated serde_yaml dependency --- 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 abee49cc..a581c7b0 100644 --- a/matrix_sdk/Cargo.toml +++ b/matrix_sdk/Cargo.toml @@ -26,7 +26,7 @@ rustls-tls = ["reqwest/rustls-tls"] socks = ["reqwest/socks"] sso_login = ["warp", "rand", "tokio-stream"] require_auth_for_profile_requests = [] -appservice = ["matrix-sdk-common/appservice", "serde_yaml"] +appservice = ["matrix-sdk-common/appservice"] docs = ["encryption", "sled_cryptostore", "sled_state_store", "sso_login"] @@ -41,7 +41,6 @@ url = "2.2.0" zeroize = "1.2.0" mime = "0.3.16" rand = { version = "0.8.2", optional = true } -serde_yaml = { version = "0.8", optional = true } bytes = "1.0.1" matrix-sdk-common = { version = "0.2.0", path = "../matrix_sdk_common" } From aaa17535acdb75ebcbf49a8109550308384e9430 Mon Sep 17 00:00:00 2001 From: Johannes Becker Date: Mon, 17 May 2021 11:50:48 +0200 Subject: [PATCH 3/7] matrix_sdk: Fix typo --- matrix_sdk/src/http_client.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix_sdk/src/http_client.rs b/matrix_sdk/src/http_client.rs index cfd55be7..63c83e60 100644 --- a/matrix_sdk/src/http_client.rs +++ b/matrix_sdk/src/http_client.rs @@ -133,7 +133,7 @@ impl HttpClient { let request = if !self.request_config.assert_identity { self.try_into_http_request(request, session, config).await? } else { - self.try_into_http_request_with_identy_assertion(request, session, config).await? + self.try_into_http_request_with_identity_assertion(request, session, config).await? }; self.inner.send_request(request, config).await @@ -180,7 +180,7 @@ impl HttpClient { } #[cfg(feature = "appservice")] - async fn try_into_http_request_with_identy_assertion( + async fn try_into_http_request_with_identity_assertion( &self, request: Request, session: Arc>>, From 20454e1666cbed242a71eac6b236e01363b74ad7 Mon Sep 17 00:00:00 2001 From: Johannes Becker Date: Mon, 17 May 2021 12:02:17 +0200 Subject: [PATCH 4/7] appservice: Put registration into Arc --- matrix_sdk_appservice/src/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/matrix_sdk_appservice/src/lib.rs b/matrix_sdk_appservice/src/lib.rs index 75e52134..aadbb577 100644 --- a/matrix_sdk_appservice/src/lib.rs +++ b/matrix_sdk_appservice/src/lib.rs @@ -82,6 +82,7 @@ use std::{ fs::File, ops::Deref, path::PathBuf, + sync::Arc, }; use http::Uri; @@ -195,7 +196,7 @@ async fn client_session_with_login_restore( pub struct Appservice { homeserver_url: Url, server_name: ServerNameBox, - registration: AppserviceRegistration, + registration: Arc, client_sender_localpart: Client, } @@ -229,6 +230,8 @@ impl Appservice { ) .await?; + let registration = Arc::new(registration); + Ok(Appservice { homeserver_url, server_name, registration, client_sender_localpart }) } From 7609c7445c2640bad16e2d77364599c19ab8584d Mon Sep 17 00:00:00 2001 From: Johannes Becker Date: Tue, 25 May 2021 10:38:43 +0200 Subject: [PATCH 5/7] matrix-sdk: Allow to get Client's RequestConfig --- matrix_sdk/src/client.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/matrix_sdk/src/client.rs b/matrix_sdk/src/client.rs index 3f2c8e81..60825070 100644 --- a/matrix_sdk/src/client.rs +++ b/matrix_sdk/src/client.rs @@ -297,6 +297,11 @@ impl ClientConfig { self } + /// Get the [`RequestConfig`] + pub fn get_request_config(&self) -> &RequestConfig { + &self.request_config + } + /// Specify a client to handle sending requests and receiving responses. /// /// Any type that implements the `HttpSend` trait can be used to From 2becb88c359415055e059385140cd52e653c0a9e Mon Sep 17 00:00:00 2001 From: Johannes Becker Date: Wed, 26 May 2021 14:00:59 +0200 Subject: [PATCH 6/7] appservice: Add client_with_config singleton --- matrix_sdk_appservice/Cargo.toml | 1 + .../examples/actix_autojoin.rs | 7 +- matrix_sdk_appservice/src/actix.rs | 2 +- matrix_sdk_appservice/src/error.rs | 6 +- matrix_sdk_appservice/src/lib.rs | 113 +++++++++++------- matrix_sdk_appservice/tests/actix.rs | 2 +- matrix_sdk_appservice/tests/tests.rs | 4 +- 7 files changed, 85 insertions(+), 50 deletions(-) diff --git a/matrix_sdk_appservice/Cargo.toml b/matrix_sdk_appservice/Cargo.toml index 766fefac..5230ff1e 100644 --- a/matrix_sdk_appservice/Cargo.toml +++ b/matrix_sdk_appservice/Cargo.toml @@ -16,6 +16,7 @@ docs = [] [dependencies] actix-rt = { version = "2", optional = true } actix-web = { version = "4.0.0-beta.6", optional = true } +dashmap = "4" futures = "0.3" futures-util = "0.3" http = "0.2" diff --git a/matrix_sdk_appservice/examples/actix_autojoin.rs b/matrix_sdk_appservice/examples/actix_autojoin.rs index ad385180..2ebdbd6c 100644 --- a/matrix_sdk_appservice/examples/actix_autojoin.rs +++ b/matrix_sdk_appservice/examples/actix_autojoin.rs @@ -34,9 +34,10 @@ impl EventHandler for AppserviceEventHandler { if let MembershipState::Invite = event.content.membership { let user_id = UserId::try_from(event.state_key.clone()).unwrap(); - self.appservice.register(user_id.localpart()).await.unwrap(); + let mut appservice = self.appservice.clone(); + appservice.register(user_id.localpart()).await.unwrap(); - let client = self.appservice.client(Some(user_id.localpart())).await.unwrap(); + let client = appservice.client(Some(user_id.localpart())).await.unwrap(); client.join_room_by_id(room.room_id()).await.unwrap(); } @@ -53,7 +54,7 @@ pub async fn main() -> std::io::Result<()> { let registration = AppserviceRegistration::try_from_yaml_file("./tests/registration.yaml").unwrap(); - let appservice = Appservice::new(homeserver_url, server_name, registration).await.unwrap(); + let mut appservice = Appservice::new(homeserver_url, server_name, registration).await.unwrap(); let event_handler = AppserviceEventHandler::new(appservice.clone()); diff --git a/matrix_sdk_appservice/src/actix.rs b/matrix_sdk_appservice/src/actix.rs index bf5d2800..672f417a 100644 --- a/matrix_sdk_appservice/src/actix.rs +++ b/matrix_sdk_appservice/src/actix.rs @@ -65,7 +65,7 @@ async fn push_transactions( return Ok(HttpResponse::Unauthorized().finish()); } - appservice.client(None).await?.receive_transaction(request.incoming).await?; + appservice.get_cached_client(None)?.receive_transaction(request.incoming).await?; Ok(HttpResponse::Ok().json("{}")) } diff --git a/matrix_sdk_appservice/src/error.rs b/matrix_sdk_appservice/src/error.rs index fcbdf997..c42b3370 100644 --- a/matrix_sdk_appservice/src/error.rs +++ b/matrix_sdk_appservice/src/error.rs @@ -16,9 +16,6 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum Error { - #[error("tried to run without webserver configured")] - RunWithoutServer, - #[error("missing access token")] MissingAccessToken, @@ -31,6 +28,9 @@ pub enum Error { #[error("no port found")] MissingRegistrationPort, + #[error("no client for localpart found")] + NoClientForLocalpart, + #[error(transparent)] HttpRequest(#[from] matrix_sdk::FromHttpRequestError), diff --git a/matrix_sdk_appservice/src/lib.rs b/matrix_sdk_appservice/src/lib.rs index aadbb577..c597fddc 100644 --- a/matrix_sdk_appservice/src/lib.rs +++ b/matrix_sdk_appservice/src/lib.rs @@ -58,7 +58,7 @@ //! regex: '@_appservice_.*' //! ")?; //! -//! let appservice = Appservice::new(homeserver_url, server_name, registration).await?; +//! let mut appservice = Appservice::new(homeserver_url, server_name, registration).await?; //! appservice.set_event_handler(Box::new(AppserviceEventHandler)).await?; //! //! let (host, port) = appservice.registration().get_host_and_port()?; @@ -85,6 +85,7 @@ use std::{ sync::Arc, }; +use dashmap::DashMap; use http::Uri; #[doc(inline)] pub use matrix_sdk::api_appservice as api; @@ -100,8 +101,7 @@ use matrix_sdk::{ assign, identifiers::{self, DeviceId, ServerNameBox, UserId}, reqwest::Url, - Client, ClientConfig, EventHandler, FromHttpResponseError, HttpError, RequestConfig, - ServerError, Session, + Client, ClientConfig, EventHandler, FromHttpResponseError, HttpError, ServerError, Session, }; use regex::Regex; use tracing::warn; @@ -175,21 +175,14 @@ impl Deref for AppserviceRegistration { } } -async fn client_session_with_login_restore( - client: &Client, - registration: &AppserviceRegistration, - localpart: impl AsRef + Into>, - server_name: &ServerNameBox, -) -> Result<()> { - let session = Session { - access_token: registration.as_token.clone(), - user_id: UserId::parse_with_server_name(localpart, server_name)?, - device_id: DeviceId::new(), - }; - client.restore_login(session).await?; +type Localpart = String; - Ok(()) -} +/// The main appservice user is the `sender_localpart` from the given +/// [`AppserviceRegistration`] +/// +/// Dummy type for shared documentation +#[allow(dead_code)] +pub type MainAppserviceUser = (); /// Appservice #[derive(Debug, Clone)] @@ -197,7 +190,7 @@ pub struct Appservice { homeserver_url: Url, server_name: ServerNameBox, registration: Arc, - client_sender_localpart: Client, + clients: Arc>, } impl Appservice { @@ -235,12 +228,14 @@ impl Appservice { Ok(Appservice { homeserver_url, server_name, registration, client_sender_localpart }) } - /// Get a [`Client`] + /// Create a [`Client`] /// - /// Will return a `Client` that's configured to [assert the identity] on all - /// outgoing homeserver requests if `localpart` is given. If not given - /// the `Client` will use the main user associated with this appservice, - /// that is the `sender_localpart` in the [`AppserviceRegistration`] + /// Will create and return a [`Client`] that's configured to [assert the + /// identity] on all outgoing homeserver requests if `localpart` is + /// given. If not given the [`Client`] will use the [`MainAppserviceUser`]. + /// + /// This method is a singleton that saves the client internally for re-use + /// based on the `localpart`. /// /// # Arguments /// @@ -248,26 +243,47 @@ impl Appservice { /// /// [registration]: https://matrix.org/docs/spec/application_service/r0.1.2#registration /// [assert the identity]: https://matrix.org/docs/spec/application_service/r0.1.2#identity-assertion - pub async fn client(&self, localpart: Option<&str>) -> Result { + pub async fn client(&mut self, localpart: Option<&str>) -> Result { + let client = self.client_with_config(localpart, ClientConfig::default()).await?; + + Ok(client) + } + + /// Same as [`Self::client`] but with the ability to pass in a + /// [`ClientConfig`] + /// + /// Since this method is a singleton follow-up calls with different + /// [`ClientConfig`]s will be ignored. + pub async fn client_with_config( + &mut self, + localpart: Option<&str>, + config: ClientConfig, + ) -> Result { let localpart = localpart.unwrap_or_else(|| self.registration.sender_localpart.as_ref()); - // The `as_token` in the `Session` maps to the main appservice user - // (`sender_localpart`) by default, so we don't need to assert identity - // in that case - let client = if localpart == self.registration.sender_localpart { - self.client_sender_localpart.clone() + let client = if let Some(client) = self.clients.get(localpart) { + client.clone() } else { - let request_config = RequestConfig::default().assert_identity(); - let config = ClientConfig::default().request_config(request_config); + let user_id = UserId::parse_with_server_name(localpart, &self.server_name)?; + + // The `as_token` in the `Session` maps to the [`MainAppserviceUser`] + // (`sender_localpart`) by default, so we don't need to assert identity + // in that case + if localpart != self.registration.sender_localpart { + config.get_request_config().assert_identity(); + } + let client = Client::new_with_config(self.homeserver_url.clone(), config)?; - client_session_with_login_restore( - &client, - &self.registration, - localpart, - &self.server_name, - ) - .await?; + let session = Session { + access_token: self.registration.as_token.clone(), + user_id: user_id.clone(), + // TODO: expose & proper E2EE + device_id: DeviceId::new(), + }; + + client.restore_login(session).await?; + self.clients.insert(localpart.to_owned(), client.clone()); client }; @@ -275,9 +291,26 @@ impl Appservice { Ok(client) } + /// Get cached [`Client`] + /// + /// Will return the client for the given `localpart` if previously + /// constructed with [`Self::client()`] or [`Self::client_with_config()`]. + /// If no client for the `localpart` is found it will return an Error. + pub fn get_cached_client(&self, localpart: Option<&str>) -> Result { + let localpart = localpart.unwrap_or_else(|| self.registration.sender_localpart.as_ref()); + + let entry = self.clients.get(localpart).ok_or(Error::NoClientForLocalpart)?; + + Ok(entry.value().clone()) + } + /// Convenience wrapper around [`Client::set_event_handler()`] - pub async fn set_event_handler(&self, handler: Box) -> Result<()> { + /// + /// Attaches the event handler to [`Self::client()`] with `None` as + /// `localpart` + pub async fn set_event_handler(&mut self, handler: Box) -> Result<()> { let client = self.client(None).await?; + client.set_event_handler(handler).await; Ok(()) @@ -290,7 +323,7 @@ impl Appservice { /// /// * `localpart` - The localpart of the user to register. Must be covered /// by the namespaces in the [`Registration`] in order to succeed. - pub async fn register(&self, localpart: impl AsRef) -> Result<()> { + pub async fn register(&mut self, localpart: impl AsRef) -> Result<()> { let request = assign!(RegistrationRequest::new(), { username: Some(localpart.as_ref()), login_type: Some(&LoginType::ApplicationService), diff --git a/matrix_sdk_appservice/tests/actix.rs b/matrix_sdk_appservice/tests/actix.rs index 4bb4b8cd..f7186698 100644 --- a/matrix_sdk_appservice/tests/actix.rs +++ b/matrix_sdk_appservice/tests/actix.rs @@ -12,7 +12,7 @@ mod actix { Appservice::new( mockito::server_url().as_ref(), "test.local", - AppserviceRegistration::try_from_yaml_file("./tests/registration.yaml").unwrap(), + AppserviceRegistration::try_from_yaml_str(include_str!("./registration.yaml")).unwrap(), ) .await .unwrap() diff --git a/matrix_sdk_appservice/tests/tests.rs b/matrix_sdk_appservice/tests/tests.rs index 61fe0ee4..bcd8f8c7 100644 --- a/matrix_sdk_appservice/tests/tests.rs +++ b/matrix_sdk_appservice/tests/tests.rs @@ -59,7 +59,7 @@ fn member_json() -> serde_json::Value { #[async_test] async fn test_event_handler() -> Result<()> { - let appservice = appservice(None).await?; + let mut appservice = appservice(None).await?; struct Example {} @@ -94,7 +94,7 @@ async fn test_event_handler() -> Result<()> { #[async_test] async fn test_transaction() -> Result<()> { - let appservice = appservice(None).await?; + let mut appservice = appservice(None).await?; let event = serde_json::from_value::(member_json()).unwrap(); let event: Raw = AnyRoomEvent::State(event).into(); From 3f2fecd309b071c791a7183843e8aae341f9aa0d Mon Sep 17 00:00:00 2001 From: Johannes Becker Date: Mon, 31 May 2021 12:50:53 +0200 Subject: [PATCH 7/7] appservice: Add new_with_client_config --- matrix_sdk_appservice/src/lib.rs | 46 ++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/matrix_sdk_appservice/src/lib.rs b/matrix_sdk_appservice/src/lib.rs index c597fddc..82c5331f 100644 --- a/matrix_sdk_appservice/src/lib.rs +++ b/matrix_sdk_appservice/src/lib.rs @@ -196,6 +196,10 @@ pub struct Appservice { impl Appservice { /// Create new Appservice /// + /// Also creates and caches a [`Client`] with the [`MainAppserviceUser`]. + /// The default [`ClientConfig`] is used, if you want to customize it + /// use [`Self::new_with_client_config()`] instead. + /// /// # Arguments /// /// * `homeserver_url` - The homeserver that the client should connect to. @@ -210,22 +214,36 @@ impl Appservice { server_name: impl TryInto, registration: AppserviceRegistration, ) -> Result { - let homeserver_url = homeserver_url.try_into()?; - let server_name = server_name.try_into()?; - - let client_sender_localpart = Client::new(homeserver_url.clone())?; - - client_session_with_login_restore( - &client_sender_localpart, - ®istration, - registration.sender_localpart.as_ref(), - &server_name, + let appservice = Self::new_with_client_config( + homeserver_url, + server_name, + registration, + ClientConfig::default(), ) .await?; - let registration = Arc::new(registration); + Ok(appservice) + } - Ok(Appservice { homeserver_url, server_name, registration, client_sender_localpart }) + /// Same as [`Self::new()`] but lets you provide a [`ClientConfig`] for the + /// [`Client`] + pub async fn new_with_client_config( + homeserver_url: impl TryInto, + server_name: impl TryInto, + registration: AppserviceRegistration, + client_config: ClientConfig, + ) -> Result { + let homeserver_url = homeserver_url.try_into()?; + let server_name = server_name.try_into()?; + let registration = Arc::new(registration); + let clients = Arc::new(DashMap::new()); + + let appservice = Appservice { homeserver_url, server_name, registration, clients }; + + // we cache the [`MainAppserviceUser`] by default + appservice.client_with_config(None, client_config).await?; + + Ok(appservice) } /// Create a [`Client`] @@ -243,7 +261,7 @@ impl Appservice { /// /// [registration]: https://matrix.org/docs/spec/application_service/r0.1.2#registration /// [assert the identity]: https://matrix.org/docs/spec/application_service/r0.1.2#identity-assertion - pub async fn client(&mut self, localpart: Option<&str>) -> Result { + pub async fn client(&self, localpart: Option<&str>) -> Result { let client = self.client_with_config(localpart, ClientConfig::default()).await?; Ok(client) @@ -255,7 +273,7 @@ impl Appservice { /// Since this method is a singleton follow-up calls with different /// [`ClientConfig`]s will be ignored. pub async fn client_with_config( - &mut self, + &self, localpart: Option<&str>, config: ClientConfig, ) -> Result {