src/main/java/net/viper/status/StatusAPI.java aktualisiert

This commit is contained in:
2026-01-17 18:26:24 +00:00
parent 59944ece4e
commit e60bff9c6d

View File

@@ -19,9 +19,21 @@ import java.io.OutputStream;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.net.Socket; import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
import java.util.concurrent.TimeUnit; 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 { public class StatusAPI extends Plugin implements Runnable {
private Thread thread; private Thread thread;
@@ -46,17 +58,13 @@ public class StatusAPI extends Plugin implements Runnable {
// 2. Modul-System starten // 2. Modul-System starten
moduleManager = new ModuleManager(); moduleManager = new ModuleManager();
// 3. MODULE REGISTRIEREN (Hier erweiterst du die API in Zukunft!) // 3. MODULE REGISTRIEREN
// 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 StatsModule()); // Statistik System laden
moduleManager.registerModule(new VerifyModule()); // Verify Modul moduleManager.registerModule(new VerifyModule()); // Verify Modul
moduleManager.registerModule(new GlobalChatModule()); // GlobalChat moduleManager.registerModule(new GlobalChatModule()); // GlobalChat
moduleManager.registerModule(new NavigationModule()); //Server Switcher moduleManager.registerModule(new NavigationModule()); //Server Switcher
// Broadcast Modul registrieren (neu)
moduleManager.registerModule(new net.viper.status.modules.broadcast.BroadcastModule());
// 4. Alle Module aktivieren // 4. Alle Module aktivieren
moduleManager.enableAll(this); moduleManager.enableAll(this);
@@ -78,7 +86,7 @@ public class StatusAPI extends Plugin implements Runnable {
if (backupFile.exists()) { if (backupFile.exists()) {
ProxyServer.getInstance().getScheduler().schedule(this, () -> { ProxyServer.getInstance().getScheduler().schedule(this, () -> {
if (backupFile.exists()) backupFile.delete(); if (backupFile.exists()) backupFile.delete();
}, 1, TimeUnit.MINUTES); },1, TimeUnit.MINUTES);
} }
// Sofortiger Check // Sofortiger Check
@@ -184,11 +192,187 @@ 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) { private void handleConnection(Socket clientSocket) {
try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), "UTF-8")); try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), "UTF-8"));
OutputStream out = clientSocket.getOutputStream()) { OutputStream out = clientSocket.getOutputStream()) {
String inputLine = in.readLine(); 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<String, String> 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")) { if (inputLine != null && inputLine.startsWith("GET")) {
Map<String, Object> data = new LinkedHashMap<>(); Map<String, Object> data = new LinkedHashMap<>();
data.put("online", true); data.put("online", true);
@@ -267,6 +451,64 @@ 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 + "\"";
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<String, Object> data) { private String buildJsonString(Map<String, Object> data) {
StringBuilder sb = new StringBuilder("{"); StringBuilder sb = new StringBuilder("{");
boolean first = true; boolean first = true;