diff --git a/bans-api/src/main/java/space/arim/libertybans/api/LibertyBans.java b/bans-api/src/main/java/space/arim/libertybans/api/LibertyBans.java index d36c03800..5a1ce7052 100644 --- a/bans-api/src/main/java/space/arim/libertybans/api/LibertyBans.java +++ b/bans-api/src/main/java/space/arim/libertybans/api/LibertyBans.java @@ -1,23 +1,25 @@ -/* - * LibertyBans-api - * Copyright © 2020 Anand Beh - * - * LibertyBans-api is free software: you can redistribute it and/or modify +/* + * LibertyBans + * Copyright © 2023 Anand Beh + * + * LibertyBans is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * - * LibertyBans-api is distributed in the hope that it will be useful, + * + * LibertyBans is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License - * along with LibertyBans-api. If not, see + * along with LibertyBans. If not, see * and navigate to version 3 of the GNU Affero General Public License. */ + package space.arim.libertybans.api; +import space.arim.libertybans.api.user.AccountSupervisor; import space.arim.omnibus.Omnibus; import space.arim.omnibus.util.concurrent.FactoryOfTheFuture; @@ -103,4 +105,11 @@ public interface LibertyBans { */ UserResolver getUserResolver(); + /** + * Gets the account supervisor for alt detection and management + * + * @return the account supervisor + */ + AccountSupervisor getAccountSupervisor(); + } diff --git a/bans-api/src/main/java/space/arim/libertybans/api/user/AccountBase.java b/bans-api/src/main/java/space/arim/libertybans/api/user/AccountBase.java new file mode 100644 index 000000000..5084abbb2 --- /dev/null +++ b/bans-api/src/main/java/space/arim/libertybans/api/user/AccountBase.java @@ -0,0 +1,54 @@ +/* + * LibertyBans + * Copyright © 2023 Anand Beh + * + * LibertyBans is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * LibertyBans is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with LibertyBans. If not, see + * and navigate to version 3 of the GNU Affero General Public License. + */ + +package space.arim.libertybans.api.user; + +import space.arim.libertybans.api.NetworkAddress; + +import java.util.Optional; +import java.util.UUID; + +/** + * Base interface for accounts + * + */ +public interface AccountBase { + + /** + * The account UUID + * + * @return the UUID + */ + UUID uuid(); + + /** + * The latest username for the corresponding user, if it is known + * + * @return the latest username if there is one + */ + Optional latestUsername(); + + /** + * The account address + * + * @return the address + */ + NetworkAddress address(); + +} diff --git a/bans-api/src/main/java/space/arim/libertybans/api/user/AccountSupervisor.java b/bans-api/src/main/java/space/arim/libertybans/api/user/AccountSupervisor.java new file mode 100644 index 000000000..f0990c6fa --- /dev/null +++ b/bans-api/src/main/java/space/arim/libertybans/api/user/AccountSupervisor.java @@ -0,0 +1,68 @@ +/* + * LibertyBans + * Copyright © 2023 Anand Beh + * + * LibertyBans is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * LibertyBans is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with LibertyBans. If not, see + * and navigate to version 3 of the GNU Affero General Public License. + */ + +package space.arim.libertybans.api.user; + +import space.arim.libertybans.api.NetworkAddress; +import space.arim.omnibus.util.concurrent.CentralisedFuture; + +import java.util.List; +import java.util.UUID; + +/** + * Enables detecting alts leveraging all the capabilities provided by the implementation + * + */ +public interface AccountSupervisor { + + /** + * Begins to detects alts for the given player + * + * @param uuid the player's UUID + * @param address the player's address + * @return a detection query builder + */ + DetectionQuery.Builder detectAlts(UUID uuid, NetworkAddress address); + + /** + * Finds accounts matching the given UUID + * + * @param uuid the uuid + * @return all matching accounts + */ + CentralisedFuture> findAccountsMatching(UUID uuid); + + /** + * Finds accounts matching the given address + * + * @param address the address + * @return all matching accounts + */ + CentralisedFuture> findAccountsMatching(NetworkAddress address); + + /** + * Finds accounts matching the given UUID or address + * + * @param uuid the uuid + * @param address the address + * @return all accounts matching the UUID OR the address + */ + CentralisedFuture> findAccountsMatching(UUID uuid, NetworkAddress address); + +} diff --git a/bans-api/src/main/java/space/arim/libertybans/api/user/AltAccount.java b/bans-api/src/main/java/space/arim/libertybans/api/user/AltAccount.java new file mode 100644 index 000000000..5e058f0e3 --- /dev/null +++ b/bans-api/src/main/java/space/arim/libertybans/api/user/AltAccount.java @@ -0,0 +1,48 @@ +/* + * LibertyBans + * Copyright © 2023 Anand Beh + * + * LibertyBans is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * LibertyBans is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with LibertyBans. If not, see + * and navigate to version 3 of the GNU Affero General Public License. + */ + +package space.arim.libertybans.api.user; + +import space.arim.libertybans.api.PunishmentType; + +import java.time.Instant; + +/** + * A detected alt account + * + */ +public interface AltAccount extends AccountBase { + + /** + * The most recent time this alt was observed + * + * @return the last time this alt account was observed + */ + Instant lastObserved(); + + /** + * Whether the alt account has ANY active punishments of the specified type + * + * @param type the punishment type + * @return true if a punishment with the type exists and is active + * @throws IllegalArgumentException if the type was not queried for in the {@link DetectionQuery} + */ + boolean hasActivePunishment(PunishmentType type); + +} diff --git a/bans-api/src/main/java/space/arim/libertybans/api/user/DetectionQuery.java b/bans-api/src/main/java/space/arim/libertybans/api/user/DetectionQuery.java new file mode 100644 index 000000000..8c9885756 --- /dev/null +++ b/bans-api/src/main/java/space/arim/libertybans/api/user/DetectionQuery.java @@ -0,0 +1,103 @@ +/* + * LibertyBans + * Copyright © 2023 Anand Beh + * + * LibertyBans is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * LibertyBans is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with LibertyBans. If not, see + * and navigate to version 3 of the GNU Affero General Public License. + */ + +package space.arim.libertybans.api.user; + +import space.arim.libertybans.api.NetworkAddress; +import space.arim.libertybans.api.PunishmentType; +import space.arim.omnibus.util.concurrent.CentralisedFuture; + +import java.util.List; +import java.util.Set; +import java.util.UUID; + +/** + * Query to detect alts + * + */ +public interface DetectionQuery { + + /** + * The uuid of the player whose alts to search for + * + * @return the uuid + */ + UUID uuid(); + + /** + * The network address of the player whose alts to search for + * + * @return the address + */ + NetworkAddress address(); + + /** + * The punishment types to scan alts for + * + * @return the punishment types + */ + Set punishmentTypes(); + + /** + * Builder for detection queries + * + */ + interface Builder { + + /** + * The punishment types to scan alts for.
+ *
+ * If enabled, the alt detection will attempt to look for punishment types applying to the specified + * alts on a best effort basis. + * + * @param types the punishment types to match alts with + * @return this builder + */ + default Builder punishmentTypes(PunishmentType...types) { + return punishmentTypes(Set.of(types)); + } + + /** + * The punishment types to scan alts for.
+ *
+ * If enabled, the alt detection will attempt to look for punishment types applying to the specified + * alts on a best effort basis. + * + * @param types the punishment types to match alts with + * @return this builder + */ + Builder punishmentTypes(Set types); + + /** + * Builds into a detection query + * + * @return the detection query + */ + DetectionQuery build(); + + } + + /** + * Performs the detection + * + * @return a future yielding the alt accounts + */ + CentralisedFuture> detect(); + +} diff --git a/bans-api/src/main/java/space/arim/libertybans/api/user/KnownAccount.java b/bans-api/src/main/java/space/arim/libertybans/api/user/KnownAccount.java new file mode 100644 index 000000000..15f510a59 --- /dev/null +++ b/bans-api/src/main/java/space/arim/libertybans/api/user/KnownAccount.java @@ -0,0 +1,47 @@ +/* + * LibertyBans + * Copyright © 2023 Anand Beh + * + * LibertyBans is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * LibertyBans is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with LibertyBans. If not, see + * and navigate to version 3 of the GNU Affero General Public License. + */ + +package space.arim.libertybans.api.user; + +import space.arim.omnibus.util.concurrent.CentralisedFuture; + +import java.time.Instant; + +/** + * A known login for a certain player + * + */ +public interface KnownAccount extends AccountBase { + + /** + * When the login took place + * + * @return the time the login was recorded + */ + Instant recorded(); + + /** + * Attempts to delete this known account + * + * @return a future yielding true if deleted, false if it does not exist + * (perhaps because it was deleted by another instance) + */ + CentralisedFuture deleteFromHistory(); + +} diff --git a/bans-core/src/main/java/space/arim/libertybans/core/ApiBindModule.java b/bans-core/src/main/java/space/arim/libertybans/core/ApiBindModule.java index df71f6ab9..522df2991 100644 --- a/bans-core/src/main/java/space/arim/libertybans/core/ApiBindModule.java +++ b/bans-core/src/main/java/space/arim/libertybans/core/ApiBindModule.java @@ -1,25 +1,28 @@ -/* - * LibertyBans-core - * Copyright © 2020 Anand Beh - * - * LibertyBans-core is free software: you can redistribute it and/or modify +/* + * LibertyBans + * Copyright © 2023 Anand Beh + * + * LibertyBans is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * - * LibertyBans-core is distributed in the hope that it will be useful, + * + * LibertyBans is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License - * along with LibertyBans-core. If not, see + * along with LibertyBans. If not, see * and navigate to version 3 of the GNU Affero General Public License. */ + package space.arim.libertybans.core; import jakarta.inject.Singleton; +import space.arim.libertybans.api.user.AccountSupervisor; +import space.arim.libertybans.core.alts.Supervisor; import space.arim.omnibus.util.concurrent.FactoryOfTheFuture; import space.arim.libertybans.api.LibertyBans; @@ -79,4 +82,8 @@ public UserResolver userResolver(UUIDManager uuidManager) { return uuidManager; } + public AccountSupervisor accountSupervisor(Supervisor supervisor) { + return supervisor; + } + } diff --git a/bans-core/src/main/java/space/arim/libertybans/core/LibertyBansApi.java b/bans-core/src/main/java/space/arim/libertybans/core/LibertyBansApi.java index 60fbd5166..7fb554229 100644 --- a/bans-core/src/main/java/space/arim/libertybans/core/LibertyBansApi.java +++ b/bans-core/src/main/java/space/arim/libertybans/core/LibertyBansApi.java @@ -1,30 +1,26 @@ -/* - * LibertyBans-core - * Copyright © 2020 Anand Beh - * - * LibertyBans-core is free software: you can redistribute it and/or modify +/* + * LibertyBans + * Copyright © 2023 Anand Beh + * + * LibertyBans is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * - * LibertyBans-core is distributed in the hope that it will be useful, + * + * LibertyBans is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License - * along with LibertyBans-core. If not, see + * along with LibertyBans. If not, see * and navigate to version 3 of the GNU Affero General Public License. */ + package space.arim.libertybans.core; import jakarta.inject.Inject; import jakarta.inject.Provider; -import jakarta.inject.Singleton; - -import space.arim.omnibus.Omnibus; -import space.arim.omnibus.util.concurrent.FactoryOfTheFuture; - import space.arim.libertybans.api.LibertyBans; import space.arim.libertybans.api.database.PunishmentDatabase; import space.arim.libertybans.api.formatter.PunishmentFormatter; @@ -32,9 +28,11 @@ import space.arim.libertybans.api.punish.PunishmentRevoker; import space.arim.libertybans.api.scope.ScopeManager; import space.arim.libertybans.api.select.PunishmentSelector; +import space.arim.libertybans.api.user.AccountSupervisor; import space.arim.libertybans.api.user.UserResolver; +import space.arim.omnibus.Omnibus; +import space.arim.omnibus.util.concurrent.FactoryOfTheFuture; -@Singleton public class LibertyBansApi implements LibertyBans { private final Omnibus omnibus; @@ -42,24 +40,27 @@ public class LibertyBansApi implements LibertyBans { private final PunishmentDrafter drafter; private final PunishmentRevoker revoker; private final PunishmentSelector selector; - private final Provider databaseProvider; + private final Provider dbProvider; private final PunishmentFormatter formatter; private final ScopeManager scopeManager; private final UserResolver userResolver; + private final AccountSupervisor accountSupervisor; @Inject - public LibertyBansApi(Omnibus omnibus, FactoryOfTheFuture futuresFactory, PunishmentDrafter drafter, - PunishmentRevoker revoker, PunishmentSelector selector, Provider databaseProvider, - PunishmentFormatter formatter, ScopeManager scopeManager, UserResolver userResolver) { + public LibertyBansApi(Omnibus omnibus, FactoryOfTheFuture futuresFactory, Provider dbProvider, + PunishmentDrafter drafter, PunishmentRevoker revoker, PunishmentSelector selector, + PunishmentFormatter formatter, ScopeManager scopeManager, + UserResolver userResolver, AccountSupervisor accountSupervisor) { this.omnibus = omnibus; this.futuresFactory = futuresFactory; this.drafter = drafter; this.revoker = revoker; this.selector = selector; - this.databaseProvider = databaseProvider; + this.dbProvider = dbProvider; this.formatter = formatter; this.scopeManager = scopeManager; this.userResolver = userResolver; + this.accountSupervisor = accountSupervisor; } @Override @@ -89,7 +90,7 @@ public PunishmentSelector getSelector() { @Override public PunishmentDatabase getDatabase() { - return databaseProvider.get(); + return dbProvider.get(); } @Override @@ -107,4 +108,9 @@ public UserResolver getUserResolver() { return userResolver; } + @Override + public AccountSupervisor getAccountSupervisor() { + return accountSupervisor; + } + } diff --git a/bans-core/src/main/java/space/arim/libertybans/core/alts/AccountHistory.java b/bans-core/src/main/java/space/arim/libertybans/core/alts/AccountHistory.java index ebab4435e..9b0bc588a 100644 --- a/bans-core/src/main/java/space/arim/libertybans/core/alts/AccountHistory.java +++ b/bans-core/src/main/java/space/arim/libertybans/core/alts/AccountHistory.java @@ -1,6 +1,6 @@ /* * LibertyBans - * Copyright © 2022 Anand Beh + * Copyright © 2023 Anand Beh * * LibertyBans is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -24,8 +24,10 @@ import org.jooq.Condition; import space.arim.libertybans.api.AddressVictim; import space.arim.libertybans.api.CompositeVictim; +import space.arim.libertybans.api.NetworkAddress; import space.arim.libertybans.api.PlayerVictim; import space.arim.libertybans.api.Victim; +import space.arim.libertybans.api.user.KnownAccount; import space.arim.libertybans.core.database.execute.QueryExecutor; import space.arim.libertybans.core.database.execute.SQLFunction; import space.arim.libertybans.core.punish.MiscUtil; @@ -33,6 +35,7 @@ import java.time.Instant; import java.util.List; +import java.util.Optional; import java.util.UUID; import static space.arim.libertybans.core.schema.tables.Addresses.ADDRESSES; @@ -47,21 +50,37 @@ public AccountHistory(Provider queryExecutor) { this.queryExecutor = queryExecutor; } - private CentralisedFuture> knownAccountsWhere(Condition condition) { + record KnownAccountImpl(UUID uuid, String username, NetworkAddress address, Instant recorded, + AccountHistory accountHistory) implements KnownAccount { + + @Override + public Optional latestUsername() { + return Optional.ofNullable(username); + } + + @Override + public CentralisedFuture deleteFromHistory() { + return accountHistory.deleteAccount(uuid, recorded); + } + + } + + private CentralisedFuture> knownAccountsWhere(Condition condition) { return queryExecutor.get().query(SQLFunction.readOnly((context) -> { return context .select(ADDRESSES.UUID, ADDRESSES.ADDRESS, LATEST_NAMES.NAME, ADDRESSES.UPDATED) .from(ADDRESSES) - .innerJoin(LATEST_NAMES) + .leftJoin(LATEST_NAMES) .on(ADDRESSES.UUID.eq(LATEST_NAMES.UUID)) .where(condition) .orderBy(ADDRESSES.UPDATED.asc()) .fetch((record) -> { - return new KnownAccount( + return new KnownAccountImpl( record.get(ADDRESSES.UUID), record.get(LATEST_NAMES.NAME), record.get(ADDRESSES.ADDRESS), - record.get(ADDRESSES.UPDATED) + record.get(ADDRESSES.UPDATED), + this ); }); })); @@ -76,7 +95,7 @@ private CentralisedFuture> knownAccountsWhere(Condition condi * @param victim the uuid or IP address * @return the detected alts, sorted in order of oldest first */ - public CentralisedFuture> knownAccounts(Victim victim) { + public CentralisedFuture> knownAccounts(Victim victim) { if (victim instanceof PlayerVictim playerVictim) { return knownAccountsWhere(ADDRESSES.UUID.eq(playerVictim.getUUID())); diff --git a/bans-core/src/main/java/space/arim/libertybans/core/alts/AccountHistoryFormatter.java b/bans-core/src/main/java/space/arim/libertybans/core/alts/AccountHistoryFormatter.java index 1143b5ded..c01d75711 100644 --- a/bans-core/src/main/java/space/arim/libertybans/core/alts/AccountHistoryFormatter.java +++ b/bans-core/src/main/java/space/arim/libertybans/core/alts/AccountHistoryFormatter.java @@ -1,6 +1,6 @@ /* * LibertyBans - * Copyright © 2021 Anand Beh + * Copyright © 2023 Anand Beh * * LibertyBans is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -23,6 +23,7 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.ComponentLike; import space.arim.api.jsonchat.adventure.util.ComponentText; +import space.arim.libertybans.api.user.KnownAccount; import space.arim.libertybans.core.config.Configs; import space.arim.libertybans.core.config.InternalFormatter; @@ -40,27 +41,22 @@ public AccountHistoryFormatter(Configs configs, InternalFormatter formatter) { listFormat = new ListFormat<>(formatter, new KnownAccountFormat(configs, formatter)); } - public Component formatMessage(String target, List knownAccounts) { + public Component formatMessage(String target, List knownAccounts) { ComponentText header = configs.getMessagesConfig().accountHistory().listing().header(); return listFormat.formatMessage(header, target, knownAccounts); } - private static final class KnownAccountFormat implements ListFormat.ElementFormat { - - private final Configs configs; - private final InternalFormatter formatter; - - private KnownAccountFormat(Configs configs, InternalFormatter formatter) { - this.configs = configs; - this.formatter = formatter; - } + private record KnownAccountFormat(Configs configs, + InternalFormatter formatter) implements ListFormat.ElementFormat { @Override public ComponentLike format(String target, KnownAccount knownAccount) { - Instant recorded = knownAccount.updated(); + Instant recorded = knownAccount.recorded(); return configs.getMessagesConfig().accountHistory().listing().layout() .replaceText("%TARGET%", target) - .replaceText("%USERNAME%", knownAccount.username()) + .replaceText("%USERNAME%", knownAccount.latestUsername().orElse( + configs.getMessagesConfig().formatting().victimDisplay().playerNameUnknown() + )) .replaceText("%ADDRESS%", knownAccount.address().toString()) .replaceText("%DATE_RECORDED%", formatter.formatAbsoluteDate(recorded)) .replaceText("%DATE_RECORDED_RAW%", Long.toString(recorded.getEpochSecond())); diff --git a/bans-core/src/main/java/space/arim/libertybans/core/alts/AltCheckFormatter.java b/bans-core/src/main/java/space/arim/libertybans/core/alts/AltCheckFormatter.java index 6c9224b47..2357803de 100644 --- a/bans-core/src/main/java/space/arim/libertybans/core/alts/AltCheckFormatter.java +++ b/bans-core/src/main/java/space/arim/libertybans/core/alts/AltCheckFormatter.java @@ -1,6 +1,6 @@ /* * LibertyBans - * Copyright © 2021 Anand Beh + * Copyright © 2023 Anand Beh * * LibertyBans is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -42,60 +42,51 @@ public Component formatMessage(ComponentText header, String target, List { - - private final Configs configs; - private final InternalFormatter formatter; - - private DetectedAltFormat(Configs configs, InternalFormatter formatter) { - this.configs = configs; - this.formatter = formatter; - } + private record DetectedAltFormat(Configs configs, InternalFormatter formatter) + implements ListFormat.ElementFormat { private AltsSection.Formatting formatting() { return configs.getMessagesConfig().alts().formatting(); } - private ComponentLike getKind(DetectionKind detectionKind) { - switch (detectionKind) { - case NORMAL: - return formatting().normal(); - case STRICT: - return formatting().strict(); - default: - throw new IllegalArgumentException("Unknown kind " + detectionKind); + private ComponentLike formatUsername(DetectedAlt detectedAlt) { + PunishmentType type; + if (detectedAlt.hasActivePunishment(PunishmentType.BAN)) { + type = PunishmentType.BAN; + } else if (detectedAlt.hasActivePunishment(PunishmentType.MUTE)) { + type = PunishmentType.MUTE; + } else { + type = null; } - } - - private ComponentText getUsernameFormat(PunishmentType type) { + ComponentText usernameFormat; var nameDisplay = formatting().nameDisplay(); if (type == null) { - return nameDisplay.notPunished(); - } - switch (type) { - case BAN: - return nameDisplay.banned(); - case MUTE: - return nameDisplay.muted(); - default: - throw new IllegalArgumentException("Punishment type " + type + " not found"); + usernameFormat = nameDisplay.notPunished(); + } else { + usernameFormat = switch (type) { + case BAN -> nameDisplay.banned(); + case MUTE -> nameDisplay.muted(); + default -> throw new IllegalArgumentException("Punishment type " + type + " not available"); + }; } - } - - private ComponentLike formatUsername(DetectedAlt detectedAlt) { - return getUsernameFormat(detectedAlt.punishmentType().orElse(null)) - .replaceText("%USERNAME%", detectedAlt.relevantUserName()); + return usernameFormat.replaceText("%USERNAME%", detectedAlt.latestUsername().orElse( + configs.getMessagesConfig().formatting().victimDisplay().playerNameUnknown() + )); } @Override public ComponentLike format(String target, DetectedAlt detectedAlt) { return formatting().layout() - .replaceText("%ADDRESS%", detectedAlt.relevantAddress().toString()) - .replaceText("%RELEVANT_USERID%", detectedAlt.relevantUserId().toString()) - .replaceText("%DATE_RECORDED%", formatter.formatAbsoluteDate(detectedAlt.dateAccountRecorded())) + .replaceText("%ADDRESS%", detectedAlt.address().toString()) + .replaceText("%RELEVANT_USERID%", detectedAlt.uuid().toString()) + .replaceText("%DATE_RECORDED%", formatter.formatAbsoluteDate(detectedAlt.lastObserved())) .asComponent() .replaceText((config) -> { - config.matchLiteral("%DETECTION_KIND%").replacement(getKind(detectedAlt.detectionKind())); + Component detectionKind = switch (detectedAlt.detectionKind()) { + case NORMAL -> formatting().normal(); + case STRICT -> formatting().strict(); + }; + config.matchLiteral("%DETECTION_KIND%").replacement(detectionKind); }) .replaceText((config) -> { config.matchLiteral("%RELEVANT_USER%").replacement(formatUsername(detectedAlt)); diff --git a/bans-core/src/main/java/space/arim/libertybans/core/alts/AltDetection.java b/bans-core/src/main/java/space/arim/libertybans/core/alts/AltDetection.java index 666db30c2..8395cb794 100644 --- a/bans-core/src/main/java/space/arim/libertybans/core/alts/AltDetection.java +++ b/bans-core/src/main/java/space/arim/libertybans/core/alts/AltDetection.java @@ -1,6 +1,6 @@ /* * LibertyBans - * Copyright © 2021 Anand Beh + * Copyright © 2023 Anand Beh * * LibertyBans is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -23,28 +23,38 @@ import jakarta.inject.Provider; import org.jooq.DSLContext; import org.jooq.Field; -import org.jooq.impl.DSL; +import org.jooq.SelectField; +import org.jooq.Table; import space.arim.libertybans.api.NetworkAddress; import space.arim.libertybans.api.PunishmentType; +import space.arim.libertybans.api.user.AltAccount; +import space.arim.libertybans.api.user.DetectionQuery; import space.arim.libertybans.core.config.Configs; import space.arim.libertybans.core.database.execute.QueryExecutor; import space.arim.libertybans.core.database.execute.SQLFunction; import space.arim.libertybans.core.database.sql.AccountExpirationCondition; import space.arim.libertybans.core.database.sql.EndTimeCondition; -import space.arim.libertybans.core.database.sql.SimpleViewFields; +import space.arim.libertybans.core.database.sql.TableForType; import space.arim.libertybans.core.database.sql.VictimCondition; import space.arim.libertybans.core.env.UUIDAndAddress; import space.arim.libertybans.core.service.Time; import space.arim.omnibus.util.concurrent.CentralisedFuture; import java.time.Instant; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.EnumSet; import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; import java.util.UUID; +import java.util.function.Predicate; +import static org.jooq.impl.DSL.field; import static space.arim.libertybans.core.schema.tables.Addresses.ADDRESSES; import static space.arim.libertybans.core.schema.tables.LatestNames.LATEST_NAMES; -import static space.arim.libertybans.core.schema.tables.SimpleBans.SIMPLE_BANS; -import static space.arim.libertybans.core.schema.tables.SimpleMutes.SIMPLE_MUTES; public class AltDetection { @@ -70,43 +80,46 @@ public AltDetection(Configs configs, Provider queryExecutor, Time * the lack of pagination we want to show a short and linear progression from old to new. * * @param context the query source with which to contact the database - * @param uuid the user's uuid - * @param address the user's address - * @param whichAlts which alts to detect + * @param query the detection query + * @param whichAlts which alts to remove from the resulting list * @return the detected alts, sorted in order of oldest first */ - public List detectAlts(DSLContext context, UUID uuid, NetworkAddress address, - WhichAlts whichAlts) { + private List detectAlts(DSLContext context, DetectionQuery query, WhichAlts whichAlts) { // This implementation relies on strict detection including normal detection // The detection kind is inferred while processing the results final Instant currentTime = time.currentTimestamp(); var detectedAlt = ADDRESSES.as("detected_alt"); - Field hasBan = DSL.field(SIMPLE_BANS.VICTIM_TYPE.isNotNull()).as("has_ban"); - Field hasMute = DSL.field(SIMPLE_MUTES.VICTIM_TYPE.isNotNull()).as("has_mute"); - List detectedAlts = context - .select( - detectedAlt.ADDRESS, detectedAlt.UUID, - LATEST_NAMES.NAME, detectedAlt.UPDATED, - hasBan, hasMute - ) - .from(ADDRESSES) + + List> selectFields = new ArrayList<>(List.of( + detectedAlt.ADDRESS, detectedAlt.UUID, + LATEST_NAMES.NAME, detectedAlt.UPDATED + )); + Table joinedTables = ADDRESSES // Detect alts .innerJoin(detectedAlt) .on(ADDRESSES.ADDRESS.eq(detectedAlt.ADDRESS)) .and(ADDRESSES.UUID.notEqual(detectedAlt.UUID)) - // Map to names - .innerJoin(LATEST_NAMES) - .on(LATEST_NAMES.UUID.eq(detectedAlt.UUID)) - // Pair with bans - .leftJoin(SIMPLE_BANS) - .on(new VictimCondition(new SimpleViewFields(SIMPLE_BANS)).matchesUUID(detectedAlt.UUID)) - .and(new EndTimeCondition(new SimpleViewFields(SIMPLE_BANS)).isNotExpired(currentTime)) - // Pair with mutes - .leftJoin(SIMPLE_MUTES) - .on(new VictimCondition(new SimpleViewFields(SIMPLE_MUTES)).matchesUUID(detectedAlt.UUID)) - .and(new EndTimeCondition(new SimpleViewFields(SIMPLE_MUTES)).isNotExpired(currentTime)) + // Pair with latest names + .leftJoin(LATEST_NAMES) + .on(LATEST_NAMES.UUID.eq(detectedAlt.UUID)); + + Map> hasTypeFields = new EnumMap<>(PunishmentType.class); + for (PunishmentType type : query.punishmentTypes()) { + // Pair with that particular type + var simpleView = new TableForType(type).simpleView(); + joinedTables = joinedTables + .leftJoin(simpleView.table()) + .on(new VictimCondition(simpleView).matchesUUID(detectedAlt.UUID)) + .and(new EndTimeCondition(simpleView).isNotExpired(currentTime)); + Field hasTypeField = field(simpleView.victimType().isNotNull().as("has_" + type.toString().toLowerCase(Locale.ROOT))); + hasTypeFields.put(type, hasTypeField); + selectFields.add(hasTypeField); + } + List detectedAlts = context + .select(selectFields) + .from(joinedTables) // Select alts for the player in question - .where(ADDRESSES.UUID.eq(uuid)) + .where(ADDRESSES.UUID.eq(query.uuid())) // Filter non-expired alts .and(new AccountExpirationCondition(detectedAlt.UPDATED).isNotExpired(configs, currentTime)) // Order with oldest first @@ -114,40 +127,55 @@ public List detectAlts(DSLContext context, UUID uuid, NetworkAddres .fetch((record) -> { NetworkAddress detectedAddress = record.get(detectedAlt.ADDRESS); // If this alt can be detected 'normally', then the address will be the same - DetectionKind detectionKind = (address.equals(detectedAddress)) ? DetectionKind.NORMAL : DetectionKind.STRICT; - // Determine most significant punishment - PunishmentType punishmentType; - if (record.get(hasBan)) { - punishmentType = PunishmentType.BAN; - } else if (record.get(hasMute)) { - punishmentType = PunishmentType.MUTE; - } else { - punishmentType = null; + DetectionKind detectionKind = (query.address().equals(detectedAddress)) ? DetectionKind.NORMAL : DetectionKind.STRICT; + // Determine scanned scannedTypes + Set scannedTypes = EnumSet.noneOf(PunishmentType.class); + for (PunishmentType scanFor : query.punishmentTypes()) { + var hasType = hasTypeFields.get(scanFor).get(record); + Objects.requireNonNull(hasType); + if (hasType) { + scannedTypes.add(scanFor); + } } return new DetectedAlt( - detectionKind, - punishmentType, - detectedAddress, record.get(detectedAlt.UUID), record.get(LATEST_NAMES.NAME), - record.get(detectedAlt.UPDATED) + detectedAddress, + record.get(detectedAlt.UPDATED), + detectionKind, + query, + scannedTypes ); }); - switch (whichAlts) { - case ALL_ALTS: - break; - case BANNED_OR_MUTED_ALTS: - detectedAlts.removeIf((alt) -> alt.punishmentType().isEmpty()); - break; - case BANNED_ALTS: - detectedAlts.removeIf((alt) -> alt.punishmentType().orElse(null) != PunishmentType.BAN); - break; - default: - throw new IllegalArgumentException("Unknown WhichAlts " + whichAlts); - } + Predicate removeIf = switch (whichAlts) { + case ALL_ALTS -> (alt) -> false; + case BANNED_OR_MUTED_ALTS -> (alt) -> alt.scannedTypes().isEmpty(); + case BANNED_ALTS -> (alt) -> !alt.scannedTypes().contains(PunishmentType.BAN); + }; + detectedAlts.removeIf(removeIf); return detectedAlts; } + record AltQuery(UUID uuid, NetworkAddress address, + Set punishmentTypes, AltDetection impl) implements DetectionQuery { + + @Override + public CentralisedFuture> detect() { + return impl.queryExecutor.get().query(SQLFunction.readOnly((context) -> { + return impl.detectAlts(context, this, WhichAlts.ALL_ALTS); + })); + } + + } + + public List detectAlts(DSLContext context, UUID uuid, NetworkAddress address, WhichAlts whichAlts) { + return detectAlts( + context, + new AltQuery(uuid, address, Set.of(PunishmentType.BAN, PunishmentType.MUTE), this), + whichAlts + ); + } + public CentralisedFuture> detectAlts(UUID uuid, NetworkAddress address, WhichAlts whichAlts) { return queryExecutor.get().query(SQLFunction.readOnly((context) -> { return detectAlts(context, uuid, address, whichAlts); diff --git a/bans-core/src/main/java/space/arim/libertybans/core/alts/DetectedAlt.java b/bans-core/src/main/java/space/arim/libertybans/core/alts/DetectedAlt.java index c433d248c..088544e3d 100644 --- a/bans-core/src/main/java/space/arim/libertybans/core/alts/DetectedAlt.java +++ b/bans-core/src/main/java/space/arim/libertybans/core/alts/DetectedAlt.java @@ -1,6 +1,6 @@ /* * LibertyBans - * Copyright © 2021 Anand Beh + * Copyright © 2023 Anand Beh * * LibertyBans is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -21,86 +21,39 @@ import space.arim.libertybans.api.NetworkAddress; import space.arim.libertybans.api.PunishmentType; +import space.arim.libertybans.api.user.AltAccount; +import space.arim.libertybans.api.user.DetectionQuery; import java.time.Instant; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.UUID; -public final class DetectedAlt { +public record DetectedAlt(UUID uuid, String username, NetworkAddress address, Instant lastObserved, + DetectionKind detectionKind, DetectionQuery query, Set scannedTypes) + implements AltAccount { - private final DetectionKind detectionKind; - private final PunishmentType punishmentType; - private final NetworkAddress relevantAddress; - private final UUID relevantUserId; - private final String relevantUserName; - private final Instant dateAccountRecorded; - - public DetectedAlt(DetectionKind detectionKind, PunishmentType punishmentType, - NetworkAddress relevantAddress, UUID relevantUserId, String relevantUserName, - Instant dateAccountRecorded) { - this.detectionKind = Objects.requireNonNull(detectionKind, "detectionKind"); - this.punishmentType = punishmentType; - this.relevantAddress = Objects.requireNonNull(relevantAddress, "relevantAddress"); - this.relevantUserId = Objects.requireNonNull(relevantUserId, "relevantUserId"); - this.relevantUserName = Objects.requireNonNull(relevantUserName, "relevantUserName"); - this.dateAccountRecorded = Objects.requireNonNull(dateAccountRecorded, "dateAccountRecorded"); - } - - public DetectionKind detectionKind() { - return detectionKind; - } - - public Optional punishmentType() { - return Optional.ofNullable(punishmentType); - } - - public NetworkAddress relevantAddress() { - return relevantAddress; - } - - public UUID relevantUserId() { - return relevantUserId; - } - - public String relevantUserName() { - return relevantUserName; - } - - public Instant dateAccountRecorded() { - return dateAccountRecorded; + public DetectedAlt { + Objects.requireNonNull(uuid, "uuid"); + Objects.requireNonNull(address, "address"); + Objects.requireNonNull(lastObserved, "lastObserved"); + Objects.requireNonNull(detectionKind, "detectionKind"); + Objects.requireNonNull(query, "query"); + scannedTypes = Set.copyOf(scannedTypes); } @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - DetectedAlt that = (DetectedAlt) o; - return detectionKind == that.detectionKind && Objects.equals(punishmentType, that.punishmentType) - && relevantAddress.equals(that.relevantAddress) && relevantUserId.equals(that.relevantUserId) && relevantUserName.equals(that.relevantUserName) - && dateAccountRecorded.equals(that.dateAccountRecorded); + public Optional latestUsername() { + return Optional.ofNullable(username); } @Override - public int hashCode() { - int result = detectionKind.hashCode(); - result = 31 * result + Objects.hashCode(punishmentType); - result = 31 * result + relevantAddress.hashCode(); - result = 31 * result + relevantUserId.hashCode(); - result = 31 * result + relevantUserName.hashCode(); - result = 31 * result + dateAccountRecorded.hashCode(); - return result; + public boolean hasActivePunishment(PunishmentType type) { + if (!query.punishmentTypes().contains(type)) { + throw new IllegalArgumentException("Type not part of the detection query: " + type); + } + return scannedTypes.contains(type); } - @Override - public String toString() { - return "DetectedAlt{" + - "detectionKind=" + detectionKind + - ", punishmentType=" + punishmentType + - ", relevantAddress=" + relevantAddress + - ", relevantUserId=" + relevantUserId + - ", relevantUserName='" + relevantUserName + '\'' + - ", dateAccountRecorded=" + dateAccountRecorded + - '}'; - } } diff --git a/bans-core/src/main/java/space/arim/libertybans/core/alts/KnownAccount.java b/bans-core/src/main/java/space/arim/libertybans/core/alts/KnownAccount.java deleted file mode 100644 index a3c0d1224..000000000 --- a/bans-core/src/main/java/space/arim/libertybans/core/alts/KnownAccount.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * LibertyBans - * Copyright © 2021 Anand Beh - * - * LibertyBans is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * LibertyBans is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with LibertyBans. If not, see - * and navigate to version 3 of the GNU Affero General Public License. - */ - -package space.arim.libertybans.core.alts; - -import space.arim.libertybans.api.NetworkAddress; - -import java.time.Instant; -import java.util.Objects; -import java.util.UUID; - -public final class KnownAccount { - - private final UUID uuid; - private final String username; - private final NetworkAddress address; - private final Instant updated; - - public KnownAccount(UUID uuid, String username, NetworkAddress address, Instant updated) { - this.uuid = Objects.requireNonNull(uuid, "uuid"); - this.username = Objects.requireNonNull(username, "username"); - this.address = Objects.requireNonNull(address, "address"); - this.updated = Objects.requireNonNull(updated, "updated"); - } - - public UUID uuid() { - return uuid; - } - - public String username() { - return username; - } - - public NetworkAddress address() { - return address; - } - - public Instant updated() { - return updated; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - KnownAccount that = (KnownAccount) o; - return uuid.equals(that.uuid) && username.equals(that.username) && address.equals(that.address) && updated.equals(that.updated); - } - - @Override - public int hashCode() { - int result = uuid.hashCode(); - result = 31 * result + username.hashCode(); - result = 31 * result + address.hashCode(); - result = 31 * result + updated.hashCode(); - return result; - } - - @Override - public String toString() { - return "KnownAccount{" + - "uuid=" + uuid + - ", username='" + username + '\'' + - ", address=" + address + - ", updated=" + updated + - '}'; - } -} diff --git a/bans-core/src/main/java/space/arim/libertybans/core/alts/ListFormat.java b/bans-core/src/main/java/space/arim/libertybans/core/alts/ListFormat.java index 583a0321d..550f56e14 100644 --- a/bans-core/src/main/java/space/arim/libertybans/core/alts/ListFormat.java +++ b/bans-core/src/main/java/space/arim/libertybans/core/alts/ListFormat.java @@ -1,6 +1,6 @@ /* * LibertyBans - * Copyright © 2021 Anand Beh + * Copyright © 2023 Anand Beh * * LibertyBans is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -38,7 +38,7 @@ class ListFormat { this.elementFormat = elementFormat; } - Component formatMessage(ComponentText header, String target, List data) { + Component formatMessage(ComponentText header, String target, List data) { List messages = new ArrayList<>(data.size() + 1); messages.add(formatter.prefix(header.replaceText("%TARGET%", target))); for (T datum : data) { diff --git a/bans-core/src/main/java/space/arim/libertybans/core/alts/Supervisor.java b/bans-core/src/main/java/space/arim/libertybans/core/alts/Supervisor.java new file mode 100644 index 000000000..f841ab43b --- /dev/null +++ b/bans-core/src/main/java/space/arim/libertybans/core/alts/Supervisor.java @@ -0,0 +1,83 @@ +/* + * LibertyBans + * Copyright © 2023 Anand Beh + * + * LibertyBans is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * LibertyBans is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with LibertyBans. If not, see + * and navigate to version 3 of the GNU Affero General Public License. + */ + +package space.arim.libertybans.core.alts; + +import jakarta.inject.Inject; +import space.arim.libertybans.api.AddressVictim; +import space.arim.libertybans.api.CompositeVictim; +import space.arim.libertybans.api.NetworkAddress; +import space.arim.libertybans.api.PlayerVictim; +import space.arim.libertybans.api.PunishmentType; +import space.arim.libertybans.api.user.AccountSupervisor; +import space.arim.libertybans.api.user.DetectionQuery; +import space.arim.libertybans.api.user.KnownAccount; +import space.arim.omnibus.util.concurrent.CentralisedFuture; + +import java.util.List; +import java.util.Set; +import java.util.UUID; + +public final class Supervisor implements AccountSupervisor { + + private final AltDetection altDetection; + private final AccountHistory accountHistory; + + @Inject + public Supervisor(AltDetection altDetection, AccountHistory accountHistory) { + this.altDetection = altDetection; + this.accountHistory = accountHistory; + } + + @Override + public DetectionQuery.Builder detectAlts(UUID uuid, NetworkAddress address) { + class Builder implements DetectionQuery.Builder { + + private Set types = Set.of(); + + @Override + public DetectionQuery.Builder punishmentTypes(Set types) { + this.types = Set.copyOf(types); + return this; + } + + @Override + public DetectionQuery build() { + return new AltDetection.AltQuery(uuid, address, types, altDetection); + } + } + return new Builder(); + } + + @Override + public CentralisedFuture> findAccountsMatching(UUID uuid) { + return accountHistory.knownAccounts(PlayerVictim.of(uuid)); + } + + @Override + public CentralisedFuture> findAccountsMatching(NetworkAddress address) { + return accountHistory.knownAccounts(AddressVictim.of(address)); + } + + @Override + public CentralisedFuture> findAccountsMatching(UUID uuid, NetworkAddress address) { + return accountHistory.knownAccounts(CompositeVictim.of(uuid, address)); + } + +}