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 UInt,
10 api::client::discovery::get_capabilities::v3::{
11 AccountModerationCapability, Capabilities, ChangePasswordCapability,
12 DelayedEventsCapability, ForgetForcedUponLeaveCapability, GetLoginTokenCapability,
13 ProfileFieldsCapability, Request, Response, RoomVersionsCapability,
14 ThirdPartyIdChangesCapability,
15 },
16 profile::ProfileFieldName,
17};
18use serde_json::json;
19use tuwunel_core::{Result, utils::BoolExt};
20use tuwunel_service::{Services, delayed_events::Limits};
21
22use crate::{Ruma, utils::may_set_profile_field};
23
24pub(crate) async fn get_capabilities_route(
29 State(services): State<crate::State>,
30 body: Ruma<Request>,
31) -> Result<Response> {
32 let account_moderation = services
35 .admin
36 .user_is_admin(body.sender_user())
37 .await;
38
39 let may_set = async |field| {
40 may_set_profile_field(&services, &body, &field, || ready(account_moderation)).await
41 };
42
43 let set_displayname = may_set(ProfileFieldName::DisplayName).await;
44 let set_avatar_url = may_set(ProfileFieldName::AvatarUrl).await;
45
46 capabilities(&services, set_displayname, set_avatar_url, account_moderation)
47 .map(Response::new)
48}
49
50#[expect(deprecated)]
51fn capabilities(
52 services: &Services,
53 set_displayname: bool,
54 set_avatar_url: bool,
55 account_moderation: bool,
56) -> Result<Capabilities> {
57 let available = services
58 .config
59 .supported_room_versions()
60 .collect();
61
62 let default = services
63 .server
64 .config
65 .rooms
66 .default_version
67 .clone();
68
69 let disallowed: Vec<_> = [
71 (set_displayname, ProfileFieldName::DisplayName),
72 (set_avatar_url, ProfileFieldName::AvatarUrl),
73 ]
74 .into_iter()
75 .filter_map(|(allowed, field)| allowed.is_false().then_some(field))
76 .collect();
77
78 let disallowed = disallowed
79 .is_empty()
80 .is_false()
81 .then_some(disallowed);
82
83 let mut capabilities = Capabilities::default(); capabilities.room_versions = RoomVersionsCapability { available, default };
86
87 capabilities.set_displayname = SetDisplayNameCapability::new(set_displayname);
89 capabilities.set_avatar_url = SetAvatarUrlCapability::new(set_avatar_url);
90
91 capabilities.thirdparty_id_changes =
93 ThirdPartyIdChangesCapability { enabled: services.sendmail.is_enabled() };
94
95 let enabled = services
96 .server
97 .config
98 .authentication
99 .login
100 .via_existing_session;
101
102 capabilities.get_login_token = GetLoginTokenCapability { enabled };
103
104 capabilities.profile_fields = ProfileFieldsCapability {
105 disallowed,
106 ..ProfileFieldsCapability::new(true)
107 }
108 .into();
109
110 let enabled = services
111 .server
112 .config
113 .authentication
114 .login
115 .with_password;
116
117 capabilities.change_password = ChangePasswordCapability { enabled };
118
119 capabilities.forget_forced_upon_leave =
120 ForgetForcedUponLeaveCapability::new(services.config.rooms.forget_forced_upon_leave);
121
122 capabilities.set(
123 "org.matrix.msc4267.forget_forced_upon_leave",
124 json!({"enabled": services.config.rooms.forget_forced_upon_leave}),
125 )?;
126
127 set_preview_url(&mut capabilities, services.media.url_preview_enabled())?;
129
130 if services.config.push.msc3664_related_event_match {
133 capabilities.set("im.nheko.msc3664.related_event_match", json!({"enabled": true}))?;
134 }
135
136 if account_moderation {
137 capabilities.account_moderation = AccountModerationCapability::new(true, true);
138 }
139
140 let delayed_events = services
143 .delayed_events
144 .limits()
145 .map(|limits| delayed_events_capability(DelayedEventsCapability::default(), limits));
146
147 capabilities.delayed_events = delayed_events;
148 capabilities.unstable_delayed_events = delayed_events;
149
150 Ok(capabilities)
151}
152
153fn set_preview_url(capabilities: &mut Capabilities, enabled: bool) -> Result {
154 let value = json!({"enabled": enabled});
155
156 capabilities.set("m.preview_url", value.clone())?;
157 capabilities.set("io.element.msc4452.preview_url", value)?;
158 Ok(())
159}
160
161fn delayed_events_capability(
163 mut capability: DelayedEventsCapability,
164 limits: Limits,
165) -> DelayedEventsCapability {
166 capability.max_delay_ms = Some(UInt::new_saturating(limits.max_delay_ms));
167 capability.max_scheduled = u64::try_from(limits.max_scheduled)
168 .ok()
169 .map(UInt::new_saturating);
170
171 capability
172}
173
174#[cfg(test)]
175mod tests {
176 use super::{Capabilities, json, set_preview_url};
177
178 #[test]
179 fn preview_url_capability_names_agree() {
180 for enabled in [false, true] {
181 let mut capabilities = Capabilities::default();
182
183 set_preview_url(&mut capabilities, enabled).expect("custom capabilities");
184 let stable = capabilities
185 .get("m.preview_url")
186 .expect("stable capability");
187
188 assert_eq!(stable.as_ref(), &json!({"enabled": enabled}));
189 assert_eq!(capabilities.get("io.element.msc4452.preview_url"), Some(stable));
190 }
191 }
192}