finish general admin room cleanup
Signed-off-by: strawberry <strawberry@puppygock.gay>
This commit is contained in:
parent
a36b37ee3d
commit
893707d501
19 changed files with 751 additions and 725 deletions
|
@ -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<RoomId>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// - Enables incoming federation handling for a room again.
|
|
||||||
EnableRoom {
|
|
||||||
room_id: Box<RoomId>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// - 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<ServerName>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn process(command: FederationCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
|
||||||
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!(
|
|
||||||
"<p>Got JSON response:</p>\n<pre><code class=\"language-json\">{}\n</code></pre>\n",
|
|
||||||
HtmlEscape(&pretty_json)
|
|
||||||
),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
126
src/service/admin/federation/federation_commands.rs
Normal file
126
src/service/admin/federation/federation_commands.rs
Normal file
|
@ -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<RoomId>) -> Result<RoomMessageEventContent> {
|
||||||
|
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<RoomId>) -> Result<RoomMessageEventContent> {
|
||||||
|
services().rooms.metadata.disable_room(&room_id, false)?;
|
||||||
|
Ok(RoomMessageEventContent::text_plain("Room enabled."))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn incoming_federeation(_body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||||
|
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<RoomMessageEventContent> {
|
||||||
|
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<RoomMessageEventContent> {
|
||||||
|
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<ServerName>,
|
||||||
|
) -> Result<RoomMessageEventContent> {
|
||||||
|
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!(
|
||||||
|
"<p>Got JSON response:</p>\n<pre><code class=\"language-json\">{}\n</code></pre>\n",
|
||||||
|
HtmlEscape(&pretty_json)
|
||||||
|
),
|
||||||
|
))
|
||||||
|
}
|
68
src/service/admin/federation/mod.rs
Normal file
68
src/service/admin/federation/mod.rs
Normal file
|
@ -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<RoomId>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// - Enables incoming federation handling for a room again.
|
||||||
|
EnableRoom {
|
||||||
|
room_id: Box<RoomId>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// - 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<ServerName>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn process(command: FederationCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||||
|
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?,
|
||||||
|
})
|
||||||
|
}
|
|
@ -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<Box<MxcUri>>,
|
|
||||||
|
|
||||||
/// - The message event ID which contains the media and thumbnail MXC
|
|
||||||
/// URLs
|
|
||||||
#[arg(long)]
|
|
||||||
event_id: Option<Box<EventId>>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// - 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<RoomMessageEventContent> {
|
|
||||||
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::<Vec<_>>();
|
|
||||||
|
|
||||||
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
|
|
||||||
)))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
171
src/service/admin/media/media_commands.rs
Normal file
171
src/service/admin/media/media_commands.rs
Normal file
|
@ -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<Box<MxcUri>>, event_id: Option<Box<EventId>>,
|
||||||
|
) -> Result<RoomMessageEventContent> {
|
||||||
|
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<RoomMessageEventContent> {
|
||||||
|
if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" {
|
||||||
|
let mxc_list = body.clone().drain(1..body.len() - 1).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
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<RoomMessageEventContent> {
|
||||||
|
let deleted_count = services()
|
||||||
|
.media
|
||||||
|
.delete_all_remote_media_at_after_time(duration)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(RoomMessageEventContent::text_plain(format!(
|
||||||
|
"Deleted {deleted_count} total files.",
|
||||||
|
)))
|
||||||
|
}
|
49
src/service/admin/media/mod.rs
Normal file
49
src/service/admin/media/mod.rs
Normal file
|
@ -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<Box<MxcUri>>,
|
||||||
|
|
||||||
|
/// - The message event ID which contains the media and thumbnail MXC
|
||||||
|
/// URLs
|
||||||
|
#[arg(long)]
|
||||||
|
event_id: Option<Box<EventId>>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// - 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<RoomMessageEventContent> {
|
||||||
|
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?,
|
||||||
|
})
|
||||||
|
}
|
|
@ -42,9 +42,6 @@ pub(crate) mod fsck;
|
||||||
pub(crate) mod media;
|
pub(crate) mod media;
|
||||||
pub(crate) mod query;
|
pub(crate) mod query;
|
||||||
pub(crate) mod room;
|
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 server;
|
||||||
pub(crate) mod user;
|
pub(crate) mod user;
|
||||||
|
|
||||||
|
|
|
@ -1,36 +1,8 @@
|
||||||
use clap::Subcommand;
|
use ruma::events::room::message::RoomMessageEventContent;
|
||||||
use ruma::{
|
|
||||||
events::{room::message::RoomMessageEventContent, RoomAccountDataEventType},
|
|
||||||
RoomId, UserId,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
use super::AccountData;
|
||||||
use crate::{services, Result};
|
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<UserId>,
|
|
||||||
/// UNIX timestamp since (u64)
|
|
||||||
since: u64,
|
|
||||||
/// Optional room ID of the account data
|
|
||||||
room_id: Option<Box<RoomId>>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// - Searches the account data for a specific kind.
|
|
||||||
Get {
|
|
||||||
/// Full user ID
|
|
||||||
user_id: Box<UserId>,
|
|
||||||
/// Account data event type
|
|
||||||
kind: RoomAccountDataEventType,
|
|
||||||
/// Optional room ID of the account data
|
|
||||||
room_id: Option<Box<RoomId>>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// All the getters and iterators from src/database/key_value/account_data.rs
|
/// All the getters and iterators from src/database/key_value/account_data.rs
|
||||||
pub(super) async fn account_data(subcommand: AccountData) -> Result<RoomMessageEventContent> {
|
pub(super) async fn account_data(subcommand: AccountData) -> Result<RoomMessageEventContent> {
|
||||||
match subcommand {
|
match subcommand {
|
||||||
|
|
|
@ -1,19 +1,8 @@
|
||||||
use clap::Subcommand;
|
|
||||||
use ruma::events::room::message::RoomMessageEventContent;
|
use ruma::events::room::message::RoomMessageEventContent;
|
||||||
|
|
||||||
|
use super::Appservice;
|
||||||
use crate::{services, Result};
|
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<str>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// All the getters and iterators from src/database/key_value/appservice.rs
|
/// All the getters and iterators from src/database/key_value/appservice.rs
|
||||||
pub(super) async fn appservice(subcommand: Appservice) -> Result<RoomMessageEventContent> {
|
pub(super) async fn appservice(subcommand: Appservice) -> Result<RoomMessageEventContent> {
|
||||||
match subcommand {
|
match subcommand {
|
||||||
|
|
|
@ -1,27 +1,8 @@
|
||||||
use clap::Subcommand;
|
use ruma::events::room::message::RoomMessageEventContent;
|
||||||
use ruma::{events::room::message::RoomMessageEventContent, ServerName};
|
|
||||||
|
|
||||||
|
use super::Globals;
|
||||||
use crate::{services, Result};
|
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<ServerName>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// All the getters and iterators from src/database/key_value/globals.rs
|
/// All the getters and iterators from src/database/key_value/globals.rs
|
||||||
pub(super) async fn globals(subcommand: Globals) -> Result<RoomMessageEventContent> {
|
pub(super) async fn globals(subcommand: Globals) -> Result<RoomMessageEventContent> {
|
||||||
match subcommand {
|
match subcommand {
|
||||||
|
|
|
@ -5,14 +5,13 @@ pub(crate) mod presence;
|
||||||
pub(crate) mod room_alias;
|
pub(crate) mod room_alias;
|
||||||
|
|
||||||
use clap::Subcommand;
|
use clap::Subcommand;
|
||||||
use ruma::events::room::message::RoomMessageEventContent;
|
use ruma::{
|
||||||
|
events::{room::message::RoomMessageEventContent, RoomAccountDataEventType},
|
||||||
|
RoomAliasId, RoomId, ServerName, UserId,
|
||||||
|
};
|
||||||
|
|
||||||
use self::{
|
use self::{
|
||||||
account_data::{account_data, AccountData},
|
account_data::account_data, appservice::appservice, globals::globals, presence::presence, room_alias::room_alias,
|
||||||
appservice::{appservice, Appservice},
|
|
||||||
globals::{globals, Globals},
|
|
||||||
presence::{presence, Presence},
|
|
||||||
room_alias::{room_alias, RoomAlias},
|
|
||||||
};
|
};
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
|
|
||||||
|
@ -41,14 +40,105 @@ pub(crate) enum QueryCommand {
|
||||||
Globals(Globals),
|
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<UserId>,
|
||||||
|
/// UNIX timestamp since (u64)
|
||||||
|
since: u64,
|
||||||
|
/// Optional room ID of the account data
|
||||||
|
room_id: Option<Box<RoomId>>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// - Searches the account data for a specific kind.
|
||||||
|
Get {
|
||||||
|
/// Full user ID
|
||||||
|
user_id: Box<UserId>,
|
||||||
|
/// Account data event type
|
||||||
|
kind: RoomAccountDataEventType,
|
||||||
|
/// Optional room ID of the account data
|
||||||
|
room_id: Option<Box<RoomId>>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<str>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<UserId>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// - 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<RoomAliasId>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// - Iterator of all our local room aliases for the room ID
|
||||||
|
LocalAliasesForRoom {
|
||||||
|
/// Full room ID
|
||||||
|
room_id: Box<RoomId>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// - 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<ServerName>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
/// Processes admin query commands
|
/// Processes admin query commands
|
||||||
#[allow(non_snake_case)]
|
|
||||||
pub(crate) async fn process(command: QueryCommand, _body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
pub(crate) async fn process(command: QueryCommand, _body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||||
Ok(match command {
|
Ok(match command {
|
||||||
QueryCommand::AccountData(AccountData) => account_data(AccountData).await?,
|
QueryCommand::AccountData(command) => account_data(command).await?,
|
||||||
QueryCommand::Appservice(Appservice) => appservice(Appservice).await?,
|
QueryCommand::Appservice(command) => appservice(command).await?,
|
||||||
QueryCommand::Presence(Presence) => presence(Presence).await?,
|
QueryCommand::Presence(command) => presence(command).await?,
|
||||||
QueryCommand::RoomAlias(RoomAlias) => room_alias(RoomAlias).await?,
|
QueryCommand::RoomAlias(command) => room_alias(command).await?,
|
||||||
QueryCommand::Globals(Globals) => globals(Globals).await?,
|
QueryCommand::Globals(command) => globals(command).await?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,8 @@
|
||||||
use clap::Subcommand;
|
use ruma::events::room::message::RoomMessageEventContent;
|
||||||
use ruma::{events::room::message::RoomMessageEventContent, UserId};
|
|
||||||
|
|
||||||
|
use super::Presence;
|
||||||
use crate::{services, Result};
|
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<UserId>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// - 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
|
/// All the getters and iterators in key_value/presence.rs
|
||||||
pub(super) async fn presence(subcommand: Presence) -> Result<RoomMessageEventContent> {
|
pub(super) async fn presence(subcommand: Presence) -> Result<RoomMessageEventContent> {
|
||||||
match subcommand {
|
match subcommand {
|
||||||
|
|
|
@ -1,27 +1,8 @@
|
||||||
use clap::Subcommand;
|
use ruma::events::room::message::RoomMessageEventContent;
|
||||||
use ruma::{events::room::message::RoomMessageEventContent, RoomAliasId, RoomId};
|
|
||||||
|
|
||||||
|
use super::RoomAlias;
|
||||||
use crate::{services, Result};
|
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<RoomAliasId>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// - Iterator of all our local room aliases for the room ID
|
|
||||||
LocalAliasesForRoom {
|
|
||||||
/// Full room ID
|
|
||||||
room_id: Box<RoomId>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// - 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
|
/// All the getters and iterators in src/database/key_value/rooms/alias.rs
|
||||||
pub(super) async fn room_alias(subcommand: RoomAlias) -> Result<RoomMessageEventContent> {
|
pub(super) async fn room_alias(subcommand: RoomAlias) -> Result<RoomMessageEventContent> {
|
||||||
match subcommand {
|
match subcommand {
|
||||||
|
|
|
@ -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<usize>,
|
|
||||||
},
|
|
||||||
|
|
||||||
#[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<RoomMessageEventContent> {
|
|
||||||
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::<Vec<_>>();
|
|
||||||
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::<Vec<_>>();
|
|
||||||
|
|
||||||
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::<Vec<_>>()
|
|
||||||
.join("\n")
|
|
||||||
);
|
|
||||||
let output_html = format!(
|
|
||||||
"<table><caption>Room list - page \
|
|
||||||
{page}</caption>\n<tr><th>id</th>\t<th>members</th>\t<th>name</th></tr>\n{}</table>",
|
|
||||||
rooms
|
|
||||||
.iter()
|
|
||||||
.fold(String::new(), |mut output, (id, members, name)| {
|
|
||||||
writeln!(
|
|
||||||
output,
|
|
||||||
"<tr><td>{}</td>\t<td>{}</td>\t<td>{}</td></tr>",
|
|
||||||
escape_html(id.as_ref()),
|
|
||||||
members,
|
|
||||||
escape_html(name)
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
output
|
|
||||||
})
|
|
||||||
);
|
|
||||||
Ok(RoomMessageEventContent::text_html(output_plain, output_html))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
160
src/service/admin/room/mod.rs
Normal file
160
src/service/admin/room/mod.rs
Normal file
|
@ -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<usize>,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[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<RoomId>,
|
||||||
|
|
||||||
|
/// 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<Box<RoomId>>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<RoomId>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// - Unpublish a room to the room directory
|
||||||
|
Unpublish {
|
||||||
|
/// The room id of the room to unpublish
|
||||||
|
room_id: Box<RoomId>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// - List rooms that are published
|
||||||
|
List {
|
||||||
|
page: Option<usize>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<RoomOrAliasId>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// - 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<RoomOrAliasId>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// - List of all rooms we have banned
|
||||||
|
ListBannedRooms,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn process(command: RoomCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||||
|
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?,
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,46 +1,10 @@
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
|
|
||||||
use clap::Subcommand;
|
use ruma::{events::room::message::RoomMessageEventContent, RoomAliasId};
|
||||||
use ruma::{events::room::message::RoomMessageEventContent, RoomAliasId, RoomId};
|
|
||||||
|
|
||||||
|
use super::RoomAliasCommand;
|
||||||
use crate::{service::admin::escape_html, services, Result};
|
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<RoomId>,
|
|
||||||
|
|
||||||
/// 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<Box<RoomId>>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn process(command: RoomAliasCommand, _body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
pub(crate) async fn process(command: RoomAliasCommand, _body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||||
match command {
|
match command {
|
||||||
RoomAliasCommand::Set {
|
RoomAliasCommand::Set {
|
59
src/service/admin/room/room_commands.rs
Normal file
59
src/service/admin/room/room_commands.rs
Normal file
|
@ -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<usize>) -> Result<RoomMessageEventContent> {
|
||||||
|
// 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::<Vec<_>>();
|
||||||
|
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::<Vec<_>>();
|
||||||
|
|
||||||
|
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::<Vec<_>>()
|
||||||
|
.join("\n")
|
||||||
|
);
|
||||||
|
let output_html = format!(
|
||||||
|
"<table><caption>Room list - page \
|
||||||
|
{page}</caption>\n<tr><th>id</th>\t<th>members</th>\t<th>name</th></tr>\n{}</table>",
|
||||||
|
rooms
|
||||||
|
.iter()
|
||||||
|
.fold(String::new(), |mut output, (id, members, name)| {
|
||||||
|
writeln!(
|
||||||
|
output,
|
||||||
|
"<tr><td>{}</td>\t<td>{}</td>\t<td>{}</td></tr>",
|
||||||
|
escape_html(id.as_ref()),
|
||||||
|
members,
|
||||||
|
escape_html(name)
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
output
|
||||||
|
})
|
||||||
|
);
|
||||||
|
Ok(RoomMessageEventContent::text_html(output_plain, output_html))
|
||||||
|
}
|
|
@ -1,47 +1,26 @@
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
|
|
||||||
use clap::Subcommand;
|
use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId};
|
||||||
use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId, RoomId};
|
|
||||||
|
|
||||||
|
use super::RoomDirectoryCommand;
|
||||||
use crate::{
|
use crate::{
|
||||||
service::admin::{escape_html, get_room_info, PAGE_SIZE},
|
service::admin::{escape_html, get_room_info, PAGE_SIZE},
|
||||||
services, Result,
|
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<RoomId>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// - Unpublish a room to the room directory
|
|
||||||
Unpublish {
|
|
||||||
/// The room id of the room to unpublish
|
|
||||||
room_id: Box<RoomId>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// - List rooms that are published
|
|
||||||
List {
|
|
||||||
page: Option<usize>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn process(command: RoomDirectoryCommand, _body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
pub(crate) async fn process(command: RoomDirectoryCommand, _body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||||
match command {
|
match command {
|
||||||
RoomDirectoryCommand::Publish {
|
RoomDirectoryCommand::Publish {
|
||||||
room_id,
|
room_id,
|
||||||
} => match services().rooms.directory.set_public(&room_id) {
|
} => match services().rooms.directory.set_public(&room_id) {
|
||||||
Ok(()) => Ok(RoomMessageEventContent::text_plain("Room published")),
|
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 {
|
RoomDirectoryCommand::Unpublish {
|
||||||
room_id,
|
room_id,
|
||||||
} => match services().rooms.directory.set_not_public(&room_id) {
|
} => match services().rooms.directory.set_not_public(&room_id) {
|
||||||
Ok(()) => Ok(RoomMessageEventContent::text_plain("Room unpublished")),
|
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 {
|
RoomDirectoryCommand::List {
|
||||||
page,
|
page,
|
|
@ -1,75 +1,17 @@
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
|
|
||||||
use clap::Subcommand;
|
|
||||||
use ruma::{
|
use ruma::{
|
||||||
events::room::message::RoomMessageEventContent, OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, RoomOrAliasId,
|
events::room::message::RoomMessageEventContent, OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, RoomOrAliasId,
|
||||||
};
|
};
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
|
use super::RoomModerationCommand;
|
||||||
use crate::{
|
use crate::{
|
||||||
api::client_server::{get_alias_helper, leave_room},
|
api::client_server::{get_alias_helper, leave_room},
|
||||||
service::admin::{escape_html, Service},
|
service::admin::{escape_html, Service},
|
||||||
services, Result,
|
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<RoomOrAliasId>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// - 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<RoomOrAliasId>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// - List of all rooms we have banned
|
|
||||||
ListBannedRooms,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn process(command: RoomModerationCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
pub(crate) async fn process(command: RoomModerationCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
||||||
match command {
|
match command {
|
||||||
RoomModerationCommand::BanRoom {
|
RoomModerationCommand::BanRoom {
|
Loading…
Add table
Add a link
Reference in a new issue