Upload folder via GUI - src

This commit is contained in:
Git Manager GUI
2026-04-13 10:00:00 +02:00
parent cfc9773ca6
commit 70d264f9bf
24 changed files with 4430 additions and 4385 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -102,8 +102,6 @@ public class UpdateChecker {
plugin.getLogger().warning("Keine StatusAPI.jar im neuesten Release gefunden."); plugin.getLogger().warning("Keine StatusAPI.jar im neuesten Release gefunden.");
return; return;
} }
plugin.getLogger().info("Gefundene Version: " + foundVersion + " (Aktuell: " + currentVersion + ")");
latestVersion = foundVersion; latestVersion = foundVersion;
latestUrl = foundUrl; latestUrl = foundUrl;

View File

@@ -1,60 +1,59 @@
package net.viper.status.module; package net.viper.status.module;
import net.md_5.bungee.api.plugin.Plugin; import net.md_5.bungee.api.plugin.Plugin;
import java.util.Collection; import java.util.LinkedHashMap;
import java.util.HashMap; import java.util.Map;
import java.util.Map;
/**
/** * Verwaltet alle geladenen Module.
* Verwaltet alle geladenen Module. * Verwendet LinkedHashMap um die Registrierungsreihenfolge zu erhalten,
*/ * damit Abhängigkeiten (z.B. VanishModule → ChatModule) korrekt aufgelöst werden.
public class ModuleManager { */
public class ModuleManager {
private final Map<String, Module> modules = new HashMap<>();
private final Map<String, Module> modules = new LinkedHashMap<>();
public void registerModule(Module module) {
modules.put(module.getName().toLowerCase(), module); public void registerModule(Module module) {
} modules.put(module.getName().toLowerCase(), module);
}
public void enableAll(Plugin plugin) {
for (Module module : modules.values()) { public void enableAll(Plugin plugin) {
try { for (Module module : modules.values()) {
plugin.getLogger().info("Aktiviere Modul: " + module.getName() + "..."); try {
module.onEnable(plugin); module.onEnable(plugin);
} catch (Exception e) { } catch (Exception e) {
plugin.getLogger().severe("Fehler beim Aktivieren von Modul " + module.getName() + ": " + e.getMessage()); plugin.getLogger().severe("Fehler beim Aktivieren von Modul " + module.getName() + ": " + e.getMessage());
e.printStackTrace(); e.printStackTrace();
} }
} }
} }
public void disableAll(Plugin plugin) { public void disableAll(Plugin plugin) {
for (Module module : modules.values()) { for (Module module : modules.values()) {
try { try {
plugin.getLogger().info("Deaktiviere Modul: " + module.getName() + "..."); module.onDisable(plugin);
module.onDisable(plugin); } catch (Exception e) {
} catch (Exception e) { plugin.getLogger().warning("Fehler beim Deaktivieren von Modul " + module.getName());
plugin.getLogger().warning("Fehler beim Deaktivieren von Modul " + module.getName()); }
} }
} modules.clear();
modules.clear(); }
}
/**
/** * Ermöglicht anderen Komponenten (wie dem WebServer) Zugriff auf spezifische Module.
* Ermöglicht anderen Komponenten (wie dem WebServer) Zugriff auf spezifische Module. */
*/ public Module getModule(String name) {
public Module getModule(String name) { return modules.get(name.toLowerCase());
return modules.get(name.toLowerCase()); }
}
@SuppressWarnings("unchecked")
@SuppressWarnings("unchecked") public <T extends Module> T getModule(Class<T> clazz) {
public <T extends Module> T getModule(Class<T> clazz) { for (Module m : modules.values()) {
for (Module m : modules.values()) { if (clazz.isInstance(m)) {
if (clazz.isInstance(m)) { return (T) m;
return (T) m; }
} }
} return null;
return null; }
} }
}

View File

@@ -1,115 +1,132 @@
package net.viper.status.modules.AutoMessage; package net.viper.status.modules.AutoMessage;
import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.ProxyServer; import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.plugin.Plugin; import net.md_5.bungee.api.chat.TextComponent;
import net.viper.status.StatusAPI; import net.md_5.bungee.api.plugin.Command;
import net.viper.status.module.Module; import net.md_5.bungee.api.plugin.Plugin;
import net.viper.status.StatusAPI;
import java.io.File; import net.viper.status.module.Module;
import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.io.File;
import java.nio.file.Files; import java.io.IOException;
import java.util.List; import java.nio.charset.StandardCharsets;
import java.util.Properties; import java.nio.file.Files;
import java.util.concurrent.TimeUnit; import java.util.List;
import java.util.Properties;
public class AutoMessageModule implements Module { import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
private int taskId = -1;
/**
// Diese Methode fehlte bisher und ist zwingend für das Interface * AutoMessageModule
@Override *
public String getName() { * Fix #5:
return "AutoMessage"; * - Nachrichten werden bei jedem Zyklus frisch aus der Datei gelesen,
} * damit Änderungen an messages.txt sofort wirken ohne Neustart.
* - Neuer Befehl /automessage reload (Permission: statusapi.automessage)
@Override * lädt die Konfiguration neu und setzt den Zähler zurück.
public void onEnable(Plugin plugin) { * - TextComponent.fromLegacy() → ChatColor.translateAlternateColorCodes für §-Codes.
// Hier casten wir das Plugin-Objekt zu StatusAPI, um an spezifische Methoden zu kommen */
StatusAPI api = (StatusAPI) plugin; public class AutoMessageModule implements Module {
// Konfiguration aus der zentralen verify.properties laden private int taskId = -1;
Properties props = api.getVerifyProperties(); private StatusAPI api;
private final AtomicInteger currentIndex = new AtomicInteger(0);
boolean enabled = Boolean.parseBoolean(props.getProperty("automessage.enabled", "false"));
// Konfiguration (für Reload zugänglich)
if (!enabled) { private volatile boolean enabled = false;
api.getLogger().info("AutoMessage-Modul ist deaktiviert."); private volatile int intervalSeconds = 300;
return; private volatile String fileName = "messages.txt";
} private volatile String prefix = "";
// Interval in Sekunden einlesen @Override
int intervalSeconds; public String getName() { return "AutoMessage"; }
try {
intervalSeconds = Integer.parseInt(props.getProperty("automessage.interval", "300")); @Override
} catch (NumberFormatException e) { public void onEnable(Plugin plugin) {
api.getLogger().warning("Ungültiges Intervall für AutoMessage! Nutze Standard (300s)."); this.api = (StatusAPI) plugin;
intervalSeconds = 300; loadSettings();
}
if (!enabled) return;
// Dateiname einlesen (Standard: messages.txt)
String fileName = props.getProperty("automessage.file", "messages.txt"); registerReloadCommand();
File messageFile = new File(api.getDataFolder(), fileName); scheduleTask();
}
if (!messageFile.exists()) {
api.getLogger().warning("Die Datei '" + fileName + "' wurde nicht gefunden (" + messageFile.getAbsolutePath() + ")!"); @Override
api.getLogger().info("Erstelle eine leere Datei '" + fileName + "' als Vorlage..."); public void onDisable(Plugin plugin) {
try { cancelTask();
messageFile.createNewFile(); }
} catch (IOException e) {
api.getLogger().severe("Konnte Datei nicht erstellen: " + e.getMessage()); private void loadSettings() {
} Properties props = api.getVerifyProperties();
return; enabled = Boolean.parseBoolean(props.getProperty("automessage.enabled", "false"));
} String rawInterval = props.getProperty("automessage.interval", "300");
try { intervalSeconds = Integer.parseInt(rawInterval); }
// Nachrichten aus der Datei lesen catch (NumberFormatException e) { api.getLogger().warning("Ungültiges Intervall für AutoMessage! Nutze Standard (300s)."); intervalSeconds = 300; }
List<String> messages; fileName = props.getProperty("automessage.file", "messages.txt");
try { prefix = props.getProperty("automessage.prefix", "");
messages = Files.readAllLines(messageFile.toPath(), StandardCharsets.UTF_8); }
} catch (IOException e) {
api.getLogger().severe("Fehler beim Lesen von '" + fileName + "': " + e.getMessage()); private void registerReloadCommand() {
return; ProxyServer.getInstance().getPluginManager().registerCommand(api, new Command("automessage", "statusapi.automessage") {
} @Override
public void execute(CommandSender sender, String[] args) {
// Leere Zeilen und Kommentare herausfiltern if (args.length > 0 && "reload".equalsIgnoreCase(args[0])) {
messages.removeIf(line -> line.trim().isEmpty() || line.trim().startsWith("#")); cancelTask();
loadSettings();
if (messages.isEmpty()) { currentIndex.set(0);
api.getLogger().warning("Die Datei '" + fileName + "' enthält keine gültigen Nachrichten!"); if (enabled) {
return; scheduleTask();
} sender.sendMessage(ChatColor.GREEN + "[AutoMessage] Neu geladen. Intervall: " + intervalSeconds + "s");
} else {
// Optional: Prefix aus Config lesen sender.sendMessage(ChatColor.YELLOW + "[AutoMessage] Modul ist deaktiviert (automessage.enabled=false).");
String prefixRaw = props.getProperty("automessage.prefix", ""); }
String prefix = ChatColor.translateAlternateColorCodes('&', prefixRaw); } else {
sender.sendMessage(ChatColor.YELLOW + "/automessage reload");
api.getLogger().info("Starte AutoMessage-Task (" + messages.size() + " Nachrichten aus " + fileName + ")"); }
}
// Finaler Index für den Lambda-Ausdruck });
final int[] currentIndex = {0}; }
// Task planen private void scheduleTask() {
taskId = ProxyServer.getInstance().getScheduler().schedule(plugin, () -> { taskId = ProxyServer.getInstance().getScheduler().schedule(api, () -> {
String msg = messages.get(currentIndex[0]); File messageFile = new File(api.getDataFolder(), fileName);
if (!messageFile.exists()) {
String finalMessage = (prefix.isEmpty() ? "" : prefix + " ") + msg; api.getLogger().warning("[AutoMessage] Datei nicht gefunden: " + messageFile.getAbsolutePath());
return;
// Nachricht an alle auf dem Proxy senden }
ProxyServer.getInstance().broadcast(TextComponent.fromLegacy(finalMessage));
// Fix #5: Datei bei jedem Tick neu einlesen → Änderungen wirken sofort
// Index erhöhen und Loop starten List<String> messages;
currentIndex[0] = (currentIndex[0] + 1) % messages.size(); try {
}, intervalSeconds, intervalSeconds, TimeUnit.SECONDS).getId(); messages = Files.readAllLines(messageFile.toPath(), StandardCharsets.UTF_8);
} } catch (IOException e) {
api.getLogger().severe("[AutoMessage] Fehler beim Lesen von '" + fileName + "': " + e.getMessage());
@Override return;
public void onDisable(Plugin plugin) { }
if (taskId != -1) { messages.removeIf(line -> line.trim().isEmpty() || line.trim().startsWith("#"));
ProxyServer.getInstance().getScheduler().cancel(taskId); if (messages.isEmpty()) return;
taskId = -1;
plugin.getLogger().info("AutoMessage-Task gestoppt."); // Index wrappen (threadsafe)
} int idx = currentIndex.getAndUpdate(i -> (i + 1) % messages.size());
} if (idx >= messages.size()) idx = 0;
}
String raw = messages.get(idx);
String prefixPart = prefix.isEmpty() ? "" : ChatColor.translateAlternateColorCodes('&', prefix) + " ";
// Fix: §-Codes direkt übersetzen (messages.txt nutzt §-Codes)
String text = prefixPart + ChatColor.translateAlternateColorCodes('&',
raw.replace("\u00a7", "&").replace("§", "&"));
ProxyServer.getInstance().broadcast(new TextComponent(text));
}, intervalSeconds, intervalSeconds, TimeUnit.SECONDS).getId();
}
private void cancelTask() {
if (taskId != -1) {
ProxyServer.getInstance().getScheduler().cancel(taskId);
taskId = -1;
}
}
}

View File

