From 893707d501049a5894d6e017b54e49f9a1f7293b Mon Sep 17 00:00:00 2001 From: strawberry Date: Sat, 20 Apr 2024 19:55:14 -0400 Subject: [PATCH] finish general admin room cleanup Signed-off-by: strawberry --- src/service/admin/federation.rs | 172 -------------- .../admin/federation/federation_commands.rs | 126 ++++++++++ src/service/admin/federation/mod.rs | 68 ++++++ src/service/admin/media.rs | 216 ------------------ src/service/admin/media/media_commands.rs | 171 ++++++++++++++ src/service/admin/media/mod.rs | 49 ++++ src/service/admin/mod.rs | 3 - src/service/admin/query/account_data.rs | 32 +-- src/service/admin/query/appservice.rs | 13 +- src/service/admin/query/globals.rs | 23 +- src/service/admin/query/mod.rs | 114 ++++++++- src/service/admin/query/presence.rs | 22 +- src/service/admin/query/room_alias.rs | 23 +- src/service/admin/room.rs | 96 -------- src/service/admin/room/mod.rs | 160 +++++++++++++ .../room_alias_commands.rs} | 40 +--- src/service/admin/room/room_commands.rs | 59 +++++ .../room_directory_commands.rs} | 29 +-- .../room_moderation_commands.rs} | 60 +---- 19 files changed, 751 insertions(+), 725 deletions(-) delete mode 100644 src/service/admin/federation.rs create mode 100644 src/service/admin/federation/federation_commands.rs create mode 100644 src/service/admin/federation/mod.rs delete mode 100644 src/service/admin/media.rs create mode 100644 src/service/admin/media/media_commands.rs create mode 100644 src/service/admin/media/mod.rs delete mode 100644 src/service/admin/room.rs create mode 100644 src/service/admin/room/mod.rs rename src/service/admin/{room_alias.rs => room/room_alias_commands.rs} (84%) create mode 100644 src/service/admin/room/room_commands.rs rename src/service/admin/{room_directory.rs => room/room_directory_commands.rs} (80%) rename src/service/admin/{room_moderation.rs => room/room_moderation_commands.rs} (90%) diff --git a/src/service/admin/federation.rs b/src/service/admin/federation.rs deleted file mode 100644 index c7a61103..00000000 --- a/src/service/admin/federation.rs +++ /dev/null @@ -1,172 +0,0 @@ -use std::{collections::BTreeMap, fmt::Write as _}; - -use clap::Subcommand; -use ruma::{events::room::message::RoomMessageEventContent, RoomId, ServerName}; -use tokio::sync::RwLock; - -use crate::{services, utils::HtmlEscape, Result}; - -#[cfg_attr(test, derive(Debug))] -#[derive(Subcommand)] -pub(crate) enum FederationCommand { - /// - List all rooms we are currently handling an incoming pdu from - IncomingFederation, - - /// - Disables incoming federation handling for a room. - DisableRoom { - room_id: Box, - }, - - /// - Enables incoming federation handling for a room again. - EnableRoom { - room_id: Box, - }, - - /// - Verify json signatures - /// - /// This command needs a JSON blob provided in a Markdown code block below - /// the command. - SignJson, - - /// - Verify json signatures - /// - /// This command needs a JSON blob provided in a Markdown code block below - /// the command. - VerifyJson, - - /// - Fetch `/.well-known/matrix/support` from the specified server - /// - /// Despite the name, this is not a federation endpoint and does not go - /// through the federation / server resolution process as per-spec this is - /// supposed to be served at the server_name. - /// - /// Respecting homeservers put this file here for listing administration, - /// moderation, and security inquiries. This command provides a way to - /// easily fetch that information. - FetchSupportWellKnown { - server_name: Box, - }, -} - -pub(crate) async fn process(command: FederationCommand, body: Vec<&str>) -> Result { - match command { - FederationCommand::DisableRoom { - room_id, - } => { - services().rooms.metadata.disable_room(&room_id, true)?; - Ok(RoomMessageEventContent::text_plain("Room disabled.")) - }, - FederationCommand::EnableRoom { - room_id, - } => { - services().rooms.metadata.disable_room(&room_id, false)?; - Ok(RoomMessageEventContent::text_plain("Room enabled.")) - }, - FederationCommand::IncomingFederation => { - let map = services().globals.roomid_federationhandletime.read().await; - let mut msg = format!("Handling {} incoming pdus:\n", map.len()); - - for (r, (e, i)) in map.iter() { - let elapsed = i.elapsed(); - let _ = writeln!(msg, "{} {}: {}m{}s", r, e, elapsed.as_secs() / 60, elapsed.as_secs() % 60); - } - Ok(RoomMessageEventContent::text_plain(&msg)) - }, - FederationCommand::SignJson => { - if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" { - let string = body[1..body.len() - 1].join("\n"); - match serde_json::from_str(&string) { - Ok(mut value) => { - ruma::signatures::sign_json( - services().globals.server_name().as_str(), - services().globals.keypair(), - &mut value, - ) - .expect("our request json is what ruma expects"); - let json_text = serde_json::to_string_pretty(&value).expect("canonical json is valid json"); - Ok(RoomMessageEventContent::text_plain(json_text)) - }, - Err(e) => Ok(RoomMessageEventContent::text_plain(format!("Invalid json: {e}"))), - } - } else { - Ok(RoomMessageEventContent::text_plain( - "Expected code block in command body. Add --help for details.", - )) - } - }, - FederationCommand::VerifyJson => { - if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" { - let string = body[1..body.len() - 1].join("\n"); - match serde_json::from_str(&string) { - Ok(value) => { - let pub_key_map = RwLock::new(BTreeMap::new()); - - services() - .rooms - .event_handler - .fetch_required_signing_keys([&value], &pub_key_map) - .await?; - - let pub_key_map = pub_key_map.read().await; - match ruma::signatures::verify_json(&pub_key_map, &value) { - Ok(()) => Ok(RoomMessageEventContent::text_plain("Signature correct")), - Err(e) => Ok(RoomMessageEventContent::text_plain(format!( - "Signature verification failed: {e}" - ))), - } - }, - Err(e) => Ok(RoomMessageEventContent::text_plain(format!("Invalid json: {e}"))), - } - } else { - Ok(RoomMessageEventContent::text_plain( - "Expected code block in command body. Add --help for details.", - )) - } - }, - FederationCommand::FetchSupportWellKnown { - server_name, - } => { - let response = services() - .globals - .client - .default - .get(format!("https://{server_name}/.well-known/matrix/support")) - .send() - .await?; - - let text = response.text().await?; - - if text.is_empty() { - return Ok(RoomMessageEventContent::text_plain("Response text/body is empty.")); - } - - if text.len() > 1500 { - return Ok(RoomMessageEventContent::text_plain( - "Response text/body is over 1500 characters, assuming no support well-known.", - )); - } - - let json: serde_json::Value = match serde_json::from_str(&text) { - Ok(json) => json, - Err(_) => { - return Ok(RoomMessageEventContent::text_plain("Response text/body is not valid JSON.")); - }, - }; - - let pretty_json: String = match serde_json::to_string_pretty(&json) { - Ok(json) => json, - Err(_) => { - return Ok(RoomMessageEventContent::text_plain("Response text/body is not valid JSON.")); - }, - }; - - Ok(RoomMessageEventContent::text_html( - format!("Got JSON response:\n\n```json\n{pretty_json}\n```"), - format!( - "

