From d7e17457145e44956ffcae52dbaa1e0b3b02d861 Mon Sep 17 00:00:00 2001 From: M_Viper Date: Sat, 14 Nov 2026 23:40:19 +0000 Subject: [PATCH] Upload via GUI (61 Dateien) --- .../status/modules/chat/ChatGameBridge.java | 147 + .../viper/status/modules/chat/ChatModule.java | 2876 +++++++++-------- 2 files changed, 1624 insertions(+), 1399 deletions(-) create mode 100644 StatusAPI/src/main/java/net/viper/status/modules/chat/ChatGameBridge.java diff --git a/StatusAPI/src/main/java/net/viper/status/modules/chat/ChatGameBridge.java b/StatusAPI/src/main/java/net/viper/status/modules/chat/ChatGameBridge.java new file mode 100644 index 0000000..b265364 --- /dev/null +++ b/StatusAPI/src/main/java/net/viper/status/modules/chat/ChatGameBridge.java @@ -0,0 +1,147 @@ +package net.viper.status.modules.chat; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +/** + * Nachrichtenformat für Chat-Spiele (NexusLobby) zwischen Sub-Server und Proxy. + * + * Hintergrund: Der Chat läuft komplett über diesen Proxy ({@link ChatModule}). Die Sub-Server bekommen + * keine Chat-Ereignisse und können Antworten auf ein Chat-Spiel deshalb nicht selbst erkennen. Der + * Sub-Server meldet dem Proxy Start und Ende eines Spiels samt gültiger Antworten. Der Proxy erkennt die + * Antwort im Chat und meldet sie dem Sub-Server zurück. + * + * Richtung Proxy: Kanal "BungeeCord", Unterkanal {@link #SUBCHANNEL} + * START: Unterkanal, "START", Spiel-ID, Sekunden, Groß/Klein beachten, Antwort verstecken, Anzahl, Antworten... + * STOP: Unterkanal, "STOP", Spiel-ID + * Richtung Sub-Server: Kanal {@link #ANSWER_CHANNEL} + * ANSWER: "ANSWER", Spiel-ID, Spieler-UUID, Nachricht + * + * Das Format muss mit ChatGameProtocol in NexusLobby übereinstimmen. + */ +public final class ChatGameBridge { + + public static final String SUBCHANNEL = "NexusChatGame"; + public static final String ANSWER_CHANNEL = "statusapi:chatgame"; + + /** So viele Sekunden länger als angegeben bleibt ein Spiel im Proxy gültig (Sicherheitspuffer). */ + private static final long GRACE_SECONDS = 5; + private static final int MAX_ANSWERS = 20; + + private ChatGameBridge() {} + + /** Ein laufendes Chat-Spiel auf einem Sub-Server. */ + public static final class Game { + public final String id; + public final List answers; + public final boolean caseSensitive; + public final boolean hideAnswer; + public final long expiresAt; + + public Game(String id, List answers, boolean caseSensitive, boolean hideAnswer, long expiresAt) { + this.id = id; + this.answers = Collections.unmodifiableList(new ArrayList<>(answers)); + this.caseSensitive = caseSensitive; + this.hideAnswer = hideAnswer; + this.expiresAt = expiresAt; + } + + public boolean expired(long now) { + return now > expiresAt; + } + + /** Prüft, ob die Chat-Nachricht eine gültige Antwort ist. */ + public boolean matches(String message) { + String text = clean(message); + if (text.isEmpty()) return false; + for (String answer : answers) { + if (caseSensitive ? answer.equals(text) : answer.equalsIgnoreCase(text)) return true; + } + return false; + } + } + + /** Eine Nachricht des Sub-Servers an den Proxy. */ + public static final class Message { + public final String action; // START oder STOP + public final String gameId; + public final Game game; // nur bei START, sonst null + + Message(String action, String gameId, Game game) { + this.action = action; + this.gameId = gameId; + this.game = game; + } + } + + /** + * Liest eine Plugin-Message des Sub-Servers. + * + * @param data Daten einer Nachricht auf dem Kanal "BungeeCord" + * @param now aktuelle Zeit in Millisekunden + * @return die Nachricht oder null, wenn sie nicht zum Chat-Spiel gehört oder fehlerhaft ist + */ + public static Message read(byte[] data, long now) { + if (data == null) return null; + try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(data))) { + if (!SUBCHANNEL.equals(in.readUTF())) return null; + String action = in.readUTF(); + String id = in.readUTF(); + + if ("STOP".equals(action)) return new Message("STOP", id, null); + if (!"START".equals(action)) return null; + + int seconds = Math.max(1, Math.min(600, in.readInt())); + boolean caseSensitive = in.readBoolean(); + boolean hideAnswer = in.readBoolean(); + int count = in.readInt(); + if (count < 1 || count > MAX_ANSWERS) return null; + + List answers = new ArrayList<>(); + for (int i = 0; i < count; i++) answers.add(clean(in.readUTF())); + + long expiresAt = now + (seconds + GRACE_SECONDS) * 1000L; + return new Message("START", id, new Game(id, answers, caseSensitive, hideAnswer, expiresAt)); + } catch (IOException | RuntimeException e) { + return null; + } + } + + /** Baut die Antwort-Nachricht an den Sub-Server. */ + public static byte[] answer(String gameId, UUID player, String message) { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + DataOutputStream out = new DataOutputStream(bytes); + out.writeUTF("ANSWER"); + out.writeUTF(gameId); + out.writeUTF(player.toString()); + out.writeUTF(message); + return bytes.toByteArray(); + } catch (IOException e) { + throw new IllegalStateException(e); // kann bei einem Byte-Array nicht passieren + } + } + + /** Entfernt Farbcodes (Paragraphenzeichen), unsichtbare Zeichen und überflüssige Leerzeichen. */ + public static String clean(String s) { + if (s == null) return ""; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == 0xA7) { // Paragraphenzeichen: das Zeichen danach gehört zum Farbcode + i++; + continue; + } + if ((c >= 0x200B && c <= 0x200F) || c == 0x2060 || c == 0xFEFF) continue; // unsichtbar + sb.append(c == 0xA0 ? ' ' : c); + } + return sb.toString().trim(); + } +} diff --git a/StatusAPI/src/main/java/net/viper/status/modules/chat/ChatModule.java b/StatusAPI/src/main/java/net/viper/status/modules/chat/ChatModule.java index 3c0eef0..e9c0ae1 100644 --- a/StatusAPI/src/main/java/net/viper/status/modules/chat/ChatModule.java +++ b/StatusAPI/src/main/java/net/viper/status/modules/chat/ChatModule.java @@ -1,1400 +1,1478 @@ -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\u00fcr StatusAPI (BungeeCord) - * - * Features: - * ✅ Mehrere Kan\u00e4le mit Permissions - * ✅ Server-Erkennung im Chat-Format - * ✅ /helpop f\u00fcr Spieler - * ✅ Emoji-Unterst\u00fctzung (: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\u00fcr 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\u00f6gert 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\u00fcr 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\u00fcr 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\u00e4fix f\u00fcr 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.fine("[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\u00fccken - 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.fine("[ChatModule] Aktiviert – " + config.getChannels().size() + " Kan\u00e4le 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\u00e4ndert durchl\u00e4sst - // (kein setCancelled), pr\u00fcft Paper die Signatur → ung\u00fcltig → Fehler. - // Wenn wir setCancelled(true) setzen und die Nachricht selbst senden, - // fehlt die Signatur → Paper lehnt sie ebenfalls ab. - // - // L\u00f6sung: 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\u00f6tig) - // kombiniert mit BungeeCord IP-Forwarding deaktiviert Paper die Pr\u00fcfung. - // ========================================================= - - @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\u00fcr 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\u00fcr 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\u00e4hnt!")); - 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\u00fcr 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\u00fcr eine \u00dcbersicht.")); - 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\u00fccken (nur f\u00fcr 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\u00fcgbare Kan\u00e4le:")); - 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\u00fcr 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\u00e4chsten 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\u00fcr normale Spieler nicht erreichbar. - // Admins k\u00f6nnen 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\u00fcr 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\u00f6nnen 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\u00fcr &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\u00fcltige Dauer. Bitte Zahl eingeben.")); return; } - } - muteManager.mute(target.getUniqueId(), duration); - String durationStr = duration <= 0 ? "permanent" : duration + " Minuten"; - target.sendMessage(color("&cDu wurdest f\u00fcr " + durationStr + " stummgeschaltet.")); - sender.sendMessage(color("&a" + target.getName() + " wurde f\u00fcr " + durationStr + " gemutet.")); - notifyAdmins("&8[&cMute&8] &f" + (sender instanceof ProxiedPlayer ? sender.getName() : "Konsole") - + " &7hat &f" + target.getName() + " &7f\u00fcr &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 \u00fcber 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\u00fcpft"; - String telegramInfo = (link != null && !link.telegramUserId.isEmpty()) - ? "&a" + link.telegramUsername + " &8(" + link.telegramUserId + ")" : "&7Nicht verkn\u00fcpft"; - - 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\u00e4ge 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\u00fcr n\u00e4chste Eingabe(n) \u00fcberspringen - 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\u00fcberbr\u00fcckt. &7N\u00e4chste Nachricht geht direkt an den Server.")); - p.sendMessage(color("&7Mit &f/chatbypass &7wieder einschalten.")); - } - } - }; - ProxyServer.getInstance().getPluginManager().registerCommand(plugin, bypassCmd); - - // /discordlink – Discord-Account verkn\u00fcpfen - 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\u00fcpfung")); - p.sendMessage(color("&7Schreibe dem Bot auf Discord:")); - p.sendMessage(color("&f!link &b" + token)); - p.sendMessage(color("&7Token g\u00fcltig f\u00fcr &f10 Minuten&7.")); - p.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); - } - }; - ProxyServer.getInstance().getPluginManager().registerCommand(plugin, discordLinkCmd); - - // /telegramlink – Telegram-Account verkn\u00fcpfen - 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\u00fcpfung")); - p.sendMessage(color("&7Schreibe dem Bot auf Telegram:")); - p.sendMessage(color("&f/link &b" + token)); - p.sendMessage(color("&7Token g\u00fcltig f\u00fcr &f10 Minuten&7.")); - p.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); - } - }; - ProxyServer.getInstance().getPluginManager().registerCommand(plugin, telegramLinkCmd); - - // /unlink – Verkn\u00fcpfung 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\u00fcpfung aufgehoben.")); - else - p.sendMessage(color("&cKein Discord-Account verkn\u00fcpft.")); - break; - case "telegram": - if (linkManager.unlinkTelegram(p.getUniqueId())) - p.sendMessage(color("&aTelegram-Verkn\u00fcpfung aufgehoben.")); - else - p.sendMessage(color("&cKein Telegram-Account verkn\u00fcpft.")); - break; - case "all": - boolean d = linkManager.unlinkDiscord(p.getUniqueId()); - boolean t = linkManager.unlinkTelegram(p.getUniqueId()); - if (d || t) p.sendMessage(color("&aAlle Verkn\u00fcpfungen aufgehoben.")); - else p.sendMessage(color("&cKeine Verkn\u00fcpfungen 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\u00fcr /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\u00e4chsten 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-\u00dcbersicht - 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\u00fcr 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\u00dfen.")); - } - } - }; - 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 - // ========================================================= - - /** - * \u00d6ffentliche API f\u00fcr Sub-Server-Plugins oder BungeeCord-eigene Plugins. - * Setzt den Bypass-Status f\u00fcr 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\u00fccksichtigt 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\u00fcr 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\u00fcbersetzen damit &#RRGGBB im Display-Namen - // korrekt sitzt; der Rest wird am Ausgabepunkt via translateColors() \u00fcbersetzt. - 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)); - } - - /** - * \u00dcbersetzt 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\u00fcltige 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 \u00fcber 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\u00dcCKEN - // ========================================================= - - 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); - } - } +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.connection.Server; +import net.md_5.bungee.api.event.ChatEvent; +import net.md_5.bungee.api.event.PlayerDisconnectEvent; +import net.md_5.bungee.api.event.PluginMessageEvent; +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\u00fcr StatusAPI (BungeeCord) + * + * Features: + * ✅ Mehrere Kan\u00e4le mit Permissions + * ✅ Server-Erkennung im Chat-Format + * ✅ /helpop f\u00fcr Spieler + * ✅ Emoji-Unterst\u00fctzung (: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\u00fcr 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\u00f6gert 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\u00fcr 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\u00fcr 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<>()); + + // Laufende Chat-Spiele (NexusLobby): Servername → Spiel mit den gültigen Antworten + private final Map chatGames = new ConcurrentHashMap<>(); + + // Geyser-Pr\u00e4fix f\u00fcr 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.fine("[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\u00fccken + 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); + ProxyServer.getInstance().registerChannel(ChatGameBridge.ANSWER_CHANNEL); + registerCommands(); + + logger.fine("[ChatModule] Aktiviert – " + config.getChannels().size() + " Kan\u00e4le 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(); + chatGames.clear(); + try { ProxyServer.getInstance().unregisterChannel(ChatGameBridge.ANSWER_CHANNEL); } catch (Exception ignored) {} + } + + // ========================================================= + // CHAT-EVENTS (BungeeCord original, 1.20+) + // + // Das Bypass-Problem mit Paper: + // Wenn BungeeCord eine signierte Nachricht unver\u00e4ndert durchl\u00e4sst + // (kein setCancelled), pr\u00fcft Paper die Signatur → ung\u00fcltig → Fehler. + // Wenn wir setCancelled(true) setzen und die Nachricht selbst senden, + // fehlt die Signatur → Paper lehnt sie ebenfalls ab. + // + // L\u00f6sung: 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\u00f6tig) + // kombiniert mit BungeeCord IP-Forwarding deaktiviert Paper die Pr\u00fcfung. + // ========================================================= + + @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 + } + + // Antwort auf ein Chat-Spiel (NexusLobby auf dem Sub-Server)? + if (handleChatGameAnswer(player, e.getMessage())) { + e.setCancelled(true); + return; + } + + e.setCancelled(true); + processChat(player, e.getMessage()); + } + + // ========================================================= + // CHAT-SPIELE (NexusLobby) + // + // Der Chat läuft komplett über diesen Proxy, die Sub-Server bekommen keine Chat-Ereignisse. + // Damit ein Chat-Spiel auf einem Sub-Server die Antwort erkennt, meldet der Sub-Server Start und + // Ende eines Spiels samt gültiger Antworten (BungeeCord-Kanal, Unterkanal "NexusChatGame"). + // Passt eine Chat-Nachricht zur Antwort, melden wir sie über den Kanal "statusapi:chatgame" + // an den Sub-Server und zeigen sie (je nach Einstellung des Spiels) nicht im Chat an. + // Format und Prüfung: siehe ChatGameBridge. + // ========================================================= + + /** Nachrichten der Sub-Server: Start und Ende eines Chat-Spiels. */ + @EventHandler + public void onChatGameMessage(PluginMessageEvent e) { + if (!"BungeeCord".equals(e.getTag()) && !"bungeecord:main".equals(e.getTag())) return; + // Nur Nachrichten von Servern, niemals von Spielern + if (!(e.getSender() instanceof Server)) return; + + ChatGameBridge.Message msg = ChatGameBridge.read(e.getData(), System.currentTimeMillis()); + if (msg == null) return; + e.setCancelled(true); + + String serverName = ((Server) e.getSender()).getInfo().getName(); + if ("START".equals(msg.action)) { + chatGames.put(serverName, msg.game); + } else if ("STOP".equals(msg.action)) { + ChatGameBridge.Game running = chatGames.get(serverName); + if (running != null && running.id.equals(msg.gameId)) chatGames.remove(serverName, running); + } + } + + /** + * Prüft, ob die Nachricht die Antwort auf das laufende Chat-Spiel des Servers ist, auf dem der + * Spieler steht. Dann wird sie dem Sub-Server gemeldet. + * + * @return true, wenn die Nachricht nicht im Chat angezeigt werden soll + */ + private boolean handleChatGameAnswer(ProxiedPlayer player, String message) { + if (chatGames.isEmpty() || message == null) return false; + + Server server = player.getServer(); + if (server == null) return false; + + String serverName = server.getInfo().getName(); + ChatGameBridge.Game game = chatGames.get(serverName); + if (game == null) return false; + if (game.expired(System.currentTimeMillis())) { + chatGames.remove(serverName, game); + return false; + } + + // Gemutete Spieler dürfen nicht antworten (Admins mit Bypass werden nie gemutet) + boolean isAdmin = player.hasPermission(config.getAdminBypassPermission()); + if (!isAdmin && muteManager.isMuted(player.getUniqueId())) return false; + + if (!game.matches(message)) return false; + + // Nur der erste richtige Treffer zählt: Danach ist das Spiel hier beendet + if (!chatGames.remove(serverName, game)) return false; + + server.sendData(ChatGameBridge.ANSWER_CHANNEL, ChatGameBridge.answer(game.id, player.getUniqueId(), message)); + return game.hideAnswer; + } + + /** + * 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\u00fcr 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\u00fcr 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\u00e4hnt!")); + 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\u00fcr 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\u00fcr eine \u00dcbersicht.")); + 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\u00fccken (nur f\u00fcr 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\u00fcgbare Kan\u00e4le:")); + 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\u00fcr 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\u00e4chsten 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\u00fcr normale Spieler nicht erreichbar. + // Admins k\u00f6nnen 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\u00fcr 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\u00f6nnen 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\u00fcr &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\u00fcltige Dauer. Bitte Zahl eingeben.")); return; } + } + muteManager.mute(target.getUniqueId(), duration); + String durationStr = duration <= 0 ? "permanent" : duration + " Minuten"; + target.sendMessage(color("&cDu wurdest f\u00fcr " + durationStr + " stummgeschaltet.")); + sender.sendMessage(color("&a" + target.getName() + " wurde f\u00fcr " + durationStr + " gemutet.")); + notifyAdmins("&8[&cMute&8] &f" + (sender instanceof ProxiedPlayer ? sender.getName() : "Konsole") + + " &7hat &f" + target.getName() + " &7f\u00fcr &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 \u00fcber 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\u00fcpft"; + String telegramInfo = (link != null && !link.telegramUserId.isEmpty()) + ? "&a" + link.telegramUsername + " &8(" + link.telegramUserId + ")" : "&7Nicht verkn\u00fcpft"; + + 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\u00e4ge 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\u00fcr n\u00e4chste Eingabe(n) \u00fcberspringen + 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\u00fcberbr\u00fcckt. &7N\u00e4chste Nachricht geht direkt an den Server.")); + p.sendMessage(color("&7Mit &f/chatbypass &7wieder einschalten.")); + } + } + }; + ProxyServer.getInstance().getPluginManager().registerCommand(plugin, bypassCmd); + + // /discordlink – Discord-Account verkn\u00fcpfen + 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\u00fcpfung")); + p.sendMessage(color("&7Schreibe dem Bot auf Discord:")); + p.sendMessage(color("&f!link &b" + token)); + p.sendMessage(color("&7Token g\u00fcltig f\u00fcr &f10 Minuten&7.")); + p.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); + } + }; + ProxyServer.getInstance().getPluginManager().registerCommand(plugin, discordLinkCmd); + + // /telegramlink – Telegram-Account verkn\u00fcpfen + 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\u00fcpfung")); + p.sendMessage(color("&7Schreibe dem Bot auf Telegram:")); + p.sendMessage(color("&f/link &b" + token)); + p.sendMessage(color("&7Token g\u00fcltig f\u00fcr &f10 Minuten&7.")); + p.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); + } + }; + ProxyServer.getInstance().getPluginManager().registerCommand(plugin, telegramLinkCmd); + + // /unlink – Verkn\u00fcpfung 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\u00fcpfung aufgehoben.")); + else + p.sendMessage(color("&cKein Discord-Account verkn\u00fcpft.")); + break; + case "telegram": + if (linkManager.unlinkTelegram(p.getUniqueId())) + p.sendMessage(color("&aTelegram-Verkn\u00fcpfung aufgehoben.")); + else + p.sendMessage(color("&cKein Telegram-Account verkn\u00fcpft.")); + break; + case "all": + boolean d = linkManager.unlinkDiscord(p.getUniqueId()); + boolean t = linkManager.unlinkTelegram(p.getUniqueId()); + if (d || t) p.sendMessage(color("&aAlle Verkn\u00fcpfungen aufgehoben.")); + else p.sendMessage(color("&cKeine Verkn\u00fcpfungen 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\u00fcr /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\u00e4chsten 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-\u00dcbersicht + 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\u00fcr 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\u00dfen.")); + } + } + }; + 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 + // ========================================================= + + /** + * \u00d6ffentliche API f\u00fcr Sub-Server-Plugins oder BungeeCord-eigene Plugins. + * Setzt den Bypass-Status f\u00fcr 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\u00fccksichtigt 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\u00fcr 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\u00fcbersetzen damit &#RRGGBB im Display-Namen + // korrekt sitzt; der Rest wird am Ausgabepunkt via translateColors() \u00fcbersetzt. + 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)); + } + + /** + * \u00dcbersetzt 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\u00fcltige 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 \u00fcber 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\u00dcCKEN + // ========================================================= + + 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