Upload folder via GUI - src
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -102,8 +102,6 @@ public class UpdateChecker {
|
|||||||
plugin.getLogger().warning("Keine StatusAPI.jar im neuesten Release gefunden.");
|
plugin.getLogger().warning("Keine StatusAPI.jar im neuesten Release gefunden.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
plugin.getLogger().info("Gefundene Version: " + foundVersion + " (Aktuell: " + currentVersion + ")");
|
|
||||||
latestVersion = foundVersion;
|
latestVersion = foundVersion;
|
||||||
latestUrl = foundUrl;
|
latestUrl = foundUrl;
|
||||||
|
|
||||||
|
|||||||
@@ -1,60 +1,59 @@
|
|||||||
package net.viper.status.module;
|
package net.viper.status.module;
|
||||||
|
|
||||||
import net.md_5.bungee.api.plugin.Plugin;
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.HashMap;
|
import java.util.Map;
|
||||||
import java.util.Map;
|
|
||||||
|
/**
|
||||||
/**
|
* Verwaltet alle geladenen Module.
|
||||||
* Verwaltet alle geladenen Module.
|
* Verwendet LinkedHashMap um die Registrierungsreihenfolge zu erhalten,
|
||||||
*/
|
* damit Abhängigkeiten (z.B. VanishModule → ChatModule) korrekt aufgelöst werden.
|
||||||
public class ModuleManager {
|
*/
|
||||||
|
public class ModuleManager {
|
||||||
private final Map<String, Module> modules = new HashMap<>();
|
|
||||||
|
private final Map<String, Module> modules = new LinkedHashMap<>();
|
||||||
public void registerModule(Module module) {
|
|
||||||
modules.put(module.getName().toLowerCase(), module);
|
public void registerModule(Module module) {
|
||||||
}
|
modules.put(module.getName().toLowerCase(), module);
|
||||||
|
}
|
||||||
public void enableAll(Plugin plugin) {
|
|
||||||
for (Module module : modules.values()) {
|
public void enableAll(Plugin plugin) {
|
||||||
try {
|
for (Module module : modules.values()) {
|
||||||
plugin.getLogger().info("Aktiviere Modul: " + module.getName() + "...");
|
try {
|
||||||
module.onEnable(plugin);
|
module.onEnable(plugin);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
plugin.getLogger().severe("Fehler beim Aktivieren von Modul " + module.getName() + ": " + e.getMessage());
|
plugin.getLogger().severe("Fehler beim Aktivieren von Modul " + module.getName() + ": " + e.getMessage());
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void disableAll(Plugin plugin) {
|
public void disableAll(Plugin plugin) {
|
||||||
for (Module module : modules.values()) {
|
for (Module module : modules.values()) {
|
||||||
try {
|
try {
|
||||||
plugin.getLogger().info("Deaktiviere Modul: " + module.getName() + "...");
|
module.onDisable(plugin);
|
||||||
module.onDisable(plugin);
|
} catch (Exception e) {
|
||||||
} catch (Exception e) {
|
plugin.getLogger().warning("Fehler beim Deaktivieren von Modul " + module.getName());
|
||||||
plugin.getLogger().warning("Fehler beim Deaktivieren von Modul " + module.getName());
|
}
|
||||||
}
|
}
|
||||||
}
|
modules.clear();
|
||||||
modules.clear();
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
/**
|
* Ermöglicht anderen Komponenten (wie dem WebServer) Zugriff auf spezifische Module.
|
||||||
* Ermöglicht anderen Komponenten (wie dem WebServer) Zugriff auf spezifische Module.
|
*/
|
||||||
*/
|
public Module getModule(String name) {
|
||||||
public Module getModule(String name) {
|
return modules.get(name.toLowerCase());
|
||||||
return modules.get(name.toLowerCase());
|
}
|
||||||
}
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
@SuppressWarnings("unchecked")
|
public <T extends Module> T getModule(Class<T> clazz) {
|
||||||
public <T extends Module> T getModule(Class<T> clazz) {
|
for (Module m : modules.values()) {
|
||||||
for (Module m : modules.values()) {
|
if (clazz.isInstance(m)) {
|
||||||
if (clazz.isInstance(m)) {
|
return (T) m;
|
||||||
return (T) m;
|
}
|
||||||
}
|
}
|
||||||
}
|
return null;
|
||||||
return null;
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,115 +1,132 @@
|
|||||||
package net.viper.status.modules.AutoMessage;
|
package net.viper.status.modules.AutoMessage;
|
||||||
|
|
||||||
import net.md_5.bungee.api.ChatColor;
|
import net.md_5.bungee.api.ChatColor;
|
||||||
import net.md_5.bungee.api.ProxyServer;
|
import net.md_5.bungee.api.CommandSender;
|
||||||
import net.md_5.bungee.api.chat.TextComponent;
|
import net.md_5.bungee.api.ProxyServer;
|
||||||
import net.md_5.bungee.api.plugin.Plugin;
|
import net.md_5.bungee.api.chat.TextComponent;
|
||||||
import net.viper.status.StatusAPI;
|
import net.md_5.bungee.api.plugin.Command;
|
||||||
import net.viper.status.module.Module;
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
import java.io.File;
|
import net.viper.status.module.Module;
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.io.File;
|
||||||
import java.nio.file.Files;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Properties;
|
import java.nio.file.Files;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.List;
|
||||||
|
import java.util.Properties;
|
||||||
public class AutoMessageModule implements Module {
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
private int taskId = -1;
|
|
||||||
|
/**
|
||||||
// Diese Methode fehlte bisher und ist zwingend für das Interface
|
* AutoMessageModule
|
||||||
@Override
|
*
|
||||||
public String getName() {
|
* Fix #5:
|
||||||
return "AutoMessage";
|
* - 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)
|
||||||
@Override
|
* lädt die Konfiguration neu und setzt den Zähler zurück.
|
||||||
public void onEnable(Plugin plugin) {
|
* - TextComponent.fromLegacy() → ChatColor.translateAlternateColorCodes für §-Codes.
|
||||||
// Hier casten wir das Plugin-Objekt zu StatusAPI, um an spezifische Methoden zu kommen
|
*/
|
||||||
StatusAPI api = (StatusAPI) plugin;
|
public class AutoMessageModule implements Module {
|
||||||
|
|
||||||
// Konfiguration aus der zentralen verify.properties laden
|
private int taskId = -1;
|
||||||
Properties props = api.getVerifyProperties();
|
private StatusAPI api;
|
||||||
|
private final AtomicInteger currentIndex = new AtomicInteger(0);
|
||||||
boolean enabled = Boolean.parseBoolean(props.getProperty("automessage.enabled", "false"));
|
|
||||||
|
// Konfiguration (für Reload zugänglich)
|
||||||
if (!enabled) {
|
private volatile boolean enabled = false;
|
||||||
api.getLogger().info("AutoMessage-Modul ist deaktiviert.");
|
private volatile int intervalSeconds = 300;
|
||||||
return;
|
private volatile String fileName = "messages.txt";
|
||||||
}
|
private volatile String prefix = "";
|
||||||
|
|
||||||
// Interval in Sekunden einlesen
|
@Override
|
||||||
int intervalSeconds;
|
public String getName() { return "AutoMessage"; }
|
||||||
try {
|
|
||||||
intervalSeconds = Integer.parseInt(props.getProperty("automessage.interval", "300"));
|
@Override
|
||||||
} catch (NumberFormatException e) {
|
public void onEnable(Plugin plugin) {
|
||||||
api.getLogger().warning("Ungültiges Intervall für AutoMessage! Nutze Standard (300s).");
|
this.api = (StatusAPI) plugin;
|
||||||
intervalSeconds = 300;
|
loadSettings();
|
||||||
}
|
|
||||||
|
if (!enabled) return;
|
||||||
// Dateiname einlesen (Standard: messages.txt)
|
|
||||||
String fileName = props.getProperty("automessage.file", "messages.txt");
|
registerReloadCommand();
|
||||||
File messageFile = new File(api.getDataFolder(), fileName);
|
scheduleTask();
|
||||||
|
}
|
||||||
if (!messageFile.exists()) {
|
|
||||||
api.getLogger().warning("Die Datei '" + fileName + "' wurde nicht gefunden (" + messageFile.getAbsolutePath() + ")!");
|
@Override
|
||||||
api.getLogger().info("Erstelle eine leere Datei '" + fileName + "' als Vorlage...");
|
public void onDisable(Plugin plugin) {
|
||||||
try {
|
cancelTask();
|
||||||
messageFile.createNewFile();
|
}
|
||||||
} catch (IOException e) {
|
|
||||||
api.getLogger().severe("Konnte Datei nicht erstellen: " + e.getMessage());
|
private void loadSettings() {
|
||||||
}
|
Properties props = api.getVerifyProperties();
|
||||||
return;
|
enabled = Boolean.parseBoolean(props.getProperty("automessage.enabled", "false"));
|
||||||
}
|
String rawInterval = props.getProperty("automessage.interval", "300");
|
||||||
|
try { intervalSeconds = Integer.parseInt(rawInterval); }
|
||||||
// Nachrichten aus der Datei lesen
|
catch (NumberFormatException e) { api.getLogger().warning("Ungültiges Intervall für AutoMessage! Nutze Standard (300s)."); intervalSeconds = 300; }
|
||||||
List<String> messages;
|
fileName = props.getProperty("automessage.file", "messages.txt");
|
||||||
try {
|
prefix = props.getProperty("automessage.prefix", "");
|
||||||
messages = Files.readAllLines(messageFile.toPath(), StandardCharsets.UTF_8);
|
}
|
||||||
} catch (IOException e) {
|
|
||||||
api.getLogger().severe("Fehler beim Lesen von '" + fileName + "': " + e.getMessage());
|
private void registerReloadCommand() {
|
||||||
return;
|
ProxyServer.getInstance().getPluginManager().registerCommand(api, new Command("automessage", "statusapi.automessage") {
|
||||||
}
|
@Override
|
||||||
|
public void execute(CommandSender sender, String[] args) {
|
||||||
// Leere Zeilen und Kommentare herausfiltern
|
if (args.length > 0 && "reload".equalsIgnoreCase(args[0])) {
|
||||||
messages.removeIf(line -> line.trim().isEmpty() || line.trim().startsWith("#"));
|
cancelTask();
|
||||||
|
loadSettings();
|
||||||
if (messages.isEmpty()) {
|
currentIndex.set(0);
|
||||||
api.getLogger().warning("Die Datei '" + fileName + "' enthält keine gültigen Nachrichten!");
|
if (enabled) {
|
||||||
return;
|
scheduleTask();
|
||||||
}
|
sender.sendMessage(ChatColor.GREEN + "[AutoMessage] Neu geladen. Intervall: " + intervalSeconds + "s");
|
||||||
|
} else {
|
||||||
// Optional: Prefix aus Config lesen
|
sender.sendMessage(ChatColor.YELLOW + "[AutoMessage] Modul ist deaktiviert (automessage.enabled=false).");
|
||||||
String prefixRaw = props.getProperty("automessage.prefix", "");
|
}
|
||||||
String prefix = ChatColor.translateAlternateColorCodes('&', prefixRaw);
|
} else {
|
||||||
|
sender.sendMessage(ChatColor.YELLOW + "/automessage reload");
|
||||||
api.getLogger().info("Starte AutoMessage-Task (" + messages.size() + " Nachrichten aus " + fileName + ")");
|
}
|
||||||
|
}
|
||||||
// Finaler Index für den Lambda-Ausdruck
|
});
|
||||||
final int[] currentIndex = {0};
|
}
|
||||||
|
|
||||||
// Task planen
|
private void scheduleTask() {
|
||||||
taskId = ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
|
taskId = ProxyServer.getInstance().getScheduler().schedule(api, () -> {
|
||||||
String msg = messages.get(currentIndex[0]);
|
File messageFile = new File(api.getDataFolder(), fileName);
|
||||||
|
if (!messageFile.exists()) {
|
||||||
String finalMessage = (prefix.isEmpty() ? "" : prefix + " ") + msg;
|
api.getLogger().warning("[AutoMessage] Datei nicht gefunden: " + messageFile.getAbsolutePath());
|
||||||
|
return;
|
||||||
// Nachricht an alle auf dem Proxy senden
|
}
|
||||||
ProxyServer.getInstance().broadcast(TextComponent.fromLegacy(finalMessage));
|
|
||||||
|
// Fix #5: Datei bei jedem Tick neu einlesen → Änderungen wirken sofort
|
||||||
// Index erhöhen und Loop starten
|
List<String> messages;
|
||||||
currentIndex[0] = (currentIndex[0] + 1) % messages.size();
|
try {
|
||||||
}, intervalSeconds, intervalSeconds, TimeUnit.SECONDS).getId();
|
messages = Files.readAllLines(messageFile.toPath(), StandardCharsets.UTF_8);
|
||||||
}
|
} catch (IOException e) {
|
||||||
|
api.getLogger().severe("[AutoMessage] Fehler beim Lesen von '" + fileName + "': " + e.getMessage());
|
||||||
@Override
|
return;
|
||||||
public void onDisable(Plugin plugin) {
|
}
|
||||||
if (taskId != -1) {
|
messages.removeIf(line -> line.trim().isEmpty() || line.trim().startsWith("#"));
|
||||||
ProxyServer.getInstance().getScheduler().cancel(taskId);
|
if (messages.isEmpty()) return;
|
||||||
taskId = -1;
|
|
||||||
plugin.getLogger().info("AutoMessage-Task gestoppt.");
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,381 +1,313 @@
|
|||||||
package net.viper.status.modules.broadcast;
|
package net.viper.status.modules.broadcast;
|
||||||
|
|
||||||
import net.md_5.bungee.api.ChatColor;
|
import net.md_5.bungee.api.ChatColor;
|
||||||
import net.md_5.bungee.api.plugin.Plugin;
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||||
import net.md_5.bungee.api.chat.TextComponent;
|
import net.md_5.bungee.api.chat.TextComponent;
|
||||||
import net.md_5.bungee.api.plugin.Listener;
|
import net.md_5.bungee.api.plugin.Listener;
|
||||||
import net.viper.status.module.Module;
|
import net.viper.status.module.Module;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BroadcastModule
|
* BroadcastModule
|
||||||
*
|
*
|
||||||
* Speichert geplante Broadcasts jetzt persistent in 'broadcasts.schedules'.
|
* Fixes:
|
||||||
* Beim Neustart werden diese automatisch wieder geladen.
|
* - loadSchedules(): ID-Split nutzt jetzt indexOf/lastIndexOf statt split("\\.") mit length==2-Check.
|
||||||
*/
|
* Damit werden auch clientScheduleIds die Punkte enthalten korrekt geladen. (Bug #2)
|
||||||
public class BroadcastModule implements Module, Listener {
|
*/
|
||||||
|
public class BroadcastModule implements Module, Listener {
|
||||||
private Plugin plugin;
|
|
||||||
private boolean enabled = true;
|
private Plugin plugin;
|
||||||
private String requiredApiKey = "";
|
private boolean enabled = true;
|
||||||
private String format = "%prefix% %message%";
|
private String requiredApiKey = "";
|
||||||
private String fallbackPrefix = "[Broadcast]";
|
private String format = "%prefix% %message%";
|
||||||
private String fallbackPrefixColor = "&c";
|
private String fallbackPrefix = "[Broadcast]";
|
||||||
private String fallbackBracketColor = "&8"; // Neu
|
private String fallbackPrefixColor = "&c";
|
||||||
private String fallbackMessageColor = "&f";
|
private String fallbackBracketColor = "&8";
|
||||||
|
private String fallbackMessageColor = "&f";
|
||||||
private final Map<String, ScheduledBroadcast> scheduledByClientId = new ConcurrentHashMap<>();
|
|
||||||
private File schedulesFile;
|
private final Map<String, ScheduledBroadcast> scheduledByClientId = new ConcurrentHashMap<>();
|
||||||
private final SimpleDateFormat dateFormat;
|
private File schedulesFile;
|
||||||
|
private final SimpleDateFormat dateFormat;
|
||||||
public BroadcastModule() {
|
|
||||||
dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
|
public BroadcastModule() {
|
||||||
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
|
||||||
}
|
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||||
|
}
|
||||||
@Override
|
|
||||||
public String getName() {
|
@Override
|
||||||
return "BroadcastModule";
|
public String getName() { return "BroadcastModule"; }
|
||||||
}
|
|
||||||
|
@Override
|
||||||
@Override
|
public void onEnable(Plugin plugin) {
|
||||||
public void onEnable(Plugin plugin) {
|
this.plugin = plugin;
|
||||||
this.plugin = plugin;
|
schedulesFile = new File(plugin.getDataFolder(), "broadcasts.schedules");
|
||||||
schedulesFile = new File(plugin.getDataFolder(), "broadcasts.schedules");
|
loadConfig();
|
||||||
loadConfig();
|
if (!enabled) return;
|
||||||
|
try { plugin.getProxy().getPluginManager().registerListener(plugin, this); } catch (Throwable ignored) {}
|
||||||
if (!enabled) {
|
plugin.getLogger().info("[BroadcastModule] aktiviert. Format: " + format);
|
||||||
plugin.getLogger().info("[BroadcastModule] deaktiviert via verify.properties (broadcast.enabled=false)");
|
loadSchedules();
|
||||||
return;
|
plugin.getProxy().getScheduler().schedule(plugin, this::processScheduled, 1, 1, TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
@Override
|
||||||
plugin.getProxy().getPluginManager().registerListener(plugin, this);
|
public void onDisable(Plugin plugin) {
|
||||||
} catch (Throwable ignored) {}
|
saveSchedules();
|
||||||
|
scheduledByClientId.clear();
|
||||||
plugin.getLogger().info("[BroadcastModule] aktiviert. Format: " + format);
|
}
|
||||||
loadSchedules();
|
|
||||||
plugin.getProxy().getScheduler().schedule(plugin, this::processScheduled, 1, 1, TimeUnit.SECONDS);
|
private void loadConfig() {
|
||||||
}
|
File file = new File(plugin.getDataFolder(), "verify.properties");
|
||||||
|
if (!file.exists()) return;
|
||||||
@Override
|
try (InputStream in = new FileInputStream(file)) {
|
||||||
public void onDisable(Plugin plugin) {
|
Properties props = new Properties();
|
||||||
plugin.getLogger().info("[BroadcastModule] deaktiviert.");
|
props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
|
||||||
saveSchedules();
|
enabled = Boolean.parseBoolean(props.getProperty("broadcast.enabled", "true"));
|
||||||
scheduledByClientId.clear();
|
requiredApiKey = props.getProperty("broadcast.api_key", "").trim();
|
||||||
}
|
format = props.getProperty("broadcast.format", format).trim();
|
||||||
|
if (format.isEmpty()) format = "%prefix% %message%";
|
||||||
private void loadConfig() {
|
fallbackPrefix = props.getProperty("broadcast.prefix", fallbackPrefix).trim();
|
||||||
File file = new File(plugin.getDataFolder(), "verify.properties");
|
fallbackPrefixColor = props.getProperty("broadcast.prefix-color", fallbackPrefixColor).trim();
|
||||||
if (!file.exists()) {
|
fallbackBracketColor = props.getProperty("broadcast.bracket-color", fallbackBracketColor).trim();
|
||||||
enabled = true;
|
fallbackMessageColor = props.getProperty("broadcast.message-color", fallbackMessageColor).trim();
|
||||||
requiredApiKey = "";
|
} catch (IOException e) {
|
||||||
format = "%prefix% %message%";
|
plugin.getLogger().warning("[BroadcastModule] Fehler beim Laden von verify.properties: " + e.getMessage());
|
||||||
fallbackPrefix = "[Broadcast]";
|
}
|
||||||
fallbackPrefixColor = "&c";
|
}
|
||||||
fallbackBracketColor = "&8"; // Neu
|
|
||||||
fallbackMessageColor = "&f";
|
public boolean handleBroadcast(String sourceName, String message, String type, String apiKeyHeader,
|
||||||
return;
|
String prefix, String prefixColor, String bracketColor, String messageColor) {
|
||||||
}
|
loadConfig();
|
||||||
|
if (!enabled) return false;
|
||||||
try (InputStream in = new FileInputStream(file)) {
|
if (requiredApiKey != null && !requiredApiKey.isEmpty()) {
|
||||||
Properties props = new Properties();
|
if (apiKeyHeader == null || !requiredApiKey.equals(apiKeyHeader)) {
|
||||||
props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
|
plugin.getLogger().warning("[BroadcastModule] Broadcast abgelehnt: API-Key fehlt oder inkorrekt.");
|
||||||
enabled = Boolean.parseBoolean(props.getProperty("broadcast.enabled", "true"));
|
return false;
|
||||||
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();
|
if (message == null) message = "";
|
||||||
fallbackPrefixColor = props.getProperty("broadcast.prefix-color", fallbackPrefixColor).trim();
|
if (sourceName == null || sourceName.isEmpty()) sourceName = "System";
|
||||||
fallbackBracketColor = props.getProperty("broadcast.bracket-color", fallbackBracketColor).trim(); // Neu
|
if (type == null) type = "global";
|
||||||
fallbackMessageColor = props.getProperty("broadcast.message-color", fallbackMessageColor).trim();
|
|
||||||
} catch (IOException e) {
|
String usedPrefix = (prefix != null && !prefix.trim().isEmpty()) ? prefix.trim() : fallbackPrefix;
|
||||||
plugin.getLogger().warning("[BroadcastModule] Fehler beim Laden von verify.properties: " + e.getMessage());
|
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;
|
||||||
|
|
||||||
public boolean handleBroadcast(String sourceName, String message, String type, String apiKeyHeader,
|
String prefixColorCode = normalizeColorCode(usedPrefixColor);
|
||||||
String prefix, String prefixColor, String bracketColor, String messageColor) {
|
String bracketColorCode = normalizeColorCode(usedBracketColor);
|
||||||
loadConfig();
|
String messageColorCode = normalizeColorCode(usedMessageColor);
|
||||||
|
|
||||||
if (!enabled) {
|
String finalPrefix;
|
||||||
plugin.getLogger().info("[BroadcastModule] Broadcast abgelehnt: Modul ist deaktiviert.");
|
if (!bracketColorCode.isEmpty()) {
|
||||||
return false;
|
String textContent = usedPrefix;
|
||||||
}
|
if (textContent.startsWith("[")) textContent = textContent.substring(1);
|
||||||
|
if (textContent.endsWith("]")) textContent = textContent.substring(0, textContent.length() - 1);
|
||||||
if (requiredApiKey != null && !requiredApiKey.isEmpty()) {
|
finalPrefix = bracketColorCode + "[" + prefixColorCode + textContent + bracketColorCode + "]" + ChatColor.RESET;
|
||||||
if (apiKeyHeader == null || !requiredApiKey.equals(apiKeyHeader)) {
|
} else {
|
||||||
plugin.getLogger().warning("[BroadcastModule] Broadcast abgelehnt: API-Key fehlt oder inkorrekt.");
|
finalPrefix = prefixColorCode + usedPrefix + ChatColor.RESET;
|
||||||
return false;
|
}
|
||||||
}
|
|
||||||
}
|
String coloredMessage = (messageColorCode.isEmpty() ? "" : messageColorCode) + message;
|
||||||
|
String out = format
|
||||||
if (message == null) message = "";
|
.replace("%name%", sourceName)
|
||||||
if (sourceName == null || sourceName.isEmpty()) sourceName = "System";
|
.replace("%prefix%", finalPrefix)
|
||||||
if (type == null) type = "global";
|
.replace("%prefixColored%", finalPrefix)
|
||||||
|
.replace("%message%", message)
|
||||||
String usedPrefix = (prefix != null && !prefix.trim().isEmpty()) ? prefix.trim() : fallbackPrefix;
|
.replace("%messageColored%",coloredMessage)
|
||||||
String usedPrefixColor = (prefixColor != null && !prefixColor.trim().isEmpty()) ? prefixColor.trim() : fallbackPrefixColor;
|
.replace("%type%", type);
|
||||||
String usedBracketColor = (bracketColor != null && !bracketColor.trim().isEmpty()) ? bracketColor.trim() : fallbackBracketColor; // Neu
|
|
||||||
String usedMessageColor = (messageColor != null && !messageColor.trim().isEmpty()) ? messageColor.trim() : fallbackMessageColor;
|
TextComponent tc = new TextComponent(out);
|
||||||
|
int sent = 0;
|
||||||
String prefixColorCode = normalizeColorCode(usedPrefixColor);
|
for (ProxiedPlayer p : plugin.getProxy().getPlayers()) {
|
||||||
String bracketColorCode = normalizeColorCode(usedBracketColor); // Neu
|
try { p.sendMessage(tc); sent++; } catch (Throwable ignored) {}
|
||||||
String messageColorCode = normalizeColorCode(usedMessageColor);
|
}
|
||||||
|
plugin.getLogger().info("[BroadcastModule] Broadcast gesendet (Empfänger=" + sent + "): " + message);
|
||||||
// --- KLAMMER LOGIK ---
|
return true;
|
||||||
String finalPrefix;
|
}
|
||||||
|
|
||||||
// Wenn eine Klammerfarbe gesetzt ist, bauen wir den Prefix neu zusammen
|
private String normalizeColorCode(String code) {
|
||||||
// Format: [BracketColor][ [PrefixColor]Text [BracketColor]]
|
if (code == null) return "";
|
||||||
if (!bracketColorCode.isEmpty()) {
|
code = code.trim();
|
||||||
String textContent = usedPrefix;
|
if (code.isEmpty()) return "";
|
||||||
// Entferne manuelle Klammern, falls der User [Broadcast] in das Textfeld geschrieben hat
|
return code.contains("&") ? ChatColor.translateAlternateColorCodes('&', code) : code;
|
||||||
if (textContent.startsWith("[")) textContent = textContent.substring(1);
|
}
|
||||||
if (textContent.endsWith("]")) textContent = textContent.substring(0, textContent.length() - 1);
|
|
||||||
|
private void saveSchedules() {
|
||||||
finalPrefix = bracketColorCode + "[" + prefixColorCode + textContent + bracketColorCode + "]" + ChatColor.RESET;
|
Properties props = new Properties();
|
||||||
} else {
|
for (Map.Entry<String, ScheduledBroadcast> entry : scheduledByClientId.entrySet()) {
|
||||||
// Altes Verhalten: Ganzen String einfärben
|
String id = entry.getKey();
|
||||||
finalPrefix = prefixColorCode + usedPrefix + ChatColor.RESET;
|
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);
|
||||||
String coloredMessage = (messageColorCode.isEmpty() ? "" : messageColorCode) + message;
|
props.setProperty(id + ".message", sb.message);
|
||||||
|
props.setProperty(id + ".type", sb.type);
|
||||||
String out = format.replace("%name%", sourceName)
|
props.setProperty(id + ".prefix", sb.prefix);
|
||||||
.replace("%prefix%", finalPrefix) // Neu verwendete Variable
|
props.setProperty(id + ".prefixColor", sb.prefixColor);
|
||||||
.replace("%prefixColored%", finalPrefix) // Fallback
|
props.setProperty(id + ".bracketColor", sb.bracketColor);
|
||||||
.replace("%message%", message)
|
props.setProperty(id + ".messageColor", sb.messageColor);
|
||||||
.replace("%messageColored%", coloredMessage)
|
props.setProperty(id + ".recur", sb.recur);
|
||||||
.replace("%type%", type);
|
}
|
||||||
|
try (OutputStream out = new FileOutputStream(schedulesFile)) {
|
||||||
if (!out.contains("%prefixColored%") && !out.contains("%messageColored%") && !out.contains("%prefix%") && !out.contains("%message%")) {
|
props.store(out, "PulseCast Scheduled Broadcasts");
|
||||||
out = finalPrefix + " " + coloredMessage;
|
} catch (IOException e) {
|
||||||
}
|
plugin.getLogger().severe("[BroadcastModule] Konnte Schedules nicht speichern: " + e.getMessage());
|
||||||
|
}
|
||||||
TextComponent tc = new TextComponent(out);
|
}
|
||||||
int sent = 0;
|
|
||||||
for (ProxiedPlayer p : plugin.getProxy().getPlayers()) {
|
/**
|
||||||
try { p.sendMessage(tc); sent++; } catch (Throwable ignored) {}
|
* 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.
|
||||||
plugin.getLogger().info("[BroadcastModule] Broadcast gesendet (Empfänger=" + sent + "): " + message);
|
*
|
||||||
return true;
|
* Bekannte Felder: nextRunMillis, sourceName, message, type, prefix, prefixColor,
|
||||||
}
|
* bracketColor, messageColor, recur → alle ohne Punkte im Namen.
|
||||||
|
*/
|
||||||
private String normalizeColorCode(String code) {
|
private void loadSchedules() {
|
||||||
if (code == null) return "";
|
if (!schedulesFile.exists()) return;
|
||||||
code = code.trim();
|
Properties props = new Properties();
|
||||||
if (code.isEmpty()) return "";
|
try (InputStream in = new FileInputStream(schedulesFile)) {
|
||||||
return code.contains("&") ? ChatColor.translateAlternateColorCodes('&', code) : code;
|
props.load(in);
|
||||||
}
|
} catch (IOException e) {
|
||||||
|
plugin.getLogger().severe("[BroadcastModule] Konnte Schedules nicht laden: " + e.getMessage());
|
||||||
private void saveSchedules() {
|
return;
|
||||||
Properties props = new Properties();
|
}
|
||||||
for (Map.Entry<String, ScheduledBroadcast> entry : scheduledByClientId.entrySet()) {
|
|
||||||
String id = entry.getKey();
|
// Bekannte Feld-Suffixe
|
||||||
ScheduledBroadcast sb = entry.getValue();
|
Set<String> knownFields = new HashSet<>(Arrays.asList(
|
||||||
props.setProperty(id + ".nextRunMillis", String.valueOf(sb.nextRunMillis));
|
"nextRunMillis", "sourceName", "message", "type",
|
||||||
props.setProperty(id + ".sourceName", sb.sourceName);
|
"prefix", "prefixColor", "bracketColor", "messageColor", "recur"
|
||||||
props.setProperty(id + ".message", sb.message);
|
));
|
||||||
props.setProperty(id + ".type", sb.type);
|
|
||||||
props.setProperty(id + ".prefix", sb.prefix);
|
Map<String, ScheduledBroadcast> loaded = new LinkedHashMap<>();
|
||||||
props.setProperty(id + ".prefixColor", sb.prefixColor);
|
for (String key : props.stringPropertyNames()) {
|
||||||
props.setProperty(id + ".bracketColor", sb.bracketColor); // Neu
|
// Finde das letzte '.' das einen bekannten Feldnamen abtrennt
|
||||||
props.setProperty(id + ".messageColor", sb.messageColor);
|
int lastDot = key.lastIndexOf('.');
|
||||||
props.setProperty(id + ".recur", sb.recur);
|
if (lastDot < 0) continue;
|
||||||
}
|
String field = key.substring(lastDot + 1);
|
||||||
|
if (!knownFields.contains(field)) continue;
|
||||||
try (OutputStream out = new FileOutputStream(schedulesFile)) {
|
String id = key.substring(0, lastDot);
|
||||||
props.store(out, "PulseCast Scheduled Broadcasts");
|
if (id.isEmpty()) continue;
|
||||||
} catch (IOException e) {
|
String value = props.getProperty(key);
|
||||||
plugin.getLogger().severe("[BroadcastModule] Konnte Schedules nicht speichern: " + e.getMessage());
|
|
||||||
}
|
ScheduledBroadcast sb = loaded.computeIfAbsent(id,
|
||||||
}
|
k -> new ScheduledBroadcast(k, 0, "", "", "", "", "", "", "", ""));
|
||||||
|
|
||||||
private void loadSchedules() {
|
switch (field) {
|
||||||
if (!schedulesFile.exists()) {
|
case "nextRunMillis": try { sb.nextRunMillis = Long.parseLong(value); } catch (NumberFormatException ignored) {} break;
|
||||||
plugin.getLogger().info("[BroadcastModule] Keine bestehenden Schedules gefunden (Neustart).");
|
case "sourceName": sb.sourceName = value; break;
|
||||||
return;
|
case "message": sb.message = value; break;
|
||||||
}
|
case "type": sb.type = value; break;
|
||||||
|
case "prefix": sb.prefix = value; break;
|
||||||
Properties props = new Properties();
|
case "prefixColor": sb.prefixColor = value; break;
|
||||||
try (InputStream in = new FileInputStream(schedulesFile)) {
|
case "bracketColor": sb.bracketColor = value; break;
|
||||||
props.load(in);
|
case "messageColor": sb.messageColor = value; break;
|
||||||
} catch (IOException e) {
|
case "recur": sb.recur = value; break;
|
||||||
plugin.getLogger().severe("[BroadcastModule] Konnte Schedules nicht laden: " + e.getMessage());
|
}
|
||||||
return;
|
}
|
||||||
}
|
scheduledByClientId.putAll(loaded);
|
||||||
|
plugin.getLogger().info("[BroadcastModule] " + loaded.size() + " geplante Broadcasts aus Datei wiederhergestellt.");
|
||||||
Map<String, ScheduledBroadcast> loaded = new HashMap<>();
|
}
|
||||||
for (String key : props.stringPropertyNames()) {
|
|
||||||
if (!key.contains(".")) continue;
|
public boolean scheduleBroadcast(long timestampMillis, String sourceName, String message, String type,
|
||||||
String[] parts = key.split("\\.");
|
String apiKeyHeader, String prefix, String prefixColor, String bracketColor,
|
||||||
if (parts.length != 2) continue;
|
String messageColor, String recur, String clientScheduleId) {
|
||||||
|
loadConfig();
|
||||||
String id = parts[0];
|
if (!enabled) return false;
|
||||||
String field = parts[1];
|
if (requiredApiKey != null && !requiredApiKey.isEmpty()) {
|
||||||
String value = props.getProperty(key);
|
if (apiKeyHeader == null || !requiredApiKey.equals(apiKeyHeader)) {
|
||||||
|
plugin.getLogger().warning("[BroadcastModule] schedule abgelehnt: API-Key fehlt oder inkorrekt.");
|
||||||
ScheduledBroadcast sb = loaded.get(id);
|
return false;
|
||||||
if (sb == null) {
|
}
|
||||||
sb = new ScheduledBroadcast(id, 0, "", "", "", "", "", "", "", ""); // Ein leerer String mehr für Bracket
|
}
|
||||||
loaded.put(id, sb);
|
if (message == null) message = "";
|
||||||
}
|
if (sourceName == null || sourceName.isEmpty()) sourceName = "System";
|
||||||
|
if (type == null) type = "global";
|
||||||
switch (field) {
|
if (recur == null) recur = "none";
|
||||||
case "nextRunMillis":
|
|
||||||
try { sb.nextRunMillis = Long.parseLong(value); } catch (NumberFormatException ignored) {}
|
String id = (clientScheduleId != null && !clientScheduleId.trim().isEmpty())
|
||||||
break;
|
? clientScheduleId.trim() : UUID.randomUUID().toString();
|
||||||
case "sourceName": sb.sourceName = value; break;
|
|
||||||
case "message": sb.message = value; break;
|
long now = System.currentTimeMillis();
|
||||||
case "type": sb.type = value; break;
|
if (timestampMillis <= now) {
|
||||||
case "prefix": sb.prefix = value; break;
|
plugin.getLogger().warning("[BroadcastModule] Geplante Zeit in der Vergangenheit → sende sofort!");
|
||||||
case "prefixColor": sb.prefixColor = value; break;
|
return handleBroadcast(sourceName, message, type, apiKeyHeader, prefix, prefixColor, bracketColor, messageColor);
|
||||||
case "bracketColor": sb.bracketColor = value; break; // Neu
|
}
|
||||||
case "messageColor": sb.messageColor = value; break;
|
|
||||||
case "recur": sb.recur = value; break;
|
ScheduledBroadcast sb = new ScheduledBroadcast(id, timestampMillis, sourceName, message, type,
|
||||||
}
|
prefix, prefixColor, bracketColor, messageColor, recur);
|
||||||
}
|
scheduledByClientId.put(id, sb);
|
||||||
scheduledByClientId.putAll(loaded);
|
saveSchedules();
|
||||||
plugin.getLogger().info("[BroadcastModule] " + loaded.size() + " geplante Broadcasts aus Datei wiederhergestellt.");
|
plugin.getLogger().info("[BroadcastModule] Neue geplante Nachricht registriert: " + id
|
||||||
}
|
+ " @ " + dateFormat.format(new Date(timestampMillis)));
|
||||||
|
return true;
|
||||||
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) {
|
public boolean cancelScheduled(String clientScheduleId) {
|
||||||
loadConfig();
|
if (clientScheduleId == null || clientScheduleId.trim().isEmpty()) return false;
|
||||||
|
ScheduledBroadcast removed = scheduledByClientId.remove(clientScheduleId);
|
||||||
if (!enabled) {
|
if (removed != null) { plugin.getLogger().info("[BroadcastModule] Schedule abgebrochen: " + clientScheduleId); saveSchedules(); return true; }
|
||||||
plugin.getLogger().info("[BroadcastModule] schedule abgelehnt: Modul deaktiviert.");
|
return false;
|
||||||
return false;
|
}
|
||||||
}
|
|
||||||
|
private void processScheduled() {
|
||||||
if (requiredApiKey != null && !requiredApiKey.isEmpty()) {
|
if (scheduledByClientId.isEmpty()) return;
|
||||||
if (apiKeyHeader == null || !requiredApiKey.equals(apiKeyHeader)) {
|
long now = System.currentTimeMillis();
|
||||||
plugin.getLogger().warning("[BroadcastModule] schedule abgelehnt: API-Key fehlt oder inkorrekt.");
|
List<String> toRemove = new ArrayList<>();
|
||||||
return false;
|
boolean changed = false;
|
||||||
}
|
|
||||||
}
|
for (Map.Entry<String, ScheduledBroadcast> entry : scheduledByClientId.entrySet()) {
|
||||||
|
ScheduledBroadcast sb = entry.getValue();
|
||||||
if (message == null) message = "";
|
if (sb.nextRunMillis <= now) {
|
||||||
if (sourceName == null || sourceName.isEmpty()) sourceName = "System";
|
plugin.getLogger().info("[BroadcastModule] ⏰ Sende geplante Nachricht (ID: " + entry.getKey() + ")");
|
||||||
if (type == null) type = "global";
|
handleBroadcast(sb.sourceName, sb.message, sb.type, "", sb.prefix, sb.prefixColor, sb.bracketColor, sb.messageColor);
|
||||||
if (recur == null) recur = "none";
|
if (!"none".equalsIgnoreCase(sb.recur)) {
|
||||||
|
long next = computeNextMillis(sb.nextRunMillis, sb.recur);
|
||||||
String id = (clientScheduleId != null && !clientScheduleId.trim().isEmpty()) ? clientScheduleId.trim() : UUID.randomUUID().toString();
|
if (next > 0) { sb.nextRunMillis = next; changed = true; }
|
||||||
|
else { toRemove.add(entry.getKey()); changed = true; }
|
||||||
long now = System.currentTimeMillis();
|
} else { toRemove.add(entry.getKey()); changed = true; }
|
||||||
String scheduledTimeStr = dateFormat.format(new Date(timestampMillis));
|
}
|
||||||
|
}
|
||||||
plugin.getLogger().info("[BroadcastModule] Neue geplante Nachricht registriert: " + id + " @ " + scheduledTimeStr);
|
if (changed || !toRemove.isEmpty()) {
|
||||||
|
for (String k : toRemove) { scheduledByClientId.remove(k); }
|
||||||
if (timestampMillis <= now) {
|
saveSchedules();
|
||||||
plugin.getLogger().warning("[BroadcastModule] Geplante Zeit liegt in der Vergangenheit -> sende sofort!");
|
}
|
||||||
return handleBroadcast(sourceName, message, type, apiKeyHeader, prefix, prefixColor, bracketColor, messageColor);
|
}
|
||||||
}
|
|
||||||
|
private long computeNextMillis(long currentMillis, String recur) {
|
||||||
ScheduledBroadcast sb = new ScheduledBroadcast(id, timestampMillis, sourceName, message, type, prefix, prefixColor, bracketColor, messageColor, recur);
|
switch (recur.toLowerCase(Locale.ROOT)) {
|
||||||
scheduledByClientId.put(id, sb);
|
case "hourly": return currentMillis + TimeUnit.HOURS.toMillis(1);
|
||||||
saveSchedules();
|
case "daily": return currentMillis + TimeUnit.DAYS.toMillis(1);
|
||||||
return true;
|
case "weekly": return currentMillis + TimeUnit.DAYS.toMillis(7);
|
||||||
}
|
default: return -1L;
|
||||||
|
}
|
||||||
public boolean cancelScheduled(String clientScheduleId) {
|
}
|
||||||
if (clientScheduleId == null || clientScheduleId.trim().isEmpty()) return false;
|
|
||||||
ScheduledBroadcast removed = scheduledByClientId.remove(clientScheduleId);
|
private static class ScheduledBroadcast {
|
||||||
if (removed != null) {
|
final String clientId;
|
||||||
plugin.getLogger().info("[BroadcastModule] Geplante Nachricht abgebrochen: id=" + clientScheduleId);
|
long nextRunMillis;
|
||||||
saveSchedules();
|
String sourceName, message, type, prefix, prefixColor, bracketColor, messageColor, recur;
|
||||||
return true;
|
|
||||||
}
|
ScheduledBroadcast(String clientId, long nextRunMillis, String sourceName, String message, String type,
|
||||||
return false;
|
String prefix, String prefixColor, String bracketColor, String messageColor, String recur) {
|
||||||
}
|
this.clientId = clientId;
|
||||||
|
this.nextRunMillis = nextRunMillis;
|
||||||
private void processScheduled() {
|
this.sourceName = sourceName;
|
||||||
if (scheduledByClientId.isEmpty()) return;
|
this.message = message;
|
||||||
|
this.type = type;
|
||||||
long now = System.currentTimeMillis();
|
this.prefix = prefix;
|
||||||
List<String> toRemove = new ArrayList<>();
|
this.prefixColor = prefixColor;
|
||||||
boolean changed = false;
|
this.bracketColor = bracketColor;
|
||||||
|
this.messageColor = messageColor;
|
||||||
for (Map.Entry<String, ScheduledBroadcast> entry : scheduledByClientId.entrySet()) {
|
this.recur = recur == null ? "none" : recur;
|
||||||
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 + ")");
|
|
||||||
|
|
||||||
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 (changed || !toRemove.isEmpty()) {
|
|
||||||
for (String k : toRemove) {
|
|
||||||
scheduledByClientId.remove(k);
|
|
||||||
plugin.getLogger().info("[BroadcastModule] Schedule entfernt: " + k);
|
|
||||||
}
|
|
||||||
saveSchedules();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 "weekly": return currentMillis + TimeUnit.DAYS.toMillis(7);
|
|
||||||
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;
|
|
||||||
|
|
||||||
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.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -304,7 +304,6 @@ public class AccountLinkManager {
|
|||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
logger.warning("[ChatModule] Fehler beim Laden der Account-Links: " + e.getMessage());
|
logger.warning("[ChatModule] Fehler beim Laden der Account-Links: " + e.getMessage());
|
||||||
}
|
}
|
||||||
logger.info("[ChatModule] " + links.size() + " Account-Verknüpfungen geladen.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String esc(String s) {
|
private static String esc(String s) {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -134,6 +134,13 @@ public class ChatFilter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 6. Anti-Werbung ──
|
||||||
|
if (cfg.antiAdEnabled && !isAdmin) {
|
||||||
|
if (containsAdvertisement(result)) {
|
||||||
|
return new FilterResponse(FilterResult.BLOCKED, result, cfg.antiAdMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return new FilterResponse(
|
return new FilterResponse(
|
||||||
modified ? FilterResult.MODIFIED : FilterResult.ALLOWED,
|
modified ? FilterResult.MODIFIED : FilterResult.ALLOWED,
|
||||||
result,
|
result,
|
||||||
@@ -187,14 +194,88 @@ public class ChatFilter {
|
|||||||
|
|
||||||
// Kein Recht → & und nächstes Zeichen überspringen
|
// Kein Recht → & und nächstes Zeichen überspringen
|
||||||
if (isColor || isFormat) { i++; continue; }
|
if (isColor || isFormat) { i++; continue; }
|
||||||
// Hex: &# + 6 Zeichen überspringen
|
// Hex: &# + 6 Zeichen überspringen (i zeigt auf &, +1 = #, +2..+7 = RRGGBB)
|
||||||
if (isHex && i + 7 < message.length()) { i += 7; continue; }
|
if (isHex && i + 7 <= message.length()) { i += 7; continue; }
|
||||||
}
|
}
|
||||||
sb.append(c);
|
sb.append(c);
|
||||||
}
|
}
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Anti-Werbung =====
|
||||||
|
|
||||||
|
// Vorkompilierte Patterns (einmalig beim Classload)
|
||||||
|
private static final Pattern PATTERN_IP =
|
||||||
|
Pattern.compile("\\b(\\d{1,3}[.,]){3}\\d{1,3}(:\\d{1,5})?\\b");
|
||||||
|
|
||||||
|
private static final Pattern PATTERN_DOMAIN_GENERIC =
|
||||||
|
Pattern.compile("(?i)\\b[a-z0-9-]{2,63}\\.[a-z]{2,10}(?:[/:\\d]\\S*)?\\b");
|
||||||
|
|
||||||
|
private static final Pattern PATTERN_URL_PREFIX =
|
||||||
|
Pattern.compile("(?i)(https?://|www\\.)\\S+");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ob die Nachricht Werbung enthält (IP, URL, fremde Domain).
|
||||||
|
* Domains auf der Whitelist werden ignoriert.
|
||||||
|
*
|
||||||
|
* Erkennt:
|
||||||
|
* - http:// / https:// / www. Prefixe
|
||||||
|
* - IPv4-Adressen (auch mit Port)
|
||||||
|
* - Domain-Namen mit konfigurierten TLDs (z.B. .net, .de, .com)
|
||||||
|
* - Verschleierungsversuche mit Leerzeichen um Punkte ("play . server . net")
|
||||||
|
*/
|
||||||
|
private boolean containsAdvertisement(String message) {
|
||||||
|
// Normalisierung: "play . server . net" → "play.server.net"
|
||||||
|
String normalized = message.replaceAll("\\s*\\.\\s*", ".");
|
||||||
|
|
||||||
|
// 1. Explizite URL-Prefixe
|
||||||
|
if (PATTERN_URL_PREFIX.matcher(normalized).find()) {
|
||||||
|
return !allMatchesWhitelisted(normalized, PATTERN_URL_PREFIX);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. IP-Adressen (werden nie whitelisted)
|
||||||
|
if (PATTERN_IP.matcher(normalized).find()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Domains mit bekannten TLDs
|
||||||
|
if (!cfg.antiAdBlockedTlds.isEmpty()) {
|
||||||
|
java.util.regex.Matcher m = PATTERN_DOMAIN_GENERIC.matcher(normalized);
|
||||||
|
while (m.find()) {
|
||||||
|
String match = m.group();
|
||||||
|
String tld = extractTld(match);
|
||||||
|
if (cfg.antiAdBlockedTlds.contains(tld.toLowerCase())) {
|
||||||
|
if (!isOnWhitelist(match)) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** true wenn ALLE Treffer des Patterns auf der Whitelist stehen. */
|
||||||
|
private boolean allMatchesWhitelisted(String message, Pattern pattern) {
|
||||||
|
java.util.regex.Matcher m = pattern.matcher(message);
|
||||||
|
while (m.find()) {
|
||||||
|
if (!isOnWhitelist(m.group())) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isOnWhitelist(String match) {
|
||||||
|
String lower = match.toLowerCase();
|
||||||
|
for (String entry : cfg.antiAdWhitelist) {
|
||||||
|
if (lower.contains(entry.toLowerCase())) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String extractTld(String domain) {
|
||||||
|
String clean = domain.split("[/:]")[0];
|
||||||
|
int dot = clean.lastIndexOf('.');
|
||||||
|
return dot >= 0 ? clean.substring(dot + 1) : "";
|
||||||
|
}
|
||||||
|
|
||||||
private static String buildStars(int length) {
|
private static String buildStars(int length) {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
for (int i = 0; i < Math.max(length, 4); i++) sb.append('*');
|
for (int i = 0; i < Math.max(length, 4); i++) sb.append('*');
|
||||||
@@ -235,5 +316,16 @@ public class ChatFilter {
|
|||||||
public boolean capsFilterEnabled = true;
|
public boolean capsFilterEnabled = true;
|
||||||
public int capsMinLength = 6; // Mindestlänge für Caps-Check
|
public int capsMinLength = 6; // Mindestlänge für Caps-Check
|
||||||
public int capsMaxPercent = 70; // Max. % Großbuchstaben
|
public int capsMaxPercent = 70; // Max. % Großbuchstaben
|
||||||
|
|
||||||
|
// Anti-Werbung
|
||||||
|
public boolean antiAdEnabled = true;
|
||||||
|
public String antiAdMessage = "&cWerbung ist in diesem Chat nicht erlaubt!";
|
||||||
|
// Domains/Substrings die NICHT geblockt werden (z.B. eigene Serveradresse)
|
||||||
|
public List<String> antiAdWhitelist = new ArrayList<>();
|
||||||
|
// TLDs die als Werbung gewertet werden (leer = alle TLDs prüfen)
|
||||||
|
public List<String> antiAdBlockedTlds = new ArrayList<>(Arrays.asList(
|
||||||
|
"net", "com", "de", "org", "gg", "io", "eu", "tv", "xyz",
|
||||||
|
"info", "me", "cc", "co", "app", "online", "site", "fun"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,6 +43,8 @@ import java.util.logging.Logger;
|
|||||||
* ✅ Report-System (/report, /reports, /reportclose)
|
* ✅ Report-System (/report, /reports, /reportclose)
|
||||||
* ✅ Admin-Benachrichtigung bei Reports (sofort oder verzögert nach Login)
|
* ✅ Admin-Benachrichtigung bei Reports (sofort oder verzögert nach Login)
|
||||||
* ✅ Server-Farben pro Server (&-Codes und &#RRGGBB HEX)
|
* ✅ Server-Farben pro Server (&-Codes und &#RRGGBB HEX)
|
||||||
|
* ✅ Join / Leave Nachrichten (mit Vanish-Support)
|
||||||
|
* ✅ BungeeCord-Vanish-Integration via VanishProvider
|
||||||
*/
|
*/
|
||||||
public class ChatModule implements Module, Listener {
|
public class ChatModule implements Module, Listener {
|
||||||
|
|
||||||
@@ -74,10 +76,13 @@ public class ChatModule implements Module, Listener {
|
|||||||
private final Map<UUID, Long> helpopCooldowns = new ConcurrentHashMap<>();
|
private final Map<UUID, Long> helpopCooldowns = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
// Report-Cooldown: UUID → letzter Report-Zeitstempel (Sekunden)
|
// Report-Cooldown: UUID → letzter Report-Zeitstempel (Sekunden)
|
||||||
private final Map<UUID, Long> reportCooldowns = new ConcurrentHashMap<>(); // NEU
|
private final Map<UUID, Long> reportCooldowns = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
// Letzte Chatnachricht pro Spieler (für Report-Kontext): name.toLowerCase() → message
|
// Letzte Chatnachricht pro Spieler (für Report-Kontext): name.toLowerCase() → message
|
||||||
private final Map<String, String> lastChatMessages = new ConcurrentHashMap<>(); // NEU
|
private final Map<String, String> lastChatMessages = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// UUIDs die gerade auf Plugin-Chat-Eingabe warten
|
||||||
|
private final Set<UUID> awaitingInput = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||||
|
|
||||||
// Geyser-Präfix für Bedrock-Spieler (Standard: ".")
|
// Geyser-Präfix für Bedrock-Spieler (Standard: ".")
|
||||||
private static final String GEYSER_PREFIX = ".";
|
private static final String GEYSER_PREFIX = ".";
|
||||||
@@ -105,13 +110,13 @@ public class ChatModule implements Module, Listener {
|
|||||||
emojiParser = new EmojiParser(config.getEmojiMappings(), config.isEmojiEnabled());
|
emojiParser = new EmojiParser(config.getEmojiMappings(), config.isEmojiEnabled());
|
||||||
chatFilter = new ChatFilter(config.getFilterConfig());
|
chatFilter = new ChatFilter(config.getFilterConfig());
|
||||||
|
|
||||||
// NEU: ChatLogger
|
// ChatLogger
|
||||||
if (config.isChatlogEnabled()) {
|
if (config.isChatlogEnabled()) {
|
||||||
chatLogger = new ChatLogger(plugin.getDataFolder(), logger, config.getChatlogRetentionDays());
|
chatLogger = new ChatLogger(plugin.getDataFolder(), logger, config.getChatlogRetentionDays());
|
||||||
logger.info("[ChatModule] Chat-Log aktiviert (" + config.getChatlogRetentionDays() + " Tage Aufbewahrung).");
|
logger.info("[ChatModule] Chat-Log aktiviert (" + config.getChatlogRetentionDays() + " Tage Aufbewahrung).");
|
||||||
}
|
}
|
||||||
|
|
||||||
// NEU: ReportManager
|
// ReportManager
|
||||||
if (config.isReportsEnabled()) {
|
if (config.isReportsEnabled()) {
|
||||||
reportManager = new ReportManager(plugin.getDataFolder(), logger);
|
reportManager = new ReportManager(plugin.getDataFolder(), logger);
|
||||||
reportManager.load();
|
reportManager.load();
|
||||||
@@ -155,7 +160,6 @@ public class ChatModule implements Module, Listener {
|
|||||||
helpopCooldowns.clear();
|
helpopCooldowns.clear();
|
||||||
reportCooldowns.clear();
|
reportCooldowns.clear();
|
||||||
lastChatMessages.clear();
|
lastChatMessages.clear();
|
||||||
logger.info("[ChatModule] Deaktiviert.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================
|
// =========================================================
|
||||||
@@ -182,11 +186,6 @@ public class ChatModule implements Module, Listener {
|
|||||||
if (!(e.getSender() instanceof ProxiedPlayer)) return;
|
if (!(e.getSender() instanceof ProxiedPlayer)) return;
|
||||||
ProxiedPlayer player = (ProxiedPlayer) e.getSender();
|
ProxiedPlayer player = (ProxiedPlayer) e.getSender();
|
||||||
|
|
||||||
// Bypass: Spieler wartet auf Plugin-Eingabe (CMI etc.)
|
|
||||||
// Event komplett unberührt lassen → Originalnachricht mit Signatur
|
|
||||||
// geht direkt zum Sub-Server. Funktioniert auf Spigot ohne Einschränkung.
|
|
||||||
// Auf Paper-Sub-Servern muss reject-chat-unsigned: false gesetzt sein —
|
|
||||||
// das ist eine Paper-Limitierung, nicht lösbar auf BungeeCord-Ebene.
|
|
||||||
if (awaitingInput.contains(player.getUniqueId())) {
|
if (awaitingInput.contains(player.getUniqueId())) {
|
||||||
awaitingInput.remove(player.getUniqueId());
|
awaitingInput.remove(player.getUniqueId());
|
||||||
return; // Event NICHT cancellen → Nachricht geht mit Originalsignatur durch
|
return; // Event NICHT cancellen → Nachricht geht mit Originalsignatur durch
|
||||||
@@ -198,7 +197,6 @@ public class ChatModule implements Module, Listener {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Zentrale Chat-Verarbeitungslogik.
|
* Zentrale Chat-Verarbeitungslogik.
|
||||||
* Wird von beiden Event-Handlern aufgerufen.
|
|
||||||
*/
|
*/
|
||||||
private void processChat(ProxiedPlayer player, String rawMessage) {
|
private void processChat(ProxiedPlayer player, String rawMessage) {
|
||||||
if (rawMessage == null || rawMessage.trim().isEmpty()) return;
|
if (rawMessage == null || rawMessage.trim().isEmpty()) return;
|
||||||
@@ -236,7 +234,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
player.sendMessage(color(filterResp.denyReason));
|
player.sendMessage(color(filterResp.denyReason));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
message = filterResp.message; // ggf. modifiziert (Caps, Blacklist)
|
message = filterResp.message;
|
||||||
|
|
||||||
String serverName = player.getServer() != null
|
String serverName = player.getServer() != null
|
||||||
? player.getServer().getInfo().getName()
|
? player.getServer().getInfo().getName()
|
||||||
@@ -259,7 +257,6 @@ public class ChatModule implements Module, Listener {
|
|||||||
ProxiedPlayer mentioned = ProxyServer.getInstance().getPlayer(targetName);
|
ProxiedPlayer mentioned = ProxyServer.getInstance().getPlayer(targetName);
|
||||||
if (mentioned != null && !mentioned.getUniqueId().equals(uuid)) {
|
if (mentioned != null && !mentioned.getUniqueId().equals(uuid)) {
|
||||||
mentionedPlayers.add(mentioned.getUniqueId());
|
mentionedPlayers.add(mentioned.getUniqueId());
|
||||||
// Wort hervorheben
|
|
||||||
word = translateColors(highlightColor + word + "&r");
|
word = translateColors(highlightColor + word + "&r");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -316,10 +313,8 @@ public class ChatModule implements Module, Listener {
|
|||||||
&& !mentionsDisabled.contains(recipient.getUniqueId());
|
&& !mentionsDisabled.contains(recipient.getUniqueId());
|
||||||
|
|
||||||
if (isMentioned) {
|
if (isMentioned) {
|
||||||
// Prefix-Nachricht über der Chat-Zeile
|
|
||||||
recipient.sendMessage(color(config.getMentionsNotifyPrefix()
|
recipient.sendMessage(color(config.getMentionsNotifyPrefix()
|
||||||
+ "&7" + finalSenderName + " &7hat dich erwähnt!"));
|
+ "&7" + finalSenderName + " &7hat dich erwähnt!"));
|
||||||
// Sound via Plugin-Messaging an Sub-Server senden
|
|
||||||
sendMentionSound(recipient, config.getMentionsSound());
|
sendMentionSound(recipient, config.getMentionsSound());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,44 +330,147 @@ public class ChatModule implements Module, Listener {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler
|
|
||||||
public void onDisconnect(PlayerDisconnectEvent e) {
|
|
||||||
UUID uuid = e.getPlayer().getUniqueId();
|
|
||||||
chatFilter.cleanup(uuid);
|
|
||||||
playerChannels.remove(uuid);
|
|
||||||
mentionsDisabled.remove(uuid);
|
|
||||||
awaitingInput.remove(uuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================================================
|
// =========================================================
|
||||||
// LOGIN-EVENT: Kanal setzen + Report-Benachrichtigung
|
// LOGIN-EVENT: Kanal setzen + Join-Nachricht + Report-Info
|
||||||
// =========================================================
|
// =========================================================
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
public void onLogin(PostLoginEvent e) {
|
public void onLogin(PostLoginEvent e) {
|
||||||
ProxiedPlayer player = e.getPlayer();
|
ProxiedPlayer player = e.getPlayer();
|
||||||
playerChannels.put(player.getUniqueId(), config.getDefaultChannelId());
|
UUID uuid = player.getUniqueId();
|
||||||
|
|
||||||
// NEU: Offene Reports nach 2 Sekunden anzeigen (damit Update-Meldungen nicht überlagert werden)
|
// Standard-Kanal setzen
|
||||||
if (reportManager == null) return;
|
playerChannels.put(uuid, config.getDefaultChannelId());
|
||||||
if (!player.hasPermission(config.getAdminNotifyPermission())
|
|
||||||
&& !player.hasPermission(config.getAdminBypassPermission())) return;
|
|
||||||
|
|
||||||
int openCount = reportManager.getOpenCount();
|
|
||||||
if (openCount == 0) return;
|
|
||||||
|
|
||||||
|
// Join-Nachricht und Report-Benachrichtigung mit kurzem Delay senden,
|
||||||
|
// damit alle anderen Proxy-Initialisierungen (inkl. VanishModule) abgeschlossen sind.
|
||||||
|
// 2s statt 1s: VanishModule markiert den Spieler oft erst beim ServerConnectedEvent.
|
||||||
plugin.getProxy().getScheduler().schedule(plugin, () -> {
|
plugin.getProxy().getScheduler().schedule(plugin, () -> {
|
||||||
if (!player.isConnected()) return;
|
if (!player.isConnected()) return;
|
||||||
int count = reportManager.getOpenCount();
|
|
||||||
if (count == 0) return;
|
|
||||||
|
|
||||||
player.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
// ── Join-Nachricht ──
|
||||||
player.sendMessage(color("&c&l⚑ OFFENE REPORTS &8| &f" + count + " ausstehend"));
|
if (config.isJoinLeaveEnabled()) {
|
||||||
player.sendMessage(color("&7Tippe &f/reports &7für eine Übersicht."));
|
broadcastJoinLeave(player, true);
|
||||||
player.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
}
|
||||||
|
|
||||||
|
// ── 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);
|
}, 2, TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// DISCONNECT-EVENT: Cleanup + Leave-Nachricht
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onDisconnect(PlayerDisconnectEvent e) {
|
||||||
|
ProxiedPlayer player = e.getPlayer();
|
||||||
|
UUID uuid = player.getUniqueId();
|
||||||
|
|
||||||
|
// Leave-Nachricht
|
||||||
|
if (config.isJoinLeaveEnabled()) {
|
||||||
|
broadcastJoinLeave(player, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
chatFilter.cleanup(uuid);
|
||||||
|
playerChannels.remove(uuid);
|
||||||
|
mentionsDisabled.remove(uuid);
|
||||||
|
awaitingInput.remove(uuid);
|
||||||
|
VanishProvider.cleanup(uuid); // Vanish-Status bereinigen
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// JOIN / LEAVE NACHRICHTEN (mit Vanish-Support)
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sendet eine Join- oder Leave-Nachricht an alle Spieler.
|
||||||
|
*
|
||||||
|
* Vanish-Logik:
|
||||||
|
* - Unsichtbare Spieler: kein Broadcast an normale Spieler.
|
||||||
|
* - Ist vanish-show-to-admins=true, erhalten Admins (bypass-permission)
|
||||||
|
* eine dezente Vanish-Benachrichtigung.
|
||||||
|
*
|
||||||
|
* @param player Der betroffene Spieler
|
||||||
|
* @param isJoin true = Join, false = Leave
|
||||||
|
*/
|
||||||
|
private void broadcastJoinLeave(ProxiedPlayer player, boolean isJoin) {
|
||||||
|
boolean isVanished = VanishProvider.isVanished(player);
|
||||||
|
|
||||||
|
String prefix = getLuckPermsPrefix(player);
|
||||||
|
String suffix = getLuckPermsSuffix(player);
|
||||||
|
String server = (player.getServer() != null)
|
||||||
|
? config.getServerDisplay(player.getServer().getInfo().getName())
|
||||||
|
: "Netzwerk";
|
||||||
|
|
||||||
|
String normalFormat = isJoin ? config.getJoinFormat() : config.getLeaveFormat();
|
||||||
|
String vanishFormat = isJoin ? config.getVanishJoinFormat() : config.getVanishLeaveFormat();
|
||||||
|
|
||||||
|
// Platzhalter ersetzen
|
||||||
|
String normalMsg = normalFormat
|
||||||
|
.replace("{player}", player.getName())
|
||||||
|
.replace("{prefix}", prefix != null ? prefix : "")
|
||||||
|
.replace("{suffix}", suffix != null ? suffix : "")
|
||||||
|
.replace("{server}", server);
|
||||||
|
|
||||||
|
String vanishMsg = vanishFormat
|
||||||
|
.replace("{player}", player.getName())
|
||||||
|
.replace("{prefix}", prefix != null ? prefix : "")
|
||||||
|
.replace("{suffix}", suffix != null ? suffix : "")
|
||||||
|
.replace("{server}", server);
|
||||||
|
|
||||||
|
for (ProxiedPlayer recipient : ProxyServer.getInstance().getPlayers()) {
|
||||||
|
// Spieler sieht seine eigene Join-/Leave-Meldung nie
|
||||||
|
if (recipient.getUniqueId().equals(player.getUniqueId())) continue;
|
||||||
|
|
||||||
|
// Admin = bypass-permission ODER notify-permission
|
||||||
|
boolean recipientIsAdmin = recipient.hasPermission(config.getAdminBypassPermission())
|
||||||
|
|| recipient.hasPermission(config.getAdminNotifyPermission());
|
||||||
|
|
||||||
|
if (isVanished) {
|
||||||
|
// Vanished: Nur Admins sehen eine dezente Meldung (wenn konfiguriert)
|
||||||
|
if (recipientIsAdmin && config.isVanishShowToAdmins()) {
|
||||||
|
recipient.sendMessage(color(vanishMsg));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Normaler Spieler: alle erhalten die Nachricht
|
||||||
|
// Vanished Admins sehen Join/Leave-Events anderer Spieler normal
|
||||||
|
recipient.sendMessage(color(normalMsg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Konsole immer informieren
|
||||||
|
String logMsg = isVanished
|
||||||
|
? "[ChatModule] " + (isJoin ? "JOIN(V)" : "LEAVE(V)") + " " + player.getName()
|
||||||
|
: "[ChatModule] " + (isJoin ? "JOIN" : "LEAVE") + " " + player.getName();
|
||||||
|
ProxyServer.getInstance().getConsole().sendMessage(color(
|
||||||
|
isVanished ? "&8" + logMsg : "&7" + logMsg));
|
||||||
|
|
||||||
|
// Brücken (nur für nicht-vanished Spieler)
|
||||||
|
if (!isVanished) {
|
||||||
|
String cleanMsg = ChatColor.stripColor(ChatColor.translateAlternateColorCodes('&', normalMsg));
|
||||||
|
if (discordBridge != null && !config.getJoinLeaveDiscordWebhook().isEmpty()) {
|
||||||
|
discordBridge.sendToDiscord(config.getJoinLeaveDiscordWebhook(),
|
||||||
|
player.getName(), cleanMsg, null);
|
||||||
|
}
|
||||||
|
if (telegramBridge != null && !config.getJoinLeaveTelegramChatId().isEmpty()) {
|
||||||
|
telegramBridge.sendToTelegram(config.getJoinLeaveTelegramChatId(),
|
||||||
|
config.getJoinLeaveTelegramThreadId(), cleanMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================================
|
// =========================================================
|
||||||
// BEFEHLE REGISTRIEREN
|
// BEFEHLE REGISTRIEREN
|
||||||
// =========================================================
|
// =========================================================
|
||||||
@@ -452,17 +550,24 @@ public class ChatModule implements Module, Listener {
|
|||||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, helpop);
|
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, helpop);
|
||||||
|
|
||||||
// /msg <spieler> <nachricht>
|
// /msg <spieler> <nachricht>
|
||||||
|
// Vanish: Vanished Spieler sind für normale Spieler nicht erreichbar.
|
||||||
|
// Admins können vanished Spieler per PM kontaktieren.
|
||||||
Command msgCmd = new Command("msg", null, "tell", "w", "whisper") {
|
Command msgCmd = new Command("msg", null, "tell", "w", "whisper") {
|
||||||
@Override
|
@Override
|
||||||
public void execute(CommandSender sender, String[] args) {
|
public void execute(CommandSender sender, String[] args) {
|
||||||
if (!config.isPmEnabled()) { sender.sendMessage(color("&cPrivat-Nachrichten sind deaktiviert.")); return; }
|
if (!config.isPmEnabled()) { sender.sendMessage(color("&cPrivat-Nachrichten sind deaktiviert.")); return; }
|
||||||
if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(color("&cNur Spieler!")); return; }
|
if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(color("&cNur Spieler!")); return; }
|
||||||
if (args.length < 2) { sender.sendMessage(color("&cBenutzung: /msg <Spieler> <Nachricht>")); return; }
|
if (args.length < 2) { sender.sendMessage(color("&cBenutzung: /msg <Spieler> <Nachricht>")); return; }
|
||||||
|
|
||||||
ProxiedPlayer from = (ProxiedPlayer) sender;
|
ProxiedPlayer from = (ProxiedPlayer) sender;
|
||||||
ProxiedPlayer to = ProxyServer.getInstance().getPlayer(args[0]);
|
boolean fromIsAdmin = from.hasPermission(config.getAdminBypassPermission());
|
||||||
|
|
||||||
|
// Ziel suchen – vanished Spieler sind für Nicht-Admins "nicht gefunden"
|
||||||
|
ProxiedPlayer to = findVisiblePlayer(args[0], fromIsAdmin);
|
||||||
if (to == null || !to.isConnected()) {
|
if (to == null || !to.isConnected()) {
|
||||||
from.sendMessage(color("&cSpieler &f" + args[0] + " &cnicht gefunden.")); return;
|
from.sendMessage(color("&cSpieler &f" + args[0] + " &cnicht gefunden.")); return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String message = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
|
String message = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
|
||||||
pmManager.send(from, to, message, config, config.getAdminBypassPermission());
|
pmManager.send(from, to, message, config, config.getAdminBypassPermission());
|
||||||
}
|
}
|
||||||
@@ -652,27 +757,24 @@ public class ChatModule implements Module, Listener {
|
|||||||
UUID tUUID = target.getUniqueId();
|
UUID tUUID = target.getUniqueId();
|
||||||
String tServer = target.getServer() != null ? target.getServer().getInfo().getName() : "Proxy";
|
String tServer = target.getServer() != null ? target.getServer().getInfo().getName() : "Proxy";
|
||||||
|
|
||||||
// Kanal
|
|
||||||
String channelId = playerChannels.getOrDefault(tUUID, config.getDefaultChannelId());
|
String channelId = playerChannels.getOrDefault(tUUID, config.getDefaultChannelId());
|
||||||
ChatChannel ch = config.getChannel(channelId);
|
ChatChannel ch = config.getChannel(channelId);
|
||||||
String channelName = ch != null ? ch.getFormattedTag() + " &f" + ch.getName() : "&f" + channelId;
|
String channelName = ch != null ? ch.getFormattedTag() + " &f" + ch.getName() : "&f" + channelId;
|
||||||
|
|
||||||
// Mute-Status
|
|
||||||
String muteStatus = muteManager.isMuted(tUUID)
|
String muteStatus = muteManager.isMuted(tUUID)
|
||||||
? "&cJa &8(noch: &f" + muteManager.getRemainingTime(tUUID) + "&8)"
|
? "&cJa &8(noch: &f" + muteManager.getRemainingTime(tUUID) + "&8)"
|
||||||
: "&aKein";
|
: "&aKein";
|
||||||
|
|
||||||
// Blockierungen
|
|
||||||
Set<UUID> blocked = blockManager.getBlockedBy(tUUID);
|
Set<UUID> blocked = blockManager.getBlockedBy(tUUID);
|
||||||
|
|
||||||
// Account-Links
|
|
||||||
AccountLinkManager.LinkedAccount link = linkManager.getByUUID(tUUID);
|
AccountLinkManager.LinkedAccount link = linkManager.getByUUID(tUUID);
|
||||||
String discordInfo = (link != null && !link.discordUserId.isEmpty())
|
String discordInfo = (link != null && !link.discordUserId.isEmpty())
|
||||||
? "&a" + link.discordUsername + " &8(" + link.discordUserId + ")" : "&7Nicht verknüpft";
|
? "&a" + link.discordUsername + " &8(" + link.discordUserId + ")" : "&7Nicht verknüpft";
|
||||||
String telegramInfo = (link != null && !link.telegramUserId.isEmpty())
|
String telegramInfo = (link != null && !link.telegramUserId.isEmpty())
|
||||||
? "&a" + link.telegramUsername + " &8(" + link.telegramUserId + ")" : "&7Nicht verknüpft";
|
? "&a" + link.telegramUsername + " &8(" + link.telegramUserId + ")" : "&7Nicht verknüpft";
|
||||||
|
|
||||||
// Ausgabe
|
String vanishStatus = VanishProvider.isVanished(target) ? "&eJa" : "&aKein";
|
||||||
|
|
||||||
sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
||||||
sender.sendMessage(color("&eChatInfo: &f" + target.getName() + " &8@ &7" + tServer));
|
sender.sendMessage(color("&eChatInfo: &f" + target.getName() + " &8@ &7" + tServer));
|
||||||
sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
||||||
@@ -680,6 +782,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
sender.sendMessage(color("&7Mute: " + muteStatus));
|
sender.sendMessage(color("&7Mute: " + muteStatus));
|
||||||
sender.sendMessage(color("&7Chat-aus: " + (selfChatMuted.contains(tUUID) ? "&cJa" : "&aKein")));
|
sender.sendMessage(color("&7Chat-aus: " + (selfChatMuted.contains(tUUID) ? "&cJa" : "&aKein")));
|
||||||
sender.sendMessage(color("&7Mentions: " + (mentionsDisabled.contains(tUUID) ? "&cDeaktiviert" : "&aAktiv")));
|
sender.sendMessage(color("&7Mentions: " + (mentionsDisabled.contains(tUUID) ? "&cDeaktiviert" : "&aAktiv")));
|
||||||
|
sender.sendMessage(color("&7Vanish: " + vanishStatus));
|
||||||
sender.sendMessage(color("&7Blockiert: &f" + blocked.size() + " Spieler"));
|
sender.sendMessage(color("&7Blockiert: &f" + blocked.size() + " Spieler"));
|
||||||
if (!blocked.isEmpty()) {
|
if (!blocked.isEmpty()) {
|
||||||
for (UUID bUUID : blocked) {
|
for (UUID bUUID : blocked) {
|
||||||
@@ -707,7 +810,6 @@ public class ChatModule implements Module, Listener {
|
|||||||
int lines = config.getHistoryDefaultLines();
|
int lines = config.getHistoryDefaultLines();
|
||||||
|
|
||||||
if (args.length >= 1) {
|
if (args.length >= 1) {
|
||||||
// Erstes Arg: Spielername oder Zahl?
|
|
||||||
try {
|
try {
|
||||||
lines = Math.min(Integer.parseInt(args[0]), config.getHistoryMaxLines());
|
lines = Math.min(Integer.parseInt(args[0]), config.getHistoryMaxLines());
|
||||||
} catch (NumberFormatException ex) {
|
} catch (NumberFormatException ex) {
|
||||||
@@ -765,7 +867,6 @@ public class ChatModule implements Module, Listener {
|
|||||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, mentionsCmd);
|
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, mentionsCmd);
|
||||||
|
|
||||||
// /chatbypass – Chat-Verarbeitung für nächste Eingabe(n) überspringen
|
// /chatbypass – Chat-Verarbeitung für nächste Eingabe(n) überspringen
|
||||||
// Nützlich wenn ein Plugin (CMI, Shop, etc.) auf eine Chat-Eingabe wartet
|
|
||||||
Command bypassCmd = new Command("chatbypass", null, "cbp") {
|
Command bypassCmd = new Command("chatbypass", null, "cbp") {
|
||||||
@Override
|
@Override
|
||||||
public void execute(CommandSender sender, String[] args) {
|
public void execute(CommandSender sender, String[] args) {
|
||||||
@@ -784,9 +885,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
};
|
};
|
||||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, bypassCmd);
|
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, bypassCmd);
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────
|
// /discordlink – Discord-Account verknüpfen
|
||||||
// /discordlink – Discord-Account verknüpfen
|
|
||||||
// ─────────────────────────────────────────────────────
|
|
||||||
Command discordLinkCmd = new Command("discordlink", null, "dlink") {
|
Command discordLinkCmd = new Command("discordlink", null, "dlink") {
|
||||||
@Override
|
@Override
|
||||||
public void execute(CommandSender sender, String[] args) {
|
public void execute(CommandSender sender, String[] args) {
|
||||||
@@ -806,9 +905,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
};
|
};
|
||||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, discordLinkCmd);
|
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, discordLinkCmd);
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────
|
// /telegramlink – Telegram-Account verknüpfen
|
||||||
// /telegramlink – Telegram-Account verknüpfen
|
|
||||||
// ─────────────────────────────────────────────────────
|
|
||||||
Command telegramLinkCmd = new Command("telegramlink", null, "tlink") {
|
Command telegramLinkCmd = new Command("telegramlink", null, "tlink") {
|
||||||
@Override
|
@Override
|
||||||
public void execute(CommandSender sender, String[] args) {
|
public void execute(CommandSender sender, String[] args) {
|
||||||
@@ -828,9 +925,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
};
|
};
|
||||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, telegramLinkCmd);
|
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, telegramLinkCmd);
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────
|
// /unlink – Verknüpfung aufheben
|
||||||
// /unlink – Verknüpfung aufheben
|
|
||||||
// ─────────────────────────────────────────────────────
|
|
||||||
Command unlinkCmd = new Command("unlink") {
|
Command unlinkCmd = new Command("unlink") {
|
||||||
@Override
|
@Override
|
||||||
public void execute(CommandSender sender, String[] args) {
|
public void execute(CommandSender sender, String[] args) {
|
||||||
@@ -868,9 +963,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
};
|
};
|
||||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, unlinkCmd);
|
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, unlinkCmd);
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────
|
// /report <spieler> <grund>
|
||||||
// NEU: /report <spieler> <grund>
|
|
||||||
// ─────────────────────────────────────────────────────
|
|
||||||
Command reportCmd = new Command("report") {
|
Command reportCmd = new Command("report") {
|
||||||
@Override
|
@Override
|
||||||
public void execute(CommandSender sender, String[] args) {
|
public void execute(CommandSender sender, String[] args) {
|
||||||
@@ -879,7 +972,6 @@ public class ChatModule implements Module, Listener {
|
|||||||
|
|
||||||
ProxiedPlayer p = (ProxiedPlayer) sender;
|
ProxiedPlayer p = (ProxiedPlayer) sender;
|
||||||
|
|
||||||
// Permission prüfen (optional)
|
|
||||||
String reqPerm = config.getReportPermission();
|
String reqPerm = config.getReportPermission();
|
||||||
if (reqPerm != null && !reqPerm.isEmpty() && !p.hasPermission(reqPerm)) {
|
if (reqPerm != null && !reqPerm.isEmpty() && !p.hasPermission(reqPerm)) {
|
||||||
p.sendMessage(color("&cDu hast keine Berechtigung für /report.")); return;
|
p.sendMessage(color("&cDu hast keine Berechtigung für /report.")); return;
|
||||||
@@ -890,7 +982,6 @@ public class ChatModule implements Module, Listener {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cooldown
|
|
||||||
long now = System.currentTimeMillis() / 1000L;
|
long now = System.currentTimeMillis() / 1000L;
|
||||||
Long last = reportCooldowns.get(p.getUniqueId());
|
Long last = reportCooldowns.get(p.getUniqueId());
|
||||||
if (last != null && (now - last) < config.getReportCooldown()) {
|
if (last != null && (now - last) < config.getReportCooldown()) {
|
||||||
@@ -902,67 +993,54 @@ public class ChatModule implements Module, Listener {
|
|||||||
String reportedName = args[0];
|
String reportedName = args[0];
|
||||||
String reason = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
|
String reason = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
|
||||||
String server = p.getServer() != null ? p.getServer().getInfo().getName() : "Proxy";
|
String server = p.getServer() != null ? p.getServer().getInfo().getName() : "Proxy";
|
||||||
|
|
||||||
// Letzte Nachricht des Gemeldeten als Kontext
|
|
||||||
String msgContext = lastChatMessages.getOrDefault(reportedName.toLowerCase(), "(keine Chat-Nachricht bekannt)");
|
String msgContext = lastChatMessages.getOrDefault(reportedName.toLowerCase(), "(keine Chat-Nachricht bekannt)");
|
||||||
|
|
||||||
// Report erstellen
|
|
||||||
String reportId = reportManager.createReport(
|
String reportId = reportManager.createReport(
|
||||||
p.getName(), p.getUniqueId(), reportedName, server, msgContext, reason);
|
p.getName(), p.getUniqueId(), reportedName, server, msgContext, reason);
|
||||||
|
|
||||||
// Report auch ins Chatlog schreiben (ID sichtbar)
|
|
||||||
if (chatLogger != null) {
|
if (chatLogger != null) {
|
||||||
String logMsg = "[REPORT] Reporter: " + p.getName() + ", Gemeldet: " + reportedName + ", Grund: " + reason +
|
String logMsg = "[REPORT] Reporter: " + p.getName() + ", Gemeldet: " + reportedName
|
||||||
" | Letzte Nachricht: " + msgContext + " | Report-ID: " + reportId;
|
+ ", Grund: " + reason + " | Letzte Nachricht: " + msgContext
|
||||||
String msgId = reportId; // Damit die ID im Chatlog und im Report identisch ist
|
+ " | Report-ID: " + reportId;
|
||||||
chatLogger.log(msgId, server, "report", p.getName(), logMsg);
|
chatLogger.log(reportId, server, "report", p.getName(), logMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==== Discord/Telegram Benachrichtigung ====
|
|
||||||
// Discord Webhook
|
|
||||||
String reportWebhook = config.getReportDiscordWebhook();
|
String reportWebhook = config.getReportDiscordWebhook();
|
||||||
logger.info("[Debug] DiscordWebhookEnabled=" + config.isReportWebhookEnabled()
|
if (config.isReportWebhookEnabled() && discordBridge != null
|
||||||
+ ", discordBridge=" + (discordBridge != null)
|
&& reportWebhook != null && !reportWebhook.isEmpty()) {
|
||||||
+ ", reportWebhook=" + reportWebhook);
|
|
||||||
if (config.isReportWebhookEnabled() && discordBridge != null && reportWebhook != null && !reportWebhook.isEmpty()) {
|
|
||||||
String title = "Neuer Report eingegangen";
|
String title = "Neuer Report eingegangen";
|
||||||
String desc = "**Reporter:** " + p.getName() +
|
String desc = "**Reporter:** " + p.getName()
|
||||||
"\n**Gemeldet:** " + reportedName +
|
+ "\n**Gemeldet:** " + reportedName
|
||||||
"\n**Server:** " + server +
|
+ "\n**Server:** " + server
|
||||||
"\n**Grund:** " + reason +
|
+ "\n**Grund:** " + reason
|
||||||
"\n**Letzte Nachricht:** " + msgContext +
|
+ "\n**Letzte Nachricht:** " + msgContext
|
||||||
"\n**Report-ID:** " + reportId;
|
+ "\n**Report-ID:** " + reportId;
|
||||||
discordBridge.sendEmbedToDiscord(reportWebhook, title, desc, config.getDiscordEmbedColor());
|
discordBridge.sendEmbedToDiscord(reportWebhook, title, desc, config.getDiscordEmbedColor());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Telegram Benachrichtigung
|
|
||||||
String reportTgChatId = config.getReportTelegramChatId();
|
String reportTgChatId = config.getReportTelegramChatId();
|
||||||
if (telegramBridge != null && reportTgChatId != null && !reportTgChatId.isEmpty()) {
|
if (telegramBridge != null && reportTgChatId != null && !reportTgChatId.isEmpty()) {
|
||||||
String header = "Neuer Report eingegangen";
|
String header = "Neuer Report eingegangen";
|
||||||
String content = "Reporter: " + p.getName() +
|
String content = "Reporter: " + p.getName()
|
||||||
"\nGemeldet: " + reportedName +
|
+ "\nGemeldet: " + reportedName
|
||||||
"\nServer: " + server +
|
+ "\nServer: " + server
|
||||||
"\nGrund: " + reason +
|
+ "\nGrund: " + reason
|
||||||
"\nLetzte Nachricht: " + msgContext +
|
+ "\nLetzte Nachricht: " + msgContext
|
||||||
"\nReport-ID: " + reportId;
|
+ "\nReport-ID: " + reportId;
|
||||||
telegramBridge.sendFormattedToTelegram(reportTgChatId, header, content);
|
telegramBridge.sendFormattedToTelegram(reportTgChatId, header, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
reportCooldowns.put(p.getUniqueId(), now);
|
reportCooldowns.put(p.getUniqueId(), now);
|
||||||
|
|
||||||
// Bestätigung an Reporter
|
|
||||||
String confirm = config.getReportConfirm().replace("{id}", reportId);
|
String confirm = config.getReportConfirm().replace("{id}", reportId);
|
||||||
p.sendMessage(color(confirm));
|
p.sendMessage(color(confirm));
|
||||||
|
|
||||||
// ── Online-Admins sofort benachrichtigen ──
|
|
||||||
notifyAdminsReport(reportId, p.getName(), reportedName, server, reason, msgContext);
|
notifyAdminsReport(reportId, p.getName(), reportedName, server, reason, msgContext);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, reportCmd);
|
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, reportCmd);
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────
|
// /reports [all] – Admin-Übersicht
|
||||||
// NEU: /reports [all] – Admin-Übersicht
|
|
||||||
// ─────────────────────────────────────────────────────
|
|
||||||
Command reportsCmd = new Command("reports", config.getReportViewPermission()) {
|
Command reportsCmd = new Command("reports", config.getReportViewPermission()) {
|
||||||
@Override
|
@Override
|
||||||
public void execute(CommandSender sender, String[] args) {
|
public void execute(CommandSender sender, String[] args) {
|
||||||
@@ -988,15 +1066,12 @@ public class ChatModule implements Module, Listener {
|
|||||||
String statusColor = r.closed ? "&a✔" : "&c✘";
|
String statusColor = r.closed ? "&a✔" : "&c✘";
|
||||||
|
|
||||||
if (sender instanceof ProxiedPlayer) {
|
if (sender instanceof ProxiedPlayer) {
|
||||||
// Klickbare Zeile: ID-Click kopiert ID in Zwischenablage
|
|
||||||
ComponentBuilder line = new ComponentBuilder("");
|
ComponentBuilder line = new ComponentBuilder("");
|
||||||
|
|
||||||
// Status
|
|
||||||
line.append(ChatColor.translateAlternateColorCodes('&', statusColor + " "))
|
line.append(ChatColor.translateAlternateColorCodes('&', statusColor + " "))
|
||||||
.event((ClickEvent) null)
|
.event((ClickEvent) null)
|
||||||
.event((HoverEvent) null);
|
.event((HoverEvent) null);
|
||||||
|
|
||||||
// Klickbare Report-ID
|
|
||||||
line.append(ChatColor.translateAlternateColorCodes('&', "&8[&f" + r.id + "&8]"))
|
line.append(ChatColor.translateAlternateColorCodes('&', "&8[&f" + r.id + "&8]"))
|
||||||
.event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, r.id))
|
.event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, r.id))
|
||||||
.event(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
|
.event(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
|
||||||
@@ -1009,7 +1084,6 @@ public class ChatModule implements Module, Listener {
|
|||||||
+ "\n" + ChatColor.YELLOW + "Grund: " + ChatColor.RED + r.reason
|
+ "\n" + ChatColor.YELLOW + "Grund: " + ChatColor.RED + r.reason
|
||||||
+ (r.closed ? "\n" + ChatColor.GREEN + "Geschlossen von: " + r.closedBy : "")).create()));
|
+ (r.closed ? "\n" + ChatColor.GREEN + "Geschlossen von: " + r.closedBy : "")).create()));
|
||||||
|
|
||||||
// Rest der Zeile
|
|
||||||
line.append(ChatColor.translateAlternateColorCodes('&',
|
line.append(ChatColor.translateAlternateColorCodes('&',
|
||||||
" &b" + r.reportedName + " &8← &7" + r.reporterName
|
" &b" + r.reportedName + " &8← &7" + r.reporterName
|
||||||
+ " &8@ &a" + r.server
|
+ " &8@ &a" + r.server
|
||||||
@@ -1019,7 +1093,6 @@ public class ChatModule implements Module, Listener {
|
|||||||
|
|
||||||
((ProxiedPlayer) sender).sendMessage(line.create());
|
((ProxiedPlayer) sender).sendMessage(line.create());
|
||||||
} else {
|
} else {
|
||||||
// Konsole: plain text
|
|
||||||
sender.sendMessage(color(statusColor + " &8[&f" + r.id + "&8] &b" + r.reportedName
|
sender.sendMessage(color(statusColor + " &8[&f" + r.id + "&8] &b" + r.reportedName
|
||||||
+ " &8← &7" + r.reporterName + " &8@ &a" + r.server
|
+ " &8← &7" + r.reporterName + " &8@ &a" + r.server
|
||||||
+ " &8| &e" + r.getFormattedTime()
|
+ " &8| &e" + r.getFormattedTime()
|
||||||
@@ -1035,9 +1108,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
};
|
};
|
||||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, reportsCmd);
|
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, reportsCmd);
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────
|
// /reportclose <ID>
|
||||||
// NEU: /reportclose <ID>
|
|
||||||
// ─────────────────────────────────────────────────────
|
|
||||||
Command reportCloseCmd = new Command("reportclose", config.getReportClosePermission()) {
|
Command reportCloseCmd = new Command("reportclose", config.getReportClosePermission()) {
|
||||||
@Override
|
@Override
|
||||||
public void execute(CommandSender sender, String[] args) {
|
public void execute(CommandSender sender, String[] args) {
|
||||||
@@ -1062,14 +1133,12 @@ public class ChatModule implements Module, Listener {
|
|||||||
|
|
||||||
sender.sendMessage(color("&aReport &f" + id + " &awurde geschlossen."));
|
sender.sendMessage(color("&aReport &f" + id + " &awurde geschlossen."));
|
||||||
|
|
||||||
// Reporter benachrichtigen, falls online
|
|
||||||
ProxiedPlayer reporter = ProxyServer.getInstance().getPlayer(report.reporterUUID);
|
ProxiedPlayer reporter = ProxyServer.getInstance().getPlayer(report.reporterUUID);
|
||||||
if (reporter != null && reporter.isConnected()) {
|
if (reporter != null && reporter.isConnected()) {
|
||||||
reporter.sendMessage(color("&8[&aReport&8] &7Dein Report &f" + id
|
reporter.sendMessage(color("&8[&aReport&8] &7Dein Report &f" + id
|
||||||
+ " &7gegen &c" + report.reportedName + " &7wurde von &b" + adminName + " &7bearbeitet."));
|
+ " &7gegen &c" + report.reportedName + " &7wurde von &b" + adminName + " &7bearbeitet."));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Andere Admins informieren
|
|
||||||
notifyAdmins("&8[&aReport geschlossen&8] &f" + adminName + " &7hat Report &f" + id + " &7geschlossen.");
|
notifyAdmins("&8[&aReport geschlossen&8] &f" + adminName + " &7hat Report &f" + id + " &7geschlossen.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1078,38 +1147,10 @@ public class ChatModule implements Module, Listener {
|
|||||||
|
|
||||||
// =========================================================
|
// =========================================================
|
||||||
// PLUGIN-INPUT BYPASS
|
// PLUGIN-INPUT BYPASS
|
||||||
//
|
|
||||||
// Spieler die gerade auf eine Chat-Eingabe eines Sub-Server-
|
|
||||||
// Plugins warten (CMI, Shops, etc.) werden vom ChatModule
|
|
||||||
// übersprungen. Die Nachricht geht direkt an den Sub-Server.
|
|
||||||
//
|
|
||||||
// Drei Erkennungsmethoden:
|
|
||||||
// 1. Manueller Bypass-Toggle via /chatbypass (für Admins)
|
|
||||||
// 2. Programmatische API: ChatModule.setAwaitingInput(uuid, true)
|
|
||||||
// 3. Automatische Erkennung bekannter Plugin-Nachrichten
|
|
||||||
// =========================================================
|
// =========================================================
|
||||||
|
|
||||||
// UUIDs die gerade auf Plugin-Chat-Eingabe warten
|
|
||||||
private final Set<UUID> awaitingInput = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prüft ob ein Spieler gerade auf eine Chat-Eingabe eines
|
* Öffentliche API für Sub-Server-Plugins oder BungeeCord-eigene Plugins.
|
||||||
* Sub-Server-Plugins wartet und das ChatModule überspringen soll.
|
|
||||||
*/
|
|
||||||
private boolean isAwaitingPluginInput(ProxiedPlayer player) {
|
|
||||||
// 1. Manuell / programmatisch gesetzt
|
|
||||||
if (awaitingInput.contains(player.getUniqueId())) return true;
|
|
||||||
|
|
||||||
// 2. Automatische Erkennung: BungeeCord leitet SubServer-Nachrichten
|
|
||||||
// via PluginChannel weiter – wir prüfen bekannte CMI-Patterns nicht,
|
|
||||||
// da wir keinen Zugriff auf SubServer-Metadaten haben.
|
|
||||||
// Stattdessen: Spieler kann selbst /chatbypass togglen oder
|
|
||||||
// Sub-Server-Plugin ruft setAwaitingInput() auf.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Öffentliche API für Sub-Server-Plugins oder BungeeCord-eigene Plugins:
|
|
||||||
* Setzt den Bypass-Status für einen Spieler.
|
* Setzt den Bypass-Status für einen Spieler.
|
||||||
*
|
*
|
||||||
* Beispiel aus einem anderen BungeeCord-Plugin:
|
* Beispiel aus einem anderen BungeeCord-Plugin:
|
||||||
@@ -1126,6 +1167,24 @@ public class ChatModule implements Module, Listener {
|
|||||||
return awaitingInput.contains(uuid);
|
return awaitingInput.contains(uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// VANISH-HILFSMETHODEN
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sucht einen Spieler nach Name und berücksichtigt den Vanish-Status.
|
||||||
|
*
|
||||||
|
* @param name Spielername (case-insensitiv)
|
||||||
|
* @param callerIsAdmin true → Vanished Spieler werden ebenfalls gefunden
|
||||||
|
* @return ProxiedPlayer oder null wenn nicht gefunden / vanished (für Nicht-Admins)
|
||||||
|
*/
|
||||||
|
private ProxiedPlayer findVisiblePlayer(String name, boolean callerIsAdmin) {
|
||||||
|
ProxiedPlayer target = ProxyServer.getInstance().getPlayer(name);
|
||||||
|
if (target == null) return null;
|
||||||
|
if (!callerIsAdmin && VanishProvider.isVanished(target)) return null;
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================================
|
// =========================================================
|
||||||
// HILFSMETHODEN
|
// HILFSMETHODEN
|
||||||
// =========================================================
|
// =========================================================
|
||||||
@@ -1133,7 +1192,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
private String buildFormat(String format, String server, String prefix,
|
private String buildFormat(String format, String server, String prefix,
|
||||||
String player, String suffix, String message) {
|
String player, String suffix, String message) {
|
||||||
String serverColor = config.getServerColor(server);
|
String serverColor = config.getServerColor(server);
|
||||||
String serverDisplay = config.getServerDisplay(server); // Anzeigename aus config
|
String serverDisplay = config.getServerDisplay(server);
|
||||||
String coloredServer = translateColors(serverColor + serverDisplay + "&r");
|
String coloredServer = translateColors(serverColor + serverDisplay + "&r");
|
||||||
|
|
||||||
return format
|
return format
|
||||||
@@ -1158,23 +1217,13 @@ public class ChatModule implements Module, Listener {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Übersetzt sowohl klassische &-Farbcodes als auch HEX-Codes im Format &#RRGGBB.
|
* Übersetzt sowohl klassische &-Farbcodes als auch HEX-Codes im Format &#RRGGBB.
|
||||||
*
|
|
||||||
* Beispiele:
|
|
||||||
* &a → §a (Grün)
|
|
||||||
* &#FF5500 → BungeeCord HEX-Farbe Orange
|
|
||||||
* &l&#FF5500Text → Fett + Orange
|
|
||||||
*
|
|
||||||
* BungeeCord unterstützt ChatColor.of("#RRGGBB") ab 1.16-kompatiblen Builds.
|
|
||||||
* Ältere Builds erhalten automatisch den nächsten &-Code als Fallback.
|
|
||||||
*/
|
*/
|
||||||
private String translateColors(String text) {
|
private String translateColors(String text) {
|
||||||
if (text == null) return "";
|
if (text == null) return "";
|
||||||
|
|
||||||
// 1. Schritt: &#RRGGBB → BungeeCord ChatColor
|
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
int i = 0;
|
int i = 0;
|
||||||
while (i < text.length()) {
|
while (i < text.length()) {
|
||||||
// Prüfe auf &#RRGGBB (8 Zeichen: & # R R G G B B)
|
|
||||||
if (i + 7 < text.length()
|
if (i + 7 < text.length()
|
||||||
&& text.charAt(i) == '&'
|
&& text.charAt(i) == '&'
|
||||||
&& text.charAt(i + 1) == '#') {
|
&& text.charAt(i + 1) == '#') {
|
||||||
@@ -1195,7 +1244,6 @@ public class ChatModule implements Module, Listener {
|
|||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Schritt: Standard &-Codes übersetzen
|
|
||||||
return ChatColor.translateAlternateColorCodes('&', sb.toString());
|
return ChatColor.translateAlternateColorCodes('&', sb.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1209,11 +1257,9 @@ public class ChatModule implements Module, Listener {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Benachrichtigt alle online Admins über einen neuen Report.
|
* Benachrichtigt alle online Admins über einen neuen Report.
|
||||||
* Baut eine mehrzeilige, klickbare Nachricht.
|
|
||||||
*/
|
*/
|
||||||
private void notifyAdminsReport(String reportId, String reporter, String reported,
|
private void notifyAdminsReport(String reportId, String reporter, String reported,
|
||||||
String server, String reason, String msgContext) {
|
String server, String reason, String msgContext) {
|
||||||
// Zeitstempel
|
|
||||||
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("dd.MM.yyyy HH:mm:ss");
|
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("dd.MM.yyyy HH:mm:ss");
|
||||||
String zeit = sdf.format(new java.util.Date());
|
String zeit = sdf.format(new java.util.Date());
|
||||||
|
|
||||||
@@ -1221,7 +1267,6 @@ public class ChatModule implements Module, Listener {
|
|||||||
if (!p.hasPermission(config.getAdminNotifyPermission())
|
if (!p.hasPermission(config.getAdminNotifyPermission())
|
||||||
&& !p.hasPermission(config.getAdminBypassPermission())) continue;
|
&& !p.hasPermission(config.getAdminBypassPermission())) continue;
|
||||||
|
|
||||||
// Mehrzeilige Report-Notification
|
|
||||||
p.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
p.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
||||||
p.sendMessage(color("&8[&cReport&8] &7Zeit: &e" + zeit));
|
p.sendMessage(color("&8[&cReport&8] &7Zeit: &e" + zeit));
|
||||||
p.sendMessage(color("&7Reporter: &b" + reporter));
|
p.sendMessage(color("&7Reporter: &b" + reporter));
|
||||||
@@ -1229,7 +1274,6 @@ public class ChatModule implements Module, Listener {
|
|||||||
p.sendMessage(color("&7Letzte Nachricht: &f" + msgContext));
|
p.sendMessage(color("&7Letzte Nachricht: &f" + msgContext));
|
||||||
p.sendMessage(color("&7Grund: &c" + reason));
|
p.sendMessage(color("&7Grund: &c" + reason));
|
||||||
|
|
||||||
// Klickbare ID-Zeile
|
|
||||||
ComponentBuilder idLine = new ComponentBuilder(ChatColor.GRAY + "ID: ");
|
ComponentBuilder idLine = new ComponentBuilder(ChatColor.GRAY + "ID: ");
|
||||||
idLine.append(ChatColor.WHITE + "" + ChatColor.BOLD + reportId)
|
idLine.append(ChatColor.WHITE + "" + ChatColor.BOLD + reportId)
|
||||||
.event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, reportId))
|
.event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, reportId))
|
||||||
@@ -1244,28 +1288,22 @@ public class ChatModule implements Module, Listener {
|
|||||||
p.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
p.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Konsole ebenfalls informieren
|
|
||||||
ProxyServer.getInstance().getConsole().sendMessage(color(
|
ProxyServer.getInstance().getConsole().sendMessage(color(
|
||||||
"&8[&cReport " + reportId + "&8] &7Reporter: &b" + reporter
|
"&8[&cReport " + reportId + "&8] &7Reporter: &b" + reporter
|
||||||
+ " &7→ &c" + reported + " &7@ &a" + server + " &8| &cGrund: &f" + reason));
|
+ " &7→ &c" + reported + " &7@ &a" + server + " &8| &cGrund: &f" + reason));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Baut eine BungeeCord-Nachricht ohne sichtbare ID.
|
* Baut eine BungeeCord-Nachricht mit klickbarem [⚑] Melden-Button.
|
||||||
* Am Ende erscheint ein klickbarer [⚑] Melden-Button (nur wenn Reports aktiviert).
|
|
||||||
*
|
|
||||||
* Layout: <formatierter Chat> §8[§c⚑§8]
|
|
||||||
*/
|
*/
|
||||||
private BaseComponent[] buildClickableMessage(String msgId, String formatted, String senderName) {
|
private BaseComponent[] buildClickableMessage(String msgId, String formatted, String senderName) {
|
||||||
ComponentBuilder builder = new ComponentBuilder("");
|
ComponentBuilder builder = new ComponentBuilder("");
|
||||||
|
|
||||||
// Eigentliche Nachricht (kein ID-Tag mehr sichtbar)
|
|
||||||
builder.append(ChatColor.translateAlternateColorCodes('&', formatted),
|
builder.append(ChatColor.translateAlternateColorCodes('&', formatted),
|
||||||
ComponentBuilder.FormatRetention.NONE)
|
ComponentBuilder.FormatRetention.NONE)
|
||||||
.event((ClickEvent) null)
|
.event((ClickEvent) null)
|
||||||
.event((HoverEvent) null);
|
.event((HoverEvent) null);
|
||||||
|
|
||||||
// [⚑] Melden-Button am Ende (nur wenn Report-System aktiv und Sender bekannt)
|
|
||||||
if (msgId != null && senderName != null && reportManager != null) {
|
if (msgId != null && senderName != null && reportManager != null) {
|
||||||
builder.append(" ", ComponentBuilder.FormatRetention.NONE)
|
builder.append(" ", ComponentBuilder.FormatRetention.NONE)
|
||||||
.event((ClickEvent) null)
|
.event((ClickEvent) null)
|
||||||
@@ -1287,15 +1325,11 @@ public class ChatModule implements Module, Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sendet einen Sound an einen Spieler via Plugin-Messaging.
|
* Sendet einen Sound-Hinweis via Actionbar (Mention-Feedback).
|
||||||
* Der Sub-Server muss den Kanal "BungeeCord" registriert haben (standard).
|
|
||||||
* Sound wird als Proxy-Message gesendet → Sub-Server-Plugin nötig für echten Sound.
|
|
||||||
* Als Fallback: Actionbar-Nachricht mit ♪-Symbol.
|
|
||||||
*/
|
*/
|
||||||
private void sendMentionSound(ProxiedPlayer player, String soundName) {
|
private void sendMentionSound(ProxiedPlayer player, String soundName) {
|
||||||
if (soundName == null || soundName.isEmpty()) return;
|
if (soundName == null || soundName.isEmpty()) return;
|
||||||
try {
|
try {
|
||||||
// Actionbar als visuellen Feedback (funktioniert ohne Sub-Server-Plugin)
|
|
||||||
net.md_5.bungee.api.chat.TextComponent actionBar =
|
net.md_5.bungee.api.chat.TextComponent actionBar =
|
||||||
new net.md_5.bungee.api.chat.TextComponent(
|
new net.md_5.bungee.api.chat.TextComponent(
|
||||||
ChatColor.translateAlternateColorCodes('&', "&e♪ Mention!"));
|
ChatColor.translateAlternateColorCodes('&', "&e♪ Mention!"));
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ public class ReportManager {
|
|||||||
logger.warning("[ChatModule] Fehler beim Laden der Reports: " + e.getMessage());
|
logger.warning("[ChatModule] Fehler beim Laden der Reports: " + e.getMessage());
|
||||||
}
|
}
|
||||||
idCounter.set(maxNum);
|
idCounter.set(maxNum);
|
||||||
logger.info("[ChatModule] " + reports.size() + " Reports geladen (" + getOpenCount() + " offen).");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Escape-Helfer (Pipe-Zeichen und Zeilenumbrüche escapen) =====
|
// ===== Escape-Helfer (Pipe-Zeichen und Zeilenumbrüche escapen) =====
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,424 +1,323 @@
|
|||||||
package net.viper.status.modules.chat.bridge;
|
package net.viper.status.modules.chat.bridge;
|
||||||
|
|
||||||
import net.md_5.bungee.api.ChatColor;
|
import net.md_5.bungee.api.ChatColor;
|
||||||
import net.md_5.bungee.api.ProxyServer;
|
import net.md_5.bungee.api.ProxyServer;
|
||||||
import net.md_5.bungee.api.chat.TextComponent;
|
import net.md_5.bungee.api.chat.TextComponent;
|
||||||
import net.md_5.bungee.api.plugin.Plugin;
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
import net.viper.status.modules.chat.AccountLinkManager;
|
import net.viper.status.modules.chat.AccountLinkManager;
|
||||||
import net.viper.status.modules.chat.ChatConfig;
|
import net.viper.status.modules.chat.ChatConfig;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Discord-Brücke für bidirektionale Kommunikation.
|
* Discord-Brücke für bidirektionale Kommunikation.
|
||||||
*
|
*
|
||||||
* Minecraft → Discord: Via Webhook (kein Bot benötigt)
|
* Fix #12: extractJsonString() behandelt Escape-Sequenzen jetzt korrekt.
|
||||||
* Discord → Minecraft: Via Bot-Polling der Discord REST-API
|
* Statt Zeichenvergleich mit dem Vorgänger-Char wird ein expliziter Escape-Flag verwendet.
|
||||||
*
|
*/
|
||||||
* Voraussetzungen:
|
public class DiscordBridge {
|
||||||
* - Bot mit "Read Message History" und "Send Messages" Permissions
|
|
||||||
* - Bot muss in den jeweiligen Kanälen sein
|
private final Plugin plugin;
|
||||||
* - Bot-Token in chat.yml eintragen
|
private final ChatConfig config;
|
||||||
*/
|
private final Logger logger;
|
||||||
public class DiscordBridge {
|
private AccountLinkManager linkManager;
|
||||||
|
|
||||||
private final Plugin plugin;
|
private final java.util.Map<String, AtomicLong> lastMessageIds = new java.util.concurrent.ConcurrentHashMap<>();
|
||||||
private final ChatConfig config;
|
private volatile boolean running = false;
|
||||||
private final Logger logger;
|
|
||||||
private AccountLinkManager linkManager;
|
public DiscordBridge(Plugin plugin, ChatConfig config) {
|
||||||
|
this.plugin = plugin;
|
||||||
// Letztes verarbeitetes Discord Message-ID pro Kanal (für Polling)
|
this.config = config;
|
||||||
private final java.util.Map<String, AtomicLong> lastMessageIds = new java.util.concurrent.ConcurrentHashMap<>();
|
this.logger = plugin.getLogger();
|
||||||
|
}
|
||||||
private volatile boolean running = false;
|
|
||||||
|
public void setLinkManager(AccountLinkManager linkManager) { this.linkManager = linkManager; }
|
||||||
public DiscordBridge(Plugin plugin, ChatConfig config) {
|
|
||||||
this.plugin = plugin;
|
public void start() {
|
||||||
this.config = config;
|
if (!config.isDiscordEnabled()
|
||||||
this.logger = plugin.getLogger();
|
|| config.getDiscordBotToken().isEmpty()
|
||||||
}
|
|| config.getDiscordBotToken().equals("YOUR_BOT_TOKEN_HERE")) {
|
||||||
|
logger.warning("[ChatModule-Discord] Bot-Token nicht konfiguriert. Discord-Empfang deaktiviert.");
|
||||||
/** Setzt den AccountLinkManager – muss vor start() aufgerufen werden. */
|
return;
|
||||||
public void setLinkManager(AccountLinkManager linkManager) {
|
}
|
||||||
this.linkManager = linkManager;
|
running = true;
|
||||||
}
|
int interval = Math.max(2, config.getDiscordPollInterval());
|
||||||
|
plugin.getProxy().getScheduler().schedule(plugin, this::pollAllChannels, interval, interval, TimeUnit.SECONDS);
|
||||||
public void start() {
|
logger.info("[ChatModule-Discord] Brücke gestartet (Poll-Intervall: " + interval + "s).");
|
||||||
if (!config.isDiscordEnabled()
|
}
|
||||||
|| config.getDiscordBotToken().isEmpty()
|
|
||||||
|| config.getDiscordBotToken().equals("YOUR_BOT_TOKEN_HERE")) {
|
public void stop() { running = false; }
|
||||||
logger.warning("[ChatModule-Discord] Bot-Token nicht konfiguriert. Discord-Empfang deaktiviert.");
|
|
||||||
return;
|
// ===== Minecraft → Discord =====
|
||||||
}
|
|
||||||
|
public void sendToDiscord(String webhookUrl, String username, String message, String avatarUrl) {
|
||||||
running = true;
|
if (webhookUrl == null || webhookUrl.isEmpty()) return;
|
||||||
|
plugin.getProxy().getScheduler().runAsync(plugin, () -> {
|
||||||
// Starte Polling-Task für alle konfigurierten Kanäle
|
try {
|
||||||
int interval = Math.max(2, config.getDiscordPollInterval());
|
String payload = "{\"username\":\"" + escapeJson(username) + "\""
|
||||||
plugin.getProxy().getScheduler().schedule(plugin, this::pollAllChannels,
|
+ (avatarUrl != null && !avatarUrl.isEmpty() ? ",\"avatar_url\":\"" + avatarUrl + "\"" : "")
|
||||||
interval, interval, TimeUnit.SECONDS);
|
+ ",\"content\":\"" + escapeJson(message) + "\"}";
|
||||||
|
postJson(webhookUrl, payload, null);
|
||||||
logger.info("[ChatModule-Discord] Brücke gestartet (Poll-Intervall: " + interval + "s).");
|
} catch (Exception e) {
|
||||||
}
|
logger.warning("[ChatModule-Discord] Webhook-Fehler: " + e.getMessage());
|
||||||
|
}
|
||||||
public void stop() {
|
});
|
||||||
running = false;
|
}
|
||||||
}
|
|
||||||
|
public void sendEmbedToDiscord(String webhookUrl, String title, String description, String colorHex) {
|
||||||
// ===== Minecraft → Discord =====
|
if (webhookUrl == null || webhookUrl.isEmpty()) return;
|
||||||
|
plugin.getProxy().getScheduler().runAsync(plugin, () -> {
|
||||||
/**
|
try {
|
||||||
* Sendet eine Nachricht via Webhook an Discord.
|
int color = 0x5865F2;
|
||||||
* Funktioniert ohne Bot-Token!
|
try { color = Integer.parseInt(colorHex.replace("#", ""), 16); } catch (Exception ignored) {}
|
||||||
*/
|
String payload = "{\"embeds\":[{\"title\":\"" + escapeJson(title) + "\""
|
||||||
public void sendToDiscord(String webhookUrl, String username, String message, String avatarUrl) {
|
+ ",\"description\":\"" + escapeJson(description) + "\""
|
||||||
if (webhookUrl == null || webhookUrl.isEmpty()) return;
|
+ ",\"color\":" + color + "}]}";
|
||||||
|
postJson(webhookUrl, payload, null);
|
||||||
plugin.getProxy().getScheduler().runAsync(plugin, () -> {
|
} catch (Exception e) {
|
||||||
try {
|
logger.warning("[ChatModule-Discord] Embed-Fehler: " + e.getMessage());
|
||||||
String safeUsername = escapeJson(username);
|
}
|
||||||
String safeMessage = escapeJson(message);
|
});
|
||||||
String payload = "{\"username\":\"" + safeUsername + "\""
|
}
|
||||||
+ (avatarUrl != null && !avatarUrl.isEmpty()
|
|
||||||
? ",\"avatar_url\":\"" + avatarUrl + "\""
|
public void sendToChannel(String channelId, String message) {
|
||||||
: "")
|
if (channelId == null || channelId.isEmpty()) return;
|
||||||
+ ",\"content\":\"" + safeMessage + "\"}";
|
if (config.getDiscordBotToken().isEmpty()) return;
|
||||||
|
plugin.getProxy().getScheduler().runAsync(plugin, () -> {
|
||||||
postJson(webhookUrl, payload, null);
|
try {
|
||||||
} catch (Exception e) {
|
String url = "https://discord.com/api/v10/channels/" + channelId + "/messages";
|
||||||
logger.warning("[ChatModule-Discord] Webhook-Fehler: " + e.getMessage());
|
postJson(url, "{\"content\":\"" + escapeJson(message) + "\"}", "Bot " + config.getDiscordBotToken());
|
||||||
}
|
} catch (Exception e) {
|
||||||
});
|
logger.warning("[ChatModule-Discord] Send-to-Channel-Fehler: " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
});
|
||||||
/**
|
}
|
||||||
* Sendet eine Embed-Nachricht (für HelpOp, Broadcast) an einen Discord-Kanal via Webhook.
|
|
||||||
*/
|
// ===== Discord → Minecraft (Polling) =====
|
||||||
public void sendEmbedToDiscord(String webhookUrl, String title, String description, String colorHex) {
|
|
||||||
if (webhookUrl == null || webhookUrl.isEmpty()) return;
|
private void pollAllChannels() {
|
||||||
|
if (!running) return;
|
||||||
plugin.getProxy().getScheduler().runAsync(plugin, () -> {
|
java.util.Set<String> channelIds = new java.util.LinkedHashSet<>();
|
||||||
try {
|
for (net.viper.status.modules.chat.ChatChannel ch : config.getChannels().values()) {
|
||||||
int color = 0;
|
if (!ch.getDiscordChannelId().isEmpty()) channelIds.add(ch.getDiscordChannelId());
|
||||||
try { color = Integer.parseInt(colorHex.replace("#", ""), 16); }
|
}
|
||||||
catch (Exception ignored) { color = 0x5865F2; }
|
if (!config.getDiscordAdminChannelId().isEmpty()) channelIds.add(config.getDiscordAdminChannelId());
|
||||||
|
for (String channelId : channelIds) pollChannel(channelId);
|
||||||
String payload = "{\"embeds\":[{\"title\":\"" + escapeJson(title) + "\""
|
}
|
||||||
+ ",\"description\":\"" + escapeJson(description) + "\""
|
|
||||||
+ ",\"color\":" + color + "}]}";
|
private void pollChannel(String channelId) {
|
||||||
|
try {
|
||||||
logger.info("[ChatModule-Discord] Sende Embed an Webhook: " + webhookUrl);
|
AtomicLong lastId = lastMessageIds.computeIfAbsent(channelId, k -> new AtomicLong(0L));
|
||||||
logger.info("[ChatModule-Discord] Payload: " + payload);
|
if (lastId.get() == 0L) {
|
||||||
|
String initResp = getJson("https://discord.com/api/v10/channels/" + channelId + "/messages?limit=1",
|
||||||
postJson(webhookUrl, payload, null);
|
"Bot " + config.getDiscordBotToken());
|
||||||
|
if (initResp != null && !initResp.equals("[]") && !initResp.isEmpty()) {
|
||||||
logger.info("[ChatModule-Discord] Embed erfolgreich an Discord gesendet.");
|
java.util.List<DiscordMessage> initMsgs = parseMessages(initResp);
|
||||||
} catch (Exception e) {
|
if (!initMsgs.isEmpty()) lastId.set(initMsgs.get(0).id);
|
||||||
logger.warning("[ChatModule-Discord] Embed-Fehler: " + e.getMessage());
|
}
|
||||||
}
|
return;
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
String url = "https://discord.com/api/v10/channels/" + channelId + "/messages?after=" + lastId.get() + "&limit=10";
|
||||||
/**
|
String response = getJson(url, "Bot " + config.getDiscordBotToken());
|
||||||
* Sendet eine Nachricht direkt in einen Discord-Kanal via Bot-Token.
|
if (response == null || response.equals("[]") || response.isEmpty()) return;
|
||||||
* Benötigt: DISCORD_BOT_TOKEN, channel-id
|
|
||||||
*/
|
java.util.List<DiscordMessage> messages = parseMessages(response);
|
||||||
public void sendToChannel(String channelId, String message) {
|
messages.sort(java.util.Comparator.comparingLong(m -> m.id));
|
||||||
if (channelId == null || channelId.isEmpty()) return;
|
|
||||||
if (config.getDiscordBotToken().isEmpty()) return;
|
for (DiscordMessage msg : messages) {
|
||||||
|
if (msg.id <= lastId.get()) continue;
|
||||||
plugin.getProxy().getScheduler().runAsync(plugin, () -> {
|
if (msg.isBot) continue;
|
||||||
try {
|
if (msg.content.isEmpty()) continue;
|
||||||
String url = "https://discord.com/api/v10/channels/" + channelId + "/messages";
|
lastId.set(msg.id);
|
||||||
String payload = "{\"content\":\"" + escapeJson(message) + "\"}";
|
|
||||||
postJson(url, payload, "Bot " + config.getDiscordBotToken());
|
if (msg.content.startsWith("!link ")) {
|
||||||
} catch (Exception e) {
|
String token = msg.content.substring(6).trim().toUpperCase();
|
||||||
logger.warning("[ChatModule-Discord] Send-to-Channel-Fehler: " + e.getMessage());
|
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.");
|
||||||
|
}
|
||||||
// ===== Discord → Minecraft (Polling) =====
|
continue;
|
||||||
|
}
|
||||||
private void pollAllChannels() {
|
|
||||||
if (!running) return;
|
String displayName = (linkManager != null)
|
||||||
|
? linkManager.resolveDiscordName(msg.authorId, msg.authorName) : msg.authorName;
|
||||||
// Alle Kanal-IDs aus der Konfiguration sammeln
|
String mcFormat = resolveFormat(channelId);
|
||||||
java.util.Set<String> channelIds = new java.util.LinkedHashSet<>();
|
if (mcFormat == null) continue;
|
||||||
|
|
||||||
for (net.viper.status.modules.chat.ChatChannel ch : config.getChannels().values()) {
|
String formatted = ChatColor.translateAlternateColorCodes('&',
|
||||||
if (!ch.getDiscordChannelId().isEmpty()) {
|
mcFormat.replace("{user}", displayName).replace("{message}", msg.content));
|
||||||
channelIds.add(ch.getDiscordChannelId());
|
ProxyServer.getInstance().getScheduler().runAsync(plugin,
|
||||||
}
|
() -> ProxyServer.getInstance().broadcast(new TextComponent(formatted)));
|
||||||
}
|
}
|
||||||
if (!config.getDiscordAdminChannelId().isEmpty()) {
|
} catch (Exception e) {
|
||||||
channelIds.add(config.getDiscordAdminChannelId());
|
logger.fine("[ChatModule-Discord] Poll-Fehler für Kanal " + channelId + ": " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
for (String channelId : channelIds) {
|
|
||||||
pollChannel(channelId);
|
private String resolveFormat(String channelId) {
|
||||||
}
|
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();
|
||||||
private void pollChannel(String channelId) {
|
}
|
||||||
try {
|
return null;
|
||||||
AtomicLong lastId = lastMessageIds.computeIfAbsent(channelId, k -> new AtomicLong(0L));
|
}
|
||||||
|
|
||||||
// Beim ersten Poll: aktuelle neueste ID holen und merken, nicht broadcasten.
|
// ===== HTTP =====
|
||||||
// So werden beim Start keine alten Discord-Nachrichten in Minecraft angezeigt.
|
|
||||||
if (lastId.get() == 0L) {
|
private void postJson(String urlStr, String payload, String authorization) throws Exception {
|
||||||
String initUrl = "https://discord.com/api/v10/channels/" + channelId + "/messages?limit=1";
|
HttpURLConnection conn = openConnection(urlStr, "POST", authorization);
|
||||||
String initResp = getJson(initUrl, "Bot " + config.getDiscordBotToken());
|
byte[] data = payload.getBytes(StandardCharsets.UTF_8);
|
||||||
if (initResp != null && !initResp.equals("[]") && !initResp.isEmpty()) {
|
conn.setRequestProperty("Content-Length", String.valueOf(data.length));
|
||||||
java.util.List<DiscordMessage> initMsgs = parseMessages(initResp);
|
conn.setDoOutput(true);
|
||||||
if (!initMsgs.isEmpty()) {
|
try (OutputStream os = conn.getOutputStream()) { os.write(data); }
|
||||||
lastId.set(initMsgs.get(0).id);
|
int code = conn.getResponseCode();
|
||||||
}
|
if (code >= 400) logger.warning("[ChatModule-Discord] HTTP " + code + ": " + readStream(conn.getErrorStream()));
|
||||||
}
|
conn.disconnect();
|
||||||
return; // Erster Poll nur zum Initialisieren, nichts broadcasten
|
}
|
||||||
}
|
|
||||||
|
private String getJson(String urlStr, String authorization) throws Exception {
|
||||||
String afterParam = "?after=" + lastId.get() + "&limit=10";
|
HttpURLConnection conn = openConnection(urlStr, "GET", authorization);
|
||||||
|
int code = conn.getResponseCode();
|
||||||
String url = "https://discord.com/api/v10/channels/" + channelId + "/messages" + afterParam;
|
if (code != 200) { conn.disconnect(); return null; }
|
||||||
String response = getJson(url, "Bot " + config.getDiscordBotToken());
|
String result = readStream(conn.getInputStream());
|
||||||
if (response == null || response.equals("[]") || response.isEmpty()) return;
|
conn.disconnect();
|
||||||
|
return result;
|
||||||
// JSON-Array von Nachrichten parsen (ohne externe Library)
|
}
|
||||||
java.util.List<DiscordMessage> messages = parseMessages(response);
|
|
||||||
|
private HttpURLConnection openConnection(String urlStr, String method, String authorization) throws Exception {
|
||||||
// Nachrichten chronologisch verarbeiten (älteste zuerst)
|
HttpURLConnection conn = (HttpURLConnection) new URL(urlStr).openConnection();
|
||||||
messages.sort(java.util.Comparator.comparingLong(m -> m.id));
|
conn.setRequestMethod(method);
|
||||||
|
conn.setConnectTimeout(5000);
|
||||||
for (DiscordMessage msg : messages) {
|
conn.setReadTimeout(8000);
|
||||||
if (msg.id <= lastId.get()) continue;
|
conn.setRequestProperty("Content-Type", "application/json");
|
||||||
if (msg.isBot) continue;
|
conn.setRequestProperty("User-Agent", "StatusAPI-ChatModule/1.0");
|
||||||
if (msg.content.isEmpty()) continue;
|
if (authorization != null && !authorization.isEmpty()) conn.setRequestProperty("Authorization", authorization);
|
||||||
|
return conn;
|
||||||
lastId.set(msg.id);
|
}
|
||||||
|
|
||||||
// ── Token-Einlösung: !link <TOKEN> ──
|
private String readStream(InputStream in) throws IOException {
|
||||||
if (msg.content.startsWith("!link ")) {
|
if (in == null) return "";
|
||||||
String token = msg.content.substring(6).trim().toUpperCase();
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
|
||||||
if (linkManager != null) {
|
StringBuilder sb = new StringBuilder(); String line;
|
||||||
AccountLinkManager.LinkedAccount acc =
|
while ((line = br.readLine()) != null) sb.append(line);
|
||||||
linkManager.redeemDiscord(token, msg.authorId, msg.authorName);
|
return sb.toString();
|
||||||
if (acc != null) {
|
}
|
||||||
sendToChannel(channelId,
|
}
|
||||||
"✅ Verknüpfung erfolgreich! Minecraft-Account: **"
|
|
||||||
+ acc.minecraftName + "**");
|
// ===== JSON Mini-Parser =====
|
||||||
} else {
|
|
||||||
sendToChannel(channelId,
|
private static class DiscordMessage {
|
||||||
"❌ Ungültiger oder abgelaufener Token. Bitte `/discordlink` im Spiel erneut ausführen.");
|
long id;
|
||||||
}
|
String authorId = "", authorName = "", content = "";
|
||||||
}
|
boolean isBot = false;
|
||||||
continue; // Nicht als Chat-Nachricht weiterleiten
|
}
|
||||||
}
|
|
||||||
|
private java.util.List<DiscordMessage> parseMessages(String json) {
|
||||||
// ── Account-Name auflösen ──
|
java.util.List<DiscordMessage> result = new java.util.ArrayList<>();
|
||||||
String displayName = (linkManager != null)
|
int depth = 0, start = -1;
|
||||||
? linkManager.resolveDiscordName(msg.authorId, msg.authorName)
|
for (int i = 0; i < json.length(); i++) {
|
||||||
: msg.authorName;
|
char c = json.charAt(i);
|
||||||
|
if (c == '{') { if (depth++ == 0) start = i; }
|
||||||
// Welchem Kanal gehört diese Discord-Kanal-ID?
|
else if (c == '}') {
|
||||||
final String mcFormat = resolveFormat(channelId);
|
if (--depth == 0 && start != -1) {
|
||||||
if (mcFormat == null) continue;
|
DiscordMessage msg = parseMessage(json.substring(start, i + 1));
|
||||||
|
if (msg != null) result.add(msg);
|
||||||
final String formatted = ChatColor.translateAlternateColorCodes('&',
|
start = -1;
|
||||||
mcFormat.replace("{user}", displayName)
|
}
|
||||||
.replace("{message}", msg.content));
|
}
|
||||||
|
}
|
||||||
ProxyServer.getInstance().getScheduler().runAsync(plugin, () ->
|
return result;
|
||||||
ProxyServer.getInstance().broadcast(new TextComponent(formatted))
|
}
|
||||||
);
|
|
||||||
}
|
private DiscordMessage parseMessage(String obj) {
|
||||||
} catch (Exception e) {
|
try {
|
||||||
logger.fine("[ChatModule-Discord] Poll-Fehler für Kanal " + channelId + ": " + e.getMessage());
|
DiscordMessage msg = new DiscordMessage();
|
||||||
}
|
msg.id = Long.parseLong(extractJsonString(obj, "id"));
|
||||||
}
|
msg.content = unescapeJson(extractJsonString(obj, "content"));
|
||||||
|
|
||||||
private String resolveFormat(String channelId) {
|
// Webhook-Nachrichten als Bot markieren (Echo-Loop verhindern)
|
||||||
// Admin-Kanal?
|
if (!extractJsonString(obj, "webhook_id").isEmpty()) {
|
||||||
if (channelId.equals(config.getDiscordAdminChannelId())) {
|
msg.isBot = true;
|
||||||
return config.getDiscordFromFormat();
|
return msg;
|
||||||
}
|
}
|
||||||
// Reguläre Kanäle
|
|
||||||
for (net.viper.status.modules.chat.ChatChannel ch : config.getChannels().values()) {
|
int authStart = obj.indexOf("\"author\"");
|
||||||
if (channelId.equals(ch.getDiscordChannelId())) {
|
if (authStart >= 0) {
|
||||||
return config.getDiscordFromFormat();
|
String authBlock = extractJsonObject(obj, authStart);
|
||||||
}
|
msg.authorId = extractJsonString(authBlock, "id");
|
||||||
}
|
msg.authorName = unescapeJson(extractJsonString(authBlock, "username"));
|
||||||
return null;
|
msg.isBot = "true".equals(extractJsonString(authBlock, "bot"));
|
||||||
}
|
}
|
||||||
|
return msg;
|
||||||
// ===== HTTP-Hilfsklassen =====
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
private void postJson(String urlStr, String payload, String authorization) throws Exception {
|
}
|
||||||
HttpURLConnection conn = openConnection(urlStr, "POST", authorization);
|
}
|
||||||
byte[] data = payload.getBytes(StandardCharsets.UTF_8);
|
|
||||||
conn.setRequestProperty("Content-Length", String.valueOf(data.length));
|
/**
|
||||||
conn.setDoOutput(true);
|
* FIX #12: Escape-Sequenzen werden korrekt mit einem Escape-Flag behandelt
|
||||||
try (OutputStream os = conn.getOutputStream()) { os.write(data); }
|
* statt den Vorgänger-Char zu vergleichen (der bei '\\' + '"' versagt).
|
||||||
int code = conn.getResponseCode();
|
* Gibt immer einen leeren String zurück wenn der Key nicht gefunden wird (nie null).
|
||||||
if (code >= 400) {
|
*/
|
||||||
String err = readStream(conn.getErrorStream());
|
private String extractJsonString(String json, String key) {
|
||||||
logger.warning("[ChatModule-Discord] HTTP " + code + ": " + err);
|
if (json == null || key == null) return "";
|
||||||
}
|
String fullKey = "\"" + key + "\"";
|
||||||
conn.disconnect();
|
int keyIdx = json.indexOf(fullKey);
|
||||||
}
|
if (keyIdx < 0) return "";
|
||||||
|
int colon = json.indexOf(':', keyIdx + fullKey.length());
|
||||||
private String getJson(String urlStr, String authorization) throws Exception {
|
if (colon < 0) return "";
|
||||||
HttpURLConnection conn = openConnection(urlStr, "GET", authorization);
|
int valStart = colon + 1;
|
||||||
int code = conn.getResponseCode();
|
while (valStart < json.length() && json.charAt(valStart) == ' ') valStart++;
|
||||||
if (code != 200) { conn.disconnect(); return null; }
|
if (valStart >= json.length()) return "";
|
||||||
String result = readStream(conn.getInputStream());
|
char first = json.charAt(valStart);
|
||||||
conn.disconnect();
|
if (first == '"') {
|
||||||
return result;
|
// FIX: Expliziter Escape-Flag statt Vorgänger-Char-Vergleich
|
||||||
}
|
int end = valStart + 1;
|
||||||
|
boolean escaped = false;
|
||||||
private HttpURLConnection openConnection(String urlStr, String method, String authorization) throws Exception {
|
while (end < json.length()) {
|
||||||
HttpURLConnection conn = (HttpURLConnection) new URL(urlStr).openConnection();
|
char ch = json.charAt(end);
|
||||||
conn.setRequestMethod(method);
|
if (escaped) {
|
||||||
conn.setConnectTimeout(5000);
|
escaped = false;
|
||||||
conn.setReadTimeout(8000);
|
} else if (ch == '\\') {
|
||||||
conn.setRequestProperty("Content-Type", "application/json");
|
escaped = true;
|
||||||
conn.setRequestProperty("User-Agent", "StatusAPI-ChatModule/1.0");
|
} else if (ch == '"') {
|
||||||
if (authorization != null && !authorization.isEmpty()) {
|
break;
|
||||||
conn.setRequestProperty("Authorization", authorization);
|
}
|
||||||
}
|
end++;
|
||||||
return conn;
|
}
|
||||||
}
|
return json.substring(valStart + 1, end);
|
||||||
|
} else {
|
||||||
private String readStream(InputStream in) throws IOException {
|
int end = valStart;
|
||||||
if (in == null) return "";
|
while (end < json.length() && ",}\n".indexOf(json.charAt(end)) < 0) end++;
|
||||||
try (BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
|
return json.substring(valStart, end).trim();
|
||||||
StringBuilder sb = new StringBuilder();
|
}
|
||||||
String line;
|
}
|
||||||
while ((line = br.readLine()) != null) sb.append(line);
|
|
||||||
return sb.toString();
|
private String extractJsonObject(String json, int fromIndex) {
|
||||||
}
|
int depth = 0, start = -1;
|
||||||
}
|
for (int i = fromIndex; i < json.length(); i++) {
|
||||||
|
char c = json.charAt(i);
|
||||||
// ===== JSON Mini-Parser =====
|
if (c == '{') { if (depth++ == 0) start = i; }
|
||||||
|
else if (c == '}') { if (--depth == 0 && start >= 0) return json.substring(start, i + 1); }
|
||||||
/** Repräsentiert eine Discord-Nachricht (minimale Felder). */
|
}
|
||||||
private static class DiscordMessage {
|
return "";
|
||||||
long id;
|
}
|
||||||
String authorId = "";
|
|
||||||
String authorName = "";
|
private static String escapeJson(String s) {
|
||||||
String content = "";
|
if (s == null) return "";
|
||||||
boolean isBot = false;
|
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private static String unescapeJson(String s) {
|
||||||
* Parst ein JSON-Array von Discord-Nachrichten ohne externe Bibliothek.
|
if (s == null) return "";
|
||||||
* Nur die benötigten Felder werden extrahiert.
|
return s.replace("\\\"", "\"").replace("\\n", "\n").replace("\\r", "\r").replace("\\\\", "\\");
|
||||||
*/
|
}
|
||||||
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);
|
|
||||||
if (msg != null) result.add(msg);
|
|
||||||
start = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private DiscordMessage parseMessage(String obj) {
|
|
||||||
try {
|
|
||||||
DiscordMessage msg = new DiscordMessage();
|
|
||||||
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
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
return msg;
|
|
||||||
} catch (Exception e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String extractJsonString(String json, String key) {
|
|
||||||
int keyIdx = json.indexOf(key);
|
|
||||||
if (keyIdx < 0) return "";
|
|
||||||
int colon = json.indexOf(':', keyIdx + key.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
|
|
||||||
int end = valStart + 1;
|
|
||||||
while (end < json.length()) {
|
|
||||||
if (json.charAt(end) == '"' && json.charAt(end - 1) != '\\') 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String extractJsonObject(String json, int fromIndex) {
|
|
||||||
int depth = 0, start = -1;
|
|
||||||
for (int i = fromIndex; i < json.length(); i++) {
|
|
||||||
char c = json.charAt(i);
|
|
||||||
if (c == '{') { if (depth++ == 0) start = i; }
|
|
||||||
else if (c == '}') { if (--depth == 0 && start >= 0) return json.substring(start, i + 1); }
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String escapeJson(String s) {
|
|
||||||
if (s == null) return "";
|
|
||||||
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("\\\\", "\\");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -26,153 +26,92 @@ import java.util.concurrent.TimeUnit;
|
|||||||
/**
|
/**
|
||||||
* ForumBridgeModule: Verbindet das WP Business Forum mit dem Minecraft-Server.
|
* ForumBridgeModule: Verbindet das WP Business Forum mit dem Minecraft-Server.
|
||||||
*
|
*
|
||||||
* HTTP-Endpoints (werden vom StatusAPI WebServer geroutet):
|
* Fix #13: extractJsonString() gibt jetzt immer einen leeren String statt null zurück.
|
||||||
* POST /forum/notify — WordPress pusht Benachrichtigung
|
* Alle Aufrufer müssen nicht mehr auf null prüfen, was NullPointerExceptions verhindert.
|
||||||
* POST /forum/unlink — WordPress informiert über Verknüpfungslösung
|
|
||||||
* GET /forum/status — Verbindungstest
|
|
||||||
*
|
|
||||||
* Commands:
|
|
||||||
* /forumlink <token> — Account mit Forum verknüpfen
|
|
||||||
* /forum — Ausstehende Benachrichtigungen anzeigen
|
|
||||||
*/
|
*/
|
||||||
public class ForumBridgeModule implements Module, Listener {
|
public class ForumBridgeModule implements Module, Listener {
|
||||||
|
|
||||||
private Plugin plugin;
|
private Plugin plugin;
|
||||||
private ForumNotifStorage storage;
|
private ForumNotifStorage storage;
|
||||||
|
|
||||||
// Konfiguration aus verify.properties
|
|
||||||
private boolean enabled = true;
|
private boolean enabled = true;
|
||||||
private String wpBaseUrl = "";
|
private String wpBaseUrl = "";
|
||||||
private String apiSecret = "";
|
private String apiSecret = "";
|
||||||
private int loginDelaySeconds = 3;
|
private int loginDelaySeconds = 3;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public String getName() { return "ForumBridgeModule"; }
|
||||||
return "ForumBridgeModule";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable(Plugin plugin) {
|
public void onEnable(Plugin plugin) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
|
|
||||||
// Config laden
|
|
||||||
loadConfig(plugin);
|
loadConfig(plugin);
|
||||||
|
if (!enabled) { plugin.getLogger().info("ForumBridgeModule ist deaktiviert."); return; }
|
||||||
|
|
||||||
if (!enabled) {
|
|
||||||
plugin.getLogger().info("ForumBridgeModule ist deaktiviert.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Storage initialisieren und laden
|
|
||||||
storage = new ForumNotifStorage(plugin.getDataFolder(), plugin.getLogger());
|
storage = new ForumNotifStorage(plugin.getDataFolder(), plugin.getLogger());
|
||||||
storage.load();
|
storage.load();
|
||||||
|
|
||||||
// Event Listener registrieren
|
|
||||||
plugin.getProxy().getPluginManager().registerListener(plugin, this);
|
plugin.getProxy().getPluginManager().registerListener(plugin, this);
|
||||||
|
|
||||||
// Commands registrieren
|
|
||||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new ForumLinkCommand());
|
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new ForumLinkCommand());
|
||||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new ForumCommand());
|
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new ForumCommand());
|
||||||
|
|
||||||
// Auto-Save alle 10 Minuten
|
|
||||||
plugin.getProxy().getScheduler().schedule(plugin, () -> {
|
plugin.getProxy().getScheduler().schedule(plugin, () -> {
|
||||||
try {
|
try { storage.save(); } catch (Exception e) { plugin.getLogger().warning("ForumBridge Auto-Save Fehler: " + e.getMessage()); }
|
||||||
storage.save();
|
|
||||||
} catch (Exception e) {
|
|
||||||
plugin.getLogger().warning("ForumBridge Auto-Save Fehler: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}, 10, 10, TimeUnit.MINUTES);
|
}, 10, 10, TimeUnit.MINUTES);
|
||||||
|
|
||||||
// Alte Benachrichtigungen aufräumen (täglich, max 30 Tage)
|
plugin.getProxy().getScheduler().schedule(plugin, () -> storage.purgeOld(30), 1, 24, TimeUnit.HOURS);
|
||||||
plugin.getProxy().getScheduler().schedule(plugin, () -> {
|
|
||||||
storage.purgeOld(30);
|
|
||||||
}, 1, 24, TimeUnit.HOURS);
|
|
||||||
|
|
||||||
plugin.getLogger().info("ForumBridgeModule aktiviert.");
|
plugin.getLogger().info("ForumBridgeModule aktiviert.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDisable(Plugin plugin) {
|
public void onDisable(Plugin plugin) {
|
||||||
if (storage != null) {
|
if (storage != null) { storage.save(); plugin.getLogger().info("Forum-Benachrichtigungen gespeichert."); }
|
||||||
storage.save();
|
|
||||||
plugin.getLogger().info("Forum-Benachrichtigungen gespeichert.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== CONFIG =====
|
|
||||||
|
|
||||||
private void loadConfig(Plugin plugin) {
|
private void loadConfig(Plugin plugin) {
|
||||||
try {
|
try {
|
||||||
Properties props = new Properties();
|
Properties props = new Properties();
|
||||||
File configFile = new File(plugin.getDataFolder(), "verify.properties");
|
File configFile = new File(plugin.getDataFolder(), "verify.properties");
|
||||||
if (configFile.exists()) {
|
if (configFile.exists()) {
|
||||||
try (FileInputStream fis = new FileInputStream(configFile)) {
|
try (FileInputStream fis = new FileInputStream(configFile)) { props.load(fis); }
|
||||||
props.load(fis);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.enabled = !"false".equalsIgnoreCase(props.getProperty("forum.enabled", "true"));
|
this.enabled = !"false".equalsIgnoreCase(props.getProperty("forum.enabled", "true"));
|
||||||
this.wpBaseUrl = props.getProperty("forum.wp_url", props.getProperty("wp_verify_url", ""));
|
this.wpBaseUrl = props.getProperty("forum.wp_url", props.getProperty("wp_verify_url", ""));
|
||||||
this.apiSecret = props.getProperty("forum.api_secret", "");
|
this.apiSecret = props.getProperty("forum.api_secret", "");
|
||||||
this.loginDelaySeconds = parseInt(props.getProperty("forum.login_delay_seconds", "3"), 3);
|
this.loginDelaySeconds = parseInt(props.getProperty("forum.login_delay_seconds", "3"), 3);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
plugin.getLogger().warning("Fehler beim Laden der ForumBridge-Config: " + e.getMessage());
|
plugin.getLogger().warning("Fehler beim Laden der ForumBridge-Config: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int parseInt(String s, int def) {
|
private int parseInt(String s, int def) { try { return Integer.parseInt(s); } catch (Exception e) { return def; } }
|
||||||
try { return Integer.parseInt(s); } catch (Exception e) { return def; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== HTTP HANDLER (aufgerufen vom StatusAPI WebServer) =====
|
// ===== HTTP HANDLER =====
|
||||||
|
|
||||||
/**
|
|
||||||
* Verarbeitet POST /forum/notify von WordPress.
|
|
||||||
* WordPress sendet Benachrichtigungen wenn ein verknüpfter Spieler
|
|
||||||
* eine neue Antwort, Erwähnung oder PN erhält.
|
|
||||||
*
|
|
||||||
* Erwarteter JSON-Body:
|
|
||||||
* {
|
|
||||||
* "player_uuid": "uuid-string",
|
|
||||||
* "type": "reply|mention|message",
|
|
||||||
* "title": "Thread-Titel oder PN",
|
|
||||||
* "author": "Absender-Name",
|
|
||||||
* "url": "Forum-Link",
|
|
||||||
* "wp_user_id": 123
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
public String handleNotify(String body, String apiKeyHeader) {
|
public String handleNotify(String body, String apiKeyHeader) {
|
||||||
// API-Key prüfen
|
|
||||||
if (!apiSecret.isEmpty() && !apiSecret.equals(apiKeyHeader)) {
|
if (!apiSecret.isEmpty() && !apiSecret.equals(apiKeyHeader)) {
|
||||||
return "{\"success\":false,\"error\":\"unauthorized\"}";
|
return "{\"success\":false,\"error\":\"unauthorized\"}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIX #13: extractJsonString gibt "" statt null → kein NullPointerException möglich
|
||||||
String playerUuid = extractJsonString(body, "player_uuid");
|
String playerUuid = extractJsonString(body, "player_uuid");
|
||||||
String type = extractJsonString(body, "type");
|
String type = extractJsonString(body, "type");
|
||||||
String title = extractJsonString(body, "title");
|
String title = extractJsonString(body, "title");
|
||||||
String author = extractJsonString(body, "author");
|
String author = extractJsonString(body, "author");
|
||||||
String url = extractJsonString(body, "url");
|
String url = extractJsonString(body, "url");
|
||||||
|
|
||||||
if (playerUuid == null || playerUuid.isEmpty()) {
|
if (playerUuid.isEmpty()) return "{\"success\":false,\"error\":\"missing_player_uuid\"}";
|
||||||
return "{\"success\":false,\"error\":\"missing_player_uuid\"}";
|
|
||||||
}
|
|
||||||
|
|
||||||
java.util.UUID uuid;
|
java.util.UUID uuid;
|
||||||
try {
|
try { uuid = java.util.UUID.fromString(playerUuid); }
|
||||||
uuid = java.util.UUID.fromString(playerUuid);
|
catch (Exception e) { return "{\"success\":false,\"error\":\"invalid_uuid\"}"; }
|
||||||
} catch (Exception e) {
|
|
||||||
return "{\"success\":false,\"error\":\"invalid_uuid\"}";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: Wenn type 'thread' und title enthält 'Umfrage', dann als 'poll' behandeln
|
if ("thread".equalsIgnoreCase(type) && title.toLowerCase().contains("umfrage")) type = "poll";
|
||||||
if (type != null && type.equalsIgnoreCase("thread") && title != null && title.toLowerCase().contains("umfrage")) {
|
if (type.isEmpty()) type = "reply";
|
||||||
type = "poll";
|
|
||||||
}
|
|
||||||
if (type == null || type.isEmpty()) type = "reply";
|
|
||||||
|
|
||||||
// Notification erstellen
|
// Alle Werte sind garantiert nicht null (extractJsonString gibt "" zurück)
|
||||||
ForumNotification notification = new ForumNotification(uuid, type, title, author, url);
|
ForumNotification notification = new ForumNotification(uuid, type, title, author, url);
|
||||||
|
|
||||||
// Sofort zustellen wenn online
|
|
||||||
ProxiedPlayer online = ProxyServer.getInstance().getPlayer(uuid);
|
ProxiedPlayer online = ProxyServer.getInstance().getPlayer(uuid);
|
||||||
if (online != null && online.isConnected()) {
|
if (online != null && online.isConnected()) {
|
||||||
deliverNotification(online, notification);
|
deliverNotification(online, notification);
|
||||||
@@ -180,62 +119,30 @@ public class ForumBridgeModule implements Module, Listener {
|
|||||||
return "{\"success\":true,\"delivered\":true}";
|
return "{\"success\":true,\"delivered\":true}";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Offline → speichern für späteren Login
|
|
||||||
storage.add(notification);
|
storage.add(notification);
|
||||||
return "{\"success\":true,\"delivered\":false}";
|
return "{\"success\":true,\"delivered\":false}";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Verarbeitet POST /forum/unlink von WordPress.
|
|
||||||
* Wird aufgerufen wenn ein User seine Verknüpfung im Forum löst.
|
|
||||||
*/
|
|
||||||
public String handleUnlink(String body, String apiKeyHeader) {
|
public String handleUnlink(String body, String apiKeyHeader) {
|
||||||
if (!apiSecret.isEmpty() && !apiSecret.equals(apiKeyHeader)) {
|
if (!apiSecret.isEmpty() && !apiSecret.equals(apiKeyHeader)) return "{\"success\":false,\"error\":\"unauthorized\"}";
|
||||||
return "{\"success\":false,\"error\":\"unauthorized\"}";
|
|
||||||
}
|
|
||||||
// Aktuell keine lokale Aktion nötig — die Zuordnung liegt in WordPress
|
|
||||||
return "{\"success\":true}";
|
return "{\"success\":true}";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Verarbeitet GET /forum/status — Verbindungstest.
|
|
||||||
*/
|
|
||||||
public String handleStatus() {
|
public String handleStatus() {
|
||||||
String version = "unknown";
|
String version = "unknown";
|
||||||
try {
|
try { if (plugin.getDescription() != null) version = plugin.getDescription().getVersion(); } catch (Exception ignored) {}
|
||||||
if (plugin.getDescription() != null) {
|
|
||||||
version = plugin.getDescription().getVersion();
|
|
||||||
}
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
|
|
||||||
return "{\"success\":true,\"module\":\"ForumBridgeModule\",\"version\":\"" + version + "\"}";
|
return "{\"success\":true,\"module\":\"ForumBridgeModule\",\"version\":\"" + version + "\"}";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== NOTIFICATION ZUSTELLUNG =====
|
// ===== NOTIFICATION =====
|
||||||
|
|
||||||
/**
|
|
||||||
* Stellt eine einzelne Benachrichtigung an einen Online-Spieler zu.
|
|
||||||
*/
|
|
||||||
private void deliverNotification(ProxiedPlayer player, ForumNotification notif) {
|
private void deliverNotification(ProxiedPlayer player, ForumNotification notif) {
|
||||||
String color = notif.getTypeColor();
|
String color = notif.getTypeColor();
|
||||||
String label = notif.getTypeLabel();
|
String label = notif.getTypeLabel();
|
||||||
|
|
||||||
// Trennlinie
|
|
||||||
player.sendMessage(new TextComponent("§8§m "));
|
player.sendMessage(new TextComponent("§8§m "));
|
||||||
|
player.sendMessage(new TextComponent("§6§l✉ Forum §8» " + color + label));
|
||||||
// Hauptnachricht
|
if (!notif.getTitle().isEmpty()) player.sendMessage(new TextComponent("§7 " + notif.getTitle()));
|
||||||
TextComponent header = new TextComponent("§6§l✉ Forum §8» " + color + label);
|
if (!notif.getAuthor().isEmpty()) player.sendMessage(new TextComponent("§7 von §f" + notif.getAuthor()));
|
||||||
player.sendMessage(header);
|
|
||||||
|
|
||||||
// Details
|
|
||||||
if (!notif.getTitle().isEmpty()) {
|
|
||||||
player.sendMessage(new TextComponent("§7 " + notif.getTitle()));
|
|
||||||
}
|
|
||||||
if (!notif.getAuthor().isEmpty()) {
|
|
||||||
player.sendMessage(new TextComponent("§7 von §f" + notif.getAuthor()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Klickbarer Link (wenn URL vorhanden)
|
|
||||||
if (!notif.getUrl().isEmpty()) {
|
if (!notif.getUrl().isEmpty()) {
|
||||||
TextComponent link = new TextComponent("§a ➜ Im Forum ansehen");
|
TextComponent link = new TextComponent("§a ➜ Im Forum ansehen");
|
||||||
link.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, notif.getUrl()));
|
link.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, notif.getUrl()));
|
||||||
@@ -243,94 +150,56 @@ public class ForumBridgeModule implements Module, Listener {
|
|||||||
new ComponentBuilder("§7Klicke um den Beitrag im Forum zu öffnen").create()));
|
new ComponentBuilder("§7Klicke um den Beitrag im Forum zu öffnen").create()));
|
||||||
player.sendMessage(link);
|
player.sendMessage(link);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trennlinie
|
|
||||||
player.sendMessage(new TextComponent("§8§m "));
|
player.sendMessage(new TextComponent("§8§m "));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Stellt alle ausstehenden Benachrichtigungen an einen Spieler zu.
|
|
||||||
* Wird beim Login aufgerufen (mit kurzem Delay).
|
|
||||||
*/
|
|
||||||
private void deliverPending(ProxiedPlayer player) {
|
private void deliverPending(ProxiedPlayer player) {
|
||||||
List<ForumNotification> pending = storage.getPending(player.getUniqueId());
|
List<ForumNotification> pending = storage.getPending(player.getUniqueId());
|
||||||
if (pending.isEmpty()) return;
|
if (pending.isEmpty()) return;
|
||||||
|
|
||||||
int count = pending.size();
|
int count = pending.size();
|
||||||
|
|
||||||
// Zusammenfassung wenn mehr als 3
|
|
||||||
if (count > 3) {
|
if (count > 3) {
|
||||||
player.sendMessage(new TextComponent("§8§m "));
|
player.sendMessage(new TextComponent("§8§m "));
|
||||||
player.sendMessage(new TextComponent("§6§l✉ Forum §8» §fDu hast §e" + count + " §fneue Benachrichtigungen!"));
|
player.sendMessage(new TextComponent("§6§l✉ Forum §8» §fDu hast §e" + count + " §fneue Benachrichtigungen!"));
|
||||||
player.sendMessage(new TextComponent("§7 Tippe §e/forum §7um sie anzuzeigen."));
|
player.sendMessage(new TextComponent("§7 Tippe §e/forum §7um sie anzuzeigen."));
|
||||||
player.sendMessage(new TextComponent("§8§m "));
|
player.sendMessage(new TextComponent("§8§m "));
|
||||||
} else {
|
} else {
|
||||||
// Einzeln zustellen
|
for (ForumNotification n : pending) deliverNotification(player, n);
|
||||||
for (ForumNotification n : pending) {
|
|
||||||
deliverNotification(player, n);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alle als zugestellt markieren und aufräumen
|
|
||||||
storage.markAllDelivered(player.getUniqueId());
|
storage.markAllDelivered(player.getUniqueId());
|
||||||
storage.clearDelivered(player.getUniqueId());
|
storage.clearDelivered(player.getUniqueId());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== EVENTS =====
|
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
public void onJoin(PostLoginEvent e) {
|
public void onJoin(PostLoginEvent e) {
|
||||||
ProxiedPlayer player = e.getPlayer();
|
ProxiedPlayer player = e.getPlayer();
|
||||||
|
|
||||||
// Verzögert zustellen damit der Spieler den Server-Wechsel abgeschlossen hat
|
|
||||||
plugin.getProxy().getScheduler().schedule(plugin, () -> {
|
plugin.getProxy().getScheduler().schedule(plugin, () -> {
|
||||||
if (player.isConnected()) {
|
if (player.isConnected()) deliverPending(player);
|
||||||
deliverPending(player);
|
|
||||||
}
|
|
||||||
}, loginDelaySeconds, TimeUnit.SECONDS);
|
}, loginDelaySeconds, TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== COMMANDS =====
|
// ===== COMMANDS =====
|
||||||
|
|
||||||
/**
|
|
||||||
* /forumlink <token> — Verknüpft den MC-Account mit dem Forum.
|
|
||||||
*/
|
|
||||||
private class ForumLinkCommand extends Command {
|
private class ForumLinkCommand extends Command {
|
||||||
|
public ForumLinkCommand() { super("forumlink", null, "fl"); }
|
||||||
public ForumLinkCommand() {
|
|
||||||
super("forumlink", null, "fl");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void execute(CommandSender sender, String[] args) {
|
public void execute(CommandSender sender, String[] args) {
|
||||||
if (!(sender instanceof ProxiedPlayer)) {
|
if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(new TextComponent("§cNur Spieler können diesen Befehl benutzen.")); return; }
|
||||||
sender.sendMessage(new TextComponent("§cNur Spieler können diesen Befehl benutzen."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ProxiedPlayer p = (ProxiedPlayer) sender;
|
ProxiedPlayer p = (ProxiedPlayer) sender;
|
||||||
|
|
||||||
if (args.length != 1) {
|
if (args.length != 1) {
|
||||||
p.sendMessage(new TextComponent("§eBenutzung: §f/forumlink <token>"));
|
p.sendMessage(new TextComponent("§eBenutzung: §f/forumlink <token>"));
|
||||||
p.sendMessage(new TextComponent("§7Den Token erhältst du in deinem Forum-Profil unter §fMinecraft-Verknüpfung§7."));
|
p.sendMessage(new TextComponent("§7Den Token erhältst du in deinem Forum-Profil unter §fMinecraft-Verknüpfung§7."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String token = args[0].trim().toUpperCase();
|
String token = args[0].trim().toUpperCase();
|
||||||
|
if (wpBaseUrl.isEmpty()) { p.sendMessage(new TextComponent("§cForum-Verknüpfung ist nicht konfiguriert.")); return; }
|
||||||
if (wpBaseUrl.isEmpty()) {
|
|
||||||
p.sendMessage(new TextComponent("§cForum-Verknüpfung ist nicht konfiguriert."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
p.sendMessage(new TextComponent("§7Überprüfe Token..."));
|
p.sendMessage(new TextComponent("§7Überprüfe Token..."));
|
||||||
|
|
||||||
// Asynchron an WordPress senden
|
|
||||||
plugin.getProxy().getScheduler().runAsync(plugin, () -> {
|
plugin.getProxy().getScheduler().runAsync(plugin, () -> {
|
||||||
try {
|
try {
|
||||||
String endpoint = wpBaseUrl + "/wp-json/mc-bridge/v1/verify-link";
|
String endpoint = wpBaseUrl + "/wp-json/mc-bridge/v1/verify-link";
|
||||||
String payload = "{\"token\":\"" + escapeJson(token) + "\","
|
String payload = "{\"token\":\"" + escapeJson(token) + "\","
|
||||||
+ "\"mc_uuid\":\"" + p.getUniqueId().toString() + "\","
|
+ "\"mc_uuid\":\"" + p.getUniqueId() + "\","
|
||||||
+ "\"mc_name\":\"" + escapeJson(p.getName()) + "\"}";
|
+ "\"mc_name\":\"" + escapeJson(p.getName()) + "\"}";
|
||||||
|
|
||||||
HttpURLConnection conn = (HttpURLConnection) new URL(endpoint).openConnection();
|
HttpURLConnection conn = (HttpURLConnection) new URL(endpoint).openConnection();
|
||||||
@@ -339,50 +208,32 @@ public class ForumBridgeModule implements Module, Listener {
|
|||||||
conn.setConnectTimeout(5000);
|
conn.setConnectTimeout(5000);
|
||||||
conn.setReadTimeout(7000);
|
conn.setReadTimeout(7000);
|
||||||
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
||||||
if (!apiSecret.isEmpty()) {
|
if (!apiSecret.isEmpty()) conn.setRequestProperty("X-Api-Key", apiSecret);
|
||||||
conn.setRequestProperty("X-Api-Key", apiSecret);
|
|
||||||
}
|
|
||||||
|
|
||||||
Charset utf8 = Charset.forName("UTF-8");
|
Charset utf8 = Charset.forName("UTF-8");
|
||||||
try (OutputStream os = conn.getOutputStream()) {
|
try (OutputStream os = conn.getOutputStream()) { os.write(payload.getBytes(utf8)); }
|
||||||
os.write(payload.getBytes(utf8));
|
|
||||||
}
|
|
||||||
|
|
||||||
int code = conn.getResponseCode();
|
int code = conn.getResponseCode();
|
||||||
String resp;
|
String resp = code >= 200 && code < 300
|
||||||
if (code >= 200 && code < 300) {
|
? streamToString(conn.getInputStream(), utf8)
|
||||||
resp = streamToString(conn.getInputStream(), utf8);
|
: streamToString(conn.getErrorStream(), utf8);
|
||||||
} else {
|
|
||||||
resp = streamToString(conn.getErrorStream(), utf8);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Antwort auswerten
|
|
||||||
if (resp != null && resp.contains("\"success\":true")) {
|
if (resp != null && resp.contains("\"success\":true")) {
|
||||||
String displayName = extractJsonString(resp, "display_name");
|
String displayName = extractJsonString(resp, "display_name");
|
||||||
String username = extractJsonString(resp, "username");
|
String username = extractJsonString(resp, "username");
|
||||||
String show = (displayName != null && !displayName.isEmpty()) ? displayName : username;
|
String show = !displayName.isEmpty() ? displayName : username;
|
||||||
|
|
||||||
p.sendMessage(new TextComponent("§8§m "));
|
p.sendMessage(new TextComponent("§8§m "));
|
||||||
p.sendMessage(new TextComponent("§a§l✓ §fForum-Account erfolgreich verknüpft!"));
|
p.sendMessage(new TextComponent("§a§l✓ §fForum-Account erfolgreich verknüpft!"));
|
||||||
if (show != null && !show.isEmpty()) {
|
if (!show.isEmpty()) p.sendMessage(new TextComponent("§7 Forum-User: §f" + show));
|
||||||
p.sendMessage(new TextComponent("§7 Forum-User: §f" + show));
|
|
||||||
}
|
|
||||||
p.sendMessage(new TextComponent("§7 Du erhältst jetzt Ingame-Benachrichtigungen."));
|
p.sendMessage(new TextComponent("§7 Du erhältst jetzt Ingame-Benachrichtigungen."));
|
||||||
p.sendMessage(new TextComponent("§8§m "));
|
p.sendMessage(new TextComponent("§8§m "));
|
||||||
} else {
|
} else {
|
||||||
// Fehlermeldung auslesen
|
String error = extractJsonString(resp, "error");
|
||||||
String error = extractJsonString(resp, "error");
|
|
||||||
String message = extractJsonString(resp, "message");
|
String message = extractJsonString(resp, "message");
|
||||||
|
if ("token_expired".equals(error)) p.sendMessage(new TextComponent("§c✗ Der Token ist abgelaufen."));
|
||||||
if ("token_expired".equals(error)) {
|
else if ("uuid_already_linked".equals(error)) p.sendMessage(new TextComponent("§c✗ " + (!message.isEmpty() ? message : "Diese UUID ist bereits verknüpft.")));
|
||||||
p.sendMessage(new TextComponent("§c✗ Der Token ist abgelaufen. Generiere einen neuen im Forum."));
|
else if ("invalid_token".equals(error)) p.sendMessage(new TextComponent("§c✗ Ungültiger Token."));
|
||||||
} else if ("uuid_already_linked".equals(error)) {
|
else p.sendMessage(new TextComponent("§c✗ Verknüpfung fehlgeschlagen: " + (!error.isEmpty() ? error : "Unbekannter Fehler")));
|
||||||
p.sendMessage(new TextComponent("§c✗ " + (message != null ? message : "Diese UUID ist bereits verknüpft.")));
|
|
||||||
} else if ("invalid_token".equals(error)) {
|
|
||||||
p.sendMessage(new TextComponent("§c✗ Ungültiger Token. Prüfe die Eingabe oder generiere einen neuen."));
|
|
||||||
} else {
|
|
||||||
p.sendMessage(new TextComponent("§c✗ Verknüpfung fehlgeschlagen: " + (error != null ? error : "Unbekannter Fehler")));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
p.sendMessage(new TextComponent("§c✗ Fehler bei der Verbindung zum Forum."));
|
p.sendMessage(new TextComponent("§c✗ Fehler bei der Verbindung zum Forum."));
|
||||||
@@ -392,34 +243,21 @@ public class ForumBridgeModule implements Module, Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* /forum — Zeigt ausstehende Forum-Benachrichtigungen an.
|
|
||||||
*/
|
|
||||||
private class ForumCommand extends Command {
|
private class ForumCommand extends Command {
|
||||||
|
public ForumCommand() { super("forum"); }
|
||||||
public ForumCommand() {
|
|
||||||
super("forum");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void execute(CommandSender sender, String[] args) {
|
public void execute(CommandSender sender, String[] args) {
|
||||||
if (!(sender instanceof ProxiedPlayer)) {
|
if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(new TextComponent("§cNur Spieler können diesen Befehl benutzen.")); return; }
|
||||||
sender.sendMessage(new TextComponent("§cNur Spieler können diesen Befehl benutzen."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ProxiedPlayer p = (ProxiedPlayer) sender;
|
ProxiedPlayer p = (ProxiedPlayer) sender;
|
||||||
List<ForumNotification> pending = storage.getPending(p.getUniqueId());
|
List<ForumNotification> pending = storage.getPending(p.getUniqueId());
|
||||||
|
|
||||||
if (pending.isEmpty()) {
|
if (pending.isEmpty()) {
|
||||||
p.sendMessage(new TextComponent("§7Keine neuen Forum-Benachrichtigungen."));
|
p.sendMessage(new TextComponent("§7Keine neuen Forum-Benachrichtigungen."));
|
||||||
|
|
||||||
// Forum-Link anzeigen wenn konfiguriert
|
|
||||||
if (!wpBaseUrl.isEmpty()) {
|
if (!wpBaseUrl.isEmpty()) {
|
||||||
TextComponent link = new TextComponent("§a➜ Forum öffnen");
|
TextComponent link = new TextComponent("§a➜ Forum öffnen");
|
||||||
link.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, wpBaseUrl));
|
link.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, wpBaseUrl));
|
||||||
link.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
|
link.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("§7Klicke um das Forum zu öffnen").create()));
|
||||||
new ComponentBuilder("§7Klicke um das Forum zu öffnen").create()));
|
|
||||||
p.sendMessage(link);
|
p.sendMessage(link);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -428,39 +266,22 @@ public class ForumBridgeModule implements Module, Listener {
|
|||||||
p.sendMessage(new TextComponent("§8§m "));
|
p.sendMessage(new TextComponent("§8§m "));
|
||||||
p.sendMessage(new TextComponent("§6§l✉ Forum-Benachrichtigungen §8(§f" + pending.size() + "§8)"));
|
p.sendMessage(new TextComponent("§6§l✉ Forum-Benachrichtigungen §8(§f" + pending.size() + "§8)"));
|
||||||
p.sendMessage(new TextComponent(""));
|
p.sendMessage(new TextComponent(""));
|
||||||
|
|
||||||
int shown = 0;
|
int shown = 0;
|
||||||
for (ForumNotification n : pending) {
|
for (ForumNotification n : pending) {
|
||||||
if (shown >= 10) {
|
if (shown >= 10) { p.sendMessage(new TextComponent("§7 ... und " + (pending.size() - 10) + " weitere")); break; }
|
||||||
p.sendMessage(new TextComponent("§7 ... und " + (pending.size() - 10) + " weitere"));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
String color = n.getTypeColor();
|
String color = n.getTypeColor();
|
||||||
TextComponent line = new TextComponent(color + " • " + n.getTypeLabel() + "§7: ");
|
TextComponent line = new TextComponent(color + " • " + n.getTypeLabel() + "§7: ");
|
||||||
|
TextComponent detail = new TextComponent(!n.getTitle().isEmpty() ? "§f" + n.getTitle() : "§fvon " + n.getAuthor());
|
||||||
TextComponent detail;
|
|
||||||
if (!n.getTitle().isEmpty()) {
|
|
||||||
detail = new TextComponent("§f" + n.getTitle());
|
|
||||||
} else {
|
|
||||||
detail = new TextComponent("§fvon " + n.getAuthor());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!n.getUrl().isEmpty()) {
|
if (!n.getUrl().isEmpty()) {
|
||||||
detail.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, n.getUrl()));
|
detail.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, n.getUrl()));
|
||||||
detail.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
|
detail.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("§7Klicke zum Öffnen").create()));
|
||||||
new ComponentBuilder("§7Klicke zum Öffnen").create()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
line.addExtra(detail);
|
line.addExtra(detail);
|
||||||
p.sendMessage(line);
|
p.sendMessage(line);
|
||||||
shown++;
|
shown++;
|
||||||
}
|
}
|
||||||
|
|
||||||
p.sendMessage(new TextComponent(""));
|
p.sendMessage(new TextComponent(""));
|
||||||
p.sendMessage(new TextComponent("§8§m "));
|
p.sendMessage(new TextComponent("§8§m "));
|
||||||
|
|
||||||
// Alle als gelesen markieren
|
|
||||||
storage.markAllDelivered(p.getUniqueId());
|
storage.markAllDelivered(p.getUniqueId());
|
||||||
storage.clearDelivered(p.getUniqueId());
|
storage.clearDelivered(p.getUniqueId());
|
||||||
}
|
}
|
||||||
@@ -468,21 +289,22 @@ public class ForumBridgeModule implements Module, Listener {
|
|||||||
|
|
||||||
// ===== HELPER =====
|
// ===== HELPER =====
|
||||||
|
|
||||||
/** Getter für den Storage (für StatusAPI HTTP-Handler) */
|
public ForumNotifStorage getStorage() { return storage; }
|
||||||
public ForumNotifStorage getStorage() {
|
|
||||||
return storage;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIX #13: Gibt immer einen leeren String zurück, niemals null.
|
||||||
|
* Verhindert NullPointerExceptions in allen Aufrufern.
|
||||||
|
*/
|
||||||
private static String extractJsonString(String json, String key) {
|
private static String extractJsonString(String json, String key) {
|
||||||
if (json == null || key == null) return null;
|
if (json == null || key == null) return "";
|
||||||
String search = "\"" + key + "\"";
|
String search = "\"" + key + "\"";
|
||||||
int idx = json.indexOf(search);
|
int idx = json.indexOf(search);
|
||||||
if (idx < 0) return null;
|
if (idx < 0) return "";
|
||||||
int colon = json.indexOf(':', idx + search.length());
|
int colon = json.indexOf(':', idx + search.length());
|
||||||
if (colon < 0) return null;
|
if (colon < 0) return "";
|
||||||
int i = colon + 1;
|
int i = colon + 1;
|
||||||
while (i < json.length() && Character.isWhitespace(json.charAt(i))) i++;
|
while (i < json.length() && Character.isWhitespace(json.charAt(i))) i++;
|
||||||
if (i >= json.length()) return null;
|
if (i >= json.length()) return "";
|
||||||
char c = json.charAt(i);
|
char c = json.charAt(i);
|
||||||
if (c == '"') {
|
if (c == '"') {
|
||||||
i++;
|
i++;
|
||||||
@@ -491,15 +313,11 @@ public class ForumBridgeModule implements Module, Listener {
|
|||||||
while (i < json.length()) {
|
while (i < json.length()) {
|
||||||
char ch = json.charAt(i++);
|
char ch = json.charAt(i++);
|
||||||
if (escape) { sb.append(ch); escape = false; }
|
if (escape) { sb.append(ch); escape = false; }
|
||||||
else {
|
else { if (ch == '\\') escape = true; else if (ch == '"') break; else sb.append(ch); }
|
||||||
if (ch == '\\') escape = true;
|
|
||||||
else if (ch == '"') break;
|
|
||||||
else sb.append(ch);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
return null;
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String escapeJson(String s) {
|
private static String escapeJson(String s) {
|
||||||
@@ -510,8 +328,7 @@ public class ForumBridgeModule implements Module, Listener {
|
|||||||
private static String streamToString(InputStream in, Charset charset) throws IOException {
|
private static String streamToString(InputStream in, Charset charset) throws IOException {
|
||||||
if (in == null) return "";
|
if (in == null) return "";
|
||||||
try (BufferedReader br = new BufferedReader(new InputStreamReader(in, charset))) {
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(in, charset))) {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder(); String line;
|
||||||
String line;
|
|
||||||
while ((line = br.readLine()) != null) sb.append(line);
|
while ((line = br.readLine()) != null) sb.append(line);
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ public class NetworkInfoModule implements Module {
|
|||||||
loadConfig();
|
loadConfig();
|
||||||
|
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
plugin.getLogger().info("[NetworkInfoModule] deaktiviert via " + CONFIG_FILE_NAME + " (networkinfo.enabled=false)");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ public class NetworkInfoModule implements Module {
|
|||||||
alertTask = ProxyServer.getInstance().getScheduler().schedule(plugin, this::evaluateAndSendAlerts, interval, interval, TimeUnit.SECONDS);
|
alertTask = ProxyServer.getInstance().getScheduler().schedule(plugin, this::evaluateAndSendAlerts, interval, interval, TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
plugin.getLogger().info("[NetworkInfoModule] aktiviert. commandEnabled=" + commandEnabled + ", includePlayerNames=" + includePlayerNames + ", webhookEnabled=" + webhookEnabled + ", notifyStartStop=" + webhookNotifyStartStop + ", webhookUrlPresent=" + !webhookUrl.isEmpty());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -514,7 +514,7 @@ public class NetworkInfoModule implements Module {
|
|||||||
while ((read = in.read(buffer)) != -1) {
|
while ((read = in.read(buffer)) != -1) {
|
||||||
out.write(buffer, 0, read);
|
out.write(buffer, 0, read);
|
||||||
}
|
}
|
||||||
plugin.getLogger().info("[NetworkInfoModule] " + CONFIG_FILE_NAME + " wurde erstellt.");
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
plugin.getLogger().warning("[NetworkInfoModule] Konnte " + CONFIG_FILE_NAME + " nicht erstellen: " + e.getMessage());
|
plugin.getLogger().warning("[NetworkInfoModule] Konnte " + CONFIG_FILE_NAME + " nicht erstellen: " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
|||||||
269
src/main/java/net/viper/status/modules/vanish/VanishModule.java
Normal file
269
src/main/java/net/viper/status/modules/vanish/VanishModule.java
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,248 +1,190 @@
|
|||||||
package net.viper.status.modules.verify;
|
package net.viper.status.modules.verify;
|
||||||
|
|
||||||
import net.md_5.bungee.api.ChatColor;
|
import net.md_5.bungee.api.ChatColor;
|
||||||
import net.md_5.bungee.api.CommandSender;
|
import net.md_5.bungee.api.CommandSender;
|
||||||
import net.md_5.bungee.api.ProxyServer;
|
import net.md_5.bungee.api.ProxyServer;
|
||||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||||
import net.md_5.bungee.api.plugin.Command;
|
import net.md_5.bungee.api.plugin.Command;
|
||||||
import net.md_5.bungee.api.plugin.Plugin;
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
import net.viper.status.module.Module;
|
import net.viper.status.module.Module;
|
||||||
|
|
||||||
import javax.crypto.Mac;
|
import javax.crypto.Mac;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* VerifyModule: Multi-Server Support.
|
* VerifyModule: Multi-Server Support.
|
||||||
* Liest pro Server die passende ID und das Secret aus der verify.properties.
|
*
|
||||||
*/
|
* Fix #7: Servernamen werden jetzt case-insensitiv verglichen.
|
||||||
public class VerifyModule implements Module {
|
* Keys in serverConfigs werden beim Laden auf lowercase normalisiert
|
||||||
|
* und die Suche erfolgt ebenfalls lowercase.
|
||||||
private String wpVerifyUrl;
|
*/
|
||||||
// Speichert für jeden Servernamen (z.B. "Lobby") die passende Konfiguration
|
public class VerifyModule implements Module {
|
||||||
private final Map<String, ServerConfig> serverConfigs = new HashMap<>();
|
|
||||||
|
private String wpVerifyUrl;
|
||||||
@Override
|
// Keys sind lowercase normalisiert für case-insensitiven Vergleich
|
||||||
public String getName() {
|
private final Map<String, ServerConfig> serverConfigs = new HashMap<>();
|
||||||
return "VerifyModule";
|
|
||||||
}
|
@Override
|
||||||
|
public String getName() { return "VerifyModule"; }
|
||||||
@Override
|
|
||||||
public void onEnable(Plugin plugin) {
|
@Override
|
||||||
loadConfig(plugin);
|
public void onEnable(Plugin plugin) {
|
||||||
|
loadConfig(plugin);
|
||||||
// Befehl registrieren
|
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new VerifyCommand());
|
||||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new VerifyCommand());
|
plugin.getLogger().info("VerifyModule aktiviert. " + serverConfigs.size() + " Server-Konfigurationen geladen.");
|
||||||
|
}
|
||||||
plugin.getLogger().info("VerifyModule aktiviert. " + serverConfigs.size() + " Server-Konfigurationen geladen.");
|
|
||||||
}
|
@Override
|
||||||
|
public void onDisable(Plugin plugin) {}
|
||||||
@Override
|
|
||||||
public void onDisable(Plugin plugin) {
|
private void loadConfig(Plugin plugin) {
|
||||||
// Befehl muss nicht manuell entfernt werden, BungeeCord übernimmt das beim Plugin-Stop
|
String fileName = "verify.properties";
|
||||||
}
|
File configFile = new File(plugin.getDataFolder(), fileName);
|
||||||
|
Properties props = new Properties();
|
||||||
// --- Konfiguration Laden & Kopieren ---
|
|
||||||
private void loadConfig(Plugin plugin) {
|
if (!configFile.exists()) {
|
||||||
String fileName = "verify.properties";
|
plugin.getDataFolder().mkdirs();
|
||||||
File configFile = new File(plugin.getDataFolder(), fileName);
|
try (InputStream in = plugin.getResourceAsStream(fileName);
|
||||||
Properties props = new Properties();
|
OutputStream out = new FileOutputStream(configFile)) {
|
||||||
|
if (in == null) { plugin.getLogger().warning("Standard-config '" + fileName + "' nicht in JAR."); return; }
|
||||||
// 1. Datei kopieren, falls sie noch nicht existiert
|
byte[] buffer = new byte[1024]; int length;
|
||||||
if (!configFile.exists()) {
|
while ((length = in.read(buffer)) > 0) out.write(buffer, 0, length);
|
||||||
plugin.getDataFolder().mkdirs();
|
plugin.getLogger().info("Konfigurationsdatei '" + fileName + "' erstellt.");
|
||||||
try (InputStream in = plugin.getResourceAsStream(fileName);
|
} catch (Exception e) { plugin.getLogger().severe("Fehler beim Erstellen der Config: " + e.getMessage()); return; }
|
||||||
OutputStream out = new FileOutputStream(configFile)) {
|
}
|
||||||
if (in == null) {
|
|
||||||
plugin.getLogger().warning("Standard-config '" + fileName + "' nicht in JAR gefunden. Erstelle manuell.");
|
try (InputStream in = new FileInputStream(configFile)) {
|
||||||
return;
|
props.load(in);
|
||||||
}
|
} catch (IOException e) { e.printStackTrace(); return; }
|
||||||
byte[] buffer = new byte[1024];
|
|
||||||
int length;
|
this.wpVerifyUrl = props.getProperty("wp_verify_url", "https://deine-wp-domain.tld");
|
||||||
while ((length = in.read(buffer)) > 0) {
|
|
||||||
out.write(buffer, 0, length);
|
// FIX #7: Keys beim Laden auf lowercase normalisieren
|
||||||
}
|
this.serverConfigs.clear();
|
||||||
plugin.getLogger().info("Konfigurationsdatei '" + fileName + "' erstellt.");
|
for (String key : props.stringPropertyNames()) {
|
||||||
} catch (Exception e) {
|
if (key.startsWith("server.")) {
|
||||||
plugin.getLogger().severe("Fehler beim Erstellen der Config: " + e.getMessage());
|
String[] parts = key.split("\\.");
|
||||||
return;
|
if (parts.length == 3) {
|
||||||
}
|
// Servername lowercase → case-insensitiver Lookup
|
||||||
}
|
String serverName = parts[1].toLowerCase();
|
||||||
|
String type = parts[2];
|
||||||
// 2. Eigentliche Config laden
|
ServerConfig config = serverConfigs.computeIfAbsent(serverName, k -> new ServerConfig());
|
||||||
try (InputStream in = new FileInputStream(configFile)) {
|
if ("id".equalsIgnoreCase(type)) {
|
||||||
props.load(in);
|
try { config.serverId = Integer.parseInt(props.getProperty(key)); }
|
||||||
} catch (IOException e) {
|
catch (NumberFormatException e) { plugin.getLogger().warning("Ungültige Server ID für " + serverName); }
|
||||||
e.printStackTrace();
|
} else if ("secret".equalsIgnoreCase(type)) {
|
||||||
return;
|
config.sharedSecret = props.getProperty(key);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Globale URL
|
}
|
||||||
this.wpVerifyUrl = props.getProperty("wp_verify_url", "https://deine-wp-domain.tld");
|
}
|
||||||
|
}
|
||||||
// Server-Configs parsen (z.B. server.Lobby.id)
|
|
||||||
this.serverConfigs.clear();
|
private static class ServerConfig {
|
||||||
for (String key : props.stringPropertyNames()) {
|
int serverId = 0;
|
||||||
if (key.startsWith("server.")) {
|
String sharedSecret = "";
|
||||||
// Key Struktur: server.<ServerName>.id oder .secret
|
}
|
||||||
String[] parts = key.split("\\.");
|
|
||||||
if (parts.length == 3) {
|
private class VerifyCommand extends Command {
|
||||||
String serverName = parts[1];
|
public VerifyCommand() { super("verify"); }
|
||||||
String type = parts[2];
|
|
||||||
|
@Override
|
||||||
// Eintrag in der Map erstellen oder holen
|
public void execute(CommandSender sender, String[] args) {
|
||||||
ServerConfig config = serverConfigs.computeIfAbsent(serverName, k -> new ServerConfig());
|
if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(ChatColor.RED + "Nur Spieler können diesen Befehl benutzen."); return; }
|
||||||
|
ProxiedPlayer p = (ProxiedPlayer) sender;
|
||||||
if ("id".equalsIgnoreCase(type)) {
|
if (args.length != 1) { p.sendMessage(ChatColor.YELLOW + "Benutzung: /verify <token>"); return; }
|
||||||
try {
|
|
||||||
config.serverId = Integer.parseInt(props.getProperty(key));
|
// FIX #7: Servername lowercase für case-insensitiven Lookup
|
||||||
} catch (NumberFormatException e) {
|
String serverName = p.getServer().getInfo().getName().toLowerCase();
|
||||||
plugin.getLogger().warning("Ungültige Server ID für " + serverName);
|
ServerConfig config = serverConfigs.get(serverName);
|
||||||
}
|
|
||||||
} else if ("secret".equalsIgnoreCase(type)) {
|
if (config == null || config.serverId == 0 || config.sharedSecret.isEmpty()) {
|
||||||
config.sharedSecret = props.getProperty(key);
|
p.sendMessage(ChatColor.RED + "✗ Dieser Server ist nicht in der Verify-Konfiguration hinterlegt.");
|
||||||
}
|
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();
|
||||||
// --- Hilfsklasse für die Daten eines Servers ---
|
String playerName = p.getName();
|
||||||
private static class ServerConfig {
|
|
||||||
int serverId = 0;
|
HttpURLConnection conn = null;
|
||||||
String sharedSecret = "";
|
try {
|
||||||
}
|
Charset utf8 = Charset.forName("UTF-8");
|
||||||
|
String signature = hmacSHA256(playerName + token, config.sharedSecret, utf8);
|
||||||
// --- Die Command Klasse ---
|
String payload = "{\"player\":\"" + escapeJson(playerName)
|
||||||
private class VerifyCommand extends Command {
|
+ "\",\"token\":\"" + escapeJson(token)
|
||||||
|
+ "\",\"server_id\":" + config.serverId
|
||||||
public VerifyCommand() {
|
+ ",\"signature\":\"" + signature + "\"}";
|
||||||
super("verify");
|
|
||||||
}
|
URL url = new URL(wpVerifyUrl + "/wp-json/mc-gallery/v1/verify");
|
||||||
|
conn = (HttpURLConnection) url.openConnection();
|
||||||
@Override
|
conn.setConnectTimeout(5000);
|
||||||
public void execute(CommandSender sender, String[] args) {
|
conn.setReadTimeout(7000);
|
||||||
if (!(sender instanceof ProxiedPlayer)) {
|
conn.setDoOutput(true);
|
||||||
sender.sendMessage(ChatColor.RED + "Nur Spieler können diesen Befehl benutzen.");
|
conn.setRequestMethod("POST");
|
||||||
return;
|
conn.setRequestProperty("Content-Type", "application/json; charset=utf-8");
|
||||||
}
|
try (OutputStream os = conn.getOutputStream()) { os.write(payload.getBytes(utf8)); }
|
||||||
|
|
||||||
ProxiedPlayer p = (ProxiedPlayer) sender;
|
int code = conn.getResponseCode();
|
||||||
if (args.length != 1) {
|
String resp = code >= 200 && code < 300
|
||||||
p.sendMessage(ChatColor.YELLOW + "Benutzung: /verify <token>");
|
? streamToString(conn.getInputStream(), utf8)
|
||||||
return;
|
: streamToString(conn.getErrorStream(), utf8);
|
||||||
}
|
|
||||||
|
if (resp != null && !resp.isEmpty() && resp.trim().startsWith("{")) {
|
||||||
// --- WICHTIG: Servernamen ermitteln ---
|
boolean isSuccess = resp.contains("\"success\":true");
|
||||||
String serverName = p.getServer().getInfo().getName();
|
String message = "Ein unbekannter Fehler ist aufgetreten.";
|
||||||
|
int keyIndex = resp.indexOf("\"message\":\"");
|
||||||
// Konfiguration für diesen Server laden
|
if (keyIndex != -1) {
|
||||||
ServerConfig config = serverConfigs.get(serverName);
|
int startIndex = keyIndex + 11;
|
||||||
|
int endIndex = resp.indexOf("\"", startIndex);
|
||||||
// Check ob Konfig existiert
|
if (endIndex != -1) message = resp.substring(startIndex, endIndex);
|
||||||
if (config == null || config.serverId == 0 || config.sharedSecret.isEmpty()) {
|
}
|
||||||
p.sendMessage(ChatColor.RED + "✗ Dieser Server ist nicht in der Verify-Konfiguration hinterlegt.");
|
if (isSuccess) {
|
||||||
p.sendMessage(ChatColor.GRAY + "Aktueller Servername: " + ChatColor.WHITE + serverName);
|
p.sendMessage(ChatColor.GREEN + "✓ " + message);
|
||||||
p.sendMessage(ChatColor.GRAY + "Bitte kontaktiere einen Admin.");
|
p.sendMessage(ChatColor.GRAY + "Du kannst nun Bilder hochladen!");
|
||||||
return;
|
} else {
|
||||||
}
|
p.sendMessage(ChatColor.RED + "✗ " + message);
|
||||||
|
}
|
||||||
String token = args[0].trim();
|
} else {
|
||||||
String playerName = p.getName();
|
p.sendMessage(ChatColor.RED + "✗ Fehler beim Verbinden mit der Webseite (Code: " + code + ")");
|
||||||
|
}
|
||||||
HttpURLConnection conn = null;
|
} catch (Exception ex) {
|
||||||
try {
|
p.sendMessage(ChatColor.RED + "✗ Ein interner Fehler ist aufgetreten.");
|
||||||
Charset utf8 = Charset.forName("UTF-8");
|
ProxyServer.getInstance().getLogger().warning("Verify error: " + ex.getMessage());
|
||||||
// Wir signieren Name + Token mit dem SERVER-SPECIFISCHEN Secret
|
ex.printStackTrace();
|
||||||
String signature = hmacSHA256(playerName + token, config.sharedSecret, utf8);
|
} finally {
|
||||||
|
if (conn != null) conn.disconnect();
|
||||||
// Payload aufbauen mit der SERVER-SPECIFISCHEN ID
|
}
|
||||||
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();
|
private static String hmacSHA256(String data, String key, Charset charset) throws Exception {
|
||||||
conn.setConnectTimeout(5000);
|
Mac mac = Mac.getInstance("HmacSHA256");
|
||||||
conn.setReadTimeout(7000);
|
mac.init(new SecretKeySpec(key.getBytes(charset), "HmacSHA256"));
|
||||||
conn.setDoOutput(true);
|
byte[] raw = mac.doFinal(data.getBytes(charset));
|
||||||
conn.setRequestMethod("POST");
|
StringBuilder sb = new StringBuilder();
|
||||||
conn.setRequestProperty("Content-Type", "application/json; charset=utf-8");
|
for (byte b : raw) sb.append(String.format("%02x", b));
|
||||||
|
return sb.toString();
|
||||||
try (OutputStream os = conn.getOutputStream()) {
|
}
|
||||||
os.write(payload.getBytes(utf8));
|
|
||||||
}
|
private static String streamToString(InputStream in, Charset charset) throws IOException {
|
||||||
|
if (in == null) return "";
|
||||||
int code = conn.getResponseCode();
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(in, charset))) {
|
||||||
String resp;
|
StringBuilder sb = new StringBuilder(); String line;
|
||||||
|
while ((line = br.readLine()) != null) sb.append(line);
|
||||||
if (code >= 200 && code < 300) {
|
return sb.toString();
|
||||||
resp = streamToString(conn.getInputStream(), utf8);
|
}
|
||||||
} else {
|
}
|
||||||
resp = streamToString(conn.getErrorStream(), utf8);
|
|
||||||
}
|
private static String escapeJson(String s) {
|
||||||
|
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r");
|
||||||
// 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 (isSuccess) {
|
|
||||||
p.sendMessage(ChatColor.GREEN + "✓ " + message);
|
|
||||||
p.sendMessage(ChatColor.GRAY + "Du kannst nun Bilder hochladen!");
|
|
||||||
} else {
|
|
||||||
p.sendMessage(ChatColor.RED + "✗ " + message);
|
|
||||||
}
|
|
||||||
} 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 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"));
|
|
||||||
byte[] raw = mac.doFinal(data.getBytes(charset));
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
for (byte b : raw) sb.append(String.format("%02x", b));
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,6 +11,20 @@ public class PlayerStats {
|
|||||||
public long currentSessionStart;
|
public long currentSessionStart;
|
||||||
public int joins;
|
public int joins;
|
||||||
|
|
||||||
|
// Economy
|
||||||
|
public double balance;
|
||||||
|
public double totalEarned;
|
||||||
|
public double totalSpent;
|
||||||
|
public int transactionsCount;
|
||||||
|
|
||||||
|
// Punishments
|
||||||
|
public int bansCount;
|
||||||
|
public int mutesCount;
|
||||||
|
public int warnsCount;
|
||||||
|
public long lastPunishmentAt; // Unix-Timestamp (Sek.), 0 = nie
|
||||||
|
public String lastPunishmentType; // "ban", "mute", "warn", "kick", "" = nie
|
||||||
|
public int punishmentScore;
|
||||||
|
|
||||||
public PlayerStats(UUID uuid, String name) {
|
public PlayerStats(UUID uuid, String name) {
|
||||||
this.uuid = uuid;
|
this.uuid = uuid;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
@@ -20,6 +34,16 @@ public class PlayerStats {
|
|||||||
this.totalPlaytime = 0;
|
this.totalPlaytime = 0;
|
||||||
this.currentSessionStart = 0;
|
this.currentSessionStart = 0;
|
||||||
this.joins = 0;
|
this.joins = 0;
|
||||||
|
this.balance = 0.0;
|
||||||
|
this.totalEarned = 0.0;
|
||||||
|
this.totalSpent = 0.0;
|
||||||
|
this.transactionsCount = 0;
|
||||||
|
this.bansCount = 0;
|
||||||
|
this.mutesCount = 0;
|
||||||
|
this.warnsCount = 0;
|
||||||
|
this.lastPunishmentAt = 0;
|
||||||
|
this.lastPunishmentType = "";
|
||||||
|
this.punishmentScore = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void onJoin() {
|
public synchronized void onJoin() {
|
||||||
@@ -47,7 +71,10 @@ public class PlayerStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public synchronized String toLine() {
|
public synchronized String toLine() {
|
||||||
return uuid + "|" + name.replace("|", "_") + "|" + firstSeen + "|" + lastSeen + "|" + totalPlaytime + "|" + currentSessionStart + "|" + joins;
|
String safeType = (lastPunishmentType == null ? "" : lastPunishmentType).replace("|", "_");
|
||||||
|
return uuid + "|" + name.replace("|", "_") + "|" + firstSeen + "|" + lastSeen + "|" + totalPlaytime + "|" + currentSessionStart + "|" + joins
|
||||||
|
+ "|" + balance + "|" + totalEarned + "|" + totalSpent + "|" + transactionsCount
|
||||||
|
+ "|" + bansCount + "|" + mutesCount + "|" + warnsCount + "|" + lastPunishmentAt + "|" + safeType + "|" + punishmentScore;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PlayerStats fromLine(String line) {
|
public static PlayerStats fromLine(String line) {
|
||||||
@@ -62,6 +89,22 @@ public class PlayerStats {
|
|||||||
ps.totalPlaytime = Long.parseLong(parts[4]);
|
ps.totalPlaytime = Long.parseLong(parts[4]);
|
||||||
ps.currentSessionStart = Long.parseLong(parts[5]);
|
ps.currentSessionStart = Long.parseLong(parts[5]);
|
||||||
ps.joins = Integer.parseInt(parts[6]);
|
ps.joins = Integer.parseInt(parts[6]);
|
||||||
|
// Economy (felder 7-10)
|
||||||
|
if (parts.length >= 11) {
|
||||||
|
try { ps.balance = Double.parseDouble(parts[7]); } catch (Exception ignored) {}
|
||||||
|
try { ps.totalEarned = Double.parseDouble(parts[8]); } catch (Exception ignored) {}
|
||||||
|
try { ps.totalSpent = Double.parseDouble(parts[9]); } catch (Exception ignored) {}
|
||||||
|
try { ps.transactionsCount = Integer.parseInt(parts[10]); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
// Punishments (felder 11-16)
|
||||||
|
if (parts.length >= 17) {
|
||||||
|
try { ps.bansCount = Integer.parseInt(parts[11]); } catch (Exception ignored) {}
|
||||||
|
try { ps.mutesCount = Integer.parseInt(parts[12]); } catch (Exception ignored) {}
|
||||||
|
try { ps.warnsCount = Integer.parseInt(parts[13]); } catch (Exception ignored) {}
|
||||||
|
try { ps.lastPunishmentAt = Long.parseLong(parts[14]); } catch (Exception ignored) {}
|
||||||
|
ps.lastPunishmentType = parts[15];
|
||||||
|
try { ps.punishmentScore = Integer.parseInt(parts[16]); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
return ps;
|
return ps;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ public class StatsModule implements Module, Listener {
|
|||||||
// Laden
|
// Laden
|
||||||
try {
|
try {
|
||||||
storage.load(manager);
|
storage.load(manager);
|
||||||
plugin.getLogger().info("Player-Stats wurden erfolgreich geladen.");
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
plugin.getLogger().warning("Fehler beim Laden der Stats: " + e.getMessage());
|
plugin.getLogger().warning("Fehler beim Laden der Stats: " + e.getMessage());
|
||||||
}
|
}
|
||||||
@@ -44,7 +43,6 @@ public class StatsModule implements Module, Listener {
|
|||||||
plugin.getProxy().getScheduler().schedule(plugin, () -> {
|
plugin.getProxy().getScheduler().schedule(plugin, () -> {
|
||||||
try {
|
try {
|
||||||
storage.save(manager);
|
storage.save(manager);
|
||||||
plugin.getLogger().info("Auto-Save: Player-Stats gespeichert.");
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
plugin.getLogger().warning("Fehler beim Auto-Save: " + e.getMessage());
|
plugin.getLogger().warning("Fehler beim Auto-Save: " + e.getMessage());
|
||||||
}
|
}
|
||||||
@@ -69,7 +67,6 @@ public class StatsModule implements Module, Listener {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
storage.save(manager);
|
storage.save(manager);
|
||||||
plugin.getLogger().info("Player-Stats beim Shutdown gespeichert.");
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
plugin.getLogger().warning("Fehler beim Speichern (Shutdown): " + e.getMessage());
|
plugin.getLogger().warning("Fehler beim Speichern (Shutdown): " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,46 @@
|
|||||||
package net.viper.status.stats;
|
package net.viper.status.stats;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
|
|
||||||
public class StatsStorage {
|
/**
|
||||||
private final File file;
|
* Fix #9: save() und load() sind jetzt synchronized um Race Conditions
|
||||||
|
* zwischen Auto-Save-Task und Shutdown-Aufruf zu verhindern.
|
||||||
public StatsStorage(File pluginFolder) {
|
*/
|
||||||
if (!pluginFolder.exists()) pluginFolder.mkdirs();
|
public class StatsStorage {
|
||||||
this.file = new File(pluginFolder, "stats.dat");
|
private final File file;
|
||||||
}
|
private final Object fileLock = new Object();
|
||||||
|
|
||||||
public void save(StatsManager manager) {
|
public StatsStorage(File pluginFolder) {
|
||||||
try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
|
if (!pluginFolder.exists()) pluginFolder.mkdirs();
|
||||||
for (PlayerStats ps : manager.all()) {
|
this.file = new File(pluginFolder, "stats.dat");
|
||||||
bw.write(ps.toLine());
|
}
|
||||||
bw.newLine();
|
|
||||||
}
|
public void save(StatsManager manager) {
|
||||||
bw.flush();
|
synchronized (fileLock) {
|
||||||
} catch (IOException e) {
|
try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
|
||||||
e.printStackTrace();
|
for (PlayerStats ps : manager.all()) {
|
||||||
}
|
bw.write(ps.toLine());
|
||||||
}
|
bw.newLine();
|
||||||
|
}
|
||||||
public void load(StatsManager manager) {
|
bw.flush();
|
||||||
if (!file.exists()) return;
|
} catch (IOException e) {
|
||||||
try (BufferedReader br = new BufferedReader(new FileReader(file))) {
|
e.printStackTrace();
|
||||||
String line;
|
}
|
||||||
while ((line = br.readLine()) != null) {
|
}
|
||||||
PlayerStats ps = PlayerStats.fromLine(line);
|
}
|
||||||
if (ps != null) manager.put(ps);
|
|
||||||
}
|
public void load(StatsManager manager) {
|
||||||
} catch (IOException e) {
|
if (!file.exists()) return;
|
||||||
e.printStackTrace();
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -135,6 +135,30 @@ private-messages:
|
|||||||
format-social-spy: "&8[&dSPY &7{sender} &8→ &7{receiver}&8] &f{message}"
|
format-social-spy: "&8[&dSPY &7{sender} &8→ &7{receiver}&8] &f{message}"
|
||||||
social-spy-permission: "chat.socialspy"
|
social-spy-permission: "chat.socialspy"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# JOIN / LEAVE NACHRICHTEN
|
||||||
|
# Platzhalter:
|
||||||
|
# {player} - Spielername
|
||||||
|
# {prefix} - LuckPerms Prefix
|
||||||
|
# {suffix} - LuckPerms Suffix
|
||||||
|
# {server} - Zuletzt bekannter Server (bei Leave) oder "Netzwerk"
|
||||||
|
# ============================================================
|
||||||
|
join-leave:
|
||||||
|
enabled: true
|
||||||
|
# Normale Join/Leave-Nachrichten (für alle sichtbar)
|
||||||
|
join-format: "&8[&a+&8] {prefix}&a{player}&r &7hat das Netzwerk betreten."
|
||||||
|
leave-format: "&8[&c-&8] {prefix}&c{player}&r &7hat das Netzwerk verlassen."
|
||||||
|
# Vanish: Unsichtbare Spieler erzeugen keine normalen Join/Leave-Meldungen.
|
||||||
|
# Ist vanish-show-to-admins true, sehen Admins mit bypass-permission eine
|
||||||
|
# abweichende, dezente Benachrichtigung.
|
||||||
|
vanish-show-to-admins: true
|
||||||
|
vanish-join-format: "&8[&7+&8] &8{player} &7hat das Netzwerk betreten. &8(Vanish)"
|
||||||
|
vanish-leave-format: "&8[&7-&8] &8{player} &7hat das Netzwerk verlassen. &8(Vanish)"
|
||||||
|
# Brücken-Weitergabe (leer = deaktiviert)
|
||||||
|
discord-webhook: ""
|
||||||
|
telegram-chat-id: ""
|
||||||
|
telegram-thread-id: 0
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# GLOBALES RATE-LIMIT-FRAMEWORK
|
# GLOBALES RATE-LIMIT-FRAMEWORK
|
||||||
# Zentraler Schutz für Chat/PM/Command-Flood.
|
# Zentraler Schutz für Chat/PM/Command-Flood.
|
||||||
@@ -304,6 +328,36 @@ chat-filter:
|
|||||||
min-length: 6
|
min-length: 6
|
||||||
max-percent: 70
|
max-percent: 70
|
||||||
|
|
||||||
|
anti-ad:
|
||||||
|
enabled: true
|
||||||
|
message: "&cWerbung ist in diesem Chat nicht erlaubt!"
|
||||||
|
# Domains/Substrings die NICHT geblockt werden (z.B. eigene Serveradresse)
|
||||||
|
# Vergleich ist case-insensitiv und prüft ob der Substring im Match enthalten ist
|
||||||
|
whitelist:
|
||||||
|
- "viper-network.de"
|
||||||
|
- "m-viper.de"
|
||||||
|
- "https://www.spigotmc.org"
|
||||||
|
# TLDs die als Werbung gewertet werden.
|
||||||
|
# Leer = alle Domain-Treffer blockieren (nicht empfohlen, hohe False-Positive-Rate)
|
||||||
|
blocked-tlds:
|
||||||
|
- "net"
|
||||||
|
- "com"
|
||||||
|
- "de"
|
||||||
|
- "org"
|
||||||
|
- "gg"
|
||||||
|
- "io"
|
||||||
|
- "eu"
|
||||||
|
- "tv"
|
||||||
|
- "xyz"
|
||||||
|
- "info"
|
||||||
|
- "me"
|
||||||
|
- "cc"
|
||||||
|
- "co"
|
||||||
|
- "app"
|
||||||
|
- "online"
|
||||||
|
- "site"
|
||||||
|
- "fun"
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# MENTIONS (@Spielername)
|
# MENTIONS (@Spielername)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,7 +1,265 @@
|
|||||||
name: StatusAPIBridge
|
name: StatusAPI
|
||||||
version: 1.0.0
|
main: net.viper.status.StatusAPI
|
||||||
main: net.viper.statusapibridge.StatusAPIBridge
|
version: 4.1.0
|
||||||
api-version: 1.21
|
author: M_Viper
|
||||||
description: Sendet Vault-Economy-Daten an die BungeeCord StatusAPI
|
description: StatusAPI für BungeeCord inkl. Update-Checker, Modul-System und ChatModule
|
||||||
authors: [Viper]
|
# Mindestanforderung: Minecraft 1.20 / BungeeCord mit PlayerChatEvent-Unterstützung
|
||||||
softdepend: [Vault]
|
|
||||||
|
softdepend:
|
||||||
|
- LuckPerms
|
||||||
|
- Geyser-BungeeCord
|
||||||
|
|
||||||
|
commands:
|
||||||
|
# ── VanishModule ──────────────────────────────────────────
|
||||||
|
vanish:
|
||||||
|
description: Vanish ein-/ausschalten
|
||||||
|
usage: /vanish [Spieler]
|
||||||
|
aliases: [v]
|
||||||
|
|
||||||
|
vanishlist:
|
||||||
|
description: Alle unsichtbaren Spieler anzeigen
|
||||||
|
usage: /vanishlist
|
||||||
|
aliases: [vlist]
|
||||||
|
|
||||||
|
# ── Verify Modul ──────────────────────────────────────────
|
||||||
|
verify:
|
||||||
|
description: Verifiziere dich mit einem Token
|
||||||
|
usage: /verify <token>
|
||||||
|
|
||||||
|
# ── ForumBridge Modul ─────────────────────────────────────
|
||||||
|
forumlink:
|
||||||
|
description: Verknüpfe deinen Minecraft-Account mit dem Forum
|
||||||
|
usage: /forumlink <token>
|
||||||
|
aliases: [fl]
|
||||||
|
|
||||||
|
forum:
|
||||||
|
description: Zeigt ausstehende Forum-Benachrichtigungen an
|
||||||
|
usage: /forum
|
||||||
|
|
||||||
|
# ── NetworkInfo Modul ─────────────────────────────────────
|
||||||
|
netinfo:
|
||||||
|
description: Zeigt erweiterte Proxy- und Systeminfos an
|
||||||
|
usage: /netinfo
|
||||||
|
|
||||||
|
antibot:
|
||||||
|
description: Zeigt AntiBot-Status und Verwaltung
|
||||||
|
usage: /antibot <status|clearblocks|unblock|profile|reload>
|
||||||
|
|
||||||
|
# ── AutoMessage Modul ─────────────────────────────────────
|
||||||
|
automessage:
|
||||||
|
description: AutoMessage Verwaltung
|
||||||
|
usage: /automessage reload
|
||||||
|
|
||||||
|
# ── ChatModule – Kanal ────────────────────────────────────
|
||||||
|
channel:
|
||||||
|
description: Kanal wechseln oder Kanalliste anzeigen
|
||||||
|
usage: /channel [kanalname]
|
||||||
|
aliases: [ch, kanal]
|
||||||
|
|
||||||
|
# ── ChatModule – HelpOp ───────────────────────────────────
|
||||||
|
helpop:
|
||||||
|
description: Sende eine Hilfeanfrage an das Team
|
||||||
|
usage: /helpop <Nachricht>
|
||||||
|
|
||||||
|
# ── ChatModule – Privat-Nachrichten ───────────────────────
|
||||||
|
msg:
|
||||||
|
description: Sende eine private Nachricht
|
||||||
|
usage: /msg <Spieler> <Nachricht>
|
||||||
|
aliases: [tell, w, whisper]
|
||||||
|
|
||||||
|
r:
|
||||||
|
description: Antworte auf die letzte private Nachricht
|
||||||
|
usage: /r <Nachricht>
|
||||||
|
aliases: [reply, antwort]
|
||||||
|
|
||||||
|
# ── ChatModule – Blockieren ───────────────────────────────
|
||||||
|
ignore:
|
||||||
|
description: Spieler ignorieren
|
||||||
|
usage: /ignore <Spieler>
|
||||||
|
aliases: [block]
|
||||||
|
|
||||||
|
unignore:
|
||||||
|
description: Spieler nicht mehr ignorieren
|
||||||
|
usage: /unignore <Spieler>
|
||||||
|
aliases: [unblock]
|
||||||
|
|
||||||
|
# ── ChatModule – Mute (Admin) ─────────────────────────────
|
||||||
|
chatmute:
|
||||||
|
description: Spieler im Chat stumm schalten
|
||||||
|
usage: /chatmute <Spieler> [Minuten]
|
||||||
|
aliases: [gmute]
|
||||||
|
|
||||||
|
chatunmute:
|
||||||
|
description: Chat-Stummschaltung aufheben
|
||||||
|
usage: /chatunmute <Spieler>
|
||||||
|
aliases: [gunmute]
|
||||||
|
|
||||||
|
# ── ChatModule – Selbst-Mute ──────────────────────────────
|
||||||
|
chataus:
|
||||||
|
description: Eigenen Chat-Empfang ein-/ausschalten
|
||||||
|
usage: /chataus
|
||||||
|
aliases: [togglechat, chaton, chatoff]
|
||||||
|
|
||||||
|
# ── ChatModule – Broadcast ────────────────────────────────
|
||||||
|
broadcast:
|
||||||
|
description: Nachricht an alle Spieler senden
|
||||||
|
usage: /broadcast <Nachricht>
|
||||||
|
aliases: [bc, alert]
|
||||||
|
|
||||||
|
# ── ChatModule – Emoji ────────────────────────────────────
|
||||||
|
emoji:
|
||||||
|
description: Liste aller verfügbaren Emojis
|
||||||
|
usage: /emoji
|
||||||
|
aliases: [emojis]
|
||||||
|
|
||||||
|
# ── ChatModule – Social Spy ───────────────────────────────
|
||||||
|
socialspy:
|
||||||
|
description: Private Nachrichten mitlesen (Admin)
|
||||||
|
usage: /socialspy
|
||||||
|
aliases: [spy]
|
||||||
|
|
||||||
|
# ── ChatModule – Reload ───────────────────────────────────
|
||||||
|
chatreload:
|
||||||
|
description: Chat-Konfiguration neu laden
|
||||||
|
usage: /chatreload
|
||||||
|
|
||||||
|
# ── ChatModule – Admin-Info ───────────────────────────────
|
||||||
|
chatinfo:
|
||||||
|
description: Chat-Informationen ueber einen Spieler anzeigen (Admin)
|
||||||
|
usage: /chatinfo <Spieler>
|
||||||
|
|
||||||
|
# ── ChatModule – Chat-History ─────────────────────────────
|
||||||
|
chathist:
|
||||||
|
description: Chat-History aus dem Logfile anzeigen (Admin)
|
||||||
|
usage: /chathist [Spieler] [Anzahl]
|
||||||
|
|
||||||
|
# ── ChatModule – Mentions ─────────────────────────────────
|
||||||
|
mentions:
|
||||||
|
description: Mention-Benachrichtigungen ein-/ausschalten
|
||||||
|
usage: /mentions
|
||||||
|
aliases: [mention]
|
||||||
|
|
||||||
|
# ── ChatModule – Plugin-Bypass ────────────────────────────
|
||||||
|
chatbypass:
|
||||||
|
description: ChatModule fuer naechste Eingabe ueberspringen (fuer Plugin-Dialoge wie CMI)
|
||||||
|
usage: /chatbypass
|
||||||
|
aliases: [cbp]
|
||||||
|
|
||||||
|
# ── ChatModule – Account-Verknuepfung ─────────────────────
|
||||||
|
# FIX #4: Command-Namen stimmen jetzt mit der Code-Registrierung überein.
|
||||||
|
# Im ChatModule wird "discordlink" mit Alias "dlink" registriert,
|
||||||
|
# und "telegramlink" mit Alias "tlink".
|
||||||
|
discordlink:
|
||||||
|
description: Minecraft-Account mit Discord verknuepfen
|
||||||
|
usage: /discordlink
|
||||||
|
aliases: [dlink]
|
||||||
|
|
||||||
|
telegramlink:
|
||||||
|
description: Minecraft-Account mit Telegram verknuepfen
|
||||||
|
usage: /telegramlink
|
||||||
|
aliases: [tlink]
|
||||||
|
|
||||||
|
unlink:
|
||||||
|
description: Account-Verknuepfung aufheben
|
||||||
|
usage: /unlink <discord|telegram|all>
|
||||||
|
|
||||||
|
# ── ChatModule – Report ───────────────────────────────────
|
||||||
|
report:
|
||||||
|
description: Spieler melden
|
||||||
|
usage: /report <Spieler> <Grund>
|
||||||
|
|
||||||
|
reports:
|
||||||
|
description: Offene Reports anzeigen (Admin)
|
||||||
|
usage: /reports [all]
|
||||||
|
|
||||||
|
reportclose:
|
||||||
|
description: Report schliessen (Admin)
|
||||||
|
usage: /reportclose <ID>
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
# ── StatusAPI Core ────────────────────────────────────────
|
||||||
|
statusapi.update.notify:
|
||||||
|
description: Erlaubt Update-Benachrichtigungen
|
||||||
|
default: op
|
||||||
|
|
||||||
|
statusapi.netinfo:
|
||||||
|
description: Zugriff auf /netinfo
|
||||||
|
default: op
|
||||||
|
|
||||||
|
statusapi.antibot:
|
||||||
|
description: Zugriff auf /antibot
|
||||||
|
default: op
|
||||||
|
|
||||||
|
statusapi.automessage:
|
||||||
|
description: Zugriff auf /automessage reload
|
||||||
|
default: op
|
||||||
|
|
||||||
|
# ── ChatModule – Kanaele ──────────────────────────────────
|
||||||
|
chat.channel.local:
|
||||||
|
description: Zugang zum Local-Kanal
|
||||||
|
default: true
|
||||||
|
|
||||||
|
chat.channel.trade:
|
||||||
|
description: Zugang zum Trade-Kanal
|
||||||
|
default: true
|
||||||
|
|
||||||
|
chat.channel.staff:
|
||||||
|
description: Zugang zum Staff-Kanal
|
||||||
|
default: false
|
||||||
|
|
||||||
|
# ── ChatModule – HelpOp ───────────────────────────────────
|
||||||
|
chat.helpop.receive:
|
||||||
|
description: HelpOp-Nachrichten empfangen
|
||||||
|
default: false
|
||||||
|
|
||||||
|
# ── ChatModule – Mute ─────────────────────────────────────
|
||||||
|
chat.mute:
|
||||||
|
description: Spieler muten / unmuten
|
||||||
|
default: false
|
||||||
|
|
||||||
|
# ── ChatModule – Broadcast ────────────────────────────────
|
||||||
|
chat.broadcast:
|
||||||
|
description: Broadcast-Nachrichten senden
|
||||||
|
default: false
|
||||||
|
|
||||||
|
# ── ChatModule – Social Spy ───────────────────────────────
|
||||||
|
chat.socialspy:
|
||||||
|
description: Private Nachrichten mitlesen
|
||||||
|
default: false
|
||||||
|
|
||||||
|
# ── ChatModule – Admin ────────────────────────────────────
|
||||||
|
chat.admin.bypass:
|
||||||
|
description: Admin-Bypass - Kann nicht geblockt/gemutet werden
|
||||||
|
default: op
|
||||||
|
|
||||||
|
chat.admin.notify:
|
||||||
|
description: Benachrichtigungen ueber Mutes und Blocks erhalten
|
||||||
|
default: false
|
||||||
|
|
||||||
|
# ── ChatModule – Report ───────────────────────────────────
|
||||||
|
chat.report:
|
||||||
|
description: Spieler reporten (/report)
|
||||||
|
default: true
|
||||||
|
|
||||||
|
# ── ChatModule – Farben ───────────────────────────────────
|
||||||
|
chat.color:
|
||||||
|
description: Farbcodes (&a, &b, ...) im Chat nutzen
|
||||||
|
default: false
|
||||||
|
|
||||||
|
chat.color.format:
|
||||||
|
description: Formatierungen (&l, &o, &n, ...) im Chat nutzen
|
||||||
|
default: false
|
||||||
|
|
||||||
|
# ── ChatModule – Filter ───────────────────────────────────
|
||||||
|
chat.filter.bypass:
|
||||||
|
description: Chat-Filter (Anti-Spam, Caps, Blacklist) umgehen
|
||||||
|
default: false
|
||||||
|
|
||||||
|
# ── CommandBlocker ────────────────────────────────────────
|
||||||
|
commandblocker.bypass:
|
||||||
|
description: Command-Blocker umgehen
|
||||||
|
default: op
|
||||||
|
|
||||||
|
commandblocker.admin:
|
||||||
|
description: CommandBlocker verwalten (/cb)
|
||||||
|
default: op
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ broadcast.format=%prefixColored% %messageColored%
|
|||||||
# ===========================
|
# ===========================
|
||||||
statusapi.port=9191
|
statusapi.port=9191
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ===========================
|
# ===========================
|
||||||
# WORDPRESS / VERIFY EINSTELLUNGEN
|
# WORDPRESS / VERIFY EINSTELLUNGEN
|
||||||
# ===========================
|
# ===========================
|
||||||
|
|||||||
Reference in New Issue
Block a user