Skip to main content

tuwunel_api/client/
capabilities.rs

1use 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
22/// # `GET /_matrix/client/v3/capabilities`
23///
24/// Get information on the supported feature set and other relevant capabilities
25/// of this server.
26pub(crate) async fn get_capabilities_route(
27	State(services): State<crate::State>,
28	body: Ruma<Request>,
29) -> Result<Response> {
30	// MSC4323: advertise admin moderation only to admins; absence implies
31	// neither suspend nor lock is available to the caller.
32	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	// Matrix 1.16 clients read the display name policy from m.profile_fields.
61	let disallowed = set_displayname
62		.is_false()
63		.then(|| vec![ProfileFieldName::DisplayName]);
64
65	let mut capabilities = Capabilities::default(); // non_exhaustive: no struct literal
66
67	capabilities.room_versions = RoomVersionsCapability { available, default };
68
69	// MSC3283: deprecated displayname/avatar capabilities for pre-1.16 clients.
70	capabilities.set_displayname = SetDisplayNameCapability::new(set_displayname);
71	capabilities.set_avatar_url = SetAvatarUrlCapability::new(true);
72
73	// 3PID add/remove is available only when the email subsystem can send.
74	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	// MSC4452: enabled mirrors the per-URL gate; empty allowlists 403 every URL.
100	capabilities.set(
101		"io.element.msc4452.preview_url",
102		json!({"enabled": preview_url_enabled(services)}),
103	)?;
104
105	// MSC3664: absent rather than present-and-disabled, since a client testing
106	// for the key rather than its value would otherwise read support.
107	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}