From 39aadbf89462aec847fc87b5e665c32ba93a57fa Mon Sep 17 00:00:00 2001 From: d3vel0per Date: Thu, 27 Jan 2022 12:43:41 +0100 Subject: [PATCH 1/6] Added top-helpers command --- application/build.gradle | 2 + application/config.json.template | 5 +- .../togetherjava/tjbot/commands/Features.java | 4 + .../commands/tophelper/PresentationUtils.java | 65 +++++++++++++ .../commands/tophelper/TopHelpersCommand.java | 96 +++++++++++++++++++ .../tophelper/TopHelpersMessageListener.java | 65 +++++++++++++ .../commands/tophelper/package-info.java | 5 + .../org/togetherjava/tjbot/config/Config.java | 16 +++- .../db/V7__Add_Top_Helper_System.sql | 8 ++ 9 files changed, 262 insertions(+), 4 deletions(-) create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/tophelper/PresentationUtils.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersCommand.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersMessageListener.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/tophelper/package-info.java create mode 100644 application/src/main/resources/db/V7__Add_Top_Helper_System.sql diff --git a/application/build.gradle b/application/build.gradle index 44cd02255d..3ed6987a3f 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -59,6 +59,8 @@ dependencies { implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-csv:2.13.0' implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.0' + implementation 'com.github.freva:ascii-table:1.2.0' + implementation 'com.github.ben-manes.caffeine:caffeine:3.0.4' testImplementation 'org.mockito:mockito-core:4.0.0' diff --git a/application/config.json.template b/application/config.json.template index 363599f186..6ebd7715db 100644 --- a/application/config.json.template +++ b/application/config.json.template @@ -15,5 +15,6 @@ ] } - ] -} \ No newline at end of file + ], + "helpChannelPattern": "([a-zA-Z_]+_)?help(_\\d+)?" +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/Features.java b/application/src/main/java/org/togetherjava/tjbot/commands/Features.java index 6960cca19f..dbf26edce4 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/Features.java @@ -13,6 +13,8 @@ import org.togetherjava.tjbot.commands.tags.TagManageCommand; import org.togetherjava.tjbot.commands.tags.TagSystem; import org.togetherjava.tjbot.commands.tags.TagsCommand; +import org.togetherjava.tjbot.commands.tophelper.TopHelpersCommand; +import org.togetherjava.tjbot.commands.tophelper.TopHelpersMessageListener; import org.togetherjava.tjbot.db.Database; import org.togetherjava.tjbot.routines.ModAuditLogRoutine; @@ -55,6 +57,7 @@ public enum Features { features.add(new TemporaryModerationRoutine(jda, actionsStore)); // Message receivers + features.add(new TopHelpersMessageListener(database)); // Event receivers features.add(new RejoinMuteListener(actionsStore)); @@ -73,6 +76,7 @@ public enum Features { features.add(new AuditCommand(actionsStore)); features.add(new MuteCommand(actionsStore)); features.add(new UnmuteCommand(actionsStore)); + features.add(new TopHelpersCommand(database)); // Mixtures features.add(new FreeCommand()); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/PresentationUtils.java b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/PresentationUtils.java new file mode 100644 index 0000000000..aa055e240d --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/PresentationUtils.java @@ -0,0 +1,65 @@ +package org.togetherjava.tjbot.commands.tophelper; + +import com.github.freva.asciitable.AsciiTable; +import com.github.freva.asciitable.Column; +import com.github.freva.asciitable.ColumnData; +import com.github.freva.asciitable.HorizontalAlign; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Objects; +import java.util.stream.IntStream; + +/** + * Utility class for representation of data in various formats + */ +public final class PresentationUtils { + private PresentationUtils() {} + + /** + * Flattens a dataFrame to String representation of a table. + * + * eg. + * + *
+     * {
+     *     @code
+     *     var dataframe = List.of(List.of("Apple", "Fruit"), List.of("Potato", "Vegetable"));
+     *     var columnHeaders = new String[] {"Item", "Category"};
+     *     var horizontalAlignment =
+     *             new HorizontalAlign[] {HorizontalAlign.LEFT, HorizontalAlign.LEFT};
+     *     dataFrameToAsciiTable(dataframe, columnHeaders, horizontalAlignment);
+     * }
+     * 
+ * + * will return: + * + *
+     * {@code
+     *  Item   | Category
+     * --------+-----------
+     *  Apple  | Fruit
+     *  Potato | Vegetable
+     * }
+     * 
+ * + * @param dataFrame dataframe represented as List> where List represents a + * single row + * @param headers column headers for the table + * @param horizontalAligns column alignment for the table + * @return String representation of the dataFrame in tabular form + */ + public static String dataFrameToAsciiTable(@NotNull List> dataFrame, + @NotNull String[] headers, @NotNull HorizontalAlign[] horizontalAligns) { + Objects.requireNonNull(dataFrame, "DataFrame cannot be null"); + Objects.requireNonNull(headers, "Headers cannot be null"); + Objects.requireNonNull(horizontalAligns, "HorizontalAligns cannot be null"); + Character[] characters = AsciiTable.BASIC_ASCII_NO_DATA_SEPARATORS_NO_OUTSIDE_BORDER; + List>> columnData = IntStream.range(0, headers.length) + .mapToObj(i -> new Column().header(headers[i]) + .dataAlign(horizontalAligns[i]) + .>with(row -> row.get(i))) + .toList(); + return AsciiTable.getTable(characters, dataFrame, columnData); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersCommand.java new file mode 100644 index 0000000000..fee051460b --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersCommand.java @@ -0,0 +1,96 @@ +package org.togetherjava.tjbot.commands.tophelper; + +import com.github.freva.asciitable.HorizontalAlign; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; +import net.dv8tion.jda.api.interactions.Interaction; +import org.jetbrains.annotations.NotNull; +import org.jooq.Records; +import org.jooq.impl.DSL; +import org.togetherjava.tjbot.commands.SlashCommandAdapter; +import org.togetherjava.tjbot.commands.SlashCommandVisibility; +import org.togetherjava.tjbot.db.Database; +import org.togetherjava.tjbot.db.generated.tables.HelpChannelMessages; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Command to retrieve top helpers for last 30 days. + */ +public final class TopHelpersCommand extends SlashCommandAdapter { + private static final String COMMAND_NAME = "top-helpers"; + + public static final String PLAINTEXT_MESSAGE_TEMPLATE = "```\n%s\n```"; + private static final String COUNT_OPTION = "count"; + private static final String NO_ENTRIES = "No entries"; + + private static final int HELPER_LIMIT = 30; + + private record TopHelperRow(Integer serialId, Long userId, Long messageCount) { + } + + private final Database database; + + /** + * Initializes TopHelpers with a database. + * + * @param database the database to store the key-value pairs in + */ + public TopHelpersCommand(@NotNull Database database) { + super(COMMAND_NAME, "Lists top helpers for the last 30 days", SlashCommandVisibility.GUILD); + this.database = database; + } + + @Override + public void onSlashCommand(@NotNull SlashCommandEvent event) { + long guildId = event.getGuild().getIdLong(); + database.readAndConsume(context -> { + List records = context.with("TOPHELPERS") + .as(DSL + .select(HelpChannelMessages.HELP_CHANNEL_MESSAGES.AUTHOR_ID, + DSL.count().as("COUNT")) + .from(HelpChannelMessages.HELP_CHANNEL_MESSAGES) + .where(HelpChannelMessages.HELP_CHANNEL_MESSAGES.GUILD_ID.eq(guildId)) + .groupBy(HelpChannelMessages.HELP_CHANNEL_MESSAGES.AUTHOR_ID) + .orderBy(DSL.count().desc()) + .limit(HELPER_LIMIT)) + .select(DSL.rowNumber() + .over(DSL.orderBy(DSL.field(DSL.name("COUNT")).desc())) + .as("#"), DSL.field(DSL.name("AUTHOR_ID"), Long.class), + DSL.field(DSL.name("COUNT"), Long.class)) + .from(DSL.table(DSL.name("TOPHELPERS"))) + .fetch(Records.mapping(TopHelperRow::new)); + generateResponse(event, records); + }); + } + + private static @NotNull String prettyFormatOutput(@NotNull List> dataFrame) { + return String.format(PLAINTEXT_MESSAGE_TEMPLATE, + dataFrame.isEmpty() ? NO_ENTRIES + : PresentationUtils.dataFrameToAsciiTable(dataFrame, + new String[] {"#", "Name", "Message Count (in the last 30 days)"}, + new HorizontalAlign[] {HorizontalAlign.RIGHT, HorizontalAlign.LEFT, + HorizontalAlign.RIGHT})); + } + + private static void generateResponse(@NotNull Interaction event, + @NotNull Collection records) { + List userIds = records.stream().map(TopHelperRow::userId).toList(); + event.getGuild().retrieveMembersByIds(userIds).onSuccess(members -> { + Map activeUserIdToEffectiveNames = members.stream() + .collect(Collectors.toMap(Member::getIdLong, Member::getEffectiveName)); + List> topHelpersDataframe = records.stream() + .map(topHelperRow -> List.of(topHelperRow.serialId.toString(), + activeUserIdToEffectiveNames.getOrDefault(topHelperRow.userId, + // Any user who is no more a part of the guild is marked as + // [UNKNOWN] + "[UNKNOWN]"), + topHelperRow.messageCount.toString())) + .toList(); + event.reply(prettyFormatOutput(topHelpersDataframe)).queue(); + }); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersMessageListener.java b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersMessageListener.java new file mode 100644 index 0000000000..7ee78bd5d7 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersMessageListener.java @@ -0,0 +1,65 @@ +package org.togetherjava.tjbot.commands.tophelper; + +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.togetherjava.tjbot.commands.MessageReceiverAdapter; +import org.togetherjava.tjbot.config.Config; +import org.togetherjava.tjbot.db.Database; +import org.togetherjava.tjbot.db.generated.tables.HelpChannelMessages; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.regex.Pattern; + +/** + * Listener responsible for persistence of text message metadata. + */ +public final class TopHelpersMessageListener extends MessageReceiverAdapter { + private static final Logger logger = LoggerFactory.getLogger(TopHelpersMessageListener.class); + + private static final int MESSAGE_METADATA_ARCHIVAL_DAYS = 30; + + private final Database database; + + /** + * Creates a new message metadata listener, using the given database. + * + * @param database the database to store message metadata. + */ + public TopHelpersMessageListener(@NotNull Database database) { + super(Pattern.compile(Config.getInstance().getHelpChannelPattern())); + this.database = database; + } + + @Override + public void onMessageReceived(@NotNull GuildMessageReceivedEvent event) { + var channel = event.getChannel(); + if (!event.getAuthor().isBot() && !event.isWebhookMessage()) { + var messageId = event.getMessage().getIdLong(); + var guildId = event.getGuild().getIdLong(); + var channelId = channel.getIdLong(); + var userId = event.getAuthor().getIdLong(); + var createTimestamp = event.getMessage().getTimeCreated().toInstant(); + database.write(dsl -> { + dsl.newRecord(HelpChannelMessages.HELP_CHANNEL_MESSAGES) + .setMessageId(messageId) + .setGuildId(guildId) + .setChannelId(channelId) + .setAuthorId(userId) + .setSentAt(createTimestamp) + .insert(); + int noOfRowsDeleted = dsl.deleteFrom(HelpChannelMessages.HELP_CHANNEL_MESSAGES) + .where(HelpChannelMessages.HELP_CHANNEL_MESSAGES.SENT_AT + .le(Instant.now().minus(MESSAGE_METADATA_ARCHIVAL_DAYS, ChronoUnit.DAYS))) + .execute(); + if (noOfRowsDeleted > 0) { + logger.debug( + "{} old records have been deleted based on archival criteria of {} days.", + noOfRowsDeleted, MESSAGE_METADATA_ARCHIVAL_DAYS); + } + }); + } + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/package-info.java b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/package-info.java new file mode 100644 index 0000000000..61969abb57 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/package-info.java @@ -0,0 +1,5 @@ +/** + * This packages offers all the functionality for the top-helpers command system. The core class is + * {@link org.togetherjava.tjbot.commands.tophelper.TopHelpersCommand}. + */ +package org.togetherjava.tjbot.commands.tophelper; diff --git a/application/src/main/java/org/togetherjava/tjbot/config/Config.java b/application/src/main/java/org/togetherjava/tjbot/config/Config.java index 35c077dc13..470758b199 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/Config.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/Config.java @@ -32,8 +32,8 @@ public final class Config { private final String heavyModerationRolePattern; private final String softModerationRolePattern; private final String tagManageRolePattern; - private final List freeCommand; + private final String helpChannelPattern; @SuppressWarnings("ConstructorWithTooManyParameters") @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) @@ -46,7 +46,8 @@ private Config(@JsonProperty("token") String token, @JsonProperty("heavyModerationRolePattern") String heavyModerationRolePattern, @JsonProperty("softModerationRolePattern") String softModerationRolePattern, @JsonProperty("tagManageRolePattern") String tagManageRolePattern, - @JsonProperty("freeCommand") List freeCommand) { + @JsonProperty("freeCommand") List freeCommand, + @JsonProperty("helpChannelPattern") String helpChannelPattern) { this.token = token; this.databasePath = databasePath; this.projectWebsite = projectWebsite; @@ -57,6 +58,7 @@ private Config(@JsonProperty("token") String token, this.softModerationRolePattern = softModerationRolePattern; this.tagManageRolePattern = tagManageRolePattern; this.freeCommand = Collections.unmodifiableList(freeCommand); + this.helpChannelPattern = helpChannelPattern; } /** @@ -178,4 +180,14 @@ public String getTagManageRolePattern() { public @NotNull Collection getFreeCommandConfig() { return freeCommand; // already unmodifiable } + + /** + * Gets the REGEX pattern used to identify channels that are used for helping people with their + * questions. + * + * @return the channel name pattern + */ + public String getHelpChannelPattern() { + return helpChannelPattern; + } } diff --git a/application/src/main/resources/db/V7__Add_Top_Helper_System.sql b/application/src/main/resources/db/V7__Add_Top_Helper_System.sql new file mode 100644 index 0000000000..ab0b05b552 --- /dev/null +++ b/application/src/main/resources/db/V7__Add_Top_Helper_System.sql @@ -0,0 +1,8 @@ +CREATE TABLE help_channel_messages +( + message_id BIGINT NOT NULL PRIMARY KEY, + guild_id BIGINT NOT NULL, + channel_id BIGINT NOT NULL, + author_id BIGINT NOT NULL, + sent_at TIMESTAMP NOT NULL +) From 50dcd4814dde6923de0e9ab0179510df12afb02f Mon Sep 17 00:00:00 2001 From: Zabuzard Date: Thu, 27 Jan 2022 18:12:55 +0100 Subject: [PATCH 2/6] Improved top-helpers code * changed the layout of the methods * fixed style issues * fixed various design issues * streamlined the code more the logic itself merely changed --- .../commands/tophelper/PresentationUtils.java | 65 ------- .../commands/tophelper/TopHelpersCommand.java | 158 +++++++++++------- .../tophelper/TopHelpersMessageListener.java | 73 ++++---- 3 files changed, 143 insertions(+), 153 deletions(-) delete mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/tophelper/PresentationUtils.java diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/PresentationUtils.java b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/PresentationUtils.java deleted file mode 100644 index aa055e240d..0000000000 --- a/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/PresentationUtils.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.togetherjava.tjbot.commands.tophelper; - -import com.github.freva.asciitable.AsciiTable; -import com.github.freva.asciitable.Column; -import com.github.freva.asciitable.ColumnData; -import com.github.freva.asciitable.HorizontalAlign; -import org.jetbrains.annotations.NotNull; - -import java.util.List; -import java.util.Objects; -import java.util.stream.IntStream; - -/** - * Utility class for representation of data in various formats - */ -public final class PresentationUtils { - private PresentationUtils() {} - - /** - * Flattens a dataFrame to String representation of a table. - * - * eg. - * - *
-     * {
-     *     @code
-     *     var dataframe = List.of(List.of("Apple", "Fruit"), List.of("Potato", "Vegetable"));
-     *     var columnHeaders = new String[] {"Item", "Category"};
-     *     var horizontalAlignment =
-     *             new HorizontalAlign[] {HorizontalAlign.LEFT, HorizontalAlign.LEFT};
-     *     dataFrameToAsciiTable(dataframe, columnHeaders, horizontalAlignment);
-     * }
-     * 
- * - * will return: - * - *
-     * {@code
-     *  Item   | Category
-     * --------+-----------
-     *  Apple  | Fruit
-     *  Potato | Vegetable
-     * }
-     * 
- * - * @param dataFrame dataframe represented as List> where List represents a - * single row - * @param headers column headers for the table - * @param horizontalAligns column alignment for the table - * @return String representation of the dataFrame in tabular form - */ - public static String dataFrameToAsciiTable(@NotNull List> dataFrame, - @NotNull String[] headers, @NotNull HorizontalAlign[] horizontalAligns) { - Objects.requireNonNull(dataFrame, "DataFrame cannot be null"); - Objects.requireNonNull(headers, "Headers cannot be null"); - Objects.requireNonNull(horizontalAligns, "HorizontalAligns cannot be null"); - Character[] characters = AsciiTable.BASIC_ASCII_NO_DATA_SEPARATORS_NO_OUTSIDE_BORDER; - List>> columnData = IntStream.range(0, headers.length) - .mapToObj(i -> new Column().header(headers[i]) - .dataAlign(horizontalAligns[i]) - .>with(row -> row.get(i))) - .toList(); - return AsciiTable.getTable(characters, dataFrame, columnData); - } -} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersCommand.java index fee051460b..e8d53afb05 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersCommand.java @@ -1,43 +1,51 @@ package org.togetherjava.tjbot.commands.tophelper; +import com.github.freva.asciitable.AsciiTable; +import com.github.freva.asciitable.Column; +import com.github.freva.asciitable.ColumnData; import com.github.freva.asciitable.HorizontalAlign; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; import net.dv8tion.jda.api.interactions.Interaction; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.jooq.Records; import org.jooq.impl.DSL; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.SlashCommandAdapter; import org.togetherjava.tjbot.commands.SlashCommandVisibility; import org.togetherjava.tjbot.db.Database; -import org.togetherjava.tjbot.db.generated.tables.HelpChannelMessages; +import java.time.Instant; +import java.time.Period; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.function.Function; +import java.util.function.IntFunction; import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.togetherjava.tjbot.db.generated.tables.HelpChannelMessages.HELP_CHANNEL_MESSAGES; /** - * Command to retrieve top helpers for last 30 days. + * Command that displays the top helpers of a given time range. + * + * Top helpers are measured by their message count in help channels, as set by + * {@link TopHelpersMessageListener}. */ public final class TopHelpersCommand extends SlashCommandAdapter { + private static final Logger logger = LoggerFactory.getLogger(TopHelpersCommand.class); private static final String COMMAND_NAME = "top-helpers"; - - public static final String PLAINTEXT_MESSAGE_TEMPLATE = "```\n%s\n```"; - private static final String COUNT_OPTION = "count"; - private static final String NO_ENTRIES = "No entries"; - - private static final int HELPER_LIMIT = 30; - - private record TopHelperRow(Integer serialId, Long userId, Long messageCount) { - } + private static final int TOP_HELPER_LIMIT = 20; private final Database database; /** - * Initializes TopHelpers with a database. - * - * @param database the database to store the key-value pairs in + * Creates a new instance. + * + * @param database the database containing the message counts of top helpers */ public TopHelpersCommand(@NotNull Database database) { super(COMMAND_NAME, "Lists top helpers for the last 30 days", SlashCommandVisibility.GUILD); @@ -46,51 +54,89 @@ public TopHelpersCommand(@NotNull Database database) { @Override public void onSlashCommand(@NotNull SlashCommandEvent event) { - long guildId = event.getGuild().getIdLong(); - database.readAndConsume(context -> { - List records = context.with("TOPHELPERS") - .as(DSL - .select(HelpChannelMessages.HELP_CHANNEL_MESSAGES.AUTHOR_ID, - DSL.count().as("COUNT")) - .from(HelpChannelMessages.HELP_CHANNEL_MESSAGES) - .where(HelpChannelMessages.HELP_CHANNEL_MESSAGES.GUILD_ID.eq(guildId)) - .groupBy(HelpChannelMessages.HELP_CHANNEL_MESSAGES.AUTHOR_ID) - .orderBy(DSL.count().desc()) - .limit(HELPER_LIMIT)) - .select(DSL.rowNumber() - .over(DSL.orderBy(DSL.field(DSL.name("COUNT")).desc())) - .as("#"), DSL.field(DSL.name("AUTHOR_ID"), Long.class), - DSL.field(DSL.name("COUNT"), Long.class)) - .from(DSL.table(DSL.name("TOPHELPERS"))) - .fetch(Records.mapping(TopHelperRow::new)); - generateResponse(event, records); - }); + List topHelpers = + computeTopHelpersDescending(event.getGuild().getIdLong()); + + if (topHelpers.isEmpty()) { + event.reply("No entries for the selected time range.").queue(); + } + event.deferReply().queue(); + + List topHelperIds = topHelpers.stream().map(TopHelperResult::authorId).toList(); + event.getGuild() + .retrieveMembersByIds(topHelperIds) + .onError(error -> handleError(error, event)) + .onSuccess(members -> handleTopHelpers(topHelpers, members, event)); + } + + private @NotNull List computeTopHelpersDescending(long guildId) { + return database.read(context -> context.select(HELP_CHANNEL_MESSAGES.AUTHOR_ID, DSL.count()) + .from(HELP_CHANNEL_MESSAGES) + .where(HELP_CHANNEL_MESSAGES.GUILD_ID.eq(guildId) + .and(HELP_CHANNEL_MESSAGES.SENT_AT + .greaterOrEqual(Instant.now().minus(Period.ofDays(30))))) + .groupBy(HELP_CHANNEL_MESSAGES.AUTHOR_ID) + .orderBy(DSL.count().desc()) + .limit(TOP_HELPER_LIMIT) + .fetch(Records.mapping(TopHelperResult::new))); + } + + private static void handleError(@NotNull Throwable error, @NotNull Interaction event) { + logger.warn("Failed to compute top-helpers", error); + event.getHook().editOriginal("Sorry, something went wrong.").queue(); + } + + private static void handleTopHelpers(@NotNull Collection topHelpers, + @NotNull Collection members, @NotNull Interaction event) { + Map userIdToMember = + members.stream().collect(Collectors.toMap(Member::getIdLong, Function.identity())); + + List> topHelpersDataTable = topHelpers.stream() + .map(topHelper -> topHelperToDataRow(topHelper, + userIdToMember.get(topHelper.authorId()))) + .toList(); + + String message = "```java%n%s%n```".formatted(dataTableToString(topHelpersDataTable)); + + event.getHook().editOriginal(message).queue(); + } + + private static @NotNull List topHelperToDataRow(@NotNull TopHelperResult topHelper, + @Nullable Member member) { + String id = Long.toString(topHelper.authorId()); + String name = member == null ? "UNKNOWN_USER" : member.getEffectiveName(); + String messageCount = Integer.toString(topHelper.messageCount()); + + return List.of(id, name, messageCount); + } + + private static @NotNull String dataTableToString(@NotNull Collection> dataTable) { + return dataTableToAsciiTable(dataTable, + List.of(new ColumnSetting("Id", HorizontalAlign.RIGHT), + new ColumnSetting("Name", HorizontalAlign.RIGHT), + new ColumnSetting("Message count (30 days)", HorizontalAlign.RIGHT))); + } + + private static @NotNull String dataTableToAsciiTable( + @NotNull Collection> dataTable, + @NotNull List columnSettings) { + IntFunction headerToAlignment = i -> columnSettings.get(i).headerName(); + IntFunction indexToAlignment = i -> columnSettings.get(i).alignment(); + + IntFunction>> indexToColumn = + i -> new Column().header(headerToAlignment.apply(i)) + .dataAlign(indexToAlignment.apply(i)) + .with(row -> row.get(i)); + + List>> columns = + IntStream.range(0, columnSettings.size()).mapToObj(indexToColumn).toList(); + + return AsciiTable.getTable(AsciiTable.BASIC_ASCII_NO_DATA_SEPARATORS, dataTable, columns); } - private static @NotNull String prettyFormatOutput(@NotNull List> dataFrame) { - return String.format(PLAINTEXT_MESSAGE_TEMPLATE, - dataFrame.isEmpty() ? NO_ENTRIES - : PresentationUtils.dataFrameToAsciiTable(dataFrame, - new String[] {"#", "Name", "Message Count (in the last 30 days)"}, - new HorizontalAlign[] {HorizontalAlign.RIGHT, HorizontalAlign.LEFT, - HorizontalAlign.RIGHT})); + private record TopHelperResult(long authorId, int messageCount) { } - private static void generateResponse(@NotNull Interaction event, - @NotNull Collection records) { - List userIds = records.stream().map(TopHelperRow::userId).toList(); - event.getGuild().retrieveMembersByIds(userIds).onSuccess(members -> { - Map activeUserIdToEffectiveNames = members.stream() - .collect(Collectors.toMap(Member::getIdLong, Member::getEffectiveName)); - List> topHelpersDataframe = records.stream() - .map(topHelperRow -> List.of(topHelperRow.serialId.toString(), - activeUserIdToEffectiveNames.getOrDefault(topHelperRow.userId, - // Any user who is no more a part of the guild is marked as - // [UNKNOWN] - "[UNKNOWN]"), - topHelperRow.messageCount.toString())) - .toList(); - event.reply(prettyFormatOutput(topHelpersDataframe)).queue(); - }); + private record ColumnSetting(String headerName, HorizontalAlign alignment) { } } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersMessageListener.java b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersMessageListener.java index 7ee78bd5d7..4322c11206 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersMessageListener.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersMessageListener.java @@ -7,26 +7,30 @@ import org.togetherjava.tjbot.commands.MessageReceiverAdapter; import org.togetherjava.tjbot.config.Config; import org.togetherjava.tjbot.db.Database; -import org.togetherjava.tjbot.db.generated.tables.HelpChannelMessages; import java.time.Instant; -import java.time.temporal.ChronoUnit; +import java.time.Period; import java.util.regex.Pattern; +import static org.togetherjava.tjbot.db.generated.tables.HelpChannelMessages.HELP_CHANNEL_MESSAGES; + /** - * Listener responsible for persistence of text message metadata. + * Listener that receives all sent help messages and puts them into the database for + * {@link TopHelpersCommand} to pick them up. + * + * Also runs a cleanup routine to get rid of old entries. In general, it manages the database data + * to determine top-helpers. */ public final class TopHelpersMessageListener extends MessageReceiverAdapter { private static final Logger logger = LoggerFactory.getLogger(TopHelpersMessageListener.class); - - private static final int MESSAGE_METADATA_ARCHIVAL_DAYS = 30; + private static final Period DELETE_MESSAGE_RECORDS_AFTER = Period.ofDays(90); private final Database database; /** - * Creates a new message metadata listener, using the given database. + * Creates a new listener to receive all message sent in help channels. * - * @param database the database to store message metadata. + * @param database to store message meta-data in */ public TopHelpersMessageListener(@NotNull Database database) { super(Pattern.compile(Config.getInstance().getHelpChannelPattern())); @@ -35,31 +39,36 @@ public TopHelpersMessageListener(@NotNull Database database) { @Override public void onMessageReceived(@NotNull GuildMessageReceivedEvent event) { - var channel = event.getChannel(); - if (!event.getAuthor().isBot() && !event.isWebhookMessage()) { - var messageId = event.getMessage().getIdLong(); - var guildId = event.getGuild().getIdLong(); - var channelId = channel.getIdLong(); - var userId = event.getAuthor().getIdLong(); - var createTimestamp = event.getMessage().getTimeCreated().toInstant(); - database.write(dsl -> { - dsl.newRecord(HelpChannelMessages.HELP_CHANNEL_MESSAGES) - .setMessageId(messageId) - .setGuildId(guildId) - .setChannelId(channelId) - .setAuthorId(userId) - .setSentAt(createTimestamp) - .insert(); - int noOfRowsDeleted = dsl.deleteFrom(HelpChannelMessages.HELP_CHANNEL_MESSAGES) - .where(HelpChannelMessages.HELP_CHANNEL_MESSAGES.SENT_AT - .le(Instant.now().minus(MESSAGE_METADATA_ARCHIVAL_DAYS, ChronoUnit.DAYS))) - .execute(); - if (noOfRowsDeleted > 0) { - logger.debug( - "{} old records have been deleted based on archival criteria of {} days.", - noOfRowsDeleted, MESSAGE_METADATA_ARCHIVAL_DAYS); - } - }); + if (event.getAuthor().isBot() || event.isWebhookMessage()) { + return; + } + + addMessageRecord(event); + // TODO Use a routine that runs every 4 hours for the deletion instead + deleteOldMessageRecords(); + } + + private void addMessageRecord(@NotNull GuildMessageReceivedEvent event) { + database.write(context -> context.newRecord(HELP_CHANNEL_MESSAGES) + .setMessageId(event.getMessage().getIdLong()) + .setGuildId(event.getGuild().getIdLong()) + .setChannelId(event.getChannel().getIdLong()) + .setAuthorId(event.getAuthor().getIdLong()) + .setSentAt(event.getMessage().getTimeCreated().toInstant()) + .insert()); + } + + private void deleteOldMessageRecords() { + int recordsDeleted = + database.writeAndProvide(context -> context.deleteFrom(HELP_CHANNEL_MESSAGES) + .where(HELP_CHANNEL_MESSAGES.SENT_AT + .lessOrEqual(Instant.now().minus(DELETE_MESSAGE_RECORDS_AFTER))) + .execute()); + + if (recordsDeleted > 0) { + logger.debug( + "{} old help message records have been deleted because they are older than {}.", + recordsDeleted, DELETE_MESSAGE_RECORDS_AFTER); } } } From 8d759d1d85a44dc24529ec3991f49eed9919779b Mon Sep 17 00:00:00 2001 From: Zabuzard Date: Thu, 27 Jan 2022 18:21:36 +0100 Subject: [PATCH 3/6] Require soft mod role for top-helpers --- .../commands/tophelper/TopHelpersCommand.java | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersCommand.java index e8d53afb05..008da6da06 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersCommand.java @@ -5,6 +5,7 @@ import com.github.freva.asciitable.ColumnData; import com.github.freva.asciitable.HorizontalAlign; import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Role; import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; import net.dv8tion.jda.api.interactions.Interaction; import org.jetbrains.annotations.NotNull; @@ -15,6 +16,7 @@ import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.SlashCommandAdapter; import org.togetherjava.tjbot.commands.SlashCommandVisibility; +import org.togetherjava.tjbot.config.Config; import org.togetherjava.tjbot.db.Database; import java.time.Instant; @@ -24,6 +26,8 @@ import java.util.Map; import java.util.function.Function; import java.util.function.IntFunction; +import java.util.function.Predicate; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -31,7 +35,7 @@ /** * Command that displays the top helpers of a given time range. - * + *

* Top helpers are measured by their message count in help channels, as set by * {@link TopHelpersMessageListener}. */ @@ -41,19 +45,27 @@ public final class TopHelpersCommand extends SlashCommandAdapter { private static final int TOP_HELPER_LIMIT = 20; private final Database database; + private final Predicate hasRequiredRole; /** * Creates a new instance. - * + * * @param database the database containing the message counts of top helpers */ public TopHelpersCommand(@NotNull Database database) { super(COMMAND_NAME, "Lists top helpers for the last 30 days", SlashCommandVisibility.GUILD); this.database = database; + + hasRequiredRole = Pattern.compile(Config.getInstance().getSoftModerationRolePattern()) + .asMatchPredicate(); } @Override public void onSlashCommand(@NotNull SlashCommandEvent event) { + if (!handleHasAuthorRole(event.getMember(), event)) { + return; + } + List topHelpers = computeTopHelpersDescending(event.getGuild().getIdLong()); @@ -69,6 +81,17 @@ public void onSlashCommand(@NotNull SlashCommandEvent event) { .onSuccess(members -> handleTopHelpers(topHelpers, members, event)); } + @SuppressWarnings("BooleanMethodNameMustStartWithQuestion") + private boolean handleHasAuthorRole(@NotNull Member author, @NotNull Interaction event) { + if (author.getRoles().stream().map(Role::getName).anyMatch(hasRequiredRole)) { + return true; + } + event.reply("You can not compute the top-helpers since you do not have the required role.") + .setEphemeral(true) + .queue(); + return false; + } + private @NotNull List computeTopHelpersDescending(long guildId) { return database.read(context -> context.select(HELP_CHANNEL_MESSAGES.AUTHOR_ID, DSL.count()) .from(HELP_CHANNEL_MESSAGES) From 9caad4816ec56a432919ef0ab613bb5e2ad1209c Mon Sep 17 00:00:00 2001 From: Zabuzard Date: Thu, 27 Jan 2022 18:42:15 +0100 Subject: [PATCH 4/6] Bugfix for time ranges with no data --- .../togetherjava/tjbot/commands/tophelper/TopHelpersCommand.java | 1 + 1 file changed, 1 insertion(+) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersCommand.java index 008da6da06..6e11101158 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersCommand.java @@ -71,6 +71,7 @@ public void onSlashCommand(@NotNull SlashCommandEvent event) { if (topHelpers.isEmpty()) { event.reply("No entries for the selected time range.").queue(); + return; } event.deferReply().queue(); From 05265409cb2daf087c84d5587c200aa7547e2ae1 Mon Sep 17 00:00:00 2001 From: Zabuzard Date: Thu, 27 Jan 2022 19:05:31 +0100 Subject: [PATCH 5/6] Use previous month as default time range * with proper java.time computation instead of just "last 30 days" --- .../commands/tophelper/TopHelpersCommand.java | 56 ++++++++++++++----- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersCommand.java index 6e11101158..876ce3dcb2 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersCommand.java @@ -20,9 +20,13 @@ import org.togetherjava.tjbot.db.Database; import java.time.Instant; -import java.time.Period; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.TextStyle; +import java.time.temporal.TemporalAdjusters; import java.util.Collection; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.function.Function; import java.util.function.IntFunction; @@ -53,11 +57,11 @@ public final class TopHelpersCommand extends SlashCommandAdapter { * @param database the database containing the message counts of top helpers */ public TopHelpersCommand(@NotNull Database database) { - super(COMMAND_NAME, "Lists top helpers for the last 30 days", SlashCommandVisibility.GUILD); - this.database = database; - + super(COMMAND_NAME, "Lists top helpers for the last month", SlashCommandVisibility.GUILD); + // TODO Add options to optionally pick a time range once JDA/Discord offers a date-picker hasRequiredRole = Pattern.compile(Config.getInstance().getSoftModerationRolePattern()) .asMatchPredicate(); + this.database = database; } @Override @@ -66,11 +70,15 @@ public void onSlashCommand(@NotNull SlashCommandEvent event) { return; } + TimeRange timeRange = computeDefaultTimeRange(); List topHelpers = - computeTopHelpersDescending(event.getGuild().getIdLong()); + computeTopHelpersDescending(event.getGuild().getIdLong(), timeRange); if (topHelpers.isEmpty()) { - event.reply("No entries for the selected time range.").queue(); + event + .reply("No entries for the selected time range (%s)." + .formatted(timeRange.description())) + .queue(); return; } event.deferReply().queue(); @@ -79,7 +87,7 @@ public void onSlashCommand(@NotNull SlashCommandEvent event) { event.getGuild() .retrieveMembersByIds(topHelperIds) .onError(error -> handleError(error, event)) - .onSuccess(members -> handleTopHelpers(topHelpers, members, event)); + .onSuccess(members -> handleTopHelpers(topHelpers, members, timeRange, event)); } @SuppressWarnings("BooleanMethodNameMustStartWithQuestion") @@ -93,12 +101,24 @@ private boolean handleHasAuthorRole(@NotNull Member author, @NotNull Interaction return false; } - private @NotNull List computeTopHelpersDescending(long guildId) { + private static @NotNull TimeRange computeDefaultTimeRange() { + // Last month + ZonedDateTime start = Instant.now() + .atZone(ZoneOffset.UTC) + .minusMonths(1) + .with(TemporalAdjusters.firstDayOfMonth()); + ZonedDateTime end = start.with(TemporalAdjusters.lastDayOfMonth()); + String description = start.getMonth().getDisplayName(TextStyle.FULL_STANDALONE, Locale.US); + + return new TimeRange(start.toInstant(), end.toInstant(), description); + } + + private @NotNull List computeTopHelpersDescending(long guildId, + @NotNull TimeRange timeRange) { return database.read(context -> context.select(HELP_CHANNEL_MESSAGES.AUTHOR_ID, DSL.count()) .from(HELP_CHANNEL_MESSAGES) .where(HELP_CHANNEL_MESSAGES.GUILD_ID.eq(guildId) - .and(HELP_CHANNEL_MESSAGES.SENT_AT - .greaterOrEqual(Instant.now().minus(Period.ofDays(30))))) + .and(HELP_CHANNEL_MESSAGES.SENT_AT.between(timeRange.start(), timeRange.end()))) .groupBy(HELP_CHANNEL_MESSAGES.AUTHOR_ID) .orderBy(DSL.count().desc()) .limit(TOP_HELPER_LIMIT) @@ -111,7 +131,8 @@ private static void handleError(@NotNull Throwable error, @NotNull Interaction e } private static void handleTopHelpers(@NotNull Collection topHelpers, - @NotNull Collection members, @NotNull Interaction event) { + @NotNull Collection members, @NotNull TimeRange timeRange, + @NotNull Interaction event) { Map userIdToMember = members.stream().collect(Collectors.toMap(Member::getIdLong, Function.identity())); @@ -120,7 +141,8 @@ private static void handleTopHelpers(@NotNull Collection topHel userIdToMember.get(topHelper.authorId()))) .toList(); - String message = "```java%n%s%n```".formatted(dataTableToString(topHelpersDataTable)); + String message = + "```java%n%s%n```".formatted(dataTableToString(topHelpersDataTable, timeRange)); event.getHook().editOriginal(message).queue(); } @@ -134,11 +156,14 @@ private static void handleTopHelpers(@NotNull Collection topHel return List.of(id, name, messageCount); } - private static @NotNull String dataTableToString(@NotNull Collection> dataTable) { + private static @NotNull String dataTableToString(@NotNull Collection> dataTable, + @NotNull TimeRange timeRange) { return dataTableToAsciiTable(dataTable, List.of(new ColumnSetting("Id", HorizontalAlign.RIGHT), new ColumnSetting("Name", HorizontalAlign.RIGHT), - new ColumnSetting("Message count (30 days)", HorizontalAlign.RIGHT))); + new ColumnSetting( + "Message count (for %s)".formatted(timeRange.description()), + HorizontalAlign.RIGHT))); } private static @NotNull String dataTableToAsciiTable( @@ -158,6 +183,9 @@ private static void handleTopHelpers(@NotNull Collection topHel return AsciiTable.getTable(AsciiTable.BASIC_ASCII_NO_DATA_SEPARATORS, dataTable, columns); } + private record TimeRange(Instant start, Instant end, String description) { + } + private record TopHelperResult(long authorId, int messageCount) { } From 1241d05b935cd1205bdaea90f39ec0c70d2dc239 Mon Sep 17 00:00:00 2001 From: Zabuzard Date: Wed, 2 Feb 2022 12:32:14 +0100 Subject: [PATCH 6/6] Moved message purging into routine --- .../togetherjava/tjbot/commands/Features.java | 2 + .../tophelper/TopHelpersMessageListener.java | 26 --------- .../TopHelpersPurgeMessagesRoutine.java | 54 +++++++++++++++++++ 3 files changed, 56 insertions(+), 26 deletions(-) create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersPurgeMessagesRoutine.java diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/Features.java b/application/src/main/java/org/togetherjava/tjbot/commands/Features.java index dbf26edce4..393172ed99 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/Features.java @@ -15,6 +15,7 @@ import org.togetherjava.tjbot.commands.tags.TagsCommand; import org.togetherjava.tjbot.commands.tophelper.TopHelpersCommand; import org.togetherjava.tjbot.commands.tophelper.TopHelpersMessageListener; +import org.togetherjava.tjbot.commands.tophelper.TopHelpersPurgeMessagesRoutine; import org.togetherjava.tjbot.db.Database; import org.togetherjava.tjbot.routines.ModAuditLogRoutine; @@ -55,6 +56,7 @@ public enum Features { // Routines features.add(new ModAuditLogRoutine(database)); features.add(new TemporaryModerationRoutine(jda, actionsStore)); + features.add(new TopHelpersPurgeMessagesRoutine(database)); // Message receivers features.add(new TopHelpersMessageListener(database)); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersMessageListener.java b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersMessageListener.java index 4322c11206..fadb5b50ff 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersMessageListener.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersMessageListener.java @@ -2,14 +2,10 @@ import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent; import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.MessageReceiverAdapter; import org.togetherjava.tjbot.config.Config; import org.togetherjava.tjbot.db.Database; -import java.time.Instant; -import java.time.Period; import java.util.regex.Pattern; import static org.togetherjava.tjbot.db.generated.tables.HelpChannelMessages.HELP_CHANNEL_MESSAGES; @@ -17,14 +13,8 @@ /** * Listener that receives all sent help messages and puts them into the database for * {@link TopHelpersCommand} to pick them up. - * - * Also runs a cleanup routine to get rid of old entries. In general, it manages the database data - * to determine top-helpers. */ public final class TopHelpersMessageListener extends MessageReceiverAdapter { - private static final Logger logger = LoggerFactory.getLogger(TopHelpersMessageListener.class); - private static final Period DELETE_MESSAGE_RECORDS_AFTER = Period.ofDays(90); - private final Database database; /** @@ -44,8 +34,6 @@ public void onMessageReceived(@NotNull GuildMessageReceivedEvent event) { } addMessageRecord(event); - // TODO Use a routine that runs every 4 hours for the deletion instead - deleteOldMessageRecords(); } private void addMessageRecord(@NotNull GuildMessageReceivedEvent event) { @@ -57,18 +45,4 @@ private void addMessageRecord(@NotNull GuildMessageReceivedEvent event) { .setSentAt(event.getMessage().getTimeCreated().toInstant()) .insert()); } - - private void deleteOldMessageRecords() { - int recordsDeleted = - database.writeAndProvide(context -> context.deleteFrom(HELP_CHANNEL_MESSAGES) - .where(HELP_CHANNEL_MESSAGES.SENT_AT - .lessOrEqual(Instant.now().minus(DELETE_MESSAGE_RECORDS_AFTER))) - .execute()); - - if (recordsDeleted > 0) { - logger.debug( - "{} old help message records have been deleted because they are older than {}.", - recordsDeleted, DELETE_MESSAGE_RECORDS_AFTER); - } - } } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersPurgeMessagesRoutine.java b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersPurgeMessagesRoutine.java new file mode 100644 index 0000000000..527c908f1d --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersPurgeMessagesRoutine.java @@ -0,0 +1,54 @@ +package org.togetherjava.tjbot.commands.tophelper; + +import net.dv8tion.jda.api.JDA; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.togetherjava.tjbot.commands.Routine; +import org.togetherjava.tjbot.db.Database; + +import java.time.Instant; +import java.time.Period; +import java.util.concurrent.TimeUnit; + +import static org.togetherjava.tjbot.db.generated.tables.HelpChannelMessages.HELP_CHANNEL_MESSAGES; + +/** + * Cleanup routine to get rid of old database top-helper message entries. + */ +public final class TopHelpersPurgeMessagesRoutine implements Routine { + private static final Logger logger = + LoggerFactory.getLogger(TopHelpersPurgeMessagesRoutine.class); + private static final Period DELETE_MESSAGE_RECORDS_AFTER = Period.ofDays(90); + + private final Database database; + + /** + * Creates a new cleanup routine. + * + * @param database the database that contains the messages to purge + */ + public TopHelpersPurgeMessagesRoutine(@NotNull Database database) { + this.database = database; + } + + @Override + public @NotNull Schedule createSchedule() { + return new Schedule(ScheduleMode.FIXED_RATE, 0, 4, TimeUnit.HOURS); + } + + @Override + public void runRoutine(@NotNull JDA jda) { + int recordsDeleted = + database.writeAndProvide(context -> context.deleteFrom(HELP_CHANNEL_MESSAGES) + .where(HELP_CHANNEL_MESSAGES.SENT_AT + .lessOrEqual(Instant.now().minus(DELETE_MESSAGE_RECORDS_AFTER))) + .execute()); + + if (recordsDeleted > 0) { + logger.debug( + "{} old help message records have been deleted because they are older than {}.", + recordsDeleted, DELETE_MESSAGE_RECORDS_AFTER); + } + } +}