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() diff --git a/Cargo.toml b/Cargo.toml index 730eef0..bbdf33d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,44 +9,63 @@ 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"] +members = ["crates/core", "crates/matrix", "crates/router", "crates/test"] +default-members = ["crates/router"] resolver = "2" [workspace.dependencies] -anyhow = "1.0.75" +axum-extra = { version = "0.9.3", features = ["typed-header"] } async-trait = "0.1.74" -axum = { version = "0.7.4", features = ["tokio"] } -chrono = { version = "0.4.34", features = ["serde"] } -dotenv = "0.15.0" +# async-stream = "0.3.5" +bytes = "1.5.0" +email_address = { version = "0.2.4", features = ["serde", "serde_support"] } +figment = { version = "0.10.14", features = ["toml", "env"] } hex = "0.4.3" +tokio-rustls = "0.25.0" +# futures = "0.3.30" hmac = "0.12.1" +sha1 = "0.10.6" +anyhow = "1.0.75" +axum = { version = "0.7.4", features = ["tokio", "macros"] } 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", ] } 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" +serde_json = "1.0.114" +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" } + +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 } +ruma-identifiers-validation = { version = "0.9.3", default_features = false } + [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..38f633e --- /dev/null +++ b/commune-example.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 = "" +password = "" +tls = false diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 9989278..a6961dc 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -10,18 +10,23 @@ path = "src/lib.rs" [dependencies] # Workspace Dependencies -handlebars = { workspace = true } -lettre = { workspace = true } +anyhow = { workspace = true } +axum = { workspace = true } rand = { workspace = true } +email_address = { 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"] } +headers = { workspace = true } +tokio-rustls = { workspace = true } # Local Dependencies -matrix = { path = "../matrix" } +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/error.rs b/crates/core/src/account/error.rs deleted file mode 100644 index 390e505..0000000 --- a/crates/core/src/account/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/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/mod.rs b/crates/core/src/account/mod.rs deleted file mode 100644 index e1001c6..0000000 --- a/crates/core/src/account/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod error; -pub mod model; -pub mod service; diff --git a/crates/core/src/account/model.rs b/crates/core/src/account/model.rs deleted file mode 100644 index cdc5d3e..0000000 --- a/crates/core/src/account/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/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/service.rs b/crates/core/src/account/service.rs deleted file mode 100644 index d0f99d0..0000000 --- a/crates/core/src/account/service.rs +++ /dev/null @@ -1,388 +0,0 @@ -use std::sync::Arc; - -use matrix::{ - admin::resources::user::UserService, client::resources::session::Session, ruma_common::UserId, -}; -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 { - admin: Arc, - auth: 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/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 e1001c6..0000000 --- a/crates/core/src/auth/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod error; -pub mod model; -pub mod service; 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 39d89c2..0000000 --- a/crates/core/src/auth/service.rs +++ /dev/null @@ -1,152 +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, -} - -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/config.rs b/crates/core/src/config.rs new file mode 100644 index 0000000..57b5f82 --- /dev/null +++ b/crates/core/src/config.rs @@ -0,0 +1,34 @@ +use matrix::ruma_common::OwnedServerName; +use serde::Deserialize; +use url::Url; + +use crate::util::secret::Secret; + +#[derive(Debug, Deserialize)] +pub struct Config { + pub registration_verification: bool, + pub public_loopback: bool, + pub port: Option, + + pub allowed_domains: Option>, + pub blocked_domains: Option>, + + pub matrix: Matrix, + pub mail: SMTP, +} + +#[derive(Debug, Deserialize)] +pub struct SMTP { + pub host: Url, + 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, +} diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index 34542fe..7134727 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -1,59 +1,32 @@ -use http::StatusCode; +use axum::{http::StatusCode, response::IntoResponse}; use thiserror::Error; -use crate::{ - account::error::AccountErrorCode, 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; -} - #[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::HandleError), -impl From for Error { - fn from(err: AccountErrorCode) -> Self { - Error::User(err) - } -} + #[error("instance does not allow email address originating from this domain")] + EmailDomain, -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, - } - } + #[error("failed to validate identifier: {0}")] + InvalidIdentifier(#[from] matrix::ruma_identifiers_validation::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), +} - 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", - } +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 afc2a0a..2ccd121 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -1,155 +1,128 @@ -pub mod account; -pub mod auth; +//! This library deals with our core logic, such as authorizing user +//! interactions, forwarding regular events and constructing custom requests. + +pub mod config; pub mod error; -pub mod mail; -pub mod room; pub mod util; -pub use error::{Error, HttpStatusCode, Result}; - -use mail::service::MailService; -use room::service::RoomService; -use url::Url; - -use std::{fmt::Debug, str::FromStr, sync::Arc}; - -use matrix::Client as MatrixAdminClient; +pub mod account; +pub mod profile; -use self::{account::service::AccountService, auth::service::AuthService}; +use std::sync::RwLock; -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"; -} +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}, +}; -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct CommuneConfig { - 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_registration_shared_secret: String, -} +static COMMUNE: RwLock> = RwLock::new(None); -impl Default for CommuneConfig { - fn default() -> Self { - Self::new() - } +pub struct Commune { + pub config: Config, + client: matrix::Client, + // smtp: SmtpClient>, } -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), - } +pub async 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(); + + 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"); } - fn var(name: &str) -> P { - if let Ok(value) = std::env::var(name) { - if let Ok(value) = value.parse() { - return value; - } - } - - panic!( - "Failed to parse {} as {:?}", - name, - std::any::type_name::

() - ); - } - - fn var_opt(name: &str) -> Option

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

