feat: remake the player database to not use JSON at ALL
This commit is contained in:
@@ -1 +1 @@
|
||||
3558
|
||||
3565
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user