From 39bbe8d4ad101b5a21c158ba362c13e3b105d9cf Mon Sep 17 00:00:00 2001 From: M_Viper Date: Mon, 30 Mar 2026 20:45:29 +0200 Subject: [PATCH] Update from Git Manager GUI --- src/main/java/net/viper/status/StatusAPI.java | 602 ++++++++++++++++++ .../java/net/viper/status/UpdateChecker.java | 144 +++++ .../java/net/viper/status/module/Module.java | 24 + .../viper/status/module/ModuleManager.java | 60 ++ .../AutoMessage/AutoMessageModule.java | 115 ++++ .../modules/broadcast/BroadcastModule.java | 381 +++++++++++ .../commandblocker/CommandBlockerModule.java | 180 ++++++ .../customcommands/CustomCommandModule.java | 242 +++++++ .../modules/customcommands/ForwardSender.java | 118 ++++ .../modules/forum/ForumBridgeModule.java | 519 +++++++++++++++ .../modules/forum/ForumNotifStorage.java | 127 ++++ .../modules/forum/ForumNotification.java | 114 ++++ .../status/modules/verify/VerifyModule.java | 248 ++++++++ .../net/viper/status/stats/PlayerStats.java | 70 ++ .../net/viper/status/stats/StatsManager.java | 35 + .../net/viper/status/stats/StatsModule.java | 100 +++ .../net/viper/status/stats/StatsStorage.java | 37 ++ src/main/resources/filter.yml | 0 src/main/resources/messages.txt | 18 + src/main/resources/plugin.yml | 29 + src/main/resources/verify.properties | 80 +++ src/main/resources/verify.template.properties | 79 +++ src/main/resources/welcome.yml | 8 + 23 files changed, 3330 insertions(+) create mode 100644 src/main/java/net/viper/status/StatusAPI.java create mode 100644 src/main/java/net/viper/status/UpdateChecker.java create mode 100644 src/main/java/net/viper/status/module/Module.java create mode 100644 src/main/java/net/viper/status/module/ModuleManager.java create mode 100644 src/main/java/net/viper/status/modules/AutoMessage/AutoMessageModule.java create mode 100644 src/main/java/net/viper/status/modules/broadcast/BroadcastModule.java create mode 100644 src/main/java/net/viper/status/modules/commandblocker/CommandBlockerModule.java create mode 100644 src/main/java/net/viper/status/modules/customcommands/CustomCommandModule.java create mode 100644 src/main/java/net/viper/status/modules/customcommands/ForwardSender.java create mode 100644 src/main/java/net/viper/status/modules/forum/ForumBridgeModule.java create mode 100644 src/main/java/net/viper/status/modules/forum/ForumNotifStorage.java create mode 100644 src/main/java/net/viper/status/modules/forum/ForumNotification.java create mode 100644 src/main/java/net/viper/status/modules/verify/VerifyModule.java create mode 100644 src/main/java/net/viper/status/stats/PlayerStats.java create mode 100644 src/main/java/net/viper/status/stats/StatsManager.java create mode 100644 src/main/java/net/viper/status/stats/StatsModule.java create mode 100644 src/main/java/net/viper/status/stats/StatsStorage.java create mode 100644 src/main/resources/filter.yml create mode 100644 src/main/resources/messages.txt create mode 100644 src/main/resources/plugin.yml create mode 100644 src/main/resources/verify.properties create mode 100644 src/main/resources/verify.template.properties create mode 100644 src/main/resources/welcome.yml diff --git a/src/main/java/net/viper/status/StatusAPI.java b/src/main/java/net/viper/status/StatusAPI.java new file mode 100644 index 0000000..20a8946 --- /dev/null +++ b/src/main/java/net/viper/status/StatusAPI.java @@ -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 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 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 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> playersList = new ArrayList<>(); + + for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { + Map 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 data) { + StringBuilder sb = new StringBuilder("{"); + boolean first = true; + for (Map.Entry 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 m = (Map) 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) 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"); + } +} \ No newline at end of file diff --git a/src/main/java/net/viper/status/UpdateChecker.java b/src/main/java/net/viper/status/UpdateChecker.java new file mode 100644 index 0000000..1990d54 --- /dev/null +++ b/src/main/java/net/viper/status/UpdateChecker.java @@ -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 names = new java.util.ArrayList<>(); + java.util.List 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); + } + } +} \ No newline at end of file diff --git a/src/main/java/net/viper/status/module/Module.java b/src/main/java/net/viper/status/module/Module.java new file mode 100644 index 0000000..371b1c7 --- /dev/null +++ b/src/main/java/net/viper/status/module/Module.java @@ -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(); +} \ No newline at end of file diff --git a/src/main/java/net/viper/status/module/ModuleManager.java b/src/main/java/net/viper/status/module/ModuleManager.java new file mode 100644 index 0000000..2fcc15e --- /dev/null +++ b/src/main/java/net/viper/status/module/ModuleManager.java @@ -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 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 getModule(Class clazz) { + for (Module m : modules.values()) { + if (clazz.isInstance(m)) { + return (T) m; + } + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/net/viper/status/modules/AutoMessage/AutoMessageModule.java b/src/main/java/net/viper/status/modules/AutoMessage/AutoMessageModule.java new file mode 100644 index 0000000..9c36b5d --- /dev/null +++ b/src/main/java/net/viper/status/modules/AutoMessage/AutoMessageModule.java @@ -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 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."); + } + } +} \ No newline at end of file diff --git a/src/main/java/net/viper/status/modules/broadcast/BroadcastModule.java b/src/main/java/net/viper/status/modules/broadcast/BroadcastModule.java new file mode 100644 index 0000000..03ce738 --- /dev/null +++ b/src/main/java/net/viper/status/modules/broadcast/BroadcastModule.java @@ -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 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 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 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 toRemove = new ArrayList<>(); + boolean changed = false; + + for (Map.Entry 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); + } + } +} \ No newline at end of file diff --git a/src/main/java/net/viper/status/modules/commandblocker/CommandBlockerModule.java b/src/main/java/net/viper/status/modules/commandblocker/CommandBlockerModule.java new file mode 100644 index 0000000..68408c7 --- /dev/null +++ b/src/main/java/net/viper/status/modules/commandblocker/CommandBlockerModule.java @@ -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 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 "); + sender.sendMessage(ChatColor.YELLOW + "/cb remove "); + 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 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 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()); + } + } +} diff --git a/src/main/java/net/viper/status/modules/customcommands/CustomCommandModule.java b/src/main/java/net/viper/status/modules/customcommands/CustomCommandModule.java new file mode 100644 index 0000000..0b46042 --- /dev/null +++ b/src/main/java/net/viper/status/modules/customcommands/CustomCommandModule.java @@ -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 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 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); + } +} diff --git a/src/main/java/net/viper/status/modules/customcommands/ForwardSender.java b/src/main/java/net/viper/status/modules/customcommands/ForwardSender.java new file mode 100644 index 0000000..b1598bc --- /dev/null +++ b/src/main/java/net/viper/status/modules/customcommands/ForwardSender.java @@ -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 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 getPermissions() { + Collection 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(); + } +} diff --git a/src/main/java/net/viper/status/modules/forum/ForumBridgeModule.java b/src/main/java/net/viper/status/modules/forum/ForumBridgeModule.java new file mode 100644 index 0000000..0bf0712 --- /dev/null +++ b/src/main/java/net/viper/status/modules/forum/ForumBridgeModule.java @@ -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 — 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 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 — 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 ")); + 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 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(); + } + } +} diff --git a/src/main/java/net/viper/status/modules/forum/ForumNotifStorage.java b/src/main/java/net/viper/status/modules/forum/ForumNotifStorage.java new file mode 100644 index 0000000..8a283ca --- /dev/null +++ b/src/main/java/net/viper/status/modules/forum/ForumNotifStorage.java @@ -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> 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 getPending(UUID playerUuid) { + CopyOnWriteArrayList list = pending.get(playerUuid); + if (list == null) return Collections.emptyList(); + List result = new ArrayList<>(); + for (ForumNotification n : list) { + if (!n.isDelivered()) result.add(n); + } + return result; + } + + /** + * Anzahl ausstehender Benachrichtigungen. + */ + public int getPendingCount(UUID playerUuid) { + CopyOnWriteArrayList 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 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 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> 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 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()); + } + } +} diff --git a/src/main/java/net/viper/status/modules/forum/ForumNotification.java b/src/main/java/net/viper/status/modules/forum/ForumNotification.java new file mode 100644 index 0000000..9a9d2fa --- /dev/null +++ b/src/main/java/net/viper/status/modules/forum/ForumNotification.java @@ -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; + } + } +} diff --git a/src/main/java/net/viper/status/modules/verify/VerifyModule.java b/src/main/java/net/viper/status/modules/verify/VerifyModule.java new file mode 100644 index 0000000..e0c4ebb --- /dev/null +++ b/src/main/java/net/viper/status/modules/verify/VerifyModule.java @@ -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 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..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 "); + 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"); + } +} \ No newline at end of file diff --git a/src/main/java/net/viper/status/stats/PlayerStats.java b/src/main/java/net/viper/status/stats/PlayerStats.java new file mode 100644 index 0000000..67a4715 --- /dev/null +++ b/src/main/java/net/viper/status/stats/PlayerStats.java @@ -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; + } + } +} \ No newline at end of file diff --git a/src/main/java/net/viper/status/stats/StatsManager.java b/src/main/java/net/viper/status/stats/StatsManager.java new file mode 100644 index 0000000..8c7e222 --- /dev/null +++ b/src/main/java/net/viper/status/stats/StatsManager.java @@ -0,0 +1,35 @@ +package net.viper.status.stats; + +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class StatsManager { + private final ConcurrentHashMap 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 all() { + return map.values(); + } + + public void put(PlayerStats ps) { + map.put(ps.uuid, ps); + } + + public void remove(UUID uuid) { + map.remove(uuid); + } +} \ No newline at end of file diff --git a/src/main/java/net/viper/status/stats/StatsModule.java b/src/main/java/net/viper/status/stats/StatsModule.java new file mode 100644 index 0000000..018bdff --- /dev/null +++ b/src/main/java/net/viper/status/stats/StatsModule.java @@ -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) {} + } +} \ No newline at end of file diff --git a/src/main/java/net/viper/status/stats/StatsStorage.java b/src/main/java/net/viper/status/stats/StatsStorage.java new file mode 100644 index 0000000..e08ea2a --- /dev/null +++ b/src/main/java/net/viper/status/stats/StatsStorage.java @@ -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(); + } + } +} \ No newline at end of file diff --git a/src/main/resources/filter.yml b/src/main/resources/filter.yml new file mode 100644 index 0000000..e69de29 diff --git a/src/main/resources/messages.txt b/src/main/resources/messages.txt new file mode 100644 index 0000000..8c1f95a --- /dev/null +++ b/src/main/resources/messages.txt @@ -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! \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..d3bfa28 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -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 + + # ForumBridge Modul Befehle + forumlink: + description: Verknüpfe deinen Minecraft-Account mit dem Forum + usage: /forumlink + + + +permissions: + # StatusAPI Core Permissions + statusapi.update.notify: + description: 'Erlaubt Update-Benachrichtigungen' + default: op + diff --git a/src/main/resources/verify.properties b/src/main/resources/verify.properties new file mode 100644 index 0000000..8b23f0c --- /dev/null +++ b/src/main/resources/verify.properties @@ -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 diff --git a/src/main/resources/verify.template.properties b/src/main/resources/verify.template.properties new file mode 100644 index 0000000..caf28a4 --- /dev/null +++ b/src/main/resources/verify.template.properties @@ -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. = +# 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 diff --git a/src/main/resources/welcome.yml b/src/main/resources/welcome.yml new file mode 100644 index 0000000..249a6d5 --- /dev/null +++ b/src/main/resources/welcome.yml @@ -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!" \ No newline at end of file