From d6d847fcd496492e5f45b401ef93a3e46be3546f Mon Sep 17 00:00:00 2001 From: James Mortemore Date: Sat, 4 Apr 2026 13:47:28 +0100 Subject: [PATCH 1/2] refactor: resolve TODOs, optimise queries, and improve shutdown safety - Wrap RollbackCommand in transaction for atomicity; restore PlayerReportDeletedEvent firing for reports case - Extract generic cache helpers in RollbackSync using Predicate/Consumer - Add bulk markAllRead() to PlayerWarnStorage, replace N+1 deleteRecent with single DELETE subquery - Replace SELECT-then-loop-delete in PlayerHistoryStorage.purge() with DELETE WHERE IN; batch save() updates with chunked IN clause - Replace in-memory aggregation in getNamesSummary() with SQL GROUP BY - Batch report deletions in PlayerReportStorage while preserving events - Hoist repeated queryForId out of punishAlts loop in CommonJoinListener - Eliminate double isBanned/getBan lookups across listeners and commands - Combine InfoCommand 8 aggregate queries into single UNION ALL subquery - Batch FindAltsCommand per-alt ban record lookups - Batch KickAllCommand kick logging with callBatchTasks - Run PlayerStorage autocomplete async with volatile shutdown guard - Cache permission checks before async blocks in Unban/UnmuteCommand - Add cancelAll() to CommonScheduler; implement in Sponge API 7 and Velocity schedulers tracking only repeating tasks - Introduce typed HistoryEntry replacing HashMap for history methods - Fix ActivityStorage connection management with try-with-resources - Deduplicate HistoryStorage query methods --- .../banmanager/common/BanManagerPlugin.java | 5 +- .../banmanager/common/CommonScheduler.java | 1 + .../common/commands/FindAltsCommand.java | 53 ++- .../common/commands/InfoCommand.java | 115 +++-- .../common/commands/KickAllCommand.java | 18 +- .../common/commands/RollbackCommand.java | 280 ++++++------ .../common/commands/UnbanCommand.java | 6 +- .../common/commands/UnmuteCommand.java | 6 +- .../banmanager/common/data/HistoryEntry.java | 33 ++ .../common/listeners/CommonChatListener.java | 17 +- .../listeners/CommonCommandListener.java | 7 +- .../common/listeners/CommonJoinListener.java | 90 ++-- .../common/runnables/RollbackSync.java | 167 +++---- .../common/storage/ActivityStorage.java | 101 ++--- .../common/storage/HistoryStorage.java | 426 +++--------------- .../common/storage/PlayerHistoryStorage.java | 99 ++-- .../common/storage/PlayerReportStorage.java | 19 +- .../common/storage/PlayerStorage.java | 17 +- .../common/storage/PlayerWarnStorage.java | 31 +- .../banmanager/sponge/BMSpongePlugin.java | 6 +- .../banmanager/sponge/SpongeScheduler.java | 13 +- .../banmanager/velocity/BMVelocityPlugin.java | 5 +- .../velocity/VelocityScheduler.java | 14 +- 23 files changed, 656 insertions(+), 873 deletions(-) create mode 100644 common/src/main/java/me/confuser/banmanager/common/data/HistoryEntry.java diff --git a/common/src/main/java/me/confuser/banmanager/common/BanManagerPlugin.java b/common/src/main/java/me/confuser/banmanager/common/BanManagerPlugin.java index 3e1a6eb54..ac0c8a452 100644 --- a/common/src/main/java/me/confuser/banmanager/common/BanManagerPlugin.java +++ b/common/src/main/java/me/confuser/banmanager/common/BanManagerPlugin.java @@ -251,7 +251,10 @@ public final void disable() { } if (localConn != null) { - // Save all player histories + if (playerStorage != null) { + playerStorage.shutdown(); + } + if (config.isLogIpsEnabled() && playerHistoryStorage != null) { playerHistoryStorage.save(); } diff --git a/common/src/main/java/me/confuser/banmanager/common/CommonScheduler.java b/common/src/main/java/me/confuser/banmanager/common/CommonScheduler.java index fda824298..88d32d7c9 100644 --- a/common/src/main/java/me/confuser/banmanager/common/CommonScheduler.java +++ b/common/src/main/java/me/confuser/banmanager/common/CommonScheduler.java @@ -8,4 +8,5 @@ public interface CommonScheduler { void runSync(Runnable task); void runSyncLater(Runnable task, Duration delay); void runAsyncRepeating(Runnable task, Duration initialDelay, Duration period); + default void cancelAll() {} } diff --git a/common/src/main/java/me/confuser/banmanager/common/commands/FindAltsCommand.java b/common/src/main/java/me/confuser/banmanager/common/commands/FindAltsCommand.java index 721c17c5b..d87fa2e37 100644 --- a/common/src/main/java/me/confuser/banmanager/common/commands/FindAltsCommand.java +++ b/common/src/main/java/me/confuser/banmanager/common/commands/FindAltsCommand.java @@ -14,9 +14,10 @@ import me.confuser.banmanager.common.util.IPUtils; import me.confuser.banmanager.common.util.Message; + import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; +import java.util.*; +import java.util.stream.Collectors; public class FindAltsCommand extends CommonCommand { @@ -84,28 +85,46 @@ public boolean onCommand(final CommonSender sender, CommandParser parser) { public static TextComponent alts(List players) { TextComponent.Builder message = Component.text(); - int index = 0; + + List unbanned = new ArrayList<>(); + Map colours = new HashMap<>(); for (PlayerData player : players) { - TextColor colour = NamedTextColor.GREEN; + PlayerBanData ban = BanManagerPlugin.getInstance().getPlayerBanStorage().getBan(player.getUUID()); - if (BanManagerPlugin.getInstance().getPlayerBanStorage().isBanned(player.getUUID())) { - PlayerBanData ban = BanManagerPlugin.getInstance().getPlayerBanStorage().getBan(player.getUUID()); + if (ban != null) { + colours.put(player.getUUID(), ban.getExpires() == 0 ? NamedTextColor.RED : NamedTextColor.GOLD); + } else { + unbanned.add(player); + } + } - if (ban.getExpires() == 0) { - colour = NamedTextColor.RED; - } else { - colour = NamedTextColor.GOLD; + if (!unbanned.isEmpty()) { + try { + Set withRecords = BanManagerPlugin.getInstance().getPlayerBanRecordStorage() + .queryBuilder() + .selectColumns("player_id") + .where().in("player_id", unbanned.stream().map(PlayerData::getId).collect(Collectors.toList())) + .query() + .stream() + .map(r -> r.getPlayer().getUUID()) + .collect(Collectors.toSet()); + + for (PlayerData player : unbanned) { + colours.put(player.getUUID(), withRecords.contains(player.getUUID()) ? NamedTextColor.YELLOW : NamedTextColor.GREEN); } - } else { - try { - if (BanManagerPlugin.getInstance().getPlayerBanRecordStorage().getCount(player) != 0) { - colour = NamedTextColor.YELLOW; - } - } catch (SQLException e) { - BanManagerPlugin.getInstance().getLogger().warning("Failed to execute findalts command", e); + } catch (SQLException e) { + BanManagerPlugin.getInstance().getLogger().warning("Failed to execute findalts command", e); + for (PlayerData player : unbanned) { + colours.put(player.getUUID(), NamedTextColor.GREEN); } } + } + + int index = 0; + + for (PlayerData player : players) { + TextColor colour = colours.getOrDefault(player.getUUID(), NamedTextColor.GREEN); message .append( diff --git a/common/src/main/java/me/confuser/banmanager/common/commands/InfoCommand.java b/common/src/main/java/me/confuser/banmanager/common/commands/InfoCommand.java index 288655230..31168c1c8 100644 --- a/common/src/main/java/me/confuser/banmanager/common/commands/InfoCommand.java +++ b/common/src/main/java/me/confuser/banmanager/common/commands/InfoCommand.java @@ -11,6 +11,11 @@ import me.confuser.banmanager.common.kyori.text.format.NamedTextColor; import me.confuser.banmanager.common.maxmind.db.model.CountryResponse; import me.confuser.banmanager.common.ormlite.dao.CloseableIterator; +import me.confuser.banmanager.common.ormlite.field.SqlType; +import me.confuser.banmanager.common.ormlite.stmt.StatementBuilder; +import me.confuser.banmanager.common.ormlite.support.CompiledStatement; +import me.confuser.banmanager.common.ormlite.support.DatabaseConnection; +import me.confuser.banmanager.common.ormlite.support.DatabaseResults; import me.confuser.banmanager.common.util.DateUtils; import me.confuser.banmanager.common.util.IPUtils; import me.confuser.banmanager.common.util.Message; @@ -21,7 +26,6 @@ import java.net.InetAddress; import java.sql.SQLException; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; public class InfoCommand extends CommonCommand { @@ -109,7 +113,7 @@ public void ipInfo(CommonSender sender, IPAddress ip, InfoCommandParser parser) return; } - ArrayList> results; + List results; if (parser.getTime() != null && !parser.getTime().isEmpty()) { results = getPlugin().getHistoryStorage().getSince(ip, since, parser); @@ -117,22 +121,21 @@ public void ipInfo(CommonSender sender, IPAddress ip, InfoCommandParser parser) results = getPlugin().getHistoryStorage().getAll(ip, parser); } - if (results == null || results.size() == 0) { + if (results == null || results.isEmpty()) { Message.get("info.history.noResults").sendTo(sender); return; } String dateTimeFormat = Message.getString("info.history.dateTimeFormat"); - for (HashMap result : results) { + for (HistoryEntry result : results) { Message message = Message.get("info.history.row") - .set("id", (int) result.get("id")) - .set("reason", (String) result.get("reason")) - .set("type", (String) result.get("type")) - .set("created", DateUtils - .format(dateTimeFormat, (long) result.get("created"))) - .set("actor", (String) result.get("actor")) - .set("meta", (String) result.get("meta")); + .set("id", result.getId()) + .set("reason", result.getReason()) + .set("type", result.getType()) + .set("created", DateUtils.format(dateTimeFormat, result.getCreated())) + .set("actor", result.getActor()) + .set("meta", result.getMeta()); messages.add(message.toString()); } @@ -369,7 +372,7 @@ public void playerInfo(CommonSender sender, String name, Integer index, InfoComm handleIpHistory(messages, player, since, page); } else { - ArrayList> results; + List results; if (parser.getTime() != null && !parser.getTime().isEmpty()) { results = getPlugin().getHistoryStorage().getSince(player, since, parser); @@ -377,22 +380,21 @@ public void playerInfo(CommonSender sender, String name, Integer index, InfoComm results = getPlugin().getHistoryStorage().getAll(player, parser); } - if (results == null || results.size() == 0) { + if (results == null || results.isEmpty()) { Message.get("info.history.noResults").sendTo(sender); return; } String dateTimeFormat = Message.getString("info.history.dateTimeFormat"); - for (HashMap result : results) { + for (HistoryEntry result : results) { Message message = Message.get("info.history.row") - .set("id", (int) result.get("id")) - .set("reason", (String) result.get("reason")) - .set("type", (String) result.get("type")) - .set("created", DateUtils - .format(dateTimeFormat, (long) result.get("created"))) - .set("actor", (String) result.get("actor")) - .set("meta", (String) result.get("meta")); + .set("id", result.getId()) + .set("reason", result.getReason()) + .set("type", result.getType()) + .set("created", DateUtils.format(dateTimeFormat, result.getCreated())) + .set("actor", result.getActor()) + .set("meta", result.getMeta()); messages.add(message.toString()); } @@ -401,13 +403,55 @@ public void playerInfo(CommonSender sender, String name, Integer index, InfoComm } else { if (sender.hasPermission("bm.command.bminfo.playerstats")) { - long banTotal = getPlugin().getPlayerBanRecordStorage().getCount(player); - long muteTotal = getPlugin().getPlayerMuteRecordStorage().getCount(player); - long warnTotal = getPlugin().getPlayerWarnStorage().getCount(player); - double warnPointsTotal = getPlugin().getPlayerWarnStorage().getPointsCount(player); - long kickTotal = getPlugin().getPlayerKickStorage().getCount(player); - long noteTotal = getPlugin().getPlayerNoteStorage().getCount(player); - long reportTotal = getPlugin().getPlayerReportStorage().getCount(player); + String banRecordsTable = getPlugin().getPlayerBanRecordStorage().getTableInfo().getTableName(); + String muteRecordsTable = getPlugin().getPlayerMuteRecordStorage().getTableInfo().getTableName(); + String warningsTable = getPlugin().getPlayerWarnStorage().getTableName(); + String kicksTable = getPlugin().getPlayerKickStorage().getTableInfo().getTableName(); + String notesTable = getPlugin().getPlayerNoteStorage().getTableInfo().getTableName(); + String reportsTable = getPlugin().getPlayerReportStorage().getTableName(); + + String sql = "SELECT 'bans' AS type, COUNT(*) AS cnt, 0 AS pts FROM `" + banRecordsTable + "` WHERE `player_id` = ?" + + " UNION ALL SELECT 'mutes', COUNT(*), 0 FROM `" + muteRecordsTable + "` WHERE `player_id` = ?" + + " UNION ALL SELECT 'warns', COUNT(*), 0 FROM `" + warningsTable + "` WHERE `player_id` = ?" + + " UNION ALL SELECT 'warnPoints', 0, COALESCE(SUM(`points`), 0) FROM `" + warningsTable + "` WHERE `player_id` = ?" + + " UNION ALL SELECT 'kicks', COUNT(*), 0 FROM `" + kicksTable + "` WHERE `player_id` = ?" + + " UNION ALL SELECT 'notes', COUNT(*), 0 FROM `" + notesTable + "` WHERE `player_id` = ?" + + " UNION ALL SELECT 'reports', COUNT(*), 0 FROM `" + reportsTable + "` WHERE `player_id` = ?"; + + long banTotal = 0, muteTotal = 0, warnTotal = 0, kickTotal = 0, noteTotal = 0, reportTotal = 0; + double warnPointsTotal = 0; + + try (DatabaseConnection conn = getPlugin().getLocalConn().getReadOnlyConnection("")) { + CompiledStatement stmt = conn.compileStatement(sql, + StatementBuilder.StatementType.SELECT, null, + DatabaseConnection.DEFAULT_RESULT_FLAGS, false); + try { + for (int i = 0; i < 7; i++) { + stmt.setObject(i, player.getId(), SqlType.BYTE_ARRAY); + } + DatabaseResults results = stmt.runQuery(null); + try { + while (results.next()) { + String type = results.getString(0); + switch (type) { + case "bans": banTotal = results.getLong(1); break; + case "mutes": muteTotal = results.getLong(1); break; + case "warns": warnTotal = results.getLong(1); break; + case "warnPoints": warnPointsTotal = results.getDouble(2); break; + case "kicks": kickTotal = results.getLong(1); break; + case "notes": noteTotal = results.getLong(1); break; + case "reports": reportTotal = results.getLong(1); break; + } + } + } finally { + try { results.close(); } catch (IOException ignored) { } + } + } finally { + try { stmt.close(); } catch (IOException ignored) { } + } + } catch (IOException e) { + throw new SQLException("Failed to query player stats", e); + } messages.add(Message.get("info.stats.player") .set("player", player.getName()) @@ -516,8 +560,9 @@ public void playerInfo(CommonSender sender, String name, Integer index, InfoComm .set("rangebans", Long.toString(ipRangeBanTotal)) .toString()); - if (getPlugin().getIpBanStorage().isBanned(player.getIp())) { - IpBanData ban = getPlugin().getIpBanStorage().getBan(player.getIp()); + IpBanData ipBan = getPlugin().getIpBanStorage().getBan(player.getIp()); + if (ipBan != null) { + IpBanData ban = ipBan; Message message; @@ -539,8 +584,9 @@ public void playerInfo(CommonSender sender, String name, Integer index, InfoComm } } - if (getPlugin().getPlayerBanStorage().isBanned(player.getUUID())) { - PlayerBanData ban = getPlugin().getPlayerBanStorage().getBan(player.getUUID()); + PlayerBanData playerBan = getPlugin().getPlayerBanStorage().getBan(player.getUUID()); + if (playerBan != null) { + PlayerBanData ban = playerBan; Message message; @@ -562,8 +608,9 @@ public void playerInfo(CommonSender sender, String name, Integer index, InfoComm .toString()); } - if (getPlugin().getPlayerMuteStorage().isMuted(player.getUUID())) { - PlayerMuteData mute = getPlugin().getPlayerMuteStorage().getMute(player.getUUID()); + PlayerMuteData playerMute = getPlugin().getPlayerMuteStorage().getMute(player.getUUID()); + if (playerMute != null) { + PlayerMuteData mute = playerMute; Message message; diff --git a/common/src/main/java/me/confuser/banmanager/common/commands/KickAllCommand.java b/common/src/main/java/me/confuser/banmanager/common/commands/KickAllCommand.java index c13fee342..7e937db67 100644 --- a/common/src/main/java/me/confuser/banmanager/common/commands/KickAllCommand.java +++ b/common/src/main/java/me/confuser/banmanager/common/commands/KickAllCommand.java @@ -7,6 +7,8 @@ import me.confuser.banmanager.common.util.Message; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; public class KickAllCommand extends CommonCommand { @@ -40,20 +42,28 @@ public boolean onCommand(final CommonSender sender, CommandParser parser) { CommonPlayer[] onlinePlayers = getPlugin().getServer().getOnlinePlayers(); if (getPlugin().getConfig().isKickLoggingEnabled()) { + List kicks = new ArrayList<>(); + for (CommonPlayer player : onlinePlayers) { if (!sender.hasPermission("bm.exempt.override.kick") && player.hasPermission("bm.exempt.kick")) { continue; } PlayerData playerData = player.getData(); - if (playerData == null) continue; - PlayerKickData data = new PlayerKickData(playerData, actor, reason); + kicks.add(new PlayerKickData(playerData, actor, reason)); + } + if (!kicks.isEmpty()) { try { - getPlugin().getPlayerKickStorage().addKick(data, isSilent); - } catch (SQLException e) { + getPlugin().getPlayerKickStorage().callBatchTasks(() -> { + for (PlayerKickData data : kicks) { + getPlugin().getPlayerKickStorage().addKick(data, isSilent); + } + return null; + }); + } catch (Exception e) { sender.sendMessage(Message.get("sender.error.exception").toString()); getPlugin().getLogger().warning("Failed to execute kickall command", e); } diff --git a/common/src/main/java/me/confuser/banmanager/common/commands/RollbackCommand.java b/common/src/main/java/me/confuser/banmanager/common/commands/RollbackCommand.java index 6d35652ee..19e43ddb1 100644 --- a/common/src/main/java/me/confuser/banmanager/common/commands/RollbackCommand.java +++ b/common/src/main/java/me/confuser/banmanager/common/commands/RollbackCommand.java @@ -5,8 +5,11 @@ import me.confuser.banmanager.common.data.*; import me.confuser.banmanager.common.ormlite.stmt.DeleteBuilder; import me.confuser.banmanager.common.ormlite.stmt.QueryBuilder; + +import java.util.List; import me.confuser.banmanager.common.util.DateUtils; import me.confuser.banmanager.common.util.Message; +import me.confuser.banmanager.common.util.TransactionHelper; import java.sql.SQLException; import java.util.ArrayList; @@ -97,203 +100,174 @@ public boolean onCommand(final CommonSender sender, CommandParser parser) { if (!RollbackCommand.types.contains(type)) { Message.get("bmrollback.error.invalid").set("type", type).set("types", String.join(",", types)).sendTo(sender); return; - } else if (sender.hasPermission("bm.command.bmrollback." + type)) { - try { - getPlugin().getRollbackStorage() - .create(new RollbackData(player, sender.getData(), type, expires, now)); - } catch (SQLException e) { - sender.sendMessage(Message.get("sender.error.exception").toString()); - getPlugin().getLogger().warning("Failed to execute rollback command", e); - return; - } } } - // Forces running in order - // I.e bans must be executed before banrecords etc - for (String type : RollbackCommand.types) { - if (!types.contains(type)) continue; - - // @TODO Transactions for robustness - try { - switch (type) { - case "bans": - DeleteBuilder bans = getPlugin().getPlayerBanStorage().deleteBuilder(); - bans.where().eq("actor_id", player.getId()).and().le("created", now).and().ge("created", expires); - bans.delete(); - break; - - case "banrecords": - QueryBuilder banRecords = getPlugin().getPlayerBanRecordStorage() - .queryBuilder(); - banRecords.where().eq("actor_id", player.getId()).and().le("created", now).and().ge("created", expires); - - for (PlayerBanRecord record : banRecords.query()) { - try { - // Only restore if the original ban wasn't also by the malicious mod + try { + TransactionHelper.runInTransaction(getPlugin().getLocalConn(), () -> { + for (String type : types) { + if (sender.hasPermission("bm.command.bmrollback." + type)) { + getPlugin().getRollbackStorage() + .create(new RollbackData(player, sender.getData(), type, expires, now)); + } + } + + for (String type : RollbackCommand.types) { + if (!types.contains(type)) continue; + + switch (type) { + case "bans": + DeleteBuilder bans = getPlugin().getPlayerBanStorage().deleteBuilder(); + bans.where().eq("actor_id", player.getId()).and().le("created", now).and().ge("created", expires); + bans.delete(); + break; + + case "banrecords": + QueryBuilder banRecords = getPlugin().getPlayerBanRecordStorage() + .queryBuilder(); + banRecords.where().eq("actor_id", player.getId()).and().le("created", now).and().ge("created", expires); + + for (PlayerBanRecord record : banRecords.query()) { if (getPlugin().getPlayerBanStorage().retrieveBan(record.getPlayer().getUUID()) == null && !record.getPastActor().getUUID().equals(player.getUUID())) { getPlugin().getPlayerBanStorage().create(new PlayerBanData(record)); } getPlugin().getPlayerBanRecordStorage().delete(record); - } catch (SQLException e) { - sender.sendMessage(Message.get("sender.error.exception").toString()); - getPlugin().getLogger().warning("Failed to execute rollback command", e); - return; } - } - - // Also delete records where the malicious mod was the original banner (pastActor) - // These are bans made by the malicious mod that were later legitimately unbanned - DeleteBuilder maliciousBanRecords = getPlugin().getPlayerBanRecordStorage().deleteBuilder(); - maliciousBanRecords.where().eq("pastActor_id", player.getId()).and().le("pastCreated", now).and().ge("pastCreated", expires); - maliciousBanRecords.delete(); - - break; - - case "ipbans": - DeleteBuilder ipBans = getPlugin().getIpBanStorage().deleteBuilder(); - ipBans.where().eq("actor_id", player.getId()).and().le("created", now).and().ge("created", expires); - ipBans.delete(); - break; - - case "ipbanrecords": - QueryBuilder ipBanRecords = getPlugin().getIpBanRecordStorage().queryBuilder(); - ipBanRecords.where().eq("actor_id", player.getId()).and().le("created", now).and() - .ge("created", expires); - - for (IpBanRecord record : ipBanRecords.query()) { - try { - // Only restore if the original ban wasn't also by the malicious mod + + DeleteBuilder maliciousBanRecords = getPlugin().getPlayerBanRecordStorage().deleteBuilder(); + maliciousBanRecords.where().eq("pastActor_id", player.getId()).and().le("pastCreated", now).and().ge("pastCreated", expires); + maliciousBanRecords.delete(); + + break; + + case "ipbans": + DeleteBuilder ipBans = getPlugin().getIpBanStorage().deleteBuilder(); + ipBans.where().eq("actor_id", player.getId()).and().le("created", now).and().ge("created", expires); + ipBans.delete(); + break; + + case "ipbanrecords": + QueryBuilder ipBanRecords = getPlugin().getIpBanRecordStorage().queryBuilder(); + ipBanRecords.where().eq("actor_id", player.getId()).and().le("created", now).and() + .ge("created", expires); + + for (IpBanRecord record : ipBanRecords.query()) { if (getPlugin().getIpBanStorage().retrieveBan(record.getIp()) == null && !record.getPastActor().getUUID().equals(player.getUUID())) { getPlugin().getIpBanStorage().create(new IpBanData(record)); } getPlugin().getIpBanRecordStorage().delete(record); - } catch (SQLException e) { - sender.sendMessage(Message.get("sender.error.exception").toString()); - getPlugin().getLogger().warning("Failed to execute rollback command", e); - return; } - } - - // Also delete records where the malicious mod was the original banner (pastActor) - DeleteBuilder maliciousIpBanRecords = getPlugin().getIpBanRecordStorage().deleteBuilder(); - maliciousIpBanRecords.where().eq("pastActor_id", player.getId()).and().le("pastCreated", now).and().ge("pastCreated", expires); - maliciousIpBanRecords.delete(); - break; + DeleteBuilder maliciousIpBanRecords = getPlugin().getIpBanRecordStorage().deleteBuilder(); + maliciousIpBanRecords.where().eq("pastActor_id", player.getId()).and().le("pastCreated", now).and().ge("pastCreated", expires); + maliciousIpBanRecords.delete(); - case "kicks": - DeleteBuilder kicks = getPlugin().getPlayerKickStorage().deleteBuilder(); - kicks.where().eq("actor_id", player.getId()).and().le("created", now).and().ge("created", expires); - kicks.delete(); + break; - break; + case "kicks": + DeleteBuilder kicks = getPlugin().getPlayerKickStorage().deleteBuilder(); + kicks.where().eq("actor_id", player.getId()).and().le("created", now).and().ge("created", expires); + kicks.delete(); + break; - case "mutes": - DeleteBuilder mutes = getPlugin().getPlayerMuteStorage().deleteBuilder(); - mutes.where().eq("actor_id", player.getId()).and().le("created", now).and().ge("created", expires); - mutes.delete(); - break; + case "mutes": + DeleteBuilder mutes = getPlugin().getPlayerMuteStorage().deleteBuilder(); + mutes.where().eq("actor_id", player.getId()).and().le("created", now).and().ge("created", expires); + mutes.delete(); + break; - case "muterecords": - QueryBuilder muteRecords = getPlugin().getPlayerMuteRecordStorage() - .queryBuilder(); - muteRecords.where().eq("actor_id", player.getId()).and().le("created", now).and() - .ge("created", expires); + case "muterecords": + QueryBuilder muteRecords = getPlugin().getPlayerMuteRecordStorage() + .queryBuilder(); + muteRecords.where().eq("actor_id", player.getId()).and().le("created", now).and() + .ge("created", expires); - for (PlayerMuteRecord record : muteRecords.query()) { - try { - // Only restore if the original mute wasn't also by the malicious mod + for (PlayerMuteRecord record : muteRecords.query()) { if (getPlugin().getPlayerMuteStorage().retrieveMute(record.getPlayer().getUUID()) == null && !record.getPastActor().getUUID().equals(player.getUUID())) { getPlugin().getPlayerMuteStorage().create(new PlayerMuteData(record)); } getPlugin().getPlayerMuteRecordStorage().delete(record); - } catch (SQLException e) { - sender.sendMessage(Message.get("sender.error.exception").toString()); - getPlugin().getLogger().warning("Failed to execute rollback command", e); - return; } - } - // Also delete records where the malicious mod was the original muter (pastActor) - DeleteBuilder maliciousMuteRecords = getPlugin().getPlayerMuteRecordStorage().deleteBuilder(); - maliciousMuteRecords.where().eq("pastActor_id", player.getId()).and().le("pastCreated", now).and().ge("pastCreated", expires); - maliciousMuteRecords.delete(); + DeleteBuilder maliciousMuteRecords = getPlugin().getPlayerMuteRecordStorage().deleteBuilder(); + maliciousMuteRecords.where().eq("pastActor_id", player.getId()).and().le("pastCreated", now).and().ge("pastCreated", expires); + maliciousMuteRecords.delete(); - break; + break; - case "ipmutes": - DeleteBuilder ipMutes = getPlugin().getIpMuteStorage().deleteBuilder(); - ipMutes.where().eq("actor_id", player.getId()).and().le("created", now).and().ge("created", expires); - ipMutes.delete(); - break; + case "ipmutes": + DeleteBuilder ipMutes = getPlugin().getIpMuteStorage().deleteBuilder(); + ipMutes.where().eq("actor_id", player.getId()).and().le("created", now).and().ge("created", expires); + ipMutes.delete(); + break; - case "ipmuterecords": - QueryBuilder ipMuteRecords = getPlugin().getIpMuteRecordStorage().queryBuilder(); - ipMuteRecords.where().eq("actor_id", player.getId()).and().le("created", now).and() - .ge("created", expires); + case "ipmuterecords": + QueryBuilder ipMuteRecords = getPlugin().getIpMuteRecordStorage().queryBuilder(); + ipMuteRecords.where().eq("actor_id", player.getId()).and().le("created", now).and() + .ge("created", expires); - for (IpMuteRecord record : ipMuteRecords.query()) { - try { - // Only restore if the original mute wasn't also by the malicious mod + for (IpMuteRecord record : ipMuteRecords.query()) { if (getPlugin().getIpMuteStorage().retrieveMute(record.getIp()) == null && !record.getPastActor().getUUID().equals(player.getUUID())) { getPlugin().getIpMuteStorage().create(new IpMuteData(record)); } getPlugin().getIpMuteRecordStorage().delete(record); - } catch (SQLException e) { - sender.sendMessage(Message.get("sender.error.exception").toString()); - getPlugin().getLogger().warning("Failed to execute rollback command", e); - return; } - } - - // Also delete records where the malicious mod was the original muter (pastActor) - DeleteBuilder maliciousIpMuteRecords = getPlugin().getIpMuteRecordStorage().deleteBuilder(); - maliciousIpMuteRecords.where().eq("pastActor_id", player.getId()).and().le("pastCreated", now).and().ge("pastCreated", expires); - maliciousIpMuteRecords.delete(); - - break; - - case "notes": - DeleteBuilder notes = getPlugin().getPlayerNoteStorage().deleteBuilder(); - notes.where().eq("actor_id", player.getId()).and().le("created", now).and().ge("created", expires); - notes.delete(); - break; - - case "reports": - QueryBuilder reports = getPlugin().getPlayerReportStorage().queryBuilder(); - reports.where().eq("actor_id", player.getId()).and().le("created", now).and().ge("created", expires); - - for (PlayerReportData record : reports.query()) { - getPlugin().getPlayerReportStorage().deleteById(record.getId()); - } - break; - - case "warnings": - DeleteBuilder warnings = getPlugin().getPlayerWarnStorage().deleteBuilder(); - warnings.where().eq("actor_id", player.getId()).and().le("created", now).and().ge("created", expires); - warnings.delete(); - break; + + DeleteBuilder maliciousIpMuteRecords = getPlugin().getIpMuteRecordStorage().deleteBuilder(); + maliciousIpMuteRecords.where().eq("pastActor_id", player.getId()).and().le("pastCreated", now).and().ge("pastCreated", expires); + maliciousIpMuteRecords.delete(); + + break; + + case "notes": + DeleteBuilder notes = getPlugin().getPlayerNoteStorage().deleteBuilder(); + notes.where().eq("actor_id", player.getId()).and().le("created", now).and().ge("created", expires); + notes.delete(); + break; + + case "reports": + QueryBuilder reportQb = getPlugin().getPlayerReportStorage().queryBuilder(); + reportQb.where().eq("actor_id", player.getId()).and().le("created", now).and().ge("created", expires); + List matchedReports = reportQb.query(); + + for (PlayerReportData report : matchedReports) { + getPlugin().getServer().callEvent("PlayerReportDeletedEvent", report); + } + + if (!matchedReports.isEmpty()) { + DeleteBuilder reports = getPlugin().getPlayerReportStorage().deleteBuilder(); + reports.where().eq("actor_id", player.getId()).and().le("created", now).and().ge("created", expires); + reports.delete(); + } + break; + + case "warnings": + DeleteBuilder warnings = getPlugin().getPlayerWarnStorage().deleteBuilder(); + warnings.where().eq("actor_id", player.getId()).and().le("created", now).and().ge("created", expires); + warnings.delete(); + break; + } } - } catch (SQLException e) { - sender.sendMessage(Message.get("sender.error.exception").toString()); - getPlugin().getLogger().warning("Failed to execute rollback command", e); - return; + }); + + for (String type : types) { + Message.get("bmrollback.notify") + .set("type", type) + .set("player", player.getName()) + .set("playerId", player.getUUID().toString()) + .sendTo(sender); } - - Message.get("bmrollback.notify") - .set("type", type) - .set("player", player.getName()) - .set("playerId", player.getUUID().toString()) - .sendTo(sender); + } catch (SQLException e) { + sender.sendMessage(Message.get("sender.error.exception").toString()); + getPlugin().getLogger().warning("Failed to execute rollback command", e); } }); diff --git a/common/src/main/java/me/confuser/banmanager/common/commands/UnbanCommand.java b/common/src/main/java/me/confuser/banmanager/common/commands/UnbanCommand.java index 1ccb12082..ce53ca001 100644 --- a/common/src/main/java/me/confuser/banmanager/common/commands/UnbanCommand.java +++ b/common/src/main/java/me/confuser/banmanager/common/commands/UnbanCommand.java @@ -55,6 +55,8 @@ public boolean onCommand(final CommonSender sender, CommandParser originalParser } final String reason = parser.getReason().getMessage(); + final boolean canOverride = sender.hasPermission("bm.exempt.override.ban"); + final boolean isOwnOnly = sender.hasPermission("bm.command.unban.own"); getPlugin().getScheduler().runAsync(() -> { PlayerBanData ban; @@ -72,9 +74,7 @@ public boolean onCommand(final CommonSender sender, CommandParser originalParser final PlayerData actor = sender.getData(); - //TODO refactor if async perm check is problem - if (!actor.getUUID().equals(ban.getActor().getUUID()) && !sender.hasPermission("bm.exempt.override.ban") - && sender.hasPermission("bm.command.unban.own")) { + if (!actor.getUUID().equals(ban.getActor().getUUID()) && !canOverride && isOwnOnly) { Message.get("unban.error.notOwn").set("player", ban.getPlayer().getName()).sendTo(sender); return; } diff --git a/common/src/main/java/me/confuser/banmanager/common/commands/UnmuteCommand.java b/common/src/main/java/me/confuser/banmanager/common/commands/UnmuteCommand.java index db22bbabc..1d46002be 100644 --- a/common/src/main/java/me/confuser/banmanager/common/commands/UnmuteCommand.java +++ b/common/src/main/java/me/confuser/banmanager/common/commands/UnmuteCommand.java @@ -56,6 +56,8 @@ public boolean onCommand(final CommonSender sender, CommandParser originalParser } final String reason = parser.getReason().getMessage(); + final boolean canOverride = sender.hasPermission("bm.exempt.override.mute"); + final boolean isOwnOnly = sender.hasPermission("bm.command.unmute.own"); getPlugin().getScheduler().runAsync(new Runnable() { @@ -77,9 +79,7 @@ public void run() { final PlayerData actor = sender.getData(); - //TODO refactor if async perm check is problem - if (!actor.getUUID().equals(mute.getActor().getUUID()) && !sender.hasPermission("bm.exempt.override.mute") - && sender.hasPermission("bm.command.unmute.own")) { + if (!actor.getUUID().equals(mute.getActor().getUUID()) && !canOverride && isOwnOnly) { Message.get("unmute.error.notOwn").set("player", mute.getPlayer().getName()).sendTo(sender); return; } diff --git a/common/src/main/java/me/confuser/banmanager/common/data/HistoryEntry.java b/common/src/main/java/me/confuser/banmanager/common/data/HistoryEntry.java new file mode 100644 index 000000000..fa39dce0f --- /dev/null +++ b/common/src/main/java/me/confuser/banmanager/common/data/HistoryEntry.java @@ -0,0 +1,33 @@ +package me.confuser.banmanager.common.data; + +import lombok.Getter; + +public class HistoryEntry { + + @Getter + private final int id; + + @Getter + private final String type; + + @Getter + private final String actor; + + @Getter + private final long created; + + @Getter + private final String reason; + + @Getter + private final String meta; + + public HistoryEntry(int id, String type, String actor, long created, String reason, String meta) { + this.id = id; + this.type = type; + this.actor = actor; + this.created = created; + this.reason = reason; + this.meta = meta; + } +} diff --git a/common/src/main/java/me/confuser/banmanager/common/listeners/CommonChatListener.java b/common/src/main/java/me/confuser/banmanager/common/listeners/CommonChatListener.java index a1f6d0ed4..289f72965 100755 --- a/common/src/main/java/me/confuser/banmanager/common/listeners/CommonChatListener.java +++ b/common/src/main/java/me/confuser/banmanager/common/listeners/CommonChatListener.java @@ -19,9 +19,12 @@ public CommonChatListener(BanManagerPlugin plugin) { } public boolean onPlayerChat(CommonPlayer player, CommonChatHandler handler, String chatMessage) { - if (!plugin.getPlayerMuteStorage().isMuted(player.getUniqueId())) { - if (plugin.getPlayerWarnStorage().isMuted(player.getUniqueId())) { - PlayerWarnData warning = plugin.getPlayerWarnStorage().getMute(player.getUniqueId()); + PlayerMuteData mute = plugin.getPlayerMuteStorage().getMute(player.getUniqueId()); + + if (mute == null) { + PlayerWarnData warning = plugin.getPlayerWarnStorage().getMute(player.getUniqueId()); + + if (warning != null) { if (warning.getReason().toLowerCase().equals(chatMessage.toLowerCase())) { plugin.getPlayerWarnStorage().removeMute(player.getUniqueId()); @@ -37,8 +40,6 @@ public boolean onPlayerChat(CommonPlayer player, CommonChatHandler handler, Stri return false; } - PlayerMuteData mute = plugin.getPlayerMuteStorage().getMute(player.getUniqueId()); - if (mute.hasExpired()) { plugin.getPlayerMuteStorage().removeMute(mute); @@ -91,12 +92,12 @@ public boolean onPlayerChat(CommonPlayer player, CommonChatHandler handler, Stri } public boolean onIpChat(CommonPlayer player, InetAddress address, CommonChatHandler handler, String chatMessage) { - if (!plugin.getIpMuteStorage().isMuted(address)) { + IpMuteData mute = plugin.getIpMuteStorage().getMute(address); + + if (mute == null) { return false; } - IpMuteData mute = plugin.getIpMuteStorage().getMute(address); - if (mute.hasExpired()) { plugin.getIpMuteStorage().removeMute(mute); diff --git a/common/src/main/java/me/confuser/banmanager/common/listeners/CommonCommandListener.java b/common/src/main/java/me/confuser/banmanager/common/listeners/CommonCommandListener.java index 7f0746060..6a149aae2 100755 --- a/common/src/main/java/me/confuser/banmanager/common/listeners/CommonCommandListener.java +++ b/common/src/main/java/me/confuser/banmanager/common/listeners/CommonCommandListener.java @@ -2,6 +2,7 @@ import me.confuser.banmanager.common.BanManagerPlugin; import me.confuser.banmanager.common.CommonPlayer; +import me.confuser.banmanager.common.data.PlayerMuteData; import me.confuser.banmanager.common.util.Message; import me.confuser.banmanager.common.util.StringUtils; @@ -13,7 +14,9 @@ public CommonCommandListener(BanManagerPlugin plugin) { } public boolean onCommand(CommonPlayer player, String cmd, String[] args) { - if (!plugin.getPlayerMuteStorage().isMuted(player.getUniqueId())) { + PlayerMuteData mute = plugin.getPlayerMuteStorage().getMute(player.getUniqueId()); + + if (mute == null) { return false; } @@ -23,7 +26,7 @@ public boolean onCommand(CommonPlayer player, String cmd, String[] args) { startIndex = 1; } - boolean isSoft = plugin.getPlayerMuteStorage().getMute(player.getUniqueId()).isSoft(); + boolean isSoft = mute.isSoft(); boolean deepCheck = isSoft ? !plugin.getConfig().isSoftBlockedCommand(cmd) : !plugin.getConfig().isBlockedCommand(cmd); if (deepCheck) { diff --git a/common/src/main/java/me/confuser/banmanager/common/listeners/CommonJoinListener.java b/common/src/main/java/me/confuser/banmanager/common/listeners/CommonJoinListener.java index d63115e41..21ce94a2c 100755 --- a/common/src/main/java/me/confuser/banmanager/common/listeners/CommonJoinListener.java +++ b/common/src/main/java/me/confuser/banmanager/common/listeners/CommonJoinListener.java @@ -88,12 +88,11 @@ public void banCheck(UUID id, String name, IPAddress address, CommonJoinHandler } } - if (plugin.getIpRangeBanStorage().isBanned(address)) { - IpRangeBanData data = plugin.getIpRangeBanStorage().getBan(address); - - if (data.hasExpired()) { + IpRangeBanData ipRangeBan = plugin.getIpRangeBanStorage().getBan(address); + if (ipRangeBan != null) { + if (ipRangeBan.hasExpired()) { try { - plugin.getIpRangeBanStorage().unban(data, plugin.getPlayerStorage().getConsole()); + plugin.getIpRangeBanStorage().unban(ipRangeBan, plugin.getPlayerStorage().getConsole()); } catch (SQLException e) { plugin.getLogger().warning("Failed to process player join", e); } @@ -101,41 +100,40 @@ public void banCheck(UUID id, String name, IPAddress address, CommonJoinHandler return; } - if (data.getExpires() == 0 && plugin.getExemptionsConfig().isExempt(id, "baniprange")) { + if (ipRangeBan.getExpires() == 0 && plugin.getExemptionsConfig().isExempt(id, "baniprange")) { return; - } else if (data.getExpires() != 0 && plugin.getExemptionsConfig().isExempt(id, "tempbaniprange")) { + } else if (ipRangeBan.getExpires() != 0 && plugin.getExemptionsConfig().isExempt(id, "tempbaniprange")) { return; } String dateTimeFormat; Message message; - if (data.getExpires() == 0) { + if (ipRangeBan.getExpires() == 0) { message = Message.get("baniprange.ip.disallowed"); dateTimeFormat = Message.getString("baniprange.ip.dateTimeFormat"); } else { message = Message.get("tempbaniprange.ip.disallowed"); - message.set("expires", DateUtils.getDifferenceFormat(data.getExpires())); + message.set("expires", DateUtils.getDifferenceFormat(ipRangeBan.getExpires())); dateTimeFormat = Message.getString("tempbaniprange.ip.dateTimeFormat"); } - message.set("id", data.getId()); + message.set("id", ipRangeBan.getId()); message.set("ip", address.toString()); - message.set("reason", data.getReason()); - message.set("actor", data.getActor().getName()); - message.set("created", DateUtils.format(dateTimeFormat, data.getCreated())); + message.set("reason", ipRangeBan.getReason()); + message.set("actor", ipRangeBan.getActor().getName()); + message.set("created", DateUtils.format(dateTimeFormat, ipRangeBan.getCreated())); handler.handleDeny(message); return; } - if (plugin.getIpBanStorage().isBanned(address)) { - IpBanData data = plugin.getIpBanStorage().getBan(address); - - if (data.hasExpired()) { + IpBanData ipBan = plugin.getIpBanStorage().getBan(address); + if (ipBan != null) { + if (ipBan.hasExpired()) { try { - plugin.getIpBanStorage().unban(data, plugin.getPlayerStorage().getConsole()); + plugin.getIpBanStorage().unban(ipBan, plugin.getPlayerStorage().getConsole()); } catch (SQLException e) { plugin.getLogger().warning("Failed to process player join", e); } @@ -146,33 +144,32 @@ public void banCheck(UUID id, String name, IPAddress address, CommonJoinHandler String dateTimeFormat; Message message; - if (data.getExpires() == 0) { + if (ipBan.getExpires() == 0) { message = Message.get("banip.ip.disallowed"); dateTimeFormat = Message.getString("banip.ip.dateTimeFormat"); } else { message = Message.get("tempbanip.ip.disallowed"); - message.set("expires", DateUtils.getDifferenceFormat(data.getExpires())); + message.set("expires", DateUtils.getDifferenceFormat(ipBan.getExpires())); dateTimeFormat = Message.getString("tempbanip.ip.dateTimeFormat"); } - message.set("id", data.getId()); + message.set("id", ipBan.getId()); message.set("ip", address.toString()); - message.set("reason", data.getReason()); - message.set("actor", data.getActor().getName()); - message.set("created", DateUtils.format(dateTimeFormat, data.getCreated())); + message.set("reason", ipBan.getReason()); + message.set("actor", ipBan.getActor().getName()); + message.set("created", DateUtils.format(dateTimeFormat, ipBan.getCreated())); handler.handleDeny(message); - handleJoinDeny(address.toString(), data.getActor(), data.getReason()); + handleJoinDeny(address.toString(), ipBan.getActor(), ipBan.getReason()); return; } - if (plugin.getNameBanStorage().isBanned(name)) { - NameBanData data = plugin.getNameBanStorage().getBan(name); - - if (data.hasExpired()) { + NameBanData nameBan = plugin.getNameBanStorage().getBan(name); + if (nameBan != null) { + if (nameBan.hasExpired()) { try { - plugin.getNameBanStorage().unban(data, plugin.getPlayerStorage().getConsole()); + plugin.getNameBanStorage().unban(nameBan, plugin.getPlayerStorage().getConsole()); } catch (SQLException e) { plugin.getLogger().warning("Failed to process player join", e); } @@ -183,21 +180,21 @@ public void banCheck(UUID id, String name, IPAddress address, CommonJoinHandler String dateTimeFormat; Message message; - if (data.getExpires() == 0) { + if (nameBan.getExpires() == 0) { message = Message.get("banname.name.disallowed"); dateTimeFormat = Message.getString("banname.name.dateTimeFormat"); } else { message = Message.get("tempbanname.name.disallowed"); - message.set("expires", DateUtils.getDifferenceFormat(data.getExpires())); + message.set("expires", DateUtils.getDifferenceFormat(nameBan.getExpires())); dateTimeFormat = Message.getString("tempbanname.name.dateTimeFormat"); } - message.set("id", data.getId()); + message.set("id", nameBan.getId()); message.set("name", name); - message.set("reason", data.getReason()); - message.set("actor", data.getActor().getName()); - message.set("created", DateUtils.format(dateTimeFormat, data.getCreated())); + message.set("reason", nameBan.getReason()); + message.set("actor", nameBan.getActor().getName()); + message.set("created", DateUtils.format(dateTimeFormat, nameBan.getCreated())); handler.handleDeny(message); return; @@ -323,8 +320,10 @@ public void onJoin(final CommonPlayer player) { CloseableIterator warnings = null; try { warnings = plugin.getPlayerWarnStorage().getUnreadWarnings(id); + boolean hasWarnings = false; while (warnings.hasNext()) { + hasWarnings = true; PlayerWarnData warning = warnings.next(); Message.get("warn.player.warned") @@ -334,10 +333,10 @@ public void onJoin(final CommonPlayer player) { .set("actor", warning.getActor().getName()) .set("id", warning.getId()) .sendTo(plugin.getServer().getPlayer(player.getUniqueId())); + } - warning.setRead(true); - // TODO Move to one update query to set all warnings for player to read - plugin.getPlayerWarnStorage().update(warning); + if (hasWarnings) { + plugin.getPlayerWarnStorage().markAllRead(id); } } catch (SQLException e) { plugin.getLogger().warning("Failed to process player join", e); @@ -559,8 +558,10 @@ private void denyAlts(List duplicates, final UUID uuid) { } private void punishAlts(List duplicates, UUID uuid) throws SQLException { + PlayerData targetPlayer = plugin.getPlayerStorage().queryForId(UUIDUtils.toBytes(uuid)); + PlayerData console = plugin.getPlayerStorage().getConsole(); + if (!plugin.getPlayerBanStorage().isBanned(uuid)) { - // Auto ban for (PlayerData player : duplicates) { if (player.getUUID().equals(uuid)) { continue; @@ -571,8 +572,7 @@ private void punishAlts(List duplicates, UUID uuid) throws SQLExcept if (ban == null) continue; if (ban.hasExpired()) continue; - final PlayerBanData newBan = new PlayerBanData(plugin.getPlayerStorage().queryForId(UUIDUtils.toBytes(uuid)), - plugin.getPlayerStorage().getConsole(), + final PlayerBanData newBan = new PlayerBanData(targetPlayer, console, ban.getReason(), ban.isSilent(), ban.getExpires()); @@ -580,7 +580,6 @@ private void punishAlts(List duplicates, UUID uuid) throws SQLExcept try { plugin.getPlayerBanStorage().ban(newBan); } catch (SQLIntegrityConstraintViolationException e) { - // Ignore duplicate entry errors plugin.getPlayerBanStorage().addBan(newBan); } @@ -598,7 +597,6 @@ private void punishAlts(List duplicates, UUID uuid) throws SQLExcept }); } } else if (!plugin.getPlayerMuteStorage().isMuted(uuid)) { - // Auto mute for (PlayerData player : duplicates) { if (player.getUUID().equals(uuid)) { continue; @@ -609,8 +607,7 @@ private void punishAlts(List duplicates, UUID uuid) throws SQLExcept if (mute == null) continue; if (mute.hasExpired()) continue; - PlayerMuteData newMute = new PlayerMuteData(plugin.getPlayerStorage().queryForId(UUIDUtils.toBytes(uuid)), - plugin.getPlayerStorage().getConsole(), + PlayerMuteData newMute = new PlayerMuteData(targetPlayer, console, mute.getReason(), mute.isSilent(), mute.isSoft(), @@ -619,7 +616,6 @@ private void punishAlts(List duplicates, UUID uuid) throws SQLExcept try { plugin.getPlayerMuteStorage().mute(newMute); } catch (SQLIntegrityConstraintViolationException e) { - // Ignore duplicate entry errors plugin.getPlayerMuteStorage().addMute(newMute); } } diff --git a/common/src/main/java/me/confuser/banmanager/common/runnables/RollbackSync.java b/common/src/main/java/me/confuser/banmanager/common/runnables/RollbackSync.java index 6d77a42f4..bafaac7be 100644 --- a/common/src/main/java/me/confuser/banmanager/common/runnables/RollbackSync.java +++ b/common/src/main/java/me/confuser/banmanager/common/runnables/RollbackSync.java @@ -5,9 +5,11 @@ import me.confuser.banmanager.common.ormlite.dao.CloseableIterator; import java.sql.SQLException; -import java.util.Iterator; import java.util.Map; import java.util.UUID; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; public class RollbackSync extends BmRunnable { @@ -26,142 +28,64 @@ public void run() { final RollbackData data = itr.next(); switch (data.getType()) { - // @TODO Refactor/Clean up case "bans": - for (Iterator> it = plugin.getPlayerBanStorage().getBans().entrySet() - .iterator(); it.hasNext(); ) { - Map.Entry entry = it.next(); - - if (!entry.getValue().getActor().getUUID().equals(data.getPlayer().getUUID())) continue; - if (!(entry.getValue().getCreated() <= data.getCreated() && entry.getValue().getCreated() >= data - .getExpires())) continue; - - it.remove(); - } + evictFromCache(plugin.getPlayerBanStorage().getBans(), + v -> v.getActor().getUUID(), PlayerBanData::getCreated, data); break; case "ipbans": - for (Iterator> it = plugin.getIpBanStorage().getBans().entrySet().iterator(); it - .hasNext(); ) { - Map.Entry entry = it.next(); - - if (!entry.getValue().getActor().getUUID().equals(data.getPlayer().getUUID())) continue; - if (!(entry.getValue().getCreated() <= data.getCreated() && entry.getValue().getCreated() >= data - .getExpires())) continue; - - it.remove(); - } + evictFromCache(plugin.getIpBanStorage().getBans(), + v -> v.getActor().getUUID(), IpBanData::getCreated, data); break; case "ipmutes": - for (Iterator> it = plugin.getIpMuteStorage().getMutes().entrySet() - .iterator(); it.hasNext(); ) { - Map.Entry entry = it.next(); - - if (!entry.getValue().getActor().getUUID().equals(data.getPlayer().getUUID())) continue; - if (!(entry.getValue().getCreated() <= data.getCreated() && entry.getValue().getCreated() >= data - .getExpires())) continue; - - it.remove(); - } + evictFromCache(plugin.getIpMuteStorage().getMutes(), + v -> v.getActor().getUUID(), IpMuteData::getCreated, data); break; case "mutes": - for (Iterator> it = plugin.getPlayerMuteStorage().getMutes().entrySet() - .iterator(); it.hasNext(); ) { - Map.Entry entry = it.next(); - - if (!entry.getValue().getActor().getUUID().equals(data.getPlayer().getUUID())) continue; - if (!(entry.getValue().getCreated() <= data.getCreated() && entry.getValue().getCreated() >= data - .getExpires())) continue; - - it.remove(); - } + evictFromCache(plugin.getPlayerMuteStorage().getMutes(), + v -> v.getActor().getUUID(), PlayerMuteData::getCreated, data); break; case "banrecords": - // Sync restored bans to in-memory cache - // Query for bans that were restored (created within rollback timeframe) - CloseableIterator restoredBans = plugin.getPlayerBanStorage() - .queryBuilder() - .where() + restoreToCache(plugin.getPlayerBanStorage() + .queryBuilder().where() .le("created", data.getCreated()) .and().ge("created", data.getExpires()) - .iterator(); - - try { - while (restoredBans.hasNext()) { - PlayerBanData ban = restoredBans.next(); - if (!plugin.getPlayerBanStorage().isBanned(ban.getPlayer().getUUID())) { - plugin.getPlayerBanStorage().addBan(ban); - } - } - } finally { - restoredBans.closeQuietly(); - } + .iterator(), + ban -> !plugin.getPlayerBanStorage().isBanned(ban.getPlayer().getUUID()), + ban -> plugin.getPlayerBanStorage().addBan(ban)); break; case "ipbanrecords": - // Sync restored IP bans to in-memory cache - CloseableIterator restoredIpBans = plugin.getIpBanStorage() - .queryBuilder() - .where() + restoreToCache(plugin.getIpBanStorage() + .queryBuilder().where() .le("created", data.getCreated()) .and().ge("created", data.getExpires()) - .iterator(); - - try { - while (restoredIpBans.hasNext()) { - IpBanData ban = restoredIpBans.next(); - if (!plugin.getIpBanStorage().isBanned(ban.getIp())) { - plugin.getIpBanStorage().addBan(ban); - } - } - } finally { - restoredIpBans.closeQuietly(); - } + .iterator(), + ban -> !plugin.getIpBanStorage().isBanned(ban.getIp()), + ban -> plugin.getIpBanStorage().addBan(ban)); break; case "muterecords": - // Sync restored mutes to in-memory cache - CloseableIterator restoredMutes = plugin.getPlayerMuteStorage() - .queryBuilder() - .where() + restoreToCache(plugin.getPlayerMuteStorage() + .queryBuilder().where() .le("created", data.getCreated()) .and().ge("created", data.getExpires()) - .iterator(); - - try { - while (restoredMutes.hasNext()) { - PlayerMuteData mute = restoredMutes.next(); - if (!plugin.getPlayerMuteStorage().isMuted(mute.getPlayer().getUUID())) { - plugin.getPlayerMuteStorage().addMute(mute); - } - } - } finally { - restoredMutes.closeQuietly(); - } + .iterator(), + mute -> !plugin.getPlayerMuteStorage().isMuted(mute.getPlayer().getUUID()), + mute -> plugin.getPlayerMuteStorage().addMute(mute)); break; case "ipmuterecords": - // Sync restored IP mutes to in-memory cache - CloseableIterator restoredIpMutes = plugin.getIpMuteStorage() - .queryBuilder() - .where() + restoreToCache(plugin.getIpMuteStorage() + .queryBuilder().where() .le("created", data.getCreated()) .and().ge("created", data.getExpires()) - .iterator(); - - try { - while (restoredIpMutes.hasNext()) { - IpMuteData mute = restoredIpMutes.next(); - if (!plugin.getIpMuteStorage().isMuted(mute.getIp())) { - plugin.getIpMuteStorage().addMute(mute); - } - } - } finally { - restoredIpMutes.closeQuietly(); - } + .iterator(), + mute -> !plugin.getIpMuteStorage().isMuted(mute.getIp()), + mute -> plugin.getIpMuteStorage().addMute(mute)); break; } } @@ -172,4 +96,33 @@ public void run() { if (itr != null) itr.closeQuietly(); } } + + private void evictFromCache(Map cache, Function getActor, + Function getCreated, RollbackData data) { + UUID actorUUID = data.getPlayer().getUUID(); + long created = data.getCreated(); + long expires = data.getExpires(); + + cache.entrySet().removeIf(entry -> { + V value = entry.getValue(); + long entryCreated = getCreated.apply(value); + return getActor.apply(value).equals(actorUUID) + && entryCreated <= created + && entryCreated >= expires; + }); + } + + private void restoreToCache(CloseableIterator itr, + Predicate shouldRestore, Consumer action) { + try { + while (itr.hasNext()) { + T item = itr.next(); + if (shouldRestore.test(item)) { + action.accept(item); + } + } + } finally { + itr.closeQuietly(); + } + } } diff --git a/common/src/main/java/me/confuser/banmanager/common/storage/ActivityStorage.java b/common/src/main/java/me/confuser/banmanager/common/storage/ActivityStorage.java index 21205283a..3b7f076a9 100644 --- a/common/src/main/java/me/confuser/banmanager/common/storage/ActivityStorage.java +++ b/common/src/main/java/me/confuser/banmanager/common/storage/ActivityStorage.java @@ -9,6 +9,7 @@ import me.confuser.banmanager.common.ormlite.support.DatabaseResults; import me.confuser.banmanager.common.util.IPUtils; +import java.io.IOException; import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; @@ -246,86 +247,62 @@ public List> getSince(long since) { } public List> getSince(long since, PlayerData actor) { - DatabaseConnection connection; - - try { - connection = plugin.getLocalConn().getReadOnlyConnection(""); - } catch (SQLException e) { - plugin.getLogger().warning("Failed to process activity operation", e); - - return null; - } - - final DatabaseResults result; boolean hasActor = actor != null; - try { + try (DatabaseConnection connection = plugin.getLocalConn().getReadOnlyConnection("")) { CompiledStatement statement = connection .compileStatement(hasActor ? sincePlayerSql : sinceSql, StatementBuilder.StatementType.SELECT, null, DatabaseConnection.DEFAULT_RESULT_FLAGS, false); - int maxItems = hasActor ? 28 : 14; - - for (int i = 0; i < maxItems; i++) { - statement.setObject(i, since, SqlType.LONG); - if (hasActor) { - i++; - statement.setObject(i, actor.getId(), SqlType.BYTE_ARRAY); - } - } - result = statement.runQuery(null); - } catch (SQLException e) { - plugin.getLogger().warning("Failed to process activity operation", e); - + List> results = new ArrayList<>(); try { - plugin.getLocalConn().releaseConnection(connection); - } catch (SQLException e1) { - plugin.getLogger().warning("Failed to process activity operation", e1); - } + int maxItems = hasActor ? 28 : 14; + + for (int i = 0; i < maxItems; i++) { + statement.setObject(i, since, SqlType.LONG); + if (hasActor) { + i++; + statement.setObject(i, actor.getId(), SqlType.BYTE_ARRAY); + } + } - return null; - } + DatabaseResults result = statement.runQuery(null); + try { + while (result.next()) { + Map map = new HashMap<>(hasActor ? 3 : 4); - List> results = new ArrayList<>(); + int ipIndex = 3; + map.put("type", result.getString(0)); - try { - while (result.next()) { - Map map = new HashMap<>(hasActor ? 3 : 4); + if (hasActor) { + map.put("created", result.getLong(2)); + } else { + map.put("actor", result.getString(2)); + map.put("created", result.getLong(3)); + ipIndex = 4; + } - int ipIndex = 3; - map.put("type", result.getString(0)); + String ip = result.getString(1); - if (hasActor) { - map.put("created", result.getLong(2)); - } else { - map.put("actor", result.getString(2)); - map.put("created", result.getLong(3)); - ipIndex = 4; - } + if (!result.getString(ipIndex).isEmpty()) { + ip = ip + " - " + result.getString(ipIndex); + } - // ip or name - String ip = result.getString(1); + map.put("player", ip); - if (!result.getString(ipIndex).isEmpty()) { - ip = ip + " - " + result.getString(ipIndex); + results.add(map); + } + } finally { + result.closeQuietly(); } - - map.put("player", ip); - - results.add(map); + } finally { + try { statement.close(); } catch (IOException ignored) { } } - } catch (SQLException e) { - plugin.getLogger().warning("Failed to process activity operation", e); - } finally { - result.closeQuietly(); - } - try { - plugin.getLocalConn().releaseConnection(connection); - } catch (SQLException e) { + return results; + } catch (SQLException | IOException e) { plugin.getLogger().warning("Failed to process activity operation", e); + return null; } - - return results; } } diff --git a/common/src/main/java/me/confuser/banmanager/common/storage/HistoryStorage.java b/common/src/main/java/me/confuser/banmanager/common/storage/HistoryStorage.java index 655acab54..0a9e64489 100644 --- a/common/src/main/java/me/confuser/banmanager/common/storage/HistoryStorage.java +++ b/common/src/main/java/me/confuser/banmanager/common/storage/HistoryStorage.java @@ -1,6 +1,7 @@ package me.confuser.banmanager.common.storage; import me.confuser.banmanager.common.BanManagerPlugin; +import me.confuser.banmanager.common.data.HistoryEntry; import me.confuser.banmanager.common.data.PlayerData; import me.confuser.banmanager.common.ipaddr.IPAddress; import me.confuser.banmanager.common.ormlite.field.SqlType; @@ -10,9 +11,10 @@ import me.confuser.banmanager.common.ormlite.support.DatabaseResults; import me.confuser.banmanager.common.util.parsers.InfoCommandParser; +import java.io.IOException; import java.sql.SQLException; import java.util.ArrayList; -import java.util.HashMap; +import java.util.List; public class HistoryStorage { @@ -21,7 +23,7 @@ public class HistoryStorage { " ) subquery" + " ORDER BY created DESC"; private BanManagerPlugin plugin; - // Queries + private final String banSql; private final String muteSql; private final String kickSql; @@ -89,402 +91,102 @@ public HistoryStorage(BanManagerPlugin plugin) { " WHERE ? BETWEEN fromIp AND toIp"; } - public ArrayList> getSince(PlayerData player, long since, InfoCommandParser parser) { - DatabaseConnection connection; - - try { - connection = plugin.getLocalConn().getReadOnlyConnection(""); - } catch (SQLException e) { - plugin.getLogger().warning("Failed to process history operation", e); - - return null; - } - - final DatabaseResults result; - String sql; + public List getSince(PlayerData player, long since, InfoCommandParser parser) { StringBuilder unions = new StringBuilder(); int typeCount = 0; - // TODO refactor - if (parser.isBans()) { - unions.append(banSql); - unions.append(" AND created >= ").append(since); - unions.append(" UNION ALL "); - typeCount++; - } - - if (parser.isMutes()) { - unions.append(muteSql); - unions.append(" AND created >= ").append(since); - unions.append(" UNION ALL "); - typeCount++; - } - if (parser.isKicks()) { - unions.append(kickSql); - unions.append(" AND created >= ").append(since); - unions.append(" UNION ALL "); - typeCount++; - } - if (parser.isNotes()) { - unions.append(noteSql); - unions.append(" AND created >= ").append(since); - unions.append(" UNION ALL "); - typeCount++; - } - if (parser.isReports()) { - unions.append(reportSql); - unions.append(" AND created >= ").append(since); - unions.append(" UNION ALL "); - typeCount++; - } - if (parser.isWarnings()) { - unions.append(warningSql); - unions.append(" AND created >= ").append(since); - unions.append(" UNION ALL "); - typeCount++; - } - - unions.setLength(unions.length() - 11); - - sql = playerSql.replace("{QUERIES}", unions.toString()); - - try { - CompiledStatement statement = connection - .compileStatement(sql, StatementBuilder.StatementType.SELECT, null, DatabaseConnection - .DEFAULT_RESULT_FLAGS, false); - - for (int i = 0; i < typeCount; i++) { - statement.setObject(i, player.getId(), SqlType.BYTE_ARRAY); - } - - result = statement.runQuery(null); - } catch (SQLException e) { - plugin.getLogger().warning("Failed to process history operation", e); - - try { - plugin.getLocalConn().releaseConnection(connection); - } catch (SQLException e1) { - plugin.getLogger().warning("Failed to process history operation", e1); - } - - return null; - } - - ArrayList> results = new ArrayList<>(); - - try { - while (result.next()) { - results.add(new HashMap(4) { - - { - put("id", result.getInt(0)); - put("type", result.getString(1)); - put("actor", result.getString(2)); - put("created", result.getLong(3)); - put("reason", result.getString(4)); - put("meta", result.getString(5)); - } - }); - } - } catch (SQLException e) { - plugin.getLogger().warning("Failed to process history operation", e); - } finally { - result.closeQuietly(); - } - - try { - plugin.getLocalConn().releaseConnection(connection); - } catch (SQLException e) { - plugin.getLogger().warning("Failed to process history operation", e); - } + if (parser.isBans()) { unions.append(banSql).append(" AND created >= ").append(since).append(" UNION ALL "); typeCount++; } + if (parser.isMutes()) { unions.append(muteSql).append(" AND created >= ").append(since).append(" UNION ALL "); typeCount++; } + if (parser.isKicks()) { unions.append(kickSql).append(" AND created >= ").append(since).append(" UNION ALL "); typeCount++; } + if (parser.isNotes()) { unions.append(noteSql).append(" AND created >= ").append(since).append(" UNION ALL "); typeCount++; } + if (parser.isReports()) { unions.append(reportSql).append(" AND created >= ").append(since).append(" UNION ALL "); typeCount++; } + if (parser.isWarnings()) { unions.append(warningSql).append(" AND created >= ").append(since).append(" UNION ALL "); typeCount++; } - return results; + return executeQuery(player.getId(), SqlType.BYTE_ARRAY, unions, typeCount); } - public ArrayList> getAll(PlayerData player, InfoCommandParser parser) { - DatabaseConnection connection; - - try { - connection = plugin.getLocalConn().getReadOnlyConnection(""); - } catch (SQLException e) { - plugin.getLogger().warning("Failed to process history operation", e); - - return null; - } - - final DatabaseResults result; - String sql; + public List getAll(PlayerData player, InfoCommandParser parser) { StringBuilder unions = new StringBuilder(); int typeCount = 0; - // TODO refactor - if (parser.isBans()) { - unions.append(banSql); - unions.append(" UNION ALL "); - typeCount++; - } - - if (parser.isMutes()) { - unions.append(muteSql); - unions.append(" UNION ALL "); - typeCount++; - } - if (parser.isKicks()) { - unions.append(kickSql); - unions.append(" UNION ALL "); - typeCount++; - } - if (parser.isNotes()) { - unions.append(noteSql); - unions.append(" UNION ALL "); - typeCount++; - } - if (parser.isReports()) { - unions.append(reportSql); - unions.append(" UNION ALL "); - typeCount++; - } - if (parser.isWarnings()) { - unions.append(warningSql); - unions.append(" UNION ALL "); - typeCount++; - } - - unions.setLength(unions.length() - 11); - - sql = playerSql.replace("{QUERIES}", unions.toString()); - - try { - CompiledStatement statement = connection - .compileStatement(sql, StatementBuilder.StatementType.SELECT, null, DatabaseConnection - .DEFAULT_RESULT_FLAGS, false); - - for (int i = 0; i < typeCount; i++) { - statement.setObject(i, player.getId(), SqlType.BYTE_ARRAY); - } - - result = statement.runQuery(null); - } catch (SQLException e) { - plugin.getLogger().warning("Failed to process history operation", e); - - try { - plugin.getLocalConn().releaseConnection(connection); - } catch (SQLException e1) { - plugin.getLogger().warning("Failed to process history operation", e1); - } - - return null; - } - - ArrayList> results = new ArrayList<>(); + if (parser.isBans()) { unions.append(banSql).append(" UNION ALL "); typeCount++; } + if (parser.isMutes()) { unions.append(muteSql).append(" UNION ALL "); typeCount++; } + if (parser.isKicks()) { unions.append(kickSql).append(" UNION ALL "); typeCount++; } + if (parser.isNotes()) { unions.append(noteSql).append(" UNION ALL "); typeCount++; } + if (parser.isReports()) { unions.append(reportSql).append(" UNION ALL "); typeCount++; } + if (parser.isWarnings()) { unions.append(warningSql).append(" UNION ALL "); typeCount++; } - try { - while (result.next()) { - results.add(new HashMap(4) { - - { - put("id", result.getInt(0)); - put("type", result.getString(1)); - put("actor", result.getString(2)); - put("created", result.getLong(3)); - put("reason", result.getString(4)); - put("meta", result.getString(5)); - } - }); - } - } catch (SQLException e) { - plugin.getLogger().warning("Failed to process history operation", e); - } finally { - result.closeQuietly(); - } - - try { - plugin.getLocalConn().releaseConnection(connection); - } catch (SQLException e) { - plugin.getLogger().warning("Failed to process history operation", e); - } - - return results; + return executeQuery(player.getId(), SqlType.BYTE_ARRAY, unions, typeCount); } - public ArrayList> getSince(IPAddress ip, long since, InfoCommandParser parser) { - DatabaseConnection connection; - - try { - connection = plugin.getLocalConn().getReadOnlyConnection(""); - } catch (SQLException e) { - plugin.getLogger().warning("Failed to process history operation", e); - - return null; - } - - final DatabaseResults result; - String sql; + public List getSince(IPAddress ip, long since, InfoCommandParser parser) { StringBuilder unions = new StringBuilder(); int typeCount = 0; if (parser.isBans()) { - unions.append(ipBanSql); - unions.append(" AND created >= ").append(since); - unions.append(" UNION ALL "); - typeCount++; - - unions.append(ipRangeBanSql); - unions.append(" AND created >= ").append(since); - unions.append(" UNION ALL "); - typeCount++; - } - - if (parser.isMutes()) { - unions.append(ipMuteSql); - unions.append(" AND created >= ").append(since); - unions.append(" UNION ALL "); - typeCount++; + unions.append(ipBanSql).append(" AND created >= ").append(since).append(" UNION ALL "); typeCount++; + unions.append(ipRangeBanSql).append(" AND created >= ").append(since).append(" UNION ALL "); typeCount++; } + if (parser.isMutes()) { unions.append(ipMuteSql).append(" AND created >= ").append(since).append(" UNION ALL "); typeCount++; } - unions.setLength(unions.length() - 11); - - sql = playerSql.replace("{QUERIES}", unions.toString()); - - try { - CompiledStatement statement = connection - .compileStatement(sql, StatementBuilder.StatementType.SELECT, null, DatabaseConnection - .DEFAULT_RESULT_FLAGS, false); - - for (int i = 0; i < typeCount; i++) { - statement.setObject(i, ip.getBytes(), SqlType.BYTE_ARRAY); - } - - result = statement.runQuery(null); - } catch (SQLException e) { - plugin.getLogger().warning("Failed to process history operation", e); - - try { - plugin.getLocalConn().releaseConnection(connection); - } catch (SQLException e1) { - plugin.getLogger().warning("Failed to process history operation", e1); - } - - return null; - } - - ArrayList> results = new ArrayList<>(); - - try { - while (result.next()) { - results.add(new HashMap(4) { - - { - put("id", result.getInt(0)); - put("type", result.getString(1)); - put("actor", result.getString(2)); - put("created", result.getLong(3)); - put("reason", result.getString(4)); - put("meta", result.getString(5)); - } - }); - } - } catch (SQLException e) { - plugin.getLogger().warning("Failed to process history operation", e); - } finally { - result.closeQuietly(); - } - - try { - plugin.getLocalConn().releaseConnection(connection); - } catch (SQLException e) { - plugin.getLogger().warning("Failed to process history operation", e); - } - - return results; + return executeQuery(ip.getBytes(), SqlType.BYTE_ARRAY, unions, typeCount); } - public ArrayList> getAll(IPAddress ip, InfoCommandParser parser) { - DatabaseConnection connection; - - try { - connection = plugin.getLocalConn().getReadOnlyConnection(""); - } catch (SQLException e) { - plugin.getLogger().warning("Failed to process history operation", e); - - return null; - } - - final DatabaseResults result; - String sql; + public List getAll(IPAddress ip, InfoCommandParser parser) { StringBuilder unions = new StringBuilder(); int typeCount = 0; if (parser.isBans()) { - unions.append(ipBanSql); - unions.append(" UNION ALL "); - typeCount++; - - unions.append(ipRangeBanSql); - unions.append(" UNION ALL "); - typeCount++; + unions.append(ipBanSql).append(" UNION ALL "); typeCount++; + unions.append(ipRangeBanSql).append(" UNION ALL "); typeCount++; } + if (parser.isMutes()) { unions.append(ipMuteSql).append(" UNION ALL "); typeCount++; } + + return executeQuery(ip.getBytes(), SqlType.BYTE_ARRAY, unions, typeCount); + } - if (parser.isMutes()) { - unions.append(ipMuteSql); - unions.append(" UNION ALL "); - typeCount++; + private List executeQuery(Object paramValue, SqlType paramType, + StringBuilder unions, int typeCount) { + if (typeCount == 0) { + return new ArrayList<>(); } unions.setLength(unions.length() - 11); + String sql = playerSql.replace("{QUERIES}", unions.toString()); - sql = playerSql.replace("{QUERIES}", unions.toString()); - - try { - CompiledStatement statement = connection - .compileStatement(sql, StatementBuilder.StatementType.SELECT, null, DatabaseConnection - .DEFAULT_RESULT_FLAGS, false); - - for (int i = 0; i < typeCount; i++) { - statement.setObject(i, ip.getBytes(), SqlType.BYTE_ARRAY); - } - - result = statement.runQuery(null); - } catch (SQLException e) { - plugin.getLogger().warning("Failed to process history operation", e); + try (DatabaseConnection connection = plugin.getLocalConn().getReadOnlyConnection("")) { + CompiledStatement statement = connection.compileStatement( + sql, StatementBuilder.StatementType.SELECT, null, + DatabaseConnection.DEFAULT_RESULT_FLAGS, false); + List results = new ArrayList<>(); try { - plugin.getLocalConn().releaseConnection(connection); - } catch (SQLException e1) { - plugin.getLogger().warning("Failed to process history operation", e1); - } - - return null; - } - - ArrayList> results = new ArrayList<>(); - - try { - while (result.next()) { - results.add(new HashMap(4) { - - { - put("id", result.getInt(0)); - put("type", result.getString(1)); - put("actor", result.getString(2)); - put("created", result.getLong(3)); - put("reason", result.getString(4)); - put("meta", result.getString(5)); + for (int i = 0; i < typeCount; i++) { + statement.setObject(i, paramValue, paramType); + } + + DatabaseResults dbResults = statement.runQuery(null); + try { + while (dbResults.next()) { + results.add(new HistoryEntry( + dbResults.getInt(0), + dbResults.getString(1), + dbResults.getString(2), + dbResults.getLong(3), + dbResults.getString(4), + dbResults.getString(5))); } - }); + } finally { + dbResults.closeQuietly(); + } + } finally { + try { statement.close(); } catch (IOException ignored) { } } - } catch (SQLException e) { - plugin.getLogger().warning("Failed to process history operation", e); - } finally { - result.closeQuietly(); - } - try { - plugin.getLocalConn().releaseConnection(connection); - } catch (SQLException e) { + return results; + } catch (SQLException | IOException e) { plugin.getLogger().warning("Failed to process history operation", e); + return null; } - - return results; } } diff --git a/common/src/main/java/me/confuser/banmanager/common/storage/PlayerHistoryStorage.java b/common/src/main/java/me/confuser/banmanager/common/storage/PlayerHistoryStorage.java index b463b8ece..134dc4b77 100644 --- a/common/src/main/java/me/confuser/banmanager/common/storage/PlayerHistoryStorage.java +++ b/common/src/main/java/me/confuser/banmanager/common/storage/PlayerHistoryStorage.java @@ -12,13 +12,19 @@ import me.confuser.banmanager.common.ormlite.table.DatabaseTableConfig; import me.confuser.banmanager.common.ormlite.table.TableUtils; +import me.confuser.banmanager.common.ormlite.field.SqlType; +import me.confuser.banmanager.common.ormlite.stmt.StatementBuilder; +import me.confuser.banmanager.common.ormlite.support.CompiledStatement; +import me.confuser.banmanager.common.ormlite.support.DatabaseConnection; +import me.confuser.banmanager.common.ormlite.support.DatabaseResults; + +import java.io.IOException; import java.sql.SQLException; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; public class PlayerHistoryStorage extends BaseDaoImpl { @@ -147,40 +153,38 @@ public CloseableIterator getSince(PlayerData player, long sin * @throws SQLException if database error occurs */ public List getNamesSummary(PlayerData player) throws SQLException { - List sessions = queryBuilder() - .where() - .eq("player_id", player) - .query(); - - if (sessions.isEmpty()) { - return new ArrayList<>(); - } - - Map nameStats = new HashMap<>(); - - for (PlayerHistoryData session : sessions) { - String name = session.getName(); - if (name == null || name.isEmpty()) continue; + String table = getTableInfo().getTableName(); + String sql = "SELECT `name`, MIN(`join`) AS firstSeen, MAX(`leave`) AS lastSeen" + + " FROM `" + table + "`" + + " WHERE `player_id` = ? AND `name` IS NOT NULL AND `name` != ''" + + " GROUP BY `name` ORDER BY lastSeen DESC"; - long joinTime = session.getJoin(); - long leaveTime = session.getLeave(); + List summaries = new ArrayList<>(); - if (!nameStats.containsKey(name)) { - nameStats.put(name, new long[]{joinTime, leaveTime}); - } else { - long[] stats = nameStats.get(name); - stats[0] = Math.min(stats[0], joinTime); - stats[1] = Math.max(stats[1], leaveTime); + try (DatabaseConnection connection = connectionSource.getReadOnlyConnection(table)) { + CompiledStatement statement = connection.compileStatement( + sql, StatementBuilder.StatementType.SELECT, null, + DatabaseConnection.DEFAULT_RESULT_FLAGS, false); + try { + statement.setObject(0, player.getId(), SqlType.BYTE_ARRAY); + DatabaseResults results = statement.runQuery(null); + try { + while (results.next()) { + summaries.add(new PlayerNameSummary( + results.getString(0), + results.getLong(1), + results.getLong(2))); + } + } finally { + try { results.close(); } catch (IOException ignored) { } + } + } finally { + try { statement.close(); } catch (IOException ignored) { } } + } catch (IOException e) { + throw new SQLException("Failed to query name summary", e); } - List summaries = new ArrayList<>(); - for (Map.Entry entry : nameStats.entrySet()) { - summaries.add(new PlayerNameSummary(entry.getKey(), entry.getValue()[0], entry.getValue()[1])); - } - - summaries.sort((a, b) -> Long.compare(b.getLastSeen(), a.getLastSeen())); - return summaries; } @@ -220,15 +224,20 @@ public void save() { String table = getTableInfo().getTableName(); String nowExpr = dbConfig.getTimestampNow(); - for (Map.Entry entry : activeSessions.entrySet()) { + List ids = new ArrayList<>(activeSessions.values()); + + for (int i = 0; i < ids.size(); i += 500) { + List batch = ids.subList(i, Math.min(i + 500, ids.size())); + String csv = batch.stream().map(String::valueOf).collect(Collectors.joining(",")); try { - updateRaw("UPDATE `" + table + "` SET `leave` = " + nowExpr + " WHERE `id` = ?", - String.valueOf(entry.getValue())); + updateRaw("UPDATE `" + table + "` SET `leave` = " + nowExpr + + " WHERE `id` IN (" + csv + ")"); } catch (SQLException e) { plugin.getLogger().warning("Failed to process player history operation", e); break; } } + activeSessions.clear(); } @@ -242,24 +251,18 @@ public void save() { public void purge(CleanUp cleanup) throws SQLException { if (cleanup.getDays() == 0) return; + String table = getTableInfo().getTableName(); String banTable = BanManagerPlugin.getInstance().getIpBanStorage() .getTableInfo() .getTableName(); - // Note: INTERVAL syntax doesn't support parameterization in most databases - // cleanup.getDays() is from config (integer), not user input, so safe to concatenate - String sql = "SELECT ph.id FROM " + getTableInfo().getTableName() + " AS ph " + - "LEFT JOIN " + banTable + " b ON ph.ip = b.ip " + - "WHERE (ph.ip IS NULL OR b.ip IS NULL) " + - "AND ph.`leave` < UNIX_TIMESTAMP(CURRENT_TIMESTAMP - INTERVAL " + cleanup.getDays() + " DAY)"; - - CloseableIterator results = queryRaw(sql).closeableIterator(); - - while (results.hasNext()) { - int id = Integer.parseInt(results.next()[0]); - deleteById(id); - } + String sql = "DELETE FROM `" + table + "` WHERE `id` IN (" + + "SELECT ph.id FROM (SELECT p.id FROM `" + table + "` p " + + "LEFT JOIN `" + banTable + "` b ON p.ip = b.ip " + + "WHERE (p.ip IS NULL OR b.ip IS NULL) " + + "AND p.`leave` < UNIX_TIMESTAMP(CURRENT_TIMESTAMP - INTERVAL " + + cleanup.getDays() + " DAY)) AS ph)"; - results.closeQuietly(); + updateRaw(sql); } } diff --git a/common/src/main/java/me/confuser/banmanager/common/storage/PlayerReportStorage.java b/common/src/main/java/me/confuser/banmanager/common/storage/PlayerReportStorage.java index 97b737dcd..e8852c868 100644 --- a/common/src/main/java/me/confuser/banmanager/common/storage/PlayerReportStorage.java +++ b/common/src/main/java/me/confuser/banmanager/common/storage/PlayerReportStorage.java @@ -4,6 +4,7 @@ import me.confuser.banmanager.common.api.events.CommonEvent; import me.confuser.banmanager.common.data.PlayerData; import me.confuser.banmanager.common.data.PlayerReportData; +import me.confuser.banmanager.common.ormlite.stmt.DeleteBuilder; import me.confuser.banmanager.common.ormlite.stmt.QueryBuilder; import me.confuser.banmanager.common.ormlite.stmt.Where; import me.confuser.banmanager.common.ormlite.support.ConnectionSource; @@ -80,12 +81,15 @@ public ReportList getReports(long page, int state) throws SQLException { public int deleteAll(PlayerData player) throws SQLException { List reports = queryForEq("player_id", player); + if (reports.isEmpty()) return 0; for (PlayerReportData report : reports) { - deleteById(report.getId()); + plugin.getServer().callEvent("PlayerReportDeletedEvent", report); } - return reports.size(); + DeleteBuilder builder = deleteBuilder(); + builder.where().eq("player_id", player); + return builder.delete(); } public boolean isRecentlyReported(PlayerData player, long cooldown) throws SQLException { @@ -114,13 +118,16 @@ public int deleteById(Integer id) throws SQLException { public int deleteIds(Collection ids) throws SQLException { if (ids == null || ids.isEmpty()) return 0; - int count = 0; - for (Integer id : ids) { - if (deleteById(id) != 0) count++; + PlayerReportData report = queryForId(id); + if (report != null) { + plugin.getServer().callEvent("PlayerReportDeletedEvent", report); + } } - return count; + DeleteBuilder builder = deleteBuilder(); + builder.where().in("id", ids); + return builder.delete(); } public long getCount(PlayerData player) throws SQLException { diff --git a/common/src/main/java/me/confuser/banmanager/common/storage/PlayerStorage.java b/common/src/main/java/me/confuser/banmanager/common/storage/PlayerStorage.java index 17d8e56eb..a3480cca7 100644 --- a/common/src/main/java/me/confuser/banmanager/common/storage/PlayerStorage.java +++ b/common/src/main/java/me/confuser/banmanager/common/storage/PlayerStorage.java @@ -28,7 +28,9 @@ public class PlayerStorage extends BaseDaoImpl { private BanManagerPlugin plugin; @Getter - private RadixTree autoCompleteTree; + private RadixTree autoCompleteTree = new ConcurrentRadixTree<>(new SmartArrayBasedNodeFactory()); + + private volatile boolean shuttingDown = false; @Getter private PlayerData console; @@ -46,8 +48,7 @@ public PlayerStorage(BanManagerPlugin plugin) throws SQLException { setupConsole(); if (plugin.getConfig().isOfflineAutoComplete()) { - // @TODO run in a separate thread to speed up start up - setupAutoComplete(); + plugin.getScheduler().runAsync(this::setupAutoComplete); } } @@ -82,21 +83,25 @@ public void setupConsole() throws SQLException { } public void setupAutoComplete() { - autoCompleteTree = new ConcurrentRadixTree<>(new SmartArrayBasedNodeFactory()); CloseableIterator itr = null; try { itr = this.queryBuilder().selectColumns("name").iterator(); - while (itr.hasNext()) { + while (!shuttingDown && itr.hasNext()) { autoCompleteTree.put(itr.next().getName(), VoidValue.SINGLETON); } } catch (SQLException e) { - plugin.getLogger().warning("Failed to process player storage operation", e); + if (!shuttingDown) { + plugin.getLogger().warning("Failed to process player storage operation", e); + } } finally { if (itr != null) itr.closeQuietly(); } + } + public void shutdown() { + shuttingDown = true; } public CreateOrUpdateStatus upsert(PlayerData data) throws SQLException { diff --git a/common/src/main/java/me/confuser/banmanager/common/storage/PlayerWarnStorage.java b/common/src/main/java/me/confuser/banmanager/common/storage/PlayerWarnStorage.java index 5f6927d2d..59bae920d 100644 --- a/common/src/main/java/me/confuser/banmanager/common/storage/PlayerWarnStorage.java +++ b/common/src/main/java/me/confuser/banmanager/common/storage/PlayerWarnStorage.java @@ -12,6 +12,7 @@ import me.confuser.banmanager.common.ormlite.stmt.DeleteBuilder; import me.confuser.banmanager.common.ormlite.stmt.QueryBuilder; import me.confuser.banmanager.common.ormlite.stmt.StatementBuilder; +import me.confuser.banmanager.common.ormlite.stmt.UpdateBuilder; import me.confuser.banmanager.common.ormlite.stmt.Where; import me.confuser.banmanager.common.ormlite.support.CompiledStatement; import me.confuser.banmanager.common.ormlite.support.ConnectionSource; @@ -144,12 +145,32 @@ public boolean isRecentlyWarned(PlayerData player, long cooldown) throws SQLExce .countOf() > 0; } - public int deleteRecent(PlayerData player) throws SQLException { - // TODO use a raw DELETE query to reduce to one query - PlayerWarnData warning = queryBuilder().limit(1L).orderBy("created", false).where().eq("player_id", player) - .queryForFirst(); + public int markAllRead(UUID playerId) throws SQLException { + UpdateBuilder builder = updateBuilder(); + builder.updateColumnValue("read", true); + builder.where().eq("player_id", UUIDUtils.toBytes(playerId)).and().eq("read", false); + return builder.update(); + } - return delete(warning); + public int deleteRecent(PlayerData player) throws SQLException { + String table = getTableName(); + + try (DatabaseConnection connection = connectionSource.getReadWriteConnection(table)) { + CompiledStatement statement = connection.compileStatement( + "DELETE FROM `" + table + "` WHERE `id` = (" + + "SELECT `id` FROM (SELECT `id` FROM `" + table + + "` WHERE `player_id` = ? ORDER BY `created` DESC LIMIT 1) AS tmp)", + StatementBuilder.StatementType.UPDATE, null, + DatabaseConnection.DEFAULT_RESULT_FLAGS, false); + try { + statement.setObject(0, player.getId(), SqlType.BYTE_ARRAY); + return statement.runUpdate(); + } finally { + try { statement.close(); } catch (IOException ignored) { } + } + } catch (IOException e) { + throw new SQLException("Failed to delete recent warning", e); + } } public void purge(CleanUp cleanup, boolean read) throws SQLException { diff --git a/sponge-api7/src/main/java/me/confuser/banmanager/sponge/BMSpongePlugin.java b/sponge-api7/src/main/java/me/confuser/banmanager/sponge/BMSpongePlugin.java index c4cad9039..efb180955 100644 --- a/sponge-api7/src/main/java/me/confuser/banmanager/sponge/BMSpongePlugin.java +++ b/sponge-api7/src/main/java/me/confuser/banmanager/sponge/BMSpongePlugin.java @@ -48,6 +48,7 @@ public class BMSpongePlugin { private BanManagerPlugin plugin; private Metrics metrics; private ChatListener chatListener; + private SpongeScheduler scheduler; @Inject @ConfigDir(sharedRoot = false) @@ -75,7 +76,7 @@ public BMSpongePlugin(Logger logger, Metrics.Factory metrics) { @Listener public void onDisable(GameStoppingServerEvent event) { - // @TODO Disable scheduled tasks somehow + scheduler.cancelAll(); if (plugin != null) plugin.disable(); } @@ -91,7 +92,8 @@ public void onEnable(GamePreInitializationEvent event) { return; } - this.plugin = new BanManagerPlugin(pluginInfo, this.logger, dataFolder.toFile(), new SpongeScheduler(this), server, new SpongeMetrics(metrics)); + this.scheduler = new SpongeScheduler(this); + this.plugin = new BanManagerPlugin(pluginInfo, this.logger, dataFolder.toFile(), scheduler, server, new SpongeMetrics(metrics)); server.enable(plugin); diff --git a/sponge-api7/src/main/java/me/confuser/banmanager/sponge/SpongeScheduler.java b/sponge-api7/src/main/java/me/confuser/banmanager/sponge/SpongeScheduler.java index 70bce85df..09081394b 100644 --- a/sponge-api7/src/main/java/me/confuser/banmanager/sponge/SpongeScheduler.java +++ b/sponge-api7/src/main/java/me/confuser/banmanager/sponge/SpongeScheduler.java @@ -6,9 +6,12 @@ import org.spongepowered.api.scheduler.Task; import java.time.Duration; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; public class SpongeScheduler implements CommonScheduler { private Object plugin; + private final List repeatingTasks = new CopyOnWriteArrayList<>(); public SpongeScheduler(Object plugin) { this.plugin = plugin; @@ -45,6 +48,14 @@ public void runAsyncRepeating(Runnable task, Duration initialDelay, Duration per long initialTicks = SchedulerTime.durationToTicksCeil(initialDelay); long periodTicks = SchedulerTime.durationToTicksCeil(period); Task.Builder builder = Sponge.getGame().getScheduler().createTaskBuilder(); - builder.async().execute(task).delayTicks(initialTicks).intervalTicks(periodTicks).submit(plugin); + repeatingTasks.add(builder.async().execute(task).delayTicks(initialTicks).intervalTicks(periodTicks).submit(plugin)); + } + + @Override + public void cancelAll() { + for (Task task : repeatingTasks) { + task.cancel(); + } + repeatingTasks.clear(); } } diff --git a/velocity/src/main/java/me/confuser/banmanager/velocity/BMVelocityPlugin.java b/velocity/src/main/java/me/confuser/banmanager/velocity/BMVelocityPlugin.java index 4816474d4..ffc4ae78b 100644 --- a/velocity/src/main/java/me/confuser/banmanager/velocity/BMVelocityPlugin.java +++ b/velocity/src/main/java/me/confuser/banmanager/velocity/BMVelocityPlugin.java @@ -49,6 +49,7 @@ public class BMVelocityPlugin { @Getter private BanManagerPlugin plugin; + private VelocityScheduler scheduler; @Getter private VelocityConfig velocityConfig; private final String[] configs = new String[]{ @@ -92,7 +93,8 @@ public void onProxyInitialization(ProxyInitializeEvent event) { return; } - plugin = new BanManagerPlugin(pluginInfo, new PluginLogger(logger), dataDirectory, new VelocityScheduler(this, this.server), server, new VelocityMetrics(metrics)); + this.scheduler = new VelocityScheduler(this, this.server); + plugin = new BanManagerPlugin(pluginInfo, new PluginLogger(logger), dataDirectory, scheduler, server, new VelocityMetrics(metrics)); server.enable(plugin, this.server); @@ -123,6 +125,7 @@ public void onProxyInitialization(ProxyInitializeEvent event) { @Subscribe public void onProxyShutdown(ProxyShutdownEvent event) { + if (scheduler != null) scheduler.cancelAll(); if (plugin != null) plugin.disable(); } diff --git a/velocity/src/main/java/me/confuser/banmanager/velocity/VelocityScheduler.java b/velocity/src/main/java/me/confuser/banmanager/velocity/VelocityScheduler.java index 2efba0289..f7d61a9b1 100644 --- a/velocity/src/main/java/me/confuser/banmanager/velocity/VelocityScheduler.java +++ b/velocity/src/main/java/me/confuser/banmanager/velocity/VelocityScheduler.java @@ -1,14 +1,18 @@ package me.confuser.banmanager.velocity; import com.velocitypowered.api.proxy.ProxyServer; +import com.velocitypowered.api.scheduler.ScheduledTask; import me.confuser.banmanager.common.CommonScheduler; import java.time.Duration; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; public class VelocityScheduler implements CommonScheduler { private Object plugin; private ProxyServer server; + private final List repeatingTasks = new CopyOnWriteArrayList<>(); public VelocityScheduler(Object plugin, ProxyServer server) { this.plugin = plugin; @@ -37,6 +41,14 @@ public void runSyncLater(Runnable task, Duration delay) { @Override public void runAsyncRepeating(Runnable task, Duration initialDelay, Duration period) { - server.getScheduler().buildTask(plugin, task).delay(initialDelay).repeat(period).schedule(); + repeatingTasks.add(server.getScheduler().buildTask(plugin, task).delay(initialDelay).repeat(period).schedule()); + } + + @Override + public void cancelAll() { + for (ScheduledTask task : repeatingTasks) { + task.cancel(); + } + repeatingTasks.clear(); } } From cc4dfe4dcd09cf77d2cda22cfd25041997097672 Mon Sep 17 00:00:00 2001 From: James Mortemore Date: Sat, 4 Apr 2026 13:57:30 +0100 Subject: [PATCH 2/2] test: add coverage for rollback reports events, UNION ALL stats, and markAllRead - RollbackCommandTest: verify reports rollback fires PlayerReportDeletedEvent per report and deletes all matching reports from DB - InfoCommandTest: exercise UNION ALL statistics query with records across all count categories (ban records, mute records, warns, notes, reports) - PlayerWarnStorageTest: verify markAllRead bulk update marks only the target player's unread warnings without affecting other players --- .../common/commands/InfoCommandTest.java | 33 +++++++++++-- .../common/commands/RollbackCommandTest.java | 36 +++++++++++++-- .../common/storage/PlayerWarnStorageTest.java | 46 +++++++++++++++++++ 3 files changed, 105 insertions(+), 10 deletions(-) diff --git a/common/src/test/java/me/confuser/banmanager/common/commands/InfoCommandTest.java b/common/src/test/java/me/confuser/banmanager/common/commands/InfoCommandTest.java index 0f93f6552..972ed0b9d 100644 --- a/common/src/test/java/me/confuser/banmanager/common/commands/InfoCommandTest.java +++ b/common/src/test/java/me/confuser/banmanager/common/commands/InfoCommandTest.java @@ -2,11 +2,7 @@ import me.confuser.banmanager.common.BasePluginDbTest; import me.confuser.banmanager.common.CommonServer; -import me.confuser.banmanager.common.data.PlayerBanData; -import me.confuser.banmanager.common.data.PlayerData; -import me.confuser.banmanager.common.data.PlayerMuteData; -import me.confuser.banmanager.common.data.PlayerNoteData; -import me.confuser.banmanager.common.data.PlayerWarnData; +import me.confuser.banmanager.common.data.*; import me.confuser.banmanager.common.util.parsers.InfoCommandParser; import org.junit.Before; import org.junit.Test; @@ -153,4 +149,31 @@ public void shouldHideNamesWithoutPermission() throws SQLException { await().untilAsserted(() -> verify(sender, never()).sendMessage(contains("Known names"))); } + + @Test + public void shouldShowStatsWithMultipleRecordTypes() throws SQLException { + PlayerData player = testUtils.createRandomPlayer(); + PlayerData actor = testUtils.createRandomPlayer(); + CommonServer server = spy(plugin.getServer()); + CommonSender sender = spy(server.getConsoleSender()); + + PlayerBanData ban = testUtils.createBan(player, actor, "test ban"); + plugin.getPlayerBanStorage().unban(ban, actor, "unbanned"); + + PlayerMuteData mute = testUtils.createMute(player, actor, "test mute"); + plugin.getPlayerMuteStorage().unmute(mute, actor, "unmuted"); + + plugin.getPlayerWarnStorage().addWarning(new PlayerWarnData(player, actor, "warn 1", 3.0), false); + plugin.getPlayerWarnStorage().addWarning(new PlayerWarnData(player, actor, "warn 2", 2.0), false); + + plugin.getPlayerNoteStorage().create(new PlayerNoteData(player, actor, "a note")); + + ReportState openState = plugin.getReportStateStorage().queryForId(1); + plugin.getPlayerReportStorage().create(new PlayerReportData(player, actor, "report", openState)); + + String[] args = new String[]{player.getName()}; + assert (cmd.onCommand(sender, new InfoCommandParser(plugin, args))); + + await().untilAsserted(() -> verify(sender, atLeastOnce()).sendMessage(anyString())); + } } diff --git a/common/src/test/java/me/confuser/banmanager/common/commands/RollbackCommandTest.java b/common/src/test/java/me/confuser/banmanager/common/commands/RollbackCommandTest.java index 29a90152c..1db257eed 100644 --- a/common/src/test/java/me/confuser/banmanager/common/commands/RollbackCommandTest.java +++ b/common/src/test/java/me/confuser/banmanager/common/commands/RollbackCommandTest.java @@ -2,11 +2,7 @@ import me.confuser.banmanager.common.BasePluginDbTest; import me.confuser.banmanager.common.CommonServer; -import me.confuser.banmanager.common.data.PlayerBanData; -import me.confuser.banmanager.common.data.PlayerBanRecord; -import me.confuser.banmanager.common.data.PlayerData; -import me.confuser.banmanager.common.data.PlayerMuteData; -import me.confuser.banmanager.common.data.PlayerMuteRecord; +import me.confuser.banmanager.common.data.*; import org.junit.Before; import org.junit.Test; @@ -372,4 +368,34 @@ public void shouldNotRestoreMuteWhenBothActionsAreMalicious() throws SQLExceptio assertNull(plugin.getPlayerMuteStorage().retrieveMute(victim.getUUID())); assertFalse(plugin.getPlayerMuteStorage().isMuted(victim.getUUID())); } + + @Test + public void shouldRollbackReportsAndFireEvents() throws SQLException { + PlayerData victim = testUtils.createRandomPlayer(); + PlayerData maliciousMod = testUtils.createRandomPlayer(); + ReportState openState = plugin.getReportStateStorage().queryForId(1); + + PlayerReportData report1 = new PlayerReportData(victim, maliciousMod, "bad report 1", openState); + plugin.getPlayerReportStorage().create(report1); + PlayerReportData report2 = new PlayerReportData(victim, maliciousMod, "bad report 2", openState); + plugin.getPlayerReportStorage().create(report2); + + assertEquals(2, plugin.getPlayerReportStorage().getCount(victim)); + + CommonSender sender = spy(plugin.getServer().getConsoleSender()); + String[] args = new String[]{maliciousMod.getName(), "1d", "reports"}; + + assertTrue(cmd.onCommand(sender, new CommandParser(plugin, args, 0))); + + await().until(() -> { + try { + return plugin.getPlayerReportStorage().getCount(victim) == 0; + } catch (SQLException e) { + return false; + } + }); + + assertEquals(0, plugin.getPlayerReportStorage().getCount(victim)); + verify(server, times(2)).callEvent(eq("PlayerReportDeletedEvent"), any(PlayerReportData.class)); + } } diff --git a/common/src/test/java/me/confuser/banmanager/common/storage/PlayerWarnStorageTest.java b/common/src/test/java/me/confuser/banmanager/common/storage/PlayerWarnStorageTest.java index 32ac666df..6bf031b19 100644 --- a/common/src/test/java/me/confuser/banmanager/common/storage/PlayerWarnStorageTest.java +++ b/common/src/test/java/me/confuser/banmanager/common/storage/PlayerWarnStorageTest.java @@ -98,4 +98,50 @@ public void shouldHandleTempWarningExpiry() throws SQLException { assertTrue(warning.getExpires() > 0 && warning.getExpires() < System.currentTimeMillis() / 1000L); } + + @Test + public void shouldMarkAllRead() throws SQLException { + PlayerData player = testUtils.createRandomPlayer(); + PlayerData actor = testUtils.createRandomPlayer(); + + PlayerWarnData w1 = new PlayerWarnData(player, actor, "unread 1", 1.0, false); + plugin.getPlayerWarnStorage().addWarning(w1, false); + PlayerWarnData w2 = new PlayerWarnData(player, actor, "unread 2", 1.0, false); + plugin.getPlayerWarnStorage().addWarning(w2, false); + + long unreadBefore = plugin.getPlayerWarnStorage().queryBuilder() + .where().eq("player_id", player.getId()).and().eq("read", false) + .countOf(); + assertEquals(2, unreadBefore); + + int updated = plugin.getPlayerWarnStorage().markAllRead(player.getUUID()); + assertEquals(2, updated); + + long unreadAfter = plugin.getPlayerWarnStorage().queryBuilder() + .where().eq("player_id", player.getId()).and().eq("read", false) + .countOf(); + assertEquals(0, unreadAfter); + } + + @Test + public void shouldNotMarkOtherPlayersRead() throws SQLException { + PlayerData player1 = testUtils.createRandomPlayer(); + PlayerData player2 = testUtils.createRandomPlayer(); + PlayerData actor = testUtils.createRandomPlayer(); + + plugin.getPlayerWarnStorage().addWarning(new PlayerWarnData(player1, actor, "p1 warn", 1.0, false), false); + plugin.getPlayerWarnStorage().addWarning(new PlayerWarnData(player2, actor, "p2 warn", 1.0, false), false); + + plugin.getPlayerWarnStorage().markAllRead(player1.getUUID()); + + long p1Unread = plugin.getPlayerWarnStorage().queryBuilder() + .where().eq("player_id", player1.getId()).and().eq("read", false) + .countOf(); + assertEquals(0, p1Unread); + + long p2Unread = plugin.getPlayerWarnStorage().queryBuilder() + .where().eq("player_id", player2.getId()).and().eq("read", false) + .countOf(); + assertEquals(1, p2Unread); + } }