Update from Git Manager GUI
This commit is contained in:
602
src/main/java/net/viper/status/StatusAPI.java
Normal file
602
src/main/java/net/viper/status/StatusAPI.java
Normal file
@@ -0,0 +1,602 @@
|
||||
package net.viper.status;
|
||||
|
||||
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 net.viper.status.module.ModuleManager;
|
||||
import net.viper.status.stats.PlayerStats;
|
||||
import net.viper.status.stats.StatsModule;
|
||||
import net.viper.status.modules.verify.VerifyModule;
|
||||
import net.viper.status.modules.commandblocker.CommandBlockerModule;
|
||||
import net.viper.status.modules.broadcast.BroadcastModule;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
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;
|
||||
|
||||
/**
|
||||
* StatusAPI - zentraler Bungee HTTP-Status- und Broadcast-Endpunkt
|
||||
*/
|
||||
|
||||
public class StatusAPI extends Plugin implements Runnable {
|
||||
|
||||
private Thread thread;
|
||||
private int port = 9191;
|
||||
|
||||
private ModuleManager moduleManager;
|
||||
private UpdateChecker updateChecker;
|
||||
private Properties verifyProperties;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
getLogger().info("StatusAPI Core wird initialisiert...");
|
||||
|
||||
if (!getDataFolder().exists()) {
|
||||
getDataFolder().mkdirs();
|
||||
}
|
||||
|
||||
// Config mergen/updaten
|
||||
mergeVerifyConfig();
|
||||
|
||||
// Port aus verify.properties lesen
|
||||
String portStr = verifyProperties != null ? verifyProperties.getProperty("statusapi.port", "9191") : "9191";
|
||||
try {
|
||||
port = Integer.parseInt(portStr);
|
||||
} catch (NumberFormatException e) {
|
||||
getLogger().warning("Ungültiger Port in verify.properties, nutze Standard-Port 9191.");
|
||||
port = 9191;
|
||||
}
|
||||
|
||||
moduleManager = new ModuleManager();
|
||||
|
||||
// Module registrieren
|
||||
moduleManager.registerModule(new StatsModule());
|
||||
moduleManager.registerModule(new VerifyModule());
|
||||
moduleManager.registerModule(new BroadcastModule());
|
||||
moduleManager.registerModule(new CommandBlockerModule());
|
||||
// ForumBridge für /forumlink
|
||||
try {
|
||||
Class<?> forumBridge = Class.forName("net.viper.status.modules.forum.ForumBridgeModule");
|
||||
Object forumBridgeInstance = forumBridge.getDeclaredConstructor().newInstance();
|
||||
moduleManager.registerModule((net.viper.status.module.Module) forumBridgeInstance);
|
||||
} catch (Exception e) {
|
||||
getLogger().warning("ForumBridgeModule konnte nicht geladen werden: " + e.getMessage());
|
||||
}
|
||||
|
||||
moduleManager.enableAll(this);
|
||||
|
||||
// WebServer starten
|
||||
getLogger().info("Starte Web-Server auf Port " + port + "...");
|
||||
thread = new Thread(this, "StatusAPI-HTTP-Server");
|
||||
thread.start();
|
||||
|
||||
// Update System
|
||||
String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0";
|
||||
updateChecker = new UpdateChecker(this, currentVersion, 6);
|
||||
// ...fileDownloader entfernt...
|
||||
|
||||
checkAndMaybeUpdate();
|
||||
ProxyServer.getInstance().getScheduler().schedule(this, this::checkAndMaybeUpdate, 6, 6, TimeUnit.HOURS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
getLogger().info("Stoppe Module...");
|
||||
if (moduleManager != null) {
|
||||
moduleManager.disableAll(this);
|
||||
}
|
||||
|
||||
getLogger().info("Stoppe Web-Server...");
|
||||
if (thread != null) {
|
||||
thread.interrupt();
|
||||
try { thread.join(1000); } catch (InterruptedException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
Properties p = new Properties();
|
||||
p.load(new StringReader("dummy=" + input));
|
||||
return p.getProperty("dummy");
|
||||
} catch (IOException e) {
|
||||
return input; // Fallback, falls Parsing fehlschlägt
|
||||
}
|
||||
}
|
||||
|
||||
// --- MERGE LOGIK ---
|
||||
private void mergeVerifyConfig() {
|
||||
try {
|
||||
File file = new File(getDataFolder(), "verify.properties");
|
||||
verifyProperties = new Properties();
|
||||
if (file.exists()) {
|
||||
try (FileInputStream fis = new FileInputStream(file)) {
|
||||
verifyProperties.load(fis);
|
||||
}
|
||||
getLogger().info("verify.properties geladen.");
|
||||
} else {
|
||||
getLogger().warning("verify.properties nicht gefunden.");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
getLogger().severe("Fehler beim Laden der verify.properties: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public Properties getVerifyProperties() {
|
||||
synchronized (this) {
|
||||
return verifyProperties;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Update Logik ---
|
||||
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("----------------------------------------");
|
||||
|
||||
File pluginFile = getFile();
|
||||
File newFile = new File(pluginFile.getParentFile(), "StatusAPI.jar.new");
|
||||
|
||||
// ...fileDownloader entfernt...
|
||||
}
|
||||
} catch (Exception e) {
|
||||
getLogger().severe("Fehler beim Update-Check: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void triggerUpdateScript(File currentFile, File newFile) {
|
||||
try {
|
||||
File pluginsFolder = currentFile.getParentFile();
|
||||
File rootFolder = pluginsFolder.getParentFile();
|
||||
File batFile = new File(rootFolder, "StatusAPI_Update_" + System.currentTimeMillis() + ".bat");
|
||||
|
||||
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\"";
|
||||
|
||||
try (PrintWriter out = new PrintWriter(batFile)) { out.println(batContent); }
|
||||
|
||||
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
||||
p.disconnect("§cServer fährt für ein Update neu herunter. Bitte etwas warten.");
|
||||
}
|
||||
|
||||
getLogger().info("Starte Update-Skript...");
|
||||
Runtime.getRuntime().exec("cmd /c start \"Update_Proc\" \"" + batFile.getAbsolutePath() + "\"");
|
||||
ProxyServer.getInstance().stop();
|
||||
|
||||
} catch (Exception e) {
|
||||
getLogger().severe("Fehler beim Vorbereiten des Updates: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// --- WebServer & JSON ---
|
||||
@Override
|
||||
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) {}
|
||||
catch (IOException e) {
|
||||
getLogger().warning("Fehler beim Akzeptieren einer Verbindung: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
getLogger().severe("Konnte ServerSocket nicht starten: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleConnection(Socket clientSocket) {
|
||||
|
||||
// (doppelter/fehlerhafter Block entfernt)
|
||||
try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), "UTF-8"));
|
||||
OutputStream out = clientSocket.getOutputStream()) {
|
||||
|
||||
String inputLine = in.readLine();
|
||||
if (inputLine == null) return;
|
||||
|
||||
String[] reqParts = inputLine.split(" ");
|
||||
if (reqParts.length < 2) return;
|
||||
String method = reqParts[0].trim();
|
||||
String path = reqParts[1].trim();
|
||||
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
String line;
|
||||
while ((line = in.readLine()) != null && !line.isEmpty()) {
|
||||
int idx = line.indexOf(':');
|
||||
if (idx > 0) {
|
||||
String key = line.substring(0, idx).trim().toLowerCase(Locale.ROOT);
|
||||
String val = line.substring(idx + 1).trim();
|
||||
headers.put(key, val);
|
||||
}
|
||||
}
|
||||
|
||||
// --- POST /forum/notify ---
|
||||
if ("POST".equalsIgnoreCase(method) && path.equalsIgnoreCase("/forum/notify")) {
|
||||
int contentLength = 0;
|
||||
if (headers.containsKey("content-length")) {
|
||||
try { contentLength = Integer.parseInt(headers.get("content-length")); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
char[] bodyChars = new char[Math.max(0, contentLength)];
|
||||
if (contentLength > 0) {
|
||||
int read = 0;
|
||||
while (read < contentLength) {
|
||||
int r = in.read(bodyChars, read, contentLength - read);
|
||||
if (r == -1) break;
|
||||
read += r;
|
||||
}
|
||||
}
|
||||
String body = new String(bodyChars);
|
||||
String apiKeyHeader = headers.getOrDefault("x-api-key", headers.getOrDefault("x-apikey", ""));
|
||||
Object mod = moduleManager.getModule("ForumBridgeModule");
|
||||
if (mod != null) {
|
||||
try {
|
||||
java.lang.reflect.Method m = mod.getClass().getMethod("handleNotify", String.class, String.class);
|
||||
String resp = (String) m.invoke(mod, body, apiKeyHeader);
|
||||
sendHttpResponse(out, resp, 200);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
sendHttpResponse(out, "{\"success\":false,\"error\":\"internal\"}", 500);
|
||||
}
|
||||
} else {
|
||||
sendHttpResponse(out, "{\"success\":false,\"error\":\"no_forum_module\"}", 500);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// --- POST /broadcast/cancel ---
|
||||
if ("POST".equalsIgnoreCase(method) && (path.equalsIgnoreCase("/broadcast/cancel") || path.equalsIgnoreCase("/cancel"))) {
|
||||
int contentLength = 0;
|
||||
if (headers.containsKey("content-length")) {
|
||||
try { contentLength = Integer.parseInt(headers.get("content-length")); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
char[] bodyChars = new char[Math.max(0, contentLength)];
|
||||
if (contentLength > 0) {
|
||||
int read = 0;
|
||||
while (read < contentLength) {
|
||||
int r = in.read(bodyChars, read, contentLength - read);
|
||||
if (r == -1) break;
|
||||
read += r;
|
||||
}
|
||||
}
|
||||
String body = new String(bodyChars);
|
||||
String clientScheduleId = extractJsonString(body, "clientScheduleId");
|
||||
if (clientScheduleId == null || clientScheduleId.isEmpty()) {
|
||||
sendHttpResponse(out, "{\"success\":false,\"error\":\"missing_clientScheduleId\"}", 400);
|
||||
return;
|
||||
}
|
||||
Object mod = moduleManager.getModule("BroadcastModule");
|
||||
if (mod instanceof BroadcastModule) {
|
||||
boolean ok = ((BroadcastModule) mod).cancelScheduled(clientScheduleId);
|
||||
if (ok) sendHttpResponse(out, "{\"success\":true}", 200);
|
||||
else sendHttpResponse(out, "{\"success\":false,\"error\":\"not_found\"}", 404);
|
||||
return;
|
||||
} else {
|
||||
sendHttpResponse(out, "{\"success\":false,\"error\":\"no_broadcast_module\"}", 500);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// --- POST /broadcast ---
|
||||
if ("POST".equalsIgnoreCase(method) && ("/broadcast".equalsIgnoreCase(path) || "/".equals(path) || path.isEmpty())) {
|
||||
int contentLength = 0;
|
||||
if (headers.containsKey("content-length")) {
|
||||
try { contentLength = Integer.parseInt(headers.get("content-length")); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
char[] bodyChars = new char[Math.max(0, contentLength)];
|
||||
if (contentLength > 0) {
|
||||
int read = 0;
|
||||
while (read < contentLength) {
|
||||
int r = in.read(bodyChars, read, contentLength - read);
|
||||
if (r == -1) break;
|
||||
read += r;
|
||||
}
|
||||
}
|
||||
String body = new String(bodyChars);
|
||||
|
||||
String apiKeyHeader = headers.getOrDefault("x-api-key", headers.getOrDefault("x-apikey", ""));
|
||||
|
||||
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");
|
||||
String messageColor = extractJsonString(body, "messageColor");
|
||||
String sourceName = extractJsonString(body, "source");
|
||||
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 (scheduleTimeStr != null && !scheduleTimeStr.trim().isEmpty()) {
|
||||
long scheduleMillis = 0L;
|
||||
try {
|
||||
scheduleMillis = Long.parseLong(scheduleTimeStr.trim());
|
||||
if (scheduleMillis < 1_000_000_000_000L) scheduleMillis = scheduleMillis * 1000L;
|
||||
} catch (NumberFormatException ignored) {
|
||||
try {
|
||||
double d = Double.parseDouble(scheduleTimeStr.trim());
|
||||
long v = (long) d;
|
||||
if (v < 1_000_000_000_000L) v = v * 1000L;
|
||||
scheduleMillis = v;
|
||||
} catch (Exception ex) {
|
||||
sendHttpResponse(out, "{\"success\":false,\"error\":\"bad_scheduleTime\"}", 400);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Object mod = moduleManager.getModule("BroadcastModule");
|
||||
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));
|
||||
|
||||
if (ok) sendHttpResponse(out, "{\"success\":true}", 200);
|
||||
else sendHttpResponse(out, "{\"success\":false,\"error\":\"rejected\"}", 403);
|
||||
return;
|
||||
} else {
|
||||
sendHttpResponse(out, "{\"success\":false,\"error\":\"no_broadcast_module\"}", 500);
|
||||
return;
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
t.printStackTrace();
|
||||
sendHttpResponse(out, "{\"success\":false,\"error\":\"internal\"}", 500);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (message == null || message.isEmpty()) {
|
||||
String resp = "{\"success\":false,\"error\":\"missing_message\"}";
|
||||
sendHttpResponse(out, resp, 400);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Object mod = moduleManager.getModule("BroadcastModule");
|
||||
if (mod instanceof 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);
|
||||
return;
|
||||
} else {
|
||||
sendHttpResponse(out, "{\"success\":false,\"error\":\"no_broadcast_module\"}", 500);
|
||||
return;
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
t.printStackTrace();
|
||||
sendHttpResponse(out, "{\"success\":false,\"error\":\"internal\"}", 500);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// --- GET Handler ---
|
||||
if (inputLine != null && inputLine.startsWith("GET")) {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("online", true);
|
||||
|
||||
String versionRaw = ProxyServer.getInstance().getVersion();
|
||||
String versionClean = (versionRaw != null && versionRaw.contains(":")) ? versionRaw.split(":")[2].trim() : versionRaw;
|
||||
data.put("version", versionClean);
|
||||
data.put("max_players", String.valueOf(ProxyServer.getInstance().getConfig().getPlayerLimit()));
|
||||
|
||||
String motd = "BungeeCord";
|
||||
try {
|
||||
Iterator<ListenerInfo> it = ProxyServer.getInstance().getConfig().getListeners().iterator();
|
||||
if (it.hasNext()) motd = it.next().getMotd();
|
||||
} catch (Exception ignored) {}
|
||||
data.put("motd", motd);
|
||||
|
||||
StatsModule statsModule = (StatsModule) moduleManager.getModule("StatsModule");
|
||||
|
||||
boolean luckPermsEnabled = ProxyServer.getInstance().getPluginManager().getPlugin("LuckPerms") != null;
|
||||
List<Map<String, Object>> playersList = new ArrayList<>();
|
||||
|
||||
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
||||
Map<String, Object> playerInfo = new LinkedHashMap<>();
|
||||
playerInfo.put("name", p.getName());
|
||||
try { playerInfo.put("uuid", p.getUniqueId().toString()); } catch (Exception ignored) {}
|
||||
|
||||
// Floodgate-Bedrock-Erkennung
|
||||
boolean isBedrock = false;
|
||||
String bedrockId = null;
|
||||
try {
|
||||
Class<?> floodgateApi = Class.forName("org.geysermc.floodgate.api.FloodgateApi");
|
||||
Object api = floodgateApi.getMethod("getInstance").invoke(null);
|
||||
isBedrock = (boolean) api.getClass().getMethod("isBedrockPlayer", UUID.class).invoke(api, p.getUniqueId());
|
||||
if (isBedrock) {
|
||||
bedrockId = (String) api.getClass().getMethod("getBedrockId", UUID.class).invoke(api, p.getUniqueId());
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
playerInfo.put("isBedrock", isBedrock);
|
||||
if (bedrockId != null) playerInfo.put("bedrockId", bedrockId);
|
||||
|
||||
String prefix = "";
|
||||
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);
|
||||
|
||||
if (statsModule != null) {
|
||||
PlayerStats ps = statsModule.getManager().getIfPresent(p.getUniqueId());
|
||||
if (ps != null) {
|
||||
playerInfo.put("playtime", ps.getPlaytimeWithCurrentSession());
|
||||
playerInfo.put("joins", ps.joins);
|
||||
playerInfo.put("first_seen", ps.firstSeen);
|
||||
playerInfo.put("last_seen", ps.lastSeen);
|
||||
}
|
||||
}
|
||||
playersList.add(playerInfo);
|
||||
}
|
||||
data.put("players", playersList);
|
||||
|
||||
String json = buildJsonString(data);
|
||||
byte[] jsonBytes = json.getBytes("UTF-8");
|
||||
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\r\n");
|
||||
out.write(response.toString().getBytes("UTF-8"));
|
||||
out.write(jsonBytes);
|
||||
out.flush();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
getLogger().severe("Fehler beim Verarbeiten der Anfrage: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String extractJsonString(String json, String key) {
|
||||
if (json == null || key == null) return null;
|
||||
String search = "\"" + key + "\"";
|
||||
int idx = json.indexOf(search);
|
||||
if (idx < 0) return null;
|
||||
int colon = json.indexOf(':', idx + search.length());
|
||||
if (colon < 0) return null;
|
||||
int i = colon + 1;
|
||||
while (i < json.length() && Character.isWhitespace(json.charAt(i))) i++;
|
||||
if (i >= json.length()) return null;
|
||||
char c = json.charAt(i);
|
||||
if (c == '"') {
|
||||
i++;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
boolean escape = false;
|
||||
while (i < json.length()) {
|
||||
char ch = json.charAt(i++);
|
||||
if (escape) {
|
||||
sb.append(ch);
|
||||
escape = false;
|
||||
} else {
|
||||
if (ch == '\\') escape = true;
|
||||
else if (ch == '"') break;
|
||||
else sb.append(ch);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
} else {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
while (i < json.length()) {
|
||||
char ch = json.charAt(i);
|
||||
if (ch == ',' || ch == '}' || ch == '\n' || ch == '\r') break;
|
||||
sb.append(ch);
|
||||
i++;
|
||||
}
|
||||
return sb.toString().trim();
|
||||
}
|
||||
}
|
||||
|
||||
private void sendHttpResponse(OutputStream out, String json, int code) throws IOException {
|
||||
byte[] jsonBytes = json.getBytes(StandardCharsets.UTF_8);
|
||||
StringBuilder response = new StringBuilder();
|
||||
response.append("HTTP/1.1 ").append(code).append(" ").append(code == 200 ? "OK" : "ERROR").append("\r\n");
|
||||
response.append("Content-Type: application/json; charset=UTF-8\r\n");
|
||||
response.append("Access-Control-Allow-Origin: *\r\n");
|
||||
response.append("Content-Length: ").append(jsonBytes.length).append("\r\n");
|
||||
response.append("Connection: close\r\n\r\n");
|
||||
out.write(response.toString().getBytes(StandardCharsets.UTF_8));
|
||||
out.write(jsonBytes);
|
||||
out.flush();
|
||||
}
|
||||
|
||||
private String buildJsonString(Map<String, Object> data) {
|
||||
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("\":").append(valueToString(entry.getValue()));
|
||||
}
|
||||
sb.append("}");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String valueToString(Object value) {
|
||||
if (value == null) return "null";
|
||||
else if (value instanceof Boolean) return value.toString();
|
||||
else if (value instanceof Number) return value.toString();
|
||||
else if (value instanceof Map) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> m = (Map<String, Object>) value;
|
||||
return buildJsonString(m);
|
||||
} 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);
|
||||
if (item instanceof Map) sb.append(buildJsonString((Map<String, Object>) item));
|
||||
else if (item instanceof Number) sb.append(item.toString());
|
||||
else sb.append("\"").append(escapeJson(String.valueOf(item))).append("\"");
|
||||
}
|
||||
sb.append("]");
|
||||
return sb.toString();
|
||||
}
|
||||
else return "\"" + escapeJson(String.valueOf(value)) + "\"";
|
||||
}
|
||||
|
||||
private String escapeJson(String s) {
|
||||
if (s == null) return "";
|
||||
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r");
|
||||
}
|
||||
}
|
||||
144
src/main/java/net/viper/status/UpdateChecker.java
Normal file
144
src/main/java/net/viper/status/UpdateChecker.java
Normal file
@@ -0,0 +1,144 @@
|
||||
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;
|
||||
|
||||
// Neue Domain und korrekter API-Pfad für Releases
|
||||
private final String apiUrl = "https://git.viper.ipv64.net/api/v1/repos/M_Viper/StatusAPI/releases";
|
||||
|
||||
private volatile String latestVersion = "";
|
||||
private volatile String latestUrl = "";
|
||||
|
||||
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);
|
||||
|
||||
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/Forgejo 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();
|
||||
|
||||
// Neu: Da die API ein JSON-Array von Releases zurückgibt, nehmen wir das erste (neueste) Release
|
||||
// Wir suchen den ersten Block mit tag_name
|
||||
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;
|
||||
}
|
||||
if (foundVersion.startsWith("v") || foundVersion.startsWith("V")) {
|
||||
foundVersion = foundVersion.substring(1);
|
||||
}
|
||||
|
||||
String foundUrl = null;
|
||||
|
||||
// Wir suchen im gesamten Body nach der JAR-Datei "StatusAPI.jar"
|
||||
// Da das neueste Release zuerst kommt, brechen wir ab, sobald wir eine passende JAR finden
|
||||
Matcher nameMatcher = ASSET_NAME_PATTERN.matcher(body);
|
||||
Matcher downloadMatcher = DOWNLOAD_PATTERN.matcher(body);
|
||||
|
||||
java.util.List<String> names = new java.util.ArrayList<>();
|
||||
java.util.List<String> urls = new java.util.ArrayList<>();
|
||||
|
||||
while (nameMatcher.find()) {
|
||||
names.add(nameMatcher.group(1));
|
||||
}
|
||||
while (downloadMatcher.find()) {
|
||||
urls.add(downloadMatcher.group(1));
|
||||
}
|
||||
|
||||
int pairs = Math.min(names.size(), urls.size());
|
||||
for (int i = 0; i < pairs; i++) {
|
||||
String name = names.get(i).trim();
|
||||
String url = urls.get(i);
|
||||
if ("StatusAPI.jar".equalsIgnoreCase(name)) {
|
||||
foundUrl = url;
|
||||
break; // Erste (also neueste) passende JAR nehmen
|
||||
}
|
||||
}
|
||||
|
||||
if (foundUrl == null) {
|
||||
plugin.getLogger().warning("Keine StatusAPI.jar im neuesten 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/main/java/net/viper/status/module/Module.java
Normal file
24
src/main/java/net/viper/status/module/Module.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package net.viper.status.module;
|
||||
|
||||
import net.md_5.bungee.api.plugin.Plugin;
|
||||
|
||||
/**
|
||||
* Interface für alle zukünftigen Erweiterungen.
|
||||
*/
|
||||
public interface Module {
|
||||
|
||||
/**
|
||||
* Wird aufgerufen, wenn die API startet.
|
||||
*/
|
||||
void onEnable(Plugin plugin);
|
||||
|
||||
/**
|
||||
* Wird aufgerufen, wenn die API stoppt.
|
||||
*/
|
||||
void onDisable(Plugin plugin);
|
||||
|
||||
/**
|
||||
* Eindeutiger Name des Moduls (z.B. "StatsModule").
|
||||
*/
|
||||
String getName();
|
||||
}
|
||||
60
src/main/java/net/viper/status/module/ModuleManager.java
Normal file
60
src/main/java/net/viper/status/module/ModuleManager.java
Normal file
@@ -0,0 +1,60 @@
|
||||
package net.viper.status.module;
|
||||
|
||||
import net.md_5.bungee.api.plugin.Plugin;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Verwaltet alle geladenen Module.
|
||||
*/
|
||||
public class ModuleManager {
|
||||
|
||||
private final Map<String, Module> modules = new HashMap<>();
|
||||
|
||||
public void registerModule(Module module) {
|
||||
modules.put(module.getName().toLowerCase(), module);
|
||||
}
|
||||
|
||||
public void enableAll(Plugin plugin) {
|
||||
for (Module module : modules.values()) {
|
||||
try {
|
||||
plugin.getLogger().info("Aktiviere Modul: " + module.getName() + "...");
|
||||
module.onEnable(plugin);
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().severe("Fehler beim Aktivieren von Modul " + module.getName() + ": " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void disableAll(Plugin plugin) {
|
||||
for (Module module : modules.values()) {
|
||||
try {
|
||||
plugin.getLogger().info("Deaktiviere Modul: " + module.getName() + "...");
|
||||
module.onDisable(plugin);
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("Fehler beim Deaktivieren von Modul " + module.getName());
|
||||
}
|
||||
}
|
||||
modules.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ermöglicht anderen Komponenten (wie dem WebServer) Zugriff auf spezifische Module.
|
||||
*/
|
||||
public Module getModule(String name) {
|
||||
return modules.get(name.toLowerCase());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends Module> T getModule(Class<T> clazz) {
|
||||
for (Module m : modules.values()) {
|
||||
if (clazz.isInstance(m)) {
|
||||
return (T) m;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
package net.viper.status.modules.broadcast;
|
||||
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import net.md_5.bungee.api.plugin.Plugin;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import net.md_5.bungee.api.plugin.Listener;
|
||||
import net.viper.status.module.Module;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* BroadcastModule
|
||||
*
|
||||
* Speichert geplante Broadcasts jetzt persistent in 'broadcasts.schedules'.
|
||||
* Beim Neustart werden diese automatisch wieder geladen.
|
||||
*/
|
||||
public class BroadcastModule implements Module, Listener {
|
||||
|
||||
private Plugin plugin;
|
||||
private boolean enabled = true;
|
||||
private String requiredApiKey = "";
|
||||
private String format = "%prefix% %message%";
|
||||
private String fallbackPrefix = "[Broadcast]";
|
||||
private String fallbackPrefixColor = "&c";
|
||||
private String fallbackBracketColor = "&8"; // Neu
|
||||
private String fallbackMessageColor = "&f";
|
||||
|
||||
private final Map<String, ScheduledBroadcast> scheduledByClientId = new ConcurrentHashMap<>();
|
||||
private File schedulesFile;
|
||||
private final SimpleDateFormat dateFormat;
|
||||
|
||||
public BroadcastModule() {
|
||||
dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
|
||||
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "BroadcastModule";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable(Plugin plugin) {
|
||||
this.plugin = plugin;
|
||||
schedulesFile = new File(plugin.getDataFolder(), "broadcasts.schedules");
|
||||
loadConfig();
|
||||
|
||||
if (!enabled) {
|
||||
plugin.getLogger().info("[BroadcastModule] deaktiviert via verify.properties (broadcast.enabled=false)");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
plugin.getProxy().getPluginManager().registerListener(plugin, this);
|
||||
} catch (Throwable ignored) {}
|
||||
|
||||
plugin.getLogger().info("[BroadcastModule] aktiviert. Format: " + format);
|
||||
loadSchedules();
|
||||
plugin.getProxy().getScheduler().schedule(plugin, this::processScheduled, 1, 1, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable(Plugin plugin) {
|
||||
plugin.getLogger().info("[BroadcastModule] deaktiviert.");
|
||||
saveSchedules();
|
||||
scheduledByClientId.clear();
|
||||
}
|
||||
|
||||
private void loadConfig() {
|
||||
File file = new File(plugin.getDataFolder(), "verify.properties");
|
||||
if (!file.exists()) {
|
||||
enabled = true;
|
||||
requiredApiKey = "";
|
||||
format = "%prefix% %message%";
|
||||
fallbackPrefix = "[Broadcast]";
|
||||
fallbackPrefixColor = "&c";
|
||||
fallbackBracketColor = "&8"; // Neu
|
||||
fallbackMessageColor = "&f";
|
||||
return;
|
||||
}
|
||||
|
||||
try (InputStream in = new FileInputStream(file)) {
|
||||
Properties props = new Properties();
|
||||
props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
|
||||
enabled = Boolean.parseBoolean(props.getProperty("broadcast.enabled", "true"));
|
||||
requiredApiKey = props.getProperty("broadcast.api_key", "").trim();
|
||||
format = props.getProperty("broadcast.format", format).trim();
|
||||
if (format.isEmpty()) format = "%prefix% %message%";
|
||||
fallbackPrefix = props.getProperty("broadcast.prefix", fallbackPrefix).trim();
|
||||
fallbackPrefixColor = props.getProperty("broadcast.prefix-color", fallbackPrefixColor).trim();
|
||||
fallbackBracketColor = props.getProperty("broadcast.bracket-color", fallbackBracketColor).trim(); // Neu
|
||||
fallbackMessageColor = props.getProperty("broadcast.message-color", fallbackMessageColor).trim();
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().warning("[BroadcastModule] Fehler beim Laden von verify.properties: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean handleBroadcast(String sourceName, String message, String type, String apiKeyHeader,
|
||||
String prefix, String prefixColor, String bracketColor, String messageColor) {
|
||||
loadConfig();
|
||||
|
||||
if (!enabled) {
|
||||
plugin.getLogger().info("[BroadcastModule] Broadcast abgelehnt: Modul ist deaktiviert.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (requiredApiKey != null && !requiredApiKey.isEmpty()) {
|
||||
if (apiKeyHeader == null || !requiredApiKey.equals(apiKeyHeader)) {
|
||||
plugin.getLogger().warning("[BroadcastModule] Broadcast abgelehnt: API-Key fehlt oder inkorrekt.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (message == null) message = "";
|
||||
if (sourceName == null || sourceName.isEmpty()) sourceName = "System";
|
||||
if (type == null) type = "global";
|
||||
|
||||
String usedPrefix = (prefix != null && !prefix.trim().isEmpty()) ? prefix.trim() : fallbackPrefix;
|
||||
String usedPrefixColor = (prefixColor != null && !prefixColor.trim().isEmpty()) ? prefixColor.trim() : fallbackPrefixColor;
|
||||
String usedBracketColor = (bracketColor != null && !bracketColor.trim().isEmpty()) ? bracketColor.trim() : fallbackBracketColor; // Neu
|
||||
String usedMessageColor = (messageColor != null && !messageColor.trim().isEmpty()) ? messageColor.trim() : fallbackMessageColor;
|
||||
|
||||
String prefixColorCode = normalizeColorCode(usedPrefixColor);
|
||||
String bracketColorCode = normalizeColorCode(usedBracketColor); // Neu
|
||||
String messageColorCode = normalizeColorCode(usedMessageColor);
|
||||
|
||||
// --- KLAMMER LOGIK ---
|
||||
String finalPrefix;
|
||||
|
||||
// Wenn eine Klammerfarbe gesetzt ist, bauen wir den Prefix neu zusammen
|
||||
// Format: [BracketColor][ [PrefixColor]Text [BracketColor]]
|
||||
if (!bracketColorCode.isEmpty()) {
|
||||
String textContent = usedPrefix;
|
||||
// Entferne manuelle Klammern, falls der User [Broadcast] in das Textfeld geschrieben hat
|
||||
if (textContent.startsWith("[")) textContent = textContent.substring(1);
|
||||
if (textContent.endsWith("]")) textContent = textContent.substring(0, textContent.length() - 1);
|
||||
|
||||
finalPrefix = bracketColorCode + "[" + prefixColorCode + textContent + bracketColorCode + "]" + ChatColor.RESET;
|
||||
} else {
|
||||
// Altes Verhalten: Ganzen String einfärben
|
||||
finalPrefix = prefixColorCode + usedPrefix + ChatColor.RESET;
|
||||
}
|
||||
// ---------------------
|
||||
|
||||
String coloredMessage = (messageColorCode.isEmpty() ? "" : messageColorCode) + message;
|
||||
|
||||
String out = format.replace("%name%", sourceName)
|
||||
.replace("%prefix%", finalPrefix) // Neu verwendete Variable
|
||||
.replace("%prefixColored%", finalPrefix) // Fallback
|
||||
.replace("%message%", message)
|
||||
.replace("%messageColored%", coloredMessage)
|
||||
.replace("%type%", type);
|
||||
|
||||
if (!out.contains("%prefixColored%") && !out.contains("%messageColored%") && !out.contains("%prefix%") && !out.contains("%message%")) {
|
||||
out = finalPrefix + " " + coloredMessage;
|
||||
}
|
||||
|
||||
TextComponent tc = new TextComponent(out);
|
||||
int sent = 0;
|
||||
for (ProxiedPlayer p : plugin.getProxy().getPlayers()) {
|
||||
try { p.sendMessage(tc); sent++; } catch (Throwable ignored) {}
|
||||
}
|
||||
|
||||
plugin.getLogger().info("[BroadcastModule] Broadcast gesendet (Empfänger=" + sent + "): " + message);
|
||||
return true;
|
||||
}
|
||||
|
||||
private String normalizeColorCode(String code) {
|
||||
if (code == null) return "";
|
||||
code = code.trim();
|
||||
if (code.isEmpty()) return "";
|
||||
return code.contains("&") ? ChatColor.translateAlternateColorCodes('&', code) : code;
|
||||
}
|
||||
|
||||
private void saveSchedules() {
|
||||
Properties props = new Properties();
|
||||
for (Map.Entry<String, ScheduledBroadcast> entry : scheduledByClientId.entrySet()) {
|
||||
String id = entry.getKey();
|
||||
ScheduledBroadcast sb = entry.getValue();
|
||||
props.setProperty(id + ".nextRunMillis", String.valueOf(sb.nextRunMillis));
|
||||
props.setProperty(id + ".sourceName", sb.sourceName);
|
||||
props.setProperty(id + ".message", sb.message);
|
||||
props.setProperty(id + ".type", sb.type);
|
||||
props.setProperty(id + ".prefix", sb.prefix);
|
||||
props.setProperty(id + ".prefixColor", sb.prefixColor);
|
||||
props.setProperty(id + ".bracketColor", sb.bracketColor); // Neu
|
||||
props.setProperty(id + ".messageColor", sb.messageColor);
|
||||
props.setProperty(id + ".recur", sb.recur);
|
||||
}
|
||||
|
||||
try (OutputStream out = new FileOutputStream(schedulesFile)) {
|
||||
props.store(out, "PulseCast Scheduled Broadcasts");
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().severe("[BroadcastModule] Konnte Schedules nicht speichern: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void loadSchedules() {
|
||||
if (!schedulesFile.exists()) {
|
||||
plugin.getLogger().info("[BroadcastModule] Keine bestehenden Schedules gefunden (Neustart).");
|
||||
return;
|
||||
}
|
||||
|
||||
Properties props = new Properties();
|
||||
try (InputStream in = new FileInputStream(schedulesFile)) {
|
||||
props.load(in);
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().severe("[BroadcastModule] Konnte Schedules nicht laden: " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, ScheduledBroadcast> loaded = new HashMap<>();
|
||||
for (String key : props.stringPropertyNames()) {
|
||||
if (!key.contains(".")) continue;
|
||||
String[] parts = key.split("\\.");
|
||||
if (parts.length != 2) continue;
|
||||
|
||||
String id = parts[0];
|
||||
String field = parts[1];
|
||||
String value = props.getProperty(key);
|
||||
|
||||
ScheduledBroadcast sb = loaded.get(id);
|
||||
if (sb == null) {
|
||||
sb = new ScheduledBroadcast(id, 0, "", "", "", "", "", "", "", ""); // Ein leerer String mehr für Bracket
|
||||
loaded.put(id, sb);
|
||||
}
|
||||
|
||||
switch (field) {
|
||||
case "nextRunMillis":
|
||||
try { sb.nextRunMillis = Long.parseLong(value); } catch (NumberFormatException ignored) {}
|
||||
break;
|
||||
case "sourceName": sb.sourceName = value; break;
|
||||
case "message": sb.message = value; break;
|
||||
case "type": sb.type = value; break;
|
||||
case "prefix": sb.prefix = value; break;
|
||||
case "prefixColor": sb.prefixColor = value; break;
|
||||
case "bracketColor": sb.bracketColor = value; break; // Neu
|
||||
case "messageColor": sb.messageColor = value; break;
|
||||
case "recur": sb.recur = value; break;
|
||||
}
|
||||
}
|
||||
scheduledByClientId.putAll(loaded);
|
||||
plugin.getLogger().info("[BroadcastModule] " + loaded.size() + " geplante Broadcasts aus Datei wiederhergestellt.");
|
||||
}
|
||||
|
||||
public boolean scheduleBroadcast(long timestampMillis, String sourceName, String message, String type,
|
||||
String apiKeyHeader, String prefix, String prefixColor, String bracketColor, String messageColor,
|
||||
String recur, String clientScheduleId) {
|
||||
loadConfig();
|
||||
|
||||
if (!enabled) {
|
||||
plugin.getLogger().info("[BroadcastModule] schedule abgelehnt: Modul deaktiviert.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (requiredApiKey != null && !requiredApiKey.isEmpty()) {
|
||||
if (apiKeyHeader == null || !requiredApiKey.equals(apiKeyHeader)) {
|
||||
plugin.getLogger().warning("[BroadcastModule] schedule abgelehnt: API-Key fehlt oder inkorrekt.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (message == null) message = "";
|
||||
if (sourceName == null || sourceName.isEmpty()) sourceName = "System";
|
||||
if (type == null) type = "global";
|
||||
if (recur == null) recur = "none";
|
||||
|
||||
String id = (clientScheduleId != null && !clientScheduleId.trim().isEmpty()) ? clientScheduleId.trim() : UUID.randomUUID().toString();
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
String scheduledTimeStr = dateFormat.format(new Date(timestampMillis));
|
||||
|
||||
plugin.getLogger().info("[BroadcastModule] Neue geplante Nachricht registriert: " + id + " @ " + scheduledTimeStr);
|
||||
|
||||
if (timestampMillis <= now) {
|
||||
plugin.getLogger().warning("[BroadcastModule] Geplante Zeit liegt in der Vergangenheit -> sende sofort!");
|
||||
return handleBroadcast(sourceName, message, type, apiKeyHeader, prefix, prefixColor, bracketColor, messageColor);
|
||||
}
|
||||
|
||||
ScheduledBroadcast sb = new ScheduledBroadcast(id, timestampMillis, sourceName, message, type, prefix, prefixColor, bracketColor, messageColor, recur);
|
||||
scheduledByClientId.put(id, sb);
|
||||
saveSchedules();
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean cancelScheduled(String clientScheduleId) {
|
||||
if (clientScheduleId == null || clientScheduleId.trim().isEmpty()) return false;
|
||||
ScheduledBroadcast removed = scheduledByClientId.remove(clientScheduleId);
|
||||
if (removed != null) {
|
||||
plugin.getLogger().info("[BroadcastModule] Geplante Nachricht abgebrochen: id=" + clientScheduleId);
|
||||
saveSchedules();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void processScheduled() {
|
||||
if (scheduledByClientId.isEmpty()) return;
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
List<String> toRemove = new ArrayList<>();
|
||||
boolean changed = false;
|
||||
|
||||
for (Map.Entry<String, ScheduledBroadcast> entry : scheduledByClientId.entrySet()) {
|
||||
ScheduledBroadcast sb = entry.getValue();
|
||||
|
||||
if (sb.nextRunMillis <= now) {
|
||||
String timeStr = dateFormat.format(new Date(sb.nextRunMillis));
|
||||
plugin.getLogger().info("[BroadcastModule] ⏰ Sende geplante Nachricht (ID: " + entry.getKey() + ", Zeit: " + timeStr + ")");
|
||||
|
||||
handleBroadcast(sb.sourceName, sb.message, sb.type, "", sb.prefix, sb.prefixColor, sb.bracketColor, sb.messageColor);
|
||||
|
||||
if (!"none".equalsIgnoreCase(sb.recur)) {
|
||||
long next = computeNextMillis(sb.nextRunMillis, sb.recur);
|
||||
if (next > 0) {
|
||||
sb.nextRunMillis = next;
|
||||
String nextTimeStr = dateFormat.format(new Date(next));
|
||||
plugin.getLogger().info("[BroadcastModule] Nächste Wiederholung (" + sb.recur + "): " + nextTimeStr);
|
||||
changed = true;
|
||||
} else {
|
||||
toRemove.add(entry.getKey());
|
||||
changed = true;
|
||||
}
|
||||
} else {
|
||||
toRemove.add(entry.getKey());
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (changed || !toRemove.isEmpty()) {
|
||||
for (String k : toRemove) {
|
||||
scheduledByClientId.remove(k);
|
||||
plugin.getLogger().info("[BroadcastModule] Schedule entfernt: " + k);
|
||||
}
|
||||
saveSchedules();
|
||||
}
|
||||
}
|
||||
|
||||
private long computeNextMillis(long currentMillis, String recur) {
|
||||
switch (recur.toLowerCase(Locale.ROOT)) {
|
||||
case "hourly": return currentMillis + TimeUnit.HOURS.toMillis(1);
|
||||
case "daily": return currentMillis + TimeUnit.DAYS.toMillis(1);
|
||||
case "weekly": return currentMillis + TimeUnit.DAYS.toMillis(7);
|
||||
default: return -1L;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ScheduledBroadcast {
|
||||
final String clientId;
|
||||
long nextRunMillis;
|
||||
String sourceName;
|
||||
String message;
|
||||
String type;
|
||||
String prefix;
|
||||
String prefixColor;
|
||||
String bracketColor; // Neu
|
||||
String messageColor;
|
||||
String recur;
|
||||
|
||||
ScheduledBroadcast(String clientId, long nextRunMillis, String sourceName, String message, String type,
|
||||
String prefix, String prefixColor, String bracketColor, String messageColor, String recur) {
|
||||
this.clientId = clientId;
|
||||
this.nextRunMillis = nextRunMillis;
|
||||
this.sourceName = sourceName;
|
||||
this.message = message;
|
||||
this.type = type;
|
||||
this.prefix = prefix;
|
||||
this.prefixColor = prefixColor;
|
||||
this.bracketColor = bracketColor; // Neu
|
||||
this.messageColor = messageColor;
|
||||
this.recur = (recur == null ? "none" : recur);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package net.viper.status.modules.customcommands;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
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();
|
||||
if (this.config == null) {
|
||||
this.config = new Configuration();
|
||||
this.plugin.getLogger().warning("customcommands.yml konnte nicht geladen werden. Verwende leere Konfiguration.");
|
||||
}
|
||||
|
||||
// /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;
|
||||
if (player.getServer() == null) {
|
||||
player.sendMessage(new TextComponent(ChatColor.RED + "Konnte deinen Server nicht ermitteln. Bitte versuche es erneut."));
|
||||
return;
|
||||
}
|
||||
String msg = String.join(" ", args);
|
||||
if (msg.trim().isEmpty()) {
|
||||
player.sendMessage(new TextComponent(ChatColor.RED + "Bitte gib eine Nachricht an."));
|
||||
return;
|
||||
}
|
||||
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
|
||||
InputStream in = this.plugin.getResourceAsStream("customcommands.yml");
|
||||
if (in == null) {
|
||||
this.plugin.getLogger().warning("customcommands.yml nicht in JAR gefunden. Erstelle leere Datei.");
|
||||
file.createNewFile();
|
||||
} else {
|
||||
Files.copy(in, file.toPath(), new CopyOption[0]);
|
||||
in.close();
|
||||
}
|
||||
}
|
||||
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
|
||||
.replace("%args%", args)
|
||||
.replace("%sender%", playerName);
|
||||
|
||||
String commandToDispatch = parsed.startsWith("/") ? parsed.substring(1) : parsed;
|
||||
ProxyServer.getInstance().getPluginManager().dispatchCommand(sender, commandToDispatch);
|
||||
}
|
||||
}
|
||||
@@ -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 = new java.util.ArrayList<>(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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,519 @@
|
||||
package net.viper.status.modules.forum;
|
||||
|
||||
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.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.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.event.PostLoginEvent;
|
||||
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 net.md_5.bungee.event.EventHandler;
|
||||
import net.viper.status.module.Module;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* ForumBridgeModule: Verbindet das WP Business Forum mit dem Minecraft-Server.
|
||||
*
|
||||
* HTTP-Endpoints (werden vom StatusAPI WebServer geroutet):
|
||||
* POST /forum/notify — WordPress pusht Benachrichtigung
|
||||
* POST /forum/unlink — WordPress informiert über Verknüpfungslösung
|
||||
* GET /forum/status — Verbindungstest
|
||||
*
|
||||
* Commands:
|
||||
* /forumlink <token> — Account mit Forum verknüpfen
|
||||
* /forum — Ausstehende Benachrichtigungen anzeigen
|
||||
*/
|
||||
public class ForumBridgeModule implements Module, Listener {
|
||||
|
||||
private Plugin plugin;
|
||||
private ForumNotifStorage storage;
|
||||
|
||||
// Konfiguration aus verify.properties
|
||||
private boolean enabled = true;
|
||||
private String wpBaseUrl = "";
|
||||
private String apiSecret = "";
|
||||
private int loginDelaySeconds = 3;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "ForumBridgeModule";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable(Plugin plugin) {
|
||||
this.plugin = plugin;
|
||||
|
||||
// Config laden
|
||||
loadConfig(plugin);
|
||||
|
||||
if (!enabled) {
|
||||
plugin.getLogger().info("ForumBridgeModule ist deaktiviert.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Storage initialisieren und laden
|
||||
storage = new ForumNotifStorage(plugin.getDataFolder(), plugin.getLogger());
|
||||
storage.load();
|
||||
|
||||
// Event Listener registrieren
|
||||
plugin.getProxy().getPluginManager().registerListener(plugin, this);
|
||||
|
||||
// Commands registrieren
|
||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new ForumLinkCommand());
|
||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new ForumCommand());
|
||||
|
||||
// Auto-Save alle 10 Minuten
|
||||
plugin.getProxy().getScheduler().schedule(plugin, () -> {
|
||||
try {
|
||||
storage.save();
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("ForumBridge Auto-Save Fehler: " + e.getMessage());
|
||||
}
|
||||
}, 10, 10, TimeUnit.MINUTES);
|
||||
|
||||
// Alte Benachrichtigungen aufräumen (täglich, max 30 Tage)
|
||||
plugin.getProxy().getScheduler().schedule(plugin, () -> {
|
||||
storage.purgeOld(30);
|
||||
}, 1, 24, TimeUnit.HOURS);
|
||||
|
||||
plugin.getLogger().info("ForumBridgeModule aktiviert.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable(Plugin plugin) {
|
||||
if (storage != null) {
|
||||
storage.save();
|
||||
plugin.getLogger().info("Forum-Benachrichtigungen gespeichert.");
|
||||
}
|
||||
}
|
||||
|
||||
// ===== CONFIG =====
|
||||
|
||||
private void loadConfig(Plugin plugin) {
|
||||
try {
|
||||
Properties props = new Properties();
|
||||
File configFile = new File(plugin.getDataFolder(), "verify.properties");
|
||||
if (configFile.exists()) {
|
||||
try (FileInputStream fis = new FileInputStream(configFile)) {
|
||||
props.load(fis);
|
||||
}
|
||||
}
|
||||
this.enabled = !"false".equalsIgnoreCase(props.getProperty("forum.enabled", "true"));
|
||||
this.wpBaseUrl = props.getProperty("forum.wp_url", props.getProperty("wp_verify_url", ""));
|
||||
this.apiSecret = props.getProperty("forum.api_secret", "");
|
||||
this.loginDelaySeconds = parseInt(props.getProperty("forum.login_delay_seconds", "3"), 3);
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("Fehler beim Laden der ForumBridge-Config: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private int parseInt(String s, int def) {
|
||||
try { return Integer.parseInt(s); } catch (Exception e) { return def; }
|
||||
}
|
||||
|
||||
// ===== HTTP HANDLER (aufgerufen vom StatusAPI WebServer) =====
|
||||
|
||||
/**
|
||||
* Verarbeitet POST /forum/notify von WordPress.
|
||||
* WordPress sendet Benachrichtigungen wenn ein verknüpfter Spieler
|
||||
* eine neue Antwort, Erwähnung oder PN erhält.
|
||||
*
|
||||
* Erwarteter JSON-Body:
|
||||
* {
|
||||
* "player_uuid": "uuid-string",
|
||||
* "type": "reply|mention|message",
|
||||
* "title": "Thread-Titel oder PN",
|
||||
* "author": "Absender-Name",
|
||||
* "url": "Forum-Link",
|
||||
* "wp_user_id": 123
|
||||
* }
|
||||
*/
|
||||
public String handleNotify(String body, String apiKeyHeader) {
|
||||
// API-Key prüfen
|
||||
if (!apiSecret.isEmpty() && !apiSecret.equals(apiKeyHeader)) {
|
||||
return "{\"success\":false,\"error\":\"unauthorized\"}";
|
||||
}
|
||||
|
||||
String playerUuid = extractJsonString(body, "player_uuid");
|
||||
String type = extractJsonString(body, "type");
|
||||
String title = extractJsonString(body, "title");
|
||||
String author = extractJsonString(body, "author");
|
||||
String url = extractJsonString(body, "url");
|
||||
|
||||
if (playerUuid == null || playerUuid.isEmpty()) {
|
||||
return "{\"success\":false,\"error\":\"missing_player_uuid\"}";
|
||||
}
|
||||
|
||||
java.util.UUID uuid;
|
||||
try {
|
||||
uuid = java.util.UUID.fromString(playerUuid);
|
||||
} catch (Exception e) {
|
||||
return "{\"success\":false,\"error\":\"invalid_uuid\"}";
|
||||
}
|
||||
|
||||
// Fallback: Wenn type 'thread' und title enthält 'Umfrage', dann als 'poll' behandeln
|
||||
if (type != null && type.equalsIgnoreCase("thread") && title != null && title.toLowerCase().contains("umfrage")) {
|
||||
type = "poll";
|
||||
}
|
||||
if (type == null || type.isEmpty()) type = "reply";
|
||||
|
||||
// Notification erstellen
|
||||
ForumNotification notification = new ForumNotification(uuid, type, title, author, url);
|
||||
|
||||
// Sofort zustellen wenn online
|
||||
ProxiedPlayer online = ProxyServer.getInstance().getPlayer(uuid);
|
||||
if (online != null && online.isConnected()) {
|
||||
deliverNotification(online, notification);
|
||||
notification.setDelivered(true);
|
||||
return "{\"success\":true,\"delivered\":true}";
|
||||
}
|
||||
|
||||
// Offline → speichern für späteren Login
|
||||
storage.add(notification);
|
||||
return "{\"success\":true,\"delivered\":false}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet POST /forum/unlink von WordPress.
|
||||
* Wird aufgerufen wenn ein User seine Verknüpfung im Forum löst.
|
||||
*/
|
||||
public String handleUnlink(String body, String apiKeyHeader) {
|
||||
if (!apiSecret.isEmpty() && !apiSecret.equals(apiKeyHeader)) {
|
||||
return "{\"success\":false,\"error\":\"unauthorized\"}";
|
||||
}
|
||||
// Aktuell keine lokale Aktion nötig — die Zuordnung liegt in WordPress
|
||||
return "{\"success\":true}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet GET /forum/status — Verbindungstest.
|
||||
*/
|
||||
public String handleStatus() {
|
||||
String version = "unknown";
|
||||
try {
|
||||
if (plugin.getDescription() != null) {
|
||||
version = plugin.getDescription().getVersion();
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
return "{\"success\":true,\"module\":\"ForumBridgeModule\",\"version\":\"" + version + "\"}";
|
||||
}
|
||||
|
||||
// ===== NOTIFICATION ZUSTELLUNG =====
|
||||
|
||||
/**
|
||||
* Stellt eine einzelne Benachrichtigung an einen Online-Spieler zu.
|
||||
*/
|
||||
private void deliverNotification(ProxiedPlayer player, ForumNotification notif) {
|
||||
String color = notif.getTypeColor();
|
||||
String label = notif.getTypeLabel();
|
||||
|
||||
// Trennlinie
|
||||
player.sendMessage(new TextComponent("§8§m "));
|
||||
|
||||
// Hauptnachricht
|
||||
TextComponent header = new TextComponent("§6§l✉ Forum §8» " + color + label);
|
||||
player.sendMessage(header);
|
||||
|
||||
// Details
|
||||
if (!notif.getTitle().isEmpty()) {
|
||||
player.sendMessage(new TextComponent("§7 " + notif.getTitle()));
|
||||
}
|
||||
if (!notif.getAuthor().isEmpty()) {
|
||||
player.sendMessage(new TextComponent("§7 von §f" + notif.getAuthor()));
|
||||
}
|
||||
|
||||
// Klickbarer Link (wenn URL vorhanden)
|
||||
if (!notif.getUrl().isEmpty()) {
|
||||
TextComponent link = new TextComponent("§a ➜ Im Forum ansehen");
|
||||
link.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, notif.getUrl()));
|
||||
link.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
|
||||
new ComponentBuilder("§7Klicke um den Beitrag im Forum zu öffnen").create()));
|
||||
player.sendMessage(link);
|
||||
}
|
||||
|
||||
// Trennlinie
|
||||
player.sendMessage(new TextComponent("§8§m "));
|
||||
}
|
||||
|
||||
/**
|
||||
* Stellt alle ausstehenden Benachrichtigungen an einen Spieler zu.
|
||||
* Wird beim Login aufgerufen (mit kurzem Delay).
|
||||
*/
|
||||
private void deliverPending(ProxiedPlayer player) {
|
||||
List<ForumNotification> pending = storage.getPending(player.getUniqueId());
|
||||
if (pending.isEmpty()) return;
|
||||
|
||||
int count = pending.size();
|
||||
|
||||
// Zusammenfassung wenn mehr als 3
|
||||
if (count > 3) {
|
||||
player.sendMessage(new TextComponent("§8§m "));
|
||||
player.sendMessage(new TextComponent("§6§l✉ Forum §8» §fDu hast §e" + count + " §fneue Benachrichtigungen!"));
|
||||
player.sendMessage(new TextComponent("§7 Tippe §e/forum §7um sie anzuzeigen."));
|
||||
player.sendMessage(new TextComponent("§8§m "));
|
||||
} else {
|
||||
// Einzeln zustellen
|
||||
for (ForumNotification n : pending) {
|
||||
deliverNotification(player, n);
|
||||
}
|
||||
}
|
||||
|
||||
// Alle als zugestellt markieren und aufräumen
|
||||
storage.markAllDelivered(player.getUniqueId());
|
||||
storage.clearDelivered(player.getUniqueId());
|
||||
}
|
||||
|
||||
// ===== EVENTS =====
|
||||
|
||||
@EventHandler
|
||||
public void onJoin(PostLoginEvent e) {
|
||||
ProxiedPlayer player = e.getPlayer();
|
||||
|
||||
// Verzögert zustellen damit der Spieler den Server-Wechsel abgeschlossen hat
|
||||
plugin.getProxy().getScheduler().schedule(plugin, () -> {
|
||||
if (player.isConnected()) {
|
||||
deliverPending(player);
|
||||
}
|
||||
}, loginDelaySeconds, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
// ===== COMMANDS =====
|
||||
|
||||
/**
|
||||
* /forumlink <token> — Verknüpft den MC-Account mit dem Forum.
|
||||
*/
|
||||
private class ForumLinkCommand extends Command {
|
||||
|
||||
public ForumLinkCommand() {
|
||||
super("forumlink", null, "fl");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandSender sender, String[] args) {
|
||||
if (!(sender instanceof ProxiedPlayer)) {
|
||||
sender.sendMessage(new TextComponent("§cNur Spieler können diesen Befehl benutzen."));
|
||||
return;
|
||||
}
|
||||
|
||||
ProxiedPlayer p = (ProxiedPlayer) sender;
|
||||
|
||||
if (args.length != 1) {
|
||||
p.sendMessage(new TextComponent("§eBenutzung: §f/forumlink <token>"));
|
||||
p.sendMessage(new TextComponent("§7Den Token erhältst du in deinem Forum-Profil unter §fMinecraft-Verknüpfung§7."));
|
||||
return;
|
||||
}
|
||||
|
||||
String token = args[0].trim().toUpperCase();
|
||||
|
||||
if (wpBaseUrl.isEmpty()) {
|
||||
p.sendMessage(new TextComponent("§cForum-Verknüpfung ist nicht konfiguriert."));
|
||||
return;
|
||||
}
|
||||
|
||||
p.sendMessage(new TextComponent("§7Überprüfe Token..."));
|
||||
|
||||
// Asynchron an WordPress senden
|
||||
plugin.getProxy().getScheduler().runAsync(plugin, () -> {
|
||||
try {
|
||||
String endpoint = wpBaseUrl + "/wp-json/mc-bridge/v1/verify-link";
|
||||
String payload = "{\"token\":\"" + escapeJson(token) + "\","
|
||||
+ "\"mc_uuid\":\"" + p.getUniqueId().toString() + "\","
|
||||
+ "\"mc_name\":\"" + escapeJson(p.getName()) + "\"}";
|
||||
|
||||
HttpURLConnection conn = (HttpURLConnection) new URL(endpoint).openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setDoOutput(true);
|
||||
conn.setConnectTimeout(5000);
|
||||
conn.setReadTimeout(7000);
|
||||
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
||||
if (!apiSecret.isEmpty()) {
|
||||
conn.setRequestProperty("X-Api-Key", apiSecret);
|
||||
}
|
||||
|
||||
Charset utf8 = Charset.forName("UTF-8");
|
||||
try (OutputStream os = conn.getOutputStream()) {
|
||||
os.write(payload.getBytes(utf8));
|
||||
}
|
||||
|
||||
int code = conn.getResponseCode();
|
||||
String resp;
|
||||
if (code >= 200 && code < 300) {
|
||||
resp = streamToString(conn.getInputStream(), utf8);
|
||||
} else {
|
||||
resp = streamToString(conn.getErrorStream(), utf8);
|
||||
}
|
||||
|
||||
// Antwort auswerten
|
||||
if (resp != null && resp.contains("\"success\":true")) {
|
||||
String displayName = extractJsonString(resp, "display_name");
|
||||
String username = extractJsonString(resp, "username");
|
||||
String show = (displayName != null && !displayName.isEmpty()) ? displayName : username;
|
||||
|
||||
p.sendMessage(new TextComponent("§8§m "));
|
||||
p.sendMessage(new TextComponent("§a§l✓ §fForum-Account erfolgreich verknüpft!"));
|
||||
if (show != null && !show.isEmpty()) {
|
||||
p.sendMessage(new TextComponent("§7 Forum-User: §f" + show));
|
||||
}
|
||||
p.sendMessage(new TextComponent("§7 Du erhältst jetzt Ingame-Benachrichtigungen."));
|
||||
p.sendMessage(new TextComponent("§8§m "));
|
||||
} else {
|
||||
// Fehlermeldung auslesen
|
||||
String error = extractJsonString(resp, "error");
|
||||
String message = extractJsonString(resp, "message");
|
||||
|
||||
if ("token_expired".equals(error)) {
|
||||
p.sendMessage(new TextComponent("§c✗ Der Token ist abgelaufen. Generiere einen neuen im Forum."));
|
||||
} else if ("uuid_already_linked".equals(error)) {
|
||||
p.sendMessage(new TextComponent("§c✗ " + (message != null ? message : "Diese UUID ist bereits verknüpft.")));
|
||||
} else if ("invalid_token".equals(error)) {
|
||||
p.sendMessage(new TextComponent("§c✗ Ungültiger Token. Prüfe die Eingabe oder generiere einen neuen."));
|
||||
} else {
|
||||
p.sendMessage(new TextComponent("§c✗ Verknüpfung fehlgeschlagen: " + (error != null ? error : "Unbekannter Fehler")));
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
p.sendMessage(new TextComponent("§c✗ Fehler bei der Verbindung zum Forum."));
|
||||
plugin.getLogger().warning("ForumLink Fehler: " + ex.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* /forum — Zeigt ausstehende Forum-Benachrichtigungen an.
|
||||
*/
|
||||
private class ForumCommand extends Command {
|
||||
|
||||
public ForumCommand() {
|
||||
super("forum");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandSender sender, String[] args) {
|
||||
if (!(sender instanceof ProxiedPlayer)) {
|
||||
sender.sendMessage(new TextComponent("§cNur Spieler können diesen Befehl benutzen."));
|
||||
return;
|
||||
}
|
||||
|
||||
ProxiedPlayer p = (ProxiedPlayer) sender;
|
||||
List<ForumNotification> pending = storage.getPending(p.getUniqueId());
|
||||
|
||||
if (pending.isEmpty()) {
|
||||
p.sendMessage(new TextComponent("§7Keine neuen Forum-Benachrichtigungen."));
|
||||
|
||||
// Forum-Link anzeigen wenn konfiguriert
|
||||
if (!wpBaseUrl.isEmpty()) {
|
||||
TextComponent link = new TextComponent("§a➜ Forum öffnen");
|
||||
link.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, wpBaseUrl));
|
||||
link.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
|
||||
new ComponentBuilder("§7Klicke um das Forum zu öffnen").create()));
|
||||
p.sendMessage(link);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
p.sendMessage(new TextComponent("§8§m "));
|
||||
p.sendMessage(new TextComponent("§6§l✉ Forum-Benachrichtigungen §8(§f" + pending.size() + "§8)"));
|
||||
p.sendMessage(new TextComponent(""));
|
||||
|
||||
int shown = 0;
|
||||
for (ForumNotification n : pending) {
|
||||
if (shown >= 10) {
|
||||
p.sendMessage(new TextComponent("§7 ... und " + (pending.size() - 10) + " weitere"));
|
||||
break;
|
||||
}
|
||||
|
||||
String color = n.getTypeColor();
|
||||
TextComponent line = new TextComponent(color + " • " + n.getTypeLabel() + "§7: ");
|
||||
|
||||
TextComponent detail;
|
||||
if (!n.getTitle().isEmpty()) {
|
||||
detail = new TextComponent("§f" + n.getTitle());
|
||||
} else {
|
||||
detail = new TextComponent("§fvon " + n.getAuthor());
|
||||
}
|
||||
|
||||
if (!n.getUrl().isEmpty()) {
|
||||
detail.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, n.getUrl()));
|
||||
detail.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
|
||||
new ComponentBuilder("§7Klicke zum Öffnen").create()));
|
||||
}
|
||||
|
||||
line.addExtra(detail);
|
||||
p.sendMessage(line);
|
||||
shown++;
|
||||
}
|
||||
|
||||
p.sendMessage(new TextComponent(""));
|
||||
p.sendMessage(new TextComponent("§8§m "));
|
||||
|
||||
// Alle als gelesen markieren
|
||||
storage.markAllDelivered(p.getUniqueId());
|
||||
storage.clearDelivered(p.getUniqueId());
|
||||
}
|
||||
}
|
||||
|
||||
// ===== HELPER =====
|
||||
|
||||
/** Getter für den Storage (für StatusAPI HTTP-Handler) */
|
||||
public ForumNotifStorage getStorage() {
|
||||
return storage;
|
||||
}
|
||||
|
||||
private static String extractJsonString(String json, String key) {
|
||||
if (json == null || key == null) return null;
|
||||
String search = "\"" + key + "\"";
|
||||
int idx = json.indexOf(search);
|
||||
if (idx < 0) return null;
|
||||
int colon = json.indexOf(':', idx + search.length());
|
||||
if (colon < 0) return null;
|
||||
int i = colon + 1;
|
||||
while (i < json.length() && Character.isWhitespace(json.charAt(i))) i++;
|
||||
if (i >= json.length()) return null;
|
||||
char c = json.charAt(i);
|
||||
if (c == '"') {
|
||||
i++;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
boolean escape = false;
|
||||
while (i < json.length()) {
|
||||
char ch = json.charAt(i++);
|
||||
if (escape) { sb.append(ch); escape = false; }
|
||||
else {
|
||||
if (ch == '\\') escape = true;
|
||||
else if (ch == '"') break;
|
||||
else sb.append(ch);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String escapeJson(String s) {
|
||||
if (s == null) return "";
|
||||
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r");
|
||||
}
|
||||
|
||||
private static String streamToString(InputStream in, Charset charset) throws IOException {
|
||||
if (in == null) return "";
|
||||
try (BufferedReader br = new BufferedReader(new InputStreamReader(in, charset))) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) sb.append(line);
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package net.viper.status.modules.forum;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Speichert ausstehende Forum-Benachrichtigungen (Datei-basiert).
|
||||
* Benachrichtigungen die nicht sofort zugestellt werden konnten (Spieler offline)
|
||||
* werden hier gespeichert und beim nächsten Login zugestellt.
|
||||
*/
|
||||
public class ForumNotifStorage {
|
||||
|
||||
private final File file;
|
||||
private final Logger logger;
|
||||
|
||||
// UUID -> Liste ausstehender Notifications
|
||||
private final ConcurrentHashMap<UUID, CopyOnWriteArrayList<ForumNotification>> pending = new ConcurrentHashMap<>();
|
||||
|
||||
public ForumNotifStorage(File pluginFolder, Logger logger) {
|
||||
if (!pluginFolder.exists()) pluginFolder.mkdirs();
|
||||
this.file = new File(pluginFolder, "forum_notifications.dat");
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fügt eine Benachrichtigung hinzu.
|
||||
*/
|
||||
public void add(ForumNotification notification) {
|
||||
pending.computeIfAbsent(notification.getPlayerUuid(), k -> new CopyOnWriteArrayList<>())
|
||||
.add(notification);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle ausstehenden (nicht zugestellten) Benachrichtigungen eines Spielers zurück.
|
||||
*/
|
||||
public List<ForumNotification> getPending(UUID playerUuid) {
|
||||
CopyOnWriteArrayList<ForumNotification> list = pending.get(playerUuid);
|
||||
if (list == null) return Collections.emptyList();
|
||||
List<ForumNotification> result = new ArrayList<>();
|
||||
for (ForumNotification n : list) {
|
||||
if (!n.isDelivered()) result.add(n);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Anzahl ausstehender Benachrichtigungen.
|
||||
*/
|
||||
public int getPendingCount(UUID playerUuid) {
|
||||
CopyOnWriteArrayList<ForumNotification> list = pending.get(playerUuid);
|
||||
if (list == null) return 0;
|
||||
int count = 0;
|
||||
for (ForumNotification n : list) {
|
||||
if (!n.isDelivered()) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Markiert alle Benachrichtigungen eines Spielers als zugestellt und entfernt sie.
|
||||
*/
|
||||
public void clearDelivered(UUID playerUuid) {
|
||||
CopyOnWriteArrayList<ForumNotification> list = pending.get(playerUuid);
|
||||
if (list == null) return;
|
||||
list.removeIf(ForumNotification::isDelivered);
|
||||
if (list.isEmpty()) pending.remove(playerUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Markiert alle als zugestellt.
|
||||
*/
|
||||
public void markAllDelivered(UUID playerUuid) {
|
||||
CopyOnWriteArrayList<ForumNotification> list = pending.get(playerUuid);
|
||||
if (list == null) return;
|
||||
for (ForumNotification n : list) {
|
||||
n.setDelivered(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt Benachrichtigungen die älter als maxDays Tage sind.
|
||||
*/
|
||||
public void purgeOld(int maxDays) {
|
||||
long cutoff = System.currentTimeMillis() - ((long) maxDays * 24 * 60 * 60 * 1000);
|
||||
for (Map.Entry<UUID, CopyOnWriteArrayList<ForumNotification>> entry : pending.entrySet()) {
|
||||
entry.getValue().removeIf(n -> n.getTimestamp() < cutoff);
|
||||
if (entry.getValue().isEmpty()) pending.remove(entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Datei-Operationen =====
|
||||
|
||||
public void save() {
|
||||
try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF-8"))) {
|
||||
for (CopyOnWriteArrayList<ForumNotification> list : pending.values()) {
|
||||
for (ForumNotification n : list) {
|
||||
if (!n.isDelivered()) {
|
||||
bw.write(n.toLine());
|
||||
bw.newLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
bw.flush();
|
||||
} catch (IOException e) {
|
||||
logger.warning("Fehler beim Speichern der Forum-Benachrichtigungen: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void load() {
|
||||
if (!file.exists()) return;
|
||||
try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"))) {
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
ForumNotification n = ForumNotification.fromLine(line);
|
||||
if (n != null && !n.isDelivered()) {
|
||||
pending.computeIfAbsent(n.getPlayerUuid(), k -> new CopyOnWriteArrayList<>())
|
||||
.add(n);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warning("Fehler beim Laden der Forum-Benachrichtigungen: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package net.viper.status.modules.forum;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Eine einzelne Forum-Benachrichtigung.
|
||||
*/
|
||||
public class ForumNotification {
|
||||
|
||||
private final UUID playerUuid;
|
||||
private final String type; // reply, mention, message
|
||||
private final String title;
|
||||
private final String author;
|
||||
private final String url;
|
||||
private final long timestamp;
|
||||
private boolean delivered;
|
||||
|
||||
public ForumNotification(UUID playerUuid, String type, String title, String author, String url) {
|
||||
this.playerUuid = playerUuid;
|
||||
this.type = type != null ? type : "reply";
|
||||
this.title = title != null ? title : "";
|
||||
this.author = author != null ? author : "Unbekannt";
|
||||
this.url = url != null ? url : "";
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
this.delivered = false;
|
||||
}
|
||||
|
||||
/** Interner Konstruktor für Deserialisierung */
|
||||
ForumNotification(UUID playerUuid, String type, String title, String author, String url, long timestamp, boolean delivered) {
|
||||
this.playerUuid = playerUuid;
|
||||
this.type = type;
|
||||
this.title = title;
|
||||
this.author = author;
|
||||
this.url = url;
|
||||
this.timestamp = timestamp;
|
||||
this.delivered = delivered;
|
||||
}
|
||||
|
||||
// --- Getter ---
|
||||
|
||||
public UUID getPlayerUuid() { return playerUuid; }
|
||||
public String getType() { return type; }
|
||||
public String getTitle() { return title; }
|
||||
public String getAuthor() { return author; }
|
||||
public String getUrl() { return url; }
|
||||
public long getTimestamp() { return timestamp; }
|
||||
public boolean isDelivered() { return delivered; }
|
||||
public void setDelivered(boolean d) { this.delivered = d; }
|
||||
|
||||
/**
|
||||
* Deutsches Label für den Benachrichtigungstyp.
|
||||
*/
|
||||
public String getTypeLabel() {
|
||||
switch (type) {
|
||||
case "reply": return "Neue Antwort";
|
||||
case "mention": return "Erwähnung";
|
||||
case "message": return "Neue PN";
|
||||
case "thread": return "Neuer Thread";
|
||||
case "poll": return "Neue Umfrage";
|
||||
case "answer": return "Antwort auf deinen Thread";
|
||||
default: return "Benachrichtigung";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Farbcode (Minecraft) je nach Typ.
|
||||
*/
|
||||
public String getTypeColor() {
|
||||
switch (type) {
|
||||
case "reply": return "§b"; // Aqua
|
||||
case "mention": return "§e"; // Gelb
|
||||
case "message": return "§d"; // Rosa
|
||||
case "thread": return "§a"; // Grün
|
||||
case "poll": return "§3"; // Dunkel-Aqua
|
||||
case "answer": return "§2"; // Dunkel-Grün
|
||||
default: return "§f"; // Weiß
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialisierung für Datei-Speicherung.
|
||||
* Format: uuid|type|title|author|url|timestamp|delivered
|
||||
*/
|
||||
public String toLine() {
|
||||
return playerUuid.toString() + "|"
|
||||
+ type + "|"
|
||||
+ title.replace("|", "_") + "|"
|
||||
+ author.replace("|", "_") + "|"
|
||||
+ url.replace("|", "_") + "|"
|
||||
+ timestamp + "|"
|
||||
+ (delivered ? "1" : "0");
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialisierung aus einer Zeile.
|
||||
*/
|
||||
public static ForumNotification fromLine(String line) {
|
||||
if (line == null || line.trim().isEmpty()) return null;
|
||||
String[] parts = line.split("\\|", -1);
|
||||
if (parts.length < 7) return null;
|
||||
try {
|
||||
UUID uuid = UUID.fromString(parts[0]);
|
||||
String type = parts[1];
|
||||
String title = parts[2];
|
||||
String author = parts[3];
|
||||
String url = parts[4];
|
||||
long timestamp = Long.parseLong(parts[5]);
|
||||
boolean delivered = "1".equals(parts[6]);
|
||||
return new ForumNotification(uuid, type, title, author, url, timestamp, delivered);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
248
src/main/java/net/viper/status/modules/verify/VerifyModule.java
Normal file
248
src/main/java/net/viper/status/modules/verify/VerifyModule.java
Normal file
@@ -0,0 +1,248 @@
|
||||
package net.viper.status.modules.verify;
|
||||
|
||||
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.plugin.Command;
|
||||
import net.md_5.bungee.api.plugin.Plugin;
|
||||
import net.viper.status.module.Module;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.*;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* VerifyModule: Multi-Server Support.
|
||||
* Liest pro Server die passende ID und das Secret aus der verify.properties.
|
||||
*/
|
||||
public class VerifyModule implements Module {
|
||||
|
||||
private String wpVerifyUrl;
|
||||
// Speichert für jeden Servernamen (z.B. "Lobby") die passende Konfiguration
|
||||
private final Map<String, ServerConfig> serverConfigs = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "VerifyModule";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable(Plugin plugin) {
|
||||
loadConfig(plugin);
|
||||
|
||||
// Befehl registrieren
|
||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new VerifyCommand());
|
||||
|
||||
plugin.getLogger().info("VerifyModule aktiviert. " + serverConfigs.size() + " Server-Konfigurationen geladen.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable(Plugin plugin) {
|
||||
// Befehl muss nicht manuell entfernt werden, BungeeCord übernimmt das beim Plugin-Stop
|
||||
}
|
||||
|
||||
// --- Konfiguration Laden & Kopieren ---
|
||||
private void loadConfig(Plugin plugin) {
|
||||
String fileName = "verify.properties";
|
||||
File configFile = new File(plugin.getDataFolder(), fileName);
|
||||
Properties props = new Properties();
|
||||
|
||||
// 1. Datei kopieren, falls sie noch nicht existiert
|
||||
if (!configFile.exists()) {
|
||||
plugin.getDataFolder().mkdirs();
|
||||
try (InputStream in = plugin.getResourceAsStream(fileName);
|
||||
OutputStream out = new FileOutputStream(configFile)) {
|
||||
if (in == null) {
|
||||
plugin.getLogger().warning("Standard-config '" + fileName + "' nicht in JAR gefunden. Erstelle manuell.");
|
||||
return;
|
||||
}
|
||||
byte[] buffer = new byte[1024];
|
||||
int length;
|
||||
while ((length = in.read(buffer)) > 0) {
|
||||
out.write(buffer, 0, length);
|
||||
}
|
||||
plugin.getLogger().info("Konfigurationsdatei '" + fileName + "' erstellt.");
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().severe("Fehler beim Erstellen der Config: " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Eigentliche Config laden
|
||||
try (InputStream in = new FileInputStream(configFile)) {
|
||||
props.load(in);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
|
||||
// Globale URL
|
||||
this.wpVerifyUrl = props.getProperty("wp_verify_url", "https://deine-wp-domain.tld");
|
||||
|
||||
// Server-Configs parsen (z.B. server.Lobby.id)
|
||||
this.serverConfigs.clear();
|
||||
for (String key : props.stringPropertyNames()) {
|
||||
if (key.startsWith("server.")) {
|
||||
// Key Struktur: server.<ServerName>.id oder .secret
|
||||
String[] parts = key.split("\\.");
|
||||
if (parts.length == 3) {
|
||||
String serverName = parts[1];
|
||||
String type = parts[2];
|
||||
|
||||
// Eintrag in der Map erstellen oder holen
|
||||
ServerConfig config = serverConfigs.computeIfAbsent(serverName, k -> new ServerConfig());
|
||||
|
||||
if ("id".equalsIgnoreCase(type)) {
|
||||
try {
|
||||
config.serverId = Integer.parseInt(props.getProperty(key));
|
||||
} catch (NumberFormatException e) {
|
||||
plugin.getLogger().warning("Ungültige Server ID für " + serverName);
|
||||
}
|
||||
} else if ("secret".equalsIgnoreCase(type)) {
|
||||
config.sharedSecret = props.getProperty(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Hilfsklasse für die Daten eines Servers ---
|
||||
private static class ServerConfig {
|
||||
int serverId = 0;
|
||||
String sharedSecret = "";
|
||||
}
|
||||
|
||||
// --- Die Command Klasse ---
|
||||
private class VerifyCommand extends Command {
|
||||
|
||||
public VerifyCommand() {
|
||||
super("verify");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandSender sender, String[] args) {
|
||||
if (!(sender instanceof ProxiedPlayer)) {
|
||||
sender.sendMessage(ChatColor.RED + "Nur Spieler können diesen Befehl benutzen.");
|
||||
return;
|
||||
}
|
||||
|
||||
ProxiedPlayer p = (ProxiedPlayer) sender;
|
||||
if (args.length != 1) {
|
||||
p.sendMessage(ChatColor.YELLOW + "Benutzung: /verify <token>");
|
||||
return;
|
||||
}
|
||||
|
||||
// --- WICHTIG: Servernamen ermitteln ---
|
||||
String serverName = p.getServer().getInfo().getName();
|
||||
|
||||
// Konfiguration für diesen Server laden
|
||||
ServerConfig config = serverConfigs.get(serverName);
|
||||
|
||||
// Check ob Konfig existiert
|
||||
if (config == null || config.serverId == 0 || config.sharedSecret.isEmpty()) {
|
||||
p.sendMessage(ChatColor.RED + "✗ Dieser Server ist nicht in der Verify-Konfiguration hinterlegt.");
|
||||
p.sendMessage(ChatColor.GRAY + "Aktueller Servername: " + ChatColor.WHITE + serverName);
|
||||
p.sendMessage(ChatColor.GRAY + "Bitte kontaktiere einen Admin.");
|
||||
return;
|
||||
}
|
||||
|
||||
String token = args[0].trim();
|
||||
String playerName = p.getName();
|
||||
|
||||
HttpURLConnection conn = null;
|
||||
try {
|
||||
Charset utf8 = Charset.forName("UTF-8");
|
||||
// Wir signieren Name + Token mit dem SERVER-SPECIFISCHEN Secret
|
||||
String signature = hmacSHA256(playerName + token, config.sharedSecret, utf8);
|
||||
|
||||
// Payload aufbauen mit der SERVER-SPECIFISCHEN ID
|
||||
String payload = "{\"player\":\"" + escapeJson(playerName) + "\",\"token\":\"" + escapeJson(token) + "\",\"server_id\":" + config.serverId + ",\"signature\":\"" + signature + "\"}";
|
||||
|
||||
URL url = new URL(wpVerifyUrl + "/wp-json/mc-gallery/v1/verify");
|
||||
conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setConnectTimeout(5000);
|
||||
conn.setReadTimeout(7000);
|
||||
conn.setDoOutput(true);
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setRequestProperty("Content-Type", "application/json; charset=utf-8");
|
||||
|
||||
try (OutputStream os = conn.getOutputStream()) {
|
||||
os.write(payload.getBytes(utf8));
|
||||
}
|
||||
|
||||
int code = conn.getResponseCode();
|
||||
String resp;
|
||||
|
||||
if (code >= 200 && code < 300) {
|
||||
resp = streamToString(conn.getInputStream(), utf8);
|
||||
} else {
|
||||
resp = streamToString(conn.getErrorStream(), utf8);
|
||||
}
|
||||
|
||||
// Antwort verarbeiten
|
||||
if (resp != null && !resp.isEmpty() && resp.trim().startsWith("{")) {
|
||||
boolean isSuccess = resp.contains("\"success\":true");
|
||||
String message = "Ein unbekannter Fehler ist aufgetreten.";
|
||||
|
||||
int keyIndex = resp.indexOf("\"message\":\"");
|
||||
if (keyIndex != -1) {
|
||||
int startIndex = keyIndex + 11;
|
||||
int endIndex = resp.indexOf("\"", startIndex);
|
||||
if (endIndex != -1) {
|
||||
message = resp.substring(startIndex, endIndex);
|
||||
}
|
||||
}
|
||||
|
||||
if (isSuccess) {
|
||||
p.sendMessage(ChatColor.GREEN + "✓ " + message);
|
||||
p.sendMessage(ChatColor.GRAY + "Du kannst nun Bilder hochladen!");
|
||||
} else {
|
||||
p.sendMessage(ChatColor.RED + "✗ " + message);
|
||||
}
|
||||
} else {
|
||||
p.sendMessage(ChatColor.RED + "✗ Fehler beim Verbinden mit der Webseite (Code: " + code + ")");
|
||||
}
|
||||
|
||||
} catch (Exception ex) {
|
||||
p.sendMessage(ChatColor.RED + "✗ Ein interner Fehler ist aufgetreten.");
|
||||
ProxyServer.getInstance().getLogger().warning("Verify error: " + ex.getMessage());
|
||||
ex.printStackTrace();
|
||||
} finally {
|
||||
if (conn != null) {
|
||||
conn.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Helper Methoden ---
|
||||
private static String hmacSHA256(String data, String key, Charset charset) throws Exception {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(key.getBytes(charset), "HmacSHA256"));
|
||||
byte[] raw = mac.doFinal(data.getBytes(charset));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : raw) sb.append(String.format("%02x", b));
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String streamToString(InputStream in, Charset charset) throws IOException {
|
||||
if (in == null) return "";
|
||||
try (BufferedReader br = new BufferedReader(new InputStreamReader(in, charset))) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) sb.append(line);
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private static String escapeJson(String s) {
|
||||
return s.replace("\\", "\\\\").replace("\"","\\\"").replace("\n","\\n").replace("\r","\\r");
|
||||
}
|
||||
}
|
||||
70
src/main/java/net/viper/status/stats/PlayerStats.java
Normal file
70
src/main/java/net/viper/status/stats/PlayerStats.java
Normal file
@@ -0,0 +1,70 @@
|
||||
package net.viper.status.stats;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class PlayerStats {
|
||||
public final UUID uuid;
|
||||
public String name;
|
||||
public long firstSeen;
|
||||
public long lastSeen;
|
||||
public long totalPlaytime;
|
||||
public long currentSessionStart;
|
||||
public int joins;
|
||||
|
||||
public PlayerStats(UUID uuid, String name) {
|
||||
this.uuid = uuid;
|
||||
this.name = name;
|
||||
long now = System.currentTimeMillis() / 1000L;
|
||||
this.firstSeen = now;
|
||||
this.lastSeen = now;
|
||||
this.totalPlaytime = 0;
|
||||
this.currentSessionStart = 0;
|
||||
this.joins = 0;
|
||||
}
|
||||
|
||||
public synchronized void onJoin() {
|
||||
long now = System.currentTimeMillis() / 1000L;
|
||||
if (this.currentSessionStart == 0) this.currentSessionStart = now;
|
||||
this.lastSeen = now;
|
||||
this.joins++;
|
||||
if (this.firstSeen == 0) this.firstSeen = now;
|
||||
}
|
||||
|
||||
public synchronized void onQuit() {
|
||||
long now = System.currentTimeMillis() / 1000L;
|
||||
if (this.currentSessionStart > 0) {
|
||||
long session = now - this.currentSessionStart;
|
||||
if (session > 0) this.totalPlaytime += session;
|
||||
this.currentSessionStart = 0;
|
||||
}
|
||||
this.lastSeen = now;
|
||||
}
|
||||
|
||||
public synchronized long getPlaytimeWithCurrentSession() {
|
||||
long now = System.currentTimeMillis() / 1000L;
|
||||
if (this.currentSessionStart > 0) return totalPlaytime + (now - currentSessionStart);
|
||||
return totalPlaytime;
|
||||
}
|
||||
|
||||
public synchronized String toLine() {
|
||||
return uuid + "|" + name.replace("|", "_") + "|" + firstSeen + "|" + lastSeen + "|" + totalPlaytime + "|" + currentSessionStart + "|" + joins;
|
||||
}
|
||||
|
||||
public static PlayerStats fromLine(String line) {
|
||||
String[] parts = line.split("\\|", -1);
|
||||
if (parts.length < 7) return null;
|
||||
try {
|
||||
UUID uuid = UUID.fromString(parts[0]);
|
||||
String name = parts[1];
|
||||
PlayerStats ps = new PlayerStats(uuid, name);
|
||||
ps.firstSeen = Long.parseLong(parts[2]);
|
||||
ps.lastSeen = Long.parseLong(parts[3]);
|
||||
ps.totalPlaytime = Long.parseLong(parts[4]);
|
||||
ps.currentSessionStart = Long.parseLong(parts[5]);
|
||||
ps.joins = Integer.parseInt(parts[6]);
|
||||
return ps;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/main/java/net/viper/status/stats/StatsManager.java
Normal file
35
src/main/java/net/viper/status/stats/StatsManager.java
Normal file
@@ -0,0 +1,35 @@
|
||||
package net.viper.status.stats;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class StatsManager {
|
||||
private final ConcurrentHashMap<UUID, PlayerStats> map = new ConcurrentHashMap<>();
|
||||
|
||||
public PlayerStats get(UUID uuid, String name) {
|
||||
return map.compute(uuid, (k, v) -> {
|
||||
if (v == null) {
|
||||
return new PlayerStats(uuid, name != null ? name : "");
|
||||
} else {
|
||||
if (name != null && !name.isEmpty()) v.name = name;
|
||||
return v;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public PlayerStats getIfPresent(UUID uuid) {
|
||||
return map.get(uuid);
|
||||
}
|
||||
|
||||
public Iterable<PlayerStats> all() {
|
||||
return map.values();
|
||||
}
|
||||
|
||||
public void put(PlayerStats ps) {
|
||||
map.put(ps.uuid, ps);
|
||||
}
|
||||
|
||||
public void remove(UUID uuid) {
|
||||
map.remove(uuid);
|
||||
}
|
||||
}
|
||||
100
src/main/java/net/viper/status/stats/StatsModule.java
Normal file
100
src/main/java/net/viper/status/stats/StatsModule.java
Normal file
@@ -0,0 +1,100 @@
|
||||
package net.viper.status.stats;
|
||||
|
||||
import net.md_5.bungee.api.event.PlayerDisconnectEvent;
|
||||
import net.md_5.bungee.api.event.PostLoginEvent;
|
||||
import net.md_5.bungee.api.plugin.Plugin;
|
||||
import net.md_5.bungee.api.plugin.Listener;
|
||||
import net.md_5.bungee.event.EventHandler;
|
||||
import net.viper.status.module.Module;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* StatsModule: Kümmert sich eigenständig um das Tracking der Spielerdaten.
|
||||
* Implementiert Module (für das Lifecycle) und Listener (für die Events).
|
||||
*/
|
||||
public class StatsModule implements Module, Listener {
|
||||
|
||||
private StatsManager manager;
|
||||
private StatsStorage storage;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "StatsModule";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable(Plugin plugin) {
|
||||
// Initialisierung
|
||||
manager = new StatsManager();
|
||||
storage = new StatsStorage(plugin.getDataFolder());
|
||||
|
||||
// Laden
|
||||
try {
|
||||
storage.load(manager);
|
||||
plugin.getLogger().info("Player-Stats wurden erfolgreich geladen.");
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("Fehler beim Laden der Stats: " + e.getMessage());
|
||||
}
|
||||
|
||||
// Event Listener registrieren
|
||||
plugin.getProxy().getPluginManager().registerListener(plugin, this);
|
||||
|
||||
// Auto-Save Task (alle 5 Minuten)
|
||||
plugin.getProxy().getScheduler().schedule(plugin, () -> {
|
||||
try {
|
||||
storage.save(manager);
|
||||
plugin.getLogger().info("Auto-Save: Player-Stats gespeichert.");
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("Fehler beim Auto-Save: " + e.getMessage());
|
||||
}
|
||||
}, 5, 5, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable(Plugin plugin) {
|
||||
if (manager != null && storage != null) {
|
||||
// Laufende Sessions beenden vor dem Speichern
|
||||
long now = System.currentTimeMillis() / 1000L;
|
||||
for (PlayerStats ps : manager.all()) {
|
||||
synchronized (ps) {
|
||||
if (ps.currentSessionStart > 0) {
|
||||
long delta = now - ps.currentSessionStart;
|
||||
if (delta > 0) {
|
||||
ps.totalPlaytime += delta;
|
||||
}
|
||||
ps.currentSessionStart = 0; // Session beenden
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
storage.save(manager);
|
||||
plugin.getLogger().info("Player-Stats beim Shutdown gespeichert.");
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("Fehler beim Speichern (Shutdown): " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Öffentlicher Zugriff für den WebServer
|
||||
public StatsManager getManager() {
|
||||
return manager;
|
||||
}
|
||||
|
||||
// --- Events ---
|
||||
|
||||
@EventHandler
|
||||
public void onJoin(PostLoginEvent e) {
|
||||
try {
|
||||
manager.get(e.getPlayer().getUniqueId(), e.getPlayer().getName()).onJoin();
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onQuit(PlayerDisconnectEvent e) {
|
||||
try {
|
||||
PlayerStats ps = manager.getIfPresent(e.getPlayer().getUniqueId());
|
||||
if (ps != null) ps.onQuit();
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
37
src/main/java/net/viper/status/stats/StatsStorage.java
Normal file
37
src/main/java/net/viper/status/stats/StatsStorage.java
Normal file
@@ -0,0 +1,37 @@
|
||||
package net.viper.status.stats;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
public class StatsStorage {
|
||||
private final File file;
|
||||
|
||||
public StatsStorage(File pluginFolder) {
|
||||
if (!pluginFolder.exists()) pluginFolder.mkdirs();
|
||||
this.file = new File(pluginFolder, "stats.dat");
|
||||
}
|
||||
|
||||
public void save(StatsManager manager) {
|
||||
try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
|
||||
for (PlayerStats ps : manager.all()) {
|
||||
bw.write(ps.toLine());
|
||||
bw.newLine();
|
||||
}
|
||||
bw.flush();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void load(StatsManager manager) {
|
||||
if (!file.exists()) return;
|
||||
try (BufferedReader br = new BufferedReader(new FileReader(file))) {
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
PlayerStats ps = PlayerStats.fromLine(line);
|
||||
if (ps != null) manager.put(ps);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
0
src/main/resources/filter.yml
Normal file
0
src/main/resources/filter.yml
Normal file
18
src/main/resources/messages.txt
Normal file
18
src/main/resources/messages.txt
Normal 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!
|
||||
29
src/main/resources/plugin.yml
Normal file
29
src/main/resources/plugin.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
name: StatusAPI
|
||||
main: net.viper.status.StatusAPI
|
||||
version: 4.0.6
|
||||
author: M_Viper
|
||||
description: StatusAPI für BungeeCord inkl. Update-Checker und Modul-System
|
||||
|
||||
softdepend:
|
||||
- LuckPerms
|
||||
|
||||
|
||||
commands:
|
||||
# Verify Modul Befehle
|
||||
verify:
|
||||
description: Verifiziere dich mit einem Token
|
||||
usage: /verify <token>
|
||||
|
||||
# ForumBridge Modul Befehle
|
||||
forumlink:
|
||||
description: Verknüpfe deinen Minecraft-Account mit dem Forum
|
||||
usage: /forumlink <token>
|
||||
|
||||
|
||||
|
||||
permissions:
|
||||
# StatusAPI Core Permissions
|
||||
statusapi.update.notify:
|
||||
description: 'Erlaubt Update-Benachrichtigungen'
|
||||
default: op
|
||||
|
||||
80
src/main/resources/verify.properties
Normal file
80
src/main/resources/verify.properties
Normal file
@@ -0,0 +1,80 @@
|
||||
# _____ __ __ ___ ____ ____
|
||||
# / ___// /_____ _/ /___ _______/ | / __ \/ _/
|
||||
# \__ \/ __/ __ `/ __/ / / / ___/ /| | / /_/ // /
|
||||
# ___/ / /_/ /_/ / /_/ /_/ (__ ) ___ |/ ____// /
|
||||
# /____/\__/\__,_/\__/\__,_/____/_/ |_/_/ /___/
|
||||
|
||||
|
||||
broadcast.enabled=true
|
||||
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%
|
||||
|
||||
# ===========================
|
||||
# StatusAPI Einstellungen
|
||||
# ===========================
|
||||
statusapi.port=9191
|
||||
|
||||
# ===========================
|
||||
# WORDPRESS / VERIFY EINSTELLUNGEN
|
||||
# ===========================
|
||||
wp_verify_url=https://example.com
|
||||
|
||||
# Gemeinsames API-Secret (muss identisch sein mit mc_bridge_api_secret in den WP Forum-Einstellungen)
|
||||
forum.api_secret=HIER_EIN_SICHERES_PASSWORT_SETZEN
|
||||
|
||||
# Verzögerung in Sekunden bevor Login-Benachrichtigungen zugestellt werden
|
||||
# (damit der Spieler den Server-Wechsel abgeschlossen hat)
|
||||
forum.login_delay_seconds=3
|
||||
|
||||
# ===========================
|
||||
# COMMAND BLOCKER
|
||||
# ===========================
|
||||
commandblocker.enabled=true
|
||||
commandblocker.bypass.permission=commandblocker.bypass
|
||||
|
||||
# ===========================
|
||||
# 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=64
|
||||
server.Lobby.secret=GeheimesWortFuerLobby789
|
||||
|
||||
# Server 1: Citybuild
|
||||
server.citybuild=&bCitybuild
|
||||
server.citybuild.id=67
|
||||
server.citybuild.secret=GeheimesWortFuerCitybuild789
|
||||
|
||||
# Server 2: Survival
|
||||
server.survival=&aSurvival
|
||||
server.survival.id=68
|
||||
server.survival.secret=GeheimesWortFuerSurvival789
|
||||
|
||||
# Server 3: SkyBlock
|
||||
server.skyblock=&dSkyBlock
|
||||
server.skyblock.id=3
|
||||
server.skyblock.secret=GeheimesWortFuerSkyBlock789
|
||||
|
||||
# ===========================
|
||||
# AUTOMESSAGE
|
||||
# ===========================
|
||||
# Aktiviert den automatischen Nachrichten-Rundruf
|
||||
automessage.enabled=true
|
||||
|
||||
# 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
|
||||
79
src/main/resources/verify.template.properties
Normal file
79
src/main/resources/verify.template.properties
Normal file
@@ -0,0 +1,79 @@
|
||||
# _____ __ __ ___ ____ ____
|
||||
# / ___// /_____ _/ /___ _______/ | / __ \/ _/
|
||||
# \__ \/ __/ __ `/ __/ / / / ___/ /| | / /_/ // /
|
||||
# ___/ / /_/ /_/ / /_/ /_/ (__ ) ___ |/ ____// /
|
||||
# /____/\__/\__,_/\__/\__,_/____/_/ |_/_/ /___/
|
||||
|
||||
|
||||
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
|
||||
8
src/main/resources/welcome.yml
Normal file
8
src/main/resources/welcome.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
# Willkommensnachrichten, die zufällig gesendet werden, wenn ein Spieler joint.
|
||||
# %player% wird durch den Spielernamen ersetzt.
|
||||
welcome-messages:
|
||||
- "&aWillkommen, %player%! Viel Spaß auf unserem Server!"
|
||||
- "&aHey %player%, schön dich hier zu sehen! Los geht's!"
|
||||
- "&a%player%, dein Abenteuer beginnt jetzt! Viel Spaß!"
|
||||
- "&aWillkommen an Bord, %player%! Entdecke den Server!"
|
||||
- "&a%player%, herzlich willkommen! Lass uns loslegen!"
|
||||
Reference in New Issue
Block a user