Skip to main content

tuwunel_api/client/
profile.rs

1use axum::extract::State;
2use futures::StreamExt;
3use ruma::{
4	api::client::profile::{
5		PropagateTo, delete_profile_field, get_profile, get_profile_field, set_profile_field,
6	},
7	presence::PresenceState,
8	profile::ProfileFieldValue,
9};
10use tuwunel_core::{Err, Result, err};
11use tuwunel_service::profile::Propagation;
12
13use crate::{ClientIp, Ruma};
14
15/// Resolve a `PropagateTo` request value against the server default.
16///
17/// MSC4466's `_Custom` variant is treated as the server default so
18/// unknown values do not silently change behavior.
19pub(super) fn resolve_propagation(propagate_to: &PropagateTo) -> Propagation {
20	match propagate_to {
21		| PropagateTo::Unchanged => Propagation::Unchanged,
22		| PropagateTo::None => Propagation::None,
23		| _ => Propagation::All,
24	}
25}
26
27/// # `GET /_matrix/client/v3/profile/{userId}`
28///
29/// Returns the displayname, avatar_url, blurhash, and tz of the user.
30///
31/// - If user is on another server and we do not have a local copy already,
32///   fetch profile over federation.
33pub(crate) async fn get_profile_route(
34	State(services): State<crate::State>,
35	body: Ruma<get_profile::v3::Request>,
36) -> Result<get_profile::v3::Response> {
37	if !services.globals.user_is_local(&body.user_id) {
38		services
39			.profile
40			.fetch_remote_profile(&body.user_id)
41			.await?;
42	}
43
44	if !services.users.exists(&body.user_id).await {
45		// Return 404 if this user doesn't exist and we couldn't fetch it over
46		// federation
47		return Err!(Request(NotFound("Profile was not found.")));
48	}
49
50	let response = services
51		.profile
52		.all_profile_keys(&body.user_id)
53		.collect()
54		.await;
55
56	Ok(response)
57}
58
59/// # `GET /_matrix/client/v3/profile/{userId}/{field}`
60///
61/// Gets the profile key-value field of a user, as per MSC4133.
62///
63/// - If user is on another server and we do not have a local copy already fetch
64///   `timezone` over federation
65pub(crate) async fn get_profile_field_route(
66	State(services): State<crate::State>,
67	body: Ruma<get_profile_field::v3::Request>,
68) -> Result<get_profile_field::v3::Response> {
69	if !services.globals.user_is_local(&body.user_id) {
70		services
71			.profile
72			.fetch_remote_profile(&body.user_id)
73			.await?;
74	}
75
76	if !services.users.exists(&body.user_id).await {
77		// Return 404 if this user doesn't exist and we couldn't fetch it over
78		// federation
79		return Err!(Request(NotFound("Profile was not found.")));
80	}
81
82	let value = services
83		.profile
84		.profile_key(&body.user_id, &body.field)
85		.await?;
86
87	let profile_value = ProfileFieldValue::new(body.field.as_str(), value).map_err(|_| {
88		err!(Database(
89			error!(user_id = %body.user_id, key = %body.field, "Invalid json in database profile value")
90		))
91	})?;
92
93	Ok(get_profile_field::v3::Response { value: Some(profile_value) })
94}
95
96/// # `PUT /_matrix/client/v3/profile/{user_id}/{field}`
97///
98/// Updates the profile key-value field of a user. Stabilized as part of
99/// Matrix 1.16 (MSC4133); ruma's history block keeps the unstable
100/// `uk.tcpip.msc4133` path mounted for older clients.
101///
102/// This also handles the avatar_url and displayname being updated.
103pub(crate) async fn set_profile_field_route(
104	State(services): State<crate::State>,
105	ClientIp(client): ClientIp,
106	body: Ruma<set_profile_field::v3::Request>,
107) -> Result<set_profile_field::v3::Response> {
108	let sender_user = body.sender_user();
109
110	if *sender_user != body.user_id
111		&& !body
112			.appservice_info
113			.as_ref()
114			.is_some_and(|registration| registration.is_user_match(&body.user_id))
115	{
116		return Err!(Request(Forbidden("You cannot update the profile of another user")));
117	}
118
119	let propagation = resolve_propagation(&body.propagate_to);
120
121	services
122		.profile
123		.set_profile_keys(
124			&body.user_id,
125			&[(body.value.field_name(), Some(body.value.value().into_owned()))],
126			Some(propagation),
127		)
128		.await?;
129
130	// Presence update
131	services
132		.presence
133		.maybe_ping_presence(
134			&body.user_id,
135			body.sender_device.as_deref(),
136			Some(client),
137			&PresenceState::Online,
138		)
139		.await?;
140
141	Ok(set_profile_field::v3::Response {})
142}
143
144/// # `DELETE /_matrix/client/v3/profile/{user_id}/{field}`
145///
146/// Deletes the profile key-value field of a user, as per MSC4133.
147///
148/// This also handles the avatar_url and displayname being updated.
149pub(crate) async fn delete_profile_field_route(
150	State(services): State<crate::State>,
151	ClientIp(client): ClientIp,
152	body: Ruma<delete_profile_field::v3::Request>,
153) -> Result<delete_profile_field::v3::Response> {
154	let sender_user = body.sender_user();
155
156	if *sender_user != body.user_id
157		&& !body
158			.appservice_info
159			.as_ref()
160			.is_some_and(|registration| registration.is_user_match(&body.user_id))
161	{
162		return Err!(Request(Forbidden("You cannot update the profile of another user")));
163	}
164
165	let propagation = resolve_propagation(&body.propagate_to);
166
167	services
168		.profile
169		.set_profile_keys(&body.user_id, &[(body.field.clone(), None)], Some(propagation))
170		.await?;
171
172	// Presence update
173	services
174		.presence
175		.maybe_ping_presence(
176			&body.user_id,
177			body.sender_device.as_deref(),
178			Some(client),
179			&PresenceState::Online,
180		)
181		.await?;
182
183	Ok(delete_profile_field::v3::Response {})
184}