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> { 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 { 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 { 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 { 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) { 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 { 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) { 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 { 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 + '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, ) { 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; } }