diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/commands/filters.rs | 79 | ||||
-rw-r--r-- | src/handler.rs | 139 | ||||
-rw-r--r-- | src/main.rs | 25 | ||||
-rw-r--r-- | src/models.rs | 10 |
4 files changed, 235 insertions, 18 deletions
diff --git a/src/commands/filters.rs b/src/commands/filters.rs index 35fefac..5e8f900 100644 --- a/src/commands/filters.rs +++ b/src/commands/filters.rs @@ -1,5 +1,6 @@ -use crate::{Context, Error, models::{FilterAction, MessageFilter}}; +use crate::{Context, Error, models::{FilterAction, MessageFilter, ConfigChannel}}; use poise::{serenity_prelude as serenity, AutocompleteChoice}; +use regex::Regex; use std::str::FromStr; @@ -28,8 +29,8 @@ pub async fn list(ctx: Context<'_>) -> Result<(), Error> { if filters.len() == 0 { list = "No filters set, try adding one with `/filter add`!".to_string(); } else { - for f in filters { - list.push_str(&format!("{}\n", f.to_string())); + for (i, f) in filters.iter().enumerate() { + list.push_str(&format!("**{}.** {}\n", (i + 1), f.to_string())); } } @@ -49,7 +50,7 @@ pub async fn list(ctx: Context<'_>) -> Result<(), Error> { /// filter add <pattern> <action> /// /// Where <action> is one of "review" or "delete" -#[poise::command(slash_command, ephemeral, prefix_command)] +#[poise::command(slash_command, ephemeral, prefix_command, check = "c_modify_filters")] pub async fn add(ctx: Context<'_>, #[description = "The regular expression to match against"] regex: String, @@ -62,8 +63,9 @@ pub async fn add(ctx: Context<'_>, let action = FilterAction::from_str(&action)?; + let regex = Regex::new(®ex)?; sqlx::query("INSERT INTO message_filter (pattern, action, guild_id) VALUES ($1, $2, $3)") - .bind(regex) + .bind(regex.to_string()) .bind(action as FilterAction) .bind(ctx.guild().unwrap().id.0.to_string()) .execute(&pool) @@ -74,9 +76,76 @@ pub async fn add(ctx: Context<'_>, Ok(()) } +/// Deletes a message filter +/// +/// Usage: +/// filter del <id> +/// +/// You can get the filter ID from `filter list` +#[poise::command(slash_command, ephemeral, prefix_command, check = "c_modify_filters")] +pub async fn del(ctx: Context<'_>, + #[description = "The ID to delete"] + id: i32, + ) -> Result<(), Error> { + let pool = ctx.data().pg.lock().unwrap().clone(); + + sqlx::query("DELETE FROM message_filter WHERE id=$1") + .bind(id) + .execute(&pool) + .await?; + + ctx.say("Got it! I've removed that from my list.").await?; + Ok(()) +} + +/// Sets the review channel +/// +/// Usage: +/// filter channel <channel> +#[poise::command(slash_command, ephemeral, prefix_command, check = "c_modify_filters")] +pub async fn channel(ctx: Context<'_>, + #[description = "The channel to send flagged messages"] + channel_id: serenity::ChannelId, + ) -> Result<(), Error> { + let pool = ctx.data().pg.lock().unwrap().clone(); + + if let Ok(m) = sqlx::query_as!(ConfigChannel, "SELECT * FROM channels WHERE guild_id=$1 AND purpose='review'", ctx.guild().unwrap().id.0.to_string()).fetch_one(&pool).await { + sqlx::query!("UPDATE channels SET channel_id=$1 WHERE id=$2", channel_id.0.to_string(), m.id).execute(&pool).await?; + } else { + sqlx::query!("INSERT INTO channels (channel_id, purpose, guild_id) VALUES ($1, 'review', $2)", channel_id.0.to_string(), ctx.guild().unwrap().id.0.to_string()) + .execute(&pool).await?; + } + + ctx.say(format!("All set! I'll send flagged messages to <#{}> from now on", channel_id.0)).await?; + Ok(()) +} + async fn ac_action(_ctx: Context<'_>, partial: String) -> impl Stream<Item = String> { futures::stream::iter(&["review", "delete"]) .filter(move |name| futures::future::ready(name.starts_with(&partial))) .map(|name| name.to_string()) } + +#[cfg(debug_assertions)] +async fn c_modify_filters(ctx: Context<'_>) -> Result<bool, Error> { + if ctx.author().id.0 == 118455061222260736u64 { + Ok(true) + } else { + Ok(false) + } +} +#[cfg(not(debug_assertions))] +async fn c_modify_filters(ctx: Context<'_>) -> Result<bool, Error> { + if let Some(guild) = ctx.guild() { + if guild.owner_id == ctx.author().id { + Ok(true) + } else { + let member = guild.member(ctx.discord(), ctx.author().id).await?; + let member_permissions = member.permissions(ctx.discord())?; + Ok(member_permissions.manage_messages()) + } + } else { + Ok(false) + } +} diff --git a/src/handler.rs b/src/handler.rs index a52b31a..b83d789 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,4 +1,6 @@ -use poise::serenity_prelude as serenity; +use poise::serenity_prelude::{self as serenity, Interaction, MessageComponentInteraction, MessageId, ChannelId}; +use regex::Regex; +use serde_json::json; use crate::models::*; use crate::{Data, Error}; @@ -78,17 +80,150 @@ pub async fn event_handler( if new_message.author.id.0 != current_user.id.0 { // This message does *not* belong to the bot - println!("Message from not-bot"); let filters = sqlx::query_as::<_, MessageFilter>( "SELECT * FROM message_filter WHERE guild_id=$1") .bind(new_message.guild_id.unwrap().0.to_string()) .fetch_all(&pool) .await?; + + let content = new_message.content.clone(); + + for f in filters { + let r = Regex::new(&f.pattern)?; + + if r.is_match(&content) { + match f.action { + FilterAction::Review => { + if let Ok(c) = sqlx::query_as!(ConfigChannel, "SELECT * FROM channels WHERE purpose='review' AND guild_id=$1", new_message.guild_id.unwrap().0.to_string()) + .fetch_one(&pool) + .await { + let chan = serenity::ChannelId(c.channel_id.parse::<u64>()?); + + chan.send_message(&ctx, |m| { + m.content(format!("Message was flagged for review (using rule {})", f.id)); + m.components(|c| { + c.create_action_row(|r| { + r.create_button(|b| { + b.style(serenity::ButtonStyle::Primary); + b.label("Approve"); + b.custom_id("approve"); + b + }); + r.create_button(|b| { + b.style(serenity::ButtonStyle::Danger); + b.label("Deny"); + b.custom_id("deny"); + b + }); + r + }); + c + }); + m.embed(|e| { + e.author(|a| { + a.name(new_message.clone().author.name); + a.icon_url(new_message.clone().author.face()) + }); + e.description(content.clone()); + e.footer(|f| { + f.text(format!("{}:{}", new_message.channel_id.0, new_message.id.0.to_string())) + }); + e.field("Link", new_message.link().clone(), true) + }) + }).await?; + } else { + let guild = new_message.guild(&ctx).unwrap(); + + let owner = guild.owner_id; + let dm_chan = owner.create_dm_channel(&ctx.http).await?; + dm_chan.say(&ctx.http, format!("Hi! I tried to flag a message for review in {}, but you haven't set a review channel yet! Set one now by running `filter channel` in the server.", guild.name.clone())).await?; + } + }, + FilterAction::Delete => { + new_message.delete(&ctx.http).await?; + } + } + } + } } }, + poise::Event::InteractionCreate { interaction } => { + match interaction { + Interaction::MessageComponent(mci) => { + let data = &mci.data; + + let mci2 = mci.clone(); + + match data.custom_id.as_str() { + "approve" => { + + let msg_report = mci2.message.clone(); + let footer = msg_report.embeds[0].clone().footer.unwrap().text; + let mut footer = footer.split(':'); + let orig_channel_id = footer.next().unwrap(); + let orig_msg_id = footer.next().unwrap(); + let orig_msg_id = MessageId(orig_msg_id.parse::<u64>()?); + let orig_channel_id = ChannelId(orig_channel_id.parse::<u64>()?); + + let orig_msg = ctx.http.get_message(orig_channel_id.0, orig_msg_id.0).await?; + + + disable_buttons(&ctx, mci2).await?; + }, + "deny" => { + let msg_report = mci2.message.clone(); + let footer = msg_report.embeds[0].clone().footer.unwrap().text; + let mut footer = footer.split(':'); + let orig_channel_id = footer.next().unwrap(); + let orig_msg_id = footer.next().unwrap(); + let orig_msg_id = MessageId(orig_msg_id.parse::<u64>()?); + let orig_channel_id = ChannelId(orig_channel_id.parse::<u64>()?); + + let orig_msg = ctx.http.get_message(orig_channel_id.0, orig_msg_id.0).await?; + + orig_msg.delete(&ctx).await?; + + disable_buttons(&ctx, mci2).await?; + }, + _ => (), + }; + }, + _ => () + }; + }, _ => (), } } Ok(()) } + +async fn disable_buttons(ctx: &serenity::Context, mut mci: MessageComponentInteraction) -> Result<(), Error> { + mci.message.edit(&ctx, |m| { + m.components(|c| { + c.create_action_row(|r| { + r.create_button(|b| { + b.style(serenity::ButtonStyle::Primary); + b.label("Approve"); + b.custom_id("approve"); + b.disabled(true); + b + }); + r.create_button(|b| { + b.style(serenity::ButtonStyle::Danger); + b.label("Deny"); + b.custom_id("deny"); + b.disabled(true); + b + }); + r + }); + c + }); + m + }).await?; + + ctx.http.create_interaction_response(mci.id.0, &mci.token, &json!({ "type": 7})).await?; + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 38a849b..de322ae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,12 +5,16 @@ * This iteration will focus on code correctness and durability. The major goal is to avoid what * happened to the Campmaster by avoiding code rot and forcing documentation on _everything_ */ +#[macro_use] +extern crate tracing; use std::{sync::Mutex, time::Duration}; use dotenv::dotenv; use sqlx::{postgres::PgPoolOptions, PgPool}; use tracing::{info, instrument}; +use poise::serenity_prelude as serenity; + type Error = Box<dyn std::error::Error + Send + Sync>; type Context<'a> = poise::Context<'a, Data, Error>; @@ -43,14 +47,14 @@ async fn on_error(error: poise::FrameworkError<'_, Data, Error>) { match error { poise::FrameworkError::Setup { error } => panic!("Failed to start bot: {:?}", error), poise::FrameworkError::Command { error, ctx } => { - println!("Error in command {}: {:?}", ctx.command().name, error); - ctx.say(format!("Whoops! Something went wrong: {:?}", error)) + error!("Error in command {}: {:?}", ctx.command().name, error); + ctx.say(format!("Whoops! Something went wrong: `{:?}`", error)) .await .unwrap(); } error => { if let Err(e) = poise::builtins::on_error(error).await { - println!("Error handling error: {}", e); + error!("Error handling error: {}", e); } } } @@ -81,11 +85,7 @@ async fn main() { commands::meta::ping(), commands::meta::about(), commands::meta::userinfo(), - commands::actions::boop(), - commands::actions::hug(), - commands::pony::randpony(), - commands::pony::tpony(), - commands::pony::ponybyid(), + commands::actions::boop(), commands::actions::hug(), commands::pony::randpony(), commands::pony::tpony(), commands::pony::ponybyid(), commands::osu::osup(), commands::osu::osubm(), poise::Command { @@ -100,6 +100,8 @@ async fn main() { subcommands: vec![ commands::filters::list(), commands::filters::add(), + commands::filters::del(), + commands::filters::channel(), ], ..commands::filters::filter() }, @@ -109,12 +111,12 @@ async fn main() { // Honestly could probably be removed, but it's kept in for ~reasons~ pre_command: |ctx| { Box::pin(async move { - println!("Executing command {}...", ctx.command().name); + debug!("Executing command {}...", ctx.command().name); }) }, post_command: |ctx| { Box::pin(async move { - println!("Done executing command {}!", ctx.command().name); + debug!("Done executing command {}!", ctx.command().name); }) }, @@ -136,6 +138,9 @@ async fn main() { poise::Framework::build() .token(std::env::var("DISCORD_TOKEN").unwrap_or("BAD-TOKEN".into())) + .client_settings(|c| { + c.intents(serenity::GatewayIntents::all()) + }) .user_data_setup(move |_ctx, _ready, _framework| { Box::pin(async move { /* diff --git a/src/models.rs b/src/models.rs index b8dd5b1..5f11df6 100644 --- a/src/models.rs +++ b/src/models.rs @@ -48,6 +48,14 @@ pub struct MessageFilter { pub guild_id: String, } +#[derive(Debug, Clone)] +pub struct ConfigChannel { + pub id: i32, + pub channel_id: String, + pub guild_id: String, + pub purpose: String +} + impl FromStr for FilterAction { type Err = Error; fn from_str(s: &str) -> Result<Self, Self::Err> { @@ -72,7 +80,7 @@ impl ToString for FilterAction { impl ToString for MessageFilter { fn to_string(&self) -> String { - format!("`{}`: {}", self.pattern, self.action.to_string()) + format!("`{}`: {} ({})", self.pattern, self.action.to_string(), self.id) } } |