Skip to content
Snippets Groups Projects
handler.rs 6.92 KiB
Newer Older
  • Learn to ignore specific revisions
  • use std::time::Instant;
    
    
    use clap::{CommandFactory, Parser};
    
    Jason Volk's avatar
    Jason Volk committed
    use conduit::trace;
    
    use ruma::events::{
    	relation::InReplyTo,
    	room::message::{Relation::Reply, RoomMessageEventContent},
    
    Jason Volk's avatar
    Jason Volk committed
    };
    
    extern crate conduit_service as service;
    
    
    use conduit::{utils::string::common_prefix, Result};
    
    pub(crate) use service::admin::{Command, Service};
    
    use service::admin::{CommandOutput, CommandResult, HandlerResult};
    
    Jason Volk's avatar
    Jason Volk committed
    
    use crate::{
    
    Jason Volk's avatar
    Jason Volk committed
    	appservice, appservice::AppserviceCommand, check, check::CheckCommand, debug, debug::DebugCommand, federation,
    	federation::FederationCommand, media, media::MediaCommand, query, query::QueryCommand, room, room::RoomCommand,
    	server, server::ServerCommand, services, user, user::UserCommand,
    
    Jason Volk's avatar
    Jason Volk committed
    };
    pub(crate) const PAGE_SIZE: usize = 100;
    
    #[cfg_attr(test, derive(Debug))]
    #[derive(Parser)]
    
    Jason Volk's avatar
    Jason Volk committed
    #[command(name = "admin", version = env!("CARGO_PKG_VERSION"))]
    
    Jason Volk's avatar
    Jason Volk committed
    pub(crate) enum AdminCommand {
    	#[command(subcommand)]
    	/// - Commands for managing appservices
    	Appservices(AppserviceCommand),
    
    	#[command(subcommand)]
    	/// - Commands for managing local users
    	Users(UserCommand),
    
    	#[command(subcommand)]
    	/// - Commands for managing rooms
    	Rooms(RoomCommand),
    
    	#[command(subcommand)]
    	/// - Commands for managing federation
    	Federation(FederationCommand),
    
    	#[command(subcommand)]
    	/// - Commands for managing the server
    	Server(ServerCommand),
    
    	#[command(subcommand)]
    	/// - Commands for managing media
    	Media(MediaCommand),
    
    
    Jason Volk's avatar
    Jason Volk committed
    	#[command(subcommand)]
    	/// - Commands for checking integrity
    	Check(CheckCommand),
    
    
    Jason Volk's avatar
    Jason Volk committed
    	#[command(subcommand)]
    	/// - Commands for debugging things
    	Debug(DebugCommand),
    
    	#[command(subcommand)]
    
    Jason Volk's avatar
    Jason Volk committed
    	/// - Low-level queries for database getters and iterators
    
    Jason Volk's avatar
    Jason Volk committed
    	Query(QueryCommand),
    }
    
    #[must_use]
    
    pub(crate) fn handle(command: Command) -> HandlerResult { Box::pin(handle_command(command)) }
    
    #[must_use]
    pub(crate) fn complete(line: &str) -> String { complete_admin_command(AdminCommand::command(), line) }
    
    Jason Volk's avatar
    Jason Volk committed
    
    
    Jason Volk's avatar
    Jason Volk committed
    #[tracing::instrument(skip_all, name = "admin")]
    
    async fn handle_command(command: Command) -> CommandResult {
    
    	let Some(mut content) = process_admin_message(command.command).await else {
    		return Ok(None);
    	};
    
    
    	content.relates_to = command.reply_id.map(|event_id| Reply {
    
    Jason Volk's avatar
    Jason Volk committed
    		in_reply_to: InReplyTo {
    
    Jason Volk's avatar
    Jason Volk committed
    		},
    	});
    
    Jason Volk's avatar
    Jason Volk committed
    
    
    Jason Volk's avatar
    Jason Volk committed
    }
    
    // Parse and process a message from the admin room
    
    async fn process_admin_message(msg: String) -> CommandOutput {
    
    	let mut lines = msg.lines().filter(|l| !l.trim().is_empty());
    
    	let command = lines.next().expect("each string has at least one line");
    
    Jason Volk's avatar
    Jason Volk committed
    	let body = lines.collect::<Vec<_>>();
    
    	let parsed = match parse_admin_command(command) {
    		Ok(parsed) => parsed,
    
    Jason Volk's avatar
    Jason Volk committed
    		Err(error) => {
    			let server_name = services().globals.server_name();
    			let message = error.replace("server.name", server_name.as_str());
    
    			return Some(RoomMessageEventContent::notice_markdown(message));
    
    Jason Volk's avatar
    Jason Volk committed
    		},
    	};
    
    
    	let timer = Instant::now();
    	let result = process_admin_command(parsed, body).await;
    	let elapsed = timer.elapsed();
    	conduit::debug!(?command, ok = result.is_ok(), "command processed in {elapsed:?}");
    	match result {
    		Ok(reply) => Some(reply),
    		Err(error) => Some(RoomMessageEventContent::notice_markdown(format!(
    			"Encountered an error while handling the command:\n```\n{error}\n```"
    		))),
    
    #[tracing::instrument(skip_all, name = "command")]
    async fn process_admin_command(command: AdminCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> {
    	let reply_message_content = match command {
    		AdminCommand::Appservices(command) => appservice::process(command, body).await?,
    		AdminCommand::Media(command) => media::process(command, body).await?,
    		AdminCommand::Users(command) => user::process(command, body).await?,
    		AdminCommand::Rooms(command) => room::process(command, body).await?,
    		AdminCommand::Federation(command) => federation::process(command, body).await?,
    		AdminCommand::Server(command) => server::process(command, body).await?,
    		AdminCommand::Debug(command) => debug::process(command, body).await?,
    		AdminCommand::Query(command) => query::process(command, body).await?,
    		AdminCommand::Check(command) => check::process(command, body).await?,
    	};
    
    	Ok(reply_message_content)
    }
    
    
    Jason Volk's avatar
    Jason Volk committed
    // Parse chat messages from the admin room into an AdminCommand object
    fn parse_admin_command(command_line: &str) -> Result<AdminCommand, String> {
    
    	let argv = parse_command_line(command_line);
    	AdminCommand::try_parse_from(argv).map_err(|error| error.to_string())
    }
    
    fn complete_admin_command(mut cmd: clap::Command, line: &str) -> String {
    	let argv = parse_command_line(line);
    
    	let mut ret = Vec::<String>::with_capacity(argv.len().saturating_add(1));
    
    
    	'token: for token in argv.into_iter().skip(1) {
    		let cmd_ = cmd.clone();
    
    		let mut choice = Vec::new();
    
    
    		for sub in cmd_.get_subcommands() {
    			let name = sub.get_name();
    			if *name == token {
    				// token already complete; recurse to subcommand
    				ret.push(token);
    				cmd.clone_from(sub);
    				continue 'token;
    
    			} else if name.starts_with(&token) {
    
    				// partial match; add to choices
    				choice.push(name);
    			}
    		}
    
    
    		if choice.len() == 1 {
    
    			// One choice. Add extra space because it's complete
    
    			let choice = *choice.first().expect("only choice");
    			ret.push(choice.to_owned());
    
    			ret.push(String::new());
    
    		} else if choice.is_empty() {
    			// Nothing found, return original string
    			ret.push(token);
    
    		} else {
    			// Find the common prefix
    			ret.push(common_prefix(&choice).into());
    		}
    
    		// Return from completion
    		return ret.join(" ");
    	}
    
    	// Return from no completion. Needs a space though.
    
    	ret.push(String::new());
    	ret.join(" ")
    
    }
    
    // Parse chat messages from the admin room into an AdminCommand object
    fn parse_command_line(command_line: &str) -> Vec<String> {
    	let mut argv = command_line
    		.split_whitespace()
    		.map(str::to_owned)
    		.collect::<Vec<String>>();
    
    Jason Volk's avatar
    Jason Volk committed
    
    
    	// Remove any escapes that came with a server-side escape command
    	if !argv.is_empty() && argv[0].ends_with("admin") {
    
    		argv[0] = argv[0].trim_start_matches('\\').into();
    
    Jason Volk's avatar
    Jason Volk committed
    	// First indice has to be "admin" but for console convenience we add it here
    
    	let server_user = services().globals.server_user.as_str();
    	if !argv.is_empty() && !argv[0].ends_with("admin") && !argv[0].starts_with(server_user) {
    
    		argv.insert(0, "admin".to_owned());
    
    Jason Volk's avatar
    Jason Volk committed
    	// Replace `help command` with `command --help`
    	// Clap has a help subcommand, but it omits the long help description.
    	if argv.len() > 1 && argv[1] == "help" {
    		argv.remove(1);
    
    		argv.push("--help".to_owned());
    
    Jason Volk's avatar
    Jason Volk committed
    	}
    
    	// Backwards compatibility with `register_appservice`-style commands
    	if argv.len() > 1 && argv[1].contains('_') {
    
    		argv[1] = argv[1].replace('_', "-");
    
    Jason Volk's avatar
    Jason Volk committed
    	}
    
    	// Backwards compatibility with `register_appservice`-style commands
    	if argv.len() > 2 && argv[2].contains('_') {
    
    		argv[2] = argv[2].replace('_', "-");
    
    Jason Volk's avatar
    Jason Volk committed
    	}
    
    	// if the user is using the `query` command (argv[1]), replace the database
    	// function/table calls with underscores to match the codebase
    	if argv.len() > 3 && argv[1].eq("query") {
    
    		argv[3] = argv[3].replace('_', "-");
    
    Jason Volk's avatar
    Jason Volk committed
    	trace!(?command_line, ?argv, "parse");
    
    Jason Volk's avatar
    Jason Volk committed
    }