diff --git a/src/main/java/net/viper/status/modules/network/NetworkInfoModule.java b/src/main/java/net/viper/status/modules/network/NetworkInfoModule.java deleted file mode 100644 index d95a8da..0000000 --- a/src/main/java/net/viper/status/modules/network/NetworkInfoModule.java +++ /dev/null @@ -1,862 +0,0 @@ -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.md_5.bungee.api.scheduler.ScheduledTask; -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 alertTpsEnabled = true; - private double alertTpsThreshold = 18.0D; - private boolean attackNotificationsEnabled = true; - private String attackApiKey = ""; - private String attackDefaultSource = "Viper-Network"; - private long lastMemoryAlertAt = 0L; - private long lastPlayerAlertAt = 0L; - private long lastTpsAlertAt = 0L; - private volatile double currentProxyTps = 20.0D; - - /** FIX: Öffentlicher Getter damit ScoreboardModule als TPS-Fallback darauf zugreifen kann */ - public double getProxyTps() { return currentProxyTps; } - private long lastTpsSampleAtMs = 0L; - private ScheduledTask alertTask; - private ScheduledTask tpsSamplerTask; - - @Override - public String getName() { - return "NetworkInfoModule"; - } - - @Override - public void onEnable(Plugin plugin) { - this.plugin = plugin; - this.startedAtMillis = System.currentTimeMillis(); - ensureModuleConfigExists(); - loadConfig(); - - if (!enabled) { - - return; - } - - if (commandEnabled) { - ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new NetInfoCommand()); - } - - tpsSamplerTask = ProxyServer.getInstance().getScheduler().schedule(plugin, this::sampleProxyTps, 1, 1, TimeUnit.SECONDS); - - if (webhookEnabled && !webhookUrl.isEmpty()) { - if (webhookNotifyStartStop) { - boolean delivered = sendLifecycleStartNotification(); - if (!delivered) { - plugin.getLogger().warning("[NetworkInfoModule] Start-Webhook konnte nicht direkt zugestellt werden. Wiederhole in 10 Sekunden."); - ProxyServer.getInstance().getScheduler().schedule(plugin, () -> { - boolean retryDelivered = sendLifecycleStartNotification(); - if (!retryDelivered) { - plugin.getLogger().warning("[NetworkInfoModule] Start-Webhook auch beim zweiten Versuch fehlgeschlagen."); - } - }, 10L, TimeUnit.SECONDS); - } - } - int interval = Math.max(10, webhookCheckSeconds); - alertTask = ProxyServer.getInstance().getScheduler().schedule(plugin, this::evaluateAndSendAlerts, interval, interval, TimeUnit.SECONDS); - } - - - } - - @Override - public void onDisable(Plugin plugin) { - if (alertTask != null) { - alertTask.cancel(); - alertTask = null; - } - if (tpsSamplerTask != null) { - tpsSamplerTask.cancel(); - tpsSamplerTask = null; - } - - if (enabled && webhookEnabled && webhookNotifyStartStop && webhookUrl != null && !webhookUrl.isEmpty()) { - boolean delivered = sendLifecycleStopNotification(); - if (!delivered) { - plugin.getLogger().warning("[NetworkInfoModule] Stop-Webhook konnte nicht zugestellt werden."); - } - } - } - - private boolean sendLifecycleStartNotification() { - if (isCompactEmbedMode()) { - return sendWebhookEmbed( - webhookUrl, - "✅ NetworkInfo gestartet", - "Proxy: **" + ProxyServer.getInstance().getName() + "**\nÜberwachung und Webhook-Alerts sind jetzt aktiv.", - 0x2ECC71, - null, - false - ); - } - - 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); - return sendWebhookEmbed( - webhookUrl, - "✅ NetworkInfo gestartet", - "Überwachung und Webhook-Alerts sind jetzt aktiv.", - 0x2ECC71, - fields.toString(), - false - ); - } - - private boolean sendLifecycleStopNotification() { - if (isCompactEmbedMode()) { - return sendWebhookEmbed( - webhookUrl, - "🛑 NetworkInfo gestoppt", - "Die NetworkInfo-Überwachung wurde gestoppt.\nKeine weiteren Auto-Alerts bis zum nächsten Start.", - 0xE74C3C, - null, - false - ); - } - - StringBuilder fields = new StringBuilder(); - appendEmbedField(fields, "Proxy", ProxyServer.getInstance().getName(), true); - appendEmbedField(fields, "Modus", "Detailed", true); - appendEmbedField(fields, "Status", "Monitoring pausiert", false); - return sendWebhookEmbed( - webhookUrl, - "🛑 NetworkInfo gestoppt", - "Die NetworkInfo-Überwachung wurde gestoppt.", - 0xE74C3C, - fields.toString(), - false - ); - } - - 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(); - - // getPlayerLimit() liefert -1 wenn kein globales Limit gesetzt ist. - // In diesem Fall den Listener-Wert (angezeigte Max-Spielerzahl im Server-Ping) nutzen. - if (maxPlayers <= 0) { - try { - java.util.Iterator listenerIt = - ProxyServer.getInstance().getConfig().getListeners().iterator(); - if (listenerIt.hasNext()) { - int listenerMax = listenerIt.next().getMaxPlayers(); - if (listenerMax > 0) { - maxPlayers = listenerMax; - } - } - } catch (Exception ignored) {} - } - - 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()); - system.put("proxy_tps", roundDouble(currentProxyTps, 2)); - - 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> playerNames = new ArrayList>(); - for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { - Map entry = new LinkedHashMap(); - entry.put("name", p.getName()); - try { entry.put("uuid", p.getUniqueId().toString()); } catch (Exception ignored) {} - try { - if (p.getServer() != null && p.getServer().getInfo() != null) { - entry.put("server", p.getServer().getInfo().getName()); - } - } catch (Exception ignored) {} - playerNames.add(entry); - } - out.put("player_names", playerNames); - } - - 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; - alertTpsEnabled = true; - alertTpsThreshold = 18.0D; - 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); - alertTpsEnabled = Boolean.parseBoolean(props.getProperty("networkinfo.alert.tps_enabled", "true")); - alertTpsThreshold = parseDouble(props.getProperty("networkinfo.alert.tps_threshold", "18.0"), 18.0D); - 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; - alertTpsEnabled = true; - alertTpsThreshold = 18.0D; - 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); - } - - } 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")); - double proxyTps = currentProxyTps; - - 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() - ); - } - } - - if (alertTpsEnabled && proxyTps > 0D && proxyTps < Math.max(1D, alertTpsThreshold) && canSend(lastTpsAlertAt, now)) { - lastTpsAlertAt = now; - String tpsText = String.format(Locale.ROOT, "%.2f", proxyTps); - String thresholdText = String.format(Locale.ROOT, "%.2f", Math.max(1D, alertTpsThreshold)); - - if (isCompactEmbedMode()) { - sendWebhookEmbed( - webhookUrl, - "🟥 Niedrige Proxy-TPS", - "Aktuell: **" + tpsText + " TPS**\nSchwelle: **" + thresholdText + " TPS**", - 0xE74C3C - ); - } else { - StringBuilder fields = new StringBuilder(); - appendEmbedField(fields, "Proxy TPS", tpsText, true); - appendEmbedField(fields, "Schwelle", thresholdText, true); - appendEmbedField(fields, "Check-Intervall", Math.max(10, webhookCheckSeconds) + "s", true); - sendWebhookEmbed( - webhookUrl, - "🟥 Niedrige Proxy-TPS", - "Die gemessene Proxy-TPS liegt unter der konfigurierten Schwelle.", - 0xE74C3C, - fields.toString() - ); - } - } - } - - private void sampleProxyTps() { - long now = System.currentTimeMillis(); - if (lastTpsSampleAtMs <= 0L) { - lastTpsSampleAtMs = now; - currentProxyTps = 20.0D; - return; - } - - long deltaMs = now - lastTpsSampleAtMs; - lastTpsSampleAtMs = now; - if (deltaMs <= 0L) { - return; - } - - // 1s Scheduler-Tick sollte etwa 20 TPS entsprechen. Abweichung zeigt Main-Thread-Lag. - double instantTps = (1000.0D / (double) deltaMs) * 20.0D; - instantTps = Math.max(0.1D, Math.min(20.0D, instantTps)); - currentProxyTps = (currentProxyTps * 0.7D) + (instantTps * 0.3D); - } - - 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 double parseDouble(String s, double fallback) { - try { - return Double.parseDouble(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 double roundDouble(double value, int digits) { - double factor = Math.pow(10D, Math.max(0, digits)); - return Math.round(value * factor) / factor; - } - - private boolean sendWebhookEmbed(String targetWebhookUrl, String title, String description, int color) { - return sendWebhookEmbed(targetWebhookUrl, title, description, color, null, true); - } - - private boolean sendWebhookEmbed(String targetWebhookUrl, String title, String description, int color, String fieldsJson) { - return sendWebhookEmbed(targetWebhookUrl, title, description, color, fieldsJson, true); - } - - private boolean sendWebhookEmbed(String targetWebhookUrl, String title, String description, int color, String fieldsJson, boolean async) { - if (targetWebhookUrl == null || targetWebhookUrl.isEmpty()) { - return false; - } - - 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("}]}"); - return postWebhookPayload(targetWebhookUrl, embed.toString(), async); - } - - 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(), true); - } - - 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 boolean postWebhookPayload(String targetWebhookUrl, String payload, boolean async) { - if (async) { - ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> executeWebhookPost(targetWebhookUrl, payload)); - return true; - } - return executeWebhookPost(targetWebhookUrl, payload); - } - - private boolean executeWebhookPost(String targetWebhookUrl, String payload) { - 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 >= 200 && code < 300) { - return true; - } - - plugin.getLogger().warning("[NetworkInfoModule] Discord Webhook HTTP " + code + ": " + readErrorBody(conn)); - return false; - } catch (Exception e) { - plugin.getLogger().warning("[NetworkInfoModule] Discord Webhook Fehler: " + e.getMessage()); - return false; - } finally { - if (conn != null) { - conn.disconnect(); - } - } - } - - private String readErrorBody(HttpURLConnection conn) { - if (conn == null) { - return ""; - } - try (InputStream errorStream = conn.getErrorStream()) { - if (errorStream == null) { - return ""; - } - byte[] bytes = new byte[1024]; - int read = errorStream.read(bytes); - if (read <= 0) { - return ""; - } - return new String(bytes, 0, read, StandardCharsets.UTF_8).trim(); - } catch (Exception ignored) { - return ""; - } - } - - 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 system = (Map) snapshot.get("system"); - @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 + "Proxy TPS: " + ChatColor.WHITE + system.get("proxy_tps") + ChatColor.GRAY + " (Alert < " + String.format(Locale.ROOT, "%.2f", alertTpsThreshold) + ")"); - sender.sendMessage(ChatColor.YELLOW + "Backends: " + ChatColor.WHITE + ((List) snapshot.get("backend_servers")).size()); - } - } -} \ No newline at end of file