diff options
| author | seth <[email protected]> | 2023-11-27 21:37:52 -0500 |
|---|---|---|
| committer | seth <[email protected]> | 2023-12-01 07:12:49 -0500 |
| commit | db52e639b85d79bed870020aec7a045851ca5ee3 (patch) | |
| tree | b5895e3c219260e07d39149fa2f2215f8c9b95aa /src | |
| parent | 47b69d937ed944aaa41fa80661cdfa9ec72246ca (diff) | |
feat: add reactboard
Diffstat (limited to 'src')
| -rw-r--r-- | src/handler/mod.rs | 17 | ||||
| -rw-r--r-- | src/handler/pinboard.rs | 128 | ||||
| -rw-r--r-- | src/handler/reactboard.rs | 66 | ||||
| -rw-r--r-- | src/main.rs | 9 | ||||
| -rw-r--r-- | src/settings.rs | 80 | ||||
| -rw-r--r-- | src/utils.rs | 60 |
6 files changed, 251 insertions, 109 deletions
diff --git a/src/handler/mod.rs b/src/handler/mod.rs index 6085617..3489b4a 100644 --- a/src/handler/mod.rs +++ b/src/handler/mod.rs @@ -4,11 +4,12 @@ use poise::Event; mod message; pub mod pinboard; +mod reactboard; pub async fn handle( ctx: &serenity::Context, event: &Event<'_>, - _framework: poise::FrameworkContext<'_, Data, Error>, + framework: poise::FrameworkContext<'_, Data, Error>, data: &Data, ) -> Result<(), Error> { match event { @@ -17,15 +18,19 @@ pub async fn handle( } Event::Message { new_message } => { - message::handle(ctx, event, _framework, data, new_message).await?; + message::handle(ctx, event, framework, data, new_message).await? } Event::ChannelPinsUpdate { pin } => { - let Some(pin_board) = &data.pin_board else { - return Ok(()); - }; + if let Some(settings) = &data.settings { + pinboard::handle(ctx, pin, settings).await + } + } - pin_board.handle_pin(ctx, pin).await; + Event::ReactionAdd { add_reaction } => { + if let Some(settings) = &data.settings { + reactboard::handle(ctx, add_reaction, settings).await? + } } _ => {} diff --git a/src/handler/pinboard.rs b/src/handler/pinboard.rs index 35d477d..0c87a5b 100644 --- a/src/handler/pinboard.rs +++ b/src/handler/pinboard.rs @@ -1,112 +1,44 @@ +use crate::settings::Settings; use crate::utils; use log::*; use poise::serenity_prelude::model::prelude::*; -use poise::serenity_prelude::{Context, CreateEmbed}; +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) = utils::parse_snowflake_from_env("PIN_BOARD_TARGET", ChannelId) else { - return None; - }; - let sources = utils::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"); +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; } } - 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 = utils::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); - - let attached_image = pin - .attachments - .iter() - .filter(|a| { - a.content_type - .as_ref() - .filter(|ct| ct.contains("image/")) - .is_some() - }) - .map(|a| &a.url) - .next(); - - let attachments_len = pin.attachments.len(); - - 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| { - // only use the first embed if it's in the message, since more could be a little spammy - if let Some(pinned_embed) = pin.embeds.first() { - embed.clone_from(&CreateEmbed::from(pinned_embed.clone())) - } - - embed.author(|author| { - author.name(&pin.author.name).icon_url(pin.author.face()) - }); - - if let Some(color) = color { - embed.color(color); - } + 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!?"); - if let Some(attachment) = attached_image { - embed.image(attachment); - } + 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"); + } +} - if attachments_len > 1 { - embed.footer(|footer| { - // yes it will say '1 attachments' no i do not care - footer.text(format!("{} attachments", attachments_len)) - }); - } +async fn redirect(ctx: &Context, pin: &Message, pinner: Option<UserId>, target: ChannelId) { + let pinner = pinner.map_or("*someone*".to_owned(), |u| format!("<@{u}>")); + let embed = utils::resolve_message_to_embed(ctx, pin).await; - embed.description(truncated_content) - }) - }) - .await - .expect("couldn't redirect message"); - } + 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 diff --git a/src/handler/reactboard.rs b/src/handler/reactboard.rs new file mode 100644 index 0000000..36f8361 --- /dev/null +++ b/src/handler/reactboard.rs @@ -0,0 +1,66 @@ +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/main.rs b/src/main.rs index 74ef531..a93e102 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,15 @@ use std::{error, time}; -use handler::pinboard::PinBoard; use log::*; use poise::serenity_prelude as serentiy; +use settings::Settings; mod api; mod colors; mod commands; mod consts; mod handler; +mod settings; mod utils; type Error = Box<dyn error::Error + Send + Sync>; @@ -16,14 +17,14 @@ type Context<'a> = poise::Context<'a, Data, Error>; #[derive(Clone)] pub struct Data { - pin_board: Option<PinBoard>, + settings: Option<Settings>, } impl Data { pub fn new() -> Self { - let pin_board = PinBoard::new(); + let settings = Settings::new(); - Self { pin_board } + Self { settings } } } diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 0000000..f0fef31 --- /dev/null +++ b/src/settings.rs @@ -0,0 +1,80 @@ +use crate::utils; +use log::*; +use poise::serenity_prelude::{ChannelId, EmojiId, MessageReaction, ReactionType}; + +#[derive(Clone)] +pub struct Settings { + pub pinboard_target: ChannelId, + pub pinboard_sources: Option<Vec<ChannelId>>, + pub reactboard_target: ChannelId, + pub reactboard_requirement: Option<u64>, + pub reactboard_custom_reactions: Vec<EmojiId>, + pub reactboard_unicode_reactions: Vec<String>, +} + +impl Settings { + pub fn new() -> Option<Self> { + let Some(pinboard_target) = utils::parse_snowflake_from_env("PIN_BOARD_TARGET", ChannelId) + else { + return None; + }; + 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 reactboard_requirement = utils::parse_snowflake_from_env("REACT_BOARD_MIN", u64::from); + + let reactboard_custom_reactions = + utils::parse_snowflakes_from_env("REACT_BOARD_CUSTOM_REACTIONS", EmojiId) + .unwrap_or_default(); + + let reactboard_unicode_reactions = std::env::var("REACT_BOARD_UNICODE_REACTIONS") + .ok() + .map(|v| { + v.split(',') + .map(|vs| vs.to_string()) + .collect::<Vec<String>>() + }) + .unwrap_or_default(); + + 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 + ); + + Some(Self { + pinboard_target, + pinboard_sources, + reactboard_target, + reactboard_requirement, + reactboard_custom_reactions, + reactboard_unicode_reactions, + }) + } + + 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, + } + } +} diff --git a/src/utils.rs b/src/utils.rs index 28cacaa..af079ff 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,8 +2,9 @@ use crate::{colors, consts, Context, Error}; use log::*; use once_cell::sync::Lazy; -use poise::serenity_prelude::GuildId; +use poise::serenity_prelude as serenity; use rand::seq::SliceRandom; +use serenity::{CreateEmbed, GuildId, Message}; use url::Url; pub fn parse_snowflake_from_env<T, F: Fn(u64) -> T>(key: &str, f: F) -> Option<T> { @@ -81,3 +82,60 @@ pub async fn send_url_as_embed(ctx: Context<'_>, url: String) -> Result<(), Erro Ok(()) } + +pub async fn resolve_message_to_embed(ctx: &serenity::Context, msg: &Message) -> CreateEmbed { + let truncation_point = floor_char_boundary(&msg.content, 700); + let truncated_content = if msg.content.len() <= truncation_point { + msg.content.to_string() + } else { + format!("{}...", &msg.content[..truncation_point]) + }; + + let color = msg + .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); + + let attached_image = msg + .attachments + .iter() + .filter(|a| { + a.content_type + .as_ref() + .filter(|ct| ct.contains("image/")) + .is_some() + }) + .map(|a| &a.url) + .next(); + + let attachments_len = msg.attachments.len(); + + let mut embed = msg + .embeds + .first() + .map(|embed| CreateEmbed::from(embed.clone())) + .unwrap_or_default(); + + embed.author(|author| author.name(&msg.author.name).icon_url(&msg.author.face())); + + if let Some(color) = color { + embed.color(color); + } + + if let Some(attachment) = attached_image { + embed.image(attachment); + } + + if attachments_len > 1 { + embed.footer(|footer| { + // yes it will say '1 attachments' no i do not care + footer.text(format!("{} attachments", attachments_len)) + }); + } + + embed.description(truncated_content); + embed +} |
