Refactor admin rooms moderation

Split alias.*_alias_by from alias.*_ailias
This commit is contained in:
dasha_uwu
2026-01-25 02:51:14 +05:00
parent 6014c0fd6c
commit 45f4496e4f
9 changed files with 132 additions and 400 deletions

View File

@@ -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

View File

@@ -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::<Vec<_>>();
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<OwnedRoomId> = Vec::new();
let mut room_ids: Vec<OwnedRoomId> = 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

View File

@@ -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?

View File

@@ -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 {

View File

@@ -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();
})

View File

@@ -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

View File

@@ -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;
};

View File

@@ -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);
}

View File

@@ -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();