diff options
| author | seth <[email protected]> | 2024-08-09 23:35:41 -0400 |
|---|---|---|
| committer | GitHub <[email protected]> | 2024-08-09 23:35:41 -0400 |
| commit | b643a6a235b0c1c9902b97421f24eff2b0d0a5ac (patch) | |
| tree | 350794c0e9330fb77367838313bc6bb97278a0aa /src/events | |
| parent | 372780546b508684839916e5ad54c9e90456a94f (diff) | |
tree-wide: end of summer cleanup (#214)
* api: refactor & rename module to http
* client: split from main.rs
* tree-wide: use eyre::Report as error
* nix: alejandra -> nixfmt
* nix: start using treefmt-nix
* nix: simplify flake
* nix: refactor derivation & docker image
* nix: remove overlay
* ci: update & cleanup workflows
* commands: assign all commands automatically
* commands/copypasta: remove
* http/teawie: update response struct for upstream rust rewrite
* handlers: rename modules to events; flatten
* crates: rename self to teawie-bot
* nix: fenix -> rust-overlay
i want a specific rust version grrrrrrr
* ci: pin rust to 1.79
this is what our nix dev shell uses and what we can compile on. it seems
the time crate doesn't like v1.80 of the compiler :(
* ci: always run release gates
* nix: fix static toolchain
* nix: rust-overlay -> nixpkgs
* ci: adopt actions-rust-lang actions
* nix: use docker arch names for containers
* crates/time: 0.3.30 -> 0.3.36
fixes building on rust 1.80.0
Diffstat (limited to 'src/events')
| -rw-r--r-- | src/events/error.rs | 54 | ||||
| -rw-r--r-- | src/events/guild.rs | 37 | ||||
| -rw-r--r-- | src/events/message.rs | 44 | ||||
| -rw-r--r-- | src/events/mod.rs | 52 | ||||
| -rw-r--r-- | src/events/pinboard.rs | 81 | ||||
| -rw-r--r-- | src/events/reactboard.rs | 143 |
6 files changed, 411 insertions, 0 deletions
diff --git a/src/events/error.rs b/src/events/error.rs new file mode 100644 index 0000000..d98bb5c --- /dev/null +++ b/src/events/error.rs @@ -0,0 +1,54 @@ +use crate::{ + client::{Data, Error}, + consts::Colors, +}; + +use log::error; +use poise::serenity_prelude::{CreateEmbed, Timestamp}; +use poise::{CreateReply, FrameworkError}; + +pub async fn handle(error: poise::FrameworkError<'_, Data, Error>) { + match error { + FrameworkError::Setup { + error, framework, .. + } => { + error!("Error setting up client! Bailing out"); + framework.shard_manager().shutdown_all().await; + + panic!("{error}") + } + + FrameworkError::Command { error, ctx, .. } => { + error!("Error in command {}:\n{error}", ctx.command().name); + + let embed = CreateEmbed::default() + .title("Something went wrong!") + .description("oopsie") + .timestamp(Timestamp::now()) + .color(Colors::Orange); + + let reply = CreateReply::default().embed(embed); + + ctx.send(reply).await.ok(); + } + + FrameworkError::EventHandler { + error, + ctx: _, + event, + framework: _, + .. + } => { + error!( + "Error while handling event {}:\n{error}", + event.snake_case_name() + ); + } + + error => { + if let Err(e) = poise::builtins::on_error(error).await { + error!("Unhandled error occurred:\n{e:#?}"); + } + } + } +} diff --git a/src/events/guild.rs b/src/events/guild.rs new file mode 100644 index 0000000..06af978 --- /dev/null +++ b/src/events/guild.rs @@ -0,0 +1,37 @@ +use crate::{client::Data, storage}; +use storage::settings::Settings; + +use eyre::Result; +use log::{debug, warn}; +use poise::serenity_prelude::{Guild, UnavailableGuild}; + +pub async fn handle_create(guild: &Guild, data: &Data) -> Result<()> { + if let Some(storage) = &data.storage { + if storage.guild_settings_exist(&guild.id).await? { + debug!("Not recreating settings key for {}", guild.id); + return Ok(()); + } + + let settings = Settings { + guild_id: guild.id, + ..Default::default() + }; + + warn!("Creating new settings key for {}:\n{settings:#?}", guild.id); + storage.create_guild_settings(settings).await?; + } else { + warn!("Can't create guild settings; no storage backend found!"); + } + + Ok(()) +} + +pub async fn handle_delete(guild: &UnavailableGuild, data: &Data) -> Result<()> { + if let Some(storage) = &data.storage { + storage.delete_guild_settings(&guild.id).await?; + } else { + warn!("Can't delete guild settings; no storage backend found!"); + } + + Ok(()) +} diff --git a/src/events/message.rs b/src/events/message.rs new file mode 100644 index 0000000..e115eb9 --- /dev/null +++ b/src/events/message.rs @@ -0,0 +1,44 @@ +use crate::{client::Data, consts}; + +use eyre::{eyre, Result}; +use log::{debug, warn}; +use poise::serenity_prelude::{Context, Message}; + +pub async fn handle(ctx: &Context, msg: &Message, data: &Data) -> Result<()> { + if should_echo(ctx, msg, data).await? { + msg.reply(ctx, &msg.content).await?; + } + + Ok(()) +} + +async fn should_echo(ctx: &Context, msg: &Message, data: &Data) -> Result<bool> { + if (msg.author.bot && msg.webhook_id.is_none()) || msg.is_own(ctx) { + debug!("Not repeating another bot"); + return Ok(false); + } + + let gid = msg + .guild_id + .ok_or_else(|| eyre!("Couldn't get GuildId from {}!", msg.id))?; + + if let Some(storage) = &data.storage { + let settings = storage.get_guild_settings(&gid).await?; + + if !settings.optional_commands_enabled { + debug!("Not echoing in guild {gid}"); + return Ok(false); + } + } else { + warn!("Ignoring restrictions on echoing messages; no storage backend is attached!"); + } + + let content = &msg.content; + + Ok(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/events/mod.rs b/src/events/mod.rs new file mode 100644 index 0000000..390c3a8 --- /dev/null +++ b/src/events/mod.rs @@ -0,0 +1,52 @@ +use crate::{client::Data, consts}; + +use eyre::Result; +use log::{debug, info}; +use poise::serenity_prelude::{self as serenity, CreateBotAuthParameters}; +use serenity::FullEvent; + +pub mod error; +mod guild; +mod message; +mod pinboard; +mod reactboard; + +pub async fn handle(ctx: &serenity::Context, event: &FullEvent, data: &Data) -> Result<()> { + match event { + FullEvent::Ready { data_about_bot } => { + info!("Logged in as {}!", data_about_bot.user.name); + + if let Ok(invite_link) = CreateBotAuthParameters::new().auto_client_id(ctx).await { + let link = invite_link + .scopes(consts::bot_scopes()) + .permissions(*consts::bot_permissions()) + .build(); + info!("Invite me to your server at {link}"); + } else { + debug!("Not displaying invite_link since we couldn't find our client ID"); + } + } + + FullEvent::Message { new_message } => { + message::handle(ctx, new_message, data).await?; + pinboard::handle(ctx, new_message, data).await?; + } + + FullEvent::ReactionAdd { add_reaction } => { + reactboard::handle(ctx, add_reaction, data).await?; + } + + FullEvent::GuildCreate { guild, is_new: _ } => { + guild::handle_create(guild, data).await?; + } + + FullEvent::GuildDelete { + incomplete, + full: _, + } => guild::handle_delete(incomplete, data).await?, + + _ => {} + } + + Ok(()) +} diff --git a/src/events/pinboard.rs b/src/events/pinboard.rs new file mode 100644 index 0000000..bb0dfe0 --- /dev/null +++ b/src/events/pinboard.rs @@ -0,0 +1,81 @@ +use crate::{client::Data, utils}; + +use eyre::{eyre, Context as _, OptionExt as _, Result}; +use log::{debug, warn}; +use poise::serenity_prelude::{ + ChannelId, Context, CreateAllowedMentions, CreateMessage, Message, MessageType, User, +}; + +pub async fn handle(ctx: &Context, message: &Message, data: &Data) -> Result<()> { + if message.kind != MessageType::PinsAdd { + return Ok(()); + } + + let gid = message.guild_id.unwrap_or_default(); + let Some(storage) = &data.storage else { + warn!("Can't create PinBoard entry; no storage backend found!"); + return Ok(()); + }; + + let settings = storage.get_guild_settings(&gid).await?; + + if !settings.pinboard_enabled { + debug!("PinBoard is disabled in {gid}, ignoring"); + return Ok(()); + } + + let Some(target) = settings.pinboard_channel else { + debug!("PinBoard is disabled in {gid}, ignoring"); + return Ok(()); + }; + + if let Some(sources) = settings.pinboard_watch { + if !sources.contains(&message.channel_id) { + debug!( + "{} not listed in PinBoard settings for {gid}, ignoring", + message.channel_id + ); + + return Ok(()); + } + } + + let reference_id = message + .clone() + .message_reference + .ok_or_eyre("Couldn't get referenced message of pin!")? + .message_id + .ok_or_eyre("Couldn't get id of referenced message of pin!")?; + + let pins = message + .channel_id + .pins(ctx) + .await + .wrap_err("Couldn't get a list of pins!?")?; + + let pin = pins + .iter() + .find(|pin| pin.id == reference_id) + .ok_or_else(|| eyre!("Couldn't find a pin for message {reference_id}!"))?; + + redirect(ctx, pin, &message.author, &target).await?; + pin.unpin(ctx).await?; + + Ok(()) +} + +async fn redirect(ctx: &Context, pin: &Message, pinner: &User, target: &ChannelId) -> Result<()> { + let embed = utils::resolve_message_to_embed(ctx, pin).await; + let mentions = CreateAllowedMentions::new().empty_roles().empty_users(); + let message = CreateMessage::default() + .allowed_mentions(mentions) + .content(format!("📌'd by {pinner} in {}", pin.link())) + .embed(embed); + + target + .send_message(&ctx.http, message) + .await + .wrap_err("Couldn't redirect message")?; + + Ok(()) +} diff --git a/src/events/reactboard.rs b/src/events/reactboard.rs new file mode 100644 index 0000000..c27bd80 --- /dev/null +++ b/src/events/reactboard.rs @@ -0,0 +1,143 @@ +use crate::{client::Data, storage, utils}; +use storage::reactboard::ReactBoardEntry; + +use eyre::{eyre, Context as _, Result}; +use log::{debug, warn}; +use poise::serenity_prelude::{ + Context, CreateMessage, EditMessage, GuildId, Message, MessageReaction, Reaction, +}; + +pub async fn handle(ctx: &Context, reaction: &Reaction, data: &Data) -> Result<()> { + // TODO @getchoo: don't do anything if this message is old + 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, + &reaction.guild_id.unwrap_or_default(), + data, + ) + .await?; + + Ok(()) +} + +async fn send_to_reactboard( + ctx: &Context, + reaction: &MessageReaction, + msg: &Message, + guild_id: &GuildId, + data: &Data, +) -> Result<()> { + let Some(storage) = &data.storage else { + warn!("Can't make ReactBoard entry; no storage backend found!"); + return Ok(()); + }; + + let settings = storage.get_guild_settings(guild_id).await?; + + // make sure everything is in order... + if !settings.reactboard_enabled { + debug!("ReactBoard is disabled in {guild_id}, ignoring"); + return Ok(()); + } + + let Some(target) = settings.reactboard_channel else { + debug!("ReactBoard is disabled in {guild_id}, ignoring"); + return Ok(()); + }; + + if !settings.can_use_reaction(&reaction.reaction_type) { + debug!("Reaction {} can't be used!", reaction.reaction_type); + return Ok(()); + } + + let count = if msg + .reaction_users(ctx, reaction.reaction_type.clone(), None, None) + .await? + .contains(&msg.author) + { + reaction.count - 1 + } else { + reaction.count + }; + + if count < settings.reactboard_requirement.unwrap_or(5) { + debug!( + "Ignoring message {} on ReactBoard, not enough reactions", + msg.id + ); + return Ok(()); + } + + let content = format!("{} **#{}**", reaction.reaction_type, count); + + let entry = if storage.reactboard_entry_exists(guild_id, &msg.id).await? { + // bump reaction count if previous entry exists + let mut entry = storage.get_reactboard_entry(guild_id, &msg.id).await?; + + // bail if we don't need to edit anything + if entry.reaction_count >= count { + debug!("Message {} doesn't need updating", msg.id); + return Ok(()); + } + + debug!( + "Bumping {} reaction count from {} to {}", + msg.id, entry.reaction_count, count + ); + + let edited = EditMessage::new().content(content); + + ctx.http + .get_message(entry.posted_channel_id, entry.posted_message_id) + .await + .wrap_err_with(|| { + format!( + "Couldn't get previous message from ReactBoardEntry {} in Redis DB!", + entry.original_message_id + ) + })? + .edit(ctx, edited) + .await?; + + // update reaction count in redis + entry.reaction_count = count; + entry + } else { + // make new message and add entry to redis otherwise + let embed = utils::resolve_message_to_embed(ctx, msg).await; + let message = CreateMessage::default().content(content).embed(embed); + + let resp = target.send_message(ctx, message).await?; + + ReactBoardEntry { + original_message_id: msg.id, + reaction_count: count, + posted_channel_id: resp.channel_id, + posted_message_id: resp.id, + } + }; + + debug!("Creating new ReactBoard entry:\n{entry:#?}"); + storage.create_reactboard_entry(guild_id, entry).await?; + + Ok(()) +} |
