7 Commits
3.6.1 ... 3.6.2

7 changed files with 381 additions and 398 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

@@ -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