diff --git a/src/main/java/net/viper/status/StatusAPI.java b/src/main/java/net/viper/status/StatusAPI.java index 33084ca..e2a0833 100644 --- a/src/main/java/net/viper/status/StatusAPI.java +++ b/src/main/java/net/viper/status/StatusAPI.java @@ -10,71 +10,68 @@ import net.viper.status.stats.StatsModule; import net.viper.status.modules.verify.VerifyModule; import net.viper.status.modules.globalchat.GlobalChatModule; import net.viper.status.modules.navigation.NavigationModule; +import net.viper.status.modules.commandblocker.CommandBlockerModule; +import net.viper.status.modules.broadcast.BroadcastModule; import java.io.BufferedReader; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; +import java.io.OutputStreamWriter; import java.io.PrintWriter; +import java.io.StringReader; import java.net.ServerSocket; import java.net.Socket; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.*; import java.util.concurrent.TimeUnit; /** * StatusAPI - zentraler Bungee HTTP-Status- und Broadcast-Endpunkt - * - * Ergänzungen: - * - BroadcastModule Registrierung - * - POST /broadcast UND POST / (Root) werden als Broadcasts behandelt - * - Unterstützung für prefix, prefixColor, messageColor Felder in JSON-Payload - * - Unterstützung für bracketColor (neu) - * - Unterstützung für scheduleTime (ms oder s), recur, clientScheduleId - * - API-Key Header (X-Api-Key) wird an BroadcastModule weitergereicht */ public class StatusAPI extends Plugin implements Runnable { private Thread thread; private int port = 9191; - // Das neue Modul-System private ModuleManager moduleManager; - - // Alte Komponenten (UpdateChecker/FileDownloader bleiben hier, da sie Core-Updates steuern) private UpdateChecker updateChecker; private FileDownloader fileDownloader; + private Properties verifyProperties; @Override public void onEnable() { getLogger().info("StatusAPI Core wird initialisiert..."); - // 1. Ordner sicherstellen if (!getDataFolder().exists()) { getDataFolder().mkdirs(); } - // 2. Modul-System starten + // Config mergen/updaten + mergeVerifyConfig(); + moduleManager = new ModuleManager(); - // 3. MODULE REGISTRIEREN - moduleManager.registerModule(new StatsModule()); // Statistik System laden - moduleManager.registerModule(new VerifyModule()); // Verify Modul - moduleManager.registerModule(new GlobalChatModule()); // GlobalChat - moduleManager.registerModule(new NavigationModule()); //Server Switcher - // Broadcast Modul registrieren (neu) - moduleManager.registerModule(new net.viper.status.modules.broadcast.BroadcastModule()); + // Module registrieren + moduleManager.registerModule(new StatsModule()); + moduleManager.registerModule(new VerifyModule()); + moduleManager.registerModule(new GlobalChatModule()); + moduleManager.registerModule(new NavigationModule()); + moduleManager.registerModule(new BroadcastModule()); + moduleManager.registerModule(new CommandBlockerModule()); - // 4. Alle Module aktivieren moduleManager.enableAll(this); - // 5. WebServer Thread starten + // WebServer starten getLogger().info("Starte Web-Server auf Port " + port + "..."); thread = new Thread(this, "StatusAPI-HTTP-Server"); thread.start(); - // 6. Update System Initialisieren + // Update System String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0"; updateChecker = new UpdateChecker(this, currentVersion, 6); fileDownloader = new FileDownloader(this); @@ -82,16 +79,13 @@ public class StatusAPI extends Plugin implements Runnable { File pluginFile = getFile(); File backupFile = new File(pluginFile.getParentFile(), "StatusAPI.jar.bak"); - // Backup Cleanup if (backupFile.exists()) { ProxyServer.getInstance().getScheduler().schedule(this, () -> { if (backupFile.exists()) backupFile.delete(); },1, TimeUnit.MINUTES); } - // Sofortiger Check checkAndMaybeUpdate(); - // Regelmäßiger Check ProxyServer.getInstance().getScheduler().schedule(this, this::checkAndMaybeUpdate, 6, 6, TimeUnit.HOURS); } @@ -109,7 +103,225 @@ public class StatusAPI extends Plugin implements Runnable { } } - // --- Update Logik (unverändert) --- + /** + * Hilfsmethode um Property-Werte zu bereinigen (unescape). + * Entfernt z.B. den Backslash vor URLs wie https\:// + */ + private String unescapePropertiesString(String input) { + if (input == null) return null; + try { + Properties p = new Properties(); + p.load(new StringReader("dummy=" + input)); + return p.getProperty("dummy"); + } catch (IOException e) { + return input; // Fallback, falls Parsing fehlschlägt + } + } + + // --- MERGE LOGIK --- + private void mergeVerifyConfig() { + try { + File file = new File(getDataFolder(), "verify.properties"); + + // 1. Das exakte Template definieren + List templateLines = Arrays.asList( + "# _____ __ __ ___ ____ ____", + "# / ___// /_____ _/ /___ _______/ | / __ \\/ _/", + "# \\__ \\/ __/ __ `/ __/ / / / ___/ /| | / /_/ // / ", + "# ___/ / /_/ /_/ / /_/ /_/ (__ ) ___ |/ ____// / ", + "# /____/\\__/\\__,_/\\__/\\__,_/____/_/ |_/_/ /___/ ", + " ", + "# ===========================", + "# GLOBALCHAT AKTIVIERUNG", + "# ===========================", + "chat.enabled=false", + "", + "# ------------------------------", + "# Broadcast", + "# ------------------------------", + "", + "broadcast.enabled=false", + "broadcast.prefix=[Broadcast]", + "broadcast.prefix-color=&c", + "broadcast.message-color=&f", + "broadcast.format=%prefixColored% %messageColored%", + "# broadcast.format kann angepasst werden; nutze Platzhalter: %name%, %prefix%, %prefixColored%, %message%, %messageColored%, %type%", + "", + "# ===========================", + "# NAVIGATION / SERVER SWITCHER", + "# ===========================", + "# Hier kannst du das interne Navigationssystem aktivieren/deaktivieren.", + "# Wenn aktiviert, erstellt das Plugin automatisch Befehle basierend auf den Servernamen (z.B. /lobby, /survival).", + "navigation.enabled=false", + "", + "# ===========================", + "# WORDPRESS / VERIFY EINSTELLUNGEN", + "# ===========================", + "wp_verify_url=https://deine-wp-domain.tld", + "", + "# ===========================", + "# SERVER KONFIGURATION", + "# ===========================", + "# Hier legst du für jeden Server alles fest:", + "# 1. Den Anzeigenamen für den Chat (z.B. &bLobby)", + "# 2. Die Server ID für WordPress (z.B. id=1)", + "# 3. Das Secret für WordPress (z.B. secret=...)", + "", + "# Server 1: Lobby", + "server.lobby=&bLobby", + "server.lobby.id=1", + "server.lobby.secret=GeheimesWortFuerLobby123", + "", + "# Server 2: Survival", + "server.survival=&aSurvival", + "server.survival.id=2", + "server.survival.secret=GeheimesWortFuerSurvival456", + "", + "# Server 3: SkyBlock", + "server.skyblock=&dSkyBlock", + "server.skyblock.id=3", + "server.skyblock.secret=GeheimesWortFuerSkyBlock789", + "", + "# ===========================", + "# Manuelle Ränge (Overrides)", + "# ===========================", + "# Syntax: override. = ", + "# WICHTIG: Die Gruppe (z.B. Owner) muss unten bei groupformat definiert sein!", + "", + "# Beispiel: Deinen UUID hier einfügen", + "override.uuid-hier-einfügen = Owner", + "override.uuid-hier-einfügen-2 = Admin", + "override.uuid-hier-einfügen-3 = Developer", + "", + "# ===========================", + "# Chat-Formate für Gruppen", + "# ===========================", + "", + "# Der Name hinter dem Punkt (z.B. Owner) muss exakt mit der LuckPerms Gruppe übereinstimmen.", + "# Nutze & für Farbcodes.", + "", + "# Ränge mit neuer Syntax: Rank || Spielerfarbe || Chatfarbe", + "# Beispiel: Rot (Rang) || Blau (Name) || Lila (Chat)", + "groupformat.owner=&c[Owner] || &b || &d", + "groupformat.admin=&4[Admin] || &9 || &c", + "groupformat.developer=&b[Dev] || &3 || &a", + "groupformat.premium=&6[Premium] || &e || &7", + "groupformat.spieler=&f[Spieler] || &7 || &8", + "", + "# ===========================", + "# COMMAND BLOCKER", + "# ===========================", + "commandblocker.enabled=true", + "commandblocker.bypass.permission=commandblocker.bypass" + ); + + // 2. Existierende Werte einlesen + Map existingValues = new HashMap<>(); + // Liste für Override-Keys, die nicht im Template stehen + List> pendingOverrides = new ArrayList<>(); + + if (file.exists()) { + List existing = Files.readAllLines(file.toPath(), StandardCharsets.UTF_8); + for (String line : existing) { + String trimmed = line.trim(); + if (!trimmed.isEmpty() && !trimmed.startsWith("#") && trimmed.contains("=")) { + int idx = trimmed.indexOf('='); + String key = trimmed.substring(0, idx).trim(); + String val = trimmed.substring(idx + 1).trim(); + // Wert speichern, Escaping entfernen für sauberen Output + existingValues.put(key, unescapePropertiesString(val)); + } + } + } + + // 3. Alle Override-Keys aus der alten Config sammeln + for (Map.Entry entry : existingValues.entrySet()) { + if (entry.getKey().startsWith("override.")) { + pendingOverrides.add(entry); + } + } + + // 4. Datei Zeile für Zeile basierend auf Template schreiben + List outputLines = new ArrayList<>(); + Set usedKeys = new HashSet<>(); + + for (int i = 0; i < templateLines.size(); i++) { + String line = templateLines.get(i); + String trimmed = line.trim(); + + // PRÜFUNG: Wenn wir am Anfang des nächsten Abschnitts sind ("Chat-Formate") + // Fügen wir die fehlenden Overrides VORHER ein. + if (trimmed.startsWith("#") && trimmed.contains("====")) { + if (i + 1 < templateLines.size()) { + String nextTrimmed = templateLines.get(i + 1).trim(); + // Wenn wir kurz vor "Chat-Formate" stehen und noch Overrides offen sind -> Einfügen + if (nextTrimmed.contains("Chat-Formate") && !pendingOverrides.isEmpty()) { + outputLines.add(""); + for (Map.Entry ov : pendingOverrides) { + if (!usedKeys.contains(ov.getKey())) { + // Bereinigten Wert schreiben + outputLines.add(ov.getKey() + "=" + ov.getValue()); + usedKeys.add(ov.getKey()); + } + } + pendingOverrides.clear(); + outputLines.add(""); + } + } + } + + // LOGIK: Template Zeile verarbeiten + if (!trimmed.startsWith("#") && !trimmed.isEmpty() && trimmed.contains("=")) { + String[] parts = trimmed.split("=", 2); + String key = parts[0].trim(); + + if (existingValues.containsKey(key)) { + // Wert aus alter Config übernehmen (bereinigt) + String cleanVal = unescapePropertiesString(existingValues.get(key)); + outputLines.add(key + "=" + cleanVal); + usedKeys.add(key); + + pendingOverrides.removeIf(e -> e.getKey().equals(key)); + } else { + // Standardwert aus Template übernehmen + outputLines.add(line); + } + } else { + outputLines.add(line); + } + } + + // Falls noch was übrig ist, am Ende anhängen + if (!pendingOverrides.isEmpty()) { + outputLines.add(""); + for (Map.Entry ov : pendingOverrides) { + outputLines.add(ov.getKey() + "=" + ov.getValue()); + } + } + + // 5. Datei schreiben + Files.write(file.toPath(), outputLines, StandardCharsets.UTF_8); + + // 6. Properties laden + verifyProperties = new Properties(); + try (FileInputStream fis = new FileInputStream(file)) { + verifyProperties.load(fis); + } + + getLogger().info("verify.properties erfolgreich aktualisiert."); + + } catch (IOException e) { + getLogger().severe("Fehler beim Merge der verify.properties: " + e.getMessage()); + } + } + + public Properties getVerifyProperties() { + synchronized (this) { + return verifyProperties; + } + } + + // --- Update Logik --- private void checkAndMaybeUpdate() { try { updateChecker.checkNow(); @@ -173,7 +385,6 @@ public class StatusAPI extends Plugin implements Runnable { } // --- WebServer & JSON --- - @Override public void run() { try (ServerSocket serverSocket = new ServerSocket(port)) { @@ -192,15 +403,6 @@ public class StatusAPI extends Plugin implements Runnable { } } - /** - * Erweiterter HTTP-Handler: - * - GET weiterhin liefert Status JSON wie bisher - * - POST an /broadcast oder POST an / (Root) wird als Broadcast verarbeitet - * (Payload JSON mit message, type, prefix, prefixColor, bracketColor, messageColor) - * - Wenn Feld scheduleTime (ms oder s) vorhanden ist, wird Nachricht als geplante Nachricht registriert - * und an BroadcastModule.scheduleBroadcast weitergegeben. - * - POST /broadcast/cancel erwartet clientScheduleId im Body und ruft cancelScheduled(clientScheduleId) auf. - */ private void handleConnection(Socket clientSocket) { try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), "UTF-8")); OutputStream out = clientSocket.getOutputStream()) { @@ -208,13 +410,11 @@ public class StatusAPI extends Plugin implements Runnable { String inputLine = in.readLine(); if (inputLine == null) return; - // Request-Line zerlegen: METHOD PATH HTTP/VERSION String[] reqParts = inputLine.split(" "); if (reqParts.length < 2) return; String method = reqParts[0].trim(); String path = reqParts[1].trim(); - // Header einlesen (case-insensitive keys) Map headers = new HashMap<>(); String line; while ((line = in.readLine()) != null && !line.isEmpty()) { @@ -226,7 +426,7 @@ public class StatusAPI extends Plugin implements Runnable { } } - // --- POST /broadcast/cancel (optional) --- + // --- POST /broadcast/cancel --- if ("POST".equalsIgnoreCase(method) && (path.equalsIgnoreCase("/broadcast/cancel") || path.equalsIgnoreCase("/cancel"))) { int contentLength = 0; if (headers.containsKey("content-length")) { @@ -248,8 +448,8 @@ public class StatusAPI extends Plugin implements Runnable { return; } Object mod = moduleManager.getModule("BroadcastModule"); - if (mod instanceof net.viper.status.modules.broadcast.BroadcastModule) { - boolean ok = ((net.viper.status.modules.broadcast.BroadcastModule) mod).cancelScheduled(clientScheduleId); + if (mod instanceof BroadcastModule) { + boolean ok = ((BroadcastModule) mod).cancelScheduled(clientScheduleId); if (ok) sendHttpResponse(out, "{\"success\":true}", 200); else sendHttpResponse(out, "{\"success\":false,\"error\":\"not_found\"}", 404); return; @@ -259,7 +459,7 @@ public class StatusAPI extends Plugin implements Runnable { } } - // --- POST /broadcast oder POST / (Root) für register/send --- + // --- POST /broadcast --- if ("POST".equalsIgnoreCase(method) && ("/broadcast".equalsIgnoreCase(path) || "/".equals(path) || path.isEmpty())) { int contentLength = 0; if (headers.containsKey("content-length")) { @@ -277,34 +477,28 @@ public class StatusAPI extends Plugin implements Runnable { } String body = new String(bodyChars); - // Header X-Api-Key (case-insensitive) String apiKeyHeader = headers.getOrDefault("x-api-key", headers.getOrDefault("x-apikey", "")); - // parse minimal JSON fields we need String message = extractJsonString(body, "message"); String type = extractJsonString(body, "type"); String prefix = extractJsonString(body, "prefix"); String prefixColor = extractJsonString(body, "prefixColor"); - String bracketColor = extractJsonString(body, "bracketColor"); // HINZUGEFÜGT + String bracketColor = extractJsonString(body, "bracketColor"); String messageColor = extractJsonString(body, "messageColor"); String sourceName = extractJsonString(body, "source"); - String scheduleTimeStr = extractJsonString(body, "scheduleTime"); // expecting millis OR seconds + String scheduleTimeStr = extractJsonString(body, "scheduleTime"); String recur = extractJsonString(body, "recur"); String clientScheduleId = extractJsonString(body, "clientScheduleId"); if (sourceName == null || sourceName.isEmpty()) sourceName = "PulseCast"; if (type == null || type.isEmpty()) type = "global"; - // if scheduleTime present -> register schedule with BroadcastModule if (scheduleTimeStr != null && !scheduleTimeStr.trim().isEmpty()) { long scheduleMillis = 0L; try { - // scheduleTime may be numeric string (ms or s) scheduleMillis = Long.parseLong(scheduleTimeStr.trim()); - // if looks like seconds (less than 1e12), convert to ms if (scheduleMillis < 1_000_000_000_000L) scheduleMillis = scheduleMillis * 1000L; } catch (NumberFormatException ignored) { - // try to parse as double then convert try { double d = Double.parseDouble(scheduleTimeStr.trim()); long v = (long) d; @@ -316,21 +510,15 @@ public class StatusAPI extends Plugin implements Runnable { } } - // BroadcastModule: scheduleBroadcast(timestampMillis, sourceName, message, type, apiKeyHeader, prefix, prefixColor, bracketColor, messageColor, recur, clientId) try { Object mod = moduleManager.getModule("BroadcastModule"); - if (mod instanceof net.viper.status.modules.broadcast.BroadcastModule) { - net.viper.status.modules.broadcast.BroadcastModule bm = - (net.viper.status.modules.broadcast.BroadcastModule) mod; - + if (mod instanceof BroadcastModule) { + BroadcastModule bm = (BroadcastModule) mod; boolean ok = bm.scheduleBroadcast(scheduleMillis, sourceName, message, type, apiKeyHeader, - prefix, prefixColor, bracketColor, messageColor, (recur == null ? "none" : recur), (clientScheduleId == null ? null : clientScheduleId)); // bracketColor HINZUGEFÜGT + prefix, prefixColor, bracketColor, messageColor, (recur == null ? "none" : recur), (clientScheduleId == null ? null : clientScheduleId)); - if (ok) { - sendHttpResponse(out, "{\"success\":true}", 200); - } else { - sendHttpResponse(out, "{\"success\":false,\"error\":\"rejected\"}", 403); - } + if (ok) sendHttpResponse(out, "{\"success\":true}", 200); + else sendHttpResponse(out, "{\"success\":false,\"error\":\"rejected\"}", 403); return; } else { sendHttpResponse(out, "{\"success\":false,\"error\":\"no_broadcast_module\"}", 500); @@ -343,7 +531,6 @@ public class StatusAPI extends Plugin implements Runnable { } } - // If no scheduleTime -> immediate broadcast if (message == null || message.isEmpty()) { String resp = "{\"success\":false,\"error\":\"missing_message\"}"; sendHttpResponse(out, resp, 400); @@ -352,11 +539,9 @@ public class StatusAPI extends Plugin implements Runnable { try { Object mod = moduleManager.getModule("BroadcastModule"); - if (mod instanceof net.viper.status.modules.broadcast.BroadcastModule) { - net.viper.status.modules.broadcast.BroadcastModule bm = - (net.viper.status.modules.broadcast.BroadcastModule) mod; - - boolean ok = bm.handleBroadcast(sourceName, message, type, apiKeyHeader, prefix, prefixColor, bracketColor, messageColor); // bracketColor HINZUGEFÜGT + if (mod instanceof BroadcastModule) { + BroadcastModule bm = (BroadcastModule) mod; + boolean ok = bm.handleBroadcast(sourceName, message, type, apiKeyHeader, prefix, prefixColor, bracketColor, messageColor); if (ok) sendHttpResponse(out, "{\"success\":true}", 200); else sendHttpResponse(out, "{\"success\":false,\"error\":\"rejected\"}", 403); @@ -372,12 +557,11 @@ public class StatusAPI extends Plugin implements Runnable { } } - // --- bisheriger GET-Handler (unverändert) --- + // --- GET Handler --- if (inputLine != null && inputLine.startsWith("GET")) { Map data = new LinkedHashMap<>(); data.put("online", true); - // Version & Info String versionRaw = ProxyServer.getInstance().getVersion(); String versionClean = (versionRaw != null && versionRaw.contains(":")) ? versionRaw.split(":")[2].trim() : versionRaw; data.put("version", versionClean); @@ -390,7 +574,6 @@ public class StatusAPI extends Plugin implements Runnable { } catch (Exception ignored) {} data.put("motd", motd); - // StatsModul holen (Service Locator) StatsModule statsModule = (StatsModule) moduleManager.getModule("StatsModule"); boolean luckPermsEnabled = ProxyServer.getInstance().getPluginManager().getPlugin("LuckPerms") != null; @@ -420,7 +603,6 @@ public class StatusAPI extends Plugin implements Runnable { } playerInfo.put("prefix", prefix); - // Stats Integration via Modul if (statsModule != null) { PlayerStats ps = statsModule.getManager().getIfPresent(p.getUniqueId()); if (ps != null) { @@ -451,10 +633,6 @@ public class StatusAPI extends Plugin implements Runnable { } } - /** - * Minimaler JSON-String-Extractor (robust genug für einfache Payloads). - * Liefert null, wenn Schlüssel nicht gefunden oder kein String-Wert. - */ private String extractJsonString(String json, String key) { if (json == null || key == null) return null; String search = "\"" + key + "\""; @@ -463,12 +641,11 @@ public class StatusAPI extends Plugin implements Runnable { int colon = json.indexOf(':', idx + search.length()); if (colon < 0) return null; int i = colon + 1; - // skip spaces while (i < json.length() && Character.isWhitespace(json.charAt(i))) i++; if (i >= json.length()) return null; char c = json.charAt(i); if (c == '"') { - i++; // skip opening quote + i++; StringBuilder sb = new StringBuilder(); boolean escape = false; while (i < json.length()) { @@ -484,7 +661,6 @@ public class StatusAPI extends Plugin implements Runnable { } return sb.toString(); } else { - // not a quoted string -> read until comma or } StringBuilder sb = new StringBuilder(); while (i < json.length()) { char ch = json.charAt(i);