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)
$name_html = '<span style="color:black;">' . esc_html($name) . '</span>';
// 3. Zusammenbauen
$display_html = trim($prefix_html) . ' ' . $name_html;
// 3. Zusammenbauen (Nur Abstand, wenn Prefix existiert)
if (!empty($prefix_html)) {
$display_html = $prefix_html . ' ' . $name_html;
} else {
$display_html = $name_html;
}
$players_info[] = [
'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 Repository:** [https://git.viper.ipv64.net/M_Viper/StatusAPI](https://git.viper.ipv64.net/M_Viper/StatusAPI)
---
## Features
- 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
- Anpassbare Darstellung für verschiedene Layouts
---
## 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.
```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
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.
Die StatusAPI stellt einen HTTP-Endpunkt bereit (Standard: `http://localhost:9191`), den das WordPress-Plugin abfragt.
### Shortcode für WordPress
Um den Serverstatus auf deiner WordPress-Seite anzuzeigen, füge einfach folgenden Shortcode ein:
```html
[bungeecord_status id="Bungeecord"]
```
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
**Status:** Online
@@ -42,17 +87,52 @@ Ersetze `"Bungeecord"` durch die ID deines Servers, falls mehrere Server eingebu
**Avatare:**
![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
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
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
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 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>
<artifactId>StatusAPI</artifactId>
<version>1.1</version>
<version>3.6.2</version>
<packaging>jar</packaging>
<name>StatusAPI</name>
@@ -20,7 +20,7 @@
</properties>
<dependencies>
<!-- BungeeCord API (lokal installiert) -->
<!-- BungeeCord API -->
<dependency>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-api</artifactId>
@@ -28,12 +28,13 @@
<scope>provided</scope>
</dependency>
<!-- LuckPerms API für Prefix Support -->
<!-- LuckPerms API (Optional) -->
<dependency>
<groupId>net.luckperms</groupId>
<artifactId>api</artifactId>
<version>5.4</version>
<scope>provided</scope>
<optional>true</optional>
</dependency>
</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;
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.config.ListenerInfo;
import net.md_5.bungee.api.connection.ProxiedPlayer;
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.Socket;
import java.util.*;
@@ -20,6 +21,7 @@ public class StatusAPI extends Plugin implements Runnable {
private Thread thread;
private int port = 9191;
private UpdateChecker updateChecker;
private FileDownloader fileDownloader;
@Override
public void onEnable() {
@@ -30,18 +32,33 @@ public class StatusAPI extends Plugin implements Runnable {
thread = new Thread(this, "StatusAPI-HTTP-Server");
thread.start();
// Start UpdateChecker: initialer Check (async) + regelmäßiger Schedule
String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0";
updateChecker = new UpdateChecker(this, currentVersion, 6); // 6 Stunden Intervall
fileDownloader = new FileDownloader(this);
// initialer sofortiger Start (async)
ProxyServer.getInstance().getScheduler().runAsync(this, () -> updateChecker.checkNow());
File pluginFile = getFile();
File backupFile = new File(pluginFile.getParentFile(), "StatusAPI.jar.bak");
// planmäßiger Intervall (alle 6 Stunden)
ProxyServer.getInstance().getScheduler().schedule(this, updateChecker, 6, 6, TimeUnit.HOURS);
// --- AUTOMATISCHES BACKUP-CLEANUP ---
// 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
ProxyServer.getInstance().getPluginManager().registerListener(this, new UpdateListener(this, updateChecker));
// Sofortiger Start-Check
checkAndMaybeUpdate();
// Regelmäßiger Check (alle 6 Stunden)
ProxyServer.getInstance().getScheduler().schedule(this, this::checkAndMaybeUpdate, 6, 6, TimeUnit.HOURS);
}
@Override
@@ -49,9 +66,99 @@ public class StatusAPI extends Plugin implements Runnable {
getLogger().info("Stoppe Web-Server...");
if (thread != null) {
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 {
thread.join(1000);
} catch (InterruptedException ignored) {}
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
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() {
try (ServerSocket serverSocket = new ServerSocket(port)) {
serverSocket.setSoTimeout(1000);
while (!Thread.interrupted()) {
try {
Socket clientSocket = serverSocket.accept();
handleConnection(clientSocket);
} catch (java.net.SocketTimeoutException e) {
// Loop Check (timeout) - erlaubt Unterbrechungsschleife
} catch (IOException e) {
} catch (java.net.SocketTimeoutException e) {}
catch (IOException e) {
getLogger().warning("Fehler beim Akzeptieren einer Verbindung: " + e.getMessage());
}
}
} catch (IOException e) {
getLogger().severe("Konnte ServerSocket nicht starten auf Port " + port + ": " + e.getMessage());
e.printStackTrace();
}
}
private void handleConnection(Socket clientSocket) {
BufferedReader in = null;
OutputStream out = null;
try {
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), "UTF-8"));
out = clientSocket.getOutputStream();
try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), "UTF-8"));
OutputStream out = clientSocket.getOutputStream()) {
String inputLine = in.readLine();
if (inputLine != null && inputLine.startsWith("GET")) {
Map<String, Object> data = new LinkedHashMap<>();
data.put("online", true);
// --- VERSION CLEANUP START ---
// Version
String versionRaw = ProxyServer.getInstance().getVersion();
String versionClean = versionRaw;
if (versionRaw != null && versionRaw.contains(":")) {
String[] parts = versionRaw.split(":");
if (parts.length >= 3) {
versionClean = parts[2].trim();
}
if (parts.length >= 3) versionClean = parts[2].trim();
}
data.put("version", versionClean);
// --- VERSION CLEANUP ENDE ---
data.put("max_players", String.valueOf(ProxyServer.getInstance().getConfig().getPlayerLimit()));
// Motd
String motd = "BungeeCord";
try {
Iterator<ListenerInfo> it = ProxyServer.getInstance().getConfig().getListeners().iterator();
if (it.hasNext()) {
ListenerInfo listener = it.next();
if (listener != null && listener.getMotd() != null) {
motd = listener.getMotd();
}
if (listener != null && listener.getMotd() != null) motd = listener.getMotd();
}
} catch (Exception e) {
// Fallback bleibt "BungeeCord"
}
} catch (Exception ignored) {}
data.put("motd", motd);
// --- LUCKPERMS INTEGRATION START ---
// LuckPerms API über Provider holen (Verhindert ClassCastException)
LuckPerms luckPermsApi = null;
try {
luckPermsApi = LuckPermsProvider.get();
} catch (IllegalStateException e) {
// LuckPerms ist nicht geladen
}
// LuckPerms (Optional)
boolean luckPermsEnabled = ProxyServer.getInstance().getPluginManager().getPlugin("LuckPerms") != null;
List<Map<String, String>> playersList = new ArrayList<>();
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
Map<String, String> playerInfo = new LinkedHashMap<>();
playerInfo.put("name", p.getName());
// Prefix abfragen, falls LP gefunden wurde
String prefix = "";
if (luckPermsApi != null) {
User user = luckPermsApi.getUserManager().getUser(p.getUniqueId());
if (user != null) {
// Context bestimmen (Global oder Server-spezifisch)
QueryOptions queryOptions = luckPermsApi.getContextManager().getQueryOptions(user)
.orElse(QueryOptions.defaultContextualOptions());
String lpPrefix = user.getCachedData().getMetaData(queryOptions).getPrefix();
if (lpPrefix != null) {
prefix = lpPrefix;
if (luckPermsEnabled) {
try {
Class<?> providerClass = Class.forName("net.luckperms.api.LuckPermsProvider");
Object luckPermsApi = providerClass.getMethod("get").invoke(null);
Object userManager = luckPermsApi.getClass().getMethod("getUserManager").invoke(luckPermsApi);
Object user = userManager.getClass().getMethod("getUser", UUID.class).invoke(userManager, p.getUniqueId());
if (user != null) {
Class<?> queryOptionsClass = Class.forName("net.luckperms.api.query.QueryOptions");
Object queryOptions = queryOptionsClass.getMethod("defaultContextualOptions").invoke(null);
Object cachedData = user.getClass().getMethod("getCachedData").invoke(user);
Object metaData = cachedData.getClass().getMethod("getMetaData", queryOptionsClass).invoke(cachedData, queryOptions);
Object result = metaData.getClass().getMethod("getPrefix").invoke(metaData);
if (result != null) prefix = (String) result;
}
}
} catch (Exception ignored) {}
}
playerInfo.put("prefix", prefix);
playersList.add(playerInfo);
}
data.put("players", playersList);
// --- LUCKPERMS INTEGRATION ENDE ---
// Response
String json = buildJsonString(data);
byte[] jsonBytes = json.getBytes("UTF-8");
// HTTP Response mit korrekter Byte-Length
StringBuilder response = new StringBuilder();
response.append("HTTP/1.1 200 OK\r\n");
response.append("Content-Type: application/json; charset=UTF-8\r\n");
response.append("Access-Control-Allow-Origin: *\r\n");
response.append("Content-Length: ").append(jsonBytes.length).append("\r\n");
response.append("Connection: close\r\n");
response.append("\r\n");
// Header senden
response.append("Connection: close\r\n\r\n");
out.write(response.toString().getBytes("UTF-8"));
// Body senden
out.write(jsonBytes);
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) {
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) {
StringBuilder sb = new StringBuilder("{");
boolean first = true;
for (Map.Entry<String, Object> entry : data.entrySet()) {
if (!first) sb.append(",");
first = false;
sb.append("\"").append(escapeJson(entry.getKey())).append("\":");
// Wert verarbeiten (String, Boolean, List oder Map)
sb.append(valueToString(entry.getValue()));
sb.append("\"").append(escapeJson(entry.getKey())).append("\":").append(valueToString(entry.getValue()));
}
sb.append("}");
return sb.toString();
}
/**
* Hilfsmethode, um verschiedene Objekttypen in JSON-Strings umzuwandeln.
*/
private String valueToString(Object value) {
if (value == null) {
return "null";
} else if (value instanceof Boolean) {
return value.toString();
} else if (value instanceof List) {
if (value == null) return "null";
else if (value instanceof Boolean) return value.toString();
else if (value instanceof List) {
StringBuilder sb = new StringBuilder("[");
List<?> list = (List<?>) value;
for (int i = 0; i < list.size(); i++) {
if (i > 0) sb.append(",");
Object item = list.get(i);
// Wenn es eine Map ist (Player-Objekt), rufen wir buildJsonString rekursiv auf
if (item instanceof Map) {
sb.append(buildJsonString((Map<String, Object>) item));
} else {
// Andernfalls String behandeln
sb.append("\"").append(escapeJson(String.valueOf(item))).append("\"");
}
if (item instanceof Map) sb.append(buildJsonString((Map<String, Object>) item));
else sb.append("\"").append(escapeJson(String.valueOf(item))).append("\"");
}
sb.append("]");
return sb.toString();
} else {
// Standard String Behandlung
return "\"" + escapeJson(String.valueOf(value)) + "\"";
}
else return "\"" + escapeJson(String.valueOf(value)) + "\"";
}
private String escapeJson(String s) {

View File

@@ -1,211 +1,151 @@
package net.viper.status;
import net.md_5.bungee.api.plugin.Plugin;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.chat.TextComponent;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* UpdateChecker:
* - Fragt die Gitea Releases API ab
* - Sucht in den Releases nach dem Asset "StatusAPI.jar"
* - Extrahiert die Version (tag_name bevorzugt, name als Fallback)
* - Cache die gefundene Version + URL (volatile fields)
*/
public class UpdateChecker implements Runnable {
private final Plugin plugin;
private final String currentVersion;
private final int intervalHours;
// Gitea Releases API für dein Repo
private final String apiUrl = "https://git.viper.ipv64.net/api/v1/repos/M_Viper/Minecraft-BungeeCord-Status/releases";
// cached results (volatile für Thread-Sichtbarkeit)
private volatile String latestVersion = "";
private volatile String latestUrl = "";
// 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);
private static final Pattern DOWNLOAD_PATTERN = Pattern.compile("\"browser_download_url\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
private static final Pattern TAG_NAME_PATTERN = Pattern.compile("\"tag_name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
private static final Pattern NAME_FIELD_PATTERN = Pattern.compile("\"name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
public UpdateChecker(Plugin plugin, String currentVersion, int intervalHours) {
this.plugin = plugin;
this.currentVersion = currentVersion != null ? currentVersion : "0.0.0";
this.intervalHours = Math.max(1, intervalHours);
}
@Override
public void run() {
// scheduled task calls checkNow
checkNow();
}
/**
* Führt einen sofortigen Check aus und updated cache (synchron for the calling thread).
*/
public void checkNow() {
try {
plugin.getLogger().info("Prüfe StatusAPI Releases via Gitea API...");
HttpURLConnection conn = (HttpURLConnection) new URL(apiUrl).openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
conn.setRequestProperty("User-Agent", "StatusAPI-UpdateChecker/1.0");
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
int code = conn.getResponseCode();
if (code != 200) {
plugin.getLogger().warning("Gitea API nicht erreichbar (HTTP " + code + ")");
return;
}
StringBuilder sb = new StringBuilder();
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"))) {
String line;
while ((line = br.readLine()) != null) {
sb.append(line).append("\n");
}
}
String body = sb.toString();
// Suche Release mit Asset "StatusAPI.jar"
String foundVersion = null;
String foundUrl = null;
// Split releases by top-level objects: we simply find repeating {...} blocks and inspect them
// This is robust enough for Gitea JSON responses.
Pattern releasePattern = Pattern.compile("(?s)\\{.*?\\}");
Matcher releaseMatcher = releasePattern.matcher(body);
while (releaseMatcher.find()) {
String block = releaseMatcher.group();
// extract asset names & download urls inside the block
java.util.List<String> names = new java.util.ArrayList<>();
java.util.List<String> downloads = new java.util.ArrayList<>();
Matcher nm = ASSET_NAME_PATTERN.matcher(block);
while (nm.find()) names.add(nm.group(1));
Matcher dm = DOWNLOAD_PATTERN.matcher(block);
while (dm.find()) downloads.add(dm.group(1));
int pairs = Math.min(names.size(), downloads.size());
for (int i = 0; i < pairs; i++) {
String name = names.get(i);
String dl = downloads.get(i);
if ("StatusAPI.jar".equalsIgnoreCase(name.trim())) {
// find tag_name or name field in the same block
Matcher tagM = TAG_NAME_PATTERN.matcher(block);
if (tagM.find()) {
foundVersion = tagM.group(1).trim();
} else {
Matcher nameFieldM = NAME_FIELD_PATTERN.matcher(block);
if (nameFieldM.find()) {
foundVersion = nameFieldM.group(1).trim();
}
}
foundUrl = dl;
break;
}
}
if (foundUrl != null) break;
}
if (foundVersion == null || foundUrl == null) {
plugin.getLogger().info("Kein Release mit Asset 'StatusAPI.jar' gefunden.");
return;
}
// Normalize version
if (foundVersion.startsWith("v") || foundVersion.startsWith("V")) {
foundVersion = foundVersion.substring(1);
}
plugin.getLogger().info("Gefundene Version: " + foundVersion + " (aktuell: " + currentVersion + ")");
// set cache
latestVersion = foundVersion;
latestUrl = foundUrl;
} catch (Exception e) {
plugin.getLogger().log(Level.SEVERE, "Fehler beim Update-Check: " + e.getMessage(), e);
}
}
/**
* 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) {}
}
}
}
package net.viper.status;
import net.md_5.bungee.api.plugin.Plugin;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class UpdateChecker {
private final Plugin plugin;
private final String currentVersion;
private final int intervalHours;
private final String apiUrl = "https://git.viper.ipv64.net/api/v1/repos/M_Viper/Minecraft-BungeeCord-Status/releases";
private volatile String latestVersion = "";
private volatile String latestUrl = "";
// Pattern für Dateinamen im Assets-Array
private static final Pattern ASSET_NAME_PATTERN = Pattern.compile("\"name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
// Pattern für Download-URL
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)
private static final Pattern TAG_NAME_PATTERN = Pattern.compile("\"tag_name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
public UpdateChecker(Plugin plugin, String currentVersion, int intervalHours) {
this.plugin = plugin;
this.currentVersion = currentVersion != null ? currentVersion : "0.0.0";
this.intervalHours = Math.max(1, intervalHours);
}
public void checkNow() {
try {
HttpURLConnection conn = (HttpURLConnection) new URL(apiUrl).openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
conn.setRequestProperty("User-Agent", "StatusAPI-UpdateChecker/2.0");
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
int code = conn.getResponseCode();
if (code != 200) {
plugin.getLogger().warning("Gitea API nicht erreichbar (HTTP " + code + ")");
return;
}
StringBuilder sb = new StringBuilder();
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"))) {
String line;
while ((line = br.readLine()) != null) sb.append(line).append("\n");
}
String body = sb.toString();
// 1. Die LATEST Version (Tag) finden
// Wir suchen das erste "tag_name" im gesamten JSON, das ist meistens das neueste Release.
String foundVersion = null;
Matcher tagM = TAG_NAME_PATTERN.matcher(body);
if (tagM.find()) {
foundVersion = tagM.group(1).trim();
}
if (foundVersion == null) {
plugin.getLogger().warning("Keine Version (Tag) im Release gefunden.");
return;
}
// Version säubern (v vorne entfernen)
if (foundVersion.startsWith("v") || foundVersion.startsWith("V")) {
foundVersion = foundVersion.substring(1);
}
// 2. Download URL für StatusAPI.jar finden
// Wir suchen das Asset "StatusAPI.jar"
String foundUrl = null;
Pattern releasePattern = Pattern.compile("(?s)\\{.*?\\}");
Matcher releaseMatcher = releasePattern.matcher(body);
while (releaseMatcher.find()) {
String block = releaseMatcher.group();
java.util.List<String> names = new java.util.ArrayList<>();
java.util.List<String> downloads = new java.util.ArrayList<>();
Matcher nm = ASSET_NAME_PATTERN.matcher(block);
while (nm.find()) names.add(nm.group(1));
Matcher dm = DOWNLOAD_PATTERN.matcher(block);
while (dm.find()) downloads.add(dm.group(1));
int pairs = Math.min(names.size(), downloads.size());
for (int i = 0; i < pairs; i++) {
String name = names.get(i);
String dl = downloads.get(i);
if ("StatusAPI.jar".equalsIgnoreCase(name.trim())) {
foundUrl = dl;
break;
}
}
if (foundUrl != null) break;
}
if (foundUrl == null) {
plugin.getLogger().warning("Keine JAR-Datei für dieses Release gefunden.");
return;
}
plugin.getLogger().info("Gefundene Version: " + foundVersion + " (Aktuell: " + currentVersion + ")");
latestVersion = foundVersion;
latestUrl = foundUrl;
} catch (Exception e) {
plugin.getLogger().log(Level.SEVERE, "Fehler beim Update-Check: " + e.getMessage(), e);
}
}
public String getLatestVersion() {
return latestVersion != null ? latestVersion : "";
}
public String getLatestUrl() {
return latestUrl != null ? latestUrl : "";
}
public boolean isUpdateAvailable(String currentVer) {
String lv = getLatestVersion();
if (lv.isEmpty()) return false;
return compareVersions(lv, currentVer) > 0;
}
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);
}
}
}

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
main: net.viper.status.StatusAPI
version: 1.1
version: 3.6.2
author: M_Viper
description: StatusAPI für BungeeCord inkl. Update-Checker
softdepend:
- LuckPerms
permissions:
statusapi.update.notify:
description: 'Erlaubt Update-Benachrichtigungen'
default: op
default: op