Update from Git Manager GUI
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package net.viper.status.modules.commandblocker;
|
||||
|
||||
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.event.ChatEvent;
|
||||
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.viper.status.StatusAPI;
|
||||
import net.viper.status.module.Module;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public class CommandBlockerModule implements Module, Listener {
|
||||
|
||||
private StatusAPI plugin;
|
||||
private boolean enabled = true; // Standardmäßig aktiv
|
||||
private String bypassPermission = "commandblocker.bypass"; // Standard Permission
|
||||
|
||||
private File file;
|
||||
private Set<String> blocked = new HashSet<>();
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "CommandBlockerModule";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable(Plugin plugin) {
|
||||
if (!(plugin instanceof StatusAPI)) return; // Sicherheit
|
||||
this.plugin = (StatusAPI) plugin;
|
||||
|
||||
// Datei laden
|
||||
file = new File(this.plugin.getDataFolder(), "blocked-commands.yml");
|
||||
loadFile();
|
||||
|
||||
// Listener registrieren
|
||||
ProxyServer.getInstance().getPluginManager().registerListener(this.plugin, this);
|
||||
|
||||
// /cb Befehl registrieren
|
||||
ProxyServer.getInstance().getPluginManager().registerCommand(this.plugin,
|
||||
new net.md_5.bungee.api.plugin.Command("cb", "commandblocker.admin") {
|
||||
@Override
|
||||
public void execute(CommandSender sender, String[] args) {
|
||||
handleCommand(sender, args);
|
||||
}
|
||||
});
|
||||
|
||||
this.plugin.getLogger().info("[CommandBlocker] aktiviert (" + blocked.size() + " Commands).");
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onDisable(Plugin plugin) {
|
||||
blocked.clear();
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onCommand(ChatEvent event) {
|
||||
if (!enabled || !event.isCommand()) return;
|
||||
|
||||
if (!(event.getSender() instanceof ProxiedPlayer)) return;
|
||||
ProxiedPlayer player = (ProxiedPlayer) event.getSender();
|
||||
|
||||
if (player.hasPermission(bypassPermission)) return;
|
||||
|
||||
String msg = event.getMessage();
|
||||
if (msg == null || msg.length() <= 1) return;
|
||||
|
||||
String cmd = msg.substring(1).toLowerCase(Locale.ROOT);
|
||||
String base = cmd.split(" ")[0];
|
||||
|
||||
if (blocked.contains(base)) {
|
||||
event.setCancelled(true);
|
||||
player.sendMessage(ChatColor.RED + "Dieser Befehl ist auf diesem Netzwerk blockiert.");
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCommand(CommandSender sender, String[] args) {
|
||||
if (args == null || args.length == 0) {
|
||||
sender.sendMessage(ChatColor.YELLOW + "/cb add <command>");
|
||||
sender.sendMessage(ChatColor.YELLOW + "/cb remove <command>");
|
||||
sender.sendMessage(ChatColor.YELLOW + "/cb list");
|
||||
sender.sendMessage(ChatColor.YELLOW + "/cb reload");
|
||||
return;
|
||||
}
|
||||
|
||||
String action = args[0].toLowerCase();
|
||||
|
||||
switch (action) {
|
||||
case "add":
|
||||
if (args.length < 2) break;
|
||||
blocked.add(args[1].toLowerCase());
|
||||
saveFile();
|
||||
sender.sendMessage(ChatColor.GREEN + "Command blockiert: " + args[1]);
|
||||
break;
|
||||
case "remove":
|
||||
if (args.length < 2) break;
|
||||
blocked.remove(args[1].toLowerCase());
|
||||
saveFile();
|
||||
sender.sendMessage(ChatColor.GREEN + "Command freigegeben: " + args[1]);
|
||||
break;
|
||||
case "list":
|
||||
sender.sendMessage(ChatColor.GOLD + "Blockierte Commands:");
|
||||
for (String c : blocked) {
|
||||
sender.sendMessage(ChatColor.RED + "- " + c);
|
||||
}
|
||||
break;
|
||||
case "reload":
|
||||
loadFile();
|
||||
sender.sendMessage(ChatColor.GREEN + "CommandBlocker neu geladen.");
|
||||
break;
|
||||
default:
|
||||
sender.sendMessage(ChatColor.RED + "Unbekannter Unterbefehl.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void loadFile() {
|
||||
try {
|
||||
if (!file.exists()) {
|
||||
File parent = file.getParentFile();
|
||||
if (parent != null && !parent.exists()) parent.mkdirs();
|
||||
file.createNewFile();
|
||||
saveFile();
|
||||
}
|
||||
|
||||
Yaml yaml = new Yaml();
|
||||
Map<String, Object> data = null;
|
||||
FileInputStream fis = null;
|
||||
try {
|
||||
fis = new FileInputStream(file);
|
||||
data = yaml.loadAs(fis, Map.class);
|
||||
} finally {
|
||||
if (fis != null) try { fis.close(); } catch (IOException ignored) {}
|
||||
}
|
||||
|
||||
blocked.clear();
|
||||
if (data != null && data.containsKey("blocked")) {
|
||||
Object obj = data.get("blocked");
|
||||
if (obj instanceof List) {
|
||||
List list = (List) obj;
|
||||
for (Object o : list) {
|
||||
if (o != null) blocked.add(String.valueOf(o).toLowerCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
if (plugin != null) plugin.getLogger().severe("[CommandBlocker] Fehler beim Laden: " + e.getMessage());
|
||||
else System.err.println("[CommandBlocker] Fehler beim Laden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void saveFile() {
|
||||
try {
|
||||
Yaml yaml = new Yaml();
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
out.put("blocked", new ArrayList<>(blocked));
|
||||
FileWriter fw = null;
|
||||
try {
|
||||
fw = new FileWriter(file);
|
||||
yaml.dump(out, fw);
|
||||
} finally {
|
||||
if (fw != null) try { fw.close(); } catch (IOException ignored) {}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (plugin != null) plugin.getLogger().severe("[CommandBlocker] Fehler beim Speichern: " + e.getMessage());
|
||||
else System.err.println("[CommandBlocker] Fehler beim Speichern: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package net.viper.status.modules.customcommands;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.CopyOption;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
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.config.ServerInfo;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.event.ChatEvent;
|
||||
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 für das Interface Argument
|
||||
import net.md_5.bungee.config.Configuration;
|
||||
import net.md_5.bungee.config.ConfigurationProvider;
|
||||
import net.md_5.bungee.config.YamlConfiguration;
|
||||
import net.md_5.bungee.event.EventHandler;
|
||||
import net.viper.status.StatusAPI;
|
||||
import net.viper.status.module.Module;
|
||||
|
||||
public class CustomCommandModule implements Module, Listener {
|
||||
|
||||
private StatusAPI plugin;
|
||||
private Configuration config;
|
||||
private Command chatCommand;
|
||||
|
||||
public CustomCommandModule() {
|
||||
// Leerer Konstruktor
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "CustomCommandModule";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable(Plugin plugin) {
|
||||
// Hier casten wir 'Plugin' zu 'StatusAPI', da wir wissen, dass es das ist
|
||||
this.plugin = (StatusAPI) plugin;
|
||||
|
||||
this.plugin.getLogger().info("Lade CustomCommandModule...");
|
||||
reloadConfig();
|
||||
if (this.config == null) {
|
||||
this.config = new Configuration();
|
||||
this.plugin.getLogger().warning("customcommands.yml konnte nicht geladen werden. Verwende leere Konfiguration.");
|
||||
}
|
||||
|
||||
// /bcmds Reload Befehl registrieren
|
||||
this.plugin.getProxy().getPluginManager().registerCommand(this.plugin, new Command("bcmds") {
|
||||
@Override
|
||||
public void execute(CommandSender sender, String[] args) {
|
||||
if (!sender.hasPermission("statusapi.bcmds")) {
|
||||
sender.sendMessage(new TextComponent(ChatColor.RED + "You don't have permission."));
|
||||
} else {
|
||||
reloadConfig();
|
||||
sender.sendMessage(new TextComponent(ChatColor.GREEN + "Config reloaded."));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// /chat Befehl registrieren (falls aktiviert)
|
||||
if (config.getBoolean("chat-command", true)) {
|
||||
chatCommand = new Command("chat") {
|
||||
@Override
|
||||
public void execute(CommandSender sender, String[] args) {
|
||||
if (sender instanceof ProxiedPlayer) {
|
||||
ProxiedPlayer player = (ProxiedPlayer) sender;
|
||||
if (player.getServer() == null) {
|
||||
player.sendMessage(new TextComponent(ChatColor.RED + "Konnte deinen Server nicht ermitteln. Bitte versuche es erneut."));
|
||||
return;
|
||||
}
|
||||
String msg = String.join(" ", args);
|
||||
if (msg.trim().isEmpty()) {
|
||||
player.sendMessage(new TextComponent(ChatColor.RED + "Bitte gib eine Nachricht an."));
|
||||
return;
|
||||
}
|
||||
ChatEvent e = new ChatEvent(player, player.getServer(), msg);
|
||||
ProxyServer.getInstance().getPluginManager().callEvent(e);
|
||||
if (!e.isCancelled()) {
|
||||
if (!e.isCommand() || !ProxyServer.getInstance().getPluginManager().dispatchCommand(sender, msg.substring(1))) {
|
||||
player.chat(msg);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
String msg = String.join(" ", args);
|
||||
if(msg.startsWith("/")) {
|
||||
ProxyServer.getInstance().getPluginManager().dispatchCommand(sender, msg.substring(1));
|
||||
} else {
|
||||
sender.sendMessage(new TextComponent("Console cannot send chat messages via /chat usually."));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
this.plugin.getProxy().getPluginManager().registerCommand(this.plugin, chatCommand);
|
||||
}
|
||||
|
||||
this.plugin.getProxy().getPluginManager().registerListener(this.plugin, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable(Plugin plugin) {
|
||||
// Optional: Cleanup logic, falls nötig.
|
||||
// Wir nutzen hier das übergebene 'plugin' Argument (oder this.plugin, ist egal)
|
||||
// Listener und Commands werden automatisch entfernt, wenn das Plugin stoppt.
|
||||
}
|
||||
|
||||
public void reloadConfig() {
|
||||
try {
|
||||
if (!this.plugin.getDataFolder().exists()) {
|
||||
this.plugin.getDataFolder().mkdirs();
|
||||
}
|
||||
File file = new File(this.plugin.getDataFolder(), "customcommands.yml");
|
||||
if (!file.exists()) {
|
||||
// Kopieren aus Resources
|
||||
InputStream in = this.plugin.getResourceAsStream("customcommands.yml");
|
||||
if (in == null) {
|
||||
this.plugin.getLogger().warning("customcommands.yml nicht in JAR gefunden. Erstelle leere Datei.");
|
||||
file.createNewFile();
|
||||
} else {
|
||||
Files.copy(in, file.toPath(), new CopyOption[0]);
|
||||
in.close();
|
||||
}
|
||||
}
|
||||
this.config = ConfigurationProvider.getProvider(YamlConfiguration.class).load(file);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
this.plugin.getLogger().severe("Konnte customcommands.yml nicht laden!");
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = 64)
|
||||
public void onCommand(ChatEvent e) {
|
||||
if (!e.isCommand()) return;
|
||||
if (!(e.getSender() instanceof ProxiedPlayer)) return;
|
||||
|
||||
final ProxiedPlayer player = (ProxiedPlayer) e.getSender();
|
||||
String[] split = e.getMessage().split(" ");
|
||||
String label = split[0].substring(1);
|
||||
|
||||
final List<String> args = new ArrayList<>(Arrays.asList(split));
|
||||
args.remove(0);
|
||||
|
||||
Configuration cmds = config.getSection("commands");
|
||||
if (cmds == null) return;
|
||||
|
||||
Configuration section = null;
|
||||
String foundKey = null;
|
||||
|
||||
for (String key : cmds.getKeys()) {
|
||||
Configuration cmdSection = cmds.getSection(key);
|
||||
if (key.equalsIgnoreCase(label)) {
|
||||
section = cmdSection;
|
||||
foundKey = key;
|
||||
break;
|
||||
}
|
||||
for (String alias : cmdSection.getStringList("aliases")) {
|
||||
if (alias.equalsIgnoreCase(label)) {
|
||||
section = cmdSection;
|
||||
foundKey = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (section != null) break;
|
||||
}
|
||||
|
||||
if (section == null) return;
|
||||
|
||||
String type = section.getString("type", "line");
|
||||
String sendertype = section.getString("sender", "default");
|
||||
String permission = section.getString("permission", "");
|
||||
final List<String> commands = section.getStringList("commands");
|
||||
|
||||
if (!permission.isEmpty() && !player.hasPermission(permission)) {
|
||||
player.sendMessage(new TextComponent(ChatColor.RED + "You don't have permission."));
|
||||
e.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
e.setCancelled(true);
|
||||
|
||||
final CommandSender target;
|
||||
if (sendertype.equals("default")) {
|
||||
target = player;
|
||||
} else if (sendertype.equals("admin")) {
|
||||
target = new ForwardSender(player, true);
|
||||
} else if (sendertype.equals("console")) {
|
||||
target = ProxyServer.getInstance().getConsole();
|
||||
} else {
|
||||
ProxiedPlayer targetPlayer = ProxyServer.getInstance().getPlayer(sendertype);
|
||||
if (targetPlayer == null || !targetPlayer.isConnected()) {
|
||||
player.sendMessage(new TextComponent(ChatColor.RED + "Player " + sendertype + " is not online."));
|
||||
return;
|
||||
}
|
||||
target = targetPlayer;
|
||||
}
|
||||
|
||||
String argsString = args.size() >= 1 ? String.join(" ", args) : "";
|
||||
final String finalArgs = argsString;
|
||||
final String senderName = player.getName();
|
||||
|
||||
if (type.equals("random")) {
|
||||
int randomIndex = new Random().nextInt(commands.size());
|
||||
String rawCommand = commands.get(randomIndex);
|
||||
executeCommand(target, rawCommand, finalArgs, senderName);
|
||||
} else if (type.equals("line")) {
|
||||
ProxyServer.getInstance().getScheduler().runAsync(this.plugin, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (String rawCommand : commands) {
|
||||
executeCommand(target, rawCommand, finalArgs, senderName);
|
||||
try {
|
||||
Thread.sleep(100L);
|
||||
} catch (InterruptedException ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.plugin.getLogger().warning("Unknown type '" + type + "' for command " + foundKey);
|
||||
}
|
||||
}
|
||||
|
||||
private void executeCommand(CommandSender sender, String rawCommand, String args, String playerName) {
|
||||
String parsed = rawCommand
|
||||
.replace("%args%", args)
|
||||
.replace("%sender%", playerName);
|
||||
|
||||
String commandToDispatch = parsed.startsWith("/") ? parsed.substring(1) : parsed;
|
||||
ProxyServer.getInstance().getPluginManager().dispatchCommand(sender, commandToDispatch);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package net.viper.status.modules.customcommands;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
import java.util.Collection;
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
import net.md_5.bungee.api.chat.BaseComponent;
|
||||
import net.md_5.bungee.api.connection.Connection;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.connection.Connection.Unsafe;
|
||||
|
||||
public class ForwardSender implements CommandSender, Connection {
|
||||
private ProxiedPlayer target;
|
||||
private Boolean admin;
|
||||
|
||||
public ForwardSender(ProxiedPlayer sender, Boolean admin) {
|
||||
this.target = sender;
|
||||
this.admin = admin;
|
||||
}
|
||||
|
||||
public ProxiedPlayer target() {
|
||||
return this.target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.target.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(String message) {
|
||||
this.target.sendMessage(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessages(String... messages) {
|
||||
this.target.sendMessages(messages);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(BaseComponent... message) {
|
||||
this.target.sendMessage(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(BaseComponent message) {
|
||||
this.target.sendMessage(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getGroups() {
|
||||
return this.target.getGroups();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addGroups(String... groups) {
|
||||
this.target.addGroups(groups);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeGroups(String... groups) {
|
||||
this.target.removeGroups(groups);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(String permission) {
|
||||
return this.admin ? true : this.target.hasPermission(permission);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPermission(String permission, boolean value) {
|
||||
this.target.setPermission(permission, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getPermissions() {
|
||||
Collection<String> perms = new java.util.ArrayList<>(this.target.getPermissions());
|
||||
if (this.admin) {
|
||||
perms.add("*");
|
||||
}
|
||||
return perms;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InetSocketAddress getAddress() {
|
||||
return this.target.getAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SocketAddress getSocketAddress() {
|
||||
return this.target.getSocketAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect(String reason) {
|
||||
this.target.disconnect(reason);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect(BaseComponent... reason) {
|
||||
this.target.disconnect(reason);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect(BaseComponent reason) {
|
||||
this.target.disconnect(reason);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
return this.target.isConnected();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Unsafe unsafe() {
|
||||
return this.target.unsafe();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,519 @@
|
||||
package net.viper.status.modules.forum;
|
||||
|
||||
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.ClickEvent;
|
||||
import net.md_5.bungee.api.chat.ComponentBuilder;
|
||||
import net.md_5.bungee.api.chat.HoverEvent;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
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.viper.status.module.Module;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
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
|
||||
*/
|
||||
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";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable(Plugin plugin) {
|
||||
this.plugin = plugin;
|
||||
|
||||
// Config laden
|
||||
loadConfig(plugin);
|
||||
|
||||
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());
|
||||
}
|
||||
}, 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.getLogger().info("ForumBridgeModule aktiviert.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable(Plugin plugin) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
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; }
|
||||
}
|
||||
|
||||
// ===== HTTP HANDLER (aufgerufen vom StatusAPI WebServer) =====
|
||||
|
||||
/**
|
||||
* 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\"}";
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
if (playerUuid == null || 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\"}";
|
||||
}
|
||||
|
||||
// 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";
|
||||
|
||||
// Notification erstellen
|
||||
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);
|
||||
notification.setDelivered(true);
|
||||
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
|
||||
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) {}
|
||||
|
||||
return "{\"success\":true,\"module\":\"ForumBridgeModule\",\"version\":\"" + version + "\"}";
|
||||
}
|
||||
|
||||
// ===== NOTIFICATION ZUSTELLUNG =====
|
||||
|
||||
/**
|
||||
* 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)
|
||||
if (!notif.getUrl().isEmpty()) {
|
||||
TextComponent link = new TextComponent("§a ➜ Im Forum ansehen");
|
||||
link.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, notif.getUrl()));
|
||||
link.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}, 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");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandSender sender, String[] args) {
|
||||
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;
|
||||
}
|
||||
|
||||
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() + "\","
|
||||
+ "\"mc_name\":\"" + escapeJson(p.getName()) + "\"}";
|
||||
|
||||
HttpURLConnection conn = (HttpURLConnection) new URL(endpoint).openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setDoOutput(true);
|
||||
conn.setConnectTimeout(5000);
|
||||
conn.setReadTimeout(7000);
|
||||
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
||||
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));
|
||||
}
|
||||
|
||||
int code = conn.getResponseCode();
|
||||
String resp;
|
||||
if (code >= 200 && code < 300) {
|
||||
resp = streamToString(conn.getInputStream(), utf8);
|
||||
} else {
|
||||
resp = 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;
|
||||
|
||||
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));
|
||||
}
|
||||
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 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")));
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
p.sendMessage(new TextComponent("§c✗ Fehler bei der Verbindung zum Forum."));
|
||||
plugin.getLogger().warning("ForumLink Fehler: " + ex.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* /forum — Zeigt ausstehende Forum-Benachrichtigungen an.
|
||||
*/
|
||||
private class ForumCommand extends Command {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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()));
|
||||
p.sendMessage(link);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
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()));
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
// ===== HELPER =====
|
||||
|
||||
/** Getter für den Storage (für StatusAPI HTTP-Handler) */
|
||||
public ForumNotifStorage getStorage() {
|
||||
return storage;
|
||||
}
|
||||
|
||||
private static String extractJsonString(String json, String key) {
|
||||
if (json == null || key == null) return null;
|
||||
String search = "\"" + key + "\"";
|
||||
int idx = json.indexOf(search);
|
||||
if (idx < 0) return null;
|
||||
int colon = json.indexOf(':', idx + search.length());
|
||||
if (colon < 0) return null;
|
||||
int i = colon + 1;
|
||||
while (i < json.length() && Character.isWhitespace(json.charAt(i))) i++;
|
||||
if (i >= json.length()) return null;
|
||||
char c = json.charAt(i);
|
||||
if (c == '"') {
|
||||
i++;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
boolean escape = false;
|
||||
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);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String escapeJson(String s) {
|
||||
if (s == null) return "";
|
||||
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r");
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package net.viper.status.modules.forum;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Speichert ausstehende Forum-Benachrichtigungen (Datei-basiert).
|
||||
* Benachrichtigungen die nicht sofort zugestellt werden konnten (Spieler offline)
|
||||
* werden hier gespeichert und beim nächsten Login zugestellt.
|
||||
*/
|
||||
public class ForumNotifStorage {
|
||||
|
||||
private final File file;
|
||||
private final Logger logger;
|
||||
|
||||
// UUID -> Liste ausstehender Notifications
|
||||
private final ConcurrentHashMap<UUID, CopyOnWriteArrayList<ForumNotification>> pending = new ConcurrentHashMap<>();
|
||||
|
||||
public ForumNotifStorage(File pluginFolder, Logger logger) {
|
||||
if (!pluginFolder.exists()) pluginFolder.mkdirs();
|
||||
this.file = new File(pluginFolder, "forum_notifications.dat");
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fügt eine Benachrichtigung hinzu.
|
||||
*/
|
||||
public void add(ForumNotification notification) {
|
||||
pending.computeIfAbsent(notification.getPlayerUuid(), k -> new CopyOnWriteArrayList<>())
|
||||
.add(notification);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle ausstehenden (nicht zugestellten) Benachrichtigungen eines Spielers zurück.
|
||||
*/
|
||||
public List<ForumNotification> getPending(UUID playerUuid) {
|
||||
CopyOnWriteArrayList<ForumNotification> list = pending.get(playerUuid);
|
||||
if (list == null) return Collections.emptyList();
|
||||
List<ForumNotification> result = new ArrayList<>();
|
||||
for (ForumNotification n : list) {
|
||||
if (!n.isDelivered()) result.add(n);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Anzahl ausstehender Benachrichtigungen.
|
||||
*/
|
||||
public int getPendingCount(UUID playerUuid) {
|
||||
CopyOnWriteArrayList<ForumNotification> list = pending.get(playerUuid);
|
||||
if (list == null) return 0;
|
||||
int count = 0;
|
||||
for (ForumNotification n : list) {
|
||||
if (!n.isDelivered()) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Markiert alle Benachrichtigungen eines Spielers als zugestellt und entfernt sie.
|
||||
*/
|
||||
public void clearDelivered(UUID playerUuid) {
|
||||
CopyOnWriteArrayList<ForumNotification> list = pending.get(playerUuid);
|
||||
if (list == null) return;
|
||||
list.removeIf(ForumNotification::isDelivered);
|
||||
if (list.isEmpty()) pending.remove(playerUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Markiert alle als zugestellt.
|
||||
*/
|
||||
public void markAllDelivered(UUID playerUuid) {
|
||||
CopyOnWriteArrayList<ForumNotification> list = pending.get(playerUuid);
|
||||
if (list == null) return;
|
||||
for (ForumNotification n : list) {
|
||||
n.setDelivered(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt Benachrichtigungen die älter als maxDays Tage sind.
|
||||
*/
|
||||
public void purgeOld(int maxDays) {
|
||||
long cutoff = System.currentTimeMillis() - ((long) maxDays * 24 * 60 * 60 * 1000);
|
||||
for (Map.Entry<UUID, CopyOnWriteArrayList<ForumNotification>> entry : pending.entrySet()) {
|
||||
entry.getValue().removeIf(n -> n.getTimestamp() < cutoff);
|
||||
if (entry.getValue().isEmpty()) pending.remove(entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Datei-Operationen =====
|
||||
|
||||
public void save() {
|
||||
try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF-8"))) {
|
||||
for (CopyOnWriteArrayList<ForumNotification> list : pending.values()) {
|
||||
for (ForumNotification n : list) {
|
||||
if (!n.isDelivered()) {
|
||||
bw.write(n.toLine());
|
||||
bw.newLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
bw.flush();
|
||||
} catch (IOException e) {
|
||||
logger.warning("Fehler beim Speichern der Forum-Benachrichtigungen: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void load() {
|
||||
if (!file.exists()) return;
|
||||
try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"))) {
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
ForumNotification n = ForumNotification.fromLine(line);
|
||||
if (n != null && !n.isDelivered()) {
|
||||
pending.computeIfAbsent(n.getPlayerUuid(), k -> new CopyOnWriteArrayList<>())
|
||||
.add(n);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warning("Fehler beim Laden der Forum-Benachrichtigungen: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package net.viper.status.modules.forum;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Eine einzelne Forum-Benachrichtigung.
|
||||
*/
|
||||
public class ForumNotification {
|
||||
|
||||
private final UUID playerUuid;
|
||||
private final String type; // reply, mention, message
|
||||
private final String title;
|
||||
private final String author;
|
||||
private final String url;
|
||||
private final long timestamp;
|
||||
private boolean delivered;
|
||||
|
||||
public ForumNotification(UUID playerUuid, String type, String title, String author, String url) {
|
||||
this.playerUuid = playerUuid;
|
||||
this.type = type != null ? type : "reply";
|
||||
this.title = title != null ? title : "";
|
||||
this.author = author != null ? author : "Unbekannt";
|
||||
this.url = url != null ? url : "";
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
this.delivered = false;
|
||||
}
|
||||
|
||||
/** Interner Konstruktor für Deserialisierung */
|
||||
ForumNotification(UUID playerUuid, String type, String title, String author, String url, long timestamp, boolean delivered) {
|
||||
this.playerUuid = playerUuid;
|
||||
this.type = type;
|
||||
this.title = title;
|
||||
this.author = author;
|
||||
this.url = url;
|
||||
this.timestamp = timestamp;
|
||||
this.delivered = delivered;
|
||||
}
|
||||
|
||||
// --- Getter ---
|
||||
|
||||
public UUID getPlayerUuid() { return playerUuid; }
|
||||
public String getType() { return type; }
|
||||
public String getTitle() { return title; }
|
||||
public String getAuthor() { return author; }
|
||||
public String getUrl() { return url; }
|
||||
public long getTimestamp() { return timestamp; }
|
||||
public boolean isDelivered() { return delivered; }
|
||||
public void setDelivered(boolean d) { this.delivered = d; }
|
||||
|
||||
/**
|
||||
* Deutsches Label für den Benachrichtigungstyp.
|
||||
*/
|
||||
public String getTypeLabel() {
|
||||
switch (type) {
|
||||
case "reply": return "Neue Antwort";
|
||||
case "mention": return "Erwähnung";
|
||||
case "message": return "Neue PN";
|
||||
case "thread": return "Neuer Thread";
|
||||
case "poll": return "Neue Umfrage";
|
||||
case "answer": return "Antwort auf deinen Thread";
|
||||
default: return "Benachrichtigung";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Farbcode (Minecraft) je nach Typ.
|
||||
*/
|
||||
public String getTypeColor() {
|
||||
switch (type) {
|
||||
case "reply": return "§b"; // Aqua
|
||||
case "mention": return "§e"; // Gelb
|
||||
case "message": return "§d"; // Rosa
|
||||
case "thread": return "§a"; // Grün
|
||||
case "poll": return "§3"; // Dunkel-Aqua
|
||||
case "answer": return "§2"; // Dunkel-Grün
|
||||
default: return "§f"; // Weiß
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialisierung für Datei-Speicherung.
|
||||
* Format: uuid|type|title|author|url|timestamp|delivered
|
||||
*/
|
||||
public String toLine() {
|
||||
return playerUuid.toString() + "|"
|
||||
+ type + "|"
|
||||
+ title.replace("|", "_") + "|"
|
||||
+ author.replace("|", "_") + "|"
|
||||
+ url.replace("|", "_") + "|"
|
||||
+ timestamp + "|"
|
||||
+ (delivered ? "1" : "0");
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialisierung aus einer Zeile.
|
||||
*/
|
||||
public static ForumNotification fromLine(String line) {
|
||||
if (line == null || line.trim().isEmpty()) return null;
|
||||
String[] parts = line.split("\\|", -1);
|
||||
if (parts.length < 7) return null;
|
||||
try {
|
||||
UUID uuid = UUID.fromString(parts[0]);
|
||||
String type = parts[1];
|
||||
String title = parts[2];
|
||||
String author = parts[3];
|
||||
String url = parts[4];
|
||||
long timestamp = Long.parseLong(parts[5]);
|
||||
boolean delivered = "1".equals(parts[6]);
|
||||
return new ForumNotification(uuid, type, title, author, url, timestamp, delivered);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
248
src/main/java/net/viper/status/modules/verify/VerifyModule.java
Normal file
248
src/main/java/net/viper/status/modules/verify/VerifyModule.java
Normal file
@@ -0,0 +1,248 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user