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; #[derive(Deserialize, Serialize, Debug)] struct CommentPayload { nickname: String, message: String, } static BAD_WORDS_REGEX: LazyLock = LazyLock::new(|| { Regex::new(r"(?i)(хуй|хуя|хуе|хул|пизд|ебат|ебан|ебл|ебуч|бляд|блят|шлюх|сука|сучк|жоп|пидор|пидар|педик|голубо|гондон|гандон|залуп|мудак|мудил|мраз|уеб|чмо|долбое|даун|чурк|нигер|нигга|хач|хохол|кацап|жид|порно|наркот|убить|насилие|соли|закладк|мефедрон|гашиш|спайс|heroin|meth|murder|gore|cpam|scam|crypto|binance|bitco)").unwrap() }); 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 = None; let lower_input = input.to_lowercase(); for ch in lower_input.chars() { // СОХРАНЯЕМ ПРОБЕЛЫ, чтобы не склеивать соседние слова ("нас укачало" != "насукачало") if ch.is_whitespace() { normalized.push(' '); prev_char = Some(' '); continue; } // Вырезаем пунктуацию (чтобы ловить х.у.й), но пропускаем дальше только буквы/цифры if !ch.is_alphanumeric() { continue; } // Замена "leetspeak" (англо-русская подмена и цифры) let mapped = match ch { 'a' | '@' => 'а', 'b' => 'в', 'c' => 'с', 'e' | '3' => 'е', 'o' | '0' => 'о', 'p' => 'р', 'x' => 'х', 'y' => 'у', 'm' => 'м', 't' => 'т', 'k' => 'к', 'h' => 'н', '4' => 'ч', _ => ch, }; // Сжатие повторяющихся символов (хххуууууууй -> хуй) // Не сжимаем пробелы, только буквы if let Some(p) = prev_char { if p == mapped && mapped != ' ' { continue; } } 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; if input.len() > 10 && uppercase_ratio > 0.7 { return true; } // Проверка на зажатую клавиатуру (aaaaaaaaaaaa) for word in input.split_whitespace() { // Игнорируем длинные ссылки (http/https) if word.len() > 35 && !word.starts_with("http") { 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) -> 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); // Сначала защищаем нормальные слова, затем проверяем регуляркой 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, safe_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.") } } }