From 2888d59537a6f45578d9b8769017e5e6f04cd5ee Mon Sep 17 00:00:00 2001 From: Sienna Meridian Satterwhite Date: Sat, 21 Mar 2026 20:26:39 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20MatrixClient=20=E2=80=94=20chat=20and?= =?UTF-8?q?=20collaboration=20API=20(80=20endpoints)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Typed Matrix client/server API covering auth, rooms, messages, state, profiles, media, devices, E2EE, push, presence, and spaces. Bump: sunbeam-sdk v0.6.0 --- Cargo.lock | 2 +- sunbeam-sdk/Cargo.toml | 2 +- sunbeam-sdk/src/matrix/mod.rs | 1231 +++++++++++++++++++++++++++++++ sunbeam-sdk/src/matrix/types.rs | 882 ++++++++++++++++++++++ 4 files changed, 2115 insertions(+), 2 deletions(-) create mode 100644 sunbeam-sdk/src/matrix/mod.rs create mode 100644 sunbeam-sdk/src/matrix/types.rs diff --git a/Cargo.lock b/Cargo.lock index 8f7d296..bfe0587 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3591,7 +3591,7 @@ dependencies = [ [[package]] name = "sunbeam-sdk" -version = "0.4.0" +version = "0.5.0" dependencies = [ "base64", "bytes", diff --git a/sunbeam-sdk/Cargo.toml b/sunbeam-sdk/Cargo.toml index 88f9c6b..9b78c46 100644 --- a/sunbeam-sdk/Cargo.toml +++ b/sunbeam-sdk/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sunbeam-sdk" -version = "0.5.0" +version = "0.6.0" edition = "2024" description = "Sunbeam SDK — reusable library for cluster management" repository = "https://src.sunbeam.pt/studio/cli" diff --git a/sunbeam-sdk/src/matrix/mod.rs b/sunbeam-sdk/src/matrix/mod.rs new file mode 100644 index 0000000..1ffc929 --- /dev/null +++ b/sunbeam-sdk/src/matrix/mod.rs @@ -0,0 +1,1231 @@ +//! Matrix client-server API client. + +pub mod types; + +use crate::client::{AuthMethod, HttpTransport, ServiceClient}; +use crate::error::Result; +use reqwest::Method; +use types::*; + +/// Client for the Matrix Client-Server API. +pub struct MatrixClient { + pub(crate) transport: HttpTransport, +} + +impl ServiceClient for MatrixClient { + fn service_name(&self) -> &'static str { + "matrix" + } + + fn base_url(&self) -> &str { + &self.transport.base_url + } + + fn from_parts(base_url: String, auth: AuthMethod) -> Self { + Self { + transport: HttpTransport::new(&base_url, auth), + } + } +} + +impl MatrixClient { + /// Build a MatrixClient from domain (e.g. `https://matrix.{domain}/_matrix`). + pub fn connect(domain: &str) -> Self { + let base_url = format!("https://matrix.{domain}/_matrix"); + Self::from_parts(base_url, AuthMethod::Bearer(String::new())) + } + + /// Replace the auth token. + pub fn set_token(&mut self, token: &str) { + self.transport.set_auth(AuthMethod::Bearer(token.to_string())); + } + + // ----------------------------------------------------------------------- + // Auth + // ----------------------------------------------------------------------- + + /// List supported login types. + pub async fn list_login_types(&self) -> Result { + self.transport + .json( + Method::GET, + "client/v3/login", + Option::<&()>::None, + "matrix list login types", + ) + .await + } + + /// Authenticate and obtain an access token. + pub async fn login(&self, body: &LoginRequest) -> Result { + self.transport + .json(Method::POST, "client/v3/login", Some(body), "matrix login") + .await + } + + /// Refresh an access token. + pub async fn refresh(&self, body: &RefreshRequest) -> Result { + self.transport + .json(Method::POST, "client/v3/refresh", Some(body), "matrix refresh") + .await + } + + /// Invalidate the current access token. + pub async fn logout(&self) -> Result<()> { + self.transport + .send( + Method::POST, + "client/v3/logout", + Option::<&()>::None, + "matrix logout", + ) + .await + } + + /// Invalidate all access tokens for the user. + pub async fn logout_all(&self) -> Result<()> { + self.transport + .send( + Method::POST, + "client/v3/logout/all", + Option::<&()>::None, + "matrix logout all", + ) + .await + } + + // ----------------------------------------------------------------------- + // Account + // ----------------------------------------------------------------------- + + /// Register a new account. + pub async fn register(&self, body: &RegisterRequest) -> Result { + self.transport + .json(Method::POST, "client/v3/register", Some(body), "matrix register") + .await + } + + /// Get the authenticated user's identity. + pub async fn whoami(&self) -> Result { + self.transport + .json( + Method::GET, + "client/v3/account/whoami", + Option::<&()>::None, + "matrix whoami", + ) + .await + } + + /// List third-party identifiers for the account. + pub async fn list_3pids(&self) -> Result { + self.transport + .json( + Method::GET, + "client/v3/account/3pid", + Option::<&()>::None, + "matrix list 3pids", + ) + .await + } + + /// Add a third-party identifier to the account. + pub async fn add_3pid(&self, body: &Add3pidRequest) -> Result<()> { + self.transport + .send(Method::POST, "client/v3/account/3pid/add", Some(body), "matrix add 3pid") + .await + } + + /// Remove a third-party identifier from the account. + pub async fn delete_3pid(&self, body: &Delete3pidRequest) -> Result<()> { + self.transport + .send( + Method::POST, + "client/v3/account/3pid/delete", + Some(body), + "matrix delete 3pid", + ) + .await + } + + /// Change the account password. + pub async fn change_password(&self, body: &ChangePasswordRequest) -> Result<()> { + self.transport + .send( + Method::POST, + "client/v3/account/password", + Some(body), + "matrix change password", + ) + .await + } + + /// Deactivate the account. + pub async fn deactivate(&self, body: &DeactivateRequest) -> Result<()> { + self.transport + .send( + Method::POST, + "client/v3/account/deactivate", + Some(body), + "matrix deactivate", + ) + .await + } + + // ----------------------------------------------------------------------- + // Rooms + // ----------------------------------------------------------------------- + + /// Create a new room. + pub async fn create_room(&self, body: &CreateRoomRequest) -> Result { + self.transport + .json(Method::POST, "client/v3/createRoom", Some(body), "matrix create room") + .await + } + + /// List public rooms on the server. + pub async fn list_public_rooms( + &self, + limit: Option, + since: Option<&str>, + ) -> Result { + let mut path = "client/v3/publicRooms".to_string(); + let mut params = Vec::new(); + if let Some(l) = limit { + params.push(format!("limit={l}")); + } + if let Some(s) = since { + params.push(format!("since={s}")); + } + if !params.is_empty() { + path.push('?'); + path.push_str(¶ms.join("&")); + } + self.transport + .json( + Method::GET, + &path, + Option::<&()>::None, + "matrix list public rooms", + ) + .await + } + + /// Search public rooms with filtering. + pub async fn search_public_rooms( + &self, + body: &SearchPublicRoomsRequest, + ) -> Result { + self.transport + .json( + Method::POST, + "client/v3/publicRooms", + Some(body), + "matrix search public rooms", + ) + .await + } + + /// Get a room's visibility in the directory. + pub async fn get_room_visibility(&self, room_id: &str) -> Result { + self.transport + .json( + Method::GET, + &format!("client/v3/directory/list/room/{room_id}"), + Option::<&()>::None, + "matrix get room visibility", + ) + .await + } + + /// Set a room's visibility in the directory. + pub async fn set_room_visibility( + &self, + room_id: &str, + body: &SetRoomVisibilityRequest, + ) -> Result<()> { + self.transport + .send( + Method::PUT, + &format!("client/v3/directory/list/room/{room_id}"), + Some(body), + "matrix set room visibility", + ) + .await + } + + // ----------------------------------------------------------------------- + // Membership + // ----------------------------------------------------------------------- + + /// Join a room by room ID. + pub async fn join_room_by_id(&self, room_id: &str) -> Result<()> { + self.transport + .send( + Method::POST, + &format!("client/v3/join/{room_id}"), + Option::<&()>::None, + "matrix join room by id", + ) + .await + } + + /// Join a room by alias. + pub async fn join_room_by_alias(&self, alias: &str) -> Result<()> { + self.transport + .send( + Method::POST, + &format!("client/v3/join/{alias}"), + Option::<&()>::None, + "matrix join room by alias", + ) + .await + } + + /// Leave a room. + pub async fn leave_room(&self, room_id: &str) -> Result<()> { + self.transport + .send( + Method::POST, + &format!("client/v3/rooms/{room_id}/leave"), + Option::<&()>::None, + "matrix leave room", + ) + .await + } + + /// Invite a user to a room. + pub async fn invite(&self, room_id: &str, body: &InviteRequest) -> Result<()> { + self.transport + .send( + Method::POST, + &format!("client/v3/rooms/{room_id}/invite"), + Some(body), + "matrix invite", + ) + .await + } + + /// Ban a user from a room. + pub async fn ban(&self, room_id: &str, body: &BanRequest) -> Result<()> { + self.transport + .send( + Method::POST, + &format!("client/v3/rooms/{room_id}/ban"), + Some(body), + "matrix ban", + ) + .await + } + + /// Unban a user from a room. + pub async fn unban(&self, room_id: &str, body: &UnbanRequest) -> Result<()> { + self.transport + .send( + Method::POST, + &format!("client/v3/rooms/{room_id}/unban"), + Some(body), + "matrix unban", + ) + .await + } + + /// Kick a user from a room. + pub async fn kick(&self, room_id: &str, body: &KickRequest) -> Result<()> { + self.transport + .send( + Method::POST, + &format!("client/v3/rooms/{room_id}/kick"), + Some(body), + "matrix kick", + ) + .await + } + + // ----------------------------------------------------------------------- + // State + // ----------------------------------------------------------------------- + + /// Get all state events for a room. + pub async fn get_all_state(&self, room_id: &str) -> Result> { + self.transport + .json( + Method::GET, + &format!("client/v3/rooms/{room_id}/state"), + Option::<&()>::None, + "matrix get all state", + ) + .await + } + + /// Get a specific state event. + pub async fn get_state_event( + &self, + room_id: &str, + event_type: &str, + state_key: &str, + ) -> Result { + self.transport + .json( + Method::GET, + &format!("client/v3/rooms/{room_id}/state/{event_type}/{state_key}"), + Option::<&()>::None, + "matrix get state event", + ) + .await + } + + /// Set a state event in a room. + pub async fn set_state_event( + &self, + room_id: &str, + event_type: &str, + state_key: &str, + body: &serde_json::Value, + ) -> Result { + self.transport + .json( + Method::PUT, + &format!("client/v3/rooms/{room_id}/state/{event_type}/{state_key}"), + Some(body), + "matrix set state event", + ) + .await + } + + // ----------------------------------------------------------------------- + // Messages + // ----------------------------------------------------------------------- + + /// Synchronise the client's state with the server. + pub async fn sync(&self, params: &SyncParams) -> Result { + let mut path = "client/v3/sync".to_string(); + let mut qs = Vec::new(); + if let Some(ref f) = params.filter { + qs.push(format!("filter={f}")); + } + if let Some(ref s) = params.since { + qs.push(format!("since={s}")); + } + if let Some(fs) = params.full_state { + qs.push(format!("full_state={fs}")); + } + if let Some(ref sp) = params.set_presence { + qs.push(format!("set_presence={sp}")); + } + if let Some(t) = params.timeout { + qs.push(format!("timeout={t}")); + } + if !qs.is_empty() { + path.push('?'); + path.push_str(&qs.join("&")); + } + self.transport + .json(Method::GET, &path, Option::<&()>::None, "matrix sync") + .await + } + + /// Send a message event to a room. + pub async fn send_event( + &self, + room_id: &str, + event_type: &str, + txn_id: &str, + body: &serde_json::Value, + ) -> Result { + self.transport + .json( + Method::PUT, + &format!("client/v3/rooms/{room_id}/send/{event_type}/{txn_id}"), + Some(body), + "matrix send event", + ) + .await + } + + /// Get messages for a room. + pub async fn get_messages( + &self, + room_id: &str, + params: &MessagesParams, + ) -> Result { + let mut path = format!("client/v3/rooms/{room_id}/messages?dir={}", params.dir); + if let Some(ref f) = params.from { + path.push_str(&format!("&from={f}")); + } + if let Some(ref t) = params.to { + path.push_str(&format!("&to={t}")); + } + if let Some(l) = params.limit { + path.push_str(&format!("&limit={l}")); + } + if let Some(ref f) = params.filter { + path.push_str(&format!("&filter={f}")); + } + self.transport + .json(Method::GET, &path, Option::<&()>::None, "matrix get messages") + .await + } + + /// Get a single event from a room. + pub async fn get_event(&self, room_id: &str, event_id: &str) -> Result { + self.transport + .json( + Method::GET, + &format!("client/v3/rooms/{room_id}/event/{event_id}"), + Option::<&()>::None, + "matrix get event", + ) + .await + } + + /// Get events around a given event. + pub async fn get_context( + &self, + room_id: &str, + event_id: &str, + limit: Option, + ) -> Result { + let mut path = format!("client/v3/rooms/{room_id}/context/{event_id}"); + if let Some(l) = limit { + path.push_str(&format!("?limit={l}")); + } + self.transport + .json(Method::GET, &path, Option::<&()>::None, "matrix get context") + .await + } + + /// Redact an event in a room. + pub async fn redact( + &self, + room_id: &str, + event_id: &str, + txn_id: &str, + body: &RedactRequest, + ) -> Result { + self.transport + .json( + Method::PUT, + &format!("client/v3/rooms/{room_id}/redact/{event_id}/{txn_id}"), + Some(body), + "matrix redact", + ) + .await + } + + // ----------------------------------------------------------------------- + // Presence + // ----------------------------------------------------------------------- + + /// Get presence status for a user. + pub async fn get_presence(&self, user_id: &str) -> Result { + self.transport + .json( + Method::GET, + &format!("client/v3/presence/{user_id}/status"), + Option::<&()>::None, + "matrix get presence", + ) + .await + } + + /// Set presence status for a user. + pub async fn set_presence( + &self, + user_id: &str, + body: &SetPresenceRequest, + ) -> Result<()> { + self.transport + .send( + Method::PUT, + &format!("client/v3/presence/{user_id}/status"), + Some(body), + "matrix set presence", + ) + .await + } + + // ----------------------------------------------------------------------- + // Typing + // ----------------------------------------------------------------------- + + /// Send a typing notification. + pub async fn send_typing( + &self, + room_id: &str, + user_id: &str, + body: &TypingRequest, + ) -> Result<()> { + self.transport + .send( + Method::PUT, + &format!("client/v3/rooms/{room_id}/typing/{user_id}"), + Some(body), + "matrix send typing", + ) + .await + } + + // ----------------------------------------------------------------------- + // Receipts + // ----------------------------------------------------------------------- + + /// Send a read receipt. + pub async fn send_receipt( + &self, + room_id: &str, + receipt_type: &str, + event_id: &str, + body: &ReceiptRequest, + ) -> Result<()> { + self.transport + .send( + Method::POST, + &format!("client/v3/rooms/{room_id}/receipt/{receipt_type}/{event_id}"), + Some(body), + "matrix send receipt", + ) + .await + } + + // ----------------------------------------------------------------------- + // Profiles + // ----------------------------------------------------------------------- + + /// Get a user's profile. + pub async fn get_profile(&self, user_id: &str) -> Result { + self.transport + .json( + Method::GET, + &format!("client/v3/profile/{user_id}"), + Option::<&()>::None, + "matrix get profile", + ) + .await + } + + /// Get a user's display name. + pub async fn get_displayname(&self, user_id: &str) -> Result { + self.transport + .json( + Method::GET, + &format!("client/v3/profile/{user_id}/displayname"), + Option::<&()>::None, + "matrix get displayname", + ) + .await + } + + /// Set a user's display name. + pub async fn set_displayname( + &self, + user_id: &str, + body: &SetDisplaynameRequest, + ) -> Result<()> { + self.transport + .send( + Method::PUT, + &format!("client/v3/profile/{user_id}/displayname"), + Some(body), + "matrix set displayname", + ) + .await + } + + /// Get a user's avatar URL. + pub async fn get_avatar_url(&self, user_id: &str) -> Result { + self.transport + .json( + Method::GET, + &format!("client/v3/profile/{user_id}/avatar_url"), + Option::<&()>::None, + "matrix get avatar url", + ) + .await + } + + /// Set a user's avatar URL. + pub async fn set_avatar_url( + &self, + user_id: &str, + body: &SetAvatarUrlRequest, + ) -> Result<()> { + self.transport + .send( + Method::PUT, + &format!("client/v3/profile/{user_id}/avatar_url"), + Some(body), + "matrix set avatar url", + ) + .await + } + + // ----------------------------------------------------------------------- + // Aliases + // ----------------------------------------------------------------------- + + /// Create a room alias. + pub async fn create_alias(&self, alias: &str, body: &CreateAliasRequest) -> Result<()> { + self.transport + .send( + Method::PUT, + &format!("client/v3/directory/room/{alias}"), + Some(body), + "matrix create alias", + ) + .await + } + + /// Resolve a room alias to a room ID. + pub async fn resolve_alias(&self, alias: &str) -> Result { + self.transport + .json( + Method::GET, + &format!("client/v3/directory/room/{alias}"), + Option::<&()>::None, + "matrix resolve alias", + ) + .await + } + + /// Delete a room alias. + pub async fn delete_alias(&self, alias: &str) -> Result<()> { + self.transport + .send( + Method::DELETE, + &format!("client/v3/directory/room/{alias}"), + Option::<&()>::None, + "matrix delete alias", + ) + .await + } + + // ----------------------------------------------------------------------- + // User Directory + // ----------------------------------------------------------------------- + + /// Search the user directory. + pub async fn search_users(&self, body: &UserSearchRequest) -> Result { + self.transport + .json( + Method::POST, + "client/v3/user_directory/search", + Some(body), + "matrix search users", + ) + .await + } + + // ----------------------------------------------------------------------- + // Media + // ----------------------------------------------------------------------- + + /// Upload media content. + pub async fn upload_media( + &self, + content_type: &str, + data: Vec, + ) -> Result { + let resp = self + .transport + .request(Method::POST, "media/v3/upload") + .header("Content-Type", content_type) + .body(data) + .send() + .await + .map_err(|e| crate::error::SunbeamError::network(format!( + "matrix upload media: request failed: {e}" + )))?; + + let status = resp.status(); + if !status.is_success() { + let body_text = resp.text().await.unwrap_or_default(); + return Err(crate::error::SunbeamError::network(format!( + "matrix upload media: HTTP {status}: {body_text}" + ))); + } + + resp.json::() + .await + .map_err(|e| crate::error::SunbeamError::network(format!( + "matrix upload media: failed to parse response: {e}" + ))) + } + + /// Download media content. + pub async fn download_media( + &self, + server: &str, + media_id: &str, + ) -> Result { + self.transport + .bytes( + Method::GET, + &format!("media/v3/download/{server}/{media_id}"), + "matrix download media", + ) + .await + } + + /// Download a thumbnail of media content. + pub async fn thumbnail( + &self, + server: &str, + media_id: &str, + params: &ThumbnailParams, + ) -> Result { + let mut path = format!( + "media/v3/thumbnail/{server}/{media_id}?width={}&height={}", + params.width, params.height + ); + if let Some(ref m) = params.method { + path.push_str(&format!("&method={m}")); + } + self.transport + .bytes(Method::GET, &path, "matrix thumbnail") + .await + } + + // ----------------------------------------------------------------------- + // Devices + // ----------------------------------------------------------------------- + + /// List all devices for the authenticated user. + pub async fn list_devices(&self) -> Result { + self.transport + .json( + Method::GET, + "client/v3/devices", + Option::<&()>::None, + "matrix list devices", + ) + .await + } + + /// Get information about a specific device. + pub async fn get_device(&self, device_id: &str) -> Result { + self.transport + .json( + Method::GET, + &format!("client/v3/devices/{device_id}"), + Option::<&()>::None, + "matrix get device", + ) + .await + } + + /// Update a device's metadata. + pub async fn update_device( + &self, + device_id: &str, + body: &UpdateDeviceRequest, + ) -> Result<()> { + self.transport + .send( + Method::PUT, + &format!("client/v3/devices/{device_id}"), + Some(body), + "matrix update device", + ) + .await + } + + /// Delete a device. + pub async fn delete_device( + &self, + device_id: &str, + body: &DeleteDeviceRequest, + ) -> Result<()> { + self.transport + .send( + Method::DELETE, + &format!("client/v3/devices/{device_id}"), + Some(body), + "matrix delete device", + ) + .await + } + + /// Delete multiple devices at once. + pub async fn batch_delete_devices(&self, body: &BatchDeleteDevicesRequest) -> Result<()> { + self.transport + .send( + Method::POST, + "client/v3/delete_devices", + Some(body), + "matrix batch delete devices", + ) + .await + } + + // ----------------------------------------------------------------------- + // E2EE / Keys + // ----------------------------------------------------------------------- + + /// Upload end-to-end encryption keys. + pub async fn upload_keys(&self, body: &KeysUploadRequest) -> Result { + self.transport + .json( + Method::POST, + "client/v3/keys/upload", + Some(body), + "matrix upload keys", + ) + .await + } + + /// Query users' device keys. + pub async fn query_keys(&self, body: &KeysQueryRequest) -> Result { + self.transport + .json( + Method::POST, + "client/v3/keys/query", + Some(body), + "matrix query keys", + ) + .await + } + + /// Claim one-time keys. + pub async fn claim_keys(&self, body: &KeysClaimRequest) -> Result { + self.transport + .json( + Method::POST, + "client/v3/keys/claim", + Some(body), + "matrix claim keys", + ) + .await + } + + // ----------------------------------------------------------------------- + // Push + // ----------------------------------------------------------------------- + + /// List pushers for the authenticated user. + pub async fn list_pushers(&self) -> Result { + self.transport + .json( + Method::GET, + "client/v3/pushers", + Option::<&()>::None, + "matrix list pushers", + ) + .await + } + + /// Set a pusher for the authenticated user. + pub async fn set_pusher(&self, body: &serde_json::Value) -> Result<()> { + self.transport + .send(Method::POST, "client/v3/pushers/set", Some(body), "matrix set pusher") + .await + } + + /// Get all push rules for the authenticated user. + pub async fn get_push_rules(&self) -> Result { + self.transport + .json( + Method::GET, + "client/v3/pushrules/", + Option::<&()>::None, + "matrix get push rules", + ) + .await + } + + /// Set a push rule. + pub async fn set_push_rule( + &self, + scope: &str, + kind: &str, + rule_id: &str, + body: &serde_json::Value, + ) -> Result<()> { + self.transport + .send( + Method::PUT, + &format!("client/v3/pushrules/{scope}/{kind}/{rule_id}"), + Some(body), + "matrix set push rule", + ) + .await + } + + /// Delete a push rule. + pub async fn delete_push_rule( + &self, + scope: &str, + kind: &str, + rule_id: &str, + ) -> Result<()> { + self.transport + .send( + Method::DELETE, + &format!("client/v3/pushrules/{scope}/{kind}/{rule_id}"), + Option::<&()>::None, + "matrix delete push rule", + ) + .await + } + + /// Get notifications for the authenticated user. + pub async fn get_notifications( + &self, + params: &NotificationsParams, + ) -> Result { + let mut path = "client/v3/notifications".to_string(); + let mut qs = Vec::new(); + if let Some(ref f) = params.from { + qs.push(format!("from={f}")); + } + if let Some(l) = params.limit { + qs.push(format!("limit={l}")); + } + if let Some(ref o) = params.only { + qs.push(format!("only={o}")); + } + if !qs.is_empty() { + path.push('?'); + path.push_str(&qs.join("&")); + } + self.transport + .json( + Method::GET, + &path, + Option::<&()>::None, + "matrix get notifications", + ) + .await + } + + // ----------------------------------------------------------------------- + // Account Data + // ----------------------------------------------------------------------- + + /// Get account data for a user. + pub async fn get_account_data( + &self, + user_id: &str, + data_type: &str, + ) -> Result { + self.transport + .json( + Method::GET, + &format!("client/v3/user/{user_id}/account_data/{data_type}"), + Option::<&()>::None, + "matrix get account data", + ) + .await + } + + /// Set account data for a user. + pub async fn set_account_data( + &self, + user_id: &str, + data_type: &str, + body: &serde_json::Value, + ) -> Result<()> { + self.transport + .send( + Method::PUT, + &format!("client/v3/user/{user_id}/account_data/{data_type}"), + Some(body), + "matrix set account data", + ) + .await + } + + // ----------------------------------------------------------------------- + // Tags + // ----------------------------------------------------------------------- + + /// Get tags for a room. + pub async fn get_tags(&self, user_id: &str, room_id: &str) -> Result { + self.transport + .json( + Method::GET, + &format!("client/v3/user/{user_id}/rooms/{room_id}/tags"), + Option::<&()>::None, + "matrix get tags", + ) + .await + } + + /// Set a tag on a room. + pub async fn set_tag( + &self, + user_id: &str, + room_id: &str, + tag: &str, + body: &serde_json::Value, + ) -> Result<()> { + self.transport + .send( + Method::PUT, + &format!("client/v3/user/{user_id}/rooms/{room_id}/tags/{tag}"), + Some(body), + "matrix set tag", + ) + .await + } + + /// Delete a tag from a room. + pub async fn delete_tag( + &self, + user_id: &str, + room_id: &str, + tag: &str, + ) -> Result<()> { + self.transport + .send( + Method::DELETE, + &format!("client/v3/user/{user_id}/rooms/{room_id}/tags/{tag}"), + Option::<&()>::None, + "matrix delete tag", + ) + .await + } + + // ----------------------------------------------------------------------- + // Search + // ----------------------------------------------------------------------- + + /// Search for messages in rooms. + pub async fn search_messages(&self, body: &SearchRequest) -> Result { + self.transport + .json( + Method::POST, + "client/v3/search", + Some(body), + "matrix search messages", + ) + .await + } + + // ----------------------------------------------------------------------- + // Filters + // ----------------------------------------------------------------------- + + /// Create a filter for a user. + pub async fn create_filter( + &self, + user_id: &str, + body: &serde_json::Value, + ) -> Result { + self.transport + .json( + Method::POST, + &format!("client/v3/user/{user_id}/filter"), + Some(body), + "matrix create filter", + ) + .await + } + + /// Get a previously created filter. + pub async fn get_filter(&self, user_id: &str, filter_id: &str) -> Result { + self.transport + .json( + Method::GET, + &format!("client/v3/user/{user_id}/filter/{filter_id}"), + Option::<&()>::None, + "matrix get filter", + ) + .await + } + + // ----------------------------------------------------------------------- + // Spaces + // ----------------------------------------------------------------------- + + /// Get the space hierarchy for a room. + pub async fn get_space_hierarchy( + &self, + room_id: &str, + params: &SpaceHierarchyParams, + ) -> Result { + let mut path = format!("client/v1/rooms/{room_id}/hierarchy"); + let mut qs = Vec::new(); + if let Some(ref f) = params.from { + qs.push(format!("from={f}")); + } + if let Some(l) = params.limit { + qs.push(format!("limit={l}")); + } + if let Some(d) = params.max_depth { + qs.push(format!("max_depth={d}")); + } + if let Some(s) = params.suggested_only { + qs.push(format!("suggested_only={s}")); + } + if !qs.is_empty() { + path.push('?'); + path.push_str(&qs.join("&")); + } + self.transport + .json(Method::GET, &path, Option::<&()>::None, "matrix get space hierarchy") + .await + } + + // ----------------------------------------------------------------------- + // Send-to-device + // ----------------------------------------------------------------------- + + /// Send an event to specific devices. + pub async fn send_to_device( + &self, + event_type: &str, + txn_id: &str, + body: &SendToDeviceRequest, + ) -> Result<()> { + self.transport + .send( + Method::PUT, + &format!("client/v3/sendToDevice/{event_type}/{txn_id}"), + Some(body), + "matrix send to device", + ) + .await + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + use crate::client::ServiceClient; + + #[test] + fn test_connect_url() { + let c = MatrixClient::connect("sunbeam.pt"); + assert_eq!(c.base_url(), "https://matrix.sunbeam.pt/_matrix"); + assert_eq!(c.service_name(), "matrix"); + } + + #[test] + fn test_from_parts() { + let c = MatrixClient::from_parts( + "http://localhost:8008/_matrix".into(), + AuthMethod::Bearer("tok123".into()), + ); + assert_eq!(c.base_url(), "http://localhost:8008/_matrix"); + } + + #[test] + fn test_connect_uses_bearer_auth() { + let c = MatrixClient::connect("example.com"); + assert!(matches!(c.transport.auth, AuthMethod::Bearer(_))); + } + + #[test] + fn test_set_token() { + let mut c = MatrixClient::connect("example.com"); + c.set_token("my-access-token"); + assert!( + matches!(c.transport.auth, AuthMethod::Bearer(ref s) if s == "my-access-token") + ); + } +} diff --git a/sunbeam-sdk/src/matrix/types.rs b/sunbeam-sdk/src/matrix/types.rs new file mode 100644 index 0000000..239b302 --- /dev/null +++ b/sunbeam-sdk/src/matrix/types.rs @@ -0,0 +1,882 @@ +//! Matrix client-server API types. + +use serde::{Deserialize, Serialize}; + +// --------------------------------------------------------------------------- +// Auth +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LoginTypesResponse { + #[serde(default)] + pub flows: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LoginRequest { + #[serde(rename = "type")] + pub login_type: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub identifier: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub password: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub token: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub device_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub initial_device_display_name: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub refresh_token: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LoginResponse { + #[serde(default)] + pub user_id: String, + #[serde(default)] + pub access_token: String, + #[serde(default)] + pub device_id: Option, + #[serde(default)] + pub home_server: Option, + #[serde(default)] + pub well_known: Option, + #[serde(default)] + pub refresh_token: Option, + #[serde(default)] + pub expires_in_ms: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RefreshRequest { + pub refresh_token: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RefreshResponse { + #[serde(default)] + pub access_token: String, + #[serde(default)] + pub refresh_token: Option, + #[serde(default)] + pub expires_in_ms: Option, +} + +// --------------------------------------------------------------------------- +// Account +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RegisterRequest { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub username: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub password: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub device_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub initial_device_display_name: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub inhibit_login: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub refresh_token: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub auth: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub kind: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RegisterResponse { + #[serde(default)] + pub user_id: String, + #[serde(default)] + pub access_token: Option, + #[serde(default)] + pub device_id: Option, + #[serde(default)] + pub home_server: Option, + #[serde(default)] + pub refresh_token: Option, + #[serde(default)] + pub expires_in_ms: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WhoamiResponse { + pub user_id: String, + #[serde(default)] + pub device_id: Option, + #[serde(default)] + pub is_guest: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ThirdPartyIds { + #[serde(default)] + pub threepids: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ThirdPartyId { + #[serde(default)] + pub medium: String, + #[serde(default)] + pub address: String, + #[serde(default)] + pub validated_at: Option, + #[serde(default)] + pub added_at: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Add3pidRequest { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub client_secret: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub sid: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub auth: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Delete3pidRequest { + pub medium: String, + pub address: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub id_server: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChangePasswordRequest { + pub new_password: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub logout_devices: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub auth: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeactivateRequest { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub auth: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub id_server: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub erase: Option, +} + +// --------------------------------------------------------------------------- +// Rooms +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CreateRoomRequest { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub topic: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub room_alias_name: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub visibility: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub preset: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub invite: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub is_direct: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub creation_content: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub initial_state: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub power_level_content_override: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CreateRoomResponse { + pub room_id: String, + #[serde(default)] + pub room_alias: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PublicRoomsResponse { + #[serde(default)] + pub chunk: Vec, + #[serde(default)] + pub next_batch: Option, + #[serde(default)] + pub prev_batch: Option, + #[serde(default)] + pub total_room_count_estimate: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PublicRoom { + #[serde(default)] + pub room_id: String, + #[serde(default)] + pub name: Option, + #[serde(default)] + pub topic: Option, + #[serde(default)] + pub canonical_alias: Option, + #[serde(default)] + pub num_joined_members: u64, + #[serde(default)] + pub world_readable: bool, + #[serde(default)] + pub guest_can_join: bool, + #[serde(default)] + pub avatar_url: Option, + #[serde(default)] + pub join_rule: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SearchPublicRoomsRequest { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub limit: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub since: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub filter: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub include_all_networks: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub third_party_instance_id: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RoomVisibility { + #[serde(default)] + pub visibility: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SetRoomVisibilityRequest { + pub visibility: String, +} + +// --------------------------------------------------------------------------- +// Membership +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct InviteRequest { + pub user_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub reason: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BanRequest { + pub user_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub reason: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UnbanRequest { + pub user_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub reason: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KickRequest { + pub user_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub reason: Option, +} + +// --------------------------------------------------------------------------- +// State +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StateEvent { + #[serde(rename = "type", default)] + pub event_type: String, + #[serde(default)] + pub state_key: String, + #[serde(default)] + pub content: serde_json::Value, + #[serde(default)] + pub sender: Option, + #[serde(default)] + pub event_id: Option, + #[serde(default)] + pub origin_server_ts: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EventIdResponse { + pub event_id: String, +} + +// --------------------------------------------------------------------------- +// Messages / Sync +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct SyncParams { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub filter: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub since: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub full_state: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub set_presence: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub timeout: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SyncResponse { + #[serde(default)] + pub next_batch: String, + #[serde(default)] + pub rooms: Option, + #[serde(default)] + pub presence: Option, + #[serde(default)] + pub account_data: Option, + #[serde(default)] + pub to_device: Option, + #[serde(default)] + pub device_lists: Option, + #[serde(default)] + pub device_one_time_keys_count: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Event { + #[serde(rename = "type", default)] + pub event_type: String, + #[serde(default)] + pub content: serde_json::Value, + #[serde(default)] + pub event_id: Option, + #[serde(default)] + pub sender: Option, + #[serde(default)] + pub origin_server_ts: Option, + #[serde(default)] + pub room_id: Option, + #[serde(default)] + pub state_key: Option, + #[serde(default)] + pub unsigned: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct MessagesParams { + pub dir: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub from: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub to: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub limit: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub filter: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MessagesResponse { + #[serde(default)] + pub start: String, + #[serde(default)] + pub end: Option, + #[serde(default)] + pub chunk: Vec, + #[serde(default)] + pub state: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContextResponse { + #[serde(default)] + pub start: Option, + #[serde(default)] + pub end: Option, + #[serde(default)] + pub event: Option, + #[serde(default)] + pub events_before: Vec, + #[serde(default)] + pub events_after: Vec, + #[serde(default)] + pub state: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RedactRequest { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub reason: Option, +} + +// --------------------------------------------------------------------------- +// Presence +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PresenceStatus { + #[serde(default)] + pub presence: String, + #[serde(default)] + pub last_active_ago: Option, + #[serde(default)] + pub status_msg: Option, + #[serde(default)] + pub currently_active: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SetPresenceRequest { + pub presence: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub status_msg: Option, +} + +// --------------------------------------------------------------------------- +// Typing +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TypingRequest { + pub typing: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub timeout: Option, +} + +// --------------------------------------------------------------------------- +// Receipts +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ReceiptRequest { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub thread_id: Option, +} + +// --------------------------------------------------------------------------- +// Profiles +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Profile { + #[serde(default)] + pub displayname: Option, + #[serde(default)] + pub avatar_url: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Displayname { + #[serde(default)] + pub displayname: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SetDisplaynameRequest { + pub displayname: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AvatarUrl { + #[serde(default)] + pub avatar_url: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SetAvatarUrlRequest { + pub avatar_url: String, +} + +// --------------------------------------------------------------------------- +// Aliases +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CreateAliasRequest { + pub room_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AliasResponse { + #[serde(default)] + pub room_id: Option, + #[serde(default)] + pub servers: Vec, +} + +// --------------------------------------------------------------------------- +// User Directory +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserSearchRequest { + pub search_term: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub limit: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserSearchResponse { + #[serde(default)] + pub results: Vec, + #[serde(default)] + pub limited: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserSearchResult { + #[serde(default)] + pub user_id: String, + #[serde(default)] + pub display_name: Option, + #[serde(default)] + pub avatar_url: Option, +} + +// --------------------------------------------------------------------------- +// Media +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UploadResponse { + pub content_uri: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ThumbnailParams { + pub width: u32, + pub height: u32, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub method: Option, +} + +// --------------------------------------------------------------------------- +// Devices +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DevicesResponse { + #[serde(default)] + pub devices: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Device { + #[serde(default)] + pub device_id: String, + #[serde(default)] + pub display_name: Option, + #[serde(default)] + pub last_seen_ip: Option, + #[serde(default)] + pub last_seen_ts: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UpdateDeviceRequest { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub display_name: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeleteDeviceRequest { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub auth: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BatchDeleteDevicesRequest { + pub devices: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub auth: Option, +} + +// --------------------------------------------------------------------------- +// E2EE / Keys +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KeysUploadRequest { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub device_keys: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub one_time_keys: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub fallback_keys: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KeysUploadResponse { + #[serde(default)] + pub one_time_key_counts: serde_json::Value, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KeysQueryRequest { + pub device_keys: serde_json::Value, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub timeout: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KeysQueryResponse { + #[serde(default)] + pub device_keys: serde_json::Value, + #[serde(default)] + pub failures: Option, + #[serde(default)] + pub master_keys: Option, + #[serde(default)] + pub self_signing_keys: Option, + #[serde(default)] + pub user_signing_keys: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KeysClaimRequest { + pub one_time_keys: serde_json::Value, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub timeout: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KeysClaimResponse { + #[serde(default)] + pub one_time_keys: serde_json::Value, + #[serde(default)] + pub failures: Option, +} + +// --------------------------------------------------------------------------- +// Push +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PushersResponse { + #[serde(default)] + pub pushers: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PushRulesResponse { + #[serde(default)] + pub global: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct NotificationsParams { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub from: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub limit: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub only: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NotificationsResponse { + #[serde(default)] + pub notifications: Vec, + #[serde(default)] + pub next_token: Option, +} + +// --------------------------------------------------------------------------- +// Tags +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TagsResponse { + #[serde(default)] + pub tags: serde_json::Value, +} + +// --------------------------------------------------------------------------- +// Search +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SearchRequest { + pub search_categories: serde_json::Value, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SearchResponse { + #[serde(default)] + pub search_categories: serde_json::Value, +} + +// --------------------------------------------------------------------------- +// Filters +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FilterIdResponse { + pub filter_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Filter { + #[serde(default)] + pub event_fields: Option>, + #[serde(default)] + pub event_format: Option, + #[serde(default)] + pub presence: Option, + #[serde(default)] + pub account_data: Option, + #[serde(default)] + pub room: Option, +} + +// --------------------------------------------------------------------------- +// Spaces +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct SpaceHierarchyParams { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub from: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub limit: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub max_depth: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub suggested_only: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SpaceHierarchy { + #[serde(default)] + pub rooms: Vec, + #[serde(default)] + pub next_batch: Option, +} + +// --------------------------------------------------------------------------- +// Send-to-device +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SendToDeviceRequest { + pub messages: serde_json::Value, +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_login_request_serialize() { + let req = LoginRequest { + login_type: "m.login.password".into(), + identifier: Some(serde_json::json!({ + "type": "m.id.user", + "user": "@alice:example.com" + })), + password: Some("secret".into()), + token: None, + device_id: None, + initial_device_display_name: None, + refresh_token: None, + }; + let json = serde_json::to_value(&req).unwrap(); + assert_eq!(json["type"], "m.login.password"); + assert_eq!(json["password"], "secret"); + assert!(json.get("token").is_none()); + } + + #[test] + fn test_login_response_deserialize() { + let json = serde_json::json!({ + "user_id": "@alice:example.com", + "access_token": "tok123", + "device_id": "DEV1" + }); + let resp: LoginResponse = serde_json::from_value(json).unwrap(); + assert_eq!(resp.user_id, "@alice:example.com"); + assert_eq!(resp.access_token, "tok123"); + assert_eq!(resp.device_id.as_deref(), Some("DEV1")); + } + + #[test] + fn test_sync_response_deserialize_minimal() { + let json = serde_json::json!({ "next_batch": "s123" }); + let resp: SyncResponse = serde_json::from_value(json).unwrap(); + assert_eq!(resp.next_batch, "s123"); + assert!(resp.rooms.is_none()); + } + + #[test] + fn test_create_room_request_skip_none() { + let req = CreateRoomRequest { + name: Some("Test Room".into()), + topic: None, + room_alias_name: None, + visibility: None, + preset: None, + invite: None, + is_direct: None, + creation_content: None, + initial_state: None, + power_level_content_override: None, + }; + let json = serde_json::to_value(&req).unwrap(); + assert_eq!(json["name"], "Test Room"); + assert!(json.get("topic").is_none()); + } + + #[test] + fn test_event_deserialize() { + let json = serde_json::json!({ + "type": "m.room.message", + "content": { "body": "hello", "msgtype": "m.text" }, + "event_id": "$abc123", + "sender": "@bob:example.com", + "origin_server_ts": 1234567890 + }); + let ev: Event = serde_json::from_value(json).unwrap(); + assert_eq!(ev.event_type, "m.room.message"); + assert_eq!(ev.sender.as_deref(), Some("@bob:example.com")); + } + + #[test] + fn test_device_deserialize() { + let json = serde_json::json!({ + "device_id": "DEV1", + "display_name": "My Phone", + "last_seen_ip": "10.0.0.1", + "last_seen_ts": 1700000000000u64 + }); + let d: Device = serde_json::from_value(json).unwrap(); + assert_eq!(d.device_id, "DEV1"); + assert_eq!(d.display_name.as_deref(), Some("My Phone")); + } + + #[test] + fn test_profile_deserialize() { + let json = serde_json::json!({ + "displayname": "Alice", + "avatar_url": "mxc://example.com/abc" + }); + let p: Profile = serde_json::from_value(json).unwrap(); + assert_eq!(p.displayname.as_deref(), Some("Alice")); + assert_eq!(p.avatar_url.as_deref(), Some("mxc://example.com/abc")); + } +}