Initial implementations, still WIP

This commit is contained in:
Roni Lehto 2021-07-04 22:31:01 +03:00
parent 451451f05c
commit 459dbf80ec
17 changed files with 1777 additions and 7 deletions

39
config.yml Normal file
View File

@ -0,0 +1,39 @@
# Aquamarine configuration v0.1.0
# Storage method;
# 'mysql' for MySQL
# 'file' for flat-file storage
# 'memory' for non-persistent memory storage
storage-method: "file"
# Database configuration (if storage-method set to 'mysql')
mysql-host: localhost
mysql-port: 3306
mysql-user: user
mysql-pass: pass
mysql-db: database
mysql-table: aquamarine_tickets
# Max open tickets per radius
enable-max-per-radius: true
check-radius: 5.0
max-per-radius: 3
# Max open tickets per player
enable-max-per-player: true
max-per-player: 3
# Formatting
message-prefix: "&8[&bAquamarine&8] &7"
date-format: "dd.MM.yyyy kk:mm:ss" # (SimpleDateFormat)
# Join announcement for users with permissions
enable-join-announce: true
join-announce-delay-seconds: 5
# Discord webhook settings
enable-webhook: false
webhook-url: https://example.com/
# Do not change
config-version: 1.0

22
lang.yml Normal file
View File

@ -0,0 +1,22 @@
# Language file for XAquamarine
generic-no-permission: "Sinulla ei ole oikeutta tuohon."
command-ticket-usage: "Käytä: &b/%label% [viesti]"
ticket-deny-nearby: "Et voi luoda uutta apupyyntöä tässä, koska lähellä on tehty jo muita apupyyntöjä."
ticket-deny-player: "Et voi luoda uutta apupyyntöä, koska sinulla on jo useita odottavia apupyyntöjä."
ticket-created: "Avasit uuden apupyynnön. Numero: &b%ticketId%"
ticket-created-announcement: "&b%player% &7avasi apupyynnön &b#%ticketId%&7:"
ticket-join-announcement: "Palvelimella odottaa &b%ticketCount% apupyyntöä"
ticket-preview-teleport: "Teleporttaa"
ticket-preview-solve: "Muuta ratkaistuksi"
ticket-hover-title: "&bApupyyntö #%ticketId%"
ticket-hover-sender: "&7Lähettäjä: &f%s"
ticket-hover-timestamp: "&7Aikaleima: &f%s"
ticket-hover-location: "&7Sijainti: &f%s"
ticket-hover-solver: "&7Selvittäjä: &f%s"
ticket-hover-solved-at: "&7Selvitetty: &f%s"
ticket-hover-comment: "&7Kommentti:"

View File

@ -3,11 +3,24 @@ description: A ticket plugin for Xeno
author: Xeno
website: https://xeno.fi/
commands:
xnpc:
description: Ukkeleiden toiminnallisia höpötyksiä
ticket:
description: Open a new ticket
aliases: [apupyyntö, avunpyyntö, tiketti, helpop]
permission: aquamarine.ticket
tickets:
description: Show a list of your recent tickets
aliases: [apupyynnöt, avunpyynnöt, tiketit]
permission: aquamarine.ticket
xt:
description: Staff features
permission: aquamarine.staff
xti:
description: Staff features
permission: aquamarine.staff
permissions:
xfig.admin:
description: Oikeudet luoda, poistaa, jne.
version: 2.0
aquamarine.ticket:
description: Permission to open new tickets
aquamarine.staff:
description: Permission to teleport to tickets and solve them
version: 0.1.0
main: fi.xeno.aquamarine.XTicketsPlugin
load: STARTUP

View File

@ -0,0 +1,9 @@
package fi.xeno.aquamarine;
public class AquamarinePermission {
public static final String STAFF = "aquamarine.staff";
public static final String CREATE_TICKET = "aquamarine.ticket";
}

View File

@ -0,0 +1,108 @@
package fi.xeno.aquamarine;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.chat.hover.content.Text;
public class XText {
/**
* Create a clickable text button which runs a command
*/
public static TextComponent commandButton(String coloredText, String hoverText, String clickCommand){
TextComponent btn = (new TextComponent(TextComponent.fromLegacyText(coloredText)));
btn.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(TextComponent.fromLegacyText(hoverText))));
btn.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, clickCommand));
return btn;
}
/**
* Create a clickable text button which suggests a command
*/
public static TextComponent commandSuggestButton(String coloredText, String hoverText, String clickCommand){
TextComponent btn = (new TextComponent(TextComponent.fromLegacyText(coloredText)));
btn.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(TextComponent.fromLegacyText(hoverText))));
btn.setClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, clickCommand));
return btn;
}
/**
* Create a clickable text button which suggests a command
*/
public static TextComponent hoverText(String coloredText, String hoverText){
TextComponent btn = (new TextComponent(TextComponent.fromLegacyText(coloredText)));
btn.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(TextComponent.fromLegacyText(hoverText))));
return btn;
}
/**
* Create a clickable text button which copies text to clipboard
*/
public static TextComponent copyButton(String coloredText, String hoverText, String clickCopy){
TextComponent btn = (new TextComponent(TextComponent.fromLegacyText(coloredText)));
btn.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(TextComponent.fromLegacyText(hoverText))));
btn.setClickEvent(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, clickCopy));
return btn;
}
/**
* Create a clickable text button which opens a URL address
*/
public static TextComponent linkButton(String coloredText, String hoverText, String url) {
TextComponent btn = (new TextComponent(TextComponent.fromLegacyText(coloredText)));
btn.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(TextComponent.fromLegacyText(hoverText))));
btn.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, url));
return btn;
}
/**
* Shorthand for TextComponent::fromLegacyText
*/
public static TextComponent t(String coloredText) {
return new TextComponent(TextComponent.fromLegacyText(coloredText));
}
/**
* Split a string into rows
*/
public static String wordWrap(String input, int charsPerLine) {
StringBuilder out = new StringBuilder();
String[] words = input.split(" ");
int line = 0;
for (String s:words) {
out.append(s);
line += s.length();
if (line >= charsPerLine) {
out.append('\n');
line = 0;
}
}
return out.toString();
}
}

