From 0025ad5ea8d412aacc3184d18063fd5ff3de0175 Mon Sep 17 00:00:00 2001 From: seth Date: Sat, 2 Dec 2023 07:00:24 -0500 Subject: feat: add per guild configuration --- src/commands/ask.rs | 18 ----- src/commands/bing.rs | 10 --- src/commands/convert.rs | 56 --------------- src/commands/copypasta.rs | 81 --------------------- src/commands/general/ask.rs | 17 +++++ src/commands/general/bing.rs | 9 +++ src/commands/general/convert.rs | 55 ++++++++++++++ src/commands/general/mod.rs | 11 +++ src/commands/general/random.rs | 29 ++++++++ src/commands/general/version.rs | 38 ++++++++++ src/commands/mod.rs | 27 +++---- src/commands/moderation/config.rs | 140 ++++++++++++++++++++++++++++++++++++ src/commands/moderation/mod.rs | 3 + src/commands/optional/copypasta.rs | 80 +++++++++++++++++++++ src/commands/optional/mod.rs | 5 ++ src/commands/optional/teawiespam.rs | 20 ++++++ src/commands/random.rs | 30 -------- src/commands/teawiespam.rs | 19 ----- src/commands/version.rs | 39 ---------- src/handlers/error.rs | 6 +- src/handlers/event/guild.rs | 26 +++++++ src/handlers/event/message.rs | 35 ++++----- src/handlers/event/mod.rs | 7 ++ src/handlers/event/pinboard.rs | 22 ++++-- src/handlers/event/reactboard.rs | 24 ++++--- src/main.rs | 8 +-- src/settings.rs | 139 ++++++++++++++++++----------------- src/utils.rs | 11 --- 28 files changed, 578 insertions(+), 387 deletions(-) delete mode 100644 src/commands/ask.rs delete mode 100644 src/commands/bing.rs delete mode 100644 src/commands/convert.rs delete mode 100644 src/commands/copypasta.rs create mode 100644 src/commands/general/ask.rs create mode 100644 src/commands/general/bing.rs create mode 100644 src/commands/general/convert.rs create mode 100644 src/commands/general/mod.rs create mode 100644 src/commands/general/random.rs create mode 100644 src/commands/general/version.rs create mode 100644 src/commands/moderation/config.rs create mode 100644 src/commands/moderation/mod.rs create mode 100644 src/commands/optional/copypasta.rs create mode 100644 src/commands/optional/mod.rs create mode 100644 src/commands/optional/teawiespam.rs delete mode 100644 src/commands/random.rs delete mode 100644 src/commands/teawiespam.rs delete mode 100644 src/commands/version.rs create mode 100644 src/handlers/event/guild.rs diff --git a/src/commands/ask.rs b/src/commands/ask.rs deleted file mode 100644 index 3589484..0000000 --- a/src/commands/ask.rs +++ /dev/null @@ -1,18 +0,0 @@ -use crate::{consts, utils, Context}; - -use color_eyre::eyre::{Context as _, Result}; - -/// ask teawie a question! -#[poise::command(prefix_command, slash_command)] -pub async fn ask( - ctx: Context<'_>, - #[description = "the question you want to ask teawie"] - #[rename = "question"] - _question: String, -) -> 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 deleted file mode 100644 index b80ebca..0000000 --- a/src/commands/bing.rs +++ /dev/null @@ -1,10 +0,0 @@ -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<()> { - ctx.say("bong!").await?; - Ok(()) -} diff --git a/src/commands/convert.rs b/src/commands/convert.rs deleted file mode 100644 index cbbf8dc..0000000 --- a/src/commands/convert.rs +++ /dev/null @@ -1,56 +0,0 @@ -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<()> { - Ok(()) -} - -/// ask teawie to convert °F to °C -#[poise::command(slash_command)] -pub async fn to_celsius( - ctx: Context<'_>, - #[description = "what teawie will convert"] degrees_fahrenheit: f32, -) -> Result<()> { - let temp = (degrees_fahrenheit - 32.0) * (5.0 / 9.0); - ctx.say(temp.to_string()).await?; - Ok(()) -} - -/// ask teawie to convert °C to °F -#[poise::command(slash_command)] -pub async fn to_fahrenheit( - ctx: Context<'_>, - #[description = "what teawie will convert"] degrees_celsius: f32, -) -> Result<()> { - let temp = (degrees_celsius * (9.0 / 5.0)) + 32.0; - ctx.say(temp.to_string()).await?; - Ok(()) -} - -/// teawie will translate to bottom 🥺 -#[poise::command(slash_command)] -pub async fn to_bottom( - ctx: Context<'_>, - #[description = "what teawie will translate into bottom"] message: String, -) -> Result<()> { - let encoded = bottom::encode_string(&message); - ctx.say(encoded).await?; - Ok(()) -} - -/// teawie will translate from bottom 🥸 -#[poise::command(slash_command)] -pub async fn from_bottom( - ctx: Context<'_>, - #[description = "what teawie will translate from bottom"] message: String, -) -> Result<()> { - let decoded = bottom::decode_string(&message)?; - ctx.say(decoded).await?; - Ok(()) -} diff --git a/src/commands/copypasta.rs b/src/commands/copypasta.rs deleted file mode 100644 index 16ac562..0000000 --- a/src/commands/copypasta.rs +++ /dev/null @@ -1,81 +0,0 @@ -use crate::Context; - -use std::collections::HashMap; - -use color_eyre::eyre::{eyre, Result}; -use include_dir::{include_dir, Dir}; -use log::*; - -const FILES: Dir = include_dir!("src/copypastas"); - -#[allow(clippy::upper_case_acronyms)] -#[derive(Debug, poise::ChoiceParameter)] -pub enum Copypastas { - Astral, - DVD, - Egrill, - HappyMeal, - Sus, - TickTock, - Twitter, -} - -impl Copypastas { - fn as_str(&self) -> &str { - match self { - Copypastas::Astral => "astral", - Copypastas::DVD => "dvd", - Copypastas::Egrill => "egrill", - Copypastas::HappyMeal => "happymeal", - Copypastas::Sus => "sus", - Copypastas::TickTock => "ticktock", - Copypastas::Twitter => "twitter", - } - } -} - -fn get_copypasta(name: Copypastas) -> Result { - let mut files: HashMap<&str, &str> = HashMap::new(); - - for file in FILES.files() { - let name = file - .path() - .file_stem() - .ok_or_else(|| eyre!("couldn't get file stem from {file:#?}"))? - .to_str() - .ok_or_else(|| eyre!("couldn't convert file stem to str!"))?; - - let contents = file - .contents_utf8() - .ok_or_else(|| eyre!("couldnt get contents from copypasta!"))?; - - // refer to files by their name w/o extension - files.insert(name, contents); - } - - if files.contains_key(name.as_str()) { - Ok(files[name.as_str()].to_string()) - } else { - Err(eyre!("couldnt find copypasta {name}!")) - } -} - -/// ask teawie to send funni copypasta -#[poise::command(slash_command)] -pub async fn copypasta( - ctx: Context<'_>, - #[description = "the copypasta you want to send"] copypasta: Copypastas, -) -> Result<()> { - let gid = ctx - .guild_id() - .ok_or_else(|| eyre!("couldnt get guild from message!"))?; - - if !ctx.data().settings.is_guild_allowed(gid) { - info!("not running copypasta command in {gid}"); - return Ok(()); - } - - ctx.say(get_copypasta(copypasta)?).await?; - - Ok(()) -} diff --git a/src/commands/general/ask.rs b/src/commands/general/ask.rs new file mode 100644 index 0000000..4bbf82e --- /dev/null +++ b/src/commands/general/ask.rs @@ -0,0 +1,17 @@ +use crate::{consts, utils, Context}; +use color_eyre::eyre::{Context as _, Result}; + +/// ask teawie a question! +#[poise::command(prefix_command, slash_command)] +pub async fn ask( + ctx: Context<'_>, + #[description = "the question you want to ask teawie"] + #[rename = "question"] + _question: String, +) -> 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/general/bing.rs b/src/commands/general/bing.rs new file mode 100644 index 0000000..fefbaf1 --- /dev/null +++ b/src/commands/general/bing.rs @@ -0,0 +1,9 @@ +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<()> { + ctx.say("bong!").await?; + Ok(()) +} diff --git a/src/commands/general/convert.rs b/src/commands/general/convert.rs new file mode 100644 index 0000000..60135c4 --- /dev/null +++ b/src/commands/general/convert.rs @@ -0,0 +1,55 @@ +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<()> { + Ok(()) +} + +/// ask teawie to convert °F to °C +#[poise::command(slash_command)] +pub async fn to_celsius( + ctx: Context<'_>, + #[description = "what teawie will convert"] degrees_fahrenheit: f32, +) -> Result<()> { + let temp = (degrees_fahrenheit - 32.0) * (5.0 / 9.0); + ctx.say(temp.to_string()).await?; + Ok(()) +} + +/// ask teawie to convert °C to °F +#[poise::command(slash_command)] +pub async fn to_fahrenheit( + ctx: Context<'_>, + #[description = "what teawie will convert"] degrees_celsius: f32, +) -> Result<()> { + let temp = (degrees_celsius * (9.0 / 5.0)) + 32.0; + ctx.say(temp.to_string()).await?; + Ok(()) +} + +/// teawie will translate to bottom 🥺 +#[poise::command(slash_command)] +pub async fn to_bottom( + ctx: Context<'_>, + #[description = "what teawie will translate into bottom"] message: String, +) -> Result<()> { + let encoded = bottom::encode_string(&message); + ctx.say(encoded).await?; + Ok(()) +} + +/// teawie will translate from bottom 🥸 +#[poise::command(slash_command)] +pub async fn from_bottom( + ctx: Context<'_>, + #[description = "what teawie will translate from bottom"] message: String, +) -> Result<()> { + let decoded = bottom::decode_string(&message)?; + ctx.say(decoded).await?; + Ok(()) +} diff --git a/src/commands/general/mod.rs b/src/commands/general/mod.rs new file mode 100644 index 0000000..ffb4d63 --- /dev/null +++ b/src/commands/general/mod.rs @@ -0,0 +1,11 @@ +mod ask; +mod bing; +mod convert; +mod random; +mod version; + +pub use ask::ask; +pub use bing::bing; +pub use convert::convert; +pub use random::random; +pub use version::version; diff --git a/src/commands/general/random.rs b/src/commands/general/random.rs new file mode 100644 index 0000000..9aa282a --- /dev/null +++ b/src/commands/general/random.rs @@ -0,0 +1,29 @@ +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<()> { + Ok(()) +} + +/// get a random piece of teawie lore! +#[poise::command(prefix_command, slash_command)] +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<()> { + 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<()> { + let url = api::shiggy::get_random_shiggy().await?; + utils::send_url_as_embed(ctx, url).await +} diff --git a/src/commands/general/version.rs b/src/commands/general/version.rs new file mode 100644 index 0000000..8b8d1f1 --- /dev/null +++ b/src/commands/general/version.rs @@ -0,0 +1,38 @@ +use crate::colors::Colors; +use crate::Context; +use color_eyre::eyre::Result; + +/// get version info +#[poise::command(slash_command)] +pub async fn version(ctx: Context<'_>) -> Result<()> { + let sha = option_env!("GIT_SHA").unwrap_or("main"); + + let revision_url = format!( + "[{}]({}/tree/{})", + sha, + option_env!("CARGO_PKG_REPOSITORY").unwrap_or("https://github.com/getchoo/teawieBot"), + sha, + ); + + let fields = [ + ( + "Version:", + option_env!("CARGO_PKG_VERSION").unwrap_or("not found"), + false, + ), + ("Revision:", &revision_url, false), + ("User Agent:", &crate::api::USER_AGENT, false), + ]; + + ctx.send(|c| { + c.embed(|e| { + e.title("Version Information") + .description("powered by poise!") + .fields(fields) + .color(Colors::Blue) + }) + }) + .await?; + + Ok(()) +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 5edf0b7..8c265d3 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,28 +1,23 @@ -pub mod ask; -pub mod bing; -pub mod convert; -pub mod copypasta; -pub mod random; -pub mod teawiespam; -pub mod version; - use crate::Data; use color_eyre::eyre::Report; use poise::Command; +mod general; +mod moderation; +mod optional; + pub fn to_global_commands() -> Vec> { vec![ - ask::ask(), - bing::bing(), - convert::convert(), - random::random(), - copypasta::copypasta(), - teawiespam::teawiespam(), - version::version(), + general::ask(), + general::bing(), + general::convert(), + general::random(), + general::version(), + moderation::config(), ] } pub fn to_guild_commands() -> Vec> { - vec![copypasta::copypasta(), teawiespam::teawiespam()] + vec![optional::copypasta(), optional::teawiespam()] } diff --git a/src/commands/moderation/config.rs b/src/commands/moderation/config.rs new file mode 100644 index 0000000..2d1410c --- /dev/null +++ b/src/commands/moderation/config.rs @@ -0,0 +1,140 @@ +use crate::settings::{Settings, SettingsProperties}; +use crate::Context; + +use color_eyre::eyre::{eyre, Context as _, ContextCompat, Result}; +use log::*; +use poise::serenity_prelude::{GuildChannel, ReactionType}; + +#[poise::command( + slash_command, + subcommands("set", "get"), + default_member_permissions = "MANAGE_GUILD" +)] +pub async fn config(_ctx: Context<'_>) -> Result<()> { + Ok(()) +} + +#[poise::command(slash_command, ephemeral, guild_only)] +pub async fn set( + ctx: Context<'_>, + #[channel_types("Text")] + #[description = "Where to redirect pins from channels. If empty (the default), the PinBoard is disabled."] + pinboard_channel: Option, + #[channel_types("Text")] + #[description = "A channel that PinBoard will redirect pins from. This will be all channels if empty."] + pinboard_watch: Option, + #[channel_types("Text")] + #[description = "Where to post messages that made it to the ReactBoard. If left empty, ReactBoard is disabled."] + reactboard_channel: Option, + #[description = "An emoji that will get messages on the ReactBoard. If empty, ReactBoard is disabled."] + reactboard_reaction: Option, + #[description = "Minimum number of reactions a message needs to make it to the ReactBoard (defaults to 5)"] + reactboard_requirement: Option, + #[description = "Enables 'extra' commands like teawiespam and copypasta. Defaults to false."] + optional_commands_enabled: Option, +) -> Result<()> { + let redis = &ctx.data().redis; + let gid = ctx.guild_id().unwrap_or_default(); + let mut settings = Settings::from_redis(redis, &gid).await?; + let previous_settings = settings.clone(); + + if let Some(channel) = pinboard_channel { + settings.pinboard_channel = Some(channel.id); + } + + if let Some(watch) = pinboard_watch { + if let Some(mut prev) = settings.pinboard_watch { + prev.push(watch.id); + settings.pinboard_watch = Some(prev); + } else { + let new = Vec::from([watch.id]); + debug!("Setting pinboard_watch to {new:#?} for {} in Redis", gid); + + settings.pinboard_watch = Some(new); + } + } + + if let Some(channel) = reactboard_channel { + debug!( + "Setting reactboard_channel to {channel} for {} in Redis", + gid + ); + + settings.reactboard_channel = Some(channel.id); + } + + if let Some(requirement) = reactboard_requirement { + debug!( + "Setting reactboard_requirement to {requirement} for {} in Redis", + gid + ); + + settings.reactboard_requirement = Some(requirement); + } + + if let Some(reaction) = reactboard_reaction { + let emoji = reaction + .parse::() + .wrap_err_with(|| format!("Couldn't parse {reaction} as string!"))?; + + if let Some(mut prev) = settings.reactboard_reactions { + prev.push(emoji); + settings.reactboard_reactions = Some(prev); + } else { + let new = Vec::from([emoji]); + debug!("Setting pinboard_watch to {new:#?} for {} in Redis", gid); + + settings.reactboard_reactions = Some(new); + } + } + + if let Some(enabled) = optional_commands_enabled { + debug!( + "Setting optional_commands_enabled to {enabled} for {} in Redis", + gid + ); + + settings.optional_commands_enabled = enabled; + } + + if previous_settings != settings { + settings.save(redis).await?; + ctx.reply("Configuration updated!").await?; + } else { + ctx.reply("No changes made, so i'm not updating anything") + .await?; + } + + Ok(()) +} + +#[poise::command(slash_command, ephemeral, guild_only)] +pub async fn get( + ctx: Context<'_>, + #[description = "The setting you want to get"] setting: SettingsProperties, +) -> Result<()> { + let gid = &ctx + .guild_id() + .wrap_err_with(|| eyre!("Failed to get GuildId from context!"))?; + + let settings = Settings::from_redis(&ctx.data().redis, gid).await?; + + let value = match setting { + SettingsProperties::GuildId => settings.guild_id.to_string(), + SettingsProperties::PinBoardChannel => format!("{:#?}", settings.pinboard_channel), + SettingsProperties::PinBoardWatch => format!("{:#?}", settings.pinboard_watch), + SettingsProperties::ReactBoardChannel => format!("{:#?}", settings.reactboard_channel), + SettingsProperties::ReactBoardRequirement => { + format!("{:?}", settings.reactboard_requirement) + } + SettingsProperties::ReactBoardReactions => format!("{:?}", settings.reactboard_reactions), + SettingsProperties::OptionalCommandsEnabled => { + settings.optional_commands_enabled.to_string() + } + }; + + ctx.send(|m| m.embed(|e| e.field(setting, value, false))) + .await?; + + Ok(()) +} diff --git a/src/commands/moderation/mod.rs b/src/commands/moderation/mod.rs new file mode 100644 index 0000000..d54b3f8 --- /dev/null +++ b/src/commands/moderation/mod.rs @@ -0,0 +1,3 @@ +mod config; + +pub use config::config; diff --git a/src/commands/optional/copypasta.rs b/src/commands/optional/copypasta.rs new file mode 100644 index 0000000..ea23f5f --- /dev/null +++ b/src/commands/optional/copypasta.rs @@ -0,0 +1,80 @@ +use crate::{Context, Settings}; + +use std::collections::HashMap; + +use color_eyre::eyre::{eyre, Result}; +use include_dir::{include_dir, Dir}; +use log::*; + +const FILES: Dir = include_dir!("src/copypastas"); + +#[allow(clippy::upper_case_acronyms)] +#[derive(Debug, poise::ChoiceParameter)] +pub enum Copypastas { + Astral, + DVD, + Egrill, + HappyMeal, + Sus, + TickTock, + Twitter, +} + +impl Copypastas { + fn as_str(&self) -> &str { + match self { + Copypastas::Astral => "astral", + Copypastas::DVD => "dvd", + Copypastas::Egrill => "egrill", + Copypastas::HappyMeal => "happymeal", + Copypastas::Sus => "sus", + Copypastas::TickTock => "ticktock", + Copypastas::Twitter => "twitter", + } + } +} + +fn get_copypasta(name: Copypastas) -> Result { + let mut files: HashMap<&str, &str> = HashMap::new(); + + for file in FILES.files() { + let name = file + .path() + .file_stem() + .ok_or_else(|| eyre!("couldn't get file stem from {file:#?}"))? + .to_str() + .ok_or_else(|| eyre!("couldn't convert file stem to str!"))?; + + let contents = file + .contents_utf8() + .ok_or_else(|| eyre!("couldnt get contents from copypasta!"))?; + + // refer to files by their name w/o extension + files.insert(name, contents); + } + + if files.contains_key(name.as_str()) { + Ok(files[name.as_str()].to_string()) + } else { + Err(eyre!("couldnt find copypasta {name}!")) + } +} + +/// ask teawie to send funni copypasta +#[poise::command(slash_command)] +pub async fn copypasta( + ctx: Context<'_>, + #[description = "the copypasta you want to send"] copypasta: Copypastas, +) -> Result<()> { + let gid = ctx.guild_id().unwrap_or_default(); + let settings = Settings::from_redis(&ctx.data().redis, &gid).await?; + + if !settings.optional_commands_enabled { + debug!("Not running copypasta command in {gid} since it's disabled"); + return Ok(()); + } + + ctx.say(get_copypasta(copypasta)?).await?; + + Ok(()) +} diff --git a/src/commands/optional/mod.rs b/src/commands/optional/mod.rs new file mode 100644 index 0000000..451deeb --- /dev/null +++ b/src/commands/optional/mod.rs @@ -0,0 +1,5 @@ +mod copypasta; +mod teawiespam; + +pub use copypasta::copypasta; +pub use teawiespam::teawiespam; diff --git a/src/commands/optional/teawiespam.rs b/src/commands/optional/teawiespam.rs new file mode 100644 index 0000000..c1b3b29 --- /dev/null +++ b/src/commands/optional/teawiespam.rs @@ -0,0 +1,20 @@ +use crate::{Context, Settings}; + +use color_eyre::eyre::Result; +use log::*; + +/// teawie will spam you. +#[poise::command(slash_command, prefix_command)] +pub async fn teawiespam(ctx: Context<'_>) -> Result<()> { + let gid = ctx.guild_id().unwrap_or_default(); + let settings = Settings::from_redis(&ctx.data().redis, &gid).await?; + + if !settings.optional_commands_enabled { + debug!("Not running teawiespam in {gid} since it's disabled"); + return Ok(()); + } + + let wies = "<:teawiesmile:1056438046440042546>".repeat(50); + ctx.say(wies).await?; + Ok(()) +} diff --git a/src/commands/random.rs b/src/commands/random.rs deleted file mode 100644 index 9595d09..0000000 --- a/src/commands/random.rs +++ /dev/null @@ -1,30 +0,0 @@ -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<()> { - Ok(()) -} - -/// get a random piece of teawie lore! -#[poise::command(prefix_command, slash_command)] -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<()> { - 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<()> { - 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 deleted file mode 100644 index aeea255..0000000 --- a/src/commands/teawiespam.rs +++ /dev/null @@ -1,19 +0,0 @@ -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<()> { - let gid = ctx.guild_id().unwrap_or_default(); - - if !ctx.data().settings.is_guild_allowed(gid) { - info!("not running teawiespam command in {gid}"); - return Ok(()); - } - - let wies = "<:teawiesmile:1056438046440042546>".repeat(50); - ctx.say(wies).await?; - Ok(()) -} diff --git a/src/commands/version.rs b/src/commands/version.rs deleted file mode 100644 index 8d9a1f3..0000000 --- a/src/commands/version.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::colors::Colors; -use crate::Context; - -use color_eyre::eyre::Result; - -/// get version info -#[poise::command(slash_command)] -pub async fn version(ctx: Context<'_>) -> Result<()> { - let sha = option_env!("GIT_SHA").unwrap_or("main"); - - let revision_url = format!( - "[{}]({}/tree/{})", - sha, - option_env!("CARGO_PKG_REPOSITORY").unwrap_or("https://github.com/getchoo/teawieBot"), - sha, - ); - - let fields = [ - ( - "Version:", - option_env!("CARGO_PKG_VERSION").unwrap_or("not found"), - false, - ), - ("Revision:", &revision_url, false), - ("User Agent:", &crate::api::USER_AGENT, false), - ]; - - ctx.send(|c| { - c.embed(|e| { - e.title("Version Information") - .description("powered by poise!") - .fields(fields) - .color(Colors::Blue) - }) - }) - .await?; - - Ok(()) -} diff --git a/src/handlers/error.rs b/src/handlers/error.rs index e256bcf..b5b259d 100644 --- a/src/handlers/error.rs +++ b/src/handlers/error.rs @@ -8,7 +8,7 @@ use poise::FrameworkError; pub async fn handle(error: poise::FrameworkError<'_, Data, Report>) { match error { - FrameworkError::Setup { error, .. } => error!("error setting up client! {error:#?}"), + FrameworkError::Setup { error, .. } => error!("Error setting up client!\n{error:#?}"), FrameworkError::Command { error, ctx } => { error!("Error in command {}:\n{error:?}", ctx.command().name); @@ -27,10 +27,10 @@ pub async fn handle(error: poise::FrameworkError<'_, Data, Report>) { FrameworkError::EventHandler { error, ctx: _, - event: _, + event, framework: _, } => { - error!("Error while handling event:\n{error:#?}"); + error!("Error while handling event {}:\n{error:?}", event.name()); } error => { diff --git a/src/handlers/event/guild.rs b/src/handlers/event/guild.rs new file mode 100644 index 0000000..b7a4028 --- /dev/null +++ b/src/handlers/event/guild.rs @@ -0,0 +1,26 @@ +use color_eyre::eyre::Result; +use log::*; +use poise::serenity_prelude::{Guild, UnavailableGuild}; + +use crate::{Data, Settings}; + +pub async fn handle_create(guild: &Guild, is_new: &bool, data: &Data) -> Result<()> { + if !is_new && Settings::from_redis(&data.redis, &guild.id).await.is_ok() { + debug!("Not recreating Redis key for {}", guild.id); + return Ok(()); + } + + info!("Creating new Redis key for {}", guild.id); + Settings::new_redis(&data.redis, &guild.id).await?; + Ok(()) +} + +pub async fn handle_delete(guild: &UnavailableGuild, data: &Data) -> Result<()> { + let redis = &data.redis; + + info!("Deleting redis key for {}", guild.id); + let settings = Settings::from_redis(redis, &guild.id).await?; + settings.delete(redis).await?; + + Ok(()) +} diff --git a/src/handlers/event/message.rs b/src/handlers/event/message.rs index cf619c5..0004caf 100644 --- a/src/handlers/event/message.rs +++ b/src/handlers/event/message.rs @@ -1,8 +1,7 @@ -use crate::Settings; -use crate::{consts, Data}; +use crate::{consts, Data, Settings}; -use color_eyre::eyre::{Report, Result}; -use log::info; +use color_eyre::eyre::{eyre, Report, Result}; +use log::*; use poise::serenity_prelude::{Context, Message}; use poise::FrameworkContext; @@ -12,35 +11,39 @@ pub async fn handle( msg: &Message, data: &Data, ) -> Result<()> { - if should_echo(framework, msg, &data.settings) { + if should_echo(framework, msg, data).await? { msg.reply(ctx, &msg.content).await?; } Ok(()) } -fn should_echo( +async fn should_echo( _framework: FrameworkContext<'_, Data, Report>, msg: &Message, - settings: &Settings, -) -> bool { - let gid = msg.guild_id.unwrap_or_default(); + data: &Data, +) -> Result { if msg.author.bot && msg.webhook_id.is_none() { - info!("Not repeating another bot"); - return false; + debug!("Not repeating another bot"); + return Ok(false); } - if !settings.is_guild_allowed(gid) { - info!("Not echoing in guild {gid}"); - return false; + let gid = msg + .guild_id + .ok_or_else(|| eyre!("Couldn't get GuildId from {}!", msg.id))?; + let settings = Settings::from_redis(&data.redis, &gid).await?; + + if !settings.optional_commands_enabled { + debug!("Not echoing in guild {gid}"); + return Ok(false); } let content = &msg.content; - content == "🗿" + Ok(content == "🗿" || consts::TEAMOJIS.contains(&content.as_str()) || content.to_ascii_lowercase() == "moyai" || content .to_ascii_lowercase() - .contains("twitter's recommendation algorithm") + .contains("twitter's recommendation algorithm")) } diff --git a/src/handlers/event/mod.rs b/src/handlers/event/mod.rs index a587c77..6dd5fe4 100644 --- a/src/handlers/event/mod.rs +++ b/src/handlers/event/mod.rs @@ -4,6 +4,7 @@ use color_eyre::eyre::{Report, Result}; use poise::serenity_prelude as serenity; use poise::{Event, FrameworkContext}; +mod guild; mod message; mod pinboard; mod reactboard; @@ -27,6 +28,12 @@ pub async fn handle( Event::ReactionAdd { add_reaction } => reactboard::handle(ctx, add_reaction, data).await?, + Event::GuildCreate { guild, is_new } => guild::handle_create(guild, is_new, data).await?, + Event::GuildDelete { + incomplete, + full: _, + } => guild::handle_delete(incomplete, data).await?, + _ => {} } diff --git a/src/handlers/event/pinboard.rs b/src/handlers/event/pinboard.rs index 33d2680..21e8170 100644 --- a/src/handlers/event/pinboard.rs +++ b/src/handlers/event/pinboard.rs @@ -1,4 +1,4 @@ -use crate::{utils, Data}; +use crate::{utils, Data, Settings}; use color_eyre::eyre::{eyre, Context as _, Result}; use log::*; @@ -6,9 +6,23 @@ use poise::serenity_prelude::model::prelude::*; use poise::serenity_prelude::Context; pub async fn handle(ctx: &Context, pin: &ChannelPinsUpdateEvent, data: &Data) -> Result<()> { - if let Some(sources) = &data.settings.pinboard_sources { + let gid = pin.guild_id.unwrap_or_default(); + let settings = Settings::from_redis(&data.redis, &gid).await?; + + let target = if let Some(target) = settings.reactboard_channel { + target + } else { + debug!("PinBoard is disabled in {gid}, ignoring"); + return Ok(()); + }; + + if let Some(sources) = settings.pinboard_watch { if !sources.contains(&pin.channel_id) { - warn!("Can't access source of pin!"); + debug!( + "{} not listed in PinBoard settings for {gid}, ignoring", + &pin.channel_id + ); + return Ok(()); } } @@ -22,7 +36,7 @@ pub async fn handle(ctx: &Context, pin: &ChannelPinsUpdateEvent, data: &Data) -> for pin in pins { // We call `take` because it's supposed to be just for the latest message. - redirect(ctx, &pin, pinner.take(), data.settings.pinboard_target).await?; + redirect(ctx, &pin, pinner.take(), target).await?; pin.unpin(&ctx).await?; } diff --git a/src/handlers/event/reactboard.rs b/src/handlers/event/reactboard.rs index 2a417da..2435976 100644 --- a/src/handlers/event/reactboard.rs +++ b/src/handlers/event/reactboard.rs @@ -1,4 +1,4 @@ -use crate::{utils, Data}; +use crate::{utils, Data, Settings}; use color_eyre::eyre::{eyre, Context as _, Result}; use log::*; @@ -52,14 +52,24 @@ async fn send_to_reactboard( msg: &Message, data: &Data, ) -> Result<()> { + let gid = msg.guild_id.unwrap_or_default(); + let settings = Settings::from_redis(&data.redis, &gid).await?; + // make sure everything is in order... - if !data.settings.can_use_reaction(reaction) { - info!("Reaction {} can't be used!", reaction.reaction_type); + let target = if let Some(target) = settings.reactboard_channel { + target + } else { + debug!("Reactboard is disabled in {gid}, ignoring"); + return Ok(()); + }; + + if !settings.can_use_reaction(&reaction.reaction_type) { + debug!("Reaction {} can't be used!", reaction.reaction_type); return Ok(()); } - if reaction.count < data.settings.reactboard_requirement.unwrap_or(5) { - info!( + if reaction.count < settings.reactboard_requirement.unwrap_or(5) { + debug!( "Ignoring message {} on reactboard, not enough reactions", msg.id ); @@ -137,9 +147,7 @@ async fn send_to_reactboard( } else { let embed = utils::resolve_message_to_embed(ctx, msg).await; - let resp = data - .settings - .reactboard_target + let resp = target .send_message(ctx, |m| { m.allowed_mentions(|am| am.empty_parse()) .content(content) diff --git a/src/main.rs b/src/main.rs index 6dc99a1..6921f8a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,29 +19,25 @@ type Context<'a> = poise::Context<'a, Data, Report>; #[derive(Clone)] pub struct Data { - settings: Settings, redis: redis::Client, } impl Data { pub fn new() -> Result { - let settings = - Settings::new().ok_or_else(|| eyre!("Couldn't create new settings object!"))?; - let redis_url = std::env::var("REDIS_URL") .wrap_err_with(|| eyre!("Couldn't find Redis URL in environment!"))?; let redis = redis::Client::open(redis_url)?; - Ok(Self { settings, redis }) + Ok(Self { redis }) } } #[tokio::main] async fn main() -> Result<()> { + dotenvy::dotenv().ok(); color_eyre::install()?; env_logger::init(); - dotenvy::dotenv().ok(); let token = std::env::var("TOKEN").wrap_err_with(|| eyre!("Couldn't find token in environment!"))?; diff --git a/src/settings.rs b/src/settings.rs index 406b990..6c02e5c 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,89 +1,88 @@ -use crate::{consts, utils}; -use log::*; -use poise::serenity_prelude::{ChannelId, EmojiId, GuildId, MessageReaction, ReactionType}; +use color_eyre::eyre::{Context as _, Result}; +use poise::serenity_prelude::{ChannelId, GuildId, ReactionType}; +use redis::{AsyncCommands as _, Client}; +use redis_macros::{FromRedisValue, ToRedisArgs}; +use serde::{Deserialize, Serialize}; -#[derive(Clone)] +const ROOT_KEY: &str = "settings-v1"; + +#[derive(poise::ChoiceParameter)] +pub enum SettingsProperties { + GuildId, + PinBoardChannel, + PinBoardWatch, + ReactBoardChannel, + ReactBoardRequirement, + ReactBoardReactions, + OptionalCommandsEnabled, +} + +#[derive(Clone, Default, PartialEq, Serialize, Deserialize, FromRedisValue, ToRedisArgs)] pub struct Settings { - pub allowed_guilds: Vec, - pub pinboard_target: ChannelId, - pub pinboard_sources: Option>, - pub reactboard_target: ChannelId, + pub guild_id: GuildId, + pub pinboard_channel: Option, + pub pinboard_watch: Option>, + pub reactboard_channel: Option, pub reactboard_requirement: Option, - pub reactboard_custom_reactions: Vec, - pub reactboard_unicode_reactions: Vec, + pub reactboard_reactions: Option>, + pub optional_commands_enabled: bool, } impl Settings { - pub fn new() -> Option { - let allowed_guilds = utils::parse_snowflakes_from_env("ALLOWED_GUILDS", GuildId) - .unwrap_or_else(|| vec![consts::TEAWIE_GUILD, GuildId(1091969030694375444)]); - - let Some(pinboard_target) = utils::parse_snowflake_from_env("PIN_BOARD_TARGET", ChannelId) - else { - return None; + pub async fn new_redis(redis: &Client, gid: &GuildId) -> Result<()> { + let key = format!("{ROOT_KEY}:{gid}"); + let settings = Self { + guild_id: *gid, + optional_commands_enabled: false, + ..Default::default() }; - let pinboard_sources = utils::parse_snowflakes_from_env("PIN_BOARD_SOURCES", ChannelId); - let Some(reactboard_target) = - utils::parse_snowflake_from_env("REACT_BOARD_TARGET", ChannelId) - else { - return None; - }; + let mut con = redis.get_async_connection().await?; + con.set(&key, settings) + .await + .wrap_err_with(|| format!("Couldn't set key {key} in Redis!"))?; - let reactboard_requirement = utils::parse_snowflake_from_env("REACT_BOARD_MIN", u64::from); + Ok(()) + } - let reactboard_custom_reactions = - utils::parse_snowflakes_from_env("REACT_BOARD_CUSTOM_REACTIONS", EmojiId) - .unwrap_or_default(); + pub async fn from_redis(redis: &Client, gid: &GuildId) -> Result { + let key = format!("{ROOT_KEY}:{gid}"); + let mut con = redis.get_async_connection().await?; - let reactboard_unicode_reactions = std::env::var("REACT_BOARD_UNICODE_REACTIONS") - .ok() - .map(|v| { - v.split(',') - .map(|vs| vs.to_string()) - .collect::>() - }) - .unwrap_or_default(); + let settings: Settings = con + .get(&key) + .await + .wrap_err_with(|| format!("Couldn't get {key} from Redis!"))?; - info!("PinBoard target is {}", pinboard_target); - if let Some(sources) = &pinboard_sources { - info!("PinBoard sources are {:#?}", sources); - } - info!("ReactBoard target is {}", reactboard_target); - info!( - "ReactBoard custom reactions are {:#?}", - reactboard_custom_reactions - ); - info!( - "ReactBoard unicode reactions are {:#?}", - reactboard_unicode_reactions - ); + Ok(settings) + } + + pub async fn delete(&self, redis: &Client) -> Result<()> { + let key = format!("{ROOT_KEY}:{}", self.guild_id); + let mut con = redis.get_async_connection().await?; + + con.del(&key) + .await + .wrap_err_with(|| format!("Couldn't delete {key} from Redis!"))?; - Some(Self { - allowed_guilds, - pinboard_target, - pinboard_sources, - reactboard_target, - reactboard_requirement, - reactboard_custom_reactions, - reactboard_unicode_reactions, - }) + Ok(()) } - pub fn can_use_reaction(&self, reaction: &MessageReaction) -> bool { - match &reaction.reaction_type { - ReactionType::Custom { - animated: _, - id, - name: _, - } => self.reactboard_custom_reactions.contains(id), - ReactionType::Unicode(name) => self.reactboard_unicode_reactions.contains(name), - // no other types exist yet, so assume we can't use them :p - _ => false, - } + pub async fn save(&self, redis: &Client) -> Result<()> { + let key = format!("{ROOT_KEY}:{}", self.guild_id); + let mut con = redis.get_async_connection().await?; + + con.set(&key, self) + .await + .wrap_err_with(|| format!("Couldn't save {key} in Redis!"))?; + Ok(()) } - pub fn is_guild_allowed(&self, gid: GuildId) -> bool { - self.allowed_guilds.contains(&gid) + pub fn can_use_reaction(&self, reaction: &ReactionType) -> bool { + if let Some(reactions) = &self.reactboard_reactions { + reactions.iter().any(|r| r == reaction) + } else { + false + } } } diff --git a/src/utils.rs b/src/utils.rs index 10140f4..e4ac03e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -6,17 +6,6 @@ use rand::seq::SliceRandom; use serenity::{CreateEmbed, Message}; use url::Url; -pub fn parse_snowflake_from_env T>(key: &str, f: F) -> Option { - std::env::var(key).ok().and_then(|v| v.parse().map(&f).ok()) -} -pub fn parse_snowflakes_from_env T>(key: &str, f: F) -> Option> { - std::env::var(key).ok().and_then(|gs| { - gs.split(',') - .map(|g| g.parse().map(&f)) - .collect::, _>>() - .ok() - }) -} /* * chooses a random element from an array */ -- cgit v1.2.3