From 45f4496e4f0f1c538cd4a335341b0cb8970a3bea Mon Sep 17 00:00:00 2001 From: dasha_uwu Date: Sun, 25 Jan 2026 02:51:14 +0500 Subject: [PATCH] Refactor `admin rooms moderation` Split alias.*_alias_by from alias.*_ailias --- src/admin/room/alias.rs | 23 +- src/admin/room/moderation.rs | 448 +++++++------------------------- src/api/client/alias.rs | 4 +- src/api/client/room/create.rs | 2 +- src/api/client/room/upgrade.rs | 4 +- src/service/admin/create.rs | 4 +- src/service/admin/mod.rs | 4 + src/service/rooms/alias/mod.rs | 41 +-- src/service/rooms/delete/mod.rs | 2 +- 9 files changed, 132 insertions(+), 400 deletions(-) diff --git a/src/admin/room/alias.rs b/src/admin/room/alias.rs index d6b140e4..c4b6f28e 100644 --- a/src/admin/room/alias.rs +++ b/src/admin/room/alias.rs @@ -44,7 +44,6 @@ pub(crate) enum RoomAliasCommand { pub(super) async fn process(command: RoomAliasCommand, context: &Context<'_>) -> Result { let services = context.services; - let server_user = &services.globals.server_user; match command { | RoomAliasCommand::Set { ref room_alias_localpart, .. } @@ -68,10 +67,7 @@ pub(super) async fn process(command: RoomAliasCommand, context: &Context<'_>) -> .await, ) { | (true, Ok(id)) => { - match services - .alias - .set_alias(&room_alias, &room_id, server_user) - { + match services.alias.set_alias(&room_alias, &room_id) { | Err(err) => Err!("Failed to remove alias: {err}"), | Ok(()) => context @@ -85,14 +81,9 @@ pub(super) async fn process(command: RoomAliasCommand, context: &Context<'_>) -> "Refusing to overwrite in use alias for {id}, use -f or --force to \ overwrite" ), - | (_, Err(_)) => { - match services - .alias - .set_alias(&room_alias, &room_id, server_user) - { - | Err(err) => Err!("Failed to remove alias: {err}"), - | Ok(()) => context.write_str("Successfully set alias").await, - } + | (_, Err(_)) => match services.alias.set_alias(&room_alias, &room_id) { + | Err(err) => Err!("Failed to remove alias: {err}"), + | Ok(()) => context.write_str("Successfully set alias").await, }, } }, @@ -103,11 +94,7 @@ pub(super) async fn process(command: RoomAliasCommand, context: &Context<'_>) -> .await { | Err(_) => Err!("Alias isn't in use."), - | Ok(id) => match services - .alias - .remove_alias(&room_alias, server_user) - .await - { + | Ok(id) => match services.alias.remove_alias(&room_alias).await { | Err(err) => Err!("Failed to remove alias: {err}"), | Ok(()) => context diff --git a/src/admin/room/moderation.rs b/src/admin/room/moderation.rs index 434a8b6f..60952ba2 100644 --- a/src/admin/room/moderation.rs +++ b/src/admin/room/moderation.rs @@ -1,11 +1,12 @@ use clap::Subcommand; use futures::{FutureExt, StreamExt}; -use ruma::{OwnedRoomId, OwnedRoomOrAliasId, RoomAliasId, RoomId, RoomOrAliasId}; +use ruma::{OwnedRoomId, OwnedRoomOrAliasId, RoomId, RoomOrAliasId}; use tuwunel_core::{ - Err, Result, debug, + Err, Result, debug, is_equal_to, utils::{IterStream, ReadyExt}, warn, }; +use tuwunel_service::Services; use crate::{admin_command, admin_command_dispatch, get_room_info}; @@ -44,6 +45,59 @@ pub(crate) enum RoomModerationCommand { }, } +async fn do_ban_room(services: &Services, room_id: &RoomId) { + services.metadata.ban_room(room_id); + + debug!("Banned {room_id} successfully"); + + debug!("Making all users leave the room {room_id} and forgetting it"); + let mut users = services + .state_cache + .room_members(room_id) + .ready_filter(|user| services.globals.user_is_local(user)) + .map(ToOwned::to_owned) + .boxed(); + + while let Some(ref user_id) = users.next().await { + debug!( + "Attempting leave for user {user_id} in room {room_id} (ignoring all errors, \ + evicting admins too)", + ); + + let state_lock = services.state.mutex.lock(room_id).await; + + if let Err(e) = services + .membership + .leave(user_id, room_id, None, false, &state_lock) + .boxed() + .await + { + warn!("Failed to leave room: {e}"); + } + + drop(state_lock); + + services.state_cache.forget(room_id, user_id); + } + + // remove any local aliases, ignore errors + services + .alias + .local_aliases_for_room(room_id) + .map(ToOwned::to_owned) + .for_each(async |local_alias| { + if let Err(e) = services.alias.remove_alias(&local_alias).await { + warn!("Error removing alias {local_alias} for {room_id}: {e}"); + } + }) + .await; + + // unpublish from room directory, ignore errors + services.directory.set_not_public(room_id); + + services.metadata.disable_room(room_id); +} + #[admin_command] async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result { debug!("Got room alias or ID: {}", room); @@ -56,137 +110,9 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result { return Err!("Not allowed to ban the admin room."); } - let room_id = if room.is_room_id() { - let room_id = match RoomId::parse(&room) { - | Ok(room_id) => room_id, - | Err(e) => { - return Err!( - "Failed to parse room ID {room}. Please note that this requires a full room \ - ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \ - (`#roomalias:example.com`): {e}" - ); - }, - }; + let room_id = self.services.alias.maybe_resolve(&room).await?; - debug!("Room specified is a room ID, banning room ID"); - self.services.metadata.ban_room(room_id); - - room_id.to_owned() - } else if room.is_room_alias_id() { - let room_alias = match RoomAliasId::parse(&room) { - | Ok(room_alias) => room_alias, - | Err(e) => { - return Err!( - "Failed to parse room ID {room}. Please note that this requires a full room \ - ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \ - (`#roomalias:example.com`): {e}" - ); - }, - }; - - debug!( - "Room specified is not a room ID, attempting to resolve room alias to a room ID \ - locally, if not using get_alias_helper to fetch room ID remotely" - ); - - let room_id = match self - .services - .alias - .resolve_local_alias(room_alias) - .await - { - | Ok(room_id) => room_id, - | _ => { - debug!( - "We don't have this room alias to a room ID locally, attempting to fetch \ - room ID over federation" - ); - - match self - .services - .alias - .resolve_alias(room_alias) - .await - { - | Ok((room_id, servers)) => { - debug!( - ?room_id, - ?servers, - "Got federation response fetching room ID for {room_id}" - ); - room_id - }, - | Err(e) => { - return Err!( - "Failed to resolve room alias {room_alias} to a room ID: {e}" - ); - }, - } - }, - }; - - self.services.metadata.ban_room(&room_id); - - room_id - } else { - return Err!( - "Room specified is not a room ID or room alias. Please note that this requires a \ - full room ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \ - (`#roomalias:example.com`)", - ); - }; - - debug!("Making all users leave the room {room_id} and forgetting it"); - let mut users = self - .services - .state_cache - .room_members(&room_id) - .map(ToOwned::to_owned) - .ready_filter(|user| self.services.globals.user_is_local(user)) - .boxed(); - - while let Some(ref user_id) = users.next().await { - debug!( - "Attempting leave for user {user_id} in room {room_id} (ignoring all errors, \ - evicting admins too)", - ); - - let state_lock = self.services.state.mutex.lock(&room_id).await; - - if let Err(e) = self - .services - .membership - .leave(user_id, &room_id, None, false, &state_lock) - .boxed() - .await - { - warn!("Failed to leave room: {e}"); - } - - drop(state_lock); - - self.services - .state_cache - .forget(&room_id, user_id); - } - - self.services - .alias - .local_aliases_for_room(&room_id) - .map(ToOwned::to_owned) - .for_each(async |local_alias| { - self.services - .alias - .remove_alias(&local_alias, &self.services.globals.server_user) - .await - .ok(); - }) - .await; - - // unpublish from room directory - self.services.directory.set_not_public(&room_id); - - self.services.metadata.disable_room(&room_id); + do_ban_room(self.services, &room_id).await; self.write_str( "Room banned, removed all our local users, and disabled incoming federation with room.", @@ -209,164 +135,51 @@ async fn ban_list_of_rooms(&self) -> Result { .drain(1..self.body.len().saturating_sub(1)) .collect::>(); - let admin_room_alias = &self.services.admin.admin_alias; + let admin_room_id = self.services.admin.get_admin_room().await.ok(); - let mut room_ban_count: usize = 0; - let mut room_ids: Vec = Vec::new(); + let mut room_ids: Vec = Vec::with_capacity(rooms_s.len()); - for &room in &rooms_s { - match <&RoomOrAliasId>::try_from(room) { - | Ok(room_alias_or_id) => { - if let Ok(admin_room_id) = self.services.admin.get_admin_room().await - && (room.to_owned().eq(&admin_room_id) - || room.to_owned().eq(admin_room_alias)) - { - warn!("User specified admin room in bulk ban list, ignoring"); - continue; - } - - if room_alias_or_id.is_room_id() { - let room_id = match RoomId::parse(room_alias_or_id) { - | Ok(room_id) => room_id, - | Err(e) => { - // ignore rooms we failed to parse - warn!( - "Error parsing room \"{room}\" during bulk room banning, \ - ignoring error and logging here: {e}" - ); - continue; - }, - }; - - room_ids.push(room_id.to_owned()); - } - - if room_alias_or_id.is_room_alias_id() { - match RoomAliasId::parse(room_alias_or_id) { - | Ok(room_alias) => { - let room_id = match self - .services - .alias - .resolve_local_alias(room_alias) - .await - { - | Ok(room_id) => room_id, - | _ => { - debug!( - "We don't have this room alias to a room ID locally, \ - attempting to fetch room ID over federation" - ); - - match self - .services - .alias - .resolve_alias(room_alias) - .await - { - | Ok((room_id, servers)) => { - debug!( - ?room_id, - ?servers, - "Got federation response fetching room ID for \ - {room}", - ); - room_id - }, - | Err(e) => { - warn!( - "Failed to resolve room alias {room} to a room \ - ID: {e}" - ); - continue; - }, - } - }, - }; - - room_ids.push(room_id); - }, - | Err(e) => { - warn!( - "Error parsing room \"{room}\" during bulk room banning, \ - ignoring error and logging here: {e}" - ); - continue; - }, - } - } - }, + for room in rooms_s { + let room_alias_or_id = match <&RoomOrAliasId>::try_from(room) { + | Ok(room_alias_or_id) => room_alias_or_id, | Err(e) => { - warn!( - "Error parsing room \"{room}\" during bulk room banning, ignoring error and \ - logging here: {e}" - ); + warn!("Error parsing room {room} during bulk room banning, ignoring: {e}"); continue; }, + }; + + let room_id = match self + .services + .alias + .maybe_resolve(room_alias_or_id) + .await + { + | Ok(room_id) => room_id, + | Err(e) => { + warn!("Failed to resolve room alias {room_alias_or_id} to a room ID: {e}"); + continue; + }, + }; + + if admin_room_id + .as_ref() + .is_some_and(is_equal_to!(&room_id)) + { + warn!("User specified admin room in bulk ban list, ignoring"); + continue; } + + room_ids.push(room_id); } + let rooms_len = room_ids.len(); + for room_id in room_ids { - self.services.metadata.ban_room(&room_id); - - debug!("Banned {room_id} successfully"); - room_ban_count = room_ban_count.saturating_add(1); - - debug!("Making all users leave the room {room_id} and forgetting it"); - let mut users = self - .services - .state_cache - .room_members(&room_id) - .map(ToOwned::to_owned) - .ready_filter(|user| self.services.globals.user_is_local(user)) - .boxed(); - - while let Some(ref user_id) = users.next().await { - debug!( - "Attempting leave for user {user_id} in room {room_id} (ignoring all errors, \ - evicting admins too)", - ); - - let state_lock = self.services.state.mutex.lock(&room_id).await; - - if let Err(e) = self - .services - .membership - .leave(user_id, &room_id, None, false, &state_lock) - .boxed() - .await - { - warn!("Failed to leave room: {e}"); - } - - drop(state_lock); - - self.services - .state_cache - .forget(&room_id, user_id); - } - - // remove any local aliases, ignore errors - self.services - .alias - .local_aliases_for_room(&room_id) - .map(ToOwned::to_owned) - .for_each(async |local_alias| { - self.services - .alias - .remove_alias(&local_alias, &self.services.globals.server_user) - .await - .ok(); - }) - .await; - - // unpublish from room directory, ignore errors - self.services.directory.set_not_public(&room_id); - - self.services.metadata.disable_room(&room_id); + do_ban_room(self.services, &room_id).await; } self.write_str(&format!( - "Finished bulk room ban, banned {room_ban_count} total rooms, evicted all users, and \ + "Finished bulk room ban, banned {rooms_len} total rooms, evicted all users, and \ disabled incoming federation with the room." )) .await @@ -374,84 +187,9 @@ async fn ban_list_of_rooms(&self) -> Result { #[admin_command] async fn unban_room(&self, room: OwnedRoomOrAliasId) -> Result { - let room_id = if room.is_room_id() { - let room_id = match RoomId::parse(&room) { - | Ok(room_id) => room_id, - | Err(e) => { - return Err!( - "Failed to parse room ID {room}. Please note that this requires a full room \ - ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \ - (`#roomalias:example.com`): {e}" - ); - }, - }; - - debug!("Room specified is a room ID, unbanning room ID"); - self.services.metadata.unban_room(room_id); - - room_id.to_owned() - } else if room.is_room_alias_id() { - let room_alias = match RoomAliasId::parse(&room) { - | Ok(room_alias) => room_alias, - | Err(e) => { - return Err!( - "Failed to parse room ID {room}. Please note that this requires a full room \ - ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \ - (`#roomalias:example.com`): {e}" - ); - }, - }; - - debug!( - "Room specified is not a room ID, attempting to resolve room alias to a room ID \ - locally, if not using get_alias_helper to fetch room ID remotely" - ); - - let room_id = match self - .services - .alias - .resolve_local_alias(room_alias) - .await - { - | Ok(room_id) => room_id, - | _ => { - debug!( - "We don't have this room alias to a room ID locally, attempting to fetch \ - room ID over federation" - ); - - match self - .services - .alias - .resolve_alias(room_alias) - .await - { - | Ok((room_id, servers)) => { - debug!( - ?room_id, - ?servers, - "Got federation response fetching room ID for room {room}" - ); - room_id - }, - | Err(e) => { - return Err!("Failed to resolve room alias {room} to a room ID: {e}"); - }, - } - }, - }; - - self.services.metadata.unban_room(&room_id); - - room_id - } else { - return Err!( - "Room specified is not a room ID or room alias. Please note that this requires a \ - full room ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \ - (`#roomalias:example.com`)", - ); - }; + let room_id = self.services.alias.maybe_resolve(&room).await?; + self.services.metadata.unban_room(&room_id); self.services.metadata.enable_room(&room_id); self.write_str("Room unbanned and federation re-enabled.") .await diff --git a/src/api/client/alias.rs b/src/api/client/alias.rs index 3db04bda..238d9a2a 100644 --- a/src/api/client/alias.rs +++ b/src/api/client/alias.rs @@ -44,7 +44,7 @@ pub(crate) async fn create_alias_route( services .alias - .set_alias(&body.room_alias, &body.room_id, sender_user)?; + .set_alias_by(&body.room_alias, &body.room_id, sender_user)?; Ok(create_alias::v3::Response::new()) } @@ -66,7 +66,7 @@ pub(crate) async fn delete_alias_route( services .alias - .remove_alias(&body.room_alias, sender_user) + .remove_alias_by(&body.room_alias, sender_user) .await?; // TODO: update alt_aliases? diff --git a/src/api/client/room/create.rs b/src/api/client/room/create.rs index 087cbf81..a037a631 100644 --- a/src/api/client/room/create.rs +++ b/src/api/client/room/create.rs @@ -406,7 +406,7 @@ pub(crate) async fn create_room_route( if let Some(alias) = alias { services .alias - .set_alias(&alias, &room_id, sender_user)?; + .set_alias_by(&alias, &room_id, sender_user)?; } if body.visibility == room::Visibility::Public { diff --git a/src/api/client/room/upgrade.rs b/src/api/client/room/upgrade.rs index 8d28128c..3621f1e3 100644 --- a/src/api/client/room/upgrade.rs +++ b/src/api/client/room/upgrade.rs @@ -448,7 +448,7 @@ async fn move_local_aliases(&self) -> Result { .filter_map(|alias| { self.services .alias - .remove_alias(alias, self.sender_user) + .remove_alias_by(alias, self.sender_user) .inspect_err(move |e| error!(?alias, ?self, "Failed to remove alias: {e}")) .map_ok(move |()| alias) .ok() @@ -456,7 +456,7 @@ async fn move_local_aliases(&self) -> Result { .ready_for_each(|alias| { self.services .alias - .set_alias(alias, self.new_room_id, self.sender_user) + .set_alias_by(alias, self.new_room_id, self.sender_user) .inspect_err(|e| error!(?self, "Failed to add alias: {e}")) .ok(); }) diff --git a/src/service/admin/create.rs b/src/service/admin/create.rs index 1f2f7100..1292472f 100644 --- a/src/service/admin/create.rs +++ b/src/service/admin/create.rs @@ -201,9 +201,7 @@ pub async fn create_admin_room(services: &Services) -> Result { .boxed() .await?; - services - .alias - .set_alias(alias, &room_id, server_user)?; + services.alias.set_alias(alias, &room_id)?; // 7. (ad-hoc) Disable room URL previews for everyone by default services diff --git a/src/service/admin/mod.rs b/src/service/admin/mod.rs index 1cf31f03..51304c1f 100644 --- a/src/service/admin/mod.rs +++ b/src/service/admin/mod.rs @@ -223,6 +223,10 @@ impl Service { /// Checks whether a given user is an admin of this server pub async fn user_is_admin(&self, user_id: &UserId) -> bool { + if user_id == self.services.globals.server_user { + return true; + } + let Ok(admin_room) = self.get_admin_room().await else { return false; }; diff --git a/src/service/rooms/alias/mod.rs b/src/service/rooms/alias/mod.rs index 377351f8..089af780 100644 --- a/src/service/rooms/alias/mod.rs +++ b/src/service/rooms/alias/mod.rs @@ -41,8 +41,19 @@ impl crate::Service for Service { } impl Service { + pub fn set_alias(&self, alias: &RoomAliasId, room_id: &RoomId) -> Result { + self.check_alias_local(alias)?; + + self.set_alias_by(alias, room_id, &self.services.globals.server_user) + } + #[tracing::instrument(skip(self))] - pub fn set_alias(&self, alias: &RoomAliasId, room_id: &RoomId, user_id: &UserId) -> Result { + pub fn set_alias_by( + &self, + alias: &RoomAliasId, + room_id: &RoomId, + user_id: &UserId, + ) -> Result { self.check_alias_local(alias)?; if alias == self.services.admin.admin_alias @@ -53,32 +64,30 @@ impl Service { let count = self.services.globals.next_count(); + let localpart = alias.alias(); + // Comes first as we don't want a stuck alias - self.db - .alias_userid - .insert(alias.alias().as_bytes(), user_id.as_bytes()); + self.db.alias_userid.insert(localpart, user_id); - self.db - .alias_roomid - .insert(alias.alias().as_bytes(), room_id.as_bytes()); - - let mut aliasid = room_id.as_bytes().to_vec(); - aliasid.push(0xFF); - aliasid.extend_from_slice(&count.to_be_bytes()); + self.db.alias_roomid.insert(localpart, room_id); self.db .aliasid_alias - .insert(&aliasid, alias.as_bytes()); + .put_raw((room_id, *count), alias); Ok(()) } - #[tracing::instrument(skip(self))] - pub async fn remove_alias(&self, alias: &RoomAliasId, user_id: &UserId) -> Result { + pub async fn remove_alias_by(&self, alias: &RoomAliasId, user_id: &UserId) -> Result { if !self.user_can_remove_alias(alias, user_id).await? { return Err!(Request(Forbidden("User is not permitted to remove this alias."))); } + self.remove_alias(alias).await + } + + #[tracing::instrument(skip(self))] + pub async fn remove_alias(&self, alias: &RoomAliasId) -> Result { let alias = alias.alias(); let Ok(room_id) = self.db.alias_roomid.get(&alias).await else { return Err!(Request(NotFound("Alias does not exist or is invalid."))); @@ -198,16 +207,12 @@ impl Service { .await .map_err(|_| err!(Request(NotFound("Alias not found."))))?; - let server_user = &self.services.globals.server_user; - // The creator of an alias can remove it if self .who_created_alias(alias).await .is_ok_and(|user| user == user_id) // Server admins can remove any local alias || self.services.admin.user_is_admin(user_id).await - // Always allow the server service account to remove the alias, since there may not be an admin room - || server_user == user_id { return Ok(true); } diff --git a/src/service/rooms/delete/mod.rs b/src/service/rooms/delete/mod.rs index f96576e0..c81dde0f 100644 --- a/src/service/rooms/delete/mod.rs +++ b/src/service/rooms/delete/mod.rs @@ -100,7 +100,7 @@ impl Service { .for_each(async |local_alias| { self.services .alias - .remove_alias(local_alias, &self.services.globals.server_user) + .remove_alias(local_alias) .await .log_err() .ok();