View File

@ -0,0 +1,118 @@
package fi.xeno.aquamarine;
import fi.xeno.aquamarine.storage.XTicketDataStorage;
import fi.xeno.aquamarine.util.XStoredLocation;
import fi.xeno.aquamarine.util.XTicket;
import org.bukkit.Bukkit;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import java.util.List;
public class XTicketManager implements Listener {
private final XTicketsPlugin plugin;
private final XTicketDataStorage storage;
public XTicketManager(XTicketsPlugin plugin, XTicketDataStorage storage) {
this.plugin = plugin;
this.storage = storage;
}
public XTicketDataStorage getStorage() {
return storage;
}
public TicketCreationStatus canCreateTicket(Player player) {
if (plugin.getConfig().getBoolean("enable-max-per-radius", false)) {
int nearbyTickets = storage.getWaitingNearbyTickets(new XStoredLocation(player.getLocation()),
plugin.getConfig().getDouble("check-radius", 3d)).size();
if (nearbyTickets >= plugin.getConfig().getInt("max-per-radius", 3)) {
return TicketCreationStatus.DENY_NEARBY;
}
}
if (plugin.getConfig().getBoolean("enable-max-per-player", false)) {
int playerTickets = storage.getWaitingTicketsBySender(player.getUniqueId()).size();
if (playerTickets >= plugin.getConfig().getInt("max-per-player", 3)) {
return TicketCreationStatus.DENY_PLAYER;
}
}
return TicketCreationStatus.ALLOW;
}
public XTicket createTicket(Player player, String message) {
XTicket ticket = storage.createTicket(player, message);
if (ticket == null)
throw new RuntimeException("Unable to create new ticket. Check your storage method.");
String staffAnnounce = plugin.lang("ticket-created-announcement")
.replace("player", player.getName())
.replace("ticketId", ""+ticket.getId());
Bukkit.getOnlinePlayers()
.stream()
.filter(p -> p.hasPermission(AquamarinePermission.STAFF))
.forEach(p -> plugin.sendPrefixed(staffAnnounce, p));
return ticket;
}
@EventHandler
private void onPlayerJoin(PlayerJoinEvent e) {
if (!plugin.getConfig().getBoolean("enable-join-announce", false))
return;
Bukkit.getScheduler().runTaskLater(plugin, () -> {
Player player = e.getPlayer();
if (player.hasPermission(AquamarinePermission.STAFF)) {
storage.getWaitingTicketsAsync((List<XTicket> tickets) -> {
if (tickets.size() > 0) {
plugin.sendPrefixed(plugin.lang("ticket-join-announcement").replace("%ticketCount%", ""+tickets.size()));
player.playSound(player.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 1f, 1.5f);
}
});
}
}, 20L*plugin.getConfig().getInt("join-announce-delay-seconds", 5));
}
public static enum TicketCreationStatus {
ALLOW(true),
DENY_NEARBY(false),
DENY_PLAYER(false);
private boolean wasAllowed;
TicketCreationStatus(boolean b) {
this.wasAllowed = b;
}
public boolean wasAllowed() {
return wasAllowed;
}
}
}

View File

@ -1,8 +1,26 @@
package fi.xeno.aquamarine;
import fi.xeno.aquamarine.command.CommandTicket;
import fi.xeno.aquamarine.sql.XHikariDatabase;
import fi.xeno.aquamarine.storage.XFlatFileTicketDataStorage;
import fi.xeno.aquamarine.storage.XMemoryTicketDataStorage;
import fi.xeno.aquamarine.storage.XSQLTicketDataStorage;
import fi.xeno.aquamarine.storage.XTicketDataStorage;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.command.TabExecutor;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.sql.Date;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collection;
import java.util.logging.Logger;
public class XTicketsPlugin extends JavaPlugin {
private static XTicketsPlugin instance;
@ -10,10 +28,99 @@ public class XTicketsPlugin extends JavaPlugin {
return instance;
}
private static String INFO_PREFIX = "§e";
private Logger logger;
private XTicketManager ticketManager;
private File fileLang;
private YamlConfiguration lang;
private SimpleDateFormat dateFormat;
public void onEnable() {
instance = this;
logger = this.getLogger();
if (!this.getDataFolder().exists()) {
this.getDataFolder().mkdir();
}
this.saveDefaultConfig();
fileLang = new File(this.getDataFolder(), "lang.yml");
if (!fileLang.exists()) this.saveResource("lang.yml", false);
lang = YamlConfiguration.loadConfiguration(fileLang);
INFO_PREFIX = ChatColor.translateAlternateColorCodes('&', this.getConfig().getString("message-prefix", INFO_PREFIX));
dateFormat = new SimpleDateFormat(this.getConfig().getString("date-format", "dd.MM.yyyy kk:mm:ss"));
XTicketDataStorage dataStorage;
switch (this.getConfig().getString("storage-method", "file").toLowerCase()) {
case "mysql":
XHikariDatabase db = new XHikariDatabase(this,
this.getConfig().getString("mysql-host"),
this.getConfig().getString("mysql-port"),
this.getConfig().getString("mysql-db"),
this.getConfig().getString("mysql-user"),
this.getConfig().getString("mysql-pass"));
dataStorage = new XSQLTicketDataStorage(this, db, this.getConfig().getString("mysql-table", "aquamarine_tickets"));
break;
case "file":
dataStorage = new XFlatFileTicketDataStorage(this, new File(this.getDataFolder(), "tickets.json"));
break;
case "memory":
dataStorage = new XMemoryTicketDataStorage(this);
break;
default:
throw new RuntimeException("Storage method '" + this.getConfig().getString("storage-method") + "' is not supported");
}
ticketManager = new XTicketManager(this, dataStorage);
registerCommand("ticket", new CommandTicket(this, ticketManager));
logger.info("Aquamarine has been enabled!");
}
public void onDisable() {}
public void onDisable() {
ticketManager.getStorage().close();
logger.info("Aquamarine has been disabled.");
}
private void registerCommand(String label, TabExecutor command) {
this.getCommand(label).setExecutor(command);
this.getCommand(label).setTabCompleter(command);
}
public void sendPrefixed(String message, Player... recipients) {
sendPrefixed(message, Arrays.asList(recipients));
}
public void sendPrefixed(String message, Iterable<Player> recipients) {
String out = INFO_PREFIX + message;
recipients.forEach(p -> p.sendMessage(out));
}
public String lang(String key){
return lang.getString(key, "lang["+key+"]");
}
public String formatTimestamp(long timestamp) {
return dateFormat.format(Date.from(Instant.ofEpochMilli(timestamp)));
}
}

