Skip to main content

matrix_sdk/client/
homeserver_capabilities.rs

1use std::sync::Arc;
2
3use matrix_sdk_base::{StateStoreDataKey, StateStoreDataValue, StoreError, ttl::TtlValue};
4use ruma::{
5    api::{
6        Metadata,
7        client::{
8            discovery::get_capabilities::{
9                self,
10                v3::{
11                    AccountModerationCapability, Capabilities, ProfileFieldsCapability,
12                    RoomVersionsCapability,
13                },
14            },
15            profile::delete_profile_field,
16        },
17    },
18    profile::ProfileFieldName,
19};
20use tracing::{debug, warn};
21
22use crate::{Client, HttpError, HttpResult, client::caches::CachedValue};
23
24/// Helper to check what [`Capabilities`] are supported by the homeserver.
25///
26/// Spec: <https://spec.matrix.org/latest/client-server-api/#capabilities-negotiation>
27#[derive(Debug, Clone)]
28pub struct HomeserverCapabilities {
29    client: Client,
30}
31
32impl HomeserverCapabilities {
33    /// Creates a new [`HomeserverCapabilities`] instance.
34    pub fn new(client: Client) -> Self {
35        Self { client }
36    }
37
38    /// Forces a refresh of the cached value using the `/capabilities` endpoint.
39    pub async fn refresh(&self) -> crate::Result<()> {
40        self.get_and_cache_remote_capabilities().await?;
41        Ok(())
42    }
43
44    /// Returns whether the user can change their password or not.
45    pub async fn can_change_password(&self) -> crate::Result<bool> {
46        let capabilities = self.load_or_fetch_homeserver_capabilities().await?;
47        Ok(capabilities.change_password.enabled)
48    }
49
50    /// Returns whether the user can change their display name or not.
51    ///
52    /// This will first check the `m.profile_fields` capability and use it if
53    /// present, or fall back to `m.set_displayname` otherwise.
54    ///
55    /// Spec: <https://spec.matrix.org/latest/client-server-api/#mset_displayname-capability>
56    pub async fn can_change_displayname(&self) -> crate::Result<bool> {
57        let capabilities = self.profile_capabilities().await?;
58
59        if let Some(profile_fields) = capabilities.profile_fields {
60            Ok(profile_fields.can_set_field(&ProfileFieldName::DisplayName))
61        } else {
62            Ok(capabilities.set_displayname)
63        }
64    }
65
66    /// Returns whether the user can change their avatar or not.
67    ///
68    /// This will first check the `m.profile_fields` capability and use it if
69    /// present, or fall back to `m.set_avatar_url` otherwise.
70    ///
71    /// Spec: <https://spec.matrix.org/latest/client-server-api/#mset_avatar_url-capability>
72    pub async fn can_change_avatar(&self) -> crate::Result<bool> {
73        let capabilities = self.profile_capabilities().await?;
74
75        if let Some(profile_fields) = capabilities.profile_fields {
76            Ok(profile_fields.can_set_field(&ProfileFieldName::AvatarUrl))
77        } else {
78            Ok(capabilities.set_avatar_url)
79        }
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        Ok(self
109            .profile_capabilities()
110            .await?
111            .profile_fields
112            .unwrap_or_else(|| 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`] from the local cache.
141    async fn homeserver_capabilities_cached(&self) -> Result<Option<Capabilities>, StoreError> {
142        let capabilities_cache = &self.client.inner.caches.homeserver_capabilities;
143
144        let value = if let CachedValue::Cached(cached) = capabilities_cache.value() {
145            cached
146        } else if let Some(stored) = self
147            .client
148            .state_store()
149            .get_kv_data(StateStoreDataKey::HomeserverCapabilities)
150            .await?
151            .and_then(|value| value.into_homeserver_capabilities())
152        {
153            // Copy the data from the store in the in-memory cache.
154            capabilities_cache.set_value(stored.clone());
155
156            stored
157        } else {
158            return Ok(None);
159        };
160
161        // Spawn a task to refresh the cache if it has expired.
162        if value.has_expired() {
163            debug!("spawning task to refresh homeserver capabilities cache");
164
165            let homeserver_capabilities = self.clone();
166            self.client.task_monitor().spawn_finite_task(
167                "refresh homeserver capabilities cache",
168                async move {
169                    if let Err(error) =
170                        homeserver_capabilities.get_and_cache_remote_capabilities().await
171                    {
172                        warn!("failed to refresh homeserver capabilities cache: {error}");
173                    }
174                },
175            );
176        }
177
178        Ok(Some(value.into_data()))
179    }
180
181    /// Gets the supported [`Capabilities`] either from the local cache or from
182    /// the homeserver using the `/capabilities` endpoint if the data is not
183    /// cached.
184    ///
185    /// To ensure you get updated values, you should call [`Self::refresh`]
186    /// instead.
187    async fn load_or_fetch_homeserver_capabilities(&self) -> crate::Result<Capabilities> {
188        match self.homeserver_capabilities_cached().await {
189            Ok(Some(capabilities)) => {
190                return Ok(capabilities);
191            }
192            Ok(None) => {
193                // fallthrough: cache is empty
194            }
195            Err(err) => {
196                warn!("error when loading cached homeserver capabilities: {err}");
197                // fallthrough to network.
198            }
199        }
200
201        Ok(self.get_and_cache_remote_capabilities().await?)
202    }
203
204    /// Gets and caches the capabilities of the homeserver.
205    async fn get_and_cache_remote_capabilities(&self) -> HttpResult<Capabilities> {
206        let capabilities_cache = &self.client.inner.caches.homeserver_capabilities;
207
208        let mut capabilities_guard = match capabilities_cache.refresh_lock.try_lock() {
209            Ok(guard) => guard,
210            Err(_) => {
211                // There is already a refresh in progress, wait for it to finish.
212                let guard = capabilities_cache.refresh_lock.lock().await;
213
214                if let Err(error) = guard.as_ref() {
215                    // There was an error in the previous refresh, return it.
216                    return Err(HttpError::Cached(error.clone()));
217                }
218
219                // Reuse the data if it was cached and it hasn't expired.
220                if let CachedValue::Cached(value) = capabilities_cache.value()
221                    && !value.has_expired()
222                {
223                    return Ok(value.into_data());
224                }
225
226                // The data wasn't cached or has expired, we need to make another request.
227                guard
228            }
229        };
230
231        let capabilities = match self.client.send(get_capabilities::v3::Request::new()).await {
232            Ok(response) => {
233                *capabilities_guard = Ok(());
234                TtlValue::new(response.capabilities)
235            }
236            Err(error) => {
237                let error = Arc::new(error);
238                *capabilities_guard = Err(error.clone());
239                return Err(HttpError::Cached(error));
240            }
241        };
242
243        if let Err(err) = self
244            .client
245            .state_store()
246            .set_kv_data(
247                StateStoreDataKey::HomeserverCapabilities,
248                StateStoreDataValue::HomeserverCapabilities(capabilities.clone()),
249            )
250            .await
251        {
252            warn!("error when caching homeserver capabilities: {err}");
253        }
254
255        capabilities_cache.set_value(capabilities.clone());
256
257        Ok(capabilities.into_data())
258    }
259
260    /// Gets or computes the supported [`ProfileCapabilities`].
261    async fn profile_capabilities(&self) -> crate::Result<ProfileCapabilities> {
262        let capabilities = self.load_or_fetch_homeserver_capabilities().await?;
263
264        let profile_fields = match capabilities.profile_fields {
265            Some(profile_fields) => Some(profile_fields),
266            None => {
267                // According to the Matrix spec about the `m.profile_fields` capability:
268                //
269                // > When this capability is not listed, clients SHOULD assume the user is
270                // > able to change profile fields without any restrictions, provided the
271                // > homeserver advertises a specification version that includes the
272                // > `m.profile_fields` capability in the `/versions` response.
273                if self.homeserver_supports_extended_profile_fields().await? {
274                    Some(ProfileFieldsCapability::new(true))
275                } else {
276                    None
277                }
278            }
279        };
280
281        #[allow(deprecated)]
282        Ok(ProfileCapabilities {
283            profile_fields,
284            set_displayname: capabilities.set_displayname.enabled,
285            set_avatar_url: capabilities.set_avatar_url.enabled,
286        })
287    }
288
289    /// Whether the homeserver supports extended profile fields.
290    ///
291    ///
292    /// [Matrix spec]: https://spec.matrix.org/latest/client-server-api/#mprofile_fields-capability
293    async fn homeserver_supports_extended_profile_fields(&self) -> crate::Result<bool> {
294        let supported_versions = self.client.supported_versions().await?;
295        // If the homeserver supports the endpoint to delete profile fields, it supports
296        // extended profile fields.
297        Ok(delete_profile_field::v3::Request::PATH_BUILDER.is_supported(&supported_versions))
298    }
299}
300
301/// All the capabilities to change a profile field.
302struct ProfileCapabilities {
303    /// The capability to change profile fields, advertised by the homeserver or
304    /// computed.
305    profile_fields: Option<ProfileFieldsCapability>,
306    /// The capability to set the display name advertised by the homeserver.
307    set_displayname: bool,
308    /// The capability to set the avatar URL advertised by the homeserver.
309    set_avatar_url: bool,
310}
311
312#[cfg(all(not(target_family = "wasm"), test))]
313mod tests {
314    use std::time::Duration;
315
316    use assert_matches::assert_matches;
317    use matrix_sdk_base::sleep::sleep;
318    use matrix_sdk_test::async_test;
319    #[allow(deprecated)]
320    use ruma::api::{
321        MatrixVersion,
322        client::discovery::get_capabilities::v3::{
323            SetAvatarUrlCapability, SetDisplayNameCapability,
324        },
325    };
326
327    use super::*;
328    use crate::test_utils::mocks::MatrixMockServer;
329
330    #[async_test]
331    async fn test_refresh_always_updates_capabilities() {
332        let server = MatrixMockServer::new().await;
333        let client = server.client_builder().build().await;
334
335        // Set the expected capabilities to something we can check
336        let mut expected_capabilities = Capabilities::default();
337        expected_capabilities.change_password.enabled = true;
338        server
339            .mock_get_homeserver_capabilities()
340            .ok_with_capabilities(expected_capabilities)
341            .mock_once()
342            .mount()
343            .await;
344
345        // Refresh the capabilities
346        let capabilities = client.homeserver_capabilities();
347        capabilities.refresh().await.expect("refreshing capabilities failed");
348
349        // Check the values we get are updated
350        assert!(capabilities.can_change_password().await.expect("checking capabilities failed"));
351
352        let mut expected_capabilities = Capabilities::default();
353        expected_capabilities.change_password.enabled = false;
354        server
355            .mock_get_homeserver_capabilities()
356            .ok_with_capabilities(expected_capabilities)
357            .mock_once()
358            .mount()
359            .await;
360
361        // Check the values we get are not updated without a refresh, they're loaded
362        // from the cache
363        assert!(capabilities.can_change_password().await.expect("checking capabilities failed"));
364
365        // Do another refresh to make sure we get the updated values
366        capabilities.refresh().await.expect("refreshing capabilities failed");
367
368        // Check the values we get are updated
369        assert!(!capabilities.can_change_password().await.expect("checking capabilities failed"));
370    }
371
372    #[async_test]
373    async fn test_get_functions_refresh_the_data_if_not_available_or_use_cache_if_available() {
374        let server = MatrixMockServer::new().await;
375        let client = server.client_builder().build().await;
376
377        // Set the expected capabilities to something we can check
378        let mut expected_capabilities = Capabilities::default();
379        let mut profile_fields = ProfileFieldsCapability::new(true);
380        profile_fields.allowed = Some(vec![ProfileFieldName::DisplayName]);
381        expected_capabilities.profile_fields = Some(profile_fields);
382        server
383            .mock_get_homeserver_capabilities()
384            .ok_with_capabilities(expected_capabilities)
385            // Ensure it's called just once
386            .mock_once()
387            .mount()
388            .await;
389
390        // Refresh the capabilities
391        let capabilities = client.homeserver_capabilities();
392
393        // Check the values we get are updated
394        assert!(capabilities.can_change_displayname().await.expect("checking capabilities failed"));
395
396        // Now revert the previous mock so we can check we're getting the cached value
397        // instead of this one
398        let mut expected_capabilities = Capabilities::default();
399        let mut profile_fields = ProfileFieldsCapability::new(true);
400        profile_fields.disallowed = Some(vec![ProfileFieldName::DisplayName]);
401        expected_capabilities.profile_fields = Some(profile_fields);
402        server
403            .mock_get_homeserver_capabilities()
404            .ok_with_capabilities(expected_capabilities)
405            .expect(1)
406            .mount()
407            .await;
408
409        // Check the values we get are not updated without a refresh, they're loaded
410        // from the cache
411        assert!(capabilities.can_change_displayname().await.expect("checking capabilities failed"));
412
413        // Force an expiry of the data.
414        let capabilities_data =
415            capabilities.homeserver_capabilities_cached().await.unwrap().unwrap();
416        let mut ttl_value = TtlValue::new(capabilities_data);
417        ttl_value.expire();
418        client.inner.caches.homeserver_capabilities.set_value(ttl_value);
419
420        // Call a method to trigger a cache refresh background task.
421        capabilities.homeserver_capabilities_cached().await.unwrap().unwrap();
422
423        // We wait for the task to finish, the endpoint should have been called again.
424        sleep(Duration::from_secs(1)).await;
425        assert_matches!(client.inner.caches.homeserver_capabilities.value(), CachedValue::Cached(value) if !value.has_expired());
426    }
427
428    #[async_test]
429    #[allow(deprecated)]
430    async fn test_deprecated_profile_fields_capabilities() {
431        let server = MatrixMockServer::new().await;
432
433        // The user can only set the display name but not the avatar url or extended
434        // profile fields.
435        let mut capabilities = Capabilities::new();
436        capabilities.profile_fields.take();
437        capabilities.set_displayname = SetDisplayNameCapability::new(true);
438        capabilities.set_avatar_url = SetAvatarUrlCapability::new(false);
439        server
440            .mock_get_homeserver_capabilities()
441            .ok_with_capabilities(capabilities)
442            // It should be called once by each client below.
443            .expect(2)
444            .mount()
445            .await;
446
447        // Client with Matrix 1.12 that did not support extended profile fields yet.
448        // Because there is no `m.profile_fields` capability, we rely on the legacy
449        // profile capabilities.
450        let client =
451            server.client_builder().server_versions(vec![MatrixVersion::V1_12]).build().await;
452        let capabilities_api = client.homeserver_capabilities();
453        assert!(
454            capabilities_api
455                .can_change_displayname()
456                .await
457                .expect("checking displayname capability failed")
458        );
459        assert!(
460            !capabilities_api.can_change_avatar().await.expect("checking avatar capability failed")
461        );
462        assert!(
463            !capabilities_api
464                .extended_profile_fields()
465                .await
466                .expect("checking profile fields capability failed")
467                .enabled
468        );
469
470        // Client with Matrix 1.16 that added support for extended profile fields, the
471        // deprecated profile capabilities are ignored.
472        let client =
473            server.client_builder().server_versions(vec![MatrixVersion::V1_16]).build().await;
474        let capabilities_api = client.homeserver_capabilities();
475        assert!(
476            capabilities_api
477                .can_change_displayname()
478                .await
479                .expect("checking displayname capability failed")
480        );
481        assert!(
482            capabilities_api.can_change_avatar().await.expect("checking avatar capability failed")
483        );
484        assert!(
485            capabilities_api
486                .extended_profile_fields()
487                .await
488                .expect("checking profile fields capability failed")
489                .enabled
490        );
491    }
492
493    #[async_test]
494    #[allow(deprecated)]
495    async fn test_extended_profile_fields_capabilities_enabled() {
496        let server = MatrixMockServer::new().await;
497
498        // The user can set any profile field.
499        // The legacy capabilities say differently, but they will be ignored.
500        let mut capabilities = Capabilities::new();
501        capabilities.profile_fields = Some(ProfileFieldsCapability::new(true));
502        capabilities.set_displayname = SetDisplayNameCapability::new(true);
503        capabilities.set_avatar_url = SetAvatarUrlCapability::new(false);
504        server
505            .mock_get_homeserver_capabilities()
506            .ok_with_capabilities(capabilities)
507            // It should be called once by each client below.
508            .expect(2)
509            .mount()
510            .await;
511
512        // Client with Matrix 1.12 that did not support extended profile fields yet.
513        // However, because there is an `m.profile_fields` capability, we still rely on
514        // it.
515        let client =
516            server.client_builder().server_versions(vec![MatrixVersion::V1_12]).build().await;
517        let capabilities_api = client.homeserver_capabilities();
518        assert!(
519            capabilities_api
520                .can_change_displayname()
521                .await
522                .expect("checking displayname capability failed")
523        );
524        assert!(
525            capabilities_api.can_change_avatar().await.expect("checking avatar capability failed")
526        );
527        assert!(
528            capabilities_api
529                .extended_profile_fields()
530                .await
531                .expect("checking profile fields capability failed")
532                .enabled
533        );
534
535        // Client with Matrix 1.16 that added support for extended profile fields, only
536        // the `m.profile_fields` capability is used too.
537        let client =
538            server.client_builder().server_versions(vec![MatrixVersion::V1_16]).build().await;
539        let capabilities_api = client.homeserver_capabilities();
540        assert!(
541            capabilities_api
542                .can_change_displayname()
543                .await
544                .expect("checking displayname capability failed")
545        );
546        assert!(
547            capabilities_api.can_change_avatar().await.expect("checking avatar capability failed")
548        );
549        assert!(
550            capabilities_api
551                .extended_profile_fields()
552                .await
553                .expect("checking profile fields capability failed")
554                .enabled
555        );
556    }
557
558    #[async_test]
559    #[allow(deprecated)]
560    async fn test_extended_profile_fields_capabilities_disabled() {
561        let server = MatrixMockServer::new().await;
562
563        // The user cannot set any profile field.
564        // The legacy capabilities say differently, but they will be ignored.
565        let mut capabilities = Capabilities::new();
566        capabilities.profile_fields = Some(ProfileFieldsCapability::new(false));
567        capabilities.set_displayname = SetDisplayNameCapability::new(true);
568        capabilities.set_avatar_url = SetAvatarUrlCapability::new(false);
569        server
570            .mock_get_homeserver_capabilities()
571            .ok_with_capabilities(capabilities)
572            // It should be called once by each client below.
573            .expect(2)
574            .mount()
575            .await;
576
577        // Client with Matrix 1.12 that did not support extended profile fields yet.
578        // However, because there is an `m.profile_fields` capability, we still rely on
579        // it.
580        let client =
581            server.client_builder().server_versions(vec![MatrixVersion::V1_12]).build().await;
582        let capabilities_api = client.homeserver_capabilities();
583        assert!(
584            !capabilities_api
585                .can_change_displayname()
586                .await
587                .expect("checking displayname capability failed")
588        );
589        assert!(
590            !capabilities_api.can_change_avatar().await.expect("checking avatar capability failed")
591        );
592        assert!(
593            !capabilities_api
594                .extended_profile_fields()
595                .await
596                .expect("checking profile fields capability failed")
597                .enabled
598        );
599
600        // Client with Matrix 1.16 that added support for extended profile fields, only
601        // the `m.profile_fields` capability is used too.
602        let client =
603            server.client_builder().server_versions(vec![MatrixVersion::V1_16]).build().await;
604        let capabilities_api = client.homeserver_capabilities();
605        assert!(
606            !capabilities_api
607                .can_change_displayname()
608                .await
609                .expect("checking displayname capability failed")
610        );
611        assert!(
612            !capabilities_api.can_change_avatar().await.expect("checking avatar capability failed")
613        );
614        assert!(
615            !capabilities_api
616                .extended_profile_fields()
617                .await
618                .expect("checking profile fields capability failed")
619                .enabled
620        );
621    }
622
623    #[async_test]
624    #[allow(deprecated)]
625    async fn test_fine_grained_extended_profile_fields_capabilities() {
626        let server = MatrixMockServer::new().await;
627
628        // The user can only set the avatar URL.
629        // The legacy capabilities say differently, but they will be ignored.
630        let mut profile_fields = ProfileFieldsCapability::new(true);
631        profile_fields.allowed = Some(vec![ProfileFieldName::AvatarUrl]);
632        let mut capabilities = Capabilities::new();
633        capabilities.profile_fields = Some(profile_fields);
634        capabilities.set_displayname = SetDisplayNameCapability::new(true);
635        capabilities.set_avatar_url = SetAvatarUrlCapability::new(false);
636        server
637            .mock_get_homeserver_capabilities()
638            .ok_with_capabilities(capabilities)
639            // It should be called once by each client below.
640            .expect(2)
641            .mount()
642            .await;
643
644        // Client with Matrix 1.12 that did not support extended profile fields yet.
645        // However, because there is an `m.profile_fields` capability, we still rely on
646        // it.
647        let client =
648            server.client_builder().server_versions(vec![MatrixVersion::V1_12]).build().await;
649        let capabilities_api = client.homeserver_capabilities();
650        assert!(
651            !capabilities_api
652                .can_change_displayname()
653                .await
654                .expect("checking displayname capability failed")
655        );
656        assert!(
657            capabilities_api.can_change_avatar().await.expect("checking avatar capability failed")
658        );
659        assert!(
660            capabilities_api
661                .extended_profile_fields()
662                .await
663                .expect("checking profile fields capability failed")
664                .enabled
665        );
666
667        // Client with Matrix 1.16 that added support for extended profile fields, only
668        // the `m.profile_fields` capability is used too.
669        let client =
670            server.client_builder().server_versions(vec![MatrixVersion::V1_16]).build().await;
671        let capabilities_api = client.homeserver_capabilities();
672        assert!(
673            !capabilities_api
674                .can_change_displayname()
675                .await
676                .expect("checking displayname capability failed")
677        );
678        assert!(
679            capabilities_api.can_change_avatar().await.expect("checking avatar capability failed")
680        );
681        assert!(
682            capabilities_api
683                .extended_profile_fields()
684                .await
685                .expect("checking profile fields capability failed")
686                .enabled
687        );
688    }
689}