Add automated warning and banning
This commit is contained in:
Generated
+1
@@ -1225,6 +1225,7 @@ dependencies = [
|
|||||||
"colored",
|
"colored",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"include_dir",
|
"include_dir",
|
||||||
|
"itertools",
|
||||||
"linkify",
|
"linkify",
|
||||||
"log",
|
"log",
|
||||||
"notify",
|
"notify",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ clap = { version = "4.5.42", features = ["derive", "env", "string"] }
|
|||||||
colored = "3.0.0"
|
colored = "3.0.0"
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
include_dir = "0.7.4"
|
include_dir = "0.7.4"
|
||||||
|
itertools = "0.14.0"
|
||||||
linkify = "0.10.0"
|
linkify = "0.10.0"
|
||||||
log = { version = "0.4.27", features = ["serde"] }
|
log = { version = "0.4.27", features = ["serde"] }
|
||||||
notify = "8.2.0"
|
notify = "8.2.0"
|
||||||
|
|||||||
@@ -8,3 +8,6 @@ pub const COLOR_WHITE: u32 = 0xE2D9D0;
|
|||||||
|
|
||||||
/// A red color used for error messages and moderator embeds.
|
/// A red color used for error messages and moderator embeds.
|
||||||
pub const COLOR_RED: u32 = 0xFF8787;
|
pub const COLOR_RED: u32 = 0xFF8787;
|
||||||
|
|
||||||
|
/// A yellow color used for mask notifications and warnings.
|
||||||
|
pub const COLOR_YELLOW: u32 = 0xFFC868;
|
||||||
|
|||||||
+228
-2
@@ -9,7 +9,7 @@ use scylla::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use time::{OffsetDateTime, UtcDateTime};
|
use time::{Duration, OffsetDateTime, UtcDateTime, UtcOffset};
|
||||||
use twilight_http::Client;
|
use twilight_http::Client;
|
||||||
use twilight_model::{
|
use twilight_model::{
|
||||||
channel::Message as TwilightMessage,
|
channel::Message as TwilightMessage,
|
||||||
@@ -134,8 +134,13 @@ impl<'a> Object<'a> for User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct NewUserIncident {
|
||||||
|
pub incident: UserIncident,
|
||||||
|
pub counting_incidents: usize,
|
||||||
|
pub is_first_incident: bool,
|
||||||
|
}
|
||||||
|
|
||||||
impl User {
|
impl User {
|
||||||
#[allow(dead_code)]
|
|
||||||
pub async fn get_by_id(id: u64, session: &Session) -> Result<Option<Self>, DatabaseError> {
|
pub async fn get_by_id(id: u64, session: &Session) -> Result<Option<Self>, DatabaseError> {
|
||||||
let id = u64_to_i64(id);
|
let id = u64_to_i64(id);
|
||||||
|
|
||||||
@@ -175,6 +180,77 @@ impl User {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn insert(&self, session: &Session) -> Result<(), DatabaseError> {
|
||||||
|
session
|
||||||
|
.query_unpaged(
|
||||||
|
r#"
|
||||||
|
INSERT INTO payphone.users (
|
||||||
|
user_id,
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
is_masked,
|
||||||
|
is_moderator,
|
||||||
|
joined_at,
|
||||||
|
last_seen_at,
|
||||||
|
supporter_since,
|
||||||
|
supporter_until
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||||
|
"#,
|
||||||
|
Self::to_row(self.clone()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(DatabaseError::QueryError)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn incident(
|
||||||
|
&self,
|
||||||
|
reason: String,
|
||||||
|
message: MessageData,
|
||||||
|
created_by_id: Option<u64>,
|
||||||
|
session: &Session,
|
||||||
|
) -> Result<NewUserIncident, DatabaseError> {
|
||||||
|
let incidents = UserIncident::get_for_user(self.id, session).await?;
|
||||||
|
|
||||||
|
let counting_incidents = incidents
|
||||||
|
.iter()
|
||||||
|
.filter(|incident| incident.created_at + Duration::weeks(12) > UtcDateTime::now())
|
||||||
|
.count();
|
||||||
|
|
||||||
|
let incident_type = if counting_incidents != 0 {
|
||||||
|
UserIncidentType::Ban
|
||||||
|
} else {
|
||||||
|
UserIncidentType::Warning
|
||||||
|
};
|
||||||
|
|
||||||
|
let duration = match counting_incidents {
|
||||||
|
0 => None, // Warning
|
||||||
|
1 => Some(Duration::days(1)), // First ban
|
||||||
|
_ => None, // Permanent ban
|
||||||
|
};
|
||||||
|
|
||||||
|
let incident = UserIncident {
|
||||||
|
user_id: self.id,
|
||||||
|
incident_id: get_random_u64(),
|
||||||
|
report_id: None,
|
||||||
|
r#type: incident_type,
|
||||||
|
created_at: UtcDateTime::now(),
|
||||||
|
expires_at: duration.map(|d| UtcDateTime::now() + d),
|
||||||
|
message,
|
||||||
|
reason,
|
||||||
|
created_by_id,
|
||||||
|
duration: duration.map(|_| "1 day".into()),
|
||||||
|
};
|
||||||
|
incident.insert(session).await?;
|
||||||
|
|
||||||
|
Ok(NewUserIncident {
|
||||||
|
incident,
|
||||||
|
counting_incidents,
|
||||||
|
is_first_incident: incidents.len() == 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<TwilightUser> for User {
|
impl From<TwilightUser> for User {
|
||||||
@@ -1008,3 +1084,153 @@ impl Message {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum UserIncidentType {
|
||||||
|
Warning,
|
||||||
|
Ban,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct UserIncident {
|
||||||
|
pub user_id: u64,
|
||||||
|
pub incident_id: u64,
|
||||||
|
pub report_id: Option<u64>,
|
||||||
|
|
||||||
|
pub r#type: UserIncidentType,
|
||||||
|
|
||||||
|
pub message: MessageData,
|
||||||
|
|
||||||
|
pub reason: String,
|
||||||
|
|
||||||
|
pub created_by_id: Option<u64>,
|
||||||
|
|
||||||
|
pub created_at: UtcDateTime,
|
||||||
|
pub expires_at: Option<UtcDateTime>,
|
||||||
|
pub duration: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Object<'a> for UserIncident {
|
||||||
|
type Row = (
|
||||||
|
i64,
|
||||||
|
i64,
|
||||||
|
Option<i64>,
|
||||||
|
String,
|
||||||
|
MessageData,
|
||||||
|
String,
|
||||||
|
Option<i64>,
|
||||||
|
OffsetDateTime,
|
||||||
|
Option<OffsetDateTime>,
|
||||||
|
Option<String>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fn from_row(row: Self::Row) -> Self {
|
||||||
|
Self {
|
||||||
|
user_id: i64_to_u64(row.0),
|
||||||
|
incident_id: i64_to_u64(row.1),
|
||||||
|
report_id: row.2.map(i64_to_u64),
|
||||||
|
r#type: match row.3.as_str() {
|
||||||
|
"warning" => UserIncidentType::Warning,
|
||||||
|
"ban" => UserIncidentType::Ban,
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
|
message: row.4,
|
||||||
|
reason: row.5,
|
||||||
|
created_by_id: row.6.map(i64_to_u64),
|
||||||
|
created_at: row.7.to_utc(),
|
||||||
|
expires_at: row.8.map(|i| i.to_utc()),
|
||||||
|
duration: row.9,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_row(value: Self) -> Self::Row {
|
||||||
|
(
|
||||||
|
u64_to_i64(value.user_id),
|
||||||
|
u64_to_i64(value.incident_id),
|
||||||
|
value.report_id.map(u64_to_i64),
|
||||||
|
match value.r#type {
|
||||||
|
UserIncidentType::Warning => "warning".into(),
|
||||||
|
UserIncidentType::Ban => "ban".into(),
|
||||||
|
},
|
||||||
|
value.message,
|
||||||
|
value.reason,
|
||||||
|
value.created_by_id.map(u64_to_i64),
|
||||||
|
value.created_at.to_offset(UtcOffset::UTC),
|
||||||
|
value.expires_at.map(|i| i.to_offset(UtcOffset::UTC)),
|
||||||
|
value.duration,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserIncident {
|
||||||
|
pub fn is_active(&self) -> bool {
|
||||||
|
self.expires_at
|
||||||
|
.map(|e| e > UtcDateTime::now())
|
||||||
|
.unwrap_or(true)
|
||||||
|
&& self.r#type == UserIncidentType::Ban
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_for_user(user_id: u64, session: &Session) -> Result<Vec<Self>, DatabaseError> {
|
||||||
|
let user_id = u64_to_i64(user_id);
|
||||||
|
|
||||||
|
let result = session
|
||||||
|
.query_unpaged(
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
user_id,
|
||||||
|
incident_id,
|
||||||
|
report_id,
|
||||||
|
type,
|
||||||
|
message,
|
||||||
|
reason,
|
||||||
|
created_by_id,
|
||||||
|
created_at,
|
||||||
|
expires_at,
|
||||||
|
duration
|
||||||
|
FROM
|
||||||
|
payphone.user_incidents
|
||||||
|
WHERE
|
||||||
|
user_id = ?;
|
||||||
|
"#,
|
||||||
|
(user_id,),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(DatabaseError::QueryError)?
|
||||||
|
.into_rows_result()
|
||||||
|
.map_err(|_| DatabaseError::ResultNotRows)?;
|
||||||
|
|
||||||
|
Ok(result
|
||||||
|
.rows::<<Self as Object>::Row>()
|
||||||
|
.map_err(|_| DatabaseError::ResultNotRows)?
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(|_| DatabaseError::DeserializationFailed)?
|
||||||
|
.into_iter()
|
||||||
|
.map(Self::from_row)
|
||||||
|
.collect::<Vec<_>>())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn insert(&self, session: &Session) -> Result<(), DatabaseError> {
|
||||||
|
session
|
||||||
|
.query_unpaged(
|
||||||
|
r#"
|
||||||
|
INSERT INTO payphone.user_incidents (
|
||||||
|
user_id,
|
||||||
|
incident_id,
|
||||||
|
report_id,
|
||||||
|
type,
|
||||||
|
message,
|
||||||
|
reason,
|
||||||
|
created_by_id,
|
||||||
|
created_at,
|
||||||
|
expires_at,
|
||||||
|
duration
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||||
|
"#,
|
||||||
|
Self::to_row(self.clone()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(DatabaseError::QueryError)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -1,15 +1,22 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use itertools::Itertools;
|
||||||
use twilight_model::{gateway::payload::incoming::MessageCreate, id::Id};
|
use twilight_model::{gateway::payload::incoming::MessageCreate, id::Id};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::models::{Call, Channel, Message},
|
database::models::{Call, Channel, Message, User, UserIncident},
|
||||||
|
discord::sanitizing::SanitizeDiscordMarkdown,
|
||||||
framework::{
|
framework::{
|
||||||
events::traits::{EventResult, OnMessageHandler},
|
events::traits::{EventResult, OnMessageHandler},
|
||||||
|
interactions::components::ActionRow,
|
||||||
runtime::Runtime,
|
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,
|
state::State,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,6 +80,16 @@ impl OnMessageHandler<State> for MessageRedirector {
|
|||||||
return Ok(());
|
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(
|
let Some(opposite_channel) = Channel::get_by_id(
|
||||||
call.other_party_channel_id(event.channel_id.get()),
|
call.other_party_channel_id(event.channel_id.get()),
|
||||||
&rt.state.db,
|
&rt.state.db,
|
||||||
@@ -97,23 +114,99 @@ impl OnMessageHandler<State> for MessageRedirector {
|
|||||||
|
|
||||||
let profile = get_display_profile(event);
|
let profile = get_display_profile(event);
|
||||||
|
|
||||||
Message::build()
|
let message = Message::build()
|
||||||
.call_id(call.id)
|
.call_id(call.id)
|
||||||
.message(event.0.clone())
|
.message(event.0.clone())
|
||||||
.profile(profile.clone())
|
.profile(profile.clone())
|
||||||
.build()
|
.build()
|
||||||
.expect(
|
.expect(
|
||||||
"The message builder was missing either the call ID, the message, or the profile",
|
"The message builder was missing either the call ID, the message, or the profile",
|
||||||
|
);
|
||||||
|
message.insert(&rt.state.db).await?;
|
||||||
|
|
||||||
|
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,
|
||||||
)
|
)
|
||||||
.insert(&rt.state.db)
|
|
||||||
.await?;
|
.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
|
rt.http
|
||||||
.execute_webhook(Id::new(webhook_id), &webhook_token)
|
.execute_webhook(Id::new(webhook_id), &webhook_token)
|
||||||
.allowed_mentions(None)
|
.allowed_mentions(None)
|
||||||
.username(&profile.name)
|
.username(&profile.name)
|
||||||
.avatar_url(&profile.avatar_url)
|
.avatar_url(&profile.avatar_url)
|
||||||
.content(&escape_message(event))
|
.content(&content)
|
||||||
.await
|
.await
|
||||||
.inspect_err(|e| {
|
.inspect_err(|e| {
|
||||||
log::error!(
|
log::error!(
|
||||||
@@ -122,6 +215,7 @@ impl OnMessageHandler<State> for MessageRedirector {
|
|||||||
opposite_channel.id
|
opposite_channel.id
|
||||||
);
|
);
|
||||||
})?;
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ use twilight_validate::request::webhook_username;
|
|||||||
|
|
||||||
use crate::discord::images;
|
use crate::discord::images;
|
||||||
|
|
||||||
|
pub(super) mod components;
|
||||||
|
pub(super) mod embeds;
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod events;
|
pub mod events;
|
||||||
pub(super) mod moderation;
|
pub(super) mod moderation;
|
||||||
|
|||||||
@@ -85,7 +85,6 @@
|
|||||||
|
|
||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
use linkify::LinkFinder;
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use twilight_model::gateway::payload::incoming::MessageCreate;
|
use twilight_model::gateway::payload::incoming::MessageCreate;
|
||||||
@@ -101,8 +100,9 @@ use crate::discord::sanitizing::SanitizeDiscordMarkdown;
|
|||||||
/// * `event`: The incoming message event to escape.
|
/// * `event`: The incoming message event to escape.
|
||||||
///
|
///
|
||||||
/// Returns:
|
/// Returns:
|
||||||
/// [`String`] - The escaped and censored message.
|
/// [`(String, Vec<Arc<dyn Rule>>)`] - The escaped and censored message, and a vector of the rules
|
||||||
pub fn escape_message(event: &MessageCreate) -> String {
|
/// broken.
|
||||||
|
pub fn escape_message(event: &MessageCreate) -> (String, Vec<Arc<dyn Rule>>) {
|
||||||
let mut message = event.content.to_string();
|
let mut message = event.content.to_string();
|
||||||
|
|
||||||
for mention in &event.mentions {
|
for mention in &event.mentions {
|
||||||
@@ -123,20 +123,11 @@ pub fn escape_message(event: &MessageCreate) -> String {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut finder = LinkFinder::new();
|
let (censored, rules) = moderate_message(&message);
|
||||||
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);
|
|
||||||
|
|
||||||
log::debug!("Escaped and normalized message: {censored:?}");
|
log::debug!("Escaped and normalized message: {censored:?}");
|
||||||
|
|
||||||
censored
|
(censored, rules)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A normalized message and its mappings to the original message's individual grapheme clusters.
|
/// A normalized message and its mappings to the original message's individual grapheme clusters.
|
||||||
@@ -158,6 +149,8 @@ pub struct NormalizedMessage {
|
|||||||
|
|
||||||
/// Generates a normalized message out of the original message string.
|
/// Generates a normalized message out of the original message string.
|
||||||
///
|
///
|
||||||
|
/// TODO: Replace * in the message with wildcards to match censored words.
|
||||||
|
///
|
||||||
/// Arguments:
|
/// Arguments:
|
||||||
/// * `message`: The original message to normalize.
|
/// * `message`: The original message to normalize.
|
||||||
///
|
///
|
||||||
@@ -165,6 +158,7 @@ pub struct NormalizedMessage {
|
|||||||
/// [`NormalizedMessage`] - The normalized message and its mappings to the original string.
|
/// [`NormalizedMessage`] - The normalized message and its mappings to the original string.
|
||||||
pub fn normalize(message: String) -> NormalizedMessage {
|
pub fn normalize(message: String) -> NormalizedMessage {
|
||||||
let replacements: HashMap<char, char> = [
|
let replacements: HashMap<char, char> = [
|
||||||
|
('$', 's'),
|
||||||
('@', 'a'),
|
('@', 'a'),
|
||||||
('0', 'o'),
|
('0', 'o'),
|
||||||
('1', 'i'),
|
('1', 'i'),
|
||||||
|
|||||||
@@ -54,3 +54,21 @@ pub trait Bye: Sized {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Bye for T {}
|
impl<T> Bye for T {}
|
||||||
|
|
||||||
|
/// Trims a string to a maximum length, adding an ellipsis if it exceeds the limit.
|
||||||
|
///
|
||||||
|
/// Arguments:
|
||||||
|
/// * `string` - The string to trim with ellipse.
|
||||||
|
/// * `max_length` - The max length the string may have.
|
||||||
|
///
|
||||||
|
/// Returns:
|
||||||
|
/// [`String`] - The trimmed string with an ellipsis if it exceeded the limit.
|
||||||
|
pub fn ellipse(string: String, max_length: usize) -> String {
|
||||||
|
if string.len() <= max_length {
|
||||||
|
string
|
||||||
|
} else {
|
||||||
|
let len = max_length.saturating_sub(3);
|
||||||
|
|
||||||
|
format!("{}...", &string[..len])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user