From cfadfc540edb019ee61b339f0d075b9d4050307a Mon Sep 17 00:00:00 2001 From: M_Viper Date: Thu, 7 May 2026 19:39:42 +0000 Subject: [PATCH] Soft-delete copy _trash/2026-05-07T19-39-34-009Z/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/ChatModule.java --- .../viper/status/modules/chat/ChatModule.java | 1400 +++++++++++++++++ 1 file changed, 1400 insertions(+) create mode 100644 _trash/2026-05-07T19-39-34-009Z/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/ChatModule.java diff --git a/_trash/2026-05-07T19-39-34-009Z/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/ChatModule.java b/_trash/2026-05-07T19-39-34-009Z/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/ChatModule.java new file mode 100644 index 0000000..05711b6 --- /dev/null +++ b/_trash/2026-05-07T19-39-34-009Z/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/ChatModule.java @@ -0,0 +1,1400 @@ +package net.viper.status.modules.chat; + +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.api.chat.*; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.event.ChatEvent; +import net.md_5.bungee.api.event.PlayerDisconnectEvent; +import net.md_5.bungee.api.event.PostLoginEvent; +import net.md_5.bungee.api.plugin.Command; +import net.md_5.bungee.api.plugin.Listener; +import net.md_5.bungee.api.plugin.Plugin; +import net.md_5.bungee.event.EventHandler; +import net.md_5.bungee.event.EventPriority; +import net.viper.status.module.Module; +import net.viper.status.modules.chat.bridge.DiscordBridge; +import net.viper.status.modules.chat.bridge.TelegramBridge; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +/** + * ChatModule für StatusAPI (BungeeCord) + * + * Features: + * ✅ Mehrere Kanäle mit Permissions + * ✅ Server-Erkennung im Chat-Format + * ✅ /helpop für Spieler + * ✅ Emoji-Unterstützung (:smile: → 😊) + * ✅ Admin-Mute / Spieler-eigener Chat-Mute + * ✅ CMI & Plugin-kompatibel (kein Eingriff in SubServer-Befehle) + * ✅ Privat-Nachrichten (/msg, /r) + * ✅ Spieler-Blocking (/ignore, /unignore) + * ✅ Discord & Telegram Integration + * ✅ Admin-Bypass (kann nicht gemutet/geblockt werden) + * ✅ /broadcast für Admins + * ✅ Secure-Chat kompatibel (1.19+ & Bedrock/Geyser) + * ✅ Chat-Log mit konfigurierbarer Aufbewahrung (7 / 14 Tage) + * ✅ Nachrichten-IDs (klickbar → Zwischenablage) + * ✅ Report-System (/report, /reports, /reportclose) + * ✅ Admin-Benachrichtigung bei Reports (sofort oder verzögert nach Login) + * ✅ Server-Farben pro Server (&-Codes und &#RRGGBB HEX) + * ✅ Join / Leave Nachrichten (mit Vanish-Support) + * ✅ BungeeCord-Vanish-Integration via VanishProvider + */ +public class ChatModule implements Module, Listener { + + private Plugin plugin; + private Logger logger; + + private ChatConfig config; + private MuteManager muteManager; + private BlockManager blockManager; + private PrivateMsgManager pmManager; + private EmojiParser emojiParser; + private ChatFilter chatFilter; + private DiscordBridge discordBridge; + private TelegramBridge telegramBridge; + private ChatLogger chatLogger; + private ReportManager reportManager; + private AccountLinkManager linkManager; + + // UUID → aktiver Kanal-ID + private final Map playerChannels = new ConcurrentHashMap<>(); + + // UUIDs die ihren eigenen Chat-Empfang deaktiviert haben + private final Set selfChatMuted = Collections.newSetFromMap(new ConcurrentHashMap<>()); + + // UUIDs die Mentions für sich deaktiviert haben + private final Set mentionsDisabled = Collections.newSetFromMap(new ConcurrentHashMap<>()); + + // HelpOp Cooldown: UUID → letzter Zeitstempel (Sekunden) + private final Map helpopCooldowns = new ConcurrentHashMap<>(); + + // Report-Cooldown: UUID → letzter Report-Zeitstempel (Sekunden) + private final Map reportCooldowns = new ConcurrentHashMap<>(); + + // Letzte Chatnachricht pro Spieler (für Report-Kontext): name.toLowerCase() → message + private final Map lastChatMessages = new ConcurrentHashMap<>(); + + // UUIDs die gerade auf Plugin-Chat-Eingabe warten + private final Set awaitingInput = Collections.newSetFromMap(new ConcurrentHashMap<>()); + + // Geyser-Präfix für Bedrock-Spieler (Standard: ".") + private static final String GEYSER_PREFIX = "."; + + @Override + public String getName() { return "ChatModule"; } + + @Override + public void onEnable(Plugin plugin) { + this.plugin = plugin; + this.logger = plugin.getLogger(); + + // Konfiguration laden + config = new ChatConfig(plugin); + config.load(); + + // Manager initialisieren + muteManager = new MuteManager(plugin.getDataFolder(), logger); + muteManager.load(); + + blockManager = new BlockManager(plugin.getDataFolder(), logger); + blockManager.load(); + + pmManager = new PrivateMsgManager(blockManager); + emojiParser = new EmojiParser(config.getEmojiMappings(), config.isEmojiEnabled()); + chatFilter = new ChatFilter(config.getFilterConfig()); + + // ChatLogger + if (config.isChatlogEnabled()) { + chatLogger = new ChatLogger(plugin.getDataFolder(), logger, config.getChatlogRetentionDays()); + logger.info("[ChatModule] Chat-Log aktiviert (" + config.getChatlogRetentionDays() + " Tage Aufbewahrung)."); + } + + // ReportManager + if (config.isReportsEnabled()) { + reportManager = new ReportManager(plugin.getDataFolder(), logger); + reportManager.load(); + } + + // AccountLinkManager + linkManager = new AccountLinkManager(plugin.getDataFolder(), logger); + linkManager.load(); + + // Externe Brücken + if (config.isDiscordEnabled() || config.isReportWebhookEnabled()) { + discordBridge = new DiscordBridge(plugin, config); + discordBridge.setLinkManager(linkManager); + if (config.isDiscordEnabled()) { + discordBridge.start(); + } + } + if (config.isTelegramEnabled()) { + telegramBridge = new TelegramBridge(plugin, config); + telegramBridge.setLinkManager(linkManager); + telegramBridge.start(); + } + + // Listener & Befehle registrieren + ProxyServer.getInstance().getPluginManager().registerListener(plugin, this); + registerCommands(); + + logger.info("[ChatModule] Aktiviert – " + config.getChannels().size() + " Kanäle geladen."); + } + + @Override + public void onDisable(Plugin plugin) { + if (discordBridge != null) discordBridge.stop(); + if (telegramBridge != null) telegramBridge.stop(); + if (muteManager != null) muteManager.save(); + if (blockManager != null) blockManager.save(); + if (reportManager != null) reportManager.save(); + if (linkManager != null) linkManager.save(); + playerChannels.clear(); + selfChatMuted.clear(); + helpopCooldowns.clear(); + reportCooldowns.clear(); + lastChatMessages.clear(); + } + + // ========================================================= + // CHAT-EVENTS (BungeeCord original, 1.20+) + // + // Das Bypass-Problem mit Paper: + // Wenn BungeeCord eine signierte Nachricht unverändert durchlässt + // (kein setCancelled), prüft Paper die Signatur → ungültig → Fehler. + // Wenn wir setCancelled(true) setzen und die Nachricht selbst senden, + // fehlt die Signatur → Paper lehnt sie ebenfalls ab. + // + // Lösung: In der paper-global.yml auf JEDEM Sub-Server: + // messages: + // reject-chat-unsigned: false + // Das erlaubt unsignierte Nachrichten vom Proxy durch. + // Alternativ: In spigot.yml → settings: bungeecord: true (bereits nötig) + // kombiniert mit BungeeCord IP-Forwarding deaktiviert Paper die Prüfung. + // ========================================================= + + @EventHandler(priority = EventPriority.HIGHEST) + public void onChat(ChatEvent e) { + if (e.isCancelled()) return; + if (e.isCommand()) return; + if (!(e.getSender() instanceof ProxiedPlayer)) return; + ProxiedPlayer player = (ProxiedPlayer) e.getSender(); + + if (awaitingInput.contains(player.getUniqueId())) { + awaitingInput.remove(player.getUniqueId()); + return; // Event NICHT cancellen → Nachricht geht mit Originalsignatur durch + } + + e.setCancelled(true); + processChat(player, e.getMessage()); + } + + /** + * Zentrale Chat-Verarbeitungslogik. + */ + private void processChat(ProxiedPlayer player, String rawMessage) { + if (rawMessage == null || rawMessage.trim().isEmpty()) return; + UUID uuid = player.getUniqueId(); + + boolean isAdmin = player.hasPermission(config.getAdminBypassPermission()); + + if (!isAdmin && muteManager.isMuted(uuid)) { + String remaining = muteManager.getRemainingTime(uuid); + player.sendMessage(color(config.getMutedMessage().replace("{time}", remaining))); + return; + } + + String channelId = playerChannels.getOrDefault(uuid, config.getDefaultChannelId()); + ChatChannel channel = config.getChannel(channelId); + if (channel == null) channel = config.getDefaultChannel(); + + if (channel.hasPermission() && !player.hasPermission(channel.getPermission())) { + player.sendMessage(color("&cDu hast keine Berechtigung für den Kanal &f" + channel.getName() + "&c.")); + channelId = config.getDefaultChannelId(); + channel = config.getDefaultChannel(); + playerChannels.put(uuid, channelId); + } + + String message = emojiParser.parse(rawMessage); + + // ── Chat-Filter ── + boolean hasColorPerm = player.hasPermission("chat.color"); + boolean hasFormatPerm = player.hasPermission("chat.color.format"); + boolean filterBypass = isAdmin || player.hasPermission("chat.filter.bypass"); + ChatFilter.FilterResponse filterResp = chatFilter.filter( + uuid, message, filterBypass, hasColorPerm, hasFormatPerm); + + if (filterResp.result == ChatFilter.FilterResult.BLOCKED) { + player.sendMessage(color(filterResp.denyReason)); + return; + } + message = filterResp.message; + + String serverName = player.getServer() != null + ? player.getServer().getInfo().getName() + : "Proxy"; + + String prefix = getLuckPermsPrefix(player); + String suffix = getLuckPermsSuffix(player); + + // ── Mentions erkennen (@Spielername) ── + final Set mentionedPlayers = new java.util.HashSet<>(); + final String messageWithMentions; + if (config.isMentionsEnabled()) { + String[] words = message.split(" "); + StringBuilder mentionBuilder = new StringBuilder(); + String highlightColor = config.getMentionsHighlightColor(); + for (int wi = 0; wi < words.length; wi++) { + String word = words[wi]; + if (word.startsWith("@") && word.length() > 1) { + String targetName = word.substring(1); + ProxiedPlayer mentioned = ProxyServer.getInstance().getPlayer(targetName); + if (mentioned != null && !mentioned.getUniqueId().equals(uuid)) { + mentionedPlayers.add(mentioned.getUniqueId()); + word = translateColors(highlightColor + word + "&r"); + } + } + mentionBuilder.append(word); + if (wi < words.length - 1) mentionBuilder.append(" "); + } + messageWithMentions = mentionBuilder.toString(); + } else { + messageWithMentions = message; + } + + final String finalMessage = messageWithMentions; + final String formatted = buildFormat(channel.getFormat(), serverName, prefix, + player.getName(), suffix, finalMessage); + + // Nachricht loggen und ID generieren + final String msgId; + if (chatLogger != null) { + msgId = chatLogger.generateMessageId(); + chatLogger.log(msgId, serverName, channel.getId(), player.getName(), finalMessage); + } else { + msgId = null; + } + + // Letzte Nachricht des Spielers speichern (für Report-Kontext) + lastChatMessages.put(player.getName().toLowerCase(), finalMessage); + + final ChatChannel finalChannel = channel; + final String finalFormatted = formatted; + final String finalSenderName = player.getName(); + + plugin.getProxy().getScheduler().runAsync(plugin, () -> { + for (ProxiedPlayer recipient : ProxyServer.getInstance().getPlayers()) { + boolean isSelf = recipient.getUniqueId().equals(uuid); + + if (!isSelf && selfChatMuted.contains(recipient.getUniqueId())) continue; + + boolean recipientIsAdmin = recipient.hasPermission(config.getAdminBypassPermission()); + if (!recipientIsAdmin && !blockManager.canReceive(recipient.getUniqueId(), uuid)) continue; + + if (finalChannel.isLocalOnly()) { + if (player.getServer() == null || recipient.getServer() == null) continue; + if (!player.getServer().getInfo().getName() + .equals(recipient.getServer().getInfo().getName())) continue; + } + + if (finalChannel.hasPermission() && !recipient.hasPermission(finalChannel.getPermission())) { + if (!recipient.getUniqueId().equals(uuid)) continue; + } + + // Mention-Benachrichtigung + boolean isMentioned = mentionedPlayers.contains(recipient.getUniqueId()) + && config.isMentionsEnabled() + && !mentionsDisabled.contains(recipient.getUniqueId()); + + if (isMentioned) { + recipient.sendMessage(color(config.getMentionsNotifyPrefix() + + "&7" + finalSenderName + " &7hat dich erwähnt!")); + sendMentionSound(recipient, config.getMentionsSound()); + } + + if (msgId != null) { + recipient.sendMessage(buildClickableMessage(msgId, finalFormatted, finalSenderName)); + } else { + recipient.sendMessage(color(finalFormatted)); + } + } + + bridgeToDiscord(finalChannel, player.getName(), finalMessage, serverName); + bridgeToTelegram(finalChannel, player.getName(), finalMessage, serverName); + }); + } + + // ========================================================= + // LOGIN-EVENT: Kanal setzen + Join-Nachricht + Report-Info + // ========================================================= + + @EventHandler + public void onLogin(PostLoginEvent e) { + ProxiedPlayer player = e.getPlayer(); + UUID uuid = player.getUniqueId(); + + // Standard-Kanal setzen + playerChannels.put(uuid, config.getDefaultChannelId()); + + // Join-Nachricht und Report-Benachrichtigung mit kurzem Delay senden, + // damit alle anderen Proxy-Initialisierungen (inkl. VanishModule) abgeschlossen sind. + // 2s statt 1s: VanishModule markiert den Spieler oft erst beim ServerConnectedEvent. + plugin.getProxy().getScheduler().schedule(plugin, () -> { + if (!player.isConnected()) return; + + // ── Join-Nachricht ── + if (config.isJoinLeaveEnabled()) { + broadcastJoinLeave(player, true); + } + + // ── Offene Reports für Admins anzeigen ── + if (reportManager != null + && (player.hasPermission(config.getAdminNotifyPermission()) + || player.hasPermission(config.getAdminBypassPermission()))) { + int count = reportManager.getOpenCount(); + if (count > 0) { + player.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); + player.sendMessage(color("&c&l⚑ OFFENE REPORTS &8| &f" + count + " ausstehend")); + player.sendMessage(color("&7Tippe &f/reports &7für eine Übersicht.")); + player.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); + } + } + }, 2, TimeUnit.SECONDS); + } + + // ========================================================= + // DISCONNECT-EVENT: Cleanup + Leave-Nachricht + // ========================================================= + + @EventHandler + public void onDisconnect(PlayerDisconnectEvent e) { + ProxiedPlayer player = e.getPlayer(); + UUID uuid = player.getUniqueId(); + + // Leave-Nachricht + if (config.isJoinLeaveEnabled()) { + broadcastJoinLeave(player, false); + } + + // Cleanup + chatFilter.cleanup(uuid); + playerChannels.remove(uuid); + mentionsDisabled.remove(uuid); + awaitingInput.remove(uuid); + VanishProvider.cleanup(uuid); // Vanish-Status bereinigen + } + + // ========================================================= + // JOIN / LEAVE NACHRICHTEN (mit Vanish-Support) + // ========================================================= + + /** + * Sendet eine Join- oder Leave-Nachricht an alle Spieler. + * + * Vanish-Logik: + * - Unsichtbare Spieler: kein Broadcast an normale Spieler. + * - Ist vanish-show-to-admins=true, erhalten Admins (bypass-permission) + * eine dezente Vanish-Benachrichtigung. + * + * @param player Der betroffene Spieler + * @param isJoin true = Join, false = Leave + */ + private void broadcastJoinLeave(ProxiedPlayer player, boolean isJoin) { + boolean isVanished = VanishProvider.isVanished(player); + + String prefix = getLuckPermsPrefix(player); + String suffix = getLuckPermsSuffix(player); + String server = (player.getServer() != null) + ? config.getServerDisplay(player.getServer().getInfo().getName()) + : "Netzwerk"; + + String normalFormat = isJoin ? config.getJoinFormat() : config.getLeaveFormat(); + String vanishFormat = isJoin ? config.getVanishJoinFormat() : config.getVanishLeaveFormat(); + + // Platzhalter ersetzen + String normalMsg = normalFormat + .replace("{player}", player.getName()) + .replace("{prefix}", prefix != null ? prefix : "") + .replace("{suffix}", suffix != null ? suffix : "") + .replace("{server}", server); + + String vanishMsg = vanishFormat + .replace("{player}", player.getName()) + .replace("{prefix}", prefix != null ? prefix : "") + .replace("{suffix}", suffix != null ? suffix : "") + .replace("{server}", server); + + for (ProxiedPlayer recipient : ProxyServer.getInstance().getPlayers()) { + // Spieler sieht seine eigene Join-/Leave-Meldung nie + if (recipient.getUniqueId().equals(player.getUniqueId())) continue; + + // Admin = bypass-permission ODER notify-permission + boolean recipientIsAdmin = recipient.hasPermission(config.getAdminBypassPermission()) + || recipient.hasPermission(config.getAdminNotifyPermission()); + + if (isVanished) { + // Vanished: Nur Admins sehen eine dezente Meldung (wenn konfiguriert) + if (recipientIsAdmin && config.isVanishShowToAdmins()) { + recipient.sendMessage(color(vanishMsg)); + } + } else { + // Normaler Spieler: alle erhalten die Nachricht + // Vanished Admins sehen Join/Leave-Events anderer Spieler normal + recipient.sendMessage(color(normalMsg)); + } + } + + // Konsole immer informieren + String logMsg = isVanished + ? "[ChatModule] " + (isJoin ? "JOIN(V)" : "LEAVE(V)") + " " + player.getName() + : "[ChatModule] " + (isJoin ? "JOIN" : "LEAVE") + " " + player.getName(); + ProxyServer.getInstance().getConsole().sendMessage(color( + isVanished ? "&8" + logMsg : "&7" + logMsg)); + + // Brücken (nur für nicht-vanished Spieler) + if (!isVanished) { + String cleanMsg = ChatColor.stripColor(ChatColor.translateAlternateColorCodes('&', normalMsg)); + if (discordBridge != null && !config.getJoinLeaveDiscordWebhook().isEmpty()) { + discordBridge.sendToDiscord(config.getJoinLeaveDiscordWebhook(), + player.getName(), cleanMsg, null); + } + if (telegramBridge != null && !config.getJoinLeaveTelegramChatId().isEmpty()) { + telegramBridge.sendToTelegram(config.getJoinLeaveTelegramChatId(), + config.getJoinLeaveTelegramThreadId(), cleanMsg); + } + } + } + + // ========================================================= + // BEFEHLE REGISTRIEREN + // ========================================================= + + private void registerCommands() { + + // /channel | /ch + Command chCmd = new Command("channel", null, "ch", "kanal") { + @Override + public void execute(CommandSender sender, String[] args) { + if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(color("&cNur Spieler!")); return; } + ProxiedPlayer p = (ProxiedPlayer) sender; + if (args.length == 0) { + p.sendMessage(color("&8▸ &eVerfügbare Kanäle:")); + for (ChatChannel ch : config.getChannels().values()) { + boolean hasPerm = !ch.hasPermission() || p.hasPermission(ch.getPermission()); + String active = ch.getId().equals(playerChannels.getOrDefault(p.getUniqueId(), config.getDefaultChannelId())) ? " &a✔" : ""; + p.sendMessage(color( + " " + ch.getFormattedTag() + + " &f" + ch.getName() + + (hasPerm ? active : " &8(keine Berechtigung)"))); + } + return; + } + String target = args[0].toLowerCase(); + ChatChannel ch = config.getChannel(target); + if (ch == null) { p.sendMessage(color("&cKanal &f" + args[0] + " &cnicht gefunden.")); return; } + if (ch.hasPermission() && !p.hasPermission(ch.getPermission())) { + p.sendMessage(color("&cDu hast keine Berechtigung für diesen Kanal.")); return; + } + playerChannels.put(p.getUniqueId(), ch.getId()); + p.sendMessage(color("&aKanal gewechselt: " + ch.getFormattedTag() + " &a" + ch.getName())); + } + }; + ProxyServer.getInstance().getPluginManager().registerCommand(plugin, chCmd); + + // /helpop + Command helpop = new Command("helpop") { + @Override + public void execute(CommandSender sender, String[] args) { + if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(color("&cNur Spieler!")); return; } + ProxiedPlayer p = (ProxiedPlayer) sender; + if (args.length == 0) { p.sendMessage(color("&cBenutzung: /helpop ")); return; } + + long now = System.currentTimeMillis() / 1000L; + Long last = helpopCooldowns.get(p.getUniqueId()); + if (last != null && (now - last) < config.getHelpopCooldown()) { + long wait = config.getHelpopCooldown() - (now - last); + p.sendMessage(color("&cBitte warte noch &f" + wait + "s &cvor dem nächsten HelpOp.")); + return; + } + helpopCooldowns.put(p.getUniqueId(), now); + + String msg = String.join(" ", args); + String server = p.getServer() != null ? p.getServer().getInfo().getName() : "Proxy"; + String formatted = buildSimpleFormat(config.getHelpopFormat(), + "player", p.getName(), "server", server, "message", msg); + + for (ProxiedPlayer op : ProxyServer.getInstance().getPlayers()) { + if (op.hasPermission(config.getHelpopPermission())) { + op.sendMessage(color(formatted)); + } + } + ProxyServer.getInstance().getConsole().sendMessage(color(formatted)); + p.sendMessage(color(config.getHelpopConfirm())); + + if (discordBridge != null && !config.getHelpopDiscordWebhook().isEmpty()) { + discordBridge.sendEmbedToDiscord(config.getHelpopDiscordWebhook(), + "🆘 HelpOp von " + p.getName() + " (" + server + ")", msg, "FFAA00"); + } + if (telegramBridge != null && !config.getHelpopTelegramChatId().isEmpty()) { + telegramBridge.sendFormattedToTelegram(config.getHelpopTelegramChatId(), + "🆘 HelpOp: " + p.getName() + " @" + server, msg); + } + } + }; + ProxyServer.getInstance().getPluginManager().registerCommand(plugin, helpop); + + // /msg + // Vanish: Vanished Spieler sind für normale Spieler nicht erreichbar. + // Admins können vanished Spieler per PM kontaktieren. + Command msgCmd = new Command("msg", null, "tell", "w", "whisper") { + @Override + public void execute(CommandSender sender, String[] args) { + if (!config.isPmEnabled()) { sender.sendMessage(color("&cPrivat-Nachrichten sind deaktiviert.")); return; } + if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(color("&cNur Spieler!")); return; } + if (args.length < 2) { sender.sendMessage(color("&cBenutzung: /msg ")); return; } + + ProxiedPlayer from = (ProxiedPlayer) sender; + boolean fromIsAdmin = from.hasPermission(config.getAdminBypassPermission()); + + // Ziel suchen – vanished Spieler sind für Nicht-Admins "nicht gefunden" + ProxiedPlayer to = findVisiblePlayer(args[0], fromIsAdmin); + if (to == null || !to.isConnected()) { + from.sendMessage(color("&cSpieler &f" + args[0] + " &cnicht gefunden.")); return; + } + + String message = String.join(" ", Arrays.copyOfRange(args, 1, args.length)); + pmManager.send(from, to, message, config, config.getAdminBypassPermission()); + } + }; + ProxyServer.getInstance().getPluginManager().registerCommand(plugin, msgCmd); + + // /r + Command replyCmd = new Command("r", null, "reply", "antwort") { + @Override + public void execute(CommandSender sender, String[] args) { + if (!config.isPmEnabled()) { sender.sendMessage(color("&cPrivat-Nachrichten sind deaktiviert.")); return; } + if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(color("&cNur Spieler!")); return; } + if (args.length == 0) { sender.sendMessage(color("&cBenutzung: /r ")); return; } + ProxiedPlayer p = (ProxiedPlayer) sender; + pmManager.reply(p, String.join(" ", args), config, config.getAdminBypassPermission()); + } + }; + ProxyServer.getInstance().getPluginManager().registerCommand(plugin, replyCmd); + + // /ignore | /unignore + Command ignoreCmd = new Command("ignore", null, "block") { + @Override + public void execute(CommandSender sender, String[] args) { + if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(color("&cNur Spieler!")); return; } + if (args.length == 0) { sender.sendMessage(color("&cBenutzung: /ignore ")); return; } + ProxiedPlayer p = (ProxiedPlayer) sender; + ProxiedPlayer target = ProxyServer.getInstance().getPlayer(args[0]); + if (target == null) { p.sendMessage(color("&cSpieler nicht gefunden.")); return; } + if (target.hasPermission(config.getAdminBypassPermission())) { + p.sendMessage(color("&cAdmins können nicht ignoriert werden.")); return; + } + if (blockManager.isBlocked(p.getUniqueId(), target.getUniqueId())) { + p.sendMessage(color("&cDu hast &f" + target.getName() + " &cbereits ignoriert.")); return; + } + blockManager.block(p.getUniqueId(), target.getUniqueId()); + p.sendMessage(color("&aDu ignorierst jetzt &f" + target.getName() + "&a.")); + } + }; + ProxyServer.getInstance().getPluginManager().registerCommand(plugin, ignoreCmd); + + Command unignoreCmd = new Command("unignore", null, "unblock") { + @Override + public void execute(CommandSender sender, String[] args) { + if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(color("&cNur Spieler!")); return; } + if (args.length == 0) { sender.sendMessage(color("&cBenutzung: /unignore ")); return; } + ProxiedPlayer p = (ProxiedPlayer) sender; + ProxiedPlayer target = ProxyServer.getInstance().getPlayer(args[0]); + if (target == null) { + p.sendMessage(color("&cSpieler nicht online.")); + return; + } + if (!blockManager.isBlocked(p.getUniqueId(), target.getUniqueId())) { + p.sendMessage(color("&cDu hast diesen Spieler nicht ignoriert.")); return; + } + blockManager.unblock(p.getUniqueId(), target.getUniqueId()); + p.sendMessage(color("&aIgnore für &f" + args[0] + " &aaufgehoben.")); + } + }; + ProxyServer.getInstance().getPluginManager().registerCommand(plugin, unignoreCmd); + + // /chatmute | /chatunmute + Command muteCmd = new Command("chatmute", "chat.mute", "gmute") { + @Override + public void execute(CommandSender sender, String[] args) { + if (args.length == 0) { sender.sendMessage(color("&cBenutzung: /chatmute [Minuten]")); return; } + ProxiedPlayer target = ProxyServer.getInstance().getPlayer(args[0]); + if (target == null) { sender.sendMessage(color("&cSpieler &f" + args[0] + " &cist nicht online.")); return; } + if (target.hasPermission(config.getAdminBypassPermission())) { + sender.sendMessage(color("&cDieser Spieler kann nicht gemutet werden.")); return; + } + int duration = config.getDefaultMuteDuration(); + if (args.length >= 2) { + try { duration = Integer.parseInt(args[1]); } + catch (NumberFormatException ex) { sender.sendMessage(color("&cUngültige Dauer. Bitte Zahl eingeben.")); return; } + } + muteManager.mute(target.getUniqueId(), duration); + String durationStr = duration <= 0 ? "permanent" : duration + " Minuten"; + target.sendMessage(color("&cDu wurdest für " + durationStr + " stummgeschaltet.")); + sender.sendMessage(color("&a" + target.getName() + " wurde für " + durationStr + " gemutet.")); + notifyAdmins("&8[&cMute&8] &f" + (sender instanceof ProxiedPlayer ? sender.getName() : "Konsole") + + " &7hat &f" + target.getName() + " &7für &f" + durationStr + " &7gemutet."); + } + }; + ProxyServer.getInstance().getPluginManager().registerCommand(plugin, muteCmd); + + Command unmuteCmd = new Command("chatunmute", "chat.mute", "gunmute") { + @Override + public void execute(CommandSender sender, String[] args) { + if (args.length == 0) { sender.sendMessage(color("&cBenutzung: /chatunmute ")); return; } + ProxiedPlayer target = ProxyServer.getInstance().getPlayer(args[0]); + if (target == null) { sender.sendMessage(color("&cSpieler nicht online.")); return; } + if (!muteManager.isMuted(target.getUniqueId())) { + sender.sendMessage(color("&cDieser Spieler ist nicht gemutet.")); return; + } + muteManager.unmute(target.getUniqueId()); + target.sendMessage(color("&aDeine Stummschaltung wurde aufgehoben.")); + sender.sendMessage(color("&a" + target.getName() + " wurde entmutet.")); + notifyAdmins("&8[&aUnmute&8] &f" + (sender instanceof ProxiedPlayer ? sender.getName() : "Konsole") + + " &7hat &f" + target.getName() + " &7entmutet."); + } + }; + ProxyServer.getInstance().getPluginManager().registerCommand(plugin, unmuteCmd); + + // /chataus (Selbst-Mute) + Command selfMuteCmd = new Command("chataus", null, "togglechat", "chaton", "chatoff") { + @Override + public void execute(CommandSender sender, String[] args) { + if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(color("&cNur Spieler!")); return; } + ProxiedPlayer p = (ProxiedPlayer) sender; + if (selfChatMuted.contains(p.getUniqueId())) { + selfChatMuted.remove(p.getUniqueId()); + p.sendMessage(color("&aChat &l✔ eingeschaltet.")); + } else { + selfChatMuted.add(p.getUniqueId()); + p.sendMessage(color("&cChat &l✘ ausgeschaltet. &7Mit &f/chataus &7wieder einschalten.")); + } + } + }; + ProxyServer.getInstance().getPluginManager().registerCommand(plugin, selfMuteCmd); + + // /broadcast + Command broadcastCmd = new Command("broadcast", config.getBroadcastPermission(), "bc", "alert") { + @Override + public void execute(CommandSender sender, String[] args) { + if (args.length == 0) { sender.sendMessage(color("&cBenutzung: /broadcast ")); return; } + String message = String.join(" ", args); + String formatted = config.getBroadcastFormat().replace("{message}", message); + ProxyServer.getInstance().broadcast(color(formatted)); + + if (discordBridge != null) { + if (!config.getDiscordAdminChannelId().isEmpty()) { + discordBridge.sendToChannel(config.getDiscordAdminChannelId(), + "📢 **Broadcast:** " + ChatColor.stripColor( + ChatColor.translateAlternateColorCodes('&', message))); + } + } + if (telegramBridge != null && !config.getTelegramAdminChatId().isEmpty()) { + telegramBridge.sendFormattedToTelegram(config.getTelegramAdminChatId(), + "📢 Broadcast", + ChatColor.stripColor(ChatColor.translateAlternateColorCodes('&', message))); + } + } + }; + ProxyServer.getInstance().getPluginManager().registerCommand(plugin, broadcastCmd); + + // /emoji + Command emojiCmd = new Command("emoji", null, "emojis") { + @Override + public void execute(CommandSender sender, String[] args) { + sender.sendMessage(color(emojiParser.buildEmojiList())); + } + }; + ProxyServer.getInstance().getPluginManager().registerCommand(plugin, emojiCmd); + + // /socialspy + Command spyCmd = new Command("socialspy", "chat.socialspy", "spy") { + @Override + public void execute(CommandSender sender, String[] args) { + if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(color("&cNur Spieler!")); return; } + ProxiedPlayer p = (ProxiedPlayer) sender; + boolean now = pmManager.toggleSpy(p.getUniqueId()); + p.sendMessage(color(now ? "&aSocial-Spy aktiviert." : "&cSocial-Spy deaktiviert.")); + } + }; + ProxyServer.getInstance().getPluginManager().registerCommand(plugin, spyCmd); + + // /chatreload + Command reloadCmd = new Command("chatreload", "chat.admin.bypass") { + @Override + public void execute(CommandSender sender, String[] args) { + config.load(); + emojiParser = new EmojiParser(config.getEmojiMappings(), config.isEmojiEnabled()); + chatFilter = new ChatFilter(config.getFilterConfig()); + sender.sendMessage(color("&aChat-Konfiguration neu geladen.")); + } + }; + ProxyServer.getInstance().getPluginManager().registerCommand(plugin, reloadCmd); + + // /chatinfo – Admin-Info über einen Spieler + Command chatInfoCmd = new Command("chatinfo", "chat.admin.bypass") { + @Override + public void execute(CommandSender sender, String[] args) { + if (args.length == 0) { sender.sendMessage(color("&cBenutzung: /chatinfo ")); return; } + ProxiedPlayer target = ProxyServer.getInstance().getPlayer(args[0]); + if (target == null) { sender.sendMessage(color("&cSpieler &f" + args[0] + " &cnicht online.")); return; } + + UUID tUUID = target.getUniqueId(); + String tServer = target.getServer() != null ? target.getServer().getInfo().getName() : "Proxy"; + + String channelId = playerChannels.getOrDefault(tUUID, config.getDefaultChannelId()); + ChatChannel ch = config.getChannel(channelId); + String channelName = ch != null ? ch.getFormattedTag() + " &f" + ch.getName() : "&f" + channelId; + + String muteStatus = muteManager.isMuted(tUUID) + ? "&cJa &8(noch: &f" + muteManager.getRemainingTime(tUUID) + "&8)" + : "&aKein"; + + Set blocked = blockManager.getBlockedBy(tUUID); + + AccountLinkManager.LinkedAccount link = linkManager.getByUUID(tUUID); + String discordInfo = (link != null && !link.discordUserId.isEmpty()) + ? "&a" + link.discordUsername + " &8(" + link.discordUserId + ")" : "&7Nicht verknüpft"; + String telegramInfo = (link != null && !link.telegramUserId.isEmpty()) + ? "&a" + link.telegramUsername + " &8(" + link.telegramUserId + ")" : "&7Nicht verknüpft"; + + String vanishStatus = VanishProvider.isVanished(target) ? "&eJa" : "&aKein"; + + sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); + sender.sendMessage(color("&eChatInfo: &f" + target.getName() + " &8@ &7" + tServer)); + sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); + sender.sendMessage(color("&7Kanal: " + channelName)); + sender.sendMessage(color("&7Mute: " + muteStatus)); + sender.sendMessage(color("&7Chat-aus: " + (selfChatMuted.contains(tUUID) ? "&cJa" : "&aKein"))); + sender.sendMessage(color("&7Mentions: " + (mentionsDisabled.contains(tUUID) ? "&cDeaktiviert" : "&aAktiv"))); + sender.sendMessage(color("&7Vanish: " + vanishStatus)); + sender.sendMessage(color("&7Blockiert: &f" + blocked.size() + " Spieler")); + if (!blocked.isEmpty()) { + for (UUID bUUID : blocked) { + ProxiedPlayer bp = ProxyServer.getInstance().getPlayer(bUUID); + String bName = bp != null ? bp.getName() : bUUID.toString().substring(0, 8) + "..."; + sender.sendMessage(color(" &8- &7" + bName)); + } + } + sender.sendMessage(color("&7Discord: " + discordInfo)); + sender.sendMessage(color("&7Telegram: " + telegramInfo)); + sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); + } + }; + ProxyServer.getInstance().getPluginManager().registerCommand(plugin, chatInfoCmd); + + // /chathist [spieler] [anzahl] – Chat-History aus dem Logfile + Command chatHistCmd = new Command("chathist", "chat.admin.bypass") { + @Override + public void execute(CommandSender sender, String[] args) { + if (chatLogger == null) { + sender.sendMessage(color("&cChat-Log ist deaktiviert.")); return; + } + + String playerFilter = null; + int lines = config.getHistoryDefaultLines(); + + if (args.length >= 1) { + try { + lines = Math.min(Integer.parseInt(args[0]), config.getHistoryMaxLines()); + } catch (NumberFormatException ex) { + playerFilter = args[0]; + } + } + if (args.length >= 2) { + try { + lines = Math.min(Integer.parseInt(args[1]), config.getHistoryMaxLines()); + } catch (NumberFormatException ignored) {} + } + + final String finalFilter = playerFilter; + final int finalLines = lines; + + plugin.getProxy().getScheduler().runAsync(plugin, () -> { + List history = chatLogger.readLastLines(finalFilter, finalLines); + + sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); + sender.sendMessage(color("&eChatHistory &8| &f" + history.size() + " Zeilen" + + (finalFilter != null ? " &8| &7Spieler: &f" + finalFilter : ""))); + sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); + + if (history.isEmpty()) { + sender.sendMessage(color("&7Keine Einträge gefunden.")); + } else { + for (String line : history) { + sender.sendMessage(color("&8" + line)); + } + } + sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); + }); + } + }; + ProxyServer.getInstance().getPluginManager().registerCommand(plugin, chatHistCmd); + + // /mentions – Mentions ein-/ausschalten + Command mentionsCmd = new Command("mentions", null, "mention") { + @Override + public void execute(CommandSender sender, String[] args) { + if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(color("&cNur Spieler!")); return; } + if (!config.isMentionsEnabled()) { sender.sendMessage(color("&cMentions sind deaktiviert.")); return; } + if (!config.isMentionsAllowToggle()) { sender.sendMessage(color("&cDas Deaktivieren von Mentions ist nicht erlaubt.")); return; } + + ProxiedPlayer p = (ProxiedPlayer) sender; + if (mentionsDisabled.contains(p.getUniqueId())) { + mentionsDisabled.remove(p.getUniqueId()); + p.sendMessage(color("&aMentions &l✔ &aaktiviert. Du wirst benachrichtigt wenn jemand @" + p.getName() + " schreibt.")); + } else { + mentionsDisabled.add(p.getUniqueId()); + p.sendMessage(color("&cMentions &l✘ &cdeaktiviert. Du wirst nicht mehr benachrichtigt.")); + } + } + }; + ProxyServer.getInstance().getPluginManager().registerCommand(plugin, mentionsCmd); + + // /chatbypass – Chat-Verarbeitung für nächste Eingabe(n) überspringen + Command bypassCmd = new Command("chatbypass", null, "cbp") { + @Override + public void execute(CommandSender sender, String[] args) { + if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(color("&cNur Spieler!")); return; } + ProxiedPlayer p = (ProxiedPlayer) sender; + + if (awaitingInput.contains(p.getUniqueId())) { + awaitingInput.remove(p.getUniqueId()); + p.sendMessage(color("&aChatModule &l✔ &aaktiv.")); + } else { + awaitingInput.add(p.getUniqueId()); + p.sendMessage(color("&eChatModule &l⏸ &eüberbrückt. &7Nächste Nachricht geht direkt an den Server.")); + p.sendMessage(color("&7Mit &f/chatbypass &7wieder einschalten.")); + } + } + }; + ProxyServer.getInstance().getPluginManager().registerCommand(plugin, bypassCmd); + + // /discordlink – Discord-Account verknüpfen + Command discordLinkCmd = new Command("discordlink", null, "dlink") { + @Override + public void execute(CommandSender sender, String[] args) { + if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(color("&cNur Spieler!")); return; } + if (!config.isDiscordEnabled()) { sender.sendMessage(color("&cDiscord ist nicht aktiviert.")); return; } + ProxiedPlayer p = (ProxiedPlayer) sender; + + String token = linkManager.generateToken(p.getUniqueId(), p.getName(), "discord"); + + p.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); + p.sendMessage(color("&9&lDiscord-Verknüpfung")); + p.sendMessage(color("&7Schreibe dem Bot auf Discord:")); + p.sendMessage(color("&f!link &b" + token)); + p.sendMessage(color("&7Token gültig für &f10 Minuten&7.")); + p.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); + } + }; + ProxyServer.getInstance().getPluginManager().registerCommand(plugin, discordLinkCmd); + + // /telegramlink – Telegram-Account verknüpfen + Command telegramLinkCmd = new Command("telegramlink", null, "tlink") { + @Override + public void execute(CommandSender sender, String[] args) { + if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(color("&cNur Spieler!")); return; } + if (!config.isTelegramEnabled()) { sender.sendMessage(color("&cTelegram ist nicht aktiviert.")); return; } + ProxiedPlayer p = (ProxiedPlayer) sender; + + String token = linkManager.generateToken(p.getUniqueId(), p.getName(), "telegram"); + + p.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); + p.sendMessage(color("&3&lTelegram-Verknüpfung")); + p.sendMessage(color("&7Schreibe dem Bot auf Telegram:")); + p.sendMessage(color("&f/link &b" + token)); + p.sendMessage(color("&7Token gültig für &f10 Minuten&7.")); + p.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); + } + }; + ProxyServer.getInstance().getPluginManager().registerCommand(plugin, telegramLinkCmd); + + // /unlink – Verknüpfung aufheben + Command unlinkCmd = new Command("unlink") { + @Override + public void execute(CommandSender sender, String[] args) { + if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(color("&cNur Spieler!")); return; } + ProxiedPlayer p = (ProxiedPlayer) sender; + + if (args.length == 0) { + p.sendMessage(color("&cBenutzung: /unlink ")); + return; + } + + switch (args[0].toLowerCase()) { + case "discord": + if (linkManager.unlinkDiscord(p.getUniqueId())) + p.sendMessage(color("&aDiscord-Verknüpfung aufgehoben.")); + else + p.sendMessage(color("&cKein Discord-Account verknüpft.")); + break; + case "telegram": + if (linkManager.unlinkTelegram(p.getUniqueId())) + p.sendMessage(color("&aTelegram-Verknüpfung aufgehoben.")); + else + p.sendMessage(color("&cKein Telegram-Account verknüpft.")); + break; + case "all": + boolean d = linkManager.unlinkDiscord(p.getUniqueId()); + boolean t = linkManager.unlinkTelegram(p.getUniqueId()); + if (d || t) p.sendMessage(color("&aAlle Verknüpfungen aufgehoben.")); + else p.sendMessage(color("&cKeine Verknüpfungen vorhanden.")); + break; + default: + p.sendMessage(color("&cBenutzung: /unlink ")); + } + } + }; + ProxyServer.getInstance().getPluginManager().registerCommand(plugin, unlinkCmd); + + // /report + Command reportCmd = new Command("report") { + @Override + public void execute(CommandSender sender, String[] args) { + if (reportManager == null) { sender.sendMessage(color("&cDas Report-System ist deaktiviert.")); return; } + if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(color("&cNur Spieler!")); return; } + + ProxiedPlayer p = (ProxiedPlayer) sender; + + String reqPerm = config.getReportPermission(); + if (reqPerm != null && !reqPerm.isEmpty() && !p.hasPermission(reqPerm)) { + p.sendMessage(color("&cDu hast keine Berechtigung für /report.")); return; + } + + if (args.length < 2) { + p.sendMessage(color("&cBenutzung: /report ")); + return; + } + + long now = System.currentTimeMillis() / 1000L; + Long last = reportCooldowns.get(p.getUniqueId()); + if (last != null && (now - last) < config.getReportCooldown()) { + long wait = config.getReportCooldown() - (now - last); + p.sendMessage(color("&cBitte warte noch &f" + wait + "s &cvor dem nächsten Report.")); + return; + } + + String reportedName = args[0]; + String reason = String.join(" ", Arrays.copyOfRange(args, 1, args.length)); + String server = p.getServer() != null ? p.getServer().getInfo().getName() : "Proxy"; + String msgContext = lastChatMessages.getOrDefault(reportedName.toLowerCase(), "(keine Chat-Nachricht bekannt)"); + + String reportId = reportManager.createReport( + p.getName(), p.getUniqueId(), reportedName, server, msgContext, reason); + + if (chatLogger != null) { + String logMsg = "[REPORT] Reporter: " + p.getName() + ", Gemeldet: " + reportedName + + ", Grund: " + reason + " | Letzte Nachricht: " + msgContext + + " | Report-ID: " + reportId; + chatLogger.log(reportId, server, "report", p.getName(), logMsg); + } + + String reportWebhook = config.getReportDiscordWebhook(); + if (config.isReportWebhookEnabled() && discordBridge != null + && reportWebhook != null && !reportWebhook.isEmpty()) { + String title = "Neuer Report eingegangen"; + String desc = "**Reporter:** " + p.getName() + + "\n**Gemeldet:** " + reportedName + + "\n**Server:** " + server + + "\n**Grund:** " + reason + + "\n**Letzte Nachricht:** " + msgContext + + "\n**Report-ID:** " + reportId; + discordBridge.sendEmbedToDiscord(reportWebhook, title, desc, config.getDiscordEmbedColor()); + } + + String reportTgChatId = config.getReportTelegramChatId(); + if (telegramBridge != null && reportTgChatId != null && !reportTgChatId.isEmpty()) { + String header = "Neuer Report eingegangen"; + String content = "Reporter: " + p.getName() + + "\nGemeldet: " + reportedName + + "\nServer: " + server + + "\nGrund: " + reason + + "\nLetzte Nachricht: " + msgContext + + "\nReport-ID: " + reportId; + telegramBridge.sendFormattedToTelegram(reportTgChatId, header, content); + } + + reportCooldowns.put(p.getUniqueId(), now); + + String confirm = config.getReportConfirm().replace("{id}", reportId); + p.sendMessage(color(confirm)); + + notifyAdminsReport(reportId, p.getName(), reportedName, server, reason, msgContext); + } + }; + ProxyServer.getInstance().getPluginManager().registerCommand(plugin, reportCmd); + + // /reports [all] – Admin-Übersicht + Command reportsCmd = new Command("reports", config.getReportViewPermission()) { + @Override + public void execute(CommandSender sender, String[] args) { + if (reportManager == null) { sender.sendMessage(color("&cDas Report-System ist deaktiviert.")); return; } + + boolean showAll = args.length > 0 && args[0].equalsIgnoreCase("all"); + List list = showAll + ? reportManager.getAllReports() + : reportManager.getOpenReports(); + + sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); + sender.sendMessage(color("&c&l⚑ REPORTS &8| &f" + list.size() + + (showAll ? " gesamt" : " offen") + + " &8| &7/reports all für alle")); + sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); + + if (list.isEmpty()) { + sender.sendMessage(color("&7Keine " + (showAll ? "" : "offenen ") + "Reports vorhanden.")); + return; + } + + for (ReportManager.ChatReport r : list) { + String statusColor = r.closed ? "&a✔" : "&c✘"; + + if (sender instanceof ProxiedPlayer) { + ComponentBuilder line = new ComponentBuilder(""); + + line.append(ChatColor.translateAlternateColorCodes('&', statusColor + " ")) + .event((ClickEvent) null) + .event((HoverEvent) null); + + line.append(ChatColor.translateAlternateColorCodes('&', "&8[&f" + r.id + "&8]")) + .event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, r.id)) + .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, + new ComponentBuilder(ChatColor.GRAY + "Klicken zum Kopieren: " + r.id + + "\n" + ChatColor.YELLOW + "Reporter: " + ChatColor.WHITE + r.reporterName + + "\n" + ChatColor.YELLOW + "Gemeldet: " + ChatColor.RED + r.reportedName + + "\n" + ChatColor.YELLOW + "Server: " + ChatColor.GREEN + r.server + + "\n" + ChatColor.YELLOW + "Zeit: " + ChatColor.WHITE + r.getFormattedTime() + + "\n" + ChatColor.YELLOW + "Kontext: " + ChatColor.GRAY + r.messageContext + + "\n" + ChatColor.YELLOW + "Grund: " + ChatColor.RED + r.reason + + (r.closed ? "\n" + ChatColor.GREEN + "Geschlossen von: " + r.closedBy : "")).create())); + + line.append(ChatColor.translateAlternateColorCodes('&', + " &b" + r.reportedName + " &8← &7" + r.reporterName + + " &8@ &a" + r.server + + " &8| &e" + r.getFormattedTime())) + .event((ClickEvent) null) + .event((HoverEvent) null); + + ((ProxiedPlayer) sender).sendMessage(line.create()); + } else { + sender.sendMessage(color(statusColor + " &8[&f" + r.id + "&8] &b" + r.reportedName + + " &8← &7" + r.reporterName + " &8@ &a" + r.server + + " &8| &e" + r.getFormattedTime() + + " &8| &cGrund: &f" + r.reason)); + } + } + + sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); + if (!showAll && sender instanceof ProxiedPlayer) { + sender.sendMessage(color("&7Tipp: &f/reportclose &7zum Schließen.")); + } + } + }; + ProxyServer.getInstance().getPluginManager().registerCommand(plugin, reportsCmd); + + // /reportclose + Command reportCloseCmd = new Command("reportclose", config.getReportClosePermission()) { + @Override + public void execute(CommandSender sender, String[] args) { + if (reportManager == null) { sender.sendMessage(color("&cDas Report-System ist deaktiviert.")); return; } + if (args.length == 0) { sender.sendMessage(color("&cBenutzung: /reportclose ")); return; } + + String id = args[0].toUpperCase(); + ReportManager.ChatReport report = reportManager.getReport(id); + + if (report == null) { + sender.sendMessage(color("&cReport &f" + id + " &cnicht gefunden.")); return; + } + if (report.closed) { + sender.sendMessage(color("&cReport &f" + id + " &cist bereits geschlossen" + + (report.closedBy != null && !report.closedBy.isEmpty() + ? " &8(von &7" + report.closedBy + "&8)" : "") + ".")); + return; + } + + String adminName = (sender instanceof ProxiedPlayer) ? sender.getName() : "Konsole"; + reportManager.closeReport(id, adminName); + + sender.sendMessage(color("&aReport &f" + id + " &awurde geschlossen.")); + + ProxiedPlayer reporter = ProxyServer.getInstance().getPlayer(report.reporterUUID); + if (reporter != null && reporter.isConnected()) { + reporter.sendMessage(color("&8[&aReport&8] &7Dein Report &f" + id + + " &7gegen &c" + report.reportedName + " &7wurde von &b" + adminName + " &7bearbeitet.")); + } + + notifyAdmins("&8[&aReport geschlossen&8] &f" + adminName + " &7hat Report &f" + id + " &7geschlossen."); + } + }; + ProxyServer.getInstance().getPluginManager().registerCommand(plugin, reportCloseCmd); + } + + // ========================================================= + // PLUGIN-INPUT BYPASS + // ========================================================= + + /** + * Öffentliche API für Sub-Server-Plugins oder BungeeCord-eigene Plugins. + * Setzt den Bypass-Status für einen Spieler. + * + * Beispiel aus einem anderen BungeeCord-Plugin: + * ChatModule chatModule = (ChatModule) proxy.getPluginManager() + * .getPlugin("StatusAPI").getModule("ChatModule"); + * chatModule.setAwaitingInput(player.getUniqueId(), true); + */ + public void setAwaitingInput(UUID uuid, boolean awaiting) { + if (awaiting) awaitingInput.add(uuid); + else awaitingInput.remove(uuid); + } + + public boolean isAwaitingInput(UUID uuid) { + return awaitingInput.contains(uuid); + } + + // ========================================================= + // VANISH-HILFSMETHODEN + // ========================================================= + + /** + * Sucht einen Spieler nach Name und berücksichtigt den Vanish-Status. + * + * @param name Spielername (case-insensitiv) + * @param callerIsAdmin true → Vanished Spieler werden ebenfalls gefunden + * @return ProxiedPlayer oder null wenn nicht gefunden / vanished (für Nicht-Admins) + */ + private ProxiedPlayer findVisiblePlayer(String name, boolean callerIsAdmin) { + ProxiedPlayer target = ProxyServer.getInstance().getPlayer(name); + if (target == null) return null; + if (!callerIsAdmin && VanishProvider.isVanished(target)) return null; + return target; + } + + // ========================================================= + // HILFSMETHODEN + // ========================================================= + + private String buildFormat(String format, String server, String prefix, + String player, String suffix, String message) { + String serverColor = config.getServerColor(server); + String serverDisplay = config.getServerDisplay(server); + // Nur den Servernamen-Teil vorübersetzen damit &#RRGGBB im Display-Namen + // korrekt sitzt; der Rest wird am Ausgabepunkt via translateColors() übersetzt. + String coloredServer = serverColor + serverDisplay + "&r"; + + return format + .replace("{server}", coloredServer) + .replace("{prefix}", prefix != null ? prefix : "") + .replace("{player}", player) + .replace("{suffix}", suffix != null ? suffix : "") + .replace("{message}", message); + } + + private String buildSimpleFormat(String format, String... kvPairs) { + String result = format; + for (int i = 0; i + 1 < kvPairs.length; i += 2) { + result = result.replace("{" + kvPairs[i] + "}", kvPairs[i + 1]); + } + return result; + } + + private TextComponent color(String text) { + return new TextComponent(translateColors(text)); + } + + /** + * Übersetzt sowohl klassische &-Farbcodes als auch HEX-Codes im Format &#RRGGBB. + */ + private String translateColors(String text) { + if (text == null) return ""; + + StringBuilder sb = new StringBuilder(); + int i = 0; + while (i < text.length()) { + if (i + 7 < text.length() + && text.charAt(i) == '&' + && text.charAt(i + 1) == '#') { + String hex = text.substring(i + 2, i + 8); + boolean validHex = hex.chars().allMatch(c -> + (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')); + if (validHex) { + try { + sb.append(ChatColor.of("#" + hex).toString()); + i += 8; + continue; + } catch (Exception ignored) { + // Ungültige Farbe → als normalen Text behandeln + } + } + } + sb.append(text.charAt(i)); + i++; + } + + return ChatColor.translateAlternateColorCodes('&', sb.toString()); + } + + private void notifyAdmins(String message) { + for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { + if (p.hasPermission(config.getAdminNotifyPermission())) { + p.sendMessage(color(message)); + } + } + } + + /** + * Benachrichtigt alle online Admins über einen neuen Report. + */ + private void notifyAdminsReport(String reportId, String reporter, String reported, + String server, String reason, String msgContext) { + java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("dd.MM.yyyy HH:mm:ss"); + String zeit = sdf.format(new java.util.Date()); + + for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { + if (!p.hasPermission(config.getAdminNotifyPermission()) + && !p.hasPermission(config.getAdminBypassPermission())) continue; + + p.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); + p.sendMessage(color("&8[&cReport&8] &7Zeit: &e" + zeit)); + p.sendMessage(color("&7Reporter: &b" + reporter)); + p.sendMessage(color("&7Server: &a" + server)); + p.sendMessage(color("&7Letzte Nachricht: &f" + msgContext)); + p.sendMessage(color("&7Grund: &c" + reason)); + + ComponentBuilder idLine = new ComponentBuilder(ChatColor.GRAY + "ID: "); + idLine.append(ChatColor.WHITE + "" + ChatColor.BOLD + reportId) + .event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, reportId)) + .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, + new ComponentBuilder(ChatColor.GRAY + "Klicken zum Kopieren · " + + ChatColor.YELLOW + "/reportclose " + reportId).create())) + .append(ChatColor.GRAY + " (klicken zum Kopieren)", ComponentBuilder.FormatRetention.NONE) + .event((ClickEvent) null) + .event((HoverEvent) null); + p.sendMessage(idLine.create()); + + p.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); + } + + ProxyServer.getInstance().getConsole().sendMessage(color( + "&8[&cReport " + reportId + "&8] &7Reporter: &b" + reporter + + " &7→ &c" + reported + " &7@ &a" + server + " &8| &cGrund: &f" + reason)); + } + + /** + * Baut eine BungeeCord-Nachricht mit klickbarem [⚑] Melden-Button. + */ + private BaseComponent[] buildClickableMessage(String msgId, String formatted, String senderName) { + ComponentBuilder builder = new ComponentBuilder(""); + + builder.append(translateColors(formatted), + ComponentBuilder.FormatRetention.NONE) + .event((ClickEvent) null) + .event((HoverEvent) null); + + if (msgId != null && senderName != null && reportManager != null) { + builder.append(" ", ComponentBuilder.FormatRetention.NONE) + .event((ClickEvent) null) + .event((HoverEvent) null); + builder.append(ChatColor.DARK_GRAY + "[" + ChatColor.RED + "⚑" + ChatColor.DARK_GRAY + "]", + ComponentBuilder.FormatRetention.NONE) + .event(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, + "/report " + senderName + " ")) + .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, + new ComponentBuilder(ChatColor.GRAY + "Spieler melden\n" + + ChatColor.YELLOW + "/report " + senderName + " ").create())); + } + + return builder.create(); + } + + private boolean isBedrock(ProxiedPlayer player) { + return player.getName().startsWith(GEYSER_PREFIX); + } + + /** + * Sendet einen Sound-Hinweis via Actionbar (Mention-Feedback). + */ + private void sendMentionSound(ProxiedPlayer player, String soundName) { + if (soundName == null || soundName.isEmpty()) return; + try { + net.md_5.bungee.api.chat.TextComponent actionBar = + new net.md_5.bungee.api.chat.TextComponent( + ChatColor.translateAlternateColorCodes('&', "&e♪ Mention!")); + player.sendMessage(net.md_5.bungee.api.ChatMessageType.ACTION_BAR, actionBar); + } catch (Exception ignored) {} + } + + private String getLuckPermsPrefix(ProxiedPlayer player) { + try { + net.luckperms.api.LuckPerms lp = net.luckperms.api.LuckPermsProvider.get(); + net.luckperms.api.model.user.User user = lp.getUserManager().getUser(player.getUniqueId()); + if (user == null) return ""; + String prefix = user.getCachedData().getMetaData().getPrefix(); + if (prefix == null) return ""; + return ChatColor.translateAlternateColorCodes('&', prefix); + } catch (Exception e) { + return ""; + } + } + + private String getLuckPermsSuffix(ProxiedPlayer player) { + try { + net.luckperms.api.LuckPerms lp = net.luckperms.api.LuckPermsProvider.get(); + net.luckperms.api.model.user.User user = lp.getUserManager().getUser(player.getUniqueId()); + if (user == null) return ""; + String suffix = user.getCachedData().getMetaData().getSuffix(); + if (suffix == null) return ""; + return ChatColor.translateAlternateColorCodes('&', suffix); + } catch (Exception e) { + return ""; + } + } + + // ========================================================= + // EXTERNE BRÜCKEN + // ========================================================= + + private void bridgeToDiscord(ChatChannel channel, String playerName, String message, String server) { + if (discordBridge == null) return; + String cleanMessage = "[" + server + "] " + playerName + ": " + + ChatColor.stripColor(ChatColor.translateAlternateColorCodes('&', message)); + if (!channel.getDiscordWebhook().isEmpty()) { + discordBridge.sendToDiscord(channel.getDiscordWebhook(), playerName, cleanMessage, null); + } + if (!channel.getDiscordChannelId().isEmpty()) { + discordBridge.sendToChannel(channel.getDiscordChannelId(), cleanMessage); + } + if (channel.isUseAdminBridge() && !config.getDiscordAdminChannelId().isEmpty()) { + discordBridge.sendToChannel(config.getDiscordAdminChannelId(), cleanMessage); + } + } + + private void bridgeToTelegram(ChatChannel channel, String playerName, String message, String server) { + if (telegramBridge == null) return; + String cleanMessage = "[" + server + "] " + playerName + ": " + + ChatColor.stripColor(ChatColor.translateAlternateColorCodes('&', message)); + if (!channel.getTelegramChatId().isEmpty()) { + telegramBridge.sendToTelegram(channel.getTelegramChatId(), + channel.getTelegramThreadId(), cleanMessage); + } + if (channel.isUseAdminBridge() && !config.getTelegramAdminChatId().isEmpty()) { + telegramBridge.sendToTelegram(config.getTelegramAdminChatId(), + config.getTelegramAdminTopicId(), cleanMessage); + } + } +} \ No newline at end of file