diff --git a/src/commands/anon.rs b/src/commands/anon.rs index 97d4f17..fad4ac5 100644 --- a/src/commands/anon.rs +++ b/src/commands/anon.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use anyhow::{bail, Context as _}; +use anyhow::{anyhow, bail, Context as _}; use poise::{ command, serenity_prelude::{ @@ -26,6 +26,7 @@ use poise::{ use crate::{ config::{get_config_key, Config}, emoji::*, + error::UserError, persist::load_or_save_default, Context, Error, }; @@ -50,15 +51,17 @@ pub(crate) async fn anon( } = load_or_save_default(ctx, &get_config_key(ctx)?)?; if !anon_enabled { - bail!("`/anon` is not enabled, the `anon_enabled` config option is `false`"); + bail!(UserError(anyhow!( + "`/anon` is not enabled, the `anon_enabled` config option is `false`", + ))); } if let Some(anon_channel) = anon_channel { if anon_channel != ctx.channel_id() { - bail!( + bail!(UserError(anyhow!( "`/anon` is only allowed in {} due to the `anon_channel` config option being set", anon_channel.mention(), - ); + ))); } } diff --git a/src/commands/debug.rs b/src/commands/debug.rs index a796b66..3a301fd 100644 --- a/src/commands/debug.rs +++ b/src/commands/debug.rs @@ -14,10 +14,17 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use anyhow::anyhow; -use poise::command; +use anyhow::{anyhow, bail}; +use poise::{command, ChoiceParameter}; -use crate::{config::get_config_key, emoji::*, Context, Error}; +use crate::{config::get_config_key, emoji::*, error::UserError, Context, Error}; + +#[derive(ChoiceParameter)] +enum ErrorKind { + User, + Command, + Internal, +} /// Commands to aid in development of the bot #[command(slash_command, subcommands("error", "delete_config"))] @@ -27,8 +34,19 @@ pub(crate) async fn debug(_ctx: Context<'_>) -> Result<(), Error> { /// Fails intentionally #[command(slash_command)] -async fn error(_ctx: Context<'_>) -> Result<(), Error> { - Err(anyhow!("This is a test error").context("This is a wrapper test error")) +async fn error( + _ctx: Context<'_>, + #[description = "Kind of error to return"] kind: ErrorKind, +) -> Result<(), Error> { + match kind { + ErrorKind::User => bail!(UserError( + anyhow!("This is an example of a user error") + .context("This is an example of extra context") + )), + ErrorKind::Command => Err(anyhow!("This is an example of a command error") + .context("This is an example of extra context")), + ErrorKind::Internal => panic!("This is an example of an internal error"), + } } /// Deletes the config file for the current server diff --git a/src/commands/strike.rs b/src/commands/strike.rs index 9121111..6496a13 100644 --- a/src/commands/strike.rs +++ b/src/commands/strike.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use anyhow::{bail, Context as _}; +use anyhow::{anyhow, bail, Context as _}; use chrono::Months; use poise::{ command, @@ -29,6 +29,7 @@ use serde::{Deserialize, Serialize}; use crate::{ config::{get_config_key, Config}, emoji::*, + error::UserError, persist::load_or_save_default, Context, Error, }; @@ -107,7 +108,9 @@ fn pre_strike_command(ctx: Context<'_>) -> Result, Error> { } = load_or_save_default(ctx, &get_config_key(ctx)?)?; if !strikes_enabled { - bail!("Strikes are not enabled, see `/config get strikes_enabled`"); + bail!(UserError(anyhow!( + "Strikes are not enabled, see `/config get strikes_enabled`", + ))); } Ok(strikes_log_channel) @@ -215,9 +218,9 @@ async fn history( .permissions .map_or(false, |permissions| permissions.view_audit_log()) { - bail!( - "You must have the View Audit Log permission to see the strike history of other users" - ); + bail!(UserError(anyhow!( + "You must have the View Audit Log permission to see the strike history of other users", + ))); } let strikes_key = &get_strikes_key(ctx, user.id)?; @@ -286,7 +289,9 @@ async fn repeal( strike_i: Option, ) -> Result<(), Error> { if user == ctx.author().id { - bail!("You cannot repeal one of your own strikes"); + bail!(UserError(anyhow!( + "You cannot repeal one of your own strikes", + ))); } let log_channel = pre_strike_command(ctx)?; @@ -295,14 +300,16 @@ async fn repeal( let strike_i = strike_i.unwrap_or(strikes.len()); let repealer = &mut strikes .get_mut(strike_i - 1) - .context(format!("User does not have a strike #{strike_i}"))? + .context(UserError(anyhow!( + "User does not have a strike #{strike_i}", + )))? .repealer; if repealer.is_some() { - bail!( + bail!(UserError(anyhow!( "{}'s strike #{strike_i} has already been repealed", user.mention(), - ); + ))); } *repealer = Some(ctx.author().id); diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..b26279c --- /dev/null +++ b/src/error.rs @@ -0,0 +1,174 @@ +// Goober Bot, Discord bot +// Copyright (C) 2024 Valentine Briese +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use std::fmt::{self, Debug}; + +use poise::{ + serenity_prelude::{self, Color, CreateAllowedMentions, CreateEmbed}, + CreateReply, FrameworkError, +}; +use tracing::{error, warn}; + +use crate::emoji::*; + +pub(crate) type Error = anyhow::Error; + +#[derive(Debug)] +pub(crate) struct UserError(pub(crate) Error); + +impl fmt::Display for UserError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "User performed an action improperly") + } +} + +impl std::error::Error for UserError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(self.0.as_ref()) + } +} + +pub(super) async fn on_error( + error: FrameworkError<'_, impl Debug, Error>, +) -> Result<(), serenity_prelude::Error> { + match error { + FrameworkError::Command { error, ctx, .. } => { + if let Some(downcasted_error) = error.downcast_ref() { + let user_error: &UserError = downcasted_error; + + warn!("{error:#}"); + + ctx.send( + CreateReply::default() + .embed( + CreateEmbed::new() + .title(format!("User Error {A_FLOOF_LOAD}")) + .description(format!("{:?}", user_error.0)) + .color(Color::GOLD), + ) + .allowed_mentions(CreateAllowedMentions::new()) + .ephemeral(true), + ) + .await?; + + return Ok(()); + } + + error!("An error occured in a command: {error:#?}"); + + ctx.send( + CreateReply::default() + .embed( + CreateEmbed::new() + .title(format!("Command Error {A_FLOOF_LOAD}")) + .description(format!("{error:?}")) + .color(Color::RED), + ) + .allowed_mentions(CreateAllowedMentions::new()) + .ephemeral(true), + ) + .await?; + } + FrameworkError::CommandPanic { + payload: _, ctx, .. + } => { + // Not showing the payload to the user because it may contain sensitive info + ctx.send( + CreateReply::default() + .embed( + CreateEmbed::new() + .title(format!("Internal Error {FLOOF_NERVOUS}")) + .description("Something went *seriously* wrong- please join the support server and let a developer know!") + .color(Color::DARK_RED), + ) + .ephemeral(true), + ) + .await?; + } + FrameworkError::ArgumentParse { + error, input, ctx, .. + } => { + let for_input = match input { + Some(input) => format!(" for input \"{input}\""), + None => String::new(), + }; + + error!("An argument parsing error occured{for_input}: {error}: {ctx:#?}"); + + ctx.send( + CreateReply::default() + .embed( + CreateEmbed::new() + .title(format!("Argument Parsing Error {A_FLOOF_LOAD}")) + .description("There's probably been an update to this command recently. Please try running it again in a few seconds.") + .color(Color::RED), + ) + .ephemeral(true), + ) + .await?; + } + FrameworkError::MissingBotPermissions { + missing_permissions, + ctx, + .. + } => { + warn!("Missing bot permissions: {missing_permissions}: {ctx:#?}"); + + ctx.send( + CreateReply::default() + .embed( + CreateEmbed::new() + .title(format!("Missing Bot Permissions {FLOOF_NERVOUS}")) + .description(format!("I can't execute this command because I don't have these permissions: {missing_permissions}")) + .color(Color::GOLD), + ) + .ephemeral(true), + ) + .await?; + } + FrameworkError::MissingUserPermissions { + missing_permissions, + ctx, + .. + } => { + ctx.send( + CreateReply::default() + .embed( + CreateEmbed::new() + .title(format!("Missing User Permissions {FLOOF_NERVOUS}")) + .description(match missing_permissions { + Some(missing_permissions) => { + warn!("Missing user permissions: {missing_permissions}: {ctx:#?}"); + + format!("You need these permissions to use this command: {missing_permissions}") + }, + None => { + warn!("Missing user permissions: {ctx:#?}"); + + "I'm not sure what exactly you're missing, but you're missing some permission you need for this command, so I can't let you continue. Sorry!".to_string() + }, + }) + .color(Color::GOLD), + ) + .ephemeral(true), + ) + .await?; + } + other => poise::builtins::on_error(other).await?, + } + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 2afeca6..2b892c8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,24 +21,25 @@ mod activity; mod commands; mod config; mod emoji; +mod error; mod persist; +pub(crate) use crate::error::Error; + use std::fmt::Debug; -use activity::start_activity_loop; use anyhow::Context as _; -use emoji::*; use octocrab::Octocrab; use poise::{ - serenity_prelude::{ - self, ClientBuilder, Color, CreateAllowedMentions, CreateEmbed, GatewayIntents, - }, - CreateReply, Framework, FrameworkError, FrameworkOptions, + serenity_prelude::{ClientBuilder, GatewayIntents}, + Framework, FrameworkOptions, }; use shuttle_persist_msgpack::PersistInstance; use shuttle_runtime::{CustomError, SecretStore}; use shuttle_serenity::ShuttleSerenity; -use tracing::{error, info, warn}; +use tracing::{error, info}; + +use crate::activity::start_activity_loop; /// User data, which is stored and accessible in all command invocations #[derive(Debug)] @@ -46,120 +47,8 @@ struct Data { persist: PersistInstance, } -type Error = anyhow::Error; type Context<'a> = poise::Context<'a, Data, Error>; -async fn on_error( - error: FrameworkError<'_, U, E>, -) -> Result<(), serenity_prelude::Error> { - match error { - FrameworkError::Command { error, ctx, .. } => { - error!("An error occured in a command: {error:#?}"); - - ctx.send( - CreateReply::default() - .embed( - CreateEmbed::new() - .title(format!("Command Error {A_FLOOF_LOAD}")) - .description(format!("{error:?}")) - .color(Color::RED), - ) - .allowed_mentions(CreateAllowedMentions::new()) - .ephemeral(true), - ) - .await?; - } - FrameworkError::CommandPanic { - payload: _, ctx, .. - } => { - // Not showing the payload to the user because it may contain sensitive info - ctx.send( - CreateReply::default() - .embed( - CreateEmbed::new() - .title(format!("Internal Error {FLOOF_NERVOUS}")) - .description("Something went *seriously* wrong- please join the support server and let a developer know!") - .color(Color::RED), - ) - .ephemeral(true), - ) - .await?; - } - FrameworkError::ArgumentParse { - error, input, ctx, .. - } => { - let for_input = match input { - Some(input) => format!(" for input \"{input}\""), - None => String::new(), - }; - - error!("An argument parsing error occured{for_input}: {error}: {ctx:#?}"); - - ctx.send( - CreateReply::default() - .embed( - CreateEmbed::new() - .title(format!("Argument Parsing Error {A_FLOOF_LOAD}")) - .description("There's probably been an update to this command recently. Please try running it again in a few seconds.") - .color(Color::RED), - ) - .ephemeral(true), - ) - .await?; - } - FrameworkError::MissingBotPermissions { - missing_permissions, - ctx, - .. - } => { - warn!("Missing bot permissions: {missing_permissions}: {ctx:#?}"); - - ctx.send( - CreateReply::default() - .embed( - CreateEmbed::new() - .title(format!("Missing Bot Permissions {FLOOF_NERVOUS}")) - .description(format!("I can't execute this command because I don't have these permissions: {missing_permissions}")) - .color(Color::RED), - ) - .ephemeral(true), - ) - .await?; - } - FrameworkError::MissingUserPermissions { - missing_permissions, - ctx, - .. - } => { - ctx.send( - CreateReply::default() - .embed( - CreateEmbed::new() - .title(format!("Missing User Permissions {FLOOF_NERVOUS}")) - .description(match missing_permissions { - Some(missing_permissions) => { - warn!("Missing user permissions: {missing_permissions}: {ctx:#?}"); - - format!("You need these permissions to use this command: {missing_permissions}") - }, - None => { - warn!("Missing user permissions: {ctx:#?}"); - - "I'm not sure what exactly you're missing, but you're missing some permission you need for this command, so I can't let you continue. Sorry!".to_string() - }, - }) - .color(Color::RED), - ) - .ephemeral(true), - ) - .await?; - } - other => poise::builtins::on_error(other).await?, - } - - Ok(()) -} - #[shuttle_runtime::main] async fn main( #[shuttle_runtime::Secrets] secret_store: SecretStore, @@ -192,7 +81,7 @@ async fn main( ], on_error: |error| { Box::pin(async move { - if let Err(e) = on_error(error).await { + if let Err(e) = error::on_error(error).await { error!("Error while handling error: {e}"); } })