rust gateway for cms and comments filter
This commit is contained in:
@@ -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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user