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