feat: media and logging channels and unban command

This commit is contained in:
oSumAtrIX 2022-08-28 07:34:59 +02:00
parent b7d333c86c
commit 4ab4c7e00c
No known key found for this signature in database
GPG Key ID: A9B3094ACDB604B4
12 changed files with 327 additions and 239 deletions

View File

@ -5,7 +5,9 @@
"mute": { "mute": {
"role": 0, "role": 0,
"take": [0] "take": [0]
} },
"media_channels": [0],
"logging_channel": 0
}, },
"administrators": { "administrators": {
"roles": [0], "roles": [0],

View File

@ -4,7 +4,9 @@
"mute": { "mute": {
"role": 953984696491061289, "role": 953984696491061289,
"take": [996121272897519687, 965267139902705744, 995126555867086938] "take": [996121272897519687, 965267139902705744, 995126555867086938]
} },
"media_channels": [954148665646260314],
"logging_channel": 952987428786941952
}, },
"administrators": { "administrators": {
"roles": [955220417969262612], "roles": [955220417969262612],

View File

@ -19,15 +19,17 @@
"description": "The id of the role." "description": "The id of the role."
}, },
"take": { "take": {
"type": "array", "$ref": "#/$defs/roles"
"items": { }
"type": "integer" }
}, },
"description": "A list of role ids which will be revoked from the user.", "media_channels": {
"minItems": 1, "$ref": "#/$defs/channels",
"uniqueItems": true "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." "description": "A list of role ids. Users with these roles have administrative privileges over this Discord bot."
}, },
"users": { "users": {
"type": "array", "$ref": "#/$defs/users",
"items": { "description": "A list of user ids. Users with these ids have administrative privileges over this Discord bot."
"type": "integer"
},
"description": "A list of user ids. Users with these ids have administrative privileges over this Discord bot.",
"minItems": 1,
"uniqueItems": true
} }
}, },
"description": "The list of administrators to control the Discord bot." "description": "The list of administrators to control the Discord bot."
@ -127,16 +124,16 @@
"type": "integer", "type": "integer",
"description": "The color of the embed." "description": "The color of the embed."
}, },
"roles": { "users": {
"type": "array", "$ref": "#/$defs/ids"
"items": {
"type": "integer"
}, },
"description": "A list of role ids.", "roles": {
"uniqueItems": true, "$ref": "#/$defs/ids"
"minItems": 1
}, },
"channels": { "channels": {
"$ref": "#/$defs/ids"
},
"ids": {
"type": "array", "type": "array",
"items": { "items": {
"type": "integer" "type": "integer"

View File

@ -32,7 +32,7 @@ pub async fn unmute(
respond_moderation( respond_moderation(
&ctx, &ctx,
ModerationKind::Unmute( &ModerationKind::Unmute(
queue_unmute_member( queue_unmute_member(
&ctx.discord().http, &ctx.discord().http,
&data.database, &data.database,
@ -44,6 +44,7 @@ pub async fn unmute(
.unwrap(), .unwrap(),
), ),
&member.user, &member.user,
&configuration,
) )
.await .await
} }
@ -174,8 +175,9 @@ pub async fn mute(
respond_moderation( respond_moderation(
&ctx, &ctx,
ModerationKind::Mute(reason, format!("<t:{}:F>", unmute_time.timestamp()), result), &ModerationKind::Mute(reason, format!("<t:{}:F>", unmute_time.timestamp()), result),
&member.user, &member.user,
&configuration,
) )
.await .await
} }
@ -300,24 +302,35 @@ pub async fn ban(
#[description = "Amount of days to delete messages"] dmd: Option<u8>, #[description = "Amount of days to delete messages"] dmd: Option<u8>,
#[description = "Reason for the ban"] reason: Option<String>, #[description = "Reason for the ban"] reason: Option<String>,
) -> Result<(), Error> { ) -> Result<(), Error> {
respond_moderation( handle_ban(&ctx, &BanKind::Ban(user, dmd, reason)).await
&ctx,
ModerationKind::Ban(
reason.clone(),
ban_moderation(&ctx, BanKind::Ban(user.clone(), dmd, reason)).await,
),
&user,
)
.await
} }
/// Unban a user. /// Unban a user.
#[poise::command(slash_command)] #[poise::command(slash_command)]
pub async fn unban(ctx: Context<'_>, #[description = "User"] user: User) -> Result<(), Error> { 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( respond_moderation(
&ctx, &ctx,
ModerationKind::Unban(ban_moderation(&ctx, BanKind::Unban(user.clone())).await), &match kind {
&user, 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 .await
} }

View File

@ -1,140 +1,10 @@
use chrono::{DateTime, Duration, NaiveDateTime, Utc};
use poise::serenity_prelude::Attachment;
use regex::Regex;
use tracing::debug;
use super::*; use super::*;
use crate::utils::bot::get_data_lock; use crate::utils::autorespond::auto_respond;
use crate::utils::ocr; use crate::utils::media_channel::handle_media_channel;
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 message_create(ctx: &serenity::Context, new_message: &serenity::Message) { pub async fn message_create(ctx: &serenity::Context, new_message: &serenity::Message) {
debug!("Received message: {}", new_message.content); let is_media_channel = handle_media_channel(ctx, new_message).await;
if !is_media_channel {
if new_message.guild_id.is_none() || new_message.author.bot { auto_respond(ctx, new_message).await;
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::<Utc>::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.");
}
} }
} }

View File

@ -49,6 +49,7 @@ async fn main() {
moderation::unmute(), moderation::unmute(),
moderation::purge(), moderation::purge(),
moderation::ban(), moderation::ban(),
moderation::unban(),
misc::reply(), misc::reply(),
]; ];
poise::set_qualified_names(&mut commands); poise::set_qualified_names(&mut commands);

View File

@ -67,6 +67,8 @@ impl Configuration {
pub struct General { pub struct General {
pub embed_color: i32, pub embed_color: i32,
pub mute: Mute, pub mute: Mute,
pub media_channels: Vec<u64>,
pub logging_channel: u64,
} }
#[derive(Default, Serialize, Deserialize)] #[derive(Default, Serialize, Deserialize)]

138
src/utils/autorespond.rs Normal file
View File

@ -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::<Utc>::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.");
}
}
}

