Initial implementations, still WIP
This commit is contained in:
parent
451451f05c
commit
459dbf80ec
39
config.yml
Normal file
39
config.yml
Normal 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
22
lang.yml
Normal 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:"
|
25
plugin.yml
25
plugin.yml
|
@ -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
|
||||
|
|
9
src/fi/xeno/aquamarine/AquamarinePermission.java
Normal file
9
src/fi/xeno/aquamarine/AquamarinePermission.java
Normal 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";
|
||||
|
||||
}
|
108
src/fi/xeno/aquamarine/XText.java
Normal file
108
src/fi/xeno/aquamarine/XText.java
Normal 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();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
118
src/fi/xeno/aquamarine/XTicketManager.java
Normal file
118
src/fi/xeno/aquamarine/XTicketManager.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
public void onDisable() {}
|
||||
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() {
|
||||
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)));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
79
src/fi/xeno/aquamarine/command/CommandTicket.java
Normal file
79
src/fi/xeno/aquamarine/command/CommandTicket.java
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
}
|
76
src/fi/xeno/aquamarine/command/CommandXt.java
Normal file
76
src/fi/xeno/aquamarine/command/CommandXt.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
97
src/fi/xeno/aquamarine/sql/XHikariDatabase.java
Normal file
97
src/fi/xeno/aquamarine/sql/XHikariDatabase.java
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
205
src/fi/xeno/aquamarine/storage/XFlatFileTicketDataStorage.java
Normal file
205
src/fi/xeno/aquamarine/storage/XFlatFileTicketDataStorage.java
Normal 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();
|
||||
}
|
||||
|
||||
}
|
126
src/fi/xeno/aquamarine/storage/XMemoryTicketDataStorage.java
Normal file
126
src/fi/xeno/aquamarine/storage/XMemoryTicketDataStorage.java
Normal 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() {}
|
||||
|
||||
}
|
292
src/fi/xeno/aquamarine/storage/XSQLTicketDataStorage.java
Normal file
292
src/fi/xeno/aquamarine/storage/XSQLTicketDataStorage.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
98
src/fi/xeno/aquamarine/storage/XTicketDataStorage.java
Normal file
98
src/fi/xeno/aquamarine/storage/XTicketDataStorage.java
Normal 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()));
|
||||
}
|
||||
|
||||
}
|
37
src/fi/xeno/aquamarine/util/TimestampedValue.java
Normal file
37
src/fi/xeno/aquamarine/util/TimestampedValue.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
114
src/fi/xeno/aquamarine/util/XStoredLocation.java
Normal file
114
src/fi/xeno/aquamarine/util/XStoredLocation.java
Normal 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);
|
||||
|
||||
}
|
||||
|
||||
}
|
230
src/fi/xeno/aquamarine/util/XTicket.java
Normal file
230
src/fi/xeno/aquamarine/util/XTicket.java
Normal 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
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user