mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2025-07-05 19:55:49 +02:00
Some checks failed
Documentation / Build and Deploy Documentation (push) Has been skipped
Checks / Prefligit / prefligit (push) Failing after 2s
Release Docker Image / define-variables (push) Failing after 1s
Release Docker Image / build-image (linux/amd64, release, linux-amd64, base) (push) Has been skipped
Release Docker Image / build-image (linux/arm64, release, linux-arm64, base) (push) Has been skipped
Release Docker Image / merge (push) Has been skipped
Checks / Rust / Format (push) Failing after 1s
Checks / Rust / Clippy (push) Failing after 29s
Checks / Rust / Cargo Test (push) Failing after 28s
Fixes the issue where room bans prevent federating leave events, resulting in local users being stuck in remote rooms
464 lines
12 KiB
Rust
464 lines
12 KiB
Rust
use api::client::leave_room;
|
|
use clap::Subcommand;
|
|
use conduwuit::{
|
|
Err, Result, debug, info,
|
|
utils::{IterStream, ReadyExt},
|
|
warn,
|
|
};
|
|
use futures::StreamExt;
|
|
use ruma::{OwnedRoomId, OwnedRoomOrAliasId, RoomAliasId, RoomId, RoomOrAliasId};
|
|
|
|
use crate::{admin_command, admin_command_dispatch, get_room_info};
|
|
|
|
#[admin_command_dispatch]
|
|
#[derive(Debug, Subcommand)]
|
|
pub(crate) enum RoomModerationCommand {
|
|
/// - Bans a room from local users joining and evicts all our local users
|
|
/// (including server
|
|
/// admins)
|
|
/// from the room. Also blocks any invites (local and remote) for the
|
|
/// banned room, and disables federation entirely with it.
|
|
BanRoom {
|
|
/// The room in the format of `!roomid:example.com` or a room alias in
|
|
/// the format of `#roomalias:example.com`
|
|
room: OwnedRoomOrAliasId,
|
|
},
|
|
|
|
/// - Bans a list of rooms (room IDs and room aliases) from a newline
|
|
/// delimited codeblock similar to `user deactivate-all`. Applies the same
|
|
/// steps as ban-room
|
|
BanListOfRooms,
|
|
|
|
/// - Unbans a room to allow local users to join again
|
|
UnbanRoom {
|
|
/// The room in the format of `!roomid:example.com` or a room alias in
|
|
/// the format of `#roomalias:example.com`
|
|
room: OwnedRoomOrAliasId,
|
|
},
|
|
|
|
/// - List of all rooms we have banned
|
|
ListBannedRooms {
|
|
#[arg(long)]
|
|
/// Whether to only output room IDs without supplementary room
|
|
/// information
|
|
no_details: bool,
|
|
},
|
|
}
|
|
|
|
#[admin_command]
|
|
async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
|
|
debug!("Got room alias or ID: {}", room);
|
|
|
|
let admin_room_alias = &self.services.globals.admin_alias;
|
|
|
|
if let Ok(admin_room_id) = self.services.admin.get_admin_room().await {
|
|
if room.to_string().eq(&admin_room_id) || room.to_string().eq(admin_room_alias) {
|
|
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}"
|
|
);
|
|
},
|
|
};
|
|
|
|
debug!("Room specified is a room ID, banning 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"
|
|
);
|
|
|
|
match self
|
|
.services
|
|
.rooms
|
|
.alias
|
|
.resolve_alias(room_alias, None)
|
|
.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}");
|
|
},
|
|
}
|
|
} 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`)",
|
|
);
|
|
};
|
|
|
|
info!("Making all users leave the room {room_id} and forgetting it");
|
|
let mut users = self
|
|
.services
|
|
.rooms
|
|
.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 {
|
|
info!(
|
|
"Attempting leave for user {user_id} in room {room_id} (ignoring all errors, \
|
|
evicting admins too)",
|
|
);
|
|
|
|
if let Err(e) = leave_room(self.services, user_id, &room_id, None).await {
|
|
warn!("Failed to leave room: {e}");
|
|
}
|
|
|
|
self.services.rooms.state_cache.forget(&room_id, user_id);
|
|
}
|
|
|
|
self.services
|
|
.rooms
|
|
.alias
|
|
.local_aliases_for_room(&room_id)
|
|
.map(ToOwned::to_owned)
|
|
.for_each(|local_alias| async move {
|
|
self.services
|
|
.rooms
|
|
.alias
|
|
.remove_alias(&local_alias, &self.services.globals.server_user)
|
|
.await
|
|
.ok();
|
|
})
|
|
.await;
|
|
|
|
self.services.rooms.directory.set_not_public(&room_id); // remove from the room directory
|
|
self.services.rooms.metadata.ban_room(&room_id, true); // prevent further joins
|
|
self.services.rooms.metadata.disable_room(&room_id, true); // disable federation
|
|
|
|
self.write_str(
|
|
"Room banned, removed all our local users, and disabled incoming federation with room.",
|
|
)
|
|
.await
|
|
}
|
|
|
|
#[admin_command]
|
|
async fn ban_list_of_rooms(&self) -> Result {
|
|
if self.body.len() < 2
|
|
|| !self.body[0].trim().starts_with("```")
|
|
|| self.body.last().unwrap_or(&"").trim() != "```"
|
|
{
|
|
return Err!("Expected code block in command body. Add --help for details.",);
|
|
}
|
|
|
|
let rooms_s = self
|
|
.body
|
|
.to_vec()
|
|
.drain(1..self.body.len().saturating_sub(1))
|
|
.collect::<Vec<_>>();
|
|
|
|
let admin_room_alias = &self.services.globals.admin_alias;
|
|
|
|
let mut room_ban_count: usize = 0;
|
|
let mut room_ids: Vec<OwnedRoomId> = Vec::new();
|
|
|
|
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 {
|
|
if 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
|
|
.rooms
|
|
.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
|
|
.rooms
|
|
.alias
|
|
.resolve_alias(room_alias, None)
|
|
.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;
|
|
},
|
|
}
|
|
}
|
|
},
|
|
| Err(e) => {
|
|
warn!(
|
|
"Error parsing room \"{room}\" during bulk room banning, ignoring error and \
|
|
logging here: {e}"
|
|
);
|
|
continue;
|
|
},
|
|
}
|
|
}
|
|
|
|
for room_id in room_ids {
|
|
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
|
|
.rooms
|
|
.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)",
|
|
);
|
|
|
|
if let Err(e) = leave_room(self.services, user_id, &room_id, None).await {
|
|
warn!("Failed to leave room: {e}");
|
|
}
|
|
|
|
self.services.rooms.state_cache.forget(&room_id, user_id);
|
|
}
|
|
|
|
// remove any local aliases, ignore errors
|
|
self.services
|
|
.rooms
|
|
.alias
|
|
.local_aliases_for_room(&room_id)
|
|
.map(ToOwned::to_owned)
|
|
.for_each(|local_alias| async move {
|
|
self.services
|
|
.rooms
|
|
.alias
|
|
.remove_alias(&local_alias, &self.services.globals.server_user)
|
|
.await
|
|
.ok();
|
|
})
|
|
.await;
|
|
|
|
self.services.rooms.metadata.ban_room(&room_id, true);
|
|
// unpublish from room directory, ignore errors
|
|
self.services.rooms.directory.set_not_public(&room_id);
|
|
self.services.rooms.metadata.disable_room(&room_id, true);
|
|
}
|
|
|
|
self.write_str(&format!(
|
|
"Finished bulk room ban, banned {room_ban_count} total rooms, evicted all users, and \
|
|
disabled incoming federation with the room."
|
|
))
|
|
.await
|
|
}
|
|
|
|
#[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.rooms.metadata.ban_room(room_id, false);
|
|
|
|
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
|
|
.rooms
|
|
.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
|
|
.rooms
|
|
.alias
|
|
.resolve_alias(room_alias, None)
|
|
.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.rooms.metadata.ban_room(&room_id, false);
|
|
|
|
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`)",
|
|
);
|
|
};
|
|
|
|
self.services.rooms.metadata.disable_room(&room_id, false);
|
|
self.write_str("Room unbanned and federation re-enabled.")
|
|
.await
|
|
}
|
|
|
|
#[admin_command]
|
|
async fn list_banned_rooms(&self, no_details: bool) -> Result {
|
|
let room_ids: Vec<OwnedRoomId> = self
|
|
.services
|
|
.rooms
|
|
.metadata
|
|
.list_banned_rooms()
|
|
.map(Into::into)
|
|
.collect()
|
|
.await;
|
|
|
|
if room_ids.is_empty() {
|
|
return Err!("No rooms are banned.");
|
|
}
|
|
|
|
let mut rooms = room_ids
|
|
.iter()
|
|
.stream()
|
|
.then(|room_id| get_room_info(self.services, room_id))
|
|
.collect::<Vec<_>>()
|
|
.await;
|
|
|
|
rooms.sort_by_key(|r| r.1);
|
|
rooms.reverse();
|
|
|
|
let num = rooms.len();
|
|
|
|
let body = rooms
|
|
.iter()
|
|
.map(|(id, members, name)| {
|
|
if no_details {
|
|
format!("{id}")
|
|
} else {
|
|
format!("{id}\tMembers: {members}\tName: {name}")
|
|
}
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
|
|
self.write_str(&format!("Rooms Banned ({num}):\n```\n{body}\n```",))
|
|
.await
|
|
}
|