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.");
return;
}
plugin.getLogger().info("Gefundene Version: " + foundVersion + " (Aktuell: " + currentVersion + ")");
latestVersion = foundVersion;
latestUrl = foundUrl;

View File

@@ -2,16 +2,17 @@ package net.viper.status.module;
import net.md_5.bungee.api.plugin.Plugin;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 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 {
private final Map<String, Module> modules = new HashMap<>();
private final Map<String, Module> modules = new LinkedHashMap<>();
public void registerModule(Module module) {
modules.put(module.getName().toLowerCase(), module);
@@ -20,7 +21,6 @@ public class ModuleManager {
public void enableAll(Plugin plugin) {
for (Module module : modules.values()) {
try {
plugin.getLogger().info("Aktiviere Modul: " + module.getName() + "...");
module.onEnable(plugin);
} catch (Exception e) {
plugin.getLogger().severe("Fehler beim Aktivieren von Modul " + module.getName() + ": " + e.getMessage());
@@ -32,7 +32,6 @@ public class ModuleManager {
public void disableAll(Plugin plugin) {
for (Module module : modules.values()) {
try {
plugin.getLogger().info("Deaktiviere Modul: " + module.getName() + "...");
module.onDisable(plugin);
} catch (Exception e) {
plugin.getLogger().warning("Fehler beim Deaktivieren von Modul " + module.getName());

View File

@@ -1,8 +1,10 @@
package net.viper.status.modules.AutoMessage;
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.plugin.Command;
import net.md_5.bungee.api.plugin.Plugin;
import net.viper.status.StatusAPI;
import net.viper.status.module.Module;
@@ -14,102 +16,117 @@ import java.nio.file.Files;
import java.util.List;
import java.util.Properties;
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 {
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
public String getName() {
return "AutoMessage";
}
public String getName() { return "AutoMessage"; }
@Override
public void onEnable(Plugin plugin) {
// Hier casten wir das Plugin-Objekt zu StatusAPI, um an spezifische Methoden zu kommen
StatusAPI api = (StatusAPI) plugin;
this.api = (StatusAPI) plugin;
loadSettings();
// Konfiguration aus der zentralen verify.properties laden
Properties props = api.getVerifyProperties();
if (!enabled) return;
boolean enabled = Boolean.parseBoolean(props.getProperty("automessage.enabled", "false"));
if (!enabled) {
api.getLogger().info("AutoMessage-Modul ist deaktiviert.");
return;
}
// Interval in Sekunden einlesen
int intervalSeconds;
try {
intervalSeconds = Integer.parseInt(props.getProperty("automessage.interval", "300"));
} catch (NumberFormatException e) {
api.getLogger().warning("Ungültiges Intervall für AutoMessage! Nutze Standard (300s).");
intervalSeconds = 300;
}
// Dateiname einlesen (Standard: messages.txt)
String fileName = props.getProperty("automessage.file", "messages.txt");
File messageFile = new File(api.getDataFolder(), fileName);
if (!messageFile.exists()) {
api.getLogger().warning("Die Datei '" + fileName + "' wurde nicht gefunden (" + messageFile.getAbsolutePath() + ")!");
api.getLogger().info("Erstelle eine leere Datei '" + fileName + "' als Vorlage...");
try {
messageFile.createNewFile();
} catch (IOException e) {
api.getLogger().severe("Konnte Datei nicht erstellen: " + e.getMessage());
}
return;
}
// Nachrichten aus der Datei lesen
List<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();
registerReloadCommand();
scheduleTask();
}
@Override
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) {
ProxyServer.getInstance().getScheduler().cancel(taskId);
taskId = -1;
plugin.getLogger().info("AutoMessage-Task gestoppt.");
}
}
}

View File

@@ -17,8 +17,9 @@ import java.util.concurrent.TimeUnit;
/**
* BroadcastModule
*
* Speichert geplante Broadcasts jetzt persistent in 'broadcasts.schedules'.
* Beim Neustart werden diese automatisch wieder geladen.
* Fixes:
* - 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 {
@@ -28,7 +29,7 @@ public class BroadcastModule implements Module, Listener {
private String format = "%prefix% %message%";
private String fallbackPrefix = "[Broadcast]";
private String fallbackPrefixColor = "&c";
private String fallbackBracketColor = "&8"; // Neu
private String fallbackBracketColor = "&8";
private String fallbackMessageColor = "&f";
private final Map<String, ScheduledBroadcast> scheduledByClientId = new ConcurrentHashMap<>();
@@ -41,25 +42,15 @@ public class BroadcastModule implements Module, Listener {
}
@Override
public String getName() {
return "BroadcastModule";
}
public String getName() { return "BroadcastModule"; }
@Override
public void onEnable(Plugin plugin) {
this.plugin = plugin;
schedulesFile = new File(plugin.getDataFolder(), "broadcasts.schedules");
loadConfig();
if (!enabled) {
plugin.getLogger().info("[BroadcastModule] deaktiviert via verify.properties (broadcast.enabled=false)");
return;
}
try {
plugin.getProxy().getPluginManager().registerListener(plugin, this);
} catch (Throwable ignored) {}
if (!enabled) return;
try { plugin.getProxy().getPluginManager().registerListener(plugin, this); } catch (Throwable ignored) {}
plugin.getLogger().info("[BroadcastModule] aktiviert. Format: " + format);
loadSchedules();
plugin.getProxy().getScheduler().schedule(plugin, this::processScheduled, 1, 1, TimeUnit.SECONDS);
@@ -67,34 +58,23 @@ public class BroadcastModule implements Module, Listener {
@Override
public void onDisable(Plugin plugin) {
plugin.getLogger().info("[BroadcastModule] deaktiviert.");
saveSchedules();
scheduledByClientId.clear();
}
private void loadConfig() {
File file = new File(plugin.getDataFolder(), "verify.properties");
if (!file.exists()) {
enabled = true;
requiredApiKey = "";
format = "%prefix% %message%";
fallbackPrefix = "[Broadcast]";
fallbackPrefixColor = "&c";
fallbackBracketColor = "&8"; // Neu
fallbackMessageColor = "&f";
return;
}
if (!file.exists()) return;
try (InputStream in = new FileInputStream(file)) {
Properties props = new Properties();
props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
enabled = Boolean.parseBoolean(props.getProperty("broadcast.enabled", "true"));
requiredApiKey = props.getProperty("broadcast.api_key", "").trim();
format = props.getProperty("broadcast.format", format).trim();
enabled = Boolean.parseBoolean(props.getProperty("broadcast.enabled", "true"));
requiredApiKey = props.getProperty("broadcast.api_key", "").trim();
format = props.getProperty("broadcast.format", format).trim();
if (format.isEmpty()) format = "%prefix% %message%";
fallbackPrefix = props.getProperty("broadcast.prefix", fallbackPrefix).trim();
fallbackPrefixColor = props.getProperty("broadcast.prefix-color", fallbackPrefixColor).trim();
fallbackBracketColor = props.getProperty("broadcast.bracket-color", fallbackBracketColor).trim(); // Neu
fallbackPrefix = props.getProperty("broadcast.prefix", fallbackPrefix).trim();
fallbackPrefixColor = props.getProperty("broadcast.prefix-color", fallbackPrefixColor).trim();
fallbackBracketColor = props.getProperty("broadcast.bracket-color", fallbackBracketColor).trim();
fallbackMessageColor = props.getProperty("broadcast.message-color", fallbackMessageColor).trim();
} catch (IOException e) {
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,
String prefix, String prefixColor, String bracketColor, String messageColor) {
loadConfig();
if (!enabled) {
plugin.getLogger().info("[BroadcastModule] Broadcast abgelehnt: Modul ist deaktiviert.");
return false;
}
if (!enabled) return false;
if (requiredApiKey != null && !requiredApiKey.isEmpty()) {
if (apiKeyHeader == null || !requiredApiKey.equals(apiKeyHeader)) {
plugin.getLogger().warning("[BroadcastModule] Broadcast abgelehnt: API-Key fehlt oder inkorrekt.");
@@ -121,52 +96,39 @@ public class BroadcastModule implements Module, Listener {
if (sourceName == null || sourceName.isEmpty()) sourceName = "System";
if (type == null) type = "global";
String usedPrefix = (prefix != null && !prefix.trim().isEmpty()) ? prefix.trim() : fallbackPrefix;
String usedPrefixColor = (prefixColor != null && !prefixColor.trim().isEmpty()) ? prefixColor.trim() : fallbackPrefixColor;
String usedBracketColor = (bracketColor != null && !bracketColor.trim().isEmpty()) ? bracketColor.trim() : fallbackBracketColor; // Neu
String usedPrefix = (prefix != null && !prefix.trim().isEmpty()) ? prefix.trim() : fallbackPrefix;
String usedPrefixColor = (prefixColor != null && !prefixColor.trim().isEmpty()) ? prefixColor.trim() : fallbackPrefixColor;
String usedBracketColor = (bracketColor != null && !bracketColor.trim().isEmpty()) ? bracketColor.trim() : fallbackBracketColor;
String usedMessageColor = (messageColor != null && !messageColor.trim().isEmpty()) ? messageColor.trim() : fallbackMessageColor;
String prefixColorCode = normalizeColorCode(usedPrefixColor);
String bracketColorCode = normalizeColorCode(usedBracketColor); // Neu
String prefixColorCode = normalizeColorCode(usedPrefixColor);
String bracketColorCode = normalizeColorCode(usedBracketColor);
String messageColorCode = normalizeColorCode(usedMessageColor);
// --- KLAMMER LOGIK ---
String finalPrefix;
// Wenn eine Klammerfarbe gesetzt ist, bauen wir den Prefix neu zusammen
// Format: [BracketColor][ [PrefixColor]Text [BracketColor]]
if (!bracketColorCode.isEmpty()) {
String textContent = usedPrefix;
// Entferne manuelle Klammern, falls der User [Broadcast] in das Textfeld geschrieben hat
if (textContent.startsWith("[")) textContent = textContent.substring(1);
if (textContent.endsWith("]")) textContent = textContent.substring(0, textContent.length() - 1);
if (textContent.endsWith("]")) textContent = textContent.substring(0, textContent.length() - 1);
finalPrefix = bracketColorCode + "[" + prefixColorCode + textContent + bracketColorCode + "]" + ChatColor.RESET;
} else {
// Altes Verhalten: Ganzen String einfärben
finalPrefix = prefixColorCode + usedPrefix + ChatColor.RESET;
}
// ---------------------
String coloredMessage = (messageColorCode.isEmpty() ? "" : messageColorCode) + message;
String out = format.replace("%name%", sourceName)
.replace("%prefix%", finalPrefix) // Neu verwendete Variable
.replace("%prefixColored%", finalPrefix) // Fallback
.replace("%message%", message)
.replace("%messageColored%", coloredMessage)
.replace("%type%", type);
if (!out.contains("%prefixColored%") && !out.contains("%messageColored%") && !out.contains("%prefix%") && !out.contains("%message%")) {
out = finalPrefix + " " + coloredMessage;
}
String out = format
.replace("%name%", sourceName)
.replace("%prefix%", finalPrefix)
.replace("%prefixColored%", finalPrefix)
.replace("%message%", message)
.replace("%messageColored%",coloredMessage)
.replace("%type%", type);
TextComponent tc = new TextComponent(out);
int sent = 0;
for (ProxiedPlayer p : plugin.getProxy().getPlayers()) {
try { p.sendMessage(tc); sent++; } catch (Throwable ignored) {}
}
plugin.getLogger().info("[BroadcastModule] Broadcast gesendet (Empfänger=" + sent + "): " + message);
return true;
}
@@ -183,17 +145,17 @@ public class BroadcastModule implements Module, Listener {
for (Map.Entry<String, ScheduledBroadcast> entry : scheduledByClientId.entrySet()) {
String id = entry.getKey();
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 + ".sourceName", sb.sourceName);
props.setProperty(id + ".message", sb.message);
props.setProperty(id + ".type", sb.type);
props.setProperty(id + ".prefix", sb.prefix);
props.setProperty(id + ".prefixColor", sb.prefixColor);
props.setProperty(id + ".bracketColor", sb.bracketColor); // Neu
props.setProperty(id + ".messageColor", sb.messageColor);
props.setProperty(id + ".recur", sb.recur);
props.setProperty(id + ".sourceName", sb.sourceName);
props.setProperty(id + ".message", sb.message);
props.setProperty(id + ".type", sb.type);
props.setProperty(id + ".prefix", sb.prefix);
props.setProperty(id + ".prefixColor", sb.prefixColor);
props.setProperty(id + ".bracketColor", sb.bracketColor);
props.setProperty(id + ".messageColor", sb.messageColor);
props.setProperty(id + ".recur", sb.recur);
}
try (OutputStream out = new FileOutputStream(schedulesFile)) {
props.store(out, "PulseCast Scheduled Broadcasts");
} catch (IOException e) {
@@ -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() {
if (!schedulesFile.exists()) {
plugin.getLogger().info("[BroadcastModule] Keine bestehenden Schedules gefunden (Neustart).");
return;
}
if (!schedulesFile.exists()) return;
Properties props = new Properties();
try (InputStream in = new FileInputStream(schedulesFile)) {
props.load(in);
@@ -215,34 +181,36 @@ public class BroadcastModule implements Module, Listener {
return;
}
Map<String, ScheduledBroadcast> loaded = new HashMap<>();
for (String key : props.stringPropertyNames()) {
if (!key.contains(".")) continue;
String[] parts = key.split("\\.");
if (parts.length != 2) continue;
// Bekannte Feld-Suffixe
Set<String> knownFields = new HashSet<>(Arrays.asList(
"nextRunMillis", "sourceName", "message", "type",
"prefix", "prefixColor", "bracketColor", "messageColor", "recur"
));
String id = parts[0];
String field = parts[1];
Map<String, ScheduledBroadcast> loaded = new LinkedHashMap<>();
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);
ScheduledBroadcast sb = loaded.get(id);
if (sb == null) {
sb = new ScheduledBroadcast(id, 0, "", "", "", "", "", "", "", ""); // Ein leerer String mehr für Bracket
loaded.put(id, sb);
}
ScheduledBroadcast sb = loaded.computeIfAbsent(id,
k -> new ScheduledBroadcast(k, 0, "", "", "", "", "", "", "", ""));
switch (field) {
case "nextRunMillis":
try { sb.nextRunMillis = Long.parseLong(value); } catch (NumberFormatException ignored) {}
break;
case "sourceName": sb.sourceName = value; break;
case "message": sb.message = value; break;
case "type": sb.type = value; break;
case "prefix": sb.prefix = value; break;
case "prefixColor": sb.prefixColor = value; break;
case "bracketColor": sb.bracketColor = value; break; // Neu
case "messageColor": sb.messageColor = value; break;
case "recur": sb.recur = value; break;
case "nextRunMillis": try { sb.nextRunMillis = Long.parseLong(value); } catch (NumberFormatException ignored) {} break;
case "sourceName": sb.sourceName = value; break;
case "message": sb.message = value; break;
case "type": sb.type = value; break;
case "prefix": sb.prefix = value; break;
case "prefixColor": sb.prefixColor = value; break;
case "bracketColor": sb.bracketColor = value; break;
case "messageColor": sb.messageColor = value; break;
case "recur": sb.recur = value; break;
}
}
scheduledByClientId.putAll(loaded);
@@ -250,95 +218,66 @@ public class BroadcastModule implements Module, Listener {
}
public boolean scheduleBroadcast(long timestampMillis, String sourceName, String message, String type,
String apiKeyHeader, String prefix, String prefixColor, String bracketColor, String messageColor,
String recur, String clientScheduleId) {
String apiKeyHeader, String prefix, String prefixColor, String bracketColor,
String messageColor, String recur, String clientScheduleId) {
loadConfig();
if (!enabled) {
plugin.getLogger().info("[BroadcastModule] schedule abgelehnt: Modul deaktiviert.");
return false;
}
if (!enabled) return false;
if (requiredApiKey != null && !requiredApiKey.isEmpty()) {
if (apiKeyHeader == null || !requiredApiKey.equals(apiKeyHeader)) {
plugin.getLogger().warning("[BroadcastModule] schedule abgelehnt: API-Key fehlt oder inkorrekt.");
return false;
}
}
if (message == null) message = "";
if (message == null) message = "";
if (sourceName == null || sourceName.isEmpty()) sourceName = "System";
if (type == null) type = "global";
if (recur == null) recur = "none";
if (type == null) type = "global";
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();
String scheduledTimeStr = dateFormat.format(new Date(timestampMillis));
plugin.getLogger().info("[BroadcastModule] Neue geplante Nachricht registriert: " + id + " @ " + scheduledTimeStr);
if (timestampMillis <= now) {
plugin.getLogger().warning("[BroadcastModule] Geplante Zeit liegt in der Vergangenheit -> sende sofort!");
plugin.getLogger().warning("[BroadcastModule] Geplante Zeit in der Vergangenheit sende sofort!");
return handleBroadcast(sourceName, message, type, apiKeyHeader, prefix, prefixColor, bracketColor, messageColor);
}
ScheduledBroadcast sb = new ScheduledBroadcast(id, timestampMillis, sourceName, message, type, prefix, prefixColor, bracketColor, messageColor, recur);
ScheduledBroadcast sb = new ScheduledBroadcast(id, timestampMillis, sourceName, message, type,
prefix, prefixColor, bracketColor, messageColor, recur);
scheduledByClientId.put(id, sb);
saveSchedules();
plugin.getLogger().info("[BroadcastModule] Neue geplante Nachricht registriert: " + id
+ " @ " + dateFormat.format(new Date(timestampMillis)));
return true;
}
public boolean cancelScheduled(String clientScheduleId) {
if (clientScheduleId == null || clientScheduleId.trim().isEmpty()) return false;
ScheduledBroadcast removed = scheduledByClientId.remove(clientScheduleId);
if (removed != null) {
plugin.getLogger().info("[BroadcastModule] Geplante Nachricht abgebrochen: id=" + clientScheduleId);
saveSchedules();
return true;
}
if (removed != null) { plugin.getLogger().info("[BroadcastModule] Schedule abgebrochen: " + clientScheduleId); saveSchedules(); return true; }
return false;
}
private void processScheduled() {
if (scheduledByClientId.isEmpty()) return;
long now = System.currentTimeMillis();
List<String> toRemove = new ArrayList<>();
boolean changed = false;
for (Map.Entry<String, ScheduledBroadcast> entry : scheduledByClientId.entrySet()) {
ScheduledBroadcast sb = entry.getValue();
if (sb.nextRunMillis <= now) {
String timeStr = dateFormat.format(new Date(sb.nextRunMillis));
plugin.getLogger().info("[BroadcastModule] ⏰ Sende geplante Nachricht (ID: " + entry.getKey() + ", Zeit: " + timeStr + ")");
plugin.getLogger().info("[BroadcastModule] ⏰ Sende geplante Nachricht (ID: " + entry.getKey() + ")");
handleBroadcast(sb.sourceName, sb.message, sb.type, "", sb.prefix, sb.prefixColor, sb.bracketColor, sb.messageColor);
if (!"none".equalsIgnoreCase(sb.recur)) {
long next = computeNextMillis(sb.nextRunMillis, sb.recur);
if (next > 0) {
sb.nextRunMillis = next;
String nextTimeStr = dateFormat.format(new Date(next));
plugin.getLogger().info("[BroadcastModule] Nächste Wiederholung (" + sb.recur + "): " + nextTimeStr);
changed = true;
} else {
toRemove.add(entry.getKey());
changed = true;
}
} else {
toRemove.add(entry.getKey());
changed = true;
}
if (next > 0) { sb.nextRunMillis = next; changed = true; }
else { toRemove.add(entry.getKey()); changed = true; }
} else { toRemove.add(entry.getKey()); changed = true; }
}
}
if (changed || !toRemove.isEmpty()) {
for (String k : toRemove) {
scheduledByClientId.remove(k);
plugin.getLogger().info("[BroadcastModule] Schedule entfernt: " + k);
}
for (String k : toRemove) { scheduledByClientId.remove(k); }
saveSchedules();
}
}
@@ -346,36 +285,29 @@ public class BroadcastModule implements Module, Listener {
private long computeNextMillis(long currentMillis, String recur) {
switch (recur.toLowerCase(Locale.ROOT)) {
case "hourly": return currentMillis + TimeUnit.HOURS.toMillis(1);
case "daily": return currentMillis + TimeUnit.DAYS.toMillis(1);
case "daily": return currentMillis + TimeUnit.DAYS.toMillis(1);
case "weekly": return currentMillis + TimeUnit.DAYS.toMillis(7);
default: return -1L;
default: return -1L;
}
}
private static class ScheduledBroadcast {
final String clientId;
long nextRunMillis;
String sourceName;
String message;
String type;
String prefix;
String prefixColor;
String bracketColor; // Neu
String messageColor;
String recur;
String sourceName, message, type, prefix, prefixColor, bracketColor, messageColor, recur;
ScheduledBroadcast(String clientId, long nextRunMillis, String sourceName, String message, String type,
String prefix, String prefixColor, String bracketColor, String messageColor, String recur) {
this.clientId = clientId;
this.clientId = clientId;
this.nextRunMillis = nextRunMillis;
this.sourceName = sourceName;
this.message = message;
this.type = type;
this.prefix = prefix;
this.prefixColor = prefixColor;
this.bracketColor = bracketColor; // Neu
this.messageColor = messageColor;
this.recur = (recur == null ? "none" : recur);
this.sourceName = sourceName;
this.message = message;
this.type = type;
this.prefix = prefix;
this.prefixColor = prefixColor;
this.bracketColor = bracketColor;
this.messageColor = messageColor;
this.recur = recur == null ? "none" : recur;
}
}
}

View File

@@ -304,7 +304,6 @@ public class AccountLinkManager {
} catch (IOException e) {
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) {

View File

@@ -11,120 +11,78 @@ import java.util.*;
/**
* 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 {
private final Plugin plugin;
private Configuration config;
// Geladene Kanäle
private final Map<String, ChatChannel> channels = new LinkedHashMap<>();
private String defaultChannel;
// HelpOp
private String helpopFormat;
private String helpopPermission;
private String helpopFormat, helpopPermission, helpopConfirm, helpopDiscordWebhook, helpopTelegramChatId;
private int helpopCooldown;
private String helpopConfirm;
private String helpopDiscordWebhook;
private String helpopTelegramChatId;
// Broadcast
private String broadcastFormat;
private String broadcastPermission;
private String broadcastFormat, broadcastPermission;
// Private Messages
private boolean pmEnabled;
private String pmFormatSender;
private String pmFormatReceiver;
private String pmFormatSpy;
private String pmSpyPermission;
private String pmFormatSender, pmFormatReceiver, pmFormatSpy, pmSpyPermission, pmRateLimitMessage;
private boolean pmRateLimitEnabled;
private long pmRateLimitWindowMs;
private int pmRateLimitMaxActions;
private long pmRateLimitBlockMs;
private String pmRateLimitMessage;
// Mute
private int defaultMuteDuration;
private String mutedMessage;
// Emoji
private boolean emojiEnabled;
private boolean emojiBedrockSupport;
private boolean emojiEnabled, emojiBedrockSupport;
private final Map<String, String> emojiMappings = new LinkedHashMap<>();
// Discord
private boolean discordEnabled;
private String discordBotToken;
private String discordGuildId;
private String discordBotToken, discordGuildId, discordFromFormat, discordAdminChannelId, discordEmbedColor;
private int discordPollInterval;
private String discordFromFormat;
private String discordAdminChannelId;
private String discordEmbedColor;
// Telegram
private boolean telegramEnabled;
private String telegramBotToken;
private int telegramPollInterval;
private String telegramFromFormat;
private String telegramAdminChatId;
private int telegramChatTopicId;
private int telegramAdminTopicId;
private String telegramBotToken, telegramFromFormat, telegramAdminChatId;
private int telegramPollInterval, telegramChatTopicId, telegramAdminTopicId;
// Account-Linking
private boolean linkingEnabled;
private String linkDiscordMessage;
private String linkTelegramMessage;
private String linkSuccessDiscord;
private String linkSuccessTelegram;
private String linkBotSuccessDiscord;
private String linkBotSuccessTelegram;
private String linkedDiscordFormat;
private String linkedTelegramFormat;
private int telegramAdminThreadId;
private String linkDiscordMessage, linkTelegramMessage, linkSuccessDiscord, linkSuccessTelegram;
private String linkBotSuccessDiscord, linkBotSuccessTelegram, linkedDiscordFormat, linkedTelegramFormat;
private int telegramAdminThreadId;
// Admin
private String adminBypassPermission;
private String adminNotifyPermission;
private String adminBypassPermission, adminNotifyPermission;
// ===== NEU: Chatlog =====
private boolean chatlogEnabled;
private int chatlogRetentionDays;
// ===== NEU: Server-Farben =====
private final Map<String, String> serverColors = new LinkedHashMap<>();
private final Map<String, String> serverDisplayNames = new LinkedHashMap<>();
private String serverColorDefault;
// ===== NEU: Reports =====
private boolean reportsEnabled;
private String reportConfirm;
private boolean reportsEnabled, reportWebhookEnabled;
private String reportConfirm, reportPermission, reportClosePermission, reportViewPermission;
private String reportDiscordWebhook, reportTelegramChatId;
private int reportCooldown;
// ===== Chat-Filter =====
private ChatFilter.ChatFilterConfig filterConfig = new ChatFilter.ChatFilterConfig();
// ===== Mentions =====
private boolean mentionsEnabled;
private String mentionsHighlightColor;
private String mentionsSound;
private boolean mentionsAllowToggle;
private String mentionsNotifyPrefix;
private boolean mentionsEnabled, mentionsAllowToggle;
private String mentionsHighlightColor, mentionsSound, mentionsNotifyPrefix;
// ===== Chat-History =====
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;
private int historyMaxLines, historyDefaultLines;
public ChatConfig(Plugin plugin) {
this.plugin = plugin;
}
private boolean joinLeaveEnabled, vanishShowToAdmins;
private String joinFormat, leaveFormat, vanishJoinFormat, vanishLeaveFormat;
private String joinLeaveDiscordWebhook, joinLeaveTelegramChatId;
private int joinLeaveTelegramThreadId;
public ChatConfig(Plugin plugin) { this.plugin = plugin; }
public void load() {
File file = new File(plugin.getDataFolder(), "chat.yml");
@@ -132,24 +90,18 @@ public class ChatConfig {
plugin.getDataFolder().mkdirs();
InputStream in = plugin.getResourceAsStream("chat.yml");
if (in != null) {
try {
Files.copy(in, file.toPath());
} catch (IOException e) {
plugin.getLogger().severe("[ChatModule] Konnte chat.yml nicht erstellen: " + e.getMessage());
}
try { Files.copy(in, file.toPath()); }
catch (IOException e) { plugin.getLogger().severe("[ChatModule] Konnte chat.yml nicht erstellen: " + e.getMessage()); }
} 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 {
config = ConfigurationProvider.getProvider(YamlConfiguration.class).load(file);
} catch (IOException e) {
try { config = ConfigurationProvider.getProvider(YamlConfiguration.class).load(file); }
catch (IOException e) {
plugin.getLogger().severe("[ChatModule] Fehler beim Laden der chat.yml: " + e.getMessage());
config = new Configuration();
}
parseConfig();
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")) {
channels.put("global", new ChatChannel(
"global", "Global", "G", "", "&a",
"&8[&a{server}&8] {prefix}&r{player}{suffix}&8: &f{message}",
false, "", "", "", 0, false
));
channels.put("global", new ChatChannel("global", "Global", "G", "", "&a",
"&8[&a{server}&8] {prefix}&r{player}{suffix}&8: &f{message}", false, "", "", "", 0, false));
}
// --- HelpOp ---
@@ -199,31 +146,29 @@ public class ChatConfig {
helpopConfirm = ho.getString("confirm-message", "&aHilferuf gesendet!");
helpopDiscordWebhook = ho.getString("discord-webhook", "");
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 ---
Configuration bc = config.getSection("broadcast");
if (bc != null) {
broadcastFormat = bc.getString("format", "&c[&6Broadcast&c] &e{message}");
broadcastPermission = bc.getString("permission", "chat.broadcast");
}
broadcastFormat = bc != null ? bc.getString("format", "&c[&6Broadcast&c] &e{message}") : "&c[&6Broadcast&c] &e{message}";
broadcastPermission = bc != null ? bc.getString("permission", "chat.broadcast") : "chat.broadcast";
// --- Private Messages ---
Configuration pm = config.getSection("private-messages");
if (pm != null) {
pmEnabled = pm.getBoolean("enabled", true);
pmFormatSender = pm.getString("format-sender", "&8[&7Du &8→ &b{player}&8] &f{message}");
pmFormatReceiver = pm.getString("format-receiver", "&8[&b{player} &8→ &7Dir&8] &f{message}");
pmFormatSpy = pm.getString("format-social-spy","&8[&dSPY &7{sender} &8→ &7{receiver}&8] &f{message}");
pmSpyPermission = pm.getString("social-spy-permission", "chat.socialspy");
}
pmEnabled = pm == null || 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}";
pmFormatReceiver = pm != null ? pm.getString("format-receiver", "&8[&b{player} &8→ &7Dir&8] &f{message}") : "&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}";
pmSpyPermission = pm != null ? pm.getString("social-spy-permission", "chat.socialspy") : "chat.socialspy";
// --- Mute ---
Configuration mu = config.getSection("mute");
if (mu != null) {
defaultMuteDuration = mu.getInt("default-duration-minutes", 60);
mutedMessage = mu.getString("muted-message", "&cDu bist stummgeschaltet. Noch: &f{time}");
}
defaultMuteDuration = mu != null ? mu.getInt("default-duration-minutes", 60) : 60;
mutedMessage = mu != null ? mu.getString("muted-message", "&cDu bist stummgeschaltet. Noch: &f{time}") : "&cDu bist stummgeschaltet. Noch: &f{time}";
// --- Emoji ---
Configuration em = config.getSection("emoji");
@@ -232,12 +177,8 @@ public class ChatConfig {
emojiBedrockSupport = em.getBoolean("bedrock-support", true);
emojiMappings.clear();
Configuration map = em.getSection("mappings");
if (map != null) {
for (String key : map.getKeys()) {
emojiMappings.put(key, map.getString(key, key));
}
}
}
if (map != null) { for (String key : map.getKeys()) emojiMappings.put(key, map.getString(key, key)); }
} else { emojiEnabled = true; emojiBedrockSupport = true; }
// --- 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}");
discordAdminChannelId = dc.getString("admin-channel-id", "");
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 ---
Configuration tg = config.getSection("telegram");
@@ -261,33 +202,19 @@ public class ChatConfig {
telegramAdminChatId = tg.getString("admin-chat-id", "");
telegramChatTopicId = tg.getInt("chat-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 ---
Configuration al = config.getSection("account-linking");
if (al != null) {
linkingEnabled = al.getBoolean("enabled", true);
linkDiscordMessage = al.getString("discord-link-message", "&aCode: &f{token}");
linkTelegramMessage = al.getString("telegram-link-message", "&aCode: &f{token}");
linkSuccessDiscord = al.getString("success-discord", "&aDiscord verknüpft!");
linkSuccessTelegram = al.getString("success-telegram", "&aTelegram verknüpft!");
linkBotSuccessDiscord = al.getString("bot-success-discord", "✅ Verknüpft: {player}");
linkBotSuccessTelegram = al.getString("bot-success-telegram", "✅ Verknüpft: {player}");
linkedDiscordFormat = al.getString("linked-discord-format",
"&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}";
}
linkingEnabled = al == null || al.getBoolean("enabled", true);
linkDiscordMessage = al != null ? al.getString("discord-link-message", "&aCode: &f{token}") : "&aCode: &f{token}";
linkTelegramMessage = al != null ? al.getString("telegram-link-message", "&aCode: &f{token}") : "&aCode: &f{token}";
linkSuccessDiscord = al != null ? al.getString("success-discord", "&aDiscord verknüpft!") : "&aDiscord verknüpft!";
linkSuccessTelegram = al != null ? al.getString("success-telegram", "&aTelegram verknüpft!") : "&aTelegram verknüpft!";
linkBotSuccessDiscord = al != null ? al.getString("bot-success-discord", "✅ Verknüpft: {player}") : "✅ Verknüpft: {player}";
linkBotSuccessTelegram = al != null ? al.getString("bot-success-telegram", "✅ Verknüpft: {player}") : "✅ 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}";
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}";
// --- Chat-Filter ---
filterConfig = new ChatFilter.ChatFilterConfig();
@@ -299,9 +226,12 @@ public class ChatConfig {
filterConfig.spamCooldownMs = spam.getInt("cooldown-ms", 1500);
filterConfig.spamMaxMessages = spam.getInt("max-messages", 3);
filterConfig.spamMessage = spam.getString("message", "&cNicht so schnell!");
filterConfig.globalRateLimitWindowMs = Math.max(500L, filterConfig.spamCooldownMs);
filterConfig.globalRateLimitMaxActions = Math.max(1, filterConfig.spamMaxMessages);
filterConfig.globalRateLimitBlockMs = Math.max(2000L, filterConfig.spamCooldownMs * 2L);
// 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.globalRateLimitMaxActions = Math.max(1, filterConfig.spamMaxMessages);
filterConfig.globalRateLimitBlockMs = Math.max(2000L, filterConfig.spamCooldownMs * 4L);
}
Configuration dup = cf.getSection("duplicate-check");
if (dup != null) {
@@ -312,13 +242,14 @@ public class ChatConfig {
if (bl != null) {
filterConfig.blacklistEnabled = bl.getBoolean("enabled", true);
filterConfig.blacklistWords.clear();
// words ist eine YAML-Liste, nicht eine Section → getList() verwenden
loadFilterWords(filterConfig.blacklistWords);
try {
java.util.List<?> wordList = bl.getList("words");
if (wordList != null) {
for (Object o : wordList) {
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,254 +261,236 @@ public class ChatConfig {
filterConfig.capsMinLength = caps.getInt("min-length", 6);
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 ---
pmRateLimitEnabled = true;
pmRateLimitWindowMs = 5000L;
// --- Rate-Limit (FIX #8: dieser Block setzt die endgültigen Werte, hat Vorrang) ---
pmRateLimitEnabled = true;
pmRateLimitWindowMs = 5000L;
pmRateLimitMaxActions = 4;
pmRateLimitBlockMs = 10000L;
pmRateLimitMessage = "&cDu sendest zu viele private Nachrichten. Bitte warte kurz.";
pmRateLimitBlockMs = 10000L;
pmRateLimitMessage = "&cDu sendest zu viele private Nachrichten. Bitte warte kurz.";
Configuration rl = config.getSection("rate-limit");
if (rl != null) {
Configuration rlChat = rl.getSection("chat");
if (rlChat != null) {
filterConfig.globalRateLimitEnabled = rlChat.getBoolean("enabled", true);
filterConfig.globalRateLimitWindowMs = rlChat.getLong("window-ms", filterConfig.globalRateLimitWindowMs);
filterConfig.globalRateLimitMaxActions = rlChat.getInt("max-actions", filterConfig.globalRateLimitMaxActions);
filterConfig.globalRateLimitBlockMs = rlChat.getLong("block-ms", filterConfig.globalRateLimitBlockMs);
filterConfig.spamMessage = rlChat.getString("message", filterConfig.spamMessage);
// FIX #8: rate-limit.chat überschreibt die anti-spam-Fallbacks vollständig
filterConfig.globalRateLimitEnabled = rlChat.getBoolean("enabled", true);
filterConfig.globalRateLimitWindowMs = rlChat.getLong("window-ms", filterConfig.globalRateLimitWindowMs);
filterConfig.globalRateLimitMaxActions = rlChat.getInt("max-actions", filterConfig.globalRateLimitMaxActions);
filterConfig.globalRateLimitBlockMs = rlChat.getLong("block-ms", filterConfig.globalRateLimitBlockMs);
filterConfig.spamMessage = rlChat.getString("message", filterConfig.spamMessage);
}
Configuration rlPm = rl.getSection("private-messages");
if (rlPm != null) {
pmRateLimitEnabled = rlPm.getBoolean("enabled", pmRateLimitEnabled);
pmRateLimitWindowMs = rlPm.getLong("window-ms", pmRateLimitWindowMs);
pmRateLimitMaxActions = rlPm.getInt("max-actions", pmRateLimitMaxActions);
pmRateLimitBlockMs = rlPm.getLong("block-ms", pmRateLimitBlockMs);
pmRateLimitMessage = rlPm.getString("message", pmRateLimitMessage);
pmRateLimitEnabled = rlPm.getBoolean("enabled", pmRateLimitEnabled);
pmRateLimitWindowMs = rlPm.getLong("window-ms", pmRateLimitWindowMs);
pmRateLimitMaxActions = rlPm.getInt("max-actions", pmRateLimitMaxActions);
pmRateLimitBlockMs = rlPm.getLong("block-ms", pmRateLimitBlockMs);
pmRateLimitMessage = rlPm.getString("message", pmRateLimitMessage);
}
}
// --- Mentions ---
Configuration mn = config.getSection("mentions");
if (mn != null) {
mentionsEnabled = mn.getBoolean("enabled", true);
mentionsHighlightColor = mn.getString("highlight-color", "&e&l");
mentionsSound = mn.getString("sound", "ENTITY_EXPERIENCE_ORB_PICKUP");
mentionsAllowToggle = mn.getBoolean("allow-toggle", true);
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";
}
mentionsEnabled = mn == null || mn.getBoolean("enabled", true);
mentionsHighlightColor = mn != null ? mn.getString("highlight-color", "&e&l") : "&e&l";
mentionsSound = mn != null ? mn.getString("sound", "ENTITY_EXPERIENCE_ORB_PICKUP") : "ENTITY_EXPERIENCE_ORB_PICKUP";
mentionsAllowToggle = mn == null || mn.getBoolean("allow-toggle", true);
mentionsNotifyPrefix = mn != null ? mn.getString("notify-prefix", "&e&l[Mention] &r") : "&e&l[Mention] &r";
// --- Chat-History ---
Configuration ch = config.getSection("chat-history");
if (ch != null) {
historyMaxLines = ch.getInt("max-lines", 50);
historyDefaultLines = ch.getInt("default-lines", 10);
} else {
historyMaxLines = 50;
historyDefaultLines = 10;
}
historyMaxLines = ch != null ? ch.getInt("max-lines", 50) : 50;
historyDefaultLines = ch != null ? ch.getInt("default-lines", 10) : 10;
// --- Admin ---
Configuration adm = config.getSection("admin");
if (adm != null) {
adminBypassPermission = adm.getString("bypass-permission", "chat.admin.bypass");
adminNotifyPermission = adm.getString("notify-permission", "chat.admin.notify");
} else {
adminBypassPermission = "chat.admin.bypass";
adminNotifyPermission = "chat.admin.notify";
}
adminBypassPermission = adm != null ? adm.getString("bypass-permission", "chat.admin.bypass") : "chat.admin.bypass";
adminNotifyPermission = adm != null ? adm.getString("notify-permission", "chat.admin.notify") : "chat.admin.notify";
// --- Server-Farben ---
serverColors.clear();
serverDisplayNames.clear();
serverColors.clear(); serverDisplayNames.clear();
Configuration sc = config.getSection("server-colors");
if (sc != null) {
serverColorDefault = sc.getString("default", "&7");
for (String key : sc.getKeys()) {
if (key.equals("default")) continue;
// Neues Format: server hat Untersektion mit color + display
Configuration sub = sc.getSection(key);
if (sub != null) {
serverColors.put(key.toLowerCase(), sub.getString("color", "&7"));
String display = sub.getString("display", "");
if (!display.isEmpty()) {
serverDisplayNames.put(key.toLowerCase(), display);
}
if (!display.isEmpty()) serverDisplayNames.put(key.toLowerCase(), display);
} else {
// Altes Format: server: "&a" (nur Farbe, kein display)
serverColors.put(key.toLowerCase(), sc.getString(key, "&7"));
}
}
} else {
serverColorDefault = "&7";
}
} else { serverColorDefault = "&7"; }
// --- Chatlog (NEU) ---
// --- Chatlog ---
Configuration cl = config.getSection("chatlog");
if (cl != null) {
chatlogEnabled = cl.getBoolean("enabled", true);
// Nur 7 oder 14 erlaubt; Standardwert 7
int raw = cl.getInt("retention-days", 7);
chatlogRetentionDays = (raw == 14) ? 14 : 7;
} else {
chatlogEnabled = true;
chatlogRetentionDays = 7;
}
chatlogEnabled = cl == null || cl.getBoolean("enabled", true);
int raw = cl != null ? cl.getInt("retention-days", 7) : 7;
chatlogRetentionDays = (raw == 14) ? 14 : 7;
// --- Reports (NEU) ---
// --- Reports ---
Configuration rp = config.getSection("reports");
if (rp != null) {
reportsEnabled = rp.getBoolean("enabled", true);
reportWebhookEnabled = rp.getBoolean("webhook-enabled", false);
reportConfirm = rp.getString("confirm-message",
"&aDein Report &8(§f{id}&8) &awurde eingereicht. Danke!");
reportPermission = rp.getString("report-permission", ""); // leer = jeder
reportClosePermission= rp.getString("close-permission", "chat.admin.bypass");
reportViewPermission = rp.getString("view-permission", "chat.admin.bypass");
reportCooldown = rp.getInt("cooldown", 60);
reportDiscordWebhook = rp.getString("discord-webhook", "");
reportTelegramChatId = rp.getString("telegram-chat-id", "");
reportsEnabled = rp.getBoolean("enabled", true);
reportWebhookEnabled = rp.getBoolean("webhook-enabled", false);
reportConfirm = rp.getString("confirm-message", "&aDein Report &8({id}) &awurde eingereicht. Danke!");
reportPermission = rp.getString("report-permission", "");
reportClosePermission = rp.getString("close-permission", "chat.admin.bypass");
reportViewPermission = rp.getString("view-permission", "chat.admin.bypass");
reportCooldown = rp.getInt("cooldown", 60);
reportDiscordWebhook = rp.getString("discord-webhook", "");
reportTelegramChatId = rp.getString("telegram-chat-id", "");
} else {
reportsEnabled = true;
reportWebhookEnabled = false;
reportConfirm = "&aDein Report &8({id}) &awurde eingereicht. Danke!";
reportPermission = "";
reportClosePermission = "chat.admin.bypass";
reportViewPermission = "chat.admin.bypass";
reportCooldown = 60;
reportDiscordWebhook = "";
reportTelegramChatId = "";
reportsEnabled = true; reportWebhookEnabled = false;
reportConfirm = "&aDein Report &8({id}) &awurde eingereicht. Danke!";
reportPermission = ""; reportClosePermission = "chat.admin.bypass"; reportViewPermission = "chat.admin.bypass";
reportCooldown = 60; reportDiscordWebhook = ""; reportTelegramChatId = "";
}
// --- Join / Leave ---
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) =====
public Map<String, ChatChannel> getChannels() { return Collections.unmodifiableMap(channels); }
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 String getDefaultChannelId() { return defaultChannel; }
public String getHelpopFormat() { return helpopFormat; }
public String getHelpopPermission() { return helpopPermission; }
public int getHelpopCooldown() { return helpopCooldown; }
public String getHelpopConfirm() { return helpopConfirm; }
public String getHelpopDiscordWebhook() { return helpopDiscordWebhook; }
public String getHelpopTelegramChatId() { return helpopTelegramChatId; }
public String getBroadcastFormat() { return broadcastFormat; }
public String getBroadcastPermission() { return broadcastPermission; }
public boolean isPmEnabled() { return pmEnabled; }
public String getPmFormatSender() { return pmFormatSender; }
public String getPmFormatReceiver() { return pmFormatReceiver; }
public String getPmFormatSpy() { return pmFormatSpy; }
public String getPmSpyPermission() { return pmSpyPermission; }
public boolean isPmRateLimitEnabled() { return pmRateLimitEnabled; }
public long getPmRateLimitWindowMs() { return pmRateLimitWindowMs; }
public int getPmRateLimitMaxActions() { return pmRateLimitMaxActions; }
public long getPmRateLimitBlockMs() { return pmRateLimitBlockMs; }
public String getPmRateLimitMessage() { return pmRateLimitMessage; }
public int getDefaultMuteDuration() { return defaultMuteDuration; }
public String getMutedMessage() { return mutedMessage; }
public boolean isEmojiEnabled() { return emojiEnabled; }
public boolean isEmojiBedrockSupport() { return emojiBedrockSupport; }
public Map<String, String> getEmojiMappings() { return Collections.unmodifiableMap(emojiMappings); }
public boolean isDiscordEnabled() { return discordEnabled; }
public String getDiscordBotToken() { return discordBotToken; }
public String getDiscordGuildId() { return discordGuildId; }
public int getDiscordPollInterval() { return discordPollInterval; }
public String getDiscordFromFormat() { return discordFromFormat; }
public String getDiscordAdminChannelId() { return discordAdminChannelId; }
public String getDiscordEmbedColor() { return discordEmbedColor; }
public boolean isTelegramEnabled() { return telegramEnabled; }
public String getTelegramBotToken() { return telegramBotToken; }
public int getTelegramPollInterval() { return telegramPollInterval; }
public String getTelegramFromFormat() { return telegramFromFormat; }
public String getTelegramAdminChatId() { return telegramAdminChatId; }
public int getTelegramChatTopicId() { return telegramChatTopicId; }
public int getTelegramAdminTopicId() { return telegramAdminTopicId; }
// ===== Getter (Account-Linking) =====
public boolean isLinkingEnabled() { return linkingEnabled; }
public String getLinkDiscordMessage() { return linkDiscordMessage; }
public String getLinkTelegramMessage() { return linkTelegramMessage; }
public String getLinkSuccessDiscord() { return linkSuccessDiscord; }
public String getLinkSuccessTelegram() { return linkSuccessTelegram; }
public String getLinkBotSuccessDiscord() { return linkBotSuccessDiscord; }
public String getLinkBotSuccessTelegram() { return linkBotSuccessTelegram; }
public String getLinkedDiscordFormat() { return linkedDiscordFormat; }
public String getLinkedTelegramFormat() { return linkedTelegramFormat; }
public String getAdminBypassPermission() { return adminBypassPermission; }
public String getAdminNotifyPermission() { return adminNotifyPermission; }
// ===== 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;
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()); }
}
public Map<String, String> getServerColors() { return Collections.unmodifiableMap(serverColors); }
public String getServerColorDefault() { return serverColorDefault; }
// ===== Getter =====
/**
* 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 int getChatlogRetentionDays() { return chatlogRetentionDays; }
// ===== Getter (NEU: Reports) =====
public boolean isReportsEnabled() { return reportsEnabled; }
public String getReportConfirm() { return reportConfirm; }
public String getReportPermission() { return reportPermission; }
public String getReportClosePermission() { return reportClosePermission; }
public String getReportViewPermission() { return reportViewPermission; }
public int getReportCooldown() { return reportCooldown; }
public Map<String, ChatChannel> getChannels() { return Collections.unmodifiableMap(channels); }
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 String getDefaultChannelId() { return defaultChannel; }
public String getHelpopFormat() { return helpopFormat; }
public String getHelpopPermission() { return helpopPermission; }
public int getHelpopCooldown() { return helpopCooldown; }
public String getHelpopConfirm() { return helpopConfirm; }
public String getHelpopDiscordWebhook() { return helpopDiscordWebhook; }
public String getHelpopTelegramChatId() { return helpopTelegramChatId; }
public String getBroadcastFormat() { return broadcastFormat; }
public String getBroadcastPermission() { return broadcastPermission; }
public boolean isPmEnabled() { return pmEnabled; }
public String getPmFormatSender() { return pmFormatSender; }
public String getPmFormatReceiver() { return pmFormatReceiver; }
public String getPmFormatSpy() { return pmFormatSpy; }
public String getPmSpyPermission() { return pmSpyPermission; }
public boolean isPmRateLimitEnabled() { return pmRateLimitEnabled; }
public long getPmRateLimitWindowMs() { return pmRateLimitWindowMs; }
public int getPmRateLimitMaxActions() { return pmRateLimitMaxActions; }
public long getPmRateLimitBlockMs() { return pmRateLimitBlockMs; }
public String getPmRateLimitMessage() { return pmRateLimitMessage; }
public int getDefaultMuteDuration() { return defaultMuteDuration; }
public String getMutedMessage() { return mutedMessage; }
public boolean isEmojiEnabled() { return emojiEnabled; }
public boolean isEmojiBedrockSupport() { return emojiBedrockSupport; }
public Map<String, String> getEmojiMappings() { return Collections.unmodifiableMap(emojiMappings); }
public boolean isDiscordEnabled() { return discordEnabled; }
public String getDiscordBotToken() { return discordBotToken; }
public String getDiscordGuildId() { return discordGuildId; }
public int getDiscordPollInterval() { return discordPollInterval; }
public String getDiscordFromFormat() { return discordFromFormat; }
public String getDiscordAdminChannelId() { return discordAdminChannelId; }
public String getDiscordEmbedColor() { return discordEmbedColor; }
public boolean isTelegramEnabled() { return telegramEnabled; }
public String getTelegramBotToken() { return telegramBotToken; }
public int getTelegramPollInterval() { return telegramPollInterval; }
public String getTelegramFromFormat() { return telegramFromFormat; }
public String getTelegramAdminChatId() { return telegramAdminChatId; }
public int getTelegramChatTopicId() { return telegramChatTopicId; }
public int getTelegramAdminTopicId() { return telegramAdminTopicId; }
public boolean isLinkingEnabled() { return linkingEnabled; }
public String getLinkDiscordMessage() { return linkDiscordMessage; }
public String getLinkTelegramMessage() { return linkTelegramMessage; }
public String getLinkSuccessDiscord() { return linkSuccessDiscord; }
public String getLinkSuccessTelegram() { return linkSuccessTelegram; }
public String getLinkBotSuccessDiscord() { return linkBotSuccessDiscord; }
public String getLinkBotSuccessTelegram() { return linkBotSuccessTelegram; }
public String getLinkedDiscordFormat() { return linkedDiscordFormat; }
public String getLinkedTelegramFormat() { return linkedTelegramFormat; }
public String getAdminBypassPermission() { return adminBypassPermission; }
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; }
public Map<String, String> getServerColors() { return Collections.unmodifiableMap(serverColors); }
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; }
public boolean isChatlogEnabled() { return chatlogEnabled; }
public int getChatlogRetentionDays() { return chatlogRetentionDays; }
public boolean isReportsEnabled() { return reportsEnabled; }
public String getReportConfirm() { return reportConfirm; }
public String getReportPermission() { return reportPermission; }
public String getReportClosePermission() { return reportClosePermission; }
public String getReportViewPermission() { return reportViewPermission; }
public int getReportCooldown() { return reportCooldown; }
public String getReportDiscordWebhook() { return reportDiscordWebhook; }
public String getReportTelegramChatId() { return reportTelegramChatId; }
public boolean isReportWebhookEnabled() { return reportWebhookEnabled; }
// ===== Getter (Chat-Filter) =====
public ChatFilter.ChatFilterConfig getFilterConfig() { return filterConfig; }
// ===== Getter (Mentions) =====
public boolean isMentionsEnabled() { return mentionsEnabled; }
public String getMentionsHighlightColor() { return mentionsHighlightColor; }
public String getMentionsSound() { return mentionsSound; }
public boolean isMentionsAllowToggle() { return mentionsAllowToggle; }
public String getMentionsNotifyPrefix() { return mentionsNotifyPrefix; }
// ===== Getter (Chat-History) =====
public int getHistoryMaxLines() { return historyMaxLines; }
public int getHistoryDefaultLines() { return historyDefaultLines; }
public boolean isMentionsEnabled() { return mentionsEnabled; }
public String getMentionsHighlightColor() { return mentionsHighlightColor; }
public String getMentionsSound() { return mentionsSound; }
public boolean isMentionsAllowToggle() { return mentionsAllowToggle; }
public String getMentionsNotifyPrefix() { return mentionsNotifyPrefix; }
public int getHistoryMaxLines() { return historyMaxLines; }
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(
modified ? FilterResult.MODIFIED : FilterResult.ALLOWED,
result,
@@ -187,14 +194,88 @@ public class ChatFilter {
// Kein Recht → & und nächstes Zeichen überspringen
if (isColor || isFormat) { i++; continue; }
// Hex: &# + 6 Zeichen überspringen
if (isHex && i + 7 < message.length()) { i += 7; continue; }
// Hex: &# + 6 Zeichen überspringen (i zeigt auf &, +1 = #, +2..+7 = RRGGBB)
if (isHex && i + 7 <= message.length()) { i += 7; continue; }
}
sb.append(c);
}
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) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < Math.max(length, 4); i++) sb.append('*');
@@ -235,5 +316,16 @@ public class ChatFilter {
public boolean capsFilterEnabled = true;
public int capsMinLength = 6; // Mindestlänge für Caps-Check
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)
* ✅ Admin-Benachrichtigung bei Reports (sofort oder verzögert nach Login)
* ✅ 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 {
@@ -74,10 +76,13 @@ public class ChatModule implements Module, Listener {
private final Map<UUID, Long> helpopCooldowns = new ConcurrentHashMap<>();
// 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
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: ".")
private static final String GEYSER_PREFIX = ".";
@@ -105,13 +110,13 @@ public class ChatModule implements Module, Listener {
emojiParser = new EmojiParser(config.getEmojiMappings(), config.isEmojiEnabled());
chatFilter = new ChatFilter(config.getFilterConfig());
// NEU: ChatLogger
// ChatLogger
if (config.isChatlogEnabled()) {
chatLogger = new ChatLogger(plugin.getDataFolder(), logger, config.getChatlogRetentionDays());
logger.info("[ChatModule] Chat-Log aktiviert (" + config.getChatlogRetentionDays() + " Tage Aufbewahrung).");
}
// NEU: ReportManager
// ReportManager
if (config.isReportsEnabled()) {
reportManager = new ReportManager(plugin.getDataFolder(), logger);
reportManager.load();
@@ -155,7 +160,6 @@ public class ChatModule implements Module, Listener {
helpopCooldowns.clear();
reportCooldowns.clear();
lastChatMessages.clear();
logger.info("[ChatModule] Deaktiviert.");
}
// =========================================================
@@ -182,11 +186,6 @@ public class ChatModule implements Module, Listener {
if (!(e.getSender() instanceof ProxiedPlayer)) return;
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())) {
awaitingInput.remove(player.getUniqueId());
return; // Event NICHT cancellen → Nachricht geht mit Originalsignatur durch
@@ -198,7 +197,6 @@ public class ChatModule implements Module, Listener {
/**
* Zentrale Chat-Verarbeitungslogik.
* Wird von beiden Event-Handlern aufgerufen.
*/
private void processChat(ProxiedPlayer player, String rawMessage) {
if (rawMessage == null || rawMessage.trim().isEmpty()) return;
@@ -236,7 +234,7 @@ public class ChatModule implements Module, Listener {
player.sendMessage(color(filterResp.denyReason));
return;
}
message = filterResp.message; // ggf. modifiziert (Caps, Blacklist)
message = filterResp.message;
String serverName = player.getServer() != null
? player.getServer().getInfo().getName()
@@ -259,7 +257,6 @@ public class ChatModule implements Module, Listener {
ProxiedPlayer mentioned = ProxyServer.getInstance().getPlayer(targetName);
if (mentioned != null && !mentioned.getUniqueId().equals(uuid)) {
mentionedPlayers.add(mentioned.getUniqueId());
// Wort hervorheben
word = translateColors(highlightColor + word + "&r");
}
}
@@ -316,10 +313,8 @@ public class ChatModule implements Module, Listener {
&& !mentionsDisabled.contains(recipient.getUniqueId());
if (isMentioned) {
// Prefix-Nachricht über der Chat-Zeile
recipient.sendMessage(color(config.getMentionsNotifyPrefix()
+ "&7" + finalSenderName + " &7hat dich erwähnt!"));
// Sound via Plugin-Messaging an Sub-Server senden
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
public void onLogin(PostLoginEvent e) {
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)
if (reportManager == null) return;
if (!player.hasPermission(config.getAdminNotifyPermission())
&& !player.hasPermission(config.getAdminBypassPermission())) return;
int openCount = reportManager.getOpenCount();
if (openCount == 0) return;
// Standard-Kanal setzen
playerChannels.put(uuid, config.getDefaultChannelId());
// 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, () -> {
if (!player.isConnected()) return;
int count = reportManager.getOpenCount();
if (count == 0) return;
player.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
player.sendMessage(color("&c&l⚑ OFFENE REPORTS &8| &f" + count + " ausstehend"));
player.sendMessage(color("&7Tippe &f/reports &7für eine Übersicht."));
player.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
// ── 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("&c&l⚑ OFFENE REPORTS &8| &f" + count + " ausstehend"));
player.sendMessage(color("&7Tippe &f/reports &7für eine Übersicht."));
player.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
}
}
}, 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
// =========================================================
@@ -452,17 +550,24 @@ public class ChatModule implements Module, Listener {
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, helpop);
// /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") {
@Override
public void execute(CommandSender sender, String[] args) {
if (!config.isPmEnabled()) { sender.sendMessage(color("&cPrivat-Nachrichten sind deaktiviert.")); return; }
if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(color("&cNur Spieler!")); return; }
if (args.length < 2) { sender.sendMessage(color("&cBenutzung: /msg <Spieler> <Nachricht>")); return; }
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()) {
from.sendMessage(color("&cSpieler &f" + args[0] + " &cnicht gefunden.")); return;
}
String message = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
pmManager.send(from, to, message, config, config.getAdminBypassPermission());
}
@@ -652,27 +757,24 @@ public class ChatModule implements Module, Listener {
UUID tUUID = target.getUniqueId();
String tServer = target.getServer() != null ? target.getServer().getInfo().getName() : "Proxy";
// Kanal
String channelId = playerChannels.getOrDefault(tUUID, config.getDefaultChannelId());
ChatChannel ch = config.getChannel(channelId);
String channelName = ch != null ? ch.getFormattedTag() + " &f" + ch.getName() : "&f" + channelId;
// Mute-Status
String muteStatus = muteManager.isMuted(tUUID)
? "&cJa &8(noch: &f" + muteManager.getRemainingTime(tUUID) + "&8)"
: "&aKein";
// Blockierungen
Set<UUID> blocked = blockManager.getBlockedBy(tUUID);
// Account-Links
AccountLinkManager.LinkedAccount link = linkManager.getByUUID(tUUID);
String discordInfo = (link != null && !link.discordUserId.isEmpty())
? "&a" + link.discordUsername + " &8(" + link.discordUserId + ")" : "&7Nicht verknüpft";
String telegramInfo = (link != null && !link.telegramUserId.isEmpty())
? "&a" + link.telegramUsername + " &8(" + link.telegramUserId + ")" : "&7Nicht verknüpft";
// Ausgabe
String vanishStatus = VanishProvider.isVanished(target) ? "&eJa" : "&aKein";
sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
sender.sendMessage(color("&eChatInfo: &f" + target.getName() + " &8@ &7" + tServer));
sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
@@ -680,6 +782,7 @@ public class ChatModule implements Module, Listener {
sender.sendMessage(color("&7Mute: " + muteStatus));
sender.sendMessage(color("&7Chat-aus: " + (selfChatMuted.contains(tUUID) ? "&cJa" : "&aKein")));
sender.sendMessage(color("&7Mentions: " + (mentionsDisabled.contains(tUUID) ? "&cDeaktiviert" : "&aAktiv")));
sender.sendMessage(color("&7Vanish: " + vanishStatus));
sender.sendMessage(color("&7Blockiert: &f" + blocked.size() + " Spieler"));
if (!blocked.isEmpty()) {
for (UUID bUUID : blocked) {
@@ -707,7 +810,6 @@ public class ChatModule implements Module, Listener {
int lines = config.getHistoryDefaultLines();
if (args.length >= 1) {
// Erstes Arg: Spielername oder Zahl?
try {
lines = Math.min(Integer.parseInt(args[0]), config.getHistoryMaxLines());
} catch (NumberFormatException ex) {
@@ -765,7 +867,6 @@ public class ChatModule implements Module, Listener {
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, mentionsCmd);
// /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") {
@Override
public void execute(CommandSender sender, String[] args) {
@@ -784,9 +885,7 @@ public class ChatModule implements Module, Listener {
};
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, bypassCmd);
// ─────────────────────────────────────────────────────
// /discordlink Discord-Account verknüpfen
// ─────────────────────────────────────────────────────
// /discordlink Discord-Account verknüpfen
Command discordLinkCmd = new Command("discordlink", null, "dlink") {
@Override
public void execute(CommandSender sender, String[] args) {
@@ -806,9 +905,7 @@ public class ChatModule implements Module, Listener {
};
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, discordLinkCmd);
// ─────────────────────────────────────────────────────
// /telegramlink Telegram-Account verknüpfen
// ─────────────────────────────────────────────────────
// /telegramlink Telegram-Account verknüpfen
Command telegramLinkCmd = new Command("telegramlink", null, "tlink") {
@Override
public void execute(CommandSender sender, String[] args) {
@@ -828,9 +925,7 @@ public class ChatModule implements Module, Listener {
};
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, telegramLinkCmd);
// ─────────────────────────────────────────────────────
// /unlink Verknüpfung aufheben
// ─────────────────────────────────────────────────────
// /unlink Verknüpfung aufheben
Command unlinkCmd = new Command("unlink") {
@Override
public void execute(CommandSender sender, String[] args) {
@@ -868,9 +963,7 @@ public class ChatModule implements Module, Listener {
};
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, unlinkCmd);
// ─────────────────────────────────────────────────────
// NEU: /report <spieler> <grund>
// ─────────────────────────────────────────────────────
// /report <spieler> <grund>
Command reportCmd = new Command("report") {
@Override
public void execute(CommandSender sender, String[] args) {
@@ -879,7 +972,6 @@ public class ChatModule implements Module, Listener {
ProxiedPlayer p = (ProxiedPlayer) sender;
// Permission prüfen (optional)
String reqPerm = config.getReportPermission();
if (reqPerm != null && !reqPerm.isEmpty() && !p.hasPermission(reqPerm)) {
p.sendMessage(color("&cDu hast keine Berechtigung für /report.")); return;
@@ -890,7 +982,6 @@ public class ChatModule implements Module, Listener {
return;
}
// Cooldown
long now = System.currentTimeMillis() / 1000L;
Long last = reportCooldowns.get(p.getUniqueId());
if (last != null && (now - last) < config.getReportCooldown()) {
@@ -902,67 +993,54 @@ public class ChatModule implements Module, Listener {
String reportedName = args[0];
String reason = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
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)");
// Report erstellen
String reportId = reportManager.createReport(
p.getName(), p.getUniqueId(), reportedName, server, msgContext, reason);
// Report auch ins Chatlog schreiben (ID sichtbar)
if (chatLogger != null) {
String logMsg = "[REPORT] Reporter: " + p.getName() + ", Gemeldet: " + reportedName + ", Grund: " + reason +
" | Letzte Nachricht: " + msgContext + " | Report-ID: " + reportId;
String msgId = reportId; // Damit die ID im Chatlog und im Report identisch ist
chatLogger.log(msgId, server, "report", p.getName(), logMsg);
String logMsg = "[REPORT] Reporter: " + p.getName() + ", Gemeldet: " + reportedName
+ ", Grund: " + reason + " | Letzte Nachricht: " + msgContext
+ " | Report-ID: " + reportId;
chatLogger.log(reportId, server, "report", p.getName(), logMsg);
}
// ==== Discord/Telegram Benachrichtigung ====
// Discord Webhook
String reportWebhook = config.getReportDiscordWebhook();
logger.info("[Debug] DiscordWebhookEnabled=" + config.isReportWebhookEnabled()
+ ", discordBridge=" + (discordBridge != null)
+ ", reportWebhook=" + reportWebhook);
if (config.isReportWebhookEnabled() && discordBridge != null && reportWebhook != null && !reportWebhook.isEmpty()) {
if (config.isReportWebhookEnabled() && discordBridge != null
&& reportWebhook != null && !reportWebhook.isEmpty()) {
String title = "Neuer Report eingegangen";
String desc = "**Reporter:** " + p.getName() +
"\n**Gemeldet:** " + reportedName +
"\n**Server:** " + server +
"\n**Grund:** " + reason +
"\n**Letzte Nachricht:** " + msgContext +
"\n**Report-ID:** " + reportId;
String desc = "**Reporter:** " + p.getName()
+ "\n**Gemeldet:** " + reportedName
+ "\n**Server:** " + server
+ "\n**Grund:** " + reason
+ "\n**Letzte Nachricht:** " + msgContext
+ "\n**Report-ID:** " + reportId;
discordBridge.sendEmbedToDiscord(reportWebhook, title, desc, config.getDiscordEmbedColor());
}
// Telegram Benachrichtigung
String reportTgChatId = config.getReportTelegramChatId();
if (telegramBridge != null && reportTgChatId != null && !reportTgChatId.isEmpty()) {
String header = "Neuer Report eingegangen";
String content = "Reporter: " + p.getName() +
"\nGemeldet: " + reportedName +
"\nServer: " + server +
"\nGrund: " + reason +
"\nLetzte Nachricht: " + msgContext +
"\nReport-ID: " + reportId;
String content = "Reporter: " + p.getName()
+ "\nGemeldet: " + reportedName
+ "\nServer: " + server
+ "\nGrund: " + reason
+ "\nLetzte Nachricht: " + msgContext
+ "\nReport-ID: " + reportId;
telegramBridge.sendFormattedToTelegram(reportTgChatId, header, content);
}
reportCooldowns.put(p.getUniqueId(), now);
// Bestätigung an Reporter
String confirm = config.getReportConfirm().replace("{id}", reportId);
p.sendMessage(color(confirm));
// ── Online-Admins sofort benachrichtigen ──
notifyAdminsReport(reportId, p.getName(), reportedName, server, reason, msgContext);
}
};
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, reportCmd);
// ─────────────────────────────────────────────────────
// NEU: /reports [all] Admin-Übersicht
// ─────────────────────────────────────────────────────
// /reports [all] Admin-Übersicht
Command reportsCmd = new Command("reports", config.getReportViewPermission()) {
@Override
public void execute(CommandSender sender, String[] args) {
@@ -988,15 +1066,12 @@ public class ChatModule implements Module, Listener {
String statusColor = r.closed ? "&a✔" : "&c✘";
if (sender instanceof ProxiedPlayer) {
// Klickbare Zeile: ID-Click kopiert ID in Zwischenablage
ComponentBuilder line = new ComponentBuilder("");
// Status
line.append(ChatColor.translateAlternateColorCodes('&', statusColor + " "))
.event((ClickEvent) null)
.event((HoverEvent) null);
// Klickbare Report-ID
line.append(ChatColor.translateAlternateColorCodes('&', "&8[&f" + r.id + "&8]"))
.event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, r.id))
.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
+ (r.closed ? "\n" + ChatColor.GREEN + "Geschlossen von: " + r.closedBy : "")).create()));
// Rest der Zeile
line.append(ChatColor.translateAlternateColorCodes('&',
" &b" + r.reportedName + " &8← &7" + r.reporterName
+ " &8@ &a" + r.server
@@ -1019,7 +1093,6 @@ public class ChatModule implements Module, Listener {
((ProxiedPlayer) sender).sendMessage(line.create());
} else {
// Konsole: plain text
sender.sendMessage(color(statusColor + " &8[&f" + r.id + "&8] &b" + r.reportedName
+ " &8← &7" + r.reporterName + " &8@ &a" + r.server
+ " &8| &e" + r.getFormattedTime()
@@ -1035,9 +1108,7 @@ public class ChatModule implements Module, Listener {
};
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, reportsCmd);
// ─────────────────────────────────────────────────────
// NEU: /reportclose <ID>
// ─────────────────────────────────────────────────────
// /reportclose <ID>
Command reportCloseCmd = new Command("reportclose", config.getReportClosePermission()) {
@Override
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."));
// Reporter benachrichtigen, falls online
ProxiedPlayer reporter = ProxyServer.getInstance().getPlayer(report.reporterUUID);
if (reporter != null && reporter.isConnected()) {
reporter.sendMessage(color("&8[&aReport&8] &7Dein Report &f" + id
+ " &7gegen &c" + report.reportedName + " &7wurde von &b" + adminName + " &7bearbeitet."));
}
// Andere Admins informieren
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
//
// 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
* 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:
* Öffentliche API für Sub-Server-Plugins oder BungeeCord-eigene Plugins.
* Setzt den Bypass-Status für einen Spieler.
*
* Beispiel aus einem anderen BungeeCord-Plugin:
@@ -1126,6 +1167,24 @@ public class ChatModule implements Module, Listener {
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
// =========================================================
@@ -1133,7 +1192,7 @@ public class ChatModule implements Module, Listener {
private String buildFormat(String format, String server, String prefix,
String player, String suffix, String message) {
String serverColor = config.getServerColor(server);
String serverDisplay = config.getServerDisplay(server); // Anzeigename aus config
String serverDisplay = config.getServerDisplay(server);
String coloredServer = translateColors(serverColor + serverDisplay + "&r");
return format
@@ -1158,23 +1217,13 @@ public class ChatModule implements Module, Listener {
/**
* Ü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) {
if (text == null) return "";
// 1. Schritt: &#RRGGBB → BungeeCord ChatColor
StringBuilder sb = new StringBuilder();
int i = 0;
while (i < text.length()) {
// Prüfe auf &#RRGGBB (8 Zeichen: & # R R G G B B)
if (i + 7 < text.length()
&& text.charAt(i) == '&'
&& text.charAt(i + 1) == '#') {
@@ -1195,7 +1244,6 @@ public class ChatModule implements Module, Listener {
i++;
}
// 2. Schritt: Standard &-Codes übersetzen
return ChatColor.translateAlternateColorCodes('&', sb.toString());
}
@@ -1209,11 +1257,9 @@ public class ChatModule implements Module, Listener {
/**
* Benachrichtigt alle online Admins über einen neuen Report.
* Baut eine mehrzeilige, klickbare Nachricht.
*/
private void notifyAdminsReport(String reportId, String reporter, String reported,
String server, String reason, String msgContext) {
// Zeitstempel
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("dd.MM.yyyy HH:mm:ss");
String zeit = sdf.format(new java.util.Date());
@@ -1221,7 +1267,6 @@ public class ChatModule implements Module, Listener {
if (!p.hasPermission(config.getAdminNotifyPermission())
&& !p.hasPermission(config.getAdminBypassPermission())) continue;
// Mehrzeilige Report-Notification
p.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
p.sendMessage(color("&8[&cReport&8] &7Zeit: &e" + zeit));
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("&7Grund: &c" + reason));
// Klickbare ID-Zeile
ComponentBuilder idLine = new ComponentBuilder(ChatColor.GRAY + "ID: ");
idLine.append(ChatColor.WHITE + "" + ChatColor.BOLD + reportId)
.event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, reportId))
@@ -1244,28 +1288,22 @@ public class ChatModule implements Module, Listener {
p.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
}
// Konsole ebenfalls informieren
ProxyServer.getInstance().getConsole().sendMessage(color(
"&8[&cReport " + reportId + "&8] &7Reporter: &b" + reporter
+ " &7→ &c" + reported + " &7@ &a" + server + " &8| &cGrund: &f" + reason));
}
/**
* Baut eine BungeeCord-Nachricht ohne sichtbare ID.
* Am Ende erscheint ein klickbarer [⚑] Melden-Button (nur wenn Reports aktiviert).
*
* Layout: <formatierter Chat> §8[§c⚑§8]
* Baut eine BungeeCord-Nachricht mit klickbarem [⚑] Melden-Button.
*/
private BaseComponent[] buildClickableMessage(String msgId, String formatted, String senderName) {
ComponentBuilder builder = new ComponentBuilder("");
// Eigentliche Nachricht (kein ID-Tag mehr sichtbar)
builder.append(ChatColor.translateAlternateColorCodes('&', formatted),
ComponentBuilder.FormatRetention.NONE)
.event((ClickEvent) null)
.event((HoverEvent) null);
// [⚑] Melden-Button am Ende (nur wenn Report-System aktiv und Sender bekannt)
if (msgId != null && senderName != null && reportManager != null) {
builder.append(" ", ComponentBuilder.FormatRetention.NONE)
.event((ClickEvent) null)
@@ -1287,15 +1325,11 @@ public class ChatModule implements Module, Listener {
}
/**
* Sendet einen Sound an einen Spieler via Plugin-Messaging.
* 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.
* Sendet einen Sound-Hinweis via Actionbar (Mention-Feedback).
*/
private void sendMentionSound(ProxiedPlayer player, String soundName) {
if (soundName == null || soundName.isEmpty()) return;
try {
// Actionbar als visuellen Feedback (funktioniert ohne Sub-Server-Plugin)
net.md_5.bungee.api.chat.TextComponent actionBar =
new net.md_5.bungee.api.chat.TextComponent(
ChatColor.translateAlternateColorCodes('&', "&e♪ Mention!"));

View File

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

View File

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

View File

@@ -82,7 +82,7 @@ public class NetworkInfoModule implements Module {
loadConfig();
if (!enabled) {
plugin.getLogger().info("[NetworkInfoModule] deaktiviert via " + CONFIG_FILE_NAME + " (networkinfo.enabled=false)");
return;
}
@@ -109,7 +109,7 @@ public class NetworkInfoModule implements Module {
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
@@ -514,7 +514,7 @@ public class NetworkInfoModule implements Module {
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
plugin.getLogger().info("[NetworkInfoModule] " + CONFIG_FILE_NAME + " wurde erstellt.");
} catch (Exception e) {
plugin.getLogger().warning("[NetworkInfoModule] Konnte " + CONFIG_FILE_NAME + " nicht erstellen: " + e.getMessage());
}

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

View File

@@ -11,6 +11,20 @@ public class PlayerStats {
public long currentSessionStart;
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) {
this.uuid = uuid;
this.name = name;
@@ -20,6 +34,16 @@ public class PlayerStats {
this.totalPlaytime = 0;
this.currentSessionStart = 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() {
@@ -47,7 +71,10 @@ public class PlayerStats {
}
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) {
@@ -62,6 +89,22 @@ public class PlayerStats {
ps.totalPlaytime = Long.parseLong(parts[4]);
ps.currentSessionStart = Long.parseLong(parts[5]);
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;
} catch (Exception e) {
return null;

View File

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

View File

@@ -2,8 +2,13 @@ package net.viper.status.stats;
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 {
private final File file;
private final Object fileLock = new Object();
public StatsStorage(File pluginFolder) {
if (!pluginFolder.exists()) pluginFolder.mkdirs();
@@ -11,27 +16,31 @@ public class StatsStorage {
}
public void save(StatsManager manager) {
try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
for (PlayerStats ps : manager.all()) {
bw.write(ps.toLine());
bw.newLine();
synchronized (fileLock) {
try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
for (PlayerStats ps : manager.all()) {
bw.write(ps.toLine());
bw.newLine();
}
bw.flush();
} catch (IOException e) {
e.printStackTrace();
}
bw.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
public void load(StatsManager manager) {
if (!file.exists()) return;
try (BufferedReader br = new BufferedReader(new FileReader(file))) {
String line;
while ((line = br.readLine()) != null) {
PlayerStats ps = PlayerStats.fromLine(line);
if (ps != null) manager.put(ps);
synchronized (fileLock) {
try (BufferedReader br = new BufferedReader(new FileReader(file))) {
String line;
while ((line = br.readLine()) != null) {
PlayerStats ps = PlayerStats.fromLine(line);
if (ps != null) manager.put(ps);
}
} catch (IOException e) {
e.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

View File

@@ -135,6 +135,30 @@ private-messages:
format-social-spy: "&8[&dSPY &7{sender} &8→ &7{receiver}&8] &f{message}"
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
# Zentraler Schutz für Chat/PM/Command-Flood.
@@ -304,6 +328,36 @@ chat-filter:
min-length: 6
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)
# ============================================================

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
version: 1.0.0
main: net.viper.statusapibridge.StatusAPIBridge
api-version: 1.21
description: Sendet Vault-Economy-Daten an die BungeeCord StatusAPI
authors: [Viper]
softdepend: [Vault]
name: StatusAPI
main: net.viper.status.StatusAPI
version: 4.1.0
author: M_Viper
description: StatusAPI für BungeeCord inkl. Update-Checker, Modul-System und ChatModule
# Mindestanforderung: Minecraft 1.20 / BungeeCord mit PlayerChatEvent-Unterstützung
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
# ===========================
# WORDPRESS / VERIFY EINSTELLUNGEN
# ===========================