diff --git a/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/ChatFilter.java b/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/ChatFilter.java new file mode 100644 index 0000000..098829c --- /dev/null +++ b/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/ChatFilter.java @@ -0,0 +1,331 @@ +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