Got JSON response:

\n
{}\n
\n", - HtmlEscape(&pretty_json) - ), - )) - }, - } -} diff --git a/src/service/admin/federation/federation_commands.rs b/src/service/admin/federation/federation_commands.rs new file mode 100644 index 00000000..845c2f91 --- /dev/null +++ b/src/service/admin/federation/federation_commands.rs @@ -0,0 +1,126 @@ +use std::{collections::BTreeMap, fmt::Write as _}; + +use ruma::{events::room::message::RoomMessageEventContent, RoomId, ServerName}; +use tokio::sync::RwLock; + +use crate::{services, utils::HtmlEscape, Result}; + +pub(super) async fn disable_room(_body: Vec<&str>, room_id: Box) -> Result { + services().rooms.metadata.disable_room(&room_id, true)?; + Ok(RoomMessageEventContent::text_plain("Room disabled.")) +} + +pub(super) async fn enable_room(_body: Vec<&str>, room_id: Box) -> Result { + services().rooms.metadata.disable_room(&room_id, false)?; + Ok(RoomMessageEventContent::text_plain("Room enabled.")) +} + +pub(super) async fn incoming_federeation(_body: Vec<&str>) -> Result { + let map = services().globals.roomid_federationhandletime.read().await; + let mut msg = format!("Handling {} incoming pdus:\n", map.len()); + + for (r, (e, i)) in map.iter() { + let elapsed = i.elapsed(); + let _ = writeln!(msg, "{} {}: {}m{}s", r, e, elapsed.as_secs() / 60, elapsed.as_secs() % 60); + } + Ok(RoomMessageEventContent::text_plain(&msg)) +} + +pub(super) async fn sign_json(body: Vec<&str>) -> Result { + if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" { + let string = body[1..body.len() - 1].join("\n"); + match serde_json::from_str(&string) { + Ok(mut value) => { + ruma::signatures::sign_json( + services().globals.server_name().as_str(), + services().globals.keypair(), + &mut value, + ) + .expect("our request json is what ruma expects"); + let json_text = serde_json::to_string_pretty(&value).expect("canonical json is valid json"); + Ok(RoomMessageEventContent::text_plain(json_text)) + }, + Err(e) => Ok(RoomMessageEventContent::text_plain(format!("Invalid json: {e}"))), + } + } else { + Ok(RoomMessageEventContent::text_plain( + "Expected code block in command body. Add --help for details.", + )) + } +} + +pub(super) async fn verify_json(body: Vec<&str>) -> Result { + if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" { + let string = body[1..body.len() - 1].join("\n"); + match serde_json::from_str(&string) { + Ok(value) => { + let pub_key_map = RwLock::new(BTreeMap::new()); + + services() + .rooms + .event_handler + .fetch_required_signing_keys([&value], &pub_key_map) + .await?; + + let pub_key_map = pub_key_map.read().await; + match ruma::signatures::verify_json(&pub_key_map, &value) { + Ok(()) => Ok(RoomMessageEventContent::text_plain("Signature correct")), + Err(e) => Ok(RoomMessageEventContent::text_plain(format!( + "Signature verification failed: {e}" + ))), + } + }, + Err(e) => Ok(RoomMessageEventContent::text_plain(format!("Invalid json: {e}"))), + } + } else { + Ok(RoomMessageEventContent::text_plain( + "Expected code block in command body. Add --help for details.", + )) + } +} + +pub(super) async fn fetch_support_well_known( + _body: Vec<&str>, server_name: Box, +) -> Result { + let response = services() + .globals + .client + .default + .get(format!("https://{server_name}/.well-known/matrix/support")) + .send() + .await?; + + let text = response.text().await?; + + if text.is_empty() { + return Ok(RoomMessageEventContent::text_plain("Response text/body is empty.")); + } + + if text.len() > 1500 { + return Ok(RoomMessageEventContent::text_plain( + "Response text/body is over 1500 characters, assuming no support well-known.", + )); + } + + let json: serde_json::Value = match serde_json::from_str(&text) { + Ok(json) => json, + Err(_) => { + return Ok(RoomMessageEventContent::text_plain("Response text/body is not valid JSON.")); + }, + }; + + let pretty_json: String = match serde_json::to_string_pretty(&json) { + Ok(json) => json, + Err(_) => { + return Ok(RoomMessageEventContent::text_plain("Response text/body is not valid JSON.")); + }, + }; + + Ok(RoomMessageEventContent::text_html( + format!("Got JSON response:\n\n```json\n{pretty_json}\n```"), + format!( + "

Got JSON response:

\n
{}\n
\n", + HtmlEscape(&pretty_json) + ), + )) +} diff --git a/src/service/admin/federation/mod.rs b/src/service/admin/federation/mod.rs new file mode 100644 index 00000000..74878e36 --- /dev/null +++ b/src/service/admin/federation/mod.rs @@ -0,0 +1,68 @@ +use clap::Subcommand; +use ruma::{events::room::message::RoomMessageEventContent, RoomId, ServerName}; + +use self::federation_commands::{ + disable_room, enable_room, fetch_support_well_known, incoming_federeation, sign_json, verify_json, +}; +use crate::Result; + +pub(crate) mod federation_commands; + +#[cfg_attr(test, derive(Debug))] +#[derive(Subcommand)] +pub(crate) enum FederationCommand { + /// - List all rooms we are currently handling an incoming pdu from + IncomingFederation, + + /// - Disables incoming federation handling for a room. + DisableRoom { + room_id: Box, + }, + + /// - Enables incoming federation handling for a room again. + EnableRoom { + room_id: Box, + }, + + /// - Verify json signatures + /// + /// This command needs a JSON blob provided in a Markdown code block below + /// the command. + SignJson, + + /// - Verify json signatures + /// + /// This command needs a JSON blob provided in a Markdown code block below + /// the command. + VerifyJson, + + /// - Fetch `/.well-known/matrix/support` from the specified server + /// + /// Despite the name, this is not a federation endpoint and does not go + /// through the federation / server resolution process as per-spec this is + /// supposed to be served at the server_name. + /// + /// Respecting homeservers put this file here for listing administration, + /// moderation, and security inquiries. This command provides a way to + /// easily fetch that information. + FetchSupportWellKnown { + server_name: Box, + }, +} + +pub(crate) async fn process(command: FederationCommand, body: Vec<&str>) -> Result { + Ok(match command { + FederationCommand::DisableRoom { + room_id, + } => disable_room(body, room_id).await?, + FederationCommand::EnableRoom { + room_id, + } => enable_room(body, room_id).await?, + FederationCommand::IncomingFederation => incoming_federeation(body).await?, + FederationCommand::SignJson => sign_json(body).await?, + FederationCommand::VerifyJson => verify_json(body).await?, + FederationCommand::FetchSupportWellKnown { + server_name, + } => fetch_support_well_known(body, server_name).await?, + }) +} diff --git a/src/service/admin/media.rs b/src/service/admin/media.rs deleted file mode 100644 index ee86401e..00000000 --- a/src/service/admin/media.rs +++ /dev/null @@ -1,216 +0,0 @@ -use clap::Subcommand; -use ruma::{events::room::message::RoomMessageEventContent, EventId}; -use tracing::{debug, info}; - -use crate::{service::admin::MxcUri, services, Result}; - -#[cfg_attr(test, derive(Debug))] -#[derive(Subcommand)] -pub(crate) enum MediaCommand { - /// - Deletes a single media file from our database and on the filesystem - /// via a single MXC URL - Delete { - /// The MXC URL to delete - #[arg(long)] - mxc: Option>, - - /// - The message event ID which contains the media and thumbnail MXC - /// URLs - #[arg(long)] - event_id: Option>, - }, - - /// - Deletes a codeblock list of MXC URLs from our database and on the - /// filesystem - DeleteList, - - /// - Deletes all remote media in the last X amount of time using filesystem - /// metadata first created at date. - DeletePastRemoteMedia { - /// - The duration (at or after), e.g. "5m" to delete all media in the - /// past 5 minutes - duration: String, - }, -} - -pub(crate) async fn process(command: MediaCommand, body: Vec<&str>) -> Result { - match command { - MediaCommand::Delete { - mxc, - event_id, - } => { - if event_id.is_some() && mxc.is_some() { - return Ok(RoomMessageEventContent::text_plain( - "Please specify either an MXC or an event ID, not both.", - )); - } - - if let Some(mxc) = mxc { - if !mxc.to_string().starts_with("mxc://") { - return Ok(RoomMessageEventContent::text_plain("MXC provided is not valid.")); - } - - debug!("Got MXC URL: {}", mxc); - services().media.delete(mxc.to_string()).await?; - - return Ok(RoomMessageEventContent::text_plain( - "Deleted the MXC from our database and on our filesystem.", - )); - } else if let Some(event_id) = event_id { - debug!("Got event ID to delete media from: {}", event_id); - - let mut mxc_urls = vec![]; - let mut mxc_deletion_count = 0; - - // parsing the PDU for any MXC URLs begins here - if let Some(event_json) = services().rooms.timeline.get_pdu_json(&event_id)? { - if let Some(content_key) = event_json.get("content") { - debug!("Event ID has \"content\"."); - let content_obj = content_key.as_object(); - - if let Some(content) = content_obj { - // 1. attempts to parse the "url" key - debug!("Attempting to go into \"url\" key for main media file"); - if let Some(url) = content.get("url") { - debug!("Got a URL in the event ID {event_id}: {url}"); - - if url.to_string().starts_with("\"mxc://") { - debug!("Pushing URL {} to list of MXCs to delete", url); - let final_url = url.to_string().replace('"', ""); - mxc_urls.push(final_url); - } else { - info!( - "Found a URL in the event ID {event_id} but did not start with mxc://, \ - ignoring" - ); - } - } - - // 2. attempts to parse the "info" key - debug!("Attempting to go into \"info\" key for thumbnails"); - if let Some(info_key) = content.get("info") { - debug!("Event ID has \"info\"."); - let info_obj = info_key.as_object(); - - if let Some(info) = info_obj { - if let Some(thumbnail_url) = info.get("thumbnail_url") { - debug!("Found a thumbnail_url in info key: {thumbnail_url}"); - - if thumbnail_url.to_string().starts_with("\"mxc://") { - debug!("Pushing thumbnail URL {} to list of MXCs to delete", thumbnail_url); - let final_thumbnail_url = thumbnail_url.to_string().replace('"', ""); - mxc_urls.push(final_thumbnail_url); - } else { - info!( - "Found a thumbnail URL in the event ID {event_id} but did not start \ - with mxc://, ignoring" - ); - } - } else { - info!("No \"thumbnail_url\" key in \"info\" key, assuming no thumbnails."); - } - } - } - - // 3. attempts to parse the "file" key - debug!("Attempting to go into \"file\" key"); - if let Some(file_key) = content.get("file") { - debug!("Event ID has \"file\"."); - let file_obj = file_key.as_object(); - - if let Some(file) = file_obj { - if let Some(url) = file.get("url") { - debug!("Found url in file key: {url}"); - - if url.to_string().starts_with("\"mxc://") { - debug!("Pushing URL {} to list of MXCs to delete", url); - let final_url = url.to_string().replace('"', ""); - mxc_urls.push(final_url); - } else { - info!( - "Found a URL in the event ID {event_id} but did not start with \ - mxc://, ignoring" - ); - } - } else { - info!("No \"url\" key in \"file\" key."); - } - } - } - } else { - return Ok(RoomMessageEventContent::text_plain( - "Event ID does not have a \"content\" key or failed parsing the event ID JSON.", - )); - } - } else { - return Ok(RoomMessageEventContent::text_plain( - "Event ID does not have a \"content\" key, this is not a message or an event type that \ - contains media.", - )); - } - } else { - return Ok(RoomMessageEventContent::text_plain( - "Event ID does not exist or is not known to us.", - )); - } - - if mxc_urls.is_empty() { - // we shouldn't get here (should have errored earlier) but just in case for - // whatever reason we do... - info!("Parsed event ID {event_id} but did not contain any MXC URLs."); - return Ok(RoomMessageEventContent::text_plain("Parsed event ID but found no MXC URLs.")); - } - - for mxc_url in mxc_urls { - services().media.delete(mxc_url).await?; - mxc_deletion_count += 1; - } - - return Ok(RoomMessageEventContent::text_plain(format!( - "Deleted {mxc_deletion_count} total MXCs from our database and the filesystem from event ID \ - {event_id}." - ))); - } - - Ok(RoomMessageEventContent::text_plain( - "Please specify either an MXC using --mxc or an event ID using --event-id of the message containing \ - an image. See --help for details.", - )) - }, - MediaCommand::DeleteList => { - if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" { - let mxc_list = body.clone().drain(1..body.len() - 1).collect::>(); - - let mut mxc_deletion_count = 0; - - for mxc in mxc_list { - debug!("Deleting MXC {} in bulk", mxc); - services().media.delete(mxc.to_owned()).await?; - mxc_deletion_count += 1; - } - - return Ok(RoomMessageEventContent::text_plain(format!( - "Finished bulk MXC deletion, deleted {} total MXCs from our database and the filesystem.", - mxc_deletion_count - ))); - } - - Ok(RoomMessageEventContent::text_plain( - "Expected code block in command body. Add --help for details.", - )) - }, - MediaCommand::DeletePastRemoteMedia { - duration, - } => { - let deleted_count = services() - .media - .delete_all_remote_media_at_after_time(duration) - .await?; - - Ok(RoomMessageEventContent::text_plain(format!( - "Deleted {} total files.", - deleted_count - ))) - }, - } -} diff --git a/src/service/admin/media/media_commands.rs b/src/service/admin/media/media_commands.rs new file mode 100644 index 00000000..d7745088 --- /dev/null +++ b/src/service/admin/media/media_commands.rs @@ -0,0 +1,171 @@ +use ruma::{events::room::message::RoomMessageEventContent, EventId}; +use tracing::{debug, info}; + +use crate::{service::admin::MxcUri, services, Result}; + +pub(super) async fn delete( + _body: Vec<&str>, mxc: Option>, event_id: Option>, +) -> Result { + if event_id.is_some() && mxc.is_some() { + return Ok(RoomMessageEventContent::text_plain( + "Please specify either an MXC or an event ID, not both.", + )); + } + + if let Some(mxc) = mxc { + debug!("Got MXC URL: {mxc}"); + services().media.delete(mxc.to_string()).await?; + + return Ok(RoomMessageEventContent::text_plain( + "Deleted the MXC from our database and on our filesystem.", + )); + } else if let Some(event_id) = event_id { + debug!("Got event ID to delete media from: {event_id}"); + + let mut mxc_urls = vec![]; + let mut mxc_deletion_count = 0; + + // parsing the PDU for any MXC URLs begins here + if let Some(event_json) = services().rooms.timeline.get_pdu_json(&event_id)? { + if let Some(content_key) = event_json.get("content") { + debug!("Event ID has \"content\"."); + let content_obj = content_key.as_object(); + + if let Some(content) = content_obj { + // 1. attempts to parse the "url" key + debug!("Attempting to go into \"url\" key for main media file"); + if let Some(url) = content.get("url") { + debug!("Got a URL in the event ID {event_id}: {url}"); + + if url.to_string().starts_with("\"mxc://") { + debug!("Pushing URL {url} to list of MXCs to delete"); + let final_url = url.to_string().replace('"', ""); + mxc_urls.push(final_url); + } else { + info!("Found a URL in the event ID {event_id} but did not start with mxc://, ignoring"); + } + } + + // 2. attempts to parse the "info" key + debug!("Attempting to go into \"info\" key for thumbnails"); + if let Some(info_key) = content.get("info") { + debug!("Event ID has \"info\"."); + let info_obj = info_key.as_object(); + + if let Some(info) = info_obj { + if let Some(thumbnail_url) = info.get("thumbnail_url") { + debug!("Found a thumbnail_url in info key: {thumbnail_url}"); + + if thumbnail_url.to_string().starts_with("\"mxc://") { + debug!("Pushing thumbnail URL {thumbnail_url} to list of MXCs to delete"); + let final_thumbnail_url = thumbnail_url.to_string().replace('"', ""); + mxc_urls.push(final_thumbnail_url); + } else { + info!( + "Found a thumbnail URL in the event ID {event_id} but did not start with \ + mxc://, ignoring" + ); + } + } else { + info!("No \"thumbnail_url\" key in \"info\" key, assuming no thumbnails."); + } + } + } + + // 3. attempts to parse the "file" key + debug!("Attempting to go into \"file\" key"); + if let Some(file_key) = content.get("file") { + debug!("Event ID has \"file\"."); + let file_obj = file_key.as_object(); + + if let Some(file) = file_obj { + if let Some(url) = file.get("url") { + debug!("Found url in file key: {url}"); + + if url.to_string().starts_with("\"mxc://") { + debug!("Pushing URL {url} to list of MXCs to delete"); + let final_url = url.to_string().replace('"', ""); + mxc_urls.push(final_url); + } else { + info!( + "Found a URL in the event ID {event_id} but did not start with mxc://, \ + ignoring" + ); + } + } else { + info!("No \"url\" key in \"file\" key."); + } + } + } + } else { + return Ok(RoomMessageEventContent::text_plain( + "Event ID does not have a \"content\" key or failed parsing the event ID JSON.", + )); + } + } else { + return Ok(RoomMessageEventContent::text_plain( + "Event ID does not have a \"content\" key, this is not a message or an event type that contains \ + media.", + )); + } + } else { + return Ok(RoomMessageEventContent::text_plain( + "Event ID does not exist or is not known to us.", + )); + } + + if mxc_urls.is_empty() { + // we shouldn't get here (should have errored earlier) but just in case for + // whatever reason we do... + info!("Parsed event ID {event_id} but did not contain any MXC URLs."); + return Ok(RoomMessageEventContent::text_plain("Parsed event ID but found no MXC URLs.")); + } + + for mxc_url in mxc_urls { + services().media.delete(mxc_url).await?; + mxc_deletion_count += 1; + } + + return Ok(RoomMessageEventContent::text_plain(format!( + "Deleted {mxc_deletion_count} total MXCs from our database and the filesystem from event ID {event_id}." + ))); + } + + Ok(RoomMessageEventContent::text_plain( + "Please specify either an MXC using --mxc or an event ID using --event-id of the message containing an image. \ + See --help for details.", + )) +} + +pub(super) async fn delete_list(body: Vec<&str>) -> Result { + if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" { + let mxc_list = body.clone().drain(1..body.len() - 1).collect::>(); + + let mut mxc_deletion_count = 0; + + for mxc in mxc_list { + debug!("Deleting MXC {mxc} in bulk"); + services().media.delete(mxc.to_owned()).await?; + mxc_deletion_count += 1; + } + + return Ok(RoomMessageEventContent::text_plain(format!( + "Finished bulk MXC deletion, deleted {mxc_deletion_count} total MXCs from our database and the filesystem.", + ))); + } + + Ok(RoomMessageEventContent::text_plain( + "Expected code block in command body. Add --help for details.", + )) +} + +pub(super) async fn delete_past_remote_media(_body: Vec<&str>, duration: String) -> Result { + let deleted_count = services() + .media + .delete_all_remote_media_at_after_time(duration) + .await?; + + Ok(RoomMessageEventContent::text_plain(format!( + "Deleted {deleted_count} total files.", + ))) +} diff --git a/src/service/admin/media/mod.rs b/src/service/admin/media/mod.rs new file mode 100644 index 00000000..d091f94a --- /dev/null +++ b/src/service/admin/media/mod.rs @@ -0,0 +1,49 @@ +use clap::Subcommand; +use ruma::{events::room::message::RoomMessageEventContent, EventId}; + +use self::media_commands::{delete, delete_list, delete_past_remote_media}; +use crate::{service::admin::MxcUri, Result}; + +pub(crate) mod media_commands; + +#[cfg_attr(test, derive(Debug))] +#[derive(Subcommand)] +pub(crate) enum MediaCommand { + /// - Deletes a single media file from our database and on the filesystem + /// via a single MXC URL + Delete { + /// The MXC URL to delete + #[arg(long)] + mxc: Option>, + + /// - The message event ID which contains the media and thumbnail MXC + /// URLs + #[arg(long)] + event_id: Option>, + }, + + /// - Deletes a codeblock list of MXC URLs from our database and on the + /// filesystem + DeleteList, + + /// - Deletes all remote media in the last X amount of time using filesystem + /// metadata first created at date. + DeletePastRemoteMedia { + /// - The duration (at or after), e.g. "5m" to delete all media in the + /// past 5 minutes + duration: String, + }, +} + +pub(crate) async fn process(command: MediaCommand, body: Vec<&str>) -> Result { + Ok(match command { + MediaCommand::Delete { + mxc, + event_id, + } => delete(body, mxc, event_id).await?, + MediaCommand::DeleteList => delete_list(body).await?, + MediaCommand::DeletePastRemoteMedia { + duration, + } => delete_past_remote_media(body, duration).await?, + }) +} diff --git a/src/service/admin/mod.rs b/src/service/admin/mod.rs index 1971461d..0f361a4c 100644 --- a/src/service/admin/mod.rs +++ b/src/service/admin/mod.rs @@ -42,9 +42,6 @@ pub(crate) mod fsck; pub(crate) mod media; pub(crate) mod query; pub(crate) mod room; -pub(crate) mod room_alias; -pub(crate) mod room_directory; -pub(crate) mod room_moderation; pub(crate) mod server; pub(crate) mod user; diff --git a/src/service/admin/query/account_data.rs b/src/service/admin/query/account_data.rs index f335489e..15d45633 100644 --- a/src/service/admin/query/account_data.rs +++ b/src/service/admin/query/account_data.rs @@ -1,36 +1,8 @@ -use clap::Subcommand; -use ruma::{ - events::{room::message::RoomMessageEventContent, RoomAccountDataEventType}, - RoomId, UserId, -}; +use ruma::events::room::message::RoomMessageEventContent; +use super::AccountData; use crate::{services, Result}; -#[cfg_attr(test, derive(Debug))] -#[derive(Subcommand)] -/// All the getters and iterators from src/database/key_value/account_data.rs -pub(crate) enum AccountData { - /// - Returns all changes to the account data that happened after `since`. - ChangesSince { - /// Full user ID - user_id: Box, - /// UNIX timestamp since (u64) - since: u64, - /// Optional room ID of the account data - room_id: Option>, - }, - - /// - Searches the account data for a specific kind. - Get { - /// Full user ID - user_id: Box, - /// Account data event type - kind: RoomAccountDataEventType, - /// Optional room ID of the account data - room_id: Option>, - }, -} - /// All the getters and iterators from src/database/key_value/account_data.rs pub(super) async fn account_data(subcommand: AccountData) -> Result { match subcommand { diff --git a/src/service/admin/query/appservice.rs b/src/service/admin/query/appservice.rs index c576f7db..bfb63c95 100644 --- a/src/service/admin/query/appservice.rs +++ b/src/service/admin/query/appservice.rs @@ -1,19 +1,8 @@ -use clap::Subcommand; use ruma::events::room::message::RoomMessageEventContent; +use super::Appservice; use crate::{services, Result}; -#[cfg_attr(test, derive(Debug))] -#[derive(Subcommand)] -/// All the getters and iterators from src/database/key_value/appservice.rs -pub(crate) enum Appservice { - /// - Gets the appservice registration info/details from the ID as a string - GetRegistration { - /// Appservice registration ID - appservice_id: Box, - }, -} - /// All the getters and iterators from src/database/key_value/appservice.rs pub(super) async fn appservice(subcommand: Appservice) -> Result { match subcommand { diff --git a/src/service/admin/query/globals.rs b/src/service/admin/query/globals.rs index 25c3e337..ff962cb5 100644 --- a/src/service/admin/query/globals.rs +++ b/src/service/admin/query/globals.rs @@ -1,27 +1,8 @@ -use clap::Subcommand; -use ruma::{events::room::message::RoomMessageEventContent, ServerName}; +use ruma::events::room::message::RoomMessageEventContent; +use super::Globals; use crate::{services, Result}; -#[cfg_attr(test, derive(Debug))] -#[derive(Subcommand)] -/// All the getters and iterators from src/database/key_value/globals.rs -pub(crate) enum Globals { - DatabaseVersion, - - CurrentCount, - - LastCheckForUpdatesId, - - LoadKeypair, - - /// - This returns an empty `Ok(BTreeMap<..>)` when there are no keys found - /// for the server. - SigningKeysFor { - origin: Box, - }, -} - /// All the getters and iterators from src/database/key_value/globals.rs pub(super) async fn globals(subcommand: Globals) -> Result { match subcommand { diff --git a/src/service/admin/query/mod.rs b/src/service/admin/query/mod.rs index 8033e731..60867fb2 100644 --- a/src/service/admin/query/mod.rs +++ b/src/service/admin/query/mod.rs @@ -5,14 +5,13 @@ pub(crate) mod presence; pub(crate) mod room_alias; use clap::Subcommand; -use ruma::events::room::message::RoomMessageEventContent; +use ruma::{ + events::{room::message::RoomMessageEventContent, RoomAccountDataEventType}, + RoomAliasId, RoomId, ServerName, UserId, +}; use self::{ - account_data::{account_data, AccountData}, - appservice::{appservice, Appservice}, - globals::{globals, Globals}, - presence::{presence, Presence}, - room_alias::{room_alias, RoomAlias}, + account_data::account_data, appservice::appservice, globals::globals, presence::presence, room_alias::room_alias, }; use crate::Result; @@ -41,14 +40,105 @@ pub(crate) enum QueryCommand { Globals(Globals), } +#[cfg_attr(test, derive(Debug))] +#[derive(Subcommand)] +/// All the getters and iterators from src/database/key_value/account_data.rs +pub(crate) enum AccountData { + /// - Returns all changes to the account data that happened after `since`. + ChangesSince { + /// Full user ID + user_id: Box, + /// UNIX timestamp since (u64) + since: u64, + /// Optional room ID of the account data + room_id: Option>, + }, + + /// - Searches the account data for a specific kind. + Get { + /// Full user ID + user_id: Box, + /// Account data event type + kind: RoomAccountDataEventType, + /// Optional room ID of the account data + room_id: Option>, + }, +} + +#[cfg_attr(test, derive(Debug))] +#[derive(Subcommand)] +/// All the getters and iterators from src/database/key_value/appservice.rs +pub(crate) enum Appservice { + /// - Gets the appservice registration info/details from the ID as a string + GetRegistration { + /// Appservice registration ID + appservice_id: Box, + }, +} + +#[cfg_attr(test, derive(Debug))] +#[derive(Subcommand)] +/// All the getters and iterators from src/database/key_value/presence.rs +pub(crate) enum Presence { + /// - Returns the latest presence event for the given user. + GetPresence { + /// Full user ID + user_id: Box, + }, + + /// - Iterator of the most recent presence updates that happened after the + /// event with id `since`. + PresenceSince { + /// UNIX timestamp since (u64) + since: u64, + }, +} + +#[cfg_attr(test, derive(Debug))] +#[derive(Subcommand)] +/// All the getters and iterators from src/database/key_value/rooms/alias.rs +pub(crate) enum RoomAlias { + ResolveLocalAlias { + /// Full room alias + alias: Box, + }, + + /// - Iterator of all our local room aliases for the room ID + LocalAliasesForRoom { + /// Full room ID + room_id: Box, + }, + + /// - Iterator of all our local aliases in our database with their room IDs + AllLocalAliases, +} + +#[cfg_attr(test, derive(Debug))] +#[derive(Subcommand)] +/// All the getters and iterators from src/database/key_value/globals.rs +pub(crate) enum Globals { + DatabaseVersion, + + CurrentCount, + + LastCheckForUpdatesId, + + LoadKeypair, + + /// - This returns an empty `Ok(BTreeMap<..>)` when there are no keys found + /// for the server. + SigningKeysFor { + origin: Box, + }, +} + /// Processes admin query commands -#[allow(non_snake_case)] pub(crate) async fn process(command: QueryCommand, _body: Vec<&str>) -> Result { Ok(match command { - QueryCommand::AccountData(AccountData) => account_data(AccountData).await?, - QueryCommand::Appservice(Appservice) => appservice(Appservice).await?, - QueryCommand::Presence(Presence) => presence(Presence).await?, - QueryCommand::RoomAlias(RoomAlias) => room_alias(RoomAlias).await?, - QueryCommand::Globals(Globals) => globals(Globals).await?, + QueryCommand::AccountData(command) => account_data(command).await?, + QueryCommand::Appservice(command) => appservice(command).await?, + QueryCommand::Presence(command) => presence(command).await?, + QueryCommand::RoomAlias(command) => room_alias(command).await?, + QueryCommand::Globals(command) => globals(command).await?, }) } diff --git a/src/service/admin/query/presence.rs b/src/service/admin/query/presence.rs index bb55b88f..0e32bbd7 100644 --- a/src/service/admin/query/presence.rs +++ b/src/service/admin/query/presence.rs @@ -1,26 +1,8 @@ -use clap::Subcommand; -use ruma::{events::room::message::RoomMessageEventContent, UserId}; +use ruma::events::room::message::RoomMessageEventContent; +use super::Presence; use crate::{services, Result}; -#[cfg_attr(test, derive(Debug))] -#[derive(Subcommand)] -/// All the getters and iterators from src/database/key_value/presence.rs -pub(crate) enum Presence { - /// - Returns the latest presence event for the given user. - GetPresence { - /// Full user ID - user_id: Box, - }, - - /// - Iterator of the most recent presence updates that happened after the - /// event with id `since`. - PresenceSince { - /// UNIX timestamp since (u64) - since: u64, - }, -} - /// All the getters and iterators in key_value/presence.rs pub(super) async fn presence(subcommand: Presence) -> Result { match subcommand { diff --git a/src/service/admin/query/room_alias.rs b/src/service/admin/query/room_alias.rs index e854f643..e5238f38 100644 --- a/src/service/admin/query/room_alias.rs +++ b/src/service/admin/query/room_alias.rs @@ -1,27 +1,8 @@ -use clap::Subcommand; -use ruma::{events::room::message::RoomMessageEventContent, RoomAliasId, RoomId}; +use ruma::events::room::message::RoomMessageEventContent; +use super::RoomAlias; use crate::{services, Result}; -#[cfg_attr(test, derive(Debug))] -#[derive(Subcommand)] -/// All the getters and iterators from src/database/key_value/rooms/alias.rs -pub(crate) enum RoomAlias { - ResolveLocalAlias { - /// Full room alias - alias: Box, - }, - - /// - Iterator of all our local room aliases for the room ID - LocalAliasesForRoom { - /// Full room ID - room_id: Box, - }, - - /// - Iterator of all our local aliases in our database with their room IDs - AllLocalAliases, -} - /// All the getters and iterators in src/database/key_value/rooms/alias.rs pub(super) async fn room_alias(subcommand: RoomAlias) -> Result { match subcommand { diff --git a/src/service/admin/room.rs b/src/service/admin/room.rs deleted file mode 100644 index 721191b1..00000000 --- a/src/service/admin/room.rs +++ /dev/null @@ -1,96 +0,0 @@ -use std::fmt::Write as _; - -use clap::Subcommand; -use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId}; - -use crate::{ - service::admin::{ - escape_html, get_room_info, room_alias, room_alias::RoomAliasCommand, room_directory, - room_directory::RoomDirectoryCommand, room_moderation, room_moderation::RoomModerationCommand, PAGE_SIZE, - }, - services, Result, -}; - -#[cfg_attr(test, derive(Debug))] -#[derive(Subcommand)] -pub(crate) enum RoomCommand { - /// - List all rooms the server knows about - List { - page: Option, - }, - - #[command(subcommand)] - /// - Manage moderation of remote or local rooms - Moderation(RoomModerationCommand), - - #[command(subcommand)] - /// - Manage rooms' aliases - Alias(RoomAliasCommand), - - #[command(subcommand)] - /// - Manage the room directory - Directory(RoomDirectoryCommand), -} - -pub(crate) async fn process(command: RoomCommand, body: Vec<&str>) -> Result { - match command { - RoomCommand::Alias(command) => room_alias::process(command, body).await, - - RoomCommand::Directory(command) => room_directory::process(command, body).await, - - RoomCommand::Moderation(command) => room_moderation::process(command, body).await, - - RoomCommand::List { - page, - } => { - // TODO: i know there's a way to do this with clap, but i can't seem to find it - let page = page.unwrap_or(1); - let mut rooms = services() - .rooms - .metadata - .iter_ids() - .filter_map(Result::ok) - .map(|id: OwnedRoomId| get_room_info(&id)) - .collect::>(); - rooms.sort_by_key(|r| r.1); - rooms.reverse(); - - let rooms = rooms - .into_iter() - .skip(page.saturating_sub(1) * PAGE_SIZE) - .take(PAGE_SIZE) - .collect::>(); - - if rooms.is_empty() { - return Ok(RoomMessageEventContent::text_plain("No more rooms.")); - }; - - let output_plain = format!( - "Rooms:\n{}", - rooms - .iter() - .map(|(id, members, name)| format!("{id}\tMembers: {members}\tName: {name}")) - .collect::>() - .join("\n") - ); - let output_html = format!( - "\n\t\t\n{}
Room list - page \ - {page}
idmembersname
", - rooms - .iter() - .fold(String::new(), |mut output, (id, members, name)| { - writeln!( - output, - "{}\t{}\t{}", - escape_html(id.as_ref()), - members, - escape_html(name) - ) - .unwrap(); - output - }) - ); - Ok(RoomMessageEventContent::text_html(output_plain, output_html)) - }, - } -} diff --git a/src/service/admin/room/mod.rs b/src/service/admin/room/mod.rs new file mode 100644 index 00000000..b4b7b279 --- /dev/null +++ b/src/service/admin/room/mod.rs @@ -0,0 +1,160 @@ +use clap::Subcommand; +use ruma::{events::room::message::RoomMessageEventContent, RoomId, RoomOrAliasId}; + +use self::room_commands::list; +use crate::Result; + +pub(crate) mod room_alias_commands; +pub(crate) mod room_commands; +pub(crate) mod room_directory_commands; +pub(crate) mod room_moderation_commands; + +#[cfg_attr(test, derive(Debug))] +#[derive(Subcommand)] +pub(crate) enum RoomCommand { + /// - List all rooms the server knows about + List { + page: Option, + }, + + #[command(subcommand)] + /// - Manage moderation of remote or local rooms + Moderation(RoomModerationCommand), + + #[command(subcommand)] + /// - Manage rooms' aliases + Alias(RoomAliasCommand), + + #[command(subcommand)] + /// - Manage the room directory + Directory(RoomDirectoryCommand), +} + +#[cfg_attr(test, derive(Debug))] +#[derive(Subcommand)] +pub(crate) enum RoomAliasCommand { + /// - Make an alias point to a room. + Set { + #[arg(short, long)] + /// Set the alias even if a room is already using it + force: bool, + + /// The room id to set the alias on + room_id: Box, + + /// The alias localpart to use (`alias`, not `#alias:servername.tld`) + room_alias_localpart: String, + }, + + /// - Remove an alias + Remove { + /// The alias localpart to remove (`alias`, not `#alias:servername.tld`) + room_alias_localpart: String, + }, + + /// - Show which room is using an alias + Which { + /// The alias localpart to look up (`alias`, not + /// `#alias:servername.tld`) + room_alias_localpart: String, + }, + + /// - List aliases currently being used + List { + /// If set, only list the aliases for this room + room_id: Option>, + }, +} + +#[cfg_attr(test, derive(Debug))] +#[derive(Subcommand)] +pub(crate) enum RoomDirectoryCommand { + /// - Publish a room to the room directory + Publish { + /// The room id of the room to publish + room_id: Box, + }, + + /// - Unpublish a room to the room directory + Unpublish { + /// The room id of the room to unpublish + room_id: Box, + }, + + /// - List rooms that are published + List { + page: Option, + }, +} + +#[cfg_attr(test, derive(Debug))] +#[derive(Subcommand)] +pub(crate) enum RoomModerationCommand { + /// - Bans a room from local users joining and evicts all our local users + /// from the room. Also blocks any invites (local and remote) for the + /// banned room. + /// + /// Server admins (users in the conduwuit admin room) will not be evicted + /// and server admins can still join the room. To evict admins too, use + /// --force (also ignores errors) To disable incoming federation of the + /// room, use --disable-federation + BanRoom { + #[arg(short, long)] + /// Evicts admins out of the room and ignores any potential errors when + /// making our local users leave the room + force: bool, + + #[arg(long)] + /// Disables incoming federation of the room after banning and evicting + /// users + disable_federation: bool, + + /// The room in the format of `!roomid:example.com` or a room alias in + /// the format of `#roomalias:example.com` + room: Box, + }, + + /// - Bans a list of rooms (room IDs and room aliases) from a newline + /// delimited codeblock similar to `user deactivate-all` + BanListOfRooms { + #[arg(short, long)] + /// Evicts admins out of the room and ignores any potential errors when + /// making our local users leave the room + force: bool, + + #[arg(long)] + /// Disables incoming federation of the room after banning and evicting + /// users + disable_federation: bool, + }, + + /// - Unbans a room to allow local users to join again + /// + /// To re-enable incoming federation of the room, use --enable-federation + UnbanRoom { + #[arg(long)] + /// Enables incoming federation of the room after unbanning + enable_federation: bool, + + /// The room in the format of `!roomid:example.com` or a room alias in + /// the format of `#roomalias:example.com` + room: Box, + }, + + /// - List of all rooms we have banned + ListBannedRooms, +} + +pub(crate) async fn process(command: RoomCommand, body: Vec<&str>) -> Result { + Ok(match command { + RoomCommand::Alias(command) => room_alias_commands::process(command, body).await?, + + RoomCommand::Directory(command) => room_directory_commands::process(command, body).await?, + + RoomCommand::Moderation(command) => room_moderation_commands::process(command, body).await?, + + RoomCommand::List { + page, + } => list(body, page).await?, + }) +} diff --git a/src/service/admin/room_alias.rs b/src/service/admin/room/room_alias_commands.rs similarity index 84% rename from src/service/admin/room_alias.rs rename to src/service/admin/room/room_alias_commands.rs index f1621344..516df071 100644 --- a/src/service/admin/room_alias.rs +++ b/src/service/admin/room/room_alias_commands.rs @@ -1,46 +1,10 @@ use std::fmt::Write as _; -use clap::Subcommand; -use ruma::{events::room::message::RoomMessageEventContent, RoomAliasId, RoomId}; +use ruma::{events::room::message::RoomMessageEventContent, RoomAliasId}; +use super::RoomAliasCommand; use crate::{service::admin::escape_html, services, Result}; -#[cfg_attr(test, derive(Debug))] -#[derive(Subcommand)] -pub(crate) enum RoomAliasCommand { - /// - Make an alias point to a room. - Set { - #[arg(short, long)] - /// Set the alias even if a room is already using it - force: bool, - - /// The room id to set the alias on - room_id: Box, - - /// The alias localpart to use (`alias`, not `#alias:servername.tld`) - room_alias_localpart: String, - }, - - /// - Remove an alias - Remove { - /// The alias localpart to remove (`alias`, not `#alias:servername.tld`) - room_alias_localpart: String, - }, - - /// - Show which room is using an alias - Which { - /// The alias localpart to look up (`alias`, not - /// `#alias:servername.tld`) - room_alias_localpart: String, - }, - - /// - List aliases currently being used - List { - /// If set, only list the aliases for this room - room_id: Option>, - }, -} - pub(crate) async fn process(command: RoomAliasCommand, _body: Vec<&str>) -> Result { match command { RoomAliasCommand::Set { diff --git a/src/service/admin/room/room_commands.rs b/src/service/admin/room/room_commands.rs new file mode 100644 index 00000000..f4964adf --- /dev/null +++ b/src/service/admin/room/room_commands.rs @@ -0,0 +1,59 @@ +use std::fmt::Write as _; + +use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId}; + +use crate::{ + service::admin::{escape_html, get_room_info, PAGE_SIZE}, + services, Result, +}; + +pub(super) async fn list(_body: Vec<&str>, page: Option) -> Result { + // TODO: i know there's a way to do this with clap, but i can't seem to find it + let page = page.unwrap_or(1); + let mut rooms = services() + .rooms + .metadata + .iter_ids() + .filter_map(Result::ok) + .map(|id: OwnedRoomId| get_room_info(&id)) + .collect::>(); + rooms.sort_by_key(|r| r.1); + rooms.reverse(); + + let rooms = rooms + .into_iter() + .skip(page.saturating_sub(1) * PAGE_SIZE) + .take(PAGE_SIZE) + .collect::>(); + + if rooms.is_empty() { + return Ok(RoomMessageEventContent::text_plain("No more rooms.")); + }; + + let output_plain = format!( + "Rooms:\n{}", + rooms + .iter() + .map(|(id, members, name)| format!("{id}\tMembers: {members}\tName: {name}")) + .collect::>() + .join("\n") + ); + let output_html = format!( + "\n\t\t\n{}
Room list - page \ + {page}
idmembersname
", + rooms + .iter() + .fold(String::new(), |mut output, (id, members, name)| { + writeln!( + output, + "{}\t{}\t{}", + escape_html(id.as_ref()), + members, + escape_html(name) + ) + .unwrap(); + output + }) + ); + Ok(RoomMessageEventContent::text_html(output_plain, output_html)) +} diff --git a/src/service/admin/room_directory.rs b/src/service/admin/room/room_directory_commands.rs similarity index 80% rename from src/service/admin/room_directory.rs rename to src/service/admin/room/room_directory_commands.rs index 86dc03d6..bfdc7a60 100644 --- a/src/service/admin/room_directory.rs +++ b/src/service/admin/room/room_directory_commands.rs @@ -1,47 +1,26 @@ use std::fmt::Write as _; -use clap::Subcommand; -use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId, RoomId}; +use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId}; +use super::RoomDirectoryCommand; use crate::{ service::admin::{escape_html, get_room_info, PAGE_SIZE}, services, Result, }; -#[cfg_attr(test, derive(Debug))] -#[derive(Subcommand)] -pub(crate) enum RoomDirectoryCommand { - /// - Publish a room to the room directory - Publish { - /// The room id of the room to publish - room_id: Box, - }, - - /// - Unpublish a room to the room directory - Unpublish { - /// The room id of the room to unpublish - room_id: Box, - }, - - /// - List rooms that are published - List { - page: Option, - }, -} - pub(crate) async fn process(command: RoomDirectoryCommand, _body: Vec<&str>) -> Result { match command { RoomDirectoryCommand::Publish { room_id, } => match services().rooms.directory.set_public(&room_id) { Ok(()) => Ok(RoomMessageEventContent::text_plain("Room published")), - Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Unable to update room: {}", err))), + Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Unable to update room: {err}"))), }, RoomDirectoryCommand::Unpublish { room_id, } => match services().rooms.directory.set_not_public(&room_id) { Ok(()) => Ok(RoomMessageEventContent::text_plain("Room unpublished")), - Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Unable to update room: {}", err))), + Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Unable to update room: {err}"))), }, RoomDirectoryCommand::List { page, diff --git a/src/service/admin/room_moderation.rs b/src/service/admin/room/room_moderation_commands.rs similarity index 90% rename from src/service/admin/room_moderation.rs rename to src/service/admin/room/room_moderation_commands.rs index 18a42a37..c02d8d16 100644 --- a/src/service/admin/room_moderation.rs +++ b/src/service/admin/room/room_moderation_commands.rs @@ -1,75 +1,17 @@ use std::fmt::Write as _; -use clap::Subcommand; use ruma::{ events::room::message::RoomMessageEventContent, OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, RoomOrAliasId, }; use tracing::{debug, error, info, warn}; +use super::RoomModerationCommand; use crate::{ api::client_server::{get_alias_helper, leave_room}, service::admin::{escape_html, Service}, services, Result, }; -#[cfg_attr(test, derive(Debug))] -#[derive(Subcommand)] -pub(crate) enum RoomModerationCommand { - /// - Bans a room from local users joining and evicts all our local users - /// from the room. Also blocks any invites (local and remote) for the - /// banned room. - /// - /// Server admins (users in the conduwuit admin room) will not be evicted - /// and server admins can still join the room. To evict admins too, use - /// --force (also ignores errors) To disable incoming federation of the - /// room, use --disable-federation - BanRoom { - #[arg(short, long)] - /// Evicts admins out of the room and ignores any potential errors when - /// making our local users leave the room - force: bool, - - #[arg(long)] - /// Disables incoming federation of the room after banning and evicting - /// users - disable_federation: bool, - - /// The room in the format of `!roomid:example.com` or a room alias in - /// the format of `#roomalias:example.com` - room: Box, - }, - - /// - Bans a list of rooms (room IDs and room aliases) from a newline - /// delimited codeblock similar to `user deactivate-all` - BanListOfRooms { - #[arg(short, long)] - /// Evicts admins out of the room and ignores any potential errors when - /// making our local users leave the room - force: bool, - - #[arg(long)] - /// Disables incoming federation of the room after banning and evicting - /// users - disable_federation: bool, - }, - - /// - Unbans a room to allow local users to join again - /// - /// To re-enable incoming federation of the room, use --enable-federation - UnbanRoom { - #[arg(long)] - /// Enables incoming federation of the room after unbanning - enable_federation: bool, - - /// The room in the format of `!roomid:example.com` or a room alias in - /// the format of `#roomalias:example.com` - room: Box, - }, - - /// - List of all rooms we have banned - ListBannedRooms, -} - pub(crate) async fn process(command: RoomModerationCommand, body: Vec<&str>) -> Result { match command { RoomModerationCommand::BanRoom {