diff --git a/src/database/models.rs b/src/database/models.rs index e89e7a4..cbe1aec 100644 --- a/src/database/models.rs +++ b/src/database/models.rs @@ -253,6 +253,36 @@ impl Server { } } + pub async fn get_or_create_by_id( + id: u64, + session: &Session, + http: &Client, + ) -> Result { + let server = Server::get_by_id(id, session) + .await + .map_err(DatabaseOrDiscordError::Database)?; + + if let Some(server) = server { + Ok(server) + } else { + let server: Server = http + .guild(Id::new(id)) + .await + .map_err(DatabaseOrDiscordError::Discord)? + .model() + .await + .map_err(|_| DatabaseOrDiscordError::DiscordDeserializationFailed)? + .into(); + + server + .insert(session) + .await + .map_err(DatabaseOrDiscordError::Database)?; + + Ok(server) + } + } + pub async fn insert(&self, session: &Session) -> Result<(), DatabaseError> { session .query_unpaged( diff --git a/src/framework/commands/arguments.rs b/src/framework/commands/arguments.rs index 3b88a1b..701b691 100644 --- a/src/framework/commands/arguments.rs +++ b/src/framework/commands/arguments.rs @@ -31,6 +31,10 @@ impl FromArguments for String { } } +/// Consumes the entire remaining string as a single argument. If no string is remaining, it +/// returns an empty string. +/// +/// Parsing this argument type never fails. pub struct GreedyString(pub String); impl FromArguments for GreedyString { diff --git a/src/framework/commands/mod.rs b/src/framework/commands/mod.rs index 72a13c9..22cb951 100644 --- a/src/framework/commands/mod.rs +++ b/src/framework/commands/mod.rs @@ -20,6 +20,9 @@ pub enum CommandError { #[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, } #[async_trait] @@ -66,7 +69,10 @@ pub fn get_command_parts<'a>(prefix: &str, message: &'a str) -> Option Some(CommandParts::new(name, args)), diff --git a/src/framework/commands/runner.rs b/src/framework/commands/runner.rs index 7f2e1c0..b1e8b1e 100644 --- a/src/framework/commands/runner.rs +++ b/src/framework/commands/runner.rs @@ -32,7 +32,10 @@ impl OnMessageHandler for CommandRunner { if let Some(command) = command { command - .execute(CommandContext::new(event, rt.clone(), &prefixes[0]), parts.args) + .execute( + CommandContext::new(event, rt.clone(), &prefixes[0]), + parts.args, + ) .await?; break; diff --git a/src/groups/servers/commands.rs b/src/groups/servers/commands.rs new file mode 100644 index 0000000..660877a --- /dev/null +++ b/src/groups/servers/commands.rs @@ -0,0 +1,83 @@ +use async_trait::async_trait; + +use crate::{ + constants::DEFAULT_PREFIX, + database::models::Server, + discord::sanitizing::SanitizeDiscordMarkdown, + framework::commands::{ + Command, CommandError, + arguments::{Argument, GreedyString, ParseArguments}, + context::CommandContext, + }, + state::State, +}; + +#[derive(Debug)] +pub struct PrefixCommand; + +#[async_trait] +impl Command for PrefixCommand { + fn name(&self) -> &'static str { + "prefix" + } + + fn aliases(&self) -> &'static [&'static str] { + &["p"] + } + + fn description(&self) -> &'static str { + "Sets the command prefix for the server." + } + + fn arguments(&self) -> &'static [Argument] { + &[Argument { + name: "prefix", + is_optional: true, + is_greedy: true, + }] + } + + async fn execute( + &self, + ctx: CommandContext<'_, State>, + args: &str, + ) -> Result<(), CommandError> { + let prefix: GreedyString = ParseArguments::parse(args).unwrap(); + let mut prefix = prefix.trim(); + + let mut server = Server::get_or_create_by_id( + ctx.event + .guild_id + .ok_or(CommandError::ServerCommandRunInDMs)? + .get(), + &ctx.rt.state.db, + &ctx.rt.http, + ) + .await + .inspect_err(|e| log::error!("Failed to get or create server: {e}")) + .map_err(|_| CommandError::DatabaseQueryFailed)?; + + if prefix.is_empty() { + prefix = DEFAULT_PREFIX; + } + + server.prefix = prefix.to_string(); + server + .upsert(&ctx.rt.state.db) + .await + .map_err(|_| CommandError::DatabaseQueryFailed)?; + + ctx.send() + .content(format!( + "✅ Prefix updated! Run `{}help` to see the new prefix.", + prefix.sanitize(false) + )) + .await + .map_err(|e| { + log::error!("Failed to send prefix update message: {e}"); + CommandError::MessageSendingFailed + })?; + + Ok(()) + } +} diff --git a/src/groups/servers/mod.rs b/src/groups/servers/mod.rs index a7e3886..6d110dc 100644 --- a/src/groups/servers/mod.rs +++ b/src/groups/servers/mod.rs @@ -1,6 +1,7 @@ -//! Adding, updating, and removing servers from the database. +//! Syncing servers with the database and server configuration commands. //! //! This sub-module is responsible for registering servers in the database, updating them when they //! update, and removing them when they get deleted or Payphone is kicked. +pub mod commands; pub mod events; diff --git a/src/main.rs b/src/main.rs index 5cdd0f5..7d964b4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -116,18 +116,26 @@ use tower_http::services::ServeDir; use twilight_gateway::Intents; use crate::{ - cli::{Args, Commands}, constants::DEFAULT_PREFIX, database::models::Server, framework::{ + cli::{Args, Commands}, + constants::DEFAULT_PREFIX, + database::models::Server, + framework::{ bot::{Bot, PrefixGetter}, runtime::Runtime, - }, groups::{ + }, + groups::{ about::{commands::HelpCommand, components::HelpGoBackButton}, calls::{ commands::{CallCommand, HangupCommand, SkipCommand}, events::MessageRedirector, }, gateway::events::{ReadyHandler, ReconnectHandler}, - servers::events::{ServerCreateHandler, ServerDeleteHandler, ServerUpdateHandler}, - }, state::State + servers::{ + commands::PrefixCommand, + events::{ServerCreateHandler, ServerDeleteHandler, ServerUpdateHandler}, + }, + }, + state::State, }; mod cli; @@ -217,6 +225,7 @@ async fn start(token: String, database_addr: String) { .command(CallCommand::new()) .command(SkipCommand) .command(HangupCommand) + .command(PrefixCommand) .component(HelpGoBackButton) .on_ready(ReadyHandler) .on_reconnect(ReconnectHandler)