Files
continuwuity/src/api/client/profile.rs
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

218 lines
5.5 KiB
Rust
Raw Normal View History

use std::collections::BTreeMap;
2024-07-16 08:05:25 +00:00
use axum::extract::State;
use conduwuit::{Err, Result};
use conduwuit_service::Services;
2020-07-30 18:14:47 +02:00
use ruma::{
UserId,
api::{
client::profile::{
2026-05-13 13:40:16 -04:00
PropagateTo, delete_profile_field, get_profile, get_profile_field, set_profile_field,
},
federation,
2020-07-30 18:14:47 +02:00
},
assign,
profile::{ProfileFieldName, ProfileFieldValue},
2020-07-30 18:14:47 +02:00
};
use serde_json::Value;
use service::users::ProfileFieldChange;
2024-07-22 07:43:51 +00:00
use crate::Ruma;
2020-07-30 18:14:47 +02:00
/// # `GET /_matrix/client/v3/profile/{userId}`
2021-08-31 19:14:37 +02:00
///
/// Returns the user's profile information.
2021-08-31 19:14:37 +02:00
///
/// - If user is on another server and we do not have a local copy already,
2024-06-16 00:36:49 +00:00
/// fetch profile over federation.
2024-07-16 08:05:25 +00:00
pub(crate) async fn get_profile_route(
State(services): State<crate::State>,
body: Ruma<get_profile::v3::Request>,
2024-07-16 08:05:25 +00:00
) -> Result<get_profile::v3::Response> {
let Some(profile) = fetch_full_profile(&services, &body.user_id).await else {
return Err!(Request(NotFound("This user's profile could not be fetched.")));
};
2026-04-13 18:31:16 -04:00
Ok(get_profile::v3::Response::from_iter(profile))
}
pub(crate) async fn get_profile_field_route(
State(services): State<crate::State>,
body: Ruma<get_profile_field::v3::Request>,
) -> Result<get_profile_field::v3::Response> {
let value = fetch_profile_field(&services, &body.user_id, body.field.clone()).await?;
Ok(assign!(get_profile_field::v3::Response::default(), { value }))
}
pub(crate) async fn set_profile_field_route(
State(services): State<crate::State>,
body: Ruma<set_profile_field::v3::Request>,
) -> Result<set_profile_field::v3::Response> {
if body.user_id != body.identity.expect_sender_user()?
&& !(body.identity.is_appservice()
|| services
.admin
.user_is_admin(body.identity.expect_sender_user()?)
.await)
{
return Err!(Request(Forbidden("You may not change other users' profile data.")));
}
2024-07-22 07:43:51 +00:00
if !services.globals.user_is_local(&body.user_id) {
return Err!(Request(InvalidParam("You may not change a remote user's profile data.")));
}
if services
.oidc
.restricted_profile_fields()
.contains(&body.value.field_name())
{
return Err!(Request(Forbidden(
"This profile field is controlled by your identity provider."
)));
}
services
.users
.set_profile_field(
&body.user_id,
ProfileFieldChange::Set(body.value.clone()),
body.propagate_to.clone(),
)
.await?;
Ok(set_profile_field::v3::Response::new())
}
pub(crate) async fn delete_profile_field_route(
State(services): State<crate::State>,
body: Ruma<delete_profile_field::v3::Request>,
) -> Result<delete_profile_field::v3::Response> {
if body.user_id != body.identity.expect_sender_user()?
&& !(body.identity.is_appservice()
|| services
.admin
.user_is_admin(body.identity.expect_sender_user()?)
.await)
{
return Err!(Request(Forbidden("You may not change other users' profile data.")));
}
if !services.globals.user_is_local(&body.user_id) {
return Err!(Request(InvalidParam("You may not change a remote user's profile data.")));
}
if services
.oidc
.restricted_profile_fields()
.contains(&body.field)
{
return Err!(Request(Forbidden(
"This profile field is controlled by your identity provider."
)));
}
services
.users
.set_profile_field(
&body.user_id,
ProfileFieldChange::Delete(body.field.clone()),
body.propagate_to.clone(),
)
.await?;
Ok(delete_profile_field::v3::Response::new())
}
async fn fetch_full_profile(
services: &Services,
user_id: &UserId,
) -> Option<BTreeMap<String, Value>> {
// If the user exists locally, fetch their local profile
if services.users.status(user_id).await.is_found() {
return Some(services.users.get_local_profile(user_id).await);
}
// Otherwise ask their homeserver
let Ok(response) = services
.sending
.send_federation_request(
user_id.server_name(),
federation::query::get_profile_information::v1::Request::new(user_id.to_owned()),
)
.await
else {
return None;
};
// Update our local copies of their profile fields
services.users.clear_profile(user_id).await;
for (field, value) in response.iter() {
2026-04-13 18:31:16 -04:00
let Ok(value) = ProfileFieldValue::new(field, value.to_owned()) else {
// Skip malformed fields
continue;
};
let _ = services
.users
.set_profile_field(user_id, ProfileFieldChange::Set(value), PropagateTo::None)
.await;
}
2026-04-13 18:31:16 -04:00
Some(BTreeMap::from_iter(response))
}
async fn fetch_profile_field(
services: &Services,
user_id: &UserId,
field: ProfileFieldName,
) -> Result<Option<ProfileFieldValue>> {
// If the user exists locally, fetch their local profile field
if services.globals.user_is_local(user_id) {
return Ok(services.users.get_local_profile_field(user_id, field).await);
}
// Otherwise ask their homeserver
let Ok(response) = services
.sending
.send_federation_request(
user_id.server_name(),
assign!(federation::query::get_profile_information::v1::Request::new(user_id.to_owned()), {
field: Some(field.clone())
}),
)
.await
else {
return Err!(Request(NotFound(
"User's homeserver could not provide this profile field."
)));
};
if let Some(value) = response.get(field.as_str()).map(ToOwned::to_owned) {
if let Ok(value) = ProfileFieldValue::new(field.as_str(), value) {
let _ = services
.users
.set_profile_field(
user_id,
ProfileFieldChange::Set(value.clone()),
PropagateTo::None,
)
.await;
Ok(Some(value))
} else {
Err!(Request(Unknown(
"User's homeserver returned malformed data for this profile field."
)))
}
} else {
let _ = services
.users
.set_profile_field(user_id, ProfileFieldChange::Delete(field), PropagateTo::None)
.await;
Ok(None)
}
}