1use std::collections::BTreeMap;
2
3use axum::extract::State;
4use futures::{
5 StreamExt,
6 future::{join, join4},
7};
8use ruma::{
9 MxcUri, OwnedRoomId,
10 api::{
11 client::profile::{
12 PropagateTo, get_avatar_url, get_display_name, get_profile, set_avatar_url,
13 set_display_name,
14 },
15 federation::query::get_profile_information,
16 },
17 presence::PresenceState,
18};
19use serde_json::Value as JsonValue;
20use tuwunel_core::{Err, Result, utils::future::TryExtExt};
21use tuwunel_service::users::{Propagation, propagation_default};
22
23use crate::{ClientIp, Ruma};
24
25pub(super) type ProfileResponse = get_profile_information::v1::Response;
26
27pub(super) fn profile_str<'a>(resp: &'a ProfileResponse, field: &str) -> Option<&'a str> {
31 resp.get(field).and_then(JsonValue::as_str)
32}
33
34pub(super) fn profile_mxc<'a>(resp: &'a ProfileResponse, field: &str) -> Option<&'a MxcUri> {
35 profile_str(resp, field).map(<&MxcUri>::from)
36}
37
38pub(super) fn resolve_propagation(
43 propagate_to: &PropagateTo,
44 server_default: Propagation,
45) -> Propagation {
46 match propagate_to {
47 | PropagateTo::All => Propagation::All,
48 | PropagateTo::Unchanged => Propagation::Unchanged,
49 | PropagateTo::None => Propagation::None,
50 | _ => server_default,
51 }
52}
53
54pub(crate) async fn set_displayname_route(
60 State(services): State<crate::State>,
61 ClientIp(client): ClientIp,
62 body: Ruma<set_display_name::v3::Request>,
63) -> Result<set_display_name::v3::Response> {
64 let sender_user = body.sender_user();
65
66 if *sender_user != body.user_id && body.appservice_info.is_none() {
67 return Err!(Request(Forbidden("You cannot update the profile of another user")));
68 }
69
70 let all_joined_rooms: Vec<OwnedRoomId> = services
71 .state_cache
72 .rooms_joined(&body.user_id)
73 .map(ToOwned::to_owned)
74 .collect()
75 .await;
76
77 let propagation = resolve_propagation(
78 &body.propagate_to,
79 propagation_default(
80 services
81 .server
82 .config
83 .preserve_room_profile_overrides,
84 ),
85 );
86
87 services
88 .users
89 .update_displayname(
90 &body.user_id,
91 body.displayname.as_deref(),
92 &all_joined_rooms,
93 propagation,
94 )
95 .await;
96
97 services
99 .presence
100 .maybe_ping_presence(
101 &body.user_id,
102 body.sender_device.as_deref(),
103 Some(client),
104 &PresenceState::Online,
105 )
106 .await?;
107
108 Ok(set_display_name::v3::Response {})
109}
110
111pub(crate) async fn get_displayname_route(
118 State(services): State<crate::State>,
119 body: Ruma<get_display_name::v3::Request>,
120) -> Result<get_display_name::v3::Response> {
121 if !services.globals.user_is_local(&body.user_id) {
122 if let Ok(response) = services
124 .federation
125 .execute(body.user_id.server_name(), get_profile_information::v1::Request {
126 user_id: body.user_id.clone(),
127 field: None, })
129 .await
130 {
131 if !services.users.exists(&body.user_id).await {
132 services
133 .users
134 .create(&body.user_id, None, None)
135 .await?;
136 }
137
138 let displayname = profile_str(&response, "displayname");
139 services
140 .users
141 .set_displayname(&body.user_id, displayname);
142 services
143 .users
144 .set_avatar_url(&body.user_id, profile_mxc(&response, "avatar_url"));
145 services
146 .users
147 .set_blurhash(&body.user_id, profile_str(&response, "blurhash"));
148
149 return Ok(get_display_name::v3::Response {
150 displayname: displayname.map(str::to_owned),
151 });
152 }
153 }
154
155 if !services.users.exists(&body.user_id).await {
156 return Err!(Request(NotFound("Profile was not found.")));
159 }
160
161 Ok(get_display_name::v3::Response {
162 displayname: services
163 .users
164 .displayname(&body.user_id)
165 .await
166 .ok(),
167 })
168}
169
170pub(crate) async fn set_avatar_url_route(
176 State(services): State<crate::State>,
177 ClientIp(client): ClientIp,
178 body: Ruma<set_avatar_url::v3::Request>,
179) -> Result<set_avatar_url::v3::Response> {
180 let sender_user = body.sender_user();
181
182 if *sender_user != body.user_id && body.appservice_info.is_none() {
183 return Err!(Request(Forbidden("You cannot update the profile of another user")));
184 }
185
186 let all_joined_rooms: Vec<OwnedRoomId> = services
187 .state_cache
188 .rooms_joined(&body.user_id)
189 .map(ToOwned::to_owned)
190 .collect()
191 .await;
192
193 let propagation = resolve_propagation(
194 &body.propagate_to,
195 propagation_default(
196 services
197 .server
198 .config
199 .preserve_room_profile_overrides,
200 ),
201 );
202
203 services
204 .users
205 .update_avatar_url(
206 &body.user_id,
207 body.avatar_url.as_deref(),
208 body.blurhash.as_deref(),
209 &all_joined_rooms,
210 propagation,
211 )
212 .await;
213
214 services
216 .presence
217 .maybe_ping_presence(
218 &body.user_id,
219 body.sender_device.as_deref(),
220 Some(client),
221 &PresenceState::Online,
222 )
223 .await
224 .ok();
225
226 Ok(set_avatar_url::v3::Response {})
227}
228
229pub(crate) async fn get_avatar_url_route(
236 State(services): State<crate::State>,
237 body: Ruma<get_avatar_url::v3::Request>,
238) -> Result<get_avatar_url::v3::Response> {
239 if !services.globals.user_is_local(&body.user_id) {
240 if let Ok(response) = services
242 .federation
243 .execute(body.user_id.server_name(), get_profile_information::v1::Request {
244 user_id: body.user_id.clone(),
245 field: None, })
247 .await
248 {
249 if !services.users.exists(&body.user_id).await {
250 services
251 .users
252 .create(&body.user_id, None, None)
253 .await?;
254 }
255
256 let avatar_url = profile_mxc(&response, "avatar_url");
257 let blurhash = profile_str(&response, "blurhash");
258 services
259 .users
260 .set_displayname(&body.user_id, profile_str(&response, "displayname"));
261 services
262 .users
263 .set_avatar_url(&body.user_id, avatar_url);
264 services
265 .users
266 .set_blurhash(&body.user_id, blurhash);
267
268 return Ok(get_avatar_url::v3::Response {
269 avatar_url: avatar_url.map(ToOwned::to_owned),
270 blurhash: blurhash.map(str::to_owned),
271 });
272 }
273 }
274
275 if !services.users.exists(&body.user_id).await {
276 return Err!(Request(NotFound("Profile was not found.")));
279 }
280
281 let (avatar_url, blurhash) = join(
282 services.users.avatar_url(&body.user_id).ok(),
283 services.users.blurhash(&body.user_id).ok(),
284 )
285 .await;
286
287 Ok(get_avatar_url::v3::Response { avatar_url, blurhash })
288}
289
290pub(crate) async fn get_profile_route(
297 State(services): State<crate::State>,
298 body: Ruma<get_profile::v3::Request>,
299) -> Result<get_profile::v3::Response> {
300 const CANONICAL_FIELDS: &[&str] = &["avatar_url", "blurhash", "displayname", "m.tz"];
301
302 if !services.globals.user_is_local(&body.user_id) {
303 if let Ok(response) = services
305 .federation
306 .execute(body.user_id.server_name(), get_profile_information::v1::Request {
307 user_id: body.user_id.clone(),
308 field: None,
309 })
310 .await
311 {
312 if !services.users.exists(&body.user_id).await {
313 services
314 .users
315 .create(&body.user_id, None, None)
316 .await?;
317 }
318
319 services
320 .users
321 .set_displayname(&body.user_id, profile_str(&response, "displayname"));
322 services
323 .users
324 .set_avatar_url(&body.user_id, profile_mxc(&response, "avatar_url"));
325 services
326 .users
327 .set_blurhash(&body.user_id, profile_str(&response, "blurhash"));
328 services
329 .users
330 .set_timezone(&body.user_id, profile_str(&response, "m.tz"));
331
332 for (key, value) in response.iter() {
333 if CANONICAL_FIELDS.contains(&key.as_str()) {
334 continue;
335 }
336 services
337 .users
338 .set_profile_key(&body.user_id, key, Some(value));
339 }
340
341 return Ok(response
342 .iter()
343 .map(|(key, val)| (key.clone(), val.clone()))
344 .collect::<get_profile::v3::Response>());
345 }
346 }
347
348 if !services.users.exists(&body.user_id).await {
349 return Err!(Request(NotFound("Profile was not found.")));
352 }
353
354 let mut custom_profile_fields: BTreeMap<String, _> = services
355 .users
356 .all_profile_keys(&body.user_id)
357 .collect()
358 .await;
359
360 custom_profile_fields.remove("us.cloke.msc4175.tz");
362 custom_profile_fields.remove("m.tz");
363
364 let (avatar_url, blurhash, displayname, tz) = join4(
365 services.users.avatar_url(&body.user_id).ok(),
366 services.users.blurhash(&body.user_id).ok(),
367 services.users.displayname(&body.user_id).ok(),
368 services.users.timezone(&body.user_id).ok(),
369 )
370 .await;
371
372 let canonical_fields = [
373 ("avatar_url", avatar_url.map(Into::into)),
374 ("blurhash", blurhash),
375 ("displayname", displayname),
376 ("m.tz", tz),
377 ];
378
379 Ok(canonical_fields
380 .into_iter()
381 .map(|(key, val)| (key.to_owned(), val))
382 .filter_map(|(key, val)| {
383 val.map(serde_json::to_value)
384 .transpose()
385 .ok()
386 .flatten()
387 .map(|val| (key, val))
388 })
389 .chain(
390 custom_profile_fields
391 .into_iter()
392 .filter_map(|(key, val)| {
393 serde_json::to_value(val.json())
394 .map(|val| (key, val))
395 .ok()
396 }),
397 )
398 .collect())
399}