diff --git a/Cargo.toml b/Cargo.toml index 5fee52d..ea335e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,15 @@ [workspace] -members = [ - "crates/core", - "crates/matrix", - "crates/server", - "crates/test" -] +members = ["crates/core", "crates/matrix", "crates/server", "crates/test"] default-members = ["crates/server"] -resolver = "1" +resolver = "2" [workspace.dependencies] +async-trait = "0.1.74" +hex = "0.4.3" +hmac = "0.12.1" +serde_path_to_error = "0.1.14" +serde_qs = "0.12.0" +sha1 = "0.10.6" anyhow = "1.0.75" axum = { version = "0.7.4", features = ["tokio"] } chrono = { version = "0.4.34", features = ["serde"] } @@ -17,7 +18,12 @@ http = "0.2.11" 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"] } +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" diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index afc2a0a..f0f9183 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -1,4 +1,7 @@ -pub mod account; +//! This library deals with our core logic, such as authorizing user interactions, +//! forwarding regular events and constructing custom requests. + +pub mod session; pub mod auth; pub mod error; pub mod mail; @@ -9,14 +12,11 @@ pub use error::{Error, HttpStatusCode, Result}; use mail::service::MailService; use room::service::RoomService; +use tokio::sync::mpsc::Receiver; use url::Url; use std::{fmt::Debug, str::FromStr, sync::Arc}; -use matrix::Client as MatrixAdminClient; - -use self::{account::service::AccountService, auth::service::AuthService}; - pub mod env { pub const COMMUNE_SYNAPSE_HOST: &str = "COMMUNE_SYNAPSE_HOST"; pub const COMMUNE_SYNAPSE_ADMIN_TOKEN: &str = "COMMUNE_SYNAPSE_ADMIN_TOKEN"; @@ -92,9 +92,7 @@ impl CommuneConfig { } pub struct Commune { - pub account: Arc, - pub auth: Arc, - pub room: Arc, + } impl Commune { diff --git a/crates/core/src/account/error.rs b/crates/core/src/session/error.rs similarity index 100% rename from crates/core/src/account/error.rs rename to crates/core/src/session/error.rs diff --git a/crates/core/src/account/mod.rs b/crates/core/src/session/mod.rs similarity index 100% rename from crates/core/src/account/mod.rs rename to crates/core/src/session/mod.rs diff --git a/crates/core/src/account/model.rs b/crates/core/src/session/model.rs similarity index 100% rename from crates/core/src/account/model.rs rename to crates/core/src/session/model.rs diff --git a/crates/core/src/account/service.rs b/crates/core/src/session/service.rs similarity index 100% rename from crates/core/src/account/service.rs rename to crates/core/src/session/service.rs diff --git a/crates/matrix/Cargo.toml b/crates/matrix/Cargo.toml index 27796bc..82ee55f 100644 --- a/crates/matrix/Cargo.toml +++ b/crates/matrix/Cargo.toml @@ -5,22 +5,31 @@ edition = "2021" publish = false [dependencies] -async-trait = "0.1.74" -hex = "0.4.3" -hmac = "0.12.1" -serde_path_to_error = "0.1.14" -serde_qs = "0.12.0" -sha1 = "0.10.6" -ruma-events = { version = "0.27.11", features = ["html", "markdown"] } -ruma-common = { version = "0.12.1", features = ["rand"] } -ruma-macros = "0.12.0" +ruma-events = { version = "0.27.11", default_features = false, features = [ + "html", + "markdown", +] } +ruma-common = { version = "0.12.0", default_features = false, features = [ + "api", + "rand", +] } +ruma-macros = { version = "0.12.0", default_features = false } +ruma-client = { version = "0.12.0", default_features = false } # Workspace Dependencies -anyhow = { workspace = true } -chrono = { workspace = true, features = ["serde"] } mime = { workspace = true } reqwest = { workspace = true, features = ["json"] } serde = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } +sha1 = { workspace = true } url = { workspace = true, features = ["serde"] } +hex = { workspace = true } +hmac = { workspace = true } +http.workspace = true +bytes = "1.5.0" +async-trait = "0.1.77" + +[features] +client = [] +server = [] diff --git a/crates/matrix/src/admin/mod.rs b/crates/matrix/src/admin/mod.rs index 6d0fe39..75a9d6d 100644 --- a/crates/matrix/src/admin/mod.rs +++ b/crates/matrix/src/admin/mod.rs @@ -1 +1,7 @@ -pub mod resources; +//! 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 room; +pub mod session; +pub mod user; diff --git a/crates/matrix/src/admin/resources/mod.rs b/crates/matrix/src/admin/resources/mod.rs deleted file mode 100644 index c29990d..0000000 --- a/crates/matrix/src/admin/resources/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod room; -pub mod token; -pub mod user; diff --git a/crates/matrix/src/admin/resources/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..b55a66b --- /dev/null +++ b/crates/matrix/src/admin/room/get_room.rs @@ -0,0 +1,28 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedRoomId, +}; + +use super::Room; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_synapse/admin/v1/rooms/:room_id", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub room_id: OwnedRoomId, +} + +#[response(error = crate::Error)] +pub struct Response { + #[ruma_api(body)] + pub room: Room, +} diff --git a/crates/matrix/src/admin/room/get_rooms.rs b/crates/matrix/src/admin/room/get_rooms.rs new file mode 100644 index 0000000..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..84ee0f7 --- /dev/null +++ b/crates/matrix/src/admin/session.rs @@ -0,0 +1,45 @@ +//! This module contains handlers for user registration. +//! +//! reference: https://matrix-org.github.io/synapse/latest/admin_api/register_api.html + +use hmac::Mac; + +pub mod get_nonce; +pub mod register; + +#[derive(Clone, Debug)] +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..c71d8f0 --- /dev/null +++ b/crates/matrix/src/admin/session/register.rs @@ -0,0 +1,43 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedDeviceId, OwnedServerName, OwnedUserId, +}; + +use super::Hmac; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_synapse/admin/v1/register", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + pub nonce: String, + + pub username: String, + + pub password: String, + + #[serde(skip_deserializing_if = "String::is_empty")] + pub displayname: String, + + pub admin: bool, + + pub hmac: Hmac, +} + +#[response(error = crate::Error)] +pub struct Response { + pub access_token: String, + + pub user_id: OwnedUserId, + + pub home_server: OwnedServerName, + + pub device_id: OwnedDeviceId, +} diff --git a/crates/matrix/src/admin/user.rs b/crates/matrix/src/admin/user.rs 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..2b9e56c --- /dev/null +++ b/crates/matrix/src/admin/user/get_user_by_3pid.rs @@ -0,0 +1,31 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, thirdparty::Medium, +}; + +use super::User; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_synapse/admin/v1/threepid/:medium/users/:address", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub medium: Medium, + + #[ruma_api(path)] + pub address: String, +} + +#[response(error = crate::Error)] +pub struct Response { + #[ruma_api(body)] + pub user: User, +} diff --git a/crates/matrix/src/admin/user/get_users.rs b/crates/matrix/src/admin/user/get_users.rs new file mode 100644 index 0000000..40a926d --- /dev/null +++ b/crates/matrix/src/admin/user/get_users.rs @@ -0,0 +1,83 @@ +use ruma_common::{ + api::{request, response, Direction, 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", + } +}; + +#[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 { + users: Vec, + + next_token: String, + + total: u64, +} + +#[derive(Clone, Debug, Default)] +#[allow(dead_code)] +pub enum OrderBy { + #[default] + Name, + + Admin, + + UserType, + + Deactivated, + + ShadowBanned, + + Displayname, + + AvatarUrl, + + CreationTs, + + LastSeenTs, +} diff --git a/crates/matrix/src/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/mod.rs b/crates/matrix/src/client/mod.rs index 6d0fe39..196920e 100644 --- a/crates/matrix/src/client/mod.rs +++ b/crates/matrix/src/client/mod.rs @@ -1 +1,6 @@ -pub mod resources; +//! This module is the root of the client-server API. +//! +//! reference: https://spec.matrix.org/unstable/client-server-api + +pub mod room; +pub mod session; diff --git a/crates/matrix/src/client/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/events.rs b/crates/matrix/src/client/resources/events.rs deleted file mode 100644 index 2953b2b..0000000 --- a/crates/matrix/src/client/resources/events.rs +++ /dev/null @@ -1,310 +0,0 @@ -use anyhow::Result; -use ruma_common::{serde::Raw, EventId, OwnedEventId, OwnedTransactionId, RoomId}; - -use ruma_events::{ - relation::RelationType, AnyMessageLikeEvent, AnyStateEvent, AnyStateEventContent, - AnyTimelineEvent, MessageLikeEventContent, MessageLikeEventType, StateEventContent, - StateEventType, -}; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use tracing::instrument; - -use crate::{admin::resources::room::Direction, error::MatrixError, Client}; - -pub struct EventsService; - -#[derive(Debug, Default, Clone, Serialize)] -pub struct GetMessagesQuery { - #[serde(skip_serializing_if = "Option::is_none")] - pub from: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub to: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub limit: Option, - - pub dir: Direction, - - #[serde(skip_serializing_if = "String::is_empty")] - pub filter: String, -} - -#[derive(Debug, Default, Clone, Serialize)] -pub struct GetRelationsQuery { - #[serde(skip_serializing_if = "Option::is_none")] - pub from: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub to: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub limit: Option, - - pub dir: Direction, -} - -#[derive(Debug, Deserialize)] -pub struct GetMessagesResponse { - pub chunk: Vec>, - pub start: String, - pub end: String, - pub state: Option>>, -} - -#[derive(Debug, Deserialize)] -#[serde(transparent)] -pub struct GetStateResponse(pub Vec>); - -#[derive(Debug, Deserialize)] -pub struct GetRelationsResponse { - pub chunk: Vec>, - pub prev_batch: Option, - pub next_batch: Option, -} - -#[derive(Debug, Default, Serialize)] -pub struct SendRedactionBody { - #[serde(skip_serializing_if = "String::is_empty")] - pub reason: String, -} - -#[derive(Debug, Deserialize)] -pub struct SendMessageResponse { - pub event_id: OwnedEventId, -} - -#[derive(Debug, Deserialize)] -pub struct SendStateResponse { - pub event_id: OwnedEventId, -} - -#[derive(Debug, Deserialize)] -pub struct SendRedactionResponse { - pub event_id: OwnedEventId, -} - -impl EventsService { - #[instrument(skip(client, access_token))] - pub async fn get_event( - client: &Client, - access_token: impl Into, - room_id: &RoomId, - event_id: &EventId, - ) -> Result> { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let resp = tmp - .get(format!( - "/_matrix/client/v3/rooms/{room_id}/event/{event_id}", - room_id = room_id, - event_id = event_id, - )) - .await?; - - Ok(resp.json().await?) - } - - #[instrument(skip(client, access_token))] - pub async fn get_messages( - client: &Client, - access_token: impl Into, - room_id: &RoomId, - query: GetMessagesQuery, - ) -> Result { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let resp = tmp - .get_query( - format!( - "/_matrix/client/v3/rooms/{room_id}/messages", - room_id = room_id, - ), - &query, - ) - .await?; - - Ok(resp.json().await?) - } - - #[instrument(skip(client, access_token))] - pub async fn get_state( - client: &Client, - access_token: impl Into, - room_id: &RoomId, - ) -> Result { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let resp = tmp - .get(format!( - "/_matrix/client/v3/rooms/{room_id}/state", - room_id = room_id, - )) - .await?; - - Ok(resp.json().await?) - } - - #[instrument(skip(client, access_token))] - pub async fn get_state_content( - client: &Client, - access_token: impl Into, - room_id: &RoomId, - event_type: StateEventType, - state_key: Option, - ) -> Result> { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let mut path = format!( - "/_matrix/client/v3/rooms/{room_id}/state/{event_type}", - room_id = room_id, - event_type = event_type, - ); - - if let Some(state_key) = state_key { - path.push_str(&format!("/{state_key}", state_key = state_key)) - } - - let resp = tmp.get(path).await?; - - Ok(resp.json().await?) - } - - #[instrument(skip(client, access_token))] - pub async fn get_relations( - client: &Client, - access_token: impl Into, - room_id: &RoomId, - event_id: &EventId, - rel_type: Option>, - event_type: Option, - query: GetRelationsQuery, - ) -> Result { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let mut path = format!( - "/_matrix/client/v3/rooms/{room_id}/relations/{event_id}", - room_id = room_id, - event_id = event_id, - ); - - if let Some(rel_type) = rel_type { - path.push_str(&format!( - "/{rel_type}", - rel_type = rel_type - .as_ref() - .map_or("m.in_reply_to".into(), ToString::to_string) - )); - - if let Some(event_type) = event_type { - path.push_str(&format!("/{event_type}", event_type = event_type)) - } - } - - let resp = tmp.get_query(path, &query).await?; - - Ok(resp.json().await?) - } - - #[instrument(skip(client, access_token, body))] - pub async fn send_message( - client: &Client, - access_token: impl Into, - room_id: &RoomId, - txn_id: OwnedTransactionId, - body: T, - ) -> Result { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let resp = tmp - .put_json( - format!( - "/_matrix/client/v3/rooms/{room_id}/send/{event_type}/{txn_id}", - room_id = room_id, - event_type = body.event_type(), - txn_id = txn_id, - ), - &body, - ) - .await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } - - #[instrument(skip(client, access_token, body))] - pub async fn send_state( - client: &Client, - access_token: impl Into, - room_id: &RoomId, - state_key: Option, - body: T, - ) -> Result { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let mut path = format!( - "/_matrix/client/v3/rooms/{room_id}/state/{event_type}", - room_id = room_id, - event_type = body.event_type(), - ); - - if let Some(state_key) = state_key { - path.push_str(&format!("/{state_key}", state_key = state_key)) - } - - let resp = tmp.put_json(path, &body).await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } - - #[instrument(skip(client, access_token, body))] - pub async fn send_redaction( - client: &Client, - access_token: impl Into, - room_id: &RoomId, - event_id: &EventId, - txn_id: OwnedTransactionId, - body: SendRedactionBody, - ) -> Result { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let resp = tmp - .put_json( - format!( - "/_matrix/client/v3/rooms/{room_id}/redact/{event_id}/{txn_id}", - room_id = room_id, - event_id = event_id, - txn_id = txn_id, - ), - &body, - ) - .await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.error)) - } -} diff --git a/crates/matrix/src/client/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/mxc.rs b/crates/matrix/src/client/resources/mxc.rs deleted file mode 100644 index 7dc669a..0000000 --- a/crates/matrix/src/client/resources/mxc.rs +++ /dev/null @@ -1,184 +0,0 @@ -use std::str::FromStr; - -use anyhow::Result; -use mime::Mime; -use ruma_common::{MxcUri, OwnedMxcUri}; -use serde::{de, Deserialize, Deserializer, Serialize}; -use tracing::instrument; - -use chrono::{serde::ts_microseconds_option, DateTime, Utc}; - -use crate::error::MatrixError; - -fn parse_mime_opt<'de, D>(d: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - Option::<&str>::deserialize(d)? - .map(::from_str) - .transpose() - .map_err(de::Error::custom) -} - -#[derive(Debug, Serialize)] -pub struct GetPreviewUrlQuery { - pub url: url::Url, - pub ts: DateTime, -} - -#[derive(Debug, Deserialize)] -pub struct CreateMxcUriResponse { - pub content_uri: String, - - #[serde(with = "ts_microseconds_option")] - pub unused_expires_at: Option>, -} - -#[derive(Debug, Deserialize)] -pub struct GetPreviewUrlResponse { - #[serde(rename = "matrix:image_size")] - pub image_size: Option, - - #[serde(rename = "og:description")] - pub description: Option, - - #[serde(rename = "og:image")] - pub image: Option, - - #[serde(rename = "og:image:height")] - pub height: Option, - - #[serde(rename = "og:image:width")] - pub width: Option, - - #[serde(rename = "og:image:type", deserialize_with = "parse_mime_opt")] - pub kind: Option, - - #[serde(rename = "og:title")] - pub title: Option, -} - -#[derive(Debug, Deserialize)] -pub struct GetConfigResponse { - #[serde(rename = "m.upload.size")] - pub upload_size: Option, -} - -#[derive(Debug, Serialize)] -pub enum ResizeMethod { - Crop, - Scale, -} - -pub struct MxcService; - -#[derive(Debug, Deserialize)] -pub struct MxcError { - #[serde(flatten)] - pub inner: MatrixError, - - pub retry_after_ms: u64, -} - -impl MxcService { - /// Creates a new `MxcUri`, independently of the content being uploaded - /// - /// Refer: https://spec.matrix.org/v1.9/client-server-api/#post_matrixmediav1create - #[instrument(skip(client, access_token))] - pub async fn create( - client: &crate::http::Client, - access_token: impl Into, - ) -> Result { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let resp = tmp.post("/_matrix/media/v1/create").await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.inner.error)) - } - - /// Retrieve the configuration of the content repository - /// - /// Refer: https://spec.matrix.org/v1.9/client-server-api/#get_matrixmediav3config - #[instrument(skip(client, access_token))] - pub async fn get_config( - client: &crate::http::Client, - access_token: impl Into, - ) -> Result { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let resp = tmp.get("/_matrix/media/v3/config").await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.inner.error)) - } - - /// Retrieve a URL to download content from the content repository, - /// optionally replacing the name of the file. - /// - /// Refer: https://spec.matrix.org/v1.9/client-server-api/#get_matrixmediav3downloadservernamemediaid - #[instrument(skip(client, access_token))] - pub async fn get_download_url( - client: &crate::http::Client, - access_token: impl Into, - mxc_uri: &MxcUri, - mut base_url: url::Url, - file_name: Option, - ) -> Result { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let (server_name, media_id) = mxc_uri.parts().unwrap(); - - let mut path = format!( - "/_matrix/media/v3/download/{server_name}/{media_id}", - server_name = server_name, - media_id = media_id, - ); - - if let Some(file_name) = file_name { - path.push_str(&format!("/{file_name}", file_name = file_name)) - } - - base_url.set_path(&path); - - Ok(base_url) - } - - /// - /// - /// Refer: https://spec.matrix.org/v1.9/client-server-api/#get_matrixmediav3preview_url - #[instrument(skip(client, access_token))] - pub async fn get_preview( - client: &crate::http::Client, - access_token: impl Into, - query: GetPreviewUrlQuery, - ) -> Result { - let mut tmp = (*client).clone(); - tmp.set_token(access_token)?; - - let resp = tmp - .get_query("/_matrix/media/v3/preview_url".to_string(), &query) - .await?; - - if resp.status().is_success() { - return Ok(resp.json().await?); - } - - let error = resp.json::().await?; - - Err(anyhow::anyhow!(error.inner.error)) - } -} diff --git a/crates/matrix/src/client/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/room.rs b/crates/matrix/src/client/room.rs new file mode 100644 index 0000000..27a797f --- /dev/null +++ b/crates/matrix/src/client/room.rs @@ -0,0 +1,12 @@ +//! This module contains handlers to interact with rooms. +//! +//! reference: https://spec.matrix.org/unstable/client-server-api/#rooms + +mod ban_from_room; +mod create_room; +mod forget_room; +mod get_rooms; +mod join_room; +mod kick_from_room; +mod leave_room; +mod unban_from_room; diff --git a/crates/matrix/src/client/room/ban_from_room.rs b/crates/matrix/src/client/room/ban_from_room.rs new file mode 100644 index 0000000..5e52971 --- /dev/null +++ b/crates/matrix/src/client/room/ban_from_room.rs @@ -0,0 +1,28 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedRoomId, OwnedUserId, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: true, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/v3/rooms/{room_id}/ban", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub room_id: OwnedRoomId, + + pub user_id: OwnedUserId, + + #[serde(skip_serializing_if = "String::is_empty")] + pub reason: String, +} + +#[response(error = crate::Error)] +pub struct Response {} diff --git a/crates/matrix/src/client/room/create_room.rs b/crates/matrix/src/client/room/create_room.rs new file mode 100644 index 0000000..140bfed --- /dev/null +++ b/crates/matrix/src/client/room/create_room.rs @@ -0,0 +1,70 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedRoomId, OwnedUserId, serde::Raw, +}; +use ruma_events::{room::power_levels::RoomPowerLevels, AnyInitialStateEvent}; +use serde::Serialize; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: true, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/v3/createRoom", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[serde(skip_serializing_if = "Option::is_none")] + pub creation_content: Option, + + #[serde(skip_serializing_if = "<[_]>::is_empty")] + pub initial_state: Vec>, + + #[serde(skip_serializing_if = "<[_]>::is_empty")] + pub invite: Vec, + + pub is_direct: bool, + + #[serde(skip_serializing_if = "String::is_empty")] + pub name: String, + + #[serde( + rename = "power_level_content_override", + skip_serializing_if = "Option::is_none" + )] + pub power_override: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub preset: RoomPreset, + + #[serde(rename = "room_alias_name", skip_serializing_if = "String::is_empty")] + pub alias: String, + + #[serde(skip_serializing_if = "String::is_empty")] + pub topic: String, +} + +#[response(error = crate::Error)] +pub struct Response { + pub room_id: OwnedRoomId, +} + +#[derive(Clone, Debug, Serialize)] +pub struct RoomCreationContent { + #[serde(rename = "m.federate")] + pub federate: bool, +} + +#[derive(Clone, Debug, Default, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum RoomPreset { + PublicChat, + + PrivateChat, + + #[default] + TrustedPrivateChat, +} diff --git a/crates/matrix/src/client/room/forget_room.rs b/crates/matrix/src/client/room/forget_room.rs new file mode 100644 index 0000000..1b72ee2 --- /dev/null +++ b/crates/matrix/src/client/room/forget_room.rs @@ -0,0 +1,26 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedRoomId, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: true, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/v3/rooms/{room_id}/forget", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub room_id: OwnedRoomId, + + #[serde(skip_serializing_if = "String::is_empty")] + pub reason: String, +} + +#[response(error = crate::Error)] +pub struct Response {} diff --git a/crates/matrix/src/client/room/get_rooms.rs b/crates/matrix/src/client/room/get_rooms.rs new file mode 100644 index 0000000..5208a1a --- /dev/null +++ b/crates/matrix/src/client/room/get_rooms.rs @@ -0,0 +1,70 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, + room::RoomType, + OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedServerName, +}; +use ruma_events::room::join_rules::JoinRule; +use serde::Deserialize; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: None, + history: { + unstable => "/_matrix/client/v3/publicRooms", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(query)] + #[serde(skip_serializing_if = "Option::is_none")] + limit: Option, + + #[ruma_api(query)] + server: OwnedServerName, + + #[ruma_api(query)] + #[serde(skip_serializing_if = "String::is_empty")] + since: String, +} + +#[response(error = crate::Error)] +pub struct Response { + chunk: Vec, + + #[serde(skip_serializing_if = "Option::is_none")] + next_batch: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + prev_batch: Option, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct Room { + #[serde(skip_serializing_if = "Option::is_none")] + pub avatar_url: Option, + + #[serde(rename = "canonical_alias", skip_serializing_if = "Option::is_none")] + pub alias: Option, + + pub join_rule: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + + #[serde(rename = "num_joined_members")] + pub members: Option, + + pub room_id: OwnedRoomId, + + #[serde(skip_serializing_if = "Option::is_none")] + pub room_type: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub topic: Option, + + pub world_readable: bool, +} diff --git a/crates/matrix/src/client/room/invite_to_room.rs b/crates/matrix/src/client/room/invite_to_room.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/matrix/src/client/room/join_room.rs b/crates/matrix/src/client/room/join_room.rs new file mode 100644 index 0000000..1fdadd4 --- /dev/null +++ b/crates/matrix/src/client/room/join_room.rs @@ -0,0 +1,28 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedRoomOrAliasId, OwnedRoomId, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: true, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/v3/join/{alias_or_id}", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub alias_or_id: OwnedRoomOrAliasId, + + #[serde(skip_serializing_if = "String::is_empty")] + pub reason: String, +} + +#[response(error = crate::Error)] +pub struct Response { + pub room_id: OwnedRoomId, +} diff --git a/crates/matrix/src/client/room/kick_from_room.rs b/crates/matrix/src/client/room/kick_from_room.rs new file mode 100644 index 0000000..c3079b9 --- /dev/null +++ b/crates/matrix/src/client/room/kick_from_room.rs @@ -0,0 +1,28 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedUserId, OwnedRoomId, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: true, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/v3/rooms/{room_id}/kick", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub room_id: OwnedRoomId, + + pub user_id: OwnedUserId, + + #[serde(skip_serializing_if = "String::is_empty")] + pub reason: String, +} + +#[response(error = crate::Error)] +pub struct Response {} diff --git a/crates/matrix/src/client/room/leave_room.rs b/crates/matrix/src/client/room/leave_room.rs new file mode 100644 index 0000000..6d50593 --- /dev/null +++ b/crates/matrix/src/client/room/leave_room.rs @@ -0,0 +1,26 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedRoomId, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: true, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/v3/rooms/{room_id}/leave", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub room_id: OwnedRoomId, + + #[serde(skip_serializing_if = "String::is_empty")] + pub reason: String, +} + +#[response(error = crate::Error)] +pub struct Response {} diff --git a/crates/matrix/src/client/room/unban_from_room.rs b/crates/matrix/src/client/room/unban_from_room.rs new file mode 100644 index 0000000..a34ace7 --- /dev/null +++ b/crates/matrix/src/client/room/unban_from_room.rs @@ -0,0 +1,28 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedRoomId, OwnedUserId, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: true, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/v3/rooms/{room_id}/unban", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[ruma_api(path)] + pub room_id: OwnedRoomId, + + pub user_id: OwnedUserId, + + #[serde(skip_serializing_if = "String::is_empty")] + pub reason: String, +} + +#[response(error = crate::Error)] +pub struct Response {} diff --git a/crates/matrix/src/client/session.rs b/crates/matrix/src/client/session.rs new file mode 100644 index 0000000..18328e9 --- /dev/null +++ b/crates/matrix/src/client/session.rs @@ -0,0 +1,9 @@ +//! This module contains handlers for user sessions. +//! +//! reference: https://spec.matrix.org/unstable/client-server-api/#client-authentication + +mod get_flows; +mod login; +mod register; +mod username_available; +mod whoami; diff --git a/crates/matrix/src/client/session/get_flows.rs b/crates/matrix/src/client/session/get_flows.rs new file mode 100644 index 0000000..14e9a29 --- /dev/null +++ b/crates/matrix/src/client/session/get_flows.rs @@ -0,0 +1,46 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, +}; +use serde::Deserialize; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: true, + authentication: None, + history: { + unstable => "/_matrix/client/v3/login", + } +}; + +#[request(error = crate::Error)] +pub struct Request {} + +#[response(error = crate::Error)] +pub struct Response { + pub body: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct LoginFlow { + #[serde(skip_serializing_if = "Option::is_none")] + pub get_login_token: Option, + + pub kind: LoginType, +} + +#[derive(Clone, Debug, Deserialize)] +pub enum LoginType { + #[serde(rename = "m.login.password")] + Password, + + #[serde(rename = "m.login.token")] + Token, + + #[serde(rename = "m.login.sso")] + Sso, + + #[serde(rename = "m.login.application_service")] + ApplicationService, +} diff --git a/crates/matrix/src/client/session/login.rs b/crates/matrix/src/client/session/login.rs new file mode 100644 index 0000000..889401b --- /dev/null +++ b/crates/matrix/src/client/session/login.rs @@ -0,0 +1,108 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedMxcUri, thirdparty::Medium, OwnedDeviceId, OwnedUserId, +}; +use serde::{Serialize, Deserialize}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: true, + authentication: None, + history: { + unstable => "/_matrix/client/v3/login", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + #[serde(flatten, rename = "type")] + pub kind: LoginType, + + #[serde(skip_serializing_if = "Option::is_none")] + pub identifier: Option, + + #[serde( + rename = "initial_device_display_name", + skip_serializing_if = "String::is_empty" + )] + pub device_name: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub refresh_token: Option, +} + +#[response(error = crate::Error)] +pub struct Response { + pub access_token: String, + + pub device_id: OwnedDeviceId, + + #[serde(skip_serializing_if = "Option::is_none")] + pub expires_in_ms: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub refresh_token: Option, + + pub user_id: OwnedUserId, + + #[serde(skip_serializing_if = "Option::is_none")] + pub well_known: Option, +} + +#[derive(Clone, Debug, Serialize)] +pub struct IdentityProvider { + pub id: String, + + #[serde(skip_serializing_if = "String::is_empty")] + pub name: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub icon: Option, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(tag = "type")] +pub enum LoginType { + #[serde(rename = "m.login.password")] + Password { password: String }, + + #[serde(rename = "m.login.token")] + Token { token: String }, + + #[serde(rename = "m.login.sso")] + Sso { + #[serde(skip_serializing_if = "<[_]>::is_empty")] + identity_providers: Vec, + }, + + #[serde(rename = "m.login.application_service")] + ApplicationService, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(tag = "type")] +pub enum Identifier { + #[serde(rename = "m.id.user")] + User { user: String }, + + #[serde(rename = "m.id.thirdparty")] + ThirdParty { medium: Medium, address: String }, + + #[serde(rename = "m.id.phone")] + Phone { country: String, phone: String }, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct BaseUrl { + pub base_url: url::Url, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct WellKnown { + #[serde(rename = "m.homeserver")] + pub homeserver: BaseUrl, + + #[serde(rename = "m.identity_server")] + pub identity_server: BaseUrl, +} diff --git a/crates/matrix/src/client/session/register.rs b/crates/matrix/src/client/session/register.rs new file mode 100644 index 0000000..505e8e9 --- /dev/null +++ b/crates/matrix/src/client/session/register.rs @@ -0,0 +1,45 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedDeviceId, OwnedUserId, +}; + +#[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 = "String::is_empty" + )] + pub device_name: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub refresh_token: Option, +} + +#[response(error = crate::Error)] +pub struct Response { + pub access_token: String, + + pub device_id: OwnedDeviceId, + + #[serde(skip_serializing_if = "Option::is_none")] + pub expires_in_ms: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub refresh_token: Option, + + pub user_id: OwnedUserId, +} diff --git a/crates/matrix/src/client/session/username_available.rs b/crates/matrix/src/client/session/username_available.rs new file mode 100644 index 0000000..e380f6f --- /dev/null +++ b/crates/matrix/src/client/session/username_available.rs @@ -0,0 +1,24 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: true, + authentication: None, + history: { + unstable => "/_matrix/client/v3/register/available", + } +}; + +#[request(error = crate::Error)] +pub struct Request { + pub username: String, +} + +#[response(error = crate::Error)] +pub struct Response { + pub available: bool, +} diff --git a/crates/matrix/src/client/session/whoami.rs b/crates/matrix/src/client/session/whoami.rs new file mode 100644 index 0000000..5913bfb --- /dev/null +++ b/crates/matrix/src/client/session/whoami.rs @@ -0,0 +1,24 @@ +use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedDeviceId, OwnedUserId, +}; + +#[allow(dead_code)] +const METADATA: Metadata = metadata! { + method: GET, + rate_limited: true, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/v3/account/whoami", + } +}; + +#[request(error = crate::Error)] +pub struct Request {} + +#[response(error = crate::Error)] +pub struct Response { + pub device_id: OwnedDeviceId, + + pub user_id: OwnedUserId, +} 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..1ae34a5 100644 --- a/crates/matrix/src/lib.rs +++ b/crates/matrix/src/lib.rs @@ -1,26 +1,69 @@ -//! 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::{DefaultConstructibleHttpClient, HttpClient, HttpClientExt}; -pub mod filter; +#[allow(unused_imports)] +use ruma_common::api::error::MatrixError as Error; -pub use http::Client; +#[derive(Debug)] +pub struct Handle { + inner: reqwest::Client, +} -/// Implementation on the Administrator API of Matrix -/// -/// Refer: https://matrix-org.github.io/synapse/latest/usage/administration/index.html -pub mod admin; +impl Handle { + pub async fn new() { + self.send_matrix_request(, access_token, for_versions, request) + } -/// 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; + pub async fn dispatch(&self) { + self.send_matrix_request(, access_token, for_versions, request) + } +} + +#[async_trait] +impl HttpClient for Handle { + type RequestBody = BytesMut; + type ResponseBody = Bytes; + type Error = reqwest::Error; + + async fn send_http_request( + &self, + req: http::Request, + ) -> Result, reqwest::Error> { + let req = req.map(|body| body.freeze()).try_into()?; + let mut res = self.inner.execute(req).await?; + + let mut http_builder = http::Response::builder() + .status(res.status()) + .version(res.version()); + std::mem::swap( + http_builder + .headers_mut() + .expect("http::response::Builder to be usable"), + res.headers_mut(), + ); + + Ok(http_builder + .body(res.bytes().await?) + .expect("http::Response construction to work")) + } +} -/// Ruma re-exports -pub use ruma_common; -pub use ruma_events; +impl DefaultConstructibleHttpClient for Handle { + fn default() -> Self { + Self { + inner: reqwest::Client::new(), + } + } +} diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index 3dae89d..4917232 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -20,8 +20,8 @@ axum = { workspace = true, features = ["tokio"] } anyhow = { workspace = true } dotenv = { workspace = true } http = { workspace = true } -openssl = { workspace = true, features = ["vendored"] } -openssl-sys = { workspace = true, features = ["vendored"] } +# 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 } @@ -32,4 +32,3 @@ 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" }