aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorCara Salter <cara@devcara.com>2022-05-03 13:57:09 -0400
committerCara Salter <cara@devcara.com>2022-05-03 13:57:09 -0400
commit5999e6a803a7b848acf054918fec9ee5024d5697 (patch)
treeeae1f3986c41f7d7c1a956e4606a8120c6032e05 /src
parent8f4277c55a2079edf1c9a69383c353e1cb9ef55c (diff)
downloadglitch-ng-5999e6a803a7b848acf054918fec9ee5024d5697.tar.gz
glitch-ng-5999e6a803a7b848acf054918fec9ee5024d5697.zip
filter: Initial message filter implementation
Also a custom error type, tracing_subscriber, and unsafe impls
Diffstat (limited to 'src')
-rw-r--r--src/commands/filters.rs82
-rw-r--r--src/commands/mod.rs1
-rw-r--r--src/errors.rs21
-rw-r--r--src/handler.rs17
-rw-r--r--src/main.rs11
-rw-r--r--src/models.rs73
6 files changed, 202 insertions, 3 deletions
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 <pattern> <action>
+///
+/// Where <action> 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<Item = String> {
+ 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<dyn std::error::Error>),
+}
+
+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<Self, Self::Err> {
+ 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<FilterAction> for AutocompleteChoice<String> {
+ 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()
+ }
+ }
+ }
+}