diff --git a/src/main/java/net/viper/status/modules/chat/ChatFilter.java b/src/main/java/net/viper/status/modules/chat/ChatFilter.java deleted file mode 100644 index 098829c..0000000 --- a/src/main/java/net/viper/status/modules/chat/ChatFilter.java +++ /dev/null @@ -1,331 +0,0 @@ -package net.viper.status.modules.chat; - -import net.viper.status.ratelimit.GlobalRateLimitFramework; - -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.regex.Pattern; - -/** - * Chat-Filter: Anti-Spam, Caps-Filter, Wort-Blacklist, Farbcode-Filter. - * - * Reihenfolge der Prüfungen in processChat(): - * 1. Spam-Cooldown (zu schnell geschrieben?) - * 2. Gleiche Nachricht wiederholt? - * 3. Zu viele Großbuchstaben? - * 4. Verbotene Wörter → ersetzen durch **** - * 5. Farbcodes (& Codes) → nur mit Permission erlaubt - */ -public class ChatFilter { - - private final ChatFilterConfig cfg; - private final GlobalRateLimitFramework rateLimiter = GlobalRateLimitFramework.getInstance(); - - // UUID → letzte Nachricht (für Duplikat-Check) - private final Map lastMessageText = new ConcurrentHashMap<>(); - - // Kompilierte Regex-Pattern für Blacklist-Wörter - private final List blacklistPatterns = new ArrayList<>(); - - public ChatFilter(ChatFilterConfig cfg) { - this.cfg = cfg; - compilePatterns(); - } - - private void compilePatterns() { - blacklistPatterns.clear(); - for (String word : cfg.blacklistWords) { - // Case-insensitiv, ganzes Wort oder Teilwort je nach Config - blacklistPatterns.add(Pattern.compile( - "(?i)" + Pattern.quote(word))); - } - } - - // ===== Ergebnis-Klasse ===== - - public enum FilterResult { - ALLOWED, // Nachricht darf durch - BLOCKED, // Nachricht blockiert (Spam/Flood) - MODIFIED // Nachricht wurde verändert (Wörter ersetzt / Caps reduziert) - } - - public static class FilterResponse { - public final FilterResult result; - public final String message; // ggf. modifizierte Nachricht - public final String denyReason; // Nachricht an den Spieler wenn BLOCKED - - FilterResponse(FilterResult result, String message, String denyReason) { - this.result = result; - this.message = message; - this.denyReason = denyReason; - } - } - - // ===== Haupt-Filtermethode ===== - - /** - * Wendet alle aktiven Filter auf eine Nachricht an. - * - * @param uuid UUID des sendenden Spielers - * @param message Originalnachricht - * @param isAdmin true → Farbcodes und Caps-Filter überspringen - * @param hasColorPerm true → &-Farbcodes erlaubt - * @param hasFormatPerm true → &l, &o etc. erlaubt - * @return FilterResponse mit Ergebnis und ggf. modifizierter Nachricht - */ - public FilterResponse filter(UUID uuid, String message, boolean isAdmin, - boolean hasColorPerm, boolean hasFormatPerm) { - - // ── 1. Spam-Cooldown ── - if (cfg.antiSpamEnabled && !isAdmin) { - if (cfg.globalRateLimitEnabled) { - GlobalRateLimitFramework.Result rl = rateLimiter.check( - "chat.message", - uuid.toString(), - new GlobalRateLimitFramework.Rule( - true, - cfg.globalRateLimitWindowMs, - cfg.globalRateLimitMaxActions, - cfg.globalRateLimitBlockMs - ) - ); - if (rl.isBlocked()) { - return new FilterResponse(FilterResult.BLOCKED, message, cfg.spamMessage); - } - } - } - - // ── 2. Duplikat-Check ── - if (cfg.duplicateCheckEnabled && !isAdmin) { - String lastText = lastMessageText.get(uuid); - if (message.equalsIgnoreCase(lastText)) { - return new FilterResponse(FilterResult.BLOCKED, message, cfg.duplicateMessage); - } - lastMessageText.put(uuid, message); - } - - String result = message; - boolean modified = false; - - // ── 3. Blacklist ── - if (cfg.blacklistEnabled) { - String filtered = applyBlacklist(result); - if (!filtered.equals(result)) { - result = filtered; - modified = true; - } - } - - // ── 4. Caps-Filter ── - if (cfg.capsFilterEnabled && !isAdmin) { - String capped = applyCapsFilter(result); - if (!capped.equals(result)) { - result = capped; - modified = true; - } - } - - // ── 5. Farbcodes filtern (nur wenn keine Permission) ── - if (!isAdmin) { - String colorFiltered = applyColorFilter(result, hasColorPerm, hasFormatPerm); - if (!colorFiltered.equals(result)) { - result = colorFiltered; - modified = true; - } - } - - // ── 6. Anti-Werbung ── - if (cfg.antiAdEnabled && !isAdmin) { - if (containsAdvertisement(result)) { - return new FilterResponse(FilterResult.BLOCKED, result, cfg.antiAdMessage); - } - } - - return new FilterResponse( - modified ? FilterResult.MODIFIED : FilterResult.ALLOWED, - result, - null - ); - } - - // ===== Einzelne Filter ===== - - private String applyBlacklist(String message) { - String result = message; - for (Pattern p : blacklistPatterns) { - result = p.matcher(result).replaceAll(buildStars(p.pattern() - .replace("(?i)", "").replace("\\Q", "").replace("\\E", "").length())); - } - return result; - } - - private String applyCapsFilter(String message) { - // Zähle Großbuchstaben - int total = 0, upper = 0; - for (char c : message.toCharArray()) { - if (Character.isLetter(c)) { total++; if (Character.isUpperCase(c)) upper++; } - } - if (total < cfg.capsMinLength) return message; // Kurze Nachrichten ignorieren - double ratio = total > 0 ? (double) upper / total : 0; - if (ratio < cfg.capsMaxPercent / 100.0) return message; - // Zu viele Caps → alles lowercase - return message.toLowerCase(); - } - - /** - * Entfernt &-Farbcodes je nach Permission. - * hasColorPerm → &0-&9, &a-&f erlaubt - * hasFormatPerm → &l, &o, &n, &m, &k erlaubt - * Beide false → alle &-Codes entfernen - */ - private String applyColorFilter(String message, boolean hasColorPerm, boolean hasFormatPerm) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < message.length(); i++) { - char c = message.charAt(i); - if (c == '&' && i + 1 < message.length()) { - char next = Character.toLowerCase(message.charAt(i + 1)); - boolean isColor = (next >= '0' && next <= '9') || (next >= 'a' && next <= 'f'); - boolean isFormat = "lonmkr".indexOf(next) >= 0; - boolean isHex = next == '#'; - - if (isColor && hasColorPerm) { sb.append(c); continue; } - if (isFormat && hasFormatPerm) { sb.append(c); continue; } - if (isHex && hasColorPerm) { sb.append(c); continue; } - - // Kein Recht → & und nächstes Zeichen überspringen - if (isColor || isFormat) { i++; continue; } - // Hex: &# + 6 Zeichen überspringen (i zeigt auf &, +1 = #, +2..+7 = RRGGBB) - if (isHex && i + 7 <= message.length()) { i += 7; continue; } - } - sb.append(c); - } - return sb.toString(); - } - - // ===== Anti-Werbung ===== - - // Vorkompilierte Patterns (einmalig beim Classload) - private static final Pattern PATTERN_IP = - Pattern.compile("\\b(\\d{1,3}[.,]){3}\\d{1,3}(:\\d{1,5})?\\b"); - - private static final Pattern PATTERN_DOMAIN_GENERIC = - Pattern.compile("(?i)\\b[a-z0-9-]{2,63}\\.[a-z]{2,10}(?:[/:\\d]\\S*)?\\b"); - - private static final Pattern PATTERN_URL_PREFIX = - Pattern.compile("(?i)(https?://|www\\.)\\S+"); - - /** - * Prüft ob die Nachricht Werbung enthält (IP, URL, fremde Domain). - * Domains auf der Whitelist werden ignoriert. - * - * Erkennt: - * - http:// / https:// / www. Prefixe - * - IPv4-Adressen (auch mit Port) - * - Domain-Namen mit konfigurierten TLDs (z.B. .net, .de, .com) - * - Verschleierungsversuche mit Leerzeichen um Punkte ("play . server . net") - */ - private boolean containsAdvertisement(String message) { - // Normalisierung: "play . server . net" → "play.server.net" - String normalized = message.replaceAll("\\s*\\.\\s*", "."); - - // 1. Explizite URL-Prefixe - if (PATTERN_URL_PREFIX.matcher(normalized).find()) { - return !allMatchesWhitelisted(normalized, PATTERN_URL_PREFIX); - } - - // 2. IP-Adressen (werden nie whitelisted) - if (PATTERN_IP.matcher(normalized).find()) { - return true; - } - - // 3. Domains mit bekannten TLDs - if (!cfg.antiAdBlockedTlds.isEmpty()) { - java.util.regex.Matcher m = PATTERN_DOMAIN_GENERIC.matcher(normalized); - while (m.find()) { - String match = m.group(); - String tld = extractTld(match); - if (cfg.antiAdBlockedTlds.contains(tld.toLowerCase())) { - if (!isOnWhitelist(match)) return true; - } - } - } - - return false; - } - - /** true wenn ALLE Treffer des Patterns auf der Whitelist stehen. */ - private boolean allMatchesWhitelisted(String message, Pattern pattern) { - java.util.regex.Matcher m = pattern.matcher(message); - while (m.find()) { - if (!isOnWhitelist(m.group())) return false; - } - return true; - } - - private boolean isOnWhitelist(String match) { - String lower = match.toLowerCase(); - for (String entry : cfg.antiAdWhitelist) { - if (lower.contains(entry.toLowerCase())) return true; - } - return false; - } - - private static String extractTld(String domain) { - String clean = domain.split("[/:]")[0]; - int dot = clean.lastIndexOf('.'); - return dot >= 0 ? clean.substring(dot + 1) : ""; - } - - private static String buildStars(int length) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < Math.max(length, 4); i++) sb.append('*'); - return sb.toString(); - } - - // ===== Cleanup beim Logout ===== - - public void cleanup(UUID uuid) { - lastMessageText.remove(uuid); - rateLimiter.clearActor(uuid.toString()); - } - - // ===== Konfigurationsklasse ===== - - public static class ChatFilterConfig { - // Anti-Spam - public boolean antiSpamEnabled = true; - public long spamCooldownMs = 1500; // Legacy-Feld fuer Kompatibilitaet - public int spamMaxMessages = 3; // Legacy-Feld fuer Kompatibilitaet - public String spamMessage = "&cBitte nicht so schnell schreiben!"; - - // Globales Rate-Limit-Framework - public boolean globalRateLimitEnabled = true; - public long globalRateLimitWindowMs = 2500; - public int globalRateLimitMaxActions = 3; - public long globalRateLimitBlockMs = 6000; - - // Duplikat - public boolean duplicateCheckEnabled = true; - public String duplicateMessage = "&cBitte keine identischen Nachrichten senden."; - - // Blacklist - public boolean blacklistEnabled = true; - public List blacklistWords = new ArrayList<>(); - - // Caps - public boolean capsFilterEnabled = true; - public int capsMinLength = 6; // Mindestlänge für Caps-Check - public int capsMaxPercent = 70; // Max. % Großbuchstaben - - // Anti-Werbung - public boolean antiAdEnabled = true; - public String antiAdMessage = "&cWerbung ist in diesem Chat nicht erlaubt!"; - // Domains/Substrings die NICHT geblockt werden (z.B. eigene Serveradresse) - public List antiAdWhitelist = new ArrayList<>(); - // TLDs die als Werbung gewertet werden (leer = alle TLDs prüfen) - public List antiAdBlockedTlds = new ArrayList<>(Arrays.asList( - "net", "com", "de", "org", "gg", "io", "eu", "tv", "xyz", - "info", "me", "cc", "co", "app", "online", "site", "fun" - )); - } -} \ No newline at end of file