diff --git a/Cargo.lock b/Cargo.lock index 5480247c3b4b6cb8387bede7af1dddb83cdb5230..ceef6b8c68cbd996c4293a2b832445484053ebc5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,6 +14,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" +[[package]] +name = "anyhow" +version = "1.0.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" + [[package]] name = "arrayvec" version = "0.4.12" @@ -116,6 +122,7 @@ checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" name = "cat-disruptor-6500" version = "0.1.2" dependencies = [ + "anyhow", "bigdecimal", "diesel", "dotenv", diff --git a/Cargo.toml b/Cargo.toml index ab17f677b2a8f70ee4d6fac812e8a59646834925..7a64ced8488831fd3e27e1b1adf55e09daa5d693 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,3 +22,4 @@ serde_json = "1.0" rusttype = "0.4.3" rand = "0.8" itertools = "0.10" +anyhow = "1.0" diff --git a/migrations/2022-10-05-220626_server_settings/down.sql b/migrations/2022-10-05-220626_server_settings/down.sql new file mode 100644 index 0000000000000000000000000000000000000000..7cc9fced19fb1c38ed0a78ad551fcec7171ba0b9 --- /dev/null +++ b/migrations/2022-10-05-220626_server_settings/down.sql @@ -0,0 +1 @@ +DROP TABLE server_settings; diff --git a/migrations/2022-10-05-220626_server_settings/up.sql b/migrations/2022-10-05-220626_server_settings/up.sql new file mode 100644 index 0000000000000000000000000000000000000000..0f6908906d4ffb80cacbb1d728afa3bebc08bb68 --- /dev/null +++ b/migrations/2022-10-05-220626_server_settings/up.sql @@ -0,0 +1,7 @@ +CREATE TABLE server_settings ( + id SERIAL PRIMARY KEY NOT NULL, + guild_id NUMERIC NOT NULL UNIQUE, + starboard_threshold INTEGER NOT NULL, + starboard_emoji_id NUMERIC NOT NULL, + starboard_channel NUMERIC NOT NULL +); diff --git a/migrations/2022-10-06-221533_starboard_mapping/down.sql b/migrations/2022-10-06-221533_starboard_mapping/down.sql new file mode 100644 index 0000000000000000000000000000000000000000..bad20246c7414330c6069c180c84250c60173ac4 --- /dev/null +++ b/migrations/2022-10-06-221533_starboard_mapping/down.sql @@ -0,0 +1 @@ +DROP TABLE starboard_mappings; diff --git a/migrations/2022-10-06-221533_starboard_mapping/up.sql b/migrations/2022-10-06-221533_starboard_mapping/up.sql new file mode 100644 index 0000000000000000000000000000000000000000..82e7f1606f5a1f01bcd6398b27180e59bb6477f2 --- /dev/null +++ b/migrations/2022-10-06-221533_starboard_mapping/up.sql @@ -0,0 +1,4 @@ +CREATE TABLE starboard_mappings ( + original_id NUMERIC NOT NULL UNIQUE PRIMARY KEY, + repost_id NUMERIC UNIQUE +); diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index d1c71e30621e825ce11299bcdbb1719ac3df069c..7df01df2c768f9e9c2de25e919eb4c4da861adbe 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -1,12 +1,14 @@ mod horse; mod joke; mod react; +mod starboard; mod sus; mod xbasic; use crate::handlers::horse::HorseHandler; use crate::handlers::joke::*; use crate::handlers::react::*; +use crate::handlers::starboard::StarboardHandler; use crate::handlers::sus::*; use crate::handlers::xbasic::*; @@ -14,7 +16,7 @@ use diesel::{Connection, PgConnection}; use dotenv::dotenv; use serenity::async_trait; use serenity::model::channel::Message; -use serenity::model::prelude::Ready; +use serenity::model::prelude::{Reaction, Ready}; use serenity::prelude::*; use std::env; use std::sync::Arc; @@ -31,8 +33,14 @@ pub(crate) trait LineHandler: Send + Sync { async fn line(&self, _ctx: &Context, _msg: &Message, _line: &str) {} } +#[async_trait] +pub(crate) trait ReactionHandler: Send + Sync { + async fn reaction(&self, _ctx: &Context, reaction: &Reaction); +} + pub(crate) struct Dispatcher { handlers: Vec<Box<dyn LineHandler>>, + reacts: Vec<Box<dyn ReactionHandler>>, } #[async_trait] @@ -43,6 +51,12 @@ impl EventHandler for Dispatcher { } } + async fn reaction_add(&self, ctx: Context, reaction: Reaction) { + for r in &self.reacts { + r.reaction(&ctx, &reaction).await; + } + } + async fn ready(&self, _: Context, ready: Ready) { println!("{} is connected", ready.user.name); } @@ -54,12 +68,13 @@ impl Default for Dispatcher { Self { handlers: vec![ - Box::new(XbasicHandler::new(conn)), + Box::new(XbasicHandler::new(conn.clone())), Box::new(JokeHandler::default()), Box::new(ReactHandler::default()), Box::new(SusHandler::default()), Box::new(HorseHandler::default()), ], + reacts: vec![Box::new(StarboardHandler::new(conn))], } } } diff --git a/src/handlers/starboard.rs b/src/handlers/starboard.rs new file mode 100644 index 0000000000000000000000000000000000000000..e19d07707a7d3993044df15ba640acd8a3c70f4c --- /dev/null +++ b/src/handlers/starboard.rs @@ -0,0 +1,137 @@ +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 repost = ChannelId( + gs.starboard_channel + .to_u64() + .context("Could not convert starboard channel to a u64")?, + ) + .send_message(ctx, |m| { + m.embed(|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) + }) + }) + .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); + } + } +} diff --git a/src/handlers/xbasic.rs b/src/handlers/xbasic.rs index fe3b27d1567552a8f16a291f2f62f82d52b1fa5c..efae2bd2cf92b7be3e85e559dd2d4b602955d4ee 100644 --- a/src/handlers/xbasic.rs +++ b/src/handlers/xbasic.rs @@ -1,6 +1,7 @@ use crate::framebuffer::FrameBuffer; use crate::handlers::LineHandler; use crate::program::Program; + use diesel::PgConnection; use serenity::async_trait; use serenity::http::AttachmentType; diff --git a/src/models.rs b/src/models.rs index 0b7f70519457ae868c5e23eb765b968a0f8b3ca1..10d32fea3696c52717ba4eb92d647342a6c39ac4 100644 --- a/src/models.rs +++ b/src/models.rs @@ -16,3 +16,12 @@ pub struct NewUserProgram<'a> { pub name: &'a str, pub code: &'a str, } + +#[derive(Queryable)] +pub struct ServerSetting { + pub id: i32, + pub guild_id: BigDecimal, + pub starboard_threshold: i32, + pub starboard_emoji_id: BigDecimal, + pub starboard_channel: BigDecimal, +} diff --git a/src/schema.rs b/src/schema.rs index 3278e43014c6a352eb533a370c60b35245b43e10..4a883c63425e0e2a5e64207792079a9764683d35 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,9 +1,32 @@ table! { - user_programs (id) { - id -> Int4, - discord_user_id -> Numeric, - name -> Varchar, - code -> Text, - published -> Int4, - } + server_settings (id) { + id -> Int4, + guild_id -> Numeric, + starboard_threshold -> Int4, + starboard_emoji_id -> Numeric, + starboard_channel -> Numeric, + } } + +table! { + starboard_mappings (original_id) { + original_id -> Numeric, + repost_id -> Nullable<Numeric>, + } +} + +table! { + user_programs (id) { + id -> Int4, + discord_user_id -> Numeric, + name -> Varchar, + code -> Text, + published -> Int4, + } +} + +allow_tables_to_appear_in_same_query!( + server_settings, + starboard_mappings, + user_programs, +);