diff --git a/Cargo.lock b/Cargo.lock index 5e238598f7ab62064f37de196d00a4b380f81d62..f8b1c5ef9e981e247ef0b7575e189b4401f79ea7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -302,7 +302,7 @@ dependencies = [ [[package]] name = "axum-server" version = "0.6.0" -source = "git+https://github.com/lexe-app/axum-server?branch=phlip9/fix-graceful-shutdown#8e3368d899079818934e61cc9c839abcbbcada8a" +source = "git+https://github.com/girlbossceo/axum-server?branch=phlip9/fix-graceful-shutdown#8e3368d899079818934e61cc9c839abcbbcada8a" dependencies = [ "arc-swap", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 58e84a2223770b20ced62206995fb2528d2255ff..63528ef4a39867f19984ecccc9d2b5efb9c856c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -429,7 +429,7 @@ branch = "tracing-subscriber/env-filter-clone-0.1.x-backport" # fixes hyper graceful shutdowns [https://github.com/programatik29/axum-server/issues/114] [patch.crates-io.axum-server] -git = "https://github.com/lexe-app/axum-server" +git = "https://github.com/girlbossceo/axum-server" branch = "phlip9/fix-graceful-shutdown" # diff --git a/src/admin/user/user_commands.rs b/src/admin/user/user_commands.rs index 8b90894e940f2881a7f930b9247ae3a92c975647..aba3241799b98c7b6d32da9e8289371c8d270ad8 100644 --- a/src/admin/user/user_commands.rs +++ b/src/admin/user/user_commands.rs @@ -1,6 +1,6 @@ use std::{collections::BTreeMap, fmt::Write as _}; -use api::client::{join_room_by_id_helper, leave_all_rooms}; +use api::client::{join_room_by_id_helper, leave_all_rooms, update_avatar_url, update_displayname}; use conduit::utils; use ruma::{ events::{ @@ -151,6 +151,15 @@ pub(crate) async fn deactivate( "Making {user_id} leave all rooms after deactivation..." ))) .await; + + let all_joined_rooms: Vec<OwnedRoomId> = services() + .rooms + .state_cache + .rooms_joined(&user_id) + .filter_map(Result::ok) + .collect(); + update_displayname(user_id.clone(), None, all_joined_rooms.clone()).await?; + update_avatar_url(user_id.clone(), None, None, all_joined_rooms).await?; leave_all_rooms(&user_id).await; } @@ -249,6 +258,14 @@ pub(crate) async fn deactivate_all( deactivation_count = deactivation_count.saturating_add(1); if !no_leave_rooms { info!("Forcing user {user_id} to leave all rooms apart of deactivate-all"); + let all_joined_rooms: Vec<OwnedRoomId> = services() + .rooms + .state_cache + .rooms_joined(&user_id) + .filter_map(Result::ok) + .collect(); + update_displayname(user_id.clone(), None, all_joined_rooms.clone()).await?; + update_avatar_url(user_id.clone(), None, None, all_joined_rooms).await?; leave_all_rooms(&user_id).await; } }, diff --git a/src/api/client/account.rs b/src/api/client/account.rs index dc384c4cfeaf2c40858e51748da255d66ae20ac8..5521bdff6400b68121866fcae374595ec3d977b7 100644 --- a/src/api/client/account.rs +++ b/src/api/client/account.rs @@ -15,7 +15,7 @@ uiaa::{AuthFlow, AuthType, UiaaInfo}, }, events::{room::message::RoomMessageEventContent, GlobalAccountDataEventType}, - push, UserId, + push, OwnedRoomId, UserId, }; use tracing::{error, info, warn}; @@ -547,12 +547,22 @@ pub(crate) async fn deactivate_route( return Err(Error::BadRequest(ErrorKind::NotJson, "Not json.")); } - // Make the user leave all rooms before deactivation - super::leave_all_rooms(sender_user).await; - // Remove devices and mark account as deactivated services().users.deactivate_account(sender_user)?; + // Remove profile pictures and display name + let all_joined_rooms: Vec<OwnedRoomId> = services() + .rooms + .state_cache + .rooms_joined(sender_user) + .filter_map(Result::ok) + .collect(); + super::update_displayname(sender_user.clone(), None, all_joined_rooms.clone()).await?; + super::update_avatar_url(sender_user.clone(), None, None, all_joined_rooms).await?; + + // Make the user leave all rooms before deactivation + super::leave_all_rooms(sender_user).await; + info!("User {sender_user} deactivated their account."); services() .admin diff --git a/src/api/client/membership.rs b/src/api/client/membership.rs index 79ea50bd10bbe9027defa55b0604b588cf312c88..f781808c9f56b18f2d7e457e3517bccc3a9c7b89 100644 --- a/src/api/client/membership.rs +++ b/src/api/client/membership.rs @@ -1,6 +1,7 @@ use std::{ cmp, collections::{hash_map::Entry, BTreeMap, HashMap, HashSet}, + net::IpAddr, sync::Arc, time::{Duration, Instant}, }; @@ -36,13 +37,12 @@ use super::get_alias_helper; use crate::{ + client::{update_avatar_url, update_displayname}, service::{ pdu::{gen_event_id_canonical_json, PduBuilder}, server_is_ours, user_is_local, }, - services, - utils::{self}, - Error, PduEvent, Result, Ruma, + services, utils, Error, PduEvent, Result, Ruma, }; /// Checks if the room is banned in any way possible and the sender user is not @@ -51,7 +51,9 @@ /// Performs automatic deactivation if `auto_deactivate_banned_room_attempts` is /// enabled #[tracing::instrument] -async fn banned_room_check(user_id: &UserId, room_id: Option<&RoomId>, server_name: Option<&ServerName>) -> Result<()> { +async fn banned_room_check( + user_id: &UserId, room_id: Option<&RoomId>, server_name: Option<&ServerName>, client_ip: IpAddr, +) -> Result<()> { if !services().users.is_admin(user_id)? { if let Some(room_id) = room_id { if services().rooms.metadata.is_banned(room_id)? @@ -63,7 +65,7 @@ async fn banned_room_check(user_id: &UserId, room_id: Option<&RoomId>, server_na { warn!( "User {user_id} who is not an admin attempted to send an invite for or attempted to join a banned \ - room or banned room server name: {room_id}." + room or banned room server name: {room_id}" ); if services() @@ -75,15 +77,25 @@ async fn banned_room_check(user_id: &UserId, room_id: Option<&RoomId>, server_na services() .admin .send_message(RoomMessageEventContent::text_plain(format!( - "Automatically deactivating user {user_id} due to attempted banned room join" + "Automatically deactivating user {user_id} due to attempted banned room join from IP \ + {client_ip}" ))) .await; - // ignore errors - leave_all_rooms(user_id).await; if let Err(e) = services().users.deactivate_account(user_id) { - warn!(%e, "Failed to deactivate account"); + warn!(%user_id, %e, "Failed to deactivate account"); } + + let all_joined_rooms: Vec<OwnedRoomId> = services() + .rooms + .state_cache + .rooms_joined(user_id) + .filter_map(Result::ok) + .collect(); + + update_displayname(user_id.into(), None, all_joined_rooms.clone()).await?; + update_avatar_url(user_id.into(), None, None, all_joined_rooms).await?; + leave_all_rooms(user_id).await; } return Err(Error::BadRequest( @@ -112,15 +124,25 @@ async fn banned_room_check(user_id: &UserId, room_id: Option<&RoomId>, server_na services() .admin .send_message(RoomMessageEventContent::text_plain(format!( - "Automatically deactivating user {user_id} due to attempted banned room join" + "Automatically deactivating user {user_id} due to attempted banned room join from IP \ + {client_ip}" ))) .await; - // ignore errors - leave_all_rooms(user_id).await; if let Err(e) = services().users.deactivate_account(user_id) { - warn!(%e, "Failed to deactivate account"); + warn!(%user_id, %e, "Failed to deactivate account"); } + + let all_joined_rooms: Vec<OwnedRoomId> = services() + .rooms + .state_cache + .rooms_joined(user_id) + .filter_map(Result::ok) + .collect(); + + update_displayname(user_id.into(), None, all_joined_rooms.clone()).await?; + update_avatar_url(user_id.into(), None, None, all_joined_rooms).await?; + leave_all_rooms(user_id).await; } return Err(Error::BadRequest( @@ -148,7 +170,7 @@ pub(crate) async fn join_room_by_id_route( ) -> Result<join_room_by_id::v3::Response> { let sender_user = body.sender_user.as_ref().expect("user is authenticated"); - banned_room_check(sender_user, Some(&body.room_id), body.room_id.server_name()).await?; + banned_room_check(sender_user, Some(&body.room_id), body.room_id.server_name(), client_ip).await?; // There is no body.server_name for /roomId/join let mut servers = services() @@ -204,7 +226,7 @@ pub(crate) async fn join_room_by_id_or_alias_route( let (servers, room_id) = match OwnedRoomId::try_from(body.room_id_or_alias) { Ok(room_id) => { - banned_room_check(sender_user, Some(&room_id), room_id.server_name()).await?; + banned_room_check(sender_user, Some(&room_id), room_id.server_name(), client_ip).await?; let mut servers = body.server_name.clone(); servers.extend( @@ -238,7 +260,7 @@ pub(crate) async fn join_room_by_id_or_alias_route( Err(room_alias) => { let response = get_alias_helper(room_alias.clone(), Some(body.server_name.clone())).await?; - banned_room_check(sender_user, Some(&response.room_id), Some(room_alias.server_name())).await?; + banned_room_check(sender_user, Some(&response.room_id), Some(room_alias.server_name()), client_ip).await?; let mut servers = body.server_name; servers.extend(response.servers); @@ -315,7 +337,7 @@ pub(crate) async fn invite_user_route( )); } - banned_room_check(sender_user, Some(&body.room_id), body.room_id.server_name()).await?; + banned_room_check(sender_user, Some(&body.room_id), body.room_id.server_name(), client_ip).await?; if let invite_user::v3::InvitationRecipient::UserId { user_id, @@ -1578,11 +1600,11 @@ pub async fn leave_all_rooms(user_id: &UserId) { }; // ignore errors - if let Err(e) = services().rooms.state_cache.forget(&room_id, user_id) { - warn!(%e, "Failed to forget room"); - } if let Err(e) = leave_room(user_id, &room_id, None).await { - warn!(%e, "Failed to leave room"); + warn!(%room_id, %user_id, %e, "Failed to leave room"); + } + if let Err(e) = services().rooms.state_cache.forget(&room_id, user_id) { + warn!(%room_id, %user_id, %e, "Failed to forget room"); } } } diff --git a/src/api/client/mod.rs b/src/api/client/mod.rs index 6a6f16a75226039a76706250d6b46b59545e34d5..1461be94536ddabd4004e5050ff9f8cf585ed701 100644 --- a/src/api/client/mod.rs +++ b/src/api/client/mod.rs @@ -51,6 +51,7 @@ pub(super) use message::*; pub(super) use presence::*; pub(super) use profile::*; +pub use profile::{update_all_rooms, update_avatar_url, update_displayname}; pub(super) use push::*; pub(super) use read_marker::*; pub(super) use redact::*; diff --git a/src/api/client/profile.rs b/src/api/client/profile.rs index 96cbe7fddc8f57c693ae3fd21cd85f796943e9ca..8a53b335b0bc7236e551c9e74ae98a92fe76cc88 100644 --- a/src/api/client/profile.rs +++ b/src/api/client/profile.rs @@ -10,6 +10,7 @@ }, events::{room::member::RoomMemberEventContent, StateEventType, TimelineEventType}, presence::PresenceState, + OwnedMxcUri, OwnedRoomId, OwnedUserId, }; use serde_json::value::to_raw_value; use tracing::warn; @@ -28,70 +29,14 @@ pub(crate) async fn set_displayname_route( body: Ruma<set_display_name::v3::Request>, ) -> Result<set_display_name::v3::Response> { let sender_user = body.sender_user.as_ref().expect("user is authenticated"); - - services() - .users - .set_displayname(sender_user, body.displayname.clone()) - .await?; - - // Send a new membership event and presence update into all joined rooms - let all_rooms_joined: Vec<_> = services() + let all_joined_rooms: Vec<OwnedRoomId> = services() .rooms .state_cache .rooms_joined(sender_user) .filter_map(Result::ok) - .map(|room_id| { - Ok::<_, Error>(( - PduBuilder { - event_type: TimelineEventType::RoomMember, - content: to_raw_value(&RoomMemberEventContent { - displayname: body.displayname.clone(), - join_authorized_via_users_server: None, - ..serde_json::from_str( - services() - .rooms - .state_accessor - .room_state_get(&room_id, &StateEventType::RoomMember, sender_user.as_str())? - .ok_or_else(|| { - Error::bad_database("Tried to send displayname update for user not in the room.") - })? - .content - .get(), - ) - .map_err(|_| Error::bad_database("Database contains invalid PDU."))? - }) - .expect("event is valid, we just created it"), - unsigned: None, - state_key: Some(sender_user.to_string()), - redacts: None, - }, - room_id, - )) - }) - .filter_map(Result::ok) .collect(); - for (pdu_builder, room_id) in all_rooms_joined { - let mutex_state = Arc::clone( - services() - .globals - .roomid_mutex_state - .write() - .await - .entry(room_id.clone()) - .or_default(), - ); - let state_lock = mutex_state.lock().await; - - if let Err(e) = services() - .rooms - .timeline - .build_and_append_pdu(pdu_builder, sender_user, &room_id, &state_lock) - .await - { - warn!(%e, "Failed to update/send new display name in room"); - } - } + update_displayname(sender_user.clone(), body.displayname.clone(), all_joined_rooms).await?; if services().globals.allow_local_presence() { // Presence update @@ -168,75 +113,20 @@ pub(crate) async fn set_avatar_url_route( body: Ruma<set_avatar_url::v3::Request>, ) -> Result<set_avatar_url::v3::Response> { let sender_user = body.sender_user.as_ref().expect("user is authenticated"); - - services() - .users - .set_avatar_url(sender_user, body.avatar_url.clone()) - .await?; - - services() - .users - .set_blurhash(sender_user, body.blurhash.clone()) - .await?; - - // Send a new membership event and presence update into all joined rooms - let all_joined_rooms: Vec<_> = services() + let all_joined_rooms: Vec<OwnedRoomId> = services() .rooms .state_cache .rooms_joined(sender_user) .filter_map(Result::ok) - .map(|room_id| { - Ok::<_, Error>(( - PduBuilder { - event_type: TimelineEventType::RoomMember, - content: to_raw_value(&RoomMemberEventContent { - avatar_url: body.avatar_url.clone(), - join_authorized_via_users_server: None, - ..serde_json::from_str( - services() - .rooms - .state_accessor - .room_state_get(&room_id, &StateEventType::RoomMember, sender_user.as_str())? - .ok_or_else(|| { - Error::bad_database("Tried to send displayname update for user not in the room.") - })? - .content - .get(), - ) - .map_err(|_| Error::bad_database("Database contains invalid PDU."))? - }) - .expect("event is valid, we just created it"), - unsigned: None, - state_key: Some(sender_user.to_string()), - redacts: None, - }, - room_id, - )) - }) - .filter_map(Result::ok) .collect(); - for (pdu_builder, room_id) in all_joined_rooms { - let mutex_state = Arc::clone( - services() - .globals - .roomid_mutex_state - .write() - .await - .entry(room_id.clone()) - .or_default(), - ); - let state_lock = mutex_state.lock().await; - - if let Err(e) = services() - .rooms - .timeline - .build_and_append_pdu(pdu_builder, sender_user, &room_id, &state_lock) - .await - { - warn!(%e, "Failed to set/update room with new avatar URL / pfp"); - } - } + update_avatar_url( + sender_user.clone(), + body.avatar_url.clone(), + body.blurhash.clone(), + all_joined_rooms, + ) + .await?; if services().globals.allow_local_presence() { // Presence update @@ -363,3 +253,126 @@ pub(crate) async fn get_profile_route(body: Ruma<get_profile::v3::Request>) -> R displayname: services().users.displayname(&body.user_id)?, }) } + +pub async fn update_displayname( + user_id: OwnedUserId, displayname: Option<String>, all_joined_rooms: Vec<OwnedRoomId>, +) -> Result<()> { + services() + .users + .set_displayname(&user_id, displayname.clone()) + .await?; + + // Send a new join membership event into all joined rooms + let all_joined_rooms: Vec<_> = all_joined_rooms + .iter() + .map(|room_id| { + Ok::<_, Error>(( + PduBuilder { + event_type: TimelineEventType::RoomMember, + content: to_raw_value(&RoomMemberEventContent { + displayname: displayname.clone(), + join_authorized_via_users_server: None, + ..serde_json::from_str( + services() + .rooms + .state_accessor + .room_state_get(room_id, &StateEventType::RoomMember, user_id.as_str())? + .ok_or_else(|| { + Error::bad_database("Tried to send display name update for user not in the room.") + })? + .content + .get(), + ) + .map_err(|_| Error::bad_database("Database contains invalid PDU."))? + }) + .expect("event is valid, we just created it"), + unsigned: None, + state_key: Some(user_id.to_string()), + redacts: None, + }, + room_id, + )) + }) + .filter_map(Result::ok) + .collect(); + + update_all_rooms(all_joined_rooms, user_id).await; + + Ok(()) +} + +pub async fn update_avatar_url( + user_id: OwnedUserId, avatar_url: Option<OwnedMxcUri>, blurhash: Option<String>, all_joined_rooms: Vec<OwnedRoomId>, +) -> Result<()> { + services() + .users + .set_avatar_url(&user_id, avatar_url.clone()) + .await?; + services() + .users + .set_blurhash(&user_id, blurhash.clone()) + .await?; + + // Send a new join membership event into all joined rooms + let all_joined_rooms: Vec<_> = all_joined_rooms + .iter() + .map(|room_id| { + Ok::<_, Error>(( + PduBuilder { + event_type: TimelineEventType::RoomMember, + content: to_raw_value(&RoomMemberEventContent { + avatar_url: avatar_url.clone(), + blurhash: blurhash.clone(), + join_authorized_via_users_server: None, + ..serde_json::from_str( + services() + .rooms + .state_accessor + .room_state_get(room_id, &StateEventType::RoomMember, user_id.as_str())? + .ok_or_else(|| { + Error::bad_database("Tried to send avatar URL update for user not in the room.") + })? + .content + .get(), + ) + .map_err(|_| Error::bad_database("Database contains invalid PDU."))? + }) + .expect("event is valid, we just created it"), + unsigned: None, + state_key: Some(user_id.to_string()), + redacts: None, + }, + room_id, + )) + }) + .filter_map(Result::ok) + .collect(); + + update_all_rooms(all_joined_rooms, user_id).await; + + Ok(()) +} + +pub async fn update_all_rooms(all_joined_rooms: Vec<(PduBuilder, &OwnedRoomId)>, user_id: OwnedUserId) { + for (pdu_builder, room_id) in all_joined_rooms { + let mutex_state = Arc::clone( + services() + .globals + .roomid_mutex_state + .write() + .await + .entry(room_id.clone()) + .or_default(), + ); + let state_lock = mutex_state.lock().await; + + if let Err(e) = services() + .rooms + .timeline + .build_and_append_pdu(pdu_builder, &user_id, room_id, &state_lock) + .await + { + warn!(%user_id, %room_id, %e, "Failed to update/send new profile join membership update in room"); + } + } +} diff --git a/src/service/rooms/timeline/mod.rs b/src/service/rooms/timeline/mod.rs index affe35abdad3b17506d24c54066f16aa10770377..df23da8fb8be05877adb20ece4cf5e226e594d92 100644 --- a/src/service/rooms/timeline/mod.rs +++ b/src/service/rooms/timeline/mod.rs @@ -236,11 +236,19 @@ pub async fn append_pdu( "prev_content".to_owned(), CanonicalJsonValue::Object( utils::to_canonical_object(prev_state.content.clone()).map_err(|e| { - error!("Failed to convert prev_state to canonical JSON: {}", e); + error!("Failed to convert prev_state to canonical JSON: {e}"); Error::bad_database("Failed to convert prev_state to canonical JSON.") })?, ), ); + unsigned.insert( + String::from("prev_sender"), + CanonicalJsonValue::String(prev_state.sender.clone().to_string()), + ); + unsigned.insert( + String::from("replaces_state"), + CanonicalJsonValue::String(prev_state.event_id.clone().to_string()), + ); } } } else { @@ -660,6 +668,10 @@ pub fn create_hash_and_sign_event( "prev_sender".to_owned(), serde_json::to_value(&prev_pdu.sender).expect("UserId::to_value always works"), ); + unsigned.insert( + "replaces_state".to_owned(), + serde_json::to_value(&prev_pdu.event_id).expect("EventId is valid json"), + ); } } diff --git a/src/service/users/data.rs b/src/service/users/data.rs index f23557550f48e4a7d9e50e6f0069555ffa7697bf..5d3eadd8627c2d523529abfe2a32059ee78d424e 100644 --- a/src/service/users/data.rs +++ b/src/service/users/data.rs @@ -307,7 +307,7 @@ fn blurhash(&self, user_id: &UserId) -> Result<Option<String>> { .transpose() } - /// Sets a new avatar_url or removes it if avatar_url is None. + /// Sets a new blurhash or removes it if blurhash is None. fn set_blurhash(&self, user_id: &UserId, blurhash: Option<String>) -> Result<()> { if let Some(blurhash) = blurhash { self.userid_blurhash diff --git a/src/service/users/mod.rs b/src/service/users/mod.rs index ec17e796a62965831be984f0c4fcf010efb23145..b326078b2fa81205338e7fd2887eae39698d653c 100644 --- a/src/service/users/mod.rs +++ b/src/service/users/mod.rs @@ -299,7 +299,7 @@ pub async fn set_avatar_url(&self, user_id: &UserId, avatar_url: Option<OwnedMxc /// Get the blurhash of a user. pub fn blurhash(&self, user_id: &UserId) -> Result<Option<String>> { self.db.blurhash(user_id) } - /// Sets a new avatar_url or removes it if avatar_url is None. + /// Sets a new blurhash or removes it if blurhash is None. pub async fn set_blurhash(&self, user_id: &UserId, blurhash: Option<String>) -> Result<()> { self.db.set_blurhash(user_id, blurhash) }