aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/commands/filters.rs79
-rw-r--r--src/handler.rs139
-rw-r--r--src/main.rs25
-rw-r--r--src/models.rs10
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(&regex)?;
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)
}
}