Skip to main content

tuwunel_api/client/
unstable.rs

1use axum::extract::State;
2use futures::StreamExt;
3use ruma::{
4	OwnedRoomId,
5	api::{
6		client::{
7			membership::mutual_rooms,
8			profile::{delete_profile_field, get_profile_field, set_profile_field},
9		},
10		federation,
11	},
12	presence::PresenceState,
13	profile::{ProfileFieldName, ProfileFieldValue},
14};
15use tuwunel_core::{Err, Result, err};
16use tuwunel_service::users::propagation_default;
17
18use super::profile::{profile_mxc, profile_str, resolve_propagation};
19use crate::{ClientIp, Ruma};
20
21/// # `GET /_matrix/client/unstable/uk.half-shot.msc2666/user/mutual_rooms`
22///
23/// Gets all the rooms the sender shares with the specified user.
24///
25/// TODO: Implement pagination, currently this just returns everything
26///
27/// An implementation of [MSC2666](https://github.com/matrix-org/matrix-spec-proposals/pull/2666)
28#[tracing::instrument(skip_all, fields(%client), name = "mutual_rooms")]
29pub(crate) async fn get_mutual_rooms_route(
30	State(services): State<crate::State>,
31	ClientIp(client): ClientIp,
32	body: Ruma<mutual_rooms::unstable::Request>,
33) -> Result<mutual_rooms::unstable::Response> {
34	let sender_user = body.sender_user();
35
36	if sender_user == body.user_id {
37		return Err!(Request(Unknown("You cannot request rooms in common with yourself.")));
38	}
39
40	if !services.users.exists(&body.user_id).await {
41		return Ok(mutual_rooms::unstable::Response { joined: vec![], next_batch_token: None });
42	}
43
44	let mutual_rooms: Vec<OwnedRoomId> = services
45		.state_cache
46		.get_shared_rooms(sender_user, &body.user_id)
47		.map(ToOwned::to_owned)
48		.collect()
49		.await;
50
51	Ok(mutual_rooms::unstable::Response {
52		joined: mutual_rooms,
53		next_batch_token: None,
54	})
55}
56
57/// # `PUT /_matrix/client/v3/profile/{user_id}/{field}`
58///
59/// Updates the profile key-value field of a user. Stabilized as part of
60/// Matrix 1.16 (MSC4133); ruma's history block keeps the unstable
61/// `uk.tcpip.msc4133` path mounted for older clients.
62///
63/// This also handles the avatar_url and displayname being updated.
64pub(crate) async fn set_profile_field_route(
65	State(services): State<crate::State>,
66	ClientIp(client): ClientIp,
67	body: Ruma<set_profile_field::v3::Request>,
68) -> Result<set_profile_field::v3::Response> {
69	let sender_user = body.sender_user();
70
71	if *sender_user != body.user_id && body.appservice_info.is_none() {
72		return Err!(Request(Forbidden("You cannot update the profile of another user")));
73	}
74
75	// MSC3823: displayname/avatar are forbidden during suspension; custom
76	// MSC4133 fields fall through.
77	if matches!(body.value, ProfileFieldValue::DisplayName(_) | ProfileFieldValue::AvatarUrl(_))
78		&& services.users.is_suspended(sender_user).await
79	{
80		return Err!(Request(UserSuspended("Account is suspended.")));
81	}
82
83	if body.value.field_name().as_str().len() > 128 {
84		return Err!(Request(BadJson("Key names cannot be longer than 128 bytes")));
85	}
86
87	let propagation = resolve_propagation(
88		&body.propagate_to,
89		propagation_default(
90			services
91				.server
92				.config
93				.preserve_room_profile_overrides,
94		),
95	);
96
97	match &body.value {
98		| ProfileFieldValue::DisplayName(displayname) => {
99			let all_joined_rooms: Vec<OwnedRoomId> = services
100				.state_cache
101				.rooms_joined(&body.user_id)
102				.map(Into::into)
103				.collect()
104				.await;
105
106			services
107				.users
108				.update_displayname(
109					&body.user_id,
110					Some(displayname),
111					&all_joined_rooms,
112					propagation,
113				)
114				.await;
115		},
116		| ProfileFieldValue::AvatarUrl(avatar_url) => {
117			let all_joined_rooms: Vec<OwnedRoomId> = services
118				.state_cache
119				.rooms_joined(&body.user_id)
120				.map(Into::into)
121				.collect()
122				.await;
123
124			services
125				.users
126				.update_avatar_url(
127					&body.user_id,
128					Some(avatar_url),
129					None,
130					&all_joined_rooms,
131					propagation,
132				)
133				.await;
134		},
135		| _ => {
136			services.users.set_profile_key(
137				&body.user_id,
138				body.value.field_name().as_str(),
139				Some(&body.value.value()),
140			);
141		},
142	}
143
144	// Presence update
145	services
146		.presence
147		.maybe_ping_presence(
148			&body.user_id,
149			body.sender_device.as_deref(),
150			Some(client),
151			&PresenceState::Online,
152		)
153		.await?;
154
155	Ok(set_profile_field::v3::Response {})
156}
157
158/// # `DELETE /_matrix/client/unstable/uk.tcpip.msc4133/profile/{user_id}/{field}`
159///
160/// Deletes the profile key-value field of a user, as per MSC4133.
161///
162/// This also handles the avatar_url and displayname being updated.
163pub(crate) async fn delete_profile_field_route(
164	State(services): State<crate::State>,
165	ClientIp(client): ClientIp,
166	body: Ruma<delete_profile_field::v3::Request>,
167) -> Result<delete_profile_field::v3::Response> {
168	let sender_user = body.sender_user();
169
170	if *sender_user != body.user_id && body.appservice_info.is_none() {
171		return Err!(Request(Forbidden("You cannot update the profile of another user")));
172	}
173
174	// MSC3823: displayname/avatar are forbidden during suspension; custom
175	// MSC4133 fields fall through.
176	if matches!(body.field, ProfileFieldName::DisplayName | ProfileFieldName::AvatarUrl)
177		&& services.users.is_suspended(sender_user).await
178	{
179		return Err!(Request(UserSuspended("Account is suspended.")));
180	}
181
182	let propagation = resolve_propagation(
183		&body.propagate_to,
184		propagation_default(
185			services
186				.server
187				.config
188				.preserve_room_profile_overrides,
189		),
190	);
191
192	match body.field {
193		| ProfileFieldName::DisplayName => {
194			let all_joined_rooms: Vec<OwnedRoomId> = services
195				.state_cache
196				.rooms_joined(&body.user_id)
197				.map(Into::into)
198				.collect()
199				.await;
200
201			services
202				.users
203				.update_displayname(&body.user_id, None, &all_joined_rooms, propagation)
204				.await;
205		},
206		| ProfileFieldName::AvatarUrl => {
207			let all_joined_rooms: Vec<OwnedRoomId> = services
208				.state_cache
209				.rooms_joined(&body.user_id)
210				.map(Into::into)
211				.collect()
212				.await;
213
214			services
215				.users
216				.update_avatar_url(&body.user_id, None, None, &all_joined_rooms, propagation)
217				.await;
218		},
219		| _ => {
220			services
221				.users
222				.set_profile_key(&body.user_id, body.field.as_str(), None);
223		},
224	}
225
226	// Presence update
227	services
228		.presence
229		.maybe_ping_presence(
230			&body.user_id,
231			body.sender_device.as_deref(),
232			Some(client),
233			&PresenceState::Online,
234		)
235		.await?;
236
237	Ok(delete_profile_field::v3::Response {})
238}
239
240/// # `GET /_matrix/client/v3/profile/{userId}/{field}`
241///
242/// Gets the profile key-value field of a user, as per MSC4133.
243///
244/// - If user is on another server and we do not have a local copy already fetch
245///   `timezone` over federation
246pub(crate) async fn get_profile_field_route(
247	State(services): State<crate::State>,
248	body: Ruma<get_profile_field::v3::Request>,
249) -> Result<get_profile_field::v3::Response> {
250	if !services.globals.user_is_local(&body.user_id) {
251		// Create and update our local copy of the user
252		if let Ok(response) = services
253			.federation
254			.execute(
255				body.user_id.server_name(),
256				federation::query::get_profile_information::v1::Request {
257					user_id: body.user_id.clone(),
258					field: None, // we want the full user's profile to update locally as well
259				},
260			)
261			.await
262		{
263			if !services.users.exists(&body.user_id).await {
264				services
265					.users
266					.create(&body.user_id, None, None)
267					.await?;
268			}
269
270			services
271				.users
272				.set_displayname(&body.user_id, profile_str(&response, "displayname"));
273
274			services
275				.users
276				.set_avatar_url(&body.user_id, profile_mxc(&response, "avatar_url"));
277
278			services
279				.users
280				.set_blurhash(&body.user_id, profile_str(&response, "blurhash"));
281
282			services
283				.users
284				.set_timezone(&body.user_id, profile_str(&response, "m.tz"));
285
286			let value = response.get(body.field.as_str()).ok_or_else(|| {
287				err!(Request(NotFound("The requested profile key does not exist.")))
288			})?;
289
290			services
291				.users
292				.set_profile_key(&body.user_id, body.field.as_str(), Some(value));
293
294			let profile_key_value = ProfileFieldValue::new(body.field.as_str(), value.clone())?;
295
296			return Ok(get_profile_field::v3::Response { value: Some(profile_key_value) });
297		}
298	}
299
300	if !services.users.exists(&body.user_id).await {
301		// Return 404 if this user doesn't exist and we couldn't fetch it over
302		// federation
303		return Err!(Request(NotFound("Profile was not found.")));
304	}
305
306	let value = services
307		.users
308		.profile_key(&body.user_id, body.field.as_str())
309		.await
310		.and_then(|val| serde_json::to_value(val.json()).map_err(Into::into))
311		.map_err(|_| err!(Request(NotFound("The requested profile key does not exist."))))?;
312
313	let profile_key_value = ProfileFieldValue::new(body.field.as_str(), value)?;
314
315	Ok(get_profile_field::v3::Response { value: Some(profile_key_value) })
316}