From 5999e6a803a7b848acf054918fec9ee5024d5697 Mon Sep 17 00:00:00 2001 From: Cara Salter Date: Tue, 3 May 2022 13:57:09 -0400 Subject: filter: Initial message filter implementation Also a custom error type, tracing_subscriber, and unsafe impls --- src/commands/filters.rs | 82 +++++++++++++++++++++++++++++++++++++++++++++++++ src/commands/mod.rs | 1 + src/errors.rs | 21 +++++++++++++ src/handler.rs | 17 ++++++++-- src/main.rs | 11 ++++++- src/models.rs | 73 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 202 insertions(+), 3 deletions(-) create mode 100644 src/commands/filters.rs create mode 100644 src/errors.rs (limited to 'src') diff --git a/src/commands/filters.rs b/src/commands/filters.rs new file mode 100644 index 0000000..35fefac --- /dev/null +++ b/src/commands/filters.rs @@ -0,0 +1,82 @@ +use crate::{Context, Error, models::{FilterAction, MessageFilter}}; +use poise::{serenity_prelude as serenity, AutocompleteChoice}; + +use std::str::FromStr; + +use futures::{Stream, StreamExt}; + +/// Base command for all filter actions +/// +/// Provides a CRUD interface for managing automatic message filters +#[poise::command(slash_command, prefix_command)] +pub async fn filter(ctx: Context<'_>) -> Result<(), Error> { + Ok(()) +} + +/// Lists message filters +/// +/// Usage: +/// filter list +#[poise::command(slash_command, ephemeral, prefix_command)] +pub async fn list(ctx: Context<'_>) -> Result<(), Error> { + let pool = ctx.data().pg.lock().unwrap().clone(); + + let filters = sqlx::query_as::<_, MessageFilter>("SELECT * FROM message_filter WHERE guild_id=$1").bind(ctx.guild().unwrap().id.0.to_string()) + .fetch_all(&pool).await?; + + let mut list = String::from(""); + 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())); + } + } + + ctx.send(|m| { + m.embed(|e| { + e.title("Message Filter List"); + e.description(list); + e + }) + }).await?; + Ok(()) +} + +/// Creates a new message filter +/// +/// Usage: +/// filter add +/// +/// Where is one of "review" or "delete" +#[poise::command(slash_command, ephemeral, prefix_command)] +pub async fn add(ctx: Context<'_>, + #[description = "The regular expression to match against"] + regex: String, + #[description = "The action to take when the expression is matched"] + #[autocomplete = "ac_action"] + action: String, + ) -> Result<(), Error> { + + let pool = ctx.data().pg.lock().unwrap().clone(); + + let action = FilterAction::from_str(&action)?; + + sqlx::query("INSERT INTO message_filter (pattern, action, guild_id) VALUES ($1, $2, $3)") + .bind(regex) + .bind(action as FilterAction) + .bind(ctx.guild().unwrap().id.0.to_string()) + .execute(&pool) + .await?; + + ctx.say("Sounds good! I've written that down and will keep a close eye out").await?; + + Ok(()) +} + + +async fn ac_action(_ctx: Context<'_>, partial: String) -> impl Stream { + futures::stream::iter(&["review", "delete"]) + .filter(move |name| futures::future::ready(name.starts_with(&partial))) + .map(|name| name.to_string()) +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 3ffb136..2ce5d6a 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -3,3 +3,4 @@ pub mod meta; pub mod osu; pub mod pony; pub mod reactionroles; +pub mod filters; diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..6e78011 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,21 @@ +use poise::FrameworkError; + + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Serenity: {0}")] + Serenity(#[from] serenity::prelude::SerenityError), + + #[error("SQL: {0}")] + Sql(#[from] sqlx::Error), + + #[error("Unknown: {0}")] + Unknown(String), + + #[error("Catch-all: {0}")] + CatchAll(#[from] Box), +} + +unsafe impl Send for Error { } +unsafe impl Sync for Error { } + diff --git a/src/handler.rs b/src/handler.rs index 7b4568f..a52b31a 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,6 +1,6 @@ use poise::serenity_prelude as serenity; -use crate::models::ReactionRole; +use crate::models::*; use crate::{Data, Error}; use tracing::info; @@ -72,7 +72,20 @@ pub async fn event_handler( } add_reaction.delete(&ctx.http).await?; - } + }, + poise::Event::Message { new_message } => { + let current_user = ctx.http.get_current_user().await?; + + 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?; + } + }, _ => (), } } diff --git a/src/main.rs b/src/main.rs index 88d612e..38a849b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -#![forbid(missing_docs)] +#![deny(missing_docs)] /*! * Full rewrite of the [Glitch](https://glitchbot.net) bot in Poise with slash commands * @@ -17,6 +17,7 @@ type Context<'a> = poise::Context<'a, Data, Error>; mod commands; mod handler; mod models; +mod errors; /// Contains data shared between all commands pub struct Data { @@ -70,6 +71,7 @@ async fn register(ctx: Context<'_>, #[flag] global: bool) -> Result<(), Error> { async fn main() { // Initialize environment and logging dotenv().unwrap(); + color_eyre::install().unwrap(); tracing_subscriber::fmt::init(); info!("Initialized logging"); let options = poise::FrameworkOptions { @@ -94,6 +96,13 @@ async fn main() { ], ..commands::reactionroles::rroles() }, + poise::Command { + subcommands: vec![ + commands::filters::list(), + commands::filters::add(), + ], + ..commands::filters::filter() + }, ], // This requires a closure, for some reason on_error: |error| Box::pin(on_error(error)), diff --git a/src/models.rs b/src/models.rs index 09d5d7c..b8dd5b1 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,3 +1,9 @@ +use std::str::FromStr; + +use poise::AutocompleteChoice; + +use crate::errors::Error; + /** * Describes a Reaction Role as it appears in SQL */ @@ -17,3 +23,70 @@ pub struct ReactionRole { /// The ID of the role to be toggled by the menu option pub role_id: String, } + + +#[derive(Debug, Clone, sqlx::Type)] +#[sqlx(type_name="filter_action", rename_all="lowercase")] +pub enum FilterAction { + Review, + Delete +} +/** + * Describes a message filter as it appears in SQL + */ + +#[derive(Debug, Clone, sqlx::FromRow)] +pub struct MessageFilter { + /// Primary Key + pub id: i32, + + /// Pattern + pub pattern: String, + + pub action: FilterAction, + + pub guild_id: String, +} + +impl FromStr for FilterAction { + type Err = Error; + fn from_str(s: &str) -> Result { + match s { + "review" => Ok(FilterAction::Review), + "delete" => Ok(FilterAction::Delete), + _ => { + return Err(Error::Unknown("invalid option".to_string())); + } + } + } +} + +impl ToString for FilterAction { + fn to_string(&self) -> String { + match self { + FilterAction::Review => "Review".to_string(), + FilterAction::Delete => "Delete".to_string(), + } + } +} + +impl ToString for MessageFilter { + fn to_string(&self) -> String { + format!("`{}`: {}", self.pattern, self.action.to_string()) + } +} + +impl From for AutocompleteChoice { + fn from(m: FilterAction) -> Self { + match m { + FilterAction::Review => AutocompleteChoice { + name: "Flag the message for review and send it to a channel for moderators".to_string(), + value: "review".to_string() + }, + FilterAction::Delete => AutocompleteChoice { + name: "Deletes the message and logs the deletion in a channel for moderators".to_string(), + value: "delete".to_string() + } + } + } +} -- cgit v1.2.3