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