From ebc8df1c4d4111ad11430be73311cf7cac2586e5 Mon Sep 17 00:00:00 2001 From: timedout Date: Sun, 18 Jan 2026 18:47:15 +0000 Subject: [PATCH] feat: Add endpoints required for API-based takedowns and room bans --- Cargo.lock | 37 ++++++---- Cargo.toml | 2 +- src/api/admin/mod.rs | 1 + src/api/admin/rooms/ban.rs | 132 ++++++++++++++++++++++++++++++++++++ src/api/admin/rooms/list.rs | 35 ++++++++++ src/api/admin/rooms/mod.rs | 2 + src/api/mod.rs | 5 +- src/api/router.rs | 6 +- 8 files changed, 202 insertions(+), 18 deletions(-) create mode 100644 src/api/admin/mod.rs create mode 100644 src/api/admin/rooms/ban.rs create mode 100644 src/api/admin/rooms/list.rs create mode 100644 src/api/admin/rooms/mod.rs diff --git a/Cargo.lock b/Cargo.lock index f3a955ad..3ed1a530 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1314,6 +1314,16 @@ dependencies = [ "typewit", ] +[[package]] +name = "continuwuity-admin-api" +version = "0.1.0" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=85d00fb5746cba23904234b4fd3c838dcf141541#85d00fb5746cba23904234b4fd3c838dcf141541" +dependencies = [ + "ruma-common", + "serde", + "serde_json", +] + [[package]] name = "convert_case" version = "0.10.0" @@ -1687,7 +1697,7 @@ dependencies = [ [[package]] name = "draupnir-antispam" version = "0.1.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=f9e74cb206cfa45cf5f17d39282253b43a15fcd5#f9e74cb206cfa45cf5f17d39282253b43a15fcd5" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=85d00fb5746cba23904234b4fd3c838dcf141541#85d00fb5746cba23904234b4fd3c838dcf141541" dependencies = [ "ruma-common", "serde", @@ -3035,7 +3045,7 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "meowlnir-antispam" version = "0.1.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=f9e74cb206cfa45cf5f17d39282253b43a15fcd5#f9e74cb206cfa45cf5f17d39282253b43a15fcd5" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=85d00fb5746cba23904234b4fd3c838dcf141541#85d00fb5746cba23904234b4fd3c838dcf141541" dependencies = [ "ruma-common", "serde", @@ -4250,9 +4260,10 @@ dependencies = [ [[package]] name = "ruma" version = "0.10.1" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=f9e74cb206cfa45cf5f17d39282253b43a15fcd5#f9e74cb206cfa45cf5f17d39282253b43a15fcd5" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=85d00fb5746cba23904234b4fd3c838dcf141541#85d00fb5746cba23904234b4fd3c838dcf141541" dependencies = [ "assign", + "continuwuity-admin-api", "draupnir-antispam", "js_int", "js_option", @@ -4272,7 +4283,7 @@ dependencies = [ [[package]] name = "ruma-appservice-api" version = "0.10.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=f9e74cb206cfa45cf5f17d39282253b43a15fcd5#f9e74cb206cfa45cf5f17d39282253b43a15fcd5" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=85d00fb5746cba23904234b4fd3c838dcf141541#85d00fb5746cba23904234b4fd3c838dcf141541" dependencies = [ "js_int", "ruma-common", @@ -4284,7 +4295,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.18.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=f9e74cb206cfa45cf5f17d39282253b43a15fcd5#f9e74cb206cfa45cf5f17d39282253b43a15fcd5" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=85d00fb5746cba23904234b4fd3c838dcf141541#85d00fb5746cba23904234b4fd3c838dcf141541" dependencies = [ "as_variant", "assign", @@ -4307,7 +4318,7 @@ dependencies = [ [[package]] name = "ruma-common" version = "0.13.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=f9e74cb206cfa45cf5f17d39282253b43a15fcd5#f9e74cb206cfa45cf5f17d39282253b43a15fcd5" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=85d00fb5746cba23904234b4fd3c838dcf141541#85d00fb5746cba23904234b4fd3c838dcf141541" dependencies = [ "as_variant", "base64 0.22.1", @@ -4339,7 +4350,7 @@ dependencies = [ [[package]] name = "ruma-events" version = "0.28.1" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=f9e74cb206cfa45cf5f17d39282253b43a15fcd5#f9e74cb206cfa45cf5f17d39282253b43a15fcd5" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=85d00fb5746cba23904234b4fd3c838dcf141541#85d00fb5746cba23904234b4fd3c838dcf141541" dependencies = [ "as_variant", "indexmap", @@ -4364,7 +4375,7 @@ dependencies = [ [[package]] name = "ruma-federation-api" version = "0.9.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=f9e74cb206cfa45cf5f17d39282253b43a15fcd5#f9e74cb206cfa45cf5f17d39282253b43a15fcd5" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=85d00fb5746cba23904234b4fd3c838dcf141541#85d00fb5746cba23904234b4fd3c838dcf141541" dependencies = [ "bytes", "headers", @@ -4386,7 +4397,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.9.5" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=f9e74cb206cfa45cf5f17d39282253b43a15fcd5#f9e74cb206cfa45cf5f17d39282253b43a15fcd5" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=85d00fb5746cba23904234b4fd3c838dcf141541#85d00fb5746cba23904234b4fd3c838dcf141541" dependencies = [ "js_int", "thiserror 2.0.17", @@ -4395,7 +4406,7 @@ dependencies = [ [[package]] name = "ruma-identity-service-api" version = "0.9.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=f9e74cb206cfa45cf5f17d39282253b43a15fcd5#f9e74cb206cfa45cf5f17d39282253b43a15fcd5" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=85d00fb5746cba23904234b4fd3c838dcf141541#85d00fb5746cba23904234b4fd3c838dcf141541" dependencies = [ "js_int", "ruma-common", @@ -4405,7 +4416,7 @@ dependencies = [ [[package]] name = "ruma-macros" version = "0.13.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=f9e74cb206cfa45cf5f17d39282253b43a15fcd5#f9e74cb206cfa45cf5f17d39282253b43a15fcd5" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=85d00fb5746cba23904234b4fd3c838dcf141541#85d00fb5746cba23904234b4fd3c838dcf141541" dependencies = [ "cfg-if", "proc-macro-crate", @@ -4420,7 +4431,7 @@ dependencies = [ [[package]] name = "ruma-push-gateway-api" version = "0.9.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=f9e74cb206cfa45cf5f17d39282253b43a15fcd5#f9e74cb206cfa45cf5f17d39282253b43a15fcd5" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=85d00fb5746cba23904234b4fd3c838dcf141541#85d00fb5746cba23904234b4fd3c838dcf141541" dependencies = [ "js_int", "ruma-common", @@ -4432,7 +4443,7 @@ dependencies = [ [[package]] name = "ruma-signatures" version = "0.15.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=f9e74cb206cfa45cf5f17d39282253b43a15fcd5#f9e74cb206cfa45cf5f17d39282253b43a15fcd5" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=85d00fb5746cba23904234b4fd3c838dcf141541#85d00fb5746cba23904234b4fd3c838dcf141541" dependencies = [ "base64 0.22.1", "ed25519-dalek", diff --git a/Cargo.toml b/Cargo.toml index 9c773e3a..38def82f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -342,7 +342,7 @@ version = "0.1.2" # Used for matrix spec type definitions and helpers [workspace.dependencies.ruma] git = "https://forgejo.ellis.link/continuwuation/ruwuma" -rev = "f9e74cb206cfa45cf5f17d39282253b43a15fcd5" +rev = "85d00fb5746cba23904234b4fd3c838dcf141541" features = [ "compat", "rand", diff --git a/src/api/admin/mod.rs b/src/api/admin/mod.rs new file mode 100644 index 00000000..5d505fa8 --- /dev/null +++ b/src/api/admin/mod.rs @@ -0,0 +1 @@ +pub mod rooms; diff --git a/src/api/admin/rooms/ban.rs b/src/api/admin/rooms/ban.rs new file mode 100644 index 00000000..4c6d3c85 --- /dev/null +++ b/src/api/admin/rooms/ban.rs @@ -0,0 +1,132 @@ +use axum::extract::State; +use conduwuit::{Err, Result, info, utils::ReadyExt, warn}; +use futures::{FutureExt, StreamExt}; +use ruma::{ + OwnedRoomAliasId, continuwuity_admin_api::rooms, + events::room::message::RoomMessageEventContent, +}; + +use crate::{Ruma, client::leave_room}; + +/// # `PUT /_continuwuity/admin/rooms/{roomID}/ban` +/// +/// Bans or unbans a room. +pub(crate) async fn ban_room( + State(services): State, + body: Ruma, +) -> Result { + let sender_user = body.sender_user(); + if !services.users.is_admin(sender_user).await { + return Err!(Request(Forbidden("Only server administrators can use this endpoint"))); + } + + if body.banned { + // Don't ban again if already banned + if services.rooms.metadata.is_banned(&body.room_id).await { + return Err!(Request(InvalidParam("Room is already banned"))); + } + info!(%sender_user, "Banning room {}", body.room_id); + + services + .admin + .notice(&format!("{sender_user} banned {} (ban in progress)", body.room_id)) + .await; + + let mut users = services + .rooms + .state_cache + .room_members(&body.room_id) + .map(ToOwned::to_owned) + .ready_filter(|user| services.globals.user_is_local(user)) + .boxed(); + let mut evicted = Vec::new(); + let mut failed_evicted = Vec::new(); + + while let Some(ref user_id) = users.next().await { + info!("Evicting user {} from room {}", user_id, body.room_id); + match leave_room(&services, user_id, &body.room_id, None) + .boxed() + .await + { + | Ok(()) => { + services.rooms.state_cache.forget(&body.room_id, user_id); + evicted.push(user_id.clone()); + }, + | Err(e) => { + warn!("Failed to evict user {} from room {}: {}", user_id, body.room_id, e); + failed_evicted.push(user_id.clone()); + }, + } + } + + let aliases: Vec = services + .rooms + .alias + .local_aliases_for_room(&body.room_id) + .map(ToOwned::to_owned) + .collect::>() + .await; + for alias in &aliases { + info!("Removing alias {} for banned room {}", alias, body.room_id); + services + .rooms + .alias + .remove_alias(alias, &services.globals.server_user) + .await?; + } + + services.rooms.directory.set_not_public(&body.room_id); // remove from the room directory + services.rooms.metadata.ban_room(&body.room_id, true); // prevent further joins + services.rooms.metadata.disable_room(&body.room_id, true); // disable federation + + services + .admin + .notice(&format!( + "Finished banning {}: Removed {} users ({} failed) and {} aliases", + body.room_id, + evicted.len(), + failed_evicted.len(), + aliases.len() + )) + .await; + if !evicted.is_empty() || !failed_evicted.is_empty() || !aliases.is_empty() { + let msg = services + .admin + .text_or_file(RoomMessageEventContent::text_markdown(format!( + "Removed users:\n{}\n\nFailed to remove users:\n{}\n\nRemoved aliases: {}", + evicted + .iter() + .map(|u| u.as_str()) + .collect::>() + .join("\n"), + failed_evicted + .iter() + .map(|u| u.as_str()) + .collect::>() + .join("\n"), + aliases + .iter() + .map(|a| a.as_str()) + .collect::>() + .join(", "), + ))) + .await; + services.admin.send_message(msg).await.ok(); + } + + Ok(rooms::ban::v1::Response::new(evicted, failed_evicted, aliases)) + } else { + // Don't unban if not banned + if !services.rooms.metadata.is_banned(&body.room_id).await { + return Err!(Request(InvalidParam("Room is not banned"))); + } + info!(%sender_user, "Unbanning room {}", body.room_id); + services.rooms.metadata.disable_room(&body.room_id, false); + services.rooms.metadata.ban_room(&body.room_id, false); + services + .admin + .notice(&format!("{sender_user} unbanned {}", body.room_id)) + .await; + Ok(rooms::ban::v1::Response::new(Vec::new(), Vec::new(), Vec::new())) + } +} diff --git a/src/api/admin/rooms/list.rs b/src/api/admin/rooms/list.rs new file mode 100644 index 00000000..c05dba4a --- /dev/null +++ b/src/api/admin/rooms/list.rs @@ -0,0 +1,35 @@ +use axum::extract::State; +use conduwuit::{Err, Result}; +use futures::StreamExt; +use ruma::{OwnedRoomId, continuwuity_admin_api::rooms}; + +use crate::Ruma; + +/// # `GET /_continuwuity/admin/rooms/list` +/// +/// Lists all rooms known to this server, excluding banned ones. +pub(crate) async fn list_rooms( + State(services): State, + body: Ruma, +) -> Result { + let sender_user = body.sender_user(); + if !services.users.is_admin(sender_user).await { + return Err!(Request(Forbidden("Only server administrators can use this endpoint"))); + } + + let mut rooms: Vec = services + .rooms + .metadata + .iter_ids() + .filter_map(|room_id| async move { + if !services.rooms.metadata.is_banned(room_id).await { + Some(room_id.to_owned()) + } else { + None + } + }) + .collect() + .await; + rooms.sort(); + Ok(rooms::list::v1::Response::new(rooms)) +} diff --git a/src/api/admin/rooms/mod.rs b/src/api/admin/rooms/mod.rs new file mode 100644 index 00000000..accb6f6a --- /dev/null +++ b/src/api/admin/rooms/mod.rs @@ -0,0 +1,2 @@ +pub mod ban; +pub mod list; diff --git a/src/api/mod.rs b/src/api/mod.rs index 9ca24e72..a2be1c79 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,12 +1,13 @@ #![type_length_limit = "16384"] //TODO: reduce me #![allow(clippy::toplevel_ref_arg)] +extern crate conduwuit_core as conduwuit; +extern crate conduwuit_service as service; pub mod client; pub mod router; pub mod server; -extern crate conduwuit_core as conduwuit; -extern crate conduwuit_service as service; +pub mod admin; pub(crate) use self::router::{Ruma, RumaResponse, State}; diff --git a/src/api/router.rs b/src/api/router.rs index 7d4e7118..133a3455 100644 --- a/src/api/router.rs +++ b/src/api/router.rs @@ -17,7 +17,7 @@ use http::{Uri, uri}; use self::handler::RouterExt; pub(super) use self::{args::Args as Ruma, response::RumaResponse}; -use crate::{client, server}; +use crate::{admin, client, server}; pub fn build(router: Router, server: &Server) -> Router { let config = &server.config; @@ -187,7 +187,9 @@ pub fn build(router: Router, server: &Server) -> Router { .route("/_conduwuit/server_version", get(client::conduwuit_server_version)) .route("/_continuwuity/server_version", get(client::conduwuit_server_version)) .ruma_route(&client::room_initial_sync_route) - .route("/client/server.json", get(client::syncv3_client_server_json)); + .route("/client/server.json", get(client::syncv3_client_server_json)) + .ruma_route(&admin::rooms::ban::ban_room) + .ruma_route(&admin::rooms::list::list_rooms); if config.allow_federation { router = router