View File

@ -11,10 +11,5 @@ pub fn load_configuration() -> Configuration {
// Share the lock reference between the threads in serenity framework // Share the lock reference between the threads in serenity framework
pub async fn get_data_lock(ctx: &serenity::Context) -> Arc<RwLock<Data>> { pub async fn get_data_lock(ctx: &serenity::Context) -> Arc<RwLock<Data>> {
ctx.data ctx.data.read().await.get::<Data>().unwrap().clone()
.read()
.await
.get::<Data>()
.expect("Expected Configuration in TypeMap.")
.clone()
} }

View File

@ -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
}

View File

@ -5,3 +5,5 @@ pub mod decancer;
pub mod embed; pub mod embed;
pub mod moderation; pub mod moderation;
pub mod ocr; pub mod ocr;
pub mod autorespond;
pub mod media_channel;

View File

@ -2,7 +2,7 @@ use std::cmp;
use std::sync::Arc; use std::sync::Arc;
use mongodb::options::FindOptions; use mongodb::options::FindOptions;
use poise::serenity_prelude::{Http, User}; use poise::serenity_prelude::{ChannelId, Http, User};
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use tracing::{debug, error, trace}; use tracing::{debug, error, trace};
@ -10,8 +10,10 @@ use super::bot::get_data_lock;
use super::*; use super::*;
use crate::db::database::Database; use crate::db::database::Database;
use crate::db::model::Muted; use crate::db::model::Muted;
use crate::model::application::Configuration;
use crate::serenity::SerenityError; use crate::serenity::SerenityError;
use crate::{Context, Error}; use crate::{Context, Error};
pub enum ModerationKind { pub enum ModerationKind {
Mute(String, String, Option<Error>), // Reason, Expires, Error Mute(String, String, Option<Error>), // Reason, Expires, Error
Unmute(Option<Error>), // Error Unmute(Option<Error>), // Error
@ -112,20 +114,14 @@ pub fn queue_unmute_member(
} }
// TODO: refactor // TODO: refactor
pub async fn respond_moderation( pub async fn respond_moderation<'a>(
ctx: &Context<'_>, ctx: &Context<'_>,
moderation: ModerationKind, moderation: &ModerationKind,
user: &serenity::User, user: &serenity::User,
configuration: &Configuration,
) -> Result<(), Error> { ) -> Result<(), Error> {
let create_embed = |f: &mut serenity::CreateEmbed| {
let tag = user.tag(); 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 { match moderation {
ModerationKind::Mute(reason, expires, error) => match error { ModerationKind::Mute(reason, expires, error) => match error {
Some(err) => f.title(format!("Failed to mute {}", tag)).field( Some(err) => f.title(format!("Failed to mute {}", tag)).field(
@ -169,8 +165,38 @@ pub async fn respond_moderation(
None => f.title(format!("Unbanned {}", tag)), None => f.title(format!("Unbanned {}", tag)),
}, },
} }
.color(embed_color) .color(configuration.general.embed_color)
.thumbnail(image) .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
),
false,
)
}) })
}) })
.await?; .await?;
@ -178,18 +204,24 @@ pub async fn respond_moderation(
Ok(()) Ok(())
} }
pub async fn ban_moderation(ctx: &Context<'_>, kind: BanKind) -> Option<SerenityError> { pub async fn ban_moderation(ctx: &Context<'_>, kind: &BanKind) -> Option<SerenityError> {
let guild_id = ctx.guild_id().unwrap().0; let guild_id = ctx.guild_id().unwrap().0;
let http = &ctx.discord().http; let http = &ctx.discord().http;
match kind { match kind {
BanKind::Ban(user, dmd, reason) => { BanKind::Ban(user, dmd, reason) => {
let reason = &reason let reason = reason
.clone()
.or_else(|| Some("None specified".to_string())) .or_else(|| Some("None specified".to_string()))
.unwrap(); .unwrap();
let ban_result = http 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; .await;
if let Err(err) = ban_result { if let Err(err) = ban_result {