diff --git a/build-number.txt b/build-number.txt index 81187442..832d4caf 100644 --- a/build-number.txt +++ b/build-number.txt @@ -1 +1 @@ -1235 \ No newline at end of file +1256 \ No newline at end of file diff --git a/build.gradle b/build.gradle index 43dc097d..80e6b17f 100644 --- a/build.gradle +++ b/build.gradle @@ -46,6 +46,7 @@ dependencies { implementation 'com.google.code.gson:gson:2.11.0' implementation 'com.google.guava:guava:31.1-jre' implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.2' + implementation 'com.mysql:mysql-connector-j:9.1.0' implementation 'org.jline:jline:3.23.0' implementation 'ch.qos.logback:logback-classic:1.5.11' implementation 'com.github.pircbotx:pircbotx:2.3.1' diff --git a/src/main/java/me/chayapak1/chomens_bot/Bot.java b/src/main/java/me/chayapak1/chomens_bot/Bot.java index 3981e18e..5f0c414a 100644 --- a/src/main/java/me/chayapak1/chomens_bot/Bot.java +++ b/src/main/java/me/chayapak1/chomens_bot/Bot.java @@ -61,6 +61,7 @@ public class Bot { public ChatPlugin chat; public CommandSpyPlugin commandSpy; public PositionPlugin position; + public DatabasePlugin database; public ServerPluginsManagerPlugin serverPluginsManager; public SelfCarePlugin selfCare; public QueryPlugin query; @@ -93,7 +94,7 @@ public class Bot { public FormatCheckerPlugin formatChecker; public ClearChatNameAnnouncerPlugin clearChatNameAnnouncer; public WhitelistPlugin whitelist; - public PlayersPersistentDataPlugin playersPersistent; + public PlayersDatabasePlugin playersDatabase; public IPFilterPlugin ipFilter; public Bot (Configuration.BotOption botOption, List bots, Configuration config) { @@ -144,7 +145,7 @@ public class Bot { this.formatChecker = new FormatCheckerPlugin(this); this.clearChatNameAnnouncer = new ClearChatNameAnnouncerPlugin(this); this.whitelist = new WhitelistPlugin(this); - this.playersPersistent = new PlayersPersistentDataPlugin(this); + this.playersDatabase = new PlayersDatabasePlugin(this); this.ipFilter = new IPFilterPlugin(this); for (Listener listener : listeners) listener.loadedPlugins(); diff --git a/src/main/java/me/chayapak1/chomens_bot/Configuration.java b/src/main/java/me/chayapak1/chomens_bot/Configuration.java index a5da14f5..cdfe3e92 100644 --- a/src/main/java/me/chayapak1/chomens_bot/Configuration.java +++ b/src/main/java/me/chayapak1/chomens_bot/Configuration.java @@ -14,6 +14,8 @@ public class Configuration { public Keys keys = new Keys(); public Backup backup = new Backup(); + public Database database = new Database(); + public String weatherApiKey; public String bossBarNamespace = "chomens_bot"; @@ -57,6 +59,13 @@ public class Configuration { public int failTimes = 2; } + public static class Database { + public boolean enabled = false; + public String address = "localhost"; + public String username = "chomens_bot"; + public String password = "123456"; + } + public static class Keys { public String trustedKey; public String adminKey; diff --git a/src/main/java/me/chayapak1/chomens_bot/Main.java b/src/main/java/me/chayapak1/chomens_bot/Main.java index 3589be7b..3e3e385a 100644 --- a/src/main/java/me/chayapak1/chomens_bot/Main.java +++ b/src/main/java/me/chayapak1/chomens_bot/Main.java @@ -1,10 +1,7 @@ package me.chayapak1.chomens_bot; import com.google.common.util.concurrent.ThreadFactoryBuilder; -import me.chayapak1.chomens_bot.plugins.ConsolePlugin; -import me.chayapak1.chomens_bot.plugins.DiscordPlugin; -import me.chayapak1.chomens_bot.plugins.IRCPlugin; -import me.chayapak1.chomens_bot.plugins.LoggerPlugin; +import me.chayapak1.chomens_bot.plugins.*; import me.chayapak1.chomens_bot.util.*; import net.dv8tion.jda.api.requests.restaction.MessageCreateAction; import org.yaml.snakeyaml.LoaderOptions; @@ -42,6 +39,7 @@ public class Main { private static int backupFailTimes = 0; + public static DatabasePlugin database; private static DiscordPlugin discord; public static void main(String[] args) throws IOException { @@ -123,11 +121,10 @@ public class Main { bots.add(bot); } - // initialize util classes and plugins - PersistentDataUtilities.init(); - + // initialize plugins new ConsolePlugin(); LoggerPlugin.init(); + if (config.database.enabled) database = new DatabasePlugin(config); if (config.discord.enabled) discord = new DiscordPlugin(config); if (config.irc.enabled) new IRCPlugin(config); @@ -149,8 +146,6 @@ public class Main { executor.shutdown(); - PersistentDataUtilities.stop(); - executorService.shutdown(); try { diff --git a/src/main/java/me/chayapak1/chomens_bot/commands/FilterCommand.java b/src/main/java/me/chayapak1/chomens_bot/commands/FilterCommand.java index 1f1d6c70..b6291d38 100644 --- a/src/main/java/me/chayapak1/chomens_bot/commands/FilterCommand.java +++ b/src/main/java/me/chayapak1/chomens_bot/commands/FilterCommand.java @@ -1,14 +1,12 @@ package me.chayapak1.chomens_bot.commands; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import me.chayapak1.chomens_bot.Bot; import me.chayapak1.chomens_bot.command.Command; import me.chayapak1.chomens_bot.command.CommandContext; import me.chayapak1.chomens_bot.command.CommandException; import me.chayapak1.chomens_bot.command.TrustLevel; import me.chayapak1.chomens_bot.data.FilteredPlayer; +import me.chayapak1.chomens_bot.plugins.DatabasePlugin; import me.chayapak1.chomens_bot.plugins.FilterPlugin; import me.chayapak1.chomens_bot.util.ColorUtilities; import net.kyori.adventure.text.Component; @@ -46,7 +44,7 @@ public class FilterCommand extends Command { boolean ignoreCase = false; boolean regex = false; - String action = context.getString(false, true); + String action = context.getString(false, true, true); // run 2 times. for example `*filter -ignorecase -regex add test` will be both accepted for (int i = 0; i < 2; i++) { @@ -59,13 +57,14 @@ public class FilterCommand extends Command { } } - final ObjectMapper objectMapper = FilterPlugin.objectMapper; - switch (action) { case "add" -> { final String player = context.getString(true, true); - bot.filter.add(player, regex, ignoreCase); + final boolean finalRegex = regex; + final boolean finalIgnoreCase = ignoreCase; + + DatabasePlugin.executorService.submit(() -> bot.filter.add(player, finalRegex, finalIgnoreCase)); return Component.translatable( "Added %s to the filters", Component.text(player).color(ColorUtilities.getColorByString(bot.config.colorPalette.username)) @@ -74,23 +73,23 @@ public class FilterCommand extends Command { case "remove" -> { context.checkOverloadArgs(2); - try { - final int index = context.getInteger(true); + final int index = context.getInteger(true); - final FilteredPlayer removed = bot.filter.remove(index); + final FilteredPlayer player = FilterPlugin.localList.get(index); - return Component.translatable( - "Removed %s from the filters", - Component.text(removed.playerName).color(ColorUtilities.getColorByString(bot.config.colorPalette.username)) - ).color(ColorUtilities.getColorByString(bot.config.colorPalette.defaultColor)); - } catch (IndexOutOfBoundsException | IllegalArgumentException | NullPointerException ignored) { - throw new CommandException(Component.text("Invalid index")); - } + if (player == null) throw new CommandException(Component.text("Invalid index")); + + DatabasePlugin.executorService.submit(() -> bot.filter.remove(player.playerName)); + + return Component.translatable( + "Removed %s from the filters", + Component.text(player.playerName).color(ColorUtilities.getColorByString(bot.config.colorPalette.username)) + ).color(ColorUtilities.getColorByString(bot.config.colorPalette.defaultColor)); } case "clear" -> { context.checkOverloadArgs(1); - bot.filter.clear(); + DatabasePlugin.executorService.submit(() -> bot.filter.clear()); return Component.text("Cleared the filter").color(ColorUtilities.getColorByString(bot.config.colorPalette.defaultColor)); } case "list" -> { @@ -99,42 +98,36 @@ public class FilterCommand extends Command { final List filtersComponents = new ArrayList<>(); int index = 0; - for (JsonNode playerElement : FilterPlugin.filteredPlayers.deepCopy()) { - try { - final FilteredPlayer player = objectMapper.treeToValue(playerElement, FilteredPlayer.class); + for (FilteredPlayer player : FilterPlugin.localList) { + Component options = Component.empty().color(NamedTextColor.DARK_GRAY); - Component options = Component.empty().color(NamedTextColor.DARK_GRAY); + if (player.ignoreCase || player.regex) { + final List args = new ArrayList<>(); - if (player.ignoreCase || player.regex) { - final List args = new ArrayList<>(); + if (player.ignoreCase) args.add(Component.text("ignore case")); + if (player.regex) args.add(Component.text("regex")); - if (player.ignoreCase) args.add(Component.text("ignore case")); - if (player.regex) args.add(Component.text("regex")); - - options = options.append(Component.text("(")); - options = options.append(Component.join(JoinConfiguration.commas(true), args).color(ColorUtilities.getColorByString(bot.config.colorPalette.string))); - options = options.append(Component.text(")")); - } - - filtersComponents.add( - Component.translatable( - "%s › %s %s", - Component.text(index).color(ColorUtilities.getColorByString(bot.config.colorPalette.number)), - Component.text(player.playerName).color(ColorUtilities.getColorByString(bot.config.colorPalette.username)), - options - ).color(NamedTextColor.DARK_GRAY) - ); - - index++; - } catch (JsonProcessingException e) { - e.printStackTrace(); + options = options.append(Component.text("(")); + options = options.append(Component.join(JoinConfiguration.commas(true), args).color(ColorUtilities.getColorByString(bot.config.colorPalette.string))); + options = options.append(Component.text(")")); } + + filtersComponents.add( + Component.translatable( + "%s › %s %s", + Component.text(index).color(ColorUtilities.getColorByString(bot.config.colorPalette.number)), + Component.text(player.playerName).color(ColorUtilities.getColorByString(bot.config.colorPalette.username)), + options + ).color(NamedTextColor.DARK_GRAY) + ); + + index++; } return Component.empty() .append(Component.text("Filtered players ").color(NamedTextColor.GREEN)) .append(Component.text("(").color(NamedTextColor.DARK_GRAY)) - .append(Component.text(FilterPlugin.filteredPlayers.size()).color(NamedTextColor.GRAY)) + .append(Component.text(FilterPlugin.localList.size()).color(NamedTextColor.GRAY)) .append(Component.text(")").color(NamedTextColor.DARK_GRAY)) .append(Component.newline()) .append( diff --git a/src/main/java/me/chayapak1/chomens_bot/commands/FindAltsCommand.java b/src/main/java/me/chayapak1/chomens_bot/commands/FindAltsCommand.java index c7e73f71..22d2ba5c 100644 --- a/src/main/java/me/chayapak1/chomens_bot/commands/FindAltsCommand.java +++ b/src/main/java/me/chayapak1/chomens_bot/commands/FindAltsCommand.java @@ -1,19 +1,16 @@ package me.chayapak1.chomens_bot.commands; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import me.chayapak1.chomens_bot.Bot; import me.chayapak1.chomens_bot.command.Command; import me.chayapak1.chomens_bot.command.CommandContext; +import me.chayapak1.chomens_bot.command.CommandException; import me.chayapak1.chomens_bot.command.TrustLevel; import me.chayapak1.chomens_bot.data.PlayerEntry; -import me.chayapak1.chomens_bot.plugins.PlayersPersistentDataPlugin; import me.chayapak1.chomens_bot.util.ColorUtilities; import net.kyori.adventure.text.Component; -import java.util.Map; +import java.util.List; import java.util.concurrent.CompletableFuture; -import java.util.stream.Stream; public class FindAltsCommand extends Command { public FindAltsCommand() { @@ -31,6 +28,8 @@ public class FindAltsCommand extends Command { public Component execute(CommandContext context) throws Exception { final Bot bot = context.bot; + if (bot.database == null) throw new CommandException(Component.text("Database is not enabled in the bot's config")); + final String player = context.getString(true, true); final PlayerEntry playerEntry = bot.players.getEntry(player); @@ -52,27 +51,7 @@ public class FindAltsCommand extends Command { } private Component handle (Bot bot, String targetIP, boolean argumentIsIP, String player) { - PlayersPersistentDataPlugin.lock.lock(); - - final Stream matches = PlayersPersistentDataPlugin.playersObject.deepCopy().properties() - .stream() - .filter( - entry -> { - final ObjectNode ipsObject = (ObjectNode) entry - .getValue().get("ips"); - - if (ipsObject == null || ipsObject.isNull()) return false; - - final JsonNode currentServerIP = ipsObject.get(bot.host + ":" + bot.port); - - if (currentServerIP == null || currentServerIP.isNull()) return false; - - return currentServerIP.asText().equals(targetIP); - } - ) - .map(Map.Entry::getKey); - - PlayersPersistentDataPlugin.lock.unlock(); + final List alts = bot.playersDatabase.findPlayerAlts(targetIP); Component component = Component .translatable("Possible alts for the %s %s:") @@ -84,11 +63,11 @@ public class FindAltsCommand extends Command { .appendNewline(); int i = 0; - for (String name : matches.toList()) { + for (String username : alts) { component = component .append( Component - .text(name) + .text(username) .color((i++ & 1) == 0 ? ColorUtilities.getColorByString(bot.config.colorPalette.primary) : ColorUtilities.getColorByString(bot.config.colorPalette.secondary)) ) .appendSpace(); diff --git a/src/main/java/me/chayapak1/chomens_bot/commands/IPFilterCommand.java b/src/main/java/me/chayapak1/chomens_bot/commands/IPFilterCommand.java index 791ff606..952696c4 100644 --- a/src/main/java/me/chayapak1/chomens_bot/commands/IPFilterCommand.java +++ b/src/main/java/me/chayapak1/chomens_bot/commands/IPFilterCommand.java @@ -1,11 +1,11 @@ package me.chayapak1.chomens_bot.commands; -import com.fasterxml.jackson.databind.JsonNode; import me.chayapak1.chomens_bot.Bot; import me.chayapak1.chomens_bot.command.Command; import me.chayapak1.chomens_bot.command.CommandContext; import me.chayapak1.chomens_bot.command.CommandException; import me.chayapak1.chomens_bot.command.TrustLevel; +import me.chayapak1.chomens_bot.plugins.DatabasePlugin; import me.chayapak1.chomens_bot.plugins.IPFilterPlugin; import me.chayapak1.chomens_bot.util.ColorUtilities; import net.kyori.adventure.text.Component; @@ -19,7 +19,7 @@ public class IPFilterCommand extends Command { public IPFilterCommand() { super( "ipfilter", - "Filter IPs", + "Filters IPs", new String[] { "add ", "remove ", @@ -43,7 +43,7 @@ public class IPFilterCommand extends Command { case "add" -> { final String ip = context.getString(true, true); - bot.ipFilter.add(ip); + DatabasePlugin.executorService.submit(() -> bot.ipFilter.add(ip)); return Component.translatable( "Added %s to the filters", Component.text(ip).color(ColorUtilities.getColorByString(bot.config.colorPalette.username)) @@ -52,23 +52,23 @@ public class IPFilterCommand extends Command { case "remove" -> { context.checkOverloadArgs(2); - try { - final int index = context.getInteger(true); + final int index = context.getInteger(true); - final String removed = bot.ipFilter.remove(index); + final String targetIP = IPFilterPlugin.localList.get(index); - return Component.translatable( - "Removed %s from the filters", - Component.text(removed).color(ColorUtilities.getColorByString(bot.config.colorPalette.username)) - ).color(ColorUtilities.getColorByString(bot.config.colorPalette.defaultColor)); - } catch (IndexOutOfBoundsException | IllegalArgumentException | NullPointerException ignored) { - throw new CommandException(Component.text("Invalid index")); - } + if (targetIP == null) throw new CommandException(Component.text("Invalid index")); + + DatabasePlugin.executorService.submit(() -> bot.ipFilter.remove(targetIP)); + + return Component.translatable( + "Removed %s from the filters", + Component.text(targetIP).color(ColorUtilities.getColorByString(bot.config.colorPalette.username)) + ).color(ColorUtilities.getColorByString(bot.config.colorPalette.defaultColor)); } case "clear" -> { context.checkOverloadArgs(1); - bot.ipFilter.clear(); + DatabasePlugin.executorService.submit(() -> bot.ipFilter.clear()); return Component.text("Cleared the filter").color(ColorUtilities.getColorByString(bot.config.colorPalette.defaultColor)); } case "list" -> { @@ -77,12 +77,12 @@ public class IPFilterCommand extends Command { final List filtersComponents = new ArrayList<>(); int index = 0; - for (JsonNode playerElement : IPFilterPlugin.filteredIPs.deepCopy()) { + for (String ip : IPFilterPlugin.localList) { filtersComponents.add( Component.translatable( "%s › %s", Component.text(index).color(ColorUtilities.getColorByString(bot.config.colorPalette.number)), - Component.text(playerElement.asText()).color(ColorUtilities.getColorByString(bot.config.colorPalette.username)) + Component.text(ip).color(ColorUtilities.getColorByString(bot.config.colorPalette.username)) ).color(NamedTextColor.DARK_GRAY) ); @@ -92,16 +92,14 @@ public class IPFilterCommand extends Command { return Component.empty() .append(Component.text("Filtered IPs ").color(NamedTextColor.GREEN)) .append(Component.text("(").color(NamedTextColor.DARK_GRAY)) - .append(Component.text(IPFilterPlugin.filteredIPs.size()).color(NamedTextColor.GRAY)) + .append(Component.text(IPFilterPlugin.localList.size()).color(NamedTextColor.GRAY)) .append(Component.text(")").color(NamedTextColor.DARK_GRAY)) .append(Component.newline()) .append( Component.join(JoinConfiguration.newlines(), filtersComponents) ); } - default -> { - throw new CommandException(Component.text("Invalid action")); - } + default -> throw new CommandException(Component.text("Invalid action")); } } } diff --git a/src/main/java/me/chayapak1/chomens_bot/commands/MailCommand.java b/src/main/java/me/chayapak1/chomens_bot/commands/MailCommand.java index 98b814f7..8a65c0e4 100644 --- a/src/main/java/me/chayapak1/chomens_bot/commands/MailCommand.java +++ b/src/main/java/me/chayapak1/chomens_bot/commands/MailCommand.java @@ -1,8 +1,5 @@ package me.chayapak1.chomens_bot.commands; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import me.chayapak1.chomens_bot.Bot; import me.chayapak1.chomens_bot.command.Command; import me.chayapak1.chomens_bot.command.CommandContext; @@ -10,9 +7,8 @@ import me.chayapak1.chomens_bot.command.CommandException; import me.chayapak1.chomens_bot.command.TrustLevel; import me.chayapak1.chomens_bot.data.Mail; import me.chayapak1.chomens_bot.data.PlayerEntry; -import me.chayapak1.chomens_bot.plugins.MailPlugin; +import me.chayapak1.chomens_bot.plugins.DatabasePlugin; import me.chayapak1.chomens_bot.util.ColorUtilities; -import me.chayapak1.chomens_bot.util.PersistentDataUtilities; import me.chayapak1.chomens_bot.util.UUIDUtilities; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.JoinConfiguration; @@ -43,28 +39,32 @@ public class MailCommand extends Command { public Component execute(CommandContext context) throws CommandException { final Bot bot = context.bot; - final PlayerEntry sender = context.sender; + if (bot.database == null) throw new CommandException(Component.text("Database is not enabled in the bot's config")); - final ObjectMapper objectMapper = MailPlugin.objectMapper; + final PlayerEntry sender = context.sender; // kinda messy ngl final String action = context.getString(false, true, true); switch (action) { - case "send" -> { - bot.mail.send( - new Mail( - sender.profile.getName(), - context.getString(false, true), - Instant.now().toEpochMilli(), - bot.host + ":" + bot.port, - context.getString(true, true) - ) - ); + case "send" -> DatabasePlugin.executorService.submit(() -> { + try { + bot.mail.send( + new Mail( + sender.profile.getName(), + context.getString(false, true), + Instant.now().toEpochMilli(), + bot.host + ":" + bot.port, + context.getString(true, true) + ) + ); - return Component.text("Mail sent!").color(ColorUtilities.getColorByString(bot.config.colorPalette.defaultColor)); - } + context.sendOutput(Component.text("Mail sent!").color(ColorUtilities.getColorByString(bot.config.colorPalette.defaultColor))); + } catch (CommandException e) { + context.sendOutput(e.message.color(NamedTextColor.RED)); + } + }); case "sendselecteditem" -> { context.checkOverloadArgs(2); @@ -79,55 +79,54 @@ public class MailCommand extends Command { throw new CommandException(Component.text("Player has no `message` NBT tag in their selected item's minecraft:custom_data")); } - bot.mail.send( - new Mail( - sender.profile.getName(), - context.getString(true, true), - Instant.now().toEpochMilli(), - bot.host + ":" + bot.port, - output - ) - ); + DatabasePlugin.executorService.submit(() -> { + try { + bot.mail.send( + new Mail( + sender.profile.getName(), + context.getString(true, true), + Instant.now().toEpochMilli(), + bot.host + ":" + bot.port, + output + ) + ); + + context.sendOutput( + Component.text("Mail sent!").color(ColorUtilities.getColorByString(bot.config.colorPalette.defaultColor)) + ); + } catch (CommandException e) { + context.sendOutput(e.message.color(NamedTextColor.RED)); + } + }); } catch (CommandException e) { context.sendOutput(e.message.color(NamedTextColor.RED)); - return output; + return null; } - context.sendOutput( - Component.text("Mail sent!").color(ColorUtilities.getColorByString(bot.config.colorPalette.defaultColor)) - ); - return output; }); } case "read" -> { context.checkOverloadArgs(1); - // TODO: use less for loops? - - int senderMailSize = 0; - for (JsonNode mailElement : MailPlugin.mails.deepCopy()) { - try { - final Mail mail = objectMapper.treeToValue(mailElement, Mail.class); + DatabasePlugin.executorService.submit(() -> { + final List mails = bot.mail.list(); + int senderMailSize = 0; + for (Mail mail : mails) { if (!mail.sentTo.equals(sender.profile.getName())) continue; senderMailSize++; - } catch (JsonProcessingException e) { - e.printStackTrace(); } - } - if (senderMailSize == 0) { - throw new CommandException(Component.text("You have no new mails")); - } + if (senderMailSize == 0) { + context.sendOutput(Component.text("You have no new mails").color(NamedTextColor.RED)); + } - final List mailsComponent = new ArrayList<>(); - - int count = 1; - for (JsonNode mailElement : MailPlugin.mails.deepCopy()) { - try { - final Mail mail = objectMapper.treeToValue(mailElement, Mail.class); + final int tempFinalSenderMailSize = senderMailSize; + final List mailsComponent = new ArrayList<>(); + int count = 1; + for (Mail mail : mails) { if (!mail.sentTo.equals(sender.profile.getName())) continue; final Instant instant = Instant.ofEpochMilli(mail.timeSent); @@ -166,39 +165,27 @@ public class MailCommand extends Command { ); count++; - } catch (JsonProcessingException e) { - e.printStackTrace(); } - } - final Component component = Component.empty() - .append(Component.text("Mails ").color(NamedTextColor.GREEN)) - .append(Component.text("(").color(NamedTextColor.DARK_GRAY)) - .append(Component.text(senderMailSize).color(NamedTextColor.GRAY)) - .append(Component.text(")").color(NamedTextColor.DARK_GRAY)) - .append(Component.newline()) - .append(Component.join(JoinConfiguration.newlines(), mailsComponent)); + final Component component = Component.empty() + .append(Component.text("Mails ").color(NamedTextColor.GREEN)) + .append(Component.text("(").color(NamedTextColor.DARK_GRAY)) + .append(Component.text(tempFinalSenderMailSize).color(NamedTextColor.GRAY)) + .append(Component.text(")").color(NamedTextColor.DARK_GRAY)) + .append(Component.newline()) + .append(Component.join(JoinConfiguration.newlines(), mailsComponent)); - if (context.inGame) { - bot.chat.tellraw( - component, - context.sender.profile.getId() - ); - } else { - context.sendOutput(component); - } - - for (JsonNode mailElement : MailPlugin.mails.deepCopy()) { - try { - final Mail mail = objectMapper.treeToValue(mailElement, Mail.class); - - if (mail.sentTo.equals(sender.profile.getName())) bot.mail.remove(mailElement); - } catch (JsonProcessingException e) { - e.printStackTrace(); + if (context.inGame) { + bot.chat.tellraw( + component, + context.sender.profile.getId() + ); + } else { + context.sendOutput(component); } - } - PersistentDataUtilities.put("mails", MailPlugin.mails); + bot.mail.clear(sender.profile.getName()); + }); } default -> context.sendOutput(Component.text("Invalid action").color(NamedTextColor.RED)); } diff --git a/src/main/java/me/chayapak1/chomens_bot/commands/SeenCommand.java b/src/main/java/me/chayapak1/chomens_bot/commands/SeenCommand.java index 2609b99c..7e1b4de3 100644 --- a/src/main/java/me/chayapak1/chomens_bot/commands/SeenCommand.java +++ b/src/main/java/me/chayapak1/chomens_bot/commands/SeenCommand.java @@ -7,7 +7,7 @@ import me.chayapak1.chomens_bot.command.Command; import me.chayapak1.chomens_bot.command.CommandContext; import me.chayapak1.chomens_bot.command.CommandException; import me.chayapak1.chomens_bot.command.TrustLevel; -import me.chayapak1.chomens_bot.plugins.PlayersPersistentDataPlugin; +import me.chayapak1.chomens_bot.plugins.DatabasePlugin; import me.chayapak1.chomens_bot.util.ColorUtilities; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.JoinConfiguration; @@ -33,9 +33,11 @@ public class SeenCommand extends Command { } @Override - public Component execute(CommandContext context) throws Exception { + public Component execute(CommandContext context) throws CommandException { final Bot bot = context.bot; + if (bot.database == null) throw new CommandException(Component.text("Database is not enabled in the bot's config")); + final String player = context.getString(true, true); boolean online = false; @@ -58,32 +60,44 @@ public class SeenCommand extends Command { if (online) return Component.join(JoinConfiguration.newlines(), onlineComponents); - final JsonNode playerElement = PlayersPersistentDataPlugin.playersObject.get(player); - if (playerElement == null || playerElement.isNull()) throw new CommandException(Component.translatable( - "%s was never seen", - Component.text(player) - )); + DatabasePlugin.executorService.submit(() -> { + try { + final JsonNode playerElement = bot.playersDatabase.getPlayerData(player); + if (playerElement == null) throw new CommandException(Component.translatable( + "%s was never seen", + Component.text(player) + )); - final ObjectNode lastSeen = (ObjectNode) playerElement.get("lastSeen"); + final ObjectNode lastSeen = (ObjectNode) playerElement.get("lastSeen"); - final JsonNode time = lastSeen.get("time"); + if (lastSeen == null || lastSeen.isNull()) throw new CommandException(Component.text("This player doesn't seem to have the last seen entry in the database for some reason.")); - if (time == null || time.isNull()) throw new CommandException(Component.text("This player does not have the `lastSeen.time` entry in the database for some reason.")); + final JsonNode time = lastSeen.get("time"); - final Instant instant = Instant.ofEpochMilli(time.asLong()); - final ZoneId zoneId = ZoneId.of("UTC"); // should i be doing this? - final OffsetDateTime localDateTime = OffsetDateTime.ofInstant(instant, zoneId); + if (time == null || time.isNull()) throw new CommandException(Component.text("This player doesn't seem to have the `lastSeen.time` entry in the database for some reason.")); - final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("EEEE, MMMM d, yyyy, hh:mm:ss a Z"); - final String formattedTime = localDateTime.format(formatter); + final Instant instant = Instant.ofEpochMilli(time.asLong()); + final ZoneId zoneId = ZoneId.of("UTC"); // should i be doing this? + final OffsetDateTime localDateTime = OffsetDateTime.ofInstant(instant, zoneId); - final String server = lastSeen.get("server").asText(); + final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("EEEE, MMMM d, yyyy, hh:mm:ss a Z"); + final String formattedTime = localDateTime.format(formatter); - return Component.translatable( - "%s was last seen at %s on %s", - Component.text(player).color(ColorUtilities.getColorByString(bot.config.colorPalette.username)), - Component.text(formattedTime).color(ColorUtilities.getColorByString(bot.config.colorPalette.string)), - Component.text(server).color(ColorUtilities.getColorByString(bot.config.colorPalette.string)) - ).color(ColorUtilities.getColorByString(bot.config.colorPalette.defaultColor)); + final String server = lastSeen.get("server").asText(); + + context.sendOutput(Component.translatable( + "%s was last seen at %s on %s", + Component.text(player).color(ColorUtilities.getColorByString(bot.config.colorPalette.username)), + Component.text(formattedTime).color(ColorUtilities.getColorByString(bot.config.colorPalette.string)), + Component.text(server).color(ColorUtilities.getColorByString(bot.config.colorPalette.string)) + ).color(ColorUtilities.getColorByString(bot.config.colorPalette.defaultColor))); + } catch (CommandException e) { + context.sendOutput(e.message.color(NamedTextColor.RED)); + } catch (Exception e) { + e.printStackTrace(); + } + }); + + return null; } } diff --git a/src/main/java/me/chayapak1/chomens_bot/plugins/DatabasePlugin.java b/src/main/java/me/chayapak1/chomens_bot/plugins/DatabasePlugin.java new file mode 100644 index 00000000..8e308879 --- /dev/null +++ b/src/main/java/me/chayapak1/chomens_bot/plugins/DatabasePlugin.java @@ -0,0 +1,53 @@ +package me.chayapak1.chomens_bot.plugins; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import me.chayapak1.chomens_bot.Bot; +import me.chayapak1.chomens_bot.Configuration; +import me.chayapak1.chomens_bot.Main; + +import java.sql.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class DatabasePlugin { + // is it OK to have a completely separate ExecutorService to do the executions? + public static final ExecutorService executorService = Executors.newFixedThreadPool( + Runtime.getRuntime().availableProcessors(), + new ThreadFactoryBuilder().setNameFormat("ExecutorService (database) #%d").build() + ); + + public Connection connection; + + public DatabasePlugin (Configuration config) { + try { + connection = DriverManager.getConnection( + "jdbc:mysql://" + config.database.address + "/chomens_bot", + config.database.username, + config.database.password + ); + } catch (SQLException e) { + e.printStackTrace(); + return; + } + + for (Bot bot : Main.bots) bot.database = this; + } + + public boolean execute (String query) throws SQLException { + final Statement statement = connection.createStatement(); + + return statement.execute(query); + } + + public ResultSet query (String query) throws SQLException { + final Statement statement = connection.createStatement(); + + return statement.executeQuery(query); + } + + public int update (String query) throws SQLException { + final Statement statement = connection.createStatement(); + + return statement.executeUpdate(query); + } +} diff --git a/src/main/java/me/chayapak1/chomens_bot/plugins/FilterPlugin.java b/src/main/java/me/chayapak1/chomens_bot/plugins/FilterPlugin.java index 7fc79ce7..f08b0895 100644 --- a/src/main/java/me/chayapak1/chomens_bot/plugins/FilterPlugin.java +++ b/src/main/java/me/chayapak1/chomens_bot/plugins/FilterPlugin.java @@ -1,34 +1,54 @@ package me.chayapak1.chomens_bot.plugins; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.JsonNodeFactory; import me.chayapak1.chomens_bot.Bot; +import me.chayapak1.chomens_bot.Main; import me.chayapak1.chomens_bot.data.FilteredPlayer; import me.chayapak1.chomens_bot.data.PlayerEntry; import me.chayapak1.chomens_bot.data.chat.PlayerMessage; import me.chayapak1.chomens_bot.util.ComponentUtilities; -import me.chayapak1.chomens_bot.util.PersistentDataUtilities; import me.chayapak1.chomens_bot.util.UUIDUtilities; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; public class FilterPlugin extends PlayersPlugin.Listener { - public static final ObjectMapper objectMapper = new ObjectMapper(); + private static final String CREATE_TABLE = "CREATE TABLE IF NOT EXISTS filters (name VARCHAR(255) PRIMARY KEY, regex BOOLEAN, ignoreCase BOOLEAN);"; + private static final String LIST_FILTERS = "SELECT * FROM filters;"; + private static final String INSERT_FILTER = "INSERT INTO filters (name, regex, ignoreCase) VALUES (?, ?, ?);"; + private static final String REMOVE_FILTER = "DELETE FROM filters WHERE name = ?;"; + private static final String CLEAR_FILTER = "DELETE FROM filters;"; + + public static List localList = new ArrayList<>(); + + static { + if (Main.database != null) { + DatabasePlugin.executorService.submit(() -> { + try { + Main.database.execute(CREATE_TABLE); + } catch (SQLException e) { + e.printStackTrace(); + } + }); + + Main.executor.scheduleAtFixedRate(FilterPlugin::list, 3, 5, TimeUnit.SECONDS); + } + } private final Bot bot; - public static ArrayNode filteredPlayers = (ArrayNode) PersistentDataUtilities.getOrDefault("filters", JsonNodeFactory.instance.arrayNode()); - public FilterPlugin (Bot bot) { this.bot = bot; + if (Main.database == null) return; + bot.players.addListener(this); bot.chat.addListener(new ChatPlugin.Listener() { @@ -50,16 +70,34 @@ public class FilterPlugin extends PlayersPlugin.Listener { bot.executor.scheduleAtFixedRate(this::kick, 0, 10, TimeUnit.SECONDS); } - private FilteredPlayer getPlayer (String name) { - for (JsonNode filteredPlayerElement : filteredPlayers.deepCopy()) { - try { - final FilteredPlayer filteredPlayer = objectMapper.treeToValue(filteredPlayerElement, FilteredPlayer.class); + public static List list () { + final List output = new ArrayList<>(); - if (matchesPlayer(name, filteredPlayer)) { - return filteredPlayer; - } - } catch (JsonProcessingException e) { - e.printStackTrace(); + try (ResultSet result = Main.database.query(LIST_FILTERS)) { + if (result == null) return output; + + while (result.next()) { + final FilteredPlayer filteredPlayer = new FilteredPlayer( + result.getString("name"), + result.getBoolean("regex"), + result.getBoolean("ignoreCase") + ); + + output.add(filteredPlayer); + } + } catch (SQLException e) { + e.printStackTrace(); + } + + localList = output; + + return output; + } + + private FilteredPlayer getPlayer (String name) { + for (FilteredPlayer filteredPlayer : localList) { + if (matchesPlayer(name, filteredPlayer)) { + return filteredPlayer; } } @@ -180,9 +218,19 @@ public class FilterPlugin extends PlayersPlugin.Listener { } public void add (String playerName, boolean regex, boolean ignoreCase) { - filteredPlayers.add(objectMapper.valueToTree(new FilteredPlayer(playerName, regex, ignoreCase))); + try { + final PreparedStatement statement = bot.database.connection.prepareStatement(INSERT_FILTER); - PersistentDataUtilities.put("filters", filteredPlayers); + statement.setString(1, playerName); + statement.setBoolean(2, regex); + statement.setBoolean(3, ignoreCase); + + statement.executeUpdate(); + + list(); + } catch (SQLException e) { + e.printStackTrace(); + } final PlayerEntry target = bot.players.getEntry(playerName); // fix not working for regex and ignorecase @@ -191,21 +239,27 @@ public class FilterPlugin extends PlayersPlugin.Listener { doAll(target); } - public FilteredPlayer remove (int index) { - final JsonNode element = filteredPlayers.remove(index); - - PersistentDataUtilities.put("filters", filteredPlayers); - + public void remove (String playerName) { try { - return objectMapper.treeToValue(element, FilteredPlayer.class); - } catch (JsonProcessingException e) { - return null; + final PreparedStatement statement = bot.database.connection.prepareStatement(REMOVE_FILTER); + + statement.setString(1, playerName); + + statement.executeUpdate(); + + list(); + } catch (SQLException e) { + e.printStackTrace(); } } public void clear () { - while (!filteredPlayers.isEmpty()) filteredPlayers.remove(0); + try { + bot.database.update(CLEAR_FILTER); - PersistentDataUtilities.put("filters", filteredPlayers); + list(); + } catch (SQLException e) { + e.printStackTrace(); + } } } diff --git a/src/main/java/me/chayapak1/chomens_bot/plugins/IPFilterPlugin.java b/src/main/java/me/chayapak1/chomens_bot/plugins/IPFilterPlugin.java index 55debb1e..25b3684f 100644 --- a/src/main/java/me/chayapak1/chomens_bot/plugins/IPFilterPlugin.java +++ b/src/main/java/me/chayapak1/chomens_bot/plugins/IPFilterPlugin.java @@ -1,23 +1,47 @@ package me.chayapak1.chomens_bot.plugins; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.JsonNodeFactory; import me.chayapak1.chomens_bot.Bot; +import me.chayapak1.chomens_bot.Main; import me.chayapak1.chomens_bot.data.PlayerEntry; -import me.chayapak1.chomens_bot.util.PersistentDataUtilities; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; public class IPFilterPlugin extends PlayersPlugin.Listener { - private final Bot bot; + private static final String CREATE_TABLE = "CREATE TABLE IF NOT EXISTS ipFilters (ip VARCHAR(255) PRIMARY KEY);"; + private static final String LIST_FILTERS = "SELECT * FROM ipFilters;"; + private static final String INSERT_FILTER = "INSERT INTO ipFilters (ip) VALUES (?);"; + private static final String REMOVE_FILTER = "DELETE FROM ipFilters WHERE ip = ?;"; + private static final String CLEAR_FILTER = "DELETE FROM ipFilters;"; - public static ArrayNode filteredIPs = (ArrayNode) PersistentDataUtilities.getOrDefault("ipFilters", JsonNodeFactory.instance.arrayNode()); + public static List localList = new ArrayList<>(); + + static { + if (Main.database != null) { + DatabasePlugin.executorService.submit(() -> { + try { + Main.database.execute(CREATE_TABLE); + } catch (SQLException e) { + e.printStackTrace(); + } + }); + + Main.executor.scheduleAtFixedRate(IPFilterPlugin::list, 3, 5, TimeUnit.SECONDS); + } + } + + private final Bot bot; public IPFilterPlugin (Bot bot) { this.bot = bot; + if (Main.database == null) return; + bot.players.addListener(this); bot.executor.scheduleAtFixedRate(this::checkAllPlayers, 0, 10, TimeUnit.SECONDS); @@ -25,7 +49,7 @@ public class IPFilterPlugin extends PlayersPlugin.Listener { @Override public void playerJoined(PlayerEntry target) { - if (filteredIPs.isEmpty()) return; + if (localList.isEmpty()) return; check(target); } @@ -44,39 +68,75 @@ public class IPFilterPlugin extends PlayersPlugin.Listener { }); } - public void add (String ip) { - filteredIPs.add(ip); - PersistentDataUtilities.put("ipFilters", filteredIPs); + + public static List list () { + final List output = new ArrayList<>(); + + try (ResultSet result = Main.database.query(LIST_FILTERS)) { + if (result == null) return output; + + while (result.next()) output.add(result.getString("ip")); + } catch (SQLException e) { + e.printStackTrace(); + } + + localList = output; + + return output; + } + + public void add (String ip) { + try { + final PreparedStatement statement = bot.database.connection.prepareStatement(INSERT_FILTER); + + statement.setString(1, ip); + + statement.executeUpdate(); + + list(); + } catch (SQLException e) { + e.printStackTrace(); + } checkAllPlayers(); } private void checkAllPlayers () { - if (filteredIPs.isEmpty()) return; + if (localList.isEmpty()) return; bot.executorService.submit(() -> { for (PlayerEntry entry : bot.players.list) check(entry); }); } - public String remove (int index) { - final JsonNode element = filteredIPs.remove(index); + public void remove (String ip) { + try { + final PreparedStatement statement = bot.database.connection.prepareStatement(REMOVE_FILTER); - PersistentDataUtilities.put("ipFilters", filteredIPs); + statement.setString(1, ip); - return element.asText(); + statement.executeUpdate(); + + list(); + } catch (SQLException e) { + e.printStackTrace(); + } } public void clear () { - while (!filteredIPs.isEmpty()) filteredIPs.remove(0); + try { + bot.database.update(CLEAR_FILTER); - PersistentDataUtilities.put("ipFilters", filteredIPs); + list(); + } catch (SQLException e) { + e.printStackTrace(); + } } private void handleIP (String ip, PlayerEntry entry) { - for (JsonNode element : filteredIPs.deepCopy()) { - if (!element.asText().equals(ip)) continue; + for (String eachIP : localList) { + if (!eachIP.equals(ip)) continue; if (entry.profile.getId().equals(bot.profile.getId())) continue; diff --git a/src/main/java/me/chayapak1/chomens_bot/plugins/MailPlugin.java b/src/main/java/me/chayapak1/chomens_bot/plugins/MailPlugin.java index 79f41cd7..feca1616 100644 --- a/src/main/java/me/chayapak1/chomens_bot/plugins/MailPlugin.java +++ b/src/main/java/me/chayapak1/chomens_bot/plugins/MailPlugin.java @@ -1,80 +1,126 @@ package me.chayapak1.chomens_bot.plugins; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.JsonNodeFactory; import me.chayapak1.chomens_bot.Bot; +import me.chayapak1.chomens_bot.Main; import me.chayapak1.chomens_bot.data.Mail; import me.chayapak1.chomens_bot.data.PlayerEntry; import me.chayapak1.chomens_bot.util.ColorUtilities; -import me.chayapak1.chomens_bot.util.PersistentDataUtilities; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + public class MailPlugin extends PlayersPlugin.Listener { - public static final ObjectMapper objectMapper = new ObjectMapper(); + private static final String CREATE_TABLE = "CREATE TABLE IF NOT EXISTS mails (sentBy VARCHAR(255), sentTo VARCHAR(255), timeSent BIGINT, server VARCHAR(255), contents TEXT);"; + private static final String INSERT_MAIL = "INSERT INTO mails (sentBy, sentTo, timeSent, server, contents) VALUES (?, ?, ?, ?, ?);"; + private static final String LIST_MAILS = "SELECT * FROM mails;"; + private static final String REMOVE_MAIL = "DELETE FROM mails WHERE sentTo = ?;"; + + static { + if (Main.database != null) { + DatabasePlugin.executorService.submit(() -> { + try { + Main.database.execute(CREATE_TABLE); + } catch (SQLException e) { + e.printStackTrace(); + } + }); + } + } private final Bot bot; - public static ArrayNode mails = (ArrayNode) PersistentDataUtilities.getOrDefault("mails", JsonNodeFactory.instance.arrayNode()); - public MailPlugin (Bot bot) { this.bot = bot; + if (Main.database == null) return; + bot.players.addListener(this); } @Override public void playerJoined(PlayerEntry target) { - final String name = target.profile.getName(); + DatabasePlugin.executorService.submit(() -> { + final String name = target.profile.getName(); - int sendToTargetSize = 0; + int sendToTargetSize = 0; - boolean shouldSend = false; - for (JsonNode mailNode : mails.deepCopy()) { - try { - final Mail mail = objectMapper.treeToValue(mailNode, Mail.class); + final List mails = list(); + for (Mail mail : mails) { if (!mail.sentTo.equals(name)) continue; - shouldSend = true; - sendToTargetSize++; - } catch (JsonProcessingException e) { - e.printStackTrace(); } - } - if (shouldSend) { - final Component component = Component.translatable( - "You have %s new mail%s!\n" + - "Do %s or %s to read", - Component.text(sendToTargetSize).color(NamedTextColor.GREEN), - Component.text((sendToTargetSize > 1) ? "s" : ""), - Component.text(bot.config.commandSpyPrefixes.get(0) + "mail read").color(ColorUtilities.getColorByString(bot.config.colorPalette.primary)), - Component.text(bot.config.prefixes.get(0) + "mail read").color(ColorUtilities.getColorByString(bot.config.colorPalette.primary)) - ).color(NamedTextColor.GOLD); + if (sendToTargetSize > 0) { + final Component component = Component.translatable( + "You have %s new mail%s!\n" + + "Run %s or %s to read", + Component.text(sendToTargetSize).color(NamedTextColor.GREEN), + Component.text((sendToTargetSize > 1) ? "s" : ""), + Component.text(bot.config.commandSpyPrefixes.get(0) + "mail read").color(ColorUtilities.getColorByString(bot.config.colorPalette.primary)), + Component.text(bot.config.prefixes.get(0) + "mail read").color(ColorUtilities.getColorByString(bot.config.colorPalette.primary)) + ).color(NamedTextColor.GOLD); - bot.chat.tellraw(component, target.profile.getId()); - } + bot.chat.tellraw(component, target.profile.getId()); + } + }); } public void send (Mail mail) { - mails.add(objectMapper.valueToTree(mail)); + try { + final PreparedStatement statement = bot.database.connection.prepareStatement(INSERT_MAIL); - PersistentDataUtilities.put("mails", mails); - } + statement.setString(1, mail.sentBy); + statement.setString(2, mail.sentTo); + statement.setLong(3, mail.timeSent); + statement.setString(4, mail.server); + statement.setString(5, mail.contents); - public void remove (JsonNode mail) { - for (int i = 0; i < mails.size(); i++) { - final JsonNode currentNode = mails.get(i); - - if (currentNode.equals(mail)) { - mails.remove(i); - break; - } + statement.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); } } + + public void clear (String sentTo) { + try { + final PreparedStatement statement = bot.database.connection.prepareStatement(REMOVE_MAIL); + + statement.setString(1, sentTo); + + statement.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public List list () { + final List output = new ArrayList<>(); + + try (ResultSet result = bot.database.query(LIST_MAILS)) { + if (result == null) return output; + + while (result.next()) { + final Mail mail = new Mail( + result.getString("sentBy"), + result.getString("sentTo"), + result.getLong("timeSent"), + result.getString("server"), + result.getString("contents") + ); + + output.add(mail); + } + } catch (SQLException e) { + e.printStackTrace(); + } + + return output; + } } diff --git a/src/main/java/me/chayapak1/chomens_bot/plugins/PlayersDatabasePlugin.java b/src/main/java/me/chayapak1/chomens_bot/plugins/PlayersDatabasePlugin.java new file mode 100644 index 00000000..d1bff53a --- /dev/null +++ b/src/main/java/me/chayapak1/chomens_bot/plugins/PlayersDatabasePlugin.java @@ -0,0 +1,163 @@ +package me.chayapak1.chomens_bot.plugins; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import me.chayapak1.chomens_bot.Bot; +import me.chayapak1.chomens_bot.Main; +import me.chayapak1.chomens_bot.data.PlayerEntry; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class PlayersDatabasePlugin extends PlayersPlugin.Listener { + private static final String CREATE_TABLE = "CREATE TABLE IF NOT EXISTS players (username VARCHAR(255) PRIMARY KEY, data JSON);"; + private static final String INSERT_PLAYER = "INSERT IGNORE INTO players (username, data) VALUES (?, ?);"; + private static final String UPDATE_PLAYER = "UPDATE players SET data = JSON_SET(data, ?, JSON_MERGE_PATCH(data -> ?, ?)) WHERE username = ?;"; + private static final String GET_DATA = "SELECT data FROM players WHERE username = ?;"; + private static final String FIND_ALTS = "SELECT username FROM players WHERE JSON_CONTAINS(data -> '$.ips', JSON_OBJECT(?, ?));"; + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private final Bot bot; + + static { + if (Main.database != null) { + DatabasePlugin.executorService.submit(() -> { + try { + Main.database.execute(CREATE_TABLE); + } catch (SQLException e) { + e.printStackTrace(); + } + }); + } + } + + public PlayersDatabasePlugin (Bot bot) { + this.bot = bot; + + if (Main.database == null) return; + + bot.players.addListener(this); + } + + public JsonNode getPlayerData (String username) { + try { + final PreparedStatement statement = bot.database.connection.prepareStatement(GET_DATA); + + statement.setString(1, username); + + final ResultSet result = statement.executeQuery(); + + // this will use only the first one in the output + result.next(); + final String stringJson = result.getString("data"); + + return objectMapper.readTree(stringJson); + } catch (SQLException | JsonProcessingException e) { + e.printStackTrace(); + return null; + } + } + + public List findPlayerAlts (String ip) { + try { + final List output = new ArrayList<>(); + + final PreparedStatement statement = bot.database.connection.prepareStatement(FIND_ALTS); + + statement.setString(1, bot.host + ":" + bot.port); + statement.setString(2, ip); + + final ResultSet result = statement.executeQuery(); + + while (result.next()) output.add(result.getString("username")); + + return output; + } catch (SQLException e) { + e.printStackTrace(); + return null; + } + } + + @Override + public void playerJoined (PlayerEntry target) { + DatabasePlugin.executorService.submit(() -> { + try { + final PreparedStatement insertPlayerStatement = bot.database.connection.prepareStatement(INSERT_PLAYER); + + insertPlayerStatement.setString(1, target.profile.getName()); + + final ObjectNode baseObject = JsonNodeFactory.instance.objectNode(); + baseObject.put("uuid", target.profile.getIdAsString()); + baseObject.set("ips", JsonNodeFactory.instance.objectNode()); + baseObject.set("lastSeen", JsonNodeFactory.instance.objectNode()); + + insertPlayerStatement.setString(2, objectMapper.writeValueAsString(baseObject)); + + insertPlayerStatement.executeUpdate(); + + final CompletableFuture future = bot.players.getPlayerIP(target); + + if (future == null) return; + + future.thenApplyAsync(output -> { + if (output == null) return null; + + try { + final PreparedStatement updatePlayerStatement = bot.database.connection.prepareStatement(UPDATE_PLAYER); + + updatePlayerStatement.setString(1, "$.ips"); + updatePlayerStatement.setString(2, "$.ips"); + + final ObjectNode ipsObject = JsonNodeFactory.instance.objectNode(); + ipsObject.put(bot.host + ":" + bot.port, output); + + updatePlayerStatement.setString(3, objectMapper.writeValueAsString(ipsObject)); + + updatePlayerStatement.setString(4, target.profile.getName()); + + updatePlayerStatement.executeUpdate(); + } catch (SQLException | JsonProcessingException e) { + e.printStackTrace(); + } + + return output; + }); + } catch (SQLException | JsonProcessingException e) { + e.printStackTrace(); + } + }); + } + + @Override + public void playerLeft(PlayerEntry target) { + DatabasePlugin.executorService.submit(() -> { + try { + final PreparedStatement updatePlayerStatement = bot.database.connection.prepareStatement(UPDATE_PLAYER); + + updatePlayerStatement.setString(1, "$.lastSeen"); + updatePlayerStatement.setString(2, "$.lastSeen"); + + final ObjectNode object = JsonNodeFactory.instance.objectNode(); + object.put("time", Instant.now().toEpochMilli()); + object.put("server", bot.host + ":" + bot.port); + + updatePlayerStatement.setString(3, objectMapper.writeValueAsString(object)); + + updatePlayerStatement.setString(4, target.profile.getName()); + + updatePlayerStatement.executeUpdate(); + } catch (SQLException | JsonProcessingException e) { + e.printStackTrace(); + } + }); + } +} diff --git a/src/main/java/me/chayapak1/chomens_bot/plugins/PlayersPersistentDataPlugin.java b/src/main/java/me/chayapak1/chomens_bot/plugins/PlayersPersistentDataPlugin.java deleted file mode 100644 index 30879bfe..00000000 --- a/src/main/java/me/chayapak1/chomens_bot/plugins/PlayersPersistentDataPlugin.java +++ /dev/null @@ -1,109 +0,0 @@ -package me.chayapak1.chomens_bot.plugins; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.JsonNodeFactory; -import com.fasterxml.jackson.databind.node.ObjectNode; -import me.chayapak1.chomens_bot.Bot; -import me.chayapak1.chomens_bot.data.PlayerEntry; -import me.chayapak1.chomens_bot.util.PersistentDataUtilities; - -import java.time.Instant; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.locks.ReentrantLock; - -// this is the most problematic plugin that breaks everything when it crashes -public class PlayersPersistentDataPlugin extends PlayersPlugin.Listener { - public static ObjectNode playersObject = (ObjectNode) PersistentDataUtilities.getOrDefault("players", JsonNodeFactory.instance.objectNode()); - - public static final ReentrantLock lock = new ReentrantLock(); - - private final Bot bot; - - public PlayersPersistentDataPlugin (Bot bot) { - this.bot = bot; - - bot.players.addListener(this); - } - - @Override - public void playerJoined(PlayerEntry target) { - try { - final JsonNode originalElement = playersObject.get(target.profile.getName()); - - ObjectNode object; - - if (originalElement == null || originalElement.isNull()) { - object = JsonNodeFactory.instance.objectNode(); - object.put("uuid", target.profile.getIdAsString()); - object.set("ips", JsonNodeFactory.instance.objectNode()); - } else if (originalElement instanceof ObjectNode) { - object = (ObjectNode) originalElement; - } else { - return; - } - - final CompletableFuture future = bot.players.getPlayerIP(target); - - if (future == null) { - setPersistentEntry(target, object); - return; - } - - future.thenApplyAsync(output -> { - if (output != null) { - ((ObjectNode) object.get("ips")).put(bot.host + ":" + bot.port, output); - } - - setPersistentEntry(target, object); - - return output; - }); - } catch (Exception e) { - e.printStackTrace(); - } - } - - // is this bad? - private void setPersistentEntry (PlayerEntry target, ObjectNode object) { - try { - lock.lock(); - - playersObject.set(getName(target), object); - - PersistentDataUtilities.put("players", playersObject); - - lock.unlock(); - } catch (Exception e) { - e.printStackTrace(); - } - } - - private String getName (PlayerEntry target) { - return bot.options.creayun ? target.profile.getName().replaceAll("§.", "") : target.profile.getName(); - } - - @Override - public void playerLeft(PlayerEntry target) { - bot.executorService.submit(() -> { - try { - lock.lock(); - - if (!playersObject.has(getName(target))) return; - - final ObjectNode player = (ObjectNode) playersObject.get(getName(target)); - - final ObjectNode object = JsonNodeFactory.instance.objectNode(); - object.put("time", Instant.now().toEpochMilli()); - object.put("server", bot.host + ":" + bot.port); - - player.set("lastSeen", object); - - PersistentDataUtilities.put("players", playersObject); - - lock.unlock(); - } catch (Exception e) { - e.printStackTrace(); - } - }); - } -} diff --git a/src/main/java/me/chayapak1/chomens_bot/util/PersistentDataUtilities.java b/src/main/java/me/chayapak1/chomens_bot/util/PersistentDataUtilities.java deleted file mode 100644 index ec24630d..00000000 --- a/src/main/java/me/chayapak1/chomens_bot/util/PersistentDataUtilities.java +++ /dev/null @@ -1,124 +0,0 @@ -package me.chayapak1.chomens_bot.util; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.JsonNodeFactory; -import com.fasterxml.jackson.databind.node.ObjectNode; -import me.chayapak1.chomens_bot.Main; - -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.ReentrantLock; - -public class PersistentDataUtilities { - private static final Path path = Path.of("persistent.json"); - - private static final ObjectMapper objectMapper = new ObjectMapper(); - - private static ObjectNode jsonObject = JsonNodeFactory.instance.objectNode(); - - private static final ReentrantLock lock = new ReentrantLock(); - - private static ScheduledFuture writeFuture; - - private static volatile boolean stopping = false; - - public static void init () { - lock.lock(); - - try { - if (Files.exists(path)) { - try (BufferedReader reader = Files.newBufferedReader(path)) { - jsonObject = (ObjectNode) objectMapper.readTree(reader); - } - } else { - Files.createFile(path); - } - } catch (IOException e) { - e.printStackTrace(); - } finally { - lock.unlock(); - } - - writeFuture = Main.executor.scheduleAtFixedRate(PersistentDataUtilities::writeToFile, 10, 30, TimeUnit.SECONDS); - } - - private static synchronized void writeToFile () { writeToFile(false); } - private static synchronized void writeToFile (boolean force) { - if (stopping && !force) return; - - ObjectNode safeCopy; - - lock.lock(); - - try { - safeCopy = jsonObject.deepCopy(); - } finally { - lock.unlock(); - } - - try (BufferedWriter writer = Files.newBufferedWriter(path, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { - writer.write(objectMapper.writeValueAsString(safeCopy)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - public static synchronized void stop () { - stopping = true; - - writeFuture.cancel(false); - - lock.lock(); - - try { - writeToFile(true); - } finally { - lock.unlock(); - } - } - - public static synchronized boolean has (String property) { - return jsonObject.has(property); - } - - public static synchronized JsonNode get (String property) { - return jsonObject.get(property); - } - - public static synchronized JsonNode getOrDefault (String property, JsonNode defaultElement) { - return has(property) ? get(property) : defaultElement; - } - - public static synchronized void put (String property, JsonNode value) { - lock.lock(); - - try { - jsonObject.set(property, value); - } finally { - lock.unlock(); - } - } - - public static synchronized void put (String property, String value) { - put(property, JsonNodeFactory.instance.textNode(value)); - } - - public static synchronized void put (String property, boolean value) { - put(property, JsonNodeFactory.instance.booleanNode(value)); - } - - public static synchronized void put (String property, int value) { - put(property, JsonNodeFactory.instance.numberNode(value)); - } - - public static synchronized void put (String property, char value) { - put(property, JsonNodeFactory.instance.textNode(String.valueOf(value))); - } -} diff --git a/src/main/resources/default-config.yml b/src/main/resources/default-config.yml index 6d706ecb..754ab3ff 100644 --- a/src/main/resources/default-config.yml +++ b/src/main/resources/default-config.yml @@ -23,6 +23,12 @@ backup: interval: 1000 # in milliseconds failTimes: 2 +database: + enabled: false + address: 'localhost' # jdbc:mysql://
/chomens_bot + username: 'chomens_bot' + password: '123456' + discord: enabled: false prefix: 'default!'