use std::sync::Arc;
use matrix_sdk_base::{StateStoreDataKey, StateStoreDataValue, StoreError, ttl::TtlValue};
use ruma::{
api::{
Metadata,
client::{
discovery::get_capabilities::{
self,
v3::{
AccountModerationCapability, Capabilities, ProfileFieldsCapability,
RoomVersionsCapability,
},
},
profile::delete_profile_field,
},
},
profile::ProfileFieldName,
};
use tracing::{debug, warn};
use crate::{Client, HttpError, HttpResult, client::caches::CachedValue};
#[derive(Debug, Clone)]
pub struct HomeserverCapabilities {
client: Client,
}
impl HomeserverCapabilities {
pub fn new(client: Client) -> Self {
Self { client }
}
pub async fn refresh(&self) -> crate::Result<()> {
self.get_and_cache_remote_capabilities().await?;
Ok(())
}
pub async fn can_change_password(&self) -> crate::Result<bool> {
let capabilities = self.load_or_fetch_homeserver_capabilities().await?;
Ok(capabilities.change_password.enabled)
}
pub async fn can_change_displayname(&self) -> crate::Result<bool> {
let capabilities = self.profile_capabilities().await?;
if let Some(profile_fields) = capabilities.profile_fields {
Ok(profile_fields.can_set_field(&ProfileFieldName::DisplayName))
} else {
Ok(capabilities.set_displayname)
}
}
pub async fn can_change_avatar(&self) -> crate::Result<bool> {
let capabilities = self.profile_capabilities().await?;
if let Some(profile_fields) = capabilities.profile_fields {
Ok(profile_fields.can_set_field(&ProfileFieldName::AvatarUrl))
} else {
Ok(capabilities.set_avatar_url)
}
}
pub async fn can_change_thirdparty_ids(&self) -> crate::Result<bool> {
let capabilities = self.load_or_fetch_homeserver_capabilities().await?;
Ok(capabilities.thirdparty_id_changes.enabled)
}
pub async fn can_get_login_token(&self) -> crate::Result<bool> {
let capabilities = self.load_or_fetch_homeserver_capabilities().await?;
Ok(capabilities.get_login_token.enabled)
}
pub async fn extended_profile_fields(&self) -> crate::Result<ProfileFieldsCapability> {
Ok(self
.profile_capabilities()
.await?
.profile_fields
.unwrap_or_else(|| ProfileFieldsCapability::new(false)))
}
pub async fn room_versions(&self) -> crate::Result<RoomVersionsCapability> {
let capabilities = self.load_or_fetch_homeserver_capabilities().await?;
Ok(capabilities.room_versions)
}
pub async fn account_moderation(&self) -> crate::Result<AccountModerationCapability> {
let capabilities = self.load_or_fetch_homeserver_capabilities().await?;
Ok(capabilities.account_moderation)
}
pub async fn forgets_room_when_leaving(&self) -> crate::Result<bool> {
let capabilities = self.load_or_fetch_homeserver_capabilities().await?;
Ok(capabilities.forget_forced_upon_leave.enabled)
}
async fn homeserver_capabilities_cached(&self) -> Result<Option<Capabilities>, StoreError> {
let capabilities_cache = &self.client.inner.caches.homeserver_capabilities;
let value = if let CachedValue::Cached(cached) = capabilities_cache.value() {
cached
} else if let Some(stored) = self
.client
.state_store()
.get_kv_data(StateStoreDataKey::HomeserverCapabilities)
.await?
.and_then(|value| value.into_homeserver_capabilities())
{
capabilities_cache.set_value(stored.clone());
stored
} else {
return Ok(None);
};
if value.has_expired() {
debug!("spawning task to refresh homeserver capabilities cache");
let homeserver_capabilities = self.clone();
self.client.task_monitor().spawn_finite_task(
"refresh homeserver capabilities cache",
async move {
if let Err(error) =
homeserver_capabilities.get_and_cache_remote_capabilities().await
{
warn!("failed to refresh homeserver capabilities cache: {error}");
}
},
);
}
Ok(Some(value.into_data()))
}
async fn load_or_fetch_homeserver_capabilities(&self) -> crate::Result<Capabilities> {
match self.homeserver_capabilities_cached().await {
Ok(Some(capabilities)) => {
return Ok(capabilities);
}
Ok(None) => {
}
Err(err) => {
warn!("error when loading cached homeserver capabilities: {err}");
}
}
Ok(self.get_and_cache_remote_capabilities().await?)
}
async fn get_and_cache_remote_capabilities(&self) -> HttpResult<Capabilities> {
let capabilities_cache = &self.client.inner.caches.homeserver_capabilities;
let mut capabilities_guard = match capabilities_cache.refresh_lock.try_lock() {
Ok(guard) => guard,
Err(_) => {
let guard = capabilities_cache.refresh_lock.lock().await;
if let Err(error) = guard.as_ref() {
return Err(HttpError::Cached(error.clone()));
}
if let CachedValue::Cached(value) = capabilities_cache.value()
&& !value.has_expired()
{
return Ok(value.into_data());
}
guard
}
};
let capabilities = match self.client.send(get_capabilities::v3::Request::new()).await {
Ok(response) => {
*capabilities_guard = Ok(());
TtlValue::new(response.capabilities)
}
Err(error) => {
let error = Arc::new(error);
*capabilities_guard = Err(error.clone());
return Err(HttpError::Cached(error));
}
};
if let Err(err) = self
.client
.state_store()
.set_kv_data(
StateStoreDataKey::HomeserverCapabilities,
StateStoreDataValue::HomeserverCapabilities(capabilities.clone()),
)
.await
{
warn!("error when caching homeserver capabilities: {err}");
}
capabilities_cache.set_value(capabilities.clone());
Ok(capabilities.into_data())
}
async fn profile_capabilities(&self) -> crate::Result<ProfileCapabilities> {
let capabilities = self.load_or_fetch_homeserver_capabilities().await?;
let profile_fields = match capabilities.profile_fields {
Some(profile_fields) => Some(profile_fields),
None => {
if self.homeserver_supports_extended_profile_fields().await? {
Some(ProfileFieldsCapability::new(true))
} else {
None
}
}
};
#[allow(deprecated)]
Ok(ProfileCapabilities {
profile_fields,
set_displayname: capabilities.set_displayname.enabled,
set_avatar_url: capabilities.set_avatar_url.enabled,
})
}
async fn homeserver_supports_extended_profile_fields(&self) -> crate::Result<bool> {
let supported_versions = self.client.supported_versions().await?;
Ok(delete_profile_field::v3::Request::PATH_BUILDER.is_supported(&supported_versions))
}
}
struct ProfileCapabilities {
profile_fields: Option<ProfileFieldsCapability>,
set_displayname: bool,
set_avatar_url: bool,
}
#[cfg(all(not(target_family = "wasm"), test))]
mod tests {
use std::time::Duration;
use assert_matches::assert_matches;
use matrix_sdk_base::sleep::sleep;
use matrix_sdk_test::async_test;
#[allow(deprecated)]
use ruma::api::{
MatrixVersion,
client::discovery::get_capabilities::v3::{
SetAvatarUrlCapability, SetDisplayNameCapability,
},
};
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;
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;
let capabilities = client.homeserver_capabilities();
capabilities.refresh().await.expect("refreshing capabilities failed");
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;
assert!(capabilities.can_change_password().await.expect("checking capabilities failed"));
capabilities.refresh().await.expect("refreshing capabilities failed");
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;
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)
.mock_once()
.mount()
.await;
let capabilities = client.homeserver_capabilities();
assert!(capabilities.can_change_displayname().await.expect("checking capabilities failed"));
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)
.expect(1)
.mount()
.await;
assert!(capabilities.can_change_displayname().await.expect("checking capabilities failed"));
let capabilities_data =
capabilities.homeserver_capabilities_cached().await.unwrap().unwrap();
let mut ttl_value = TtlValue::new(capabilities_data);
ttl_value.expire();
client.inner.caches.homeserver_capabilities.set_value(ttl_value);
capabilities.homeserver_capabilities_cached().await.unwrap().unwrap();
sleep(Duration::from_secs(1)).await;
assert_matches!(client.inner.caches.homeserver_capabilities.value(), CachedValue::Cached(value) if !value.has_expired());
}
#[async_test]
#[allow(deprecated)]
async fn test_deprecated_profile_fields_capabilities() {
let server = MatrixMockServer::new().await;
let mut capabilities = Capabilities::new();
capabilities.profile_fields.take();
capabilities.set_displayname = SetDisplayNameCapability::new(true);
capabilities.set_avatar_url = SetAvatarUrlCapability::new(false);
server
.mock_get_homeserver_capabilities()
.ok_with_capabilities(capabilities)
.expect(2)
.mount()
.await;
let client =
server.client_builder().server_versions(vec![MatrixVersion::V1_12]).build().await;
let capabilities_api = client.homeserver_capabilities();
assert!(
capabilities_api
.can_change_displayname()
.await
.expect("checking displayname capability failed")
);
assert!(
!capabilities_api.can_change_avatar().await.expect("checking avatar capability failed")
);
assert!(
!capabilities_api
.extended_profile_fields()
.await
.expect("checking profile fields capability failed")
.enabled
);
let client =
server.client_builder().server_versions(vec![MatrixVersion::V1_16]).build().await;
let capabilities_api = client.homeserver_capabilities();
assert!(
capabilities_api
.can_change_displayname()
.await
.expect("checking displayname capability failed")
);
assert!(
capabilities_api.can_change_avatar().await.expect("checking avatar capability failed")
);
assert!(
capabilities_api
.extended_profile_fields()
.await
.expect("checking profile fields capability failed")
.enabled
);
}
#[async_test]
#[allow(deprecated)]
async fn test_extended_profile_fields_capabilities_enabled() {
let server = MatrixMockServer::new().await;
let mut capabilities = Capabilities::new();
capabilities.profile_fields = Some(ProfileFieldsCapability::new(true));
capabilities.set_displayname = SetDisplayNameCapability::new(true);
capabilities.set_avatar_url = SetAvatarUrlCapability::new(false);
server
.mock_get_homeserver_capabilities()
.ok_with_capabilities(capabilities)
.expect(2)
.mount()
.await;
let client =
server.client_builder().server_versions(vec![MatrixVersion::V1_12]).build().await;
let capabilities_api = client.homeserver_capabilities();
assert!(
capabilities_api
.can_change_displayname()
.await
.expect("checking displayname capability failed")
);
assert!(
capabilities_api.can_change_avatar().await.expect("checking avatar capability failed")
);
assert!(
capabilities_api
.extended_profile_fields()
.await
.expect("checking profile fields capability failed")
.enabled
);
let client =
server.client_builder().server_versions(vec![MatrixVersion::V1_16]).build().await;
let capabilities_api = client.homeserver_capabilities();
assert!(
capabilities_api
.can_change_displayname()
.await
.expect("checking displayname capability failed")
);
assert!(
capabilities_api.can_change_avatar().await.expect("checking avatar capability failed")
);
assert!(
capabilities_api
.extended_profile_fields()
.await
.expect("checking profile fields capability failed")
.enabled
);
}
#[async_test]
#[allow(deprecated)]
async fn test_extended_profile_fields_capabilities_disabled() {
let server = MatrixMockServer::new().await;
let mut capabilities = Capabilities::new();
capabilities.profile_fields = Some(ProfileFieldsCapability::new(false));
capabilities.set_displayname = SetDisplayNameCapability::new(true);
capabilities.set_avatar_url = SetAvatarUrlCapability::new(false);
server
.mock_get_homeserver_capabilities()
.ok_with_capabilities(capabilities)
.expect(2)
.mount()
.await;
let client =
server.client_builder().server_versions(vec![MatrixVersion::V1_12]).build().await;
let capabilities_api = client.homeserver_capabilities();
assert!(
!capabilities_api
.can_change_displayname()
.await
.expect("checking displayname capability failed")
);
assert!(
!capabilities_api.can_change_avatar().await.expect("checking avatar capability failed")
);
assert!(
!capabilities_api
.extended_profile_fields()
.await
.expect("checking profile fields capability failed")
.enabled
);
let client =
server.client_builder().server_versions(vec![MatrixVersion::V1_16]).build().await;
let capabilities_api = client.homeserver_capabilities();
assert!(
!capabilities_api
.can_change_displayname()
.await
.expect("checking displayname capability failed")
);
assert!(
!capabilities_api.can_change_avatar().await.expect("checking avatar capability failed")
);
assert!(
!capabilities_api
.extended_profile_fields()
.await
.expect("checking profile fields capability failed")
.enabled
);
}
#[async_test]
#[allow(deprecated)]
async fn test_fine_grained_extended_profile_fields_capabilities() {
let server = MatrixMockServer::new().await;
let mut profile_fields = ProfileFieldsCapability::new(true);
profile_fields.allowed = Some(vec![ProfileFieldName::AvatarUrl]);
let mut capabilities = Capabilities::new();
capabilities.profile_fields = Some(profile_fields);
capabilities.set_displayname = SetDisplayNameCapability::new(true);
capabilities.set_avatar_url = SetAvatarUrlCapability::new(false);
server
.mock_get_homeserver_capabilities()
.ok_with_capabilities(capabilities)
.expect(2)
.mount()
.await;
let client =
server.client_builder().server_versions(vec![MatrixVersion::V1_12]).build().await;
let capabilities_api = client.homeserver_capabilities();
assert!(
!capabilities_api
.can_change_displayname()
.await
.expect("checking displayname capability failed")
);
assert!(
capabilities_api.can_change_avatar().await.expect("checking avatar capability failed")
);
assert!(
capabilities_api
.extended_profile_fields()
.await
.expect("checking profile fields capability failed")
.enabled
);
let client =
server.client_builder().server_versions(vec![MatrixVersion::V1_16]).build().await;
let capabilities_api = client.homeserver_capabilities();
assert!(
!capabilities_api
.can_change_displayname()
.await
.expect("checking displayname capability failed")
);
assert!(
capabilities_api.can_change_avatar().await.expect("checking avatar capability failed")
);
assert!(
capabilities_api
.extended_profile_fields()
.await
.expect("checking profile fields capability failed")
.enabled
);
}
}