Skip to main content

tuwunel_api/client/
profile.rs

1use axum::extract::State;
2use futures::StreamExt;
3use ruma::{
4	UserId,
5	api::client::profile::{
6		PropagateTo, delete_profile_field, get_profile,
7		get_profile_field::{self, v3::Response as GetProfileFieldResponse},
8		set_profile_field,
9	},
10	profile::{ProfileFieldName, ProfileFieldValue},
11};
12use tuwunel_core::{Err, Result, err, utils::BoolExt};
13use tuwunel_service::{Services, presence::Ping, profile::Propagation};
14
15use crate::{ClientIp, Ruma, client::utils::may_set_displayname};
16
17/// Resolve a `PropagateTo` request value against the server default.
18///
19/// MSC4466's `_Custom` variant is treated as the server default so
20/// unknown values do not silently change behavior.
21pub(super) fn resolve_propagation(propagate_to: &PropagateTo) -> Propagation {
22	match propagate_to {
23		| PropagateTo::Unchanged => Propagation::Unchanged,
24		| PropagateTo::None => Propagation::None,
25		| _ => Propagation::All,
26	}
27}
28
29/// # `GET /_matrix/client/v3/profile/{userId}`
30///
31/// Returns the displayname, avatar_url, blurhash, and tz of the user.
32///
33/// - If user is on another server and we do not have a local copy already,
34///   fetch profile over federation.
35pub(crate) async fn get_profile_route(
36	State(services): State<crate::State>,
37	body: Ruma<get_profile::v3::Request>,
38) -> Result<get_profile::v3::Response> {
39	shared_rooms_check(&services, &body, &body.user_id).await?;
40
41	if !services.globals.user_is_local(&body.user_id) {
42		services
43			.profile
44			.fetch_remote_profile(&body.user_id)
45			.await?;
46	}
47
48	if !services.users.exists(&body.user_id).await {
49		// Return 404 if this user doesn't exist and we couldn't fetch it over
50		// federation
51		return Err!(Request(NotFound("Profile was not found.")));
52	}
53
54	let response = services
55		.profile
56		.all_profile_keys(&body.user_id)
57		.collect()
58		.await;
59
60	Ok(response)
61}
62
63/// # `GET /_matrix/client/v3/profile/{userId}/{field}`
64///
65/// Gets the profile key-value field of a user, as per MSC4133.
66///
67/// - If user is on another server and we do not have a local copy already fetch
68///   `timezone` over federation
69/// - An unset `displayname` or `avatar_url` is a 200 with the field omitted, as
70///   before Matrix 1.16; other unset fields are a 404 per MSC4133.
71pub(crate) async fn get_profile_field_route(
72	State(services): State<crate::State>,
73	body: Ruma<get_profile_field::v3::Request>,
74) -> Result<GetProfileFieldResponse> {
75	shared_rooms_check(&services, &body, &body.user_id).await?;
76
77	if !services.globals.user_is_local(&body.user_id) {
78		services
79			.profile
80			.fetch_remote_profile(&body.user_id)
81			.await?;
82	}
83
84	if !services.users.exists(&body.user_id).await {
85		// Return 404 if this user doesn't exist and we couldn't fetch it over
86		// federation
87		return Err!(Request(NotFound("Profile was not found.")));
88	}
89
90	let legacy =
91		matches!(body.field, ProfileFieldName::AvatarUrl | ProfileFieldName::DisplayName);
92
93	let value = services
94		.profile
95		.profile_key(&body.user_id, &body.field)
96		.await
97		.map(Some)
98		.or_else(|error| {
99			(legacy && error.is_not_found())
100				.then_some(None)
101				.ok_or(error)
102		})?
103		.map(|value| ProfileFieldValue::new(body.field.as_str(), value))
104		.transpose()
105		.map_err(|_| {
106			err!(Database(error!(
107				user_id = %body.user_id,
108				key = %body.field,
109				"Invalid json in database profile value",
110			)))
111		})?;
112
113	Ok(GetProfileFieldResponse { value })
114}
115
116/// # `PUT /_matrix/client/v3/profile/{user_id}/{field}`
117///
118/// Updates the profile key-value field of a user. Stabilized as part of
119/// Matrix 1.16 (MSC4133); ruma's history block keeps the unstable
120/// `uk.tcpip.msc4133` path mounted for older clients.
121///
122/// This also handles the avatar_url and displayname being updated.
123pub(crate) async fn set_profile_field_route(
124	State(services): State<crate::State>,
125	ClientIp(client): ClientIp,
126	body: Ruma<set_profile_field::v3::Request>,
127) -> Result<set_profile_field::v3::Response> {
128	let sender_user = body.sender_user();
129	let field = body.value.field_name();
130
131	displayname_check(&services, &body, &field).await?;
132
133	if *sender_user != body.user_id
134		&& !body
135			.appservice_info
136			.as_ref()
137			.is_some_and(|registration| registration.is_user_match(&body.user_id))
138	{
139		return Err!(Request(Forbidden("You cannot update the profile of another user")));
140	}
141
142	let propagation = resolve_propagation(&body.propagate_to);
143
144	services
145		.profile
146		.set_profile_keys(
147			&body.user_id,
148			&[(field, Some(body.value.value().into_owned()))],
149			Some(propagation),
150		)
151		.await?;
152
153	// Presence update
154	let ping = Ping {
155		device_id: body.sender_device.as_deref(),
156		client_ip: Some(client),
157		appservice: body.appservice_info.as_ref(),
158		..Default::default()
159	};
160
161	services
162		.presence
163		.maybe_ping_presence(&body.user_id, ping)
164		.await?;
165
166	Ok(set_profile_field::v3::Response {})
167}
168
169/// # `DELETE /_matrix/client/v3/profile/{user_id}/{field}`
170///
171/// Deletes the profile key-value field of a user, as per MSC4133.
172///
173/// This also handles the avatar_url and displayname being updated.
174pub(crate) async fn delete_profile_field_route(
175	State(services): State<crate::State>,
176	ClientIp(client): ClientIp,
177	body: Ruma<delete_profile_field::v3::Request>,
178) -> Result<delete_profile_field::v3::Response> {
179	let sender_user = body.sender_user();
180
181	displayname_check(&services, &body, &body.field).await?;
182
183	if *sender_user != body.user_id
184		&& !body
185			.appservice_info
186			.as_ref()
187			.is_some_and(|registration| registration.is_user_match(&body.user_id))
188	{
189		return Err!(Request(Forbidden("You cannot update the profile of another user")));
190	}
191
192	let propagation = resolve_propagation(&body.propagate_to);
193
194	services
195		.profile
196		.set_profile_keys(&body.user_id, &[(body.field.clone(), None)], Some(propagation))
197		.await?;
198
199	// Presence update
200	let ping = Ping {
201		device_id: body.sender_device.as_deref(),
202		client_ip: Some(client),
203		appservice: body.appservice_info.as_ref(),
204		..Default::default()
205	};
206
207	services
208		.presence
209		.maybe_ping_presence(&body.user_id, ping)
210		.await?;
211
212	Ok(delete_profile_field::v3::Response {})
213}
214
215/// Refuses a profile read withheld by
216/// `limit_profile_requests_to_users_who_share_rooms`.
217///
218/// Appservices and a user reading their own profile are exempt. The refusal
219/// precedes the existence check, so it discloses nothing about the profile.
220async fn shared_rooms_check<T>(services: &Services, body: &Ruma<T>, user_id: &UserId) -> Result
221where
222	T: Sync,
223{
224	if services
225		.config
226		.limit_profile_requests_to_users_who_share_rooms
227		.is_false()
228		|| body.appservice_info.is_some()
229	{
230		return Ok(());
231	}
232
233	let visible = match body.sender_user.as_deref() {
234		| None => false,
235		| Some(sender_user) if sender_user == user_id => true,
236		| Some(sender_user) =>
237			services
238				.state_cache
239				.user_sees_user(sender_user, user_id)
240				.await,
241	};
242
243	visible
244		.into_option()
245		.ok_or_else(|| err!(Request(Forbidden("Profile isn't available."))))
246}
247
248/// Refuses a display name change withheld by `enable_set_displayname`.
249///
250/// Only display name writes are gated; `may_set_displayname` decides who is
251/// exempt.
252async fn displayname_check<T>(
253	services: &Services,
254	body: &Ruma<T>,
255	field: &ProfileFieldName,
256) -> Result
257where
258	T: Sync,
259{
260	let is_admin = || services.admin.user_is_admin(body.sender_user());
261
262	if *field != ProfileFieldName::DisplayName
263		|| may_set_displayname(services, body, is_admin).await
264	{
265		return Ok(());
266	}
267
268	Err!(Request(Forbidden("Setting display names has been disabled.")))
269}