Rework admin commands to use subcommands.

This commit doesn't add, remove, or change any
commands, it only organizes them
This commit is contained in:
tezlm 2023-10-03 16:52:52 -07:00 committed by strawberry
parent 5106203d67
commit 3e9c564209

View file

@ -5,7 +5,7 @@ use std::{
time::Instant,
};
use clap::Parser;
use clap::{Parser, Subcommand};
use regex::Regex;
use ruma::{
events::{
@ -41,6 +41,37 @@ use super::pdu::PduBuilder;
#[derive(Parser)]
#[command(name = "@conduit:server.name:", version = env!("CARGO_PKG_VERSION"))]
enum AdminCommand {
#[command(subcommand)]
/// Commands for managing appservices
Appservice(AppserviceCommand),
#[command(subcommand)]
/// Commands for managing local users
User(UserCommand),
#[command(subcommand)]
/// Commands for managing rooms
Room(RoomCommand),
#[command(subcommand)]
/// Commands for managing federation
Federation(FederationCommand),
#[command(subcommand)]
/// Commands for managing the server
Server(ServerCommand),
#[command(subcommand)]
// TODO: should i split out debug commands to a separate thing? the
// debug commands seem like they could fit in the other categories fine
// this is more like a "miscellaneous" category than a debug one
/// Commands for debugging things
Debug(DebugCommand),
}
#[cfg_attr(test, derive(Debug))]
#[derive(Subcommand)]
enum AppserviceCommand {
#[command(verbatim_doc_comment)]
/// Register an appservice using its registration YAML
///
@ -54,33 +85,42 @@ enum AdminCommand {
/// # ```
/// # yaml content here
/// # ```
RegisterAppservice,
Register,
/// Unregister an appservice using its ID
///
/// You can find the ID using the `list-appservices` command.
UnregisterAppservice {
Unregister {
/// The appservice to unregister
appservice_identifier: String,
},
/// List all the currently registered appservices
ListAppservices,
List,
}
/// List all rooms the server knows about
ListRooms,
#[cfg_attr(test, derive(Debug))]
#[derive(Subcommand)]
enum UserCommand {
/// Create a new user
Create {
/// Username of the new user
username: String,
/// Password of the new user, if unspecified one is generated
password: Option<String>,
},
/// List users in the database
ListLocalUsers,
/// List all rooms we are currently handling an incoming pdu from
IncomingFederation,
/// Reset user password
ResetPassword {
/// Username of the user for whom the password should be reset
username: String,
},
/// Deactivate a user
///
/// User will not be removed from all rooms by default.
/// Use --leave-rooms to force the user to leave all rooms
DeactivateUser {
Deactivate {
#[arg(short, long)]
leave_rooms: bool,
user_id: Box<UserId>,
@ -109,6 +149,49 @@ enum AdminCommand {
force: bool,
},
/// List local users in the database
List,
}
#[cfg_attr(test, derive(Debug))]
#[derive(Subcommand)]
enum RoomCommand {
/// List all rooms the server knows about
List,
}
#[cfg_attr(test, derive(Debug))]
#[derive(Subcommand)]
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> },
#[command(verbatim_doc_comment)]
/// Verify json signatures
/// [commandbody]
/// # ```
/// # json here
/// # ```
SignJson,
#[command(verbatim_doc_comment)]
/// Verify json signatures
/// [commandbody]
/// # ```
/// # json here
/// # ```
VerifyJson,
}
#[cfg_attr(test, derive(Debug))]
#[derive(Subcommand)]
enum DebugCommand {
/// Get the auth_chain of a PDU
GetAuthChain {
/// An event ID (the $ character followed by the base64 reference hash)
@ -132,6 +215,13 @@ enum AdminCommand {
/// An event ID (a $ followed by the base64 reference hash)
event_id: Box<EventId>,
},
}
#[cfg_attr(test, derive(Debug))]
#[derive(Subcommand)]
enum ServerCommand {
/// Show configuration values
ShowConfig,
/// Print database memory usage statistics
MemoryUsage,
@ -141,42 +231,6 @@ enum AdminCommand {
/// Clears all of Conduit's service caches with index smaller than the amount
ClearServiceCaches { amount: u32 },
/// Show configuration values
ShowConfig,
/// Reset user password
ResetPassword {
/// Username of the user for whom the password should be reset
username: String,
},
/// Create a new user
CreateUser {
/// Username of the new user
username: String,
/// Password of the new user, if unspecified one is generated
password: Option<String>,
},
/// 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
/// [commandbody]
/// # ```
/// # json here
/// # ```
SignJson,
/// Verify json signatures
/// [commandbody]
/// # ```
/// # json here
/// # ```
VerifyJson,
}
#[derive(Debug)]
@ -341,8 +395,11 @@ impl Service {
body: Vec<&str>,
) -> Result<RoomMessageEventContent> {
let reply_message_content = match command {
AdminCommand::RegisterAppservice => {
if body.len() > 2 && body[0].trim() == "```" && body.last().unwrap().trim() == "```"
AdminCommand::Appservice(command) => match command {
AppserviceCommand::Register => {
if body.len() > 2
&& body[0].trim() == "```"
&& body.last().unwrap().trim() == "```"
{
let appservice_config = body[1..body.len() - 1].join("\n");
let parsed_config =
@ -366,7 +423,7 @@ impl Service {
)
}
}
AdminCommand::UnregisterAppservice {
AppserviceCommand::Unregister {
appservice_identifier,
} => match services()
.appservice
@ -377,7 +434,7 @@ impl Service {
"Failed to unregister appservice: {e}"
)),
},
AdminCommand::ListAppservices => {
AppserviceCommand::List => {
if let Ok(appservices) = services()
.appservice
.iter_ids()
@ -398,217 +455,18 @@ impl Service {
RoomMessageEventContent::text_plain("Failed to get appservices.")
}
}
AdminCommand::ListRooms => {
let room_ids = services().rooms.metadata.iter_ids();
let output = format!(
"Rooms:\n{}",
room_ids
.filter_map(|r| r.ok())
.map(|id| id.to_string()
+ "\tMembers: "
+ &services()
.rooms
.state_cache
.room_joined_count(&id)
.ok()
.flatten()
.unwrap_or(0)
.to_string())
.collect::<Vec<_>>()
.join("\n")
);
RoomMessageEventContent::text_plain(output)
}
AdminCommand::ListLocalUsers => match services().users.list_local_users() {
},
AdminCommand::User(command) => match command {
UserCommand::List => match services().users.list_local_users() {
Ok(users) => {
let mut msg: String = format!("Found {} local user account(s):\n", users.len());
let mut msg: String =
format!("Found {} local user account(s):\n", users.len());
msg += &users.join("\n");
RoomMessageEventContent::text_plain(&msg)
}
Err(e) => RoomMessageEventContent::text_plain(e.to_string()),
},
AdminCommand::IncomingFederation => {
let map = services()
.globals
.roomid_federationhandletime
.read()
.unwrap();
let mut msg: String = format!("Handling {} incoming pdus:\n", map.len());
for (r, (e, i)) in map.iter() {
let elapsed = i.elapsed();
msg += &format!(
"{} {}: {}m{}s\n",
r,
e,
elapsed.as_secs() / 60,
elapsed.as_secs() % 60
);
}
RoomMessageEventContent::text_plain(&msg)
}
AdminCommand::GetAuthChain { event_id } => {
let event_id = Arc::<EventId>::from(event_id);
if let Some(event) = services().rooms.timeline.get_pdu_json(&event_id)? {
let room_id_str = event
.get("room_id")
.and_then(|val| val.as_str())
.ok_or_else(|| Error::bad_database("Invalid event in database"))?;
let room_id = <&RoomId>::try_from(room_id_str).map_err(|_| {
Error::bad_database("Invalid room id field in event in database")
})?;
let start = Instant::now();
let count = services()
.rooms
.auth_chain
.get_auth_chain(room_id, vec![event_id])
.await?
.count();
let elapsed = start.elapsed();
RoomMessageEventContent::text_plain(format!(
"Loaded auth chain with length {count} in {elapsed:?}"
))
} else {
RoomMessageEventContent::text_plain("Event not found.")
}
}
AdminCommand::ParsePdu => {
if body.len() > 2 && body[0].trim() == "```" && body.last().unwrap().trim() == "```"
{
let string = body[1..body.len() - 1].join("\n");
match serde_json::from_str(&string) {
Ok(value) => {
match ruma::signatures::reference_hash(&value, &RoomVersionId::V6) {
Ok(hash) => {
let event_id = EventId::parse(format!("${hash}"));
match serde_json::from_value::<PduEvent>(
serde_json::to_value(value).expect("value is json"),
) {
Ok(pdu) => RoomMessageEventContent::text_plain(format!(
"EventId: {event_id:?}\n{pdu:#?}"
)),
Err(e) => RoomMessageEventContent::text_plain(format!(
"EventId: {event_id:?}\nCould not parse event: {e}"
)),
}
}
Err(e) => RoomMessageEventContent::text_plain(format!(
"Could not parse PDU JSON: {e:?}"
)),
}
}
Err(e) => RoomMessageEventContent::text_plain(format!(
"Invalid json in command body: {e}"
)),
}
} else {
RoomMessageEventContent::text_plain("Expected code block in command body.")
}
}
AdminCommand::GetPdu { event_id } => {
let mut outlier = false;
let mut pdu_json = services()
.rooms
.timeline
.get_non_outlier_pdu_json(&event_id)?;
if pdu_json.is_none() {
outlier = true;
pdu_json = services().rooms.timeline.get_pdu_json(&event_id)?;
}
match pdu_json {
Some(json) => {
let json_text = serde_json::to_string_pretty(&json)
.expect("canonical json is valid json");
RoomMessageEventContent::text_html(
format!(
"{}\n```json\n{}\n```",
if outlier {
"PDU is outlier"
} else {
"PDU was accepted"
},
json_text
),
format!(
"<p>{}</p>\n<pre><code class=\"language-json\">{}\n</code></pre>\n",
if outlier {
"PDU is outlier"
} else {
"PDU was accepted"
},
HtmlEscape(&json_text)
),
)
}
None => RoomMessageEventContent::text_plain("PDU not found."),
}
}
AdminCommand::MemoryUsage => {
let response1 = services().memory_usage();
let response2 = services().globals.db.memory_usage();
RoomMessageEventContent::text_plain(format!(
"Services:\n{response1}\n\nDatabase:\n{response2}"
))
}
AdminCommand::ClearDatabaseCaches { amount } => {
services().globals.db.clear_caches(amount);
RoomMessageEventContent::text_plain("Done.")
}
AdminCommand::ClearServiceCaches { amount } => {
services().clear_caches(amount);
RoomMessageEventContent::text_plain("Done.")
}
AdminCommand::ShowConfig => {
// Construct and send the response
RoomMessageEventContent::text_plain(format!("{}", services().globals.config))
}
AdminCommand::ResetPassword { username } => {
let user_id = match UserId::parse_with_server_name(
username.as_str().to_lowercase(),
services().globals.server_name(),
) {
Ok(id) => id,
Err(e) => {
return Ok(RoomMessageEventContent::text_plain(format!(
"The supplied username is not a valid username: {e}"
)))
}
};
// Check if the specified user is valid
if !services().users.exists(&user_id)?
|| user_id
== UserId::parse_with_server_name(
"conduit",
services().globals.server_name(),
)
.expect("conduit user exists")
{
return Ok(RoomMessageEventContent::text_plain(
"The specified user does not exist!",
));
}
let new_password = utils::random_string(AUTO_GEN_PASSWORD_LENGTH);
match services()
.users
.set_password(&user_id, Some(new_password.as_str()))
{
Ok(()) => RoomMessageEventContent::text_plain(format!(
"Successfully reset the password for user {user_id}: {new_password}"
)),
Err(e) => RoomMessageEventContent::text_plain(format!(
"Couldn't reset the password for user {user_id}: {e}"
)),
}
}
AdminCommand::CreateUser { username, password } => {
UserCommand::Create { username, password } => {
let password =
password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH));
// Validate user id
@ -670,15 +528,7 @@ impl Service {
"Created user with user_id: {user_id} and password: {password}"
))
}
AdminCommand::DisableRoom { room_id } => {
services().rooms.metadata.disable_room(&room_id, true)?;
RoomMessageEventContent::text_plain("Room disabled.")
}
AdminCommand::EnableRoom { room_id } => {
services().rooms.metadata.disable_room(&room_id, false)?;
RoomMessageEventContent::text_plain("Room enabled.")
}
AdminCommand::DeactivateUser {
UserCommand::Deactivate {
leave_rooms,
user_id,
} => {
@ -703,8 +553,51 @@ impl Service {
))
}
}
AdminCommand::DeactivateAll { leave_rooms, force } => {
if body.len() > 2 && body[0].trim() == "```" && body.last().unwrap().trim() == "```"
UserCommand::ResetPassword { username } => {
let user_id = match UserId::parse_with_server_name(
username.as_str().to_lowercase(),
services().globals.server_name(),
) {
Ok(id) => id,
Err(e) => {
return Ok(RoomMessageEventContent::text_plain(format!(
"The supplied username is not a valid username: {e}"
)))
}
};
// Check if the specified user is valid
if !services().users.exists(&user_id)?
|| user_id
== UserId::parse_with_server_name(
"conduit",
services().globals.server_name(),
)
.expect("conduit user exists")
{
return Ok(RoomMessageEventContent::text_plain(
"The specified user does not exist!",
));
}
let new_password = utils::random_string(AUTO_GEN_PASSWORD_LENGTH);
match services()
.users
.set_password(&user_id, Some(new_password.as_str()))
{
Ok(()) => RoomMessageEventContent::text_plain(format!(
"Successfully reset the password for user {user_id}: {new_password}"
)),
Err(e) => RoomMessageEventContent::text_plain(format!(
"Couldn't reset the password for user {user_id}: {e}"
)),
}
}
UserCommand::DeactivateAll { leave_rooms, force } => {
if body.len() > 2
&& body[0].trim() == "```"
&& body.last().unwrap().trim() == "```"
{
let usernames = body.clone().drain(1..body.len() - 1).collect::<Vec<_>>();
@ -762,8 +655,63 @@ impl Service {
)
}
}
AdminCommand::SignJson => {
if body.len() > 2 && body[0].trim() == "```" && body.last().unwrap().trim() == "```"
},
AdminCommand::Room(command) => match command {
RoomCommand::List => {
let room_ids = services().rooms.metadata.iter_ids();
let output = format!(
"Rooms:\n{}",
room_ids
.filter_map(|r| r.ok())
.map(|id| id.to_string()
+ "\tMembers: "
+ &services()
.rooms
.state_cache
.room_joined_count(&id)
.ok()
.flatten()
.unwrap_or(0)
.to_string())
.collect::<Vec<_>>()
.join("\n")
);
RoomMessageEventContent::text_plain(output)
}
},
AdminCommand::Federation(command) => match command {
FederationCommand::DisableRoom { room_id } => {
services().rooms.metadata.disable_room(&room_id, true)?;
RoomMessageEventContent::text_plain("Room disabled.")
}
FederationCommand::EnableRoom { room_id } => {
services().rooms.metadata.disable_room(&room_id, false)?;
RoomMessageEventContent::text_plain("Room enabled.")
}
FederationCommand::IncomingFederation => {
let map = services()
.globals
.roomid_federationhandletime
.read()
.unwrap();
let mut msg: String = format!("Handling {} incoming pdus:\n", map.len());
for (r, (e, i)) in map.iter() {
let elapsed = i.elapsed();
msg += &format!(
"{} {}: {}m{}s\n",
r,
e,
elapsed.as_secs() / 60,
elapsed.as_secs() % 60
);
}
RoomMessageEventContent::text_plain(&msg)
}
FederationCommand::SignJson => {
if body.len() > 2
&& body[0].trim() == "```"
&& body.last().unwrap().trim() == "```"
{
let string = body[1..body.len() - 1].join("\n");
match serde_json::from_str(&string) {
@ -778,7 +726,9 @@ impl Service {
.expect("canonical json is valid json");
RoomMessageEventContent::text_plain(json_text)
}
Err(e) => RoomMessageEventContent::text_plain(format!("Invalid json: {e}")),
Err(e) => {
RoomMessageEventContent::text_plain(format!("Invalid json: {e}"))
}
}
} else {
RoomMessageEventContent::text_plain(
@ -786,8 +736,10 @@ impl Service {
)
}
}
AdminCommand::VerifyJson => {
if body.len() > 2 && body[0].trim() == "```" && body.last().unwrap().trim() == "```"
FederationCommand::VerifyJson => {
if body.len() > 2
&& body[0].trim() == "```"
&& body.last().unwrap().trim() == "```"
{
let string = body[1..body.len() - 1].join("\n");
match serde_json::from_str(&string) {
@ -802,13 +754,17 @@ impl Service {
let pub_key_map = pub_key_map.read().unwrap();
match ruma::signatures::verify_json(&pub_key_map, &value) {
Ok(_) => RoomMessageEventContent::text_plain("Signature correct"),
Ok(_) => {
RoomMessageEventContent::text_plain("Signature correct")
}
Err(e) => RoomMessageEventContent::text_plain(format!(
"Signature verification failed: {e}"
)),
}
}
Err(e) => RoomMessageEventContent::text_plain(format!("Invalid json: {e}")),
Err(e) => {
RoomMessageEventContent::text_plain(format!("Invalid json: {e}"))
}
}
} else {
RoomMessageEventContent::text_plain(
@ -816,6 +772,133 @@ impl Service {
)
}
}
},
AdminCommand::Server(command) => match command {
ServerCommand::ShowConfig => {
// Construct and send the response
RoomMessageEventContent::text_plain(format!("{}", services().globals.config))
}
ServerCommand::MemoryUsage => {
let response1 = services().memory_usage();
let response2 = services().globals.db.memory_usage();
RoomMessageEventContent::text_plain(format!(
"Services:\n{response1}\n\nDatabase:\n{response2}"
))
}
ServerCommand::ClearDatabaseCaches { amount } => {
services().globals.db.clear_caches(amount);
RoomMessageEventContent::text_plain("Done.")
}
ServerCommand::ClearServiceCaches { amount } => {
services().clear_caches(amount);
RoomMessageEventContent::text_plain("Done.")
}
},
AdminCommand::Debug(command) => match command {
DebugCommand::GetAuthChain { event_id } => {
let event_id = Arc::<EventId>::from(event_id);
if let Some(event) = services().rooms.timeline.get_pdu_json(&event_id)? {
let room_id_str = event
.get("room_id")
.and_then(|val| val.as_str())
.ok_or_else(|| Error::bad_database("Invalid event in database"))?;
let room_id = <&RoomId>::try_from(room_id_str).map_err(|_| {
Error::bad_database("Invalid room id field in event in database")
})?;
let start = Instant::now();
let count = services()
.rooms
.auth_chain
.get_auth_chain(room_id, vec![event_id])
.await?
.count();
let elapsed = start.elapsed();
RoomMessageEventContent::text_plain(format!(
"Loaded auth chain with length {count} in {elapsed:?}"
))
} else {
RoomMessageEventContent::text_plain("Event not found.")
}
}
DebugCommand::ParsePdu => {
if body.len() > 2
&& body[0].trim() == "```"
&& body.last().unwrap().trim() == "```"
{
let string = body[1..body.len() - 1].join("\n");
match serde_json::from_str(&string) {
Ok(value) => {
match ruma::signatures::reference_hash(&value, &RoomVersionId::V6) {
Ok(hash) => {
let event_id = EventId::parse(format!("${hash}"));
match serde_json::from_value::<PduEvent>(
serde_json::to_value(value).expect("value is json"),
) {
Ok(pdu) => RoomMessageEventContent::text_plain(
format!("EventId: {event_id:?}\n{pdu:#?}"),
),
Err(e) => RoomMessageEventContent::text_plain(format!(
"EventId: {event_id:?}\nCould not parse event: {e}"
)),
}
}
Err(e) => RoomMessageEventContent::text_plain(format!(
"Could not parse PDU JSON: {e:?}"
)),
}
}
Err(e) => RoomMessageEventContent::text_plain(format!(
"Invalid json in command body: {e}"
)),
}
} else {
RoomMessageEventContent::text_plain("Expected code block in command body.")
}
}
DebugCommand::GetPdu { event_id } => {
let mut outlier = false;
let mut pdu_json = services()
.rooms
.timeline
.get_non_outlier_pdu_json(&event_id)?;
if pdu_json.is_none() {
outlier = true;
pdu_json = services().rooms.timeline.get_pdu_json(&event_id)?;
}
match pdu_json {
Some(json) => {
let json_text = serde_json::to_string_pretty(&json)
.expect("canonical json is valid json");
RoomMessageEventContent::text_html(
format!(
"{}\n```json\n{}\n```",
if outlier {
"PDU is outlier"
} else {
"PDU was accepted"
},
json_text
),
format!(
"<p>{}</p>\n<pre><code class=\"language-json\">{}\n</code></pre>\n",
if outlier {
"PDU is outlier"
} else {
"PDU was accepted"
},
HtmlEscape(&json_text)
),
)
}
None => RoomMessageEventContent::text_plain("PDU not found."),
}
}
},
};
Ok(reply_message_content)