View File

@ -0,0 +1,79 @@
package fi.xeno.aquamarine.command;
import fi.xeno.aquamarine.AquamarinePermission;
import fi.xeno.aquamarine.XTicketManager;
import fi.xeno.aquamarine.XTicketsPlugin;
import fi.xeno.aquamarine.util.XTicket;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import java.util.List;
import java.util.StringJoiner;
public class CommandTicket implements TabExecutor {
private final XTicketsPlugin plugin;
private final XTicketManager ticketManager;
public CommandTicket(XTicketsPlugin plugin, XTicketManager ticketManager) {
this.plugin = plugin;
this.ticketManager = ticketManager;
}
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
if (!sender.hasPermission(AquamarinePermission.CREATE_TICKET)) {
sender.sendMessage(plugin.lang("generic-no-permission"));
return true;
}
if (!(sender instanceof Player)) {
sender.sendMessage("§cThis command can only be executed by a player.");
return true;
}
if (args.length == 0) {
sender.sendMessage(plugin.lang("command-ticket-usage").replace("%label%", label));
return true;
}
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
StringJoiner msgJoiner = new StringJoiner(" ");
for (String s:args) msgJoiner.add(s);
Player player = (Player)sender;
switch (ticketManager.canCreateTicket(player)) {
case DENY_NEARBY:
plugin.sendPrefixed(plugin.lang("ticket-deny-nearby"), player);
return;
case DENY_PLAYER:
plugin.sendPrefixed(plugin.lang("ticket-deny-player"), player);
return;
}
XTicket ticket = ticketManager.createTicket(player, msgJoiner.toString().trim());
plugin.sendPrefixed(plugin.lang("ticket-created").replace("%ticketId%", ""+ticket.getId()));
});
return true;
}
@Override
public List<String> onTabComplete(CommandSender sender, Command cmd, String label, String[] args) {
return null;
}
}

View File

@ -0,0 +1,76 @@
package fi.xeno.aquamarine.command;
import fi.xeno.aquamarine.AquamarinePermission;
import fi.xeno.aquamarine.XTicketManager;
import fi.xeno.aquamarine.XTicketsPlugin;
import fi.xeno.aquamarine.util.TimestampedValue;
import fi.xeno.aquamarine.util.XTicket;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class CommandXt implements TabExecutor {
private final XTicketsPlugin plugin;
private final XTicketManager ticketManager;
private Map<UUID, TimestampedValue<Integer>> lastTicket = new ConcurrentHashMap<>();
public CommandXt(XTicketsPlugin plugin, XTicketManager ticketManager) {
this.plugin = plugin;
this.ticketManager = ticketManager;
Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, () -> {
List<UUID> toRemove = new ArrayList<>();
lastTicket.forEach((k, v) -> {
if (v.isOlderThan(1000L*60*60)) {
toRemove.add(k);
}
});
toRemove.forEach(lastTicket::remove);
}, 20L*60*30, 20L*60*30);
}
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
if (!sender.hasPermission(AquamarinePermission.STAFF)) {
sender.sendMessage(plugin.lang("generic-no-permission"));
return true;
}
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
if (args.length == 0) {
List<XTicket> tickets = ticketManager.getStorage().getWaitingTickets();
return;
}
});
return true;
}
@Override
public List<String> onTabComplete(CommandSender sender, Command cmd, String label, String[] args) {
return null;
}
}

View File

@ -0,0 +1,97 @@
package fi.xeno.aquamarine.sql;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.bukkit.plugin.Plugin;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class XHikariDatabase {
private HikariConfig config;
private HikariDataSource dataSource;
private Plugin plugin;
public XHikariDatabase(Plugin plugin, String host, String port, String db, String user, String pass) {
this.plugin = plugin;
config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://" + host + ":" + port + "/" + db);
config.setUsername(user);
config.setPassword(pass);
config.setDriverClassName("com.mysql.jdbc.Driver");
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("prepStmtCacheSize", "250");
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
dataSource = new HikariDataSource(config);
}
/**
* Get the raw data source
* @return
*/
public HikariDataSource getDataSource() {
return dataSource;
}
/**
* Get a connection
* @return
* @throws SQLException
*/
public Connection c() throws SQLException {
return dataSource.getConnection();
}
/**
* Check if connection is closed
* @return
*/
public boolean isClosed() {
return dataSource.isClosed();
}
/**
* Close the connection
*/
public void close() {
if (!dataSource.isClosed()) {
dataSource.close();
}
}
/**
* Close used things automatically
* @param rs
* @param st
* @param c
* @throws SQLException
*/
public void close(ResultSet rs, PreparedStatement st, Connection c) throws SQLException {
if (rs != null && !rs.isClosed()) {
rs.close();
}
if (st != null && !st.isClosed()) {
st.close();
}
if (c != null && !c.isClosed()) {
c.close();
}
}
}

