From 7d674b39aa9b9ff18659d0dc926e90df7dc23c09 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 5 Sep 2023 14:53:11 +0200 Subject: [PATCH] Remove matrix-sdk-appservice There is unfortunately no capacity for maintaining it as a first-party component of the Rust SDK. --- .github/workflows/ci.yml | 44 - Cargo.lock | 67 -- codecov.yaml | 1 - crates/matrix-sdk-appservice/Cargo.toml | 54 - crates/matrix-sdk-appservice/src/error.rs | 114 -- .../src/event_handler.rs | 49 - crates/matrix-sdk-appservice/src/lib.rs | 1028 ----------------- .../matrix-sdk-appservice/src/registration.rs | 124 -- crates/matrix-sdk-appservice/src/user.rs | 154 --- crates/matrix-sdk-appservice/src/webserver.rs | 255 ---- .../tests/registration.yaml | 13 - examples/appservice_autojoin/Cargo.toml | 18 - .../appservice-registration.yaml | 13 - examples/appservice_autojoin/src/main.rs | 80 -- xtask/src/ci.rs | 13 - 15 files changed, 2027 deletions(-) delete mode 100644 crates/matrix-sdk-appservice/Cargo.toml delete mode 100644 crates/matrix-sdk-appservice/src/error.rs delete mode 100644 crates/matrix-sdk-appservice/src/event_handler.rs delete mode 100644 crates/matrix-sdk-appservice/src/lib.rs delete mode 100644 crates/matrix-sdk-appservice/src/registration.rs delete mode 100644 crates/matrix-sdk-appservice/src/user.rs delete mode 100644 crates/matrix-sdk-appservice/src/webserver.rs delete mode 100644 crates/matrix-sdk-appservice/tests/registration.yaml delete mode 100644 examples/appservice_autojoin/Cargo.toml delete mode 100644 examples/appservice_autojoin/appservice-registration.yaml delete mode 100644 examples/appservice_autojoin/src/main.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 426131f35..3546e1080 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -269,50 +269,6 @@ jobs: run: | target/debug/xtask ci wasm-pack ${{ matrix.cmd }} - test-appservice: - name: ${{ matrix.os-name }} [m]-appservice - needs: xtask - if: github.event_name == 'push' || !github.event.pull_request.draft - - runs-on: ${{ matrix.os }} - strategy: - fail-fast: true - matrix: - include: - - os: ubuntu-latest - os-name: ๐Ÿง - xtask-cachekey: "${{ needs.xtask.outputs.cachekey-linux }}" - - - os: macos-latest - os-name: ๐Ÿ - xtask-cachekey: "${{ needs.xtask.outputs.cachekey-macos }}" - - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - - - name: Load cache - uses: Swatinem/rust-cache@v2 - with: - save-if: ${{ github.ref == 'refs/heads/main' }} - - - name: Install nextest - uses: taiki-e/install-action@nextest - - - name: Get xtask - uses: actions/cache/restore@v3 - with: - path: target/debug/xtask - key: "${{ matrix.xtask-cachekey }}" - fail-on-cache-miss: true - - - name: Run checks - run: | - target/debug/xtask ci test-appservice - formatting: name: Check Formatting runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index 11ef90592..6779fdd2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -492,8 +492,6 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", - "serde_json", - "serde_path_to_error", "sync_wrapper", "tower", "tower-layer", @@ -1554,17 +1552,6 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" -[[package]] -name = "example-appservice-autojoin" -version = "0.1.0" -dependencies = [ - "anyhow", - "matrix-sdk-appservice", - "tokio", - "tracing", - "tracing-subscriber", -] - [[package]] name = "example-autojoin" version = "0.1.0" @@ -3102,31 +3089,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "matrix-sdk-appservice" -version = "0.1.0" -dependencies = [ - "axum", - "dashmap", - "http", - "hyper", - "matrix-sdk", - "matrix-sdk-test", - "regex", - "ruma", - "serde", - "serde_html_form", - "serde_json", - "serde_yaml", - "thiserror", - "tokio", - "tower", - "tracing", - "tracing-subscriber", - "url", - "wiremock", -] - [[package]] name = "matrix-sdk-base" version = "0.6.1" @@ -5378,16 +5340,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_path_to_error" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" -dependencies = [ - "itoa", - "serde", -] - [[package]] name = "serde_qs" version = "0.8.5" @@ -5449,19 +5401,6 @@ dependencies = [ "syn 2.0.28", ] -[[package]] -name = "serde_yaml" -version = "0.9.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" -dependencies = [ - "indexmap 2.0.0", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - [[package]] name = "sha1" version = "0.10.5" @@ -6415,12 +6354,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "unsafe-libyaml" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" - [[package]] name = "untrusted" version = "0.7.1" diff --git a/codecov.yaml b/codecov.yaml index 47818635d..4db66214c 100644 --- a/codecov.yaml +++ b/codecov.yaml @@ -8,7 +8,6 @@ coverage: threshold: 1% paths: - "crates/matrix-sdk/" - - "crates/matrix-sdk-appservice/" - "crates/matrix-sdk-base/" - "crates/matrix-sdk-common/" - "crates/matrix-sdk-crypto/" diff --git a/crates/matrix-sdk-appservice/Cargo.toml b/crates/matrix-sdk-appservice/Cargo.toml deleted file mode 100644 index ea5f5f0c6..000000000 --- a/crates/matrix-sdk-appservice/Cargo.toml +++ /dev/null @@ -1,54 +0,0 @@ -[package] -authors = ["Johannes Becker "] -edition = "2021" -homepage = "https://github.com/matrix-org/matrix-rust-sdk" -repository = "https://github.com/matrix-org/matrix-rust-sdk" -description = "Appservice SDK based on the matrix-sdk" -keywords = ["matrix", "chat", "messaging", "ruma", "nio", "appservice"] -license = "Apache-2.0" -name = "matrix-sdk-appservice" -version = "0.1.0" -rust-version = { workspace = true } -publish = false - -[features] -default = ["native-tls"] - -anyhow = ["matrix-sdk/anyhow"] -e2e-encryption = [ - "matrix-sdk/e2e-encryption" -] -eyre = ["matrix-sdk/eyre"] -sqlite = ["matrix-sdk/sqlite"] - -markdown = ["matrix-sdk/markdown"] -native-tls = ["matrix-sdk/native-tls"] -rustls-tls = ["matrix-sdk/rustls-tls"] -socks = ["matrix-sdk/socks"] -sso-login = ["matrix-sdk/sso-login"] - -docs = [] - -[dependencies] -axum = { version = "0.6.1", default-features = false, features = ["json"] } -dashmap = { workspace = true } -http = { workspace = true } -hyper = { version = "0.14.20", features = ["http1", "http2", "server"] } -matrix-sdk = { version = "0.6.0", path = "../matrix-sdk", default-features = false, features = ["appservice"] } -regex = "1.5.5" -ruma = { workspace = true, features = ["appservice-api-s"] } -serde = { workspace = true } -serde_html_form = { workspace = true } -serde_json = { workspace = true } -serde_yaml = "0.9.4" -tokio = { workspace = true, features = ["rt-multi-thread"] } -thiserror = { workspace = true } -tower = { version = "0.4.13", default-features = false } -tracing = { workspace = true } -url = "2.2.2" - -[dev-dependencies] -matrix-sdk-test = { version = "0.6.0", path = "../../testing/matrix-sdk-test", features = ["appservice"] } -tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } -tracing-subscriber = "0.3.11" -wiremock = "0.5.13" diff --git a/crates/matrix-sdk-appservice/src/error.rs b/crates/matrix-sdk-appservice/src/error.rs deleted file mode 100644 index a48f6ec82..000000000 --- a/crates/matrix-sdk-appservice/src/error.rs +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright 2021 Famedly GmbH -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use ruma::api::client::uiaa::UiaaInfo; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum Error { - #[error("missing access token")] - MissingAccessToken, - - #[error("missing host on registration url")] - MissingRegistrationHost, - - #[error("http request builder error")] - UnknownHttpRequestBuilder, - - #[error("no port found")] - MissingRegistrationPort, - - #[error("no client for localpart found")] - NoClientForLocalpart, - - #[error("could not convert host:port to socket addr")] - HostPortToSocketAddrs, - - #[error("uri has empty path")] - UriEmptyPath, - - #[error("uri path is unknown")] - UriPathUnknown, - - #[error("HTTP request parsing error: {0}")] - FromHttpRequest(#[from] ruma::api::error::FromHttpRequestError), - - #[error("identifier failed to parse: {0}")] - Identifier(#[from] ruma::IdParseError), - - #[error("HTTP error: {0}")] - Http(#[from] http::Error), - - #[error("url parse error: {0}")] - Url(#[from] url::ParseError), - - #[error("deserialization error: {0}")] - Serde(#[from] serde::de::value::Error), - - #[error("I/O error: {0}")] - Io(#[from] std::io::Error), - - #[error("http uri invalid error: {0}")] - InvalidUri(#[from] http::uri::InvalidUri), - - #[error(transparent)] - Matrix(#[from] matrix_sdk::Error), - - #[error("regex error: {0}")] - Regex(#[from] regex::Error), - - #[error("serde yaml error: {0}")] - SerdeYaml(#[from] serde_yaml::Error), - - #[error("serde json error: {0}")] - SerdeJson(#[from] serde_json::Error), - - #[error("utf8 error: {0}")] - Utf8(#[from] std::str::Utf8Error), - - #[error("hyper error: {0}")] - Hyper(#[from] hyper::Error), -} - -impl Error { - /// Try to destructure the error into an universal interactive auth info. - /// - /// Some requests require universal interactive auth, doing such a request - /// will always fail the first time with a 401 status code, the response - /// body will contain info how the client can authenticate. - /// - /// The request will need to be retried, this time containing additional - /// authentication data. - /// - /// This method is an convenience method to get to the info the server - /// returned on the first, failed request. - pub fn as_uiaa_response(&self) -> Option<&UiaaInfo> { - match self { - Error::Matrix(matrix) => matrix.as_uiaa_response(), - _ => None, - } - } -} - -impl From for Error { - fn from(e: matrix_sdk::HttpError) -> Self { - matrix_sdk::Error::from(e).into() - } -} - -impl From for Error { - fn from(e: matrix_sdk::StoreError) -> Self { - matrix_sdk::Error::from(e).into() - } -} diff --git a/crates/matrix-sdk-appservice/src/event_handler.rs b/crates/matrix-sdk-appservice/src/event_handler.rs deleted file mode 100644 index 5c7d7de8f..000000000 --- a/crates/matrix-sdk-appservice/src/event_handler.rs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2022 Famedly GmbH -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::{future::Future, pin::Pin, sync::Arc}; - -use tokio::sync::Mutex; - -use crate::{ - ruma::api::appservice::query::{ - query_room_alias::v1 as query_room, query_user_id::v1 as query_user, - }, - AppService, -}; - -pub(crate) type BoxFuture<'a, T> = Pin + Send + 'a>>; -pub(crate) type AppserviceFn = - Box BoxFuture<'static, R> + Send + Sync + 'static>; - -#[derive(Default, Clone)] -pub struct EventHandler { - pub users: Arc>>>, - pub rooms: Arc>>>, -} - -impl std::fmt::Debug for EventHandler { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut debug = f.debug_struct("EventHandler"); - match self.users.try_lock() { - Ok(lock) => debug.field("users", &lock.is_some()), - Err(_) => debug.field("users", &format_args!("")), - }; - match self.rooms.try_lock() { - Ok(lock) => debug.field("rooms", &lock.is_some()), - Err(_) => debug.field("rooms", &format_args!("")), - }; - debug.finish() - } -} diff --git a/crates/matrix-sdk-appservice/src/lib.rs b/crates/matrix-sdk-appservice/src/lib.rs deleted file mode 100644 index c3d2cc5d4..000000000 --- a/crates/matrix-sdk-appservice/src/lib.rs +++ /dev/null @@ -1,1028 +0,0 @@ -// Copyright 2021 Famedly GmbH -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Matrix [Application Service] library -//! -//! The appservice crate aims to provide a batteries-included experience by -//! being a thin wrapper around the [`matrix_sdk`]. That means that we -//! -//! - [x] ship with functionality to configure your webserver crate or simply -//! run the webserver for you -//! - [x] receive and validate requests from the homeserver correctly -//! - [x] allow calling the homeserver with proper user identity assertion -//! - [x] have consistent room state by leveraging matrix-sdk's state store -//! - [ ] provide E2EE support by leveraging matrix-sdk's crypto store -//! -//! # Status -//! -//! The crate is in an experimental state. Follow -//! [matrix-org/matrix-rust-sdk#228] for progress. -//! -//! # Registration -//! -//! The crate relies on the appservice registration being always in sync with -//! the actual registration used by the homeserver. That's because it's required -//! for the access tokens and because membership states for appservice users are -//! determined based on the registered namespaces. -//! -//! # Quickstart -//! -//! ```no_run -//! # async { -//! # -//! use matrix_sdk_appservice::{ -//! ruma::events::room::member::SyncRoomMemberEvent, AppService, -//! AppServiceRegistration, -//! }; -//! -//! let homeserver_url = "http://127.0.0.1:8008"; -//! let server_name = "localhost"; -//! let registration = AppServiceRegistration::try_from_yaml_file( -//! "./tests/registration.yaml", -//! )?; -//! -//! let mut appservice = AppService::builder( -//! homeserver_url.try_into()?, -//! server_name.try_into()?, -//! registration, -//! ) -//! .build() -//! .await?; -//! appservice.user(None).await?.add_event_handler( -//! |_ev: SyncRoomMemberEvent| async { -//! // do stuff -//! }, -//! ); -//! -//! let (host, port) = appservice.registration().get_host_and_port()?; -//! appservice.run(host, port).await?; -//! # -//! # Ok::<(), Box>(()) -//! # }; -//! ``` -//! -//! Check the [examples directory] for fully working examples. -//! -//! [Application Service]: https://matrix.org/docs/spec/application_service/r0.1.2 -//! [matrix-org/matrix-rust-sdk#228]: https://github.com/matrix-org/matrix-rust-sdk/issues/228 -//! [examples directory]: https://github.com/matrix-org/matrix-rust-sdk/tree/main/crates/matrix-sdk-appservice/examples - -use std::{fmt::Debug, sync::Arc}; - -use axum::body::HttpBody; -use dashmap::DashMap; -pub use error::Error; -use event_handler::AppserviceFn; -pub use matrix_sdk; -#[doc(no_inline)] -pub use matrix_sdk::ruma; -use matrix_sdk::{config::RequestConfig, reqwest::Url, Client, ClientBuilder}; -use ruma::{ - api::{ - appservice::{ - event::push_events, - query::{query_room_alias::v1 as query_room, query_user_id::v1 as query_user}, - }, - client::{account::register, sync::sync_events}, - }, - assign, - events::{room::member::MembershipState, AnyStateEvent, AnyTimelineEvent}, - DeviceId, OwnedRoomId, OwnedServerName, -}; -use serde::Deserialize; -use thiserror::Error; -use tokio::task::JoinHandle; -use tracing::{debug, info, warn}; - -mod error; -pub mod event_handler; -pub mod registration; -pub mod user; -mod webserver; - -pub use registration::AppServiceRegistration; -use registration::NamespaceCache; -pub use user::UserBuilder; -pub use webserver::AppServiceRouter; - -pub type Result = std::result::Result; - -const USER_KEY: &[u8] = b"appservice.users."; -const USER_MEMBER: &[u8] = b"appservice.users.membership."; - -type Localpart = String; - -/// AppService -#[derive(Debug, Clone)] -pub struct AppService { - homeserver_url: Url, - server_name: OwnedServerName, - registration: Arc, - namespaces: Arc, - clients: Arc>, - event_handler: event_handler::EventHandler, - default_request_config: Option, -} - -/// Builder for an AppService -#[derive(Debug, Clone)] -pub struct AppServiceBuilder { - homeserver_url: Url, - server_name: OwnedServerName, - registration: AppServiceRegistration, - client_builder: Option, - default_request_config: Option, -} - -impl AppServiceBuilder { - /// Create a new AppService builder. - /// - /// # Arguments - /// - /// * `homeserver_url` - The homeserver that the client should connect to. - /// * `server_name` - The server name to use when constructing user ids from - /// the localpart. - /// * `registration` - The [AppService Registration] to use when interacting - /// with the homeserver. - /// - /// [AppService Registration]: https://matrix.org/docs/spec/application_service/r0.1.2#registration - pub fn new( - homeserver_url: Url, - server_name: OwnedServerName, - registration: AppServiceRegistration, - ) -> Self { - AppServiceBuilder { - homeserver_url, - server_name, - registration, - client_builder: None, - default_request_config: None, - } - } - - /// Set the client builder to use for the appservice user. - pub fn client_builder(mut self, client_builder: ClientBuilder) -> Self { - self.client_builder = Some(client_builder); - self - } - - /// Set the default `[RequestConfig]` to use for appservice users. - pub fn default_request_config(mut self, default_request_config: RequestConfig) -> Self { - self.default_request_config = Some(default_request_config); - self - } - - /// Build the AppService. - /// - /// This will also construct an appservice [`user()`][AppService::user] - /// for the `sender_localpart` of the given registration. This - /// user can be used to register an event handler for all incoming - /// events. Other appservice users only receive events if they're known to - /// be a member of a room. - pub async fn build(self) -> Result { - let homeserver_url = self.homeserver_url; - let server_name = self.server_name; - let registration = Arc::new(self.registration); - let namespaces = Arc::new(NamespaceCache::from_registration(®istration)?); - let clients = Arc::new(DashMap::new()); - let sender_localpart = registration.sender_localpart.clone(); - let event_handler = event_handler::EventHandler::default(); - let default_request_config = self.default_request_config; - - let appservice = AppService { - homeserver_url, - server_name, - registration, - namespaces, - clients, - event_handler, - default_request_config, - }; - if let Some(client_builder) = self.client_builder { - appservice - .user_builder(&sender_localpart) - .client_builder(client_builder) - .build() - .await?; - } else { - appservice.user_builder(&sender_localpart).build().await?; - } - Ok(appservice) - } -} - -impl AppService { - /// Create a new [`AppServiceBuilder`]. - pub fn builder( - homeserver_url: Url, - server_name: OwnedServerName, - registration: AppServiceRegistration, - ) -> AppServiceBuilder { - AppServiceBuilder::new(homeserver_url, server_name, registration) - } - - /// Create an appservice user client. - /// - /// Will create and return a client that's configured to [assert the - /// identity] on outgoing homeserver requests that need authentication. - /// - /// This method is a singleton that saves the client internally for re-use - /// based on the `localpart`. The cached client can be retrieved by calling - /// this method again. - /// - /// Note that if you want to do actions like joining rooms with a - /// user it needs to be registered first. - /// [`register_user()`][Self::register_user] can be used - /// for that purpose. - /// - /// # Arguments - /// - /// * `localpart` - Used for constructing the user accordingly. If `None` is - /// given it uses the `sender_localpart` from the registration. - /// - /// [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 user(&self, localpart: Option<&str>) -> Result { - let localpart = localpart.unwrap_or_else(|| self.registration.sender_localpart.as_ref()); - let builder = match self.default_request_config { - Some(config) => self - .user_builder(localpart) - .client_builder(Client::builder().request_config(config)), - None => self.user_builder(localpart), - }; - builder.build().await - } - - /// Same as [`user()`][Self::user] but with - /// the ability to pass in a [`ClientBuilder`]. - /// - /// Since this method is a singleton follow-up calls with different - /// [`ClientBuilder`]s will be ignored. - pub async fn user_with_client_builder( - &self, - localpart: Option<&str>, - builder: ClientBuilder, - ) -> Result { - let localpart = localpart.unwrap_or_else(|| self.registration.sender_localpart.as_ref()); - self.user_builder(localpart).client_builder(builder).build().await - } - - /// Create a new appservice user builder for the given `localpart`. - pub fn user_builder<'a>(&'a self, localpart: &'a str) -> UserBuilder<'a> { - UserBuilder::new(self, localpart) - } - - /// Get the map containing all constructed appservice user clients. - pub fn users(&self) -> Arc> { - self.clients.clone() - } - - /// Register a responder for queries about the existence of a user with a - /// given mxid. - /// - /// See [GET /_matrix/app/v1/users/{userId}](https://matrix.org/docs/spec/application_service/r0.1.2#get-matrix-app-v1-users-userid). - /// - /// # Examples - /// ```no_run - /// # use matrix_sdk_appservice::AppService; - /// # fn run(appservice: AppService) { - /// appservice.register_user_query(Box::new(|appservice, req| { - /// Box::pin(async move { - /// println!("Got request for {}", req.user_id); - /// true - /// }) - /// })); - /// # } - /// ``` - pub async fn register_user_query(&self, handler: AppserviceFn) { - *self.event_handler.users.lock().await = Some(handler); - } - - /// Register a responder for queries about the existence of a room with the - /// given alias. - /// - /// See [GET /_matrix/app/v1/rooms/{roomAlias}](https://matrix.org/docs/spec/application_service/r0.1.2#get-matrix-app-v1-rooms-roomalias). - /// - /// # Examples - /// ```no_run - /// # use matrix_sdk_appservice::AppService; - /// # fn run(appservice: AppService) { - /// appservice.register_room_query(Box::new(|appservice, req| { - /// Box::pin(async move { - /// println!("Got request for {}", req.room_alias); - /// true - /// }) - /// })); - /// # } - /// ``` - pub async fn register_room_query(&self, handler: AppserviceFn) { - *self.event_handler.rooms.lock().await = Some(handler); - } - - /// Register an appservice user by sending a [`register::v3::Request`] to - /// the homeserver. - /// - /// # Arguments - /// - /// * `localpart` - The localpart of the user to register. Must be covered - /// by the namespaces in the registration in order to succeed. - /// - /// # Returns - /// This function may return a UIAA response, which should be checked for - /// with [`Error::as_uiaa_response()`]. - pub async fn register_user(&self, localpart: &str, device_id: Option<&DeviceId>) -> Result<()> { - if self.is_user_registered(localpart).await? { - return Ok(()); - } - let request = assign!(register::v3::Request::new(), { - username: Some(localpart.to_owned()), - login_type: Some(register::LoginType::ApplicationService), - device_id: device_id.map(ToOwned::to_owned), - }); - - let client = self.user(None).await?; - client.matrix_auth().register(request).await?; - self.set_user_registered(localpart).await?; - - Ok(()) - } - - /// Add the given localpart to the database of registered localparts. - async fn set_user_registered(&self, localpart: impl AsRef) -> Result<()> { - let client = self.user(None).await?; - client - .store() - .set_custom_value( - &[USER_KEY, localpart.as_ref().as_bytes()].concat(), - vec![u8::from(true)], - ) - .await?; - Ok(()) - } - - /// Get whether a localpart is listed in the database as registered. - async fn is_user_registered(&self, localpart: impl AsRef) -> Result { - let client = self.user(None).await?; - let key = [USER_KEY, localpart.as_ref().as_bytes()].concat(); - let store = client.store().get_custom_value(&key).await?; - let registered = store.is_some_and(|vec| vec.first().copied() == Some(u8::from(true))); - Ok(registered) - } - - /// Get the [`AppServiceRegistration`]. - pub fn registration(&self) -> &AppServiceRegistration { - &self.registration - } - - /// Compare the given `hs_token` against the registration's `hs_token`. - /// - /// Returns `true` if the tokens match, `false` otherwise. - pub fn compare_hs_token(&self, hs_token: impl AsRef) -> bool { - self.registration.hs_token == hs_token.as_ref() - } - - /// Check if given `user_id` is in any of the [`AppServiceRegistration`]'s - /// `users` namespaces. - pub fn user_id_is_in_namespace(&self, user_id: impl AsRef) -> bool { - let user_id = user_id.as_ref(); - self.namespaces.users.iter().any(|regex| regex.is_match(user_id)) - } - - /// Returns a [`Service`][tower::Service] that processes appservice - /// requests. - pub fn service(&self) -> AppServiceRouter - where - B: HttpBody + Send + 'static, - B::Data: Send, - B::Error: Into, - { - webserver::router(self.clone()) - } - - /// Receive an incoming [transaction], pushing the contained events to - /// active clients. - /// - /// [transaction]: https://spec.matrix.org/v1.2/application-service-api/#put_matrixappv1transactionstxnid - async fn receive_transaction(&self, transaction: push_events::v1::Request) -> Result<()> { - let sender_localpart_client = self.user(None).await?; - - // Find membership events affecting members in our namespace, and update - // membership accordingly - for raw_event in transaction.events.iter() { - let res = raw_event.deserialize(); - let Ok(AnyTimelineEvent::State(AnyStateEvent::RoomMember(event))) = res else { - continue; - }; - if !self.user_id_is_in_namespace(event.state_key()) { - continue; - } - let localpart = event.state_key().localpart(); - sender_localpart_client - .store() - .set_custom_value( - &[USER_MEMBER, event.room_id().as_bytes(), b".", localpart.as_bytes()].concat(), - event.membership().to_string().into_bytes(), - ) - .await?; - } - - /// Helper type for extracting the room id for an event - #[derive(Debug, Deserialize)] - struct EventRoomId { - room_id: Option, - } - - // Spawn a task for each client that constructs and pushes a sync event - let mut tasks: Vec> = Vec::new(); - let transaction = Arc::new(transaction); - for user_client in self.clients.iter() { - let client = sender_localpart_client.clone(); - let user_client = user_client.clone(); - let transaction = transaction.clone(); - let sender_localpart = self.registration.sender_localpart.clone(); - - let task = tokio::spawn(async move { - let Some(user_id) = user_client.user_id() else { - // The client is not logged in, skipping - return Ok(()); - }; - let user_localpart = user_id.localpart(); - let mut response = sync_events::v3::Response::new(transaction.txn_id.to_string()); - - // Clients expect events to be grouped per room, where the - // group also denotes what the client's membership of the given - // room is. We take all the events in the transaction and sort - // them into appropriate groups. - // - // We special-case the `sender_localpart` user which receives all events and - // by falling back to a membership of "join" if it's unknown. - for raw_event in &transaction.events { - let Some(room_id) = raw_event.deserialize_as::()?.room_id else { - warn!("Transaction contained event with no ID"); - continue; - }; - let key = &[USER_MEMBER, room_id.as_bytes(), b".", user_localpart.as_bytes()] - .concat(); - let membership = match client.store().get_custom_value(key).await? { - Some(value) => String::from_utf8(value).ok().map(MembershipState::from), - // Assume the `sender_localpart` user is in every known room - None if user_localpart == sender_localpart => Some(MembershipState::Join), - None => None, - }; - - match membership { - Some(MembershipState::Join) => { - let room = response.rooms.join.entry(room_id).or_default(); - room.timeline.events.push(raw_event.clone().cast()) - } - Some(MembershipState::Leave | MembershipState::Ban) => { - let room = response.rooms.leave.entry(room_id).or_default(); - room.timeline.events.push(raw_event.clone().cast()) - } - Some(MembershipState::Knock) => { - response.rooms.knock.entry(room_id).or_default(); - } - Some(MembershipState::Invite) => { - response.rooms.invite.entry(room_id).or_default(); - } - Some(unknown) => debug!("Unknown membership type: {unknown}"), - None => debug!("Assuming {user_localpart} is not in {room_id}"), - } - } - user_client.receive_transaction(&transaction.txn_id, response).await?; - Ok::<_, Error>(()) - }); - - tasks.push(task); - } - for task in tasks { - if let Err(e) = task.await { - warn!("Joining sync task failed: {e}"); - } - } - Ok(()) - } - - /// Convenience method that runs an http server. - /// - /// This is a blocking call that tries to listen on the provided host and - /// port. - pub async fn run(&self, host: impl Into, port: impl Into) -> Result<()> { - let host = host.into(); - let port = port.into(); - info!(host, port, "Starting AppService"); - - webserver::run_server(self.clone(), host, port).await?; - Ok(()) - } - - /// Set the default RequestConfig - pub fn set_default_request_config( - &mut self, - request_config: Option, - ) -> Result<()> { - self.default_request_config = request_config; - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use std::sync::{Arc, Mutex}; - - use http::{Method, Request}; - use hyper::Body; - use matrix_sdk::{ - config::RequestConfig, - ruma::{api::appservice::Registration, events::room::member::OriginalSyncRoomMemberEvent}, - Client, RoomMemberships, RoomState, - }; - use matrix_sdk_test::{appservice::TransactionBuilder, async_test, TimelineTestEvent}; - use ruma::{ - api::{appservice::event::push_events, MatrixVersion}, - events::AnyTimelineEvent, - room_id, - serde::Raw, - }; - use serde_json::json; - use tower::{Service, ServiceExt}; - use wiremock::{ - matchers::{body_json, header, method, path}, - Mock, MockServer, ResponseTemplate, - }; - - use crate::{AppService, AppServiceBuilder, AppServiceRegistration, Result}; - - fn registration_string() -> String { - include_str!("../tests/registration.yaml").to_owned() - } - - async fn appservice( - homeserver_url: Option, - registration: Option, - ) -> Result { - let _ = tracing_subscriber::fmt::try_init(); - - let registration = match registration { - Some(registration) => registration.into(), - None => AppServiceRegistration::try_from_yaml_str(registration_string()).unwrap(), - }; - - let homeserver_url = homeserver_url.unwrap_or_else(|| "http://localhost:1234".to_owned()); - let server_name = "localhost"; - - let client_builder = Client::builder() - .request_config(RequestConfig::default().disable_retry()) - .server_versions([MatrixVersion::V1_0]); - - AppServiceBuilder::new(homeserver_url.parse()?, server_name.parse()?, registration) - .client_builder(client_builder) - .build() - .await - } - - #[async_test] - async fn test_register_user() -> Result<()> { - let server = MockServer::start().await; - let appservice = appservice(Some(server.uri()), None).await?; - - let localpart = "someone"; - Mock::given(method("POST")) - .and(path("/_matrix/client/r0/register")) - .and(header( - "authorization", - format!("Bearer {}", appservice.registration().as_token).as_str(), - )) - .and(body_json(json!({ - "username": localpart.to_owned(), - "type": "m.login.application_service" - }))) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "access_token": "abc123", - "device_id": "GHTYAJCE", - "user_id": format!("@{localpart}:localhost"), - }))) - .mount(&server) - .await; - - appservice.register_user(localpart, None).await?; - - Ok(()) - } - - #[async_test] - async fn test_put_transaction() -> Result<()> { - let uri = "/_matrix/app/v1/transactions/1?access_token=hs_token"; - - let mut transaction_builder = TransactionBuilder::new(); - transaction_builder.add_timeline_event(TimelineTestEvent::Member); - let transaction = transaction_builder.build_transaction(); - - let response = appservice(None, None) - .await? - .service() - .oneshot( - Request::builder() - .method(Method::PUT) - .uri(uri) - .body(Body::from(transaction)) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), 200); - - Ok(()) - } - - #[async_test] - async fn test_put_transaction_with_repeating_txn_id() -> Result<()> { - let uri = "/_matrix/app/v1/transactions/1?access_token=hs_token"; - - let mut transaction_builder = TransactionBuilder::new(); - transaction_builder.add_timeline_event(TimelineTestEvent::Member); - let transaction = transaction_builder.build_transaction(); - - let appservice = appservice(None, None).await?; - - #[allow(clippy::mutex_atomic)] - let on_state_member = Arc::new(Mutex::new(false)); - appservice.user(None).await?.add_event_handler({ - let on_state_member = on_state_member.clone(); - move |_ev: OriginalSyncRoomMemberEvent| async move { - *on_state_member.lock().unwrap() = true; - } - }); - - let mut service = appservice.service(); - - let response = service - .call( - Request::builder() - .method(Method::PUT) - .uri(uri) - .body(Body::from(transaction.clone())) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), 200); - { - let on_room_member_called = *on_state_member.lock().unwrap(); - assert!(on_room_member_called); - } - - // Reset this to check that next time it doesn't get called. - { - let mut on_room_member_called = on_state_member.lock().unwrap(); - *on_room_member_called = false; - } - - let response = service - .call( - Request::builder() - .method(Method::PUT) - .uri(uri) - .body(Body::from(transaction)) - .unwrap(), - ) - .await - .unwrap(); - - // According to https://spec.matrix.org/v1.2/application-service-api/#pushing-events - // This should noop and return 200. - assert_eq!(response.status(), 200); - { - let on_room_member_called = *on_state_member.lock().unwrap(); - // This time we should not have called the event handler. - assert!(!on_room_member_called); - } - - Ok(()) - } - - #[async_test] - async fn test_get_user() -> Result<()> { - let appservice = appservice(None, None).await?; - appservice.register_user_query(Box::new(|_, _| Box::pin(async move { true }))).await; - - let uri = "/_matrix/app/v1/users/%40_botty_1:dev.famedly.local?access_token=hs_token"; - - let response = appservice - .service() - .oneshot(Request::builder().uri(uri).body(Body::empty()).unwrap()) - .await - .unwrap(); - - assert_eq!(response.status(), 200); - - Ok(()) - } - - #[async_test] - async fn test_get_room() -> Result<()> { - let appservice = appservice(None, None).await?; - appservice.register_room_query(Box::new(|_, _| Box::pin(async move { true }))).await; - - let uri = "/_matrix/app/v1/rooms/%23magicforest:example.com?access_token=hs_token"; - - let response = appservice - .service() - .oneshot(Request::builder().uri(uri).body(Body::empty()).unwrap()) - .await - .unwrap(); - - assert_eq!(response.status(), 200); - - Ok(()) - } - - #[async_test] - async fn test_invalid_access_token() -> Result<()> { - let uri = "/_matrix/app/v1/transactions/1?access_token=invalid_token"; - - let mut transaction_builder = TransactionBuilder::new(); - let transaction = - transaction_builder.add_timeline_event(TimelineTestEvent::Member).build_transaction(); - - let appservice = appservice(None, None).await?; - - let response = appservice - .service() - .oneshot( - Request::builder() - .method(Method::PUT) - .uri(uri) - .body(Body::from(transaction)) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), 401); - - Ok(()) - } - - #[async_test] - async fn test_no_access_token() -> Result<()> { - let uri = "/_matrix/app/v1/transactions/1"; - - let mut transaction_builder = TransactionBuilder::new(); - transaction_builder.add_timeline_event(TimelineTestEvent::Member); - let transaction = transaction_builder.build_transaction(); - - let appservice = appservice(None, None).await?; - - let response = appservice - .service() - .oneshot( - Request::builder() - .method(Method::PUT) - .uri(uri) - .body(Body::from(transaction)) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), 401); - - Ok(()) - } - - #[async_test] - async fn test_event_handler() -> Result<()> { - let appservice = appservice(None, None).await?; - - #[allow(clippy::mutex_atomic)] - let on_state_member = Arc::new(Mutex::new(false)); - appservice.user(None).await?.add_event_handler({ - let on_state_member = on_state_member.clone(); - move |_ev: OriginalSyncRoomMemberEvent| async move { - *on_state_member.lock().unwrap() = true; - } - }); - - let uri = "/_matrix/app/v1/transactions/1?access_token=hs_token"; - - let mut transaction_builder = TransactionBuilder::new(); - transaction_builder.add_timeline_event(TimelineTestEvent::Member); - let transaction = transaction_builder.build_transaction(); - - appservice - .service() - .oneshot( - Request::builder() - .method(Method::PUT) - .uri(uri) - .body(Body::from(transaction)) - .unwrap(), - ) - .await - .unwrap(); - - let on_room_member_called = *on_state_member.lock().unwrap(); - assert!(on_room_member_called); - - Ok(()) - } - - #[async_test] - async fn test_appservice_on_sub_path() -> Result<()> { - let room_id = room_id!("!SVkFJHzfwvuaIEawgC:localhost"); - let uri_1 = "/sub_path/_matrix/app/v1/transactions/1?access_token=hs_token"; - let uri_2 = "/sub_path/_matrix/app/v1/transactions/2?access_token=hs_token"; - - let mut transaction_builder = TransactionBuilder::new(); - transaction_builder.add_timeline_event(TimelineTestEvent::Member); - let transaction_1 = transaction_builder.build_transaction(); - - let mut transaction_builder = TransactionBuilder::new(); - transaction_builder.add_timeline_event(TimelineTestEvent::MemberNameChange); - let transaction_2 = transaction_builder.build_transaction(); - - let appservice = appservice(None, None).await?; - let mut service = axum::Router::new().nest_service("/sub_path", appservice.service()); - - service - .call( - Request::builder() - .method(Method::PUT) - .uri(uri_1) - .body(Body::from(transaction_1))?, - ) - .await - .unwrap(); - service - .call( - Request::builder() - .method(Method::PUT) - .uri(uri_2) - .body(Body::from(transaction_2))?, - ) - .await - .unwrap(); - - let members = appservice - .user(None) - .await? - .get_room(room_id) - .expect("Expected room to be available") - .members_no_sync(RoomMemberships::empty()) - .await?; - - assert_eq!(members[0].display_name().unwrap(), "changed"); - - Ok(()) - } - - #[async_test] - async fn test_receive_transaction() -> Result<()> { - tracing_subscriber::fmt().try_init().ok(); - let json = vec![ - Raw::new(&json!({ - "content": { - "avatar_url": null, - "displayname": "Appservice", - "membership": "join" - }, - "event_id": "$151800140479rdvjg:localhost", - "membership": "join", - "origin_server_ts": 151800140, - "sender": "@_appservice:localhost", - "state_key": "@_appservice:localhost", - "type": "m.room.member", - "room_id": "!coolplace:localhost", - "unsigned": { - "age": 2970366 - } - }))? - .cast::(), - Raw::new(&json!({ - "content": { - "avatar_url": null, - "displayname": "Appservice", - "membership": "join" - }, - "event_id": "$151800140491rfbja:localhost", - "membership": "join", - "origin_server_ts": 151800140, - "sender": "@_appservice:localhost", - "state_key": "@_appservice:localhost", - "type": "m.room.member", - "room_id": "!boringplace:localhost", - "unsigned": { - "age": 2970366 - } - }))? - .cast::(), - Raw::new(&json!({ - "content": { - "avatar_url": null, - "displayname": "Alice", - "membership": "join" - }, - "event_id": "$151800140517rfvjc:localhost", - "membership": "join", - "origin_server_ts": 151800140, - "sender": "@_appservice_alice:localhost", - "state_key": "@_appservice_alice:localhost", - "type": "m.room.member", - "room_id": "!coolplace:localhost", - "unsigned": { - "age": 2970366 - } - }))? - .cast::(), - Raw::new(&json!({ - "content": { - "avatar_url": null, - "displayname": "Bob", - "membership": "invite" - }, - "event_id": "$151800140594rfvjc:localhost", - "membership": "invite", - "origin_server_ts": 151800174, - "sender": "@_appservice_bob:localhost", - "state_key": "@_appservice_bob:localhost", - "type": "m.room.member", - "room_id": "!boringplace:localhost", - "unsigned": { - "age": 2970366 - } - }))? - .cast::(), - ]; - let appservice = appservice(None, None).await?; - - let alice = appservice.user(Some("_appservice_alice")).await?; - let bob = appservice.user(Some("_appservice_bob")).await?; - appservice - .receive_transaction(push_events::v1::Request::new("dontcare".into(), json)) - .await?; - let coolplace = room_id!("!coolplace:localhost"); - let boringplace = room_id!("!boringplace:localhost"); - assert_eq!( - alice.get_room(coolplace).unwrap().state(), - RoomState::Joined, - "Alice's membership in coolplace should be join" - ); - assert_eq!( - bob.get_room(boringplace).unwrap().state(), - RoomState::Invited, - "Bob's membership in boringplace should be invite" - ); - assert!(alice.get_room(boringplace).is_none(), "Alice should not know about boringplace"); - assert!(bob.get_room(coolplace).is_none(), "Bob should not know about coolplace"); - Ok(()) - } - - mod registration { - use ruma::api::appservice::Registration; - - use crate::{tests::registration_string, AppServiceRegistration, Result}; - - #[test] - fn test_registration() -> Result<()> { - let registration: Registration = serde_yaml::from_str(®istration_string())?; - let registration: AppServiceRegistration = registration.into(); - - assert_eq!(registration.id, "appservice"); - - Ok(()) - } - - #[test] - fn test_registration_from_yaml_file() -> Result<()> { - let registration = - AppServiceRegistration::try_from_yaml_file("./tests/registration.yaml")?; - - assert_eq!(registration.id, "appservice"); - - Ok(()) - } - - #[test] - fn test_registration_from_yaml_str() -> Result<()> { - let registration = AppServiceRegistration::try_from_yaml_str(registration_string())?; - - assert_eq!(registration.id, "appservice"); - - Ok(()) - } - } -} diff --git a/crates/matrix-sdk-appservice/src/registration.rs b/crates/matrix-sdk-appservice/src/registration.rs deleted file mode 100644 index 4da736588..000000000 --- a/crates/matrix-sdk-appservice/src/registration.rs +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright 2022 Famedly GmbH -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! AppService Registration. - -use std::{fs::File, ops::Deref, path::PathBuf}; - -use http::Uri; -use regex::Regex; -use ruma::api::appservice::Registration; - -use crate::{Error, Result}; - -pub type Host = String; -pub type Port = u16; - -/// AppService Registration -/// -/// Wrapper around [`Registration`]. See also . -#[derive(Debug, Clone)] -pub struct AppServiceRegistration { - inner: Registration, -} - -impl AppServiceRegistration { - /// Try to load registration from yaml string - /// - /// See the fields of [`Registration`] for the required format - pub fn try_from_yaml_str(value: impl AsRef) -> Result { - Ok(Self { inner: serde_yaml::from_str(value.as_ref())? }) - } - - /// Try to load registration from yaml file - /// - /// See the fields of [`Registration`] for the required format - pub fn try_from_yaml_file(path: impl Into) -> Result { - let file = File::open(path.into())?; - - Ok(Self { inner: serde_yaml::from_reader(file)? }) - } - - /// Get the host and port from the registration URL - /// - /// If no port is found it falls back to scheme defaults: 80 for http and - /// 443 for https - pub fn get_host_and_port(&self) -> Result<(Host, Port)> { - let uri = Uri::try_from(&self.inner.url)?; - - let host = uri.host().ok_or(Error::MissingRegistrationHost)?.to_owned(); - let port = match uri.port() { - Some(port) => Ok(port.as_u16()), - None => match uri.scheme_str() { - Some("http") => Ok(80), - Some("https") => Ok(443), - _ => Err(Error::MissingRegistrationPort), - }, - }?; - - Ok((host, port)) - } -} - -impl From for AppServiceRegistration { - fn from(value: Registration) -> Self { - Self { inner: value } - } -} - -impl Deref for AppServiceRegistration { - type Target = Registration; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -/// Cache data for the registration namespaces. -#[derive(Debug, Clone)] -pub struct NamespaceCache { - /// List of user regexes in our namespace - pub(crate) users: Vec, - /// List of alias regexes in our namespace - #[allow(dead_code)] - aliases: Vec, - /// List of room id regexes in our namespace - #[allow(dead_code)] - rooms: Vec, -} - -impl NamespaceCache { - /// Creates a new registration cache from a [`Registration`] value - pub fn from_registration(registration: &Registration) -> Result { - let users = registration - .namespaces - .users - .iter() - .map(|user| Regex::new(&user.regex)) - .collect::, _>>()?; - let aliases = registration - .namespaces - .aliases - .iter() - .map(|user| Regex::new(&user.regex)) - .collect::, _>>()?; - let rooms = registration - .namespaces - .rooms - .iter() - .map(|user| Regex::new(&user.regex)) - .collect::, _>>()?; - Ok(NamespaceCache { users, aliases, rooms }) - } -} diff --git a/crates/matrix-sdk-appservice/src/user.rs b/crates/matrix-sdk-appservice/src/user.rs deleted file mode 100644 index ff0137618..000000000 --- a/crates/matrix-sdk-appservice/src/user.rs +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright 2022 Famedly GmbH -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! AppService users. - -use matrix_sdk::{ - config::RequestConfig, - matrix_auth::{Session, SessionTokens}, - Client, ClientBuildError, ClientBuilder, SessionMeta, -}; -use ruma::{ - api::client::{session::login, uiaa::UserIdentifier}, - assign, DeviceId, OwnedDeviceId, UserId, -}; -use tracing::warn; - -use crate::{AppService, Result}; - -/// Builder for an appservice user -#[derive(Debug)] -pub struct UserBuilder<'a> { - appservice: &'a AppService, - localpart: &'a str, - device_id: Option, - client_builder: ClientBuilder, - log_in: bool, - restored_session: Option, -} - -impl<'a> UserBuilder<'a> { - /// Create a new appservice user builder - /// # Arguments - /// - /// * `localpart` - The localpart of the appservice user - pub fn new(appservice: &'a AppService, localpart: &'a str) -> Self { - Self { - appservice, - localpart, - device_id: None, - client_builder: Client::builder(), - log_in: false, - restored_session: None, - } - } - - /// Set the device ID of the appservice user - pub fn device_id(mut self, device_id: Option) -> Self { - self.device_id = device_id; - self - } - - /// Sets the client builder to use for the appservice user - pub fn client_builder(mut self, client_builder: ClientBuilder) -> Self { - self.client_builder = client_builder; - self - } - - /// Log in as the appservice user - /// - /// In some cases it is necessary to log in as the user, such as to - /// upload device keys - pub fn login(mut self) -> Self { - self.log_in = true; - self - } - - /// Restore a persisted session - /// - /// This is primarily useful if you enable - /// [`UserBuilder::login()`] and want to restore a session - /// from a previous run. - pub fn restored_session(mut self, session: Session) -> Self { - self.restored_session = Some(session); - self - } - - /// Build the appservice user - /// - /// # Errors - /// This function returns an error if an invalid localpart is provided. - pub async fn build(self) -> Result { - if let Some(client) = self.appservice.clients.get(self.localpart) { - return Ok(client.clone()); - } - - let user_id = UserId::parse_with_server_name(self.localpart, &self.appservice.server_name)?; - if !(self.appservice.user_id_is_in_namespace(&user_id) - || self.localpart == self.appservice.registration.sender_localpart) - { - warn!("Client id '{user_id}' is not in the namespace") - } - - let mut builder = self.client_builder; - - if !self.log_in && self.localpart != self.appservice.registration.sender_localpart { - builder = builder.assert_identity(); - } - - let client = builder - .homeserver_url(self.appservice.homeserver_url.clone()) - .appservice_mode() - .build() - .await - .map_err(ClientBuildError::assert_valid_builder_args)?; - - let session = if let Some(session) = self.restored_session { - session - } else if self.log_in && self.localpart != self.appservice.registration.sender_localpart { - let login_info = - login::v3::LoginInfo::ApplicationService(login::v3::ApplicationService::new( - UserIdentifier::UserIdOrLocalpart(self.localpart.to_owned()), - )); - - let request = assign!(login::v3::Request::new(login_info), { - device_id: self.device_id, - initial_device_display_name: None, - }); - - let response = - client.send(request, Some(RequestConfig::short_retry().force_auth())).await?; - - Session::from(&response) - } else { - // Donโ€™t log in - Session { - meta: SessionMeta { - user_id: user_id.clone(), - device_id: self.device_id.unwrap_or_else(DeviceId::new), - }, - tokens: SessionTokens { - access_token: self.appservice.registration.as_token.clone(), - refresh_token: None, - }, - } - }; - - client.restore_session(session).await?; - - self.appservice.clients.insert(self.localpart.to_owned(), client.clone()); - - Ok(client) - } -} diff --git a/crates/matrix-sdk-appservice/src/webserver.rs b/crates/matrix-sdk-appservice/src/webserver.rs deleted file mode 100644 index 363ced024..000000000 --- a/crates/matrix-sdk-appservice/src/webserver.rs +++ /dev/null @@ -1,255 +0,0 @@ -// Copyright 2021 Famedly GmbH -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::{ - convert::Infallible, - future::Future, - net::ToSocketAddrs, - pin::Pin, - task::{self, Poll}, -}; - -use axum::{ - async_trait, - body::{Bytes, HttpBody}, - extract::{FromRequest, FromRequestParts, Path}, - middleware::{self, Next}, - response::{ErrorResponse, IntoResponse, Response}, - routing::{future::RouteFuture, get, put}, - BoxError, Extension, Json, Router, ServiceExt, -}; -use http::StatusCode; -use hyper::Body; -use matrix_sdk::ruma::api::IncomingRequest; -use serde::{Deserialize, Serialize}; -use tower::{Service, ServiceBuilder}; - -use crate::{AppService, Error, Result}; - -pub async fn run_server( - appservice: AppService, - host: impl Into, - port: impl Into, -) -> Result<()> { - let router: AppServiceRouter = router(appservice); - - let mut addr = (host.into(), port.into()).to_socket_addrs()?; - if let Some(addr) = addr.next() { - hyper::Server::bind(&addr).serve(router.into_make_service()).await?; - Ok(()) - } else { - Err(Error::HostPortToSocketAddrs) - } -} - -pub fn router(appservice: AppService) -> AppServiceRouter -where - B: HttpBody + Send + 'static, - B::Data: Send, - B::Error: Into, -{ - AppServiceRouter( - Router::new() - .route("/_matrix/app/v1/users/:user_id", get(handlers::user)) - .route("/_matrix/app/v1/rooms/:room_id", get(handlers::room)) - .route("/_matrix/app/v1/transactions/:txn_id", put(handlers::transaction)) - .route("/users/:user_id", get(handlers::user)) - .route("/rooms/:room_id", get(handlers::room)) - .route("/transactions/:txn_id", put(handlers::transaction)) - // FIXME: Use Route::with_state instead of an Extension layer in axum 0.6 - .layer( - ServiceBuilder::new() - .layer(Extension(appservice)) - .layer(middleware::from_fn(validate_access_token)), - ), - ) -} - -#[derive(Debug)] -pub struct AppServiceRouter(Router<(), B>); - -impl Clone for AppServiceRouter { - fn clone(&self) -> Self { - Self(self.0.clone()) - } -} - -impl Service> for AppServiceRouter -where - B: HttpBody + Send + 'static, - B::Data: Send, - B::Error: Into, -{ - // axum's Response type is part of the signature because axum::Router::nest - // requires the inner service to have that exact response (body) type in - // 0.5.x; this will be fixed in axum 0.6.0. - type Response = Response; - type Error = Infallible; - type Future = AppServiceRouteFuture; - - fn poll_ready(&mut self, cx: &mut task::Context<'_>) -> Poll> { - self.0.poll_ready(cx) - } - - fn call(&mut self, mut req: http::Request) -> Self::Future { - // When the AppServiceRouter is nested inside another axum Router under - // a path that includes path parameters, those should not be received by - // the Path extractor used inside the `handlers` module. - req.extensions_mut().clear(); - - AppServiceRouteFuture(self.0.call(req)) - } -} - -pub struct AppServiceRouteFuture(RouteFuture); - -impl Future for AppServiceRouteFuture -where - B: HttpBody, -{ - type Output = Result; - - fn poll(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll { - Pin::new(&mut self.0).poll(cx) - } -} - -pub struct MatrixRequest(T); - -#[async_trait] -impl FromRequest for MatrixRequest -where - S: Send + Sync, - B: HttpBody + Send + 'static, - B::Data: Send, - B::Error: Into, - T: IncomingRequest, -{ - type Rejection = Response; - - async fn from_request( - req: http::request::Request, - state: &S, - ) -> Result { - let (mut parts, body) = req.into_parts(); - let path_params = Path::>::from_request_parts(&mut parts, state) - .await - .map_err(IntoResponse::into_response)?; - let bytes = Bytes::from_request(http::Request::new(body), state) - .await - .map_err(IntoResponse::into_response)?; - let http_request = http::Request::from_parts(parts, bytes); - - let request = T::try_from_http_request(http_request, &path_params).map_err(|_e| { - // TODO: JSON error response - StatusCode::BAD_REQUEST.into_response() - })?; - - Ok(Self(request)) - } -} - -mod handlers { - use axum::{response::IntoResponse, Extension, Json}; - use http::StatusCode; - use ruma::api::appservice::{ - event::push_events, - query::{query_room_alias, query_user_id}, - }; - use serde::Serialize; - - use super::{ErrorMessage, MatrixRequest}; - use crate::AppService; - - #[derive(Serialize)] - struct EmptyObject {} - - pub async fn user( - Extension(appservice): Extension, - MatrixRequest(request): MatrixRequest, - ) -> impl IntoResponse { - if let Some(user_exists) = appservice.event_handler.users.lock().await.as_mut() { - if user_exists(appservice.clone(), request).await { - Ok(Json(EmptyObject {})) - } else { - Err(StatusCode::NOT_FOUND) - } - } else { - Ok(Json(EmptyObject {})) - } - } - - pub async fn room( - Extension(appservice): Extension, - MatrixRequest(request): MatrixRequest, - ) -> impl IntoResponse { - if let Some(room_exists) = appservice.event_handler.rooms.lock().await.as_mut() { - if room_exists(appservice.clone(), request).await { - Ok(Json(&EmptyObject {})) - } else { - Err(StatusCode::NOT_FOUND) - } - } else { - Ok(Json(&EmptyObject {})) - } - } - - pub async fn transaction( - appservice: Extension, - MatrixRequest(request): MatrixRequest, - ) -> impl IntoResponse { - match appservice.receive_transaction(request).await { - Ok(_) => Ok(Json(&EmptyObject {})), - Err(e) => { - let status_code = StatusCode::INTERNAL_SERVER_ERROR; - Err(( - status_code, - Json(ErrorMessage { code: status_code.as_u16(), message: e.to_string() }), - )) - } - } - } -} - -#[derive(Deserialize)] -struct QueryParameters { - access_token: String, -} - -#[derive(Serialize)] -struct ErrorMessage { - code: u16, - message: String, -} - -async fn validate_access_token( - req: http::Request, - next: Next, -) -> Result { - let appservice = - req.extensions().get::().ok_or(StatusCode::INTERNAL_SERVER_ERROR)?; - - let query_string = req.uri().query().unwrap_or(""); - match serde_html_form::from_str::(query_string) { - Ok(query) if query.access_token == appservice.registration.hs_token => { - Ok(next.run(req).await) - } - _ => { - let status_code = StatusCode::UNAUTHORIZED; - let message = - ErrorMessage { code: status_code.as_u16(), message: "UNAUTHORIZED".into() }; - Err((status_code, Json(message)).into()) - } - } -} diff --git a/crates/matrix-sdk-appservice/tests/registration.yaml b/crates/matrix-sdk-appservice/tests/registration.yaml deleted file mode 100644 index eb51a8140..000000000 --- a/crates/matrix-sdk-appservice/tests/registration.yaml +++ /dev/null @@ -1,13 +0,0 @@ -id: appservice -url: http://localhost:9009 -as_token: as_token -hs_token: hs_token -sender_localpart: _appservice -namespaces: - aliases: [] - rooms: [] - users: - - exclusive: true - regex: '@_appservice_.*' -rate_limited: false -protocols: [] diff --git a/examples/appservice_autojoin/Cargo.toml b/examples/appservice_autojoin/Cargo.toml deleted file mode 100644 index 6dc25a089..000000000 --- a/examples/appservice_autojoin/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "example-appservice-autojoin" -version = "0.1.0" -edition = "2021" -publish = false - -[[bin]] -name = "example-appservice-autojoin" -test = false - -[dependencies] -anyhow = "1" -tokio = { version = "1.24.2", features = ["macros", "rt-multi-thread"] } -tracing-subscriber = "0.3.15" -tracing = { workspace = true } -# when copy-pasting this, please use a git dependency or make sure that you -# have copied the example as it was at the time of the release you use. -matrix-sdk-appservice = { path = "../../crates/matrix-sdk-appservice" } diff --git a/examples/appservice_autojoin/appservice-registration.yaml b/examples/appservice_autojoin/appservice-registration.yaml deleted file mode 100644 index eb51a8140..000000000 --- a/examples/appservice_autojoin/appservice-registration.yaml +++ /dev/null @@ -1,13 +0,0 @@ -id: appservice -url: http://localhost:9009 -as_token: as_token -hs_token: hs_token -sender_localpart: _appservice -namespaces: - aliases: [] - rooms: [] - users: - - exclusive: true - regex: '@_appservice_.*' -rate_limited: false -protocols: [] diff --git a/examples/appservice_autojoin/src/main.rs b/examples/appservice_autojoin/src/main.rs deleted file mode 100644 index f5c18ec49..000000000 --- a/examples/appservice_autojoin/src/main.rs +++ /dev/null @@ -1,80 +0,0 @@ -use std::env; - -use matrix_sdk_appservice::{ - matrix_sdk::{ - event_handler::Ctx, - ruma::{ - events::room::member::{MembershipState, OriginalSyncRoomMemberEvent}, - UserId, - }, - Room, - }, - ruma::api::client::error::ErrorKind, - AppService, AppServiceBuilder, AppServiceRegistration, Result, -}; -use tracing::trace; - -pub async fn handle_room_member( - appservice: AppService, - room: Room, - event: OriginalSyncRoomMemberEvent, -) -> Result<()> { - if !appservice.user_id_is_in_namespace(&event.state_key) { - trace!("not an appservice user: {}", event.state_key); - } else if let MembershipState::Invite = event.content.membership { - let user_id = UserId::parse(event.state_key.as_str())?; - if let Err(error) = appservice.register_user(user_id.localpart(), None).await { - error_if_user_not_in_use(error)?; - } - - let client = appservice.user(Some(user_id.localpart())).await?; - client.join_room_by_id(room.room_id()).await?; - } - - Ok(()) -} - -pub fn error_if_user_not_in_use(error: matrix_sdk_appservice::Error) -> Result<()> { - // FIXME: Use if-let chain once available - match &error { - // If user is already in use that's OK. - matrix_sdk_appservice::Error::Matrix(err) - if err.client_api_error_kind() == Some(&ErrorKind::UserInUse) => - { - Ok(()) - } - // In all other cases return with an error. - _ => Err(error), - } -} - -#[tokio::main] -pub async fn main() -> anyhow::Result<()> { - env::set_var("RUST_LOG", "matrix_sdk=debug,matrix_sdk_appservice=debug"); - tracing_subscriber::fmt::init(); - - let homeserver_url = "http://localhost:8008"; - let server_name = "localhost"; - let registration = - AppServiceRegistration::try_from_yaml_file("./appservice-registration.yaml")?; - let appservice = - AppServiceBuilder::new(homeserver_url.parse()?, server_name.parse()?, registration) - .build() - .await?; - - appservice.register_user_query(Box::new(|_, _| Box::pin(async { true }))).await; - - let user = appservice.user(None).await?; - - user.add_event_handler_context(appservice.clone()); - user.add_event_handler( - move |event: OriginalSyncRoomMemberEvent, room: Room, Ctx(appservice): Ctx| { - handle_room_member(appservice, room, event) - }, - ); - - let (host, port) = appservice.registration().get_host_and_port()?; - appservice.run(host, port).await?; - - Ok(()) -} diff --git a/xtask/src/ci.rs b/xtask/src/ci.rs index f1f69cced..7cfa32ed1 100644 --- a/xtask/src/ci.rs +++ b/xtask/src/ci.rs @@ -35,9 +35,6 @@ enum CiCommand { cmd: Option, }, - /// Run tests for the appservice crate - TestAppservice, - /// Run checks for the wasm target Wasm { #[clap(subcommand)] @@ -97,7 +94,6 @@ impl CiArgs { CiCommand::Clippy => check_clippy(), CiCommand::Docs => check_docs(), CiCommand::TestFeatures { cmd } => run_feature_tests(cmd), - CiCommand::TestAppservice => run_appservice_tests(), CiCommand::Wasm { cmd } => run_wasm_checks(cmd), CiCommand::WasmPack { cmd } => run_wasm_pack_tests(cmd), CiCommand::TestCrypto => run_crypto_tests(), @@ -110,7 +106,6 @@ impl CiArgs { check_typos()?; check_docs()?; run_feature_tests(None)?; - run_appservice_tests()?; run_wasm_checks(None)?; run_crypto_tests()?; check_examples()?; @@ -265,14 +260,6 @@ fn run_crypto_tests() -> Result<()> { Ok(()) } -fn run_appservice_tests() -> Result<()> { - cmd!("rustup run stable cargo clippy -p matrix-sdk-appservice -- -D warnings").run()?; - cmd!("rustup run stable cargo nextest run -p matrix-sdk-appservice").run()?; - cmd!("rustup run stable cargo test --doc -p matrix-sdk-appservice").run()?; - - Ok(()) -} - fn run_wasm_checks(cmd: Option) -> Result<()> { if let Some(WasmFeatureSet::Indexeddb) = cmd { run_wasm_checks(Some(WasmFeatureSet::IndexeddbNoCrypto))?;