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::{BoolExpressionMethods, ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl}; use serenity::async_trait; use serenity::model::prelude::{ ChannelId, Embed, EmojiId, GuildChannel, GuildId, Message, Reaction, ReactionType, }; use serenity::prelude::*; use std::sync::Arc; use tokio::sync::Mutex; 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<()> { let guild = match reaction.channel_id.to_channel(ctx).await?.guild() { Some(x) => x, None => return Ok(()), }; // get corresponding guild settings let 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(&*self.conn.lock().await)?; let gs = match gs.first() { 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) { self.handle_matching_reaction(ctx, gs, mr.count, &msg, &guild.guild_id) .await?; break; } } } Ok(()) } async fn handle_matching_reaction( &self, ctx: &Context, gs: &ServerSetting, count: u64, msg: &Message, guild: &GuildId, ) -> anyhow::Result<()> { if count >= gs.starboard_threshold as u64 { let original_id = BigDecimal::from(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 channel let name = msg .author .nick_in(&ctx, guild) .await .unwrap_or_else(|| msg.author.tag()); let image = msg .attachments .iter() .filter(|a| a.width.is_some()) .map(|a| &a.url) .next(); let repost = ChannelId( gs.starboard_channel .to_u64() .context("Could not convert starboard channel to a u64")?, ) .send_message(ctx, |m| { m.embed(|e| { let mut e = e .description(format!( "[Jump to source]({})\n{}", msg.link(), msg.content )) .author(|a| a.name(&name).icon_url(msg.author.face())) .timestamp(&msg.timestamp); if let Some(url) = image { e = e.image(url); } e }) }) .await?; // update the DB let repost_id = BigDecimal::from_u64(repost.id.0); 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(()) } fn emoji_match(rt: &ReactionType, em: EmojiId) -> bool { matches!(rt, ReactionType::Custom { id, .. } if *id == em) } } #[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); } } }