View File

@ -0,0 +1,205 @@
package fi.xeno.aquamarine.storage;
import com.google.gson.*;
import com.google.gson.stream.JsonReader;
import fi.xeno.aquamarine.XTicketsPlugin;
import fi.xeno.aquamarine.util.XStoredLocation;
import fi.xeno.aquamarine.util.XTicket;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.io.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.stream.Collectors;
public class XFlatFileTicketDataStorage extends XTicketDataStorage {
private final XTicketsPlugin plugin;
private final File file;
private final Gson gson = new GsonBuilder()
.setPrettyPrinting()
.create();
private final Map<Integer, XTicket> tickets = new ConcurrentHashMap<>();
public XFlatFileTicketDataStorage(XTicketsPlugin plugin, File file) {
this.plugin = plugin;
this.file = file;
try {
load();
} catch (IOException e) {
plugin.getLogger().severe("Unable to load data from flat file storage:");
e.printStackTrace();
}
}
private void load() throws IOException {
tickets.clear();
if (!file.exists()) {
return;
}
JsonReader reader = new JsonReader(new FileReader(file));
JsonParser parser = new JsonParser();
JsonElement root = parser.parse(reader);
if (!root.isJsonArray()) {
throw new RuntimeException("Unable to parse ticket data: JSON is not formatted as an array.");
}
JsonArray rawArr = root.getAsJsonArray();
for (JsonElement el:rawArr) {
if (!el.isJsonObject()) {
throw new RuntimeException("Unable to parse ticket data: ticket is not formatted as a JSON object");
}
XTicket ticket = XTicket.fromJson(el.getAsJsonObject());
tickets.put(ticket.getId(), ticket);
}
reader.close();
}
private void save() {
JsonArray out = new JsonArray();
tickets.values()
.stream()
.sorted(Comparator.comparingInt(XTicket::getId))
.forEach(t -> out.add(t.toJson()));
try {
Writer writer = new FileWriter(file);
gson.toJson(out, writer);
} catch (IOException e) {
plugin.getLogger().severe("Unable to save ticket JSON data:");
e.printStackTrace();
}
}
private void saveAsync() {
Bukkit.getScheduler().runTaskAsynchronously(plugin, this::save);
}
@Override
public XTicket createTicket(Player player, String message) {
XTicket ticket = XTicket.asPlayer(getNextTicketId(), player, message);
tickets.put(ticket.getId(), ticket);
saveAsync();
return ticket;
}
@Override
public void solveTicket(XTicket ticket, Player solver, String comment) {
ticket.setSolved(true);
ticket.setSolvedByUuid(solver.getUniqueId());
ticket.setSolvedByName(solver.getName());
ticket.setSolveComment(comment);
ticket.setTimeSolved(System.currentTimeMillis());
// this SHOULD be unnecessary, but just to be sure...
tickets.put(ticket.getId(), ticket);
saveAsync();
}
@Override
public Optional<XTicket> getTicketByNumber(int id) {
return Optional.ofNullable(tickets.getOrDefault(id, null));
}
@Override
public List<XTicket> getTickets() {
return tickets.values()
.stream()
.sorted(Comparator.comparingInt(XTicket::getId))
.collect(Collectors.toList());
}
@Override
public List<XTicket> getWaitingTickets() {
return tickets.values()
.stream()
.filter(t -> !t.isSolved())
.sorted(Comparator.comparingInt(XTicket::getId))
.collect(Collectors.toList());
}
@Override
public List<XTicket> getSolvedTickets() {
return tickets.values()
.stream()
.filter(XTicket::isSolved)
.sorted(Comparator.comparingInt(XTicket::getId))
.collect(Collectors.toList());
}
@Override
public List<XTicket> getWaitingNearbyTickets(XStoredLocation location, double radius) {
return tickets.values()
.stream()
.filter(t -> !t.isSolved() && t.getLocation().isInRadius(location, radius))
.sorted(Comparator.comparingInt(XTicket::getId))
.collect(Collectors.toList());
}
@Override
public List<XTicket> getWaitingTicketsBySender(UUID uuid) {
return tickets.values()
.stream()
.filter(t -> !t.isSolved() && t.getSentByUuid().equals(uuid))
.sorted(Comparator.comparingInt(XTicket::getId))
.collect(Collectors.toList());
}
@Override
public List<XTicket> getTicketsBySender(UUID uuid) {
return tickets.values()
.stream()
.filter(t -> t.getSentByUuid().equals(uuid))
.sorted(Comparator.comparingInt(XTicket::getId))
.collect(Collectors.toList());
}
@Override
public List<XTicket> getTicketsBySolver(UUID uuid) {
return tickets.values()
.stream()
.filter(t -> t.isSolved() && t.getSolvedByUuid().equals(uuid))
.sorted(Comparator.comparingInt(XTicket::getId))
.collect(Collectors.toList());
}
@Override
public int getNextTicketId() {
return tickets.keySet().stream().max(Comparator.comparingInt(Integer::intValue)).orElse(0) + 1;
}
@Override
public void close() {
plugin.getLogger().info("Saving ticket storage...");
save();
}
}

View File