@@ -1,381 +1,313 @@
package net.viper.status.modules.broadcast; package net.viper.status.modules.broadcast;
import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.plugin.Plugin; import net.md_5.bungee.api.plugin.Plugin;
import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.plugin.Listener; import net.md_5.bungee.api.plugin.Listener;
import net.viper.status.module.Module; import net.viper.status.module.Module;
import java.io.*; import java.io.*;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
* BroadcastModule * BroadcastModule
* *
* Speichert geplante Broadcasts jetzt persistent in 'broadcasts.schedules'. * Fixes:
* Beim Neustart werden diese automatisch wieder geladen. * - loadSchedules(): ID-Split nutzt jetzt indexOf/lastIndexOf statt split("\\.") mit length==2-Check.
*/ * Damit werden auch clientScheduleIds die Punkte enthalten korrekt geladen. (Bug #2)
public class BroadcastModule implements Module, Listener { */
public class BroadcastModule implements Module, Listener {
private Plugin plugin;
private boolean enabled = true; private Plugin plugin;
private String requiredApiKey = ""; private boolean enabled = true;
private String format = "%prefix% %message%"; private String requiredApiKey = "";
private String fallbackPrefix = "[Broadcast]"; private String format = "%prefix% %message%";
private String fallbackPrefixColor = "&c"; private String fallbackPrefix = "[Broadcast]";
private String fallbackBracketColor = "&8"; // Neu private String fallbackPrefixColor = "&c";
private String fallbackMessageColor = "&f"; private String fallbackBracketColor = "&8";
private String fallbackMessageColor = "&f";
private final Map<String, ScheduledBroadcast> scheduledByClientId = new ConcurrentHashMap<>();
private File schedulesFile; private final Map<String, ScheduledBroadcast> scheduledByClientId = new ConcurrentHashMap<>();
private final SimpleDateFormat dateFormat; private File schedulesFile;
private final SimpleDateFormat dateFormat;
public BroadcastModule() {
dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z"); public BroadcastModule() {
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
} dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
}
@Override
public String getName() { @Override
return "BroadcastModule"; public String getName() { return "BroadcastModule"; }
}
@Override
@Override public void onEnable(Plugin plugin) {
public void onEnable(Plugin plugin) { this.plugin = plugin;
this.plugin = plugin; schedulesFile = new File(plugin.getDataFolder(), "broadcasts.schedules");
schedulesFile = new File(plugin.getDataFolder(), "broadcasts.schedules"); loadConfig();
loadConfig(); if (!enabled) return;
try { plugin.getProxy().getPluginManager().registerListener(plugin, this); } catch (Throwable ignored) {}
if (!enabled) { plugin.getLogger().info("[BroadcastModule] aktiviert. Format: " + format);
plugin.getLogger().info("[BroadcastModule] deaktiviert via verify.properties (broadcast.enabled=false)"); loadSchedules();
return; plugin.getProxy().getScheduler().schedule(plugin, this::processScheduled, 1, 1, TimeUnit.SECONDS);
} }
try { @Override
plugin.getProxy().getPluginManager().registerListener(plugin, this); public void onDisable(Plugin plugin) {
} catch (Throwable ignored) {} saveSchedules();
scheduledByClientId.clear();
plugin.getLogger().info("[BroadcastModule] aktiviert. Format: " + format); }
loadSchedules();
plugin.getProxy().getScheduler().schedule(plugin, this::processScheduled, 1, 1, TimeUnit.SECONDS); private void loadConfig() {
} File file = new File(plugin.getDataFolder(), "verify.properties");
if (!file.exists()) return;
@Override try (InputStream in = new FileInputStream(file)) {
public void onDisable(Plugin plugin) { Properties props = new Properties();
plugin.getLogger().info("[BroadcastModule] deaktiviert."); props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
saveSchedules(); enabled = Boolean.parseBoolean(props.getProperty("broadcast.enabled", "true"));
scheduledByClientId.clear(); requiredApiKey = props.getProperty("broadcast.api_key", "").trim();
} format = props.getProperty("broadcast.format", format).trim();
if (format.isEmpty()) format = "%prefix% %message%";
private void loadConfig() { fallbackPrefix = props.getProperty("broadcast.prefix", fallbackPrefix).trim();
File file = new File(plugin.getDataFolder(), "verify.properties"); fallbackPrefixColor = props.getProperty("broadcast.prefix-color", fallbackPrefixColor).trim();
if (!file.exists()) { fallbackBracketColor = props.getProperty("broadcast.bracket-color", fallbackBracketColor).trim();
enabled = true; fallbackMessageColor = props.getProperty("broadcast.message-color", fallbackMessageColor).trim();
requiredApiKey = ""; } catch (IOException e) {
format = "%prefix% %message%"; plugin.getLogger().warning("[BroadcastModule] Fehler beim Laden von verify.properties: " + e.getMessage());
fallbackPrefix = "[Broadcast]"; }
fallbackPrefixColor = "&c"; }
fallbackBracketColor = "&8"; // Neu
fallbackMessageColor = "&f"; public boolean handleBroadcast(String sourceName, String message, String type, String apiKeyHeader,
return; String prefix, String prefixColor, String bracketColor, String messageColor) {
} loadConfig();
if (!enabled) return false;
try (InputStream in = new FileInputStream(file)) { if (requiredApiKey != null && !requiredApiKey.isEmpty()) {
Properties props = new Properties(); if (apiKeyHeader == null || !requiredApiKey.equals(apiKeyHeader)) {
props.load(new InputStreamReader(in, StandardCharsets.UTF_8)); plugin.getLogger().warning("[BroadcastModule] Broadcast abgelehnt: API-Key fehlt oder inkorrekt.");
enabled = Boolean.parseBoolean(props.getProperty("broadcast.enabled", "true")); return false;
requiredApiKey = props.getProperty("broadcast.api_key", "").trim(); }
format = props.getProperty("broadcast.format", format).trim(); }
if (format.isEmpty()) format = "%prefix% %message%";
fallbackPrefix = props.getProperty("broadcast.prefix", fallbackPrefix).trim(); if (message == null) message = "";
fallbackPrefixColor = props.getProperty("broadcast.prefix-color", fallbackPrefixColor).trim(); if (sourceName == null || sourceName.isEmpty()) sourceName = "System";
fallbackBracketColor = props.getProperty("broadcast.bracket-color", fallbackBracketColor).trim(); // Neu if (type == null) type = "global";
fallbackMessageColor = props.getProperty("broadcast.message-color", fallbackMessageColor).trim();
} catch (IOException e) { String usedPrefix = (prefix != null && !prefix.trim().isEmpty()) ? prefix.trim() : fallbackPrefix;
plugin.getLogger().warning("[BroadcastModule] Fehler beim Laden von verify.properties: " + e.getMessage()); String usedPrefixColor = (prefixColor != null && !prefixColor.trim().isEmpty()) ? prefixColor.trim() : fallbackPrefixColor;
} String usedBracketColor = (bracketColor != null && !bracketColor.trim().isEmpty()) ? bracketColor.trim() : fallbackBracketColor;
} String usedMessageColor = (messageColor != null && !messageColor.trim().isEmpty()) ? messageColor.trim() : fallbackMessageColor;
public boolean handleBroadcast(String sourceName, String message, String type, String apiKeyHeader, String prefixColorCode = normalizeColorCode(usedPrefixColor);
String prefix, String prefixColor, String bracketColor, String messageColor) { String bracketColorCode = normalizeColorCode(usedBracketColor);
loadConfig(); String messageColorCode = normalizeColorCode(usedMessageColor);
if (!enabled) { String finalPrefix;
plugin.getLogger().info("[BroadcastModule] Broadcast abgelehnt: Modul ist deaktiviert."); if (!bracketColorCode.isEmpty()) {
return false; String textContent = usedPrefix;
} if (textContent.startsWith("[")) textContent = textContent.substring(1);
if (textContent.endsWith("]")) textContent = textContent.substring(0, textContent.length() - 1);
if (requiredApiKey != null && !requiredApiKey.isEmpty()) { finalPrefix = bracketColorCode + "[" + prefixColorCode + textContent + bracketColorCode + "]" + ChatColor.RESET;
if (apiKeyHeader == null || !requiredApiKey.equals(apiKeyHeader)) { } else {
plugin.getLogger().warning("[BroadcastModule] Broadcast abgelehnt: API-Key fehlt oder inkorrekt."); finalPrefix = prefixColorCode + usedPrefix + ChatColor.RESET;
return false; }
}
} String coloredMessage = (messageColorCode.isEmpty() ? "" : messageColorCode) + message;
String out = format
if (message == null) message = ""; .replace("%name%", sourceName)
if (sourceName == null || sourceName.isEmpty()) sourceName = "System"; .replace("%prefix%", finalPrefix)
if (type == null) type = "global"; .replace("%prefixColored%", finalPrefix)
.replace("%message%", message)
String usedPrefix = (prefix != null && !prefix.trim().isEmpty()) ? prefix.trim() : fallbackPrefix; .replace("%messageColored%",coloredMessage)
String usedPrefixColor = (prefixColor != null && !prefixColor.trim().isEmpty()) ? prefixColor.trim() : fallbackPrefixColor; .replace("%type%", type);
String usedBracketColor = (bracketColor != null && !bracketColor.trim().isEmpty()) ? bracketColor.trim() : fallbackBracketColor; // Neu
String usedMessageColor = (messageColor != null && !messageColor.trim().isEmpty()) ? messageColor.trim() : fallbackMessageColor; TextComponent tc = new TextComponent(out);
int sent = 0;
String prefixColorCode = normalizeColorCode(usedPrefixColor); for (ProxiedPlayer p : plugin.getProxy().getPlayers()) {
String bracketColorCode = normalizeColorCode(usedBracketColor); // Neu try { p.sendMessage(tc); sent++; } catch (Throwable ignored) {}
String messageColorCode = normalizeColorCode(usedMessageColor); }
plugin.getLogger().info("[BroadcastModule] Broadcast gesendet (Empfänger=" + sent + "): " + message);
// --- KLAMMER LOGIK --- return true;
String finalPrefix; }
// Wenn eine Klammerfarbe gesetzt ist, bauen wir den Prefix neu zusammen private String normalizeColorCode(String code) {
// Format: [BracketColor][ [PrefixColor]Text [BracketColor]] if (code == null) return "";
if (!bracketColorCode.isEmpty()) { code = code.trim();
String textContent = usedPrefix; if (code.isEmpty()) return "";
// Entferne manuelle Klammern, falls der User [Broadcast] in das Textfeld geschrieben hat return code.contains("&") ? ChatColor.translateAlternateColorCodes('&', code) : code;
if (textContent.startsWith("[")) textContent = textContent.substring(1); }
if (textContent.endsWith("]")) textContent = textContent.substring(0, textContent.length() - 1);
private void saveSchedules() {
finalPrefix = bracketColorCode + "[" + prefixColorCode + textContent + bracketColorCode + "]" + ChatColor.RESET; Properties props = new Properties();
} else { for (Map.Entry<String, ScheduledBroadcast> entry : scheduledByClientId.entrySet()) {
// Altes Verhalten: Ganzen String einfärben String id = entry.getKey();
finalPrefix = prefixColorCode + usedPrefix + ChatColor.RESET; ScheduledBroadcast sb = entry.getValue();
} // Wir escapen den ID-Wert damit Punkte in der ID nicht den Parser verwirren
// --------------------- props.setProperty(id + ".nextRunMillis", String.valueOf(sb.nextRunMillis));
props.setProperty(id + ".sourceName", sb.sourceName);
String coloredMessage = (messageColorCode.isEmpty() ? "" : messageColorCode) + message; props.setProperty(id + ".message", sb.message);
props.setProperty(id + ".type", sb.type);
String out = format.replace("%name%", sourceName) props.setProperty(id + ".prefix", sb.prefix);
.replace("%prefix%", finalPrefix) // Neu verwendete Variable props.setProperty(id + ".prefixColor", sb.prefixColor);
.replace("%prefixColored%", finalPrefix) // Fallback props.setProperty(id + ".bracketColor", sb.bracketColor);
.replace("%message%", message) props.setProperty(id + ".messageColor", sb.messageColor);
.replace("%messageColored%", coloredMessage) props.setProperty(id + ".recur", sb.recur);
.replace("%type%", type); }
try (OutputStream out = new FileOutputStream(schedulesFile)) {
if (!out.contains("%prefixColored%") && !out.contains("%messageColored%") && !out.contains("%prefix%") && !out.contains("%message%")) { props.store(out, "PulseCast Scheduled Broadcasts");
out = finalPrefix + " " + coloredMessage; } catch (IOException e) {
} plugin.getLogger().severe("[BroadcastModule] Konnte Schedules nicht speichern: " + e.getMessage());
}
TextComponent tc = new TextComponent(out); }
int sent = 0;
for (ProxiedPlayer p : plugin.getProxy().getPlayers()) { /**
try { p.sendMessage(tc); sent++; } catch (Throwable ignored) {} * FIX #2: Robusteres Parsen der Property-Keys.
} * Statt split("\\.") mit length==2-Check nutzen wir lastIndexOf('.') um den letzten
* Punkt als Trenner zu verwenden. Damit funktionieren auch IDs die Punkte enthalten.
plugin.getLogger().info("[BroadcastModule] Broadcast gesendet (Empfänger=" + sent + "): " + message); *
return true; * Bekannte Felder: nextRunMillis, sourceName, message, type, prefix, prefixColor,
} * bracketColor, messageColor, recur → alle ohne Punkte im Namen.
*/
private String normalizeColorCode(String code) { private void loadSchedules() {
if (code == null) return ""; if (!schedulesFile.exists()) return;
code = code.trim(); Properties props = new Properties();
if (code.isEmpty()) return ""; try (InputStream in = new FileInputStream(schedulesFile)) {
return code.contains("&") ? ChatColor.translateAlternateColorCodes('&', code) : code; props.load(in);
} } catch (IOException e) {
plugin.getLogger().severe("[BroadcastModule] Konnte Schedules nicht laden: " + e.getMessage());
private void saveSchedules() { return;
Properties props = new Properties(); }
for (Map.Entry<String, ScheduledBroadcast> entry : scheduledByClientId.entrySet()) {
String id = entry.getKey(); // Bekannte Feld-Suffixe
ScheduledBroadcast sb = entry.getValue(); Set<String> knownFields = new HashSet<>(Arrays.asList(
props.setProperty(id + ".nextRunMillis", String.valueOf(sb.nextRunMillis)); "nextRunMillis", "sourceName", "message", "type",
props.setProperty(id + ".sourceName", sb.sourceName); "prefix", "prefixColor", "bracketColor", "messageColor", "recur"
props.setProperty(id + ".message", sb.message); ));
props.setProperty(id + ".type", sb.type);
props.setProperty(id + ".prefix", sb.prefix); Map<String, ScheduledBroadcast> loaded = new LinkedHashMap<>();
props.setProperty(id + ".prefixColor", sb.prefixColor); for (String key : props.stringPropertyNames()) {
props.setProperty(id + ".bracketColor", sb.bracketColor); // Neu // Finde das letzte '.' das einen bekannten Feldnamen abtrennt
props.setProperty(id + ".messageColor", sb.messageColor); int lastDot = key.lastIndexOf('.');
props.setProperty(id + ".recur", sb.recur); if (lastDot < 0) continue;
} String field = key.substring(lastDot + 1);
if (!knownFields.contains(field)) continue;
try (OutputStream out = new FileOutputStream(schedulesFile)) { String id = key.substring(0, lastDot);
props.store(out, "PulseCast Scheduled Broadcasts"); if (id.isEmpty()) continue;
} catch (IOException e) { String value = props.getProperty(key);
plugin.getLogger().severe("[BroadcastModule] Konnte Schedules nicht speichern: " + e.getMessage());
} ScheduledBroadcast sb = loaded.computeIfAbsent(id,
} k -> new ScheduledBroadcast(k, 0, "", "", "", "", "", "", "", ""));
private void loadSchedules() { switch (field) {
if (!schedulesFile.exists()) { case "nextRunMillis": try { sb.nextRunMillis = Long.parseLong(value); } catch (NumberFormatException ignored) {} break;
plugin.getLogger().info("[BroadcastModule] Keine bestehenden Schedules gefunden (Neustart)."); case "sourceName": sb.sourceName = value; break;
return; case "message": sb.message = value; break;
} case "type": sb.type = value; break;
case "prefix": sb.prefix = value; break;
Properties props = new Properties(); case "prefixColor": sb.prefixColor = value; break;
try (InputStream in = new FileInputStream(schedulesFile)) { case "bracketColor": sb.bracketColor = value; break;
props.load(in); case "messageColor": sb.messageColor = value; break;
} catch (IOException e) { case "recur": sb.recur = value; break;
plugin.getLogger().severe("[BroadcastModule] Konnte Schedules nicht laden: " + e.getMessage()); }
return; }
} scheduledByClientId.putAll(loaded);
plugin.getLogger().info("[BroadcastModule] " + loaded.size() + " geplante Broadcasts aus Datei wiederhergestellt.");
Map<String, ScheduledBroadcast> loaded = new HashMap<>(); }
for (String key : props.stringPropertyNames()) {
if (!key.contains(".")) continue; public boolean scheduleBroadcast(long timestampMillis, String sourceName, String message, String type,
String[] parts = key.split("\\."); String apiKeyHeader, String prefix, String prefixColor, String bracketColor,
if (parts.length != 2) continue; String messageColor, String recur, String clientScheduleId) {
loadConfig();
String id = parts[0]; if (!enabled) return false;
String field = parts[1]; if (requiredApiKey != null && !requiredApiKey.isEmpty()) {
String value = props.getProperty(key); if (apiKeyHeader == null || !requiredApiKey.equals(apiKeyHeader)) {
plugin.getLogger().warning("[BroadcastModule] schedule abgelehnt: API-Key fehlt oder inkorrekt.");
ScheduledBroadcast sb = loaded.get(id); return false;
if (sb == null) { }
sb = new ScheduledBroadcast(id, 0, "", "", "", "", "", "", "", ""); // Ein leerer String mehr für Bracket }
loaded.put(id, sb); if (message == null) message = "";
} if (sourceName == null || sourceName.isEmpty()) sourceName = "System";
if (type == null) type = "global";
switch (field) { if (recur == null) recur = "none";
case "nextRunMillis":
try { sb.nextRunMillis = Long.parseLong(value); } catch (NumberFormatException ignored) {} String id = (clientScheduleId != null && !clientScheduleId.trim().isEmpty())
break; ? clientScheduleId.trim() : UUID.randomUUID().toString();
case "sourceName": sb.sourceName = value; break;
case "message": sb.message = value; break; long now = System.currentTimeMillis();
case "type": sb.type = value; break; if (timestampMillis <= now) {
case "prefix": sb.prefix = value; break; plugin.getLogger().warning("[BroadcastModule] Geplante Zeit in der Vergangenheit → sende sofort!");
case "prefixColor": sb.prefixColor = value; break; return handleBroadcast(sourceName, message, type, apiKeyHeader, prefix, prefixColor, bracketColor, messageColor);
case "bracketColor": sb.bracketColor = value; break; // Neu }
case "messageColor": sb.messageColor = value; break;
case "recur": sb.recur = value; break; ScheduledBroadcast sb = new ScheduledBroadcast(id, timestampMillis, sourceName, message, type,
} prefix, prefixColor, bracketColor, messageColor, recur);
} scheduledByClientId.put(id, sb);
scheduledByClientId.putAll(loaded); saveSchedules();
plugin.getLogger().info("[BroadcastModule] " + loaded.size() + " geplante Broadcasts aus Datei wiederhergestellt."); plugin.getLogger().info("[BroadcastModule] Neue geplante Nachricht registriert: " + id
} + " @ " + dateFormat.format(new Date(timestampMillis)));
return true;
public boolean scheduleBroadcast(long timestampMillis, String sourceName, String message, String type, }
String apiKeyHeader, String prefix, String prefixColor, String bracketColor, String messageColor,
String recur, String clientScheduleId) { public boolean cancelScheduled(String clientScheduleId) {
loadConfig(); if (clientScheduleId == null || clientScheduleId.trim().isEmpty()) return false;
ScheduledBroadcast removed = scheduledByClientId.remove(clientScheduleId);
if (!enabled) { if (removed != null) { plugin.getLogger().info("[BroadcastModule] Schedule abgebrochen: " + clientScheduleId); saveSchedules(); return true; }
plugin.getLogger().info("[BroadcastModule] schedule abgelehnt: Modul deaktiviert."); return false;
return false; }
}
private void processScheduled() {
if (requiredApiKey != null && !requiredApiKey.isEmpty()) { if (scheduledByClientId.isEmpty()) return;
if (apiKeyHeader == null || !requiredApiKey.equals(apiKeyHeader)) { long now = System.currentTimeMillis();
plugin.getLogger().warning("[BroadcastModule] schedule abgelehnt: API-Key fehlt oder inkorrekt."); List<String> toRemove = new ArrayList<>();
return false; boolean changed = false;
}
} for (Map.Entry<String, ScheduledBroadcast> entry : scheduledByClientId.entrySet()) {
ScheduledBroadcast sb = entry.getValue();
if (message == null) message = ""; if (sb.nextRunMillis <= now) {
if (sourceName == null || sourceName.isEmpty()) sourceName = "System"; plugin.getLogger().info("[BroadcastModule] ⏰ Sende geplante Nachricht (ID: " + entry.getKey() + ")");
if (type == null) type = "global"; handleBroadcast(sb.sourceName, sb.message, sb.type, "", sb.prefix, sb.prefixColor, sb.bracketColor, sb.messageColor);
if (recur == null) recur = "none"; if (!"none".equalsIgnoreCase(sb.recur)) {
long next = computeNextMillis(sb.nextRunMillis, sb.recur);
String id = (clientScheduleId != null && !clientScheduleId.trim().isEmpty()) ? clientScheduleId.trim() : UUID.randomUUID().toString(); if (next > 0) { sb.nextRunMillis = next; changed = true; }
else { toRemove.add(entry.getKey()); changed = true; }
long now = System.currentTimeMillis(); } else { toRemove.add(entry.getKey()); changed = true; }
String scheduledTimeStr = dateFormat.format(new Date(timestampMillis)); }
}
plugin.getLogger().info("[BroadcastModule] Neue geplante Nachricht registriert: " + id + " @ " + scheduledTimeStr); if (changed || !toRemove.isEmpty()) {
for (String k : toRemove) { scheduledByClientId.remove(k); }
if (timestampMillis <= now) { saveSchedules();
plugin.getLogger().warning("[BroadcastModule] Geplante Zeit liegt in der Vergangenheit -> sende sofort!"); }
return handleBroadcast(sourceName, message, type, apiKeyHeader, prefix, prefixColor, bracketColor, messageColor); }
}
private long computeNextMillis(long currentMillis, String recur) {
ScheduledBroadcast sb = new ScheduledBroadcast(id, timestampMillis, sourceName, message, type, prefix, prefixColor, bracketColor, messageColor, recur); switch (recur.toLowerCase(Locale.ROOT)) {
scheduledByClientId.put(id, sb); case "hourly": return currentMillis + TimeUnit.HOURS.toMillis(1);
saveSchedules(); case "daily": return currentMillis + TimeUnit.DAYS.toMillis(1);
return true; case "weekly": return currentMillis + TimeUnit.DAYS.toMillis(7);
} default: return -1L;
}
public boolean cancelScheduled(String clientScheduleId) { }
if (clientScheduleId == null || clientScheduleId.trim().isEmpty()) return false;
ScheduledBroadcast removed = scheduledByClientId.remove(clientScheduleId); private static class ScheduledBroadcast {
if (removed != null) { final String clientId;
plugin.getLogger().info("[BroadcastModule] Geplante Nachricht abgebrochen: id=" + clientScheduleId); long nextRunMillis;
saveSchedules(); String sourceName, message, type, prefix, prefixColor, bracketColor, messageColor, recur;
return true;
} ScheduledBroadcast(String clientId, long nextRunMillis, String sourceName, String message, String type,
return false; String prefix, String prefixColor, String bracketColor, String messageColor, String recur) {
} this.clientId = clientId;
this.nextRunMillis = nextRunMillis;
private void processScheduled() { this.sourceName = sourceName;
if (scheduledByClientId.isEmpty()) return; this.message = message;
this.type = type;
long now = System.currentTimeMillis(); this.prefix = prefix;
List<String> toRemove = new ArrayList<>(); this.prefixColor = prefixColor;
boolean changed = false; this.bracketColor = bracketColor;
this.messageColor = messageColor;
for (Map.Entry<String, ScheduledBroadcast> entry : scheduledByClientId.entrySet()) { this.recur = recur == null ? "none" : recur;
ScheduledBroadcast sb = entry.getValue(); }
}
if (sb.nextRunMillis <= now) { }
String timeStr = dateFormat.format(new Date(sb.nextRunMillis));
plugin.getLogger().info("[BroadcastModule] ⏰ Sende geplante Nachricht (ID: " + entry.getKey() + ", Zeit: " + timeStr + ")");
handleBroadcast(sb.sourceName, sb.message, sb.type, "", sb.prefix, sb.prefixColor, sb.bracketColor, sb.messageColor);
if (!"none".equalsIgnoreCase(sb.recur)) {
long next = computeNextMillis(sb.nextRunMillis, sb.recur);
if (next > 0) {
sb.nextRunMillis = next;
String nextTimeStr = dateFormat.format(new Date(next));
plugin.getLogger().info("[BroadcastModule] Nächste Wiederholung (" + sb.recur + "): " + nextTimeStr);
changed = true;
} else {
toRemove.add(entry.getKey());
changed = true;
}
} else {
toRemove.add(entry.getKey());
changed = true;
}
}
}
if (changed || !toRemove.isEmpty()) {
for (String k : toRemove) {
scheduledByClientId.remove(k);
plugin.getLogger().info("[BroadcastModule] Schedule entfernt: " + k);
}
saveSchedules();
}
}
private long computeNextMillis(long currentMillis, String recur) {
switch (recur.toLowerCase(Locale.ROOT)) {
case "hourly": return currentMillis + TimeUnit.HOURS.toMillis(1);
case "daily": return currentMillis + TimeUnit.DAYS.toMillis(1);
case "weekly": return currentMillis + TimeUnit.DAYS.toMillis(7);
default: return -1L;
}
}
private static class ScheduledBroadcast {
final String clientId;
long nextRunMillis;
String sourceName;
String message;
String type;
String prefix;
String prefixColor;
String bracketColor; // Neu
String messageColor;
String recur;
ScheduledBroadcast(String clientId, long nextRunMillis, String sourceName, String message, String type,
String prefix, String prefixColor, String bracketColor, String messageColor, String recur) {
this.clientId = clientId;
this.nextRunMillis = nextRunMillis;
this.sourceName = sourceName;
this.message = message;
this.type = type;
this.prefix = prefix;
this.prefixColor = prefixColor;
this.bracketColor = bracketColor; // Neu
this.messageColor = messageColor;
this.recur = (recur == null ? "none" : recur);
}
}
}

View File

@@ -304,7 +304,6 @@ public class AccountLinkManager {
} catch (IOException e) { } catch (IOException e) {
logger.warning("[ChatModule] Fehler beim Laden der Account-Links: " + e.getMessage()); logger.warning("[ChatModule] Fehler beim Laden der Account-Links: " + e.getMessage());
} }
logger.info("[ChatModule] " + links.size() + " Account-Verknüpfungen geladen.");
} }
private static String esc(String s) { private static String esc(String s) {

File diff suppressed because it is too large Load Diff

View File

@@ -134,6 +134,13 @@ public class ChatFilter {
} }
} }
// ── 6. Anti-Werbung ──
if (cfg.antiAdEnabled && !isAdmin) {
if (containsAdvertisement(result)) {
return new FilterResponse(FilterResult.BLOCKED, result, cfg.antiAdMessage);
}
}
return new FilterResponse( return new FilterResponse(
modified ? FilterResult.MODIFIED : FilterResult.ALLOWED, modified ? FilterResult.MODIFIED : FilterResult.ALLOWED,
result, result,
@@ -187,14 +194,88 @@ public class ChatFilter {
// Kein Recht → & und nächstes Zeichen überspringen // Kein Recht → & und nächstes Zeichen überspringen
if (isColor || isFormat) { i++; continue; } if (isColor || isFormat) { i++; continue; }
// Hex: &# + 6 Zeichen überspringen // Hex: &# + 6 Zeichen überspringen (i zeigt auf &, +1 = #, +2..+7 = RRGGBB)
if (isHex && i + 7 < message.length()) { i += 7; continue; } if (isHex && i + 7 <= message.length()) { i += 7; continue; }
} }
sb.append(c); sb.append(c);
} }
return sb.toString(); return sb.toString();
} }
// ===== Anti-Werbung =====
// Vorkompilierte Patterns (einmalig beim Classload)
private static final Pattern PATTERN_IP =
Pattern.compile("\\b(\\d{1,3}[.,]){3}\\d{1,3}(:\\d{1,5})?\\b");
private static final Pattern PATTERN_DOMAIN_GENERIC =
Pattern.compile("(?i)\\b[a-z0-9-]{2,63}\\.[a-z]{2,10}(?:[/:\\d]\\S*)?\\b");
private static final Pattern PATTERN_URL_PREFIX =
Pattern.compile("(?i)(https?://|www\\.)\\S+");
/**
* Prüft ob die Nachricht Werbung enthält (IP, URL, fremde Domain).
* Domains auf der Whitelist werden ignoriert.
*
* Erkennt:
* - http:// / https:// / www. Prefixe
* - IPv4-Adressen (auch mit Port)
* - Domain-Namen mit konfigurierten TLDs (z.B. .net, .de, .com)
* - Verschleierungsversuche mit Leerzeichen um Punkte ("play . server . net")
*/
private boolean containsAdvertisement(String message) {
// Normalisierung: "play . server . net" → "play.server.net"
String normalized = message.replaceAll("\\s*\\.\\s*", ".");
// 1. Explizite URL-Prefixe
if (PATTERN_URL_PREFIX.matcher(normalized).find()) {
return !allMatchesWhitelisted(normalized, PATTERN_URL_PREFIX);
}
// 2. IP-Adressen (werden nie whitelisted)
if (PATTERN_IP.matcher(normalized).find()) {
return true;
}
// 3. Domains mit bekannten TLDs
if (!cfg.antiAdBlockedTlds.isEmpty()) {
java.util.regex.Matcher m = PATTERN_DOMAIN_GENERIC.matcher(normalized);
while (m.find()) {
String match = m.group();
String tld = extractTld(match);
if (cfg.antiAdBlockedTlds.contains(tld.toLowerCase())) {
if (!isOnWhitelist(match)) return true;
}
}
}
return false;
}
/** true wenn ALLE Treffer des Patterns auf der Whitelist stehen. */
private boolean allMatchesWhitelisted(String message, Pattern pattern) {
java.util.regex.Matcher m = pattern.matcher(message);
while (m.find()) {
if (!isOnWhitelist(m.group())) return false;
}
return true;
}
private boolean isOnWhitelist(String match) {
String lower = match.toLowerCase();
for (String entry : cfg.antiAdWhitelist) {
if (lower.contains(entry.toLowerCase())) return true;
}
return false;
}
private static String extractTld(String domain) {
String clean = domain.split("[/:]")[0];
int dot = clean.lastIndexOf('.');
return dot >= 0 ? clean.substring(dot + 1) : "";
}
private static String buildStars(int length) { private static String buildStars(int length) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
for (int i = 0; i < Math.max(length, 4); i++) sb.append('*'); for (int i = 0; i < Math.max(length, 4); i++) sb.append('*');
@@ -235,5 +316,16 @@ public class ChatFilter {
public boolean capsFilterEnabled = true; public boolean capsFilterEnabled = true;
public int capsMinLength = 6; // Mindestlänge für Caps-Check public int capsMinLength = 6; // Mindestlänge für Caps-Check
public int capsMaxPercent = 70; // Max. % Großbuchstaben public int capsMaxPercent = 70; // Max. % Großbuchstaben
// Anti-Werbung
public boolean antiAdEnabled = true;
public String antiAdMessage = "&cWerbung ist in diesem Chat nicht erlaubt!";
// Domains/Substrings die NICHT geblockt werden (z.B. eigene Serveradresse)
public List<String> antiAdWhitelist = new ArrayList<>();
// TLDs die als Werbung gewertet werden (leer = alle TLDs prüfen)
public List<String> antiAdBlockedTlds = new ArrayList<>(Arrays.asList(
"net", "com", "de", "org", "gg", "io", "eu", "tv", "xyz",
"info", "me", "cc", "co", "app", "online", "site", "fun"
));
} }
} }

View File

