Files
TicketSystem/src/main/java/de/ticketsystem/manager/TicketManager.java
2026-02-21 16:00:03 +01:00

376 lines
19 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package de.ticketsystem.manager;
import de.ticketsystem.TicketPlugin;
import de.ticketsystem.model.ConfigCategory;
import de.ticketsystem.model.Ticket;
import de.ticketsystem.model.TicketStatus;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class TicketManager {
private final TicketPlugin plugin;
/** Cooldown Map: UUID → Zeitstempel letztes Ticket */
private final Map<UUID, Long> cooldowns = new HashMap<>();
public TicketManager(TicketPlugin plugin) {
this.plugin = plugin;
}
// ─────────────────────────── Cooldown ──────────────────────────────────
public boolean hasCooldown(UUID uuid) {
if (!cooldowns.containsKey(uuid)) return false;
long cooldownSeconds = plugin.getConfig().getLong("ticket-cooldown", 60) * 1000L;
return (System.currentTimeMillis() - cooldowns.get(uuid)) < cooldownSeconds;
}
public long getRemainingCooldown(UUID uuid) {
long cooldownMillis = plugin.getConfig().getLong("ticket-cooldown", 60) * 1000L;
long elapsed = System.currentTimeMillis() - cooldowns.getOrDefault(uuid, 0L);
return Math.max(0, (cooldownMillis - elapsed) / 1000);
}
public void setCooldown(UUID uuid) { cooldowns.put(uuid, System.currentTimeMillis()); }
// ─────────────────────────── Benachrichtigungen ────────────────────────
/**
* Benachrichtigt alle Supporter/Admins über ein neues Ticket auch auf anderen Servern.
*
* Lokal online Spieler werden direkt angesprochen.
* Über BungeeCord werden alle anderen Server im Netzwerk ebenfalls benachrichtigt.
* Optional sendet der Discord-Webhook eine Nachricht.
*/
public void notifyTeam(Ticket ticket) {
String creatorName = ticket.getCreatorName() != null ? ticket.getCreatorName() : "Unbekannt";
String message = ticket.getMessage() != null ? ticket.getMessage() : "";
// Kategorie & Priorität optional anzeigen
String categoryInfo = "";
String priorityInfo = "";
if (plugin.getConfig().getBoolean("categories-enabled", true)) {
ConfigCategory cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey());
categoryInfo = " §7[§r" + cat.getColored() + "§7]";
}
if (plugin.getConfig().getBoolean("priorities-enabled", true)) {
priorityInfo = " §7Priorität: §r" + ticket.getPriority().getColored();
}
// BungeeCord: Server-Herkunft anzeigen wenn BungeeCord aktiviert
String serverInfo = "";
if (plugin.isBungeeCordEnabled() && !"unknown".equals(ticket.getServerName())) {
serverInfo = " §7Server: §b" + ticket.getServerName();
}
String msg = plugin.formatMessage("messages.new-ticket-notify")
.replace("{player}", creatorName)
.replace("{message}", message)
.replace("{id}", String.valueOf(ticket.getId()))
+ categoryInfo + priorityInfo + serverInfo;
String guiHint = plugin.color("&7» Klicke &e/ticket list &7um die GUI zu öffnen.");
if (plugin.isBungeeCordEnabled()) {
// ─ BungeeCord-Modus: Team-Broadcast über alle Server ─────────────────
// BungeeMessenger sendet lokal direkt, dann per Forward an alle anderen Server.
// Beide Nachrichten werden zu einer zusammengefasst um ein einzelnes
// Forward-Paket zu erzeugen statt zwei (reduziert Netzwerklast und
// verhindert mögliche Reihenfolge-Probleme).
plugin.getBungeeMessenger().broadcastTeamNotification(msg + "\n" + guiHint);
} else {
// ─ Standalone-Modus: Nur lokal ───────────────────────────────
for (Player p : Bukkit.getOnlinePlayers()) {
if (p.hasPermission("ticket.support") || p.hasPermission("ticket.admin")) {
p.sendMessage(msg);
p.sendMessage(guiHint);
}
}
}
plugin.getDiscordWebhook().sendNewTicket(ticket);
}
/**
* Benachrichtigt den Ersteller, wenn sein Ticket angenommen wurde.
* Setzt claimer_notified = true und persistiert es.
*
* BungeeCord: Zustellung auch wenn der Spieler auf einem anderen Server ist.
*/
public void notifyCreatorClaimed(Ticket ticket) {
String claimerName = resolveClaimerName(ticket);
String msg = plugin.formatMessage("messages.ticket-claimed-notify")
.replace("{id}", String.valueOf(ticket.getId()))
.replace("{claimer}", claimerName);
deliverToPlayer(ticket.getCreatorUUID(), ticket.getCreatorName(), msg);
// Persistiert setzen, damit Join-Listener weiß, dass Spieler bereits informiert ist
plugin.getDatabaseManager().markClaimerNotified(ticket.getId());
}
/**
* Wird beim Server-Join aufgerufen informiert den Spieler über Tickets,
* die geclaimt oder weitergeleitet wurden während er offline war.
*/
public void notifyClaimedWhileOffline(Player player) {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
var tickets = plugin.getDatabaseManager().getTicketsByStatus(
TicketStatus.CLAIMED, TicketStatus.FORWARDED);
for (Ticket t : tickets) {
if (!t.getCreatorUUID().equals(player.getUniqueId())) continue;
if (t.isClaimerNotified()) continue;
String claimerName = t.getClaimerName() != null ? t.getClaimerName() : "Support";
final String name = claimerName;
Bukkit.getScheduler().runTask(plugin, () -> {
if (!player.isOnline()) return;
if (t.getStatus() == TicketStatus.CLAIMED) {
String msg = plugin.formatMessage("messages.ticket-claimed-notify")
.replace("{id}", String.valueOf(t.getId()))
.replace("{claimer}", name);
player.sendMessage(msg);
} else {
String forwardedTo = t.getForwardedToName() != null ? t.getForwardedToName() : "einen Supporter";
String msg = plugin.formatMessage("messages.ticket-forwarded-creator-notify")
.replace("{id}", String.valueOf(t.getId()))
.replace("{supporter}", forwardedTo);
player.sendMessage(msg);
}
});
plugin.getDatabaseManager().markClaimerNotified(t.getId());
}
});
}
/**
* Benachrichtigt den Ersteller, wenn sein Ticket weitergeleitet wurde.
* BungeeCord: Cross-Server-Zustellung.
*/
public void notifyCreatorForwarded(Ticket ticket) {
String forwardedTo = ticket.getForwardedToName() != null ? ticket.getForwardedToName() : "einen Supporter";
String msg = plugin.formatMessage("messages.ticket-forwarded-creator-notify")
.replace("{id}", String.valueOf(ticket.getId()))
.replace("{supporter}", forwardedTo);
deliverToPlayer(ticket.getCreatorUUID(), ticket.getCreatorName(), msg);
// Auch bei Weiterleitung notified setzen
plugin.getDatabaseManager().markClaimerNotified(ticket.getId());
}
/**
* Sendet dem weitergeleiteten Supporter eine Benachrichtigung.
* BungeeCord: Zustellung auch wenn der Supporter auf einem anderen Server ist.
*/
public void notifyForwardedTo(Ticket ticket, String fromName) {
if (ticket.getForwardedToUUID() == null) return;
String creatorName = ticket.getCreatorName() != null ? ticket.getCreatorName() : "Unbekannt";
String msg = plugin.formatMessage("messages.ticket-forwarded-notify")
.replace("{player}", creatorName)
.replace("{id}", String.valueOf(ticket.getId()));
deliverToPlayer(ticket.getForwardedToUUID(), ticket.getForwardedToName(), msg);
plugin.getDiscordWebhook().sendTicketForwarded(ticket, fromName);
}
/**
* Benachrichtigt den Ersteller, wenn sein Ticket geschlossen wurde.
* BungeeCord: Cross-Server-Zustellung + Fallback in Pending-DB.
*/
public void notifyCreatorClosed(Ticket ticket) { notifyCreatorClosed(ticket, null); }
public void notifyCreatorClosed(Ticket ticket, String closerName) {
// 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()));
String comment = (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty())
? ticket.getCloseComment() : "";
// Hauptnachricht
String msg = plugin.formatMessage("messages.ticket-closed-notify")
.replace("{id}", String.valueOf(ticket.getId()))
.replace("{comment}", comment);
// Bewertungsaufforderung
String ratingMsg = null;
if (plugin.getConfig().getBoolean("rating-enabled", true)) {
ratingMsg = plugin.color(
"&8&m &r\n" +
"&6Wie zufrieden bist du mit dem Support?\n" +
"&a/ticket rate " + ticket.getId() + " good &7 👍 Gut\n" +
"&c/ticket rate " + ticket.getId() + " bad &7 👎 Schlecht\n" +
"&8&m ");
}
// Prüfen ob Ersteller lokal online ist
Player creator = Bukkit.getPlayer(ticket.getCreatorUUID());
if (creator != null && creator.isOnline()) {
// ─ Lokal online: direkt zustellen ────────────────────────────
creator.sendMessage(msg);
if (!comment.isEmpty())
creator.sendMessage(plugin.color("&7Kommentar des Supports: &f" + comment));
if (ratingMsg != null) creator.sendMessage(ratingMsg);
} else if (plugin.isBungeeCordEnabled()) {
// ─ BungeeCord: via Plugin-Messaging auf anderen Servern zustellen ─
// KEIN savePendingClosedNotification hier! Das würde bei Server-Wechsel
// als "Offline-Nachricht" doppelt angezeigt werden.
// BungeeCord's "Message"-Kanal erreicht den Spieler netzwerkweit sofern er online ist.
// Ist er wirklich offline, sieht er beim nächsten Login via PlayerJoinListener
// eine frische Benachrichtigung (close_notified=true verhindert Duplikate).
plugin.getBungeeMessenger().sendMessageToPlayer(
ticket.getCreatorUUID(), ticket.getCreatorName(), msg);
if (!comment.isEmpty())
plugin.getBungeeMessenger().sendMessageToPlayer(
ticket.getCreatorUUID(), ticket.getCreatorName(),
plugin.color("&7Kommentar des Supports: &f" + comment));
if (ratingMsg != null)
plugin.getBungeeMessenger().sendMessageToPlayer(
ticket.getCreatorUUID(), ticket.getCreatorName(), ratingMsg);
} else {
// ─ Standalone, Spieler offline: in Pending-DB speichern ──────
savePendingClosedNotification(ticket, comment);
}
String closer = closerName != null ? closerName : "Unbekannt";
plugin.getDiscordWebhook().sendTicketClosed(ticket, closer);
}
/**
* Bug-Fix: Nutzt jetzt close_notified aus der DB statt ein In-Memory-Set.
* Funktioniert damit auch nach Server-Wechseln in BungeeCord-Netzwerken korrekt.
*
* @deprecated Bitte stattdessen ticket.isCloseNotified() direkt prüfen,
* da das Ticket-Objekt aus der DB bereits den korrekten Wert hat.
*/
public boolean wasClosedNotificationSent(int ticketId) {
// Direkt in der DB nachschlagen kein In-Memory-Set, kein Server-gebundener State
Ticket t = plugin.getDatabaseManager().getTicketById(ticketId);
return t != null && t.isCloseNotified();
}
// ─────────────────────────── BungeeCord Hilfsmethoden ──────────────────
// ── BUG FIX #2 ──────────────────────────────────────────────────────────
// Vorher: addPendingNotification() wurde IMMER asynchron ausgeführt
// auch wenn der Spieler lokal online war oder BungeeCord die
// Nachricht bereits zugestellt hat. Das führte dazu, dass Spieler
// beim nächsten Login immer noch eine "verpasste Nachricht" sahen,
// obwohl sie die Nachricht bereits erhalten hatten.
//
// Fix: addPendingNotification() wird nur noch aufgerufen wenn:
// 1. Der Spieler NICHT lokal online ist, UND
// 2. BungeeCord NICHT aktiviert ist (Standalone-Fallback).
// Im BungeeCord-Modus ist der BungeeCord-"Message"-Kanal für die
// Zustellung zuständig. Offline-Spieler werden über close_notified
// und den PlayerJoinListener beim nächsten Login benachrichtigt.
// ────────────────────────────────────────────────────────────────────────
/**
* Zustellung einer Nachricht an einen Spieler.
*
* Ablauf:
* 1. Spieler lokal online → direkt
* 2. BungeeCord aktiv → via Plugin-Messaging (kein Pending-Eintrag)
* 3. Offline + Standalone → Pending-DB (Zustellung beim nächsten Login)
*
* @param uuid UUID des Empfängers
* @param name Spielername (für BungeeCord-Lookup)
* @param message Bereits color-übersetzter Text
*/
private void deliverToPlayer(UUID uuid, String name, String message) {
Player local = Bukkit.getPlayer(uuid);
if (local != null && local.isOnline()) {
// Lokal online → direkt zustellen, fertig
local.sendMessage(message);
return;
}
if (plugin.isBungeeCordEnabled()) {
// BungeeCord-Modus: Nachricht über Plugin-Messaging weiterleiten.
// KEIN Pending-Eintrag! BungeeCord übernimmt die Zustellung.
// Ist der Spieler wirklich offline, kümmert sich der PlayerJoinListener
// beim nächsten Login um die Benachrichtigung.
plugin.getBungeeMessenger().sendMessageToPlayer(uuid, name, message);
return;
}
// Standalone-Modus, Spieler offline → in Pending-DB speichern
Bukkit.getScheduler().runTaskAsynchronously(plugin, () ->
plugin.getDatabaseManager().addPendingNotification(uuid, message));
}
/**
* Speichert eine ausstehende Schließ-Benachrichtigung in der DB.
*/
private void savePendingClosedNotification(Ticket ticket, String comment) {
String pendingMsg = "&e[Ticket #" + ticket.getId() + "] &7Dein Ticket wurde geschlossen."
+ (comment.isEmpty() ? "" : " &7Kommentar: &f" + comment)
+ (plugin.getConfig().getBoolean("rating-enabled", true)
? " &7Bewertung: &e/ticket rate " + ticket.getId() + " good/bad" : "");
Bukkit.getScheduler().runTaskAsynchronously(plugin, () ->
plugin.getDatabaseManager().addPendingNotification(ticket.getCreatorUUID(), pendingMsg));
}
// ─────────────────────────── Hilfsmethoden ─────────────────────────────
private String resolveClaimerName(Ticket ticket) {
if (ticket.getClaimerName() != null) return ticket.getClaimerName();
if (ticket.getClaimerUUID() != null) {
String name = Bukkit.getOfflinePlayer(ticket.getClaimerUUID()).getName();
if (name != null) return name;
}
return "Support";
}
public boolean hasReachedTicketLimit(UUID uuid) {
int max = plugin.getConfig().getInt("max-open-tickets-per-player", 2);
if (max <= 0) return false;
return plugin.getDatabaseManager().countOpenTicketsByPlayer(uuid) >= max;
}
public void sendHelpMessage(Player player) {
player.sendMessage(plugin.color("&8&m "));
player.sendMessage(plugin.color("&6TicketSystem &7 Befehle"));
player.sendMessage(plugin.color("&8&m "));
player.sendMessage(plugin.color("&e/ticket create [Kategorie] <Text> &7 Neues Ticket erstellen"));
player.sendMessage(plugin.color("&e/ticket list &7 Deine Tickets ansehen (GUI)"));
player.sendMessage(plugin.color("&e/ticket comment <ID> <Text> &7 Nachricht zu einem Ticket"));
if (plugin.getConfig().getBoolean("rating-enabled", true))
player.sendMessage(plugin.color("&e/ticket rate <ID> <good|bad> &7 Support bewerten"));
if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.color("&e/ticket claim <ID> &7 Ticket annehmen"));
player.sendMessage(plugin.color("&e/ticket close <ID> [Kommentar] &7 Ticket schließen"));
}
if (player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.color("&e/ticket forward <ID> <Spieler> &7 Ticket weiterleiten"));
player.sendMessage(plugin.color("&e/ticket blacklist <add|remove|list> [Spieler] [Grund] &7 Blacklist verwalten"));
player.sendMessage(plugin.color("&e/ticket reload &7 Konfiguration neu laden"));
player.sendMessage(plugin.color("&e/ticket stats &7 Statistiken anzeigen"));
}
player.sendMessage(plugin.color("&8&m "));
// BungeeCord-Status anzeigen
if (player.hasPermission("ticket.admin") && plugin.isBungeeCordEnabled()) {
player.sendMessage(plugin.color("&8[BungeeCord] &7Server: &b" + plugin.getServerName()
+ " &8| Cross-Server-Benachrichtigungen &aaktiv"));
}
}
}