@ -0,0 +1,126 @@
package fi.xeno.aquamarine.storage;
import com.google.gson.*;
import com.google.gson.stream.JsonReader;
import fi.xeno.aquamarine.XTicketsPlugin;
import fi.xeno.aquamarine.util.XStoredLocation;
import fi.xeno.aquamarine.util.XTicket;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.io.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
public class XMemoryTicketDataStorage extends XTicketDataStorage {
private final XTicketsPlugin plugin;
private final Map<Integer, XTicket> tickets = new ConcurrentHashMap<>();
public XMemoryTicketDataStorage(XTicketsPlugin plugin) {
this.plugin = plugin;
}
@Override
public XTicket createTicket(Player player, String message) {
XTicket ticket = XTicket.asPlayer(getNextTicketId(), player, message);
tickets.put(ticket.getId(), ticket);
return ticket;
}
@Override
public void solveTicket(XTicket ticket, Player solver, String comment) {
ticket.setSolved(true);
ticket.setSolvedByUuid(solver.getUniqueId());
ticket.setSolvedByName(solver.getName());
ticket.setSolveComment(comment);
ticket.setTimeSolved(System.currentTimeMillis());
// this SHOULD be unnecessary, but just to be sure...
tickets.put(ticket.getId(), ticket);
}
@Override
public Optional<XTicket> getTicketByNumber(int id) {
return Optional.ofNullable(tickets.getOrDefault(id, null));
}
@Override
public List<XTicket> getTickets() {
return tickets.values()
.stream()
.sorted(Comparator.comparingInt(XTicket::getId))
.collect(Collectors.toList());
}
@Override
public List<XTicket> getWaitingTickets() {
return tickets.values()
.stream()
.filter(t -> !t.isSolved())
.sorted(Comparator.comparingInt(XTicket::getId))
.collect(Collectors.toList());
}
@Override
public List<XTicket> getSolvedTickets() {
return tickets.values()
.stream()
.filter(XTicket::isSolved)
.sorted(Comparator.comparingInt(XTicket::getId))
.collect(Collectors.toList());
}
@Override
public List<XTicket> getWaitingNearbyTickets(XStoredLocation location, double radius) {
return tickets.values()
.stream()
.filter(t -> !t.isSolved() && t.getLocation().isInRadius(location, radius))
.sorted(Comparator.comparingInt(XTicket::getId))
.collect(Collectors.toList());
}
@Override
public List<XTicket> getWaitingTicketsBySender(UUID uuid) {
return tickets.values()
.stream()
.filter(t -> !t.isSolved() && t.getSentByUuid().equals(uuid))
.sorted(Comparator.comparingInt(XTicket::getId))
.collect(Collectors.toList());
}
@Override
public List<XTicket> getTicketsBySender(UUID uuid) {
return tickets.values()
.stream()
.filter(t -> t.getSentByUuid().equals(uuid))
.sorted(Comparator.comparingInt(XTicket::getId))
.collect(Collectors.toList());
}
@Override
public List<XTicket> getTicketsBySolver(UUID uuid) {
return tickets.values()
.stream()
.filter(t -> t.isSolved() && t.getSolvedByUuid().equals(uuid))
.sorted(Comparator.comparingInt(XTicket::getId))
.collect(Collectors.toList());
}
@Override
public int getNextTicketId() {
return tickets.keySet().stream().max(Comparator.comparingInt(Integer::intValue)).orElse(0) + 1;
}
@Override
public void close() {}
}

View File

