src/main/java/net/viper/status/StatusAPI.java aktualisiert
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user