Dateien nach "src/main/java/net/viper/status/modules/chat" hochladen
This commit is contained in:
@@ -0,0 +1,319 @@
|
|||||||
|
package net.viper.status.modules.chat;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verwaltet die Verknüpfung von Minecraft-Accounts mit Discord/Telegram.
|
||||||
|
*
|
||||||
|
* Ablauf:
|
||||||
|
* 1. Spieler tippt /linkdiscord oder /linktelegram → Token wird generiert
|
||||||
|
* 2. Spieler schickt Token an den Bot
|
||||||
|
* 3. Bot-Polling erkennt Token → Verknüpfung wird gespeichert
|
||||||
|
*
|
||||||
|
* Speicherformat (chat_links.dat):
|
||||||
|
* minecraft:<uuid>|name:<spielername>|discord:<discord-user-id>|telegram:<telegram-user-id>
|
||||||
|
*/
|
||||||
|
public class AccountLinkManager {
|
||||||
|
|
||||||
|
private final File file;
|
||||||
|
private final Logger logger;
|
||||||
|
|
||||||
|
// UUID → verknüpfte Accounts
|
||||||
|
private final ConcurrentHashMap<UUID, LinkedAccount> links = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// Ausstehende Token: token → UUID (läuft nach 10 Min ab)
|
||||||
|
private final ConcurrentHashMap<String, PendingToken> pendingTokens = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public AccountLinkManager(File dataFolder, Logger logger) {
|
||||||
|
this.file = new File(dataFolder, "chat_links.dat");
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Datenklassen =====
|
||||||
|
|
||||||
|
public static class LinkedAccount {
|
||||||
|
public UUID minecraftUUID;
|
||||||
|
public String minecraftName;
|
||||||
|
public String discordUserId = ""; // leer = nicht verknüpft
|
||||||
|
public String telegramUserId = ""; // leer = nicht verknüpft
|
||||||
|
public String telegramUsername = ""; // @username für Anzeige
|
||||||
|
public String discordUsername = ""; // für Anzeige
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class PendingToken {
|
||||||
|
UUID uuid;
|
||||||
|
String playerName;
|
||||||
|
String type; // "discord" oder "telegram"
|
||||||
|
long expiresAt; // Unix-Millis
|
||||||
|
|
||||||
|
PendingToken(UUID uuid, String playerName, String type) {
|
||||||
|
this.uuid = uuid;
|
||||||
|
this.playerName = playerName;
|
||||||
|
this.type = type;
|
||||||
|
this.expiresAt = System.currentTimeMillis() + (10 * 60 * 1000L); // 10 Min
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isExpired() {
|
||||||
|
return System.currentTimeMillis() > expiresAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Token-Generierung =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generiert einen neuen Verknüpfungs-Token für einen Spieler.
|
||||||
|
* Bestehende Token für denselben Spieler+Typ werden überschrieben.
|
||||||
|
*
|
||||||
|
* @param uuid UUID des Spielers
|
||||||
|
* @param playerName Anzeigename
|
||||||
|
* @param type "discord" oder "telegram"
|
||||||
|
* @return 6-stelliger alphanumerischer Token (z.B. "A3F9K2")
|
||||||
|
*/
|
||||||
|
public String generateToken(UUID uuid, String playerName, String type) {
|
||||||
|
// Alte Token für diesen Spieler+Typ entfernen
|
||||||
|
pendingTokens.entrySet().removeIf(e ->
|
||||||
|
e.getValue().uuid.equals(uuid) && e.getValue().type.equals(type));
|
||||||
|
|
||||||
|
// Abgelaufene Token bereinigen
|
||||||
|
pendingTokens.entrySet().removeIf(e -> e.getValue().isExpired());
|
||||||
|
|
||||||
|
String token;
|
||||||
|
do {
|
||||||
|
token = generateRandomToken();
|
||||||
|
} while (pendingTokens.containsKey(token));
|
||||||
|
|
||||||
|
pendingTokens.put(token, new PendingToken(uuid, playerName, type));
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateRandomToken() {
|
||||||
|
String chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // ohne I,O,0,1 (Verwechslungsgefahr)
|
||||||
|
Random rnd = new Random();
|
||||||
|
StringBuilder sb = new StringBuilder(6);
|
||||||
|
for (int i = 0; i < 6; i++) sb.append(chars.charAt(rnd.nextInt(chars.length())));
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Token einlösen =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Versucht einen Token einzulösen (aufgerufen wenn Bot eine Nachricht empfängt).
|
||||||
|
*
|
||||||
|
* @param token Der eingesendete Token
|
||||||
|
* @param externalId Discord User-ID oder Telegram User-ID (als String)
|
||||||
|
* @param externalName Discord-Username oder Telegram-@username
|
||||||
|
* @return LinkedAccount wenn erfolgreich, null wenn Token ungültig/abgelaufen
|
||||||
|
*/
|
||||||
|
public LinkedAccount redeemToken(String token, String externalId, String externalName, String type) {
|
||||||
|
token = token.trim().toUpperCase();
|
||||||
|
PendingToken pending = pendingTokens.get(token);
|
||||||
|
|
||||||
|
if (pending == null || pending.isExpired()) {
|
||||||
|
pendingTokens.remove(token);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Typ muss übereinstimmen
|
||||||
|
if (!pending.type.equals(type)) return null;
|
||||||
|
|
||||||
|
pendingTokens.remove(token);
|
||||||
|
|
||||||
|
// Bestehenden Account holen oder neu anlegen
|
||||||
|
LinkedAccount account = links.computeIfAbsent(pending.uuid, k -> {
|
||||||
|
LinkedAccount a = new LinkedAccount();
|
||||||
|
a.minecraftUUID = pending.uuid;
|
||||||
|
a.minecraftName = pending.playerName;
|
||||||
|
return a;
|
||||||
|
});
|
||||||
|
account.minecraftName = pending.playerName; // aktuell halten
|
||||||
|
|
||||||
|
if ("discord".equals(pending.type)) {
|
||||||
|
account.discordUserId = externalId;
|
||||||
|
account.discordUsername = externalName;
|
||||||
|
} else if ("telegram".equals(pending.type)) {
|
||||||
|
account.telegramUserId = externalId;
|
||||||
|
account.telegramUsername = externalName;
|
||||||
|
}
|
||||||
|
|
||||||
|
save();
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Lookup =====
|
||||||
|
|
||||||
|
public LinkedAccount getByUUID(UUID uuid) {
|
||||||
|
return links.get(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LinkedAccount getByDiscordId(String discordUserId) {
|
||||||
|
for (LinkedAccount a : links.values()) {
|
||||||
|
if (discordUserId.equals(a.discordUserId)) return a;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LinkedAccount getByTelegramId(String telegramUserId) {
|
||||||
|
for (LinkedAccount a : links.values()) {
|
||||||
|
if (telegramUserId.equals(a.telegramUserId)) return a;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gibt den Minecraft-Namen für eine Discord-User-ID zurück, oder null. */
|
||||||
|
public String getMinecraftNameByDiscordId(String discordUserId) {
|
||||||
|
LinkedAccount a = getByDiscordId(discordUserId);
|
||||||
|
return a != null ? a.minecraftName : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gibt den Minecraft-Namen für eine Telegram-User-ID zurück, oder null. */
|
||||||
|
public String getMinecraftNameByTelegramId(String telegramUserId) {
|
||||||
|
LinkedAccount a = getByTelegramId(telegramUserId);
|
||||||
|
return a != null ? a.minecraftName : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Prüft ob ein Token gerade aussteht (für Tab-Complete etc.). */
|
||||||
|
public boolean hasPendingToken(UUID uuid, String type) {
|
||||||
|
for (PendingToken t : pendingTokens.values()) {
|
||||||
|
if (t.uuid.equals(uuid) && t.type.equals(type) && !t.isExpired()) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Verknüpfung aufheben =====
|
||||||
|
|
||||||
|
public boolean unlinkDiscord(UUID uuid) {
|
||||||
|
LinkedAccount a = links.get(uuid);
|
||||||
|
if (a == null || a.discordUserId.isEmpty()) return false;
|
||||||
|
a.discordUserId = "";
|
||||||
|
a.discordUsername = "";
|
||||||
|
cleanupEmpty(uuid);
|
||||||
|
save();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean unlinkTelegram(UUID uuid) {
|
||||||
|
LinkedAccount a = links.get(uuid);
|
||||||
|
if (a == null || a.telegramUserId.isEmpty()) return false;
|
||||||
|
a.telegramUserId = "";
|
||||||
|
a.telegramUsername = "";
|
||||||
|
cleanupEmpty(uuid);
|
||||||
|
save();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanupEmpty(UUID uuid) {
|
||||||
|
LinkedAccount a = links.get(uuid);
|
||||||
|
if (a != null && a.discordUserId.isEmpty() && a.telegramUserId.isEmpty()) {
|
||||||
|
links.remove(uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Convenience-Methoden für Bridges =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Löst einen Telegram-Token ein.
|
||||||
|
* Wrapper für redeemToken mit type="telegram".
|
||||||
|
*/
|
||||||
|
public LinkedAccount redeemTelegram(String token, String telegramUserId, String telegramUsername) {
|
||||||
|
return redeemToken(token, telegramUserId, telegramUsername, "telegram");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Löst einen Discord-Token ein.
|
||||||
|
* Wrapper für redeemToken mit type="discord".
|
||||||
|
*/
|
||||||
|
public LinkedAccount redeemDiscord(String token, String discordUserId, String discordUsername) {
|
||||||
|
return redeemToken(token, discordUserId, discordUsername, "discord");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt den Anzeigenamen für einen Telegram-Nutzer zurück.
|
||||||
|
* Wenn verknüpft: "MinecraftName (@telegram)", sonst: "@telegram"
|
||||||
|
*/
|
||||||
|
public String resolveTelegramName(String telegramUserId, String fallbackName) {
|
||||||
|
String mc = getMinecraftNameByTelegramId(telegramUserId);
|
||||||
|
return mc != null ? mc : fallbackName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt den Anzeigenamen für einen Discord-Nutzer zurück.
|
||||||
|
* Wenn verknüpft: Minecraft-Name, sonst: Discord-Username
|
||||||
|
*/
|
||||||
|
public String resolveDiscordName(String discordUserId, String fallbackName) {
|
||||||
|
String mc = getMinecraftNameByDiscordId(discordUserId);
|
||||||
|
return mc != null ? mc : fallbackName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt das korrekte Format-String zurück abhängig ob Account verknüpft.
|
||||||
|
* linked=true → linkedFormat (mit {player}), false → unlinkedFormat (mit {user})
|
||||||
|
*/
|
||||||
|
public boolean isLinkedTelegram(String telegramUserId) {
|
||||||
|
return getByTelegramId(telegramUserId) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isLinkedDiscord(String discordUserId) {
|
||||||
|
return getByDiscordId(discordUserId) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Persistenz =====
|
||||||
|
|
||||||
|
public void save() {
|
||||||
|
try (BufferedWriter bw = new BufferedWriter(
|
||||||
|
new OutputStreamWriter(new FileOutputStream(file), "UTF-8"))) {
|
||||||
|
for (LinkedAccount a : links.values()) {
|
||||||
|
bw.write(a.minecraftUUID
|
||||||
|
+ "|" + esc(a.minecraftName)
|
||||||
|
+ "|" + esc(a.discordUserId)
|
||||||
|
+ "|" + esc(a.discordUsername)
|
||||||
|
+ "|" + esc(a.telegramUserId)
|
||||||
|
+ "|" + esc(a.telegramUsername));
|
||||||
|
bw.newLine();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warning("[ChatModule] Fehler beim Speichern der Account-Links: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void load() {
|
||||||
|
links.clear();
|
||||||
|
if (!file.exists()) return;
|
||||||
|
try (BufferedReader br = new BufferedReader(
|
||||||
|
new InputStreamReader(new FileInputStream(file), "UTF-8"))) {
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
line = line.trim();
|
||||||
|
if (line.isEmpty()) continue;
|
||||||
|
String[] p = line.split("\\|", -1);
|
||||||
|
if (p.length < 6) continue;
|
||||||
|
try {
|
||||||
|
LinkedAccount a = new LinkedAccount();
|
||||||
|
a.minecraftUUID = UUID.fromString(p[0]);
|
||||||
|
a.minecraftName = unesc(p[1]);
|
||||||
|
a.discordUserId = unesc(p[2]);
|
||||||
|
a.discordUsername = unesc(p[3]);
|
||||||
|
a.telegramUserId = unesc(p[4]);
|
||||||
|
a.telegramUsername = unesc(p[5]);
|
||||||
|
if (!a.discordUserId.isEmpty() || !a.telegramUserId.isEmpty()) {
|
||||||
|
links.put(a.minecraftUUID, a);
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
} 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) {
|
||||||
|
if (s == null) return "";
|
||||||
|
return s.replace("\\", "\\\\").replace("|", "\\p");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String unesc(String s) {
|
||||||
|
if (s == null) return "";
|
||||||
|
return s.replace("\\p", "|").replace("\\\\", "\\");
|
||||||
|
}
|
||||||
|
}
|
||||||
124
src/main/java/net/viper/status/modules/chat/BlockManager.java
Normal file
124
src/main/java/net/viper/status/modules/chat/BlockManager.java
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
package net.viper.status.modules.chat;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verwaltet den gegenseitigen Blockier-/Ignore-Status zwischen Spielern.
|
||||||
|
*
|
||||||
|
* Admins/OPs mit dem Bypass-Permission sind nicht blockierbar.
|
||||||
|
*
|
||||||
|
* Format der Speicherdatei:
|
||||||
|
* <blocker-uuid>|<blocked-uuid1>,<blocked-uuid2>,...
|
||||||
|
*/
|
||||||
|
public class BlockManager {
|
||||||
|
|
||||||
|
private final File file;
|
||||||
|
private final Logger logger;
|
||||||
|
|
||||||
|
// blocker UUID → Set der blockierten UUIDs
|
||||||
|
private final ConcurrentHashMap<UUID, Set<UUID>> blocked = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public BlockManager(File dataFolder, Logger logger) {
|
||||||
|
this.file = new File(dataFolder, "chat_blocked.dat");
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Block-Logik =====
|
||||||
|
|
||||||
|
/** Spieler `blocker` blockiert Spieler `target`. */
|
||||||
|
public void block(UUID blocker, UUID target) {
|
||||||
|
blocked.computeIfAbsent(blocker, k -> Collections.newSetFromMap(new ConcurrentHashMap<>()))
|
||||||
|
.add(target);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Spieler `blocker` hebt den Block für `target` auf. */
|
||||||
|
public void unblock(UUID blocker, UUID target) {
|
||||||
|
Set<UUID> set = blocked.get(blocker);
|
||||||
|
if (set != null) {
|
||||||
|
set.remove(target);
|
||||||
|
if (set.isEmpty()) blocked.remove(blocker);
|
||||||
|
}
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ob `blocker` den Spieler `target` blockiert hat.
|
||||||
|
* Admins (isAdmin=true) sind niemals blockiert.
|
||||||
|
*/
|
||||||
|
public boolean isBlocked(UUID blocker, UUID target) {
|
||||||
|
Set<UUID> set = blocked.get(blocker);
|
||||||
|
return set != null && set.contains(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ob eine Nachricht von `sender` an `receiver` zugestellt werden soll.
|
||||||
|
* Gibt false zurück, wenn einer der beiden den anderen blockiert.
|
||||||
|
*/
|
||||||
|
public boolean canReceive(UUID sender, UUID receiver) {
|
||||||
|
// receiver hat sender blockiert → keine Nachricht
|
||||||
|
if (isBlocked(receiver, sender)) return false;
|
||||||
|
// sender hat receiver blockiert → keine Nachricht (Komfort)
|
||||||
|
if (isBlocked(sender, receiver)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gibt alle UUIDs zurück, die `blocker` blockiert hat. */
|
||||||
|
public Set<UUID> getBlockedBy(UUID blocker) {
|
||||||
|
Set<UUID> set = blocked.get(blocker);
|
||||||
|
if (set == null) return Collections.emptySet();
|
||||||
|
return Collections.unmodifiableSet(set);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Persistenz =====
|
||||||
|
|
||||||
|
public void save() {
|
||||||
|
try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF-8"))) {
|
||||||
|
for (Map.Entry<UUID, Set<UUID>> e : blocked.entrySet()) {
|
||||||
|
if (e.getValue().isEmpty()) continue;
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append(e.getKey()).append("|");
|
||||||
|
Iterator<UUID> it = e.getValue().iterator();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
sb.append(it.next());
|
||||||
|
if (it.hasNext()) sb.append(",");
|
||||||
|
}
|
||||||
|
bw.write(sb.toString());
|
||||||
|
bw.newLine();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warning("[ChatModule] Fehler beim Speichern der Block-Liste: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void load() {
|
||||||
|
blocked.clear();
|
||||||
|
if (!file.exists()) return;
|
||||||
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"))) {
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
line = line.trim();
|
||||||
|
if (line.isEmpty()) continue;
|
||||||
|
String[] parts = line.split("\\|", 2);
|
||||||
|
if (parts.length < 2) continue;
|
||||||
|
try {
|
||||||
|
UUID blocker = UUID.fromString(parts[0]);
|
||||||
|
Set<UUID> targets = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||||
|
for (String rawUUID : parts[1].split(",")) {
|
||||||
|
rawUUID = rawUUID.trim();
|
||||||
|
if (!rawUUID.isEmpty()) {
|
||||||
|
try { targets.add(UUID.fromString(rawUUID)); }
|
||||||
|
catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!targets.isEmpty()) blocked.put(blocker, targets);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warning("[ChatModule] Fehler beim Laden der Block-Liste: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
69
src/main/java/net/viper/status/modules/chat/ChatChannel.java
Normal file
69
src/main/java/net/viper/status/modules/chat/ChatChannel.java
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package net.viper.status.modules.chat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repräsentiert einen Chat-Kanal mit allen zugehörigen Einstellungen.
|
||||||
|
*/
|
||||||
|
public class ChatChannel {
|
||||||
|
|
||||||
|
private final String id;
|
||||||
|
private final String name;
|
||||||
|
private final String symbol;
|
||||||
|
private final String permission;
|
||||||
|
private final String color;
|
||||||
|
private final String format;
|
||||||
|
private final boolean localOnly;
|
||||||
|
|
||||||
|
// Bridge-Einstellungen
|
||||||
|
private final String discordWebhook;
|
||||||
|
private final String discordChannelId;
|
||||||
|
private final String telegramChatId;
|
||||||
|
private final int telegramThreadId; // 0 = kein Thema, >0 = Themen-ID
|
||||||
|
private final boolean useAdminBridge;
|
||||||
|
|
||||||
|
public ChatChannel(String id, String name, String symbol, String permission,
|
||||||
|
String color, String format, boolean localOnly,
|
||||||
|
String discordWebhook, String discordChannelId,
|
||||||
|
String telegramChatId, int telegramThreadId, boolean useAdminBridge) {
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
this.symbol = symbol;
|
||||||
|
this.permission = permission;
|
||||||
|
this.color = color;
|
||||||
|
this.format = format;
|
||||||
|
this.localOnly = localOnly;
|
||||||
|
this.discordWebhook = discordWebhook;
|
||||||
|
this.discordChannelId = discordChannelId;
|
||||||
|
this.telegramChatId = telegramChatId;
|
||||||
|
this.telegramThreadId = telegramThreadId;
|
||||||
|
this.useAdminBridge = useAdminBridge;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() { return id; }
|
||||||
|
public String getName() { return name; }
|
||||||
|
public String getSymbol() { return symbol; }
|
||||||
|
public String getPermission() { return permission; }
|
||||||
|
public String getColor() { return color; }
|
||||||
|
public String getFormat() { return format; }
|
||||||
|
public boolean isLocalOnly() { return localOnly; }
|
||||||
|
public String getDiscordWebhook() { return discordWebhook; }
|
||||||
|
public String getDiscordChannelId() { return discordChannelId; }
|
||||||
|
public String getTelegramChatId() { return telegramChatId; }
|
||||||
|
public int getTelegramThreadId() { return telegramThreadId; }
|
||||||
|
public boolean isUseAdminBridge() { return useAdminBridge; }
|
||||||
|
|
||||||
|
/** Prüft ob der Kanal eine Permission erfordert. */
|
||||||
|
public boolean hasPermission() {
|
||||||
|
return permission != null && !permission.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gibt das formatierte Kanalprefix zurück, z.B. §a[G] */
|
||||||
|
public String getFormattedTag() {
|
||||||
|
return net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&',
|
||||||
|
color + "[" + symbol + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "ChatChannel{id=" + id + ", name=" + name + "}";
|
||||||
|
}
|
||||||
|
}
|
||||||
542
src/main/java/net/viper/status/modules/chat/ChatConfig.java
Normal file
542
src/main/java/net/viper/status/modules/chat/ChatConfig.java
Normal file
@@ -0,0 +1,542 @@
|
|||||||
|
package net.viper.status.modules.chat;
|
||||||
|
|
||||||
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
|
import net.md_5.bungee.config.Configuration;
|
||||||
|
import net.md_5.bungee.config.ConfigurationProvider;
|
||||||
|
import net.md_5.bungee.config.YamlConfiguration;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lädt und verwaltet die chat.yml Konfiguration.
|
||||||
|
*/
|
||||||
|
public class ChatConfig {
|
||||||
|
|
||||||
|
private final Plugin plugin;
|
||||||
|
private Configuration config;
|
||||||
|
|
||||||
|
// Geladene Kanäle
|
||||||
|
private final Map<String, ChatChannel> channels = new LinkedHashMap<>();
|
||||||
|
private String defaultChannel;
|
||||||
|
|
||||||
|
// HelpOp
|
||||||
|
private String helpopFormat;
|
||||||
|
private String helpopPermission;
|
||||||
|
private int helpopCooldown;
|
||||||
|
private String helpopConfirm;
|
||||||
|
private String helpopDiscordWebhook;
|
||||||
|
private String helpopTelegramChatId;
|
||||||
|
|
||||||
|
// Broadcast
|
||||||
|
private String broadcastFormat;
|
||||||
|
private String broadcastPermission;
|
||||||
|
|
||||||
|
// Private Messages
|
||||||
|
private boolean pmEnabled;
|
||||||
|
private String pmFormatSender;
|
||||||
|
private String pmFormatReceiver;
|
||||||
|
private String pmFormatSpy;
|
||||||
|
private String pmSpyPermission;
|
||||||
|
|
||||||
|
// Mute
|
||||||
|
private int defaultMuteDuration;
|
||||||
|
private String mutedMessage;
|
||||||
|
|
||||||
|
// Emoji
|
||||||
|
private boolean emojiEnabled;
|
||||||
|
private boolean emojiBedrockSupport;
|
||||||
|
private final Map<String, String> emojiMappings = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
// Discord
|
||||||
|
private boolean discordEnabled;
|
||||||
|
private String discordBotToken;
|
||||||
|
private String discordGuildId;
|
||||||
|
private int discordPollInterval;
|
||||||
|
private String discordFromFormat;
|
||||||
|
private String discordAdminChannelId;
|
||||||
|
private String discordEmbedColor;
|
||||||
|
|
||||||
|
// Telegram
|
||||||
|
private boolean telegramEnabled;
|
||||||
|
private String telegramBotToken;
|
||||||
|
private int telegramPollInterval;
|
||||||
|
private String telegramFromFormat;
|
||||||
|
private String telegramAdminChatId;
|
||||||
|
private int telegramChatTopicId;
|
||||||
|
private int telegramAdminTopicId;
|
||||||
|
|
||||||
|
// Account-Linking
|
||||||
|
private boolean linkingEnabled;
|
||||||
|
private String linkDiscordMessage;
|
||||||
|
private String linkTelegramMessage;
|
||||||
|
private String linkSuccessDiscord;
|
||||||
|
private String linkSuccessTelegram;
|
||||||
|
private String linkBotSuccessDiscord;
|
||||||
|
private String linkBotSuccessTelegram;
|
||||||
|
private String linkedDiscordFormat;
|
||||||
|
private String linkedTelegramFormat;
|
||||||
|
private int telegramAdminThreadId;
|
||||||
|
|
||||||
|
// Admin
|
||||||
|
private String adminBypassPermission;
|
||||||
|
private String adminNotifyPermission;
|
||||||
|
|
||||||
|
// ===== NEU: Chatlog =====
|
||||||
|
private boolean chatlogEnabled;
|
||||||
|
private int chatlogRetentionDays;
|
||||||
|
|
||||||
|
// ===== NEU: Server-Farben =====
|
||||||
|
private final Map<String, String> serverColors = new LinkedHashMap<>();
|
||||||
|
private final Map<String, String> serverDisplayNames = new LinkedHashMap<>();
|
||||||
|
private String serverColorDefault;
|
||||||
|
|
||||||
|
// ===== NEU: Reports =====
|
||||||
|
private boolean reportsEnabled;
|
||||||
|
private String reportConfirm;
|
||||||
|
|
||||||
|
// ===== Chat-Filter =====
|
||||||
|
private ChatFilter.ChatFilterConfig filterConfig = new ChatFilter.ChatFilterConfig();
|
||||||
|
|
||||||
|
// ===== Mentions =====
|
||||||
|
private boolean mentionsEnabled;
|
||||||
|
private String mentionsHighlightColor;
|
||||||
|
private String mentionsSound;
|
||||||
|
private boolean mentionsAllowToggle;
|
||||||
|
private String mentionsNotifyPrefix;
|
||||||
|
|
||||||
|
// ===== Chat-History =====
|
||||||
|
private int historyMaxLines;
|
||||||
|
private int historyDefaultLines;
|
||||||
|
private String reportPermission;
|
||||||
|
private String reportClosePermission;
|
||||||
|
private String reportViewPermission;
|
||||||
|
private int reportCooldown;
|
||||||
|
private String reportDiscordWebhook;
|
||||||
|
private String reportTelegramChatId;
|
||||||
|
private boolean reportWebhookEnabled;
|
||||||
|
|
||||||
|
public ChatConfig(Plugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void load() {
|
||||||
|
File file = new File(plugin.getDataFolder(), "chat.yml");
|
||||||
|
if (!file.exists()) {
|
||||||
|
plugin.getDataFolder().mkdirs();
|
||||||
|
InputStream in = plugin.getResourceAsStream("chat.yml");
|
||||||
|
if (in != null) {
|
||||||
|
try {
|
||||||
|
Files.copy(in, file.toPath());
|
||||||
|
} catch (IOException e) {
|
||||||
|
plugin.getLogger().severe("[ChatModule] Konnte chat.yml nicht erstellen: " + e.getMessage());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
plugin.getLogger().warning("[ChatModule] chat.yml nicht in JAR gefunden, erstelle leere Datei.");
|
||||||
|
try { file.createNewFile(); } catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
config = ConfigurationProvider.getProvider(YamlConfiguration.class).load(file);
|
||||||
|
} catch (IOException e) {
|
||||||
|
plugin.getLogger().severe("[ChatModule] Fehler beim Laden der chat.yml: " + e.getMessage());
|
||||||
|
config = new Configuration();
|
||||||
|
}
|
||||||
|
|
||||||
|
parseConfig();
|
||||||
|
plugin.getLogger().info("[ChatModule] " + channels.size() + " Kanäle geladen.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseConfig() {
|
||||||
|
defaultChannel = config.getString("default-channel", "global");
|
||||||
|
|
||||||
|
// --- Kanäle ---
|
||||||
|
channels.clear();
|
||||||
|
Configuration chSection = config.getSection("channels");
|
||||||
|
if (chSection != null) {
|
||||||
|
for (String id : chSection.getKeys()) {
|
||||||
|
Configuration ch = chSection.getSection(id);
|
||||||
|
if (ch == null) continue;
|
||||||
|
channels.put(id.toLowerCase(), new ChatChannel(
|
||||||
|
id.toLowerCase(),
|
||||||
|
ch.getString("name", id),
|
||||||
|
ch.getString("symbol", id.substring(0, 1).toUpperCase()),
|
||||||
|
ch.getString("permission", ""),
|
||||||
|
ch.getString("color", "&f"),
|
||||||
|
ch.getString("format", "&8[&7{server}&8] {prefix}&r{player}{suffix}&8: &f{message}"),
|
||||||
|
ch.getBoolean("local-only", false),
|
||||||
|
ch.getString("discord-webhook", ""),
|
||||||
|
ch.getString("discord-channel-id", ""),
|
||||||
|
ch.getString("telegram-chat-id", ""),
|
||||||
|
ch.getInt("telegram-thread-id", 0),
|
||||||
|
ch.getBoolean("use-admin-bridge", false)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: global-Kanal immer vorhanden
|
||||||
|
if (!channels.containsKey("global")) {
|
||||||
|
channels.put("global", new ChatChannel(
|
||||||
|
"global", "Global", "G", "", "&a",
|
||||||
|
"&8[&a{server}&8] {prefix}&r{player}{suffix}&8: &f{message}",
|
||||||
|
false, "", "", "", 0, false
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- HelpOp ---
|
||||||
|
Configuration ho = config.getSection("helpop");
|
||||||
|
if (ho != null) {
|
||||||
|
helpopFormat = ho.getString("format", "&8[&eHELPOP&8] &f{player}&8@&7{server}&8: &e{message}");
|
||||||
|
helpopPermission = ho.getString("receive-permission", "chat.helpop.receive");
|
||||||
|
helpopCooldown = ho.getInt("cooldown", 30);
|
||||||
|
helpopConfirm = ho.getString("confirm-message", "&aHilferuf gesendet!");
|
||||||
|
helpopDiscordWebhook = ho.getString("discord-webhook", "");
|
||||||
|
helpopTelegramChatId = ho.getString("telegram-chat-id", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Broadcast ---
|
||||||
|
Configuration bc = config.getSection("broadcast");
|
||||||
|
if (bc != null) {
|
||||||
|
broadcastFormat = bc.getString("format", "&c[&6Broadcast&c] &e{message}");
|
||||||
|
broadcastPermission = bc.getString("permission", "chat.broadcast");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Private Messages ---
|
||||||
|
Configuration pm = config.getSection("private-messages");
|
||||||
|
if (pm != null) {
|
||||||
|
pmEnabled = pm.getBoolean("enabled", true);
|
||||||
|
pmFormatSender = pm.getString("format-sender", "&8[&7Du &8→ &b{player}&8] &f{message}");
|
||||||
|
pmFormatReceiver = pm.getString("format-receiver", "&8[&b{player} &8→ &7Dir&8] &f{message}");
|
||||||
|
pmFormatSpy = pm.getString("format-social-spy","&8[&dSPY &7{sender} &8→ &7{receiver}&8] &f{message}");
|
||||||
|
pmSpyPermission = pm.getString("social-spy-permission", "chat.socialspy");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Mute ---
|
||||||
|
Configuration mu = config.getSection("mute");
|
||||||
|
if (mu != null) {
|
||||||
|
defaultMuteDuration = mu.getInt("default-duration-minutes", 60);
|
||||||
|
mutedMessage = mu.getString("muted-message", "&cDu bist stummgeschaltet. Noch: &f{time}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Emoji ---
|
||||||
|
Configuration em = config.getSection("emoji");
|
||||||
|
if (em != null) {
|
||||||
|
emojiEnabled = em.getBoolean("enabled", true);
|
||||||
|
emojiBedrockSupport = em.getBoolean("bedrock-support", true);
|
||||||
|
emojiMappings.clear();
|
||||||
|
Configuration map = em.getSection("mappings");
|
||||||
|
if (map != null) {
|
||||||
|
for (String key : map.getKeys()) {
|
||||||
|
emojiMappings.put(key, map.getString(key, key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Discord ---
|
||||||
|
Configuration dc = config.getSection("discord");
|
||||||
|
if (dc != null) {
|
||||||
|
discordEnabled = dc.getBoolean("enabled", false);
|
||||||
|
discordBotToken = dc.getString("bot-token", "");
|
||||||
|
discordGuildId = dc.getString("guild-id", "");
|
||||||
|
discordPollInterval = dc.getInt("poll-interval", 3);
|
||||||
|
discordFromFormat = dc.getString("from-discord-format", "&9[Discord] &b{user}&8: &f{message}");
|
||||||
|
discordAdminChannelId = dc.getString("admin-channel-id", "");
|
||||||
|
discordEmbedColor = dc.getString("embed-color", "5865F2");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Telegram ---
|
||||||
|
Configuration tg = config.getSection("telegram");
|
||||||
|
if (tg != null) {
|
||||||
|
telegramEnabled = tg.getBoolean("enabled", false);
|
||||||
|
telegramBotToken = tg.getString("bot-token", "");
|
||||||
|
telegramPollInterval = tg.getInt("poll-interval", 3);
|
||||||
|
telegramFromFormat = tg.getString("from-telegram-format", "&3[Telegram] &b{user}&8: &f{message}");
|
||||||
|
telegramAdminChatId = tg.getString("admin-chat-id", "");
|
||||||
|
telegramChatTopicId = tg.getInt("chat-topic-id", 0);
|
||||||
|
telegramAdminTopicId = tg.getInt("admin-topic-id", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Account-Linking ---
|
||||||
|
Configuration al = config.getSection("account-linking");
|
||||||
|
if (al != null) {
|
||||||
|
linkingEnabled = al.getBoolean("enabled", true);
|
||||||
|
linkDiscordMessage = al.getString("discord-link-message", "&aCode: &f{token}");
|
||||||
|
linkTelegramMessage = al.getString("telegram-link-message", "&aCode: &f{token}");
|
||||||
|
linkSuccessDiscord = al.getString("success-discord", "&aDiscord verknüpft!");
|
||||||
|
linkSuccessTelegram = al.getString("success-telegram", "&aTelegram verknüpft!");
|
||||||
|
linkBotSuccessDiscord = al.getString("bot-success-discord", "✅ Verknüpft: {player}");
|
||||||
|
linkBotSuccessTelegram = al.getString("bot-success-telegram", "✅ Verknüpft: {player}");
|
||||||
|
linkedDiscordFormat = al.getString("linked-discord-format",
|
||||||
|
"&9[&bDiscord&9] &f{player} &8(&7{user}&8)&8: &f{message}");
|
||||||
|
linkedTelegramFormat = al.getString("linked-telegram-format",
|
||||||
|
"&3[&bTelegram&3] &f{player} &8(&7{user}&8)&8: &f{message}");
|
||||||
|
} else {
|
||||||
|
linkingEnabled = true;
|
||||||
|
linkDiscordMessage = "&aCode: &f{token}";
|
||||||
|
linkTelegramMessage = "&aCode: &f{token}";
|
||||||
|
linkSuccessDiscord = "&aDiscord verknüpft!";
|
||||||
|
linkSuccessTelegram = "&aTelegram verknüpft!";
|
||||||
|
linkBotSuccessDiscord = "✅ Verknüpft: {player}";
|
||||||
|
linkBotSuccessTelegram = "✅ Verknüpft: {player}";
|
||||||
|
linkedDiscordFormat = "&9[&bDiscord&9] &f{player} &8(&7{user}&8)&8: &f{message}";
|
||||||
|
linkedTelegramFormat = "&3[&bTelegram&3] &f{player} &8(&7{user}&8)&8: &f{message}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Chat-Filter ---
|
||||||
|
filterConfig = new ChatFilter.ChatFilterConfig();
|
||||||
|
Configuration cf = config.getSection("chat-filter");
|
||||||
|
if (cf != null) {
|
||||||
|
Configuration spam = cf.getSection("anti-spam");
|
||||||
|
if (spam != null) {
|
||||||
|
filterConfig.antiSpamEnabled = spam.getBoolean("enabled", true);
|
||||||
|
filterConfig.spamCooldownMs = spam.getInt("cooldown-ms", 1500);
|
||||||
|
filterConfig.spamMaxMessages = spam.getInt("max-messages", 3);
|
||||||
|
filterConfig.spamMessage = spam.getString("message", "&cNicht so schnell!");
|
||||||
|
}
|
||||||
|
Configuration dup = cf.getSection("duplicate-check");
|
||||||
|
if (dup != null) {
|
||||||
|
filterConfig.duplicateCheckEnabled = dup.getBoolean("enabled", true);
|
||||||
|
filterConfig.duplicateMessage = dup.getString("message", "&cKeine identischen Nachrichten.");
|
||||||
|
}
|
||||||
|
Configuration bl = cf.getSection("blacklist");
|
||||||
|
if (bl != null) {
|
||||||
|
filterConfig.blacklistEnabled = bl.getBoolean("enabled", true);
|
||||||
|
filterConfig.blacklistWords.clear();
|
||||||
|
// words ist eine YAML-Liste, nicht eine Section → getList() verwenden
|
||||||
|
try {
|
||||||
|
java.util.List<?> wordList = bl.getList("words");
|
||||||
|
if (wordList != null) {
|
||||||
|
for (Object o : wordList) {
|
||||||
|
if (o != null && !o.toString().trim().isEmpty()) {
|
||||||
|
filterConfig.blacklistWords.add(o.toString().trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
Configuration caps = cf.getSection("caps-filter");
|
||||||
|
if (caps != null) {
|
||||||
|
filterConfig.capsFilterEnabled = caps.getBoolean("enabled", true);
|
||||||
|
filterConfig.capsMinLength = caps.getInt("min-length", 6);
|
||||||
|
filterConfig.capsMaxPercent = caps.getInt("max-percent", 70);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Mentions ---
|
||||||
|
Configuration mn = config.getSection("mentions");
|
||||||
|
if (mn != null) {
|
||||||
|
mentionsEnabled = mn.getBoolean("enabled", true);
|
||||||
|
mentionsHighlightColor = mn.getString("highlight-color", "&e&l");
|
||||||
|
mentionsSound = mn.getString("sound", "ENTITY_EXPERIENCE_ORB_PICKUP");
|
||||||
|
mentionsAllowToggle = mn.getBoolean("allow-toggle", true);
|
||||||
|
mentionsNotifyPrefix = mn.getString("notify-prefix", "&e&l[Mention] &r");
|
||||||
|
} else {
|
||||||
|
mentionsEnabled = true;
|
||||||
|
mentionsHighlightColor = "&e&l";
|
||||||
|
mentionsSound = "ENTITY_EXPERIENCE_ORB_PICKUP";
|
||||||
|
mentionsAllowToggle = true;
|
||||||
|
mentionsNotifyPrefix = "&e&l[Mention] &r";
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Chat-History ---
|
||||||
|
Configuration ch = config.getSection("chat-history");
|
||||||
|
if (ch != null) {
|
||||||
|
historyMaxLines = ch.getInt("max-lines", 50);
|
||||||
|
historyDefaultLines = ch.getInt("default-lines", 10);
|
||||||
|
} else {
|
||||||
|
historyMaxLines = 50;
|
||||||
|
historyDefaultLines = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Admin ---
|
||||||
|
Configuration adm = config.getSection("admin");
|
||||||
|
if (adm != null) {
|
||||||
|
adminBypassPermission = adm.getString("bypass-permission", "chat.admin.bypass");
|
||||||
|
adminNotifyPermission = adm.getString("notify-permission", "chat.admin.notify");
|
||||||
|
} else {
|
||||||
|
adminBypassPermission = "chat.admin.bypass";
|
||||||
|
adminNotifyPermission = "chat.admin.notify";
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Server-Farben ---
|
||||||
|
serverColors.clear();
|
||||||
|
serverDisplayNames.clear();
|
||||||
|
Configuration sc = config.getSection("server-colors");
|
||||||
|
if (sc != null) {
|
||||||
|
serverColorDefault = sc.getString("default", "&7");
|
||||||
|
for (String key : sc.getKeys()) {
|
||||||
|
if (key.equals("default")) continue;
|
||||||
|
// Neues Format: server hat Untersektion mit color + display
|
||||||
|
Configuration sub = sc.getSection(key);
|
||||||
|
if (sub != null) {
|
||||||
|
serverColors.put(key.toLowerCase(), sub.getString("color", "&7"));
|
||||||
|
String display = sub.getString("display", "");
|
||||||
|
if (!display.isEmpty()) {
|
||||||
|
serverDisplayNames.put(key.toLowerCase(), display);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Altes Format: server: "&a" (nur Farbe, kein display)
|
||||||
|
serverColors.put(key.toLowerCase(), sc.getString(key, "&7"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
serverColorDefault = "&7";
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Chatlog (NEU) ---
|
||||||
|
Configuration cl = config.getSection("chatlog");
|
||||||
|
if (cl != null) {
|
||||||
|
chatlogEnabled = cl.getBoolean("enabled", true);
|
||||||
|
// Nur 7 oder 14 erlaubt; Standardwert 7
|
||||||
|
int raw = cl.getInt("retention-days", 7);
|
||||||
|
chatlogRetentionDays = (raw == 14) ? 14 : 7;
|
||||||
|
} else {
|
||||||
|
chatlogEnabled = true;
|
||||||
|
chatlogRetentionDays = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Reports (NEU) ---
|
||||||
|
Configuration rp = config.getSection("reports");
|
||||||
|
if (rp != null) {
|
||||||
|
reportsEnabled = rp.getBoolean("enabled", true);
|
||||||
|
reportWebhookEnabled = rp.getBoolean("webhook-enabled", false);
|
||||||
|
reportConfirm = rp.getString("confirm-message",
|
||||||
|
"&aDein Report &8(§f{id}&8) &awurde eingereicht. Danke!");
|
||||||
|
reportPermission = rp.getString("report-permission", ""); // leer = jeder
|
||||||
|
reportClosePermission= rp.getString("close-permission", "chat.admin.bypass");
|
||||||
|
reportViewPermission = rp.getString("view-permission", "chat.admin.bypass");
|
||||||
|
reportCooldown = rp.getInt("cooldown", 60);
|
||||||
|
reportDiscordWebhook = rp.getString("discord-webhook", "");
|
||||||
|
reportTelegramChatId = rp.getString("telegram-chat-id", "");
|
||||||
|
} else {
|
||||||
|
reportsEnabled = true;
|
||||||
|
reportWebhookEnabled = false;
|
||||||
|
reportConfirm = "&aDein Report &8({id}) &awurde eingereicht. Danke!";
|
||||||
|
reportPermission = "";
|
||||||
|
reportClosePermission = "chat.admin.bypass";
|
||||||
|
reportViewPermission = "chat.admin.bypass";
|
||||||
|
reportCooldown = 60;
|
||||||
|
reportDiscordWebhook = "";
|
||||||
|
reportTelegramChatId = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Getter (bestehend) =====
|
||||||
|
|
||||||
|
public Map<String, ChatChannel> getChannels() { return Collections.unmodifiableMap(channels); }
|
||||||
|
public ChatChannel getChannel(String id) { return channels.get(id == null ? defaultChannel : id.toLowerCase()); }
|
||||||
|
public ChatChannel getDefaultChannel() { return channels.getOrDefault(defaultChannel, channels.values().iterator().next()); }
|
||||||
|
public String getDefaultChannelId() { return defaultChannel; }
|
||||||
|
|
||||||
|
public String getHelpopFormat() { return helpopFormat; }
|
||||||
|
public String getHelpopPermission() { return helpopPermission; }
|
||||||
|
public int getHelpopCooldown() { return helpopCooldown; }
|
||||||
|
public String getHelpopConfirm() { return helpopConfirm; }
|
||||||
|
public String getHelpopDiscordWebhook() { return helpopDiscordWebhook; }
|
||||||
|
public String getHelpopTelegramChatId() { return helpopTelegramChatId; }
|
||||||
|
|
||||||
|
public String getBroadcastFormat() { return broadcastFormat; }
|
||||||
|
public String getBroadcastPermission() { return broadcastPermission; }
|
||||||
|
|
||||||
|
public boolean isPmEnabled() { return pmEnabled; }
|
||||||
|
public String getPmFormatSender() { return pmFormatSender; }
|
||||||
|
public String getPmFormatReceiver() { return pmFormatReceiver; }
|
||||||
|
public String getPmFormatSpy() { return pmFormatSpy; }
|
||||||
|
public String getPmSpyPermission() { return pmSpyPermission; }
|
||||||
|
|
||||||
|
public int getDefaultMuteDuration() { return defaultMuteDuration; }
|
||||||
|
public String getMutedMessage() { return mutedMessage; }
|
||||||
|
|
||||||
|
public boolean isEmojiEnabled() { return emojiEnabled; }
|
||||||
|
public boolean isEmojiBedrockSupport() { return emojiBedrockSupport; }
|
||||||
|
public Map<String, String> getEmojiMappings() { return Collections.unmodifiableMap(emojiMappings); }
|
||||||
|
|
||||||
|
public boolean isDiscordEnabled() { return discordEnabled; }
|
||||||
|
public String getDiscordBotToken() { return discordBotToken; }
|
||||||
|
public String getDiscordGuildId() { return discordGuildId; }
|
||||||
|
public int getDiscordPollInterval() { return discordPollInterval; }
|
||||||
|
public String getDiscordFromFormat() { return discordFromFormat; }
|
||||||
|
public String getDiscordAdminChannelId() { return discordAdminChannelId; }
|
||||||
|
public String getDiscordEmbedColor() { return discordEmbedColor; }
|
||||||
|
|
||||||
|
public boolean isTelegramEnabled() { return telegramEnabled; }
|
||||||
|
public String getTelegramBotToken() { return telegramBotToken; }
|
||||||
|
public int getTelegramPollInterval() { return telegramPollInterval; }
|
||||||
|
public String getTelegramFromFormat() { return telegramFromFormat; }
|
||||||
|
public String getTelegramAdminChatId() { return telegramAdminChatId; }
|
||||||
|
public int getTelegramChatTopicId() { return telegramChatTopicId; }
|
||||||
|
public int getTelegramAdminTopicId() { return telegramAdminTopicId; }
|
||||||
|
|
||||||
|
// ===== Getter (Account-Linking) =====
|
||||||
|
public boolean isLinkingEnabled() { return linkingEnabled; }
|
||||||
|
public String getLinkDiscordMessage() { return linkDiscordMessage; }
|
||||||
|
public String getLinkTelegramMessage() { return linkTelegramMessage; }
|
||||||
|
public String getLinkSuccessDiscord() { return linkSuccessDiscord; }
|
||||||
|
public String getLinkSuccessTelegram() { return linkSuccessTelegram; }
|
||||||
|
public String getLinkBotSuccessDiscord() { return linkBotSuccessDiscord; }
|
||||||
|
public String getLinkBotSuccessTelegram() { return linkBotSuccessTelegram; }
|
||||||
|
public String getLinkedDiscordFormat() { return linkedDiscordFormat; }
|
||||||
|
public String getLinkedTelegramFormat() { return linkedTelegramFormat; }
|
||||||
|
|
||||||
|
public String getAdminBypassPermission() { return adminBypassPermission; }
|
||||||
|
public String getAdminNotifyPermission() { return adminNotifyPermission; }
|
||||||
|
|
||||||
|
// ===== Getter (NEU: Server-Farben) =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt den konfigurierten Farb-Code für einen Server zurück.
|
||||||
|
* Unterstützt &-Codes und &#RRGGBB HEX-Codes.
|
||||||
|
* Fallback: "default"-Eintrag, dann "&7".
|
||||||
|
*/
|
||||||
|
public String getServerColor(String serverName) {
|
||||||
|
if (serverName == null) return serverColorDefault;
|
||||||
|
String color = serverColors.get(serverName.toLowerCase());
|
||||||
|
return color != null ? color : serverColorDefault;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getServerColors() { return Collections.unmodifiableMap(serverColors); }
|
||||||
|
public String getServerColorDefault() { return serverColorDefault; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt den konfigurierten Anzeigenamen für einen Server zurück.
|
||||||
|
* Fallback: echter Servername (unverändert).
|
||||||
|
*/
|
||||||
|
public String getServerDisplay(String serverName) {
|
||||||
|
if (serverName == null) return "";
|
||||||
|
String display = serverDisplayNames.get(serverName.toLowerCase());
|
||||||
|
return display != null ? display : serverName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Getter (NEU: Chatlog) =====
|
||||||
|
|
||||||
|
public boolean isChatlogEnabled() { return chatlogEnabled; }
|
||||||
|
public int getChatlogRetentionDays() { return chatlogRetentionDays; }
|
||||||
|
|
||||||
|
// ===== Getter (NEU: Reports) =====
|
||||||
|
|
||||||
|
public boolean isReportsEnabled() { return reportsEnabled; }
|
||||||
|
public String getReportConfirm() { return reportConfirm; }
|
||||||
|
public String getReportPermission() { return reportPermission; }
|
||||||
|
public String getReportClosePermission() { return reportClosePermission; }
|
||||||
|
public String getReportViewPermission() { return reportViewPermission; }
|
||||||
|
public int getReportCooldown() { return reportCooldown; }
|
||||||
|
public String getReportDiscordWebhook() { return reportDiscordWebhook; }
|
||||||
|
public String getReportTelegramChatId() { return reportTelegramChatId; }
|
||||||
|
public boolean isReportWebhookEnabled() { return reportWebhookEnabled; }
|
||||||
|
|
||||||
|
// ===== Getter (Chat-Filter) =====
|
||||||
|
public ChatFilter.ChatFilterConfig getFilterConfig() { return filterConfig; }
|
||||||
|
|
||||||
|
// ===== Getter (Mentions) =====
|
||||||
|
public boolean isMentionsEnabled() { return mentionsEnabled; }
|
||||||
|
public String getMentionsHighlightColor() { return mentionsHighlightColor; }
|
||||||
|
public String getMentionsSound() { return mentionsSound; }
|
||||||
|
public boolean isMentionsAllowToggle() { return mentionsAllowToggle; }
|
||||||
|
public String getMentionsNotifyPrefix() { return mentionsNotifyPrefix; }
|
||||||
|
|
||||||
|
// ===== Getter (Chat-History) =====
|
||||||
|
public int getHistoryMaxLines() { return historyMaxLines; }
|
||||||
|
public int getHistoryDefaultLines() { return historyDefaultLines; }
|
||||||
|
}
|
||||||
231
src/main/java/net/viper/status/modules/chat/ChatFilter.java
Normal file
231
src/main/java/net/viper/status/modules/chat/ChatFilter.java
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
package net.viper.status.modules.chat;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chat-Filter: Anti-Spam, Caps-Filter, Wort-Blacklist, Farbcode-Filter.
|
||||||
|
*
|
||||||
|
* Reihenfolge der Prüfungen in processChat():
|
||||||
|
* 1. Spam-Cooldown (zu schnell geschrieben?)
|
||||||
|
* 2. Gleiche Nachricht wiederholt?
|
||||||
|
* 3. Zu viele Großbuchstaben?
|
||||||
|
* 4. Verbotene Wörter → ersetzen durch ****
|
||||||
|
* 5. Farbcodes (& Codes) → nur mit Permission erlaubt
|
||||||
|
*/
|
||||||
|
public class ChatFilter {
|
||||||
|
|
||||||
|
private final ChatFilterConfig cfg;
|
||||||
|
|
||||||
|
// UUID → letzter Nachricht-Zeitstempel (ms)
|
||||||
|
private final Map<UUID, Long> lastMessageTime = new ConcurrentHashMap<>();
|
||||||
|
// UUID → letzte Nachricht (für Duplikat-Check)
|
||||||
|
private final Map<UUID, String> lastMessageText = new ConcurrentHashMap<>();
|
||||||
|
// UUID → Spam-Zähler (aufeinanderfolgende schnelle Nachrichten)
|
||||||
|
private final Map<UUID, Integer> spamCount = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// Kompilierte Regex-Pattern für Blacklist-Wörter
|
||||||
|
private final List<Pattern> blacklistPatterns = new ArrayList<>();
|
||||||
|
|
||||||
|
public ChatFilter(ChatFilterConfig cfg) {
|
||||||
|
this.cfg = cfg;
|
||||||
|
compilePatterns();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void compilePatterns() {
|
||||||
|
blacklistPatterns.clear();
|
||||||
|
for (String word : cfg.blacklistWords) {
|
||||||
|
// Case-insensitiv, ganzes Wort oder Teilwort je nach Config
|
||||||
|
blacklistPatterns.add(Pattern.compile(
|
||||||
|
"(?i)" + Pattern.quote(word)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Ergebnis-Klasse =====
|
||||||
|
|
||||||
|
public enum FilterResult {
|
||||||
|
ALLOWED, // Nachricht darf durch
|
||||||
|
BLOCKED, // Nachricht blockiert (Spam/Flood)
|
||||||
|
MODIFIED // Nachricht wurde verändert (Wörter ersetzt / Caps reduziert)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class FilterResponse {
|
||||||
|
public final FilterResult result;
|
||||||
|
public final String message; // ggf. modifizierte Nachricht
|
||||||
|
public final String denyReason; // Nachricht an den Spieler wenn BLOCKED
|
||||||
|
|
||||||
|
FilterResponse(FilterResult result, String message, String denyReason) {
|
||||||
|
this.result = result;
|
||||||
|
this.message = message;
|
||||||
|
this.denyReason = denyReason;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Haupt-Filtermethode =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wendet alle aktiven Filter auf eine Nachricht an.
|
||||||
|
*
|
||||||
|
* @param uuid UUID des sendenden Spielers
|
||||||
|
* @param message Originalnachricht
|
||||||
|
* @param isAdmin true → Farbcodes und Caps-Filter überspringen
|
||||||
|
* @param hasColorPerm true → &-Farbcodes erlaubt
|
||||||
|
* @param hasFormatPerm true → &l, &o etc. erlaubt
|
||||||
|
* @return FilterResponse mit Ergebnis und ggf. modifizierter Nachricht
|
||||||
|
*/
|
||||||
|
public FilterResponse filter(UUID uuid, String message, boolean isAdmin,
|
||||||
|
boolean hasColorPerm, boolean hasFormatPerm) {
|
||||||
|
|
||||||
|
// ── 1. Spam-Cooldown ──
|
||||||
|
if (cfg.antiSpamEnabled && !isAdmin) {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
Long last = lastMessageTime.get(uuid);
|
||||||
|
if (last != null && (now - last) < cfg.spamCooldownMs) {
|
||||||
|
int count = spamCount.merge(uuid, 1, Integer::sum);
|
||||||
|
if (count >= cfg.spamMaxMessages) {
|
||||||
|
return new FilterResponse(FilterResult.BLOCKED, message, cfg.spamMessage);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
spamCount.put(uuid, 0);
|
||||||
|
}
|
||||||
|
lastMessageTime.put(uuid, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 2. Duplikat-Check ──
|
||||||
|
if (cfg.duplicateCheckEnabled && !isAdmin) {
|
||||||
|
String lastText = lastMessageText.get(uuid);
|
||||||
|
if (message.equalsIgnoreCase(lastText)) {
|
||||||
|
return new FilterResponse(FilterResult.BLOCKED, message, cfg.duplicateMessage);
|
||||||
|
}
|
||||||
|
lastMessageText.put(uuid, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
String result = message;
|
||||||
|
boolean modified = false;
|
||||||
|
|
||||||
|
// ── 3. Blacklist ──
|
||||||
|
if (cfg.blacklistEnabled) {
|
||||||
|
String filtered = applyBlacklist(result);
|
||||||
|
if (!filtered.equals(result)) {
|
||||||
|
result = filtered;
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 4. Caps-Filter ──
|
||||||
|
if (cfg.capsFilterEnabled && !isAdmin) {
|
||||||
|
String capped = applyCapsFilter(result);
|
||||||
|
if (!capped.equals(result)) {
|
||||||
|
result = capped;
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 5. Farbcodes filtern (nur wenn keine Permission) ──
|
||||||
|
if (!isAdmin) {
|
||||||
|
String colorFiltered = applyColorFilter(result, hasColorPerm, hasFormatPerm);
|
||||||
|
if (!colorFiltered.equals(result)) {
|
||||||
|
result = colorFiltered;
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FilterResponse(
|
||||||
|
modified ? FilterResult.MODIFIED : FilterResult.ALLOWED,
|
||||||
|
result,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Einzelne Filter =====
|
||||||
|
|
||||||
|
private String applyBlacklist(String message) {
|
||||||
|
String result = message;
|
||||||
|
for (Pattern p : blacklistPatterns) {
|
||||||
|
result = p.matcher(result).replaceAll(buildStars(p.pattern()
|
||||||
|
.replace("(?i)", "").replace("\\Q", "").replace("\\E", "").length()));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String applyCapsFilter(String message) {
|
||||||
|
// Zähle Großbuchstaben
|
||||||
|
int total = 0, upper = 0;
|
||||||
|
for (char c : message.toCharArray()) {
|
||||||
|
if (Character.isLetter(c)) { total++; if (Character.isUpperCase(c)) upper++; }
|
||||||
|
}
|
||||||
|
if (total < cfg.capsMinLength) return message; // Kurze Nachrichten ignorieren
|
||||||
|
double ratio = total > 0 ? (double) upper / total : 0;
|
||||||
|
if (ratio < cfg.capsMaxPercent / 100.0) return message;
|
||||||
|
// Zu viele Caps → alles lowercase
|
||||||
|
return message.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entfernt &-Farbcodes je nach Permission.
|
||||||
|
* hasColorPerm → &0-&9, &a-&f erlaubt
|
||||||
|
* hasFormatPerm → &l, &o, &n, &m, &k erlaubt
|
||||||
|
* Beide false → alle &-Codes entfernen
|
||||||
|
*/
|
||||||
|
private String applyColorFilter(String message, boolean hasColorPerm, boolean hasFormatPerm) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < message.length(); i++) {
|
||||||
|
char c = message.charAt(i);
|
||||||
|
if (c == '&' && i + 1 < message.length()) {
|
||||||
|
char next = Character.toLowerCase(message.charAt(i + 1));
|
||||||
|
boolean isColor = (next >= '0' && next <= '9') || (next >= 'a' && next <= 'f');
|
||||||
|
boolean isFormat = "lonmkr".indexOf(next) >= 0;
|
||||||
|
boolean isHex = next == '#';
|
||||||
|
|
||||||
|
if (isColor && hasColorPerm) { sb.append(c); continue; }
|
||||||
|
if (isFormat && hasFormatPerm) { sb.append(c); continue; }
|
||||||
|
if (isHex && hasColorPerm) { sb.append(c); continue; }
|
||||||
|
|
||||||
|
// 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; }
|
||||||
|
}
|
||||||
|
sb.append(c);
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String buildStars(int length) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < Math.max(length, 4); i++) sb.append('*');
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Cleanup beim Logout =====
|
||||||
|
|
||||||
|
public void cleanup(UUID uuid) {
|
||||||
|
lastMessageTime.remove(uuid);
|
||||||
|
lastMessageText.remove(uuid);
|
||||||
|
spamCount.remove(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Konfigurationsklasse =====
|
||||||
|
|
||||||
|
public static class ChatFilterConfig {
|
||||||
|
// Anti-Spam
|
||||||
|
public boolean antiSpamEnabled = true;
|
||||||
|
public long spamCooldownMs = 1500; // ms zwischen Nachrichten
|
||||||
|
public int spamMaxMessages = 3; // max. Nachrichten innerhalb Cooldown
|
||||||
|
public String spamMessage = "&cBitte nicht so schnell schreiben!";
|
||||||
|
|
||||||
|
// Duplikat
|
||||||
|
public boolean duplicateCheckEnabled = true;
|
||||||
|
public String duplicateMessage = "&cBitte keine identischen Nachrichten senden.";
|
||||||
|
|
||||||
|
// Blacklist
|
||||||
|
public boolean blacklistEnabled = true;
|
||||||
|
public List<String> blacklistWords = new ArrayList<>();
|
||||||
|
|
||||||
|
// Caps
|
||||||
|
public boolean capsFilterEnabled = true;
|
||||||
|
public int capsMinLength = 6; // Mindestlänge für Caps-Check
|
||||||
|
public int capsMaxPercent = 70; // Max. % Großbuchstaben
|
||||||
|
}
|
||||||
|
}
|
||||||
152
src/main/java/net/viper/status/modules/chat/ChatLogger.java
Normal file
152
src/main/java/net/viper/status/modules/chat/ChatLogger.java
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
package net.viper.status.modules.chat;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protokolliert alle Chat-Nachrichten in tagesweise rotierende Logdateien.
|
||||||
|
*
|
||||||
|
* Verzeichnis: plugins/StatusAPI/chatlogs/chatlog_YYYY-MM-DD.log
|
||||||
|
* Format: [HH:mm:ss] [MSG-XXXXXX] [SERVER] [CHANNEL] Spieler: Nachricht
|
||||||
|
*
|
||||||
|
* Alte Logs werden beim Start und täglich automatisch bereinigt.
|
||||||
|
* Die Aufbewahrungsdauer ist in der chat.yml konfigurierbar (7 oder 14 Tage).
|
||||||
|
*/
|
||||||
|
public class ChatLogger {
|
||||||
|
|
||||||
|
private final File logDir;
|
||||||
|
private final Logger logger;
|
||||||
|
private final int retentionDays;
|
||||||
|
private final AtomicInteger counter = new AtomicInteger(0);
|
||||||
|
|
||||||
|
private static final SimpleDateFormat DATE_FMT = new SimpleDateFormat("yyyy-MM-dd");
|
||||||
|
private static final SimpleDateFormat TIME_FMT = new SimpleDateFormat("HH:mm:ss");
|
||||||
|
|
||||||
|
public ChatLogger(File dataFolder, Logger logger, int retentionDays) {
|
||||||
|
this.logDir = new File(dataFolder, "chatlogs");
|
||||||
|
this.logger = logger;
|
||||||
|
this.retentionDays = Math.max(1, retentionDays);
|
||||||
|
this.logDir.mkdirs();
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Nachrichten-ID =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generiert eine eindeutige Nachrichten-ID (z.B. MSG-A3F2B1).
|
||||||
|
* Kombiniert Zeitstempel + inkrementellen Zähler für Eindeutigkeit.
|
||||||
|
*/
|
||||||
|
public String generateMessageId() {
|
||||||
|
int seq = counter.incrementAndGet();
|
||||||
|
long ts = System.currentTimeMillis();
|
||||||
|
int hash = (int)(ts ^ (ts >>> 32)) ^ (seq * 0x9E3779B9);
|
||||||
|
return "MSG-" + String.format("%06X", hash & 0xFFFFFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Logging =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loggt eine Nachricht und gibt die generierte Nachrichten-ID zurück.
|
||||||
|
*
|
||||||
|
* @param msgId Vorher generierte ID (aus generateMessageId())
|
||||||
|
* @param server Servername des Absenders
|
||||||
|
* @param channel Kanal-ID
|
||||||
|
* @param player Spielername
|
||||||
|
* @param message Nachrichtentext (Rohtext, ohne Farbcodes)
|
||||||
|
*/
|
||||||
|
public void log(String msgId, String server, String channel, String player, String message) {
|
||||||
|
String date = DATE_FMT.format(new Date());
|
||||||
|
String time = TIME_FMT.format(new Date());
|
||||||
|
|
||||||
|
// Minecraft-Farbcodes aus dem Log entfernen
|
||||||
|
String cleanMsg = stripColor(message);
|
||||||
|
|
||||||
|
String line = "[" + time + "] [" + msgId + "] [" + server + "] [" + channel + "] "
|
||||||
|
+ player + ": " + cleanMsg;
|
||||||
|
|
||||||
|
File logFile = new File(logDir, "chatlog_" + date + ".log");
|
||||||
|
try (BufferedWriter bw = new BufferedWriter(
|
||||||
|
new OutputStreamWriter(new FileOutputStream(logFile, true), "UTF-8"))) {
|
||||||
|
bw.write(line);
|
||||||
|
bw.newLine();
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warning("[ChatLogger] Fehler beim Schreiben: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Cleanup =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Löscht Log-Dateien, die älter als retentionDays Tage sind.
|
||||||
|
* Wird beim Start und kann manuell aufgerufen werden.
|
||||||
|
*/
|
||||||
|
public void cleanup() {
|
||||||
|
if (!logDir.exists()) return;
|
||||||
|
long cutoff = System.currentTimeMillis() - ((long) retentionDays * 24L * 60L * 60L * 1000L);
|
||||||
|
File[] files = logDir.listFiles((dir, name) ->
|
||||||
|
name.startsWith("chatlog_") && name.endsWith(".log"));
|
||||||
|
if (files == null) return;
|
||||||
|
for (File f : files) {
|
||||||
|
if (f.lastModified() < cutoff) {
|
||||||
|
if (f.delete()) {
|
||||||
|
logger.info("[ChatLogger] Altes Log gelöscht: " + f.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Hilfsmethoden =====
|
||||||
|
|
||||||
|
/** Entfernt §-Farbcodes aus dem Text. */
|
||||||
|
private static String stripColor(String input) {
|
||||||
|
if (input == null) return "";
|
||||||
|
return input.replaceAll("(?i)§[0-9A-FK-OR]", "")
|
||||||
|
.replaceAll("(?i)&[0-9A-FK-OR]", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRetentionDays() { return retentionDays; }
|
||||||
|
public File getLogDir() { return logDir; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest die letzten `maxLines` Zeilen aus dem heutigen Chatlog.
|
||||||
|
* Wenn ein Spielername angegeben ist, werden nur seine Zeilen zurückgegeben.
|
||||||
|
*
|
||||||
|
* @param playerFilter Spielername (case-insensitiv) oder null für alle
|
||||||
|
* @param maxLines Maximale Anzahl zurückgegebener Zeilen
|
||||||
|
* @return Liste der Logzeilen (älteste zuerst)
|
||||||
|
*/
|
||||||
|
public List<String> readLastLines(String playerFilter, int maxLines) {
|
||||||
|
String date = DATE_FMT.format(new Date());
|
||||||
|
File logFile = new File(logDir, "chatlog_" + date + ".log");
|
||||||
|
if (!logFile.exists()) return Collections.emptyList();
|
||||||
|
|
||||||
|
List<String> allLines = new ArrayList<>();
|
||||||
|
try (BufferedReader br = new BufferedReader(
|
||||||
|
new InputStreamReader(new FileInputStream(logFile), "UTF-8"))) {
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
if (line.trim().isEmpty()) continue;
|
||||||
|
// Spieler-Filter: Format ist [...] [...] [...] [...] Spieler: Nachricht
|
||||||
|
if (playerFilter != null) {
|
||||||
|
// Spielername steht nach dem 4. [...]-Block
|
||||||
|
int lastBracket = line.indexOf("] ", line.lastIndexOf("["));
|
||||||
|
if (lastBracket >= 0) {
|
||||||
|
String rest = line.substring(lastBracket + 2);
|
||||||
|
String name = rest.contains(":") ? rest.substring(0, rest.indexOf(":")).trim() : "";
|
||||||
|
if (!name.equalsIgnoreCase(playerFilter)) continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allLines.add(line);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warning("[ChatLogger] Fehler beim Lesen: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Letzte maxLines zurückgeben
|
||||||
|
if (allLines.size() <= maxLines) return allLines;
|
||||||
|
return allLines.subList(allLines.size() - maxLines, allLines.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
1364
src/main/java/net/viper/status/modules/chat/ChatModule.java
Normal file
1364
src/main/java/net/viper/status/modules/chat/ChatModule.java
Normal file
File diff suppressed because it is too large
Load Diff
53
src/main/java/net/viper/status/modules/chat/EmojiParser.java
Normal file
53
src/main/java/net/viper/status/modules/chat/EmojiParser.java
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package net.viper.status.modules.chat;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ersetzt Emoji-Shortcuts (:smile:, :heart:, …) durch Unicode-Zeichen.
|
||||||
|
*
|
||||||
|
* Bedrock-Spieler (Geyser) unterstützen Unicode-Emojis ebenfalls,
|
||||||
|
* da sie als reguläre UTF-8 Zeichen in TextComponents übertragen werden.
|
||||||
|
*/
|
||||||
|
public class EmojiParser {
|
||||||
|
|
||||||
|
private final Map<String, String> mappings;
|
||||||
|
private final boolean enabled;
|
||||||
|
|
||||||
|
public EmojiParser(Map<String, String> mappings, boolean enabled) {
|
||||||
|
this.mappings = mappings;
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konvertiert alle bekannten Emoji-Shortcuts in der Nachricht zu Unicode.
|
||||||
|
* Nicht erkannte Shortcuts bleiben unverändert.
|
||||||
|
*
|
||||||
|
* @param message Die Originalnachricht des Spielers
|
||||||
|
* @return Nachricht mit ersetzten Emojis
|
||||||
|
*/
|
||||||
|
public String parse(String message) {
|
||||||
|
if (!enabled || message == null || message.isEmpty()) return message;
|
||||||
|
|
||||||
|
String result = message;
|
||||||
|
for (Map.Entry<String, String> entry : mappings.entrySet()) {
|
||||||
|
result = result.replace(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt eine lesbare Liste aller Emojis zurück (für /emoji list).
|
||||||
|
*/
|
||||||
|
public String buildEmojiList() {
|
||||||
|
if (mappings.isEmpty()) return "&cKeine Emojis konfiguriert.";
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("&eVerfügbare Emojis:\n");
|
||||||
|
int i = 0;
|
||||||
|
for (Map.Entry<String, String> entry : mappings.entrySet()) {
|
||||||
|
sb.append("&7").append(entry.getKey()).append(" &f→ ").append(entry.getValue());
|
||||||
|
if (i < mappings.size() - 1) sb.append(" ");
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
124
src/main/java/net/viper/status/modules/chat/MuteManager.java
Normal file
124
src/main/java/net/viper/status/modules/chat/MuteManager.java
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
package net.viper.status.modules.chat;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verwaltet Mutes von Spielern.
|
||||||
|
* Speichert: UUID → Ablaufzeitpunkt (Unix-Sekunden, 0 = permanent)
|
||||||
|
*
|
||||||
|
* Admins/OPs mit dem Bypass-Permission können nicht gemutet werden.
|
||||||
|
*/
|
||||||
|
public class MuteManager {
|
||||||
|
|
||||||
|
private final File file;
|
||||||
|
private final Logger logger;
|
||||||
|
|
||||||
|
// UUID → Ablaufzeitpunkt (0 = permanent)
|
||||||
|
private final ConcurrentHashMap<UUID, Long> mutes = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public MuteManager(File dataFolder, Logger logger) {
|
||||||
|
this.file = new File(dataFolder, "chat_mutes.dat");
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Mute-Logik =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mutet einen Spieler für durationMinutes Minuten.
|
||||||
|
* durationMinutes = 0 → permanent
|
||||||
|
*/
|
||||||
|
public void mute(UUID uuid, int durationMinutes) {
|
||||||
|
long expiry = (durationMinutes <= 0)
|
||||||
|
? 0L
|
||||||
|
: (System.currentTimeMillis() / 1000L) + ((long) durationMinutes * 60);
|
||||||
|
mutes.put(uuid, expiry);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hebt den Mute auf. */
|
||||||
|
public void unmute(UUID uuid) {
|
||||||
|
mutes.remove(uuid);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Prüft ob ein Spieler aktuell gemutet ist. */
|
||||||
|
public boolean isMuted(UUID uuid) {
|
||||||
|
Long expiry = mutes.get(uuid);
|
||||||
|
if (expiry == null) return false;
|
||||||
|
if (expiry == 0L) return true; // permanent
|
||||||
|
if (System.currentTimeMillis() / 1000L >= expiry) {
|
||||||
|
// Abgelaufen → entfernen
|
||||||
|
mutes.remove(uuid);
|
||||||
|
save();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt die verbleibende Zeit als lesbaren String zurück.
|
||||||
|
* Gibt "permanent" zurück bei dauerhaftem Mute.
|
||||||
|
*/
|
||||||
|
public String getRemainingTime(UUID uuid) {
|
||||||
|
Long expiry = mutes.get(uuid);
|
||||||
|
if (expiry == null) return "0";
|
||||||
|
if (expiry == 0L) return "permanent";
|
||||||
|
|
||||||
|
long remaining = expiry - (System.currentTimeMillis() / 1000L);
|
||||||
|
if (remaining <= 0) return "0";
|
||||||
|
|
||||||
|
long hours = remaining / 3600;
|
||||||
|
long minutes = (remaining % 3600) / 60;
|
||||||
|
long seconds = remaining % 60;
|
||||||
|
|
||||||
|
if (hours > 0) return hours + "h " + minutes + "m";
|
||||||
|
if (minutes > 0) return minutes + "m " + seconds + "s";
|
||||||
|
return seconds + "s";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Persistenz =====
|
||||||
|
|
||||||
|
public void save() {
|
||||||
|
try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF-8"))) {
|
||||||
|
long now = System.currentTimeMillis() / 1000L;
|
||||||
|
for (Map.Entry<UUID, Long> e : mutes.entrySet()) {
|
||||||
|
// Nur aktive Mutes speichern
|
||||||
|
if (e.getValue() == 0L || e.getValue() > now) {
|
||||||
|
bw.write(e.getKey() + "|" + e.getValue());
|
||||||
|
bw.newLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warning("[ChatModule] Fehler beim Speichern der Mutes: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void load() {
|
||||||
|
mutes.clear();
|
||||||
|
if (!file.exists()) return;
|
||||||
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"))) {
|
||||||
|
String line;
|
||||||
|
long now = System.currentTimeMillis() / 1000L;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
line = line.trim();
|
||||||
|
if (line.isEmpty()) continue;
|
||||||
|
String[] parts = line.split("\\|");
|
||||||
|
if (parts.length < 2) continue;
|
||||||
|
try {
|
||||||
|
UUID uuid = UUID.fromString(parts[0]);
|
||||||
|
long expiry = Long.parseLong(parts[1]);
|
||||||
|
// Nur laden wenn noch aktiv
|
||||||
|
if (expiry == 0L || expiry > now) {
|
||||||
|
mutes.put(uuid, expiry);
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warning("[ChatModule] Fehler beim Laden der Mutes: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package net.viper.status.modules.chat;
|
||||||
|
|
||||||
|
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.connection.ProxiedPlayer;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verwaltet private Nachrichten (/msg, /r) und Social-Spy.
|
||||||
|
*/
|
||||||
|
public class PrivateMsgManager {
|
||||||
|
|
||||||
|
private final BlockManager blockManager;
|
||||||
|
|
||||||
|
// UUID → letzte PM-Gesprächspartner UUID (für /r)
|
||||||
|
private final Map<UUID, UUID> lastPartner = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// UUIDs die Social-Spy aktiviert haben
|
||||||
|
private final java.util.Set<UUID> spyEnabled =
|
||||||
|
java.util.Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||||
|
|
||||||
|
public PrivateMsgManager(BlockManager blockManager) {
|
||||||
|
this.blockManager = blockManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sendet eine private Nachricht von `sender` an `receiver`.
|
||||||
|
*
|
||||||
|
* @param sender Der sendende Spieler
|
||||||
|
* @param receiver Der empfangende Spieler
|
||||||
|
* @param message Die Nachricht
|
||||||
|
* @param config Chat-Konfiguration (Formate)
|
||||||
|
* @param bypassPermission Permission für Admin-Bypass (kann nicht geblockt werden)
|
||||||
|
* @return true wenn erfolgreich gesendet
|
||||||
|
*/
|
||||||
|
public boolean send(ProxiedPlayer sender, ProxiedPlayer receiver,
|
||||||
|
String message, ChatConfig config, String bypassPermission) {
|
||||||
|
|
||||||
|
// Selbst anschreiben verhindern
|
||||||
|
if (sender.getUniqueId().equals(receiver.getUniqueId())) {
|
||||||
|
sender.sendMessage(color("&cDu kannst dir nicht selbst schreiben."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin-Bypass: Wenn Sender Admin ist, kann er nicht geblockt werden
|
||||||
|
boolean senderIsAdmin = sender.hasPermission(bypassPermission);
|
||||||
|
|
||||||
|
// Block-Check (nur wenn Sender kein Admin)
|
||||||
|
if (!senderIsAdmin) {
|
||||||
|
if (!blockManager.canReceive(sender.getUniqueId(), receiver.getUniqueId())) {
|
||||||
|
sender.sendMessage(color("&cDieser Spieler hat dich blockiert oder du hast ihn blockiert."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formatierung
|
||||||
|
String toSender = format(config.getPmFormatSender(), sender.getName(), receiver.getName(), message, true);
|
||||||
|
String toReceiver = format(config.getPmFormatReceiver(), sender.getName(), receiver.getName(), message, false);
|
||||||
|
|
||||||
|
sender.sendMessage(color(toSender));
|
||||||
|
receiver.sendMessage(color(toReceiver));
|
||||||
|
|
||||||
|
// Letzte Partner speichern (für /r)
|
||||||
|
lastPartner.put(sender.getUniqueId(), receiver.getUniqueId());
|
||||||
|
lastPartner.put(receiver.getUniqueId(), sender.getUniqueId());
|
||||||
|
|
||||||
|
// Social Spy
|
||||||
|
String spyMsg = format(config.getPmFormatSpy(), sender.getName(), receiver.getName(), message, true);
|
||||||
|
broadcastSpy(spyMsg, config.getPmSpyPermission(), sender.getUniqueId(), receiver.getUniqueId());
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Antwort-Funktion (/r).
|
||||||
|
* Sucht den letzten Gesprächspartner des Senders.
|
||||||
|
*/
|
||||||
|
public void reply(ProxiedPlayer sender, String message, ChatConfig config, String bypassPermission) {
|
||||||
|
UUID partnerUuid = lastPartner.get(sender.getUniqueId());
|
||||||
|
if (partnerUuid == null) {
|
||||||
|
sender.sendMessage(color("&cDu hast noch keine Nachricht erhalten."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ProxiedPlayer partner = ProxyServer.getInstance().getPlayer(partnerUuid);
|
||||||
|
if (partner == null || !partner.isConnected()) {
|
||||||
|
sender.sendMessage(color("&cDieser Spieler ist nicht mehr online."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
send(sender, partner, message, config, bypassPermission);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Social-Spy umschalten. */
|
||||||
|
public boolean toggleSpy(UUID uuid) {
|
||||||
|
if (spyEnabled.contains(uuid)) {
|
||||||
|
spyEnabled.remove(uuid);
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
spyEnabled.add(uuid);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void broadcastSpy(String formatted, String spyPermission, UUID... exclude) {
|
||||||
|
java.util.Set<UUID> excl = new java.util.HashSet<>(java.util.Arrays.asList(exclude));
|
||||||
|
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
||||||
|
if (excl.contains(p.getUniqueId())) continue;
|
||||||
|
// Spy muss entweder via Permission aktiv oder manuell aktiviert haben
|
||||||
|
boolean hasPerm = p.hasPermission(spyPermission);
|
||||||
|
boolean hasToggle= spyEnabled.contains(p.getUniqueId());
|
||||||
|
if (hasPerm || hasToggle) {
|
||||||
|
p.sendMessage(color(formatted));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formatiert eine PM-Nachricht.
|
||||||
|
* {sender} → Name des Absenders
|
||||||
|
* {receiver} → Name des Empfängers
|
||||||
|
* {player} → Gesprächspartner aus Sicht des jeweiligen Empfängers:
|
||||||
|
* Beim Sender: der Empfänger (an wen schreibt er?)
|
||||||
|
* Beim Empfänger: der Sender (von wem kommt es?)
|
||||||
|
*
|
||||||
|
* @param viewerIsSender true wenn der aktuelle Betrachter der Absender ist
|
||||||
|
*/
|
||||||
|
private String format(String template, String sender, String receiver,
|
||||||
|
String message, boolean viewerIsSender) {
|
||||||
|
String partner = viewerIsSender ? receiver : sender;
|
||||||
|
return template
|
||||||
|
.replace("{sender}", sender)
|
||||||
|
.replace("{receiver}", receiver)
|
||||||
|
.replace("{player}", partner) // Gesprächspartner aus Sicht des Betrachters
|
||||||
|
.replace("{message}", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextComponent color(String text) {
|
||||||
|
return new TextComponent(ChatColor.translateAlternateColorCodes('&', text));
|
||||||
|
}
|
||||||
|
}
|
||||||
227
src/main/java/net/viper/status/modules/chat/ReportManager.java
Normal file
227
src/main/java/net/viper/status/modules/chat/ReportManager.java
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
package net.viper.status.modules.chat;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verwaltet Spieler-Reports (/report).
|
||||||
|
*
|
||||||
|
* Reports werden mit einer eindeutigen ID (z.B. RPT-0001) gespeichert und
|
||||||
|
* bleiben offen, bis ein Admin sie explizit mit /reportclose <ID> schließt.
|
||||||
|
*
|
||||||
|
* Online-Admins werden sofort benachrichtigt.
|
||||||
|
* Offline-Admins erhalten eine verzögerte Benachrichtigung beim nächsten Login
|
||||||
|
* (gesteuert von außen via getPendingNotificationFor()).
|
||||||
|
*
|
||||||
|
* Speicherformat (chat_reports.dat):
|
||||||
|
* id|reporter|reporterUUID|reported|server|messageContext|reason|timestamp|closed|closedBy
|
||||||
|
*/
|
||||||
|
public class ReportManager {
|
||||||
|
|
||||||
|
private final File file;
|
||||||
|
private final Logger logger;
|
||||||
|
|
||||||
|
/** Alle Reports (offen und geschlossen). */
|
||||||
|
private final ConcurrentHashMap<String, ChatReport> reports = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/** Zähler für Report-IDs. Wird beim Laden synchronisiert. */
|
||||||
|
private final AtomicInteger idCounter = new AtomicInteger(0);
|
||||||
|
|
||||||
|
private static final SimpleDateFormat DATE_FMT = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss");
|
||||||
|
|
||||||
|
// ===== Report-Datenklasse =====
|
||||||
|
|
||||||
|
public static class ChatReport {
|
||||||
|
public String id;
|
||||||
|
public String reporterName;
|
||||||
|
public UUID reporterUUID;
|
||||||
|
public String reportedName;
|
||||||
|
public String server;
|
||||||
|
public String messageContext; // letzte bekannte Chatnachricht des Gemeldeten
|
||||||
|
public String reason;
|
||||||
|
public long timestamp;
|
||||||
|
public boolean closed;
|
||||||
|
public String closedBy; // Name des schließenden Admins (oder leer)
|
||||||
|
|
||||||
|
public String getFormattedTime() {
|
||||||
|
return DATE_FMT.format(new Date(timestamp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Konstruktor =====
|
||||||
|
|
||||||
|
public ReportManager(File dataFolder, Logger logger) {
|
||||||
|
this.file = new File(dataFolder, "chat_reports.dat");
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Report-Logik =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt einen neuen Report.
|
||||||
|
*
|
||||||
|
* @param reporterName Name des meldenden Spielers
|
||||||
|
* @param reporterUUID UUID des meldenden Spielers
|
||||||
|
* @param reportedName Name des gemeldeten Spielers
|
||||||
|
* @param server Server, auf dem sich der Reporter befand
|
||||||
|
* @param messageContext Letzte bekannte Nachricht des Gemeldeten (für Kontext)
|
||||||
|
* @param reason Freitext-Begründung
|
||||||
|
* @return die neue Report-ID (z.B. RPT-0001)
|
||||||
|
*/
|
||||||
|
public String createReport(String reporterName, UUID reporterUUID,
|
||||||
|
String reportedName, String server,
|
||||||
|
String messageContext, String reason) {
|
||||||
|
String id = String.format("RPT-%04d", idCounter.incrementAndGet());
|
||||||
|
|
||||||
|
ChatReport report = new ChatReport();
|
||||||
|
report.id = id;
|
||||||
|
report.reporterName = reporterName;
|
||||||
|
report.reporterUUID = reporterUUID;
|
||||||
|
report.reportedName = reportedName;
|
||||||
|
report.server = server;
|
||||||
|
report.messageContext = messageContext != null ? messageContext : "";
|
||||||
|
report.reason = reason;
|
||||||
|
report.timestamp = System.currentTimeMillis();
|
||||||
|
report.closed = false;
|
||||||
|
report.closedBy = "";
|
||||||
|
|
||||||
|
reports.put(id, report);
|
||||||
|
save();
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schließt einen Report.
|
||||||
|
*
|
||||||
|
* @param id Report-ID (z.B. RPT-0001, case-insensitiv)
|
||||||
|
* @param adminName Name des Admins, der den Report schließt
|
||||||
|
* @return true wenn erfolgreich geschlossen, false wenn nicht gefunden / bereits geschlossen
|
||||||
|
*/
|
||||||
|
public boolean closeReport(String id, String adminName) {
|
||||||
|
ChatReport report = getReport(id);
|
||||||
|
if (report == null || report.closed) return false;
|
||||||
|
report.closed = true;
|
||||||
|
report.closedBy = adminName;
|
||||||
|
save();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gibt einen Report nach ID zurück (case-insensitiv). */
|
||||||
|
public ChatReport getReport(String id) {
|
||||||
|
if (id == null) return null;
|
||||||
|
return reports.get(id.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gibt alle offenen Reports chronologisch (älteste zuerst) zurück. */
|
||||||
|
public List<ChatReport> getOpenReports() {
|
||||||
|
List<ChatReport> list = new ArrayList<>();
|
||||||
|
for (ChatReport r : reports.values()) {
|
||||||
|
if (!r.closed) list.add(r);
|
||||||
|
}
|
||||||
|
list.sort(Comparator.comparingLong(r -> r.timestamp));
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gibt alle Reports chronologisch zurück (auch geschlossene). */
|
||||||
|
public List<ChatReport> getAllReports() {
|
||||||
|
List<ChatReport> list = new ArrayList<>(reports.values());
|
||||||
|
list.sort(Comparator.comparingLong(r -> r.timestamp));
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Anzahl offener Reports. */
|
||||||
|
public int getOpenCount() {
|
||||||
|
int count = 0;
|
||||||
|
for (ChatReport r : reports.values()) if (!r.closed) count++;
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Persistenz =====
|
||||||
|
|
||||||
|
public void save() {
|
||||||
|
try (BufferedWriter bw = new BufferedWriter(
|
||||||
|
new OutputStreamWriter(new FileOutputStream(file), "UTF-8"))) {
|
||||||
|
for (ChatReport r : reports.values()) {
|
||||||
|
bw.write(
|
||||||
|
esc(r.id) + "|" +
|
||||||
|
esc(r.reporterName) + "|" +
|
||||||
|
r.reporterUUID + "|" +
|
||||||
|
esc(r.reportedName) + "|" +
|
||||||
|
esc(r.server) + "|" +
|
||||||
|
esc(r.messageContext) + "|" +
|
||||||
|
esc(r.reason) + "|" +
|
||||||
|
r.timestamp + "|" +
|
||||||
|
r.closed + "|" +
|
||||||
|
esc(r.closedBy != null ? r.closedBy : "")
|
||||||
|
);
|
||||||
|
bw.newLine();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warning("[ChatModule] Fehler beim Speichern der Reports: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void load() {
|
||||||
|
reports.clear();
|
||||||
|
if (!file.exists()) return;
|
||||||
|
|
||||||
|
int maxNum = 0;
|
||||||
|
try (BufferedReader br = new BufferedReader(
|
||||||
|
new InputStreamReader(new FileInputStream(file), "UTF-8"))) {
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
line = line.trim();
|
||||||
|
if (line.isEmpty()) continue;
|
||||||
|
String[] p = line.split("\\|", -1);
|
||||||
|
if (p.length < 10) continue;
|
||||||
|
try {
|
||||||
|
ChatReport r = new ChatReport();
|
||||||
|
r.id = unesc(p[0]);
|
||||||
|
r.reporterName = unesc(p[1]);
|
||||||
|
r.reporterUUID = UUID.fromString(p[2]);
|
||||||
|
r.reportedName = unesc(p[3]);
|
||||||
|
r.server = unesc(p[4]);
|
||||||
|
r.messageContext = unesc(p[5]);
|
||||||
|
r.reason = unesc(p[6]);
|
||||||
|
r.timestamp = Long.parseLong(p[7]);
|
||||||
|
r.closed = Boolean.parseBoolean(p[8]);
|
||||||
|
r.closedBy = unesc(p[9]);
|
||||||
|
reports.put(r.id.toUpperCase(), r);
|
||||||
|
|
||||||
|
// Zähler auf höchste bekannte Nummer synchronisieren
|
||||||
|
if (r.id.toUpperCase().startsWith("RPT-")) {
|
||||||
|
try {
|
||||||
|
int num = Integer.parseInt(r.id.substring(4));
|
||||||
|
if (num > maxNum) maxNum = num;
|
||||||
|
} catch (NumberFormatException ignored) {}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
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) =====
|
||||||
|
|
||||||
|
private static String esc(String s) {
|
||||||
|
if (s == null) return "";
|
||||||
|
return s.replace("\\", "\\\\")
|
||||||
|
.replace("|", "\\p")
|
||||||
|
.replace("\n", "\\n")
|
||||||
|
.replace("\r", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String unesc(String s) {
|
||||||
|
if (s == null) return "";
|
||||||
|
return s.replace("\\n", "\n")
|
||||||
|
.replace("\\p", "|")
|
||||||
|
.replace("\\\\", "\\");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user