From e9d79e7ef65fc76a434415bd2805bd3bf4a979ec Mon Sep 17 00:00:00 2001 From: mikoto Date: Sat, 24 Feb 2024 08:01:44 +0000 Subject: [PATCH 01/33] WIP --- Cargo.toml | 12 +- crates/matrix/Cargo.toml | 2 +- .../src/admin/membership/joined_room.rs | 0 crates/matrix/src/admin/mod.rs | 3 +- crates/matrix/src/admin/resources/mod.rs | 3 - crates/matrix/src/admin/resources/user.rs | 269 ----------------- .../matrix/src/admin/{resources => }/room.rs | 0 crates/matrix/src/admin/session.rs | 0 .../admin/threepid/get_user_for_threepid.rs | 0 crates/matrix/src/admin/user.rs | 271 ++++++++++++++++++ crates/matrix/src/admin/user/get_user.rs | 11 + crates/matrix/src/admin/user/get_users.rs | 0 crates/matrix/src/admin/user/set_user.rs | 0 .../src/client/{resources => }/error.rs | 0 .../src/client/{resources => }/events.rs | 0 .../src/client/{resources => }/login.rs | 0 crates/matrix/src/client/mod.rs | 7 +- .../matrix/src/client/{resources => }/mxc.rs | 0 crates/matrix/src/client/resources/mod.rs | 6 - .../matrix/src/client/{resources => }/room.rs | 0 .../src/client/{resources => }/session.rs | 0 crates/matrix/src/error.rs | 7 - crates/matrix/src/http.rs | 177 ------------ crates/matrix/src/lib.rs | 28 +- .../src/{admin/resources => }/token/mod.rs | 0 .../resources => }/token/shared_secret.rs | 0 26 files changed, 308 insertions(+), 488 deletions(-) create mode 100644 crates/matrix/src/admin/membership/joined_room.rs delete mode 100644 crates/matrix/src/admin/resources/mod.rs delete mode 100644 crates/matrix/src/admin/resources/user.rs rename crates/matrix/src/admin/{resources => }/room.rs (100%) create mode 100644 crates/matrix/src/admin/session.rs create mode 100644 crates/matrix/src/admin/threepid/get_user_for_threepid.rs create mode 100644 crates/matrix/src/admin/user.rs create mode 100644 crates/matrix/src/admin/user/get_user.rs create mode 100644 crates/matrix/src/admin/user/get_users.rs create mode 100644 crates/matrix/src/admin/user/set_user.rs rename crates/matrix/src/client/{resources => }/error.rs (100%) rename crates/matrix/src/client/{resources => }/events.rs (100%) rename crates/matrix/src/client/{resources => }/login.rs (100%) rename crates/matrix/src/client/{resources => }/mxc.rs (100%) delete mode 100644 crates/matrix/src/client/resources/mod.rs rename crates/matrix/src/client/{resources => }/room.rs (100%) rename crates/matrix/src/client/{resources => }/session.rs (100%) delete mode 100644 crates/matrix/src/error.rs delete mode 100644 crates/matrix/src/http.rs rename crates/matrix/src/{admin/resources => }/token/mod.rs (100%) rename crates/matrix/src/{admin/resources => }/token/shared_secret.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 730eef0..a21a106 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,13 +14,16 @@ members = ["crates/core", "crates/matrix", "crates/server", "crates/test"] resolver = "2" [workspace.dependencies] -anyhow = "1.0.75" async-trait = "0.1.74" +hex = "0.4.3" +hmac = "0.12.1" +serde_path_to_error = "0.1.14" +serde_qs = "0.12.0" +sha1 = "0.10.6" +anyhow = "1.0.75" axum = { version = "0.7.4", features = ["tokio"] } chrono = { version = "0.4.34", features = ["serde"] } dotenv = "0.15.0" -hex = "0.4.3" -hmac = "0.12.1" http = "0.2.11" mime = "0.3.17" # openssl = { version = "0.10.63", features = ["vendored"] } @@ -33,9 +36,6 @@ reqwest = { version = "0.11.22", default-features = false, features = [ ] } serde = "1.0.192" serde_json = "1.0.108" -serde_path_to_error = "0.1.14" -serde_qs = "0.12.0" -sha1 = "0.10.6" tokio = "1.34.0" tracing = "0.1.40" tracing-subscriber = "0.3.18" diff --git a/crates/matrix/Cargo.toml b/crates/matrix/Cargo.toml index b7b2ab5..c6ef21a 100644 --- a/crates/matrix/Cargo.toml +++ b/crates/matrix/Cargo.toml @@ -10,7 +10,7 @@ path = "src/lib.rs" [dependencies] ruma-events = { version = "0.27.11", features = ["html", "markdown"] } -ruma-common = { version = "0.12.1", features = ["rand"] } +ruma-common = { version = "0.12.0", features = ["api", "rand"], default_features = false } ruma-macros = "0.12.0" # Workspace Dependencies diff --git a/crates/matrix/src/admin/membership/joined_room.rs b/crates/matrix/src/admin/membership/joined_room.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/matrix/src/admin/mod.rs b/crates/matrix/src/admin/mod.rs index 6d0fe39..5fb5809 100644 --- a/crates/matrix/src/admin/mod.rs +++ b/crates/matrix/src/admin/mod.rs @@ -1 +1,2 @@ -pub mod resources; +// pub mod room; +mod user; diff --git a/crates/matrix/src/admin/resources/mod.rs b/crates/matrix/src/admin/resources/mod.rs deleted file mode 100644 index c29990d..0000000 --- a/crates/matrix/src/admin/resources/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod room; -pub mod token; -pub mod user; diff --git a/crates/matrix/src/admin/resources/user.rs b/crates/matrix/src/admin/resources/user.rs deleted file mode 100644 index e5fc0da..0000000 --- a/crates/matrix/src/admin/resources/user.rs +++ /dev/null @@ -1,269 +0,0 @@ -//! [User Admin API](https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#user-admin-api) -//! -//! To use it, you will need to authenticate by providing an `access_token` -//! for a server admin: see Admin API. - -use anyhow::Result; -use ruma_common::UserId; -use serde::{Deserialize, Serialize}; -use tracing::instrument; -use url::Url; - -use crate::{error::MatrixError, http::Client}; - -#[derive(Default)] -pub struct UserService; - -#[derive(Debug, Serialize, Deserialize)] -pub struct ExternalId { - pub auth_provider: String, - pub external_id: String, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ThreePid { - pub medium: String, - pub address: String, - pub added_at: u64, - pub validated_at: u64, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct User { - /// User name postfixed with Matrix instance Host - /// E.g. `@user:example.com` - pub name: String, - pub displayname: Option, - pub threepids: Vec, - pub avatar_url: Option, - pub is_guest: bool, - pub admin: bool, - pub deactivated: bool, - pub erased: bool, - pub shadow_banned: bool, - pub creation_ts: u64, - pub appservice_id: Option, - pub consent_server_notice_sent: Option, - pub consent_version: Option, - pub consent_ts: Option, - pub external_ids: Vec, - pub user_type: Option, - pub locked: bool, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct CreateUserBody { - pub password: String, - pub logout_devices: bool, - pub displayname: Option, - pub avatar_url: Option, - pub threepids: Vec, - pub external_ids: Vec, - pub admin: bool, - pub deactivated: bool, - pub user_type: Option, - pub locked: bool, -} - -#[derive(Debug, Default, Serialize, Deserialize)] -pub struct ListUsersQuery { - pub user_id: Option, - pub name: Option, - pub guests: Option, - pub admins: Option, - pub deactivated: Option, - pub limit: Option, - pub from: Option, -} - -/// Data type for the response of the `GET /_synapse/admin/v2/users` endpoint. -#[derive(Debug, Default, Serialize, Deserialize)] -pub struct ListUser { - pub name: String, - pub user_type: Option, - pub is_guest: usize, - pub admin: usize, - pub deactivated: usize, - pub shadow_banned: bool, - pub avatar_url: Option, - pub creation_ts: u64, - pub last_seen_ts: Option, - pub erased: bool, - pub locked: bool, -} - -#[derive(Debug, Default, Serialize, Deserialize)] -pub struct ListUsersResponse { - pub users: Vec, - pub total: u64, - #[serde(default)] - pub next_token: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct UpdateUserBody { - pub password: String, - pub logout_devices: bool, - pub displayname: Option, - pub avatar_url: Option, - pub threepids: Vec, - pub external_ids: Vec, - pub admin: bool, - pub deactivated: bool, - pub user_type: Option, - pub locked: bool, -} - -#[derive(Debug, Default, Serialize, Deserialize)] -pub struct LoginAsUserBody { - pub valid_until_ms: Option, -} - -#[derive(Debug, Default, Serialize, Deserialize)] -pub struct LoginAsUserResponse { - pub access_token: String, -} - -pub struct QueryUserDataResponse { - pub name: String, - pub displayname: Option, - pub threepids: Vec, - pub avatar_url: Option, - pub is_guest: bool, - pub admin: bool, - pub deactivated: bool, - pub erased: bool, - pub shadow_banned: bool, - pub creation_ts: i64, - pub appservice_id: Option, - pub consent_server_notice_sent: Option, - pub consent_version: Option, - pub consent_ts: Option, - pub external_ids: Vec>, - pub user_type: Option, -} - -impl UserService { - /// This API returns information about a specific user account. - /// - /// Refer: https://matrix-org.github.io/synapse/v1.88/admin_api/user_admin_api.html#query-user-account - #[instrument(skip(client))] - pub async fn query_user_account(client: &Client, user_id: &UserId) -> Result { - let resp = client - .get(format!( - "/_synapse/admin/v2/users/{user_id}", - user_id = user_id - )) - .await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } - - /// Allows an administrator to create a user account. - /// - /// Note that internally Synapse uses this same endpoint to modify an - /// existing user account, so this method will modify the existing user - /// if [`UserId`] matches. - /// - /// Refer: https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#create-or-modify-account - #[instrument(skip(client, body))] - pub async fn create(client: &Client, user_id: &UserId, body: CreateUserBody) -> Result { - let resp = client - .put_json( - format!("/_synapse/admin/v2/users/{user_id}", user_id = user_id), - &body, - ) - .await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } - - /// Returns all local user accounts. By default, the response is ordered by - /// ascending user ID. - /// - /// Refer: https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#list-accounts - #[instrument(skip(client))] - pub async fn list(client: &Client, query: ListUsersQuery) -> Result { - let resp = client.get_query("/_synapse/admin/v2/users", &query).await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } - - /// Allows an administrator to modify a user account - /// - /// Refer: https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#create-or-modify-account - #[instrument(skip(client))] - pub async fn update(client: &Client, user_id: &UserId, body: UpdateUserBody) -> Result { - let resp = client - .put_json( - format!("/_synapse/admin/v2/users/{user_id}", user_id = user_id), - &body, - ) - .await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } - - /// **Note: This API is disabled when MSC3861 is enabled. [See #15582][1]** - /// - /// Get an access token that can be used to authenticate as that user. - /// Useful for when admins wish to do actions on behalf of a user. - /// - /// An optional `valid_until_ms` field can be specified in the request body - /// as an integer timestamp that specifies when the token should expire. - /// - /// **By default tokens do not expire. Note that this API does not allow a - /// user to login as themselves (to create more tokens).** - /// - /// Refer: https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#login-as-a-user - /// - /// [1]: https://github.com/matrix-org/synapse/pull/15582 - #[instrument(skip(client))] - pub async fn login_as_user( - client: &Client, - user_id: &UserId, - body: LoginAsUserBody, - ) -> Result { - let resp = client - .post_json( - format!( - "/_synapse/admin/v1/users/{user_id}/login", - user_id = user_id - ), - &body, - ) - .await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } -} diff --git a/crates/matrix/src/admin/resources/room.rs b/crates/matrix/src/admin/room.rs similarity index 100% rename from crates/matrix/src/admin/resources/room.rs rename to crates/matrix/src/admin/room.rs diff --git a/crates/matrix/src/admin/session.rs b/crates/matrix/src/admin/session.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/matrix/src/admin/threepid/get_user_for_threepid.rs b/crates/matrix/src/admin/threepid/get_user_for_threepid.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/matrix/src/admin/user.rs b/crates/matrix/src/admin/user.rs new file mode 100644 index 0000000..905b02f --- /dev/null +++ b/crates/matrix/src/admin/user.rs @@ -0,0 +1,271 @@ +//! [User Admin API](https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#user-admin-api) +//! +//! To use it, you will need to authenticate by providing an `access_token` +//! for a server admin: see Admin API. + +mod get_user; +mod get_users; +mod set_user; + +//use anyhow::Result; +//use ruma_common::UserId; +//use serde::{Deserialize, Serialize}; +//use tracing::instrument; +//use url::Url; + +//#[derive(Default)] +//pub struct UserService; + +//#[derive(Debug, Serialize, Deserialize)] +//pub struct ExternalId { +// pub auth_provider: String, +// pub external_id: String, +//} + +//#[derive(Debug, Serialize, Deserialize)] +//pub struct ThreePid { +// pub medium: String, +// pub address: String, +// pub added_at: u64, +// pub validated_at: u64, +//} + +//#[derive(Debug, Serialize, Deserialize)] +//pub struct User { +// /// User name postfixed with Matrix instance Host +// /// E.g. `@user:example.com` +// pub name: String, +// pub displayname: Option, +// pub threepids: Vec, +// pub avatar_url: Option, +// pub is_guest: bool, +// pub admin: bool, +// pub deactivated: bool, +// pub erased: bool, +// pub shadow_banned: bool, +// pub creation_ts: u64, +// pub appservice_id: Option, +// pub consent_server_notice_sent: Option, +// pub consent_version: Option, +// pub consent_ts: Option, +// pub external_ids: Vec, +// pub user_type: Option, +// pub locked: bool, +//} + +//#[derive(Debug, Serialize, Deserialize)] +//pub struct CreateUserBody { +// pub password: String, +// pub logout_devices: bool, +// pub displayname: Option, +// pub avatar_url: Option, +// pub threepids: Vec, +// pub external_ids: Vec, +// pub admin: bool, +// pub deactivated: bool, +// pub user_type: Option, +// pub locked: bool, +//} + +//#[derive(Debug, Default, Serialize, Deserialize)] +//pub struct ListUsersQuery { +// pub user_id: Option, +// pub name: Option, +// pub guests: Option, +// pub admins: Option, +// pub deactivated: Option, +// pub limit: Option, +// pub from: Option, +//} + +///// Data type for the response of the `GET /_synapse/admin/v2/users` endpoint. +//#[derive(Debug, Default, Serialize, Deserialize)] +//pub struct ListUser { +// pub name: String, +// pub user_type: Option, +// pub is_guest: usize, +// pub admin: usize, +// pub deactivated: usize, +// pub shadow_banned: bool, +// pub avatar_url: Option, +// pub creation_ts: u64, +// pub last_seen_ts: Option, +// pub erased: bool, +// pub locked: bool, +//} + +//#[derive(Debug, Default, Serialize, Deserialize)] +//pub struct ListUsersResponse { +// pub users: Vec, +// pub total: u64, +// #[serde(default)] +// pub next_token: Option, +//} + +//#[derive(Debug, Serialize, Deserialize)] +//pub struct UpdateUserBody { +// pub password: String, +// pub logout_devices: bool, +// pub displayname: Option, +// pub avatar_url: Option, +// pub threepids: Vec, +// pub external_ids: Vec, +// pub admin: bool, +// pub deactivated: bool, +// pub user_type: Option, +// pub locked: bool, +//} + +//#[derive(Debug, Default, Serialize, Deserialize)] +//pub struct LoginAsUserBody { +// pub valid_until_ms: Option, +//} + +//#[derive(Debug, Default, Serialize, Deserialize)] +//pub struct LoginAsUserResponse { +// pub access_token: String, +//} + +//pub struct QueryUserDataResponse { +// pub name: String, +// pub displayname: Option, +// pub threepids: Vec, +// pub avatar_url: Option, +// pub is_guest: bool, +// pub admin: bool, +// pub deactivated: bool, +// pub erased: bool, +// pub shadow_banned: bool, +// pub creation_ts: i64, +// pub appservice_id: Option, +// pub consent_server_notice_sent: Option, +// pub consent_version: Option, +// pub consent_ts: Option, +// pub external_ids: Vec>, +// pub user_type: Option, +//} + +//impl UserService { +// /// This API returns information about a specific user account. +// /// +// /// Refer: https://matrix-org.github.io/synapse/v1.88/admin_api/user_admin_api.html#query-user-account +// #[instrument(skip(client))] +// pub async fn query_user_account(client: &Client, user_id: &UserId) -> Result { +// let resp = client +// .get(format!( +// "/_synapse/admin/v2/users/{user_id}", +// user_id = user_id +// )) +// .await?; + +// if resp.status().is_success() { +// return Ok(resp.json().await?); +// } + +// let error = resp.json::().await?; + +// Err(anyhow::anyhow!(error.error)) +// } + +// /// Allows an administrator to create a user account. +// /// +// /// Note that internally Synapse uses this same endpoint to modify an +// /// existing user account, so this method will modify the existing user +// /// if [`UserId`] matches. +// /// +// /// Refer: https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#create-or-modify-account +// #[instrument(skip(client, body))] +// pub async fn create(client: &Client, user_id: &UserId, body: CreateUserBody) -> Result { +// let resp = client +// .put_json( +// format!("/_synapse/admin/v2/users/{user_id}", user_id = user_id), +// &body, +// ) +// .await?; + +// if resp.status().is_success() { +// return Ok(resp.json().await?); +// } + +// let error = resp.json::().await?; + +// Err(anyhow::anyhow!(error.error)) +// } + +// /// Returns all local user accounts. By default, the response is ordered by +// /// ascending user ID. +// /// +// /// Refer: https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#list-accounts +// #[instrument(skip(client))] +// pub async fn list(client: &Client, query: ListUsersQuery) -> Result { +// let resp = client.get_query("/_synapse/admin/v2/users", &query).await?; + +// if resp.status().is_success() { +// return Ok(resp.json().await?); +// } + +// let error = resp.json::().await?; + +// Err(anyhow::anyhow!(error.error)) +// } + +// /// Allows an administrator to modify a user account +// /// +// /// Refer: https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#create-or-modify-account +// #[instrument(skip(client))] +// pub async fn update(client: &Client, user_id: &UserId, body: UpdateUserBody) -> Result { +// let resp = client +// .put_json( +// format!("/_synapse/admin/v2/users/{user_id}", user_id = user_id), +// &body, +// ) +// .await?; + +// if resp.status().is_success() { +// return Ok(resp.json().await?); +// } + +// let error = resp.json::().await?; + +// Err(anyhow::anyhow!(error.error)) +// } + +// /// **Note: This API is disabled when MSC3861 is enabled. [See #15582][1]** +// /// +// /// Get an access token that can be used to authenticate as that user. +// /// Useful for when admins wish to do actions on behalf of a user. +// /// +// /// An optional `valid_until_ms` field can be specified in the request body +// /// as an integer timestamp that specifies when the token should expire. +// /// +// /// **By default tokens do not expire. Note that this API does not allow a +// /// user to login as themselves (to create more tokens).** +// /// +// /// Refer: https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#login-as-a-user +// /// +// /// [1]: https://github.com/matrix-org/synapse/pull/15582 +// #[instrument(skip(client))] +// pub async fn login_as_user( +// client: &Client, +// user_id: &UserId, +// body: LoginAsUserBody, +// ) -> Result { +// let resp = client +// .post_json( +// format!( +// "/_synapse/admin/v1/users/{user_id}/login", +// user_id = user_id +// ), +// &body, +// ) +// .await?; + +// if resp.status().is_success() { +// return Ok(resp.json().await?); +// } + +// let error = resp.json::().await?; + +// Err(anyhow::anyhow!(error.error)) +// } +//} diff --git a/crates/matrix/src/admin/user/get_user.rs b/crates/matrix/src/admin/user/get_user.rs new file mode 100644 index 0000000..c306f4e --- /dev/null +++ b/crates/matrix/src/admin/user/get_user.rs @@ -0,0 +1,11 @@ +use ruma_common::OwnedUserId; +use ruma_macros::request; + + + + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(query)] + pub user_id: OwnedUserId, +} diff --git a/crates/matrix/src/admin/user/get_users.rs b/crates/matrix/src/admin/user/get_users.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/matrix/src/admin/user/set_user.rs b/crates/matrix/src/admin/user/set_user.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/matrix/src/client/resources/error.rs b/crates/matrix/src/client/error.rs similarity index 100% rename from crates/matrix/src/client/resources/error.rs rename to crates/matrix/src/client/error.rs diff --git a/crates/matrix/src/client/resources/events.rs b/crates/matrix/src/client/events.rs similarity index 100% rename from crates/matrix/src/client/resources/events.rs rename to crates/matrix/src/client/events.rs diff --git a/crates/matrix/src/client/resources/login.rs b/crates/matrix/src/client/login.rs similarity index 100% rename from crates/matrix/src/client/resources/login.rs rename to crates/matrix/src/client/login.rs diff --git a/crates/matrix/src/client/mod.rs b/crates/matrix/src/client/mod.rs index 6d0fe39..43420e9 100644 --- a/crates/matrix/src/client/mod.rs +++ b/crates/matrix/src/client/mod.rs @@ -1 +1,6 @@ -pub mod resources; +pub mod error; +pub mod events; +pub mod login; +pub mod mxc; +pub mod room; +pub mod session; diff --git a/crates/matrix/src/client/resources/mxc.rs b/crates/matrix/src/client/mxc.rs similarity index 100% rename from crates/matrix/src/client/resources/mxc.rs rename to crates/matrix/src/client/mxc.rs diff --git a/crates/matrix/src/client/resources/mod.rs b/crates/matrix/src/client/resources/mod.rs deleted file mode 100644 index 43420e9..0000000 --- a/crates/matrix/src/client/resources/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod error; -pub mod events; -pub mod login; -pub mod mxc; -pub mod room; -pub mod session; diff --git a/crates/matrix/src/client/resources/room.rs b/crates/matrix/src/client/room.rs similarity index 100% rename from crates/matrix/src/client/resources/room.rs rename to crates/matrix/src/client/room.rs diff --git a/crates/matrix/src/client/resources/session.rs b/crates/matrix/src/client/session.rs similarity index 100% rename from crates/matrix/src/client/resources/session.rs rename to crates/matrix/src/client/session.rs diff --git a/crates/matrix/src/error.rs b/crates/matrix/src/error.rs deleted file mode 100644 index 31d6786..0000000 --- a/crates/matrix/src/error.rs +++ /dev/null @@ -1,7 +0,0 @@ -use serde::Deserialize; - -#[derive(Clone, Debug, Deserialize)] -pub struct MatrixError { - pub errcode: String, - pub error: String, -} diff --git a/crates/matrix/src/http.rs b/crates/matrix/src/http.rs deleted file mode 100644 index 22db5c8..0000000 --- a/crates/matrix/src/http.rs +++ /dev/null @@ -1,177 +0,0 @@ -use std::str::from_utf8; - -use anyhow::{bail, Result}; -use reqwest::{ - header::{HeaderMap, HeaderValue, AUTHORIZATION}, - Client as HttpClient, Response, -}; -use serde::Serialize; -use url::Url; - -#[derive(Clone, Debug)] -pub struct Client { - client: HttpClient, - base_url: Url, - token: Option, - server_name: String, -} - -impl Client { - pub fn new>(url: S, server_name: S) -> Result { - let url = Url::parse(url.as_ref())?; - let server_name = server_name.as_ref().to_string(); - - Ok(Self { - client: HttpClient::new(), - base_url: url, - token: None, - server_name, - }) - } - - #[inline] - pub fn server_name(&self) -> &str { - &self.server_name - } - - /// Sets the token to be used for authentication with the server. - pub fn set_token(&mut self, token: impl Into) -> Result<()> { - let token = token.into(); - - if token.is_empty() { - self.token = None; - bail!("Token cannot be empty"); - } - - self.token = Some(token); - Ok(()) - } - - /// Clear the token for safety purposes. - pub fn clear_token(&mut self) { - self.token = None; - } - - pub async fn get(&self, path: impl AsRef) -> Result { - let url = self.build_url(path)?; - let headers = self.build_headers()?; - let response = self.client.get(url).headers(headers).send().await?; - - Ok(response) - } - - pub async fn get_query( - &self, - path: impl AsRef, - params: impl Serialize, - ) -> Result { - let url = self.build_url_with_params(path, params)?; - let headers = self.build_headers()?; - let response = self.client.get(url).headers(headers).send().await?; - - Ok(response) - } - - pub async fn put_json(&self, path: impl AsRef, body: &T) -> Result - where - T: Serialize, - { - let url = self.build_url(path)?; - let headers = self.build_headers()?; - let resp = self - .client - .put(url) - .json(body) - .headers(headers) - .send() - .await?; - - Ok(resp) - } - - pub async fn post(&self, path: impl AsRef) -> Result { - let url = self.build_url(path)?; - let headers = self.build_headers()?; - let resp = self.client.post(url).headers(headers).send().await?; - - Ok(resp) - } - - pub async fn post_json(&self, path: impl AsRef, body: &T) -> Result - where - T: Serialize, - { - let url = self.build_url(path)?; - let headers = self.build_headers()?; - let resp = self - .client - .post(url) - .json(body) - .headers(headers) - .send() - .await?; - - Ok(resp) - } - - pub async fn delete(&self, path: impl AsRef) -> Result { - let url = self.build_url(path)?; - let headers = self.build_headers()?; - let response = self.client.delete(url).headers(headers).send().await?; - - Ok(response) - } - - pub async fn delete_json(&self, path: impl AsRef, body: &T) -> Result - where - T: Serialize, - { - let url = self.build_url(path)?; - let headers = self.build_headers()?; - let resp = self - .client - .delete(url) - .json(body) - .headers(headers) - .send() - .await?; - - Ok(resp) - } - - fn build_headers(&self) -> Result { - let mut headers = HeaderMap::new(); - - if let Some(token) = &self.token { - headers.insert( - AUTHORIZATION, - HeaderValue::from_str(&format!("Bearer {}", token))?, - ); - } - - Ok(headers) - } - - #[inline] - fn build_url(&self, path: impl AsRef) -> Result { - let mut next = self.base_url.clone(); - - next.set_path(path.as_ref()); - - Ok(next) - } - - fn build_url_with_params(&self, path: impl AsRef, params: impl Serialize) -> Result { - let mut url = self.build_url(path)?; - let mut buff = Vec::new(); - let qs_ser = &mut serde_qs::Serializer::new(&mut buff); - - serde_path_to_error::serialize(¶ms, qs_ser)?; - - let params = from_utf8(buff.as_slice())?.to_string(); - - url.set_query(Some(¶ms)); - - Ok(url) - } -} diff --git a/crates/matrix/src/lib.rs b/crates/matrix/src/lib.rs index 0afb5f3..2d6ce8a 100644 --- a/crates/matrix/src/lib.rs +++ b/crates/matrix/src/lib.rs @@ -2,25 +2,19 @@ //! //! Reexports `matrix_sdk` and provides implementations on Matrix Admin API. -mod http; +// mod http; -mod error; +// pub mod filter; -pub mod filter; +// pub use http::Client; -pub use http::Client; - -/// Implementation on the Administrator API of Matrix -/// -/// Refer: https://matrix-org.github.io/synapse/latest/usage/administration/index.html -pub mod admin; +/// Ruma re-exports +// pub use ruma_common; +// pub use ruma_events; -/// Implementation on the Client API of Matrix -/// -/// Different to the Matrix SDK, no user state is kept in the Client instance, -/// this is equivalent to making cURL requests to the Matrix server. -pub mod client; +// mod api { +mod admin; + // mod client; +// } -/// Ruma re-exports -pub use ruma_common; -pub use ruma_events; +use ruma_common::api::error::MatrixError as Error; diff --git a/crates/matrix/src/admin/resources/token/mod.rs b/crates/matrix/src/token/mod.rs similarity index 100% rename from crates/matrix/src/admin/resources/token/mod.rs rename to crates/matrix/src/token/mod.rs diff --git a/crates/matrix/src/admin/resources/token/shared_secret.rs b/crates/matrix/src/token/shared_secret.rs similarity index 100% rename from crates/matrix/src/admin/resources/token/shared_secret.rs rename to crates/matrix/src/token/shared_secret.rs From daa14d1ea719c88f3241acb13a91570daeaf24a9 Mon Sep 17 00:00:00 2001 From: mikoto Date: Sat, 24 Feb 2024 16:37:33 +0000 Subject: [PATCH 02/33] feat: Ruma API macro to clear up HTTP boilerplate --- Cargo.toml | 2 +- crates/matrix/Cargo.toml | 6 +- crates/matrix/src/admin/user.rs | 274 +--------------------- crates/matrix/src/admin/user/get_user.rs | 59 ++++- crates/matrix/src/admin/user/get_users.rs | 97 ++++++++ crates/matrix/src/lib.rs | 20 +- 6 files changed, 162 insertions(+), 296 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a21a106..6c8ff79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,8 +9,8 @@ repository = "https://github.com/commune-os/commune-rs" rust-version = "1.75.0" [workspace] -default-members = ["crates/server"] members = ["crates/core", "crates/matrix", "crates/server", "crates/test"] +default-members = ["crates/server"] resolver = "2" [workspace.dependencies] diff --git a/crates/matrix/Cargo.toml b/crates/matrix/Cargo.toml index c6ef21a..fa669aa 100644 --- a/crates/matrix/Cargo.toml +++ b/crates/matrix/Cargo.toml @@ -10,7 +10,7 @@ path = "src/lib.rs" [dependencies] ruma-events = { version = "0.27.11", features = ["html", "markdown"] } -ruma-common = { version = "0.12.0", features = ["api", "rand"], default_features = false } +ruma-common = { version = "0.12.0", default_features = false, features = ["api", "rand"] } ruma-macros = "0.12.0" # Workspace Dependencies @@ -22,3 +22,7 @@ serde = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } url = { workspace = true, features = ["serde"] } + +[features] +client = [] +server = [] diff --git a/crates/matrix/src/admin/user.rs b/crates/matrix/src/admin/user.rs index 905b02f..eb59f60 100644 --- a/crates/matrix/src/admin/user.rs +++ b/crates/matrix/src/admin/user.rs @@ -1,271 +1,3 @@ -//! [User Admin API](https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#user-admin-api) -//! -//! To use it, you will need to authenticate by providing an `access_token` -//! for a server admin: see Admin API. - -mod get_user; -mod get_users; -mod set_user; - -//use anyhow::Result; -//use ruma_common::UserId; -//use serde::{Deserialize, Serialize}; -//use tracing::instrument; -//use url::Url; - -//#[derive(Default)] -//pub struct UserService; - -//#[derive(Debug, Serialize, Deserialize)] -//pub struct ExternalId { -// pub auth_provider: String, -// pub external_id: String, -//} - -//#[derive(Debug, Serialize, Deserialize)] -//pub struct ThreePid { -// pub medium: String, -// pub address: String, -// pub added_at: u64, -// pub validated_at: u64, -//} - -//#[derive(Debug, Serialize, Deserialize)] -//pub struct User { -// /// User name postfixed with Matrix instance Host -// /// E.g. `@user:example.com` -// pub name: String, -// pub displayname: Option, -// pub threepids: Vec, -// pub avatar_url: Option, -// pub is_guest: bool, -// pub admin: bool, -// pub deactivated: bool, -// pub erased: bool, -// pub shadow_banned: bool, -// pub creation_ts: u64, -// pub appservice_id: Option, -// pub consent_server_notice_sent: Option, -// pub consent_version: Option, -// pub consent_ts: Option, -// pub external_ids: Vec, -// pub user_type: Option, -// pub locked: bool, -//} - -//#[derive(Debug, Serialize, Deserialize)] -//pub struct CreateUserBody { -// pub password: String, -// pub logout_devices: bool, -// pub displayname: Option, -// pub avatar_url: Option, -// pub threepids: Vec, -// pub external_ids: Vec, -// pub admin: bool, -// pub deactivated: bool, -// pub user_type: Option, -// pub locked: bool, -//} - -//#[derive(Debug, Default, Serialize, Deserialize)] -//pub struct ListUsersQuery { -// pub user_id: Option, -// pub name: Option, -// pub guests: Option, -// pub admins: Option, -// pub deactivated: Option, -// pub limit: Option, -// pub from: Option, -//} - -///// Data type for the response of the `GET /_synapse/admin/v2/users` endpoint. -//#[derive(Debug, Default, Serialize, Deserialize)] -//pub struct ListUser { -// pub name: String, -// pub user_type: Option, -// pub is_guest: usize, -// pub admin: usize, -// pub deactivated: usize, -// pub shadow_banned: bool, -// pub avatar_url: Option, -// pub creation_ts: u64, -// pub last_seen_ts: Option, -// pub erased: bool, -// pub locked: bool, -//} - -//#[derive(Debug, Default, Serialize, Deserialize)] -//pub struct ListUsersResponse { -// pub users: Vec, -// pub total: u64, -// #[serde(default)] -// pub next_token: Option, -//} - -//#[derive(Debug, Serialize, Deserialize)] -//pub struct UpdateUserBody { -// pub password: String, -// pub logout_devices: bool, -// pub displayname: Option, -// pub avatar_url: Option, -// pub threepids: Vec, -// pub external_ids: Vec, -// pub admin: bool, -// pub deactivated: bool, -// pub user_type: Option, -// pub locked: bool, -//} - -//#[derive(Debug, Default, Serialize, Deserialize)] -//pub struct LoginAsUserBody { -// pub valid_until_ms: Option, -//} - -//#[derive(Debug, Default, Serialize, Deserialize)] -//pub struct LoginAsUserResponse { -// pub access_token: String, -//} - -//pub struct QueryUserDataResponse { -// pub name: String, -// pub displayname: Option, -// pub threepids: Vec, -// pub avatar_url: Option, -// pub is_guest: bool, -// pub admin: bool, -// pub deactivated: bool, -// pub erased: bool, -// pub shadow_banned: bool, -// pub creation_ts: i64, -// pub appservice_id: Option, -// pub consent_server_notice_sent: Option, -// pub consent_version: Option, -// pub consent_ts: Option, -// pub external_ids: Vec>, -// pub user_type: Option, -//} - -//impl UserService { -// /// This API returns information about a specific user account. -// /// -// /// Refer: https://matrix-org.github.io/synapse/v1.88/admin_api/user_admin_api.html#query-user-account -// #[instrument(skip(client))] -// pub async fn query_user_account(client: &Client, user_id: &UserId) -> Result { -// let resp = client -// .get(format!( -// "/_synapse/admin/v2/users/{user_id}", -// user_id = user_id -// )) -// .await?; - -// if resp.status().is_success() { -// return Ok(resp.json().await?); -// } - -// let error = resp.json::().await?; - -// Err(anyhow::anyhow!(error.error)) -// } - -// /// Allows an administrator to create a user account. -// /// -// /// Note that internally Synapse uses this same endpoint to modify an -// /// existing user account, so this method will modify the existing user -// /// if [`UserId`] matches. -// /// -// /// Refer: https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#create-or-modify-account -// #[instrument(skip(client, body))] -// pub async fn create(client: &Client, user_id: &UserId, body: CreateUserBody) -> Result { -// let resp = client -// .put_json( -// format!("/_synapse/admin/v2/users/{user_id}", user_id = user_id), -// &body, -// ) -// .await?; - -// if resp.status().is_success() { -// return Ok(resp.json().await?); -// } - -// let error = resp.json::().await?; - -// Err(anyhow::anyhow!(error.error)) -// } - -// /// Returns all local user accounts. By default, the response is ordered by -// /// ascending user ID. -// /// -// /// Refer: https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#list-accounts -// #[instrument(skip(client))] -// pub async fn list(client: &Client, query: ListUsersQuery) -> Result { -// let resp = client.get_query("/_synapse/admin/v2/users", &query).await?; - -// if resp.status().is_success() { -// return Ok(resp.json().await?); -// } - -// let error = resp.json::().await?; - -// Err(anyhow::anyhow!(error.error)) -// } - -// /// Allows an administrator to modify a user account -// /// -// /// Refer: https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#create-or-modify-account -// #[instrument(skip(client))] -// pub async fn update(client: &Client, user_id: &UserId, body: UpdateUserBody) -> Result { -// let resp = client -// .put_json( -// format!("/_synapse/admin/v2/users/{user_id}", user_id = user_id), -// &body, -// ) -// .await?; - -// if resp.status().is_success() { -// return Ok(resp.json().await?); -// } - -// let error = resp.json::().await?; - -// Err(anyhow::anyhow!(error.error)) -// } - -// /// **Note: This API is disabled when MSC3861 is enabled. [See #15582][1]** -// /// -// /// Get an access token that can be used to authenticate as that user. -// /// Useful for when admins wish to do actions on behalf of a user. -// /// -// /// An optional `valid_until_ms` field can be specified in the request body -// /// as an integer timestamp that specifies when the token should expire. -// /// -// /// **By default tokens do not expire. Note that this API does not allow a -// /// user to login as themselves (to create more tokens).** -// /// -// /// Refer: https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#login-as-a-user -// /// -// /// [1]: https://github.com/matrix-org/synapse/pull/15582 -// #[instrument(skip(client))] -// pub async fn login_as_user( -// client: &Client, -// user_id: &UserId, -// body: LoginAsUserBody, -// ) -> Result { -// let resp = client -// .post_json( -// format!( -// "/_synapse/admin/v1/users/{user_id}/login", -// user_id = user_id -// ), -// &body, -// ) -// .await?; - -// if resp.status().is_success() { -// return Ok(resp.json().await?); -// } - -// let error = resp.json::().await?; - -// Err(anyhow::anyhow!(error.error)) -// } -//} +pub mod get_user; +pub mod get_users; +pub mod set_user; diff --git a/crates/matrix/src/admin/user/get_user.rs b/crates/matrix/src/admin/user/get_user.rs index c306f4e..f19b5b1 100644 --- a/crates/matrix/src/admin/user/get_user.rs +++ b/crates/matrix/src/admin/user/get_user.rs @@ -1,11 +1,58 @@ -use ruma_common::OwnedUserId; -use ruma_macros::request; - - +use ruma_common::{ + api::{request, response, Metadata}, + metadata, + thirdparty::ThirdPartyIdentifier, + OwnedMxcUri, OwnedUserId, +}; +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_synapse/admin/v2/users/:name", + } +}; #[request(error = crate::Error)] pub struct Request { - #[ruma_api(query)] - pub user_id: OwnedUserId, + #[ruma_api(path)] + pub name: OwnedUserId, +} + +#[response(error = crate::Error)] +pub struct Response { + pub name: OwnedUserId, + + pub displayname: Option, + + pub threepids: Vec, + + pub avatar_url: Option, + + pub admin: bool, + + pub deactivated: bool, + + pub erased: bool, + + pub shadow_banned: bool, + + pub creation_ts: u64, + + pub consent_server_notice_sent: Option, + + pub consent_ts: Option, + + pub external_ids: Vec, + + pub locked: bool, +} + +#[derive(Clone, Debug)] +pub struct ExternalId { + pub auth_provider: String, + + pub external_id: String, } diff --git a/crates/matrix/src/admin/user/get_users.rs b/crates/matrix/src/admin/user/get_users.rs index e69de29..eeab703 100644 --- a/crates/matrix/src/admin/user/get_users.rs +++ b/crates/matrix/src/admin/user/get_users.rs @@ -0,0 +1,97 @@ +use ruma_common::{ + api::{request, response, Metadata, Direction}, + thirdparty::ThirdPartyIdentifier, + OwnedMxcUri, OwnedUserId, metadata, +}; +use ruma_macros::{request, response}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_synapse/admin/v2/users", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[serde(skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub user_id: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub name: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub admins: Option, + + #[serde(skip_serializing_if = "ruma_common::serde::is_default")] + #[ruma_api(query)] + pub deactivated: bool, + + #[serde(skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub limit: Option, + + #[serde(skip_serializing_if = "ruma_common::serde::is_default")] + #[ruma_api(query)] + pub from: u64, + + #[serde(skip_serializing_if = "ruma_common::serde::is_default")] + #[ruma_api(query)] + pub order_by: OrderBy, + + #[serde(skip_serializing_if = "ruma_common::serde::is_default")] + #[ruma_api(query)] + pub dir: Direction, +} + +#[response(error = crate::Error)] +pub struct Response { + pub name: OwnedUserId, + + pub displayname: Option, + + pub threepids: Vec, + + pub avatar_url: Option, + + pub admin: bool, + + pub deactivated: bool, + + pub erased: bool, + + pub shadow_banned: bool, + + pub creation_ts: u64, + + pub locked: bool, +} + +#[derive(Clone, Debug, Default)] +#[allow(dead_code)] +pub enum OrderBy { + #[default] + Name, + + Admin, + + UserType, + + Deactivated, + + ShadowBanned, + + Displayname, + + AvatarUrl, + + CreationTs, + + LastSeenTs, +} diff --git a/crates/matrix/src/lib.rs b/crates/matrix/src/lib.rs index 2d6ce8a..16bf545 100644 --- a/crates/matrix/src/lib.rs +++ b/crates/matrix/src/lib.rs @@ -1,20 +1,6 @@ -//! Crate to centralize all Matrix dependencies. -//! -//! Reexports `matrix_sdk` and provides implementations on Matrix Admin API. - -// mod http; - +pub mod admin; +pub mod client; // pub mod filter; -// pub use http::Client; - -/// Ruma re-exports -// pub use ruma_common; -// pub use ruma_events; - -// mod api { -mod admin; - // mod client; -// } - +#[allow(unused_imports)] use ruma_common::api::error::MatrixError as Error; From dda107f05cb10e29f024a881c6e91feb85c60d7a Mon Sep 17 00:00:00 2001 From: mikoto Date: Sat, 24 Feb 2024 17:03:35 +0000 Subject: [PATCH 03/33] feat: reuse `User` type across all requests --- crates/matrix/src/admin/user.rs | 45 +++++++++++++++++++++++ crates/matrix/src/admin/user/get_user.rs | 44 ++++------------------ crates/matrix/src/admin/user/get_users.rs | 29 ++++----------- crates/matrix/src/admin/user/set_user.rs | 28 ++++++++++++++ 4 files changed, 88 insertions(+), 58 deletions(-) diff --git a/crates/matrix/src/admin/user.rs b/crates/matrix/src/admin/user.rs index eb59f60..20275af 100644 --- a/crates/matrix/src/admin/user.rs +++ b/crates/matrix/src/admin/user.rs @@ -1,3 +1,48 @@ +use ruma_common::{thirdparty::ThirdPartyIdentifier, OwnedMxcUri, OwnedUserId}; +use serde::{Deserialize, Serialize}; + pub mod get_user; pub mod get_users; pub mod set_user; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct User { + #[serde(rename = "name")] + pub user_id: OwnedUserId, + + pub displayname: Option, + + pub avatar_url: Option, + + pub threepids: Vec, + + pub external_ids: Vec, + + pub admin: bool, + + pub deactivated: bool, + + #[serde(skip_serializing)] + pub erased: bool, + + #[serde(skip_serializing)] + pub shadow_banned: bool, + + #[serde(skip_serializing)] + pub creation_ts: u64, + + #[serde(skip_serializing)] + pub consent_server_notice_sent: Option, + + #[serde(skip_serializing)] + pub consent_ts: Option, + + pub locked: bool, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ExternalId { + pub auth_provider: String, + + pub external_id: String, +} diff --git a/crates/matrix/src/admin/user/get_user.rs b/crates/matrix/src/admin/user/get_user.rs index f19b5b1..da302be 100644 --- a/crates/matrix/src/admin/user/get_user.rs +++ b/crates/matrix/src/admin/user/get_user.rs @@ -1,58 +1,28 @@ use ruma_common::{ api::{request, response, Metadata}, - metadata, - thirdparty::ThirdPartyIdentifier, - OwnedMxcUri, OwnedUserId, + metadata, OwnedUserId, }; +use super::User; + #[allow(dead_code)] const METADATA: Metadata = metadata! { method: GET, rate_limited: false, authentication: AccessToken, history: { - unstable => "/_synapse/admin/v2/users/:name", + unstable => "/_synapse/admin/v2/users/:user_id", } }; #[request(error = crate::Error)] pub struct Request { #[ruma_api(path)] - pub name: OwnedUserId, + pub user_id: OwnedUserId, } #[response(error = crate::Error)] pub struct Response { - pub name: OwnedUserId, - - pub displayname: Option, - - pub threepids: Vec, - - pub avatar_url: Option, - - pub admin: bool, - - pub deactivated: bool, - - pub erased: bool, - - pub shadow_banned: bool, - - pub creation_ts: u64, - - pub consent_server_notice_sent: Option, - - pub consent_ts: Option, - - pub external_ids: Vec, - - pub locked: bool, -} - -#[derive(Clone, Debug)] -pub struct ExternalId { - pub auth_provider: String, - - pub external_id: String, + #[ruma_api(body)] + pub user: User, } diff --git a/crates/matrix/src/admin/user/get_users.rs b/crates/matrix/src/admin/user/get_users.rs index eeab703..263fc66 100644 --- a/crates/matrix/src/admin/user/get_users.rs +++ b/crates/matrix/src/admin/user/get_users.rs @@ -1,9 +1,9 @@ use ruma_common::{ - api::{request, response, Metadata, Direction}, - thirdparty::ThirdPartyIdentifier, - OwnedMxcUri, OwnedUserId, metadata, + api::{request, response, Direction, Metadata}, + metadata, OwnedUserId, }; -use ruma_macros::{request, response}; + +use super::User; #[allow(dead_code)] const METADATA: Metadata = metadata! { @@ -52,25 +52,12 @@ pub struct Request { #[response(error = crate::Error)] pub struct Response { - pub name: OwnedUserId, - - pub displayname: Option, - - pub threepids: Vec, - - pub avatar_url: Option, - - pub admin: bool, - - pub deactivated: bool, - - pub erased: bool, - - pub shadow_banned: bool, + #[serde(flatten)] + users: Vec, - pub creation_ts: u64, + next_token: String, - pub locked: bool, + total: u64, } #[derive(Clone, Debug, Default)] diff --git a/crates/matrix/src/admin/user/set_user.rs b/crates/matrix/src/admin/user/set_user.rs index e69de29..83868f1 100644 --- a/crates/matrix/src/admin/user/set_user.rs +++ b/crates/matrix/src/admin/user/set_user.rs @@ -0,0 +1,28 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedUserId, +}; + +use super::User; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: PUT, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_synapse/admin/v2/users/:user_id", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub user_id: OwnedUserId, + + #[ruma_api(body)] + pub user: User, +} + +#[response(error = crate::Error)] +pub struct Response {} From 8a993bb823cead5f52ad95155cc022f05d5a8d4d Mon Sep 17 00:00:00 2001 From: mikoto Date: Sun, 25 Feb 2024 14:06:57 +0000 Subject: [PATCH 04/33] feat: admin migration complete, client wip --- crates/matrix/src/admin/mod.rs | 3 +- crates/matrix/src/admin/room.rs | 485 ++------------- crates/matrix/src/admin/room/delete_room.rs | 49 ++ .../forward_extremities/delete.rs} | 0 .../forward_extremities/get.rs} | 0 crates/matrix/src/admin/room/get_members.rs | 27 + crates/matrix/src/admin/room/get_room.rs | 28 + crates/matrix/src/admin/room/get_rooms.rs | 83 +++ crates/matrix/src/admin/room/get_state.rs | 36 ++ crates/matrix/src/admin/user.rs | 5 + .../matrix/src/admin/user/get_user_by_3pid.rs | 31 + crates/matrix/src/admin/user/get_users.rs | 1 - crates/matrix/src/client/room.rs | 555 ++++++++---------- .../room/ban_from_room.rs} | 0 crates/matrix/src/client/room/create_room.rs | 70 +++ crates/matrix/src/client/room/forget_room.rs | 0 .../matrix/src/client/room/invite_to_room.rs | 0 crates/matrix/src/client/room/join_room.rs | 0 .../matrix/src/client/room/kick_from_room.rs | 0 crates/matrix/src/client/room/leave_room.rs | 0 .../matrix/src/client/room/unban_from_room.rs | 0 crates/matrix/src/filter.rs | 212 ------- crates/matrix/src/lib.rs | 1 - 23 files changed, 619 insertions(+), 967 deletions(-) create mode 100644 crates/matrix/src/admin/room/delete_room.rs rename crates/matrix/src/admin/{membership/joined_room.rs => room/forward_extremities/delete.rs} (100%) rename crates/matrix/src/admin/{session.rs => room/forward_extremities/get.rs} (100%) create mode 100644 crates/matrix/src/admin/room/get_members.rs create mode 100644 crates/matrix/src/admin/room/get_room.rs create mode 100644 crates/matrix/src/admin/room/get_rooms.rs create mode 100644 crates/matrix/src/admin/room/get_state.rs create mode 100644 crates/matrix/src/admin/user/get_user_by_3pid.rs rename crates/matrix/src/{admin/threepid/get_user_for_threepid.rs => client/room/ban_from_room.rs} (100%) create mode 100644 crates/matrix/src/client/room/create_room.rs create mode 100644 crates/matrix/src/client/room/forget_room.rs create mode 100644 crates/matrix/src/client/room/invite_to_room.rs create mode 100644 crates/matrix/src/client/room/join_room.rs create mode 100644 crates/matrix/src/client/room/kick_from_room.rs create mode 100644 crates/matrix/src/client/room/leave_room.rs create mode 100644 crates/matrix/src/client/room/unban_from_room.rs delete mode 100644 crates/matrix/src/filter.rs diff --git a/crates/matrix/src/admin/mod.rs b/crates/matrix/src/admin/mod.rs index 5fb5809..2a77d65 100644 --- a/crates/matrix/src/admin/mod.rs +++ b/crates/matrix/src/admin/mod.rs @@ -1,2 +1,3 @@ -// pub mod room; mod user; +mod room; +// mod room; diff --git a/crates/matrix/src/admin/room.rs b/crates/matrix/src/admin/room.rs index 9b153c5..17117b9 100644 --- a/crates/matrix/src/admin/room.rs +++ b/crates/matrix/src/admin/room.rs @@ -1,474 +1,59 @@ -//! [Room Admin API](https://matrix-org.github.io/synapse/latest/admin_api/rooms.html) +//! reference: https://matrix-org.github.io/synapse/latest/admin_api/rooms.html //! -//! To use it, you will need to authenticate by providing an `access_token` -//! for a server admin: see Admin API. - -use anyhow::Result; -use ruma_common::{serde::Raw, EventId, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomId}; -use ruma_events::{AnyMessageLikeEvent, AnyStateEvent, AnyTimelineEvent}; -use serde::{Deserialize, Serialize}; -use tracing::instrument; - -use crate::{error::MatrixError, filter::RoomEventFilter, http::Client}; - -#[derive(Default)] -pub struct RoomService; - -#[derive(Default, Debug, Serialize)] -pub struct ListRoomQuery { - #[serde(skip_serializing_if = "Option::is_none")] - pub from: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub limit: Option, - - pub order_by: OrderBy, - - pub direction: Direction, - - #[serde(skip_serializing_if = "String::is_empty")] - pub search_term: String, -} - -#[derive(Debug, Default, Serialize)] -pub struct MessagesQuery { - #[serde(skip_serializing_if = "String::is_empty")] - pub from: String, - - #[serde(skip_serializing_if = "String::is_empty")] - pub to: String, - - #[serde(skip_serializing_if = "Option::is_none")] - pub limit: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub filter: Option, - - pub direction: Direction, -} +//! This module contains handlers for managing rooms. + +use ruma_common::{ + room::RoomType, EventEncryptionAlgorithm, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, + OwnedUserId, RoomVersionId, +}; +use ruma_events::room::{history_visibility::HistoryVisibility, join_rules::JoinRule}; +use serde::Deserialize; + +mod delete_room; +mod get_members; +pub mod get_room; +pub mod get_rooms; +mod get_state; + +#[derive(Clone, Debug, Deserialize)] +pub struct Room { + pub room_id: OwnedRoomId, -#[derive(Default, Debug, Serialize)] -pub struct TimestampToEventQuery { - #[serde(skip_serializing_if = "Option::is_none")] - pub ts: Option, + pub canonical_alias: Option, - pub direction: Direction, -} + pub avatar: Option, -#[derive(Default, Debug, Serialize)] -pub struct EventContextQuery { - #[serde(skip_serializing_if = "Option::is_none")] - pub limit: Option, + pub name: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub filter: Option, -} + pub joined_members: u64, -#[derive(Debug, Serialize)] -pub struct ReplaceRoomQuery { - #[serde(rename = "new_room_user_id")] - pub admin: OwnedUserId, + pub joined_local_members: u64, - #[serde(skip_serializing_if = "String::is_empty")] - pub room_name: String, + pub version: RoomVersionId, - #[serde(skip_serializing_if = "String::is_empty")] - pub message: String, -} + pub creator: OwnedUserId, -#[derive(Default, Debug, Serialize)] -pub struct DeleteQuery { - #[serde(flatten, skip_serializing_if = "Option::is_none")] - pub new_room: Option, + pub encryption: Option, - pub block: bool, + pub federatable: bool, - pub purge: bool, -} + pub public: bool, -#[derive(Debug, Deserialize)] -pub struct ListRoomResponse { - pub rooms: Vec, - pub offset: Option, - pub total_rooms: Option, - pub prev_batch: Option, - pub next_batch: Option, -} + pub join_rules: Option, -#[derive(Debug, Deserialize)] -pub struct MembersResponse { - pub members: Vec, - pub total: u64, -} + pub history_visibility: Option, -#[derive(Debug, Deserialize)] -pub struct State { - #[serde(rename = "type")] - pub kind: String, - pub state_key: String, - pub etc: Option, -} + pub state_events: u64, -#[derive(Debug, Deserialize)] -pub struct StateResponse { - pub state: Vec, -} + pub room_type: Option, -#[derive(Debug, Deserialize)] -pub struct Room { - /// Room ID postfixed with Matrix instance Host - /// E.g. `!room:example.com` - pub room_id: OwnedRoomId, - pub name: Option, - pub canonical_alias: Option, - pub joined_members: u64, - pub joined_local_members: u64, - pub version: Option, - pub creator: Option, - pub encryption: Option, - pub federatable: bool, - pub public: bool, - pub join_rules: Option, - pub guest_access: Option, - pub history_visibility: Option, - pub state_events: u64, - pub room_type: Option, #[serde(flatten)] pub details: Option, } -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] pub struct RoomDetails { - pub avatar: Option, pub topic: Option, - pub joined_local_devices: u64, - pub forgotten: bool, -} - -#[derive(Debug, Deserialize)] -pub struct GetEventsResponse { - pub chunk: Raw>, - pub start: String, - pub end: String, - pub state: Option>, -} - -#[derive(Debug, Deserialize)] -pub struct TimestampToEventResponse { - pub event_id: String, - pub origin_server_ts: u64, -} - -#[derive(Debug, Deserialize)] -pub struct ForwardExtremities { - pub event_id: String, - pub state_group: u64, - pub depth: u64, - pub received_ts: u64, -} - -#[derive(Debug, Deserialize)] -pub struct CheckForwardExtremitiesResponse { - pub count: u64, - pub result: Vec, -} - -#[derive(Debug, Deserialize)] -pub struct DeleteForwardExtremitiesResponse { - pub deleted: u64, -} - -#[derive(Debug, Deserialize)] -pub struct EventContextResponse { - pub start: String, - pub end: String, - pub events_before: Vec>, - pub event: Raw, - pub events_after: Vec>, - pub state: Vec>, -} - -#[derive(Debug, Deserialize)] -pub struct DeleteRoomResponse { - pub kicked_users: Vec, - pub failed_to_kick_users: Vec, - pub local_aliases: Vec, - pub new_room_id: Option, -} - -impl RoomService { - /// Returns information about a specific room - /// - /// Refer: https://matrix-org.github.io/synapse/latest/admin_api/rooms.html#room-details-api - #[instrument(skip(client))] - pub async fn get_one(client: &Client, room_id: &RoomId) -> Result { - let resp = client - .get(format!( - "/_synapse/admin/v1/rooms/{room_id}", - room_id = room_id - )) - .await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } - - /// Returns all rooms. By default, the response is ordered alphabetically by - /// room name - /// - /// Refer: https://matrix-org.github.io/synapse/latest/admin_api/rooms.html#list-room-api - #[instrument(skip(client))] - pub async fn get_all(client: &Client, query: ListRoomQuery) -> Result { - let resp = client.get_query("/_synapse/admin/v1/rooms", &query).await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } - - /// Allows a server admin to get a list of all members of a room - /// - /// Refer: https://matrix-org.github.io/synapse/latest/admin_api/rooms.html#room-members-api - #[instrument(skip(client))] - pub async fn get_members(client: &Client, room_id: &RoomId) -> Result { - let resp = client - .get(format!( - "/_synapse/admin/v1/rooms/{room_id}/members", - room_id = room_id - )) - .await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } - - /// Allows a server admin to get all messages sent to a room in a given - /// timeframe - /// - /// Refer: https://matrix-org.github.io/synapse/latest/admin_api/rooms.html#room-messages-api - #[instrument(skip(client))] - pub async fn get_state(client: &Client, room_id: &RoomId) -> Result { - let resp = client - .get(format!( - "/_synapse/admin/v1/rooms/{room_id}/state", - room_id = room_id - )) - .await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } - - /// Allows a server admin to get the `event_id` of the closest event to the - /// given timestamp - /// - /// Refer: https://matrix-org.github.io/synapse/latest/admin_api/rooms.html#room-timestamp-to-event-api - #[instrument(skip(client))] - pub async fn get_timestamp_to_event( - client: &Client, - room_id: &RoomId, - query: TimestampToEventQuery, - ) -> Result { - let resp = client - .get_query( - format!( - "/_synapse/admin/v1/rooms/{room_id}/timestamp_to_event", - room_id = room_id - ), - &query, - ) - .await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } - /// Allows a server admin to check the status of forward extremities for a - /// room - /// - /// Refer: https://matrix-org.github.io/synapse/latest/admin_api/rooms.html#check-for-forward-extremities - #[instrument(skip(client))] - pub async fn check_forward_extremities( - client: &Client, - room_id: &RoomId, - ) -> Result { - let resp = client - .get(format!( - "/_synapse/admin/v1/rooms/{room_id}/forward_extremities", - room_id = room_id - )) - .await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } - - /// Allows a server admin to delete forward extremities for a room - /// WARNING: Please ensure you know what you're doing and read the related issue [#1760](https://github.com/matrix-org/synapse/issues/1760) - /// - /// Refer: https://matrix-org.github.io/synapse/latest/admin_api/rooms.html#delete-for-forward-extremities - #[instrument(skip(client))] - pub async fn delete_forward_extremities( - client: &Client, - room_id: &RoomId, - ) -> Result { - let resp = client - .delete(format!( - "/_synapse/admin/v1/rooms/{room_id}/forward_extremities", - room_id = room_id - )) - .await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } - - /// allows server admins to remove rooms from the server and block these - /// rooms - /// - /// Refer: https://matrix-org.github.io/synapse/latest/admin_api/rooms.html#delete-room-api - #[instrument(skip(client))] - pub async fn delete_room( - client: &Client, - room_id: &RoomId, - query: DeleteQuery, - ) -> Result { - let resp = client - .delete_json( - format!("/_synapse/admin/v1/rooms/{room_id}", room_id = room_id), - &query, - ) - .await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } -} - -impl RoomService { - /// Allows a server admin to get a list of all state events in a room - /// - /// Refer: https://matrix-org.github.io/synapse/latest/admin_api/rooms.html#room-state-api - #[instrument(skip(client))] - pub async fn get_room_events( - client: &Client, - room_id: &RoomId, - query: MessagesQuery, - ) -> Result { - let resp = client - .get_query( - format!( - "/_synapse/admin/v1/rooms/{room_id}/messages", - room_id = room_id - ), - &query, - ) - .await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } - - /// This API lets a client find the context of an event. This is designed - /// primarily to investigate abuse reports. - /// - /// Refer: https://matrix-org.github.io/synapse/latest/admin_api/rooms.html#event-context-api - #[instrument(skip(client))] - pub async fn get_event_context( - client: &Client, - room_id: &RoomId, - event_id: &EventId, - query: EventContextQuery, - ) -> Result { - let resp = client - .get_query( - format!( - "/_synapse/admin/v1/rooms/{room_id}/context/{event_id}", - room_id = room_id, - event_id = event_id, - ), - &query, - ) - .await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } -} - -#[derive(Debug, Default, Clone, Serialize)] -pub enum Direction { - #[serde(rename = "f")] - #[default] - Forward, - #[serde(rename = "b")] - Backward, -} - -#[derive(Default, Debug, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum OrderBy { - #[default] - Name, - CanonicalAlias, - JoinedMembers, - JoinedLocalMembers, - Version, - Creator, - Encryption, - Federatable, - Public, - JoinRules, - GuestAccess, - HistoryVisibility, - StateEvents, + pub forgotten: bool, } diff --git a/crates/matrix/src/admin/room/delete_room.rs b/crates/matrix/src/admin/room/delete_room.rs new file mode 100644 index 0000000..2774f1e --- /dev/null +++ b/crates/matrix/src/admin/room/delete_room.rs @@ -0,0 +1,49 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedRoomId, OwnedUserId, +}; +use serde::Serialize; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: DELETE, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_synapse/admin/v2/rooms/:room_id", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub room_id: OwnedRoomId, + + #[serde(flatten, skip_serializing_if = "Option::is_none")] + pub new_room: Option, + + #[serde(skip_serializing_if = "ruma_common::serde::is_default")] + pub block: bool, + + #[serde(skip_serializing_if = "ruma_common::serde::is_true")] + pub purge: bool, + + #[serde(skip_serializing_if = "ruma_common::serde::is_default")] + pub force_purge: bool, +} + +#[response(error = crate::Error)] +pub struct Response { + delete_id: String, +} + +#[derive(Clone, Debug, Serialize)] +pub struct NewRoomParams { + pub creator: OwnedUserId, + + #[serde(skip_serializing_if = "String::is_empty")] + pub name: String, + + #[serde(skip_serializing_if = "String::is_empty")] + pub message: String, +} diff --git a/crates/matrix/src/admin/membership/joined_room.rs b/crates/matrix/src/admin/room/forward_extremities/delete.rs similarity index 100% rename from crates/matrix/src/admin/membership/joined_room.rs rename to crates/matrix/src/admin/room/forward_extremities/delete.rs diff --git a/crates/matrix/src/admin/session.rs b/crates/matrix/src/admin/room/forward_extremities/get.rs similarity index 100% rename from crates/matrix/src/admin/session.rs rename to crates/matrix/src/admin/room/forward_extremities/get.rs diff --git a/crates/matrix/src/admin/room/get_members.rs b/crates/matrix/src/admin/room/get_members.rs new file mode 100644 index 0000000..79cd4e7 --- /dev/null +++ b/crates/matrix/src/admin/room/get_members.rs @@ -0,0 +1,27 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedRoomId, OwnedUserId, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_synapse/admin/v1/rooms/:room_id/members", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub room_id: OwnedRoomId, +} + +#[response(error = crate::Error)] +pub struct Response { + pub members: Vec, + + pub total: u64, +} diff --git a/crates/matrix/src/admin/room/get_room.rs b/crates/matrix/src/admin/room/get_room.rs new file mode 100644 index 0000000..b55a66b --- /dev/null +++ b/crates/matrix/src/admin/room/get_room.rs @@ -0,0 +1,28 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedRoomId, +}; + +use super::Room; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_synapse/admin/v1/rooms/:room_id", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub room_id: OwnedRoomId, +} + +#[response(error = crate::Error)] +pub struct Response { + #[ruma_api(body)] + pub room: Room, +} diff --git a/crates/matrix/src/admin/room/get_rooms.rs b/crates/matrix/src/admin/room/get_rooms.rs new file mode 100644 index 0000000..5607c2b --- /dev/null +++ b/crates/matrix/src/admin/room/get_rooms.rs @@ -0,0 +1,83 @@ +use ruma_common::{ + api::{request, response, Metadata, Direction}, + metadata, +}; +use serde::Serialize; + +use super::Room; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_synapse/admin/v1/rooms", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[serde(default)] + #[ruma_api(query)] + pub from: u64, + + #[serde(skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub limit: Option, + + #[ruma_api(query)] + pub order_by: OrderBy, + + #[ruma_api(query)] + pub direction: Direction, + + #[serde(skip_serializing_if = "String::is_empty")] + #[ruma_api(query)] + pub search_term: String, +} + +#[response(error = crate::Error)] +pub struct Response { + rooms: Vec, + + offset: u64, + + #[serde(rename = "total_rooms")] + total: u64, + + next_batch: Option, + + prev_batch: Option, +} + +#[derive(Clone, Default, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum OrderBy { + #[default] + Name, + + CanonicalAlias, + + JoinedMembers, + + JoinedLocalMembers, + + Version, + + Creator, + + Encryption, + + Federatable, + + Public, + + JoinRules, + + GuestAccess, + + HistoryVisibility, + + StateEvents, +} diff --git a/crates/matrix/src/admin/room/get_state.rs b/crates/matrix/src/admin/room/get_state.rs new file mode 100644 index 0000000..6cf649b --- /dev/null +++ b/crates/matrix/src/admin/room/get_state.rs @@ -0,0 +1,36 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedRoomId, +}; +use serde::Deserialize; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_synapse/admin/v1/rooms/:room_id/state", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub room_id: OwnedRoomId, +} + +#[response(error = crate::Error)] +pub struct Response { + pub state: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct State { + #[serde(rename = "type")] + pub kind: String, + + pub state_key: String, + + pub etc: bool, +} diff --git a/crates/matrix/src/admin/user.rs b/crates/matrix/src/admin/user.rs index 20275af..2e39665 100644 --- a/crates/matrix/src/admin/user.rs +++ b/crates/matrix/src/admin/user.rs @@ -1,7 +1,12 @@ +//! reference: https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html +//! +//! This module contains handlers for managing user accounts. + use ruma_common::{thirdparty::ThirdPartyIdentifier, OwnedMxcUri, OwnedUserId}; use serde::{Deserialize, Serialize}; pub mod get_user; +pub mod get_user_by_3pid; pub mod get_users; pub mod set_user; diff --git a/crates/matrix/src/admin/user/get_user_by_3pid.rs b/crates/matrix/src/admin/user/get_user_by_3pid.rs new file mode 100644 index 0000000..2b9e56c --- /dev/null +++ b/crates/matrix/src/admin/user/get_user_by_3pid.rs @@ -0,0 +1,31 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, thirdparty::Medium, +}; + +use super::User; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_synapse/admin/v1/threepid/:medium/users/:address", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub medium: Medium, + + #[ruma_api(path)] + pub address: String, +} + +#[response(error = crate::Error)] +pub struct Response { + #[ruma_api(body)] + pub user: User, +} diff --git a/crates/matrix/src/admin/user/get_users.rs b/crates/matrix/src/admin/user/get_users.rs index 263fc66..40a926d 100644 --- a/crates/matrix/src/admin/user/get_users.rs +++ b/crates/matrix/src/admin/user/get_users.rs @@ -52,7 +52,6 @@ pub struct Request { #[response(error = crate::Error)] pub struct Response { - #[serde(flatten)] users: Vec, next_token: String, diff --git a/crates/matrix/src/client/room.rs b/crates/matrix/src/client/room.rs index 5d51035..9c28b1c 100644 --- a/crates/matrix/src/client/room.rs +++ b/crates/matrix/src/client/room.rs @@ -1,302 +1,253 @@ -use anyhow::Result; -use ruma_common::{serde::Raw, OwnedRoomId, OwnedUserId, RoomId, RoomOrAliasId}; -use ruma_events::{room::power_levels::RoomPowerLevelsEventContent, AnyInitialStateEvent}; -use serde::{Deserialize, Serialize}; -use tracing::instrument; - -use crate::error::MatrixError; - -#[derive(Debug, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum RoomPreset { - PrivateChat, - PublicChat, - TrustedPrivateChat, -} - -#[derive(Default, Debug, Serialize)] -pub struct RoomCreationContent { - #[serde(rename = "m.federate")] - pub federate: bool, -} - -#[derive(Default, Debug, Serialize)] -#[serde(rename_all = "lowercase")] -pub enum RoomVisibility { - Public, - #[default] - Private, -} - -#[derive(Default, Debug, Serialize)] -pub struct CreateRoomBody { - #[serde(skip_serializing_if = "<[_]>::is_empty")] - pub initial_state: Vec>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub creation_content: Option, - - #[serde(skip_serializing_if = "<[_]>::is_empty")] - pub invite: Vec, - - pub is_direct: bool, - - #[serde(skip_serializing_if = "String::is_empty")] - pub name: String, - - #[serde(skip_serializing_if = "Option::is_none")] - pub power_override: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub preset: Option, - - #[serde(skip_serializing_if = "String::is_empty")] - pub room_alias_name: String, - - #[serde(skip_serializing_if = "String::is_empty")] - pub topic: String, - - #[serde(skip_serializing_if = "Option::is_none")] - pub visibility: Option, -} - -#[derive(Default, Debug, Serialize)] -pub struct JoinRoomBody { - #[serde(skip_serializing_if = "String::is_empty")] - pub reason: String, -} - -#[derive(Default, Debug, Serialize)] -pub struct ForgetRoomBody { - #[serde(skip_serializing_if = "String::is_empty")] - pub reason: String, -} - -#[derive(Default, Debug, Serialize)] -pub struct LeaveRoomBody { - #[serde(skip_serializing_if = "String::is_empty")] - pub reason: String, -} - -#[derive(Debug, Serialize)] -pub struct RoomKickOrBanBody { - #[serde(skip_serializing_if = "String::is_empty")] - pub reason: String, - - pub user_id: OwnedUserId, -} - -#[derive(Debug, Deserialize)] -pub struct CreateRoomResponse { - pub room_id: OwnedRoomId, -} - -#[derive(Debug, Deserialize)] -pub struct JoinRoomResponse { - pub room_id: OwnedRoomId, -} - -#[derive(Debug, Deserialize)] -pub struct LeaveRoomResponse {} - -#[derive(Debug, Deserialize)] -pub struct ForgetRoomResponse {} - -#[derive(Debug, Deserialize)] -pub struct RoomKickOrBanResponse {} - -pub struct RoomService; - -impl RoomService { - /// Create a new room with various configuration options. - /// - /// Refer: https://spec.matrix.org/v1.9/client-server-api/#creation - #[instrument(skip(client, access_token))] - pub async fn create( - client: &crate::http::Client, - access_token: impl Into, - body: CreateRoomBody, - ) -> Result { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let resp = tmp - .post_json("/_matrix/client/v3/createRoom", &body) - .await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } - - /// Join a particular room, if we are allowed to participate. - /// - /// Refer: https://spec.matrix.org/v1.9/client-server-api/#joining-rooms - #[instrument(skip(client, access_token))] - pub async fn join( - client: &crate::http::Client, - access_token: impl Into, - alias_or_id: &RoomOrAliasId, - body: JoinRoomBody, - ) -> Result { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let resp = tmp - .post_json(format!("/_matrix/client/v3/join/{alias_or_id}"), &body) - .await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } - - /// Leave a particular room. - /// They are still allowed to retrieve the history which they were - /// previously allowed to see. - /// - /// Refer: https://spec.matrix.org/v1.9/client-server-api/#leaving-rooms - #[instrument(skip(client, access_token))] - pub async fn leave( - client: &crate::http::Client, - access_token: impl Into, - room_id: &RoomId, - body: LeaveRoomBody, - ) -> Result { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let resp = tmp - .post_json(format!("/_matrix/client/v3/rooms/{room_id}/leave"), &body) - .await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } - - /// Forget a particular room. - /// This will prevent the user from accessing the history of the room. - /// - /// Refer: https://spec.matrix.org/v1.9/client-server-api/#leaving-rooms - #[instrument(skip(client, access_token))] - pub async fn forget( - client: &crate::http::Client, - access_token: impl Into, - room_id: &RoomId, - body: ForgetRoomBody, - ) -> Result { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let resp = tmp - .post_json(format!("/_matrix/client/v3/rooms/{room_id}/forget"), &body) - .await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } - - /// Kick a user from a particular room. - /// The caller must have the required power level in order to perform this - /// operation. - /// - /// Refer: https://spec.matrix.org/v1.9/client-server-api/#leaving-rooms - #[instrument(skip(client, access_token))] - pub async fn kick( - client: &crate::http::Client, - access_token: impl Into, - room_id: &RoomId, - body: RoomKickOrBanBody, - ) -> Result { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let resp = tmp - .post_json(format!("/_matrix/client/v3/rooms/{room_id}/kick"), &body) - .await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } - - /// Ban a user from a particular room. - /// This will kick them too if they are still a member. - /// The caller must have the required power level in order to perform this - /// operation. - /// - /// Refer: https://spec.matrix.org/v1.9/client-server-api/#leaving-rooms - #[instrument(skip(client, access_token))] - pub async fn ban( - client: &crate::http::Client, - access_token: impl Into, - room_id: &RoomId, - body: RoomKickOrBanBody, - ) -> Result { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let resp = tmp - .post_json(format!("/_matrix/client/v3/rooms/{room_id}/ban"), &body) - .await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } - - /// Unban a user from a particular room. - /// This will allow them to re-join or be re-invited. - /// The caller must have the required power level in order to perform this - /// operation. - /// - /// Refer: https://spec.matrix.org/v1.9/client-server-api/#banning-users-in-a-room - #[instrument(skip(client, access_token))] - pub async fn unban( - client: &crate::http::Client, - access_token: impl Into, - room_id: &RoomId, - body: RoomKickOrBanBody, - ) -> Result { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let resp = tmp - .post_json(format!("/_matrix/client/v3/rooms/{room_id}/unban"), &body) - .await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } -} +mod create_room; +mod get_room; +mod join_room; +mod forget_room; +mod leave_room; +mod kick_from_room; +mod ban_from_room; +mod unban_from_room; + +//#[derive(Default, Debug, Serialize)] +//pub struct CreateRoomBody { +//} + +//#[derive(Default, Debug, Serialize)] +//pub struct JoinRoomBody { +// #[serde(skip_serializing_if = "String::is_empty")] +// pub reason: String, +//} + +//#[derive(Default, Debug, Serialize)] +//pub struct ForgetRoomBody { +// #[serde(skip_serializing_if = "String::is_empty")] +// pub reason: String, +//} + +//#[derive(Default, Debug, Serialize)] +//pub struct LeaveRoomBody { +// #[serde(skip_serializing_if = "String::is_empty")] +// pub reason: String, +//} + +//#[derive(Debug, Serialize)] +//pub struct RoomKickOrBanBody { +// #[serde(skip_serializing_if = "String::is_empty")] +// pub reason: String, + +// pub user_id: OwnedUserId, +//} + +//#[derive(Debug, Deserialize)] +//pub struct CreateRoomResponse { +// pub room_id: OwnedRoomId, +//} + +//#[derive(Debug, Deserialize)] +//pub struct JoinRoomResponse { +// pub room_id: OwnedRoomId, +//} + +//#[derive(Debug, Deserialize)] +//pub struct LeaveRoomResponse {} + +//#[derive(Debug, Deserialize)] +//pub struct ForgetRoomResponse {} + +//#[derive(Debug, Deserialize)] +//pub struct RoomKickOrBanResponse {} + +//pub struct RoomService; + +//impl RoomService { +// /// Create a new room with various configuration options. +// /// +// /// Refer: https://spec.matrix.org/v1.9/client-server-api/#creation +// #[instrument(skip(client, access_token))] +// pub async fn create( +// client: &crate::http::Client, +// access_token: impl Into, +// body: CreateRoomBody, +// ) -> Result { +// let mut tmp = (*client).clone(); +// tmp.set_token(access_token)?; + +// let resp = tmp +// .post_json("/_matrix/client/v3/createRoom", &body) +// .await?; + +// if resp.status().is_success() { +// return Ok(resp.json().await?); +// } + +// let error = resp.json::().await?; + +// Err(anyhow::anyhow!(error.error)) +// } + +// /// Join a particular room, if we are allowed to participate. +// /// +// /// Refer: https://spec.matrix.org/v1.9/client-server-api/#joining-rooms +// #[instrument(skip(client, access_token))] +// pub async fn join( +// client: &crate::http::Client, +// access_token: impl Into, +// alias_or_id: &RoomOrAliasId, +// body: JoinRoomBody, +// ) -> Result { +// let mut tmp = (*client).clone(); +// tmp.set_token(access_token)?; + +// let resp = tmp +// .post_json(format!("/_matrix/client/v3/join/{alias_or_id}"), +// &body) .await?; + +// if resp.status().is_success() { +// return Ok(resp.json().await?); +// } + +// let error = resp.json::().await?; + +// Err(anyhow::anyhow!(error.error)) +// } + +// /// Leave a particular room. +// /// They are still allowed to retrieve the history which they were +// /// previously allowed to see. +// /// +// /// Refer: https://spec.matrix.org/v1.9/client-server-api/#leaving-rooms +// #[instrument(skip(client, access_token))] +// pub async fn leave( +// client: &crate::http::Client, +// access_token: impl Into, +// room_id: &RoomId, +// body: LeaveRoomBody, +// ) -> Result { +// let mut tmp = (*client).clone(); +// tmp.set_token(access_token)?; + +// let resp = tmp +// .post_json(format!("/_matrix/client/v3/rooms/{room_id}/leave"), +// &body) .await?; + +// if resp.status().is_success() { +// return Ok(resp.json().await?); +// } + +// let error = resp.json::().await?; + +// Err(anyhow::anyhow!(error.error)) +// } + +// /// Forget a particular room. +// /// This will prevent the user from accessing the history of the room. +// /// +// /// Refer: https://spec.matrix.org/v1.9/client-server-api/#leaving-rooms +// #[instrument(skip(client, access_token))] +// pub async fn forget( +// client: &crate::http::Client, +// access_token: impl Into, +// room_id: &RoomId, +// body: ForgetRoomBody, +// ) -> Result { +// let mut tmp = (*client).clone(); +// tmp.set_token(access_token)?; + +// let resp = tmp +// .post_json(format!("/_matrix/client/v3/rooms/{room_id}/forget"), +// &body) .await?; + +// if resp.status().is_success() { +// return Ok(resp.json().await?); +// } + +// let error = resp.json::().await?; + +// Err(anyhow::anyhow!(error.error)) +// } + +// /// Kick a user from a particular room. +// /// The caller must have the required power level in order to perform this +// /// operation. +// /// +// /// Refer: https://spec.matrix.org/v1.9/client-server-api/#leaving-rooms +// #[instrument(skip(client, access_token))] +// pub async fn kick( +// client: &crate::http::Client, +// access_token: impl Into, +// room_id: &RoomId, +// body: RoomKickOrBanBody, +// ) -> Result { +// let mut tmp = (*client).clone(); +// tmp.set_token(access_token)?; + +// let resp = tmp +// .post_json(format!("/_matrix/client/v3/rooms/{room_id}/kick"), +// &body) .await?; + +// if resp.status().is_success() { +// return Ok(resp.json().await?); +// } + +// let error = resp.json::().await?; + +// Err(anyhow::anyhow!(error.error)) +// } + +// /// Ban a user from a particular room. +// /// This will kick them too if they are still a member. +// /// The caller must have the required power level in order to perform this +// /// operation. +// /// +// /// Refer: https://spec.matrix.org/v1.9/client-server-api/#leaving-rooms +// #[instrument(skip(client, access_token))] +// pub async fn ban( +// client: &crate::http::Client, +// access_token: impl Into, +// room_id: &RoomId, +// body: RoomKickOrBanBody, +// ) -> Result { +// let mut tmp = (*client).clone(); +// tmp.set_token(access_token)?; + +// let resp = tmp +// .post_json(format!("/_matrix/client/v3/rooms/{room_id}/ban"), +// &body) .await?; + +// if resp.status().is_success() { +// return Ok(resp.json().await?); +// } + +// let error = resp.json::().await?; + +// Err(anyhow::anyhow!(error.error)) +// } + +// /// Unban a user from a particular room. +// /// This will allow them to re-join or be re-invited. +// /// The caller must have the required power level in order to perform this +// /// operation. +// /// +// /// Refer: https://spec.matrix.org/v1.9/client-server-api/#banning-users-in-a-room +// #[instrument(skip(client, access_token))] +// pub async fn unban( +// client: &crate::http::Client, +// access_token: impl Into, +// room_id: &RoomId, +// body: RoomKickOrBanBody, +// ) -> Result { +// let mut tmp = (*client).clone(); +// tmp.set_token(access_token)?; + +// let resp = tmp +// .post_json(format!("/_matrix/client/v3/rooms/{room_id}/unban"), +// &body) .await?; + +// if resp.status().is_success() { +// return Ok(resp.json().await?); +// } + +// let error = resp.json::().await?; + +// Err(anyhow::anyhow!(error.error)) +// } +//} diff --git a/crates/matrix/src/admin/threepid/get_user_for_threepid.rs b/crates/matrix/src/client/room/ban_from_room.rs similarity index 100% rename from crates/matrix/src/admin/threepid/get_user_for_threepid.rs rename to crates/matrix/src/client/room/ban_from_room.rs diff --git a/crates/matrix/src/client/room/create_room.rs b/crates/matrix/src/client/room/create_room.rs new file mode 100644 index 0000000..73f9793 --- /dev/null +++ b/crates/matrix/src/client/room/create_room.rs @@ -0,0 +1,70 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedRoomId, OwnedUserId, +}; +use ruma_events::room::power_levels::RoomPowerLevels; +use serde::Serialize; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: false, + authentication: AccessToken, + history: { + 1.9 => "/_matrix/client/v3/createRoom", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[serde(skip_serializing_if = "Option::is_none")] + pub creation_content: Option, + + // #[serde(skip_serializing_if = "<[_]>::is_empty")] + // pub initial_state: Vec>, // TODO + #[serde(skip_serializing_if = "<[_]>::is_empty")] + pub invite: Vec, + + pub is_direct: bool, + + #[serde(skip_serializing_if = "String::is_empty")] + pub name: String, + + #[serde( + rename = "power_level_content_override", + skip_serializing_if = "Option::is_none" + )] + pub power_override: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub preset: RoomPreset, + + #[serde(rename = "alias", skip_serializing_if = "String::is_empty")] + pub room_alias_name: String, + + #[serde(skip_serializing_if = "String::is_empty")] + pub topic: String, +} + +#[response(error = crate::Error)] +pub struct Response { + room_id: OwnedRoomId, +} + +#[derive(Clone, Debug, Default, Serialize)] +pub struct RoomCreationContent { + #[serde(rename = "m.federate")] + pub federate: bool, +} + +#[derive(Clone, Debug, Default, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum RoomPreset { + PublicChat, + + PrivateChat, + + #[default] + TrustedPrivateChat, +} + diff --git a/crates/matrix/src/client/room/forget_room.rs b/crates/matrix/src/client/room/forget_room.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/matrix/src/client/room/invite_to_room.rs b/crates/matrix/src/client/room/invite_to_room.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/matrix/src/client/room/join_room.rs b/crates/matrix/src/client/room/join_room.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/matrix/src/client/room/kick_from_room.rs b/crates/matrix/src/client/room/kick_from_room.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/matrix/src/client/room/leave_room.rs b/crates/matrix/src/client/room/leave_room.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/matrix/src/client/room/unban_from_room.rs b/crates/matrix/src/client/room/unban_from_room.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/matrix/src/filter.rs b/crates/matrix/src/filter.rs deleted file mode 100644 index 3d4e398..0000000 --- a/crates/matrix/src/filter.rs +++ /dev/null @@ -1,212 +0,0 @@ -use anyhow::Result; -use ruma_common::{OwnedRoomId, OwnedUserId, UserId}; -use ruma_events::TimelineEventType; -use serde::{Deserialize, Serialize}; - -use crate::{error::MatrixError, Client}; - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub enum EventFormat { - #[default] - Client, - Federation, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct Filter { - #[serde(skip_serializing_if = "Option::is_none", rename = "account_data")] - pub account: Option, - - #[serde(skip_serializing_if = "<[_]>::is_empty")] - pub event_fields: Vec, - - #[serde(skip_serializing_if = "Option::is_none")] - pub event_format: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub presence: Option, - - #[serde(skip_serializing_if = "Option::is_none", rename = "room")] - pub room: Option, -} - -impl Filter { - pub fn room_events(filter: RoomEventFilter) -> Self { - Self { - room: Some(RoomFilter { - timeline: Some(filter), - ..Default::default() - }), - ..Default::default() - } - } -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct EventFilter { - #[serde(skip_serializing_if = "Option::is_none")] - pub limit: Option, - - #[serde(skip_serializing_if = "<[_]>::is_empty")] - pub not_senders: Vec, - - #[serde(skip_serializing_if = "<[_]>::is_empty")] - pub not_types: Vec, - - #[serde(skip_serializing_if = "<[_]>::is_empty")] - pub senders: Vec, - - #[serde(skip_serializing_if = "<[_]>::is_empty")] - pub types: Vec, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct RoomFilter { - #[serde(skip_serializing_if = "Option::is_none")] - pub account_data: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub ephemeral: Option, - - pub include_leave: bool, - - #[serde(skip_serializing_if = "<[_]>::is_empty")] - pub not_rooms: Vec, - - #[serde(skip_serializing_if = "<[_]>::is_empty")] - pub rooms: Vec, - - #[serde(skip_serializing_if = "Option::is_none")] - pub state: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub timeline: Option, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct RoomEventFilter { - #[serde(skip_serializing_if = "Option::is_none")] - pub limit: Option, - - #[serde(skip_serializing_if = "<[_]>::is_empty")] - pub not_rooms: Vec, - - #[serde(skip_serializing_if = "<[_]>::is_empty")] - pub not_senders: Vec, - - #[serde(skip_serializing_if = "<[_]>::is_empty")] - pub not_types: Vec, - - #[serde(skip_serializing_if = "<[_]>::is_empty")] - pub rooms: Vec, - - #[serde(skip_serializing_if = "<[_]>::is_empty")] - pub senders: Vec, - - #[serde(skip_serializing_if = "<[_]>::is_empty")] - pub types: Vec, - - #[serde(skip_serializing_if = "Option::is_none", rename = "contains_url")] - pub include_urls: Option, - - pub include_redundant_members: bool, - - pub lazy_load_members: bool, - - pub unread_thread_notifications: bool, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct StateFilter { - #[serde(skip_serializing_if = "Option::is_none")] - pub limit: Option, - - #[serde(skip_serializing_if = "<[_]>::is_empty")] - pub not_rooms: Vec, - - #[serde(skip_serializing_if = "<[_]>::is_empty")] - pub not_senders: Vec, - - #[serde(skip_serializing_if = "<[_]>::is_empty")] - pub not_types: Vec, - - #[serde(skip_serializing_if = "<[_]>::is_empty")] - pub rooms: Vec, - - #[serde(skip_serializing_if = "<[_]>::is_empty")] - pub senders: Vec, - - #[serde(skip_serializing_if = "<[_]>::is_empty")] - pub types: Vec, - - #[serde(skip_serializing_if = "Option::is_none", rename = "contains_url")] - pub include_urls: Option, - - pub include_redundant_members: bool, - - pub lazy_load_members: bool, - - pub unread_thread_notifications: bool, -} - -pub struct FilterService; - -#[derive(Debug, Deserialize)] -pub struct FilterResponse { - pub filter_id: String, -} - -impl FilterService { - pub async fn create( - client: &Client, - access_token: impl Into, - user_id: &UserId, - body: Filter, - ) -> Result { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let resp = tmp - .post_json( - format!( - "/_matrix/client/v3/user/{user_id}/filter", - user_id = user_id - ), - &body, - ) - .await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } - - pub async fn get( - client: &Client, - access_token: impl Into, - user_id: &UserId, - filter_id: String, - ) -> Result { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let resp = tmp - .get(format!( - "/_matrix/client/v3/user/{user_id}/filter/{filter_id}", - user_id = user_id - )) - .await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } -} diff --git a/crates/matrix/src/lib.rs b/crates/matrix/src/lib.rs index 16bf545..2b2848e 100644 --- a/crates/matrix/src/lib.rs +++ b/crates/matrix/src/lib.rs @@ -1,6 +1,5 @@ pub mod admin; pub mod client; -// pub mod filter; #[allow(unused_imports)] use ruma_common::api::error::MatrixError as Error; From e0da21485f5b4c2d9f4339570a1efab11bae9d8d Mon Sep 17 00:00:00 2001 From: mikoto Date: Tue, 27 Feb 2024 09:02:06 +0000 Subject: [PATCH 05/33] fix: client room API --- crates/matrix/src/client/room.rs | 253 +----------------- .../matrix/src/client/room/ban_from_room.rs | 28 ++ crates/matrix/src/client/room/create_room.rs | 20 +- crates/matrix/src/client/room/forget_room.rs | 26 ++ crates/matrix/src/client/room/get_rooms.rs | 70 +++++ crates/matrix/src/client/room/join_room.rs | 28 ++ .../matrix/src/client/room/kick_from_room.rs | 28 ++ crates/matrix/src/client/room/leave_room.rs | 26 ++ .../matrix/src/client/room/unban_from_room.rs | 28 ++ 9 files changed, 248 insertions(+), 259 deletions(-) create mode 100644 crates/matrix/src/client/room/get_rooms.rs diff --git a/crates/matrix/src/client/room.rs b/crates/matrix/src/client/room.rs index 9c28b1c..2cf5ffe 100644 --- a/crates/matrix/src/client/room.rs +++ b/crates/matrix/src/client/room.rs @@ -1,253 +1,8 @@ +mod ban_from_room; mod create_room; -mod get_room; -mod join_room; mod forget_room; -mod leave_room; +mod get_rooms; +mod join_room; mod kick_from_room; -mod ban_from_room; +mod leave_room; mod unban_from_room; - -//#[derive(Default, Debug, Serialize)] -//pub struct CreateRoomBody { -//} - -//#[derive(Default, Debug, Serialize)] -//pub struct JoinRoomBody { -// #[serde(skip_serializing_if = "String::is_empty")] -// pub reason: String, -//} - -//#[derive(Default, Debug, Serialize)] -//pub struct ForgetRoomBody { -// #[serde(skip_serializing_if = "String::is_empty")] -// pub reason: String, -//} - -//#[derive(Default, Debug, Serialize)] -//pub struct LeaveRoomBody { -// #[serde(skip_serializing_if = "String::is_empty")] -// pub reason: String, -//} - -//#[derive(Debug, Serialize)] -//pub struct RoomKickOrBanBody { -// #[serde(skip_serializing_if = "String::is_empty")] -// pub reason: String, - -// pub user_id: OwnedUserId, -//} - -//#[derive(Debug, Deserialize)] -//pub struct CreateRoomResponse { -// pub room_id: OwnedRoomId, -//} - -//#[derive(Debug, Deserialize)] -//pub struct JoinRoomResponse { -// pub room_id: OwnedRoomId, -//} - -//#[derive(Debug, Deserialize)] -//pub struct LeaveRoomResponse {} - -//#[derive(Debug, Deserialize)] -//pub struct ForgetRoomResponse {} - -//#[derive(Debug, Deserialize)] -//pub struct RoomKickOrBanResponse {} - -//pub struct RoomService; - -//impl RoomService { -// /// Create a new room with various configuration options. -// /// -// /// Refer: https://spec.matrix.org/v1.9/client-server-api/#creation -// #[instrument(skip(client, access_token))] -// pub async fn create( -// client: &crate::http::Client, -// access_token: impl Into, -// body: CreateRoomBody, -// ) -> Result { -// let mut tmp = (*client).clone(); -// tmp.set_token(access_token)?; - -// let resp = tmp -// .post_json("/_matrix/client/v3/createRoom", &body) -// .await?; - -// if resp.status().is_success() { -// return Ok(resp.json().await?); -// } - -// let error = resp.json::().await?; - -// Err(anyhow::anyhow!(error.error)) -// } - -// /// Join a particular room, if we are allowed to participate. -// /// -// /// Refer: https://spec.matrix.org/v1.9/client-server-api/#joining-rooms -// #[instrument(skip(client, access_token))] -// pub async fn join( -// client: &crate::http::Client, -// access_token: impl Into, -// alias_or_id: &RoomOrAliasId, -// body: JoinRoomBody, -// ) -> Result { -// let mut tmp = (*client).clone(); -// tmp.set_token(access_token)?; - -// let resp = tmp -// .post_json(format!("/_matrix/client/v3/join/{alias_or_id}"), -// &body) .await?; - -// if resp.status().is_success() { -// return Ok(resp.json().await?); -// } - -// let error = resp.json::().await?; - -// Err(anyhow::anyhow!(error.error)) -// } - -// /// Leave a particular room. -// /// They are still allowed to retrieve the history which they were -// /// previously allowed to see. -// /// -// /// Refer: https://spec.matrix.org/v1.9/client-server-api/#leaving-rooms -// #[instrument(skip(client, access_token))] -// pub async fn leave( -// client: &crate::http::Client, -// access_token: impl Into, -// room_id: &RoomId, -// body: LeaveRoomBody, -// ) -> Result { -// let mut tmp = (*client).clone(); -// tmp.set_token(access_token)?; - -// let resp = tmp -// .post_json(format!("/_matrix/client/v3/rooms/{room_id}/leave"), -// &body) .await?; - -// if resp.status().is_success() { -// return Ok(resp.json().await?); -// } - -// let error = resp.json::().await?; - -// Err(anyhow::anyhow!(error.error)) -// } - -// /// Forget a particular room. -// /// This will prevent the user from accessing the history of the room. -// /// -// /// Refer: https://spec.matrix.org/v1.9/client-server-api/#leaving-rooms -// #[instrument(skip(client, access_token))] -// pub async fn forget( -// client: &crate::http::Client, -// access_token: impl Into, -// room_id: &RoomId, -// body: ForgetRoomBody, -// ) -> Result { -// let mut tmp = (*client).clone(); -// tmp.set_token(access_token)?; - -// let resp = tmp -// .post_json(format!("/_matrix/client/v3/rooms/{room_id}/forget"), -// &body) .await?; - -// if resp.status().is_success() { -// return Ok(resp.json().await?); -// } - -// let error = resp.json::().await?; - -// Err(anyhow::anyhow!(error.error)) -// } - -// /// Kick a user from a particular room. -// /// The caller must have the required power level in order to perform this -// /// operation. -// /// -// /// Refer: https://spec.matrix.org/v1.9/client-server-api/#leaving-rooms -// #[instrument(skip(client, access_token))] -// pub async fn kick( -// client: &crate::http::Client, -// access_token: impl Into, -// room_id: &RoomId, -// body: RoomKickOrBanBody, -// ) -> Result { -// let mut tmp = (*client).clone(); -// tmp.set_token(access_token)?; - -// let resp = tmp -// .post_json(format!("/_matrix/client/v3/rooms/{room_id}/kick"), -// &body) .await?; - -// if resp.status().is_success() { -// return Ok(resp.json().await?); -// } - -// let error = resp.json::().await?; - -// Err(anyhow::anyhow!(error.error)) -// } - -// /// Ban a user from a particular room. -// /// This will kick them too if they are still a member. -// /// The caller must have the required power level in order to perform this -// /// operation. -// /// -// /// Refer: https://spec.matrix.org/v1.9/client-server-api/#leaving-rooms -// #[instrument(skip(client, access_token))] -// pub async fn ban( -// client: &crate::http::Client, -// access_token: impl Into, -// room_id: &RoomId, -// body: RoomKickOrBanBody, -// ) -> Result { -// let mut tmp = (*client).clone(); -// tmp.set_token(access_token)?; - -// let resp = tmp -// .post_json(format!("/_matrix/client/v3/rooms/{room_id}/ban"), -// &body) .await?; - -// if resp.status().is_success() { -// return Ok(resp.json().await?); -// } - -// let error = resp.json::().await?; - -// Err(anyhow::anyhow!(error.error)) -// } - -// /// Unban a user from a particular room. -// /// This will allow them to re-join or be re-invited. -// /// The caller must have the required power level in order to perform this -// /// operation. -// /// -// /// Refer: https://spec.matrix.org/v1.9/client-server-api/#banning-users-in-a-room -// #[instrument(skip(client, access_token))] -// pub async fn unban( -// client: &crate::http::Client, -// access_token: impl Into, -// room_id: &RoomId, -// body: RoomKickOrBanBody, -// ) -> Result { -// let mut tmp = (*client).clone(); -// tmp.set_token(access_token)?; - -// let resp = tmp -// .post_json(format!("/_matrix/client/v3/rooms/{room_id}/unban"), -// &body) .await?; - -// if resp.status().is_success() { -// return Ok(resp.json().await?); -// } - -// let error = resp.json::().await?; - -// Err(anyhow::anyhow!(error.error)) -// } -//} diff --git a/crates/matrix/src/client/room/ban_from_room.rs b/crates/matrix/src/client/room/ban_from_room.rs index e69de29..5e52971 100644 --- a/crates/matrix/src/client/room/ban_from_room.rs +++ b/crates/matrix/src/client/room/ban_from_room.rs @@ -0,0 +1,28 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedRoomId, OwnedUserId, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: true, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/v3/rooms/{room_id}/ban", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub room_id: OwnedRoomId, + + pub user_id: OwnedUserId, + + #[serde(skip_serializing_if = "String::is_empty")] + pub reason: String, +} + +#[response(error = crate::Error)] +pub struct Response {} diff --git a/crates/matrix/src/client/room/create_room.rs b/crates/matrix/src/client/room/create_room.rs index 73f9793..f27009a 100644 --- a/crates/matrix/src/client/room/create_room.rs +++ b/crates/matrix/src/client/room/create_room.rs @@ -1,14 +1,14 @@ use ruma_common::{ api::{request, response, Metadata}, - metadata, OwnedRoomId, OwnedUserId, + metadata, OwnedRoomId, OwnedUserId, serde::Raw, }; -use ruma_events::room::power_levels::RoomPowerLevels; +use ruma_events::{room::power_levels::RoomPowerLevels, AnyInitialStateEvent}; use serde::Serialize; #[allow(dead_code)] const METADATA: Metadata = metadata! { method: POST, - rate_limited: false, + rate_limited: true, authentication: AccessToken, history: { 1.9 => "/_matrix/client/v3/createRoom", @@ -20,8 +20,9 @@ pub struct Request { #[serde(skip_serializing_if = "Option::is_none")] pub creation_content: Option, - // #[serde(skip_serializing_if = "<[_]>::is_empty")] - // pub initial_state: Vec>, // TODO + #[serde(skip_serializing_if = "<[_]>::is_empty")] + pub initial_state: Vec>, + #[serde(skip_serializing_if = "<[_]>::is_empty")] pub invite: Vec, @@ -39,8 +40,8 @@ pub struct Request { #[serde(skip_serializing_if = "Option::is_none")] pub preset: RoomPreset, - #[serde(rename = "alias", skip_serializing_if = "String::is_empty")] - pub room_alias_name: String, + #[serde(rename = "room_alias_name", skip_serializing_if = "String::is_empty")] + pub alias: String, #[serde(skip_serializing_if = "String::is_empty")] pub topic: String, @@ -48,10 +49,10 @@ pub struct Request { #[response(error = crate::Error)] pub struct Response { - room_id: OwnedRoomId, + pub room_id: OwnedRoomId, } -#[derive(Clone, Debug, Default, Serialize)] +#[derive(Clone, Debug, Serialize)] pub struct RoomCreationContent { #[serde(rename = "m.federate")] pub federate: bool, @@ -67,4 +68,3 @@ pub enum RoomPreset { #[default] TrustedPrivateChat, } - diff --git a/crates/matrix/src/client/room/forget_room.rs b/crates/matrix/src/client/room/forget_room.rs index e69de29..1b72ee2 100644 --- a/crates/matrix/src/client/room/forget_room.rs +++ b/crates/matrix/src/client/room/forget_room.rs @@ -0,0 +1,26 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedRoomId, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: true, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/v3/rooms/{room_id}/forget", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub room_id: OwnedRoomId, + + #[serde(skip_serializing_if = "String::is_empty")] + pub reason: String, +} + +#[response(error = crate::Error)] +pub struct Response {} diff --git a/crates/matrix/src/client/room/get_rooms.rs b/crates/matrix/src/client/room/get_rooms.rs new file mode 100644 index 0000000..5208a1a --- /dev/null +++ b/crates/matrix/src/client/room/get_rooms.rs @@ -0,0 +1,70 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, + room::RoomType, + OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedServerName, +}; +use ruma_events::room::join_rules::JoinRule; +use serde::Deserialize; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: None, + history: { + unstable => "/_matrix/client/v3/publicRooms", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(query)] + #[serde(skip_serializing_if = "Option::is_none")] + limit: Option, + + #[ruma_api(query)] + server: OwnedServerName, + + #[ruma_api(query)] + #[serde(skip_serializing_if = "String::is_empty")] + since: String, +} + +#[response(error = crate::Error)] +pub struct Response { + chunk: Vec, + + #[serde(skip_serializing_if = "Option::is_none")] + next_batch: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + prev_batch: Option, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct Room { + #[serde(skip_serializing_if = "Option::is_none")] + pub avatar_url: Option, + + #[serde(rename = "canonical_alias", skip_serializing_if = "Option::is_none")] + pub alias: Option, + + pub join_rule: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + + #[serde(rename = "num_joined_members")] + pub members: Option, + + pub room_id: OwnedRoomId, + + #[serde(skip_serializing_if = "Option::is_none")] + pub room_type: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub topic: Option, + + pub world_readable: bool, +} diff --git a/crates/matrix/src/client/room/join_room.rs b/crates/matrix/src/client/room/join_room.rs index e69de29..1fdadd4 100644 --- a/crates/matrix/src/client/room/join_room.rs +++ b/crates/matrix/src/client/room/join_room.rs @@ -0,0 +1,28 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedRoomOrAliasId, OwnedRoomId, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: true, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/v3/join/{alias_or_id}", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub alias_or_id: OwnedRoomOrAliasId, + + #[serde(skip_serializing_if = "String::is_empty")] + pub reason: String, +} + +#[response(error = crate::Error)] +pub struct Response { + pub room_id: OwnedRoomId, +} diff --git a/crates/matrix/src/client/room/kick_from_room.rs b/crates/matrix/src/client/room/kick_from_room.rs index e69de29..c3079b9 100644 --- a/crates/matrix/src/client/room/kick_from_room.rs +++ b/crates/matrix/src/client/room/kick_from_room.rs @@ -0,0 +1,28 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedUserId, OwnedRoomId, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: true, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/v3/rooms/{room_id}/kick", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub room_id: OwnedRoomId, + + pub user_id: OwnedUserId, + + #[serde(skip_serializing_if = "String::is_empty")] + pub reason: String, +} + +#[response(error = crate::Error)] +pub struct Response {} diff --git a/crates/matrix/src/client/room/leave_room.rs b/crates/matrix/src/client/room/leave_room.rs index e69de29..6d50593 100644 --- a/crates/matrix/src/client/room/leave_room.rs +++ b/crates/matrix/src/client/room/leave_room.rs @@ -0,0 +1,26 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedRoomId, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: true, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/v3/rooms/{room_id}/leave", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub room_id: OwnedRoomId, + + #[serde(skip_serializing_if = "String::is_empty")] + pub reason: String, +} + +#[response(error = crate::Error)] +pub struct Response {} diff --git a/crates/matrix/src/client/room/unban_from_room.rs b/crates/matrix/src/client/room/unban_from_room.rs index e69de29..a34ace7 100644 --- a/crates/matrix/src/client/room/unban_from_room.rs +++ b/crates/matrix/src/client/room/unban_from_room.rs @@ -0,0 +1,28 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedRoomId, OwnedUserId, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: true, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/v3/rooms/{room_id}/unban", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub room_id: OwnedRoomId, + + pub user_id: OwnedUserId, + + #[serde(skip_serializing_if = "String::is_empty")] + pub reason: String, +} + +#[response(error = crate::Error)] +pub struct Response {} From a4311723fb0a2888dc866e8dbad91158b689cd59 Mon Sep 17 00:00:00 2001 From: mikoto Date: Tue, 27 Feb 2024 09:06:07 +0000 Subject: [PATCH 06/33] docs --- crates/matrix/src/client/room.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/matrix/src/client/room.rs b/crates/matrix/src/client/room.rs index 2cf5ffe..ce0d312 100644 --- a/crates/matrix/src/client/room.rs +++ b/crates/matrix/src/client/room.rs @@ -1,3 +1,7 @@ +//! reference: https://spec.matrix.org/unstable/client-server-api/#rooms +//! +//! This module contains handlers to interact with rooms. + mod ban_from_room; mod create_room; mod forget_room; From cad450f422c080d04a0c13fa7e7d2d7298e1e494 Mon Sep 17 00:00:00 2001 From: mikoto Date: Tue, 27 Feb 2024 10:54:55 +0000 Subject: [PATCH 07/33] fix: client session API --- crates/matrix/src/client/session.rs | 49 +------- crates/matrix/src/client/session/get_flows.rs | 46 ++++++++ crates/matrix/src/client/session/login.rs | 108 ++++++++++++++++++ crates/matrix/src/client/session/register.rs | 108 ++++++++++++++++++ crates/matrix/src/client/session/whoami.rs | 24 ++++ 5 files changed, 289 insertions(+), 46 deletions(-) create mode 100644 crates/matrix/src/client/session/get_flows.rs create mode 100644 crates/matrix/src/client/session/login.rs create mode 100644 crates/matrix/src/client/session/register.rs create mode 100644 crates/matrix/src/client/session/whoami.rs diff --git a/crates/matrix/src/client/session.rs b/crates/matrix/src/client/session.rs index 1bad079..18b8249 100644 --- a/crates/matrix/src/client/session.rs +++ b/crates/matrix/src/client/session.rs @@ -1,46 +1,3 @@ -use anyhow::Result; -use ruma_common::OwnedUserId; -use serde::{Deserialize, Serialize}; -use tracing::instrument; - -use crate::error::MatrixError; - -#[derive(Debug, Serialize, Deserialize)] -pub struct Session { - pub device_id: String, - pub is_guest: bool, - pub user_id: OwnedUserId, -} - -impl Session { - /// Gets information about the owner of a given access token. - /// - /// Note that, as with the rest of the Client-Server API, Application - /// Services may masquerade as users within their namespace by giving a - /// user_id query parameter. In this situation, the server should verify - /// that the given user_id is registered by the appservice, and return it - /// in the response body. - /// - /// Refer: https://playground.matrix.org/#get-/_matrix/client/v3/account/whoami - #[instrument(skip(client, access_token))] - pub async fn get( - client: &crate::http::Client, - access_token: impl Into, - ) -> Result { - // Clones the client in order to temporally set a token for the `GET` - // request - let mut tmp = (*client).clone(); - - tmp.set_token(access_token)?; - - let resp = tmp.get("/_matrix/client/v3/account/whoami").await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } -} +mod login; +mod get_flows; +mod whoami; diff --git a/crates/matrix/src/client/session/get_flows.rs b/crates/matrix/src/client/session/get_flows.rs new file mode 100644 index 0000000..d3f6f7a --- /dev/null +++ b/crates/matrix/src/client/session/get_flows.rs @@ -0,0 +1,46 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, +}; +use serde::Deserialize; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: true, + authentication: None, + history: { + unstable => "/_matrix/client/v3/login", + } +}; + +#[request(error = crate::Error)] +pub struct Request {} + +#[response(error = crate::Error)] +pub struct Response { + body: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct LoginFlow { + #[serde(skip_serializing_if = "Option::is_none")] + pub get_login_token: Option, + + pub kind: LoginType, +} + +#[derive(Clone, Debug, Deserialize)] +pub enum LoginType { + #[serde(rename = "m.login.password")] + Password, + + #[serde(rename = "m.login.token")] + Token, + + #[serde(rename = "m.login.sso")] + Sso, + + #[serde(rename = "m.login.application_service")] + ApplicationService, +} diff --git a/crates/matrix/src/client/session/login.rs b/crates/matrix/src/client/session/login.rs new file mode 100644 index 0000000..889401b --- /dev/null +++ b/crates/matrix/src/client/session/login.rs @@ -0,0 +1,108 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedMxcUri, thirdparty::Medium, OwnedDeviceId, OwnedUserId, +}; +use serde::{Serialize, Deserialize}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: true, + authentication: None, + history: { + unstable => "/_matrix/client/v3/login", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[serde(flatten, rename = "type")] + pub kind: LoginType, + + #[serde(skip_serializing_if = "Option::is_none")] + pub identifier: Option, + + #[serde( + rename = "initial_device_display_name", + skip_serializing_if = "String::is_empty" + )] + pub device_name: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub refresh_token: Option, +} + +#[response(error = crate::Error)] +pub struct Response { + pub access_token: String, + + pub device_id: OwnedDeviceId, + + #[serde(skip_serializing_if = "Option::is_none")] + pub expires_in_ms: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub refresh_token: Option, + + pub user_id: OwnedUserId, + + #[serde(skip_serializing_if = "Option::is_none")] + pub well_known: Option, +} + +#[derive(Clone, Debug, Serialize)] +pub struct IdentityProvider { + pub id: String, + + #[serde(skip_serializing_if = "String::is_empty")] + pub name: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub icon: Option, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(tag = "type")] +pub enum LoginType { + #[serde(rename = "m.login.password")] + Password { password: String }, + + #[serde(rename = "m.login.token")] + Token { token: String }, + + #[serde(rename = "m.login.sso")] + Sso { + #[serde(skip_serializing_if = "<[_]>::is_empty")] + identity_providers: Vec, + }, + + #[serde(rename = "m.login.application_service")] + ApplicationService, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(tag = "type")] +pub enum Identifier { + #[serde(rename = "m.id.user")] + User { user: String }, + + #[serde(rename = "m.id.thirdparty")] + ThirdParty { medium: Medium, address: String }, + + #[serde(rename = "m.id.phone")] + Phone { country: String, phone: String }, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct BaseUrl { + pub base_url: url::Url, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct WellKnown { + #[serde(rename = "m.homeserver")] + pub homeserver: BaseUrl, + + #[serde(rename = "m.identity_server")] + pub identity_server: BaseUrl, +} diff --git a/crates/matrix/src/client/session/register.rs b/crates/matrix/src/client/session/register.rs new file mode 100644 index 0000000..e662413 --- /dev/null +++ b/crates/matrix/src/client/session/register.rs @@ -0,0 +1,108 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedMxcUri, thirdparty::Medium, OwnedDeviceId, OwnedUserId, +}; +use serde::{Serialize, Deserialize}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: true, + authentication: None, + history: { + unstable => "", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[serde(flatten, rename = "type")] + pub kind: LoginType, + + #[serde(skip_serializing_if = "Option::is_none")] + pub identifier: Option, + + #[serde( + rename = "initial_device_display_name", + skip_serializing_if = "String::is_empty" + )] + pub device_name: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub refresh_token: Option, +} + +#[response(error = crate::Error)] +pub struct Response { + pub access_token: String, + + pub device_id: OwnedDeviceId, + + #[serde(skip_serializing_if = "Option::is_none")] + pub expires_in_ms: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub refresh_token: Option, + + pub user_id: OwnedUserId, + + #[serde(skip_serializing_if = "Option::is_none")] + pub well_known: Option, +} + +#[derive(Clone, Debug, Serialize)] +pub struct IdentityProvider { + pub id: String, + + #[serde(skip_serializing_if = "String::is_empty")] + pub name: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub icon: Option, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(tag = "type")] +pub enum LoginType { + #[serde(rename = "m.login.password")] + Password { password: String }, + + #[serde(rename = "m.login.token")] + Token { token: String }, + + #[serde(rename = "m.login.sso")] + Sso { + #[serde(skip_serializing_if = "<[_]>::is_empty")] + identity_providers: Vec, + }, + + #[serde(rename = "m.login.application_service")] + ApplicationService, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(tag = "type")] +pub enum Identifier { + #[serde(rename = "m.id.user")] + User { user: String }, + + #[serde(rename = "m.id.thirdparty")] + ThirdParty { medium: Medium, address: String }, + + #[serde(rename = "m.id.phone")] + Phone { country: String, phone: String }, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct BaseUrl { + pub base_url: url::Url, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct WellKnown { + #[serde(rename = "m.homeserver")] + pub homeserver: BaseUrl, + + #[serde(rename = "m.identity_server")] + pub identity_server: BaseUrl, +} diff --git a/crates/matrix/src/client/session/whoami.rs b/crates/matrix/src/client/session/whoami.rs new file mode 100644 index 0000000..5913bfb --- /dev/null +++ b/crates/matrix/src/client/session/whoami.rs @@ -0,0 +1,24 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedDeviceId, OwnedUserId, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: true, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/v3/account/whoami", + } +}; + +#[request(error = crate::Error)] +pub struct Request {} + +#[response(error = crate::Error)] +pub struct Response { + pub device_id: OwnedDeviceId, + + pub user_id: OwnedUserId, +} From cdc6c148e78a67c773cff0c98dc74fc9345c144f Mon Sep 17 00:00:00 2001 From: mikoto Date: Tue, 27 Feb 2024 14:19:26 +0000 Subject: [PATCH 08/33] Matrix API finished --- crates/matrix/Cargo.toml | 10 +- crates/matrix/src/admin/mod.rs | 8 +- crates/matrix/src/admin/room.rs | 4 +- crates/matrix/src/admin/session.rs | 45 +++ crates/matrix/src/admin/session/get_nonce.rs | 22 ++ crates/matrix/src/admin/session/register.rs | 43 +++ crates/matrix/src/admin/user.rs | 4 +- crates/matrix/src/client/error.rs | 9 - crates/matrix/src/client/events.rs | 310 ------------------ crates/matrix/src/client/login.rs | 93 ------ crates/matrix/src/client/mod.rs | 8 +- crates/matrix/src/client/mxc.rs | 184 ----------- crates/matrix/src/client/room.rs | 4 +- crates/matrix/src/client/session.rs | 8 +- crates/matrix/src/client/session/get_flows.rs | 2 +- crates/matrix/src/client/session/register.rs | 71 +--- .../src/client/session/username_available.rs | 24 ++ crates/matrix/src/lib.rs | 8 + 18 files changed, 177 insertions(+), 680 deletions(-) create mode 100644 crates/matrix/src/admin/session.rs create mode 100644 crates/matrix/src/admin/session/get_nonce.rs create mode 100644 crates/matrix/src/admin/session/register.rs delete mode 100644 crates/matrix/src/client/error.rs delete mode 100644 crates/matrix/src/client/events.rs delete mode 100644 crates/matrix/src/client/login.rs delete mode 100644 crates/matrix/src/client/mxc.rs create mode 100644 crates/matrix/src/client/session/username_available.rs diff --git a/crates/matrix/Cargo.toml b/crates/matrix/Cargo.toml index fa669aa..b712171 100644 --- a/crates/matrix/Cargo.toml +++ b/crates/matrix/Cargo.toml @@ -10,18 +10,22 @@ path = "src/lib.rs" [dependencies] ruma-events = { version = "0.27.11", features = ["html", "markdown"] } -ruma-common = { version = "0.12.0", default_features = false, features = ["api", "rand"] } +ruma-common = { version = "0.12.0", default_features = false, features = [ + "api", + "rand", +] } ruma-macros = "0.12.0" # Workspace Dependencies -anyhow = { workspace = true } -chrono = { workspace = true, features = ["serde"] } mime = { workspace = true } reqwest = { workspace = true, features = ["json"] } serde = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } +sha1 = { workspace = true } url = { workspace = true, features = ["serde"] } +hex = { workspace = true } +hmac = { workspace = true } [features] client = [] diff --git a/crates/matrix/src/admin/mod.rs b/crates/matrix/src/admin/mod.rs index 2a77d65..dcb275b 100644 --- a/crates/matrix/src/admin/mod.rs +++ b/crates/matrix/src/admin/mod.rs @@ -1,3 +1,7 @@ -mod user; +//! This module is the root of the admin API. +//! +//! reference: https://matrix-org.github.io/synapse/latest/usage/administration/admin_api/index.html + mod room; -// mod room; +mod session; +mod user; diff --git a/crates/matrix/src/admin/room.rs b/crates/matrix/src/admin/room.rs index 17117b9..39b3a22 100644 --- a/crates/matrix/src/admin/room.rs +++ b/crates/matrix/src/admin/room.rs @@ -1,6 +1,6 @@ -//! reference: https://matrix-org.github.io/synapse/latest/admin_api/rooms.html -//! //! This module contains handlers for managing rooms. +//! +//! reference: https://matrix-org.github.io/synapse/latest/admin_api/rooms.html use ruma_common::{ room::RoomType, EventEncryptionAlgorithm, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, diff --git a/crates/matrix/src/admin/session.rs b/crates/matrix/src/admin/session.rs new file mode 100644 index 0000000..88cabf2 --- /dev/null +++ b/crates/matrix/src/admin/session.rs @@ -0,0 +1,45 @@ +//! This module contains handlers for user registration. +//! +//! reference: https://matrix-org.github.io/synapse/latest/admin_api/register_api.html + +use hmac::Mac; + +mod get_nonce; +mod register; + +#[derive(Clone, Debug)] +pub struct Hmac { + inner: Vec, +} + +impl Hmac { + fn new( + shared_secret: &str, + nonce: &str, + username: &str, + password: &str, + admin: bool, + ) -> Result { + let mut mac = hmac::Hmac::::new_from_slice(shared_secret.as_bytes())?; + let admin = match admin { + true => "admin", + false => "notadmin", + }; + + mac.update( + &[nonce, username, password, admin] + .map(str::as_bytes) + .join(&0x00), + ); + + let result = mac.finalize().into_bytes(); + + Ok(Self { + inner: result.to_vec(), + }) + } + + fn get(&self) -> String { + hex::encode(&self.inner) + } +} diff --git a/crates/matrix/src/admin/session/get_nonce.rs b/crates/matrix/src/admin/session/get_nonce.rs new file mode 100644 index 0000000..d5f5c00 --- /dev/null +++ b/crates/matrix/src/admin/session/get_nonce.rs @@ -0,0 +1,22 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_synapse/admin/v1/register", + } +}; + +#[request(error = crate::Error)] +pub struct Request {} + +#[response(error = crate::Error)] +pub struct Response { + nonce: String, +} diff --git a/crates/matrix/src/admin/session/register.rs b/crates/matrix/src/admin/session/register.rs new file mode 100644 index 0000000..c71d8f0 --- /dev/null +++ b/crates/matrix/src/admin/session/register.rs @@ -0,0 +1,43 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedDeviceId, OwnedServerName, OwnedUserId, +}; + +use super::Hmac; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_synapse/admin/v1/register", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + pub nonce: String, + + pub username: String, + + pub password: String, + + #[serde(skip_deserializing_if = "String::is_empty")] + pub displayname: String, + + pub admin: bool, + + pub hmac: Hmac, +} + +#[response(error = crate::Error)] +pub struct Response { + pub access_token: String, + + pub user_id: OwnedUserId, + + pub home_server: OwnedServerName, + + pub device_id: OwnedDeviceId, +} diff --git a/crates/matrix/src/admin/user.rs b/crates/matrix/src/admin/user.rs index 2e39665..3417bb5 100644 --- a/crates/matrix/src/admin/user.rs +++ b/crates/matrix/src/admin/user.rs @@ -1,6 +1,6 @@ -//! reference: https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html +//! This module contains handlers for managing users. //! -//! This module contains handlers for managing user accounts. +//! reference: https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html use ruma_common::{thirdparty::ThirdPartyIdentifier, OwnedMxcUri, OwnedUserId}; use serde::{Deserialize, Serialize}; diff --git a/crates/matrix/src/client/error.rs b/crates/matrix/src/client/error.rs deleted file mode 100644 index 872754b..0000000 --- a/crates/matrix/src/client/error.rs +++ /dev/null @@ -1,9 +0,0 @@ -use serde::Deserialize; - -#[derive(Clone, Debug, Deserialize)] -pub struct MatrixError { - #[serde(rename = "errcode")] - pub error_code: String, - #[serde(rename = "error")] - pub error: String, -} diff --git a/crates/matrix/src/client/events.rs b/crates/matrix/src/client/events.rs deleted file mode 100644 index 2953b2b..0000000 --- a/crates/matrix/src/client/events.rs +++ /dev/null @@ -1,310 +0,0 @@ -use anyhow::Result; -use ruma_common::{serde::Raw, EventId, OwnedEventId, OwnedTransactionId, RoomId}; - -use ruma_events::{ - relation::RelationType, AnyMessageLikeEvent, AnyStateEvent, AnyStateEventContent, - AnyTimelineEvent, MessageLikeEventContent, MessageLikeEventType, StateEventContent, - StateEventType, -}; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use tracing::instrument; - -use crate::{admin::resources::room::Direction, error::MatrixError, Client}; - -pub struct EventsService; - -#[derive(Debug, Default, Clone, Serialize)] -pub struct GetMessagesQuery { - #[serde(skip_serializing_if = "Option::is_none")] - pub from: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub to: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub limit: Option, - - pub dir: Direction, - - #[serde(skip_serializing_if = "String::is_empty")] - pub filter: String, -} - -#[derive(Debug, Default, Clone, Serialize)] -pub struct GetRelationsQuery { - #[serde(skip_serializing_if = "Option::is_none")] - pub from: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub to: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub limit: Option, - - pub dir: Direction, -} - -#[derive(Debug, Deserialize)] -pub struct GetMessagesResponse { - pub chunk: Vec>, - pub start: String, - pub end: String, - pub state: Option>>, -} - -#[derive(Debug, Deserialize)] -#[serde(transparent)] -pub struct GetStateResponse(pub Vec>); - -#[derive(Debug, Deserialize)] -pub struct GetRelationsResponse { - pub chunk: Vec>, - pub prev_batch: Option, - pub next_batch: Option, -} - -#[derive(Debug, Default, Serialize)] -pub struct SendRedactionBody { - #[serde(skip_serializing_if = "String::is_empty")] - pub reason: String, -} - -#[derive(Debug, Deserialize)] -pub struct SendMessageResponse { - pub event_id: OwnedEventId, -} - -#[derive(Debug, Deserialize)] -pub struct SendStateResponse { - pub event_id: OwnedEventId, -} - -#[derive(Debug, Deserialize)] -pub struct SendRedactionResponse { - pub event_id: OwnedEventId, -} - -impl EventsService { - #[instrument(skip(client, access_token))] - pub async fn get_event( - client: &Client, - access_token: impl Into, - room_id: &RoomId, - event_id: &EventId, - ) -> Result> { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let resp = tmp - .get(format!( - "/_matrix/client/v3/rooms/{room_id}/event/{event_id}", - room_id = room_id, - event_id = event_id, - )) - .await?; - - Ok(resp.json().await?) - } - - #[instrument(skip(client, access_token))] - pub async fn get_messages( - client: &Client, - access_token: impl Into, - room_id: &RoomId, - query: GetMessagesQuery, - ) -> Result { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let resp = tmp - .get_query( - format!( - "/_matrix/client/v3/rooms/{room_id}/messages", - room_id = room_id, - ), - &query, - ) - .await?; - - Ok(resp.json().await?) - } - - #[instrument(skip(client, access_token))] - pub async fn get_state( - client: &Client, - access_token: impl Into, - room_id: &RoomId, - ) -> Result { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let resp = tmp - .get(format!( - "/_matrix/client/v3/rooms/{room_id}/state", - room_id = room_id, - )) - .await?; - - Ok(resp.json().await?) - } - - #[instrument(skip(client, access_token))] - pub async fn get_state_content( - client: &Client, - access_token: impl Into, - room_id: &RoomId, - event_type: StateEventType, - state_key: Option, - ) -> Result> { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let mut path = format!( - "/_matrix/client/v3/rooms/{room_id}/state/{event_type}", - room_id = room_id, - event_type = event_type, - ); - - if let Some(state_key) = state_key { - path.push_str(&format!("/{state_key}", state_key = state_key)) - } - - let resp = tmp.get(path).await?; - - Ok(resp.json().await?) - } - - #[instrument(skip(client, access_token))] - pub async fn get_relations( - client: &Client, - access_token: impl Into, - room_id: &RoomId, - event_id: &EventId, - rel_type: Option>, - event_type: Option, - query: GetRelationsQuery, - ) -> Result { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let mut path = format!( - "/_matrix/client/v3/rooms/{room_id}/relations/{event_id}", - room_id = room_id, - event_id = event_id, - ); - - if let Some(rel_type) = rel_type { - path.push_str(&format!( - "/{rel_type}", - rel_type = rel_type - .as_ref() - .map_or("m.in_reply_to".into(), ToString::to_string) - )); - - if let Some(event_type) = event_type { - path.push_str(&format!("/{event_type}", event_type = event_type)) - } - } - - let resp = tmp.get_query(path, &query).await?; - - Ok(resp.json().await?) - } - - #[instrument(skip(client, access_token, body))] - pub async fn send_message( - client: &Client, - access_token: impl Into, - room_id: &RoomId, - txn_id: OwnedTransactionId, - body: T, - ) -> Result { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let resp = tmp - .put_json( - format!( - "/_matrix/client/v3/rooms/{room_id}/send/{event_type}/{txn_id}", - room_id = room_id, - event_type = body.event_type(), - txn_id = txn_id, - ), - &body, - ) - .await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } - - #[instrument(skip(client, access_token, body))] - pub async fn send_state( - client: &Client, - access_token: impl Into, - room_id: &RoomId, - state_key: Option, - body: T, - ) -> Result { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let mut path = format!( - "/_matrix/client/v3/rooms/{room_id}/state/{event_type}", - room_id = room_id, - event_type = body.event_type(), - ); - - if let Some(state_key) = state_key { - path.push_str(&format!("/{state_key}", state_key = state_key)) - } - - let resp = tmp.put_json(path, &body).await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } - - #[instrument(skip(client, access_token, body))] - pub async fn send_redaction( - client: &Client, - access_token: impl Into, - room_id: &RoomId, - event_id: &EventId, - txn_id: OwnedTransactionId, - body: SendRedactionBody, - ) -> Result { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let resp = tmp - .put_json( - format!( - "/_matrix/client/v3/rooms/{room_id}/redact/{event_id}/{txn_id}", - room_id = room_id, - event_id = event_id, - txn_id = txn_id, - ), - &body, - ) - .await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } -} diff --git a/crates/matrix/src/client/login.rs b/crates/matrix/src/client/login.rs deleted file mode 100644 index 6a65fba..0000000 --- a/crates/matrix/src/client/login.rs +++ /dev/null @@ -1,93 +0,0 @@ -use anyhow::Result; -use serde::{Deserialize, Serialize}; -use tracing::instrument; - -use crate::{error::MatrixError, http::Client}; - -#[derive(Debug, Deserialize)] -pub struct LoginCredentials { - pub access_token: String, -} - -#[derive(Debug, Serialize)] -pub struct LoginCredentialsPayload { - pub r#type: &'static str, - pub user: String, - pub password: String, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct LoginFlow { - pub get_login_token: Option, - #[serde(rename = "type")] - pub kind: String, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(transparent)] -pub struct LoginFlows { - pub flows: Vec, -} - -pub struct Login; - -impl Login { - /// Retrieves an access token by logging in with Username and Password - /// - /// This is equivalent to executing: - /// - /// ```ignore - /// curl -sS -d '{"type":"m.login.password", "user":"X", "password":"Y"}' http://server:port/_matrix/client/v3/login - /// ``` - #[instrument(skip(client, username, password))] - pub async fn login_credentials( - client: &Client, - username: impl AsRef, - password: impl AsRef, - ) -> Result { - let resp = client - .post_json( - "/_matrix/client/v3/login", - &LoginCredentialsPayload { - r#type: "m.login.password", - user: username.as_ref().to_string(), - password: password.as_ref().to_string(), - }, - ) - .await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } - - #[instrument(skip(client))] - pub async fn get_login_flows(client: &Client) -> Result { - let resp = client.get("/_matrix/client/v3/login").await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } - - #[instrument(skip(client))] - pub async fn redirect_sso(client: &Client) -> Result { - let resp = client.get("/_matrix/client/v3/login").await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } -} diff --git a/crates/matrix/src/client/mod.rs b/crates/matrix/src/client/mod.rs index 43420e9..196920e 100644 --- a/crates/matrix/src/client/mod.rs +++ b/crates/matrix/src/client/mod.rs @@ -1,6 +1,6 @@ -pub mod error; -pub mod events; -pub mod login; -pub mod mxc; +//! This module is the root of the client-server API. +//! +//! reference: https://spec.matrix.org/unstable/client-server-api + pub mod room; pub mod session; diff --git a/crates/matrix/src/client/mxc.rs b/crates/matrix/src/client/mxc.rs deleted file mode 100644 index 7dc669a..0000000 --- a/crates/matrix/src/client/mxc.rs +++ /dev/null @@ -1,184 +0,0 @@ -use std::str::FromStr; - -use anyhow::Result; -use mime::Mime; -use ruma_common::{MxcUri, OwnedMxcUri}; -use serde::{de, Deserialize, Deserializer, Serialize}; -use tracing::instrument; - -use chrono::{serde::ts_microseconds_option, DateTime, Utc}; - -use crate::error::MatrixError; - -fn parse_mime_opt<'de, D>(d: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - Option::<&str>::deserialize(d)? - .map(::from_str) - .transpose() - .map_err(de::Error::custom) -} - -#[derive(Debug, Serialize)] -pub struct GetPreviewUrlQuery { - pub url: url::Url, - pub ts: DateTime, -} - -#[derive(Debug, Deserialize)] -pub struct CreateMxcUriResponse { - pub content_uri: String, - - #[serde(with = "ts_microseconds_option")] - pub unused_expires_at: Option>, -} - -#[derive(Debug, Deserialize)] -pub struct GetPreviewUrlResponse { - #[serde(rename = "matrix:image_size")] - pub image_size: Option, - - #[serde(rename = "og:description")] - pub description: Option, - - #[serde(rename = "og:image")] - pub image: Option, - - #[serde(rename = "og:image:height")] - pub height: Option, - - #[serde(rename = "og:image:width")] - pub width: Option, - - #[serde(rename = "og:image:type", deserialize_with = "parse_mime_opt")] - pub kind: Option, - - #[serde(rename = "og:title")] - pub title: Option, -} - -#[derive(Debug, Deserialize)] -pub struct GetConfigResponse { - #[serde(rename = "m.upload.size")] - pub upload_size: Option, -} - -#[derive(Debug, Serialize)] -pub enum ResizeMethod { - Crop, - Scale, -} - -pub struct MxcService; - -#[derive(Debug, Deserialize)] -pub struct MxcError { - #[serde(flatten)] - pub inner: MatrixError, - - pub retry_after_ms: u64, -} - -impl MxcService { - /// Creates a new `MxcUri`, independently of the content being uploaded - /// - /// Refer: https://spec.matrix.org/v1.9/client-server-api/#post_matrixmediav1create - #[instrument(skip(client, access_token))] - pub async fn create( - client: &crate::http::Client, - access_token: impl Into, - ) -> Result { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let resp = tmp.post("/_matrix/media/v1/create").await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.inner.error)) - } - - /// Retrieve the configuration of the content repository - /// - /// Refer: https://spec.matrix.org/v1.9/client-server-api/#get_matrixmediav3config - #[instrument(skip(client, access_token))] - pub async fn get_config( - client: &crate::http::Client, - access_token: impl Into, - ) -> Result { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let resp = tmp.get("/_matrix/media/v3/config").await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.inner.error)) - } - - /// Retrieve a URL to download content from the content repository, - /// optionally replacing the name of the file. - /// - /// Refer: https://spec.matrix.org/v1.9/client-server-api/#get_matrixmediav3downloadservernamemediaid - #[instrument(skip(client, access_token))] - pub async fn get_download_url( - client: &crate::http::Client, - access_token: impl Into, - mxc_uri: &MxcUri, - mut base_url: url::Url, - file_name: Option, - ) -> Result { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let (server_name, media_id) = mxc_uri.parts().unwrap(); - - let mut path = format!( - "/_matrix/media/v3/download/{server_name}/{media_id}", - server_name = server_name, - media_id = media_id, - ); - - if let Some(file_name) = file_name { - path.push_str(&format!("/{file_name}", file_name = file_name)) - } - - base_url.set_path(&path); - - Ok(base_url) - } - - /// - /// - /// Refer: https://spec.matrix.org/v1.9/client-server-api/#get_matrixmediav3preview_url - #[instrument(skip(client, access_token))] - pub async fn get_preview( - client: &crate::http::Client, - access_token: impl Into, - query: GetPreviewUrlQuery, - ) -> Result { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let resp = tmp - .get_query("/_matrix/media/v3/preview_url".to_string(), &query) - .await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.inner.error)) - } -} diff --git a/crates/matrix/src/client/room.rs b/crates/matrix/src/client/room.rs index ce0d312..27a797f 100644 --- a/crates/matrix/src/client/room.rs +++ b/crates/matrix/src/client/room.rs @@ -1,6 +1,6 @@ -//! reference: https://spec.matrix.org/unstable/client-server-api/#rooms -//! //! This module contains handlers to interact with rooms. +//! +//! reference: https://spec.matrix.org/unstable/client-server-api/#rooms mod ban_from_room; mod create_room; diff --git a/crates/matrix/src/client/session.rs b/crates/matrix/src/client/session.rs index 18b8249..18328e9 100644 --- a/crates/matrix/src/client/session.rs +++ b/crates/matrix/src/client/session.rs @@ -1,3 +1,9 @@ -mod login; +//! This module contains handlers for user sessions. +//! +//! reference: https://spec.matrix.org/unstable/client-server-api/#client-authentication + mod get_flows; +mod login; +mod register; +mod username_available; mod whoami; diff --git a/crates/matrix/src/client/session/get_flows.rs b/crates/matrix/src/client/session/get_flows.rs index d3f6f7a..14e9a29 100644 --- a/crates/matrix/src/client/session/get_flows.rs +++ b/crates/matrix/src/client/session/get_flows.rs @@ -19,7 +19,7 @@ pub struct Request {} #[response(error = crate::Error)] pub struct Response { - body: Vec, + pub body: Vec, } #[derive(Clone, Debug, Deserialize)] diff --git a/crates/matrix/src/client/session/register.rs b/crates/matrix/src/client/session/register.rs index e662413..505e8e9 100644 --- a/crates/matrix/src/client/session/register.rs +++ b/crates/matrix/src/client/session/register.rs @@ -1,8 +1,7 @@ use ruma_common::{ api::{request, response, Metadata}, - metadata, OwnedMxcUri, thirdparty::Medium, OwnedDeviceId, OwnedUserId, + metadata, OwnedDeviceId, OwnedUserId, }; -use serde::{Serialize, Deserialize}; #[allow(dead_code)] const METADATA: Metadata = metadata! { @@ -10,17 +9,15 @@ const METADATA: Metadata = metadata! { rate_limited: true, authentication: None, history: { - unstable => "", + unstable => "/_matrix/client/v3/register", } }; #[request(error = crate::Error)] pub struct Request { - #[serde(flatten, rename = "type")] - pub kind: LoginType, + pub username: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub identifier: Option, + pub password: String, #[serde( rename = "initial_device_display_name", @@ -45,64 +42,4 @@ pub struct Response { pub refresh_token: Option, pub user_id: OwnedUserId, - - #[serde(skip_serializing_if = "Option::is_none")] - pub well_known: Option, -} - -#[derive(Clone, Debug, Serialize)] -pub struct IdentityProvider { - pub id: String, - - #[serde(skip_serializing_if = "String::is_empty")] - pub name: String, - - #[serde(skip_serializing_if = "Option::is_none")] - pub icon: Option, -} - -#[derive(Clone, Debug, Serialize)] -#[serde(tag = "type")] -pub enum LoginType { - #[serde(rename = "m.login.password")] - Password { password: String }, - - #[serde(rename = "m.login.token")] - Token { token: String }, - - #[serde(rename = "m.login.sso")] - Sso { - #[serde(skip_serializing_if = "<[_]>::is_empty")] - identity_providers: Vec, - }, - - #[serde(rename = "m.login.application_service")] - ApplicationService, -} - -#[derive(Clone, Debug, Serialize)] -#[serde(tag = "type")] -pub enum Identifier { - #[serde(rename = "m.id.user")] - User { user: String }, - - #[serde(rename = "m.id.thirdparty")] - ThirdParty { medium: Medium, address: String }, - - #[serde(rename = "m.id.phone")] - Phone { country: String, phone: String }, -} - -#[derive(Clone, Debug, Deserialize)] -pub struct BaseUrl { - pub base_url: url::Url, -} - -#[derive(Clone, Debug, Deserialize)] -pub struct WellKnown { - #[serde(rename = "m.homeserver")] - pub homeserver: BaseUrl, - - #[serde(rename = "m.identity_server")] - pub identity_server: BaseUrl, } diff --git a/crates/matrix/src/client/session/username_available.rs b/crates/matrix/src/client/session/username_available.rs new file mode 100644 index 0000000..e380f6f --- /dev/null +++ b/crates/matrix/src/client/session/username_available.rs @@ -0,0 +1,24 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: true, + authentication: None, + history: { + unstable => "/_matrix/client/v3/register/available", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + pub username: String, +} + +#[response(error = crate::Error)] +pub struct Response { + pub available: bool, +} diff --git a/crates/matrix/src/lib.rs b/crates/matrix/src/lib.rs index 2b2848e..ace15fb 100644 --- a/crates/matrix/src/lib.rs +++ b/crates/matrix/src/lib.rs @@ -1,3 +1,11 @@ +//! This library deals with forwarding Matrix requests to the server. +//! Comments have been used sparingly as the specification contains all the technical details. + +//! We rely on `ruma` to abstract away the boilerplate introduced by HTTP requests, +//! without sacrificing flexibility by defining our own request and response types. +//! +//! reference: https://docs.ruma.io/ruma_common/api/index.html + pub mod admin; pub mod client; From 8979119e5bf933529f5eab15ecbfae4f485612d7 Mon Sep 17 00:00:00 2001 From: mikoto Date: Wed, 28 Feb 2024 17:25:55 +0000 Subject: [PATCH 09/33] feat: handler addition, one to rule them all --- crates/core/src/lib.rs | 14 +- crates/core/src/{account => session}/error.rs | 0 crates/core/src/{account => session}/mod.rs | 0 crates/core/src/{account => session}/model.rs | 0 .../core/src/{account => session}/service.rs | 0 crates/matrix/Cargo.toml | 11 +- crates/matrix/src/admin/mod.rs | 6 +- crates/matrix/src/admin/room.rs | 6 +- crates/matrix/src/admin/room/delete_room.rs | 4 +- crates/matrix/src/admin/room/get_rooms.rs | 12 +- crates/matrix/src/admin/session.rs | 8 +- crates/matrix/src/admin/session/get_nonce.rs | 2 +- crates/matrix/src/client/room/create_room.rs | 2 +- crates/matrix/src/lib.rs | 56 ++++++ crates/matrix/src/token/mod.rs | 1 - crates/matrix/src/token/shared_secret.rs | 173 ------------------ 16 files changed, 90 insertions(+), 205 deletions(-) rename crates/core/src/{account => session}/error.rs (100%) rename crates/core/src/{account => session}/mod.rs (100%) rename crates/core/src/{account => session}/model.rs (100%) rename crates/core/src/{account => session}/service.rs (100%) delete mode 100644 crates/matrix/src/token/mod.rs delete mode 100644 crates/matrix/src/token/shared_secret.rs diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index afc2a0a..f0f9183 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -1,4 +1,7 @@ -pub mod account; +//! This library deals with our core logic, such as authorizing user interactions, +//! forwarding regular events and constructing custom requests. + +pub mod session; pub mod auth; pub mod error; pub mod mail; @@ -9,14 +12,11 @@ pub use error::{Error, HttpStatusCode, Result}; use mail::service::MailService; use room::service::RoomService; +use tokio::sync::mpsc::Receiver; use url::Url; use std::{fmt::Debug, str::FromStr, sync::Arc}; -use matrix::Client as MatrixAdminClient; - -use self::{account::service::AccountService, auth::service::AuthService}; - pub mod env { pub const COMMUNE_SYNAPSE_HOST: &str = "COMMUNE_SYNAPSE_HOST"; pub const COMMUNE_SYNAPSE_ADMIN_TOKEN: &str = "COMMUNE_SYNAPSE_ADMIN_TOKEN"; @@ -92,9 +92,7 @@ impl CommuneConfig { } pub struct Commune { - pub account: Arc, - pub auth: Arc, - pub room: Arc, + } impl Commune { diff --git a/crates/core/src/account/error.rs b/crates/core/src/session/error.rs similarity index 100% rename from crates/core/src/account/error.rs rename to crates/core/src/session/error.rs diff --git a/crates/core/src/account/mod.rs b/crates/core/src/session/mod.rs similarity index 100% rename from crates/core/src/account/mod.rs rename to crates/core/src/session/mod.rs diff --git a/crates/core/src/account/model.rs b/crates/core/src/session/model.rs similarity index 100% rename from crates/core/src/account/model.rs rename to crates/core/src/session/model.rs diff --git a/crates/core/src/account/service.rs b/crates/core/src/session/service.rs similarity index 100% rename from crates/core/src/account/service.rs rename to crates/core/src/session/service.rs diff --git a/crates/matrix/Cargo.toml b/crates/matrix/Cargo.toml index b712171..6de728e 100644 --- a/crates/matrix/Cargo.toml +++ b/crates/matrix/Cargo.toml @@ -9,12 +9,16 @@ name = "matrix" path = "src/lib.rs" [dependencies] -ruma-events = { version = "0.27.11", features = ["html", "markdown"] } +ruma-events = { version = "0.27.11", default_features = false, features = [ + "html", + "markdown", +] } ruma-common = { version = "0.12.0", default_features = false, features = [ "api", "rand", ] } -ruma-macros = "0.12.0" +ruma-macros = { version = "0.12.0", default_features = false } +ruma-client = { version = "0.12.0", default_features = false } # Workspace Dependencies mime = { workspace = true } @@ -26,6 +30,9 @@ sha1 = { workspace = true } url = { workspace = true, features = ["serde"] } hex = { workspace = true } hmac = { workspace = true } +http.workspace = true +bytes = "1.5.0" +async-trait = "0.1.77" [features] client = [] diff --git a/crates/matrix/src/admin/mod.rs b/crates/matrix/src/admin/mod.rs index dcb275b..75a9d6d 100644 --- a/crates/matrix/src/admin/mod.rs +++ b/crates/matrix/src/admin/mod.rs @@ -2,6 +2,6 @@ //! //! reference: https://matrix-org.github.io/synapse/latest/usage/administration/admin_api/index.html -mod room; -mod session; -mod user; +pub mod room; +pub mod session; +pub mod user; diff --git a/crates/matrix/src/admin/room.rs b/crates/matrix/src/admin/room.rs index 39b3a22..38a3738 100644 --- a/crates/matrix/src/admin/room.rs +++ b/crates/matrix/src/admin/room.rs @@ -9,11 +9,11 @@ use ruma_common::{ use ruma_events::room::{history_visibility::HistoryVisibility, join_rules::JoinRule}; use serde::Deserialize; -mod delete_room; -mod get_members; +pub mod delete_room; +pub mod get_members; pub mod get_room; pub mod get_rooms; -mod get_state; +pub mod get_state; #[derive(Clone, Debug, Deserialize)] pub struct Room { diff --git a/crates/matrix/src/admin/room/delete_room.rs b/crates/matrix/src/admin/room/delete_room.rs index 2774f1e..578a334 100644 --- a/crates/matrix/src/admin/room/delete_room.rs +++ b/crates/matrix/src/admin/room/delete_room.rs @@ -22,19 +22,17 @@ pub struct Request { #[serde(flatten, skip_serializing_if = "Option::is_none")] pub new_room: Option, - #[serde(skip_serializing_if = "ruma_common::serde::is_default")] pub block: bool, #[serde(skip_serializing_if = "ruma_common::serde::is_true")] pub purge: bool, - #[serde(skip_serializing_if = "ruma_common::serde::is_default")] pub force_purge: bool, } #[response(error = crate::Error)] pub struct Response { - delete_id: String, + pub delete_id: String, } #[derive(Clone, Debug, Serialize)] diff --git a/crates/matrix/src/admin/room/get_rooms.rs b/crates/matrix/src/admin/room/get_rooms.rs index 5607c2b..08a792c 100644 --- a/crates/matrix/src/admin/room/get_rooms.rs +++ b/crates/matrix/src/admin/room/get_rooms.rs @@ -1,5 +1,5 @@ use ruma_common::{ - api::{request, response, Metadata, Direction}, + api::{request, response, Direction, Metadata}, metadata, }; use serde::Serialize; @@ -39,16 +39,16 @@ pub struct Request { #[response(error = crate::Error)] pub struct Response { - rooms: Vec, + pub rooms: Vec, - offset: u64, + pub offset: u64, #[serde(rename = "total_rooms")] - total: u64, + pub total: u64, - next_batch: Option, + pub next_batch: Option, - prev_batch: Option, + pub prev_batch: Option, } #[derive(Clone, Default, Debug, Serialize)] diff --git a/crates/matrix/src/admin/session.rs b/crates/matrix/src/admin/session.rs index 88cabf2..84ee0f7 100644 --- a/crates/matrix/src/admin/session.rs +++ b/crates/matrix/src/admin/session.rs @@ -4,8 +4,8 @@ use hmac::Mac; -mod get_nonce; -mod register; +pub mod get_nonce; +pub mod register; #[derive(Clone, Debug)] pub struct Hmac { @@ -13,7 +13,7 @@ pub struct Hmac { } impl Hmac { - fn new( + pub fn new( shared_secret: &str, nonce: &str, username: &str, @@ -39,7 +39,7 @@ impl Hmac { }) } - fn get(&self) -> String { + pub fn get(&self) -> String { hex::encode(&self.inner) } } diff --git a/crates/matrix/src/admin/session/get_nonce.rs b/crates/matrix/src/admin/session/get_nonce.rs index d5f5c00..0388987 100644 --- a/crates/matrix/src/admin/session/get_nonce.rs +++ b/crates/matrix/src/admin/session/get_nonce.rs @@ -18,5 +18,5 @@ pub struct Request {} #[response(error = crate::Error)] pub struct Response { - nonce: String, + pub nonce: String, } diff --git a/crates/matrix/src/client/room/create_room.rs b/crates/matrix/src/client/room/create_room.rs index f27009a..140bfed 100644 --- a/crates/matrix/src/client/room/create_room.rs +++ b/crates/matrix/src/client/room/create_room.rs @@ -11,7 +11,7 @@ const METADATA: Metadata = metadata! { rate_limited: true, authentication: AccessToken, history: { - 1.9 => "/_matrix/client/v3/createRoom", + unstable => "/_matrix/client/v3/createRoom", } }; diff --git a/crates/matrix/src/lib.rs b/crates/matrix/src/lib.rs index ace15fb..1ae34a5 100644 --- a/crates/matrix/src/lib.rs +++ b/crates/matrix/src/lib.rs @@ -9,5 +9,61 @@ pub mod admin; pub mod client; +use async_trait::async_trait; +use bytes::{Bytes, BytesMut}; +use ruma_client::{DefaultConstructibleHttpClient, HttpClient, HttpClientExt}; + #[allow(unused_imports)] use ruma_common::api::error::MatrixError as Error; + +#[derive(Debug)] +pub struct Handle { + inner: reqwest::Client, +} + +impl Handle { + pub async fn new() { + self.send_matrix_request(, access_token, for_versions, request) + } + + pub async fn dispatch(&self) { + self.send_matrix_request(, access_token, for_versions, request) + } +} + +#[async_trait] +impl HttpClient for Handle { + type RequestBody = BytesMut; + type ResponseBody = Bytes; + type Error = reqwest::Error; + + async fn send_http_request( + &self, + req: http::Request, + ) -> Result, reqwest::Error> { + let req = req.map(|body| body.freeze()).try_into()?; + let mut res = self.inner.execute(req).await?; + + let mut http_builder = http::Response::builder() + .status(res.status()) + .version(res.version()); + std::mem::swap( + http_builder + .headers_mut() + .expect("http::response::Builder to be usable"), + res.headers_mut(), + ); + + Ok(http_builder + .body(res.bytes().await?) + .expect("http::Response construction to work")) + } +} + +impl DefaultConstructibleHttpClient for Handle { + fn default() -> Self { + Self { + inner: reqwest::Client::new(), + } + } +} diff --git a/crates/matrix/src/token/mod.rs b/crates/matrix/src/token/mod.rs deleted file mode 100644 index 24b2441..0000000 --- a/crates/matrix/src/token/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod shared_secret; diff --git a/crates/matrix/src/token/shared_secret.rs b/crates/matrix/src/token/shared_secret.rs deleted file mode 100644 index 1bba14c..0000000 --- a/crates/matrix/src/token/shared_secret.rs +++ /dev/null @@ -1,173 +0,0 @@ -//! [Shared-Secret Registration API](https://matrix-org.github.io/synapse/latest/admin_api/register_api.html#) -//! -//! # Important -//! -//! This API is disabled when MSC3861 is enabled. See [#15582](https://github.com/matrix-org/synapse/pull/15582) -//! -//! This API allows for the creation of users in an administrative and -//! non-interactive way. This is generally used for bootstrapping a Synapse -//! instance with administrator accounts. -//! -//! To authenticate yourself to the server, you will need both the shared secret -//! (registration_shared_secret in the homeserver configuration), and a one-time -//! nonce. If the registration shared secret is not configured, this API is not -//! enabled. - -use anyhow::Result; -use hex; -use hmac::{Hmac, Mac}; -use serde::{Deserialize, Serialize}; -use sha1::Sha1; - -use crate::{error::MatrixError, http::Client}; - -type HmacSha1 = Hmac; - -#[derive(Debug, Serialize, Deserialize)] -pub struct Nonce { - pub nonce: String, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct SharedSecretRegistrationDto { - pub nonce: String, - pub username: String, - pub displayname: Option, - pub password: String, - pub admin: bool, - /// The MAC is the hex digest output of the HMAC-SHA1 algorithm, with the - /// key being the shared secret and the content being the nonce, user, - /// password, either the string "admin" or "notadmin", and optionally the - /// user_type each separated by NULs. - pub mac: String, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct SharedSecretRegistration { - pub access_token: String, - pub user_id: String, - pub home_server: String, - pub device_id: String, -} - -impl SharedSecretRegistration { - /// Fetches the `Nonce` from the server. - /// - /// Refer: https://matrix-org.github.io/synapse/latest/admin_api/register_api.html#shared-secret-registration - pub async fn get_nonce(client: &Client) -> Result { - let resp = client.get("/_synapse/admin/v1/register").await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } - - /// Creates the [`SharedSecretRegistration`] instance. - /// - /// Refer: https://matrix-org.github.io/synapse/latest/admin_api/register_api.html#shared-secret-registration - pub async fn create(client: &Client, dto: SharedSecretRegistrationDto) -> Result { - let resp = client - .post_json("/_synapse/admin/v1/register", &dto) - .await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } - - /// Generates the MAC. - /// - /// # Inspiration - /// - /// This implementation is inspired by the following Python code from the - /// Synapse documentation on `Shared-Secret Registration`. - /// - /// ```python - /// import hmac, hashlib - /// - /// def generate_mac(nonce, user, password, admin=False, user_type=None): - /// - /// mac = hmac.new( - /// key=shared_secret, - /// digestmod=hashlib.sha1, - /// ) - /// - /// mac.update(nonce.encode('utf8')) - /// mac.update(b"\x00") - /// mac.update(user.encode('utf8')) - /// mac.update(b"\x00") - /// mac.update(password.encode('utf8')) - /// mac.update(b"\x00") - /// mac.update(b"admin" if admin else b"notadmin") - /// if user_type: - /// mac.update(b"\x00") - /// mac.update(user_type.encode('utf8')) - /// - /// return mac.hexdigest() - /// ``` - /// [Source](https://matrix-org.github.io/synapse/latest/admin_api/register_api.html#shared-secret-registration) - pub fn generate_mac>( - shared_secret: S, - nonce: S, - user: S, - password: S, - admin: bool, - user_type: Option, - ) -> Result { - let mut mac = HmacSha1::new_from_slice(shared_secret.as_ref().as_bytes())?; - - mac.update(nonce.as_ref().as_bytes()); - mac.update(b"\x00"); - - mac.update(user.as_ref().as_bytes()); - mac.update(b"\x00"); - - mac.update(password.as_ref().as_bytes()); - mac.update(b"\x00"); - - if admin { - mac.update("admin".as_bytes()); - } else { - mac.update("notadmin".as_bytes()); - } - - if let Some(user_type) = user_type { - mac.update(b"\x00"); - mac.update(user_type.as_ref().as_bytes()); - } - - let result = mac.finalize(); - let code_bytes = result.into_bytes(); - - Ok(hex::encode(code_bytes)) - } -} - -#[cfg(test)] -mod test { - use super::SharedSecretRegistration; - - #[test] - fn generates_mac_accordingly() { - let want = "c272fb1c287c795ff5ce238c4dba57cf95db5eff"; - let have = SharedSecretRegistration::generate_mac( - "m@;wYOUOh0f:CH5XA65sJB1^q01~DmIriOysRImot,OR_vzN&B", - "1234567890", - "groot", - "imroot!1234", - true, - None, - ) - .unwrap(); - - assert_eq!(have, want); - } -} From eb6ccd04acabefdaaffee6dbddacd7b0aba2fd73 Mon Sep 17 00:00:00 2001 From: mikoto Date: Thu, 29 Feb 2024 21:46:34 +0000 Subject: [PATCH 10/33] wip --- Cargo.toml | 4 + crates/core/Cargo.toml | 1 + crates/core/src/auth/mod.rs | 138 ++++++++++++ crates/core/src/auth/service.rs | 125 ----------- crates/core/src/error.rs | 34 +-- crates/core/src/lib.rs | 198 +++++++----------- crates/matrix/Cargo.toml | 1 + .../matrix/src/admin/user/get_user_by_3pid.rs | 3 +- crates/matrix/src/client/room.rs | 16 +- crates/matrix/src/client/room/create_room.rs | 6 +- crates/matrix/src/client/room/join_room.rs | 4 +- .../matrix/src/client/room/kick_from_room.rs | 2 +- crates/matrix/src/client/session.rs | 10 +- crates/matrix/src/client/session/login.rs | 6 +- crates/matrix/src/lib.rs | 49 +++-- 15 files changed, 284 insertions(+), 313 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6c8ff79..cc3e147 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,10 @@ default-members = ["crates/server"] resolver = "2" [workspace.dependencies] +server = { workspace = true, path = "crates/server" } +matrix = { workspace = true, path = "crates/matrix" } +commune = { workspace = true, path = "crates/core" } + async-trait = "0.1.74" hex = "0.4.3" hmac = "0.12.1" diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 9989278..63d231b 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -25,3 +25,4 @@ tokio = { workspace = true, features = ["full"] } # Local Dependencies matrix = { path = "../matrix" } +figment = { version = "0.10.14", features = ["toml"] } diff --git a/crates/core/src/auth/mod.rs b/crates/core/src/auth/mod.rs index e1001c6..d467338 100644 --- a/crates/core/src/auth/mod.rs +++ b/crates/core/src/auth/mod.rs @@ -1,3 +1,141 @@ +use std::sync::Arc; + +use matrix::client::session; + +use crate::{util::secret::Secret, Error}; + pub mod error; pub mod model; pub mod service; + +pub struct Auth { + handle: Arc, +} + +pub type AccessToken = Secret; + +impl Auth { + pub async fn login_with_password( + &self, + username: &str, + password: Secret, + ) -> Result { + use session::login::*; + + let req = Request { + kind: LoginType::Password { + password: password.inner().to_owned(), + }, + identifier: Some(Identifier::User { + user: username.to_owned(), + }), + device_name: "commune beta".to_owned(), + refresh_token: Some(true), + }; + + let resp: Response = self.handle.dispatch(None, req).await.unwrap().into(); + + Ok(AccessToken::new(resp.access_token)) + } + + // pub async fn get_login_flows(&self) -> Result { + // match Login::get_login_flows(&self.admin).await { + // Ok(flows) => Ok(flows), + // Err(err) => { + // tracing::error!("Failed to get login flows: {}", err); + // Err(Error::Unknown) + // } + // } + // } + + // pub async fn send_verification_code( + // &self, + // email: &str, + // session: &Uuid, + // ) -> Result { + // let mut conn = self.redis.get_async_connection().await.map_err(|err| { + // tracing::error!(?err, "Failed to get Redis connection"); + // AuthErrorCode::RedisConnectionError(err) + // })?; + // let verif_code = VerificationCode::new(email, session); + + // conn.set_ex::( + // Self::verification_code_key(session), + // verif_code.marshall(), + // REDIS_VERIFICATION_CODE_SECS, + // ) + // .await + // .map_err(|err| { + // tracing::error!(?err, "Failed to set verification code in Redis"); + // AuthErrorCode::RedisConnectionError(err) + // })?; + + // Ok(verif_code) + // } + + // pub async fn check_verification_code( + // &self, + // email: &str, + // session: &Uuid, + // code: &Secret, + // ) -> Result { + // let mut conn = self.redis.get_async_connection().await.map_err(|err| { + // tracing::error!(?err, "Failed to get Redis connection"); + // AuthErrorCode::RedisConnectionError(err) + // })?; + + // let maybe_marshalled_verification_code = conn + // .get::>(Self::verification_code_key(session)) + // .await + // .map_err(|err| { + // tracing::error!( + // ?err, + // ?session, + // ?email, + // "Failed to get verification code in Redis" + // ); + // AuthErrorCode::RedisConnectionError(err) + // })?; + + // if let Some(marshalled_verification_code) = + // maybe_marshalled_verification_code { let verification_code = + // VerificationCode::unmarshall(marshalled_verification_code); + + // if verification_code.email == email + // && verification_code.code == *code + // && verification_code.session == *session + // { + // return Ok(true); + // } + // } + + // tracing::warn!(?session, ?email, "Verification code not found in + // storge"); Ok(false) + // } + + // pub async fn drop_verification_code(&self, email: &str, session: &Uuid) -> + // Result { let mut conn = + // self.redis.get_async_connection().await.map_err(|err| { + // tracing::error!(?err, "Failed to get Redis connection"); + // AuthErrorCode::RedisConnectionError(err) + // })?; + + // conn.del(Self::verification_code_key(session)) + // .await + // .map_err(|err| { + // tracing::error!( + // ?err, + // ?session, + // ?email, + // "Failed to delete verification code in Redis" + // ); + // AuthErrorCode::RedisConnectionError(err) + // })?; + + // Ok(true) + // } + + // fn verification_code_key(session: &Uuid) -> String { + // format!("{}{}", REDIS_VERIFICATION_CODE_PREFIX, session) + // } +} diff --git a/crates/core/src/auth/service.rs b/crates/core/src/auth/service.rs index 39d89c2..028ffca 100644 --- a/crates/core/src/auth/service.rs +++ b/crates/core/src/auth/service.rs @@ -25,128 +25,3 @@ pub struct LoginCredentials { pub struct LoginCredentialsResponse { pub access_token: Secret, } - -pub struct AuthService { - admin: Arc, - redis: Arc, -} - -impl AuthService { - pub fn new(admin: Arc, redis: Arc) -> Self { - Self { admin, redis } - } - - pub async fn login(&self, credentials: LoginCredentials) -> Result { - let login_response = Login::login_credentials( - &self.admin, - credentials.username, - credentials.password.inner(), - ) - .await - // ??? - .unwrap(); - - Ok(LoginCredentialsResponse { - access_token: Secret::new(login_response.access_token), - }) - } - - pub async fn get_login_flows(&self) -> Result { - match Login::get_login_flows(&self.admin).await { - Ok(flows) => Ok(flows), - Err(err) => { - tracing::error!("Failed to get login flows: {}", err); - Err(Error::Unknown) - } - } - } - - pub async fn send_verification_code( - &self, - email: &str, - session: &Uuid, - ) -> Result { - let mut conn = self.redis.get_async_connection().await.map_err(|err| { - tracing::error!(?err, "Failed to get Redis connection"); - AuthErrorCode::RedisConnectionError(err) - })?; - let verif_code = VerificationCode::new(email, session); - - conn.set_ex::( - Self::verification_code_key(session), - verif_code.marshall(), - REDIS_VERIFICATION_CODE_SECS, - ) - .await - .map_err(|err| { - tracing::error!(?err, "Failed to set verification code in Redis"); - AuthErrorCode::RedisConnectionError(err) - })?; - - Ok(verif_code) - } - - pub async fn check_verification_code( - &self, - email: &str, - session: &Uuid, - code: &Secret, - ) -> Result { - let mut conn = self.redis.get_async_connection().await.map_err(|err| { - tracing::error!(?err, "Failed to get Redis connection"); - AuthErrorCode::RedisConnectionError(err) - })?; - - let maybe_marshalled_verification_code = conn - .get::>(Self::verification_code_key(session)) - .await - .map_err(|err| { - tracing::error!( - ?err, - ?session, - ?email, - "Failed to get verification code in Redis" - ); - AuthErrorCode::RedisConnectionError(err) - })?; - - if let Some(marshalled_verification_code) = maybe_marshalled_verification_code { - let verification_code = VerificationCode::unmarshall(marshalled_verification_code); - - if verification_code.email == email - && verification_code.code == *code - && verification_code.session == *session - { - return Ok(true); - } - } - - tracing::warn!(?session, ?email, "Verification code not found in storge"); - Ok(false) - } - - pub async fn drop_verification_code(&self, email: &str, session: &Uuid) -> Result { - let mut conn = self.redis.get_async_connection().await.map_err(|err| { - tracing::error!(?err, "Failed to get Redis connection"); - AuthErrorCode::RedisConnectionError(err) - })?; - - conn.del(Self::verification_code_key(session)) - .await - .map_err(|err| { - tracing::error!( - ?err, - ?session, - ?email, - "Failed to delete verification code in Redis" - ); - AuthErrorCode::RedisConnectionError(err) - })?; - - Ok(true) - } - - fn verification_code_key(session: &Uuid) -> String { - format!("{}{}", REDIS_VERIFICATION_CODE_PREFIX, session) - } -} diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index 34542fe..cabbf65 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -1,10 +1,7 @@ use http::StatusCode; use thiserror::Error; -use crate::{ - account::error::AccountErrorCode, auth::error::AuthErrorCode, mail::error::MailErrorCode, - room::error::RoomErrorCode, -}; +use crate::{auth::error::AuthErrorCode, mail::error::MailErrorCode, room::error::RoomErrorCode}; pub type Result = std::result::Result; @@ -28,32 +25,3 @@ pub enum Error { #[error("Unknown Error Occured")] Unknown, } - -impl From for Error { - fn from(err: AccountErrorCode) -> Self { - Error::User(err) - } -} - -impl HttpStatusCode for Error { - fn status_code(&self) -> StatusCode { - match self { - Error::Auth(err) => err.status_code(), - Error::User(err) => err.status_code(), - Error::Mail(err) => err.status_code(), - Error::Room(err) => err.status_code(), - Error::Startup(_) | Error::Unknown => StatusCode::INTERNAL_SERVER_ERROR, - } - } - - fn error_code(&self) -> &'static str { - match self { - Error::Auth(err) => err.error_code(), - Error::User(err) => err.error_code(), - Error::Mail(err) => err.error_code(), - Error::Room(err) => err.error_code(), - Error::Startup(_) => "SERVER_STARTUP_ERROR", - Error::Unknown => "UNKNOWN_ERROR", - } - } -} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index f0f9183..78079ae 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -1,153 +1,115 @@ -//! This library deals with our core logic, such as authorizing user interactions, -//! forwarding regular events and constructing custom requests. +//! This library deals with our core logic, such as authorizing user +//! interactions, forwarding regular events and constructing custom requests. -pub mod session; pub mod auth; pub mod error; pub mod mail; pub mod room; +pub mod session; pub mod util; +use std::sync::Arc; + pub use error::{Error, HttpStatusCode, Result}; -use mail::service::MailService; -use room::service::RoomService; -use tokio::sync::mpsc::Receiver; +use figment::{ + providers::{Format, Toml}, + Figment, +}; +use matrix::ruma_common::OwnedServerName; use url::Url; +use util::secret::Secret; -use std::{fmt::Debug, str::FromStr, sync::Arc}; - -pub mod env { - pub const COMMUNE_SYNAPSE_HOST: &str = "COMMUNE_SYNAPSE_HOST"; - pub const COMMUNE_SYNAPSE_ADMIN_TOKEN: &str = "COMMUNE_SYNAPSE_ADMIN_TOKEN"; - pub const COMMUNE_SYNAPSE_SERVER_NAME: &str = "COMMUNE_SYNAPSE_SERVER_NAME"; - pub const COMMUNE_REGISTRATION_SHARED_SECRET: &str = "COMMUNE_REGISTRATION_SHARED_SECRET"; - pub const REDIS_HOST: &str = "REDIS_HOST"; - pub const SMTP_HOST: &str = "SMTP_HOST"; - pub const MAILDEV_INCOMING_USER: &str = "MAILDEV_INCOMING_USER"; - pub const MAILDEV_INCOMING_PASS: &str = "MAILDEV_INCOMING_USER"; -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct CommuneConfig { +pub struct Config { pub smtp_host: Url, pub redis_host: Url, pub maildev_incoming_user: Option, pub maildev_incoming_pass: Option, - pub synapse_host: String, - pub synapse_admin_token: String, - pub synapse_server_name: String, + pub synapse_host: Url, + pub synapse_admin_token: Secret, + pub synapse_server_name: OwnedServerName, pub synapse_registration_shared_secret: String, } -impl Default for CommuneConfig { +impl Default for Config { fn default() -> Self { - Self::new() - } -} - -impl CommuneConfig { - pub fn new() -> Self { - Self { - smtp_host: Self::var(env::SMTP_HOST), - redis_host: Self::var(env::REDIS_HOST), - maildev_incoming_user: Self::var_opt(env::MAILDEV_INCOMING_USER), - maildev_incoming_pass: Self::var_opt(env::MAILDEV_INCOMING_PASS), - synapse_host: Self::var(env::COMMUNE_SYNAPSE_HOST), - synapse_admin_token: Self::var(env::COMMUNE_SYNAPSE_ADMIN_TOKEN), - synapse_server_name: Self::var(env::COMMUNE_SYNAPSE_SERVER_NAME), - synapse_registration_shared_secret: Self::var(env::COMMUNE_REGISTRATION_SHARED_SECRET), - } - } - - fn var(name: &str) -> P { - if let Ok(value) = std::env::var(name) { - if let Ok(value) = value.parse() { - return value; - } - } + let config = Figment::new().merge(Toml::file( + std::env::var("COMMUNE_CONFIG").unwrap_or("commune.toml".to_owned()), + )); - panic!( - "Failed to parse {} as {:?}", - name, - std::any::type_name::

() - ); - } + match config.extract::() { + Ok(s) => s, + Err(e) => { + eprintln!("Could not extract config: {e}"); - fn var_opt(name: &str) -> Option

{ - if let Ok(value) = std::env::var(name) { - if let Ok(value) = value.parse() { - return Some(value); + std::process::exit(1); } - - panic!( - "Failed to parse {} as {:?}", - name, - std::any::type_name::

() - ); } - - None } } pub struct Commune { - + handle: Arc, + admin_token: Arc, } impl Commune { - pub async fn new>(config: C) -> Result { - let config: CommuneConfig = config.into(); - let mut admin = MatrixAdminClient::new(&config.synapse_host, &config.synapse_server_name) - .map_err(|err| { - tracing::error!(?err, "Failed to create admin client"); - Error::Startup(err.to_string()) - })?; - - admin - .set_token(&config.synapse_admin_token) - .map_err(|err| { - tracing::error!(?err, "Failed to set admin token"); - Error::Startup(err.to_string()) - })?; - - let redis = { - let client = redis::Client::open(config.redis_host.to_string()).map_err(|err| { - tracing::error!(?err, host=%config.redis_host.to_string(), "Failed to open connection to Redis"); - Error::Startup(err.to_string()) - })?; - let mut conn = client.get_async_connection().await.map_err(|err| { - tracing::error!(?err, host=%config.redis_host.to_string(), "Failed to get connection to Redis"); - Error::Startup(err.to_string()) - })?; - - redis::cmd("PING").query_async(&mut conn).await.map_err(|err| { - tracing::error!(?err, host=%config.redis_host.to_string(), "Failed to ping Redis"); - Error::Startup(err.to_string()) - })?; - - tracing::info!(host=%config.redis_host.to_string(), "Connected to Redis"); - - Arc::new(client) - }; - - let admin_client = Arc::new(admin); - let auth = Arc::new(AuthService::new( - Arc::clone(&admin_client), - Arc::clone(&redis), - )); - let mail = Arc::new(MailService::new(&config)); - let account = AccountService::new( - Arc::clone(&admin_client), - Arc::clone(&auth), - Arc::clone(&mail), - ); - let room = RoomService::new(Arc::clone(&admin_client)); + pub fn new(config: Config) -> Result { + let handle = matrix::Handle::new(&config.synapse_host); Ok(Self { - account: Arc::new(account), - auth, - room: Arc::new(room), + handle: Arc::new(handle), + admin_token: Arc::new(config.synapse_admin_token), }) } } + +// admin +// .set_token(&config.synapse_admin_token) +// .map_err(|err| { +// tracing::error!(?err, "Failed to set admin token"); +// Error::Startup(err.to_string()) +// })?; + +// let redis = { +// let client = +// redis::Client::open(config.redis_host.to_string()).map_err(|err| { +// tracing::error!(?err, host=%config.redis_host.to_string(), +// "Failed to open connection to Redis"); +// Error::Startup(err.to_string()) })?; +// let mut conn = client.get_async_connection().await.map_err(|err| +// { tracing::error!(?err, host=%config.redis_host.to_string(), +// "Failed to get connection to Redis"); +// Error::Startup(err.to_string()) })?; + +// redis::cmd("PING").query_async(&mut conn).await.map_err(|err| { +// tracing::error!(?err, host=%config.redis_host.to_string(), +// "Failed to ping Redis"); Error::Startup(err.to_string()) +// })?; + +// tracing::info!(host=%config.redis_host.to_string(), "Connected to +// Redis"); + +// Arc::new(client) +// }; + +// let admin_client = Arc::new(admin); +// let auth = Arc::new(AuthService::new( +// Arc::clone(&admin_client), +// Arc::clone(&redis), +// )); +// let mail = Arc::new(MailService::new(&config)); +// let account = AccountService::new( +// Arc::clone(&admin_client), +// Arc::clone(&auth), +// Arc::clone(&mail), +// ); +// let room = RoomService::new(Arc::clone(&admin_client)); + +// Ok(Self { +// account: Arc::new(account), +// auth, +// room: Arc::new(room), +// }) +// } +// } diff --git a/crates/matrix/Cargo.toml b/crates/matrix/Cargo.toml index 6de728e..8ab23aa 100644 --- a/crates/matrix/Cargo.toml +++ b/crates/matrix/Cargo.toml @@ -16,6 +16,7 @@ ruma-events = { version = "0.27.11", default_features = false, features = [ ruma-common = { version = "0.12.0", default_features = false, features = [ "api", "rand", + "unstable-exhaustive-types" ] } ruma-macros = { version = "0.12.0", default_features = false } ruma-client = { version = "0.12.0", default_features = false } diff --git a/crates/matrix/src/admin/user/get_user_by_3pid.rs b/crates/matrix/src/admin/user/get_user_by_3pid.rs index 2b9e56c..2263ba7 100644 --- a/crates/matrix/src/admin/user/get_user_by_3pid.rs +++ b/crates/matrix/src/admin/user/get_user_by_3pid.rs @@ -1,6 +1,7 @@ use ruma_common::{ api::{request, response, Metadata}, - metadata, thirdparty::Medium, + metadata, + thirdparty::Medium, }; use super::User; diff --git a/crates/matrix/src/client/room.rs b/crates/matrix/src/client/room.rs index 27a797f..dff4c1e 100644 --- a/crates/matrix/src/client/room.rs +++ b/crates/matrix/src/client/room.rs @@ -2,11 +2,11 @@ //! //! reference: https://spec.matrix.org/unstable/client-server-api/#rooms -mod ban_from_room; -mod create_room; -mod forget_room; -mod get_rooms; -mod join_room; -mod kick_from_room; -mod leave_room; -mod unban_from_room; +pub mod ban_from_room; +pub mod create_room; +pub mod forget_room; +pub mod get_rooms; +pub mod join_room; +pub mod kick_from_room; +pub mod leave_room; +pub mod unban_from_room; diff --git a/crates/matrix/src/client/room/create_room.rs b/crates/matrix/src/client/room/create_room.rs index 140bfed..bba3069 100644 --- a/crates/matrix/src/client/room/create_room.rs +++ b/crates/matrix/src/client/room/create_room.rs @@ -1,6 +1,8 @@ use ruma_common::{ api::{request, response, Metadata}, - metadata, OwnedRoomId, OwnedUserId, serde::Raw, + metadata, + serde::Raw, + OwnedRoomId, OwnedUserId, }; use ruma_events::{room::power_levels::RoomPowerLevels, AnyInitialStateEvent}; use serde::Serialize; @@ -49,7 +51,7 @@ pub struct Request { #[response(error = crate::Error)] pub struct Response { - pub room_id: OwnedRoomId, + pub room_id: OwnedRoomId, } #[derive(Clone, Debug, Serialize)] diff --git a/crates/matrix/src/client/room/join_room.rs b/crates/matrix/src/client/room/join_room.rs index 1fdadd4..5cb0502 100644 --- a/crates/matrix/src/client/room/join_room.rs +++ b/crates/matrix/src/client/room/join_room.rs @@ -1,6 +1,6 @@ use ruma_common::{ api::{request, response, Metadata}, - metadata, OwnedRoomOrAliasId, OwnedRoomId, + metadata, OwnedRoomId, OwnedRoomOrAliasId, }; #[allow(dead_code)] @@ -24,5 +24,5 @@ pub struct Request { #[response(error = crate::Error)] pub struct Response { - pub room_id: OwnedRoomId, + pub room_id: OwnedRoomId, } diff --git a/crates/matrix/src/client/room/kick_from_room.rs b/crates/matrix/src/client/room/kick_from_room.rs index c3079b9..a618431 100644 --- a/crates/matrix/src/client/room/kick_from_room.rs +++ b/crates/matrix/src/client/room/kick_from_room.rs @@ -1,6 +1,6 @@ use ruma_common::{ api::{request, response, Metadata}, - metadata, OwnedUserId, OwnedRoomId, + metadata, OwnedRoomId, OwnedUserId, }; #[allow(dead_code)] diff --git a/crates/matrix/src/client/session.rs b/crates/matrix/src/client/session.rs index 18328e9..5ebf633 100644 --- a/crates/matrix/src/client/session.rs +++ b/crates/matrix/src/client/session.rs @@ -2,8 +2,8 @@ //! //! reference: https://spec.matrix.org/unstable/client-server-api/#client-authentication -mod get_flows; -mod login; -mod register; -mod username_available; -mod whoami; +pub mod get_flows; +pub mod login; +pub mod register; +pub mod username_available; +pub mod whoami; diff --git a/crates/matrix/src/client/session/login.rs b/crates/matrix/src/client/session/login.rs index 889401b..7774374 100644 --- a/crates/matrix/src/client/session/login.rs +++ b/crates/matrix/src/client/session/login.rs @@ -1,8 +1,10 @@ use ruma_common::{ api::{request, response, Metadata}, - metadata, OwnedMxcUri, thirdparty::Medium, OwnedDeviceId, OwnedUserId, + metadata, + thirdparty::Medium, + OwnedDeviceId, OwnedMxcUri, OwnedUserId, }; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; #[allow(dead_code)] const METADATA: Metadata = metadata! { diff --git a/crates/matrix/src/lib.rs b/crates/matrix/src/lib.rs index 1ae34a5..cecff9a 100644 --- a/crates/matrix/src/lib.rs +++ b/crates/matrix/src/lib.rs @@ -1,8 +1,10 @@ //! This library deals with forwarding Matrix requests to the server. -//! Comments have been used sparingly as the specification contains all the technical details. +//! Comments have been used sparingly as the specification contains all the +//! technical details. -//! We rely on `ruma` to abstract away the boilerplate introduced by HTTP requests, -//! without sacrificing flexibility by defining our own request and response types. +//! We rely on `ruma` to abstract away the boilerplate introduced by HTTP +//! requests, without sacrificing flexibility by defining our own request and +//! response types. //! //! reference: https://docs.ruma.io/ruma_common/api/index.html @@ -11,23 +13,46 @@ pub mod client; use async_trait::async_trait; use bytes::{Bytes, BytesMut}; -use ruma_client::{DefaultConstructibleHttpClient, HttpClient, HttpClientExt}; +use ruma_client::{HttpClient, HttpClientExt, ResponseResult}; #[allow(unused_imports)] use ruma_common::api::error::MatrixError as Error; +use ruma_common::api::{OutgoingRequest, SendAccessToken}; + +pub use ruma_common; +pub use ruma_events; + #[derive(Debug)] pub struct Handle { inner: reqwest::Client, + homeserver_url: url::Url, } impl Handle { - pub async fn new() { - self.send_matrix_request(, access_token, for_versions, request) + pub fn new(homeserver_url: &url::Url) -> Self { + Self { + inner: reqwest::Client::new(), + homeserver_url: homeserver_url.to_owned(), + } } - pub async fn dispatch(&self) { - self.send_matrix_request(, access_token, for_versions, request) + pub async fn dispatch( + &self, + access_token: Option<&str>, + request: R, + ) -> ResponseResult { + self.send_matrix_request::( + self.homeserver_url.as_str(), + access_token + .map(SendAccessToken::IfRequired) + .unwrap_or(SendAccessToken::None), + &[], + request, + ) + .await + .try_into() + .unwrap() } } @@ -59,11 +84,3 @@ impl HttpClient for Handle { .expect("http::Response construction to work")) } } - -impl DefaultConstructibleHttpClient for Handle { - fn default() -> Self { - Self { - inner: reqwest::Client::new(), - } - } -} From a4077e472bd59190281fb7df6998c16ab95d2c12 Mon Sep 17 00:00:00 2001 From: mikoto Date: Fri, 1 Mar 2024 21:56:45 +0000 Subject: [PATCH 11/33] fix: `OutgoingRequest`/`IncomingResponse` macro expanded --- crates/core/Cargo.toml | 2 +- crates/core/src/auth/mod.rs | 12 ++++---- crates/matrix/src/client/session/login.rs | 36 +++++++++++++++++++++++ 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 63d231b..f94e14f 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -24,5 +24,5 @@ url = { workspace = true, features = ["serde"] } tokio = { workspace = true, features = ["full"] } # Local Dependencies -matrix = { path = "../matrix" } +matrix = { path = "../matrix", features = ["client", "server"] } figment = { version = "0.10.14", features = ["toml"] } diff --git a/crates/core/src/auth/mod.rs b/crates/core/src/auth/mod.rs index d467338..7e9eb90 100644 --- a/crates/core/src/auth/mod.rs +++ b/crates/core/src/auth/mod.rs @@ -22,16 +22,16 @@ impl Auth { ) -> Result { use session::login::*; - let req = Request { - kind: LoginType::Password { + let req = Request::new( + LoginType::Password { password: password.inner().to_owned(), }, - identifier: Some(Identifier::User { + Some(Identifier::User { user: username.to_owned(), }), - device_name: "commune beta".to_owned(), - refresh_token: Some(true), - }; + "commune beta".to_owned(), + Some(true), + ); let resp: Response = self.handle.dispatch(None, req).await.unwrap().into(); diff --git a/crates/matrix/src/client/session/login.rs b/crates/matrix/src/client/session/login.rs index 7774374..4f5933e 100644 --- a/crates/matrix/src/client/session/login.rs +++ b/crates/matrix/src/client/session/login.rs @@ -34,6 +34,22 @@ pub struct Request { pub refresh_token: Option, } +impl Request { + pub fn new( + kind: LoginType, + identifier: Option, + device_name: String, + refresh_token: Option, + ) -> Self { + Self { + kind, + identifier, + device_name, + refresh_token, + } + } +} + #[response(error = crate::Error)] pub struct Response { pub access_token: String, @@ -52,6 +68,26 @@ pub struct Response { pub well_known: Option, } +impl Response { + pub fn new( + access_token: String, + device_id: OwnedDeviceId, + expires_in_ms: Option, + refresh_token: Option, + user_id: OwnedUserId, + well_known: Option, + ) -> Self { + Self { + access_token, + device_id, + expires_in_ms, + refresh_token, + user_id, + well_known, + } + } +} + #[derive(Clone, Debug, Serialize)] pub struct IdentityProvider { pub id: String, From a56c20294788d16afc35edfb494e9ba2cfadaec5 Mon Sep 17 00:00:00 2001 From: mikoto Date: Sat, 2 Mar 2024 16:26:53 +0000 Subject: [PATCH 12/33] matrix crate compiles --- crates/matrix/src/admin/room/get_room.rs | 3 +- crates/matrix/src/admin/session.rs | 3 +- crates/matrix/src/admin/session/register.rs | 2 +- crates/matrix/src/admin/user/get_users.rs | 3 +- crates/matrix/src/client/room/create_room.rs | 30 ++++++++++++++++++-- 5 files changed, 33 insertions(+), 8 deletions(-) diff --git a/crates/matrix/src/admin/room/get_room.rs b/crates/matrix/src/admin/room/get_room.rs index b55a66b..b7b604b 100644 --- a/crates/matrix/src/admin/room/get_room.rs +++ b/crates/matrix/src/admin/room/get_room.rs @@ -2,7 +2,6 @@ use ruma_common::{ api::{request, response, Metadata}, metadata, OwnedRoomId, }; - use super::Room; #[allow(dead_code)] @@ -12,7 +11,7 @@ const METADATA: Metadata = metadata! { authentication: AccessToken, history: { unstable => "/_synapse/admin/v1/rooms/:room_id", - } + } }; #[request(error = crate::Error)] diff --git a/crates/matrix/src/admin/session.rs b/crates/matrix/src/admin/session.rs index 84ee0f7..10d725f 100644 --- a/crates/matrix/src/admin/session.rs +++ b/crates/matrix/src/admin/session.rs @@ -3,11 +3,12 @@ //! reference: https://matrix-org.github.io/synapse/latest/admin_api/register_api.html use hmac::Mac; +use serde::Serialize; pub mod get_nonce; pub mod register; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] pub struct Hmac { inner: Vec, } diff --git a/crates/matrix/src/admin/session/register.rs b/crates/matrix/src/admin/session/register.rs index c71d8f0..4838353 100644 --- a/crates/matrix/src/admin/session/register.rs +++ b/crates/matrix/src/admin/session/register.rs @@ -23,7 +23,7 @@ pub struct Request { pub password: String, - #[serde(skip_deserializing_if = "String::is_empty")] + #[serde(skip_serializing_if = "String::is_empty")] pub displayname: String, pub admin: bool, diff --git a/crates/matrix/src/admin/user/get_users.rs b/crates/matrix/src/admin/user/get_users.rs index 40a926d..867d548 100644 --- a/crates/matrix/src/admin/user/get_users.rs +++ b/crates/matrix/src/admin/user/get_users.rs @@ -2,6 +2,7 @@ use ruma_common::{ api::{request, response, Direction, Metadata}, metadata, OwnedUserId, }; +use serde::Serialize; use super::User; @@ -59,7 +60,7 @@ pub struct Response { total: u64, } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)] #[allow(dead_code)] pub enum OrderBy { #[default] diff --git a/crates/matrix/src/client/room/create_room.rs b/crates/matrix/src/client/room/create_room.rs index bba3069..ebf7e34 100644 --- a/crates/matrix/src/client/room/create_room.rs +++ b/crates/matrix/src/client/room/create_room.rs @@ -1,10 +1,12 @@ +use std::collections::BTreeMap; + use ruma_common::{ api::{request, response, Metadata}, metadata, serde::Raw, - OwnedRoomId, OwnedUserId, + OwnedRoomId, OwnedUserId, power_levels::NotificationPowerLevels, }; -use ruma_events::{room::power_levels::RoomPowerLevels, AnyInitialStateEvent}; +use ruma_events::{AnyInitialStateEvent, TimelineEventType}; use serde::Serialize; #[allow(dead_code)] @@ -39,7 +41,6 @@ pub struct Request { )] pub power_override: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub preset: RoomPreset, #[serde(rename = "room_alias_name", skip_serializing_if = "String::is_empty")] @@ -70,3 +71,26 @@ pub enum RoomPreset { #[default] TrustedPrivateChat, } + +#[derive(Clone, Debug, Serialize)] +pub struct RoomPowerLevels { + pub ban: u64, + + pub events: BTreeMap, + + pub events_default: u64, + + pub invite: u64, + + pub kick: u64, + + pub redact: u64, + + pub state_default: u64, + + pub users: BTreeMap, + + pub users_default: u64, + + pub notifications: NotificationPowerLevels, +} From 33e63cab9b594a57ce23683910266c62d55d22b0 Mon Sep 17 00:00:00 2001 From: mikoto Date: Fri, 8 Mar 2024 10:56:32 +0000 Subject: [PATCH 13/33] chore: formatting Cargo.toml --- crates/matrix/Cargo.toml | 2 +- crates/server/Cargo.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/matrix/Cargo.toml b/crates/matrix/Cargo.toml index 8ab23aa..0c7bcfd 100644 --- a/crates/matrix/Cargo.toml +++ b/crates/matrix/Cargo.toml @@ -16,7 +16,7 @@ ruma-events = { version = "0.27.11", default_features = false, features = [ ruma-common = { version = "0.12.0", default_features = false, features = [ "api", "rand", - "unstable-exhaustive-types" + "unstable-exhaustive-types", ] } ruma-macros = { version = "0.12.0", default_features = false } ruma-client = { version = "0.12.0", default_features = false } diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index 6eb2dad..efa7239 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -24,8 +24,8 @@ serde = { workspace = true, features = ["derive"] } tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] } tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } -url = { workspace = true, features= ["serde"] } -uuid = { workspace = true, features= ["serde"] } +url = { workspace = true, features = ["serde"] } +uuid = { workspace = true, features = ["serde"] } # Local Dependencies core = { path = "../core" } From 023400819ae261c160e936936bd9af8775ef4300 Mon Sep 17 00:00:00 2001 From: mikoto Date: Sat, 9 Mar 2024 17:06:33 +0000 Subject: [PATCH 14/33] fundamental changes --- Cargo.toml | 25 +- commune-example.toml | 10 + crates/core/Cargo.toml | 11 +- crates/core/src/auth/mod.rs | 54 +++- crates/core/src/auth/model.rs | 84 ----- crates/core/src/auth/service.rs | 27 -- crates/core/src/config.rs | 63 ++++ crates/core/src/error.rs | 34 +-- crates/core/src/lib.rs | 56 +--- crates/core/src/mail/mod.rs | 286 +++++++++++++++++- crates/core/src/mail/service.rs | 128 -------- .../src/mail/templates/verification_code.hbs | 126 -------- crates/core/src/util/mod.rs | 1 - crates/core/src/util/time.rs | 10 - crates/matrix/src/admin/room/get_room.rs | 2 +- crates/matrix/src/client/room/create_room.rs | 3 +- crates/matrix/src/client/session/register.rs | 11 + crates/matrix/src/lib.rs | 5 +- crates/{server => router}/Cargo.toml | 6 +- crates/{server => router}/src/config.rs | 0 crates/router/src/lib.rs | 38 +++ crates/{server/src/bin => router/src}/main.rs | 9 +- .../{server => router}/src/router/api/mod.rs | 0 .../src/router/api/v1/account/email.rs | 0 .../src/router/api/v1/account/login.rs | 0 .../src/router/api/v1/account/mod.rs | 0 .../src/router/api/v1/account/root.rs | 0 .../src/router/api/v1/account/session.rs | 0 .../src/router/api/v1/account/verify_code.rs | 0 .../api/v1/account/verify_code_email.rs | 0 .../src/router/api/v1/mod.rs | 0 .../src/router/middleware/auth.rs | 0 .../src/router/middleware/mod.rs | 0 crates/{server => router}/src/router/mod.rs | 0 crates/{server => router}/src/services.rs | 0 crates/server/src/lib.rs | 21 -- crates/test/Cargo.toml | 5 +- 37 files changed, 507 insertions(+), 508 deletions(-) create mode 100644 commune-example.toml delete mode 100644 crates/core/src/auth/model.rs delete mode 100644 crates/core/src/auth/service.rs create mode 100644 crates/core/src/config.rs delete mode 100644 crates/core/src/mail/service.rs delete mode 100644 crates/core/src/mail/templates/verification_code.hbs rename crates/{server => router}/Cargo.toml (86%) rename crates/{server => router}/src/config.rs (100%) create mode 100644 crates/router/src/lib.rs rename crates/{server/src/bin => router/src}/main.rs (68%) rename crates/{server => router}/src/router/api/mod.rs (100%) rename crates/{server => router}/src/router/api/v1/account/email.rs (100%) rename crates/{server => router}/src/router/api/v1/account/login.rs (100%) rename crates/{server => router}/src/router/api/v1/account/mod.rs (100%) rename crates/{server => router}/src/router/api/v1/account/root.rs (100%) rename crates/{server => router}/src/router/api/v1/account/session.rs (100%) rename crates/{server => router}/src/router/api/v1/account/verify_code.rs (100%) rename crates/{server => router}/src/router/api/v1/account/verify_code_email.rs (100%) rename crates/{server => router}/src/router/api/v1/mod.rs (100%) rename crates/{server => router}/src/router/middleware/auth.rs (100%) rename crates/{server => router}/src/router/middleware/mod.rs (100%) rename crates/{server => router}/src/router/mod.rs (100%) rename crates/{server => router}/src/services.rs (100%) delete mode 100644 crates/server/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index cc3e147..27067ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,27 +9,23 @@ repository = "https://github.com/commune-os/commune-rs" rust-version = "1.75.0" [workspace] -members = ["crates/core", "crates/matrix", "crates/server", "crates/test"] -default-members = ["crates/server"] +# members = ["crates/core", "crates/matrix", "crates/router", "crates/test"] +members = ["crates/core", "crates/matrix", "crates/router"] +default-members = ["crates/router"] resolver = "2" [workspace.dependencies] -server = { workspace = true, path = "crates/server" } -matrix = { workspace = true, path = "crates/matrix" } -commune = { workspace = true, path = "crates/core" } - async-trait = "0.1.74" +figment = { version = "0.10.14", features = ["toml"] } hex = "0.4.3" hmac = "0.12.1" -serde_path_to_error = "0.1.14" -serde_qs = "0.12.0" sha1 = "0.10.6" anyhow = "1.0.75" axum = { version = "0.7.4", features = ["tokio"] } -chrono = { version = "0.4.34", features = ["serde"] } -dotenv = "0.15.0" http = "0.2.11" mime = "0.3.17" +mail-send = "0.4.7" +maud = "0.26.0" # openssl = { version = "0.10.63", features = ["vendored"] } # openssl-sys = { version = "0.9.99", features = ["vendored"] } reqwest = { version = "0.11.22", default-features = false, features = [ @@ -39,18 +35,19 @@ reqwest = { version = "0.11.22", default-features = false, features = [ "rustls", ] } serde = "1.0.192" -serde_json = "1.0.108" +time = "0.3.34" tokio = "1.34.0" tracing = "0.1.40" tracing-subscriber = "0.3.18" url = "2.4.1" -uuid = { version = "1.6.1", features = ["v4"] } -handlebars = "5.0.0" -lettre = "0.11" rand = "0.8.5" thiserror = "1.0.50" validator = { version = "0.16", features = ["derive"] } +router = { workspace = true, path = "crates/router" } +matrix = { workspace = true, path = "crates/matrix" } +commune = { workspace = true, path = "crates/core" } + [workspace.lints.rust] unreachable_pub = "warn" unsafe_code = "forbid" diff --git a/commune-example.toml b/commune-example.toml new file mode 100644 index 0000000..5c52898 --- /dev/null +++ b/commune-example.toml @@ -0,0 +1,10 @@ +[server] +listener = "local" # | "global" +ports = [1212, 2424] +tls = true +# `X-Forwarded-For` header +xff = false + +[registration] +enabled = false +verify_email = false diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index f94e14f..f5ea08d 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -10,19 +10,18 @@ path = "src/lib.rs" [dependencies] # Workspace Dependencies -handlebars = { workspace = true } -lettre = { workspace = true } +anyhow = { workspace = true } rand = { workspace = true } thiserror = { workspace = true } validator = { workspace = true, features = ["derive"] } http = { workspace = true } +mail-send = {workspace = true} +maud = {workspace = true} serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } tracing = { workspace = true } -uuid = { workspace = true, features = ["serde"] } +figment = { workspace = true } url = { workspace = true, features = ["serde"] } tokio = { workspace = true, features = ["full"] } # Local Dependencies -matrix = { path = "../matrix", features = ["client", "server"] } -figment = { version = "0.10.14", features = ["toml"] } +matrix = { path = "../matrix", features = ["client"] } diff --git a/crates/core/src/auth/mod.rs b/crates/core/src/auth/mod.rs index 7e9eb90..73b70ed 100644 --- a/crates/core/src/auth/mod.rs +++ b/crates/core/src/auth/mod.rs @@ -1,18 +1,29 @@ -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; use matrix::client::session; +use rand::Rng; -use crate::{util::secret::Secret, Error}; +use crate::{util::secret::Secret, Config, Error}; pub mod error; -pub mod model; -pub mod service; + +pub type AccessToken = Secret; pub struct Auth { handle: Arc, + config: Arc, + cache: HashMap>, } -pub type AccessToken = Secret; +impl Auth { + fn new(handle: Arc, config: Arc) -> Self { + Auth { + handle, + config, + cache: HashMap::new(), + } + } +} impl Auth { pub async fn login_with_password( @@ -33,7 +44,38 @@ impl Auth { Some(true), ); - let resp: Response = self.handle.dispatch(None, req).await.unwrap().into(); + let resp: Response = self.handle.dispatch(None, req).await.unwrap(); + + Ok(AccessToken::new(resp.access_token)) + } + + pub async fn register(&self, username: &str, password: Secret) -> Result { + use session::register::*; + + let req = Request::new(username, password.inner(), "commune beta"); + + let resp: Response = self.handle.dispatch(None, req).await.unwrap(); + + Ok(AccessToken::new(resp.access_token)) + } + + pub async fn verify_email( + &self, + username: &str, + password: Secret, + email: &str, + ) -> Result { + use session::register::*; + + let verification_code: String = rand::thread_rng() + .gen::<[u32; 6]>() + .iter() + .map(|i| char::from_digit(i % 10, 10)) + .collect(); + + let req = Request::new(username, password.inner(), "commune beta"); + + let resp: Response = self.handle.dispatch(None, req).await.unwrap(); Ok(AccessToken::new(resp.access_token)) } diff --git a/crates/core/src/auth/model.rs b/crates/core/src/auth/model.rs deleted file mode 100644 index c6fe93b..0000000 --- a/crates/core/src/auth/model.rs +++ /dev/null @@ -1,84 +0,0 @@ -use rand::{ - distributions::{Alphanumeric, DistString}, - SeedableRng, -}; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -use crate::util::secret::Secret; - -/// Quantity of elements in each of the parts conforming the verification code, -/// should be a even nember in order to have no remainder when dividing the -/// capacity of the verification code string. -const VERIFICATION_CODE_CHAR: usize = 4; - -/// Quantity of parts conforming the verification code, should be a even number -/// in order to have no remainder when dividing the capacity of the verification -/// code string. -const VERIFICATION_CODE_PART: usize = 3; - -/// Capacity of the verification code string -const VERIFICATION_CODE_CAPY: usize = - (VERIFICATION_CODE_PART * VERIFICATION_CODE_CHAR) + VERIFICATION_CODE_PART; - -#[derive(Debug, Deserialize, Serialize)] -pub struct VerificationCode { - pub email: String, - pub code: Secret, - pub session: Uuid, -} - -impl VerificationCode { - pub fn new(email: &str, session: &Uuid) -> Self { - let code = Self::generate_verification_code(); - - Self { - email: email.to_string(), - code, - session: *session, - } - } - - /// Creates the marshalled representation of the verification code which is - /// JSON - pub fn marshall(&self) -> String { - serde_json::to_string(&self).unwrap() - } - - /// Builds an instance of [`VerificationCode`] from the marshalled JSON - pub fn unmarshall(payload: String) -> Self { - serde_json::from_str(&payload).unwrap() - } - - fn generate_verification_code() -> Secret { - let mut out = String::with_capacity(VERIFICATION_CODE_CAPY - VERIFICATION_CODE_PART); - let mut rng = rand::prelude::StdRng::from_entropy(); - - Alphanumeric.append_string( - &mut rng, - &mut out, - VERIFICATION_CODE_CAPY - VERIFICATION_CODE_PART, - ); - - Secret::from( - format!("{}-{}-{}", &out[0..=3], &out[4..=7], &out[8..=11]).to_ascii_lowercase(), - ) - } -} - -#[cfg(test)] -mod test { - use super::VerificationCode; - - #[test] - fn codes_are_never_repeated() { - let codes = (1..50) - .map(|_| VerificationCode::generate_verification_code().to_string()) - .collect::>(); - - assert_eq!( - codes.len(), - codes.iter().collect::>().len() - ); - } -} diff --git a/crates/core/src/auth/service.rs b/crates/core/src/auth/service.rs deleted file mode 100644 index 028ffca..0000000 --- a/crates/core/src/auth/service.rs +++ /dev/null @@ -1,27 +0,0 @@ -use std::sync::Arc; - -use matrix::{ - client::resources::login::{Login, LoginFlows as LoginFlowsResponse}, - Client as MatrixAdminClient, -}; -use redis::AsyncCommands; -use uuid::Uuid; - -use crate::{auth::error::AuthErrorCode, util::secret::Secret, Error, Result}; - -use super::model::VerificationCode; - -/// Prefix for the verification code key in Redis -const REDIS_VERIFICATION_CODE_PREFIX: &str = "commune::verification_code::"; - -/// TTL for the verification code in Redis -const REDIS_VERIFICATION_CODE_SECS: u64 = 60 * 5; - -pub struct LoginCredentials { - pub username: String, - pub password: Secret, -} - -pub struct LoginCredentialsResponse { - pub access_token: Secret, -} diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs new file mode 100644 index 0000000..b464751 --- /dev/null +++ b/crates/core/src/config.rs @@ -0,0 +1,63 @@ +use std::sync::RwLock; + +use figment::{ + providers::{Format, Toml}, + Figment, +}; +use matrix::ruma_common::OwnedServerName; +use serde::Deserialize; +use url::Url; + +use crate::util::secret::Secret; + +static CONFIG: RwLock> = RwLock::new(None); + +#[derive(Debug, Deserialize)] +pub struct Config { + pub registration_verification: bool, + + pub matrix: Matrix, + pub mail: SMTP, +} + +#[derive(Debug, Deserialize)] +pub struct SMTP { + pub host: OwnedServerName, + pub ports: [u16; 4], + pub username: Option, + pub password: Secret, + pub tls: bool, +} + +#[derive(Debug, Deserialize)] +pub struct Matrix { + pub host: Url, + pub server_name: OwnedServerName, + pub admin_token: Secret, + pub shared_registration_secret: Secret, +} + +pub fn config() -> &'static Config { + // CONFIG.unwrap_or_else(|| { + // Figment::new() + // .merge(Toml::file( + // std::env::var("COMMUNE_CONFIG").unwrap_or("commune.toml".to_owned()), + // )) + // .extract() + // .expect("could not extract config") + // }) +} + +pub fn extract() -> Result<(), figment::Error> { + let mut config = CONFIG.write().unwrap(); + + *config = Some( + Figment::new() + .merge(Toml::file( + std::env::var("COMMUNE_CONFIG").unwrap_or("commune.toml".to_owned()), + )) + .extract()?, + ); + + Ok(()) +} diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index cabbf65..03cee3f 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -1,27 +1,19 @@ -use http::StatusCode; use thiserror::Error; -use crate::{auth::error::AuthErrorCode, mail::error::MailErrorCode, room::error::RoomErrorCode}; - -pub type Result = std::result::Result; - -pub trait HttpStatusCode { - fn status_code(&self) -> StatusCode; - fn error_code(&self) -> &'static str; -} +pub(crate) type Result = std::result::Result; #[derive(Debug, Error)] +#[non_exhaustive] pub enum Error { - #[error("{0}")] - Auth(#[from] AuthErrorCode), - #[error("{0}")] - User(AccountErrorCode), - #[error("{0}")] - Room(RoomErrorCode), - #[error("{0}")] - Mail(#[from] MailErrorCode), - #[error("An error occured while starting up. {0}")] - Startup(String), - #[error("Unknown Error Occured")] - Unknown, + #[error("forwarding a Matrix request failed: {0}")] + Matrix(#[from] matrix::Error), + + #[error("an IO operation failed: {0}")] + IO(#[from] std::io::Error), + + #[error(transparent)] + SMTP(#[from] mail_send::Error), + + #[error(transparent)] + Unknown(#[from] anyhow::Error), } diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 78079ae..e8dbf2c 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -1,60 +1,29 @@ //! This library deals with our core logic, such as authorizing user //! interactions, forwarding regular events and constructing custom requests. -pub mod auth; +// pub mod auth; pub mod error; +pub mod config; pub mod mail; -pub mod room; -pub mod session; +// pub mod room; +// pub mod session; pub mod util; use std::sync::Arc; -pub use error::{Error, HttpStatusCode, Result}; +pub(crate) use error::Result; -use figment::{ - providers::{Format, Toml}, - Figment, -}; -use matrix::ruma_common::OwnedServerName; -use url::Url; +use tokio::sync::OnceCell; use util::secret::Secret; -pub struct Config { - pub smtp_host: Url, - pub redis_host: Url, - pub maildev_incoming_user: Option, - pub maildev_incoming_pass: Option, - pub synapse_host: Url, - pub synapse_admin_token: Secret, - pub synapse_server_name: OwnedServerName, - pub synapse_registration_shared_secret: String, -} - -impl Default for Config { - fn default() -> Self { - let config = Figment::new().merge(Toml::file( - std::env::var("COMMUNE_CONFIG").unwrap_or("commune.toml".to_owned()), - )); - - match config.extract::() { - Ok(s) => s, - Err(e) => { - eprintln!("Could not extract config: {e}"); - - std::process::exit(1); - } - } - } -} - pub struct Commune { - handle: Arc, - admin_token: Arc, + pub admin_token: Arc, + pub handle: Arc, } impl Commune { - pub fn new(config: Config) -> Result { + pub fn new() -> Result { + let config = CONFIG let handle = matrix::Handle::new(&config.synapse_host); Ok(Self { @@ -63,7 +32,6 @@ impl Commune { }) } } - // admin // .set_token(&config.synapse_admin_token) // .map_err(|err| { @@ -75,11 +43,11 @@ impl Commune { // let client = // redis::Client::open(config.redis_host.to_string()).map_err(|err| { // tracing::error!(?err, host=%config.redis_host.to_string(), -// "Failed to open connection to Redis"); +// "Failed to open connection to Redis"); // Error::Startup(err.to_string()) })?; // let mut conn = client.get_async_connection().await.map_err(|err| // { tracing::error!(?err, host=%config.redis_host.to_string(), -// "Failed to get connection to Redis"); +// "Failed to get connection to Redis"); // Error::Startup(err.to_string()) })?; // redis::cmd("PING").query_async(&mut conn).await.map_err(|err| { diff --git a/crates/core/src/mail/mod.rs b/crates/core/src/mail/mod.rs index 03141d7..09ba302 100644 --- a/crates/core/src/mail/mod.rs +++ b/crates/core/src/mail/mod.rs @@ -1,2 +1,284 @@ -pub mod error; -pub mod service; +use tokio::net::TcpStream; + +use mail_send::{ + mail_builder::{headers::address::EmailAddress, MessageBuilder}, + SmtpClient, SmtpClientBuilder, +}; +use matrix::ruma_common::ServerName; +use rand::Rng; + +use crate::error::Error; + +pub struct Mail { + conn: SmtpClient, +} + +impl Mail { + pub async fn build(host: &ServerName) -> SmtpClient { + tracing::warn!("using Mailtutan as email gateway, this is only meant for development"); + + for attempt in 1..=3 { + match SmtpClientBuilder::new(host.as_str(), 587) + .implicit_tls(false) + .credentials(("admin", "admin")) + .connect_plain() + .await + { + Ok(conn) => return conn, + Err(e) => { + if attempt >= 3 { + panic!("failed to connect to the SMTP host: {e}") + } + } + } + } + + unreachable!() + } + + pub async fn send_verification( + &mut self, + server_name: &ServerName, + recipient: EmailAddress<'_>, + ) -> Result<(), Error> { + let message = MessageBuilder::new() + .from(format!("onboarding@{}", server_name)) + .to(&*recipient.email) + .subject("Please verify your email address") + .html_body( + template::Verification::build( + server_name, + &recipient.name.expect("recipient name cannot be empty"), + rand::thread_rng().gen(), + ) + .into_string(), + ); + + self.conn.send(message).await.map_err(|e| { + tracing::error!(?e, "failed to send message to SMTP host"); + + Error::SMTP(e) + })?; + + Ok(()) + } +} + +mod template { + use matrix::ruma_common::ServerName; + use maud::{html, PreEscaped, DOCTYPE}; + + pub struct Verification; + + impl Verification { + pub fn build( + server_name: &ServerName, + username: &str, + code: [u8; 6], + ) -> PreEscaped { + html! { + (DOCTYPE) + html lang="en" { + header { + (PreEscaped(STYLESHEET)) + title { "Welcome to Commune" } + + meta charset="utf-8"; + meta name="viewport" content="width=device-width, initial-scale=1"; + } + body { + div ."halftone-div"; + section { + div .title { + h3 .header { + "Commune - " (username) ", welcome abroad" + } + h5 .greeting { + "We are glad that you have decided to join the network. \ + To finish setting up your account, enter this code in your client." + } + } + code { + span { + (code.iter().map(|n| char::from_digit(*n as u32, 10).expect("code should only contain digits")).collect::()) + } + } + footer { + p .disclaimer { + "If you didn't request a code, you can safely ignore this email." + } + p .contact { + "This email was sent to you by a" + (PreEscaped( + " \ + Commune instance " + )) + "If you have any questions, please get in touch with the administrator of " (server_name) "." + } + } + } + } + } + } + } + } + + const STYLESHEET: &str = r#" + + "#; +} diff --git a/crates/core/src/mail/service.rs b/crates/core/src/mail/service.rs deleted file mode 100644 index e3de8e4..0000000 --- a/crates/core/src/mail/service.rs +++ /dev/null @@ -1,128 +0,0 @@ -use handlebars::Handlebars; -use lettre::{message::header::ContentType, Message, SmtpTransport, Transport}; -use url::Url; - -use crate::{mail::error::MailErrorCode, util::secret::Secret, CommuneConfig, Result}; - -pub struct MailDevConfig { - pub smtp_host: Url, -} - -pub enum EmailProvider { - MailDev(MailDevConfig), -} - -impl EmailProvider { - pub fn new(config: &CommuneConfig) -> Self { - // TODO: Provide support for different providers via Config - tracing::warn!( - %config.smtp_host, - "Using MailDev as email provider! This is only for development!" - ); - - EmailProvider::MailDev(MailDevConfig { - smtp_host: config.smtp_host.to_owned(), - }) - } - - pub fn send_mail(&self, from: &str, to: &str, subject: &str, body: &str) -> Result<()> { - match self { - EmailProvider::MailDev(config) => { - let email = Message::builder() - .from(from.parse().unwrap()) - .to(to.parse().unwrap()) - .subject(subject) - .header(ContentType::TEXT_HTML) - .body(body.to_owned()) - .map_err(|err| { - tracing::error!(?err, "Failed to build email message"); - MailErrorCode::InvalidMailPayload(err) - })?; - - let mailer = SmtpTransport::from_url(config.smtp_host.as_ref()) - .map_err(|err| { - tracing::error!(?err, "Failed to build email message"); - MailErrorCode::SmtpConnection(err) - })? - .build(); - - mailer.send(&email).map_err(|err| { - tracing::error!(?err, "Failed to build email message"); - MailErrorCode::SmtpConnection(err) - })?; - - Ok(()) - } - } - } -} - -pub enum EmailTemplate { - VerificationCode { code: Secret }, -} - -impl EmailTemplate { - pub fn template(&self) -> &'static str { - match self { - EmailTemplate::VerificationCode { .. } => { - include_str!("templates/verification_code.hbs") - } - } - } - - pub fn subject(&self) -> String { - match self { - EmailTemplate::VerificationCode { .. } => "Verification Code".to_owned(), - } - } - - pub fn data(&self) -> serde_json::Value { - match self { - EmailTemplate::VerificationCode { code } => serde_json::json!({ - "code": code, - }), - } - } - - pub fn render(&self, hbs: &Handlebars<'static>) -> Result { - match self { - EmailTemplate::VerificationCode { .. } => { - let data = self.data(); - let template = self.template(); - let html = hbs.render_template(template, &data).map_err(|err| { - tracing::error!(?err, "Failed to render handlebars template"); - MailErrorCode::RenderHandlebars(err) - })?; - - Ok(html) - } - } - } -} - -pub struct MailService { - pub hbs: Handlebars<'static>, - pub provider: EmailProvider, -} - -impl MailService { - pub fn new(config: &CommuneConfig) -> Self { - let provider = EmailProvider::new(config); - let hbs = Handlebars::new(); - - Self { hbs, provider } - } - - pub async fn send_mail>( - &self, - from: S, - to: S, - template: EmailTemplate, - ) -> Result<()> { - let subject = template.subject(); - let body = template.render(&self.hbs)?; - - self.provider - .send_mail(from.as_ref(), to.as_ref(), subject.as_str(), body.as_str()) - } -} diff --git a/crates/core/src/mail/templates/verification_code.hbs b/crates/core/src/mail/templates/verification_code.hbs deleted file mode 100644 index e793662..0000000 --- a/crates/core/src/mail/templates/verification_code.hbs +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - - - - - - - - - - - - - - - -

- -
- - - - - - -
- -
- - - - - - - - - - - - - - - -
-

-

- -
-
Welcome Aboard!
-
-
This is your verification code, please don't share it with anyone!
-
-
{{code}}
-
-
- -
-
- -
- - - diff --git a/crates/core/src/util/mod.rs b/crates/core/src/util/mod.rs index fe66b81..73b12db 100644 --- a/crates/core/src/util/mod.rs +++ b/crates/core/src/util/mod.rs @@ -1,2 +1 @@ pub mod secret; -pub mod time; diff --git a/crates/core/src/util/time.rs b/crates/core/src/util/time.rs index 06a62e0..e722064 100644 --- a/crates/core/src/util/time.rs +++ b/crates/core/src/util/time.rs @@ -1,13 +1,3 @@ use std::time::{SystemTime, UNIX_EPOCH}; use crate::error::Error; - -pub fn timestamp() -> Result { - let start = SystemTime::now(); - let since_the_epoch = start.duration_since(UNIX_EPOCH).map_err(|err| { - tracing::error!(?err, "Failed to get timestamp"); - Error::Unknown - })?; - - Ok(since_the_epoch.as_secs()) -} diff --git a/crates/matrix/src/admin/room/get_room.rs b/crates/matrix/src/admin/room/get_room.rs index b7b604b..967f577 100644 --- a/crates/matrix/src/admin/room/get_room.rs +++ b/crates/matrix/src/admin/room/get_room.rs @@ -1,8 +1,8 @@ +use super::Room; use ruma_common::{ api::{request, response, Metadata}, metadata, OwnedRoomId, }; -use super::Room; #[allow(dead_code)] const METADATA: Metadata = metadata! { diff --git a/crates/matrix/src/client/room/create_room.rs b/crates/matrix/src/client/room/create_room.rs index ebf7e34..a74dde4 100644 --- a/crates/matrix/src/client/room/create_room.rs +++ b/crates/matrix/src/client/room/create_room.rs @@ -3,8 +3,9 @@ use std::collections::BTreeMap; use ruma_common::{ api::{request, response, Metadata}, metadata, + power_levels::NotificationPowerLevels, serde::Raw, - OwnedRoomId, OwnedUserId, power_levels::NotificationPowerLevels, + OwnedRoomId, OwnedUserId, }; use ruma_events::{AnyInitialStateEvent, TimelineEventType}; use serde::Serialize; diff --git a/crates/matrix/src/client/session/register.rs b/crates/matrix/src/client/session/register.rs index 505e8e9..285d57e 100644 --- a/crates/matrix/src/client/session/register.rs +++ b/crates/matrix/src/client/session/register.rs @@ -29,6 +29,17 @@ pub struct Request { pub refresh_token: Option, } +impl Request { + pub fn new(username: &str, password: &str, device_name: &str) -> Self { + Self { + username: username.to_owned(), + password: password.to_owned(), + device_name: device_name.to_owned(), + refresh_token: Some(true), + } + } +} + #[response(error = crate::Error)] pub struct Response { pub access_token: String, diff --git a/crates/matrix/src/lib.rs b/crates/matrix/src/lib.rs index cecff9a..72758a7 100644 --- a/crates/matrix/src/lib.rs +++ b/crates/matrix/src/lib.rs @@ -15,14 +15,13 @@ use async_trait::async_trait; use bytes::{Bytes, BytesMut}; use ruma_client::{HttpClient, HttpClientExt, ResponseResult}; -#[allow(unused_imports)] -use ruma_common::api::error::MatrixError as Error; - use ruma_common::api::{OutgoingRequest, SendAccessToken}; pub use ruma_common; pub use ruma_events; +pub type Error = ruma_common::api::error::MatrixError; + #[derive(Debug)] pub struct Handle { inner: reqwest::Client, diff --git a/crates/server/Cargo.toml b/crates/router/Cargo.toml similarity index 86% rename from crates/server/Cargo.toml rename to crates/router/Cargo.toml index efa7239..2acb656 100644 --- a/crates/server/Cargo.toml +++ b/crates/router/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "server" +name = "router" version = "0.0.0" edition = "2021" publish = false @@ -9,14 +9,13 @@ name = "commune-server" path = "src/bin/main.rs" [lib] -name = "server" +name = "router" path = "src/lib.rs" [dependencies] # Workspace Dependencies axum = { workspace = true, features = ["tokio"] } anyhow = { workspace = true } -dotenv = { workspace = true } http = { workspace = true } # openssl = { workspace = true, features = ["vendored"] } # openssl-sys = { workspace = true, features = ["vendored"] } @@ -25,7 +24,6 @@ tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] } tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } url = { workspace = true, features = ["serde"] } -uuid = { workspace = true, features = ["serde"] } # Local Dependencies core = { path = "../core" } diff --git a/crates/server/src/config.rs b/crates/router/src/config.rs similarity index 100% rename from crates/server/src/config.rs rename to crates/router/src/config.rs diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs new file mode 100644 index 0000000..c87c248 --- /dev/null +++ b/crates/router/src/lib.rs @@ -0,0 +1,38 @@ +use anyhow::Result; +use tokio::net::TcpListener; + +// pub mod config; +// pub mod router; +// pub mod services; + +// use crate::{config::ServerConfig, router::make_router, services::Services}; + +pub async fn serve(listener: TcpListener) -> Result<()> { + todo!() + + Router::new() + .route("/session", get(session::handler)) + .route_layer(middleware::from_fn(auth)) + .route("/", post(root::handler)) + .route("/login", get(login::get)) + .route("/login", post(login::post)) + .route("/login/sso/redirect", get(login::get)) + .route("/email/:email", get(email::handler)) + .nest( + "/verify", + Router::new() + .route("/code", post(verify_code::handler)) + .route("/code/email", post(verify_code_email::handler)), + ) + + // let config = ServerConfig::from_env(); + // let services = Services::shared(config).await?; + // let router = make_router(services); + + // if let Err(err) = axum::serve(listener, router.into_make_service()).await + // { tracing::error!(%err, "Failed to initialize the server"); + // panic!("An error ocurred running the server!"); + // } + + // Ok(()) +} diff --git a/crates/server/src/bin/main.rs b/crates/router/src/main.rs similarity index 68% rename from crates/server/src/bin/main.rs rename to crates/router/src/main.rs index 16a31bb..f67f4d4 100644 --- a/crates/server/src/bin/main.rs +++ b/crates/router/src/main.rs @@ -1,14 +1,13 @@ use std::net::SocketAddr; use anyhow::Result; -use dotenv::dotenv; use tokio::net::TcpListener; #[tokio::main] async fn main() -> Result<()> { - if dotenv().ok().is_some() { - println!("Loaded variables from .env file"); - } + // if dotenv().ok().is_some() { + // println!("Loaded variables from .env file"); + // } tracing_subscriber::fmt::init(); @@ -17,7 +16,7 @@ async fn main() -> Result<()> { tracing::info!("Listening on {}", addr); - commune_server::serve(tcp).await?; + router::serve(tcp).await?; Ok(()) } diff --git a/crates/server/src/router/api/mod.rs b/crates/router/src/router/api/mod.rs similarity index 100% rename from crates/server/src/router/api/mod.rs rename to crates/router/src/router/api/mod.rs diff --git a/crates/server/src/router/api/v1/account/email.rs b/crates/router/src/router/api/v1/account/email.rs similarity index 100% rename from crates/server/src/router/api/v1/account/email.rs rename to crates/router/src/router/api/v1/account/email.rs diff --git a/crates/server/src/router/api/v1/account/login.rs b/crates/router/src/router/api/v1/account/login.rs similarity index 100% rename from crates/server/src/router/api/v1/account/login.rs rename to crates/router/src/router/api/v1/account/login.rs diff --git a/crates/server/src/router/api/v1/account/mod.rs b/crates/router/src/router/api/v1/account/mod.rs similarity index 100% rename from crates/server/src/router/api/v1/account/mod.rs rename to crates/router/src/router/api/v1/account/mod.rs diff --git a/crates/server/src/router/api/v1/account/root.rs b/crates/router/src/router/api/v1/account/root.rs similarity index 100% rename from crates/server/src/router/api/v1/account/root.rs rename to crates/router/src/router/api/v1/account/root.rs diff --git a/crates/server/src/router/api/v1/account/session.rs b/crates/router/src/router/api/v1/account/session.rs similarity index 100% rename from crates/server/src/router/api/v1/account/session.rs rename to crates/router/src/router/api/v1/account/session.rs diff --git a/crates/server/src/router/api/v1/account/verify_code.rs b/crates/router/src/router/api/v1/account/verify_code.rs similarity index 100% rename from crates/server/src/router/api/v1/account/verify_code.rs rename to crates/router/src/router/api/v1/account/verify_code.rs diff --git a/crates/server/src/router/api/v1/account/verify_code_email.rs b/crates/router/src/router/api/v1/account/verify_code_email.rs similarity index 100% rename from crates/server/src/router/api/v1/account/verify_code_email.rs rename to crates/router/src/router/api/v1/account/verify_code_email.rs diff --git a/crates/server/src/router/api/v1/mod.rs b/crates/router/src/router/api/v1/mod.rs similarity index 100% rename from crates/server/src/router/api/v1/mod.rs rename to crates/router/src/router/api/v1/mod.rs diff --git a/crates/server/src/router/middleware/auth.rs b/crates/router/src/router/middleware/auth.rs similarity index 100% rename from crates/server/src/router/middleware/auth.rs rename to crates/router/src/router/middleware/auth.rs diff --git a/crates/server/src/router/middleware/mod.rs b/crates/router/src/router/middleware/mod.rs similarity index 100% rename from crates/server/src/router/middleware/mod.rs rename to crates/router/src/router/middleware/mod.rs diff --git a/crates/server/src/router/mod.rs b/crates/router/src/router/mod.rs similarity index 100% rename from crates/server/src/router/mod.rs rename to crates/router/src/router/mod.rs diff --git a/crates/server/src/services.rs b/crates/router/src/services.rs similarity index 100% rename from crates/server/src/services.rs rename to crates/router/src/services.rs diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs deleted file mode 100644 index 033730c..0000000 --- a/crates/server/src/lib.rs +++ /dev/null @@ -1,21 +0,0 @@ -use anyhow::Result; -use tokio::net::TcpListener; - -pub mod config; -pub mod router; -pub mod services; - -use crate::{config::ServerConfig, router::make_router, services::Services}; - -pub async fn serve(listener: TcpListener) -> Result<()> { - let config = ServerConfig::from_env(); - let services = Services::shared(config).await?; - let router = make_router(services); - - if let Err(err) = axum::serve(listener, router.into_make_service()).await { - tracing::error!(%err, "Failed to initialize the server"); - panic!("An error ocurred running the server!"); - } - - Ok(()) -} diff --git a/crates/test/Cargo.toml b/crates/test/Cargo.toml index f860aae..4b96a3f 100644 --- a/crates/test/Cargo.toml +++ b/crates/test/Cargo.toml @@ -12,16 +12,13 @@ path = "src/lib.rs" # Workspace Dependencies anyhow = { workspace = true } axum = { workspace = true, features = ["tokio"] } -dotenv = { workspace = true } reqwest = { workspace = true } # openssl = { workspace = true, features = ["vendored"] } serde = { workspace = true } -serde_json = { workspace = true } tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] } url = { workspace = true } -uuid = { workspace = true, features = ["serde"] } tracing = { workspace = true } -tracing-subscriber = { workspace = true, features = ["json"] } +tracing-subscriber = { workspace = true } # Local Dependencies core = { path = "../core" } From dfed96a6620c6e8846e4135f5c8e1e9eaeb7bdb5 Mon Sep 17 00:00:00 2001 From: mikoto Date: Thu, 14 Mar 2024 02:38:15 +0000 Subject: [PATCH 15/33] suspicious failed registration test --- Cargo.toml | 16 +++- commune-example.toml | 22 ++++-- crates/core/Cargo.toml | 1 + crates/core/src/auth/mod.rs | 31 -------- crates/core/src/config.rs | 38 +-------- crates/core/src/error.rs | 11 ++- crates/core/src/lib.rs | 46 +++++++---- crates/core/src/session.rs | 4 + crates/core/src/session/login.rs | 24 ++++++ crates/core/src/session/logout.rs | 12 +++ crates/core/src/session/mod.rs | 3 - crates/core/src/session/model.rs | 15 ---- crates/core/src/session/register.rs | 36 +++++++++ crates/core/src/session/service.rs | 5 +- crates/core/src/session/username_available.rs | 11 +++ crates/matrix/Cargo.toml | 15 +--- crates/matrix/src/admin/user/get_users.rs | 6 +- crates/matrix/src/client/room/get_rooms.rs | 6 +- crates/matrix/src/client/session.rs | 1 + crates/matrix/src/client/session/login.rs | 5 +- crates/matrix/src/client/session/logout.rs | 29 +++++++ crates/matrix/src/client/session/register.rs | 2 + .../src/client/session/username_available.rs | 10 +++ crates/matrix/src/lib.rs | 3 +- crates/router/Cargo.toml | 5 +- crates/router/src/api.rs | 1 + crates/router/src/api/session.rs | 4 + crates/router/src/api/session/login.rs | 27 +++++++ crates/router/src/api/session/logout.rs | 17 ++++ crates/router/src/api/session/register.rs | 27 +++++++ .../src/api/session/username_available.rs | 18 +++++ crates/router/src/config.rs | 13 --- crates/router/src/lib.rs | 79 +++++++++++-------- crates/router/src/main.rs | 15 +--- crates/router/src/router/middleware/auth.rs | 56 ------------- crates/router/src/router/middleware/mod.rs | 3 - crates/router/src/router/mod.rs | 12 --- crates/router/src/services.rs | 25 ------ crates/test/Cargo.toml | 5 +- crates/test/src/lib.rs | 17 ++-- crates/test/src/register.rs | 51 ++++++++++++ docker-compose.yml | 22 +++--- 42 files changed, 434 insertions(+), 315 deletions(-) create mode 100644 crates/core/src/session.rs create mode 100644 crates/core/src/session/login.rs create mode 100644 crates/core/src/session/logout.rs delete mode 100644 crates/core/src/session/mod.rs delete mode 100644 crates/core/src/session/model.rs create mode 100644 crates/core/src/session/register.rs create mode 100644 crates/core/src/session/username_available.rs create mode 100644 crates/matrix/src/client/session/logout.rs create mode 100644 crates/router/src/api.rs create mode 100644 crates/router/src/api/session.rs create mode 100644 crates/router/src/api/session/login.rs create mode 100644 crates/router/src/api/session/logout.rs create mode 100644 crates/router/src/api/session/register.rs create mode 100644 crates/router/src/api/session/username_available.rs delete mode 100644 crates/router/src/config.rs delete mode 100644 crates/router/src/router/middleware/auth.rs delete mode 100644 crates/router/src/router/middleware/mod.rs delete mode 100644 crates/router/src/router/mod.rs delete mode 100644 crates/router/src/services.rs create mode 100644 crates/test/src/register.rs diff --git a/Cargo.toml b/Cargo.toml index 27067ea..e28c050 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,8 +9,7 @@ repository = "https://github.com/commune-os/commune-rs" rust-version = "1.75.0" [workspace] -# members = ["crates/core", "crates/matrix", "crates/router", "crates/test"] -members = ["crates/core", "crates/matrix", "crates/router"] +members = ["crates/core", "crates/matrix", "crates/router", "crates/test"] default-members = ["crates/router"] resolver = "2" @@ -21,7 +20,7 @@ hex = "0.4.3" hmac = "0.12.1" sha1 = "0.10.6" anyhow = "1.0.75" -axum = { version = "0.7.4", features = ["tokio"] } +axum = { version = "0.7.4", features = ["tokio", "macros"] } http = "0.2.11" mime = "0.3.17" mail-send = "0.4.7" @@ -48,6 +47,17 @@ router = { workspace = true, path = "crates/router" } matrix = { workspace = true, path = "crates/matrix" } commune = { workspace = true, path = "crates/core" } +ruma-events = { version = "0.27.11", default_features = false, features = [ + "html", + "markdown", +] } +ruma-common = { version = "0.12.0", default_features = false, features = [ + "api", + "rand", +] } +ruma-macros = { version = "0.12.0", default_features = false } +ruma-client = { version = "0.12.0", default_features = false } + [workspace.lints.rust] unreachable_pub = "warn" unsafe_code = "forbid" diff --git a/commune-example.toml b/commune-example.toml index 5c52898..b8e294d 100644 --- a/commune-example.toml +++ b/commune-example.toml @@ -1,10 +1,18 @@ -[server] -listener = "local" # | "global" -ports = [1212, 2424] +registration_verification = false +public_loopback = false +port = 6421 tls = true # `X-Forwarded-For` header -xff = false +# xff = false -[registration] -enabled = false -verify_email = false +[matrix] +server_name = "matrix.localhost" +host = "http://0.0.0.0:8008" +admin_token = "syt_YWRtaW4_FllbTksPWcQaDRUVVcYR_3LJQZ2" +shared_registration_secret = "m@;wYOUOh0f:CH5XA65sJB1^q01~DmIriOysRImot,OR_vzN&B" + +[mail] +host = "smtp://0.0.0.0:1025" +username = "" +password = "" +tls = false diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index f5ea08d..9f3831c 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -11,6 +11,7 @@ path = "src/lib.rs" [dependencies] # Workspace Dependencies anyhow = { workspace = true } +axum = { workspace = true } rand = { workspace = true } thiserror = { workspace = true } validator = { workspace = true, features = ["derive"] } diff --git a/crates/core/src/auth/mod.rs b/crates/core/src/auth/mod.rs index 73b70ed..5668c41 100644 --- a/crates/core/src/auth/mod.rs +++ b/crates/core/src/auth/mod.rs @@ -49,37 +49,6 @@ impl Auth { Ok(AccessToken::new(resp.access_token)) } - pub async fn register(&self, username: &str, password: Secret) -> Result { - use session::register::*; - - let req = Request::new(username, password.inner(), "commune beta"); - - let resp: Response = self.handle.dispatch(None, req).await.unwrap(); - - Ok(AccessToken::new(resp.access_token)) - } - - pub async fn verify_email( - &self, - username: &str, - password: Secret, - email: &str, - ) -> Result { - use session::register::*; - - let verification_code: String = rand::thread_rng() - .gen::<[u32; 6]>() - .iter() - .map(|i| char::from_digit(i % 10, 10)) - .collect(); - - let req = Request::new(username, password.inner(), "commune beta"); - - let resp: Response = self.handle.dispatch(None, req).await.unwrap(); - - Ok(AccessToken::new(resp.access_token)) - } - // pub async fn get_login_flows(&self) -> Result { // match Login::get_login_flows(&self.admin).await { // Ok(flows) => Ok(flows), diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs index b464751..47db905 100644 --- a/crates/core/src/config.rs +++ b/crates/core/src/config.rs @@ -1,20 +1,14 @@ -use std::sync::RwLock; - -use figment::{ - providers::{Format, Toml}, - Figment, -}; use matrix::ruma_common::OwnedServerName; use serde::Deserialize; use url::Url; use crate::util::secret::Secret; -static CONFIG: RwLock> = RwLock::new(None); - #[derive(Debug, Deserialize)] pub struct Config { pub registration_verification: bool, + pub public_loopback: bool, + pub port: Option, pub matrix: Matrix, pub mail: SMTP, @@ -22,8 +16,7 @@ pub struct Config { #[derive(Debug, Deserialize)] pub struct SMTP { - pub host: OwnedServerName, - pub ports: [u16; 4], + pub host: Url, pub username: Option, pub password: Secret, pub tls: bool, @@ -36,28 +29,3 @@ pub struct Matrix { pub admin_token: Secret, pub shared_registration_secret: Secret, } - -pub fn config() -> &'static Config { - // CONFIG.unwrap_or_else(|| { - // Figment::new() - // .merge(Toml::file( - // std::env::var("COMMUNE_CONFIG").unwrap_or("commune.toml".to_owned()), - // )) - // .extract() - // .expect("could not extract config") - // }) -} - -pub fn extract() -> Result<(), figment::Error> { - let mut config = CONFIG.write().unwrap(); - - *config = Some( - Figment::new() - .merge(Toml::file( - std::env::var("COMMUNE_CONFIG").unwrap_or("commune.toml".to_owned()), - )) - .extract()?, - ); - - Ok(()) -} diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index 03cee3f..c699f08 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -1,12 +1,13 @@ +use axum::{http::StatusCode, response::IntoResponse}; use thiserror::Error; -pub(crate) type Result = std::result::Result; +pub type Result = std::result::Result; #[derive(Debug, Error)] #[non_exhaustive] pub enum Error { #[error("forwarding a Matrix request failed: {0}")] - Matrix(#[from] matrix::Error), + Matrix(#[from] matrix::HandleError), #[error("an IO operation failed: {0}")] IO(#[from] std::io::Error), @@ -17,3 +18,9 @@ pub enum Error { #[error(transparent)] Unknown(#[from] anyhow::Error), } + +impl IntoResponse for Error { + fn into_response(self) -> axum::response::Response { + (StatusCode::BAD_REQUEST, self.to_string()).into_response() + } +} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index e8dbf2c..67cb423 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -2,36 +2,50 @@ //! interactions, forwarding regular events and constructing custom requests. // pub mod auth; -pub mod error; pub mod config; +pub mod error; pub mod mail; +pub mod session; // pub mod room; -// pub mod session; pub mod util; -use std::sync::Arc; +use std::sync::{RwLock, Arc}; -pub(crate) use error::Result; +use config::Config; +use figment::{ + providers::{Env, Format, Toml}, + Figment, +}; -use tokio::sync::OnceCell; -use util::secret::Secret; +static COMMUNE: RwLock> = RwLock::new(None); pub struct Commune { - pub admin_token: Arc, + pub config: Config, pub handle: Arc, } -impl Commune { - pub fn new() -> Result { - let config = CONFIG - let handle = matrix::Handle::new(&config.synapse_host); +pub fn init() { + let mut commune = COMMUNE.write().unwrap(); + + let config = Figment::new() + .merge(Toml::file( + Env::var("COMMUNE_CONFIG").unwrap_or("./commune-example.toml".to_owned()), + )) + .extract::() + .unwrap(); + + let handle = Arc::new(matrix::Handle::new(&config.matrix.host)); - Ok(Self { - handle: Arc::new(handle), - admin_token: Arc::new(config.synapse_admin_token), - }) - } + *commune = Some(Box::leak(Box::new(Commune { config, handle }))); } + +pub fn commune() -> &'static Commune { + COMMUNE + .read() + .unwrap() + .expect("commune should be initialized at this point") +} + // admin // .set_token(&config.synapse_admin_token) // .map_err(|err| { diff --git a/crates/core/src/session.rs b/crates/core/src/session.rs new file mode 100644 index 0000000..943b11d --- /dev/null +++ b/crates/core/src/session.rs @@ -0,0 +1,4 @@ +pub mod login; +pub mod logout; +pub mod register; +pub mod username_available; diff --git a/crates/core/src/session/login.rs b/crates/core/src/session/login.rs new file mode 100644 index 0000000..10066ef --- /dev/null +++ b/crates/core/src/session/login.rs @@ -0,0 +1,24 @@ +use matrix::client::session::login::*; + +use crate::{error::Result, util::secret::Secret}; + +pub async fn service( + handle: &matrix::Handle, + username: &str, + password: &Secret, +) -> Result { + let req = Request::new( + LoginType::Password { + password: password.to_string(), + }, + Some(Identifier::User { + user: username.to_owned(), + }), + "commune".to_owned(), + Some(true), + ); + + let resp: Response = handle.dispatch(None, req).await?; + + Ok(resp) +} diff --git a/crates/core/src/session/logout.rs b/crates/core/src/session/logout.rs new file mode 100644 index 0000000..c69670b --- /dev/null +++ b/crates/core/src/session/logout.rs @@ -0,0 +1,12 @@ + +use matrix::client::session::logout::*; + +use crate::error::Result; + +pub async fn service(handle: &matrix::Handle) -> Result { + let req = Request::new(); + + let resp: Response = handle.dispatch(None, req).await?; + + Ok(resp) +} diff --git a/crates/core/src/session/mod.rs b/crates/core/src/session/mod.rs deleted file mode 100644 index e1001c6..0000000 --- a/crates/core/src/session/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod error; -pub mod model; -pub mod service; diff --git a/crates/core/src/session/model.rs b/crates/core/src/session/model.rs deleted file mode 100644 index cdc5d3e..0000000 --- a/crates/core/src/session/model.rs +++ /dev/null @@ -1,15 +0,0 @@ -use url::Url; - -use matrix::ruma_common::OwnedUserId; - -#[derive(Debug, Clone)] -pub struct Account { - pub user_id: OwnedUserId, - pub username: String, - pub email: String, - pub display_name: String, - pub avatar_url: Option, - pub age: i64, - pub admin: bool, - pub verified: bool, -} diff --git a/crates/core/src/session/register.rs b/crates/core/src/session/register.rs new file mode 100644 index 0000000..59e9616 --- /dev/null +++ b/crates/core/src/session/register.rs @@ -0,0 +1,36 @@ +use matrix::client::session::register::*; + +use crate::{error::Result, util::secret::Secret}; + +pub async fn service( + handle: &matrix::Handle, + username: &str, + password: &Secret, +) -> Result { + let req = Request::new(username, password.inner(), "commune"); + + let resp: Response = handle.dispatch(None, req).await?; + + Ok(resp) +} + +// pub async fn verify_email( +// handle: matrix::Handle, +// username: &str, +// password: Secret, +// email: &str, +// ) -> Result { +// use session::register::*; + +// let verification_code: String = rand::thread_rng() +// .gen::<[u32; 6]>() +// .iter() +// .flat_map(|i| char::from_digit(i % 10, 10)) +// .collect(); + +// let req = Request::new(username, password.inner(), "commune beta"); + +// let resp: Response = handle.dispatch(None, req).await?; + +// Ok(AccessToken::new(resp.access_token)) +// } diff --git a/crates/core/src/session/service.rs b/crates/core/src/session/service.rs index d0f99d0..8c46308 100644 --- a/crates/core/src/session/service.rs +++ b/crates/core/src/session/service.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use matrix::{ - admin::resources::user::UserService, client::resources::session::Session, ruma_common::UserId, + admin::resources::user::UserService, client::resources::session::Session, ruma_common::UserId, Handle, }; use tracing::instrument; use url::Url; @@ -99,8 +99,7 @@ impl CreateAccountDto { } pub struct AccountService { - admin: Arc, - auth: Arc, + matrix: Arc, mail: Arc, } diff --git a/crates/core/src/session/username_available.rs b/crates/core/src/session/username_available.rs new file mode 100644 index 0000000..7855c47 --- /dev/null +++ b/crates/core/src/session/username_available.rs @@ -0,0 +1,11 @@ +use matrix::client::session::username_available::*; + +use crate::error::Result; + +pub async fn service(handle: &matrix::Handle, username: &str) -> Result { + let req = Request::new(username); + + let resp: Response = handle.dispatch(None, req).await?; + + Ok(resp) +} diff --git a/crates/matrix/Cargo.toml b/crates/matrix/Cargo.toml index 0c7bcfd..734eb82 100644 --- a/crates/matrix/Cargo.toml +++ b/crates/matrix/Cargo.toml @@ -9,17 +9,10 @@ name = "matrix" path = "src/lib.rs" [dependencies] -ruma-events = { version = "0.27.11", default_features = false, features = [ - "html", - "markdown", -] } -ruma-common = { version = "0.12.0", default_features = false, features = [ - "api", - "rand", - "unstable-exhaustive-types", -] } -ruma-macros = { version = "0.12.0", default_features = false } -ruma-client = { version = "0.12.0", default_features = false } +ruma-events = { workspace = true } +ruma-common = { workspace = true } +ruma-macros = { workspace = true } +ruma-client = { workspace = true } # Workspace Dependencies mime = { workspace = true } diff --git a/crates/matrix/src/admin/user/get_users.rs b/crates/matrix/src/admin/user/get_users.rs index 867d548..ad094b8 100644 --- a/crates/matrix/src/admin/user/get_users.rs +++ b/crates/matrix/src/admin/user/get_users.rs @@ -53,11 +53,11 @@ pub struct Request { #[response(error = crate::Error)] pub struct Response { - users: Vec, + pub users: Vec, - next_token: String, + pub next_token: String, - total: u64, + pub total: u64, } #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)] diff --git a/crates/matrix/src/client/room/get_rooms.rs b/crates/matrix/src/client/room/get_rooms.rs index 5208a1a..b1f05d1 100644 --- a/crates/matrix/src/client/room/get_rooms.rs +++ b/crates/matrix/src/client/room/get_rooms.rs @@ -33,13 +33,13 @@ pub struct Request { #[response(error = crate::Error)] pub struct Response { - chunk: Vec, + pub chunk: Vec, #[serde(skip_serializing_if = "Option::is_none")] - next_batch: Option, + pub next_batch: Option, #[serde(skip_serializing_if = "Option::is_none")] - prev_batch: Option, + pub prev_batch: Option, } #[derive(Clone, Debug, Deserialize)] diff --git a/crates/matrix/src/client/session.rs b/crates/matrix/src/client/session.rs index 5ebf633..193cc54 100644 --- a/crates/matrix/src/client/session.rs +++ b/crates/matrix/src/client/session.rs @@ -4,6 +4,7 @@ pub mod get_flows; pub mod login; +pub mod logout; pub mod register; pub mod username_available; pub mod whoami; diff --git a/crates/matrix/src/client/session/login.rs b/crates/matrix/src/client/session/login.rs index 4f5933e..4cd795f 100644 --- a/crates/matrix/src/client/session/login.rs +++ b/crates/matrix/src/client/session/login.rs @@ -51,6 +51,7 @@ impl Request { } #[response(error = crate::Error)] +#[derive(Serialize)] pub struct Response { pub access_token: String, @@ -131,12 +132,12 @@ pub enum Identifier { Phone { country: String, phone: String }, } -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct BaseUrl { pub base_url: url::Url, } -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct WellKnown { #[serde(rename = "m.homeserver")] pub homeserver: BaseUrl, diff --git a/crates/matrix/src/client/session/logout.rs b/crates/matrix/src/client/session/logout.rs new file mode 100644 index 0000000..83f5796 --- /dev/null +++ b/crates/matrix/src/client/session/logout.rs @@ -0,0 +1,29 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, +}; +use serde::Serialize; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/v3/logout", + } +}; + +#[request(error = crate::Error)] +pub struct Request {} + +#[allow(clippy::new_without_default)] +impl Request { + pub fn new() -> Self { + Self {} + } +} + +#[response(error = crate::Error)] +#[derive(Serialize)] +pub struct Response {} diff --git a/crates/matrix/src/client/session/register.rs b/crates/matrix/src/client/session/register.rs index 285d57e..5e211f6 100644 --- a/crates/matrix/src/client/session/register.rs +++ b/crates/matrix/src/client/session/register.rs @@ -2,6 +2,7 @@ use ruma_common::{ api::{request, response, Metadata}, metadata, OwnedDeviceId, OwnedUserId, }; +use serde::{Serialize, Deserialize}; #[allow(dead_code)] const METADATA: Metadata = metadata! { @@ -41,6 +42,7 @@ impl Request { } #[response(error = crate::Error)] +#[derive(Deserialize, Serialize)] pub struct Response { pub access_token: String, diff --git a/crates/matrix/src/client/session/username_available.rs b/crates/matrix/src/client/session/username_available.rs index e380f6f..afa68fd 100644 --- a/crates/matrix/src/client/session/username_available.rs +++ b/crates/matrix/src/client/session/username_available.rs @@ -2,6 +2,7 @@ use ruma_common::{ api::{request, response, Metadata}, metadata, }; +use serde::Serialize; #[allow(dead_code)] const METADATA: Metadata = metadata! { @@ -18,7 +19,16 @@ pub struct Request { pub username: String, } +impl Request { + pub fn new(username: &str) -> Self { + Self { + username: username.to_owned(), + } + } +} + #[response(error = crate::Error)] +#[derive(Serialize)] pub struct Response { pub available: bool, } diff --git a/crates/matrix/src/lib.rs b/crates/matrix/src/lib.rs index 72758a7..f14d448 100644 --- a/crates/matrix/src/lib.rs +++ b/crates/matrix/src/lib.rs @@ -21,6 +21,7 @@ pub use ruma_common; pub use ruma_events; pub type Error = ruma_common::api::error::MatrixError; +pub type HandleError = ruma_client::Error; #[derive(Debug)] pub struct Handle { @@ -50,8 +51,6 @@ impl Handle { request, ) .await - .try_into() - .unwrap() } } diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 2acb656..c9b7672 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -6,7 +6,7 @@ publish = false [[bin]] name = "commune-server" -path = "src/bin/main.rs" +path = "src/main.rs" [lib] name = "router" @@ -14,7 +14,7 @@ path = "src/lib.rs" [dependencies] # Workspace Dependencies -axum = { workspace = true, features = ["tokio"] } +axum = { workspace = true, features = ["tokio", "macros"] } anyhow = { workspace = true } http = { workspace = true } # openssl = { workspace = true, features = ["vendored"] } @@ -27,3 +27,4 @@ url = { workspace = true, features = ["serde"] } # Local Dependencies core = { path = "../core" } +figment = { workspace = true, features = ["toml", "env"] } diff --git a/crates/router/src/api.rs b/crates/router/src/api.rs new file mode 100644 index 0000000..f52f1c4 --- /dev/null +++ b/crates/router/src/api.rs @@ -0,0 +1 @@ +pub mod session; diff --git a/crates/router/src/api/session.rs b/crates/router/src/api/session.rs new file mode 100644 index 0000000..943b11d --- /dev/null +++ b/crates/router/src/api/session.rs @@ -0,0 +1,4 @@ +pub mod login; +pub mod logout; +pub mod register; +pub mod username_available; diff --git a/crates/router/src/api/session/login.rs b/crates/router/src/api/session/login.rs new file mode 100644 index 0000000..03ea6e0 --- /dev/null +++ b/crates/router/src/api/session/login.rs @@ -0,0 +1,27 @@ +use axum::{ + response::{IntoResponse, Response}, + Json, +}; +use commune::util::secret::Secret; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Payload { + username: String, + password: Secret, +} + +pub async fn handler( + Json(payload): Json, +) -> Response { + use commune::session::login::service; + + match service(&commune::commune().handle, &payload.username, &payload.password).await { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to login user"); + + e.into_response() + } + } +} diff --git a/crates/router/src/api/session/logout.rs b/crates/router/src/api/session/logout.rs new file mode 100644 index 0000000..bcd383d --- /dev/null +++ b/crates/router/src/api/session/logout.rs @@ -0,0 +1,17 @@ +use axum::{ + response::{IntoResponse, Response}, + Json, +}; + +pub async fn handler() -> Response { + use commune::session::logout::service; + + match service(&commune::commune().handle).await { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to logout user"); + + e.into_response() + } + } +} diff --git a/crates/router/src/api/session/register.rs b/crates/router/src/api/session/register.rs new file mode 100644 index 0000000..35bece7 --- /dev/null +++ b/crates/router/src/api/session/register.rs @@ -0,0 +1,27 @@ +use axum::{ + response::{IntoResponse, Response}, + Json, +}; +use commune::util::secret::Secret; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Payload { + pub username: String, + pub password: Secret, +} + +pub async fn handler( + Json(payload): Json, +) -> Response { + use commune::session::register::service; + + match service(&commune::commune().handle, &payload.username, &payload.password).await { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to register user"); + + e.into_response() + } + } +} diff --git a/crates/router/src/api/session/username_available.rs b/crates/router/src/api/session/username_available.rs new file mode 100644 index 0000000..44b26e8 --- /dev/null +++ b/crates/router/src/api/session/username_available.rs @@ -0,0 +1,18 @@ +use axum::{ + extract::Path, + response::{IntoResponse, Response}, + Json, +}; + +pub async fn handler(Path(username): Path) -> Response { + use commune::session::username_available::service; + + match service(&commune::commune().handle, &username).await { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to check username availability"); + + e.into_response() + } + } +} diff --git a/crates/router/src/config.rs b/crates/router/src/config.rs deleted file mode 100644 index f61bbea..0000000 --- a/crates/router/src/config.rs +++ /dev/null @@ -1,13 +0,0 @@ -use commune::CommuneConfig; - -pub struct ServerConfig { - pub commune_config: CommuneConfig, -} - -impl ServerConfig { - pub fn from_env() -> ServerConfig { - ServerConfig { - commune_config: CommuneConfig::new(), - } - } -} diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index c87c248..df7f5e0 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -1,38 +1,47 @@ -use anyhow::Result; +use std::net::SocketAddr; + +use axum::{ + routing::{get, post}, + Router, +}; use tokio::net::TcpListener; -// pub mod config; -// pub mod router; -// pub mod services; - -// use crate::{config::ServerConfig, router::make_router, services::Services}; - -pub async fn serve(listener: TcpListener) -> Result<()> { - todo!() - - Router::new() - .route("/session", get(session::handler)) - .route_layer(middleware::from_fn(auth)) - .route("/", post(root::handler)) - .route("/login", get(login::get)) - .route("/login", post(login::post)) - .route("/login/sso/redirect", get(login::get)) - .route("/email/:email", get(email::handler)) - .nest( - "/verify", - Router::new() - .route("/code", post(verify_code::handler)) - .route("/code/email", post(verify_code_email::handler)), - ) - - // let config = ServerConfig::from_env(); - // let services = Services::shared(config).await?; - // let router = make_router(services); - - // if let Err(err) = axum::serve(listener, router.into_make_service()).await - // { tracing::error!(%err, "Failed to initialize the server"); - // panic!("An error ocurred running the server!"); - // } - - // Ok(()) +pub mod api; + +pub async fn routes() -> Router { + Router::new() + .route("/", get(|| async { "hello commune!" })) + .nest( + "/_commune/client/r0", + Router::new() + .nest( + "/register", + Router::new() + .route("/", post(api::session::register::handler)) + .route( + "/username/:username", + get(api::session::username_available::handler), + ), + ) + .route("/login", post(api::session::login::handler)) + .route("/logout", post(api::session::logout::handler)), + ) +} + +pub async fn serve(public_loopback: bool, port: u16) -> anyhow::Result<()> { + let host = match public_loopback { + true => [0, 0, 0, 0], + false => [127, 0, 0, 1], + }; + + let addr = SocketAddr::from((host, port)); + let tcp_listener = TcpListener::bind(addr).await?; + + tracing::info!("Listening on {}", addr); + + let router = routes().await; + + axum::serve(tcp_listener, router.into_make_service()) + .await + .map_err(Into::into) } diff --git a/crates/router/src/main.rs b/crates/router/src/main.rs index f67f4d4..11ecc44 100644 --- a/crates/router/src/main.rs +++ b/crates/router/src/main.rs @@ -1,22 +1,13 @@ -use std::net::SocketAddr; - use anyhow::Result; -use tokio::net::TcpListener; #[tokio::main] async fn main() -> Result<()> { - // if dotenv().ok().is_some() { - // println!("Loaded variables from .env file"); - // } + commune::init(); + let config = &commune::commune().config; tracing_subscriber::fmt::init(); - let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); - let tcp = TcpListener::bind(addr).await?; - - tracing::info!("Listening on {}", addr); - - router::serve(tcp).await?; + router::serve(config.public_loopback, config.port.unwrap()).await?; Ok(()) } diff --git a/crates/router/src/router/middleware/auth.rs b/crates/router/src/router/middleware/auth.rs deleted file mode 100644 index 7c12dd3..0000000 --- a/crates/router/src/router/middleware/auth.rs +++ /dev/null @@ -1,56 +0,0 @@ -use axum::{ - body::Body, - http::{header::AUTHORIZATION, Request}, - middleware::Next, - response::{IntoResponse, Response}, -}; - -use commune::util::secret::Secret; - -use crate::{router::api::ApiError, services::SharedServices}; - -#[derive(Debug, Clone)] -pub struct AccessToken(Secret); - -impl ToString for AccessToken { - fn to_string(&self) -> String { - self.0.to_string() - } -} - -pub async fn auth(mut request: Request, next: Next) -> Result { - let access_token = request - .headers() - .get(AUTHORIZATION) - .and_then(|value| value.to_str().ok()) - .and_then(|value| value.strip_prefix("Bearer ")) - .ok_or_else(|| { - tracing::warn!("No access token provided"); - ApiError::unauthorized().into_response() - })? - .to_owned(); - - let services = request - .extensions() - .get::() - .ok_or_else(|| { - tracing::error!("SharedServices not found in request extensions"); - ApiError::internal_server_error().into_response() - })?; - - let access_token = Secret::new(access_token); - let user = services - .commune - .account - .whoami(&access_token) - .await - .map_err(|err| { - tracing::error!("Failed to validate token: {}", err); - ApiError::internal_server_error().into_response() - })?; - - request.extensions_mut().insert(user); - request.extensions_mut().insert(AccessToken(access_token)); - - Ok(next.run(request).await) -} diff --git a/crates/router/src/router/middleware/mod.rs b/crates/router/src/router/middleware/mod.rs deleted file mode 100644 index b41b0fe..0000000 --- a/crates/router/src/router/middleware/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod auth; - -pub use auth::{auth, AccessToken}; diff --git a/crates/router/src/router/mod.rs b/crates/router/src/router/mod.rs deleted file mode 100644 index 11ade43..0000000 --- a/crates/router/src/router/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -pub mod api; -pub mod middleware; - -use axum::{Extension, Router}; - -use crate::services::SharedServices; - -pub fn make_router(service: SharedServices) -> Router { - Router::new() - .merge(api::Api::routes()) - .layer(Extension(service)) -} diff --git a/crates/router/src/services.rs b/crates/router/src/services.rs deleted file mode 100644 index 8cae949..0000000 --- a/crates/router/src/services.rs +++ /dev/null @@ -1,25 +0,0 @@ -use std::sync::Arc; - -use anyhow::Result; - -use commune::Commune; - -use crate::config::ServerConfig; - -pub type SharedServices = Arc; - -pub struct Services { - pub commune: Commune, -} - -impl Services { - pub async fn new(config: ServerConfig) -> Result { - let commune = Commune::new(config.commune_config).await?; - - Ok(Self { commune }) - } - - pub async fn shared(config: ServerConfig) -> Result { - Ok(Arc::new(Self::new(config).await?)) - } -} diff --git a/crates/test/Cargo.toml b/crates/test/Cargo.toml index 4b96a3f..ac15b88 100644 --- a/crates/test/Cargo.toml +++ b/crates/test/Cargo.toml @@ -12,8 +12,7 @@ path = "src/lib.rs" # Workspace Dependencies anyhow = { workspace = true } axum = { workspace = true, features = ["tokio"] } -reqwest = { workspace = true } -# openssl = { workspace = true, features = ["vendored"] } +reqwest = { workspace = true, features = ["json"] } serde = { workspace = true } tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] } url = { workspace = true } @@ -23,4 +22,4 @@ tracing-subscriber = { workspace = true } # Local Dependencies core = { path = "../core" } matrix = { path = "../matrix" } -server = { path = "../server" } +router = { path = "../router" } diff --git a/crates/test/src/lib.rs b/crates/test/src/lib.rs index 187ab1b..61a07c5 100644 --- a/crates/test/src/lib.rs +++ b/crates/test/src/lib.rs @@ -1,11 +1,14 @@ -#[cfg(test)] -mod commune; +// #[cfg(test)] +// mod commune; -#[cfg(test)] -mod tools; +// #[cfg(test)] +// mod tools; -#[cfg(test)] -mod matrix; +// #[cfg(test)] +// mod matrix; + +// #[cfg(test)] +// mod server; #[cfg(test)] -mod server; +mod register; diff --git a/crates/test/src/register.rs b/crates/test/src/register.rs new file mode 100644 index 0000000..3d70a37 --- /dev/null +++ b/crates/test/src/register.rs @@ -0,0 +1,51 @@ +use std::time::Duration; + +use commune::util::secret::Secret; + +#[tokio::test] +async fn register() { + tracing_subscriber::fmt().init(); + + commune::init(); + + let public_loopback = commune::commune().config.public_loopback; + let port = commune::commune().config.port; + + tokio::spawn(async move { + router::serve(public_loopback, port.unwrap()) + .await + .expect("failed to start server"); + }); + + let client = reqwest::Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .build() + .unwrap(); + + let addr = url::Url::parse(&format!( + "http://127.0.0.1:{}/_commune/client/r0/register", + port.unwrap() + )) + .unwrap(); + + let resp = client + .post(addr) + .json(&router::api::session::register::Payload { + username: "steve".to_owned(), + password: Secret::new("verysecure"), + }) + .send() + .await + .unwrap(); + + dbg!(resp.status()); + + if resp.status().is_success() { + let resp = resp + .json::() + .await + .unwrap(); + + assert!(!resp.access_token.is_empty()); + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 88c074a..6d8a4dd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,11 @@ version: '3' services: - maildev: - image: 'maildev/maildev' - ports: - - '1080:1080' - - '1025:1025' + # maildev: + # image: 'maildev/maildev' + # ports: + # - '1080:1080' + # - '1025:1025' redis: image: 'redis/redis-stack' @@ -13,14 +13,14 @@ services: - '6379:6379' - '8001:8001' volumes: - - redis-state:/data + - redis-db:/data - synapse_database: + synapse-db: image: 'postgres:16' ports: - '5432:5432' volumes: - - synapse_database:/var/lib/postgresql/data + - synapse-db:/var/lib/postgresql/data env_file: - .env restart: always @@ -38,8 +38,8 @@ services: restart: always network_mode: 'host' depends_on: - - synapse_database + - synapse-db volumes: - redis-state: - synapse_database: + redis-db: + synapse-db: From 3c8341df68e26cc29014b4524f251e51e9906983 Mon Sep 17 00:00:00 2001 From: mikoto Date: Mon, 18 Mar 2024 03:19:06 +0000 Subject: [PATCH 16/33] chore: requirement to automate the dummy auth flow --- crates/core/Cargo.toml | 4 +- crates/core/src/lib.rs | 52 +-------------- crates/router/src/lib.rs | 2 +- crates/test/Cargo.toml | 1 + crates/test/fixtures/synapse/homeserver.yaml | 2 + crates/test/src/env.rs | 66 ++++++++++++++++++++ crates/test/src/lib.rs | 3 + crates/test/src/register.rs | 30 ++------- crates/test/src/tools/http.rs | 2 +- crates/test/src/util.rs | 1 + 10 files changed, 82 insertions(+), 81 deletions(-) create mode 100644 crates/test/src/env.rs create mode 100644 crates/test/src/util.rs diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 9f3831c..64a0f37 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -16,8 +16,8 @@ rand = { workspace = true } thiserror = { workspace = true } validator = { workspace = true, features = ["derive"] } http = { workspace = true } -mail-send = {workspace = true} -maud = {workspace = true} +mail-send = { workspace = true } +maud = { workspace = true } serde = { workspace = true, features = ["derive"] } tracing = { workspace = true } figment = { workspace = true } diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 67cb423..e4e8066 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -9,7 +9,7 @@ pub mod session; // pub mod room; pub mod util; -use std::sync::{RwLock, Arc}; +use std::sync::{Arc, RwLock}; use config::Config; use figment::{ @@ -45,53 +45,3 @@ pub fn commune() -> &'static Commune { .unwrap() .expect("commune should be initialized at this point") } - -// admin -// .set_token(&config.synapse_admin_token) -// .map_err(|err| { -// tracing::error!(?err, "Failed to set admin token"); -// Error::Startup(err.to_string()) -// })?; - -// let redis = { -// let client = -// redis::Client::open(config.redis_host.to_string()).map_err(|err| { -// tracing::error!(?err, host=%config.redis_host.to_string(), -// "Failed to open connection to Redis"); -// Error::Startup(err.to_string()) })?; -// let mut conn = client.get_async_connection().await.map_err(|err| -// { tracing::error!(?err, host=%config.redis_host.to_string(), -// "Failed to get connection to Redis"); -// Error::Startup(err.to_string()) })?; - -// redis::cmd("PING").query_async(&mut conn).await.map_err(|err| { -// tracing::error!(?err, host=%config.redis_host.to_string(), -// "Failed to ping Redis"); Error::Startup(err.to_string()) -// })?; - -// tracing::info!(host=%config.redis_host.to_string(), "Connected to -// Redis"); - -// Arc::new(client) -// }; - -// let admin_client = Arc::new(admin); -// let auth = Arc::new(AuthService::new( -// Arc::clone(&admin_client), -// Arc::clone(&redis), -// )); -// let mail = Arc::new(MailService::new(&config)); -// let account = AccountService::new( -// Arc::clone(&admin_client), -// Arc::clone(&auth), -// Arc::clone(&mail), -// ); -// let room = RoomService::new(Arc::clone(&admin_client)); - -// Ok(Self { -// account: Arc::new(account), -// auth, -// room: Arc::new(room), -// }) -// } -// } diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index df7f5e0..9f6454a 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -10,7 +10,7 @@ pub mod api; pub async fn routes() -> Router { Router::new() - .route("/", get(|| async { "hello commune!" })) + .route("/", get(|| async { "hello from commune!" })) .nest( "/_commune/client/r0", Router::new() diff --git a/crates/test/Cargo.toml b/crates/test/Cargo.toml index ac15b88..185b886 100644 --- a/crates/test/Cargo.toml +++ b/crates/test/Cargo.toml @@ -15,6 +15,7 @@ axum = { workspace = true, features = ["tokio"] } reqwest = { workspace = true, features = ["json"] } serde = { workspace = true } tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] } +thiserror = { workspace = true } url = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } diff --git a/crates/test/fixtures/synapse/homeserver.yaml b/crates/test/fixtures/synapse/homeserver.yaml index 11fe33b..a57c4e5 100644 --- a/crates/test/fixtures/synapse/homeserver.yaml +++ b/crates/test/fixtures/synapse/homeserver.yaml @@ -11,6 +11,7 @@ # https://matrix-org.github.io/synapse/latest/usage/configuration/config_documentation.html server_name: "matrix.localhost" pid_file: /data/homeserver.pid + listeners: - port: 8008 tls: false @@ -79,4 +80,5 @@ rc_invites: per_second: 1000 burst_count: 1000 +enable_registration: true # vim:ft=yaml diff --git a/crates/test/src/env.rs b/crates/test/src/env.rs new file mode 100644 index 0000000..d054f2b --- /dev/null +++ b/crates/test/src/env.rs @@ -0,0 +1,66 @@ +use std::net::SocketAddr; + +pub(crate) struct Env { + pub client: reqwest::Client, + pub loopback: SocketAddr, +} + +impl Env { + pub(crate) async fn new() -> Self { + tracing_subscriber::fmt().init(); + + commune::init(); + + let loopback = SocketAddr::from(( + match commune::commune().config.public_loopback { + true => [0, 0, 0, 0], + false => [127, 0, 0, 1], + }, + 5357, + )); + + tokio::spawn(async move { + tracing::info!("starting development server on {:?}", loopback); + + router::serve(commune::commune().config.public_loopback, 5357) + .await + .expect("failed to bind to address"); + }); + + let client = reqwest::Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .build() + .unwrap(); + + + if let Err(e) = client + .get(commune::commune().config.matrix.host.to_string() + "/_matrix/client/versions") + .send() + .await + { + tracing::error!( + "could not connect to Matrix: {e}\n is the testing environment running?" + ); + + std::process::exit(1); + } + + Self { client, loopback } + } + + fn path(&self, path: &str) -> String { + format!("http://{}{}", self.loopback, path) + } + + pub(crate) fn get(&self, url: &str) -> reqwest::RequestBuilder { + tracing::info!("GET {}", self.path(url)); + + self.client.get(self.path(url)) + } + + pub(crate) fn post(&self, url: &str) -> reqwest::RequestBuilder { + tracing::info!("POST {}", self.path(url)); + + self.client.post(self.path(url)) + } +} diff --git a/crates/test/src/lib.rs b/crates/test/src/lib.rs index 61a07c5..3a2d480 100644 --- a/crates/test/src/lib.rs +++ b/crates/test/src/lib.rs @@ -12,3 +12,6 @@ #[cfg(test)] mod register; + +#[cfg(test)] +mod env; diff --git a/crates/test/src/register.rs b/crates/test/src/register.rs index 3d70a37..ca01f4e 100644 --- a/crates/test/src/register.rs +++ b/crates/test/src/register.rs @@ -1,35 +1,13 @@ -use std::time::Duration; - use commune::util::secret::Secret; +use crate::env::Env; + #[tokio::test] async fn register() { - tracing_subscriber::fmt().init(); - - commune::init(); - - let public_loopback = commune::commune().config.public_loopback; - let port = commune::commune().config.port; - - tokio::spawn(async move { - router::serve(public_loopback, port.unwrap()) - .await - .expect("failed to start server"); - }); - - let client = reqwest::Client::builder() - .redirect(reqwest::redirect::Policy::none()) - .build() - .unwrap(); - - let addr = url::Url::parse(&format!( - "http://127.0.0.1:{}/_commune/client/r0/register", - port.unwrap() - )) - .unwrap(); + let client = Env::new().await; let resp = client - .post(addr) + .post("/_commune/client/r0/register") .json(&router::api::session::register::Payload { username: "steve".to_owned(), password: Secret::new("verysecure"), diff --git a/crates/test/src/tools/http.rs b/crates/test/src/tools/http.rs index fcd92e7..8464911 100644 --- a/crates/test/src/tools/http.rs +++ b/crates/test/src/tools/http.rs @@ -6,7 +6,7 @@ use tokio::net::TcpListener; use commune_server::serve; -pub(crate) struct HttpClient { +pub(crate) struct HTTP { pub client: Client, pub addr: SocketAddr, } diff --git a/crates/test/src/util.rs b/crates/test/src/util.rs new file mode 100644 index 0000000..8074a0f --- /dev/null +++ b/crates/test/src/util.rs @@ -0,0 +1 @@ +mod http; From ca8393a0ed837907348b6fe116d4c39301171081 Mon Sep 17 00:00:00 2001 From: mikoto Date: Sat, 23 Mar 2024 16:40:19 +0000 Subject: [PATCH 17/33] feat: skip auth flows that only contain a dummy stage --- Cargo.toml | 1 + crates/core/Cargo.toml | 1 + crates/core/src/lib.rs | 50 +++++++ crates/core/src/session/register.rs | 49 ++++++- crates/core/src/{util/mod.rs => util.rs} | 0 crates/matrix/Cargo.toml | 1 + crates/matrix/src/client/mod.rs | 1 + crates/matrix/src/client/session.rs | 1 + crates/matrix/src/client/session/register.rs | 14 +- crates/matrix/src/client/uiaa.rs | 142 +++++++++++++++++++ crates/matrix/src/lib.rs | 1 + crates/test/Cargo.toml | 1 + crates/test/src/register.rs | 17 ++- 13 files changed, 264 insertions(+), 15 deletions(-) rename crates/core/src/{util/mod.rs => util.rs} (100%) create mode 100644 crates/matrix/src/client/uiaa.rs diff --git a/Cargo.toml b/Cargo.toml index e28c050..e84eddd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ reqwest = { version = "0.11.22", default-features = false, features = [ "rustls", ] } serde = "1.0.192" +serde_json = "1.0.114" time = "0.3.34" tokio = "1.34.0" tracing = "0.1.40" diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 64a0f37..6f7dba1 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -19,6 +19,7 @@ http = { workspace = true } mail-send = { workspace = true } maud = { workspace = true } serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } tracing = { workspace = true } figment = { workspace = true } url = { workspace = true, features = ["serde"] } diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index e4e8066..f7fc5bc 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -45,3 +45,53 @@ pub fn commune() -> &'static Commune { .unwrap() .expect("commune should be initialized at this point") } + +// admin +// .set_token(&config.synapse_admin_token) +// .map_err(|err| { +// tracing::error!(?err, "Failed to set admin token"); +// Error::Startup(err.to_string()) +// })?; + +// let redis = { +// let client = +// redis::Client::open(config.redis_host.to_string()).map_err(|err| { +// tracing::error!(?err, host=%config.redis_host.to_string(), +// "Failed to open connection to Redis"); +// Error::Startup(err.to_string()) })?; +// let mut conn = client.get_async_connection().await.map_err(|err| +// { tracing::error!(?err, host=%config.redis_host.to_string(), +// "Failed to get connection to Redis"); +// Error::Startup(err.to_string()) })?; + +// redis::cmd("PING").query_async(&mut conn).await.map_err(|err| { +// tracing::error!(?err, host=%config.redis_host.to_string(), +// "Failed to ping Redis"); Error::Startup(err.to_string()) +// })?; + +// tracing::info!(host=%config.redis_host.to_string(), "Connected to +// Redis"); + +// Arc::new(client) +// }; + +// let admin_client = Arc::new(admin); +// let auth = Arc::new(AuthService::new( +// Arc::clone(&admin_client), +// Arc::clone(&redis), +// )); +// let mail = Arc::new(MailService::new(&config)); +// let account = AccountService::new( +// Arc::clone(&admin_client), +// Arc::clone(&auth), +// Arc::clone(&mail), +// ); +// let room = RoomService::new(Arc::clone(&admin_client)); + +// Ok(Self { +// account: Arc::new(account), +// auth, +// room: Arc::new(room), +// }) +// } +// } diff --git a/crates/core/src/session/register.rs b/crates/core/src/session/register.rs index 59e9616..357915b 100644 --- a/crates/core/src/session/register.rs +++ b/crates/core/src/session/register.rs @@ -1,4 +1,12 @@ -use matrix::client::session::register::*; +use http::StatusCode; +use matrix::{ + client::{ + session::register::*, + uiaa::{ AuthType, UiaaRequest, UiaaResponse}, + }, + ruma_client::Error::FromHttpResponse, + ruma_common::api::error::{FromHttpResponseError, MatrixError, MatrixErrorBody}, +}; use crate::{error::Result, util::secret::Secret}; @@ -7,11 +15,44 @@ pub async fn service( username: &str, password: &Secret, ) -> Result { - let req = Request::new(username, password.inner(), "commune"); + let req = Request::new(username, password.inner(), "commune", None); - let resp: Response = handle.dispatch(None, req).await?; + let resp = handle.dispatch(None, req).await; - Ok(resp) + match resp { + Ok(resp) => Ok(resp), + Err(e) => match e { + FromHttpResponse(FromHttpResponseError::Server(MatrixError { + status_code: StatusCode::UNAUTHORIZED, + body: MatrixErrorBody::Json(ref body), + })) => { + let UiaaResponse { flows, session, .. } = + serde_json::from_value::(body.clone()).unwrap(); + + match flows.as_slice() { + [value] => match value.stages.as_slice() { + [AuthType::Dummy] => { + let req = Request::new( + username, + password.inner(), + "commune", + Some(UiaaRequest { + session, + kind: AuthType::Dummy, + }), + ); + + handle.dispatch(None, req).await.map_err(Into::into) + } + _ => Err(e.into()), + }, + _ => Err(e.into()), + } + } + + _ => Err(e.into()), + }, + } } // pub async fn verify_email( diff --git a/crates/core/src/util/mod.rs b/crates/core/src/util.rs similarity index 100% rename from crates/core/src/util/mod.rs rename to crates/core/src/util.rs diff --git a/crates/matrix/Cargo.toml b/crates/matrix/Cargo.toml index 734eb82..f5dcae6 100644 --- a/crates/matrix/Cargo.toml +++ b/crates/matrix/Cargo.toml @@ -18,6 +18,7 @@ ruma-client = { workspace = true } mime = { workspace = true } reqwest = { workspace = true, features = ["json"] } serde = { workspace = true } +serde_json = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } sha1 = { workspace = true } diff --git a/crates/matrix/src/client/mod.rs b/crates/matrix/src/client/mod.rs index 196920e..b00a57e 100644 --- a/crates/matrix/src/client/mod.rs +++ b/crates/matrix/src/client/mod.rs @@ -4,3 +4,4 @@ pub mod room; pub mod session; +pub mod uiaa; diff --git a/crates/matrix/src/client/session.rs b/crates/matrix/src/client/session.rs index 193cc54..98a2995 100644 --- a/crates/matrix/src/client/session.rs +++ b/crates/matrix/src/client/session.rs @@ -6,5 +6,6 @@ pub mod get_flows; pub mod login; pub mod logout; pub mod register; +// pub mod uiaa; pub mod username_available; pub mod whoami; diff --git a/crates/matrix/src/client/session/register.rs b/crates/matrix/src/client/session/register.rs index 5e211f6..210cfe5 100644 --- a/crates/matrix/src/client/session/register.rs +++ b/crates/matrix/src/client/session/register.rs @@ -2,7 +2,9 @@ use ruma_common::{ api::{request, response, Metadata}, metadata, OwnedDeviceId, OwnedUserId, }; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; + +use crate::client::uiaa::UiaaRequest; #[allow(dead_code)] const METADATA: Metadata = metadata! { @@ -28,15 +30,23 @@ pub struct Request { #[serde(skip_serializing_if = "Option::is_none")] pub refresh_token: Option, + + /// Note that this information is not used to define how the registered user should be + /// authenticated, but is instead used to authenticate the register call itself. + /// It should be left empty, or omitted, unless an earlier call returned an response + /// with status code 401. + #[serde(skip_serializing_if = "Option::is_none")] + pub auth: Option, } impl Request { - pub fn new(username: &str, password: &str, device_name: &str) -> Self { + pub fn new(username: &str, password: &str, device_name: &str, auth: Option) -> Self { Self { username: username.to_owned(), password: password.to_owned(), device_name: device_name.to_owned(), refresh_token: Some(true), + auth } } } diff --git a/crates/matrix/src/client/uiaa.rs b/crates/matrix/src/client/uiaa.rs new file mode 100644 index 0000000..af0cd80 --- /dev/null +++ b/crates/matrix/src/client/uiaa.rs @@ -0,0 +1,142 @@ +//! Module for [User-Interactive Authentication API][uiaa] types. +//! +//! [uiaa]: https://spec.matrix.org/latest/client-server-api/#user-interactive-authentication-api + +use ruma_common::{serde::JsonObject, OwnedSessionId}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct UiaaResponse { + pub flows: Vec, + + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub completed: Vec, + + pub params: Box, + + #[serde(skip_serializing_if = "Option::is_none")] + pub session: Option, + // #[serde(flatten, skip_serializing_if = "Option::is_none")] + // pub auth_error: Option, +} + +/// Ordered list of stages required to complete authentication. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct AuthFlow { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub stages: Vec, +} + +impl AuthFlow { + pub fn new(stages: Vec) -> Self { + Self { stages } + } +} + +/// Information for one authentication stage. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[non_exhaustive] +pub enum AuthType { + /// Password-based authentication (`m.login.password`). + #[serde(rename = "m.login.password")] + Password, + + /// Google ReCaptcha 2.0 authentication (`m.login.recaptcha`). + #[serde(rename = "m.login.recaptcha")] + ReCaptcha, + + /// Email-based authentication (`m.login.email.identity`). + #[serde(rename = "m.login.email.identity")] + EmailIdentity, + + /// Phone number-based authentication (`m.login.msisdn`). + #[serde(rename = "m.login.msisdn")] + Msisdn, + + /// SSO-based authentication (`m.login.sso`). + #[serde(rename = "m.login.sso")] + Sso, + + /// Dummy authentication (`m.login.dummy`). + #[serde(rename = "m.login.dummy")] + Dummy, + + /// Registration token-based authentication (`m.login.registration_token`). + #[serde(rename = "m.login.registration_token")] + RegistrationToken, +} + +#[derive(Clone, Serialize)] +#[non_exhaustive] +#[serde(untagged)] +pub enum AuthData { + // Password-based authentication (`m.login.password`). + // Password(Password), + + // Google ReCaptcha 2.0 authentication (`m.login.recaptcha`). + // ReCaptcha(ReCaptcha), + + // Email-based authentication (`m.login.email.identity`). + // EmailIdentity(EmailIdentity), + + // Phone number-based authentication (`m.login.msisdn`). + // Msisdn(Msisdn), + + // Dummy authentication (`m.login.dummy`). + Dummy(Dummy), + // Registration token-based authentication (`m.login.registration_token`). + // RegistrationToken(RegistrationToken), + + // Fallback acknowledgement. + // FallbackAcknowledgement(FallbackAcknowledgement), +} + +/// Data for dummy UIAA flow. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[serde(tag = "type", rename = "m.login.dummy")] +pub struct Dummy { + pub session: Option, +} + +impl Dummy { + pub fn new() -> Self { + Self::default() + } +} + +#[derive(Clone, Debug, Serialize)] +pub struct UiaaRequest { + pub session: Option, + + #[serde(rename = "type")] + pub kind: AuthType, + + // #[serde(skip_serializing_if = "serde_json::Map::is_empty")] + // data: serde_json::Map, +} + +impl UiaaRequest { + pub fn serialize_json(self) -> serde_json::Value { + let mut obj = JsonObject::new(); + + match self.kind { + AuthType::Dummy => { + if let Some(session) = self.session { + obj.insert( + "session".to_owned(), + serde_json::Value::String(session.into()), + ); + } + + obj.insert( + "type".to_owned(), + serde_json::Value::String(serde_json::to_string(&AuthType::Dummy).unwrap()), + ); + + obj.into() + } + + _ => unimplemented!(), + } + } +} diff --git a/crates/matrix/src/lib.rs b/crates/matrix/src/lib.rs index f14d448..035daab 100644 --- a/crates/matrix/src/lib.rs +++ b/crates/matrix/src/lib.rs @@ -19,6 +19,7 @@ use ruma_common::api::{OutgoingRequest, SendAccessToken}; pub use ruma_common; pub use ruma_events; +pub use ruma_client; pub type Error = ruma_common::api::error::MatrixError; pub type HandleError = ruma_client::Error; diff --git a/crates/test/Cargo.toml b/crates/test/Cargo.toml index 185b886..382d607 100644 --- a/crates/test/Cargo.toml +++ b/crates/test/Cargo.toml @@ -17,6 +17,7 @@ serde = { workspace = true } tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] } thiserror = { workspace = true } url = { workspace = true } +rand = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } diff --git a/crates/test/src/register.rs b/crates/test/src/register.rs index ca01f4e..0e85611 100644 --- a/crates/test/src/register.rs +++ b/crates/test/src/register.rs @@ -1,4 +1,5 @@ use commune::util::secret::Secret; +use rand::Rng; use crate::env::Env; @@ -9,21 +10,19 @@ async fn register() { let resp = client .post("/_commune/client/r0/register") .json(&router::api::session::register::Payload { - username: "steve".to_owned(), + username: format!("steve-{}", rand::thread_rng().gen::()), password: Secret::new("verysecure"), }) .send() .await .unwrap(); - dbg!(resp.status()); + tracing::info!(?resp); - if resp.status().is_success() { - let resp = resp - .json::() - .await - .unwrap(); + let resp = resp + .json::() + .await + .unwrap(); - assert!(!resp.access_token.is_empty()); - } + assert!(!resp.access_token.is_empty()); } From 432b9b7232e0f84c7e8144b8f3ec704c5888f237 Mon Sep 17 00:00:00 2001 From: mikoto Date: Sat, 23 Mar 2024 16:47:02 +0000 Subject: [PATCH 18/33] chore: enable end-to-end testing again --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 745be2b..1154d0d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,8 +86,7 @@ jobs: env: COMMUNE_SYNAPSE_ADMIN_TOKEN: ${{ env.COMMUNE_SYNAPSE_ADMIN_TOKEN }} run: | - # just e2e -- --show-output - echo "warning: end-to-end tests are temporarily disabled" + just e2e -- --show-output - name: Print Logs if: always() From 4b4cd43cce0761c6e1bf4c0a535454d24a1e7fd1 Mon Sep 17 00:00:00 2001 From: mikoto Date: Sun, 24 Mar 2024 00:25:42 +0000 Subject: [PATCH 19/33] feat: registration/login tests --- crates/core/src/session/register.rs | 2 +- crates/matrix/src/client/session/login.rs | 2 +- crates/matrix/src/client/session/register.rs | 10 +++--- crates/router/src/api/session/login.rs | 26 +++++++++++---- crates/router/src/api/session/register.rs | 25 ++++++++++---- crates/test/src/lib.rs | 3 ++ crates/test/src/login.rs | 34 ++++++++++++++++++++ crates/test/src/register.rs | 27 +++++++++------- 8 files changed, 98 insertions(+), 31 deletions(-) create mode 100644 crates/test/src/login.rs diff --git a/crates/core/src/session/register.rs b/crates/core/src/session/register.rs index 357915b..b340106 100644 --- a/crates/core/src/session/register.rs +++ b/crates/core/src/session/register.rs @@ -2,7 +2,7 @@ use http::StatusCode; use matrix::{ client::{ session::register::*, - uiaa::{ AuthType, UiaaRequest, UiaaResponse}, + uiaa::{AuthType, UiaaRequest, UiaaResponse}, }, ruma_client::Error::FromHttpResponse, ruma_common::api::error::{FromHttpResponseError, MatrixError, MatrixErrorBody}, diff --git a/crates/matrix/src/client/session/login.rs b/crates/matrix/src/client/session/login.rs index 4cd795f..aa0d6d5 100644 --- a/crates/matrix/src/client/session/login.rs +++ b/crates/matrix/src/client/session/login.rs @@ -51,7 +51,7 @@ impl Request { } #[response(error = crate::Error)] -#[derive(Serialize)] +#[derive(Deserialize, Serialize)] pub struct Response { pub access_token: String, diff --git a/crates/matrix/src/client/session/register.rs b/crates/matrix/src/client/session/register.rs index 210cfe5..337031c 100644 --- a/crates/matrix/src/client/session/register.rs +++ b/crates/matrix/src/client/session/register.rs @@ -18,25 +18,25 @@ const METADATA: Metadata = metadata! { #[request(error = crate::Error)] pub struct Request { - pub username: String, + username: String, - pub password: String, + password: String, #[serde( rename = "initial_device_display_name", skip_serializing_if = "String::is_empty" )] - pub device_name: String, + device_name: String, #[serde(skip_serializing_if = "Option::is_none")] - pub refresh_token: Option, + refresh_token: Option, /// Note that this information is not used to define how the registered user should be /// authenticated, but is instead used to authenticate the register call itself. /// It should be left empty, or omitted, unless an earlier call returned an response /// with status code 401. #[serde(skip_serializing_if = "Option::is_none")] - pub auth: Option, + auth: Option, } impl Request { diff --git a/crates/router/src/api/session/login.rs b/crates/router/src/api/session/login.rs index 03ea6e0..47adfd2 100644 --- a/crates/router/src/api/session/login.rs +++ b/crates/router/src/api/session/login.rs @@ -3,20 +3,34 @@ use axum::{ Json, }; use commune::util::secret::Secret; -use serde::Deserialize; +use serde::{Serialize, Deserialize}; -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct Payload { username: String, password: Secret, } -pub async fn handler( - Json(payload): Json, -) -> Response { +impl Payload { + pub fn new>(username: S, password: S) -> Self { + Self { + username: username.into(), + password: Secret::new(password.into()), + } + } +} + +#[axum::debug_handler] +pub async fn handler(Json(payload): Json) -> Response { use commune::session::login::service; - match service(&commune::commune().handle, &payload.username, &payload.password).await { + match service( + &commune::commune().handle, + &payload.username, + &payload.password, + ) + .await + { Ok(resp) => Json(resp).into_response(), Err(e) => { tracing::warn!(?e, "failed to login user"); diff --git a/crates/router/src/api/session/register.rs b/crates/router/src/api/session/register.rs index 35bece7..797def2 100644 --- a/crates/router/src/api/session/register.rs +++ b/crates/router/src/api/session/register.rs @@ -7,16 +7,29 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize)] pub struct Payload { - pub username: String, - pub password: Secret, + username: String, + password: Secret, } -pub async fn handler( - Json(payload): Json, -) -> Response { +impl Payload { + pub fn new>(username: S, password: S) -> Self { + Self { + username: username.into(), + password: Secret::new(password.into()), + } + } +} + +pub async fn handler(Json(payload): Json) -> Response { use commune::session::register::service; - match service(&commune::commune().handle, &payload.username, &payload.password).await { + match service( + &commune::commune().handle, + &payload.username, + &payload.password, + ) + .await + { Ok(resp) => Json(resp).into_response(), Err(e) => { tracing::warn!(?e, "failed to register user"); diff --git a/crates/test/src/lib.rs b/crates/test/src/lib.rs index 3a2d480..9821918 100644 --- a/crates/test/src/lib.rs +++ b/crates/test/src/lib.rs @@ -13,5 +13,8 @@ #[cfg(test)] mod register; +#[cfg(test)] +mod login; + #[cfg(test)] mod env; diff --git a/crates/test/src/login.rs b/crates/test/src/login.rs new file mode 100644 index 0000000..73c4f37 --- /dev/null +++ b/crates/test/src/login.rs @@ -0,0 +1,34 @@ +use rand::Rng; + +use crate::env::Env; + +pub async fn login() -> Result { + let client = Env::new().await; + + let register_resp = crate::register::register(&client).await.unwrap(); + + dbg!(®ister_resp); + + let resp = client + .post("/_commune/client/r0/login") + .json(&router::api::session::login::Payload::new( + register_resp.user_id.as_str(), + "verysecure".into(), + )) + .send() + .await + .unwrap(); + + dbg!(&resp); + + resp.json::() + .await +} + +#[tokio::test] +async fn login_test() { + let resp = login().await.unwrap(); + + dbg!(&resp); + // assert!(!resp.access_token.is_empty()); +} diff --git a/crates/test/src/register.rs b/crates/test/src/register.rs index 0e85611..43ec19f 100644 --- a/crates/test/src/register.rs +++ b/crates/test/src/register.rs @@ -1,28 +1,31 @@ -use commune::util::secret::Secret; use rand::Rng; use crate::env::Env; -#[tokio::test] -async fn register() { - let client = Env::new().await; - +pub async fn register( + client: &Env, +) -> Result { let resp = client .post("/_commune/client/r0/register") - .json(&router::api::session::register::Payload { - username: format!("steve-{}", rand::thread_rng().gen::()), - password: Secret::new("verysecure"), - }) + .json(&router::api::session::register::Payload::new( + format!("steve-{}", rand::thread_rng().gen::()), + "verysecure".into(), + )) .send() .await .unwrap(); tracing::info!(?resp); - let resp = resp - .json::() + resp.json::() .await - .unwrap(); +} + +// #[tokio::test] +async fn register_test() { + let client = Env::new().await; + + let resp = register(&client).await.unwrap(); assert!(!resp.access_token.is_empty()); } From 7cad026f8d84a49298e913d53d88606b63f4aa96 Mon Sep 17 00:00:00 2001 From: mikoto Date: Sun, 24 Mar 2024 03:05:26 +0000 Subject: [PATCH 20/33] draft: sync handle (credits to `ruma/ruma-client-api`) --- Cargo.toml | 3 + crates/matrix/Cargo.toml | 8 +- crates/matrix/src/client/mod.rs | 1 + crates/matrix/src/client/sync.rs | 564 +++++++++++++++++++++++++++++++ crates/router/Cargo.toml | 1 + 5 files changed, 574 insertions(+), 3 deletions(-) create mode 100644 crates/matrix/src/client/sync.rs diff --git a/Cargo.toml b/Cargo.toml index e84eddd..82bcbc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,8 +15,11 @@ resolver = "2" [workspace.dependencies] async-trait = "0.1.74" +async-stream = "0.3.5" +bytes = "1.5.0" figment = { version = "0.10.14", features = ["toml"] } hex = "0.4.3" +futures = "0.3.30" hmac = "0.12.1" sha1 = "0.10.6" anyhow = "1.0.75" diff --git a/crates/matrix/Cargo.toml b/crates/matrix/Cargo.toml index f5dcae6..b48132e 100644 --- a/crates/matrix/Cargo.toml +++ b/crates/matrix/Cargo.toml @@ -25,9 +25,11 @@ sha1 = { workspace = true } url = { workspace = true, features = ["serde"] } hex = { workspace = true } hmac = { workspace = true } -http.workspace = true -bytes = "1.5.0" -async-trait = "0.1.77" +http = { workspace = true } +bytes = { workspace = true } +async-trait = { workspace = true } +async-stream = { workspace = true } +futures = { workspace = true } [features] client = [] diff --git a/crates/matrix/src/client/mod.rs b/crates/matrix/src/client/mod.rs index b00a57e..92bed12 100644 --- a/crates/matrix/src/client/mod.rs +++ b/crates/matrix/src/client/mod.rs @@ -5,3 +5,4 @@ pub mod room; pub mod session; pub mod uiaa; +pub mod sync; diff --git a/crates/matrix/src/client/sync.rs b/crates/matrix/src/client/sync.rs new file mode 100644 index 0000000..906f9cf --- /dev/null +++ b/crates/matrix/src/client/sync.rs @@ -0,0 +1,564 @@ +//! This module contains handlers for getting and synchronizing events. +//! +//! reference: https://github.com/matrix-org/matrix-spec-proposals/pull/3575 + +use std::{collections::BTreeMap, time::Duration}; + +use ruma_common::{ + api::{request, response, Metadata}, + metadata, + serde::{deserialize_cow_str, duration::opt_ms, Raw}, + DeviceKeyAlgorithm, MilliSecondsSinceUnixEpoch, OwnedMxcUri, OwnedRoomId, OwnedUserId, RoomId, +}; +use ruma_events::{ + receipt::SyncReceiptEvent, typing::SyncTypingEvent, AnyGlobalAccountDataEvent, + AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, + AnyToDeviceEvent, StateEventType, TimelineEventType, +}; +use serde::{self, de::Error as _, Deserialize, Serialize}; + +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/unstable/org.matrix.msc3575/sync", + // 1.4 => "/_matrix/client/v4/sync", + } +}; + +#[request(error = crate::Error)] +#[derive(Default)] +pub struct Request { + #[serde(skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub pos: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub delta_token: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub conn_id: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub txn_id: Option, + + #[serde(with = "opt_ms", default, skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub timeout: Option, + + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub lists: BTreeMap, + + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub room_subscriptions: BTreeMap, + + #[serde(default, skip_serializing_if = "<[_]>::is_empty")] + pub unsubscribe_rooms: Vec, + + #[serde(default, skip_serializing_if = "ExtensionsConfig::is_empty")] + pub extensions: ExtensionsConfig, +} + +#[response(error = crate::Error)] +pub struct Response { + #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")] + pub initial: bool, + + #[serde(skip_serializing_if = "Option::is_none")] + pub txn_id: Option, + + pub pos: String, + + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub lists: BTreeMap, + + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub rooms: BTreeMap, + + #[serde(default, skip_serializing_if = "Extensions::is_empty")] + pub extensions: Extensions, + + pub delta_token: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct UnreadNotificationsCount { + #[serde(skip_serializing_if = "Option::is_none")] + pub highlight_count: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub notification_count: Option, +} + +impl UnreadNotificationsCount { + pub fn is_empty(&self) -> bool { + self.highlight_count.is_none() && self.notification_count.is_none() + } +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct DeviceLists { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub changed: Vec, + + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub left: Vec, +} + +impl DeviceLists { + pub fn is_empty(&self) -> bool { + self.changed.is_empty() && self.left.is_empty() + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct SyncRequestListFilters { + #[serde(skip_serializing_if = "Option::is_none")] + pub is_dm: Option, + + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub spaces: Vec, + + #[serde(skip_serializing_if = "Option::is_none")] + pub is_encrypted: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub is_invite: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub is_tombstoned: Option, + + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub room_types: Vec, + + #[serde(default, skip_serializing_if = "<[_]>::is_empty")] + pub not_room_types: Vec, + + #[serde(skip_serializing_if = "Option::is_none")] + pub room_name_like: Option, + + #[serde(default, skip_serializing_if = "<[_]>::is_empty")] + pub tags: Vec, + + #[serde(default, skip_serializing_if = "<[_]>::is_empty")] + pub not_tags: Vec, + + #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")] + pub extensions: BTreeMap, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct SyncRequestList { + #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")] + pub slow_get_all_rooms: bool, + + pub ranges: Vec<(usize, usize)>, + + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub sort: Vec, + + #[serde(flatten)] + pub room_details: RoomDetailsConfig, + + #[serde(skip_serializing_if = "Option::is_none")] + pub include_old_rooms: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub filters: Option, + + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub bump_event_types: Vec, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct RoomDetailsConfig { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub required_state: Vec<(StateEventType, String)>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub timeline_limit: Option, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct IncludeOldRooms { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub required_state: Vec<(StateEventType, String)>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub timeline_limit: Option, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct RoomSubscription { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub required_state: Vec<(StateEventType, String)>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub timeline_limit: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum SlidingOp { + Sync, + + Insert, + + Delete, + + Invalidate, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct SyncList { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub ops: Vec, + + pub count: usize, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct SyncOp { + pub op: SlidingOp, + + pub range: Option<(usize, usize)>, + + pub index: Option, + + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub room_ids: Vec, + + pub room_id: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct SlidingSyncRoom { + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + + #[serde(default, skip_serializing_if = "Option::is_none")] + pub avatar: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub initial: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub is_dm: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub invite_state: Option>>, + + #[serde( + flatten, + default, + skip_serializing_if = "UnreadNotificationsCount::is_empty" + )] + pub unread_notifications: UnreadNotificationsCount, + + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub timeline: Vec>, + + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub required_state: Vec>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub prev_batch: Option, + + #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")] + pub limited: bool, + + #[serde(skip_serializing_if = "Option::is_none")] + pub joined_count: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub invited_count: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub num_live: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub timestamp: Option, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct ExtensionsConfig { + #[serde(default, skip_serializing_if = "ToDeviceConfig::is_empty")] + pub to_device: ToDeviceConfig, + + #[serde(default, skip_serializing_if = "E2EEConfig::is_empty")] + pub e2ee: E2EEConfig, + + #[serde(default, skip_serializing_if = "AccountDataConfig::is_empty")] + pub account_data: AccountDataConfig, + + #[serde(default, skip_serializing_if = "ReceiptsConfig::is_empty")] + pub receipts: ReceiptsConfig, + + #[serde(default, skip_serializing_if = "TypingConfig::is_empty")] + pub typing: TypingConfig, + + #[serde(flatten)] + other: BTreeMap, +} + +impl ExtensionsConfig { + pub fn is_empty(&self) -> bool { + self.to_device.is_empty() + && self.e2ee.is_empty() + && self.account_data.is_empty() + && self.receipts.is_empty() + && self.typing.is_empty() + && self.other.is_empty() + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct Extensions { + #[serde(skip_serializing_if = "Option::is_none")] + pub to_device: Option, + + #[serde(default, skip_serializing_if = "E2EE::is_empty")] + pub e2ee: E2EE, + + #[serde(default, skip_serializing_if = "AccountData::is_empty")] + pub account_data: AccountData, + + #[serde(default, skip_serializing_if = "Receipts::is_empty")] + pub receipts: Receipts, + + #[serde(default, skip_serializing_if = "Typing::is_empty")] + pub typing: Typing, +} + +impl Extensions { + pub fn is_empty(&self) -> bool { + self.to_device.is_none() + && self.e2ee.is_empty() + && self.account_data.is_empty() + && self.receipts.is_empty() + && self.typing.is_empty() + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct ToDeviceConfig { + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub since: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub lists: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub rooms: Option>, +} + +impl ToDeviceConfig { + pub fn is_empty(&self) -> bool { + self.enabled.is_none() && self.limit.is_none() && self.since.is_none() + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct ToDevice { + pub next_batch: String, + + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub events: Vec>, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct E2EEConfig { + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled: Option, +} + +impl E2EEConfig { + pub fn is_empty(&self) -> bool { + self.enabled.is_none() + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct E2EE { + #[serde(default, skip_serializing_if = "DeviceLists::is_empty")] + pub device_lists: DeviceLists, + + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub device_one_time_keys_count: BTreeMap, + + #[serde(skip_serializing_if = "Option::is_none")] + pub device_unused_fallback_key_types: Option>, +} + +impl E2EE { + pub fn is_empty(&self) -> bool { + self.device_lists.is_empty() + && self.device_one_time_keys_count.is_empty() + && self.device_unused_fallback_key_types.is_none() + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct AccountDataConfig { + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub lists: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub rooms: Option>, +} + +impl AccountDataConfig { + pub fn is_empty(&self) -> bool { + self.enabled.is_none() + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct AccountData { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub global: Vec>, + + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub rooms: BTreeMap>>, +} + +impl AccountData { + pub fn is_empty(&self) -> bool { + self.global.is_empty() && self.rooms.is_empty() + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum RoomReceiptConfig { + AllSubscribed, + + Room(OwnedRoomId), +} + +impl Serialize for RoomReceiptConfig { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + RoomReceiptConfig::AllSubscribed => serializer.serialize_str("*"), + RoomReceiptConfig::Room(r) => r.serialize(serializer), + } + } +} + +impl<'de> Deserialize<'de> for RoomReceiptConfig { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + match deserialize_cow_str(deserializer)?.as_ref() { + "*" => Ok(RoomReceiptConfig::AllSubscribed), + other => Ok(RoomReceiptConfig::Room( + RoomId::parse(other).map_err(D::Error::custom)?.to_owned(), + )), + } + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct ReceiptsConfig { + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub lists: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub rooms: Option>, +} + +impl ReceiptsConfig { + pub fn is_empty(&self) -> bool { + self.enabled.is_none() + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct Receipts { + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub rooms: BTreeMap>, +} + +impl Receipts { + pub fn is_empty(&self) -> bool { + self.rooms.is_empty() + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct TypingConfig { + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub lists: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub rooms: Option>, +} + +impl TypingConfig { + pub fn is_empty(&self) -> bool { + self.enabled.is_none() + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct Typing { + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub rooms: BTreeMap>, +} + +impl Typing { + pub fn is_empty(&self) -> bool { + self.rooms.is_empty() + } +} + +#[cfg(test)] +mod tests { + use ruma_common::owned_room_id; + + use super::RoomReceiptConfig; + + #[test] + fn serialize_room_receipt_config() { + let entry = RoomReceiptConfig::AllSubscribed; + assert_eq!(serde_json::to_string(&entry).unwrap().as_str(), r#""*""#); + + let entry = RoomReceiptConfig::Room(owned_room_id!("!n8f893n9:example.com")); + assert_eq!( + serde_json::to_string(&entry).unwrap().as_str(), + r#""!n8f893n9:example.com""# + ); + } + + #[test] + fn deserialize_room_receipt_config() { + assert_eq!( + serde_json::from_str::(r#""*""#).unwrap(), + RoomReceiptConfig::AllSubscribed + ); + + assert_eq!( + serde_json::from_str::(r#""!n8f893n9:example.com""#).unwrap(), + RoomReceiptConfig::Room(owned_room_id!("!n8f893n9:example.com")) + ); + } +} diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index c9b7672..883eee3 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -28,3 +28,4 @@ url = { workspace = true, features = ["serde"] } # Local Dependencies core = { path = "../core" } figment = { workspace = true, features = ["toml", "env"] } +async-stream = "0.3.5" From ba38495d91280446fc244c6c6a09ed17f33d2a08 Mon Sep 17 00:00:00 2001 From: mikoto Date: Tue, 26 Mar 2024 03:38:37 +0000 Subject: [PATCH 21/33] wip: account management API --- Cargo.toml | 12 +- commune-example.toml | 5 + crates/core/Cargo.toml | 3 + crates/core/src/account.rs | 8 + crates/core/src/account/email.rs | 32 ++ crates/core/src/account/login.rs | 21 + crates/core/src/account/logout.rs | 12 + crates/core/src/account/password.rs | 20 + crates/core/src/account/register.rs | 53 +++ crates/core/src/account/token.rs | 12 + crates/core/src/account/username.rs | 12 + crates/core/src/account/whoami.rs | 12 + crates/core/src/auth/error.rs | 32 -- crates/core/src/auth/mod.rs | 152 ------- crates/core/src/config.rs | 3 + crates/core/src/error.rs | 6 + crates/core/src/lib.rs | 146 ++++--- crates/core/src/mail/error.rs | 33 -- crates/core/src/mail/mod.rs | 284 ------------- crates/core/src/profile.rs | 2 + crates/core/src/profile/avatar.rs | 41 ++ crates/core/src/profile/display_name.rs | 38 ++ crates/core/src/room/error.rs | 30 -- crates/core/src/room/mod.rs | 3 - crates/core/src/room/model.rs | 4 - crates/core/src/room/service.rs | 62 --- crates/core/src/session/error.rs | 38 -- crates/core/src/session/login.rs | 24 -- crates/core/src/session/logout.rs | 12 - crates/core/src/session/register.rs | 77 ---- crates/core/src/session/service.rs | 387 ------------------ crates/core/src/session/username_available.rs | 11 - crates/core/src/util/secret.rs | 62 +-- crates/core/src/util/time.rs | 3 - crates/matrix/Cargo.toml | 3 +- crates/matrix/src/{admin/mod.rs => admin.rs} | 7 +- .../matrix/src/admin/registration_tokens.rs | 1 + .../src/admin/registration_tokens/new.rs | 42 ++ .../matrix/src/client-backup/account.rs.bk.bk | 3 + .../src/client-backup/events.rs.bk.bk.bk | 310 ++++++++++++++ .../src/client-backup/membership.rs.bk.bk | 5 + .../mod.rs => client-backup/mod.rs.bk.bk} | 4 +- .../matrix/src/client-backup/mxc.rs.bk.bk.bk | 184 +++++++++ .../matrix/src/client-backup/rooms.rs.bk.bk | 6 + .../src/client-backup/session.rs.bk.bk.bk | 2 + .../sync.rs => client-backup/sync.rs.bk.bk} | 0 crates/matrix/src/client-backup/uiaa.rs.bk.bk | 164 ++++++++ crates/matrix/src/client.rs | 10 + crates/matrix/src/client/account.rs | 2 + crates/matrix/src/client/account/password.rs | 55 +++ .../src/client/{session => account}/whoami.rs | 10 +- .../matrix/src/client/{session => }/login.rs | 63 ++- crates/matrix/src/client/logout.rs | 2 + crates/matrix/src/client/logout/all.rs | 1 + .../{session/logout.rs => logout/root.rs} | 0 crates/matrix/src/client/profile.rs | 2 + .../matrix/src/client/profile/avatar_url.rs | 2 + .../src/client/profile/avatar_url/get.rs | 31 ++ .../avatar_url/update.rs} | 22 +- .../matrix/src/client/profile/display_name.rs | 2 + .../src/client/profile/display_name/get.rs | 32 ++ .../display_name/update.rs} | 21 +- crates/matrix/src/client/register.rs | 3 + .../available.rs} | 9 +- crates/matrix/src/client/register/root.rs | 76 ++++ crates/matrix/src/client/register/token.rs | 1 + .../src/client/register/token/validity.rs | 31 ++ crates/matrix/src/client/room.rs | 12 - .../matrix/src/client/room/ban_from_room.rs | 28 -- crates/matrix/src/client/room/create_room.rs | 97 ----- crates/matrix/src/client/room/forget_room.rs | 26 -- crates/matrix/src/client/room/get_rooms.rs | 70 ---- .../matrix/src/client/room/invite_to_room.rs | 0 crates/matrix/src/client/room/join_room.rs | 28 -- crates/matrix/src/client/room/leave_room.rs | 26 -- crates/matrix/src/client/session.rs | 11 - crates/matrix/src/client/session/get_flows.rs | 46 --- crates/matrix/src/client/session/register.rs | 68 --- crates/matrix/src/client/uiaa.rs | 88 ++-- crates/matrix/src/lib.rs | 39 +- crates/router/Cargo.toml | 5 +- crates/router/src/api.rs | 8 +- crates/router/src/api/account.rs | 5 + crates/router/src/api/account/avatar.rs | 33 ++ crates/router/src/api/account/display_name.rs | 27 ++ crates/router/src/api/account/email.rs | 19 + crates/router/src/api/account/password.rs | 37 ++ crates/router/src/api/account/whoami.rs | 21 + .../session.rs => router/src/api/relative.rs} | 2 +- crates/router/src/api/relative/available.rs | 18 + crates/router/src/api/relative/login.rs | 30 ++ crates/router/src/api/relative/logout.rs | 19 + crates/router/src/api/relative/register.rs | 30 ++ crates/router/src/lib.rs | 30 +- crates/router/src/main.rs | 6 +- docker-compose.yml | 10 +- 96 files changed, 1769 insertions(+), 1828 deletions(-) create mode 100644 crates/core/src/account.rs create mode 100644 crates/core/src/account/email.rs create mode 100644 crates/core/src/account/login.rs create mode 100644 crates/core/src/account/logout.rs create mode 100644 crates/core/src/account/password.rs create mode 100644 crates/core/src/account/register.rs create mode 100644 crates/core/src/account/token.rs create mode 100644 crates/core/src/account/username.rs create mode 100644 crates/core/src/account/whoami.rs delete mode 100644 crates/core/src/auth/error.rs delete mode 100644 crates/core/src/auth/mod.rs delete mode 100644 crates/core/src/mail/error.rs delete mode 100644 crates/core/src/mail/mod.rs create mode 100644 crates/core/src/profile.rs create mode 100644 crates/core/src/profile/avatar.rs create mode 100644 crates/core/src/profile/display_name.rs delete mode 100644 crates/core/src/room/error.rs delete mode 100644 crates/core/src/room/mod.rs delete mode 100644 crates/core/src/room/model.rs delete mode 100644 crates/core/src/room/service.rs delete mode 100644 crates/core/src/session/error.rs delete mode 100644 crates/core/src/session/login.rs delete mode 100644 crates/core/src/session/logout.rs delete mode 100644 crates/core/src/session/register.rs delete mode 100644 crates/core/src/session/service.rs delete mode 100644 crates/core/src/session/username_available.rs delete mode 100644 crates/core/src/util/time.rs rename crates/matrix/src/{admin/mod.rs => admin.rs} (64%) create mode 100644 crates/matrix/src/admin/registration_tokens.rs create mode 100644 crates/matrix/src/admin/registration_tokens/new.rs create mode 100644 crates/matrix/src/client-backup/account.rs.bk.bk create mode 100644 crates/matrix/src/client-backup/events.rs.bk.bk.bk create mode 100644 crates/matrix/src/client-backup/membership.rs.bk.bk rename crates/matrix/src/{client/mod.rs => client-backup/mod.rs.bk.bk} (76%) create mode 100644 crates/matrix/src/client-backup/mxc.rs.bk.bk.bk create mode 100644 crates/matrix/src/client-backup/rooms.rs.bk.bk create mode 100644 crates/matrix/src/client-backup/session.rs.bk.bk.bk rename crates/matrix/src/{client/sync.rs => client-backup/sync.rs.bk.bk} (100%) create mode 100644 crates/matrix/src/client-backup/uiaa.rs.bk.bk create mode 100644 crates/matrix/src/client.rs create mode 100644 crates/matrix/src/client/account.rs create mode 100644 crates/matrix/src/client/account/password.rs rename crates/matrix/src/client/{session => account}/whoami.rs (76%) rename crates/matrix/src/client/{session => }/login.rs (72%) create mode 100644 crates/matrix/src/client/logout.rs create mode 100644 crates/matrix/src/client/logout/all.rs rename crates/matrix/src/client/{session/logout.rs => logout/root.rs} (100%) create mode 100644 crates/matrix/src/client/profile.rs create mode 100644 crates/matrix/src/client/profile/avatar_url.rs create mode 100644 crates/matrix/src/client/profile/avatar_url/get.rs rename crates/matrix/src/client/{room/unban_from_room.rs => profile/avatar_url/update.rs} (50%) create mode 100644 crates/matrix/src/client/profile/display_name.rs create mode 100644 crates/matrix/src/client/profile/display_name/get.rs rename crates/matrix/src/client/{room/kick_from_room.rs => profile/display_name/update.rs} (51%) create mode 100644 crates/matrix/src/client/register.rs rename crates/matrix/src/client/{session/username_available.rs => register/available.rs} (79%) create mode 100644 crates/matrix/src/client/register/root.rs create mode 100644 crates/matrix/src/client/register/token.rs create mode 100644 crates/matrix/src/client/register/token/validity.rs delete mode 100644 crates/matrix/src/client/room.rs delete mode 100644 crates/matrix/src/client/room/ban_from_room.rs delete mode 100644 crates/matrix/src/client/room/create_room.rs delete mode 100644 crates/matrix/src/client/room/forget_room.rs delete mode 100644 crates/matrix/src/client/room/get_rooms.rs delete mode 100644 crates/matrix/src/client/room/invite_to_room.rs delete mode 100644 crates/matrix/src/client/room/join_room.rs delete mode 100644 crates/matrix/src/client/room/leave_room.rs delete mode 100644 crates/matrix/src/client/session.rs delete mode 100644 crates/matrix/src/client/session/get_flows.rs delete mode 100644 crates/matrix/src/client/session/register.rs create mode 100644 crates/router/src/api/account.rs create mode 100644 crates/router/src/api/account/avatar.rs create mode 100644 crates/router/src/api/account/display_name.rs create mode 100644 crates/router/src/api/account/email.rs create mode 100644 crates/router/src/api/account/password.rs create mode 100644 crates/router/src/api/account/whoami.rs rename crates/{core/src/session.rs => router/src/api/relative.rs} (63%) create mode 100644 crates/router/src/api/relative/available.rs create mode 100644 crates/router/src/api/relative/login.rs create mode 100644 crates/router/src/api/relative/logout.rs create mode 100644 crates/router/src/api/relative/register.rs diff --git a/Cargo.toml b/Cargo.toml index 82bcbc0..bbdf33d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,12 +14,15 @@ default-members = ["crates/router"] resolver = "2" [workspace.dependencies] +axum-extra = { version = "0.9.3", features = ["typed-header"] } async-trait = "0.1.74" -async-stream = "0.3.5" +# async-stream = "0.3.5" bytes = "1.5.0" -figment = { version = "0.10.14", features = ["toml"] } +email_address = { version = "0.2.4", features = ["serde", "serde_support"] } +figment = { version = "0.10.14", features = ["toml", "env"] } hex = "0.4.3" -futures = "0.3.30" +tokio-rustls = "0.25.0" +# futures = "0.3.30" hmac = "0.12.1" sha1 = "0.10.6" anyhow = "1.0.75" @@ -28,10 +31,10 @@ http = "0.2.11" mime = "0.3.17" mail-send = "0.4.7" maud = "0.26.0" +headers = "0.4.0" # openssl = { version = "0.10.63", features = ["vendored"] } # openssl-sys = { version = "0.9.99", features = ["vendored"] } reqwest = { version = "0.11.22", default-features = false, features = [ - "blocking", "json", "multipart", "rustls", @@ -61,6 +64,7 @@ ruma-common = { version = "0.12.0", default_features = false, features = [ ] } ruma-macros = { version = "0.12.0", default_features = false } ruma-client = { version = "0.12.0", default_features = false } +ruma-identifiers-validation = { version = "0.9.3", default_features = false } [workspace.lints.rust] unreachable_pub = "warn" diff --git a/commune-example.toml b/commune-example.toml index b8e294d..df1e5bc 100644 --- a/commune-example.toml +++ b/commune-example.toml @@ -2,6 +2,11 @@ registration_verification = false public_loopback = false port = 6421 tls = true + +# Either one works but not both +blocked_domains = [] +allowed_domains = ['gmail.com', 'outlook.com'] + # `X-Forwarded-For` header # xff = false diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 6f7dba1..a6961dc 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -13,6 +13,7 @@ path = "src/lib.rs" anyhow = { workspace = true } axum = { workspace = true } rand = { workspace = true } +email_address = { workspace = true } thiserror = { workspace = true } validator = { workspace = true, features = ["derive"] } http = { workspace = true } @@ -24,6 +25,8 @@ tracing = { workspace = true } figment = { workspace = true } url = { workspace = true, features = ["serde"] } tokio = { workspace = true, features = ["full"] } +headers = { workspace = true } +tokio-rustls = { workspace = true } # Local Dependencies matrix = { path = "../matrix", features = ["client"] } diff --git a/crates/core/src/account.rs b/crates/core/src/account.rs new file mode 100644 index 0000000..ce3b5ea --- /dev/null +++ b/crates/core/src/account.rs @@ -0,0 +1,8 @@ +pub mod email; +pub mod login; +pub mod logout; +pub mod password; +pub mod register; +pub mod token; +pub mod username; +pub mod whoami; diff --git a/crates/core/src/account/email.rs b/crates/core/src/account/email.rs new file mode 100644 index 0000000..c251b8e --- /dev/null +++ b/crates/core/src/account/email.rs @@ -0,0 +1,32 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use email_address::EmailAddress; +use matrix::admin::registration_tokens::new::*; +use rand::{distributions::Uniform, prelude::Distribution}; + +use crate::{commune, error::Result}; + +pub async fn service(address: EmailAddress) -> Result<()> { + let uni = Uniform::new('0', '9'); + let token: String = uni.sample_iter(rand::thread_rng()).take(6).collect(); + + let req = Request::new( + token.clone(), + 1, + SystemTime::now() + .duration_since(UNIX_EPOCH) + // panics below should never happen + .expect("system time overflow") + .as_millis() + .try_into() + .expect("system time overflow"), + ); + + commune() + .send_matrix_request(req, Some(&commune().config.matrix.admin_token.inner())) + .await?; + + commune().send_email_verification(address, token).await?; + + Ok(()) +} diff --git a/crates/core/src/account/login.rs b/crates/core/src/account/login.rs new file mode 100644 index 0000000..7a31e83 --- /dev/null +++ b/crates/core/src/account/login.rs @@ -0,0 +1,21 @@ +use matrix::client::{login::*, uiaa::UserIdentifier}; + +use crate::{commune, error::Result, util::secret::Secret}; + +pub async fn service(username: impl Into, password: &Secret) -> Result { + let req = Request::new( + LoginType::Password { + password: password.inner(), + }, + Some(UserIdentifier::User { + user: username.into(), + }), + "commune".to_owned(), + Some(true), + ); + + commune() + .send_matrix_request(req, None) + .await + .map_err(Into::into) +} diff --git a/crates/core/src/account/logout.rs b/crates/core/src/account/logout.rs new file mode 100644 index 0000000..972d9ef --- /dev/null +++ b/crates/core/src/account/logout.rs @@ -0,0 +1,12 @@ +use matrix::client::logout::root::*; + +use crate::{commune, error::Result}; + +pub async fn service(access_token: impl AsRef) -> Result { + let req = Request::new(); + + commune() + .send_matrix_request(req, Some(access_token.as_ref())) + .await + .map_err(Into::into) +} diff --git a/crates/core/src/account/password.rs b/crates/core/src/account/password.rs new file mode 100644 index 0000000..7e7783a --- /dev/null +++ b/crates/core/src/account/password.rs @@ -0,0 +1,20 @@ +use matrix::{client::account::password::*, ruma_common::UserId}; + +use crate::{commune, error::Result, util::secret::Secret}; + +pub async fn service( + access_token: impl AsRef, + username: impl Into, + old_password: Secret, + new_password: Secret, +) -> Result { + let server_name = &crate::commune().config.matrix.server_name; + let user_id = UserId::parse_with_server_name(username.into(), server_name)?; + + let req = Request::new(new_password.inner()).with_password(user_id, old_password.inner()); + + commune() + .send_matrix_request(req, Some(access_token.as_ref())) + .await + .map_err(Into::into) +} diff --git a/crates/core/src/account/register.rs b/crates/core/src/account/register.rs new file mode 100644 index 0000000..b8b384e --- /dev/null +++ b/crates/core/src/account/register.rs @@ -0,0 +1,53 @@ +use http::StatusCode; +use matrix::{ + client::{ + register::root::*, + uiaa::{Auth, AuthData, AuthType, Dummy, UiaaResponse}, + }, + ruma_client::Error::FromHttpResponse, + ruma_common::api::error::{FromHttpResponseError, MatrixError, MatrixErrorBody}, +}; + +use crate::{commune, error::Result, util::secret::Secret}; + +pub async fn service(username: impl Into, password: Secret) -> Result { + let req = Request::new( + username.into(), + password.inner(), + Some("commune".to_owned()), + None, + None, + ); + + let mut retry_req = req.clone(); + + match commune().send_matrix_request(req, None).await { + Ok(resp) => Ok(resp), + Err(e) => match e { + FromHttpResponse(FromHttpResponseError::Server(MatrixError { + status_code: StatusCode::UNAUTHORIZED, + body: MatrixErrorBody::Json(ref body), + })) => { + let UiaaResponse { flows, session, .. } = + serde_json::from_value::(body.clone()).unwrap(); + + match flows.as_slice() { + [value] => match value.stages.as_slice() { + [AuthType::Dummy] => { + retry_req.auth = Some(Auth::new(AuthData::Dummy(Dummy {}), session)); + + commune() + .send_matrix_request(retry_req, None) + .await + .map_err(Into::into) + } + _ => Err(e.into()), + }, + _ => Err(e.into()), + } + } + + _ => Err(e.into()), + }, + } +} diff --git a/crates/core/src/account/token.rs b/crates/core/src/account/token.rs new file mode 100644 index 0000000..cc85ac4 --- /dev/null +++ b/crates/core/src/account/token.rs @@ -0,0 +1,12 @@ +use matrix::client::register::token::validity::*; + +use crate::{commune, error::Result}; + +pub async fn service(access_token: impl AsRef) -> Result { + let req = Request::new(access_token.as_ref().to_owned()); + + commune() + .send_matrix_request(req, None) + .await + .map_err(Into::into) +} diff --git a/crates/core/src/account/username.rs b/crates/core/src/account/username.rs new file mode 100644 index 0000000..37528d7 --- /dev/null +++ b/crates/core/src/account/username.rs @@ -0,0 +1,12 @@ +use matrix::client::register::available::*; + +use crate::{commune, error::Result}; + +pub async fn service(username: impl Into) -> Result { + let req = Request::new(username.into()); + + commune() + .send_matrix_request(req, None) + .await + .map_err(Into::into) +} diff --git a/crates/core/src/account/whoami.rs b/crates/core/src/account/whoami.rs new file mode 100644 index 0000000..908e1bc --- /dev/null +++ b/crates/core/src/account/whoami.rs @@ -0,0 +1,12 @@ +use matrix::client::account::whoami::*; + +use crate::{commune, error::Result}; + +pub async fn service(access_token: impl AsRef) -> Result { + let req = Request::new(); + + commune() + .send_matrix_request(req, Some(access_token.as_ref())) + .await + .map_err(Into::into) +} diff --git a/crates/core/src/auth/error.rs b/crates/core/src/auth/error.rs deleted file mode 100644 index c437aa0..0000000 --- a/crates/core/src/auth/error.rs +++ /dev/null @@ -1,32 +0,0 @@ -use http::StatusCode; -use thiserror::Error; - -use crate::error::HttpStatusCode; - -#[derive(Debug, Error)] -pub enum AuthErrorCode { - #[error("Provided credentials are not valid")] - InvalidCredentials, - #[error("Redis connection failed")] - RedisConnectionError(#[from] redis::RedisError), - #[error("Verification Code Marshall/Unmarshall failed")] - VerificationCodeMarshallError(#[from] serde_json::Error), -} - -impl HttpStatusCode for AuthErrorCode { - fn status_code(&self) -> StatusCode { - match self { - AuthErrorCode::InvalidCredentials => StatusCode::BAD_REQUEST, - AuthErrorCode::RedisConnectionError(_) - | AuthErrorCode::VerificationCodeMarshallError(_) => StatusCode::INTERNAL_SERVER_ERROR, - } - } - - fn error_code(&self) -> &'static str { - match self { - AuthErrorCode::InvalidCredentials => "INVALID_CREDENTIALS", - AuthErrorCode::RedisConnectionError(_) => "REDIS_CONNECTION_ERROR", - AuthErrorCode::VerificationCodeMarshallError(_) => "VERIFICATION_CODE_MARSHALL_ERROR", - } - } -} diff --git a/crates/core/src/auth/mod.rs b/crates/core/src/auth/mod.rs deleted file mode 100644 index 5668c41..0000000 --- a/crates/core/src/auth/mod.rs +++ /dev/null @@ -1,152 +0,0 @@ -use std::{collections::HashMap, sync::Arc}; - -use matrix::client::session; -use rand::Rng; - -use crate::{util::secret::Secret, Config, Error}; - -pub mod error; - -pub type AccessToken = Secret; - -pub struct Auth { - handle: Arc, - config: Arc, - cache: HashMap>, -} - -impl Auth { - fn new(handle: Arc, config: Arc) -> Self { - Auth { - handle, - config, - cache: HashMap::new(), - } - } -} - -impl Auth { - pub async fn login_with_password( - &self, - username: &str, - password: Secret, - ) -> Result { - use session::login::*; - - let req = Request::new( - LoginType::Password { - password: password.inner().to_owned(), - }, - Some(Identifier::User { - user: username.to_owned(), - }), - "commune beta".to_owned(), - Some(true), - ); - - let resp: Response = self.handle.dispatch(None, req).await.unwrap(); - - Ok(AccessToken::new(resp.access_token)) - } - - // pub async fn get_login_flows(&self) -> Result { - // match Login::get_login_flows(&self.admin).await { - // Ok(flows) => Ok(flows), - // Err(err) => { - // tracing::error!("Failed to get login flows: {}", err); - // Err(Error::Unknown) - // } - // } - // } - - // pub async fn send_verification_code( - // &self, - // email: &str, - // session: &Uuid, - // ) -> Result { - // let mut conn = self.redis.get_async_connection().await.map_err(|err| { - // tracing::error!(?err, "Failed to get Redis connection"); - // AuthErrorCode::RedisConnectionError(err) - // })?; - // let verif_code = VerificationCode::new(email, session); - - // conn.set_ex::( - // Self::verification_code_key(session), - // verif_code.marshall(), - // REDIS_VERIFICATION_CODE_SECS, - // ) - // .await - // .map_err(|err| { - // tracing::error!(?err, "Failed to set verification code in Redis"); - // AuthErrorCode::RedisConnectionError(err) - // })?; - - // Ok(verif_code) - // } - - // pub async fn check_verification_code( - // &self, - // email: &str, - // session: &Uuid, - // code: &Secret, - // ) -> Result { - // let mut conn = self.redis.get_async_connection().await.map_err(|err| { - // tracing::error!(?err, "Failed to get Redis connection"); - // AuthErrorCode::RedisConnectionError(err) - // })?; - - // let maybe_marshalled_verification_code = conn - // .get::>(Self::verification_code_key(session)) - // .await - // .map_err(|err| { - // tracing::error!( - // ?err, - // ?session, - // ?email, - // "Failed to get verification code in Redis" - // ); - // AuthErrorCode::RedisConnectionError(err) - // })?; - - // if let Some(marshalled_verification_code) = - // maybe_marshalled_verification_code { let verification_code = - // VerificationCode::unmarshall(marshalled_verification_code); - - // if verification_code.email == email - // && verification_code.code == *code - // && verification_code.session == *session - // { - // return Ok(true); - // } - // } - - // tracing::warn!(?session, ?email, "Verification code not found in - // storge"); Ok(false) - // } - - // pub async fn drop_verification_code(&self, email: &str, session: &Uuid) -> - // Result { let mut conn = - // self.redis.get_async_connection().await.map_err(|err| { - // tracing::error!(?err, "Failed to get Redis connection"); - // AuthErrorCode::RedisConnectionError(err) - // })?; - - // conn.del(Self::verification_code_key(session)) - // .await - // .map_err(|err| { - // tracing::error!( - // ?err, - // ?session, - // ?email, - // "Failed to delete verification code in Redis" - // ); - // AuthErrorCode::RedisConnectionError(err) - // })?; - - // Ok(true) - // } - - // fn verification_code_key(session: &Uuid) -> String { - // format!("{}{}", REDIS_VERIFICATION_CODE_PREFIX, session) - // } -} diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs index 47db905..57b5f82 100644 --- a/crates/core/src/config.rs +++ b/crates/core/src/config.rs @@ -10,6 +10,9 @@ pub struct Config { pub public_loopback: bool, pub port: Option, + pub allowed_domains: Option>, + pub blocked_domains: Option>, + pub matrix: Matrix, pub mail: SMTP, } diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index c699f08..7134727 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -9,6 +9,12 @@ pub enum Error { #[error("forwarding a Matrix request failed: {0}")] Matrix(#[from] matrix::HandleError), + #[error("instance does not allow email address originating from this domain")] + EmailDomain, + + #[error("failed to validate identifier: {0}")] + InvalidIdentifier(#[from] matrix::ruma_identifiers_validation::Error), + #[error("an IO operation failed: {0}")] IO(#[from] std::io::Error), diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index f7fc5bc..1309e2f 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -1,30 +1,36 @@ //! This library deals with our core logic, such as authorizing user //! interactions, forwarding regular events and constructing custom requests. -// pub mod auth; pub mod config; pub mod error; -pub mod mail; -pub mod session; -// pub mod room; pub mod util; -use std::sync::{Arc, RwLock}; +pub mod account; +pub mod profile; + +use std::sync::RwLock; use config::Config; +use email_address::EmailAddress; use figment::{ providers::{Env, Format, Toml}, Figment, }; +use mail_send::{mail_builder::MessageBuilder, SmtpClientBuilder}; +use matrix::{ + ruma_client::{HttpClientExt, ResponseResult}, + ruma_common::api::{OutgoingRequest, SendAccessToken}, +}; static COMMUNE: RwLock> = RwLock::new(None); pub struct Commune { pub config: Config, - pub handle: Arc, + client: matrix::Client, + // smtp: SmtpClient>, } -pub fn init() { +pub async fn init() { let mut commune = COMMUNE.write().unwrap(); let config = Figment::new() @@ -34,9 +40,21 @@ pub fn init() { .extract::() .unwrap(); - let handle = Arc::new(matrix::Handle::new(&config.matrix.host)); - - *commune = Some(Box::leak(Box::new(Commune { config, handle }))); + if config + .allowed_domains + .as_ref() + .is_some_and(|v| !v.is_empty()) + && config + .blocked_domains + .as_ref() + .is_some_and(|v| !v.is_empty()) + { + panic!("config can only contain either allowed or blocked domains"); + } + + let client = matrix::Client::default(); + + *commune = Some(Box::leak(Box::new(Commune { config, client }))); } pub fn commune() -> &'static Commune { @@ -46,52 +64,62 @@ pub fn commune() -> &'static Commune { .expect("commune should be initialized at this point") } -// admin -// .set_token(&config.synapse_admin_token) -// .map_err(|err| { -// tracing::error!(?err, "Failed to set admin token"); -// Error::Startup(err.to_string()) -// })?; - -// let redis = { -// let client = -// redis::Client::open(config.redis_host.to_string()).map_err(|err| { -// tracing::error!(?err, host=%config.redis_host.to_string(), -// "Failed to open connection to Redis"); -// Error::Startup(err.to_string()) })?; -// let mut conn = client.get_async_connection().await.map_err(|err| -// { tracing::error!(?err, host=%config.redis_host.to_string(), -// "Failed to get connection to Redis"); -// Error::Startup(err.to_string()) })?; - -// redis::cmd("PING").query_async(&mut conn).await.map_err(|err| { -// tracing::error!(?err, host=%config.redis_host.to_string(), -// "Failed to ping Redis"); Error::Startup(err.to_string()) -// })?; - -// tracing::info!(host=%config.redis_host.to_string(), "Connected to -// Redis"); - -// Arc::new(client) -// }; - -// let admin_client = Arc::new(admin); -// let auth = Arc::new(AuthService::new( -// Arc::clone(&admin_client), -// Arc::clone(&redis), -// )); -// let mail = Arc::new(MailService::new(&config)); -// let account = AccountService::new( -// Arc::clone(&admin_client), -// Arc::clone(&auth), -// Arc::clone(&mail), -// ); -// let room = RoomService::new(Arc::clone(&admin_client)); - -// Ok(Self { -// account: Arc::new(account), -// auth, -// room: Arc::new(room), -// }) -// } -// } +impl Commune { + pub async fn send_matrix_request( + &self, + request: R, + access_token: Option<&str>, + ) -> ResponseResult { + let at = match access_token { + Some(at) => SendAccessToken::Always(at), + None => SendAccessToken::None, + }; + + self.client + .send_matrix_request::(self.config.matrix.host.as_str(), at, &[], request) + .await + } + + pub async fn send_email_verification( + &self, + address: EmailAddress, + token: impl Into, + ) -> mail_send::Result<()> { + let config = &commune().config; + + let password = config.mail.password.inner(); + let username = config + .mail + .username + .as_deref() + .unwrap_or(&password) + .to_owned(); + let host = &config.mail.host; + + let mut smtp = SmtpClientBuilder::new( + host.host_str() + .expect("failed to extract host from email configuration"), + 587, + ) + .implicit_tls(false) + .credentials((username.as_str(), password.as_str())) + .connect() + .await?; + + let token = token.into(); + let from = format!("commune@{host}"); + let html = format!("

Thanks for signing up.\n\nUse this code to finish verifying your email:\n{token}

"); + let text = format!( + "Thanks for signing up.\n\nUse this code to finish verifying your email:\n{token}" + ); + + let message = MessageBuilder::new() + .from(("Commune", from.as_str())) + .to(vec![address.as_str()]) + .subject("Email Verification Code") + .html_body(html.as_str()) + .text_body(text.as_str()); + + smtp.send(message).await + } +} diff --git a/crates/core/src/mail/error.rs b/crates/core/src/mail/error.rs deleted file mode 100644 index 8a4b53a..0000000 --- a/crates/core/src/mail/error.rs +++ /dev/null @@ -1,33 +0,0 @@ -use http::StatusCode; -use lettre::{error::Error as LettreError, transport::smtp::Error as LettreSmtpError}; -use thiserror::Error; - -use crate::error::HttpStatusCode; - -#[derive(Debug, Error)] -pub enum MailErrorCode { - #[error("Failed to render handlebars template. {0}")] - RenderHandlebars(#[from] handlebars::RenderError), - #[error("Failed to connect to SMTP Server. {0}")] - SmtpConnection(LettreSmtpError), - #[error("Invalid mail payload. {0}")] - InvalidMailPayload(LettreError), -} - -impl HttpStatusCode for MailErrorCode { - fn status_code(&self) -> StatusCode { - match self { - MailErrorCode::RenderHandlebars(_) - | MailErrorCode::SmtpConnection(_) - | MailErrorCode::InvalidMailPayload(_) => StatusCode::INTERNAL_SERVER_ERROR, - } - } - - fn error_code(&self) -> &'static str { - match self { - MailErrorCode::RenderHandlebars(_) => "RENDER_HANDLEBARS", - MailErrorCode::SmtpConnection(_) => "SMTP_CONNECTION", - MailErrorCode::InvalidMailPayload(_) => "INVALID_MAIL_PAYLOAD", - } - } -} diff --git a/crates/core/src/mail/mod.rs b/crates/core/src/mail/mod.rs deleted file mode 100644 index 09ba302..0000000 --- a/crates/core/src/mail/mod.rs +++ /dev/null @@ -1,284 +0,0 @@ -use tokio::net::TcpStream; - -use mail_send::{ - mail_builder::{headers::address::EmailAddress, MessageBuilder}, - SmtpClient, SmtpClientBuilder, -}; -use matrix::ruma_common::ServerName; -use rand::Rng; - -use crate::error::Error; - -pub struct Mail { - conn: SmtpClient, -} - -impl Mail { - pub async fn build(host: &ServerName) -> SmtpClient { - tracing::warn!("using Mailtutan as email gateway, this is only meant for development"); - - for attempt in 1..=3 { - match SmtpClientBuilder::new(host.as_str(), 587) - .implicit_tls(false) - .credentials(("admin", "admin")) - .connect_plain() - .await - { - Ok(conn) => return conn, - Err(e) => { - if attempt >= 3 { - panic!("failed to connect to the SMTP host: {e}") - } - } - } - } - - unreachable!() - } - - pub async fn send_verification( - &mut self, - server_name: &ServerName, - recipient: EmailAddress<'_>, - ) -> Result<(), Error> { - let message = MessageBuilder::new() - .from(format!("onboarding@{}", server_name)) - .to(&*recipient.email) - .subject("Please verify your email address") - .html_body( - template::Verification::build( - server_name, - &recipient.name.expect("recipient name cannot be empty"), - rand::thread_rng().gen(), - ) - .into_string(), - ); - - self.conn.send(message).await.map_err(|e| { - tracing::error!(?e, "failed to send message to SMTP host"); - - Error::SMTP(e) - })?; - - Ok(()) - } -} - -mod template { - use matrix::ruma_common::ServerName; - use maud::{html, PreEscaped, DOCTYPE}; - - pub struct Verification; - - impl Verification { - pub fn build( - server_name: &ServerName, - username: &str, - code: [u8; 6], - ) -> PreEscaped { - html! { - (DOCTYPE) - html lang="en" { - header { - (PreEscaped(STYLESHEET)) - title { "Welcome to Commune" } - - meta charset="utf-8"; - meta name="viewport" content="width=device-width, initial-scale=1"; - } - body { - div ."halftone-div"; - section { - div .title { - h3 .header { - "Commune - " (username) ", welcome abroad" - } - h5 .greeting { - "We are glad that you have decided to join the network. \ - To finish setting up your account, enter this code in your client." - } - } - code { - span { - (code.iter().map(|n| char::from_digit(*n as u32, 10).expect("code should only contain digits")).collect::()) - } - } - footer { - p .disclaimer { - "If you didn't request a code, you can safely ignore this email." - } - p .contact { - "This email was sent to you by a" - (PreEscaped( - " \ - Commune instance " - )) - "If you have any questions, please get in touch with the administrator of " (server_name) "." - } - } - } - } - } - } - } - } - - const STYLESHEET: &str = r#" - - "#; -} diff --git a/crates/core/src/profile.rs b/crates/core/src/profile.rs new file mode 100644 index 0000000..bea0178 --- /dev/null +++ b/crates/core/src/profile.rs @@ -0,0 +1,2 @@ +pub mod avatar; +pub mod display_name; diff --git a/crates/core/src/profile/avatar.rs b/crates/core/src/profile/avatar.rs new file mode 100644 index 0000000..078c6cb --- /dev/null +++ b/crates/core/src/profile/avatar.rs @@ -0,0 +1,41 @@ +pub mod get { + use matrix::{client::profile::avatar_url::get::*, ruma_common::OwnedUserId}; + + use crate::{commune, error::Result}; + + pub async fn service(user_id: impl Into) -> Result { + let req = Request::new(user_id.into()); + + commune() + .send_matrix_request(req, None) + .await + .map_err(Into::into) + } +} + +pub mod update { + use matrix::{ + client::{account::whoami, profile::avatar_url::update::*}, + ruma_common::OwnedMxcUri, + }; + + use crate::{commune, error::Result}; + + pub async fn service( + access_token: impl AsRef, + mxc_uri: impl Into, + ) -> Result { + let req = whoami::Request::new(); + + let whoami::Response { user_id, .. } = commune() + .send_matrix_request(req, Some(access_token.as_ref())) + .await?; + + let req = Request::new(user_id, mxc_uri.into()); + + commune() + .send_matrix_request(req, Some(access_token.as_ref())) + .await + .map_err(Into::into) + } +} diff --git a/crates/core/src/profile/display_name.rs b/crates/core/src/profile/display_name.rs new file mode 100644 index 0000000..730ac70 --- /dev/null +++ b/crates/core/src/profile/display_name.rs @@ -0,0 +1,38 @@ +pub mod get { + use matrix::{client::profile::display_name::get::*, ruma_common::OwnedUserId}; + + use crate::{commune, error::Result}; + + pub async fn service(user_id: impl Into) -> Result { + let req = Request::new(user_id.into()); + + commune() + .send_matrix_request(req, None) + .await + .map_err(Into::into) + } +} + +pub mod update { + use matrix::client::{account::whoami, profile::display_name::update::*}; + + use crate::{commune, error::Result}; + + pub async fn service( + access_token: impl AsRef, + display_name: impl Into, + ) -> Result { + let req = whoami::Request::new(); + + let whoami::Response { user_id, .. } = commune() + .send_matrix_request(req, Some(access_token.as_ref())) + .await?; + + let req = Request::new(user_id, display_name.into()); + + commune() + .send_matrix_request(req, Some(access_token.as_ref())) + .await + .map_err(Into::into) + } +} diff --git a/crates/core/src/room/error.rs b/crates/core/src/room/error.rs deleted file mode 100644 index 845604b..0000000 --- a/crates/core/src/room/error.rs +++ /dev/null @@ -1,30 +0,0 @@ -use http::StatusCode; -use thiserror::Error; -use validator::ValidationErrors; - -use crate::error::HttpStatusCode; - -#[derive(Debug, Error)] -pub enum RoomErrorCode { - #[error("Failed to parse RoomId")] - MalformedRoomId, - #[error("Validation error. {0}")] - ValidationError(#[from] ValidationErrors), -} - -impl HttpStatusCode for RoomErrorCode { - fn status_code(&self) -> StatusCode { - match self { - RoomErrorCode::MalformedRoomId | RoomErrorCode::ValidationError(_) => { - StatusCode::BAD_REQUEST - } - } - } - - fn error_code(&self) -> &'static str { - match self { - RoomErrorCode::MalformedRoomId => "BAD_REQUEST", - RoomErrorCode::ValidationError(_) => "CREATION_DETAIL_INVALID", - } - } -} diff --git a/crates/core/src/room/mod.rs b/crates/core/src/room/mod.rs deleted file mode 100644 index e1001c6..0000000 --- a/crates/core/src/room/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod error; -pub mod model; -pub mod service; diff --git a/crates/core/src/room/model.rs b/crates/core/src/room/model.rs deleted file mode 100644 index 997e5b1..0000000 --- a/crates/core/src/room/model.rs +++ /dev/null @@ -1,4 +0,0 @@ -#[derive(Debug, Clone)] -pub struct Room { - pub room_id: String, -} diff --git a/crates/core/src/room/service.rs b/crates/core/src/room/service.rs deleted file mode 100644 index de4ed47..0000000 --- a/crates/core/src/room/service.rs +++ /dev/null @@ -1,62 +0,0 @@ -use std::sync::Arc; - -use tracing::instrument; - -use matrix::{ - client::resources::room::{ - CreateRoomBody, RoomCreationContent, RoomPreset, RoomService as MatrixRoomService, - }, - Client as MatrixAdminClient, -}; -use validator::Validate; - -use crate::{util::secret::Secret, Error, Result}; - -use super::model::Room; - -#[derive(Debug, Default, Validate)] -pub struct CreateRoomDto { - pub name: String, - pub topic: String, - pub alias: String, -} -pub struct RoomService { - admin: Arc, -} - -impl RoomService { - pub fn new(admin: Arc) -> Self { - Self { admin } - } - - /// Creates a Public Chat Room - #[instrument(skip(self, dto))] - pub async fn create_public_room( - &self, - access_token: &Secret, - dto: CreateRoomDto, - ) -> Result { - match MatrixRoomService::create( - &self.admin, - access_token.to_string(), - CreateRoomBody { - creation_content: Some(RoomCreationContent { federate: false }), - preset: Some(RoomPreset::PublicChat), - name: dto.name, - room_alias_name: dto.alias, - topic: dto.topic, - ..Default::default() - }, - ) - .await - { - Ok(room) => Ok(Room { - room_id: room.room_id.to_string(), - }), - Err(err) => { - tracing::error!("Failed to create public room: {}", err); - Err(Error::Unknown) - } - } - } -} diff --git a/crates/core/src/session/error.rs b/crates/core/src/session/error.rs deleted file mode 100644 index 390e505..0000000 --- a/crates/core/src/session/error.rs +++ /dev/null @@ -1,38 +0,0 @@ -use http::StatusCode; -use thiserror::Error; -use validator::ValidationErrors; - -use crate::error::HttpStatusCode; - -#[derive(Debug, Error)] -pub enum AccountErrorCode { - #[error("Invalid verification code")] - InvalidVerificationCode, - #[error("Vaildation error. {0}")] - ValidationError(#[from] ValidationErrors), - #[error("The username {0} is already taken")] - UsernameTaken(String), - #[error("The email {0} is already taken")] - EmailTaken(String), -} - -impl HttpStatusCode for AccountErrorCode { - fn status_code(&self) -> StatusCode { - match self { - AccountErrorCode::InvalidVerificationCode => StatusCode::UNAUTHORIZED, - AccountErrorCode::ValidationError(_) => StatusCode::BAD_REQUEST, - AccountErrorCode::UsernameTaken(_) | AccountErrorCode::EmailTaken(_) => { - StatusCode::CONFLICT - } - } - } - - fn error_code(&self) -> &'static str { - match self { - AccountErrorCode::InvalidVerificationCode => "INVALID_VERIFICATION_CODE", - AccountErrorCode::ValidationError(_) => "VALIDATION_ERROR", - AccountErrorCode::UsernameTaken(_) => "USERNAME_TAKEN", - AccountErrorCode::EmailTaken(_) => "EMAIL_TAKEN", - } - } -} diff --git a/crates/core/src/session/login.rs b/crates/core/src/session/login.rs deleted file mode 100644 index 10066ef..0000000 --- a/crates/core/src/session/login.rs +++ /dev/null @@ -1,24 +0,0 @@ -use matrix::client::session::login::*; - -use crate::{error::Result, util::secret::Secret}; - -pub async fn service( - handle: &matrix::Handle, - username: &str, - password: &Secret, -) -> Result { - let req = Request::new( - LoginType::Password { - password: password.to_string(), - }, - Some(Identifier::User { - user: username.to_owned(), - }), - "commune".to_owned(), - Some(true), - ); - - let resp: Response = handle.dispatch(None, req).await?; - - Ok(resp) -} diff --git a/crates/core/src/session/logout.rs b/crates/core/src/session/logout.rs deleted file mode 100644 index c69670b..0000000 --- a/crates/core/src/session/logout.rs +++ /dev/null @@ -1,12 +0,0 @@ - -use matrix::client::session::logout::*; - -use crate::error::Result; - -pub async fn service(handle: &matrix::Handle) -> Result { - let req = Request::new(); - - let resp: Response = handle.dispatch(None, req).await?; - - Ok(resp) -} diff --git a/crates/core/src/session/register.rs b/crates/core/src/session/register.rs deleted file mode 100644 index b340106..0000000 --- a/crates/core/src/session/register.rs +++ /dev/null @@ -1,77 +0,0 @@ -use http::StatusCode; -use matrix::{ - client::{ - session::register::*, - uiaa::{AuthType, UiaaRequest, UiaaResponse}, - }, - ruma_client::Error::FromHttpResponse, - ruma_common::api::error::{FromHttpResponseError, MatrixError, MatrixErrorBody}, -}; - -use crate::{error::Result, util::secret::Secret}; - -pub async fn service( - handle: &matrix::Handle, - username: &str, - password: &Secret, -) -> Result { - let req = Request::new(username, password.inner(), "commune", None); - - let resp = handle.dispatch(None, req).await; - - match resp { - Ok(resp) => Ok(resp), - Err(e) => match e { - FromHttpResponse(FromHttpResponseError::Server(MatrixError { - status_code: StatusCode::UNAUTHORIZED, - body: MatrixErrorBody::Json(ref body), - })) => { - let UiaaResponse { flows, session, .. } = - serde_json::from_value::(body.clone()).unwrap(); - - match flows.as_slice() { - [value] => match value.stages.as_slice() { - [AuthType::Dummy] => { - let req = Request::new( - username, - password.inner(), - "commune", - Some(UiaaRequest { - session, - kind: AuthType::Dummy, - }), - ); - - handle.dispatch(None, req).await.map_err(Into::into) - } - _ => Err(e.into()), - }, - _ => Err(e.into()), - } - } - - _ => Err(e.into()), - }, - } -} - -// pub async fn verify_email( -// handle: matrix::Handle, -// username: &str, -// password: Secret, -// email: &str, -// ) -> Result { -// use session::register::*; - -// let verification_code: String = rand::thread_rng() -// .gen::<[u32; 6]>() -// .iter() -// .flat_map(|i| char::from_digit(i % 10, 10)) -// .collect(); - -// let req = Request::new(username, password.inner(), "commune beta"); - -// let resp: Response = handle.dispatch(None, req).await?; - -// Ok(AccessToken::new(resp.access_token)) -// } diff --git a/crates/core/src/session/service.rs b/crates/core/src/session/service.rs deleted file mode 100644 index 8c46308..0000000 --- a/crates/core/src/session/service.rs +++ /dev/null @@ -1,387 +0,0 @@ -use std::sync::Arc; - -use matrix::{ - admin::resources::user::UserService, client::resources::session::Session, ruma_common::UserId, Handle, -}; -use tracing::instrument; -use url::Url; -use uuid::Uuid; -use validator::{Validate, ValidationError}; - -use matrix::{ - admin::resources::user::{CreateUserBody, ListUsersQuery, LoginAsUserBody, ThreePid}, - Client as MatrixAdminClient, -}; - -use crate::{ - auth::service::AuthService, - mail::service::{EmailTemplate, MailService}, - util::{secret::Secret, time::timestamp}, - Error, Result, -}; - -use super::{error::AccountErrorCode, model::Account}; - -const DEFAULT_AVATAR_URL: &str = "https://via.placeholder.com/150"; -const MIN_USERNAME_LENGTH: usize = 1; -const MAX_USERNAME_LENGTH: usize = 255; -const MIN_PASSWORD_LENGTH: usize = 8; - -#[derive(Debug, Validate)] -pub struct SendCodeDto { - #[validate(email)] - pub email: String, - pub session: Uuid, -} - -#[derive(Debug, Validate)] -pub struct VerifyCodeDto { - #[validate(email)] - pub email: String, - pub session: Uuid, - pub code: Secret, -} - -#[derive(Debug, Validate)] -pub struct CreateAccountDto { - #[validate(custom = "CreateAccountDto::validate_username")] - pub username: String, - #[validate(custom = "CreateAccountDto::validate_password")] - pub password: Secret, - #[validate(email)] - pub email: String, - pub session: Uuid, - pub code: Secret, -} - -#[derive(Debug, Validate)] -pub struct CreateUnverifiedAccountDto { - #[validate(custom = "CreateAccountDto::validate_username")] - pub username: String, - #[validate(custom = "CreateAccountDto::validate_password")] - pub password: Secret, - #[validate(email)] - pub email: String, -} - -impl CreateAccountDto { - /// Validation logic for usernames enforced in user creation - fn validate_username(username: &str) -> std::result::Result<(), ValidationError> { - if username.len() < MIN_USERNAME_LENGTH { - return Err(ValidationError::new("username is too short")); - } - - if username.len() > MAX_USERNAME_LENGTH { - return Err(ValidationError::new("username is too long")); - } - - if username.contains(' ') { - return Err(ValidationError::new("username cannot contain spaces")); - } - - if username.to_ascii_lowercase() != username { - return Err(ValidationError::new( - "username cannot contain uppercase letters", - )); - } - - Ok(()) - } - - /// Validation logic for passwords enforced in user creation - fn validate_password(password: &Secret) -> std::result::Result<(), ValidationError> { - if password.inner().len() < MIN_PASSWORD_LENGTH { - return Err(ValidationError::new("password is too short")); - } - - Ok(()) - } -} - -pub struct AccountService { - matrix: Arc, - mail: Arc, -} - -impl AccountService { - pub fn new( - admin: Arc, - auth: Arc, - mail: Arc, - ) -> Self { - Self { admin, auth, mail } - } - - /// Returs `true` if the given `email address` is NOT registered in the - /// Matrix Server - pub async fn is_email_available(&self, email: &str) -> Result { - let user_id = format!("@{}:{}", email, self.admin.server_name()); - let user_id = <&UserId>::try_from(user_id.as_str()).map_err(|err| { - // TODO - tracing::error!(?err, "Failed to parse username"); - Error::Unknown - })?; - - let exists = UserService::list( - &self.admin, - ListUsersQuery { - user_id: Some(user_id.to_string()), - ..Default::default() - }, - ) - .await - .map_err(|err| { - tracing::error!(?err, "Failed to list users"); - Error::Unknown - })?; - - Ok(exists.users.is_empty()) - } - - /// Sends a verification code to the given email address - #[instrument(skip(self, dto))] - pub async fn send_code(&self, dto: SendCodeDto) -> Result<()> { - let verification_code = self - .auth - .send_verification_code(&dto.email, &dto.session) - .await?; - - self.mail - .send_mail( - String::from("onboarding@commune.sh"), - dto.email, - EmailTemplate::VerificationCode { - code: verification_code.code, - }, - ) - .await?; - - Ok(()) - } - - /// Verifies the given verification code against the given email address - /// and session id - #[instrument(skip(self, dto))] - pub async fn verify_code(&self, dto: VerifyCodeDto) -> Result { - let result = self - .auth - .check_verification_code(&dto.email, &dto.session, &dto.code) - .await?; - - Ok(result) - } - - /// Registers a new user account in Matrix Server - #[instrument(skip(self, dto))] - pub async fn register(&self, dto: CreateAccountDto) -> Result { - if !self - .auth - .check_verification_code(&dto.email, &dto.session, &dto.code) - .await? - { - return Err(AccountErrorCode::InvalidVerificationCode.into()); - } - - let account = self - .register_unverified(CreateUnverifiedAccountDto { - username: dto.username, - password: dto.password, - email: dto.email.to_owned(), - }) - .await?; - - self.auth - .drop_verification_code(&dto.email, &dto.session) - .await?; - - Ok(account) - } - - /// Registers a new user account in Matrix Server without verifying the - /// email ownership. This shuld be used for testing purposes only. - #[instrument(skip(self, dto))] - pub async fn register_unverified(&self, dto: CreateUnverifiedAccountDto) -> Result { - dto.validate().map_err(|err| { - tracing::warn!(?err, "Failed to validate user creation dto"); - AccountErrorCode::from(err) - })?; - - if !self.is_email_available(&dto.email).await? { - return Err(AccountErrorCode::EmailTaken(dto.email).into()); - } - - let user_id = format!("@{}:{}", dto.username, self.admin.server_name()); - let user_id = <&UserId>::try_from(user_id.as_str()).map_err(|err| { - // TODO - tracing::error!(?err, "Failed to parse username"); - Error::Unknown - })?; - - let avatar_url = Url::parse(DEFAULT_AVATAR_URL).map_err(|err| { - tracing::error!(?err, "Failed to parse default avatar url"); - Error::Unknown - })?; - - UserService::create( - &self.admin, - user_id, - CreateUserBody { - displayname: Some(dto.username), - password: dto.password.to_string(), - logout_devices: false, - avatar_url: Some(avatar_url), - threepids: vec![ThreePid { - medium: "email".to_string(), - address: dto.email.clone(), - added_at: timestamp()?, - validated_at: timestamp()?, - }], - external_ids: Vec::default(), - admin: false, - deactivated: false, - user_type: None, - locked: false, - }, - ) - .await - .map_err(|err| { - tracing::error!(?err, "Failed to create user"); - Error::Unknown - })?; - - let matrix_account = UserService::query_user_account(&self.admin, user_id) - .await - .map_err(|err| { - tracing::error!(?err, "Failed to query user account"); - Error::Unknown - })?; - let account = Account { - user_id: user_id.into(), - username: matrix_account.name, - email: matrix_account - .threepids - .first() - .map(|t| t.address.clone()) - .unwrap_or_default(), - display_name: matrix_account.displayname.unwrap_or_default(), - avatar_url: matrix_account.avatar_url, - age: 0, - admin: matrix_account.admin, - verified: true, - }; - - Ok(account) - } - - /// Creates an access token for the given user - pub async fn issue_user_token(&self, user_id: &UserId) -> Result { - let credentials = - UserService::login_as_user(&self.admin, user_id, LoginAsUserBody::default()) - .await - .map_err(|err| { - tracing::error!(?err, ?user_id, "Failed to login as user"); - Error::Unknown - })?; - - Ok(credentials.access_token) - } - - pub async fn whoami(&self, access_token: &Secret) -> Result { - let session = Session::get(&self.admin, access_token.to_string()) - .await - .map_err(|err| { - tracing::error!(?err, "Failed to get session from matrix as client"); - Error::Unknown - })?; - let matrix_account = UserService::query_user_account(&self.admin, &session.user_id) - .await - .map_err(|err| { - tracing::error!(?err, "Failed to query user account"); - Error::Unknown - })?; - let account = Account { - user_id: session.user_id, - username: matrix_account.name, - email: matrix_account - .threepids - .first() - .map(|t| t.address.clone()) - .unwrap_or_default(), - display_name: matrix_account.displayname.unwrap_or_default(), - avatar_url: matrix_account.avatar_url, - age: 0, - admin: matrix_account.admin, - verified: true, - }; - - Ok(account) - } -} - -#[cfg(test)] -mod test { - use uuid::Uuid; - use validator::Validate; - - use crate::util::secret::Secret; - - use super::CreateAccountDto; - - #[test] - fn ensure_username_is_not_too_short() { - let dto = CreateAccountDto { - username: "".to_string(), - password: Secret::new("password"), - email: "aby@mail.com".to_string(), - code: Secret::new("1234"), - session: Uuid::new_v4(), - }; - let err = dto.validate().err().unwrap(); - - assert!(err.to_string().contains("username is too short")); - } - - #[test] - fn ensure_username_is_not_too_long() { - let dto = CreateAccountDto { - username: (0..300).map(|_| "a").collect(), - password: Secret::new("password"), - email: "aby@mail.com".to_string(), - code: Secret::new("1234"), - session: Uuid::new_v4(), - }; - let err = dto.validate().err().unwrap(); - - assert!(err.to_string().contains("username is too long")); - } - - #[test] - fn ensure_username_does_not_contain_spaces() { - let dto = CreateAccountDto { - username: "abbey road".to_string(), - password: Secret::new("password"), - email: "aby@mail.com".to_string(), - code: Secret::new("1234"), - session: Uuid::new_v4(), - }; - let err = dto.validate().err().unwrap(); - - assert!(err.to_string().contains("username cannot contain spaces")); - } - - #[test] - fn ensure_username_is_lowercased() { - let dto = CreateAccountDto { - username: "AbbeyRoad".to_string(), - password: Secret::new("password"), - email: "aby@mail.com".to_string(), - code: Secret::new("1234"), - session: Uuid::new_v4(), - }; - let err = dto.validate().err().unwrap(); - - assert!(err - .to_string() - .contains("username cannot contain uppercase letters")); - } -} diff --git a/crates/core/src/session/username_available.rs b/crates/core/src/session/username_available.rs deleted file mode 100644 index 7855c47..0000000 --- a/crates/core/src/session/username_available.rs +++ /dev/null @@ -1,11 +0,0 @@ -use matrix::client::session::username_available::*; - -use crate::error::Result; - -pub async fn service(handle: &matrix::Handle, username: &str) -> Result { - let req = Request::new(username); - - let resp: Response = handle.dispatch(None, req).await?; - - Ok(resp) -} diff --git a/crates/core/src/util/secret.rs b/crates/core/src/util/secret.rs index 2517468..d478867 100644 --- a/crates/core/src/util/secret.rs +++ b/crates/core/src/util/secret.rs @@ -1,62 +1,38 @@ -use std::{ - borrow::Cow, - convert::Infallible, - fmt::{Debug, Display}, - str::FromStr, -}; +use std::fmt::{Debug, Display}; -use serde::{Deserialize, Serialize}; +use rand::{distributions::Uniform, Rng}; +use serde::Deserialize; -/// A `String` wrapper that does not display the value when debugged or -/// displayed. -#[derive(Clone, Deserialize, PartialEq, Eq, Hash, Serialize)] -pub struct Secret(Cow<'static, str>); +#[derive(Deserialize)] +pub struct Secret(String); impl Secret { - pub fn new(s: impl Into>) -> Self { - Secret(s.into()) - } - #[inline] - pub fn inner(&self) -> &str { - &self.0 - } - - /// Returs inner value as [`String`] - /// - /// # Shadowing Note - /// - /// Intentially shadows [`std::string::ToString::to_string`] to prevent - /// getting `"[REDACTED]"` when using `to_string`. - #[allow(clippy::inherent_to_string_shadow_display)] - pub fn to_string(&self) -> String { - self.0.to_string() + pub fn new(s: impl Into) -> Self { + Self(s.into()) } -} -impl From for Secret { - fn from(s: String) -> Self { - Secret(Cow::Owned(s)) - } -} - -impl FromStr for Secret { - type Err = Infallible; - - fn from_str(s: &str) -> Result { - Ok(Secret(Cow::Owned(s.to_owned()))) + #[inline] + pub fn inner(&self) -> String { + self.0.clone() } } impl Debug for Secret { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("[REDACTED]") + f.write_str(format!("{self}").as_str()) } } impl Display for Secret { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("[REDACTED]") + let braille_range = Uniform::new('\u{2800}', '\u{28FF}'); + let s: String = rand::thread_rng() + .sample_iter(braille_range) + .take(self.0.len()) + .collect(); + + f.write_str(s.as_str()) } } @@ -85,6 +61,6 @@ mod tests { let secret = Secret::new("secret"); let value = secret.inner(); - assert_eq!(value, "secret"); + assert_eq!(value, "secret".into()); } } diff --git a/crates/core/src/util/time.rs b/crates/core/src/util/time.rs deleted file mode 100644 index e722064..0000000 --- a/crates/core/src/util/time.rs +++ /dev/null @@ -1,3 +0,0 @@ -use std::time::{SystemTime, UNIX_EPOCH}; - -use crate::error::Error; diff --git a/crates/matrix/Cargo.toml b/crates/matrix/Cargo.toml index b48132e..25a1738 100644 --- a/crates/matrix/Cargo.toml +++ b/crates/matrix/Cargo.toml @@ -13,6 +13,7 @@ ruma-events = { workspace = true } ruma-common = { workspace = true } ruma-macros = { workspace = true } ruma-client = { workspace = true } +ruma-identifiers-validation = { workspace = true } # Workspace Dependencies mime = { workspace = true } @@ -28,8 +29,6 @@ hmac = { workspace = true } http = { workspace = true } bytes = { workspace = true } async-trait = { workspace = true } -async-stream = { workspace = true } -futures = { workspace = true } [features] client = [] diff --git a/crates/matrix/src/admin/mod.rs b/crates/matrix/src/admin.rs similarity index 64% rename from crates/matrix/src/admin/mod.rs rename to crates/matrix/src/admin.rs index 75a9d6d..2608ca2 100644 --- a/crates/matrix/src/admin/mod.rs +++ b/crates/matrix/src/admin.rs @@ -2,6 +2,7 @@ //! //! reference: https://matrix-org.github.io/synapse/latest/usage/administration/admin_api/index.html -pub mod room; -pub mod session; -pub mod user; +pub mod registration_tokens; +// pub mod room; +// pub mod session; +// pub mod user; diff --git a/crates/matrix/src/admin/registration_tokens.rs b/crates/matrix/src/admin/registration_tokens.rs new file mode 100644 index 0000000..9d52a2b --- /dev/null +++ b/crates/matrix/src/admin/registration_tokens.rs @@ -0,0 +1 @@ +pub mod new; diff --git a/crates/matrix/src/admin/registration_tokens/new.rs b/crates/matrix/src/admin/registration_tokens/new.rs new file mode 100644 index 0000000..5c3578e --- /dev/null +++ b/crates/matrix/src/admin/registration_tokens/new.rs @@ -0,0 +1,42 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_synapse/admin/v1/register/new", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + pub token: String, + + pub uses_allowed: usize, + + pub expiry_time: usize, +} + +impl Request { + pub fn new( + token: String, + uses_allowed: usize, + expiry_time: usize, + ) -> Self { + Self { + token, + uses_allowed, + expiry_time, + } + } +} + +// Same fields as above are returned but we only +// care about knowing whether the call was successful. +#[response(error = crate::Error)] +pub struct Response {} diff --git a/crates/matrix/src/client-backup/account.rs.bk.bk b/crates/matrix/src/client-backup/account.rs.bk.bk new file mode 100644 index 0000000..d48afb7 --- /dev/null +++ b/crates/matrix/src/client-backup/account.rs.bk.bk @@ -0,0 +1,3 @@ +pub mod create; +pub mod password; +pub mod whoami; diff --git a/crates/matrix/src/client-backup/events.rs.bk.bk.bk b/crates/matrix/src/client-backup/events.rs.bk.bk.bk new file mode 100644 index 0000000..2953b2b --- /dev/null +++ b/crates/matrix/src/client-backup/events.rs.bk.bk.bk @@ -0,0 +1,310 @@ +use anyhow::Result; +use ruma_common::{serde::Raw, EventId, OwnedEventId, OwnedTransactionId, RoomId}; + +use ruma_events::{ + relation::RelationType, AnyMessageLikeEvent, AnyStateEvent, AnyStateEventContent, + AnyTimelineEvent, MessageLikeEventContent, MessageLikeEventType, StateEventContent, + StateEventType, +}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use tracing::instrument; + +use crate::{admin::resources::room::Direction, error::MatrixError, Client}; + +pub struct EventsService; + +#[derive(Debug, Default, Clone, Serialize)] +pub struct GetMessagesQuery { + #[serde(skip_serializing_if = "Option::is_none")] + pub from: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub to: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + + pub dir: Direction, + + #[serde(skip_serializing_if = "String::is_empty")] + pub filter: String, +} + +#[derive(Debug, Default, Clone, Serialize)] +pub struct GetRelationsQuery { + #[serde(skip_serializing_if = "Option::is_none")] + pub from: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub to: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + + pub dir: Direction, +} + +#[derive(Debug, Deserialize)] +pub struct GetMessagesResponse { + pub chunk: Vec>, + pub start: String, + pub end: String, + pub state: Option>>, +} + +#[derive(Debug, Deserialize)] +#[serde(transparent)] +pub struct GetStateResponse(pub Vec>); + +#[derive(Debug, Deserialize)] +pub struct GetRelationsResponse { + pub chunk: Vec>, + pub prev_batch: Option, + pub next_batch: Option, +} + +#[derive(Debug, Default, Serialize)] +pub struct SendRedactionBody { + #[serde(skip_serializing_if = "String::is_empty")] + pub reason: String, +} + +#[derive(Debug, Deserialize)] +pub struct SendMessageResponse { + pub event_id: OwnedEventId, +} + +#[derive(Debug, Deserialize)] +pub struct SendStateResponse { + pub event_id: OwnedEventId, +} + +#[derive(Debug, Deserialize)] +pub struct SendRedactionResponse { + pub event_id: OwnedEventId, +} + +impl EventsService { + #[instrument(skip(client, access_token))] + pub async fn get_event( + client: &Client, + access_token: impl Into, + room_id: &RoomId, + event_id: &EventId, + ) -> Result> { + let mut tmp = (*client).clone(); + tmp.set_token(access_token)?; + + let resp = tmp + .get(format!( + "/_matrix/client/v3/rooms/{room_id}/event/{event_id}", + room_id = room_id, + event_id = event_id, + )) + .await?; + + Ok(resp.json().await?) + } + + #[instrument(skip(client, access_token))] + pub async fn get_messages( + client: &Client, + access_token: impl Into, + room_id: &RoomId, + query: GetMessagesQuery, + ) -> Result { + let mut tmp = (*client).clone(); + tmp.set_token(access_token)?; + + let resp = tmp + .get_query( + format!( + "/_matrix/client/v3/rooms/{room_id}/messages", + room_id = room_id, + ), + &query, + ) + .await?; + + Ok(resp.json().await?) + } + + #[instrument(skip(client, access_token))] + pub async fn get_state( + client: &Client, + access_token: impl Into, + room_id: &RoomId, + ) -> Result { + let mut tmp = (*client).clone(); + tmp.set_token(access_token)?; + + let resp = tmp + .get(format!( + "/_matrix/client/v3/rooms/{room_id}/state", + room_id = room_id, + )) + .await?; + + Ok(resp.json().await?) + } + + #[instrument(skip(client, access_token))] + pub async fn get_state_content( + client: &Client, + access_token: impl Into, + room_id: &RoomId, + event_type: StateEventType, + state_key: Option, + ) -> Result> { + let mut tmp = (*client).clone(); + tmp.set_token(access_token)?; + + let mut path = format!( + "/_matrix/client/v3/rooms/{room_id}/state/{event_type}", + room_id = room_id, + event_type = event_type, + ); + + if let Some(state_key) = state_key { + path.push_str(&format!("/{state_key}", state_key = state_key)) + } + + let resp = tmp.get(path).await?; + + Ok(resp.json().await?) + } + + #[instrument(skip(client, access_token))] + pub async fn get_relations( + client: &Client, + access_token: impl Into, + room_id: &RoomId, + event_id: &EventId, + rel_type: Option>, + event_type: Option, + query: GetRelationsQuery, + ) -> Result { + let mut tmp = (*client).clone(); + tmp.set_token(access_token)?; + + let mut path = format!( + "/_matrix/client/v3/rooms/{room_id}/relations/{event_id}", + room_id = room_id, + event_id = event_id, + ); + + if let Some(rel_type) = rel_type { + path.push_str(&format!( + "/{rel_type}", + rel_type = rel_type + .as_ref() + .map_or("m.in_reply_to".into(), ToString::to_string) + )); + + if let Some(event_type) = event_type { + path.push_str(&format!("/{event_type}", event_type = event_type)) + } + } + + let resp = tmp.get_query(path, &query).await?; + + Ok(resp.json().await?) + } + + #[instrument(skip(client, access_token, body))] + pub async fn send_message( + client: &Client, + access_token: impl Into, + room_id: &RoomId, + txn_id: OwnedTransactionId, + body: T, + ) -> Result { + let mut tmp = (*client).clone(); + tmp.set_token(access_token)?; + + let resp = tmp + .put_json( + format!( + "/_matrix/client/v3/rooms/{room_id}/send/{event_type}/{txn_id}", + room_id = room_id, + event_type = body.event_type(), + txn_id = txn_id, + ), + &body, + ) + .await?; + + if resp.status().is_success() { + return Ok(resp.json().await?); + } + + let error = resp.json::().await?; + + Err(anyhow::anyhow!(error.error)) + } + + #[instrument(skip(client, access_token, body))] + pub async fn send_state( + client: &Client, + access_token: impl Into, + room_id: &RoomId, + state_key: Option, + body: T, + ) -> Result { + let mut tmp = (*client).clone(); + tmp.set_token(access_token)?; + + let mut path = format!( + "/_matrix/client/v3/rooms/{room_id}/state/{event_type}", + room_id = room_id, + event_type = body.event_type(), + ); + + if let Some(state_key) = state_key { + path.push_str(&format!("/{state_key}", state_key = state_key)) + } + + let resp = tmp.put_json(path, &body).await?; + + if resp.status().is_success() { + return Ok(resp.json().await?); + } + + let error = resp.json::().await?; + + Err(anyhow::anyhow!(error.error)) + } + + #[instrument(skip(client, access_token, body))] + pub async fn send_redaction( + client: &Client, + access_token: impl Into, + room_id: &RoomId, + event_id: &EventId, + txn_id: OwnedTransactionId, + body: SendRedactionBody, + ) -> Result { + let mut tmp = (*client).clone(); + tmp.set_token(access_token)?; + + let resp = tmp + .put_json( + format!( + "/_matrix/client/v3/rooms/{room_id}/redact/{event_id}/{txn_id}", + room_id = room_id, + event_id = event_id, + txn_id = txn_id, + ), + &body, + ) + .await?; + + if resp.status().is_success() { + return Ok(resp.json().await?); + } + + let error = resp.json::().await?; + + Err(anyhow::anyhow!(error.error)) + } +} diff --git a/crates/matrix/src/client-backup/membership.rs.bk.bk b/crates/matrix/src/client-backup/membership.rs.bk.bk new file mode 100644 index 0000000..86c3778 --- /dev/null +++ b/crates/matrix/src/client-backup/membership.rs.bk.bk @@ -0,0 +1,5 @@ +pub mod ban; +pub mod join; +pub mod kick; +pub mod leave; +pub mod unban; diff --git a/crates/matrix/src/client/mod.rs b/crates/matrix/src/client-backup/mod.rs.bk.bk similarity index 76% rename from crates/matrix/src/client/mod.rs rename to crates/matrix/src/client-backup/mod.rs.bk.bk index 92bed12..cc296e6 100644 --- a/crates/matrix/src/client/mod.rs +++ b/crates/matrix/src/client-backup/mod.rs.bk.bk @@ -2,7 +2,9 @@ //! //! reference: https://spec.matrix.org/unstable/client-server-api -pub mod room; +pub mod rooms; pub mod session; +pub mod membership; pub mod uiaa; pub mod sync; +pub mod account; diff --git a/crates/matrix/src/client-backup/mxc.rs.bk.bk.bk b/crates/matrix/src/client-backup/mxc.rs.bk.bk.bk new file mode 100644 index 0000000..7dc669a --- /dev/null +++ b/crates/matrix/src/client-backup/mxc.rs.bk.bk.bk @@ -0,0 +1,184 @@ +use std::str::FromStr; + +use anyhow::Result; +use mime::Mime; +use ruma_common::{MxcUri, OwnedMxcUri}; +use serde::{de, Deserialize, Deserializer, Serialize}; +use tracing::instrument; + +use chrono::{serde::ts_microseconds_option, DateTime, Utc}; + +use crate::error::MatrixError; + +fn parse_mime_opt<'de, D>(d: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + Option::<&str>::deserialize(d)? + .map(::from_str) + .transpose() + .map_err(de::Error::custom) +} + +#[derive(Debug, Serialize)] +pub struct GetPreviewUrlQuery { + pub url: url::Url, + pub ts: DateTime, +} + +#[derive(Debug, Deserialize)] +pub struct CreateMxcUriResponse { + pub content_uri: String, + + #[serde(with = "ts_microseconds_option")] + pub unused_expires_at: Option>, +} + +#[derive(Debug, Deserialize)] +pub struct GetPreviewUrlResponse { + #[serde(rename = "matrix:image_size")] + pub image_size: Option, + + #[serde(rename = "og:description")] + pub description: Option, + + #[serde(rename = "og:image")] + pub image: Option, + + #[serde(rename = "og:image:height")] + pub height: Option, + + #[serde(rename = "og:image:width")] + pub width: Option, + + #[serde(rename = "og:image:type", deserialize_with = "parse_mime_opt")] + pub kind: Option, + + #[serde(rename = "og:title")] + pub title: Option, +} + +#[derive(Debug, Deserialize)] +pub struct GetConfigResponse { + #[serde(rename = "m.upload.size")] + pub upload_size: Option, +} + +#[derive(Debug, Serialize)] +pub enum ResizeMethod { + Crop, + Scale, +} + +pub struct MxcService; + +#[derive(Debug, Deserialize)] +pub struct MxcError { + #[serde(flatten)] + pub inner: MatrixError, + + pub retry_after_ms: u64, +} + +impl MxcService { + /// Creates a new `MxcUri`, independently of the content being uploaded + /// + /// Refer: https://spec.matrix.org/v1.9/client-server-api/#post_matrixmediav1create + #[instrument(skip(client, access_token))] + pub async fn create( + client: &crate::http::Client, + access_token: impl Into, + ) -> Result { + let mut tmp = (*client).clone(); + tmp.set_token(access_token)?; + + let resp = tmp.post("/_matrix/media/v1/create").await?; + + if resp.status().is_success() { + return Ok(resp.json().await?); + } + + let error = resp.json::().await?; + + Err(anyhow::anyhow!(error.inner.error)) + } + + /// Retrieve the configuration of the content repository + /// + /// Refer: https://spec.matrix.org/v1.9/client-server-api/#get_matrixmediav3config + #[instrument(skip(client, access_token))] + pub async fn get_config( + client: &crate::http::Client, + access_token: impl Into, + ) -> Result { + let mut tmp = (*client).clone(); + tmp.set_token(access_token)?; + + let resp = tmp.get("/_matrix/media/v3/config").await?; + + if resp.status().is_success() { + return Ok(resp.json().await?); + } + + let error = resp.json::().await?; + + Err(anyhow::anyhow!(error.inner.error)) + } + + /// Retrieve a URL to download content from the content repository, + /// optionally replacing the name of the file. + /// + /// Refer: https://spec.matrix.org/v1.9/client-server-api/#get_matrixmediav3downloadservernamemediaid + #[instrument(skip(client, access_token))] + pub async fn get_download_url( + client: &crate::http::Client, + access_token: impl Into, + mxc_uri: &MxcUri, + mut base_url: url::Url, + file_name: Option, + ) -> Result { + let mut tmp = (*client).clone(); + tmp.set_token(access_token)?; + + let (server_name, media_id) = mxc_uri.parts().unwrap(); + + let mut path = format!( + "/_matrix/media/v3/download/{server_name}/{media_id}", + server_name = server_name, + media_id = media_id, + ); + + if let Some(file_name) = file_name { + path.push_str(&format!("/{file_name}", file_name = file_name)) + } + + base_url.set_path(&path); + + Ok(base_url) + } + + /// + /// + /// Refer: https://spec.matrix.org/v1.9/client-server-api/#get_matrixmediav3preview_url + #[instrument(skip(client, access_token))] + pub async fn get_preview( + client: &crate::http::Client, + access_token: impl Into, + query: GetPreviewUrlQuery, + ) -> Result { + let mut tmp = (*client).clone(); + tmp.set_token(access_token)?; + + let resp = tmp + .get_query("/_matrix/media/v3/preview_url".to_string(), &query) + .await?; + + if resp.status().is_success() { + return Ok(resp.json().await?); + } + + let error = resp.json::().await?; + + Err(anyhow::anyhow!(error.inner.error)) + } +} diff --git a/crates/matrix/src/client-backup/rooms.rs.bk.bk b/crates/matrix/src/client-backup/rooms.rs.bk.bk new file mode 100644 index 0000000..2d4cdf8 --- /dev/null +++ b/crates/matrix/src/client-backup/rooms.rs.bk.bk @@ -0,0 +1,6 @@ +//! This module contains handlers to interact with rooms. +//! +//! reference: https://spec.matrix.org/unstable/client-server-api/#rooms + +pub mod create; +pub mod forget; diff --git a/crates/matrix/src/client-backup/session.rs.bk.bk.bk b/crates/matrix/src/client-backup/session.rs.bk.bk.bk new file mode 100644 index 0000000..6bceee3 --- /dev/null +++ b/crates/matrix/src/client-backup/session.rs.bk.bk.bk @@ -0,0 +1,2 @@ +pub mod create; +pub mod invalidate; diff --git a/crates/matrix/src/client/sync.rs b/crates/matrix/src/client-backup/sync.rs.bk.bk similarity index 100% rename from crates/matrix/src/client/sync.rs rename to crates/matrix/src/client-backup/sync.rs.bk.bk diff --git a/crates/matrix/src/client-backup/uiaa.rs.bk.bk b/crates/matrix/src/client-backup/uiaa.rs.bk.bk new file mode 100644 index 0000000..c417d30 --- /dev/null +++ b/crates/matrix/src/client-backup/uiaa.rs.bk.bk @@ -0,0 +1,164 @@ +//! Module for [User-Interactive Authentication API][uiaa] types. +//! +//! [uiaa]: https://spec.matrix.org/latest/client-server-api/#user-interactive-authentication-api + +use ruma_common::{thirdparty::Medium, OwnedSessionId, OwnedUserId, UserId}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct UiaaResponse { + pub flows: Vec, + + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub completed: Vec, + + pub params: Box, + + #[serde(skip_serializing_if = "Option::is_none")] + pub session: Option, + // #[serde(flatten, skip_serializing_if = "Option::is_none")] + // pub auth_error: Option, +} + +/// Ordered list of stages required to complete authentication. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct AuthFlow { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub stages: Vec, +} + +impl AuthFlow { + pub fn new(stages: Vec) -> Self { + Self { stages } + } +} + +/// Information for one authentication stage. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[non_exhaustive] +pub enum AuthType { + /// Password-based authentication (`m.login.password`). + #[serde(rename = "m.login.password")] + Password, + + /// Google ReCaptcha 2.0 authentication (`m.login.recaptcha`). + #[serde(rename = "m.login.recaptcha")] + ReCaptcha, + + /// Email-based authentication (`m.login.email.identity`). + #[serde(rename = "m.login.email.identity")] + EmailIdentity, + + /// Phone number-based authentication (`m.login.msisdn`). + #[serde(rename = "m.login.msisdn")] + Msisdn, + + /// SSO-based authentication (`m.login.sso`). + #[serde(rename = "m.login.sso")] + Sso, + + /// Dummy authentication (`m.login.dummy`). + #[serde(rename = "m.login.dummy")] + Dummy, + + /// Registration token-based authentication (`m.login.registration_token`). + #[serde(rename = "m.login.registration_token")] + RegistrationToken, +} + +#[derive(Clone, Debug, Serialize)] +#[non_exhaustive] +#[serde(untagged)] +pub enum AuthData { + // Password-based authentication (`m.login.password`). + Password(Password), + + // Google ReCaptcha 2.0 authentication (`m.login.recaptcha`). + // ReCaptcha(ReCaptcha), + + // Email-based authentication (`m.login.email.identity`). + // EmailIdentity(EmailIdentity), + + // Phone number-based authentication (`m.login.msisdn`). + // Msisdn(Msisdn), + + // Dummy authentication (`m.login.dummy`). + Dummy(Dummy), + // Registration token-based authentication (`m.login.registration_token`). + // RegistrationToken(RegistrationToken), + + // Fallback acknowledgement. + // FallbackAcknowledgement(FallbackAcknowledgement), +} + +impl AuthData { + fn kind(&self) -> AuthType { + match self { + AuthData::Password(_) => AuthType::Password, + AuthData::Dummy(_) => AuthType::Dummy, + } + } +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[serde(tag = "type", rename = "m.login.dummy")] +pub struct Dummy {} + +impl Dummy { + pub fn new() -> Self { + Self::default() + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(tag = "type", rename = "m.login.password")] +pub struct Password { + identifier: UserIdentifier, + password: String, +} + +impl Password { + pub fn new>(user_id: impl Into, password: S) -> Self { + let user: &UserId = &user_id.into(); + + Self { + identifier: UserIdentifier::User { + user: user.localpart().to_owned(), + }, + password: password.into(), + } + } +} + +#[derive(Clone, Debug, Serialize)] +pub struct UiaaRequest { + session: Option, + + kind: AuthType, + + #[serde(flatten)] + data: AuthData, +} + +impl UiaaRequest { + pub fn new(data: AuthData, session: Option) -> Self { + Self { + session, + kind: data.kind(), + data, + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(tag = "type")] +pub enum UserIdentifier { + #[serde(rename = "m.id.user")] + User { user: String }, + + #[serde(rename = "m.id.thirdparty")] + ThirdParty { medium: Medium, address: String }, + + #[serde(rename = "m.id.phone")] + Phone { country: String, phone: String }, +} diff --git a/crates/matrix/src/client.rs b/crates/matrix/src/client.rs new file mode 100644 index 0000000..b9f8de2 --- /dev/null +++ b/crates/matrix/src/client.rs @@ -0,0 +1,10 @@ +//! This module is the root of the client-server API. +//! +//! reference: https://spec.matrix.org/unstable/client-server-api + +pub mod account; +pub mod login; +pub mod logout; +pub mod profile; +pub mod register; +pub mod uiaa; diff --git a/crates/matrix/src/client/account.rs b/crates/matrix/src/client/account.rs new file mode 100644 index 0000000..7f46308 --- /dev/null +++ b/crates/matrix/src/client/account.rs @@ -0,0 +1,2 @@ +pub mod password; +pub mod whoami; diff --git a/crates/matrix/src/client/account/password.rs b/crates/matrix/src/client/account/password.rs new file mode 100644 index 0000000..7ab5fc0 --- /dev/null +++ b/crates/matrix/src/client/account/password.rs @@ -0,0 +1,55 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedUserId, +}; +use serde::Serialize; + +use crate::client::uiaa::{self, Auth, AuthData}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: true, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/v3/account/password", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + pub auth: Auth, + + pub logout_devices: bool, + + pub new_password: String, +} + +impl Request { + pub fn new(new_password: String) -> Self { + Self { + auth: Auth::new(AuthData::Dummy(uiaa::Dummy {}), None), + logout_devices: false, + new_password, + } + } + + pub fn with_password( + mut self, + user_id: OwnedUserId, + password: String, + // auth_session: Option>, + ) -> Self { + self.auth = Auth::new( + AuthData::Password(uiaa::Password::new(user_id, password)), + // auth_session.map(Into::into), + None, + ); + + self + } +} + +#[response(error = crate::Error)] +#[derive(Serialize)] +pub struct Response {} diff --git a/crates/matrix/src/client/session/whoami.rs b/crates/matrix/src/client/account/whoami.rs similarity index 76% rename from crates/matrix/src/client/session/whoami.rs rename to crates/matrix/src/client/account/whoami.rs index 5913bfb..c2f1f73 100644 --- a/crates/matrix/src/client/session/whoami.rs +++ b/crates/matrix/src/client/account/whoami.rs @@ -2,6 +2,7 @@ use ruma_common::{ api::{request, response, Metadata}, metadata, OwnedDeviceId, OwnedUserId, }; +use serde::Serialize; #[allow(dead_code)] const METADATA: Metadata = metadata! { @@ -16,9 +17,16 @@ const METADATA: Metadata = metadata! { #[request(error = crate::Error)] pub struct Request {} +impl Request { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self {} + } +} + #[response(error = crate::Error)] +#[derive(Serialize)] pub struct Response { pub device_id: OwnedDeviceId, - pub user_id: OwnedUserId, } diff --git a/crates/matrix/src/client/session/login.rs b/crates/matrix/src/client/login.rs similarity index 72% rename from crates/matrix/src/client/session/login.rs rename to crates/matrix/src/client/login.rs index aa0d6d5..5b85734 100644 --- a/crates/matrix/src/client/session/login.rs +++ b/crates/matrix/src/client/login.rs @@ -1,11 +1,11 @@ use ruma_common::{ api::{request, response, Metadata}, - metadata, - thirdparty::Medium, - OwnedDeviceId, OwnedMxcUri, OwnedUserId, + metadata, OwnedDeviceId, OwnedMxcUri, OwnedUserId, }; use serde::{Deserialize, Serialize}; +use crate::client::uiaa::UserIdentifier; + #[allow(dead_code)] const METADATA: Metadata = metadata! { method: POST, @@ -22,7 +22,7 @@ pub struct Request { pub kind: LoginType, #[serde(skip_serializing_if = "Option::is_none")] - pub identifier: Option, + pub identifier: Option, #[serde( rename = "initial_device_display_name", @@ -37,7 +37,7 @@ pub struct Request { impl Request { pub fn new( kind: LoginType, - identifier: Option, + identifier: Option, device_name: String, refresh_token: Option, ) -> Self { @@ -51,7 +51,7 @@ impl Request { } #[response(error = crate::Error)] -#[derive(Deserialize, Serialize)] +#[derive(Serialize)] pub struct Response { pub access_token: String, @@ -69,25 +69,25 @@ pub struct Response { pub well_known: Option, } -impl Response { - pub fn new( - access_token: String, - device_id: OwnedDeviceId, - expires_in_ms: Option, - refresh_token: Option, - user_id: OwnedUserId, - well_known: Option, - ) -> Self { - Self { - access_token, - device_id, - expires_in_ms, - refresh_token, - user_id, - well_known, - } - } -} +// impl Response { +// pub fn new>( +// access_token: S, +// refresh_token: Option, +// expires_in_ms: Option, +// user_id: impl Into, +// device_id: impl Into, +// well_known: Option, +// ) -> Self { +// Self { +// access_token: access_token.into(), +// refresh_token: refresh_token.map(Into::into), +// expires_in_ms, +// device_id: device_id.into(), +// user_id: user_id.into(), +// well_known, +// } +// } +// } #[derive(Clone, Debug, Serialize)] pub struct IdentityProvider { @@ -119,19 +119,6 @@ pub enum LoginType { ApplicationService, } -#[derive(Clone, Debug, Serialize)] -#[serde(tag = "type")] -pub enum Identifier { - #[serde(rename = "m.id.user")] - User { user: String }, - - #[serde(rename = "m.id.thirdparty")] - ThirdParty { medium: Medium, address: String }, - - #[serde(rename = "m.id.phone")] - Phone { country: String, phone: String }, -} - #[derive(Clone, Debug, Deserialize, Serialize)] pub struct BaseUrl { pub base_url: url::Url, diff --git a/crates/matrix/src/client/logout.rs b/crates/matrix/src/client/logout.rs new file mode 100644 index 0000000..2422763 --- /dev/null +++ b/crates/matrix/src/client/logout.rs @@ -0,0 +1,2 @@ +pub mod all; +pub mod root; diff --git a/crates/matrix/src/client/logout/all.rs b/crates/matrix/src/client/logout/all.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/crates/matrix/src/client/logout/all.rs @@ -0,0 +1 @@ + diff --git a/crates/matrix/src/client/session/logout.rs b/crates/matrix/src/client/logout/root.rs similarity index 100% rename from crates/matrix/src/client/session/logout.rs rename to crates/matrix/src/client/logout/root.rs diff --git a/crates/matrix/src/client/profile.rs b/crates/matrix/src/client/profile.rs new file mode 100644 index 0000000..58428f4 --- /dev/null +++ b/crates/matrix/src/client/profile.rs @@ -0,0 +1,2 @@ +pub mod avatar_url; +pub mod display_name; diff --git a/crates/matrix/src/client/profile/avatar_url.rs b/crates/matrix/src/client/profile/avatar_url.rs new file mode 100644 index 0000000..0e93baa --- /dev/null +++ b/crates/matrix/src/client/profile/avatar_url.rs @@ -0,0 +1,2 @@ +pub mod get; +pub mod update; diff --git a/crates/matrix/src/client/profile/avatar_url/get.rs b/crates/matrix/src/client/profile/avatar_url/get.rs new file mode 100644 index 0000000..1d18ad6 --- /dev/null +++ b/crates/matrix/src/client/profile/avatar_url/get.rs @@ -0,0 +1,31 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedMxcUri, OwnedUserId, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: None, + history: { + unstable => "/_matrix/client/v3/profile/:user_id/avatar_url", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub user_id: OwnedUserId, +} + +impl Request { + pub fn new(user_id: OwnedUserId) -> Self { + Self { user_id } + } +} + +#[response(error = crate::Error)] +pub struct Response { + pub avatar_url: OwnedMxcUri, +} diff --git a/crates/matrix/src/client/room/unban_from_room.rs b/crates/matrix/src/client/profile/avatar_url/update.rs similarity index 50% rename from crates/matrix/src/client/room/unban_from_room.rs rename to crates/matrix/src/client/profile/avatar_url/update.rs index a34ace7..d291379 100644 --- a/crates/matrix/src/client/room/unban_from_room.rs +++ b/crates/matrix/src/client/profile/avatar_url/update.rs @@ -1,28 +1,36 @@ use ruma_common::{ api::{request, response, Metadata}, - metadata, OwnedRoomId, OwnedUserId, + metadata, OwnedMxcUri, OwnedUserId, }; +use serde::Serialize; #[allow(dead_code)] const METADATA: Metadata = metadata! { - method: POST, + method: PUT, rate_limited: true, authentication: AccessToken, history: { - unstable => "/_matrix/client/v3/rooms/{room_id}/unban", + unstable => "/_matrix/client/v3/profile/:user_id/avatar_url", } }; #[request(error = crate::Error)] pub struct Request { #[ruma_api(path)] - pub room_id: OwnedRoomId, - pub user_id: OwnedUserId, - #[serde(skip_serializing_if = "String::is_empty")] - pub reason: String, + pub avatar_url: OwnedMxcUri, +} + +impl Request { + pub fn new(user_id: OwnedUserId, avatar_url: OwnedMxcUri) -> Self { + Self { + user_id, + avatar_url, + } + } } #[response(error = crate::Error)] +#[derive(Serialize)] pub struct Response {} diff --git a/crates/matrix/src/client/profile/display_name.rs b/crates/matrix/src/client/profile/display_name.rs new file mode 100644 index 0000000..0e93baa --- /dev/null +++ b/crates/matrix/src/client/profile/display_name.rs @@ -0,0 +1,2 @@ +pub mod get; +pub mod update; diff --git a/crates/matrix/src/client/profile/display_name/get.rs b/crates/matrix/src/client/profile/display_name/get.rs new file mode 100644 index 0000000..7ce9d9a --- /dev/null +++ b/crates/matrix/src/client/profile/display_name/get.rs @@ -0,0 +1,32 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedUserId, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: None, + history: { + unstable => "/_matrix/client/v3/profile/:user_id/displayname", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub user_id: OwnedUserId, +} + +impl Request { + pub fn new(user_id: OwnedUserId) -> Self { + Self { user_id } + } +} + +#[response(error = crate::Error)] +pub struct Response { + #[serde(rename = "displayname")] + pub display_name: String, +} diff --git a/crates/matrix/src/client/room/kick_from_room.rs b/crates/matrix/src/client/profile/display_name/update.rs similarity index 51% rename from crates/matrix/src/client/room/kick_from_room.rs rename to crates/matrix/src/client/profile/display_name/update.rs index a618431..5cac54f 100644 --- a/crates/matrix/src/client/room/kick_from_room.rs +++ b/crates/matrix/src/client/profile/display_name/update.rs @@ -1,27 +1,34 @@ use ruma_common::{ api::{request, response, Metadata}, - metadata, OwnedRoomId, OwnedUserId, + metadata, OwnedUserId, }; #[allow(dead_code)] const METADATA: Metadata = metadata! { - method: POST, + method: PUT, rate_limited: true, authentication: AccessToken, history: { - unstable => "/_matrix/client/v3/rooms/{room_id}/kick", + unstable => "/_matrix/client/v3/profile/:user_id/displayname", } }; #[request(error = crate::Error)] pub struct Request { #[ruma_api(path)] - pub room_id: OwnedRoomId, - pub user_id: OwnedUserId, - #[serde(skip_serializing_if = "String::is_empty")] - pub reason: String, + #[serde(rename = "displayname")] + pub display_name: String, +} + +impl Request { + pub fn new(user_id: OwnedUserId, display_name: String) -> Self { + Self { + user_id, + display_name, + } + } } #[response(error = crate::Error)] diff --git a/crates/matrix/src/client/register.rs b/crates/matrix/src/client/register.rs new file mode 100644 index 0000000..b518083 --- /dev/null +++ b/crates/matrix/src/client/register.rs @@ -0,0 +1,3 @@ +pub mod available; +pub mod root; +pub mod token; diff --git a/crates/matrix/src/client/session/username_available.rs b/crates/matrix/src/client/register/available.rs similarity index 79% rename from crates/matrix/src/client/session/username_available.rs rename to crates/matrix/src/client/register/available.rs index afa68fd..f5ecc37 100644 --- a/crates/matrix/src/client/session/username_available.rs +++ b/crates/matrix/src/client/register/available.rs @@ -6,7 +6,7 @@ use serde::Serialize; #[allow(dead_code)] const METADATA: Metadata = metadata! { - method: POST, + method: GET, rate_limited: true, authentication: None, history: { @@ -16,14 +16,13 @@ const METADATA: Metadata = metadata! { #[request(error = crate::Error)] pub struct Request { + #[ruma_api(query)] pub username: String, } impl Request { - pub fn new(username: &str) -> Self { - Self { - username: username.to_owned(), - } + pub fn new(username: String) -> Self { + Self { username } } } diff --git a/crates/matrix/src/client/register/root.rs b/crates/matrix/src/client/register/root.rs new file mode 100644 index 0000000..860021d --- /dev/null +++ b/crates/matrix/src/client/register/root.rs @@ -0,0 +1,76 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedDeviceId, OwnedUserId, +}; +use serde::{Deserialize, Serialize}; + +use crate::client::uiaa::Auth; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: true, + authentication: None, + history: { + unstable => "/_matrix/client/v3/register", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + pub username: String, + + pub password: String, + + #[serde( + rename = "initial_device_display_name", + skip_serializing_if = "Option::is_none" + )] + pub device_name: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub refresh_token: Option, + + /// Note that this information is not used to define how the registered user + /// should be authenticated, but is instead used to authenticate the + /// register call itself. It should be left empty, or omitted, unless an + /// earlier call returned an response with status code 401. + #[serde(skip_serializing_if = "Option::is_none")] + pub auth: Option, +} + +impl Request { + pub fn new( + username: String, + password: String, + device_name: Option, + refresh_token: Option, + auth: Option, + ) -> Self { + Self { + username, + password, + device_name, + refresh_token, + auth, + } + } +} + +#[response(error = crate::Error)] +#[derive(Deserialize, Serialize)] +pub struct Response { + #[serde(default)] + pub access_token: Option, + + #[serde(default)] + pub device_id: Option, + + #[serde(default)] + pub expires_in_ms: Option, + + #[serde(default)] + pub refresh_token: Option, + + pub user_id: OwnedUserId, +} diff --git a/crates/matrix/src/client/register/token.rs b/crates/matrix/src/client/register/token.rs new file mode 100644 index 0000000..113a424 --- /dev/null +++ b/crates/matrix/src/client/register/token.rs @@ -0,0 +1 @@ +pub mod validity; diff --git a/crates/matrix/src/client/register/token/validity.rs b/crates/matrix/src/client/register/token/validity.rs new file mode 100644 index 0000000..98c9fa9 --- /dev/null +++ b/crates/matrix/src/client/register/token/validity.rs @@ -0,0 +1,31 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: true, + authentication: None, + history: { + unstable => "/_matrix/client/v1/register/m.login.registration_token/validity", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(query)] + pub token: String, +} + +impl Request { + pub fn new(token: String) -> Self { + Self { token } + } +} + +#[response(error = crate::Error)] +pub struct Response { + pub valid: bool, +} diff --git a/crates/matrix/src/client/room.rs b/crates/matrix/src/client/room.rs deleted file mode 100644 index dff4c1e..0000000 --- a/crates/matrix/src/client/room.rs +++ /dev/null @@ -1,12 +0,0 @@ -//! This module contains handlers to interact with rooms. -//! -//! reference: https://spec.matrix.org/unstable/client-server-api/#rooms - -pub mod ban_from_room; -pub mod create_room; -pub mod forget_room; -pub mod get_rooms; -pub mod join_room; -pub mod kick_from_room; -pub mod leave_room; -pub mod unban_from_room; diff --git a/crates/matrix/src/client/room/ban_from_room.rs b/crates/matrix/src/client/room/ban_from_room.rs deleted file mode 100644 index 5e52971..0000000 --- a/crates/matrix/src/client/room/ban_from_room.rs +++ /dev/null @@ -1,28 +0,0 @@ -use ruma_common::{ - api::{request, response, Metadata}, - metadata, OwnedRoomId, OwnedUserId, -}; - -#[allow(dead_code)] -const METADATA: Metadata = metadata! { - method: POST, - rate_limited: true, - authentication: AccessToken, - history: { - unstable => "/_matrix/client/v3/rooms/{room_id}/ban", - } -}; - -#[request(error = crate::Error)] -pub struct Request { - #[ruma_api(path)] - pub room_id: OwnedRoomId, - - pub user_id: OwnedUserId, - - #[serde(skip_serializing_if = "String::is_empty")] - pub reason: String, -} - -#[response(error = crate::Error)] -pub struct Response {} diff --git a/crates/matrix/src/client/room/create_room.rs b/crates/matrix/src/client/room/create_room.rs deleted file mode 100644 index a74dde4..0000000 --- a/crates/matrix/src/client/room/create_room.rs +++ /dev/null @@ -1,97 +0,0 @@ -use std::collections::BTreeMap; - -use ruma_common::{ - api::{request, response, Metadata}, - metadata, - power_levels::NotificationPowerLevels, - serde::Raw, - OwnedRoomId, OwnedUserId, -}; -use ruma_events::{AnyInitialStateEvent, TimelineEventType}; -use serde::Serialize; - -#[allow(dead_code)] -const METADATA: Metadata = metadata! { - method: POST, - rate_limited: true, - authentication: AccessToken, - history: { - unstable => "/_matrix/client/v3/createRoom", - } -}; - -#[request(error = crate::Error)] -pub struct Request { - #[serde(skip_serializing_if = "Option::is_none")] - pub creation_content: Option, - - #[serde(skip_serializing_if = "<[_]>::is_empty")] - pub initial_state: Vec>, - - #[serde(skip_serializing_if = "<[_]>::is_empty")] - pub invite: Vec, - - pub is_direct: bool, - - #[serde(skip_serializing_if = "String::is_empty")] - pub name: String, - - #[serde( - rename = "power_level_content_override", - skip_serializing_if = "Option::is_none" - )] - pub power_override: Option, - - pub preset: RoomPreset, - - #[serde(rename = "room_alias_name", skip_serializing_if = "String::is_empty")] - pub alias: String, - - #[serde(skip_serializing_if = "String::is_empty")] - pub topic: String, -} - -#[response(error = crate::Error)] -pub struct Response { - pub room_id: OwnedRoomId, -} - -#[derive(Clone, Debug, Serialize)] -pub struct RoomCreationContent { - #[serde(rename = "m.federate")] - pub federate: bool, -} - -#[derive(Clone, Debug, Default, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum RoomPreset { - PublicChat, - - PrivateChat, - - #[default] - TrustedPrivateChat, -} - -#[derive(Clone, Debug, Serialize)] -pub struct RoomPowerLevels { - pub ban: u64, - - pub events: BTreeMap, - - pub events_default: u64, - - pub invite: u64, - - pub kick: u64, - - pub redact: u64, - - pub state_default: u64, - - pub users: BTreeMap, - - pub users_default: u64, - - pub notifications: NotificationPowerLevels, -} diff --git a/crates/matrix/src/client/room/forget_room.rs b/crates/matrix/src/client/room/forget_room.rs deleted file mode 100644 index 1b72ee2..0000000 --- a/crates/matrix/src/client/room/forget_room.rs +++ /dev/null @@ -1,26 +0,0 @@ -use ruma_common::{ - api::{request, response, Metadata}, - metadata, OwnedRoomId, -}; - -#[allow(dead_code)] -const METADATA: Metadata = metadata! { - method: POST, - rate_limited: true, - authentication: AccessToken, - history: { - unstable => "/_matrix/client/v3/rooms/{room_id}/forget", - } -}; - -#[request(error = crate::Error)] -pub struct Request { - #[ruma_api(path)] - pub room_id: OwnedRoomId, - - #[serde(skip_serializing_if = "String::is_empty")] - pub reason: String, -} - -#[response(error = crate::Error)] -pub struct Response {} diff --git a/crates/matrix/src/client/room/get_rooms.rs b/crates/matrix/src/client/room/get_rooms.rs deleted file mode 100644 index b1f05d1..0000000 --- a/crates/matrix/src/client/room/get_rooms.rs +++ /dev/null @@ -1,70 +0,0 @@ -use ruma_common::{ - api::{request, response, Metadata}, - metadata, - room::RoomType, - OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedServerName, -}; -use ruma_events::room::join_rules::JoinRule; -use serde::Deserialize; - -#[allow(dead_code)] -const METADATA: Metadata = metadata! { - method: GET, - rate_limited: false, - authentication: None, - history: { - unstable => "/_matrix/client/v3/publicRooms", - } -}; - -#[request(error = crate::Error)] -pub struct Request { - #[ruma_api(query)] - #[serde(skip_serializing_if = "Option::is_none")] - limit: Option, - - #[ruma_api(query)] - server: OwnedServerName, - - #[ruma_api(query)] - #[serde(skip_serializing_if = "String::is_empty")] - since: String, -} - -#[response(error = crate::Error)] -pub struct Response { - pub chunk: Vec, - - #[serde(skip_serializing_if = "Option::is_none")] - pub next_batch: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub prev_batch: Option, -} - -#[derive(Clone, Debug, Deserialize)] -pub struct Room { - #[serde(skip_serializing_if = "Option::is_none")] - pub avatar_url: Option, - - #[serde(rename = "canonical_alias", skip_serializing_if = "Option::is_none")] - pub alias: Option, - - pub join_rule: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, - - #[serde(rename = "num_joined_members")] - pub members: Option, - - pub room_id: OwnedRoomId, - - #[serde(skip_serializing_if = "Option::is_none")] - pub room_type: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub topic: Option, - - pub world_readable: bool, -} diff --git a/crates/matrix/src/client/room/invite_to_room.rs b/crates/matrix/src/client/room/invite_to_room.rs deleted file mode 100644 index e69de29..0000000 diff --git a/crates/matrix/src/client/room/join_room.rs b/crates/matrix/src/client/room/join_room.rs deleted file mode 100644 index 5cb0502..0000000 --- a/crates/matrix/src/client/room/join_room.rs +++ /dev/null @@ -1,28 +0,0 @@ -use ruma_common::{ - api::{request, response, Metadata}, - metadata, OwnedRoomId, OwnedRoomOrAliasId, -}; - -#[allow(dead_code)] -const METADATA: Metadata = metadata! { - method: POST, - rate_limited: true, - authentication: AccessToken, - history: { - unstable => "/_matrix/client/v3/join/{alias_or_id}", - } -}; - -#[request(error = crate::Error)] -pub struct Request { - #[ruma_api(path)] - pub alias_or_id: OwnedRoomOrAliasId, - - #[serde(skip_serializing_if = "String::is_empty")] - pub reason: String, -} - -#[response(error = crate::Error)] -pub struct Response { - pub room_id: OwnedRoomId, -} diff --git a/crates/matrix/src/client/room/leave_room.rs b/crates/matrix/src/client/room/leave_room.rs deleted file mode 100644 index 6d50593..0000000 --- a/crates/matrix/src/client/room/leave_room.rs +++ /dev/null @@ -1,26 +0,0 @@ -use ruma_common::{ - api::{request, response, Metadata}, - metadata, OwnedRoomId, -}; - -#[allow(dead_code)] -const METADATA: Metadata = metadata! { - method: POST, - rate_limited: true, - authentication: AccessToken, - history: { - unstable => "/_matrix/client/v3/rooms/{room_id}/leave", - } -}; - -#[request(error = crate::Error)] -pub struct Request { - #[ruma_api(path)] - pub room_id: OwnedRoomId, - - #[serde(skip_serializing_if = "String::is_empty")] - pub reason: String, -} - -#[response(error = crate::Error)] -pub struct Response {} diff --git a/crates/matrix/src/client/session.rs b/crates/matrix/src/client/session.rs deleted file mode 100644 index 98a2995..0000000 --- a/crates/matrix/src/client/session.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! This module contains handlers for user sessions. -//! -//! reference: https://spec.matrix.org/unstable/client-server-api/#client-authentication - -pub mod get_flows; -pub mod login; -pub mod logout; -pub mod register; -// pub mod uiaa; -pub mod username_available; -pub mod whoami; diff --git a/crates/matrix/src/client/session/get_flows.rs b/crates/matrix/src/client/session/get_flows.rs deleted file mode 100644 index 14e9a29..0000000 --- a/crates/matrix/src/client/session/get_flows.rs +++ /dev/null @@ -1,46 +0,0 @@ -use ruma_common::{ - api::{request, response, Metadata}, - metadata, -}; -use serde::Deserialize; - -#[allow(dead_code)] -const METADATA: Metadata = metadata! { - method: GET, - rate_limited: true, - authentication: None, - history: { - unstable => "/_matrix/client/v3/login", - } -}; - -#[request(error = crate::Error)] -pub struct Request {} - -#[response(error = crate::Error)] -pub struct Response { - pub body: Vec, -} - -#[derive(Clone, Debug, Deserialize)] -pub struct LoginFlow { - #[serde(skip_serializing_if = "Option::is_none")] - pub get_login_token: Option, - - pub kind: LoginType, -} - -#[derive(Clone, Debug, Deserialize)] -pub enum LoginType { - #[serde(rename = "m.login.password")] - Password, - - #[serde(rename = "m.login.token")] - Token, - - #[serde(rename = "m.login.sso")] - Sso, - - #[serde(rename = "m.login.application_service")] - ApplicationService, -} diff --git a/crates/matrix/src/client/session/register.rs b/crates/matrix/src/client/session/register.rs deleted file mode 100644 index 337031c..0000000 --- a/crates/matrix/src/client/session/register.rs +++ /dev/null @@ -1,68 +0,0 @@ -use ruma_common::{ - api::{request, response, Metadata}, - metadata, OwnedDeviceId, OwnedUserId, -}; -use serde::{Deserialize, Serialize}; - -use crate::client::uiaa::UiaaRequest; - -#[allow(dead_code)] -const METADATA: Metadata = metadata! { - method: POST, - rate_limited: true, - authentication: None, - history: { - unstable => "/_matrix/client/v3/register", - } -}; - -#[request(error = crate::Error)] -pub struct Request { - username: String, - - password: String, - - #[serde( - rename = "initial_device_display_name", - skip_serializing_if = "String::is_empty" - )] - device_name: String, - - #[serde(skip_serializing_if = "Option::is_none")] - refresh_token: Option, - - /// Note that this information is not used to define how the registered user should be - /// authenticated, but is instead used to authenticate the register call itself. - /// It should be left empty, or omitted, unless an earlier call returned an response - /// with status code 401. - #[serde(skip_serializing_if = "Option::is_none")] - auth: Option, -} - -impl Request { - pub fn new(username: &str, password: &str, device_name: &str, auth: Option) -> Self { - Self { - username: username.to_owned(), - password: password.to_owned(), - device_name: device_name.to_owned(), - refresh_token: Some(true), - auth - } - } -} - -#[response(error = crate::Error)] -#[derive(Deserialize, Serialize)] -pub struct Response { - pub access_token: String, - - pub device_id: OwnedDeviceId, - - #[serde(skip_serializing_if = "Option::is_none")] - pub expires_in_ms: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub refresh_token: Option, - - pub user_id: OwnedUserId, -} diff --git a/crates/matrix/src/client/uiaa.rs b/crates/matrix/src/client/uiaa.rs index af0cd80..a49a517 100644 --- a/crates/matrix/src/client/uiaa.rs +++ b/crates/matrix/src/client/uiaa.rs @@ -2,7 +2,7 @@ //! //! [uiaa]: https://spec.matrix.org/latest/client-server-api/#user-interactive-authentication-api -use ruma_common::{serde::JsonObject, OwnedSessionId}; +use ruma_common::{thirdparty::Medium, OwnedSessionId, OwnedUserId, UserId}; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Deserialize, Serialize)] @@ -66,12 +66,12 @@ pub enum AuthType { RegistrationToken, } -#[derive(Clone, Serialize)] +#[derive(Clone, Debug, Serialize)] #[non_exhaustive] #[serde(untagged)] pub enum AuthData { // Password-based authentication (`m.login.password`). - // Password(Password), + Password(Password), // Google ReCaptcha 2.0 authentication (`m.login.recaptcha`). // ReCaptcha(ReCaptcha), @@ -91,12 +91,18 @@ pub enum AuthData { // FallbackAcknowledgement(FallbackAcknowledgement), } -/// Data for dummy UIAA flow. +impl AuthData { + fn kind(&self) -> AuthType { + match self { + AuthData::Password(_) => AuthType::Password, + AuthData::Dummy(_) => AuthType::Dummy, + } + } +} + #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(tag = "type", rename = "m.login.dummy")] -pub struct Dummy { - pub session: Option, -} +pub struct Dummy {} impl Dummy { pub fn new() -> Self { @@ -104,39 +110,55 @@ impl Dummy { } } -#[derive(Clone, Debug, Serialize)] -pub struct UiaaRequest { - pub session: Option, +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(tag = "type", rename = "m.login.password")] +pub struct Password { + identifier: UserIdentifier, + password: String, +} - #[serde(rename = "type")] - pub kind: AuthType, +impl Password { + pub fn new>(user_id: impl Into, password: S) -> Self { + let user: &UserId = &user_id.into(); - // #[serde(skip_serializing_if = "serde_json::Map::is_empty")] - // data: serde_json::Map, + Self { + identifier: UserIdentifier::User { + user: user.localpart().to_owned(), + }, + password: password.into(), + } + } } -impl UiaaRequest { - pub fn serialize_json(self) -> serde_json::Value { - let mut obj = JsonObject::new(); - - match self.kind { - AuthType::Dummy => { - if let Some(session) = self.session { - obj.insert( - "session".to_owned(), - serde_json::Value::String(session.into()), - ); - } +#[derive(Clone, Debug, Serialize)] +pub struct Auth { + session: Option, - obj.insert( - "type".to_owned(), - serde_json::Value::String(serde_json::to_string(&AuthType::Dummy).unwrap()), - ); + kind: AuthType, - obj.into() - } + #[serde(flatten)] + data: AuthData, +} - _ => unimplemented!(), +impl Auth { + pub fn new(data: AuthData, session: Option) -> Self { + Self { + session, + kind: data.kind(), + data, } } } + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(tag = "type")] +pub enum UserIdentifier { + #[serde(rename = "m.id.user")] + User { user: String }, + + #[serde(rename = "m.id.thirdparty")] + ThirdParty { medium: Medium, address: String }, + + #[serde(rename = "m.id.phone")] + Phone { country: String, phone: String }, +} diff --git a/crates/matrix/src/lib.rs b/crates/matrix/src/lib.rs index 035daab..6a7e5be 100644 --- a/crates/matrix/src/lib.rs +++ b/crates/matrix/src/lib.rs @@ -13,50 +13,23 @@ pub mod client; use async_trait::async_trait; use bytes::{Bytes, BytesMut}; -use ruma_client::{HttpClient, HttpClientExt, ResponseResult}; - -use ruma_common::api::{OutgoingRequest, SendAccessToken}; +use ruma_client::HttpClient; +pub use ruma_client; pub use ruma_common; pub use ruma_events; -pub use ruma_client; +pub use ruma_identifiers_validation; pub type Error = ruma_common::api::error::MatrixError; pub type HandleError = ruma_client::Error; -#[derive(Debug)] -pub struct Handle { +#[derive(Default, Debug)] +pub struct Client { inner: reqwest::Client, - homeserver_url: url::Url, -} - -impl Handle { - pub fn new(homeserver_url: &url::Url) -> Self { - Self { - inner: reqwest::Client::new(), - homeserver_url: homeserver_url.to_owned(), - } - } - - pub async fn dispatch( - &self, - access_token: Option<&str>, - request: R, - ) -> ResponseResult { - self.send_matrix_request::( - self.homeserver_url.as_str(), - access_token - .map(SendAccessToken::IfRequired) - .unwrap_or(SendAccessToken::None), - &[], - request, - ) - .await - } } #[async_trait] -impl HttpClient for Handle { +impl HttpClient for Client { type RequestBody = BytesMut; type ResponseBody = Bytes; type Error = reqwest::Error; diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 883eee3..c44e461 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -13,10 +13,11 @@ name = "router" path = "src/lib.rs" [dependencies] -# Workspace Dependencies axum = { workspace = true, features = ["tokio", "macros"] } +axum-extra = { workspace = true, features = ["typed-header"] } anyhow = { workspace = true } http = { workspace = true } +email_address = { workspace = true } # openssl = { workspace = true, features = ["vendored"] } # openssl-sys = { workspace = true, features = ["vendored"] } serde = { workspace = true, features = ["derive"] } @@ -27,5 +28,5 @@ url = { workspace = true, features = ["serde"] } # Local Dependencies core = { path = "../core" } +matrix = { path = "../matrix" } figment = { workspace = true, features = ["toml", "env"] } -async-stream = "0.3.5" diff --git a/crates/router/src/api.rs b/crates/router/src/api.rs index f52f1c4..9756a11 100644 --- a/crates/router/src/api.rs +++ b/crates/router/src/api.rs @@ -1 +1,7 @@ -pub mod session; +//! This module is the root of the client-server API. +//! +//! reference: https://spec.matrix.org/unstable/client-server-api + +pub mod account; +pub mod relative; +// pub mod session; diff --git a/crates/router/src/api/account.rs b/crates/router/src/api/account.rs new file mode 100644 index 0000000..080944c --- /dev/null +++ b/crates/router/src/api/account.rs @@ -0,0 +1,5 @@ +pub mod avatar; +pub mod display_name; +pub mod email; +pub mod password; +pub mod whoami; diff --git a/crates/router/src/api/account/avatar.rs b/crates/router/src/api/account/avatar.rs new file mode 100644 index 0000000..3c55ff8 --- /dev/null +++ b/crates/router/src/api/account/avatar.rs @@ -0,0 +1,33 @@ +use axum::{ + response::{IntoResponse, Response}, + Json, +}; +use axum_extra::{headers::{authorization::Bearer, Authorization}, TypedHeader}; +use matrix::ruma_common::OwnedMxcUri; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Payload { + pub mxc_uri: OwnedMxcUri, +} + +pub async fn handler( + TypedHeader(access_token): TypedHeader>, + Json(payload): Json, +) -> Response { + use commune::profile::avatar::update::service; + + match service( + access_token.token(), + payload.mxc_uri, + ) + .await + { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to update avatar"); + + e.into_response() + } + } +} diff --git a/crates/router/src/api/account/display_name.rs b/crates/router/src/api/account/display_name.rs new file mode 100644 index 0000000..0f9b321 --- /dev/null +++ b/crates/router/src/api/account/display_name.rs @@ -0,0 +1,27 @@ +use axum::{ + response::{IntoResponse, Response}, + Json, +}; +use axum_extra::{headers::{authorization::Bearer, Authorization}, TypedHeader}; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Payload { + pub display_name: String, +} + +pub async fn handler( + TypedHeader(access_token): TypedHeader>, + Json(payload): Json, +) -> Response { + use commune::profile::avatar::update::service; + + match service(access_token.token(), payload.display_name).await { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to update display name"); + + e.into_response() + } + } +} diff --git a/crates/router/src/api/account/email.rs b/crates/router/src/api/account/email.rs new file mode 100644 index 0000000..6fb66d1 --- /dev/null +++ b/crates/router/src/api/account/email.rs @@ -0,0 +1,19 @@ +use axum::{ + extract::Path, + response::{IntoResponse, Response}, + Json, +}; +use email_address::EmailAddress; + +pub async fn handler(Path(email): Path) -> Response { + use commune::account::email::service; + + match service(email).await { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to handle email verification"); + + e.into_response() + } + } +} diff --git a/crates/router/src/api/account/password.rs b/crates/router/src/api/account/password.rs new file mode 100644 index 0000000..b3ea639 --- /dev/null +++ b/crates/router/src/api/account/password.rs @@ -0,0 +1,37 @@ +use axum::{ + response::{IntoResponse, Response}, + Json, +}; +use axum_extra::{headers::{authorization::Bearer, Authorization}, TypedHeader}; +use commune::util::secret::Secret; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Payload { + username: String, + password: Secret, + new_password: Secret, +} + +pub async fn handler( + TypedHeader(access_token): TypedHeader>, + Json(payload): Json, +) -> Response { + use commune::account::password::service; + + match service( + access_token.token(), + payload.username, + payload.password, + payload.new_password, + ) + .await + { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to reset password"); + + e.into_response() + } + } +} diff --git a/crates/router/src/api/account/whoami.rs b/crates/router/src/api/account/whoami.rs new file mode 100644 index 0000000..c5a1f91 --- /dev/null +++ b/crates/router/src/api/account/whoami.rs @@ -0,0 +1,21 @@ +use axum::{ + response::{IntoResponse, Response}, + Json, +}; +use axum_extra::{ + headers::{authorization::Bearer, Authorization}, + TypedHeader, +}; + +pub async fn handler(TypedHeader(access_token): TypedHeader>) -> Response { + use commune::account::whoami::service; + + match service(access_token.token()).await { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to associate access token with user"); + + e.into_response() + } + } +} diff --git a/crates/core/src/session.rs b/crates/router/src/api/relative.rs similarity index 63% rename from crates/core/src/session.rs rename to crates/router/src/api/relative.rs index 943b11d..d6f934b 100644 --- a/crates/core/src/session.rs +++ b/crates/router/src/api/relative.rs @@ -1,4 +1,4 @@ pub mod login; pub mod logout; pub mod register; -pub mod username_available; +pub mod available; diff --git a/crates/router/src/api/relative/available.rs b/crates/router/src/api/relative/available.rs new file mode 100644 index 0000000..e37cbb0 --- /dev/null +++ b/crates/router/src/api/relative/available.rs @@ -0,0 +1,18 @@ +use axum::{ + extract::Path, + response::{IntoResponse, Response}, + Json, +}; + +pub async fn handler(Path(username): Path) -> Response { + use commune::account::username::service; + + match service(username).await { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to check username availability"); + + e.into_response() + } + } +} diff --git a/crates/router/src/api/relative/login.rs b/crates/router/src/api/relative/login.rs new file mode 100644 index 0000000..f3409a0 --- /dev/null +++ b/crates/router/src/api/relative/login.rs @@ -0,0 +1,30 @@ +use axum::{ + response::{IntoResponse, Response}, + Json, +}; +use commune::util::secret::Secret; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Payload { + pub username: String, + pub password: Secret, +} + +pub async fn handler(Json(payload): Json) -> Response { + use commune::account::login::service; + + match service( + &payload.username, + &payload.password, + ) + .await + { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to login user"); + + e.into_response() + } + } +} diff --git a/crates/router/src/api/relative/logout.rs b/crates/router/src/api/relative/logout.rs new file mode 100644 index 0000000..469db08 --- /dev/null +++ b/crates/router/src/api/relative/logout.rs @@ -0,0 +1,19 @@ +use axum::{ + response::{IntoResponse, Response}, + Json, +}; +use axum_extra::{headers::{Authorization, authorization::Bearer}, TypedHeader}; + +#[axum::debug_handler] +pub async fn handler(TypedHeader(access_token): TypedHeader>) -> Response { + use commune::account::logout::service; + + match service(access_token.token()).await { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to logout user"); + + e.into_response() + } + } +} diff --git a/crates/router/src/api/relative/register.rs b/crates/router/src/api/relative/register.rs new file mode 100644 index 0000000..673e977 --- /dev/null +++ b/crates/router/src/api/relative/register.rs @@ -0,0 +1,30 @@ +use axum::{ + response::{IntoResponse, Response}, + Json, +}; +use commune::util::secret::Secret; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Payload { + pub username: String, + pub password: Secret, +} + +pub async fn handler(Json(payload): Json) -> Response { + use commune::account::register::service; + + match service( + payload.username, + payload.password, + ) + .await + { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to create account"); + + e.into_response() + } + } +} diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 9f6454a..4f00ca4 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -1,7 +1,7 @@ use std::net::SocketAddr; use axum::{ - routing::{get, post}, + routing::{get, post, put}, Router, }; use tokio::net::TcpListener; @@ -9,23 +9,21 @@ use tokio::net::TcpListener; pub mod api; pub async fn routes() -> Router { - Router::new() - .route("/", get(|| async { "hello from commune!" })) + let router = Router::new() + .route("/register", post(api::relative::register::handler)) + .route("/register/available/:username", get(api::relative::available::handler)) + .route("/login", post(api::relative::login::handler)) + .route("/logout", post(api::relative::logout::handler)) .nest( - "/_commune/client/r0", + "/account", Router::new() - .nest( - "/register", - Router::new() - .route("/", post(api::session::register::handler)) - .route( - "/username/:username", - get(api::session::username_available::handler), - ), - ) - .route("/login", post(api::session::login::handler)) - .route("/logout", post(api::session::logout::handler)), - ) + .route("/whoami", get(api::account::whoami::handler)) + .route("/password", put(api::account::password::handler)) + .route("/display_name", put(api::account::display_name::handler)) + .route("/avatar", put(api::account::avatar::handler)) + ); + + Router::new().nest("/_commune/client/r0", router) } pub async fn serve(public_loopback: bool, port: u16) -> anyhow::Result<()> { diff --git a/crates/router/src/main.rs b/crates/router/src/main.rs index 11ecc44..7991edc 100644 --- a/crates/router/src/main.rs +++ b/crates/router/src/main.rs @@ -2,11 +2,11 @@ use anyhow::Result; #[tokio::main] async fn main() -> Result<()> { - commune::init(); - let config = &commune::commune().config; - tracing_subscriber::fmt::init(); + commune::init().await; + let config = &commune::commune().config; + router::serve(config.public_loopback, config.port.unwrap()).await?; Ok(()) diff --git a/docker-compose.yml b/docker-compose.yml index 6d8a4dd..18a4f56 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,11 @@ version: '3' services: - # maildev: - # image: 'maildev/maildev' - # ports: - # - '1080:1080' - # - '1025:1025' + mailcrab: + image: marlonb/mailcrab:latest + ports: + - '1025:1025' + networks: [default] redis: image: 'redis/redis-stack' From c2cb34c26b8c23902b9a6a4320adc38dbe8bea11 Mon Sep 17 00:00:00 2001 From: mikoto Date: Tue, 26 Mar 2024 22:29:52 +0000 Subject: [PATCH 22/33] chore: account management tests --- commune-example.toml | 2 +- crates/core/src/util/secret.rs | 12 +- crates/matrix/src/client/login.rs | 2 +- crates/matrix/src/client/logout/root.rs | 4 +- crates/router/src/api/account/avatar.rs | 12 +- crates/router/src/api/account/password.rs | 5 +- crates/router/src/api/relative.rs | 2 +- crates/router/src/api/relative/login.rs | 11 +- crates/router/src/api/relative/logout.rs | 6 +- crates/router/src/api/relative/register.rs | 4 +- crates/router/src/api/session/login.rs | 41 -- crates/router/src/api/session/logout.rs | 17 - crates/router/src/api/session/register.rs | 40 -- .../src/api/session/username_available.rs | 18 - crates/test/src/api.rs | 7 + crates/test/src/api/account.rs | 5 + crates/test/src/api/account/avatar.rs | 33 ++ crates/test/src/api/account/display_name.rs | 27 + crates/test/src/api/account/email.rs | 19 + crates/test/src/api/account/password.rs | 37 ++ crates/test/src/api/account/whoami.rs | 21 + .../session.rs => test/src/api/relative.rs} | 2 +- crates/test/src/api/relative/available.rs | 0 crates/test/src/api/relative/login.rs | 35 ++ crates/test/src/api/relative/logout.rs | 30 ++ crates/test/src/api/relative/register.rs | 42 ++ crates/test/src/commune/mod.rs | 1 - crates/test/src/commune/room/create.rs | 58 --- crates/test/src/commune/room/mod.rs | 1 - crates/test/src/env.rs | 4 +- crates/test/src/lib.rs | 5 +- crates/test/src/login.rs | 34 -- crates/test/src/matrix/events.rs | 476 ------------------ crates/test/src/matrix/mod.rs | 5 - crates/test/src/matrix/room_admin.rs | 251 --------- crates/test/src/matrix/room_client.rs | 292 ----------- .../src/matrix/shared_token_registration.rs | 42 -- crates/test/src/matrix/util.rs | 199 -------- crates/test/src/register.rs | 31 -- crates/test/src/server/api/mod.rs | 1 - .../test/src/server/api/v1/account/login.rs | 94 ---- crates/test/src/server/api/v1/account/mod.rs | 3 - crates/test/src/server/api/v1/account/root.rs | 89 ---- .../test/src/server/api/v1/account/session.rs | 149 ------ crates/test/src/server/api/v1/mod.rs | 1 - crates/test/src/server/mod.rs | 1 - crates/test/src/tools/environment.rs | 29 -- crates/test/src/tools/http.rs | 95 ---- crates/test/src/tools/maildev.rs | 104 ---- crates/test/src/tools/mod.rs | 3 - crates/test/src/util.rs | 1 - 51 files changed, 294 insertions(+), 2109 deletions(-) delete mode 100644 crates/router/src/api/session/login.rs delete mode 100644 crates/router/src/api/session/logout.rs delete mode 100644 crates/router/src/api/session/register.rs delete mode 100644 crates/router/src/api/session/username_available.rs create mode 100644 crates/test/src/api.rs create mode 100644 crates/test/src/api/account.rs create mode 100644 crates/test/src/api/account/avatar.rs create mode 100644 crates/test/src/api/account/display_name.rs create mode 100644 crates/test/src/api/account/email.rs create mode 100644 crates/test/src/api/account/password.rs create mode 100644 crates/test/src/api/account/whoami.rs rename crates/{router/src/api/session.rs => test/src/api/relative.rs} (63%) create mode 100644 crates/test/src/api/relative/available.rs create mode 100644 crates/test/src/api/relative/login.rs create mode 100644 crates/test/src/api/relative/logout.rs create mode 100644 crates/test/src/api/relative/register.rs delete mode 100644 crates/test/src/commune/mod.rs delete mode 100644 crates/test/src/commune/room/create.rs delete mode 100644 crates/test/src/commune/room/mod.rs delete mode 100644 crates/test/src/login.rs delete mode 100644 crates/test/src/matrix/events.rs delete mode 100644 crates/test/src/matrix/mod.rs delete mode 100644 crates/test/src/matrix/room_admin.rs delete mode 100644 crates/test/src/matrix/room_client.rs delete mode 100644 crates/test/src/matrix/shared_token_registration.rs delete mode 100644 crates/test/src/matrix/util.rs delete mode 100644 crates/test/src/register.rs delete mode 100644 crates/test/src/server/api/mod.rs delete mode 100644 crates/test/src/server/api/v1/account/login.rs delete mode 100644 crates/test/src/server/api/v1/account/mod.rs delete mode 100644 crates/test/src/server/api/v1/account/root.rs delete mode 100644 crates/test/src/server/api/v1/account/session.rs delete mode 100644 crates/test/src/server/api/v1/mod.rs delete mode 100644 crates/test/src/server/mod.rs delete mode 100644 crates/test/src/tools/environment.rs delete mode 100644 crates/test/src/tools/http.rs delete mode 100644 crates/test/src/tools/maildev.rs delete mode 100644 crates/test/src/tools/mod.rs delete mode 100644 crates/test/src/util.rs diff --git a/commune-example.toml b/commune-example.toml index df1e5bc..38f633e 100644 --- a/commune-example.toml +++ b/commune-example.toml @@ -5,7 +5,7 @@ tls = true # Either one works but not both blocked_domains = [] -allowed_domains = ['gmail.com', 'outlook.com'] +# allowed_domains = ['gmail.com', 'outlook.com'] # `X-Forwarded-For` header # xff = false diff --git a/crates/core/src/util/secret.rs b/crates/core/src/util/secret.rs index d478867..b1eb440 100644 --- a/crates/core/src/util/secret.rs +++ b/crates/core/src/util/secret.rs @@ -1,11 +1,21 @@ use std::fmt::{Debug, Display}; use rand::{distributions::Uniform, Rng}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; #[derive(Deserialize)] pub struct Secret(String); +// is this necessary? +impl Serialize for Secret { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.inner().serialize(serializer) + } +} + impl Secret { #[inline] pub fn new(s: impl Into) -> Self { diff --git a/crates/matrix/src/client/login.rs b/crates/matrix/src/client/login.rs index 5b85734..153f0d6 100644 --- a/crates/matrix/src/client/login.rs +++ b/crates/matrix/src/client/login.rs @@ -51,7 +51,7 @@ impl Request { } #[response(error = crate::Error)] -#[derive(Serialize)] +#[derive(Deserialize, Serialize)] pub struct Response { pub access_token: String, diff --git a/crates/matrix/src/client/logout/root.rs b/crates/matrix/src/client/logout/root.rs index 83f5796..0d4c7e4 100644 --- a/crates/matrix/src/client/logout/root.rs +++ b/crates/matrix/src/client/logout/root.rs @@ -2,7 +2,7 @@ use ruma_common::{ api::{request, response, Metadata}, metadata, }; -use serde::Serialize; +use serde::{Serialize, Deserialize}; #[allow(dead_code)] const METADATA: Metadata = metadata! { @@ -25,5 +25,5 @@ impl Request { } #[response(error = crate::Error)] -#[derive(Serialize)] +#[derive(Deserialize, Serialize)] pub struct Response {} diff --git a/crates/router/src/api/account/avatar.rs b/crates/router/src/api/account/avatar.rs index 3c55ff8..2dbb812 100644 --- a/crates/router/src/api/account/avatar.rs +++ b/crates/router/src/api/account/avatar.rs @@ -2,7 +2,10 @@ use axum::{ response::{IntoResponse, Response}, Json, }; -use axum_extra::{headers::{authorization::Bearer, Authorization}, TypedHeader}; +use axum_extra::{ + headers::{authorization::Bearer, Authorization}, + TypedHeader, +}; use matrix::ruma_common::OwnedMxcUri; use serde::Deserialize; @@ -17,12 +20,7 @@ pub async fn handler( ) -> Response { use commune::profile::avatar::update::service; - match service( - access_token.token(), - payload.mxc_uri, - ) - .await - { + match service(access_token.token(), payload.mxc_uri).await { Ok(resp) => Json(resp).into_response(), Err(e) => { tracing::warn!(?e, "failed to update avatar"); diff --git a/crates/router/src/api/account/password.rs b/crates/router/src/api/account/password.rs index b3ea639..37d28e4 100644 --- a/crates/router/src/api/account/password.rs +++ b/crates/router/src/api/account/password.rs @@ -2,7 +2,10 @@ use axum::{ response::{IntoResponse, Response}, Json, }; -use axum_extra::{headers::{authorization::Bearer, Authorization}, TypedHeader}; +use axum_extra::{ + headers::{authorization::Bearer, Authorization}, + TypedHeader, +}; use commune::util::secret::Secret; use serde::Deserialize; diff --git a/crates/router/src/api/relative.rs b/crates/router/src/api/relative.rs index d6f934b..a5a6a98 100644 --- a/crates/router/src/api/relative.rs +++ b/crates/router/src/api/relative.rs @@ -1,4 +1,4 @@ +pub mod available; pub mod login; pub mod logout; pub mod register; -pub mod available; diff --git a/crates/router/src/api/relative/login.rs b/crates/router/src/api/relative/login.rs index f3409a0..d8501f8 100644 --- a/crates/router/src/api/relative/login.rs +++ b/crates/router/src/api/relative/login.rs @@ -3,9 +3,9 @@ use axum::{ Json, }; use commune::util::secret::Secret; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct Payload { pub username: String, pub password: Secret, @@ -14,12 +14,7 @@ pub struct Payload { pub async fn handler(Json(payload): Json) -> Response { use commune::account::login::service; - match service( - &payload.username, - &payload.password, - ) - .await - { + match service(&payload.username, &payload.password).await { Ok(resp) => Json(resp).into_response(), Err(e) => { tracing::warn!(?e, "failed to login user"); diff --git a/crates/router/src/api/relative/logout.rs b/crates/router/src/api/relative/logout.rs index 469db08..2a492da 100644 --- a/crates/router/src/api/relative/logout.rs +++ b/crates/router/src/api/relative/logout.rs @@ -2,9 +2,11 @@ use axum::{ response::{IntoResponse, Response}, Json, }; -use axum_extra::{headers::{Authorization, authorization::Bearer}, TypedHeader}; +use axum_extra::{ + headers::{authorization::Bearer, Authorization}, + TypedHeader, +}; -#[axum::debug_handler] pub async fn handler(TypedHeader(access_token): TypedHeader>) -> Response { use commune::account::logout::service; diff --git a/crates/router/src/api/relative/register.rs b/crates/router/src/api/relative/register.rs index 673e977..be389a2 100644 --- a/crates/router/src/api/relative/register.rs +++ b/crates/router/src/api/relative/register.rs @@ -3,9 +3,9 @@ use axum::{ Json, }; use commune::util::secret::Secret; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; -#[derive(Debug, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct Payload { pub username: String, pub password: Secret, diff --git a/crates/router/src/api/session/login.rs b/crates/router/src/api/session/login.rs deleted file mode 100644 index 47adfd2..0000000 --- a/crates/router/src/api/session/login.rs +++ /dev/null @@ -1,41 +0,0 @@ -use axum::{ - response::{IntoResponse, Response}, - Json, -}; -use commune::util::secret::Secret; -use serde::{Serialize, Deserialize}; - -#[derive(Debug, Deserialize, Serialize)] -pub struct Payload { - username: String, - password: Secret, -} - -impl Payload { - pub fn new>(username: S, password: S) -> Self { - Self { - username: username.into(), - password: Secret::new(password.into()), - } - } -} - -#[axum::debug_handler] -pub async fn handler(Json(payload): Json) -> Response { - use commune::session::login::service; - - match service( - &commune::commune().handle, - &payload.username, - &payload.password, - ) - .await - { - Ok(resp) => Json(resp).into_response(), - Err(e) => { - tracing::warn!(?e, "failed to login user"); - - e.into_response() - } - } -} diff --git a/crates/router/src/api/session/logout.rs b/crates/router/src/api/session/logout.rs deleted file mode 100644 index bcd383d..0000000 --- a/crates/router/src/api/session/logout.rs +++ /dev/null @@ -1,17 +0,0 @@ -use axum::{ - response::{IntoResponse, Response}, - Json, -}; - -pub async fn handler() -> Response { - use commune::session::logout::service; - - match service(&commune::commune().handle).await { - Ok(resp) => Json(resp).into_response(), - Err(e) => { - tracing::warn!(?e, "failed to logout user"); - - e.into_response() - } - } -} diff --git a/crates/router/src/api/session/register.rs b/crates/router/src/api/session/register.rs deleted file mode 100644 index 797def2..0000000 --- a/crates/router/src/api/session/register.rs +++ /dev/null @@ -1,40 +0,0 @@ -use axum::{ - response::{IntoResponse, Response}, - Json, -}; -use commune::util::secret::Secret; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Deserialize, Serialize)] -pub struct Payload { - username: String, - password: Secret, -} - -impl Payload { - pub fn new>(username: S, password: S) -> Self { - Self { - username: username.into(), - password: Secret::new(password.into()), - } - } -} - -pub async fn handler(Json(payload): Json) -> Response { - use commune::session::register::service; - - match service( - &commune::commune().handle, - &payload.username, - &payload.password, - ) - .await - { - Ok(resp) => Json(resp).into_response(), - Err(e) => { - tracing::warn!(?e, "failed to register user"); - - e.into_response() - } - } -} diff --git a/crates/router/src/api/session/username_available.rs b/crates/router/src/api/session/username_available.rs deleted file mode 100644 index 44b26e8..0000000 --- a/crates/router/src/api/session/username_available.rs +++ /dev/null @@ -1,18 +0,0 @@ -use axum::{ - extract::Path, - response::{IntoResponse, Response}, - Json, -}; - -pub async fn handler(Path(username): Path) -> Response { - use commune::session::username_available::service; - - match service(&commune::commune().handle, &username).await { - Ok(resp) => Json(resp).into_response(), - Err(e) => { - tracing::warn!(?e, "failed to check username availability"); - - e.into_response() - } - } -} diff --git a/crates/test/src/api.rs b/crates/test/src/api.rs new file mode 100644 index 0000000..4230649 --- /dev/null +++ b/crates/test/src/api.rs @@ -0,0 +1,7 @@ +//! This module is the root of the client-server API. +//! +//! reference: https://spec.matrix.org/unstable/client-server-api + +// pub mod account; +pub mod relative; +// pub mod session; diff --git a/crates/test/src/api/account.rs b/crates/test/src/api/account.rs new file mode 100644 index 0000000..080944c --- /dev/null +++ b/crates/test/src/api/account.rs @@ -0,0 +1,5 @@ +pub mod avatar; +pub mod display_name; +pub mod email; +pub mod password; +pub mod whoami; diff --git a/crates/test/src/api/account/avatar.rs b/crates/test/src/api/account/avatar.rs new file mode 100644 index 0000000..3c55ff8 --- /dev/null +++ b/crates/test/src/api/account/avatar.rs @@ -0,0 +1,33 @@ +use axum::{ + response::{IntoResponse, Response}, + Json, +}; +use axum_extra::{headers::{authorization::Bearer, Authorization}, TypedHeader}; +use matrix::ruma_common::OwnedMxcUri; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Payload { + pub mxc_uri: OwnedMxcUri, +} + +pub async fn handler( + TypedHeader(access_token): TypedHeader>, + Json(payload): Json, +) -> Response { + use commune::profile::avatar::update::service; + + match service( + access_token.token(), + payload.mxc_uri, + ) + .await + { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to update avatar"); + + e.into_response() + } + } +} diff --git a/crates/test/src/api/account/display_name.rs b/crates/test/src/api/account/display_name.rs new file mode 100644 index 0000000..0f9b321 --- /dev/null +++ b/crates/test/src/api/account/display_name.rs @@ -0,0 +1,27 @@ +use axum::{ + response::{IntoResponse, Response}, + Json, +}; +use axum_extra::{headers::{authorization::Bearer, Authorization}, TypedHeader}; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Payload { + pub display_name: String, +} + +pub async fn handler( + TypedHeader(access_token): TypedHeader>, + Json(payload): Json, +) -> Response { + use commune::profile::avatar::update::service; + + match service(access_token.token(), payload.display_name).await { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to update display name"); + + e.into_response() + } + } +} diff --git a/crates/test/src/api/account/email.rs b/crates/test/src/api/account/email.rs new file mode 100644 index 0000000..6fb66d1 --- /dev/null +++ b/crates/test/src/api/account/email.rs @@ -0,0 +1,19 @@ +use axum::{ + extract::Path, + response::{IntoResponse, Response}, + Json, +}; +use email_address::EmailAddress; + +pub async fn handler(Path(email): Path) -> Response { + use commune::account::email::service; + + match service(email).await { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to handle email verification"); + + e.into_response() + } + } +} diff --git a/crates/test/src/api/account/password.rs b/crates/test/src/api/account/password.rs new file mode 100644 index 0000000..b3ea639 --- /dev/null +++ b/crates/test/src/api/account/password.rs @@ -0,0 +1,37 @@ +use axum::{ + response::{IntoResponse, Response}, + Json, +}; +use axum_extra::{headers::{authorization::Bearer, Authorization}, TypedHeader}; +use commune::util::secret::Secret; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Payload { + username: String, + password: Secret, + new_password: Secret, +} + +pub async fn handler( + TypedHeader(access_token): TypedHeader>, + Json(payload): Json, +) -> Response { + use commune::account::password::service; + + match service( + access_token.token(), + payload.username, + payload.password, + payload.new_password, + ) + .await + { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to reset password"); + + e.into_response() + } + } +} diff --git a/crates/test/src/api/account/whoami.rs b/crates/test/src/api/account/whoami.rs new file mode 100644 index 0000000..c5a1f91 --- /dev/null +++ b/crates/test/src/api/account/whoami.rs @@ -0,0 +1,21 @@ +use axum::{ + response::{IntoResponse, Response}, + Json, +}; +use axum_extra::{ + headers::{authorization::Bearer, Authorization}, + TypedHeader, +}; + +pub async fn handler(TypedHeader(access_token): TypedHeader>) -> Response { + use commune::account::whoami::service; + + match service(access_token.token()).await { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to associate access token with user"); + + e.into_response() + } + } +} diff --git a/crates/router/src/api/session.rs b/crates/test/src/api/relative.rs similarity index 63% rename from crates/router/src/api/session.rs rename to crates/test/src/api/relative.rs index 943b11d..d6f934b 100644 --- a/crates/router/src/api/session.rs +++ b/crates/test/src/api/relative.rs @@ -1,4 +1,4 @@ pub mod login; pub mod logout; pub mod register; -pub mod username_available; +pub mod available; diff --git a/crates/test/src/api/relative/available.rs b/crates/test/src/api/relative/available.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/test/src/api/relative/login.rs b/crates/test/src/api/relative/login.rs new file mode 100644 index 0000000..9058abc --- /dev/null +++ b/crates/test/src/api/relative/login.rs @@ -0,0 +1,35 @@ +use commune::util::secret::Secret; +use matrix::client::login::*; +use router::api::relative::login; + +use crate::{api::relative::register, env::Env}; + +pub async fn login(client: &Env) -> Result { + let register_resp = register::register(&client).await.unwrap(); + + tracing::info!(?register_resp); + + let resp = client + .post("/_commune/client/r0/login") + .json(&login::Payload { + username: register_resp.user_id.into(), + password: Secret::new("verysecure"), + }) + .send() + .await + .unwrap(); + + resp.json::() + .await +} + +#[tokio::test] +async fn login_test() { + let client = Env::new().await; + + let resp = login(&client).await.unwrap(); + + tracing::info!(?resp); + + assert!(!resp.access_token.is_empty()); +} diff --git a/crates/test/src/api/relative/logout.rs b/crates/test/src/api/relative/logout.rs new file mode 100644 index 0000000..eaf583a --- /dev/null +++ b/crates/test/src/api/relative/logout.rs @@ -0,0 +1,30 @@ +use matrix::client::logout::root::*; + +use crate::{api::relative::login, env::Env}; + +pub async fn logout(client: &Env) -> Result { + let login_resp = login::login(&client).await.unwrap(); + + tracing::info!(?login_resp); + + let resp = client + .post("/_commune/client/r0/logout") + .header( + reqwest::header::AUTHORIZATION, + format!("Bearer {}", &login_resp.access_token), + ) + .send() + .await + .unwrap(); + + resp.json::().await +} + +#[tokio::test] +async fn logout_test() { + let client = Env::new().await; + + let resp = logout(&client).await.unwrap(); + + tracing::info!(?resp); +} diff --git a/crates/test/src/api/relative/register.rs b/crates/test/src/api/relative/register.rs new file mode 100644 index 0000000..b387687 --- /dev/null +++ b/crates/test/src/api/relative/register.rs @@ -0,0 +1,42 @@ +use commune::util::secret::Secret; +use rand::seq::IteratorRandom; + +use matrix::client::register::root::*; +use router::api::relative::register; + +use crate::env::Env; + +pub async fn register(client: &Env) -> Result { + let allowed = ('0'..='9') + .chain('a'..='z') + .chain(['-', '.', '=', '_', '/', '+']); + let username = allowed + .choose_multiple(&mut rand::thread_rng(), 8) + .into_iter() + .collect(); + + tracing::info!(?username); + + let resp = client + .post("/_commune/client/r0/register") + .json(®ister::Payload { + username, + password: Secret::new("verysecure"), + }) + .send() + .await + .unwrap(); + + resp.json::().await +} + +#[tokio::test] +async fn register_test() { + let client = Env::new().await; + + let resp = register(&client).await.unwrap(); + + tracing::info!(?resp); + + // assert!(!resp.access_token.is_some() && resp.access_token.map(|at| !at.is_empty()).unwrap()); +} diff --git a/crates/test/src/commune/mod.rs b/crates/test/src/commune/mod.rs deleted file mode 100644 index 835cb3f..0000000 --- a/crates/test/src/commune/mod.rs +++ /dev/null @@ -1 +0,0 @@ -mod room; diff --git a/crates/test/src/commune/room/create.rs b/crates/test/src/commune/room/create.rs deleted file mode 100644 index 510ebec..0000000 --- a/crates/test/src/commune/room/create.rs +++ /dev/null @@ -1,58 +0,0 @@ -use fake::{ - faker::internet::en::{FreeEmail, Password, Username}, - Fake, -}; - -use commune::{ - account::service::CreateUnverifiedAccountDto, - auth::service::{LoginCredentials, LoginCredentialsResponse}, - room::service::CreateRoomDto, - util::secret::Secret, -}; - -use crate::tools::environment::Environment; - -#[tokio::test] -async fn creates_public_chat_room() { - let env = Environment::new().await; - let username: String = Username().fake(); - let password: String = Password(10..20).fake(); - let email: String = FreeEmail().fake(); - let password = Secret::new(password); - - env.commune - .account - .register_unverified(CreateUnverifiedAccountDto { - username: username.clone(), - password: password.clone(), - email, - }) - .await - .expect("Failed to register account"); - - let LoginCredentialsResponse { access_token } = env - .commune - .auth - .login(LoginCredentials { username, password }) - .await - .expect("Failed to login"); - - let room_name = String::from("MyVeryFirstPublicRoom"); - let room_topic = String::from("MyVeryFirstPublicRoomTopic"); - let room_alias = String::from("MyVeryFirstPublicRoomAlias"); - let room = env - .commune - .room - .create_public_room( - &access_token, - CreateRoomDto { - name: room_name, - topic: room_topic, - alias: room_alias, - }, - ) - .await - .expect("Failed to create public room"); - - assert!(!room.room_id.is_empty(), "should return room_id"); -} diff --git a/crates/test/src/commune/room/mod.rs b/crates/test/src/commune/room/mod.rs deleted file mode 100644 index 0f562a4..0000000 --- a/crates/test/src/commune/room/mod.rs +++ /dev/null @@ -1 +0,0 @@ -mod create; diff --git a/crates/test/src/env.rs b/crates/test/src/env.rs index d054f2b..e907d4f 100644 --- a/crates/test/src/env.rs +++ b/crates/test/src/env.rs @@ -7,9 +7,9 @@ pub(crate) struct Env { impl Env { pub(crate) async fn new() -> Self { - tracing_subscriber::fmt().init(); + let _ = tracing_subscriber::fmt().try_init(); - commune::init(); + commune::init().await; let loopback = SocketAddr::from(( match commune::commune().config.public_loopback { diff --git a/crates/test/src/lib.rs b/crates/test/src/lib.rs index 9821918..c84f5e1 100644 --- a/crates/test/src/lib.rs +++ b/crates/test/src/lib.rs @@ -11,10 +11,7 @@ // mod server; #[cfg(test)] -mod register; - -#[cfg(test)] -mod login; +mod api; #[cfg(test)] mod env; diff --git a/crates/test/src/login.rs b/crates/test/src/login.rs deleted file mode 100644 index 73c4f37..0000000 --- a/crates/test/src/login.rs +++ /dev/null @@ -1,34 +0,0 @@ -use rand::Rng; - -use crate::env::Env; - -pub async fn login() -> Result { - let client = Env::new().await; - - let register_resp = crate::register::register(&client).await.unwrap(); - - dbg!(®ister_resp); - - let resp = client - .post("/_commune/client/r0/login") - .json(&router::api::session::login::Payload::new( - register_resp.user_id.as_str(), - "verysecure".into(), - )) - .send() - .await - .unwrap(); - - dbg!(&resp); - - resp.json::() - .await -} - -#[tokio::test] -async fn login_test() { - let resp = login().await.unwrap(); - - dbg!(&resp); - // assert!(!resp.access_token.is_empty()); -} diff --git a/crates/test/src/matrix/events.rs b/crates/test/src/matrix/events.rs deleted file mode 100644 index 36a9479..0000000 --- a/crates/test/src/matrix/events.rs +++ /dev/null @@ -1,476 +0,0 @@ -#[cfg(test)] -mod tests { - use std::iter; - - use futures::{future, TryFutureExt}; - use matrix::{ - client::resources::events::{EventsService, GetMessagesQuery, SendRedactionBody}, - filter::RoomEventFilter, - ruma_common::{RoomVersionId, TransactionId}, - ruma_events::{ - reaction::{OriginalReactionEvent, ReactionEventContent}, - relation::{Annotation, InReplyTo}, - room::{ - message::{ - AddMentions, ForwardThread, OriginalRoomMessageEvent, Relation, - RoomMessageEvent, RoomMessageEventContent, - }, - redaction::OriginalRoomRedactionEvent, - topic::{OriginalRoomTopicEvent, RoomTopicEventContent}, - }, - MessageLikeEvent, MessageLikeEventType, - }, - }; - use tokio::sync::OnceCell; - - use crate::matrix::util::{self, join_helper, Test}; - - static TEST: OnceCell = OnceCell::const_new(); - - #[tokio::test] - async fn send_message() { - let Test { admin, samples, .. } = TEST.get_or_init(util::init).await; - let sample = samples.get(0).unwrap(); - let (owner_id, owner_token) = sample.owner(); - - let mut client = admin.clone(); - client.clear_token(); - - // first join - let joins = join_helper(&client, sample.guests(), &sample.room_id).await; - - assert!(joins.iter().all(Result::is_ok)); - - future::try_join_all( - sample - .guests() - .map(|(user_id, access_token)| { - EventsService::send_message( - &client, - access_token, - &sample.room_id, - TransactionId::new(), - RoomMessageEventContent::text_markdown(format!( - "hello, **my name is {}**", - user_id - )), - ) - }) - .chain(iter::once(EventsService::send_message( - &client, - owner_token, - &sample.room_id, - TransactionId::new(), - RoomMessageEventContent::text_plain(format!( - "and I am the admin of the room, {}", - owner_id - )), - ))), - ) - .await - .unwrap(); - - let expected: Vec<_> = sample - .guests() - .map(|(user_id, _)| format!("hello, **my name is {}**", user_id)) - .chain(iter::once(format!( - "and I am the admin of the room, {}", - owner_id - ))) - .collect(); - - let found = EventsService::get_messages( - &client, - owner_token, - &sample.room_id, - GetMessagesQuery { - limit: Some(111), - filter: serde_json::to_string(&RoomEventFilter { - types: vec![MessageLikeEventType::RoomMessage.into()], - ..Default::default() - }) - .unwrap(), - ..Default::default() - }, - ) - .await - .unwrap(); - - let found: Vec<_> = found - .chunk - .into_iter() - .map(|e| e.deserialize_as::().unwrap()) - .map(|e| e.content.body().to_owned()) - .collect(); - - assert!(expected.iter().all(|s| found.contains(s))); - } - - #[tokio::test] - async fn reply_to_message() { - let Test { admin, samples, .. } = TEST.get_or_init(util::init).await; - let sample = samples.get(2).unwrap(); - let (owner_id, owner_token) = sample.owner(); - - let mut client = admin.clone(); - client.clear_token(); - - // first join - let joins = join_helper(&client, sample.guests(), &sample.room_id).await; - assert!(joins.iter().all(Result::is_ok)); - - let root = EventsService::send_message( - &client, - owner_token, - &sample.room_id, - TransactionId::new(), - RoomMessageEventContent::text_plain(format!( - "I am at the root of the tree, {}", - owner_id - )), - ) - .map_ok(|resp| resp.event_id) - .await - .unwrap(); - - let recursion = 5; - let children = 2; - - let mut history = Vec::from([vec![root]]); - - for level in 1..recursion { - let guests: Vec<_> = sample.guests().collect(); - let (_, access_token) = guests.get((recursion - 1) % guests.len()).unwrap(); - - let prev = history.last().unwrap(); - let traverse = future::try_join_all((0..prev.len() * children).map(|i| { - EventsService::get_event( - &client, - *access_token, - &sample.room_id, - prev.get(i / children).unwrap(), - ) - .map_ok(|resp| resp.deserialize_as::().unwrap()) - .and_then(|event| { - EventsService::send_message( - &client, - *access_token, - &sample.room_id, - TransactionId::new(), - RoomMessageEventContent::text_markdown(format!("level {level}")) - .make_reply_to(&event, ForwardThread::No, AddMentions::Yes), - ) - }) - .map_ok(|resp| resp.event_id) - })) - .await - .unwrap(); - - history.push(traverse.clone()); - - tracing::info!(?traverse); - } - - let filter = serde_json::to_string(&RoomEventFilter { - types: vec![MessageLikeEventType::RoomMessage.into()], - ..Default::default() - }) - .unwrap(); - - let found: Vec<_> = EventsService::get_messages( - &client, - owner_token, - &sample.room_id, - GetMessagesQuery { - limit: Some(111), - filter: filter.clone(), - ..Default::default() - }, - ) - .map_ok(|resp| { - resp.chunk - .into_iter() - .map(|e| e.deserialize_as::().unwrap()) - .map(|e| { - ( - e.event_id, - e.content.body().to_owned(), - e.content.relates_to, - ) - }) - .collect() - }) - .await - .unwrap(); - - // this is just `map (n -> n - 1) [1, 2 , 4, 8, ...]` - let v: Vec<_> = (0..recursion) - .map(|i| children.pow(i as u32) as usize - 1) - .collect(); - - let tree: Vec<_> = v - .windows(2) - .map(|arr| (arr[0], arr[1])) - .map(|(i, j)| found[i..j].to_vec()) - .collect(); - - assert!(tree - .windows(2) - .all(|events| events[0].len() * 2 == events[1].len())); - - let ok = tree - .windows(2) - .map(|arr| (arr[0].clone(), arr[1].clone())) - .all(|(parents, children)| { - children - .iter() - .map(|(_, _, relation)| relation.clone().unwrap()) - .all(|relation| match relation { - Relation::Reply { - in_reply_to: InReplyTo { event_id, .. }, - } => parents - .iter() - .find(|(parent_id, _, _)| parent_id == &event_id) - .is_some(), - _ => panic!(), - }) - }); - - assert!(ok); - } - - #[tokio::test] - async fn redact_message() { - let Test { admin, samples, .. } = TEST.get_or_init(util::init).await; - let sample = samples.get(3).unwrap(); - let (owner_id, owner_token) = sample.owner(); - - let mut client = admin.clone(); - client.clear_token(); - - // first join - let joins = join_helper(&client, sample.guests(), &sample.room_id).await; - - assert!(joins.iter().all(Result::is_ok)); - - let messages = future::try_join_all( - sample - .guests() - .map(|(user_id, access_token)| { - EventsService::send_message( - &client, - access_token, - &sample.room_id, - TransactionId::new(), - RoomMessageEventContent::text_markdown(format!( - "hello, **my name is {}**", - user_id - )), - ) - }) - .chain(iter::once(EventsService::send_message( - &client, - owner_token, - &sample.room_id, - TransactionId::new(), - RoomMessageEventContent::text_plain(format!( - "and I am the admin of the room, {}", - owner_id - )), - ))), - ) - .await - .unwrap(); - - future::try_join_all(messages[..sample.user_ids.len() - 1].iter().map(|resp| { - EventsService::send_redaction( - &client, - owner_token, - &sample.room_id, - &resp.event_id, - TransactionId::new(), - SendRedactionBody { - reason: format!("I don't like your tone"), - }, - ) - })) - .await - .unwrap(); - - let messages: Vec<_> = EventsService::get_messages( - &client, - owner_token, - &sample.room_id, - GetMessagesQuery { - limit: Some(111), - filter: serde_json::to_string(&RoomEventFilter { - types: vec![MessageLikeEventType::RoomMessage.into()], - not_senders: vec![owner_id.to_owned()], - ..Default::default() - }) - .unwrap(), - ..Default::default() - }, - ) - .map_ok(|resp| { - resp.chunk - .into_iter() - .map(|e| e.deserialize_as::().unwrap()) - .collect() - }) - .await - .unwrap(); - - let redactions: Vec<_> = EventsService::get_messages( - &client, - owner_token, - &sample.room_id, - GetMessagesQuery { - limit: Some(111), - filter: serde_json::to_string(&RoomEventFilter { - types: vec![MessageLikeEventType::RoomRedaction.into()], - ..Default::default() - }) - .unwrap(), - ..Default::default() - }, - ) - .map_ok(|resp| { - resp.chunk - .into_iter() - .map(|e| e.deserialize_as::().unwrap()) - .collect() - }) - .await - .unwrap(); - - assert!(messages[..sample.user_ids.len() - 1] - .iter() - .all(|m| m.as_original().is_none())); - - assert!(messages[..sample.user_ids.len() - 1] - .iter() - .all(|m| redactions - .iter() - .find(|r| r.redacts(&RoomVersionId::V11) == m.event_id() && &r.sender == owner_id) - .is_some())); - - assert!(messages[..sample.user_ids.len() - 1] - .iter() - .all(|m| match m { - MessageLikeEvent::Redacted(_) => true, - _ => false, - })); - } - - #[tokio::test] - async fn annotate_message() { - let Test { admin, samples, .. } = TEST.get_or_init(util::init).await; - let sample = samples.get(3).unwrap(); - let (owner_id, owner_token) = sample.owner(); - - let mut client = admin.clone(); - client.clear_token(); - - // first join - let joins = join_helper(&client, sample.guests(), &sample.room_id).await; - - assert!(joins.iter().all(Result::is_ok)); - - let message = EventsService::send_message( - &client, - owner_token, - &sample.room_id, - TransactionId::new(), - RoomMessageEventContent::text_plain(format!( - "and I am the admin of the room, {}", - owner_id - )), - ) - .await - .unwrap(); - - future::try_join_all(sample.guests().map(|(_, access_token)| { - EventsService::send_message( - &client, - access_token, - &sample.room_id, - TransactionId::new(), - ReactionEventContent::new(Annotation::new( - message.event_id.to_owned(), - "owo".to_owned(), - )), - ) - })) - .await - .unwrap(); - - let annotations: Vec<_> = EventsService::get_messages( - &client, - owner_token, - &sample.room_id, - GetMessagesQuery { - limit: Some(111), - filter: serde_json::to_string(&RoomEventFilter { - types: vec![MessageLikeEventType::Reaction.into()], - ..Default::default() - }) - .unwrap(), - ..Default::default() - }, - ) - .map_ok(|resp| { - resp.chunk - .into_iter() - .map(|e| e.deserialize_as::().unwrap()) - .collect() - }) - .await - .unwrap(); - - assert!(annotations - .iter() - .all(|m| m.content.relates_to.event_id == message.event_id - && m.content.relates_to.key == "owo".to_owned())); - } - - #[tokio::test] - async fn send_state() { - let Test { admin, samples, .. } = TEST.get_or_init(util::init).await; - let sample = samples.get(4).unwrap(); - let (_, owner_token) = sample.owner(); - - let mut client = admin.clone(); - client.clear_token(); - - // first join - let joins = join_helper(&client, sample.guests(), &sample.room_id).await; - - assert!(joins.iter().all(Result::is_ok)); - - let _ = EventsService::send_state( - &client, - owner_token, - &sample.room_id, - None, - RoomTopicEventContent::new("secret banana party".to_owned()), - ) - .await - .unwrap(); - - let state: Vec<_> = EventsService::get_state(&client, owner_token, &sample.room_id) - .map_ok(|resp| { - resp.0 - .iter() - .filter_map(|e| e.deserialize_as::().ok()) - .collect() - }) - .await - .unwrap(); - - assert!(state - .iter() - .find(|s| s.content.topic == "secret banana party".to_owned()) - .is_some()); - } -} diff --git a/crates/test/src/matrix/mod.rs b/crates/test/src/matrix/mod.rs deleted file mode 100644 index baab593..0000000 --- a/crates/test/src/matrix/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod events; -mod room_admin; -mod room_client; -mod shared_token_registration; -mod util; diff --git a/crates/test/src/matrix/room_admin.rs b/crates/test/src/matrix/room_admin.rs deleted file mode 100644 index 44d426d..0000000 --- a/crates/test/src/matrix/room_admin.rs +++ /dev/null @@ -1,251 +0,0 @@ -#[cfg(test)] -mod tests { - use std::time::Duration; - - use futures::{future, TryFutureExt}; - use matrix::{ - admin::resources::room::{ListRoomQuery, MessagesQuery, RoomService as AdminRoomService}, - ruma_common::{RoomId, ServerName}, - ruma_events::TimelineEventType, - }; - - use tokio::sync::OnceCell; - - use crate::matrix::util::{self, Test}; - - static TEST: OnceCell = OnceCell::const_new(); - - #[tokio::test] - async fn get_all_rooms() { - let Test { - samples, - server_name, - admin, - } = TEST.get_or_init(util::init).await; - - let resp: Vec<_> = AdminRoomService::get_all(admin, ListRoomQuery::default()) - .map_ok(|resp| resp.rooms) - .await - .unwrap(); - - while let Some(_) = future::try_join_all(resp.iter().map(|r| { - AdminRoomService::get_room_events(admin, &r.room_id, Default::default()) - .map_ok(|resp| resp.chunk.deserialize().unwrap()) - })) - .await - .map(|ok| { - ok.into_iter().find(|chunk| { - chunk - .iter() - .all(|event| event.event_type() != TimelineEventType::RoomName) - }) - }) - .unwrap() - { - tokio::time::sleep(Duration::from_secs(2)).await; - } - - let resp: Vec<_> = AdminRoomService::get_all(admin, ListRoomQuery::default()) - .map_ok(|resp| resp.rooms) - .await - .unwrap(); - - assert_eq!( - samples - .iter() - .map(|s| s.owner()) - .map(|(user_id, _)| { - let (id, _) = user_id.localpart().rsplit_once("-").unwrap(); - Some(format!("{id}-room",)) - }) - .collect::>(), - resp.iter().map(|r| r.name.clone()).collect::>() - ); - assert_eq!( - samples - .iter() - .map(|s| s.owner()) - .map(|(user_id, _)| { - let (id, _) = user_id.localpart().rsplit_once("-").unwrap(); - Some(format!("#{id}-room-alias:{server_name}",)) - }) - .collect::>(), - resp.iter() - .map(|r| r.canonical_alias.clone()) - .collect::>() - ); - } - - #[tokio::test] - #[should_panic] - async fn get_all_rooms_err() { - let Test { admin, .. } = TEST.get_or_init(util::init).await; - - let _ = AdminRoomService::get_all( - admin, - ListRoomQuery { - from: Some(u64::MAX), - ..Default::default() - }, - ) - .await - .unwrap(); - } - - #[tokio::test] - async fn get_room_details() { - let Test { - samples, - server_name, - admin, - } = TEST.get_or_init(util::init).await; - - let magic_number = Box::into_raw(Box::new(12345)) as usize % samples.len(); - let rand = samples.get(magic_number).unwrap(); - let (user_id, _) = rand.owner(); - - let resp = AdminRoomService::get_one(admin, &rand.room_id) - .await - .unwrap(); - - let (id, _) = user_id.localpart().rsplit_once("-").unwrap(); - assert_eq!(Some(format!("{id}-room",)), resp.name); - assert_eq!( - Some(format!("#{id}-room-alias:{server_name}",)), - resp.canonical_alias, - ); - - assert_eq!(Some(user_id.to_string()), resp.creator); - assert_eq!( - Some(format!("{id}-room-topic",)), - resp.details.and_then(|d| d.topic), - ); - - assert_eq!(resp.join_rules, Some("public".into())); - assert!(resp.public); - assert!(resp.room_type.is_none()); - } - - #[tokio::test] - #[should_panic] - async fn get_room_details_err() { - let Test { - server_name, admin, .. - } = TEST.get_or_init(util::init).await; - - let _ = AdminRoomService::get_one( - admin, - &RoomId::new(&ServerName::parse(server_name).unwrap()), - ) - .await - .unwrap(); - } - - #[tokio::test] - async fn get_room_events() { - let Test { samples, admin, .. } = TEST.get_or_init(util::init).await; - - let magic_number = Box::into_raw(Box::new(12345)) as usize % samples.len(); - let rand = samples.get(magic_number).unwrap(); - - let resp = AdminRoomService::get_room_events( - admin, - &rand.room_id, - // no idea what the type is - MessagesQuery { - from: "".into(), - to: Default::default(), - limit: Default::default(), - filter: Default::default(), - direction: Default::default(), - }, - ) - .await - .unwrap(); - - let events = resp.chunk.deserialize().unwrap(); - assert!(events.len() == 8); - } - - #[tokio::test] - #[should_panic] - async fn get_room_events_err() { - let Test { - server_name, admin, .. - } = TEST.get_or_init(util::init).await; - - let _ = AdminRoomService::get_room_events( - admin, - <&RoomId>::try_from(server_name.as_str()).unwrap(), - MessagesQuery { - from: "".into(), - to: Default::default(), - limit: Default::default(), - filter: Default::default(), - direction: Default::default(), - }, - ) - .await - .unwrap(); - } - - #[tokio::test] - async fn get_state_events() { - let Test { samples, admin, .. } = TEST.get_or_init(util::init).await; - - let magic_number = Box::into_raw(Box::new(12345)) as usize % samples.len(); - let rand = samples.get(magic_number).unwrap(); - - let resp = AdminRoomService::get_state(admin, &rand.room_id) - .await - .unwrap(); - - assert!(resp - .state - .into_iter() - .all(|state| state.kind.contains("room"))); - } - - #[tokio::test] - #[should_panic] - async fn get_state_events_err() { - let Test { - server_name, admin, .. - } = TEST.get_or_init(util::init).await; - - let _ = - AdminRoomService::get_state(admin, <&RoomId>::try_from(server_name.as_str()).unwrap()) - .await - .unwrap(); - } - - #[tokio::test] - async fn get_members() { - let Test { samples, admin, .. } = TEST.get_or_init(util::init).await; - - let magic_number = Box::into_raw(Box::new(12345)) as usize % samples.len(); - let rand = samples.get(magic_number).unwrap(); - let (owner_id, _) = rand.owner(); - - let resp = AdminRoomService::get_members(admin, &rand.room_id) - .await - .unwrap(); - - assert_eq!(resp.members, vec![owner_id.to_string()]); - } - - #[tokio::test] - #[should_panic] - async fn get_members_err() { - let Test { - server_name, admin, .. - } = TEST.get_or_init(util::init).await; - - let _ = AdminRoomService::get_members( - admin, - <&RoomId>::try_from(server_name.as_str()).unwrap(), - ) - .await - .unwrap(); - } -} diff --git a/crates/test/src/matrix/room_client.rs b/crates/test/src/matrix/room_client.rs deleted file mode 100644 index 0570aa6..0000000 --- a/crates/test/src/matrix/room_client.rs +++ /dev/null @@ -1,292 +0,0 @@ -#[cfg(test)] -mod tests { - use futures::{future, FutureExt}; - use matrix::{ - admin::resources::room::RoomService as AdminRoomService, - client::resources::room::{ForgetRoomBody, LeaveRoomBody, RoomKickOrBanBody, RoomService}, - }; - use tokio::sync::OnceCell; - - use crate::matrix::util::{self, join_helper, Test}; - - static TEST: OnceCell = OnceCell::const_new(); - - #[tokio::test] - async fn join_all_rooms() { - let Test { admin, samples, .. } = TEST.get_or_init(util::init).await; - - let mut client = admin.clone(); - client.clear_token(); - - // first join - let result = future::join_all(samples.iter().map(|s| { - join_helper(&client, s.guests(), &s.room_id) - .map(|resp| (&s.room_id, s.guests().map(|(id, _)| id), resp)) - })) - .await; - - tracing::info!("joining all guests"); - - // check whether all guests are in the room and joined the expected room - for (room_id, guests, resps) in result { - let mut resp = AdminRoomService::get_members(&admin, room_id) - .await - .unwrap(); - resp.members.sort(); - - assert!(resps.iter().all(Result::is_ok)); - assert!(resps.iter().flatten().all(|r| &r.room_id == room_id)); - assert!(guests.cloned().all(|guest| resp.members.contains(&guest))); - } - } - - #[tokio::test] - async fn leave_all_rooms() { - let Test { samples, admin, .. } = TEST.get_or_init(util::init).await; - - let mut client = admin.clone(); - client.clear_token(); - - for sample in samples { - for (_, access_token) in sample.guests() { - RoomService::leave( - &client, - access_token, - &sample.room_id, - LeaveRoomBody::default(), - ) - .await - .unwrap(); - } - } - - // check whether all guests left the room - for sample in samples { - let resp = AdminRoomService::get_members(&admin, &sample.room_id) - .await - .unwrap(); - - assert_eq!(resp.members.len(), 1); - assert_eq!( - &[samples - .iter() - .find(|s| s.room_id == sample.room_id) - .map(|s| s.owner()) - .map(|(id, _)| id.to_owned()) - .unwrap()], - resp.members.as_slice() - ); - } - } - - #[tokio::test] - async fn forget_all_rooms() { - let Test { samples, admin, .. } = TEST.get_or_init(util::init).await; - - let mut client = admin.clone(); - client.clear_token(); - - for sample in samples { - for (_, access_token) in sample.guests() { - RoomService::forget( - &client, - access_token, - &sample.room_id, - ForgetRoomBody::default(), - ) - .await - .unwrap(); - } - } - - // check whether all guests are still not present anymore the room - for sample in samples { - let room_id = &sample.room_id; - - let resp = AdminRoomService::get_members(&admin, room_id) - .await - .unwrap(); - - assert_eq!(resp.members.len(), 1); - assert_eq!( - &[samples - .iter() - .find(|s| &s.room_id == room_id) - .map(|s| s.owner()) - .map(|(id, _)| id.to_owned()) - .unwrap()], - resp.members.as_slice() - ); - } - - // confirm a room can't be forgotten if we didn't leave first - for sample in samples { - let room_id = &sample.room_id; - let (_, access_token) = sample.owner(); - - let resp = - RoomService::forget(&client, access_token, room_id, ForgetRoomBody::default()) - .await; - - assert!(resp.is_err()); - } - } - - #[tokio::test] - async fn kick_all_guests() { - let Test { samples, admin, .. } = TEST.get_or_init(util::init).await; - - let mut client = admin.clone(); - client.clear_token(); - - // second join - let result = future::join_all(samples.iter().map(|s| { - join_helper(&client, s.guests(), &s.room_id) - .map(|resp| (&s.room_id, s.guests().map(|(id, _)| id), resp)) - })) - .await; - - tracing::info!("joining all guests"); - - // check whether all guests are in the room and joined the expected room - for (room_id, guests, resps) in result { - let mut resp = AdminRoomService::get_members(&admin, room_id) - .await - .unwrap(); - resp.members.sort(); - - assert!(resps.iter().all(Result::is_ok)); - assert!(resps.iter().flatten().all(|r| &r.room_id == room_id)); - assert!(guests.cloned().all(|guest| resp.members.contains(&guest))); - } - - for sample in samples { - for (user_id, access_token) in sample.guests() { - RoomService::kick( - &client, - access_token, - &sample.room_id, - RoomKickOrBanBody { - reason: Default::default(), - user_id: user_id.clone(), - }, - ) - .await - .unwrap(); - } - } - - // check whether all guests left the room - for sample in samples { - let resp = AdminRoomService::get_members(&admin, &sample.room_id) - .await - .unwrap(); - - assert_eq!(resp.members.len(), 1); - assert_eq!( - &[samples - .iter() - .find(|s| s.room_id == sample.room_id) - .map(|s| s.owner()) - .map(|(id, _)| id.to_owned()) - .unwrap()], - resp.members.as_slice() - ); - } - } - - #[tokio::test] - async fn ban_all_guests() { - let Test { samples, admin, .. } = TEST.get_or_init(util::init).await; - - let mut client = admin.clone(); - client.clear_token(); - - // third join - let result = future::join_all(samples.iter().map(|s| { - join_helper(&client, s.guests(), &s.room_id) - .map(|resp| (&s.room_id, s.guests().map(|(id, _)| id), resp)) - })) - .await; - - tracing::info!("joining all guests"); - - // check whether all guests are in the room and joined the expected room - for (room_id, guests, resps) in result { - let mut resp = AdminRoomService::get_members(&admin, room_id) - .await - .unwrap(); - resp.members.sort(); - - assert!(resps.iter().all(Result::is_ok)); - assert!(resps.iter().flatten().all(|r| &r.room_id == room_id)); - assert!(guests.cloned().all(|guest| resp.members.contains(&guest))); - } - - for sample in samples { - let (_, owner_token) = sample.owner(); - - for (user_id, _) in sample.guests() { - RoomService::ban( - &client, - owner_token, - &sample.room_id, - RoomKickOrBanBody { - reason: Default::default(), - user_id: user_id.clone(), - }, - ) - .await - .unwrap(); - } - } - - // fourth join - let result = future::join_all(samples.iter().map(|s| { - join_helper(&client, s.guests(), &s.room_id) - .map(|resp| (&s.room_id, s.guests().map(|(id, _)| id), resp)) - })) - .await; - - tracing::info!("joining all guests"); - - // check whether all guests got banned from the room - // check whether their join request failed - for (room_id, _, resps) in result { - let resp = AdminRoomService::get_members(&admin, &room_id) - .await - .unwrap(); - - assert_eq!(resp.members.len(), 1); - assert_eq!( - &[samples - .iter() - .find(|s| &s.room_id == room_id) - .map(|s| s.owner()) - .map(|(id, _)| id.to_owned()) - .unwrap()], - resp.members.as_slice() - ); - - assert!(resps.iter().all(|r| r.is_err())); - } - - for sample in samples { - let (_, owner_token) = sample.owner(); - - for (user_id, _) in sample.guests() { - RoomService::unban( - &client, - owner_token, - &sample.room_id, - RoomKickOrBanBody { - reason: Default::default(), - user_id: user_id.clone(), - }, - ) - .await - .unwrap(); - } - } - } -} diff --git a/crates/test/src/matrix/shared_token_registration.rs b/crates/test/src/matrix/shared_token_registration.rs deleted file mode 100644 index 89c978b..0000000 --- a/crates/test/src/matrix/shared_token_registration.rs +++ /dev/null @@ -1,42 +0,0 @@ -use matrix::admin::resources::token::shared_secret::{ - SharedSecretRegistration, SharedSecretRegistrationDto, -}; -use rand::distributions::{Alphanumeric, DistString}; - -use crate::tools::environment::Environment; - -#[tokio::test] -async fn creates_user_using_shared_secret() { - let username = Alphanumeric.sample_string(&mut rand::thread_rng(), 8); - - let env = Environment::new().await; - let nonce = SharedSecretRegistration::get_nonce(&env.client) - .await - .unwrap() - .nonce; - let mac = SharedSecretRegistration::generate_mac( - env.config.synapse_registration_shared_secret.clone(), - nonce.clone(), - username.clone(), - "verysecure".into(), - true, - None, - ) - .unwrap(); - let registration = SharedSecretRegistration::create( - &env.client, - SharedSecretRegistrationDto { - nonce, - username: username.clone(), - displayname: Some(username.clone()), - password: "verysecure".into(), - admin: true, - mac, - }, - ) - .await - .unwrap(); - - assert!(!registration.access_token.is_empty()); - assert!(!registration.user_id.is_empty()); -} diff --git a/crates/test/src/matrix/util.rs b/crates/test/src/matrix/util.rs deleted file mode 100644 index 1ba908e..0000000 --- a/crates/test/src/matrix/util.rs +++ /dev/null @@ -1,199 +0,0 @@ -use anyhow::Result; -use futures::{future, TryFutureExt}; -use matrix::{ - admin::resources::{ - room::{DeleteQuery, ListRoomQuery, ListRoomResponse, RoomService as AdminRoomService}, - user::{CreateUserBody, UserService as AdminUserService}, - }, - client::resources::{ - login::Login, - room::{ - CreateRoomBody, JoinRoomBody, JoinRoomResponse, RoomPreset, RoomService, RoomVisibility, - }, - }, - ruma_common::{OwnedRoomId, OwnedUserId, RoomId}, - Client, -}; - -use rand::Rng; - -use crate::tools::environment::Environment; - -pub struct Test { - pub samples: Vec, - pub server_name: String, - pub admin: Client, -} - -pub struct Sample { - pub user_ids: Vec, - pub room_id: OwnedRoomId, - pub access_tokens: Vec, -} - -impl Sample { - pub fn guests(&self) -> impl Iterator { - self.user_ids.iter().zip(self.access_tokens.iter()).skip(1) - } - pub fn owner(&self) -> (&OwnedUserId, &String) { - self.user_ids - .iter() - .zip(self.access_tokens.iter()) - .clone() - .next() - .unwrap() - } -} - -async fn create_accounts( - client: &Client, - server_name: String, - amount: usize, - room: usize, - seed: u64, -) -> Vec<(OwnedUserId, String)> { - let users: Vec<_> = (0..amount) - .map(|i| OwnedUserId::try_from(format!("@{seed}-{room}-{i}:{}", server_name)).unwrap()) - .collect(); - - future::try_join_all((0..amount).map(|i| { - AdminUserService::create( - &client, - &users.get(i).unwrap(), - CreateUserBody { - password: "verysecure".to_owned(), - logout_devices: false, - displayname: None, - avatar_url: None, - threepids: vec![], - external_ids: vec![], - admin: false, - deactivated: false, - user_type: None, - locked: false, - }, - ) - .and_then(|resp| { - Login::login_credentials(client, resp.name, "verysecure".to_owned()) - .map_ok(|resp| resp.access_token) - }) - })) - .await - .map(|r| users.into_iter().zip(r).collect()) - .unwrap() -} - -async fn create_rooms(client: &Client, seed: u64, tokens: &[String]) -> Vec { - future::try_join_all((0..tokens.len()).map(|i| { - let access_token = &tokens[i]; - - RoomService::create( - client, - access_token.to_owned(), - CreateRoomBody { - name: format!("{seed}-{i}-room"), - topic: format!("{seed}-{i}-room-topic"), - room_alias_name: format!("{seed}-{i}-room-alias"), - preset: Some(RoomPreset::PublicChat), - visibility: Some(RoomVisibility::Public), - ..Default::default() - }, - ) - .map_ok(|resp| resp.room_id) - })) - .await - .unwrap() -} - -async fn remove_rooms(client: &Client) { - let ListRoomResponse { rooms, .. } = - AdminRoomService::get_all(client, ListRoomQuery::default()) - .await - .unwrap(); - - tracing::info!("purging all rooms!"); - - future::try_join_all(rooms.iter().map(|room| { - AdminRoomService::delete_room( - client, - room.room_id.as_ref(), - DeleteQuery { - new_room: None, - block: true, - purge: true, - }, - ) - })) - .await - .unwrap(); -} - -pub async fn init() -> Test { - let _ = tracing_subscriber::fmt::try_init(); - - // set this higher or equal to the number of tests - let rooms = 8; - - let users_per_room = 4; - - let seed = rand::thread_rng().gen(); - - let env = Environment::new().await; - - let server_name = env.config.synapse_server_name.clone(); - let admin_token = env.config.synapse_admin_token.clone(); - let mut admin = env.client.clone(); - - admin.set_token(admin_token).unwrap(); - remove_rooms(&admin).await; - - let accounts = future::join_all( - (0..rooms) - .map(|room| create_accounts(&admin, server_name.clone(), users_per_room, room, seed)), - ) - .await; - - let rooms = create_rooms( - &admin, - seed, - &accounts - .iter() - // make first user in the array the admin - .map(|users| users[0].1.clone()) - .collect::>(), - ) - .await; - - let samples = accounts - .into_iter() - .zip(rooms.into_iter()) - .map(|(users, room_id)| (users.into_iter().unzip(), room_id)) - .map(|((user_ids, access_tokens), room_id)| Sample { - user_ids, - room_id, - access_tokens, - }) - .collect(); - - Test { - samples, - server_name, - admin, - } -} - -pub async fn join_helper( - client: &Client, - users: impl Iterator, - room_id: &RoomId, -) -> Vec> { - future::join_all(users.map(|(_, access_token)| { - RoomService::join( - &client, - access_token.clone(), - room_id.into(), - JoinRoomBody::default(), - ) - })) - .await -} diff --git a/crates/test/src/register.rs b/crates/test/src/register.rs deleted file mode 100644 index 43ec19f..0000000 --- a/crates/test/src/register.rs +++ /dev/null @@ -1,31 +0,0 @@ -use rand::Rng; - -use crate::env::Env; - -pub async fn register( - client: &Env, -) -> Result { - let resp = client - .post("/_commune/client/r0/register") - .json(&router::api::session::register::Payload::new( - format!("steve-{}", rand::thread_rng().gen::()), - "verysecure".into(), - )) - .send() - .await - .unwrap(); - - tracing::info!(?resp); - - resp.json::() - .await -} - -// #[tokio::test] -async fn register_test() { - let client = Env::new().await; - - let resp = register(&client).await.unwrap(); - - assert!(!resp.access_token.is_empty()); -} diff --git a/crates/test/src/server/api/mod.rs b/crates/test/src/server/api/mod.rs deleted file mode 100644 index fbb1de7..0000000 --- a/crates/test/src/server/api/mod.rs +++ /dev/null @@ -1 +0,0 @@ -mod v1; diff --git a/crates/test/src/server/api/v1/account/login.rs b/crates/test/src/server/api/v1/account/login.rs deleted file mode 100644 index 07307aa..0000000 --- a/crates/test/src/server/api/v1/account/login.rs +++ /dev/null @@ -1,94 +0,0 @@ -use commune_server::router::api::v1::account::root::AccountRegisterPayload; -use fake::{ - faker::internet::en::{FreeEmail, Password}, - Fake, -}; -use reqwest::StatusCode; -use scraper::Selector; -use uuid::Uuid; - -use commune::util::secret::Secret; -use commune_server::router::api::v1::account::{ - login::{AccountLoginPayload, AccountLoginResponse}, - verify_code::{AccountVerifyCodePayload, VerifyCodeResponse}, - verify_code_email::{AccountVerifyCodeEmailPayload, VerifyCodeEmailResponse}, -}; - -use crate::tools::{http::HttpClient, maildev::MailDevClient}; - -#[tokio::test] -async fn logs_into_account() { - let http_client = HttpClient::new().await; - let session = Uuid::new_v4(); - let email: String = FreeEmail().fake(); - let verify_code_pld = AccountVerifyCodePayload { - email: email.clone(), - session, - }; - let verify_code_res = http_client - .post("/api/v1/account/verify/code") - .json(&verify_code_pld) - .send() - .await; - let verify_code = verify_code_res.json::().await; - - assert!(verify_code.sent, "should return true for sent"); - - let maildev = MailDevClient::new(); - let mail = maildev.latest().await.unwrap().unwrap(); - let html = mail.html(); - let code_sel = Selector::parse("#code").unwrap(); - let mut code_el = html.select(&code_sel); - let code = code_el.next().unwrap().inner_html(); - let verify_code_email_pld = AccountVerifyCodeEmailPayload { - email: email.clone(), - code: Secret::new(code.clone()), - session, - }; - - let verify_code_res = http_client - .post("/api/v1/account/verify/code/email") - .json(&verify_code_email_pld) - .send() - .await; - let verify_code_email = verify_code_res.json::().await; - - assert!(verify_code_email.valid, "should return true for valid"); - - let username: String = (10..12).fake(); - let username = username.to_ascii_lowercase(); - let password: String = Password(14..20).fake(); - let request_payload = AccountRegisterPayload { - username: username.clone(), - password: password.clone(), - email, - code, - session, - }; - let response = http_client - .post("/api/v1/account") - .json(&request_payload) - .send() - .await; - - assert_eq!( - response.status(), - StatusCode::CREATED, - "should return 201 for successful registration" - ); - - let response = http_client - .post("/api/v1/account/login") - .json(&AccountLoginPayload { username, password }) - .send() - .await; - let response_status = response.status(); - let response_payload = response.json::().await; - - assert_eq!( - response_status, - StatusCode::OK, - "should return 200 for successful login" - ); - assert!(!response_payload.access_token.is_empty(),) -} diff --git a/crates/test/src/server/api/v1/account/mod.rs b/crates/test/src/server/api/v1/account/mod.rs deleted file mode 100644 index c71f243..0000000 --- a/crates/test/src/server/api/v1/account/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod login; -mod root; -mod session; diff --git a/crates/test/src/server/api/v1/account/root.rs b/crates/test/src/server/api/v1/account/root.rs deleted file mode 100644 index 719d307..0000000 --- a/crates/test/src/server/api/v1/account/root.rs +++ /dev/null @@ -1,89 +0,0 @@ -use fake::{ - faker::internet::en::{FreeEmail, Password}, - Fake, -}; - -use matrix::ruma_common::OwnedUserId; -use reqwest::StatusCode; -use scraper::Selector; -use uuid::Uuid; - -use commune::util::secret::Secret; -use commune_server::router::api::v1::account::{ - root::{AccountRegisterPayload, AccountRegisterResponse}, - verify_code::{AccountVerifyCodePayload, VerifyCodeResponse}, - verify_code_email::{AccountVerifyCodeEmailPayload, VerifyCodeEmailResponse}, -}; - -use crate::tools::{http::HttpClient, maildev::MailDevClient}; - -#[tokio::test] -async fn register_account_with_success() { - let http_client = HttpClient::new().await; - let session = Uuid::new_v4(); - let email: String = FreeEmail().fake(); - let verify_code_pld = AccountVerifyCodePayload { - email: email.clone(), - session, - }; - let verify_code_res = http_client - .post("/api/v1/account/verify/code") - .json(&verify_code_pld) - .send() - .await; - let verify_code = verify_code_res.json::().await; - - assert!(verify_code.sent, "should return true for sent"); - - let maildev = MailDevClient::new(); - let mail = maildev.latest().await.unwrap().unwrap(); - let html = mail.html(); - let code_sel = Selector::parse("#code").unwrap(); - let mut code_el = html.select(&code_sel); - let code = code_el.next().unwrap().inner_html(); - let verify_code_email_pld = AccountVerifyCodeEmailPayload { - email: email.clone(), - code: Secret::new(code.clone()), - session, - }; - - let verify_code_res = http_client - .post("/api/v1/account/verify/code/email") - .json(&verify_code_email_pld) - .send() - .await; - let verify_code_email = verify_code_res.json::().await; - - assert!(verify_code_email.valid, "should return true for valid"); - - let username: String = (10..12).fake(); - let username = username.to_ascii_lowercase(); - let password: String = Password(14..20).fake(); - let request_payload = AccountRegisterPayload { - username, - password, - email, - code, - session, - }; - let response = http_client - .post("/api/v1/account") - .json(&request_payload) - .send() - .await; - let response_status = response.status(); - let response_payload = response.json::().await; - - assert_eq!( - response_status, - StatusCode::CREATED, - "should return 201 for created" - ); - assert_eq!( - OwnedUserId::try_from(format!("@{}:matrix.localhost", request_payload.username)) - .map(|user_id| user_id.to_string()) - .unwrap(), - response_payload.credentials.username, - "should return the same username" - ) -} diff --git a/crates/test/src/server/api/v1/account/session.rs b/crates/test/src/server/api/v1/account/session.rs deleted file mode 100644 index 2b9cfe9..0000000 --- a/crates/test/src/server/api/v1/account/session.rs +++ /dev/null @@ -1,149 +0,0 @@ -use fake::{ - faker::internet::en::{FreeEmail, Password}, - Fake, -}; -use reqwest::StatusCode; -use scraper::Selector; -use uuid::Uuid; - -use commune::util::secret::Secret; -use commune_server::router::api::{ - v1::account::{ - login::{AccountLoginPayload, AccountLoginResponse}, - root::AccountRegisterPayload, - session::AccountSessionResponse, - verify_code::{AccountVerifyCodePayload, VerifyCodeResponse}, - verify_code_email::{AccountVerifyCodeEmailPayload, VerifyCodeEmailResponse}, - }, - ApiError, -}; - -use crate::tools::{http::HttpClient, maildev::MailDevClient}; - -#[tokio::test] -async fn retrieves_session_user_from_token() { - let http_client = HttpClient::new().await; - let session = Uuid::new_v4(); - let email: String = FreeEmail().fake(); - let verify_code_pld = AccountVerifyCodePayload { - email: email.clone(), - session, - }; - let verify_code_res = http_client - .post("/api/v1/account/verify/code") - .json(&verify_code_pld) - .send() - .await; - let verify_code = verify_code_res.json::().await; - - assert!(verify_code.sent, "should return true for sent"); - - let maildev = MailDevClient::new(); - let mail = maildev.latest().await.unwrap().unwrap(); - let html = mail.html(); - let code_sel = Selector::parse("#code").unwrap(); - let mut code_el = html.select(&code_sel); - let code = code_el.next().unwrap().inner_html(); - let verify_code_email_pld = AccountVerifyCodeEmailPayload { - email: email.clone(), - code: Secret::new(code.clone()), - session, - }; - - let verify_code_res = http_client - .post("/api/v1/account/verify/code/email") - .json(&verify_code_email_pld) - .send() - .await; - let verify_code_email = verify_code_res.json::().await; - - assert!(verify_code_email.valid, "should return true for valid"); - - let username: String = (10..12).fake(); - let username = username.to_ascii_lowercase(); - let password: String = Password(14..20).fake(); - let request_payload = AccountRegisterPayload { - username: username.clone(), - password: password.clone(), - email: email.clone(), - code, - session, - }; - let response = http_client - .post("/api/v1/account") - .json(&request_payload) - .send() - .await; - - assert_eq!( - response.status(), - StatusCode::CREATED, - "should return 201 for successful registration" - ); - - let response = http_client - .post("/api/v1/account/login") - .json(&AccountLoginPayload { - username: username.clone(), - password, - }) - .send() - .await; - let response_status = response.status(); - let response_payload = response.json::().await; - - assert_eq!( - response_status, - StatusCode::OK, - "should return 200 for successful login" - ); - assert!(!response_payload.access_token.is_empty()); - - let session_res = http_client - .get("/api/v1/account/session") - .token(response_payload.access_token) - .send() - .await; - let session_res_status = session_res.status(); - let session_res_payload = session_res.json::().await; - - assert_eq!( - session_res_status, - StatusCode::OK, - "should return 200 for successful session" - ); - assert!(session_res_payload - .credentials - .username - .starts_with(&format!("@{}", username))); - assert_eq!( - session_res_payload.credentials.email, email, - "should return email" - ); - assert!( - session_res_payload.credentials.verified, - "should return verified" - ); - assert!( - !session_res_payload.credentials.admin, - "should return admin" - ); -} - -#[tokio::test] -async fn kicks_users_with_no_token_specified() { - let http_client = HttpClient::new().await; - let session_res = http_client.get("/api/v1/account/session").send().await; - let session_res_status = session_res.status(); - let session_res_payload = session_res.json::().await; - - assert_eq!(session_res_status, StatusCode::UNAUTHORIZED.as_u16(),); - assert_eq!( - session_res_payload.code, "UNAUTHORIZED", - "should return UNAUTHORIZED" - ); - assert_eq!( - session_res_payload.message, - "You must be authenticated to access this resource", - ); -} diff --git a/crates/test/src/server/api/v1/mod.rs b/crates/test/src/server/api/v1/mod.rs deleted file mode 100644 index cfc38a2..0000000 --- a/crates/test/src/server/api/v1/mod.rs +++ /dev/null @@ -1 +0,0 @@ -mod account; diff --git a/crates/test/src/server/mod.rs b/crates/test/src/server/mod.rs deleted file mode 100644 index b32f9e2..0000000 --- a/crates/test/src/server/mod.rs +++ /dev/null @@ -1 +0,0 @@ -mod api; diff --git a/crates/test/src/tools/environment.rs b/crates/test/src/tools/environment.rs deleted file mode 100644 index a09b449..0000000 --- a/crates/test/src/tools/environment.rs +++ /dev/null @@ -1,29 +0,0 @@ -use commune::{Commune, CommuneConfig}; -use matrix::Client; - -pub struct Environment { - pub client: Client, - pub commune: Commune, - pub config: CommuneConfig, -} - -impl Environment { - pub async fn new() -> Self { - dotenv::dotenv().ok(); - - let config = CommuneConfig::new(); - let client = Client::new( - config.synapse_host.clone(), - config.synapse_server_name.clone(), - ) - .unwrap(); - - let commune = Commune::new(config.clone()).await.unwrap(); - - Self { - client, - commune, - config, - } - } -} diff --git a/crates/test/src/tools/http.rs b/crates/test/src/tools/http.rs deleted file mode 100644 index 8464911..0000000 --- a/crates/test/src/tools/http.rs +++ /dev/null @@ -1,95 +0,0 @@ -use std::net::SocketAddr; - -use dotenv::dotenv; -use reqwest::{header::AUTHORIZATION, Client, StatusCode}; -use tokio::net::TcpListener; - -use commune_server::serve; - -pub(crate) struct HTTP { - pub client: Client, - pub addr: SocketAddr, -} - -impl HttpClient { - pub(crate) async fn new() -> Self { - dotenv().ok(); - - let tcp = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = tcp.local_addr().unwrap(); - - tokio::spawn(async move { - serve(tcp).await.expect("Failed to bind to address"); - }); - - let client = reqwest::Client::builder() - .redirect(reqwest::redirect::Policy::none()) - .build() - .unwrap(); - - HttpClient { client, addr } - } - - pub(crate) fn get(&self, url: &str) -> RequestBuilder { - RequestBuilder { - builder: self.client.get(self.path(url)), - } - } - - pub(crate) fn post(&self, url: &str) -> RequestBuilder { - RequestBuilder { - builder: self.client.post(self.path(url)), - } - } - - fn path(&self, url: &str) -> String { - format!("http://{}{}", self.addr, url) - } -} - -pub(crate) struct RequestBuilder { - builder: reqwest::RequestBuilder, -} - -impl RequestBuilder { - pub(crate) async fn send(self) -> TestResponse { - TestResponse { - response: self.builder.send().await.unwrap(), - } - } - - pub(crate) fn token(mut self, token: impl AsRef) -> Self { - let next = self - .builder - .header(AUTHORIZATION, format!("Bearer {}", token.as_ref())); - - self.builder = next; - self - } - - pub(crate) fn json(mut self, json: &T) -> Self - where - T: serde::Serialize, - { - self.builder = self.builder.json(json); - self - } -} - -#[derive(Debug)] -pub(crate) struct TestResponse { - response: reqwest::Response, -} - -impl TestResponse { - pub(crate) async fn json(self) -> T - where - T: serde::de::DeserializeOwned, - { - self.response.json().await.unwrap() - } - - pub(crate) fn status(&self) -> StatusCode { - self.response.status() - } -} diff --git a/crates/test/src/tools/maildev.rs b/crates/test/src/tools/maildev.rs deleted file mode 100644 index 04d971e..0000000 --- a/crates/test/src/tools/maildev.rs +++ /dev/null @@ -1,104 +0,0 @@ -use anyhow::Result; -use reqwest::{Client, StatusCode}; -use scraper::Html; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct Recipient { - pub address: String, - pub name: String, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub(crate) struct Header { - pub from: String, - pub to: String, - pub subject: String, - pub content_type: String, - pub content_transfer_encoding: String, - pub date: String, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct EnvelopeRecipient { - pub address: String, - pub args: bool, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct Envelope { - pub from: EnvelopeRecipient, - pub to: Vec, - pub host: String, - pub remote_address: String, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct Mail { - pub html: String, - pub headers: Header, - pub subject: String, - pub priority: String, - pub from: Vec, - pub to: Vec, - pub date: String, - pub id: String, - pub time: String, - pub read: bool, - pub envelope: Envelope, - pub source: String, - pub size: usize, - pub size_human: String, - pub attachments: Option>, - pub calculated_bcc: Vec, -} - -impl Mail { - pub fn html(&self) -> Html { - Html::parse_fragment(&self.html) - } -} - -pub(crate) struct MailDevClient { - pub client: Client, -} - -impl MailDevClient { - pub(crate) fn new() -> Self { - let client = reqwest::Client::builder() - .redirect(reqwest::redirect::Policy::none()) - .build() - .unwrap(); - - Self { client } - } - - pub(crate) async fn latest(&self) -> Result> { - let response = self - .client - .get("http://localhost:1080/email") - .send() - .await?; - - match response.status() { - StatusCode::OK => { - let response_body = response.json::>().await?; - - if response_body.is_empty() { - return Ok(None); - } - - let mail = response_body.last().unwrap().clone().to_owned(); - - Ok(Some(mail)) - } - StatusCode::NOT_FOUND => Ok(None), - _ => unreachable!(), - } - } -} diff --git a/crates/test/src/tools/mod.rs b/crates/test/src/tools/mod.rs deleted file mode 100644 index 013536f..0000000 --- a/crates/test/src/tools/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod environment; -pub mod http; -pub mod maildev; diff --git a/crates/test/src/util.rs b/crates/test/src/util.rs deleted file mode 100644 index 8074a0f..0000000 --- a/crates/test/src/util.rs +++ /dev/null @@ -1 +0,0 @@ -mod http; From 38bbb2920438bab830954d1a8c72e2339ff5f509 Mon Sep 17 00:00:00 2001 From: mikoto Date: Tue, 26 Mar 2024 22:33:33 +0000 Subject: [PATCH 23/33] formatting ... --- crates/core/src/lib.rs | 5 ++++- crates/matrix/src/admin/registration_tokens/new.rs | 6 +----- crates/matrix/src/client/logout/root.rs | 2 +- crates/router/src/api/account/display_name.rs | 5 ++++- crates/router/src/api/relative/register.rs | 7 +------ crates/router/src/lib.rs | 7 +++++-- crates/test/src/api/relative.rs | 2 +- crates/test/src/api/relative/available.rs | 1 + crates/test/src/api/relative/login.rs | 3 +-- crates/test/src/api/relative/register.rs | 2 +- crates/test/src/env.rs | 2 +- 11 files changed, 21 insertions(+), 21 deletions(-) diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 1309e2f..2ccd121 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -108,7 +108,10 @@ impl Commune { let token = token.into(); let from = format!("commune@{host}"); - let html = format!("

Thanks for signing up.\n\nUse this code to finish verifying your email:\n{token}

"); + let html = format!( + "

Thanks for signing up.\n\nUse this code to finish verifying your \ + email:\n{token}

" + ); let text = format!( "Thanks for signing up.\n\nUse this code to finish verifying your email:\n{token}" ); diff --git a/crates/matrix/src/admin/registration_tokens/new.rs b/crates/matrix/src/admin/registration_tokens/new.rs index 5c3578e..7849c07 100644 --- a/crates/matrix/src/admin/registration_tokens/new.rs +++ b/crates/matrix/src/admin/registration_tokens/new.rs @@ -23,11 +23,7 @@ pub struct Request { } impl Request { - pub fn new( - token: String, - uses_allowed: usize, - expiry_time: usize, - ) -> Self { + pub fn new(token: String, uses_allowed: usize, expiry_time: usize) -> Self { Self { token, uses_allowed, diff --git a/crates/matrix/src/client/logout/root.rs b/crates/matrix/src/client/logout/root.rs index 0d4c7e4..10d0cbb 100644 --- a/crates/matrix/src/client/logout/root.rs +++ b/crates/matrix/src/client/logout/root.rs @@ -2,7 +2,7 @@ use ruma_common::{ api::{request, response, Metadata}, metadata, }; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; #[allow(dead_code)] const METADATA: Metadata = metadata! { diff --git a/crates/router/src/api/account/display_name.rs b/crates/router/src/api/account/display_name.rs index 0f9b321..7467469 100644 --- a/crates/router/src/api/account/display_name.rs +++ b/crates/router/src/api/account/display_name.rs @@ -2,7 +2,10 @@ use axum::{ response::{IntoResponse, Response}, Json, }; -use axum_extra::{headers::{authorization::Bearer, Authorization}, TypedHeader}; +use axum_extra::{ + headers::{authorization::Bearer, Authorization}, + TypedHeader, +}; use serde::Deserialize; #[derive(Debug, Deserialize)] diff --git a/crates/router/src/api/relative/register.rs b/crates/router/src/api/relative/register.rs index be389a2..e9a5323 100644 --- a/crates/router/src/api/relative/register.rs +++ b/crates/router/src/api/relative/register.rs @@ -14,12 +14,7 @@ pub struct Payload { pub async fn handler(Json(payload): Json) -> Response { use commune::account::register::service; - match service( - payload.username, - payload.password, - ) - .await - { + match service(payload.username, payload.password).await { Ok(resp) => Json(resp).into_response(), Err(e) => { tracing::warn!(?e, "failed to create account"); diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 4f00ca4..a3785e1 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -11,7 +11,10 @@ pub mod api; pub async fn routes() -> Router { let router = Router::new() .route("/register", post(api::relative::register::handler)) - .route("/register/available/:username", get(api::relative::available::handler)) + .route( + "/register/available/:username", + get(api::relative::available::handler), + ) .route("/login", post(api::relative::login::handler)) .route("/logout", post(api::relative::logout::handler)) .nest( @@ -20,7 +23,7 @@ pub async fn routes() -> Router { .route("/whoami", get(api::account::whoami::handler)) .route("/password", put(api::account::password::handler)) .route("/display_name", put(api::account::display_name::handler)) - .route("/avatar", put(api::account::avatar::handler)) + .route("/avatar", put(api::account::avatar::handler)), ); Router::new().nest("/_commune/client/r0", router) diff --git a/crates/test/src/api/relative.rs b/crates/test/src/api/relative.rs index d6f934b..a5a6a98 100644 --- a/crates/test/src/api/relative.rs +++ b/crates/test/src/api/relative.rs @@ -1,4 +1,4 @@ +pub mod available; pub mod login; pub mod logout; pub mod register; -pub mod available; diff --git a/crates/test/src/api/relative/available.rs b/crates/test/src/api/relative/available.rs index e69de29..8b13789 100644 --- a/crates/test/src/api/relative/available.rs +++ b/crates/test/src/api/relative/available.rs @@ -0,0 +1 @@ + diff --git a/crates/test/src/api/relative/login.rs b/crates/test/src/api/relative/login.rs index 9058abc..cdd28de 100644 --- a/crates/test/src/api/relative/login.rs +++ b/crates/test/src/api/relative/login.rs @@ -19,8 +19,7 @@ pub async fn login(client: &Env) -> Result { .await .unwrap(); - resp.json::() - .await + resp.json::().await } #[tokio::test] diff --git a/crates/test/src/api/relative/register.rs b/crates/test/src/api/relative/register.rs index b387687..fbe5b88 100644 --- a/crates/test/src/api/relative/register.rs +++ b/crates/test/src/api/relative/register.rs @@ -38,5 +38,5 @@ async fn register_test() { tracing::info!(?resp); - // assert!(!resp.access_token.is_some() && resp.access_token.map(|at| !at.is_empty()).unwrap()); + assert!(resp.access_token.is_some() && resp.access_token.map(|at| !at.is_empty()).unwrap()); } diff --git a/crates/test/src/env.rs b/crates/test/src/env.rs index e907d4f..3407572 100644 --- a/crates/test/src/env.rs +++ b/crates/test/src/env.rs @@ -32,7 +32,6 @@ impl Env { .build() .unwrap(); - if let Err(e) = client .get(commune::commune().config.matrix.host.to_string() + "/_matrix/client/versions") .send() @@ -52,6 +51,7 @@ impl Env { format!("http://{}{}", self.loopback, path) } + #[allow(dead_code)] pub(crate) fn get(&self, url: &str) -> reqwest::RequestBuilder { tracing::info!("GET {}", self.path(url)); From c5dc99da3c2317e34d8c660b18125b774ce13e71 Mon Sep 17 00:00:00 2001 From: mikoto Date: Tue, 26 Mar 2024 22:45:06 +0000 Subject: [PATCH 24/33] homeserver.yaml --- crates/test/fixtures/synapse/homeserver.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/test/fixtures/synapse/homeserver.yaml b/crates/test/fixtures/synapse/homeserver.yaml index a57c4e5..f6450c8 100644 --- a/crates/test/fixtures/synapse/homeserver.yaml +++ b/crates/test/fixtures/synapse/homeserver.yaml @@ -81,4 +81,4 @@ rc_invites: burst_count: 1000 enable_registration: true -# vim:ft=yaml +enable_registration_without_verification: true From fe4cf62e79ba9b6b65cabd733b06c4c0b8b1d404 Mon Sep 17 00:00:00 2001 From: mikoto Date: Wed, 27 Mar 2024 21:53:52 +0000 Subject: [PATCH 25/33] chore: testing username availability --- .../matrix/src/client/register/available.rs | 4 +- crates/test/src/api/relative/available.rs | 39 +++++++++++++++++++ crates/test/src/api/relative/login.rs | 3 +- crates/test/src/api/relative/logout.rs | 5 ++- crates/test/src/api/relative/register.rs | 3 +- crates/test/src/env.rs | 1 - crates/test/src/lib.rs | 13 +------ crates/test/src/util.rs | 11 ++++++ 8 files changed, 59 insertions(+), 20 deletions(-) create mode 100644 crates/test/src/util.rs diff --git a/crates/matrix/src/client/register/available.rs b/crates/matrix/src/client/register/available.rs index f5ecc37..d145fdc 100644 --- a/crates/matrix/src/client/register/available.rs +++ b/crates/matrix/src/client/register/available.rs @@ -2,7 +2,7 @@ use ruma_common::{ api::{request, response, Metadata}, metadata, }; -use serde::Serialize; +use serde::{Serialize, Deserialize}; #[allow(dead_code)] const METADATA: Metadata = metadata! { @@ -27,7 +27,7 @@ impl Request { } #[response(error = crate::Error)] -#[derive(Serialize)] +#[derive(Deserialize, Serialize)] pub struct Response { pub available: bool, } diff --git a/crates/test/src/api/relative/available.rs b/crates/test/src/api/relative/available.rs index 8b13789..c657fdf 100644 --- a/crates/test/src/api/relative/available.rs +++ b/crates/test/src/api/relative/available.rs @@ -1 +1,40 @@ +use matrix::client::register::available::*; +use crate::{api::relative::register, env::Env, util::generate_comforming_localpart}; + +pub async fn available(client: &Env) -> Result { + let register_resp = register::register(&client).await.unwrap(); + + tracing::info!(?register_resp); + + // taken + let username = register_resp.user_id.localpart(); + let resp = client + .get(format!("/_commune/client/r0/register/available/{username}").as_str()) + .send() + .await?; + + assert!(resp.status().is_client_error()); + + let resp = client + .get( + format!( + "/_commune/client/r0/register/available/{}", + generate_comforming_localpart() + ) + .as_str(), + ) + .send() + .await?; + + resp.json().await +} + +#[tokio::test] +async fn available_test() { + let client = Env::new().await; + + let resp = available(&client).await.unwrap(); + + tracing::info!(?resp); +} diff --git a/crates/test/src/api/relative/login.rs b/crates/test/src/api/relative/login.rs index cdd28de..2f6b828 100644 --- a/crates/test/src/api/relative/login.rs +++ b/crates/test/src/api/relative/login.rs @@ -16,8 +16,7 @@ pub async fn login(client: &Env) -> Result { password: Secret::new("verysecure"), }) .send() - .await - .unwrap(); + .await?; resp.json::().await } diff --git a/crates/test/src/api/relative/logout.rs b/crates/test/src/api/relative/logout.rs index eaf583a..2a39d4b 100644 --- a/crates/test/src/api/relative/logout.rs +++ b/crates/test/src/api/relative/logout.rs @@ -14,10 +14,11 @@ pub async fn logout(client: &Env) -> Result { format!("Bearer {}", &login_resp.access_token), ) .send() - .await - .unwrap(); + .await?; resp.json::().await + + // TODO: use `/whoami` to confirm access token is invalid } #[tokio::test] diff --git a/crates/test/src/api/relative/register.rs b/crates/test/src/api/relative/register.rs index fbe5b88..2a0522b 100644 --- a/crates/test/src/api/relative/register.rs +++ b/crates/test/src/api/relative/register.rs @@ -24,8 +24,7 @@ pub async fn register(client: &Env) -> Result { password: Secret::new("verysecure"), }) .send() - .await - .unwrap(); + .await?; resp.json::().await } diff --git a/crates/test/src/env.rs b/crates/test/src/env.rs index 3407572..45380c9 100644 --- a/crates/test/src/env.rs +++ b/crates/test/src/env.rs @@ -51,7 +51,6 @@ impl Env { format!("http://{}{}", self.loopback, path) } - #[allow(dead_code)] pub(crate) fn get(&self, url: &str) -> reqwest::RequestBuilder { tracing::info!("GET {}", self.path(url)); diff --git a/crates/test/src/lib.rs b/crates/test/src/lib.rs index c84f5e1..bdabd58 100644 --- a/crates/test/src/lib.rs +++ b/crates/test/src/lib.rs @@ -1,14 +1,5 @@ -// #[cfg(test)] -// mod commune; - -// #[cfg(test)] -// mod tools; - -// #[cfg(test)] -// mod matrix; - -// #[cfg(test)] -// mod server; +#[cfg(test)] +mod util; #[cfg(test)] mod api; diff --git a/crates/test/src/util.rs b/crates/test/src/util.rs new file mode 100644 index 0000000..d69ac35 --- /dev/null +++ b/crates/test/src/util.rs @@ -0,0 +1,11 @@ +use rand::seq::IteratorRandom; + +pub fn generate_comforming_localpart() -> String { + let allowed = ('0'..='9') + .chain('a'..='z') + .chain(['-', '.', '=', '_', '/', '+']); + allowed + .choose_multiple(&mut rand::thread_rng(), 8) + .into_iter() + .collect() +} From 6e4f886069bee11f163f8c533af66fc4a5c3087a Mon Sep 17 00:00:00 2001 From: mikoto Date: Wed, 27 Mar 2024 23:30:14 +0000 Subject: [PATCH 26/33] chore: finish second part of tests --- crates/matrix/src/client/account/password.rs | 2 +- crates/matrix/src/client/account/whoami.rs | 4 +- crates/matrix/src/client/logout/root.rs | 2 - .../src/client/profile/avatar_url/update.rs | 2 +- .../src/client/profile/display_name/update.rs | 2 + crates/router/src/api/account/avatar.rs | 6 +- crates/router/src/api/account/display_name.rs | 6 +- crates/router/src/api/account/password.rs | 12 ++-- crates/router/src/api/relative/logout.rs | 7 +- crates/test/src/api.rs | 3 +- crates/test/src/api/account/avatar.rs | 58 +++++++++-------- crates/test/src/api/account/display_name.rs | 51 ++++++++------- crates/test/src/api/account/email.rs | 19 ------ crates/test/src/api/account/password.rs | 64 +++++++++---------- crates/test/src/api/account/whoami.rs | 52 +++++++++------ crates/test/src/api/relative/logout.rs | 9 ++- crates/test/src/api/relative/register.rs | 2 +- crates/test/src/env.rs | 7 ++ 18 files changed, 156 insertions(+), 152 deletions(-) diff --git a/crates/matrix/src/client/account/password.rs b/crates/matrix/src/client/account/password.rs index 7ab5fc0..c647ba6 100644 --- a/crates/matrix/src/client/account/password.rs +++ b/crates/matrix/src/client/account/password.rs @@ -17,6 +17,7 @@ const METADATA: Metadata = metadata! { }; #[request(error = crate::Error)] +#[derive(Serialize)] pub struct Request { pub auth: Auth, @@ -51,5 +52,4 @@ impl Request { } #[response(error = crate::Error)] -#[derive(Serialize)] pub struct Response {} diff --git a/crates/matrix/src/client/account/whoami.rs b/crates/matrix/src/client/account/whoami.rs index c2f1f73..ae2e869 100644 --- a/crates/matrix/src/client/account/whoami.rs +++ b/crates/matrix/src/client/account/whoami.rs @@ -2,7 +2,7 @@ use ruma_common::{ api::{request, response, Metadata}, metadata, OwnedDeviceId, OwnedUserId, }; -use serde::Serialize; +use serde::{Serialize, Deserialize}; #[allow(dead_code)] const METADATA: Metadata = metadata! { @@ -25,7 +25,7 @@ impl Request { } #[response(error = crate::Error)] -#[derive(Serialize)] +#[derive(Deserialize, Serialize)] pub struct Response { pub device_id: OwnedDeviceId, pub user_id: OwnedUserId, diff --git a/crates/matrix/src/client/logout/root.rs b/crates/matrix/src/client/logout/root.rs index 10d0cbb..f715df0 100644 --- a/crates/matrix/src/client/logout/root.rs +++ b/crates/matrix/src/client/logout/root.rs @@ -2,7 +2,6 @@ use ruma_common::{ api::{request, response, Metadata}, metadata, }; -use serde::{Deserialize, Serialize}; #[allow(dead_code)] const METADATA: Metadata = metadata! { @@ -25,5 +24,4 @@ impl Request { } #[response(error = crate::Error)] -#[derive(Deserialize, Serialize)] pub struct Response {} diff --git a/crates/matrix/src/client/profile/avatar_url/update.rs b/crates/matrix/src/client/profile/avatar_url/update.rs index d291379..dfcbd13 100644 --- a/crates/matrix/src/client/profile/avatar_url/update.rs +++ b/crates/matrix/src/client/profile/avatar_url/update.rs @@ -15,6 +15,7 @@ const METADATA: Metadata = metadata! { }; #[request(error = crate::Error)] +#[derive(Serialize)] pub struct Request { #[ruma_api(path)] pub user_id: OwnedUserId, @@ -32,5 +33,4 @@ impl Request { } #[response(error = crate::Error)] -#[derive(Serialize)] pub struct Response {} diff --git a/crates/matrix/src/client/profile/display_name/update.rs b/crates/matrix/src/client/profile/display_name/update.rs index 5cac54f..83425fe 100644 --- a/crates/matrix/src/client/profile/display_name/update.rs +++ b/crates/matrix/src/client/profile/display_name/update.rs @@ -2,6 +2,7 @@ use ruma_common::{ api::{request, response, Metadata}, metadata, OwnedUserId, }; +use serde::{Deserialize, Serialize}; #[allow(dead_code)] const METADATA: Metadata = metadata! { @@ -14,6 +15,7 @@ const METADATA: Metadata = metadata! { }; #[request(error = crate::Error)] +#[derive(Deserialize, Serialize)] pub struct Request { #[ruma_api(path)] pub user_id: OwnedUserId, diff --git a/crates/router/src/api/account/avatar.rs b/crates/router/src/api/account/avatar.rs index 2dbb812..7e759d2 100644 --- a/crates/router/src/api/account/avatar.rs +++ b/crates/router/src/api/account/avatar.rs @@ -7,9 +7,9 @@ use axum_extra::{ TypedHeader, }; use matrix::ruma_common::OwnedMxcUri; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; -#[derive(Debug, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct Payload { pub mxc_uri: OwnedMxcUri, } @@ -21,7 +21,7 @@ pub async fn handler( use commune::profile::avatar::update::service; match service(access_token.token(), payload.mxc_uri).await { - Ok(resp) => Json(resp).into_response(), + Ok(_) => ().into_response(), Err(e) => { tracing::warn!(?e, "failed to update avatar"); diff --git a/crates/router/src/api/account/display_name.rs b/crates/router/src/api/account/display_name.rs index 7467469..e774436 100644 --- a/crates/router/src/api/account/display_name.rs +++ b/crates/router/src/api/account/display_name.rs @@ -6,9 +6,9 @@ use axum_extra::{ headers::{authorization::Bearer, Authorization}, TypedHeader, }; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct Payload { pub display_name: String, } @@ -20,7 +20,7 @@ pub async fn handler( use commune::profile::avatar::update::service; match service(access_token.token(), payload.display_name).await { - Ok(resp) => Json(resp).into_response(), + Ok(_) => ().into_response(), Err(e) => { tracing::warn!(?e, "failed to update display name"); diff --git a/crates/router/src/api/account/password.rs b/crates/router/src/api/account/password.rs index 37d28e4..f6a5d71 100644 --- a/crates/router/src/api/account/password.rs +++ b/crates/router/src/api/account/password.rs @@ -7,13 +7,13 @@ use axum_extra::{ TypedHeader, }; use commune::util::secret::Secret; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct Payload { - username: String, - password: Secret, - new_password: Secret, + pub username: String, + pub password: Secret, + pub new_password: Secret, } pub async fn handler( @@ -30,7 +30,7 @@ pub async fn handler( ) .await { - Ok(resp) => Json(resp).into_response(), + Ok(_) => ().into_response(), Err(e) => { tracing::warn!(?e, "failed to reset password"); diff --git a/crates/router/src/api/relative/logout.rs b/crates/router/src/api/relative/logout.rs index 2a492da..de2436d 100644 --- a/crates/router/src/api/relative/logout.rs +++ b/crates/router/src/api/relative/logout.rs @@ -1,7 +1,4 @@ -use axum::{ - response::{IntoResponse, Response}, - Json, -}; +use axum::response::{IntoResponse, Response}; use axum_extra::{ headers::{authorization::Bearer, Authorization}, TypedHeader, @@ -11,7 +8,7 @@ pub async fn handler(TypedHeader(access_token): TypedHeader Json(resp).into_response(), + Ok(_) => ().into_response(), Err(e) => { tracing::warn!(?e, "failed to logout user"); diff --git a/crates/test/src/api.rs b/crates/test/src/api.rs index 4230649..8d9dd19 100644 --- a/crates/test/src/api.rs +++ b/crates/test/src/api.rs @@ -2,6 +2,5 @@ //! //! reference: https://spec.matrix.org/unstable/client-server-api -// pub mod account; +pub mod account; pub mod relative; -// pub mod session; diff --git a/crates/test/src/api/account/avatar.rs b/crates/test/src/api/account/avatar.rs index 3c55ff8..030b696 100644 --- a/crates/test/src/api/account/avatar.rs +++ b/crates/test/src/api/account/avatar.rs @@ -1,33 +1,35 @@ -use axum::{ - response::{IntoResponse, Response}, - Json, -}; -use axum_extra::{headers::{authorization::Bearer, Authorization}, TypedHeader}; use matrix::ruma_common::OwnedMxcUri; -use serde::Deserialize; +use router::api::account::avatar::Payload; -#[derive(Debug, Deserialize)] -pub struct Payload { - pub mxc_uri: OwnedMxcUri, +use crate::{api::relative::login, env::Env}; + +pub async fn update_avatar(client: &Env) -> Result { + let login_resp = login::login(&client).await.unwrap(); + + tracing::info!(?login_resp); + + let resp = client + .put("/_commune/client/r0/account/avatar") + .json(&Payload { + mxc_uri: OwnedMxcUri::try_from("mxc://example.org/SEsfnsuifSDFSSEF").unwrap(), + }) + .header( + reqwest::header::AUTHORIZATION, + format!("Bearer {}", &login_resp.access_token), + ) + .send() + .await?; + + Ok(resp.status().is_success()) } -pub async fn handler( - TypedHeader(access_token): TypedHeader>, - Json(payload): Json, -) -> Response { - use commune::profile::avatar::update::service; - - match service( - access_token.token(), - payload.mxc_uri, - ) - .await - { - Ok(resp) => Json(resp).into_response(), - Err(e) => { - tracing::warn!(?e, "failed to update avatar"); - - e.into_response() - } - } +#[tokio::test] +async fn update_avatar_test() { + let client = Env::new().await; + + let resp = update_avatar(&client).await.unwrap(); + + tracing::info!(?resp); + + assert_eq!(resp, true); } diff --git a/crates/test/src/api/account/display_name.rs b/crates/test/src/api/account/display_name.rs index 0f9b321..6df2699 100644 --- a/crates/test/src/api/account/display_name.rs +++ b/crates/test/src/api/account/display_name.rs @@ -1,27 +1,34 @@ -use axum::{ - response::{IntoResponse, Response}, - Json, -}; -use axum_extra::{headers::{authorization::Bearer, Authorization}, TypedHeader}; -use serde::Deserialize; - -#[derive(Debug, Deserialize)] -pub struct Payload { - pub display_name: String, +use router::api::account::display_name::Payload; + +use crate::{api::relative::login, env::Env}; + +pub async fn update_display_name(client: &Env) -> Result { + let login_resp = login::login(&client).await.unwrap(); + + tracing::info!(?login_resp); + + let resp = client + .put("/_commune/client/r0/account/display_name") + .json(&Payload { + display_name: "awesome display name".to_owned(), + }) + .header( + reqwest::header::AUTHORIZATION, + format!("Bearer {}", &login_resp.access_token), + ) + .send() + .await?; + + Ok(resp.status().is_success()) } -pub async fn handler( - TypedHeader(access_token): TypedHeader>, - Json(payload): Json, -) -> Response { - use commune::profile::avatar::update::service; +#[tokio::test] +async fn update_display_name_test() { + let client = Env::new().await; + + let resp = update_display_name(&client).await.unwrap(); - match service(access_token.token(), payload.display_name).await { - Ok(resp) => Json(resp).into_response(), - Err(e) => { - tracing::warn!(?e, "failed to update display name"); + tracing::info!(?resp); - e.into_response() - } - } + assert_eq!(resp, true); } diff --git a/crates/test/src/api/account/email.rs b/crates/test/src/api/account/email.rs index 6fb66d1..e69de29 100644 --- a/crates/test/src/api/account/email.rs +++ b/crates/test/src/api/account/email.rs @@ -1,19 +0,0 @@ -use axum::{ - extract::Path, - response::{IntoResponse, Response}, - Json, -}; -use email_address::EmailAddress; - -pub async fn handler(Path(email): Path) -> Response { - use commune::account::email::service; - - match service(email).await { - Ok(resp) => Json(resp).into_response(), - Err(e) => { - tracing::warn!(?e, "failed to handle email verification"); - - e.into_response() - } - } -} diff --git a/crates/test/src/api/account/password.rs b/crates/test/src/api/account/password.rs index b3ea639..b95f523 100644 --- a/crates/test/src/api/account/password.rs +++ b/crates/test/src/api/account/password.rs @@ -1,37 +1,37 @@ -use axum::{ - response::{IntoResponse, Response}, - Json, -}; -use axum_extra::{headers::{authorization::Bearer, Authorization}, TypedHeader}; use commune::util::secret::Secret; -use serde::Deserialize; +use router::api::account::password::Payload; -#[derive(Debug, Deserialize)] -pub struct Payload { - username: String, - password: Secret, - new_password: Secret, +use crate::{api::relative::login, env::Env}; + +pub async fn update_password(client: &Env) -> Result { + let login_resp = login::login(&client).await.unwrap(); + + tracing::info!(?login_resp); + + let resp = client + .put("/_commune/client/r0/account/password") + .json(&Payload { + username: login_resp.user_id.localpart().to_owned(), + password: Secret::new("verysecure"), + new_password: Secret::new("notverysecure"), + }) + .header( + reqwest::header::AUTHORIZATION, + format!("Bearer {}", &login_resp.access_token), + ) + .send() + .await?; + + Ok(resp.status().is_success()) } -pub async fn handler( - TypedHeader(access_token): TypedHeader>, - Json(payload): Json, -) -> Response { - use commune::account::password::service; - - match service( - access_token.token(), - payload.username, - payload.password, - payload.new_password, - ) - .await - { - Ok(resp) => Json(resp).into_response(), - Err(e) => { - tracing::warn!(?e, "failed to reset password"); - - e.into_response() - } - } +#[tokio::test] +async fn update_password_test() { + let client = Env::new().await; + + let resp = update_password(&client).await.unwrap(); + + tracing::info!(?resp); + + assert_eq!(resp, true); } diff --git a/crates/test/src/api/account/whoami.rs b/crates/test/src/api/account/whoami.rs index c5a1f91..a5cdb01 100644 --- a/crates/test/src/api/account/whoami.rs +++ b/crates/test/src/api/account/whoami.rs @@ -1,21 +1,33 @@ -use axum::{ - response::{IntoResponse, Response}, - Json, -}; -use axum_extra::{ - headers::{authorization::Bearer, Authorization}, - TypedHeader, -}; - -pub async fn handler(TypedHeader(access_token): TypedHeader>) -> Response { - use commune::account::whoami::service; - - match service(access_token.token()).await { - Ok(resp) => Json(resp).into_response(), - Err(e) => { - tracing::warn!(?e, "failed to associate access token with user"); - - e.into_response() - } - } +use matrix::client::account::whoami::*; + +use crate::{api::relative::login, env::Env}; + +pub async fn whoami(client: &Env) -> Result { + let login_resp = login::login(&client).await.unwrap(); + + tracing::info!(?login_resp); + + let resp = client + .get("/_commune/client/r0/account/whoami") + .header( + reqwest::header::AUTHORIZATION, + format!("Bearer {}", &login_resp.access_token), + ) + .send() + .await?; + + let json = resp.json::().await?; + + assert_eq!(login_resp.user_id, json.user_id); + + Ok(json) +} + +#[tokio::test] +async fn whoami_test() { + let client = Env::new().await; + + let resp = whoami(&client).await.unwrap(); + + tracing::info!(?resp); } diff --git a/crates/test/src/api/relative/logout.rs b/crates/test/src/api/relative/logout.rs index 2a39d4b..4e713b5 100644 --- a/crates/test/src/api/relative/logout.rs +++ b/crates/test/src/api/relative/logout.rs @@ -1,8 +1,6 @@ -use matrix::client::logout::root::*; - use crate::{api::relative::login, env::Env}; -pub async fn logout(client: &Env) -> Result { +pub async fn logout(client: &Env) -> Result { let login_resp = login::login(&client).await.unwrap(); tracing::info!(?login_resp); @@ -16,9 +14,8 @@ pub async fn logout(client: &Env) -> Result { .send() .await?; - resp.json::().await - // TODO: use `/whoami` to confirm access token is invalid + Ok(resp.status().is_success()) } #[tokio::test] @@ -28,4 +25,6 @@ async fn logout_test() { let resp = logout(&client).await.unwrap(); tracing::info!(?resp); + + assert_eq!(resp, true); } diff --git a/crates/test/src/api/relative/register.rs b/crates/test/src/api/relative/register.rs index 2a0522b..b300c33 100644 --- a/crates/test/src/api/relative/register.rs +++ b/crates/test/src/api/relative/register.rs @@ -26,7 +26,7 @@ pub async fn register(client: &Env) -> Result { .send() .await?; - resp.json::().await + resp.json().await } #[tokio::test] diff --git a/crates/test/src/env.rs b/crates/test/src/env.rs index 45380c9..1954c30 100644 --- a/crates/test/src/env.rs +++ b/crates/test/src/env.rs @@ -62,4 +62,11 @@ impl Env { self.client.post(self.path(url)) } + + pub(crate) fn put(&self, url: &str) -> reqwest::RequestBuilder { + tracing::info!("PUT {}", self.path(url)); + + self.client.put(self.path(url)) + } + } From f6f7c96958013906d8b17a85d60c67081b4fc7f7 Mon Sep 17 00:00:00 2001 From: mikoto Date: Thu, 28 Mar 2024 00:26:16 +0000 Subject: [PATCH 27/33] wip: email verification --- Cargo.toml | 4 +- crates/core/src/account/email.rs | 6 +- crates/core/src/account/register.rs | 15 +- crates/matrix/src/client/uiaa.rs | 21 ++- crates/router/Cargo.toml | 5 +- crates/router/src/api.rs | 2 +- crates/router/src/api/register.rs | 3 + .../api/{relative => register}/available.rs | 0 crates/router/src/api/register/email.rs | 39 ++++++ .../register.rs => register/root.rs} | 6 +- crates/router/src/api/relative.rs | 2 - crates/router/src/lib.rs | 8 +- crates/router/src/router/api/mod.rs | 90 ------------ .../router/src/router/api/v1/account/email.rs | 34 ----- .../router/src/router/api/v1/account/login.rs | 95 ------------- .../router/src/router/api/v1/account/mod.rs | 35 ----- .../router/src/router/api/v1/account/root.rs | 128 ------------------ .../src/router/api/v1/account/session.rs | 48 ------- .../src/router/api/v1/account/verify_code.rs | 74 ---------- .../api/v1/account/verify_code_email.rs | 76 ----------- crates/router/src/router/api/v1/mod.rs | 11 -- 21 files changed, 94 insertions(+), 608 deletions(-) create mode 100644 crates/router/src/api/register.rs rename crates/router/src/api/{relative => register}/available.rs (100%) create mode 100644 crates/router/src/api/register/email.rs rename crates/router/src/api/{relative/register.rs => register/root.rs} (77%) delete mode 100644 crates/router/src/router/api/mod.rs delete mode 100644 crates/router/src/router/api/v1/account/email.rs delete mode 100644 crates/router/src/router/api/v1/account/login.rs delete mode 100644 crates/router/src/router/api/v1/account/mod.rs delete mode 100644 crates/router/src/router/api/v1/account/root.rs delete mode 100644 crates/router/src/router/api/v1/account/session.rs delete mode 100644 crates/router/src/router/api/v1/account/verify_code.rs delete mode 100644 crates/router/src/router/api/v1/account/verify_code_email.rs delete mode 100644 crates/router/src/router/api/v1/mod.rs diff --git a/Cargo.toml b/Cargo.toml index bbdf33d..42cb4b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,9 +14,10 @@ default-members = ["crates/router"] resolver = "2" [workspace.dependencies] -axum-extra = { version = "0.9.3", features = ["typed-header"] } +axum-extra = { version = "0.9.3", features = ["typed-header", "cookie"] } async-trait = "0.1.74" # async-stream = "0.3.5" +base64 = "0.22.0" bytes = "1.5.0" email_address = { version = "0.2.4", features = ["serde", "serde_support"] } figment = { version = "0.10.14", features = ["toml", "env"] } @@ -47,6 +48,7 @@ tracing = "0.1.40" tracing-subscriber = "0.3.18" url = "2.4.1" rand = "0.8.5" +ring = "0.17.8" thiserror = "1.0.50" validator = { version = "0.16", features = ["derive"] } diff --git a/crates/core/src/account/email.rs b/crates/core/src/account/email.rs index c251b8e..d37ef5e 100644 --- a/crates/core/src/account/email.rs +++ b/crates/core/src/account/email.rs @@ -6,7 +6,7 @@ use rand::{distributions::Uniform, prelude::Distribution}; use crate::{commune, error::Result}; -pub async fn service(address: EmailAddress) -> Result<()> { +pub async fn service(address: EmailAddress) -> Result { let uni = Uniform::new('0', '9'); let token: String = uni.sample_iter(rand::thread_rng()).take(6).collect(); @@ -26,7 +26,7 @@ pub async fn service(address: EmailAddress) -> Result<()> { .send_matrix_request(req, Some(&commune().config.matrix.admin_token.inner())) .await?; - commune().send_email_verification(address, token).await?; + commune().send_email_verification(address, token.clone()).await?; - Ok(()) + Ok(token) } diff --git a/crates/core/src/account/register.rs b/crates/core/src/account/register.rs index b8b384e..2c8e73a 100644 --- a/crates/core/src/account/register.rs +++ b/crates/core/src/account/register.rs @@ -2,7 +2,7 @@ use http::StatusCode; use matrix::{ client::{ register::root::*, - uiaa::{Auth, AuthData, AuthType, Dummy, UiaaResponse}, + uiaa::{Auth, AuthData, AuthType, Dummy, RegistrationToken, UiaaResponse}, }, ruma_client::Error::FromHttpResponse, ruma_common::api::error::{FromHttpResponseError, MatrixError, MatrixErrorBody}, @@ -10,13 +10,22 @@ use matrix::{ use crate::{commune, error::Result, util::secret::Secret}; -pub async fn service(username: impl Into, password: Secret) -> Result { +pub async fn service( + username: impl Into, + password: Secret, + registration_token: Option, +) -> Result { let req = Request::new( username.into(), password.inner(), Some("commune".to_owned()), None, - None, + registration_token.map(|rt| { + Auth::new( + AuthData::RegistrationToken(RegistrationToken::new(rt)), + None, + ) + }), ); let mut retry_req = req.clone(); diff --git a/crates/matrix/src/client/uiaa.rs b/crates/matrix/src/client/uiaa.rs index a49a517..919dac5 100644 --- a/crates/matrix/src/client/uiaa.rs +++ b/crates/matrix/src/client/uiaa.rs @@ -84,9 +84,9 @@ pub enum AuthData { // Dummy authentication (`m.login.dummy`). Dummy(Dummy), - // Registration token-based authentication (`m.login.registration_token`). - // RegistrationToken(RegistrationToken), + // Registration token-based authentication (`m.login.registration_token`). + RegistrationToken(RegistrationToken), // Fallback acknowledgement. // FallbackAcknowledgement(FallbackAcknowledgement), } @@ -96,6 +96,7 @@ impl AuthData { match self { AuthData::Password(_) => AuthType::Password, AuthData::Dummy(_) => AuthType::Dummy, + AuthData::RegistrationToken(_) => AuthType::RegistrationToken, } } } @@ -118,7 +119,7 @@ pub struct Password { } impl Password { - pub fn new>(user_id: impl Into, password: S) -> Self { + pub fn new(user_id: impl Into, password: impl Into) -> Self { let user: &UserId = &user_id.into(); Self { @@ -130,6 +131,20 @@ impl Password { } } +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(tag = "type", rename = "m.login.registration_token")] +pub struct RegistrationToken { + token: String, +} + +impl RegistrationToken { + pub fn new(token: impl Into) -> Self { + Self { + token: token.into(), + } + } +} + #[derive(Clone, Debug, Serialize)] pub struct Auth { session: Option, diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index c44e461..bc974a2 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -14,14 +14,17 @@ path = "src/lib.rs" [dependencies] axum = { workspace = true, features = ["tokio", "macros"] } -axum-extra = { workspace = true, features = ["typed-header"] } +axum-extra = { workspace = true, features = ["typed-header", "cookie"] } anyhow = { workspace = true } +base64 = { workspace = true } http = { workspace = true } email_address = { workspace = true } # openssl = { workspace = true, features = ["vendored"] } # openssl-sys = { workspace = true, features = ["vendored"] } +ring = { workspace = true } serde = { workspace = true, features = ["derive"] } tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] } +time = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } url = { workspace = true, features = ["serde"] } diff --git a/crates/router/src/api.rs b/crates/router/src/api.rs index 9756a11..d8d9b3a 100644 --- a/crates/router/src/api.rs +++ b/crates/router/src/api.rs @@ -4,4 +4,4 @@ pub mod account; pub mod relative; -// pub mod session; +pub mod register; diff --git a/crates/router/src/api/register.rs b/crates/router/src/api/register.rs new file mode 100644 index 0000000..0fca384 --- /dev/null +++ b/crates/router/src/api/register.rs @@ -0,0 +1,3 @@ +pub mod available; +pub mod email; +pub mod root; diff --git a/crates/router/src/api/relative/available.rs b/crates/router/src/api/register/available.rs similarity index 100% rename from crates/router/src/api/relative/available.rs rename to crates/router/src/api/register/available.rs diff --git a/crates/router/src/api/register/email.rs b/crates/router/src/api/register/email.rs new file mode 100644 index 0000000..7276f13 --- /dev/null +++ b/crates/router/src/api/register/email.rs @@ -0,0 +1,39 @@ +use axum::{ + extract::Path, + http::header::SET_COOKIE, + response::{AppendHeaders, IntoResponse, Response}, +}; +use axum_extra::extract::cookie::Cookie; +use base64::{ + alphabet, + engine::{general_purpose, GeneralPurpose}, + Engine, +}; +use email_address::EmailAddress; +use ring::digest; +use time::Duration; + +pub async fn handler(Path(address): Path) -> Response { + use commune::account::email::service; + + match service(address).await { + Ok(token) => { + let engine = GeneralPurpose::new(&alphabet::URL_SAFE, general_purpose::NO_PAD); + + let token_sha256 = digest::digest(&digest::SHA256, &token.as_bytes()); + let token_sha256_b64 = engine.encode(token_sha256); + + let cookie = Cookie::build(("registration-token", token_sha256_b64)).max_age(Duration::minutes(60).into()); + + ( + (), + AppendHeaders([(SET_COOKIE, format!("registration_token={}", cookie.to_string()))]), + ).into_response() + } + Err(e) => { + tracing::warn!(?e, "failed to send verification email"); + + e.into_response() + } + } +} diff --git a/crates/router/src/api/relative/register.rs b/crates/router/src/api/register/root.rs similarity index 77% rename from crates/router/src/api/relative/register.rs rename to crates/router/src/api/register/root.rs index e9a5323..5f13d03 100644 --- a/crates/router/src/api/relative/register.rs +++ b/crates/router/src/api/register/root.rs @@ -8,13 +8,17 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] pub struct Payload { pub username: String, + pub password: Secret, + + #[serde(default)] + pub registration_token: Option, } pub async fn handler(Json(payload): Json) -> Response { use commune::account::register::service; - match service(payload.username, payload.password).await { + match service(payload.username, payload.password, payload.registration_token).await { Ok(resp) => Json(resp).into_response(), Err(e) => { tracing::warn!(?e, "failed to create account"); diff --git a/crates/router/src/api/relative.rs b/crates/router/src/api/relative.rs index a5a6a98..1c7c4cc 100644 --- a/crates/router/src/api/relative.rs +++ b/crates/router/src/api/relative.rs @@ -1,4 +1,2 @@ -pub mod available; pub mod login; pub mod logout; -pub mod register; diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index a3785e1..c18c5e9 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -10,10 +10,14 @@ pub mod api; pub async fn routes() -> Router { let router = Router::new() - .route("/register", post(api::relative::register::handler)) + .route("/register", post(api::register::root::handler)) .route( "/register/available/:username", - get(api::relative::available::handler), + get(api::register::available::handler), + ) + .route( + "/register/email/:email", + get(api::register::email::handler), ) .route("/login", post(api::relative::login::handler)) .route("/logout", post(api::relative::logout::handler)) diff --git a/crates/router/src/router/api/mod.rs b/crates/router/src/router/api/mod.rs deleted file mode 100644 index bd8c77a..0000000 --- a/crates/router/src/router/api/mod.rs +++ /dev/null @@ -1,90 +0,0 @@ -pub mod v1; - -use axum::{response::IntoResponse, Json, Router}; -use http::StatusCode; -use serde::{Deserialize, Serialize}; - -use commune::error::HttpStatusCode; - -pub struct Api; - -impl Api { - pub fn routes() -> Router { - Router::new().nest("/api", Router::new().nest("/v1", v1::V1::routes())) - } -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct ApiError { - pub message: String, - pub code: String, - #[serde(skip)] - pub status: StatusCode, -} - -impl ApiError { - pub fn new(message: String, code: String, status: StatusCode) -> Self { - Self { - message, - code, - status, - } - } - - pub fn unauthorized() -> Self { - Self::new( - "You must be authenticated to access this resource".to_string(), - "UNAUTHORIZED".to_string(), - StatusCode::UNAUTHORIZED, - ) - } - - pub fn internal_server_error() -> Self { - Self::new( - "Internal server error".to_string(), - "INTERNAL_SERVER_ERROR".to_string(), - StatusCode::INTERNAL_SERVER_ERROR, - ) - } -} - -impl From for ApiError { - fn from(err: commune::error::Error) -> Self { - Self { - message: err.to_string(), - code: err.error_code().to_string(), - status: err.status_code(), - } - } -} - -/// Any `anyhow::Error` can be converted into an `ApiError`. -/// -/// Caveat is that given that anyhow error is generic (w/o context), the -/// error status is 500. -/// -/// Perhaps in the future, a more specific error type can be used, like with -/// `thiserror`. -impl From for ApiError { - fn from(err: anyhow::Error) -> Self { - Self { - message: err.to_string(), - code: "UNKNOWN_ERROR".to_string(), - status: StatusCode::INTERNAL_SERVER_ERROR, - } - } -} - -impl IntoResponse for ApiError { - fn into_response(self) -> axum::response::Response { - if let Ok(status) = axum::http::StatusCode::from_u16(self.status.as_u16()) { - let mut response = Json(self).into_response(); - - *response.status_mut() = status; - return response; - } - - tracing::error!(status=%self.status, "Failed to convert status code to http::StatusCode"); - ApiError::internal_server_error().into_response() - } -} diff --git a/crates/router/src/router/api/v1/account/email.rs b/crates/router/src/router/api/v1/account/email.rs deleted file mode 100644 index 004d93a..0000000 --- a/crates/router/src/router/api/v1/account/email.rs +++ /dev/null @@ -1,34 +0,0 @@ -use axum::{ - extract::Path, - http::StatusCode, - response::{IntoResponse, Response}, - Extension, Json, -}; -use serde::{Deserialize, Serialize}; -use tracing::instrument; - -use crate::{router::api::ApiError, services::SharedServices}; - -#[instrument(skip(services))] -pub async fn handler( - Extension(services): Extension, - Path(email): Path, -) -> Response { - match services.commune.account.is_email_available(&email).await { - Ok(available) => { - let mut response = Json(AccountEmailExistsResponse { available }).into_response(); - - *response.status_mut() = StatusCode::OK; - response - } - Err(err) => { - tracing::warn!(?err, ?email, "Failed to find email"); - ApiError::from(err).into_response() - } - } -} - -#[derive(Deserialize, Serialize)] -pub struct AccountEmailExistsResponse { - pub available: bool, -} diff --git a/crates/router/src/router/api/v1/account/login.rs b/crates/router/src/router/api/v1/account/login.rs deleted file mode 100644 index 0257bcd..0000000 --- a/crates/router/src/router/api/v1/account/login.rs +++ /dev/null @@ -1,95 +0,0 @@ -use axum::{ - http::StatusCode, - response::{IntoResponse, Response}, - Extension, Json, -}; -use commune::Error; -use serde::{Deserialize, Serialize}; -use tracing::instrument; - -use commune::auth::service::LoginCredentials; - -use crate::{router::api::ApiError, services::SharedServices}; - -use super::root::{AccountMatrixCredentials, AccountSpace}; - -#[instrument(skip(services))] -pub async fn get(Extension(services): Extension) -> Response { - match services.commune.auth.get_login_flows().await { - Ok(flows) => Json(flows).into_response(), - Err(err) => { - tracing::warn!(?err, "Failed to retrieve login flows"); - ApiError::from(err).into_response() - } - } -} - -#[instrument(skip(services, payload))] -pub async fn post( - Extension(services): Extension, - Json(payload): Json, -) -> Response { - let login_credentials = LoginCredentials::from(payload); - - let Ok(tokens) = services.commune.auth.login(login_credentials).await else { - tracing::warn!("Failed to authenticate user"); - return ApiError::from(Error::Auth( - commune::auth::error::AuthErrorCode::InvalidCredentials, - )) - .into_response(); - }; - - match services.commune.account.whoami(&tokens.access_token).await { - Ok(account) => { - let mut response = Json(AccountLoginResponse { - access_token: tokens.access_token.to_string(), - credentials: AccountMatrixCredentials { - username: account.username, - display_name: account.display_name, - avatar_url: account.avatar_url, - access_token: tokens.access_token.to_string(), - matrix_access_token: tokens.access_token.to_string(), - matrix_user_id: account.user_id.to_string(), - matrix_device_id: String::new(), - user_space_id: String::new(), - email: account.email, - age: account.age, - admin: account.admin, - verified: account.verified, - }, - ..Default::default() - }) - .into_response(); - - *response.status_mut() = StatusCode::OK; - response - } - Err(err) => { - tracing::warn!(?err, "Failed to authenticate user"); - ApiError::from(err).into_response() - } - } -} - -#[derive(Deserialize, Serialize)] -pub struct AccountLoginPayload { - pub username: String, - pub password: String, -} - -impl From for LoginCredentials { - fn from(payload: AccountLoginPayload) -> Self { - Self { - username: payload.username, - password: payload.password.into(), - } - } -} - -#[derive(Debug, Clone, Default, Deserialize, Serialize)] -pub struct AccountLoginResponse { - pub access_token: String, - pub credentials: AccountMatrixCredentials, - pub rooms: Vec, - pub spaces: Vec, -} diff --git a/crates/router/src/router/api/v1/account/mod.rs b/crates/router/src/router/api/v1/account/mod.rs deleted file mode 100644 index a1b74f6..0000000 --- a/crates/router/src/router/api/v1/account/mod.rs +++ /dev/null @@ -1,35 +0,0 @@ -pub mod email; -pub mod login; -pub mod root; -pub mod session; -pub mod verify_code; -pub mod verify_code_email; - -use axum::{ - middleware, - routing::{get, post}, - Router, -}; - -use crate::router::middleware::auth; - -pub struct Account; - -impl Account { - pub fn routes() -> Router { - Router::new() - .route("/session", get(session::handler)) - .route_layer(middleware::from_fn(auth)) - .route("/", post(root::handler)) - .route("/login", get(login::get)) - .route("/login", post(login::post)) - .route("/login/sso/redirect", get(login::get)) - .route("/email/:email", get(email::handler)) - .nest( - "/verify", - Router::new() - .route("/code", post(verify_code::handler)) - .route("/code/email", post(verify_code_email::handler)), - ) - } -} diff --git a/crates/router/src/router/api/v1/account/root.rs b/crates/router/src/router/api/v1/account/root.rs deleted file mode 100644 index 6358ded..0000000 --- a/crates/router/src/router/api/v1/account/root.rs +++ /dev/null @@ -1,128 +0,0 @@ -use axum::{ - http::StatusCode, - response::{IntoResponse, Response}, - Extension, Json, -}; - -use serde::{Deserialize, Serialize}; -use tracing::instrument; - -use commune::account::{model::Account, service::CreateAccountDto}; -use url::Url; -use uuid::Uuid; - -use crate::{router::api::ApiError, services::SharedServices}; - -#[instrument(skip(services, payload))] -pub async fn handler( - Extension(services): Extension, - Json(payload): Json, -) -> Response { - let dto = CreateAccountDto::from(payload); - - match services.commune.account.register(dto).await { - Ok(account) => { - let access_token = services - .commune - .account - .issue_user_token(&account.user_id) - .await - .unwrap(); - let payload = AccountRegisterResponse { - access_token: access_token.to_string(), - created: true, - credentials: AccountMatrixCredentials { - username: account.username, - display_name: account.display_name, - avatar_url: account.avatar_url, - access_token: access_token.to_string(), - matrix_access_token: access_token.to_string(), - matrix_user_id: account.user_id.to_string(), - matrix_device_id: "".to_string(), - user_space_id: "".to_string(), - email: account.email, - age: account.age, - admin: account.admin, - verified: account.verified, - }, - ..Default::default() - }; - - let mut response = Json(payload).into_response(); - - *response.status_mut() = StatusCode::CREATED; - response - } - Err(err) => { - tracing::warn!(?err, "Failed to register user"); - ApiError::from(err).into_response() - } - } -} - -#[derive(Deserialize, Serialize)] -pub struct AccountRegisterPayload { - pub username: String, - pub password: String, - pub email: String, - pub session: Uuid, - pub code: String, -} - -impl From for CreateAccountDto { - fn from(payload: AccountRegisterPayload) -> Self { - Self { - username: payload.username, - password: payload.password.into(), - email: payload.email, - session: payload.session, - code: payload.code.into(), - } - } -} - -#[derive(Debug, Clone, Default, Deserialize, Serialize)] -pub struct AccountSpace { - pub room_id: String, - pub alias: String, - pub name: String, - pub topic: Option, - pub avatar: Option, - pub header: Option, - pub is_profile: bool, - pub is_default: bool, - pub is_owner: bool, -} - -#[derive(Debug, Clone, Default, Deserialize, Serialize)] -pub struct AccountMatrixCredentials { - pub username: String, - pub display_name: String, - pub avatar_url: Option, - pub access_token: String, - pub matrix_access_token: String, - pub matrix_user_id: String, - pub matrix_device_id: String, - pub user_space_id: String, - pub email: String, - pub age: i64, - pub admin: bool, - pub verified: bool, -} - -#[derive(Debug, Clone, Default, Deserialize, Serialize)] -pub struct AccountRegisterResponse { - pub access_token: String, - pub created: bool, - pub credentials: AccountMatrixCredentials, - pub rooms: Vec, - pub spaces: Vec, -} - -impl From for AccountRegisterResponse { - fn from(_: Account) -> Self { - Self { - ..Default::default() - } - } -} diff --git a/crates/router/src/router/api/v1/account/session.rs b/crates/router/src/router/api/v1/account/session.rs deleted file mode 100644 index f17c737..0000000 --- a/crates/router/src/router/api/v1/account/session.rs +++ /dev/null @@ -1,48 +0,0 @@ -use axum::{ - response::{IntoResponse, Response}, - Extension, Json, -}; -use serde::{Deserialize, Serialize}; -use tracing::instrument; - -use commune::account::model::Account; - -use crate::router::middleware::AccessToken; - -use super::root::{AccountMatrixCredentials, AccountSpace}; - -#[instrument(skip(account))] -pub async fn handler( - Extension(account): Extension, - Extension(access_token): Extension, -) -> Response { - let response = Json(AccountSessionResponse { - credentials: AccountMatrixCredentials { - username: account.username, - display_name: account.display_name, - avatar_url: account.avatar_url, - access_token: access_token.to_string(), - matrix_access_token: access_token.to_string(), - matrix_user_id: account.user_id.to_string(), - matrix_device_id: String::new(), - user_space_id: String::new(), - email: account.email, - age: account.age, - admin: account.admin, - verified: account.verified, - }, - rooms: vec![], - spaces: vec![], - valid: true, - }); - - response.into_response() -} - -#[derive(Debug, Clone, Default, Deserialize, Serialize)] -pub struct AccountSessionResponse { - pub credentials: AccountMatrixCredentials, - pub rooms: Vec, - pub spaces: Vec, - pub valid: bool, -} diff --git a/crates/router/src/router/api/v1/account/verify_code.rs b/crates/router/src/router/api/v1/account/verify_code.rs deleted file mode 100644 index 0f135e0..0000000 --- a/crates/router/src/router/api/v1/account/verify_code.rs +++ /dev/null @@ -1,74 +0,0 @@ -use axum::{ - http::StatusCode, - response::{IntoResponse, Response}, - Extension, Json, -}; -use commune::{account::error::AccountErrorCode, Error}; -use serde::{Deserialize, Serialize}; -use tracing::instrument; -use uuid::Uuid; - -use commune::account::service::SendCodeDto; - -use crate::{router::api::ApiError, services::SharedServices}; - -#[instrument(skip(services, payload))] -pub async fn handler( - Extension(services): Extension, - Json(payload): Json, -) -> Response { - let dto = SendCodeDto::from(payload); - - match services - .commune - .account - .is_email_available(&dto.email) - .await - { - Ok(available) => { - if !available { - let email_taken_error = AccountErrorCode::EmailTaken(dto.email); - let error = Error::User(email_taken_error); - - return ApiError::from(error).into_response(); - } - } - Err(err) => { - tracing::warn!(?err, ?dto, "Failed to verify email availability"); - return ApiError::from(err).into_response(); - } - } - - match services.commune.account.send_code(dto).await { - Ok(_) => { - let mut response = Json(VerifyCodeResponse { sent: true }).into_response(); - - *response.status_mut() = StatusCode::OK; - response - } - Err(err) => { - tracing::warn!(?err, "Failed to register user"); - ApiError::from(err).into_response() - } - } -} - -#[derive(Deserialize, Serialize)] -pub struct AccountVerifyCodePayload { - pub email: String, - pub session: Uuid, -} - -impl From for SendCodeDto { - fn from(payload: AccountVerifyCodePayload) -> Self { - Self { - email: payload.email, - session: payload.session, - } - } -} - -#[derive(Deserialize, Serialize)] -pub struct VerifyCodeResponse { - pub sent: bool, -} diff --git a/crates/router/src/router/api/v1/account/verify_code_email.rs b/crates/router/src/router/api/v1/account/verify_code_email.rs deleted file mode 100644 index a5b8205..0000000 --- a/crates/router/src/router/api/v1/account/verify_code_email.rs +++ /dev/null @@ -1,76 +0,0 @@ -use axum::{ - http::StatusCode, - response::{IntoResponse, Response}, - Extension, Json, -}; -use commune::{account::error::AccountErrorCode, util::secret::Secret, Error}; -use serde::{Deserialize, Serialize}; -use tracing::instrument; -use uuid::Uuid; - -use commune::account::service::VerifyCodeDto; - -use crate::{router::api::ApiError, services::SharedServices}; - -#[instrument(skip(services, payload))] -pub async fn handler( - Extension(services): Extension, - Json(payload): Json, -) -> Response { - let dto = VerifyCodeDto::from(payload); - - match services - .commune - .account - .is_email_available(&dto.email) - .await - { - Ok(available) => { - if !available { - let email_taken_error = AccountErrorCode::EmailTaken(dto.email); - let error = Error::User(email_taken_error); - - return ApiError::from(error).into_response(); - } - } - Err(err) => { - tracing::warn!(?err, ?dto, "Failed to verify email availability"); - return ApiError::from(err).into_response(); - } - } - - match services.commune.account.verify_code(dto).await { - Ok(valid) => { - let mut response = Json(VerifyCodeEmailResponse { valid }).into_response(); - - *response.status_mut() = StatusCode::OK; - response - } - Err(err) => { - tracing::warn!(?err, "Failed to register user"); - ApiError::from(err).into_response() - } - } -} - -#[derive(Deserialize, Serialize)] -pub struct AccountVerifyCodeEmailPayload { - pub email: String, - pub session: Uuid, - pub code: Secret, -} - -impl From for VerifyCodeDto { - fn from(payload: AccountVerifyCodeEmailPayload) -> Self { - Self { - email: payload.email, - session: payload.session, - code: payload.code, - } - } -} - -#[derive(Deserialize, Serialize)] -pub struct VerifyCodeEmailResponse { - pub valid: bool, -} diff --git a/crates/router/src/router/api/v1/mod.rs b/crates/router/src/router/api/v1/mod.rs deleted file mode 100644 index 360418c..0000000 --- a/crates/router/src/router/api/v1/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub mod account; - -use axum::Router; - -pub struct V1; - -impl V1 { - pub fn routes() -> Router { - Router::new().nest("/account", account::Account::routes()) - } -} From 5ec2088682f3302709e6d2b87b6e00771377f144 Mon Sep 17 00:00:00 2001 From: mikoto Date: Thu, 28 Mar 2024 00:41:41 +0000 Subject: [PATCH 28/33] let us worry about email verification later --- commune-example.toml | 4 ++-- crates/core/src/lib.rs | 2 +- crates/test/src/api/account/email.rs | 25 ++++++++++++++++++++++++ crates/test/src/api/relative/register.rs | 3 ++- docker-compose.yml | 1 + 5 files changed, 31 insertions(+), 4 deletions(-) diff --git a/commune-example.toml b/commune-example.toml index 38f633e..e5da1b2 100644 --- a/commune-example.toml +++ b/commune-example.toml @@ -18,6 +18,6 @@ shared_registration_secret = "m@;wYOUOh0f:CH5XA65sJB1^q01~DmIriOysRImot,OR_vzN&B [mail] host = "smtp://0.0.0.0:1025" -username = "" -password = "" +username = "user" +password = "pass" tls = false diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 2ccd121..b6ea8e7 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -99,7 +99,7 @@ impl Commune { let mut smtp = SmtpClientBuilder::new( host.host_str() .expect("failed to extract host from email configuration"), - 587, + config.mail.host.port().expect("failed to extract port from email configuration"), ) .implicit_tls(false) .credentials((username.as_str(), password.as_str())) diff --git a/crates/test/src/api/account/email.rs b/crates/test/src/api/account/email.rs index e69de29..a085940 100644 --- a/crates/test/src/api/account/email.rs +++ b/crates/test/src/api/account/email.rs @@ -0,0 +1,25 @@ +use crate::{ env::Env}; + +pub async fn verify_email(client: &Env) -> Result { + let resp = client + .get(format!("/_commune/client/r0/register/email/{}", "test@127.0.0.1:1025").as_str()) + .send() + .await?; + + let token = resp.headers().get("registration-token").map(|t| t.to_str()).unwrap().unwrap(); + + tracing::info!(token); + + Ok(resp.status().is_success()) +} + +#[tokio::test] +async fn verify_email_test() { + let client = Env::new().await; + + let resp = verify_email(&client).await.unwrap(); + + tracing::info!(?resp); + + assert_eq!(resp, true); +} diff --git a/crates/test/src/api/relative/register.rs b/crates/test/src/api/relative/register.rs index b300c33..542a2ba 100644 --- a/crates/test/src/api/relative/register.rs +++ b/crates/test/src/api/relative/register.rs @@ -2,7 +2,7 @@ use commune::util::secret::Secret; use rand::seq::IteratorRandom; use matrix::client::register::root::*; -use router::api::relative::register; +use router::api::register::root as register; use crate::env::Env; @@ -22,6 +22,7 @@ pub async fn register(client: &Env) -> Result { .json(®ister::Payload { username, password: Secret::new("verysecure"), + registration_token: None, }) .send() .await?; diff --git a/docker-compose.yml b/docker-compose.yml index 18a4f56..dacf0f8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,7 @@ services: image: marlonb/mailcrab:latest ports: - '1025:1025' + - '1080:1080' networks: [default] redis: From 42bdb268682eab00f1859460550f6d9b9c7a6952 Mon Sep 17 00:00:00 2001 From: mikoto Date: Thu, 28 Mar 2024 02:04:12 +0000 Subject: [PATCH 29/33] email verification working, just need to complete the test --- .gitignore | 7 +-- Justfile | 9 ++-- commune-example.toml | 2 +- commune.toml | 23 +++++++++ crates/core/Cargo.toml | 1 + crates/core/src/account/email.rs | 20 +++----- crates/core/src/lib.rs | 17 +------ .../src/admin/registration_tokens/new.rs | 2 +- crates/matrix/src/client/uiaa.rs | 1 + crates/router/src/api/register/email.rs | 11 +++-- crates/test/src/api/account/email.rs | 48 +++++++++++++++---- crates/test/src/api/relative/register.rs | 11 +---- 12 files changed, 90 insertions(+), 62 deletions(-) create mode 100644 commune.toml diff --git a/.gitignore b/.gitignore index ee4e61b..82c3def 100644 --- a/.gitignore +++ b/.gitignore @@ -16,10 +16,5 @@ Cargo.lock # Development /docker/* !/docker/.gitkeep -!/docker/postgre -.env -access_token.txt +commune.toml dump.sql - -# System Specific -.DS_Store diff --git a/Justfile b/Justfile index a7f5e8b..0ac4d75 100644 --- a/Justfile +++ b/Justfile @@ -7,12 +7,12 @@ target_release := "x86_64-unknown-linux-musl" default: just --list -# Creates the `.env` file if it doesn't exist +# Creates the `commune.toml` file if it doesn't exist # This indicates the first invocation of `just` so we also # create the docker folders while we're at it dotenv: export DOCKER_USER="$(id -u):$(id -g)" && \ - cp -n .env.example .env || true && \ + cp -n commune-example.toml commune.toml || true && \ mkdir -p docker/synapse || true # Dump database to a file @@ -35,7 +35,6 @@ gen_synapse_conf: dotenv docker run -i --rm \ -u "$(id -u):$(id -g)" \ -v ./docker/synapse:/data \ - --env-file .env \ matrixdotorg/synapse:v1.96.1 generate # Generates a de-facto admin user @@ -49,10 +48,10 @@ gen_synapse_admin: dotenv # Retrieves admin access token uses de-facto admin user and Development Database Credentials get_access_token: - sed -i "s/COMMUNE_SYNAPSE_ADMIN_TOKEN='.*'/COMMUNE_SYNAPSE_ADMIN_TOKEN='$( \ + sed -i "s/admin_token:\s'.*'/admin_token:\s'$( \ curl -sS -d '{"type":"m.login.password", "user":"admin", "password":"admin"}' \ http://localhost:8008/_matrix/client/v3/login | jq --raw-output '.access_token' \ - )'/" .env + )'/" commune-example.toml # Runs backend dependency services backend *args='': dotenv diff --git a/commune-example.toml b/commune-example.toml index e5da1b2..ddc43db 100644 --- a/commune-example.toml +++ b/commune-example.toml @@ -13,7 +13,7 @@ blocked_domains = [] [matrix] server_name = "matrix.localhost" host = "http://0.0.0.0:8008" -admin_token = "syt_YWRtaW4_FllbTksPWcQaDRUVVcYR_3LJQZ2" +admin_token = "syt_YWRtaW4_bqFYSIwUqELzKrQeXkMk_1PsWpx" shared_registration_secret = "m@;wYOUOh0f:CH5XA65sJB1^q01~DmIriOysRImot,OR_vzN&B" [mail] diff --git a/commune.toml b/commune.toml new file mode 100644 index 0000000..e5da1b2 --- /dev/null +++ b/commune.toml @@ -0,0 +1,23 @@ +registration_verification = false +public_loopback = false +port = 6421 +tls = true + +# Either one works but not both +blocked_domains = [] +# allowed_domains = ['gmail.com', 'outlook.com'] + +# `X-Forwarded-For` header +# xff = false + +[matrix] +server_name = "matrix.localhost" +host = "http://0.0.0.0:8008" +admin_token = "syt_YWRtaW4_FllbTksPWcQaDRUVVcYR_3LJQZ2" +shared_registration_secret = "m@;wYOUOh0f:CH5XA65sJB1^q01~DmIriOysRImot,OR_vzN&B" + +[mail] +host = "smtp://0.0.0.0:1025" +username = "user" +password = "pass" +tls = false diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index a6961dc..7312c7a 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -27,6 +27,7 @@ url = { workspace = true, features = ["serde"] } tokio = { workspace = true, features = ["full"] } headers = { workspace = true } tokio-rustls = { workspace = true } +time = { workspace = true } # Local Dependencies matrix = { path = "../matrix", features = ["client"] } diff --git a/crates/core/src/account/email.rs b/crates/core/src/account/email.rs index d37ef5e..1930ecc 100644 --- a/crates/core/src/account/email.rs +++ b/crates/core/src/account/email.rs @@ -1,4 +1,4 @@ -use std::time::{SystemTime, UNIX_EPOCH}; +use std::time::UNIX_EPOCH; use email_address::EmailAddress; use matrix::admin::registration_tokens::new::*; @@ -10,23 +10,17 @@ pub async fn service(address: EmailAddress) -> Result { let uni = Uniform::new('0', '9'); let token: String = uni.sample_iter(rand::thread_rng()).take(6).collect(); - let req = Request::new( - token.clone(), - 1, - SystemTime::now() - .duration_since(UNIX_EPOCH) - // panics below should never happen - .expect("system time overflow") - .as_millis() - .try_into() - .expect("system time overflow"), - ); + // TODO: decide on whether to use timezones or UTC + let offset = UNIX_EPOCH.elapsed().unwrap().as_millis() + 60 * 60 * 60; + let req = Request::new(token.clone(), 1, offset.try_into().unwrap()); commune() .send_matrix_request(req, Some(&commune().config.matrix.admin_token.inner())) .await?; - commune().send_email_verification(address, token.clone()).await?; + commune() + .send_email_verification(address, token.clone()) + .await?; Ok(token) } diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index b6ea8e7..84befd2 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -86,14 +86,8 @@ impl Commune { token: impl Into, ) -> mail_send::Result<()> { let config = &commune().config; + tracing::info!(?config.mail); - let password = config.mail.password.inner(); - let username = config - .mail - .username - .as_deref() - .unwrap_or(&password) - .to_owned(); let host = &config.mail.host; let mut smtp = SmtpClientBuilder::new( @@ -101,17 +95,11 @@ impl Commune { .expect("failed to extract host from email configuration"), config.mail.host.port().expect("failed to extract port from email configuration"), ) - .implicit_tls(false) - .credentials((username.as_str(), password.as_str())) - .connect() + .connect_plain() .await?; let token = token.into(); let from = format!("commune@{host}"); - let html = format!( - "

Thanks for signing up.\n\nUse this code to finish verifying your \ - email:\n{token}

" - ); let text = format!( "Thanks for signing up.\n\nUse this code to finish verifying your email:\n{token}" ); @@ -120,7 +108,6 @@ impl Commune { .from(("Commune", from.as_str())) .to(vec![address.as_str()]) .subject("Email Verification Code") - .html_body(html.as_str()) .text_body(text.as_str()); smtp.send(message).await diff --git a/crates/matrix/src/admin/registration_tokens/new.rs b/crates/matrix/src/admin/registration_tokens/new.rs index 7849c07..7f189a7 100644 --- a/crates/matrix/src/admin/registration_tokens/new.rs +++ b/crates/matrix/src/admin/registration_tokens/new.rs @@ -9,7 +9,7 @@ const METADATA: Metadata = metadata! { rate_limited: false, authentication: AccessToken, history: { - unstable => "/_synapse/admin/v1/register/new", + unstable => "/_synapse/admin/v1/registration_tokens/new", } }; diff --git a/crates/matrix/src/client/uiaa.rs b/crates/matrix/src/client/uiaa.rs index 919dac5..829ac57 100644 --- a/crates/matrix/src/client/uiaa.rs +++ b/crates/matrix/src/client/uiaa.rs @@ -147,6 +147,7 @@ impl RegistrationToken { #[derive(Clone, Debug, Serialize)] pub struct Auth { + #[serde(skip_serializing_if = "Option::is_none")] session: Option, kind: AuthType, diff --git a/crates/router/src/api/register/email.rs b/crates/router/src/api/register/email.rs index 7276f13..2d46342 100644 --- a/crates/router/src/api/register/email.rs +++ b/crates/router/src/api/register/email.rs @@ -23,12 +23,17 @@ pub async fn handler(Path(address): Path) -> Response { let token_sha256 = digest::digest(&digest::SHA256, &token.as_bytes()); let token_sha256_b64 = engine.encode(token_sha256); - let cookie = Cookie::build(("registration-token", token_sha256_b64)).max_age(Duration::minutes(60).into()); + let cookie = Cookie::build(("registration-token", token_sha256_b64)) + .max_age(Duration::minutes(60).into()); ( (), - AppendHeaders([(SET_COOKIE, format!("registration_token={}", cookie.to_string()))]), - ).into_response() + AppendHeaders([( + SET_COOKIE, + cookie.to_string(), + )]), + ) + .into_response() } Err(e) => { tracing::warn!(?e, "failed to send verification email"); diff --git a/crates/test/src/api/account/email.rs b/crates/test/src/api/account/email.rs index a085940..1f0ea90 100644 --- a/crates/test/src/api/account/email.rs +++ b/crates/test/src/api/account/email.rs @@ -1,25 +1,55 @@ -use crate::{ env::Env}; +use commune::util::secret::Secret; +use matrix::client::register::root::*; +use router::api::register::root as register; +use url::form_urlencoded::byte_serialize; -pub async fn verify_email(client: &Env) -> Result { +use crate::{env::Env, util::generate_comforming_localpart}; + +pub async fn verify_email(client: &Env) -> Result { let resp = client - .get(format!("/_commune/client/r0/register/email/{}", "test@127.0.0.1:1025").as_str()) + .get( + format!( + "/_commune/client/r0/register/email/{}", + byte_serialize("commune@example.org".as_bytes()).collect::() + ) + .as_str(), + ) .send() .await?; - let token = resp.headers().get("registration-token").map(|t| t.to_str()).unwrap().unwrap(); + tracing::info!(resp = ?resp.headers()); + + let cookie = resp.headers().get("set-cookie").unwrap(); + + // TODO: make this type-safe + let s = cookie.to_str().unwrap(); + let query = s.split(';').next().unwrap(); + let token_hash = query.split('=').nth(1).unwrap(); + + tracing::info!(token_hash); + + let username = generate_comforming_localpart(); - tracing::info!(token); + tracing::info!(?username); - Ok(resp.status().is_success()) + let resp = client + .post("/_commune/client/r0/register") + .json(®ister::Payload { + username, + password: Secret::new("verysecure"), + registration_token: None, + }) + .send() + .await?; + + resp.json().await } -#[tokio::test] +// #[tokio::test] async fn verify_email_test() { let client = Env::new().await; let resp = verify_email(&client).await.unwrap(); tracing::info!(?resp); - - assert_eq!(resp, true); } diff --git a/crates/test/src/api/relative/register.rs b/crates/test/src/api/relative/register.rs index 542a2ba..303fe0a 100644 --- a/crates/test/src/api/relative/register.rs +++ b/crates/test/src/api/relative/register.rs @@ -1,19 +1,12 @@ use commune::util::secret::Secret; -use rand::seq::IteratorRandom; use matrix::client::register::root::*; use router::api::register::root as register; -use crate::env::Env; +use crate::{env::Env, util::generate_comforming_localpart}; pub async fn register(client: &Env) -> Result { - let allowed = ('0'..='9') - .chain('a'..='z') - .chain(['-', '.', '=', '_', '/', '+']); - let username = allowed - .choose_multiple(&mut rand::thread_rng(), 8) - .into_iter() - .collect(); + let username = generate_comforming_localpart(); tracing::info!(?username); From 9649cbb8c51582f06452a392d5575438ab7b7ba9 Mon Sep 17 00:00:00 2001 From: mikoto Date: Thu, 28 Mar 2024 17:33:24 +0000 Subject: [PATCH 30/33] wip: spaces --- crates/core/src/lib.rs | 8 +++++-- crates/router/src/api/register/email.rs | 9 +------- crates/router/src/lib.rs | 22 ++++++++++++------- crates/test/Cargo.toml | 1 + crates/test/src/api/account/email.rs | 29 +++++++++++++++++++++++-- docker-compose.yml | 8 +++---- 6 files changed, 53 insertions(+), 24 deletions(-) diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 84befd2..257d4f6 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -93,9 +93,13 @@ impl Commune { let mut smtp = SmtpClientBuilder::new( host.host_str() .expect("failed to extract host from email configuration"), - config.mail.host.port().expect("failed to extract port from email configuration"), + config + .mail + .host + .port() + .expect("failed to extract port from email configuration"), ) - .connect_plain() + .connect_plain() .await?; let token = token.into(); diff --git a/crates/router/src/api/register/email.rs b/crates/router/src/api/register/email.rs index 2d46342..fddfb0b 100644 --- a/crates/router/src/api/register/email.rs +++ b/crates/router/src/api/register/email.rs @@ -26,14 +26,7 @@ pub async fn handler(Path(address): Path) -> Response { let cookie = Cookie::build(("registration-token", token_sha256_b64)) .max_age(Duration::minutes(60).into()); - ( - (), - AppendHeaders([( - SET_COOKIE, - cookie.to_string(), - )]), - ) - .into_response() + ((), AppendHeaders([(SET_COOKIE, cookie.to_string())])).into_response() } Err(e) => { tracing::warn!(?e, "failed to send verification email"); diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index c18c5e9..67102f5 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -10,14 +10,15 @@ pub mod api; pub async fn routes() -> Router { let router = Router::new() - .route("/register", post(api::register::root::handler)) - .route( - "/register/available/:username", - get(api::register::available::handler), - ) - .route( - "/register/email/:email", - get(api::register::email::handler), + .nest( + "/register", + Router::new() + .route("/", post(api::register::root::handler)) + .route( + "/available/:username", + get(api::register::available::handler), + ) + .route("/email/:email", get(api::register::email::handler)), ) .route("/login", post(api::relative::login::handler)) .route("/logout", post(api::relative::logout::handler)) @@ -28,6 +29,11 @@ pub async fn routes() -> Router { .route("/password", put(api::account::password::handler)) .route("/display_name", put(api::account::display_name::handler)) .route("/avatar", put(api::account::avatar::handler)), + ) + .nest( + "/space", + Router::new() + .route("/space", post(api::spaces::create::handler)) ); Router::new().nest("/_commune/client/r0", router) diff --git a/crates/test/Cargo.toml b/crates/test/Cargo.toml index 382d607..a9caff2 100644 --- a/crates/test/Cargo.toml +++ b/crates/test/Cargo.toml @@ -20,6 +20,7 @@ url = { workspace = true } rand = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } +base64 = { workspace = true } # Local Dependencies core = { path = "../core" } diff --git a/crates/test/src/api/account/email.rs b/crates/test/src/api/account/email.rs index 1f0ea90..ee30fb3 100644 --- a/crates/test/src/api/account/email.rs +++ b/crates/test/src/api/account/email.rs @@ -1,6 +1,7 @@ use commune::util::secret::Secret; use matrix::client::register::root::*; use router::api::register::root as register; +use serde::Deserialize; use url::form_urlencoded::byte_serialize; use crate::{env::Env, util::generate_comforming_localpart}; @@ -28,6 +29,20 @@ pub async fn verify_email(client: &Env) -> Result { tracing::info!(token_hash); + let resp = reqwest::get("http://localhost:1080/api/messages").await?; + let emails: Vec = resp.json().await?; + + let EmailId { id } = emails.last().unwrap(); + let resp = reqwest::get(format!("http://localhost:1080/api/message/{id}")).await?; + + let EmailBody { text } = resp.json().await?; + let code: String = text.chars().filter(|c| c.is_digit(10)).collect(); + + let engine = GeneralPurpose::new(&alphabet::URL_SAFE, general_purpose::NO_PAD); + + let token_sha256 = digest::digest(&digest::SHA256, &token.as_bytes()); + let token_sha256_b64 = engine.encode(token_sha256); + let username = generate_comforming_localpart(); tracing::info!(?username); @@ -37,7 +52,7 @@ pub async fn verify_email(client: &Env) -> Result { .json(®ister::Payload { username, password: Secret::new("verysecure"), - registration_token: None, + registration_token: Some(code), }) .send() .await?; @@ -45,7 +60,17 @@ pub async fn verify_email(client: &Env) -> Result { resp.json().await } -// #[tokio::test] +#[derive(Deserialize)] +struct EmailId { + pub id: String, +} + +#[derive(Deserialize)] +struct EmailBody { + pub text: String, +} + +#[tokio::test] async fn verify_email_test() { let client = Env::new().await; diff --git a/docker-compose.yml b/docker-compose.yml index dacf0f8..91c52e9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,8 +22,8 @@ services: - '5432:5432' volumes: - synapse-db:/var/lib/postgresql/data - env_file: - - .env + # env_file: + # - .env restart: always synapse: @@ -34,8 +34,8 @@ services: - '8448:8448' volumes: - ./docker/synapse:/data - env_file: - - .env + # env_file: + # - .env restart: always network_mode: 'host' depends_on: From 17390a2ea9e414c857c8a1410c4e94fd0c880467 Mon Sep 17 00:00:00 2001 From: mikoto Date: Fri, 29 Mar 2024 01:55:25 +0000 Subject: [PATCH 31/33] feat: implemented space creation --- crates/core/src/lib.rs | 1 + crates/core/src/spaces.rs | 1 + crates/core/src/spaces/create.rs | 41 +++++++++ crates/matrix/src/client.rs | 6 +- crates/matrix/src/client/create_room.rs | 106 ++++++++++++++++++++++++ crates/router/src/api.rs | 1 + crates/router/src/api/rooms/create.rs | 0 crates/router/src/api/spaces.rs | 1 + crates/router/src/api/spaces/create.rs | 0 crates/router/src/api/spaces/root.rs | 39 +++++++++ crates/router/src/lib.rs | 4 +- crates/test/src/api.rs | 1 + crates/test/src/api/account/email.rs | 6 +- crates/test/src/api/spaces.rs | 1 + crates/test/src/api/spaces/create.rs | 37 +++++++++ 15 files changed, 238 insertions(+), 7 deletions(-) create mode 100644 crates/core/src/spaces.rs create mode 100644 crates/core/src/spaces/create.rs create mode 100644 crates/matrix/src/client/create_room.rs create mode 100644 crates/router/src/api/rooms/create.rs create mode 100644 crates/router/src/api/spaces.rs create mode 100644 crates/router/src/api/spaces/create.rs create mode 100644 crates/router/src/api/spaces/root.rs create mode 100644 crates/test/src/api/spaces.rs create mode 100644 crates/test/src/api/spaces/create.rs diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 257d4f6..2d8e5a2 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -7,6 +7,7 @@ pub mod util; pub mod account; pub mod profile; +pub mod spaces; use std::sync::RwLock; diff --git a/crates/core/src/spaces.rs b/crates/core/src/spaces.rs new file mode 100644 index 0000000..c5fb369 --- /dev/null +++ b/crates/core/src/spaces.rs @@ -0,0 +1 @@ +pub mod create; diff --git a/crates/core/src/spaces/create.rs b/crates/core/src/spaces/create.rs new file mode 100644 index 0000000..e8186fb --- /dev/null +++ b/crates/core/src/spaces/create.rs @@ -0,0 +1,41 @@ +use matrix::{ + client::create_room::{Request, Response, RoomCreationContent, RoomVisibility}, + ruma_common::{room::RoomType, OwnedRoomAliasId, RoomVersionId}, + ruma_events::room::power_levels::RoomPowerLevelsEventContent, +}; + +use crate::{commune, error::Result}; + +pub async fn service( + access_token: impl AsRef, + alias: Option, + name: Option, + topic: Option, +) -> Result { + let mut power_levels = RoomPowerLevelsEventContent::new(); + power_levels.events_default = 100.into(); + + let creation_content = Some(RoomCreationContent { + kind: RoomType::Space, + federate: true, + room_version: RoomVersionId::V11, + predecessor: None, + }); + + let req = Request::new( + creation_content, + Vec::new(), + Vec::new(), + false, + name, + Some(power_levels), + alias, + topic, + RoomVisibility::Public, + ); + + commune() + .send_matrix_request(req, Some(access_token.as_ref())) + .await + .map_err(Into::into) +} diff --git a/crates/matrix/src/client.rs b/crates/matrix/src/client.rs index b9f8de2..373e60b 100644 --- a/crates/matrix/src/client.rs +++ b/crates/matrix/src/client.rs @@ -2,9 +2,11 @@ //! //! reference: https://spec.matrix.org/unstable/client-server-api -pub mod account; pub mod login; pub mod logout; -pub mod profile; pub mod register; +pub mod create_room; + +pub mod account; +pub mod profile; pub mod uiaa; diff --git a/crates/matrix/src/client/create_room.rs b/crates/matrix/src/client/create_room.rs new file mode 100644 index 0000000..1013072 --- /dev/null +++ b/crates/matrix/src/client/create_room.rs @@ -0,0 +1,106 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, + room::RoomType, + serde::Raw, + OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomVersionId, +}; +use ruma_events::{ + room::{create::PreviousRoom, power_levels::RoomPowerLevelsEventContent}, + AnyInitialStateEvent, +}; +use serde::{Serialize, Deserialize}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/v3/createRoom", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[serde(skip_serializing_if = "Option::is_none")] + pub creation_content: Option, + + #[serde(skip_serializing_if = "<[_]>::is_empty")] + pub initial_state: Vec>, + + #[serde(skip_serializing_if = "<[_]>::is_empty")] + pub invite: Vec, + + pub is_direct: bool, + + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub power_override: Option, + + #[serde(rename = "room_alias_name", skip_serializing_if = "Option::is_none")] + pub alias: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub topic: Option, + + pub visibility: RoomVisibility, +} + +#[derive(Clone, Debug, Serialize)] +pub struct RoomCreationContent { + #[serde(rename = "m.federate")] + pub federate: bool, + + #[serde(rename = "type")] + pub room_version: RoomVersionId, + + #[serde(skip_serializing_if = "Option::is_none")] + pub predecessor: Option, + + #[serde(rename = "type")] + pub kind: RoomType, +} + +#[derive(Clone, Default, Debug, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum RoomVisibility { + Public, + + #[default] + Private, +} + +impl Request { + pub fn new( + creation_content: Option, + initial_state: Vec>, + invite: Vec, + is_direct: bool, + name: Option, + power_override: Option, + alias: Option, + topic: Option, + visibility: RoomVisibility, + ) -> Self { + Self { + creation_content, + initial_state, + invite, + is_direct, + name, + power_override, + alias, + topic, + visibility, + } + } +} + +#[response(error = crate::Error)] +#[derive(Deserialize, Serialize)] +pub struct Response { + pub room_id: OwnedRoomId, +} diff --git a/crates/router/src/api.rs b/crates/router/src/api.rs index d8d9b3a..9f15be2 100644 --- a/crates/router/src/api.rs +++ b/crates/router/src/api.rs @@ -5,3 +5,4 @@ pub mod account; pub mod relative; pub mod register; +pub mod spaces; diff --git a/crates/router/src/api/rooms/create.rs b/crates/router/src/api/rooms/create.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/router/src/api/spaces.rs b/crates/router/src/api/spaces.rs new file mode 100644 index 0000000..dec16f3 --- /dev/null +++ b/crates/router/src/api/spaces.rs @@ -0,0 +1 @@ +pub mod root; diff --git a/crates/router/src/api/spaces/create.rs b/crates/router/src/api/spaces/create.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/router/src/api/spaces/root.rs b/crates/router/src/api/spaces/root.rs new file mode 100644 index 0000000..da12423 --- /dev/null +++ b/crates/router/src/api/spaces/root.rs @@ -0,0 +1,39 @@ +use axum::{ + response::{IntoResponse, Response}, + Json, +}; +use axum_extra::{ + headers::{authorization::Bearer, Authorization}, + TypedHeader, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Payload { + pub alias: Option, + pub name: Option, + pub topic: Option, +} + +pub async fn handler( + TypedHeader(access_token): TypedHeader>, + Json(payload): Json, +) -> Response { + use commune::spaces::create::service; + + match service( + access_token.token(), + payload.alias, + payload.name, + payload.topic, + ) + .await + { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to create space"); + + e.into_response() + } + } +} diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 67102f5..6200b12 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -31,9 +31,9 @@ pub async fn routes() -> Router { .route("/avatar", put(api::account::avatar::handler)), ) .nest( - "/space", + "/spaces", Router::new() - .route("/space", post(api::spaces::create::handler)) + .route("/", post(api::spaces::root::handler)) ); Router::new().nest("/_commune/client/r0", router) diff --git a/crates/test/src/api.rs b/crates/test/src/api.rs index 8d9dd19..b8bbac1 100644 --- a/crates/test/src/api.rs +++ b/crates/test/src/api.rs @@ -4,3 +4,4 @@ pub mod account; pub mod relative; +pub mod spaces; diff --git a/crates/test/src/api/account/email.rs b/crates/test/src/api/account/email.rs index ee30fb3..32e48d8 100644 --- a/crates/test/src/api/account/email.rs +++ b/crates/test/src/api/account/email.rs @@ -38,10 +38,10 @@ pub async fn verify_email(client: &Env) -> Result { let EmailBody { text } = resp.json().await?; let code: String = text.chars().filter(|c| c.is_digit(10)).collect(); - let engine = GeneralPurpose::new(&alphabet::URL_SAFE, general_purpose::NO_PAD); + // let engine = GeneralPurpose::new(&alphabet::URL_SAFE, general_purpose::NO_PAD); - let token_sha256 = digest::digest(&digest::SHA256, &token.as_bytes()); - let token_sha256_b64 = engine.encode(token_sha256); + // let token_sha256 = digest::digest(&digest::SHA256, &token.as_bytes()); + // let token_sha256_b64 = engine.encode(token_sha256); let username = generate_comforming_localpart(); diff --git a/crates/test/src/api/spaces.rs b/crates/test/src/api/spaces.rs new file mode 100644 index 0000000..c5fb369 --- /dev/null +++ b/crates/test/src/api/spaces.rs @@ -0,0 +1 @@ +pub mod create; diff --git a/crates/test/src/api/spaces/create.rs b/crates/test/src/api/spaces/create.rs new file mode 100644 index 0000000..c77371b --- /dev/null +++ b/crates/test/src/api/spaces/create.rs @@ -0,0 +1,37 @@ +use matrix::client::create_room::*; +use router::api::spaces::root::Payload; + +use crate::{api::relative::login, env::Env}; + +pub async fn create_space(client: &Env) -> Result { + let login_resp = login::login(&client).await.unwrap(); + + tracing::info!(?login_resp); + + let resp = client + .post("/_commune/client/r0/spaces") + .json(&Payload { + alias: Some("alias".to_owned()), + name: Some("name".to_owned()), + topic: Some("topic".to_owned()), + }) + .header( + reqwest::header::AUTHORIZATION, + format!("Bearer {}", &login_resp.access_token), + ) + .send() + .await?; + + tracing::info!(?resp); + + resp.json().await +} + +#[tokio::test] +async fn create_space_test() { + let client = Env::new().await; + + let resp = create_space(&client).await.unwrap(); + + tracing::info!(?resp); +} From 62378934bdf90a0b350d6b89c0f28d0e2088e6b9 Mon Sep 17 00:00:00 2001 From: mikoto Date: Fri, 29 Mar 2024 02:37:38 +0000 Subject: [PATCH 32/33] feate: implemented space child creation --- crates/core/src/lib.rs | 1 + crates/core/src/rooms.rs | 1 + crates/core/src/rooms/create.rs | 52 +++++++++++++++++++++++++ crates/core/src/spaces/create.rs | 4 +- crates/matrix/src/client.rs | 1 + crates/matrix/src/client/create_room.rs | 2 +- crates/matrix/src/client/rooms.rs | 1 + crates/matrix/src/client/rooms/state.rs | 27 +++++++++++++ crates/router/src/api.rs | 1 + crates/router/src/api/rooms.rs | 1 + crates/router/src/api/rooms/create.rs | 0 crates/router/src/api/rooms/root.rs | 41 +++++++++++++++++++ crates/router/src/api/spaces/create.rs | 0 13 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 crates/core/src/rooms.rs create mode 100644 crates/core/src/rooms/create.rs create mode 100644 crates/matrix/src/client/rooms.rs create mode 100644 crates/matrix/src/client/rooms/state.rs create mode 100644 crates/router/src/api/rooms.rs delete mode 100644 crates/router/src/api/rooms/create.rs create mode 100644 crates/router/src/api/rooms/root.rs delete mode 100644 crates/router/src/api/spaces/create.rs diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 2d8e5a2..4a758cf 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -8,6 +8,7 @@ pub mod util; pub mod account; pub mod profile; pub mod spaces; +pub mod rooms; use std::sync::RwLock; diff --git a/crates/core/src/rooms.rs b/crates/core/src/rooms.rs new file mode 100644 index 0000000..c5fb369 --- /dev/null +++ b/crates/core/src/rooms.rs @@ -0,0 +1 @@ +pub mod create; diff --git a/crates/core/src/rooms/create.rs b/crates/core/src/rooms/create.rs new file mode 100644 index 0000000..81ede67 --- /dev/null +++ b/crates/core/src/rooms/create.rs @@ -0,0 +1,52 @@ +use matrix::{ + client::create_room::{Request, Response, RoomCreationContent, RoomVisibility}, + ruma_common::{room::RoomType, OwnedRoomOrAliasId, RoomVersionId}, + ruma_events::{ + room::power_levels::RoomPowerLevelsEventContent, + space::parent::{InitialSpaceParentEvent, SpaceParentEventContent}, + }, +}; + +use crate::{commune, error::Result}; + +pub async fn service( + access_token: impl AsRef, + parent: OwnedRoomOrAliasId, + alias: Option, + name: Option, + topic: Option, +) -> Result { + let creation_content = Some(RoomCreationContent { + kind: None, + federate: true, + room_version: RoomVersionId::V11, + predecessor: None, + }); + + let req = Request::new( + creation_content, + Vec::new(), + Vec::new(), + false, + name, + None, + alias, + topic, + RoomVisibility::Public, + ); + + let resp = commune() + .send_matrix_request(req, Some(access_token.as_ref())) + .await?; + + let mut content = + SpaceParentEventContent::new(vec![commune().config.matrix.server_name.clone()]); + content.canonical = true; + + let state_event = InitialSpaceParentEvent { + content, + state_key: resp.room_id.clone(), + }; + + Ok(resp) +} diff --git a/crates/core/src/spaces/create.rs b/crates/core/src/spaces/create.rs index e8186fb..6a1c6a7 100644 --- a/crates/core/src/spaces/create.rs +++ b/crates/core/src/spaces/create.rs @@ -1,6 +1,6 @@ use matrix::{ client::create_room::{Request, Response, RoomCreationContent, RoomVisibility}, - ruma_common::{room::RoomType, OwnedRoomAliasId, RoomVersionId}, + ruma_common::{room::RoomType, RoomVersionId}, ruma_events::room::power_levels::RoomPowerLevelsEventContent, }; @@ -16,7 +16,7 @@ pub async fn service( power_levels.events_default = 100.into(); let creation_content = Some(RoomCreationContent { - kind: RoomType::Space, + kind: Some(RoomType::Space), federate: true, room_version: RoomVersionId::V11, predecessor: None, diff --git a/crates/matrix/src/client.rs b/crates/matrix/src/client.rs index 373e60b..3e6f5d1 100644 --- a/crates/matrix/src/client.rs +++ b/crates/matrix/src/client.rs @@ -6,6 +6,7 @@ pub mod login; pub mod logout; pub mod register; pub mod create_room; +pub mod rooms; pub mod account; pub mod profile; diff --git a/crates/matrix/src/client/create_room.rs b/crates/matrix/src/client/create_room.rs index 1013072..1683742 100644 --- a/crates/matrix/src/client/create_room.rs +++ b/crates/matrix/src/client/create_room.rs @@ -61,7 +61,7 @@ pub struct RoomCreationContent { pub predecessor: Option, #[serde(rename = "type")] - pub kind: RoomType, + pub kind: Option, } #[derive(Clone, Default, Debug, Serialize)] diff --git a/crates/matrix/src/client/rooms.rs b/crates/matrix/src/client/rooms.rs new file mode 100644 index 0000000..266c62a --- /dev/null +++ b/crates/matrix/src/client/rooms.rs @@ -0,0 +1 @@ +pub mod state; diff --git a/crates/matrix/src/client/rooms/state.rs b/crates/matrix/src/client/rooms/state.rs new file mode 100644 index 0000000..f715df0 --- /dev/null +++ b/crates/matrix/src/client/rooms/state.rs @@ -0,0 +1,27 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/v3/logout", + } +}; + +#[request(error = crate::Error)] +pub struct Request {} + +#[allow(clippy::new_without_default)] +impl Request { + pub fn new() -> Self { + Self {} + } +} + +#[response(error = crate::Error)] +pub struct Response {} diff --git a/crates/router/src/api.rs b/crates/router/src/api.rs index 9f15be2..4117da4 100644 --- a/crates/router/src/api.rs +++ b/crates/router/src/api.rs @@ -6,3 +6,4 @@ pub mod account; pub mod relative; pub mod register; pub mod spaces; +pub mod rooms; diff --git a/crates/router/src/api/rooms.rs b/crates/router/src/api/rooms.rs new file mode 100644 index 0000000..dec16f3 --- /dev/null +++ b/crates/router/src/api/rooms.rs @@ -0,0 +1 @@ +pub mod root; diff --git a/crates/router/src/api/rooms/create.rs b/crates/router/src/api/rooms/create.rs deleted file mode 100644 index e69de29..0000000 diff --git a/crates/router/src/api/rooms/root.rs b/crates/router/src/api/rooms/root.rs new file mode 100644 index 0000000..d2d2388 --- /dev/null +++ b/crates/router/src/api/rooms/root.rs @@ -0,0 +1,41 @@ +use axum::{ + response::{IntoResponse, Response}, + Json, +}; +use axum_extra::{ + headers::{authorization::Bearer, Authorization}, + TypedHeader, +}; +use matrix::ruma_common::OwnedRoomOrAliasId; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Payload { + pub alias: Option, + pub name: Option, + pub topic: Option, + pub parent: OwnedRoomOrAliasId, +} + +pub async fn handler( + TypedHeader(access_token): TypedHeader>, + Json(payload): Json, +) -> Response { + use commune::rooms::create::service; + + match service( + access_token.token(), + payload.alias, + payload.name, + payload.topic, + ) + .await + { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to create space"); + + e.into_response() + } + } +} diff --git a/crates/router/src/api/spaces/create.rs b/crates/router/src/api/spaces/create.rs deleted file mode 100644 index e69de29..0000000 From 12bd98cb6be18954f857960a4b3e82f2307a1916 Mon Sep 17 00:00:00 2001 From: mikoto Date: Fri, 29 Mar 2024 02:51:42 +0000 Subject: [PATCH 33/33] wip: space support --- crates/core/src/direct.rs | 4 + crates/core/src/direct/create.rs | 37 +++++++++ crates/core/src/error.rs | 3 + crates/core/src/helpers.rs | 22 ++++++ crates/core/src/lib.rs | 4 +- crates/core/src/membership.rs | 2 + crates/core/src/membership/join.rs | 19 +++++ crates/core/src/membership/leave.rs | 38 ++++++++++ crates/core/src/rooms/create.rs | 52 ------------- crates/core/src/spaces.rs | 1 + crates/core/src/spaces/channels.rs | 1 + crates/core/src/spaces/channels/create.rs | 76 +++++++++++++++++++ crates/core/src/spaces/create.rs | 1 + crates/core/src/util.rs | 1 + crates/core/src/util/opaque_id.rs | 34 +++++++++ crates/matrix/src/admin.rs | 2 +- crates/matrix/src/admin/{room.rs => rooms.rs} | 11 +-- .../{room/delete_room.rs => rooms/delete.rs} | 0 .../forward_extremities/delete.rs | 0 .../forward_extremities/get.rs | 0 .../admin/{room/get_room.rs => rooms/get.rs} | 6 ++ .../{room/get_rooms.rs => rooms/get_many.rs} | 0 crates/matrix/src/admin/rooms/members.rs | 1 + .../get_members.rs => rooms/members/get.rs} | 0 crates/matrix/src/admin/rooms/state.rs | 1 + .../{room/get_state.rs => rooms/state/get.rs} | 0 crates/matrix/src/client.rs | 7 +- crates/matrix/src/client/create_room.rs | 2 +- crates/matrix/src/client/directory.rs | 1 + crates/matrix/src/client/directory/room.rs | 32 ++++++++ crates/matrix/src/client/membership.rs | 2 + crates/matrix/src/client/membership/join.rs | 44 +++++++++++ crates/matrix/src/client/membership/leave.rs | 34 +++++++++ crates/matrix/src/client/rooms/messages.rs | 0 crates/matrix/src/client/rooms/state.rs | 30 +------- .../matrix/src/client/rooms/state/create.rs | 56 ++++++++++++++ crates/matrix/src/client/rooms/state/get.rs | 53 +++++++++++++ .../matrix/src/client/rooms/state/get_many.rs | 38 ++++++++++ crates/router/src/api.rs | 5 +- crates/router/src/api/direct.rs | 1 + crates/router/src/api/direct/create.rs | 40 ++++++++++ crates/router/src/api/membership.rs | 2 + crates/router/src/api/membership/join.rs | 26 +++++++ crates/router/src/api/membership/leave.rs | 0 crates/router/src/api/rooms.rs | 2 +- .../src/api/rooms/{root.rs => create.rs} | 9 ++- crates/router/src/api/spaces.rs | 3 +- crates/router/src/api/spaces/channels.rs | 1 + .../router/src/api/spaces/channels/create.rs | 34 +++++++++ .../src/api/spaces/{root.rs => create.rs} | 0 crates/router/src/lib.rs | 11 ++- crates/test/src/api.rs | 1 + crates/test/src/api/rooms.rs | 0 crates/test/src/api/rooms/create.rs | 1 + crates/test/src/api/rooms/create/direct.rs | 35 +++++++++ crates/test/src/api/rooms/direct.rs | 0 crates/test/src/api/spaces/create.rs | 2 +- 57 files changed, 687 insertions(+), 101 deletions(-) create mode 100644 crates/core/src/direct.rs create mode 100644 crates/core/src/direct/create.rs create mode 100644 crates/core/src/helpers.rs create mode 100644 crates/core/src/membership.rs create mode 100644 crates/core/src/membership/join.rs create mode 100644 crates/core/src/membership/leave.rs delete mode 100644 crates/core/src/rooms/create.rs create mode 100644 crates/core/src/spaces/channels.rs create mode 100644 crates/core/src/spaces/channels/create.rs create mode 100644 crates/core/src/util/opaque_id.rs rename crates/matrix/src/admin/{room.rs => rooms.rs} (92%) rename crates/matrix/src/admin/{room/delete_room.rs => rooms/delete.rs} (100%) rename crates/matrix/src/admin/{room => rooms}/forward_extremities/delete.rs (100%) rename crates/matrix/src/admin/{room => rooms}/forward_extremities/get.rs (100%) rename crates/matrix/src/admin/{room/get_room.rs => rooms/get.rs} (84%) rename crates/matrix/src/admin/{room/get_rooms.rs => rooms/get_many.rs} (100%) create mode 100644 crates/matrix/src/admin/rooms/members.rs rename crates/matrix/src/admin/{room/get_members.rs => rooms/members/get.rs} (100%) create mode 100644 crates/matrix/src/admin/rooms/state.rs rename crates/matrix/src/admin/{room/get_state.rs => rooms/state/get.rs} (100%) create mode 100644 crates/matrix/src/client/directory.rs create mode 100644 crates/matrix/src/client/directory/room.rs create mode 100644 crates/matrix/src/client/membership.rs create mode 100644 crates/matrix/src/client/membership/join.rs create mode 100644 crates/matrix/src/client/membership/leave.rs create mode 100644 crates/matrix/src/client/rooms/messages.rs create mode 100644 crates/matrix/src/client/rooms/state/create.rs create mode 100644 crates/matrix/src/client/rooms/state/get.rs create mode 100644 crates/matrix/src/client/rooms/state/get_many.rs create mode 100644 crates/router/src/api/direct.rs create mode 100644 crates/router/src/api/direct/create.rs create mode 100644 crates/router/src/api/membership.rs create mode 100644 crates/router/src/api/membership/join.rs create mode 100644 crates/router/src/api/membership/leave.rs rename crates/router/src/api/rooms/{root.rs => create.rs} (78%) create mode 100644 crates/router/src/api/spaces/channels.rs create mode 100644 crates/router/src/api/spaces/channels/create.rs rename crates/router/src/api/spaces/{root.rs => create.rs} (100%) create mode 100644 crates/test/src/api/rooms.rs create mode 100644 crates/test/src/api/rooms/create.rs create mode 100644 crates/test/src/api/rooms/create/direct.rs create mode 100644 crates/test/src/api/rooms/direct.rs diff --git a/crates/core/src/direct.rs b/crates/core/src/direct.rs new file mode 100644 index 0000000..a7d59d5 --- /dev/null +++ b/crates/core/src/direct.rs @@ -0,0 +1,4 @@ +//! This library deals with personal messages, enabling `m.direct` on all rooms created through +//! this endpoint. The name `direct` was chosen to avoid confusion with regular Matrix rooms. + +pub mod create; diff --git a/crates/core/src/direct/create.rs b/crates/core/src/direct/create.rs new file mode 100644 index 0000000..9c1d35e --- /dev/null +++ b/crates/core/src/direct/create.rs @@ -0,0 +1,37 @@ +use matrix::{ + client::create_room::{Request, Response, RoomCreationContent, RoomVisibility}, + ruma_common::{OwnedUserId, RoomVersionId}, +}; + +use crate::{commune, error::Result}; + +pub async fn service( + access_token: impl AsRef, + name: Option, + topic: Option, + invite: Vec, +) -> Result { + let creation_content = Some(RoomCreationContent { + kind: None, + federate: true, + room_version: RoomVersionId::V11, + predecessor: None, + }); + + let req = Request::new( + creation_content, + Vec::new(), + invite, + true, + name, + None, + None, + topic, + RoomVisibility::Private, + ); + + commune() + .send_matrix_request(req, Some(access_token.as_ref())) + .await + .map_err(Into::into) +} diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index 7134727..70e4f65 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -9,6 +9,9 @@ pub enum Error { #[error("forwarding a Matrix request failed: {0}")] Matrix(#[from] matrix::HandleError), + #[error("(de)serializing type failed: {0}")] + Serde(#[from] serde_json::Error), + #[error("instance does not allow email address originating from this domain")] EmailDomain, diff --git a/crates/core/src/helpers.rs b/crates/core/src/helpers.rs new file mode 100644 index 0000000..6641419 --- /dev/null +++ b/crates/core/src/helpers.rs @@ -0,0 +1,22 @@ +use matrix::{ + client::directory::room::Request, + ruma_common::{OwnedRoomId, OwnedRoomOrAliasId}, +}; + +use crate::{commune, error::Error}; + +pub async fn get_room_id( + room_or_alias_id: impl Into, +) -> Result { + // this cannot error, `Result` is just provided in place of an enum + // https://github.com/ruma/ruma/issues/1761 + + match room_or_alias_id.into().try_into() { + Ok(room_id) => Ok(room_id), + Err(room_alias) => commune() + .send_matrix_request(Request::new(room_alias), None) + .await + .map(|resp| resp.room_id) + .map_err(Into::into), + } +} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 4a758cf..68847e3 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -3,12 +3,14 @@ pub mod config; pub mod error; +pub mod helpers; pub mod util; pub mod account; +pub mod direct; pub mod profile; pub mod spaces; -pub mod rooms; +pub mod membership; use std::sync::RwLock; diff --git a/crates/core/src/membership.rs b/crates/core/src/membership.rs new file mode 100644 index 0000000..2c0fcca --- /dev/null +++ b/crates/core/src/membership.rs @@ -0,0 +1,2 @@ +pub mod join; +pub mod leave; diff --git a/crates/core/src/membership/join.rs b/crates/core/src/membership/join.rs new file mode 100644 index 0000000..05ae2c0 --- /dev/null +++ b/crates/core/src/membership/join.rs @@ -0,0 +1,19 @@ +use matrix::{ + client::membership::join::{Request, Response}, + ruma_common::OwnedRoomOrAliasId, +}; + +use crate::{commune, error::Result}; + +pub async fn service( + access_token: impl AsRef, + room_id: impl Into, + reason: Option, +) -> Result { + let req = Request::new(room_id.into(), reason); + + commune() + .send_matrix_request(req, Some(access_token.as_ref())) + .await + .map_err(Into::into) +} diff --git a/crates/core/src/membership/leave.rs b/crates/core/src/membership/leave.rs new file mode 100644 index 0000000..c5327f5 --- /dev/null +++ b/crates/core/src/membership/leave.rs @@ -0,0 +1,38 @@ +use matrix::{ + client::{ + self, + membership::leave::{Request, Response}, + }, + ruma_common::OwnedRoomOrAliasId, +}; + +use crate::{commune, error::Result}; + +pub async fn service( + access_token: impl AsRef, + room_or_alias_id: impl Into, + reason: Option, +) -> Result { + let room_or_alias_id: OwnedRoomOrAliasId = room_or_alias_id.into(); + + // this cannot error, `Result` is just provided in place of an enum + // https://github.com/ruma/ruma/issues/1761 + let room_id = match room_or_alias_id.try_into() { + Ok(room_id) => room_id, + Err(room_alias) => { + let req = client::directory::room::Request::new(room_alias); + + commune() + .send_matrix_request(req, None) + .await + .map(|resp| resp.room_id)? + } + }; + + let req = Request::new(room_id, reason); + + commune() + .send_matrix_request(req, Some(access_token.as_ref())) + .await + .map_err(Into::into) +} diff --git a/crates/core/src/rooms/create.rs b/crates/core/src/rooms/create.rs deleted file mode 100644 index 81ede67..0000000 --- a/crates/core/src/rooms/create.rs +++ /dev/null @@ -1,52 +0,0 @@ -use matrix::{ - client::create_room::{Request, Response, RoomCreationContent, RoomVisibility}, - ruma_common::{room::RoomType, OwnedRoomOrAliasId, RoomVersionId}, - ruma_events::{ - room::power_levels::RoomPowerLevelsEventContent, - space::parent::{InitialSpaceParentEvent, SpaceParentEventContent}, - }, -}; - -use crate::{commune, error::Result}; - -pub async fn service( - access_token: impl AsRef, - parent: OwnedRoomOrAliasId, - alias: Option, - name: Option, - topic: Option, -) -> Result { - let creation_content = Some(RoomCreationContent { - kind: None, - federate: true, - room_version: RoomVersionId::V11, - predecessor: None, - }); - - let req = Request::new( - creation_content, - Vec::new(), - Vec::new(), - false, - name, - None, - alias, - topic, - RoomVisibility::Public, - ); - - let resp = commune() - .send_matrix_request(req, Some(access_token.as_ref())) - .await?; - - let mut content = - SpaceParentEventContent::new(vec![commune().config.matrix.server_name.clone()]); - content.canonical = true; - - let state_event = InitialSpaceParentEvent { - content, - state_key: resp.room_id.clone(), - }; - - Ok(resp) -} diff --git a/crates/core/src/spaces.rs b/crates/core/src/spaces.rs index c5fb369..70cc1d1 100644 --- a/crates/core/src/spaces.rs +++ b/crates/core/src/spaces.rs @@ -1 +1,2 @@ pub mod create; +pub mod channels; diff --git a/crates/core/src/spaces/channels.rs b/crates/core/src/spaces/channels.rs new file mode 100644 index 0000000..c5fb369 --- /dev/null +++ b/crates/core/src/spaces/channels.rs @@ -0,0 +1 @@ +pub mod create; diff --git a/crates/core/src/spaces/channels/create.rs b/crates/core/src/spaces/channels/create.rs new file mode 100644 index 0000000..9a6a590 --- /dev/null +++ b/crates/core/src/spaces/channels/create.rs @@ -0,0 +1,76 @@ +use matrix::{ + client::{ + create_room::{Request, Response, RoomCreationContent, RoomVisibility}, + rooms, + }, + ruma_common::{OwnedRoomId, RoomVersionId}, + ruma_events::{ + space::{child::SpaceChildEventContent, parent::SpaceParentEventContent}, + EventContent, + }, +}; + +use crate::{commune, error::Result, util::opaque_id::OpaqueId}; + +pub async fn service( + access_token: impl AsRef, + space_id: OpaqueId, + name: Option, + topic: Option, +) -> Result { + let server_name = &commune().config.matrix.server_name; + // this should never panic + let space_id = OwnedRoomId::try_from(format!("!{space_id}:{server_name}")) + .expect("failed to parse room ID"); + + let req = Request::new( + Some(RoomCreationContent { + kind: None, + federate: true, + room_version: RoomVersionId::V11, + predecessor: None, + }), + Vec::new(), + Vec::new(), + false, + name, + None, + None, + topic, + RoomVisibility::Public, + ); + + let resp = commune() + .send_matrix_request(req, Some(access_token.as_ref())) + .await?; + + let mut parent_content = SpaceParentEventContent::new(vec![server_name.to_owned()]); + parent_content.canonical = true; + + let req = rooms::state::create::Request::new( + parent_content.event_type(), + resp.room_id.clone(), + Some(resp.room_id.to_string()), + parent_content, + )?; + + commune() + .send_matrix_request(req, Some(access_token.as_ref())) + .await?; + + let mut child_content = SpaceChildEventContent::new(vec![server_name.to_owned()]); + child_content.suggested = true; + + let req = rooms::state::create::Request::new( + child_content.event_type(), + space_id.clone(), + Some(space_id.to_string()), + child_content, + )?; + + commune() + .send_matrix_request(req, Some(access_token.as_ref())) + .await?; + + Ok(resp) +} diff --git a/crates/core/src/spaces/create.rs b/crates/core/src/spaces/create.rs index 6a1c6a7..1bc43b1 100644 --- a/crates/core/src/spaces/create.rs +++ b/crates/core/src/spaces/create.rs @@ -12,6 +12,7 @@ pub async fn service( name: Option, topic: Option, ) -> Result { + // Only state events should be allowed in spaces let mut power_levels = RoomPowerLevelsEventContent::new(); power_levels.events_default = 100.into(); diff --git a/crates/core/src/util.rs b/crates/core/src/util.rs index 73b12db..4a4154d 100644 --- a/crates/core/src/util.rs +++ b/crates/core/src/util.rs @@ -1 +1,2 @@ pub mod secret; +pub mod opaque_id; diff --git a/crates/core/src/util/opaque_id.rs b/crates/core/src/util/opaque_id.rs new file mode 100644 index 0000000..9ebee5d --- /dev/null +++ b/crates/core/src/util/opaque_id.rs @@ -0,0 +1,34 @@ +use std::{fmt::Display, str::FromStr}; + +use matrix::ruma_identifiers_validation::Error; +use serde::{de, Deserialize, Deserializer}; + +// helper type to validate the opaque part of a room ID separately. +pub struct OpaqueId(String); + +impl Display for OpaqueId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.0.as_str()) + } +} + +impl FromStr for OpaqueId { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s.chars().all(char::is_alphanumeric) { + true => Ok(OpaqueId(s.to_owned())), + false => Err(Error::InvalidCharacters), + } + } +} + +impl<'de> Deserialize<'de> for OpaqueId { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + s.parse().map_err(de::Error::custom) + } +} diff --git a/crates/matrix/src/admin.rs b/crates/matrix/src/admin.rs index 2608ca2..917bfd9 100644 --- a/crates/matrix/src/admin.rs +++ b/crates/matrix/src/admin.rs @@ -3,6 +3,6 @@ //! reference: https://matrix-org.github.io/synapse/latest/usage/administration/admin_api/index.html pub mod registration_tokens; -// pub mod room; +pub mod rooms; // pub mod session; // pub mod user; diff --git a/crates/matrix/src/admin/room.rs b/crates/matrix/src/admin/rooms.rs similarity index 92% rename from crates/matrix/src/admin/room.rs rename to crates/matrix/src/admin/rooms.rs index 38a3738..72ed8f6 100644 --- a/crates/matrix/src/admin/room.rs +++ b/crates/matrix/src/admin/rooms.rs @@ -9,11 +9,12 @@ use ruma_common::{ use ruma_events::room::{history_visibility::HistoryVisibility, join_rules::JoinRule}; use serde::Deserialize; -pub mod delete_room; -pub mod get_members; -pub mod get_room; -pub mod get_rooms; -pub mod get_state; +pub mod members; +pub mod state; + +pub mod get; +pub mod get_many; +pub mod delete; #[derive(Clone, Debug, Deserialize)] pub struct Room { diff --git a/crates/matrix/src/admin/room/delete_room.rs b/crates/matrix/src/admin/rooms/delete.rs similarity index 100% rename from crates/matrix/src/admin/room/delete_room.rs rename to crates/matrix/src/admin/rooms/delete.rs diff --git a/crates/matrix/src/admin/room/forward_extremities/delete.rs b/crates/matrix/src/admin/rooms/forward_extremities/delete.rs similarity index 100% rename from crates/matrix/src/admin/room/forward_extremities/delete.rs rename to crates/matrix/src/admin/rooms/forward_extremities/delete.rs diff --git a/crates/matrix/src/admin/room/forward_extremities/get.rs b/crates/matrix/src/admin/rooms/forward_extremities/get.rs similarity index 100% rename from crates/matrix/src/admin/room/forward_extremities/get.rs rename to crates/matrix/src/admin/rooms/forward_extremities/get.rs diff --git a/crates/matrix/src/admin/room/get_room.rs b/crates/matrix/src/admin/rooms/get.rs similarity index 84% rename from crates/matrix/src/admin/room/get_room.rs rename to crates/matrix/src/admin/rooms/get.rs index 967f577..0feea91 100644 --- a/crates/matrix/src/admin/room/get_room.rs +++ b/crates/matrix/src/admin/rooms/get.rs @@ -20,6 +20,12 @@ pub struct Request { pub room_id: OwnedRoomId, } +impl Request { + pub fn new(room_id: OwnedRoomId) -> Self { + Self { room_id } + } +} + #[response(error = crate::Error)] pub struct Response { #[ruma_api(body)] diff --git a/crates/matrix/src/admin/room/get_rooms.rs b/crates/matrix/src/admin/rooms/get_many.rs similarity index 100% rename from crates/matrix/src/admin/room/get_rooms.rs rename to crates/matrix/src/admin/rooms/get_many.rs diff --git a/crates/matrix/src/admin/rooms/members.rs b/crates/matrix/src/admin/rooms/members.rs new file mode 100644 index 0000000..125ca70 --- /dev/null +++ b/crates/matrix/src/admin/rooms/members.rs @@ -0,0 +1 @@ +pub mod get; diff --git a/crates/matrix/src/admin/room/get_members.rs b/crates/matrix/src/admin/rooms/members/get.rs similarity index 100% rename from crates/matrix/src/admin/room/get_members.rs rename to crates/matrix/src/admin/rooms/members/get.rs diff --git a/crates/matrix/src/admin/rooms/state.rs b/crates/matrix/src/admin/rooms/state.rs new file mode 100644 index 0000000..125ca70 --- /dev/null +++ b/crates/matrix/src/admin/rooms/state.rs @@ -0,0 +1 @@ +pub mod get; diff --git a/crates/matrix/src/admin/room/get_state.rs b/crates/matrix/src/admin/rooms/state/get.rs similarity index 100% rename from crates/matrix/src/admin/room/get_state.rs rename to crates/matrix/src/admin/rooms/state/get.rs diff --git a/crates/matrix/src/client.rs b/crates/matrix/src/client.rs index 3e6f5d1..feb9cbc 100644 --- a/crates/matrix/src/client.rs +++ b/crates/matrix/src/client.rs @@ -5,9 +5,12 @@ pub mod login; pub mod logout; pub mod register; +pub mod account; +pub mod profile; + pub mod create_room; pub mod rooms; +pub mod directory; +pub mod membership; -pub mod account; -pub mod profile; pub mod uiaa; diff --git a/crates/matrix/src/client/create_room.rs b/crates/matrix/src/client/create_room.rs index 1683742..7bc4794 100644 --- a/crates/matrix/src/client/create_room.rs +++ b/crates/matrix/src/client/create_room.rs @@ -3,7 +3,7 @@ use ruma_common::{ metadata, room::RoomType, serde::Raw, - OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomVersionId, + OwnedRoomId, OwnedUserId, RoomVersionId, }; use ruma_events::{ room::{create::PreviousRoom, power_levels::RoomPowerLevelsEventContent}, diff --git a/crates/matrix/src/client/directory.rs b/crates/matrix/src/client/directory.rs new file mode 100644 index 0000000..addf7a5 --- /dev/null +++ b/crates/matrix/src/client/directory.rs @@ -0,0 +1 @@ +pub mod room; diff --git a/crates/matrix/src/client/directory/room.rs b/crates/matrix/src/client/directory/room.rs new file mode 100644 index 0000000..82c789a --- /dev/null +++ b/crates/matrix/src/client/directory/room.rs @@ -0,0 +1,32 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedRoomAliasId, OwnedServerName, OwnedRoomId, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: None, + history: { + unstable => "/_matrix/client/v3/directory/room/{room_alias}", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub room_alias: OwnedRoomAliasId, +} + +impl Request { + pub fn new(room_alias: OwnedRoomAliasId) -> Self { + Self { room_alias } + } +} + +#[response(error = crate::Error)] +pub struct Response { + pub room_id: OwnedRoomId, + pub servers: Vec, +} diff --git a/crates/matrix/src/client/membership.rs b/crates/matrix/src/client/membership.rs new file mode 100644 index 0000000..2c0fcca --- /dev/null +++ b/crates/matrix/src/client/membership.rs @@ -0,0 +1,2 @@ +pub mod join; +pub mod leave; diff --git a/crates/matrix/src/client/membership/join.rs b/crates/matrix/src/client/membership/join.rs new file mode 100644 index 0000000..9892603 --- /dev/null +++ b/crates/matrix/src/client/membership/join.rs @@ -0,0 +1,44 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, +}; +use serde::Serialize; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: true, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/v3/join/:room_id_or_alias", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub room_id_or_alias: OwnedRoomOrAliasId, + + #[ruma_api(query)] + #[serde(skip_serializing_if = "<[_]>::is_empty")] + pub server_name: Vec, + + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, +} + +impl Request { + pub fn new(room_id_or_alias: OwnedRoomOrAliasId, reason: Option) -> Self { + Self { + room_id_or_alias, + reason, + server_name: Vec::new(), + } + } +} + +#[response(error = crate::Error)] +#[derive(Serialize)] +pub struct Response { + pub room_id: OwnedRoomId, +} diff --git a/crates/matrix/src/client/membership/leave.rs b/crates/matrix/src/client/membership/leave.rs new file mode 100644 index 0000000..51470ef --- /dev/null +++ b/crates/matrix/src/client/membership/leave.rs @@ -0,0 +1,34 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedRoomId, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: true, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/v3/rooms/:room_id/leave", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub room_id: OwnedRoomId, + + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, +} + +impl Request { + pub fn new(room_id: OwnedRoomId, reason: Option) -> Self { + Self { room_id, reason } + } +} + +#[response(error = crate::Error)] +pub struct Response { + pub room_id: OwnedRoomId, +} diff --git a/crates/matrix/src/client/rooms/messages.rs b/crates/matrix/src/client/rooms/messages.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/matrix/src/client/rooms/state.rs b/crates/matrix/src/client/rooms/state.rs index f715df0..c59b5b4 100644 --- a/crates/matrix/src/client/rooms/state.rs +++ b/crates/matrix/src/client/rooms/state.rs @@ -1,27 +1,3 @@ -use ruma_common::{ - api::{request, response, Metadata}, - metadata, -}; - -#[allow(dead_code)] -const METADATA: Metadata = metadata! { - method: POST, - rate_limited: false, - authentication: AccessToken, - history: { - unstable => "/_matrix/client/v3/logout", - } -}; - -#[request(error = crate::Error)] -pub struct Request {} - -#[allow(clippy::new_without_default)] -impl Request { - pub fn new() -> Self { - Self {} - } -} - -#[response(error = crate::Error)] -pub struct Response {} +pub mod get; +pub mod get_many; +pub mod create; diff --git a/crates/matrix/src/client/rooms/state/create.rs b/crates/matrix/src/client/rooms/state/create.rs new file mode 100644 index 0000000..9534657 --- /dev/null +++ b/crates/matrix/src/client/rooms/state/create.rs @@ -0,0 +1,56 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, + serde::Raw, + OwnedEventId, OwnedRoomId, +}; +use ruma_events::{AnyStateEventContent, StateEventType}; +use serde::{Serialize, Deserialize}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: PUT, + rate_limited: true, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/v3/rooms/{room_id}/state/{event_type}/{state_key}", + } +}; + +#[request(error = crate::Error)] +#[derive(Serialize)] +pub struct Request { + #[ruma_api(path)] + pub event_type: StateEventType, + + #[ruma_api(path)] + pub room_id: OwnedRoomId, + + #[ruma_api(path)] + #[serde(skip_serializing_if = "String::is_empty")] + pub state_key: String, + + pub content: Raw, +} + +impl Request { + pub fn new( + event_type: StateEventType, + room_id: OwnedRoomId, + state_key: Option, + content: impl Into, + ) -> serde_json::Result { + Ok(Self { + event_type, + room_id, + state_key: state_key.unwrap_or_default(), + content: Raw::new(&content.into())?, + }) + } +} + +#[response(error = crate::Error)] +#[derive(Deserialize)] +pub struct Response { + pub event_id: OwnedEventId, +} diff --git a/crates/matrix/src/client/rooms/state/get.rs b/crates/matrix/src/client/rooms/state/get.rs new file mode 100644 index 0000000..c312455 --- /dev/null +++ b/crates/matrix/src/client/rooms/state/get.rs @@ -0,0 +1,53 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, + serde::Raw, + OwnedRoomId, +}; +use ruma_events::{AnyStateEventContent, StateEventType}; +use serde::{Deserialize, Serialize}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/v3/rooms/{room_id}/state/{event_type}/{state_key}", + } +}; + +#[request(error = crate::Error)] +#[derive(Serialize)] +pub struct Request { + #[ruma_api(path)] + pub event_type: StateEventType, + + #[ruma_api(path)] + pub room_id: OwnedRoomId, + + #[ruma_api(path)] + #[serde(skip_serializing_if = "String::is_empty")] + pub state_key: String, +} + +impl Request { + pub fn new( + event_type: StateEventType, + room_id: OwnedRoomId, + state_key: Option, + ) -> Self { + Self { + event_type, + room_id, + state_key: state_key.unwrap_or_default(), + } + } +} + +#[response(error = crate::Error)] +#[derive(Deserialize)] +#[serde(transparent)] +pub struct Response { + pub content: Raw, +} diff --git a/crates/matrix/src/client/rooms/state/get_many.rs b/crates/matrix/src/client/rooms/state/get_many.rs new file mode 100644 index 0000000..18cfdf4 --- /dev/null +++ b/crates/matrix/src/client/rooms/state/get_many.rs @@ -0,0 +1,38 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, + serde::Raw, + OwnedRoomId, +}; +use ruma_events::AnyStateEvent; +use serde::{Deserialize, Serialize}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: true, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/v3/rooms/{room_id}/state", + } +}; + +#[request(error = crate::Error)] +#[derive(Serialize)] +pub struct Request { + #[ruma_api(path)] + pub room_id: OwnedRoomId, +} + +impl Request { + pub fn new(room_id: OwnedRoomId) -> Self { + Self { room_id } + } +} + +#[response(error = crate::Error)] +#[derive(Deserialize)] +#[serde(transparent)] +pub struct Response { + pub event_id: Vec>, +} diff --git a/crates/router/src/api.rs b/crates/router/src/api.rs index 4117da4..21a185e 100644 --- a/crates/router/src/api.rs +++ b/crates/router/src/api.rs @@ -3,7 +3,8 @@ //! reference: https://spec.matrix.org/unstable/client-server-api pub mod account; -pub mod relative; +pub mod direct; +pub mod membership; pub mod register; +pub mod relative; pub mod spaces; -pub mod rooms; diff --git a/crates/router/src/api/direct.rs b/crates/router/src/api/direct.rs new file mode 100644 index 0000000..c5fb369 --- /dev/null +++ b/crates/router/src/api/direct.rs @@ -0,0 +1 @@ +pub mod create; diff --git a/crates/router/src/api/direct/create.rs b/crates/router/src/api/direct/create.rs new file mode 100644 index 0000000..aa0d192 --- /dev/null +++ b/crates/router/src/api/direct/create.rs @@ -0,0 +1,40 @@ +use axum::{ + response::{IntoResponse, Response}, + Json, +}; +use axum_extra::{ + headers::{authorization::Bearer, Authorization}, + TypedHeader, +}; +use matrix::ruma_common::OwnedUserId; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Payload { + pub name: Option, + pub topic: Option, + pub invite: Vec, +} + +pub async fn handler( + TypedHeader(access_token): TypedHeader>, + Json(payload): Json, +) -> Response { + use commune::direct::create::service; + + match service( + access_token.token(), + payload.name, + payload.topic, + payload.invite + ) + .await + { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to create room"); + + e.into_response() + } + } +} diff --git a/crates/router/src/api/membership.rs b/crates/router/src/api/membership.rs new file mode 100644 index 0000000..2c0fcca --- /dev/null +++ b/crates/router/src/api/membership.rs @@ -0,0 +1,2 @@ +pub mod join; +pub mod leave; diff --git a/crates/router/src/api/membership/join.rs b/crates/router/src/api/membership/join.rs new file mode 100644 index 0000000..1b38b71 --- /dev/null +++ b/crates/router/src/api/membership/join.rs @@ -0,0 +1,26 @@ +use axum::{ + extract::Path, + response::{IntoResponse, Response}, + Json, +}; +use axum_extra::{ + headers::{authorization::Bearer, Authorization}, + TypedHeader, +}; +use matrix::ruma_common::OwnedRoomOrAliasId; + +pub async fn handler( + TypedHeader(access_token): TypedHeader>, + Path(room_or_alias_id): Path, +) -> Response { + use commune::membership::join::service; + + match service(access_token.token(), room_or_alias_id, None).await { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to join space"); + + e.into_response() + } + } +} diff --git a/crates/router/src/api/membership/leave.rs b/crates/router/src/api/membership/leave.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/router/src/api/rooms.rs b/crates/router/src/api/rooms.rs index dec16f3..c5fb369 100644 --- a/crates/router/src/api/rooms.rs +++ b/crates/router/src/api/rooms.rs @@ -1 +1 @@ -pub mod root; +pub mod create; diff --git a/crates/router/src/api/rooms/root.rs b/crates/router/src/api/rooms/create.rs similarity index 78% rename from crates/router/src/api/rooms/root.rs rename to crates/router/src/api/rooms/create.rs index d2d2388..e55e691 100644 --- a/crates/router/src/api/rooms/root.rs +++ b/crates/router/src/api/rooms/create.rs @@ -6,7 +6,7 @@ use axum_extra::{ headers::{authorization::Bearer, Authorization}, TypedHeader, }; -use matrix::ruma_common::OwnedRoomOrAliasId; +use matrix::ruma_common::OwnedUserId; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] @@ -14,26 +14,27 @@ pub struct Payload { pub alias: Option, pub name: Option, pub topic: Option, - pub parent: OwnedRoomOrAliasId, + pub invite: Vec, } pub async fn handler( TypedHeader(access_token): TypedHeader>, Json(payload): Json, ) -> Response { - use commune::rooms::create::service; + use commune::rooms::create::direct::service; match service( access_token.token(), payload.alias, payload.name, payload.topic, + payload.invite ) .await { Ok(resp) => Json(resp).into_response(), Err(e) => { - tracing::warn!(?e, "failed to create space"); + tracing::warn!(?e, "failed to create room"); e.into_response() } diff --git a/crates/router/src/api/spaces.rs b/crates/router/src/api/spaces.rs index dec16f3..3c29188 100644 --- a/crates/router/src/api/spaces.rs +++ b/crates/router/src/api/spaces.rs @@ -1 +1,2 @@ -pub mod root; +pub mod channels; +pub mod create; diff --git a/crates/router/src/api/spaces/channels.rs b/crates/router/src/api/spaces/channels.rs new file mode 100644 index 0000000..c5fb369 --- /dev/null +++ b/crates/router/src/api/spaces/channels.rs @@ -0,0 +1 @@ +pub mod create; diff --git a/crates/router/src/api/spaces/channels/create.rs b/crates/router/src/api/spaces/channels/create.rs new file mode 100644 index 0000000..3f22e8b --- /dev/null +++ b/crates/router/src/api/spaces/channels/create.rs @@ -0,0 +1,34 @@ +use axum::{ + extract::Path, + response::{IntoResponse, Response}, + Json, +}; +use axum_extra::{ + headers::{authorization::Bearer, Authorization}, + TypedHeader, +}; +use commune::util::opaque_id::OpaqueId; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Payload { + pub name: Option, + pub topic: Option, +} + +pub async fn handler( + TypedHeader(access_token): TypedHeader>, + Path(space_id): Path, + Json(payload): Json, +) -> Response { + use commune::spaces::channels::create::service; + + match service(access_token.token(), space_id, payload.name, payload.topic).await { + Ok(resp) => Json(resp).into_response(), + Err(e) => { + tracing::warn!(?e, "failed to create channel for space"); + + e.into_response() + } + } +} diff --git a/crates/router/src/api/spaces/root.rs b/crates/router/src/api/spaces/create.rs similarity index 100% rename from crates/router/src/api/spaces/root.rs rename to crates/router/src/api/spaces/create.rs diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 6200b12..ea12e34 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -10,6 +10,8 @@ pub mod api; pub async fn routes() -> Router { let router = Router::new() + .route("/login", post(api::relative::login::handler)) + .route("/logout", post(api::relative::logout::handler)) .nest( "/register", Router::new() @@ -20,8 +22,6 @@ pub async fn routes() -> Router { ) .route("/email/:email", get(api::register::email::handler)), ) - .route("/login", post(api::relative::login::handler)) - .route("/logout", post(api::relative::logout::handler)) .nest( "/account", Router::new() @@ -30,10 +30,15 @@ pub async fn routes() -> Router { .route("/display_name", put(api::account::display_name::handler)) .route("/avatar", put(api::account::avatar::handler)), ) + .nest( + "/direct", + Router::new().route("/", post(api::direct::create::handler)), + ) .nest( "/spaces", Router::new() - .route("/", post(api::spaces::root::handler)) + .route("/", post(api::spaces::create::handler)) + .route("/:space_id/channels", post(api::spaces::channels::create::handler)), ); Router::new().nest("/_commune/client/r0", router) diff --git a/crates/test/src/api.rs b/crates/test/src/api.rs index b8bbac1..2e5b066 100644 --- a/crates/test/src/api.rs +++ b/crates/test/src/api.rs @@ -5,3 +5,4 @@ pub mod account; pub mod relative; pub mod spaces; +pub mod rooms; diff --git a/crates/test/src/api/rooms.rs b/crates/test/src/api/rooms.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/test/src/api/rooms/create.rs b/crates/test/src/api/rooms/create.rs new file mode 100644 index 0000000..afdae97 --- /dev/null +++ b/crates/test/src/api/rooms/create.rs @@ -0,0 +1 @@ +pub mod direct; diff --git a/crates/test/src/api/rooms/create/direct.rs b/crates/test/src/api/rooms/create/direct.rs new file mode 100644 index 0000000..a9771db --- /dev/null +++ b/crates/test/src/api/rooms/create/direct.rs @@ -0,0 +1,35 @@ +use commune::util::secret::Secret; + +use matrix::client::register::root::*; +use router::api::register::root as register; + +use crate::{env::Env, util::generate_comforming_localpart}; + +pub async fn register(client: &Env) -> Result { + let username = generate_comforming_localpart(); + + tracing::info!(?username); + + let resp = client + .post("/_commune/client/r0/rooms") + .json(®ister::Payload { + username, + password: Secret::new("verysecure"), + registration_token: None, + }) + .send() + .await?; + + resp.json().await +} + +#[tokio::test] +async fn register_test() { + let client = Env::new().await; + + let resp = register(&client).await.unwrap(); + + tracing::info!(?resp); + + assert!(resp.access_token.is_some() && resp.access_token.map(|at| !at.is_empty()).unwrap()); +} diff --git a/crates/test/src/api/rooms/direct.rs b/crates/test/src/api/rooms/direct.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/test/src/api/spaces/create.rs b/crates/test/src/api/spaces/create.rs index c77371b..a578b3b 100644 --- a/crates/test/src/api/spaces/create.rs +++ b/crates/test/src/api/spaces/create.rs @@ -1,5 +1,5 @@ use matrix::client::create_room::*; -use router::api::spaces::root::Payload; +use router::api::spaces::create::Payload; use crate::{api::relative::login, env::Env};