Add message auto-censorship
This commit is contained in:
@@ -1,15 +1,18 @@
|
|||||||
use std::{collections::HashMap, sync::Arc};
|
use std::sync::Arc;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use linkify::LinkFinder;
|
|
||||||
use twilight_model::{gateway::payload::incoming::MessageCreate, id::Id};
|
use twilight_model::{gateway::payload::incoming::MessageCreate, id::Id};
|
||||||
use twilight_validate::request::webhook_username;
|
use twilight_validate::request::webhook_username;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::models::{Call, Channel}, discord::{images, sanitizing::SanitizeDiscordMarkdown}, framework::{
|
database::models::{Call, Channel},
|
||||||
|
discord::images,
|
||||||
|
framework::{
|
||||||
events::traits::{EventResult, OnMessageHandler},
|
events::traits::{EventResult, OnMessageHandler},
|
||||||
runtime::Runtime,
|
runtime::Runtime,
|
||||||
}, groups::calls::moderation::escape_message, state::State
|
},
|
||||||
|
groups::calls::moderation::escape_message,
|
||||||
|
state::State,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
slap that ass
|
||||||
@@ -62,11 +62,11 @@ pub fn escape_message(event: &MessageCreate) -> String {
|
|||||||
message = message.replace(url, &"\\#".repeat(url.len()));
|
message = message.replace(url, &"\\#".repeat(url.len()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let normalized = normalize(message.clone());
|
let (censored, rules) = moderate_message(&message);
|
||||||
|
|
||||||
log::debug!("Escaped and normalized message: {normalized:?}");
|
log::debug!("Escaped and normalized message: {censored:?}");
|
||||||
|
|
||||||
message.sanitize(false)
|
censored
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -113,13 +113,16 @@ pub fn normalize(message: String) -> NormalizedMessage {
|
|||||||
for char in unidecoded.chars() {
|
for char in unidecoded.chars() {
|
||||||
if char.is_alphanumeric() {
|
if char.is_alphanumeric() {
|
||||||
normalized.push(char);
|
normalized.push(char);
|
||||||
mappings.push(i);
|
} else {
|
||||||
|
normalized.push(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mappings.push(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NormalizedMessage {
|
NormalizedMessage {
|
||||||
message: normalized,
|
message: normalized.to_ascii_lowercase(),
|
||||||
mappings,
|
mappings,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,8 +130,12 @@ pub fn normalize(message: String) -> NormalizedMessage {
|
|||||||
fn regex_from_phrase(phrase: &str) -> String {
|
fn regex_from_phrase(phrase: &str) -> String {
|
||||||
let mut pattern = String::new();
|
let mut pattern = String::new();
|
||||||
|
|
||||||
for ch in phrase.replace(" ", "").chars() {
|
for (i, ch) in phrase.replace(" ", "").chars().enumerate() {
|
||||||
pattern.push_str(&format!("{ch}+\\s*"));
|
if i == phrase.len() - 1 {
|
||||||
|
pattern.push_str(&format!("{ch}+"));
|
||||||
|
} else {
|
||||||
|
pattern.push_str(&format!("{ch}+\\s*"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let pattern = format!("\\b{pattern}\\b");
|
let pattern = format!("\\b{pattern}\\b");
|
||||||
@@ -170,19 +177,44 @@ pub fn censor_message(
|
|||||||
) -> String {
|
) -> String {
|
||||||
let mut censored = original.to_string();
|
let mut censored = original.to_string();
|
||||||
|
|
||||||
for (findings, rule) in analysis {
|
// Collect all ranges to censor
|
||||||
for finding in findings {
|
let mut ranges = Vec::new();
|
||||||
while let Some(start) = normalized.message.find(finding) {
|
|
||||||
let end = start + finding.len();
|
|
||||||
|
|
||||||
todo!();
|
for (findings, _) in analysis {
|
||||||
|
for finding in findings {
|
||||||
|
for (index, _) in normalized.message.match_indices(finding) {
|
||||||
|
let start_index = normalized.mappings.get(index).cloned().unwrap_or(0);
|
||||||
|
let end_index = normalized.mappings.get(index + finding.len()).cloned().unwrap_or(original.len());
|
||||||
|
|
||||||
|
ranges.push((start_index, end_index));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sort ranges in reverse order to avoid shifting indices
|
||||||
|
ranges.sort_by(|a, b| b.0.cmp(&a.0));
|
||||||
|
|
||||||
|
for (start_index, end_index) in ranges {
|
||||||
|
log::trace!("Censoring from {start_index} to {end_index}");
|
||||||
|
if start_index < end_index && end_index <= censored.len() {
|
||||||
|
censored.replace_range(start_index..end_index, "[censored]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
censored
|
censored
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn moderate_message(original: &str) -> (String, Vec<Arc<dyn Rule>>) {
|
||||||
|
let normalized = normalize(original.to_string());
|
||||||
|
|
||||||
|
log::trace!("Normalized message: {:?}", normalized.message);
|
||||||
|
|
||||||
|
let analysis = analyze_message(&normalized.message);
|
||||||
|
let censored = censor_message(original, normalized, &analysis);
|
||||||
|
|
||||||
|
(censored, analysis.iter().map(|(_, r)| r.clone()).collect())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
pub enum Action {
|
pub enum Action {
|
||||||
BlockAndPenalty,
|
BlockAndPenalty,
|
||||||
@@ -218,6 +250,8 @@ impl HateSpeechAndSlursRule {
|
|||||||
|
|
||||||
let regex = regex_from_phrases_list(&phrases);
|
let regex = regex_from_phrases_list(&phrases);
|
||||||
|
|
||||||
|
log::trace!("Hate Speech and Slurs Rule regex: {regex}");
|
||||||
|
|
||||||
Self(regex)
|
Self(regex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -248,6 +282,8 @@ impl ThreatsAndSelfHarmRule {
|
|||||||
|
|
||||||
let regex = regex_from_phrases_list(&phrases);
|
let regex = regex_from_phrases_list(&phrases);
|
||||||
|
|
||||||
|
log::trace!("Threats and Self Harm Rule regex: {regex}");
|
||||||
|
|
||||||
Self(regex)
|
Self(regex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,6 +314,8 @@ impl ExplicitSexualLanguageRule {
|
|||||||
|
|
||||||
let regex = regex_from_phrases_list(&phrases);
|
let regex = regex_from_phrases_list(&phrases);
|
||||||
|
|
||||||
|
log::trace!("Explicit Sexual Language Rule regex: {regex}");
|
||||||
|
|
||||||
Self(regex)
|
Self(regex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -307,6 +345,8 @@ impl SuggestiveLanguageRule {
|
|||||||
|
|
||||||
let regex = regex_from_phrases_list(&phrases);
|
let regex = regex_from_phrases_list(&phrases);
|
||||||
|
|
||||||
|
log::trace!("Suggestive Language Rule regex: {regex}");
|
||||||
|
|
||||||
Self(regex)
|
Self(regex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user