summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/handler/mod.rs17
-rw-r--r--src/handler/pinboard.rs128
-rw-r--r--src/handler/reactboard.rs66
-rw-r--r--src/main.rs9
-rw-r--r--src/settings.rs80
-rw-r--r--src/utils.rs60
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
+}