tuwunel_api/client/
profile.rs1use 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
17pub(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
29pub(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 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
63pub(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 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
116pub(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 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
169pub(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 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
215async 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
248async 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}