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 --- Cargo.lock | 167 +++++++++++++++++++++++- Cargo.toml | 7 + flake.lock | 17 +++ flake.nix | 22 +++- migrations/20220429210913_message_filter.sql | 8 ++ migrations/20220429214038_guild_ids_filters.sql | 2 + 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 +++++++++++ 12 files changed, 420 insertions(+), 8 deletions(-) create mode 100644 migrations/20220429210913_message_filter.sql create mode 100644 migrations/20220429214038_guild_ids_filters.sql create mode 100644 src/commands/filters.rs create mode 100644 src/errors.rs diff --git a/Cargo.lock b/Cargo.lock index 791625a..15c782d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -19,6 +28,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -70,6 +88,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11a17d453482a265fd5f8479f2a3f405566e6ca627837aaddb85af8b1ab8ef61" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide 0.5.1", + "object", + "rustc-demangle", +] + [[package]] name = "base64" version = "0.13.0" @@ -135,6 +168,33 @@ dependencies = [ "winapi", ] +[[package]] +name = "color-eyre" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ebf286c900a6d5867aeff75cfee3192857bb7f24b547d4f0df2ed6baa812c90" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba75b3d9449ecdccb27ecbc479fdc0b87fa2dd43d2f8298f9bf0e59aacc8dce" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -323,6 +383,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "eyre" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" +dependencies = [ + "indenter", + "once_cell", +] + [[package]] name = "fastrand" version = "1.7.0" @@ -341,7 +411,7 @@ dependencies = [ "cfg-if", "crc32fast", "libc", - "miniz_oxide", + "miniz_oxide 0.4.4", ] [[package]] @@ -383,6 +453,7 @@ checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -405,6 +476,17 @@ version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" +[[package]] +name = "futures-executor" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-intrusive" version = "0.4.0" @@ -422,6 +504,17 @@ version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" +[[package]] +name = "futures-macro" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.21" @@ -443,6 +536,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -472,23 +566,33 @@ dependencies = [ "wasi", ] +[[package]] +name = "gimli" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" + [[package]] name = "glitch-ng" version = "0.6.0" dependencies = [ "chrono", + "color-eyre", "dotenv", + "futures", "log", "poise", "rand", + "regex", "reqwest", "serde", "serde_json", "serenity", "sqlx", + "thiserror", "tokio", "tracing", - "tracing-subscriber", + "tracing-subscriber 0.2.25", ] [[package]] @@ -663,6 +767,12 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + [[package]] name = "indexmap" version = "1.8.0" @@ -806,6 +916,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "miniz_oxide" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082" +dependencies = [ + "adler", +] + [[package]] name = "mio" version = "0.7.14" @@ -895,6 +1014,15 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40bec70ba014595f99f7aa110b84331ffe1ee9aece7fe6f387cc7e3ecda4d456" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.9.0" @@ -940,6 +1068,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "owo-colors" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "decf7381921fea4dcb2549c5667eda59b3ec297ab7e2b5fc33eac69d2e7da87b" + [[package]] name = "parking_lot" version = "0.11.2" @@ -1094,6 +1228,8 @@ version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" dependencies = [ + "aho-corasick", + "memchr", "regex-syntax", ] @@ -1179,6 +1315,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + [[package]] name = "rustls" version = "0.19.1" @@ -1756,6 +1898,16 @@ dependencies = [ "valuable", ] +[[package]] +name = "tracing-error" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" +dependencies = [ + "tracing", + "tracing-subscriber 0.3.11", +] + [[package]] name = "tracing-log" version = "0.1.2" @@ -1799,6 +1951,17 @@ dependencies = [ "tracing-serde", ] +[[package]] +name = "tracing-subscriber" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bc28f93baff38037f64e6f43d34cfa1605f27a49c34e8a04c5e78b0babf2596" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", +] + [[package]] name = "try-lock" version = "0.2.3" diff --git a/Cargo.toml b/Cargo.toml index 88342e4..a21e76d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,11 +12,18 @@ log = "0.4" tracing = "0.1" tracing-subscriber = "0.2" +thiserror = "1" +color-eyre = "0.6" + rand = "0.8" reqwest = "0.11" chrono = { version = "0.4", features = ["serde"] } +regex = "1" + +futures = "0.3" + [dependencies.serenity] git = "https://github.com/serenity-rs/serenity" branch = "next" diff --git a/flake.lock b/flake.lock index ce6187f..90c5af7 100644 --- a/flake.lock +++ b/flake.lock @@ -15,6 +15,22 @@ "type": "github" } }, + "mozpkgs": { + "flake": false, + "locked": { + "lastModified": 1650459918, + "narHash": "sha256-sroCK+QJTmoXtcRkwZyKOP9iAYOPID2Bwdxn4GkG16w=", + "owner": "mozilla", + "repo": "nixpkgs-mozilla", + "rev": "e1f7540fc0a8b989fb8cf701dc4fd7fc76bcf168", + "type": "github" + }, + "original": { + "owner": "mozilla", + "repo": "nixpkgs-mozilla", + "type": "github" + } + }, "naersk": { "inputs": { "nixpkgs": "nixpkgs" @@ -64,6 +80,7 @@ "root": { "inputs": { "flake-utils": "flake-utils", + "mozpkgs": "mozpkgs", "naersk": "naersk", "nixpkgs": "nixpkgs_2" } diff --git a/flake.nix b/flake.nix index ceb4ddb..d9c9ba5 100644 --- a/flake.nix +++ b/flake.nix @@ -2,13 +2,29 @@ inputs = { flake-utils.url = "github:numtide/flake-utils"; naersk.url = "github:nix-community/naersk"; + + mozpkgs = { + url = "github:mozilla/nixpkgs-mozilla"; + flake = false; + }; }; - outputs = { self, nixpkgs, flake-utils, naersk }: + outputs = { self, nixpkgs, flake-utils, naersk, mozpkgs }: flake-utils.lib.eachDefaultSystem ( system: let pkgs = nixpkgs.legacyPackages."${system}"; - naersk-lib = naersk.lib."${system}"; + + mozilla = pkgs.callPackage (mozpkgs + "/package-set.nix") {}; + rust = (mozilla.rustChannelOf { + date = "2022-04-30"; + channel = "nightly"; + sha256= "pKjVkFhROJV0+JZKx2n4Fn9fJFuGX8pZW3LjUAN+Jx0="; + }).rust; + + naersk-lib = naersk.lib."${system}".override { + cargo = rust; + rustc = rust; + }; in rec { # `nix build` @@ -95,7 +111,7 @@ # `nix develop` devShell = pkgs.mkShell { - nativeBuildInputs = with pkgs; [ rustc cargo ] ++ deps; + nativeBuildInputs = with pkgs; [ rust ] ++ deps; }; } ); diff --git a/migrations/20220429210913_message_filter.sql b/migrations/20220429210913_message_filter.sql new file mode 100644 index 0000000..95b7844 --- /dev/null +++ b/migrations/20220429210913_message_filter.sql @@ -0,0 +1,8 @@ +-- Add migration script here + +CREATE TYPE filter_action AS ENUM ( 'delete', 'review' ); +CREATE TABLE message_filter( + id SERIAL PRIMARY KEY, + pattern TEXT NOT NULL, + action filter_action NOT NULL DEFAULT 'review' +); diff --git a/migrations/20220429214038_guild_ids_filters.sql b/migrations/20220429214038_guild_ids_filters.sql new file mode 100644 index 0000000..302b809 --- /dev/null +++ b/migrations/20220429214038_guild_ids_filters.sql @@ -0,0 +1,2 @@ +-- Add migration script here +ALTER TABLE message_filter ADD COLUMN guild_id TEXT NOT NULL; 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