From 8e1aa67bbed7cc3d97ed327ba6bb725f14f25e78 Mon Sep 17 00:00:00 2001 From: M_Viper Date: Thu, 2 Apr 2026 06:27:33 +0000 Subject: [PATCH] Dateien nach "src/main/java/net/viper/status/modules/network" hochladen --- .../modules/network/NetworkInfoModule.java | 687 ++++++++++++++++++ 1 file changed, 687 insertions(+) create mode 100644 src/main/java/net/viper/status/modules/network/NetworkInfoModule.java diff --git a/src/main/java/net/viper/status/modules/network/NetworkInfoModule.java b/src/main/java/net/viper/status/modules/network/NetworkInfoModule.java new file mode 100644 index 0000000..1d9cf73 --- /dev/null +++ b/src/main/java/net/viper/status/modules/network/NetworkInfoModule.java @@ -0,0 +1,687 @@ +package net.viper.status.modules.network; + +import com.sun.management.OperatingSystemMXBean; +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.config.ServerInfo; +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 java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStreamReader; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.lang.management.ManagementFactory; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * Liefert erweiterte Proxy- und Systeminformationen für API und Ingame-Debug. + */ +public class NetworkInfoModule implements Module { + + private static final String CONFIG_FILE_NAME = "network-guard.properties"; + + private Plugin plugin; + private long startedAtMillis; + + private boolean enabled = true; + private boolean commandEnabled = true; + private boolean includePlayerNames = false; + + private boolean webhookEnabled = false; + private String webhookUrl = ""; + private String webhookUsername = "StatusAPI"; + private String webhookThumbnailUrl = ""; + private boolean webhookNotifyStartStop = true; + private String webhookEmbedMode = "detailed"; + private int webhookCheckSeconds = 30; + private int alertMemoryPercent = 90; + private int alertPlayerPercent = 95; + private int alertCooldownSeconds = 300; + private boolean attackNotificationsEnabled = true; + private String attackApiKey = ""; + private String attackDefaultSource = "BetterBungee"; + private long lastMemoryAlertAt = 0L; + private long lastPlayerAlertAt = 0L; + + @Override + public String getName() { + return "NetworkInfoModule"; + } + + @Override + public void onEnable(Plugin plugin) { + this.plugin = plugin; + this.startedAtMillis = System.currentTimeMillis(); + ensureModuleConfigExists(); + loadConfig(); + + if (!enabled) { + plugin.getLogger().info("[NetworkInfoModule] deaktiviert via " + CONFIG_FILE_NAME + " (networkinfo.enabled=false)"); + return; + } + + if (commandEnabled) { + ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new NetInfoCommand()); + } + + if (webhookEnabled && !webhookUrl.isEmpty()) { + if (webhookNotifyStartStop) { + if (isCompactEmbedMode()) { + sendWebhookEmbed( + webhookUrl, + "✅ NetworkInfo gestartet", + "Proxy: **" + ProxyServer.getInstance().getName() + "**\nÜberwachung und Webhook-Alerts sind jetzt aktiv.", + 0x2ECC71 + ); + } else { + StringBuilder fields = new StringBuilder(); + appendEmbedField(fields, "Proxy", ProxyServer.getInstance().getName(), true); + appendEmbedField(fields, "Modus", "Detailed", true); + appendEmbedField(fields, "Check-Intervall", Math.max(10, webhookCheckSeconds) + "s", true); + sendWebhookEmbed( + webhookUrl, + "✅ NetworkInfo gestartet", + "Überwachung und Webhook-Alerts sind jetzt aktiv.", + 0x2ECC71, + fields.toString() + ); + } + } + int interval = Math.max(10, webhookCheckSeconds); + ProxyServer.getInstance().getScheduler().schedule(plugin, this::evaluateAndSendAlerts, interval, interval, TimeUnit.SECONDS); + } + + plugin.getLogger().info("[NetworkInfoModule] aktiviert. commandEnabled=" + commandEnabled + ", includePlayerNames=" + includePlayerNames); + } + + @Override + public void onDisable(Plugin plugin) { + if (enabled && webhookEnabled && webhookNotifyStartStop && webhookUrl != null && !webhookUrl.isEmpty()) { + if (isCompactEmbedMode()) { + sendWebhookEmbed( + webhookUrl, + "🛑 NetworkInfo gestoppt", + "Die NetworkInfo-Überwachung wurde gestoppt.\nKeine weiteren Auto-Alerts bis zum nächsten Start.", + 0xE74C3C + ); + } else { + StringBuilder fields = new StringBuilder(); + appendEmbedField(fields, "Proxy", ProxyServer.getInstance().getName(), true); + appendEmbedField(fields, "Modus", "Detailed", true); + appendEmbedField(fields, "Status", "Monitoring pausiert", false); + sendWebhookEmbed( + webhookUrl, + "🛑 NetworkInfo gestoppt", + "Die NetworkInfo-Überwachung wurde gestoppt.", + 0xE74C3C, + fields.toString() + ); + } + } + } + + public boolean isEnabled() { + return enabled; + } + + public boolean isAttackNotificationsEnabled() { + return enabled && attackNotificationsEnabled; + } + + public boolean isAttackApiKeyValid(String providedKey) { + if (attackApiKey == null || attackApiKey.isEmpty()) { + return true; + } + return providedKey != null && attackApiKey.equals(providedKey.trim()); + } + + public boolean sendAttackNotification(String eventType, + Integer connectionsPerSecond, + Integer blockedIps, + Long blockedConnections, + String source) { + if (!isAttackNotificationsEnabled()) { + return false; + } + + String usedSource = (source == null || source.trim().isEmpty()) ? attackDefaultSource : source.trim(); + String type = eventType == null ? "detected" : eventType.trim().toLowerCase(Locale.ROOT); + + String title; + String shortText; + int color; + if ("stopped".equals(type) || "mitigated".equals(type)) { + title = "✅ Attack Stopped"; + shortText = "Traffic hat sich normalisiert."; + color = 0x2ECC71; + } else { + title = "🚨 Attack Detected"; + shortText = "Ungewöhnlich hoher Verbindungs-Traffic erkannt."; + color = 0xE74C3C; + } + + StringBuilder fields = new StringBuilder(); + appendEmbedField(fields, "Source", usedSource, true); + appendEmbedField(fields, "Event", type.toUpperCase(Locale.ROOT), true); + + if (connectionsPerSecond != null && connectionsPerSecond >= 0) { + appendEmbedField(fields, "Connections / Second", String.valueOf(connectionsPerSecond), true); + } + if (blockedIps != null && blockedIps >= 0) { + appendEmbedField(fields, "Blocked IPs", String.valueOf(blockedIps), true); + } + if (blockedConnections != null && blockedConnections >= 0L) { + appendEmbedField(fields, "Blocked Connections", String.valueOf(blockedConnections), true); + } + + sendWebhookAttackEmbed(webhookUrl, title, shortText, color, fields.toString()); + return true; + } + + public Map buildSnapshot() { + Map out = new LinkedHashMap(); + + long now = System.currentTimeMillis(); + long uptimeMs = Math.max(0L, now - startedAtMillis); + + Runtime rt = Runtime.getRuntime(); + long maxMemory = rt.maxMemory(); + long totalMemory = rt.totalMemory(); + long freeMemory = rt.freeMemory(); + long usedMemory = totalMemory - freeMemory; + + Map memory = new LinkedHashMap(); + memory.put("used_mb", bytesToMb(usedMemory)); + memory.put("free_mb", bytesToMb(freeMemory)); + memory.put("total_mb", bytesToMb(totalMemory)); + memory.put("max_mb", bytesToMb(maxMemory)); + memory.put("usage_percent", percent(usedMemory, Math.max(1L, maxMemory))); + + int onlinePlayers = ProxyServer.getInstance().getPlayers().size(); + int maxPlayers = ProxyServer.getInstance().getConfig().getPlayerLimit(); + + Map ping = buildPingSummary(ProxyServer.getInstance().getPlayers()); + + Map players = new LinkedHashMap(); + players.put("online", onlinePlayers); + players.put("max", maxPlayers); + players.put("occupancy_percent", percent(onlinePlayers, Math.max(1, maxPlayers))); + players.put("bedrock_online", countBedrockPlayers()); + players.put("ping", ping); + + List> backend = buildBackendDistribution(); + + Map system = new LinkedHashMap(); + system.put("java_version", System.getProperty("java.version")); + system.put("java_vendor", System.getProperty("java.vendor")); + system.put("os_name", System.getProperty("os.name")); + system.put("os_arch", System.getProperty("os.arch")); + system.put("available_processors", Runtime.getRuntime().availableProcessors()); + system.put("system_load_percent", getSystemLoadPercent()); + + out.put("enabled", true); + out.put("timestamp_unix", now / 1000L); + out.put("uptime_seconds", uptimeMs / 1000L); + out.put("uptime_human", formatDuration(uptimeMs)); + out.put("players", players); + out.put("backend_servers", backend); + out.put("memory", memory); + out.put("system", system); + out.put("features", buildFeatureSummary()); + + if (includePlayerNames) { + List names = new ArrayList(); + for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { + names.add(p.getName()); + } + out.put("player_names", names); + } + + return out; + } + + private Map buildPingSummary(Collection players) { + Map ping = new LinkedHashMap(); + if (players.isEmpty()) { + ping.put("avg_ms", 0); + ping.put("min_ms", 0); + ping.put("max_ms", 0); + return ping; + } + + long sum = 0L; + int min = Integer.MAX_VALUE; + int max = Integer.MIN_VALUE; + + for (ProxiedPlayer p : players) { + int ms = Math.max(0, p.getPing()); + sum += ms; + if (ms < min) min = ms; + if (ms > max) max = ms; + } + + ping.put("avg_ms", Math.round((double) sum / (double) players.size())); + ping.put("min_ms", min == Integer.MAX_VALUE ? 0 : min); + ping.put("max_ms", max == Integer.MIN_VALUE ? 0 : max); + return ping; + } + + private List> buildBackendDistribution() { + List> list = new ArrayList>(); + for (Map.Entry entry : ProxyServer.getInstance().getServers().entrySet()) { + ServerInfo info = entry.getValue(); + + Map row = new LinkedHashMap(); + row.put("name", entry.getKey()); + row.put("online_players", info.getPlayers().size()); + row.put("address", String.valueOf(info.getAddress())); + list.add(row); + } + return list; + } + + private Map buildFeatureSummary() { + Map features = new LinkedHashMap(); + features.put("luckperms", ProxyServer.getInstance().getPluginManager().getPlugin("LuckPerms") != null); + features.put("floodgate", isFloodgateAvailable()); + return features; + } + + private boolean isFloodgateAvailable() { + try { + Class.forName("org.geysermc.floodgate.api.FloodgateApi"); + return true; + } catch (Throwable ignored) { + return false; + } + } + + private int countBedrockPlayers() { + try { + Class apiClass = Class.forName("org.geysermc.floodgate.api.FloodgateApi"); + Object api = apiClass.getMethod("getInstance").invoke(null); + int count = 0; + for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { + Boolean isBedrock = (Boolean) api.getClass().getMethod("isBedrockPlayer", UUID.class).invoke(api, p.getUniqueId()); + if (Boolean.TRUE.equals(isBedrock)) { + count++; + } + } + return count; + } catch (Throwable ignored) { + return 0; + } + } + + private Integer getSystemLoadPercent() { + try { + java.lang.management.OperatingSystemMXBean bean = ManagementFactory.getOperatingSystemMXBean(); + if (bean instanceof OperatingSystemMXBean) { + double load = ((OperatingSystemMXBean) bean).getSystemCpuLoad(); + if (load >= 0D) { + return (int) Math.round(load * 100D); + } + } + } catch (Throwable ignored) { + } + return null; + } + + private int bytesToMb(long bytes) { + return (int) (bytes / (1024L * 1024L)); + } + + private int percent(long value, long max) { + if (max <= 0L) return 0; + return (int) Math.min(100L, Math.round((value * 100.0D) / max)); + } + + private String formatDuration(long ms) { + long totalSeconds = ms / 1000L; + long days = totalSeconds / 86400L; + long hours = (totalSeconds % 86400L) / 3600L; + long minutes = (totalSeconds % 3600L) / 60L; + long seconds = totalSeconds % 60L; + return String.format(Locale.ROOT, "%dd %02dh %02dm %02ds", days, hours, minutes, seconds); + } + + private void loadConfig() { + File file = new File(plugin.getDataFolder(), CONFIG_FILE_NAME); + if (!file.exists()) { + enabled = true; + commandEnabled = true; + includePlayerNames = false; + webhookEnabled = false; + webhookUrl = ""; + webhookUsername = "StatusAPI"; + webhookThumbnailUrl = ""; + webhookNotifyStartStop = true; + webhookEmbedMode = "detailed"; + webhookCheckSeconds = 30; + alertMemoryPercent = 90; + alertPlayerPercent = 95; + alertCooldownSeconds = 300; + attackNotificationsEnabled = true; + attackApiKey = ""; + attackDefaultSource = "BetterBungee"; + return; + } + + Properties props = new Properties(); + try (FileInputStream in = new FileInputStream(file)) { + props.load(new InputStreamReader(in, StandardCharsets.UTF_8)); + enabled = Boolean.parseBoolean(props.getProperty("networkinfo.enabled", "true")); + commandEnabled = Boolean.parseBoolean(props.getProperty("networkinfo.command.enabled", "true")); + includePlayerNames = Boolean.parseBoolean(props.getProperty("networkinfo.include_player_names", "false")); + + webhookEnabled = Boolean.parseBoolean(props.getProperty("networkinfo.webhook.enabled", "false")); + webhookUrl = props.getProperty("networkinfo.webhook.url", "").trim(); + webhookUsername = props.getProperty("networkinfo.webhook.username", "StatusAPI").trim(); + webhookThumbnailUrl = props.getProperty("networkinfo.webhook.thumbnail_url", "").trim(); + webhookNotifyStartStop = Boolean.parseBoolean(props.getProperty("networkinfo.webhook.notify_start_stop", "true")); + webhookEmbedMode = props.getProperty("networkinfo.webhook.embed_mode", "detailed").trim(); + webhookCheckSeconds = parseInt(props.getProperty("networkinfo.webhook.check_seconds", "30"), 30); + alertMemoryPercent = parseInt(props.getProperty("networkinfo.alert.memory_percent", "90"), 90); + alertPlayerPercent = parseInt(props.getProperty("networkinfo.alert.player_percent", "95"), 95); + alertCooldownSeconds = parseInt(props.getProperty("networkinfo.alert.cooldown_seconds", "300"), 300); + attackNotificationsEnabled = Boolean.parseBoolean(props.getProperty("networkinfo.attack.enabled", "true")); + attackApiKey = props.getProperty("networkinfo.attack.api_key", "").trim(); + attackDefaultSource = props.getProperty("networkinfo.attack.source", "BetterBungee").trim(); + } catch (Exception e) { + plugin.getLogger().warning("[NetworkInfoModule] Fehler beim Laden von " + CONFIG_FILE_NAME + ": " + e.getMessage()); + enabled = true; + commandEnabled = true; + includePlayerNames = false; + webhookEnabled = false; + webhookUrl = ""; + webhookUsername = "StatusAPI"; + webhookThumbnailUrl = ""; + webhookNotifyStartStop = true; + webhookEmbedMode = "detailed"; + webhookCheckSeconds = 30; + alertMemoryPercent = 90; + alertPlayerPercent = 95; + alertCooldownSeconds = 300; + attackNotificationsEnabled = true; + attackApiKey = ""; + attackDefaultSource = "BetterBungee"; + } + } + + private void ensureModuleConfigExists() { + File target = new File(plugin.getDataFolder(), CONFIG_FILE_NAME); + if (target.exists()) { + return; + } + + if (!plugin.getDataFolder().exists()) { + plugin.getDataFolder().mkdirs(); + } + + try (InputStream in = plugin.getResourceAsStream(CONFIG_FILE_NAME); + OutputStream out = new FileOutputStream(target)) { + if (in == null) { + plugin.getLogger().warning("[NetworkInfoModule] Standarddatei " + CONFIG_FILE_NAME + " nicht im JAR gefunden."); + return; + } + byte[] buffer = new byte[4096]; + int read; + while ((read = in.read(buffer)) != -1) { + out.write(buffer, 0, read); + } + plugin.getLogger().info("[NetworkInfoModule] " + CONFIG_FILE_NAME + " wurde erstellt."); + } catch (Exception e) { + plugin.getLogger().warning("[NetworkInfoModule] Konnte " + CONFIG_FILE_NAME + " nicht erstellen: " + e.getMessage()); + } + } + + private void evaluateAndSendAlerts() { + if (!enabled || !webhookEnabled || webhookUrl == null || webhookUrl.isEmpty()) { + return; + } + + long now = System.currentTimeMillis(); + Map snapshot = buildSnapshot(); + + @SuppressWarnings("unchecked") + Map memory = (Map) snapshot.get("memory"); + @SuppressWarnings("unchecked") + Map players = (Map) snapshot.get("players"); + + int memoryPercent = toInt(memory.get("usage_percent")); + int playerPercent = toInt(players.get("occupancy_percent")); + + if (memoryPercent >= Math.max(1, alertMemoryPercent) && canSend(lastMemoryAlertAt, now)) { + lastMemoryAlertAt = now; + if (isCompactEmbedMode()) { + sendWebhookEmbed( + webhookUrl, + "⚠️ Hohe RAM-Auslastung", + "Aktuell: **" + memoryPercent + "%**\nVerbrauch: **" + memory.get("used_mb") + " MB / " + memory.get("max_mb") + " MB**\nSchwelle: **" + alertMemoryPercent + "%**", + 0xF39C12 + ); + } else { + StringBuilder fields = new StringBuilder(); + appendEmbedField(fields, "RAM-Nutzung", memoryPercent + "%", true); + appendEmbedField(fields, "Schwelle", alertMemoryPercent + "%", true); + appendEmbedField(fields, "Verbrauch", memory.get("used_mb") + " MB / " + memory.get("max_mb") + " MB", false); + sendWebhookEmbed( + webhookUrl, + "⚠️ Hohe RAM-Auslastung", + "Ein Schwellwert wurde überschritten.", + 0xF39C12, + fields.toString() + ); + } + } + + if (playerPercent >= Math.max(1, alertPlayerPercent) && canSend(lastPlayerAlertAt, now)) { + lastPlayerAlertAt = now; + if (isCompactEmbedMode()) { + sendWebhookEmbed( + webhookUrl, + "📈 Hohe Spieler-Auslastung", + "Auslastung: **" + playerPercent + "%**\nSpieler: **" + players.get("online") + "/" + players.get("max") + "**\nSchwelle: **" + alertPlayerPercent + "%**", + 0x3498DB + ); + } else { + StringBuilder fields = new StringBuilder(); + appendEmbedField(fields, "Auslastung", playerPercent + "%", true); + appendEmbedField(fields, "Schwelle", alertPlayerPercent + "%", true); + appendEmbedField(fields, "Spieler", players.get("online") + "/" + players.get("max"), true); + sendWebhookEmbed( + webhookUrl, + "📈 Hohe Spieler-Auslastung", + "Die Spielerlast ist aktuell sehr hoch.", + 0x3498DB, + fields.toString() + ); + } + } + } + + private boolean isCompactEmbedMode() { + return "compact".equalsIgnoreCase(webhookEmbedMode == null ? "" : webhookEmbedMode.trim()); + } + + private boolean canSend(long lastSentAt, long now) { + long cooldownMs = Math.max(10, alertCooldownSeconds) * 1000L; + return (now - lastSentAt) >= cooldownMs; + } + + private int parseInt(String s, int fallback) { + try { + return Integer.parseInt(s == null ? "" : s.trim()); + } catch (Exception ignored) { + return fallback; + } + } + + private int toInt(Object o) { + if (o instanceof Number) { + return ((Number) o).intValue(); + } + try { + return Integer.parseInt(String.valueOf(o)); + } catch (Exception ignored) { + return 0; + } + } + + private void sendWebhookEmbed(String targetWebhookUrl, String title, String description, int color) { + sendWebhookEmbed(targetWebhookUrl, title, description, color, null); + } + + private void sendWebhookEmbed(String targetWebhookUrl, String title, String description, int color, String fieldsJson) { + if (targetWebhookUrl == null || targetWebhookUrl.isEmpty()) { + return; + } + + StringBuilder embed = new StringBuilder(); + embed.append("{\"username\":\"").append(escapeJson(webhookUsername)).append("\",") + .append("\"embeds\":[{\"title\":\"").append(escapeJson(title)).append("\",") + .append("\"description\":\"").append(escapeJson(description)).append("\",") + .append("\"color\":").append(color).append(","); + + if (fieldsJson != null && !fieldsJson.trim().isEmpty()) { + embed.append("\"fields\":[").append(fieldsJson).append("],"); + } + + embed.append("\"footer\":{\"text\":\"StatusPulse • NetworkInfo\"},") + .append("\"timestamp\":\"").append(Instant.now().toString()).append("\""); + + if (webhookThumbnailUrl != null && !webhookThumbnailUrl.isEmpty()) { + embed.append(",\"thumbnail\":{\"url\":\"").append(escapeJson(webhookThumbnailUrl)).append("\"}"); + } + + embed.append("}]}"); + postWebhookPayload(targetWebhookUrl, embed.toString()); + } + + private void sendWebhookAttackEmbed(String targetWebhookUrl, + String title, + String description, + int color, + String fieldsJson) { + if (targetWebhookUrl == null || targetWebhookUrl.isEmpty()) { + return; + } + + StringBuilder embed = new StringBuilder(); + embed.append("{\"username\":\"").append(escapeJson(webhookUsername)).append("\",") + .append("\"embeds\":[{\"title\":\"").append(escapeJson(title)).append("\",") + .append("\"description\":\"").append(escapeJson(description)).append("\",") + .append("\"color\":").append(color).append(",") + .append("\"fields\":[").append(fieldsJson).append("],") + .append("\"footer\":{\"text\":\"StatusPulse • Network Guard\"},") + .append("\"timestamp\":\"").append(Instant.now().toString()).append("\""); + + if (webhookThumbnailUrl != null && !webhookThumbnailUrl.isEmpty()) { + embed.append(",\"thumbnail\":{\"url\":\"").append(escapeJson(webhookThumbnailUrl)).append("\"}"); + } + + embed.append("}]}"); + postWebhookPayload(targetWebhookUrl, embed.toString()); + } + + private void appendEmbedField(StringBuilder out, String name, String value, boolean inline) { + if (value == null || value.trim().isEmpty()) { + return; + } + if (out.length() > 0) { + out.append(","); + } + out.append("{\"name\":\"").append(escapeJson(name)).append("\",") + .append("\"value\":\"").append(escapeJson(value.trim())).append("\",") + .append("\"inline\":").append(inline ? "true" : "false") + .append("}"); + } + + private void postWebhookPayload(String targetWebhookUrl, String payload) { + ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> { + HttpURLConnection conn = null; + try { + byte[] bytes = payload.getBytes(StandardCharsets.UTF_8); + + conn = (HttpURLConnection) new URL(targetWebhookUrl).openConnection(); + conn.setRequestMethod("POST"); + conn.setConnectTimeout(5000); + conn.setReadTimeout(8000); + conn.setDoOutput(true); + conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); + conn.setRequestProperty("Content-Length", String.valueOf(bytes.length)); + + try (OutputStream os = conn.getOutputStream()) { + os.write(bytes); + } + + int code = conn.getResponseCode(); + if (code >= 400) { + plugin.getLogger().warning("[NetworkInfoModule] Discord Webhook HTTP " + code); + } + } catch (Exception e) { + plugin.getLogger().warning("[NetworkInfoModule] Discord Webhook Fehler: " + e.getMessage()); + } finally { + if (conn != null) { + conn.disconnect(); + } + } + }); + } + + private String escapeJson(String s) { + if (s == null) return ""; + return s.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r"); + } + + private class NetInfoCommand extends Command { + + NetInfoCommand() { + super("netinfo", "statusapi.netinfo"); + } + + @Override + public void execute(CommandSender sender, String[] args) { + if (!enabled) { + sender.sendMessage(ChatColor.RED + "NetworkInfoModule ist deaktiviert."); + return; + } + + Map snapshot = buildSnapshot(); + @SuppressWarnings("unchecked") + Map players = (Map) snapshot.get("players"); + @SuppressWarnings("unchecked") + Map memory = (Map) snapshot.get("memory"); + @SuppressWarnings("unchecked") + Map ping = (Map) players.get("ping"); + + sender.sendMessage(ChatColor.GOLD + "----- StatusAPI NetworkInfo -----"); + sender.sendMessage(ChatColor.YELLOW + "Uptime: " + ChatColor.WHITE + snapshot.get("uptime_human")); + sender.sendMessage(ChatColor.YELLOW + "Spieler: " + ChatColor.WHITE + players.get("online") + "/" + players.get("max") + ChatColor.GRAY + " (Bedrock: " + players.get("bedrock_online") + ")"); + sender.sendMessage(ChatColor.YELLOW + "Ping: " + ChatColor.WHITE + "avg " + ping.get("avg_ms") + "ms, min " + ping.get("min_ms") + "ms, max " + ping.get("max_ms") + "ms"); + sender.sendMessage(ChatColor.YELLOW + "RAM: " + ChatColor.WHITE + memory.get("used_mb") + "MB / " + memory.get("max_mb") + "MB" + ChatColor.GRAY + " (" + memory.get("usage_percent") + "%)"); + sender.sendMessage(ChatColor.YELLOW + "Backends: " + ChatColor.WHITE + ((List) snapshot.get("backend_servers")).size()); + } + } +}