use crate::handlers::ReactionHandler; use crate::models::ServerSetting; use crate::schema; use crate::schema::{server_settings, starboard_mappings}; use anyhow::Context as AnyhowContext; use bigdecimal::{BigDecimal, FromPrimitive, ToPrimitive}; use diesel::{ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl}; use serenity::async_trait; use serenity::builder::CreateEmbed; use serenity::model::prelude::{ChannelId, Emoji, EmojiId, Message, Reaction, ReactionType}; use serenity::prelude::*; use std::sync::Arc; use tokio::sync::Mutex; #[derive(Clone)] pub struct SingleMessageHandler { conn: Arc<Mutex<PgConnection>>, server_settings: ServerSetting, reaction_count: u64, msg: Message, emoji: Emoji, name: String, image: Option<String>, } impl SingleMessageHandler { pub async fn handle_reaction( conn: Arc<Mutex<PgConnection>>, ctx: &Context, reaction: &Reaction, ) -> anyhow::Result<()> { let guild = match reaction.channel_id.to_channel(ctx).await?.guild() { Some(x) => x, None => return Ok(()), }; // get corresponding guild settings let mut gs: Vec<ServerSetting> = server_settings::dsl::server_settings .filter( schema::server_settings::columns::guild_id .eq(BigDecimal::from_u64(guild.guild_id.0).unwrap()), ) .limit(1) .get_results(&*conn.lock().await)?; let gs = match gs.pop() { Some(x) => x, None => return Ok(()), }; let emoji = EmojiId( gs.starboard_emoji_id .to_u64() .context("Could not convert emoji id to u64")?, ); if Self::emoji_match(&reaction.emoji, emoji) { let msg = reaction.message(ctx).await?; for mr in &msg.reactions { if Self::emoji_match(&mr.reaction_type, emoji) { let guild_id = guild.guild_id; let emoji = guild_id .emoji(ctx, emoji) .await .context("Could not get emoji from guild")?; let name = msg .author .nick_in(ctx, guild_id) .await .unwrap_or_else(|| msg.author.tag()); let image = msg .attachments .iter() .filter(|a| a.width.is_some()) .map(|a| &a.url) .next() .cloned(); let handler = Self { conn, server_settings: gs, reaction_count: mr.count, msg, emoji, name, image, }; handler.process_match(ctx).await?; break; } } } Ok(()) } async fn process_match(&self, ctx: &Context) -> anyhow::Result<()> { if self.reaction_count >= self.server_settings.starboard_threshold as u64 { let original_id = BigDecimal::from(self.msg.id.0); diesel::insert_into(starboard_mappings::dsl::starboard_mappings) .values(starboard_mappings::columns::original_id.eq(&original_id)) .returning(starboard_mappings::columns::repost_id) .on_conflict_do_nothing() .execute(&*self.conn.lock().await)?; let repost_id = starboard_mappings::dsl::starboard_mappings .filter(starboard_mappings::columns::original_id.eq(&original_id)) .select(starboard_mappings::columns::repost_id) .limit(1) .get_results(&*self.conn.lock().await)?; let repost_id: &Option<BigDecimal> = repost_id.get(0).context("Insert of mapping failed")?; if repost_id.is_none() { // post to repost let repost = self.post_new_starboard_message(ctx).await?; // update the DB let repost_id = BigDecimal::from_u64(repost); diesel::update( starboard_mappings::dsl::starboard_mappings .filter(starboard_mappings::columns::original_id.eq(original_id)), ) .set(starboard_mappings::columns::repost_id.eq(repost_id)) .execute(&*self.conn.lock().await)?; } } Ok(()) } async fn post_new_starboard_message(&self, ctx: &Context) -> anyhow::Result<u64> { let repost = ChannelId( self.server_settings .starboard_channel .to_u64() .context("Could not convert starboard channel to a u64")?, ) .send_message(ctx, |m| m.embed(|e| self.clone().starboard_message(e))) .await?; Ok(repost.id.0) } fn starboard_message(self, e: &mut CreateEmbed) -> &mut CreateEmbed { let mut e = e .description(format!( "[Jump to source]({})\n{}", self.msg.link(), self.msg.content )) .title(format!("{} {}", self.reaction_count, self.emoji)) .author(|a| a.name(&self.name).icon_url(self.msg.author.face())) .timestamp(&self.msg.timestamp); if let Some(url) = self.image { e = e.image(url); } e } fn emoji_match(rt: &ReactionType, em: EmojiId) -> bool { matches!(rt, ReactionType::Custom { id, .. } if *id == em) } } pub struct StarboardHandler { conn: Arc<Mutex<PgConnection>>, } impl StarboardHandler { pub fn new(conn: Arc<Mutex<PgConnection>>) -> Self { Self { conn } } async fn handle_reaction(&self, ctx: &Context, reaction: &Reaction) -> anyhow::Result<()> { SingleMessageHandler::handle_reaction(self.conn.clone(), ctx, reaction).await } } #[async_trait] impl ReactionHandler for StarboardHandler { async fn reaction(&self, ctx: &Context, reaction: &Reaction) { if let Err(e) = self.handle_reaction(ctx, reaction).await { eprintln!("Error in starboard: {:?}", e); } } }