From 5d2eab119dda8abcfee86c76e38198071b58fe16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 25 Mar 2026 13:07:58 +0100 Subject: [PATCH] feat: Improve getting homeserver capabilities This extracts the `/capabilities` logic to its own `HomeserverCapabilities` component in the SDK that can be manually asked to fetch, cache locally and return these capabilities. --- bindings/matrix-sdk-ffi/CHANGELOG.md | 2 + bindings/matrix-sdk-ffi/src/client.rs | 72 +++++ .../matrix-sdk-base/src/store/memory_store.rs | 14 + crates/matrix-sdk-base/src/store/traits.rs | 22 +- .../src/state_store/mod.rs | 13 + crates/matrix-sdk-sqlite/src/state_store.rs | 11 + crates/matrix-sdk/CHANGELOG.md | 3 + crates/matrix-sdk/src/account.rs | 8 +- .../src/client/homeserver_capabilities.rs | 258 ++++++++++++++++++ crates/matrix-sdk/src/client/mod.rs | 35 +-- crates/matrix-sdk/src/event_handler/mod.rs | 6 +- crates/matrix-sdk/src/lib.rs | 1 + crates/matrix-sdk/src/test_utils/mocks/mod.rs | 22 ++ 13 files changed, 434 insertions(+), 33 deletions(-) create mode 100644 crates/matrix-sdk/src/client/homeserver_capabilities.rs diff --git a/bindings/matrix-sdk-ffi/CHANGELOG.md b/bindings/matrix-sdk-ffi/CHANGELOG.md index 712c97fc9..7acfd2792 100644 --- a/bindings/matrix-sdk-ffi/CHANGELOG.md +++ b/bindings/matrix-sdk-ffi/CHANGELOG.md @@ -47,6 +47,8 @@ All notable changes to this project will be documented in this file. accepts a `SyncListenerV2` callback that receives a `SyncResponseV2` after each successful sync. ([#6359](https://github.com/matrix-org/matrix-rust-sdk/pull/6359)) +- Added `HomeserverCapabilities` and `Client::homeserver_capabilities()` to get the capabilities + of the homeserver. ([#6371](https://github.com/matrix-org/matrix-rust-sdk/pull/6371)) - Expose `Room.send_state_event_raw()` for sending arbitrary state events through the FFI layer. ([#6350](https://github.com/matrix-org/matrix-rust-sdk/pull/6350)) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 11711a8ba..b4c52ab8c 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -2154,6 +2154,10 @@ impl Client { } })))) } + + pub fn homeserver_capabilities(&self) -> HomeserverCapabilities { + HomeserverCapabilities::new(self.inner.homeserver_capabilities()) + } } #[cfg(feature = "experimental-element-recent-emojis")] @@ -3039,6 +3043,74 @@ impl From for StoreSizes { } } +#[derive(uniffi::Object)] +pub struct HomeserverCapabilities { + inner: matrix_sdk::HomeserverCapabilities, +} + +impl HomeserverCapabilities { + pub(crate) fn new(capabilities: matrix_sdk::HomeserverCapabilities) -> Self { + Self { inner: capabilities } + } +} + +#[matrix_sdk_ffi_macros::export] +impl HomeserverCapabilities { + pub async fn refresh(&self) -> Result<(), ClientError> { + Ok(self.inner.refresh().await?) + } + + pub async fn can_change_password(&self) -> Result { + Ok(self.inner.can_change_password().await?) + } + + pub async fn can_change_displayname(&self) -> Result { + Ok(self.inner.can_change_displayname().await?) + } + + pub async fn can_change_avatar(&self) -> Result { + Ok(self.inner.can_change_avatar().await?) + } + + pub async fn can_change_thirdparty_ids(&self) -> Result { + Ok(self.inner.can_change_thirdparty_ids().await?) + } + + pub async fn can_get_login_token(&self) -> Result { + Ok(self.inner.can_get_login_token().await?) + } + + pub async fn extended_profile_fields(&self) -> Result { + let profile_fields = self.inner.extended_profile_fields().await?; + Ok(ExtendedProfileFields { + enabled: profile_fields.enabled, + allowed: profile_fields + .allowed + .unwrap_or_default() + .iter() + .map(ToString::to_string) + .collect(), + disallowed: profile_fields + .disallowed + .unwrap_or_default() + .iter() + .map(ToString::to_string) + .collect(), + }) + } + + pub async fn forgets_room_when_leaving(&self) -> Result { + Ok(self.inner.forgets_room_when_leaving().await?) + } +} + +#[derive(uniffi::Record)] +pub struct ExtendedProfileFields { + pub enabled: bool, + pub allowed: Vec, + pub disallowed: Vec, +} + #[cfg(test)] mod tests { use ruma::{ diff --git a/crates/matrix-sdk-base/src/store/memory_store.rs b/crates/matrix-sdk-base/src/store/memory_store.rs index 56c83b756..73067a8dc 100644 --- a/crates/matrix-sdk-base/src/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/store/memory_store.rs @@ -24,6 +24,7 @@ use matrix_sdk_common::{ROOM_VERSION_FALLBACK, ROOM_VERSION_RULES_FALLBACK}; use ruma::{ CanonicalJsonObject, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UserId, + api::client::discovery::get_capabilities::v3::Capabilities, canonical_json::{RedactedBecause, redact}, events::{ AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnyStrippedStateEvent, @@ -92,6 +93,7 @@ struct MemoryStoreInner { seen_knock_requests: BTreeMap>, thread_subscriptions: BTreeMap>, thread_subscriptions_catchup_tokens: Option>, + homeserver_capabilities: Option, } /// In-memory, non-persistent implementation of the `StateStore`. @@ -195,6 +197,10 @@ impl StateStore for MemoryStore { .thread_subscriptions_catchup_tokens .clone() .map(StateStoreDataValue::ThreadSubscriptionsCatchupTokens), + StateStoreDataKey::HomeserverCapabilities => inner + .homeserver_capabilities + .clone() + .map(StateStoreDataValue::HomeserverCapabilities), }) } @@ -270,6 +276,13 @@ impl StateStore for MemoryStore { "Session data is not a list of thread subscription catchup tokens", )); } + StateStoreDataKey::HomeserverCapabilities => { + inner.homeserver_capabilities = Some( + value + .into_homeserver_capabilities() + .expect("Session data is not a homeserver capabilities"), + ); + } } Ok(()) @@ -304,6 +317,7 @@ impl StateStore for MemoryStore { StateStoreDataKey::ThreadSubscriptionsCatchupTokens => { inner.thread_subscriptions_catchup_tokens = None; } + StateStoreDataKey::HomeserverCapabilities => inner.homeserver_capabilities = None, } Ok(()) } diff --git a/crates/matrix-sdk-base/src/store/traits.rs b/crates/matrix-sdk-base/src/store/traits.rs index 956347792..283435322 100644 --- a/crates/matrix-sdk-base/src/store/traits.rs +++ b/crates/matrix-sdk-base/src/store/traits.rs @@ -28,8 +28,11 @@ use ruma::{ OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UserId, api::{ SupportedVersions, - client::discovery::discover_homeserver::{ - self, HomeserverInfo, IdentityServerInfo, RtcFocusInfo, TileServerInfo, + client::discovery::{ + discover_homeserver::{ + self, HomeserverInfo, IdentityServerInfo, RtcFocusInfo, TileServerInfo, + }, + get_capabilities::v3::Capabilities, }, }, events::{ @@ -1172,6 +1175,9 @@ pub enum StateStoreDataValue { /// See documentation of [`ThreadSubscriptionCatchupToken`] for more /// details. ThreadSubscriptionsCatchupTokens(Vec), + + /// The capabilities the homeserver supports or disables. + HomeserverCapabilities(Capabilities), } /// Tokens to use when catching up on thread subscriptions. @@ -1373,6 +1379,12 @@ impl StateStoreDataValue { ) -> Option> { as_variant!(self, Self::ThreadSubscriptionsCatchupTokens) } + + /// Get this value if it is the data for the capabilities the homeserver + /// supports or disables. + pub fn into_homeserver_capabilities(self) -> Option { + as_variant!(self, Self::HomeserverCapabilities) + } } /// A key for key-value data. @@ -1415,6 +1427,9 @@ pub enum StateStoreDataKey<'a> { /// A list of thread subscriptions catchup tokens. ThreadSubscriptionsCatchupTokens, + + /// A list of capabilities that the homeserver supports. + HomeserverCapabilities, } impl StateStoreDataKey<'_> { @@ -1460,6 +1475,9 @@ impl StateStoreDataKey<'_> { /// [`ThreadSubscriptionsCatchupTokens`][Self::ThreadSubscriptionsCatchupTokens] variant. pub const THREAD_SUBSCRIPTIONS_CATCHUP_TOKENS: &'static str = "thread_subscriptions_catchup_tokens"; + + /// Key prefix to use for the homeserver's [`Capabilities`]. + pub const HOMESERVER_CAPABILITIES: &'static str = "homeserver_capabilities"; } /// Compare two thread subscription changes bump stamps, given a fixed room and diff --git a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs index 1419a2025..5deb71331 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs @@ -42,6 +42,7 @@ use matrix_sdk_store_encryption::{Error as EncryptionError, StoreCipher}; use ruma::{ CanonicalJsonObject, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UserId, + api::client::discovery::get_capabilities::v3::Capabilities, canonical_json::{RedactedBecause, redact}, events::{ AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnySyncStateEvent, @@ -506,6 +507,9 @@ impl IndexeddbStateStore { StateStoreDataKey::ThreadSubscriptionsCatchupTokens => { self.encode_key(keys::KV, StateStoreDataKey::THREAD_SUBSCRIPTIONS_CATCHUP_TOKENS) } + StateStoreDataKey::HomeserverCapabilities => { + self.encode_key(keys::KV, StateStoreDataKey::HOMESERVER_CAPABILITIES) + } } } } @@ -668,6 +672,10 @@ impl_state_store!({ .map(|f| self.deserialize_value::>(&f)) .transpose()? .map(StateStoreDataValue::ThreadSubscriptionsCatchupTokens), + StateStoreDataKey::HomeserverCapabilities => value + .map(|f| self.deserialize_value::(&f)) + .transpose()? + .map(StateStoreDataValue::HomeserverCapabilities), }; Ok(value) @@ -719,6 +727,11 @@ impl_state_store!({ .into_thread_subscriptions_catchup_tokens() .expect("Session data is not a list of thread subscription catchup tokens"), ), + StateStoreDataKey::HomeserverCapabilities => self.serialize_value( + &value + .into_homeserver_capabilities() + .expect("Session data is not a homeserver capabilities"), + ), }; let tx = self.inner.transaction(keys::KV).with_mode(TransactionMode::Readwrite).build()?; diff --git a/crates/matrix-sdk-sqlite/src/state_store.rs b/crates/matrix-sdk-sqlite/src/state_store.rs index da61766dc..e378742d5 100644 --- a/crates/matrix-sdk-sqlite/src/state_store.rs +++ b/crates/matrix-sdk-sqlite/src/state_store.rs @@ -515,6 +515,9 @@ impl SqliteStateStore { StateStoreDataKey::ThreadSubscriptionsCatchupTokens => { Cow::Borrowed(StateStoreDataKey::THREAD_SUBSCRIPTIONS_CATCHUP_TOKENS) } + StateStoreDataKey::HomeserverCapabilities => { + Cow::Borrowed(StateStoreDataKey::HOMESERVER_CAPABILITIES) + } }; self.encode_key(keys::KV_BLOB, &*key_s) @@ -1161,6 +1164,9 @@ impl StateStore for SqliteStateStore { self.deserialize_value(&data)?, ) } + StateStoreDataKey::HomeserverCapabilities => { + StateStoreDataValue::HomeserverCapabilities(self.deserialize_value(&data)?) + } }) }) .transpose() @@ -1211,6 +1217,11 @@ impl StateStore for SqliteStateStore { .into_thread_subscriptions_catchup_tokens() .expect("Session data is not a list of thread subscription catchup tokens"), )?, + StateStoreDataKey::HomeserverCapabilities => self.serialize_value( + &value + .into_homeserver_capabilities() + .expect("Session data is not the homeserver capabilities"), + )?, }; self.write() diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index ae7a11443..2c4ca5eaa 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -8,6 +8,9 @@ All notable changes to this project will be documented in this file. ### Features +- [**breaking**] Added `HomeserverCapabilities` and `Client::homeserver_capabilities()` to get the capabilities + of the homeserver. This replaces `Client::get_capabilities()`. + ([#6371](https://github.com/matrix-org/matrix-rust-sdk/pull/6371)) - [**breaking**] `matrix_sdk::error::Error` has a new variant `Timeout` which occurs when a cross-signing reset does not succeed after some period of time. ([#6325](https://github.com/matrix-org/matrix-rust-sdk/pull/6325)) diff --git a/crates/matrix-sdk/src/account.rs b/crates/matrix-sdk/src/account.rs index 89f1ff89e..ea3d18929 100644 --- a/crates/matrix-sdk/src/account.rs +++ b/crates/matrix-sdk/src/account.rs @@ -437,8 +437,8 @@ impl Account { /// Set the given field of our own user's profile. /// - /// [`Client::get_capabilities()`] should be called first to check it the - /// field can be set on the homeserver. + /// [`Client::homeserver_capabilities()`] should be called first to check it + /// the field can be set on the homeserver. /// /// # Arguments /// @@ -457,8 +457,8 @@ impl Account { /// Delete the given field of our own user's profile. /// - /// [`Client::get_capabilities()`] should be called first to check it the - /// field can be modified on the homeserver. + /// [`Client::homeserver_capabilities()`] should be called first to check it + /// the field can be modified on the homeserver. /// /// # Arguments /// diff --git a/crates/matrix-sdk/src/client/homeserver_capabilities.rs b/crates/matrix-sdk/src/client/homeserver_capabilities.rs new file mode 100644 index 000000000..6b3e69b40 --- /dev/null +++ b/crates/matrix-sdk/src/client/homeserver_capabilities.rs @@ -0,0 +1,258 @@ +use matrix_sdk_base::{StateStoreDataKey, StateStoreDataValue}; +use ruma::api::client::{ + discovery::{ + get_capabilities, + get_capabilities::v3::{Capabilities, ProfileFieldsCapability}, + }, + profile::ProfileFieldName, +}; +use tracing::warn; + +use crate::{Client, HttpResult}; + +/// Helper to check what [`Capabilities`] are supported by the homeserver. +/// +/// Spec: +#[derive(Debug, Clone)] +pub struct HomeserverCapabilities { + client: Client, +} + +impl HomeserverCapabilities { + /// Creates a new [`HomeserverCapabilities`] instance. + pub fn new(client: Client) -> Self { + Self { client } + } + + /// Forces a refresh of the cached value using the `/capabilities` endpoint. + pub async fn refresh(&self) -> crate::Result<()> { + self.get_and_cache_remote_capabilities().await?; + Ok(()) + } + + /// Returns whether the user can change their password or not. + pub async fn can_change_password(&self) -> crate::Result { + let capabilities = self.load_or_fetch_homeserver_capabilities().await?; + Ok(capabilities.change_password.enabled) + } + + /// Returns whether the user can change their display name or not. + /// + /// This will first check the `m.profile_fields` capability and use it if + /// present, or fall back to `m.set_displayname` otherwise. + /// + /// Spec: + pub async fn can_change_displayname(&self) -> crate::Result { + let capabilities = self.load_or_fetch_homeserver_capabilities().await?; + if let Some(profile_fields) = capabilities.profile_fields + && profile_fields.enabled + { + let allowed = profile_fields.allowed.unwrap_or_default(); + let disallowed = profile_fields.disallowed.unwrap_or_default(); + return Ok(allowed.contains(&ProfileFieldName::DisplayName) + || !disallowed.contains(&ProfileFieldName::DisplayName)); + } + #[allow(deprecated)] + Ok(capabilities.set_displayname.enabled) + } + + /// Returns whether the user can change their avatar or not. + /// + /// This will first check the `m.profile_fields` capability and use it if + /// present, or fall back to `m.set_avatar_url` otherwise. + /// + /// Spec: + pub async fn can_change_avatar(&self) -> crate::Result { + let capabilities = self.load_or_fetch_homeserver_capabilities().await?; + if let Some(profile_fields) = capabilities.profile_fields + && profile_fields.enabled + { + let allowed = profile_fields.allowed.unwrap_or_default(); + let disallowed = profile_fields.disallowed.unwrap_or_default(); + return Ok(allowed.contains(&ProfileFieldName::AvatarUrl) + || !disallowed.contains(&ProfileFieldName::AvatarUrl)); + } + #[allow(deprecated)] + Ok(capabilities.set_avatar_url.enabled) + } + + /// Returns whether the user can add, remove, or change 3PID associations on + /// their account. + /// + /// Spec: + pub async fn can_change_thirdparty_ids(&self) -> crate::Result { + let capabilities = self.load_or_fetch_homeserver_capabilities().await?; + Ok(capabilities.thirdparty_id_changes.enabled) + } + + /// Returns whether the user is able to use `POST /login/get_token` to + /// generate single-use, time-limited tokens to log unauthenticated + /// clients into their account. + /// + /// When not listed, clients SHOULD assume the user is unable to generate + /// tokens. + /// + /// Spec: + pub async fn can_get_login_token(&self) -> crate::Result { + let capabilities = self.load_or_fetch_homeserver_capabilities().await?; + Ok(capabilities.get_login_token.enabled) + } + + /// Returns which profile fields the user is able to change. + /// + /// Spec: + pub async fn extended_profile_fields(&self) -> crate::Result { + let capabilities = self.load_or_fetch_homeserver_capabilities().await?; + if let Some(profile_fields) = capabilities.profile_fields { + return Ok(profile_fields); + } + Ok(ProfileFieldsCapability::new(false)) + } + + /// Returns whether or not the server automatically forgets rooms which the + /// user has left. + /// + /// Spec: + pub async fn forgets_room_when_leaving(&self) -> crate::Result { + let capabilities = self.load_or_fetch_homeserver_capabilities().await?; + Ok(capabilities.forget_forced_upon_leave.enabled) + } + + /// Gets the supported [`Capabilities`] either from the local cache or from + /// the homeserver using the `/capabilities` endpoint if the data is not + /// cached. + /// + /// To ensure you get updated values, you should call [`Self::refresh`] + /// instead. + async fn load_or_fetch_homeserver_capabilities(&self) -> crate::Result { + match self.client.state_store().get_kv_data(StateStoreDataKey::HomeserverCapabilities).await + { + Ok(Some(stored)) => { + if let Some(capabilities) = stored.into_homeserver_capabilities() { + return Ok(capabilities); + } + } + Ok(None) => { + // fallthrough: cache is empty + } + Err(err) => { + warn!("error when loading cached homeserver capabilities: {err}"); + // fallthrough to network. + } + } + + Ok(self.get_and_cache_remote_capabilities().await?) + } + + /// Gets and caches the capabilities of the homeserver. + async fn get_and_cache_remote_capabilities(&self) -> HttpResult { + let res = self.client.send(get_capabilities::v3::Request::new()).await?; + + if let Err(err) = self + .client + .state_store() + .set_kv_data( + StateStoreDataKey::HomeserverCapabilities, + StateStoreDataValue::HomeserverCapabilities(res.capabilities.clone()), + ) + .await + { + warn!("error when caching homeserver capabilities: {err}"); + } + + Ok(res.capabilities) + } +} + +#[cfg(all(not(target_family = "wasm"), test))] +mod tests { + use matrix_sdk_test::async_test; + + use super::*; + use crate::test_utils::mocks::MatrixMockServer; + + #[async_test] + async fn test_refresh_always_updates_capabilities() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + // Set the expected capabilities to something we can check + let mut expected_capabilities = Capabilities::default(); + expected_capabilities.change_password.enabled = true; + server + .mock_get_homeserver_capabilities() + .ok_with_capabilities(expected_capabilities) + .mock_once() + .mount() + .await; + + // Refresh the capabilities + let capabilities = client.homeserver_capabilities(); + capabilities.refresh().await.expect("refreshing capabilities failed"); + + // Check the values we get are updated + assert!(capabilities.can_change_password().await.expect("checking capabilities failed")); + + let mut expected_capabilities = Capabilities::default(); + expected_capabilities.change_password.enabled = false; + server + .mock_get_homeserver_capabilities() + .ok_with_capabilities(expected_capabilities) + .mock_once() + .mount() + .await; + + // Check the values we get are not updated without a refresh, they're loaded + // from the cache + assert!(capabilities.can_change_password().await.expect("checking capabilities failed")); + + // Do another refresh to make sure we get the updated values + capabilities.refresh().await.expect("refreshing capabilities failed"); + + // Check the values we get are updated + assert!(!capabilities.can_change_password().await.expect("checking capabilities failed")); + } + + #[async_test] + async fn test_get_functions_refresh_the_data_if_not_available_or_use_cache_if_available() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + // Set the expected capabilities to something we can check + let mut expected_capabilities = Capabilities::default(); + let mut profile_fields = ProfileFieldsCapability::new(true); + profile_fields.allowed = Some(vec![ProfileFieldName::DisplayName]); + expected_capabilities.profile_fields = Some(profile_fields); + server + .mock_get_homeserver_capabilities() + .ok_with_capabilities(expected_capabilities) + // Ensure it's called just once + .mock_once() + .mount() + .await; + + // Refresh the capabilities + let capabilities = client.homeserver_capabilities(); + + // Check the values we get are updated + assert!(capabilities.can_change_displayname().await.expect("checking capabilities failed")); + + // Now revert the previous mock so we can check we're getting the cached value + // instead of this one + let mut expected_capabilities = Capabilities::default(); + let mut profile_fields = ProfileFieldsCapability::new(true); + profile_fields.disallowed = Some(vec![ProfileFieldName::DisplayName]); + expected_capabilities.profile_fields = Some(profile_fields); + server + .mock_get_homeserver_capabilities() + .ok_with_capabilities(expected_capabilities) + // Ensure it's not called, since we'd be using the cached values instead + .never() + .mount() + .await; + + // Check the values we get are not updated without a refresh, they're loaded + // from the cache + assert!(capabilities.can_change_displayname().await.expect("checking capabilities failed")); + } +} diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index ee8e30f3e..a3767411e 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -59,7 +59,6 @@ use ruma::{ directory::{get_public_rooms, get_public_rooms_filtered}, discovery::{ discover_homeserver::{self, RtcFocusInfo}, - get_capabilities::{self, v3::Capabilities}, get_supported_versions, }, error::{ErrorKind, UnknownTokenErrorData}, @@ -98,7 +97,10 @@ use crate::{ AuthCtx, AuthData, ReloadSessionCallback, SaveSessionCallback, matrix::MatrixAuth, oauth::OAuth, }, - client::thread_subscriptions::ThreadSubscriptionCatchup, + client::{ + homeserver_capabilities::HomeserverCapabilities, + thread_subscriptions::ThreadSubscriptionCatchup, + }, config::{RequestConfig, SyncToken}, deduplicating_handler::DeduplicatingHandler, error::HttpResult, @@ -129,6 +131,7 @@ use crate::{ mod builder; pub(crate) mod caches; pub(crate) mod futures; +pub(crate) mod homeserver_capabilities; pub(crate) mod thread_subscriptions; pub use self::builder::{ClientBuildError, ClientBuilder, sanitize_server_name}; @@ -567,30 +570,10 @@ impl Client { Ok(()) } - /// Get the capabilities of the homeserver. - /// - /// This method should be used to check what features are supported by the - /// homeserver. - /// - /// # Examples - /// - /// ```no_run - /// # use matrix_sdk::Client; - /// # use url::Url; - /// # async { - /// # let homeserver = Url::parse("http://example.com")?; - /// let client = Client::new(homeserver).await?; - /// - /// let capabilities = client.get_capabilities().await?; - /// - /// if capabilities.change_password.enabled { - /// // Change password - /// } - /// # anyhow::Ok(()) }; - /// ``` - pub async fn get_capabilities(&self) -> HttpResult { - let res = self.send(get_capabilities::v3::Request::new()).await?; - Ok(res.capabilities) + /// Retrieves a helper component to access the [`HomeserverCapabilities`] + /// supported or disabled by the homeserver. + pub fn homeserver_capabilities(&self) -> HomeserverCapabilities { + HomeserverCapabilities::new(self.clone()) } /// Get the server vendor information from the federation API. diff --git a/crates/matrix-sdk/src/event_handler/mod.rs b/crates/matrix-sdk/src/event_handler/mod.rs index 74debf5b0..946066d26 100644 --- a/crates/matrix-sdk/src/event_handler/mod.rs +++ b/crates/matrix-sdk/src/event_handler/mod.rs @@ -1052,7 +1052,11 @@ mod tests { // All of Client's async methods that do network requests (and // possibly some that don't) are `!Send` on wasm. We obviously want // to be able to use them in event handlers. - let _caps = client.get_capabilities().await.map_err(|e| anyhow::anyhow!("{}", e))?; + client + .homeserver_capabilities() + .refresh() + .await + .map_err(|e| anyhow::anyhow!("{}", e))?; anyhow::Ok(()) }); } diff --git a/crates/matrix-sdk/src/lib.rs b/crates/matrix-sdk/src/lib.rs index 6f502110b..d459ba2d3 100644 --- a/crates/matrix-sdk/src/lib.rs +++ b/crates/matrix-sdk/src/lib.rs @@ -65,6 +65,7 @@ pub mod widget; pub use account::Account; pub use authentication::{AuthApi, AuthSession, SessionTokens}; +pub use client::homeserver_capabilities::HomeserverCapabilities; #[cfg(feature = "experimental-search")] pub mod search_index; pub use client::{ diff --git a/crates/matrix-sdk/src/test_utils/mocks/mod.rs b/crates/matrix-sdk/src/test_utils/mocks/mod.rs index 25600a98b..0ef82b876 100644 --- a/crates/matrix-sdk/src/test_utils/mocks/mod.rs +++ b/crates/matrix-sdk/src/test_utils/mocks/mod.rs @@ -35,6 +35,7 @@ use ruma::{ DeviceId, EventId, MilliSecondsSinceUnixEpoch, MxcUri, OwnedDeviceId, OwnedEventId, OwnedOneTimeKeyId, OwnedRoomId, OwnedUserId, RoomId, ServerName, UserId, api::client::{ + discovery::get_capabilities::v3::Capabilities, profile::{ProfileFieldName, ProfileFieldValue}, receipt::create_receipt::v3::ReceiptType, room::Visibility, @@ -1685,6 +1686,15 @@ impl MatrixMockServer { Mock::given(method("GET")).and(path(format!("/_matrix/client/v3/profile/{user_id}"))); self.mock_endpoint(mock, GetProfileEndpoint) } + + /// Create a prebuilt mock for the endpoint used to get the capabilities of + /// the homeserver. + pub fn mock_get_homeserver_capabilities( + &self, + ) -> MockEndpoint<'_, GetHomeserverCapabilitiesEndpoint> { + let mock = Mock::given(method("GET")).and(path("/_matrix/client/v3/capabilities")); + self.mock_endpoint(mock, GetHomeserverCapabilitiesEndpoint) + } } /// A specification for a push rule ID. @@ -4916,3 +4926,15 @@ impl<'a> MockEndpoint<'a, GetProfileEndpoint> { self.respond_with(ResponseTemplate::new(200).set_body_json(profile)) } } + +/// A prebuilt mock for `GET /_matrix/client/*/capabilities`. +pub struct GetHomeserverCapabilitiesEndpoint; + +impl<'a> MockEndpoint<'a, GetHomeserverCapabilitiesEndpoint> { + /// Returns a successful empty response. + pub fn ok_with_capabilities(self, capabilities: Capabilities) -> MatrixMock<'a> { + self.respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "capabilities": capabilities, + }))) + } +}