use std::fmt::Debug; use async_trait::async_trait; use deadpool_redis::cluster::PoolError; use deadpool_redis::redis::RedisError; use thiserror::Error; use twilight_http::response::DeserializeBodyError; use crate::database::models::{DatabaseError, DatabaseOrDiscordError}; use crate::framework::commands::arguments::Argument; use crate::framework::commands::context::CommandContext; use crate::framework::commands::permissions::Permission; use crate::framework::runtime::Runtime; pub mod arguments; pub mod context; pub mod extras; pub mod messages; pub mod permissions; pub mod runner; /// An error occurred during command execution. #[derive(Debug, Error)] pub enum CommandError { #[error("A database query failed")] DatabaseFailed(#[from] DatabaseError), #[error("A database operation failed due to an inconsistent state")] DatabaseInconsistentState, #[error("The command was used in DMs, but it can only be used in servers")] ServerCommandRunInDMs, #[error(transparent)] DiscordAPIFailed(#[from] twilight_http::Error), #[error(transparent)] DiscordDeserializationFailed(#[from] DeserializeBodyError), #[error(transparent)] RedisPoolFailed(#[from] PoolError), #[error(transparent)] RedisFailed(#[from] RedisError), #[error(transparent)] DatabaseOrDiscordError(#[from] DatabaseOrDiscordError), } /// Represents a bot command. /// /// Only the [`Command::name()`] and the [`Command::execute()`] methods must be implemented. The /// rest have default implementations. They can all be overriden to tweak the command's behavior /// and display in the help command. #[async_trait] pub trait Command: Send + Sync + Debug { /// The command's name. /// /// Returns: /// [`&'static str`] - The command's name. fn name(&self) -> &'static str; /// The command's aliases. /// /// Returns: /// [`&'static [&'static str]`] - The command's aliases. fn aliases(&self) -> &'static [&'static str] { &[] } /// The command's description, shown in the help command. /// /// Returns: /// [`&'static str`] - The command's description. fn description(&self) -> &'static str { "A Payphone command." } /// The command's arguments, shown in the help command. /// /// This does not affect parsing in any way, as that's done inside [`Command::execute()`]. This /// is for display in the help command only. /// /// Returns: /// [`&'static [Argument]`] - The command's arguments. fn arguments(&self) -> &'static [Argument] { &[] } /// Permissions that will be checked for before running the command. /// /// If any of the permissions fail, the command will not be run and the permission's /// [`Permission::on_handle_denied()`] will be called. /// /// Returns: /// [`Vec + Send + Sync>>`] - A vector of the command's permissions. fn permissions(&self) -> Vec + Send + Sync + 'static>> { vec![] } /// Whether the command should be shown in the help command. /// /// This is useful for commands that are not meant to be used by users directly, like internal /// commands or easter egg commands. /// /// Returns: /// [`bool`] - Whether the command should be shown in the help command. fn show_in_help(&self, _rt: &Runtime, _extras: &E) -> bool { true } /// Whether the command only runs when the message starts with the command prefix. /// /// When this is false, the command will be run when the command is found on any part of the /// message. This is useful for about-group commands, like: /// /// ```text /// User: You can donate $6 at p.d to get unbanned. /// Bot: https://ko-fi.com/payphonedev /// ``` /// /// Returns: /// [`bool`] - Whether the command only runs when the message starts with the command prefix. fn only_runs_if_startswith(&self) -> bool { true } /// Returns the command name + all its aliases. /// /// Returns: /// [`Vec<&'static str>`] - A vector with the command's name and its aliases. fn identifiers(&self) -> Vec<&'static str> { let mut all = vec![self.name()]; all.extend_from_slice(self.aliases()); all } /// Executes the command. /// /// Arguments: /// * `ctx` - The command context, containing information about the command invocation and /// helpers for responding. /// * `args` - The raw arguments passed to the command, without any parsing. This is the part /// of the message after the command name. /// /// Returns: /// [`Result<(), CommandError>`] - Ok if the command executed successfully, or an error if /// something went wrong during execution. async fn execute(&self, ctx: CommandContext<'_, T, E>, args: &str) -> Result<(), CommandError>; } /// The parts of a command found in a message, without processing. /// /// This is not guaranteed to point to a valid command, as it only finds a prefix in a message and /// parses it without checking for command existance nor argument validity. pub struct CommandParts<'a> { /// The name of the command. pub name: &'a str, /// The arguments passed to the command, raw as a string. pub args: &'a str, /// The index in the start-trimmed message where the command was found. /// /// For example, given the prefix is `p.`: /// ```text /// Donate $6 at p.d to get unbanned. /// ^ /// index = 13 /// ``` pub start_index: usize, } impl<'a> CommandParts<'a> { /// Creates a new [`CommandParts`] struct. /// /// Arguments: /// * `name` - The name of the command. /// * `args` - The arguments passed to the command, raw as a string. /// * `start_index` - The index in the start-trimmed message where the command was found. /// /// Returns: /// [`CommandParts`] - The created struct. fn new(name: &'a str, args: &'a str, start_index: usize) -> Self { Self { name, args, start_index, } } } /// Parses a message content to get the command name and arguments. /// /// Arguments: /// * `prefix` - The prefix to look for in the message. /// * `message` - The message content to parse. /// /// Returns: /// [`Option`] - The parsed command parts, or None if the prefix was not found in the /// message. pub fn get_command_parts<'a>(prefix: &str, message: &'a str) -> Option> { let message_trimmed_start = message.trim_ascii_start(); let message_trimmed_start_lowercase = message_trimmed_start.to_ascii_lowercase(); let prefix_lowercase = prefix.to_ascii_lowercase(); if let Some(index) = message_trimmed_start_lowercase.find(&prefix_lowercase) { let command_text = &message_trimmed_start[index + prefix.len()..]; match command_text.trim_ascii_start().split_once(" ") { Some((name, args)) => Some(CommandParts::new(name, args, index)), None => Some(CommandParts::new( command_text.trim_ascii_start(), "", index, )), } } else { None } }