tuwunel_api/client/
capabilities.rs1use std::future::ready;
2
3use axum::extract::State;
4#[expect(deprecated)]
5use ruma::api::client::discovery::get_capabilities::v3::{
6 SetAvatarUrlCapability, SetDisplayNameCapability,
7};
8use ruma::{
9 api::client::discovery::get_capabilities::v3::{
10 AccountModerationCapability, Capabilities, ChangePasswordCapability,
11 ForgetForcedUponLeaveCapability, GetLoginTokenCapability, ProfileFieldsCapability,
12 Request, Response, RoomVersionsCapability, ThirdPartyIdChangesCapability,
13 },
14 profile::ProfileFieldName,
15};
16use serde_json::json;
17use tuwunel_core::{Result, utils::BoolExt};
18use tuwunel_service::Services;
19
20use crate::{Ruma, client::utils::may_set_displayname};
21
22pub(crate) async fn get_capabilities_route(
27 State(services): State<crate::State>,
28 body: Ruma<Request>,
29) -> Result<Response> {
30 let account_moderation = services
33 .admin
34 .user_is_admin(body.sender_user())
35 .await;
36
37 let set_displayname =
38 may_set_displayname(&services, &body, || ready(account_moderation)).await;
39
40 capabilities(&services, set_displayname, account_moderation).map(Response::new)
41}
42
43#[expect(deprecated)]
44fn capabilities(
45 services: &Services,
46 set_displayname: bool,
47 account_moderation: bool,
48) -> Result<Capabilities> {
49 let available = services
50 .config
51 .supported_room_versions()
52 .collect();
53
54 let default = services
55 .server
56 .config
57 .default_room_version
58 .clone();
59
60 let disallowed = set_displayname
62 .is_false()
63 .then(|| vec![ProfileFieldName::DisplayName]);
64
65 let mut capabilities = Capabilities::default(); capabilities.room_versions = RoomVersionsCapability { available, default };
68
69 capabilities.set_displayname = SetDisplayNameCapability::new(set_displayname);
71 capabilities.set_avatar_url = SetAvatarUrlCapability::new(true);
72
73 capabilities.thirdparty_id_changes =
75 ThirdPartyIdChangesCapability { enabled: services.sendmail.is_enabled() };
76
77 capabilities.get_login_token = GetLoginTokenCapability {
78 enabled: services.server.config.login_via_existing_session,
79 };
80
81 capabilities.profile_fields = ProfileFieldsCapability {
82 disallowed,
83 ..ProfileFieldsCapability::new(true)
84 }
85 .into();
86
87 capabilities.change_password = ChangePasswordCapability {
88 enabled: services.server.config.login_with_password,
89 };
90
91 capabilities.forget_forced_upon_leave =
92 ForgetForcedUponLeaveCapability::new(services.config.forget_forced_upon_leave);
93
94 capabilities.set(
95 "org.matrix.msc4267.forget_forced_upon_leave",
96 json!({"enabled": services.config.forget_forced_upon_leave}),
97 )?;
98
99 capabilities.set(
101 "io.element.msc4452.preview_url",
102 json!({"enabled": preview_url_enabled(services)}),
103 )?;
104
105 if services.config.msc3664_related_event_match {
108 capabilities.set("im.nheko.msc3664.related_event_match", json!({"enabled": true}))?;
109 }
110
111 if account_moderation {
112 capabilities.account_moderation = AccountModerationCapability::new(true, true);
113 }
114
115 Ok(capabilities)
116}
117
118fn preview_url_enabled(services: &Services) -> bool {
119 let config = &services.config;
120
121 !config
122 .url_preview_domain_contains_allowlist
123 .is_empty()
124 || !config
125 .url_preview_domain_explicit_allowlist
126 .is_empty()
127 || !config
128 .url_preview_url_contains_allowlist
129 .is_empty()
130}