rust gateway for cms and comments filter

This commit is contained in:
2026-04-24 03:33:53 +07:00
parent e8b9eb5c52
commit 84f58220d2
14 changed files with 8326 additions and 104 deletions
+159
View File
@@ -0,0 +1,159 @@
use axum::{Json, Router, http::StatusCode, response::IntoResponse, routing::post};
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 {
nickname: String,
message: String,
}
// Расширенный список КОРНЕЙ. Регулярка будет искать эти корни внутри слов.
// Используем \p{L}* (любые буквы) вокруг корня, чтобы ловить склонения.
static BAD_WORDS_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)(хуй|хуя|пизд|ебат|ебан|бляд|блят|шлюх|сука|сучк|порно|наркот|убить|насилие|heroin|meth|murder|gore|соли|закладк|мефедрон|гашиш)").unwrap()
});
// Функция жесткой нормализации текста перед проверкой
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(' ');
// Пропускаем символы, которые не являются буквами или цифрами (запятые, точки, пробелы)
if !ch.is_alphanumeric() {
continue;
}
// 3. Замена "leetspeak" и похожих английских букв на русские (базовая защита)
ch = match ch {
'a' | '@' => 'а',
'b' => 'в',
'c' => 'с',
'e' | '3' => 'е',
'o' | '0' => 'о',
'p' => 'р',
'x' => 'х',
'y' => 'у',
'm' => 'м',
't' => 'т',
'k' => 'к',
_ => ch,
};
// 4. Сжатие повторяющихся символов (хххуууууууй -> хуй)
if let Some(p) = prev_char {
if p == ch {
continue; // Пропускаем дубликат
}
}
normalized.push(ch);
prev_char = Some(ch);
}
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;
}
// Считаем процент заглавных букв
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)
for word in input.split_whitespace() {
if word.len() > 35 {
// Нормальных слов длиннее 35 символов почти нет
return true;
}
}
false
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/api/comments", post(create_comment));
println!("🚀 Netrunner Gateway started on 0.0.0.0:3001");
let listener = tokio::net::TcpListener::bind("0.0.0.0:3001").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn create_comment(Json(payload): Json<CommentPayload>) -> impl IntoResponse {
// --- 1. Проверка на спам (КАПС, длинные слова) ---
if is_spam(&payload.message) || is_spam(&payload.nickname) {
println!("🗑️ Blocked by SPAM filter: {}", payload.nickname);
return (
StatusCode::NOT_ACCEPTABLE,
"Transmission rejected: Spam pattern detected.",
);
}
// --- 2. Английский фильтр (Crate rustrict) ---
if payload.message.is_inappropriate() || payload.nickname.is_inappropriate() {
println!("🛡️ Blocked by rustrict: {}", payload.nickname);
return (
StatusCode::NOT_ACCEPTABLE,
"Transmission rejected: Policy violation.",
);
}
// --- 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) {
println!(
"🛡️ Blocked by Deep Regex: [Original: {}] -> [Normalized: {}]",
payload.message, normalized_msg
);
return (
StatusCode::NOT_ACCEPTABLE,
"Transmission rejected: Policy violation.",
);
}
// --- 4. ОТПРАВКА В POCKETBASE ---
let pb_base_url =
std::env::var("PB_INTERNAL_URL").unwrap_or_else(|_| "http://localhost:8090".to_string());
let pb_url = format!("{}/api/collections/comments/records", pb_base_url);
let client = Client::new();
let pb_response = client.post(pb_url).json(&payload).send().await;
match pb_response {
Ok(res) if res.status().is_success() => {
(StatusCode::OK, "Transmission secured and delivered.")
}
Ok(res) => {
println!("⚠️ PocketBase rejected: {:?}", res.status());
(StatusCode::BAD_REQUEST, "Database rejected transmission.")
}
Err(e) => {
println!("💥 PB Connection error: {}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Uplink offline.")
}
}
}