Update from Git Manager GUI
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package de.ticketsystem;
|
||||
|
||||
import de.ticketsystem.bungee.BungeeMessenger;
|
||||
import de.ticketsystem.commands.TicketCommand;
|
||||
import de.ticketsystem.database.DatabaseManager;
|
||||
import de.ticketsystem.discord.DiscordWebhook;
|
||||
@@ -18,11 +19,20 @@ public class TicketPlugin extends JavaPlugin {
|
||||
private static TicketPlugin instance;
|
||||
|
||||
private boolean debug;
|
||||
|
||||
/**
|
||||
* Name dieses Servers im BungeeCord-Netzwerk.
|
||||
* Konfigurierbar in config.yml → server-name
|
||||
* Wird in Tickets gespeichert und in Benachrichtigungen angezeigt.
|
||||
*/
|
||||
private String serverName;
|
||||
|
||||
private DatabaseManager databaseManager;
|
||||
private TicketManager ticketManager;
|
||||
private CategoryManager categoryManager;
|
||||
private TicketGUI ticketGUI;
|
||||
private DiscordWebhook discordWebhook;
|
||||
private BungeeMessenger bungeeMessenger;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
@@ -33,6 +43,33 @@ public class TicketPlugin extends JavaPlugin {
|
||||
// Ticket-Klasse für YAML-Serialisierung registrieren
|
||||
Ticket.register();
|
||||
|
||||
// ── BungeeCord Plugin-Messaging-Kanäle registrieren ───────────────
|
||||
// Ausgehend: BungeeCord-Standardkanal (für Forward / Message)
|
||||
getServer().getMessenger().registerOutgoingPluginChannel(this, BungeeMessenger.BUNGEE_CHANNEL);
|
||||
// Eingehend & Ausgehend: Eigener Kanal für Team- und Spielerbenachrichtigungen
|
||||
getServer().getMessenger().registerOutgoingPluginChannel(this, BungeeMessenger.CUSTOM_CHANNEL);
|
||||
|
||||
bungeeMessenger = new BungeeMessenger(this);
|
||||
getServer().getMessenger().registerIncomingPluginChannel(this, BungeeMessenger.CUSTOM_CHANNEL, bungeeMessenger);
|
||||
|
||||
// Server-Name aus Config lesen
|
||||
serverName = getConfig().getString("server-name", "unknown");
|
||||
if ("unknown".equals(serverName)) {
|
||||
getLogger().warning("[BungeeCord] Kein 'server-name' in der config.yml definiert! " +
|
||||
"Setze 'server-name: dein-server' für korrekte Cross-Server-Anzeige.");
|
||||
} else {
|
||||
getLogger().info("[BungeeCord] Server-Name: §e" + serverName);
|
||||
}
|
||||
|
||||
// BungeeCord-Hinweis prüfen
|
||||
if (!getConfig().getBoolean("bungeecord", false)) {
|
||||
getLogger().info("[BungeeCord] Hinweis: Cross-Server-Features sind deaktiviert. " +
|
||||
"Setze 'bungeecord: true' in der config.yml und stelle sicher, " +
|
||||
"dass 'bungeecord: true' auch in spigot.yml gesetzt ist.");
|
||||
} else {
|
||||
getLogger().info("[BungeeCord] Cross-Server-Benachrichtigungen aktiviert.");
|
||||
}
|
||||
|
||||
// Update-Checker
|
||||
int resourceId = 132757;
|
||||
new UpdateChecker(this, resourceId).getVersion(version -> {
|
||||
@@ -108,6 +145,10 @@ public class TicketPlugin extends JavaPlugin {
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
// Plugin-Messaging-Kanäle abmelden
|
||||
getServer().getMessenger().unregisterOutgoingPluginChannel(this);
|
||||
getServer().getMessenger().unregisterIncomingPluginChannel(this);
|
||||
|
||||
if (databaseManager != null) databaseManager.disconnect();
|
||||
getLogger().info("TicketSystem wurde deaktiviert.");
|
||||
}
|
||||
@@ -132,5 +173,18 @@ public class TicketPlugin extends JavaPlugin {
|
||||
public CategoryManager getCategoryManager() { return categoryManager; }
|
||||
public TicketGUI getTicketGUI() { return ticketGUI; }
|
||||
public DiscordWebhook getDiscordWebhook() { return discordWebhook; }
|
||||
public BungeeMessenger getBungeeMessenger() { return bungeeMessenger; }
|
||||
public boolean isDebug() { return debug; }
|
||||
|
||||
/**
|
||||
* BungeeCord: Gibt den konfigurierten Server-Namen zurück.
|
||||
* Entspricht dem Wert aus config.yml → server-name.
|
||||
*/
|
||||
public String getServerName() { return serverName; }
|
||||
|
||||
/**
|
||||
* BungeeCord: Gibt zurück ob Cross-Server-Features aktiviert sind.
|
||||
* Entspricht config.yml → bungeecord: true
|
||||
*/
|
||||
public boolean isBungeeCordEnabled() { return getConfig().getBoolean("bungeecord", false); }
|
||||
}
|
||||
263
src/main/java/de/ticketsystem/bungee/BungeeMessenger.java
Normal file
263
src/main/java/de/ticketsystem/bungee/BungeeMessenger.java
Normal file
@@ -0,0 +1,263 @@
|
||||
package de.ticketsystem.bungee;
|
||||
|
||||
import com.google.common.io.ByteArrayDataInput;
|
||||
import com.google.common.io.ByteArrayDataOutput;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import de.ticketsystem.TicketPlugin;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.messaging.PluginMessageListener;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Verwaltet die BungeeCord Plugin-Messaging-Kanäle für Cross-Server-Kommunikation.
|
||||
*
|
||||
* Kanalübersicht:
|
||||
* Ausgehend: "BungeeCord" – Standard-BungeeCord-Kanal (Forward, Message)
|
||||
* Eingehend: "ticketsystem:notify" – Eigener Kanal für weitergeleitete Nachrichten
|
||||
*
|
||||
* Voraussetzung:
|
||||
* - In spigot.yml muss "bungeecord: true" gesetzt sein
|
||||
* - In plugin.yml müssen beide Kanäle unter "channels:" deklariert sein
|
||||
*
|
||||
* Pakettypen (erstes Byte bei ticketsystem:notify):
|
||||
* 0x01 = TEAM_NOTIFY – Nachricht an alle Online-Supporter/Admins auf diesem Server
|
||||
* 0x02 = PLAYER_MSG – Nachricht an einen bestimmten Spieler (UUID + Text)
|
||||
*
|
||||
* ── BUG FIX ──────────────────────────────────────────────────────────────────
|
||||
* Problem: BungeeCord's "Forward ALL" liefert auf dem Zielserver den inneren
|
||||
* Payload BEREITS ENTPACKT via onPluginMessageReceived auf dem
|
||||
* CUSTOM_CHANNEL. Das war korrekt implementiert.
|
||||
*
|
||||
* Der eigentliche Fehler lag in broadcastTeamNotification():
|
||||
* - Nachrichten mit "\n" wurden als ein einzelner String gesendet.
|
||||
* Minecraft verarbeitet "\n" in sendMessage() nicht → beide Zeilen kamen
|
||||
* als eine zusammen an (unleserlich, aber nicht die Ursache für "gar nichts").
|
||||
* - Die Methode wird jetzt mit einer Liste von Strings aufgerufen (broadcastLines)
|
||||
* damit jede Zeile als separates Paket gesendet wird – klar und lesbar.
|
||||
*
|
||||
* Hauptursache für "gar nichts auf Lobby":
|
||||
* Die plugin.yml hatte keinen "channels:"-Block. Ohne diesen Eintrag
|
||||
* registriert BungeeCord den Kanal "ticketsystem:notify" nicht und
|
||||
* verwirft alle eingehenden Forward-Pakete lautlos auf den Ziel-Servern.
|
||||
* → plugin.yml Fix ist die primäre Lösung.
|
||||
*
|
||||
* Diese Datei enthält zusätzlich Debug-Logging (wenn debug: true in config.yml)
|
||||
* damit zukünftige Probleme schneller gefunden werden können.
|
||||
* ─────────────────────────────────────────────────────────────────────────────
|
||||
*/
|
||||
public class BungeeMessenger implements PluginMessageListener {
|
||||
|
||||
/** BungeeCord-Standardkanal für Forward/Message-Subkanäle */
|
||||
public static final String BUNGEE_CHANNEL = "BungeeCord";
|
||||
|
||||
/** Eigener Weiterleitungskanal – muss in plugin.yml unter channels stehen */
|
||||
public static final String CUSTOM_CHANNEL = "ticketsystem:notify";
|
||||
|
||||
private static final byte TYPE_TEAM_NOTIFY = 0x01;
|
||||
private static final byte TYPE_PLAYER_MSG = 0x02;
|
||||
|
||||
private final TicketPlugin plugin;
|
||||
|
||||
public BungeeMessenger(TicketPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
// ─────────────────────────── Ausgehende Nachrichten ────────────────────
|
||||
|
||||
/**
|
||||
* Sendet eine Chat-Nachricht an einen bestimmten Spieler – egal auf welchem
|
||||
* Server im Netzwerk er sich befindet.
|
||||
*
|
||||
* Reihenfolge:
|
||||
* 1. Spieler ist lokal online → direkte Zustellung
|
||||
* 2. Spieler ist woanders → BungeeCord "Message"-Subkanal (nach Name)
|
||||
* 3. Spieler ist offline → Pendende DB-Benachrichtigung (vorher speichern!)
|
||||
*/
|
||||
public void sendMessageToPlayer(UUID targetUUID, String targetName, String message) {
|
||||
// 1. Lokal online?
|
||||
Player local = Bukkit.getPlayer(targetUUID);
|
||||
if (local != null && local.isOnline()) {
|
||||
local.sendMessage(message);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Cross-Server via BungeeCord "Message"-Subkanal
|
||||
Player messenger = getAnyOnlinePlayer();
|
||||
if (messenger == null || targetName == null) return;
|
||||
|
||||
ByteArrayDataOutput out = ByteStreams.newDataOutput();
|
||||
out.writeUTF("Message");
|
||||
out.writeUTF(targetName);
|
||||
out.writeUTF(message);
|
||||
messenger.sendPluginMessage(plugin, BUNGEE_CHANNEL, out.toByteArray());
|
||||
|
||||
if (plugin.isDebug()) {
|
||||
plugin.getLogger().info("[DEBUG][BungeeMessenger] sendMessageToPlayer → " + targetName + ": " + message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcastet eine Team-Benachrichtigung an alle Supporter/Admins im gesamten Netzwerk.
|
||||
*
|
||||
* Lokal online Spieler werden sofort benachrichtigt.
|
||||
* Alle anderen Server erhalten das Paket über den "Forward ALL"-Mechanismus.
|
||||
*
|
||||
* WICHTIG: Jede Zeile wird als separates Paket gesendet damit Minecraft
|
||||
* die Nachrichten korrekt zeilenweise anzeigt.
|
||||
*/
|
||||
public void broadcastTeamNotification(String message) {
|
||||
// Lokale Supporter direkt benachrichtigen
|
||||
Bukkit.getOnlinePlayers().stream()
|
||||
.filter(p -> p.hasPermission("ticket.support") || p.hasPermission("ticket.admin"))
|
||||
.forEach(p -> p.sendMessage(message));
|
||||
|
||||
// An alle anderen Server forwarden
|
||||
Player messenger = getAnyOnlinePlayer();
|
||||
if (messenger == null) {
|
||||
if (plugin.isDebug()) {
|
||||
plugin.getLogger().warning("[DEBUG][BungeeMessenger] broadcastTeamNotification: kein Bote online – Forward nicht möglich!");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
sendForwardPacket(messenger, message);
|
||||
|
||||
if (plugin.isDebug()) {
|
||||
plugin.getLogger().info("[DEBUG][BungeeMessenger] broadcastTeamNotification gesendet via " + messenger.getName() + ": " + message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sendet eine Nachricht an einen bestimmten Spieler via eigenem Forward-Paket.
|
||||
*/
|
||||
public void forwardPlayerMessage(UUID targetUUID, String targetName, String message) {
|
||||
Player local = Bukkit.getPlayer(targetUUID);
|
||||
if (local != null && local.isOnline()) {
|
||||
local.sendMessage(message);
|
||||
return;
|
||||
}
|
||||
|
||||
Player messenger = getAnyOnlinePlayer();
|
||||
if (messenger == null) return;
|
||||
|
||||
byte[] uuidBytes = targetUUID.toString().getBytes(StandardCharsets.UTF_8);
|
||||
byte[] msgBytes = message.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
ByteArrayDataOutput inner = ByteStreams.newDataOutput();
|
||||
inner.writeByte(TYPE_PLAYER_MSG);
|
||||
inner.writeShort(uuidBytes.length);
|
||||
inner.write(uuidBytes);
|
||||
inner.writeShort(msgBytes.length);
|
||||
inner.write(msgBytes);
|
||||
byte[] innerBytes = inner.toByteArray();
|
||||
|
||||
ByteArrayDataOutput out = ByteStreams.newDataOutput();
|
||||
out.writeUTF("Forward");
|
||||
out.writeUTF("ALL");
|
||||
out.writeUTF(CUSTOM_CHANNEL);
|
||||
out.writeShort(innerBytes.length);
|
||||
out.write(innerBytes);
|
||||
|
||||
messenger.sendPluginMessage(plugin, BUNGEE_CHANNEL, out.toByteArray());
|
||||
}
|
||||
|
||||
// ─────────────────────────── Eingehende Nachrichten ────────────────────
|
||||
|
||||
@Override
|
||||
public void onPluginMessageReceived(String channel, Player player, byte[] data) {
|
||||
if (!CUSTOM_CHANNEL.equals(channel)) return;
|
||||
|
||||
if (plugin.isDebug()) {
|
||||
plugin.getLogger().info("[DEBUG][BungeeMessenger] Paket empfangen auf " + channel + ", " + data.length + " Bytes");
|
||||
}
|
||||
|
||||
try {
|
||||
ByteArrayDataInput in = ByteStreams.newDataInput(data);
|
||||
byte type = in.readByte();
|
||||
|
||||
if (type == TYPE_TEAM_NOTIFY) {
|
||||
// Rest der Bytes = UTF-8-kodierte Nachricht
|
||||
int len = data.length - 1;
|
||||
byte[] msgBytes = new byte[len];
|
||||
in.readFully(msgBytes);
|
||||
String message = new String(msgBytes, StandardCharsets.UTF_8);
|
||||
|
||||
if (plugin.isDebug()) {
|
||||
plugin.getLogger().info("[DEBUG][BungeeMessenger] TEAM_NOTIFY empfangen: " + message);
|
||||
}
|
||||
|
||||
// Im Hauptthread an lokale Supporter zustellen
|
||||
Bukkit.getScheduler().runTask(plugin, () ->
|
||||
Bukkit.getOnlinePlayers().stream()
|
||||
.filter(p -> p.hasPermission("ticket.support") || p.hasPermission("ticket.admin"))
|
||||
.forEach(p -> p.sendMessage(message))
|
||||
);
|
||||
|
||||
} else if (type == TYPE_PLAYER_MSG) {
|
||||
int uuidLen = in.readShort();
|
||||
byte[] uuidBytes = new byte[uuidLen];
|
||||
in.readFully(uuidBytes);
|
||||
UUID targetUUID = UUID.fromString(new String(uuidBytes, StandardCharsets.UTF_8));
|
||||
|
||||
int msgLen = in.readShort();
|
||||
byte[] msgBytes = new byte[msgLen];
|
||||
in.readFully(msgBytes);
|
||||
String message = new String(msgBytes, StandardCharsets.UTF_8);
|
||||
|
||||
if (plugin.isDebug()) {
|
||||
plugin.getLogger().info("[DEBUG][BungeeMessenger] PLAYER_MSG empfangen für: " + targetUUID);
|
||||
}
|
||||
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
Player target = Bukkit.getPlayer(targetUUID);
|
||||
if (target != null && target.isOnline()) {
|
||||
target.sendMessage(message);
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
plugin.getLogger().warning("[BungeeMessenger] Unbekannter Pakettyp: " + type);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[BungeeMessenger] Fehler beim Verarbeiten einer Plugin-Message: " + e.getMessage());
|
||||
if (plugin.isDebug()) e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────── Hilfsmethoden ─────────────────────────────
|
||||
|
||||
/**
|
||||
* Baut und sendet ein Forward-ALL-Paket mit TYPE_TEAM_NOTIFY.
|
||||
*/
|
||||
private void sendForwardPacket(Player messenger, String message) {
|
||||
byte[] msgBytes = message.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
ByteArrayDataOutput inner = ByteStreams.newDataOutput();
|
||||
inner.writeByte(TYPE_TEAM_NOTIFY);
|
||||
inner.write(msgBytes);
|
||||
byte[] innerBytes = inner.toByteArray();
|
||||
|
||||
ByteArrayDataOutput out = ByteStreams.newDataOutput();
|
||||
out.writeUTF("Forward");
|
||||
out.writeUTF("ALL");
|
||||
out.writeUTF(CUSTOM_CHANNEL);
|
||||
out.writeShort(innerBytes.length);
|
||||
out.write(innerBytes);
|
||||
|
||||
messenger.sendPluginMessage(plugin, BUNGEE_CHANNEL, out.toByteArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt einen beliebigen online Spieler zurück der als "Bote" für Plugin-Messages
|
||||
* verwendet werden kann. BungeeCord verlangt einen Spieler als Absender.
|
||||
*/
|
||||
private Player getAnyOnlinePlayer() {
|
||||
Collection<? extends Player> online = Bukkit.getOnlinePlayers();
|
||||
return online.isEmpty() ? null : online.iterator().next();
|
||||
}
|
||||
}
|
||||
@@ -100,13 +100,11 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
|
||||
boolean prioritiesOn = plugin.getConfig().getBoolean("priorities-enabled", true);
|
||||
|
||||
if (args.length >= 3) {
|
||||
// args[1]: erst als Kategorie prüfen, dann als Priorität
|
||||
if (categoriesOn) {
|
||||
ConfigCategory parsedCat = cm.resolve(args[1]);
|
||||
if (parsedCat != null) {
|
||||
category = parsedCat;
|
||||
messageStartIndex = 2;
|
||||
// args[2]: Priorität prüfen (nur wenn danach noch Text kommt)
|
||||
if (prioritiesOn && args.length >= 4) {
|
||||
TicketPriority parsedPrio = parsePriority(args[2]);
|
||||
if (parsedPrio != null) {
|
||||
@@ -115,7 +113,6 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Keine Kategorie erkannt → args[1] als Priorität prüfen
|
||||
if (prioritiesOn) {
|
||||
TicketPriority parsedPrio = parsePriority(args[1]);
|
||||
if (parsedPrio != null) {
|
||||
@@ -125,16 +122,12 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
|
||||
}
|
||||
}
|
||||
} else if (prioritiesOn) {
|
||||
// Kategorien aus → args[1] direkt als Priorität prüfen
|
||||
TicketPriority parsedPrio = parsePriority(args[1]);
|
||||
if (parsedPrio != null) {
|
||||
priority = parsedPrio;
|
||||
messageStartIndex = 2;
|
||||
}
|
||||
}
|
||||
} else if (args.length == 2) {
|
||||
// Nur ein Argument: könnte Kategorie oder Priorität sein, aber kein Text danach
|
||||
// → einfach als Beschreibung behandeln, nichts parsen
|
||||
}
|
||||
|
||||
String message = String.join(" ", Arrays.copyOfRange(args, messageStartIndex, args.length));
|
||||
@@ -153,6 +146,8 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
|
||||
Ticket ticket = new Ticket(player.getUniqueId(), player.getName(), message, player.getLocation());
|
||||
ticket.setCategoryKey(finalCategory.getKey());
|
||||
ticket.setPriority(finalPriority);
|
||||
// BungeeCord: Server-Name des erstellenden Servers speichern
|
||||
ticket.setServerName(plugin.getServerName());
|
||||
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
int id = plugin.getDatabaseManager().createTicket(ticket);
|
||||
@@ -200,11 +195,31 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
|
||||
if (!success) { player.sendMessage(plugin.formatMessage("messages.already-claimed")); return; }
|
||||
Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId);
|
||||
if (ticket == null) return;
|
||||
|
||||
player.sendMessage(plugin.formatMessage("messages.ticket-claimed")
|
||||
.replace("{id}", String.valueOf(ticketId))
|
||||
.replace("{player}", ticket.getCreatorName()));
|
||||
plugin.getTicketManager().notifyCreatorClaimed(ticket);
|
||||
if (ticket.getLocation() != null) player.teleport(ticket.getLocation());
|
||||
|
||||
// ── BUG FIX #1: Teleportation bei aktivem BungeeCord komplett sperren ──
|
||||
// Wenn BungeeCord aktiv ist, kann das Ticket von einem anderen Server stammen.
|
||||
// getLocation() würde null liefern (World existiert lokal nicht) oder den
|
||||
// Supporter auf dem falschen Server teleportieren.
|
||||
// Lösung: Bei aktivem BungeeCord generell keinen Teleport durchführen.
|
||||
if (plugin.isBungeeCordEnabled()) {
|
||||
// Hinweis: Server anzeigen wenn bekannt, damit Supporter weiß wo das Ticket ist
|
||||
String serverHint = !"unknown".equals(ticket.getServerName())
|
||||
? " &7(Server: &b" + ticket.getServerName() + "&7)"
|
||||
: "";
|
||||
player.sendMessage(plugin.color("&7Teleportation deaktiviert – BungeeCord-Netzwerk aktiv." + serverHint));
|
||||
} else {
|
||||
// Standalone-Modus: Normal teleportieren
|
||||
if (ticket.getLocation() != null) {
|
||||
player.teleport(ticket.getLocation());
|
||||
} else {
|
||||
player.sendMessage(plugin.color("&7Teleportation nicht möglich – World nicht gefunden."));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -228,6 +243,9 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
|
||||
boolean success = plugin.getDatabaseManager().closeTicket(ticketId, comment);
|
||||
if (success) {
|
||||
Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId);
|
||||
// Ticket in persistente Stats-Tabelle eintragen (bleibt auch nach Löschung erhalten).
|
||||
// player.getName() = der Admin der /ticket close ausgeführt hat – nicht zwingend der Claimer.
|
||||
if (ticket != null) plugin.getDatabaseManager().recordClosedTicket(ticket, player.getName());
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
player.sendMessage(plugin.formatMessage("messages.ticket-closed").replace("{id}", String.valueOf(ticketId)));
|
||||
if (ticket != null) {
|
||||
@@ -252,12 +270,23 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
|
||||
try { id = Integer.parseInt(args[1]); }
|
||||
catch (NumberFormatException e) { player.sendMessage(plugin.color("&cUngültige ID!")); return; }
|
||||
|
||||
Player target = Bukkit.getPlayer(args[2]);
|
||||
if (target == null) { player.sendMessage(plugin.color("&cSpieler nicht gefunden!")); return; }
|
||||
// BungeeCord: Ziel-Spieler lokal suchen
|
||||
Player localTarget = Bukkit.getPlayer(args[2]);
|
||||
|
||||
if (localTarget == null) {
|
||||
if (plugin.isBungeeCordEnabled()) {
|
||||
player.sendMessage(plugin.color("&7[BungeeCord] Spieler &e" + args[2]
|
||||
+ " &7ist auf diesem Server nicht online."));
|
||||
player.sendMessage(plugin.color("&7Tipp: Forwarden geht nur zu Spielern auf &bdemselben Server&7."));
|
||||
} else {
|
||||
player.sendMessage(plugin.color("&cSpieler nicht gefunden!"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final int ticketId = id;
|
||||
final String fromName = player.getName();
|
||||
final Player t = target;
|
||||
final Player t = localTarget;
|
||||
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
boolean success = plugin.getDatabaseManager().forwardTicket(ticketId, t.getUniqueId(), t.getName());
|
||||
@@ -295,7 +324,6 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.formatMessage("messages.ticket-not-found")));
|
||||
return;
|
||||
}
|
||||
// Spieler darf nur auf eigene Tickets kommentieren (Supporter/Admin: alle)
|
||||
boolean isOwner = ticket.getCreatorUUID().equals(player.getUniqueId());
|
||||
boolean isStaff = player.hasPermission("ticket.support") || player.hasPermission("ticket.admin");
|
||||
if (!isOwner && !isStaff) {
|
||||
@@ -309,7 +337,6 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
if (success) {
|
||||
player.sendMessage(plugin.color("&aDein Kommentar zu Ticket &e#" + ticketId + " &awurde gespeichert."));
|
||||
// Supporter/Admin und Ticket-Ersteller benachrichtigen
|
||||
notifyCommentReceivers(player, ticket, message);
|
||||
} else {
|
||||
player.sendMessage(plugin.color("&cFehler beim Speichern des Kommentars."));
|
||||
@@ -318,46 +345,82 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Benachrichtigt alle relevanten Empfänger über einen neuen Kommentar.
|
||||
*
|
||||
* ── BUG FIX #2 ──────────────────────────────────────────────────────────
|
||||
* Vorher: broadcastTeamNotification() wurde am Ende ZUSÄTZLICH aufgerufen –
|
||||
* obwohl alle lokalen Supporter bereits einzeln per Schleife
|
||||
* benachrichtigt wurden. Das führte zu:
|
||||
* a) Doppelter Nachricht für lokale Supporter
|
||||
* b) broadcastTeamNotification() sendet intern ebenfalls lokal →
|
||||
* lokale Supporter sahen die Nachricht dreifach
|
||||
* c) Das Forward-Paket an andere Server war korrekt, aber die
|
||||
* Empfänger auf anderen Servern sahen auch Duplikate da
|
||||
* broadcastTeamNotification() wiederum lokal sendet
|
||||
*
|
||||
* Fix: broadcastTeamNotification() ERSETZT die lokale Supporter-Schleife
|
||||
* komplett. Die Methode sendet bereits lokal direkt und forwardet
|
||||
* gleichzeitig an alle anderen BungeeCord-Server.
|
||||
* Im Standalone-Modus bleibt die lokale Schleife erhalten.
|
||||
* ────────────────────────────────────────────────────────────────────────
|
||||
*/
|
||||
private void notifyCommentReceivers(Player author, Ticket ticket, String message) {
|
||||
String onlineMsg = plugin.color("&e[Ticket #" + ticket.getId() + "] &f" + author.getName() + " &7hat kommentiert: &f" + message);
|
||||
String offlineMsg = "&e[Ticket #" + ticket.getId() + "] &f" + author.getName() + " &7hat kommentiert (während du offline warst): &f" + message;
|
||||
|
||||
// Ticket-Ersteller benachrichtigen (wenn nicht der Autor selbst)
|
||||
// ── 1. Ticket-Ersteller benachrichtigen (wenn nicht der Autor selbst) ──
|
||||
if (!ticket.getCreatorUUID().equals(author.getUniqueId())) {
|
||||
Player creator = Bukkit.getPlayer(ticket.getCreatorUUID());
|
||||
if (creator != null && creator.isOnline()) {
|
||||
creator.sendMessage(onlineMsg);
|
||||
} else if (plugin.isBungeeCordEnabled()) {
|
||||
// BungeeCord: Zustellung via Plugin-Messaging, kein Pending-Eintrag
|
||||
// (PlayerJoinListener übernimmt Offline-Fallback via close_notified-Logik)
|
||||
plugin.getBungeeMessenger().sendMessageToPlayer(
|
||||
ticket.getCreatorUUID(), ticket.getCreatorName(), onlineMsg);
|
||||
} else {
|
||||
// Offline → für nächsten Login speichern
|
||||
// Standalone: Offline → für nächsten Login speichern
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () ->
|
||||
plugin.getDatabaseManager().addPendingNotification(ticket.getCreatorUUID(), offlineMsg));
|
||||
}
|
||||
}
|
||||
|
||||
// Supporter/Admin benachrichtigen (wenn Autor kein Supporter ist)
|
||||
// ── 2. Supporter/Admin benachrichtigen (wenn Kommentar vom Spieler kommt) ──
|
||||
if (!author.hasPermission("ticket.support") && !author.hasPermission("ticket.admin")) {
|
||||
// Claimer des Tickets bevorzugt benachrichtigen
|
||||
|
||||
if (plugin.isBungeeCordEnabled()) {
|
||||
// BungeeCord-Modus: broadcastTeamNotification() übernimmt ALLES –
|
||||
// lokal direkt + Forward an alle anderen Server in einem Paket.
|
||||
// KEINE zusätzliche lokale Schleife, da das zu Duplikaten führt.
|
||||
plugin.getBungeeMessenger().broadcastTeamNotification(onlineMsg);
|
||||
|
||||
} else {
|
||||
// Standalone-Modus: Claimer gezielt benachrichtigen
|
||||
UUID claimerUUID = ticket.getClaimerUUID();
|
||||
if (claimerUUID != null && !claimerUUID.equals(author.getUniqueId())) {
|
||||
Player claimer = Bukkit.getPlayer(claimerUUID);
|
||||
if (claimer != null && claimer.isOnline()) {
|
||||
claimer.sendMessage(onlineMsg);
|
||||
} else {
|
||||
String claimerOffline = "&e[Ticket #" + ticket.getId() + "] &f" + author.getName() + " &7hat auf dein bearbeitetes Ticket kommentiert (offline): &f" + message;
|
||||
String claimerOffline = "&e[Ticket #" + ticket.getId() + "] &f" + author.getName()
|
||||
+ " &7hat auf dein bearbeitetes Ticket kommentiert (offline): &f" + message;
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () ->
|
||||
plugin.getDatabaseManager().addPendingNotification(claimerUUID, claimerOffline));
|
||||
}
|
||||
}
|
||||
// Alle anderen Online-Supporter zusätzlich informieren
|
||||
|
||||
// Alle anderen Online-Supporter auf diesem Server informieren
|
||||
for (Player p : Bukkit.getOnlinePlayers()) {
|
||||
if (p.getUniqueId().equals(author.getUniqueId())) continue;
|
||||
if (claimerUUID != null && p.getUniqueId().equals(claimerUUID)) continue; // schon oben
|
||||
if (claimerUUID != null && p.getUniqueId().equals(claimerUUID)) continue;
|
||||
if (p.hasPermission("ticket.support") || p.hasPermission("ticket.admin")) {
|
||||
p.sendMessage(onlineMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────── /ticket rate ──────────────────────────────
|
||||
|
||||
@@ -468,7 +531,6 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
|
||||
player.sendMessage(plugin.color("&7Keine gesperrten Spieler."));
|
||||
} else {
|
||||
for (String[] entry : list) {
|
||||
// {uuid, name, reason, bannedBy, bannedAt}
|
||||
player.sendMessage(plugin.color("&e" + entry[1] + " &7– &f" + entry[2]
|
||||
+ " &7(gesperrt von &e" + entry[3] + "&7)"));
|
||||
}
|
||||
@@ -488,6 +550,9 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
|
||||
plugin.reloadConfig();
|
||||
plugin.getCategoryManager().reload();
|
||||
player.sendMessage(plugin.color("&aKonfiguration wurde neu geladen. &7(inkl. Kategorien)"));
|
||||
if (plugin.isBungeeCordEnabled()) {
|
||||
player.sendMessage(plugin.color("&8[BungeeCord] &7Server: &b" + plugin.getServerName()));
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────── /ticket archive ───────────────────────────
|
||||
@@ -509,25 +574,54 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
|
||||
if (!player.hasPermission("ticket.admin")) { player.sendMessage(plugin.formatMessage("messages.no-permission")); return; }
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
var stats = plugin.getDatabaseManager().getTicketStats();
|
||||
var staffRatings = plugin.getDatabaseManager().getStaffRatings();
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
player.sendMessage(plugin.color("&8&m "));
|
||||
player.sendMessage(plugin.color("&6Ticket Statistik"));
|
||||
player.sendMessage(plugin.color("&8&m "));
|
||||
player.sendMessage(plugin.color("&eGesamt: &a" + stats.total));
|
||||
player.sendMessage(plugin.color("&eOffen: &a" + stats.open));
|
||||
player.sendMessage(plugin.color("&eGeschlossen: &a" + stats.closed));
|
||||
player.sendMessage(plugin.color("&eGeschlossen: &a" + stats.closed + " &7(historisch)"));
|
||||
player.sendMessage(plugin.color("&eWeitergeleitet: &a" + stats.forwarded));
|
||||
|
||||
if (plugin.getConfig().getBoolean("rating-enabled", true)) {
|
||||
player.sendMessage(plugin.color("&8&m "));
|
||||
player.sendMessage(plugin.color("&6Support-Bewertungen"));
|
||||
player.sendMessage(plugin.color("&6Support-Bewertungen &7(gesamt, historisch)"));
|
||||
player.sendMessage(plugin.color("&a👍 Positiv: &f" + stats.thumbsUp
|
||||
+ " &c👎 Negativ: &f" + stats.thumbsDown));
|
||||
int total = stats.thumbsUp + stats.thumbsDown;
|
||||
if (total > 0) {
|
||||
int percent = (int) Math.round(stats.thumbsUp * 100.0 / total);
|
||||
int totalRated = stats.thumbsUp + stats.thumbsDown;
|
||||
if (totalRated > 0) {
|
||||
int percent = (int) Math.round(stats.thumbsUp * 100.0 / totalRated);
|
||||
player.sendMessage(plugin.color("&7Zufriedenheit: &e" + percent + "%"));
|
||||
}
|
||||
|
||||
// Bewertungen pro Support-Mitarbeiter
|
||||
if (!staffRatings.isEmpty()) {
|
||||
player.sendMessage(plugin.color("&8&m "));
|
||||
player.sendMessage(plugin.color("&6Bewertungen nach Support-Mitarbeiter:"));
|
||||
player.sendMessage(plugin.color("&7 Name 👍 👎 Tickets Zufrieden"));
|
||||
for (String[] row : staffRatings) {
|
||||
// row: [name, up, down, totalClosed, percent]
|
||||
String name = String.format("%-16s", row[0]);
|
||||
String up = String.format("%-5s", row[1]);
|
||||
String down = String.format("%-5s", row[2]);
|
||||
String total = String.format("%-8s", row[3]);
|
||||
String percent = row[4];
|
||||
player.sendMessage(plugin.color(
|
||||
"&e " + name + " &a" + up + " &c" + down + " &7" + total + " &e" + percent));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BungeeCord: Tickets pro Server anzeigen
|
||||
if (plugin.isBungeeCordEnabled() && !stats.byServer.isEmpty()) {
|
||||
player.sendMessage(plugin.color("&8&m "));
|
||||
player.sendMessage(plugin.color("&6Tickets nach Server:"));
|
||||
stats.byServer.entrySet().stream()
|
||||
.sorted((a, b) -> b.getValue() - a.getValue())
|
||||
.forEach(e -> player.sendMessage(plugin.color("&b " + e.getKey() + ": &a" + e.getValue())));
|
||||
}
|
||||
|
||||
player.sendMessage(plugin.color("&8&m "));
|
||||
player.sendMessage(plugin.color("&6Top Ersteller:"));
|
||||
stats.byPlayer.entrySet().stream()
|
||||
@@ -625,8 +719,7 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
|
||||
});
|
||||
}
|
||||
|
||||
/** Parst Benutzer-Eingaben wie "high", "hoch", "urgent", "dringend" etc. zu TicketPriority.
|
||||
* Gibt null zurück wenn keine Übereinstimmung. */
|
||||
/** Parst Benutzer-Eingaben zu TicketPriority. Gibt null zurück wenn keine Übereinstimmung. */
|
||||
private TicketPriority parsePriority(String input) {
|
||||
if (input == null) return null;
|
||||
return switch (input.toLowerCase()) {
|
||||
@@ -661,14 +754,12 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
|
||||
&& plugin.getConfig().getBoolean("categories-enabled", true)) {
|
||||
for (ConfigCategory c : plugin.getCategoryManager().getAll())
|
||||
if (c.getKey().startsWith(args[1].toLowerCase())) completions.add(c.getKey());
|
||||
// auch Priorität direkt ohne Kategorie anbieten
|
||||
if (plugin.getConfig().getBoolean("priorities-enabled", true))
|
||||
for (String p : List.of("low", "normal", "high", "urgent"))
|
||||
if (p.startsWith(args[1].toLowerCase())) completions.add(p);
|
||||
|
||||
} else if (args.length == 3 && args[0].equalsIgnoreCase("create")
|
||||
&& plugin.getConfig().getBoolean("priorities-enabled", true)) {
|
||||
// Priorität nach Kategorie
|
||||
for (String p : List.of("low", "normal", "high", "urgent"))
|
||||
if (p.startsWith(args[2].toLowerCase())) completions.add(p);
|
||||
|
||||
@@ -677,6 +768,7 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
|
||||
if (p.startsWith(args[2].toLowerCase())) completions.add(p);
|
||||
|
||||
} else if (args.length == 3 && args[0].equalsIgnoreCase("forward")) {
|
||||
// BungeeCord: Nur lokal online Spieler als Tab-Completion
|
||||
for (Player p : Bukkit.getOnlinePlayers())
|
||||
if (p.getName().toLowerCase().startsWith(args[2].toLowerCase())) completions.add(p.getName());
|
||||
|
||||
|
||||
@@ -161,6 +161,7 @@ public class DatabaseManager {
|
||||
|
||||
private void createTables() {
|
||||
// Haupt-Tickets-Tabelle
|
||||
// BungeeCord: server_name speichert auf welchem Server das Ticket erstellt wurde
|
||||
String ticketsSql = """
|
||||
CREATE TABLE IF NOT EXISTS tickets (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
@@ -186,7 +187,9 @@ public class DatabaseManager {
|
||||
category VARCHAR(16) NOT NULL DEFAULT 'GENERAL',
|
||||
priority VARCHAR(10) NOT NULL DEFAULT 'NORMAL',
|
||||
player_rating VARCHAR(16) NULL,
|
||||
claimer_notified BOOLEAN DEFAULT FALSE
|
||||
claimer_notified BOOLEAN DEFAULT FALSE,
|
||||
close_notified BOOLEAN DEFAULT FALSE,
|
||||
server_name VARCHAR(64) NOT NULL DEFAULT 'unknown'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
""";
|
||||
|
||||
@@ -225,11 +228,50 @@ public class DatabaseManager {
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
""";
|
||||
|
||||
// Persistente Statistik-Tabelle – überlebt das Löschen/Archivieren von Tickets.
|
||||
// Wird beim Schließen eines Tickets befüllt und beim Bewerten aktualisiert.
|
||||
// So gehen keine Zahlen verloren wenn das Archiv geleert wird.
|
||||
String statsSql = """
|
||||
CREATE TABLE IF NOT EXISTS ticket_stats (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
ticket_id INT NOT NULL,
|
||||
claimer_uuid VARCHAR(36) NULL,
|
||||
claimer_name VARCHAR(16) NULL,
|
||||
creator_uuid VARCHAR(36) NOT NULL,
|
||||
creator_name VARCHAR(16) NOT NULL,
|
||||
category VARCHAR(16) NOT NULL DEFAULT 'general',
|
||||
priority VARCHAR(10) NOT NULL DEFAULT 'NORMAL',
|
||||
server_name VARCHAR(64) NOT NULL DEFAULT 'unknown',
|
||||
player_rating VARCHAR(16) NULL,
|
||||
closed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_claimer_uuid (claimer_uuid),
|
||||
INDEX idx_closed_at (closed_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
""";
|
||||
|
||||
// Ausstehende BungeeCord-Teleport-Aufträge.
|
||||
// Wird gesetzt wenn ein Admin via GUI/Command auf einen anderen Server teleportiert.
|
||||
// PlayerJoinListener liest den Eintrag beim Ankommen, teleportiert, löscht ihn dann.
|
||||
String pendingTeleportSql = """
|
||||
CREATE TABLE IF NOT EXISTS ticket_pending_teleport (
|
||||
player_uuid VARCHAR(36) NOT NULL PRIMARY KEY,
|
||||
world VARCHAR(64) NOT NULL,
|
||||
x DOUBLE NOT NULL,
|
||||
y DOUBLE NOT NULL,
|
||||
z DOUBLE NOT NULL,
|
||||
yaw FLOAT NOT NULL DEFAULT 0,
|
||||
pitch FLOAT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
""";
|
||||
|
||||
try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) {
|
||||
stmt.execute(ticketsSql);
|
||||
stmt.execute(commentsSql);
|
||||
stmt.execute(blacklistSql);
|
||||
stmt.execute(notifSql);
|
||||
stmt.execute(statsSql);
|
||||
stmt.execute(pendingTeleportSql);
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Fehler beim Erstellen der Tabellen: " + e.getMessage(), e);
|
||||
}
|
||||
@@ -237,6 +279,7 @@ public class DatabaseManager {
|
||||
|
||||
/**
|
||||
* Ergänzt fehlende Spalten in bestehenden Datenbanken automatisch.
|
||||
* Wichtig für Upgrades von älteren Versionen.
|
||||
*/
|
||||
private void ensureColumns() {
|
||||
ensureColumn("close_comment", "ALTER TABLE tickets ADD COLUMN close_comment VARCHAR(500) NULL");
|
||||
@@ -245,6 +288,13 @@ public class DatabaseManager {
|
||||
ensureColumn("priority", "ALTER TABLE tickets ADD COLUMN priority VARCHAR(10) NOT NULL DEFAULT 'NORMAL'");
|
||||
ensureColumn("player_rating", "ALTER TABLE tickets ADD COLUMN player_rating VARCHAR(16) NULL");
|
||||
ensureColumn("claimer_notified", "ALTER TABLE tickets ADD COLUMN claimer_notified BOOLEAN DEFAULT FALSE");
|
||||
// Bug-Fix: close_notified verhindert Duplikat-Discord-Nachrichten und doppelte Offline-Benachrichtigungen bei Server-Wechsel
|
||||
ensureColumn("close_notified", "ALTER TABLE tickets ADD COLUMN close_notified BOOLEAN DEFAULT FALSE");
|
||||
// BungeeCord: Server-Name-Spalte für bestehende Datenbanken nachrüsten
|
||||
ensureColumn("server_name", "ALTER TABLE tickets ADD COLUMN server_name VARCHAR(64) NOT NULL DEFAULT 'unknown'");
|
||||
|
||||
// ticket_stats: Spalte player_rating nachrüsten falls Tabelle vor diesem Feature existiert
|
||||
ensureStatsColumn("player_rating", "ALTER TABLE ticket_stats ADD COLUMN player_rating VARCHAR(16) NULL");
|
||||
}
|
||||
|
||||
private void ensureColumn(String columnName, String alterSql) {
|
||||
@@ -268,13 +318,36 @@ public class DatabaseManager {
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureStatsColumn(String columnName, String alterSql) {
|
||||
String checkSql = """
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'ticket_stats'
|
||||
AND COLUMN_NAME = ?
|
||||
""";
|
||||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(checkSql)) {
|
||||
ps.setString(1, columnName);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
if (rs.next() && rs.getInt(1) == 0) {
|
||||
try (Statement stmt = conn.createStatement()) {
|
||||
stmt.execute(alterSql);
|
||||
plugin.getLogger().info("[TicketSystem] Stats-Spalte '" + columnName + "' wurde hinzugefügt.");
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Fehler bei ensureStatsColumn(" + columnName + "): " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────── CRUD Tickets ──────────────────────────────
|
||||
|
||||
public int createTicket(Ticket ticket) {
|
||||
if (useMySQL) {
|
||||
// BungeeCord: server_name wird ebenfalls gespeichert
|
||||
String sql = """
|
||||
INSERT INTO tickets (creator_uuid, creator_name, message, world, x, y, z, yaw, pitch, category, priority)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO tickets (creator_uuid, creator_name, message, world, x, y, z, yaw, pitch,
|
||||
category, priority, server_name)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""";
|
||||
try (Connection conn = getConnection();
|
||||
PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
|
||||
@@ -289,6 +362,7 @@ public class DatabaseManager {
|
||||
ps.setFloat(9, ticket.getPitch());
|
||||
ps.setString(10, ticket.getCategoryKey());
|
||||
ps.setString(11, ticket.getPriority().name());
|
||||
ps.setString(12, ticket.getServerName()); // BungeeCord
|
||||
ps.executeUpdate();
|
||||
ResultSet rs = ps.getGeneratedKeys();
|
||||
if (rs.next()) return rs.getInt(1);
|
||||
@@ -306,13 +380,21 @@ public class DatabaseManager {
|
||||
}
|
||||
}
|
||||
|
||||
// ── BUG FIX #1 ──────────────────────────────────────────────────────────
|
||||
// Vorher: WHERE id = ? AND status = 'OPEN'
|
||||
// Problem: Ein FORWARDED-Ticket konnte nicht geclaimed werden – das UPDATE
|
||||
// schlug lautlos fehl, claimer_uuid/claimer_name wurden nie geschrieben.
|
||||
// Fix: WHERE id = ? AND status != 'CLOSED'
|
||||
// Damit können sowohl OPEN als auch FORWARDED Tickets korrekt
|
||||
// angenommen werden und claimer_uuid/claimer_name werden immer gesetzt.
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
public boolean claimTicket(int ticketId, UUID claimerUUID, String claimerName) {
|
||||
if (useMySQL) {
|
||||
String sql = """
|
||||
UPDATE tickets
|
||||
SET status = 'CLAIMED', claimer_uuid = ?, claimer_name = ?,
|
||||
claimed_at = NOW(), player_deleted = FALSE
|
||||
WHERE id = ? AND status = 'OPEN'
|
||||
WHERE id = ? AND status != 'CLOSED'
|
||||
""";
|
||||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||||
ps.setString(1, claimerUUID.toString());
|
||||
@@ -325,7 +407,7 @@ public class DatabaseManager {
|
||||
return false;
|
||||
} else {
|
||||
Ticket t = getTicketById(ticketId);
|
||||
if (t == null || t.getStatus() != TicketStatus.OPEN) return false;
|
||||
if (t == null || t.getStatus() == TicketStatus.CLOSED) return false;
|
||||
t.setStatus(TicketStatus.CLAIMED);
|
||||
t.setClaimerUUID(claimerUUID);
|
||||
t.setClaimerName(claimerName);
|
||||
@@ -452,11 +534,8 @@ public class DatabaseManager {
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────── [NEW] Claim-Benachrichtigung markieren ────
|
||||
// ─────────────────────────── Claim-Benachrichtigung markieren ──────────
|
||||
|
||||
/**
|
||||
* Setzt claimer_notified = TRUE für ein Ticket (persistiert in DB/Datei).
|
||||
*/
|
||||
public void markClaimerNotified(int ticketId) {
|
||||
if (useMySQL) {
|
||||
String sql = "UPDATE tickets SET claimer_notified = TRUE WHERE id = ?";
|
||||
@@ -476,21 +555,42 @@ public class DatabaseManager {
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────── [NEW] Bewertung ───────────────────────────
|
||||
// ─────────────────────────── Schließ-Benachrichtigung markieren ────────
|
||||
|
||||
/**
|
||||
* Speichert die Bewertung eines Spielers für sein geschlossenes Ticket.
|
||||
* @param ticketId ID des Tickets
|
||||
* @param rating "THUMBS_UP" oder "THUMBS_DOWN"
|
||||
* @return true bei Erfolg
|
||||
* Setzt close_notified = TRUE für ein Ticket (persistiert in DB/Datei).
|
||||
* Verhindert Duplikat-Benachrichtigungen und doppelte Discord-Nachrichten
|
||||
* bei Server-Wechseln in BungeeCord-Netzwerken.
|
||||
*/
|
||||
public void markCloseNotified(int ticketId) {
|
||||
if (useMySQL) {
|
||||
String sql = "UPDATE tickets SET close_notified = TRUE WHERE id = ?";
|
||||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||||
ps.setInt(1, ticketId);
|
||||
ps.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Fehler bei markCloseNotified: " + e.getMessage(), e);
|
||||
}
|
||||
} else {
|
||||
Ticket t = getTicketById(ticketId);
|
||||
if (t != null) {
|
||||
t.setCloseNotified(true);
|
||||
dataConfig.set("tickets." + ticketId, t);
|
||||
saveDataConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean rateTicket(int ticketId, String rating) {
|
||||
if (useMySQL) {
|
||||
String sql = "UPDATE tickets SET player_rating = ? WHERE id = ? AND status = 'CLOSED' AND player_rating IS NULL";
|
||||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||||
ps.setString(1, rating);
|
||||
ps.setInt(2, ticketId);
|
||||
return ps.executeUpdate() > 0;
|
||||
boolean updated = ps.executeUpdate() > 0;
|
||||
// Bewertung auch in die persistente Stats-Tabelle übertragen
|
||||
if (updated) updateStatsRating(ticketId, rating);
|
||||
return updated;
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Fehler bei rateTicket: " + e.getMessage(), e);
|
||||
}
|
||||
@@ -505,11 +605,191 @@ public class DatabaseManager {
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────── [NEW] Kommentare ──────────────────────────
|
||||
/**
|
||||
* Schreibt einen Eintrag in ticket_stats wenn ein Ticket geschlossen wird.
|
||||
* Diese Tabelle bleibt dauerhaft erhalten – auch wenn das Ticket später
|
||||
* gelöscht oder archiviert wird. So gehen Statistiken nie verloren.
|
||||
*
|
||||
* @param ticket Das gerade geschlossene Ticket-Objekt
|
||||
* @param closerName Name des Admins/Supporters der das Ticket geschlossen hat
|
||||
* (kann vom claimer_name abweichen wenn ein Admin fremde Tickets schließt)
|
||||
*/
|
||||
public void recordClosedTicket(Ticket ticket, String closerName) {
|
||||
if (!useMySQL) return;
|
||||
|
||||
// closer_name bevorzugen – falls null, auf claimer_name zurückfallen
|
||||
String effectiveCloser = (closerName != null && !closerName.isEmpty())
|
||||
? closerName : ticket.getClaimerName();
|
||||
String effectiveCloserUuid = null;
|
||||
// UUID nur setzen wenn closer == claimer (sonst haben wir die UUID des Admins nicht direkt)
|
||||
if (effectiveCloser != null && effectiveCloser.equals(ticket.getClaimerName())
|
||||
&& ticket.getClaimerUUID() != null) {
|
||||
effectiveCloserUuid = ticket.getClaimerUUID().toString();
|
||||
}
|
||||
|
||||
String sql = """
|
||||
INSERT INTO ticket_stats
|
||||
(ticket_id, claimer_uuid, claimer_name, creator_uuid, creator_name,
|
||||
category, priority, server_name, player_rating, closed_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
|
||||
ON DUPLICATE KEY UPDATE closed_at = closed_at
|
||||
""";
|
||||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||||
ps.setInt(1, ticket.getId());
|
||||
ps.setString(2, effectiveCloserUuid);
|
||||
ps.setString(3, effectiveCloser);
|
||||
ps.setString(4, ticket.getCreatorUUID().toString());
|
||||
ps.setString(5, ticket.getCreatorName());
|
||||
ps.setString(6, ticket.getCategoryKey());
|
||||
ps.setString(7, ticket.getPriority().name());
|
||||
ps.setString(8, ticket.getServerName());
|
||||
ps.setString(9, ticket.getPlayerRating());
|
||||
ps.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Fehler bei recordClosedTicket: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichert einen neuen Kommentar/Reply auf ein Ticket.
|
||||
* Aktualisiert die Bewertung in ticket_stats wenn ein Spieler sein Ticket bewertet.
|
||||
* Wird von rateTicket() intern aufgerufen.
|
||||
*/
|
||||
private void updateStatsRating(int ticketId, String rating) {
|
||||
String sql = "UPDATE ticket_stats SET player_rating = ? WHERE ticket_id = ?";
|
||||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||||
ps.setString(1, rating);
|
||||
ps.setInt(2, ticketId);
|
||||
ps.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Fehler bei updateStatsRating: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt eine Liste aller Support-Mitarbeiter mit ihren Bewertungsstatistiken zurück.
|
||||
* Liest aus ticket_stats – unabhängig davon ob die Tickets noch in der DB existieren.
|
||||
*
|
||||
* Rückgabe: Liste von String-Arrays mit
|
||||
* [0] claimer_name, [1] thumbsUp, [2] thumbsDown, [3] total, [4] prozent
|
||||
*/
|
||||
public List<String[]> getStaffRatings() {
|
||||
List<String[]> result = new ArrayList<>();
|
||||
if (!useMySQL) return result;
|
||||
|
||||
String sql = """
|
||||
SELECT
|
||||
claimer_name,
|
||||
SUM(player_rating = 'THUMBS_UP') AS thumbs_up,
|
||||
SUM(player_rating = 'THUMBS_DOWN') AS thumbs_down,
|
||||
COUNT(*) AS total_closed
|
||||
FROM ticket_stats
|
||||
WHERE claimer_name IS NOT NULL
|
||||
GROUP BY claimer_uuid, claimer_name
|
||||
ORDER BY total_closed DESC
|
||||
""";
|
||||
try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) {
|
||||
ResultSet rs = stmt.executeQuery(sql);
|
||||
while (rs.next()) {
|
||||
int up = rs.getInt("thumbs_up");
|
||||
int down = rs.getInt("thumbs_down");
|
||||
int total = rs.getInt("total_closed");
|
||||
int rated = up + down;
|
||||
String percent = rated > 0 ? Math.round(up * 100.0 / rated) + "%" : "–";
|
||||
result.add(new String[]{
|
||||
rs.getString("claimer_name"),
|
||||
String.valueOf(up),
|
||||
String.valueOf(down),
|
||||
String.valueOf(total),
|
||||
percent
|
||||
});
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Fehler bei getStaffRatings: " + e.getMessage(), e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ─────────────────────────── BungeeCord Pending-Teleport ───────────────
|
||||
|
||||
/**
|
||||
* Speichert einen ausstehenden Teleport-Auftrag für einen Admin/Supporter.
|
||||
* Wird aufgerufen bevor der Spieler via BungeeCord auf den Ziel-Server
|
||||
* geschickt wird. Der PlayerJoinListener liest den Eintrag beim Ankommen.
|
||||
*/
|
||||
public void setPendingTeleport(UUID playerUUID, String world,
|
||||
double x, double y, double z,
|
||||
float yaw, float pitch) {
|
||||
if (!useMySQL) return;
|
||||
|
||||
String sql = """
|
||||
INSERT INTO ticket_pending_teleport
|
||||
(player_uuid, world, x, y, z, yaw, pitch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
world = VALUES(world), x = VALUES(x), y = VALUES(y),
|
||||
z = VALUES(z), yaw = VALUES(yaw), pitch = VALUES(pitch),
|
||||
created_at = CURRENT_TIMESTAMP
|
||||
""";
|
||||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||||
ps.setString(1, playerUUID.toString());
|
||||
ps.setString(2, world);
|
||||
ps.setDouble(3, x);
|
||||
ps.setDouble(4, y);
|
||||
ps.setDouble(5, z);
|
||||
ps.setFloat(6, yaw);
|
||||
ps.setFloat(7, pitch);
|
||||
ps.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Fehler bei setPendingTeleport: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Liest und löscht einen ausstehenden Teleport-Auftrag in einem Schritt.
|
||||
* Gibt null zurück wenn kein Auftrag vorhanden ist.
|
||||
*
|
||||
* Rückgabe: double[] { x, y, z, yaw, pitch } + world als Index 0 im String-Array
|
||||
* Vereinfacht: gibt ein Object[] zurück: [String world, double x, y, z, float yaw, pitch]
|
||||
*/
|
||||
public PendingTeleport consumePendingTeleport(UUID playerUUID) {
|
||||
if (!useMySQL) return null;
|
||||
|
||||
String selectSql = "SELECT * FROM ticket_pending_teleport WHERE player_uuid = ?";
|
||||
String deleteSql = "DELETE FROM ticket_pending_teleport WHERE player_uuid = ?";
|
||||
|
||||
try (Connection conn = getConnection();
|
||||
PreparedStatement sel = conn.prepareStatement(selectSql)) {
|
||||
sel.setString(1, playerUUID.toString());
|
||||
ResultSet rs = sel.executeQuery();
|
||||
if (!rs.next()) return null;
|
||||
|
||||
PendingTeleport pt = new PendingTeleport(
|
||||
rs.getString("world"),
|
||||
rs.getDouble("x"),
|
||||
rs.getDouble("y"),
|
||||
rs.getDouble("z"),
|
||||
rs.getFloat("yaw"),
|
||||
rs.getFloat("pitch")
|
||||
);
|
||||
|
||||
// Sofort löschen damit kein zweites Mal teleportiert wird
|
||||
try (PreparedStatement del = conn.prepareStatement(deleteSql)) {
|
||||
del.setString(1, playerUUID.toString());
|
||||
del.executeUpdate();
|
||||
}
|
||||
return pt;
|
||||
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Fehler bei consumePendingTeleport: " + e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Einfaches Daten-Objekt für einen ausstehenden Teleport-Auftrag. */
|
||||
public record PendingTeleport(String world, double x, double y, double z, float yaw, float pitch) {}
|
||||
|
||||
|
||||
|
||||
public boolean addComment(TicketComment comment) {
|
||||
if (useMySQL) {
|
||||
String sql = """
|
||||
@@ -527,7 +807,6 @@ public class DatabaseManager {
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
// YAML: comments.<ticketId>.<index>
|
||||
int index = dataConfig.getInt("comments." + comment.getTicketId() + ".count", 0);
|
||||
String base = "comments." + comment.getTicketId() + "." + index + ".";
|
||||
dataConfig.set(base + "authorUUID", comment.getAuthorUUID().toString());
|
||||
@@ -540,9 +819,6 @@ public class DatabaseManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt alle Kommentare für ein Ticket, sortiert nach Datum.
|
||||
*/
|
||||
public List<TicketComment> getComments(int ticketId) {
|
||||
List<TicketComment> list = new ArrayList<>();
|
||||
if (useMySQL) {
|
||||
@@ -582,12 +858,8 @@ public class DatabaseManager {
|
||||
return list;
|
||||
}
|
||||
|
||||
// ─────────────────────────── Pending Notifications ────────────────────
|
||||
// ─────────────────────────── Pending Notifications ─────────────────────
|
||||
|
||||
/**
|
||||
* Speichert eine Benachrichtigung für einen offline Spieler.
|
||||
* Wird beim nächsten Login angezeigt.
|
||||
*/
|
||||
public void addPendingNotification(UUID playerUUID, String rawMessage) {
|
||||
if (useMySQL) {
|
||||
String sql = "INSERT INTO ticket_pending_notifications (player_uuid, message) VALUES (?, ?)";
|
||||
@@ -607,9 +879,6 @@ public class DatabaseManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt alle ausstehenden Benachrichtigungen für einen Spieler.
|
||||
*/
|
||||
public List<String> getPendingNotifications(UUID playerUUID) {
|
||||
List<String> messages = new ArrayList<>();
|
||||
if (useMySQL) {
|
||||
@@ -627,9 +896,6 @@ public class DatabaseManager {
|
||||
return messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Löscht alle ausstehenden Benachrichtigungen eines Spielers nach dem Anzeigen.
|
||||
*/
|
||||
public void clearPendingNotifications(UUID playerUUID) {
|
||||
if (useMySQL) {
|
||||
String sql = "DELETE FROM ticket_pending_notifications WHERE player_uuid = ?";
|
||||
@@ -645,7 +911,7 @@ public class DatabaseManager {
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────── [NEW] Blacklist ───────────────────────────
|
||||
// ─────────────────────────── Blacklist ─────────────────────────────────
|
||||
|
||||
public boolean isBlacklisted(UUID uuid) {
|
||||
if (useMySQL) {
|
||||
@@ -705,7 +971,6 @@ public class DatabaseManager {
|
||||
}
|
||||
}
|
||||
|
||||
/** Gibt alle gesperrten Spieler als Liste von String-Arrays {uuid, name, reason, bannedBy} zurück. */
|
||||
public List<String[]> getBlacklist() {
|
||||
List<String[]> list = new ArrayList<>();
|
||||
if (useMySQL) {
|
||||
@@ -890,28 +1155,69 @@ public class DatabaseManager {
|
||||
// ─────────────────────────── Statistiken ───────────────────────────────
|
||||
|
||||
public TicketStats getTicketStats() {
|
||||
// Aktuelle Live-Daten aus der tickets-Tabelle
|
||||
List<Ticket> all = getAllTickets();
|
||||
int open = 0, claimed = 0, forwarded = 0, closed = 0, thumbsUp = 0, thumbsDown = 0;
|
||||
int open = 0, claimed = 0, forwarded = 0, closedLive = 0;
|
||||
java.util.Map<String, Integer> byPlayer = new java.util.HashMap<>();
|
||||
java.util.Map<String, Integer> byServer = new java.util.HashMap<>();
|
||||
for (Ticket t : all) {
|
||||
switch (t.getStatus()) {
|
||||
case OPEN -> open++;
|
||||
case CLAIMED -> claimed++;
|
||||
case FORWARDED -> forwarded++;
|
||||
case CLOSED -> closed++;
|
||||
case CLOSED -> closedLive++;
|
||||
}
|
||||
byPlayer.merge(t.getCreatorName(), 1, Integer::sum);
|
||||
byServer.merge(t.getServerName(), 1, Integer::sum);
|
||||
}
|
||||
|
||||
// Historische Bewertungen aus der persistenten Stats-Tabelle lesen
|
||||
// (enthält auch Daten von bereits gelöschten/archivierten Tickets)
|
||||
int thumbsUp = 0, thumbsDown = 0, totalClosedEver = closedLive;
|
||||
if (useMySQL) {
|
||||
String sql = """
|
||||
SELECT
|
||||
COUNT(*) AS total,
|
||||
SUM(player_rating = 'THUMBS_UP') AS up,
|
||||
SUM(player_rating = 'THUMBS_DOWN') AS down
|
||||
FROM ticket_stats
|
||||
""";
|
||||
try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) {
|
||||
ResultSet rs = stmt.executeQuery(sql);
|
||||
if (rs.next()) {
|
||||
totalClosedEver = rs.getInt("total");
|
||||
thumbsUp = rs.getInt("up");
|
||||
thumbsDown = rs.getInt("down");
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Fehler beim Laden der persistenten Stats: " + e.getMessage(), e);
|
||||
// Fallback: Bewertungen aus Live-Daten lesen
|
||||
for (Ticket t : all) {
|
||||
if ("THUMBS_UP".equals(t.getPlayerRating())) thumbsUp++;
|
||||
if ("THUMBS_DOWN".equals(t.getPlayerRating())) thumbsDown++;
|
||||
byPlayer.merge(t.getCreatorName(), 1, Integer::sum);
|
||||
}
|
||||
return new TicketStats(all.size(), open, closed, forwarded, thumbsUp, thumbsDown, byPlayer);
|
||||
}
|
||||
} else {
|
||||
// Datei-Modus: nur Live-Daten verfügbar
|
||||
for (Ticket t : all) {
|
||||
if ("THUMBS_UP".equals(t.getPlayerRating())) thumbsUp++;
|
||||
if ("THUMBS_DOWN".equals(t.getPlayerRating())) thumbsDown++;
|
||||
}
|
||||
}
|
||||
|
||||
return new TicketStats(all.size(), open, totalClosedEver, forwarded, thumbsUp, thumbsDown, byPlayer, byServer);
|
||||
}
|
||||
|
||||
public static class TicketStats {
|
||||
public final int total, open, closed, forwarded, thumbsUp, thumbsDown;
|
||||
public final java.util.Map<String, Integer> byPlayer;
|
||||
/** BungeeCord: Anzahl Tickets pro Server */
|
||||
public final java.util.Map<String, Integer> byServer;
|
||||
|
||||
public TicketStats(int total, int open, int closed, int forwarded,
|
||||
int thumbsUp, int thumbsDown, java.util.Map<String, Integer> byPlayer) {
|
||||
int thumbsUp, int thumbsDown,
|
||||
java.util.Map<String, Integer> byPlayer,
|
||||
java.util.Map<String, Integer> byServer) {
|
||||
this.total = total;
|
||||
this.open = open;
|
||||
this.closed = closed;
|
||||
@@ -919,6 +1225,7 @@ public class DatabaseManager {
|
||||
this.thumbsUp = thumbsUp;
|
||||
this.thumbsDown = thumbsDown;
|
||||
this.byPlayer = byPlayer;
|
||||
this.byServer = byServer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -998,6 +1305,10 @@ public class DatabaseManager {
|
||||
try { t.setPriority(TicketPriority.fromString(rs.getString("priority"))); } catch (SQLException ignored) {}
|
||||
try { t.setPlayerRating(rs.getString("player_rating")); } catch (SQLException ignored) {}
|
||||
try { t.setClaimerNotified(rs.getBoolean("claimer_notified")); } catch (SQLException ignored) {}
|
||||
// Bug-Fix: close_notified für duplikat-freie Schließ-Benachrichtigungen
|
||||
try { t.setCloseNotified(rs.getBoolean("close_notified")); } catch (SQLException ignored) {}
|
||||
// BungeeCord: Server-Name einlesen
|
||||
try { t.setServerName(rs.getString("server_name")); } catch (SQLException ignored) {}
|
||||
return t;
|
||||
}
|
||||
|
||||
@@ -1032,6 +1343,9 @@ public class DatabaseManager {
|
||||
obj.put("priority", t.getPriority().name());
|
||||
if (t.getPlayerRating() != null) obj.put("playerRating", t.getPlayerRating());
|
||||
obj.put("claimerNotified", t.isClaimerNotified());
|
||||
obj.put("closeNotified", t.isCloseNotified());
|
||||
// BungeeCord: Server-Name im JSON-Export
|
||||
obj.put("serverName", t.getServerName());
|
||||
return obj;
|
||||
}
|
||||
|
||||
@@ -1060,6 +1374,9 @@ public class DatabaseManager {
|
||||
if (obj.containsKey("priority")) t.setPriority(TicketPriority.fromString((String) obj.get("priority")));
|
||||
if (obj.containsKey("playerRating")) t.setPlayerRating((String) obj.get("playerRating"));
|
||||
if (obj.containsKey("claimerNotified"))t.setClaimerNotified((Boolean) obj.get("claimerNotified"));
|
||||
if (obj.containsKey("closeNotified")) t.setCloseNotified((Boolean) obj.get("closeNotified"));
|
||||
// BungeeCord: Server-Name aus JSON
|
||||
if (obj.containsKey("serverName")) t.setServerName((String) obj.get("serverName"));
|
||||
return t;
|
||||
} catch (Exception e) {
|
||||
if (plugin != null) plugin.getLogger().severe("Fehler beim Parsen eines Tickets: " + e.getMessage());
|
||||
|
||||
@@ -3,6 +3,7 @@ package de.ticketsystem.discord;
|
||||
import de.ticketsystem.TicketPlugin;
|
||||
import de.ticketsystem.model.ConfigCategory;
|
||||
import de.ticketsystem.model.Ticket;
|
||||
import de.ticketsystem.model.TicketPriority;
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
import java.io.OutputStream;
|
||||
@@ -10,96 +11,94 @@ import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Sendet Benachrichtigungen an einen Discord-Webhook.
|
||||
* Unterstützt Embeds mit Farbe, Feldern, Timestamp, Kategorie, Priorität und Rollen-Ping.
|
||||
*
|
||||
* Relevante config.yml-Felder:
|
||||
*
|
||||
* discord:
|
||||
* enabled: true
|
||||
* webhook-url: "https://discord.com/api/webhooks/..."
|
||||
* role-ping-id: "" # Rollen-ID für @Ping, leer = kein Ping
|
||||
* messages:
|
||||
* new-ticket:
|
||||
* title: "🎫 Neues Ticket erstellt"
|
||||
* color: "3066993"
|
||||
* footer: "TicketSystem"
|
||||
* show-position: true
|
||||
* show-category: true
|
||||
* show-priority: true
|
||||
* role-ping: true # Ping bei neuem Ticket an/aus
|
||||
* ticket-closed:
|
||||
* enabled: false
|
||||
* title: "🔒 Ticket geschlossen"
|
||||
* color: "15158332"
|
||||
* footer: "TicketSystem"
|
||||
* show-category: true
|
||||
* show-priority: true
|
||||
* role-ping: false
|
||||
* ticket-forwarded:
|
||||
* enabled: false
|
||||
* title: "🔀 Ticket weitergeleitet"
|
||||
* color: "15105570"
|
||||
* footer: "TicketSystem"
|
||||
* show-category: true
|
||||
* show-priority: true
|
||||
* role-ping: false
|
||||
*/
|
||||
public class DiscordWebhook {
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Konstanten & Felder
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
private static final String AVATAR_URL = "https://mc-heads.net/avatar/%s/64";
|
||||
|
||||
private final TicketPlugin plugin;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Konstruktor
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
public DiscordWebhook(TicketPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
// ─────────────────────────── Öffentliche Methoden ──────────────────────
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Öffentliche Methoden – Webhook-Events
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Sendet eine Benachrichtigung wenn ein neues Ticket erstellt wurde.
|
||||
*/
|
||||
public void sendNewTicket(Ticket ticket) {
|
||||
if (!isEnabled()) return;
|
||||
|
||||
String webhookUrl = getWebhookUrl();
|
||||
if (webhookUrl == null) return;
|
||||
|
||||
String title = plugin.getConfig().getString ("discord.messages.new-ticket.title", "🎫 Neues Ticket erstellt");
|
||||
String color = plugin.getConfig().getString ("discord.messages.new-ticket.color", "3066993");
|
||||
// Konfiguration lesen
|
||||
String title = plugin.getConfig().getString ("discord.messages.new-ticket.title", "Neues Ticket");
|
||||
String color = plugin.getConfig().getString ("discord.messages.new-ticket.color", "5793266");
|
||||
String footer = plugin.getConfig().getString ("discord.messages.new-ticket.footer", "TicketSystem");
|
||||
boolean showPos = plugin.getConfig().getBoolean("discord.messages.new-ticket.show-position", true);
|
||||
boolean showCat = plugin.getConfig().getBoolean("discord.messages.new-ticket.show-category", true);
|
||||
boolean showPri = plugin.getConfig().getBoolean("discord.messages.new-ticket.show-priority", true);
|
||||
boolean showSrv = plugin.getConfig().getBoolean("discord.messages.new-ticket.show-server", true);
|
||||
boolean ping = plugin.getConfig().getBoolean("discord.messages.new-ticket.role-ping", true);
|
||||
|
||||
StringBuilder fields = new StringBuilder();
|
||||
fields.append(field("Spieler", ticket.getCreatorName(), true));
|
||||
fields.append(",").append(field("Ticket ID", "#" + ticket.getId(), true));
|
||||
fields.append(",").append(field("Anliegen", ticket.getMessage(), false));
|
||||
// Hilfs-Werte berechnen
|
||||
String prioEmoji = getPriorityEmoji(ticket.getPriority());
|
||||
String avatarUrl = String.format(AVATAR_URL, ticket.getCreatorUUID().toString());
|
||||
|
||||
// Felder aufbauen
|
||||
List<Field> fields = new ArrayList<>();
|
||||
fields.add(new Field("👤 Spieler", ticket.getCreatorName(), true));
|
||||
fields.add(new Field("🎫 Ticket", "#" + ticket.getId(), true));
|
||||
|
||||
if (showCat && plugin.getConfig().getBoolean("categories-enabled", true)) {
|
||||
ConfigCategory cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey());
|
||||
fields.append(",").append(field("Kategorie", cat.getName(), true));
|
||||
fields.add(new Field("🏷️ Kategorie", cat.getName(), true));
|
||||
}
|
||||
|
||||
if (showPri && plugin.getConfig().getBoolean("priorities-enabled", true)) {
|
||||
fields.append(",").append(field("Priorität", ticket.getPriority().getDisplayName(), true));
|
||||
fields.add(new Field("⚡ Priorität", prioEmoji + " " + ticket.getPriority().getDisplayName(), true));
|
||||
}
|
||||
|
||||
if (showSrv && plugin.isBungeeCordEnabled() && !"unknown".equals(ticket.getServerName())) {
|
||||
fields.add(new Field("🖥️ Server", ticket.getServerName(), true));
|
||||
}
|
||||
|
||||
if (showPos) {
|
||||
fields.append(",").append(field("Welt", ticket.getWorldName(), true));
|
||||
fields.append(",").append(field("Position",
|
||||
fields.add(new Field("🌍 Welt", ticket.getWorldName(), true));
|
||||
fields.add(new Field("📍 Position",
|
||||
String.format("%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ()), true));
|
||||
}
|
||||
|
||||
String content = ping ? buildRolePing() : "";
|
||||
String json = buildPayload(content, title, Integer.parseInt(color), fields.toString(), footer);
|
||||
// JSON zusammenbauen & senden
|
||||
String description = "**Anliegen**\n> " + j(ticket.getMessage());
|
||||
|
||||
String json = buildJson(
|
||||
ping ? buildRolePing() : "",
|
||||
prioEmoji + " " + j(title) + " #" + ticket.getId(),
|
||||
description,
|
||||
Integer.parseInt(color),
|
||||
j(ticket.getCreatorName()), avatarUrl,
|
||||
avatarUrl,
|
||||
fields,
|
||||
j(footer) + " • Neues Ticket"
|
||||
);
|
||||
|
||||
sendAsync(webhookUrl, json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sendet eine Benachrichtigung wenn ein Ticket geschlossen wurde.
|
||||
*/
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
public void sendTicketClosed(Ticket ticket, String closerName) {
|
||||
if (!isEnabled()) return;
|
||||
if (!plugin.getConfig().getBoolean("discord.messages.ticket-closed.enabled", false)) return;
|
||||
@@ -107,37 +106,68 @@ public class DiscordWebhook {
|
||||
String webhookUrl = getWebhookUrl();
|
||||
if (webhookUrl == null) return;
|
||||
|
||||
String title = plugin.getConfig().getString ("discord.messages.ticket-closed.title", "🔒 Ticket geschlossen");
|
||||
String color = plugin.getConfig().getString ("discord.messages.ticket-closed.color", "15158332");
|
||||
// Konfiguration lesen
|
||||
String title = plugin.getConfig().getString ("discord.messages.ticket-closed.title", "Ticket geschlossen");
|
||||
String color = plugin.getConfig().getString ("discord.messages.ticket-closed.color", "15548997");
|
||||
String footer = plugin.getConfig().getString ("discord.messages.ticket-closed.footer", "TicketSystem");
|
||||
boolean showCat = plugin.getConfig().getBoolean("discord.messages.ticket-closed.show-category", true);
|
||||
boolean showPri = plugin.getConfig().getBoolean("discord.messages.ticket-closed.show-priority", true);
|
||||
boolean showSrv = plugin.getConfig().getBoolean("discord.messages.ticket-closed.show-server", true);
|
||||
boolean ping = plugin.getConfig().getBoolean("discord.messages.ticket-closed.role-ping", false);
|
||||
|
||||
StringBuilder fields = new StringBuilder();
|
||||
fields.append(field("Ticket ID", "#" + ticket.getId(), true));
|
||||
fields.append(",").append(field("Ersteller", ticket.getCreatorName(), true));
|
||||
fields.append(",").append(field("Geschlossen von", closerName, true));
|
||||
// Hilfs-Werte berechnen
|
||||
String prioEmoji = getPriorityEmoji(ticket.getPriority());
|
||||
String avatarUrl = String.format(AVATAR_URL, ticket.getCreatorUUID().toString());
|
||||
|
||||
// Beschreibung aufbauen
|
||||
StringBuilder desc = new StringBuilder();
|
||||
desc.append("**Anliegen**\n> ").append(j(ticket.getMessage()));
|
||||
|
||||
if (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) {
|
||||
desc.append("\n\n**Kommentar des Supports**\n> ").append(j(ticket.getCloseComment()));
|
||||
}
|
||||
|
||||
// Felder aufbauen
|
||||
List<Field> fields = new ArrayList<>();
|
||||
fields.add(new Field("👤 Ersteller", ticket.getCreatorName(), true));
|
||||
fields.add(new Field("🔒 Geschlossen von", j(closerName), true));
|
||||
fields.add(new Field("🎫 Ticket ID", "#" + ticket.getId(), true));
|
||||
|
||||
if (showCat && plugin.getConfig().getBoolean("categories-enabled", true)) {
|
||||
ConfigCategory cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey());
|
||||
fields.append(",").append(field("Kategorie", cat.getName(), true));
|
||||
}
|
||||
if (showPri && plugin.getConfig().getBoolean("priorities-enabled", true)) {
|
||||
fields.append(",").append(field("Priorität", ticket.getPriority().getDisplayName(), true));
|
||||
}
|
||||
if (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) {
|
||||
fields.append(",").append(field("Kommentar", ticket.getCloseComment(), false));
|
||||
fields.add(new Field("🏷️ Kategorie", cat.getName(), true));
|
||||
}
|
||||
|
||||
String content = ping ? buildRolePing() : "";
|
||||
String json = buildPayload(content, title, Integer.parseInt(color), fields.toString(), footer);
|
||||
if (showPri && plugin.getConfig().getBoolean("priorities-enabled", true)) {
|
||||
fields.add(new Field("⚡ Priorität", prioEmoji + " " + ticket.getPriority().getDisplayName(), true));
|
||||
}
|
||||
|
||||
if (showSrv && plugin.isBungeeCordEnabled() && !"unknown".equals(ticket.getServerName())) {
|
||||
fields.add(new Field("🖥️ Server", ticket.getServerName(), true));
|
||||
}
|
||||
|
||||
if (plugin.getConfig().getBoolean("rating-enabled", true) && ticket.getPlayerRating() != null) {
|
||||
String rating = "THUMBS_UP".equals(ticket.getPlayerRating()) ? "👍 Positiv" : "👎 Negativ";
|
||||
fields.add(new Field("⭐ Bewertung", rating, true));
|
||||
}
|
||||
|
||||
// JSON zusammenbauen & senden
|
||||
String json = buildJson(
|
||||
ping ? buildRolePing() : "",
|
||||
"🔒 " + j(title) + " #" + ticket.getId(),
|
||||
desc.toString(),
|
||||
Integer.parseInt(color),
|
||||
j(ticket.getCreatorName()), avatarUrl,
|
||||
avatarUrl,
|
||||
fields,
|
||||
j(footer) + " • Ticket geschlossen"
|
||||
);
|
||||
|
||||
sendAsync(webhookUrl, json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sendet eine Benachrichtigung wenn ein Ticket weitergeleitet wurde.
|
||||
*/
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
public void sendTicketForwarded(Ticket ticket, String fromName) {
|
||||
if (!isEnabled()) return;
|
||||
if (!plugin.getConfig().getBoolean("discord.messages.ticket-forwarded.enabled", false)) return;
|
||||
@@ -145,98 +175,180 @@ public class DiscordWebhook {
|
||||
String webhookUrl = getWebhookUrl();
|
||||
if (webhookUrl == null) return;
|
||||
|
||||
String title = plugin.getConfig().getString ("discord.messages.ticket-forwarded.title", "🔀 Ticket weitergeleitet");
|
||||
// Konfiguration lesen
|
||||
String title = plugin.getConfig().getString ("discord.messages.ticket-forwarded.title", "Ticket weitergeleitet");
|
||||
String color = plugin.getConfig().getString ("discord.messages.ticket-forwarded.color", "15105570");
|
||||
String footer = plugin.getConfig().getString ("discord.messages.ticket-forwarded.footer", "TicketSystem");
|
||||
boolean showCat = plugin.getConfig().getBoolean("discord.messages.ticket-forwarded.show-category", true);
|
||||
boolean showPri = plugin.getConfig().getBoolean("discord.messages.ticket-forwarded.show-priority", true);
|
||||
boolean showSrv = plugin.getConfig().getBoolean("discord.messages.ticket-forwarded.show-server", true);
|
||||
boolean ping = plugin.getConfig().getBoolean("discord.messages.ticket-forwarded.role-ping", false);
|
||||
|
||||
StringBuilder fields = new StringBuilder();
|
||||
fields.append(field("Ticket ID", "#" + ticket.getId(), true));
|
||||
fields.append(",").append(field("Ersteller", ticket.getCreatorName(), true));
|
||||
fields.append(",").append(field("Weitergeleitet von", fromName, true));
|
||||
fields.append(",").append(field("Weitergeleitet an", ticket.getForwardedToName(), true));
|
||||
// Hilfs-Werte berechnen
|
||||
String prioEmoji = getPriorityEmoji(ticket.getPriority());
|
||||
String avatarUrl = String.format(AVATAR_URL, ticket.getCreatorUUID().toString());
|
||||
|
||||
// Felder aufbauen
|
||||
String forwardedTo = ticket.getForwardedToName() != null ? j(ticket.getForwardedToName()) : "–";
|
||||
|
||||
List<Field> fields = new ArrayList<>();
|
||||
fields.add(new Field("👤 Ersteller", ticket.getCreatorName(), true));
|
||||
fields.add(new Field("📤 Weitergeleitet von", j(fromName), true));
|
||||
fields.add(new Field("📥 Weitergeleitet an", forwardedTo, true));
|
||||
fields.add(new Field("🎫 Ticket ID", "#" + ticket.getId(), true));
|
||||
|
||||
if (showCat && plugin.getConfig().getBoolean("categories-enabled", true)) {
|
||||
ConfigCategory cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey());
|
||||
fields.append(",").append(field("Kategorie", cat.getName(), true));
|
||||
}
|
||||
if (showPri && plugin.getConfig().getBoolean("priorities-enabled", true)) {
|
||||
fields.append(",").append(field("Priorität", ticket.getPriority().getDisplayName(), true));
|
||||
fields.add(new Field("🏷️ Kategorie", cat.getName(), true));
|
||||
}
|
||||
|
||||
String content = ping ? buildRolePing() : "";
|
||||
String json = buildPayload(content, title, Integer.parseInt(color), fields.toString(), footer);
|
||||
if (showPri && plugin.getConfig().getBoolean("priorities-enabled", true)) {
|
||||
fields.add(new Field("⚡ Priorität", prioEmoji + " " + ticket.getPriority().getDisplayName(), true));
|
||||
}
|
||||
|
||||
if (showSrv && plugin.isBungeeCordEnabled() && !"unknown".equals(ticket.getServerName())) {
|
||||
fields.add(new Field("🖥️ Server", ticket.getServerName(), true));
|
||||
}
|
||||
|
||||
// JSON zusammenbauen & senden
|
||||
String json = buildJson(
|
||||
ping ? buildRolePing() : "",
|
||||
"🔀 " + j(title) + " #" + ticket.getId(),
|
||||
"**Anliegen**\n> " + j(ticket.getMessage()),
|
||||
Integer.parseInt(color),
|
||||
j(ticket.getCreatorName()), avatarUrl,
|
||||
avatarUrl,
|
||||
fields,
|
||||
j(footer) + " • Ticket weitergeleitet"
|
||||
);
|
||||
|
||||
sendAsync(webhookUrl, json);
|
||||
}
|
||||
|
||||
// ─────────────────────────── Private Hilfsmethoden ─────────────────────
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// JSON-Bau
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
private record Field(String name, String value, boolean inline) {}
|
||||
|
||||
/**
|
||||
* Baut den kompletten JSON-Payload ohne String.format()-Chaos.
|
||||
* Kein verschachteltes Escaping, kein ungültiges JSON.
|
||||
*/
|
||||
private String buildJson(
|
||||
String content,
|
||||
String title,
|
||||
String description,
|
||||
int color,
|
||||
String authorName,
|
||||
String authorIcon,
|
||||
String thumbnailUrl,
|
||||
List<Field> fields,
|
||||
String footer
|
||||
) {
|
||||
// Fields-Array aufbauen
|
||||
StringBuilder fieldsJson = new StringBuilder("[");
|
||||
for (int i = 0; i < fields.size(); i++) {
|
||||
Field f = fields.get(i);
|
||||
if (i > 0) fieldsJson.append(",");
|
||||
fieldsJson
|
||||
.append("{")
|
||||
.append("\"name\":") .append(jsonString(f.name())) .append(",")
|
||||
.append("\"value\":") .append(jsonString(f.value())) .append(",")
|
||||
.append("\"inline\":") .append(f.inline())
|
||||
.append("}");
|
||||
}
|
||||
fieldsJson.append("]");
|
||||
|
||||
// Embed-Objekt aufbauen
|
||||
StringBuilder embed = new StringBuilder("{");
|
||||
embed.append("\"title\":") .append(jsonString(title)) .append(",");
|
||||
embed.append("\"description\":") .append(jsonString(description)) .append(",");
|
||||
embed.append("\"color\":") .append(color) .append(",");
|
||||
embed.append("\"author\":{")
|
||||
.append("\"name\":") .append(jsonString(authorName)) .append(",")
|
||||
.append("\"icon_url\":") .append(jsonString(authorIcon))
|
||||
.append("},");
|
||||
embed.append("\"thumbnail\":{\"url\":").append(jsonString(thumbnailUrl)).append("},");
|
||||
embed.append("\"fields\":") .append(fieldsJson) .append(",");
|
||||
embed.append("\"footer\":{\"text\":").append(jsonString(footer)) .append("},");
|
||||
embed.append("\"timestamp\":") .append(jsonString(Instant.now().toString()));
|
||||
embed.append("}");
|
||||
|
||||
// Root-Objekt
|
||||
return "{" +
|
||||
"\"content\":" + jsonString(content) + "," +
|
||||
"\"embeds\":[" + embed + "]" +
|
||||
"}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt einen JSON-String-Wert zurück inkl. Anführungszeichen.
|
||||
* Alle Sonderzeichen werden korrekt escaped.
|
||||
*/
|
||||
private String jsonString(String value) {
|
||||
if (value == null) value = "";
|
||||
|
||||
StringBuilder sb = new StringBuilder("\"");
|
||||
for (char c : value.toCharArray()) {
|
||||
switch (c) {
|
||||
case '"' -> sb.append("\\\"");
|
||||
case '\\' -> sb.append("\\\\");
|
||||
case '\n' -> sb.append("\\n");
|
||||
case '\r' -> { /* ignorieren */ }
|
||||
case '\t' -> sb.append("\\t");
|
||||
default -> {
|
||||
if (c < 0x20) {
|
||||
sb.append(String.format("\\u%04x", (int) c));
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sb.append("\"");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Kurz-Alias: Escaped einen Wert für die Verwendung innerhalb von
|
||||
* description-Strings (die bereits durch jsonString() laufen).
|
||||
*/
|
||||
private String j(String s) {
|
||||
if (s == null) return "–";
|
||||
return s.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"")
|
||||
.replace("\n", " ")
|
||||
.replace("\r", "");
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Hilfsmethoden
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
private boolean isEnabled() {
|
||||
return plugin.getConfig().getBoolean("discord.enabled", false);
|
||||
}
|
||||
|
||||
/** Gibt die Webhook-URL zurück oder null wenn nicht gesetzt. */
|
||||
private String getWebhookUrl() {
|
||||
String url = plugin.getConfig().getString("discord.webhook-url", "");
|
||||
return url.isEmpty() ? null : url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut den @Rollen-Ping-String aus der konfigurierten Rollen-ID.
|
||||
* Leer wenn keine ID gesetzt.
|
||||
*/
|
||||
private String buildRolePing() {
|
||||
String roleId = plugin.getConfig().getString("discord.role-ping-id", "").trim();
|
||||
if (roleId.isEmpty()) return "";
|
||||
return "<@&" + roleId + ">";
|
||||
return roleId.isEmpty() ? "" : "<@&" + roleId + ">";
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut einen einzelnen Embed-Field als JSON-String.
|
||||
*/
|
||||
private String field(String name, String value, boolean inline) {
|
||||
String safeValue = value != null
|
||||
? value.replace("\\", "\\\\").replace("\"", "\\\"")
|
||||
: "–";
|
||||
String safeName = name.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||
return String.format("{\"name\":\"%s\",\"value\":\"%s\",\"inline\":%b}",
|
||||
safeName, safeValue, inline);
|
||||
private String getPriorityEmoji(TicketPriority priority) {
|
||||
return switch (priority) {
|
||||
case LOW -> "🟢";
|
||||
case NORMAL -> "🟡";
|
||||
case HIGH -> "🟠";
|
||||
case URGENT -> "🔴";
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut den kompletten Webhook-Payload als JSON.
|
||||
* content = optionaler Ping-Text außerhalb des Embeds.
|
||||
*/
|
||||
private String buildPayload(String content, String title, int color, String fieldsJson, String footer) {
|
||||
String timestamp = Instant.now().toString();
|
||||
String safeTitle = title.replace("\"", "\\\"");
|
||||
String safeFooter = footer.replace("\"", "\\\"");
|
||||
String safeContent = content.replace("\"", "\\\"");
|
||||
|
||||
return String.format("""
|
||||
{
|
||||
"content": "%s",
|
||||
"embeds": [{
|
||||
"title": "%s",
|
||||
"color": %d,
|
||||
"fields": [%s],
|
||||
"footer": { "text": "%s" },
|
||||
"timestamp": "%s"
|
||||
}]
|
||||
}""",
|
||||
safeContent,
|
||||
safeTitle,
|
||||
color,
|
||||
fieldsJson,
|
||||
safeFooter,
|
||||
timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sendet den JSON-Payload asynchron an den Webhook.
|
||||
*/
|
||||
private void sendAsync(String webhookUrl, String json) {
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
try {
|
||||
@@ -254,8 +366,12 @@ public class DiscordWebhook {
|
||||
}
|
||||
|
||||
int responseCode = conn.getResponseCode();
|
||||
|
||||
if (plugin.isDebug()) {
|
||||
plugin.getLogger().info("[DEBUG] Discord Webhook Response: " + responseCode);
|
||||
plugin.getLogger().info("[DEBUG][DiscordWebhook] Response: " + responseCode);
|
||||
if (responseCode != 200 && responseCode != 204) {
|
||||
plugin.getLogger().info("[DEBUG][DiscordWebhook] Payload: " + json);
|
||||
}
|
||||
}
|
||||
|
||||
if (responseCode != 200 && responseCode != 204) {
|
||||
@@ -263,6 +379,7 @@ public class DiscordWebhook {
|
||||
}
|
||||
|
||||
conn.disconnect();
|
||||
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[DiscordWebhook] Fehler beim Senden: " + e.getMessage());
|
||||
if (plugin.isDebug()) e.printStackTrace();
|
||||
|
||||
@@ -196,9 +196,32 @@ public class TicketGUI implements Listener {
|
||||
// Slot 4: Ticket-Info
|
||||
inv.setItem(4, buildDetailInfoItem(ticket));
|
||||
|
||||
// Slot 10: Teleportieren
|
||||
// ── Teleport-Button ───────────────────────────────────────────────
|
||||
// Standalone: → normaler Teleport-Button
|
||||
// BungeeCord + bungee-teleport-enabled: → serverübergreifender Teleport-Button
|
||||
// BungeeCord + bungee-teleport deaktiviert → gesperrter Button
|
||||
if (!plugin.isBungeeCordEnabled()) {
|
||||
inv.setItem(10, buildActionItem(Material.ENDER_PEARL, "§b§lTeleportieren",
|
||||
List.of("§7Teleportiert dich zur", "§7Position des Tickets.")));
|
||||
} else if (plugin.getConfig().getBoolean("bungee-teleport-enabled", true)) {
|
||||
String targetServer = ticket.getServerName();
|
||||
boolean sameServer = plugin.getServerName().equals(targetServer);
|
||||
String serverLine = "unknown".equals(targetServer)
|
||||
? "§cServer unbekannt"
|
||||
: sameServer
|
||||
? "§7Dieser Server §a(direkt)"
|
||||
: "§7Ziel-Server: §b" + targetServer;
|
||||
inv.setItem(10, buildActionItem(Material.ENDER_PEARL, "§b§lTeleportieren",
|
||||
List.of("§7Teleportiert dich zur Ticket-Position.", serverLine,
|
||||
"§8" + (sameServer ? "Lokaler Teleport" : "Server-Wechsel erforderlich"))));
|
||||
} else {
|
||||
String serverInfo = !"unknown".equals(ticket.getServerName())
|
||||
? "§7Ticket-Server: §b" + ticket.getServerName()
|
||||
: "§7Server unbekannt";
|
||||
inv.setItem(10, buildActionItem(Material.GRAY_STAINED_GLASS_PANE, "§8Teleport deaktiviert",
|
||||
List.of("§7Im BungeeCord-Modus ist", "§7Teleportation deaktiviert.", serverInfo,
|
||||
"§8(bungee-teleport-enabled: false)")));
|
||||
}
|
||||
|
||||
// Slot 12: Claimen / Löschen / Grau
|
||||
if (ticket.getStatus() == TicketStatus.OPEN) {
|
||||
@@ -299,7 +322,6 @@ public class TicketGUI implements Listener {
|
||||
|
||||
// ── Spieler-GUI ────────────────────────────────────────────────────
|
||||
if (title.equals(PLAYER_GUI_TITLE)) {
|
||||
// Navigationstasten
|
||||
int curPage = playerPage.getOrDefault(player.getUniqueId(), 0);
|
||||
if (slot == 45) { openPlayerGUI(player, curPage - 1); return; }
|
||||
if (slot == 53) { openPlayerGUI(player, curPage + 1); return; }
|
||||
@@ -355,19 +377,16 @@ public class TicketGUI implements Listener {
|
||||
|
||||
// ─────────────────────────── Navigation-Handler ─────────────────────────
|
||||
|
||||
/**
|
||||
* Verarbeitet Klicks auf die Navigationsleiste der Admin-Übersicht (Slots 45–53).
|
||||
*/
|
||||
private void handleAdminNavClick(Player player, int slot, boolean isArchive) {
|
||||
int curPage = adminPage.getOrDefault(player.getUniqueId(), 0);
|
||||
switch (slot) {
|
||||
case 45 -> openGUI(player, curPage - 1); // Zurück
|
||||
case 53 -> openGUI(player, curPage + 1); // Vor
|
||||
case 49 -> { // Archiv-Button oder Zurück im Archiv
|
||||
case 45 -> openGUI(player, curPage - 1);
|
||||
case 53 -> openGUI(player, curPage + 1);
|
||||
case 49 -> {
|
||||
if (player.hasPermission(ARCHIVE_PERMISSION)) openClosedGUI(player);
|
||||
else player.sendMessage(plugin.color("&cDu hast keine Berechtigung, das Archiv zu öffnen."));
|
||||
}
|
||||
case 47 -> { // Kategorie-Filter (wenn aktiviert)
|
||||
case 47 -> {
|
||||
if (plugin.getConfig().getBoolean("categories-enabled", true)) {
|
||||
cycleCategoryFilter(player);
|
||||
openGUI(player, 0);
|
||||
@@ -381,11 +400,10 @@ public class TicketGUI implements Listener {
|
||||
switch (slot) {
|
||||
case 45 -> openClosedGUI(player, curPage - 1);
|
||||
case 53 -> openClosedGUI(player, curPage + 1);
|
||||
case 49 -> openGUI(player); // Zurück zur Hauptübersicht
|
||||
case 49 -> openGUI(player);
|
||||
}
|
||||
}
|
||||
|
||||
/** Wechselt zum nächsten Kategorie-Filter */
|
||||
private void cycleCategoryFilter(Player player) {
|
||||
CategoryManager cm = plugin.getCategoryManager();
|
||||
List<ConfigCategory> all = cm.getAll();
|
||||
@@ -396,7 +414,7 @@ public class TicketGUI implements Listener {
|
||||
} else {
|
||||
int idx = all.indexOf(current);
|
||||
int next = idx + 1;
|
||||
if (next >= all.size()) categoryFilter.remove(player.getUniqueId()); // Zurück zu Alle
|
||||
if (next >= all.size()) categoryFilter.remove(player.getUniqueId());
|
||||
else categoryFilter.put(player.getUniqueId(), all.get(next));
|
||||
}
|
||||
ConfigCategory newFilter = categoryFilter.getOrDefault(player.getUniqueId(), null);
|
||||
@@ -416,15 +434,97 @@ public class TicketGUI implements Listener {
|
||||
});
|
||||
}
|
||||
|
||||
// ── BUG FIX: handleDetailTeleport ────────────────────────────────────────
|
||||
// Vorher: Teleport wurde immer ausgeführt – auch bei aktivem BungeeCord.
|
||||
// ticket.getLocation() gibt null zurück wenn die World auf diesem
|
||||
// Server nicht existiert → NullPointerException oder falscher Teleport.
|
||||
//
|
||||
// Fix: Bei bungeecord: true + bungee-teleport-enabled: true →
|
||||
// 1. Zielposition in DB speichern (ticket_pending_teleport)
|
||||
// 2. Spieler via Plugin Messaging Channel auf Ziel-Server schicken
|
||||
// 3. PlayerJoinListener teleportiert ihn dort zur Position
|
||||
// Bei bungeecord: true + bungee-teleport-enabled: false → gesperrt.
|
||||
// Bei bungeecord: false → normaler lokaler Teleport wie bisher.
|
||||
//
|
||||
// Hinweis: Ist der Admin bereits auf dem Ziel-Server, wird direkt teleportiert.
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
private void handleDetailTeleport(Player player, Ticket ticket) {
|
||||
if (!plugin.isBungeeCordEnabled()) {
|
||||
// ── Standalone-Modus: direkt teleportieren ──
|
||||
if (ticket.getLocation() != null) {
|
||||
player.teleport(ticket.getLocation());
|
||||
player.sendMessage(plugin.color("&7Du wurdest zu Ticket &e#" + ticket.getId() + " &7teleportiert."));
|
||||
} else {
|
||||
player.sendMessage(plugin.color("&cDie Welt des Tickets ist nicht geladen!"));
|
||||
}
|
||||
openDetailGUI(player, ticket);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── BungeeCord-Modus ──────────────────────────────────────────────
|
||||
boolean bungeeTP = plugin.getConfig().getBoolean("bungee-teleport-enabled", true);
|
||||
if (!bungeeTP) {
|
||||
String serverHint = !"unknown".equals(ticket.getServerName())
|
||||
? " §7(Server: §b" + ticket.getServerName() + "§7)" : "";
|
||||
player.sendMessage(plugin.color("&cServerübergreifender Teleport ist in der Config deaktiviert." + serverHint));
|
||||
openDetailGUI(player, ticket);
|
||||
return;
|
||||
}
|
||||
|
||||
String targetServer = ticket.getServerName();
|
||||
if ("unknown".equals(targetServer)) {
|
||||
player.sendMessage(plugin.color("&cServer des Tickets unbekannt – Teleport nicht möglich."));
|
||||
openDetailGUI(player, ticket);
|
||||
return;
|
||||
}
|
||||
|
||||
String currentServer = plugin.getServerName();
|
||||
|
||||
if (currentServer.equals(targetServer)) {
|
||||
// ── Bereits auf dem richtigen Server: direkt teleportieren ────
|
||||
if (ticket.getLocation() != null) {
|
||||
player.teleport(ticket.getLocation());
|
||||
player.sendMessage(plugin.color("&7Du wurdest zu Ticket &e#" + ticket.getId() + " &7teleportiert."));
|
||||
} else {
|
||||
player.sendMessage(plugin.color("&cDie Welt des Tickets ist nicht geladen!"));
|
||||
}
|
||||
openDetailGUI(player, ticket);
|
||||
} else {
|
||||
// ── Anderer Server: Position in DB speichern + Server-Wechsel ─
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
plugin.getDatabaseManager().setPendingTeleport(
|
||||
player.getUniqueId(),
|
||||
ticket.getWorldName(),
|
||||
ticket.getX(), ticket.getY(), ticket.getZ(),
|
||||
ticket.getYaw(), ticket.getPitch()
|
||||
);
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
// BungeeCord Plugin Messaging Channel: Spieler auf Ziel-Server schicken
|
||||
player.sendMessage(plugin.color("&7Verbinde dich mit Server &b" + targetServer
|
||||
+ " &7für Ticket &e#" + ticket.getId() + "&7..."));
|
||||
try {
|
||||
java.io.ByteArrayOutputStream b = new java.io.ByteArrayOutputStream();
|
||||
java.io.DataOutputStream out = new java.io.DataOutputStream(b);
|
||||
out.writeUTF("Connect");
|
||||
out.writeUTF(targetServer);
|
||||
player.sendPluginMessage(plugin, "BungeeCord", b.toByteArray());
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[TicketSystem] BungeeCord Connect fehlgeschlagen: " + e.getMessage());
|
||||
player.sendMessage(plugin.color("&cServer-Wechsel fehlgeschlagen. Bitte manuell verbinden."));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── BUG FIX: handleDetailClaim ───────────────────────────────────────────
|
||||
// Vorher: Nach erfolgreichem Claim wurde immer teleportiert wenn
|
||||
// ticket.getLocation() != null – unabhängig von BungeeCord.
|
||||
//
|
||||
// Fix: Teleport nach Claim nutzt dieselbe Logik wie handleDetailTeleport:
|
||||
// Standalone → direkt, BungeeCord + enabled → Server-Wechsel + pending,
|
||||
// BungeeCord + disabled → nur Nachricht, kein Teleport.
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
private void handleDetailClaim(Player player, Ticket ticket) {
|
||||
if (ticket.getStatus() != TicketStatus.OPEN) {
|
||||
player.sendMessage(plugin.formatMessage("messages.already-claimed"));
|
||||
@@ -440,11 +540,9 @@ public class TicketGUI implements Listener {
|
||||
ticket.setClaimerUUID(player.getUniqueId());
|
||||
ticket.setClaimerName(player.getName());
|
||||
plugin.getTicketManager().notifyCreatorClaimed(ticket);
|
||||
if (ticket.getLocation() != null) player.teleport(ticket.getLocation());
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
Ticket fresh = plugin.getDatabaseManager().getTicketById(ticket.getId());
|
||||
Bukkit.getScheduler().runTask(plugin, () -> { if (fresh != null) openDetailGUI(player, fresh); });
|
||||
});
|
||||
|
||||
// Teleport nach dem Claim – gleiche Logik wie handleDetailTeleport
|
||||
handleDetailTeleport(player, ticket);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -513,7 +611,6 @@ public class TicketGUI implements Listener {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private void handleShowComments(Player player, Ticket ticket) {
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
List<TicketComment> comments = plugin.getDatabaseManager().getComments(ticket.getId());
|
||||
@@ -529,7 +626,6 @@ public class TicketGUI implements Listener {
|
||||
}
|
||||
}
|
||||
player.sendMessage(plugin.color("&8&m "));
|
||||
// Gleich wieder Detail-GUI öffnen
|
||||
openDetailGUI(player, ticket);
|
||||
});
|
||||
});
|
||||
@@ -552,10 +648,21 @@ public class TicketGUI implements Listener {
|
||||
}
|
||||
|
||||
final String comment = input.equals("-") ? "" : input;
|
||||
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
boolean success = plugin.getDatabaseManager().closeTicket(ticketId, comment);
|
||||
if (success) {
|
||||
Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId);
|
||||
|
||||
// ── FIX: Schließung in persistente Stats-Tabelle eintragen ──────────
|
||||
// Vorher fehlte dieser Aufruf in der GUI – Bewertungen wurden dem
|
||||
// schließenden Admin zugeordnet nur wenn /ticket close genutzt wurde.
|
||||
// Jetzt wird player.getName() korrekt als closerName übergeben,
|
||||
// unabhängig davon ob das Ticket vorher von jemand anderem geclaimed war.
|
||||
if (ticket != null) {
|
||||
plugin.getDatabaseManager().recordClosedTicket(ticket, player.getName());
|
||||
}
|
||||
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
player.sendMessage(plugin.formatMessage("messages.ticket-closed").replace("{id}", String.valueOf(ticketId)));
|
||||
if (!comment.isEmpty()) player.sendMessage(plugin.color("&7Kommentar: &f" + comment));
|
||||
@@ -570,33 +677,24 @@ public class TicketGUI implements Listener {
|
||||
|
||||
// ─────────────────────────── Item-Builder ──────────────────────────────
|
||||
|
||||
/**
|
||||
* Füllt die Navigationsleiste (letzte Reihe, Slots 45–53).
|
||||
* Layout: [45]=Zurück | [47]=Filter | [49]=Archiv/Hauptmenü | [51]=leer | [53]=Weiter
|
||||
*/
|
||||
private void fillAdminNavigation(Inventory inv, boolean isArchiveView, Player player, int page, int totalPages) {
|
||||
ItemStack glass = makeGlass();
|
||||
for (int i = 45; i < 54; i++) inv.setItem(i, glass);
|
||||
|
||||
// Zurück (Slot 45)
|
||||
if (page > 0) {
|
||||
inv.setItem(45, buildActionItem(Material.ARROW, "§7§l◄ Zurück",
|
||||
List.of("§7Seite " + page + " von " + totalPages)));
|
||||
}
|
||||
|
||||
// Weiter (Slot 53)
|
||||
if (page < totalPages - 1) {
|
||||
inv.setItem(53, buildActionItem(Material.ARROW, "§7§lWeiter ►",
|
||||
List.of("§7Seite " + (page + 2) + " von " + totalPages)));
|
||||
}
|
||||
|
||||
// Seitenanzeige (Slot 49)
|
||||
if (!isArchiveView) {
|
||||
if (player.hasPermission(ARCHIVE_PERMISSION)) {
|
||||
inv.setItem(49, buildActionItem(Material.CHEST, "§7§lGeschlossene Tickets",
|
||||
List.of("§7Zeigt alle abgeschlossenen", "§7Tickets im Archiv an.")));
|
||||
}
|
||||
// Kategorie-Filter (Slot 47), nur wenn aktiviert
|
||||
if (plugin.getConfig().getBoolean("categories-enabled", true)) {
|
||||
ConfigCategory currentFilter = categoryFilter.getOrDefault(player.getUniqueId(), null);
|
||||
String filterLabel = currentFilter != null ? currentFilter.getColored() : "§7Alle";
|
||||
@@ -611,12 +709,10 @@ public class TicketGUI implements Listener {
|
||||
inv.setItem(47, buildActionItem(Material.HOPPER, "§e§lKategorie-Filter", filterLore));
|
||||
}
|
||||
} else {
|
||||
// Im Archiv: Zurück-Button in Slot 49
|
||||
inv.setItem(49, buildActionItem(Material.ARROW, "§7§lZurück zur Übersicht",
|
||||
List.of("§7Zeigt alle offenen Tickets.")));
|
||||
}
|
||||
|
||||
// Seitenanzeige Mitte oben (Slot 48)
|
||||
inv.setItem(48, buildActionItem(Material.PAPER, "§8Seite " + (page + 1) + "/" + totalPages,
|
||||
List.of("§7Gesamt: " + (playerSlotMap.containsKey(player.getUniqueId())
|
||||
? playerSlotMap.get(player.getUniqueId()).size() + "+" : "?") + " Tickets auf dieser Seite")));
|
||||
@@ -630,11 +726,7 @@ public class TicketGUI implements Listener {
|
||||
inv.setItem(49, buildActionItem(Material.PAPER, "§8Seite " + (page + 1) + "/" + totalPages, List.of()));
|
||||
}
|
||||
|
||||
// ─────────────────────────── Item-Builder ──────────────────────────────
|
||||
|
||||
private ItemStack buildAdminListItem(Ticket ticket) {
|
||||
// Material: Kategorie aus Config (z.B. REDSTONE für Bug, BOOK für Frage)
|
||||
// Fallback auf Status-Material wenn categories-enabled: false
|
||||
Material mat;
|
||||
if (plugin.getConfig().getBoolean("categories-enabled", true)) {
|
||||
mat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey()).getMaterial();
|
||||
@@ -651,7 +743,6 @@ public class TicketGUI implements Listener {
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta == null) return item;
|
||||
|
||||
// Priorität farblich im Titel anzeigen (wenn aktiviert)
|
||||
String priorityPrefix = plugin.getConfig().getBoolean("priorities-enabled", true)
|
||||
? ticket.getPriority().getColored() + " §8| " : "";
|
||||
meta.setDisplayName(priorityPrefix + "§6§lTicket #" + ticket.getId() + " §r" + ticket.getStatus().getColored());
|
||||
@@ -693,6 +784,9 @@ public class TicketGUI implements Listener {
|
||||
lore.add("§7Ersteller: §e" + ticket.getCreatorName());
|
||||
lore.add("§7Anliegen: §f" + ticket.getMessage());
|
||||
lore.add("§7Erstellt: §e" + DATE_FORMAT.format(ticket.getCreatedAt()));
|
||||
if (plugin.isBungeeCordEnabled() && !"unknown".equals(ticket.getServerName())) {
|
||||
lore.add("§7Server: §b" + ticket.getServerName());
|
||||
}
|
||||
lore.add("§7Welt: §e" + ticket.getWorldName());
|
||||
lore.add(String.format("§7Position: §e%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ()));
|
||||
if (plugin.getConfig().getBoolean("categories-enabled", true)) {
|
||||
@@ -739,6 +833,9 @@ public class TicketGUI implements Listener {
|
||||
lore.add("§8§m ");
|
||||
lore.add("§7Anliegen: §f" + ticket.getMessage());
|
||||
lore.add("§7Erstellt: §e" + DATE_FORMAT.format(ticket.getCreatedAt()));
|
||||
if (plugin.isBungeeCordEnabled() && !"unknown".equals(ticket.getServerName())) {
|
||||
lore.add("§7Server: §b" + ticket.getServerName());
|
||||
}
|
||||
lore.add("§7Welt: §e" + ticket.getWorldName());
|
||||
lore.add(String.format("§7Position: §e%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ()));
|
||||
if (plugin.getConfig().getBoolean("categories-enabled", true)) {
|
||||
|
||||
@@ -3,10 +3,13 @@ package de.ticketsystem.listeners;
|
||||
import java.util.List;
|
||||
|
||||
import de.ticketsystem.TicketPlugin;
|
||||
import de.ticketsystem.database.DatabaseManager;
|
||||
import de.ticketsystem.model.Ticket;
|
||||
import de.ticketsystem.model.TicketStatus;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
@@ -39,6 +42,37 @@ public class PlayerJoinListener implements Listener {
|
||||
});
|
||||
}
|
||||
|
||||
// ── BungeeCord: ausstehenden Teleport-Auftrag prüfen ─────────────
|
||||
// Wenn ein Admin via GUI auf einen anderen Server geschickt wurde,
|
||||
// liegt hier die Zielposition. Wir teleportieren ihn nach dem Spawn.
|
||||
if (plugin.isBungeeCordEnabled()
|
||||
&& plugin.getConfig().getBoolean("bungee-teleport-enabled", true)) {
|
||||
Bukkit.getScheduler().runTaskLater(plugin, () -> {
|
||||
if (!player.isOnline()) return;
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
DatabaseManager.PendingTeleport pt =
|
||||
plugin.getDatabaseManager().consumePendingTeleport(player.getUniqueId());
|
||||
if (pt == null) return;
|
||||
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
if (!player.isOnline()) return;
|
||||
World world = Bukkit.getWorld(pt.world());
|
||||
if (world == null) {
|
||||
player.sendMessage(plugin.color(
|
||||
"&cTeleport-Zielwelt &e" + pt.world() + " &cnicht gefunden!"));
|
||||
return;
|
||||
}
|
||||
Location loc = new Location(world, pt.x(), pt.y(), pt.z(), pt.yaw(), pt.pitch());
|
||||
player.teleport(loc);
|
||||
player.sendMessage(plugin.color(
|
||||
"&7Du wurdest zur Ticket-Position teleportiert. &8("
|
||||
+ String.format("%.0f, %.0f, %.0f", pt.x(), pt.y(), pt.z()) + ")"));
|
||||
});
|
||||
});
|
||||
// 40 Ticks (2 Sek) Verzögerung damit der Spieler vollständig gespawnt ist
|
||||
}, 40L);
|
||||
}
|
||||
|
||||
// ── Ausstehende Kommentar-/Schließ-Benachrichtigungen anzeigen ────
|
||||
// (Nachrichten die ankamen während der Spieler offline war)
|
||||
Bukkit.getScheduler().runTaskLater(plugin, () -> {
|
||||
@@ -66,15 +100,17 @@ public class PlayerJoinListener implements Listener {
|
||||
plugin.getTicketManager().notifyClaimedWhileOffline(player);
|
||||
}, 60L);
|
||||
|
||||
// ── Spieler: über geschlossene Tickets mit Kommentar informieren ──
|
||||
// ── Spieler: über geschlossene Tickets informieren (nur wenn noch nicht geschehen) ──
|
||||
// Bug-Fix: Nutzt close_notified aus der DB statt in-memory Set.
|
||||
// Verhindert Duplikate bei Server-Wechseln in BungeeCord-Netzwerken.
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
List<Ticket> closed = plugin.getDatabaseManager()
|
||||
.getTicketsByStatus(TicketStatus.CLOSED);
|
||||
|
||||
for (Ticket t : closed) {
|
||||
if (!t.getCreatorUUID().equals(player.getUniqueId())) continue;
|
||||
if (t.getCloseComment() == null || t.getCloseComment().isEmpty()) continue;
|
||||
if (plugin.getTicketManager().wasClosedNotificationSent(t.getId())) continue;
|
||||
// DB-Feld prüfen – funktioniert serverübergreifend
|
||||
if (t.isCloseNotified()) continue;
|
||||
|
||||
Bukkit.getScheduler().runTask(plugin, () ->
|
||||
plugin.getTicketManager().notifyCreatorClosed(t));
|
||||
|
||||
@@ -8,9 +8,7 @@ import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public class TicketManager {
|
||||
@@ -20,9 +18,6 @@ public class TicketManager {
|
||||
/** Cooldown Map: UUID → Zeitstempel letztes Ticket */
|
||||
private final Map<UUID, Long> cooldowns = new HashMap<>();
|
||||
|
||||
/** Ticket-IDs für die der Ersteller bereits über Schließung informiert wurde */
|
||||
private final Set<Integer> notifiedClosedTickets = new HashSet<>();
|
||||
|
||||
public TicketManager(TicketPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
@@ -46,8 +41,11 @@ public class TicketManager {
|
||||
// ─────────────────────────── Benachrichtigungen ────────────────────────
|
||||
|
||||
/**
|
||||
* Benachrichtigt alle Online-Supporter/Admins über ein neues Ticket
|
||||
* und sendet optional eine Discord-Webhook-Nachricht.
|
||||
* Benachrichtigt alle Supporter/Admins über ein neues Ticket – auch auf anderen Servern.
|
||||
*
|
||||
* Lokal online Spieler werden direkt angesprochen.
|
||||
* Über BungeeCord werden alle anderen Server im Netzwerk ebenfalls benachrichtigt.
|
||||
* Optional sendet der Discord-Webhook eine Nachricht.
|
||||
*/
|
||||
public void notifyTeam(Ticket ticket) {
|
||||
String creatorName = ticket.getCreatorName() != null ? ticket.getCreatorName() : "Unbekannt";
|
||||
@@ -57,23 +55,41 @@ public class TicketManager {
|
||||
String categoryInfo = "";
|
||||
String priorityInfo = "";
|
||||
if (plugin.getConfig().getBoolean("categories-enabled", true)) {
|
||||
de.ticketsystem.model.ConfigCategory cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey());
|
||||
ConfigCategory cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey());
|
||||
categoryInfo = " §7[§r" + cat.getColored() + "§7]";
|
||||
}
|
||||
if (plugin.getConfig().getBoolean("priorities-enabled", true)) {
|
||||
priorityInfo = " §7Priorität: §r" + ticket.getPriority().getColored();
|
||||
}
|
||||
|
||||
// BungeeCord: Server-Herkunft anzeigen wenn BungeeCord aktiviert
|
||||
String serverInfo = "";
|
||||
if (plugin.isBungeeCordEnabled() && !"unknown".equals(ticket.getServerName())) {
|
||||
serverInfo = " §7Server: §b" + ticket.getServerName();
|
||||
}
|
||||
|
||||
String msg = plugin.formatMessage("messages.new-ticket-notify")
|
||||
.replace("{player}", creatorName)
|
||||
.replace("{message}", message)
|
||||
.replace("{id}", String.valueOf(ticket.getId()))
|
||||
+ categoryInfo + priorityInfo;
|
||||
+ categoryInfo + priorityInfo + serverInfo;
|
||||
|
||||
String guiHint = plugin.color("&7» Klicke &e/ticket list &7um die GUI zu öffnen.");
|
||||
|
||||
if (plugin.isBungeeCordEnabled()) {
|
||||
// ─ BungeeCord-Modus: Team-Broadcast über alle Server ─────────────────
|
||||
// BungeeMessenger sendet lokal direkt, dann per Forward an alle anderen Server.
|
||||
// Beide Nachrichten werden zu einer zusammengefasst um ein einzelnes
|
||||
// Forward-Paket zu erzeugen statt zwei (reduziert Netzwerklast und
|
||||
// verhindert mögliche Reihenfolge-Probleme).
|
||||
plugin.getBungeeMessenger().broadcastTeamNotification(msg + "\n" + guiHint);
|
||||
} else {
|
||||
// ─ Standalone-Modus: Nur lokal ───────────────────────────────
|
||||
for (Player p : Bukkit.getOnlinePlayers()) {
|
||||
if (p.hasPermission("ticket.support") || p.hasPermission("ticket.admin")) {
|
||||
p.sendMessage(msg);
|
||||
p.sendMessage(plugin.color("&7» Klicke &e/ticket list &7um die GUI zu öffnen."));
|
||||
p.sendMessage(guiHint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,20 +99,18 @@ public class TicketManager {
|
||||
/**
|
||||
* Benachrichtigt den Ersteller, wenn sein Ticket angenommen wurde.
|
||||
* Setzt claimer_notified = true und persistiert es.
|
||||
*
|
||||
* BungeeCord: Zustellung auch wenn der Spieler auf einem anderen Server ist.
|
||||
*/
|
||||
public void notifyCreatorClaimed(Ticket ticket) {
|
||||
Player creator = Bukkit.getPlayer(ticket.getCreatorUUID());
|
||||
if (creator != null && creator.isOnline()) {
|
||||
String claimerName = ticket.getClaimerName();
|
||||
if (claimerName == null && ticket.getClaimerUUID() != null)
|
||||
claimerName = Bukkit.getOfflinePlayer(ticket.getClaimerUUID()).getName();
|
||||
if (claimerName == null) claimerName = "Support";
|
||||
String claimerName = resolveClaimerName(ticket);
|
||||
|
||||
String msg = plugin.formatMessage("messages.ticket-claimed-notify")
|
||||
.replace("{id}", String.valueOf(ticket.getId()))
|
||||
.replace("{claimer}", claimerName);
|
||||
creator.sendMessage(msg);
|
||||
}
|
||||
|
||||
deliverToPlayer(ticket.getCreatorUUID(), ticket.getCreatorName(), msg);
|
||||
|
||||
// Persistiert setzen, damit Join-Listener weiß, dass Spieler bereits informiert ist
|
||||
plugin.getDatabaseManager().markClaimerNotified(ticket.getId());
|
||||
}
|
||||
@@ -106,15 +120,13 @@ public class TicketManager {
|
||||
* die geclaimt oder weitergeleitet wurden während er offline war.
|
||||
*/
|
||||
public void notifyClaimedWhileOffline(Player player) {
|
||||
// Suche alle Tickets dieses Spielers, die CLAIMED/FORWARDED sind,
|
||||
// aber noch nicht notified wurden
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
var tickets = plugin.getDatabaseManager().getTicketsByStatus(
|
||||
TicketStatus.CLAIMED, TicketStatus.FORWARDED);
|
||||
|
||||
for (Ticket t : tickets) {
|
||||
if (!t.getCreatorUUID().equals(player.getUniqueId())) continue;
|
||||
if (t.isClaimerNotified()) continue; // wurde schon informiert
|
||||
if (t.isClaimerNotified()) continue;
|
||||
|
||||
String claimerName = t.getClaimerName() != null ? t.getClaimerName() : "Support";
|
||||
final String name = claimerName;
|
||||
@@ -142,63 +154,171 @@ public class TicketManager {
|
||||
|
||||
/**
|
||||
* Benachrichtigt den Ersteller, wenn sein Ticket weitergeleitet wurde.
|
||||
* BungeeCord: Cross-Server-Zustellung.
|
||||
*/
|
||||
public void notifyCreatorForwarded(Ticket ticket) {
|
||||
Player creator = Bukkit.getPlayer(ticket.getCreatorUUID());
|
||||
if (creator != null && creator.isOnline()) {
|
||||
String forwardedTo = ticket.getForwardedToName() != null ? ticket.getForwardedToName() : "einen Supporter";
|
||||
String msg = plugin.formatMessage("messages.ticket-forwarded-creator-notify")
|
||||
.replace("{id}", String.valueOf(ticket.getId()))
|
||||
.replace("{supporter}", forwardedTo);
|
||||
creator.sendMessage(msg);
|
||||
}
|
||||
// Auch hier notified setzen
|
||||
|
||||
deliverToPlayer(ticket.getCreatorUUID(), ticket.getCreatorName(), msg);
|
||||
|
||||
// Auch bei Weiterleitung notified setzen
|
||||
plugin.getDatabaseManager().markClaimerNotified(ticket.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sendet dem weitergeleiteten Supporter eine Benachrichtigung.
|
||||
* BungeeCord: Zustellung auch wenn der Supporter auf einem anderen Server ist.
|
||||
*/
|
||||
public void notifyForwardedTo(Ticket ticket, String fromName) {
|
||||
Player target = Bukkit.getPlayer(ticket.getForwardedToUUID());
|
||||
if (target != null && target.isOnline()) {
|
||||
if (ticket.getForwardedToUUID() == null) return;
|
||||
|
||||
String creatorName = ticket.getCreatorName() != null ? ticket.getCreatorName() : "Unbekannt";
|
||||
String msg = plugin.formatMessage("messages.ticket-forwarded-notify")
|
||||
.replace("{player}", creatorName)
|
||||
.replace("{id}", String.valueOf(ticket.getId()));
|
||||
target.sendMessage(msg);
|
||||
}
|
||||
|
||||
deliverToPlayer(ticket.getForwardedToUUID(), ticket.getForwardedToName(), msg);
|
||||
|
||||
plugin.getDiscordWebhook().sendTicketForwarded(ticket, fromName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Benachrichtigt den Ersteller, wenn sein Ticket geschlossen wurde.
|
||||
* BungeeCord: Cross-Server-Zustellung + Fallback in Pending-DB.
|
||||
*/
|
||||
public void notifyCreatorClosed(Ticket ticket) { notifyCreatorClosed(ticket, null); }
|
||||
|
||||
public void notifyCreatorClosed(Ticket ticket, String closerName) {
|
||||
notifiedClosedTickets.add(ticket.getId());
|
||||
// Bug-Fix: close_notified wird in der DB gespeichert – kein In-Memory-Set mehr.
|
||||
// Dadurch funktioniert der Check auch nach einem Server-Wechsel korrekt.
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () ->
|
||||
plugin.getDatabaseManager().markCloseNotified(ticket.getId()));
|
||||
|
||||
Player creator = Bukkit.getPlayer(ticket.getCreatorUUID());
|
||||
String comment = (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty())
|
||||
? ticket.getCloseComment() : "";
|
||||
|
||||
if (creator != null && creator.isOnline()) {
|
||||
// Hauptnachricht
|
||||
String msg = plugin.formatMessage("messages.ticket-closed-notify")
|
||||
.replace("{id}", String.valueOf(ticket.getId()))
|
||||
.replace("{comment}", comment);
|
||||
|
||||
// Bewertungsaufforderung
|
||||
String ratingMsg = null;
|
||||
if (plugin.getConfig().getBoolean("rating-enabled", true)) {
|
||||
ratingMsg = plugin.color(
|
||||
"&8&m &r\n" +
|
||||
"&6Wie zufrieden bist du mit dem Support?\n" +
|
||||
"&a/ticket rate " + ticket.getId() + " good &7– 👍 Gut\n" +
|
||||
"&c/ticket rate " + ticket.getId() + " bad &7– 👎 Schlecht\n" +
|
||||
"&8&m ");
|
||||
}
|
||||
|
||||
// Prüfen ob Ersteller lokal online ist
|
||||
Player creator = Bukkit.getPlayer(ticket.getCreatorUUID());
|
||||
if (creator != null && creator.isOnline()) {
|
||||
// ─ Lokal online: direkt zustellen ────────────────────────────
|
||||
creator.sendMessage(msg);
|
||||
if (!comment.isEmpty())
|
||||
creator.sendMessage(plugin.color("&7Kommentar des Supports: &f" + comment));
|
||||
if (plugin.getConfig().getBoolean("rating-enabled", true)) {
|
||||
creator.sendMessage(plugin.color("&8&m "));
|
||||
creator.sendMessage(plugin.color("&6Wie zufrieden bist du mit dem Support?"));
|
||||
creator.sendMessage(plugin.color("&a/ticket rate " + ticket.getId() + " good &7– 👍 Gut"));
|
||||
creator.sendMessage(plugin.color("&c/ticket rate " + ticket.getId() + " bad &7– 👎 Schlecht"));
|
||||
creator.sendMessage(plugin.color("&8&m "));
|
||||
}
|
||||
if (ratingMsg != null) creator.sendMessage(ratingMsg);
|
||||
|
||||
} else if (plugin.isBungeeCordEnabled()) {
|
||||
// ─ BungeeCord: via Plugin-Messaging auf anderen Servern zustellen ─
|
||||
// KEIN savePendingClosedNotification hier! Das würde bei Server-Wechsel
|
||||
// als "Offline-Nachricht" doppelt angezeigt werden.
|
||||
// BungeeCord's "Message"-Kanal erreicht den Spieler netzwerkweit sofern er online ist.
|
||||
// Ist er wirklich offline, sieht er beim nächsten Login via PlayerJoinListener
|
||||
// eine frische Benachrichtigung (close_notified=true verhindert Duplikate).
|
||||
plugin.getBungeeMessenger().sendMessageToPlayer(
|
||||
ticket.getCreatorUUID(), ticket.getCreatorName(), msg);
|
||||
if (!comment.isEmpty())
|
||||
plugin.getBungeeMessenger().sendMessageToPlayer(
|
||||
ticket.getCreatorUUID(), ticket.getCreatorName(),
|
||||
plugin.color("&7Kommentar des Supports: &f" + comment));
|
||||
if (ratingMsg != null)
|
||||
plugin.getBungeeMessenger().sendMessageToPlayer(
|
||||
ticket.getCreatorUUID(), ticket.getCreatorName(), ratingMsg);
|
||||
|
||||
} else {
|
||||
// Offline → ausstehende Benachrichtigung speichern
|
||||
// ─ Standalone, Spieler offline: in Pending-DB speichern ──────
|
||||
savePendingClosedNotification(ticket, comment);
|
||||
}
|
||||
|
||||
String closer = closerName != null ? closerName : "Unbekannt";
|
||||
plugin.getDiscordWebhook().sendTicketClosed(ticket, closer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bug-Fix: Nutzt jetzt close_notified aus der DB statt ein In-Memory-Set.
|
||||
* Funktioniert damit auch nach Server-Wechseln in BungeeCord-Netzwerken korrekt.
|
||||
*
|
||||
* @deprecated Bitte stattdessen ticket.isCloseNotified() direkt prüfen,
|
||||
* da das Ticket-Objekt aus der DB bereits den korrekten Wert hat.
|
||||
*/
|
||||
public boolean wasClosedNotificationSent(int ticketId) {
|
||||
// Direkt in der DB nachschlagen – kein In-Memory-Set, kein Server-gebundener State
|
||||
Ticket t = plugin.getDatabaseManager().getTicketById(ticketId);
|
||||
return t != null && t.isCloseNotified();
|
||||
}
|
||||
|
||||
// ─────────────────────────── BungeeCord Hilfsmethoden ──────────────────
|
||||
|
||||
// ── BUG FIX #2 ──────────────────────────────────────────────────────────
|
||||
// Vorher: addPendingNotification() wurde IMMER asynchron ausgeführt –
|
||||
// auch wenn der Spieler lokal online war oder BungeeCord die
|
||||
// Nachricht bereits zugestellt hat. Das führte dazu, dass Spieler
|
||||
// beim nächsten Login immer noch eine "verpasste Nachricht" sahen,
|
||||
// obwohl sie die Nachricht bereits erhalten hatten.
|
||||
//
|
||||
// Fix: addPendingNotification() wird nur noch aufgerufen wenn:
|
||||
// 1. Der Spieler NICHT lokal online ist, UND
|
||||
// 2. BungeeCord NICHT aktiviert ist (Standalone-Fallback).
|
||||
// Im BungeeCord-Modus ist der BungeeCord-"Message"-Kanal für die
|
||||
// Zustellung zuständig. Offline-Spieler werden über close_notified
|
||||
// und den PlayerJoinListener beim nächsten Login benachrichtigt.
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Zustellung einer Nachricht an einen Spieler.
|
||||
*
|
||||
* Ablauf:
|
||||
* 1. Spieler lokal online → direkt
|
||||
* 2. BungeeCord aktiv → via Plugin-Messaging (kein Pending-Eintrag)
|
||||
* 3. Offline + Standalone → Pending-DB (Zustellung beim nächsten Login)
|
||||
*
|
||||
* @param uuid UUID des Empfängers
|
||||
* @param name Spielername (für BungeeCord-Lookup)
|
||||
* @param message Bereits color-übersetzter Text
|
||||
*/
|
||||
private void deliverToPlayer(UUID uuid, String name, String message) {
|
||||
Player local = Bukkit.getPlayer(uuid);
|
||||
if (local != null && local.isOnline()) {
|
||||
// Lokal online → direkt zustellen, fertig
|
||||
local.sendMessage(message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (plugin.isBungeeCordEnabled()) {
|
||||
// BungeeCord-Modus: Nachricht über Plugin-Messaging weiterleiten.
|
||||
// KEIN Pending-Eintrag! BungeeCord übernimmt die Zustellung.
|
||||
// Ist der Spieler wirklich offline, kümmert sich der PlayerJoinListener
|
||||
// beim nächsten Login um die Benachrichtigung.
|
||||
plugin.getBungeeMessenger().sendMessageToPlayer(uuid, name, message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Standalone-Modus, Spieler offline → in Pending-DB speichern
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () ->
|
||||
plugin.getDatabaseManager().addPendingNotification(uuid, message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichert eine ausstehende Schließ-Benachrichtigung in der DB.
|
||||
*/
|
||||
private void savePendingClosedNotification(Ticket ticket, String comment) {
|
||||
String pendingMsg = "&e[Ticket #" + ticket.getId() + "] &7Dein Ticket wurde geschlossen."
|
||||
+ (comment.isEmpty() ? "" : " &7Kommentar: &f" + comment)
|
||||
+ (plugin.getConfig().getBoolean("rating-enabled", true)
|
||||
@@ -207,16 +327,17 @@ public class TicketManager {
|
||||
plugin.getDatabaseManager().addPendingNotification(ticket.getCreatorUUID(), pendingMsg));
|
||||
}
|
||||
|
||||
String closer = closerName != null ? closerName : "Unbekannt";
|
||||
plugin.getDiscordWebhook().sendTicketClosed(ticket, closer);
|
||||
}
|
||||
|
||||
public boolean wasClosedNotificationSent(int ticketId) {
|
||||
return notifiedClosedTickets.contains(ticketId);
|
||||
}
|
||||
|
||||
// ─────────────────────────── Hilfsmethoden ─────────────────────────────
|
||||
|
||||
private String resolveClaimerName(Ticket ticket) {
|
||||
if (ticket.getClaimerName() != null) return ticket.getClaimerName();
|
||||
if (ticket.getClaimerUUID() != null) {
|
||||
String name = Bukkit.getOfflinePlayer(ticket.getClaimerUUID()).getName();
|
||||
if (name != null) return name;
|
||||
}
|
||||
return "Support";
|
||||
}
|
||||
|
||||
public boolean hasReachedTicketLimit(UUID uuid) {
|
||||
int max = plugin.getConfig().getInt("max-open-tickets-per-player", 2);
|
||||
if (max <= 0) return false;
|
||||
@@ -245,5 +366,11 @@ public class TicketManager {
|
||||
player.sendMessage(plugin.color("&e/ticket stats &7– Statistiken anzeigen"));
|
||||
}
|
||||
player.sendMessage(plugin.color("&8&m "));
|
||||
|
||||
// BungeeCord-Status anzeigen
|
||||
if (player.hasPermission("ticket.admin") && plugin.isBungeeCordEnabled()) {
|
||||
player.sendMessage(plugin.color("&8[BungeeCord] &7Server: &b" + plugin.getServerName()
|
||||
+ " &8| Cross-Server-Benachrichtigungen &aaktiv"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,13 @@ public class Ticket implements ConfigurationSerializable {
|
||||
private double x, y, z;
|
||||
private float yaw, pitch;
|
||||
|
||||
/**
|
||||
* Name des Servers auf dem das Ticket erstellt wurde (BungeeCord-Netzwerk).
|
||||
* Entspricht dem Wert aus config.yml → server-name.
|
||||
* Standardwert: "unknown"
|
||||
*/
|
||||
private String serverName = "unknown";
|
||||
|
||||
private TicketStatus status;
|
||||
private UUID claimerUUID;
|
||||
private String claimerName;
|
||||
@@ -46,6 +53,12 @@ public class Ticket implements ConfigurationSerializable {
|
||||
private String playerRating = null;
|
||||
private boolean claimerNotified = false;
|
||||
|
||||
/**
|
||||
* Gibt an ob der Ersteller bereits über die Schließung informiert wurde.
|
||||
* Wird in der DB gespeichert damit Server-Wechsel keine Duplikate erzeugen.
|
||||
*/
|
||||
private boolean closeNotified = false;
|
||||
|
||||
public Ticket() {}
|
||||
|
||||
public Ticket(UUID creatorUUID, String creatorName, String message, Location location) {
|
||||
@@ -88,6 +101,9 @@ public class Ticket implements ConfigurationSerializable {
|
||||
if (map.containsKey("priority")) this.priority = TicketPriority.fromString((String) map.get("priority"));
|
||||
if (map.containsKey("playerRating")) this.playerRating = (String) map.get("playerRating");
|
||||
if (map.containsKey("claimerNotified")) this.claimerNotified = (boolean) map.get("claimerNotified");
|
||||
// BungeeCord: Server-Name laden (Fallback: "unknown")
|
||||
if (map.containsKey("serverName")) this.serverName = (String) map.get("serverName");
|
||||
if (map.containsKey("closeNotified")) this.closeNotified = (boolean) map.get("closeNotified");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -112,6 +128,9 @@ public class Ticket implements ConfigurationSerializable {
|
||||
map.put("priority", priority.name());
|
||||
if (playerRating != null) map.put("playerRating", playerRating);
|
||||
map.put("claimerNotified", claimerNotified);
|
||||
// BungeeCord: Server-Name speichern
|
||||
map.put("serverName", serverName);
|
||||
map.put("closeNotified", closeNotified);
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -126,6 +145,8 @@ public class Ticket implements ConfigurationSerializable {
|
||||
private static float toFloat(Object o) { return o instanceof Float f ? f : ((Number) o).floatValue(); }
|
||||
private static long toLong(Object o) { return ((Number) o).longValue(); }
|
||||
|
||||
// ─────────────────────────── Getter & Setter ───────────────────────────
|
||||
|
||||
public int getId() { return id; }
|
||||
public void setId(int id) { this.id = id; }
|
||||
public UUID getCreatorUUID() { return creatorUUID; }
|
||||
@@ -175,4 +196,14 @@ public class Ticket implements ConfigurationSerializable {
|
||||
public boolean hasRating() { return playerRating != null; }
|
||||
public boolean isClaimerNotified() { return claimerNotified; }
|
||||
public void setClaimerNotified(boolean v) { this.claimerNotified = v; }
|
||||
|
||||
/** BungeeCord: Gibt den Server-Namen zurück, auf dem das Ticket erstellt wurde. */
|
||||
public String getServerName() { return serverName != null ? serverName : "unknown"; }
|
||||
/** BungeeCord: Setzt den Server-Namen (aus config.yml → server-name). */
|
||||
public void setServerName(String v) { this.serverName = v != null ? v : "unknown"; }
|
||||
|
||||
/** Gibt an ob der Ersteller bereits über die Schließung informiert wurde (DB-persistent). */
|
||||
public boolean isCloseNotified() { return closeNotified; }
|
||||
/** Setzt den close_notified-Flag (wird in DB gespeichert). */
|
||||
public void setCloseNotified(boolean v) { this.closeNotified = v; }
|
||||
}
|
||||
@@ -17,6 +17,26 @@ version: "2.0"
|
||||
# Debug-Modus (true = Logs in der Konsole)
|
||||
debug: false
|
||||
|
||||
# ----------------------------------------------------
|
||||
# BUNGEECORD (Cross-Server-Unterstützung)
|
||||
# ----------------------------------------------------
|
||||
# VORAUSSETZUNGEN:
|
||||
# 1. In spigot.yml auf JEDEM Server: bungeecord: true
|
||||
# 2. MySQL muss aktiviert sein (use-mysql: true)
|
||||
# 3. Plugin auf JEDEM Spigot-Server installieren
|
||||
# 4. Alle Server müssen dieselbe MySQL-Datenbank verwenden
|
||||
#
|
||||
# false = Normaler Single-Server-Modus (Standard)
|
||||
# true = Cross-Server Benachrichtigungen aktiv
|
||||
# ----------------------------------------------------
|
||||
bungeecord: false
|
||||
bungee-teleport-enabled: true
|
||||
|
||||
# Name dieses Servers im BungeeCord-Netzwerk.
|
||||
# Wird in Tickets, GUI und Discord-Embeds angezeigt.
|
||||
# Auf jedem Server ANDERS einstellen! (z.B. "survival", "creative", "skyblock")
|
||||
server-name: "survival"
|
||||
|
||||
# ----------------------------------------------------
|
||||
# SPEICHERPFAD & ARCHIV
|
||||
# ----------------------------------------------------
|
||||
@@ -156,6 +176,7 @@ discord:
|
||||
show-position: true # Welt & Koordinaten im Embed anzeigen
|
||||
show-category: true # Kategorie im Embed anzeigen
|
||||
show-priority: true # Priorität im Embed anzeigen
|
||||
show-server: true # BungeeCord: Server-Name im Embed anzeigen
|
||||
role-ping: false # Rollen-Ping bei neuem Ticket senden
|
||||
|
||||
# ── Ticket geschlossen ──────────────────────────────────────────────────
|
||||
@@ -166,6 +187,7 @@ discord:
|
||||
footer: "TicketSystem"
|
||||
show-category: true # Kategorie im Embed anzeigen
|
||||
show-priority: true # Priorität im Embed anzeigen
|
||||
show-server: true # BungeeCord: Server-Name im Embed anzeigen
|
||||
role-ping: false # Rollen-Ping beim Schließen senden
|
||||
|
||||
# ── Ticket weitergeleitet ───────────────────────────────────────────────
|
||||
@@ -176,6 +198,7 @@ discord:
|
||||
footer: "TicketSystem"
|
||||
show-category: true # Kategorie im Embed anzeigen
|
||||
show-priority: true # Priorität im Embed anzeigen
|
||||
show-server: true # BungeeCord: Server-Name im Embed anzeigen
|
||||
role-ping: false # Rollen-Ping beim Weiterleiten senden
|
||||
|
||||
# ----------------------------------------------------
|
||||
|
||||
@@ -5,6 +5,12 @@ api-version: 1.20
|
||||
author: M_Viper
|
||||
description: Ingame Support Ticket System with MySQL
|
||||
|
||||
# ── BungeeCord Plugin-Messaging-Kanäle ───────────────────────────────────────
|
||||
# PFLICHTFELD für Cross-Server-Benachrichtigungen!
|
||||
channels:
|
||||
- BungeeCord
|
||||
- ticketsystem:notify
|
||||
|
||||
commands:
|
||||
ticket:
|
||||
description: TicketSystem Hauptbefehl
|
||||
|
||||
Reference in New Issue
Block a user