Add automated warning and banning
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
use crate::{framework::interactions::components::Button, state::State};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CodeOfConductButton;
|
||||
|
||||
impl Button<State> for CodeOfConductButton {
|
||||
fn label(&self) -> Option<&'static str> {
|
||||
Some("Code of Conduct")
|
||||
}
|
||||
|
||||
fn url(&self) -> Option<&'static str> {
|
||||
Some("https://payphone.lat/code-of-conduct")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NeedHelpButton;
|
||||
|
||||
impl Button<State> for NeedHelpButton {
|
||||
fn label(&self) -> Option<&'static str> {
|
||||
Some("Need Help?")
|
||||
}
|
||||
|
||||
fn url(&self) -> Option<&'static str> {
|
||||
Some("https://discord.gg/9daSsrfTmE")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use twilight_model::{
|
||||
channel::{Message, message::Embed},
|
||||
oauth::Application,
|
||||
};
|
||||
use twilight_util::builder::embed::{
|
||||
EmbedAuthorBuilder, EmbedBuilder, EmbedFieldBuilder, EmbedFooterBuilder, ImageSource,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
constants::{COLOR_RED, COLOR_YELLOW}, discord::images, groups::calls::moderation::Rule, utils::ellipse,
|
||||
};
|
||||
|
||||
pub fn first_warning_embed(
|
||||
app: &Application,
|
||||
message: &Message,
|
||||
rules_broken: Vec<Arc<dyn Rule>>,
|
||||
) -> Embed {
|
||||
EmbedBuilder::new()
|
||||
.author(
|
||||
EmbedAuthorBuilder::new(app.name.clone())
|
||||
.icon_url(
|
||||
ImageSource::url(
|
||||
images::get_avatar_url(
|
||||
app.icon,
|
||||
app.bot.as_ref().unwrap().id.get(),
|
||||
app.bot.as_ref().unwrap().discriminator as u64,
|
||||
)
|
||||
).unwrap()
|
||||
)
|
||||
)
|
||||
.title("Warning")
|
||||
.description("You broke [Payphone's code of conduct](https://payphone.lat/code-of-conduct) during a call. Since it's your first offense, we assumed you didn't read the rules and made this a warning instead. Read the code of conduct to prevent getting banned.")
|
||||
.field(EmbedFieldBuilder::new("What message broke the rules?", ellipse(message.content.clone(), 500)))
|
||||
.field(EmbedFieldBuilder::new(
|
||||
"What rules did you break?",
|
||||
rules_broken
|
||||
.iter()
|
||||
.map(|rule| format!("- {}", rule.name()))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
))
|
||||
.color(COLOR_YELLOW)
|
||||
.thumbnail(
|
||||
ImageSource::url(
|
||||
images::get_avatar_url(
|
||||
app.icon,
|
||||
app.bot.as_ref().unwrap().id.get(),
|
||||
app.bot.as_ref().unwrap().discriminator as u64,
|
||||
))
|
||||
.unwrap()
|
||||
)
|
||||
.footer(EmbedFooterBuilder::new("This is a warning because it's your first offense. Breaking the rules again will result in a ban."))
|
||||
.build()
|
||||
}
|
||||
|
||||
pub fn repeat_warning_embed(
|
||||
app: &Application,
|
||||
message: &Message,
|
||||
rules_broken: Vec<Arc<dyn Rule>>,
|
||||
) -> Embed {
|
||||
EmbedBuilder::new()
|
||||
.author(
|
||||
EmbedAuthorBuilder::new(app.name.clone())
|
||||
.icon_url(
|
||||
ImageSource::url(
|
||||
images::get_avatar_url(
|
||||
app.icon,
|
||||
app.bot.as_ref().unwrap().id.get(),
|
||||
app.bot.as_ref().unwrap().discriminator as u64,
|
||||
)
|
||||
).unwrap()
|
||||
)
|
||||
)
|
||||
.title("Long Time no See!")
|
||||
.description("You broke [Payphone's code of conduct](https://payphone.lat/code-of-conduct) during a call. It's not your first offense, yet you've gone for a long time with a good record so we're making this a warning instead. In case you've forgotten the rules, consider this a reminder to re-read them.")
|
||||
.field(EmbedFieldBuilder::new("What message broke the rules?", ellipse(message.content.clone(), 500)))
|
||||
.field(EmbedFieldBuilder::new(
|
||||
"What rules did you break?",
|
||||
rules_broken
|
||||
.iter()
|
||||
.map(|rule| format!("- {}", rule.name()))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
))
|
||||
.color(COLOR_YELLOW)
|
||||
.thumbnail(
|
||||
ImageSource::url(
|
||||
images::get_avatar_url(
|
||||
app.icon,
|
||||
app.bot.as_ref().unwrap().id.get(),
|
||||
app.bot.as_ref().unwrap().discriminator as u64,
|
||||
))
|
||||
.unwrap()
|
||||
)
|
||||
.footer(EmbedFooterBuilder::new("Breaking the rules again will result in a ban."))
|
||||
.build()
|
||||
}
|
||||
|
||||
pub fn first_ban_embed(
|
||||
app: &Application,
|
||||
message: &Message,
|
||||
rules_broken: Vec<Arc<dyn Rule>>,
|
||||
) -> Embed {
|
||||
EmbedBuilder::new()
|
||||
.author(
|
||||
EmbedAuthorBuilder::new(app.name.clone())
|
||||
.icon_url(
|
||||
ImageSource::url(
|
||||
images::get_avatar_url(
|
||||
app.icon,
|
||||
app.bot.as_ref().unwrap().id.get(),
|
||||
app.bot.as_ref().unwrap().discriminator as u64,
|
||||
)
|
||||
).unwrap()
|
||||
)
|
||||
)
|
||||
.title("Banned")
|
||||
.description("You broke [Payphone's code of conduct](https://payphone.lat/code-of-conduct) during a call. This is your first offense, and as such, you are being issued a ban. This ban will last for 24 hours, next one will be permanent.")
|
||||
.field(EmbedFieldBuilder::new("What message broke the rules?", ellipse(message.content.clone(), 500)))
|
||||
.field(EmbedFieldBuilder::new(
|
||||
"What rules did you break?",
|
||||
rules_broken
|
||||
.iter()
|
||||
.map(|rule| format!("- {}", rule.name()))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
))
|
||||
.color(COLOR_RED)
|
||||
.thumbnail(
|
||||
ImageSource::url(
|
||||
images::get_avatar_url(
|
||||
app.icon,
|
||||
app.bot.as_ref().unwrap().id.get(),
|
||||
app.bot.as_ref().unwrap().discriminator as u64,
|
||||
))
|
||||
.unwrap()
|
||||
)
|
||||
.footer(EmbedFooterBuilder::new("Breaking the rules again will result in being permanently banned."))
|
||||
.build()
|
||||
}
|
||||
|
||||
pub fn final_ban_embed(
|
||||
app: &Application,
|
||||
message: &Message,
|
||||
rules_broken: Vec<Arc<dyn Rule>>
|
||||
) -> Embed {
|
||||
EmbedBuilder::new()
|
||||
.author(
|
||||
EmbedAuthorBuilder::new(app.name.clone())
|
||||
.icon_url(
|
||||
ImageSource::url(
|
||||
images::get_avatar_url(
|
||||
app.icon,
|
||||
app.bot.as_ref().unwrap().id.get(),
|
||||
app.bot.as_ref().unwrap().discriminator as u64,
|
||||
)
|
||||
).unwrap()
|
||||
)
|
||||
)
|
||||
.title("Permanently Banned")
|
||||
.description("You broke [Payphone's code of conduct](https://payphone.lat/code-of-conduct) during a call. This is your final offense, and as such, you are being issued a permanent ban.")
|
||||
.field(EmbedFieldBuilder::new("What message broke the rules?", ellipse(message.content.clone(), 500)))
|
||||
.field(EmbedFieldBuilder::new(
|
||||
"What rules did you break?",
|
||||
rules_broken
|
||||
.iter()
|
||||
.map(|rule| format!("- {}", rule.name()))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
))
|
||||
.color(COLOR_RED)
|
||||
.thumbnail(
|
||||
ImageSource::url(
|
||||
images::get_avatar_url(
|
||||
app.icon,
|
||||
app.bot.as_ref().unwrap().id.get(),
|
||||
app.bot.as_ref().unwrap().discriminator as u64,
|
||||
)
|
||||
).unwrap()
|
||||
)
|
||||
.footer(EmbedFooterBuilder::new("You broke the rules 3 times."))
|
||||
.build()
|
||||
}
|
||||
+114
-20
@@ -1,15 +1,22 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use itertools::Itertools;
|
||||
use twilight_model::{gateway::payload::incoming::MessageCreate, id::Id};
|
||||
|
||||
use crate::{
|
||||
database::models::{Call, Channel, Message},
|
||||
database::models::{Call, Channel, Message, User, UserIncident},
|
||||
discord::sanitizing::SanitizeDiscordMarkdown,
|
||||
framework::{
|
||||
events::traits::{EventResult, OnMessageHandler},
|
||||
interactions::components::ActionRow,
|
||||
runtime::Runtime,
|
||||
},
|
||||
groups::calls::{get_display_profile, moderation::escape_message},
|
||||
groups::calls::{
|
||||
components::{CodeOfConductButton, NeedHelpButton},
|
||||
embeds, get_display_profile,
|
||||
moderation::{Action, escape_message},
|
||||
},
|
||||
state::State,
|
||||
};
|
||||
|
||||
@@ -73,6 +80,16 @@ impl OnMessageHandler<State> for MessageRedirector {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let user_incidents = UserIncident::get_for_user(event.author.id.get(), &rt.state.db)
|
||||
.await?
|
||||
.iter()
|
||||
.filter(|incident| incident.is_active())
|
||||
.count();
|
||||
|
||||
if user_incidents > 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some(opposite_channel) = Channel::get_by_id(
|
||||
call.other_party_channel_id(event.channel_id.get()),
|
||||
&rt.state.db,
|
||||
@@ -97,31 +114,108 @@ impl OnMessageHandler<State> for MessageRedirector {
|
||||
|
||||
let profile = get_display_profile(event);
|
||||
|
||||
Message::build()
|
||||
let message = Message::build()
|
||||
.call_id(call.id)
|
||||
.message(event.0.clone())
|
||||
.profile(profile.clone())
|
||||
.build()
|
||||
.expect(
|
||||
"The message builder was missing either the call ID, the message, or the profile",
|
||||
)
|
||||
.insert(&rt.state.db)
|
||||
.await?;
|
||||
);
|
||||
message.insert(&rt.state.db).await?;
|
||||
|
||||
rt.http
|
||||
.execute_webhook(Id::new(webhook_id), &webhook_token)
|
||||
.allowed_mentions(None)
|
||||
.username(&profile.name)
|
||||
.avatar_url(&profile.avatar_url)
|
||||
.content(&escape_message(event))
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
log::error!(
|
||||
"Failed to send message to webhook for call {} in channel {}: {e:?}",
|
||||
call.id,
|
||||
opposite_channel.id
|
||||
);
|
||||
})?;
|
||||
let (content, rules) = escape_message(event);
|
||||
|
||||
if rules
|
||||
.iter()
|
||||
.any(|rule| rule.action() == &Action::BlockAndPenalty)
|
||||
{
|
||||
let user = User::get_by_id(event.author.id.get(), &rt.state.db).await?;
|
||||
|
||||
let user = if let Some(user) = user {
|
||||
user
|
||||
} else {
|
||||
let user: User = event.author.clone().into();
|
||||
|
||||
user.insert(&rt.state.db).await?;
|
||||
|
||||
user
|
||||
};
|
||||
|
||||
let reason = format!(
|
||||
"You broke the following rules during a call:\n{}",
|
||||
rules
|
||||
.iter()
|
||||
.map(|rule| format!("- {}", rule.name()))
|
||||
.join("\n")
|
||||
);
|
||||
|
||||
let new_incident = user
|
||||
.incident(
|
||||
reason.clone(),
|
||||
message.data.clone(),
|
||||
Some(event.author.id.get()),
|
||||
&rt.state.db,
|
||||
)
|
||||
.await?;
|
||||
|
||||
rt.http
|
||||
.create_message(event.channel_id)
|
||||
.content(
|
||||
"❌ Your message has been blocked because it broke Payphone's code of conduct. Check your DMs for more information.",
|
||||
)
|
||||
.await?;
|
||||
|
||||
rt.http
|
||||
.create_message(Id::new(opposite_channel.id))
|
||||
.content(&format!(
|
||||
"❌ **{}** Sent a message that was blocked.\n-# If you see an inappropiate message, please report it using `Message > Apps > Report Message`.",
|
||||
profile.name.sanitize(false)
|
||||
))
|
||||
.await?;
|
||||
|
||||
let dm_channel = rt
|
||||
.http
|
||||
.create_private_channel(event.author.id)
|
||||
.await?
|
||||
.model()
|
||||
.await?;
|
||||
|
||||
let embed = if new_incident.is_first_incident {
|
||||
embeds::first_warning_embed(&rt.app, &event.0, rules)
|
||||
} else if new_incident.counting_incidents == 0 {
|
||||
embeds::repeat_warning_embed(&rt.app, &event.0, rules)
|
||||
} else if new_incident.counting_incidents == 1 {
|
||||
embeds::first_ban_embed(&rt.app, &event.0, rules)
|
||||
} else {
|
||||
embeds::final_ban_embed(&rt.app, &event.0, rules)
|
||||
};
|
||||
|
||||
rt.http
|
||||
.create_message(dm_channel.id)
|
||||
.embeds(&[embed])
|
||||
.components(&[ActionRow::new(&[
|
||||
(None, Arc::new(CodeOfConductButton)),
|
||||
(None, Arc::new(NeedHelpButton)),
|
||||
])
|
||||
.build()])
|
||||
.await?;
|
||||
} else {
|
||||
rt.http
|
||||
.execute_webhook(Id::new(webhook_id), &webhook_token)
|
||||
.allowed_mentions(None)
|
||||
.username(&profile.name)
|
||||
.avatar_url(&profile.avatar_url)
|
||||
.content(&content)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
log::error!(
|
||||
"Failed to send message to webhook for call {} in channel {}: {e:?}",
|
||||
call.id,
|
||||
opposite_channel.id
|
||||
);
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ use twilight_validate::request::webhook_username;
|
||||
|
||||
use crate::discord::images;
|
||||
|
||||
pub(super) mod components;
|
||||
pub(super) mod embeds;
|
||||
pub mod commands;
|
||||
pub mod events;
|
||||
pub(super) mod moderation;
|
||||
|
||||
@@ -85,7 +85,6 @@
|
||||
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use linkify::LinkFinder;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use twilight_model::gateway::payload::incoming::MessageCreate;
|
||||
@@ -101,8 +100,9 @@ use crate::discord::sanitizing::SanitizeDiscordMarkdown;
|
||||
/// * `event`: The incoming message event to escape.
|
||||
///
|
||||
/// Returns:
|
||||
/// [`String`] - The escaped and censored message.
|
||||
pub fn escape_message(event: &MessageCreate) -> String {
|
||||
/// [`(String, Vec<Arc<dyn Rule>>)`] - The escaped and censored message, and a vector of the rules
|
||||
/// broken.
|
||||
pub fn escape_message(event: &MessageCreate) -> (String, Vec<Arc<dyn Rule>>) {
|
||||
let mut message = event.content.to_string();
|
||||
|
||||
for mention in &event.mentions {
|
||||
@@ -123,20 +123,11 @@ pub fn escape_message(event: &MessageCreate) -> String {
|
||||
);
|
||||
}
|
||||
|
||||
let mut finder = LinkFinder::new();
|
||||
finder.url_must_have_scheme(false);
|
||||
|
||||
for link in finder.links(&message.clone()) {
|
||||
let url = link.as_str();
|
||||
|
||||
message = message.replace(url, &"\\#".repeat(url.len()));
|
||||
}
|
||||
|
||||
let (censored, _) = moderate_message(&message);
|
||||
let (censored, rules) = moderate_message(&message);
|
||||
|
||||
log::debug!("Escaped and normalized message: {censored:?}");
|
||||
|
||||
censored
|
||||
(censored, rules)
|
||||
}
|
||||
|
||||
/// A normalized message and its mappings to the original message's individual grapheme clusters.
|
||||
@@ -157,6 +148,8 @@ pub struct NormalizedMessage {
|
||||
}
|
||||
|
||||
/// Generates a normalized message out of the original message string.
|
||||
///
|
||||
/// TODO: Replace * in the message with wildcards to match censored words.
|
||||
///
|
||||
/// Arguments:
|
||||
/// * `message`: The original message to normalize.
|
||||
@@ -165,6 +158,7 @@ pub struct NormalizedMessage {
|
||||
/// [`NormalizedMessage`] - The normalized message and its mappings to the original string.
|
||||
pub fn normalize(message: String) -> NormalizedMessage {
|
||||
let replacements: HashMap<char, char> = [
|
||||
('$', 's'),
|
||||
('@', 'a'),
|
||||
('0', 'o'),
|
||||
('1', 'i'),
|
||||
|
||||
Reference in New Issue
Block a user