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/// [Specification](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    /// [Specification](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    /// [Specification](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    /// [Specification](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 clients
93    /// into their account.
94    ///
95    /// When not listed, clients SHOULD assume the user is unable to generate
96    /// tokens.
97    ///
98    /// [Specification](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    /// [Specification](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    /// [Specification](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    /// [Specification](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    /// [Specification](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
212                // finish.
213                let guard = capabilities_cache.refresh_lock.lock().await;
214
215                if let Err(error) = guard.as_ref() {
216                    // There was an error in the previous refresh, return it.
217                    return Err(HttpError::Cached(error.clone()));
218                }
219
220                // Reuse the data if it was cached and it hasn't expired.
221                if let CachedValue::Cached(value) = capabilities_cache.value()
222                    && !value.has_expired()
223                {
224                    return Ok(value.into_data());
225                }
226
227                // The data wasn't cached or has expired, we need to make
228                // another request.
229                guard
230            }
231        };
232
233        let capabilities = match self.client.send(get_capabilities::v3::Request::new()).await {
234            Ok(response) => {
235                *capabilities_guard = Ok(());
236                TtlValue::new(response.capabilities)
237            }
238            Err(error) => {
239                let error = Arc::new(error);
240                *capabilities_guard = Err(error.clone());
241                return Err(HttpError::Cached(error));
242            }
243        };
244
245        if let Err(err) = self
246            .client
247            .state_store()
248            .set_kv_data(
249                StateStoreDataKey::HomeserverCapabilities,
250                StateStoreDataValue::HomeserverCapabilities(capabilities.clone()),
251            )
252            .await
253        {
254            warn!("error when caching homeserver capabilities: {err}");
255        }
256
257        capabilities_cache.set_value(capabilities.clone());
258
259        Ok(capabilities.into_data())
260    }
261
262    /// Gets or computes the supported [`ProfileCapabilities`].
263    async fn profile_capabilities(&self) -> crate::Result<ProfileCapabilities> {
264        let capabilities = self.load_or_fetch_homeserver_capabilities().await?;
265
266        let profile_fields = match capabilities.profile_fields {
267            Some(profile_fields) => Some(profile_fields),
268            None => {
269                // According to the Matrix spec about the `m.profile_fields`
270                // capability:
271                //
272                // > When this capability is not listed, clients SHOULD assume
273                // > the user is able to change profile fields without any
274                // > restrictions, provided the homeserver advertises a
275                // > specification version that includes the `m.profile_fields`
276                // > capability in the `/versions` response.
277                if self.homeserver_supports_extended_profile_fields().await? {
278                    Some(ProfileFieldsCapability::new(true))
279                } else {
280                    None
281                }
282            }
283        };
284
285        #[allow(deprecated)]
286        Ok(ProfileCapabilities {
287            profile_fields,
288            set_displayname: capabilities.set_displayname.enabled,
289            set_avatar_url: capabilities.set_avatar_url.enabled,
290        })
291    }
292
293    /// Whether the homeserver supports extended profile fields.
294    ///
295    ///
296    /// [Matrix spec]: https://spec.matrix.org/latest/client-server-api/#mprofile_fields-capability
297    async fn homeserver_supports_extended_profile_fields(&self) -> crate::Result<bool> {
298        let supported_versions = self.client.supported_versions().await?;
299        // If the homeserver supports the endpoint to delete profile fields, it
300        // supports extended profile fields.
301        Ok(delete_profile_field::v3::Request::PATH_BUILDER.is_supported(&supported_versions))
302    }
303}
304
305/// All the capabilities to change a profile field.
306struct ProfileCapabilities {
307    /// The capability to change profile fields, advertised by the homeserver or
308    /// computed.
309    profile_fields: Option<ProfileFieldsCapability>,
310    /// The capability to set the display name advertised by the homeserver.
311    set_displayname: bool,
312    /// The capability to set the avatar URL advertised by the homeserver.
313    set_avatar_url: bool,
314}
315
316#[cfg(all(not(target_family = "wasm"), test))]
317mod tests {
318    use std::time::Duration;
319
320    use assert_matches::assert_matches;
321    use matrix_sdk_base::sleep::sleep;
322    use matrix_sdk_test::async_test;
323    #[allow(deprecated)]
324    use ruma::api::{
325        MatrixVersion,
326        client::discovery::get_capabilities::v3::{
327            SetAvatarUrlCapability, SetDisplayNameCapability,
328        },
329    };
330
331    use super::*;
332    use crate::test_utils::mocks::MatrixMockServer;
333
334    #[async_test]
335    async fn test_refresh_always_updates_capabilities() {
336        let server = MatrixMockServer::new().await;
337        let client = server.client_builder().build().await;
338
339        // Set the expected capabilities to something we can check
340        let mut expected_capabilities = Capabilities::default();
341        expected_capabilities.change_password.enabled = true;
342        server
343            .mock_get_homeserver_capabilities()
344            .ok_with_capabilities(expected_capabilities)
345            .mock_once()
346            .mount()
347            .await;
348
349        // Refresh the capabilities
350        let capabilities = client.homeserver_capabilities();
351        capabilities.refresh().await.expect("refreshing capabilities failed");
352
353        // Check the values we get are updated
354        assert!(capabilities.can_change_password().await.expect("checking capabilities failed"));
355
356        let mut expected_capabilities = Capabilities::default();
357        expected_capabilities.change_password.enabled = false;
358        server
359            .mock_get_homeserver_capabilities()
360            .ok_with_capabilities(expected_capabilities)
361            .mock_once()
362            .mount()
363            .await;
364
365        // Check the values we get are not updated without a refresh, they're
366        // loaded from the cache
367        assert!(capabilities.can_change_password().await.expect("checking capabilities failed"));
368
369        // Do another refresh to make sure we get the updated values
370        capabilities.refresh().await.expect("refreshing capabilities failed");
371
372        // Check the values we get are updated
373        assert!(!capabilities.can_change_password().await.expect("checking capabilities failed"));
374    }
375
376    #[async_test]
377    async fn test_get_functions_refresh_the_data_if_not_available_or_use_cache_if_available() {
378        let server = MatrixMockServer::new().await;
379        let client = server.client_builder().build().await;
380
381        // Set the expected capabilities to something we can check
382        let mut expected_capabilities = Capabilities::default();
383        let mut profile_fields = ProfileFieldsCapability::new(true);
384        profile_fields.allowed = Some(vec![ProfileFieldName::DisplayName]);
385        expected_capabilities.profile_fields = Some(profile_fields);
386        server
387            .mock_get_homeserver_capabilities()
388            .ok_with_capabilities(expected_capabilities)
389            // Ensure it's called just once
390            .mock_once()
391            .mount()
392            .await;
393
394        // Refresh the capabilities
395        let capabilities = client.homeserver_capabilities();
396
397        // Check the values we get are updated
398        assert!(capabilities.can_change_displayname().await.expect("checking capabilities failed"));
399
400        // Now revert the previous mock so we can check we're getting the cached
401        // value instead of this one
402        let mut expected_capabilities = Capabilities::default();
403        let mut profile_fields = ProfileFieldsCapability::new(true);
404        profile_fields.disallowed = Some(vec![ProfileFieldName::DisplayName]);
405        expected_capabilities.profile_fields = Some(profile_fields);
406        server
407            .mock_get_homeserver_capabilities()
408            .ok_with_capabilities(expected_capabilities)
409            .expect(1)
410            .mount()
411            .await;
412
413        // Check the values we get are not updated without a refresh, they're
414        // loaded from the cache
415        assert!(capabilities.can_change_displayname().await.expect("checking capabilities failed"));
416
417        // Force an expiry of the data.
418        let capabilities_data =
419            capabilities.homeserver_capabilities_cached().await.unwrap().unwrap();
420        let mut ttl_value = TtlValue::new(capabilities_data);
421        ttl_value.expire();
422        client.inner.caches.homeserver_capabilities.set_value(ttl_value);
423
424        // Call a method to trigger a cache refresh background task.
425        capabilities.homeserver_capabilities_cached().await.unwrap().unwrap();
426
427        // We wait for the task to finish, the endpoint should have been called
428        // again.
429        sleep(Duration::from_secs(1)).await;
430        assert_matches!(client.inner.caches.homeserver_capabilities.value(), CachedValue::Cached(value) if !value.has_expired());
431    }
432
433    #[async_test]
434    #[allow(deprecated)]
435    async fn test_deprecated_profile_fields_capabilities() {
436        let server = MatrixMockServer::new().await;
437
438        // The user can only set the display name but not the avatar url or
439        // extended profile fields.
440        let mut capabilities = Capabilities::new();
441        capabilities.profile_fields.take();
442        capabilities.set_displayname = SetDisplayNameCapability::new(true);
443        capabilities.set_avatar_url = SetAvatarUrlCapability::new(false);
444        server
445            .mock_get_homeserver_capabilities()
446            .ok_with_capabilities(capabilities)
447            // It should be called once by each client below.
448            .expect(2)
449            .mount()
450            .await;
451
452        // Client with Matrix 1.12 that did not support extended profile fields
453        // yet. Because there is no `m.profile_fields` capability, we rely on
454        // the legacy profile capabilities.
455        let client =
456            server.client_builder().server_versions(vec![MatrixVersion::V1_12]).build().await;
457        let capabilities_api = client.homeserver_capabilities();
458        assert!(
459            capabilities_api
460                .can_change_displayname()
461                .await
462                .expect("checking displayname capability failed")
463        );
464        assert!(
465            !capabilities_api.can_change_avatar().await.expect("checking avatar capability failed")
466        );
467        assert!(
468            !capabilities_api
469                .extended_profile_fields()
470                .await
471                .expect("checking profile fields capability failed")
472                .enabled
473        );
474
475        // Client with Matrix 1.16 that added support for extended profile
476        // fields, the deprecated profile capabilities are ignored.
477        let client =
478            server.client_builder().server_versions(vec![MatrixVersion::V1_16]).build().await;
479        let capabilities_api = client.homeserver_capabilities();
480        assert!(
481            capabilities_api
482                .can_change_displayname()
483                .await
484                .expect("checking displayname capability failed")
485        );
486        assert!(
487            capabilities_api.can_change_avatar().await.expect("checking avatar capability failed")
488        );
489        assert!(
490            capabilities_api
491                .extended_profile_fields()
492                .await
493                .expect("checking profile fields capability failed")
494                .enabled
495        );
496    }
497
498    #[async_test]
499    #[allow(deprecated)]
500    async fn test_extended_profile_fields_capabilities_enabled() {
501        let server = MatrixMockServer::new().await;
502
503        // The user can set any profile field. The legacy capabilities say
504        // differently, but they will be ignored.
505        let mut capabilities = Capabilities::new();
506        capabilities.profile_fields = Some(ProfileFieldsCapability::new(true));
507        capabilities.set_displayname = SetDisplayNameCapability::new(true);
508        capabilities.set_avatar_url = SetAvatarUrlCapability::new(false);
509        server
510            .mock_get_homeserver_capabilities()
511            .ok_with_capabilities(capabilities)
512            // It should be called once by each client below.
513            .expect(2)
514            .mount()
515            .await;
516
517        // Client with Matrix 1.12 that did not support extended profile fields
518        // yet. However, because there is an `m.profile_fields` capability, we
519        // still rely on it.
520        let client =
521            server.client_builder().server_versions(vec![MatrixVersion::V1_12]).build().await;
522        let capabilities_api = client.homeserver_capabilities();
523        assert!(
524            capabilities_api
525                .can_change_displayname()
526                .await
527                .expect("checking displayname capability failed")
528        );
529        assert!(
530            capabilities_api.can_change_avatar().await.expect("checking avatar capability failed")
531        );
532        assert!(
533            capabilities_api
534                .extended_profile_fields()
535                .await
536                .expect("checking profile fields capability failed")
537                .enabled
538        );
539
540        // Client with Matrix 1.16 that added support for extended profile
541        // fields, only the `m.profile_fields` capability is used too.
542        let client =
543            server.client_builder().server_versions(vec![MatrixVersion::V1_16]).build().await;
544        let capabilities_api = client.homeserver_capabilities();
545        assert!(
546            capabilities_api
547                .can_change_displayname()
548                .await
549                .expect("checking displayname capability failed")
550        );
551        assert!(
552            capabilities_api.can_change_avatar().await.expect("checking avatar capability failed")
553        );
554        assert!(
555            capabilities_api
556                .extended_profile_fields()
557                .await
558                .expect("checking profile fields capability failed")
559                .enabled
560        );
561    }
562
563    #[async_test]
564    #[allow(deprecated)]
565    async fn test_extended_profile_fields_capabilities_disabled() {
566        let server = MatrixMockServer::new().await;
567
568        // The user cannot set any profile field. The legacy capabilities say
569        // differently, but they will be ignored.
570        let mut capabilities = Capabilities::new();
571        capabilities.profile_fields = Some(ProfileFieldsCapability::new(false));
572        capabilities.set_displayname = SetDisplayNameCapability::new(true);
573        capabilities.set_avatar_url = SetAvatarUrlCapability::new(false);
574        server
575            .mock_get_homeserver_capabilities()
576            .ok_with_capabilities(capabilities)
577            // It should be called once by each client below.
578            .expect(2)
579            .mount()
580            .await;
581
582        // Client with Matrix 1.12 that did not support extended profile fields
583        // yet. However, because there is an `m.profile_fields` capability, we
584        // still rely on it.
585        let client =
586            server.client_builder().server_versions(vec![MatrixVersion::V1_12]).build().await;
587        let capabilities_api = client.homeserver_capabilities();
588        assert!(
589            !capabilities_api
590                .can_change_displayname()
591                .await
592                .expect("checking displayname capability failed")
593        );
594        assert!(
595            !capabilities_api.can_change_avatar().await.expect("checking avatar capability failed")
596        );
597        assert!(
598            !capabilities_api
599                .extended_profile_fields()
600                .await
601                .expect("checking profile fields capability failed")
602                .enabled
603        );
604
605        // Client with Matrix 1.16 that added support for extended profile
606        // fields, only the `m.profile_fields` capability is used too.
607        let client =
608            server.client_builder().server_versions(vec![MatrixVersion::V1_16]).build().await;
609        let capabilities_api = client.homeserver_capabilities();
610        assert!(
611            !capabilities_api
612                .can_change_displayname()
613                .await
614                .expect("checking displayname capability failed")
615        );
616        assert!(
617            !capabilities_api.can_change_avatar().await.expect("checking avatar capability failed")
618        );
619        assert!(
620            !capabilities_api
621                .extended_profile_fields()
622                .await
623                .expect("checking profile fields capability failed")
624                .enabled
625        );
626    }
627
628    #[async_test]
629    #[allow(deprecated)]
630    async fn test_fine_grained_extended_profile_fields_capabilities() {
631        let server = MatrixMockServer::new().await;
632
633        // The user can only set the avatar URL. The legacy capabilities say
634        // differently, but they will be ignored.
635        let mut profile_fields = ProfileFieldsCapability::new(true);
636        profile_fields.allowed = Some(vec![ProfileFieldName::AvatarUrl]);
637        let mut capabilities = Capabilities::new();
638        capabilities.profile_fields = Some(profile_fields);
639        capabilities.set_displayname = SetDisplayNameCapability::new(true);
640        capabilities.set_avatar_url = SetAvatarUrlCapability::new(false);
641        server
642            .mock_get_homeserver_capabilities()
643            .ok_with_capabilities(capabilities)
644            // It should be called once by each client below.
645            .expect(2)
646            .mount()
647            .await;
648
649        // Client with Matrix 1.12 that did not support extended profile fields
650        // yet. However, because there is an `m.profile_fields` capability, we
651        // still rely on it.
652        let client =
653            server.client_builder().server_versions(vec![MatrixVersion::V1_12]).build().await;
654        let capabilities_api = client.homeserver_capabilities();
655        assert!(
656            !capabilities_api
657                .can_change_displayname()
658                .await
659                .expect("checking displayname capability failed")
660        );
661        assert!(
662            capabilities_api.can_change_avatar().await.expect("checking avatar capability failed")
663        );
664        assert!(
665            capabilities_api
666                .extended_profile_fields()
667                .await
668                .expect("checking profile fields capability failed")
669                .enabled
670        );
671
672        // Client with Matrix 1.16 that added support for extended profile
673        // fields, only the `m.profile_fields` capability is used too.
674        let client =
675            server.client_builder().server_versions(vec![MatrixVersion::V1_16]).build().await;
676        let capabilities_api = client.homeserver_capabilities();
677        assert!(
678            !capabilities_api
679                .can_change_displayname()
680                .await
681                .expect("checking displayname capability failed")
682        );
683        assert!(
684            capabilities_api.can_change_avatar().await.expect("checking avatar capability failed")
685        );
686        assert!(
687            capabilities_api
688                .extended_profile_fields()
689                .await
690                .expect("checking profile fields capability failed")
691                .enabled
692        );
693    }
694}