1pub mod v3 {
9 #[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, Deserializer, Serialize, de};
28 use serde_json::{
29 Map as JsonMap, Value as JsonValue, from_value as from_json_value,
30 to_value as to_json_value,
31 };
32
33 use crate::PrivOwnedStr;
34
35 metadata! {
36 method: GET,
37 rate_limited: true,
38 authentication: AccessToken,
39 history: {
40 1.0 => "/_matrix/client/r0/capabilities",
41 1.1 => "/_matrix/client/v3/capabilities",
42 }
43 }
44
45 #[request]
47 #[derive(Default)]
48 pub struct Request {}
49
50 #[response]
52 pub struct Response {
53 pub capabilities: Capabilities,
55 }
56
57 impl Request {
58 pub fn new() -> Self {
60 Self {}
61 }
62 }
63
64 impl Response {
65 pub fn new(capabilities: Capabilities) -> Self {
67 Self { capabilities }
68 }
69 }
70
71 impl From<Capabilities> for Response {
72 fn from(capabilities: Capabilities) -> Self {
73 Self::new(capabilities)
74 }
75 }
76
77 #[derive(Clone, Debug, Default, Serialize)]
79 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
80 #[allow(deprecated)]
81 pub struct Capabilities {
82 #[serde(
84 rename = "m.change_password",
85 default,
86 skip_serializing_if = "ChangePasswordCapability::is_default"
87 )]
88 pub change_password: ChangePasswordCapability,
89
90 #[serde(
92 rename = "m.room_versions",
93 default,
94 skip_serializing_if = "RoomVersionsCapability::is_default"
95 )]
96 pub room_versions: RoomVersionsCapability,
97
98 #[serde(
100 rename = "m.set_displayname",
101 default,
102 skip_serializing_if = "SetDisplayNameCapability::is_default"
103 )]
104 #[deprecated = "Since Matrix 1.16, prefer profile_fields if it is set."]
105 pub set_displayname: SetDisplayNameCapability,
106
107 #[serde(
109 rename = "m.set_avatar_url",
110 default,
111 skip_serializing_if = "SetAvatarUrlCapability::is_default"
112 )]
113 #[deprecated = "Since Matrix 1.16, prefer profile_fields if it is set."]
114 pub set_avatar_url: SetAvatarUrlCapability,
115
116 #[serde(
119 rename = "m.3pid_changes",
120 default,
121 skip_serializing_if = "ThirdPartyIdChangesCapability::is_default"
122 )]
123 pub thirdparty_id_changes: ThirdPartyIdChangesCapability,
124
125 #[serde(
128 rename = "m.get_login_token",
129 default,
130 skip_serializing_if = "GetLoginTokenCapability::is_default"
131 )]
132 pub get_login_token: GetLoginTokenCapability,
133
134 #[serde(rename = "m.profile_fields", skip_serializing_if = "Option::is_none")]
136 pub profile_fields: Option<ProfileFieldsCapability>,
137
138 #[serde(
140 rename = "m.forget_forced_upon_leave",
141 default,
142 skip_serializing_if = "ForgetForcedUponLeaveCapability::is_default"
143 )]
144 pub forget_forced_upon_leave: ForgetForcedUponLeaveCapability,
145
146 #[serde(
151 rename = "m.account_moderation",
152 default,
153 skip_serializing_if = "AccountModerationCapability::is_default"
154 )]
155 pub account_moderation: AccountModerationCapability,
156
157 #[cfg(feature = "unstable-msc4540")]
161 #[serde(
162 rename = "org.continuwuity.msc4540.admin",
163 default,
164 skip_serializing_if = "AdminCapability::is_default"
165 )]
166 pub admin: AdminCapability,
167
168 #[serde(flatten)]
171 custom_capabilities: BTreeMap<String, JsonValue>,
172 }
173
174 impl<'de> Deserialize<'de> for Capabilities {
175 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
176 #[derive(Deserialize)]
177 #[allow(deprecated)]
178 struct CapabilitiesDeHelper {
179 #[serde(rename = "m.change_password", default)]
180 change_password: ChangePasswordCapability,
181 #[serde(rename = "m.room_versions", default)]
182 room_versions: RoomVersionsCapability,
183 #[serde(rename = "m.set_displayname", default)]
184 set_displayname: SetDisplayNameCapability,
185 #[serde(rename = "m.set_avatar_url", default)]
186 set_avatar_url: SetAvatarUrlCapability,
187 #[serde(rename = "m.3pid_changes", default)]
188 thirdparty_id_changes: ThirdPartyIdChangesCapability,
189 #[serde(rename = "m.get_login_token", default)]
190 get_login_token: GetLoginTokenCapability,
191 #[serde(rename = "m.profile_fields", alias = "uk.tcpip.msc4133.profile_fields")]
192 profile_fields: Option<ProfileFieldsCapability>,
193 #[serde(rename = "m.forget_forced_upon_leave", default)]
194 forget_forced_upon_leave: ForgetForcedUponLeaveCapability,
195 #[serde(rename = "m.account_moderation", default)]
196 account_moderation: AccountModerationCapability,
197 #[cfg(feature = "unstable-msc4540")]
198 #[serde(rename = "org.continuwuity.msc4540.admin", default)]
199 admin: AdminCapability,
200 #[serde(flatten)]
201 custom_capabilities: BTreeMap<String, JsonValue>,
202 }
203
204 let mut capabilities = JsonMap::deserialize(deserializer)?;
205 if capabilities.contains_key("m.profile_fields") {
207 capabilities.remove("uk.tcpip.msc4133.profile_fields");
208 }
209 let helper = CapabilitiesDeHelper::deserialize(JsonValue::Object(capabilities))
210 .map_err(de::Error::custom)?;
211
212 #[allow(deprecated)]
213 Ok(Self {
214 change_password: helper.change_password,
215 room_versions: helper.room_versions,
216 set_displayname: helper.set_displayname,
217 set_avatar_url: helper.set_avatar_url,
218 thirdparty_id_changes: helper.thirdparty_id_changes,
219 get_login_token: helper.get_login_token,
220 profile_fields: helper.profile_fields,
221 forget_forced_upon_leave: helper.forget_forced_upon_leave,
222 account_moderation: helper.account_moderation,
223 #[cfg(feature = "unstable-msc4540")]
224 admin: helper.admin,
225 custom_capabilities: helper.custom_capabilities,
226 })
227 }
228 }
229
230 impl Capabilities {
231 pub fn new() -> Self {
233 Default::default()
234 }
235
236 pub fn get(&self, capability: &str) -> Option<Cow<'_, JsonValue>> {
241 fn serialize<T: Serialize>(cap: &T) -> JsonValue {
242 to_json_value(cap).expect("capability serialization to succeed")
243 }
244
245 match capability {
246 "m.change_password" => Some(Cow::Owned(serialize(&self.change_password))),
247 "m.room_versions" => Some(Cow::Owned(serialize(&self.room_versions))),
248 #[allow(deprecated)]
249 "m.set_displayname" => Some(Cow::Owned(serialize(&self.set_displayname))),
250 #[allow(deprecated)]
251 "m.set_avatar_url" => Some(Cow::Owned(serialize(&self.set_avatar_url))),
252 "m.3pid_changes" => Some(Cow::Owned(serialize(&self.thirdparty_id_changes))),
253 "m.get_login_token" => Some(Cow::Owned(serialize(&self.get_login_token))),
254 "m.profile_fields" | "uk.tcpip.msc4133.profile_fields" => {
255 self.profile_fields.as_ref().map(|cap| Cow::Owned(serialize(cap)))
256 }
257 "m.forget_forced_upon_leave" => {
258 Some(Cow::Owned(serialize(&self.forget_forced_upon_leave)))
259 }
260 "m.account_moderation" => Some(Cow::Owned(serialize(&self.account_moderation))),
261 #[cfg(feature = "unstable-msc4540")]
262 "org.continuwuity.msc4540.admin" => Some(Cow::Owned(serialize(&self.admin))),
263 _ => self.custom_capabilities.get(capability).map(Cow::Borrowed),
264 }
265 }
266
267 pub fn set(&mut self, capability: &str, value: JsonValue) -> serde_json::Result<()> {
273 match capability {
274 "m.change_password" => self.change_password = from_json_value(value)?,
275 "m.room_versions" => self.room_versions = from_json_value(value)?,
276 #[allow(deprecated)]
277 "m.set_displayname" => self.set_displayname = from_json_value(value)?,
278 #[allow(deprecated)]
279 "m.set_avatar_url" => self.set_avatar_url = from_json_value(value)?,
280 "m.3pid_changes" => self.thirdparty_id_changes = from_json_value(value)?,
281 "m.get_login_token" => self.get_login_token = from_json_value(value)?,
282 "m.profile_fields" | "uk.tcpip.msc4133.profile_fields" => {
283 self.profile_fields = from_json_value(value)?;
284 }
285 "m.forget_forced_upon_leave" => {
286 self.forget_forced_upon_leave = from_json_value(value)?;
287 }
288 "m.account_moderation" => {
289 self.account_moderation = from_json_value(value)?;
290 }
291 #[cfg(feature = "unstable-msc4540")]
292 "org.continuwuity.msc4540.admin" => self.admin = from_json_value(value)?,
293 _ => {
294 self.custom_capabilities.insert(capability.to_owned(), value);
295 }
296 }
297
298 Ok(())
299 }
300 }
301
302 #[derive(Clone, Debug, Serialize, Deserialize)]
304 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
305 pub struct ChangePasswordCapability {
306 pub enabled: bool,
308 }
309
310 impl ChangePasswordCapability {
311 pub fn new(enabled: bool) -> Self {
313 Self { enabled }
314 }
315
316 pub fn is_default(&self) -> bool {
318 self.enabled
319 }
320 }
321
322 impl Default for ChangePasswordCapability {
323 fn default() -> Self {
324 Self { enabled: true }
325 }
326 }
327
328 #[derive(Clone, Debug, Serialize, Deserialize)]
330 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
331 pub struct RoomVersionsCapability {
332 pub default: RoomVersionId,
334
335 pub available: BTreeMap<RoomVersionId, RoomVersionStability>,
337 }
338
339 impl RoomVersionsCapability {
340 pub fn new(
343 default: RoomVersionId,
344 available: BTreeMap<RoomVersionId, RoomVersionStability>,
345 ) -> Self {
346 Self { default, available }
347 }
348
349 pub fn is_default(&self) -> bool {
351 self.default == RoomVersionId::V1
352 && self.available.len() == 1
353 && self
354 .available
355 .get(&RoomVersionId::V1)
356 .map(|stability| *stability == RoomVersionStability::Stable)
357 .unwrap_or(false)
358 }
359 }
360
361 impl Default for RoomVersionsCapability {
362 fn default() -> Self {
363 Self {
364 default: RoomVersionId::V1,
365 available: btreemap! { RoomVersionId::V1 => RoomVersionStability::Stable },
366 }
367 }
368 }
369
370 #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
372 #[derive(Clone, StringEnum)]
373 #[ruma_enum(rename_all = "lowercase")]
374 #[non_exhaustive]
375 pub enum RoomVersionStability {
376 Stable,
378
379 Unstable,
381
382 #[doc(hidden)]
383 _Custom(PrivOwnedStr),
384 }
385
386 #[derive(Clone, Debug, Serialize, Deserialize)]
388 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
389 #[deprecated = "Since Matrix 1.16, prefer ProfileFieldsCapability instead."]
390 pub struct SetDisplayNameCapability {
391 pub enabled: bool,
393 }
394
395 #[allow(deprecated)]
396 impl SetDisplayNameCapability {
397 pub fn new(enabled: bool) -> Self {
399 Self { enabled }
400 }
401
402 pub fn is_default(&self) -> bool {
404 self.enabled
405 }
406 }
407
408 #[allow(deprecated)]
409 impl Default for SetDisplayNameCapability {
410 fn default() -> Self {
411 Self { enabled: true }
412 }
413 }
414
415 #[derive(Clone, Debug, Serialize, Deserialize)]
417 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
418 #[deprecated = "Since Matrix 1.16, prefer ProfileFieldsCapability instead."]
419 pub struct SetAvatarUrlCapability {
420 pub enabled: bool,
422 }
423
424 #[allow(deprecated)]
425 impl SetAvatarUrlCapability {
426 pub fn new(enabled: bool) -> Self {
428 Self { enabled }
429 }
430
431 pub fn is_default(&self) -> bool {
433 self.enabled
434 }
435 }
436
437 #[allow(deprecated)]
438 impl Default for SetAvatarUrlCapability {
439 fn default() -> Self {
440 Self { enabled: true }
441 }
442 }
443
444 #[derive(Clone, Debug, Serialize, Deserialize)]
446 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
447 pub struct ThirdPartyIdChangesCapability {
448 pub enabled: bool,
451 }
452
453 impl ThirdPartyIdChangesCapability {
454 pub fn new(enabled: bool) -> Self {
456 Self { enabled }
457 }
458
459 pub fn is_default(&self) -> bool {
461 self.enabled
462 }
463 }
464
465 impl Default for ThirdPartyIdChangesCapability {
466 fn default() -> Self {
467 Self { enabled: true }
468 }
469 }
470
471 #[derive(Clone, Debug, Default, Serialize, Deserialize)]
473 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
474 pub struct GetLoginTokenCapability {
475 pub enabled: bool,
477 }
478
479 impl GetLoginTokenCapability {
480 pub fn new(enabled: bool) -> Self {
482 Self { enabled }
483 }
484
485 pub fn is_default(&self) -> bool {
487 !self.enabled
488 }
489 }
490
491 #[derive(Clone, Debug, Default, Serialize, Deserialize)]
493 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
494 pub struct ProfileFieldsCapability {
495 pub enabled: bool,
497
498 #[serde(skip_serializing_if = "Option::is_none")]
500 pub allowed: Option<Vec<ProfileFieldName>>,
501
502 #[serde(skip_serializing_if = "Option::is_none")]
506 pub disallowed: Option<Vec<ProfileFieldName>>,
507 }
508
509 impl ProfileFieldsCapability {
510 pub fn new(enabled: bool) -> Self {
512 Self { enabled, allowed: None, disallowed: None }
513 }
514
515 pub fn can_set_field(&self, field: &ProfileFieldName) -> bool {
517 if !self.enabled {
518 return false;
519 }
520
521 if let Some(allowed) = &self.allowed {
522 allowed.contains(field)
523 } else if let Some(disallowed) = &self.disallowed {
524 !disallowed.contains(field)
525 } else {
526 true
528 }
529 }
530 }
531
532 #[derive(Clone, Debug, Default, Serialize, Deserialize)]
536 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
537 pub struct ForgetForcedUponLeaveCapability {
538 pub enabled: bool,
543 }
544
545 impl ForgetForcedUponLeaveCapability {
546 pub fn new(enabled: bool) -> Self {
548 Self { enabled }
549 }
550
551 pub fn is_default(&self) -> bool {
553 !self.enabled
554 }
555 }
556
557 #[derive(Clone, Debug, Default, Serialize, Deserialize)]
559 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
560 pub struct AccountModerationCapability {
561 #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
563 pub suspend: bool,
564
565 #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
567 pub lock: bool,
568 }
569
570 impl AccountModerationCapability {
571 pub fn new(suspend: bool, lock: bool) -> Self {
574 Self { suspend, lock }
575 }
576
577 pub fn is_default(&self) -> bool {
579 !self.suspend && !self.lock
580 }
581 }
582
583 #[cfg(feature = "unstable-msc4540")]
587 #[derive(Clone, Debug, Default, Serialize, Deserialize)]
588 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
589 pub struct AdminCapability {
590 pub allowed_scopes: BTreeSet<OAuthClientScope>,
592 }
593
594 #[cfg(feature = "unstable-msc4540")]
595 impl AdminCapability {
596 pub fn new(allowed_scopes: BTreeSet<OAuthClientScope>) -> Self {
598 Self { allowed_scopes }
599 }
600
601 pub fn is_admin(&self) -> bool {
604 !self.allowed_scopes.is_empty()
605 }
606
607 pub fn is_scope_allowed(&self, scope: &OAuthClientScope) -> bool {
610 self.allowed_scopes.contains(scope)
611 }
612
613 fn is_default(&self) -> bool {
615 let Self { allowed_scopes } = self;
616
617 allowed_scopes.is_empty()
618 }
619 }
620
621 #[cfg(test)]
622 mod tests {
623 use ruma_common::RoomVersionId;
624 #[cfg(feature = "unstable-msc4540")]
625 use ruma_common::api::OAuthClientScope;
626 use serde_json::to_value as to_json_value;
627
628 #[allow(deprecated)]
629 use super::{
630 AccountModerationCapability, Capabilities, ChangePasswordCapability,
631 ForgetForcedUponLeaveCapability, GetLoginTokenCapability, ProfileFieldsCapability,
632 RoomVersionStability, RoomVersionsCapability, SetAvatarUrlCapability,
633 SetDisplayNameCapability, ThirdPartyIdChangesCapability,
634 };
635
636 #[allow(deprecated)]
638 fn non_default_capabilities() -> Capabilities {
639 Capabilities {
640 change_password: ChangePasswordCapability::new(false),
641 room_versions: RoomVersionsCapability::new(
642 RoomVersionId::V11,
643 [(RoomVersionId::V11, RoomVersionStability::Stable)].into(),
644 ),
645 set_displayname: SetDisplayNameCapability::new(false),
646 set_avatar_url: SetAvatarUrlCapability::new(false),
647 thirdparty_id_changes: ThirdPartyIdChangesCapability::new(false),
648 get_login_token: GetLoginTokenCapability::new(true),
649 profile_fields: Some(ProfileFieldsCapability::new(true)),
650 forget_forced_upon_leave: ForgetForcedUponLeaveCapability::new(true),
651 account_moderation: AccountModerationCapability::new(true, true),
652 #[cfg(feature = "unstable-msc4540")]
653 admin: super::AdminCapability::new([OAuthClientScope::ApiFullAccess].into()),
654 custom_capabilities: Default::default(),
655 }
656 }
657
658 #[test]
659 fn get_and_set_cover_typed_fields() {
660 let caps = non_default_capabilities();
661 let serialized = to_json_value(&caps).unwrap();
662 let serialized = serialized.as_object().unwrap();
663
664 let mut copy = Capabilities::new();
665 for (name, value) in serialized {
666 assert_eq!(caps.get(name).as_deref(), Some(value), "get({name})");
667 copy.set(name, value.clone()).unwrap();
668 }
669
670 assert!(copy.custom_capabilities.is_empty(), "{:?}", copy.custom_capabilities);
671 assert_eq!(to_json_value(©).unwrap(), to_json_value(&caps).unwrap());
672 }
673
674 #[test]
675 fn serialize_deserialize_capabilities() {
676 for mut caps in [Capabilities::new(), non_default_capabilities()] {
677 caps.set("com.example.custom", serde_json::json!({ "enabled": true })).unwrap();
678 let serialized = to_json_value(&caps).unwrap();
679 let deserialized: Capabilities =
680 serde_json::from_value(serialized.clone()).unwrap();
681
682 assert_eq!(to_json_value(&deserialized).unwrap(), serialized);
683 }
684 }
685
686 #[test]
687 fn deserialize_stable_and_unstable_profile_fields() {
688 let caps: Capabilities = serde_json::from_value(serde_json::json!({
689 "m.profile_fields": { "enabled": true },
690 "uk.tcpip.msc4133.profile_fields": { "enabled": false },
691 "m.room_versions": { "default": "11", "available": { "11": "stable" } },
692 }))
693 .unwrap();
694
695 assert!(caps.profile_fields.unwrap().enabled);
696 assert_eq!(caps.room_versions.default, RoomVersionId::V11);
697 assert!(caps.custom_capabilities.is_empty());
698 }
699
700 #[test]
701 fn deserialize_unstable_profile_fields() {
702 let caps: Capabilities = serde_json::from_value(serde_json::json!({
703 "uk.tcpip.msc4133.profile_fields": { "enabled": true },
704 }))
705 .unwrap();
706
707 assert!(caps.profile_fields.unwrap().enabled);
708 }
709 }
710}