Skip to main content

matrix_sdk/client/
homeserver_capabilities.rs

1use matrix_sdk_base::{StateStoreDataKey, StateStoreDataValue};
2use ruma::{
3    api::client::discovery::{
4        get_capabilities,
5        get_capabilities::v3::{
6            AccountModerationCapability, Capabilities, ProfileFieldsCapability,
7            RoomVersionsCapability,
8        },
9    },
10    profile::ProfileFieldName,
11};
12use tracing::warn;
13
14use crate::{Client, HttpResult};
15
16/// Helper to check what [`Capabilities`] are supported by the homeserver.
17///
18/// Spec: <https://spec.matrix.org/latest/client-server-api/#capabilities-negotiation>
19#[derive(Debug, Clone)]
20pub struct HomeserverCapabilities {
21    client: Client,
22}
23
24impl HomeserverCapabilities {
25    /// Creates a new [`HomeserverCapabilities`] instance.
26    pub fn new(client: Client) -> Self {
27        Self { client }
28    }
29
30    /// Forces a refresh of the cached value using the `/capabilities` endpoint.
31    pub async fn refresh(&self) -> crate::Result<()> {
32        self.get_and_cache_remote_capabilities().await?;
33        Ok(())
34    }
35
36    /// Returns whether the user can change their password or not.
37    pub async fn can_change_password(&self) -> crate::Result<bool> {
38        let capabilities = self.load_or_fetch_homeserver_capabilities().await?;
39        Ok(capabilities.change_password.enabled)
40    }
41
42    /// Returns whether the user can change their display name or not.
43    ///
44    /// This will first check the `m.profile_fields` capability and use it if
45    /// present, or fall back to `m.set_displayname` otherwise.
46    ///
47    /// Spec: <https://spec.matrix.org/latest/client-server-api/#mset_displayname-capability>
48    pub async fn can_change_displayname(&self) -> crate::Result<bool> {
49        let capabilities = self.load_or_fetch_homeserver_capabilities().await?;
50        if let Some(profile_fields) = capabilities.profile_fields
51            && profile_fields.enabled
52        {
53            let allowed = profile_fields.allowed.unwrap_or_default();
54            let disallowed = profile_fields.disallowed.unwrap_or_default();
55            return Ok(allowed.contains(&ProfileFieldName::DisplayName)
56                || !disallowed.contains(&ProfileFieldName::DisplayName));
57        }
58        #[allow(deprecated)]
59        Ok(capabilities.set_displayname.enabled)
60    }
61
62    /// Returns whether the user can change their avatar or not.
63    ///
64    /// This will first check the `m.profile_fields` capability and use it if
65    /// present, or fall back to `m.set_avatar_url` otherwise.
66    ///
67    /// Spec: <https://spec.matrix.org/latest/client-server-api/#mset_avatar_url-capability>
68    pub async fn can_change_avatar(&self) -> crate::Result<bool> {
69        let capabilities = self.load_or_fetch_homeserver_capabilities().await?;
70        if let Some(profile_fields) = capabilities.profile_fields
71            && profile_fields.enabled
72        {
73            let allowed = profile_fields.allowed.unwrap_or_default();
74            let disallowed = profile_fields.disallowed.unwrap_or_default();
75            return Ok(allowed.contains(&ProfileFieldName::AvatarUrl)
76                || !disallowed.contains(&ProfileFieldName::AvatarUrl));
77        }
78        #[allow(deprecated)]
79        Ok(capabilities.set_avatar_url.enabled)
80    }
81
82    /// Returns whether the user can add, remove, or change 3PID associations on
83    /// their account.
84    ///
85    /// Spec: <https://spec.matrix.org/latest/client-server-api/#m3pid_changes-capability>
86    pub async fn can_change_thirdparty_ids(&self) -> crate::Result<bool> {
87        let capabilities = self.load_or_fetch_homeserver_capabilities().await?;
88        Ok(capabilities.thirdparty_id_changes.enabled)
89    }
90
91    /// Returns whether the user is able to use `POST /login/get_token` to
92    /// generate single-use, time-limited tokens to log unauthenticated
93    /// clients into their account.
94    ///
95    /// When not listed, clients SHOULD assume the user is unable to generate
96    /// tokens.
97    ///
98    /// Spec: <https://spec.matrix.org/latest/client-server-api/#mget_login_token-capability>
99    pub async fn can_get_login_token(&self) -> crate::Result<bool> {
100        let capabilities = self.load_or_fetch_homeserver_capabilities().await?;
101        Ok(capabilities.get_login_token.enabled)
102    }
103
104    /// Returns which profile fields the user is able to change.
105    ///
106    /// Spec: <https://spec.matrix.org/latest/client-server-api/#mprofile_fields-capability>
107    pub async fn extended_profile_fields(&self) -> crate::Result<ProfileFieldsCapability> {
108        let capabilities = self.load_or_fetch_homeserver_capabilities().await?;
109        if let Some(profile_fields) = capabilities.profile_fields {
110            return Ok(profile_fields);
111        }
112        Ok(ProfileFieldsCapability::new(false))
113    }
114
115    /// Returns the room versions supported by the server.
116    ///
117    /// Spec: <https://spec.matrix.org/latest/client-server-api/#mroom_versions-capability>
118    pub async fn room_versions(&self) -> crate::Result<RoomVersionsCapability> {
119        let capabilities = self.load_or_fetch_homeserver_capabilities().await?;
120        Ok(capabilities.room_versions)
121    }
122
123    /// Returns whether the user can perform account moderation actions.
124    ///
125    /// Spec: <https://spec.matrix.org/latest/client-server-api/#get_matrixclientv3capabilities_response-200_accountmoderationcapability>
126    pub async fn account_moderation(&self) -> crate::Result<AccountModerationCapability> {
127        let capabilities = self.load_or_fetch_homeserver_capabilities().await?;
128        Ok(capabilities.account_moderation)
129    }
130
131    /// Returns whether or not the server automatically forgets rooms which the
132    /// user has left.
133    ///
134    /// Spec: <https://spec.matrix.org/latest/client-server-api/#mforget_forced_upon_leave-capability>
135    pub async fn forgets_room_when_leaving(&self) -> crate::Result<bool> {
136        let capabilities = self.load_or_fetch_homeserver_capabilities().await?;
137        Ok(capabilities.forget_forced_upon_leave.enabled)
138    }
139
140    /// Gets the supported [`Capabilities`] either from the local cache or from
141    /// the homeserver using the `/capabilities` endpoint if the data is not
142    /// cached.
143    ///
144    /// To ensure you get updated values, you should call [`Self::refresh`]
145    /// instead.
146    async fn load_or_fetch_homeserver_capabilities(&self) -> crate::Result<Capabilities> {
147        match self.client.state_store().get_kv_data(StateStoreDataKey::HomeserverCapabilities).await
148        {
149            Ok(Some(stored)) => {
150                if let Some(capabilities) = stored.into_homeserver_capabilities() {
151                    return Ok(capabilities);
152                }
153            }
154            Ok(None) => {
155                // fallthrough: cache is empty
156            }
157            Err(err) => {
158                warn!("error when loading cached homeserver capabilities: {err}");
159                // fallthrough to network.
160            }
161        }
162
163        Ok(self.get_and_cache_remote_capabilities().await?)
164    }
165
166    /// Gets and caches the capabilities of the homeserver.
167    async fn get_and_cache_remote_capabilities(&self) -> HttpResult<Capabilities> {
168        let res = self.client.send(get_capabilities::v3::Request::new()).await?;
169
170        if let Err(err) = self
171            .client
172            .state_store()
173            .set_kv_data(
174                StateStoreDataKey::HomeserverCapabilities,
175                StateStoreDataValue::HomeserverCapabilities(res.capabilities.clone()),
176            )
177            .await
178        {
179            warn!("error when caching homeserver capabilities: {err}");
180        }
181
182        Ok(res.capabilities)
183    }
184}
185
186#[cfg(all(not(target_family = "wasm"), test))]
187mod tests {
188    use matrix_sdk_test::async_test;
189
190    use super::*;
191    use crate::test_utils::mocks::MatrixMockServer;
192
193    #[async_test]
194    async fn test_refresh_always_updates_capabilities() {
195        let server = MatrixMockServer::new().await;
196        let client = server.client_builder().build().await;
197
198        // Set the expected capabilities to something we can check
199        let mut expected_capabilities = Capabilities::default();
200        expected_capabilities.change_password.enabled = true;
201        server
202            .mock_get_homeserver_capabilities()
203            .ok_with_capabilities(expected_capabilities)
204            .mock_once()
205            .mount()
206            .await;
207
208        // Refresh the capabilities
209        let capabilities = client.homeserver_capabilities();
210        capabilities.refresh().await.expect("refreshing capabilities failed");
211
212        // Check the values we get are updated
213        assert!(capabilities.can_change_password().await.expect("checking capabilities failed"));
214
215        let mut expected_capabilities = Capabilities::default();
216        expected_capabilities.change_password.enabled = false;
217        server
218            .mock_get_homeserver_capabilities()
219            .ok_with_capabilities(expected_capabilities)
220            .mock_once()
221            .mount()
222            .await;
223
224        // Check the values we get are not updated without a refresh, they're loaded
225        // from the cache
226        assert!(capabilities.can_change_password().await.expect("checking capabilities failed"));
227
228        // Do another refresh to make sure we get the updated values
229        capabilities.refresh().await.expect("refreshing capabilities failed");
230
231        // Check the values we get are updated
232        assert!(!capabilities.can_change_password().await.expect("checking capabilities failed"));
233    }
234
235    #[async_test]
236    async fn test_get_functions_refresh_the_data_if_not_available_or_use_cache_if_available() {
237        let server = MatrixMockServer::new().await;
238        let client = server.client_builder().build().await;
239
240        // Set the expected capabilities to something we can check
241        let mut expected_capabilities = Capabilities::default();
242        let mut profile_fields = ProfileFieldsCapability::new(true);
243        profile_fields.allowed = Some(vec![ProfileFieldName::DisplayName]);
244        expected_capabilities.profile_fields = Some(profile_fields);
245        server
246            .mock_get_homeserver_capabilities()
247            .ok_with_capabilities(expected_capabilities)
248            // Ensure it's called just once
249            .mock_once()
250            .mount()
251            .await;
252
253        // Refresh the capabilities
254        let capabilities = client.homeserver_capabilities();
255
256        // Check the values we get are updated
257        assert!(capabilities.can_change_displayname().await.expect("checking capabilities failed"));
258
259        // Now revert the previous mock so we can check we're getting the cached value
260        // instead of this one
261        let mut expected_capabilities = Capabilities::default();
262        let mut profile_fields = ProfileFieldsCapability::new(true);
263        profile_fields.disallowed = Some(vec![ProfileFieldName::DisplayName]);
264        expected_capabilities.profile_fields = Some(profile_fields);
265        server
266            .mock_get_homeserver_capabilities()
267            .ok_with_capabilities(expected_capabilities)
268            // Ensure it's not called, since we'd be using the cached values instead
269            .never()
270            .mount()
271            .await;
272
273        // Check the values we get are not updated without a refresh, they're loaded
274        // from the cache
275        assert!(capabilities.can_change_displayname().await.expect("checking capabilities failed"));
276    }
277}