11 Commits
4.0.5 ... main

14 changed files with 1465 additions and 306 deletions

55
LICENSE Normal file
View File

@@ -0,0 +1,55 @@
LIZENZ & NUTZUNGSBEDINGUNGEN
============================
StatusAPI - Modulares BungeeCord Plugin
Copyright (c) M_Viper. Alle Rechte vorbehalten.
---
NUTZUNGSBESCHRAENKUNGEN
Dieses Projekt sowie alle enthaltenen Module, Quelltexte und Ressourcen
duerfen NICHT veraendert, kopiert oder weiterverbreitet werden.
Jegliche Nutzung, Modifikation oder Weitergabe - ganz oder teilweise -
ist ausschliesslich mit vorheriger SCHRIFTLICHER GENEHMIGUNG des
Entwicklers gestattet.
---
VERBOTENE HANDLUNGEN
Ohne ausdrueckliche schriftliche Genehmigung ist es untersagt:
1. Den Quellcode zu kopieren, zu modifizieren oder abgeleitete Werke
zu erstellen
2. Das Plugin oder Teile davon weiterzuverbreiten, zu verkaufen oder
anderweitig zu uebertragen
3. Das Plugin zu dekompilieren, zu disassemblieren oder auf andere
Weise zurueckzuentwickeln (Reverse Engineering)
4. Urheberrechtshinweise, Marken oder andere Eigentumshinweise zu
entfernen oder zu veraendern
---
HAFTUNGSAUSSCHLUSS
DIESES PLUGIN WIRD "WIE BESEHEN" ZUR VERFUEGUNG GESTELLT, OHNE
JEGLICHE AUSDRUECKLICHE ODER STILLSCHWEIGENDE GEWAEHRLEISTUNG.
DER ENTWICKLER HAFTET NICHT FUER DIREKTE, INDIREKTE, ZUFAELLIGE,
BESONDERE ODER FOLGESCHAEDEN, DIE SICH AUS DER NUTZUNG ODER DER
UNMOEGLICHKEIT DER NUTZUNG DIESES PLUGINS ERGEBEN.
---
KONTAKT
Bei Fragen zur Lizenzierung oder fuer Genehmigungsanfragen wenden
Sie sich bitte an den Entwickler.
---
(c) M_Viper - Alle Rechte vorbehalten.

217
README.md
View File

