Checkpoint
This commit is contained in:
Generated
+46
@@ -29,6 +29,15 @@ dependencies = [
|
|||||||
"zerocopy",
|
"zerocopy",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "alloc-no-stdlib"
|
name = "alloc-no-stdlib"
|
||||||
version = "2.0.4"
|
version = "2.0.4"
|
||||||
@@ -1223,6 +1232,7 @@ dependencies = [
|
|||||||
"open",
|
"open",
|
||||||
"rand",
|
"rand",
|
||||||
"rand_chacha",
|
"rand_chacha",
|
||||||
|
"regex",
|
||||||
"scc",
|
"scc",
|
||||||
"scylla",
|
"scylla",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
@@ -1234,6 +1244,7 @@ dependencies = [
|
|||||||
"twilight-model",
|
"twilight-model",
|
||||||
"twilight-util",
|
"twilight-util",
|
||||||
"twilight-validate",
|
"twilight-validate",
|
||||||
|
"unicode-segmentation",
|
||||||
"unidecode",
|
"unidecode",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1367,6 +1378,35 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.14"
|
version = "0.17.14"
|
||||||
@@ -2161,6 +2201,12 @@ version = "1.0.18"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-segmentation"
|
||||||
|
version = "1.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unidecode"
|
name = "unidecode"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ once_cell = "1.21.3"
|
|||||||
open = "5.3.2"
|
open = "5.3.2"
|
||||||
rand = "0.9.2"
|
rand = "0.9.2"
|
||||||
rand_chacha = { version = "0.9.0", features = ["os_rng"] }
|
rand_chacha = { version = "0.9.0", features = ["os_rng"] }
|
||||||
|
regex = "1.11.1"
|
||||||
scc = "2.3.4"
|
scc = "2.3.4"
|
||||||
scylla = { version = "1.3.1", features = ["time-03"] }
|
scylla = { version = "1.3.1", features = ["time-03"] }
|
||||||
thiserror = "2.0.12"
|
thiserror = "2.0.12"
|
||||||
@@ -28,4 +29,5 @@ twilight-http = { version = "0.16.0", features = ["simd-json"] }
|
|||||||
twilight-model = "0.16.0"
|
twilight-model = "0.16.0"
|
||||||
twilight-util = { version = "0.16.0", features = ["builder"] }
|
twilight-util = { version = "0.16.0", features = ["builder"] }
|
||||||
twilight-validate = "0.16.0"
|
twilight-validate = "0.16.0"
|
||||||
|
unicode-segmentation = "1.12.0"
|
||||||
unidecode = "0.3.0"
|
unidecode = "0.3.0"
|
||||||
|
|||||||
+22
-5
@@ -45,6 +45,21 @@
|
|||||||
//! ```shell
|
//! ```shell
|
||||||
//! $ cargo run -- start [TOKEN]
|
//! $ cargo run -- start [TOKEN]
|
||||||
//! ```
|
//! ```
|
||||||
|
//!
|
||||||
|
//! # Running the Documentation Server
|
||||||
|
//!
|
||||||
|
//! Payphone has a built-in documentation server that serves the project's documentation and
|
||||||
|
//! refreshes it on changes. To start the documentation server, run the following command:
|
||||||
|
//!
|
||||||
|
//! ```shell
|
||||||
|
//! $ cargo run -- docs --open
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! The `--open` flag is optional and will open the documentation in the default browser after
|
||||||
|
//! starting the server. You can also use the `--bind` argument to specify the address to bind the
|
||||||
|
//! server to, which defaults to `0.0.0.0:3000`.
|
||||||
|
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
/// Payphone's argument parser.
|
/// Payphone's argument parser.
|
||||||
#[derive(clap::Parser)]
|
#[derive(clap::Parser)]
|
||||||
@@ -129,16 +144,18 @@ pub enum Commands {
|
|||||||
/// Starts a documentation server for the project which serves `cargo doc` and refreshes on
|
/// Starts a documentation server for the project which serves `cargo doc` and refreshes on
|
||||||
/// changes.
|
/// changes.
|
||||||
Docs {
|
Docs {
|
||||||
/// The port to run the documentation server on.
|
/// The address to run the documentation server on.
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
short,
|
short,
|
||||||
env = "PAYPHONE_DOCS_PORT",
|
env = "PAYPHONE_DOCS_ADDR",
|
||||||
value_name = "PORT",
|
value_name = "ADDR",
|
||||||
default_value_t = 3000
|
default_value_t = SocketAddr::from(([0, 0, 0, 0], 3000))
|
||||||
)]
|
)]
|
||||||
port: u16,
|
bind: SocketAddr,
|
||||||
|
|
||||||
|
/// When this flag is set, the documentation server will open the documentation in the
|
||||||
|
/// default browser after starting the server.
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
short,
|
short,
|
||||||
|
|||||||
@@ -6,13 +6,10 @@ 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},
|
database::models::{Call, Channel}, discord::{images, sanitizing::SanitizeDiscordMarkdown}, framework::{
|
||||||
discord::{images, sanitizing::SanitizeDiscordMarkdown},
|
|
||||||
framework::{
|
|
||||||
events::traits::{EventResult, OnMessageHandler},
|
events::traits::{EventResult, OnMessageHandler},
|
||||||
runtime::Runtime,
|
runtime::Runtime,
|
||||||
},
|
}, groups::calls::moderation::escape_message, state::State
|
||||||
state::State,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -155,74 +152,3 @@ fn get_display_profile(event: &MessageCreate) -> DisplayProfile {
|
|||||||
|
|
||||||
DisplayProfile { name, avatar_url }
|
DisplayProfile { name, avatar_url }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn escape_message(event: &MessageCreate) -> String {
|
|
||||||
let mut message = event.content.to_string();
|
|
||||||
|
|
||||||
for mention in &event.mentions {
|
|
||||||
message = message.replace(
|
|
||||||
&format!("<@{}>", mention.id),
|
|
||||||
&format!("**@{}**", mention.name.sanitize(true)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for mention in &event.mention_roles {
|
|
||||||
message = message.replace(&format!("<@&{mention}>"), "**@Role**");
|
|
||||||
}
|
|
||||||
|
|
||||||
for mention in &event.mention_channels {
|
|
||||||
message = message.replace(
|
|
||||||
&format!("<#{}>", mention.id),
|
|
||||||
&format!("**#{}**", mention.name.sanitize(true)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 replacements: HashMap<char, Option<char>> = [
|
|
||||||
('@', Some('a')),
|
|
||||||
('0', Some('o')),
|
|
||||||
('1', Some('i')),
|
|
||||||
('2', Some('z')),
|
|
||||||
('3', Some('e')),
|
|
||||||
('4', Some('a')),
|
|
||||||
('5', Some('s')),
|
|
||||||
('6', Some('b')),
|
|
||||||
('7', Some('t')),
|
|
||||||
('8', Some('g')),
|
|
||||||
('9', Some('q')),
|
|
||||||
]
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let normalized_unidecoded = unidecode::unidecode(&message)
|
|
||||||
.to_lowercase()
|
|
||||||
.split_whitespace()
|
|
||||||
.collect::<Vec<&str>>()
|
|
||||||
.join(" ");
|
|
||||||
let mut normalized = String::with_capacity(normalized_unidecoded.len());
|
|
||||||
|
|
||||||
for ch in normalized_unidecoded.chars() {
|
|
||||||
if let Some(&replacement) = replacements.get(&ch) {
|
|
||||||
if let Some(replacement) = replacement {
|
|
||||||
normalized.push(replacement);
|
|
||||||
}
|
|
||||||
} else if ch.is_alphanumeric() || ch.is_whitespace() {
|
|
||||||
normalized.push(ch);
|
|
||||||
}
|
|
||||||
|
|
||||||
// skip everything else (punctuation not in map, zero-width, etc.)
|
|
||||||
}
|
|
||||||
|
|
||||||
log::debug!("Escaped message: {normalized}");
|
|
||||||
|
|
||||||
message.sanitize(false)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
join my server
|
||||||
|
join my srv
|
||||||
|
join my sv
|
||||||
|
join svr
|
||||||
|
join my serv
|
||||||
|
join our server
|
||||||
|
join our sv
|
||||||
|
join our serv
|
||||||
|
join our svr
|
||||||
|
can you join
|
||||||
|
can u join
|
||||||
|
join please
|
||||||
|
join pls
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
scum
|
||||||
|
idiot
|
||||||
|
stupid
|
||||||
|
dumb
|
||||||
|
moron
|
||||||
|
fool
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
sex
|
||||||
|
esex
|
||||||
|
give head
|
||||||
|
gave head
|
||||||
|
give me head
|
||||||
|
giv me head
|
||||||
|
giv head
|
||||||
|
gv head
|
||||||
|
gv me head
|
||||||
|
suck me of
|
||||||
|
suck him of
|
||||||
|
suck your mom
|
||||||
|
suck your mum
|
||||||
|
suck yr mom
|
||||||
|
suck ur mom
|
||||||
|
suck ur mum
|
||||||
|
suck yr mum
|
||||||
|
suck your dad
|
||||||
|
suck your daddy
|
||||||
|
suck yr dad
|
||||||
|
suck ur daddy
|
||||||
|
suck ur dad
|
||||||
|
suck yr daddy
|
||||||
|
suck my dick
|
||||||
|
suck my dih
|
||||||
|
stick it in your dad
|
||||||
|
stick it in ur dad
|
||||||
|
stick it in yr dad
|
||||||
|
stick it in your mom
|
||||||
|
stick it in ur mom
|
||||||
|
stick it in yr mom
|
||||||
|
stick it in your pussy
|
||||||
|
stick it in ur pussy
|
||||||
|
stick it in yr pussy
|
||||||
|
cock
|
||||||
|
dick
|
||||||
|
dih
|
||||||
|
pussy
|
||||||
|
dck
|
||||||
|
psy
|
||||||
|
pu-c
|
||||||
|
eyp
|
||||||
|
emp
|
||||||
|
smd
|
||||||
|
cum
|
||||||
|
cums
|
||||||
|
cuming
|
||||||
|
cumin
|
||||||
|
gang bang
|
||||||
|
squirt
|
||||||
|
squirting
|
||||||
|
squirts
|
||||||
|
bitch
|
||||||
|
bih
|
||||||
|
bich
|
||||||
|
ho
|
||||||
|
hoe
|
||||||
|
huz
|
||||||
|
slut
|
||||||
|
dldo
|
||||||
|
dildo
|
||||||
|
goon
|
||||||
|
gooning
|
||||||
|
i will fuh you
|
||||||
|
i will fuck you
|
||||||
|
i will fuh u
|
||||||
|
i will fuck u
|
||||||
|
nut
|
||||||
|
nuting
|
||||||
|
goonbeam
|
||||||
|
goonbeaming
|
||||||
|
gooncream
|
||||||
|
gooncreaming
|
||||||
|
boty hole
|
||||||
|
suck my pp
|
||||||
|
ass cracker
|
||||||
|
ass crack
|
||||||
|
fuck yourself
|
||||||
|
fuck urself
|
||||||
|
bbc
|
||||||
|
bwc
|
||||||
|
cumy
|
||||||
|
fap
|
||||||
|
faping
|
||||||
|
masturbate
|
||||||
|
masturbating
|
||||||
|
masterbating
|
||||||
|
masterbaiting
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
niga
|
||||||
|
niger
|
||||||
|
nig
|
||||||
|
nga
|
||||||
|
ngr
|
||||||
|
nger
|
||||||
|
qiga
|
||||||
|
niqa
|
||||||
|
nqa
|
||||||
|
nlger
|
||||||
|
digga
|
||||||
|
fag
|
||||||
|
fagot
|
||||||
|
fgot
|
||||||
|
fgt
|
||||||
|
paki
|
||||||
|
retard
|
||||||
|
rtrd
|
||||||
|
tard
|
||||||
|
dumbfuck
|
||||||
|
dumbfck
|
||||||
|
dumbfuk
|
||||||
|
dumbfuh
|
||||||
|
dumbfuhk
|
||||||
|
dmbfuck
|
||||||
|
dmbfck
|
||||||
|
dmbfuk
|
||||||
|
dmbfuh
|
||||||
|
dmbfuhk
|
||||||
|
dumb fuck
|
||||||
|
dumb fck
|
||||||
|
dumb fuk
|
||||||
|
dumb fuh
|
||||||
|
dumb fuhk
|
||||||
|
stfu
|
||||||
|
shut the fuck up
|
||||||
|
syfm
|
||||||
|
shut your fucking mouth
|
||||||
|
sybau
|
||||||
|
shut your bitch as up
|
||||||
|
shut your bih as up
|
||||||
|
gtfo
|
||||||
|
get the fuck out
|
||||||
|
pedo
|
||||||
|
pdf
|
||||||
|
pdfile
|
||||||
|
blackie
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fuck
|
||||||
|
fuckas
|
||||||
|
fvckas
|
||||||
|
fvck
|
||||||
|
fck
|
||||||
|
fkas
|
||||||
|
fuh
|
||||||
|
fuck you
|
||||||
|
f u
|
||||||
|
f you
|
||||||
|
fuck u
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
kys
|
||||||
|
kill yourself
|
||||||
|
kill urself
|
||||||
|
hang urself
|
||||||
|
hang yourself
|
||||||
|
gkys
|
||||||
|
gokys
|
||||||
|
keys
|
||||||
|
get raped
|
||||||
|
rape you
|
||||||
|
rape u
|
||||||
|
slit
|
||||||
|
grape
|
||||||
|
graped
|
||||||
|
throat slit
|
||||||
|
slit your throat
|
||||||
|
slit ur throat
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod events;
|
pub mod events;
|
||||||
|
pub(super) mod moderation;
|
||||||
|
|||||||
@@ -0,0 +1,333 @@
|
|||||||
|
//! Automated moderation for call messages.
|
||||||
|
//!
|
||||||
|
//! # Rules
|
||||||
|
//!
|
||||||
|
//! Payphone censors, allows, or blocks messages based on a predefined set of rule categories:
|
||||||
|
//!
|
||||||
|
//! | Nº | Message Type | Examples | Action |
|
||||||
|
//! |:--:|-----------------------------------------|-------------------------------------------------------|-----------------------------|
|
||||||
|
//! | 1 | Hate speech and slurs | "nigga", "faggot", "retard", "dumbfuck", racial slurs | Block immediately + penalty |
|
||||||
|
//! | 2 | Threats, self harm | "kill yourself", "die" | Block immediately + penalty |
|
||||||
|
//! | 3 | Targeted harassment | "kill @user", "die @user" | Block immediately + penalty |
|
||||||
|
//! | 4 | Explicit sexual language | "fuck you", "dick", "pussy" | Block immediately + penalty |
|
||||||
|
//! | 5 | Suggestive language | "slap your ass", "sexy" | Block immediately + penalty |
|
||||||
|
//! | 6 | Advertising | "discord.gg/payphone" | Block immediately + penalty |
|
||||||
|
//! | 7 | Sensitive topics and triggering content | "blood", "knife", "suicide method" | Block immediately + penalty |
|
||||||
|
//! | 8 | Non-english chats | | Block immediately + penalty |
|
||||||
|
//! | 9 | E-dating | "let me hit" | Block immediately + penalty |
|
||||||
|
//! | 10 | Mild swear words | "damn", "shit", "ass" | Soft censor + warn |
|
||||||
|
//! | 11 | Casual insults and teasing | "stupid", "dumb" | Soft censor + warn |
|
||||||
|
//! | 12 | Links | "https://google.com/" | Soft censor + warn |
|
||||||
|
//! | 13 | Spam and flooding | | Soft censor + warn |
|
||||||
|
//! | 14 | Normal conversations | "hey", "lol", "cool" | Allowed freely |
|
||||||
|
|
||||||
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
|
use linkify::LinkFinder;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use regex::Regex;
|
||||||
|
use twilight_model::gateway::payload::incoming::MessageCreate;
|
||||||
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
use unidecode::unidecode;
|
||||||
|
|
||||||
|
use crate::discord::sanitizing::SanitizeDiscordMarkdown;
|
||||||
|
|
||||||
|
pub fn escape_message(event: &MessageCreate) -> String {
|
||||||
|
let mut message = event.content.to_string();
|
||||||
|
|
||||||
|
for mention in &event.mentions {
|
||||||
|
message = message.replace(
|
||||||
|
&format!("<@{}>", mention.id),
|
||||||
|
&format!("**@{}**", mention.name.sanitize(true)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for mention in &event.mention_roles {
|
||||||
|
message = message.replace(&format!("<@&{mention}>"), "**@Role**");
|
||||||
|
}
|
||||||
|
|
||||||
|
for mention in &event.mention_channels {
|
||||||
|
message = message.replace(
|
||||||
|
&format!("<#{}>", mention.id),
|
||||||
|
&format!("**#{}**", mention.name.sanitize(true)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 normalized = normalize(message.clone());
|
||||||
|
|
||||||
|
log::debug!("Escaped and normalized message: {normalized:?}");
|
||||||
|
|
||||||
|
message.sanitize(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct NormalizedMessage {
|
||||||
|
/// The normalized message content.
|
||||||
|
message: String,
|
||||||
|
|
||||||
|
/// Mappings from original indices to normalized indices, or None if it was removed.
|
||||||
|
mappings: HashMap<(usize, usize), Option<(usize, usize)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn normalize(message: String) -> NormalizedMessage {
|
||||||
|
let replacements: HashMap<char, char> = [
|
||||||
|
('@', 'a'),
|
||||||
|
('0', 'o'),
|
||||||
|
('1', 'i'),
|
||||||
|
('2', 'z'),
|
||||||
|
('3', 'e'),
|
||||||
|
('4', 'a'),
|
||||||
|
('5', 's'),
|
||||||
|
('6', 'b'),
|
||||||
|
('7', 't'),
|
||||||
|
('8', 'g'),
|
||||||
|
('9', 'q'),
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut normalized = String::with_capacity(message.len());
|
||||||
|
let mut mappings = HashMap::new();
|
||||||
|
|
||||||
|
let mut last_i = 0;
|
||||||
|
|
||||||
|
for (i, mut ch) in message
|
||||||
|
.grapheme_indices(true)
|
||||||
|
.map(|(i, ch)| (i, ch.to_string()))
|
||||||
|
{
|
||||||
|
if let Some(&replacement) = replacements.get(&ch.chars().next().unwrap()) {
|
||||||
|
ch = replacement.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let unidecoded = unidecode(&ch);
|
||||||
|
|
||||||
|
let ui_start = normalized.len();
|
||||||
|
let ui_end = ui_start + unidecoded.len();
|
||||||
|
|
||||||
|
if unidecoded.chars().all(|c| c.is_ascii_alphanumeric()) {
|
||||||
|
// If the character is alphanumeric, we can keep it.
|
||||||
|
normalized.push_str(&unidecoded);
|
||||||
|
mappings.insert((last_i, i), Some((ui_start, ui_end)));
|
||||||
|
} else {
|
||||||
|
mappings.insert((last_i, i), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
last_i = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
NormalizedMessage {
|
||||||
|
message: normalized,
|
||||||
|
mappings,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn regex_from_phrase(phrase: &str) -> String {
|
||||||
|
let mut pattern = String::new();
|
||||||
|
|
||||||
|
for ch in phrase.replace(" ", "").chars() {
|
||||||
|
pattern.push_str(&format!("{ch}+\\s*"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let pattern = format!("\\b{pattern}\\b");
|
||||||
|
|
||||||
|
pattern
|
||||||
|
}
|
||||||
|
|
||||||
|
fn regex_from_phrases_list(phrases: &[&str]) -> Regex {
|
||||||
|
let mut pattern = String::new();
|
||||||
|
|
||||||
|
for phrase in phrases {
|
||||||
|
if !pattern.is_empty() {
|
||||||
|
pattern.push('|');
|
||||||
|
}
|
||||||
|
|
||||||
|
pattern.push_str(&format!("({})", regex_from_phrase(phrase)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Regex::new(&pattern).expect("Failed to compile regex")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn analyze_message(message: &str) -> Vec<(Vec<String>, Arc<dyn Rule>)> {
|
||||||
|
let mut results = Vec::new();
|
||||||
|
|
||||||
|
for rule in RULES.iter() {
|
||||||
|
let detections = rule.detect(message);
|
||||||
|
if !detections.is_empty() {
|
||||||
|
results.push((detections, rule.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn censor_message(original: &str, normalized: NormalizedMessage, analysis: &[(Vec<String>, Arc<dyn Rule>)]) -> String {
|
||||||
|
let mut censored = String::with_capacity(original.len());
|
||||||
|
|
||||||
|
todo!();
|
||||||
|
|
||||||
|
censored
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
|
pub enum Action {
|
||||||
|
BlockAndPenalty,
|
||||||
|
SoftCensorAndWarn,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Rule: Send + Sync {
|
||||||
|
fn name(&self) -> &'static str;
|
||||||
|
fn action(&self) -> &'static Action;
|
||||||
|
fn detect(&self, message: &str) -> Vec<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_phrases_file(file_content: &str) -> Vec<&str> {
|
||||||
|
file_content
|
||||||
|
.lines()
|
||||||
|
.filter_map(|line| {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if !trimmed.is_empty() && !trimmed.starts_with("//") {
|
||||||
|
Some(trimmed)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HateSpeechAndSlursRule(Regex);
|
||||||
|
|
||||||
|
impl HateSpeechAndSlursRule {
|
||||||
|
fn new() -> Self {
|
||||||
|
let phrases: Vec<&str> =
|
||||||
|
parse_phrases_file(include_str!("lists/hate-speech-and-slurs.txt"));
|
||||||
|
|
||||||
|
let regex = regex_from_phrases_list(&phrases);
|
||||||
|
|
||||||
|
Self(regex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rule for HateSpeechAndSlursRule {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"Hate Speech and Slurs"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn action(&self) -> &'static Action {
|
||||||
|
&Action::BlockAndPenalty
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detect(&self, message: &str) -> Vec<String> {
|
||||||
|
self.0
|
||||||
|
.find_iter(message)
|
||||||
|
.map(|m| message[m.start()..m.end()].to_string())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ThreatsAndSelfHarmRule(Regex);
|
||||||
|
|
||||||
|
impl ThreatsAndSelfHarmRule {
|
||||||
|
fn new() -> Self {
|
||||||
|
let phrases: Vec<&str> =
|
||||||
|
parse_phrases_file(include_str!("lists/threats-and-self-harm.txt"));
|
||||||
|
|
||||||
|
let regex = regex_from_phrases_list(&phrases);
|
||||||
|
|
||||||
|
Self(regex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rule for ThreatsAndSelfHarmRule {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"Threats and Self Harm"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn action(&self) -> &'static Action {
|
||||||
|
&Action::BlockAndPenalty
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detect(&self, message: &str) -> Vec<String> {
|
||||||
|
self.0
|
||||||
|
.find_iter(message)
|
||||||
|
.map(|m| message[m.start()..m.end()].to_string())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ExplicitSexualLanguageRule(Regex);
|
||||||
|
|
||||||
|
impl ExplicitSexualLanguageRule {
|
||||||
|
fn new() -> Self {
|
||||||
|
let phrases: Vec<&str> =
|
||||||
|
parse_phrases_file(include_str!("lists/explicit-sexual-language.txt"));
|
||||||
|
|
||||||
|
let regex = regex_from_phrases_list(&phrases);
|
||||||
|
|
||||||
|
Self(regex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rule for ExplicitSexualLanguageRule {
|
||||||
|
fn action(&self) -> &'static Action {
|
||||||
|
&Action::BlockAndPenalty
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"Explicit Sexual Language"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detect(&self, message: &str) -> Vec<String> {
|
||||||
|
self.0
|
||||||
|
.find_iter(message)
|
||||||
|
.map(|m| message[m.start()..m.end()].to_string())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SuggestiveLanguageRule(Regex);
|
||||||
|
|
||||||
|
impl SuggestiveLanguageRule {
|
||||||
|
fn new() -> Self {
|
||||||
|
let phrases: Vec<&str> = parse_phrases_file(include_str!("lists/suggestive-language.txt"));
|
||||||
|
|
||||||
|
let regex = regex_from_phrases_list(&phrases);
|
||||||
|
|
||||||
|
Self(regex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rule for SuggestiveLanguageRule {
|
||||||
|
fn action(&self) -> &'static Action {
|
||||||
|
&Action::BlockAndPenalty
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"Suggestive Language"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detect(&self, message: &str) -> Vec<String> {
|
||||||
|
self.0
|
||||||
|
.find_iter(message)
|
||||||
|
.map(|m| message[m.start()..m.end()].to_string())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub static RULES: Lazy<Vec<Arc<dyn Rule>>> = Lazy::new(|| {
|
||||||
|
vec![
|
||||||
|
Arc::new(HateSpeechAndSlursRule::new()),
|
||||||
|
Arc::new(ThreatsAndSelfHarmRule::new()),
|
||||||
|
Arc::new(SuggestiveLanguageRule::new()),
|
||||||
|
Arc::new(ExplicitSexualLanguageRule::new()),
|
||||||
|
]
|
||||||
|
});
|
||||||
+21
-10
@@ -100,7 +100,12 @@
|
|||||||
//! $ cargo run -- docs --open
|
//! $ cargo run -- docs --open
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use std::{net::SocketAddr, path::Path, process::Command, sync::mpsc};
|
use std::{
|
||||||
|
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||||
|
path::Path,
|
||||||
|
process::Command,
|
||||||
|
sync::mpsc,
|
||||||
|
};
|
||||||
|
|
||||||
use axum::{Router, response::Redirect, routing};
|
use axum::{Router, response::Redirect, routing};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
@@ -150,8 +155,8 @@ pub async fn main() {
|
|||||||
} => {
|
} => {
|
||||||
start(token, database_addr).await;
|
start(token, database_addr).await;
|
||||||
}
|
}
|
||||||
Commands::Docs { port, open } => {
|
Commands::Docs { bind, open } => {
|
||||||
docs(port, open).await;
|
docs(bind, open).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,7 +195,7 @@ async fn start(token: String, database_addr: String) {
|
|||||||
bot.run(&token).await.expect("Could not run bot!");
|
bot.run(&token).await.expect("Could not run bot!");
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn docs(port: u16, open_on_start: bool) {
|
async fn docs(bind: SocketAddr, open_on_start: bool) {
|
||||||
log::info!("Compiling documentation...");
|
log::info!("Compiling documentation...");
|
||||||
|
|
||||||
Command::new("cargo")
|
Command::new("cargo")
|
||||||
@@ -229,7 +234,7 @@ async fn docs(port: u16, open_on_start: bool) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
log::info!("Starting documentation server on port {port}...");
|
log::info!("Starting documentation server on address {bind}...");
|
||||||
|
|
||||||
let docs: Router<()> = Router::new()
|
let docs: Router<()> = Router::new()
|
||||||
.route(
|
.route(
|
||||||
@@ -241,11 +246,17 @@ async fn docs(port: u16, open_on_start: bool) {
|
|||||||
"/target/doc/"
|
"/target/doc/"
|
||||||
)));
|
)));
|
||||||
|
|
||||||
let listener = TcpListener::bind(SocketAddr::from(([0, 0, 0, 0], port)))
|
let openable_address = if bind.ip() == IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)) {
|
||||||
.await
|
format!("http://localhost:{}", bind.port())
|
||||||
.expect("Could not bind to port!");
|
} else {
|
||||||
|
format!("http://{bind}/")
|
||||||
|
};
|
||||||
|
|
||||||
log::info!("Documentation server is running on http://localhost:{port}/");
|
let listener = TcpListener::bind(bind)
|
||||||
|
.await
|
||||||
|
.expect("Could not bind to address!");
|
||||||
|
|
||||||
|
log::info!("Documentation server is running on {openable_address}");
|
||||||
|
|
||||||
let handle = tokio::spawn(async move {
|
let handle = tokio::spawn(async move {
|
||||||
axum::serve(listener, docs)
|
axum::serve(listener, docs)
|
||||||
@@ -254,7 +265,7 @@ async fn docs(port: u16, open_on_start: bool) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if open_on_start {
|
if open_on_start {
|
||||||
match open::that(format!("http://localhost:{port}/")) {
|
match open::that(openable_address) {
|
||||||
Ok(_) => log::info!("Opened documentation in browser!"),
|
Ok(_) => log::info!("Opened documentation in browser!"),
|
||||||
Err(e) => log::error!("Failed to open documentation in browser: {e}"),
|
Err(e) => log::error!("Failed to open documentation in browser: {e}"),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user