feat: remake the player database to not use JSON at ALL

This commit is contained in:
ChomeNS
2025-08-16 15:07:03 +07:00
parent 564be9ff3d
commit 40219d2912
4 changed files with 104 additions and 119 deletions

View File

@@ -1 +1 @@
3558
3565

View File

@@ -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<String, JsonNode> altsMap = bot.playersDatabase.findPlayerAlts(targetIP, allServer, LIMIT);
final Map<String, Pair<Long, String>> 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<String> 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();

View File

@@ -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<Long, String> 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",

View File

@@ -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<Long, String> 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<String, JsonNode> findPlayerAlts (final String ip, final boolean allServer, final int limit) {
public Map<String, Pair<Long, String>> findPlayerAlts (final String ip, final boolean allServer, final int limit) {
try {
final Map<String, JsonNode> output = new HashMap<>();
final Map<String, Pair<Long, String>> 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<PlayerEntry> 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;
}
}