diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/tooling/banEvasion/BanEvasionRepository.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/tooling/banEvasion/BanEvasionRepository.java index 353825b..fabd57a 100644 --- a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/tooling/banEvasion/BanEvasionRepository.java +++ b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/tooling/banEvasion/BanEvasionRepository.java @@ -70,7 +70,7 @@ final class BanEvasionRepository { record Member(UUID uuid, String name) { } - record Household(long id, @Nullable String note, List members) { + record Household(long id, Instant createdAt, @Nullable String createdByName, @Nullable String note, List members) { } record Exemption(long id, String network, Instant validFrom, Instant validUntil, @Nullable String createdByName, @Nullable String note) { @@ -156,18 +156,20 @@ final class BanEvasionRepository { @Nullable CaseDecision.ExistingCase findCase(Connection connection, UUID first, UUID second) throws SQLException { long id; CaseStatus status; - try(PreparedStatement statement = connection.prepareStatement("SELECT id, status FROM ban_evasion_cases WHERE uuid_a = ? AND uuid_b = ?")) { + String outcome; + try(PreparedStatement statement = connection.prepareStatement("SELECT id, status, outcome FROM ban_evasion_cases WHERE uuid_a = ? AND uuid_b = ?")) { statement.setString(1, first.toString()); statement.setString(2, second.toString()); try(ResultSet row = statement.executeQuery()) { if(!row.next()) return null; id = row.getLong("id"); status = CaseStatus.valueOf(row.getString("status")); + outcome = row.getString("outcome"); } } Set reasons = EnumSet.noneOf(Reason.class); this.reasonsOf(connection, id).forEach(reason -> reasons.add(reason.reason())); - return new CaseDecision.ExistingCase(id, status, reasons); + return new CaseDecision.ExistingCase(id, status, outcome == null ? null : Outcome.valueOf(outcome), reasons); } /** @@ -458,21 +460,38 @@ final class BanEvasionRepository { } } + /** + * All households, newest first. + */ List listHouseholds(Connection connection) throws SQLException { + return this.households(connection, "", null); + } + + @Nullable Household household(Connection connection, long id) throws SQLException { + List households = this.households(connection, "WHERE h.id = ?", id); + return households.isEmpty() ? null : households.getFirst(); + } + + private List households(Connection connection, String where, @Nullable Long id) throws SQLException { String sql = """ - SELECT h.id, h.note, m.uuid, %s AS name + SELECT h.id, h.created_at, h.note, %s AS created_by_name, m.uuid, %s AS name FROM ban_evasion_households h JOIN ban_evasion_household_members m ON m.household_id = h.id - ORDER BY h.id, name - """.formatted(NAME_OF.formatted("m.uuid")); + %s + ORDER BY h.id DESC, name + """.formatted(NAME_OF.formatted("h.created_by"), NAME_OF.formatted("m.uuid"), where); Map households = new LinkedHashMap<>(); - try(PreparedStatement statement = connection.prepareStatement(sql); ResultSet row = statement.executeQuery()) { - while(row.next()) { - long id = row.getLong("id"); - String householdNote = row.getString("note"); - UUID uuid = UUID.fromString(row.getString("uuid")); - households.computeIfAbsent(id, key -> new Household(key, householdNote, new ArrayList<>())) - .members().add(new Member(uuid, nameOr(row.getString("name"), uuid))); + try(PreparedStatement statement = connection.prepareStatement(sql)) { + if(id != null) statement.setLong(1, id); + try(ResultSet row = statement.executeQuery()) { + while(row.next()) { + Instant createdAt = fromDb(row, "created_at"); + String createdByName = row.getString("created_by_name"); + String householdNote = row.getString("note"); + UUID uuid = UUID.fromString(row.getString("uuid")); + households.computeIfAbsent(row.getLong("id"), key -> new Household(key, createdAt, createdByName, householdNote, new ArrayList<>())) + .members().add(new Member(uuid, nameOr(row.getString("name"), uuid))); + } } } return List.copyOf(households.values()); diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/tooling/banEvasion/CaseDecision.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/tooling/banEvasion/CaseDecision.java index a204734..44f53ea 100644 --- a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/tooling/banEvasion/CaseDecision.java +++ b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/tooling/banEvasion/CaseDecision.java @@ -7,10 +7,11 @@ import java.util.Set; /** * Deduplication: one case per pair; admins are only notified about new information. - * A closed case reopens only for a new reason of severity HIGH. + * A closed case reopens only for a new reason of severity HIGH, a case closed as confirmed evasion never: + * further attempts of the banned person are expected there and only recorded. */ final class CaseDecision { - record ExistingCase(long id, CaseStatus status, Set reasons) { + record ExistingCase(long id, CaseStatus status, @Nullable Outcome outcome, Set reasons) { } /** @@ -35,6 +36,7 @@ final class CaseDecision { .anyMatch(reason -> !reason.informational && finding.reasons().get(reason) == Severity.HIGH); if(existing.status() == CaseStatus.OPEN) return new Result(false, newReasons, newRelevant); + if(existing.outcome() == Outcome.CONFIRMED) return new Result(false, newReasons, false); return new Result(newHigh, newReasons, newHigh); } }