Update from Git Manager GUI
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
package de.ticketsystem;
|
package de.ticketsystem;
|
||||||
|
|
||||||
|
import de.ticketsystem.bungee.BungeeMessenger;
|
||||||
import de.ticketsystem.commands.TicketCommand;
|
import de.ticketsystem.commands.TicketCommand;
|
||||||
import de.ticketsystem.database.DatabaseManager;
|
import de.ticketsystem.database.DatabaseManager;
|
||||||
import de.ticketsystem.discord.DiscordWebhook;
|
import de.ticketsystem.discord.DiscordWebhook;
|
||||||
@@ -18,11 +19,20 @@ public class TicketPlugin extends JavaPlugin {
|
|||||||
private static TicketPlugin instance;
|
private static TicketPlugin instance;
|
||||||
|
|
||||||
private boolean debug;
|
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 DatabaseManager databaseManager;
|
||||||
private TicketManager ticketManager;
|
private TicketManager ticketManager;
|
||||||
private CategoryManager categoryManager;
|
private CategoryManager categoryManager;
|
||||||
private TicketGUI ticketGUI;
|
private TicketGUI ticketGUI;
|
||||||
private DiscordWebhook discordWebhook;
|
private DiscordWebhook discordWebhook;
|
||||||
|
private BungeeMessenger bungeeMessenger;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
@@ -33,6 +43,33 @@ public class TicketPlugin extends JavaPlugin {
|
|||||||
// Ticket-Klasse für YAML-Serialisierung registrieren
|
// Ticket-Klasse für YAML-Serialisierung registrieren
|
||||||
Ticket.register();
|
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
|
// Update-Checker
|
||||||
int resourceId = 132757;
|
int resourceId = 132757;
|
||||||
new UpdateChecker(this, resourceId).getVersion(version -> {
|
new UpdateChecker(this, resourceId).getVersion(version -> {
|
||||||
@@ -108,6 +145,10 @@ public class TicketPlugin extends JavaPlugin {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDisable() {
|
public void onDisable() {
|
||||||
|
// Plugin-Messaging-Kanäle abmelden
|
||||||
|
getServer().getMessenger().unregisterOutgoingPluginChannel(this);
|
||||||
|
getServer().getMessenger().unregisterIncomingPluginChannel(this);
|
||||||
|
|
||||||
if (databaseManager != null) databaseManager.disconnect();
|
if (databaseManager != null) databaseManager.disconnect();
|
||||||
getLogger().info("TicketSystem wurde deaktiviert.");
|
getLogger().info("TicketSystem wurde deaktiviert.");
|
||||||
}
|
}
|
||||||
@@ -132,5 +173,18 @@ public class TicketPlugin extends JavaPlugin {
|
|||||||
public CategoryManager getCategoryManager() { return categoryManager; }
|
public CategoryManager getCategoryManager() { return categoryManager; }
|
||||||
public TicketGUI getTicketGUI() { return ticketGUI; }
|
public TicketGUI getTicketGUI() { return ticketGUI; }
|
||||||
public DiscordWebhook getDiscordWebhook() { return discordWebhook; }
|
public DiscordWebhook getDiscordWebhook() { return discordWebhook; }
|
||||||
|
public BungeeMessenger getBungeeMessenger() { return bungeeMessenger; }
|
||||||
public boolean isDebug() { return debug; }
|
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);
|
boolean prioritiesOn = plugin.getConfig().getBoolean("priorities-enabled", true);
|
||||||
|
|
||||||
if (args.length >= 3) {
|
if (args.length >= 3) {
|
||||||
// args[1]: erst als Kategorie prüfen, dann als Priorität
|
|
||||||
if (categoriesOn) {
|
if (categoriesOn) {
|
||||||
ConfigCategory parsedCat = cm.resolve(args[1]);
|
ConfigCategory parsedCat = cm.resolve(args[1]);
|
||||||
if (parsedCat != null) {
|
if (parsedCat != null) {
|
||||||
category = parsedCat;
|
category = parsedCat;
|
||||||
messageStartIndex = 2;
|
messageStartIndex = 2;
|
||||||
// args[2]: Priorität prüfen (nur wenn danach noch Text kommt)
|
|
||||||
if (prioritiesOn && args.length >= 4) {
|
if (prioritiesOn && args.length >= 4) {
|
||||||
TicketPriority parsedPrio = parsePriority(args[2]);
|
TicketPriority parsedPrio = parsePriority(args[2]);
|
||||||
if (parsedPrio != null) {
|
if (parsedPrio != null) {
|
||||||
@@ -115,7 +113,6 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Keine Kategorie erkannt → args[1] als Priorität prüfen
|
|
||||||
if (prioritiesOn) {
|
if (prioritiesOn) {
|
||||||
TicketPriority parsedPrio = parsePriority(args[1]);
|
TicketPriority parsedPrio = parsePriority(args[1]);
|
||||||
if (parsedPrio != null) {
|
if (parsedPrio != null) {
|
||||||
@@ -125,16 +122,12 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (prioritiesOn) {
|
} else if (prioritiesOn) {
|
||||||
// Kategorien aus → args[1] direkt als Priorität prüfen
|
|
||||||
TicketPriority parsedPrio = parsePriority(args[1]);
|
TicketPriority parsedPrio = parsePriority(args[1]);
|
||||||
if (parsedPrio != null) {
|
if (parsedPrio != null) {
|
||||||
priority = parsedPrio;
|
priority = parsedPrio;
|
||||||
messageStartIndex = 2;
|
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));
|
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 ticket = new Ticket(player.getUniqueId(), player.getName(), message, player.getLocation());
|
||||||
ticket.setCategoryKey(finalCategory.getKey());
|
ticket.setCategoryKey(finalCategory.getKey());
|
||||||
ticket.setPriority(finalPriority);
|
ticket.setPriority(finalPriority);
|
||||||
|
// BungeeCord: Server-Name des erstellenden Servers speichern
|
||||||
|
ticket.setServerName(plugin.getServerName());
|
||||||
|
|
||||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||||
int id = plugin.getDatabaseManager().createTicket(ticket);
|
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; }
|
if (!success) { player.sendMessage(plugin.formatMessage("messages.already-claimed")); return; }
|
||||||
Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId);
|
Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId);
|
||||||
if (ticket == null) return;
|
if (ticket == null) return;
|
||||||
|
|
||||||
player.sendMessage(plugin.formatMessage("messages.ticket-claimed")
|
player.sendMessage(plugin.formatMessage("messages.ticket-claimed")
|
||||||
.replace("{id}", String.valueOf(ticketId))
|
.replace("{id}", String.valueOf(ticketId))
|
||||||
.replace("{player}", ticket.getCreatorName()));
|
.replace("{player}", ticket.getCreatorName()));
|
||||||
plugin.getTicketManager().notifyCreatorClaimed(ticket);
|
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);
|
boolean success = plugin.getDatabaseManager().closeTicket(ticketId, comment);
|
||||||
if (success) {
|
if (success) {
|
||||||
Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId);
|
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, () -> {
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
player.sendMessage(plugin.formatMessage("messages.ticket-closed").replace("{id}", String.valueOf(ticketId)));
|
player.sendMessage(plugin.formatMessage("messages.ticket-closed").replace("{id}", String.valueOf(ticketId)));
|
||||||
if (ticket != null) {
|
if (ticket != null) {
|
||||||
@@ -252,12 +270,23 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
|
|||||||
try { id = Integer.parseInt(args[1]); }
|
try { id = Integer.parseInt(args[1]); }
|
||||||
catch (NumberFormatException e) { player.sendMessage(plugin.color("&cUngültige ID!")); return; }
|
catch (NumberFormatException e) { player.sendMessage(plugin.color("&cUngültige ID!")); return; }
|
||||||
|
|
||||||
Player target = Bukkit.getPlayer(args[2]);
|
// BungeeCord: Ziel-Spieler lokal suchen
|
||||||
if (target == null) { player.sendMessage(plugin.color("&cSpieler nicht gefunden!")); return; }
|
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 int ticketId = id;
|
||||||
final String fromName = player.getName();
|
final String fromName = player.getName();
|
||||||
final Player t = target;
|
final Player t = localTarget;
|
||||||
|
|
||||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||||
boolean success = plugin.getDatabaseManager().forwardTicket(ticketId, t.getUniqueId(), t.getName());
|
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")));
|
Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.formatMessage("messages.ticket-not-found")));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Spieler darf nur auf eigene Tickets kommentieren (Supporter/Admin: alle)
|
|
||||||
boolean isOwner = ticket.getCreatorUUID().equals(player.getUniqueId());
|
boolean isOwner = ticket.getCreatorUUID().equals(player.getUniqueId());
|
||||||
boolean isStaff = player.hasPermission("ticket.support") || player.hasPermission("ticket.admin");
|
boolean isStaff = player.hasPermission("ticket.support") || player.hasPermission("ticket.admin");
|
||||||
if (!isOwner && !isStaff) {
|
if (!isOwner && !isStaff) {
|
||||||
@@ -309,7 +337,6 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
|
|||||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
if (success) {
|
if (success) {
|
||||||
player.sendMessage(plugin.color("&aDein Kommentar zu Ticket &e#" + ticketId + " &awurde gespeichert."));
|
player.sendMessage(plugin.color("&aDein Kommentar zu Ticket &e#" + ticketId + " &awurde gespeichert."));
|
||||||
// Supporter/Admin und Ticket-Ersteller benachrichtigen
|
|
||||||
notifyCommentReceivers(player, ticket, message);
|
notifyCommentReceivers(player, ticket, message);
|
||||||
} else {
|
} else {
|
||||||
player.sendMessage(plugin.color("&cFehler beim Speichern des Kommentars."));
|
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) {
|
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 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;
|
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())) {
|
if (!ticket.getCreatorUUID().equals(author.getUniqueId())) {
|
||||||
Player creator = Bukkit.getPlayer(ticket.getCreatorUUID());
|
Player creator = Bukkit.getPlayer(ticket.getCreatorUUID());
|
||||||
if (creator != null && creator.isOnline()) {
|
if (creator != null && creator.isOnline()) {
|
||||||
creator.sendMessage(onlineMsg);
|
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 {
|
} else {
|
||||||
// Offline → für nächsten Login speichern
|
// Standalone: Offline → für nächsten Login speichern
|
||||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () ->
|
Bukkit.getScheduler().runTaskAsynchronously(plugin, () ->
|
||||||
plugin.getDatabaseManager().addPendingNotification(ticket.getCreatorUUID(), offlineMsg));
|
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")) {
|
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();
|
UUID claimerUUID = ticket.getClaimerUUID();
|
||||||
if (claimerUUID != null && !claimerUUID.equals(author.getUniqueId())) {
|
if (claimerUUID != null && !claimerUUID.equals(author.getUniqueId())) {
|
||||||
Player claimer = Bukkit.getPlayer(claimerUUID);
|
Player claimer = Bukkit.getPlayer(claimerUUID);
|
||||||
if (claimer != null && claimer.isOnline()) {
|
if (claimer != null && claimer.isOnline()) {
|
||||||
claimer.sendMessage(onlineMsg);
|
claimer.sendMessage(onlineMsg);
|
||||||
} else {
|
} 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, () ->
|
Bukkit.getScheduler().runTaskAsynchronously(plugin, () ->
|
||||||
plugin.getDatabaseManager().addPendingNotification(claimerUUID, claimerOffline));
|
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()) {
|
for (Player p : Bukkit.getOnlinePlayers()) {
|
||||||
if (p.getUniqueId().equals(author.getUniqueId())) continue;
|
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")) {
|
if (p.hasPermission("ticket.support") || p.hasPermission("ticket.admin")) {
|
||||||
p.sendMessage(onlineMsg);
|
p.sendMessage(onlineMsg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─────────────────────────── /ticket rate ──────────────────────────────
|
// ─────────────────────────── /ticket rate ──────────────────────────────
|
||||||
|
|
||||||
@@ -468,7 +531,6 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
|
|||||||
player.sendMessage(plugin.color("&7Keine gesperrten Spieler."));
|
player.sendMessage(plugin.color("&7Keine gesperrten Spieler."));
|
||||||
} else {
|
} else {
|
||||||
for (String[] entry : list) {
|
for (String[] entry : list) {
|
||||||
// {uuid, name, reason, bannedBy, bannedAt}
|
|
||||||
player.sendMessage(plugin.color("&e" + entry[1] + " &7– &f" + entry[2]
|
player.sendMessage(plugin.color("&e" + entry[1] + " &7– &f" + entry[2]
|
||||||
+ " &7(gesperrt von &e" + entry[3] + "&7)"));
|
+ " &7(gesperrt von &e" + entry[3] + "&7)"));
|
||||||
}
|
}
|
||||||
@@ -488,6 +550,9 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
|
|||||||
plugin.reloadConfig();
|
plugin.reloadConfig();
|
||||||
plugin.getCategoryManager().reload();
|
plugin.getCategoryManager().reload();
|
||||||
player.sendMessage(plugin.color("&aKonfiguration wurde neu geladen. &7(inkl. Kategorien)"));
|
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 ───────────────────────────
|
// ─────────────────────────── /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; }
|
if (!player.hasPermission("ticket.admin")) { player.sendMessage(plugin.formatMessage("messages.no-permission")); return; }
|
||||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||||
var stats = plugin.getDatabaseManager().getTicketStats();
|
var stats = plugin.getDatabaseManager().getTicketStats();
|
||||||
|
var staffRatings = plugin.getDatabaseManager().getStaffRatings();
|
||||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
player.sendMessage(plugin.color("&8&m "));
|
player.sendMessage(plugin.color("&8&m "));
|
||||||
player.sendMessage(plugin.color("&6Ticket Statistik"));
|
player.sendMessage(plugin.color("&6Ticket Statistik"));
|
||||||
player.sendMessage(plugin.color("&8&m "));
|
player.sendMessage(plugin.color("&8&m "));
|
||||||
player.sendMessage(plugin.color("&eGesamt: &a" + stats.total));
|
player.sendMessage(plugin.color("&eGesamt: &a" + stats.total));
|
||||||
player.sendMessage(plugin.color("&eOffen: &a" + stats.open));
|
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));
|
player.sendMessage(plugin.color("&eWeitergeleitet: &a" + stats.forwarded));
|
||||||
|
|
||||||
if (plugin.getConfig().getBoolean("rating-enabled", true)) {
|
if (plugin.getConfig().getBoolean("rating-enabled", true)) {
|
||||||
player.sendMessage(plugin.color("&8&m "));
|
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
|
player.sendMessage(plugin.color("&a👍 Positiv: &f" + stats.thumbsUp
|
||||||
+ " &c👎 Negativ: &f" + stats.thumbsDown));
|
+ " &c👎 Negativ: &f" + stats.thumbsDown));
|
||||||
int total = stats.thumbsUp + stats.thumbsDown;
|
int totalRated = stats.thumbsUp + stats.thumbsDown;
|
||||||
if (total > 0) {
|
if (totalRated > 0) {
|
||||||
int percent = (int) Math.round(stats.thumbsUp * 100.0 / total);
|
int percent = (int) Math.round(stats.thumbsUp * 100.0 / totalRated);
|
||||||
player.sendMessage(plugin.color("&7Zufriedenheit: &e" + percent + "%"));
|
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("&8&m "));
|
||||||
player.sendMessage(plugin.color("&6Top Ersteller:"));
|
player.sendMessage(plugin.color("&6Top Ersteller:"));
|
||||||
stats.byPlayer.entrySet().stream()
|
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.
|
/** Parst Benutzer-Eingaben zu TicketPriority. Gibt null zurück wenn keine Übereinstimmung. */
|
||||||
* Gibt null zurück wenn keine Übereinstimmung. */
|
|
||||||
private TicketPriority parsePriority(String input) {
|
private TicketPriority parsePriority(String input) {
|
||||||
if (input == null) return null;
|
if (input == null) return null;
|
||||||
return switch (input.toLowerCase()) {
|
return switch (input.toLowerCase()) {
|
||||||
@@ -661,14 +754,12 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
|
|||||||
&& plugin.getConfig().getBoolean("categories-enabled", true)) {
|
&& plugin.getConfig().getBoolean("categories-enabled", true)) {
|
||||||
for (ConfigCategory c : plugin.getCategoryManager().getAll())
|
for (ConfigCategory c : plugin.getCategoryManager().getAll())
|
||||||
if (c.getKey().startsWith(args[1].toLowerCase())) completions.add(c.getKey());
|
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))
|
if (plugin.getConfig().getBoolean("priorities-enabled", true))
|
||||||
for (String p : List.of("low", "normal", "high", "urgent"))
|
for (String p : List.of("low", "normal", "high", "urgent"))
|
||||||
if (p.startsWith(args[1].toLowerCase())) completions.add(p);
|
if (p.startsWith(args[1].toLowerCase())) completions.add(p);
|
||||||
|
|
||||||
} else if (args.length == 3 && args[0].equalsIgnoreCase("create")
|
} else if (args.length == 3 && args[0].equalsIgnoreCase("create")
|
||||||
&& plugin.getConfig().getBoolean("priorities-enabled", true)) {
|
&& plugin.getConfig().getBoolean("priorities-enabled", true)) {
|
||||||
// Priorität nach Kategorie
|
|
||||||
for (String p : List.of("low", "normal", "high", "urgent"))
|
for (String p : List.of("low", "normal", "high", "urgent"))
|
||||||
if (p.startsWith(args[2].toLowerCase())) completions.add(p);
|
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);
|
if (p.startsWith(args[2].toLowerCase())) completions.add(p);
|
||||||
|
|
||||||
} else if (args.length == 3 && args[0].equalsIgnoreCase("forward")) {
|
} else if (args.length == 3 && args[0].equalsIgnoreCase("forward")) {
|
||||||
|
// BungeeCord: Nur lokal online Spieler als Tab-Completion
|
||||||
for (Player p : Bukkit.getOnlinePlayers())
|
for (Player p : Bukkit.getOnlinePlayers())
|
||||||
if (p.getName().toLowerCase().startsWith(args[2].toLowerCase())) completions.add(p.getName());
|
if (p.getName().toLowerCase().startsWith(args[2].toLowerCase())) completions.add(p.getName());
|
||||||
|
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ public class DatabaseManager {
|
|||||||
|
|
||||||
private void createTables() {
|
private void createTables() {
|
||||||
// Haupt-Tickets-Tabelle
|
// Haupt-Tickets-Tabelle
|
||||||
|
// BungeeCord: server_name speichert auf welchem Server das Ticket erstellt wurde
|
||||||
String ticketsSql = """
|
String ticketsSql = """
|
||||||
CREATE TABLE IF NOT EXISTS tickets (
|
CREATE TABLE IF NOT EXISTS tickets (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
@@ -186,7 +187,9 @@ public class DatabaseManager {
|
|||||||
category VARCHAR(16) NOT NULL DEFAULT 'GENERAL',
|
category VARCHAR(16) NOT NULL DEFAULT 'GENERAL',
|
||||||
priority VARCHAR(10) NOT NULL DEFAULT 'NORMAL',
|
priority VARCHAR(10) NOT NULL DEFAULT 'NORMAL',
|
||||||
player_rating VARCHAR(16) NULL,
|
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;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
""";
|
""";
|
||||||
|
|
||||||
@@ -225,11 +228,50 @@ public class DatabaseManager {
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) 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()) {
|
try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) {
|
||||||
stmt.execute(ticketsSql);
|
stmt.execute(ticketsSql);
|
||||||
stmt.execute(commentsSql);
|
stmt.execute(commentsSql);
|
||||||
stmt.execute(blacklistSql);
|
stmt.execute(blacklistSql);
|
||||||
stmt.execute(notifSql);
|
stmt.execute(notifSql);
|
||||||
|
stmt.execute(statsSql);
|
||||||
|
stmt.execute(pendingTeleportSql);
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
plugin.getLogger().log(Level.SEVERE, "Fehler beim Erstellen der Tabellen: " + e.getMessage(), 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.
|
* Ergänzt fehlende Spalten in bestehenden Datenbanken automatisch.
|
||||||
|
* Wichtig für Upgrades von älteren Versionen.
|
||||||
*/
|
*/
|
||||||
private void ensureColumns() {
|
private void ensureColumns() {
|
||||||
ensureColumn("close_comment", "ALTER TABLE tickets ADD COLUMN close_comment VARCHAR(500) NULL");
|
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("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("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");
|
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) {
|
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 ──────────────────────────────
|
// ─────────────────────────── CRUD Tickets ──────────────────────────────
|
||||||
|
|
||||||
public int createTicket(Ticket ticket) {
|
public int createTicket(Ticket ticket) {
|
||||||
if (useMySQL) {
|
if (useMySQL) {
|
||||||
|
// BungeeCord: server_name wird ebenfalls gespeichert
|
||||||
String sql = """
|
String sql = """
|
||||||
INSERT INTO tickets (creator_uuid, creator_name, message, world, x, y, z, yaw, pitch, category, priority)
|
INSERT INTO tickets (creator_uuid, creator_name, message, world, x, y, z, yaw, pitch,
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
category, priority, server_name)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""";
|
""";
|
||||||
try (Connection conn = getConnection();
|
try (Connection conn = getConnection();
|
||||||
PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
|
PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
|
||||||
@@ -289,6 +362,7 @@ public class DatabaseManager {
|
|||||||
ps.setFloat(9, ticket.getPitch());
|
ps.setFloat(9, ticket.getPitch());
|
||||||
ps.setString(10, ticket.getCategoryKey());
|
ps.setString(10, ticket.getCategoryKey());
|
||||||
ps.setString(11, ticket.getPriority().name());
|
ps.setString(11, ticket.getPriority().name());
|
||||||
|
ps.setString(12, ticket.getServerName()); // BungeeCord
|
||||||
ps.executeUpdate();
|
ps.executeUpdate();
|
||||||
ResultSet rs = ps.getGeneratedKeys();
|
ResultSet rs = ps.getGeneratedKeys();
|
||||||
if (rs.next()) return rs.getInt(1);
|
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) {
|
public boolean claimTicket(int ticketId, UUID claimerUUID, String claimerName) {
|
||||||
if (useMySQL) {
|
if (useMySQL) {
|
||||||
String sql = """
|
String sql = """
|
||||||
UPDATE tickets
|
UPDATE tickets
|
||||||
SET status = 'CLAIMED', claimer_uuid = ?, claimer_name = ?,
|
SET status = 'CLAIMED', claimer_uuid = ?, claimer_name = ?,
|
||||||
claimed_at = NOW(), player_deleted = FALSE
|
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)) {
|
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||||||
ps.setString(1, claimerUUID.toString());
|
ps.setString(1, claimerUUID.toString());
|
||||||
@@ -325,7 +407,7 @@ public class DatabaseManager {
|
|||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
Ticket t = getTicketById(ticketId);
|
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.setStatus(TicketStatus.CLAIMED);
|
||||||
t.setClaimerUUID(claimerUUID);
|
t.setClaimerUUID(claimerUUID);
|
||||||
t.setClaimerName(claimerName);
|
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) {
|
public void markClaimerNotified(int ticketId) {
|
||||||
if (useMySQL) {
|
if (useMySQL) {
|
||||||
String sql = "UPDATE tickets SET claimer_notified = TRUE WHERE id = ?";
|
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.
|
* Setzt close_notified = TRUE für ein Ticket (persistiert in DB/Datei).
|
||||||
* @param ticketId ID des Tickets
|
* Verhindert Duplikat-Benachrichtigungen und doppelte Discord-Nachrichten
|
||||||
* @param rating "THUMBS_UP" oder "THUMBS_DOWN"
|
* bei Server-Wechseln in BungeeCord-Netzwerken.
|
||||||
* @return true bei Erfolg
|
|
||||||
*/
|
*/
|
||||||
|
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) {
|
public boolean rateTicket(int ticketId, String rating) {
|
||||||
if (useMySQL) {
|
if (useMySQL) {
|
||||||
String sql = "UPDATE tickets SET player_rating = ? WHERE id = ? AND status = 'CLOSED' AND player_rating IS NULL";
|
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)) {
|
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||||||
ps.setString(1, rating);
|
ps.setString(1, rating);
|
||||||
ps.setInt(2, ticketId);
|
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) {
|
} catch (SQLException e) {
|
||||||
plugin.getLogger().log(Level.SEVERE, "Fehler bei rateTicket: " + e.getMessage(), 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) {
|
public boolean addComment(TicketComment comment) {
|
||||||
if (useMySQL) {
|
if (useMySQL) {
|
||||||
String sql = """
|
String sql = """
|
||||||
@@ -527,7 +807,6 @@ public class DatabaseManager {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
// YAML: comments.<ticketId>.<index>
|
|
||||||
int index = dataConfig.getInt("comments." + comment.getTicketId() + ".count", 0);
|
int index = dataConfig.getInt("comments." + comment.getTicketId() + ".count", 0);
|
||||||
String base = "comments." + comment.getTicketId() + "." + index + ".";
|
String base = "comments." + comment.getTicketId() + "." + index + ".";
|
||||||
dataConfig.set(base + "authorUUID", comment.getAuthorUUID().toString());
|
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) {
|
public List<TicketComment> getComments(int ticketId) {
|
||||||
List<TicketComment> list = new ArrayList<>();
|
List<TicketComment> list = new ArrayList<>();
|
||||||
if (useMySQL) {
|
if (useMySQL) {
|
||||||
@@ -582,12 +858,8 @@ public class DatabaseManager {
|
|||||||
return list;
|
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) {
|
public void addPendingNotification(UUID playerUUID, String rawMessage) {
|
||||||
if (useMySQL) {
|
if (useMySQL) {
|
||||||
String sql = "INSERT INTO ticket_pending_notifications (player_uuid, message) VALUES (?, ?)";
|
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) {
|
public List<String> getPendingNotifications(UUID playerUUID) {
|
||||||
List<String> messages = new ArrayList<>();
|
List<String> messages = new ArrayList<>();
|
||||||
if (useMySQL) {
|
if (useMySQL) {
|
||||||
@@ -627,9 +896,6 @@ public class DatabaseManager {
|
|||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Löscht alle ausstehenden Benachrichtigungen eines Spielers nach dem Anzeigen.
|
|
||||||
*/
|
|
||||||
public void clearPendingNotifications(UUID playerUUID) {
|
public void clearPendingNotifications(UUID playerUUID) {
|
||||||
if (useMySQL) {
|
if (useMySQL) {
|
||||||
String sql = "DELETE FROM ticket_pending_notifications WHERE player_uuid = ?";
|
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) {
|
public boolean isBlacklisted(UUID uuid) {
|
||||||
if (useMySQL) {
|
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() {
|
public List<String[]> getBlacklist() {
|
||||||
List<String[]> list = new ArrayList<>();
|
List<String[]> list = new ArrayList<>();
|
||||||
if (useMySQL) {
|
if (useMySQL) {
|
||||||
@@ -890,28 +1155,69 @@ public class DatabaseManager {
|
|||||||
// ─────────────────────────── Statistiken ───────────────────────────────
|
// ─────────────────────────── Statistiken ───────────────────────────────
|
||||||
|
|
||||||
public TicketStats getTicketStats() {
|
public TicketStats getTicketStats() {
|
||||||
|
// Aktuelle Live-Daten aus der tickets-Tabelle
|
||||||
List<Ticket> all = getAllTickets();
|
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> byPlayer = new java.util.HashMap<>();
|
||||||
|
java.util.Map<String, Integer> byServer = new java.util.HashMap<>();
|
||||||
for (Ticket t : all) {
|
for (Ticket t : all) {
|
||||||
switch (t.getStatus()) {
|
switch (t.getStatus()) {
|
||||||
case OPEN -> open++;
|
case OPEN -> open++;
|
||||||
case CLAIMED -> claimed++;
|
case CLAIMED -> claimed++;
|
||||||
case FORWARDED -> forwarded++;
|
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_UP".equals(t.getPlayerRating())) thumbsUp++;
|
||||||
if ("THUMBS_DOWN".equals(t.getPlayerRating())) thumbsDown++;
|
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 static class TicketStats {
|
||||||
public final int total, open, closed, forwarded, thumbsUp, thumbsDown;
|
public final int total, open, closed, forwarded, thumbsUp, thumbsDown;
|
||||||
public final java.util.Map<String, Integer> byPlayer;
|
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,
|
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.total = total;
|
||||||
this.open = open;
|
this.open = open;
|
||||||
this.closed = closed;
|
this.closed = closed;
|
||||||
@@ -919,6 +1225,7 @@ public class DatabaseManager {
|
|||||||
this.thumbsUp = thumbsUp;
|
this.thumbsUp = thumbsUp;
|
||||||
this.thumbsDown = thumbsDown;
|
this.thumbsDown = thumbsDown;
|
||||||
this.byPlayer = byPlayer;
|
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.setPriority(TicketPriority.fromString(rs.getString("priority"))); } catch (SQLException ignored) {}
|
||||||
try { t.setPlayerRating(rs.getString("player_rating")); } catch (SQLException ignored) {}
|
try { t.setPlayerRating(rs.getString("player_rating")); } catch (SQLException ignored) {}
|
||||||
try { t.setClaimerNotified(rs.getBoolean("claimer_notified")); } 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;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1032,6 +1343,9 @@ public class DatabaseManager {
|
|||||||
obj.put("priority", t.getPriority().name());
|
obj.put("priority", t.getPriority().name());
|
||||||
if (t.getPlayerRating() != null) obj.put("playerRating", t.getPlayerRating());
|
if (t.getPlayerRating() != null) obj.put("playerRating", t.getPlayerRating());
|
||||||
obj.put("claimerNotified", t.isClaimerNotified());
|
obj.put("claimerNotified", t.isClaimerNotified());
|
||||||
|
obj.put("closeNotified", t.isCloseNotified());
|
||||||
|
// BungeeCord: Server-Name im JSON-Export
|
||||||
|
obj.put("serverName", t.getServerName());
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1060,6 +1374,9 @@ public class DatabaseManager {
|
|||||||
if (obj.containsKey("priority")) t.setPriority(TicketPriority.fromString((String) obj.get("priority")));
|
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("playerRating")) t.setPlayerRating((String) obj.get("playerRating"));
|
||||||
if (obj.containsKey("claimerNotified"))t.setClaimerNotified((Boolean) obj.get("claimerNotified"));
|
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;
|
return t;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (plugin != null) plugin.getLogger().severe("Fehler beim Parsen eines Tickets: " + e.getMessage());
|
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.TicketPlugin;
|
||||||
import de.ticketsystem.model.ConfigCategory;
|
import de.ticketsystem.model.ConfigCategory;
|
||||||
import de.ticketsystem.model.Ticket;
|
import de.ticketsystem.model.Ticket;
|
||||||
|
import de.ticketsystem.model.TicketPriority;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
|
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
@@ -10,96 +11,94 @@ import java.net.HttpURLConnection;
|
|||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.Instant;
|
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 {
|
public class DiscordWebhook {
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// Konstanten & Felder
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static final String AVATAR_URL = "https://mc-heads.net/avatar/%s/64";
|
||||||
|
|
||||||
private final TicketPlugin plugin;
|
private final TicketPlugin plugin;
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// Konstruktor
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
public DiscordWebhook(TicketPlugin plugin) {
|
public DiscordWebhook(TicketPlugin plugin) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────── Öffentliche Methoden ──────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// Öffentliche Methoden – Webhook-Events
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
|
||||||
* Sendet eine Benachrichtigung wenn ein neues Ticket erstellt wurde.
|
|
||||||
*/
|
|
||||||
public void sendNewTicket(Ticket ticket) {
|
public void sendNewTicket(Ticket ticket) {
|
||||||
if (!isEnabled()) return;
|
if (!isEnabled()) return;
|
||||||
|
|
||||||
String webhookUrl = getWebhookUrl();
|
String webhookUrl = getWebhookUrl();
|
||||||
if (webhookUrl == null) return;
|
if (webhookUrl == null) return;
|
||||||
|
|
||||||
String title = plugin.getConfig().getString ("discord.messages.new-ticket.title", "🎫 Neues Ticket erstellt");
|
// Konfiguration lesen
|
||||||
String color = plugin.getConfig().getString ("discord.messages.new-ticket.color", "3066993");
|
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");
|
String footer = plugin.getConfig().getString ("discord.messages.new-ticket.footer", "TicketSystem");
|
||||||
boolean showPos = plugin.getConfig().getBoolean("discord.messages.new-ticket.show-position", true);
|
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 showCat = plugin.getConfig().getBoolean("discord.messages.new-ticket.show-category", true);
|
||||||
boolean showPri = plugin.getConfig().getBoolean("discord.messages.new-ticket.show-priority", 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);
|
boolean ping = plugin.getConfig().getBoolean("discord.messages.new-ticket.role-ping", true);
|
||||||
|
|
||||||
StringBuilder fields = new StringBuilder();
|
// Hilfs-Werte berechnen
|
||||||
fields.append(field("Spieler", ticket.getCreatorName(), true));
|
String prioEmoji = getPriorityEmoji(ticket.getPriority());
|
||||||
fields.append(",").append(field("Ticket ID", "#" + ticket.getId(), true));
|
String avatarUrl = String.format(AVATAR_URL, ticket.getCreatorUUID().toString());
|
||||||
fields.append(",").append(field("Anliegen", ticket.getMessage(), false));
|
|
||||||
|
// 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)) {
|
if (showCat && plugin.getConfig().getBoolean("categories-enabled", true)) {
|
||||||
ConfigCategory cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey());
|
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)) {
|
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) {
|
if (showPos) {
|
||||||
fields.append(",").append(field("Welt", ticket.getWorldName(), true));
|
fields.add(new Field("🌍 Welt", ticket.getWorldName(), true));
|
||||||
fields.append(",").append(field("Position",
|
fields.add(new Field("📍 Position",
|
||||||
String.format("%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ()), true));
|
String.format("%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ()), true));
|
||||||
}
|
}
|
||||||
|
|
||||||
String content = ping ? buildRolePing() : "";
|
// JSON zusammenbauen & senden
|
||||||
String json = buildPayload(content, title, Integer.parseInt(color), fields.toString(), footer);
|
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);
|
sendAsync(webhookUrl, json);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
* Sendet eine Benachrichtigung wenn ein Ticket geschlossen wurde.
|
|
||||||
*/
|
|
||||||
public void sendTicketClosed(Ticket ticket, String closerName) {
|
public void sendTicketClosed(Ticket ticket, String closerName) {
|
||||||
if (!isEnabled()) return;
|
if (!isEnabled()) return;
|
||||||
if (!plugin.getConfig().getBoolean("discord.messages.ticket-closed.enabled", false)) return;
|
if (!plugin.getConfig().getBoolean("discord.messages.ticket-closed.enabled", false)) return;
|
||||||
@@ -107,37 +106,68 @@ public class DiscordWebhook {
|
|||||||
String webhookUrl = getWebhookUrl();
|
String webhookUrl = getWebhookUrl();
|
||||||
if (webhookUrl == null) return;
|
if (webhookUrl == null) return;
|
||||||
|
|
||||||
String title = plugin.getConfig().getString ("discord.messages.ticket-closed.title", "🔒 Ticket geschlossen");
|
// Konfiguration lesen
|
||||||
String color = plugin.getConfig().getString ("discord.messages.ticket-closed.color", "15158332");
|
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");
|
String footer = plugin.getConfig().getString ("discord.messages.ticket-closed.footer", "TicketSystem");
|
||||||
boolean showCat = plugin.getConfig().getBoolean("discord.messages.ticket-closed.show-category", true);
|
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 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);
|
boolean ping = plugin.getConfig().getBoolean("discord.messages.ticket-closed.role-ping", false);
|
||||||
|
|
||||||
StringBuilder fields = new StringBuilder();
|
// Hilfs-Werte berechnen
|
||||||
fields.append(field("Ticket ID", "#" + ticket.getId(), true));
|
String prioEmoji = getPriorityEmoji(ticket.getPriority());
|
||||||
fields.append(",").append(field("Ersteller", ticket.getCreatorName(), true));
|
String avatarUrl = String.format(AVATAR_URL, ticket.getCreatorUUID().toString());
|
||||||
fields.append(",").append(field("Geschlossen von", closerName, true));
|
|
||||||
|
// 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)) {
|
if (showCat && plugin.getConfig().getBoolean("categories-enabled", true)) {
|
||||||
ConfigCategory cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey());
|
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));
|
|
||||||
}
|
|
||||||
if (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) {
|
|
||||||
fields.append(",").append(field("Kommentar", ticket.getCloseComment(), false));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String content = ping ? buildRolePing() : "";
|
if (showPri && plugin.getConfig().getBoolean("priorities-enabled", true)) {
|
||||||
String json = buildPayload(content, title, Integer.parseInt(color), fields.toString(), footer);
|
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);
|
sendAsync(webhookUrl, json);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
* Sendet eine Benachrichtigung wenn ein Ticket weitergeleitet wurde.
|
|
||||||
*/
|
|
||||||
public void sendTicketForwarded(Ticket ticket, String fromName) {
|
public void sendTicketForwarded(Ticket ticket, String fromName) {
|
||||||
if (!isEnabled()) return;
|
if (!isEnabled()) return;
|
||||||
if (!plugin.getConfig().getBoolean("discord.messages.ticket-forwarded.enabled", false)) return;
|
if (!plugin.getConfig().getBoolean("discord.messages.ticket-forwarded.enabled", false)) return;
|
||||||
@@ -145,98 +175,180 @@ public class DiscordWebhook {
|
|||||||
String webhookUrl = getWebhookUrl();
|
String webhookUrl = getWebhookUrl();
|
||||||
if (webhookUrl == null) return;
|
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 color = plugin.getConfig().getString ("discord.messages.ticket-forwarded.color", "15105570");
|
||||||
String footer = plugin.getConfig().getString ("discord.messages.ticket-forwarded.footer", "TicketSystem");
|
String footer = plugin.getConfig().getString ("discord.messages.ticket-forwarded.footer", "TicketSystem");
|
||||||
boolean showCat = plugin.getConfig().getBoolean("discord.messages.ticket-forwarded.show-category", true);
|
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 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);
|
boolean ping = plugin.getConfig().getBoolean("discord.messages.ticket-forwarded.role-ping", false);
|
||||||
|
|
||||||
StringBuilder fields = new StringBuilder();
|
// Hilfs-Werte berechnen
|
||||||
fields.append(field("Ticket ID", "#" + ticket.getId(), true));
|
String prioEmoji = getPriorityEmoji(ticket.getPriority());
|
||||||
fields.append(",").append(field("Ersteller", ticket.getCreatorName(), true));
|
String avatarUrl = String.format(AVATAR_URL, ticket.getCreatorUUID().toString());
|
||||||
fields.append(",").append(field("Weitergeleitet von", fromName, true));
|
|
||||||
fields.append(",").append(field("Weitergeleitet an", ticket.getForwardedToName(), true));
|
// 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)) {
|
if (showCat && plugin.getConfig().getBoolean("categories-enabled", true)) {
|
||||||
ConfigCategory cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey());
|
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String content = ping ? buildRolePing() : "";
|
if (showPri && plugin.getConfig().getBoolean("priorities-enabled", true)) {
|
||||||
String json = buildPayload(content, title, Integer.parseInt(color), fields.toString(), footer);
|
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);
|
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() {
|
private boolean isEnabled() {
|
||||||
return plugin.getConfig().getBoolean("discord.enabled", false);
|
return plugin.getConfig().getBoolean("discord.enabled", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Gibt die Webhook-URL zurück oder null wenn nicht gesetzt. */
|
|
||||||
private String getWebhookUrl() {
|
private String getWebhookUrl() {
|
||||||
String url = plugin.getConfig().getString("discord.webhook-url", "");
|
String url = plugin.getConfig().getString("discord.webhook-url", "");
|
||||||
return url.isEmpty() ? null : url;
|
return url.isEmpty() ? null : url;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Baut den @Rollen-Ping-String aus der konfigurierten Rollen-ID.
|
|
||||||
* Leer wenn keine ID gesetzt.
|
|
||||||
*/
|
|
||||||
private String buildRolePing() {
|
private String buildRolePing() {
|
||||||
String roleId = plugin.getConfig().getString("discord.role-ping-id", "").trim();
|
String roleId = plugin.getConfig().getString("discord.role-ping-id", "").trim();
|
||||||
if (roleId.isEmpty()) return "";
|
return roleId.isEmpty() ? "" : "<@&" + roleId + ">";
|
||||||
return "<@&" + roleId + ">";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private String getPriorityEmoji(TicketPriority priority) {
|
||||||
* Baut einen einzelnen Embed-Field als JSON-String.
|
return switch (priority) {
|
||||||
*/
|
case LOW -> "🟢";
|
||||||
private String field(String name, String value, boolean inline) {
|
case NORMAL -> "🟡";
|
||||||
String safeValue = value != null
|
case HIGH -> "🟠";
|
||||||
? value.replace("\\", "\\\\").replace("\"", "\\\"")
|
case URGENT -> "🔴";
|
||||||
: "–";
|
};
|
||||||
String safeName = name.replace("\\", "\\\\").replace("\"", "\\\"");
|
|
||||||
return String.format("{\"name\":\"%s\",\"value\":\"%s\",\"inline\":%b}",
|
|
||||||
safeName, safeValue, inline);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
private void sendAsync(String webhookUrl, String json) {
|
||||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||||
try {
|
try {
|
||||||
@@ -254,8 +366,12 @@ public class DiscordWebhook {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int responseCode = conn.getResponseCode();
|
int responseCode = conn.getResponseCode();
|
||||||
|
|
||||||
if (plugin.isDebug()) {
|
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) {
|
if (responseCode != 200 && responseCode != 204) {
|
||||||
@@ -263,6 +379,7 @@ public class DiscordWebhook {
|
|||||||
}
|
}
|
||||||
|
|
||||||
conn.disconnect();
|
conn.disconnect();
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
plugin.getLogger().warning("[DiscordWebhook] Fehler beim Senden: " + e.getMessage());
|
plugin.getLogger().warning("[DiscordWebhook] Fehler beim Senden: " + e.getMessage());
|
||||||
if (plugin.isDebug()) e.printStackTrace();
|
if (plugin.isDebug()) e.printStackTrace();
|
||||||
|
|||||||
@@ -196,9 +196,32 @@ public class TicketGUI implements Listener {
|
|||||||
// Slot 4: Ticket-Info
|
// Slot 4: Ticket-Info
|
||||||
inv.setItem(4, buildDetailInfoItem(ticket));
|
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",
|
inv.setItem(10, buildActionItem(Material.ENDER_PEARL, "§b§lTeleportieren",
|
||||||
List.of("§7Teleportiert dich zur", "§7Position des Tickets.")));
|
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
|
// Slot 12: Claimen / Löschen / Grau
|
||||||
if (ticket.getStatus() == TicketStatus.OPEN) {
|
if (ticket.getStatus() == TicketStatus.OPEN) {
|
||||||
@@ -299,7 +322,6 @@ public class TicketGUI implements Listener {
|
|||||||
|
|
||||||
// ── Spieler-GUI ────────────────────────────────────────────────────
|
// ── Spieler-GUI ────────────────────────────────────────────────────
|
||||||
if (title.equals(PLAYER_GUI_TITLE)) {
|
if (title.equals(PLAYER_GUI_TITLE)) {
|
||||||
// Navigationstasten
|
|
||||||
int curPage = playerPage.getOrDefault(player.getUniqueId(), 0);
|
int curPage = playerPage.getOrDefault(player.getUniqueId(), 0);
|
||||||
if (slot == 45) { openPlayerGUI(player, curPage - 1); return; }
|
if (slot == 45) { openPlayerGUI(player, curPage - 1); return; }
|
||||||
if (slot == 53) { openPlayerGUI(player, curPage + 1); return; }
|
if (slot == 53) { openPlayerGUI(player, curPage + 1); return; }
|
||||||
@@ -355,19 +377,16 @@ public class TicketGUI implements Listener {
|
|||||||
|
|
||||||
// ─────────────────────────── Navigation-Handler ─────────────────────────
|
// ─────────────────────────── Navigation-Handler ─────────────────────────
|
||||||
|
|
||||||
/**
|
|
||||||
* Verarbeitet Klicks auf die Navigationsleiste der Admin-Übersicht (Slots 45–53).
|
|
||||||
*/
|
|
||||||
private void handleAdminNavClick(Player player, int slot, boolean isArchive) {
|
private void handleAdminNavClick(Player player, int slot, boolean isArchive) {
|
||||||
int curPage = adminPage.getOrDefault(player.getUniqueId(), 0);
|
int curPage = adminPage.getOrDefault(player.getUniqueId(), 0);
|
||||||
switch (slot) {
|
switch (slot) {
|
||||||
case 45 -> openGUI(player, curPage - 1); // Zurück
|
case 45 -> openGUI(player, curPage - 1);
|
||||||
case 53 -> openGUI(player, curPage + 1); // Vor
|
case 53 -> openGUI(player, curPage + 1);
|
||||||
case 49 -> { // Archiv-Button oder Zurück im Archiv
|
case 49 -> {
|
||||||
if (player.hasPermission(ARCHIVE_PERMISSION)) openClosedGUI(player);
|
if (player.hasPermission(ARCHIVE_PERMISSION)) openClosedGUI(player);
|
||||||
else player.sendMessage(plugin.color("&cDu hast keine Berechtigung, das Archiv zu öffnen."));
|
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)) {
|
if (plugin.getConfig().getBoolean("categories-enabled", true)) {
|
||||||
cycleCategoryFilter(player);
|
cycleCategoryFilter(player);
|
||||||
openGUI(player, 0);
|
openGUI(player, 0);
|
||||||
@@ -381,11 +400,10 @@ public class TicketGUI implements Listener {
|
|||||||
switch (slot) {
|
switch (slot) {
|
||||||
case 45 -> openClosedGUI(player, curPage - 1);
|
case 45 -> openClosedGUI(player, curPage - 1);
|
||||||
case 53 -> 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) {
|
private void cycleCategoryFilter(Player player) {
|
||||||
CategoryManager cm = plugin.getCategoryManager();
|
CategoryManager cm = plugin.getCategoryManager();
|
||||||
List<ConfigCategory> all = cm.getAll();
|
List<ConfigCategory> all = cm.getAll();
|
||||||
@@ -396,7 +414,7 @@ public class TicketGUI implements Listener {
|
|||||||
} else {
|
} else {
|
||||||
int idx = all.indexOf(current);
|
int idx = all.indexOf(current);
|
||||||
int next = idx + 1;
|
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));
|
else categoryFilter.put(player.getUniqueId(), all.get(next));
|
||||||
}
|
}
|
||||||
ConfigCategory newFilter = categoryFilter.getOrDefault(player.getUniqueId(), null);
|
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) {
|
private void handleDetailTeleport(Player player, Ticket ticket) {
|
||||||
|
if (!plugin.isBungeeCordEnabled()) {
|
||||||
|
// ── Standalone-Modus: direkt teleportieren ──
|
||||||
if (ticket.getLocation() != null) {
|
if (ticket.getLocation() != null) {
|
||||||
player.teleport(ticket.getLocation());
|
player.teleport(ticket.getLocation());
|
||||||
player.sendMessage(plugin.color("&7Du wurdest zu Ticket &e#" + ticket.getId() + " &7teleportiert."));
|
player.sendMessage(plugin.color("&7Du wurdest zu Ticket &e#" + ticket.getId() + " &7teleportiert."));
|
||||||
} else {
|
} else {
|
||||||
player.sendMessage(plugin.color("&cDie Welt des Tickets ist nicht geladen!"));
|
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) {
|
private void handleDetailClaim(Player player, Ticket ticket) {
|
||||||
if (ticket.getStatus() != TicketStatus.OPEN) {
|
if (ticket.getStatus() != TicketStatus.OPEN) {
|
||||||
player.sendMessage(plugin.formatMessage("messages.already-claimed"));
|
player.sendMessage(plugin.formatMessage("messages.already-claimed"));
|
||||||
@@ -440,11 +540,9 @@ public class TicketGUI implements Listener {
|
|||||||
ticket.setClaimerUUID(player.getUniqueId());
|
ticket.setClaimerUUID(player.getUniqueId());
|
||||||
ticket.setClaimerName(player.getName());
|
ticket.setClaimerName(player.getName());
|
||||||
plugin.getTicketManager().notifyCreatorClaimed(ticket);
|
plugin.getTicketManager().notifyCreatorClaimed(ticket);
|
||||||
if (ticket.getLocation() != null) player.teleport(ticket.getLocation());
|
|
||||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
// Teleport nach dem Claim – gleiche Logik wie handleDetailTeleport
|
||||||
Ticket fresh = plugin.getDatabaseManager().getTicketById(ticket.getId());
|
handleDetailTeleport(player, ticket);
|
||||||
Bukkit.getScheduler().runTask(plugin, () -> { if (fresh != null) openDetailGUI(player, fresh); });
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -513,7 +611,6 @@ public class TicketGUI implements Listener {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void handleShowComments(Player player, Ticket ticket) {
|
private void handleShowComments(Player player, Ticket ticket) {
|
||||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||||
List<TicketComment> comments = plugin.getDatabaseManager().getComments(ticket.getId());
|
List<TicketComment> comments = plugin.getDatabaseManager().getComments(ticket.getId());
|
||||||
@@ -529,7 +626,6 @@ public class TicketGUI implements Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
player.sendMessage(plugin.color("&8&m "));
|
player.sendMessage(plugin.color("&8&m "));
|
||||||
// Gleich wieder Detail-GUI öffnen
|
|
||||||
openDetailGUI(player, ticket);
|
openDetailGUI(player, ticket);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -552,10 +648,21 @@ public class TicketGUI implements Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final String comment = input.equals("-") ? "" : input;
|
final String comment = input.equals("-") ? "" : input;
|
||||||
|
|
||||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||||
boolean success = plugin.getDatabaseManager().closeTicket(ticketId, comment);
|
boolean success = plugin.getDatabaseManager().closeTicket(ticketId, comment);
|
||||||
if (success) {
|
if (success) {
|
||||||
Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId);
|
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, () -> {
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
player.sendMessage(plugin.formatMessage("messages.ticket-closed").replace("{id}", String.valueOf(ticketId)));
|
player.sendMessage(plugin.formatMessage("messages.ticket-closed").replace("{id}", String.valueOf(ticketId)));
|
||||||
if (!comment.isEmpty()) player.sendMessage(plugin.color("&7Kommentar: &f" + comment));
|
if (!comment.isEmpty()) player.sendMessage(plugin.color("&7Kommentar: &f" + comment));
|
||||||
@@ -570,33 +677,24 @@ public class TicketGUI implements Listener {
|
|||||||
|
|
||||||
// ─────────────────────────── Item-Builder ──────────────────────────────
|
// ─────────────────────────── 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) {
|
private void fillAdminNavigation(Inventory inv, boolean isArchiveView, Player player, int page, int totalPages) {
|
||||||
ItemStack glass = makeGlass();
|
ItemStack glass = makeGlass();
|
||||||
for (int i = 45; i < 54; i++) inv.setItem(i, glass);
|
for (int i = 45; i < 54; i++) inv.setItem(i, glass);
|
||||||
|
|
||||||
// Zurück (Slot 45)
|
|
||||||
if (page > 0) {
|
if (page > 0) {
|
||||||
inv.setItem(45, buildActionItem(Material.ARROW, "§7§l◄ Zurück",
|
inv.setItem(45, buildActionItem(Material.ARROW, "§7§l◄ Zurück",
|
||||||
List.of("§7Seite " + page + " von " + totalPages)));
|
List.of("§7Seite " + page + " von " + totalPages)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Weiter (Slot 53)
|
|
||||||
if (page < totalPages - 1) {
|
if (page < totalPages - 1) {
|
||||||
inv.setItem(53, buildActionItem(Material.ARROW, "§7§lWeiter ►",
|
inv.setItem(53, buildActionItem(Material.ARROW, "§7§lWeiter ►",
|
||||||
List.of("§7Seite " + (page + 2) + " von " + totalPages)));
|
List.of("§7Seite " + (page + 2) + " von " + totalPages)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seitenanzeige (Slot 49)
|
|
||||||
if (!isArchiveView) {
|
if (!isArchiveView) {
|
||||||
if (player.hasPermission(ARCHIVE_PERMISSION)) {
|
if (player.hasPermission(ARCHIVE_PERMISSION)) {
|
||||||
inv.setItem(49, buildActionItem(Material.CHEST, "§7§lGeschlossene Tickets",
|
inv.setItem(49, buildActionItem(Material.CHEST, "§7§lGeschlossene Tickets",
|
||||||
List.of("§7Zeigt alle abgeschlossenen", "§7Tickets im Archiv an.")));
|
List.of("§7Zeigt alle abgeschlossenen", "§7Tickets im Archiv an.")));
|
||||||
}
|
}
|
||||||
// Kategorie-Filter (Slot 47), nur wenn aktiviert
|
|
||||||
if (plugin.getConfig().getBoolean("categories-enabled", true)) {
|
if (plugin.getConfig().getBoolean("categories-enabled", true)) {
|
||||||
ConfigCategory currentFilter = categoryFilter.getOrDefault(player.getUniqueId(), null);
|
ConfigCategory currentFilter = categoryFilter.getOrDefault(player.getUniqueId(), null);
|
||||||
String filterLabel = currentFilter != null ? currentFilter.getColored() : "§7Alle";
|
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));
|
inv.setItem(47, buildActionItem(Material.HOPPER, "§e§lKategorie-Filter", filterLore));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Im Archiv: Zurück-Button in Slot 49
|
|
||||||
inv.setItem(49, buildActionItem(Material.ARROW, "§7§lZurück zur Übersicht",
|
inv.setItem(49, buildActionItem(Material.ARROW, "§7§lZurück zur Übersicht",
|
||||||
List.of("§7Zeigt alle offenen Tickets.")));
|
List.of("§7Zeigt alle offenen Tickets.")));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seitenanzeige Mitte oben (Slot 48)
|
|
||||||
inv.setItem(48, buildActionItem(Material.PAPER, "§8Seite " + (page + 1) + "/" + totalPages,
|
inv.setItem(48, buildActionItem(Material.PAPER, "§8Seite " + (page + 1) + "/" + totalPages,
|
||||||
List.of("§7Gesamt: " + (playerSlotMap.containsKey(player.getUniqueId())
|
List.of("§7Gesamt: " + (playerSlotMap.containsKey(player.getUniqueId())
|
||||||
? playerSlotMap.get(player.getUniqueId()).size() + "+" : "?") + " Tickets auf dieser Seite")));
|
? 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()));
|
inv.setItem(49, buildActionItem(Material.PAPER, "§8Seite " + (page + 1) + "/" + totalPages, List.of()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────── Item-Builder ──────────────────────────────
|
|
||||||
|
|
||||||
private ItemStack buildAdminListItem(Ticket ticket) {
|
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;
|
Material mat;
|
||||||
if (plugin.getConfig().getBoolean("categories-enabled", true)) {
|
if (plugin.getConfig().getBoolean("categories-enabled", true)) {
|
||||||
mat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey()).getMaterial();
|
mat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey()).getMaterial();
|
||||||
@@ -651,7 +743,6 @@ public class TicketGUI implements Listener {
|
|||||||
ItemMeta meta = item.getItemMeta();
|
ItemMeta meta = item.getItemMeta();
|
||||||
if (meta == null) return item;
|
if (meta == null) return item;
|
||||||
|
|
||||||
// Priorität farblich im Titel anzeigen (wenn aktiviert)
|
|
||||||
String priorityPrefix = plugin.getConfig().getBoolean("priorities-enabled", true)
|
String priorityPrefix = plugin.getConfig().getBoolean("priorities-enabled", true)
|
||||||
? ticket.getPriority().getColored() + " §8| " : "";
|
? ticket.getPriority().getColored() + " §8| " : "";
|
||||||
meta.setDisplayName(priorityPrefix + "§6§lTicket #" + ticket.getId() + " §r" + ticket.getStatus().getColored());
|
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("§7Ersteller: §e" + ticket.getCreatorName());
|
||||||
lore.add("§7Anliegen: §f" + ticket.getMessage());
|
lore.add("§7Anliegen: §f" + ticket.getMessage());
|
||||||
lore.add("§7Erstellt: §e" + DATE_FORMAT.format(ticket.getCreatedAt()));
|
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("§7Welt: §e" + ticket.getWorldName());
|
||||||
lore.add(String.format("§7Position: §e%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ()));
|
lore.add(String.format("§7Position: §e%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ()));
|
||||||
if (plugin.getConfig().getBoolean("categories-enabled", true)) {
|
if (plugin.getConfig().getBoolean("categories-enabled", true)) {
|
||||||
@@ -739,6 +833,9 @@ public class TicketGUI implements Listener {
|
|||||||
lore.add("§8§m ");
|
lore.add("§8§m ");
|
||||||
lore.add("§7Anliegen: §f" + ticket.getMessage());
|
lore.add("§7Anliegen: §f" + ticket.getMessage());
|
||||||
lore.add("§7Erstellt: §e" + DATE_FORMAT.format(ticket.getCreatedAt()));
|
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("§7Welt: §e" + ticket.getWorldName());
|
||||||
lore.add(String.format("§7Position: §e%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ()));
|
lore.add(String.format("§7Position: §e%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ()));
|
||||||
if (plugin.getConfig().getBoolean("categories-enabled", true)) {
|
if (plugin.getConfig().getBoolean("categories-enabled", true)) {
|
||||||
|
|||||||
@@ -3,10 +3,13 @@ package de.ticketsystem.listeners;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import de.ticketsystem.TicketPlugin;
|
import de.ticketsystem.TicketPlugin;
|
||||||
|
import de.ticketsystem.database.DatabaseManager;
|
||||||
import de.ticketsystem.model.Ticket;
|
import de.ticketsystem.model.Ticket;
|
||||||
import de.ticketsystem.model.TicketStatus;
|
import de.ticketsystem.model.TicketStatus;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.ChatColor;
|
import org.bukkit.ChatColor;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.World;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.event.EventHandler;
|
import org.bukkit.event.EventHandler;
|
||||||
import org.bukkit.event.Listener;
|
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 ────
|
// ── Ausstehende Kommentar-/Schließ-Benachrichtigungen anzeigen ────
|
||||||
// (Nachrichten die ankamen während der Spieler offline war)
|
// (Nachrichten die ankamen während der Spieler offline war)
|
||||||
Bukkit.getScheduler().runTaskLater(plugin, () -> {
|
Bukkit.getScheduler().runTaskLater(plugin, () -> {
|
||||||
@@ -66,15 +100,17 @@ public class PlayerJoinListener implements Listener {
|
|||||||
plugin.getTicketManager().notifyClaimedWhileOffline(player);
|
plugin.getTicketManager().notifyClaimedWhileOffline(player);
|
||||||
}, 60L);
|
}, 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, () -> {
|
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||||
List<Ticket> closed = plugin.getDatabaseManager()
|
List<Ticket> closed = plugin.getDatabaseManager()
|
||||||
.getTicketsByStatus(TicketStatus.CLOSED);
|
.getTicketsByStatus(TicketStatus.CLOSED);
|
||||||
|
|
||||||
for (Ticket t : closed) {
|
for (Ticket t : closed) {
|
||||||
if (!t.getCreatorUUID().equals(player.getUniqueId())) continue;
|
if (!t.getCreatorUUID().equals(player.getUniqueId())) continue;
|
||||||
if (t.getCloseComment() == null || t.getCloseComment().isEmpty()) continue;
|
// DB-Feld prüfen – funktioniert serverübergreifend
|
||||||
if (plugin.getTicketManager().wasClosedNotificationSent(t.getId())) continue;
|
if (t.isCloseNotified()) continue;
|
||||||
|
|
||||||
Bukkit.getScheduler().runTask(plugin, () ->
|
Bukkit.getScheduler().runTask(plugin, () ->
|
||||||
plugin.getTicketManager().notifyCreatorClosed(t));
|
plugin.getTicketManager().notifyCreatorClosed(t));
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ import org.bukkit.Bukkit;
|
|||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public class TicketManager {
|
public class TicketManager {
|
||||||
@@ -20,9 +18,6 @@ public class TicketManager {
|
|||||||
/** Cooldown Map: UUID → Zeitstempel letztes Ticket */
|
/** Cooldown Map: UUID → Zeitstempel letztes Ticket */
|
||||||
private final Map<UUID, Long> cooldowns = new HashMap<>();
|
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) {
|
public TicketManager(TicketPlugin plugin) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
}
|
}
|
||||||
@@ -46,8 +41,11 @@ public class TicketManager {
|
|||||||
// ─────────────────────────── Benachrichtigungen ────────────────────────
|
// ─────────────────────────── Benachrichtigungen ────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Benachrichtigt alle Online-Supporter/Admins über ein neues Ticket
|
* Benachrichtigt alle Supporter/Admins über ein neues Ticket – auch auf anderen Servern.
|
||||||
* und sendet optional eine Discord-Webhook-Nachricht.
|
*
|
||||||
|
* 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) {
|
public void notifyTeam(Ticket ticket) {
|
||||||
String creatorName = ticket.getCreatorName() != null ? ticket.getCreatorName() : "Unbekannt";
|
String creatorName = ticket.getCreatorName() != null ? ticket.getCreatorName() : "Unbekannt";
|
||||||
@@ -57,23 +55,41 @@ public class TicketManager {
|
|||||||
String categoryInfo = "";
|
String categoryInfo = "";
|
||||||
String priorityInfo = "";
|
String priorityInfo = "";
|
||||||
if (plugin.getConfig().getBoolean("categories-enabled", true)) {
|
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]";
|
categoryInfo = " §7[§r" + cat.getColored() + "§7]";
|
||||||
}
|
}
|
||||||
if (plugin.getConfig().getBoolean("priorities-enabled", true)) {
|
if (plugin.getConfig().getBoolean("priorities-enabled", true)) {
|
||||||
priorityInfo = " §7Priorität: §r" + ticket.getPriority().getColored();
|
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")
|
String msg = plugin.formatMessage("messages.new-ticket-notify")
|
||||||
.replace("{player}", creatorName)
|
.replace("{player}", creatorName)
|
||||||
.replace("{message}", message)
|
.replace("{message}", message)
|
||||||
.replace("{id}", String.valueOf(ticket.getId()))
|
.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()) {
|
for (Player p : Bukkit.getOnlinePlayers()) {
|
||||||
if (p.hasPermission("ticket.support") || p.hasPermission("ticket.admin")) {
|
if (p.hasPermission("ticket.support") || p.hasPermission("ticket.admin")) {
|
||||||
p.sendMessage(msg);
|
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.
|
* Benachrichtigt den Ersteller, wenn sein Ticket angenommen wurde.
|
||||||
* Setzt claimer_notified = true und persistiert es.
|
* Setzt claimer_notified = true und persistiert es.
|
||||||
|
*
|
||||||
|
* BungeeCord: Zustellung auch wenn der Spieler auf einem anderen Server ist.
|
||||||
*/
|
*/
|
||||||
public void notifyCreatorClaimed(Ticket ticket) {
|
public void notifyCreatorClaimed(Ticket ticket) {
|
||||||
Player creator = Bukkit.getPlayer(ticket.getCreatorUUID());
|
String claimerName = resolveClaimerName(ticket);
|
||||||
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 msg = plugin.formatMessage("messages.ticket-claimed-notify")
|
String msg = plugin.formatMessage("messages.ticket-claimed-notify")
|
||||||
.replace("{id}", String.valueOf(ticket.getId()))
|
.replace("{id}", String.valueOf(ticket.getId()))
|
||||||
.replace("{claimer}", claimerName);
|
.replace("{claimer}", claimerName);
|
||||||
creator.sendMessage(msg);
|
|
||||||
}
|
deliverToPlayer(ticket.getCreatorUUID(), ticket.getCreatorName(), msg);
|
||||||
|
|
||||||
// Persistiert setzen, damit Join-Listener weiß, dass Spieler bereits informiert ist
|
// Persistiert setzen, damit Join-Listener weiß, dass Spieler bereits informiert ist
|
||||||
plugin.getDatabaseManager().markClaimerNotified(ticket.getId());
|
plugin.getDatabaseManager().markClaimerNotified(ticket.getId());
|
||||||
}
|
}
|
||||||
@@ -106,15 +120,13 @@ public class TicketManager {
|
|||||||
* die geclaimt oder weitergeleitet wurden während er offline war.
|
* die geclaimt oder weitergeleitet wurden während er offline war.
|
||||||
*/
|
*/
|
||||||
public void notifyClaimedWhileOffline(Player player) {
|
public void notifyClaimedWhileOffline(Player player) {
|
||||||
// Suche alle Tickets dieses Spielers, die CLAIMED/FORWARDED sind,
|
|
||||||
// aber noch nicht notified wurden
|
|
||||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||||
var tickets = plugin.getDatabaseManager().getTicketsByStatus(
|
var tickets = plugin.getDatabaseManager().getTicketsByStatus(
|
||||||
TicketStatus.CLAIMED, TicketStatus.FORWARDED);
|
TicketStatus.CLAIMED, TicketStatus.FORWARDED);
|
||||||
|
|
||||||
for (Ticket t : tickets) {
|
for (Ticket t : tickets) {
|
||||||
if (!t.getCreatorUUID().equals(player.getUniqueId())) continue;
|
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";
|
String claimerName = t.getClaimerName() != null ? t.getClaimerName() : "Support";
|
||||||
final String name = claimerName;
|
final String name = claimerName;
|
||||||
@@ -142,63 +154,171 @@ public class TicketManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Benachrichtigt den Ersteller, wenn sein Ticket weitergeleitet wurde.
|
* Benachrichtigt den Ersteller, wenn sein Ticket weitergeleitet wurde.
|
||||||
|
* BungeeCord: Cross-Server-Zustellung.
|
||||||
*/
|
*/
|
||||||
public void notifyCreatorForwarded(Ticket ticket) {
|
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 forwardedTo = ticket.getForwardedToName() != null ? ticket.getForwardedToName() : "einen Supporter";
|
||||||
String msg = plugin.formatMessage("messages.ticket-forwarded-creator-notify")
|
String msg = plugin.formatMessage("messages.ticket-forwarded-creator-notify")
|
||||||
.replace("{id}", String.valueOf(ticket.getId()))
|
.replace("{id}", String.valueOf(ticket.getId()))
|
||||||
.replace("{supporter}", forwardedTo);
|
.replace("{supporter}", forwardedTo);
|
||||||
creator.sendMessage(msg);
|
|
||||||
}
|
deliverToPlayer(ticket.getCreatorUUID(), ticket.getCreatorName(), msg);
|
||||||
// Auch hier notified setzen
|
|
||||||
|
// Auch bei Weiterleitung notified setzen
|
||||||
plugin.getDatabaseManager().markClaimerNotified(ticket.getId());
|
plugin.getDatabaseManager().markClaimerNotified(ticket.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sendet dem weitergeleiteten Supporter eine Benachrichtigung.
|
* Sendet dem weitergeleiteten Supporter eine Benachrichtigung.
|
||||||
|
* BungeeCord: Zustellung auch wenn der Supporter auf einem anderen Server ist.
|
||||||
*/
|
*/
|
||||||
public void notifyForwardedTo(Ticket ticket, String fromName) {
|
public void notifyForwardedTo(Ticket ticket, String fromName) {
|
||||||
Player target = Bukkit.getPlayer(ticket.getForwardedToUUID());
|
if (ticket.getForwardedToUUID() == null) return;
|
||||||
if (target != null && target.isOnline()) {
|
|
||||||
String creatorName = ticket.getCreatorName() != null ? ticket.getCreatorName() : "Unbekannt";
|
String creatorName = ticket.getCreatorName() != null ? ticket.getCreatorName() : "Unbekannt";
|
||||||
String msg = plugin.formatMessage("messages.ticket-forwarded-notify")
|
String msg = plugin.formatMessage("messages.ticket-forwarded-notify")
|
||||||
.replace("{player}", creatorName)
|
.replace("{player}", creatorName)
|
||||||
.replace("{id}", String.valueOf(ticket.getId()));
|
.replace("{id}", String.valueOf(ticket.getId()));
|
||||||
target.sendMessage(msg);
|
|
||||||
}
|
deliverToPlayer(ticket.getForwardedToUUID(), ticket.getForwardedToName(), msg);
|
||||||
|
|
||||||
plugin.getDiscordWebhook().sendTicketForwarded(ticket, fromName);
|
plugin.getDiscordWebhook().sendTicketForwarded(ticket, fromName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Benachrichtigt den Ersteller, wenn sein Ticket geschlossen wurde.
|
* 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) { notifyCreatorClosed(ticket, null); }
|
||||||
|
|
||||||
public void notifyCreatorClosed(Ticket ticket, String closerName) {
|
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())
|
String comment = (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty())
|
||||||
? ticket.getCloseComment() : "";
|
? ticket.getCloseComment() : "";
|
||||||
|
|
||||||
if (creator != null && creator.isOnline()) {
|
// Hauptnachricht
|
||||||
String msg = plugin.formatMessage("messages.ticket-closed-notify")
|
String msg = plugin.formatMessage("messages.ticket-closed-notify")
|
||||||
.replace("{id}", String.valueOf(ticket.getId()))
|
.replace("{id}", String.valueOf(ticket.getId()))
|
||||||
.replace("{comment}", comment);
|
.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);
|
creator.sendMessage(msg);
|
||||||
if (!comment.isEmpty())
|
if (!comment.isEmpty())
|
||||||
creator.sendMessage(plugin.color("&7Kommentar des Supports: &f" + comment));
|
creator.sendMessage(plugin.color("&7Kommentar des Supports: &f" + comment));
|
||||||
if (plugin.getConfig().getBoolean("rating-enabled", true)) {
|
if (ratingMsg != null) creator.sendMessage(ratingMsg);
|
||||||
creator.sendMessage(plugin.color("&8&m "));
|
|
||||||
creator.sendMessage(plugin.color("&6Wie zufrieden bist du mit dem Support?"));
|
} else if (plugin.isBungeeCordEnabled()) {
|
||||||
creator.sendMessage(plugin.color("&a/ticket rate " + ticket.getId() + " good &7– 👍 Gut"));
|
// ─ BungeeCord: via Plugin-Messaging auf anderen Servern zustellen ─
|
||||||
creator.sendMessage(plugin.color("&c/ticket rate " + ticket.getId() + " bad &7– 👎 Schlecht"));
|
// KEIN savePendingClosedNotification hier! Das würde bei Server-Wechsel
|
||||||
creator.sendMessage(plugin.color("&8&m "));
|
// 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 {
|
} 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."
|
String pendingMsg = "&e[Ticket #" + ticket.getId() + "] &7Dein Ticket wurde geschlossen."
|
||||||
+ (comment.isEmpty() ? "" : " &7Kommentar: &f" + comment)
|
+ (comment.isEmpty() ? "" : " &7Kommentar: &f" + comment)
|
||||||
+ (plugin.getConfig().getBoolean("rating-enabled", true)
|
+ (plugin.getConfig().getBoolean("rating-enabled", true)
|
||||||
@@ -207,16 +327,17 @@ public class TicketManager {
|
|||||||
plugin.getDatabaseManager().addPendingNotification(ticket.getCreatorUUID(), pendingMsg));
|
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 ─────────────────────────────
|
// ─────────────────────────── 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) {
|
public boolean hasReachedTicketLimit(UUID uuid) {
|
||||||
int max = plugin.getConfig().getInt("max-open-tickets-per-player", 2);
|
int max = plugin.getConfig().getInt("max-open-tickets-per-player", 2);
|
||||||
if (max <= 0) return false;
|
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("&e/ticket stats &7– Statistiken anzeigen"));
|
||||||
}
|
}
|
||||||
player.sendMessage(plugin.color("&8&m "));
|
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 double x, y, z;
|
||||||
private float yaw, pitch;
|
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 TicketStatus status;
|
||||||
private UUID claimerUUID;
|
private UUID claimerUUID;
|
||||||
private String claimerName;
|
private String claimerName;
|
||||||
@@ -46,6 +53,12 @@ public class Ticket implements ConfigurationSerializable {
|
|||||||
private String playerRating = null;
|
private String playerRating = null;
|
||||||
private boolean claimerNotified = false;
|
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() {}
|
||||||
|
|
||||||
public Ticket(UUID creatorUUID, String creatorName, String message, Location location) {
|
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("priority")) this.priority = TicketPriority.fromString((String) map.get("priority"));
|
||||||
if (map.containsKey("playerRating")) this.playerRating = (String) map.get("playerRating");
|
if (map.containsKey("playerRating")) this.playerRating = (String) map.get("playerRating");
|
||||||
if (map.containsKey("claimerNotified")) this.claimerNotified = (boolean) map.get("claimerNotified");
|
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
|
@Override
|
||||||
@@ -112,6 +128,9 @@ public class Ticket implements ConfigurationSerializable {
|
|||||||
map.put("priority", priority.name());
|
map.put("priority", priority.name());
|
||||||
if (playerRating != null) map.put("playerRating", playerRating);
|
if (playerRating != null) map.put("playerRating", playerRating);
|
||||||
map.put("claimerNotified", claimerNotified);
|
map.put("claimerNotified", claimerNotified);
|
||||||
|
// BungeeCord: Server-Name speichern
|
||||||
|
map.put("serverName", serverName);
|
||||||
|
map.put("closeNotified", closeNotified);
|
||||||
return map;
|
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 float toFloat(Object o) { return o instanceof Float f ? f : ((Number) o).floatValue(); }
|
||||||
private static long toLong(Object o) { return ((Number) o).longValue(); }
|
private static long toLong(Object o) { return ((Number) o).longValue(); }
|
||||||
|
|
||||||
|
// ─────────────────────────── Getter & Setter ───────────────────────────
|
||||||
|
|
||||||
public int getId() { return id; }
|
public int getId() { return id; }
|
||||||
public void setId(int id) { this.id = id; }
|
public void setId(int id) { this.id = id; }
|
||||||
public UUID getCreatorUUID() { return creatorUUID; }
|
public UUID getCreatorUUID() { return creatorUUID; }
|
||||||
@@ -175,4 +196,14 @@ public class Ticket implements ConfigurationSerializable {
|
|||||||
public boolean hasRating() { return playerRating != null; }
|
public boolean hasRating() { return playerRating != null; }
|
||||||
public boolean isClaimerNotified() { return claimerNotified; }
|
public boolean isClaimerNotified() { return claimerNotified; }
|
||||||
public void setClaimerNotified(boolean v) { this.claimerNotified = v; }
|
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-Modus (true = Logs in der Konsole)
|
||||||
debug: false
|
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
|
# SPEICHERPFAD & ARCHIV
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
@@ -156,6 +176,7 @@ discord:
|
|||||||
show-position: true # Welt & Koordinaten im Embed anzeigen
|
show-position: true # Welt & Koordinaten im Embed anzeigen
|
||||||
show-category: true # Kategorie im Embed anzeigen
|
show-category: true # Kategorie im Embed anzeigen
|
||||||
show-priority: true # Priorität 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
|
role-ping: false # Rollen-Ping bei neuem Ticket senden
|
||||||
|
|
||||||
# ── Ticket geschlossen ──────────────────────────────────────────────────
|
# ── Ticket geschlossen ──────────────────────────────────────────────────
|
||||||
@@ -166,6 +187,7 @@ discord:
|
|||||||
footer: "TicketSystem"
|
footer: "TicketSystem"
|
||||||
show-category: true # Kategorie im Embed anzeigen
|
show-category: true # Kategorie im Embed anzeigen
|
||||||
show-priority: true # Priorität 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
|
role-ping: false # Rollen-Ping beim Schließen senden
|
||||||
|
|
||||||
# ── Ticket weitergeleitet ───────────────────────────────────────────────
|
# ── Ticket weitergeleitet ───────────────────────────────────────────────
|
||||||
@@ -176,6 +198,7 @@ discord:
|
|||||||
footer: "TicketSystem"
|
footer: "TicketSystem"
|
||||||
show-category: true # Kategorie im Embed anzeigen
|
show-category: true # Kategorie im Embed anzeigen
|
||||||
show-priority: true # Priorität 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
|
role-ping: false # Rollen-Ping beim Weiterleiten senden
|
||||||
|
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ api-version: 1.20
|
|||||||
author: M_Viper
|
author: M_Viper
|
||||||
description: Ingame Support Ticket System with MySQL
|
description: Ingame Support Ticket System with MySQL
|
||||||
|
|
||||||
|
# ── BungeeCord Plugin-Messaging-Kanäle ───────────────────────────────────────
|
||||||
|
# PFLICHTFELD für Cross-Server-Benachrichtigungen!
|
||||||
|
channels:
|
||||||
|
- BungeeCord
|
||||||
|
- ticketsystem:notify
|
||||||
|
|
||||||
commands:
|
commands:
|
||||||
ticket:
|
ticket:
|
||||||
description: TicketSystem Hauptbefehl
|
description: TicketSystem Hauptbefehl
|
||||||
|
|||||||
Reference in New Issue
Block a user