@@ -43,6 +43,8 @@ import java.util.logging.Logger;
* ✅ Report-System (/report, /reports, /reportclose) * ✅ Report-System (/report, /reports, /reportclose)
* ✅ Admin-Benachrichtigung bei Reports (sofort oder verzögert nach Login) * ✅ Admin-Benachrichtigung bei Reports (sofort oder verzögert nach Login)
* ✅ Server-Farben pro Server (&-Codes und &#RRGGBB HEX) * ✅ 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 { public class ChatModule implements Module, Listener {
@@ -74,10 +76,13 @@ public class ChatModule implements Module, Listener {
private final Map<UUID, Long> helpopCooldowns = new ConcurrentHashMap<>(); private final Map<UUID, Long> helpopCooldowns = new ConcurrentHashMap<>();
// Report-Cooldown: UUID → letzter Report-Zeitstempel (Sekunden) // Report-Cooldown: UUID → letzter Report-Zeitstempel (Sekunden)
private final Map<UUID, Long> reportCooldowns = new ConcurrentHashMap<>(); // NEU private final Map<UUID, Long> reportCooldowns = new ConcurrentHashMap<>();
// Letzte Chatnachricht pro Spieler (für Report-Kontext): name.toLowerCase() → message // Letzte Chatnachricht pro Spieler (für Report-Kontext): name.toLowerCase() → message
private final Map<String, String> lastChatMessages = new ConcurrentHashMap<>(); // NEU private final Map<String, String> lastChatMessages = new ConcurrentHashMap<>();
// UUIDs die gerade auf Plugin-Chat-Eingabe warten
private final Set<UUID> awaitingInput = Collections.newSetFromMap(new ConcurrentHashMap<>());
// Geyser-Präfix für Bedrock-Spieler (Standard: ".") // Geyser-Präfix für Bedrock-Spieler (Standard: ".")
private static final String GEYSER_PREFIX = "."; private static final String GEYSER_PREFIX = ".";
@@ -105,13 +110,13 @@ public class ChatModule implements Module, Listener {
emojiParser = new EmojiParser(config.getEmojiMappings(), config.isEmojiEnabled()); emojiParser = new EmojiParser(config.getEmojiMappings(), config.isEmojiEnabled());
chatFilter = new ChatFilter(config.getFilterConfig()); chatFilter = new ChatFilter(config.getFilterConfig());
// NEU: ChatLogger // ChatLogger
if (config.isChatlogEnabled()) { if (config.isChatlogEnabled()) {
chatLogger = new ChatLogger(plugin.getDataFolder(), logger, config.getChatlogRetentionDays()); chatLogger = new ChatLogger(plugin.getDataFolder(), logger, config.getChatlogRetentionDays());
logger.info("[ChatModule] Chat-Log aktiviert (" + config.getChatlogRetentionDays() + " Tage Aufbewahrung)."); logger.info("[ChatModule] Chat-Log aktiviert (" + config.getChatlogRetentionDays() + " Tage Aufbewahrung).");
} }
// NEU: ReportManager // ReportManager
if (config.isReportsEnabled()) { if (config.isReportsEnabled()) {
reportManager = new ReportManager(plugin.getDataFolder(), logger); reportManager = new ReportManager(plugin.getDataFolder(), logger);
reportManager.load(); reportManager.load();
@@ -155,7 +160,6 @@ public class ChatModule implements Module, Listener {
helpopCooldowns.clear(); helpopCooldowns.clear();
reportCooldowns.clear(); reportCooldowns.clear();
lastChatMessages.clear(); lastChatMessages.clear();
logger.info("[ChatModule] Deaktiviert.");
} }
// ========================================================= // =========================================================
@@ -182,11 +186,6 @@ public class ChatModule implements Module, Listener {
if (!(e.getSender() instanceof ProxiedPlayer)) return; if (!(e.getSender() instanceof ProxiedPlayer)) return;
ProxiedPlayer player = (ProxiedPlayer) e.getSender(); ProxiedPlayer player = (ProxiedPlayer) e.getSender();
// Bypass: Spieler wartet auf Plugin-Eingabe (CMI etc.)
// Event komplett unberührt lassen → Originalnachricht mit Signatur
// geht direkt zum Sub-Server. Funktioniert auf Spigot ohne Einschränkung.
// Auf Paper-Sub-Servern muss reject-chat-unsigned: false gesetzt sein —
// das ist eine Paper-Limitierung, nicht lösbar auf BungeeCord-Ebene.
if (awaitingInput.contains(player.getUniqueId())) { if (awaitingInput.contains(player.getUniqueId())) {
awaitingInput.remove(player.getUniqueId()); awaitingInput.remove(player.getUniqueId());
return; // Event NICHT cancellen → Nachricht geht mit Originalsignatur durch return; // Event NICHT cancellen → Nachricht geht mit Originalsignatur durch
@@ -198,7 +197,6 @@ public class ChatModule implements Module, Listener {
/** /**
* Zentrale Chat-Verarbeitungslogik. * Zentrale Chat-Verarbeitungslogik.
* Wird von beiden Event-Handlern aufgerufen.
*/ */
private void processChat(ProxiedPlayer player, String rawMessage) { private void processChat(ProxiedPlayer player, String rawMessage) {
if (rawMessage == null || rawMessage.trim().isEmpty()) return; if (rawMessage == null || rawMessage.trim().isEmpty()) return;
@@ -236,7 +234,7 @@ public class ChatModule implements Module, Listener {
player.sendMessage(color(filterResp.denyReason)); player.sendMessage(color(filterResp.denyReason));
return; return;
} }
message = filterResp.message; // ggf. modifiziert (Caps, Blacklist) message = filterResp.message;
String serverName = player.getServer() != null String serverName = player.getServer() != null
? player.getServer().getInfo().getName() ? player.getServer().getInfo().getName()
@@ -259,7 +257,6 @@ public class ChatModule implements Module, Listener {
ProxiedPlayer mentioned = ProxyServer.getInstance().getPlayer(targetName); ProxiedPlayer mentioned = ProxyServer.getInstance().getPlayer(targetName);
if (mentioned != null && !mentioned.getUniqueId().equals(uuid)) { if (mentioned != null && !mentioned.getUniqueId().equals(uuid)) {
mentionedPlayers.add(mentioned.getUniqueId()); mentionedPlayers.add(mentioned.getUniqueId());
// Wort hervorheben
word = translateColors(highlightColor + word + "&r"); word = translateColors(highlightColor + word + "&r");
} }
} }
@@ -316,10 +313,8 @@ public class ChatModule implements Module, Listener {
&& !mentionsDisabled.contains(recipient.getUniqueId()); && !mentionsDisabled.contains(recipient.getUniqueId());
if (isMentioned) { if (isMentioned) {
// Prefix-Nachricht über der Chat-Zeile
recipient.sendMessage(color(config.getMentionsNotifyPrefix() recipient.sendMessage(color(config.getMentionsNotifyPrefix()
+ "&7" + finalSenderName + " &7hat dich erwähnt!")); + "&7" + finalSenderName + " &7hat dich erwähnt!"));
// Sound via Plugin-Messaging an Sub-Server senden
sendMentionSound(recipient, config.getMentionsSound()); sendMentionSound(recipient, config.getMentionsSound());
} }
@@ -335,44 +330,147 @@ public class ChatModule implements Module, Listener {
}); });
} }
@EventHandler
public void onDisconnect(PlayerDisconnectEvent e) {
UUID uuid = e.getPlayer().getUniqueId();
chatFilter.cleanup(uuid);
playerChannels.remove(uuid);
mentionsDisabled.remove(uuid);
awaitingInput.remove(uuid);
}
// ========================================================= // =========================================================
// LOGIN-EVENT: Kanal setzen + Report-Benachrichtigung // LOGIN-EVENT: Kanal setzen + Join-Nachricht + Report-Info
// ========================================================= // =========================================================
@EventHandler @EventHandler
public void onLogin(PostLoginEvent e) { public void onLogin(PostLoginEvent e) {
ProxiedPlayer player = e.getPlayer(); ProxiedPlayer player = e.getPlayer();
playerChannels.put(player.getUniqueId(), config.getDefaultChannelId()); UUID uuid = player.getUniqueId();
// NEU: Offene Reports nach 2 Sekunden anzeigen (damit Update-Meldungen nicht überlagert werden) // Standard-Kanal setzen
if (reportManager == null) return; playerChannels.put(uuid, config.getDefaultChannelId());
if (!player.hasPermission(config.getAdminNotifyPermission())
&& !player.hasPermission(config.getAdminBypassPermission())) return;
int openCount = reportManager.getOpenCount();
if (openCount == 0) return;
// 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, () -> { plugin.getProxy().getScheduler().schedule(plugin, () -> {
if (!player.isConnected()) return; if (!player.isConnected()) return;
int count = reportManager.getOpenCount();
if (count == 0) return;
player.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); // ── Join-Nachricht ──
player.sendMessage(color("&c&l⚑ OFFENE REPORTS &8| &f" + count + " ausstehend")); if (config.isJoinLeaveEnabled()) {
player.sendMessage(color("&7Tippe &f/reports &7für eine Übersicht.")); broadcastJoinLeave(player, true);
player.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); }
// ── Offene Reports für Admins anzeigen ──
if (reportManager != null
&& (player.hasPermission(config.getAdminNotifyPermission())
|| player.hasPermission(config.getAdminBypassPermission()))) {
int count = reportManager.getOpenCount();
if (count > 0) {
player.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
player.sendMessage(color("&c&l⚑ OFFENE REPORTS &8| &f" + count + " ausstehend"));
player.sendMessage(color("&7Tippe &f/reports &7für eine Übersicht."));
player.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
}
}
}, 2, TimeUnit.SECONDS); }, 2, TimeUnit.SECONDS);
} }
// =========================================================
// DISCONNECT-EVENT: Cleanup + Leave-Nachricht
// =========================================================
@EventHandler
public void onDisconnect(PlayerDisconnectEvent e) {
ProxiedPlayer player = e.getPlayer();
UUID uuid = player.getUniqueId();
// Leave-Nachricht
if (config.isJoinLeaveEnabled()) {
broadcastJoinLeave(player, false);
}
// Cleanup
chatFilter.cleanup(uuid);
playerChannels.remove(uuid);
mentionsDisabled.remove(uuid);
awaitingInput.remove(uuid);
VanishProvider.cleanup(uuid); // Vanish-Status bereinigen
}
// =========================================================
// JOIN / LEAVE NACHRICHTEN (mit Vanish-Support)
// =========================================================
/**
* Sendet eine Join- oder Leave-Nachricht an alle Spieler.
*
* Vanish-Logik:
* - Unsichtbare Spieler: kein Broadcast an normale Spieler.
* - Ist vanish-show-to-admins=true, erhalten Admins (bypass-permission)
* eine dezente Vanish-Benachrichtigung.
*
* @param player Der betroffene Spieler
* @param isJoin true = Join, false = Leave
*/
private void broadcastJoinLeave(ProxiedPlayer player, boolean isJoin) {
boolean isVanished = VanishProvider.isVanished(player);
String prefix = getLuckPermsPrefix(player);
String suffix = getLuckPermsSuffix(player);
String server = (player.getServer() != null)
? config.getServerDisplay(player.getServer().getInfo().getName())
: "Netzwerk";
String normalFormat = isJoin ? config.getJoinFormat() : config.getLeaveFormat();
String vanishFormat = isJoin ? config.getVanishJoinFormat() : config.getVanishLeaveFormat();
// Platzhalter ersetzen
String normalMsg = normalFormat
.replace("{player}", player.getName())
.replace("{prefix}", prefix != null ? prefix : "")
.replace("{suffix}", suffix != null ? suffix : "")
.replace("{server}", server);
String vanishMsg = vanishFormat
.replace("{player}", player.getName())
.replace("{prefix}", prefix != null ? prefix : "")
.replace("{suffix}", suffix != null ? suffix : "")
.replace("{server}", server);
for (ProxiedPlayer recipient : ProxyServer.getInstance().getPlayers()) {
// Spieler sieht seine eigene Join-/Leave-Meldung nie
if (recipient.getUniqueId().equals(player.getUniqueId())) continue;
// Admin = bypass-permission ODER notify-permission
boolean recipientIsAdmin = recipient.hasPermission(config.getAdminBypassPermission())
|| recipient.hasPermission(config.getAdminNotifyPermission());
if (isVanished) {
// Vanished: Nur Admins sehen eine dezente Meldung (wenn konfiguriert)
if (recipientIsAdmin && config.isVanishShowToAdmins()) {
recipient.sendMessage(color(vanishMsg));
}
} else {
// Normaler Spieler: alle erhalten die Nachricht
// Vanished Admins sehen Join/Leave-Events anderer Spieler normal
recipient.sendMessage(color(normalMsg));
}
}
// Konsole immer informieren
String logMsg = isVanished
? "[ChatModule] " + (isJoin ? "JOIN(V)" : "LEAVE(V)") + " " + player.getName()
: "[ChatModule] " + (isJoin ? "JOIN" : "LEAVE") + " " + player.getName();
ProxyServer.getInstance().getConsole().sendMessage(color(
isVanished ? "&8" + logMsg : "&7" + logMsg));
// Brücken (nur für nicht-vanished Spieler)
if (!isVanished) {
String cleanMsg = ChatColor.stripColor(ChatColor.translateAlternateColorCodes('&', normalMsg));
if (discordBridge != null && !config.getJoinLeaveDiscordWebhook().isEmpty()) {
discordBridge.sendToDiscord(config.getJoinLeaveDiscordWebhook(),
player.getName(), cleanMsg, null);
}
if (telegramBridge != null && !config.getJoinLeaveTelegramChatId().isEmpty()) {
telegramBridge.sendToTelegram(config.getJoinLeaveTelegramChatId(),
config.getJoinLeaveTelegramThreadId(), cleanMsg);
}
}
}
// ========================================================= // =========================================================
// BEFEHLE REGISTRIEREN // BEFEHLE REGISTRIEREN
// ========================================================= // =========================================================
@@ -452,17 +550,24 @@ public class ChatModule implements Module, Listener {
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, helpop); ProxyServer.getInstance().getPluginManager().registerCommand(plugin, helpop);
// /msg <spieler> <nachricht> // /msg <spieler> <nachricht>
// Vanish: Vanished Spieler sind für normale Spieler nicht erreichbar.
// Admins können vanished Spieler per PM kontaktieren.
Command msgCmd = new Command("msg", null, "tell", "w", "whisper") { Command msgCmd = new Command("msg", null, "tell", "w", "whisper") {
@Override @Override
public void execute(CommandSender sender, String[] args) { public void execute(CommandSender sender, String[] args) {
if (!config.isPmEnabled()) { sender.sendMessage(color("&cPrivat-Nachrichten sind deaktiviert.")); return; } if (!config.isPmEnabled()) { sender.sendMessage(color("&cPrivat-Nachrichten sind deaktiviert.")); return; }
if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(color("&cNur Spieler!")); return; } if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(color("&cNur Spieler!")); return; }
if (args.length < 2) { sender.sendMessage(color("&cBenutzung: /msg <Spieler> <Nachricht>")); return; } if (args.length < 2) { sender.sendMessage(color("&cBenutzung: /msg <Spieler> <Nachricht>")); return; }
ProxiedPlayer from = (ProxiedPlayer) sender; ProxiedPlayer from = (ProxiedPlayer) sender;
ProxiedPlayer to = ProxyServer.getInstance().getPlayer(args[0]); boolean fromIsAdmin = from.hasPermission(config.getAdminBypassPermission());
// Ziel suchen vanished Spieler sind für Nicht-Admins "nicht gefunden"
ProxiedPlayer to = findVisiblePlayer(args[0], fromIsAdmin);
if (to == null || !to.isConnected()) { if (to == null || !to.isConnected()) {
from.sendMessage(color("&cSpieler &f" + args[0] + " &cnicht gefunden.")); return; from.sendMessage(color("&cSpieler &f" + args[0] + " &cnicht gefunden.")); return;
} }
String message = String.join(" ", Arrays.copyOfRange(args, 1, args.length)); String message = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
pmManager.send(from, to, message, config, config.getAdminBypassPermission()); pmManager.send(from, to, message, config, config.getAdminBypassPermission());
} }
@@ -652,27 +757,24 @@ public class ChatModule implements Module, Listener {
UUID tUUID = target.getUniqueId(); UUID tUUID = target.getUniqueId();
String tServer = target.getServer() != null ? target.getServer().getInfo().getName() : "Proxy"; String tServer = target.getServer() != null ? target.getServer().getInfo().getName() : "Proxy";
// Kanal
String channelId = playerChannels.getOrDefault(tUUID, config.getDefaultChannelId()); String channelId = playerChannels.getOrDefault(tUUID, config.getDefaultChannelId());
ChatChannel ch = config.getChannel(channelId); ChatChannel ch = config.getChannel(channelId);
String channelName = ch != null ? ch.getFormattedTag() + " &f" + ch.getName() : "&f" + channelId; String channelName = ch != null ? ch.getFormattedTag() + " &f" + ch.getName() : "&f" + channelId;
// Mute-Status
String muteStatus = muteManager.isMuted(tUUID) String muteStatus = muteManager.isMuted(tUUID)
? "&cJa &8(noch: &f" + muteManager.getRemainingTime(tUUID) + "&8)" ? "&cJa &8(noch: &f" + muteManager.getRemainingTime(tUUID) + "&8)"
: "&aKein"; : "&aKein";
// Blockierungen
Set<UUID> blocked = blockManager.getBlockedBy(tUUID); Set<UUID> blocked = blockManager.getBlockedBy(tUUID);
// Account-Links
AccountLinkManager.LinkedAccount link = linkManager.getByUUID(tUUID); AccountLinkManager.LinkedAccount link = linkManager.getByUUID(tUUID);
String discordInfo = (link != null && !link.discordUserId.isEmpty()) String discordInfo = (link != null && !link.discordUserId.isEmpty())
? "&a" + link.discordUsername + " &8(" + link.discordUserId + ")" : "&7Nicht verknüpft"; ? "&a" + link.discordUsername + " &8(" + link.discordUserId + ")" : "&7Nicht verknüpft";
String telegramInfo = (link != null && !link.telegramUserId.isEmpty()) String telegramInfo = (link != null && !link.telegramUserId.isEmpty())
? "&a" + link.telegramUsername + " &8(" + link.telegramUserId + ")" : "&7Nicht verknüpft"; ? "&a" + link.telegramUsername + " &8(" + link.telegramUserId + ")" : "&7Nicht verknüpft";
// Ausgabe String vanishStatus = VanishProvider.isVanished(target) ? "&eJa" : "&aKein";
sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
sender.sendMessage(color("&eChatInfo: &f" + target.getName() + " &8@ &7" + tServer)); sender.sendMessage(color("&eChatInfo: &f" + target.getName() + " &8@ &7" + tServer));
sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
@@ -680,6 +782,7 @@ public class ChatModule implements Module, Listener {
sender.sendMessage(color("&7Mute: " + muteStatus)); sender.sendMessage(color("&7Mute: " + muteStatus));
sender.sendMessage(color("&7Chat-aus: " + (selfChatMuted.contains(tUUID) ? "&cJa" : "&aKein"))); sender.sendMessage(color("&7Chat-aus: " + (selfChatMuted.contains(tUUID) ? "&cJa" : "&aKein")));
sender.sendMessage(color("&7Mentions: " + (mentionsDisabled.contains(tUUID) ? "&cDeaktiviert" : "&aAktiv"))); sender.sendMessage(color("&7Mentions: " + (mentionsDisabled.contains(tUUID) ? "&cDeaktiviert" : "&aAktiv")));
sender.sendMessage(color("&7Vanish: " + vanishStatus));
sender.sendMessage(color("&7Blockiert: &f" + blocked.size() + " Spieler")); sender.sendMessage(color("&7Blockiert: &f" + blocked.size() + " Spieler"));
if (!blocked.isEmpty()) { if (!blocked.isEmpty()) {
for (UUID bUUID : blocked) { for (UUID bUUID : blocked) {
@@ -707,7 +810,6 @@ public class ChatModule implements Module, Listener {
int lines = config.getHistoryDefaultLines(); int lines = config.getHistoryDefaultLines();
if (args.length >= 1) { if (args.length >= 1) {
// Erstes Arg: Spielername oder Zahl?
try { try {
lines = Math.min(Integer.parseInt(args[0]), config.getHistoryMaxLines()); lines = Math.min(Integer.parseInt(args[0]), config.getHistoryMaxLines());
} catch (NumberFormatException ex) { } catch (NumberFormatException ex) {
@@ -765,7 +867,6 @@ public class ChatModule implements Module, Listener {
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, mentionsCmd); ProxyServer.getInstance().getPluginManager().registerCommand(plugin, mentionsCmd);
// /chatbypass Chat-Verarbeitung für nächste Eingabe(n) überspringen // /chatbypass Chat-Verarbeitung für nächste Eingabe(n) überspringen
// Nützlich wenn ein Plugin (CMI, Shop, etc.) auf eine Chat-Eingabe wartet
Command bypassCmd = new Command("chatbypass", null, "cbp") { Command bypassCmd = new Command("chatbypass", null, "cbp") {
@Override @Override
public void execute(CommandSender sender, String[] args) { public void execute(CommandSender sender, String[] args) {
@@ -784,9 +885,7 @@ public class ChatModule implements Module, Listener {
}; };
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, bypassCmd); ProxyServer.getInstance().getPluginManager().registerCommand(plugin, bypassCmd);
// ───────────────────────────────────────────────────── // /discordlink Discord-Account verknüpfen
// /discordlink Discord-Account verknüpfen
// ─────────────────────────────────────────────────────
Command discordLinkCmd = new Command("discordlink", null, "dlink") { Command discordLinkCmd = new Command("discordlink", null, "dlink") {
@Override @Override
public void execute(CommandSender sender, String[] args) { public void execute(CommandSender sender, String[] args) {
@@ -806,9 +905,7 @@ public class ChatModule implements Module, Listener {
}; };
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, discordLinkCmd); ProxyServer.getInstance().getPluginManager().registerCommand(plugin, discordLinkCmd);
// ───────────────────────────────────────────────────── // /telegramlink Telegram-Account verknüpfen
// /telegramlink Telegram-Account verknüpfen
// ─────────────────────────────────────────────────────
Command telegramLinkCmd = new Command("telegramlink", null, "tlink") { Command telegramLinkCmd = new Command("telegramlink", null, "tlink") {
@Override @Override
public void execute(CommandSender sender, String[] args) { public void execute(CommandSender sender, String[] args) {
@@ -828,9 +925,7 @@ public class ChatModule implements Module, Listener {
}; };
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, telegramLinkCmd); ProxyServer.getInstance().getPluginManager().registerCommand(plugin, telegramLinkCmd);
// ───────────────────────────────────────────────────── // /unlink Verknüpfung aufheben
// /unlink Verknüpfung aufheben
// ─────────────────────────────────────────────────────
Command unlinkCmd = new Command("unlink") { Command unlinkCmd = new Command("unlink") {
@Override @Override
public void execute(CommandSender sender, String[] args) { public void execute(CommandSender sender, String[] args) {
@@ -868,9 +963,7 @@ public class ChatModule implements Module, Listener {
}; };
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, unlinkCmd); ProxyServer.getInstance().getPluginManager().registerCommand(plugin, unlinkCmd);
// ───────────────────────────────────────────────────── // /report <spieler> <grund>
// NEU: /report <spieler> <grund>
// ─────────────────────────────────────────────────────
Command reportCmd = new Command("report") { Command reportCmd = new Command("report") {
@Override @Override
public void execute(CommandSender sender, String[] args) { public void execute(CommandSender sender, String[] args) {
@@ -879,7 +972,6 @@ public class ChatModule implements Module, Listener {
ProxiedPlayer p = (ProxiedPlayer) sender; ProxiedPlayer p = (ProxiedPlayer) sender;
// Permission prüfen (optional)
String reqPerm = config.getReportPermission(); String reqPerm = config.getReportPermission();
if (reqPerm != null && !reqPerm.isEmpty() && !p.hasPermission(reqPerm)) { if (reqPerm != null && !reqPerm.isEmpty() && !p.hasPermission(reqPerm)) {
p.sendMessage(color("&cDu hast keine Berechtigung für /report.")); return; p.sendMessage(color("&cDu hast keine Berechtigung für /report.")); return;
@@ -890,7 +982,6 @@ public class ChatModule implements Module, Listener {
return; return;
} }
// Cooldown
long now = System.currentTimeMillis() / 1000L; long now = System.currentTimeMillis() / 1000L;
Long last = reportCooldowns.get(p.getUniqueId()); Long last = reportCooldowns.get(p.getUniqueId());
if (last != null && (now - last) < config.getReportCooldown()) { if (last != null && (now - last) < config.getReportCooldown()) {
@@ -902,67 +993,54 @@ public class ChatModule implements Module, Listener {
String reportedName = args[0]; String reportedName = args[0];
String reason = String.join(" ", Arrays.copyOfRange(args, 1, args.length)); String reason = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
String server = p.getServer() != null ? p.getServer().getInfo().getName() : "Proxy"; String server = p.getServer() != null ? p.getServer().getInfo().getName() : "Proxy";
// Letzte Nachricht des Gemeldeten als Kontext
String msgContext = lastChatMessages.getOrDefault(reportedName.toLowerCase(), "(keine Chat-Nachricht bekannt)"); String msgContext = lastChatMessages.getOrDefault(reportedName.toLowerCase(), "(keine Chat-Nachricht bekannt)");
// Report erstellen
String reportId = reportManager.createReport( String reportId = reportManager.createReport(
p.getName(), p.getUniqueId(), reportedName, server, msgContext, reason); p.getName(), p.getUniqueId(), reportedName, server, msgContext, reason);
// Report auch ins Chatlog schreiben (ID sichtbar)
if (chatLogger != null) { if (chatLogger != null) {
String logMsg = "[REPORT] Reporter: " + p.getName() + ", Gemeldet: " + reportedName + ", Grund: " + reason + String logMsg = "[REPORT] Reporter: " + p.getName() + ", Gemeldet: " + reportedName
" | Letzte Nachricht: " + msgContext + " | Report-ID: " + reportId; + ", Grund: " + reason + " | Letzte Nachricht: " + msgContext
String msgId = reportId; // Damit die ID im Chatlog und im Report identisch ist + " | Report-ID: " + reportId;
chatLogger.log(msgId, server, "report", p.getName(), logMsg); chatLogger.log(reportId, server, "report", p.getName(), logMsg);
} }
// ==== Discord/Telegram Benachrichtigung ====
// Discord Webhook
String reportWebhook = config.getReportDiscordWebhook(); String reportWebhook = config.getReportDiscordWebhook();
logger.info("[Debug] DiscordWebhookEnabled=" + config.isReportWebhookEnabled() if (config.isReportWebhookEnabled() && discordBridge != null
+ ", discordBridge=" + (discordBridge != null) && reportWebhook != null && !reportWebhook.isEmpty()) {
+ ", reportWebhook=" + reportWebhook);
if (config.isReportWebhookEnabled() && discordBridge != null && reportWebhook != null && !reportWebhook.isEmpty()) {
String title = "Neuer Report eingegangen"; String title = "Neuer Report eingegangen";
String desc = "**Reporter:** " + p.getName() + String desc = "**Reporter:** " + p.getName()
"\n**Gemeldet:** " + reportedName + + "\n**Gemeldet:** " + reportedName
"\n**Server:** " + server + + "\n**Server:** " + server
"\n**Grund:** " + reason + + "\n**Grund:** " + reason
"\n**Letzte Nachricht:** " + msgContext + + "\n**Letzte Nachricht:** " + msgContext
"\n**Report-ID:** " + reportId; + "\n**Report-ID:** " + reportId;
discordBridge.sendEmbedToDiscord(reportWebhook, title, desc, config.getDiscordEmbedColor()); discordBridge.sendEmbedToDiscord(reportWebhook, title, desc, config.getDiscordEmbedColor());
} }
// Telegram Benachrichtigung
String reportTgChatId = config.getReportTelegramChatId(); String reportTgChatId = config.getReportTelegramChatId();
if (telegramBridge != null && reportTgChatId != null && !reportTgChatId.isEmpty()) { if (telegramBridge != null && reportTgChatId != null && !reportTgChatId.isEmpty()) {
String header = "Neuer Report eingegangen"; String header = "Neuer Report eingegangen";
String content = "Reporter: " + p.getName() + String content = "Reporter: " + p.getName()
"\nGemeldet: " + reportedName + + "\nGemeldet: " + reportedName
"\nServer: " + server + + "\nServer: " + server
"\nGrund: " + reason + + "\nGrund: " + reason
"\nLetzte Nachricht: " + msgContext + + "\nLetzte Nachricht: " + msgContext
"\nReport-ID: " + reportId; + "\nReport-ID: " + reportId;
telegramBridge.sendFormattedToTelegram(reportTgChatId, header, content); telegramBridge.sendFormattedToTelegram(reportTgChatId, header, content);
} }
reportCooldowns.put(p.getUniqueId(), now); reportCooldowns.put(p.getUniqueId(), now);
// Bestätigung an Reporter
String confirm = config.getReportConfirm().replace("{id}", reportId); String confirm = config.getReportConfirm().replace("{id}", reportId);
p.sendMessage(color(confirm)); p.sendMessage(color(confirm));
// ── Online-Admins sofort benachrichtigen ──
notifyAdminsReport(reportId, p.getName(), reportedName, server, reason, msgContext); notifyAdminsReport(reportId, p.getName(), reportedName, server, reason, msgContext);
} }
}; };
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, reportCmd); ProxyServer.getInstance().getPluginManager().registerCommand(plugin, reportCmd);
// ───────────────────────────────────────────────────── // /reports [all] Admin-Übersicht
// NEU: /reports [all] Admin-Übersicht
// ─────────────────────────────────────────────────────
Command reportsCmd = new Command("reports", config.getReportViewPermission()) { Command reportsCmd = new Command("reports", config.getReportViewPermission()) {
@Override @Override
public void execute(CommandSender sender, String[] args) { public void execute(CommandSender sender, String[] args) {
@@ -988,15 +1066,12 @@ public class ChatModule implements Module, Listener {
String statusColor = r.closed ? "&a✔" : "&c✘"; String statusColor = r.closed ? "&a✔" : "&c✘";
if (sender instanceof ProxiedPlayer) { if (sender instanceof ProxiedPlayer) {
// Klickbare Zeile: ID-Click kopiert ID in Zwischenablage
ComponentBuilder line = new ComponentBuilder(""); ComponentBuilder line = new ComponentBuilder("");
// Status
line.append(ChatColor.translateAlternateColorCodes('&', statusColor + " ")) line.append(ChatColor.translateAlternateColorCodes('&', statusColor + " "))
.event((ClickEvent) null) .event((ClickEvent) null)
.event((HoverEvent) null); .event((HoverEvent) null);
// Klickbare Report-ID
line.append(ChatColor.translateAlternateColorCodes('&', "&8[&f" + r.id + "&8]")) line.append(ChatColor.translateAlternateColorCodes('&', "&8[&f" + r.id + "&8]"))
.event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, r.id)) .event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, r.id))
.event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
@@ -1009,7 +1084,6 @@ public class ChatModule implements Module, Listener {
+ "\n" + ChatColor.YELLOW + "Grund: " + ChatColor.RED + r.reason + "\n" + ChatColor.YELLOW + "Grund: " + ChatColor.RED + r.reason
+ (r.closed ? "\n" + ChatColor.GREEN + "Geschlossen von: " + r.closedBy : "")).create())); + (r.closed ? "\n" + ChatColor.GREEN + "Geschlossen von: " + r.closedBy : "")).create()));
// Rest der Zeile
line.append(ChatColor.translateAlternateColorCodes('&', line.append(ChatColor.translateAlternateColorCodes('&',
" &b" + r.reportedName + " &8← &7" + r.reporterName " &b" + r.reportedName + " &8← &7" + r.reporterName
+ " &8@ &a" + r.server + " &8@ &a" + r.server
@@ -1019,7 +1093,6 @@ public class ChatModule implements Module, Listener {
((ProxiedPlayer) sender).sendMessage(line.create()); ((ProxiedPlayer) sender).sendMessage(line.create());
} else { } else {
// Konsole: plain text
sender.sendMessage(color(statusColor + " &8[&f" + r.id + "&8] &b" + r.reportedName sender.sendMessage(color(statusColor + " &8[&f" + r.id + "&8] &b" + r.reportedName
+ " &8← &7" + r.reporterName + " &8@ &a" + r.server + " &8← &7" + r.reporterName + " &8@ &a" + r.server
+ " &8| &e" + r.getFormattedTime() + " &8| &e" + r.getFormattedTime()
@@ -1035,9 +1108,7 @@ public class ChatModule implements Module, Listener {
}; };
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, reportsCmd); ProxyServer.getInstance().getPluginManager().registerCommand(plugin, reportsCmd);
// ───────────────────────────────────────────────────── // /reportclose <ID>
// NEU: /reportclose <ID>
// ─────────────────────────────────────────────────────
Command reportCloseCmd = new Command("reportclose", config.getReportClosePermission()) { Command reportCloseCmd = new Command("reportclose", config.getReportClosePermission()) {
@Override @Override
public void execute(CommandSender sender, String[] args) { public void execute(CommandSender sender, String[] args) {
@@ -1062,14 +1133,12 @@ public class ChatModule implements Module, Listener {
sender.sendMessage(color("&aReport &f" + id + " &awurde geschlossen.")); sender.sendMessage(color("&aReport &f" + id + " &awurde geschlossen."));
// Reporter benachrichtigen, falls online
ProxiedPlayer reporter = ProxyServer.getInstance().getPlayer(report.reporterUUID); ProxiedPlayer reporter = ProxyServer.getInstance().getPlayer(report.reporterUUID);
if (reporter != null && reporter.isConnected()) { if (reporter != null && reporter.isConnected()) {
reporter.sendMessage(color("&8[&aReport&8] &7Dein Report &f" + id reporter.sendMessage(color("&8[&aReport&8] &7Dein Report &f" + id
+ " &7gegen &c" + report.reportedName + " &7wurde von &b" + adminName + " &7bearbeitet.")); + " &7gegen &c" + report.reportedName + " &7wurde von &b" + adminName + " &7bearbeitet."));
} }
// Andere Admins informieren
notifyAdmins("&8[&aReport geschlossen&8] &f" + adminName + " &7hat Report &f" + id + " &7geschlossen."); notifyAdmins("&8[&aReport geschlossen&8] &f" + adminName + " &7hat Report &f" + id + " &7geschlossen.");
} }
}; };
@@ -1078,38 +1147,10 @@ public class ChatModule implements Module, Listener {
// ========================================================= // =========================================================
// PLUGIN-INPUT BYPASS // PLUGIN-INPUT BYPASS
//
// Spieler die gerade auf eine Chat-Eingabe eines Sub-Server-
// Plugins warten (CMI, Shops, etc.) werden vom ChatModule
// übersprungen. Die Nachricht geht direkt an den Sub-Server.
//
// Drei Erkennungsmethoden:
// 1. Manueller Bypass-Toggle via /chatbypass (für Admins)
// 2. Programmatische API: ChatModule.setAwaitingInput(uuid, true)
// 3. Automatische Erkennung bekannter Plugin-Nachrichten
// ========================================================= // =========================================================
// UUIDs die gerade auf Plugin-Chat-Eingabe warten
private final Set<UUID> awaitingInput = Collections.newSetFromMap(new ConcurrentHashMap<>());
/** /**
* Prüft ob ein Spieler gerade auf eine Chat-Eingabe eines * Öffentliche API für Sub-Server-Plugins oder BungeeCord-eigene Plugins.
* Sub-Server-Plugins wartet und das ChatModule überspringen soll.
*/
private boolean isAwaitingPluginInput(ProxiedPlayer player) {
// 1. Manuell / programmatisch gesetzt
if (awaitingInput.contains(player.getUniqueId())) return true;
// 2. Automatische Erkennung: BungeeCord leitet SubServer-Nachrichten
// via PluginChannel weiter wir prüfen bekannte CMI-Patterns nicht,
// da wir keinen Zugriff auf SubServer-Metadaten haben.
// Stattdessen: Spieler kann selbst /chatbypass togglen oder
// Sub-Server-Plugin ruft setAwaitingInput() auf.
return false;
}
/**
* Öffentliche API für Sub-Server-Plugins oder BungeeCord-eigene Plugins:
* Setzt den Bypass-Status für einen Spieler. * Setzt den Bypass-Status für einen Spieler.
* *
* Beispiel aus einem anderen BungeeCord-Plugin: * Beispiel aus einem anderen BungeeCord-Plugin:
@@ -1126,6 +1167,24 @@ public class ChatModule implements Module, Listener {
return awaitingInput.contains(uuid); return awaitingInput.contains(uuid);
} }
// =========================================================
// VANISH-HILFSMETHODEN
// =========================================================
/**
* Sucht einen Spieler nach Name und berücksichtigt den Vanish-Status.
*
* @param name Spielername (case-insensitiv)
* @param callerIsAdmin true → Vanished Spieler werden ebenfalls gefunden
* @return ProxiedPlayer oder null wenn nicht gefunden / vanished (für Nicht-Admins)
*/
private ProxiedPlayer findVisiblePlayer(String name, boolean callerIsAdmin) {
ProxiedPlayer target = ProxyServer.getInstance().getPlayer(name);
if (target == null) return null;
if (!callerIsAdmin && VanishProvider.isVanished(target)) return null;
return target;
}
// ========================================================= // =========================================================
// HILFSMETHODEN // HILFSMETHODEN
// ========================================================= // =========================================================
@@ -1133,7 +1192,7 @@ public class ChatModule implements Module, Listener {
private String buildFormat(String format, String server, String prefix, private String buildFormat(String format, String server, String prefix,
String player, String suffix, String message) { String player, String suffix, String message) {
String serverColor = config.getServerColor(server); String serverColor = config.getServerColor(server);
String serverDisplay = config.getServerDisplay(server); // Anzeigename aus config String serverDisplay = config.getServerDisplay(server);
String coloredServer = translateColors(serverColor + serverDisplay + "&r"); String coloredServer = translateColors(serverColor + serverDisplay + "&r");
return format return format
@@ -1158,23 +1217,13 @@ public class ChatModule implements Module, Listener {
/** /**
* Übersetzt sowohl klassische &-Farbcodes als auch HEX-Codes im Format &#RRGGBB. * Übersetzt sowohl klassische &-Farbcodes als auch HEX-Codes im Format &#RRGGBB.
*
* Beispiele:
* &a → §a (Grün)
* &#FF5500 → BungeeCord HEX-Farbe Orange
* &l&#FF5500Text → Fett + Orange
*
* BungeeCord unterstützt ChatColor.of("#RRGGBB") ab 1.16-kompatiblen Builds.
* Ältere Builds erhalten automatisch den nächsten &-Code als Fallback.
*/ */
private String translateColors(String text) { private String translateColors(String text) {
if (text == null) return ""; if (text == null) return "";
// 1. Schritt: &#RRGGBB → BungeeCord ChatColor
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
int i = 0; int i = 0;
while (i < text.length()) { while (i < text.length()) {
// Prüfe auf &#RRGGBB (8 Zeichen: & # R R G G B B)
if (i + 7 < text.length() if (i + 7 < text.length()
&& text.charAt(i) == '&' && text.charAt(i) == '&'
&& text.charAt(i + 1) == '#') { && text.charAt(i + 1) == '#') {
@@ -1195,7 +1244,6 @@ public class ChatModule implements Module, Listener {
i++; i++;
} }
// 2. Schritt: Standard &-Codes übersetzen
return ChatColor.translateAlternateColorCodes('&', sb.toString()); return ChatColor.translateAlternateColorCodes('&', sb.toString());
} }
@@ -1209,11 +1257,9 @@ public class ChatModule implements Module, Listener {
/** /**
* Benachrichtigt alle online Admins über einen neuen Report. * Benachrichtigt alle online Admins über einen neuen Report.
* Baut eine mehrzeilige, klickbare Nachricht.
*/ */
private void notifyAdminsReport(String reportId, String reporter, String reported, private void notifyAdminsReport(String reportId, String reporter, String reported,
String server, String reason, String msgContext) { String server, String reason, String msgContext) {
// Zeitstempel
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("dd.MM.yyyy HH:mm:ss"); java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("dd.MM.yyyy HH:mm:ss");
String zeit = sdf.format(new java.util.Date()); String zeit = sdf.format(new java.util.Date());
@@ -1221,7 +1267,6 @@ public class ChatModule implements Module, Listener {
if (!p.hasPermission(config.getAdminNotifyPermission()) if (!p.hasPermission(config.getAdminNotifyPermission())
&& !p.hasPermission(config.getAdminBypassPermission())) continue; && !p.hasPermission(config.getAdminBypassPermission())) continue;
// Mehrzeilige Report-Notification
p.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); p.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
p.sendMessage(color("&8[&cReport&8] &7Zeit: &e" + zeit)); p.sendMessage(color("&8[&cReport&8] &7Zeit: &e" + zeit));
p.sendMessage(color("&7Reporter: &b" + reporter)); p.sendMessage(color("&7Reporter: &b" + reporter));
@@ -1229,7 +1274,6 @@ public class ChatModule implements Module, Listener {
p.sendMessage(color("&7Letzte Nachricht: &f" + msgContext)); p.sendMessage(color("&7Letzte Nachricht: &f" + msgContext));
p.sendMessage(color("&7Grund: &c" + reason)); p.sendMessage(color("&7Grund: &c" + reason));
// Klickbare ID-Zeile
ComponentBuilder idLine = new ComponentBuilder(ChatColor.GRAY + "ID: "); ComponentBuilder idLine = new ComponentBuilder(ChatColor.GRAY + "ID: ");
idLine.append(ChatColor.WHITE + "" + ChatColor.BOLD + reportId) idLine.append(ChatColor.WHITE + "" + ChatColor.BOLD + reportId)
.event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, reportId)) .event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, reportId))
@@ -1244,28 +1288,22 @@ public class ChatModule implements Module, Listener {
p.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); p.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
} }
// Konsole ebenfalls informieren
ProxyServer.getInstance().getConsole().sendMessage(color( ProxyServer.getInstance().getConsole().sendMessage(color(
"&8[&cReport " + reportId + "&8] &7Reporter: &b" + reporter "&8[&cReport " + reportId + "&8] &7Reporter: &b" + reporter
+ " &7→ &c" + reported + " &7@ &a" + server + " &8| &cGrund: &f" + reason)); + " &7→ &c" + reported + " &7@ &a" + server + " &8| &cGrund: &f" + reason));
} }
/** /**
* Baut eine BungeeCord-Nachricht ohne sichtbare ID. * Baut eine BungeeCord-Nachricht mit klickbarem [⚑] Melden-Button.
* Am Ende erscheint ein klickbarer [⚑] Melden-Button (nur wenn Reports aktiviert).
*
* Layout: <formatierter Chat> §8[§c⚑§8]
*/ */
private BaseComponent[] buildClickableMessage(String msgId, String formatted, String senderName) { private BaseComponent[] buildClickableMessage(String msgId, String formatted, String senderName) {
ComponentBuilder builder = new ComponentBuilder(""); ComponentBuilder builder = new ComponentBuilder("");
// Eigentliche Nachricht (kein ID-Tag mehr sichtbar)
builder.append(ChatColor.translateAlternateColorCodes('&', formatted), builder.append(ChatColor.translateAlternateColorCodes('&', formatted),
ComponentBuilder.FormatRetention.NONE) ComponentBuilder.FormatRetention.NONE)
.event((ClickEvent) null) .event((ClickEvent) null)
.event((HoverEvent) null); .event((HoverEvent) null);
// [⚑] Melden-Button am Ende (nur wenn Report-System aktiv und Sender bekannt)
if (msgId != null && senderName != null && reportManager != null) { if (msgId != null && senderName != null && reportManager != null) {
builder.append(" ", ComponentBuilder.FormatRetention.NONE) builder.append(" ", ComponentBuilder.FormatRetention.NONE)
.event((ClickEvent) null) .event((ClickEvent) null)
@@ -1287,15 +1325,11 @@ public class ChatModule implements Module, Listener {
} }
/** /**
* Sendet einen Sound an einen Spieler via Plugin-Messaging. * Sendet einen Sound-Hinweis via Actionbar (Mention-Feedback).
* Der Sub-Server muss den Kanal "BungeeCord" registriert haben (standard).
* Sound wird als Proxy-Message gesendet → Sub-Server-Plugin nötig für echten Sound.
* Als Fallback: Actionbar-Nachricht mit ♪-Symbol.
*/ */
private void sendMentionSound(ProxiedPlayer player, String soundName) { private void sendMentionSound(ProxiedPlayer player, String soundName) {
if (soundName == null || soundName.isEmpty()) return; if (soundName == null || soundName.isEmpty()) return;
try { try {
// Actionbar als visuellen Feedback (funktioniert ohne Sub-Server-Plugin)
net.md_5.bungee.api.chat.TextComponent actionBar = net.md_5.bungee.api.chat.TextComponent actionBar =
new net.md_5.bungee.api.chat.TextComponent( new net.md_5.bungee.api.chat.TextComponent(
ChatColor.translateAlternateColorCodes('&', "&e♪ Mention!")); ChatColor.translateAlternateColorCodes('&', "&e♪ Mention!"));

View File

@@ -205,7 +205,7 @@ public class ReportManager {
logger.warning("[ChatModule] Fehler beim Laden der Reports: " + e.getMessage()); logger.warning("[ChatModule] Fehler beim Laden der Reports: " + e.getMessage());
} }
idCounter.set(maxNum); idCounter.set(maxNum);
logger.info("[ChatModule] " + reports.size() + " Reports geladen (" + getOpenCount() + " offen).");
} }
// ===== Escape-Helfer (Pipe-Zeichen und Zeilenumbrüche escapen) ===== // ===== Escape-Helfer (Pipe-Zeichen und Zeilenumbrüche escapen) =====

View File

@@ -0,0 +1,71 @@
package net.viper.status.modules.chat;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import java.util.Collections;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* Zentrale Schnittstelle zwischen dem VanishModule und dem ChatModule.
*
* Das VanishModule (oder jedes andere Modul) ruft {@link #setVanished} auf
* um Spieler als unsichtbar zu markieren. Das ChatModule prüft via
* {@link #isVanished} bevor es Join-/Leave-Nachrichten sendet oder
* Privat-Nachrichten zulässt.
*
* Verwendung im VanishModule:
* VanishProvider.setVanished(player.getUniqueId(), true); // beim Verschwinden
* VanishProvider.setVanished(player.getUniqueId(), false); // beim Erscheinen / Disconnect
*/
public final class VanishProvider {
private VanishProvider() {}
/** Intern verwaltete Menge aller aktuell unsichtbaren Spieler-UUIDs. */
private static final Set<UUID> vanishedPlayers =
Collections.newSetFromMap(new ConcurrentHashMap<>());
// ===== Schreib-API (wird vom VanishModule aufgerufen) =====
/**
* Markiert einen Spieler als sichtbar oder unsichtbar.
*
* @param uuid UUID des Spielers
* @param vanished true = unsichtbar, false = sichtbar
*/
public static void setVanished(UUID uuid, boolean vanished) {
if (vanished) {
vanishedPlayers.add(uuid);
} else {
vanishedPlayers.remove(uuid);
}
}
/**
* Entfernt einen Spieler beim Disconnect aus der Vanish-Liste.
* Sollte vom ChatModule (onDisconnect) aufgerufen werden, damit
* kein toter Eintrag verbleibt.
*/
public static void cleanup(UUID uuid) {
vanishedPlayers.remove(uuid);
}
// ===== Lese-API (wird vom ChatModule aufgerufen) =====
/** @return true wenn der Spieler aktuell als unsichtbar markiert ist. */
public static boolean isVanished(ProxiedPlayer player) {
return player != null && vanishedPlayers.contains(player.getUniqueId());
}
/** @return true wenn der Spieler mit der angegebenen UUID unsichtbar ist. */
public static boolean isVanished(UUID uuid) {
return uuid != null && vanishedPlayers.contains(uuid);
}
/** Snapshot der aktuell unsichtbaren Spieler (für Debugging / Logs). */
public static Set<UUID> getVanishedPlayers() {
return Collections.unmodifiableSet(vanishedPlayers);
}
}

View File

@@ -1,424 +1,323 @@
package net.viper.status.modules.chat.bridge; package net.viper.status.modules.chat.bridge;
import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.ProxyServer; import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.plugin.Plugin; import net.md_5.bungee.api.plugin.Plugin;
import net.viper.status.modules.chat.AccountLinkManager; import net.viper.status.modules.chat.AccountLinkManager;
import net.viper.status.modules.chat.ChatConfig; import net.viper.status.modules.chat.ChatConfig;
import java.io.*; import java.io.*;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Logger; import java.util.logging.Logger;
/** /**
* Discord-Brücke für bidirektionale Kommunikation. * Discord-Brücke für bidirektionale Kommunikation.
* *
* Minecraft → Discord: Via Webhook (kein Bot benötigt) * Fix #12: extractJsonString() behandelt Escape-Sequenzen jetzt korrekt.
* Discord → Minecraft: Via Bot-Polling der Discord REST-API * Statt Zeichenvergleich mit dem Vorgänger-Char wird ein expliziter Escape-Flag verwendet.
* */
* Voraussetzungen: public class DiscordBridge {
* - Bot mit "Read Message History" und "Send Messages" Permissions
* - Bot muss in den jeweiligen Kanälen sein private final Plugin plugin;
* - Bot-Token in chat.yml eintragen private final ChatConfig config;
*/ private final Logger logger;
public class DiscordBridge { private AccountLinkManager linkManager;
private final Plugin plugin; private final java.util.Map<String, AtomicLong> lastMessageIds = new java.util.concurrent.ConcurrentHashMap<>();
private final ChatConfig config; private volatile boolean running = false;
private final Logger logger;
private AccountLinkManager linkManager; public DiscordBridge(Plugin plugin, ChatConfig config) {
this.plugin = plugin;
// Letztes verarbeitetes Discord Message-ID pro Kanal (für Polling) this.config = config;
private final java.util.Map<String, AtomicLong> lastMessageIds = new java.util.concurrent.ConcurrentHashMap<>(); this.logger = plugin.getLogger();
}
private volatile boolean running = false;
public void setLinkManager(AccountLinkManager linkManager) { this.linkManager = linkManager; }
public DiscordBridge(Plugin plugin, ChatConfig config) {
this.plugin = plugin; public void start() {
this.config = config; if (!config.isDiscordEnabled()
this.logger = plugin.getLogger(); || config.getDiscordBotToken().isEmpty()
} || config.getDiscordBotToken().equals("YOUR_BOT_TOKEN_HERE")) {
logger.warning("[ChatModule-Discord] Bot-Token nicht konfiguriert. Discord-Empfang deaktiviert.");
/** Setzt den AccountLinkManager muss vor start() aufgerufen werden. */ return;
public void setLinkManager(AccountLinkManager linkManager) { }
this.linkManager = linkManager; running = true;
} int interval = Math.max(2, config.getDiscordPollInterval());
plugin.getProxy().getScheduler().schedule(plugin, this::pollAllChannels, interval, interval, TimeUnit.SECONDS);
public void start() { logger.info("[ChatModule-Discord] Brücke gestartet (Poll-Intervall: " + interval + "s).");
if (!config.isDiscordEnabled() }
|| config.getDiscordBotToken().isEmpty()
|| config.getDiscordBotToken().equals("YOUR_BOT_TOKEN_HERE")) { public void stop() { running = false; }
logger.warning("[ChatModule-Discord] Bot-Token nicht konfiguriert. Discord-Empfang deaktiviert.");
return; // ===== Minecraft → Discord =====
}
public void sendToDiscord(String webhookUrl, String username, String message, String avatarUrl) {
running = true; if (webhookUrl == null || webhookUrl.isEmpty()) return;
plugin.getProxy().getScheduler().runAsync(plugin, () -> {
// Starte Polling-Task für alle konfigurierten Kanäle try {
int interval = Math.max(2, config.getDiscordPollInterval()); String payload = "{\"username\":\"" + escapeJson(username) + "\""
plugin.getProxy().getScheduler().schedule(plugin, this::pollAllChannels, + (avatarUrl != null && !avatarUrl.isEmpty() ? ",\"avatar_url\":\"" + avatarUrl + "\"" : "")
interval, interval, TimeUnit.SECONDS); + ",\"content\":\"" + escapeJson(message) + "\"}";
postJson(webhookUrl, payload, null);
logger.info("[ChatModule-Discord] Brücke gestartet (Poll-Intervall: " + interval + "s)."); } catch (Exception e) {
} logger.warning("[ChatModule-Discord] Webhook-Fehler: " + e.getMessage());
}
public void stop() { });
running = false; }
}
public void sendEmbedToDiscord(String webhookUrl, String title, String description, String colorHex) {
// ===== Minecraft → Discord ===== if (webhookUrl == null || webhookUrl.isEmpty()) return;
plugin.getProxy().getScheduler().runAsync(plugin, () -> {
/** try {
* Sendet eine Nachricht via Webhook an Discord. int color = 0x5865F2;
* Funktioniert ohne Bot-Token! try { color = Integer.parseInt(colorHex.replace("#", ""), 16); } catch (Exception ignored) {}
*/ String payload = "{\"embeds\":[{\"title\":\"" + escapeJson(title) + "\""
public void sendToDiscord(String webhookUrl, String username, String message, String avatarUrl) { + ",\"description\":\"" + escapeJson(description) + "\""
if (webhookUrl == null || webhookUrl.isEmpty()) return; + ",\"color\":" + color + "}]}";
postJson(webhookUrl, payload, null);
plugin.getProxy().getScheduler().runAsync(plugin, () -> { } catch (Exception e) {
try { logger.warning("[ChatModule-Discord] Embed-Fehler: " + e.getMessage());
String safeUsername = escapeJson(username); }
String safeMessage = escapeJson(message); });
String payload = "{\"username\":\"" + safeUsername + "\"" }
+ (avatarUrl != null && !avatarUrl.isEmpty()
? ",\"avatar_url\":\"" + avatarUrl + "\"" public void sendToChannel(String channelId, String message) {
: "") if (channelId == null || channelId.isEmpty()) return;
+ ",\"content\":\"" + safeMessage + "\"}"; if (config.getDiscordBotToken().isEmpty()) return;
plugin.getProxy().getScheduler().runAsync(plugin, () -> {
postJson(webhookUrl, payload, null); try {
} catch (Exception e) { String url = "https://discord.com/api/v10/channels/" + channelId + "/messages";
logger.warning("[ChatModule-Discord] Webhook-Fehler: " + e.getMessage()); postJson(url, "{\"content\":\"" + escapeJson(message) + "\"}", "Bot " + config.getDiscordBotToken());
} } catch (Exception e) {
}); logger.warning("[ChatModule-Discord] Send-to-Channel-Fehler: " + e.getMessage());
} }
});
/** }
* Sendet eine Embed-Nachricht (für HelpOp, Broadcast) an einen Discord-Kanal via Webhook.
*/ // ===== Discord → Minecraft (Polling) =====
public void sendEmbedToDiscord(String webhookUrl, String title, String description, String colorHex) {
if (webhookUrl == null || webhookUrl.isEmpty()) return; private void pollAllChannels() {
if (!running) return;
plugin.getProxy().getScheduler().runAsync(plugin, () -> { java.util.Set<String> channelIds = new java.util.LinkedHashSet<>();
try { for (net.viper.status.modules.chat.ChatChannel ch : config.getChannels().values()) {
int color = 0; if (!ch.getDiscordChannelId().isEmpty()) channelIds.add(ch.getDiscordChannelId());
try { color = Integer.parseInt(colorHex.replace("#", ""), 16); } }
catch (Exception ignored) { color = 0x5865F2; } if (!config.getDiscordAdminChannelId().isEmpty()) channelIds.add(config.getDiscordAdminChannelId());
for (String channelId : channelIds) pollChannel(channelId);
String payload = "{\"embeds\":[{\"title\":\"" + escapeJson(title) + "\"" }
+ ",\"description\":\"" + escapeJson(description) + "\""
+ ",\"color\":" + color + "}]}"; private void pollChannel(String channelId) {
try {
logger.info("[ChatModule-Discord] Sende Embed an Webhook: " + webhookUrl); AtomicLong lastId = lastMessageIds.computeIfAbsent(channelId, k -> new AtomicLong(0L));
logger.info("[ChatModule-Discord] Payload: " + payload); if (lastId.get() == 0L) {
String initResp = getJson("https://discord.com/api/v10/channels/" + channelId + "/messages?limit=1",
postJson(webhookUrl, payload, null); "Bot " + config.getDiscordBotToken());
if (initResp != null && !initResp.equals("[]") && !initResp.isEmpty()) {
logger.info("[ChatModule-Discord] Embed erfolgreich an Discord gesendet."); java.util.List<DiscordMessage> initMsgs = parseMessages(initResp);
} catch (Exception e) { if (!initMsgs.isEmpty()) lastId.set(initMsgs.get(0).id);
logger.warning("[ChatModule-Discord] Embed-Fehler: " + e.getMessage()); }
} return;
}); }
}
String url = "https://discord.com/api/v10/channels/" + channelId + "/messages?after=" + lastId.get() + "&limit=10";
/** String response = getJson(url, "Bot " + config.getDiscordBotToken());
* Sendet eine Nachricht direkt in einen Discord-Kanal via Bot-Token. if (response == null || response.equals("[]") || response.isEmpty()) return;
* Benötigt: DISCORD_BOT_TOKEN, channel-id
*/ java.util.List<DiscordMessage> messages = parseMessages(response);
public void sendToChannel(String channelId, String message) { messages.sort(java.util.Comparator.comparingLong(m -> m.id));
if (channelId == null || channelId.isEmpty()) return;
if (config.getDiscordBotToken().isEmpty()) return; for (DiscordMessage msg : messages) {
if (msg.id <= lastId.get()) continue;
plugin.getProxy().getScheduler().runAsync(plugin, () -> { if (msg.isBot) continue;
try { if (msg.content.isEmpty()) continue;
String url = "https://discord.com/api/v10/channels/" + channelId + "/messages"; lastId.set(msg.id);
String payload = "{\"content\":\"" + escapeJson(message) + "\"}";
postJson(url, payload, "Bot " + config.getDiscordBotToken()); if (msg.content.startsWith("!link ")) {
} catch (Exception e) { String token = msg.content.substring(6).trim().toUpperCase();
logger.warning("[ChatModule-Discord] Send-to-Channel-Fehler: " + e.getMessage()); if (linkManager != null) {
} AccountLinkManager.LinkedAccount acc = linkManager.redeemDiscord(token, msg.authorId, msg.authorName);
}); if (acc != null) sendToChannel(channelId, "✅ Verknüpfung erfolgreich! Minecraft-Account: **" + acc.minecraftName + "**");
} else sendToChannel(channelId, "❌ Ungültiger oder abgelaufener Token. Bitte `/discordlink` im Spiel erneut ausführen.");
}
// ===== Discord → Minecraft (Polling) ===== continue;
}
private void pollAllChannels() {
if (!running) return; String displayName = (linkManager != null)
? linkManager.resolveDiscordName(msg.authorId, msg.authorName) : msg.authorName;
// Alle Kanal-IDs aus der Konfiguration sammeln String mcFormat = resolveFormat(channelId);
java.util.Set<String> channelIds = new java.util.LinkedHashSet<>(); if (mcFormat == null) continue;
for (net.viper.status.modules.chat.ChatChannel ch : config.getChannels().values()) { String formatted = ChatColor.translateAlternateColorCodes('&',
if (!ch.getDiscordChannelId().isEmpty()) { mcFormat.replace("{user}", displayName).replace("{message}", msg.content));
channelIds.add(ch.getDiscordChannelId()); ProxyServer.getInstance().getScheduler().runAsync(plugin,
} () -> ProxyServer.getInstance().broadcast(new TextComponent(formatted)));
} }
if (!config.getDiscordAdminChannelId().isEmpty()) { } catch (Exception e) {
channelIds.add(config.getDiscordAdminChannelId()); logger.fine("[ChatModule-Discord] Poll-Fehler für Kanal " + channelId + ": " + e.getMessage());
} }
}
for (String channelId : channelIds) {
pollChannel(channelId); private String resolveFormat(String channelId) {
} if (channelId.equals(config.getDiscordAdminChannelId())) return config.getDiscordFromFormat();
} for (net.viper.status.modules.chat.ChatChannel ch : config.getChannels().values()) {
if (channelId.equals(ch.getDiscordChannelId())) return config.getDiscordFromFormat();
private void pollChannel(String channelId) { }
try { return null;
AtomicLong lastId = lastMessageIds.computeIfAbsent(channelId, k -> new AtomicLong(0L)); }
// Beim ersten Poll: aktuelle neueste ID holen und merken, nicht broadcasten. // ===== HTTP =====
// So werden beim Start keine alten Discord-Nachrichten in Minecraft angezeigt.
if (lastId.get() == 0L) { private void postJson(String urlStr, String payload, String authorization) throws Exception {
String initUrl = "https://discord.com/api/v10/channels/" + channelId + "/messages?limit=1"; HttpURLConnection conn = openConnection(urlStr, "POST", authorization);
String initResp = getJson(initUrl, "Bot " + config.getDiscordBotToken()); byte[] data = payload.getBytes(StandardCharsets.UTF_8);
if (initResp != null && !initResp.equals("[]") && !initResp.isEmpty()) { conn.setRequestProperty("Content-Length", String.valueOf(data.length));
java.util.List<DiscordMessage> initMsgs = parseMessages(initResp); conn.setDoOutput(true);
if (!initMsgs.isEmpty()) { try (OutputStream os = conn.getOutputStream()) { os.write(data); }
lastId.set(initMsgs.get(0).id); int code = conn.getResponseCode();
} if (code >= 400) logger.warning("[ChatModule-Discord] HTTP " + code + ": " + readStream(conn.getErrorStream()));
} conn.disconnect();
return; // Erster Poll nur zum Initialisieren, nichts broadcasten }
}
private String getJson(String urlStr, String authorization) throws Exception {
String afterParam = "?after=" + lastId.get() + "&limit=10"; HttpURLConnection conn = openConnection(urlStr, "GET", authorization);
int code = conn.getResponseCode();
String url = "https://discord.com/api/v10/channels/" + channelId + "/messages" + afterParam; if (code != 200) { conn.disconnect(); return null; }
String response = getJson(url, "Bot " + config.getDiscordBotToken()); String result = readStream(conn.getInputStream());
if (response == null || response.equals("[]") || response.isEmpty()) return; conn.disconnect();
return result;
// JSON-Array von Nachrichten parsen (ohne externe Library) }
java.util.List<DiscordMessage> messages = parseMessages(response);
private HttpURLConnection openConnection(String urlStr, String method, String authorization) throws Exception {
// Nachrichten chronologisch verarbeiten (älteste zuerst) HttpURLConnection conn = (HttpURLConnection) new URL(urlStr).openConnection();
messages.sort(java.util.Comparator.comparingLong(m -> m.id)); conn.setRequestMethod(method);
conn.setConnectTimeout(5000);
for (DiscordMessage msg : messages) { conn.setReadTimeout(8000);
if (msg.id <= lastId.get()) continue; conn.setRequestProperty("Content-Type", "application/json");
if (msg.isBot) continue; conn.setRequestProperty("User-Agent", "StatusAPI-ChatModule/1.0");
if (msg.content.isEmpty()) continue; if (authorization != null && !authorization.isEmpty()) conn.setRequestProperty("Authorization", authorization);
return conn;
lastId.set(msg.id); }
// ── Token-Einlösung: !link <TOKEN> ── private String readStream(InputStream in) throws IOException {
if (msg.content.startsWith("!link ")) { if (in == null) return "";
String token = msg.content.substring(6).trim().toUpperCase(); try (BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
if (linkManager != null) { StringBuilder sb = new StringBuilder(); String line;
AccountLinkManager.LinkedAccount acc = while ((line = br.readLine()) != null) sb.append(line);
linkManager.redeemDiscord(token, msg.authorId, msg.authorName); return sb.toString();
if (acc != null) { }
sendToChannel(channelId, }
"✅ Verknüpfung erfolgreich! Minecraft-Account: **"
+ acc.minecraftName + "**"); // ===== JSON Mini-Parser =====
} else {
sendToChannel(channelId, private static class DiscordMessage {
"❌ Ungültiger oder abgelaufener Token. Bitte `/discordlink` im Spiel erneut ausführen."); long id;
} String authorId = "", authorName = "", content = "";
} boolean isBot = false;
continue; // Nicht als Chat-Nachricht weiterleiten }
}
private java.util.List<DiscordMessage> parseMessages(String json) {
// ── Account-Name auflösen ── java.util.List<DiscordMessage> result = new java.util.ArrayList<>();
String displayName = (linkManager != null) int depth = 0, start = -1;
? linkManager.resolveDiscordName(msg.authorId, msg.authorName) for (int i = 0; i < json.length(); i++) {
: msg.authorName; char c = json.charAt(i);
if (c == '{') { if (depth++ == 0) start = i; }
// Welchem Kanal gehört diese Discord-Kanal-ID? else if (c == '}') {
final String mcFormat = resolveFormat(channelId); if (--depth == 0 && start != -1) {
if (mcFormat == null) continue; DiscordMessage msg = parseMessage(json.substring(start, i + 1));
if (msg != null) result.add(msg);
final String formatted = ChatColor.translateAlternateColorCodes('&', start = -1;
mcFormat.replace("{user}", displayName) }
.replace("{message}", msg.content)); }
}
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> return result;
ProxyServer.getInstance().broadcast(new TextComponent(formatted)) }
);
} private DiscordMessage parseMessage(String obj) {
} catch (Exception e) { try {
logger.fine("[ChatModule-Discord] Poll-Fehler für Kanal " + channelId + ": " + e.getMessage()); DiscordMessage msg = new DiscordMessage();
} msg.id = Long.parseLong(extractJsonString(obj, "id"));
} msg.content = unescapeJson(extractJsonString(obj, "content"));
private String resolveFormat(String channelId) { // Webhook-Nachrichten als Bot markieren (Echo-Loop verhindern)
// Admin-Kanal? if (!extractJsonString(obj, "webhook_id").isEmpty()) {
if (channelId.equals(config.getDiscordAdminChannelId())) { msg.isBot = true;
return config.getDiscordFromFormat(); return msg;
} }
// Reguläre Kanäle
for (net.viper.status.modules.chat.ChatChannel ch : config.getChannels().values()) { int authStart = obj.indexOf("\"author\"");
if (channelId.equals(ch.getDiscordChannelId())) { if (authStart >= 0) {
return config.getDiscordFromFormat(); String authBlock = extractJsonObject(obj, authStart);
} msg.authorId = extractJsonString(authBlock, "id");
} msg.authorName = unescapeJson(extractJsonString(authBlock, "username"));
return null; msg.isBot = "true".equals(extractJsonString(authBlock, "bot"));
} }
return msg;
// ===== HTTP-Hilfsklassen ===== } catch (Exception e) {
return null;
private void postJson(String urlStr, String payload, String authorization) throws Exception { }
HttpURLConnection conn = openConnection(urlStr, "POST", authorization); }
byte[] data = payload.getBytes(StandardCharsets.UTF_8);
conn.setRequestProperty("Content-Length", String.valueOf(data.length)); /**
conn.setDoOutput(true); * FIX #12: Escape-Sequenzen werden korrekt mit einem Escape-Flag behandelt
try (OutputStream os = conn.getOutputStream()) { os.write(data); } * statt den Vorgänger-Char zu vergleichen (der bei '\\' + '"' versagt).
int code = conn.getResponseCode(); * Gibt immer einen leeren String zurück wenn der Key nicht gefunden wird (nie null).
if (code >= 400) { */
String err = readStream(conn.getErrorStream()); private String extractJsonString(String json, String key) {
logger.warning("[ChatModule-Discord] HTTP " + code + ": " + err); if (json == null || key == null) return "";
} String fullKey = "\"" + key + "\"";
conn.disconnect(); int keyIdx = json.indexOf(fullKey);
} if (keyIdx < 0) return "";
int colon = json.indexOf(':', keyIdx + fullKey.length());
private String getJson(String urlStr, String authorization) throws Exception { if (colon < 0) return "";
HttpURLConnection conn = openConnection(urlStr, "GET", authorization); int valStart = colon + 1;
int code = conn.getResponseCode(); while (valStart < json.length() && json.charAt(valStart) == ' ') valStart++;
if (code != 200) { conn.disconnect(); return null; } if (valStart >= json.length()) return "";
String result = readStream(conn.getInputStream()); char first = json.charAt(valStart);
conn.disconnect(); if (first == '"') {
return result; // FIX: Expliziter Escape-Flag statt Vorgänger-Char-Vergleich
} int end = valStart + 1;
boolean escaped = false;
private HttpURLConnection openConnection(String urlStr, String method, String authorization) throws Exception { while (end < json.length()) {
HttpURLConnection conn = (HttpURLConnection) new URL(urlStr).openConnection(); char ch = json.charAt(end);
conn.setRequestMethod(method); if (escaped) {
conn.setConnectTimeout(5000); escaped = false;
conn.setReadTimeout(8000); } else if (ch == '\\') {
conn.setRequestProperty("Content-Type", "application/json"); escaped = true;
conn.setRequestProperty("User-Agent", "StatusAPI-ChatModule/1.0"); } else if (ch == '"') {
if (authorization != null && !authorization.isEmpty()) { break;
conn.setRequestProperty("Authorization", authorization); }
} end++;
return conn; }
} return json.substring(valStart + 1, end);
} else {
private String readStream(InputStream in) throws IOException { int end = valStart;
if (in == null) return ""; while (end < json.length() && ",}\n".indexOf(json.charAt(end)) < 0) end++;
try (BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) { return json.substring(valStart, end).trim();
StringBuilder sb = new StringBuilder(); }
String line; }
while ((line = br.readLine()) != null) sb.append(line);
return sb.toString(); private String extractJsonObject(String json, int fromIndex) {
} int depth = 0, start = -1;
} for (int i = fromIndex; i < json.length(); i++) {
char c = json.charAt(i);
// ===== JSON Mini-Parser ===== if (c == '{') { if (depth++ == 0) start = i; }
else if (c == '}') { if (--depth == 0 && start >= 0) return json.substring(start, i + 1); }
/** Repräsentiert eine Discord-Nachricht (minimale Felder). */ }
private static class DiscordMessage { return "";
long id; }
String authorId = "";
String authorName = ""; private static String escapeJson(String s) {
String content = ""; if (s == null) return "";
boolean isBot = false; return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t");
} }
/** private static String unescapeJson(String s) {
* Parst ein JSON-Array von Discord-Nachrichten ohne externe Bibliothek. if (s == null) return "";
* Nur die benötigten Felder werden extrahiert. return s.replace("\\\"", "\"").replace("\\n", "\n").replace("\\r", "\r").replace("\\\\", "\\");
*/ }
private java.util.List<DiscordMessage> parseMessages(String json) { }
java.util.List<DiscordMessage> result = new java.util.ArrayList<>();
// Jedes Objekt im Array extrahieren
int depth = 0, start = -1;
for (int i = 0; i < json.length(); i++) {
char c = json.charAt(i);
if (c == '{') { if (depth++ == 0) start = i; }
else if (c == '}') {
if (--depth == 0 && start != -1) {
String obj = json.substring(start, i + 1);
DiscordMessage msg = parseMessage(obj);
if (msg != null) result.add(msg);
start = -1;
}
}
}
return result;
}
private DiscordMessage parseMessage(String obj) {
try {
DiscordMessage msg = new DiscordMessage();
msg.id = Long.parseLong(extractJsonString(obj, "\"id\""));
msg.content = unescapeJson(extractJsonString(obj, "\"content\""));
// Webhook-Nachrichten herausfiltern (Echo-Loop verhindern):
// Nachrichten die via Webhook gesendet wurden haben "webhook_id" gesetzt.
// Das sind unsere eigenen Minecraft→Discord Nachrichten die wir ignorieren.
String webhookId = extractJsonString(obj, "\"webhook_id\"");
if (!webhookId.isEmpty()) {
msg.isBot = true; // Als Bot markieren → wird übersprungen
return msg;
}
// Author-Block
int authStart = obj.indexOf("\"author\"");
if (authStart >= 0) {
String authBlock = extractJsonObject(obj, authStart);
msg.authorId = extractJsonString(authBlock, "\"id\"");
msg.authorName = unescapeJson(extractJsonString(authBlock, "\"username\""));
String botFlag = extractJsonString(authBlock, "\"bot\"");
msg.isBot = "true".equals(botFlag);
}
return msg;
} catch (Exception e) {
return null;
}
}
private String extractJsonString(String json, String key) {
int keyIdx = json.indexOf(key);
if (keyIdx < 0) return "";
int colon = json.indexOf(':', keyIdx + key.length());
if (colon < 0) return "";
// Wert direkt nach dem Doppelpunkt
int valStart = colon + 1;
while (valStart < json.length() && json.charAt(valStart) == ' ') valStart++;
if (valStart >= json.length()) return "";
char first = json.charAt(valStart);
if (first == '"') {
// String-Wert
int end = valStart + 1;
while (end < json.length()) {
if (json.charAt(end) == '"' && json.charAt(end - 1) != '\\') break;
end++;
}
return json.substring(valStart + 1, end);
} else {
// Primitiver Wert (Zahl, Boolean)
int end = valStart;
while (end < json.length() && ",}\n".indexOf(json.charAt(end)) < 0) end++;
return json.substring(valStart, end).trim();
}
}
private String extractJsonObject(String json, int fromIndex) {
int depth = 0, start = -1;
for (int i = fromIndex; i < json.length(); i++) {
char c = json.charAt(i);
if (c == '{') { if (depth++ == 0) start = i; }
else if (c == '}') { if (--depth == 0 && start >= 0) return json.substring(start, i + 1); }
}
return "";
}
private static String escapeJson(String s) {
if (s == null) return "";
return s.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
}
private static String unescapeJson(String s) {
if (s == null) return "";
return s.replace("\\\"", "\"")
.replace("\\n", "\n")
.replace("\\r", "\r")
.replace("\\\\", "\\");
}
}

View File

@@ -26,153 +26,92 @@ import java.util.concurrent.TimeUnit;
/** /**
* ForumBridgeModule: Verbindet das WP Business Forum mit dem Minecraft-Server. * ForumBridgeModule: Verbindet das WP Business Forum mit dem Minecraft-Server.
* *
* HTTP-Endpoints (werden vom StatusAPI WebServer geroutet): * Fix #13: extractJsonString() gibt jetzt immer einen leeren String statt null zurück.
* POST /forum/notify — WordPress pusht Benachrichtigung * Alle Aufrufer müssen nicht mehr auf null prüfen, was NullPointerExceptions verhindert.
* POST /forum/unlink — WordPress informiert über Verknüpfungslösung
* GET /forum/status — Verbindungstest
*
* Commands:
* /forumlink <token> — Account mit Forum verknüpfen
* /forum — Ausstehende Benachrichtigungen anzeigen
*/ */
public class ForumBridgeModule implements Module, Listener { public class ForumBridgeModule implements Module, Listener {
private Plugin plugin; private Plugin plugin;
private ForumNotifStorage storage; private ForumNotifStorage storage;
// Konfiguration aus verify.properties
private boolean enabled = true; private boolean enabled = true;
private String wpBaseUrl = ""; private String wpBaseUrl = "";
private String apiSecret = ""; private String apiSecret = "";
private int loginDelaySeconds = 3; private int loginDelaySeconds = 3;
@Override @Override
public String getName() { public String getName() { return "ForumBridgeModule"; }
return "ForumBridgeModule";
}
@Override @Override
public void onEnable(Plugin plugin) { public void onEnable(Plugin plugin) {
this.plugin = plugin; this.plugin = plugin;
// Config laden
loadConfig(plugin); loadConfig(plugin);
if (!enabled) { plugin.getLogger().info("ForumBridgeModule ist deaktiviert."); return; }
if (!enabled) {
plugin.getLogger().info("ForumBridgeModule ist deaktiviert.");
return;
}
// Storage initialisieren und laden
storage = new ForumNotifStorage(plugin.getDataFolder(), plugin.getLogger()); storage = new ForumNotifStorage(plugin.getDataFolder(), plugin.getLogger());
storage.load(); storage.load();
// Event Listener registrieren
plugin.getProxy().getPluginManager().registerListener(plugin, this); plugin.getProxy().getPluginManager().registerListener(plugin, this);
// Commands registrieren
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new ForumLinkCommand()); ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new ForumLinkCommand());
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new ForumCommand()); ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new ForumCommand());
// Auto-Save alle 10 Minuten
plugin.getProxy().getScheduler().schedule(plugin, () -> { plugin.getProxy().getScheduler().schedule(plugin, () -> {
try { try { storage.save(); } catch (Exception e) { plugin.getLogger().warning("ForumBridge Auto-Save Fehler: " + e.getMessage()); }
storage.save();
} catch (Exception e) {
plugin.getLogger().warning("ForumBridge Auto-Save Fehler: " + e.getMessage());
}
}, 10, 10, TimeUnit.MINUTES); }, 10, 10, TimeUnit.MINUTES);
// Alte Benachrichtigungen aufräumen (täglich, max 30 Tage) plugin.getProxy().getScheduler().schedule(plugin, () -> storage.purgeOld(30), 1, 24, TimeUnit.HOURS);
plugin.getProxy().getScheduler().schedule(plugin, () -> {
storage.purgeOld(30);
}, 1, 24, TimeUnit.HOURS);
plugin.getLogger().info("ForumBridgeModule aktiviert."); plugin.getLogger().info("ForumBridgeModule aktiviert.");
} }
@Override @Override
public void onDisable(Plugin plugin) { public void onDisable(Plugin plugin) {
if (storage != null) { if (storage != null) { storage.save(); plugin.getLogger().info("Forum-Benachrichtigungen gespeichert."); }
storage.save();
plugin.getLogger().info("Forum-Benachrichtigungen gespeichert.");
}
} }
// ===== CONFIG =====
private void loadConfig(Plugin plugin) { private void loadConfig(Plugin plugin) {
try { try {
Properties props = new Properties(); Properties props = new Properties();
File configFile = new File(plugin.getDataFolder(), "verify.properties"); File configFile = new File(plugin.getDataFolder(), "verify.properties");
if (configFile.exists()) { if (configFile.exists()) {
try (FileInputStream fis = new FileInputStream(configFile)) { try (FileInputStream fis = new FileInputStream(configFile)) { props.load(fis); }
props.load(fis);
}
} }
this.enabled = !"false".equalsIgnoreCase(props.getProperty("forum.enabled", "true")); this.enabled = !"false".equalsIgnoreCase(props.getProperty("forum.enabled", "true"));
this.wpBaseUrl = props.getProperty("forum.wp_url", props.getProperty("wp_verify_url", "")); this.wpBaseUrl = props.getProperty("forum.wp_url", props.getProperty("wp_verify_url", ""));
this.apiSecret = props.getProperty("forum.api_secret", ""); this.apiSecret = props.getProperty("forum.api_secret", "");
this.loginDelaySeconds = parseInt(props.getProperty("forum.login_delay_seconds", "3"), 3); this.loginDelaySeconds = parseInt(props.getProperty("forum.login_delay_seconds", "3"), 3);
} catch (Exception e) { } catch (Exception e) {
plugin.getLogger().warning("Fehler beim Laden der ForumBridge-Config: " + e.getMessage()); plugin.getLogger().warning("Fehler beim Laden der ForumBridge-Config: " + e.getMessage());
} }
} }
private int parseInt(String s, int def) { private int parseInt(String s, int def) { try { return Integer.parseInt(s); } catch (Exception e) { return def; } }
try { return Integer.parseInt(s); } catch (Exception e) { return def; }
}
// ===== HTTP HANDLER (aufgerufen vom StatusAPI WebServer) ===== // ===== HTTP HANDLER =====
/**
* Verarbeitet POST /forum/notify von WordPress.
* WordPress sendet Benachrichtigungen wenn ein verknüpfter Spieler
* eine neue Antwort, Erwähnung oder PN erhält.
*
* Erwarteter JSON-Body:
* {
* "player_uuid": "uuid-string",
* "type": "reply|mention|message",
* "title": "Thread-Titel oder PN",
* "author": "Absender-Name",
* "url": "Forum-Link",
* "wp_user_id": 123
* }
*/
public String handleNotify(String body, String apiKeyHeader) { public String handleNotify(String body, String apiKeyHeader) {
// API-Key prüfen
if (!apiSecret.isEmpty() && !apiSecret.equals(apiKeyHeader)) { if (!apiSecret.isEmpty() && !apiSecret.equals(apiKeyHeader)) {
return "{\"success\":false,\"error\":\"unauthorized\"}"; return "{\"success\":false,\"error\":\"unauthorized\"}";
} }
// FIX #13: extractJsonString gibt "" statt null → kein NullPointerException möglich
String playerUuid = extractJsonString(body, "player_uuid"); String playerUuid = extractJsonString(body, "player_uuid");
String type = extractJsonString(body, "type"); String type = extractJsonString(body, "type");
String title = extractJsonString(body, "title"); String title = extractJsonString(body, "title");
String author = extractJsonString(body, "author"); String author = extractJsonString(body, "author");
String url = extractJsonString(body, "url"); String url = extractJsonString(body, "url");
if (playerUuid == null || playerUuid.isEmpty()) { if (playerUuid.isEmpty()) return "{\"success\":false,\"error\":\"missing_player_uuid\"}";
return "{\"success\":false,\"error\":\"missing_player_uuid\"}";
}
java.util.UUID uuid; java.util.UUID uuid;
try { try { uuid = java.util.UUID.fromString(playerUuid); }
uuid = java.util.UUID.fromString(playerUuid); catch (Exception e) { return "{\"success\":false,\"error\":\"invalid_uuid\"}"; }
} catch (Exception e) {
return "{\"success\":false,\"error\":\"invalid_uuid\"}";
}
// Fallback: Wenn type 'thread' und title enthält 'Umfrage', dann als 'poll' behandeln if ("thread".equalsIgnoreCase(type) && title.toLowerCase().contains("umfrage")) type = "poll";
if (type != null && type.equalsIgnoreCase("thread") && title != null && title.toLowerCase().contains("umfrage")) { if (type.isEmpty()) type = "reply";
type = "poll";
}
if (type == null || type.isEmpty()) type = "reply";
// Notification erstellen // Alle Werte sind garantiert nicht null (extractJsonString gibt "" zurück)
ForumNotification notification = new ForumNotification(uuid, type, title, author, url); ForumNotification notification = new ForumNotification(uuid, type, title, author, url);
// Sofort zustellen wenn online
ProxiedPlayer online = ProxyServer.getInstance().getPlayer(uuid); ProxiedPlayer online = ProxyServer.getInstance().getPlayer(uuid);
if (online != null && online.isConnected()) { if (online != null && online.isConnected()) {
deliverNotification(online, notification); deliverNotification(online, notification);
@@ -180,62 +119,30 @@ public class ForumBridgeModule implements Module, Listener {
return "{\"success\":true,\"delivered\":true}"; return "{\"success\":true,\"delivered\":true}";
} }
// Offline → speichern für späteren Login
storage.add(notification); storage.add(notification);
return "{\"success\":true,\"delivered\":false}"; return "{\"success\":true,\"delivered\":false}";
} }
/**
* Verarbeitet POST /forum/unlink von WordPress.
* Wird aufgerufen wenn ein User seine Verknüpfung im Forum löst.
*/
public String handleUnlink(String body, String apiKeyHeader) { public String handleUnlink(String body, String apiKeyHeader) {
if (!apiSecret.isEmpty() && !apiSecret.equals(apiKeyHeader)) { if (!apiSecret.isEmpty() && !apiSecret.equals(apiKeyHeader)) return "{\"success\":false,\"error\":\"unauthorized\"}";
return "{\"success\":false,\"error\":\"unauthorized\"}";
}
// Aktuell keine lokale Aktion nötig — die Zuordnung liegt in WordPress
return "{\"success\":true}"; return "{\"success\":true}";
} }
/**
* Verarbeitet GET /forum/status — Verbindungstest.
*/
public String handleStatus() { public String handleStatus() {
String version = "unknown"; String version = "unknown";
try { try { if (plugin.getDescription() != null) version = plugin.getDescription().getVersion(); } catch (Exception ignored) {}
if (plugin.getDescription() != null) {
version = plugin.getDescription().getVersion();
}
} catch (Exception ignored) {}
return "{\"success\":true,\"module\":\"ForumBridgeModule\",\"version\":\"" + version + "\"}"; return "{\"success\":true,\"module\":\"ForumBridgeModule\",\"version\":\"" + version + "\"}";
} }
// ===== NOTIFICATION ZUSTELLUNG ===== // ===== NOTIFICATION =====
/**
* Stellt eine einzelne Benachrichtigung an einen Online-Spieler zu.
*/
private void deliverNotification(ProxiedPlayer player, ForumNotification notif) { private void deliverNotification(ProxiedPlayer player, ForumNotification notif) {
String color = notif.getTypeColor(); String color = notif.getTypeColor();
String label = notif.getTypeLabel(); String label = notif.getTypeLabel();
// Trennlinie
player.sendMessage(new TextComponent("§8§m ")); player.sendMessage(new TextComponent("§8§m "));
player.sendMessage(new TextComponent("§6§l✉ Forum §8» " + color + label));
// Hauptnachricht if (!notif.getTitle().isEmpty()) player.sendMessage(new TextComponent("§7 " + notif.getTitle()));
TextComponent header = new TextComponent("§6§l✉ Forum §8» " + color + label); if (!notif.getAuthor().isEmpty()) player.sendMessage(new TextComponent("§7 von §f" + notif.getAuthor()));
player.sendMessage(header);
// Details
if (!notif.getTitle().isEmpty()) {
player.sendMessage(new TextComponent("§7 " + notif.getTitle()));
}
if (!notif.getAuthor().isEmpty()) {
player.sendMessage(new TextComponent("§7 von §f" + notif.getAuthor()));
}
// Klickbarer Link (wenn URL vorhanden)
if (!notif.getUrl().isEmpty()) { if (!notif.getUrl().isEmpty()) {
TextComponent link = new TextComponent("§a ➜ Im Forum ansehen"); TextComponent link = new TextComponent("§a ➜ Im Forum ansehen");
link.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, notif.getUrl())); link.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, notif.getUrl()));
@@ -243,94 +150,56 @@ public class ForumBridgeModule implements Module, Listener {
new ComponentBuilder("§7Klicke um den Beitrag im Forum zu öffnen").create())); new ComponentBuilder("§7Klicke um den Beitrag im Forum zu öffnen").create()));
player.sendMessage(link); player.sendMessage(link);
} }
// Trennlinie
player.sendMessage(new TextComponent("§8§m ")); player.sendMessage(new TextComponent("§8§m "));
} }
/**
* Stellt alle ausstehenden Benachrichtigungen an einen Spieler zu.
* Wird beim Login aufgerufen (mit kurzem Delay).
*/
private void deliverPending(ProxiedPlayer player) { private void deliverPending(ProxiedPlayer player) {
List<ForumNotification> pending = storage.getPending(player.getUniqueId()); List<ForumNotification> pending = storage.getPending(player.getUniqueId());
if (pending.isEmpty()) return; if (pending.isEmpty()) return;
int count = pending.size(); int count = pending.size();
// Zusammenfassung wenn mehr als 3
if (count > 3) { if (count > 3) {
player.sendMessage(new TextComponent("§8§m ")); player.sendMessage(new TextComponent("§8§m "));
player.sendMessage(new TextComponent("§6§l✉ Forum §8» §fDu hast §e" + count + " §fneue Benachrichtigungen!")); player.sendMessage(new TextComponent("§6§l✉ Forum §8» §fDu hast §e" + count + " §fneue Benachrichtigungen!"));
player.sendMessage(new TextComponent("§7 Tippe §e/forum §7um sie anzuzeigen.")); player.sendMessage(new TextComponent("§7 Tippe §e/forum §7um sie anzuzeigen."));
player.sendMessage(new TextComponent("§8§m ")); player.sendMessage(new TextComponent("§8§m "));
} else { } else {
// Einzeln zustellen for (ForumNotification n : pending) deliverNotification(player, n);
for (ForumNotification n : pending) {
deliverNotification(player, n);
}
} }
// Alle als zugestellt markieren und aufräumen
storage.markAllDelivered(player.getUniqueId()); storage.markAllDelivered(player.getUniqueId());
storage.clearDelivered(player.getUniqueId()); storage.clearDelivered(player.getUniqueId());
} }
// ===== EVENTS =====
@EventHandler @EventHandler
public void onJoin(PostLoginEvent e) { public void onJoin(PostLoginEvent e) {
ProxiedPlayer player = e.getPlayer(); ProxiedPlayer player = e.getPlayer();
// Verzögert zustellen damit der Spieler den Server-Wechsel abgeschlossen hat
plugin.getProxy().getScheduler().schedule(plugin, () -> { plugin.getProxy().getScheduler().schedule(plugin, () -> {
if (player.isConnected()) { if (player.isConnected()) deliverPending(player);
deliverPending(player);
}
}, loginDelaySeconds, TimeUnit.SECONDS); }, loginDelaySeconds, TimeUnit.SECONDS);
} }
// ===== COMMANDS ===== // ===== COMMANDS =====
/**
* /forumlink <token> — Verknüpft den MC-Account mit dem Forum.
*/
private class ForumLinkCommand extends Command { private class ForumLinkCommand extends Command {
public ForumLinkCommand() { super("forumlink", null, "fl"); }
public ForumLinkCommand() {
super("forumlink", null, "fl");
}
@Override @Override
public void execute(CommandSender sender, String[] args) { public void execute(CommandSender sender, String[] args) {
if (!(sender instanceof ProxiedPlayer)) { if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(new TextComponent("§cNur Spieler können diesen Befehl benutzen.")); return; }
sender.sendMessage(new TextComponent("§cNur Spieler können diesen Befehl benutzen."));
return;
}
ProxiedPlayer p = (ProxiedPlayer) sender; ProxiedPlayer p = (ProxiedPlayer) sender;
if (args.length != 1) { if (args.length != 1) {
p.sendMessage(new TextComponent("§eBenutzung: §f/forumlink <token>")); p.sendMessage(new TextComponent("§eBenutzung: §f/forumlink <token>"));
p.sendMessage(new TextComponent("§7Den Token erhältst du in deinem Forum-Profil unter §fMinecraft-Verknüpfung§7.")); p.sendMessage(new TextComponent("§7Den Token erhältst du in deinem Forum-Profil unter §fMinecraft-Verknüpfung§7."));
return; return;
} }
String token = args[0].trim().toUpperCase(); String token = args[0].trim().toUpperCase();
if (wpBaseUrl.isEmpty()) { p.sendMessage(new TextComponent("§cForum-Verknüpfung ist nicht konfiguriert.")); return; }
if (wpBaseUrl.isEmpty()) {
p.sendMessage(new TextComponent("§cForum-Verknüpfung ist nicht konfiguriert."));
return;
}
p.sendMessage(new TextComponent("§7Überprüfe Token...")); p.sendMessage(new TextComponent("§7Überprüfe Token..."));
// Asynchron an WordPress senden
plugin.getProxy().getScheduler().runAsync(plugin, () -> { plugin.getProxy().getScheduler().runAsync(plugin, () -> {
try { try {
String endpoint = wpBaseUrl + "/wp-json/mc-bridge/v1/verify-link"; String endpoint = wpBaseUrl + "/wp-json/mc-bridge/v1/verify-link";
String payload = "{\"token\":\"" + escapeJson(token) + "\"," String payload = "{\"token\":\"" + escapeJson(token) + "\","
+ "\"mc_uuid\":\"" + p.getUniqueId().toString() + "\"," + "\"mc_uuid\":\"" + p.getUniqueId() + "\","
+ "\"mc_name\":\"" + escapeJson(p.getName()) + "\"}"; + "\"mc_name\":\"" + escapeJson(p.getName()) + "\"}";
HttpURLConnection conn = (HttpURLConnection) new URL(endpoint).openConnection(); HttpURLConnection conn = (HttpURLConnection) new URL(endpoint).openConnection();
@@ -339,50 +208,32 @@ public class ForumBridgeModule implements Module, Listener {
conn.setConnectTimeout(5000); conn.setConnectTimeout(5000);
conn.setReadTimeout(7000); conn.setReadTimeout(7000);
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
if (!apiSecret.isEmpty()) { if (!apiSecret.isEmpty()) conn.setRequestProperty("X-Api-Key", apiSecret);
conn.setRequestProperty("X-Api-Key", apiSecret);
}
Charset utf8 = Charset.forName("UTF-8"); Charset utf8 = Charset.forName("UTF-8");
try (OutputStream os = conn.getOutputStream()) { try (OutputStream os = conn.getOutputStream()) { os.write(payload.getBytes(utf8)); }
os.write(payload.getBytes(utf8));
}
int code = conn.getResponseCode(); int code = conn.getResponseCode();
String resp; String resp = code >= 200 && code < 300
if (code >= 200 && code < 300) { ? streamToString(conn.getInputStream(), utf8)
resp = streamToString(conn.getInputStream(), utf8); : streamToString(conn.getErrorStream(), utf8);
} else {
resp = streamToString(conn.getErrorStream(), utf8);
}
// Antwort auswerten
if (resp != null && resp.contains("\"success\":true")) { if (resp != null && resp.contains("\"success\":true")) {
String displayName = extractJsonString(resp, "display_name"); String displayName = extractJsonString(resp, "display_name");
String username = extractJsonString(resp, "username"); String username = extractJsonString(resp, "username");
String show = (displayName != null && !displayName.isEmpty()) ? displayName : username; String show = !displayName.isEmpty() ? displayName : username;
p.sendMessage(new TextComponent("§8§m ")); p.sendMessage(new TextComponent("§8§m "));
p.sendMessage(new TextComponent("§a§l✓ §fForum-Account erfolgreich verknüpft!")); p.sendMessage(new TextComponent("§a§l✓ §fForum-Account erfolgreich verknüpft!"));
if (show != null && !show.isEmpty()) { if (!show.isEmpty()) p.sendMessage(new TextComponent("§7 Forum-User: §f" + show));
p.sendMessage(new TextComponent("§7 Forum-User: §f" + show));
}
p.sendMessage(new TextComponent("§7 Du erhältst jetzt Ingame-Benachrichtigungen.")); p.sendMessage(new TextComponent("§7 Du erhältst jetzt Ingame-Benachrichtigungen."));
p.sendMessage(new TextComponent("§8§m ")); p.sendMessage(new TextComponent("§8§m "));
} else { } else {
// Fehlermeldung auslesen String error = extractJsonString(resp, "error");
String error = extractJsonString(resp, "error");
String message = extractJsonString(resp, "message"); String message = extractJsonString(resp, "message");
if ("token_expired".equals(error)) p.sendMessage(new TextComponent("§c✗ Der Token ist abgelaufen."));
if ("token_expired".equals(error)) { else if ("uuid_already_linked".equals(error)) p.sendMessage(new TextComponent("§c✗ " + (!message.isEmpty() ? message : "Diese UUID ist bereits verknüpft.")));
p.sendMessage(new TextComponent("§c✗ Der Token ist abgelaufen. Generiere einen neuen im Forum.")); else if ("invalid_token".equals(error)) p.sendMessage(new TextComponent("§c✗ Ungültiger Token."));
} else if ("uuid_already_linked".equals(error)) { else p.sendMessage(new TextComponent("§c✗ Verknüpfung fehlgeschlagen: " + (!error.isEmpty() ? error : "Unbekannter Fehler")));
p.sendMessage(new TextComponent("§c✗ " + (message != null ? message : "Diese UUID ist bereits verknüpft.")));
} else if ("invalid_token".equals(error)) {
p.sendMessage(new TextComponent("§c✗ Ungültiger Token. Prüfe die Eingabe oder generiere einen neuen."));
} else {
p.sendMessage(new TextComponent("§c✗ Verknüpfung fehlgeschlagen: " + (error != null ? error : "Unbekannter Fehler")));
}
} }
} catch (Exception ex) { } catch (Exception ex) {
p.sendMessage(new TextComponent("§c✗ Fehler bei der Verbindung zum Forum.")); p.sendMessage(new TextComponent("§c✗ Fehler bei der Verbindung zum Forum."));
@@ -392,34 +243,21 @@ public class ForumBridgeModule implements Module, Listener {
} }
} }
/**
* /forum — Zeigt ausstehende Forum-Benachrichtigungen an.
*/
private class ForumCommand extends Command { private class ForumCommand extends Command {
public ForumCommand() { super("forum"); }
public ForumCommand() {
super("forum");
}
@Override @Override
public void execute(CommandSender sender, String[] args) { public void execute(CommandSender sender, String[] args) {
if (!(sender instanceof ProxiedPlayer)) { if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(new TextComponent("§cNur Spieler können diesen Befehl benutzen.")); return; }
sender.sendMessage(new TextComponent("§cNur Spieler können diesen Befehl benutzen."));
return;
}
ProxiedPlayer p = (ProxiedPlayer) sender; ProxiedPlayer p = (ProxiedPlayer) sender;
List<ForumNotification> pending = storage.getPending(p.getUniqueId()); List<ForumNotification> pending = storage.getPending(p.getUniqueId());
if (pending.isEmpty()) { if (pending.isEmpty()) {
p.sendMessage(new TextComponent("§7Keine neuen Forum-Benachrichtigungen.")); p.sendMessage(new TextComponent("§7Keine neuen Forum-Benachrichtigungen."));
// Forum-Link anzeigen wenn konfiguriert
if (!wpBaseUrl.isEmpty()) { if (!wpBaseUrl.isEmpty()) {
TextComponent link = new TextComponent("§a➜ Forum öffnen"); TextComponent link = new TextComponent("§a➜ Forum öffnen");
link.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, wpBaseUrl)); link.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, wpBaseUrl));
link.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, link.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("§7Klicke um das Forum zu öffnen").create()));
new ComponentBuilder("§7Klicke um das Forum zu öffnen").create()));
p.sendMessage(link); p.sendMessage(link);
} }
return; return;
@@ -428,39 +266,22 @@ public class ForumBridgeModule implements Module, Listener {
p.sendMessage(new TextComponent("§8§m ")); p.sendMessage(new TextComponent("§8§m "));
p.sendMessage(new TextComponent("§6§l✉ Forum-Benachrichtigungen §8(§f" + pending.size() + "§8)")); p.sendMessage(new TextComponent("§6§l✉ Forum-Benachrichtigungen §8(§f" + pending.size() + "§8)"));
p.sendMessage(new TextComponent("")); p.sendMessage(new TextComponent(""));
int shown = 0; int shown = 0;
for (ForumNotification n : pending) { for (ForumNotification n : pending) {
if (shown >= 10) { if (shown >= 10) { p.sendMessage(new TextComponent("§7 ... und " + (pending.size() - 10) + " weitere")); break; }
p.sendMessage(new TextComponent("§7 ... und " + (pending.size() - 10) + " weitere"));
break;
}
String color = n.getTypeColor(); String color = n.getTypeColor();
TextComponent line = new TextComponent(color + "" + n.getTypeLabel() + "§7: "); TextComponent line = new TextComponent(color + "" + n.getTypeLabel() + "§7: ");
TextComponent detail = new TextComponent(!n.getTitle().isEmpty() ? "§f" + n.getTitle() : "§fvon " + n.getAuthor());
TextComponent detail;
if (!n.getTitle().isEmpty()) {
detail = new TextComponent("§f" + n.getTitle());
} else {
detail = new TextComponent("§fvon " + n.getAuthor());
}
if (!n.getUrl().isEmpty()) { if (!n.getUrl().isEmpty()) {
detail.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, n.getUrl())); detail.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, n.getUrl()));
detail.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, detail.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("§7Klicke zum Öffnen").create()));
new ComponentBuilder("§7Klicke zum Öffnen").create()));
} }
line.addExtra(detail); line.addExtra(detail);
p.sendMessage(line); p.sendMessage(line);
shown++; shown++;
} }
p.sendMessage(new TextComponent("")); p.sendMessage(new TextComponent(""));
p.sendMessage(new TextComponent("§8§m ")); p.sendMessage(new TextComponent("§8§m "));
// Alle als gelesen markieren
storage.markAllDelivered(p.getUniqueId()); storage.markAllDelivered(p.getUniqueId());
storage.clearDelivered(p.getUniqueId()); storage.clearDelivered(p.getUniqueId());
} }
@@ -468,21 +289,22 @@ public class ForumBridgeModule implements Module, Listener {
// ===== HELPER ===== // ===== HELPER =====
/** Getter für den Storage (für StatusAPI HTTP-Handler) */ public ForumNotifStorage getStorage() { return storage; }
public ForumNotifStorage getStorage() {
return storage;
}
/**
* FIX #13: Gibt immer einen leeren String zurück, niemals null.
* Verhindert NullPointerExceptions in allen Aufrufern.
*/
private static String extractJsonString(String json, String key) { private static String extractJsonString(String json, String key) {
if (json == null || key == null) return null; if (json == null || key == null) return "";
String search = "\"" + key + "\""; String search = "\"" + key + "\"";
int idx = json.indexOf(search); int idx = json.indexOf(search);
if (idx < 0) return null; if (idx < 0) return "";
int colon = json.indexOf(':', idx + search.length()); int colon = json.indexOf(':', idx + search.length());
if (colon < 0) return null; if (colon < 0) return "";
int i = colon + 1; int i = colon + 1;
while (i < json.length() && Character.isWhitespace(json.charAt(i))) i++; while (i < json.length() && Character.isWhitespace(json.charAt(i))) i++;
if (i >= json.length()) return null; if (i >= json.length()) return "";
char c = json.charAt(i); char c = json.charAt(i);
if (c == '"') { if (c == '"') {
i++; i++;
@@ -491,15 +313,11 @@ public class ForumBridgeModule implements Module, Listener {
while (i < json.length()) { while (i < json.length()) {
char ch = json.charAt(i++); char ch = json.charAt(i++);
if (escape) { sb.append(ch); escape = false; } if (escape) { sb.append(ch); escape = false; }
else { else { if (ch == '\\') escape = true; else if (ch == '"') break; else sb.append(ch); }
if (ch == '\\') escape = true;
else if (ch == '"') break;
else sb.append(ch);
}
} }
return sb.toString(); return sb.toString();
} }
return null; return "";
} }
private static String escapeJson(String s) { private static String escapeJson(String s) {
@@ -510,8 +328,7 @@ public class ForumBridgeModule implements Module, Listener {
private static String streamToString(InputStream in, Charset charset) throws IOException { private static String streamToString(InputStream in, Charset charset) throws IOException {
if (in == null) return ""; if (in == null) return "";
try (BufferedReader br = new BufferedReader(new InputStreamReader(in, charset))) { try (BufferedReader br = new BufferedReader(new InputStreamReader(in, charset))) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder(); String line;
String line;
while ((line = br.readLine()) != null) sb.append(line); while ((line = br.readLine()) != null) sb.append(line);
return sb.toString(); return sb.toString();
} }

View File

@@ -82,7 +82,7 @@ public class NetworkInfoModule implements Module {
loadConfig(); loadConfig();
if (!enabled) { if (!enabled) {
plugin.getLogger().info("[NetworkInfoModule] deaktiviert via " + CONFIG_FILE_NAME + " (networkinfo.enabled=false)");
return; return;
} }
@@ -109,7 +109,7 @@ public class NetworkInfoModule implements Module {
alertTask = ProxyServer.getInstance().getScheduler().schedule(plugin, this::evaluateAndSendAlerts, interval, interval, TimeUnit.SECONDS); alertTask = ProxyServer.getInstance().getScheduler().schedule(plugin, this::evaluateAndSendAlerts, interval, interval, TimeUnit.SECONDS);
} }
plugin.getLogger().info("[NetworkInfoModule] aktiviert. commandEnabled=" + commandEnabled + ", includePlayerNames=" + includePlayerNames + ", webhookEnabled=" + webhookEnabled + ", notifyStartStop=" + webhookNotifyStartStop + ", webhookUrlPresent=" + !webhookUrl.isEmpty());
} }
@Override @Override
@@ -514,7 +514,7 @@ public class NetworkInfoModule implements Module {
while ((read = in.read(buffer)) != -1) { while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read); out.write(buffer, 0, read);
} }
plugin.getLogger().info("[NetworkInfoModule] " + CONFIG_FILE_NAME + " wurde erstellt.");
} catch (Exception e) { } catch (Exception e) {
plugin.getLogger().warning("[NetworkInfoModule] Konnte " + CONFIG_FILE_NAME + " nicht erstellen: " + e.getMessage()); plugin.getLogger().warning("[NetworkInfoModule] Konnte " + CONFIG_FILE_NAME + " nicht erstellen: " + e.getMessage());
} }

View File

@@ -0,0 +1,269 @@
package net.viper.status.modules.vanish;
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.TextComponent;
import net.md_5.bungee.api.connection.ProxiedPlayer;
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.VanishProvider;
import java.io.*;
import java.util.Collections;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* VanishModule für StatusAPI (BungeeCord)
*
* Features:
* - /vanish zum Ein-/Ausschalten
* - /vanish <Spieler> für Admin-Vanish anderer Spieler
* - /vanishlist zeigt alle aktuell unsichtbaren Spieler
* - Vanish-Status wird persistent in vanish.dat gespeichert
* - Beim Login wird gespeicherter Status wiederhergestellt
* - Volle Integration mit VanishProvider → ChatModule sieht den Status
*
* Permission:
* - vanish.use → darf vanishen
* - vanish.other → darf andere Spieler vanishen
* - vanish.list → darf /vanishlist nutzen
* - chat.admin.bypass → sieht Vanish-Join/Leave-Meldungen im Chat
*/
public class VanishModule implements Module, Listener {
private static final String PERMISSION = "chat.admin.bypass";
private static final String PERMISSION_OTHER = "chat.admin.bypass";
private static final String PERMISSION_LIST = "chat.admin.bypass";
private Plugin plugin;
// Persistente Vanish-UUIDs (werden in vanish.dat gespeichert)
private final Set<UUID> persistentVanished =
Collections.newSetFromMap(new ConcurrentHashMap<>());
private File dataFile;
@Override
public String getName() {
return "VanishModule";
}
@Override
public void onEnable(Plugin plugin) {
this.plugin = plugin;
this.dataFile = new File(plugin.getDataFolder(), "vanish.dat");
load();
plugin.getProxy().getPluginManager().registerListener(plugin, this);
registerCommands();
}
@Override
public void onDisable(Plugin plugin) {
save();
// Alle als sichtbar markieren beim Shutdown (damit beim nächsten Start
// der VanishProvider sauber ist load() setzt sie beim Login neu)
for (UUID uuid : persistentVanished) {
VanishProvider.setVanished(uuid, false);
}
}
// =========================================================
// EVENTS
// =========================================================
/**
* Beim Login: Wenn der Spieler persistent gevanisht war, sofort
* in den VanishProvider eintragen BEVOR das ChatModule die
* Join-Nachricht nach 2 Sekunden sendet.
*/
@EventHandler(priority = EventPriority.LOWEST)
public void onLogin(PostLoginEvent e) {
ProxiedPlayer player = e.getPlayer();
if (persistentVanished.contains(player.getUniqueId())) {
VanishProvider.setVanished(player.getUniqueId(), true);
// Kurze Bestätigung an den Spieler selbst (nach kurzem Delay damit
// der Client bereit ist)
plugin.getProxy().getScheduler().schedule(plugin, () -> {
if (player.isConnected()) {
player.sendMessage(color("&8[&7Vanish&8] &7Du bist &cUnsichtbar&7."));
}
}, 1, java.util.concurrent.TimeUnit.SECONDS);
}
}
@EventHandler
public void onDisconnect(PlayerDisconnectEvent e) {
// VanishProvider cleanup der Eintrag in persistentVanished bleibt
// erhalten damit der Status beim nächsten Login wiederhergestellt wird
VanishProvider.cleanup(e.getPlayer().getUniqueId());
}
// =========================================================
// COMMANDS
// =========================================================
private void registerCommands() {
// /vanish [spieler]
plugin.getProxy().getPluginManager().registerCommand(plugin,
new Command("vanish", PERMISSION, "v") {
@Override
public void execute(CommandSender sender, String[] args) {
if (args.length == 0) {
// Sich selbst vanishen
if (!(sender instanceof ProxiedPlayer)) {
sender.sendMessage(color("&cNur Spieler!"));
return;
}
toggleVanish((ProxiedPlayer) sender, (ProxiedPlayer) sender);
} else {
// Anderen Spieler vanishen
if (!sender.hasPermission(PERMISSION_OTHER)) {
sender.sendMessage(color("&cDu hast keine Berechtigung für /vanish <Spieler>."));
return;
}
ProxiedPlayer target = ProxyServer.getInstance().getPlayer(args[0]);
if (target == null) {
sender.sendMessage(color("&cSpieler &f" + args[0] + " &cnicht gefunden."));
return;
}
toggleVanish(sender, target);
}
}
});
// /vanishlist
plugin.getProxy().getPluginManager().registerCommand(plugin,
new Command("vanishlist", PERMISSION_LIST, "vlist") {
@Override
public void execute(CommandSender sender, String[] args) {
Set<UUID> vanished = VanishProvider.getVanishedPlayers();
if (vanished.isEmpty()) {
sender.sendMessage(color("&8[Vanish] &7Keine unsichtbaren Spieler."));
return;
}
sender.sendMessage(color("&8[Vanish] &7Unsichtbare Spieler &8(" + vanished.size() + ")&7:"));
for (UUID uuid : vanished) {
ProxiedPlayer p = ProxyServer.getInstance().getPlayer(uuid);
String name = p != null ? p.getName() : uuid.toString().substring(0, 8) + "...";
String online = p != null ? " &8(online)" : " &8(offline/persistent)";
sender.sendMessage(color(" &8- &7" + name + online));
}
}
});
}
// =========================================================
// VANISH-LOGIK
// =========================================================
/**
* Schaltet den Vanish-Status eines Spielers um.
*
* @param executor Der Befehlsgeber (für Feedback-Nachrichten)
* @param target Der betroffene Spieler
*/
private void toggleVanish(CommandSender executor, ProxiedPlayer target) {
boolean nowVanished = !VanishProvider.isVanished(target);
setVanished(target, nowVanished);
String statusMsg = nowVanished
? "&8[&7Vanish&8] &f" + target.getName() + " &7ist jetzt &cUnsichtbar&7."
: "&8[&7Vanish&8] &f" + target.getName() + " &7ist jetzt &aSichtbar&7.";
// Feedback an den Ausführenden
executor.sendMessage(color(statusMsg));
// Falls jemand anderes gevanisht wurde, auch dem Ziel Bescheid geben
if (!executor.equals(target)) {
String selfMsg = nowVanished
? "&8[&7Vanish&8] &7Du wurdest &cUnsichtbar &7gemacht."
: "&8[&7Vanish&8] &7Du wurdest &aSichtbar &7gemacht.";
target.sendMessage(color(selfMsg));
}
// Admins mit chat.admin.bypass informieren (außer dem Ausführenden)
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
if (p.equals(executor) || p.equals(target)) continue;
if (p.hasPermission("chat.admin.bypass")) {
p.sendMessage(color(statusMsg));
}
}
}
/**
* Setzt den Vanish-Status direkt (ohne Toggle).
* Aktualisiert VanishProvider UND die persistente Liste.
*/
public void setVanished(ProxiedPlayer player, boolean vanished) {
VanishProvider.setVanished(player.getUniqueId(), vanished);
if (vanished) {
persistentVanished.add(player.getUniqueId());
} else {
persistentVanished.remove(player.getUniqueId());
}
save();
}
/**
* Öffentliche API für andere Module.
*/
public boolean isVanished(ProxiedPlayer player) {
return VanishProvider.isVanished(player);
}
// =========================================================
// PERSISTENZ
// =========================================================
private void save() {
try (BufferedWriter bw = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(dataFile), "UTF-8"))) {
for (UUID uuid : persistentVanished) {
bw.write(uuid.toString());
bw.newLine();
}
} catch (IOException e) {
plugin.getLogger().warning("[VanishModule] Fehler beim Speichern: " + e.getMessage());
}
}
private void load() {
persistentVanished.clear();
if (!dataFile.exists()) return;
try (BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream(dataFile), "UTF-8"))) {
String line;
while ((line = br.readLine()) != null) {
line = line.trim();
if (line.isEmpty()) continue;
try {
persistentVanished.add(UUID.fromString(line));
} catch (IllegalArgumentException ignored) {}
}
} catch (IOException e) {
plugin.getLogger().warning("[VanishModule] Fehler beim Laden: " + e.getMessage());
}
}
// =========================================================
// HILFSMETHODEN
// =========================================================
private TextComponent color(String text) {
return new TextComponent(ChatColor.translateAlternateColorCodes('&', text));
}
}

View File

@@ -1,248 +1,190 @@
package net.viper.status.modules.verify; package net.viper.status.modules.verify;
import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.CommandSender; import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.ProxyServer; import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.plugin.Command; import net.md_5.bungee.api.plugin.Command;
import net.md_5.bungee.api.plugin.Plugin; import net.md_5.bungee.api.plugin.Plugin;
import net.viper.status.module.Module; import net.viper.status.module.Module;
import javax.crypto.Mac; import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import java.io.*; import java.io.*;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Properties; import java.util.Properties;
/** /**
* VerifyModule: Multi-Server Support. * VerifyModule: Multi-Server Support.
* Liest pro Server die passende ID und das Secret aus der verify.properties. *
*/ * Fix #7: Servernamen werden jetzt case-insensitiv verglichen.
public class VerifyModule implements Module { * Keys in serverConfigs werden beim Laden auf lowercase normalisiert
* und die Suche erfolgt ebenfalls lowercase.
private String wpVerifyUrl; */
// Speichert für jeden Servernamen (z.B. "Lobby") die passende Konfiguration public class VerifyModule implements Module {
private final Map<String, ServerConfig> serverConfigs = new HashMap<>();
private String wpVerifyUrl;
@Override // Keys sind lowercase normalisiert für case-insensitiven Vergleich
public String getName() { private final Map<String, ServerConfig> serverConfigs = new HashMap<>();
return "VerifyModule";
} @Override
public String getName() { return "VerifyModule"; }
@Override
public void onEnable(Plugin plugin) { @Override
loadConfig(plugin); public void onEnable(Plugin plugin) {
loadConfig(plugin);
// Befehl registrieren ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new VerifyCommand());
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new VerifyCommand()); plugin.getLogger().info("VerifyModule aktiviert. " + serverConfigs.size() + " Server-Konfigurationen geladen.");
}
plugin.getLogger().info("VerifyModule aktiviert. " + serverConfigs.size() + " Server-Konfigurationen geladen.");
} @Override
public void onDisable(Plugin plugin) {}
@Override
public void onDisable(Plugin plugin) { private void loadConfig(Plugin plugin) {
// Befehl muss nicht manuell entfernt werden, BungeeCord übernimmt das beim Plugin-Stop String fileName = "verify.properties";
} File configFile = new File(plugin.getDataFolder(), fileName);
Properties props = new Properties();
// --- Konfiguration Laden & Kopieren ---
private void loadConfig(Plugin plugin) { if (!configFile.exists()) {
String fileName = "verify.properties"; plugin.getDataFolder().mkdirs();
File configFile = new File(plugin.getDataFolder(), fileName); try (InputStream in = plugin.getResourceAsStream(fileName);
Properties props = new Properties(); OutputStream out = new FileOutputStream(configFile)) {
if (in == null) { plugin.getLogger().warning("Standard-config '" + fileName + "' nicht in JAR."); return; }
// 1. Datei kopieren, falls sie noch nicht existiert byte[] buffer = new byte[1024]; int length;
if (!configFile.exists()) { while ((length = in.read(buffer)) > 0) out.write(buffer, 0, length);
plugin.getDataFolder().mkdirs(); plugin.getLogger().info("Konfigurationsdatei '" + fileName + "' erstellt.");
try (InputStream in = plugin.getResourceAsStream(fileName); } catch (Exception e) { plugin.getLogger().severe("Fehler beim Erstellen der Config: " + e.getMessage()); return; }
OutputStream out = new FileOutputStream(configFile)) { }
if (in == null) {
plugin.getLogger().warning("Standard-config '" + fileName + "' nicht in JAR gefunden. Erstelle manuell."); try (InputStream in = new FileInputStream(configFile)) {
return; props.load(in);
} } catch (IOException e) { e.printStackTrace(); return; }
byte[] buffer = new byte[1024];
int length; this.wpVerifyUrl = props.getProperty("wp_verify_url", "https://deine-wp-domain.tld");
while ((length = in.read(buffer)) > 0) {
out.write(buffer, 0, length); // FIX #7: Keys beim Laden auf lowercase normalisieren
} this.serverConfigs.clear();
plugin.getLogger().info("Konfigurationsdatei '" + fileName + "' erstellt."); for (String key : props.stringPropertyNames()) {
} catch (Exception e) { if (key.startsWith("server.")) {
plugin.getLogger().severe("Fehler beim Erstellen der Config: " + e.getMessage()); String[] parts = key.split("\\.");
return; if (parts.length == 3) {
} // Servername lowercase → case-insensitiver Lookup
} String serverName = parts[1].toLowerCase();
String type = parts[2];
// 2. Eigentliche Config laden ServerConfig config = serverConfigs.computeIfAbsent(serverName, k -> new ServerConfig());
try (InputStream in = new FileInputStream(configFile)) { if ("id".equalsIgnoreCase(type)) {
props.load(in); try { config.serverId = Integer.parseInt(props.getProperty(key)); }
} catch (IOException e) { catch (NumberFormatException e) { plugin.getLogger().warning("Ungültige Server ID für " + serverName); }
e.printStackTrace(); } else if ("secret".equalsIgnoreCase(type)) {
return; config.sharedSecret = props.getProperty(key);
} }
}
// Globale URL }
this.wpVerifyUrl = props.getProperty("wp_verify_url", "https://deine-wp-domain.tld"); }
}
// Server-Configs parsen (z.B. server.Lobby.id)
this.serverConfigs.clear(); private static class ServerConfig {
for (String key : props.stringPropertyNames()) { int serverId = 0;
if (key.startsWith("server.")) { String sharedSecret = "";
// Key Struktur: server.<ServerName>.id oder .secret }
String[] parts = key.split("\\.");
if (parts.length == 3) { private class VerifyCommand extends Command {
String serverName = parts[1]; public VerifyCommand() { super("verify"); }
String type = parts[2];
@Override
// Eintrag in der Map erstellen oder holen public void execute(CommandSender sender, String[] args) {
ServerConfig config = serverConfigs.computeIfAbsent(serverName, k -> new ServerConfig()); if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(ChatColor.RED + "Nur Spieler können diesen Befehl benutzen."); return; }
ProxiedPlayer p = (ProxiedPlayer) sender;
if ("id".equalsIgnoreCase(type)) { if (args.length != 1) { p.sendMessage(ChatColor.YELLOW + "Benutzung: /verify <token>"); return; }
try {
config.serverId = Integer.parseInt(props.getProperty(key)); // FIX #7: Servername lowercase für case-insensitiven Lookup
} catch (NumberFormatException e) { String serverName = p.getServer().getInfo().getName().toLowerCase();
plugin.getLogger().warning("Ungültige Server ID für " + serverName); ServerConfig config = serverConfigs.get(serverName);
}
} else if ("secret".equalsIgnoreCase(type)) { if (config == null || config.serverId == 0 || config.sharedSecret.isEmpty()) {
config.sharedSecret = props.getProperty(key); p.sendMessage(ChatColor.RED + "✗ Dieser Server ist nicht in der Verify-Konfiguration hinterlegt.");
} p.sendMessage(ChatColor.GRAY + "Aktueller Servername: " + ChatColor.WHITE + p.getServer().getInfo().getName());
} p.sendMessage(ChatColor.GRAY + "Bitte kontaktiere einen Admin.");
} return;
} }
}
String token = args[0].trim();
// --- Hilfsklasse für die Daten eines Servers --- String playerName = p.getName();
private static class ServerConfig {
int serverId = 0; HttpURLConnection conn = null;
String sharedSecret = ""; try {
} Charset utf8 = Charset.forName("UTF-8");
String signature = hmacSHA256(playerName + token, config.sharedSecret, utf8);
// --- Die Command Klasse --- String payload = "{\"player\":\"" + escapeJson(playerName)
private class VerifyCommand extends Command { + "\",\"token\":\"" + escapeJson(token)
+ "\",\"server_id\":" + config.serverId
public VerifyCommand() { + ",\"signature\":\"" + signature + "\"}";
super("verify");
} URL url = new URL(wpVerifyUrl + "/wp-json/mc-gallery/v1/verify");
conn = (HttpURLConnection) url.openConnection();
@Override conn.setConnectTimeout(5000);
public void execute(CommandSender sender, String[] args) { conn.setReadTimeout(7000);
if (!(sender instanceof ProxiedPlayer)) { conn.setDoOutput(true);
sender.sendMessage(ChatColor.RED + "Nur Spieler können diesen Befehl benutzen."); conn.setRequestMethod("POST");
return; conn.setRequestProperty("Content-Type", "application/json; charset=utf-8");
} try (OutputStream os = conn.getOutputStream()) { os.write(payload.getBytes(utf8)); }
ProxiedPlayer p = (ProxiedPlayer) sender; int code = conn.getResponseCode();
if (args.length != 1) { String resp = code >= 200 && code < 300
p.sendMessage(ChatColor.YELLOW + "Benutzung: /verify <token>"); ? streamToString(conn.getInputStream(), utf8)
return; : streamToString(conn.getErrorStream(), utf8);
}
if (resp != null && !resp.isEmpty() && resp.trim().startsWith("{")) {
// --- WICHTIG: Servernamen ermitteln --- boolean isSuccess = resp.contains("\"success\":true");
String serverName = p.getServer().getInfo().getName(); String message = "Ein unbekannter Fehler ist aufgetreten.";
int keyIndex = resp.indexOf("\"message\":\"");
// Konfiguration für diesen Server laden if (keyIndex != -1) {
ServerConfig config = serverConfigs.get(serverName); int startIndex = keyIndex + 11;
int endIndex = resp.indexOf("\"", startIndex);
// Check ob Konfig existiert if (endIndex != -1) message = resp.substring(startIndex, endIndex);
if (config == null || config.serverId == 0 || config.sharedSecret.isEmpty()) { }
p.sendMessage(ChatColor.RED + "✗ Dieser Server ist nicht in der Verify-Konfiguration hinterlegt."); if (isSuccess) {
p.sendMessage(ChatColor.GRAY + "Aktueller Servername: " + ChatColor.WHITE + serverName); p.sendMessage(ChatColor.GREEN + "" + message);
p.sendMessage(ChatColor.GRAY + "Bitte kontaktiere einen Admin."); p.sendMessage(ChatColor.GRAY + "Du kannst nun Bilder hochladen!");
return; } else {
} p.sendMessage(ChatColor.RED + "" + message);
}
String token = args[0].trim(); } else {
String playerName = p.getName(); p.sendMessage(ChatColor.RED + "✗ Fehler beim Verbinden mit der Webseite (Code: " + code + ")");
}
HttpURLConnection conn = null; } catch (Exception ex) {
try { p.sendMessage(ChatColor.RED + "✗ Ein interner Fehler ist aufgetreten.");
Charset utf8 = Charset.forName("UTF-8"); ProxyServer.getInstance().getLogger().warning("Verify error: " + ex.getMessage());
// Wir signieren Name + Token mit dem SERVER-SPECIFISCHEN Secret ex.printStackTrace();
String signature = hmacSHA256(playerName + token, config.sharedSecret, utf8); } finally {
if (conn != null) conn.disconnect();
// Payload aufbauen mit der SERVER-SPECIFISCHEN ID }
String payload = "{\"player\":\"" + escapeJson(playerName) + "\",\"token\":\"" + escapeJson(token) + "\",\"server_id\":" + config.serverId + ",\"signature\":\"" + signature + "\"}"; }
}
URL url = new URL(wpVerifyUrl + "/wp-json/mc-gallery/v1/verify");
conn = (HttpURLConnection) url.openConnection(); private static String hmacSHA256(String data, String key, Charset charset) throws Exception {
conn.setConnectTimeout(5000); Mac mac = Mac.getInstance("HmacSHA256");
conn.setReadTimeout(7000); mac.init(new SecretKeySpec(key.getBytes(charset), "HmacSHA256"));
conn.setDoOutput(true); byte[] raw = mac.doFinal(data.getBytes(charset));
conn.setRequestMethod("POST"); StringBuilder sb = new StringBuilder();
conn.setRequestProperty("Content-Type", "application/json; charset=utf-8"); for (byte b : raw) sb.append(String.format("%02x", b));
return sb.toString();
try (OutputStream os = conn.getOutputStream()) { }
os.write(payload.getBytes(utf8));
} private static String streamToString(InputStream in, Charset charset) throws IOException {
if (in == null) return "";
int code = conn.getResponseCode(); try (BufferedReader br = new BufferedReader(new InputStreamReader(in, charset))) {
String resp; StringBuilder sb = new StringBuilder(); String line;
while ((line = br.readLine()) != null) sb.append(line);
if (code >= 200 && code < 300) { return sb.toString();
resp = streamToString(conn.getInputStream(), utf8); }
} else { }
resp = streamToString(conn.getErrorStream(), utf8);
} private static String escapeJson(String s) {
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r");
// Antwort verarbeiten }
if (resp != null && !resp.isEmpty() && resp.trim().startsWith("{")) { }
boolean isSuccess = resp.contains("\"success\":true");
String message = "Ein unbekannter Fehler ist aufgetreten.";
int keyIndex = resp.indexOf("\"message\":\"");
if (keyIndex != -1) {
int startIndex = keyIndex + 11;
int endIndex = resp.indexOf("\"", startIndex);
if (endIndex != -1) {
message = resp.substring(startIndex, endIndex);
}
}
if (isSuccess) {
p.sendMessage(ChatColor.GREEN + "" + message);
p.sendMessage(ChatColor.GRAY + "Du kannst nun Bilder hochladen!");
} else {
p.sendMessage(ChatColor.RED + "" + message);
}
} else {
p.sendMessage(ChatColor.RED + "✗ Fehler beim Verbinden mit der Webseite (Code: " + code + ")");
}
} catch (Exception ex) {
p.sendMessage(ChatColor.RED + "✗ Ein interner Fehler ist aufgetreten.");
ProxyServer.getInstance().getLogger().warning("Verify error: " + ex.getMessage());
ex.printStackTrace();
} finally {
if (conn != null) {
conn.disconnect();
}
}
}
}
// --- Helper Methoden ---
private static String hmacSHA256(String data, String key, Charset charset) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key.getBytes(charset), "HmacSHA256"));
byte[] raw = mac.doFinal(data.getBytes(charset));
StringBuilder sb = new StringBuilder();
for (byte b : raw) sb.append(String.format("%02x", b));
return sb.toString();
}
private static String streamToString(InputStream in, Charset charset) throws IOException {
if (in == null) return "";
try (BufferedReader br = new BufferedReader(new InputStreamReader(in, charset))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) sb.append(line);
return sb.toString();
}
}
private static String escapeJson(String s) {
return s.replace("\\", "\\\\").replace("\"","\\\"").replace("\n","\\n").replace("\r","\\r");
}
}

View File

@@ -11,6 +11,20 @@ public class PlayerStats {
public long currentSessionStart; public long currentSessionStart;
public int joins; public int joins;
// Economy
public double balance;
public double totalEarned;
public double totalSpent;
public int transactionsCount;
// Punishments
public int bansCount;
public int mutesCount;
public int warnsCount;
public long lastPunishmentAt; // Unix-Timestamp (Sek.), 0 = nie
public String lastPunishmentType; // "ban", "mute", "warn", "kick", "" = nie
public int punishmentScore;
public PlayerStats(UUID uuid, String name) { public PlayerStats(UUID uuid, String name) {
this.uuid = uuid; this.uuid = uuid;
this.name = name; this.name = name;
@@ -20,6 +34,16 @@ public class PlayerStats {
this.totalPlaytime = 0; this.totalPlaytime = 0;
this.currentSessionStart = 0; this.currentSessionStart = 0;
this.joins = 0; this.joins = 0;
this.balance = 0.0;
this.totalEarned = 0.0;
this.totalSpent = 0.0;
this.transactionsCount = 0;
this.bansCount = 0;
this.mutesCount = 0;
this.warnsCount = 0;
this.lastPunishmentAt = 0;
this.lastPunishmentType = "";
this.punishmentScore = 0;
} }
public synchronized void onJoin() { public synchronized void onJoin() {
@@ -47,7 +71,10 @@ public class PlayerStats {
} }
public synchronized String toLine() { public synchronized String toLine() {
return uuid + "|" + name.replace("|", "_") + "|" + firstSeen + "|" + lastSeen + "|" + totalPlaytime + "|" + currentSessionStart + "|" + joins; String safeType = (lastPunishmentType == null ? "" : lastPunishmentType).replace("|", "_");
return uuid + "|" + name.replace("|", "_") + "|" + firstSeen + "|" + lastSeen + "|" + totalPlaytime + "|" + currentSessionStart + "|" + joins
+ "|" + balance + "|" + totalEarned + "|" + totalSpent + "|" + transactionsCount
+ "|" + bansCount + "|" + mutesCount + "|" + warnsCount + "|" + lastPunishmentAt + "|" + safeType + "|" + punishmentScore;
} }
public static PlayerStats fromLine(String line) { public static PlayerStats fromLine(String line) {
@@ -62,6 +89,22 @@ public class PlayerStats {
ps.totalPlaytime = Long.parseLong(parts[4]); ps.totalPlaytime = Long.parseLong(parts[4]);
ps.currentSessionStart = Long.parseLong(parts[5]); ps.currentSessionStart = Long.parseLong(parts[5]);
ps.joins = Integer.parseInt(parts[6]); ps.joins = Integer.parseInt(parts[6]);
// Economy (felder 7-10)
if (parts.length >= 11) {
try { ps.balance = Double.parseDouble(parts[7]); } catch (Exception ignored) {}
try { ps.totalEarned = Double.parseDouble(parts[8]); } catch (Exception ignored) {}
try { ps.totalSpent = Double.parseDouble(parts[9]); } catch (Exception ignored) {}
try { ps.transactionsCount = Integer.parseInt(parts[10]); } catch (Exception ignored) {}
}
// Punishments (felder 11-16)
if (parts.length >= 17) {
try { ps.bansCount = Integer.parseInt(parts[11]); } catch (Exception ignored) {}
try { ps.mutesCount = Integer.parseInt(parts[12]); } catch (Exception ignored) {}
try { ps.warnsCount = Integer.parseInt(parts[13]); } catch (Exception ignored) {}
try { ps.lastPunishmentAt = Long.parseLong(parts[14]); } catch (Exception ignored) {}
ps.lastPunishmentType = parts[15];
try { ps.punishmentScore = Integer.parseInt(parts[16]); } catch (Exception ignored) {}
}
return ps; return ps;
} catch (Exception e) { } catch (Exception e) {
return null; return null;

View File

@@ -32,7 +32,6 @@ public class StatsModule implements Module, Listener {
// Laden // Laden
try { try {
storage.load(manager); storage.load(manager);
plugin.getLogger().info("Player-Stats wurden erfolgreich geladen.");
} catch (Exception e) { } catch (Exception e) {
plugin.getLogger().warning("Fehler beim Laden der Stats: " + e.getMessage()); plugin.getLogger().warning("Fehler beim Laden der Stats: " + e.getMessage());
} }
@@ -44,7 +43,6 @@ public class StatsModule implements Module, Listener {
plugin.getProxy().getScheduler().schedule(plugin, () -> { plugin.getProxy().getScheduler().schedule(plugin, () -> {
try { try {
storage.save(manager); storage.save(manager);
plugin.getLogger().info("Auto-Save: Player-Stats gespeichert.");
} catch (Exception e) { } catch (Exception e) {
plugin.getLogger().warning("Fehler beim Auto-Save: " + e.getMessage()); plugin.getLogger().warning("Fehler beim Auto-Save: " + e.getMessage());
} }
@@ -69,7 +67,6 @@ public class StatsModule implements Module, Listener {
} }
try { try {
storage.save(manager); storage.save(manager);
plugin.getLogger().info("Player-Stats beim Shutdown gespeichert.");
} catch (Exception e) { } catch (Exception e) {
plugin.getLogger().warning("Fehler beim Speichern (Shutdown): " + e.getMessage()); plugin.getLogger().warning("Fehler beim Speichern (Shutdown): " + e.getMessage());
} }

View File

@@ -1,37 +1,46 @@
package net.viper.status.stats; package net.viper.status.stats;
import java.io.*; import java.io.*;
public class StatsStorage { /**
private final File file; * Fix #9: save() und load() sind jetzt synchronized um Race Conditions
* zwischen Auto-Save-Task und Shutdown-Aufruf zu verhindern.
public StatsStorage(File pluginFolder) { */
if (!pluginFolder.exists()) pluginFolder.mkdirs(); public class StatsStorage {
this.file = new File(pluginFolder, "stats.dat"); private final File file;
} private final Object fileLock = new Object();
public void save(StatsManager manager) { public StatsStorage(File pluginFolder) {
try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) { if (!pluginFolder.exists()) pluginFolder.mkdirs();
for (PlayerStats ps : manager.all()) { this.file = new File(pluginFolder, "stats.dat");
bw.write(ps.toLine()); }
bw.newLine();
} public void save(StatsManager manager) {
bw.flush(); synchronized (fileLock) {
} catch (IOException e) { try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
e.printStackTrace(); for (PlayerStats ps : manager.all()) {
} bw.write(ps.toLine());
} bw.newLine();
}
public void load(StatsManager manager) { bw.flush();
if (!file.exists()) return; } catch (IOException e) {
try (BufferedReader br = new BufferedReader(new FileReader(file))) { e.printStackTrace();
String line; }
while ((line = br.readLine()) != null) { }
PlayerStats ps = PlayerStats.fromLine(line); }
if (ps != null) manager.put(ps);
} public void load(StatsManager manager) {
} catch (IOException e) { if (!file.exists()) return;
e.printStackTrace(); synchronized (fileLock) {
} try (BufferedReader br = new BufferedReader(new FileReader(file))) {
} String line;
} while ((line = br.readLine()) != null) {
PlayerStats ps = PlayerStats.fromLine(line);
if (ps != null) manager.put(ps);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

View File

@@ -135,6 +135,30 @@ private-messages:
format-social-spy: "&8[&dSPY &7{sender} &8→ &7{receiver}&8] &f{message}" format-social-spy: "&8[&dSPY &7{sender} &8→ &7{receiver}&8] &f{message}"
social-spy-permission: "chat.socialspy" social-spy-permission: "chat.socialspy"
# ============================================================
# JOIN / LEAVE NACHRICHTEN
# Platzhalter:
# {player} - Spielername
# {prefix} - LuckPerms Prefix
# {suffix} - LuckPerms Suffix
# {server} - Zuletzt bekannter Server (bei Leave) oder "Netzwerk"
# ============================================================
join-leave:
enabled: true
# Normale Join/Leave-Nachrichten (für alle sichtbar)
join-format: "&8[&a+&8] {prefix}&a{player}&r &7hat das Netzwerk betreten."
leave-format: "&8[&c-&8] {prefix}&c{player}&r &7hat das Netzwerk verlassen."
# Vanish: Unsichtbare Spieler erzeugen keine normalen Join/Leave-Meldungen.
# Ist vanish-show-to-admins true, sehen Admins mit bypass-permission eine
# abweichende, dezente Benachrichtigung.
vanish-show-to-admins: true
vanish-join-format: "&8[&7+&8] &8{player} &7hat das Netzwerk betreten. &8(Vanish)"
vanish-leave-format: "&8[&7-&8] &8{player} &7hat das Netzwerk verlassen. &8(Vanish)"
# Brücken-Weitergabe (leer = deaktiviert)
discord-webhook: ""
telegram-chat-id: ""
telegram-thread-id: 0
# ============================================================ # ============================================================
# GLOBALES RATE-LIMIT-FRAMEWORK # GLOBALES RATE-LIMIT-FRAMEWORK
# Zentraler Schutz für Chat/PM/Command-Flood. # Zentraler Schutz für Chat/PM/Command-Flood.
@@ -304,6 +328,36 @@ chat-filter:
min-length: 6 min-length: 6
max-percent: 70 max-percent: 70
anti-ad:
enabled: true
message: "&cWerbung ist in diesem Chat nicht erlaubt!"
# Domains/Substrings die NICHT geblockt werden (z.B. eigene Serveradresse)
# Vergleich ist case-insensitiv und prüft ob der Substring im Match enthalten ist
whitelist:
- "viper-network.de"
- "m-viper.de"
- "https://www.spigotmc.org"
# TLDs die als Werbung gewertet werden.
# Leer = alle Domain-Treffer blockieren (nicht empfohlen, hohe False-Positive-Rate)
blocked-tlds:
- "net"
- "com"
- "de"
- "org"
- "gg"
- "io"
- "eu"
- "tv"
- "xyz"
- "info"
- "me"
- "cc"
- "co"
- "app"
- "online"
- "site"
- "fun"
# ============================================================ # ============================================================
# MENTIONS (@Spielername) # MENTIONS (@Spielername)
# ============================================================ # ============================================================

View File

@@ -0,0 +1,16 @@
# ============================================================
# StatusAPI - ChatModule Wort-Blacklist
# Wörter werden case-insensitiv und als Teilwort geprüft.
# Erkannte Wörter werden durch **** ersetzt.
#
# Diese Datei wird bei /chatreload automatisch neu eingelesen.
# Wörter die hier stehen ÜBERSCHREIBEN NICHT die Einträge in
# chat.yml → beide Listen werden zusammengeführt.
# ============================================================
words:
- beispielwort1
- beispielwort2
# Hier eigene Wörter eintragen, eines pro Zeile:
# - schimpfwort
# - spam

View File

@@ -1,7 +1,265 @@
name: StatusAPIBridge name: StatusAPI
version: 1.0.0 main: net.viper.status.StatusAPI
main: net.viper.statusapibridge.StatusAPIBridge version: 4.1.0
api-version: 1.21 author: M_Viper
description: Sendet Vault-Economy-Daten an die BungeeCord StatusAPI description: StatusAPI für BungeeCord inkl. Update-Checker, Modul-System und ChatModule
authors: [Viper] # Mindestanforderung: Minecraft 1.20 / BungeeCord mit PlayerChatEvent-Unterstützung
softdepend: [Vault]
softdepend:
- LuckPerms
- Geyser-BungeeCord
commands:
# ── VanishModule ──────────────────────────────────────────
vanish:
description: Vanish ein-/ausschalten
usage: /vanish [Spieler]
aliases: [v]
vanishlist:
description: Alle unsichtbaren Spieler anzeigen
usage: /vanishlist
aliases: [vlist]
# ── Verify Modul ──────────────────────────────────────────
verify:
description: Verifiziere dich mit einem Token
usage: /verify <token>
# ── ForumBridge Modul ─────────────────────────────────────
forumlink:
description: Verknüpfe deinen Minecraft-Account mit dem Forum
usage: /forumlink <token>
aliases: [fl]
forum:
description: Zeigt ausstehende Forum-Benachrichtigungen an
usage: /forum
# ── NetworkInfo Modul ─────────────────────────────────────
netinfo:
description: Zeigt erweiterte Proxy- und Systeminfos an
usage: /netinfo
antibot:
description: Zeigt AntiBot-Status und Verwaltung
usage: /antibot <status|clearblocks|unblock|profile|reload>
# ── AutoMessage Modul ─────────────────────────────────────
automessage:
description: AutoMessage Verwaltung
usage: /automessage reload
# ── ChatModule Kanal ────────────────────────────────────
channel:
description: Kanal wechseln oder Kanalliste anzeigen
usage: /channel [kanalname]
aliases: [ch, kanal]
# ── ChatModule HelpOp ───────────────────────────────────
helpop:
description: Sende eine Hilfeanfrage an das Team
usage: /helpop <Nachricht>
# ── ChatModule Privat-Nachrichten ───────────────────────
msg:
description: Sende eine private Nachricht
usage: /msg <Spieler> <Nachricht>
aliases: [tell, w, whisper]
r:
description: Antworte auf die letzte private Nachricht
usage: /r <Nachricht>
aliases: [reply, antwort]
# ── ChatModule Blockieren ───────────────────────────────
ignore:
description: Spieler ignorieren
usage: /ignore <Spieler>
aliases: [block]
unignore:
description: Spieler nicht mehr ignorieren
usage: /unignore <Spieler>
aliases: [unblock]
# ── ChatModule Mute (Admin) ─────────────────────────────
chatmute:
description: Spieler im Chat stumm schalten
usage: /chatmute <Spieler> [Minuten]
aliases: [gmute]
chatunmute:
description: Chat-Stummschaltung aufheben
usage: /chatunmute <Spieler>
aliases: [gunmute]
# ── ChatModule Selbst-Mute ──────────────────────────────
chataus:
description: Eigenen Chat-Empfang ein-/ausschalten
usage: /chataus
aliases: [togglechat, chaton, chatoff]
# ── ChatModule Broadcast ────────────────────────────────
broadcast:
description: Nachricht an alle Spieler senden
usage: /broadcast <Nachricht>
aliases: [bc, alert]
# ── ChatModule Emoji ────────────────────────────────────
emoji:
description: Liste aller verfügbaren Emojis
usage: /emoji
aliases: [emojis]
# ── ChatModule Social Spy ───────────────────────────────
socialspy:
description: Private Nachrichten mitlesen (Admin)
usage: /socialspy
aliases: [spy]
# ── ChatModule Reload ───────────────────────────────────
chatreload:
description: Chat-Konfiguration neu laden
usage: /chatreload
# ── ChatModule Admin-Info ───────────────────────────────
chatinfo:
description: Chat-Informationen ueber einen Spieler anzeigen (Admin)
usage: /chatinfo <Spieler>
# ── ChatModule Chat-History ─────────────────────────────
chathist:
description: Chat-History aus dem Logfile anzeigen (Admin)
usage: /chathist [Spieler] [Anzahl]
# ── ChatModule Mentions ─────────────────────────────────
mentions:
description: Mention-Benachrichtigungen ein-/ausschalten
usage: /mentions
aliases: [mention]
# ── ChatModule Plugin-Bypass ────────────────────────────
chatbypass:
description: ChatModule fuer naechste Eingabe ueberspringen (fuer Plugin-Dialoge wie CMI)
usage: /chatbypass
aliases: [cbp]
# ── ChatModule Account-Verknuepfung ─────────────────────
# FIX #4: Command-Namen stimmen jetzt mit der Code-Registrierung überein.
# Im ChatModule wird "discordlink" mit Alias "dlink" registriert,
# und "telegramlink" mit Alias "tlink".
discordlink:
description: Minecraft-Account mit Discord verknuepfen
usage: /discordlink
aliases: [dlink]
telegramlink:
description: Minecraft-Account mit Telegram verknuepfen
usage: /telegramlink
aliases: [tlink]
unlink:
description: Account-Verknuepfung aufheben
usage: /unlink <discord|telegram|all>
# ── ChatModule Report ───────────────────────────────────
report:
description: Spieler melden
usage: /report <Spieler> <Grund>
reports:
description: Offene Reports anzeigen (Admin)
usage: /reports [all]
reportclose:
description: Report schliessen (Admin)
usage: /reportclose <ID>
permissions:
# ── StatusAPI Core ────────────────────────────────────────
statusapi.update.notify:
description: Erlaubt Update-Benachrichtigungen
default: op
statusapi.netinfo:
description: Zugriff auf /netinfo
default: op
statusapi.antibot:
description: Zugriff auf /antibot
default: op
statusapi.automessage:
description: Zugriff auf /automessage reload
default: op
# ── ChatModule Kanaele ──────────────────────────────────
chat.channel.local:
description: Zugang zum Local-Kanal
default: true
chat.channel.trade:
description: Zugang zum Trade-Kanal
default: true
chat.channel.staff:
description: Zugang zum Staff-Kanal
default: false
# ── ChatModule HelpOp ───────────────────────────────────
chat.helpop.receive:
description: HelpOp-Nachrichten empfangen
default: false
# ── ChatModule Mute ─────────────────────────────────────
chat.mute:
description: Spieler muten / unmuten
default: false
# ── ChatModule Broadcast ────────────────────────────────
chat.broadcast:
description: Broadcast-Nachrichten senden
default: false
# ── ChatModule Social Spy ───────────────────────────────
chat.socialspy:
description: Private Nachrichten mitlesen
default: false
# ── ChatModule Admin ────────────────────────────────────
chat.admin.bypass:
description: Admin-Bypass - Kann nicht geblockt/gemutet werden
default: op
chat.admin.notify:
description: Benachrichtigungen ueber Mutes und Blocks erhalten
default: false
# ── ChatModule Report ───────────────────────────────────
chat.report:
description: Spieler reporten (/report)
default: true
# ── ChatModule Farben ───────────────────────────────────
chat.color:
description: Farbcodes (&a, &b, ...) im Chat nutzen
default: false
chat.color.format:
description: Formatierungen (&l, &o, &n, ...) im Chat nutzen
default: false
# ── ChatModule Filter ───────────────────────────────────
chat.filter.bypass:
description: Chat-Filter (Anti-Spam, Caps, Blacklist) umgehen
default: false
# ── CommandBlocker ────────────────────────────────────────
commandblocker.bypass:
description: Command-Blocker umgehen
default: op
commandblocker.admin:
description: CommandBlocker verwalten (/cb)
default: op

View File

@@ -17,6 +17,8 @@ broadcast.format=%prefixColored% %messageColored%
# =========================== # ===========================
statusapi.port=9191 statusapi.port=9191
# =========================== # ===========================
# WORDPRESS / VERIFY EINSTELLUNGEN # WORDPRESS / VERIFY EINSTELLUNGEN
# =========================== # ===========================