mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-11-13 20:26:52 +00:00
367 lines
11 KiB
Rust
367 lines
11 KiB
Rust
use std::{borrow::Cow, collections::BTreeMap};
|
|
|
|
use conduwuit::{
|
|
Err, Result,
|
|
pdu::PartialPdu,
|
|
utils::{ReadyExt, stream::TryIgnore, to_canonical_object},
|
|
warn,
|
|
};
|
|
use database::{Deserialized, Ignore, Interfix, Json};
|
|
use futures::{Stream, StreamExt};
|
|
use ruma::{
|
|
OwnedMxcUri, UserId,
|
|
api::client::profile::PropagateTo,
|
|
events::room::member::MembershipState,
|
|
presence::PresenceState,
|
|
profile::{ProfileFieldName, ProfileFieldValue},
|
|
};
|
|
use serde_json::{Value, to_value};
|
|
|
|
pub enum ProfileFieldChange {
|
|
Set(ProfileFieldValue),
|
|
Delete(ProfileFieldName),
|
|
}
|
|
|
|
impl ProfileFieldChange {
|
|
fn field_name(&self) -> ProfileFieldName {
|
|
match self {
|
|
| &Self::Delete(ref name) => name.clone(),
|
|
| &Self::Set(ref value) => value.field_name(),
|
|
}
|
|
}
|
|
|
|
fn value(&self) -> Option<Cow<'_, Value>> {
|
|
if let Self::Set(value) = self {
|
|
Some(value.value())
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
impl super::Service {
|
|
pub async fn set_profile_field(
|
|
&self,
|
|
user_id: &UserId,
|
|
change: ProfileFieldChange,
|
|
propagate_to: PropagateTo,
|
|
) -> Result<()> {
|
|
const MAX_KEY_LENGTH_BYTES: usize = 255;
|
|
const MAX_PROFILE_LENGTH_BYTES: usize = 65536;
|
|
|
|
let field_name = change.field_name();
|
|
|
|
// TODO: The spec mentions special error codes (M_PROFILE_TOO_LARGE,
|
|
// M_KEY_TOO_LARGE) for profile field size limits, but they're not in
|
|
// its list of error codes and Ruma doesn't have them. Should we
|
|
// return those, or is M_TOO_LARGE okay?
|
|
if field_name.as_str().len() > MAX_KEY_LENGTH_BYTES {
|
|
return Err!(Request(TooLarge(
|
|
"Individual profile keys must not exceed {MAX_KEY_LENGTH_BYTES} bytes in length."
|
|
)));
|
|
}
|
|
|
|
// Serialize the entire profile as canonical JSON, including the new
|
|
// change, to check if it exceeds 64 KiB
|
|
{
|
|
let mut full_profile = self.get_local_profile(user_id).await;
|
|
|
|
match &change {
|
|
| ProfileFieldChange::Set(value) => {
|
|
full_profile.insert(
|
|
value.field_name().as_str().to_owned(),
|
|
value.value().clone().into_owned(),
|
|
);
|
|
},
|
|
| ProfileFieldChange::Delete(key) => {
|
|
full_profile.remove(key.as_str());
|
|
},
|
|
}
|
|
|
|
if let Ok(canonical_profile) = to_canonical_object(full_profile) {
|
|
if serde_json::to_string(&canonical_profile)
|
|
.expect("should be able to serialize to string")
|
|
.len() > MAX_PROFILE_LENGTH_BYTES
|
|
{
|
|
return Err!(
|
|
"Profile data must not exceed {MAX_PROFILE_LENGTH_BYTES} bytes in \
|
|
length."
|
|
);
|
|
}
|
|
} else {
|
|
return Err!(Request(BadJson("Failed to canonicalize profile.")));
|
|
}
|
|
}
|
|
|
|
// Check if this change would be a no-op
|
|
if self
|
|
.get_local_profile_field(user_id, field_name.clone())
|
|
.await
|
|
.is_some_and(|value| Some(value.value()) == change.value())
|
|
{
|
|
return Ok(());
|
|
}
|
|
|
|
// If the user is local and changed their displayname or avatar_url,
|
|
// update it in all their joined rooms. This is done before updating
|
|
// their profile data so we can check the old value of the field if
|
|
// `propagate_to` is `unchanged`.
|
|
if matches!(field_name, ProfileFieldName::AvatarUrl | ProfileFieldName::DisplayName)
|
|
&& matches!(propagate_to, PropagateTo::All | PropagateTo::Unchanged)
|
|
&& self.services.globals.user_is_local(user_id)
|
|
{
|
|
let current_displayname = self.displayname(user_id).await.ok();
|
|
let current_avatar_url = self.avatar_url(user_id).await.ok();
|
|
|
|
let mut all_joined_rooms = self.services.state_cache.rooms_joined(user_id);
|
|
|
|
while let Some(room_id) = all_joined_rooms.next().await {
|
|
// TODO: this clobbers any custom fields on the event content
|
|
let mut current_membership = match self
|
|
.services
|
|
.state_accessor
|
|
.get_member(&room_id, user_id)
|
|
.await
|
|
{
|
|
| Ok(current_membership)
|
|
if current_membership.membership == MembershipState::Join =>
|
|
current_membership,
|
|
| Ok(current_membership) => {
|
|
warn!(
|
|
?user_id,
|
|
?room_id,
|
|
"User is not joined in joined room: {current_membership:?}"
|
|
);
|
|
continue;
|
|
},
|
|
| Err(err) => {
|
|
warn!(
|
|
?user_id,
|
|
?room_id,
|
|
"Could not load membership event for joined room: {err}"
|
|
);
|
|
continue;
|
|
},
|
|
};
|
|
|
|
// If `propagate_to` is `unchanged`, and the current value of
|
|
// the field we're updating was changed from its global
|
|
// value in this room, skip it.
|
|
if matches!(propagate_to, PropagateTo::Unchanged) {
|
|
let field_changed_from_global = match field_name {
|
|
| ProfileFieldName::AvatarUrl =>
|
|
current_membership.avatar_url.as_ref() != current_avatar_url.as_ref(),
|
|
| ProfileFieldName::DisplayName =>
|
|
current_membership.displayname.as_ref()
|
|
!= current_displayname.as_ref(),
|
|
| _ => unreachable!(),
|
|
};
|
|
|
|
if field_changed_from_global {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
let state_lock = self.services.state.mutex.lock(room_id.as_str()).await;
|
|
|
|
// Preserve keys in accordance with the key copying rules
|
|
current_membership.reason = None;
|
|
current_membership.join_authorized_via_users_server = None;
|
|
match &change {
|
|
| ProfileFieldChange::Set(ProfileFieldValue::AvatarUrl(avatar_url)) => {
|
|
current_membership.avatar_url = Some(avatar_url.clone());
|
|
},
|
|
| ProfileFieldChange::Set(ProfileFieldValue::DisplayName(displayname)) => {
|
|
current_membership.displayname = Some(displayname.clone());
|
|
},
|
|
| ProfileFieldChange::Delete(ProfileFieldName::AvatarUrl) => {
|
|
current_membership.avatar_url = None;
|
|
},
|
|
| ProfileFieldChange::Delete(ProfileFieldName::DisplayName) => {
|
|
current_membership.displayname = None;
|
|
},
|
|
| _ => unreachable!(),
|
|
}
|
|
|
|
let _ = self
|
|
.services
|
|
.timeline
|
|
.build_and_append_pdu(
|
|
PartialPdu::state(user_id.to_string(), ¤t_membership),
|
|
user_id,
|
|
Some(&room_id),
|
|
&state_lock,
|
|
)
|
|
.await;
|
|
}
|
|
|
|
if self.services.config.allow_local_presence {
|
|
// Send a presence EDU to indicate the profile changed
|
|
let _ = self
|
|
.services
|
|
.presence
|
|
.ping_presence(user_id, &PresenceState::Online)
|
|
.await;
|
|
}
|
|
}
|
|
|
|
match change {
|
|
| ProfileFieldChange::Set(ProfileFieldValue::DisplayName(displayname)) => {
|
|
self.set_displayname(user_id, Some(displayname).filter(|dn| !dn.is_empty()));
|
|
},
|
|
| ProfileFieldChange::Set(ProfileFieldValue::AvatarUrl(avatar_url)) => {
|
|
self.set_avatar_url(user_id, Some(avatar_url).filter(|av| av.is_valid()));
|
|
},
|
|
| ProfileFieldChange::Delete(ProfileFieldName::DisplayName) => {
|
|
self.set_displayname(user_id, None);
|
|
},
|
|
| ProfileFieldChange::Delete(ProfileFieldName::AvatarUrl) => {
|
|
self.set_avatar_url(user_id, None);
|
|
},
|
|
| other => self.set_profile_key(
|
|
user_id,
|
|
other.field_name().as_str(),
|
|
other.value().map(Cow::into_owned),
|
|
),
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn get_local_profile(&self, user_id: &UserId) -> BTreeMap<String, Value> {
|
|
let mut profile = BTreeMap::new();
|
|
|
|
// Get displayname and avatar_url independently because
|
|
// `all_profile_keys` doesn't include them
|
|
for field in [ProfileFieldName::AvatarUrl, ProfileFieldName::DisplayName] {
|
|
let key = field.as_str().to_owned();
|
|
|
|
if let Some(value) = self.get_local_profile_field(user_id, field).await {
|
|
profile.insert(key, value.value().into_owned());
|
|
}
|
|
}
|
|
|
|
// Insert all other profile fields
|
|
let mut all_fields = self.all_profile_keys(user_id);
|
|
|
|
while let Some((key, value)) = all_fields.next().await {
|
|
profile.insert(key, value);
|
|
}
|
|
|
|
profile
|
|
}
|
|
|
|
pub async fn get_local_profile_field(
|
|
&self,
|
|
user_id: &UserId,
|
|
field: ProfileFieldName,
|
|
) -> Option<ProfileFieldValue> {
|
|
let value = match field.clone() {
|
|
| ProfileFieldName::AvatarUrl => self
|
|
.avatar_url(user_id)
|
|
.await
|
|
.ok()
|
|
.map(to_value)
|
|
.transpose()
|
|
.expect("converting avatar url to value should succeed"),
|
|
| ProfileFieldName::DisplayName => self
|
|
.displayname(user_id)
|
|
.await
|
|
.ok()
|
|
.map(to_value)
|
|
.transpose()
|
|
.expect("converting displayname to value should succeed"),
|
|
| other => self.profile_key(user_id, other.as_str()).await.ok(),
|
|
}?;
|
|
|
|
Some(
|
|
ProfileFieldValue::new(field.as_str(), value)
|
|
.expect("local profile field should be valid"),
|
|
)
|
|
}
|
|
|
|
/// Returns the displayname of a user on this homeserver.
|
|
pub async fn displayname(&self, user_id: &UserId) -> Result<String> {
|
|
self.db.userid_displayname.get(user_id).await.deserialized()
|
|
}
|
|
|
|
/// Sets a new displayname or removes it if displayname is None. You still
|
|
/// need to notify all rooms of this change.
|
|
fn set_displayname(&self, user_id: &UserId, displayname: Option<String>) {
|
|
if let Some(displayname) = displayname {
|
|
self.db.userid_displayname.insert(user_id, displayname);
|
|
} else {
|
|
self.db.userid_displayname.remove(user_id);
|
|
}
|
|
}
|
|
|
|
/// Get the `avatar_url` of a user.
|
|
pub async fn avatar_url(&self, user_id: &UserId) -> Result<OwnedMxcUri> {
|
|
self.db.userid_avatarurl.get(user_id).await.deserialized()
|
|
}
|
|
|
|
/// Sets a new avatar_url or removes it if avatar_url is None.
|
|
fn set_avatar_url(&self, user_id: &UserId, avatar_url: Option<OwnedMxcUri>) {
|
|
match avatar_url {
|
|
| Some(avatar_url) => {
|
|
self.db.userid_avatarurl.insert(user_id, &avatar_url);
|
|
},
|
|
| _ => {
|
|
self.db.userid_avatarurl.remove(user_id);
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Gets a specific user profile key
|
|
pub async fn profile_key(&self, user_id: &UserId, profile_key: &str) -> Result<Value> {
|
|
let key = (user_id, profile_key);
|
|
self.db
|
|
.useridprofilekey_value
|
|
.qry(&key)
|
|
.await
|
|
.and_then(|handle| serde_json::from_slice(&handle).map_err(Into::into))
|
|
}
|
|
|
|
/// Gets all the user's profile keys and values in an iterator
|
|
pub fn all_profile_keys<'a>(
|
|
&'a self,
|
|
user_id: &'a UserId,
|
|
) -> impl Stream<Item = (String, Value)> + 'a + Send {
|
|
type KeyVal<'a> = ((Ignore, String), &'a [u8]);
|
|
|
|
let prefix = (user_id, Interfix);
|
|
self.db
|
|
.useridprofilekey_value
|
|
.stream_prefix(&prefix)
|
|
.ignore_err()
|
|
.map(|((_, key), value): KeyVal<'_>| Ok((key, serde_json::from_slice(value)?)))
|
|
.ignore_err()
|
|
}
|
|
|
|
/// Sets a new profile key value, removes the key if value is None
|
|
fn set_profile_key(
|
|
&self,
|
|
user_id: &UserId,
|
|
profile_key: &str,
|
|
profile_key_value: Option<Value>,
|
|
) {
|
|
let key = (user_id, profile_key);
|
|
|
|
if let Some(value) = profile_key_value {
|
|
self.db.useridprofilekey_value.put(key, Json(value));
|
|
} else {
|
|
self.db.useridprofilekey_value.del(key);
|
|
}
|
|
}
|
|
|
|
/// Clears all profile data for a user, including display name and avatar
|
|
/// url.
|
|
pub async fn clear_profile(&self, user_id: &UserId) {
|
|
self.set_displayname(user_id, None);
|
|
self.set_avatar_url(user_id, None);
|
|
self.all_profile_keys(user_id)
|
|
.ready_for_each(|(key, _)| self.set_profile_key(user_id, &key, None))
|
|
.await;
|
|
}
|
|
}
|