diff --git a/Cargo.toml b/Cargo.toml index a021884..5fee52d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,15 +11,17 @@ resolver = "1" [workspace.dependencies] anyhow = "1.0.75" axum = { version = "0.7.4", features = ["tokio"] } +chrono = { version = "0.4.34", features = ["serde"] } dotenv = "0.15.0" http = "0.2.11" -reqwest = { version = "0.11.22", default-features = false, features = ["blocking", "json", "rustls", "stream", "multipart"] } +mime = "0.3.17" 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", "rustls", "multipart"] } serde = "1.0.192" serde_json = "1.0.108" tokio = "1.34.0" tracing = "0.1.40" -tracing-subscriber = "0.3.18" +tracing-subscriber = { version = "0.3.18", features = ["json"] } uuid = { version = "1.6.1", features = ["v4"] } url = "2.4.1" diff --git a/crates/core/src/account/model.rs b/crates/core/src/account/model.rs index bb1e699..cdc5d3e 100644 --- a/crates/core/src/account/model.rs +++ b/crates/core/src/account/model.rs @@ -1,10 +1,10 @@ use url::Url; -use matrix::admin::resources::user_id::UserId; +use matrix::ruma_common::OwnedUserId; #[derive(Debug, Clone)] pub struct Account { - pub user_id: UserId, + pub user_id: OwnedUserId, pub username: String, pub email: String, pub display_name: String, diff --git a/crates/core/src/account/service.rs b/crates/core/src/account/service.rs index fad30a4..d0f99d0 100644 --- a/crates/core/src/account/service.rs +++ b/crates/core/src/account/service.rs @@ -1,16 +1,15 @@ use std::sync::Arc; -use matrix::{admin::resources::user::UserService, client::resources::session::Session}; +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}, - user_id::UserId, - }, + admin::resources::user::{CreateUserBody, ListUsersQuery, LoginAsUserBody, ThreePid}, Client as MatrixAdminClient, }; @@ -117,7 +116,13 @@ impl AccountService { /// 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 = UserId::new(email, self.admin.server_name()); + 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 { @@ -206,7 +211,13 @@ impl AccountService { return Err(AccountErrorCode::EmailTaken(dto.email).into()); } - let user_id = UserId::new(dto.username.clone(), self.admin.server_name().to_string()); + 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 @@ -214,7 +225,7 @@ impl AccountService { UserService::create( &self.admin, - user_id.clone(), + user_id, CreateUserBody { displayname: Some(dto.username), password: dto.password.to_string(), @@ -239,14 +250,14 @@ impl AccountService { Error::Unknown })?; - let matrix_account = UserService::query_user_account(&self.admin, user_id.clone()) + 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: user_id.into(), username: matrix_account.name, email: matrix_account .threepids @@ -264,9 +275,9 @@ impl AccountService { } /// Creates an access token for the given user - pub async fn issue_user_token(&self, user_id: UserId) -> Result { + pub async fn issue_user_token(&self, user_id: &UserId) -> Result { let credentials = - UserService::login_as_user(&self.admin, user_id.clone(), LoginAsUserBody::default()) + UserService::login_as_user(&self.admin, user_id, LoginAsUserBody::default()) .await .map_err(|err| { tracing::error!(?err, ?user_id, "Failed to login as user"); @@ -283,7 +294,7 @@ impl AccountService { 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.clone()) + let matrix_account = UserService::query_user_account(&self.admin, &session.user_id) .await .map_err(|err| { tracing::error!(?err, "Failed to query user account"); diff --git a/crates/core/src/room/service.rs b/crates/core/src/room/service.rs index 6ba2a82..de4ed47 100644 --- a/crates/core/src/room/service.rs +++ b/crates/core/src/room/service.rs @@ -4,7 +4,7 @@ use tracing::instrument; use matrix::{ client::resources::room::{ - CreateRoomBody, Room as MatrixRoom, RoomCreationContent, RoomPreset, + CreateRoomBody, RoomCreationContent, RoomPreset, RoomService as MatrixRoomService, }, Client as MatrixAdminClient, }; @@ -36,7 +36,7 @@ impl RoomService { access_token: &Secret, dto: CreateRoomDto, ) -> Result { - match MatrixRoom::create( + match MatrixRoomService::create( &self.admin, access_token.to_string(), CreateRoomBody { diff --git a/crates/matrix/Cargo.toml b/crates/matrix/Cargo.toml index 574fd64..27796bc 100644 --- a/crates/matrix/Cargo.toml +++ b/crates/matrix/Cargo.toml @@ -17,7 +17,10 @@ ruma-macros = "0.12.0" # Workspace Dependencies anyhow = { workspace = true } +chrono = { workspace = true, features = ["serde"] } +mime = { workspace = true } reqwest = { workspace = true, features = ["json"] } serde = { workspace = true } +tokio = { workspace = true } tracing = { workspace = true } url = { workspace = true, features = ["serde"] } diff --git a/crates/matrix/src/admin/resources/room.rs b/crates/matrix/src/admin/resources/room.rs index a603f23..9936111 100644 --- a/crates/matrix/src/admin/resources/room.rs +++ b/crates/matrix/src/admin/resources/room.rs @@ -9,7 +9,7 @@ use ruma_events::{AnyMessageLikeEvent, AnyStateEvent, AnyTimelineEvent}; use serde::{Deserialize, Serialize}; use tracing::instrument; -use crate::{error::MatrixError, event_filter::RoomEventFilter, http::Client}; +use crate::{error::MatrixError, filter::RoomEventFilter, http::Client}; #[derive(Default)] pub struct RoomService; @@ -444,7 +444,7 @@ impl RoomService { } } -#[derive(Default, Debug, Serialize)] +#[derive(Debug, Default, Clone, Serialize)] pub enum Direction { #[serde(rename = "f")] #[default] diff --git a/crates/matrix/src/admin/resources/user.rs b/crates/matrix/src/admin/resources/user.rs index 03bd5a7..e5fc0da 100644 --- a/crates/matrix/src/admin/resources/user.rs +++ b/crates/matrix/src/admin/resources/user.rs @@ -4,14 +4,13 @@ //! 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}; -use super::user_id::UserId; - #[derive(Default)] pub struct UserService; @@ -149,7 +148,7 @@ impl UserService { /// /// 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 { + pub async fn query_user_account(client: &Client, user_id: &UserId) -> Result { let resp = client .get(format!( "/_synapse/admin/v2/users/{user_id}", @@ -174,7 +173,7 @@ impl UserService { /// /// 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 { + 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), @@ -212,7 +211,7 @@ impl UserService { /// /// 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 { + 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), @@ -246,7 +245,7 @@ impl UserService { #[instrument(skip(client))] pub async fn login_as_user( client: &Client, - user_id: UserId, + user_id: &UserId, body: LoginAsUserBody, ) -> Result { let resp = client diff --git a/crates/matrix/src/client/resources/events.rs b/crates/matrix/src/client/resources/events.rs index c377141..2953b2b 100644 --- a/crates/matrix/src/client/resources/events.rs +++ b/crates/matrix/src/client/resources/events.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use ruma_common::{serde::Raw, EventId, OwnedEventId, RoomId, TransactionId}; +use ruma_common::{serde::Raw, EventId, OwnedEventId, OwnedTransactionId, RoomId}; use ruma_events::{ relation::RelationType, AnyMessageLikeEvent, AnyStateEvent, AnyStateEventContent, @@ -9,25 +9,39 @@ use ruma_events::{ use serde::{de::DeserializeOwned, Deserialize, Serialize}; use tracing::instrument; -use crate::{admin::resources::room::Direction, event_filter::RoomEventFilter, Client}; +use crate::{admin::resources::room::Direction, error::MatrixError, Client}; -pub struct Events; +pub struct EventsService; -#[derive(Debug, Default, Serialize)] +#[derive(Debug, Default, Clone, Serialize)] pub struct GetMessagesQuery { + #[serde(skip_serializing_if = "Option::is_none")] pub from: Option, + + #[serde(skip_serializing_if = "Option::is_none")] pub to: Option, + + #[serde(skip_serializing_if = "Option::is_none")] pub limit: Option, - pub dir: Option, - pub filter: Option, + + pub dir: Direction, + + #[serde(skip_serializing_if = "String::is_empty")] + pub filter: String, } -#[derive(Debug, Default, Serialize)] +#[derive(Debug, Default, Clone, Serialize)] pub struct GetRelationsQuery { + #[serde(skip_serializing_if = "Option::is_none")] pub from: Option, + + #[serde(skip_serializing_if = "Option::is_none")] pub to: Option, + + #[serde(skip_serializing_if = "Option::is_none")] pub limit: Option, - pub dir: Option, + + pub dir: Direction, } #[derive(Debug, Deserialize)] @@ -40,7 +54,7 @@ pub struct GetMessagesResponse { #[derive(Debug, Deserialize)] #[serde(transparent)] -pub struct GetStateResponse(Vec>); +pub struct GetStateResponse(pub Vec>); #[derive(Debug, Deserialize)] pub struct GetRelationsResponse { @@ -70,7 +84,7 @@ pub struct SendRedactionResponse { pub event_id: OwnedEventId, } -impl Events { +impl EventsService { #[instrument(skip(client, access_token))] pub async fn get_event( client: &Client, @@ -103,10 +117,13 @@ impl Events { tmp.set_token(access_token)?; let resp = tmp - .get(format!( - "/_matrix/client/v3/rooms/{room_id}/messages", - room_id = room_id, - )) + .get_query( + format!( + "/_matrix/client/v3/rooms/{room_id}/messages", + room_id = room_id, + ), + &query, + ) .await?; Ok(resp.json().await?) @@ -199,7 +216,7 @@ impl Events { client: &Client, access_token: impl Into, room_id: &RoomId, - txn_id: &TransactionId, + txn_id: OwnedTransactionId, body: T, ) -> Result { let mut tmp = (*client).clone(); @@ -217,7 +234,13 @@ impl Events { ) .await?; - Ok(resp.json().await?) + if resp.status().is_success() { + return Ok(resp.json().await?); + } + + let error = resp.json::().await?; + + Err(anyhow::anyhow!(error.error)) } #[instrument(skip(client, access_token, body))] @@ -243,7 +266,13 @@ impl Events { let resp = tmp.put_json(path, &body).await?; - Ok(resp.json().await?) + if resp.status().is_success() { + return Ok(resp.json().await?); + } + + let error = resp.json::().await?; + + Err(anyhow::anyhow!(error.error)) } #[instrument(skip(client, access_token, body))] @@ -252,7 +281,7 @@ impl Events { access_token: impl Into, room_id: &RoomId, event_id: &EventId, - txn_id: &TransactionId, + txn_id: OwnedTransactionId, body: SendRedactionBody, ) -> Result { let mut tmp = (*client).clone(); @@ -270,6 +299,12 @@ impl Events { ) .await?; - Ok(resp.json().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 index 7bfe4b8..43420e9 100644 --- a/crates/matrix/src/client/resources/mod.rs +++ b/crates/matrix/src/client/resources/mod.rs @@ -1,5 +1,6 @@ pub mod error; pub mod events; pub mod login; +pub mod mxc; pub mod room; pub mod session; diff --git a/crates/matrix/src/client/resources/mxc.rs b/crates/matrix/src/client/resources/mxc.rs new file mode 100644 index 0000000..7dc669a --- /dev/null +++ b/crates/matrix/src/client/resources/mxc.rs @@ -0,0 +1,184 @@ +use std::str::FromStr; + +use anyhow::Result; +use mime::Mime; +use ruma_common::{MxcUri, OwnedMxcUri}; +use serde::{de, Deserialize, Deserializer, Serialize}; +use tracing::instrument; + +use chrono::{serde::ts_microseconds_option, DateTime, Utc}; + +use crate::error::MatrixError; + +fn parse_mime_opt<'de, D>(d: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + Option::<&str>::deserialize(d)? + .map(::from_str) + .transpose() + .map_err(de::Error::custom) +} + +#[derive(Debug, Serialize)] +pub struct GetPreviewUrlQuery { + pub url: url::Url, + pub ts: DateTime, +} + +#[derive(Debug, Deserialize)] +pub struct CreateMxcUriResponse { + pub content_uri: String, + + #[serde(with = "ts_microseconds_option")] + pub unused_expires_at: Option>, +} + +#[derive(Debug, Deserialize)] +pub struct GetPreviewUrlResponse { + #[serde(rename = "matrix:image_size")] + pub image_size: Option, + + #[serde(rename = "og:description")] + pub description: Option, + + #[serde(rename = "og:image")] + pub image: Option, + + #[serde(rename = "og:image:height")] + pub height: Option, + + #[serde(rename = "og:image:width")] + pub width: Option, + + #[serde(rename = "og:image:type", deserialize_with = "parse_mime_opt")] + pub kind: Option, + + #[serde(rename = "og:title")] + pub title: Option, +} + +#[derive(Debug, Deserialize)] +pub struct GetConfigResponse { + #[serde(rename = "m.upload.size")] + pub upload_size: Option, +} + +#[derive(Debug, Serialize)] +pub enum ResizeMethod { + Crop, + Scale, +} + +pub struct MxcService; + +#[derive(Debug, Deserialize)] +pub struct MxcError { + #[serde(flatten)] + pub inner: MatrixError, + + pub retry_after_ms: u64, +} + +impl MxcService { + /// Creates a new `MxcUri`, independently of the content being uploaded + /// + /// Refer: https://spec.matrix.org/v1.9/client-server-api/#post_matrixmediav1create + #[instrument(skip(client, access_token))] + pub async fn create( + client: &crate::http::Client, + access_token: impl Into, + ) -> Result { + let mut tmp = (*client).clone(); + tmp.set_token(access_token)?; + + let resp = tmp.post("/_matrix/media/v1/create").await?; + + if resp.status().is_success() { + return Ok(resp.json().await?); + } + + let error = resp.json::().await?; + + Err(anyhow::anyhow!(error.inner.error)) + } + + /// Retrieve the configuration of the content repository + /// + /// Refer: https://spec.matrix.org/v1.9/client-server-api/#get_matrixmediav3config + #[instrument(skip(client, access_token))] + pub async fn get_config( + client: &crate::http::Client, + access_token: impl Into, + ) -> Result { + let mut tmp = (*client).clone(); + tmp.set_token(access_token)?; + + let resp = tmp.get("/_matrix/media/v3/config").await?; + + if resp.status().is_success() { + return Ok(resp.json().await?); + } + + let error = resp.json::().await?; + + Err(anyhow::anyhow!(error.inner.error)) + } + + /// Retrieve a URL to download content from the content repository, + /// optionally replacing the name of the file. + /// + /// Refer: https://spec.matrix.org/v1.9/client-server-api/#get_matrixmediav3downloadservernamemediaid + #[instrument(skip(client, access_token))] + pub async fn get_download_url( + client: &crate::http::Client, + access_token: impl Into, + mxc_uri: &MxcUri, + mut base_url: url::Url, + file_name: Option, + ) -> Result { + let mut tmp = (*client).clone(); + tmp.set_token(access_token)?; + + let (server_name, media_id) = mxc_uri.parts().unwrap(); + + let mut path = format!( + "/_matrix/media/v3/download/{server_name}/{media_id}", + server_name = server_name, + media_id = media_id, + ); + + if let Some(file_name) = file_name { + path.push_str(&format!("/{file_name}", file_name = file_name)) + } + + base_url.set_path(&path); + + Ok(base_url) + } + + /// + /// + /// Refer: https://spec.matrix.org/v1.9/client-server-api/#get_matrixmediav3preview_url + #[instrument(skip(client, access_token))] + pub async fn get_preview( + client: &crate::http::Client, + access_token: impl Into, + query: GetPreviewUrlQuery, + ) -> Result { + let mut tmp = (*client).clone(); + tmp.set_token(access_token)?; + + let resp = tmp + .get_query("/_matrix/media/v3/preview_url".to_string(), &query) + .await?; + + if resp.status().is_success() { + return Ok(resp.json().await?); + } + + let error = resp.json::().await?; + + Err(anyhow::anyhow!(error.inner.error)) + } +} diff --git a/crates/matrix/src/client/resources/room.rs b/crates/matrix/src/client/resources/room.rs index a14a4df..5d51035 100644 --- a/crates/matrix/src/client/resources/room.rs +++ b/crates/matrix/src/client/resources/room.rs @@ -4,6 +4,8 @@ use ruma_events::{room::power_levels::RoomPowerLevelsEventContent, AnyInitialSta use serde::{Deserialize, Serialize}; use tracing::instrument; +use crate::error::MatrixError; + #[derive(Debug, Serialize)] #[serde(rename_all = "snake_case")] pub enum RoomPreset { @@ -103,15 +105,9 @@ pub struct ForgetRoomResponse {} #[derive(Debug, Deserialize)] pub struct RoomKickOrBanResponse {} -#[derive(Debug, Deserialize)] -pub struct MatrixError { - pub errcode: String, - pub error: String, -} - -pub struct Room; +pub struct RoomService; -impl Room { +impl RoomService { /// Create a new room with various configuration options. /// /// Refer: https://spec.matrix.org/v1.9/client-server-api/#creation diff --git a/crates/matrix/src/client/resources/session.rs b/crates/matrix/src/client/resources/session.rs index c8be158..1bad079 100644 --- a/crates/matrix/src/client/resources/session.rs +++ b/crates/matrix/src/client/resources/session.rs @@ -1,14 +1,15 @@ use anyhow::Result; +use ruma_common::OwnedUserId; use serde::{Deserialize, Serialize}; use tracing::instrument; -use crate::{admin::resources::user_id::UserId, error::MatrixError}; +use crate::error::MatrixError; #[derive(Debug, Serialize, Deserialize)] pub struct Session { pub device_id: String, pub is_guest: bool, - pub user_id: UserId, + pub user_id: OwnedUserId, } impl Session { diff --git a/crates/matrix/src/event_filter.rs b/crates/matrix/src/event_filter.rs deleted file mode 100644 index 35e30b8..0000000 --- a/crates/matrix/src/event_filter.rs +++ /dev/null @@ -1,33 +0,0 @@ -use ruma_common::{OwnedRoomId, OwnedUserId}; -use serde::Serialize; - -#[derive(Default, Debug, Serialize)] -pub struct RoomEventFilter { - #[serde(skip_serializing_if = "<[_]>::is_empty")] - pub not_types: Vec, - - #[serde(skip_serializing_if = "<[_]>::is_empty")] - pub not_rooms: Vec, - - #[serde(skip_serializing_if = "Option::is_none")] - pub limit: Option, - - #[serde(skip_serializing_if = "<[_]>::is_empty")] - pub rooms: Vec, - - #[serde(skip_serializing_if = "<[_]>::is_empty")] - pub not_senders: 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")] - pub include_urls: Option, - - pub lazy_load_members: bool, - - pub unread_thread_notifications: bool, -} diff --git a/crates/matrix/src/filter.rs b/crates/matrix/src/filter.rs new file mode 100644 index 0000000..3d4e398 --- /dev/null +++ b/crates/matrix/src/filter.rs @@ -0,0 +1,212 @@ +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 index c697d9a..22db5c8 100644 --- a/crates/matrix/src/http.rs +++ b/crates/matrix/src/http.rs @@ -47,6 +47,11 @@ impl Client { 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()?; @@ -67,7 +72,7 @@ impl Client { Ok(response) } - pub async fn post_json(&self, path: impl AsRef, body: &T) -> Result + pub async fn put_json(&self, path: impl AsRef, body: &T) -> Result where T: Serialize, { @@ -75,7 +80,7 @@ impl Client { let headers = self.build_headers()?; let resp = self .client - .post(url) + .put(url) .json(body) .headers(headers) .send() @@ -84,7 +89,15 @@ impl Client { Ok(resp) } - pub async fn put_json(&self, path: impl AsRef, body: &T) -> Result + 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, { @@ -92,7 +105,7 @@ impl Client { let headers = self.build_headers()?; let resp = self .client - .put(url) + .post(url) .json(body) .headers(headers) .send() diff --git a/crates/matrix/src/lib.rs b/crates/matrix/src/lib.rs index e5458a1..0afb5f3 100644 --- a/crates/matrix/src/lib.rs +++ b/crates/matrix/src/lib.rs @@ -6,7 +6,7 @@ mod http; mod error; -mod event_filter; +pub mod filter; pub use http::Client; diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index cd5f7c1..3dae89d 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -31,3 +31,5 @@ uuid = { workspace = true, features= ["serde"] } # Local Dependencies core = { path = "../core" } +chrono = { version = "0.4.34", features = ["serde"] } +mime = { git = "https://github.com/hyperium/mime", version = "0.4.0-a.0" } diff --git a/crates/server/src/router/api/v1/account/root.rs b/crates/server/src/router/api/v1/account/root.rs index 0a17f80..6358ded 100644 --- a/crates/server/src/router/api/v1/account/root.rs +++ b/crates/server/src/router/api/v1/account/root.rs @@ -25,7 +25,7 @@ pub async fn handler( let access_token = services .commune .account - .issue_user_token(account.user_id.clone()) + .issue_user_token(&account.user_id) .await .unwrap(); let payload = AccountRegisterResponse { diff --git a/crates/test/Cargo.toml b/crates/test/Cargo.toml index 71411f1..9edcc22 100644 --- a/crates/test/Cargo.toml +++ b/crates/test/Cargo.toml @@ -10,6 +10,7 @@ path = "src/lib.rs" [dependencies] fake = { version = "2.9.2", features = ["derive"] } +futures = "0.3.30" rand = "0.8.5" scraper = "0.18.1" @@ -20,11 +21,12 @@ dotenv = { workspace = true } reqwest = { workspace = true } openssl = { workspace = true, features = ["vendored"] } serde = { workspace = true } +serde_json = { workspace = true } tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] } url = { workspace = true } uuid = { workspace = true, features = ["serde"] } tracing = { workspace = true } -tracing-subscriber = { workspace = true } +tracing-subscriber = { workspace = true, features = ["json"] } # Local Dependencies core = { path = "../core" } diff --git a/crates/test/src/matrix/events.rs b/crates/test/src/matrix/events.rs new file mode 100644 index 0000000..36a9479 --- /dev/null +++ b/crates/test/src/matrix/events.rs @@ -0,0 +1,476 @@ +#[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 index c4a8b79..baab593 100644 --- a/crates/test/src/matrix/mod.rs +++ b/crates/test/src/matrix/mod.rs @@ -1,3 +1,4 @@ +mod events; mod room_admin; mod room_client; mod shared_token_registration; diff --git a/crates/test/src/matrix/room_admin.rs b/crates/test/src/matrix/room_admin.rs index cd7bd73..573d5d1 100644 --- a/crates/test/src/matrix/room_admin.rs +++ b/crates/test/src/matrix/room_admin.rs @@ -1,19 +1,17 @@ -use matrix::admin::resources::room::{ - ListRoomQuery, ListRoomResponse, RoomService as AdminRoomService, -}; - #[cfg(test)] mod tests { + use std::{thread, time::Duration}; + + use futures::TryFutureExt; use matrix::{ - admin::resources::room::{MessagesQuery, OrderBy}, + admin::resources::room::{ListRoomQuery, MessagesQuery, RoomService as AdminRoomService}, ruma_common::{RoomId, ServerName}, }; + use tokio::sync::OnceCell; use crate::matrix::util::{self, Test}; - use super::*; - static TEST: OnceCell = OnceCell::const_new(); #[tokio::test] @@ -21,63 +19,52 @@ mod tests { let Test { samples, server_name, - client, + admin, } = TEST.get_or_init(util::init).await; - let ListRoomResponse { rooms: resp, .. } = - AdminRoomService::get_all(client, ListRoomQuery::default()) - .await - .unwrap(); + // TODO + thread::sleep(Duration::from_secs(5)); + + let resp: Vec<_> = AdminRoomService::get_all(admin, ListRoomQuery::default()) + .map_ok(|resp| resp.rooms) + .await + .unwrap(); + + dbg!(samples.iter().map(|s| s.owner()).collect::>()); assert_eq!( samples .iter() - .map(|s| Some(format!("{id}-room-name", id = s.user_id.localpart()))) + .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::>(), + resp.iter().map(|r| r.name.clone()).collect::>() ); assert_eq!( samples .iter() - .map(|s| format!("#{id}-room-alias:{server_name}", id = s.user_id.localpart())) + .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().unwrap()) - .collect::>(), - ); - assert_eq!( - samples.iter().map(|s| &s.room_id).collect::>(), - resp.iter().map(|r| &r.room_id).collect::>(), - ); - - let ListRoomResponse { rooms: resp, .. } = AdminRoomService::get_all( - client, - ListRoomQuery { - order_by: OrderBy::Creator, - ..Default::default() - }, - ) - .await - .unwrap(); - - assert_eq!( - samples - .iter() - .map(|s| s.user_id.to_string()) - .collect::>(), - resp.iter() - .map(|r| r.creator.clone().unwrap()) - .collect::>(), + .map(|r| r.canonical_alias.clone()) + .collect::>() ); } #[tokio::test] #[should_panic] async fn get_all_rooms_err() { - let Test { client, .. } = TEST.get_or_init(util::init).await; + let Test { admin, .. } = TEST.get_or_init(util::init).await; let _ = AdminRoomService::get_all( - client, + admin, ListRoomQuery { from: Some(u64::MAX), ..Default::default() @@ -92,37 +79,32 @@ mod tests { let Test { samples, server_name, - client, + 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(client, &rand.room_id) + 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!("{}-room-name", rand.user_id.localpart())), - resp.name - ); - assert_eq!( - Some(format!( - "#{}-room-alias:{server_name}", - rand.user_id.localpart() - )), + Some(format!("#{id}-room-alias:{server_name}",)), resp.canonical_alias, ); - assert_eq!(Some(rand.user_id.to_string()), resp.creator); + assert_eq!(Some(user_id.to_string()), resp.creator); assert_eq!( - Some(format!("{}-room-topic", rand.user_id.localpart())), + 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.public); assert!(resp.room_type.is_none()); } @@ -130,13 +112,11 @@ mod tests { #[should_panic] async fn get_room_details_err() { let Test { - server_name, - client, - .. + server_name, admin, .. } = TEST.get_or_init(util::init).await; let _ = AdminRoomService::get_one( - client, + admin, &RoomId::new(&ServerName::parse(server_name).unwrap()), ) .await @@ -145,15 +125,13 @@ mod tests { #[tokio::test] async fn get_room_events() { - let Test { - samples, client, .. - } = TEST.get_or_init(util::init).await; + 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( - client, + admin, &rand.room_id, // no idea what the type is MessagesQuery { @@ -175,13 +153,11 @@ mod tests { #[should_panic] async fn get_room_events_err() { let Test { - server_name, - client, - .. + server_name, admin, .. } = TEST.get_or_init(util::init).await; let _ = AdminRoomService::get_room_events( - client, + admin, <&RoomId>::try_from(server_name.as_str()).unwrap(), MessagesQuery { from: "".into(), @@ -197,14 +173,12 @@ mod tests { #[tokio::test] async fn get_state_events() { - let Test { - samples, client, .. - } = TEST.get_or_init(util::init).await; + 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(client, &rand.room_id) + let resp = AdminRoomService::get_state(admin, &rand.room_id) .await .unwrap(); @@ -218,44 +192,39 @@ mod tests { #[should_panic] async fn get_state_events_err() { let Test { - server_name, - client, - .. + server_name, admin, .. } = TEST.get_or_init(util::init).await; let _ = - AdminRoomService::get_state(client, <&RoomId>::try_from(server_name.as_str()).unwrap()) + AdminRoomService::get_state(admin, <&RoomId>::try_from(server_name.as_str()).unwrap()) .await .unwrap(); } #[tokio::test] async fn get_members() { - let Test { - samples, client, .. - } = TEST.get_or_init(util::init).await; + 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(client, &rand.room_id) + let resp = AdminRoomService::get_members(admin, &rand.room_id) .await .unwrap(); - assert_eq!(resp.members, vec![rand.user_id.to_string()]); + assert_eq!(resp.members, vec![owner_id.to_string()]); } #[tokio::test] #[should_panic] async fn get_members_err() { let Test { - server_name, - client, - .. + server_name, admin, .. } = TEST.get_or_init(util::init).await; let _ = AdminRoomService::get_members( - client, + admin, <&RoomId>::try_from(server_name.as_str()).unwrap(), ) .await diff --git a/crates/test/src/matrix/room_client.rs b/crates/test/src/matrix/room_client.rs index 7a636fd..0570aa6 100644 --- a/crates/test/src/matrix/room_client.rs +++ b/crates/test/src/matrix/room_client.rs @@ -1,131 +1,68 @@ -use matrix::ruma_common::{OwnedRoomId, OwnedUserId}; - #[cfg(test)] mod tests { - + use futures::{future, FutureExt}; use matrix::{ admin::resources::room::RoomService as AdminRoomService, - client::resources::room::{ - ForgetRoomBody, JoinRoomBody, JoinRoomResponse, LeaveRoomBody, Room as RoomService, - RoomKickOrBanBody, - }, - ruma_common::OwnedRoomOrAliasId, + client::resources::room::{ForgetRoomBody, LeaveRoomBody, RoomKickOrBanBody, RoomService}, }; use tokio::sync::OnceCell; - use crate::matrix::util::{self, Test}; + use crate::matrix::util::{self, join_helper, Test}; - use super::*; static TEST: OnceCell = OnceCell::const_new(); - async fn join_helper() -> Vec<( - OwnedRoomId, - Vec, - Vec>, - )> { - let Test { - samples, client, .. - } = TEST.get_or_init(util::init).await; - - let mut result = Vec::with_capacity(samples.len()); - - for sample in samples { - let client = client.clone(); - - let guests: Vec<_> = samples - .iter() - .filter(|g| g.user_id != sample.user_id) - .collect(); - let mut resps = Vec::with_capacity(guests.len()); - - for guest in guests.iter() { - let client = client.clone(); - - let resp = RoomService::join( - &client, - guest.access_token.clone(), - &OwnedRoomOrAliasId::from(sample.room_id.clone()), - JoinRoomBody::default(), - ) - .await; - - resps.push(resp); - } - - result.push(( - sample.room_id.clone(), - guests.iter().map(|g| g.user_id.clone()).collect(), - resps, - )); - } - - result - } - #[tokio::test] async fn join_all_rooms() { - let Test { client: admin, .. } = TEST.get_or_init(util::init).await; + let Test { admin, samples, .. } = TEST.get_or_init(util::init).await; + + let mut client = admin.clone(); + client.clear_token(); // first join - let result = join_helper().await; - let rooms: Vec<_> = result.iter().map(|r| &r.0).collect(); - tracing::info!(?rooms, "joining all guests"); + 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) + let mut resp = AdminRoomService::get_members(&admin, room_id) .await .unwrap(); resp.members.sort(); - assert!(resps.iter().all(|r| r.is_ok())); - let resps: Vec<_> = resps.into_iter().flatten().collect(); - - assert!(resps.iter().all(|r| r.room_id == *room_id)); - - for guest in guests { - assert!(resp.members.contains(&guest)); - } + 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, client, .. - } = TEST.get_or_init(util::init).await; - - let admin = client.clone(); + let Test { samples, admin, .. } = TEST.get_or_init(util::init).await; - let mut result = Vec::with_capacity(samples.len()); + let mut client = admin.clone(); + client.clear_token(); for sample in samples { - let client = client.clone(); - - let guests: Vec<_> = samples - .iter() - .filter(|g| g.user_id != sample.user_id) - .collect(); - - for guest in guests { - let client = client.clone(); - + for (_, access_token) in sample.guests() { RoomService::leave( &client, - guest.access_token.clone(), + access_token, &sample.room_id, LeaveRoomBody::default(), ) .await .unwrap(); } - - result.push(sample.room_id.clone()); } // check whether all guests left the room - for room_id in result { - let resp = AdminRoomService::get_members(&admin, &room_id) + for sample in samples { + let resp = AdminRoomService::get_members(&admin, &sample.room_id) .await .unwrap(); @@ -133,8 +70,9 @@ mod tests { assert_eq!( &[samples .iter() - .find(|s| s.room_id == room_id) - .map(|s| s.user_id.clone()) + .find(|s| s.room_id == sample.room_id) + .map(|s| s.owner()) + .map(|(id, _)| id.to_owned()) .unwrap()], resp.members.as_slice() ); @@ -143,24 +81,16 @@ mod tests { #[tokio::test] async fn forget_all_rooms() { - let Test { - samples, client, .. - } = TEST.get_or_init(util::init).await; + let Test { samples, admin, .. } = TEST.get_or_init(util::init).await; - for sample in samples { - let client = client.clone(); - - let guests: Vec<_> = samples - .iter() - .filter(|g| g.user_id != sample.user_id) - .collect(); - - for guest in guests { - let client = client.clone(); + let mut client = admin.clone(); + client.clear_token(); + for sample in samples { + for (_, access_token) in sample.guests() { RoomService::forget( &client, - guest.access_token.clone(), + access_token, &sample.room_id, ForgetRoomBody::default(), ) @@ -170,7 +100,6 @@ mod tests { } // check whether all guests are still not present anymore the room - let admin = client.clone(); for sample in samples { let room_id = &sample.room_id; @@ -183,7 +112,8 @@ mod tests { &[samples .iter() .find(|s| &s.room_id == room_id) - .map(|s| s.user_id.clone()) + .map(|s| s.owner()) + .map(|(id, _)| id.to_owned()) .unwrap()], resp.members.as_slice() ); @@ -191,16 +121,12 @@ mod tests { // confirm a room can't be forgotten if we didn't leave first for sample in samples { - let client = client.clone(); let room_id = &sample.room_id; + let (_, access_token) = sample.owner(); - let resp = RoomService::forget( - &client, - sample.access_token.clone(), - room_id, - ForgetRoomBody::default(), - ) - .await; + let resp = + RoomService::forget(&client, access_token, room_id, ForgetRoomBody::default()) + .await; assert!(resp.is_err()); } @@ -208,52 +134,41 @@ mod tests { #[tokio::test] async fn kick_all_guests() { - let Test { - samples, client, .. - } = TEST.get_or_init(util::init).await; + let Test { samples, admin, .. } = TEST.get_or_init(util::init).await; + + let mut client = admin.clone(); + client.clear_token(); // second join - let result = join_helper().await; - let rooms: Vec<_> = result.iter().map(|r| &r.0).collect(); - tracing::info!(?rooms, "joining all guests"); + 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 - let admin = client.clone(); - for (room_id, guests, resps) in result.iter() { + 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(|r| r.is_ok())); - let resps: Vec<_> = resps.iter().flatten().collect(); - - assert!(resps.iter().all(|r| r.room_id == *room_id)); - - for guest in guests { - assert!(resp.members.contains(guest)); - } + 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 client = client.clone(); - let room_id = &sample.room_id; - - let guests: Vec<_> = samples - .iter() - .filter(|g| g.user_id != sample.user_id) - .collect(); - - for guest in guests { - let client = client.clone(); - + for (user_id, access_token) in sample.guests() { RoomService::kick( &client, - guest.access_token.clone(), - room_id, + access_token, + &sample.room_id, RoomKickOrBanBody { reason: Default::default(), - user_id: guest.user_id.clone(), + user_id: user_id.clone(), }, ) .await @@ -262,8 +177,8 @@ mod tests { } // check whether all guests left the room - for (room_id, _, _) in result { - let resp = AdminRoomService::get_members(&admin, &room_id) + for sample in samples { + let resp = AdminRoomService::get_members(&admin, &sample.room_id) .await .unwrap(); @@ -271,8 +186,9 @@ mod tests { assert_eq!( &[samples .iter() - .find(|s| s.room_id == room_id) - .map(|s| s.user_id.clone()) + .find(|s| s.room_id == sample.room_id) + .map(|s| s.owner()) + .map(|(id, _)| id.to_owned()) .unwrap()], resp.members.as_slice() ); @@ -281,52 +197,43 @@ mod tests { #[tokio::test] async fn ban_all_guests() { - let Test { - samples, client, .. - } = TEST.get_or_init(util::init).await; + let Test { samples, admin, .. } = TEST.get_or_init(util::init).await; + + let mut client = admin.clone(); + client.clear_token(); // third join - let result = join_helper().await; - let rooms: Vec<_> = result.iter().map(|r| &r.0).collect(); - tracing::info!(?rooms, "joining all guests"); + 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 - let admin = client.clone(); - for (room_id, guests, resps) in result.iter() { + 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(|r| r.is_ok())); - let resps: Vec<_> = resps.iter().flatten().collect(); - - assert!(resps.iter().all(|r| r.room_id == *room_id)); - - for guest in guests { - assert!(resp.members.contains(guest)); - } + 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 client = client.clone(); - - let guests: Vec<_> = samples - .iter() - .filter(|g| g.user_id != sample.user_id) - .collect(); - - for guest in guests { - let client = client.clone(); - let room_id = &sample.room_id; + let (_, owner_token) = sample.owner(); + for (user_id, _) in sample.guests() { RoomService::ban( &client, - sample.access_token.clone(), - room_id, + owner_token, + &sample.room_id, RoomKickOrBanBody { reason: Default::default(), - user_id: guest.user_id.clone(), + user_id: user_id.clone(), }, ) .await @@ -335,9 +242,13 @@ mod tests { } // fourth join - let result = join_helper().await; - let rooms: Vec<_> = result.iter().map(|r| &r.0).collect(); - tracing::info!(?rooms, "joining all guests"); + 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 @@ -350,8 +261,9 @@ mod tests { assert_eq!( &[samples .iter() - .find(|s| s.room_id == room_id) - .map(|s| s.user_id.clone()) + .find(|s| &s.room_id == room_id) + .map(|s| s.owner()) + .map(|(id, _)| id.to_owned()) .unwrap()], resp.members.as_slice() ); @@ -360,24 +272,16 @@ mod tests { } for sample in samples { - let client = client.clone(); - - let guests: Vec<_> = samples - .iter() - .filter(|g| g.user_id != sample.user_id) - .collect(); - - for guest in guests { - let client = client.clone(); - let room_id = &sample.room_id; + let (_, owner_token) = sample.owner(); + for (user_id, _) in sample.guests() { RoomService::unban( &client, - sample.access_token.clone(), - room_id, + owner_token, + &sample.room_id, RoomKickOrBanBody { reason: Default::default(), - user_id: guest.user_id.clone(), + user_id: user_id.clone(), }, ) .await diff --git a/crates/test/src/matrix/util.rs b/crates/test/src/matrix/util.rs index b7609dd..1ba908e 100644 --- a/crates/test/src/matrix/util.rs +++ b/crates/test/src/matrix/util.rs @@ -1,15 +1,17 @@ -use std::str::FromStr; - +use anyhow::Result; +use futures::{future, TryFutureExt}; use matrix::{ admin::resources::{ room::{DeleteQuery, ListRoomQuery, ListRoomResponse, RoomService as AdminRoomService}, - user::{ - CreateUserBody, LoginAsUserBody, LoginAsUserResponse, UserService as AdminUserService, + user::{CreateUserBody, UserService as AdminUserService}, + }, + client::resources::{ + login::Login, + room::{ + CreateRoomBody, JoinRoomBody, JoinRoomResponse, RoomPreset, RoomService, RoomVisibility, }, - user_id::UserId, }, - client::resources::room::{CreateRoomBody, Room, RoomPreset}, - ruma_common::{OwnedRoomId, OwnedUserId}, + ruma_common::{OwnedRoomId, OwnedUserId, RoomId}, Client, }; @@ -20,80 +22,87 @@ use crate::tools::environment::Environment; pub struct Test { pub samples: Vec, pub server_name: String, - pub client: Client, + pub admin: Client, } pub struct Sample { - pub user_id: OwnedUserId, + pub user_ids: Vec, pub room_id: OwnedRoomId, - pub access_token: String, + 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 mut result = Vec::with_capacity(amount); - - for i in 0..amount { - let user_id = UserId::new(format!("{seed}-{i}"), server_name.clone()); - let password = "verysecure".to_owned(); - - let body = CreateUserBody { - password, - logout_devices: false, - displayname: None, - avatar_url: None, - threepids: vec![], - external_ids: vec![], - admin: false, - deactivated: false, - user_type: None, - locked: false, - }; - AdminUserService::create(&client, user_id.clone(), body) - .await - .unwrap(); - - let body = LoginAsUserBody::default(); - let LoginAsUserResponse { access_token } = - AdminUserService::login_as_user(&client, user_id.clone(), body) - .await - .unwrap(); - - let user_id = OwnedUserId::from_str(&user_id.to_string()).unwrap(); - result.push((user_id, access_token)); - } + let users: Vec<_> = (0..amount) + .map(|i| OwnedUserId::try_from(format!("@{seed}-{room}-{i}:{}", server_name)).unwrap()) + .collect(); - result + 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, accounts: &[(OwnedUserId, String)]) -> Vec { - let mut result = Vec::with_capacity(accounts.len()); - - for (user_id, access_token) in accounts { - let id = user_id.localpart(); - let preset = Some(RoomPreset::PublicChat); - - let name = format!("{id}-room-name"); - let topic = format!("{id}-room-topic"); - let room_alias_name = format!("{id}-room-alias"); +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]; - let body = CreateRoomBody { - name, - topic, - room_alias_name, - preset, - ..Default::default() - }; - let resp = Room::create(client, access_token, body).await.unwrap(); - - result.push(resp.room_id); - } - - result + 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) { @@ -101,14 +110,10 @@ async fn remove_rooms(client: &Client) { AdminRoomService::get_all(client, ListRoomQuery::default()) .await .unwrap(); - let room_names: Vec<_> = rooms - .iter() - .map(|r| r.name.clone().unwrap_or(r.room_id.to_string())) - .collect(); - tracing::info!(?room_names, "purging all rooms!"); + tracing::info!("purging all rooms!"); - for room in rooms { + future::try_join_all(rooms.iter().map(|room| { AdminRoomService::delete_room( client, room.room_id.as_ref(), @@ -118,49 +123,77 @@ async fn remove_rooms(client: &Client) { purge: true, }, ) - .await - .unwrap(); - } + })) + .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 client = env.client.clone(); + let mut admin = env.client.clone(); + + admin.set_token(admin_token).unwrap(); + remove_rooms(&admin).await; - client.set_token(admin_token).unwrap(); - remove_rooms(&client).await; + let accounts = future::join_all( + (0..rooms) + .map(|room| create_accounts(&admin, server_name.clone(), users_per_room, room, seed)), + ) + .await; - let accounts = create_accounts(&client, server_name.clone(), 4, seed).await; let rooms = create_rooms( - &client, - accounts + &admin, + seed, + &accounts .iter() - .map(|(user_id, access_token)| (user_id.clone(), access_token.clone())) - .collect::>() - .as_slice(), + // 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(|((user_id, access_token), room_id)| Sample { - user_id, + .map(|(users, room_id)| (users.into_iter().unzip(), room_id)) + .map(|((user_ids, access_tokens), room_id)| Sample { + user_ids, room_id, - access_token, + access_tokens, }) .collect(); Test { samples, server_name, - client, + 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/v1/account/root.rs b/crates/test/src/server/api/v1/account/root.rs index fc3ad4e..719d307 100644 --- a/crates/test/src/server/api/v1/account/root.rs +++ b/crates/test/src/server/api/v1/account/root.rs @@ -3,6 +3,7 @@ use fake::{ Fake, }; +use matrix::ruma_common::OwnedUserId; use reqwest::StatusCode; use scraper::Selector; use uuid::Uuid; @@ -13,7 +14,6 @@ use commune_server::router::api::v1::account::{ verify_code::{AccountVerifyCodePayload, VerifyCodeResponse}, verify_code_email::{AccountVerifyCodeEmailPayload, VerifyCodeEmailResponse}, }; -use matrix::admin::resources::user_id::UserId; use crate::tools::{http::HttpClient, maildev::MailDevClient}; @@ -80,7 +80,9 @@ async fn register_account_with_success() { "should return 201 for created" ); assert_eq!( - UserId::new(request_payload.username, "matrix.localhost".into()).to_string(), + 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" )