From 3ab9d9edd97a6a84a71c1a08076ec2e210fdd0ac Mon Sep 17 00:00:00 2001 From: Tanish Azad <73871477+Taz03@users.noreply.github.com> Date: Wed, 3 Aug 2022 14:09:29 +0530 Subject: [PATCH 01/25] Audit rework (#472) - Audit rework * New UI for Audit command * Unlimited number of audit displayable (almost) * Added next and previous buttons --- .../commands/moderation/AuditCommand.java | 179 +++++++++++++----- 1 file changed, 130 insertions(+), 49 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/AuditCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/AuditCommand.java index edd7c0df43..6097286232 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/AuditCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/AuditCommand.java @@ -2,21 +2,28 @@ import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.MessageBuilder; import net.dv8tion.jda.api.entities.*; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback; import net.dv8tion.jda.api.interactions.commands.OptionMapping; import net.dv8tion.jda.api.interactions.commands.OptionType; -import net.dv8tion.jda.api.requests.RestAction; +import net.dv8tion.jda.api.interactions.components.ActionRow; +import net.dv8tion.jda.api.interactions.components.buttons.Button; import net.dv8tion.jda.api.utils.TimeUtil; + import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; + import org.togetherjava.tjbot.commands.SlashCommandAdapter; import org.togetherjava.tjbot.commands.SlashCommandVisibility; import org.togetherjava.tjbot.config.Config; +import java.time.Instant; import java.time.ZoneOffset; import java.util.*; +import java.util.function.Function; import java.util.function.Predicate; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -32,12 +39,15 @@ public final class AuditCommand extends SlashCommandAdapter { private static final String TARGET_OPTION = "user"; private static final String COMMAND_NAME = "audit"; private static final String ACTION_VERB = "audit"; + private static final int MAX_PAGE_LENGTH = 25; + private static final String PREVIOUS_BUTTON_LABEL = "⬅"; + private static final String NEXT_BUTTON_LABEL = "➡"; private final Predicate hasRequiredRole; private final ModerationActionsStore actionsStore; /** * Constructs an instance. - * + * * @param actionsStore used to store actions issued by this command * @param config the config to use for this */ @@ -53,27 +63,29 @@ public AuditCommand(@NotNull ModerationActionsStore actionsStore, @NotNull Confi this.actionsStore = Objects.requireNonNull(actionsStore); } - private static @NotNull MessageEmbed createSummaryMessage(@NotNull User user, + private static @NotNull EmbedBuilder createSummaryEmbed(@NotNull User user, @NotNull Collection actions) { return new EmbedBuilder().setTitle("Audit log of **%s**".formatted(user.getAsTag())) .setAuthor(user.getName(), null, user.getAvatarUrl()) .setDescription(createSummaryMessageDescription(actions)) - .setColor(ModerationUtils.AMBIENT_COLOR) - .build(); + .setColor(ModerationUtils.AMBIENT_COLOR); } private static @NotNull String createSummaryMessageDescription( @NotNull Collection actions) { int actionAmount = actions.size(); + + String shortSummary = "There are **%s actions** against the user." + .formatted(actionAmount == 0 ? "no" : actionAmount); + if (actionAmount == 0) { - return "There are **no actions** against the user."; + return shortSummary; } - String shortSummary = "There are **%d actions** against the user.".formatted(actionAmount); - // Summary of all actions with their count, like "- Warn: 5", descending Map actionTypeToCount = actions.stream() .collect(Collectors.groupingBy(ActionRecord::actionType, Collectors.counting())); + String typeCountSummary = actionTypeToCount.entrySet() .stream() .filter(typeAndCount -> typeAndCount.getValue() > 0) @@ -85,30 +97,27 @@ public AuditCommand(@NotNull ModerationActionsStore actionsStore, @NotNull Confi return shortSummary + "\n" + typeCountSummary; } - private static @NotNull RestAction actionToMessage(@NotNull ActionRecord action, + private static @NotNull MessageEmbed.Field actionToField(@NotNull ActionRecord action, @NotNull JDA jda) { - String footer = action.actionExpiresAt() == null ? null - : "Temporary action, expires at %s".formatted(TimeUtil - .getDateTimeString(action.actionExpiresAt().atOffset(ZoneOffset.UTC))); - - return jda.retrieveUserById(action.authorId()) - .onErrorMap(error -> null) - .map(author -> new EmbedBuilder().setTitle(action.actionType().name()) - .setAuthor(author == null ? "(unknown user)" : author.getAsTag(), null, - author == null ? null : author.getAvatarUrl()) - .setDescription(action.reason()) - .setTimestamp(action.issuedAt()) - .setFooter(footer) - .setColor(ModerationUtils.AMBIENT_COLOR) - .build()); - } - - private static @NotNull List prependElement(@NotNull E element, - @NotNull Collection elements) { - List allElements = new ArrayList<>(elements.size() + 1); - allElements.add(element); - allElements.addAll(elements); - return allElements; + Function formatTime = instant -> { + if (instant == null) { + return ""; + } + return TimeUtil.getDateTimeString(instant.atOffset(ZoneOffset.UTC)); + }; + + User author = jda.getUserById(action.authorId()); + + Instant expiresAt = action.actionExpiresAt(); + String expiresAtFormatted = expiresAt == null ? "" + : "\nTemporary action, expires at: " + formatTime.apply(expiresAt); + + return new MessageEmbed.Field( + action.actionType().name() + " by " + + (author == null ? "(unknown user)" : author.getAsTag()), + action.reason() + "\nIssued at: " + formatTime.apply(action.issuedAt()) + + expiresAtFormatted, + false); } @Override @@ -121,16 +130,19 @@ public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { Guild guild = Objects.requireNonNull(event.getGuild()); Member bot = guild.getSelfMember(); - if (!handleChecks(bot, author, targetOption.getAsMember(), guild, event)) { + if (!handleChecks(bot, author, targetOption.getAsMember(), event)) { return; } - auditUser(target, guild, event); + event + .reply(auditUser(guild.getIdLong(), target.getIdLong(), event.getMember().getIdLong(), + 1, event.getJDA())) + .queue(); } @SuppressWarnings("BooleanMethodNameMustStartWithQuestion") private boolean handleChecks(@NotNull Member bot, @NotNull Member author, - @Nullable Member target, @NotNull Guild guild, @NotNull IReplyCallback event) { + @Nullable Member target, @NotNull IReplyCallback event) { // Member doesn't exist if attempting to audit a user who is not part of the guild. if (target != null && !ModerationUtils.handleCanInteractWithTarget(ACTION_VERB, bot, author, target, event)) { @@ -139,26 +151,95 @@ private boolean handleChecks(@NotNull Member bot, @NotNull Member author, return ModerationUtils.handleHasAuthorRole(ACTION_VERB, hasRequiredRole, author, event); } - private void auditUser(@NotNull User user, @NotNull ISnowflake guild, - @NotNull IReplyCallback event) { - List actions = - actionsStore.getActionsByTargetAscending(guild.getIdLong(), user.getIdLong()); + private @NotNull List> groupActionsByPages( + @NotNull List actions) { + List> groupedActions = new ArrayList<>(); + for (int i = 0; i < actions.size(); i++) { + if (i % AuditCommand.MAX_PAGE_LENGTH == 0) { + groupedActions.add(new ArrayList<>(AuditCommand.MAX_PAGE_LENGTH)); + } + + groupedActions.get(groupedActions.size() - 1).add(actions.get(i)); + } + + return groupedActions; + } + + /** + * @param pageNumber page number to display when actions are divided into pages and each page + * can contain {@link AuditCommand#MAX_PAGE_LENGTH} actions + */ + private @NotNull Message auditUser(long guildId, long targetId, long callerId, int pageNumber, + @NotNull JDA jda) { + List actions = actionsStore.getActionsByTargetAscending(guildId, targetId); + List> groupedActions = groupActionsByPages(actions); + int totalPages = groupedActions.size(); + + // Handles the case of too low page number and too high page number + pageNumber = Math.max(1, pageNumber); + pageNumber = Math.min(totalPages, pageNumber); + + EmbedBuilder audit = createSummaryEmbed(jda.retrieveUserById(targetId).complete(), actions); + + if (groupedActions.isEmpty()) { + return new MessageBuilder(audit.build()).build(); + } + + groupedActions.get(pageNumber - 1) + .forEach(action -> audit.addField(actionToField(action, jda))); + + return new MessageBuilder(audit.setFooter("Page: " + pageNumber + "/" + totalPages).build()) + .setActionRows(makeActionRow(guildId, targetId, callerId, pageNumber, totalPages)) + .build(); + } + + private @NotNull ActionRow makeActionRow(long guildId, long targetId, long callerId, + int pageNumber, int totalPages) { + int previousButtonTurnPageBy = -1; + Button previousButton = createPageTurnButton(PREVIOUS_BUTTON_LABEL, guildId, targetId, + callerId, pageNumber, previousButtonTurnPageBy); + if (pageNumber == 1) { + previousButton = previousButton.asDisabled(); + } + + int nextButtonTurnPageBy = 1; + Button nextButton = createPageTurnButton(NEXT_BUTTON_LABEL, guildId, targetId, callerId, + pageNumber, nextButtonTurnPageBy); + if (pageNumber == totalPages) { + nextButton = nextButton.asDisabled(); + } + + return ActionRow.of(previousButton, nextButton); + } + + private @NotNull Button createPageTurnButton(@NotNull String label, long guildId, long targetId, + long callerId, long pageNumber, int turnPageBy) { + return Button.primary(generateComponentId(String.valueOf(guildId), String.valueOf(targetId), + String.valueOf(callerId), String.valueOf(pageNumber), String.valueOf(turnPageBy)), + label); + } + + @Override + public void onButtonClick(@NotNull ButtonInteractionEvent event, @NotNull List args) { + long callerId = Long.parseLong(args.get(2)); + long interactorId = event.getMember().getIdLong(); + + if (callerId != interactorId) { + event.reply("Only the user who triggered the command can use these buttons.") + .setEphemeral(true) + .queue(); - MessageEmbed summary = createSummaryMessage(user, actions); - if (actions.isEmpty()) { - event.replyEmbeds(summary).queue(); return; } - // Computing messages for actual actions is done deferred and might require asking the - // Discord API - event.deferReply().queue(); - JDA jda = event.getJDA(); + int currentPage = Integer.parseInt(args.get(3)); + int turnPageBy = Integer.parseInt(args.get(4)); + + long guildId = Long.parseLong(args.get(0)); + long targetId = Long.parseLong(args.get(1)); + int pageToDisplay = currentPage + turnPageBy; - RestAction> messagesTask = RestAction - .allOf(actions.stream().map(action -> actionToMessage(action, jda)).toList()); - messagesTask.map(messages -> prependElement(summary, messages)) - .flatMap(messages -> event.getHook().sendMessageEmbeds(messages)) + event.editMessage(auditUser(guildId, targetId, interactorId, pageToDisplay, event.getJDA())) .queue(); } } From d58506f08568ac33108500f1e3bc40da7ecf6705 Mon Sep 17 00:00:00 2001 From: Alex Liu Date: Fri, 5 Aug 2022 00:36:35 -0700 Subject: [PATCH 02/25] Updated map of VC activities (#473) * Updated map of VC activities * Ran spotlessApply to fix Map spacing * Removed non-public VC activities * Updated comment on list source for activities --- .../commands/basic/VcActivityCommand.java | 74 +++++++++++++------ 1 file changed, 50 insertions(+), 24 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/basic/VcActivityCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/basic/VcActivityCommand.java index 04819cd751..7e6899684f 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/basic/VcActivityCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/basic/VcActivityCommand.java @@ -50,34 +50,60 @@ public final class VcActivityCommand extends SlashCommandAdapter { private static final long MAX_AGE_DAYS_LIMIT = 7; private static final long MAX_USES_LIMIT = 100; - public static final String POKER_NAME = "Poker"; - public static final String BETRAYAL_IO_NAME = "Betrayal.io"; - public static final String FISHINGTON_IO_NAME = "Fishington.io"; - public static final String SPELLCAST_NAME = "Spellcast"; - public static final String DOODLECREW_NAME = "Doodlecrew"; - public static final String WORDSNACK_NAME = "Wordsnack"; - public static final String LETTERTILE_NAME = "Lettertile"; - - - private static final List VC_APPLICATIONS = - List.of(new Command.Choice(POKER_NAME, POKER_NAME), - new Command.Choice(BETRAYAL_IO_NAME, BETRAYAL_IO_NAME), - new Command.Choice(FISHINGTON_IO_NAME, FISHINGTON_IO_NAME), - new Command.Choice(SPELLCAST_NAME, SPELLCAST_NAME), - new Command.Choice(DOODLECREW_NAME, DOODLECREW_NAME), - new Command.Choice(WORDSNACK_NAME, WORDSNACK_NAME), - new Command.Choice(LETTERTILE_NAME, LETTERTILE_NAME)); + public static final String WATCH_TOGETHER_NAME = "Watch Together"; + public static final String POKER_NAME = "Poker Night"; + public static final String CHESS_NAME = "Chess In The Park"; + public static final String SPELLCAST_NAME = "SpellCast"; + public static final String DOODLE_CREW_NAME = "Doodle Crew"; + public static final String WORD_SNACKS_NAME = "Word Snacks"; + public static final String LETTER_LEAGUE_NAME = "Letter League"; + public static final String CHECKERS_NAME = "Checkers In The Park"; + public static final String BLAZING_EIGHTS_NAME = "Blazing 8s"; + public static final String SKETCH_HEADS_NAME = "Sketch Heads"; + public static final String PUTT_PARTY_NAME = "Putt Party"; + public static final String LAND_IO_NAME = "Land-io"; + public static final String BOBBLE_LEAGUE_NAME = "Bobble League"; + public static final String ASK_AWAY_NAME = "Ask Away"; + public static final String KNOW_WHAT_I_MEME_NAME = "Know What I Meme"; + + private static final List VC_APPLICATIONS = List.of( + new Command.Choice(WATCH_TOGETHER_NAME, WATCH_TOGETHER_NAME), + new Command.Choice(POKER_NAME, POKER_NAME), new Command.Choice(CHESS_NAME, CHESS_NAME), + new Command.Choice(SPELLCAST_NAME, SPELLCAST_NAME), + new Command.Choice(DOODLE_CREW_NAME, DOODLE_CREW_NAME), + new Command.Choice(WORD_SNACKS_NAME, WORD_SNACKS_NAME), + new Command.Choice(LETTER_LEAGUE_NAME, LETTER_LEAGUE_NAME), + new Command.Choice(CHECKERS_NAME, CHECKERS_NAME), + new Command.Choice(BLAZING_EIGHTS_NAME, BLAZING_EIGHTS_NAME), + new Command.Choice(SKETCH_HEADS_NAME, SKETCH_HEADS_NAME), + new Command.Choice(PUTT_PARTY_NAME, PUTT_PARTY_NAME), + new Command.Choice(LAND_IO_NAME, LAND_IO_NAME), + new Command.Choice(BOBBLE_LEAGUE_NAME, BOBBLE_LEAGUE_NAME), + new Command.Choice(ASK_AWAY_NAME, ASK_AWAY_NAME), + new Command.Choice(KNOW_WHAT_I_MEME_NAME, KNOW_WHAT_I_MEME_NAME)); /** - * List comes from the "Implement - * invite targets" PR on JDA. There is no official list from Discord themselves, so this is - * our best bet. + * List comes from + * this public + * list obtained by GeneralSadaf.. There is no official list from Discord themselves, so + * this is our best bet. */ private static final Map VC_APPLICATION_TO_ID = - Map.of(POKER_NAME, "755827207812677713", BETRAYAL_IO_NAME, "773336526917861400", - FISHINGTON_IO_NAME, "814288819477020702", SPELLCAST_NAME, "852509694341283871", - DOODLECREW_NAME, "878067389634314250", WORDSNACK_NAME, "879863976006127627", - LETTERTILE_NAME, "879863686565621790"); + Map.ofEntries(Map.entry(WATCH_TOGETHER_NAME, "880218394199220334"), + Map.entry(POKER_NAME, "755827207812677713"), + Map.entry(CHESS_NAME, "832012586023256104"), + Map.entry(SPELLCAST_NAME, "852509694341283871"), + Map.entry(DOODLE_CREW_NAME, "878067389634314250"), + Map.entry(WORD_SNACKS_NAME, "879863976006127627"), + Map.entry(LETTER_LEAGUE_NAME, "879863686565621790"), + Map.entry(CHECKERS_NAME, "832013003968348200"), + Map.entry(BLAZING_EIGHTS_NAME, "832025144389533716"), + Map.entry(SKETCH_HEADS_NAME, "902271654783242291"), + Map.entry(PUTT_PARTY_NAME, "945737671223947305"), + Map.entry(LAND_IO_NAME, "903769130790969345"), + Map.entry(BOBBLE_LEAGUE_NAME, "947957217959759964"), + Map.entry(ASK_AWAY_NAME, "976052223358406656"), + Map.entry(KNOW_WHAT_I_MEME_NAME, "950505761862189096")); private static final List inviteOptions = List.of(new OptionData(OptionType.INTEGER, MAX_USES_OPTION, From e7e98f068cf95aebd3135ac4445a48481e2a05c6 Mon Sep 17 00:00:00 2001 From: Daniel Tischner Date: Fri, 5 Aug 2022 13:26:19 +0200 Subject: [PATCH 03/25] Update stale action v3 -> v4 (#477) --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 2d77eb8f22..86ed01e66e 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -18,7 +18,7 @@ jobs: pull-requests: write steps: - - uses: actions/stale@v3 + - uses: actions/stale@v4 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label, comment or add the valid label or this will be closed in 5 days.' From 6eb914cc614637a751c43dd74e2fb962d1da9149 Mon Sep 17 00:00:00 2001 From: Daniel Tischner Date: Sat, 13 Aug 2022 13:16:53 +0200 Subject: [PATCH 04/25] Made active_question post more robust against hicups (#476) --- .../help/HelpThreadOverviewUpdater.java | 43 +++++++++++++++---- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadOverviewUpdater.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadOverviewUpdater.java index 0a79750c1a..8e09234b0e 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadOverviewUpdater.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadOverviewUpdater.java @@ -5,7 +5,9 @@ import net.dv8tion.jda.api.entities.*; import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.requests.RestAction; +import net.dv8tion.jda.internal.requests.CompletedRestAction; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.MessageReceiverAdapter; @@ -16,6 +18,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Predicate; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -33,6 +36,9 @@ public final class HelpThreadOverviewUpdater extends MessageReceiverAdapter impl private static final String STATUS_TITLE = "## __**Active questions**__ ##"; private static final int OVERVIEW_QUESTION_LIMIT = 150; + private static final AtomicInteger FIND_STATUS_MESSAGE_CONSECUTIVE_FAILURES = + new AtomicInteger(0); + private static final int FIND_STATUS_MESSAGE_FAILURE_THRESHOLD = 3; private final HelpSystemHelper helper; private final List allCategories; @@ -124,15 +130,10 @@ private void updateOverview(@NotNull TextChannel overviewChannel) { .setContent(STATUS_TITLE + "\n\n" + createDescription(activeThreads)) .build(); - getStatusMessage(overviewChannel).flatMap(maybeStatusMessage -> { - logger.debug("Sending the updated question overview"); - if (maybeStatusMessage.isEmpty()) { - return overviewChannel.sendMessage(message); - } - - String statusMessageId = maybeStatusMessage.orElseThrow().getId(); - return overviewChannel.editMessageById(statusMessageId, message); - }).queue(); + getStatusMessage(overviewChannel) + .flatMap(maybeStatusMessage -> sendUpdatedOverview(maybeStatusMessage.orElse(null), + message, overviewChannel)) + .queue(); } private @NotNull String createDescription(@NotNull Collection activeThreads) { @@ -178,6 +179,30 @@ private static boolean isStatusMessage(@NotNull Message message) { return content.startsWith(STATUS_TITLE); } + private @NotNull RestAction sendUpdatedOverview(@Nullable Message statusMessage, + @NotNull Message updatedStatusMessage, @NotNull MessageChannel overviewChannel) { + logger.debug("Sending the updated question overview"); + if (statusMessage == null) { + int currentFailures = FIND_STATUS_MESSAGE_CONSECUTIVE_FAILURES.incrementAndGet(); + if (currentFailures >= FIND_STATUS_MESSAGE_FAILURE_THRESHOLD) { + logger.warn( + "Failed to locate the question overview too often ({} times), sending a fresh message instead.", + currentFailures); + FIND_STATUS_MESSAGE_CONSECUTIVE_FAILURES.set(0); + return overviewChannel.sendMessage(updatedStatusMessage); + } + + logger.info( + "Failed to locate the question overview ({} times), trying again next time.", + currentFailures); + return new CompletedRestAction<>(overviewChannel.getJDA(), null, null); + } + + FIND_STATUS_MESSAGE_CONSECUTIVE_FAILURES.set(0); + String statusMessageId = statusMessage.getId(); + return overviewChannel.editMessageById(statusMessageId, updatedStatusMessage); + } + private enum ChannelType { OVERVIEW, STAGING From ed8accc8b1df677918dc9058c8eb6bef5a38c46d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Aug 2022 13:17:10 +0200 Subject: [PATCH 05/25] Bump flyway-core from 8.0.0 to 9.0.4 (#475) Bumps [flyway-core](https://github.com/flyway/flyway) from 8.0.0 to 9.0.4. - [Release notes](https://github.com/flyway/flyway/releases) - [Commits](https://github.com/flyway/flyway/compare/flyway-8.0.0...flyway-9.0.4) --- updated-dependencies: - dependency-name: org.flywaydb:flyway-core dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- database/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/build.gradle b/database/build.gradle index 117372c1ca..a69355ccab 100644 --- a/database/build.gradle +++ b/database/build.gradle @@ -6,7 +6,7 @@ var sqliteVersion = "3.36.0.3" dependencies { implementation "org.xerial:sqlite-jdbc:${sqliteVersion}" - implementation 'org.flywaydb:flyway-core:8.0.0' + implementation 'org.flywaydb:flyway-core:9.0.4' implementation 'org.jooq:jooq:3.15.3' } From d21ae6f267bca16762b0a6bfbd90d622c412143b Mon Sep 17 00:00:00 2001 From: Daniel Tischner Date: Sun, 14 Aug 2022 08:47:32 +0200 Subject: [PATCH 06/25] Update dependabot.yml (#496) --- .github/dependabot.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ac04ab32d3..e30d2aded5 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,8 +3,8 @@ updates: - package-ecosystem: "gradle" directory: "/" schedule: - interval: "monthly" + interval: "daily" ignore: - dependency-name: "*" - update-types: ["version-update:semver-patch", "version-update:semver-minor"] - open-pull-requests-limit: 10 \ No newline at end of file + update-types: ["version-update:semver-patch"] + open-pull-requests-limit: 10 From 64a32bd731b41f7a065207576c3c1b48a7a754e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Aug 2022 08:56:17 +0200 Subject: [PATCH 07/25] Bump org.sonarqube from 3.3 to 3.4.0.2513 (#498) Bumps org.sonarqube from 3.3 to 3.4.0.2513. --- updated-dependencies: - dependency-name: org.sonarqube dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 46b1ad685f..5cd835f345 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ plugins { id 'java' id "com.diffplug.spotless" version "6.0.0" - id "org.sonarqube" version "3.3" + id "org.sonarqube" version "3.4.0.2513" id "name.remal.sonarlint" version "1.5.0" } From e34f4560be7f0f9dc67bfae4425d352063671799 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Aug 2022 08:56:31 +0200 Subject: [PATCH 08/25] Bump sqlite-jdbc from 3.36.0.3 to 3.39.2.0 (#499) Bumps [sqlite-jdbc](https://github.com/xerial/sqlite-jdbc) from 3.36.0.3 to 3.39.2.0. - [Release notes](https://github.com/xerial/sqlite-jdbc/releases) - [Changelog](https://github.com/xerial/sqlite-jdbc/blob/master/CHANGELOG) - [Commits](https://github.com/xerial/sqlite-jdbc/compare/3.36.0.3...3.39.2.0) --- updated-dependencies: - dependency-name: org.xerial:sqlite-jdbc dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- application/build.gradle | 2 +- database/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/application/build.gradle b/application/build.gradle index e080e1b7d7..a2667e1e2f 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -1,6 +1,6 @@ buildscript { dependencies { - classpath 'org.xerial:sqlite-jdbc:3.36.0.3' + classpath 'org.xerial:sqlite-jdbc:3.39.2.0' } } diff --git a/database/build.gradle b/database/build.gradle index a69355ccab..0197327104 100644 --- a/database/build.gradle +++ b/database/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java' } -var sqliteVersion = "3.36.0.3" +var sqliteVersion = "3.39.2.0" dependencies { implementation "org.xerial:sqlite-jdbc:${sqliteVersion}" From 2a0728406beddba71ecad04356350384d934eca9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Aug 2022 08:56:46 +0200 Subject: [PATCH 09/25] Bump junit-jupiter-params from 5.8.1 to 5.9.0 (#500) Bumps [junit-jupiter-params](https://github.com/junit-team/junit5) from 5.8.1 to 5.9.0. - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.8.1...r5.9.0) --- updated-dependencies: - dependency-name: org.junit.jupiter:junit-jupiter-params dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- application/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/build.gradle b/application/build.gradle index a2667e1e2f..9cd714198d 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -67,7 +67,7 @@ dependencies { testImplementation 'org.mockito:mockito-core:4.0.0' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' - testImplementation 'org.junit.jupiter:junit-jupiter-params:5.8.1' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.0' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' } From e8d7d7180c9b413c46aab605b84834f8485b2c82 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Aug 2022 08:57:19 +0200 Subject: [PATCH 10/25] Bump log4j-slf4j18-impl from 2.16.0 to 2.18.0 (#502) Bumps log4j-slf4j18-impl from 2.16.0 to 2.18.0. --- updated-dependencies: - dependency-name: org.apache.logging.log4j:log4j-slf4j18-impl dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- application/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/build.gradle b/application/build.gradle index 9cd714198d..b7464a731d 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -46,7 +46,7 @@ dependencies { implementation 'net.dv8tion:JDA:5.0.0-alpha.9' implementation 'org.apache.logging.log4j:log4j-core:2.16.0' - runtimeOnly 'org.apache.logging.log4j:log4j-slf4j18-impl:2.16.0' + runtimeOnly 'org.apache.logging.log4j:log4j-slf4j18-impl:2.18.0' implementation 'org.jooq:jooq:3.15.3' From d86fc3b63b253fc2b6c5ae3b7bdd12a693cc96ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Aug 2022 08:57:51 +0200 Subject: [PATCH 11/25] Bump ascii-table from 1.2.0 to 1.3.0 (#503) Bumps [ascii-table](https://github.com/freva/ascii-table) from 1.2.0 to 1.3.0. - [Release notes](https://github.com/freva/ascii-table/releases) - [Commits](https://github.com/freva/ascii-table/compare/1.2.0...1.3.0) --- updated-dependencies: - dependency-name: com.github.freva:ascii-table dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- application/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/build.gradle b/application/build.gradle index b7464a731d..bbfba45194 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -61,7 +61,7 @@ dependencies { implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml: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.freva:ascii-table:1.3.0' implementation 'com.github.ben-manes.caffeine:caffeine:3.0.4' From f47892f38eac1831f010f671b6819c2e0b2d6fcc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Aug 2022 08:58:07 +0200 Subject: [PATCH 12/25] Bump caffeine from 3.0.4 to 3.1.1 (#504) Bumps [caffeine](https://github.com/ben-manes/caffeine) from 3.0.4 to 3.1.1. - [Release notes](https://github.com/ben-manes/caffeine/releases) - [Commits](https://github.com/ben-manes/caffeine/compare/v3.0.4...v3.1.1) --- updated-dependencies: - dependency-name: com.github.ben-manes.caffeine:caffeine dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- application/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/build.gradle b/application/build.gradle index bbfba45194..c86cd2d8b6 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -63,7 +63,7 @@ dependencies { implementation 'com.github.freva:ascii-table:1.3.0' - implementation 'com.github.ben-manes.caffeine:caffeine:3.0.4' + implementation 'com.github.ben-manes.caffeine:caffeine:3.1.1' testImplementation 'org.mockito:mockito-core:4.0.0' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' From 782c65556afc25e54903d6f9b2a7389df8b8d9f4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Aug 2022 08:58:23 +0200 Subject: [PATCH 13/25] Bump com.google.cloud.tools.jib from 3.1.4 to 3.2.1 (#505) Bumps com.google.cloud.tools.jib from 3.1.4 to 3.2.1. --- updated-dependencies: - dependency-name: com.google.cloud.tools.jib dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- application/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/build.gradle b/application/build.gradle index c86cd2d8b6..d413f830f2 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -6,7 +6,7 @@ buildscript { plugins { id 'application' - id 'com.google.cloud.tools.jib' version '3.1.4' + id 'com.google.cloud.tools.jib' version '3.2.1' id 'com.github.johnrengelman.shadow' version '7.1.0' id 'database-settings' } From 29749b95bbd227a96ecabddc13d9eb508253ff45 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Aug 2022 08:58:40 +0200 Subject: [PATCH 14/25] Bump flyway-core from 9.0.4 to 9.1.3 (#507) Bumps [flyway-core](https://github.com/flyway/flyway) from 9.0.4 to 9.1.3. - [Release notes](https://github.com/flyway/flyway/releases) - [Commits](https://github.com/flyway/flyway/compare/flyway-9.0.4...flyway-9.1.3) --- updated-dependencies: - dependency-name: org.flywaydb:flyway-core dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- database/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/build.gradle b/database/build.gradle index 0197327104..6f23030a96 100644 --- a/database/build.gradle +++ b/database/build.gradle @@ -6,7 +6,7 @@ var sqliteVersion = "3.39.2.0" dependencies { implementation "org.xerial:sqlite-jdbc:${sqliteVersion}" - implementation 'org.flywaydb:flyway-core:9.0.4' + implementation 'org.flywaydb:flyway-core:9.1.3' implementation 'org.jooq:jooq:3.15.3' } From fa0d983b9744104f209b01de338414a171c81d2d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Aug 2022 09:57:48 +0200 Subject: [PATCH 15/25] Bump junit-jupiter-engine from 5.8.1 to 5.9.0 (#501) Bumps [junit-jupiter-engine](https://github.com/junit-team/junit5) from 5.8.1 to 5.9.0. - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.8.1...r5.9.0) --- updated-dependencies: - dependency-name: org.junit.jupiter:junit-jupiter-engine dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- application/build.gradle | 2 +- formatter/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/application/build.gradle b/application/build.gradle index d413f830f2..e64ca89378 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -68,7 +68,7 @@ dependencies { testImplementation 'org.mockito:mockito-core:4.0.0' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.0' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0' } application { diff --git a/formatter/build.gradle b/formatter/build.gradle index cf09af47fb..20f1d90121 100644 --- a/formatter/build.gradle +++ b/formatter/build.gradle @@ -4,6 +4,6 @@ plugins { dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0' } From b7cf9669fdcc31683ee5e4eabf0ac79f84b50577 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Aug 2022 09:58:08 +0200 Subject: [PATCH 16/25] Bump com.diffplug.spotless from 6.0.0 to 6.9.1 (#506) Bumps com.diffplug.spotless from 6.0.0 to 6.9.1. --- updated-dependencies: - dependency-name: com.diffplug.spotless dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 5cd835f345..2c1bf4ffeb 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id "com.diffplug.spotless" version "6.0.0" + id "com.diffplug.spotless" version "6.9.1" id "org.sonarqube" version "3.4.0.2513" id "name.remal.sonarlint" version "1.5.0" } From 5658412a2d1d7200970ed0f2b3b6473cc06441f9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Aug 2022 09:58:31 +0200 Subject: [PATCH 17/25] Bump mockito-core from 4.0.0 to 4.7.0 (#510) Bumps [mockito-core](https://github.com/mockito/mockito) from 4.0.0 to 4.7.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v4.0.0...v4.7.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- application/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/build.gradle b/application/build.gradle index e64ca89378..9f75dfb731 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -65,7 +65,7 @@ dependencies { implementation 'com.github.ben-manes.caffeine:caffeine:3.1.1' - testImplementation 'org.mockito:mockito-core:4.0.0' + testImplementation 'org.mockito:mockito-core:4.7.0' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.0' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0' From 5e71489845fd72cd9f5136c38826dc3c523b4866 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Aug 2022 09:58:37 +0200 Subject: [PATCH 18/25] Bump jooq from 3.15.3 to 3.17.2 (#508) Bumps jooq from 3.15.3 to 3.17.2. --- updated-dependencies: - dependency-name: org.jooq:jooq dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- application/build.gradle | 2 +- database/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/application/build.gradle b/application/build.gradle index 9f75dfb731..168ab0244d 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -48,7 +48,7 @@ dependencies { implementation 'org.apache.logging.log4j:log4j-core:2.16.0' runtimeOnly 'org.apache.logging.log4j:log4j-slf4j18-impl:2.18.0' - implementation 'org.jooq:jooq:3.15.3' + implementation 'org.jooq:jooq:3.17.2' implementation 'io.mikael:urlbuilder:2.0.9' diff --git a/database/build.gradle b/database/build.gradle index 6f23030a96..f7a7c2e634 100644 --- a/database/build.gradle +++ b/database/build.gradle @@ -7,6 +7,6 @@ var sqliteVersion = "3.39.2.0" dependencies { implementation "org.xerial:sqlite-jdbc:${sqliteVersion}" implementation 'org.flywaydb:flyway-core:9.1.3' - implementation 'org.jooq:jooq:3.15.3' + implementation 'org.jooq:jooq:3.17.2' } From f2f2ef5ba6594d83ba8eaf7488be6df0cc7ddbdc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Aug 2022 10:22:20 +0200 Subject: [PATCH 19/25] Bump junit-jupiter-api from 5.8.1 to 5.9.0 (#509) Bumps [junit-jupiter-api](https://github.com/junit-team/junit5) from 5.8.1 to 5.9.0. - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.8.1...r5.9.0) --- updated-dependencies: - dependency-name: org.junit.jupiter:junit-jupiter-api dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- application/build.gradle | 2 +- formatter/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/application/build.gradle b/application/build.gradle index 168ab0244d..8e29f5ec47 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -66,7 +66,7 @@ dependencies { implementation 'com.github.ben-manes.caffeine:caffeine:3.1.1' testImplementation 'org.mockito:mockito-core:4.7.0' - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0' testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.0' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0' } diff --git a/formatter/build.gradle b/formatter/build.gradle index 20f1d90121..1b77b211f5 100644 --- a/formatter/build.gradle +++ b/formatter/build.gradle @@ -3,7 +3,7 @@ plugins { } dependencies { - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0' } From 1edd585149b0e39671cbd8a062b8b09d56aae875 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Aug 2022 10:22:29 +0200 Subject: [PATCH 20/25] Bump log4j-core from 2.16.0 to 2.18.0 (#511) Bumps log4j-core from 2.16.0 to 2.18.0. --- updated-dependencies: - dependency-name: org.apache.logging.log4j:log4j-core dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- application/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/build.gradle b/application/build.gradle index 8e29f5ec47..b239730637 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -45,7 +45,7 @@ dependencies { implementation 'net.dv8tion:JDA:5.0.0-alpha.9' - implementation 'org.apache.logging.log4j:log4j-core:2.16.0' + implementation 'org.apache.logging.log4j:log4j-core:2.18.0' runtimeOnly 'org.apache.logging.log4j:log4j-slf4j18-impl:2.18.0' implementation 'org.jooq:jooq:3.17.2' From 8a16edca5d73950cda53be7a5b7d523312734f85 Mon Sep 17 00:00:00 2001 From: Tanish Azad <73871477+Taz03@users.noreply.github.com> Date: Sun, 14 Aug 2022 16:15:59 +0530 Subject: [PATCH 21/25] fixed unknown class referenced (#514) * fixed unknown class referenced --- .../java/org/togetherjava/tjbot/Application.java | 11 +++++++---- .../tjbot/commands/componentids/ComponentId.java | 1 + .../componentids/ComponentIdGenerator.java | 14 +++++++++----- .../commands/componentids/ComponentIdParser.java | 8 ++++++-- .../commands/componentids/ComponentIdStore.java | 5 +++-- 5 files changed, 26 insertions(+), 13 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/Application.java b/application/src/main/java/org/togetherjava/tjbot/Application.java index e0995470d4..9bcb011b33 100644 --- a/application/src/main/java/org/togetherjava/tjbot/Application.java +++ b/application/src/main/java/org/togetherjava/tjbot/Application.java @@ -3,14 +3,19 @@ import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDABuilder; import net.dv8tion.jda.api.requests.GatewayIntent; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.togetherjava.tjbot.commands.Features; import org.togetherjava.tjbot.commands.system.BotCore; import org.togetherjava.tjbot.config.Config; import org.togetherjava.tjbot.db.Database; +import org.togetherjava.tjbot.commands.SlashCommandAdapter; import javax.security.auth.login.LoginException; + import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -19,10 +24,8 @@ /** * Main class of the application. Use {@link #main(String[])} to start an instance of it. *

- * New commands can be created by implementing - * {@link net.dv8tion.jda.api.events.interaction.SlashCommandInteractionEvent} or extending - * {@link org.togetherjava.tjbot.commands.SlashCommandAdapter}. They can then be registered in - * {@link Features}. + * New commands can be created by implementing {@link SlashCommandInteractionEvent} or extending + * {@link SlashCommandAdapter}. They can then be registered in {@link Features}. */ public enum Application { ; diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentId.java b/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentId.java index 49596eec7f..c5205e258b 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentId.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentId.java @@ -1,6 +1,7 @@ package org.togetherjava.tjbot.commands.componentids; import org.jetbrains.annotations.NotNull; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import java.util.List; diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdGenerator.java b/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdGenerator.java index 41b9ef0a3c..7514983890 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdGenerator.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdGenerator.java @@ -3,6 +3,11 @@ import net.dv8tion.jda.api.entities.Emoji; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.interactions.components.ComponentInteraction; +import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle; +import net.dv8tion.jda.api.interactions.components.buttons.Button; + +import org.togetherjava.tjbot.commands.SlashCommand; + import org.jetbrains.annotations.NotNull; /** @@ -11,9 +16,8 @@ * Component IDs are used during button or selection menu events. They can carry arbitrary data and * are persisted by the system. *

- * See - * {@link org.togetherjava.tjbot.commands.SlashCommand#onSlashCommand(SlashCommandInteractionEvent)} - * for more context on how to use this. + * See {@link SlashCommand#onSlashCommand(SlashCommandInteractionEvent)} for more context on how to + * use this. *

* The interface {@link ComponentIdParser} is the counterpart to this, offering parsing back the * payload from the ID. @@ -25,8 +29,8 @@ public interface ComponentIdGenerator { * interactions, such as button or selection menus. *

* See {@link ComponentInteraction#getComponentId()} and - * {@link net.dv8tion.jda.api.interactions.components.Button#of(ButtonStyle, String, Emoji)} for - * details on where the generated ID can be used. + * {@link Button#of(ButtonStyle, String, Emoji)} for details on where the generated ID can be + * used. * * @param componentId the component ID payload to persist and generate a valid ID for * @param lifespan the lifespan of the generated and persisted component ID diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdParser.java b/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdParser.java index e616592bd0..5dbfcc4374 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdParser.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdParser.java @@ -2,6 +2,10 @@ import net.dv8tion.jda.api.entities.Emoji; import net.dv8tion.jda.api.interactions.components.ComponentInteraction; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle; +import net.dv8tion.jda.api.interactions.components.buttons.Button; + import org.jetbrains.annotations.NotNull; import java.util.Optional; @@ -26,8 +30,8 @@ public interface ComponentIdParser { * interactions, such as button or selection menus. *

* See {@link ComponentInteraction#getComponentId()} and - * {@link net.dv8tion.jda.api.interactions.components.Button#of(ButtonStyle, String, Emoji)} for - * details on where the ID was originally transported with. + * {@link Button#of(ButtonStyle, String, Emoji)} for details on where the ID was originally + * transported with. * * @param uuid the UUID to parse which represents the component ID * @return the payload associated to the given UUID, if empty the component ID either never diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdStore.java b/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdStore.java index f611d74be7..7d1de1d526 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdStore.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdStore.java @@ -4,6 +4,8 @@ import com.fasterxml.jackson.dataformat.csv.CsvMapper; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import org.togetherjava.tjbot.commands.SlashCommand; import org.jetbrains.annotations.NotNull; import org.jooq.Result; import org.slf4j.Logger; @@ -26,8 +28,7 @@ /** * Thread-safe storage for component IDs. Can put, persist and get back component IDs based on * UUIDs. Component IDs are used for button and selection menu commands, see - * {@link org.togetherjava.tjbot.commands.SlashCommand#onSlashCommand(SlashCommandInteractionEvent)} - * for details. + * {@link SlashCommand#onSlashCommand(SlashCommandInteractionEvent)} for details. *

* Use {@link #putOrThrow(UUID, ComponentId, Lifespan)} to put and persist a component ID; and * {@link #get(UUID)} to get it back. Component IDs are persisted during application runs and can From e579de60cec34b20a2bf49fa4ac6125434cece83 Mon Sep 17 00:00:00 2001 From: Daniel Tischner Date: Mon, 15 Aug 2022 09:30:59 +0200 Subject: [PATCH 22/25] Adding uncaught exception handling (#494) * for scheduled services, it has to be done manually since their exception gets supressed and put into the future. also, those routines shouldnt stop if one failed --- .../org/togetherjava/tjbot/Application.java | 18 ++++++++++-------- .../componentids/ComponentIdStore.java | 12 ++++++++++-- .../help/HelpThreadOverviewUpdater.java | 16 ++++++++++++---- .../tjbot/commands/system/BotCore.java | 10 +++++++--- 4 files changed, 39 insertions(+), 17 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/Application.java b/application/src/main/java/org/togetherjava/tjbot/Application.java index 9bcb011b33..a4bf02d277 100644 --- a/application/src/main/java/org/togetherjava/tjbot/Application.java +++ b/application/src/main/java/org/togetherjava/tjbot/Application.java @@ -4,7 +4,7 @@ import net.dv8tion.jda.api.JDABuilder; import net.dv8tion.jda.api.requests.GatewayIntent; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; - +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,11 +56,10 @@ public static void main(final String[] args) { return; } - try { - runBot(config); - } catch (Exception t) { - logger.error("Unknown error", t); - } + Thread.setDefaultUncaughtExceptionHandler(Application::onUncaughtException); + Runtime.getRuntime().addShutdownHook(new Thread(Application::onShutdown)); + + runBot(config); } /** @@ -86,8 +85,6 @@ public static void runBot(Config config) { jda.addEventListener(new BotCore(jda, database, config)); jda.awaitReady(); logger.info("Bot is ready"); - - Runtime.getRuntime().addShutdownHook(new Thread(Application::onShutdown)); } catch (LoginException e) { logger.error("Failed to login", e); } catch (InterruptedException e) { @@ -109,4 +106,9 @@ private static void onShutdown() { logger.info("Bot has been stopped"); } + private static void onUncaughtException(@NotNull Thread failingThread, + @NotNull Throwable failure) { + logger.error("Unknown error in thread {}.", failingThread.getName(), failure); + } + } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdStore.java b/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdStore.java index 7d1de1d526..7f93b79961 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdStore.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdStore.java @@ -114,8 +114,16 @@ public ComponentIdStore(@NotNull Database database, long evictEveryInitialDelay, .maximumSize(CACHE_SIZE) .expireAfterAccess(EVICT_CACHE_OLDER_THAN, TimeUnit.of(EVICT_CACHE_OLDER_THAN_UNIT)) .build(); - evictionTask = evictionService.scheduleWithFixedDelay(this::evictDatabase, - evictEveryInitialDelay, evictEveryDelay, TimeUnit.of(evictEveryUnit)); + + Runnable evictCommand = () -> { + try { + evictDatabase(); + } catch (Exception e) { + logger.error("Unknown error while evicting the component ID store database.", e); + } + }; + evictionTask = evictionService.scheduleWithFixedDelay(evictCommand, evictEveryInitialDelay, + evictEveryDelay, TimeUnit.of(evictEveryUnit)); logDebugSizeStatistics(); } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadOverviewUpdater.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadOverviewUpdater.java index 8e09234b0e..92a9c4a23f 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadOverviewUpdater.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadOverviewUpdater.java @@ -81,10 +81,18 @@ public void onMessageReceived(@NotNull MessageReceivedEvent event) { message.delete().queue(); // Thread creation can sometimes take a bit longer than the actual message, so that - // "getThreadChannels()" - // would not pick it up, hence we execute the update with some slight delay. - UPDATE_SERVICE.schedule(() -> updateOverviewForGuild(event.getGuild()), 2, - TimeUnit.SECONDS); + // "getThreadChannels()" would not pick it up, hence we execute the update with some slight + // delay. + Runnable updateOverviewCommand = () -> { + try { + updateOverviewForGuild(event.getGuild()); + } catch (Exception e) { + logger.error( + "Unknown error while attempting to update the help overview for guild {}.", + event.getGuild().getId(), e); + } + }; + UPDATE_SERVICE.schedule(updateOverviewCommand, 2, TimeUnit.SECONDS); } private void updateOverviewForGuild(@NotNull Guild guild) { diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/system/BotCore.java b/application/src/main/java/org/togetherjava/tjbot/commands/system/BotCore.java index b6847bc882..6ffcbf1be8 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/system/BotCore.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/system/BotCore.java @@ -94,9 +94,13 @@ public BotCore(@NotNull JDA jda, @NotNull Database database, @NotNull Config con .forEach(routine -> { Runnable command = () -> { String routineName = routine.getClass().getSimpleName(); - logger.debug("Running routine %s...".formatted(routineName)); - routine.runRoutine(jda); - logger.debug("Finished routine %s.".formatted(routineName)); + try { + logger.debug("Running routine %s...".formatted(routineName)); + routine.runRoutine(jda); + logger.debug("Finished routine %s.".formatted(routineName)); + } catch (Exception e) { + logger.error("Unknown error in routine {}.", routineName, e); + } }; Routine.Schedule schedule = routine.createSchedule(); From 7130b769854d7d77ed7ae637964011c6faaaf3c0 Mon Sep 17 00:00:00 2001 From: Daniel Tischner Date: Fri, 19 Aug 2022 11:51:28 +0200 Subject: [PATCH 23/25] Added help thread activity detection (#492) * Added help thread activity detection * Use help-thread-name system when creating threads as well * Slightly increasing limit for better UX --- .../togetherjava/tjbot/commands/Features.java | 1 + .../tjbot/commands/help/AskCommand.java | 5 +- .../commands/help/BotMessageCleanup.java | 2 +- .../tjbot/commands/help/HelpSystemHelper.java | 85 +++++++++++-- .../help/HelpThreadActivityUpdater.java | 120 ++++++++++++++++++ .../help/HelpThreadOverviewUpdater.java | 11 +- .../commands/help/ImplicitAskListener.java | 5 +- 7 files changed, 207 insertions(+), 22 deletions(-) create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadActivityUpdater.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 401171192a..f82b777473 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/Features.java @@ -75,6 +75,7 @@ public enum Features { features.add(new RemindRoutine(database)); features.add(new ScamHistoryPurgeRoutine(scamHistoryStore)); features.add(new BotMessageCleanup(config)); + features.add(new HelpThreadActivityUpdater(helpSystemHelper)); // Message receivers features.add(new TopHelpersMessageListener(database, config)); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/AskCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/AskCommand.java index 63f0b23951..d29f72807b 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/help/AskCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/AskCommand.java @@ -97,7 +97,10 @@ public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { Guild guild = event.getGuild(); event.deferReply(true).queue(); - overviewChannel.createThreadChannel("[%s] %s".formatted(category, title)) + HelpSystemHelper.HelpThreadName name = new HelpSystemHelper.HelpThreadName( + HelpSystemHelper.ThreadActivity.NEEDS_HELP, category, title); + + overviewChannel.createThreadChannel(name.toChannelName()) .flatMap(threadChannel -> handleEvent(eventHook, threadChannel, author, title, category, guild)) .queue(any -> { diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/BotMessageCleanup.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/BotMessageCleanup.java index 5678072773..8584b29430 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/help/BotMessageCleanup.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/BotMessageCleanup.java @@ -117,7 +117,7 @@ private static boolean shouldMessageBeCleanedUp(@NotNull Message message) { logger.debug("Found {} messages to delete", messageIdsToDelete.size()); if (messageIdsToDelete.isEmpty()) { - return new CompletedRestAction<>(channel.getJDA(), null, null); + return new CompletedRestAction<>(channel.getJDA(), null); } if (messageIdsToDelete.size() == 1) { diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpSystemHelper.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpSystemHelper.java index c6bbd74544..e9c00a7460 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpSystemHelper.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpSystemHelper.java @@ -5,6 +5,7 @@ import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback; import net.dv8tion.jda.api.requests.RestAction; import net.dv8tion.jda.api.requests.restaction.MessageAction; +import net.dv8tion.jda.internal.requests.CompletedRestAction; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -19,6 +20,7 @@ import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Stream; /** * Helper class offering certain methods used by the help system. @@ -29,10 +31,13 @@ public final class HelpSystemHelper { static final Color AMBIENT_COLOR = new Color(255, 255, 165); private static final String CODE_SYNTAX_EXAMPLE_PATH = "codeSyntaxExample.png"; + + private static final String ACTIVITY_GROUP = "activity"; private static final String CATEGORY_GROUP = "category"; private static final String TITLE_GROUP = "title"; - private static final Pattern EXTRACT_CATEGORY_TITLE_PATTERN = Pattern - .compile("(?:\\[(?<%s>[^\\[]+)] )?(?<%s>.+)".formatted(CATEGORY_GROUP, TITLE_GROUP)); + private static final Pattern EXTRACT_HELP_NAME_PATTERN = + Pattern.compile("(?:(?<%s>\\W) )?(?:\\[(?<%s>[^\\[]+)] )?(?<%s>.+)" + .formatted(ACTIVITY_GROUP, CATEGORY_GROUP, TITLE_GROUP)); private static final Pattern TITLE_COMPACT_REMOVAL_PATTERN = Pattern.compile("\\W"); static final int TITLE_COMPACT_LENGTH_MIN = 2; @@ -143,17 +148,40 @@ Optional getCategoryOfChannel(@NotNull Channel channel) { RestAction renameChannelToCategory(@NotNull GuildChannel channel, @NotNull String category) { HelpThreadName currentName = HelpThreadName.ofChannelName(channel.getName()); - HelpThreadName changedName = new HelpThreadName(category, currentName.title); + HelpThreadName nextName = + new HelpThreadName(currentName.activity, category, currentName.title); - return channel.getManager().setName(changedName.toChannelName()); + return renameChannel(channel, currentName, nextName); } @NotNull RestAction renameChannelToTitle(@NotNull GuildChannel channel, @NotNull String title) { HelpThreadName currentName = HelpThreadName.ofChannelName(channel.getName()); - HelpThreadName changedName = new HelpThreadName(currentName.category, title); + HelpThreadName nextName = + new HelpThreadName(currentName.activity, currentName.category, title); + + return renameChannel(channel, currentName, nextName); + } + + @NotNull + RestAction renameChannelToActivity(@NotNull GuildChannel channel, + @NotNull ThreadActivity activity) { + HelpThreadName currentName = HelpThreadName.ofChannelName(channel.getName()); + HelpThreadName nextName = + new HelpThreadName(activity, currentName.category, currentName.title); - return channel.getManager().setName(changedName.toChannelName()); + return renameChannel(channel, currentName, nextName); + } + + @NotNull + private RestAction renameChannel(@NotNull GuildChannel channel, + @NotNull HelpThreadName currentName, @NotNull HelpThreadName nextName) { + if (currentName.equals(nextName)) { + // Do not stress rate limits if no actual change is done + return new CompletedRestAction<>(channel.getJDA(), null); + } + + return channel.getManager().setName(nextName.toChannelName()); } boolean isOverviewChannelName(@NotNull String channelName) { @@ -185,7 +213,7 @@ static boolean isTitleValid(@NotNull CharSequence title) { Optional handleRequireOverviewChannelForAsk(@NotNull Guild guild, @NotNull MessageChannel respondTo) { Predicate isChannelName = this::isOverviewChannelName; - String channelPattern = this.getOverviewChannelPattern(); + String channelPattern = getOverviewChannelPattern(); Optional maybeChannel = guild.getTextChannelCache() .stream() @@ -206,20 +234,55 @@ Optional handleRequireOverviewChannelForAsk(@NotNull Guild guild, return maybeChannel; } - private record HelpThreadName(@Nullable String category, @NotNull String title) { + record HelpThreadName(@Nullable ThreadActivity activity, @Nullable String category, + @NotNull String title) { static @NotNull HelpThreadName ofChannelName(@NotNull CharSequence channelName) { - Matcher matcher = EXTRACT_CATEGORY_TITLE_PATTERN.matcher(channelName); + Matcher matcher = EXTRACT_HELP_NAME_PATTERN.matcher(channelName); if (!matcher.matches()) { throw new AssertionError("Pattern must match any thread name"); } - return new HelpThreadName(matcher.group(CATEGORY_GROUP), matcher.group(TITLE_GROUP)); + String activityText = matcher.group(ACTIVITY_GROUP); + + ThreadActivity activity = + activityText == null ? null : ThreadActivity.ofSymbol(activityText); + String category = matcher.group(CATEGORY_GROUP); + String title = matcher.group(TITLE_GROUP); + + return new HelpThreadName(activity, category, title); } @NotNull String toChannelName() { - return category == null ? title : "[%s] %s".formatted(category, title); + String activityText = activity == null ? "" : activity.getSymbol() + " "; + String categoryText = category == null ? "" : "[%s] ".formatted(category); + + return activityText + categoryText + title; + } + } + + enum ThreadActivity { + NEEDS_HELP("🔻"), + LIKELY_NEEDS_HELP("🔸"), + SEEMS_GOOD("🔹"); + + private final String symbol; + + ThreadActivity(@NotNull String symbol) { + this.symbol = symbol; + } + + public @NotNull String getSymbol() { + return symbol; + } + + static @NotNull ThreadActivity ofSymbol(@NotNull String symbol) { + return Stream.of(values()) + .filter(activity -> activity.getSymbol().equals(symbol)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException( + "Unknown thread activity symbol: " + symbol)); } } } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadActivityUpdater.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadActivityUpdater.java new file mode 100644 index 0000000000..3114410ff4 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadActivityUpdater.java @@ -0,0 +1,120 @@ +package org.togetherjava.tjbot.commands.help; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.*; +import net.dv8tion.jda.api.requests.RestAction; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.togetherjava.tjbot.commands.Routine; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * Routine that periodically checks all help threads and updates their activity based on heuristics. + *

+ * The activity indicates to helpers which channels are in most need of help and which likely + * already received attention by helpers. + */ +public final class HelpThreadActivityUpdater implements Routine { + private static final Logger logger = LoggerFactory.getLogger(HelpThreadActivityUpdater.class); + private static final int SCHEDULE_MINUTES = 30; + private static final int ACTIVITY_DETERMINE_MESSAGE_LIMIT = 11; + + private final HelpSystemHelper helper; + + /** + * Creates a new instance. + * + * @param helper the helper to use + */ + public HelpThreadActivityUpdater(@NotNull HelpSystemHelper helper) { + this.helper = helper; + } + + @Override + public @NotNull Schedule createSchedule() { + return new Schedule(ScheduleMode.FIXED_RATE, 1, SCHEDULE_MINUTES, TimeUnit.MINUTES); + } + + @Override + public void runRoutine(@NotNull JDA jda) { + jda.getGuildCache().forEach(this::updateActivityForGuild); + } + + private void updateActivityForGuild(@NotNull Guild guild) { + Optional maybeOverviewChannel = handleRequireOverviewChannel(guild); + + if (maybeOverviewChannel.isEmpty()) { + return; + } + + logger.debug("Updating activities of active questions"); + + List activeThreads = maybeOverviewChannel.orElseThrow() + .getThreadChannels() + .stream() + .filter(Predicate.not(ThreadChannel::isArchived)) + .toList(); + + logger.debug("Found {} active questions", activeThreads.size()); + + activeThreads.forEach(this::updateActivityForThread); + } + + private @NotNull Optional handleRequireOverviewChannel(@NotNull Guild guild) { + Predicate isChannelName = helper::isOverviewChannelName; + String channelPattern = helper.getOverviewChannelPattern(); + + Optional maybeChannel = guild.getTextChannelCache() + .stream() + .filter(channel -> isChannelName.test(channel.getName())) + .findAny(); + + if (maybeChannel.isEmpty()) { + logger.warn( + "Unable to update help thread overview, did not find an overview channel matching the configured pattern '{}' for guild '{}'", + channelPattern, guild.getName()); + return Optional.empty(); + } + + return maybeChannel; + } + + private void updateActivityForThread(@NotNull ThreadChannel threadChannel) { + determineActivity(threadChannel) + .flatMap( + threadActivity -> helper.renameChannelToActivity(threadChannel, threadActivity)) + .queue(); + } + + private static @NotNull RestAction determineActivity( + MessageChannel channel) { + return channel.getHistory().retrievePast(ACTIVITY_DETERMINE_MESSAGE_LIMIT).map(messages -> { + if (messages.size() >= ACTIVITY_DETERMINE_MESSAGE_LIMIT) { + // There are likely even more messages, but we hit the limit + return HelpSystemHelper.ThreadActivity.SEEMS_GOOD; + } + + Map> authorToMessages = messages.stream() + .filter(Predicate.not(HelpThreadActivityUpdater::isBotMessage)) + .collect(Collectors.groupingBy(Message::getAuthor)); + + boolean isThereActivity = authorToMessages.size() >= 2 && authorToMessages.values() + .stream() + .anyMatch(messagesByAuthor -> messagesByAuthor.size() >= 2); + + return isThereActivity ? HelpSystemHelper.ThreadActivity.LIKELY_NEEDS_HELP + : HelpSystemHelper.ThreadActivity.NEEDS_HELP; + }); + } + + private static boolean isBotMessage(@NotNull Message message) { + return message.getAuthor().equals(message.getJDA().getSelfUser()); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadOverviewUpdater.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadOverviewUpdater.java index 92a9c4a23f..aaac70c55c 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadOverviewUpdater.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadOverviewUpdater.java @@ -116,8 +116,8 @@ private void updateOverviewForGuild(@NotNull Guild guild) { if (maybeChannel.isEmpty()) { logger.warn( - "Unable to update help thread overview, did not find a {} channel matching the configured pattern '{}' for guild '{}'", - ChannelType.OVERVIEW, channelPattern, guild.getName()); + "Unable to update help thread overview, did not find an overview channel matching the configured pattern '{}' for guild '{}'", + channelPattern, guild.getName()); return Optional.empty(); } @@ -203,7 +203,7 @@ private static boolean isStatusMessage(@NotNull Message message) { logger.info( "Failed to locate the question overview ({} times), trying again next time.", currentFailures); - return new CompletedRestAction<>(overviewChannel.getJDA(), null, null); + return new CompletedRestAction<>(overviewChannel.getJDA(), null); } FIND_STATUS_MESSAGE_CONSECUTIVE_FAILURES.set(0); @@ -211,11 +211,6 @@ private static boolean isStatusMessage(@NotNull Message message) { return overviewChannel.editMessageById(statusMessageId, updatedStatusMessage); } - private enum ChannelType { - OVERVIEW, - STAGING - } - private record CategoryWithThreads(@NotNull String category, @NotNull List threads) { diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/ImplicitAskListener.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/ImplicitAskListener.java index 5af0f1ae68..3652ef0a4a 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/help/ImplicitAskListener.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/ImplicitAskListener.java @@ -92,7 +92,10 @@ public void onMessageReceived(@NotNull MessageReceivedEvent event) { } TextChannel overviewChannel = maybeOverviewChannel.orElseThrow(); - overviewChannel.createThreadChannel(title) + HelpSystemHelper.HelpThreadName name = new HelpSystemHelper.HelpThreadName( + HelpSystemHelper.ThreadActivity.NEEDS_HELP, null, title); + + overviewChannel.createThreadChannel(name.toChannelName()) .flatMap(threadChannel -> handleEvent(threadChannel, message, title)) .queue(any -> { }, ImplicitAskListener::handleFailure); From 617eb819f0745a2bd8d77ab0f153ae11dd441757 Mon Sep 17 00:00:00 2001 From: Connor Schweighoefer <88111627+SquidXTV@users.noreply.github.com> Date: Fri, 19 Aug 2022 13:33:08 +0200 Subject: [PATCH 24/25] Gist auto filesharing in help threads (#491) * initial commit * initial commit * delete * worked on PR comments * added api key * Added default api key to config * fixed api key * changed to record * removed comments * added multiple uploads to one gist * Worked on pr comments * Fixed Sonarlint checks * Added documentation * Worked on pr comments * Accidentally pushed * pr comments * pr comments * pr comments * pr comments * pr comments --- application/config.json.template | 1 + .../togetherjava/tjbot/commands/Features.java | 7 +- .../FileSharingMessageListener.java | 213 ++++++++++++++++++ .../tjbot/commands/filesharing/GistFile.java | 10 + .../tjbot/commands/filesharing/GistFiles.java | 13 ++ .../commands/filesharing/GistRequest.java | 12 + .../commands/filesharing/GistResponse.java | 23 ++ .../commands/filesharing/package-info.java | 5 + .../org/togetherjava/tjbot/config/Config.java | 15 ++ 9 files changed, 295 insertions(+), 4 deletions(-) create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/filesharing/FileSharingMessageListener.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistFile.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistFiles.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistRequest.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistResponse.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/filesharing/package-info.java diff --git a/application/config.json.template b/application/config.json.template index 8238dba370..6c433a2043 100644 --- a/application/config.json.template +++ b/application/config.json.template @@ -1,5 +1,6 @@ { "token": "", + "gistApiKey": "", "databasePath": "local-database.db", "projectWebsite": "https://github.com/Together-Java/TJ-Bot", "discordGuildInvite": "https://discord.com/invite/XXFUXzK", 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 f82b777473..44b1d7eb4f 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/Features.java @@ -2,10 +2,8 @@ import net.dv8tion.jda.api.JDA; import org.jetbrains.annotations.NotNull; -import org.togetherjava.tjbot.commands.basic.PingCommand; -import org.togetherjava.tjbot.commands.basic.RoleSelectCommand; -import org.togetherjava.tjbot.commands.basic.SuggestionsUpDownVoter; -import org.togetherjava.tjbot.commands.basic.VcActivityCommand; +import org.togetherjava.tjbot.commands.basic.*; +import org.togetherjava.tjbot.commands.filesharing.FileSharingMessageListener; import org.togetherjava.tjbot.commands.help.*; import org.togetherjava.tjbot.commands.mathcommands.TeXCommand; import org.togetherjava.tjbot.commands.mathcommands.wolframalpha.WolframAlphaCommand; @@ -82,6 +80,7 @@ public enum Features { features.add(new SuggestionsUpDownVoter(config)); features.add(new ScamBlocker(actionsStore, scamHistoryStore, config)); features.add(new ImplicitAskListener(config, helpSystemHelper)); + features.add(new FileSharingMessageListener(config)); // Event receivers features.add(new RejoinModerationRoleListener(actionsStore, config)); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/FileSharingMessageListener.java b/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/FileSharingMessageListener.java new file mode 100644 index 0000000000..1e1c0a3639 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/FileSharingMessageListener.java @@ -0,0 +1,213 @@ +package org.togetherjava.tjbot.commands.filesharing; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import net.dv8tion.jda.api.entities.ChannelType; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.ThreadChannel; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; +import net.dv8tion.jda.api.interactions.components.buttons.Button; +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 java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +/** + * Listener that receives all sent help messages and uploads them to a share service if the message + * contains a file with the given extension in the + * {@link FileSharingMessageListener#extensionFilter}. + */ +public class FileSharingMessageListener extends MessageReceiverAdapter { + + private static final Logger LOGGER = LoggerFactory.getLogger(FileSharingMessageListener.class); + private static final ObjectMapper JSON = new ObjectMapper(); + + private static final String SHARE_API = "https://api.github.com/gists"; + private static final HttpClient CLIENT = HttpClient.newHttpClient(); + + private final String gistApiKey; + private final Set extensionFilter = Set.of("txt", "java", "gradle", "xml", "kt", "json", + "fxml", "css", "c", "h", "cpp", "py", "yml"); + + private final Predicate isStagingChannelName; + private final Predicate isOverviewChannelName; + + + public FileSharingMessageListener(@NotNull Config config) { + super(Pattern.compile(".*")); + + gistApiKey = config.getGistApiKey(); + isStagingChannelName = Pattern.compile(config.getHelpSystem().getStagingChannelPattern()) + .asMatchPredicate(); + isOverviewChannelName = Pattern.compile(config.getHelpSystem().getOverviewChannelPattern()) + .asMatchPredicate(); + } + + @Override + public void onMessageReceived(@NotNull MessageReceivedEvent event) { + User author = event.getAuthor(); + if (author.isBot() || event.isWebhookMessage()) { + return; + } + + if (!isHelpThread(event)) { + return; + } + + + List attachments = event.getMessage() + .getAttachments() + .stream() + .filter(this::isAttachmentRelevant) + .toList(); + + CompletableFuture.runAsync(() -> { + try { + processAttachments(event, attachments); + } catch (Exception e) { + LOGGER.error("Unknown error while processing attachments", e); + } + }); + } + + private boolean isAttachmentRelevant(@NotNull Message.Attachment attachment) { + String extension = attachment.getFileExtension(); + if (extension == null) { + return false; + } + return extensionFilter.contains(extension); + } + + + private void processAttachments(@NotNull MessageReceivedEvent event, + @NotNull List attachments) { + + Map nameToFile = new ConcurrentHashMap<>(); + + List> tasks = new ArrayList<>(); + for (Message.Attachment attachment : attachments) { + CompletableFuture task = attachment.retrieveInputStream() + .thenApply(this::readAttachment) + .thenAccept( + content -> nameToFile.put(getNameOf(attachment), new GistFile(content))); + + tasks.add(task); + } + + tasks.forEach(CompletableFuture::join); + + GistFiles files = new GistFiles(nameToFile); + GistRequest request = new GistRequest(event.getAuthor().getName(), false, files); + String url = uploadToGist(request); + sendResponse(event, url); + } + + private @NotNull String readAttachment(@NotNull InputStream stream) { + try { + return new String(stream.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private @NotNull String getNameOf(@NotNull Message.Attachment attachment) { + String fileName = attachment.getFileName(); + String fileExtension = attachment.getFileExtension(); + + if (fileExtension == null || fileExtension.equals("txt")) { + fileExtension = "java"; + } else if (fileExtension.equals("fxml")) { + fileExtension = "xml"; + } + + int extensionIndex = fileName.lastIndexOf('.'); + if (extensionIndex != -1) { + fileName = fileName.substring(0, extensionIndex); + } + + fileName += "." + fileExtension; + + return fileName; + } + + private @NotNull String uploadToGist(@NotNull GistRequest jsonRequest) { + String body; + try { + body = JSON.writeValueAsString(jsonRequest); + } catch (JsonProcessingException e) { + throw new IllegalStateException( + "Attempting to upload a file to gist, but unable to create the JSON request.", + e); + } + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(SHARE_API)) + .header("Accept", "application/json") + .header("Authorization", "token " + gistApiKey) + .POST(HttpRequest.BodyPublishers.ofString(body)) + .build(); + + HttpResponse apiResponse; + try { + apiResponse = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException( + "Attempting to upload a file to gist, but the request got interrupted.", e); + } + + int statusCode = apiResponse.statusCode(); + + if (statusCode < HttpURLConnection.HTTP_OK + || statusCode >= HttpURLConnection.HTTP_MULT_CHOICE) { + throw new IllegalStateException("Gist API unexpected response: " + apiResponse.body()); + } + + GistResponse gistResponse; + try { + gistResponse = JSON.readValue(apiResponse.body(), GistResponse.class); + } catch (JsonProcessingException e) { + throw new IllegalStateException( + "Attempting to upload file to gist, but unable to parse its JSON response.", e); + } + return gistResponse.getHtmlUrl(); + } + + private void sendResponse(@NotNull MessageReceivedEvent event, @NotNull String url) { + Message message = event.getMessage(); + String messageContent = + "I uploaded your attachments as **gist**. That way, they are easier to read for everyone, especially mobile users 👍"; + + message.reply(messageContent).setActionRow(Button.link(url, "gist")).queue(); + } + + private boolean isHelpThread(@NotNull MessageReceivedEvent event) { + if (event.getChannelType() != ChannelType.GUILD_PUBLIC_THREAD) { + return false; + } + + ThreadChannel thread = event.getThreadChannel(); + String rootChannelName = thread.getParentChannel().getName(); + return isStagingChannelName.test(rootChannelName) + || isOverviewChannelName.test(rootChannelName); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistFile.java b/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistFile.java new file mode 100644 index 0000000000..cba4472b4e --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistFile.java @@ -0,0 +1,10 @@ +package org.togetherjava.tjbot.commands.filesharing; + +import org.jetbrains.annotations.NotNull; + +/** + * @see Create a Gist via + * API + */ +record GistFile(@NotNull String content) { +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistFiles.java b/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistFiles.java new file mode 100644 index 0000000000..3d3f8ca032 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistFiles.java @@ -0,0 +1,13 @@ +package org.togetherjava.tjbot.commands.filesharing; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; + +/** + * @see Create a Gist via + * API + */ +record GistFiles(@NotNull @JsonAnyGetter Map nameToContent) { +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistRequest.java b/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistRequest.java new file mode 100644 index 0000000000..d1e1156547 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistRequest.java @@ -0,0 +1,12 @@ +package org.togetherjava.tjbot.commands.filesharing; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.jetbrains.annotations.NotNull; + +/** + * @see Create a Gist via + * API + */ +record GistRequest(@NotNull String description, @JsonProperty("public") boolean isPublic, + @NotNull GistFiles files) { +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistResponse.java b/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistResponse.java new file mode 100644 index 0000000000..af7d3f2ddf --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/GistResponse.java @@ -0,0 +1,23 @@ +package org.togetherjava.tjbot.commands.filesharing; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.jetbrains.annotations.NotNull; + +/** + * @see Create a Gist via + * API + */ +@JsonIgnoreProperties(ignoreUnknown = true) +final class GistResponse { + @JsonProperty("html_url") + private String htmlUrl; + + public @NotNull String getHtmlUrl() { + return this.htmlUrl; + } + + public void setHtmlUrl(@NotNull String htmlUrl) { + this.htmlUrl = htmlUrl; + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/package-info.java b/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/package-info.java new file mode 100644 index 0000000000..3eba1af69d --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/package-info.java @@ -0,0 +1,5 @@ +/** + * This package offers all the functionality for automatically uploading files to sharing services. + * The core class is {@link org.togetherjava.tjbot.commands.filesharing.FileSharingMessageListener}. + */ +package org.togetherjava.tjbot.commands.filesharing; 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 fb4ef623b8..9888d12191 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/Config.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/Config.java @@ -14,6 +14,7 @@ */ public final class Config { private final String token; + private final String gistApiKey; private final String databasePath; private final String projectWebsite; private final String discordGuildInvite; @@ -31,6 +32,7 @@ public final class Config { @SuppressWarnings("ConstructorWithTooManyParameters") @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) private Config(@JsonProperty("token") String token, + @JsonProperty("gistApiKey") String gistApiKey, @JsonProperty("databasePath") String databasePath, @JsonProperty("projectWebsite") String projectWebsite, @JsonProperty("discordGuildInvite") String discordGuildInvite, @@ -45,6 +47,7 @@ private Config(@JsonProperty("token") String token, @JsonProperty("wolframAlphaAppId") String wolframAlphaAppId, @JsonProperty("helpSystem") HelpSystemConfig helpSystem) { this.token = token; + this.gistApiKey = gistApiKey; this.databasePath = databasePath; this.projectWebsite = projectWebsite; this.discordGuildInvite = discordGuildInvite; @@ -100,6 +103,18 @@ public String getToken() { return token; } + /** + * Gets the API Key of GitHub to upload pastes via the API. + * + * @return the upload services API Key + * @see Create + * a GitHub key + */ + public String getGistApiKey() { + return gistApiKey; + } + /** * Gets the path where the database of the application is located at. * From 09d914d0dbfa6f44050b8ac67ce9eaded80196e8 Mon Sep 17 00:00:00 2001 From: Daniel Tischner Date: Sat, 20 Aug 2022 21:28:03 +0200 Subject: [PATCH 25/25] Auto-prune of full helper roles (#495) * Adding auto prune of full helper roles * Added hints to change logging level (also for bot only) * Fixed bug with routines starting too early * Message mods if pruning didnt help * also fixed a bug with BotCore missing its onReady event * Undid accidental commit * Fix after rebase * CR improvements --- .../org/togetherjava/tjbot/Application.java | 8 +- .../togetherjava/tjbot/commands/Features.java | 2 + .../commands/help/AutoPruneHelperRoutine.java | 168 ++++++++++++++++++ .../tjbot/commands/system/BotCore.java | 72 +++++--- .../tjbot/moderation/ModAuditLogWriter.java | 26 +-- application/src/main/resources/log4j2.xml | 4 + 6 files changed, 240 insertions(+), 40 deletions(-) create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/help/AutoPruneHelperRoutine.java diff --git a/application/src/main/java/org/togetherjava/tjbot/Application.java b/application/src/main/java/org/togetherjava/tjbot/Application.java index a4bf02d277..4dc8ec128b 100644 --- a/application/src/main/java/org/togetherjava/tjbot/Application.java +++ b/application/src/main/java/org/togetherjava/tjbot/Application.java @@ -82,8 +82,14 @@ public static void runBot(Config config) { JDA jda = JDABuilder.createDefault(config.getToken()) .enableIntents(GatewayIntent.GUILD_MEMBERS) .build(); - jda.addEventListener(new BotCore(jda, database, config)); + + BotCore core = new BotCore(jda, database, config); + jda.addEventListener(core); jda.awaitReady(); + + // We fire the event manually, since the core might be added too late to receive the + // actual event fired from JDA + core.onReady(jda); logger.info("Bot is ready"); } catch (LoginException e) { logger.error("Failed to login", e); 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 44b1d7eb4f..81657ce752 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/Features.java @@ -74,6 +74,8 @@ public enum Features { features.add(new ScamHistoryPurgeRoutine(scamHistoryStore)); features.add(new BotMessageCleanup(config)); features.add(new HelpThreadActivityUpdater(helpSystemHelper)); + features + .add(new AutoPruneHelperRoutine(config, helpSystemHelper, modAuditLogWriter, database)); // Message receivers features.add(new TopHelpersMessageListener(database, config)); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/AutoPruneHelperRoutine.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/AutoPruneHelperRoutine.java new file mode 100644 index 0000000000..f633972da8 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/AutoPruneHelperRoutine.java @@ -0,0 +1,168 @@ +package org.togetherjava.tjbot.commands.help; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.entities.TextChannel; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.togetherjava.tjbot.commands.Routine; +import org.togetherjava.tjbot.config.Config; +import org.togetherjava.tjbot.db.Database; +import org.togetherjava.tjbot.moderation.ModAuditLogWriter; + +import java.time.Duration; +import java.time.Instant; +import java.time.Period; +import java.util.*; +import java.util.concurrent.TimeUnit; + +import static org.togetherjava.tjbot.db.generated.tables.HelpChannelMessages.HELP_CHANNEL_MESSAGES; + +/** + * Due to a technical limitation in Discord, roles with more than 100 users can not be ghost-pinged + * into helper threads. + *

+ * This routine mitigates the problem by automatically pruning inactive users from helper roles + * approaching this limit. + */ +public final class AutoPruneHelperRoutine implements Routine { + private static final Logger logger = LoggerFactory.getLogger(AutoPruneHelperRoutine.class); + + private static final int ROLE_FULL_LIMIT = 100; + private static final int ROLE_FULL_THRESHOLD = 95; + private static final int PRUNE_MEMBER_AMOUNT = 10; + private static final Period INACTIVE_AFTER = Period.ofDays(90); + private static final int RECENTLY_JOINED_DAYS = 7; + + private final HelpSystemHelper helper; + private final ModAuditLogWriter modAuditLogWriter; + private final Database database; + private final List allCategories; + + /** + * Creates a new instance. + * + * @param config to determine all helper categories + * @param helper the helper to use + * @param modAuditLogWriter to inform mods when manual pruning becomes necessary + * @param database to determine whether an user is inactive + */ + public AutoPruneHelperRoutine(@NotNull Config config, @NotNull HelpSystemHelper helper, + @NotNull ModAuditLogWriter modAuditLogWriter, @NotNull Database database) { + allCategories = config.getHelpSystem().getCategories(); + this.helper = helper; + this.modAuditLogWriter = modAuditLogWriter; + this.database = database; + } + + @Override + public @NotNull Schedule createSchedule() { + return new Schedule(ScheduleMode.FIXED_RATE, 0, 1, TimeUnit.HOURS); + } + + @Override + public void runRoutine(@NotNull JDA jda) { + jda.getGuildCache().forEach(this::pruneForGuild); + } + + private void pruneForGuild(@NotNull Guild guild) { + TextChannel overviewChannel = guild.getTextChannels() + .stream() + .filter(channel -> helper.isOverviewChannelName(channel.getName())) + .findAny() + .orElseThrow(); + Instant now = Instant.now(); + + allCategories.stream() + .map(category -> helper.handleFindRoleForCategory(category, guild)) + .filter(Optional::isPresent) + .map(Optional::orElseThrow) + .forEach(role -> pruneRoleIfFull(role, overviewChannel, now)); + } + + private void pruneRoleIfFull(@NotNull Role role, @NotNull TextChannel overviewChannel, + @NotNull Instant when) { + role.getGuild().findMembersWithRoles(role).onSuccess(members -> { + if (isRoleFull(members)) { + logger.debug("Helper role {} is full, starting to prune.", role.getName()); + pruneRole(role, members, overviewChannel, when); + } + }); + } + + private boolean isRoleFull(@NotNull Collection members) { + return members.size() >= ROLE_FULL_THRESHOLD; + } + + private void pruneRole(@NotNull Role role, @NotNull List members, + @NotNull TextChannel overviewChannel, @NotNull Instant when) { + List membersShuffled = new ArrayList<>(members); + Collections.shuffle(membersShuffled); + + List membersToPrune = membersShuffled.stream() + .filter(member -> isMemberInactive(member, when)) + .limit(PRUNE_MEMBER_AMOUNT) + .toList(); + if (membersToPrune.size() < PRUNE_MEMBER_AMOUNT) { + warnModsAbout( + "Attempting to prune helpers from role **%s** (%d members), but only found %d inactive users. That is less than expected, the category might eventually grow beyond the limit." + .formatted(role.getName(), members.size(), membersToPrune.size()), + role.getGuild()); + } + if (members.size() - membersToPrune.size() >= ROLE_FULL_LIMIT) { + warnModsAbout( + "The helper role **%s** went beyond its member limit (%d), despite automatic pruning. It will not function correctly anymore. Please manually prune some users." + .formatted(role.getName(), ROLE_FULL_LIMIT), + role.getGuild()); + } + + logger.info("Pruning {} users {} from role {}", membersToPrune.size(), membersToPrune, + role.getName()); + membersToPrune.forEach(member -> pruneMemberFromRole(member, role, overviewChannel)); + } + + private boolean isMemberInactive(@NotNull Member member, @NotNull Instant when) { + if (member.hasTimeJoined()) { + Instant memberJoined = member.getTimeJoined().toInstant(); + if (Duration.between(memberJoined, when).toDays() <= RECENTLY_JOINED_DAYS) { + // New users are protected from purging to not immediately kick them out of the role + // again + return false; + } + } + + Instant latestActiveMoment = when.minus(INACTIVE_AFTER); + + // Has no recent help message + return database.read(context -> context.fetchCount(HELP_CHANNEL_MESSAGES, + HELP_CHANNEL_MESSAGES.GUILD_ID.eq(member.getGuild().getIdLong()) + .and(HELP_CHANNEL_MESSAGES.AUTHOR_ID.eq(member.getIdLong())) + .and(HELP_CHANNEL_MESSAGES.SENT_AT.greaterThan(latestActiveMoment)))) == 0; + } + + private void pruneMemberFromRole(@NotNull Member member, @NotNull Role role, + @NotNull TextChannel overviewChannel) { + Guild guild = member.getGuild(); + + String dmMessage = + """ + You seem to have been inactive for some time in server **%s**, hence we removed you from the **%s** role. + If that was a mistake, just head back to %s and select the role again. + Sorry for any inconvenience caused by this 🙇""" + .formatted(guild.getName(), role.getName(), overviewChannel.getAsMention()); + + guild.removeRoleFromMember(member, role) + .flatMap(any -> member.getUser().openPrivateChannel()) + .flatMap(channel -> channel.sendMessage(dmMessage)) + .queue(); + } + + private void warnModsAbout(@NotNull String message, @NotNull Guild guild) { + logger.warn(message); + + modAuditLogWriter.write("Auto-prune helpers", message, null, Instant.now(), guild); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/system/BotCore.java b/application/src/main/java/org/togetherjava/tjbot/commands/system/BotCore.java index 6ffcbf1be8..5921a542fc 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/system/BotCore.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/system/BotCore.java @@ -5,7 +5,6 @@ import net.dv8tion.jda.api.entities.Channel; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.TextChannel; -import net.dv8tion.jda.api.events.ReadyEvent; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; import net.dv8tion.jda.api.events.interaction.component.SelectMenuInteractionEvent; @@ -31,6 +30,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -56,9 +56,11 @@ public final class BotCore extends ListenerAdapter implements SlashCommandProvid Executors.newScheduledThreadPool(5); private final Config config; private final Map nameToInteractor; + private final List routines; private final ComponentIdParser componentIdParser; private final ComponentIdStore componentIdStore; private final Map channelNameToMessageReceiver = new HashMap<>(); + private final AtomicBoolean receivedOnReady = new AtomicBoolean(false); /** * Creates a new command system which uses the given database to allow commands to persist data. @@ -87,31 +89,11 @@ public BotCore(@NotNull JDA jda, @NotNull Database database, @NotNull Config con .map(EventReceiver.class::cast) .forEach(jda::addEventListener); - // Routines - features.stream() + // Routines (are scheduled once the core is ready) + routines = features.stream() .filter(Routine.class::isInstance) .map(Routine.class::cast) - .forEach(routine -> { - Runnable command = () -> { - String routineName = routine.getClass().getSimpleName(); - try { - logger.debug("Running routine %s...".formatted(routineName)); - routine.runRoutine(jda); - logger.debug("Finished routine %s.".formatted(routineName)); - } catch (Exception e) { - logger.error("Unknown error in routine {}.", routineName, e); - } - }; - - Routine.Schedule schedule = routine.createSchedule(); - switch (schedule.mode()) { - case FIXED_RATE -> ROUTINE_SERVICE.scheduleAtFixedRate(command, - schedule.initialDuration(), schedule.duration(), schedule.unit()); - case FIXED_DELAY -> ROUTINE_SERVICE.scheduleWithFixedDelay(command, - schedule.initialDuration(), schedule.duration(), schedule.unit()); - default -> throw new AssertionError("Unsupported schedule mode"); - } - }); + .toList(); // User Interactors (e.g. slash commands) nameToInteractor = features.stream() @@ -159,16 +141,50 @@ public BotCore(@NotNull JDA jda, @NotNull Database database, @NotNull Config con .map(SlashCommand.class::cast); } - @Override - public void onReady(@NotNull ReadyEvent event) { + /** + * Trigger once JDA is ready. Subsequent calls are ignored. + * + * @param jda the JDA instance to work with + */ + public void onReady(@NotNull JDA jda) { + if (!receivedOnReady.compareAndSet(false, true)) { + // Ensures that we only enter the event once + return; + } + // Register reload on all guilds logger.debug("JDA is ready, registering reload command"); - event.getJDA() - .getGuildCache() + jda.getGuildCache() .forEach(guild -> COMMAND_SERVICE.execute(() -> registerReloadCommand(guild))); // NOTE We do not have to wait for reload to complete for the command system to be ready // itself logger.debug("Bot core is now ready"); + + scheduleRoutines(jda); + } + + private void scheduleRoutines(@NotNull JDA jda) { + routines.forEach(routine -> { + Runnable command = () -> { + String routineName = routine.getClass().getSimpleName(); + try { + logger.debug("Running routine %s...".formatted(routineName)); + routine.runRoutine(jda); + logger.debug("Finished routine %s.".formatted(routineName)); + } catch (Exception e) { + logger.error("Unknown error in routine {}.", routineName, e); + } + }; + + Routine.Schedule schedule = routine.createSchedule(); + switch (schedule.mode()) { + case FIXED_RATE -> ROUTINE_SERVICE.scheduleAtFixedRate(command, + schedule.initialDuration(), schedule.duration(), schedule.unit()); + case FIXED_DELAY -> ROUTINE_SERVICE.scheduleWithFixedDelay(command, + schedule.initialDuration(), schedule.duration(), schedule.unit()); + default -> throw new AssertionError("Unsupported schedule mode"); + } + }); } @Override diff --git a/application/src/main/java/org/togetherjava/tjbot/moderation/ModAuditLogWriter.java b/application/src/main/java/org/togetherjava/tjbot/moderation/ModAuditLogWriter.java index 0b032d16e2..193f16edfd 100644 --- a/application/src/main/java/org/togetherjava/tjbot/moderation/ModAuditLogWriter.java +++ b/application/src/main/java/org/togetherjava/tjbot/moderation/ModAuditLogWriter.java @@ -7,6 +7,7 @@ import net.dv8tion.jda.api.requests.restaction.MessageAction; import net.dv8tion.jda.api.utils.AttachmentOption; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.config.Config; @@ -51,12 +52,12 @@ public ModAuditLogWriter(@NotNull Config config) { * * @param title the title of the log embed * @param description the description of the log embed - * @param author the author of the log message + * @param author the author of the log message, if any * @param timestamp the timestamp of the log message * @param guild the guild to write this log to * @param attachments attachments that will be added to the message. none or many. */ - public void write(@NotNull String title, @NotNull String description, @NotNull User author, + public void write(@NotNull String title, @NotNull String description, @Nullable User author, @NotNull TemporalAccessor timestamp, @NotNull Guild guild, @NotNull Attachment... attachments) { Optional auditLogChannel = getAndHandleModAuditLogChannel(guild); @@ -64,13 +65,16 @@ public void write(@NotNull String title, @NotNull String description, @NotNull U return; } - MessageAction message = auditLogChannel.orElseThrow() - .sendMessageEmbeds(new EmbedBuilder().setTitle(title) - .setDescription(description) - .setAuthor(author.getAsTag(), null, author.getAvatarUrl()) - .setTimestamp(timestamp) - .setColor(EMBED_COLOR) - .build()); + EmbedBuilder embedBuilder = new EmbedBuilder().setTitle(title) + .setDescription(description) + .setTimestamp(timestamp) + .setColor(EMBED_COLOR); + if (author != null) { + embedBuilder.setAuthor(author.getAsTag(), null, author.getAvatarUrl()); + } + + MessageAction message = + auditLogChannel.orElseThrow().sendMessageEmbeds(embedBuilder.build()); for (Attachment attachment : attachments) { message = message.addFile(attachment.getContentRaw(), attachment.name()); @@ -102,14 +106,14 @@ public Optional getAndHandleModAuditLogChannel(@NotNull Guild guild /** * Represents attachment to messages, as for example used by * {@link MessageAction#addFile(File, String, AttachmentOption...)}. - * + * * @param name the name of the attachment, example: {@code "foo.md"} * @param content the content of the attachment */ public record Attachment(@NotNull String name, @NotNull String content) { /** * Gets the content raw, interpreted as UTF-8. - * + * * @return the raw content of the attachment */ public byte @NotNull [] getContentRaw() { diff --git a/application/src/main/resources/log4j2.xml b/application/src/main/resources/log4j2.xml index 43540687ee..4f68a45e5f 100644 --- a/application/src/main/resources/log4j2.xml +++ b/application/src/main/resources/log4j2.xml @@ -22,6 +22,10 @@ + + + +