Skip to main content

ruma_client_api/discovery/
get_capabilities.rs

1//! `GET /_matrix/client/*/capabilities`
2//!
3//! Get information about the server's supported feature set and other relevant capabilities
4//! ([spec]).
5//!
6//! [spec]: https://spec.matrix.org/v1.19/client-server-api/#capabilities-negotiation
7
8pub mod v3 {
9    //! `/v3/` ([spec])
10    //!
11    //! [spec]: https://spec.matrix.org/v1.19/client-server-api/#get_matrixclientv3capabilities
12
13    #[cfg(feature = "unstable-msc4540")]
14    use std::collections::BTreeSet;
15    use std::{borrow::Cow, collections::BTreeMap};
16
17    use maplit::btreemap;
18    #[cfg(feature = "unstable-msc4540")]
19    use ruma_common::api::OAuthClientScope;
20    use ruma_common::{
21        RoomVersionId,
22        api::{auth_scheme::AccessToken, request, response},
23        metadata,
24        profile::ProfileFieldName,
25        serde::StringEnum,
26    };
27    use serde::{Deserialize, Serialize};
28    use serde_json::{
29        Value as JsonValue, from_value as from_json_value, to_value as to_json_value,
30    };
31
32    use crate::PrivOwnedStr;
33
34    metadata! {
35        method: GET,
36        rate_limited: true,
37        authentication: AccessToken,
38        history: {
39            1.0 => "/_matrix/client/r0/capabilities",
40            1.1 => "/_matrix/client/v3/capabilities",
41        }
42    }
43
44    /// Request type for the `get_capabilities` endpoint.
45    #[request]
46    #[derive(Default)]
47    pub struct Request {}
48
49    /// Response type for the `get_capabilities` endpoint.
50    #[response]
51    pub struct Response {
52        /// The capabilities the server supports
53        pub capabilities: Capabilities,
54    }
55
56    impl Request {
57        /// Creates an empty `Request`.
58        pub fn new() -> Self {
59            Self {}
60        }
61    }
62
63    impl Response {
64        /// Creates a new `Response` with the given capabilities.
65        pub fn new(capabilities: Capabilities) -> Self {
66            Self { capabilities }
67        }
68    }
69
70    impl From<Capabilities> for Response {
71        fn from(capabilities: Capabilities) -> Self {
72            Self::new(capabilities)
73        }
74    }
75
76    /// Contains information about all the capabilities that the server supports.
77    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
78    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
79    #[allow(deprecated)]
80    pub struct Capabilities {
81        /// Capability to indicate if the user can change their password.
82        #[serde(
83            rename = "m.change_password",
84            default,
85            skip_serializing_if = "ChangePasswordCapability::is_default"
86        )]
87        pub change_password: ChangePasswordCapability,
88
89        /// The room versions the server supports.
90        #[serde(
91            rename = "m.room_versions",
92            default,
93            skip_serializing_if = "RoomVersionsCapability::is_default"
94        )]
95        pub room_versions: RoomVersionsCapability,
96
97        /// Capability to indicate if the user can change their display name.
98        #[serde(
99            rename = "m.set_displayname",
100            default,
101            skip_serializing_if = "SetDisplayNameCapability::is_default"
102        )]
103        #[deprecated = "Since Matrix 1.16, prefer profile_fields if it is set."]
104        pub set_displayname: SetDisplayNameCapability,
105
106        /// Capability to indicate if the user can change their avatar.
107        #[serde(
108            rename = "m.set_avatar_url",
109            default,
110            skip_serializing_if = "SetAvatarUrlCapability::is_default"
111        )]
112        #[deprecated = "Since Matrix 1.16, prefer profile_fields if it is set."]
113        pub set_avatar_url: SetAvatarUrlCapability,
114
115        /// Capability to indicate if the user can change the third-party identifiers associated
116        /// with their account.
117        #[serde(
118            rename = "m.3pid_changes",
119            default,
120            skip_serializing_if = "ThirdPartyIdChangesCapability::is_default"
121        )]
122        pub thirdparty_id_changes: ThirdPartyIdChangesCapability,
123
124        /// Capability to indicate if the user can generate tokens to log further clients into
125        /// their account.
126        #[serde(
127            rename = "m.get_login_token",
128            default,
129            skip_serializing_if = "GetLoginTokenCapability::is_default"
130        )]
131        pub get_login_token: GetLoginTokenCapability,
132
133        /// Capability to indicate if the user can set extended profile fields.
134        #[serde(
135            rename = "m.profile_fields",
136            alias = "uk.tcpip.msc4133.profile_fields",
137            skip_serializing_if = "Option::is_none"
138        )]
139        pub profile_fields: Option<ProfileFieldsCapability>,
140
141        /// Capability to indicate if the server automatically forgets rooms that the user leaves.
142        #[serde(
143            rename = "m.forget_forced_upon_leave",
144            default,
145            skip_serializing_if = "ForgetForcedUponLeaveCapability::is_default"
146        )]
147        pub forget_forced_upon_leave: ForgetForcedUponLeaveCapability,
148
149        /// Capability to indicate if the user can perform account moderation actions via [server
150        /// administration] endpoints.
151        ///
152        /// [server administration]: https://spec.matrix.org/v1.19/client-server-api/#server-administration
153        #[serde(
154            rename = "m.account_moderation",
155            default,
156            skip_serializing_if = "AccountModerationCapability::is_default"
157        )]
158        pub account_moderation: AccountModerationCapability,
159
160        /// Capability to indicate if the user can perform administrative actions. ([MSC4540])
161        ///
162        /// [MSC4540]: https://github.com/matrix-org/matrix-spec-proposals/pull/4540
163        #[cfg(feature = "unstable-msc4540")]
164        #[serde(
165            rename = "org.continuwuity.msc4540.admin",
166            default,
167            skip_serializing_if = "AdminCapability::is_default"
168        )]
169        pub admin: AdminCapability,
170
171        /// Any other custom capabilities that the server supports outside of the specification,
172        /// labeled using the Java package naming convention and stored as arbitrary JSON values.
173        #[serde(flatten)]
174        custom_capabilities: BTreeMap<String, JsonValue>,
175    }
176
177    impl Capabilities {
178        /// Creates empty `Capabilities`.
179        pub fn new() -> Self {
180            Default::default()
181        }
182
183        /// Returns the value of the given capability.
184        ///
185        /// Prefer to use the public fields of `Capabilities` where possible; this method is meant
186        /// to be used for unsupported capabilities only.
187        pub fn get(&self, capability: &str) -> Option<Cow<'_, JsonValue>> {
188            fn serialize<T: Serialize>(cap: &T) -> JsonValue {
189                to_json_value(cap).expect("capability serialization to succeed")
190            }
191
192            match capability {
193                "m.change_password" => Some(Cow::Owned(serialize(&self.change_password))),
194                "m.room_versions" => Some(Cow::Owned(serialize(&self.room_versions))),
195                #[allow(deprecated)]
196                "m.set_displayname" => Some(Cow::Owned(serialize(&self.set_displayname))),
197                #[allow(deprecated)]
198                "m.set_avatar_url" => Some(Cow::Owned(serialize(&self.set_avatar_url))),
199                "m.3pid_changes" => Some(Cow::Owned(serialize(&self.thirdparty_id_changes))),
200                "m.get_login_token" => Some(Cow::Owned(serialize(&self.get_login_token))),
201                "m.profile_fields" | "uk.tcpip.msc4133.profile_fields" => {
202                    self.profile_fields.as_ref().map(|cap| Cow::Owned(serialize(cap)))
203                }
204                "m.forget_forced_upon_leave" => {
205                    Some(Cow::Owned(serialize(&self.forget_forced_upon_leave)))
206                }
207                "m.account_moderation" => Some(Cow::Owned(serialize(&self.account_moderation))),
208                #[cfg(feature = "unstable-msc4540")]
209                "org.continuwuity.msc4540.admin" => Some(Cow::Owned(serialize(&self.admin))),
210                _ => self.custom_capabilities.get(capability).map(Cow::Borrowed),
211            }
212        }
213
214        /// Sets a capability to the given value.
215        ///
216        /// Prefer to use the public fields of `Capabilities` where possible; this method is meant
217        /// to be used for unsupported capabilities only and does not allow setting
218        /// arbitrary data for supported ones.
219        pub fn set(&mut self, capability: &str, value: JsonValue) -> serde_json::Result<()> {
220            match capability {
221                "m.change_password" => self.change_password = from_json_value(value)?,
222                "m.room_versions" => self.room_versions = from_json_value(value)?,
223                #[allow(deprecated)]
224                "m.set_displayname" => self.set_displayname = from_json_value(value)?,
225                #[allow(deprecated)]
226                "m.set_avatar_url" => self.set_avatar_url = from_json_value(value)?,
227                "m.3pid_changes" => self.thirdparty_id_changes = from_json_value(value)?,
228                "m.get_login_token" => self.get_login_token = from_json_value(value)?,
229                "m.profile_fields" | "uk.tcpip.msc4133.profile_fields" => {
230                    self.profile_fields = from_json_value(value)?;
231                }
232                "m.forget_forced_upon_leave" => {
233                    self.forget_forced_upon_leave = from_json_value(value)?;
234                }
235                "m.account_moderation" => {
236                    self.account_moderation = from_json_value(value)?;
237                }
238                #[cfg(feature = "unstable-msc4540")]
239                "org.continuwuity.msc4540.admin" => self.admin = from_json_value(value)?,
240                _ => {
241                    self.custom_capabilities.insert(capability.to_owned(), value);
242                }
243            }
244
245            Ok(())
246        }
247    }
248
249    /// Information about the m.change_password capability
250    #[derive(Clone, Debug, Serialize, Deserialize)]
251    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
252    pub struct ChangePasswordCapability {
253        /// `true` if the user can change their password, `false` otherwise.
254        pub enabled: bool,
255    }
256
257    impl ChangePasswordCapability {
258        /// Creates a new `ChangePasswordCapability` with the given enabled flag.
259        pub fn new(enabled: bool) -> Self {
260            Self { enabled }
261        }
262
263        /// Returns whether all fields have their default value.
264        pub fn is_default(&self) -> bool {
265            self.enabled
266        }
267    }
268
269    impl Default for ChangePasswordCapability {
270        fn default() -> Self {
271            Self { enabled: true }
272        }
273    }
274
275    /// Information about the m.room_versions capability
276    #[derive(Clone, Debug, Serialize, Deserialize)]
277    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
278    pub struct RoomVersionsCapability {
279        /// The default room version the server is using for new rooms.
280        pub default: RoomVersionId,
281
282        /// A detailed description of the room versions the server supports.
283        pub available: BTreeMap<RoomVersionId, RoomVersionStability>,
284    }
285
286    impl RoomVersionsCapability {
287        /// Creates a new `RoomVersionsCapability` with the given default room version ID and room
288        /// version descriptions.
289        pub fn new(
290            default: RoomVersionId,
291            available: BTreeMap<RoomVersionId, RoomVersionStability>,
292        ) -> Self {
293            Self { default, available }
294        }
295
296        /// Returns whether all fields have their default value.
297        pub fn is_default(&self) -> bool {
298            self.default == RoomVersionId::V1
299                && self.available.len() == 1
300                && self
301                    .available
302                    .get(&RoomVersionId::V1)
303                    .map(|stability| *stability == RoomVersionStability::Stable)
304                    .unwrap_or(false)
305        }
306    }
307
308    impl Default for RoomVersionsCapability {
309        fn default() -> Self {
310            Self {
311                default: RoomVersionId::V1,
312                available: btreemap! { RoomVersionId::V1 => RoomVersionStability::Stable },
313            }
314        }
315    }
316
317    /// The stability of a room version.
318    #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
319    #[derive(Clone, StringEnum)]
320    #[ruma_enum(rename_all = "lowercase")]
321    #[non_exhaustive]
322    pub enum RoomVersionStability {
323        /// Support for the given version is stable.
324        Stable,
325
326        /// Support for the given version is unstable.
327        Unstable,
328
329        #[doc(hidden)]
330        _Custom(PrivOwnedStr),
331    }
332
333    /// Information about the `m.set_displayname` capability
334    #[derive(Clone, Debug, Serialize, Deserialize)]
335    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
336    #[deprecated = "Since Matrix 1.16, prefer ProfileFieldsCapability instead."]
337    pub struct SetDisplayNameCapability {
338        /// `true` if the user can change their display name, `false` otherwise.
339        pub enabled: bool,
340    }
341
342    #[allow(deprecated)]
343    impl SetDisplayNameCapability {
344        /// Creates a new `SetDisplayNameCapability` with the given enabled flag.
345        pub fn new(enabled: bool) -> Self {
346            Self { enabled }
347        }
348
349        /// Returns whether all fields have their default value.
350        pub fn is_default(&self) -> bool {
351            self.enabled
352        }
353    }
354
355    #[allow(deprecated)]
356    impl Default for SetDisplayNameCapability {
357        fn default() -> Self {
358            Self { enabled: true }
359        }
360    }
361
362    /// Information about the `m.set_avatar_url` capability
363    #[derive(Clone, Debug, Serialize, Deserialize)]
364    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
365    #[deprecated = "Since Matrix 1.16, prefer ProfileFieldsCapability instead."]
366    pub struct SetAvatarUrlCapability {
367        /// `true` if the user can change their avatar, `false` otherwise.
368        pub enabled: bool,
369    }
370
371    #[allow(deprecated)]
372    impl SetAvatarUrlCapability {
373        /// Creates a new `SetAvatarUrlCapability` with the given enabled flag.
374        pub fn new(enabled: bool) -> Self {
375            Self { enabled }
376        }
377
378        /// Returns whether all fields have their default value.
379        pub fn is_default(&self) -> bool {
380            self.enabled
381        }
382    }
383
384    #[allow(deprecated)]
385    impl Default for SetAvatarUrlCapability {
386        fn default() -> Self {
387            Self { enabled: true }
388        }
389    }
390
391    /// Information about the `m.3pid_changes` capability
392    #[derive(Clone, Debug, Serialize, Deserialize)]
393    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
394    pub struct ThirdPartyIdChangesCapability {
395        /// `true` if the user can change the third-party identifiers associated with their
396        /// account, `false` otherwise.
397        pub enabled: bool,
398    }
399
400    impl ThirdPartyIdChangesCapability {
401        /// Creates a new `ThirdPartyIdChangesCapability` with the given enabled flag.
402        pub fn new(enabled: bool) -> Self {
403            Self { enabled }
404        }
405
406        /// Returns whether all fields have their default value.
407        pub fn is_default(&self) -> bool {
408            self.enabled
409        }
410    }
411
412    impl Default for ThirdPartyIdChangesCapability {
413        fn default() -> Self {
414            Self { enabled: true }
415        }
416    }
417
418    /// Information about the `m.get_login_token` capability.
419    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
420    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
421    pub struct GetLoginTokenCapability {
422        /// Whether the user can request a login token.
423        pub enabled: bool,
424    }
425
426    impl GetLoginTokenCapability {
427        /// Creates a new `GetLoginTokenCapability` with the given enabled flag.
428        pub fn new(enabled: bool) -> Self {
429            Self { enabled }
430        }
431
432        /// Returns whether all fields have their default value.
433        pub fn is_default(&self) -> bool {
434            !self.enabled
435        }
436    }
437
438    /// Information about the `m.profile_fields` capability.
439    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
440    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
441    pub struct ProfileFieldsCapability {
442        /// Whether the user can set extended profile fields.
443        pub enabled: bool,
444
445        /// The fields that can be set by the user.
446        #[serde(skip_serializing_if = "Option::is_none")]
447        pub allowed: Option<Vec<ProfileFieldName>>,
448
449        /// The fields that cannot be set by the user.
450        ///
451        /// This list is ignored if `allowed` is provided.
452        #[serde(skip_serializing_if = "Option::is_none")]
453        pub disallowed: Option<Vec<ProfileFieldName>>,
454    }
455
456    impl ProfileFieldsCapability {
457        /// Creates a new `ProfileFieldsCapability` with the given enabled flag.
458        pub fn new(enabled: bool) -> Self {
459            Self { enabled, allowed: None, disallowed: None }
460        }
461
462        /// Whether the server advertises that the field with the given name can be set.
463        pub fn can_set_field(&self, field: &ProfileFieldName) -> bool {
464            if !self.enabled {
465                return false;
466            }
467
468            if let Some(allowed) = &self.allowed {
469                allowed.contains(field)
470            } else if let Some(disallowed) = &self.disallowed {
471                !disallowed.contains(field)
472            } else {
473                // The default is that any field is allowed.
474                true
475            }
476        }
477    }
478
479    /// Information about the [`m.forget_forced_upon_leave`] capability.
480    ///
481    /// [`m.forget_forced_upon_leave`]: https://spec.matrix.org/v1.19/client-server-api/#mforget_forced_upon_leave-capability
482    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
483    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
484    pub struct ForgetForcedUponLeaveCapability {
485        /// Whether the server will automatically forget any room that the user leaves.
486        ///
487        /// This behavior applies irrespective of whether the user has left the room on their own
488        /// or has been kicked or banned from the room by another user.
489        pub enabled: bool,
490    }
491
492    impl ForgetForcedUponLeaveCapability {
493        /// Creates a new `ForgetForcedUponLeaveCapability` with the given enabled flag.
494        pub fn new(enabled: bool) -> Self {
495            Self { enabled }
496        }
497
498        /// Returns whether all fields have their default value.
499        pub fn is_default(&self) -> bool {
500            !self.enabled
501        }
502    }
503
504    /// Information about the `m.account_moderation` capability.
505    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
506    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
507    pub struct AccountModerationCapability {
508        /// Whether the user can suspend a user via `PUT /admin/suspend/{userId}`.
509        #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
510        pub suspend: bool,
511
512        /// Whether the user can lock a user via `PUT /admin/lock/{userId}`.
513        #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
514        pub lock: bool,
515    }
516
517    impl AccountModerationCapability {
518        /// Creates a new `AccountModerationCapability` with the given suspend and lock
519        /// capabilities.
520        pub fn new(suspend: bool, lock: bool) -> Self {
521            Self { suspend, lock }
522        }
523
524        /// Returns whether all fields have their default value.
525        pub fn is_default(&self) -> bool {
526            !self.suspend && !self.lock
527        }
528    }
529
530    /// Information about the `m.admin` capability. ([MSC4540])
531    ///
532    /// [MSC4540]: https://github.com/matrix-org/matrix-spec-proposals/pull/4540
533    #[cfg(feature = "unstable-msc4540")]
534    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
535    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
536    pub struct AdminCapability {
537        /// The scopes the device may request to access administrative functionality.
538        pub allowed_scopes: BTreeSet<OAuthClientScope>,
539    }
540
541    #[cfg(feature = "unstable-msc4540")]
542    impl AdminCapability {
543        /// Create a new [`AdminCapability`].
544        pub fn new(allowed_scopes: BTreeSet<OAuthClientScope>) -> Self {
545            Self { allowed_scopes }
546        }
547
548        /// Returns whether the capability indicates that the authenticated user
549        /// is able to access some administrative functionality.
550        pub fn is_admin(&self) -> bool {
551            !self.allowed_scopes.is_empty()
552        }
553
554        /// Returns whether the admin functionality gated by a particular scope
555        /// is available to the authenticated user.
556        pub fn is_scope_allowed(&self, scope: &OAuthClientScope) -> bool {
557            self.allowed_scopes.contains(scope)
558        }
559
560        /// Returns whether all fields have their default value.
561        fn is_default(&self) -> bool {
562            let Self { allowed_scopes } = self;
563
564            allowed_scopes.is_empty()
565        }
566    }
567
568    #[cfg(test)]
569    mod tests {
570        use ruma_common::RoomVersionId;
571        #[cfg(feature = "unstable-msc4540")]
572        use ruma_common::api::OAuthClientScope;
573        use serde_json::to_value as to_json_value;
574
575        #[allow(deprecated)]
576        use super::{
577            AccountModerationCapability, Capabilities, ChangePasswordCapability,
578            ForgetForcedUponLeaveCapability, GetLoginTokenCapability, ProfileFieldsCapability,
579            RoomVersionStability, RoomVersionsCapability, SetAvatarUrlCapability,
580            SetDisplayNameCapability, ThirdPartyIdChangesCapability,
581        };
582
583        /// Capabilities with every typed field set to a non-default value.
584        #[allow(deprecated)]
585        fn non_default_capabilities() -> Capabilities {
586            Capabilities {
587                change_password: ChangePasswordCapability::new(false),
588                room_versions: RoomVersionsCapability::new(
589                    RoomVersionId::V11,
590                    [(RoomVersionId::V11, RoomVersionStability::Stable)].into(),
591                ),
592                set_displayname: SetDisplayNameCapability::new(false),
593                set_avatar_url: SetAvatarUrlCapability::new(false),
594                thirdparty_id_changes: ThirdPartyIdChangesCapability::new(false),
595                get_login_token: GetLoginTokenCapability::new(true),
596                profile_fields: Some(ProfileFieldsCapability::new(true)),
597                forget_forced_upon_leave: ForgetForcedUponLeaveCapability::new(true),
598                account_moderation: AccountModerationCapability::new(true, true),
599                #[cfg(feature = "unstable-msc4540")]
600                admin: super::AdminCapability::new([OAuthClientScope::ApiFullAccess].into()),
601                custom_capabilities: Default::default(),
602            }
603        }
604
605        #[test]
606        fn get_and_set_cover_typed_fields() {
607            let caps = non_default_capabilities();
608            let serialized = to_json_value(&caps).unwrap();
609            let serialized = serialized.as_object().unwrap();
610
611            let mut copy = Capabilities::new();
612            for (name, value) in serialized {
613                assert_eq!(caps.get(name).as_deref(), Some(value), "get({name})");
614                copy.set(name, value.clone()).unwrap();
615            }
616
617            assert!(copy.custom_capabilities.is_empty(), "{:?}", copy.custom_capabilities);
618            assert_eq!(to_json_value(&copy).unwrap(), to_json_value(&caps).unwrap());
619        }
620    }
621}