From 76c0f94e6d7aa108424b34826eb7d8514b026287 Mon Sep 17 00:00:00 2001 From: seth Date: Thu, 30 Nov 2023 22:18:51 -0500 Subject: feat: use eyre, better logging, & refactor small commits be damned --- Cargo.lock | 113 +++++++++++++++++++++++++++++++++++++++ Cargo.toml | 7 ++- src/api/guzzle.rs | 30 ++++------- src/api/mod.rs | 6 ++- src/api/shiggy.rs | 30 ++++------- src/colors.rs | 2 + src/commands/ask.rs | 23 ++++---- src/commands/bing.rs | 6 ++- src/commands/convert.rs | 29 ++++------ src/commands/copypasta.rs | 10 ++-- src/commands/mod.rs | 8 +-- src/commands/random.rs | 42 +++++---------- src/commands/teawiespam.rs | 8 +-- src/commands/version.rs | 6 ++- src/handler/message.rs | 35 ------------ src/handler/mod.rs | 40 -------------- src/handler/pinboard.rs | 77 -------------------------- src/handler/reactboard.rs | 66 ----------------------- src/handlers/error.rs | 42 +++++++++++++++ src/handlers/event/message.rs | 35 ++++++++++++ src/handlers/event/mod.rs | 40 ++++++++++++++ src/handlers/event/pinboard.rs | 77 ++++++++++++++++++++++++++ src/handlers/event/reactboard.rs | 64 ++++++++++++++++++++++ src/handlers/mod.rs | 5 ++ src/main.rs | 73 ++++++++++++------------- src/utils.rs | 60 ++++++++++----------- 26 files changed, 527 insertions(+), 407 deletions(-) delete mode 100644 src/handler/message.rs delete mode 100644 src/handler/mod.rs delete mode 100644 src/handler/pinboard.rs delete mode 100644 src/handler/reactboard.rs create mode 100644 src/handlers/error.rs create mode 100644 src/handlers/event/message.rs create mode 100644 src/handlers/event/mod.rs create mode 100644 src/handlers/event/pinboard.rs create mode 100644 src/handlers/event/reactboard.rs create mode 100644 src/handlers/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 861ee8f..303384f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -221,6 +221,33 @@ dependencies = [ "vec_map", ] +[[package]] +name = "color-eyre" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a667583cca8c4f8436db8de46ea8233c42a7d9ae424a82d338f2e4675229204" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -383,6 +410,16 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "eyre" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80f656be11ddf91bd709454d15d5bd896fbaf4cc3314e69349e4d1569f5b46cd" +dependencies = [ + "indenter", + "once_cell", +] + [[package]] name = "flate2" version = "1.0.28" @@ -705,6 +742,12 @@ dependencies = [ "quote", ] +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + [[package]] name = "indexmap" version = "1.9.3" @@ -747,6 +790,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.150" @@ -860,6 +909,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + [[package]] name = "parking_lot" version = "0.12.1" @@ -1377,6 +1432,24 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -1490,6 +1563,7 @@ name = "teawiebot" version = "1.0.0" dependencies = [ "bottomify", + "color-eyre", "dotenvy", "env_logger", "include_dir", @@ -1541,6 +1615,16 @@ dependencies = [ "syn 2.0.39", ] +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "time" version = "0.3.30" @@ -1597,6 +1681,7 @@ dependencies = [ "mio", "num_cpus", "pin-project-lite", + "signal-hook-registry", "socket2 0.5.5", "tokio-macros", "windows-sys", @@ -1684,6 +1769,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", ] [[package]] @@ -1791,6 +1898,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vec_map" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index 69ce46c..9969c79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ readme = "README.md" [dependencies] bottomify = "1.2.0" +color-eyre = "0.6.2" dotenvy = "0.15.7" env_logger = "0.10.0" include_dir = "0.7.3" @@ -22,5 +23,9 @@ reqwest = { version = "0.11.22", default-features = false, features = [ "json", ] } serde = "1.0.193" -tokio = { version = "1.33.0", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.33.0", features = [ + "macros", + "rt-multi-thread", + "signal", +] } url = { version = "2.5.0", features = ["serde"] } diff --git a/src/api/guzzle.rs b/src/api/guzzle.rs index 17d2c0c..83c159e 100644 --- a/src/api/guzzle.rs +++ b/src/api/guzzle.rs @@ -1,6 +1,6 @@ use crate::api::REQWEST_CLIENT; -use crate::Error; +use color_eyre::eyre::{eyre, Result}; use log::*; use reqwest::StatusCode; use serde::{Deserialize, Serialize}; @@ -11,32 +11,20 @@ struct GuzzleResponse { } const GUZZLE: &str = "https://api.mydadleft.me"; +const RANDOM_TEAWIE: &str = "/random_teawie"; -pub async fn get_random_teawie() -> Result { - let endpoint = "/get_random_teawie"; - +pub async fn get_random_teawie() -> Result { let req = REQWEST_CLIENT - .get(format!("{GUZZLE}{endpoint}")) - .build() - .unwrap(); + .get(format!("{GUZZLE}{RANDOM_TEAWIE}")) + .build()?; info!("making request to {}", req.url()); - let resp = REQWEST_CLIENT.execute(req).await.unwrap(); + let resp = REQWEST_CLIENT.execute(req).await?; let status = resp.status(); if let StatusCode::OK = status { - match resp.json::().await { - Ok(data) => Ok(data.url), - Err(why) => { - if let Some(url) = why.url() { - error!("error parsing json from {}! {}", url, why) - } else { - error!("couldn't even get the url! {}", why); - } - - Err(Box::new(why)) - } - } + let data = resp.json::().await?; + Ok(data.url) } else { error!( "couldn't fetch random teawie from {}! {}", @@ -44,6 +32,6 @@ pub async fn get_random_teawie() -> Result { status ); - Err(status.to_string().into()) + Err(eyre!("failed to get random teawie with {status}")) } } diff --git a/src/api/mod.rs b/src/api/mod.rs index a1e0e97..2ce664e 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -3,11 +3,13 @@ use once_cell::sync::Lazy; pub mod guzzle; pub mod shiggy; -pub const USER_AGENT: &str = "teawieBot/0.1.0"; +pub const USER_AGENT: &str = "teawieBot/"; pub static REQWEST_CLIENT: Lazy = Lazy::new(|| { + let version = option_env!("CARGO_PKG_VERSION").unwrap_or("development"); + reqwest::Client::builder() - .user_agent(USER_AGENT) + .user_agent(format!("{USER_AGENT}/{version}")) .build() .unwrap_or_default() }); diff --git a/src/api/shiggy.rs b/src/api/shiggy.rs index 8dbadef..7a582ee 100644 --- a/src/api/shiggy.rs +++ b/src/api/shiggy.rs @@ -1,42 +1,30 @@ use crate::api::REQWEST_CLIENT; -use crate::Error; +use color_eyre::eyre::{eyre, Result}; use log::*; use reqwest::StatusCode; use serde::Deserialize; const SHIGGY: &str = "https://safebooru.donmai.us"; +const RANDOM_SHIGGY: &str = "/posts/random.json?tags=kemomimi-chan_(naga_u)+naga_u&only=file_url"; #[derive(Deserialize)] struct SafebooruResponse { file_url: String, } -pub async fn get_random_shiggy() -> Result { - let endpoint = "/posts/random.json?tags=kemomimi-chan_(naga_u)+naga_u&only=file_url"; - +pub async fn get_random_shiggy() -> Result { let req = REQWEST_CLIENT - .get(format!("{SHIGGY}{endpoint}")) - .build() - .unwrap(); + .get(format!("{SHIGGY}{RANDOM_SHIGGY}")) + .build()?; info!("making request to {}", req.url()); - let resp = REQWEST_CLIENT.execute(req).await.unwrap(); + let resp = REQWEST_CLIENT.execute(req).await?; let status = resp.status(); if let StatusCode::OK = status { - match resp.json::().await { - Ok(data) => Ok(data.file_url), - Err(why) => { - if let Some(url) = why.url() { - error!("failed to make a request to {}! {}", url, why) - } else { - error!("couldn't even figure out the url! {}", why) - }; - - Err(Box::new(why)) - } - } + let data = resp.json::().await?; + Ok(data.file_url) } else { error!( "couldn't fetch random teawie from {}! {}", @@ -44,6 +32,6 @@ pub async fn get_random_shiggy() -> Result { status ); - Err(status.to_string().into()) + Err(eyre!("failed to get random teawie with {status}")) } } diff --git a/src/colors.rs b/src/colors.rs index ebd231e..5291933 100644 --- a/src/colors.rs +++ b/src/colors.rs @@ -2,12 +2,14 @@ use poise::serenity_prelude::Colour; pub enum Colors { Blue, + Orange, } impl From for Colour { fn from(val: Colors) -> Self { match val { Colors::Blue => Colour::from((136, 199, 253)), + Colors::Orange => Colour::from((255, 179, 74)), } } } diff --git a/src/commands/ask.rs b/src/commands/ask.rs index 7cc82a1..3589484 100644 --- a/src/commands/ask.rs +++ b/src/commands/ask.rs @@ -1,6 +1,6 @@ -use crate::consts; -use crate::utils; -use crate::{Context, Error}; +use crate::{consts, utils, Context}; + +use color_eyre::eyre::{Context as _, Result}; /// ask teawie a question! #[poise::command(prefix_command, slash_command)] @@ -9,15 +9,10 @@ pub async fn ask( #[description = "the question you want to ask teawie"] #[rename = "question"] _question: String, -) -> Result<(), Error> { - match utils::random_choice(consts::RESPONSES) { - Ok(resp) => { - ctx.say(resp).await?; - Ok(()) - } - Err(why) => { - ctx.say("idk").await?; - Err(why) - } - } +) -> Result<()> { + let resp = utils::random_choice(consts::RESPONSES) + .wrap_err("couldn't choose from random responses!")?; + + ctx.say(resp).await?; + Ok(()) } diff --git a/src/commands/bing.rs b/src/commands/bing.rs index ed91bb3..b80ebca 100644 --- a/src/commands/bing.rs +++ b/src/commands/bing.rs @@ -1,8 +1,10 @@ -use crate::{Context, Error}; +use crate::Context; + +use color_eyre::eyre::Result; /// make sure the wie is alive #[poise::command(prefix_command)] -pub async fn bing(ctx: Context<'_>) -> Result<(), Error> { +pub async fn bing(ctx: Context<'_>) -> Result<()> { ctx.say("bong!").await?; Ok(()) } diff --git a/src/commands/convert.rs b/src/commands/convert.rs index 1f39ae4..cbbf8dc 100644 --- a/src/commands/convert.rs +++ b/src/commands/convert.rs @@ -1,11 +1,13 @@ -use crate::{Context, Error}; +use crate::Context; + use bottomify::bottom; +use color_eyre::eyre::Result; #[poise::command( slash_command, subcommands("to_fahrenheit", "to_celsius", "to_bottom", "from_bottom") )] -pub async fn convert(_ctx: Context<'_>) -> Result<(), Error> { +pub async fn convert(_ctx: Context<'_>) -> Result<()> { Ok(()) } @@ -14,7 +16,7 @@ pub async fn convert(_ctx: Context<'_>) -> Result<(), Error> { pub async fn to_celsius( ctx: Context<'_>, #[description = "what teawie will convert"] degrees_fahrenheit: f32, -) -> Result<(), Error> { +) -> Result<()> { let temp = (degrees_fahrenheit - 32.0) * (5.0 / 9.0); ctx.say(temp.to_string()).await?; Ok(()) @@ -25,7 +27,7 @@ pub async fn to_celsius( pub async fn to_fahrenheit( ctx: Context<'_>, #[description = "what teawie will convert"] degrees_celsius: f32, -) -> Result<(), Error> { +) -> Result<()> { let temp = (degrees_celsius * (9.0 / 5.0)) + 32.0; ctx.say(temp.to_string()).await?; Ok(()) @@ -36,7 +38,7 @@ pub async fn to_fahrenheit( pub async fn to_bottom( ctx: Context<'_>, #[description = "what teawie will translate into bottom"] message: String, -) -> Result<(), Error> { +) -> Result<()> { let encoded = bottom::encode_string(&message); ctx.say(encoded).await?; Ok(()) @@ -47,17 +49,8 @@ pub async fn to_bottom( pub async fn from_bottom( ctx: Context<'_>, #[description = "what teawie will translate from bottom"] message: String, -) -> Result<(), Error> { - let d = bottom::decode_string(&message); - match d { - Ok(decoded) => { - ctx.say(decoded).await?; - Ok(()) - } - Err(why) => { - ctx.say("couldn't decode that for you, i'm sowwy!! :((".to_string()) - .await?; - Err(Box::new(why)) - } - } +) -> Result<()> { + let decoded = bottom::decode_string(&message)?; + ctx.say(decoded).await?; + Ok(()) } diff --git a/src/commands/copypasta.rs b/src/commands/copypasta.rs index 14a6673..313cd13 100644 --- a/src/commands/copypasta.rs +++ b/src/commands/copypasta.rs @@ -1,7 +1,8 @@ -use crate::{utils, Context, Error}; +use crate::{utils, Context}; use std::collections::HashMap; +use color_eyre::eyre::{eyre, Result}; use include_dir::{include_dir, Dir}; use log::*; @@ -58,8 +59,11 @@ fn get_copypasta(name: Copypastas) -> String { pub async fn copypasta( ctx: Context<'_>, #[description = "the copypasta you want to send"] copypasta: Copypastas, -) -> Result<(), Error> { - let gid = ctx.guild_id().unwrap_or_default(); +) -> Result<()> { + let gid = ctx + .guild_id() + .ok_or_else(|| eyre!("couldnt get guild from message!"))?; + if !utils::is_guild_allowed(gid) { info!("not running copypasta command in {gid}"); return Ok(()); diff --git a/src/commands/mod.rs b/src/commands/mod.rs index b6130ab..5edf0b7 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -6,10 +6,12 @@ pub mod random; pub mod teawiespam; pub mod version; -use crate::{Data, Error}; +use crate::Data; + +use color_eyre::eyre::Report; use poise::Command; -pub fn to_global_commands() -> Vec> { +pub fn to_global_commands() -> Vec> { vec![ ask::ask(), bing::bing(), @@ -21,6 +23,6 @@ pub fn to_global_commands() -> Vec> { ] } -pub fn to_guild_commands() -> Vec> { +pub fn to_guild_commands() -> Vec> { vec![copypasta::copypasta(), teawiespam::teawiespam()] } diff --git a/src/commands/random.rs b/src/commands/random.rs index bc34928..9595d09 100644 --- a/src/commands/random.rs +++ b/src/commands/random.rs @@ -1,44 +1,30 @@ -use crate::{api, consts, utils, Context, Error}; +use crate::{api, consts, utils, Context}; + +use color_eyre::eyre::Result; #[poise::command(slash_command, subcommands("lore", "teawie", "shiggy"))] -pub async fn random(_ctx: Context<'_>) -> Result<(), Error> { +pub async fn random(_ctx: Context<'_>) -> Result<()> { Ok(()) } /// get a random piece of teawie lore! #[poise::command(prefix_command, slash_command)] -pub async fn lore(ctx: Context<'_>) -> Result<(), Error> { - match utils::random_choice(consts::LORE) { - Ok(resp) => { - ctx.say(resp).await?; - Ok(()) - } - Err(why) => { - ctx.say("i can't think of any right now :(").await?; - Err(why) - } - } +pub async fn lore(ctx: Context<'_>) -> Result<()> { + let resp = utils::random_choice(consts::LORE)?; + ctx.say(resp).await?; + Ok(()) } /// get a random teawie #[poise::command(prefix_command, slash_command)] -pub async fn teawie(ctx: Context<'_>) -> Result<(), Error> { - if let Ok(url) = api::guzzle::get_random_teawie().await { - utils::send_url_as_embed(ctx, url).await - } else { - ctx.say("i'm too lazy to send a selfie right now :(") - .await?; - Ok(()) - } +pub async fn teawie(ctx: Context<'_>) -> Result<()> { + let url = api::guzzle::get_random_teawie().await?; + utils::send_url_as_embed(ctx, url).await } /// get a random shiggy #[poise::command(prefix_command, slash_command)] -pub async fn shiggy(ctx: Context<'_>) -> Result<(), Error> { - if let Ok(url) = api::shiggy::get_random_shiggy().await { - utils::send_url_as_embed(ctx, url).await - } else { - ctx.say("i couldn't get a shiggy right now :(").await?; - Ok(()) - } +pub async fn shiggy(ctx: Context<'_>) -> Result<()> { + let url = api::shiggy::get_random_shiggy().await?; + utils::send_url_as_embed(ctx, url).await } diff --git a/src/commands/teawiespam.rs b/src/commands/teawiespam.rs index 4964e90..da01af9 100644 --- a/src/commands/teawiespam.rs +++ b/src/commands/teawiespam.rs @@ -1,13 +1,15 @@ use crate::utils; -use crate::{Context, Error}; +use crate::Context; + +use color_eyre::eyre::Result; use log::*; /// teawie will spam you. #[poise::command(slash_command, prefix_command)] -pub async fn teawiespam(ctx: Context<'_>) -> Result<(), Error> { +pub async fn teawiespam(ctx: Context<'_>) -> Result<()> { let gid = ctx.guild_id().unwrap_or_default(); if !utils::is_guild_allowed(gid) { - info!("not running copypasta command in {gid}"); + info!("not running teawiespam command in {gid}"); return Ok(()); } diff --git a/src/commands/version.rs b/src/commands/version.rs index e87ab4d..c5e97f9 100644 --- a/src/commands/version.rs +++ b/src/commands/version.rs @@ -1,9 +1,11 @@ use crate::colors::Colors; -use crate::{Context, Error}; +use crate::Context; + +use color_eyre::eyre::Result; /// get version info #[poise::command(slash_command)] -pub async fn version(ctx: Context<'_>) -> Result<(), Error> { +pub async fn version(ctx: Context<'_>) -> Result<()> { let sha = option_env!("GIT_SHA").unwrap_or("main"); let revision_url = format!( diff --git a/src/handler/message.rs b/src/handler/message.rs deleted file mode 100644 index 37a49bf..0000000 --- a/src/handler/message.rs +++ /dev/null @@ -1,35 +0,0 @@ -use crate::{consts, utils, Data, Error}; -use log::*; -use poise::serenity_prelude as serenity; -use poise::{Event, FrameworkContext}; - -fn should_echo(framework: FrameworkContext<'_, Data, Error>, msg: &serenity::Message) -> bool { - let gid = msg.guild_id.unwrap_or_default(); - if msg.author.id == framework.bot_id || !utils::is_guild_allowed(gid) { - info!("not running copypasta command in {gid}"); - return false; - } - - let content = &msg.content; - - content == "🗿" - || consts::TEAMOJIS.contains(&content.as_str()) - || content.to_ascii_lowercase() == "moyai" - || content - .to_ascii_lowercase() - .contains("twitter's recommendation algorithm") -} - -pub async fn handle( - ctx: &serenity::Context, - _event: &Event<'_>, - framework: FrameworkContext<'_, Data, Error>, - _data: &Data, - msg: &serenity::Message, -) -> Result<(), Error> { - if should_echo(framework, msg) { - msg.reply(ctx, &msg.content).await?; - } - - Ok(()) -} diff --git a/src/handler/mod.rs b/src/handler/mod.rs deleted file mode 100644 index 3489b4a..0000000 --- a/src/handler/mod.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::{Data, Error}; -use poise::serenity_prelude as serenity; -use poise::Event; - -mod message; -pub mod pinboard; -mod reactboard; - -pub async fn handle( - ctx: &serenity::Context, - event: &Event<'_>, - framework: poise::FrameworkContext<'_, Data, Error>, - data: &Data, -) -> Result<(), Error> { - match event { - Event::Ready { data_about_bot } => { - log::info!("logged in as {}", data_about_bot.user.name) - } - - Event::Message { new_message } => { - message::handle(ctx, event, framework, data, new_message).await? - } - - Event::ChannelPinsUpdate { pin } => { - if let Some(settings) = &data.settings { - pinboard::handle(ctx, pin, settings).await - } - } - - Event::ReactionAdd { add_reaction } => { - if let Some(settings) = &data.settings { - reactboard::handle(ctx, add_reaction, settings).await? - } - } - - _ => {} - } - - Ok(()) -} diff --git a/src/handler/pinboard.rs b/src/handler/pinboard.rs deleted file mode 100644 index 0c87a5b..0000000 --- a/src/handler/pinboard.rs +++ /dev/null @@ -1,77 +0,0 @@ -use crate::settings::Settings; -use crate::utils; - -use log::*; -use poise::serenity_prelude::model::prelude::*; -use poise::serenity_prelude::Context; - -pub async fn handle(ctx: &Context, pin: &ChannelPinsUpdateEvent, settings: &Settings) { - if let Some(sources) = &settings.pinboard_sources { - if !sources.contains(&pin.channel_id) { - warn!("can't access source of pin!"); - return; - } - } - - let mut pinner = guess_pinner(ctx, pin).await; - let pins = pin - .channel_id - .pins(&ctx.http) - .await - .expect("couldn't get a list of pins!?"); - - for pin in pins { - // We call `take` because it's supposed to be just for the latest message. - redirect(ctx, &pin, pinner.take(), settings.pinboard_target).await; - pin.unpin(&ctx).await.expect("couldn't unpin message"); - } -} - -async fn redirect(ctx: &Context, pin: &Message, pinner: Option, target: ChannelId) { - let pinner = pinner.map_or("*someone*".to_owned(), |u| format!("<@{u}>")); - let embed = utils::resolve_message_to_embed(ctx, pin).await; - - target - .send_message(&ctx.http, |m| { - m.allowed_mentions(|am| am.empty_parse()) - .content(format!("📌'd by {pinner} in {}", pin.link())) - .set_embed(embed) - }) - .await - .expect("couldn't redirect message"); -} - -/// (Desperate, best-effort) attempt to get the user that pinned the last message -/// -/// Now, since Discord is SUPER annoying, it doesn't actually tell you which bloody user -/// that triggered the pins update event. So, you have to dig into the audit log. -/// Unfortunately, while you do get a timestamp, the REST API does not return the time at -/// which each action is logged, which, to me, means that it is not a freaking log *at all*. -/// -/// I love Discord. -/// -/// So, the plan is that only the first pinned message gets clear pinner information, -/// since we can just get the latest pin, which should happen on the exact second. -/// We can't reliably say the same for any existing pins, so we can only /shrug and say -/// *somebody* did it. Ugh. -async fn guess_pinner(ctx: &Context, pin: &ChannelPinsUpdateEvent) -> Option { - if let Some(g) = pin.guild_id { - g.audit_logs( - &ctx.http, - // This `num` call shouldn't be necessary. - // See https://github.com/serenity-rs/serenity/issues/2488 - Some(Action::Message(MessageAction::Pin).num()), - None, // user id - None, // before - Some(1), // limit - ) - .await - .ok() - .and_then(|mut logs| logs.entries.pop()) - .map(|first| first.user_id) - } else { - // TODO: mayyyyybe we can guess who pinned something in a DM...? - warn!("couldn't figure out who pinned in {}!", pin.channel_id); - None - } -} diff --git a/src/handler/reactboard.rs b/src/handler/reactboard.rs deleted file mode 100644 index 36f8361..0000000 --- a/src/handler/reactboard.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crate::Error; -use crate::{settings::Settings, utils}; -use log::*; -use poise::serenity_prelude::{Context, Message, MessageReaction, Reaction}; - -pub async fn handle(ctx: &Context, reaction: &Reaction, settings: &Settings) -> Result<(), Error> { - let msg = match reaction.message(&ctx.http).await { - Ok(msg) => msg, - Err(why) => { - warn!("couldn't get message of reaction! {}", why); - return Err(Box::new(why)); - } - }; - - if let Some(matched) = msg - .clone() - .reactions - .into_iter() - .find(|r| r.reaction_type == reaction.emoji) - { - send_to_reactboard(ctx, &matched, &msg, settings).await?; - } else { - warn!( - "couldn't find any matching reactions for {} in {}", - reaction.emoji.as_data(), - msg.id - ) - } - - Ok(()) -} - -async fn send_to_reactboard( - ctx: &Context, - reaction: &MessageReaction, - msg: &Message, - settings: &Settings, -) -> Result<(), Error> { - if !settings.can_use_reaction(reaction) { - info!("reaction {} can't be used!", reaction.reaction_type); - return Ok(()); - } - - if reaction.count == settings.reactboard_requirement.unwrap_or(5) { - let embed = utils::resolve_message_to_embed(ctx, msg).await; - - settings - .reactboard_target - .send_message(&ctx.http, |m| { - m.allowed_mentions(|am| am.empty_parse()) - .content(format!( - "{} **#{}**", - reaction.reaction_type, reaction.count - )) - .set_embed(embed) - }) - .await?; - } else { - info!( - "not putting message {} on reactboard, not enough reactions", - msg.id - ) - } - - Ok(()) -} diff --git a/src/handlers/error.rs b/src/handlers/error.rs new file mode 100644 index 0000000..b4e1361 --- /dev/null +++ b/src/handlers/error.rs @@ -0,0 +1,42 @@ +use crate::colors::Colors; +use crate::Data; + +use color_eyre::eyre::Report; +use log::*; +use poise::serenity_prelude::Timestamp; +use poise::FrameworkError; + +pub async fn handle(error: poise::FrameworkError<'_, Data, Report>) { + match error { + FrameworkError::Setup { error, .. } => error!("error setting up client! {error:#?}"), + + FrameworkError::Command { error, ctx } => { + error!("error in command {}:\n{error:?}", ctx.command().name); + ctx.send(|c| { + c.embed(|e| { + e.title("Something went wrong!") + .description("oopsie") + .timestamp(Timestamp::now()) + .color(Colors::Orange) + }) + }) + .await + .ok(); + } + + FrameworkError::EventHandler { + error, + ctx: _, + event: _, + framework: _, + } => { + error!("error while handling event:\n{error:#?}"); + } + + error => { + if let Err(e) = poise::builtins::on_error(error).await { + error!("error while handling an error: {}", e); + } + } + } +} diff --git a/src/handlers/event/message.rs b/src/handlers/event/message.rs new file mode 100644 index 0000000..a84ec59 --- /dev/null +++ b/src/handlers/event/message.rs @@ -0,0 +1,35 @@ +use crate::{consts, utils, Data}; + +use color_eyre::eyre::{Report, Result}; +use log::*; +use poise::serenity_prelude::{Context, Message}; +use poise::FrameworkContext; + +pub async fn handle( + ctx: &Context, + framework: FrameworkContext<'_, Data, Report>, + msg: &Message, +) -> Result<()> { + if should_echo(framework, msg) { + msg.reply(ctx, &msg.content).await?; + } + + Ok(()) +} + +fn should_echo(framework: FrameworkContext<'_, Data, Report>, msg: &Message) -> bool { + let gid = msg.guild_id.unwrap_or_default(); + if msg.author.id == framework.bot_id || !utils::is_guild_allowed(gid) { + info!("not running copypasta command in {gid}"); + return false; + } + + let content = &msg.content; + + content == "🗿" + || consts::TEAMOJIS.contains(&content.as_str()) + || content.to_ascii_lowercase() == "moyai" + || content + .to_ascii_lowercase() + .contains("twitter's recommendation algorithm") +} diff --git a/src/handlers/event/mod.rs b/src/handlers/event/mod.rs new file mode 100644 index 0000000..09be62b --- /dev/null +++ b/src/handlers/event/mod.rs @@ -0,0 +1,40 @@ +use crate::Data; + +use color_eyre::eyre::{Report, Result}; +use poise::serenity_prelude as serenity; +use poise::{Event, FrameworkContext}; + +mod message; +mod pinboard; +mod reactboard; + +pub async fn handle( + ctx: &serenity::Context, + event: &Event<'_>, + framework: FrameworkContext<'_, Data, Report>, + data: &Data, +) -> Result<()> { + match event { + Event::Ready { data_about_bot } => { + log::info!("logged in as {}", data_about_bot.user.name) + } + + Event::Message { new_message } => message::handle(ctx, framework, new_message).await?, + + Event::ChannelPinsUpdate { pin } => { + if let Some(settings) = &data.settings { + pinboard::handle(ctx, pin, settings).await + } + } + + Event::ReactionAdd { add_reaction } => { + if let Some(settings) = &data.settings { + reactboard::handle(ctx, add_reaction, settings).await? + } + } + + _ => {} + } + + Ok(()) +} diff --git a/src/handlers/event/pinboard.rs b/src/handlers/event/pinboard.rs new file mode 100644 index 0000000..0c87a5b --- /dev/null +++ b/src/handlers/event/pinboard.rs @@ -0,0 +1,77 @@ +use crate::settings::Settings; +use crate::utils; + +use log::*; +use poise::serenity_prelude::model::prelude::*; +use poise::serenity_prelude::Context; + +pub async fn handle(ctx: &Context, pin: &ChannelPinsUpdateEvent, settings: &Settings) { + if let Some(sources) = &settings.pinboard_sources { + if !sources.contains(&pin.channel_id) { + warn!("can't access source of pin!"); + return; + } + } + + let mut pinner = guess_pinner(ctx, pin).await; + let pins = pin + .channel_id + .pins(&ctx.http) + .await + .expect("couldn't get a list of pins!?"); + + for pin in pins { + // We call `take` because it's supposed to be just for the latest message. + redirect(ctx, &pin, pinner.take(), settings.pinboard_target).await; + pin.unpin(&ctx).await.expect("couldn't unpin message"); + } +} + +async fn redirect(ctx: &Context, pin: &Message, pinner: Option, target: ChannelId) { + let pinner = pinner.map_or("*someone*".to_owned(), |u| format!("<@{u}>")); + let embed = utils::resolve_message_to_embed(ctx, pin).await; + + target + .send_message(&ctx.http, |m| { + m.allowed_mentions(|am| am.empty_parse()) + .content(format!("📌'd by {pinner} in {}", pin.link())) + .set_embed(embed) + }) + .await + .expect("couldn't redirect message"); +} + +/// (Desperate, best-effort) attempt to get the user that pinned the last message +/// +/// Now, since Discord is SUPER annoying, it doesn't actually tell you which bloody user +/// that triggered the pins update event. So, you have to dig into the audit log. +/// Unfortunately, while you do get a timestamp, the REST API does not return the time at +/// which each action is logged, which, to me, means that it is not a freaking log *at all*. +/// +/// I love Discord. +/// +/// So, the plan is that only the first pinned message gets clear pinner information, +/// since we can just get the latest pin, which should happen on the exact second. +/// We can't reliably say the same for any existing pins, so we can only /shrug and say +/// *somebody* did it. Ugh. +async fn guess_pinner(ctx: &Context, pin: &ChannelPinsUpdateEvent) -> Option { + if let Some(g) = pin.guild_id { + g.audit_logs( + &ctx.http, + // This `num` call shouldn't be necessary. + // See https://github.com/serenity-rs/serenity/issues/2488 + Some(Action::Message(MessageAction::Pin).num()), + None, // user id + None, // before + Some(1), // limit + ) + .await + .ok() + .and_then(|mut logs| logs.entries.pop()) + .map(|first| first.user_id) + } else { + // TODO: mayyyyybe we can guess who pinned something in a DM...? + warn!("couldn't figure out who pinned in {}!", pin.channel_id); + None + } +} diff --git a/src/handlers/event/reactboard.rs b/src/handlers/event/reactboard.rs new file mode 100644 index 0000000..3972931 --- /dev/null +++ b/src/handlers/event/reactboard.rs @@ -0,0 +1,64 @@ +use crate::{settings::Settings, utils}; + +use color_eyre::eyre::{eyre, Context as _, Result}; +use log::*; +use poise::serenity_prelude::{Context, Message, MessageReaction, Reaction}; + +pub async fn handle(ctx: &Context, reaction: &Reaction, settings: &Settings) -> Result<()> { + let msg = reaction + .message(&ctx.http) + .await + .wrap_err("couldn't get reaction from message!")?; + + let matched = msg + .clone() + .reactions + .into_iter() + .find(|r| r.reaction_type == reaction.emoji) + .ok_or_else(|| { + eyre!( + "couldn't find any matching reactions for {} in message {}!", + reaction.emoji.as_data(), + msg.id + ) + })?; + + send_to_reactboard(ctx, &matched, &msg, settings).await?; + + Ok(()) +} + +async fn send_to_reactboard( + ctx: &Context, + reaction: &MessageReaction, + msg: &Message, + settings: &Settings, +) -> Result<()> { + if !settings.can_use_reaction(reaction) { + info!("reaction {} can't be used!", reaction.reaction_type); + return Ok(()); + } + + if reaction.count == settings.reactboard_requirement.unwrap_or(5) { + let embed = utils::resolve_message_to_embed(ctx, msg).await; + + settings + .reactboard_target + .send_message(&ctx.http, |m| { + m.allowed_mentions(|am| am.empty_parse()) + .content(format!( + "{} **#{}**", + reaction.reaction_type, reaction.count + )) + .set_embed(embed) + }) + .await?; + } else { + info!( + "not putting message {} on reactboard, not enough reactions", + msg.id + ) + } + + Ok(()) +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs new file mode 100644 index 0000000..2ae0539 --- /dev/null +++ b/src/handlers/mod.rs @@ -0,0 +1,5 @@ +mod error; +mod event; + +pub use error::handle as handle_error; +pub use event::handle as handle_event; diff --git a/src/main.rs b/src/main.rs index a93e102..98ebe5f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,21 @@ -use std::{error, time}; +use std::time::Duration; +use color_eyre::eyre::{eyre, Context as _, Report, Result}; use log::*; -use poise::serenity_prelude as serentiy; +use poise::{ + serenity_prelude as serenity, EditTracker, Framework, FrameworkOptions, PrefixFrameworkOptions, +}; use settings::Settings; mod api; mod colors; mod commands; mod consts; -mod handler; +mod handlers; mod settings; mod utils; -type Error = Box; -type Context<'a> = poise::Context<'a, Data, Error>; +type Context<'a> = poise::Context<'a, Data, Report>; #[derive(Clone)] pub struct Data { @@ -34,54 +36,43 @@ impl Default for Data { } } -async fn on_error(error: poise::FrameworkError<'_, Data, Error>) { - match error { - poise::FrameworkError::Setup { error, .. } => panic!("failed to start bot: {error:?}"), - poise::FrameworkError::Command { error, ctx } => { - error!("error in command {}: {:?}", ctx.command().name, error); - } - error => { - if let Err(e) = poise::builtins::on_error(error).await { - error!("error while handling an error: {}", e); - } - } - } -} - #[tokio::main] -async fn main() { +async fn main() -> Result<()> { + color_eyre::install()?; env_logger::init(); dotenvy::dotenv().ok(); - let options = poise::FrameworkOptions { + let token = + std::env::var("TOKEN").wrap_err_with(|| eyre!("Couldn't find token in environment!"))?; + + let intents = + serenity::GatewayIntents::non_privileged() | serenity::GatewayIntents::MESSAGE_CONTENT; + + let options = FrameworkOptions { commands: commands::to_global_commands(), + on_error: |error| Box::pin(handlers::handle_error(error)), + command_check: Some(|ctx| { + Box::pin(async move { Ok(ctx.author().id != ctx.framework().bot_id) }) + }), event_handler: |ctx, event, framework, data| { - Box::pin(handler::handle(ctx, event, framework, data)) + Box::pin(handlers::handle_event(ctx, event, framework, data)) }, - prefix_options: poise::PrefixFrameworkOptions { + prefix_options: PrefixFrameworkOptions { prefix: Some("!".into()), - edit_tracker: Some(poise::EditTracker::for_timespan(time::Duration::from_secs( - 3600, - ))), + edit_tracker: Some(EditTracker::for_timespan(Duration::from_secs(3600))), ..Default::default() }, - on_error: |error| Box::pin(on_error(error)), - command_check: Some(|ctx| { - Box::pin(async move { Ok(ctx.author().id != ctx.framework().bot_id) }) - }), ..Default::default() }; - let framework = poise::Framework::builder() + let framework = Framework::builder() + .token(token) + .intents(intents) .options(options) - .token(std::env::var("TOKEN").expect("couldn't find token in environment.")) - .intents( - serentiy::GatewayIntents::non_privileged() | serentiy::GatewayIntents::MESSAGE_CONTENT, - ) .setup(|ctx, _ready, framework| { Box::pin(async move { poise::builtins::register_globally(ctx, &framework.options().commands).await?; - info!("registered global commands!"); + info!("Registered global commands!"); poise::builtins::register_in_guild( ctx, @@ -89,11 +80,17 @@ async fn main() { consts::TEAWIE_GUILD, ) .await?; - info!("registered guild commands!"); + info!("Registered guild commands to {}", consts::TEAWIE_GUILD); Ok(Data::new()) }) }); - framework.run().await.unwrap() + tokio::select! { + result = framework.run() => { result.map_err(Report::from) }, + _ = tokio::signal::ctrl_c() => { + info!("Interrupted! Exiting..."); + std::process::exit(130); + } + } } diff --git a/src/utils.rs b/src/utils.rs index af079ff..9a1d09c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,6 @@ -use crate::{colors, consts, Context, Error}; +use crate::{colors, consts, Context}; -use log::*; +use color_eyre::eyre::{eyre, Result}; use once_cell::sync::Lazy; use poise::serenity_prelude as serenity; use rand::seq::SliceRandom; @@ -21,13 +21,13 @@ pub fn parse_snowflakes_from_env T>(key: &str, f: F) -> Option< /* * chooses a random element from an array */ -pub fn random_choice(arr: [&str; N]) -> Result { +pub fn random_choice(arr: [&str; N]) -> Result { let mut rng = rand::thread_rng(); - if let Some(resp) = arr.choose(&mut rng) { - Ok((*resp).to_string()) - } else { - Err(Into::into("couldn't choose from arr!")) - } + let resp = arr + .choose(&mut rng) + .ok_or_else(|| eyre!("couldn't choose from array!"))?; + + Ok((*resp).to_string()) } // waiting for `round_char_boundary` to stabilize @@ -54,31 +54,25 @@ pub fn is_guild_allowed(gid: GuildId) -> bool { ALLOWED_GUILDS.contains(&gid) } -pub async fn send_url_as_embed(ctx: Context<'_>, url: String) -> Result<(), Error> { - match Url::parse(&url) { - Ok(parsed) => { - let title = parsed - .path_segments() - .unwrap() - .last() - .unwrap_or("image") - .replace("%20", " "); - - ctx.send(|c| { - c.embed(|e| { - e.title(title) - .image(&url) - .url(url) - .color(colors::Colors::Blue) - }) - }) - .await?; - } - Err(why) => { - error!("failed to parse url {}! {}", url, why); - ctx.say("i can't get that for you right now :(").await?; - } - } +pub async fn send_url_as_embed(ctx: Context<'_>, url: String) -> Result<()> { + let parsed = Url::parse(&url)?; + + let title = parsed + .path_segments() + .unwrap() + .last() + .unwrap_or("image") + .replace("%20", " "); + + ctx.send(|c| { + c.embed(|e| { + e.title(title) + .image(&url) + .url(url) + .color(colors::Colors::Blue) + }) + }) + .await?; Ok(()) } -- cgit v1.2.3