diff --git a/src/main/java/net/viper/status/StatusAPI.java b/src/main/java/net/viper/status/StatusAPI.java index a84addc..33084ca 100644 --- a/src/main/java/net/viper/status/StatusAPI.java +++ b/src/main/java/net/viper/status/StatusAPI.java @@ -1,310 +1,552 @@ -package net.viper.status; - -import net.md_5.bungee.api.ProxyServer; -import net.md_5.bungee.api.config.ListenerInfo; -import net.md_5.bungee.api.connection.ProxiedPlayer; -import net.md_5.bungee.api.plugin.Plugin; -import net.viper.status.module.ModuleManager; -import net.viper.status.stats.PlayerStats; -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 java.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.net.ServerSocket; -import java.net.Socket; -import java.util.*; -import java.util.concurrent.TimeUnit; - -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; - - @Override - public void onEnable() { - getLogger().info("StatusAPI Core wird initialisiert..."); - - // 1. Ordner sicherstellen - if (!getDataFolder().exists()) { - getDataFolder().mkdirs(); - } - - // 2. Modul-System starten - moduleManager = new ModuleManager(); - - // 3. MODULE REGISTRIEREN (Hier erweiterst du die API in Zukunft!) - // Um ein neues Feature hinzuzufügen, erstelle eine Klasse, die 'Module' implementiert - // und füge hier eine Zeile hinzu: moduleManager.registerModule(new MeinNeuesModul()); - - moduleManager.registerModule(new StatsModule()); // Statistik System laden - - moduleManager.registerModule(new VerifyModule()); // Verify Modul - - moduleManager.registerModule(new GlobalChatModule()); // GlobalChat - - moduleManager.registerModule(new NavigationModule()); //Server Switcher - - // 4. Alle Module aktivieren - moduleManager.enableAll(this); - - // 5. WebServer Thread starten - getLogger().info("Starte Web-Server auf Port " + port + "..."); - thread = new Thread(this, "StatusAPI-HTTP-Server"); - thread.start(); - - // 6. Update System Initialisieren - String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0"; - updateChecker = new UpdateChecker(this, currentVersion, 6); - fileDownloader = new FileDownloader(this); - - 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); - } - - @Override - public void onDisable() { - getLogger().info("Stoppe Module..."); - if (moduleManager != null) { - moduleManager.disableAll(this); - } - - getLogger().info("Stoppe Web-Server..."); - if (thread != null) { - thread.interrupt(); - try { thread.join(1000); } catch (InterruptedException ignored) {} - } - } - - // --- Update Logik (unverändert) --- - private void checkAndMaybeUpdate() { - try { - updateChecker.checkNow(); - String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0"; - - if (updateChecker.isUpdateAvailable(currentVersion)) { - String newVersion = updateChecker.getLatestVersion(); - String url = updateChecker.getLatestUrl(); - getLogger().warning("----------------------------------------"); - getLogger().warning("Neue Version verfügbar: " + newVersion); - getLogger().warning("Starte automatisches Update..."); - getLogger().warning("----------------------------------------"); - - File pluginFile = getFile(); - File newFile = new File(pluginFile.getParentFile(), "StatusAPI.jar.new"); - - fileDownloader.downloadFile(url, newFile, () -> triggerUpdateScript(pluginFile, newFile)); - } - } catch (Exception e) { - getLogger().severe("Fehler beim Update-Check: " + e.getMessage()); - } - } - - private void triggerUpdateScript(File currentFile, File newFile) { - try { - File pluginsFolder = currentFile.getParentFile(); - File rootFolder = pluginsFolder.getParentFile(); - File batFile = new File(rootFolder, "StatusAPI_Update_" + System.currentTimeMillis() + ".bat"); - - String batContent = "@echo off\n" + - "echo Bitte warten, der Server fährt herunter...\n" + - "timeout /t 5 /nobreak >nul\n" + - "cd /d \"" + pluginsFolder.getAbsolutePath().replace("\\", "/") + "\"\n" + - "echo Fuehre Datei-Tausch durch...\n" + - "if exist StatusAPI.jar.bak del StatusAPI.jar.bak\n" + - "if exist StatusAPI.jar (\n" + - " ren StatusAPI.jar StatusAPI.jar.bak\n" + - ")\n" + - "if exist StatusAPI.new.jar (\n" + - " ren StatusAPI.new.jar StatusAPI.jar\n" + - " echo Update erfolgreich!\n" + - ") else (\n" + - " echo FEHLER: StatusAPI.new.jar nicht gefunden!\n" + - " pause\n" + - ")\n" + - "del \"%~f0\""; - - try (PrintWriter out = new PrintWriter(batFile)) { out.println(batContent); } - - for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { - p.disconnect("§cServer fährt für ein Update neu herunter. Bitte etwas warten."); - } - - getLogger().info("Starte Update-Skript..."); - Runtime.getRuntime().exec("cmd /c start \"Update_Proc\" \"" + batFile.getAbsolutePath() + "\""); - ProxyServer.getInstance().stop(); - - } catch (Exception e) { - getLogger().severe("Fehler beim Vorbereiten des Updates: " + e.getMessage()); - } - } - - // --- WebServer & JSON --- - - @Override - public void run() { - try (ServerSocket serverSocket = new ServerSocket(port)) { - serverSocket.setSoTimeout(1000); - while (!Thread.interrupted()) { - try { - Socket clientSocket = serverSocket.accept(); - handleConnection(clientSocket); - } catch (java.net.SocketTimeoutException e) {} - catch (IOException e) { - getLogger().warning("Fehler beim Akzeptieren einer Verbindung: " + e.getMessage()); - } - } - } catch (IOException e) { - getLogger().severe("Konnte ServerSocket nicht starten: " + e.getMessage()); - } - } - - private void handleConnection(Socket clientSocket) { - try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), "UTF-8")); - OutputStream out = clientSocket.getOutputStream()) { - - String inputLine = in.readLine(); - 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); - data.put("max_players", String.valueOf(ProxyServer.getInstance().getConfig().getPlayerLimit())); - - String motd = "BungeeCord"; - try { - Iterator it = ProxyServer.getInstance().getConfig().getListeners().iterator(); - if (it.hasNext()) motd = it.next().getMotd(); - } 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; - List> playersList = new ArrayList<>(); - - for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { - Map playerInfo = new LinkedHashMap<>(); - playerInfo.put("name", p.getName()); - try { playerInfo.put("uuid", p.getUniqueId().toString()); } catch (Exception ignored) {} - - String prefix = ""; - if (luckPermsEnabled) { - try { - Class providerClass = Class.forName("net.luckperms.api.LuckPermsProvider"); - Object luckPermsApi = providerClass.getMethod("get").invoke(null); - Object userManager = luckPermsApi.getClass().getMethod("getUserManager").invoke(luckPermsApi); - Object user = userManager.getClass().getMethod("getUser", UUID.class).invoke(userManager, p.getUniqueId()); - if (user != null) { - Class queryOptionsClass = Class.forName("net.luckperms.api.query.QueryOptions"); - Object queryOptions = queryOptionsClass.getMethod("defaultContextualOptions").invoke(null); - Object cachedData = user.getClass().getMethod("getCachedData").invoke(user); - Object metaData = cachedData.getClass().getMethod("getMetaData", queryOptionsClass).invoke(cachedData, queryOptions); - Object result = metaData.getClass().getMethod("getPrefix").invoke(metaData); - if (result != null) prefix = (String) result; - } - } catch (Exception ignored) {} - } - playerInfo.put("prefix", prefix); - - // Stats Integration via Modul - if (statsModule != null) { - PlayerStats ps = statsModule.getManager().getIfPresent(p.getUniqueId()); - if (ps != null) { - playerInfo.put("playtime", ps.getPlaytimeWithCurrentSession()); - playerInfo.put("joins", ps.joins); - playerInfo.put("first_seen", ps.firstSeen); - playerInfo.put("last_seen", ps.lastSeen); - } - } - playersList.add(playerInfo); - } - data.put("players", playersList); - - String json = buildJsonString(data); - byte[] jsonBytes = json.getBytes("UTF-8"); - StringBuilder response = new StringBuilder(); - response.append("HTTP/1.1 200 OK\r\n"); - response.append("Content-Type: application/json; charset=UTF-8\r\n"); - response.append("Access-Control-Allow-Origin: *\r\n"); - response.append("Content-Length: ").append(jsonBytes.length).append("\r\n"); - response.append("Connection: close\r\n\r\n"); - out.write(response.toString().getBytes("UTF-8")); - out.write(jsonBytes); - out.flush(); - } - } catch (Exception e) { - getLogger().severe("Fehler beim Verarbeiten der Anfrage: " + e.getMessage()); - } - } - - private String buildJsonString(Map data) { - StringBuilder sb = new StringBuilder("{"); - boolean first = true; - for (Map.Entry entry : data.entrySet()) { - if (!first) sb.append(","); - first = false; - sb.append("\"").append(escapeJson(entry.getKey())).append("\":").append(valueToString(entry.getValue())); - } - sb.append("}"); - return sb.toString(); - } - - private String valueToString(Object value) { - if (value == null) return "null"; - else if (value instanceof Boolean) return value.toString(); - else if (value instanceof Number) return value.toString(); - else if (value instanceof Map) { - @SuppressWarnings("unchecked") - Map m = (Map) value; - return buildJsonString(m); - } else if (value instanceof List) { - StringBuilder sb = new StringBuilder("["); - List list = (List) value; - for (int i = 0; i < list.size(); i++) { - if (i > 0) sb.append(","); - Object item = list.get(i); - if (item instanceof Map) sb.append(buildJsonString((Map) item)); - else if (item instanceof Number) sb.append(item.toString()); - else sb.append("\"").append(escapeJson(String.valueOf(item))).append("\""); - } - sb.append("]"); - return sb.toString(); - } - else return "\"" + escapeJson(String.valueOf(value)) + "\""; - } - - private String escapeJson(String s) { - if (s == null) return ""; - return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r"); - } +package net.viper.status; + +import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.api.config.ListenerInfo; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.plugin.Plugin; +import net.viper.status.module.ModuleManager; +import net.viper.status.stats.PlayerStats; +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 java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +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; + + @Override + public void onEnable() { + getLogger().info("StatusAPI Core wird initialisiert..."); + + // 1. Ordner sicherstellen + if (!getDataFolder().exists()) { + getDataFolder().mkdirs(); + } + + // 2. Modul-System starten + 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()); + + // 4. Alle Module aktivieren + moduleManager.enableAll(this); + + // 5. WebServer Thread starten + getLogger().info("Starte Web-Server auf Port " + port + "..."); + thread = new Thread(this, "StatusAPI-HTTP-Server"); + thread.start(); + + // 6. Update System Initialisieren + String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0"; + updateChecker = new UpdateChecker(this, currentVersion, 6); + fileDownloader = new FileDownloader(this); + + 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); + } + + @Override + public void onDisable() { + getLogger().info("Stoppe Module..."); + if (moduleManager != null) { + moduleManager.disableAll(this); + } + + getLogger().info("Stoppe Web-Server..."); + if (thread != null) { + thread.interrupt(); + try { thread.join(1000); } catch (InterruptedException ignored) {} + } + } + + // --- Update Logik (unverändert) --- + private void checkAndMaybeUpdate() { + try { + updateChecker.checkNow(); + String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0"; + + if (updateChecker.isUpdateAvailable(currentVersion)) { + String newVersion = updateChecker.getLatestVersion(); + String url = updateChecker.getLatestUrl(); + getLogger().warning("----------------------------------------"); + getLogger().warning("Neue Version verfügbar: " + newVersion); + getLogger().warning("Starte automatisches Update..."); + getLogger().warning("----------------------------------------"); + + File pluginFile = getFile(); + File newFile = new File(pluginFile.getParentFile(), "StatusAPI.jar.new"); + + fileDownloader.downloadFile(url, newFile, () -> triggerUpdateScript(pluginFile, newFile)); + } + } catch (Exception e) { + getLogger().severe("Fehler beim Update-Check: " + e.getMessage()); + } + } + + private void triggerUpdateScript(File currentFile, File newFile) { + try { + File pluginsFolder = currentFile.getParentFile(); + File rootFolder = pluginsFolder.getParentFile(); + File batFile = new File(rootFolder, "StatusAPI_Update_" + System.currentTimeMillis() + ".bat"); + + String batContent = "@echo off\n" + + "echo Bitte warten, der Server fährt herunter...\n" + + "timeout /t 5 /nobreak >nul\n" + + "cd /d \"" + pluginsFolder.getAbsolutePath().replace("\\", "/") + "\"\n" + + "echo Fuehre Datei-Tausch durch...\n" + + "if exist StatusAPI.jar.bak del StatusAPI.jar.bak\n" + + "if exist StatusAPI.jar (\n" + + " ren StatusAPI.jar StatusAPI.jar.bak\n" + + ")\n" + + "if exist StatusAPI.new.jar (\n" + + " ren StatusAPI.new.jar StatusAPI.jar\n" + + " echo Update erfolgreich!\n" + + ") else (\n" + + " echo FEHLER: StatusAPI.new.jar nicht gefunden!\n" + + " pause\n" + + ")\n" + + "del \"%~f0\""; + + try (PrintWriter out = new PrintWriter(batFile)) { out.println(batContent); } + + for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { + p.disconnect("§cServer fährt für ein Update neu herunter. Bitte etwas warten."); + } + + getLogger().info("Starte Update-Skript..."); + Runtime.getRuntime().exec("cmd /c start \"Update_Proc\" \"" + batFile.getAbsolutePath() + "\""); + ProxyServer.getInstance().stop(); + + } catch (Exception e) { + getLogger().severe("Fehler beim Vorbereiten des Updates: " + e.getMessage()); + } + } + + // --- WebServer & JSON --- + + @Override + public void run() { + try (ServerSocket serverSocket = new ServerSocket(port)) { + serverSocket.setSoTimeout(1000); + while (!Thread.interrupted()) { + try { + Socket clientSocket = serverSocket.accept(); + handleConnection(clientSocket); + } catch (java.net.SocketTimeoutException e) {} + catch (IOException e) { + getLogger().warning("Fehler beim Akzeptieren einer Verbindung: " + e.getMessage()); + } + } + } catch (IOException e) { + getLogger().severe("Konnte ServerSocket nicht starten: " + e.getMessage()); + } + } + + /** + * 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()) { + + 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()) { + int idx = line.indexOf(':'); + if (idx > 0) { + String key = line.substring(0, idx).trim().toLowerCase(Locale.ROOT); + String val = line.substring(idx + 1).trim(); + headers.put(key, val); + } + } + + // --- POST /broadcast/cancel (optional) --- + if ("POST".equalsIgnoreCase(method) && (path.equalsIgnoreCase("/broadcast/cancel") || path.equalsIgnoreCase("/cancel"))) { + int contentLength = 0; + if (headers.containsKey("content-length")) { + try { contentLength = Integer.parseInt(headers.get("content-length")); } catch (NumberFormatException ignored) {} + } + char[] bodyChars = new char[Math.max(0, contentLength)]; + if (contentLength > 0) { + int read = 0; + while (read < contentLength) { + int r = in.read(bodyChars, read, contentLength - read); + if (r == -1) break; + read += r; + } + } + String body = new String(bodyChars); + String clientScheduleId = extractJsonString(body, "clientScheduleId"); + if (clientScheduleId == null || clientScheduleId.isEmpty()) { + sendHttpResponse(out, "{\"success\":false,\"error\":\"missing_clientScheduleId\"}", 400); + 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 (ok) sendHttpResponse(out, "{\"success\":true}", 200); + else sendHttpResponse(out, "{\"success\":false,\"error\":\"not_found\"}", 404); + return; + } else { + sendHttpResponse(out, "{\"success\":false,\"error\":\"no_broadcast_module\"}", 500); + return; + } + } + + // --- POST /broadcast oder POST / (Root) für register/send --- + if ("POST".equalsIgnoreCase(method) && ("/broadcast".equalsIgnoreCase(path) || "/".equals(path) || path.isEmpty())) { + int contentLength = 0; + if (headers.containsKey("content-length")) { + try { contentLength = Integer.parseInt(headers.get("content-length")); } catch (NumberFormatException ignored) {} + } + + char[] bodyChars = new char[Math.max(0, contentLength)]; + if (contentLength > 0) { + int read = 0; + while (read < contentLength) { + int r = in.read(bodyChars, read, contentLength - read); + if (r == -1) break; + read += r; + } + } + 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 messageColor = extractJsonString(body, "messageColor"); + String sourceName = extractJsonString(body, "source"); + String scheduleTimeStr = extractJsonString(body, "scheduleTime"); // expecting millis OR seconds + 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; + if (v < 1_000_000_000_000L) v = v * 1000L; + scheduleMillis = v; + } catch (Exception ex) { + sendHttpResponse(out, "{\"success\":false,\"error\":\"bad_scheduleTime\"}", 400); + return; + } + } + + // 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; + + boolean ok = bm.scheduleBroadcast(scheduleMillis, sourceName, message, type, apiKeyHeader, + prefix, prefixColor, bracketColor, messageColor, (recur == null ? "none" : recur), (clientScheduleId == null ? null : clientScheduleId)); // bracketColor HINZUGEFÜGT + + 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); + return; + } + } catch (Throwable t) { + t.printStackTrace(); + sendHttpResponse(out, "{\"success\":false,\"error\":\"internal\"}", 500); + return; + } + } + + // If no scheduleTime -> immediate broadcast + if (message == null || message.isEmpty()) { + String resp = "{\"success\":false,\"error\":\"missing_message\"}"; + sendHttpResponse(out, resp, 400); + return; + } + + 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 (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); + return; + } + } catch (Throwable t) { + t.printStackTrace(); + sendHttpResponse(out, "{\"success\":false,\"error\":\"internal\"}", 500); + return; + } + } + + // --- bisheriger GET-Handler (unverändert) --- + 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); + data.put("max_players", String.valueOf(ProxyServer.getInstance().getConfig().getPlayerLimit())); + + String motd = "BungeeCord"; + try { + Iterator it = ProxyServer.getInstance().getConfig().getListeners().iterator(); + if (it.hasNext()) motd = it.next().getMotd(); + } 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; + List> playersList = new ArrayList<>(); + + for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { + Map playerInfo = new LinkedHashMap<>(); + playerInfo.put("name", p.getName()); + try { playerInfo.put("uuid", p.getUniqueId().toString()); } catch (Exception ignored) {} + + String prefix = ""; + if (luckPermsEnabled) { + try { + Class providerClass = Class.forName("net.luckperms.api.LuckPermsProvider"); + Object luckPermsApi = providerClass.getMethod("get").invoke(null); + Object userManager = luckPermsApi.getClass().getMethod("getUserManager").invoke(luckPermsApi); + Object user = userManager.getClass().getMethod("getUser", UUID.class).invoke(userManager, p.getUniqueId()); + if (user != null) { + Class queryOptionsClass = Class.forName("net.luckperms.api.query.QueryOptions"); + Object queryOptions = queryOptionsClass.getMethod("defaultContextualOptions").invoke(null); + Object cachedData = user.getClass().getMethod("getCachedData").invoke(user); + Object metaData = cachedData.getClass().getMethod("getMetaData", queryOptionsClass).invoke(cachedData, queryOptions); + Object result = metaData.getClass().getMethod("getPrefix").invoke(metaData); + if (result != null) prefix = (String) result; + } + } catch (Exception ignored) {} + } + playerInfo.put("prefix", prefix); + + // Stats Integration via Modul + if (statsModule != null) { + PlayerStats ps = statsModule.getManager().getIfPresent(p.getUniqueId()); + if (ps != null) { + playerInfo.put("playtime", ps.getPlaytimeWithCurrentSession()); + playerInfo.put("joins", ps.joins); + playerInfo.put("first_seen", ps.firstSeen); + playerInfo.put("last_seen", ps.lastSeen); + } + } + playersList.add(playerInfo); + } + data.put("players", playersList); + + String json = buildJsonString(data); + byte[] jsonBytes = json.getBytes("UTF-8"); + StringBuilder response = new StringBuilder(); + response.append("HTTP/1.1 200 OK\r\n"); + response.append("Content-Type: application/json; charset=UTF-8\r\n"); + response.append("Access-Control-Allow-Origin: *\r\n"); + response.append("Content-Length: ").append(jsonBytes.length).append("\r\n"); + response.append("Connection: close\r\n\r\n"); + out.write(response.toString().getBytes("UTF-8")); + out.write(jsonBytes); + out.flush(); + } + } catch (Exception e) { + getLogger().severe("Fehler beim Verarbeiten der Anfrage: " + e.getMessage()); + } + } + + /** + * 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 + "\""; + int idx = json.indexOf(search); + if (idx < 0) return null; + 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 + StringBuilder sb = new StringBuilder(); + boolean escape = false; + while (i < json.length()) { + char ch = json.charAt(i++); + if (escape) { + sb.append(ch); + escape = false; + } else { + if (ch == '\\') escape = true; + else if (ch == '"') break; + else sb.append(ch); + } + } + 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); + if (ch == ',' || ch == '}' || ch == '\n' || ch == '\r') break; + sb.append(ch); + i++; + } + return sb.toString().trim(); + } + } + + private void sendHttpResponse(OutputStream out, String json, int code) throws IOException { + byte[] jsonBytes = json.getBytes(StandardCharsets.UTF_8); + StringBuilder response = new StringBuilder(); + response.append("HTTP/1.1 ").append(code).append(" ").append(code == 200 ? "OK" : "ERROR").append("\r\n"); + response.append("Content-Type: application/json; charset=UTF-8\r\n"); + response.append("Access-Control-Allow-Origin: *\r\n"); + response.append("Content-Length: ").append(jsonBytes.length).append("\r\n"); + response.append("Connection: close\r\n\r\n"); + out.write(response.toString().getBytes(StandardCharsets.UTF_8)); + out.write(jsonBytes); + out.flush(); + } + + private String buildJsonString(Map data) { + StringBuilder sb = new StringBuilder("{"); + boolean first = true; + for (Map.Entry entry : data.entrySet()) { + if (!first) sb.append(","); + first = false; + sb.append("\"").append(escapeJson(entry.getKey())).append("\":").append(valueToString(entry.getValue())); + } + sb.append("}"); + return sb.toString(); + } + + private String valueToString(Object value) { + if (value == null) return "null"; + else if (value instanceof Boolean) return value.toString(); + else if (value instanceof Number) return value.toString(); + else if (value instanceof Map) { + @SuppressWarnings("unchecked") + Map m = (Map) value; + return buildJsonString(m); + } else if (value instanceof List) { + StringBuilder sb = new StringBuilder("["); + List list = (List) value; + for (int i = 0; i < list.size(); i++) { + if (i > 0) sb.append(","); + Object item = list.get(i); + if (item instanceof Map) sb.append(buildJsonString((Map) item)); + else if (item instanceof Number) sb.append(item.toString()); + else sb.append("\"").append(escapeJson(String.valueOf(item))).append("\""); + } + sb.append("]"); + return sb.toString(); + } + else return "\"" + escapeJson(String.valueOf(value)) + "\""; + } + + private String escapeJson(String s) { + if (s == null) return ""; + return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r"); + } } \ No newline at end of file