diff --git a/GlobalChat/src/main/java/de/viper/globalchat/GlobalChat.java b/GlobalChat/src/main/java/de/viper/globalchat/GlobalChat.java new file mode 100644 index 0000000..ac07d26 --- /dev/null +++ b/GlobalChat/src/main/java/de/viper/globalchat/GlobalChat.java @@ -0,0 +1,575 @@ +package de.viper.globalchat; + +import net.luckperms.api.LuckPerms; +import net.luckperms.api.LuckPermsProvider; +import net.luckperms.api.model.user.User; +import net.luckperms.api.cacheddata.CachedMetaData; + +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.chat.ComponentBuilder; +import net.md_5.bungee.api.chat.HoverEvent; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.chat.HoverEvent.Action; +import net.md_5.bungee.api.config.ServerInfo; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.event.ChatEvent; +import net.md_5.bungee.api.event.ServerConnectEvent; +import net.md_5.bungee.api.event.ServerSwitchEvent; +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 java.io.*; +import java.nio.file.*; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; + +/** + * GlobalChat - Bungee Plugin + * - Filter (filter.yml) + * - Logs (plugins/GlobalChat/logs/YYYY-MM-DD.log) + * - /support, /reply, /info + * - Prefix/Suffix via LuckPerms (Proxy-side) + * - ServerSwitch announcement (sends message to chat when a player switches from one server to another) + * - Attempts to suppress Spigot join/quit messages during server switch + */ +public class GlobalChat extends Plugin implements Listener { + + private static final String CHANNEL_CONTROL = "global:control"; + + private List badWords = new ArrayList<>(); + private File logFolder; + private boolean chatMuted = false; + + // Optional: falls Spigot-Server OP-Info per PluginMessage sendet + private final Map playerIsOp = new ConcurrentHashMap<>(); + // Letzte Support-Kontakte (staff UUID -> target UUID) + private final Map lastSupportContact = new ConcurrentHashMap<>(); + // Tracking für unterdrückte Join-/Quit-Nachrichten + private final Set suppressJoinQuit = ConcurrentHashMap.newKeySet(); + + // =========================== + // Neu: Welcome-Nachrichten + // =========================== + private List welcomeMessages = new ArrayList<>(); + + private void loadWelcomeMessages() { + File file = new File(getDataFolder(), "welcome.yml"); + if (!file.exists()) { + try { + getDataFolder().mkdirs(); + file.createNewFile(); + try (PrintWriter out = new PrintWriter(file)) { + out.println("welcome-messages:"); + out.println(" - \"&aWillkommen, %player%! Viel Spaß auf unserem Server!\""); + out.println(" - \"&aHey %player%, schön dich hier zu sehen! Los geht's!\""); + out.println(" - \"&a%player%, dein Abenteuer beginnt jetzt! Viel Spaß!\""); + out.println(" - \"&aWillkommen an Bord, %player%! Entdecke den Server!\""); + out.println(" - \"&a%player%, herzlich willkommen! Lass uns loslegen!\""); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + try { + List lines = Files.readAllLines(file.toPath()); + welcomeMessages.clear(); + for (String line : lines) { + line = line.trim(); + if (line.startsWith("-")) welcomeMessages.add(line.substring(1).trim()); + } + getLogger().info("§eGeladene Welcome-Nachrichten: " + welcomeMessages.size()); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void sendRandomWelcomeMessage(ProxiedPlayer player) { + if (welcomeMessages.isEmpty()) return; + + Random rand = new Random(); + String message = welcomeMessages.get(rand.nextInt(welcomeMessages.size())); + message = message.replace("%player%", player.getName()); + message = ChatColor.translateAlternateColorCodes('&', message); + + player.sendMessage(new TextComponent(message)); + } + + @Override + public void onEnable() { + // Plugin channel registrieren (für Steuer-Nachrichten an Spigot) + try { + getProxy().registerChannel(CHANNEL_CONTROL); + } catch (Throwable ignored) { + getLogger().warning("Konnte Kanal " + CHANNEL_CONTROL + " nicht registrieren."); + } + + getProxy().getPluginManager().registerListener(this, this); + + loadFilter(); + loadWelcomeMessages(); + + logFolder = new File(getDataFolder(), "logs"); + if (!logFolder.exists()) logFolder.mkdirs(); + cleanupOldLogs(); + + // Befehle registrieren + getProxy().getPluginManager().registerCommand(this, new ReloadCommand()); + getProxy().getPluginManager().registerCommand(this, new MuteCommand()); + getProxy().getPluginManager().registerCommand(this, new SupportCommand()); + getProxy().getPluginManager().registerCommand(this, new ReplyCommand()); + getProxy().getPluginManager().registerCommand(this, new InfoCommand()); + + getLogger().info("§aGlobalChat aktiviert (Zensur, Logs, Reload, Mute, Support, Reply & Info)!"); + } + + @Override + public void onDisable() { + getLogger().info("§cGlobalChat deaktiviert!"); + try { + getProxy().unregisterChannel(CHANNEL_CONTROL); + } catch (Throwable ignored) { + getLogger().warning("Konnte Kanal " + CHANNEL_CONTROL + " nicht deregistrieren."); + } + } + + // =========================== + // Chatfilter & Global-Chat + // =========================== + @EventHandler + public void onChat(ChatEvent e) { + if (!(e.getSender() instanceof ProxiedPlayer)) return; + if (e.isCommand()) return; + + ProxiedPlayer player = (ProxiedPlayer) e.getSender(); + String originalMsg = e.getMessage(); + + // Debugging: Logge alle Chat-Nachrichten, um zu prüfen, ob Join-/Quit-Nachrichten ankommen + getLogger().info("ChatEvent: Spieler=" + player.getName() + ", Nachricht=" + originalMsg); + + // Versuche, Join-/Quit-Nachrichten zu filtern + if (suppressJoinQuit.contains(player.getUniqueId()) && + (originalMsg.contains("joined the Game") || originalMsg.contains("left the Game"))) { + getLogger().info("Unterdrücke Join-/Quit-Nachricht für " + player.getName() + ": " + originalMsg); + e.setCancelled(true); + return; + } + + // Globaler Mute + if (chatMuted && !player.hasPermission("globalchat.bypass")) { + player.sendMessage(new TextComponent("§cDer globale Chat ist derzeit deaktiviert!")); + e.setCancelled(true); + return; + } + + // Badword-Zensur (nur für Chat-Ausgabe; Log bleibt unzensiert) + String censoredMsg = originalMsg; + for (String bad : badWords) { + if (bad == null || bad.trim().isEmpty()) continue; + censoredMsg = censoredMsg.replaceAll("(?i)" + Pattern.quote(bad), repeat("*", bad.length())); + } + + e.setCancelled(true); + + String serverName = player.getServer().getInfo().getName(); + + // Prefix/Suffix holen (LuckPerms Proxy-side) + String[] ps = getPrefixSuffix(player); + String prefix = ps[0] == null ? "" : ps[0].trim(); + String suffix = ps[1] == null ? "" : ps[1].trim(); + + // Entscheide: zeige entweder Prefix (falls vorhanden) oder, falls kein Prefix, das Suffix. + String displayTag = ""; + if (!prefix.isEmpty()) { + displayTag = prefix; + } else if (!suffix.isEmpty()) { + displayTag = suffix; + } + + // saubere Leerzeichen um displayTag + if (!displayTag.isEmpty() && !displayTag.endsWith(" ")) displayTag = displayTag + " "; + + // Format: [Server] Name: Nachricht + StringBuilder out = new StringBuilder(); + out.append("§7[").append(serverName).append("] "); + if (!displayTag.isEmpty()) out.append(displayTag); + out.append(player.getName()); + out.append("§f: ").append(censoredMsg); + + String chatOut = out.toString(); + + for (ProxiedPlayer p : getProxy().getPlayers()) { + p.sendMessage(new TextComponent(chatOut)); + } + + // Log unzensiert (ohne color codes) + String logEntry = "[" + serverName + "] " + + (displayTag.isEmpty() ? "" : stripColor(displayTag) + " ") + + player.getName() + + ": " + originalMsg; + logMessage(logEntry); + } + + // =========================== + // Server connect (pre-switch suppression) + // =========================== + @EventHandler + public void onServerConnect(ServerConnectEvent e) { + if (e.isCancelled()) return; + + ProxiedPlayer player = e.getPlayer(); + ServerInfo target = e.getTarget(); + ServerInfo from = player.getServer() != null ? player.getServer().getInfo() : null; + + if (from == null || from.equals(target)) return; + + // Markiere Spieler für Unterdrückung + suppressJoinQuit.add(player.getUniqueId()); + getLogger().info("Markiert " + player.getName() + " für Join-/Quit-Unterdrückung"); + + // Suppress quit on old server + try { + sendSuppressJoinQuit(from, player.getUniqueId()); + getLogger().info("Sent suppress quit message for " + player.getName() + " to server " + from.getName()); + } catch (Throwable ex) { + getLogger().warning("Fehler beim Senden der Quit-Unterdrückung an " + from.getName() + ": " + ex.getMessage()); + } + + // Suppress join on new server + try { + sendSuppressJoinQuit(target, player.getUniqueId()); + getLogger().info("Sent suppress join message for " + player.getName() + " to server " + target.getName()); + } catch (Throwable ex) { + getLogger().warning("Fehler beim Senden der Join-Unterdrückung an " + target.getName() + ": " + ex.getMessage()); + } + + // Entferne Unterdrückung nach kurzer Zeit (2 Sekunden) + getProxy().getScheduler().schedule(this, () -> { + suppressJoinQuit.remove(player.getUniqueId()); + getLogger().info("Entfernte Unterdrückung für " + player.getName()); + }, 2, java.util.concurrent.TimeUnit.SECONDS); + } + + // =========================== + // Server switch announcement + // =========================== + @EventHandler + public void onServerSwitch(ServerSwitchEvent e) { + ProxiedPlayer player = e.getPlayer(); + ServerInfo from = e.getFrom(); // kann null sein beim ersten Join + ServerInfo to = player.getServer() != null ? player.getServer().getInfo() : null; + + if (to == null) return; + + // Wenn from == null -> erster Join, keine Nachricht senden + if (from == null) return; + + // Nur senden, wenn Server wirklich gewechselt wurde + if (from.getName().equalsIgnoreCase(to.getName())) return; + + String fromName = from.getName(); + String toName = to.getName(); + + // Prefix/Suffix für Anzeige (show either prefix or suffix) + String[] ps = getPrefixSuffix(player); + String prefix = ps[0] == null ? "" : ps[0].trim(); + String suffix = ps[1] == null ? "" : ps[1].trim(); + + String displayTag = ""; + if (!prefix.isEmpty()) displayTag = prefix; + else if (!suffix.isEmpty()) displayTag = suffix; + + if (!displayTag.isEmpty() && !displayTag.endsWith(" ")) displayTag = displayTag + " "; + + StringBuilder msg = new StringBuilder(); + msg.append("§7[").append(toName).append("] "); + if (!displayTag.isEmpty()) msg.append(displayTag); + msg.append(player.getName()); + msg.append(" §7hat den Server gewechselt: §e") + .append(fromName).append(" §7→ §e").append(toName).append("§7."); + + String finalMsg = msg.toString(); + + // An alle Spieler senden (sichtbar im Chat) + for (ProxiedPlayer p : getProxy().getPlayers()) { + p.sendMessage(new TextComponent(finalMsg)); + } + + // Log (ohne Farb-Codes) + String logEntry = "[" + toName + "] " + + (displayTag.isEmpty() ? "" : stripColor(displayTag) + " ") + + player.getName() + " hat den Server gewechselt: " + fromName + " -> " + toName + "."; + logMessage(logEntry); + } + + /** + * Sendet an den Ziel-Server eine PluginMessage mit dem Befehl, Join/Quit für den Spieler zu unterdrücken. + * Format: writeUTF("suppress"); writeUTF(playerUUID) + */ + private void sendSuppressJoinQuit(ServerInfo server, UUID playerId) { + if (server == null) return; + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream out = new DataOutputStream(baos)) { + out.writeUTF("suppress"); + out.writeUTF(playerId.toString()); + server.sendData(CHANNEL_CONTROL, baos.toByteArray()); + } catch (IOException ex) { + getLogger().warning("Fehler beim Senden der suppress-Nachricht an " + server.getName() + ": " + ex.getMessage()); + } + } + + // Java8-kompatible repeat + private String repeat(String str, int count) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < count; i++) sb.append(str); + return sb.toString(); + } + + // =========================== + // Prefix/Suffix via LuckPerms (Proxy-side) + // =========================== + private String[] getPrefixSuffix(ProxiedPlayer player) { + String prefix = ""; + String suffix = ""; + + try { + LuckPerms lp = LuckPermsProvider.get(); + if (lp != null) { + // Versuche schnell aus dem Cache zu holen + User user = lp.getUserManager().getUser(player.getUniqueId()); + + // Falls nicht im Cache: synchrones Laden (Fallback) — kann blockieren, aber stellt Prefix sicher + if (user == null) { + try { + user = lp.getUserManager().loadUser(player.getUniqueId()).join(); + } catch (Exception ignored) { + user = null; + } + } + + if (user != null) { + CachedMetaData meta = user.getCachedData().getMetaData(); + if (meta != null) { + String p = meta.getPrefix(); + String s = meta.getSuffix(); + if (p != null) prefix = p; + if (s != null) suffix = s; + } + } + } + } catch (Throwable ignored) { + // LuckPerms nicht vorhanden oder andere Fehler -> kein Prefix/Suffix + } + + // Farbcodes übersetzen (& -> §) + if (prefix != null && !prefix.isEmpty()) prefix = ChatColor.translateAlternateColorCodes('&', prefix); + if (suffix != null && !suffix.isEmpty()) suffix = ChatColor.translateAlternateColorCodes('&', suffix); + + if (prefix == null) prefix = ""; + if (suffix == null) suffix = ""; + + return new String[]{prefix, suffix}; + } + + // Entfernt Bungee-Farbcodes aus Strings (für saubere Logs) + private String stripColor(String s) { + if (s == null) return ""; + return ChatColor.stripColor(s); + } + + // =========================== + // Filter einlesen + // =========================== + private void loadFilter() { + File file = new File(getDataFolder(), "filter.yml"); + if (!file.exists()) { + try { + getDataFolder().mkdirs(); + file.createNewFile(); + try (PrintWriter out = new PrintWriter(file)) { + out.println("badwords:"); + out.println(" - arsch"); + out.println(" - hurensohn"); + out.println(" - scheiße"); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + try { + List lines = Files.readAllLines(file.toPath()); + badWords.clear(); + for (String line : lines) { + line = line.trim(); + if (line.startsWith("-")) badWords.add(line.substring(1).trim()); + } + getLogger().info("§eGeladene Filter-Wörter: " + badWords.size()); + } catch (IOException e) { + e.printStackTrace(); + } + } + + // =========================== + // Logs aufräumen / schreiben + // =========================== + private void cleanupOldLogs() { + File[] files = logFolder.listFiles(); + if (files == null) return; + + long now = System.currentTimeMillis(); + long sevenDays = 1000L * 60 * 60 * 24 * 7; + + for (File f : files) { + if (now - f.lastModified() > sevenDays) { + f.delete(); + } + } + } + + private void logMessage(String message) { + String date = new SimpleDateFormat("yyyy-MM-dd").format(new Date()); + File logFile = new File(logFolder, date + ".log"); + + try (BufferedWriter bw = new BufferedWriter(new FileWriter(logFile, true))) { + String time = new SimpleDateFormat("HH:mm:ss").format(new Date()); + bw.write("[" + time + "] " + message); + bw.newLine(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + // =========================== + // Staff-Check (Op / team perms) + // =========================== + private boolean isStaff(ProxiedPlayer p) { + if (p == null) return false; + Boolean reportedOp = playerIsOp.get(p.getUniqueId()); + if (reportedOp != null && reportedOp) return true; + + if (p.hasPermission("team")) return true; + if (p.hasPermission("bungeecord.admin")) return true; + if (p.hasPermission("globalchat.op")) return true; + if (p.hasPermission("*")) return true; + + if (p.hasPermission("bungeecord.command.alert")) return true; + if (p.hasPermission("bungeecord.command.reload")) return true; + if (p.hasPermission("bungeecord.command.kick")) return true; + if (p.hasPermission("bungeecord.command.send")) return true; + if (p.hasPermission("bungeecord.command.perms")) return true; + + return false; + } + + // =========================== + // Commands + // =========================== + public class ReloadCommand extends Command { + public ReloadCommand() { super("globalreload", "globalchat.reload"); } + @Override + public void execute(CommandSender sender, String[] args) { + loadFilter(); + sender.sendMessage(new TextComponent("§aFilter wurde neu geladen!")); + } + } + + public class MuteCommand extends Command { + public MuteCommand() { super("globalmute", "globalchat.mute"); } + @Override + public void execute(CommandSender sender, String[] args) { + chatMuted = !chatMuted; + String status = chatMuted ? "§caktiviert" : "§aaufgehoben"; + for (ProxiedPlayer p : getProxy().getPlayers()) { + p.sendMessage(new TextComponent("§7[GlobalChat] §eDer globale Chat Mute wurde " + status + "§e!")); + } + getLogger().info("GlobalMute wurde " + (chatMuted ? "aktiviert" : "deaktiviert") + "."); + } + } + + // /support + public class SupportCommand extends Command { + public SupportCommand() { super("support"); } + @Override + public void execute(CommandSender sender, String[] args) { + if (!(sender instanceof ProxiedPlayer)) { + sender.sendMessage(new TextComponent("§cNur Spieler können Support-Nachrichten senden.")); + return; + } + + ProxiedPlayer player = (ProxiedPlayer) sender; + if (args.length == 0) { + player.sendMessage(new TextComponent("§cBitte eine Nachricht angeben: /support ")); + return; + } + + String msg = String.join(" ", args); + String serverName = player.getServer().getInfo().getName(); + + TextComponent supportMsg = new TextComponent("§7[Support] §b" + player.getName() + " §7vom Server §e" + serverName + " §7: §f" + msg); + supportMsg.setHoverEvent(new HoverEvent(Action.SHOW_TEXT, new ComponentBuilder("Klicke, um /reply " + player.getName() + " zu schreiben").create())); + supportMsg.setClickEvent(new net.md_5.bungee.api.chat.ClickEvent(net.md_5.bungee.api.chat.ClickEvent.Action.SUGGEST_COMMAND, "/reply " + player.getName() + " ")); + + // an alle Staff + for (ProxiedPlayer p : getProxy().getPlayers()) { + if (isStaff(p)) { + p.sendMessage(supportMsg); + lastSupportContact.put(p.getUniqueId(), player.getUniqueId()); + } + } + + player.sendMessage(new TextComponent("§aDeine Support-Nachricht wurde gesendet.")); + logMessage("[Support][" + serverName + "] " + player.getName() + ": " + msg); + } + } + + // /reply + public class ReplyCommand extends Command { + public ReplyCommand() { super("reply"); } + @Override + public void execute(CommandSender sender, String[] args) { + if (!(sender instanceof ProxiedPlayer)) return; + + ProxiedPlayer staff = (ProxiedPlayer) sender; + UUID targetId = lastSupportContact.get(staff.getUniqueId()); + if (targetId == null) { + staff.sendMessage(new TextComponent("§cKein Spieler zum Antworten gefunden.")); + return; + } + + if (args.length == 0) { + staff.sendMessage(new TextComponent("§cBitte eine Nachricht angeben.")); + return; + } + + ProxiedPlayer target = getProxy().getPlayer(targetId); + if (target == null) { + staff.sendMessage(new TextComponent("§cSpieler ist nicht online.")); + return; + } + + String msg = String.join(" ", args); + target.sendMessage(new TextComponent("§7[Reply von §b" + staff.getName() + "§7]: §f" + msg)); + staff.sendMessage(new TextComponent("§aDeine Nachricht wurde an §b" + target.getName() + "§a gesendet.")); + } + } + + // /info + public class InfoCommand extends Command { + public InfoCommand() { super("info"); } + @Override + public void execute(CommandSender sender, String[] args) { + sender.sendMessage(new TextComponent("§8§m------------------------------")); + sender.sendMessage(new TextComponent("§6§lGlobalChat Info")); + sender.sendMessage(new TextComponent("§ePlugin-Name: §b" + getDescription().getName())); + sender.sendMessage(new TextComponent("§eVersion: §b" + getDescription().getVersion())); + sender.sendMessage(new TextComponent("§eErsteller: §bM_Viper")); + sender.sendMessage(new TextComponent("§8§m------------------------------")); + } + } +} \ No newline at end of file