() - ); - } - - None - } + *commune = Some(Box::leak(Box::new(Commune { config, client }))); } -pub struct Commune { - pub account: Arc, - pub auth: Arc, - pub room: Arc, +pub fn commune() -> &'static Commune { + COMMUNE + .read() + .unwrap() + .expect("commune should be initialized at this point") } 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) + 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, }; - 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), + 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 room = RoomService::new(Arc::clone(&admin_client)); + 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()); - Ok(Self { - account: Arc::new(account), - auth, - room: Arc::new(room), - }) + 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 03141d7..0000000 --- a/crates/core/src/mail/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod error; -pub mod service; 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/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/util/mod.rs b/crates/core/src/util.rs similarity index 53% rename from crates/core/src/util/mod.rs rename to crates/core/src/util.rs index fe66b81..73b12db 100644 --- a/crates/core/src/util/mod.rs +++ b/crates/core/src/util.rs @@ -1,2 +1 @@ pub mod secret; -pub mod time; diff --git a/crates/core/src/util/secret.rs b/crates/core/src/util/secret.rs index 2517468..b1eb440 100644 --- a/crates/core/src/util/secret.rs +++ b/crates/core/src/util/secret.rs @@ -1,62 +1,48 @@ -use std::{ - borrow::Cow, - convert::Infallible, - fmt::{Debug, Display}, - str::FromStr, -}; +use std::fmt::{Debug, Display}; +use rand::{distributions::Uniform, Rng}; use serde::{Deserialize, Serialize}; -/// 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() +// is this necessary? +impl Serialize for Secret { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.inner().serialize(serializer) } } -impl From for Secret { - fn from(s: String) -> Self { - Secret(Cow::Owned(s)) +impl Secret { + #[inline] + pub fn new(s: impl Into) -> Self { + Self(s.into()) } -} -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 +71,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 06a62e0..0000000 --- a/crates/core/src/util/time.rs +++ /dev/null @@ -1,13 +0,0 @@ -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/Cargo.toml b/crates/matrix/Cargo.toml index b7b2ab5..25a1738 100644 --- a/crates/matrix/Cargo.toml +++ b/crates/matrix/Cargo.toml @@ -9,16 +9,27 @@ name = "matrix" path = "src/lib.rs" [dependencies] -ruma-events = { version = "0.27.11", features = ["html", "markdown"] } -ruma-common = { version = "0.12.1", features = ["rand"] } -ruma-macros = "0.12.0" +ruma-events = { workspace = true } +ruma-common = { workspace = true } +ruma-macros = { workspace = true } +ruma-client = { workspace = true } +ruma-identifiers-validation = { workspace = true } # Workspace Dependencies -anyhow = { workspace = true } -chrono = { workspace = true, features = ["serde"] } mime = { workspace = true } reqwest = { workspace = true, features = ["json"] } serde = { workspace = true } +serde_json = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } +sha1 = { workspace = true } url = { workspace = true, features = ["serde"] } +hex = { workspace = true } +hmac = { workspace = true } +http = { workspace = true } +bytes = { workspace = true } +async-trait = { workspace = true } + +[features] +client = [] +server = [] diff --git a/crates/matrix/src/admin.rs b/crates/matrix/src/admin.rs new file mode 100644 index 0000000..2608ca2 --- /dev/null +++ b/crates/matrix/src/admin.rs @@ -0,0 +1,8 @@ +//! This module is the root of the admin API. +//! +//! reference: https://matrix-org.github.io/synapse/latest/usage/administration/admin_api/index.html + +pub mod registration_tokens; +// pub mod room; +// pub mod session; +// pub mod user; diff --git a/crates/matrix/src/admin/mod.rs b/crates/matrix/src/admin/mod.rs deleted file mode 100644 index 6d0fe39..0000000 --- a/crates/matrix/src/admin/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod resources; 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..7849c07 --- /dev/null +++ b/crates/matrix/src/admin/registration_tokens/new.rs @@ -0,0 +1,38 @@ +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/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/room.rs b/crates/matrix/src/admin/resources/room.rs deleted file mode 100644 index 9b153c5..0000000 --- a/crates/matrix/src/admin/resources/room.rs +++ /dev/null @@ -1,474 +0,0 @@ -//! [Room Admin API](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, -} - -#[derive(Default, Debug, Serialize)] -pub struct TimestampToEventQuery { - #[serde(skip_serializing_if = "Option::is_none")] - pub ts: Option, - - pub direction: Direction, -} - -#[derive(Default, Debug, Serialize)] -pub struct EventContextQuery { - #[serde(skip_serializing_if = "Option::is_none")] - pub limit: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub filter: Option, -} - -#[derive(Debug, Serialize)] -pub struct ReplaceRoomQuery { - #[serde(rename = "new_room_user_id")] - pub admin: OwnedUserId, - - #[serde(skip_serializing_if = "String::is_empty")] - pub room_name: String, - - #[serde(skip_serializing_if = "String::is_empty")] - pub message: String, -} - -#[derive(Default, Debug, Serialize)] -pub struct DeleteQuery { - #[serde(flatten, skip_serializing_if = "Option::is_none")] - pub new_room: Option, - - pub block: bool, - - pub purge: 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, -} - -#[derive(Debug, Deserialize)] -pub struct MembersResponse { - pub members: Vec, - pub total: u64, -} - -#[derive(Debug, Deserialize)] -pub struct State { - #[serde(rename = "type")] - pub kind: String, - pub state_key: String, - pub etc: Option, -} - -#[derive(Debug, Deserialize)] -pub struct StateResponse { - pub state: Vec, -} - -#[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)] -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, -} diff --git a/crates/matrix/src/admin/resources/token/mod.rs b/crates/matrix/src/admin/resources/token/mod.rs deleted file mode 100644 index 24b2441..0000000 --- a/crates/matrix/src/admin/resources/token/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod shared_secret; diff --git a/crates/matrix/src/admin/resources/token/shared_secret.rs b/crates/matrix/src/admin/resources/token/shared_secret.rs deleted file mode 100644 index 1bba14c..0000000 --- a/crates/matrix/src/admin/resources/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); - } -} 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/room.rs b/crates/matrix/src/admin/room.rs new file mode 100644 index 0000000..38a3738 --- /dev/null +++ b/crates/matrix/src/admin/room.rs @@ -0,0 +1,59 @@ +//! 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, + OwnedUserId, RoomVersionId, +}; +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; + +#[derive(Clone, Debug, Deserialize)] +pub struct Room { + pub room_id: OwnedRoomId, + + pub canonical_alias: Option, + + pub avatar: Option, + + pub name: Option, + + pub joined_members: u64, + + pub joined_local_members: u64, + + pub version: RoomVersionId, + + pub creator: OwnedUserId, + + pub encryption: Option, + + pub federatable: bool, + + pub public: bool, + + pub join_rules: Option, + + pub history_visibility: Option, + + pub state_events: u64, + + pub room_type: Option, + + #[serde(flatten)] + pub details: Option, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct RoomDetails { + pub topic: Option, + + 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..578a334 --- /dev/null +++ b/crates/matrix/src/admin/room/delete_room.rs @@ -0,0 +1,47 @@ +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, + + pub block: bool, + + #[serde(skip_serializing_if = "ruma_common::serde::is_true")] + pub purge: bool, + + pub force_purge: bool, +} + +#[response(error = crate::Error)] +pub struct Response { + pub 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/room/forward_extremities/delete.rs b/crates/matrix/src/admin/room/forward_extremities/delete.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/matrix/src/admin/room/forward_extremities/get.rs b/crates/matrix/src/admin/room/forward_extremities/get.rs new file mode 100644 index 0000000..e69de29 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..967f577 --- /dev/null +++ b/crates/matrix/src/admin/room/get_room.rs @@ -0,0 +1,27 @@ +use super::Room; +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedRoomId, +}; + +#[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..08a792c --- /dev/null +++ b/crates/matrix/src/admin/room/get_rooms.rs @@ -0,0 +1,83 @@ +use ruma_common::{ + api::{request, response, Direction, Metadata}, + 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 { + pub rooms: Vec, + + pub offset: u64, + + #[serde(rename = "total_rooms")] + pub total: u64, + + pub next_batch: Option, + + pub 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/session.rs b/crates/matrix/src/admin/session.rs new file mode 100644 index 0000000..10d725f --- /dev/null +++ b/crates/matrix/src/admin/session.rs @@ -0,0 +1,46 @@ +//! This module contains handlers for user registration. +//! +//! 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, Serialize)] +pub struct Hmac { + inner: Vec, +} + +impl Hmac { + pub 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(), + }) + } + + 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 new file mode 100644 index 0000000..0388987 --- /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 { + pub 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..4838353 --- /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_serializing_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 new file mode 100644 index 0000000..3417bb5 --- /dev/null +++ b/crates/matrix/src/admin/user.rs @@ -0,0 +1,53 @@ +//! This module contains handlers for managing users. +//! +//! 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}; + +pub mod get_user; +pub mod get_user_by_3pid; +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 new file mode 100644 index 0000000..da302be --- /dev/null +++ b/crates/matrix/src/admin/user/get_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: GET, + 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, +} + +#[response(error = crate::Error)] +pub struct Response { + #[ruma_api(body)] + pub user: 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..2263ba7 --- /dev/null +++ b/crates/matrix/src/admin/user/get_user_by_3pid.rs @@ -0,0 +1,32 @@ +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 new file mode 100644 index 0000000..ad094b8 --- /dev/null +++ b/crates/matrix/src/admin/user/get_users.rs @@ -0,0 +1,84 @@ +use ruma_common::{ + api::{request, response, Direction, Metadata}, + metadata, OwnedUserId, +}; +use serde::Serialize; + +use super::User; + +#[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 users: Vec, + + pub next_token: String, + + pub total: u64, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)] +#[allow(dead_code)] +pub enum OrderBy { + #[default] + Name, + + Admin, + + UserType, + + Deactivated, + + ShadowBanned, + + Displayname, + + AvatarUrl, + + CreationTs, + + LastSeenTs, +} 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..83868f1 --- /dev/null +++ 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 {} 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/resources/events.rs b/crates/matrix/src/client-backup/events.rs.bk.bk.bk similarity index 100% rename from crates/matrix/src/client/resources/events.rs rename to crates/matrix/src/client-backup/events.rs.bk.bk.bk 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-backup/mod.rs.bk.bk b/crates/matrix/src/client-backup/mod.rs.bk.bk new file mode 100644 index 0000000..cc296e6 --- /dev/null +++ b/crates/matrix/src/client-backup/mod.rs.bk.bk @@ -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 rooms; +pub mod session; +pub mod membership; +pub mod uiaa; +pub mod sync; +pub mod account; diff --git a/crates/matrix/src/client/resources/mxc.rs b/crates/matrix/src/client-backup/mxc.rs.bk.bk.bk similarity index 100% rename from crates/matrix/src/client/resources/mxc.rs rename to crates/matrix/src/client-backup/mxc.rs.bk.bk.bk 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-backup/sync.rs.bk.bk b/crates/matrix/src/client-backup/sync.rs.bk.bk new file mode 100644 index 0000000..906f9cf --- /dev/null +++ b/crates/matrix/src/client-backup/sync.rs.bk.bk @@ -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/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/account/whoami.rs b/crates/matrix/src/client/account/whoami.rs new file mode 100644 index 0000000..c2f1f73 --- /dev/null +++ b/crates/matrix/src/client/account/whoami.rs @@ -0,0 +1,32 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedDeviceId, OwnedUserId, +}; +use serde::Serialize; + +#[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 {} + +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/login.rs b/crates/matrix/src/client/login.rs new file mode 100644 index 0000000..153f0d6 --- /dev/null +++ b/crates/matrix/src/client/login.rs @@ -0,0 +1,134 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedDeviceId, OwnedMxcUri, OwnedUserId, +}; +use serde::{Deserialize, Serialize}; + +use crate::client::uiaa::UserIdentifier; + +#[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, +} + +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)] +#[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, + + #[serde(skip_serializing_if = "Option::is_none")] + pub well_known: Option, +} + +// 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 { + 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, Deserialize, Serialize)] +pub struct BaseUrl { + pub base_url: url::Url, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +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/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/logout/root.rs b/crates/matrix/src/client/logout/root.rs new file mode 100644 index 0000000..10d0cbb --- /dev/null +++ b/crates/matrix/src/client/logout/root.rs @@ -0,0 +1,29 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, +}; +use serde::{Deserialize, 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(Deserialize, Serialize)] +pub struct Response {} diff --git a/crates/matrix/src/client/mod.rs b/crates/matrix/src/client/mod.rs deleted file mode 100644 index 6d0fe39..0000000 --- a/crates/matrix/src/client/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod resources; 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/profile/avatar_url/update.rs b/crates/matrix/src/client/profile/avatar_url/update.rs new file mode 100644 index 0000000..d291379 --- /dev/null +++ b/crates/matrix/src/client/profile/avatar_url/update.rs @@ -0,0 +1,36 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedMxcUri, OwnedUserId, +}; +use serde::Serialize; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: PUT, + rate_limited: true, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/v3/profile/:user_id/avatar_url", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub user_id: OwnedUserId, + + 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/profile/display_name/update.rs b/crates/matrix/src/client/profile/display_name/update.rs new file mode 100644 index 0000000..5cac54f --- /dev/null +++ b/crates/matrix/src/client/profile/display_name/update.rs @@ -0,0 +1,35 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedUserId, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: PUT, + rate_limited: true, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/v3/profile/:user_id/displayname", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub user_id: OwnedUserId, + + #[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)] +pub struct Response {} 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/register/available.rs b/crates/matrix/src/client/register/available.rs new file mode 100644 index 0000000..f5ecc37 --- /dev/null +++ b/crates/matrix/src/client/register/available.rs @@ -0,0 +1,33 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, +}; +use serde::Serialize; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: true, + authentication: None, + history: { + unstable => "/_matrix/client/v3/register/available", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(query)] + pub username: String, +} + +impl Request { + pub fn new(username: String) -> Self { + Self { username } + } +} + +#[response(error = crate::Error)] +#[derive(Serialize)] +pub struct Response { + pub available: bool, +} 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/resources/error.rs b/crates/matrix/src/client/resources/error.rs deleted file mode 100644 index 872754b..0000000 --- a/crates/matrix/src/client/resources/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/resources/login.rs b/crates/matrix/src/client/resources/login.rs deleted file mode 100644 index 6a65fba..0000000 --- a/crates/matrix/src/client/resources/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/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/resources/room.rs deleted file mode 100644 index 5d51035..0000000 --- a/crates/matrix/src/client/resources/room.rs +++ /dev/null @@ -1,302 +0,0 @@ -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)) - } -} diff --git a/crates/matrix/src/client/resources/session.rs b/crates/matrix/src/client/resources/session.rs deleted file mode 100644 index 1bad079..0000000 --- a/crates/matrix/src/client/resources/session.rs +++ /dev/null @@ -1,46 +0,0 @@ -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)) - } -} diff --git a/crates/matrix/src/client/uiaa.rs b/crates/matrix/src/client/uiaa.rs new file mode 100644 index 0000000..a49a517 --- /dev/null +++ b/crates/matrix/src/client/uiaa.rs @@ -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 Auth { + session: Option, + + kind: AuthType, + + #[serde(flatten)] + data: AuthData, +} + +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/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/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/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..6a7e5be 100644 --- a/crates/matrix/src/lib.rs +++ b/crates/matrix/src/lib.rs @@ -1,26 +1,58 @@ -//! Crate to centralize all Matrix dependencies. +//! 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. //! -//! Reexports `matrix_sdk` and provides implementations on Matrix Admin API. +//! reference: https://docs.ruma.io/ruma_common/api/index.html -mod http; +pub mod admin; +pub mod client; -mod error; +use async_trait::async_trait; +use bytes::{Bytes, BytesMut}; +use ruma_client::HttpClient; -pub mod filter; +pub use ruma_client; +pub use ruma_common; +pub use ruma_events; +pub use ruma_identifiers_validation; -pub use http::Client; +pub type Error = ruma_common::api::error::MatrixError; +pub type HandleError = ruma_client::Error; -/// Implementation on the Administrator API of Matrix -/// -/// Refer: https://matrix-org.github.io/synapse/latest/usage/administration/index.html -pub mod admin; +#[derive(Default, Debug)] +pub struct Client { + inner: reqwest::Client, +} -/// 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; +#[async_trait] +impl HttpClient for Client { + type RequestBody = BytesMut; + type ResponseBody = Bytes; + type Error = reqwest::Error; -/// Ruma re-exports -pub use ruma_common; -pub use ruma_events; + 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")) + } +} diff --git a/crates/server/Cargo.toml b/crates/router/Cargo.toml similarity index 62% rename from crates/server/Cargo.toml rename to crates/router/Cargo.toml index 6eb2dad..c44e461 100644 --- a/crates/server/Cargo.toml +++ b/crates/router/Cargo.toml @@ -1,31 +1,32 @@ [package] -name = "server" +name = "router" version = "0.0.0" edition = "2021" publish = false [[bin]] name = "commune-server" -path = "src/bin/main.rs" +path = "src/main.rs" [lib] -name = "server" +name = "router" path = "src/lib.rs" [dependencies] -# Workspace Dependencies -axum = { workspace = true, features = ["tokio"] } +axum = { workspace = true, features = ["tokio", "macros"] } +axum-extra = { workspace = true, features = ["typed-header"] } anyhow = { workspace = true } -dotenv = { 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"] } 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"] } # Local Dependencies core = { path = "../core" } +matrix = { path = "../matrix" } +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..9756a11 --- /dev/null +++ b/crates/router/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/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..2dbb812 --- /dev/null +++ b/crates/router/src/api/account/avatar.rs @@ -0,0 +1,31 @@ +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..7467469 --- /dev/null +++ b/crates/router/src/api/account/display_name.rs @@ -0,0 +1,30 @@ +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..37d28e4 --- /dev/null +++ b/crates/router/src/api/account/password.rs @@ -0,0 +1,40 @@ +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/router/src/api/relative.rs b/crates/router/src/api/relative.rs new file mode 100644 index 0000000..a5a6a98 --- /dev/null +++ b/crates/router/src/api/relative.rs @@ -0,0 +1,4 @@ +pub mod available; +pub mod login; +pub mod logout; +pub mod register; 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..d8501f8 --- /dev/null +++ b/crates/router/src/api/relative/login.rs @@ -0,0 +1,25 @@ +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::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..2a492da --- /dev/null +++ b/crates/router/src/api/relative/logout.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::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..e9a5323 --- /dev/null +++ b/crates/router/src/api/relative/register.rs @@ -0,0 +1,25 @@ +use axum::{ + response::{IntoResponse, Response}, + Json, +}; +use commune::util::secret::Secret; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, 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 new file mode 100644 index 0000000..a3785e1 --- /dev/null +++ b/crates/router/src/lib.rs @@ -0,0 +1,48 @@ +use std::net::SocketAddr; + +use axum::{ + routing::{get, post, put}, + Router, +}; +use tokio::net::TcpListener; + +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("/login", post(api::relative::login::handler)) + .route("/logout", post(api::relative::logout::handler)) + .nest( + "/account", + Router::new() + .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<()> { + 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 new file mode 100644 index 0000000..7991edc --- /dev/null +++ b/crates/router/src/main.rs @@ -0,0 +1,13 @@ +use anyhow::Result; + +#[tokio::main] +async fn main() -> Result<()> { + 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/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/bin/main.rs b/crates/server/src/bin/main.rs deleted file mode 100644 index 16a31bb..0000000 --- a/crates/server/src/bin/main.rs +++ /dev/null @@ -1,23 +0,0 @@ -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"); - } - - tracing_subscriber::fmt::init(); - - let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); - let tcp = TcpListener::bind(addr).await?; - - tracing::info!("Listening on {}", addr); - - commune_server::serve(tcp).await?; - - Ok(()) -} diff --git a/crates/server/src/config.rs b/crates/server/src/config.rs deleted file mode 100644 index f61bbea..0000000 --- a/crates/server/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/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/server/src/router/middleware/auth.rs b/crates/server/src/router/middleware/auth.rs deleted file mode 100644 index 7c12dd3..0000000 --- a/crates/server/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/server/src/router/middleware/mod.rs b/crates/server/src/router/middleware/mod.rs deleted file mode 100644 index b41b0fe..0000000 --- a/crates/server/src/router/middleware/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod auth; - -pub use auth::{auth, AccessToken}; diff --git a/crates/server/src/router/mod.rs b/crates/server/src/router/mod.rs deleted file mode 100644 index 11ade43..0000000 --- a/crates/server/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/server/src/services.rs b/crates/server/src/services.rs deleted file mode 100644 index 8cae949..0000000 --- a/crates/server/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 f860aae..382d607 100644 --- a/crates/test/Cargo.toml +++ b/crates/test/Cargo.toml @@ -12,18 +12,16 @@ 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"] } +reqwest = { workspace = true, features = ["json"] } serde = { workspace = true } -serde_json = { workspace = true } tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] } +thiserror = { workspace = true } url = { workspace = true } -uuid = { workspace = true, features = ["serde"] } +rand = { workspace = true } tracing = { workspace = true } -tracing-subscriber = { workspace = true, features = ["json"] } +tracing-subscriber = { workspace = true } # Local Dependencies core = { path = "../core" } matrix = { path = "../matrix" } -server = { path = "../server" } +router = { path = "../router" } diff --git a/crates/test/fixtures/synapse/homeserver.yaml b/crates/test/fixtures/synapse/homeserver.yaml index 11fe33b..f6450c8 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 -# vim:ft=yaml +enable_registration: true +enable_registration_without_verification: true 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/test/src/api/relative.rs b/crates/test/src/api/relative.rs new file mode 100644 index 0000000..a5a6a98 --- /dev/null +++ b/crates/test/src/api/relative.rs @@ -0,0 +1,4 @@ +pub mod available; +pub mod login; +pub mod logout; +pub mod register; diff --git a/crates/test/src/api/relative/available.rs b/crates/test/src/api/relative/available.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ 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 new file mode 100644 index 0000000..cdd28de --- /dev/null +++ b/crates/test/src/api/relative/login.rs @@ -0,0 +1,34 @@ +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..fbe5b88 --- /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 new file mode 100644 index 0000000..3407572 --- /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 { + let _ = tracing_subscriber::fmt().try_init(); + + commune::init().await; + + 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) + } + + #[allow(dead_code)] + 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 187ab1b..c84f5e1 100644 --- a/crates/test/src/lib.rs +++ b/crates/test/src/lib.rs @@ -1,11 +1,17 @@ -#[cfg(test)] -mod commune; +// #[cfg(test)] +// mod commune; -#[cfg(test)] -mod tools; +// #[cfg(test)] +// mod tools; + +// #[cfg(test)] +// mod matrix; + +// #[cfg(test)] +// mod server; #[cfg(test)] -mod matrix; +mod api; #[cfg(test)] -mod server; +mod env; 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/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 fcd92e7..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 HttpClient { - 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/docker-compose.yml b/docker-compose.yml index 88c074a..18a4f56 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,11 @@ version: '3' services: - maildev: - image: 'maildev/maildev' + mailcrab: + image: marlonb/mailcrab:latest ports: - - '1080:1080' - '1025:1025' + networks: [default] 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: