From 4ab4c7e00ca8a00b725cb8ee69079e04edd2b454 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sun, 28 Aug 2022 07:34:59 +0200 Subject: [PATCH] feat: media and logging channels and unban command --- configuration.example.json | 4 +- configuration.revanced.json | 4 +- configuration.schema.json | 39 +++++----- src/commands/moderation.rs | 39 ++++++---- src/events/message_create.rs | 140 ++-------------------------------- src/main.rs | 1 + src/model/application.rs | 2 + src/utils/autorespond.rs | 138 +++++++++++++++++++++++++++++++++ src/utils/bot.rs | 7 +- src/utils/media_channel.rs | 34 +++++++++ src/utils/mod.rs | 4 +- src/utils/moderation.rs | 144 +++++++++++++++++++++-------------- 12 files changed, 322 insertions(+), 234 deletions(-) create mode 100644 src/utils/autorespond.rs create mode 100644 src/utils/media_channel.rs diff --git a/configuration.example.json b/configuration.example.json index b3b96f1..459d350 100644 --- a/configuration.example.json +++ b/configuration.example.json @@ -5,7 +5,9 @@ "mute": { "role": 0, "take": [0] - } + }, + "media_channels": [0], + "logging_channel": 0 }, "administrators": { "roles": [0], diff --git a/configuration.revanced.json b/configuration.revanced.json index cf57705..987a264 100644 --- a/configuration.revanced.json +++ b/configuration.revanced.json @@ -4,7 +4,9 @@ "mute": { "role": 953984696491061289, "take": [996121272897519687, 965267139902705744, 995126555867086938] - } + }, + "media_channels": [954148665646260314], + "logging_channel": 952987428786941952 }, "administrators": { "roles": [955220417969262612], diff --git a/configuration.schema.json b/configuration.schema.json index 5cb6021..8834770 100644 --- a/configuration.schema.json +++ b/configuration.schema.json @@ -19,15 +19,17 @@ "description": "The id of the role." }, "take": { - "type": "array", - "items": { - "type": "integer" - }, - "description": "A list of role ids which will be revoked from the user.", - "minItems": 1, - "uniqueItems": true + "$ref": "#/$defs/roles" } } + }, + "media_channels": { + "$ref": "#/$defs/channels", + "description": "A list of channel ids where only media is allowed." + }, + "logging_channel": { + "type": "integer", + "description": "The id of the channel to send logs to." } } }, @@ -39,13 +41,8 @@ "description": "A list of role ids. Users with these roles have administrative privileges over this Discord bot." }, "users": { - "type": "array", - "items": { - "type": "integer" - }, - "description": "A list of user ids. Users with these ids have administrative privileges over this Discord bot.", - "minItems": 1, - "uniqueItems": true + "$ref": "#/$defs/users", + "description": "A list of user ids. Users with these ids have administrative privileges over this Discord bot." } }, "description": "The list of administrators to control the Discord bot." @@ -127,16 +124,16 @@ "type": "integer", "description": "The color of the embed." }, + "users": { + "$ref": "#/$defs/ids" + }, "roles": { - "type": "array", - "items": { - "type": "integer" - }, - "description": "A list of role ids.", - "uniqueItems": true, - "minItems": 1 + "$ref": "#/$defs/ids" }, "channels": { + "$ref": "#/$defs/ids" + }, + "ids": { "type": "array", "items": { "type": "integer" diff --git a/src/commands/moderation.rs b/src/commands/moderation.rs index 87386fd..d137810 100644 --- a/src/commands/moderation.rs +++ b/src/commands/moderation.rs @@ -32,7 +32,7 @@ pub async fn unmute( respond_moderation( &ctx, - ModerationKind::Unmute( + &ModerationKind::Unmute( queue_unmute_member( &ctx.discord().http, &data.database, @@ -44,6 +44,7 @@ pub async fn unmute( .unwrap(), ), &member.user, + &configuration, ) .await } @@ -174,8 +175,9 @@ pub async fn mute( respond_moderation( &ctx, - ModerationKind::Mute(reason, format!("", unmute_time.timestamp()), result), + &ModerationKind::Mute(reason, format!("", unmute_time.timestamp()), result), &member.user, + &configuration, ) .await } @@ -300,24 +302,35 @@ pub async fn ban( #[description = "Amount of days to delete messages"] dmd: Option, #[description = "Reason for the ban"] reason: Option, ) -> Result<(), Error> { - respond_moderation( - &ctx, - ModerationKind::Ban( - reason.clone(), - ban_moderation(&ctx, BanKind::Ban(user.clone(), dmd, reason)).await, - ), - &user, - ) - .await + handle_ban(&ctx, &BanKind::Ban(user, dmd, reason)).await } /// Unban a user. #[poise::command(slash_command)] pub async fn unban(ctx: Context<'_>, #[description = "User"] user: User) -> Result<(), Error> { + handle_ban(&ctx, &BanKind::Unban(user)).await +} + +async fn handle_ban(ctx: &Context<'_>, kind: &BanKind) -> Result<(), Error> { + let data = ctx.data().read().await; + + let ban_result = ban_moderation(&ctx, &kind).await; + + let moderated_user; respond_moderation( &ctx, - ModerationKind::Unban(ban_moderation(&ctx, BanKind::Unban(user.clone())).await), - &user, + &match kind { + BanKind::Ban(user, _, reason) => { + moderated_user = user; + ModerationKind::Ban(reason.clone(), ban_result) + }, + BanKind::Unban(user) => { + moderated_user = user; + ModerationKind::Unban(ban_result) + }, + }, + &moderated_user, + &data.configuration, ) .await } diff --git a/src/events/message_create.rs b/src/events/message_create.rs index 25526ec..05a3b36 100644 --- a/src/events/message_create.rs +++ b/src/events/message_create.rs @@ -1,140 +1,10 @@ -use chrono::{DateTime, Duration, NaiveDateTime, Utc}; -use poise::serenity_prelude::Attachment; -use regex::Regex; -use tracing::debug; - use super::*; -use crate::utils::bot::get_data_lock; -use crate::utils::ocr; - -fn contains_match(regex: &[Regex], text: &str) -> bool { - regex.iter().any(|r| r.is_match(text)) -} - -async fn attachments_contains(attachments: &[Attachment], regex: &[Regex]) -> bool { - for attachment in attachments { - debug!("Checking attachment {}", &attachment.url); - - if !&attachment.content_type.as_ref().unwrap().contains("image") { - continue; - } - - if contains_match( - regex, - &ocr::get_text_from_image_url(&attachment.url).await.unwrap(), - ) { - return true; - } - } - false -} +use crate::utils::autorespond::auto_respond; +use crate::utils::media_channel::handle_media_channel; pub async fn message_create(ctx: &serenity::Context, new_message: &serenity::Message) { - debug!("Received message: {}", new_message.content); - - if new_message.guild_id.is_none() || new_message.author.bot { - return; - } - - let data_lock = get_data_lock(ctx).await; - let responses = &data_lock.read().await.configuration.message_responses; - - for response in responses { - // check if the message was sent in a channel that is included in the responder - if !response - .includes - .channels - .iter() - .any(|&channel_id| channel_id == new_message.channel_id.0) - { - continue; - } - - let excludes = &response.excludes; - // check if the message was sent by a user that is not excluded from the responder - if excludes - .roles - .iter() - .any(|&role_id| role_id == new_message.author.id.0) - { - continue; - } - - let message = &new_message.content; - let contains_attachments = !new_message.attachments.is_empty(); - - // check if the message does not match any of the excludes - if contains_match(&excludes.match_field.text, message) { - continue; - } - - if contains_attachments - && !excludes.match_field.ocr.is_empty() - && attachments_contains(&new_message.attachments, &excludes.match_field.ocr).await - { - continue; - } - - // check if the message does match any of the includes - if !(contains_match(&response.includes.match_field.text, message) - || (contains_attachments - && !response.includes.match_field.ocr.is_empty() - && attachments_contains( - &new_message.attachments, - &response.includes.match_field.ocr, - ) - .await)) - { - continue; - } - - let min_age = response.condition.user.server_age; - - if min_age != 0 { - let joined_at = ctx - .http - .get_member(new_message.guild_id.unwrap().0, new_message.author.id.0) - .await - .unwrap() - .joined_at - .unwrap() - .unix_timestamp(); - - let must_joined_at = - DateTime::::from_utc(NaiveDateTime::from_timestamp(joined_at, 0), Utc); - let but_joined_at = Utc::now() - Duration::days(min_age); - - if must_joined_at <= but_joined_at { - return; - } - - new_message - .channel_id - .send_message(&ctx.http, |m| { - m.reference_message(new_message); - match &response.response.embed { - Some(embed) => m.embed(|e| { - e.title(&embed.title) - .description(&embed.description) - .color(embed.color) - .fields(embed.fields.iter().map(|field| { - (field.name.clone(), field.value.clone(), field.inline) - })) - .footer(|f| { - f.text(&embed.footer.text); - f.icon_url(&embed.footer.icon_url) - }) - .thumbnail(&embed.thumbnail.url) - .image(&embed.image.url) - .author(|a| { - a.name(&embed.author.name).icon_url(&embed.author.icon_url) - }) - }), - None => m.content(response.response.message.as_ref().unwrap()), - } - }) - .await - .expect("Could not reply to message author."); - } + let is_media_channel = handle_media_channel(ctx, new_message).await; + if !is_media_channel { + auto_respond(ctx, new_message).await; } } diff --git a/src/main.rs b/src/main.rs index da9611f..b53ac1e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -49,6 +49,7 @@ async fn main() { moderation::unmute(), moderation::purge(), moderation::ban(), + moderation::unban(), misc::reply(), ]; poise::set_qualified_names(&mut commands); diff --git a/src/model/application.rs b/src/model/application.rs index 61ccc6e..6e7d994 100644 --- a/src/model/application.rs +++ b/src/model/application.rs @@ -67,6 +67,8 @@ impl Configuration { pub struct General { pub embed_color: i32, pub mute: Mute, + pub media_channels: Vec, + pub logging_channel: u64, } #[derive(Default, Serialize, Deserialize)] diff --git a/src/utils/autorespond.rs b/src/utils/autorespond.rs new file mode 100644 index 0000000..34f63c7 --- /dev/null +++ b/src/utils/autorespond.rs @@ -0,0 +1,138 @@ +use chrono::{DateTime, Duration, NaiveDateTime, Utc}; +use poise::serenity_prelude::Attachment; +use regex::Regex; +use tracing::debug; + +use super::*; +use crate::utils::bot::get_data_lock; +use crate::utils::ocr; + +fn contains_match(regex: &[Regex], text: &str) -> bool { + regex.iter().any(|r| r.is_match(text)) +} + +async fn attachments_contains(attachments: &[Attachment], regex: &[Regex]) -> bool { + for attachment in attachments { + debug!("Checking attachment {}", &attachment.url); + + if !&attachment.content_type.as_ref().unwrap().contains("image") { + continue; + } + + if contains_match( + regex, + &ocr::get_text_from_image_url(&attachment.url).await.unwrap(), + ) { + return true; + } + } + false +} + +pub async fn auto_respond(ctx: &serenity::Context, new_message: &serenity::Message) { + if new_message.guild_id.is_none() || new_message.author.bot { + return; + } + + let data_lock = get_data_lock(ctx).await; + let responses = &data_lock.read().await.configuration.message_responses; + + for response in responses { + // check if the message was sent in a channel that is included in the responder + if !response + .includes + .channels + .iter() + .any(|&channel_id| channel_id == new_message.channel_id.0) + { + continue; + } + + let excludes = &response.excludes; + // check if the message was sent by a user that is not excluded from the responder + if excludes + .roles + .iter() + .any(|&role_id| role_id == new_message.author.id.0) + { + continue; + } + + let message = &new_message.content; + let contains_attachments = !new_message.attachments.is_empty(); + + // check if the message does not match any of the excludes + if contains_match(&excludes.match_field.text, message) { + continue; + } + + if contains_attachments + && !excludes.match_field.ocr.is_empty() + && attachments_contains(&new_message.attachments, &excludes.match_field.ocr).await + { + continue; + } + + // check if the message does match any of the includes + if !(contains_match(&response.includes.match_field.text, message) + || (contains_attachments + && !response.includes.match_field.ocr.is_empty() + && attachments_contains( + &new_message.attachments, + &response.includes.match_field.ocr, + ) + .await)) + { + continue; + } + + let min_age = response.condition.user.server_age; + + if min_age != 0 { + let joined_at = ctx + .http + .get_member(new_message.guild_id.unwrap().0, new_message.author.id.0) + .await + .unwrap() + .joined_at + .unwrap() + .unix_timestamp(); + + let must_joined_at = + DateTime::::from_utc(NaiveDateTime::from_timestamp(joined_at, 0), Utc); + let but_joined_at = Utc::now() - Duration::days(min_age); + + if must_joined_at <= but_joined_at { + return; + } + + new_message + .channel_id + .send_message(&ctx.http, |m| { + m.reference_message(new_message); + match &response.response.embed { + Some(embed) => m.embed(|e| { + e.title(&embed.title) + .description(&embed.description) + .color(embed.color) + .fields(embed.fields.iter().map(|field| { + (field.name.clone(), field.value.clone(), field.inline) + })) + .footer(|f| { + f.text(&embed.footer.text); + f.icon_url(&embed.footer.icon_url) + }) + .thumbnail(&embed.thumbnail.url) + .image(&embed.image.url) + .author(|a| { + a.name(&embed.author.name).icon_url(&embed.author.icon_url) + }) + }), + None => m.content(response.response.message.as_ref().unwrap()), + } + }) + .await + .expect("Could not reply to message author."); + } + } +} diff --git a/src/utils/bot.rs b/src/utils/bot.rs index b7010cf..322c080 100644 --- a/src/utils/bot.rs +++ b/src/utils/bot.rs @@ -11,10 +11,5 @@ pub fn load_configuration() -> Configuration { // Share the lock reference between the threads in serenity framework pub async fn get_data_lock(ctx: &serenity::Context) -> Arc> { - ctx.data - .read() - .await - .get::() - .expect("Expected Configuration in TypeMap.") - .clone() + ctx.data.read().await.get::().unwrap().clone() } diff --git a/src/utils/media_channel.rs b/src/utils/media_channel.rs new file mode 100644 index 0000000..90d28cd --- /dev/null +++ b/src/utils/media_channel.rs @@ -0,0 +1,34 @@ +use tracing::error; + +use super::bot::get_data_lock; +use super::*; + +pub async fn handle_media_channel( + ctx: &serenity::Context, + new_message: &serenity::Message, +) -> bool { + let current_channel = new_message.channel_id.0; + + let data_lock = get_data_lock(ctx).await; + + let configuration = &data_lock.read().await.configuration; + + let is_media_channel = configuration + .general + .media_channels + .iter() + .any(|&channel| channel == current_channel); + + if !configuration + .administrators + .users + .contains(&new_message.author.id.0) + && is_media_channel + { + if let Err(why) = new_message.delete(&ctx.http).await { + error!("Error deleting message: {:?}", why); + } + } + + is_media_channel +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 8bd34b6..49fd6fc 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -4,4 +4,6 @@ pub mod bot; pub mod decancer; pub mod embed; pub mod moderation; -pub mod ocr; \ No newline at end of file +pub mod ocr; +pub mod autorespond; +pub mod media_channel; \ No newline at end of file diff --git a/src/utils/moderation.rs b/src/utils/moderation.rs index 8ccabe0..61a507e 100644 --- a/src/utils/moderation.rs +++ b/src/utils/moderation.rs @@ -2,7 +2,7 @@ use std::cmp; use std::sync::Arc; use mongodb::options::FindOptions; -use poise::serenity_prelude::{Http, User}; +use poise::serenity_prelude::{ChannelId, Http, User}; use tokio::task::JoinHandle; use tracing::{debug, error, trace}; @@ -10,8 +10,10 @@ use super::bot::get_data_lock; use super::*; use crate::db::database::Database; use crate::db::model::Muted; +use crate::model::application::Configuration; use crate::serenity::SerenityError; use crate::{Context, Error}; + pub enum ModerationKind { Mute(String, String, Option), // Reason, Expires, Error Unmute(Option), // Error @@ -112,84 +114,114 @@ pub fn queue_unmute_member( } // TODO: refactor -pub async fn respond_moderation( +pub async fn respond_moderation<'a>( ctx: &Context<'_>, - moderation: ModerationKind, + moderation: &ModerationKind, user: &serenity::User, + configuration: &Configuration, ) -> Result<(), Error> { - let tag = user.tag(); - let image = user - .avatar_url() - .unwrap_or_else(|| user.default_avatar_url()); - - let embed_color = ctx.data().read().await.configuration.general.embed_color; - - ctx.send(|f| { - f.embed(|f| { - match moderation { - ModerationKind::Mute(reason, expires, error) => match error { - Some(err) => f.title(format!("Failed to mute {}", tag)).field( + let create_embed = |f: &mut serenity::CreateEmbed| { + let tag = user.tag(); + match moderation { + ModerationKind::Mute(reason, expires, error) => match error { + Some(err) => f.title(format!("Failed to mute {}", tag)).field( + "Exception", + err.to_string(), + false, + ), + None => f.title(format!("Muted {}", tag)), + } + .field("Reason", reason, false) + .field("Expires", expires, false), + ModerationKind::Unmute(error) => match error { + Some(err) => f.title(format!("Failed to unmute {}", tag)).field( + "Exception", + err.to_string(), + false, + ), + None => f.title(format!("Unmuted {}", tag)), + }, + ModerationKind::Ban(reason, error) => { + let f = match error { + Some(err) => f.title(format!("Failed to ban {}", tag)).field( "Exception", err.to_string(), false, ), - None => f.title(format!("Muted {}", tag)), + None => f.title(format!("Banned {}", tag)), + }; + if let Some(reason) = reason { + f.field("Reason", reason, false) + } else { + f } - .field("Reason", reason, false) - .field("Expires", expires, false), - ModerationKind::Unmute(error) => match error { - Some(err) => f.title(format!("Failed to unmute {}", tag)).field( - "Exception", - err.to_string(), - false, - ), - None => f.title(format!("Unmuted {}", tag)), - }, - ModerationKind::Ban(reason, error) => { - let f = match error { - Some(err) => f.title(format!("Failed to ban {}", tag)).field( - "Exception", - err.to_string(), - false, - ), - None => f.title(format!("Banned {}", tag)), - }; - if let Some(reason) = reason { - f.field("Reason", reason, false) - } else { - f - } - }, - ModerationKind::Unban(error) => match error { - Some(err) => f.title(format!("Failed to unban {}", tag)).field( - "Exception", - err.to_string(), - false, + }, + ModerationKind::Unban(error) => match error { + Some(err) => f.title(format!("Failed to unban {}", tag)).field( + "Exception", + err.to_string(), + false, + ), + None => f.title(format!("Unbanned {}", tag)), + }, + } + .color(configuration.general.embed_color) + .thumbnail( + &user + .avatar_url() + .unwrap_or_else(|| user.default_avatar_url()), + ); + }; + + let reply = ctx + .send(|reply| { + reply.embed(|embed| { + create_embed(embed); + embed + }) + }) + .await?; + + let response = reply.message().await?; + ChannelId(configuration.general.logging_channel) + .send_message(&ctx.discord().http, |reply| { + reply.embed(|embed| { + create_embed(embed); + embed.field( + "Reference", + format!( + "[Jump to message](https://discord.com/channels/{}/{}/{})", + ctx.guild_id().unwrap().0, + response.channel_id, + response.id ), - None => f.title(format!("Unbanned {}", tag)), - }, - } - .color(embed_color) - .thumbnail(image) + false, + ) + }) }) - }) - .await?; + .await?; Ok(()) } -pub async fn ban_moderation(ctx: &Context<'_>, kind: BanKind) -> Option { +pub async fn ban_moderation(ctx: &Context<'_>, kind: &BanKind) -> Option { let guild_id = ctx.guild_id().unwrap().0; let http = &ctx.discord().http; match kind { BanKind::Ban(user, dmd, reason) => { - let reason = &reason + let reason = reason + .clone() .or_else(|| Some("None specified".to_string())) .unwrap(); let ban_result = http - .ban_user(guild_id, user.id.0, cmp::min(dmd.unwrap_or(0), 7), reason) + .ban_user( + guild_id, + user.id.0, + cmp::min(dmd.unwrap_or(0), 7), + reason.as_ref(), + ) .await; if let Err(err) = ban_result {