diff --git a/build-number.txt b/build-number.txt index 7c0636fb..4e273fbd 100644 --- a/build-number.txt +++ b/build-number.txt @@ -1 +1 @@ -3558 \ No newline at end of file +3565 \ No newline at end of file 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 b643a55f..a0ad4a02 100644 --- a/src/main/java/me/chayapak1/chomens_bot/commands/FindAltsCommand.java +++ b/src/main/java/me/chayapak1/chomens_bot/commands/FindAltsCommand.java @@ -1,6 +1,6 @@ package me.chayapak1.chomens_bot.commands; -import com.fasterxml.jackson.databind.JsonNode; +import it.unimi.dsi.fastutil.Pair; import me.chayapak1.chomens_bot.Bot; import me.chayapak1.chomens_bot.Main; import me.chayapak1.chomens_bot.command.Command; @@ -14,7 +14,6 @@ import net.kyori.adventure.text.Component; import java.util.List; import java.util.Map; -import java.util.Optional; public class FindAltsCommand extends Command { // we allow both, since the flag used to be `allserver` @@ -54,9 +53,11 @@ public class FindAltsCommand extends Command { final String ipFromUsername; - if (playerInTheServer == null || playerInTheServer.persistingData.ip == null) + if (playerInTheServer == null || playerInTheServer.persistingData.ip == null) { ipFromUsername = bot.playersDatabase.getPlayerIP(player); - else ipFromUsername = playerInTheServer.persistingData.ip; + } else { + ipFromUsername = playerInTheServer.persistingData.ip; + } if (ipFromUsername == null) { context.sendOutput(handle(bot, player, player, allServer)); @@ -69,7 +70,7 @@ public class FindAltsCommand extends Command { } private Component handle (final Bot bot, final String targetIP, final String player, final boolean allServer) { - final Map altsMap = bot.playersDatabase.findPlayerAlts(targetIP, allServer, LIMIT); + final Map> altsMap = bot.playersDatabase.findPlayerAlts(targetIP, allServer, LIMIT); final Component playerComponent = Component.text(player, bot.colorPalette.username); @@ -84,28 +85,17 @@ public class FindAltsCommand extends Command { Component.translatable( "%s (%s)", playerComponent, - Component - .text(targetIP) - .color(bot.colorPalette.number) + Component.text(targetIP, bot.colorPalette.number) ) ) .appendNewline(); final List sorted = altsMap.entrySet().stream() - .limit(200) // only find 200 alts because more than this is simply too many .sorted((a, b) -> { - final JsonNode aTimeNode = Optional.ofNullable(a.getValue().get("lastSeen")) - .map(node -> node.get("time")) - .orElse(null); - final JsonNode bTimeNode = Optional.ofNullable(b.getValue().get("lastSeen")) - .map(node -> node.get("time")) - .orElse(null); + final long aTime = a.getValue().left(); + final long bTime = b.getValue().left(); - if (aTimeNode == null && bTimeNode == null) return 0; - if (aTimeNode == null) return 1; - if (bTimeNode == null) return -1; - - return Long.compare(bTimeNode.asLong(), aTimeNode.asLong()); + return Long.compare(bTime, aTime); }) .map(Map.Entry::getKey) .toList(); 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 4dc5ec97..60f84faa 100644 --- a/src/main/java/me/chayapak1/chomens_bot/commands/SeenCommand.java +++ b/src/main/java/me/chayapak1/chomens_bot/commands/SeenCommand.java @@ -1,7 +1,6 @@ package me.chayapak1.chomens_bot.commands; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; +import it.unimi.dsi.fastutil.Pair; import me.chayapak1.chomens_bot.Bot; import me.chayapak1.chomens_bot.Main; import me.chayapak1.chomens_bot.command.Command; @@ -62,30 +61,21 @@ public class SeenCommand extends Command { DatabasePlugin.EXECUTOR_SERVICE.execute(() -> { try { - final JsonNode playerElement = bot.playersDatabase.getPlayerData(player); - if (playerElement == null) throw new CommandException(Component.translatable( + final Pair pair = bot.playersDatabase.getPlayerLastSeen(player); + if (pair == null) throw new CommandException(Component.translatable( "commands.seen.error.never_seen", Component.text(player) )); - final ObjectNode lastSeen = (ObjectNode) playerElement.get("lastSeen"); - - if (lastSeen == null || lastSeen.isNull()) - throw new CommandException(Component.translatable("commands.seen.error.no_last_seen_entry")); - - final JsonNode time = lastSeen.get("time"); - - if (time == null || time.isNull()) - throw new CommandException(Component.translatable("commands.seen.error.no_time_entry")); + final long time = pair.left(); + final String server = pair.right(); final String formattedTime = TimeUtilities.formatTime( - time.asLong(), + time, "EEEE, MMMM d, yyyy, hh:mm:ss a Z", ZoneId.of("UTC") ); - final String server = lastSeen.get("server").asText(); - context.sendOutput( Component.translatable( "commands.seen.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 index 063283c4..ef7c698b 100644 --- a/src/main/java/me/chayapak1/chomens_bot/plugins/PlayersDatabasePlugin.java +++ b/src/main/java/me/chayapak1/chomens_bot/plugins/PlayersDatabasePlugin.java @@ -1,10 +1,7 @@ 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 it.unimi.dsi.fastutil.Pair; +import it.unimi.dsi.fastutil.longs.LongObjectImmutablePair; import me.chayapak1.chomens_bot.Bot; import me.chayapak1.chomens_bot.Main; import me.chayapak1.chomens_bot.data.listener.Listener; @@ -15,22 +12,59 @@ import org.geysermc.mcprotocollib.network.event.session.DisconnectedEvent; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; -import java.time.Instant; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class PlayersDatabasePlugin implements Listener { - private static final String CREATE_TABLE = "CREATE TABLE IF NOT EXISTS players (username VARCHAR(255) PRIMARY KEY, data LONGTEXT);"; - 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(JSON_EXTRACT(data, ?), ?)) WHERE username = ?;"; - private static final String GET_DATA = "SELECT data FROM players WHERE username = ?;"; - private static final String GET_IP = "SELECT JSON_UNQUOTE(JSON_VALUE(data, ?)) AS ip FROM players WHERE username = ?;"; - private static final String FIND_ALTS_SINGLE_SERVER = "SELECT * FROM players WHERE JSON_CONTAINS(JSON_EXTRACT(data, '$.ips'), JSON_OBJECT(?, ?)) LIMIT ?;"; - private static final String FIND_ALTS_ALL_SERVERS = "SELECT * FROM players WHERE JSON_SEARCH(JSON_EXTRACT(data, '$.ips'), 'one', ?) IS NOT NULL LIMIT ?;"; + private static final String CREATE_MAIN_TABLE = "CREATE TABLE IF NOT EXISTS players (" + + "uuid CHAR(36) PRIMARY KEY, " + + "username VARCHAR(32) NOT NULL, " + + "lastSeenTime TIMESTAMP," + + "lastSeenServer VARCHAR(100)" + + ");"; + private static final String CREATE_IPS_TABLE = "CREATE TABLE IF NOT EXISTS playerIPs (" + + "uuid CHAR(36) NOT NULL, " + + "server VARCHAR(100) NOT NULL, " + + "ip VARCHAR(45) NOT NULL, " + + "PRIMARY KEY (uuid, server), " + + "INDEX idx_ip_server (ip, server), " + + "FOREIGN KEY (uuid) REFERENCES players(uuid) ON DELETE CASCADE" + + ");"; + private static final String INSERT_PLAYER = "INSERT INTO players " + + "(uuid, username, lastSeenTime, lastSeenServer) " + + "VALUES (?, ?, NOW(), ?) " + + "ON DUPLICATE KEY UPDATE " + + "username = VALUES(username), " + + "lastSeenTime = VALUES(lastSeenTime), " + + "lastSeenServer = VALUES(lastSeenServer);"; + private static final String UPDATE_PLAYER_IP = "INSERT INTO playerIPs " + + "(uuid, server, ip) " + + "VALUES (?, ?, ?) " + + "ON DUPLICATE KEY UPDATE ip = VALUES(ip);"; + private static final String UPDATE_LAST_SEEN = "UPDATE players " + + "SET lastSeenTime = NOW(), lastSeenServer = ? " + + "WHERE uuid = ?;"; - private static final ObjectMapper objectMapper = new ObjectMapper(); + private static final String GET_LAST_SEEN = "SELECT " + + "lastSeenTime AS time, " + + "lastSeenServer AS server " + + "FROM players WHERE username = ?;"; + private static final String GET_IP = "SELECT ipInfo.ip " + + "FROM playerIPs ipInfo " + + "JOIN players player ON ipInfo.uuid = player.uuid " + + "WHERE player.username = ? AND ipInfo.server = ?;"; + private static final String FIND_ALTS_SINGLE_SERVER = "SELECT player.username, player.lastSeenTime, player.lastSeenServer " + + "FROM playerIPs ipInfo " + + "JOIN players player ON ipInfo.uuid = player.uuid " + + "WHERE ipInfo.ip = ? AND ipInfo.server = ? " + + "LIMIT ?;"; + private static final String FIND_ALTS_ALL_SERVERS = "SELECT player.username, player.lastSeenTime, player.lastSeenServer " + + "FROM playerIPs ipInfo " + + "JOIN players player ON ipInfo.uuid = player.uuid " + + "WHERE ipInfo.ip = ? " + + "LIMIT ?;"; private final Bot bot; @@ -38,7 +72,8 @@ public class PlayersDatabasePlugin implements Listener { if (Main.database != null) { DatabasePlugin.EXECUTOR_SERVICE.execute(() -> { try { - Main.database.execute(CREATE_TABLE); + Main.database.execute(CREATE_MAIN_TABLE); + Main.database.execute(CREATE_IPS_TABLE); } catch (final SQLException e) { LoggerUtilities.error(e); } @@ -54,24 +89,21 @@ public class PlayersDatabasePlugin implements Listener { bot.listener.addListener(this); } - public JsonNode getPlayerData (final String username) { + public Pair getPlayerLastSeen (final String username) { if (Main.database == null || Main.database.connection == null) return null; try { - final PreparedStatement statement = Main.database.connection.prepareStatement(GET_DATA); - + final PreparedStatement statement = Main.database.connection.prepareStatement(GET_LAST_SEEN); statement.setString(1, username); final ResultSet result = statement.executeQuery(); + if (!result.next()) return null; // doesn't exist - if (!result.isBeforeFirst()) return null; // doesn't exist + final long time = result.getTimestamp("time").getTime(); + final String server = result.getString("server"); - // this will use only the first one in the output - result.next(); - final String stringJson = result.getString("data"); - - return objectMapper.readTree(stringJson); - } catch (final SQLException | JsonProcessingException e) { + return LongObjectImmutablePair.of(time, server); + } catch (final SQLException e) { bot.logger.error(e); return null; } @@ -83,16 +115,11 @@ public class PlayersDatabasePlugin implements Listener { try { final PreparedStatement statement = Main.database.connection.prepareStatement(GET_IP); - // this may be dangerous but the server address is configured only in the config - // so this should still be safe - statement.setString(1, "$.ips.\"" + bot.getServerString(true) + "\""); - statement.setString(2, username); + statement.setString(1, username); + statement.setString(2, bot.getServerString(true)); final ResultSet result = statement.executeQuery(); - - if (!result.isBeforeFirst()) return null; // no ip for player in this server - - result.next(); + if (!result.next()) return null; // no ip for player in this server return result.getString("ip"); } catch (final SQLException e) { @@ -101,9 +128,9 @@ public class PlayersDatabasePlugin implements Listener { } } - public Map findPlayerAlts (final String ip, final boolean allServer, final int limit) { + public Map> findPlayerAlts (final String ip, final boolean allServer, final int limit) { try { - final Map output = new HashMap<>(); + final Map> output = new HashMap<>(); final PreparedStatement statement; @@ -115,8 +142,8 @@ public class PlayersDatabasePlugin implements Listener { } else { statement = Main.database.connection.prepareStatement(FIND_ALTS_SINGLE_SERVER); - statement.setString(1, bot.getServerString(true)); - statement.setString(2, ip); + statement.setString(1, ip); + statement.setString(2, bot.getServerString(true)); statement.setInt(3, limit); } @@ -125,12 +152,15 @@ public class PlayersDatabasePlugin implements Listener { while (result.next()) { output.put( result.getString("username"), - objectMapper.readTree(result.getString("data")) + LongObjectImmutablePair.of( + result.getTimestamp("lastSeenTime").getTime(), + result.getString("lastSeenServer") + ) ); } return output; - } catch (final SQLException | JsonProcessingException e) { + } catch (final SQLException e) { bot.logger.error(e); return null; } @@ -142,20 +172,14 @@ public class PlayersDatabasePlugin implements Listener { try { final PreparedStatement insertPlayerStatement = Main.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()); - // ||| this will be replaced after the player leaves, it is here - // ||| in case the bot leaves before the player leaves it will - // VVV prevent the last seen entry being empty - baseObject.set("lastSeen", getLastSeenObject()); - - insertPlayerStatement.setString(2, objectMapper.writeValueAsString(baseObject)); + insertPlayerStatement.setString(1, target.profile.getIdAsString()); + insertPlayerStatement.setString(2, target.profile.getName()); + insertPlayerStatement.setString(3, bot.getServerString(true)); insertPlayerStatement.executeUpdate(); - } catch (final SQLException | JsonProcessingException e) { + + updateLastSeenEntry(target); + } catch (final SQLException e) { bot.logger.error(e); } }); @@ -165,20 +189,14 @@ public class PlayersDatabasePlugin implements Listener { public void onQueriedPlayerIP (final PlayerEntry target, final String ip) { DatabasePlugin.EXECUTOR_SERVICE.execute(() -> { try { - final PreparedStatement updatePlayerStatement = Main.database.connection.prepareStatement(UPDATE_PLAYER); + final PreparedStatement updatePlayerStatement = Main.database.connection.prepareStatement(UPDATE_PLAYER_IP); - updatePlayerStatement.setString(1, "$.ips"); - updatePlayerStatement.setString(2, "$.ips"); - - final ObjectNode ipsObject = JsonNodeFactory.instance.objectNode(); - ipsObject.put(bot.getServerString(true), ip); - - updatePlayerStatement.setString(3, objectMapper.writeValueAsString(ipsObject)); - - updatePlayerStatement.setString(4, target.profile.getName()); + updatePlayerStatement.setString(1, target.profile.getIdAsString()); + updatePlayerStatement.setString(2, bot.getServerString(true)); + updatePlayerStatement.setString(3, ip); updatePlayerStatement.executeUpdate(); - } catch (final SQLException | JsonProcessingException e) { + } catch (final SQLException e) { bot.logger.error(e); } }); @@ -189,6 +207,8 @@ public class PlayersDatabasePlugin implements Listener { if (Main.stopping) return; synchronized (bot.players.list) { + if (bot.players.list.isEmpty()) return; + final List clonedList = new ArrayList<>(bot.players.list); DatabasePlugin.EXECUTOR_SERVICE.execute(() -> { @@ -206,29 +226,14 @@ public class PlayersDatabasePlugin implements Listener { private void updateLastSeenEntry (final PlayerEntry target) { try { - final PreparedStatement updatePlayerStatement = Main.database.connection.prepareStatement(UPDATE_PLAYER); + final PreparedStatement updateLastSeenStatement = Main.database.connection.prepareStatement(UPDATE_LAST_SEEN); - updatePlayerStatement.setString(1, "$.lastSeen"); - updatePlayerStatement.setString(2, "$.lastSeen"); + updateLastSeenStatement.setString(1, bot.getServerString(true)); + updateLastSeenStatement.setString(2, target.profile.getIdAsString()); - final ObjectNode lastSeenObject = getLastSeenObject(); - - updatePlayerStatement.setString(3, objectMapper.writeValueAsString(lastSeenObject)); - - updatePlayerStatement.setString(4, target.profile.getName()); - - updatePlayerStatement.executeUpdate(); - } catch (final SQLException | JsonProcessingException e) { + updateLastSeenStatement.executeUpdate(); + } catch (final SQLException e) { bot.logger.error(e); } } - - private ObjectNode getLastSeenObject () { - final ObjectNode object = JsonNodeFactory.instance.objectNode(); - - object.put("time", Instant.now().toEpochMilli()); - object.put("server", bot.getServerString(true)); - - return object; - } }