From 35550290de02b35b88c609e150468a89b6b81337 Mon Sep 17 00:00:00 2001 From: Zabuzard Date: Fri, 3 Dec 2021 11:29:39 +0100 Subject: [PATCH 1/6] Added ModerationActionsStore --- .../moderation/ModerationActionsStore.java | 88 +++++++++++++++++++ .../db/V5__Add_Moderation_Actions.sql | 11 +++ 2 files changed, 99 insertions(+) create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationActionsStore.java create mode 100644 application/src/main/resources/db/V5__Add_Moderation_Actions.sql diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationActionsStore.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationActionsStore.java new file mode 100644 index 0000000000..9b1897d299 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationActionsStore.java @@ -0,0 +1,88 @@ +package org.togetherjava.tjbot.commands.moderation; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.togetherjava.tjbot.db.Database; +import org.togetherjava.tjbot.db.generated.tables.ModerationActions; +import org.togetherjava.tjbot.db.generated.tables.records.ModerationActionsRecord; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +// FIXME Javadoc on the whole class +final class ModerationActionsStore { + private final Database database; + + ModerationActionsStore(@NotNull Database database) { + this.database = database; + } + + public @NotNull List getActionsByTypeAscending(long guildId, + @NotNull ModerationUtils.Action actionType) { + return database.read(context -> context.selectFrom(ModerationActions.MODERATION_ACTIONS) + .where(ModerationActions.MODERATION_ACTIONS.GUILD_ID.eq(guildId) + .and(ModerationActions.MODERATION_ACTIONS.ACTION_TYPE.eq(actionType.name()))) + .orderBy(ModerationActions.MODERATION_ACTIONS.ISSUED_AT.asc()) + .stream() + .map(ActionRecord::of) + .toList()); + } + + public @NotNull List getActionsByTargetAscending(long guildId, long targetId) { + return database.read(context -> context.selectFrom(ModerationActions.MODERATION_ACTIONS) + .where(ModerationActions.MODERATION_ACTIONS.GUILD_ID.eq(guildId) + .and(ModerationActions.MODERATION_ACTIONS.TARGET_ID.eq(targetId))) + .orderBy(ModerationActions.MODERATION_ACTIONS.ISSUED_AT.asc()) + .stream() + .map(ActionRecord::of) + .toList()); + } + + public @NotNull List getActionsByAuthorAscending(long guildId, long authorId) { + return database.read(context -> context.selectFrom(ModerationActions.MODERATION_ACTIONS) + .where(ModerationActions.MODERATION_ACTIONS.GUILD_ID.eq(guildId) + .and(ModerationActions.MODERATION_ACTIONS.AUTHOR_ID.eq(authorId))) + .orderBy(ModerationActions.MODERATION_ACTIONS.ISSUED_AT.asc()) + .stream() + .map(ActionRecord::of) + .toList()); + } + + public @NotNull Optional findActionByCaseId(int caseId) { + return Optional + .of(database.read(context -> context.selectFrom(ModerationActions.MODERATION_ACTIONS) + .where(ModerationActions.MODERATION_ACTIONS.CASE_ID.eq(caseId)) + .fetchOne())) + .map(ActionRecord::of); + } + + public int addAction(long guildId, long authorId, long targetId, + @NotNull ModerationUtils.Action actionType, @Nullable Instant actionExpiresAt, + @NotNull String reason) { + return database.writeAndProvide(context -> { + ModerationActionsRecord actionRecord = + context.newRecord(ModerationActions.MODERATION_ACTIONS) + .setIssuedAt(Instant.now()) + .setGuildId(guildId) + .setAuthorId(authorId) + .setTargetId(targetId) + .setActionType(actionType.name()) + .setActionExpiresAt(actionExpiresAt) + .setReason(reason); + actionRecord.insert(); + return actionRecord.getCaseId(); + }); + } + + record ActionRecord(int caseId, @NotNull Instant issuedAt, long guildId, long authorId, + long targetId, @NotNull ModerationUtils.Action actionType, + @Nullable Instant actionExpiresAt, @NotNull String reason) { + private static ActionRecord of(@NotNull ModerationActionsRecord action) { + return new ActionRecord(action.getCaseId(), action.getIssuedAt(), action.getGuildId(), + action.getAuthorId(), action.getTargetId(), + ModerationUtils.Action.valueOf(action.getActionType()), + action.getActionExpiresAt(), action.getReason()); + } + } +} diff --git a/application/src/main/resources/db/V5__Add_Moderation_Actions.sql b/application/src/main/resources/db/V5__Add_Moderation_Actions.sql new file mode 100644 index 0000000000..8cc8141f7a --- /dev/null +++ b/application/src/main/resources/db/V5__Add_Moderation_Actions.sql @@ -0,0 +1,11 @@ +CREATE TABLE moderation_actions +( + case_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + issued_at TIMESTAMP NOT NULL, + guild_id BIGINT NOT NULL, + author_id BIGINT NOT NULL, + target_id BIGINT NOT NULL, + action_type TEXT NOT NULL, + action_expires_at TIMESTAMP, + reason TEXT NOT NULL +) \ No newline at end of file From 5d214ebd268d6ad20fbeb236bd94b8a8461b4aed Mon Sep 17 00:00:00 2001 From: Zabuzard Date: Fri, 3 Dec 2021 11:46:25 +0100 Subject: [PATCH 2/6] Integrated store into moderation commands * ban * unban * kick --- .../togetherjava/tjbot/commands/Commands.java | 8 +++++--- .../tjbot/commands/moderation/BanCommand.java | 17 ++++++++++++----- .../tjbot/commands/moderation/KickCommand.java | 15 +++++++++++---- .../moderation/ModerationActionsStore.java | 6 +++--- .../tjbot/commands/moderation/UnbanCommand.java | 11 +++++++++-- 5 files changed, 40 insertions(+), 17 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/Commands.java b/application/src/main/java/org/togetherjava/tjbot/commands/Commands.java index 0d6b32526e..f1e054e641 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/Commands.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/Commands.java @@ -8,6 +8,7 @@ import org.togetherjava.tjbot.commands.mathcommands.TeXCommand; import org.togetherjava.tjbot.commands.moderation.BanCommand; import org.togetherjava.tjbot.commands.moderation.KickCommand; +import org.togetherjava.tjbot.commands.moderation.ModerationActionsStore; import org.togetherjava.tjbot.commands.moderation.UnbanCommand; import org.togetherjava.tjbot.commands.tags.TagCommand; import org.togetherjava.tjbot.commands.tags.TagManageCommand; @@ -41,6 +42,7 @@ public enum Commands { public static @NotNull Collection createSlashCommands( @NotNull Database database) { TagSystem tagSystem = new TagSystem(database); + ModerationActionsStore actionsStore = new ModerationActionsStore(database); // NOTE The command system can add special system relevant commands also by itself, // hence this list may not necessarily represent the full list of all commands actually // available. @@ -53,9 +55,9 @@ public enum Commands { commands.add(new TagManageCommand(tagSystem)); commands.add(new TagsCommand(tagSystem)); commands.add(new VcActivityCommand()); - commands.add(new KickCommand()); - commands.add(new BanCommand()); - commands.add(new UnbanCommand()); + commands.add(new KickCommand(actionsStore)); + commands.add(new BanCommand(actionsStore)); + commands.add(new UnbanCommand(actionsStore)); commands.add(new FreeCommand()); return commands; diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/BanCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/BanCommand.java index 427eb63ee4..82e6a4d205 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/BanCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/BanCommand.java @@ -43,11 +43,14 @@ public final class BanCommand extends SlashCommandAdapter { private static final String COMMAND_NAME = "ban"; private static final String ACTION_VERB = "ban"; private final Predicate hasRequiredRole; + private final ModerationActionsStore actionsStore; /** * Constructs an instance. + * + * @param actionsStore used to store actions issued by this command */ - public BanCommand() { + public BanCommand(@NotNull ModerationActionsStore actionsStore) { super(COMMAND_NAME, "Bans the given user from the server", SlashCommandVisibility.GUILD); getData().addOption(OptionType.USER, TARGET_OPTION, "The user who you want to ban", true) @@ -58,6 +61,7 @@ public BanCommand() { hasRequiredRole = Pattern.compile(Config.getInstance().getHeavyModerationRolePattern()) .asMatchPredicate(); + this.actionsStore = actionsStore; } private static RestAction handleAlreadyBanned(@NotNull Guild.Ban ban, @@ -72,9 +76,9 @@ private static RestAction handleAlreadyBanned(@NotNull Guild.Ba } @SuppressWarnings("MethodWithTooManyParameters") - private static RestAction banUserFlow(@NotNull User target, - @NotNull Member author, @NotNull String reason, int deleteHistoryDays, - @NotNull Guild guild, @NotNull SlashCommandEvent event) { + private RestAction banUserFlow(@NotNull User target, @NotNull Member author, + @NotNull String reason, int deleteHistoryDays, @NotNull Guild guild, + @NotNull SlashCommandEvent event) { return sendDm(target, reason, guild, event) .flatMap(hasSentDm -> banUser(target, author, reason, deleteHistoryDays, guild) .map(banResult -> hasSentDm)) @@ -97,13 +101,16 @@ private static RestAction sendDm(@NotNull ISnowflake target, @NotNull S .map(Result::isSuccess); } - private static AuditableRestAction banUser(@NotNull User target, @NotNull Member author, + private AuditableRestAction banUser(@NotNull User target, @NotNull Member author, @NotNull String reason, int deleteHistoryDays, @NotNull Guild guild) { logger.info( "'{}' ({}) banned the user '{}' ({}) from guild '{}' and deleted their message history of the last {} days, for reason '{}'.", author.getUser().getAsTag(), author.getId(), target.getAsTag(), target.getId(), guild.getName(), deleteHistoryDays, reason); + actionsStore.addAction(guild.getIdLong(), author.getIdLong(), target.getIdLong(), + ModerationUtils.Action.BAN, null, reason); + return guild.ban(target, deleteHistoryDays, reason); } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/KickCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/KickCommand.java index 27f81c3501..eb2ff8ac63 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/KickCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/KickCommand.java @@ -36,11 +36,14 @@ public final class KickCommand extends SlashCommandAdapter { private static final String COMMAND_NAME = "kick"; private static final String ACTION_VERB = "kick"; private final Predicate hasRequiredRole; + private final ModerationActionsStore actionsStore; /** * Constructs an instance. + * + * @param actionsStore used to store actions issued by this command */ - public KickCommand() { + public KickCommand(@NotNull ModerationActionsStore actionsStore) { super(COMMAND_NAME, "Kicks the given user from the server", SlashCommandVisibility.GUILD); getData().addOption(OptionType.USER, TARGET_OPTION, "The user who you want to kick", true) @@ -48,6 +51,7 @@ public KickCommand() { hasRequiredRole = Pattern.compile(Config.getInstance().getSoftModerationRolePattern()) .asMatchPredicate(); + this.actionsStore = actionsStore; } private static void handleAbsentTarget(@NotNull Interaction event) { @@ -56,7 +60,7 @@ private static void handleAbsentTarget(@NotNull Interaction event) { .queue(); } - private static void kickUserFlow(@NotNull Member target, @NotNull Member author, + private void kickUserFlow(@NotNull Member target, @NotNull Member author, @NotNull String reason, @NotNull Guild guild, @NotNull SlashCommandEvent event) { sendDm(target, reason, guild, event) .flatMap(hasSentDm -> kickUser(target, author, reason, guild) @@ -81,12 +85,15 @@ private static RestAction sendDm(@NotNull ISnowflake target, @NotNull S .map(Result::isSuccess); } - private static AuditableRestAction kickUser(@NotNull Member target, - @NotNull Member author, @NotNull String reason, @NotNull Guild guild) { + private AuditableRestAction kickUser(@NotNull Member target, @NotNull Member author, + @NotNull String reason, @NotNull Guild guild) { logger.info("'{}' ({}) kicked the user '{}' ({}) from guild '{}' for reason '{}'.", author.getUser().getAsTag(), author.getId(), target.getUser().getAsTag(), target.getId(), guild.getName(), reason); + actionsStore.addAction(guild.getIdLong(), author.getIdLong(), target.getIdLong(), + ModerationUtils.Action.KICK, null, reason); + return guild.kick(target, reason).reason(reason); } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationActionsStore.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationActionsStore.java index 9b1897d299..a8db212473 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationActionsStore.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationActionsStore.java @@ -11,10 +11,10 @@ import java.util.Optional; // FIXME Javadoc on the whole class -final class ModerationActionsStore { +public final class ModerationActionsStore { private final Database database; - ModerationActionsStore(@NotNull Database database) { + public ModerationActionsStore(@NotNull Database database) { this.database = database; } @@ -75,7 +75,7 @@ public int addAction(long guildId, long authorId, long targetId, }); } - record ActionRecord(int caseId, @NotNull Instant issuedAt, long guildId, long authorId, + public record ActionRecord(int caseId, @NotNull Instant issuedAt, long guildId, long authorId, long targetId, @NotNull ModerationUtils.Action actionType, @Nullable Instant actionExpiresAt, @NotNull String reason) { private static ActionRecord of(@NotNull ModerationActionsRecord action) { diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/UnbanCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/UnbanCommand.java index a8908e20fe..af71242672 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/UnbanCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/UnbanCommand.java @@ -29,11 +29,14 @@ public final class UnbanCommand extends SlashCommandAdapter { private static final String COMMAND_NAME = "unban"; private static final String ACTION_VERB = "unban"; private final Predicate hasRequiredRole; + private final ModerationActionsStore actionsStore; /** * Constructs an instance. + * + * @param actionsStore used to store actions issued by this command */ - public UnbanCommand() { + public UnbanCommand(@NotNull ModerationActionsStore actionsStore) { super(COMMAND_NAME, "Unbans the given user from the server", SlashCommandVisibility.GUILD); getData() @@ -43,9 +46,10 @@ public UnbanCommand() { hasRequiredRole = Pattern.compile(Config.getInstance().getHeavyModerationRolePattern()) .asMatchPredicate(); + this.actionsStore = actionsStore; } - private static void unban(@NotNull User target, @NotNull Member author, @NotNull String reason, + private void unban(@NotNull User target, @NotNull Member author, @NotNull String reason, @NotNull Guild guild, @NotNull Interaction event) { guild.unban(target).reason(reason).queue(result -> { MessageEmbed message = ModerationUtils.createActionResponse(author.getUser(), @@ -55,6 +59,9 @@ private static void unban(@NotNull User target, @NotNull Member author, @NotNull logger.info("'{}' ({}) unbanned the user '{}' ({}) from guild '{}' for reason '{}'.", author.getUser().getAsTag(), author.getId(), target.getAsTag(), target.getId(), guild.getName(), reason); + + actionsStore.addAction(guild.getIdLong(), author.getIdLong(), target.getIdLong(), + ModerationUtils.Action.UNBAN, null, reason); }, unbanFailure -> handleFailure(unbanFailure, target, event)); } From f0e4d9ecab05305f5b14f9e73060528ab8b84cab Mon Sep 17 00:00:00 2001 From: Zabuzard Date: Tue, 7 Dec 2021 18:58:08 +0100 Subject: [PATCH 3/6] Javadoc and Linter --- .../commands/moderation/ActionRecord.java | 40 ++++++++++ .../moderation/ModerationActionsStore.java | 80 ++++++++++++++++--- 2 files changed, 108 insertions(+), 12 deletions(-) create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/moderation/ActionRecord.java diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ActionRecord.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ActionRecord.java new file mode 100644 index 0000000000..dbfb81fa24 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ActionRecord.java @@ -0,0 +1,40 @@ +package org.togetherjava.tjbot.commands.moderation; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.togetherjava.tjbot.db.generated.tables.records.ModerationActionsRecord; + +import java.time.Instant; + +/** + * Record for actions as maintained by {@link ModerationActionsStore}. Each action has a unique + * caseId. + * + * @param caseId the unique case id associated with this action + * @param issuedAt the instant at which this action was issued + * @param guildId the id of the guild in which context this action happened + * @param authorId the id of the user who issued the action + * @param targetId the id of the user who was the target of the action + * @param actionType the type of the action + * @param actionExpiresAt the instant at which this action expires, for temporary actions; otherwise + * {@code null} + * @param reason the reason why this action was executed + */ +public record ActionRecord(int caseId, @NotNull Instant issuedAt, long guildId, long authorId, + long targetId, @NotNull ModerationUtils.Action actionType, + @Nullable Instant actionExpiresAt, @NotNull String reason) { + + /** + * Creates the action record that corresponds to the given action entry from the database table. + * + * @param action the action to convert + * @return the corresponding action record + */ + @SuppressWarnings("StaticMethodOnlyUsedInOneClass") + static @NotNull ActionRecord of(@NotNull ModerationActionsRecord action) { + return new ActionRecord(action.getCaseId(), action.getIssuedAt(), action.getGuildId(), + action.getAuthorId(), action.getTargetId(), + ModerationUtils.Action.valueOf(action.getActionType()), action.getActionExpiresAt(), + action.getReason()); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationActionsStore.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationActionsStore.java index a8db212473..e8cd72e832 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationActionsStore.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationActionsStore.java @@ -10,14 +10,40 @@ import java.util.List; import java.util.Optional; -// FIXME Javadoc on the whole class +/** + * Store for moderation actions, e.g. as banning users. Can be used to retrieve information about + * past events, such as when a user has been banned the last time. + * + * The store persists the actions and is thread safe. + * + * Actions have to be added to the store using + * {@link #addAction(long, long, long, ModerationUtils.Action, Instant, String)} at the time they + * are executed and can then be retrieved by methods such as + * {@link #getActionsByTypeAscending(long, ModerationUtils.Action)} or + * {@link #findActionByCaseId(int)}. + */ +@SuppressWarnings("ClassCanBeRecord") public final class ModerationActionsStore { private final Database database; + /** + * Creates a new instance which writes and retrieves actions from a given database. + * + * @param database the database to write and retrieve actions from + */ public ModerationActionsStore(@NotNull Database database) { this.database = database; } + /** + * Gets all actions of a given type that have been written to the store, chronologically + * ascending with the earliest action first. + * + * @param guildId the id of the guild, only actions that happened in the context of that guild + * will be retrieved + * @param actionType the type of action to filter for + * @return a list of all actions with the given type, chronologically ascending + */ public @NotNull List getActionsByTypeAscending(long guildId, @NotNull ModerationUtils.Action actionType) { return database.read(context -> context.selectFrom(ModerationActions.MODERATION_ACTIONS) @@ -29,6 +55,15 @@ public ModerationActionsStore(@NotNull Database database) { .toList()); } + /** + * Gets all actions executed against a given target that have been written to the store, + * chronologically ascending with the earliest action first. + * + * @param guildId the id of the guild, only actions that happened in the context of that guild + * will be retrieved + * @param targetId the id of the target user to filter for + * @return a list of all actions executed against the target, chronologically ascending + */ public @NotNull List getActionsByTargetAscending(long guildId, long targetId) { return database.read(context -> context.selectFrom(ModerationActions.MODERATION_ACTIONS) .where(ModerationActions.MODERATION_ACTIONS.GUILD_ID.eq(guildId) @@ -39,6 +74,15 @@ public ModerationActionsStore(@NotNull Database database) { .toList()); } + /** + * Gets all actions executed by a given author that have been written to the store, + * chronologically ascending with the earliest action first. + * + * @param guildId the id of the guild, only actions that happened in the context of that guild + * will be retrieved + * @param authorId the id of the author user to filter for + * @return a list of all actions executed by the author, chronologically ascending + */ public @NotNull List getActionsByAuthorAscending(long guildId, long authorId) { return database.read(context -> context.selectFrom(ModerationActions.MODERATION_ACTIONS) .where(ModerationActions.MODERATION_ACTIONS.GUILD_ID.eq(guildId) @@ -49,6 +93,12 @@ public ModerationActionsStore(@NotNull Database database) { .toList()); } + /** + * Gets the action with the given case id from the store, if present. + * + * @param caseId the actions' case id to search for + * @return the action with the given case id, if present + */ public @NotNull Optional findActionByCaseId(int caseId) { return Optional .of(database.read(context -> context.selectFrom(ModerationActions.MODERATION_ACTIONS) @@ -57,6 +107,23 @@ public ModerationActionsStore(@NotNull Database database) { .map(ActionRecord::of); } + /** + * Adds the given action to the store. A unique case id will be associated to the action and + * returned. + * + * It is assumed that the action is issued at the point in time this method is called. It is not + * possible to assign a different timestamp, especially not an earlier point in time. + * + * @param guildId the id of the guild in which context this action happened + * @param authorId the id of the user who issued the action + * @param targetId the id of the user who was the target of the action + * @param actionType the type of the action + * @param actionExpiresAt the instant at which this action expires, for temporary actions; + * otherwise {@code null} + * @param reason the reason why this action was executed + * @return the unique case id associated with the action + */ + @SuppressWarnings("MethodWithTooManyParameters") public int addAction(long guildId, long authorId, long targetId, @NotNull ModerationUtils.Action actionType, @Nullable Instant actionExpiresAt, @NotNull String reason) { @@ -74,15 +141,4 @@ public int addAction(long guildId, long authorId, long targetId, return actionRecord.getCaseId(); }); } - - public record ActionRecord(int caseId, @NotNull Instant issuedAt, long guildId, long authorId, - long targetId, @NotNull ModerationUtils.Action actionType, - @Nullable Instant actionExpiresAt, @NotNull String reason) { - private static ActionRecord of(@NotNull ModerationActionsRecord action) { - return new ActionRecord(action.getCaseId(), action.getIssuedAt(), action.getGuildId(), - action.getAuthorId(), action.getTargetId(), - ModerationUtils.Action.valueOf(action.getActionType()), - action.getActionExpiresAt(), action.getReason()); - } - } } From 127af26900374491bb9fc6f047e2bd304b24b94c Mon Sep 17 00:00:00 2001 From: Zabuzard Date: Tue, 7 Dec 2021 19:06:22 +0100 Subject: [PATCH 4/6] Note about timestamps being slightly off by design --- .../commands/moderation/ModerationActionsStore.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationActionsStore.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationActionsStore.java index e8cd72e832..9593fbe3c3 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationActionsStore.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationActionsStore.java @@ -14,13 +14,16 @@ * Store for moderation actions, e.g. as banning users. Can be used to retrieve information about * past events, such as when a user has been banned the last time. * - * The store persists the actions and is thread safe. - * * Actions have to be added to the store using * {@link #addAction(long, long, long, ModerationUtils.Action, Instant, String)} at the time they * are executed and can then be retrieved by methods such as * {@link #getActionsByTypeAscending(long, ModerationUtils.Action)} or * {@link #findActionByCaseId(int)}. + * + * Be aware that timestamps associated with actions, such as {@link ActionRecord#issuedAt()} are + * slightly off the timestamps used by Discord. + * + * The store persists the actions and is thread safe. */ @SuppressWarnings("ClassCanBeRecord") public final class ModerationActionsStore { @@ -113,6 +116,8 @@ public ModerationActionsStore(@NotNull Database database) { * * It is assumed that the action is issued at the point in time this method is called. It is not * possible to assign a different timestamp, especially not an earlier point in time. + * Consequently, this causes the timestamps to be slightly off from the timestamps recorded by + * Discord itself. * * @param guildId the id of the guild in which context this action happened * @param authorId the id of the user who issued the action From df4b87c516f71fe9ad20a22238c70a26ea1e2e17 Mon Sep 17 00:00:00 2001 From: Zabuzard Date: Tue, 7 Dec 2021 19:10:10 +0100 Subject: [PATCH 5/6] helper method to get rid of code duplication (CR by Zabuzard) --- .../moderation/ModerationActionsStore.java | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationActionsStore.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationActionsStore.java index 9593fbe3c3..ff88dc5a28 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationActionsStore.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationActionsStore.java @@ -2,6 +2,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jooq.Condition; import org.togetherjava.tjbot.db.Database; import org.togetherjava.tjbot.db.generated.tables.ModerationActions; import org.togetherjava.tjbot.db.generated.tables.records.ModerationActionsRecord; @@ -49,13 +50,8 @@ public ModerationActionsStore(@NotNull Database database) { */ public @NotNull List getActionsByTypeAscending(long guildId, @NotNull ModerationUtils.Action actionType) { - return database.read(context -> context.selectFrom(ModerationActions.MODERATION_ACTIONS) - .where(ModerationActions.MODERATION_ACTIONS.GUILD_ID.eq(guildId) - .and(ModerationActions.MODERATION_ACTIONS.ACTION_TYPE.eq(actionType.name()))) - .orderBy(ModerationActions.MODERATION_ACTIONS.ISSUED_AT.asc()) - .stream() - .map(ActionRecord::of) - .toList()); + return getActionsFromGuildAscending(guildId, + ModerationActions.MODERATION_ACTIONS.ACTION_TYPE.eq(actionType.name())); } /** @@ -68,13 +64,8 @@ public ModerationActionsStore(@NotNull Database database) { * @return a list of all actions executed against the target, chronologically ascending */ public @NotNull List getActionsByTargetAscending(long guildId, long targetId) { - return database.read(context -> context.selectFrom(ModerationActions.MODERATION_ACTIONS) - .where(ModerationActions.MODERATION_ACTIONS.GUILD_ID.eq(guildId) - .and(ModerationActions.MODERATION_ACTIONS.TARGET_ID.eq(targetId))) - .orderBy(ModerationActions.MODERATION_ACTIONS.ISSUED_AT.asc()) - .stream() - .map(ActionRecord::of) - .toList()); + return getActionsFromGuildAscending(guildId, + ModerationActions.MODERATION_ACTIONS.TARGET_ID.eq(targetId)); } /** @@ -87,13 +78,8 @@ public ModerationActionsStore(@NotNull Database database) { * @return a list of all actions executed by the author, chronologically ascending */ public @NotNull List getActionsByAuthorAscending(long guildId, long authorId) { - return database.read(context -> context.selectFrom(ModerationActions.MODERATION_ACTIONS) - .where(ModerationActions.MODERATION_ACTIONS.GUILD_ID.eq(guildId) - .and(ModerationActions.MODERATION_ACTIONS.AUTHOR_ID.eq(authorId))) - .orderBy(ModerationActions.MODERATION_ACTIONS.ISSUED_AT.asc()) - .stream() - .map(ActionRecord::of) - .toList()); + return getActionsFromGuildAscending(guildId, + ModerationActions.MODERATION_ACTIONS.AUTHOR_ID.eq(authorId)); } /** @@ -146,4 +132,14 @@ public int addAction(long guildId, long authorId, long targetId, return actionRecord.getCaseId(); }); } + + private @NotNull List getActionsFromGuildAscending(long guildId, + @NotNull Condition condition) { + return database.read(context -> context.selectFrom(ModerationActions.MODERATION_ACTIONS) + .where(ModerationActions.MODERATION_ACTIONS.GUILD_ID.eq(guildId).and(condition)) + .orderBy(ModerationActions.MODERATION_ACTIONS.ISSUED_AT.asc()) + .stream() + .map(ActionRecord::of) + .toList()); + } } From 834de32cb90f41234cbaa745b019a1b03bfe5e76 Mon Sep 17 00:00:00 2001 From: Zabuzard Date: Thu, 9 Dec 2021 10:26:41 +0100 Subject: [PATCH 6/6] added not-null checks for fail-fast --- .../tjbot/commands/moderation/BanCommand.java | 2 +- .../tjbot/commands/moderation/KickCommand.java | 2 +- .../commands/moderation/ModerationActionsStore.java | 10 +++++++++- .../tjbot/commands/moderation/UnbanCommand.java | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/BanCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/BanCommand.java index 82e6a4d205..5e48064647 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/BanCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/BanCommand.java @@ -61,7 +61,7 @@ public BanCommand(@NotNull ModerationActionsStore actionsStore) { hasRequiredRole = Pattern.compile(Config.getInstance().getHeavyModerationRolePattern()) .asMatchPredicate(); - this.actionsStore = actionsStore; + this.actionsStore = Objects.requireNonNull(actionsStore); } private static RestAction handleAlreadyBanned(@NotNull Guild.Ban ban, diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/KickCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/KickCommand.java index eb2ff8ac63..731e3ef83d 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/KickCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/KickCommand.java @@ -51,7 +51,7 @@ public KickCommand(@NotNull ModerationActionsStore actionsStore) { hasRequiredRole = Pattern.compile(Config.getInstance().getSoftModerationRolePattern()) .asMatchPredicate(); - this.actionsStore = actionsStore; + this.actionsStore = Objects.requireNonNull(actionsStore); } private static void handleAbsentTarget(@NotNull Interaction event) { diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationActionsStore.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationActionsStore.java index ff88dc5a28..27555e3097 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationActionsStore.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/ModerationActionsStore.java @@ -9,6 +9,7 @@ import java.time.Instant; import java.util.List; +import java.util.Objects; import java.util.Optional; /** @@ -36,7 +37,7 @@ public final class ModerationActionsStore { * @param database the database to write and retrieve actions from */ public ModerationActionsStore(@NotNull Database database) { - this.database = database; + this.database = Objects.requireNonNull(database); } /** @@ -50,6 +51,8 @@ public ModerationActionsStore(@NotNull Database database) { */ public @NotNull List getActionsByTypeAscending(long guildId, @NotNull ModerationUtils.Action actionType) { + Objects.requireNonNull(actionType); + return getActionsFromGuildAscending(guildId, ModerationActions.MODERATION_ACTIONS.ACTION_TYPE.eq(actionType.name())); } @@ -118,6 +121,9 @@ public ModerationActionsStore(@NotNull Database database) { public int addAction(long guildId, long authorId, long targetId, @NotNull ModerationUtils.Action actionType, @Nullable Instant actionExpiresAt, @NotNull String reason) { + Objects.requireNonNull(actionType); + Objects.requireNonNull(reason); + return database.writeAndProvide(context -> { ModerationActionsRecord actionRecord = context.newRecord(ModerationActions.MODERATION_ACTIONS) @@ -135,6 +141,8 @@ public int addAction(long guildId, long authorId, long targetId, private @NotNull List getActionsFromGuildAscending(long guildId, @NotNull Condition condition) { + Objects.requireNonNull(condition); + return database.read(context -> context.selectFrom(ModerationActions.MODERATION_ACTIONS) .where(ModerationActions.MODERATION_ACTIONS.GUILD_ID.eq(guildId).and(condition)) .orderBy(ModerationActions.MODERATION_ACTIONS.ISSUED_AT.asc()) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/UnbanCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/UnbanCommand.java index af71242672..b44daa3730 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/UnbanCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/UnbanCommand.java @@ -46,7 +46,7 @@ public UnbanCommand(@NotNull ModerationActionsStore actionsStore) { hasRequiredRole = Pattern.compile(Config.getInstance().getHeavyModerationRolePattern()) .asMatchPredicate(); - this.actionsStore = actionsStore; + this.actionsStore = Objects.requireNonNull(actionsStore); } private void unban(@NotNull User target, @NotNull Member author, @NotNull String reason,