From 30fca2754b98ccce0f2b52d206046379583f50d7 Mon Sep 17 00:00:00 2001 From: Ryan Cao <70191398+ryanccn@users.noreply.github.com> Date: Mon, 23 Oct 2023 19:21:48 +0800 Subject: [PATCH] feat: comprehensive error handling --- Cargo.lock | 10 +++ Cargo.toml | 1 + src/handlers/error.rs | 142 ++++++++++++++++++++++++++++++++++++++++++ src/handlers/mod.rs | 5 +- src/main.rs | 124 ++++++++++++++---------------------- 5 files changed, 203 insertions(+), 79 deletions(-) create mode 100644 src/handlers/error.rs diff --git a/Cargo.lock b/Cargo.lock index a65646d..07baefe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1033,6 +1033,15 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "nanoid" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" +dependencies = [ + "rand", +] + [[package]] name = "num" version = "0.4.1" @@ -1925,6 +1934,7 @@ dependencies = [ "chrono", "dotenvy", "humantime", + "nanoid", "num", "once_cell", "owo-colors", diff --git a/Cargo.toml b/Cargo.toml index 10f10e1..ee6b44d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ anyhow = "1.0.75" chrono = "0.4.31" dotenvy = "0.15.7" humantime = "2.1.0" +nanoid = "0.4.0" num = "0.4.1" once_cell = "1.18.0" owo-colors = { version = "3.5.0", features = ["supports-colors"] } diff --git a/src/handlers/error.rs b/src/handlers/error.rs new file mode 100644 index 0000000..da016db --- /dev/null +++ b/src/handlers/error.rs @@ -0,0 +1,142 @@ +use nanoid::nanoid; +use owo_colors::OwoColorize; +use poise::{ + serenity_prelude::{ChannelId, CreateEmbed, CreateEmbedFooter, CreateMessage, Timestamp}, + CreateReply, FrameworkError, +}; + +use crate::{Context, Data}; + +enum ErrorOrPanic<'a> { + Error(&'a anyhow::Error), + Panic(&'a Option), +} + +impl ErrorOrPanic<'_> { + fn type_(&self) -> String { + match self { + Self::Panic(_) => "panic".to_owned(), + Self::Error(_) => "error".to_owned(), + } + } +} + +struct ValfiskError<'a> { + error_or_panic: ErrorOrPanic<'a>, + ctx: &'a Context<'a>, + error_id: String, +} + +impl ValfiskError<'_> { + fn from_error<'a>(error: &'a anyhow::Error, ctx: &'a Context) -> ValfiskError<'a> { + ValfiskError { + error_or_panic: ErrorOrPanic::Error(&error), + ctx, + error_id: nanoid!(8), + } + } + + fn from_panic<'a>(panic_payload: &'a Option, ctx: &'a Context) -> ValfiskError<'a> { + ValfiskError { + error_or_panic: ErrorOrPanic::Panic(&panic_payload), + ctx, + error_id: nanoid!(8), + } + } + + fn log(&self) { + eprintln!( + "{}\n {} {}\n {} {}\n{:#?}", + format!("Encountered {}!", self.error_or_panic.type_()).red(), + "ID:".dimmed(), + self.error_id, + "Command:".dimmed(), + self.ctx.invoked_command_name(), + self.error_or_panic + ); + } + + async fn reply(&self) { + self.ctx + .send( + CreateReply::new().embed( + CreateEmbed::new() + .title("An error occurred!") + .description("Hmm. I wonder what happened there?") + .footer(CreateEmbedFooter::new(&self.error_id)) + .timestamp(Timestamp::now()) + .color(0xef4444), + ), + ) + .await + .ok(); + } + + async fn post(&self) { + let channel_id = match std::env::var("ERROR_LOGS_CHANNEL") { + Ok(channel_id_str) => Some(channel_id_str.parse::()), + Err(_) => None, + }; + + if let Some(Ok(channel_id)) = channel_id { + let channel = ChannelId::new(channel_id); + + let embed = CreateEmbed::new() + .title("An error occurred!") + .description(format!("```\n{:#?}\n```", self.error_or_panic)) + .footer(CreateEmbedFooter::new(&self.error_id)) + .timestamp(Timestamp::now()) + .color(0xef4444); + + channel + .send_message(&self.ctx, CreateMessage::new().embed(embed)) + .await + .ok(); + } + } + + async fn handle_all(&self) { + self.log(); + self.reply().await; + self.post().await; + } +} + +impl std::fmt::Debug for ErrorOrPanic<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Error(e) => e.fmt(f), + Self::Panic(p) => p.fmt(f), + } + } +} + +pub async fn handle_error(err: &FrameworkError<'_, Data, anyhow::Error>) { + match err { + FrameworkError::Setup { error, .. } => { + eprintln!( + "{} setting up client:\n {}", + "Encountered error".red(), + error + ); + } + + FrameworkError::EventHandler { error, .. } => { + eprintln!( + "{} handling event!\n{:#?}", + "Encountered error".red(), + error + ); + } + + FrameworkError::Command { error, ctx, .. } => { + ValfiskError::from_error(error, ctx).handle_all().await; + } + + FrameworkError::CommandPanic { payload, ctx, .. } => { + ValfiskError::from_panic(payload, ctx).handle_all().await; + } + + _ => {} + } +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index ca2db01..8880211 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -1,9 +1,12 @@ use anyhow::Result; use poise::serenity_prelude as serenity; +mod error; mod github_expansion; -pub async fn handle(message: &serenity::Message, ctx: &serenity::Context) -> Result<()> { +pub use error::handle_error; + +pub async fn handle_message(message: &serenity::Message, ctx: &serenity::Context) -> Result<()> { tokio::try_join!(github_expansion::handle(message, ctx))?; Ok(()) diff --git a/src/main.rs b/src/main.rs index cbfa312..02e5560 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,8 +2,8 @@ use anyhow::{Error, Result}; use owo_colors::OwoColorize; use poise::{ - serenity_prelude::{Client, CreateEmbed, FullEvent, GatewayIntents}, - CreateReply, Framework, FrameworkError, FrameworkOptions, + serenity_prelude::{Client, FullEvent, GatewayIntents}, + Framework, FrameworkOptions, }; use crate::utils::Pluralize; @@ -22,90 +22,58 @@ async fn main() -> Result<()> { #[cfg(debug_assertions)] dotenvy::dotenv().ok(); - let mut client = Client::builder( - std::env::var("DISCORD_TOKEN").expect("missing DISCORD_TOKEN"), - GatewayIntents::all(), - ) - .framework(Framework::new( - FrameworkOptions { - commands: commands::vec(), - event_handler: |ev, _, _| { - Box::pin(async move { - match ev { - FullEvent::Message { new_message, ctx } => { - handlers::handle(new_message, ctx).await?; - } + let mut client = Client::builder(std::env::var("DISCORD_TOKEN")?, GatewayIntents::all()) + .framework(Framework::new( + FrameworkOptions { + commands: commands::vec(), + event_handler: |ev, _, _| { + Box::pin(async move { + match ev { + FullEvent::Message { new_message, ctx } => { + handlers::handle_message(new_message, ctx).await?; + } - FullEvent::PresenceUpdate { new_data, .. } => { - let mut presence_store = presence_api::PRESENCE_STORE.lock().await; - presence_store.insert( - new_data.user.id, - presence_api::ValfiskPresenceData::from_presence(new_data), - ); - } + FullEvent::PresenceUpdate { new_data, .. } => { + let mut presence_store = presence_api::PRESENCE_STORE.lock().await; + presence_store.insert( + new_data.user.id, + presence_api::ValfiskPresenceData::from_presence(new_data), + ); + } - &_ => {} - } + &_ => {} + } - Ok(()) - }) + Ok(()) + }) + }, + on_error: |err| { + Box::pin(async move { + handlers::handle_error(&err).await; + }) + }, + ..Default::default() }, - on_error: |err| { + |ctx, ready, framework| { Box::pin(async move { - match err { - FrameworkError::Setup { error, .. } => eprintln!("{}", error), - FrameworkError::Command { error, ctx, .. } => { - eprintln!( - "Encountered error handling command {}: {}", - ctx.invoked_command_name(), - error - ); + let tag = ready.user.tag(); + println!("{} to Discord as {}", "Connected".green(), tag.cyan()); - ctx.send( - CreateReply::new().embed( - CreateEmbed::new() - .title("An error occurred!") - .description(format!("```\n{}\n```", error)), - ), - ) - .await - .ok(); - } - FrameworkError::EventHandler { error, .. } => { - eprintln!("{}", error); - } - FrameworkError::CommandPanic { - payload: Some(payload), - .. - } => { - eprintln!("{}", payload); - } - _ => {} - } - }) - }, - ..Default::default() - }, - |ctx, ready, framework| { - Box::pin(async move { - let tag = ready.user.tag(); - println!("{} to Discord as {}", "Connected".green(), tag.cyan()); - - let commands = &framework.options().commands; + let commands = &framework.options().commands; - poise::builtins::register_globally(&ctx, commands).await?; - println!( - "{} {} {}", - "Registered".blue(), - commands.len(), - "command".pluralize(commands.len()) - ); + poise::builtins::register_globally(&ctx, commands).await?; + println!( + "{} {} {}", + "Registered".blue(), + commands.len(), + "command".pluralize(commands.len()) + ); - Ok(Data {}) - }) - }, - )) - .await?; + Ok(Data {}) + }) + }, + )) + .await?; tokio::select! { result = client.start() => { result.map_err(anyhow::Error::from) },