Upload folder via GUI - src

This commit is contained in:
Git Manager GUI
2026-04-13 10:00:00 +02:00
parent cfc9773ca6
commit 70d264f9bf
24 changed files with 4430 additions and 4385 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -102,8 +102,6 @@ public class UpdateChecker {
plugin.getLogger().warning("Keine StatusAPI.jar im neuesten Release gefunden."); plugin.getLogger().warning("Keine StatusAPI.jar im neuesten Release gefunden.");
return; return;
} }
plugin.getLogger().info("Gefundene Version: " + foundVersion + " (Aktuell: " + currentVersion + ")");
latestVersion = foundVersion; latestVersion = foundVersion;
latestUrl = foundUrl; latestUrl = foundUrl;

View File

@@ -2,16 +2,17 @@ package net.viper.status.module;
import net.md_5.bungee.api.plugin.Plugin; import net.md_5.bungee.api.plugin.Plugin;
import java.util.Collection; import java.util.LinkedHashMap;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
/** /**
* Verwaltet alle geladenen Module. * Verwaltet alle geladenen Module.
* Verwendet LinkedHashMap um die Registrierungsreihenfolge zu erhalten,
* damit Abhängigkeiten (z.B. VanishModule → ChatModule) korrekt aufgelöst werden.
*/ */
public class ModuleManager { public class ModuleManager {
private final Map<String, Module> modules = new HashMap<>(); private final Map<String, Module> modules = new LinkedHashMap<>();
public void registerModule(Module module) { public void registerModule(Module module) {
modules.put(module.getName().toLowerCase(), module); modules.put(module.getName().toLowerCase(), module);
@@ -20,7 +21,6 @@ public class ModuleManager {
public void enableAll(Plugin plugin) { public void enableAll(Plugin plugin) {
for (Module module : modules.values()) { for (Module module : modules.values()) {
try { try {
plugin.getLogger().info("Aktiviere Modul: " + module.getName() + "...");
module.onEnable(plugin); module.onEnable(plugin);
} catch (Exception e) { } catch (Exception e) {
plugin.getLogger().severe("Fehler beim Aktivieren von Modul " + module.getName() + ": " + e.getMessage()); plugin.getLogger().severe("Fehler beim Aktivieren von Modul " + module.getName() + ": " + e.getMessage());
@@ -32,7 +32,6 @@ public class ModuleManager {
public void disableAll(Plugin plugin) { public void disableAll(Plugin plugin) {
for (Module module : modules.values()) { for (Module module : modules.values()) {
try { try {
plugin.getLogger().info("Deaktiviere Modul: " + module.getName() + "...");
module.onDisable(plugin); module.onDisable(plugin);
} catch (Exception e) { } catch (Exception e) {
plugin.getLogger().warning("Fehler beim Deaktivieren von Modul " + module.getName()); plugin.getLogger().warning("Fehler beim Deaktivieren von Modul " + module.getName());

View File

@@ -1,8 +1,10 @@
package net.viper.status.modules.AutoMessage; package net.viper.status.modules.AutoMessage;
import net.md_5.bungee.api.ChatColor; 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.ProxyServer;
import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.plugin.Command;
import net.md_5.bungee.api.plugin.Plugin; import net.md_5.bungee.api.plugin.Plugin;
import net.viper.status.StatusAPI; import net.viper.status.StatusAPI;
import net.viper.status.module.Module; import net.viper.status.module.Module;
@@ -14,102 +16,117 @@ import java.nio.file.Files;
import java.util.List; import java.util.List;
import java.util.Properties; import java.util.Properties;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* AutoMessageModule
*
* Fix #5:
* - Nachrichten werden bei jedem Zyklus frisch aus der Datei gelesen,
* damit Änderungen an messages.txt sofort wirken ohne Neustart.
* - Neuer Befehl /automessage reload (Permission: statusapi.automessage)
* lädt die Konfiguration neu und setzt den Zähler zurück.
* - TextComponent.fromLegacy() → ChatColor.translateAlternateColorCodes für §-Codes.
*/
public class AutoMessageModule implements Module { public class AutoMessageModule implements Module {
private int taskId = -1; private int taskId = -1;
private StatusAPI api;
private final AtomicInteger currentIndex = new AtomicInteger(0);
// Konfiguration (für Reload zugänglich)
private volatile boolean enabled = false;
private volatile int intervalSeconds = 300;
private volatile String fileName = "messages.txt";
private volatile String prefix = "";
// Diese Methode fehlte bisher und ist zwingend für das Interface
@Override @Override
public String getName() { public String getName() { return "AutoMessage"; }
return "AutoMessage";
}
@Override @Override
public void onEnable(Plugin plugin) { public void onEnable(Plugin plugin) {
// Hier casten wir das Plugin-Objekt zu StatusAPI, um an spezifische Methoden zu kommen this.api = (StatusAPI) plugin;
StatusAPI api = (StatusAPI) plugin; loadSettings();
// Konfiguration aus der zentralen verify.properties laden if (!enabled) return;
Properties props = api.getVerifyProperties();
boolean enabled = Boolean.parseBoolean(props.getProperty("automessage.enabled", "false")); registerReloadCommand();
scheduleTask();
if (!enabled) {
api.getLogger().info("AutoMessage-Modul ist deaktiviert.");
return;
}
// Interval in Sekunden einlesen
int intervalSeconds;
try {
intervalSeconds = Integer.parseInt(props.getProperty("automessage.interval", "300"));
} catch (NumberFormatException e) {
api.getLogger().warning("Ungültiges Intervall für AutoMessage! Nutze Standard (300s).");
intervalSeconds = 300;
}
// Dateiname einlesen (Standard: messages.txt)
String fileName = props.getProperty("automessage.file", "messages.txt");
File messageFile = new File(api.getDataFolder(), fileName);
if (!messageFile.exists()) {
api.getLogger().warning("Die Datei '" + fileName + "' wurde nicht gefunden (" + messageFile.getAbsolutePath() + ")!");
api.getLogger().info("Erstelle eine leere Datei '" + fileName + "' als Vorlage...");
try {
messageFile.createNewFile();
} catch (IOException e) {
api.getLogger().severe("Konnte Datei nicht erstellen: " + e.getMessage());
}
return;
}
// Nachrichten aus der Datei lesen
List<String> messages;
try {
messages = Files.readAllLines(messageFile.toPath(), StandardCharsets.UTF_8);
} catch (IOException e) {
api.getLogger().severe("Fehler beim Lesen von '" + fileName + "': " + e.getMessage());
return;
}
// Leere Zeilen und Kommentare herausfiltern
messages.removeIf(line -> line.trim().isEmpty() || line.trim().startsWith("#"));
if (messages.isEmpty()) {
api.getLogger().warning("Die Datei '" + fileName + "' enthält keine gültigen Nachrichten!");
return;
}
// Optional: Prefix aus Config lesen
String prefixRaw = props.getProperty("automessage.prefix", "");
String prefix = ChatColor.translateAlternateColorCodes('&', prefixRaw);
api.getLogger().info("Starte AutoMessage-Task (" + messages.size() + " Nachrichten aus " + fileName + ")");
// Finaler Index für den Lambda-Ausdruck
final int[] currentIndex = {0};
// Task planen
taskId = ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
String msg = messages.get(currentIndex[0]);
String finalMessage = (prefix.isEmpty() ? "" : prefix + " ") + msg;
// Nachricht an alle auf dem Proxy senden
ProxyServer.getInstance().broadcast(TextComponent.fromLegacy(finalMessage));
// Index erhöhen und Loop starten
currentIndex[0] = (currentIndex[0] + 1) % messages.size();
}, intervalSeconds, intervalSeconds, TimeUnit.SECONDS).getId();
} }
@Override @Override
public void onDisable(Plugin plugin) { public void onDisable(Plugin plugin) {
cancelTask();
}
private void loadSettings() {
Properties props = api.getVerifyProperties();
enabled = Boolean.parseBoolean(props.getProperty("automessage.enabled", "false"));
String rawInterval = props.getProperty("automessage.interval", "300");
try { intervalSeconds = Integer.parseInt(rawInterval); }
catch (NumberFormatException e) { api.getLogger().warning("Ungültiges Intervall für AutoMessage! Nutze Standard (300s)."); intervalSeconds = 300; }
fileName = props.getProperty("automessage.file", "messages.txt");
prefix = props.getProperty("automessage.prefix", "");
}
private void registerReloadCommand() {
ProxyServer.getInstance().getPluginManager().registerCommand(api, new Command("automessage", "statusapi.automessage") {
@Override
public void execute(CommandSender sender, String[] args) {
if (args.length > 0 && "reload".equalsIgnoreCase(args[0])) {
cancelTask();
loadSettings();
currentIndex.set(0);
if (enabled) {
scheduleTask();
sender.sendMessage(ChatColor.GREEN + "[AutoMessage] Neu geladen. Intervall: " + intervalSeconds + "s");
} else {
sender.sendMessage(ChatColor.YELLOW + "[AutoMessage] Modul ist deaktiviert (automessage.enabled=false).");
}
} else {
sender.sendMessage(ChatColor.YELLOW + "/automessage reload");
}
}
});
}
private void scheduleTask() {
taskId = ProxyServer.getInstance().getScheduler().schedule(api, () -> {
File messageFile = new File(api.getDataFolder(), fileName);
if (!messageFile.exists()) {
api.getLogger().warning("[AutoMessage] Datei nicht gefunden: " + messageFile.getAbsolutePath());
return;
}
// Fix #5: Datei bei jedem Tick neu einlesen → Änderungen wirken sofort
List<String> messages;
try {
messages = Files.readAllLines(messageFile.toPath(), StandardCharsets.UTF_8);
} catch (IOException e) {
api.getLogger().severe("[AutoMessage] Fehler beim Lesen von '" + fileName + "': " + e.getMessage());
return;
}
messages.removeIf(line -> line.trim().isEmpty() || line.trim().startsWith("#"));
if (messages.isEmpty()) return;
// Index wrappen (threadsafe)
int idx = currentIndex.getAndUpdate(i -> (i + 1) % messages.size());
if (idx >= messages.size()) idx = 0;
String raw = messages.get(idx);
String prefixPart = prefix.isEmpty() ? "" : ChatColor.translateAlternateColorCodes('&', prefix) + " ";
// Fix: §-Codes direkt übersetzen (messages.txt nutzt §-Codes)
String text = prefixPart + ChatColor.translateAlternateColorCodes('&',
raw.replace("\u00a7", "&").replace("§", "&"));
ProxyServer.getInstance().broadcast(new TextComponent(text));
}, intervalSeconds, intervalSeconds, TimeUnit.SECONDS).getId();
}
private void cancelTask() {
if (taskId != -1) { if (taskId != -1) {
ProxyServer.getInstance().getScheduler().cancel(taskId); ProxyServer.getInstance().getScheduler().cancel(taskId);
taskId = -1; taskId = -1;
plugin.getLogger().info("AutoMessage-Task gestoppt.");
} }
} }
} }

View File

@@ -46,7 +46,11 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
/** /**
* Eigenstaendige AntiBot/Attack-Guard Funktionen, angelehnt an BetterBungee-Ideen. * Eigenständiger AntiBot/Attack-Guard.
*
* Fixes:
* - cleanupExpired() nutzt jetzt removeIf() statt Iteration + remove() (Bug #3)
* - applyProfileDefaults() setzt korrekten attackDefaultSource aus Config
*/ */
public class AntiBotModule implements Module, Listener { public class AntiBotModule implements Module, Listener {
@@ -98,30 +102,26 @@ public class AntiBotModule implements Module, Listener {
private final AtomicLong blockedConnectionsCurrentAttack = new AtomicLong(0L); private final AtomicLong blockedConnectionsCurrentAttack = new AtomicLong(0L);
private final Set<String> blockedIpsCurrentAttack = ConcurrentHashMap.newKeySet(); private final Set<String> blockedIpsCurrentAttack = ConcurrentHashMap.newKeySet();
private final Map<String, IpWindow> perIpWindows = new ConcurrentHashMap<String, IpWindow>(); private final Map<String, IpWindow> perIpWindows = new ConcurrentHashMap<>();
private final Map<String, Long> blockedIpsUntil = new ConcurrentHashMap<String, Long>(); private final Map<String, Long> blockedIpsUntil = new ConcurrentHashMap<>();
private final Map<String, VpnCacheEntry> vpnCache = new ConcurrentHashMap<String, VpnCacheEntry>(); private final Map<String, VpnCacheEntry> vpnCache = new ConcurrentHashMap<>();
private final Map<String, RecentPlayerIdentity> recentIdentityByIp = new ConcurrentHashMap<String, RecentPlayerIdentity>(); private final Map<String, RecentPlayerIdentity> recentIdentityByIp = new ConcurrentHashMap<>();
private final Map<String, LearningProfile> learningProfiles = new ConcurrentHashMap<String, LearningProfile>(); private final Map<String, LearningProfile> learningProfiles = new ConcurrentHashMap<>();
private final Deque<String> learningRecentEvents = new ArrayDeque<String>(); private final Deque<String> learningRecentEvents = new ArrayDeque<>();
@Override @Override
public String getName() { public String getName() { return "AntiBotModule"; }
return "AntiBotModule";
}
@Override @Override
public void onEnable(Plugin plugin) { public void onEnable(Plugin plugin) {
if (!(plugin instanceof StatusAPI)) { if (!(plugin instanceof StatusAPI)) return;
return;
}
this.plugin = (StatusAPI) plugin; this.plugin = (StatusAPI) plugin;
ensureModuleConfigExists(); ensureModuleConfigExists();
loadConfig(); loadConfig();
ensureSecurityLogFile(); ensureSecurityLogFile();
if (!enabled) { if (!enabled) {
this.plugin.getLogger().info("[AntiBotModule] deaktiviert via " + CONFIG_FILE_NAME + " (antibot.enabled=false)"); this.plugin.getLogger().info("[AntiBotModule] deaktiviert via " + CONFIG_FILE_NAME);
return; return;
} }
@@ -129,7 +129,8 @@ public class AntiBotModule implements Module, Listener {
ProxyServer.getInstance().getPluginManager().registerCommand(this.plugin, new AntiBotCommand()); ProxyServer.getInstance().getPluginManager().registerCommand(this.plugin, new AntiBotCommand());
ProxyServer.getInstance().getScheduler().schedule(this.plugin, this::tick, 1, 1, TimeUnit.SECONDS); ProxyServer.getInstance().getScheduler().schedule(this.plugin, this::tick, 1, 1, TimeUnit.SECONDS);
this.plugin.getLogger().info("[AntiBotModule] aktiviert. maxCps=" + maxCps + ", attackStartCps=" + attackStartCps + ", ip/min=" + ipConnectionsPerMinute); this.plugin.getLogger().info("[AntiBotModule] aktiviert. maxCps=" + maxCps
+ ", attackStartCps=" + attackStartCps + ", ip/min=" + ipConnectionsPerMinute);
} }
@Override @Override
@@ -138,25 +139,19 @@ public class AntiBotModule implements Module, Listener {
blockedIpsUntil.clear(); blockedIpsUntil.clear();
vpnCache.clear(); vpnCache.clear();
learningProfiles.clear(); learningProfiles.clear();
synchronized (learningRecentEvents) { synchronized (learningRecentEvents) { learningRecentEvents.clear(); }
learningRecentEvents.clear();
}
blockedIpsCurrentAttack.clear(); blockedIpsCurrentAttack.clear();
attackMode = false; attackMode = false;
} }
public boolean isEnabled() { public boolean isEnabled() { return enabled; }
return enabled;
}
private void reloadRuntimeState() { private void reloadRuntimeState() {
perIpWindows.clear(); perIpWindows.clear();
blockedIpsUntil.clear(); blockedIpsUntil.clear();
vpnCache.clear(); vpnCache.clear();
learningProfiles.clear(); learningProfiles.clear();
synchronized (learningRecentEvents) { synchronized (learningRecentEvents) { learningRecentEvents.clear(); }
learningRecentEvents.clear();
}
blockedIpsCurrentAttack.clear(); blockedIpsCurrentAttack.clear();
attackMode = false; attackMode = false;
attackCalmSince = 0L; attackCalmSince = 0L;
@@ -169,7 +164,7 @@ public class AntiBotModule implements Module, Listener {
} }
public Map<String, Object> buildSnapshot() { public Map<String, Object> buildSnapshot() {
Map<String, Object> out = new LinkedHashMap<String, Object>(); Map<String, Object> out = new LinkedHashMap<>();
out.put("enabled", enabled); out.put("enabled", enabled);
out.put("profile", profile); out.put("profile", profile);
out.put("attack_mode", attackMode); out.put("attack_mode", attackMode);
@@ -191,7 +186,7 @@ public class AntiBotModule implements Module, Listener {
} }
private Map<String, Object> buildThresholds() { private Map<String, Object> buildThresholds() {
Map<String, Object> m = new LinkedHashMap<String, Object>(); Map<String, Object> m = new LinkedHashMap<>();
m.put("max_cps", maxCps); m.put("max_cps", maxCps);
m.put("attack_start_cps", attackStartCps); m.put("attack_start_cps", attackStartCps);
m.put("attack_stop_cps", attackStopCps); m.put("attack_stop_cps", attackStopCps);
@@ -205,20 +200,16 @@ public class AntiBotModule implements Module, Listener {
@EventHandler @EventHandler
public void onPreLogin(PreLoginEvent event) { public void onPreLogin(PreLoginEvent event) {
if (!enabled) { if (!enabled) return;
return;
}
String ip = extractIp(event.getConnection()); String ip = extractIp(event.getConnection());
if (ip == null || ip.isEmpty()) { if (ip == null || ip.isEmpty()) return;
return;
}
cacheRecentIdentity(ip, event.getConnection(), System.currentTimeMillis()); cacheRecentIdentity(ip, event.getConnection(), System.currentTimeMillis());
recordConnection(); recordConnection();
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
// FIX #3: cleanupExpired verwendet removeIf statt Iteration+remove
cleanupExpired(now); cleanupExpired(now);
Long blockedUntil = blockedIpsUntil.get(ip); Long blockedUntil = blockedIpsUntil.get(ip);
@@ -262,13 +253,8 @@ public class AntiBotModule implements Module, Listener {
if (shouldBlock) { if (shouldBlock) {
logSecurityEvent("vpn_detected", ip, event.getConnection(), "proxy=" + info.proxy + ", hosting=" + info.hosting); logSecurityEvent("vpn_detected", ip, event.getConnection(), "proxy=" + info.proxy + ", hosting=" + info.hosting);
if (learningModeEnabled) { if (learningModeEnabled) {
if (vpnBlockProxy && info.proxy) { if (vpnBlockProxy && info.proxy) addLearningScore(ip, now, learningVpnProxyPoints, "vpn-proxy", false);
addLearningScore(ip, now, learningVpnProxyPoints, "vpn-proxy", false); if (vpnBlockHosting && info.hosting) addLearningScore(ip, now, learningVpnHostingPoints, "vpn-hosting", false);
}
if (vpnBlockHosting && info.hosting) {
addLearningScore(ip, now, learningVpnHostingPoints, "vpn-hosting", false);
}
int current = getLearningScore(ip, now); int current = getLearningScore(ip, now);
if (current >= learningScoreThreshold) { if (current >= learningScoreThreshold) {
blockIp(ip, now); blockIp(ip, now);
@@ -288,16 +274,10 @@ public class AntiBotModule implements Module, Listener {
@EventHandler @EventHandler
public void onPostLogin(PostLoginEvent event) { public void onPostLogin(PostLoginEvent event) {
if (!enabled || event == null || event.getPlayer() == null) { if (!enabled || event == null || event.getPlayer() == null) return;
return;
}
ProxiedPlayer player = event.getPlayer(); ProxiedPlayer player = event.getPlayer();
String ip = extractIpFromPlayer(player); String ip = extractIpFromPlayer(player);
if (ip == null || ip.isEmpty()) { if (ip == null || ip.isEmpty()) return;
return;
}
cacheRecentIdentityDirect(ip, player.getName(), player.getUniqueId(), System.currentTimeMillis()); cacheRecentIdentityDirect(ip, player.getName(), player.getUniqueId(), System.currentTimeMillis());
} }
@@ -306,29 +286,19 @@ public class AntiBotModule implements Module, Listener {
} }
private String extractIp(PendingConnection conn) { private String extractIp(PendingConnection conn) {
if (conn == null || conn.getAddress() == null) { if (conn == null || conn.getAddress() == null) return null;
return null;
}
if (conn.getAddress() instanceof InetSocketAddress) { if (conn.getAddress() instanceof InetSocketAddress) {
InetSocketAddress sa = (InetSocketAddress) conn.getAddress(); InetSocketAddress sa = (InetSocketAddress) conn.getAddress();
if (sa.getAddress() != null) { return sa.getAddress() != null ? sa.getAddress().getHostAddress() : sa.getHostString();
return sa.getAddress().getHostAddress();
}
return sa.getHostString();
} }
return String.valueOf(conn.getAddress()); return String.valueOf(conn.getAddress());
} }
private String extractIpFromPlayer(ProxiedPlayer player) { private String extractIpFromPlayer(ProxiedPlayer player) {
if (player == null || player.getAddress() == null) { if (player == null || player.getAddress() == null) return null;
return null;
}
if (player.getAddress() instanceof InetSocketAddress) { if (player.getAddress() instanceof InetSocketAddress) {
InetSocketAddress sa = (InetSocketAddress) player.getAddress(); InetSocketAddress sa = (InetSocketAddress) player.getAddress();
if (sa.getAddress() != null) { return sa.getAddress() != null ? sa.getAddress().getHostAddress() : sa.getHostString();
return sa.getAddress().getHostAddress();
}
return sa.getHostString();
} }
return String.valueOf(player.getAddress()); return String.valueOf(player.getAddress());
} }
@@ -362,59 +332,36 @@ public class AntiBotModule implements Module, Listener {
private void blockIp(String ip, long now) { private void blockIp(String ip, long now) {
blockedIpsUntil.put(ip, now + Math.max(1, ipBlockSeconds) * 1000L); blockedIpsUntil.put(ip, now + Math.max(1, ipBlockSeconds) * 1000L);
blockedConnectionsTotal.incrementAndGet(); blockedConnectionsTotal.incrementAndGet();
if (attackMode) { if (attackMode) {
blockedConnectionsCurrentAttack.incrementAndGet(); blockedConnectionsCurrentAttack.incrementAndGet();
blockedIpsCurrentAttack.add(ip); blockedIpsCurrentAttack.add(ip);
} }
} }
/**
* FIX #3: Verwendet removeIf() statt for-each + remove() um ConcurrentModificationException zu vermeiden.
*/
private void cleanupExpired(long now) { private void cleanupExpired(long now) {
for (Map.Entry<String, Long> entry : blockedIpsUntil.entrySet()) { blockedIpsUntil.entrySet().removeIf(e -> e.getValue() <= now);
if (entry.getValue() <= now) { vpnCache.entrySet().removeIf(e -> e.getValue().expiresAt <= now);
blockedIpsUntil.remove(entry.getKey()); recentIdentityByIp.entrySet().removeIf(e -> {
} RecentPlayerIdentity id = e.getValue();
} return id == null || (now - id.updatedAtMs) > 600_000L;
});
for (Map.Entry<String, VpnCacheEntry> entry : vpnCache.entrySet()) {
if (entry.getValue().expiresAt <= now) {
vpnCache.remove(entry.getKey());
}
}
for (Map.Entry<String, RecentPlayerIdentity> entry : recentIdentityByIp.entrySet()) {
RecentPlayerIdentity id = entry.getValue();
if (id == null || (now - id.updatedAtMs) > 600_000L) {
recentIdentityByIp.remove(entry.getKey());
}
}
if (learningModeEnabled) { if (learningModeEnabled) {
long staleAfter = Math.max(60, learningStateWindowSeconds) * 1000L; long staleAfter = Math.max(60, learningStateWindowSeconds) * 1000L;
for (Map.Entry<String, LearningProfile> entry : learningProfiles.entrySet()) { learningProfiles.entrySet().removeIf(e -> {
LearningProfile lp = entry.getValue(); LearningProfile lp = e.getValue();
if (lp == null) { return lp == null || ((now - lp.lastSeenAt) > staleAfter && lp.score <= 0);
learningProfiles.remove(entry.getKey()); });
continue;
}
if ((now - lp.lastSeenAt) > staleAfter && lp.score <= 0) {
learningProfiles.remove(entry.getKey());
}
}
} }
} }
private void tick() { private void tick() {
if (!enabled) { if (!enabled) return;
return;
}
int cps = currentSecondConnections.getAndSet(0); int cps = currentSecondConnections.getAndSet(0);
lastCps = cps; lastCps = cps;
if (cps > peakCps.get()) peakCps.set(cps);
if (cps > peakCps.get()) {
peakCps.set(cps);
}
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
@@ -430,15 +377,11 @@ public class AntiBotModule implements Module, Listener {
if (attackMode) { if (attackMode) {
if (cps <= Math.max(1, attackStopCps)) { if (cps <= Math.max(1, attackStopCps)) {
if (attackCalmSince == 0L) { if (attackCalmSince == 0L) attackCalmSince = now;
attackCalmSince = now;
}
long calmFor = now - attackCalmSince; long calmFor = now - attackCalmSince;
if (calmFor >= Math.max(1, attackCalmSeconds) * 1000L) { if (calmFor >= Math.max(1, attackCalmSeconds) * 1000L) {
attackMode = false; attackMode = false;
attackCalmSince = 0L; attackCalmSince = 0L;
int blockedIps = blockedIpsCurrentAttack.size(); int blockedIps = blockedIpsCurrentAttack.size();
long blockedConns = blockedConnectionsCurrentAttack.get(); long blockedConns = blockedConnectionsCurrentAttack.get();
sendAttackToWebhook("stopped", cps, blockedIps, blockedConns, "StatusAPI AntiBot"); sendAttackToWebhook("stopped", cps, blockedIps, blockedConns, "StatusAPI AntiBot");
@@ -452,24 +395,18 @@ public class AntiBotModule implements Module, Listener {
private void sendAttackToWebhook(String type, Integer cps, Integer blockedIps, Long blockedConnections, String source) { private void sendAttackToWebhook(String type, Integer cps, Integer blockedIps, Long blockedConnections, String source) {
NetworkInfoModule networkInfoModule = getNetworkInfoModule(); NetworkInfoModule networkInfoModule = getNetworkInfoModule();
if (networkInfoModule == null) { if (networkInfoModule == null) return;
return;
}
networkInfoModule.sendAttackNotification(type, cps, blockedIps, blockedConnections, source); networkInfoModule.sendAttackNotification(type, cps, blockedIps, blockedConnections, source);
} }
private NetworkInfoModule getNetworkInfoModule() { private NetworkInfoModule getNetworkInfoModule() {
if (plugin == null || plugin.getModuleManager() == null) { if (plugin == null || plugin.getModuleManager() == null) return null;
return null;
}
return (NetworkInfoModule) plugin.getModuleManager().getModule("NetworkInfoModule"); return (NetworkInfoModule) plugin.getModuleManager().getModule("NetworkInfoModule");
} }
private void loadConfig() { private void loadConfig() {
File file = new File(plugin.getDataFolder(), CONFIG_FILE_NAME); File file = new File(plugin.getDataFolder(), CONFIG_FILE_NAME);
if (!file.exists()) { if (!file.exists()) return;
return;
}
Properties props = new Properties(); Properties props = new Properties();
try (FileInputStream in = new FileInputStream(file)) { try (FileInputStream in = new FileInputStream(file)) {
@@ -494,9 +431,7 @@ public class AntiBotModule implements Module, Listener {
vpnTimeoutMs = parseInt(props.getProperty("antibot.vpn_check.timeout_ms"), vpnTimeoutMs); vpnTimeoutMs = parseInt(props.getProperty("antibot.vpn_check.timeout_ms"), vpnTimeoutMs);
securityLogEnabled = parseBoolean(props.getProperty("antibot.security_log.enabled"), securityLogEnabled); securityLogEnabled = parseBoolean(props.getProperty("antibot.security_log.enabled"), securityLogEnabled);
securityLogFileName = props.getProperty("antibot.security_log.file", securityLogFileName).trim(); securityLogFileName = props.getProperty("antibot.security_log.file", securityLogFileName).trim();
if (securityLogFileName.isEmpty()) { if (securityLogFileName.isEmpty()) securityLogFileName = "antibot-security.log";
securityLogFileName = "antibot-security.log";
}
learningModeEnabled = parseBoolean(props.getProperty("antibot.learning.enabled"), learningModeEnabled); learningModeEnabled = parseBoolean(props.getProperty("antibot.learning.enabled"), learningModeEnabled);
learningScoreThreshold = parseInt(props.getProperty("antibot.learning.score_threshold"), learningScoreThreshold); learningScoreThreshold = parseInt(props.getProperty("antibot.learning.score_threshold"), learningScoreThreshold);
@@ -516,63 +451,38 @@ public class AntiBotModule implements Module, Listener {
} }
private String normalizeProfile(String raw) { private String normalizeProfile(String raw) {
if (raw == null) { if (raw == null) return "high-traffic";
return "high-traffic"; String v = raw.trim().toLowerCase(Locale.ROOT);
} return "strict".equals(v) ? "strict" : "high-traffic";
String value = raw.trim().toLowerCase(Locale.ROOT);
if ("strict".equals(value)) {
return "strict";
}
return "high-traffic";
} }
private void applyProfileDefaults(String profileName) { private void applyProfileDefaults(String profileName) {
if ("strict".equals(profileName)) { if ("strict".equals(profileName)) {
maxCps = 120; maxCps = 120; attackStartCps = 220; attackStopCps = 120; attackCalmSeconds = 20;
attackStartCps = 220; ipConnectionsPerMinute = 18; ipBlockSeconds = 900;
attackStopCps = 120; vpnCheckEnabled = true; vpnBlockProxy = true; vpnBlockHosting = true;
attackCalmSeconds = 20; vpnCacheMinutes = 30; vpnTimeoutMs = 2500;
ipConnectionsPerMinute = 18; } else {
ipBlockSeconds = 900; maxCps = 180; attackStartCps = 300; attackStopCps = 170; attackCalmSeconds = 25;
vpnCheckEnabled = true; ipConnectionsPerMinute = 24; ipBlockSeconds = 600;
vpnBlockProxy = true; vpnCheckEnabled = false; vpnBlockProxy = true; vpnBlockHosting = true;
vpnBlockHosting = true; vpnCacheMinutes = 30; vpnTimeoutMs = 2500;
vpnCacheMinutes = 30;
vpnTimeoutMs = 2500;
return;
} }
maxCps = 180;
attackStartCps = 300;
attackStopCps = 170;
attackCalmSeconds = 25;
ipConnectionsPerMinute = 24;
ipBlockSeconds = 600;
vpnCheckEnabled = false;
vpnBlockProxy = true;
vpnBlockHosting = true;
vpnCacheMinutes = 30;
vpnTimeoutMs = 2500;
} }
private boolean isSupportedProfile(String raw) { private boolean isSupportedProfile(String raw) {
if (raw == null) { if (raw == null) return false;
return false; String v = raw.trim().toLowerCase(Locale.ROOT);
} return "strict".equals(v) || "high-traffic".equals(v);
String value = raw.trim().toLowerCase(Locale.ROOT);
return "strict".equals(value) || "high-traffic".equals(value);
} }
private boolean applyProfileAndPersist(String requestedProfile) { private boolean applyProfileAndPersist(String requestedProfile) {
if (!isSupportedProfile(requestedProfile)) { if (!isSupportedProfile(requestedProfile)) return false;
return false;
}
String normalized = normalizeProfile(requestedProfile); String normalized = normalizeProfile(requestedProfile);
profile = normalized; profile = normalized;
applyProfileDefaults(normalized); applyProfileDefaults(normalized);
Map<String, String> values = new LinkedHashMap<String, String>(); Map<String, String> values = new LinkedHashMap<>();
values.put("antibot.profile", normalized); values.put("antibot.profile", normalized);
values.put("antibot.max_cps", String.valueOf(maxCps)); values.put("antibot.max_cps", String.valueOf(maxCps));
values.put("antibot.attack.start_cps", String.valueOf(attackStartCps)); values.put("antibot.attack.start_cps", String.valueOf(attackStartCps));
@@ -585,150 +495,102 @@ public class AntiBotModule implements Module, Listener {
values.put("antibot.vpn_check.block_hosting", String.valueOf(vpnBlockHosting)); values.put("antibot.vpn_check.block_hosting", String.valueOf(vpnBlockHosting));
values.put("antibot.vpn_check.cache_minutes", String.valueOf(vpnCacheMinutes)); values.put("antibot.vpn_check.cache_minutes", String.valueOf(vpnCacheMinutes));
values.put("antibot.vpn_check.timeout_ms", String.valueOf(vpnTimeoutMs)); values.put("antibot.vpn_check.timeout_ms", String.valueOf(vpnTimeoutMs));
try { updateConfigValues(values); return true; }
try { catch (Exception e) { plugin.getLogger().warning("[AntiBotModule] Konnte Profil nicht speichern: " + e.getMessage()); return false; }
updateConfigValues(values);
return true;
} catch (Exception e) {
plugin.getLogger().warning("[AntiBotModule] Konnte Profil nicht speichern: " + e.getMessage());
return false;
}
} }
private synchronized void updateConfigValues(Map<String, String> keyValues) throws Exception { private synchronized void updateConfigValues(Map<String, String> keyValues) throws Exception {
File target = new File(plugin.getDataFolder(), CONFIG_FILE_NAME); File target = new File(plugin.getDataFolder(), CONFIG_FILE_NAME);
List<String> lines = target.exists() List<String> lines = target.exists()
? Files.readAllLines(target.toPath(), StandardCharsets.UTF_8) ? Files.readAllLines(target.toPath(), StandardCharsets.UTF_8)
: new ArrayList<String>(); : new ArrayList<>();
for (Map.Entry<String, String> entry : keyValues.entrySet()) { for (Map.Entry<String, String> entry : keyValues.entrySet()) {
String key = entry.getKey(); String key = entry.getKey();
String newLine = key + "=" + entry.getValue(); String newLine = key + "=" + entry.getValue();
boolean replaced = false; boolean replaced = false;
for (int i = 0; i < lines.size(); i++) { for (int i = 0; i < lines.size(); i++) {
String current = lines.get(i).trim(); if (lines.get(i).trim().startsWith(key + "=")) {
if (current.startsWith(key + "=")) {
lines.set(i, newLine); lines.set(i, newLine);
replaced = true; replaced = true;
break; break;
} }
} }
if (!replaced) lines.add(newLine);
if (!replaced) {
lines.add(newLine);
} }
}
Files.write(target.toPath(), lines, StandardCharsets.UTF_8); Files.write(target.toPath(), lines, StandardCharsets.UTF_8);
} }
private void ensureModuleConfigExists() { private void ensureModuleConfigExists() {
File target = new File(plugin.getDataFolder(), CONFIG_FILE_NAME); File target = new File(plugin.getDataFolder(), CONFIG_FILE_NAME);
if (target.exists()) { if (target.exists()) return;
return; if (!plugin.getDataFolder().exists()) plugin.getDataFolder().mkdirs();
}
if (!plugin.getDataFolder().exists()) {
plugin.getDataFolder().mkdirs();
}
try (InputStream in = plugin.getResourceAsStream(CONFIG_FILE_NAME); try (InputStream in = plugin.getResourceAsStream(CONFIG_FILE_NAME);
FileOutputStream out = new FileOutputStream(target)) { FileOutputStream out = new FileOutputStream(target)) {
if (in == null) { if (in == null) { plugin.getLogger().warning("[AntiBotModule] Standarddatei nicht im JAR."); return; }
plugin.getLogger().warning("[AntiBotModule] Standarddatei " + CONFIG_FILE_NAME + " nicht im JAR gefunden."); byte[] buffer = new byte[4096]; int read;
return; while ((read = in.read(buffer)) != -1) out.write(buffer, 0, read);
}
byte[] buffer = new byte[4096];
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
plugin.getLogger().info("[AntiBotModule] " + CONFIG_FILE_NAME + " wurde erstellt.");
} catch (Exception e) { } catch (Exception e) {
plugin.getLogger().warning("[AntiBotModule] Konnte " + CONFIG_FILE_NAME + " nicht erstellen: " + e.getMessage()); plugin.getLogger().warning("[AntiBotModule] Konnte Config nicht erstellen: " + e.getMessage());
} }
} }
private void evaluateLearningBaseline(String ip, long now) { private void evaluateLearningBaseline(String ip, long now) {
LearningProfile profile = learningProfiles.computeIfAbsent(ip, k -> new LearningProfile(now)); LearningProfile lp = learningProfiles.computeIfAbsent(ip, k -> new LearningProfile(now));
synchronized (lp) {
synchronized (profile) { decayLearningProfile(lp, now);
decayLearningProfile(profile, now); long delta = now - lp.lastConnectionAt;
if (lp.lastConnectionAt > 0 && delta <= Math.max(250L, learningRapidWindowMs)) {
long delta = now - profile.lastConnectionAt; lp.rapidStreak++;
if (profile.lastConnectionAt > 0 && delta <= Math.max(250L, learningRapidWindowMs)) { int points = learningRapidPoints + Math.min(lp.rapidStreak, 5);
profile.rapidStreak++; lp.score += Math.max(1, points);
int points = learningRapidPoints + Math.min(profile.rapidStreak, 5); recordLearningEvent("IP=" + ip + " +" + points + " rapid-connect score=" + lp.score);
profile.score += Math.max(1, points);
recordLearningEvent("IP=" + ip + " +" + points + " rapid-connect score=" + profile.score);
} else { } else {
profile.rapidStreak = 0; lp.rapidStreak = 0;
} }
if (attackMode) { lp.score += Math.max(1, learningAttackModePoints); recordLearningEvent("IP=" + ip + " +" + learningAttackModePoints + " attack-mode score=" + lp.score); }
if (attackMode) { if (lastCps >= Math.max(1, maxCps)) { lp.score += Math.max(1, learningHighCpsPoints); recordLearningEvent("IP=" + ip + " +" + learningHighCpsPoints + " high-cps score=" + lp.score); }
profile.score += Math.max(1, learningAttackModePoints); lp.lastConnectionAt = now;
recordLearningEvent("IP=" + ip + " +" + learningAttackModePoints + " attack-mode score=" + profile.score); lp.lastSeenAt = now;
} if (lp.score >= learningScoreThreshold) {
if (lastCps >= Math.max(1, maxCps)) {
profile.score += Math.max(1, learningHighCpsPoints);
recordLearningEvent("IP=" + ip + " +" + learningHighCpsPoints + " high-cps score=" + profile.score);
}
profile.lastConnectionAt = now;
profile.lastSeenAt = now;
if (profile.score >= learningScoreThreshold) {
blockIp(ip, now); blockIp(ip, now);
recordLearningEvent("BLOCK " + ip + " reason=learning-threshold score=" + profile.score); recordLearningEvent("BLOCK " + ip + " reason=learning-threshold score=" + lp.score);
} }
} }
} }
private int addLearningScore(String ip, long now, int points, String reason, boolean checkThreshold) { private int addLearningScore(String ip, long now, int points, String reason, boolean checkThreshold) {
LearningProfile profile = learningProfiles.computeIfAbsent(ip, k -> new LearningProfile(now)); LearningProfile lp = learningProfiles.computeIfAbsent(ip, k -> new LearningProfile(now));
synchronized (profile) { synchronized (lp) {
decayLearningProfile(profile, now); decayLearningProfile(lp, now);
int add = Math.max(1, points); int add = Math.max(1, points);
profile.score += add; lp.score += add;
profile.lastSeenAt = now; lp.lastSeenAt = now;
recordLearningEvent("IP=" + ip + " +" + add + " " + reason + " score=" + profile.score); recordLearningEvent("IP=" + ip + " +" + add + " " + reason + " score=" + lp.score);
if (checkThreshold && lp.score >= learningScoreThreshold) {
if (checkThreshold && profile.score >= learningScoreThreshold) {
blockIp(ip, now); blockIp(ip, now);
recordLearningEvent("BLOCK " + ip + " reason=" + reason + " score=" + profile.score); recordLearningEvent("BLOCK " + ip + " reason=" + reason + " score=" + lp.score);
} }
return lp.score;
return profile.score;
} }
} }
private int getLearningScore(String ip, long now) { private int getLearningScore(String ip, long now) {
LearningProfile profile = learningProfiles.get(ip); LearningProfile lp = learningProfiles.get(ip);
if (profile == null) { if (lp == null) return 0;
return 0; synchronized (lp) { decayLearningProfile(lp, now); return lp.score; }
}
synchronized (profile) {
decayLearningProfile(profile, now);
return profile.score;
}
} }
private void decayLearningProfile(LearningProfile profile, long now) { private void decayLearningProfile(LearningProfile lp, long now) {
long elapsedMs = Math.max(0L, now - profile.lastScoreUpdateAt); long elapsedMs = Math.max(0L, now - lp.lastScoreUpdateAt);
if (elapsedMs > 0L) { if (elapsedMs > 0L) {
long decay = (elapsedMs / 1000L) * Math.max(0, learningDecayPerSecond); long decay = (elapsedMs / 1000L) * Math.max(0, learningDecayPerSecond);
if (decay > 0L) { if (decay > 0L) lp.score = (int) Math.max(0L, lp.score - decay);
profile.score = (int) Math.max(0L, profile.score - decay); lp.lastScoreUpdateAt = now;
} }
profile.lastScoreUpdateAt = now;
}
long resetAfter = Math.max(30, learningStateWindowSeconds) * 1000L; long resetAfter = Math.max(30, learningStateWindowSeconds) * 1000L;
if (profile.lastSeenAt > 0L && now - profile.lastSeenAt > resetAfter) { if (lp.lastSeenAt > 0L && now - lp.lastSeenAt > resetAfter) {
profile.score = 0; lp.score = 0;
profile.rapidStreak = 0; lp.rapidStreak = 0;
} }
} }
@@ -736,62 +598,33 @@ public class AntiBotModule implements Module, Listener {
String line = new SimpleDateFormat("HH:mm:ss").format(new Date()) + " " + event; String line = new SimpleDateFormat("HH:mm:ss").format(new Date()) + " " + event;
synchronized (learningRecentEvents) { synchronized (learningRecentEvents) {
learningRecentEvents.addLast(line); learningRecentEvents.addLast(line);
while (learningRecentEvents.size() > Math.max(5, learningRecentEventLimit)) { while (learningRecentEvents.size() > Math.max(5, learningRecentEventLimit)) learningRecentEvents.pollFirst();
learningRecentEvents.pollFirst();
}
} }
} }
private void ensureSecurityLogFile() { private void ensureSecurityLogFile() {
if (plugin == null) { if (plugin == null) return;
return; if (!plugin.getDataFolder().exists()) plugin.getDataFolder().mkdirs();
}
if (!plugin.getDataFolder().exists()) {
plugin.getDataFolder().mkdirs();
}
securityLogFile = new File(plugin.getDataFolder(), securityLogFileName); securityLogFile = new File(plugin.getDataFolder(), securityLogFileName);
try { try { if (!securityLogFile.exists()) securityLogFile.createNewFile(); }
if (!securityLogFile.exists()) { catch (Exception e) { plugin.getLogger().warning("[AntiBotModule] Konnte Sicherheitslog nicht erstellen: " + e.getMessage()); }
securityLogFile.createNewFile();
}
} catch (Exception e) {
plugin.getLogger().warning("[AntiBotModule] Konnte Sicherheitslog nicht erstellen: " + e.getMessage());
}
} }
private void logSecurityEvent(String eventType, String ip, PendingConnection conn, String details) { private void logSecurityEvent(String eventType, String ip, PendingConnection conn, String details) {
if (!securityLogEnabled || plugin == null) { if (!securityLogEnabled || plugin == null) return;
return; if (securityLogFile == null) { ensureSecurityLogFile(); if (securityLogFile == null) return; }
}
if (securityLogFile == null) {
ensureSecurityLogFile();
if (securityLogFile == null) {
return;
}
}
String name = extractPlayerName(conn); String name = extractPlayerName(conn);
String uuid = extractPlayerUuid(conn, name); String uuid = extractPlayerUuid(conn, name);
if ((name == null || name.isEmpty() || "unknown".equalsIgnoreCase(name)) && ip != null) { if ((name == null || name.isEmpty() || "unknown".equalsIgnoreCase(name)) && ip != null) {
RecentPlayerIdentity cached = recentIdentityByIp.get(ip); RecentPlayerIdentity cached = recentIdentityByIp.get(ip);
if (cached != null) { if (cached != null) {
if (cached.playerName != null && !cached.playerName.trim().isEmpty()) { if (cached.playerName != null && !cached.playerName.trim().isEmpty()) name = cached.playerName;
name = cached.playerName; if ((uuid == null || uuid.isEmpty()) && cached.playerUuid != null && !cached.playerUuid.trim().isEmpty()) uuid = cached.playerUuid;
}
if ((uuid == null || uuid.isEmpty() || "unknown".equalsIgnoreCase(uuid))
&& cached.playerUuid != null && !cached.playerUuid.trim().isEmpty()) {
uuid = cached.playerUuid;
} }
} }
} if (name == null || name.trim().isEmpty()) name = "unknown";
if (uuid == null || uuid.trim().isEmpty()) uuid = "unknown";
if (name == null || name.trim().isEmpty()) {
name = "unknown";
}
if (uuid == null || uuid.trim().isEmpty()) {
uuid = "unknown";
}
String line = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) String line = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())
+ " | event=" + safeLog(eventType) + " | event=" + safeLog(eventType)
@@ -802,8 +635,7 @@ public class AntiBotModule implements Module, Listener {
synchronized (securityLogLock) { synchronized (securityLogLock) {
try (BufferedWriter bw = new BufferedWriter(new FileWriter(securityLogFile, true))) { try (BufferedWriter bw = new BufferedWriter(new FileWriter(securityLogFile, true))) {
bw.write(line); bw.write(line); bw.newLine();
bw.newLine();
} catch (Exception e) { } catch (Exception e) {
plugin.getLogger().warning("[AntiBotModule] Sicherheitslog-Schreibfehler: " + e.getMessage()); plugin.getLogger().warning("[AntiBotModule] Sicherheitslog-Schreibfehler: " + e.getMessage());
} }
@@ -811,99 +643,58 @@ public class AntiBotModule implements Module, Listener {
} }
private void cacheRecentIdentity(String ip, PendingConnection conn, long now) { private void cacheRecentIdentity(String ip, PendingConnection conn, long now) {
if (ip == null || ip.isEmpty() || conn == null) { if (ip == null || ip.isEmpty() || conn == null) return;
return;
}
String name = extractPlayerName(conn); String name = extractPlayerName(conn);
String uuid = extractPlayerUuid(conn, name); String uuid = extractPlayerUuid(conn, name);
if ((name == null || name.isEmpty()) && (uuid == null || uuid.isEmpty())) return;
if ((name == null || name.isEmpty()) && (uuid == null || uuid.isEmpty())) {
return;
}
RecentPlayerIdentity identity = recentIdentityByIp.computeIfAbsent(ip, k -> new RecentPlayerIdentity()); RecentPlayerIdentity identity = recentIdentityByIp.computeIfAbsent(ip, k -> new RecentPlayerIdentity());
synchronized (identity) { synchronized (identity) {
if (name != null && !name.trim().isEmpty()) { if (name != null && !name.trim().isEmpty()) identity.playerName = name.trim();
identity.playerName = name.trim(); if (uuid != null && !uuid.trim().isEmpty()) identity.playerUuid = uuid.trim();
}
if (uuid != null && !uuid.trim().isEmpty()) {
identity.playerUuid = uuid.trim();
}
identity.updatedAtMs = now; identity.updatedAtMs = now;
} }
} }
private void cacheRecentIdentityDirect(String ip, String playerName, UUID playerUuid, long now) { private void cacheRecentIdentityDirect(String ip, String playerName, UUID playerUuid, long now) {
if (ip == null || ip.isEmpty()) { if (ip == null || ip.isEmpty()) return;
return;
}
String name = playerName == null ? "" : playerName.trim(); String name = playerName == null ? "" : playerName.trim();
String uuid = playerUuid == null ? "" : playerUuid.toString(); String uuid = playerUuid == null ? "" : playerUuid.toString();
if (name.isEmpty() && uuid.isEmpty()) { if (name.isEmpty() && uuid.isEmpty()) return;
return;
}
RecentPlayerIdentity identity = recentIdentityByIp.computeIfAbsent(ip, k -> new RecentPlayerIdentity()); RecentPlayerIdentity identity = recentIdentityByIp.computeIfAbsent(ip, k -> new RecentPlayerIdentity());
synchronized (identity) { synchronized (identity) {
if (!name.isEmpty()) { if (!name.isEmpty()) identity.playerName = name;
identity.playerName = name; if (!uuid.isEmpty()) identity.playerUuid = uuid;
}
if (!uuid.isEmpty()) {
identity.playerUuid = uuid;
}
identity.updatedAtMs = now; identity.updatedAtMs = now;
} }
} }
private String extractPlayerName(PendingConnection conn) { private String extractPlayerName(PendingConnection conn) {
if (conn == null) { if (conn == null) return "";
return ""; try { String raw = conn.getName(); return raw == null ? "" : raw.trim(); } catch (Exception ignored) { return ""; }
}
try {
String raw = conn.getName();
return raw == null ? "" : raw.trim();
} catch (Exception ignored) {
return "";
}
} }
private String extractPlayerUuid(PendingConnection conn, String playerName) { private String extractPlayerUuid(PendingConnection conn, String playerName) {
if (conn != null) { if (conn != null) {
try { try { UUID uuid = conn.getUniqueId(); if (uuid != null) return uuid.toString(); } catch (Exception ignored) {}
UUID uuid = conn.getUniqueId();
if (uuid != null) {
return uuid.toString();
} }
} catch (Exception ignored) {
}
}
if (playerName != null && !playerName.trim().isEmpty()) { if (playerName != null && !playerName.trim().isEmpty()) {
UUID offlineUuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + playerName.trim()).getBytes(StandardCharsets.UTF_8)); return UUID.nameUUIDFromBytes(("OfflinePlayer:" + playerName.trim()).getBytes(StandardCharsets.UTF_8)).toString();
return offlineUuid.toString();
} }
return ""; return "";
} }
private String safeLog(String input) { private String safeLog(String input) {
if (input == null || input.isEmpty()) { if (input == null || input.isEmpty()) return "-";
return "-";
}
return input.replace("\n", " ").replace("\r", " ").trim(); return input.replace("\n", " ").replace("\r", " ").trim();
} }
private List<String> getRecentLearningEvents(int max) { private List<String> getRecentLearningEvents(int max) {
List<String> out = new ArrayList<String>(); List<String> out = new ArrayList<>();
synchronized (learningRecentEvents) { synchronized (learningRecentEvents) {
int skip = Math.max(0, learningRecentEvents.size() - Math.max(1, max)); int skip = Math.max(0, learningRecentEvents.size() - Math.max(1, max));
int idx = 0; int idx = 0;
for (String line : learningRecentEvents) { for (String line : learningRecentEvents) {
if (idx++ < skip) { if (idx++ < skip) continue;
continue;
}
out.add(line); out.add(line);
} }
} }
@@ -916,19 +707,12 @@ public class AntiBotModule implements Module, Listener {
} }
private int parseInt(String s, int fallback) { private int parseInt(String s, int fallback) {
try { try { return Integer.parseInt(s == null ? "" : s.trim()); } catch (Exception ignored) { return fallback; }
return Integer.parseInt(s == null ? "" : s.trim());
} catch (Exception ignored) {
return fallback;
}
} }
private VpnCheckResult getVpnInfo(String ip, long now) { private VpnCheckResult getVpnInfo(String ip, long now) {
VpnCacheEntry cached = vpnCache.get(ip); VpnCacheEntry cached = vpnCache.get(ip);
if (cached != null && cached.expiresAt > now) { if (cached != null && cached.expiresAt > now) return cached.result;
return cached.result;
}
VpnCheckResult fresh = requestIpApi(ip); VpnCheckResult fresh = requestIpApi(ip);
if (fresh != null) { if (fresh != null) {
VpnCacheEntry entry = new VpnCacheEntry(); VpnCacheEntry entry = new VpnCacheEntry();
@@ -948,100 +732,54 @@ public class AntiBotModule implements Module, Listener {
conn.setConnectTimeout(vpnTimeoutMs); conn.setConnectTimeout(vpnTimeoutMs);
conn.setReadTimeout(vpnTimeoutMs); conn.setReadTimeout(vpnTimeoutMs);
conn.setRequestProperty("User-Agent", "StatusAPI-AntiBot/1.0"); conn.setRequestProperty("User-Agent", "StatusAPI-AntiBot/1.0");
if (conn.getResponseCode() < 200 || conn.getResponseCode() >= 300) return null;
int code = conn.getResponseCode();
if (code < 200 || code >= 300) {
return null;
}
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) { try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
String line; String line; while ((line = br.readLine()) != null) sb.append(line);
while ((line = br.readLine()) != null) {
sb.append(line);
} }
}
String json = sb.toString(); String json = sb.toString();
if (json.isEmpty() || !json.contains("\"status\":\"success\"")) { if (json.isEmpty() || !json.contains("\"status\":\"success\"")) return null;
return null;
}
VpnCheckResult result = new VpnCheckResult(); VpnCheckResult result = new VpnCheckResult();
result.proxy = json.contains("\"proxy\":true"); result.proxy = json.contains("\"proxy\":true");
result.hosting = json.contains("\"hosting\":true"); result.hosting = json.contains("\"hosting\":true");
return result; return result;
} catch (Exception ignored) { } catch (Exception ignored) { return null; }
return null; finally { if (conn != null) conn.disconnect(); }
} finally {
if (conn != null) {
conn.disconnect();
}
}
} }
// --- Interne Klassen ---
private static class IpWindow { private static class IpWindow {
long windowStart; long windowStart; int count;
int count; IpWindow(long now) { this.windowStart = now; this.count = 0; }
IpWindow(long now) {
this.windowStart = now;
this.count = 0;
}
} }
private static class VpnCacheEntry { private static class VpnCacheEntry { VpnCheckResult result; long expiresAt; }
VpnCheckResult result; private static class VpnCheckResult { boolean proxy; boolean hosting; }
long expiresAt;
}
private static class VpnCheckResult {
boolean proxy;
boolean hosting;
}
private static class LearningProfile { private static class LearningProfile {
long lastConnectionAt; long lastConnectionAt, lastScoreUpdateAt, lastSeenAt;
long lastScoreUpdateAt; int rapidStreak, score;
long lastSeenAt; LearningProfile(long now) { lastConnectionAt = lastScoreUpdateAt = lastSeenAt = now; }
int rapidStreak;
int score;
LearningProfile(long now) {
this.lastConnectionAt = now;
this.lastScoreUpdateAt = now;
this.lastSeenAt = now;
this.rapidStreak = 0;
this.score = 0;
}
} }
private static class RecentPlayerIdentity { private static class RecentPlayerIdentity { String playerName; String playerUuid; long updatedAtMs; }
String playerName;
String playerUuid; // --- Command ---
long updatedAtMs;
}
private class AntiBotCommand extends Command { private class AntiBotCommand extends Command {
AntiBotCommand() { AntiBotCommand() { super("antibot", "statusapi.antibot"); }
super("antibot", "statusapi.antibot");
}
@Override @Override
public void execute(CommandSender sender, String[] args) { public void execute(CommandSender sender, String[] args) {
if (!enabled) { if (!enabled) { sender.sendMessage(ChatColor.RED + "AntiBotModule ist deaktiviert."); return; }
sender.sendMessage(ChatColor.RED + "AntiBotModule ist deaktiviert.");
return;
}
if (args.length == 0 || "status".equalsIgnoreCase(args[0])) { if (args.length == 0 || "status".equalsIgnoreCase(args[0])) {
sender.sendMessage(ChatColor.GOLD + "----- AntiBot Status -----"); sender.sendMessage(ChatColor.GOLD + "----- AntiBot Status -----");
sender.sendMessage(ChatColor.YELLOW + "Schutz aktiv: " + ChatColor.WHITE + enabled + ChatColor.GRAY + " (Modul eingeschaltet)"); sender.sendMessage(ChatColor.YELLOW + "Schutz aktiv: " + ChatColor.WHITE + enabled);
if (attackMode) { sender.sendMessage(ChatColor.YELLOW + "Profil: " + ChatColor.WHITE + profile);
sender.sendMessage(ChatColor.YELLOW + "Attack Mode: " + ChatColor.RED + "AKTIV" + ChatColor.GRAY + " (Angriff erkannt)"); if (attackMode) sender.sendMessage(ChatColor.YELLOW + "Attack Mode: " + ChatColor.RED + "AKTIV");
} else { else sender.sendMessage(ChatColor.YELLOW + "Attack Mode: " + ChatColor.GREEN + "Normal");
sender.sendMessage(ChatColor.YELLOW + "Attack Mode: " + ChatColor.GREEN + "Normal" + ChatColor.GRAY + " (kein Angriff erkannt)");
}
sender.sendMessage(ChatColor.YELLOW + "CPS: " + ChatColor.WHITE + lastCps + ChatColor.GRAY + " (Peak " + peakCps.get() + ")"); sender.sendMessage(ChatColor.YELLOW + "CPS: " + ChatColor.WHITE + lastCps + ChatColor.GRAY + " (Peak " + peakCps.get() + ")");
sender.sendMessage(ChatColor.YELLOW + "Schwellen: " + ChatColor.WHITE + "start " + attackStartCps + ChatColor.GRAY + " / " + ChatColor.WHITE + "stop " + attackStopCps + ChatColor.GRAY + " CPS"); sender.sendMessage(ChatColor.YELLOW + "Schwellen: " + ChatColor.WHITE + "start " + attackStartCps + ChatColor.GRAY + " / " + ChatColor.WHITE + "stop " + attackStopCps + ChatColor.GRAY + " CPS");
sender.sendMessage(ChatColor.YELLOW + "Active IP Blocks: " + ChatColor.WHITE + blockedIpsUntil.size()); sender.sendMessage(ChatColor.YELLOW + "Active IP Blocks: " + ChatColor.WHITE + blockedIpsUntil.size());
@@ -1049,13 +787,10 @@ public class AntiBotModule implements Module, Listener {
sender.sendMessage(ChatColor.YELLOW + "VPN Check: " + ChatColor.WHITE + vpnCheckEnabled); sender.sendMessage(ChatColor.YELLOW + "VPN Check: " + ChatColor.WHITE + vpnCheckEnabled);
sender.sendMessage(ChatColor.YELLOW + "Learning Mode: " + ChatColor.WHITE + learningModeEnabled sender.sendMessage(ChatColor.YELLOW + "Learning Mode: " + ChatColor.WHITE + learningModeEnabled
+ ChatColor.GRAY + " (threshold=" + learningScoreThreshold + ")"); + ChatColor.GRAY + " (threshold=" + learningScoreThreshold + ")");
List<String> recent = getRecentLearningEvents(3); List<String> recent = getRecentLearningEvents(3);
if (!recent.isEmpty()) { if (!recent.isEmpty()) {
sender.sendMessage(ChatColor.YELLOW + "Learning Events:"); sender.sendMessage(ChatColor.YELLOW + "Learning Events:");
for (String line : recent) { for (String line : recent) sender.sendMessage(ChatColor.GRAY + "- " + line);
sender.sendMessage(ChatColor.GRAY + "- " + line);
}
} }
return; return;
} }
@@ -1068,12 +803,8 @@ public class AntiBotModule implements Module, Listener {
if ("unblock".equalsIgnoreCase(args[0]) && args.length >= 2) { if ("unblock".equalsIgnoreCase(args[0]) && args.length >= 2) {
String ip = args[1].trim(); String ip = args[1].trim();
Long removed = blockedIpsUntil.remove(ip); if (blockedIpsUntil.remove(ip) != null) sender.sendMessage(ChatColor.GREEN + "IP entblockt: " + ip);
if (removed != null) { else sender.sendMessage(ChatColor.RED + "IP war nicht geblockt: " + ip);
sender.sendMessage(ChatColor.GREEN + "IP entblockt: " + ip);
} else {
sender.sendMessage(ChatColor.RED + "IP war nicht geblockt: " + ip);
}
return; return;
} }
@@ -1083,19 +814,10 @@ public class AntiBotModule implements Module, Listener {
sender.sendMessage(ChatColor.YELLOW + "Benutzung: /antibot profile <strict|high-traffic>"); sender.sendMessage(ChatColor.YELLOW + "Benutzung: /antibot profile <strict|high-traffic>");
return; return;
} }
String requested = args[1].trim().toLowerCase(Locale.ROOT); String requested = args[1].trim().toLowerCase(Locale.ROOT);
if (!isSupportedProfile(requested)) { if (!isSupportedProfile(requested)) { sender.sendMessage(ChatColor.RED + "Unbekanntes Profil. Erlaubt: strict, high-traffic"); return; }
sender.sendMessage(ChatColor.RED + "Unbekanntes Profil. Erlaubt: strict, high-traffic");
return;
}
boolean ok = applyProfileAndPersist(requested); boolean ok = applyProfileAndPersist(requested);
if (!ok) { if (!ok) { sender.sendMessage(ChatColor.RED + "Profil konnte nicht gespeichert werden."); return; }
sender.sendMessage(ChatColor.RED + "Profil konnte nicht gespeichert werden. Siehe Konsole.");
return;
}
sender.sendMessage(ChatColor.GREEN + "AntiBot-Profil umgestellt auf: " + requested); sender.sendMessage(ChatColor.GREEN + "AntiBot-Profil umgestellt auf: " + requested);
sender.sendMessage(ChatColor.GRAY + "Werte wurden in " + CONFIG_FILE_NAME + " gespeichert."); sender.sendMessage(ChatColor.GRAY + "Werte wurden in " + CONFIG_FILE_NAME + " gespeichert.");
return; return;

View File

@@ -17,8 +17,9 @@ import java.util.concurrent.TimeUnit;
/** /**
* BroadcastModule * BroadcastModule
* *
* Speichert geplante Broadcasts jetzt persistent in 'broadcasts.schedules'. * Fixes:
* Beim Neustart werden diese automatisch wieder geladen. * - loadSchedules(): ID-Split nutzt jetzt indexOf/lastIndexOf statt split("\\.") mit length==2-Check.
* Damit werden auch clientScheduleIds die Punkte enthalten korrekt geladen. (Bug #2)
*/ */
public class BroadcastModule implements Module, Listener { public class BroadcastModule implements Module, Listener {
@@ -28,7 +29,7 @@ public class BroadcastModule implements Module, Listener {
private String format = "%prefix% %message%"; private String format = "%prefix% %message%";
private String fallbackPrefix = "[Broadcast]"; private String fallbackPrefix = "[Broadcast]";
private String fallbackPrefixColor = "&c"; private String fallbackPrefixColor = "&c";
private String fallbackBracketColor = "&8"; // Neu private String fallbackBracketColor = "&8";
private String fallbackMessageColor = "&f"; private String fallbackMessageColor = "&f";
private final Map<String, ScheduledBroadcast> scheduledByClientId = new ConcurrentHashMap<>(); private final Map<String, ScheduledBroadcast> scheduledByClientId = new ConcurrentHashMap<>();
@@ -41,25 +42,15 @@ public class BroadcastModule implements Module, Listener {
} }
@Override @Override
public String getName() { public String getName() { return "BroadcastModule"; }
return "BroadcastModule";
}
@Override @Override
public void onEnable(Plugin plugin) { public void onEnable(Plugin plugin) {
this.plugin = plugin; this.plugin = plugin;
schedulesFile = new File(plugin.getDataFolder(), "broadcasts.schedules"); schedulesFile = new File(plugin.getDataFolder(), "broadcasts.schedules");
loadConfig(); loadConfig();
if (!enabled) return;
if (!enabled) { try { plugin.getProxy().getPluginManager().registerListener(plugin, this); } catch (Throwable ignored) {}
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); plugin.getLogger().info("[BroadcastModule] aktiviert. Format: " + format);
loadSchedules(); loadSchedules();
plugin.getProxy().getScheduler().schedule(plugin, this::processScheduled, 1, 1, TimeUnit.SECONDS); plugin.getProxy().getScheduler().schedule(plugin, this::processScheduled, 1, 1, TimeUnit.SECONDS);
@@ -67,24 +58,13 @@ public class BroadcastModule implements Module, Listener {
@Override @Override
public void onDisable(Plugin plugin) { public void onDisable(Plugin plugin) {
plugin.getLogger().info("[BroadcastModule] deaktiviert.");
saveSchedules(); saveSchedules();
scheduledByClientId.clear(); scheduledByClientId.clear();
} }
private void loadConfig() { private void loadConfig() {
File file = new File(plugin.getDataFolder(), "verify.properties"); File file = new File(plugin.getDataFolder(), "verify.properties");
if (!file.exists()) { if (!file.exists()) return;
enabled = true;
requiredApiKey = "";
format = "%prefix% %message%";
fallbackPrefix = "[Broadcast]";
fallbackPrefixColor = "&c";
fallbackBracketColor = "&8"; // Neu
fallbackMessageColor = "&f";
return;
}
try (InputStream in = new FileInputStream(file)) { try (InputStream in = new FileInputStream(file)) {
Properties props = new Properties(); Properties props = new Properties();
props.load(new InputStreamReader(in, StandardCharsets.UTF_8)); props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
@@ -94,7 +74,7 @@ public class BroadcastModule implements Module, Listener {
if (format.isEmpty()) format = "%prefix% %message%"; if (format.isEmpty()) format = "%prefix% %message%";
fallbackPrefix = props.getProperty("broadcast.prefix", fallbackPrefix).trim(); fallbackPrefix = props.getProperty("broadcast.prefix", fallbackPrefix).trim();
fallbackPrefixColor = props.getProperty("broadcast.prefix-color", fallbackPrefixColor).trim(); fallbackPrefixColor = props.getProperty("broadcast.prefix-color", fallbackPrefixColor).trim();
fallbackBracketColor = props.getProperty("broadcast.bracket-color", fallbackBracketColor).trim(); // Neu fallbackBracketColor = props.getProperty("broadcast.bracket-color", fallbackBracketColor).trim();
fallbackMessageColor = props.getProperty("broadcast.message-color", fallbackMessageColor).trim(); fallbackMessageColor = props.getProperty("broadcast.message-color", fallbackMessageColor).trim();
} catch (IOException e) { } catch (IOException e) {
plugin.getLogger().warning("[BroadcastModule] Fehler beim Laden von verify.properties: " + e.getMessage()); plugin.getLogger().warning("[BroadcastModule] Fehler beim Laden von verify.properties: " + e.getMessage());
@@ -104,12 +84,7 @@ public class BroadcastModule implements Module, Listener {
public boolean handleBroadcast(String sourceName, String message, String type, String apiKeyHeader, public boolean handleBroadcast(String sourceName, String message, String type, String apiKeyHeader,
String prefix, String prefixColor, String bracketColor, String messageColor) { String prefix, String prefixColor, String bracketColor, String messageColor) {
loadConfig(); loadConfig();
if (!enabled) return false;
if (!enabled) {
plugin.getLogger().info("[BroadcastModule] Broadcast abgelehnt: Modul ist deaktiviert.");
return false;
}
if (requiredApiKey != null && !requiredApiKey.isEmpty()) { if (requiredApiKey != null && !requiredApiKey.isEmpty()) {
if (apiKeyHeader == null || !requiredApiKey.equals(apiKeyHeader)) { if (apiKeyHeader == null || !requiredApiKey.equals(apiKeyHeader)) {
plugin.getLogger().warning("[BroadcastModule] Broadcast abgelehnt: API-Key fehlt oder inkorrekt."); plugin.getLogger().warning("[BroadcastModule] Broadcast abgelehnt: API-Key fehlt oder inkorrekt.");
@@ -123,50 +98,37 @@ public class BroadcastModule implements Module, Listener {
String usedPrefix = (prefix != null && !prefix.trim().isEmpty()) ? prefix.trim() : fallbackPrefix; String usedPrefix = (prefix != null && !prefix.trim().isEmpty()) ? prefix.trim() : fallbackPrefix;
String usedPrefixColor = (prefixColor != null && !prefixColor.trim().isEmpty()) ? prefixColor.trim() : fallbackPrefixColor; String usedPrefixColor = (prefixColor != null && !prefixColor.trim().isEmpty()) ? prefixColor.trim() : fallbackPrefixColor;
String usedBracketColor = (bracketColor != null && !bracketColor.trim().isEmpty()) ? bracketColor.trim() : fallbackBracketColor; // Neu String usedBracketColor = (bracketColor != null && !bracketColor.trim().isEmpty()) ? bracketColor.trim() : fallbackBracketColor;
String usedMessageColor = (messageColor != null && !messageColor.trim().isEmpty()) ? messageColor.trim() : fallbackMessageColor; String usedMessageColor = (messageColor != null && !messageColor.trim().isEmpty()) ? messageColor.trim() : fallbackMessageColor;
String prefixColorCode = normalizeColorCode(usedPrefixColor); String prefixColorCode = normalizeColorCode(usedPrefixColor);
String bracketColorCode = normalizeColorCode(usedBracketColor); // Neu String bracketColorCode = normalizeColorCode(usedBracketColor);
String messageColorCode = normalizeColorCode(usedMessageColor); String messageColorCode = normalizeColorCode(usedMessageColor);
// --- KLAMMER LOGIK ---
String finalPrefix; String finalPrefix;
// Wenn eine Klammerfarbe gesetzt ist, bauen wir den Prefix neu zusammen
// Format: [BracketColor][ [PrefixColor]Text [BracketColor]]
if (!bracketColorCode.isEmpty()) { if (!bracketColorCode.isEmpty()) {
String textContent = usedPrefix; String textContent = usedPrefix;
// Entferne manuelle Klammern, falls der User [Broadcast] in das Textfeld geschrieben hat
if (textContent.startsWith("[")) textContent = textContent.substring(1); if (textContent.startsWith("[")) textContent = textContent.substring(1);
if (textContent.endsWith("]")) textContent = textContent.substring(0, textContent.length() - 1); if (textContent.endsWith("]")) textContent = textContent.substring(0, textContent.length() - 1);
finalPrefix = bracketColorCode + "[" + prefixColorCode + textContent + bracketColorCode + "]" + ChatColor.RESET; finalPrefix = bracketColorCode + "[" + prefixColorCode + textContent + bracketColorCode + "]" + ChatColor.RESET;
} else { } else {
// Altes Verhalten: Ganzen String einfärben
finalPrefix = prefixColorCode + usedPrefix + ChatColor.RESET; finalPrefix = prefixColorCode + usedPrefix + ChatColor.RESET;
} }
// ---------------------
String coloredMessage = (messageColorCode.isEmpty() ? "" : messageColorCode) + message; String coloredMessage = (messageColorCode.isEmpty() ? "" : messageColorCode) + message;
String out = format
String out = format.replace("%name%", sourceName) .replace("%name%", sourceName)
.replace("%prefix%", finalPrefix) // Neu verwendete Variable .replace("%prefix%", finalPrefix)
.replace("%prefixColored%", finalPrefix) // Fallback .replace("%prefixColored%", finalPrefix)
.replace("%message%", message) .replace("%message%", message)
.replace("%messageColored%", coloredMessage) .replace("%messageColored%",coloredMessage)
.replace("%type%", type); .replace("%type%", type);
if (!out.contains("%prefixColored%") && !out.contains("%messageColored%") && !out.contains("%prefix%") && !out.contains("%message%")) {
out = finalPrefix + " " + coloredMessage;
}
TextComponent tc = new TextComponent(out); TextComponent tc = new TextComponent(out);
int sent = 0; int sent = 0;
for (ProxiedPlayer p : plugin.getProxy().getPlayers()) { for (ProxiedPlayer p : plugin.getProxy().getPlayers()) {
try { p.sendMessage(tc); sent++; } catch (Throwable ignored) {} try { p.sendMessage(tc); sent++; } catch (Throwable ignored) {}
} }
plugin.getLogger().info("[BroadcastModule] Broadcast gesendet (Empfänger=" + sent + "): " + message); plugin.getLogger().info("[BroadcastModule] Broadcast gesendet (Empfänger=" + sent + "): " + message);
return true; return true;
} }
@@ -183,17 +145,17 @@ public class BroadcastModule implements Module, Listener {
for (Map.Entry<String, ScheduledBroadcast> entry : scheduledByClientId.entrySet()) { for (Map.Entry<String, ScheduledBroadcast> entry : scheduledByClientId.entrySet()) {
String id = entry.getKey(); String id = entry.getKey();
ScheduledBroadcast sb = entry.getValue(); ScheduledBroadcast sb = entry.getValue();
// Wir escapen den ID-Wert damit Punkte in der ID nicht den Parser verwirren
props.setProperty(id + ".nextRunMillis", String.valueOf(sb.nextRunMillis)); props.setProperty(id + ".nextRunMillis", String.valueOf(sb.nextRunMillis));
props.setProperty(id + ".sourceName", sb.sourceName); props.setProperty(id + ".sourceName", sb.sourceName);
props.setProperty(id + ".message", sb.message); props.setProperty(id + ".message", sb.message);
props.setProperty(id + ".type", sb.type); props.setProperty(id + ".type", sb.type);
props.setProperty(id + ".prefix", sb.prefix); props.setProperty(id + ".prefix", sb.prefix);
props.setProperty(id + ".prefixColor", sb.prefixColor); props.setProperty(id + ".prefixColor", sb.prefixColor);
props.setProperty(id + ".bracketColor", sb.bracketColor); // Neu props.setProperty(id + ".bracketColor", sb.bracketColor);
props.setProperty(id + ".messageColor", sb.messageColor); props.setProperty(id + ".messageColor", sb.messageColor);
props.setProperty(id + ".recur", sb.recur); props.setProperty(id + ".recur", sb.recur);
} }
try (OutputStream out = new FileOutputStream(schedulesFile)) { try (OutputStream out = new FileOutputStream(schedulesFile)) {
props.store(out, "PulseCast Scheduled Broadcasts"); props.store(out, "PulseCast Scheduled Broadcasts");
} catch (IOException e) { } catch (IOException e) {
@@ -201,12 +163,16 @@ public class BroadcastModule implements Module, Listener {
} }
} }
/**
* FIX #2: Robusteres Parsen der Property-Keys.
* Statt split("\\.") mit length==2-Check nutzen wir lastIndexOf('.') um den letzten
* Punkt als Trenner zu verwenden. Damit funktionieren auch IDs die Punkte enthalten.
*
* Bekannte Felder: nextRunMillis, sourceName, message, type, prefix, prefixColor,
* bracketColor, messageColor, recur → alle ohne Punkte im Namen.
*/
private void loadSchedules() { private void loadSchedules() {
if (!schedulesFile.exists()) { if (!schedulesFile.exists()) return;
plugin.getLogger().info("[BroadcastModule] Keine bestehenden Schedules gefunden (Neustart).");
return;
}
Properties props = new Properties(); Properties props = new Properties();
try (InputStream in = new FileInputStream(schedulesFile)) { try (InputStream in = new FileInputStream(schedulesFile)) {
props.load(in); props.load(in);
@@ -215,32 +181,34 @@ public class BroadcastModule implements Module, Listener {
return; return;
} }
Map<String, ScheduledBroadcast> loaded = new HashMap<>(); // Bekannte Feld-Suffixe
for (String key : props.stringPropertyNames()) { Set<String> knownFields = new HashSet<>(Arrays.asList(
if (!key.contains(".")) continue; "nextRunMillis", "sourceName", "message", "type",
String[] parts = key.split("\\."); "prefix", "prefixColor", "bracketColor", "messageColor", "recur"
if (parts.length != 2) continue; ));
String id = parts[0]; Map<String, ScheduledBroadcast> loaded = new LinkedHashMap<>();
String field = parts[1]; for (String key : props.stringPropertyNames()) {
// Finde das letzte '.' das einen bekannten Feldnamen abtrennt
int lastDot = key.lastIndexOf('.');
if (lastDot < 0) continue;
String field = key.substring(lastDot + 1);
if (!knownFields.contains(field)) continue;
String id = key.substring(0, lastDot);
if (id.isEmpty()) continue;
String value = props.getProperty(key); String value = props.getProperty(key);
ScheduledBroadcast sb = loaded.get(id); ScheduledBroadcast sb = loaded.computeIfAbsent(id,
if (sb == null) { k -> new ScheduledBroadcast(k, 0, "", "", "", "", "", "", "", ""));
sb = new ScheduledBroadcast(id, 0, "", "", "", "", "", "", "", ""); // Ein leerer String mehr für Bracket
loaded.put(id, sb);
}
switch (field) { switch (field) {
case "nextRunMillis": case "nextRunMillis": try { sb.nextRunMillis = Long.parseLong(value); } catch (NumberFormatException ignored) {} break;
try { sb.nextRunMillis = Long.parseLong(value); } catch (NumberFormatException ignored) {}
break;
case "sourceName": sb.sourceName = value; break; case "sourceName": sb.sourceName = value; break;
case "message": sb.message = value; break; case "message": sb.message = value; break;
case "type": sb.type = value; break; case "type": sb.type = value; break;
case "prefix": sb.prefix = value; break; case "prefix": sb.prefix = value; break;
case "prefixColor": sb.prefixColor = value; break; case "prefixColor": sb.prefixColor = value; break;
case "bracketColor": sb.bracketColor = value; break; // Neu case "bracketColor": sb.bracketColor = value; break;
case "messageColor": sb.messageColor = value; break; case "messageColor": sb.messageColor = value; break;
case "recur": sb.recur = value; break; case "recur": sb.recur = value; break;
} }
@@ -250,95 +218,66 @@ public class BroadcastModule implements Module, Listener {
} }
public boolean scheduleBroadcast(long timestampMillis, String sourceName, String message, String type, public boolean scheduleBroadcast(long timestampMillis, String sourceName, String message, String type,
String apiKeyHeader, String prefix, String prefixColor, String bracketColor, String messageColor, String apiKeyHeader, String prefix, String prefixColor, String bracketColor,
String recur, String clientScheduleId) { String messageColor, String recur, String clientScheduleId) {
loadConfig(); loadConfig();
if (!enabled) return false;
if (!enabled) {
plugin.getLogger().info("[BroadcastModule] schedule abgelehnt: Modul deaktiviert.");
return false;
}
if (requiredApiKey != null && !requiredApiKey.isEmpty()) { if (requiredApiKey != null && !requiredApiKey.isEmpty()) {
if (apiKeyHeader == null || !requiredApiKey.equals(apiKeyHeader)) { if (apiKeyHeader == null || !requiredApiKey.equals(apiKeyHeader)) {
plugin.getLogger().warning("[BroadcastModule] schedule abgelehnt: API-Key fehlt oder inkorrekt."); plugin.getLogger().warning("[BroadcastModule] schedule abgelehnt: API-Key fehlt oder inkorrekt.");
return false; return false;
} }
} }
if (message == null) message = ""; if (message == null) message = "";
if (sourceName == null || sourceName.isEmpty()) sourceName = "System"; if (sourceName == null || sourceName.isEmpty()) sourceName = "System";
if (type == null) type = "global"; if (type == null) type = "global";
if (recur == null) recur = "none"; if (recur == null) recur = "none";
String id = (clientScheduleId != null && !clientScheduleId.trim().isEmpty()) ? clientScheduleId.trim() : UUID.randomUUID().toString(); String id = (clientScheduleId != null && !clientScheduleId.trim().isEmpty())
? clientScheduleId.trim() : UUID.randomUUID().toString();
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
String scheduledTimeStr = dateFormat.format(new Date(timestampMillis));
plugin.getLogger().info("[BroadcastModule] Neue geplante Nachricht registriert: " + id + " @ " + scheduledTimeStr);
if (timestampMillis <= now) { if (timestampMillis <= now) {
plugin.getLogger().warning("[BroadcastModule] Geplante Zeit liegt in der Vergangenheit -> sende sofort!"); plugin.getLogger().warning("[BroadcastModule] Geplante Zeit in der Vergangenheit sende sofort!");
return handleBroadcast(sourceName, message, type, apiKeyHeader, prefix, prefixColor, bracketColor, messageColor); return handleBroadcast(sourceName, message, type, apiKeyHeader, prefix, prefixColor, bracketColor, messageColor);
} }
ScheduledBroadcast sb = new ScheduledBroadcast(id, timestampMillis, sourceName, message, type, prefix, prefixColor, bracketColor, messageColor, recur); ScheduledBroadcast sb = new ScheduledBroadcast(id, timestampMillis, sourceName, message, type,
prefix, prefixColor, bracketColor, messageColor, recur);
scheduledByClientId.put(id, sb); scheduledByClientId.put(id, sb);
saveSchedules(); saveSchedules();
plugin.getLogger().info("[BroadcastModule] Neue geplante Nachricht registriert: " + id
+ " @ " + dateFormat.format(new Date(timestampMillis)));
return true; return true;
} }
public boolean cancelScheduled(String clientScheduleId) { public boolean cancelScheduled(String clientScheduleId) {
if (clientScheduleId == null || clientScheduleId.trim().isEmpty()) return false; if (clientScheduleId == null || clientScheduleId.trim().isEmpty()) return false;
ScheduledBroadcast removed = scheduledByClientId.remove(clientScheduleId); ScheduledBroadcast removed = scheduledByClientId.remove(clientScheduleId);
if (removed != null) { if (removed != null) { plugin.getLogger().info("[BroadcastModule] Schedule abgebrochen: " + clientScheduleId); saveSchedules(); return true; }
plugin.getLogger().info("[BroadcastModule] Geplante Nachricht abgebrochen: id=" + clientScheduleId);
saveSchedules();
return true;
}
return false; return false;
} }
private void processScheduled() { private void processScheduled() {
if (scheduledByClientId.isEmpty()) return; if (scheduledByClientId.isEmpty()) return;
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
List<String> toRemove = new ArrayList<>(); List<String> toRemove = new ArrayList<>();
boolean changed = false; boolean changed = false;
for (Map.Entry<String, ScheduledBroadcast> entry : scheduledByClientId.entrySet()) { for (Map.Entry<String, ScheduledBroadcast> entry : scheduledByClientId.entrySet()) {
ScheduledBroadcast sb = entry.getValue(); ScheduledBroadcast sb = entry.getValue();
if (sb.nextRunMillis <= now) { if (sb.nextRunMillis <= now) {
String timeStr = dateFormat.format(new Date(sb.nextRunMillis)); plugin.getLogger().info("[BroadcastModule] ⏰ Sende geplante Nachricht (ID: " + entry.getKey() + ")");
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); handleBroadcast(sb.sourceName, sb.message, sb.type, "", sb.prefix, sb.prefixColor, sb.bracketColor, sb.messageColor);
if (!"none".equalsIgnoreCase(sb.recur)) { if (!"none".equalsIgnoreCase(sb.recur)) {
long next = computeNextMillis(sb.nextRunMillis, sb.recur); long next = computeNextMillis(sb.nextRunMillis, sb.recur);
if (next > 0) { if (next > 0) { sb.nextRunMillis = next; changed = true; }
sb.nextRunMillis = next; else { toRemove.add(entry.getKey()); changed = true; }
String nextTimeStr = dateFormat.format(new Date(next)); } else { toRemove.add(entry.getKey()); changed = true; }
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()) { if (changed || !toRemove.isEmpty()) {
for (String k : toRemove) { for (String k : toRemove) { scheduledByClientId.remove(k); }
scheduledByClientId.remove(k);
plugin.getLogger().info("[BroadcastModule] Schedule entfernt: " + k);
}
saveSchedules(); saveSchedules();
} }
} }
@@ -355,14 +294,7 @@ public class BroadcastModule implements Module, Listener {
private static class ScheduledBroadcast { private static class ScheduledBroadcast {
final String clientId; final String clientId;
long nextRunMillis; long nextRunMillis;
String sourceName; String sourceName, message, type, prefix, prefixColor, bracketColor, messageColor, recur;
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, ScheduledBroadcast(String clientId, long nextRunMillis, String sourceName, String message, String type,
String prefix, String prefixColor, String bracketColor, String messageColor, String recur) { String prefix, String prefixColor, String bracketColor, String messageColor, String recur) {
@@ -373,9 +305,9 @@ public class BroadcastModule implements Module, Listener {
this.type = type; this.type = type;
this.prefix = prefix; this.prefix = prefix;
this.prefixColor = prefixColor; this.prefixColor = prefixColor;
this.bracketColor = bracketColor; // Neu this.bracketColor = bracketColor;
this.messageColor = messageColor; this.messageColor = messageColor;
this.recur = (recur == null ? "none" : recur); this.recur = recur == null ? "none" : recur;
} }
} }
} }

View File

@@ -304,7 +304,6 @@ public class AccountLinkManager {
} catch (IOException e) { } catch (IOException e) {
logger.warning("[ChatModule] Fehler beim Laden der Account-Links: " + e.getMessage()); logger.warning("[ChatModule] Fehler beim Laden der Account-Links: " + e.getMessage());
} }
logger.info("[ChatModule] " + links.size() + " Account-Verknüpfungen geladen.");
} }
private static String esc(String s) { private static String esc(String s) {

View File

@@ -11,120 +11,78 @@ import java.util.*;
/** /**
* Lädt und verwaltet die chat.yml Konfiguration. * Lädt und verwaltet die chat.yml Konfiguration.
*
* Fix #8: Rate-Limit-Werte aus anti-spam werden nicht mehr durch nachfolgende
* Berechnungen überschrieben. Der rate-limit.chat-Block hat jetzt Vorrang.
* Die Reihenfolge ist: erst rate-limit.chat einlesen, dann ggf. durch anti-spam
* als Fallback ergänzen, nicht umgekehrt.
*/ */
public class ChatConfig { public class ChatConfig {
private final Plugin plugin; private final Plugin plugin;
private Configuration config; private Configuration config;
// Geladene Kanäle
private final Map<String, ChatChannel> channels = new LinkedHashMap<>(); private final Map<String, ChatChannel> channels = new LinkedHashMap<>();
private String defaultChannel; private String defaultChannel;
// HelpOp private String helpopFormat, helpopPermission, helpopConfirm, helpopDiscordWebhook, helpopTelegramChatId;
private String helpopFormat;
private String helpopPermission;
private int helpopCooldown; private int helpopCooldown;
private String helpopConfirm;
private String helpopDiscordWebhook;
private String helpopTelegramChatId;
// Broadcast private String broadcastFormat, broadcastPermission;
private String broadcastFormat;
private String broadcastPermission;
// Private Messages
private boolean pmEnabled; private boolean pmEnabled;
private String pmFormatSender; private String pmFormatSender, pmFormatReceiver, pmFormatSpy, pmSpyPermission, pmRateLimitMessage;
private String pmFormatReceiver;
private String pmFormatSpy;
private String pmSpyPermission;
private boolean pmRateLimitEnabled; private boolean pmRateLimitEnabled;
private long pmRateLimitWindowMs; private long pmRateLimitWindowMs;
private int pmRateLimitMaxActions; private int pmRateLimitMaxActions;
private long pmRateLimitBlockMs; private long pmRateLimitBlockMs;
private String pmRateLimitMessage;
// Mute
private int defaultMuteDuration; private int defaultMuteDuration;
private String mutedMessage; private String mutedMessage;
// Emoji private boolean emojiEnabled, emojiBedrockSupport;
private boolean emojiEnabled;
private boolean emojiBedrockSupport;
private final Map<String, String> emojiMappings = new LinkedHashMap<>(); private final Map<String, String> emojiMappings = new LinkedHashMap<>();
// Discord
private boolean discordEnabled; private boolean discordEnabled;
private String discordBotToken; private String discordBotToken, discordGuildId, discordFromFormat, discordAdminChannelId, discordEmbedColor;
private String discordGuildId;
private int discordPollInterval; private int discordPollInterval;
private String discordFromFormat;
private String discordAdminChannelId;
private String discordEmbedColor;
// Telegram
private boolean telegramEnabled; private boolean telegramEnabled;
private String telegramBotToken; private String telegramBotToken, telegramFromFormat, telegramAdminChatId;
private int telegramPollInterval; private int telegramPollInterval, telegramChatTopicId, telegramAdminTopicId;
private String telegramFromFormat;
private String telegramAdminChatId;
private int telegramChatTopicId;
private int telegramAdminTopicId;
// Account-Linking
private boolean linkingEnabled; private boolean linkingEnabled;
private String linkDiscordMessage; private String linkDiscordMessage, linkTelegramMessage, linkSuccessDiscord, linkSuccessTelegram;
private String linkTelegramMessage; private String linkBotSuccessDiscord, linkBotSuccessTelegram, linkedDiscordFormat, linkedTelegramFormat;
private String linkSuccessDiscord;
private String linkSuccessTelegram;
private String linkBotSuccessDiscord;
private String linkBotSuccessTelegram;
private String linkedDiscordFormat;
private String linkedTelegramFormat;
private int telegramAdminThreadId; private int telegramAdminThreadId;
// Admin private String adminBypassPermission, adminNotifyPermission;
private String adminBypassPermission;
private String adminNotifyPermission;
// ===== NEU: Chatlog =====
private boolean chatlogEnabled; private boolean chatlogEnabled;
private int chatlogRetentionDays; private int chatlogRetentionDays;
// ===== NEU: Server-Farben =====
private final Map<String, String> serverColors = new LinkedHashMap<>(); private final Map<String, String> serverColors = new LinkedHashMap<>();
private final Map<String, String> serverDisplayNames = new LinkedHashMap<>(); private final Map<String, String> serverDisplayNames = new LinkedHashMap<>();
private String serverColorDefault; private String serverColorDefault;
// ===== NEU: Reports ===== private boolean reportsEnabled, reportWebhookEnabled;
private boolean reportsEnabled; private String reportConfirm, reportPermission, reportClosePermission, reportViewPermission;
private String reportConfirm; private String reportDiscordWebhook, reportTelegramChatId;
private int reportCooldown;
// ===== Chat-Filter =====
private ChatFilter.ChatFilterConfig filterConfig = new ChatFilter.ChatFilterConfig(); private ChatFilter.ChatFilterConfig filterConfig = new ChatFilter.ChatFilterConfig();
// ===== Mentions ===== private boolean mentionsEnabled, mentionsAllowToggle;
private boolean mentionsEnabled; private String mentionsHighlightColor, mentionsSound, mentionsNotifyPrefix;
private String mentionsHighlightColor;
private String mentionsSound;
private boolean mentionsAllowToggle;
private String mentionsNotifyPrefix;
// ===== Chat-History ===== private int historyMaxLines, historyDefaultLines;
private int historyMaxLines;
private int historyDefaultLines;
private String reportPermission;
private String reportClosePermission;
private String reportViewPermission;
private int reportCooldown;
private String reportDiscordWebhook;
private String reportTelegramChatId;
private boolean reportWebhookEnabled;
public ChatConfig(Plugin plugin) { private boolean joinLeaveEnabled, vanishShowToAdmins;
this.plugin = plugin; private String joinFormat, leaveFormat, vanishJoinFormat, vanishLeaveFormat;
} private String joinLeaveDiscordWebhook, joinLeaveTelegramChatId;
private int joinLeaveTelegramThreadId;
public ChatConfig(Plugin plugin) { this.plugin = plugin; }
public void load() { public void load() {
File file = new File(plugin.getDataFolder(), "chat.yml"); File file = new File(plugin.getDataFolder(), "chat.yml");
@@ -132,24 +90,18 @@ public class ChatConfig {
plugin.getDataFolder().mkdirs(); plugin.getDataFolder().mkdirs();
InputStream in = plugin.getResourceAsStream("chat.yml"); InputStream in = plugin.getResourceAsStream("chat.yml");
if (in != null) { if (in != null) {
try { try { Files.copy(in, file.toPath()); }
Files.copy(in, file.toPath()); catch (IOException e) { plugin.getLogger().severe("[ChatModule] Konnte chat.yml nicht erstellen: " + e.getMessage()); }
} catch (IOException e) {
plugin.getLogger().severe("[ChatModule] Konnte chat.yml nicht erstellen: " + e.getMessage());
}
} else { } else {
plugin.getLogger().warning("[ChatModule] chat.yml nicht in JAR gefunden, erstelle leere Datei."); plugin.getLogger().warning("[ChatModule] chat.yml nicht in JAR, erstelle leere Datei.");
try { file.createNewFile(); } catch (IOException ignored) {} try { file.createNewFile(); } catch (IOException ignored) {}
} }
} }
try { config = ConfigurationProvider.getProvider(YamlConfiguration.class).load(file); }
try { catch (IOException e) {
config = ConfigurationProvider.getProvider(YamlConfiguration.class).load(file);
} catch (IOException e) {
plugin.getLogger().severe("[ChatModule] Fehler beim Laden der chat.yml: " + e.getMessage()); plugin.getLogger().severe("[ChatModule] Fehler beim Laden der chat.yml: " + e.getMessage());
config = new Configuration(); config = new Configuration();
} }
parseConfig(); parseConfig();
plugin.getLogger().info("[ChatModule] " + channels.size() + " Kanäle geladen."); plugin.getLogger().info("[ChatModule] " + channels.size() + " Kanäle geladen.");
} }
@@ -180,14 +132,9 @@ public class ChatConfig {
)); ));
} }
} }
// Fallback: global-Kanal immer vorhanden
if (!channels.containsKey("global")) { if (!channels.containsKey("global")) {
channels.put("global", new ChatChannel( channels.put("global", new ChatChannel("global", "Global", "G", "", "&a",
"global", "Global", "G", "", "&a", "&8[&a{server}&8] {prefix}&r{player}{suffix}&8: &f{message}", false, "", "", "", 0, false));
"&8[&a{server}&8] {prefix}&r{player}{suffix}&8: &f{message}",
false, "", "", "", 0, false
));
} }
// --- HelpOp --- // --- HelpOp ---
@@ -199,31 +146,29 @@ public class ChatConfig {
helpopConfirm = ho.getString("confirm-message", "&aHilferuf gesendet!"); helpopConfirm = ho.getString("confirm-message", "&aHilferuf gesendet!");
helpopDiscordWebhook = ho.getString("discord-webhook", ""); helpopDiscordWebhook = ho.getString("discord-webhook", "");
helpopTelegramChatId = ho.getString("telegram-chat-id", ""); helpopTelegramChatId = ho.getString("telegram-chat-id", "");
} else {
helpopFormat = "&8[&eHELPOP&8] &f{player}&8@&7{server}&8: &e{message}";
helpopPermission = "chat.helpop.receive"; helpopCooldown = 30;
helpopConfirm = "&aHilferuf gesendet!"; helpopDiscordWebhook = ""; helpopTelegramChatId = "";
} }
// --- Broadcast --- // --- Broadcast ---
Configuration bc = config.getSection("broadcast"); Configuration bc = config.getSection("broadcast");
if (bc != null) { broadcastFormat = bc != null ? bc.getString("format", "&c[&6Broadcast&c] &e{message}") : "&c[&6Broadcast&c] &e{message}";
broadcastFormat = bc.getString("format", "&c[&6Broadcast&c] &e{message}"); broadcastPermission = bc != null ? bc.getString("permission", "chat.broadcast") : "chat.broadcast";
broadcastPermission = bc.getString("permission", "chat.broadcast");
}
// --- Private Messages --- // --- Private Messages ---
Configuration pm = config.getSection("private-messages"); Configuration pm = config.getSection("private-messages");
if (pm != null) { pmEnabled = pm == null || pm.getBoolean("enabled", true);
pmEnabled = pm.getBoolean("enabled", true); pmFormatSender = pm != null ? pm.getString("format-sender", "&8[&7Du &8→ &b{player}&8] &f{message}") : "&8[&7Du &8→ &b{player}&8] &f{message}";
pmFormatSender = pm.getString("format-sender", "&8[&7Du &8→ &b{player}&8] &f{message}"); pmFormatReceiver = pm != null ? pm.getString("format-receiver", "&8[&b{player} &8→ &7Dir&8] &f{message}") : "&8[&b{player} &8→ &7Dir&8] &f{message}";
pmFormatReceiver = pm.getString("format-receiver", "&8[&b{player} &8→ &7Dir&8] &f{message}"); pmFormatSpy = pm != null ? pm.getString("format-social-spy","&8[&dSPY &7{sender} &8→ &7{receiver}&8] &f{message}") : "&8[&dSPY &7{sender} &8→ &7{receiver}&8] &f{message}";
pmFormatSpy = pm.getString("format-social-spy","&8[&dSPY &7{sender} &8→ &7{receiver}&8] &f{message}"); pmSpyPermission = pm != null ? pm.getString("social-spy-permission", "chat.socialspy") : "chat.socialspy";
pmSpyPermission = pm.getString("social-spy-permission", "chat.socialspy");
}
// --- Mute --- // --- Mute ---
Configuration mu = config.getSection("mute"); Configuration mu = config.getSection("mute");
if (mu != null) { defaultMuteDuration = mu != null ? mu.getInt("default-duration-minutes", 60) : 60;
defaultMuteDuration = mu.getInt("default-duration-minutes", 60); mutedMessage = mu != null ? mu.getString("muted-message", "&cDu bist stummgeschaltet. Noch: &f{time}") : "&cDu bist stummgeschaltet. Noch: &f{time}";
mutedMessage = mu.getString("muted-message", "&cDu bist stummgeschaltet. Noch: &f{time}");
}
// --- Emoji --- // --- Emoji ---
Configuration em = config.getSection("emoji"); Configuration em = config.getSection("emoji");
@@ -232,12 +177,8 @@ public class ChatConfig {
emojiBedrockSupport = em.getBoolean("bedrock-support", true); emojiBedrockSupport = em.getBoolean("bedrock-support", true);
emojiMappings.clear(); emojiMappings.clear();
Configuration map = em.getSection("mappings"); Configuration map = em.getSection("mappings");
if (map != null) { if (map != null) { for (String key : map.getKeys()) emojiMappings.put(key, map.getString(key, key)); }
for (String key : map.getKeys()) { } else { emojiEnabled = true; emojiBedrockSupport = true; }
emojiMappings.put(key, map.getString(key, key));
}
}
}
// --- Discord --- // --- Discord ---
Configuration dc = config.getSection("discord"); Configuration dc = config.getSection("discord");
@@ -249,7 +190,7 @@ public class ChatConfig {
discordFromFormat = dc.getString("from-discord-format", "&9[Discord] &b{user}&8: &f{message}"); discordFromFormat = dc.getString("from-discord-format", "&9[Discord] &b{user}&8: &f{message}");
discordAdminChannelId = dc.getString("admin-channel-id", ""); discordAdminChannelId = dc.getString("admin-channel-id", "");
discordEmbedColor = dc.getString("embed-color", "5865F2"); discordEmbedColor = dc.getString("embed-color", "5865F2");
} } else { discordEnabled = false; discordBotToken = ""; discordGuildId = ""; discordPollInterval = 3; discordFromFormat = "&9[Discord] &b{user}&8: &f{message}"; discordAdminChannelId = ""; discordEmbedColor = "5865F2"; }
// --- Telegram --- // --- Telegram ---
Configuration tg = config.getSection("telegram"); Configuration tg = config.getSection("telegram");
@@ -261,33 +202,19 @@ public class ChatConfig {
telegramAdminChatId = tg.getString("admin-chat-id", ""); telegramAdminChatId = tg.getString("admin-chat-id", "");
telegramChatTopicId = tg.getInt("chat-topic-id", 0); telegramChatTopicId = tg.getInt("chat-topic-id", 0);
telegramAdminTopicId = tg.getInt("admin-topic-id", 0); telegramAdminTopicId = tg.getInt("admin-topic-id", 0);
} } else { telegramEnabled = false; telegramBotToken = ""; telegramPollInterval = 3; telegramFromFormat = "&3[Telegram] &b{user}&8: &f{message}"; telegramAdminChatId = ""; telegramChatTopicId = 0; telegramAdminTopicId = 0; }
// --- Account-Linking --- // --- Account-Linking ---
Configuration al = config.getSection("account-linking"); Configuration al = config.getSection("account-linking");
if (al != null) { linkingEnabled = al == null || al.getBoolean("enabled", true);
linkingEnabled = al.getBoolean("enabled", true); linkDiscordMessage = al != null ? al.getString("discord-link-message", "&aCode: &f{token}") : "&aCode: &f{token}";
linkDiscordMessage = al.getString("discord-link-message", "&aCode: &f{token}"); linkTelegramMessage = al != null ? al.getString("telegram-link-message", "&aCode: &f{token}") : "&aCode: &f{token}";
linkTelegramMessage = al.getString("telegram-link-message", "&aCode: &f{token}"); linkSuccessDiscord = al != null ? al.getString("success-discord", "&aDiscord verknüpft!") : "&aDiscord verknüpft!";
linkSuccessDiscord = al.getString("success-discord", "&aDiscord verknüpft!"); linkSuccessTelegram = al != null ? al.getString("success-telegram", "&aTelegram verknüpft!") : "&aTelegram verknüpft!";
linkSuccessTelegram = al.getString("success-telegram", "&aTelegram verknüpft!"); linkBotSuccessDiscord = al != null ? al.getString("bot-success-discord", "✅ Verknüpft: {player}") : "✅ Verknüpft: {player}";
linkBotSuccessDiscord = al.getString("bot-success-discord", "✅ Verknüpft: {player}"); linkBotSuccessTelegram = al != null ? al.getString("bot-success-telegram", "✅ Verknüpft: {player}") : "✅ Verknüpft: {player}";
linkBotSuccessTelegram = al.getString("bot-success-telegram", "✅ Verknüpft: {player}"); linkedDiscordFormat = al != null ? al.getString("linked-discord-format", "&9[&bDiscord&9] &f{player} &8(&7{user}&8)&8: &f{message}") : "&9[&bDiscord&9] &f{player} &8(&7{user}&8)&8: &f{message}";
linkedDiscordFormat = al.getString("linked-discord-format", linkedTelegramFormat = al != null ? al.getString("linked-telegram-format", "&3[&bTelegram&3] &f{player} &8(&7{user}&8)&8: &f{message}") : "&3[&bTelegram&3] &f{player} &8(&7{user}&8)&8: &f{message}";
"&9[&bDiscord&9] &f{player} &8(&7{user}&8)&8: &f{message}");
linkedTelegramFormat = al.getString("linked-telegram-format",
"&3[&bTelegram&3] &f{player} &8(&7{user}&8)&8: &f{message}");
} else {
linkingEnabled = true;
linkDiscordMessage = "&aCode: &f{token}";
linkTelegramMessage = "&aCode: &f{token}";
linkSuccessDiscord = "&aDiscord verknüpft!";
linkSuccessTelegram = "&aTelegram verknüpft!";
linkBotSuccessDiscord = "✅ Verknüpft: {player}";
linkBotSuccessTelegram = "✅ Verknüpft: {player}";
linkedDiscordFormat = "&9[&bDiscord&9] &f{player} &8(&7{user}&8)&8: &f{message}";
linkedTelegramFormat = "&3[&bTelegram&3] &f{player} &8(&7{user}&8)&8: &f{message}";
}
// --- Chat-Filter --- // --- Chat-Filter ---
filterConfig = new ChatFilter.ChatFilterConfig(); filterConfig = new ChatFilter.ChatFilterConfig();
@@ -299,9 +226,12 @@ public class ChatConfig {
filterConfig.spamCooldownMs = spam.getInt("cooldown-ms", 1500); filterConfig.spamCooldownMs = spam.getInt("cooldown-ms", 1500);
filterConfig.spamMaxMessages = spam.getInt("max-messages", 3); filterConfig.spamMaxMessages = spam.getInt("max-messages", 3);
filterConfig.spamMessage = spam.getString("message", "&cNicht so schnell!"); filterConfig.spamMessage = spam.getString("message", "&cNicht so schnell!");
// FIX #8: Fallback-Werte aus anti-spam werden NUR gesetzt wenn rate-limit.chat nicht
// konfiguriert ist. Wir setzen die Werte hier als Vorbelegung und überschreiben sie
// unten mit dem rate-limit.chat-Block wenn vorhanden.
filterConfig.globalRateLimitWindowMs = Math.max(500L, filterConfig.spamCooldownMs); filterConfig.globalRateLimitWindowMs = Math.max(500L, filterConfig.spamCooldownMs);
filterConfig.globalRateLimitMaxActions = Math.max(1, filterConfig.spamMaxMessages); filterConfig.globalRateLimitMaxActions = Math.max(1, filterConfig.spamMaxMessages);
filterConfig.globalRateLimitBlockMs = Math.max(2000L, filterConfig.spamCooldownMs * 2L); filterConfig.globalRateLimitBlockMs = Math.max(2000L, filterConfig.spamCooldownMs * 4L);
} }
Configuration dup = cf.getSection("duplicate-check"); Configuration dup = cf.getSection("duplicate-check");
if (dup != null) { if (dup != null) {
@@ -312,13 +242,14 @@ public class ChatConfig {
if (bl != null) { if (bl != null) {
filterConfig.blacklistEnabled = bl.getBoolean("enabled", true); filterConfig.blacklistEnabled = bl.getBoolean("enabled", true);
filterConfig.blacklistWords.clear(); filterConfig.blacklistWords.clear();
// words ist eine YAML-Liste, nicht eine Section → getList() verwenden loadFilterWords(filterConfig.blacklistWords);
try { try {
java.util.List<?> wordList = bl.getList("words"); java.util.List<?> wordList = bl.getList("words");
if (wordList != null) { if (wordList != null) {
for (Object o : wordList) { for (Object o : wordList) {
if (o != null && !o.toString().trim().isEmpty()) { if (o != null && !o.toString().trim().isEmpty()) {
filterConfig.blacklistWords.add(o.toString().trim()); String w = o.toString().trim();
if (!filterConfig.blacklistWords.contains(w)) filterConfig.blacklistWords.add(w);
} }
} }
} }
@@ -330,9 +261,18 @@ public class ChatConfig {
filterConfig.capsMinLength = caps.getInt("min-length", 6); filterConfig.capsMinLength = caps.getInt("min-length", 6);
filterConfig.capsMaxPercent = caps.getInt("max-percent", 70); filterConfig.capsMaxPercent = caps.getInt("max-percent", 70);
} }
Configuration antiAd = cf.getSection("anti-ad");
if (antiAd != null) {
filterConfig.antiAdEnabled = antiAd.getBoolean("enabled", true);
filterConfig.antiAdMessage = antiAd.getString("message", "&cWerbung ist nicht erlaubt!");
java.util.List<?> wl = antiAd.getList("whitelist");
if (wl != null) { filterConfig.antiAdWhitelist.clear(); for (Object o : wl) if (o != null) filterConfig.antiAdWhitelist.add(o.toString()); }
java.util.List<?> tlds = antiAd.getList("blocked-tlds");
if (tlds != null) { filterConfig.antiAdBlockedTlds.clear(); for (Object o : tlds) if (o != null) filterConfig.antiAdBlockedTlds.add(o.toString()); }
}
} }
// --- Globales Rate-Limit-Framework --- // --- Rate-Limit (FIX #8: dieser Block setzt die endgültigen Werte, hat Vorrang) ---
pmRateLimitEnabled = true; pmRateLimitEnabled = true;
pmRateLimitWindowMs = 5000L; pmRateLimitWindowMs = 5000L;
pmRateLimitMaxActions = 4; pmRateLimitMaxActions = 4;
@@ -343,13 +283,13 @@ public class ChatConfig {
if (rl != null) { if (rl != null) {
Configuration rlChat = rl.getSection("chat"); Configuration rlChat = rl.getSection("chat");
if (rlChat != null) { if (rlChat != null) {
// FIX #8: rate-limit.chat überschreibt die anti-spam-Fallbacks vollständig
filterConfig.globalRateLimitEnabled = rlChat.getBoolean("enabled", true); filterConfig.globalRateLimitEnabled = rlChat.getBoolean("enabled", true);
filterConfig.globalRateLimitWindowMs = rlChat.getLong("window-ms", filterConfig.globalRateLimitWindowMs); filterConfig.globalRateLimitWindowMs = rlChat.getLong("window-ms", filterConfig.globalRateLimitWindowMs);
filterConfig.globalRateLimitMaxActions = rlChat.getInt("max-actions", filterConfig.globalRateLimitMaxActions); filterConfig.globalRateLimitMaxActions = rlChat.getInt("max-actions", filterConfig.globalRateLimitMaxActions);
filterConfig.globalRateLimitBlockMs = rlChat.getLong("block-ms", filterConfig.globalRateLimitBlockMs); filterConfig.globalRateLimitBlockMs = rlChat.getLong("block-ms", filterConfig.globalRateLimitBlockMs);
filterConfig.spamMessage = rlChat.getString("message", filterConfig.spamMessage); filterConfig.spamMessage = rlChat.getString("message", filterConfig.spamMessage);
} }
Configuration rlPm = rl.getSection("private-messages"); Configuration rlPm = rl.getSection("private-messages");
if (rlPm != null) { if (rlPm != null) {
pmRateLimitEnabled = rlPm.getBoolean("enabled", pmRateLimitEnabled); pmRateLimitEnabled = rlPm.getBoolean("enabled", pmRateLimitEnabled);
@@ -362,120 +302,125 @@ public class ChatConfig {
// --- Mentions --- // --- Mentions ---
Configuration mn = config.getSection("mentions"); Configuration mn = config.getSection("mentions");
if (mn != null) { mentionsEnabled = mn == null || mn.getBoolean("enabled", true);
mentionsEnabled = mn.getBoolean("enabled", true); mentionsHighlightColor = mn != null ? mn.getString("highlight-color", "&e&l") : "&e&l";
mentionsHighlightColor = mn.getString("highlight-color", "&e&l"); mentionsSound = mn != null ? mn.getString("sound", "ENTITY_EXPERIENCE_ORB_PICKUP") : "ENTITY_EXPERIENCE_ORB_PICKUP";
mentionsSound = mn.getString("sound", "ENTITY_EXPERIENCE_ORB_PICKUP"); mentionsAllowToggle = mn == null || mn.getBoolean("allow-toggle", true);
mentionsAllowToggle = mn.getBoolean("allow-toggle", true); mentionsNotifyPrefix = mn != null ? mn.getString("notify-prefix", "&e&l[Mention] &r") : "&e&l[Mention] &r";
mentionsNotifyPrefix = mn.getString("notify-prefix", "&e&l[Mention] &r");
} else {
mentionsEnabled = true;
mentionsHighlightColor = "&e&l";
mentionsSound = "ENTITY_EXPERIENCE_ORB_PICKUP";
mentionsAllowToggle = true;
mentionsNotifyPrefix = "&e&l[Mention] &r";
}
// --- Chat-History --- // --- Chat-History ---
Configuration ch = config.getSection("chat-history"); Configuration ch = config.getSection("chat-history");
if (ch != null) { historyMaxLines = ch != null ? ch.getInt("max-lines", 50) : 50;
historyMaxLines = ch.getInt("max-lines", 50); historyDefaultLines = ch != null ? ch.getInt("default-lines", 10) : 10;
historyDefaultLines = ch.getInt("default-lines", 10);
} else {
historyMaxLines = 50;
historyDefaultLines = 10;
}
// --- Admin --- // --- Admin ---
Configuration adm = config.getSection("admin"); Configuration adm = config.getSection("admin");
if (adm != null) { adminBypassPermission = adm != null ? adm.getString("bypass-permission", "chat.admin.bypass") : "chat.admin.bypass";
adminBypassPermission = adm.getString("bypass-permission", "chat.admin.bypass"); adminNotifyPermission = adm != null ? adm.getString("notify-permission", "chat.admin.notify") : "chat.admin.notify";
adminNotifyPermission = adm.getString("notify-permission", "chat.admin.notify");
} else {
adminBypassPermission = "chat.admin.bypass";
adminNotifyPermission = "chat.admin.notify";
}
// --- Server-Farben --- // --- Server-Farben ---
serverColors.clear(); serverColors.clear(); serverDisplayNames.clear();
serverDisplayNames.clear();
Configuration sc = config.getSection("server-colors"); Configuration sc = config.getSection("server-colors");
if (sc != null) { if (sc != null) {
serverColorDefault = sc.getString("default", "&7"); serverColorDefault = sc.getString("default", "&7");
for (String key : sc.getKeys()) { for (String key : sc.getKeys()) {
if (key.equals("default")) continue; if (key.equals("default")) continue;
// Neues Format: server hat Untersektion mit color + display
Configuration sub = sc.getSection(key); Configuration sub = sc.getSection(key);
if (sub != null) { if (sub != null) {
serverColors.put(key.toLowerCase(), sub.getString("color", "&7")); serverColors.put(key.toLowerCase(), sub.getString("color", "&7"));
String display = sub.getString("display", ""); String display = sub.getString("display", "");
if (!display.isEmpty()) { if (!display.isEmpty()) serverDisplayNames.put(key.toLowerCase(), display);
serverDisplayNames.put(key.toLowerCase(), display);
}
} else { } else {
// Altes Format: server: "&a" (nur Farbe, kein display)
serverColors.put(key.toLowerCase(), sc.getString(key, "&7")); serverColors.put(key.toLowerCase(), sc.getString(key, "&7"));
} }
} }
} else { } else { serverColorDefault = "&7"; }
serverColorDefault = "&7";
}
// --- Chatlog (NEU) --- // --- Chatlog ---
Configuration cl = config.getSection("chatlog"); Configuration cl = config.getSection("chatlog");
if (cl != null) { chatlogEnabled = cl == null || cl.getBoolean("enabled", true);
chatlogEnabled = cl.getBoolean("enabled", true); int raw = cl != null ? cl.getInt("retention-days", 7) : 7;
// Nur 7 oder 14 erlaubt; Standardwert 7
int raw = cl.getInt("retention-days", 7);
chatlogRetentionDays = (raw == 14) ? 14 : 7; chatlogRetentionDays = (raw == 14) ? 14 : 7;
} else {
chatlogEnabled = true;
chatlogRetentionDays = 7;
}
// --- Reports (NEU) --- // --- Reports ---
Configuration rp = config.getSection("reports"); Configuration rp = config.getSection("reports");
if (rp != null) { if (rp != null) {
reportsEnabled = rp.getBoolean("enabled", true); reportsEnabled = rp.getBoolean("enabled", true);
reportWebhookEnabled = rp.getBoolean("webhook-enabled", false); reportWebhookEnabled = rp.getBoolean("webhook-enabled", false);
reportConfirm = rp.getString("confirm-message", reportConfirm = rp.getString("confirm-message", "&aDein Report &8({id}) &awurde eingereicht. Danke!");
"&aDein Report &8(§f{id}&8) &awurde eingereicht. Danke!"); reportPermission = rp.getString("report-permission", "");
reportPermission = rp.getString("report-permission", ""); // leer = jeder reportClosePermission = rp.getString("close-permission", "chat.admin.bypass");
reportClosePermission= rp.getString("close-permission", "chat.admin.bypass");
reportViewPermission = rp.getString("view-permission", "chat.admin.bypass"); reportViewPermission = rp.getString("view-permission", "chat.admin.bypass");
reportCooldown = rp.getInt("cooldown", 60); reportCooldown = rp.getInt("cooldown", 60);
reportDiscordWebhook = rp.getString("discord-webhook", ""); reportDiscordWebhook = rp.getString("discord-webhook", "");
reportTelegramChatId = rp.getString("telegram-chat-id", ""); reportTelegramChatId = rp.getString("telegram-chat-id", "");
} else { } else {
reportsEnabled = true; reportsEnabled = true; reportWebhookEnabled = false;
reportWebhookEnabled = false;
reportConfirm = "&aDein Report &8({id}) &awurde eingereicht. Danke!"; reportConfirm = "&aDein Report &8({id}) &awurde eingereicht. Danke!";
reportPermission = ""; reportPermission = ""; reportClosePermission = "chat.admin.bypass"; reportViewPermission = "chat.admin.bypass";
reportClosePermission = "chat.admin.bypass"; reportCooldown = 60; reportDiscordWebhook = ""; reportTelegramChatId = "";
reportViewPermission = "chat.admin.bypass"; }
reportCooldown = 60;
reportDiscordWebhook = ""; // --- Join / Leave ---
reportTelegramChatId = ""; Configuration jl = config.getSection("join-leave");
if (jl != null) {
joinLeaveEnabled = jl.getBoolean("enabled", true);
joinFormat = jl.getString("join-format", "&8[&a+&8] {prefix}&a{player}&r &7hat das Netzwerk betreten.");
leaveFormat = jl.getString("leave-format", "&8[&c-&8] {prefix}&c{player}&r &7hat das Netzwerk verlassen.");
vanishShowToAdmins = jl.getBoolean("vanish-show-to-admins", true);
vanishJoinFormat = jl.getString("vanish-join-format", "&8[&7+&8] &8{player} &7hat das Netzwerk betreten. &8(Vanish)");
vanishLeaveFormat = jl.getString("vanish-leave-format", "&8[&7-&8] &8{player} &7hat das Netzwerk verlassen. &8(Vanish)");
joinLeaveDiscordWebhook = jl.getString("discord-webhook", "");
joinLeaveTelegramChatId = jl.getString("telegram-chat-id", "");
joinLeaveTelegramThreadId = jl.getInt("telegram-thread-id", 0);
} else {
joinLeaveEnabled = true;
joinFormat = "&8[&a+&8] {prefix}&a{player}&r &7hat das Netzwerk betreten.";
leaveFormat = "&8[&c-&8] {prefix}&c{player}&r &7hat das Netzwerk verlassen.";
vanishShowToAdmins = true;
vanishJoinFormat = "&8[&7+&8] &8{player} &7hat das Netzwerk betreten. &8(Vanish)";
vanishLeaveFormat = "&8[&7-&8] &8{player} &7hat das Netzwerk verlassen. &8(Vanish)";
joinLeaveDiscordWebhook = ""; joinLeaveTelegramChatId = ""; joinLeaveTelegramThreadId = 0;
} }
} }
// ===== Getter (bestehend) ===== private void loadFilterWords(java.util.List<String> target) {
File filterFile = new File(plugin.getDataFolder(), "filter.yml");
if (!filterFile.exists()) {
try {
plugin.getDataFolder().mkdirs();
try (java.io.FileWriter fw = new java.io.FileWriter(filterFile)) {
fw.write("# StatusAPI - Wort-Blacklist\n# words:\n# - beispielwort\nwords:\n");
}
plugin.getLogger().info("[ChatModule] filter.yml erstellt.");
} catch (IOException e) { plugin.getLogger().warning("[ChatModule] Konnte filter.yml nicht erstellen: " + e.getMessage()); }
return;
}
try {
Configuration fc = ConfigurationProvider.getProvider(YamlConfiguration.class).load(filterFile);
java.util.List<?> words = fc.getList("words");
if (words != null) {
for (Object o : words) {
if (o != null && !o.toString().trim().isEmpty()) target.add(o.toString().trim().toLowerCase());
}
}
} catch (Exception e) { plugin.getLogger().warning("[ChatModule] Fehler beim Laden der filter.yml: " + e.getMessage()); }
}
// ===== Getter =====
public Map<String, ChatChannel> getChannels() { return Collections.unmodifiableMap(channels); } public Map<String, ChatChannel> getChannels() { return Collections.unmodifiableMap(channels); }
public ChatChannel getChannel(String id) { return channels.get(id == null ? defaultChannel : id.toLowerCase()); } public ChatChannel getChannel(String id) { return channels.get(id == null ? defaultChannel : id.toLowerCase()); }
public ChatChannel getDefaultChannel() { return channels.getOrDefault(defaultChannel, channels.values().iterator().next()); } public ChatChannel getDefaultChannel() { return channels.getOrDefault(defaultChannel, channels.values().iterator().next()); }
public String getDefaultChannelId() { return defaultChannel; } public String getDefaultChannelId() { return defaultChannel; }
public String getHelpopFormat() { return helpopFormat; } public String getHelpopFormat() { return helpopFormat; }
public String getHelpopPermission() { return helpopPermission; } public String getHelpopPermission() { return helpopPermission; }
public int getHelpopCooldown() { return helpopCooldown; } public int getHelpopCooldown() { return helpopCooldown; }
public String getHelpopConfirm() { return helpopConfirm; } public String getHelpopConfirm() { return helpopConfirm; }
public String getHelpopDiscordWebhook() { return helpopDiscordWebhook; } public String getHelpopDiscordWebhook() { return helpopDiscordWebhook; }
public String getHelpopTelegramChatId() { return helpopTelegramChatId; } public String getHelpopTelegramChatId() { return helpopTelegramChatId; }
public String getBroadcastFormat() { return broadcastFormat; } public String getBroadcastFormat() { return broadcastFormat; }
public String getBroadcastPermission() { return broadcastPermission; } public String getBroadcastPermission() { return broadcastPermission; }
public boolean isPmEnabled() { return pmEnabled; } public boolean isPmEnabled() { return pmEnabled; }
public String getPmFormatSender() { return pmFormatSender; } public String getPmFormatSender() { return pmFormatSender; }
public String getPmFormatReceiver() { return pmFormatReceiver; } public String getPmFormatReceiver() { return pmFormatReceiver; }
@@ -486,14 +431,11 @@ public class ChatConfig {
public int getPmRateLimitMaxActions() { return pmRateLimitMaxActions; } public int getPmRateLimitMaxActions() { return pmRateLimitMaxActions; }
public long getPmRateLimitBlockMs() { return pmRateLimitBlockMs; } public long getPmRateLimitBlockMs() { return pmRateLimitBlockMs; }
public String getPmRateLimitMessage() { return pmRateLimitMessage; } public String getPmRateLimitMessage() { return pmRateLimitMessage; }
public int getDefaultMuteDuration() { return defaultMuteDuration; } public int getDefaultMuteDuration() { return defaultMuteDuration; }
public String getMutedMessage() { return mutedMessage; } public String getMutedMessage() { return mutedMessage; }
public boolean isEmojiEnabled() { return emojiEnabled; } public boolean isEmojiEnabled() { return emojiEnabled; }
public boolean isEmojiBedrockSupport() { return emojiBedrockSupport; } public boolean isEmojiBedrockSupport() { return emojiBedrockSupport; }
public Map<String, String> getEmojiMappings() { return Collections.unmodifiableMap(emojiMappings); } public Map<String, String> getEmojiMappings() { return Collections.unmodifiableMap(emojiMappings); }
public boolean isDiscordEnabled() { return discordEnabled; } public boolean isDiscordEnabled() { return discordEnabled; }
public String getDiscordBotToken() { return discordBotToken; } public String getDiscordBotToken() { return discordBotToken; }
public String getDiscordGuildId() { return discordGuildId; } public String getDiscordGuildId() { return discordGuildId; }
@@ -501,7 +443,6 @@ public class ChatConfig {
public String getDiscordFromFormat() { return discordFromFormat; } public String getDiscordFromFormat() { return discordFromFormat; }
public String getDiscordAdminChannelId() { return discordAdminChannelId; } public String getDiscordAdminChannelId() { return discordAdminChannelId; }
public String getDiscordEmbedColor() { return discordEmbedColor; } public String getDiscordEmbedColor() { return discordEmbedColor; }
public boolean isTelegramEnabled() { return telegramEnabled; } public boolean isTelegramEnabled() { return telegramEnabled; }
public String getTelegramBotToken() { return telegramBotToken; } public String getTelegramBotToken() { return telegramBotToken; }
public int getTelegramPollInterval() { return telegramPollInterval; } public int getTelegramPollInterval() { return telegramPollInterval; }
@@ -509,8 +450,6 @@ public class ChatConfig {
public String getTelegramAdminChatId() { return telegramAdminChatId; } public String getTelegramAdminChatId() { return telegramAdminChatId; }
public int getTelegramChatTopicId() { return telegramChatTopicId; } public int getTelegramChatTopicId() { return telegramChatTopicId; }
public int getTelegramAdminTopicId() { return telegramAdminTopicId; } public int getTelegramAdminTopicId() { return telegramAdminTopicId; }
// ===== Getter (Account-Linking) =====
public boolean isLinkingEnabled() { return linkingEnabled; } public boolean isLinkingEnabled() { return linkingEnabled; }
public String getLinkDiscordMessage() { return linkDiscordMessage; } public String getLinkDiscordMessage() { return linkDiscordMessage; }
public String getLinkTelegramMessage() { return linkTelegramMessage; } public String getLinkTelegramMessage() { return linkTelegramMessage; }
@@ -520,43 +459,14 @@ public class ChatConfig {
public String getLinkBotSuccessTelegram() { return linkBotSuccessTelegram; } public String getLinkBotSuccessTelegram() { return linkBotSuccessTelegram; }
public String getLinkedDiscordFormat() { return linkedDiscordFormat; } public String getLinkedDiscordFormat() { return linkedDiscordFormat; }
public String getLinkedTelegramFormat() { return linkedTelegramFormat; } public String getLinkedTelegramFormat() { return linkedTelegramFormat; }
public String getAdminBypassPermission() { return adminBypassPermission; } public String getAdminBypassPermission() { return adminBypassPermission; }
public String getAdminNotifyPermission() { return adminNotifyPermission; } public String getAdminNotifyPermission() { return adminNotifyPermission; }
public String getServerColor(String serverName) { if (serverName == null) return serverColorDefault; String c = serverColors.get(serverName.toLowerCase()); return c != null ? c : serverColorDefault; }
// ===== Getter (NEU: Server-Farben) =====
/**
* Gibt den konfigurierten Farb-Code für einen Server zurück.
* Unterstützt &-Codes und &#RRGGBB HEX-Codes.
* Fallback: "default"-Eintrag, dann "&7".
*/
public String getServerColor(String serverName) {
if (serverName == null) return serverColorDefault;
String color = serverColors.get(serverName.toLowerCase());
return color != null ? color : serverColorDefault;
}
public Map<String, String> getServerColors() { return Collections.unmodifiableMap(serverColors); } public Map<String, String> getServerColors() { return Collections.unmodifiableMap(serverColors); }
public String getServerColorDefault() { return serverColorDefault; } public String getServerColorDefault() { return serverColorDefault; }
public String getServerDisplay(String serverName) { if (serverName == null) return ""; String d = serverDisplayNames.get(serverName.toLowerCase()); return d != null ? d : serverName; }
/**
* Gibt den konfigurierten Anzeigenamen für einen Server zurück.
* Fallback: echter Servername (unverändert).
*/
public String getServerDisplay(String serverName) {
if (serverName == null) return "";
String display = serverDisplayNames.get(serverName.toLowerCase());
return display != null ? display : serverName;
}
// ===== Getter (NEU: Chatlog) =====
public boolean isChatlogEnabled() { return chatlogEnabled; } public boolean isChatlogEnabled() { return chatlogEnabled; }
public int getChatlogRetentionDays() { return chatlogRetentionDays; } public int getChatlogRetentionDays() { return chatlogRetentionDays; }
// ===== Getter (NEU: Reports) =====
public boolean isReportsEnabled() { return reportsEnabled; } public boolean isReportsEnabled() { return reportsEnabled; }
public String getReportConfirm() { return reportConfirm; } public String getReportConfirm() { return reportConfirm; }
public String getReportPermission() { return reportPermission; } public String getReportPermission() { return reportPermission; }
@@ -566,18 +476,21 @@ public class ChatConfig {
public String getReportDiscordWebhook() { return reportDiscordWebhook; } public String getReportDiscordWebhook() { return reportDiscordWebhook; }
public String getReportTelegramChatId() { return reportTelegramChatId; } public String getReportTelegramChatId() { return reportTelegramChatId; }
public boolean isReportWebhookEnabled() { return reportWebhookEnabled; } public boolean isReportWebhookEnabled() { return reportWebhookEnabled; }
// ===== Getter (Chat-Filter) =====
public ChatFilter.ChatFilterConfig getFilterConfig() { return filterConfig; } public ChatFilter.ChatFilterConfig getFilterConfig() { return filterConfig; }
// ===== Getter (Mentions) =====
public boolean isMentionsEnabled() { return mentionsEnabled; } public boolean isMentionsEnabled() { return mentionsEnabled; }
public String getMentionsHighlightColor() { return mentionsHighlightColor; } public String getMentionsHighlightColor() { return mentionsHighlightColor; }
public String getMentionsSound() { return mentionsSound; } public String getMentionsSound() { return mentionsSound; }
public boolean isMentionsAllowToggle() { return mentionsAllowToggle; } public boolean isMentionsAllowToggle() { return mentionsAllowToggle; }
public String getMentionsNotifyPrefix() { return mentionsNotifyPrefix; } public String getMentionsNotifyPrefix() { return mentionsNotifyPrefix; }
// ===== Getter (Chat-History) =====
public int getHistoryMaxLines() { return historyMaxLines; } public int getHistoryMaxLines() { return historyMaxLines; }
public int getHistoryDefaultLines() { return historyDefaultLines; } public int getHistoryDefaultLines() { return historyDefaultLines; }
public boolean isJoinLeaveEnabled() { return joinLeaveEnabled; }
public String getJoinFormat() { return joinFormat; }
public String getLeaveFormat() { return leaveFormat; }
public boolean isVanishShowToAdmins() { return vanishShowToAdmins; }
public String getVanishJoinFormat() { return vanishJoinFormat; }
public String getVanishLeaveFormat() { return vanishLeaveFormat; }
public String getJoinLeaveDiscordWebhook() { return joinLeaveDiscordWebhook; }
public String getJoinLeaveTelegramChatId() { return joinLeaveTelegramChatId; }
public int getJoinLeaveTelegramThreadId() { return joinLeaveTelegramThreadId; }
} }

View File

@@ -134,6 +134,13 @@ public class ChatFilter {
} }
} }
// ── 6. Anti-Werbung ──
if (cfg.antiAdEnabled && !isAdmin) {
if (containsAdvertisement(result)) {
return new FilterResponse(FilterResult.BLOCKED, result, cfg.antiAdMessage);
}
}
return new FilterResponse( return new FilterResponse(
modified ? FilterResult.MODIFIED : FilterResult.ALLOWED, modified ? FilterResult.MODIFIED : FilterResult.ALLOWED,
result, result,
@@ -187,14 +194,88 @@ public class ChatFilter {
// Kein Recht → & und nächstes Zeichen überspringen // Kein Recht → & und nächstes Zeichen überspringen
if (isColor || isFormat) { i++; continue; } if (isColor || isFormat) { i++; continue; }
// Hex: &# + 6 Zeichen überspringen // Hex: &# + 6 Zeichen überspringen (i zeigt auf &, +1 = #, +2..+7 = RRGGBB)
if (isHex && i + 7 < message.length()) { i += 7; continue; } if (isHex && i + 7 <= message.length()) { i += 7; continue; }
} }
sb.append(c); sb.append(c);
} }
return sb.toString(); return sb.toString();
} }
// ===== Anti-Werbung =====
// Vorkompilierte Patterns (einmalig beim Classload)
private static final Pattern PATTERN_IP =
Pattern.compile("\\b(\\d{1,3}[.,]){3}\\d{1,3}(:\\d{1,5})?\\b");
private static final Pattern PATTERN_DOMAIN_GENERIC =
Pattern.compile("(?i)\\b[a-z0-9-]{2,63}\\.[a-z]{2,10}(?:[/:\\d]\\S*)?\\b");
private static final Pattern PATTERN_URL_PREFIX =
Pattern.compile("(?i)(https?://|www\\.)\\S+");
/**
* Prüft ob die Nachricht Werbung enthält (IP, URL, fremde Domain).
* Domains auf der Whitelist werden ignoriert.
*
* Erkennt:
* - http:// / https:// / www. Prefixe
* - IPv4-Adressen (auch mit Port)
* - Domain-Namen mit konfigurierten TLDs (z.B. .net, .de, .com)
* - Verschleierungsversuche mit Leerzeichen um Punkte ("play . server . net")
*/
private boolean containsAdvertisement(String message) {
// Normalisierung: "play . server . net" → "play.server.net"
String normalized = message.replaceAll("\\s*\\.\\s*", ".");
// 1. Explizite URL-Prefixe
if (PATTERN_URL_PREFIX.matcher(normalized).find()) {
return !allMatchesWhitelisted(normalized, PATTERN_URL_PREFIX);
}
// 2. IP-Adressen (werden nie whitelisted)
if (PATTERN_IP.matcher(normalized).find()) {
return true;
}
// 3. Domains mit bekannten TLDs
if (!cfg.antiAdBlockedTlds.isEmpty()) {
java.util.regex.Matcher m = PATTERN_DOMAIN_GENERIC.matcher(normalized);
while (m.find()) {
String match = m.group();
String tld = extractTld(match);
if (cfg.antiAdBlockedTlds.contains(tld.toLowerCase())) {
if (!isOnWhitelist(match)) return true;
}
}
}
return false;
}
/** true wenn ALLE Treffer des Patterns auf der Whitelist stehen. */
private boolean allMatchesWhitelisted(String message, Pattern pattern) {
java.util.regex.Matcher m = pattern.matcher(message);
while (m.find()) {
if (!isOnWhitelist(m.group())) return false;
}
return true;
}
private boolean isOnWhitelist(String match) {
String lower = match.toLowerCase();
for (String entry : cfg.antiAdWhitelist) {
if (lower.contains(entry.toLowerCase())) return true;
}
return false;
}
private static String extractTld(String domain) {
String clean = domain.split("[/:]")[0];
int dot = clean.lastIndexOf('.');
return dot >= 0 ? clean.substring(dot + 1) : "";
}
private static String buildStars(int length) { private static String buildStars(int length) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
for (int i = 0; i < Math.max(length, 4); i++) sb.append('*'); for (int i = 0; i < Math.max(length, 4); i++) sb.append('*');
@@ -235,5 +316,16 @@ public class ChatFilter {
public boolean capsFilterEnabled = true; public boolean capsFilterEnabled = true;
public int capsMinLength = 6; // Mindestlänge für Caps-Check public int capsMinLength = 6; // Mindestlänge für Caps-Check
public int capsMaxPercent = 70; // Max. % Großbuchstaben public int capsMaxPercent = 70; // Max. % Großbuchstaben
// Anti-Werbung
public boolean antiAdEnabled = true;
public String antiAdMessage = "&cWerbung ist in diesem Chat nicht erlaubt!";
// Domains/Substrings die NICHT geblockt werden (z.B. eigene Serveradresse)
public List<String> antiAdWhitelist = new ArrayList<>();
// TLDs die als Werbung gewertet werden (leer = alle TLDs prüfen)
public List<String> antiAdBlockedTlds = new ArrayList<>(Arrays.asList(
"net", "com", "de", "org", "gg", "io", "eu", "tv", "xyz",
"info", "me", "cc", "co", "app", "online", "site", "fun"
));
} }
} }

View File

@@ -43,6 +43,8 @@ import java.util.logging.Logger;
* ✅ Report-System (/report, /reports, /reportclose) * ✅ Report-System (/report, /reports, /reportclose)
* ✅ Admin-Benachrichtigung bei Reports (sofort oder verzögert nach Login) * ✅ Admin-Benachrichtigung bei Reports (sofort oder verzögert nach Login)
* ✅ Server-Farben pro Server (&-Codes und &#RRGGBB HEX) * ✅ Server-Farben pro Server (&-Codes und &#RRGGBB HEX)
* ✅ Join / Leave Nachrichten (mit Vanish-Support)
* ✅ BungeeCord-Vanish-Integration via VanishProvider
*/ */
public class ChatModule implements Module, Listener { public class ChatModule implements Module, Listener {
@@ -74,10 +76,13 @@ public class ChatModule implements Module, Listener {
private final Map<UUID, Long> helpopCooldowns = new ConcurrentHashMap<>(); private final Map<UUID, Long> helpopCooldowns = new ConcurrentHashMap<>();
// Report-Cooldown: UUID → letzter Report-Zeitstempel (Sekunden) // Report-Cooldown: UUID → letzter Report-Zeitstempel (Sekunden)
private final Map<UUID, Long> reportCooldowns = new ConcurrentHashMap<>(); // NEU private final Map<UUID, Long> reportCooldowns = new ConcurrentHashMap<>();
// Letzte Chatnachricht pro Spieler (für Report-Kontext): name.toLowerCase() → message // Letzte Chatnachricht pro Spieler (für Report-Kontext): name.toLowerCase() → message
private final Map<String, String> lastChatMessages = new ConcurrentHashMap<>(); // NEU private final Map<String, String> lastChatMessages = new ConcurrentHashMap<>();
// UUIDs die gerade auf Plugin-Chat-Eingabe warten
private final Set<UUID> awaitingInput = Collections.newSetFromMap(new ConcurrentHashMap<>());
// Geyser-Präfix für Bedrock-Spieler (Standard: ".") // Geyser-Präfix für Bedrock-Spieler (Standard: ".")
private static final String GEYSER_PREFIX = "."; private static final String GEYSER_PREFIX = ".";
@@ -105,13 +110,13 @@ public class ChatModule implements Module, Listener {
emojiParser = new EmojiParser(config.getEmojiMappings(), config.isEmojiEnabled()); emojiParser = new EmojiParser(config.getEmojiMappings(), config.isEmojiEnabled());
chatFilter = new ChatFilter(config.getFilterConfig()); chatFilter = new ChatFilter(config.getFilterConfig());
// NEU: ChatLogger // ChatLogger
if (config.isChatlogEnabled()) { if (config.isChatlogEnabled()) {
chatLogger = new ChatLogger(plugin.getDataFolder(), logger, config.getChatlogRetentionDays()); chatLogger = new ChatLogger(plugin.getDataFolder(), logger, config.getChatlogRetentionDays());
logger.info("[ChatModule] Chat-Log aktiviert (" + config.getChatlogRetentionDays() + " Tage Aufbewahrung)."); logger.info("[ChatModule] Chat-Log aktiviert (" + config.getChatlogRetentionDays() + " Tage Aufbewahrung).");
} }
// NEU: ReportManager // ReportManager
if (config.isReportsEnabled()) { if (config.isReportsEnabled()) {
reportManager = new ReportManager(plugin.getDataFolder(), logger); reportManager = new ReportManager(plugin.getDataFolder(), logger);
reportManager.load(); reportManager.load();
@@ -155,7 +160,6 @@ public class ChatModule implements Module, Listener {
helpopCooldowns.clear(); helpopCooldowns.clear();
reportCooldowns.clear(); reportCooldowns.clear();
lastChatMessages.clear(); lastChatMessages.clear();
logger.info("[ChatModule] Deaktiviert.");
} }
// ========================================================= // =========================================================
@@ -182,11 +186,6 @@ public class ChatModule implements Module, Listener {
if (!(e.getSender() instanceof ProxiedPlayer)) return; if (!(e.getSender() instanceof ProxiedPlayer)) return;
ProxiedPlayer player = (ProxiedPlayer) e.getSender(); ProxiedPlayer player = (ProxiedPlayer) e.getSender();
// Bypass: Spieler wartet auf Plugin-Eingabe (CMI etc.)
// Event komplett unberührt lassen → Originalnachricht mit Signatur
// geht direkt zum Sub-Server. Funktioniert auf Spigot ohne Einschränkung.
// Auf Paper-Sub-Servern muss reject-chat-unsigned: false gesetzt sein —
// das ist eine Paper-Limitierung, nicht lösbar auf BungeeCord-Ebene.
if (awaitingInput.contains(player.getUniqueId())) { if (awaitingInput.contains(player.getUniqueId())) {
awaitingInput.remove(player.getUniqueId()); awaitingInput.remove(player.getUniqueId());
return; // Event NICHT cancellen → Nachricht geht mit Originalsignatur durch return; // Event NICHT cancellen → Nachricht geht mit Originalsignatur durch
@@ -198,7 +197,6 @@ public class ChatModule implements Module, Listener {
/** /**
* Zentrale Chat-Verarbeitungslogik. * Zentrale Chat-Verarbeitungslogik.
* Wird von beiden Event-Handlern aufgerufen.
*/ */
private void processChat(ProxiedPlayer player, String rawMessage) { private void processChat(ProxiedPlayer player, String rawMessage) {
if (rawMessage == null || rawMessage.trim().isEmpty()) return; if (rawMessage == null || rawMessage.trim().isEmpty()) return;
@@ -236,7 +234,7 @@ public class ChatModule implements Module, Listener {
player.sendMessage(color(filterResp.denyReason)); player.sendMessage(color(filterResp.denyReason));
return; return;
} }
message = filterResp.message; // ggf. modifiziert (Caps, Blacklist) message = filterResp.message;
String serverName = player.getServer() != null String serverName = player.getServer() != null
? player.getServer().getInfo().getName() ? player.getServer().getInfo().getName()
@@ -259,7 +257,6 @@ public class ChatModule implements Module, Listener {
ProxiedPlayer mentioned = ProxyServer.getInstance().getPlayer(targetName); ProxiedPlayer mentioned = ProxyServer.getInstance().getPlayer(targetName);
if (mentioned != null && !mentioned.getUniqueId().equals(uuid)) { if (mentioned != null && !mentioned.getUniqueId().equals(uuid)) {
mentionedPlayers.add(mentioned.getUniqueId()); mentionedPlayers.add(mentioned.getUniqueId());
// Wort hervorheben
word = translateColors(highlightColor + word + "&r"); word = translateColors(highlightColor + word + "&r");
} }
} }
@@ -316,10 +313,8 @@ public class ChatModule implements Module, Listener {
&& !mentionsDisabled.contains(recipient.getUniqueId()); && !mentionsDisabled.contains(recipient.getUniqueId());
if (isMentioned) { if (isMentioned) {
// Prefix-Nachricht über der Chat-Zeile
recipient.sendMessage(color(config.getMentionsNotifyPrefix() recipient.sendMessage(color(config.getMentionsNotifyPrefix()
+ "&7" + finalSenderName + " &7hat dich erwähnt!")); + "&7" + finalSenderName + " &7hat dich erwähnt!"));
// Sound via Plugin-Messaging an Sub-Server senden
sendMentionSound(recipient, config.getMentionsSound()); sendMentionSound(recipient, config.getMentionsSound());
} }
@@ -335,44 +330,147 @@ public class ChatModule implements Module, Listener {
}); });
} }
@EventHandler
public void onDisconnect(PlayerDisconnectEvent e) {
UUID uuid = e.getPlayer().getUniqueId();
chatFilter.cleanup(uuid);
playerChannels.remove(uuid);
mentionsDisabled.remove(uuid);
awaitingInput.remove(uuid);
}
// ========================================================= // =========================================================
// LOGIN-EVENT: Kanal setzen + Report-Benachrichtigung // LOGIN-EVENT: Kanal setzen + Join-Nachricht + Report-Info
// ========================================================= // =========================================================
@EventHandler @EventHandler
public void onLogin(PostLoginEvent e) { public void onLogin(PostLoginEvent e) {
ProxiedPlayer player = e.getPlayer(); ProxiedPlayer player = e.getPlayer();
playerChannels.put(player.getUniqueId(), config.getDefaultChannelId()); UUID uuid = player.getUniqueId();
// NEU: Offene Reports nach 2 Sekunden anzeigen (damit Update-Meldungen nicht überlagert werden) // Standard-Kanal setzen
if (reportManager == null) return; playerChannels.put(uuid, config.getDefaultChannelId());
if (!player.hasPermission(config.getAdminNotifyPermission())
&& !player.hasPermission(config.getAdminBypassPermission())) return;
int openCount = reportManager.getOpenCount();
if (openCount == 0) return;
// Join-Nachricht und Report-Benachrichtigung mit kurzem Delay senden,
// damit alle anderen Proxy-Initialisierungen (inkl. VanishModule) abgeschlossen sind.
// 2s statt 1s: VanishModule markiert den Spieler oft erst beim ServerConnectedEvent.
plugin.getProxy().getScheduler().schedule(plugin, () -> { plugin.getProxy().getScheduler().schedule(plugin, () -> {
if (!player.isConnected()) return; if (!player.isConnected()) return;
int count = reportManager.getOpenCount();
if (count == 0) return;
// ── Join-Nachricht ──
if (config.isJoinLeaveEnabled()) {
broadcastJoinLeave(player, true);
}
// ── Offene Reports für Admins anzeigen ──
if (reportManager != null
&& (player.hasPermission(config.getAdminNotifyPermission())
|| player.hasPermission(config.getAdminBypassPermission()))) {
int count = reportManager.getOpenCount();
if (count > 0) {
player.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); player.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
player.sendMessage(color("&c&l⚑ OFFENE REPORTS &8| &f" + count + " ausstehend")); player.sendMessage(color("&c&l⚑ OFFENE REPORTS &8| &f" + count + " ausstehend"));
player.sendMessage(color("&7Tippe &f/reports &7für eine Übersicht.")); player.sendMessage(color("&7Tippe &f/reports &7für eine Übersicht."));
player.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); player.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
}
}
}, 2, TimeUnit.SECONDS); }, 2, TimeUnit.SECONDS);
} }
// =========================================================
// DISCONNECT-EVENT: Cleanup + Leave-Nachricht
// =========================================================
@EventHandler
public void onDisconnect(PlayerDisconnectEvent e) {
ProxiedPlayer player = e.getPlayer();
UUID uuid = player.getUniqueId();
// Leave-Nachricht
if (config.isJoinLeaveEnabled()) {
broadcastJoinLeave(player, false);
}
// Cleanup
chatFilter.cleanup(uuid);
playerChannels.remove(uuid);
mentionsDisabled.remove(uuid);
awaitingInput.remove(uuid);
VanishProvider.cleanup(uuid); // Vanish-Status bereinigen
}
// =========================================================
// JOIN / LEAVE NACHRICHTEN (mit Vanish-Support)
// =========================================================
/**
* Sendet eine Join- oder Leave-Nachricht an alle Spieler.
*
* Vanish-Logik:
* - Unsichtbare Spieler: kein Broadcast an normale Spieler.
* - Ist vanish-show-to-admins=true, erhalten Admins (bypass-permission)
* eine dezente Vanish-Benachrichtigung.
*
* @param player Der betroffene Spieler
* @param isJoin true = Join, false = Leave
*/
private void broadcastJoinLeave(ProxiedPlayer player, boolean isJoin) {
boolean isVanished = VanishProvider.isVanished(player);
String prefix = getLuckPermsPrefix(player);
String suffix = getLuckPermsSuffix(player);
String server = (player.getServer() != null)
? config.getServerDisplay(player.getServer().getInfo().getName())
: "Netzwerk";
String normalFormat = isJoin ? config.getJoinFormat() : config.getLeaveFormat();
String vanishFormat = isJoin ? config.getVanishJoinFormat() : config.getVanishLeaveFormat();
// Platzhalter ersetzen
String normalMsg = normalFormat
.replace("{player}", player.getName())
.replace("{prefix}", prefix != null ? prefix : "")
.replace("{suffix}", suffix != null ? suffix : "")
.replace("{server}", server);
String vanishMsg = vanishFormat
.replace("{player}", player.getName())
.replace("{prefix}", prefix != null ? prefix : "")
.replace("{suffix}", suffix != null ? suffix : "")
.replace("{server}", server);
for (ProxiedPlayer recipient : ProxyServer.getInstance().getPlayers()) {
// Spieler sieht seine eigene Join-/Leave-Meldung nie
if (recipient.getUniqueId().equals(player.getUniqueId())) continue;
// Admin = bypass-permission ODER notify-permission
boolean recipientIsAdmin = recipient.hasPermission(config.getAdminBypassPermission())
|| recipient.hasPermission(config.getAdminNotifyPermission());
if (isVanished) {
// Vanished: Nur Admins sehen eine dezente Meldung (wenn konfiguriert)
if (recipientIsAdmin && config.isVanishShowToAdmins()) {
recipient.sendMessage(color(vanishMsg));
}
} else {
// Normaler Spieler: alle erhalten die Nachricht
// Vanished Admins sehen Join/Leave-Events anderer Spieler normal
recipient.sendMessage(color(normalMsg));
}
}
// Konsole immer informieren
String logMsg = isVanished
? "[ChatModule] " + (isJoin ? "JOIN(V)" : "LEAVE(V)") + " " + player.getName()
: "[ChatModule] " + (isJoin ? "JOIN" : "LEAVE") + " " + player.getName();
ProxyServer.getInstance().getConsole().sendMessage(color(
isVanished ? "&8" + logMsg : "&7" + logMsg));
// Brücken (nur für nicht-vanished Spieler)
if (!isVanished) {
String cleanMsg = ChatColor.stripColor(ChatColor.translateAlternateColorCodes('&', normalMsg));
if (discordBridge != null && !config.getJoinLeaveDiscordWebhook().isEmpty()) {
discordBridge.sendToDiscord(config.getJoinLeaveDiscordWebhook(),
player.getName(), cleanMsg, null);
}
if (telegramBridge != null && !config.getJoinLeaveTelegramChatId().isEmpty()) {
telegramBridge.sendToTelegram(config.getJoinLeaveTelegramChatId(),
config.getJoinLeaveTelegramThreadId(), cleanMsg);
}
}
}
// ========================================================= // =========================================================
// BEFEHLE REGISTRIEREN // BEFEHLE REGISTRIEREN
// ========================================================= // =========================================================
@@ -452,17 +550,24 @@ public class ChatModule implements Module, Listener {
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, helpop); ProxyServer.getInstance().getPluginManager().registerCommand(plugin, helpop);
// /msg <spieler> <nachricht> // /msg <spieler> <nachricht>
// Vanish: Vanished Spieler sind für normale Spieler nicht erreichbar.
// Admins können vanished Spieler per PM kontaktieren.
Command msgCmd = new Command("msg", null, "tell", "w", "whisper") { Command msgCmd = new Command("msg", null, "tell", "w", "whisper") {
@Override @Override
public void execute(CommandSender sender, String[] args) { public void execute(CommandSender sender, String[] args) {
if (!config.isPmEnabled()) { sender.sendMessage(color("&cPrivat-Nachrichten sind deaktiviert.")); return; } if (!config.isPmEnabled()) { sender.sendMessage(color("&cPrivat-Nachrichten sind deaktiviert.")); return; }
if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(color("&cNur Spieler!")); return; } if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(color("&cNur Spieler!")); return; }
if (args.length < 2) { sender.sendMessage(color("&cBenutzung: /msg <Spieler> <Nachricht>")); return; } if (args.length < 2) { sender.sendMessage(color("&cBenutzung: /msg <Spieler> <Nachricht>")); return; }
ProxiedPlayer from = (ProxiedPlayer) sender; ProxiedPlayer from = (ProxiedPlayer) sender;
ProxiedPlayer to = ProxyServer.getInstance().getPlayer(args[0]); boolean fromIsAdmin = from.hasPermission(config.getAdminBypassPermission());
// Ziel suchen vanished Spieler sind für Nicht-Admins "nicht gefunden"
ProxiedPlayer to = findVisiblePlayer(args[0], fromIsAdmin);
if (to == null || !to.isConnected()) { if (to == null || !to.isConnected()) {
from.sendMessage(color("&cSpieler &f" + args[0] + " &cnicht gefunden.")); return; from.sendMessage(color("&cSpieler &f" + args[0] + " &cnicht gefunden.")); return;
} }
String message = String.join(" ", Arrays.copyOfRange(args, 1, args.length)); String message = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
pmManager.send(from, to, message, config, config.getAdminBypassPermission()); pmManager.send(from, to, message, config, config.getAdminBypassPermission());
} }
@@ -652,27 +757,24 @@ public class ChatModule implements Module, Listener {
UUID tUUID = target.getUniqueId(); UUID tUUID = target.getUniqueId();
String tServer = target.getServer() != null ? target.getServer().getInfo().getName() : "Proxy"; String tServer = target.getServer() != null ? target.getServer().getInfo().getName() : "Proxy";
// Kanal
String channelId = playerChannels.getOrDefault(tUUID, config.getDefaultChannelId()); String channelId = playerChannels.getOrDefault(tUUID, config.getDefaultChannelId());
ChatChannel ch = config.getChannel(channelId); ChatChannel ch = config.getChannel(channelId);
String channelName = ch != null ? ch.getFormattedTag() + " &f" + ch.getName() : "&f" + channelId; String channelName = ch != null ? ch.getFormattedTag() + " &f" + ch.getName() : "&f" + channelId;
// Mute-Status
String muteStatus = muteManager.isMuted(tUUID) String muteStatus = muteManager.isMuted(tUUID)
? "&cJa &8(noch: &f" + muteManager.getRemainingTime(tUUID) + "&8)" ? "&cJa &8(noch: &f" + muteManager.getRemainingTime(tUUID) + "&8)"
: "&aKein"; : "&aKein";
// Blockierungen
Set<UUID> blocked = blockManager.getBlockedBy(tUUID); Set<UUID> blocked = blockManager.getBlockedBy(tUUID);
// Account-Links
AccountLinkManager.LinkedAccount link = linkManager.getByUUID(tUUID); AccountLinkManager.LinkedAccount link = linkManager.getByUUID(tUUID);
String discordInfo = (link != null && !link.discordUserId.isEmpty()) String discordInfo = (link != null && !link.discordUserId.isEmpty())
? "&a" + link.discordUsername + " &8(" + link.discordUserId + ")" : "&7Nicht verknüpft"; ? "&a" + link.discordUsername + " &8(" + link.discordUserId + ")" : "&7Nicht verknüpft";
String telegramInfo = (link != null && !link.telegramUserId.isEmpty()) String telegramInfo = (link != null && !link.telegramUserId.isEmpty())
? "&a" + link.telegramUsername + " &8(" + link.telegramUserId + ")" : "&7Nicht verknüpft"; ? "&a" + link.telegramUsername + " &8(" + link.telegramUserId + ")" : "&7Nicht verknüpft";
// Ausgabe String vanishStatus = VanishProvider.isVanished(target) ? "&eJa" : "&aKein";
sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
sender.sendMessage(color("&eChatInfo: &f" + target.getName() + " &8@ &7" + tServer)); sender.sendMessage(color("&eChatInfo: &f" + target.getName() + " &8@ &7" + tServer));
sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
@@ -680,6 +782,7 @@ public class ChatModule implements Module, Listener {
sender.sendMessage(color("&7Mute: " + muteStatus)); sender.sendMessage(color("&7Mute: " + muteStatus));
sender.sendMessage(color("&7Chat-aus: " + (selfChatMuted.contains(tUUID) ? "&cJa" : "&aKein"))); sender.sendMessage(color("&7Chat-aus: " + (selfChatMuted.contains(tUUID) ? "&cJa" : "&aKein")));
sender.sendMessage(color("&7Mentions: " + (mentionsDisabled.contains(tUUID) ? "&cDeaktiviert" : "&aAktiv"))); sender.sendMessage(color("&7Mentions: " + (mentionsDisabled.contains(tUUID) ? "&cDeaktiviert" : "&aAktiv")));
sender.sendMessage(color("&7Vanish: " + vanishStatus));
sender.sendMessage(color("&7Blockiert: &f" + blocked.size() + " Spieler")); sender.sendMessage(color("&7Blockiert: &f" + blocked.size() + " Spieler"));
if (!blocked.isEmpty()) { if (!blocked.isEmpty()) {
for (UUID bUUID : blocked) { for (UUID bUUID : blocked) {
@@ -707,7 +810,6 @@ public class ChatModule implements Module, Listener {
int lines = config.getHistoryDefaultLines(); int lines = config.getHistoryDefaultLines();
if (args.length >= 1) { if (args.length >= 1) {
// Erstes Arg: Spielername oder Zahl?
try { try {
lines = Math.min(Integer.parseInt(args[0]), config.getHistoryMaxLines()); lines = Math.min(Integer.parseInt(args[0]), config.getHistoryMaxLines());
} catch (NumberFormatException ex) { } catch (NumberFormatException ex) {
@@ -765,7 +867,6 @@ public class ChatModule implements Module, Listener {
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, mentionsCmd); ProxyServer.getInstance().getPluginManager().registerCommand(plugin, mentionsCmd);
// /chatbypass Chat-Verarbeitung für nächste Eingabe(n) überspringen // /chatbypass Chat-Verarbeitung für nächste Eingabe(n) überspringen
// Nützlich wenn ein Plugin (CMI, Shop, etc.) auf eine Chat-Eingabe wartet
Command bypassCmd = new Command("chatbypass", null, "cbp") { Command bypassCmd = new Command("chatbypass", null, "cbp") {
@Override @Override
public void execute(CommandSender sender, String[] args) { public void execute(CommandSender sender, String[] args) {
@@ -784,9 +885,7 @@ public class ChatModule implements Module, Listener {
}; };
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, bypassCmd); ProxyServer.getInstance().getPluginManager().registerCommand(plugin, bypassCmd);
// ─────────────────────────────────────────────────────
// /discordlink Discord-Account verknüpfen // /discordlink Discord-Account verknüpfen
// ─────────────────────────────────────────────────────
Command discordLinkCmd = new Command("discordlink", null, "dlink") { Command discordLinkCmd = new Command("discordlink", null, "dlink") {
@Override @Override
public void execute(CommandSender sender, String[] args) { public void execute(CommandSender sender, String[] args) {
@@ -806,9 +905,7 @@ public class ChatModule implements Module, Listener {
}; };
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, discordLinkCmd); ProxyServer.getInstance().getPluginManager().registerCommand(plugin, discordLinkCmd);
// ─────────────────────────────────────────────────────
// /telegramlink Telegram-Account verknüpfen // /telegramlink Telegram-Account verknüpfen
// ─────────────────────────────────────────────────────
Command telegramLinkCmd = new Command("telegramlink", null, "tlink") { Command telegramLinkCmd = new Command("telegramlink", null, "tlink") {
@Override @Override
public void execute(CommandSender sender, String[] args) { public void execute(CommandSender sender, String[] args) {
@@ -828,9 +925,7 @@ public class ChatModule implements Module, Listener {
}; };
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, telegramLinkCmd); ProxyServer.getInstance().getPluginManager().registerCommand(plugin, telegramLinkCmd);
// ─────────────────────────────────────────────────────
// /unlink Verknüpfung aufheben // /unlink Verknüpfung aufheben
// ─────────────────────────────────────────────────────
Command unlinkCmd = new Command("unlink") { Command unlinkCmd = new Command("unlink") {
@Override @Override
public void execute(CommandSender sender, String[] args) { public void execute(CommandSender sender, String[] args) {
@@ -868,9 +963,7 @@ public class ChatModule implements Module, Listener {
}; };
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, unlinkCmd); ProxyServer.getInstance().getPluginManager().registerCommand(plugin, unlinkCmd);
// ───────────────────────────────────────────────────── // /report <spieler> <grund>
// NEU: /report <spieler> <grund>
// ─────────────────────────────────────────────────────
Command reportCmd = new Command("report") { Command reportCmd = new Command("report") {
@Override @Override
public void execute(CommandSender sender, String[] args) { public void execute(CommandSender sender, String[] args) {
@@ -879,7 +972,6 @@ public class ChatModule implements Module, Listener {
ProxiedPlayer p = (ProxiedPlayer) sender; ProxiedPlayer p = (ProxiedPlayer) sender;
// Permission prüfen (optional)
String reqPerm = config.getReportPermission(); String reqPerm = config.getReportPermission();
if (reqPerm != null && !reqPerm.isEmpty() && !p.hasPermission(reqPerm)) { if (reqPerm != null && !reqPerm.isEmpty() && !p.hasPermission(reqPerm)) {
p.sendMessage(color("&cDu hast keine Berechtigung für /report.")); return; p.sendMessage(color("&cDu hast keine Berechtigung für /report.")); return;
@@ -890,7 +982,6 @@ public class ChatModule implements Module, Listener {
return; return;
} }
// Cooldown
long now = System.currentTimeMillis() / 1000L; long now = System.currentTimeMillis() / 1000L;
Long last = reportCooldowns.get(p.getUniqueId()); Long last = reportCooldowns.get(p.getUniqueId());
if (last != null && (now - last) < config.getReportCooldown()) { if (last != null && (now - last) < config.getReportCooldown()) {
@@ -902,67 +993,54 @@ public class ChatModule implements Module, Listener {
String reportedName = args[0]; String reportedName = args[0];
String reason = String.join(" ", Arrays.copyOfRange(args, 1, args.length)); String reason = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
String server = p.getServer() != null ? p.getServer().getInfo().getName() : "Proxy"; String server = p.getServer() != null ? p.getServer().getInfo().getName() : "Proxy";
// Letzte Nachricht des Gemeldeten als Kontext
String msgContext = lastChatMessages.getOrDefault(reportedName.toLowerCase(), "(keine Chat-Nachricht bekannt)"); String msgContext = lastChatMessages.getOrDefault(reportedName.toLowerCase(), "(keine Chat-Nachricht bekannt)");
// Report erstellen
String reportId = reportManager.createReport( String reportId = reportManager.createReport(
p.getName(), p.getUniqueId(), reportedName, server, msgContext, reason); p.getName(), p.getUniqueId(), reportedName, server, msgContext, reason);
// Report auch ins Chatlog schreiben (ID sichtbar)
if (chatLogger != null) { if (chatLogger != null) {
String logMsg = "[REPORT] Reporter: " + p.getName() + ", Gemeldet: " + reportedName + ", Grund: " + reason + String logMsg = "[REPORT] Reporter: " + p.getName() + ", Gemeldet: " + reportedName
" | Letzte Nachricht: " + msgContext + " | Report-ID: " + reportId; + ", Grund: " + reason + " | Letzte Nachricht: " + msgContext
String msgId = reportId; // Damit die ID im Chatlog und im Report identisch ist + " | Report-ID: " + reportId;
chatLogger.log(msgId, server, "report", p.getName(), logMsg); chatLogger.log(reportId, server, "report", p.getName(), logMsg);
} }
// ==== Discord/Telegram Benachrichtigung ====
// Discord Webhook
String reportWebhook = config.getReportDiscordWebhook(); String reportWebhook = config.getReportDiscordWebhook();
logger.info("[Debug] DiscordWebhookEnabled=" + config.isReportWebhookEnabled() if (config.isReportWebhookEnabled() && discordBridge != null
+ ", discordBridge=" + (discordBridge != null) && reportWebhook != null && !reportWebhook.isEmpty()) {
+ ", reportWebhook=" + reportWebhook);
if (config.isReportWebhookEnabled() && discordBridge != null && reportWebhook != null && !reportWebhook.isEmpty()) {
String title = "Neuer Report eingegangen"; String title = "Neuer Report eingegangen";
String desc = "**Reporter:** " + p.getName() + String desc = "**Reporter:** " + p.getName()
"\n**Gemeldet:** " + reportedName + + "\n**Gemeldet:** " + reportedName
"\n**Server:** " + server + + "\n**Server:** " + server
"\n**Grund:** " + reason + + "\n**Grund:** " + reason
"\n**Letzte Nachricht:** " + msgContext + + "\n**Letzte Nachricht:** " + msgContext
"\n**Report-ID:** " + reportId; + "\n**Report-ID:** " + reportId;
discordBridge.sendEmbedToDiscord(reportWebhook, title, desc, config.getDiscordEmbedColor()); discordBridge.sendEmbedToDiscord(reportWebhook, title, desc, config.getDiscordEmbedColor());
} }
// Telegram Benachrichtigung
String reportTgChatId = config.getReportTelegramChatId(); String reportTgChatId = config.getReportTelegramChatId();
if (telegramBridge != null && reportTgChatId != null && !reportTgChatId.isEmpty()) { if (telegramBridge != null && reportTgChatId != null && !reportTgChatId.isEmpty()) {
String header = "Neuer Report eingegangen"; String header = "Neuer Report eingegangen";
String content = "Reporter: " + p.getName() + String content = "Reporter: " + p.getName()
"\nGemeldet: " + reportedName + + "\nGemeldet: " + reportedName
"\nServer: " + server + + "\nServer: " + server
"\nGrund: " + reason + + "\nGrund: " + reason
"\nLetzte Nachricht: " + msgContext + + "\nLetzte Nachricht: " + msgContext
"\nReport-ID: " + reportId; + "\nReport-ID: " + reportId;
telegramBridge.sendFormattedToTelegram(reportTgChatId, header, content); telegramBridge.sendFormattedToTelegram(reportTgChatId, header, content);
} }
reportCooldowns.put(p.getUniqueId(), now); reportCooldowns.put(p.getUniqueId(), now);
// Bestätigung an Reporter
String confirm = config.getReportConfirm().replace("{id}", reportId); String confirm = config.getReportConfirm().replace("{id}", reportId);
p.sendMessage(color(confirm)); p.sendMessage(color(confirm));
// ── Online-Admins sofort benachrichtigen ──
notifyAdminsReport(reportId, p.getName(), reportedName, server, reason, msgContext); notifyAdminsReport(reportId, p.getName(), reportedName, server, reason, msgContext);
} }
}; };
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, reportCmd); ProxyServer.getInstance().getPluginManager().registerCommand(plugin, reportCmd);
// ───────────────────────────────────────────────────── // /reports [all] Admin-Übersicht
// NEU: /reports [all] Admin-Übersicht
// ─────────────────────────────────────────────────────
Command reportsCmd = new Command("reports", config.getReportViewPermission()) { Command reportsCmd = new Command("reports", config.getReportViewPermission()) {
@Override @Override
public void execute(CommandSender sender, String[] args) { public void execute(CommandSender sender, String[] args) {
@@ -988,15 +1066,12 @@ public class ChatModule implements Module, Listener {
String statusColor = r.closed ? "&a✔" : "&c✘"; String statusColor = r.closed ? "&a✔" : "&c✘";
if (sender instanceof ProxiedPlayer) { if (sender instanceof ProxiedPlayer) {
// Klickbare Zeile: ID-Click kopiert ID in Zwischenablage
ComponentBuilder line = new ComponentBuilder(""); ComponentBuilder line = new ComponentBuilder("");
// Status
line.append(ChatColor.translateAlternateColorCodes('&', statusColor + " ")) line.append(ChatColor.translateAlternateColorCodes('&', statusColor + " "))
.event((ClickEvent) null) .event((ClickEvent) null)
.event((HoverEvent) null); .event((HoverEvent) null);
// Klickbare Report-ID
line.append(ChatColor.translateAlternateColorCodes('&', "&8[&f" + r.id + "&8]")) line.append(ChatColor.translateAlternateColorCodes('&', "&8[&f" + r.id + "&8]"))
.event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, r.id)) .event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, r.id))
.event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
@@ -1009,7 +1084,6 @@ public class ChatModule implements Module, Listener {
+ "\n" + ChatColor.YELLOW + "Grund: " + ChatColor.RED + r.reason + "\n" + ChatColor.YELLOW + "Grund: " + ChatColor.RED + r.reason
+ (r.closed ? "\n" + ChatColor.GREEN + "Geschlossen von: " + r.closedBy : "")).create())); + (r.closed ? "\n" + ChatColor.GREEN + "Geschlossen von: " + r.closedBy : "")).create()));
// Rest der Zeile
line.append(ChatColor.translateAlternateColorCodes('&', line.append(ChatColor.translateAlternateColorCodes('&',
" &b" + r.reportedName + " &8← &7" + r.reporterName " &b" + r.reportedName + " &8← &7" + r.reporterName
+ " &8@ &a" + r.server + " &8@ &a" + r.server
@@ -1019,7 +1093,6 @@ public class ChatModule implements Module, Listener {
((ProxiedPlayer) sender).sendMessage(line.create()); ((ProxiedPlayer) sender).sendMessage(line.create());
} else { } else {
// Konsole: plain text
sender.sendMessage(color(statusColor + " &8[&f" + r.id + "&8] &b" + r.reportedName sender.sendMessage(color(statusColor + " &8[&f" + r.id + "&8] &b" + r.reportedName
+ " &8← &7" + r.reporterName + " &8@ &a" + r.server + " &8← &7" + r.reporterName + " &8@ &a" + r.server
+ " &8| &e" + r.getFormattedTime() + " &8| &e" + r.getFormattedTime()
@@ -1035,9 +1108,7 @@ public class ChatModule implements Module, Listener {
}; };
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, reportsCmd); ProxyServer.getInstance().getPluginManager().registerCommand(plugin, reportsCmd);
// ───────────────────────────────────────────────────── // /reportclose <ID>
// NEU: /reportclose <ID>
// ─────────────────────────────────────────────────────
Command reportCloseCmd = new Command("reportclose", config.getReportClosePermission()) { Command reportCloseCmd = new Command("reportclose", config.getReportClosePermission()) {
@Override @Override
public void execute(CommandSender sender, String[] args) { public void execute(CommandSender sender, String[] args) {
@@ -1062,14 +1133,12 @@ public class ChatModule implements Module, Listener {
sender.sendMessage(color("&aReport &f" + id + " &awurde geschlossen.")); sender.sendMessage(color("&aReport &f" + id + " &awurde geschlossen."));
// Reporter benachrichtigen, falls online
ProxiedPlayer reporter = ProxyServer.getInstance().getPlayer(report.reporterUUID); ProxiedPlayer reporter = ProxyServer.getInstance().getPlayer(report.reporterUUID);
if (reporter != null && reporter.isConnected()) { if (reporter != null && reporter.isConnected()) {
reporter.sendMessage(color("&8[&aReport&8] &7Dein Report &f" + id reporter.sendMessage(color("&8[&aReport&8] &7Dein Report &f" + id
+ " &7gegen &c" + report.reportedName + " &7wurde von &b" + adminName + " &7bearbeitet.")); + " &7gegen &c" + report.reportedName + " &7wurde von &b" + adminName + " &7bearbeitet."));
} }
// Andere Admins informieren
notifyAdmins("&8[&aReport geschlossen&8] &f" + adminName + " &7hat Report &f" + id + " &7geschlossen."); notifyAdmins("&8[&aReport geschlossen&8] &f" + adminName + " &7hat Report &f" + id + " &7geschlossen.");
} }
}; };
@@ -1078,38 +1147,10 @@ public class ChatModule implements Module, Listener {
// ========================================================= // =========================================================
// PLUGIN-INPUT BYPASS // PLUGIN-INPUT BYPASS
//
// Spieler die gerade auf eine Chat-Eingabe eines Sub-Server-
// Plugins warten (CMI, Shops, etc.) werden vom ChatModule
// übersprungen. Die Nachricht geht direkt an den Sub-Server.
//
// Drei Erkennungsmethoden:
// 1. Manueller Bypass-Toggle via /chatbypass (für Admins)
// 2. Programmatische API: ChatModule.setAwaitingInput(uuid, true)
// 3. Automatische Erkennung bekannter Plugin-Nachrichten
// ========================================================= // =========================================================
// UUIDs die gerade auf Plugin-Chat-Eingabe warten
private final Set<UUID> awaitingInput = Collections.newSetFromMap(new ConcurrentHashMap<>());
/** /**
* Prüft ob ein Spieler gerade auf eine Chat-Eingabe eines * Öffentliche API für Sub-Server-Plugins oder BungeeCord-eigene Plugins.
* Sub-Server-Plugins wartet und das ChatModule überspringen soll.
*/
private boolean isAwaitingPluginInput(ProxiedPlayer player) {
// 1. Manuell / programmatisch gesetzt
if (awaitingInput.contains(player.getUniqueId())) return true;
// 2. Automatische Erkennung: BungeeCord leitet SubServer-Nachrichten
// via PluginChannel weiter wir prüfen bekannte CMI-Patterns nicht,
// da wir keinen Zugriff auf SubServer-Metadaten haben.
// Stattdessen: Spieler kann selbst /chatbypass togglen oder
// Sub-Server-Plugin ruft setAwaitingInput() auf.
return false;
}
/**
* Öffentliche API für Sub-Server-Plugins oder BungeeCord-eigene Plugins:
* Setzt den Bypass-Status für einen Spieler. * Setzt den Bypass-Status für einen Spieler.
* *
* Beispiel aus einem anderen BungeeCord-Plugin: * Beispiel aus einem anderen BungeeCord-Plugin:
@@ -1126,6 +1167,24 @@ public class ChatModule implements Module, Listener {
return awaitingInput.contains(uuid); return awaitingInput.contains(uuid);
} }
// =========================================================
// VANISH-HILFSMETHODEN
// =========================================================
/**
* Sucht einen Spieler nach Name und berücksichtigt den Vanish-Status.
*
* @param name Spielername (case-insensitiv)
* @param callerIsAdmin true → Vanished Spieler werden ebenfalls gefunden
* @return ProxiedPlayer oder null wenn nicht gefunden / vanished (für Nicht-Admins)
*/
private ProxiedPlayer findVisiblePlayer(String name, boolean callerIsAdmin) {
ProxiedPlayer target = ProxyServer.getInstance().getPlayer(name);
if (target == null) return null;
if (!callerIsAdmin && VanishProvider.isVanished(target)) return null;
return target;
}
// ========================================================= // =========================================================
// HILFSMETHODEN // HILFSMETHODEN
// ========================================================= // =========================================================
@@ -1133,7 +1192,7 @@ public class ChatModule implements Module, Listener {
private String buildFormat(String format, String server, String prefix, private String buildFormat(String format, String server, String prefix,
String player, String suffix, String message) { String player, String suffix, String message) {
String serverColor = config.getServerColor(server); String serverColor = config.getServerColor(server);
String serverDisplay = config.getServerDisplay(server); // Anzeigename aus config String serverDisplay = config.getServerDisplay(server);
String coloredServer = translateColors(serverColor + serverDisplay + "&r"); String coloredServer = translateColors(serverColor + serverDisplay + "&r");
return format return format
@@ -1158,23 +1217,13 @@ public class ChatModule implements Module, Listener {
/** /**
* Übersetzt sowohl klassische &-Farbcodes als auch HEX-Codes im Format &#RRGGBB. * Übersetzt sowohl klassische &-Farbcodes als auch HEX-Codes im Format &#RRGGBB.
*
* Beispiele:
* &a → §a (Grün)
* &#FF5500 → BungeeCord HEX-Farbe Orange
* &l&#FF5500Text → Fett + Orange
*
* BungeeCord unterstützt ChatColor.of("#RRGGBB") ab 1.16-kompatiblen Builds.
* Ältere Builds erhalten automatisch den nächsten &-Code als Fallback.
*/ */
private String translateColors(String text) { private String translateColors(String text) {
if (text == null) return ""; if (text == null) return "";
// 1. Schritt: &#RRGGBB → BungeeCord ChatColor
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
int i = 0; int i = 0;
while (i < text.length()) { while (i < text.length()) {
// Prüfe auf &#RRGGBB (8 Zeichen: & # R R G G B B)
if (i + 7 < text.length() if (i + 7 < text.length()
&& text.charAt(i) == '&' && text.charAt(i) == '&'
&& text.charAt(i + 1) == '#') { && text.charAt(i + 1) == '#') {
@@ -1195,7 +1244,6 @@ public class ChatModule implements Module, Listener {
i++; i++;
} }
// 2. Schritt: Standard &-Codes übersetzen
return ChatColor.translateAlternateColorCodes('&', sb.toString()); return ChatColor.translateAlternateColorCodes('&', sb.toString());
} }
@@ -1209,11 +1257,9 @@ public class ChatModule implements Module, Listener {
/** /**
* Benachrichtigt alle online Admins über einen neuen Report. * Benachrichtigt alle online Admins über einen neuen Report.
* Baut eine mehrzeilige, klickbare Nachricht.
*/ */
private void notifyAdminsReport(String reportId, String reporter, String reported, private void notifyAdminsReport(String reportId, String reporter, String reported,
String server, String reason, String msgContext) { String server, String reason, String msgContext) {
// Zeitstempel
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("dd.MM.yyyy HH:mm:ss"); java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("dd.MM.yyyy HH:mm:ss");
String zeit = sdf.format(new java.util.Date()); String zeit = sdf.format(new java.util.Date());
@@ -1221,7 +1267,6 @@ public class ChatModule implements Module, Listener {
if (!p.hasPermission(config.getAdminNotifyPermission()) if (!p.hasPermission(config.getAdminNotifyPermission())
&& !p.hasPermission(config.getAdminBypassPermission())) continue; && !p.hasPermission(config.getAdminBypassPermission())) continue;
// Mehrzeilige Report-Notification
p.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); p.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
p.sendMessage(color("&8[&cReport&8] &7Zeit: &e" + zeit)); p.sendMessage(color("&8[&cReport&8] &7Zeit: &e" + zeit));
p.sendMessage(color("&7Reporter: &b" + reporter)); p.sendMessage(color("&7Reporter: &b" + reporter));
@@ -1229,7 +1274,6 @@ public class ChatModule implements Module, Listener {
p.sendMessage(color("&7Letzte Nachricht: &f" + msgContext)); p.sendMessage(color("&7Letzte Nachricht: &f" + msgContext));
p.sendMessage(color("&7Grund: &c" + reason)); p.sendMessage(color("&7Grund: &c" + reason));
// Klickbare ID-Zeile
ComponentBuilder idLine = new ComponentBuilder(ChatColor.GRAY + "ID: "); ComponentBuilder idLine = new ComponentBuilder(ChatColor.GRAY + "ID: ");
idLine.append(ChatColor.WHITE + "" + ChatColor.BOLD + reportId) idLine.append(ChatColor.WHITE + "" + ChatColor.BOLD + reportId)
.event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, reportId)) .event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, reportId))
@@ -1244,28 +1288,22 @@ public class ChatModule implements Module, Listener {
p.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); p.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
} }
// Konsole ebenfalls informieren
ProxyServer.getInstance().getConsole().sendMessage(color( ProxyServer.getInstance().getConsole().sendMessage(color(
"&8[&cReport " + reportId + "&8] &7Reporter: &b" + reporter "&8[&cReport " + reportId + "&8] &7Reporter: &b" + reporter
+ " &7→ &c" + reported + " &7@ &a" + server + " &8| &cGrund: &f" + reason)); + " &7→ &c" + reported + " &7@ &a" + server + " &8| &cGrund: &f" + reason));
} }
/** /**
* Baut eine BungeeCord-Nachricht ohne sichtbare ID. * Baut eine BungeeCord-Nachricht mit klickbarem [⚑] Melden-Button.
* Am Ende erscheint ein klickbarer [⚑] Melden-Button (nur wenn Reports aktiviert).
*
* Layout: <formatierter Chat> §8[§c⚑§8]
*/ */
private BaseComponent[] buildClickableMessage(String msgId, String formatted, String senderName) { private BaseComponent[] buildClickableMessage(String msgId, String formatted, String senderName) {
ComponentBuilder builder = new ComponentBuilder(""); ComponentBuilder builder = new ComponentBuilder("");
// Eigentliche Nachricht (kein ID-Tag mehr sichtbar)
builder.append(ChatColor.translateAlternateColorCodes('&', formatted), builder.append(ChatColor.translateAlternateColorCodes('&', formatted),
ComponentBuilder.FormatRetention.NONE) ComponentBuilder.FormatRetention.NONE)
.event((ClickEvent) null) .event((ClickEvent) null)
.event((HoverEvent) null); .event((HoverEvent) null);
// [⚑] Melden-Button am Ende (nur wenn Report-System aktiv und Sender bekannt)
if (msgId != null && senderName != null && reportManager != null) { if (msgId != null && senderName != null && reportManager != null) {
builder.append(" ", ComponentBuilder.FormatRetention.NONE) builder.append(" ", ComponentBuilder.FormatRetention.NONE)
.event((ClickEvent) null) .event((ClickEvent) null)
@@ -1287,15 +1325,11 @@ public class ChatModule implements Module, Listener {
} }
/** /**
* Sendet einen Sound an einen Spieler via Plugin-Messaging. * Sendet einen Sound-Hinweis via Actionbar (Mention-Feedback).
* Der Sub-Server muss den Kanal "BungeeCord" registriert haben (standard).
* Sound wird als Proxy-Message gesendet → Sub-Server-Plugin nötig für echten Sound.
* Als Fallback: Actionbar-Nachricht mit ♪-Symbol.
*/ */
private void sendMentionSound(ProxiedPlayer player, String soundName) { private void sendMentionSound(ProxiedPlayer player, String soundName) {
if (soundName == null || soundName.isEmpty()) return; if (soundName == null || soundName.isEmpty()) return;
try { try {
// Actionbar als visuellen Feedback (funktioniert ohne Sub-Server-Plugin)
net.md_5.bungee.api.chat.TextComponent actionBar = net.md_5.bungee.api.chat.TextComponent actionBar =
new net.md_5.bungee.api.chat.TextComponent( new net.md_5.bungee.api.chat.TextComponent(
ChatColor.translateAlternateColorCodes('&', "&e♪ Mention!")); ChatColor.translateAlternateColorCodes('&', "&e♪ Mention!"));

View File

@@ -205,7 +205,7 @@ public class ReportManager {
logger.warning("[ChatModule] Fehler beim Laden der Reports: " + e.getMessage()); logger.warning("[ChatModule] Fehler beim Laden der Reports: " + e.getMessage());
} }
idCounter.set(maxNum); idCounter.set(maxNum);
logger.info("[ChatModule] " + reports.size() + " Reports geladen (" + getOpenCount() + " offen).");
} }
// ===== Escape-Helfer (Pipe-Zeichen und Zeilenumbrüche escapen) ===== // ===== Escape-Helfer (Pipe-Zeichen und Zeilenumbrüche escapen) =====

View File

@@ -0,0 +1,71 @@
package net.viper.status.modules.chat;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import java.util.Collections;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* Zentrale Schnittstelle zwischen dem VanishModule und dem ChatModule.
*
* Das VanishModule (oder jedes andere Modul) ruft {@link #setVanished} auf
* um Spieler als unsichtbar zu markieren. Das ChatModule prüft via
* {@link #isVanished} bevor es Join-/Leave-Nachrichten sendet oder
* Privat-Nachrichten zulässt.
*
* Verwendung im VanishModule:
* VanishProvider.setVanished(player.getUniqueId(), true); // beim Verschwinden
* VanishProvider.setVanished(player.getUniqueId(), false); // beim Erscheinen / Disconnect
*/
public final class VanishProvider {
private VanishProvider() {}
/** Intern verwaltete Menge aller aktuell unsichtbaren Spieler-UUIDs. */
private static final Set<UUID> vanishedPlayers =
Collections.newSetFromMap(new ConcurrentHashMap<>());
// ===== Schreib-API (wird vom VanishModule aufgerufen) =====
/**
* Markiert einen Spieler als sichtbar oder unsichtbar.
*
* @param uuid UUID des Spielers
* @param vanished true = unsichtbar, false = sichtbar
*/
public static void setVanished(UUID uuid, boolean vanished) {
if (vanished) {
vanishedPlayers.add(uuid);
} else {
vanishedPlayers.remove(uuid);
}
}
/**
* Entfernt einen Spieler beim Disconnect aus der Vanish-Liste.
* Sollte vom ChatModule (onDisconnect) aufgerufen werden, damit
* kein toter Eintrag verbleibt.
*/
public static void cleanup(UUID uuid) {
vanishedPlayers.remove(uuid);
}
// ===== Lese-API (wird vom ChatModule aufgerufen) =====
/** @return true wenn der Spieler aktuell als unsichtbar markiert ist. */
public static boolean isVanished(ProxiedPlayer player) {
return player != null && vanishedPlayers.contains(player.getUniqueId());
}
/** @return true wenn der Spieler mit der angegebenen UUID unsichtbar ist. */
public static boolean isVanished(UUID uuid) {
return uuid != null && vanishedPlayers.contains(uuid);
}
/** Snapshot der aktuell unsichtbaren Spieler (für Debugging / Logs). */
public static Set<UUID> getVanishedPlayers() {
return Collections.unmodifiableSet(vanishedPlayers);
}
}

View File

@@ -18,13 +18,8 @@ import java.util.logging.Logger;
/** /**
* Discord-Brücke für bidirektionale Kommunikation. * Discord-Brücke für bidirektionale Kommunikation.
* *
* Minecraft → Discord: Via Webhook (kein Bot benötigt) * Fix #12: extractJsonString() behandelt Escape-Sequenzen jetzt korrekt.
* Discord → Minecraft: Via Bot-Polling der Discord REST-API * Statt Zeichenvergleich mit dem Vorgänger-Char wird ein expliziter Escape-Flag verwendet.
*
* Voraussetzungen:
* - Bot mit "Read Message History" und "Send Messages" Permissions
* - Bot muss in den jeweiligen Kanälen sein
* - Bot-Token in chat.yml eintragen
*/ */
public class DiscordBridge { public class DiscordBridge {
@@ -33,9 +28,7 @@ public class DiscordBridge {
private final Logger logger; private final Logger logger;
private AccountLinkManager linkManager; private AccountLinkManager linkManager;
// Letztes verarbeitetes Discord Message-ID pro Kanal (für Polling)
private final java.util.Map<String, AtomicLong> lastMessageIds = new java.util.concurrent.ConcurrentHashMap<>(); private final java.util.Map<String, AtomicLong> lastMessageIds = new java.util.concurrent.ConcurrentHashMap<>();
private volatile boolean running = false; private volatile boolean running = false;
public DiscordBridge(Plugin plugin, ChatConfig config) { public DiscordBridge(Plugin plugin, ChatConfig config) {
@@ -44,10 +37,7 @@ public class DiscordBridge {
this.logger = plugin.getLogger(); this.logger = plugin.getLogger();
} }
/** Setzt den AccountLinkManager muss vor start() aufgerufen werden. */ public void setLinkManager(AccountLinkManager linkManager) { this.linkManager = linkManager; }
public void setLinkManager(AccountLinkManager linkManager) {
this.linkManager = linkManager;
}
public void start() { public void start() {
if (!config.isDiscordEnabled() if (!config.isDiscordEnabled()
@@ -56,40 +46,23 @@ public class DiscordBridge {
logger.warning("[ChatModule-Discord] Bot-Token nicht konfiguriert. Discord-Empfang deaktiviert."); logger.warning("[ChatModule-Discord] Bot-Token nicht konfiguriert. Discord-Empfang deaktiviert.");
return; return;
} }
running = true; running = true;
// Starte Polling-Task für alle konfigurierten Kanäle
int interval = Math.max(2, config.getDiscordPollInterval()); int interval = Math.max(2, config.getDiscordPollInterval());
plugin.getProxy().getScheduler().schedule(plugin, this::pollAllChannels, plugin.getProxy().getScheduler().schedule(plugin, this::pollAllChannels, interval, interval, TimeUnit.SECONDS);
interval, interval, TimeUnit.SECONDS);
logger.info("[ChatModule-Discord] Brücke gestartet (Poll-Intervall: " + interval + "s)."); logger.info("[ChatModule-Discord] Brücke gestartet (Poll-Intervall: " + interval + "s).");
} }
public void stop() { public void stop() { running = false; }
running = false;
}
// ===== Minecraft → Discord ===== // ===== Minecraft → Discord =====
/**
* Sendet eine Nachricht via Webhook an Discord.
* Funktioniert ohne Bot-Token!
*/
public void sendToDiscord(String webhookUrl, String username, String message, String avatarUrl) { public void sendToDiscord(String webhookUrl, String username, String message, String avatarUrl) {
if (webhookUrl == null || webhookUrl.isEmpty()) return; if (webhookUrl == null || webhookUrl.isEmpty()) return;
plugin.getProxy().getScheduler().runAsync(plugin, () -> { plugin.getProxy().getScheduler().runAsync(plugin, () -> {
try { try {
String safeUsername = escapeJson(username); String payload = "{\"username\":\"" + escapeJson(username) + "\""
String safeMessage = escapeJson(message); + (avatarUrl != null && !avatarUrl.isEmpty() ? ",\"avatar_url\":\"" + avatarUrl + "\"" : "")
String payload = "{\"username\":\"" + safeUsername + "\"" + ",\"content\":\"" + escapeJson(message) + "\"}";
+ (avatarUrl != null && !avatarUrl.isEmpty()
? ",\"avatar_url\":\"" + avatarUrl + "\""
: "")
+ ",\"content\":\"" + safeMessage + "\"}";
postJson(webhookUrl, payload, null); postJson(webhookUrl, payload, null);
} catch (Exception e) { } catch (Exception e) {
logger.warning("[ChatModule-Discord] Webhook-Fehler: " + e.getMessage()); logger.warning("[ChatModule-Discord] Webhook-Fehler: " + e.getMessage());
@@ -97,47 +70,29 @@ public class DiscordBridge {
}); });
} }
/**
* Sendet eine Embed-Nachricht (für HelpOp, Broadcast) an einen Discord-Kanal via Webhook.
*/
public void sendEmbedToDiscord(String webhookUrl, String title, String description, String colorHex) { public void sendEmbedToDiscord(String webhookUrl, String title, String description, String colorHex) {
if (webhookUrl == null || webhookUrl.isEmpty()) return; if (webhookUrl == null || webhookUrl.isEmpty()) return;
plugin.getProxy().getScheduler().runAsync(plugin, () -> { plugin.getProxy().getScheduler().runAsync(plugin, () -> {
try { try {
int color = 0; int color = 0x5865F2;
try { color = Integer.parseInt(colorHex.replace("#", ""), 16); } try { color = Integer.parseInt(colorHex.replace("#", ""), 16); } catch (Exception ignored) {}
catch (Exception ignored) { color = 0x5865F2; }
String payload = "{\"embeds\":[{\"title\":\"" + escapeJson(title) + "\"" String payload = "{\"embeds\":[{\"title\":\"" + escapeJson(title) + "\""
+ ",\"description\":\"" + escapeJson(description) + "\"" + ",\"description\":\"" + escapeJson(description) + "\""
+ ",\"color\":" + color + "}]}"; + ",\"color\":" + color + "}]}";
logger.info("[ChatModule-Discord] Sende Embed an Webhook: " + webhookUrl);
logger.info("[ChatModule-Discord] Payload: " + payload);
postJson(webhookUrl, payload, null); postJson(webhookUrl, payload, null);
logger.info("[ChatModule-Discord] Embed erfolgreich an Discord gesendet.");
} catch (Exception e) { } catch (Exception e) {
logger.warning("[ChatModule-Discord] Embed-Fehler: " + e.getMessage()); logger.warning("[ChatModule-Discord] Embed-Fehler: " + e.getMessage());
} }
}); });
} }
/**
* Sendet eine Nachricht direkt in einen Discord-Kanal via Bot-Token.
* Benötigt: DISCORD_BOT_TOKEN, channel-id
*/
public void sendToChannel(String channelId, String message) { public void sendToChannel(String channelId, String message) {
if (channelId == null || channelId.isEmpty()) return; if (channelId == null || channelId.isEmpty()) return;
if (config.getDiscordBotToken().isEmpty()) return; if (config.getDiscordBotToken().isEmpty()) return;
plugin.getProxy().getScheduler().runAsync(plugin, () -> { plugin.getProxy().getScheduler().runAsync(plugin, () -> {
try { try {
String url = "https://discord.com/api/v10/channels/" + channelId + "/messages"; String url = "https://discord.com/api/v10/channels/" + channelId + "/messages";
String payload = "{\"content\":\"" + escapeJson(message) + "\"}"; postJson(url, "{\"content\":\"" + escapeJson(message) + "\"}", "Bot " + config.getDiscordBotToken());
postJson(url, payload, "Bot " + config.getDiscordBotToken());
} catch (Exception e) { } catch (Exception e) {
logger.warning("[ChatModule-Discord] Send-to-Channel-Fehler: " + e.getMessage()); logger.warning("[ChatModule-Discord] Send-to-Channel-Fehler: " + e.getMessage());
} }
@@ -148,95 +103,59 @@ public class DiscordBridge {
private void pollAllChannels() { private void pollAllChannels() {
if (!running) return; if (!running) return;
// Alle Kanal-IDs aus der Konfiguration sammeln
java.util.Set<String> channelIds = new java.util.LinkedHashSet<>(); java.util.Set<String> channelIds = new java.util.LinkedHashSet<>();
for (net.viper.status.modules.chat.ChatChannel ch : config.getChannels().values()) { for (net.viper.status.modules.chat.ChatChannel ch : config.getChannels().values()) {
if (!ch.getDiscordChannelId().isEmpty()) { if (!ch.getDiscordChannelId().isEmpty()) channelIds.add(ch.getDiscordChannelId());
channelIds.add(ch.getDiscordChannelId());
}
}
if (!config.getDiscordAdminChannelId().isEmpty()) {
channelIds.add(config.getDiscordAdminChannelId());
}
for (String channelId : channelIds) {
pollChannel(channelId);
} }
if (!config.getDiscordAdminChannelId().isEmpty()) channelIds.add(config.getDiscordAdminChannelId());
for (String channelId : channelIds) pollChannel(channelId);
} }
private void pollChannel(String channelId) { private void pollChannel(String channelId) {
try { try {
AtomicLong lastId = lastMessageIds.computeIfAbsent(channelId, k -> new AtomicLong(0L)); AtomicLong lastId = lastMessageIds.computeIfAbsent(channelId, k -> new AtomicLong(0L));
// Beim ersten Poll: aktuelle neueste ID holen und merken, nicht broadcasten.
// So werden beim Start keine alten Discord-Nachrichten in Minecraft angezeigt.
if (lastId.get() == 0L) { if (lastId.get() == 0L) {
String initUrl = "https://discord.com/api/v10/channels/" + channelId + "/messages?limit=1"; String initResp = getJson("https://discord.com/api/v10/channels/" + channelId + "/messages?limit=1",
String initResp = getJson(initUrl, "Bot " + config.getDiscordBotToken()); "Bot " + config.getDiscordBotToken());
if (initResp != null && !initResp.equals("[]") && !initResp.isEmpty()) { if (initResp != null && !initResp.equals("[]") && !initResp.isEmpty()) {
java.util.List<DiscordMessage> initMsgs = parseMessages(initResp); java.util.List<DiscordMessage> initMsgs = parseMessages(initResp);
if (!initMsgs.isEmpty()) { if (!initMsgs.isEmpty()) lastId.set(initMsgs.get(0).id);
lastId.set(initMsgs.get(0).id);
} }
} return;
return; // Erster Poll nur zum Initialisieren, nichts broadcasten
} }
String afterParam = "?after=" + lastId.get() + "&limit=10"; String url = "https://discord.com/api/v10/channels/" + channelId + "/messages?after=" + lastId.get() + "&limit=10";
String url = "https://discord.com/api/v10/channels/" + channelId + "/messages" + afterParam;
String response = getJson(url, "Bot " + config.getDiscordBotToken()); String response = getJson(url, "Bot " + config.getDiscordBotToken());
if (response == null || response.equals("[]") || response.isEmpty()) return; if (response == null || response.equals("[]") || response.isEmpty()) return;
// JSON-Array von Nachrichten parsen (ohne externe Library)
java.util.List<DiscordMessage> messages = parseMessages(response); java.util.List<DiscordMessage> messages = parseMessages(response);
// Nachrichten chronologisch verarbeiten (älteste zuerst)
messages.sort(java.util.Comparator.comparingLong(m -> m.id)); messages.sort(java.util.Comparator.comparingLong(m -> m.id));
for (DiscordMessage msg : messages) { for (DiscordMessage msg : messages) {
if (msg.id <= lastId.get()) continue; if (msg.id <= lastId.get()) continue;
if (msg.isBot) continue; if (msg.isBot) continue;
if (msg.content.isEmpty()) continue; if (msg.content.isEmpty()) continue;
lastId.set(msg.id); lastId.set(msg.id);
// ── Token-Einlösung: !link <TOKEN> ──
if (msg.content.startsWith("!link ")) { if (msg.content.startsWith("!link ")) {
String token = msg.content.substring(6).trim().toUpperCase(); String token = msg.content.substring(6).trim().toUpperCase();
if (linkManager != null) { if (linkManager != null) {
AccountLinkManager.LinkedAccount acc = AccountLinkManager.LinkedAccount acc = linkManager.redeemDiscord(token, msg.authorId, msg.authorName);
linkManager.redeemDiscord(token, msg.authorId, msg.authorName); if (acc != null) sendToChannel(channelId, "✅ Verknüpfung erfolgreich! Minecraft-Account: **" + acc.minecraftName + "**");
if (acc != null) { else sendToChannel(channelId, "❌ Ungültiger oder abgelaufener Token. Bitte `/discordlink` im Spiel erneut ausführen.");
sendToChannel(channelId,
"✅ Verknüpfung erfolgreich! Minecraft-Account: **"
+ acc.minecraftName + "**");
} else {
sendToChannel(channelId,
"❌ Ungültiger oder abgelaufener Token. Bitte `/discordlink` im Spiel erneut ausführen.");
} }
} continue;
continue; // Nicht als Chat-Nachricht weiterleiten
} }
// ── Account-Name auflösen ──
String displayName = (linkManager != null) String displayName = (linkManager != null)
? linkManager.resolveDiscordName(msg.authorId, msg.authorName) ? linkManager.resolveDiscordName(msg.authorId, msg.authorName) : msg.authorName;
: msg.authorName; String mcFormat = resolveFormat(channelId);
// Welchem Kanal gehört diese Discord-Kanal-ID?
final String mcFormat = resolveFormat(channelId);
if (mcFormat == null) continue; if (mcFormat == null) continue;
final String formatted = ChatColor.translateAlternateColorCodes('&', String formatted = ChatColor.translateAlternateColorCodes('&',
mcFormat.replace("{user}", displayName) mcFormat.replace("{user}", displayName).replace("{message}", msg.content));
.replace("{message}", msg.content)); ProxyServer.getInstance().getScheduler().runAsync(plugin,
() -> ProxyServer.getInstance().broadcast(new TextComponent(formatted)));
ProxyServer.getInstance().getScheduler().runAsync(plugin, () ->
ProxyServer.getInstance().broadcast(new TextComponent(formatted))
);
} }
} catch (Exception e) { } catch (Exception e) {
logger.fine("[ChatModule-Discord] Poll-Fehler für Kanal " + channelId + ": " + e.getMessage()); logger.fine("[ChatModule-Discord] Poll-Fehler für Kanal " + channelId + ": " + e.getMessage());
@@ -244,20 +163,14 @@ public class DiscordBridge {
} }
private String resolveFormat(String channelId) { private String resolveFormat(String channelId) {
// Admin-Kanal? if (channelId.equals(config.getDiscordAdminChannelId())) return config.getDiscordFromFormat();
if (channelId.equals(config.getDiscordAdminChannelId())) {
return config.getDiscordFromFormat();
}
// Reguläre Kanäle
for (net.viper.status.modules.chat.ChatChannel ch : config.getChannels().values()) { for (net.viper.status.modules.chat.ChatChannel ch : config.getChannels().values()) {
if (channelId.equals(ch.getDiscordChannelId())) { if (channelId.equals(ch.getDiscordChannelId())) return config.getDiscordFromFormat();
return config.getDiscordFromFormat();
}
} }
return null; return null;
} }
// ===== HTTP-Hilfsklassen ===== // ===== HTTP =====
private void postJson(String urlStr, String payload, String authorization) throws Exception { private void postJson(String urlStr, String payload, String authorization) throws Exception {
HttpURLConnection conn = openConnection(urlStr, "POST", authorization); HttpURLConnection conn = openConnection(urlStr, "POST", authorization);
@@ -266,10 +179,7 @@ public class DiscordBridge {
conn.setDoOutput(true); conn.setDoOutput(true);
try (OutputStream os = conn.getOutputStream()) { os.write(data); } try (OutputStream os = conn.getOutputStream()) { os.write(data); }
int code = conn.getResponseCode(); int code = conn.getResponseCode();
if (code >= 400) { if (code >= 400) logger.warning("[ChatModule-Discord] HTTP " + code + ": " + readStream(conn.getErrorStream()));
String err = readStream(conn.getErrorStream());
logger.warning("[ChatModule-Discord] HTTP " + code + ": " + err);
}
conn.disconnect(); conn.disconnect();
} }
@@ -289,17 +199,14 @@ public class DiscordBridge {
conn.setReadTimeout(8000); conn.setReadTimeout(8000);
conn.setRequestProperty("Content-Type", "application/json"); conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("User-Agent", "StatusAPI-ChatModule/1.0"); conn.setRequestProperty("User-Agent", "StatusAPI-ChatModule/1.0");
if (authorization != null && !authorization.isEmpty()) { if (authorization != null && !authorization.isEmpty()) conn.setRequestProperty("Authorization", authorization);
conn.setRequestProperty("Authorization", authorization);
}
return conn; return conn;
} }
private String readStream(InputStream in) throws IOException { private String readStream(InputStream in) throws IOException {
if (in == null) return ""; if (in == null) return "";
try (BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) { try (BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder(); String line;
String line;
while ((line = br.readLine()) != null) sb.append(line); while ((line = br.readLine()) != null) sb.append(line);
return sb.toString(); return sb.toString();
} }
@@ -307,30 +214,21 @@ public class DiscordBridge {
// ===== JSON Mini-Parser ===== // ===== JSON Mini-Parser =====
/** Repräsentiert eine Discord-Nachricht (minimale Felder). */
private static class DiscordMessage { private static class DiscordMessage {
long id; long id;
String authorId = ""; String authorId = "", authorName = "", content = "";
String authorName = "";
String content = "";
boolean isBot = false; boolean isBot = false;
} }
/**
* Parst ein JSON-Array von Discord-Nachrichten ohne externe Bibliothek.
* Nur die benötigten Felder werden extrahiert.
*/
private java.util.List<DiscordMessage> parseMessages(String json) { private java.util.List<DiscordMessage> parseMessages(String json) {
java.util.List<DiscordMessage> result = new java.util.ArrayList<>(); java.util.List<DiscordMessage> result = new java.util.ArrayList<>();
// Jedes Objekt im Array extrahieren
int depth = 0, start = -1; int depth = 0, start = -1;
for (int i = 0; i < json.length(); i++) { for (int i = 0; i < json.length(); i++) {
char c = json.charAt(i); char c = json.charAt(i);
if (c == '{') { if (depth++ == 0) start = i; } if (c == '{') { if (depth++ == 0) start = i; }
else if (c == '}') { else if (c == '}') {
if (--depth == 0 && start != -1) { if (--depth == 0 && start != -1) {
String obj = json.substring(start, i + 1); DiscordMessage msg = parseMessage(json.substring(start, i + 1));
DiscordMessage msg = parseMessage(obj);
if (msg != null) result.add(msg); if (msg != null) result.add(msg);
start = -1; start = -1;
} }
@@ -342,26 +240,21 @@ public class DiscordBridge {
private DiscordMessage parseMessage(String obj) { private DiscordMessage parseMessage(String obj) {
try { try {
DiscordMessage msg = new DiscordMessage(); DiscordMessage msg = new DiscordMessage();
msg.id = Long.parseLong(extractJsonString(obj, "\"id\"")); msg.id = Long.parseLong(extractJsonString(obj, "id"));
msg.content = unescapeJson(extractJsonString(obj, "\"content\"")); msg.content = unescapeJson(extractJsonString(obj, "content"));
// Webhook-Nachrichten herausfiltern (Echo-Loop verhindern): // Webhook-Nachrichten als Bot markieren (Echo-Loop verhindern)
// Nachrichten die via Webhook gesendet wurden haben "webhook_id" gesetzt. if (!extractJsonString(obj, "webhook_id").isEmpty()) {
// Das sind unsere eigenen Minecraft→Discord Nachrichten die wir ignorieren. msg.isBot = true;
String webhookId = extractJsonString(obj, "\"webhook_id\"");
if (!webhookId.isEmpty()) {
msg.isBot = true; // Als Bot markieren → wird übersprungen
return msg; return msg;
} }
// Author-Block
int authStart = obj.indexOf("\"author\""); int authStart = obj.indexOf("\"author\"");
if (authStart >= 0) { if (authStart >= 0) {
String authBlock = extractJsonObject(obj, authStart); String authBlock = extractJsonObject(obj, authStart);
msg.authorId = extractJsonString(authBlock, "\"id\""); msg.authorId = extractJsonString(authBlock, "id");
msg.authorName = unescapeJson(extractJsonString(authBlock, "\"username\"")); msg.authorName = unescapeJson(extractJsonString(authBlock, "username"));
String botFlag = extractJsonString(authBlock, "\"bot\""); msg.isBot = "true".equals(extractJsonString(authBlock, "bot"));
msg.isBot = "true".equals(botFlag);
} }
return msg; return msg;
} catch (Exception e) { } catch (Exception e) {
@@ -369,26 +262,39 @@ public class DiscordBridge {
} }
} }
/**
* FIX #12: Escape-Sequenzen werden korrekt mit einem Escape-Flag behandelt
* statt den Vorgänger-Char zu vergleichen (der bei '\\' + '"' versagt).
* Gibt immer einen leeren String zurück wenn der Key nicht gefunden wird (nie null).
*/
private String extractJsonString(String json, String key) { private String extractJsonString(String json, String key) {
int keyIdx = json.indexOf(key); if (json == null || key == null) return "";
String fullKey = "\"" + key + "\"";
int keyIdx = json.indexOf(fullKey);
if (keyIdx < 0) return ""; if (keyIdx < 0) return "";
int colon = json.indexOf(':', keyIdx + key.length()); int colon = json.indexOf(':', keyIdx + fullKey.length());
if (colon < 0) return ""; if (colon < 0) return "";
// Wert direkt nach dem Doppelpunkt
int valStart = colon + 1; int valStart = colon + 1;
while (valStart < json.length() && json.charAt(valStart) == ' ') valStart++; while (valStart < json.length() && json.charAt(valStart) == ' ') valStart++;
if (valStart >= json.length()) return ""; if (valStart >= json.length()) return "";
char first = json.charAt(valStart); char first = json.charAt(valStart);
if (first == '"') { if (first == '"') {
// String-Wert // FIX: Expliziter Escape-Flag statt Vorgänger-Char-Vergleich
int end = valStart + 1; int end = valStart + 1;
boolean escaped = false;
while (end < json.length()) { while (end < json.length()) {
if (json.charAt(end) == '"' && json.charAt(end - 1) != '\\') break; char ch = json.charAt(end);
if (escaped) {
escaped = false;
} else if (ch == '\\') {
escaped = true;
} else if (ch == '"') {
break;
}
end++; end++;
} }
return json.substring(valStart + 1, end); return json.substring(valStart + 1, end);
} else { } else {
// Primitiver Wert (Zahl, Boolean)
int end = valStart; int end = valStart;
while (end < json.length() && ",}\n".indexOf(json.charAt(end)) < 0) end++; while (end < json.length() && ",}\n".indexOf(json.charAt(end)) < 0) end++;
return json.substring(valStart, end).trim(); return json.substring(valStart, end).trim();
@@ -407,18 +313,11 @@ public class DiscordBridge {
private static String escapeJson(String s) { private static String escapeJson(String s) {
if (s == null) return ""; if (s == null) return "";
return s.replace("\\", "\\\\") return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t");
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
} }
private static String unescapeJson(String s) { private static String unescapeJson(String s) {
if (s == null) return ""; if (s == null) return "";
return s.replace("\\\"", "\"") return s.replace("\\\"", "\"").replace("\\n", "\n").replace("\\r", "\r").replace("\\\\", "\\");
.replace("\\n", "\n")
.replace("\\r", "\r")
.replace("\\\\", "\\");
} }
} }

View File

@@ -26,89 +26,54 @@ import java.util.concurrent.TimeUnit;
/** /**
* ForumBridgeModule: Verbindet das WP Business Forum mit dem Minecraft-Server. * ForumBridgeModule: Verbindet das WP Business Forum mit dem Minecraft-Server.
* *
* HTTP-Endpoints (werden vom StatusAPI WebServer geroutet): * Fix #13: extractJsonString() gibt jetzt immer einen leeren String statt null zurück.
* POST /forum/notify — WordPress pusht Benachrichtigung * Alle Aufrufer müssen nicht mehr auf null prüfen, was NullPointerExceptions verhindert.
* POST /forum/unlink — WordPress informiert über Verknüpfungslösung
* GET /forum/status — Verbindungstest
*
* Commands:
* /forumlink <token> — Account mit Forum verknüpfen
* /forum — Ausstehende Benachrichtigungen anzeigen
*/ */
public class ForumBridgeModule implements Module, Listener { public class ForumBridgeModule implements Module, Listener {
private Plugin plugin; private Plugin plugin;
private ForumNotifStorage storage; private ForumNotifStorage storage;
// Konfiguration aus verify.properties
private boolean enabled = true; private boolean enabled = true;
private String wpBaseUrl = ""; private String wpBaseUrl = "";
private String apiSecret = ""; private String apiSecret = "";
private int loginDelaySeconds = 3; private int loginDelaySeconds = 3;
@Override @Override
public String getName() { public String getName() { return "ForumBridgeModule"; }
return "ForumBridgeModule";
}
@Override @Override
public void onEnable(Plugin plugin) { public void onEnable(Plugin plugin) {
this.plugin = plugin; this.plugin = plugin;
// Config laden
loadConfig(plugin); loadConfig(plugin);
if (!enabled) { plugin.getLogger().info("ForumBridgeModule ist deaktiviert."); return; }
if (!enabled) {
plugin.getLogger().info("ForumBridgeModule ist deaktiviert.");
return;
}
// Storage initialisieren und laden
storage = new ForumNotifStorage(plugin.getDataFolder(), plugin.getLogger()); storage = new ForumNotifStorage(plugin.getDataFolder(), plugin.getLogger());
storage.load(); storage.load();
// Event Listener registrieren
plugin.getProxy().getPluginManager().registerListener(plugin, this); plugin.getProxy().getPluginManager().registerListener(plugin, this);
// Commands registrieren
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new ForumLinkCommand()); ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new ForumLinkCommand());
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new ForumCommand()); ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new ForumCommand());
// Auto-Save alle 10 Minuten
plugin.getProxy().getScheduler().schedule(plugin, () -> { plugin.getProxy().getScheduler().schedule(plugin, () -> {
try { try { storage.save(); } catch (Exception e) { plugin.getLogger().warning("ForumBridge Auto-Save Fehler: " + e.getMessage()); }
storage.save();
} catch (Exception e) {
plugin.getLogger().warning("ForumBridge Auto-Save Fehler: " + e.getMessage());
}
}, 10, 10, TimeUnit.MINUTES); }, 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.getProxy().getScheduler().schedule(plugin, () -> {
storage.purgeOld(30);
}, 1, 24, TimeUnit.HOURS);
plugin.getLogger().info("ForumBridgeModule aktiviert."); plugin.getLogger().info("ForumBridgeModule aktiviert.");
} }
@Override @Override
public void onDisable(Plugin plugin) { public void onDisable(Plugin plugin) {
if (storage != null) { if (storage != null) { storage.save(); plugin.getLogger().info("Forum-Benachrichtigungen gespeichert."); }
storage.save();
plugin.getLogger().info("Forum-Benachrichtigungen gespeichert.");
} }
}
// ===== CONFIG =====
private void loadConfig(Plugin plugin) { private void loadConfig(Plugin plugin) {
try { try {
Properties props = new Properties(); Properties props = new Properties();
File configFile = new File(plugin.getDataFolder(), "verify.properties"); File configFile = new File(plugin.getDataFolder(), "verify.properties");
if (configFile.exists()) { if (configFile.exists()) {
try (FileInputStream fis = new FileInputStream(configFile)) { try (FileInputStream fis = new FileInputStream(configFile)) { props.load(fis); }
props.load(fis);
}
} }
this.enabled = !"false".equalsIgnoreCase(props.getProperty("forum.enabled", "true")); this.enabled = !"false".equalsIgnoreCase(props.getProperty("forum.enabled", "true"));
this.wpBaseUrl = props.getProperty("forum.wp_url", props.getProperty("wp_verify_url", "")); this.wpBaseUrl = props.getProperty("forum.wp_url", props.getProperty("wp_verify_url", ""));
@@ -119,60 +84,34 @@ public class ForumBridgeModule implements Module, Listener {
} }
} }
private int parseInt(String s, int def) { private int parseInt(String s, int def) { try { return Integer.parseInt(s); } catch (Exception e) { return def; } }
try { return Integer.parseInt(s); } catch (Exception e) { return def; }
}
// ===== HTTP HANDLER (aufgerufen vom StatusAPI WebServer) ===== // ===== HTTP HANDLER =====
/**
* 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) { public String handleNotify(String body, String apiKeyHeader) {
// API-Key prüfen
if (!apiSecret.isEmpty() && !apiSecret.equals(apiKeyHeader)) { if (!apiSecret.isEmpty() && !apiSecret.equals(apiKeyHeader)) {
return "{\"success\":false,\"error\":\"unauthorized\"}"; return "{\"success\":false,\"error\":\"unauthorized\"}";
} }
// FIX #13: extractJsonString gibt "" statt null → kein NullPointerException möglich
String playerUuid = extractJsonString(body, "player_uuid"); String playerUuid = extractJsonString(body, "player_uuid");
String type = extractJsonString(body, "type"); String type = extractJsonString(body, "type");
String title = extractJsonString(body, "title"); String title = extractJsonString(body, "title");
String author = extractJsonString(body, "author"); String author = extractJsonString(body, "author");
String url = extractJsonString(body, "url"); String url = extractJsonString(body, "url");
if (playerUuid == null || playerUuid.isEmpty()) { if (playerUuid.isEmpty()) return "{\"success\":false,\"error\":\"missing_player_uuid\"}";
return "{\"success\":false,\"error\":\"missing_player_uuid\"}";
}
java.util.UUID uuid; java.util.UUID uuid;
try { try { uuid = java.util.UUID.fromString(playerUuid); }
uuid = java.util.UUID.fromString(playerUuid); catch (Exception e) { return "{\"success\":false,\"error\":\"invalid_uuid\"}"; }
} catch (Exception e) {
return "{\"success\":false,\"error\":\"invalid_uuid\"}";
}
// Fallback: Wenn type 'thread' und title enthält 'Umfrage', dann als 'poll' behandeln if ("thread".equalsIgnoreCase(type) && title.toLowerCase().contains("umfrage")) type = "poll";
if (type != null && type.equalsIgnoreCase("thread") && title != null && title.toLowerCase().contains("umfrage")) { if (type.isEmpty()) type = "reply";
type = "poll";
}
if (type == null || type.isEmpty()) type = "reply";
// Notification erstellen // Alle Werte sind garantiert nicht null (extractJsonString gibt "" zurück)
ForumNotification notification = new ForumNotification(uuid, type, title, author, url); ForumNotification notification = new ForumNotification(uuid, type, title, author, url);
// Sofort zustellen wenn online
ProxiedPlayer online = ProxyServer.getInstance().getPlayer(uuid); ProxiedPlayer online = ProxyServer.getInstance().getPlayer(uuid);
if (online != null && online.isConnected()) { if (online != null && online.isConnected()) {
deliverNotification(online, notification); deliverNotification(online, notification);
@@ -180,62 +119,30 @@ public class ForumBridgeModule implements Module, Listener {
return "{\"success\":true,\"delivered\":true}"; return "{\"success\":true,\"delivered\":true}";
} }
// Offline → speichern für späteren Login
storage.add(notification); storage.add(notification);
return "{\"success\":true,\"delivered\":false}"; 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) { public String handleUnlink(String body, String apiKeyHeader) {
if (!apiSecret.isEmpty() && !apiSecret.equals(apiKeyHeader)) { if (!apiSecret.isEmpty() && !apiSecret.equals(apiKeyHeader)) return "{\"success\":false,\"error\":\"unauthorized\"}";
return "{\"success\":false,\"error\":\"unauthorized\"}";
}
// Aktuell keine lokale Aktion nötig — die Zuordnung liegt in WordPress
return "{\"success\":true}"; return "{\"success\":true}";
} }
/**
* Verarbeitet GET /forum/status — Verbindungstest.
*/
public String handleStatus() { public String handleStatus() {
String version = "unknown"; String version = "unknown";
try { try { if (plugin.getDescription() != null) version = plugin.getDescription().getVersion(); } catch (Exception ignored) {}
if (plugin.getDescription() != null) {
version = plugin.getDescription().getVersion();
}
} catch (Exception ignored) {}
return "{\"success\":true,\"module\":\"ForumBridgeModule\",\"version\":\"" + version + "\"}"; return "{\"success\":true,\"module\":\"ForumBridgeModule\",\"version\":\"" + version + "\"}";
} }
// ===== NOTIFICATION ZUSTELLUNG ===== // ===== NOTIFICATION =====
/**
* Stellt eine einzelne Benachrichtigung an einen Online-Spieler zu.
*/
private void deliverNotification(ProxiedPlayer player, ForumNotification notif) { private void deliverNotification(ProxiedPlayer player, ForumNotification notif) {
String color = notif.getTypeColor(); String color = notif.getTypeColor();
String label = notif.getTypeLabel(); String label = notif.getTypeLabel();
// Trennlinie
player.sendMessage(new TextComponent("§8§m ")); player.sendMessage(new TextComponent("§8§m "));
player.sendMessage(new TextComponent("§6§l✉ Forum §8» " + color + label));
// Hauptnachricht if (!notif.getTitle().isEmpty()) player.sendMessage(new TextComponent("§7 " + notif.getTitle()));
TextComponent header = new TextComponent("§6§l✉ Forum §8» " + color + label); if (!notif.getAuthor().isEmpty()) player.sendMessage(new TextComponent("§7 von §f" + notif.getAuthor()));
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()) { if (!notif.getUrl().isEmpty()) {
TextComponent link = new TextComponent("§a ➜ Im Forum ansehen"); TextComponent link = new TextComponent("§a ➜ Im Forum ansehen");
link.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, notif.getUrl())); link.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, notif.getUrl()));
@@ -243,94 +150,56 @@ public class ForumBridgeModule implements Module, Listener {
new ComponentBuilder("§7Klicke um den Beitrag im Forum zu öffnen").create())); new ComponentBuilder("§7Klicke um den Beitrag im Forum zu öffnen").create()));
player.sendMessage(link); player.sendMessage(link);
} }
// Trennlinie
player.sendMessage(new TextComponent("§8§m ")); 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) { private void deliverPending(ProxiedPlayer player) {
List<ForumNotification> pending = storage.getPending(player.getUniqueId()); List<ForumNotification> pending = storage.getPending(player.getUniqueId());
if (pending.isEmpty()) return; if (pending.isEmpty()) return;
int count = pending.size(); int count = pending.size();
// Zusammenfassung wenn mehr als 3
if (count > 3) { if (count > 3) {
player.sendMessage(new TextComponent("§8§m ")); player.sendMessage(new TextComponent("§8§m "));
player.sendMessage(new TextComponent("§6§l✉ Forum §8» §fDu hast §e" + count + " §fneue Benachrichtigungen!")); 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("§7 Tippe §e/forum §7um sie anzuzeigen."));
player.sendMessage(new TextComponent("§8§m ")); player.sendMessage(new TextComponent("§8§m "));
} else { } else {
// Einzeln zustellen for (ForumNotification n : pending) deliverNotification(player, n);
for (ForumNotification n : pending) {
deliverNotification(player, n);
} }
}
// Alle als zugestellt markieren und aufräumen
storage.markAllDelivered(player.getUniqueId()); storage.markAllDelivered(player.getUniqueId());
storage.clearDelivered(player.getUniqueId()); storage.clearDelivered(player.getUniqueId());
} }
// ===== EVENTS =====
@EventHandler @EventHandler
public void onJoin(PostLoginEvent e) { public void onJoin(PostLoginEvent e) {
ProxiedPlayer player = e.getPlayer(); ProxiedPlayer player = e.getPlayer();
// Verzögert zustellen damit der Spieler den Server-Wechsel abgeschlossen hat
plugin.getProxy().getScheduler().schedule(plugin, () -> { plugin.getProxy().getScheduler().schedule(plugin, () -> {
if (player.isConnected()) { if (player.isConnected()) deliverPending(player);
deliverPending(player);
}
}, loginDelaySeconds, TimeUnit.SECONDS); }, loginDelaySeconds, TimeUnit.SECONDS);
} }
// ===== COMMANDS ===== // ===== COMMANDS =====
/**
* /forumlink <token> — Verknüpft den MC-Account mit dem Forum.
*/
private class ForumLinkCommand extends Command { private class ForumLinkCommand extends Command {
public ForumLinkCommand() { super("forumlink", null, "fl"); }
public ForumLinkCommand() {
super("forumlink", null, "fl");
}
@Override @Override
public void execute(CommandSender sender, String[] args) { public void execute(CommandSender sender, String[] args) {
if (!(sender instanceof ProxiedPlayer)) { if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(new TextComponent("§cNur Spieler können diesen Befehl benutzen.")); return; }
sender.sendMessage(new TextComponent("§cNur Spieler können diesen Befehl benutzen."));
return;
}
ProxiedPlayer p = (ProxiedPlayer) sender; ProxiedPlayer p = (ProxiedPlayer) sender;
if (args.length != 1) { if (args.length != 1) {
p.sendMessage(new TextComponent("§eBenutzung: §f/forumlink <token>")); p.sendMessage(new TextComponent("§eBenutzung: §f/forumlink <token>"));
p.sendMessage(new TextComponent("§7Den Token erhältst du in deinem Forum-Profil unter §fMinecraft-Verknüpfung§7.")); p.sendMessage(new TextComponent("§7Den Token erhältst du in deinem Forum-Profil unter §fMinecraft-Verknüpfung§7."));
return; return;
} }
String token = args[0].trim().toUpperCase(); String token = args[0].trim().toUpperCase();
if (wpBaseUrl.isEmpty()) { p.sendMessage(new TextComponent("§cForum-Verknüpfung ist nicht konfiguriert.")); return; }
if (wpBaseUrl.isEmpty()) {
p.sendMessage(new TextComponent("§cForum-Verknüpfung ist nicht konfiguriert."));
return;
}
p.sendMessage(new TextComponent("§7Überprüfe Token...")); p.sendMessage(new TextComponent("§7Überprüfe Token..."));
// Asynchron an WordPress senden
plugin.getProxy().getScheduler().runAsync(plugin, () -> { plugin.getProxy().getScheduler().runAsync(plugin, () -> {
try { try {
String endpoint = wpBaseUrl + "/wp-json/mc-bridge/v1/verify-link"; String endpoint = wpBaseUrl + "/wp-json/mc-bridge/v1/verify-link";
String payload = "{\"token\":\"" + escapeJson(token) + "\"," String payload = "{\"token\":\"" + escapeJson(token) + "\","
+ "\"mc_uuid\":\"" + p.getUniqueId().toString() + "\"," + "\"mc_uuid\":\"" + p.getUniqueId() + "\","
+ "\"mc_name\":\"" + escapeJson(p.getName()) + "\"}"; + "\"mc_name\":\"" + escapeJson(p.getName()) + "\"}";
HttpURLConnection conn = (HttpURLConnection) new URL(endpoint).openConnection(); HttpURLConnection conn = (HttpURLConnection) new URL(endpoint).openConnection();
@@ -339,50 +208,32 @@ public class ForumBridgeModule implements Module, Listener {
conn.setConnectTimeout(5000); conn.setConnectTimeout(5000);
conn.setReadTimeout(7000); conn.setReadTimeout(7000);
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
if (!apiSecret.isEmpty()) { if (!apiSecret.isEmpty()) conn.setRequestProperty("X-Api-Key", apiSecret);
conn.setRequestProperty("X-Api-Key", apiSecret);
}
Charset utf8 = Charset.forName("UTF-8"); Charset utf8 = Charset.forName("UTF-8");
try (OutputStream os = conn.getOutputStream()) { try (OutputStream os = conn.getOutputStream()) { os.write(payload.getBytes(utf8)); }
os.write(payload.getBytes(utf8));
}
int code = conn.getResponseCode(); int code = conn.getResponseCode();
String resp; String resp = code >= 200 && code < 300
if (code >= 200 && code < 300) { ? streamToString(conn.getInputStream(), utf8)
resp = streamToString(conn.getInputStream(), utf8); : streamToString(conn.getErrorStream(), utf8);
} else {
resp = streamToString(conn.getErrorStream(), utf8);
}
// Antwort auswerten
if (resp != null && resp.contains("\"success\":true")) { if (resp != null && resp.contains("\"success\":true")) {
String displayName = extractJsonString(resp, "display_name"); String displayName = extractJsonString(resp, "display_name");
String username = extractJsonString(resp, "username"); String username = extractJsonString(resp, "username");
String show = (displayName != null && !displayName.isEmpty()) ? displayName : username; String show = !displayName.isEmpty() ? displayName : username;
p.sendMessage(new TextComponent("§8§m ")); p.sendMessage(new TextComponent("§8§m "));
p.sendMessage(new TextComponent("§a§l✓ §fForum-Account erfolgreich verknüpft!")); p.sendMessage(new TextComponent("§a§l✓ §fForum-Account erfolgreich verknüpft!"));
if (show != null && !show.isEmpty()) { if (!show.isEmpty()) p.sendMessage(new TextComponent("§7 Forum-User: §f" + show));
p.sendMessage(new TextComponent("§7 Forum-User: §f" + show));
}
p.sendMessage(new TextComponent("§7 Du erhältst jetzt Ingame-Benachrichtigungen.")); p.sendMessage(new TextComponent("§7 Du erhältst jetzt Ingame-Benachrichtigungen."));
p.sendMessage(new TextComponent("§8§m ")); p.sendMessage(new TextComponent("§8§m "));
} else { } else {
// Fehlermeldung auslesen
String error = extractJsonString(resp, "error"); String error = extractJsonString(resp, "error");
String message = extractJsonString(resp, "message"); String message = extractJsonString(resp, "message");
if ("token_expired".equals(error)) p.sendMessage(new TextComponent("§c✗ Der Token ist abgelaufen."));
if ("token_expired".equals(error)) { else if ("uuid_already_linked".equals(error)) p.sendMessage(new TextComponent("§c✗ " + (!message.isEmpty() ? message : "Diese UUID ist bereits verknüpft.")));
p.sendMessage(new TextComponent("§c✗ Der Token ist abgelaufen. Generiere einen neuen im Forum.")); else if ("invalid_token".equals(error)) p.sendMessage(new TextComponent("§c✗ Ungültiger Token."));
} else if ("uuid_already_linked".equals(error)) { else p.sendMessage(new TextComponent("§c✗ Verknüpfung fehlgeschlagen: " + (!error.isEmpty() ? error : "Unbekannter Fehler")));
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) { } catch (Exception ex) {
p.sendMessage(new TextComponent("§c✗ Fehler bei der Verbindung zum Forum.")); p.sendMessage(new TextComponent("§c✗ Fehler bei der Verbindung zum Forum."));
@@ -392,34 +243,21 @@ public class ForumBridgeModule implements Module, Listener {
} }
} }
/**
* /forum — Zeigt ausstehende Forum-Benachrichtigungen an.
*/
private class ForumCommand extends Command { private class ForumCommand extends Command {
public ForumCommand() { super("forum"); }
public ForumCommand() {
super("forum");
}
@Override @Override
public void execute(CommandSender sender, String[] args) { public void execute(CommandSender sender, String[] args) {
if (!(sender instanceof ProxiedPlayer)) { if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(new TextComponent("§cNur Spieler können diesen Befehl benutzen.")); return; }
sender.sendMessage(new TextComponent("§cNur Spieler können diesen Befehl benutzen."));
return;
}
ProxiedPlayer p = (ProxiedPlayer) sender; ProxiedPlayer p = (ProxiedPlayer) sender;
List<ForumNotification> pending = storage.getPending(p.getUniqueId()); List<ForumNotification> pending = storage.getPending(p.getUniqueId());
if (pending.isEmpty()) { if (pending.isEmpty()) {
p.sendMessage(new TextComponent("§7Keine neuen Forum-Benachrichtigungen.")); p.sendMessage(new TextComponent("§7Keine neuen Forum-Benachrichtigungen."));
// Forum-Link anzeigen wenn konfiguriert
if (!wpBaseUrl.isEmpty()) { if (!wpBaseUrl.isEmpty()) {
TextComponent link = new TextComponent("§a➜ Forum öffnen"); TextComponent link = new TextComponent("§a➜ Forum öffnen");
link.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, wpBaseUrl)); link.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, wpBaseUrl));
link.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, link.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("§7Klicke um das Forum zu öffnen").create()));
new ComponentBuilder("§7Klicke um das Forum zu öffnen").create()));
p.sendMessage(link); p.sendMessage(link);
} }
return; return;
@@ -428,39 +266,22 @@ public class ForumBridgeModule implements Module, Listener {
p.sendMessage(new TextComponent("§8§m ")); p.sendMessage(new TextComponent("§8§m "));
p.sendMessage(new TextComponent("§6§l✉ Forum-Benachrichtigungen §8(§f" + pending.size() + "§8)")); p.sendMessage(new TextComponent("§6§l✉ Forum-Benachrichtigungen §8(§f" + pending.size() + "§8)"));
p.sendMessage(new TextComponent("")); p.sendMessage(new TextComponent(""));
int shown = 0; int shown = 0;
for (ForumNotification n : pending) { for (ForumNotification n : pending) {
if (shown >= 10) { if (shown >= 10) { p.sendMessage(new TextComponent("§7 ... und " + (pending.size() - 10) + " weitere")); break; }
p.sendMessage(new TextComponent("§7 ... und " + (pending.size() - 10) + " weitere"));
break;
}
String color = n.getTypeColor(); String color = n.getTypeColor();
TextComponent line = new TextComponent(color + "" + n.getTypeLabel() + "§7: "); TextComponent line = new TextComponent(color + "" + n.getTypeLabel() + "§7: ");
TextComponent detail = new TextComponent(!n.getTitle().isEmpty() ? "§f" + n.getTitle() : "§fvon " + n.getAuthor());
TextComponent detail;
if (!n.getTitle().isEmpty()) {
detail = new TextComponent("§f" + n.getTitle());
} else {
detail = new TextComponent("§fvon " + n.getAuthor());
}
if (!n.getUrl().isEmpty()) { if (!n.getUrl().isEmpty()) {
detail.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, n.getUrl())); detail.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, n.getUrl()));
detail.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, detail.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("§7Klicke zum Öffnen").create()));
new ComponentBuilder("§7Klicke zum Öffnen").create()));
} }
line.addExtra(detail); line.addExtra(detail);
p.sendMessage(line); p.sendMessage(line);
shown++; shown++;
} }
p.sendMessage(new TextComponent("")); p.sendMessage(new TextComponent(""));
p.sendMessage(new TextComponent("§8§m ")); p.sendMessage(new TextComponent("§8§m "));
// Alle als gelesen markieren
storage.markAllDelivered(p.getUniqueId()); storage.markAllDelivered(p.getUniqueId());
storage.clearDelivered(p.getUniqueId()); storage.clearDelivered(p.getUniqueId());
} }
@@ -468,21 +289,22 @@ public class ForumBridgeModule implements Module, Listener {
// ===== HELPER ===== // ===== HELPER =====
/** Getter für den Storage (für StatusAPI HTTP-Handler) */ public ForumNotifStorage getStorage() { return storage; }
public ForumNotifStorage getStorage() {
return storage;
}
/**
* FIX #13: Gibt immer einen leeren String zurück, niemals null.
* Verhindert NullPointerExceptions in allen Aufrufern.
*/
private static String extractJsonString(String json, String key) { private static String extractJsonString(String json, String key) {
if (json == null || key == null) return null; if (json == null || key == null) return "";
String search = "\"" + key + "\""; String search = "\"" + key + "\"";
int idx = json.indexOf(search); int idx = json.indexOf(search);
if (idx < 0) return null; if (idx < 0) return "";
int colon = json.indexOf(':', idx + search.length()); int colon = json.indexOf(':', idx + search.length());
if (colon < 0) return null; if (colon < 0) return "";
int i = colon + 1; int i = colon + 1;
while (i < json.length() && Character.isWhitespace(json.charAt(i))) i++; while (i < json.length() && Character.isWhitespace(json.charAt(i))) i++;
if (i >= json.length()) return null; if (i >= json.length()) return "";
char c = json.charAt(i); char c = json.charAt(i);
if (c == '"') { if (c == '"') {
i++; i++;
@@ -491,15 +313,11 @@ public class ForumBridgeModule implements Module, Listener {
while (i < json.length()) { while (i < json.length()) {
char ch = json.charAt(i++); char ch = json.charAt(i++);
if (escape) { sb.append(ch); escape = false; } if (escape) { sb.append(ch); escape = false; }
else { else { if (ch == '\\') escape = true; else if (ch == '"') break; else sb.append(ch); }
if (ch == '\\') escape = true;
else if (ch == '"') break;
else sb.append(ch);
}
} }
return sb.toString(); return sb.toString();
} }
return null; return "";
} }
private static String escapeJson(String s) { private static String escapeJson(String s) {
@@ -510,8 +328,7 @@ public class ForumBridgeModule implements Module, Listener {
private static String streamToString(InputStream in, Charset charset) throws IOException { private static String streamToString(InputStream in, Charset charset) throws IOException {
if (in == null) return ""; if (in == null) return "";
try (BufferedReader br = new BufferedReader(new InputStreamReader(in, charset))) { try (BufferedReader br = new BufferedReader(new InputStreamReader(in, charset))) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder(); String line;
String line;
while ((line = br.readLine()) != null) sb.append(line); while ((line = br.readLine()) != null) sb.append(line);
return sb.toString(); return sb.toString();
} }

View File

@@ -82,7 +82,7 @@ public class NetworkInfoModule implements Module {
loadConfig(); loadConfig();
if (!enabled) { if (!enabled) {
plugin.getLogger().info("[NetworkInfoModule] deaktiviert via " + CONFIG_FILE_NAME + " (networkinfo.enabled=false)");
return; return;
} }
@@ -109,7 +109,7 @@ public class NetworkInfoModule implements Module {
alertTask = ProxyServer.getInstance().getScheduler().schedule(plugin, this::evaluateAndSendAlerts, interval, interval, TimeUnit.SECONDS); alertTask = ProxyServer.getInstance().getScheduler().schedule(plugin, this::evaluateAndSendAlerts, interval, interval, TimeUnit.SECONDS);
} }
plugin.getLogger().info("[NetworkInfoModule] aktiviert. commandEnabled=" + commandEnabled + ", includePlayerNames=" + includePlayerNames + ", webhookEnabled=" + webhookEnabled + ", notifyStartStop=" + webhookNotifyStartStop + ", webhookUrlPresent=" + !webhookUrl.isEmpty());
} }
@Override @Override
@@ -514,7 +514,7 @@ public class NetworkInfoModule implements Module {
while ((read = in.read(buffer)) != -1) { while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read); out.write(buffer, 0, read);
} }
plugin.getLogger().info("[NetworkInfoModule] " + CONFIG_FILE_NAME + " wurde erstellt.");
} catch (Exception e) { } catch (Exception e) {
plugin.getLogger().warning("[NetworkInfoModule] Konnte " + CONFIG_FILE_NAME + " nicht erstellen: " + e.getMessage()); plugin.getLogger().warning("[NetworkInfoModule] Konnte " + CONFIG_FILE_NAME + " nicht erstellen: " + e.getMessage());
} }

View File

@@ -0,0 +1,269 @@
package net.viper.status.modules.vanish;
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.connection.ProxiedPlayer;
import net.md_5.bungee.api.event.PlayerDisconnectEvent;
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.md_5.bungee.event.EventPriority;
import net.viper.status.module.Module;
import net.viper.status.modules.chat.VanishProvider;
import java.io.*;
import java.util.Collections;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* VanishModule für StatusAPI (BungeeCord)
*
* Features:
* - /vanish zum Ein-/Ausschalten
* - /vanish <Spieler> für Admin-Vanish anderer Spieler
* - /vanishlist zeigt alle aktuell unsichtbaren Spieler
* - Vanish-Status wird persistent in vanish.dat gespeichert
* - Beim Login wird gespeicherter Status wiederhergestellt
* - Volle Integration mit VanishProvider → ChatModule sieht den Status
*
* Permission:
* - vanish.use → darf vanishen
* - vanish.other → darf andere Spieler vanishen
* - vanish.list → darf /vanishlist nutzen
* - chat.admin.bypass → sieht Vanish-Join/Leave-Meldungen im Chat
*/
public class VanishModule implements Module, Listener {
private static final String PERMISSION = "chat.admin.bypass";
private static final String PERMISSION_OTHER = "chat.admin.bypass";
private static final String PERMISSION_LIST = "chat.admin.bypass";
private Plugin plugin;
// Persistente Vanish-UUIDs (werden in vanish.dat gespeichert)
private final Set<UUID> persistentVanished =
Collections.newSetFromMap(new ConcurrentHashMap<>());
private File dataFile;
@Override
public String getName() {
return "VanishModule";
}
@Override
public void onEnable(Plugin plugin) {
this.plugin = plugin;
this.dataFile = new File(plugin.getDataFolder(), "vanish.dat");
load();
plugin.getProxy().getPluginManager().registerListener(plugin, this);
registerCommands();
}
@Override
public void onDisable(Plugin plugin) {
save();
// Alle als sichtbar markieren beim Shutdown (damit beim nächsten Start
// der VanishProvider sauber ist load() setzt sie beim Login neu)
for (UUID uuid : persistentVanished) {
VanishProvider.setVanished(uuid, false);
}
}
// =========================================================
// EVENTS
// =========================================================
/**
* Beim Login: Wenn der Spieler persistent gevanisht war, sofort
* in den VanishProvider eintragen BEVOR das ChatModule die
* Join-Nachricht nach 2 Sekunden sendet.
*/
@EventHandler(priority = EventPriority.LOWEST)
public void onLogin(PostLoginEvent e) {
ProxiedPlayer player = e.getPlayer();
if (persistentVanished.contains(player.getUniqueId())) {
VanishProvider.setVanished(player.getUniqueId(), true);
// Kurze Bestätigung an den Spieler selbst (nach kurzem Delay damit
// der Client bereit ist)
plugin.getProxy().getScheduler().schedule(plugin, () -> {
if (player.isConnected()) {
player.sendMessage(color("&8[&7Vanish&8] &7Du bist &cUnsichtbar&7."));
}
}, 1, java.util.concurrent.TimeUnit.SECONDS);
}
}
@EventHandler
public void onDisconnect(PlayerDisconnectEvent e) {
// VanishProvider cleanup der Eintrag in persistentVanished bleibt
// erhalten damit der Status beim nächsten Login wiederhergestellt wird
VanishProvider.cleanup(e.getPlayer().getUniqueId());
}
// =========================================================
// COMMANDS
// =========================================================
private void registerCommands() {
// /vanish [spieler]
plugin.getProxy().getPluginManager().registerCommand(plugin,
new Command("vanish", PERMISSION, "v") {
@Override
public void execute(CommandSender sender, String[] args) {
if (args.length == 0) {
// Sich selbst vanishen
if (!(sender instanceof ProxiedPlayer)) {
sender.sendMessage(color("&cNur Spieler!"));
return;
}
toggleVanish((ProxiedPlayer) sender, (ProxiedPlayer) sender);
} else {
// Anderen Spieler vanishen
if (!sender.hasPermission(PERMISSION_OTHER)) {
sender.sendMessage(color("&cDu hast keine Berechtigung für /vanish <Spieler>."));
return;
}
ProxiedPlayer target = ProxyServer.getInstance().getPlayer(args[0]);
if (target == null) {
sender.sendMessage(color("&cSpieler &f" + args[0] + " &cnicht gefunden."));
return;
}
toggleVanish(sender, target);
}
}
});
// /vanishlist
plugin.getProxy().getPluginManager().registerCommand(plugin,
new Command("vanishlist", PERMISSION_LIST, "vlist") {
@Override
public void execute(CommandSender sender, String[] args) {
Set<UUID> vanished = VanishProvider.getVanishedPlayers();
if (vanished.isEmpty()) {
sender.sendMessage(color("&8[Vanish] &7Keine unsichtbaren Spieler."));
return;
}
sender.sendMessage(color("&8[Vanish] &7Unsichtbare Spieler &8(" + vanished.size() + ")&7:"));
for (UUID uuid : vanished) {
ProxiedPlayer p = ProxyServer.getInstance().getPlayer(uuid);
String name = p != null ? p.getName() : uuid.toString().substring(0, 8) + "...";
String online = p != null ? " &8(online)" : " &8(offline/persistent)";
sender.sendMessage(color(" &8- &7" + name + online));
}
}
});
}
// =========================================================
// VANISH-LOGIK
// =========================================================
/**
* Schaltet den Vanish-Status eines Spielers um.
*
* @param executor Der Befehlsgeber (für Feedback-Nachrichten)
* @param target Der betroffene Spieler
*/
private void toggleVanish(CommandSender executor, ProxiedPlayer target) {
boolean nowVanished = !VanishProvider.isVanished(target);
setVanished(target, nowVanished);
String statusMsg = nowVanished
? "&8[&7Vanish&8] &f" + target.getName() + " &7ist jetzt &cUnsichtbar&7."
: "&8[&7Vanish&8] &f" + target.getName() + " &7ist jetzt &aSichtbar&7.";
// Feedback an den Ausführenden
executor.sendMessage(color(statusMsg));
// Falls jemand anderes gevanisht wurde, auch dem Ziel Bescheid geben
if (!executor.equals(target)) {
String selfMsg = nowVanished
? "&8[&7Vanish&8] &7Du wurdest &cUnsichtbar &7gemacht."
: "&8[&7Vanish&8] &7Du wurdest &aSichtbar &7gemacht.";
target.sendMessage(color(selfMsg));
}
// Admins mit chat.admin.bypass informieren (außer dem Ausführenden)
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
if (p.equals(executor) || p.equals(target)) continue;
if (p.hasPermission("chat.admin.bypass")) {
p.sendMessage(color(statusMsg));
}
}
}
/**
* Setzt den Vanish-Status direkt (ohne Toggle).
* Aktualisiert VanishProvider UND die persistente Liste.
*/
public void setVanished(ProxiedPlayer player, boolean vanished) {
VanishProvider.setVanished(player.getUniqueId(), vanished);
if (vanished) {
persistentVanished.add(player.getUniqueId());
} else {
persistentVanished.remove(player.getUniqueId());
}
save();
}
/**
* Öffentliche API für andere Module.
*/
public boolean isVanished(ProxiedPlayer player) {
return VanishProvider.isVanished(player);
}
// =========================================================
// PERSISTENZ
// =========================================================
private void save() {
try (BufferedWriter bw = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(dataFile), "UTF-8"))) {
for (UUID uuid : persistentVanished) {
bw.write(uuid.toString());
bw.newLine();
}
} catch (IOException e) {
plugin.getLogger().warning("[VanishModule] Fehler beim Speichern: " + e.getMessage());
}
}
private void load() {
persistentVanished.clear();
if (!dataFile.exists()) return;
try (BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream(dataFile), "UTF-8"))) {
String line;
while ((line = br.readLine()) != null) {
line = line.trim();
if (line.isEmpty()) continue;
try {
persistentVanished.add(UUID.fromString(line));
} catch (IllegalArgumentException ignored) {}
}
} catch (IOException e) {
plugin.getLogger().warning("[VanishModule] Fehler beim Laden: " + e.getMessage());
}
}
// =========================================================
// HILFSMETHODEN
// =========================================================
private TextComponent color(String text) {
return new TextComponent(ChatColor.translateAlternateColorCodes('&', text));
}
}

View File

@@ -20,91 +20,65 @@ import java.util.Properties;
/** /**
* VerifyModule: Multi-Server Support. * VerifyModule: Multi-Server Support.
* Liest pro Server die passende ID und das Secret aus der verify.properties. *
* Fix #7: Servernamen werden jetzt case-insensitiv verglichen.
* Keys in serverConfigs werden beim Laden auf lowercase normalisiert
* und die Suche erfolgt ebenfalls lowercase.
*/ */
public class VerifyModule implements Module { public class VerifyModule implements Module {
private String wpVerifyUrl; private String wpVerifyUrl;
// Speichert für jeden Servernamen (z.B. "Lobby") die passende Konfiguration // Keys sind lowercase normalisiert für case-insensitiven Vergleich
private final Map<String, ServerConfig> serverConfigs = new HashMap<>(); private final Map<String, ServerConfig> serverConfigs = new HashMap<>();
@Override @Override
public String getName() { public String getName() { return "VerifyModule"; }
return "VerifyModule";
}
@Override @Override
public void onEnable(Plugin plugin) { public void onEnable(Plugin plugin) {
loadConfig(plugin); loadConfig(plugin);
// Befehl registrieren
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new VerifyCommand()); ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new VerifyCommand());
plugin.getLogger().info("VerifyModule aktiviert. " + serverConfigs.size() + " Server-Konfigurationen geladen."); plugin.getLogger().info("VerifyModule aktiviert. " + serverConfigs.size() + " Server-Konfigurationen geladen.");
} }
@Override @Override
public void onDisable(Plugin plugin) { 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) { private void loadConfig(Plugin plugin) {
String fileName = "verify.properties"; String fileName = "verify.properties";
File configFile = new File(plugin.getDataFolder(), fileName); File configFile = new File(plugin.getDataFolder(), fileName);
Properties props = new Properties(); Properties props = new Properties();
// 1. Datei kopieren, falls sie noch nicht existiert
if (!configFile.exists()) { if (!configFile.exists()) {
plugin.getDataFolder().mkdirs(); plugin.getDataFolder().mkdirs();
try (InputStream in = plugin.getResourceAsStream(fileName); try (InputStream in = plugin.getResourceAsStream(fileName);
OutputStream out = new FileOutputStream(configFile)) { OutputStream out = new FileOutputStream(configFile)) {
if (in == null) { if (in == null) { plugin.getLogger().warning("Standard-config '" + fileName + "' nicht in JAR."); return; }
plugin.getLogger().warning("Standard-config '" + fileName + "' nicht in JAR gefunden. Erstelle manuell."); byte[] buffer = new byte[1024]; int length;
return; while ((length = in.read(buffer)) > 0) out.write(buffer, 0, length);
}
byte[] buffer = new byte[1024];
int length;
while ((length = in.read(buffer)) > 0) {
out.write(buffer, 0, length);
}
plugin.getLogger().info("Konfigurationsdatei '" + fileName + "' erstellt."); plugin.getLogger().info("Konfigurationsdatei '" + fileName + "' erstellt.");
} catch (Exception e) { } catch (Exception e) { plugin.getLogger().severe("Fehler beim Erstellen der Config: " + e.getMessage()); return; }
plugin.getLogger().severe("Fehler beim Erstellen der Config: " + e.getMessage());
return;
}
} }
// 2. Eigentliche Config laden
try (InputStream in = new FileInputStream(configFile)) { try (InputStream in = new FileInputStream(configFile)) {
props.load(in); props.load(in);
} catch (IOException e) { } catch (IOException e) { e.printStackTrace(); return; }
e.printStackTrace();
return;
}
// Globale URL
this.wpVerifyUrl = props.getProperty("wp_verify_url", "https://deine-wp-domain.tld"); this.wpVerifyUrl = props.getProperty("wp_verify_url", "https://deine-wp-domain.tld");
// Server-Configs parsen (z.B. server.Lobby.id) // FIX #7: Keys beim Laden auf lowercase normalisieren
this.serverConfigs.clear(); this.serverConfigs.clear();
for (String key : props.stringPropertyNames()) { for (String key : props.stringPropertyNames()) {
if (key.startsWith("server.")) { if (key.startsWith("server.")) {
// Key Struktur: server.<ServerName>.id oder .secret
String[] parts = key.split("\\."); String[] parts = key.split("\\.");
if (parts.length == 3) { if (parts.length == 3) {
String serverName = parts[1]; // Servername lowercase → case-insensitiver Lookup
String serverName = parts[1].toLowerCase();
String type = parts[2]; String type = parts[2];
// Eintrag in der Map erstellen oder holen
ServerConfig config = serverConfigs.computeIfAbsent(serverName, k -> new ServerConfig()); ServerConfig config = serverConfigs.computeIfAbsent(serverName, k -> new ServerConfig());
if ("id".equalsIgnoreCase(type)) { if ("id".equalsIgnoreCase(type)) {
try { try { config.serverId = Integer.parseInt(props.getProperty(key)); }
config.serverId = Integer.parseInt(props.getProperty(key)); catch (NumberFormatException e) { plugin.getLogger().warning("Ungültige Server ID für " + serverName); }
} catch (NumberFormatException e) {
plugin.getLogger().warning("Ungültige Server ID für " + serverName);
}
} else if ("secret".equalsIgnoreCase(type)) { } else if ("secret".equalsIgnoreCase(type)) {
config.sharedSecret = props.getProperty(key); config.sharedSecret = props.getProperty(key);
} }
@@ -113,42 +87,27 @@ public class VerifyModule implements Module {
} }
} }
// --- Hilfsklasse für die Daten eines Servers ---
private static class ServerConfig { private static class ServerConfig {
int serverId = 0; int serverId = 0;
String sharedSecret = ""; String sharedSecret = "";
} }
// --- Die Command Klasse ---
private class VerifyCommand extends Command { private class VerifyCommand extends Command {
public VerifyCommand() { super("verify"); }
public VerifyCommand() {
super("verify");
}
@Override @Override
public void execute(CommandSender sender, String[] args) { public void execute(CommandSender sender, String[] args) {
if (!(sender instanceof ProxiedPlayer)) { if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(ChatColor.RED + "Nur Spieler können diesen Befehl benutzen."); return; }
sender.sendMessage(ChatColor.RED + "Nur Spieler können diesen Befehl benutzen.");
return;
}
ProxiedPlayer p = (ProxiedPlayer) sender; ProxiedPlayer p = (ProxiedPlayer) sender;
if (args.length != 1) { if (args.length != 1) { p.sendMessage(ChatColor.YELLOW + "Benutzung: /verify <token>"); return; }
p.sendMessage(ChatColor.YELLOW + "Benutzung: /verify <token>");
return;
}
// --- WICHTIG: Servernamen ermitteln --- // FIX #7: Servername lowercase für case-insensitiven Lookup
String serverName = p.getServer().getInfo().getName(); String serverName = p.getServer().getInfo().getName().toLowerCase();
// Konfiguration für diesen Server laden
ServerConfig config = serverConfigs.get(serverName); ServerConfig config = serverConfigs.get(serverName);
// Check ob Konfig existiert
if (config == null || config.serverId == 0 || config.sharedSecret.isEmpty()) { 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.RED + "✗ Dieser Server ist nicht in der Verify-Konfiguration hinterlegt.");
p.sendMessage(ChatColor.GRAY + "Aktueller Servername: " + ChatColor.WHITE + serverName); p.sendMessage(ChatColor.GRAY + "Aktueller Servername: " + ChatColor.WHITE + p.getServer().getInfo().getName());
p.sendMessage(ChatColor.GRAY + "Bitte kontaktiere einen Admin."); p.sendMessage(ChatColor.GRAY + "Bitte kontaktiere einen Admin.");
return; return;
} }
@@ -159,11 +118,11 @@ public class VerifyModule implements Module {
HttpURLConnection conn = null; HttpURLConnection conn = null;
try { try {
Charset utf8 = Charset.forName("UTF-8"); Charset utf8 = Charset.forName("UTF-8");
// Wir signieren Name + Token mit dem SERVER-SPECIFISCHEN Secret
String signature = hmacSHA256(playerName + token, config.sharedSecret, utf8); String signature = hmacSHA256(playerName + token, config.sharedSecret, utf8);
String payload = "{\"player\":\"" + escapeJson(playerName)
// Payload aufbauen mit der SERVER-SPECIFISCHEN ID + "\",\"token\":\"" + escapeJson(token)
String payload = "{\"player\":\"" + escapeJson(playerName) + "\",\"token\":\"" + escapeJson(token) + "\",\"server_id\":" + config.serverId + ",\"signature\":\"" + signature + "\"}"; + "\",\"server_id\":" + config.serverId
+ ",\"signature\":\"" + signature + "\"}";
URL url = new URL(wpVerifyUrl + "/wp-json/mc-gallery/v1/verify"); URL url = new URL(wpVerifyUrl + "/wp-json/mc-gallery/v1/verify");
conn = (HttpURLConnection) url.openConnection(); conn = (HttpURLConnection) url.openConnection();
@@ -172,34 +131,22 @@ public class VerifyModule implements Module {
conn.setDoOutput(true); conn.setDoOutput(true);
conn.setRequestMethod("POST"); conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json; charset=utf-8"); conn.setRequestProperty("Content-Type", "application/json; charset=utf-8");
try (OutputStream os = conn.getOutputStream()) { os.write(payload.getBytes(utf8)); }
try (OutputStream os = conn.getOutputStream()) {
os.write(payload.getBytes(utf8));
}
int code = conn.getResponseCode(); int code = conn.getResponseCode();
String resp; String resp = code >= 200 && code < 300
? streamToString(conn.getInputStream(), utf8)
: streamToString(conn.getErrorStream(), utf8);
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("{")) { if (resp != null && !resp.isEmpty() && resp.trim().startsWith("{")) {
boolean isSuccess = resp.contains("\"success\":true"); boolean isSuccess = resp.contains("\"success\":true");
String message = "Ein unbekannter Fehler ist aufgetreten."; String message = "Ein unbekannter Fehler ist aufgetreten.";
int keyIndex = resp.indexOf("\"message\":\""); int keyIndex = resp.indexOf("\"message\":\"");
if (keyIndex != -1) { if (keyIndex != -1) {
int startIndex = keyIndex + 11; int startIndex = keyIndex + 11;
int endIndex = resp.indexOf("\"", startIndex); int endIndex = resp.indexOf("\"", startIndex);
if (endIndex != -1) { if (endIndex != -1) message = resp.substring(startIndex, endIndex);
message = resp.substring(startIndex, endIndex);
} }
}
if (isSuccess) { if (isSuccess) {
p.sendMessage(ChatColor.GREEN + "" + message); p.sendMessage(ChatColor.GREEN + "" + message);
p.sendMessage(ChatColor.GRAY + "Du kannst nun Bilder hochladen!"); p.sendMessage(ChatColor.GRAY + "Du kannst nun Bilder hochladen!");
@@ -209,20 +156,16 @@ public class VerifyModule implements Module {
} else { } else {
p.sendMessage(ChatColor.RED + "✗ Fehler beim Verbinden mit der Webseite (Code: " + code + ")"); p.sendMessage(ChatColor.RED + "✗ Fehler beim Verbinden mit der Webseite (Code: " + code + ")");
} }
} catch (Exception ex) { } catch (Exception ex) {
p.sendMessage(ChatColor.RED + "✗ Ein interner Fehler ist aufgetreten."); p.sendMessage(ChatColor.RED + "✗ Ein interner Fehler ist aufgetreten.");
ProxyServer.getInstance().getLogger().warning("Verify error: " + ex.getMessage()); ProxyServer.getInstance().getLogger().warning("Verify error: " + ex.getMessage());
ex.printStackTrace(); ex.printStackTrace();
} finally { } finally {
if (conn != null) { if (conn != null) conn.disconnect();
conn.disconnect();
}
} }
} }
} }
// --- Helper Methoden ---
private static String hmacSHA256(String data, String key, Charset charset) throws Exception { private static String hmacSHA256(String data, String key, Charset charset) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256"); Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key.getBytes(charset), "HmacSHA256")); mac.init(new SecretKeySpec(key.getBytes(charset), "HmacSHA256"));
@@ -235,14 +178,13 @@ public class VerifyModule implements Module {
private static String streamToString(InputStream in, Charset charset) throws IOException { private static String streamToString(InputStream in, Charset charset) throws IOException {
if (in == null) return ""; if (in == null) return "";
try (BufferedReader br = new BufferedReader(new InputStreamReader(in, charset))) { try (BufferedReader br = new BufferedReader(new InputStreamReader(in, charset))) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder(); String line;
String line;
while ((line = br.readLine()) != null) sb.append(line); while ((line = br.readLine()) != null) sb.append(line);
return sb.toString(); return sb.toString();
} }
} }
private static String escapeJson(String s) { private static String escapeJson(String s) {
return s.replace("\\", "\\\\").replace("\"","\\\"").replace("\n","\\n").replace("\r","\\r"); return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r");
} }
} }

View File

@@ -11,6 +11,20 @@ public class PlayerStats {
public long currentSessionStart; public long currentSessionStart;
public int joins; public int joins;
// Economy
public double balance;
public double totalEarned;
public double totalSpent;
public int transactionsCount;
// Punishments
public int bansCount;
public int mutesCount;
public int warnsCount;
public long lastPunishmentAt; // Unix-Timestamp (Sek.), 0 = nie
public String lastPunishmentType; // "ban", "mute", "warn", "kick", "" = nie
public int punishmentScore;
public PlayerStats(UUID uuid, String name) { public PlayerStats(UUID uuid, String name) {
this.uuid = uuid; this.uuid = uuid;
this.name = name; this.name = name;
@@ -20,6 +34,16 @@ public class PlayerStats {
this.totalPlaytime = 0; this.totalPlaytime = 0;
this.currentSessionStart = 0; this.currentSessionStart = 0;
this.joins = 0; this.joins = 0;
this.balance = 0.0;
this.totalEarned = 0.0;
this.totalSpent = 0.0;
this.transactionsCount = 0;
this.bansCount = 0;
this.mutesCount = 0;
this.warnsCount = 0;
this.lastPunishmentAt = 0;
this.lastPunishmentType = "";
this.punishmentScore = 0;
} }
public synchronized void onJoin() { public synchronized void onJoin() {
@@ -47,7 +71,10 @@ public class PlayerStats {
} }
public synchronized String toLine() { public synchronized String toLine() {
return uuid + "|" + name.replace("|", "_") + "|" + firstSeen + "|" + lastSeen + "|" + totalPlaytime + "|" + currentSessionStart + "|" + joins; String safeType = (lastPunishmentType == null ? "" : lastPunishmentType).replace("|", "_");
return uuid + "|" + name.replace("|", "_") + "|" + firstSeen + "|" + lastSeen + "|" + totalPlaytime + "|" + currentSessionStart + "|" + joins
+ "|" + balance + "|" + totalEarned + "|" + totalSpent + "|" + transactionsCount
+ "|" + bansCount + "|" + mutesCount + "|" + warnsCount + "|" + lastPunishmentAt + "|" + safeType + "|" + punishmentScore;
} }
public static PlayerStats fromLine(String line) { public static PlayerStats fromLine(String line) {
@@ -62,6 +89,22 @@ public class PlayerStats {
ps.totalPlaytime = Long.parseLong(parts[4]); ps.totalPlaytime = Long.parseLong(parts[4]);
ps.currentSessionStart = Long.parseLong(parts[5]); ps.currentSessionStart = Long.parseLong(parts[5]);
ps.joins = Integer.parseInt(parts[6]); ps.joins = Integer.parseInt(parts[6]);
// Economy (felder 7-10)
if (parts.length >= 11) {
try { ps.balance = Double.parseDouble(parts[7]); } catch (Exception ignored) {}
try { ps.totalEarned = Double.parseDouble(parts[8]); } catch (Exception ignored) {}
try { ps.totalSpent = Double.parseDouble(parts[9]); } catch (Exception ignored) {}
try { ps.transactionsCount = Integer.parseInt(parts[10]); } catch (Exception ignored) {}
}
// Punishments (felder 11-16)
if (parts.length >= 17) {
try { ps.bansCount = Integer.parseInt(parts[11]); } catch (Exception ignored) {}
try { ps.mutesCount = Integer.parseInt(parts[12]); } catch (Exception ignored) {}
try { ps.warnsCount = Integer.parseInt(parts[13]); } catch (Exception ignored) {}
try { ps.lastPunishmentAt = Long.parseLong(parts[14]); } catch (Exception ignored) {}
ps.lastPunishmentType = parts[15];
try { ps.punishmentScore = Integer.parseInt(parts[16]); } catch (Exception ignored) {}
}
return ps; return ps;
} catch (Exception e) { } catch (Exception e) {
return null; return null;

View File

@@ -32,7 +32,6 @@ public class StatsModule implements Module, Listener {
// Laden // Laden
try { try {
storage.load(manager); storage.load(manager);
plugin.getLogger().info("Player-Stats wurden erfolgreich geladen.");
} catch (Exception e) { } catch (Exception e) {
plugin.getLogger().warning("Fehler beim Laden der Stats: " + e.getMessage()); plugin.getLogger().warning("Fehler beim Laden der Stats: " + e.getMessage());
} }
@@ -44,7 +43,6 @@ public class StatsModule implements Module, Listener {
plugin.getProxy().getScheduler().schedule(plugin, () -> { plugin.getProxy().getScheduler().schedule(plugin, () -> {
try { try {
storage.save(manager); storage.save(manager);
plugin.getLogger().info("Auto-Save: Player-Stats gespeichert.");
} catch (Exception e) { } catch (Exception e) {
plugin.getLogger().warning("Fehler beim Auto-Save: " + e.getMessage()); plugin.getLogger().warning("Fehler beim Auto-Save: " + e.getMessage());
} }
@@ -69,7 +67,6 @@ public class StatsModule implements Module, Listener {
} }
try { try {
storage.save(manager); storage.save(manager);
plugin.getLogger().info("Player-Stats beim Shutdown gespeichert.");
} catch (Exception e) { } catch (Exception e) {
plugin.getLogger().warning("Fehler beim Speichern (Shutdown): " + e.getMessage()); plugin.getLogger().warning("Fehler beim Speichern (Shutdown): " + e.getMessage());
} }

View File

@@ -2,8 +2,13 @@ package net.viper.status.stats;
import java.io.*; import java.io.*;
/**
* Fix #9: save() und load() sind jetzt synchronized um Race Conditions
* zwischen Auto-Save-Task und Shutdown-Aufruf zu verhindern.
*/
public class StatsStorage { public class StatsStorage {
private final File file; private final File file;
private final Object fileLock = new Object();
public StatsStorage(File pluginFolder) { public StatsStorage(File pluginFolder) {
if (!pluginFolder.exists()) pluginFolder.mkdirs(); if (!pluginFolder.exists()) pluginFolder.mkdirs();
@@ -11,6 +16,7 @@ public class StatsStorage {
} }
public void save(StatsManager manager) { public void save(StatsManager manager) {
synchronized (fileLock) {
try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) { try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
for (PlayerStats ps : manager.all()) { for (PlayerStats ps : manager.all()) {
bw.write(ps.toLine()); bw.write(ps.toLine());
@@ -21,9 +27,11 @@ public class StatsStorage {
e.printStackTrace(); e.printStackTrace();
} }
} }
}
public void load(StatsManager manager) { public void load(StatsManager manager) {
if (!file.exists()) return; if (!file.exists()) return;
synchronized (fileLock) {
try (BufferedReader br = new BufferedReader(new FileReader(file))) { try (BufferedReader br = new BufferedReader(new FileReader(file))) {
String line; String line;
while ((line = br.readLine()) != null) { while ((line = br.readLine()) != null) {
@@ -34,4 +42,5 @@ public class StatsStorage {
e.printStackTrace(); e.printStackTrace();
} }
} }
}
} }

View File

@@ -135,6 +135,30 @@ private-messages:
format-social-spy: "&8[&dSPY &7{sender} &8→ &7{receiver}&8] &f{message}" format-social-spy: "&8[&dSPY &7{sender} &8→ &7{receiver}&8] &f{message}"
social-spy-permission: "chat.socialspy" social-spy-permission: "chat.socialspy"
# ============================================================
# JOIN / LEAVE NACHRICHTEN
# Platzhalter:
# {player} - Spielername
# {prefix} - LuckPerms Prefix
# {suffix} - LuckPerms Suffix
# {server} - Zuletzt bekannter Server (bei Leave) oder "Netzwerk"
# ============================================================
join-leave:
enabled: true
# Normale Join/Leave-Nachrichten (für alle sichtbar)
join-format: "&8[&a+&8] {prefix}&a{player}&r &7hat das Netzwerk betreten."
leave-format: "&8[&c-&8] {prefix}&c{player}&r &7hat das Netzwerk verlassen."
# Vanish: Unsichtbare Spieler erzeugen keine normalen Join/Leave-Meldungen.
# Ist vanish-show-to-admins true, sehen Admins mit bypass-permission eine
# abweichende, dezente Benachrichtigung.
vanish-show-to-admins: true
vanish-join-format: "&8[&7+&8] &8{player} &7hat das Netzwerk betreten. &8(Vanish)"
vanish-leave-format: "&8[&7-&8] &8{player} &7hat das Netzwerk verlassen. &8(Vanish)"
# Brücken-Weitergabe (leer = deaktiviert)
discord-webhook: ""
telegram-chat-id: ""
telegram-thread-id: 0
# ============================================================ # ============================================================
# GLOBALES RATE-LIMIT-FRAMEWORK # GLOBALES RATE-LIMIT-FRAMEWORK
# Zentraler Schutz für Chat/PM/Command-Flood. # Zentraler Schutz für Chat/PM/Command-Flood.
@@ -304,6 +328,36 @@ chat-filter:
min-length: 6 min-length: 6
max-percent: 70 max-percent: 70
anti-ad:
enabled: true
message: "&cWerbung ist in diesem Chat nicht erlaubt!"
# Domains/Substrings die NICHT geblockt werden (z.B. eigene Serveradresse)
# Vergleich ist case-insensitiv und prüft ob der Substring im Match enthalten ist
whitelist:
- "viper-network.de"
- "m-viper.de"
- "https://www.spigotmc.org"
# TLDs die als Werbung gewertet werden.
# Leer = alle Domain-Treffer blockieren (nicht empfohlen, hohe False-Positive-Rate)
blocked-tlds:
- "net"
- "com"
- "de"
- "org"
- "gg"
- "io"
- "eu"
- "tv"
- "xyz"
- "info"
- "me"
- "cc"
- "co"
- "app"
- "online"
- "site"
- "fun"
# ============================================================ # ============================================================
# MENTIONS (@Spielername) # MENTIONS (@Spielername)
# ============================================================ # ============================================================

View File

@@ -0,0 +1,16 @@
# ============================================================
# StatusAPI - ChatModule Wort-Blacklist
# Wörter werden case-insensitiv und als Teilwort geprüft.
# Erkannte Wörter werden durch **** ersetzt.
#
# Diese Datei wird bei /chatreload automatisch neu eingelesen.
# Wörter die hier stehen ÜBERSCHREIBEN NICHT die Einträge in
# chat.yml → beide Listen werden zusammengeführt.
# ============================================================
words:
- beispielwort1
- beispielwort2
# Hier eigene Wörter eintragen, eines pro Zeile:
# - schimpfwort
# - spam

View File

@@ -1,7 +1,265 @@
name: StatusAPIBridge name: StatusAPI
version: 1.0.0 main: net.viper.status.StatusAPI
main: net.viper.statusapibridge.StatusAPIBridge version: 4.1.0
api-version: 1.21 author: M_Viper
description: Sendet Vault-Economy-Daten an die BungeeCord StatusAPI description: StatusAPI für BungeeCord inkl. Update-Checker, Modul-System und ChatModule
authors: [Viper] # Mindestanforderung: Minecraft 1.20 / BungeeCord mit PlayerChatEvent-Unterstützung
softdepend: [Vault]
softdepend:
- LuckPerms
- Geyser-BungeeCord
commands:
# ── VanishModule ──────────────────────────────────────────
vanish:
description: Vanish ein-/ausschalten
usage: /vanish [Spieler]
aliases: [v]
vanishlist:
description: Alle unsichtbaren Spieler anzeigen
usage: /vanishlist
aliases: [vlist]
# ── Verify Modul ──────────────────────────────────────────
verify:
description: Verifiziere dich mit einem Token
usage: /verify <token>
# ── ForumBridge Modul ─────────────────────────────────────
forumlink:
description: Verknüpfe deinen Minecraft-Account mit dem Forum
usage: /forumlink <token>
aliases: [fl]
forum:
description: Zeigt ausstehende Forum-Benachrichtigungen an
usage: /forum
# ── NetworkInfo Modul ─────────────────────────────────────
netinfo:
description: Zeigt erweiterte Proxy- und Systeminfos an
usage: /netinfo
antibot:
description: Zeigt AntiBot-Status und Verwaltung
usage: /antibot <status|clearblocks|unblock|profile|reload>
# ── AutoMessage Modul ─────────────────────────────────────
automessage:
description: AutoMessage Verwaltung
usage: /automessage reload
# ── ChatModule Kanal ────────────────────────────────────
channel:
description: Kanal wechseln oder Kanalliste anzeigen
usage: /channel [kanalname]
aliases: [ch, kanal]
# ── ChatModule HelpOp ───────────────────────────────────
helpop:
description: Sende eine Hilfeanfrage an das Team
usage: /helpop <Nachricht>
# ── ChatModule Privat-Nachrichten ───────────────────────
msg:
description: Sende eine private Nachricht
usage: /msg <Spieler> <Nachricht>
aliases: [tell, w, whisper]
r:
description: Antworte auf die letzte private Nachricht
usage: /r <Nachricht>
aliases: [reply, antwort]
# ── ChatModule Blockieren ───────────────────────────────
ignore:
description: Spieler ignorieren
usage: /ignore <Spieler>
aliases: [block]
unignore:
description: Spieler nicht mehr ignorieren
usage: /unignore <Spieler>
aliases: [unblock]
# ── ChatModule Mute (Admin) ─────────────────────────────
chatmute:
description: Spieler im Chat stumm schalten
usage: /chatmute <Spieler> [Minuten]
aliases: [gmute]
chatunmute:
description: Chat-Stummschaltung aufheben
usage: /chatunmute <Spieler>
aliases: [gunmute]
# ── ChatModule Selbst-Mute ──────────────────────────────
chataus:
description: Eigenen Chat-Empfang ein-/ausschalten
usage: /chataus
aliases: [togglechat, chaton, chatoff]
# ── ChatModule Broadcast ────────────────────────────────
broadcast:
description: Nachricht an alle Spieler senden
usage: /broadcast <Nachricht>
aliases: [bc, alert]
# ── ChatModule Emoji ────────────────────────────────────
emoji:
description: Liste aller verfügbaren Emojis
usage: /emoji
aliases: [emojis]
# ── ChatModule Social Spy ───────────────────────────────
socialspy:
description: Private Nachrichten mitlesen (Admin)
usage: /socialspy
aliases: [spy]
# ── ChatModule Reload ───────────────────────────────────
chatreload:
description: Chat-Konfiguration neu laden
usage: /chatreload
# ── ChatModule Admin-Info ───────────────────────────────
chatinfo:
description: Chat-Informationen ueber einen Spieler anzeigen (Admin)
usage: /chatinfo <Spieler>
# ── ChatModule Chat-History ─────────────────────────────
chathist:
description: Chat-History aus dem Logfile anzeigen (Admin)
usage: /chathist [Spieler] [Anzahl]
# ── ChatModule Mentions ─────────────────────────────────
mentions:
description: Mention-Benachrichtigungen ein-/ausschalten
usage: /mentions
aliases: [mention]
# ── ChatModule Plugin-Bypass ────────────────────────────
chatbypass:
description: ChatModule fuer naechste Eingabe ueberspringen (fuer Plugin-Dialoge wie CMI)
usage: /chatbypass
aliases: [cbp]
# ── ChatModule Account-Verknuepfung ─────────────────────
# FIX #4: Command-Namen stimmen jetzt mit der Code-Registrierung überein.
# Im ChatModule wird "discordlink" mit Alias "dlink" registriert,
# und "telegramlink" mit Alias "tlink".
discordlink:
description: Minecraft-Account mit Discord verknuepfen
usage: /discordlink
aliases: [dlink]
telegramlink:
description: Minecraft-Account mit Telegram verknuepfen
usage: /telegramlink
aliases: [tlink]
unlink:
description: Account-Verknuepfung aufheben
usage: /unlink <discord|telegram|all>
# ── ChatModule Report ───────────────────────────────────
report:
description: Spieler melden
usage: /report <Spieler> <Grund>
reports:
description: Offene Reports anzeigen (Admin)
usage: /reports [all]
reportclose:
description: Report schliessen (Admin)
usage: /reportclose <ID>
permissions:
# ── StatusAPI Core ────────────────────────────────────────
statusapi.update.notify:
description: Erlaubt Update-Benachrichtigungen
default: op
statusapi.netinfo:
description: Zugriff auf /netinfo
default: op
statusapi.antibot:
description: Zugriff auf /antibot
default: op
statusapi.automessage:
description: Zugriff auf /automessage reload
default: op
# ── ChatModule Kanaele ──────────────────────────────────
chat.channel.local:
description: Zugang zum Local-Kanal
default: true
chat.channel.trade:
description: Zugang zum Trade-Kanal
default: true
chat.channel.staff:
description: Zugang zum Staff-Kanal
default: false
# ── ChatModule HelpOp ───────────────────────────────────
chat.helpop.receive:
description: HelpOp-Nachrichten empfangen
default: false
# ── ChatModule Mute ─────────────────────────────────────
chat.mute:
description: Spieler muten / unmuten
default: false
# ── ChatModule Broadcast ────────────────────────────────
chat.broadcast:
description: Broadcast-Nachrichten senden
default: false
# ── ChatModule Social Spy ───────────────────────────────
chat.socialspy:
description: Private Nachrichten mitlesen
default: false
# ── ChatModule Admin ────────────────────────────────────
chat.admin.bypass:
description: Admin-Bypass - Kann nicht geblockt/gemutet werden
default: op
chat.admin.notify:
description: Benachrichtigungen ueber Mutes und Blocks erhalten
default: false
# ── ChatModule Report ───────────────────────────────────
chat.report:
description: Spieler reporten (/report)
default: true
# ── ChatModule Farben ───────────────────────────────────
chat.color:
description: Farbcodes (&a, &b, ...) im Chat nutzen
default: false
chat.color.format:
description: Formatierungen (&l, &o, &n, ...) im Chat nutzen
default: false
# ── ChatModule Filter ───────────────────────────────────
chat.filter.bypass:
description: Chat-Filter (Anti-Spam, Caps, Blacklist) umgehen
default: false
# ── CommandBlocker ────────────────────────────────────────
commandblocker.bypass:
description: Command-Blocker umgehen
default: op
commandblocker.admin:
description: CommandBlocker verwalten (/cb)
default: op

View File

@@ -17,6 +17,8 @@ broadcast.format=%prefixColored% %messageColored%
# =========================== # ===========================
statusapi.port=9191 statusapi.port=9191
# =========================== # ===========================
# WORDPRESS / VERIFY EINSTELLUNGEN # WORDPRESS / VERIFY EINSTELLUNGEN
# =========================== # ===========================