summaryrefslogtreecommitdiff
path: root/src/handler
diff options
context:
space:
mode:
Diffstat (limited to 'src/handler')
-rw-r--r--src/handler/events.rs24
-rw-r--r--src/handler/message.rs35
-rw-r--r--src/handler/mod.rs51
-rw-r--r--src/handler/pinboard.rs113
4 files changed, 174 insertions, 49 deletions
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<Vec<ChannelId>>,
+ target: ChannelId,
+}
+impl PinBoard {
+ pub fn new() -> Option<Self> {
+ 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<UserId>) {
+ 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<UserId> {
+ 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
+ }
+}