Delete src/main/java/net/viper/status/modules/network/NetworkInfoModule.java via Git Manager GUI

This commit is contained in:
2026-05-24 19:43:36 +00:00
parent 1b04892356
commit af80800a41

View File

@@ -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<String, Object> buildSnapshot() {
Map<String, Object> out = new LinkedHashMap<String, Object>();
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<String, Object> memory = new LinkedHashMap<String, Object>();
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<net.md_5.bungee.api.config.ListenerInfo> listenerIt =
ProxyServer.getInstance().getConfig().getListeners().iterator();
if (listenerIt.hasNext()) {
int listenerMax = listenerIt.next().getMaxPlayers();
if (listenerMax > 0) {
maxPlayers = listenerMax;
}
}
} catch (Exception ignored) {}
}
Map<String, Object> ping = buildPingSummary(ProxyServer.getInstance().getPlayers());
Map<String, Object> players = new LinkedHashMap<String, Object>();
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<Map<String, Object>> backend = buildBackendDistribution();
Map<String, Object> system = new LinkedHashMap<String, Object>();
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<Map<String, Object>> playerNames = new ArrayList<Map<String, Object>>();
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
Map<String, Object> entry = new LinkedHashMap<String, Object>();
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<String, Object> buildPingSummary(Collection<ProxiedPlayer> players) {
Map<String, Object> ping = new LinkedHashMap<String, Object>();
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<Map<String, Object>> buildBackendDistribution() {
List<Map<String, Object>> list = new ArrayList<Map<String, Object>>();
for (Map.Entry<String, ServerInfo> entry : ProxyServer.getInstance().getServers().entrySet()) {
ServerInfo info = entry.getValue();
Map<String, Object> row = new LinkedHashMap<String, Object>();
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<String, Object> buildFeatureSummary() {
Map<String, Object> features = new LinkedHashMap<String, Object>();
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<String, Object> snapshot = buildSnapshot();
@SuppressWarnings("unchecked")
Map<String, Object> memory = (Map<String, Object>) snapshot.get("memory");
@SuppressWarnings("unchecked")
Map<String, Object> players = (Map<String, Object>) 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<String, Object> snapshot = buildSnapshot();
@SuppressWarnings("unchecked")
Map<String, Object> players = (Map<String, Object>) snapshot.get("players");
@SuppressWarnings("unchecked")
Map<String, Object> memory = (Map<String, Object>) snapshot.get("memory");
@SuppressWarnings("unchecked")
Map<String, Object> system = (Map<String, Object>) snapshot.get("system");
@SuppressWarnings("unchecked")
Map<String, Object> ping = (Map<String, Object>) 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());
}
}
}