8 Commits
3.6.1 ... main

8 changed files with 467 additions and 404 deletions

View File

@@ -559,8 +559,12 @@ function mcss_fetch_server_with_ranks($srv) {
// 2. Namen immer Schwarz darstellen (überschreibt eventuelle Farben aus dem Prefix) // 2. Namen immer Schwarz darstellen (überschreibt eventuelle Farben aus dem Prefix)
$name_html = '<span style="color:black;">' . esc_html($name) . '</span>'; $name_html = '<span style="color:black;">' . esc_html($name) . '</span>';
// 3. Zusammenbauen // 3. Zusammenbauen (Nur Abstand, wenn Prefix existiert)
$display_html = trim($prefix_html) . ' ' . $name_html; if (!empty($prefix_html)) {
$display_html = $prefix_html . ' ' . $name_html;
} else {
$display_html = $name_html;
}
$players_info[] = [ $players_info[] = [
'name' => $name, 'name' => $name,

View File

@@ -2,6 +2,10 @@
StatusAPI zeigt den aktuellen Status deines Minecraft-Servers direkt auf deiner Webseite an inklusive Online/Offline, Version, Ping, Spieleranzahl und Spieler-Avatare. StatusAPI zeigt den aktuellen Status deines Minecraft-Servers direkt auf deiner Webseite an inklusive Online/Offline, Version, Ping, Spieleranzahl und Spieler-Avatare.
**StatusAPI Repository:** [https://git.viper.ipv64.net/M_Viper/StatusAPI](https://git.viper.ipv64.net/M_Viper/StatusAPI)
---
## Features ## Features
- Zeigt Serverstatus: **Online / Offline** - Zeigt Serverstatus: **Online / Offline**
@@ -12,26 +16,67 @@ StatusAPI zeigt den aktuellen Status deines Minecraft-Servers direkt auf deiner
- Hinweise/Banner können vom Nutzer geschlossen werden - Hinweise/Banner können vom Nutzer geschlossen werden
- Anpassbare Darstellung für verschiedene Layouts - Anpassbare Darstellung für verschiedene Layouts
---
## Wichtige Hinweise ## Wichtige Hinweise
### BungeeCord Plugin ### BungeeCord Plugin (Voraussetzung)
⚠️ **Die [StatusAPI](https://git.viper.ipv64.net/M_Viper/StatusAPI) MUSS auf deinem BungeeCord-Server installiert sein!**
Das Plugin **StatusAPI.jar** muss in den BungeeCord Plugin-Ordner kopiert werden, damit die API korrekt funktioniert. Das Plugin **StatusAPI.jar** muss in den BungeeCord Plugin-Ordner kopiert werden, damit die API korrekt funktioniert.
```text
BungeeCord/
├─ plugins/
│ └─ StatusAPI.jar ← PFLICHT
└─ config.yml
```
**Download:** [https://git.viper.ipv64.net/M_Viper/StatusAPI](https://git.viper.ipv64.net/M_Viper/StatusAPI)
### WordPress Integration ### WordPress Integration
In WordPress muss der API-Port auf **9191** eingestellt sein, damit die Serverdaten korrekt abgerufen werden. In WordPress muss der API-Port auf **9191** eingestellt sein, damit die Serverdaten korrekt abgerufen werden.
Stelle sicher, dass dein Server und die Webseite auf diesen Port zugreifen können. Stelle sicher, dass dein Server und die Webseite auf diesen Port zugreifen können.
Die StatusAPI stellt einen HTTP-Endpunkt bereit (Standard: `http://localhost:9191`), den das WordPress-Plugin abfragt.
### Shortcode für WordPress ### Shortcode für WordPress
Um den Serverstatus auf deiner WordPress-Seite anzuzeigen, füge einfach folgenden Shortcode ein: Um den Serverstatus auf deiner WordPress-Seite anzuzeigen, füge einfach folgenden Shortcode ein:
```html ```html
[bungeecord_status id="Bungeecord"] [bungeecord_status id="Bungeecord"]
``` ```
Ersetze `"Bungeecord"` durch die ID deines Servers, falls mehrere Server eingebunden werden. Ersetze `"Bungeecord"` durch die ID deines Servers, falls mehrere Server eingebunden werden.
---
## Installation
### Schritt 1: StatusAPI auf BungeeCord installieren
1. Lade die **[StatusAPI](https://git.viper.ipv64.net/M_Viper/StatusAPI)** herunter
2. Kopiere die **StatusAPI.jar** in den `plugins`-Ordner deines BungeeCord-Servers
3. Starte den BungeeCord-Server neu
4. Die API läuft nun standardmäßig auf Port **9191**
### Schritt 2: WordPress-Plugin einrichten
1. Installiere das WordPress-Plugin für den BungeeCord-Status
2. Gehe zu den Plugin-Einstellungen
3. Trage die **StatusAPI URL** ein (z.B. `http://localhost:9191`)
4. Speichere die Einstellungen
### Schritt 3: Shortcode einbinden
Füge den Shortcode auf einer beliebigen WordPress-Seite ein:
```html
[bungeecord_status id="Bungeecord"]
```
---
## Beispielanzeige ## Beispielanzeige
**Status:** Online **Status:** Online
@@ -42,17 +87,52 @@ Ersetze `"Bungeecord"` durch die ID deines Servers, falls mehrere Server eingebu
**Avatare:** **Avatare:**
![M_Viper Avatar](https://example.com/avatar/M_Viper.png) <!-- Platzhalter wird im Plugin dynamisch geladen --> ![M_Viper Avatar](https://example.com/avatar/M_Viper.png) <!-- Platzhalter wird im Plugin dynamisch geladen -->
> Der Abstand zwischen dem Text „Spieler: und den Avataren ist bewusst etwas größer, um die Übersichtlichkeit zu erhöhen. > Der Abstand zwischen dem Text „Spieler:" und den Avataren ist bewusst etwas größer, um die Übersichtlichkeit zu erhöhen.
---
## Nutzung ## Nutzung
1. Shortcode an der gewünschten Stelle einfügen 1. Shortcode an der gewünschten Stelle einfügen
2. Der Serverstatus aktualisiert sich automatisch, sodass die angezeigten Spieler und der Ping immer aktuell sind 2. Der Serverstatus aktualisiert sich automatisch, sodass die angezeigten Spieler und der Ping immer aktuell sind
3. Alle Daten werden live von der StatusAPI bezogen
---
## Technische Details
- **API-Port:** 9191 (Standard)
- **Update-Intervall:** 2 Sekunden (automatisch)
- **Protokoll:** HTTP/JSON
- **Datenquelle:** StatusAPI auf BungeeCord
---
## Support ## Support
Bei Problemen überprüfe bitte: Bei Problemen überprüfe bitte:
- Ob das Plugin **StatusAPI.jar** korrekt im BungeeCord Plugin-Ordner liegt - Ob das Plugin **[StatusAPI.jar](https://git.viper.ipv64.net/M_Viper/StatusAPI)** korrekt im BungeeCord Plugin-Ordner liegt
- Ob der API-Port in WordPress korrekt auf **9191** eingestellt ist - Ob der API-Port in WordPress korrekt auf **9191** eingestellt ist
- Ob der Server erreichbar ist und Spieler online sind - Ob der BungeeCord-Server läuft und die StatusAPI erreichbar ist
- Ob keine Firewall den Port 9191 blockiert
- Ob die StatusAPI gültige JSON-Daten zurückliefert
### Häufige Fehler
**"Server offline" obwohl Server läuft**
- StatusAPI ist nicht installiert oder läuft nicht
- Falscher Port in WordPress eingestellt
- Firewall blockiert Port 9191
**Keine Spieler werden angezeigt**
- StatusAPI liefert keine Spielerdaten
- API-Verbindung fehlgeschlagen
- WordPress kann die API-URL nicht erreichen
---
## Weitere Informationen
Für detaillierte Informationen zur Installation, Konfiguration und Fehlerbehebung der StatusAPI:
**[https://git.viper.ipv64.net/M_Viper/StatusAPI](https://git.viper.ipv64.net/M_Viper/StatusAPI)**

View File

@@ -7,7 +7,7 @@
<groupId>net.viper.bungee</groupId> <groupId>net.viper.bungee</groupId>
<artifactId>StatusAPI</artifactId> <artifactId>StatusAPI</artifactId>
<version>1.1</version> <version>3.6.2</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>StatusAPI</name> <name>StatusAPI</name>
@@ -20,7 +20,7 @@
</properties> </properties>
<dependencies> <dependencies>
<!-- BungeeCord API (lokal installiert) --> <!-- BungeeCord API -->
<dependency> <dependency>
<groupId>net.md-5</groupId> <groupId>net.md-5</groupId>
<artifactId>bungeecord-api</artifactId> <artifactId>bungeecord-api</artifactId>
@@ -28,12 +28,13 @@
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<!-- LuckPerms API für Prefix Support --> <!-- LuckPerms API (Optional) -->
<dependency> <dependency>
<groupId>net.luckperms</groupId> <groupId>net.luckperms</groupId>
<artifactId>api</artifactId> <artifactId>api</artifactId>
<version>5.4</version> <version>5.4</version>
<scope>provided</scope> <scope>provided</scope>
<optional>true</optional>
</dependency> </dependency>
</dependencies> </dependencies>

View File

@@ -0,0 +1,59 @@
package net.viper.status;
import net.md_5.bungee.api.plugin.Plugin;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.util.function.Consumer;
/**
* FileDownloader: Lädt Dateien asynchron herunter (CMILib Style).
*/
public class FileDownloader {
private final Plugin plugin;
public FileDownloader(Plugin plugin) {
this.plugin = plugin;
}
/**
* Lädt eine Datei herunter.
* @param urlString Die Download URL
* @param destination Die Zieldatei
* @param onSuccess Callback, der im Hauptthread ausgeführt wird, wenn fertig.
*/
public void downloadFile(String urlString, File destination, Runnable onSuccess) {
plugin.getProxy().getScheduler().runAsync(plugin, () -> {
BufferedInputStream bufferedInputStream = null;
FileOutputStream fileOutputStream = null;
try {
URL url = new URL(urlString);
bufferedInputStream = new BufferedInputStream(url.openStream());
fileOutputStream = new FileOutputStream(destination);
byte[] buffer = new byte[1024];
int count;
while ((count = bufferedInputStream.read(buffer, 0, 1024)) != -1) {
fileOutputStream.write(buffer, 0, count);
}
// Schließen
fileOutputStream.close();
bufferedInputStream.close();
// Callback im Main Thread
plugin.getProxy().getScheduler().schedule(plugin, onSuccess, 1, java.util.concurrent.TimeUnit.MILLISECONDS);
} catch (Throwable e) {
plugin.getLogger().warning("Download fehlgeschlagen: " + e.getMessage());
if (destination.exists()) destination.delete();
} finally {
if (fileOutputStream != null) try { fileOutputStream.close(); } catch (IOException ignored) {}
if (bufferedInputStream != null) try { bufferedInputStream.close(); } catch (IOException ignored) {}
}
});
}
}

View File

@@ -1,15 +1,16 @@
package net.viper.status; package net.viper.status;
import net.luckperms.api.LuckPerms;
import net.luckperms.api.LuckPermsProvider;
import net.luckperms.api.model.user.User;
import net.luckperms.api.query.QueryOptions;
import net.md_5.bungee.api.ProxyServer; import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.config.ListenerInfo; import net.md_5.bungee.api.config.ListenerInfo;
import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.plugin.Plugin; import net.md_5.bungee.api.plugin.Plugin;
import java.io.*; 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.ServerSocket;
import java.net.Socket; import java.net.Socket;
import java.util.*; import java.util.*;
@@ -20,6 +21,7 @@ public class StatusAPI extends Plugin implements Runnable {
private Thread thread; private Thread thread;
private int port = 9191; private int port = 9191;
private UpdateChecker updateChecker; private UpdateChecker updateChecker;
private FileDownloader fileDownloader;
@Override @Override
public void onEnable() { public void onEnable() {
@@ -30,18 +32,33 @@ public class StatusAPI extends Plugin implements Runnable {
thread = new Thread(this, "StatusAPI-HTTP-Server"); thread = new Thread(this, "StatusAPI-HTTP-Server");
thread.start(); thread.start();
// Start UpdateChecker: initialer Check (async) + regelmäßiger Schedule
String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0"; String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0";
updateChecker = new UpdateChecker(this, currentVersion, 6); // 6 Stunden Intervall updateChecker = new UpdateChecker(this, currentVersion, 6); // 6 Stunden Intervall
fileDownloader = new FileDownloader(this);
// initialer sofortiger Start (async) File pluginFile = getFile();
ProxyServer.getInstance().getScheduler().runAsync(this, () -> updateChecker.checkNow()); File backupFile = new File(pluginFile.getParentFile(), "StatusAPI.jar.bak");
// planmäßiger Intervall (alle 6 Stunden) // --- AUTOMATISCHES BACKUP-CLEANUP ---
ProxyServer.getInstance().getScheduler().schedule(this, updateChecker, 6, 6, TimeUnit.HOURS); // Falls ein altes Update (.bak) im Ordner liegt, löschen wir es nach 1 Minute Startzeit.
// Wenn der Server kurz crashen würde, hast du noch 60 Sekunden Zeit, ihn zu stoppen,
// damit das Backup erhalten bleibt. Läuft er stabil, wird der Platz freigegeben.
if (backupFile.exists()) {
ProxyServer.getInstance().getScheduler().schedule(this, () -> {
if (backupFile.exists()) {
if (backupFile.delete()) {
getLogger().info("Altes Backup (.bak) wurde erfolgreich gelöscht.");
}
}
}, 1, TimeUnit.MINUTES);
}
// ---------------------------------------
// Register join listener to notify OPs on login // Sofortiger Start-Check
ProxyServer.getInstance().getPluginManager().registerListener(this, new UpdateListener(this, updateChecker)); checkAndMaybeUpdate();
// Regelmäßiger Check (alle 6 Stunden)
ProxyServer.getInstance().getScheduler().schedule(this, this::checkAndMaybeUpdate, 6, 6, TimeUnit.HOURS);
} }
@Override @Override
@@ -49,9 +66,99 @@ public class StatusAPI extends Plugin implements Runnable {
getLogger().info("Stoppe Web-Server..."); getLogger().info("Stoppe Web-Server...");
if (thread != null) { if (thread != null) {
thread.interrupt(); thread.interrupt();
try { thread.join(1000); } catch (InterruptedException ignored) {}
}
}
/**
* Prüft Update und startet Download falls nötig.
*/
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("----------------------------------------");
// Download Starten
File pluginFile = getFile();
File newFile = new File(pluginFile.getParentFile(), "StatusAPI.jar.new");
fileDownloader.downloadFile(url, newFile, () -> {
// Callback: Wenn Download erfolgreich
triggerUpdateScript(pluginFile, newFile);
});
}
} catch (Exception e) {
getLogger().severe("Fehler beim Update-Check: " + e.getMessage());
}
}
/**
* Erstellt ein externes Batch-Skript, startet es und stoppt den Server.
* Das Skript führt den Datei-Tausch durch, wenn der Server weg ist.
*/
private void triggerUpdateScript(File currentFile, File newFile) {
try {
File pluginsFolder = currentFile.getParentFile();
// Wir legen das Skript neben die Haupt-JAR (root) damit es nicht im Plugin-Ordner liegt
File rootFolder = pluginsFolder.getParentFile();
File batFile = new File(rootFolder, "StatusAPI_Update_" + System.currentTimeMillis() + ".bat");
// Batch Inhalt
// 1. Wartet 5 Sekunden (Server fährt runter)
// 2. Geht in den Plugin Ordner
// 3. Sichert .jar zu .bak
// 4. Benennt .new zu .jar um
// 5. Löscht sich selbst
String batContent = "@echo off\n" +
"echo Bitte warten, der Server f\"ahrt 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);
}
// Spieler kicken
try { try {
thread.join(1000); for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
} catch (InterruptedException ignored) {} p.disconnect("§cServer f\"ahrt f\"ur ein Update neu herunter. Bitte etwas warten.");
}
} catch (Exception ignored) {}
// Skript starten
getLogger().info("Starte Update-Skript im Hintergrund...");
try {
Runtime.getRuntime().exec("cmd /c start \"Update_Proc\" \"" + batFile.getAbsolutePath() + "\"");
} catch (IOException e) {
getLogger().warning("Konnte Skript nicht starten. Update wird manuell ben\"otigt.");
}
// Server stoppen
ProxyServer.getInstance().stop();
} catch (Exception e) {
getLogger().severe("Fehler beim Vorbereiten des Updates: " + e.getMessage());
} }
} }
@@ -59,189 +166,124 @@ public class StatusAPI extends Plugin implements Runnable {
public void run() { public void run() {
try (ServerSocket serverSocket = new ServerSocket(port)) { try (ServerSocket serverSocket = new ServerSocket(port)) {
serverSocket.setSoTimeout(1000); serverSocket.setSoTimeout(1000);
while (!Thread.interrupted()) { while (!Thread.interrupted()) {
try { try {
Socket clientSocket = serverSocket.accept(); Socket clientSocket = serverSocket.accept();
handleConnection(clientSocket); handleConnection(clientSocket);
} catch (java.net.SocketTimeoutException e) { } catch (java.net.SocketTimeoutException e) {}
// Loop Check (timeout) - erlaubt Unterbrechungsschleife catch (IOException e) {
} catch (IOException e) {
getLogger().warning("Fehler beim Akzeptieren einer Verbindung: " + e.getMessage()); getLogger().warning("Fehler beim Akzeptieren einer Verbindung: " + e.getMessage());
} }
} }
} catch (IOException e) { } catch (IOException e) {
getLogger().severe("Konnte ServerSocket nicht starten auf Port " + port + ": " + e.getMessage()); getLogger().severe("Konnte ServerSocket nicht starten auf Port " + port + ": " + e.getMessage());
e.printStackTrace();
} }
} }
private void handleConnection(Socket clientSocket) { private void handleConnection(Socket clientSocket) {
BufferedReader in = null; try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), "UTF-8"));
OutputStream out = null; OutputStream out = clientSocket.getOutputStream()) {
try {
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), "UTF-8"));
out = clientSocket.getOutputStream();
String inputLine = in.readLine(); String inputLine = in.readLine();
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);
// --- VERSION CLEANUP START --- // Version
String versionRaw = ProxyServer.getInstance().getVersion(); String versionRaw = ProxyServer.getInstance().getVersion();
String versionClean = versionRaw; String versionClean = versionRaw;
if (versionRaw != null && versionRaw.contains(":")) { if (versionRaw != null && versionRaw.contains(":")) {
String[] parts = versionRaw.split(":"); String[] parts = versionRaw.split(":");
if (parts.length >= 3) { if (parts.length >= 3) versionClean = parts[2].trim();
versionClean = parts[2].trim();
}
} }
data.put("version", versionClean); data.put("version", versionClean);
// --- VERSION CLEANUP ENDE ---
data.put("max_players", String.valueOf(ProxyServer.getInstance().getConfig().getPlayerLimit())); data.put("max_players", String.valueOf(ProxyServer.getInstance().getConfig().getPlayerLimit()));
// Motd
String motd = "BungeeCord"; String motd = "BungeeCord";
try { try {
Iterator<ListenerInfo> it = ProxyServer.getInstance().getConfig().getListeners().iterator(); Iterator<ListenerInfo> it = ProxyServer.getInstance().getConfig().getListeners().iterator();
if (it.hasNext()) { if (it.hasNext()) {
ListenerInfo listener = it.next(); ListenerInfo listener = it.next();
if (listener != null && listener.getMotd() != null) { if (listener != null && listener.getMotd() != null) motd = listener.getMotd();
motd = listener.getMotd();
}
} }
} catch (Exception e) { } catch (Exception ignored) {}
// Fallback bleibt "BungeeCord"
}
data.put("motd", motd); data.put("motd", motd);
// --- LUCKPERMS INTEGRATION START --- // LuckPerms (Optional)
// LuckPerms API über Provider holen (Verhindert ClassCastException) boolean luckPermsEnabled = ProxyServer.getInstance().getPluginManager().getPlugin("LuckPerms") != null;
LuckPerms luckPermsApi = null;
try {
luckPermsApi = LuckPermsProvider.get();
} catch (IllegalStateException e) {
// LuckPerms ist nicht geladen
}
List<Map<String, String>> playersList = new ArrayList<>(); List<Map<String, String>> playersList = new ArrayList<>();
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
Map<String, String> playerInfo = new LinkedHashMap<>(); Map<String, String> playerInfo = new LinkedHashMap<>();
playerInfo.put("name", p.getName()); playerInfo.put("name", p.getName());
// Prefix abfragen, falls LP gefunden wurde
String prefix = ""; String prefix = "";
if (luckPermsApi != null) { if (luckPermsEnabled) {
User user = luckPermsApi.getUserManager().getUser(p.getUniqueId()); try {
if (user != null) { Class<?> providerClass = Class.forName("net.luckperms.api.LuckPermsProvider");
// Context bestimmen (Global oder Server-spezifisch) Object luckPermsApi = providerClass.getMethod("get").invoke(null);
QueryOptions queryOptions = luckPermsApi.getContextManager().getQueryOptions(user) Object userManager = luckPermsApi.getClass().getMethod("getUserManager").invoke(luckPermsApi);
.orElse(QueryOptions.defaultContextualOptions()); Object user = userManager.getClass().getMethod("getUser", UUID.class).invoke(userManager, p.getUniqueId());
if (user != null) {
String lpPrefix = user.getCachedData().getMetaData(queryOptions).getPrefix(); Class<?> queryOptionsClass = Class.forName("net.luckperms.api.query.QueryOptions");
if (lpPrefix != null) { Object queryOptions = queryOptionsClass.getMethod("defaultContextualOptions").invoke(null);
prefix = lpPrefix; 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); playerInfo.put("prefix", prefix);
playersList.add(playerInfo); playersList.add(playerInfo);
} }
data.put("players", playersList); data.put("players", playersList);
// --- LUCKPERMS INTEGRATION ENDE ---
// Response
String json = buildJsonString(data); String json = buildJsonString(data);
byte[] jsonBytes = json.getBytes("UTF-8"); byte[] jsonBytes = json.getBytes("UTF-8");
// HTTP Response mit korrekter Byte-Length
StringBuilder response = new StringBuilder(); StringBuilder response = new StringBuilder();
response.append("HTTP/1.1 200 OK\r\n"); response.append("HTTP/1.1 200 OK\r\n");
response.append("Content-Type: application/json; charset=UTF-8\r\n"); response.append("Content-Type: application/json; charset=UTF-8\r\n");
response.append("Access-Control-Allow-Origin: *\r\n"); response.append("Access-Control-Allow-Origin: *\r\n");
response.append("Content-Length: ").append(jsonBytes.length).append("\r\n"); response.append("Content-Length: ").append(jsonBytes.length).append("\r\n");
response.append("Connection: close\r\n"); response.append("Connection: close\r\n\r\n");
response.append("\r\n");
// Header senden
out.write(response.toString().getBytes("UTF-8")); out.write(response.toString().getBytes("UTF-8"));
// Body senden
out.write(jsonBytes); out.write(jsonBytes);
out.flush(); out.flush();
} else {
// Ungültige Anfrage -> 400
String resp = "HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n";
out.write(resp.getBytes("UTF-8"));
out.flush();
} }
} catch (Exception e) { } catch (Exception e) {
getLogger().severe("Fehler beim Verarbeiten der Anfrage: " + e.getMessage()); getLogger().severe("Fehler beim Verarbeiten der Anfrage: " + e.getMessage());
e.printStackTrace();
} finally {
// Sauber aufräumen
try {
if (out != null) out.close();
if (in != null) in.close();
if (clientSocket != null && !clientSocket.isClosed()) {
clientSocket.close();
}
} catch (IOException e) {
// Ignorieren
}
} }
} }
/**
* Rekursiver JSON Builder, der nun auch Listen von Objekten (Maps) verarbeiten kann.
*/
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;
for (Map.Entry<String, Object> entry : data.entrySet()) { for (Map.Entry<String, Object> entry : data.entrySet()) {
if (!first) sb.append(","); if (!first) sb.append(",");
first = false; first = false;
sb.append("\"").append(escapeJson(entry.getKey())).append("\":"); sb.append("\"").append(escapeJson(entry.getKey())).append("\":").append(valueToString(entry.getValue()));
// Wert verarbeiten (String, Boolean, List oder Map)
sb.append(valueToString(entry.getValue()));
} }
sb.append("}"); sb.append("}");
return sb.toString(); return sb.toString();
} }
/**
* Hilfsmethode, um verschiedene Objekttypen in JSON-Strings umzuwandeln.
*/
private String valueToString(Object value) { private String valueToString(Object value) {
if (value == null) { if (value == null) return "null";
return "null"; else if (value instanceof Boolean) return value.toString();
} else if (value instanceof Boolean) { else if (value instanceof List) {
return value.toString();
} else if (value instanceof List) {
StringBuilder sb = new StringBuilder("["); StringBuilder sb = new StringBuilder("[");
List<?> list = (List<?>) value; List<?> list = (List<?>) value;
for (int i = 0; i < list.size(); i++) { for (int i = 0; i < list.size(); i++) {
if (i > 0) sb.append(","); if (i > 0) sb.append(",");
Object item = list.get(i); Object item = list.get(i);
// Wenn es eine Map ist (Player-Objekt), rufen wir buildJsonString rekursiv auf if (item instanceof Map) sb.append(buildJsonString((Map<String, Object>) item));
if (item instanceof Map) { else sb.append("\"").append(escapeJson(String.valueOf(item))).append("\"");
sb.append(buildJsonString((Map<String, Object>) item));
} else {
// Andernfalls String behandeln
sb.append("\"").append(escapeJson(String.valueOf(item))).append("\"");
}
} }
sb.append("]"); sb.append("]");
return sb.toString(); return sb.toString();
} else {
// Standard String Behandlung
return "\"" + escapeJson(String.valueOf(value)) + "\"";
} }
else return "\"" + escapeJson(String.valueOf(value)) + "\"";
} }
private String escapeJson(String s) { private String escapeJson(String s) {

View File

@@ -1,211 +1,151 @@
package net.viper.status; package net.viper.status;
import net.md_5.bungee.api.plugin.Plugin; import net.md_5.bungee.api.plugin.Plugin;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.connection.ProxiedPlayer; import java.io.BufferedReader;
import net.md_5.bungee.api.chat.TextComponent; import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.io.BufferedReader; import java.net.URL;
import java.io.InputStreamReader; import java.util.logging.Level;
import java.net.HttpURLConnection; import java.util.regex.Matcher;
import java.net.URL; import java.util.regex.Pattern;
import java.util.logging.Level;
import java.util.regex.Matcher; public class UpdateChecker {
import java.util.regex.Pattern;
private final Plugin plugin;
/** private final String currentVersion;
* UpdateChecker: private final int intervalHours;
* - Fragt die Gitea Releases API ab
* - Sucht in den Releases nach dem Asset "StatusAPI.jar" private final String apiUrl = "https://git.viper.ipv64.net/api/v1/repos/M_Viper/Minecraft-BungeeCord-Status/releases";
* - Extrahiert die Version (tag_name bevorzugt, name als Fallback)
* - Cache die gefundene Version + URL (volatile fields) private volatile String latestVersion = "";
*/ private volatile String latestUrl = "";
public class UpdateChecker implements Runnable {
// Pattern für Dateinamen im Assets-Array
private final Plugin plugin; private static final Pattern ASSET_NAME_PATTERN = Pattern.compile("\"name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
private final String currentVersion; // Pattern für Download-URL
private final int intervalHours; private static final Pattern DOWNLOAD_PATTERN = Pattern.compile("\"browser_download_url\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
// Pattern für Tag Version (WICHTIG: Wir suchen global, um das Haupt-Release zu finden)
// Gitea Releases API für dein Repo private static final Pattern TAG_NAME_PATTERN = Pattern.compile("\"tag_name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
private final String apiUrl = "https://git.viper.ipv64.net/api/v1/repos/M_Viper/Minecraft-BungeeCord-Status/releases";
public UpdateChecker(Plugin plugin, String currentVersion, int intervalHours) {
// cached results (volatile für Thread-Sichtbarkeit) this.plugin = plugin;
private volatile String latestVersion = ""; this.currentVersion = currentVersion != null ? currentVersion : "0.0.0";
private volatile String latestUrl = ""; this.intervalHours = Math.max(1, intervalHours);
}
// Patterns for quick parse (we parse JSON as text; this is lightweight and robust for our needs)
private static final Pattern ASSET_NAME_PATTERN = Pattern.compile("\"name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE); public void checkNow() {
private static final Pattern DOWNLOAD_PATTERN = Pattern.compile("\"browser_download_url\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE); try {
private static final Pattern TAG_NAME_PATTERN = Pattern.compile("\"tag_name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE); HttpURLConnection conn = (HttpURLConnection) new URL(apiUrl).openConnection();
private static final Pattern NAME_FIELD_PATTERN = Pattern.compile("\"name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE); conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
public UpdateChecker(Plugin plugin, String currentVersion, int intervalHours) { conn.setRequestProperty("User-Agent", "StatusAPI-UpdateChecker/2.0");
this.plugin = plugin; conn.setConnectTimeout(5000);
this.currentVersion = currentVersion != null ? currentVersion : "0.0.0"; conn.setReadTimeout(5000);
this.intervalHours = Math.max(1, intervalHours);
} int code = conn.getResponseCode();
if (code != 200) {
@Override plugin.getLogger().warning("Gitea API nicht erreichbar (HTTP " + code + ")");
public void run() { return;
// scheduled task calls checkNow }
checkNow();
} StringBuilder sb = new StringBuilder();
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"))) {
/** String line;
* Führt einen sofortigen Check aus und updated cache (synchron for the calling thread). while ((line = br.readLine()) != null) sb.append(line).append("\n");
*/ }
public void checkNow() {
try { String body = sb.toString();
plugin.getLogger().info("Prüfe StatusAPI Releases via Gitea API...");
// 1. Die LATEST Version (Tag) finden
HttpURLConnection conn = (HttpURLConnection) new URL(apiUrl).openConnection(); // Wir suchen das erste "tag_name" im gesamten JSON, das ist meistens das neueste Release.
conn.setRequestMethod("GET"); String foundVersion = null;
conn.setRequestProperty("Accept", "application/json"); Matcher tagM = TAG_NAME_PATTERN.matcher(body);
conn.setRequestProperty("User-Agent", "StatusAPI-UpdateChecker/1.0"); if (tagM.find()) {
conn.setConnectTimeout(5000); foundVersion = tagM.group(1).trim();
conn.setReadTimeout(5000); }
int code = conn.getResponseCode(); if (foundVersion == null) {
if (code != 200) { plugin.getLogger().warning("Keine Version (Tag) im Release gefunden.");
plugin.getLogger().warning("Gitea API nicht erreichbar (HTTP " + code + ")"); return;
return; }
}
// Version säubern (v vorne entfernen)
StringBuilder sb = new StringBuilder(); if (foundVersion.startsWith("v") || foundVersion.startsWith("V")) {
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"))) { foundVersion = foundVersion.substring(1);
String line; }
while ((line = br.readLine()) != null) {
sb.append(line).append("\n"); // 2. Download URL für StatusAPI.jar finden
} // Wir suchen das Asset "StatusAPI.jar"
} String foundUrl = null;
String body = sb.toString(); Pattern releasePattern = Pattern.compile("(?s)\\{.*?\\}");
Matcher releaseMatcher = releasePattern.matcher(body);
// Suche Release mit Asset "StatusAPI.jar" while (releaseMatcher.find()) {
String foundVersion = null; String block = releaseMatcher.group();
String foundUrl = null; java.util.List<String> names = new java.util.ArrayList<>();
java.util.List<String> downloads = new java.util.ArrayList<>();
// Split releases by top-level objects: we simply find repeating {...} blocks and inspect them
// This is robust enough for Gitea JSON responses. Matcher nm = ASSET_NAME_PATTERN.matcher(block);
Pattern releasePattern = Pattern.compile("(?s)\\{.*?\\}"); while (nm.find()) names.add(nm.group(1));
Matcher releaseMatcher = releasePattern.matcher(body);
while (releaseMatcher.find()) { Matcher dm = DOWNLOAD_PATTERN.matcher(block);
String block = releaseMatcher.group(); while (dm.find()) downloads.add(dm.group(1));
// extract asset names & download urls inside the block int pairs = Math.min(names.size(), downloads.size());
java.util.List<String> names = new java.util.ArrayList<>(); for (int i = 0; i < pairs; i++) {
java.util.List<String> downloads = new java.util.ArrayList<>(); String name = names.get(i);
String dl = downloads.get(i);
Matcher nm = ASSET_NAME_PATTERN.matcher(block); if ("StatusAPI.jar".equalsIgnoreCase(name.trim())) {
while (nm.find()) names.add(nm.group(1)); foundUrl = dl;
break;
Matcher dm = DOWNLOAD_PATTERN.matcher(block); }
while (dm.find()) downloads.add(dm.group(1)); }
if (foundUrl != null) break;
int pairs = Math.min(names.size(), downloads.size()); }
for (int i = 0; i < pairs; i++) {
String name = names.get(i); if (foundUrl == null) {
String dl = downloads.get(i); plugin.getLogger().warning("Keine JAR-Datei für dieses Release gefunden.");
if ("StatusAPI.jar".equalsIgnoreCase(name.trim())) { return;
// find tag_name or name field in the same block }
Matcher tagM = TAG_NAME_PATTERN.matcher(block);
if (tagM.find()) { plugin.getLogger().info("Gefundene Version: " + foundVersion + " (Aktuell: " + currentVersion + ")");
foundVersion = tagM.group(1).trim();
} else { latestVersion = foundVersion;
Matcher nameFieldM = NAME_FIELD_PATTERN.matcher(block); latestUrl = foundUrl;
if (nameFieldM.find()) {
foundVersion = nameFieldM.group(1).trim(); } catch (Exception e) {
} plugin.getLogger().log(Level.SEVERE, "Fehler beim Update-Check: " + e.getMessage(), e);
} }
foundUrl = dl; }
break;
} public String getLatestVersion() {
} return latestVersion != null ? latestVersion : "";
}
if (foundUrl != null) break;
} public String getLatestUrl() {
return latestUrl != null ? latestUrl : "";
if (foundVersion == null || foundUrl == null) { }
plugin.getLogger().info("Kein Release mit Asset 'StatusAPI.jar' gefunden.");
return; public boolean isUpdateAvailable(String currentVer) {
} String lv = getLatestVersion();
if (lv.isEmpty()) return false;
// Normalize version return compareVersions(lv, currentVer) > 0;
if (foundVersion.startsWith("v") || foundVersion.startsWith("V")) { }
foundVersion = foundVersion.substring(1);
} private int compareVersions(String a, String b) {
try {
plugin.getLogger().info("Gefundene Version: " + foundVersion + " (aktuell: " + currentVersion + ")"); String[] aa = a.split("\\.");
String[] bb = b.split("\\.");
// set cache int len = Math.max(aa.length, bb.length);
latestVersion = foundVersion; for (int i = 0; i < len; i++) {
latestUrl = foundUrl; int ai = i < aa.length ? Integer.parseInt(aa[i].replaceAll("\\D","")) : 0;
int bi = i < bb.length ? Integer.parseInt(bb[i].replaceAll("\\D","")) : 0;
} catch (Exception e) { if (ai != bi) return Integer.compare(ai, bi);
plugin.getLogger().log(Level.SEVERE, "Fehler beim Update-Check: " + e.getMessage(), e); }
} return 0;
} } catch (Exception ex) {
return a.compareTo(b);
/** }
* Gibt die zuletzt gecachte Version (oder empty string). }
*/ }
public String getLatestVersion() {
return latestVersion != null ? latestVersion : "";
}
/**
* Gibt die zuletzt gecachte download-URL (oder empty string).
*/
public String getLatestUrl() {
return latestUrl != null ? latestUrl : "";
}
/**
* Prüft ob die gecachte latestVersion größer ist als übergebene version.
*/
public boolean isUpdateAvailable(String currentVer) {
String lv = getLatestVersion();
if (lv.isEmpty()) return false;
return compareVersions(lv, currentVer) > 0;
}
/**
* Einfacher SemVer-Vergleich (1.2.3). Liefert >0 wenn a>b, 0 wenn gleich, <0 wenn a<b.
*/
private int compareVersions(String a, String b) {
try {
String[] aa = a.split("\\.");
String[] bb = b.split("\\.");
int len = Math.max(aa.length, bb.length);
for (int i = 0; i < len; i++) {
int ai = i < aa.length ? Integer.parseInt(aa[i].replaceAll("\\D","")) : 0;
int bi = i < bb.length ? Integer.parseInt(bb[i].replaceAll("\\D","")) : 0;
if (ai != bi) return Integer.compare(ai, bi);
}
return 0;
} catch (Exception ex) {
return a.compareTo(b);
}
}
/**
* Convenience: prüft cached state und falls update vorhanden, notifies online players with given permission(s).
*/
public void notifyOnlineOpsIfAvailable(String[] perms) {
String lv = getLatestVersion();
String url = getLatestUrl();
if (lv.isEmpty()) return;
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
try {
for (String perm : perms) {
if (p.hasPermission(perm)) {
p.sendMessage(new TextComponent("§6[StatusAPI] §eNeue Version verfügbar: §a" + lv));
p.sendMessage(new TextComponent("§eDownload: §b" + url));
break;
}
}
} catch (Exception ignored) {}
}
}
}

View File

@@ -1,67 +0,0 @@
package net.viper.status;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.event.PostLoginEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.event.EventHandler;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.plugin.Plugin;
import java.util.concurrent.TimeUnit;
/**
* Listener: Benachrichtigt OP-Spieler beim Login, falls ein Update bekannt ist oder führt
* asynchronen Einzelcheck durch und benachrichtigt den Spieler danach.
*/
public class UpdateListener implements Listener {
private final Plugin plugin;
private final UpdateChecker checker;
private final String[] notifyPerms = new String[] { "statusapi.update.notify", "statusapi.notify", "bungeecord.command.alert" };
public UpdateListener(Plugin plugin, UpdateChecker checker) {
this.plugin = plugin;
this.checker = checker;
}
@EventHandler
public void onPostLogin(PostLoginEvent event) {
ProxiedPlayer player = event.getPlayer();
// Only notify players with the notify permission(s)
boolean hasPerm = false;
for (String perm : notifyPerms) {
if (player.hasPermission(perm)) { hasPerm = true; break; }
}
if (!hasPerm) return;
String currentVersion = plugin.getDescription() != null ? plugin.getDescription().getVersion() : "0.0.0";
// If we already have a cached update and it's newer -> notify immediately
if (checker.isUpdateAvailable(currentVersion)) {
String lv = checker.getLatestVersion();
String url = checker.getLatestUrl();
player.sendMessage(new TextComponent("§6[StatusAPI] §eNeue Version verfügbar: §a" + lv));
player.sendMessage(new TextComponent("§eDownload: §b" + url));
return;
}
// No cached update yet -> run an async check for this player and notify if found
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
// perform network call
checker.checkNow();
if (checker.isUpdateAvailable(currentVersion)) {
String lv = checker.getLatestVersion();
String url = checker.getLatestUrl();
// ensure player is still online before sending message
ProxiedPlayer p = ProxyServer.getInstance().getPlayer(player.getUniqueId());
if (p != null) {
p.sendMessage(new TextComponent("§6[StatusAPI] §eNeue Version verfügbar: §a" + lv));
p.sendMessage(new TextComponent("§eDownload: §b" + url));
}
}
});
}
}

View File

@@ -1,9 +1,13 @@
name: StatusAPI name: StatusAPI
main: net.viper.status.StatusAPI main: net.viper.status.StatusAPI
version: 1.1 version: 3.6.2
author: M_Viper author: M_Viper
description: StatusAPI für BungeeCord inkl. Update-Checker description: StatusAPI für BungeeCord inkl. Update-Checker
softdepend:
- LuckPerms
permissions: permissions:
statusapi.update.notify: statusapi.update.notify:
description: 'Erlaubt Update-Benachrichtigungen' description: 'Erlaubt Update-Benachrichtigungen'
default: op default: op