@@ -1,105 +1,160 @@
# StatusAPI
![Build Status](https://img.shields.io/badge/build-passing-brightgreen)
![Version](https://img.shields.io/badge/version-4.0.1-blue)
![Version](https://img.shields.io/badge/version-4.0.6-blue)
Ein modulares und mächtiges Plugin für BungeeCord, das einen zentralen JSON-Status, ein globales Chat-System, WordPress-Verifizierung und dynamische Server-Navigation bereitstellt.
Ein modulares und leistungsstarkes Plugin für **BungeeCord**, das einen zentralen JSON-Status, globalen Chat, WordPress-Verifizierung, Server-Navigation und optionale Sicherheitsmodule bereitstellt.
---
## ⚡ Features
- **Modulares System**: Aktiviere nur die Features, die du brauchst (Chat, Stats, Verify)
- **JSON API**: Liefert Echtzeit-Server-Statistiken (Spieleranzahl, Motd, Versionen) für deine Webseite (Port 9191)
- **Global Chat**: Ein einheitlicher Chat über alle Server hinweg mit Badword-Filter und LuckPerms-Support
- **Server Navigation**: Automatische Generierung von Befehlen basierend auf deiner Config (z.B. `/survival` statt `/server survival`)
- **WordPress Verify**: Verifiziere Spieler direkt mit deiner WordPress-Seite (CPT Integration)
- **Auto-Updater**: Prüft automatisch auf Updates und lädt diese herunter
- **Logging**: Speichert den kompletten Chatverlauf in Dateien
- **Modulares System**
Aktiviere nur die Module, die du benötigst (Chat, Navigation, Verify, Security).
- **JSON Status API**
Liefert Echtzeit-Serverdaten für Webseiten oder externe Dienste (Port `9191`).
- **Global Chat**
Serverübergreifender Chat mit Badword-Filter, LuckPerms-Unterstützung und Logging.
- **Server Navigation**
Automatische Generierung von Server-Befehlen (z. B. `/survival` statt `/server survival`).
- **WordPress Verify**
Spieler-Verifizierung über WordPress (Token-basiert, CPT-kompatibel).
- **CommandBlocker (Security Modul)**
Zentrale Blockierung von Commands auf Netzwerkebene inkl. Admin-Bypass.
- **Auto-Updater**
Prüft automatisch auf Updates und lädt diese herunter.
- **Logging**
Persistente Speicherung von Chat-Nachrichten mit automatischer Bereinigung.
---
## 📥 Installation
1. Lade die aktuellste `StatusAPI.jar` herunter
2. Lege die Datei in den `plugins` Ordner deiner BungeeCord-Installation
3. Starte den Server neu
4. Die Config-Dateien werden automatisch erstellt
1. Lade die aktuelle `StatusAPI.jar`
2. Lege sie in den `plugins/` Ordner deiner BungeeCord-Installation
3. Starte den Proxy neu
4. Alle Konfigurationsdateien werden automatisch erstellt
---
## ⚙️ Konfiguration
Alle Haupt-Einstellungen werden in der `plugins/StatusAPI/verify.properties` zentral verwaltet.
Alle Hauptmodule werden zentral über
`plugins/StatusAPI/verify.properties` gesteuert.
### verify.properties
Hier konfigurierst du Server-Namen, Farben und die Verbindung zu WordPress.
```properties
# ===========================
# GLOBALE EINSTELLUNGEN
# ===========================
# Aktiviert oder deaktiviert das globale Chat-Modul
chat.enabled=true
# Aktiviert die automatischen Server-Switch Befehle (z.B. /survival)
navigation.enabled=true
commandblocker.enabled=true
# ===========================
# WORDPRESS VERIFY
# ===========================
# URL deiner WordPress-Installation
wp_verify_url=https://deine-wordpress-domain.tld
# ===========================
# SERVER KONFIGURATION
# ===========================
# Der Key-Name (z.B. bungee-server-1) MUSS exakt mit dem
# Namen in deiner BungeeCord config.yml übereinstimmen!
#
# - server.<Name>=<Anzeigename> -> Wird im Chat und als Befehl genutzt
# - server.<Name>.id=<ID> -> WordPress Server ID
# - server.<Name>.secret=<Key> -> WordPress Secret
# Beispiel: Server mit Namen "Lobby"
server.lobby=&bLobby
server.lobby.id=1606
server.lobby.secret=DeinSecretHier
# Beispiel: Server mit Namen "bungee-server-1" (Name aus BungeeConfig)
server.bungee-server-1=&aSurvival
server.bungee-server-1.id=2
server.bungee-server-1.secret=GeheimesWortFuerSurvival
server.survival=&aSurvival
server.survival.id=2
server.survival.secret=GeheimesWortFuerSurvival
```
## 🔒 CommandBlocker Modul
# Beispiel: SkyBlock
server.skyblock=&dSkyBlock
server.skyblock.id=3
server.skyblock.secret=GeheimesWortFuerSkyBlock
Das **CommandBlockerModule** blockiert definierte Commands **netzwerkweit**, bevor sie an Backend-Server weitergeleitet werden.
### Eigenschaften
- Blockierung auf **BungeeCord-Ebene**
- **YAML-basierte** Konfiguration
- **Live-Verwaltung** per Command
- **Bypass-Permission** für Admins
- **Keine Abhängigkeit** von anderen Modulen
---
### Konfigurationsdatei
**Datei:**
`plugins/StatusAPI/blocked-commands.yml`
```yaml
blocked:
- plugins
- pl
- version
- about
```
### Weitere Dateien
Änderungen können entweder **manuell** oder **per Befehl** vorgenommen werden.
- **filter.yml**: Hier trägst du Badwords ein, die im Chat zensiert werden sollen
- **welcome.yml**: Hier definierst du Willkommensnachrichten für neue Spieler (nur sichtbar, wenn Welcome-Events aktiviert sind)
- **plugins/StatusAPI/logs/**: Hier werden alle Chatnachrichten als Textdateien gespeichert (Löschung nach 7 Tagen)
---
## 💻 Befehle
## Befehle
| Befehl | Beschreibung | Permission |
|--------|--------------|------------|
| `/verify <token>` | Verifiziert den Spieler mit der WordPress-Seite | - |
| `/survival` / `/lobby` | Führt dich auf den entsprechenden Server um (wird dynamisch erstellt) | - |
| `/globalmute` | Aktiviert oder deaktiviert den globalen Chat | `globalchat.mute` |
| `/globalreload` | Lädt Filter und Konfigurationen neu | `globalchat.reload` |
| `/clearchat, cc` | Löscht den Chatverlauf | `globalchat.clear` |
| `/togglechat` | schaltet für den Spieler den Chat ab | - |
| `/support <msg>` | Sendet eine Nachricht an das Online-Team | - |
| `/reply <msg>` | Antwortet auf eine Support-Anfrage | - |
| `/info` | Zeigt Plugin-Informationen an | - |
|------|-------------|------------|
| `/cb add <command>` | Blockiert einen Command | `commandblocker.admin` |
| `/cb remove <command>` | Entfernt einen Block | `commandblocker.admin` |
| `/cb list` | Zeigt alle blockierten Commands | `commandblocker.admin` |
| `/cb reload` | Lädt die YAML neu | `commandblocker.admin` |
## 🌐 JSON API (Status)
---
Die API läuft unter `http://DEINE_IP:9191/`.
## Bypass
### Beispiel Request
```bash
curl http://127.0.0.1:9191/
Spieler mit folgender Permission umgehen den CommandBlocker vollständig:
```yaml
commandblocker.bypass
```
### Beispiel Antwort
---
## 💻 Allgemeine Befehle
| Befehl | Beschreibung | Permission |
|------|-------------|------------|
| `/verify <token>` | WordPress-Verifizierung | - |
| `/globalmute` | Globalen Chat sperren | `globalchat.mute` |
| `/globalreload` | Config neu laden | `globalchat.reload` |
| `/clearchat`, `/cc` | Chat leeren | `globalchat.clear` |
| `/togglechat` | Chat lokal deaktivieren | - |
| `/support <msg>` | Support-Anfrage senden | - |
| `/reply <msg>` | Auf Support antworten | - |
| `/info` | Plugin-Infos anzeigen | - |
---
## 🌐 JSON API
**Endpoint:**
```yaml
http://DEINE_IP:9191/
```
### Beispiel-Antwort
```json
{
"online": true,
@@ -117,28 +172,54 @@ curl http://127.0.0.1:9191/
}
```
---
## 🛠️ Für Entwickler
Die StatusAPI ist modular aufgebaut. Du kannst eigene Module erstellen, ohne den Core-Code zu berühren.
Die **StatusAPI** ist vollständig modular aufgebaut und erlaubt die einfache Erweiterung um eigene Module, ohne den Core-Code zu verändern.
### Beispiel
---
### Eigenes Modul erstellen
1. Klasse implementiert das `Module`-Interface
2. Optional zusätzlich `Listener` implementieren
3. Registrierung im Core (z. B. im ModuleManager):
1. Erstelle eine Klasse, die `Module` implementiert
2. Registriere sie in der StatusAPI Hauptdatei:
```java
moduleManager.registerModule(new MeinModul());
```
### Modul-Lifecycle
3. Dein Modul hat Zugriff auf `onEnable` und `onDisable` des Haupt-Plugins
Jedes Modul besitzt Zugriff auf folgende Lifecycle-Methoden:
## 📝 Permissions
- `onEnable(Plugin plugin)`
- `onDisable(Plugin plugin)`
- `globalchat.mute` - Erlaubt das Stummschalten des Chats
- `globalchat.bypass` - Erlaubt das Schreiben, auch wenn der Chat gemuted ist
- `globalchat.reload` - Erlaubt das Neuladen der Konfiguration
- `globalchat.clear` - Löscht den Kompletten Chatverlauf
Diese Methoden werden automatisch beim **Starten** bzw. **Stoppen** des Proxys aufgerufen.
---
## 📝 Permissions Übersicht
- `globalchat.mute`
- `globalchat.bypass`
- `globalchat.reload`
- `globalchat.clear`
- `commandblocker.admin`
- `commandblocker.bypass`
---
## 🤝 Credits
Entwickelt von **M_Viper**.
Unterstützt durch BungeeCord und LuckPerms Community.
---
## 📜 Lizenz & Nutzung
Dieses Projekt sowie alle enthaltenen Module, Quelltexte und Ressourcen dürfen **nicht verändert, kopiert oder weiterverbreitet** werden.
Jegliche Nutzung, Modifikation oder Weitergabe ganz oder teilweise ist **ausschließlich mit vorheriger schriftlicher Genehmigung** des Entwicklers gestattet.
© Entwickelt von **M_Viper**. Alle Rechte vorbehalten.

View File

@@ -7,7 +7,7 @@
<groupId>net.viper.bungee</groupId>
<artifactId>StatusAPI</artifactId>
<version>4.0.5</version>
<version>4.0.8</version>
<packaging>jar</packaging>
<name>StatusAPI</name>

View File

@@ -1,42 +0,0 @@
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;
public class FileDownloader {
private final Plugin plugin;
public FileDownloader(Plugin plugin) { this.plugin = plugin; }
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);
}
fileOutputStream.close();
bufferedInputStream.close();
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,8 +1,14 @@
package net.viper.status;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.ComponentBuilder;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.config.ListenerInfo;
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.api.plugin.Plugin;
import net.viper.status.module.ModuleManager;
import net.viper.status.stats.PlayerStats;
@@ -10,89 +16,192 @@ import net.viper.status.stats.StatsModule;
import net.viper.status.modules.verify.VerifyModule;
import net.viper.status.modules.globalchat.GlobalChatModule;
import net.viper.status.modules.navigation.NavigationModule;
import net.viper.status.modules.commandblocker.CommandBlockerModule;
import net.viper.status.modules.broadcast.BroadcastModule;
import net.viper.status.modules.AutoMessage.AutoMessageModule;
import net.viper.status.modules.customcommands.CustomCommandModule;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.*;
import java.util.concurrent.TimeUnit;
import net.md_5.bungee.event.EventHandler;
/**
* StatusAPI - zentraler Bungee HTTP-Status- und Broadcast-Endpunkt
*
* Ergänzungen:
* - BroadcastModule Registrierung
* - POST /broadcast UND POST / (Root) werden als Broadcasts behandelt
* - Unterstützung für prefix, prefixColor, messageColor Felder in JSON-Payload
* - Unterstützung für bracketColor (neu)
* - Unterstützung für scheduleTime (ms oder s), recur, clientScheduleId
* - API-Key Header (X-Api-Key) wird an BroadcastModule weitergereicht
*/
public class StatusAPI extends Plugin implements Runnable {
public class StatusAPI extends Plugin implements Runnable, Listener {
private Thread thread;
private int port = 9191;
// Das neue Modul-System
private ModuleManager moduleManager;
private Properties verifyProperties;
// Alte Komponenten (UpdateChecker/FileDownloader bleiben hier, da sie Core-Updates steuern)
private UpdateChecker updateChecker;
private FileDownloader fileDownloader;
// Speicher für Updates beim Join
private boolean updatePending = false;
private String latestVersionString = "";
private final Set<UUID> informedAdmins = new HashSet<>();
@Override
public void onEnable() {
getLogger().info("StatusAPI Core wird initialisiert...");
// 1. Ordner sicherstellen
// Event Listener registrieren
getProxy().getPluginManager().registerListener(this, this);
if (!getDataFolder().exists()) {
getDataFolder().mkdirs();
}
// 2. Modul-System starten
// Config mergen/updaten
mergeVerifyConfig();
moduleManager = new ModuleManager();
// 3. MODULE REGISTRIEREN
moduleManager.registerModule(new StatsModule()); // Statistik System laden
moduleManager.registerModule(new VerifyModule()); // Verify Modul
moduleManager.registerModule(new GlobalChatModule()); // GlobalChat
moduleManager.registerModule(new NavigationModule()); //Server Switcher
// Broadcast Modul registrieren (neu)
moduleManager.registerModule(new net.viper.status.modules.broadcast.BroadcastModule());
// Module registrieren
moduleManager.registerModule(new StatsModule());
moduleManager.registerModule(new VerifyModule());
moduleManager.registerModule(new GlobalChatModule());
moduleManager.registerModule(new NavigationModule());
moduleManager.registerModule(new BroadcastModule());
moduleManager.registerModule(new CommandBlockerModule());
moduleManager.registerModule(new AutoMessageModule());
// CustomCommandModule nur laden, wenn aktiviert
boolean customCommandsEnabled = Boolean.parseBoolean(getVerifyProperties().getProperty("customcommands.enabled", "false"));
if (customCommandsEnabled) {
moduleManager.registerModule(new CustomCommandModule()); // Korrigiert: Kein Argument im Konstruktor
getLogger().info("CustomCommandModule aktiviert.");
} else {
getLogger().info("CustomCommandModule deaktiviert (siehe verify.properties).");
}
// 4. Alle Module aktivieren
moduleManager.enableAll(this);
// 5. WebServer Thread starten
// WebServer starten
getLogger().info("Starte Web-Server auf Port " + port + "...");
thread = new Thread(this, "StatusAPI-HTTP-Server");
thread.start();
// 6. Update System Initialisieren
String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0";
updateChecker = new UpdateChecker(this, currentVersion, 6);
fileDownloader = new FileDownloader(this);
// Einfacher Update-Check (Nur Benachrichtigung für Admins beim Start oder beim Join)
checkForUpdatesAndNotify();
}
File pluginFile = getFile();
File backupFile = new File(pluginFile.getParentFile(), "StatusAPI.jar.bak");
@EventHandler
public void onPlayerJoin(PostLoginEvent event) {
ProxiedPlayer player = event.getPlayer();
// Backup Cleanup
if (backupFile.exists()) {
ProxyServer.getInstance().getScheduler().schedule(this, () -> {
if (backupFile.exists()) backupFile.delete();
},1, TimeUnit.MINUTES);
// Wir prüfen hier nur, ob ein Update aussteht. Der Rest wird im Scheduler verzögert geprüft.
if (updatePending && isAdmin(player)) {
// Verzögerung um 2 Sekunden
getProxy().getScheduler().schedule(this, () -> {
// Prüfen, ob Spieler noch online ist und noch nicht informiert wurde
if (player.isConnected() && !informedAdmins.contains(player.getUniqueId()) && isAdmin(player)) {
sendStyledUpdateMessage(player);
informedAdmins.add(player.getUniqueId());
getLogger().info("Admin " + player.getName() + " wurde über das neue Update informiert.");
}
}, 2, TimeUnit.SECONDS);
}
}
// Sofortiger Check
checkAndMaybeUpdate();
// Regelmäßiger Check
ProxyServer.getInstance().getScheduler().schedule(this, this::checkAndMaybeUpdate, 6, 6, TimeUnit.HOURS);
/**
* Sendet eine formatierte Nachricht mit klickbarem Link.
*/
private void sendStyledUpdateMessage(ProxiedPlayer player) {
String currentVersion = getDescription() != null ? getDescription().getVersion() : "Unbekannt";
String downloadUrl = "https://git.viper.ipv64.net/M_Viper/StatusAPI/releases";
ComponentBuilder builder = new ComponentBuilder("");
// Zeile 1: Update Info
// Wir konvertieren Legacy-Strings zu BaseComponents und fügen sie an den Builder an
builder.append(TextComponent.fromLegacyText("§a[StatusAPI] "))
.append(TextComponent.fromLegacyText("§eEine neue Version ist verfügbar: "))
.append(TextComponent.fromLegacyText("§c" + latestVersionString))
.append(TextComponent.fromLegacyText(" §7(Deine Version: "))
.append(TextComponent.fromLegacyText("§c" + currentVersion))
.append(TextComponent.fromLegacyText("§7)\n"));
// Zeile 2: Download
builder.append(TextComponent.fromLegacyText("§7Download: "));
// Der Klickbare Teil
TextComponent link = new TextComponent("§e§nLink");
link.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, downloadUrl));
link.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, TextComponent.fromLegacyText("§aKlicke hier um die Release-Seite zu öffnen")));
builder.append(link);
// Korrektur: Nur die Components senden, nicht den Spieler als Argument
player.sendMessage(builder.create());
}
/**
* Überprüft auf Updates und benachrichtigt Spieler mit Admin-Rechten (OP/Owner).
*/
private void checkForUpdatesAndNotify() {
String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0";
// Der UpdateChecker wird nur lokal instanziiert für den Check
UpdateChecker checker = new UpdateChecker(this, currentVersion, 6);
try {
checker.checkNow();
if (checker.isUpdateAvailable(currentVersion)) {
String newVersion = checker.getLatestVersion();
String url = checker.getLatestUrl();
getLogger().warning("------------------------------------------------");
getLogger().warning("Neue Version verfügbar: " + newVersion);
getLogger().warning("Aktuelle Version: " + currentVersion);
getLogger().warning("Download: " + url);
getLogger().warning("------------------------------------------------");
// Info speichern (nur Version, URL ist statisch für den Chat-Link)
this.updatePending = true;
this.latestVersionString = newVersion;
// Benachrichtigung an online Admins / OPs (mit Verzögerung, um konsistent mit Join zu sein)
boolean notifiedSomeone = false;
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
if (isAdmin(p)) {
final ProxiedPlayer player = p;
getProxy().getScheduler().schedule(this, () -> {
if (player.isConnected() && !informedAdmins.contains(player.getUniqueId())) {
sendStyledUpdateMessage(player);
informedAdmins.add(player.getUniqueId());
}
}, 2, TimeUnit.SECONDS);
notifiedSomeone = true;
}
}
if (!notifiedSomeone) {
getLogger().info("Keine Admins waren online, um benachrichtigt zu werden. Sie werden beim Join informiert.");
}
} else {
getLogger().info("Keine Updates verfügbar.");
}
} catch (Exception e) {
getLogger().severe("Fehler beim Update-Check: " + e.getMessage());
}
}
// Hilfsmethode zum Prüfen von Admin-Rechten
private boolean isAdmin(ProxiedPlayer player) {
return player.hasPermission("statusapi.admin") || player.hasPermission("bungeecord.command.server");
}
@Override
@@ -109,71 +218,246 @@ public class StatusAPI extends Plugin implements Runnable {
}
}
// --- Update Logik (unverändert) ---
private void checkAndMaybeUpdate() {
/**
* Hilfsmethode um Property-Werte zu bereinigen (unescape).
* Entfernt z.B. den Backslash vor URLs wie https\://
*/
private String unescapePropertiesString(String input) {
if (input == null) return null;
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("----------------------------------------");
File pluginFile = getFile();
File newFile = new File(pluginFile.getParentFile(), "StatusAPI.jar.new");
fileDownloader.downloadFile(url, newFile, () -> triggerUpdateScript(pluginFile, newFile));
}
} catch (Exception e) {
getLogger().severe("Fehler beim Update-Check: " + e.getMessage());
Properties p = new Properties();
p.load(new StringReader("dummy=" + input));
return p.getProperty("dummy");
} catch (IOException e) {
return input; // Fallback, falls Parsing fehlschlägt
}
}
private void triggerUpdateScript(File currentFile, File newFile) {
// --- MERGE LOGIK ---
private void mergeVerifyConfig() {
try {
File pluginsFolder = currentFile.getParentFile();
File rootFolder = pluginsFolder.getParentFile();
File batFile = new File(rootFolder, "StatusAPI_Update_" + System.currentTimeMillis() + ".bat");
File file = new File(getDataFolder(), "verify.properties");
String batContent = "@echo off\n" +
"echo Bitte warten, der Server fährt 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\"";
// 1. Das exakte Template definieren
List<String> templateLines = Arrays.asList(
"# _____ __ __ ___ ____ ____",
"# / ___// /_____ _/ /___ _______/ | / __ \\/ _/",
"# \\__ \\/ __/ __ `/ __/ / / / ___/ /| | / /_/ // / ",
"# ___/ / /_/ /_/ / /_/ /_/ (__ ) ___ |/ ____// / ",
"# /____/\\__/\\__,_/\\__/\\__,_/____/_/ |_/_/ /___/ ",
" ",
"# ===========================",
"# GLOBALCHAT AKTIVIERUNG",
"# ===========================",
"chat.enabled=false",
"",
"# ------------------------------",
"# Broadcast",
"# ------------------------------",
"",
"broadcast.enabled=false",
"broadcast.prefix=[Broadcast]",
"broadcast.prefix-color=&c",
"broadcast.message-color=&f",
"broadcast.format=%prefixColored% %messageColored%",
"# broadcast.format kann angepasst werden; nutze Platzhalter: %name%, %prefix%, %prefixColored%, %message%, %messageColored%, %type%",
"",
"# ===========================",
"# NAVIGATION / SERVER SWITCHER",
"# ===========================",
"# Hier kannst du das interne Navigationssystem aktivieren/deaktivieren.",
"# Wenn aktiviert, erstellt das Plugin automatisch Befehle basierend auf den Servernamen (z.B. /lobby, /survival).",
"navigation.enabled=false",
"",
"# ===========================",
"# WORDPRESS / VERIFY EINSTELLUNGEN",
"# ===========================",
"wp_verify_url=https://deine-wp-domain.tld",
"",
"# ===========================",
"# SERVER KONFIGURATION",
"# ===========================",
"# Hier legst du für jeden Server alles fest:",
"# 1. Den Anzeigenamen für den Chat (z.B. &bLobby)",
"# 2. Die Server ID für WordPress (z.B. id=1)",
"# 3. Das Secret für WordPress (z.B. secret=...)",
"",
"# Server 1: Lobby",
"server.lobby=&bLobby",
"server.lobby.id=1",
"server.lobby.secret=GeheimesWortFuerLobby123",
"",
"# Server 2: Survival",
"server.survival=&aSurvival",
"server.survival.id=2",
"server.survival.secret=GeheimesWortFuerSurvival456",
"",
"# Server 3: SkyBlock",
"server.skyblock=&dSkyBlock",
"server.skyblock.id=3",
"server.skyblock.secret=GeheimesWortFuerSkyBlock789",
"",
"# ===========================",
"# Manuelle Ränge (Overrides)",
"# ===========================",
"# Syntax: override.<Spieler-UUID> = <Gruppenname>",
"# WICHTIG: Die Gruppe (z.B. Owner) muss unten bei groupformat definiert sein!",
"",
"# Beispiel: Deinen UUID hier einfügen",
"override.uuid-hier-einfügen = Owner",
"",
"# ===========================",
"# Chat-Formate für Gruppen",
"# ===========================",
"",
"# Der Name hinter dem Punkt (z.B. Owner) muss exakt mit der LuckPerms Gruppe übereinstimmen.",
"# Nutze & für Farbcodes.",
"",
"# Ränge mit neuer Syntax: Rank || Spielerfarbe || Chatfarbe",
"# Beispiel: Rot (Rang) || Blau (Name) || Lila (Chat)",
"groupformat.owner=&c[Owner] || &b || &d",
"groupformat.admin=&4[Admin] || &9 || &c",
"groupformat.developer=&b[Dev] || &3 || &a",
"groupformat.premium=&6[Premium] || &e || &7",
"groupformat.spieler=&f[Spieler] || &7 || &8",
"",
"# ===========================",
"# AUTOMESSAGE",
"# ===========================",
"# Aktiviert den automatischen Nachrichten-Rundruf",
"automessage.enabled=false",
"",
"# Zeitintervall in Sekunden (Standard: 300 = 5 Minuten)",
"automessage.interval=300",
"",
"# Optional: Ein Prefix, das VOR jede Nachricht aus der Datei gesetzt wird.",
"# Wenn du das Prefix bereits IN der messages.txt hast, lass dieses Feld einfach leer.",
"automessage.prefix=",
"",
"# Der Name der Datei, in der die Nachrichten stehen (liegt im Plugin-Ordner)",
"automessage.file=messages.txt",
"",
"# ===========================",
"# COMMAND BLOCKER",
"# ===========================",
"commandblocker.enabled=true",
"commandblocker.bypass.permission=commandblocker.bypass",
"",
"# ===========================",
"# CUSTOM COMMANDS",
"# ===========================",
"# Aktiviert das Modul für benutzerdefinierte Befehle und Aliases.",
"# Wenn aktiviert, wird die Datei 'customcommands.yml' geladen.",
"customcommands.enabled=true"
);
try (PrintWriter out = new PrintWriter(batFile)) { out.println(batContent); }
// 2. Existierende Werte einlesen
Map<String, String> existingValues = new HashMap<>();
// Liste für Override-Keys, die nicht im Template stehen
List<Map.Entry<String, String>> pendingOverrides = new ArrayList<>();
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
p.disconnect("§cServer fährt für ein Update neu herunter. Bitte etwas warten.");
if (file.exists()) {
List<String> existing = Files.readAllLines(file.toPath(), StandardCharsets.UTF_8);
for (String line : existing) {
String trimmed = line.trim();
if (!trimmed.isEmpty() && !trimmed.startsWith("#") && trimmed.contains("=")) {
int idx = trimmed.indexOf('=');
String key = trimmed.substring(0, idx).trim();
String val = trimmed.substring(idx + 1).trim();
// Wert speichern, Escaping entfernen für sauberen Output
existingValues.put(key, unescapePropertiesString(val));
}
}
}
getLogger().info("Starte Update-Skript...");
Runtime.getRuntime().exec("cmd /c start \"Update_Proc\" \"" + batFile.getAbsolutePath() + "\"");
ProxyServer.getInstance().stop();
// 3. Alle Override-Keys aus der alten Config sammeln
for (Map.Entry<String, String> entry : existingValues.entrySet()) {
if (entry.getKey().startsWith("override.")) {
pendingOverrides.add(entry);
}
}
} catch (Exception e) {
getLogger().severe("Fehler beim Vorbereiten des Updates: " + e.getMessage());
// 4. Datei Zeile für Zeile basierend auf Template schreiben
List<String> outputLines = new ArrayList<>();
Set<String> usedKeys = new HashSet<>();
for (int i = 0; i < templateLines.size(); i++) {
String line = templateLines.get(i);
String trimmed = line.trim();
// PRÜFUNG: Wenn wir am Anfang des nächsten Abschnitts sind ("Chat-Formate")
// Fügen wir die fehlenden Overrides VORHER ein.
if (trimmed.startsWith("#") && trimmed.contains("====")) {
if (i + 1 < templateLines.size()) {
String nextTrimmed = templateLines.get(i + 1).trim();
// Wenn wir kurz vor "Chat-Formate" stehen und noch Overrides offen sind -> Einfügen
if (nextTrimmed.contains("Chat-Formate") && !pendingOverrides.isEmpty()) {
outputLines.add("");
for (Map.Entry<String, String> ov : pendingOverrides) {
if (!usedKeys.contains(ov.getKey())) {
// Bereinigten Wert schreiben
outputLines.add(ov.getKey() + "=" + ov.getValue());
usedKeys.add(ov.getKey());
}
}
pendingOverrides.clear();
outputLines.add("");
}
}
}
// LOGIK: Template Zeile verarbeiten
if (!trimmed.startsWith("#") && !trimmed.isEmpty() && trimmed.contains("=")) {
String[] parts = trimmed.split("=", 2);
String key = parts[0].trim();
if (existingValues.containsKey(key)) {
// Wert aus alter Config übernehmen (bereinigt)
String cleanVal = unescapePropertiesString(existingValues.get(key));
outputLines.add(key + "=" + cleanVal);
usedKeys.add(key);
pendingOverrides.removeIf(e -> e.getKey().equals(key));
} else {
// Standardwert aus Template übernehmen
outputLines.add(line);
}
} else {
outputLines.add(line);
}
}
// Falls noch was übrig ist, am Ende anhängen
if (!pendingOverrides.isEmpty()) {
outputLines.add("");
for (Map.Entry<String, String> ov : pendingOverrides) {
outputLines.add(ov.getKey() + "=" + ov.getValue());
}
}
// 5. Datei schreiben
Files.write(file.toPath(), outputLines, StandardCharsets.UTF_8);
// 6. Properties laden
verifyProperties = new Properties();
try (FileInputStream fis = new FileInputStream(file)) {
verifyProperties.load(fis);
}
getLogger().info("verify.properties erfolgreich aktualisiert.");
} catch (IOException e) {
getLogger().severe("Fehler beim Merge der verify.properties: " + e.getMessage());
}
}
public Properties getVerifyProperties() {
synchronized (this) {
return verifyProperties;
}
}
// --- WebServer & JSON ---
@Override
public void run() {
try (ServerSocket serverSocket = new ServerSocket(port)) {
@@ -192,15 +476,6 @@ public class StatusAPI extends Plugin implements Runnable {
}
}
/**
* Erweiterter HTTP-Handler:
* - GET weiterhin liefert Status JSON wie bisher
* - POST an /broadcast oder POST an / (Root) wird als Broadcast verarbeitet
* (Payload JSON mit message, type, prefix, prefixColor, bracketColor, messageColor)
* - Wenn Feld scheduleTime (ms oder s) vorhanden ist, wird Nachricht als geplante Nachricht registriert
* und an BroadcastModule.scheduleBroadcast weitergegeben.
* - POST /broadcast/cancel erwartet clientScheduleId im Body und ruft cancelScheduled(clientScheduleId) auf.
*/
private void handleConnection(Socket clientSocket) {
try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), "UTF-8"));
OutputStream out = clientSocket.getOutputStream()) {
@@ -208,13 +483,11 @@ public class StatusAPI extends Plugin implements Runnable {
String inputLine = in.readLine();
if (inputLine == null) return;
// Request-Line zerlegen: METHOD PATH HTTP/VERSION
String[] reqParts = inputLine.split(" ");
if (reqParts.length < 2) return;
String method = reqParts[0].trim();
String path = reqParts[1].trim();
// Header einlesen (case-insensitive keys)
Map<String, String> headers = new HashMap<>();
String line;
while ((line = in.readLine()) != null && !line.isEmpty()) {
@@ -226,7 +499,7 @@ public class StatusAPI extends Plugin implements Runnable {
}
}
// --- POST /broadcast/cancel (optional) ---
// --- POST /broadcast/cancel ---
if ("POST".equalsIgnoreCase(method) && (path.equalsIgnoreCase("/broadcast/cancel") || path.equalsIgnoreCase("/cancel"))) {
int contentLength = 0;
if (headers.containsKey("content-length")) {
@@ -248,8 +521,8 @@ public class StatusAPI extends Plugin implements Runnable {
return;
}
Object mod = moduleManager.getModule("BroadcastModule");
if (mod instanceof net.viper.status.modules.broadcast.BroadcastModule) {
boolean ok = ((net.viper.status.modules.broadcast.BroadcastModule) mod).cancelScheduled(clientScheduleId);
if (mod instanceof BroadcastModule) {
boolean ok = ((BroadcastModule) mod).cancelScheduled(clientScheduleId);
if (ok) sendHttpResponse(out, "{\"success\":true}", 200);
else sendHttpResponse(out, "{\"success\":false,\"error\":\"not_found\"}", 404);
return;
@@ -259,7 +532,7 @@ public class StatusAPI extends Plugin implements Runnable {
}
}
// --- POST /broadcast oder POST / (Root) für register/send ---
// --- POST /broadcast ---
if ("POST".equalsIgnoreCase(method) && ("/broadcast".equalsIgnoreCase(path) || "/".equals(path) || path.isEmpty())) {
int contentLength = 0;
if (headers.containsKey("content-length")) {
@@ -277,34 +550,28 @@ public class StatusAPI extends Plugin implements Runnable {
}
String body = new String(bodyChars);
// Header X-Api-Key (case-insensitive)
String apiKeyHeader = headers.getOrDefault("x-api-key", headers.getOrDefault("x-apikey", ""));
// parse minimal JSON fields we need
String message = extractJsonString(body, "message");
String type = extractJsonString(body, "type");
String prefix = extractJsonString(body, "prefix");
String prefixColor = extractJsonString(body, "prefixColor");
String bracketColor = extractJsonString(body, "bracketColor"); // HINZUGEFÜGT
String bracketColor = extractJsonString(body, "bracketColor");
String messageColor = extractJsonString(body, "messageColor");
String sourceName = extractJsonString(body, "source");
String scheduleTimeStr = extractJsonString(body, "scheduleTime"); // expecting millis OR seconds
String scheduleTimeStr = extractJsonString(body, "scheduleTime");
String recur = extractJsonString(body, "recur");
String clientScheduleId = extractJsonString(body, "clientScheduleId");
if (sourceName == null || sourceName.isEmpty()) sourceName = "PulseCast";
if (type == null || type.isEmpty()) type = "global";
// if scheduleTime present -> register schedule with BroadcastModule
if (scheduleTimeStr != null && !scheduleTimeStr.trim().isEmpty()) {
long scheduleMillis = 0L;
try {
// scheduleTime may be numeric string (ms or s)
scheduleMillis = Long.parseLong(scheduleTimeStr.trim());
// if looks like seconds (less than 1e12), convert to ms
if (scheduleMillis < 1_000_000_000_000L) scheduleMillis = scheduleMillis * 1000L;
} catch (NumberFormatException ignored) {
// try to parse as double then convert
try {
double d = Double.parseDouble(scheduleTimeStr.trim());
long v = (long) d;
@@ -316,21 +583,15 @@ public class StatusAPI extends Plugin implements Runnable {
}
}
// BroadcastModule: scheduleBroadcast(timestampMillis, sourceName, message, type, apiKeyHeader, prefix, prefixColor, bracketColor, messageColor, recur, clientId)
try {
Object mod = moduleManager.getModule("BroadcastModule");
if (mod instanceof net.viper.status.modules.broadcast.BroadcastModule) {
net.viper.status.modules.broadcast.BroadcastModule bm =
(net.viper.status.modules.broadcast.BroadcastModule) mod;
if (mod instanceof BroadcastModule) {
BroadcastModule bm = (BroadcastModule) mod;
boolean ok = bm.scheduleBroadcast(scheduleMillis, sourceName, message, type, apiKeyHeader,
prefix, prefixColor, bracketColor, messageColor, (recur == null ? "none" : recur), (clientScheduleId == null ? null : clientScheduleId)); // bracketColor HINZUGEFÜGT
prefix, prefixColor, bracketColor, messageColor, (recur == null ? "none" : recur), (clientScheduleId == null ? null : clientScheduleId));
if (ok) {
sendHttpResponse(out, "{\"success\":true}", 200);
} else {
sendHttpResponse(out, "{\"success\":false,\"error\":\"rejected\"}", 403);
}
if (ok) sendHttpResponse(out, "{\"success\":true}", 200);
else sendHttpResponse(out, "{\"success\":false,\"error\":\"rejected\"}", 403);
return;
} else {
sendHttpResponse(out, "{\"success\":false,\"error\":\"no_broadcast_module\"}", 500);
@@ -343,7 +604,6 @@ public class StatusAPI extends Plugin implements Runnable {
}
}
// If no scheduleTime -> immediate broadcast
if (message == null || message.isEmpty()) {
String resp = "{\"success\":false,\"error\":\"missing_message\"}";
sendHttpResponse(out, resp, 400);
@@ -352,11 +612,9 @@ public class StatusAPI extends Plugin implements Runnable {
try {
Object mod = moduleManager.getModule("BroadcastModule");
if (mod instanceof net.viper.status.modules.broadcast.BroadcastModule) {
net.viper.status.modules.broadcast.BroadcastModule bm =
(net.viper.status.modules.broadcast.BroadcastModule) mod;
boolean ok = bm.handleBroadcast(sourceName, message, type, apiKeyHeader, prefix, prefixColor, bracketColor, messageColor); // bracketColor HINZUGEFÜGT
if (mod instanceof BroadcastModule) {
BroadcastModule bm = (BroadcastModule) mod;
boolean ok = bm.handleBroadcast(sourceName, message, type, apiKeyHeader, prefix, prefixColor, bracketColor, messageColor);
if (ok) sendHttpResponse(out, "{\"success\":true}", 200);
else sendHttpResponse(out, "{\"success\":false,\"error\":\"rejected\"}", 403);
@@ -372,12 +630,11 @@ public class StatusAPI extends Plugin implements Runnable {
}
}
// --- bisheriger GET-Handler (unverändert) ---
// --- GET Handler ---
if (inputLine != null && inputLine.startsWith("GET")) {
Map<String, Object> data = new LinkedHashMap<>();
data.put("online", true);
// Version & Info
String versionRaw = ProxyServer.getInstance().getVersion();
String versionClean = (versionRaw != null && versionRaw.contains(":")) ? versionRaw.split(":")[2].trim() : versionRaw;
data.put("version", versionClean);
@@ -390,7 +647,6 @@ public class StatusAPI extends Plugin implements Runnable {
} catch (Exception ignored) {}
data.put("motd", motd);
// StatsModul holen (Service Locator)
StatsModule statsModule = (StatsModule) moduleManager.getModule("StatsModule");
boolean luckPermsEnabled = ProxyServer.getInstance().getPluginManager().getPlugin("LuckPerms") != null;
@@ -420,7 +676,6 @@ public class StatusAPI extends Plugin implements Runnable {
}
playerInfo.put("prefix", prefix);
// Stats Integration via Modul
if (statsModule != null) {
PlayerStats ps = statsModule.getManager().getIfPresent(p.getUniqueId());
if (ps != null) {
@@ -451,10 +706,6 @@ public class StatusAPI extends Plugin implements Runnable {
}
}
/**
* Minimaler JSON-String-Extractor (robust genug für einfache Payloads).
* Liefert null, wenn Schlüssel nicht gefunden oder kein String-Wert.
*/
private String extractJsonString(String json, String key) {
if (json == null || key == null) return null;
String search = "\"" + key + "\"";
@@ -463,12 +714,11 @@ public class StatusAPI extends Plugin implements Runnable {
int colon = json.indexOf(':', idx + search.length());
if (colon < 0) return null;
int i = colon + 1;
// skip spaces
while (i < json.length() && Character.isWhitespace(json.charAt(i))) i++;
if (i >= json.length()) return null;
char c = json.charAt(i);
if (c == '"') {
i++; // skip opening quote
i++;
StringBuilder sb = new StringBuilder();
boolean escape = false;
while (i < json.length()) {
@@ -484,7 +734,6 @@ public class StatusAPI extends Plugin implements Runnable {
}
return sb.toString();
} else {
// not a quoted string -> read until comma or }
StringBuilder sb = new StringBuilder();
while (i < json.length()) {
char ch = json.charAt(i);

View File

@@ -0,0 +1,115 @@
package net.viper.status.modules.AutoMessage;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.plugin.Plugin;
import net.viper.status.StatusAPI;
import net.viper.status.module.Module;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
public class AutoMessageModule implements Module {
private int taskId = -1;
// Diese Methode fehlte bisher und ist zwingend für das Interface
@Override
public String getName() {
return "AutoMessage";
}
@Override
public void onEnable(Plugin plugin) {
// Hier casten wir das Plugin-Objekt zu StatusAPI, um an spezifische Methoden zu kommen
StatusAPI api = (StatusAPI) plugin;
// Konfiguration aus der zentralen verify.properties laden
Properties props = api.getVerifyProperties();
boolean enabled = Boolean.parseBoolean(props.getProperty("automessage.enabled", "false"));
if (!enabled) {
api.getLogger().info("AutoMessage-Modul ist deaktiviert.");
return;
}
// Interval in Sekunden einlesen
int intervalSeconds;
try {
intervalSeconds = Integer.parseInt(props.getProperty("automessage.interval", "300"));
} catch (NumberFormatException e) {
api.getLogger().warning("Ungültiges Intervall für AutoMessage! Nutze Standard (300s).");
intervalSeconds = 300;
}
// Dateiname einlesen (Standard: messages.txt)
String fileName = props.getProperty("automessage.file", "messages.txt");
File messageFile = new File(api.getDataFolder(), fileName);
if (!messageFile.exists()) {
api.getLogger().warning("Die Datei '" + fileName + "' wurde nicht gefunden (" + messageFile.getAbsolutePath() + ")!");
api.getLogger().info("Erstelle eine leere Datei '" + fileName + "' als Vorlage...");
try {
messageFile.createNewFile();
} catch (IOException e) {
api.getLogger().severe("Konnte Datei nicht erstellen: " + e.getMessage());
}
return;
}
// Nachrichten aus der Datei lesen
List<String> messages;
try {
messages = Files.readAllLines(messageFile.toPath(), StandardCharsets.UTF_8);
} catch (IOException e) {
api.getLogger().severe("Fehler beim Lesen von '" + fileName + "': " + e.getMessage());
return;
}
// Leere Zeilen und Kommentare herausfiltern
messages.removeIf(line -> line.trim().isEmpty() || line.trim().startsWith("#"));
if (messages.isEmpty()) {
api.getLogger().warning("Die Datei '" + fileName + "' enthält keine gültigen Nachrichten!");
return;
}
// Optional: Prefix aus Config lesen
String prefixRaw = props.getProperty("automessage.prefix", "");
String prefix = ChatColor.translateAlternateColorCodes('&', prefixRaw);
api.getLogger().info("Starte AutoMessage-Task (" + messages.size() + " Nachrichten aus " + fileName + ")");
// Finaler Index für den Lambda-Ausdruck
final int[] currentIndex = {0};
// Task planen
taskId = ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
String msg = messages.get(currentIndex[0]);
String finalMessage = (prefix.isEmpty() ? "" : prefix + " ") + msg;
// Nachricht an alle auf dem Proxy senden
ProxyServer.getInstance().broadcast(TextComponent.fromLegacy(finalMessage));
// Index erhöhen und Loop starten
currentIndex[0] = (currentIndex[0] + 1) % messages.size();
}, intervalSeconds, intervalSeconds, TimeUnit.SECONDS).getId();
}
@Override
public void onDisable(Plugin plugin) {
if (taskId != -1) {
ProxyServer.getInstance().getScheduler().cancel(taskId);
taskId = -1;
plugin.getLogger().info("AutoMessage-Task gestoppt.");
}
}
}

View File

@@ -0,0 +1,180 @@
package net.viper.status.modules.commandblocker;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.event.ChatEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.api.plugin.Plugin;
import net.md_5.bungee.event.EventHandler;
import net.viper.status.StatusAPI;
import net.viper.status.module.Module;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.util.*;
import org.yaml.snakeyaml.Yaml;
@SuppressWarnings({"unchecked", "rawtypes"})
public class CommandBlockerModule implements Module, Listener {
private StatusAPI plugin;
private boolean enabled = true; // Standardmäßig aktiv
private String bypassPermission = "commandblocker.bypass"; // Standard Permission
private File file;
private Set<String> blocked = new HashSet<>();
@Override
public String getName() {
return "CommandBlockerModule";
}
@Override
public void onEnable(Plugin plugin) {
if (!(plugin instanceof StatusAPI)) return; // Sicherheit
this.plugin = (StatusAPI) plugin;
// Datei laden
file = new File(this.plugin.getDataFolder(), "blocked-commands.yml");
loadFile();
// Listener registrieren
ProxyServer.getInstance().getPluginManager().registerListener(this.plugin, this);
// /cb Befehl registrieren
ProxyServer.getInstance().getPluginManager().registerCommand(this.plugin,
new net.md_5.bungee.api.plugin.Command("cb", "commandblocker.admin") {
@Override
public void execute(CommandSender sender, String[] args) {
handleCommand(sender, args);
}
});
this.plugin.getLogger().info("[CommandBlocker] aktiviert (" + blocked.size() + " Commands).");
}
@Override
public void onDisable(Plugin plugin) {
blocked.clear();
}
@EventHandler
public void onCommand(ChatEvent event) {
if (!enabled || !event.isCommand()) return;
if (!(event.getSender() instanceof ProxiedPlayer)) return;
ProxiedPlayer player = (ProxiedPlayer) event.getSender();
if (player.hasPermission(bypassPermission)) return;
String msg = event.getMessage();
if (msg == null || msg.length() <= 1) return;
String cmd = msg.substring(1).toLowerCase(Locale.ROOT);
String base = cmd.split(" ")[0];
if (blocked.contains(base)) {
event.setCancelled(true);
player.sendMessage(ChatColor.RED + "Dieser Befehl ist auf diesem Netzwerk blockiert.");
}
}
private void handleCommand(CommandSender sender, String[] args) {
if (args == null || args.length == 0) {
sender.sendMessage(ChatColor.YELLOW + "/cb add <command>");
sender.sendMessage(ChatColor.YELLOW + "/cb remove <command>");
sender.sendMessage(ChatColor.YELLOW + "/cb list");
sender.sendMessage(ChatColor.YELLOW + "/cb reload");
return;
}
String action = args[0].toLowerCase();
switch (action) {
case "add":
if (args.length < 2) break;
blocked.add(args[1].toLowerCase());
saveFile();
sender.sendMessage(ChatColor.GREEN + "Command blockiert: " + args[1]);
break;
case "remove":
if (args.length < 2) break;
blocked.remove(args[1].toLowerCase());
saveFile();
sender.sendMessage(ChatColor.GREEN + "Command freigegeben: " + args[1]);
break;
case "list":
sender.sendMessage(ChatColor.GOLD + "Blockierte Commands:");
for (String c : blocked) {
sender.sendMessage(ChatColor.RED + "- " + c);
}
break;
case "reload":
loadFile();
sender.sendMessage(ChatColor.GREEN + "CommandBlocker neu geladen.");
break;
default:
sender.sendMessage(ChatColor.RED + "Unbekannter Unterbefehl.");
break;
}
}
private void loadFile() {
try {
if (!file.exists()) {
File parent = file.getParentFile();
if (parent != null && !parent.exists()) parent.mkdirs();
file.createNewFile();
saveFile();
}
Yaml yaml = new Yaml();
Map<String, Object> data = null;
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
data = yaml.loadAs(fis, Map.class);
} finally {
if (fis != null) try { fis.close(); } catch (IOException ignored) {}
}
blocked.clear();
if (data != null && data.containsKey("blocked")) {
Object obj = data.get("blocked");
if (obj instanceof List) {
List list = (List) obj;
for (Object o : list) {
if (o != null) blocked.add(String.valueOf(o).toLowerCase());
}
}
}
} catch (Exception e) {
if (plugin != null) plugin.getLogger().severe("[CommandBlocker] Fehler beim Laden: " + e.getMessage());
else System.err.println("[CommandBlocker] Fehler beim Laden: " + e.getMessage());
}
}
private void saveFile() {
try {
Yaml yaml = new Yaml();
Map<String, Object> out = new LinkedHashMap<>();
out.put("blocked", new ArrayList<>(blocked));
FileWriter fw = null;
try {
fw = new FileWriter(file);
yaml.dump(out, fw);
} finally {
if (fw != null) try { fw.close(); } catch (IOException ignored) {}
}
} catch (IOException e) {
if (plugin != null) plugin.getLogger().severe("[CommandBlocker] Fehler beim Speichern: " + e.getMessage());
else System.err.println("[CommandBlocker] Fehler beim Speichern: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,222 @@
package net.viper.status.modules.customcommands;
import java.io.File;
import java.io.IOException;
import java.nio.file.CopyOption;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.config.ServerInfo;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.event.ChatEvent;
import net.md_5.bungee.api.plugin.Command;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.api.plugin.Plugin; // Import für das Interface Argument
import net.md_5.bungee.config.Configuration;
import net.md_5.bungee.config.ConfigurationProvider;
import net.md_5.bungee.config.YamlConfiguration;
import net.md_5.bungee.event.EventHandler;
import net.viper.status.StatusAPI;
import net.viper.status.module.Module;
public class CustomCommandModule implements Module, Listener {
private StatusAPI plugin;
private Configuration config;
private Command chatCommand;
public CustomCommandModule() {
// Leerer Konstruktor
}
@Override
public String getName() {
return "CustomCommandModule";
}
@Override
public void onEnable(Plugin plugin) {
// Hier casten wir 'Plugin' zu 'StatusAPI', da wir wissen, dass es das ist
this.plugin = (StatusAPI) plugin;
this.plugin.getLogger().info("Lade CustomCommandModule...");
reloadConfig();
// /bcmds Reload Befehl registrieren
this.plugin.getProxy().getPluginManager().registerCommand(this.plugin, new Command("bcmds") {
@Override
public void execute(CommandSender sender, String[] args) {
if (!sender.hasPermission("statusapi.bcmds")) {
sender.sendMessage(new TextComponent(ChatColor.RED + "You don't have permission."));
} else {
reloadConfig();
sender.sendMessage(new TextComponent(ChatColor.GREEN + "Config reloaded."));
}
}
});
// /chat Befehl registrieren (falls aktiviert)
if (config.getBoolean("chat-command", true)) {
chatCommand = new Command("chat") {
@Override
public void execute(CommandSender sender, String[] args) {
if (sender instanceof ProxiedPlayer) {
ProxiedPlayer player = (ProxiedPlayer) sender;
String msg = String.join(" ", args);
ChatEvent e = new ChatEvent(player, player.getServer(), msg);
ProxyServer.getInstance().getPluginManager().callEvent(e);
if (!e.isCancelled()) {
if (!e.isCommand() || !ProxyServer.getInstance().getPluginManager().dispatchCommand(sender, msg.substring(1))) {
player.chat(msg);
}
}
} else {
String msg = String.join(" ", args);
if(msg.startsWith("/")) {
ProxyServer.getInstance().getPluginManager().dispatchCommand(sender, msg.substring(1));
} else {
sender.sendMessage(new TextComponent("Console cannot send chat messages via /chat usually."));
}
}
}
};
this.plugin.getProxy().getPluginManager().registerCommand(this.plugin, chatCommand);
}
this.plugin.getProxy().getPluginManager().registerListener(this.plugin, this);
}
@Override
public void onDisable(Plugin plugin) {
// Optional: Cleanup logic, falls nötig.
// Wir nutzen hier das übergebene 'plugin' Argument (oder this.plugin, ist egal)
// Listener und Commands werden automatisch entfernt, wenn das Plugin stoppt.
}
public void reloadConfig() {
try {
if (!this.plugin.getDataFolder().exists()) {
this.plugin.getDataFolder().mkdirs();
}
File file = new File(this.plugin.getDataFolder(), "customcommands.yml");
if (!file.exists()) {
// Kopieren aus Resources
Files.copy(this.plugin.getResourceAsStream("customcommands.yml"), file.toPath(), new CopyOption[0]);
}
this.config = ConfigurationProvider.getProvider(YamlConfiguration.class).load(file);
} catch (IOException e) {
e.printStackTrace();
this.plugin.getLogger().severe("Konnte customcommands.yml nicht laden!");
}
}
@EventHandler(priority = 64)
public void onCommand(ChatEvent e) {
if (!e.isCommand()) return;
if (!(e.getSender() instanceof ProxiedPlayer)) return;
final ProxiedPlayer player = (ProxiedPlayer) e.getSender();
String[] split = e.getMessage().split(" ");
String label = split[0].substring(1);
final List<String> args = new ArrayList<>(Arrays.asList(split));
args.remove(0);
Configuration cmds = config.getSection("commands");
if (cmds == null) return;
Configuration section = null;
String foundKey = null;
for (String key : cmds.getKeys()) {
Configuration cmdSection = cmds.getSection(key);
if (key.equalsIgnoreCase(label)) {
section = cmdSection;
foundKey = key;
break;
}
for (String alias : cmdSection.getStringList("aliases")) {
if (alias.equalsIgnoreCase(label)) {
section = cmdSection;
foundKey = key;
break;
}
}
if (section != null) break;
}
if (section == null) return;
String type = section.getString("type", "line");
String sendertype = section.getString("sender", "default");
String permission = section.getString("permission", "");
final List<String> commands = section.getStringList("commands");
if (!permission.isEmpty() && !player.hasPermission(permission)) {
player.sendMessage(new TextComponent(ChatColor.RED + "You don't have permission."));
e.setCancelled(true);
return;
}
e.setCancelled(true);
final CommandSender target;
if (sendertype.equals("default")) {
target = player;
} else if (sendertype.equals("admin")) {
target = new ForwardSender(player, true);
} else if (sendertype.equals("console")) {
target = ProxyServer.getInstance().getConsole();
} else {
ProxiedPlayer targetPlayer = ProxyServer.getInstance().getPlayer(sendertype);
if (targetPlayer == null || !targetPlayer.isConnected()) {
player.sendMessage(new TextComponent(ChatColor.RED + "Player " + sendertype + " is not online."));
return;
}
target = targetPlayer;
}
String argsString = args.size() >= 1 ? String.join(" ", args) : "";
final String finalArgs = argsString;
final String senderName = player.getName();
if (type.equals("random")) {
int randomIndex = new Random().nextInt(commands.size());
String rawCommand = commands.get(randomIndex);
executeCommand(target, rawCommand, finalArgs, senderName);
} else if (type.equals("line")) {
ProxyServer.getInstance().getScheduler().runAsync(this.plugin, new Runnable() {
@Override
public void run() {
for (String rawCommand : commands) {
executeCommand(target, rawCommand, finalArgs, senderName);
try {
Thread.sleep(100L);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
});
} else {
this.plugin.getLogger().warning("Unknown type '" + type + "' for command " + foundKey);
}
}
private void executeCommand(CommandSender sender, String rawCommand, String args, String playerName) {
String parsed = rawCommand
.replaceAll("%args%", args)
.replaceAll("%sender%", playerName);
String commandToDispatch = parsed.startsWith("/") ? parsed.substring(1) : parsed;
ProxyServer.getInstance().getPluginManager().dispatchCommand(sender, commandToDispatch);
}
}

View File

@@ -0,0 +1,118 @@
package net.viper.status.modules.customcommands;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Collection;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.connection.Connection;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.connection.Connection.Unsafe;
public class ForwardSender implements CommandSender, Connection {
private ProxiedPlayer target;
private Boolean admin;
public ForwardSender(ProxiedPlayer sender, Boolean admin) {
this.target = sender;
this.admin = admin;
}
public ProxiedPlayer target() {
return this.target;
}
@Override
public String getName() {
return this.target.getName();
}
@Override
public void sendMessage(String message) {
this.target.sendMessage(message);
}
@Override
public void sendMessages(String... messages) {
this.target.sendMessages(messages);
}
@Override
public void sendMessage(BaseComponent... message) {
this.target.sendMessage(message);
}
@Override
public void sendMessage(BaseComponent message) {
this.target.sendMessage(message);
}
@Override
public Collection<String> getGroups() {
return this.target.getGroups();
}
@Override
public void addGroups(String... groups) {
this.target.addGroups(groups);
}
@Override
public void removeGroups(String... groups) {
this.target.removeGroups(groups);
}
@Override
public boolean hasPermission(String permission) {
return this.admin ? true : this.target.hasPermission(permission);
}
@Override
public void setPermission(String permission, boolean value) {
this.target.setPermission(permission, value);
}
@Override
public Collection<String> getPermissions() {
Collection<String> perms = this.target.getPermissions();
if (this.admin) {
perms.add("*");
}
return perms;
}
@Override
public InetSocketAddress getAddress() {
return this.target.getAddress();
}
@Override
public SocketAddress getSocketAddress() {
return this.target.getSocketAddress();
}
@Override
public void disconnect(String reason) {
this.target.disconnect(reason);
}
@Override
public void disconnect(BaseComponent... reason) {
this.target.disconnect(reason);
}
@Override
public void disconnect(BaseComponent reason) {
this.target.disconnect(reason);
}
@Override
public boolean isConnected() {
return this.target.isConnected();
}
@Override
public Unsafe unsafe() {
return this.target.unsafe();
}
}

View File

@@ -0,0 +1,41 @@
# If you want to enable /chat
chat-command: true
# The commands
commands:
test:
aliases:
- test2
permission: ""
type: random
sender: default
commands:
- "alert Das ist ein Random Test Befehl!"
- "glist"
anothercommand:
aliases:
- thisisacommand
permission: "myplugin.mypermission"
type: line
sender: default
commands:
- "alert Hello World!"
- "alert How're you?"
- "alert I'd like to tell you that %args%"
# - "test" # Vorsicht: Rekursion, falls 'test' in dieser Liste steht und 'test' selbst definiert ist
sudo:
aliases:
- runasadmin
permission: "commands.sudo"
type: line
sender: admin
commands:
- "alert Running as Admin: %args%"
- "%args%"
something:
aliases:
- anything
permission: "some.permission"
type: line
sender: console
commands:
- "alert %sender% would like to say that %args%"

View File

@@ -0,0 +1,18 @@
§8[§2Viper-Netzwerk§8] §7Der Server läuft 24/7 also keine Hektik beim Spielen :)
§8[§2Viper-Netzwerk§8] §7Dies ist ein privater Server hier zählt der Zusammenhalt.
§8[§dTipp§8] §7Wenn du denkst, du bist sicher… schau nochmal nach. Creeper machen keine Geräusche beim Tippen.
§8[§2Viper-Netzwerk§8] §7Wähle einen Server, leg los der Rest ergibt sich. Oder explodiert.
§8[§2Viper-Netzwerk§8] §7Mehr Server. Mehr Blöcke. Mehr Unfälle. Willkommen!
§8[§dTipp§8] §7Halte eine Spitzhacke mit Glück bereit. Man weiß nie, wann das nächste Erz kommt.
§8[§dTipp§8] §7Mit §e/home§7 kannst du dich jederzeit nach Hause teleportieren.
§8[§2Viper-Netzwerk§8] §7Das wichtigste Plugin? Du selbst. Spiel fair, sei kreativ!
§8[§2Viper-Netzwerk§8] §7Redstone ist keine Magie aber fast.
§8[§dTipp§8] §7Schilde sind cool. Besonders wenn Skelette zielen.
§8[§2Viper-Netzwerk§8] §7Wenn du in Lava fällst, bist du nicht der Erste. Nur der Nächste.
§8[§dTipp§8] §7Villager sind nicht dumm nur sehr… eigen.
§8[§2Viper-Netzwerk§8] §7Bau groß, bau sicher oder bau eine Treppe zur Nachbarschaftsklage.
§8[§2Viper-Netzwerk§8] §7Gras wächst. Spieler auch. Gib jedem eine Chance!
§8[§2Viper-Netzwerk§8] §7Ein Creeper ist keine Begrüßung. Es sei denn, du willst es spannend machen.
§8[§dTipp§8] §7Ein voller Magen ist halbe Miete. Farmen lohnt sich!
§8[§2Viper-Netzwerk§8] §7Wir haben keine Probleme nur Redstone-Schaltungen mit Charakter.
§8[§dTipp§8] §7Markiere dein Grundstück mit §e/p claim§7, bevor es jemand anderes tut!

View File

@@ -1,6 +1,6 @@
name: StatusAPI
main: net.viper.status.StatusAPI
version: 4.0.5
version: 4.0.8
author: M_Viper
description: StatusAPI für BungeeCord inkl. Update-Checker und Modul-System

View File

@@ -1,3 +1,10 @@
# _____ __ __ ___ ____ ____
# / ___// /_____ _/ /___ _______/ | / __ \/ _/
# \__ \/ __/ __ `/ __/ / / / ___/ /| | / /_/ // /
# ___/ / /_/ /_/ / /_/ /_/ (__ ) ___ |/ ____// /
# /____/\__/\__,_/\__/\__,_/____/_/ |_/_/ /___/
# ===========================
# GLOBALCHAT AKTIVIERUNG
# ===========================
@@ -57,8 +64,6 @@ server.skyblock.secret=GeheimesWortFuerSkyBlock789
# Beispiel: Deinen UUID hier einfügen
override.uuid-hier-einfügen = Owner
override.uuid-hier-einfügen = Admin
override.uuid-hier-einfügen = Developer
# ===========================
# Chat-Formate für Gruppen
@@ -74,3 +79,32 @@ groupformat.admin=&4[Admin] || &9 || &c
groupformat.developer=&b[Dev] || &3 || &a
groupformat.premium=&6[Premium] || &e || &7
groupformat.spieler=&f[Spieler] || &7 || &8
# ===========================
# AUTOMESSAGE
# ===========================
# Aktiviert den automatischen Nachrichten-Rundruf
automessage.enabled=false
# Zeitintervall in Sekunden (Standard: 300 = 5 Minuten)
automessage.interval=300
# Optional: Ein Prefix, das VOR jede Nachricht aus der Datei gesetzt wird.
# Wenn du das Prefix bereits IN der messages.txt hast, lass dieses Feld einfach leer.
automessage.prefix=
# Der Name der Datei, in der die Nachrichten stehen (liegt im Plugin-Ordner)
automessage.file=messages.txt
# ===========================
# COMMAND BLOCKER
# ===========================
commandblocker.enabled=true
commandblocker.bypass.permission=commandblocker.bypass
# ===========================
# CUSTOM COMMANDS
# ===========================
# Aktiviert das Modul für benutzerdefinierte Befehle und Aliases.
# Wenn aktiviert, wird die Datei 'customcommands.yml' geladen.
customcommands.enabled=true

View File

@@ -0,0 +1,88 @@
# _____ __ __ ___ ____ ____
# / ___// /_____ _/ /___ _______/ | / __ \/ _/
# \__ \/ __/ __ `/ __/ / / / ___/ /| | / /_/ // /
# ___/ / /_/ /_/ / /_/ /_/ (__ ) ___ |/ ____// /
# /____/\__/\__,_/\__/\__,_/____/_/ |_/_/ /___/
# ===========================
# GLOBALCHAT AKTIVIERUNG
# ===========================
chat.enabled=false
# ------------------------------
# Broadcast
# ------------------------------
broadcast.enabled=false
broadcast.prefix=[Broadcast]
broadcast.prefix-color=&c
broadcast.message-color=&f
broadcast.format=%prefixColored% %messageColored%
# broadcast.format kann angepasst werden; nutze Platzhalter: %name%, %prefix%, %prefixColored%, %message%, %messageColored%, %type%
# ===========================
# NAVIGATION / SERVER SWITCHER
# ===========================
# Hier kannst du das interne Navigationssystem aktivieren/deaktivieren.
# Wenn aktiviert, erstellt das Plugin automatisch Befehle basierend auf den Servernamen (z.B. /lobby, /survival).
navigation.enabled=false
# ===========================
# WORDPRESS / VERIFY EINSTELLUNGEN
# ===========================
wp_verify_url=https://deine-wp-domain.tld
# ===========================
# SERVER KONFIGURATION
# ===========================
# Hier legst du für jeden Server alles fest:
# 1. Den Anzeigenamen für den Chat (z.B. &bLobby)
# 2. Die Server ID für WordPress (z.B. id=1)
# 3. Das Secret für WordPress (z.B. secret=...)
# Server 1: Lobby
server.lobby=&bLobby
server.lobby.id=1
server.lobby.secret=GeheimesWortFuerLobby123
# Server 2: Survival
server.survival=&aSurvival
server.survival.id=2
server.survival.secret=GeheimesWortFuerSurvival456
# Server 3: SkyBlock
server.skyblock=&dSkyBlock
server.skyblock.id=3
server.skyblock.secret=GeheimesWortFuerSkyBlock789
# ===========================
# Manuelle Ränge (Overrides)
# ===========================
# Syntax: override.<Spieler-UUID> = <Gruppenname>
# WICHTIG: Die Gruppe (z.B. Owner) muss unten bei groupformat definiert sein!
# Beispiel: Deinen UUID hier einfügen
override.uuid-hier-einfügen = Owner
# ===========================
# Chat-Formate für Gruppen
# ===========================
# Der Name hinter dem Punkt (z.B. Owner) muss exakt mit der LuckPerms Gruppe übereinstimmen.
# Nutze & für Farbcodes.
# Ränge mit neuer Syntax: Rank || Spielerfarbe || Chatfarbe
# Beispiel: Rot (Rang) || Blau (Name) || Lila (Chat)
groupformat.owner=&c[Owner] || &b || &d
groupformat.admin=&4[Admin] || &9 || &c
groupformat.developer=&b[Dev] || &3 || &a
groupformat.premium=&6[Premium] || &e || &7
groupformat.spieler=&f[Spieler] || &7 || &8
# ===========================
# COMMAND BLOCKER
# ===========================
commandblocker.enabled=true
commandblocker.bypass.permission=commandblocker.bypass