From fcf951b7712da947f3bc3a474b04a0b46c287ede Mon Sep 17 00:00:00 2001 From: seth Date: Thu, 16 Nov 2023 00:33:59 -0500 Subject: feat: implement event handler correctly --- src/consts.rs | 3 +- src/handler/events.rs | 24 ---------- src/handler/message.rs | 35 +++++++++++++++ src/handler/mod.rs | 51 +++++++++++----------- src/handler/pinboard.rs | 113 ++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 24 +++------- src/pinboard.rs | 113 ------------------------------------------------ 7 files changed, 181 insertions(+), 182 deletions(-) delete mode 100644 src/handler/events.rs create mode 100644 src/handler/message.rs create mode 100644 src/handler/pinboard.rs delete mode 100644 src/pinboard.rs (limited to 'src') diff --git a/src/consts.rs b/src/consts.rs index 27a70bc..afcd499 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -1,7 +1,6 @@ -use poise::serenity_prelude::{GuildId, UserId}; +use poise::serenity_prelude::GuildId; pub const TEAWIE_GUILD: GuildId = GuildId(1055663552679137310); -pub const BOT: UserId = UserId(1056467120986271764); pub const TEAMOJIS: [&str; 15] = [ "<:teawiecry:1056438041872433303>", diff --git a/src/handler/events.rs b/src/handler/events.rs deleted file mode 100644 index d971b25..0000000 --- a/src/handler/events.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::handler::Handler; -use log::*; -use poise::async_trait; -use poise::serenity_prelude::{ChannelPinsUpdateEvent, Context, EventHandler, Message}; - -#[async_trait] -impl EventHandler for Handler { - async fn message(&self, ctx: Context, msg: Message) { - if self.should_echo(&msg) { - let send = msg.reply(&ctx, &msg.content); - if let Err(why) = send.await { - error!("error when replying to {:?}: {:?}", msg.content, why); - } - } - } - - async fn channel_pins_update(&self, ctx: Context, pin: ChannelPinsUpdateEvent) { - let Some(pin_board) = &self.data.pin_board else { - return; - }; - - pin_board.handle_pin(&ctx, &pin).await; - } -} diff --git a/src/handler/message.rs b/src/handler/message.rs new file mode 100644 index 0000000..37a49bf --- /dev/null +++ b/src/handler/message.rs @@ -0,0 +1,35 @@ +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 index 7f7c881..6085617 100644 --- a/src/handler/mod.rs +++ b/src/handler/mod.rs @@ -1,34 +1,35 @@ -use crate::utils; -use crate::{consts, Data}; -use log::*; +use crate::{Data, Error}; +use poise::serenity_prelude as serenity; +use poise::Event; -use poise::serenity_prelude::Message; +mod message; +pub mod pinboard; -mod events; +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) + } -pub struct Handler { - data: Data, -} + Event::Message { new_message } => { + message::handle(ctx, event, _framework, data, new_message).await?; + } -impl Handler { - pub fn new(data: Data) -> Self { - Self { data } - } + Event::ChannelPinsUpdate { pin } => { + let Some(pin_board) = &data.pin_board else { + return Ok(()); + }; - fn should_echo(&self, msg: &Message) -> bool { - let gid = msg.guild_id.unwrap_or_default(); - if msg.author.id == self.data.bot || !utils::is_guild_allowed(gid) { - info!("not running copypasta command in {gid}"); - return false; + pin_board.handle_pin(ctx, pin).await; } - 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") + _ => {} } + + Ok(()) } diff --git a/src/handler/pinboard.rs b/src/handler/pinboard.rs new file mode 100644 index 0000000..56f53b4 --- /dev/null +++ b/src/handler/pinboard.rs @@ -0,0 +1,113 @@ +use crate::utils::{floor_char_boundary, parse_snowflake_from_env, parse_snowflakes_from_env}; +use log::*; +use poise::serenity_prelude::model::prelude::*; +use poise::serenity_prelude::Context; + +#[derive(Clone)] +pub struct PinBoard { + sources: Option>, + target: ChannelId, +} +impl PinBoard { + pub fn new() -> Option { + let Some(target) = parse_snowflake_from_env("PIN_BOARD_TARGET", ChannelId) else { + return None; + }; + let sources = parse_snowflakes_from_env("PIN_BOARD_SOURCES", ChannelId); + + Some(Self { sources, target }) + } + + pub async fn handle_pin(&self, ctx: &Context, pin: &ChannelPinsUpdateEvent) { + if let Some(sources) = &self.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. + self.redirect(ctx, &pin, pinner.take()).await; + pin.unpin(&ctx).await.expect("couldn't unpin message"); + } + } + + async fn redirect(&self, ctx: &Context, pin: &Message, pinner: Option) { + let pinner = pinner.map_or("*someone*".to_owned(), |u| format!("<@{u}>")); + + let truncation_point = floor_char_boundary(&pin.content, 700); + let truncated_content = if pin.content.len() <= truncation_point { + pin.content.to_string() + } else { + format!("{}...", &pin.content[..truncation_point]) + }; + let color = pin + .member(ctx) + .await + .ok() + .and_then(|m| m.highest_role_info(&ctx.cache)) + .and_then(|(role, _)| role.to_role_cached(&ctx.cache)) + .map(|role| role.colour); + + self.target + .send_message(&ctx.http, |m| { + m.allowed_mentions(|am| am.empty_parse()) + .content(format!("📌'd by {pinner} in {}", pin.link())) + .add_embed(|embed| { + embed.author(|author| { + author.name(&pin.author.name).icon_url(pin.author.face()) + }); + + if let Some(color) = color { + embed.color(color); + } + embed.description(truncated_content) + }) + }) + .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/main.rs b/src/main.rs index c604df6..6aa1cb4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,14 @@ use std::time::Duration; use std::{env, error}; +use handler::pinboard::PinBoard; use log::*; -use pinboard::PinBoard; use poise::serenity_prelude as serentiy; -use poise::serenity_prelude::*; mod api; mod commands; mod consts; mod handler; -mod pinboard; mod utils; type Error = Box; @@ -18,7 +16,6 @@ type Context<'a> = poise::Context<'a, Data, Error>; #[derive(Clone)] pub struct Data { - bot: serentiy::UserId, pin_board: Option, } @@ -30,10 +27,9 @@ impl Default for Data { impl Data { pub fn new() -> Self { - let bot = utils::parse_snowflake_from_env("BOT", UserId).unwrap_or(consts::BOT); let pin_board = PinBoard::new(); - Self { bot, pin_board } + Self { pin_board } } } @@ -58,13 +54,8 @@ async fn main() { let options = poise::FrameworkOptions { commands: commands::to_global_commands(), - event_handler: |ctx, event, _, data| { - Box::pin(async move { - // yes this is dumb. no i don't care. - let handler = handler::Handler::new(data.clone()); - event.clone().dispatch(ctx.clone(), &handler).await; - Ok(()) - }) + event_handler: |ctx, event, framework, data| { + Box::pin(handler::handle(ctx, event, framework, data)) }, prefix_options: poise::PrefixFrameworkOptions { prefix: Some("!".into()), @@ -73,9 +64,7 @@ async fn main() { }, on_error: |error| Box::pin(on_error(error)), command_check: Some(|ctx| { - Box::pin(async move { - Ok(ctx.author().id != ctx.framework().bot_id && ctx.author().id != consts::BOT) - }) + Box::pin(async move { Ok(ctx.author().id != ctx.framework().bot_id) }) }), ..Default::default() }; @@ -86,10 +75,9 @@ async fn main() { .intents(serentiy::GatewayIntents::all()) .setup(|ctx, _ready, framework| { Box::pin(async move { - info!("logged in as {}", _ready.user.name); - poise::builtins::register_globally(ctx, &framework.options().commands).await?; info!("registered global commands!"); + poise::builtins::register_in_guild( ctx, &commands::to_guild_commands(), diff --git a/src/pinboard.rs b/src/pinboard.rs deleted file mode 100644 index 56f53b4..0000000 --- a/src/pinboard.rs +++ /dev/null @@ -1,113 +0,0 @@ -use crate::utils::{floor_char_boundary, parse_snowflake_from_env, parse_snowflakes_from_env}; -use log::*; -use poise::serenity_prelude::model::prelude::*; -use poise::serenity_prelude::Context; - -#[derive(Clone)] -pub struct PinBoard { - sources: Option>, - target: ChannelId, -} -impl PinBoard { - pub fn new() -> Option { - let Some(target) = parse_snowflake_from_env("PIN_BOARD_TARGET", ChannelId) else { - return None; - }; - let sources = parse_snowflakes_from_env("PIN_BOARD_SOURCES", ChannelId); - - Some(Self { sources, target }) - } - - pub async fn handle_pin(&self, ctx: &Context, pin: &ChannelPinsUpdateEvent) { - if let Some(sources) = &self.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. - self.redirect(ctx, &pin, pinner.take()).await; - pin.unpin(&ctx).await.expect("couldn't unpin message"); - } - } - - async fn redirect(&self, ctx: &Context, pin: &Message, pinner: Option) { - let pinner = pinner.map_or("*someone*".to_owned(), |u| format!("<@{u}>")); - - let truncation_point = floor_char_boundary(&pin.content, 700); - let truncated_content = if pin.content.len() <= truncation_point { - pin.content.to_string() - } else { - format!("{}...", &pin.content[..truncation_point]) - }; - let color = pin - .member(ctx) - .await - .ok() - .and_then(|m| m.highest_role_info(&ctx.cache)) - .and_then(|(role, _)| role.to_role_cached(&ctx.cache)) - .map(|role| role.colour); - - self.target - .send_message(&ctx.http, |m| { - m.allowed_mentions(|am| am.empty_parse()) - .content(format!("📌'd by {pinner} in {}", pin.link())) - .add_embed(|embed| { - embed.author(|author| { - author.name(&pin.author.name).icon_url(pin.author.face()) - }); - - if let Some(color) = color { - embed.color(color); - } - embed.description(truncated_content) - }) - }) - .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 - } -} -- cgit v1.2.3