filter update, hero optimization

This commit is contained in:
2026-04-24 12:10:38 +07:00
parent 487e75a405
commit 8bf72addb2
6 changed files with 281 additions and 115 deletions
+60 -27
View File
@@ -1,10 +1,9 @@
use axum::{Json, Router, http::StatusCode, response::IntoResponse, routing::post};
use axum::{http::StatusCode, response::IntoResponse, routing::post, Json, Router};
use regex::Regex;
use reqwest::Client;
use rustrict::CensorStr;
use serde::{Deserialize, Serialize};
use std::sync::LazyLock;
use unicode_segmentation::UnicodeSegmentation;
#[derive(Deserialize, Serialize, Debug)]
struct CommentPayload {
@@ -12,31 +11,59 @@ struct CommentPayload {
message: String,
}
// Расширенный список КОРНЕЙ. Регулярка будет искать эти корни внутри слов.
// Используем \p{L}* (любые буквы) вокруг корня, чтобы ловить склонения.
// 1. Расширенный список КОРНЕЙ.
static BAD_WORDS_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)(хуй|хуя|пизд|ебат|ебан|бляд|блят|шлюх|сука|сучк|порно|наркот|убить|насилие|heroin|meth|murder|gore|соли|закладк|мефедрон|гашиш)").unwrap()
Regex::new(r"(?i)(хуй|хуя|хуе|пизд|ебат|ебан|ебл|бляд|блят|шлюх|сука|сучк|порно|наркот|убить|насилие|heroin|meth|murder|gore|соли|закладк|мефедрон|гашиш|спайс|cpam|scam|crypto|binance)").unwrap()
});
// 2. БЕЛЫЙ СПИСОК (Исключения). Защищает нормальные слова, содержащие плохие корни.
const SAFE_WORDS: &[&str] = &[
"колебат",
"оскорблят",
"употреблят",
"расслаблят",
"потреблят",
"страхуй",
"рубл",
"сабл",
"хлебат",
"греб",
"истреб",
"щебет",
"мебел",
];
// Заменяет легальные слова-исключения на плейсхолдер перед проверкой
fn apply_allowlist(text: &str) -> String {
let mut safe_text = text.to_string();
for &safe_word in SAFE_WORDS {
safe_text = safe_text.replace(safe_word, "[SAFE]");
}
safe_text
}
// Функция жесткой нормализации текста перед проверкой
fn normalize_for_filter(input: &str) -> String {
let mut normalized = String::new();
let mut prev_char: Option<char> = None;
// 1. Приводим к нижнему регистру
let lower_input = input.to_lowercase();
// 2. Идем по символам (с учетом юникода)
for g in lower_input.graphemes(true) {
let mut ch = g.chars().next().unwrap_or(' ');
for ch in lower_input.chars() {
// СОХРАНЯЕМ ПРОБЕЛЫ, чтобы не склеивать соседние слова ("нас укачало" != "насукачало")
if ch.is_whitespace() {
normalized.push(' ');
prev_char = Some(' ');
continue;
}
// Пропускаем символы, которые не являются буквами или цифрами (запятые, точки, пробелы)
// Вырезаем пунктуацию (чтобы ловить х.у.й), но пропускаем дальше только буквы/цифры
if !ch.is_alphanumeric() {
continue;
}
// 3. Замена "leetspeak" и похожих английских букв на русские (базовая защита)
ch = match ch {
// Замена "leetspeak" (англо-русская подмена и цифры)
let mapped = match ch {
'a' | '@' => 'а',
'b' => 'в',
'c' => 'с',
@@ -48,43 +75,45 @@ fn normalize_for_filter(input: &str) -> String {
'm' => 'м',
't' => 'т',
'k' => 'к',
'h' => 'н',
'4' => 'ч',
_ => ch,
};
// 4. Сжатие повторяющихся символов (хххуууууууй -> хуй)
// Сжатие повторяющихся символов (хххуууууууй -> хуй)
// Не сжимаем пробелы, только буквы
if let Some(p) = prev_char {
if p == ch {
continue; // Пропускаем дубликат
if p == mapped && mapped != ' ' {
continue;
}
}
normalized.push(ch);
prev_char = Some(ch);
normalized.push(mapped);
prev_char = Some(mapped);
}
normalized
}
// Функция проверки на спам (КАПС или слишком длинные слова)
// Функция проверки на спам (КАПС или слишком длинные бессмысленные слова)
fn is_spam(input: &str) -> bool {
let alpha_chars: String = input.chars().filter(|c| c.is_alphabetic()).collect();
if alpha_chars.is_empty() {
return false;
}
// Считаем процент заглавных букв
// Проверка на КАПС (> 70% заглавных при длине > 10)
let uppercase_count = alpha_chars.chars().filter(|c| c.is_uppercase()).count();
let uppercase_ratio = uppercase_count as f64 / alpha_chars.len() as f64;
// Если больше 70% текста - КАПС (при длине текста больше 10 символов)
if input.len() > 10 && uppercase_ratio > 0.7 {
return true;
}
// Проверка на слишком длинные "слова" без пробелов (защита от aaaaaaaaaaaaaa)
// Проверка на зажатую клавиатуру (aaaaaaaaaaaa)
for word in input.split_whitespace() {
if word.len() > 35 {
// Нормальных слов длиннее 35 символов почти нет
// Игнорируем длинные ссылки (http/https)
if word.len() > 35 && !word.starts_with("http") {
return true;
}
}
@@ -102,7 +131,7 @@ async fn main() {
}
async fn create_comment(Json(payload): Json<CommentPayload>) -> impl IntoResponse {
// --- 1. Проверка на спам (КАПС, длинные слова) ---
// --- 1. Проверка на спам ---
if is_spam(&payload.message) || is_spam(&payload.nickname) {
println!("🗑️ Blocked by SPAM filter: {}", payload.nickname);
return (
@@ -120,14 +149,18 @@ async fn create_comment(Json(payload): Json<CommentPayload>) -> impl IntoRespons
);
}
// --- 3. Русский хардкорный фильтр ---
// --- 3. Русский хардкорный фильтр с защитой от ложных срабатываний ---
let normalized_msg = normalize_for_filter(&payload.message);
let normalized_nick = normalize_for_filter(&payload.nickname);
if BAD_WORDS_REGEX.is_match(&normalized_msg) || BAD_WORDS_REGEX.is_match(&normalized_nick) {
// Сначала защищаем нормальные слова, затем проверяем регуляркой
let safe_msg = apply_allowlist(&normalized_msg);
let safe_nick = apply_allowlist(&normalized_nick);
if BAD_WORDS_REGEX.is_match(&safe_msg) || BAD_WORDS_REGEX.is_match(&safe_nick) {
println!(
"🛡️ Blocked by Deep Regex: [Original: {}] -> [Normalized: {}]",
payload.message, normalized_msg
payload.message, safe_msg
);
return (
StatusCode::NOT_ACCEPTABLE,