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}");
}
})