@ -0,0 +1,292 @@
package fi.xeno.aquamarine.storage;
import fi.xeno.aquamarine.XTicketsPlugin;
import fi.xeno.aquamarine.sql.XHikariDatabase;
import fi.xeno.aquamarine.util.XStoredLocation;
import fi.xeno.aquamarine.util.XTicket;
import org.bukkit.entity.Player;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import java.util.stream.Collectors;
public class XSQLTicketDataStorage extends XTicketDataStorage {
private final XTicketsPlugin plugin;
private final XHikariDatabase db;
private String tableName;
public XSQLTicketDataStorage(XTicketsPlugin plugin, XHikariDatabase db, String tableName) {
this.plugin = plugin;
this.db = db;
if (tableName.matches("[^A-Za-z0-9\\_\\-]")) {
throw new RuntimeException("Table name '" + tableName + "' contains illegal characters.");
}
Connection c = null;
PreparedStatement st = null;
try {
c = db.c();
st = c.prepareStatement("CREATE TABLE IF NOT EXISTS `" + tableName + "` (" +
" `id` int(11) NOT NULL AUTO_INCREMENT," +
" `ticketId` int(11) DEFAULT NULL," +
" `timestamp` bigint(22) DEFAULT NULL," +
" `sentByUuid` varchar(64) DEFAULT NULL," +
" `sentByName` varchar(64) DEFAULT NULL," +
" `location` varchar(128) DEFAULT NULL," +
" `message` text DEFAULT NULL," +
" `isSolved` tinyint(1) DEFAULT NULL," +
" `solvedByUuid` varchar(64) DEFAULT NULL," +
" `solvedByName` varchar(64) DEFAULT NULL," +
" `solveComment` text DEFAULT NULL," +
" `timeSolved` bigint(22) DEFAULT NULL," +
" PRIMARY KEY (`id`)" +
")"
);
st.executeUpdate();
} catch (SQLException throwables) {
plugin.getLogger().severe("Unable to create database tables:");
throwables.printStackTrace();
} finally {
try {
db.close(null, st, c);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
@Override
public XTicket createTicket(Player player, String message) {
XTicket ticket = XTicket.asPlayer(getNextTicketId(), player, message);
Connection c = null;
PreparedStatement st = null;
try {
c = db.c();
st = c.prepareStatement("INSERT INTO " + tableName + " " +
"(id, ticketId, timestamp, sentByUuid, sentByName, location, message, isSolved, solvedByUuid, solvedByName, solveComment, timeSolved) VALUES " +
"(NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
st.setInt(1, ticket.getId());
st.setLong(2, ticket.getTimestamp());
st.setString(3, ticket.getSentByUuid().toString());
st.setString(4, ticket.getSentByName());
st.setString(5, ticket.getLocation().toString());
st.setString(6, ticket.getMessage());
st.setInt(7, ticket.isSolved() ? 1 : 0);
st.setString(8, "");
st.setString(9, "");
st.setString(10, "");
st.setLong(11, 0L);
int count = st.executeUpdate();
if (count <= 0) ticket = null;
} catch (SQLException throwables) {
throwables.printStackTrace();
ticket = null;
} finally {
try {
db.close(null, st, c);
} catch (SQLException throwables) {
throwables.printStackTrace();
ticket = null;
}
}
return ticket;
}
@Override
public void solveTicket(XTicket ticket, Player solver, String comment) {
// these SHOULD be unneeded, but just in case...
ticket.setSolved(true);
ticket.setSolvedByUuid(solver.getUniqueId());
ticket.setSolvedByName(solver.getName());
ticket.setSolveComment(comment);
ticket.setTimeSolved(System.currentTimeMillis());
Connection c = null;
PreparedStatement st = null;
try {
c = db.c();
st = c.prepareStatement("UPDATE " + tableName + " " +
"SET isSolved = 1, solvedByUuid = ?, solvedByName = ?, solveComment = ?, timeSolved = ? " +
"WHERE ticketId = ?");
st.setString(1, solver.getUniqueId().toString());
st.setString(2, solver.getName());
st.setString(3, comment);
st.setLong(4, System.currentTimeMillis());
st.setInt(5, ticket.getId());
st.executeUpdate();
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
try {
db.close(null, st, c);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
@Override
public Optional<XTicket> getTicketByNumber(int id) {
return Optional.empty();
}
@Override
public List<XTicket> getTickets() {
return queryTickets("SELECT * FROM " + tableName + " ORDER BY id DESC", o());
}
@Override
public List<XTicket> getWaitingTickets() {
return queryTickets("SELECT * FROM " + tableName + " WHERE isSolved = 0 ORDER BY id DESC", o());
}
@Override
public List<XTicket> getSolvedTickets() {
return queryTickets("SELECT * FROM " + tableName + " WHERE isSolved = 1 ORDER BY id DESC", o());
}
@Override
public List<XTicket> getWaitingNearbyTickets(XStoredLocation location, double radius) {
return getWaitingTickets()
.stream()
.filter(t -> t.getLocation().isInRadius(location, radius))
.collect(Collectors.toList());
}
@Override
public List<XTicket> getWaitingTicketsBySender(UUID uuid) {
return queryTickets("SELECT * FROM " + tableName + " WHERE isSolved = 0 AND sentByUuid = ? ORDER BY id DESC", o(uuid.toString()));
}
@Override
public List<XTicket> getTicketsBySender(UUID uuid) {
return queryTickets("SELECT * FROM " + tableName + " WHERE sentByUuid = ?", o(uuid.toString()));
}
@Override
public List<XTicket> getTicketsBySolver(UUID uuid) {
return queryTickets("SELECT * FROM " + tableName + " WHERE isSolved = 1 AND solvedByUuid = ?", o(uuid.toString()));
}
private List<XTicket> queryTickets(String query, Object[] params) {
Connection c = null;
PreparedStatement st = null;
ResultSet rs = null;
List<XTicket> out = new ArrayList<>();
try {
c = db.c();
st = c.prepareStatement(query);
int n = 1;
for (Object _o:params) {
if (_o instanceof String) {
st.setString(n, (String)_o);
} else if (_o instanceof Integer) {
st.setInt(n, (Integer)_o);
} else if (_o instanceof Long) {
st.setLong(n, (Long)_o);
}
n++;
}
rs = st.executeQuery();
while (rs.next()) {
out.add(XTicket.fromSQLResult(rs));
}
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
try {
db.close(rs, st, c);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
return out;
}
@Override
public int getNextTicketId() {
Connection c = null;
PreparedStatement st = null;
ResultSet rs = null;
int lastTicketId = 0;
try {
c = db.c();
st = c.prepareStatement("SELECT * FROM " + tableName + " ORDER BY id DESC LIMIT 1");
rs = st.executeQuery();
if (rs.next())
lastTicketId = rs.getInt("ticketId");
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
try {
db.close(rs, st, c);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
return lastTicketId + 1;
}
@Override
public void close() {
plugin.getLogger().info("Closing database connection...");
this.db.close();
}
private Object[] o(Object... objects){
return objects;
}
}

View File

@ -0,0 +1,98 @@
package fi.xeno.aquamarine.storage;
import fi.xeno.aquamarine.XTicketsPlugin;
import fi.xeno.aquamarine.util.XStoredLocation;
import fi.xeno.aquamarine.util.XTicket;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Consumer;
public abstract class XTicketDataStorage {
public abstract XTicket createTicket(Player player, String message);
public abstract void solveTicket(XTicket ticket, Player solver, String comment);
public abstract Optional<XTicket> getTicketByNumber(int id);
public abstract List<XTicket> getTickets();
public abstract List<XTicket> getWaitingTickets();
public abstract List<XTicket> getSolvedTickets();
public abstract List<XTicket> getWaitingNearbyTickets(XStoredLocation location, double radius);
public abstract List<XTicket> getWaitingTicketsBySender(UUID uuid);
public abstract List<XTicket> getTicketsBySender(UUID uuid);
public abstract List<XTicket> getTicketsBySolver(UUID uuid);
public abstract int getNextTicketId();
public abstract void close();
public void createTicketAsync(Player player, String message, Consumer<XTicket> callback) {
Bukkit.getScheduler().runTaskAsynchronously(XTicketsPlugin.getInstance(), () -> callback.accept(createTicket(player, message)));
}
public void solveTicketAsync(XTicket ticket, Player solver, String comment, Runnable callback) {
Bukkit.getScheduler().runTaskAsynchronously(XTicketsPlugin.getInstance(), () -> {
solveTicket(ticket, solver, comment);
callback.run();
});
}
public Optional<XTicket> solveTicket(int ticketId, Player solver, String comment) {
XTicket ticket = getTicketByNumber(ticketId).orElse(null);
if (ticket == null)
return Optional.empty();
solveTicket(ticket, solver, comment);
return Optional.of(ticket);
}
public void solveTicketAsync(int ticketId, Player solver, String comment, Consumer<Optional<XTicket>> callback) {
Bukkit.getScheduler().runTaskAsynchronously(XTicketsPlugin.getInstance(), () -> callback.accept(solveTicket(ticketId, solver, comment)));
}
public void getTicketByNumberAsync(int id, Consumer<Optional<XTicket>> callback) {
Bukkit.getScheduler().runTaskAsynchronously(XTicketsPlugin.getInstance(), () -> callback.accept(getTicketByNumber(id)));
}
public void getTicketsAsync(Consumer<List<XTicket>> callback) {
Bukkit.getScheduler().runTaskAsynchronously(XTicketsPlugin.getInstance(), () -> callback.accept(getTickets()));
}
public void getWaitingTicketsAsync(Consumer<List<XTicket>> callback) {
Bukkit.getScheduler().runTaskAsynchronously(XTicketsPlugin.getInstance(), () -> callback.accept(getWaitingTickets()));
}
public void getSolvedTicketsAsync(Consumer<List<XTicket>> callback) {
Bukkit.getScheduler().runTaskAsynchronously(XTicketsPlugin.getInstance(), () -> callback.accept(getSolvedTickets()));
}
public void getWaitingNearbyTicketsAsync(XStoredLocation location, double radius, Consumer<List<XTicket>> callback) {
Bukkit.getScheduler().runTaskAsynchronously(XTicketsPlugin.getInstance(), () -> callback.accept(getWaitingNearbyTickets(location, radius)));
}
public void getWaitingTicketsBySenderAsync(UUID uuid, Consumer<List<XTicket>> callback) {
Bukkit.getScheduler().runTaskAsynchronously(XTicketsPlugin.getInstance(), () -> callback.accept(getWaitingTicketsBySender(uuid)));
}
public void getTicketsBySenderAsync(UUID uuid, Consumer<List<XTicket>> callback) {
Bukkit.getScheduler().runTaskAsynchronously(XTicketsPlugin.getInstance(), () -> callback.accept(getTicketsBySender(uuid)));
}
public void getTicketsBySolverAsync(UUID uuid, Consumer<List<XTicket>> callback) {
Bukkit.getScheduler().runTaskAsynchronously(XTicketsPlugin.getInstance(), () -> callback.accept(getTicketsBySolver(uuid)));
}
public void getNextTicketIdAsync(Consumer<Integer> callback) {
Bukkit.getScheduler().runTaskAsynchronously(XTicketsPlugin.getInstance(), () -> callback.accept(getNextTicketId()));
}
}

View File

@ -0,0 +1,37 @@
package fi.xeno.aquamarine.util;
public class TimestampedValue<T> {
private long timestamp;
private T value;
public TimestampedValue(long timestamp, T value) {
this.timestamp = timestamp;
this.value = value;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
public void update() {
this.timestamp = System.currentTimeMillis();
}
public boolean isOlderThan(long millis) {
return System.currentTimeMillis() - this.timestamp >= millis;
}
}

View File

@ -0,0 +1,114 @@
package fi.xeno.aquamarine.util;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.entity.LivingEntity;
public class XStoredLocation {
private String world;
private double x;
private double y;
private double z;
private float pitch;
private float yaw;
public XStoredLocation(String world, double x, double y, double z, float pitch, float yaw) {
this.world = world;
this.x = x;
this.y = y;
this.z = z;
this.pitch = pitch;
this.yaw = yaw;
}
public XStoredLocation(Location loc) {
this.world = loc.getWorld().getName();
this.x = loc.getX();
this.y = loc.getY();
this.z = loc.getZ();
this.pitch = loc.getPitch();
this.yaw = loc.getYaw();
}
public Location toLocation() {
return new Location(Bukkit.getWorld(world), x, y, z);
}
public Block toBlock() {
return toLocation().getBlock();
}
public void teleport(LivingEntity ent) {
ent.teleport(this.toLocation());
}
public boolean isInRadius(XStoredLocation otherLocation, double radius) {
double dx = otherLocation.getX() - this.getX();
double dy = otherLocation.getY() - this.getY();
double dz = otherLocation.getZ() - this.getZ();
return otherLocation.getWorld().equals(this.getWorld())
&& Math.sqrt(dx*dx + dy*dy + dz*dz) <= radius;
}
public String getWorld() {
return world;
}
public double getX() {
return x;
}
public double getY() {
return y;
}
public double getZ() {
return z;
}
public float getPitch() {
return pitch;
}
public float getYaw() {
return yaw;
}
@Override
public String toString() {
return world + "/" + x + "/" + y + "/" + z + "/" + pitch + "/" + yaw;
}
public String toReadable() {
return world + "/" + ((int)x) + "/" + ((int)y) + "/" + ((int)z);
}
public static XStoredLocation fromString(String s) {
String[] pts = s.split("\\/");
if (pts.length < 4) {
throw new RuntimeException("Invalid location string: '" + s + "'");
}
String worldName = pts[0];
double x = Double.parseDouble(pts[1]);
double y = Double.parseDouble(pts[2]);
double z = Double.parseDouble(pts[3]);
float pitch = Float.parseFloat(pts[4]);
float yaw = Float.parseFloat(pts[5]);
return new XStoredLocation(worldName, x, y, z, pitch, yaw);
}
}

View File

@ -0,0 +1,230 @@
package fi.xeno.aquamarine.util;
import com.google.gson.JsonObject;
import fi.xeno.aquamarine.XText;
import fi.xeno.aquamarine.XTicketsPlugin;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.ComponentBuilder;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.entity.Player;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.UUID;
public class XTicket {
private int id;
private long timestamp;
private UUID sentByUuid;
private String sentByName;
private XStoredLocation location;
private String message;
private UUID solvedByUuid;
private String solvedByName;
private String solveComment;
private boolean isSolved;
private long timeSolved;
public XTicket(int id, long timestamp, UUID sentByUuid, String sentByName, XStoredLocation location, String message) {
this.id = id;
this.timestamp = timestamp;
this.sentByUuid = sentByUuid;
this.sentByName = sentByName;
this.location = location;
this.message = message;
this.solvedByUuid = null;
this.solvedByName = null;
this.isSolved = false;
this.timeSolved = -1;
}
public XTicket(int id, long timestamp, UUID sentByUuid, String sentByName, XStoredLocation location, String message, UUID solvedByUuid, String solvedByName, String solveComment, boolean isSolved, long timeSolved) {
this.id = id;
this.timestamp = timestamp;
this.sentByUuid = sentByUuid;
this.sentByName = sentByName;
this.location = location;
this.message = message;
this.solvedByUuid = solvedByUuid;
this.solvedByName = solvedByName;
this.solveComment = solveComment;
this.isSolved = isSolved;
this.timeSolved = timeSolved;
}
public static XTicket asPlayer(int id, Player player, String message) {
return new XTicket(id, System.currentTimeMillis(), player.getUniqueId(), player.getName(), new XStoredLocation(player.getLocation()), message);
}
public int getId() {
return id;
}
public long getTimestamp() {
return timestamp;
}
public UUID getSentByUuid() {
return sentByUuid;
}
public String getSentByName() {
return sentByName;
}
public XStoredLocation getLocation() {
return location;
}
public String getMessage() {
return message;
}
public UUID getSolvedByUuid() {
return solvedByUuid;
}
public String getSolvedByName() {
return solvedByName;
}
public boolean isSolved() {
return isSolved;
}
public long getTimeSolved() {
return timeSolved;
}
public void setSolvedByUuid(UUID solvedByUuid) {
this.solvedByUuid = solvedByUuid;
}
public void setSolvedByName(String solvedByName) {
this.solvedByName = solvedByName;
}
public void setSolveComment(String solveComment) {
this.solveComment = solveComment;
}
public void setSolved(boolean solved) {
isSolved = solved;
}
public void setTimeSolved(long timeSolved) {
this.timeSolved = timeSolved;
}
public BaseComponent[] renderChatPreview(boolean withButtons) {
ComponentBuilder out = new ComponentBuilder();
XTicketsPlugin plugin = XTicketsPlugin.getInstance();
if (withButtons) {
out.append(XText.commandButton("§7[§e»§7] ", "§e" + XTicketsPlugin.getInstance().lang("ticket-preview-teleport"), "/xt goto " + this.getId()));
out.append(XText.commandButton("§7[§a✔§7] ", "§a" + XTicketsPlugin.getInstance().lang("ticket-preview-solve"), "/xt solve " + this.getId()));
}
out.append(TextComponent.fromLegacyText("§b#" + this.getId() + " §f" + this.getSentByName() + " §7"));
String previewString = this.message.length() > 40 ? this.message.substring(0, 40).trim() + "..." : this.message;
String solvedString = "";
if (this.isSolved) {
solvedString = String.format(plugin.lang("ticket-hover-solver"), this.solvedByName) + "\n" +
String.format(plugin.lang("ticket-hover-solved-at"), plugin.formatTimestamp(this.timeSolved)) + "\n" +
plugin.lang("ticket-hover-comment") + "§f§o" + XText.wordWrap(this.solveComment, 40);
}
String hoverString = plugin.lang("ticket-hover-title").replace("%ticketId%", ""+this.id) + "\n" +
String.format(plugin.lang("ticket-hover-sender"), this.sentByName) + "\n" +
String.format(plugin.lang("ticket-hover-timestamp"), plugin.formatTimestamp(this.timestamp)) + "\n" +
String.format(plugin.lang("ticket-hover-location"), this.location.toReadable() + "\n" +
solvedString + "\n\n" +
"§f§o" + XText.wordWrap(this.message, 40));
out.append(XText.hoverText("§7" + previewString, hoverString));
return out.create();
}
public JsonObject toJson() {
JsonObject out = new JsonObject();
out.addProperty("id", this.id);
out.addProperty("timestamp", this.timestamp);
out.addProperty("sentByUuid", this.sentByUuid.toString());
out.addProperty("sentByName", this.sentByName);
out.addProperty("location", this.location.toString());
out.addProperty("message", this.message);
out.addProperty("isSolved", this.isSolved);
if (this.isSolved) {
out.addProperty("solvedByUuid", this.solvedByUuid.toString());
out.addProperty("solvedByName", this.solvedByName);
out.addProperty("solveComment", this.solveComment);
out.addProperty("timeSolved", this.timeSolved);
}
return out;
}
public static XTicket fromSQLResult(ResultSet rs) throws SQLException {
boolean isSolved = rs.getInt("isSolved") == 1;
return new XTicket(
rs.getInt("ticketId"),
rs.getLong("timestamp"),
UUID.fromString(rs.getString("sentByUuid")),
rs.getString("sentByName"),
XStoredLocation.fromString(rs.getString("location")),
rs.getString("message"),
isSolved ? UUID.fromString(rs.getString("solvedByUuid")) : null,
isSolved ? rs.getString("solvedByName") : null,
isSolved ? rs.getString("solveComment") : null,
isSolved,
isSolved ? rs.getLong("timeSolved") : -1L
);
}
public static XTicket fromJson(JsonObject o) {
boolean isSolved = o.has("isSolved") && o.get("isSolved").getAsBoolean();
return new XTicket(
o.get("id").getAsInt(),
o.get("timestamp").getAsLong(),
UUID.fromString(o.get("sentByUuid").getAsString()),
o.get("sentByName").getAsString(),
XStoredLocation.fromString(o.get("location").getAsString()),
o.get("message").getAsString(),
isSolved ? UUID.fromString(o.get("solvedByUuid").getAsString()) : null,
isSolved ? o.get("solvedByName").getAsString() : null,
isSolved ? o.get("solveComment").getAsString() : null,
isSolved,
isSolved ? o.get("timeSolved").getAsLong() : -1L
);
}
}