From ee9e0e84698e00fb23db161b323fad9d1e332425 Mon Sep 17 00:00:00 2001 From: christolis Date: Thu, 17 Oct 2024 22:56:26 +0300 Subject: [PATCH 1/8] feat: add role application system config --- application/config.json.template | 7 +++++ .../org/togetherjava/tjbot/config/Config.java | 15 +++++++++- .../config/RoleApplicationSystemConfig.java | 30 +++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 application/src/main/java/org/togetherjava/tjbot/config/RoleApplicationSystemConfig.java diff --git a/application/config.json.template b/application/config.json.template index a1aec8f470..79d0d2cfea 100644 --- a/application/config.json.template +++ b/application/config.json.template @@ -115,5 +115,12 @@ "fallbackChannelPattern": "java-news-and-changes", "pollIntervalInMinutes": 10 }, + "roleApplicationSystem": { + "submissionsChannelPattern": "staff-applications", + "defaultQuestion": "What makes you a valuable addition to the team? 😎", + "minimumAnswerLength": 50, + "maximumAnswerLength": 500, + "applicationSubmitCooldownMinutes": 5 + }, "memberCountCategoryPattern": "Info" } 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 e819f8e7d1..780c7089cc 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/Config.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/Config.java @@ -46,6 +46,7 @@ public final class Config { private final RSSFeedsConfig rssFeedsConfig; private final String selectRolesChannelPattern; private final String memberCountCategoryPattern; + private final RoleApplicationSystemConfig roleApplicationSystemConfig; @SuppressWarnings("ConstructorWithTooManyParameters") @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) @@ -94,7 +95,9 @@ private Config(@JsonProperty(value = "token", required = true) String token, required = true) FeatureBlacklistConfig featureBlacklistConfig, @JsonProperty(value = "rssConfig", required = true) RSSFeedsConfig rssFeedsConfig, @JsonProperty(value = "selectRolesChannelPattern", - required = true) String selectRolesChannelPattern) { + required = true) String selectRolesChannelPattern, + @JsonProperty(value = "roleApplicationSystem", + required = true) RoleApplicationSystemConfig roleApplicationSystemConfig) { this.token = Objects.requireNonNull(token); this.githubApiKey = Objects.requireNonNull(githubApiKey); this.databasePath = Objects.requireNonNull(databasePath); @@ -127,6 +130,7 @@ private Config(@JsonProperty(value = "token", required = true) String token, this.featureBlacklistConfig = Objects.requireNonNull(featureBlacklistConfig); this.rssFeedsConfig = Objects.requireNonNull(rssFeedsConfig); this.selectRolesChannelPattern = Objects.requireNonNull(selectRolesChannelPattern); + this.roleApplicationSystemConfig = roleApplicationSystemConfig; } /** @@ -410,6 +414,15 @@ public String getMemberCountCategoryPattern() { return memberCountCategoryPattern; } + /** + * The configuration related to the application form. + * + * @return the application form config + */ + public RoleApplicationSystemConfig getRoleApplicationSystemConfig() { + return roleApplicationSystemConfig; + } + /** * Gets the RSS feeds configuration. * diff --git a/application/src/main/java/org/togetherjava/tjbot/config/RoleApplicationSystemConfig.java b/application/src/main/java/org/togetherjava/tjbot/config/RoleApplicationSystemConfig.java new file mode 100644 index 0000000000..1e3644c149 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/config/RoleApplicationSystemConfig.java @@ -0,0 +1,30 @@ +package org.togetherjava.tjbot.config; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +/** + * Represents the configuration for an application form, including roles and application channel + * pattern. + */ +public record RoleApplicationSystemConfig( + @JsonProperty(value = "submissionsChannelPattern", + required = true) String submissionsChannelPattern, + @JsonProperty(value = "defaultQuestion", required = true) String defaultQuestion, + @JsonProperty(value = "minimumAnswerLength", required = true) int minimumAnswerLength, + @JsonProperty(value = "maximumAnswerLength", required = true) int maximumAnswerLength, + @JsonProperty(value = "applicationSubmitCooldownMinutes", + required = true) int applicationSubmitCooldownMinutes) { + + /** + * Constructs an instance of {@link RoleApplicationSystemConfig} with the provided parameters. + * + * @param submissionsChannelPattern the pattern used to identify the application channel + * @param defaultQuestion the default question for the form + */ + public RoleApplicationSystemConfig { + Objects.requireNonNull(submissionsChannelPattern); + Objects.requireNonNull(defaultQuestion); + } +} From a8178b9db2e2d9ef3e7b6faa6fdd364453d0fd16 Mon Sep 17 00:00:00 2001 From: christolis Date: Thu, 17 Oct 2024 22:57:12 +0300 Subject: [PATCH 2/8] feat: create role application command Co-authored-by: Suraj Kumar <76599223+surajkumar@users.noreply.github.com> --- .../togetherjava/tjbot/features/Features.java | 2 + .../ApplicationApplyHandler.java | 132 ++++++++ .../ApplicationCreateCommand.java | 297 ++++++++++++++++++ .../roleapplication/package-info.java | 12 + 4 files changed, 443 insertions(+) create mode 100644 application/src/main/java/org/togetherjava/tjbot/features/roleapplication/ApplicationApplyHandler.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/features/roleapplication/ApplicationCreateCommand.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/features/roleapplication/package-info.java diff --git a/application/src/main/java/org/togetherjava/tjbot/features/Features.java b/application/src/main/java/org/togetherjava/tjbot/features/Features.java index 893adbc00f..ce5af3875d 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/Features.java @@ -64,6 +64,7 @@ import org.togetherjava.tjbot.features.moderation.temp.TemporaryModerationRoutine; import org.togetherjava.tjbot.features.reminder.RemindRoutine; import org.togetherjava.tjbot.features.reminder.ReminderCommand; +import org.togetherjava.tjbot.features.roleapplication.ApplicationCreateCommand; import org.togetherjava.tjbot.features.system.BotCore; import org.togetherjava.tjbot.features.system.LogLevelCommand; import org.togetherjava.tjbot.features.tags.TagCommand; @@ -192,6 +193,7 @@ public static Collection createFeatures(JDA jda, Database database, Con features.add(new BookmarksCommand(bookmarksSystem)); features.add(new ChatGptCommand(chatGptService, helpSystemHelper)); features.add(new JShellCommand(jshellEval)); + features.add(new ApplicationCreateCommand(config)); FeatureBlacklist> blacklist = blacklistConfig.normal(); return blacklist.filterStream(features.stream(), Object::getClass).toList(); diff --git a/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/ApplicationApplyHandler.java b/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/ApplicationApplyHandler.java new file mode 100644 index 0000000000..10f6ec37ee --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/ApplicationApplyHandler.java @@ -0,0 +1,132 @@ +package org.togetherjava.tjbot.features.roleapplication; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; +import net.dv8tion.jda.api.interactions.modals.ModalMapping; + +import org.togetherjava.tjbot.config.RoleApplicationSystemConfig; + +import java.time.Duration; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +/** + * Handles the actual process of submitting role applications. + *

+ * This class is responsible for managing application submissions via modal interactions, ensuring + * that submissions are sent to the appropriate application channel, and enforcing cooldowns for + * users to prevent spamming. + */ +public class ApplicationApplyHandler { + + private final Cache applicationSubmitCooldown; + private final Predicate applicationChannelPattern; + private final RoleApplicationSystemConfig roleApplicationSystemConfig; + + /** + * Constructs a new {@code ApplicationApplyHandler} instance. + * + * @param roleApplicationSystemConfig the configuration that contains the details for the application form + * including the cooldown duration and channel pattern. + */ + public ApplicationApplyHandler(RoleApplicationSystemConfig roleApplicationSystemConfig) { + this.roleApplicationSystemConfig = roleApplicationSystemConfig; + this.applicationChannelPattern = + Pattern.compile(roleApplicationSystemConfig.submissionsChannelPattern()).asMatchPredicate(); + + final Duration applicationSubmitCooldownDuration = + Duration.ofMinutes(roleApplicationSystemConfig.applicationSubmitCooldownMinutes()); + applicationSubmitCooldown = + Caffeine.newBuilder().expireAfterWrite(applicationSubmitCooldownDuration).build(); + } + + /** + * Sends the result of an application submission to the designated application channel in the + * guild. + *

+ * The {@code args} parameter should contain the applicant's name and the role they are applying + * for. + * + * @param event the modal interaction event triggering the application submission + * @param args the arguments provided in the application submission + * @param answer the answer provided by the applicant to the default question + */ + protected void sendApplicationResult(final ModalInteractionEvent event, List args, + String answer) { + Guild guild = event.getGuild(); + if (args.size() != 2 || guild == null) { + return; + } + + Optional applicationChannel = getApplicationChannel(guild); + if (applicationChannel.isEmpty()) { + return; + } + + User applicant = event.getUser(); + EmbedBuilder embed = + new EmbedBuilder().setAuthor(applicant.getName(), null, applicant.getAvatarUrl()) + .setColor(ApplicationCreateCommand.AMBIENT_COLOR) + .setTimestamp(Instant.now()) + .setFooter("Submitted at"); + + String roleString = args.getLast(); + MessageEmbed.Field roleField = new MessageEmbed.Field("Role", roleString, false); + embed.addField(roleField); + + MessageEmbed.Field answerField = + new MessageEmbed.Field(roleApplicationSystemConfig.defaultQuestion(), answer, false); + embed.addField(answerField); + + applicationChannel.get().sendMessageEmbeds(embed.build()).queue(); + } + + /** + * Retrieves the application channel from the given {@link Guild}. + * + * @param guild the guild from which to retrieve the application channel + * @return an {@link Optional} containing the {@link TextChannel} representing the application + * channel, or an empty {@link Optional} if no such channel is found + */ + private Optional getApplicationChannel(Guild guild) { + return guild.getChannels() + .stream() + .filter(channel -> applicationChannelPattern.test(channel.getName())) + .filter(channel -> channel.getType().isMessage()) + .map(TextChannel.class::cast) + .findFirst(); + } + + public Cache getApplicationSubmitCooldown() { + return applicationSubmitCooldown; + } + + protected void submitApplicationFromModalInteraction(ModalInteractionEvent event, + List args) { + Guild guild = event.getGuild(); + + if (guild == null) { + return; + } + + ModalMapping modalAnswer = event.getValues().getFirst(); + + sendApplicationResult(event, args, modalAnswer.getAsString()); + event.reply("Your application has been submitted. Thank you for applying! 😎") + .setEphemeral(true) + .queue(); + + applicationSubmitCooldown.put(event.getMember(), OffsetDateTime.now()); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/ApplicationCreateCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/ApplicationCreateCommand.java new file mode 100644 index 0000000000..56d66b3fd6 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/ApplicationCreateCommand.java @@ -0,0 +1,297 @@ +package org.togetherjava.tjbot.features.roleapplication; + +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.entities.emoji.Emoji; +import net.dv8tion.jda.api.entities.emoji.EmojiUnion; +import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.CommandInteraction; +import net.dv8tion.jda.api.interactions.commands.OptionMapping; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; +import net.dv8tion.jda.api.interactions.components.ActionRow; +import net.dv8tion.jda.api.interactions.components.selections.SelectOption; +import net.dv8tion.jda.api.interactions.components.selections.StringSelectMenu; +import net.dv8tion.jda.api.interactions.components.text.TextInput; +import net.dv8tion.jda.api.interactions.components.text.TextInputStyle; +import net.dv8tion.jda.api.interactions.modals.Modal; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.togetherjava.tjbot.config.Config; +import org.togetherjava.tjbot.config.RoleApplicationSystemConfig; +import org.togetherjava.tjbot.features.CommandVisibility; +import org.togetherjava.tjbot.features.SlashCommandAdapter; +import org.togetherjava.tjbot.features.componentids.Lifespan; + +import javax.annotation.Nullable; + +import java.awt.Color; +import java.time.Duration; +import java.time.OffsetDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.IntStream; + +/** + * Represents a command to create an application form for members to apply for roles. + *

+ * This command is designed to generate an application form for members to apply for roles within a + * guild. + */ +public class ApplicationCreateCommand extends SlashCommandAdapter { + private static final Logger logger = LoggerFactory.getLogger(ApplicationCreateCommand.class); + + protected static final Color AMBIENT_COLOR = new Color(24, 221, 136, 255); + private static final int OPTIONAL_ROLES_AMOUNT = 5; + private static final String ROLE_COMPONENT_ID_HEADER = "application-create"; + private static final String VALUE_DELIMITER = "_"; + private static final int ARG_COUNT = 3; + + private final ApplicationApplyHandler applicationApplyHandler; + private final RoleApplicationSystemConfig roleApplicationSystemConfig; + + /** + * Constructs a new {@link ApplicationCreateCommand} with the specified configuration. + *

+ * This command is designed to generate an application form for members to apply for roles. + * + * @param config the configuration containing the settings for the application form + */ + public ApplicationCreateCommand(Config config) { + super("application-form", "Generates an application form for members to apply for roles.", + CommandVisibility.GUILD); + + this.roleApplicationSystemConfig = config.getRoleApplicationSystemConfig(); + + generateRoleOptions(getData()); + applicationApplyHandler = new ApplicationApplyHandler(roleApplicationSystemConfig); + } + + /** + * Populates a {@link SlashCommandData} object with the proper arguments. + * + * @param data the object to populate + */ + private void generateRoleOptions(SlashCommandData data) { + IntStream.range(1, OPTIONAL_ROLES_AMOUNT + 1).forEach(index -> { + data.addOption(OptionType.STRING, generateOptionId("title", index), + "The title of the role"); + data.addOption(OptionType.STRING, generateOptionId("description", index), + "The description of the role"); + data.addOption(OptionType.STRING, generateOptionId("emoji", index), + "The emoji of the role"); + }); + } + + private static String generateOptionId(String name, int id) { + return "%s%s%d".formatted(name, VALUE_DELIMITER, id); + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event) { + if (!handleHasPermissions(event)) { + return; + } + + final List optionMappings = event.getInteraction().getOptions(); + if (optionMappings.isEmpty()) { + event.reply("You have to select at least one role.").setEphemeral(true).queue(); + return; + } + + long incorrectArgsCount = getIncorrectRoleArgsCount(optionMappings); + if (incorrectArgsCount > 0) { + event.reply("Missing information for %d roles.".formatted(incorrectArgsCount)) + .setEphemeral(true) + .queue(); + return; + } + + sendMenu(event); + } + + @Override + public void onStringSelectSelection(StringSelectInteractionEvent event, List args) { + SelectOption selectOption = event.getSelectedOptions().getFirst(); + + if (selectOption == null) { + return; + } + + OffsetDateTime timeSentCache = applicationApplyHandler.getApplicationSubmitCooldown() + .getIfPresent(event.getMember()); + if (timeSentCache != null) { + Duration duration = Duration.between(timeSentCache, OffsetDateTime.now()); + long remainingMinutes = + roleApplicationSystemConfig.applicationSubmitCooldownMinutes() - duration.toMinutes(); + + if (duration.toMinutes() < roleApplicationSystemConfig.applicationSubmitCooldownMinutes()) { + event + .reply("Please wait %d minutes before sending a new application form." + .formatted(remainingMinutes)) + .setEphemeral(true) + .queue(); + return; + } + } + + String questionLabel = roleApplicationSystemConfig.defaultQuestion(); + if (questionLabel.length() > TextInput.MAX_LABEL_LENGTH) { + questionLabel = questionLabel.substring(0, TextInput.MAX_LABEL_LENGTH); + } + + TextInput body = TextInput + .create(generateComponentId(event.getUser().getId()), questionLabel, + TextInputStyle.PARAGRAPH) + .setRequired(true) + .setRequiredRange(roleApplicationSystemConfig.minimumAnswerLength(), roleApplicationSystemConfig.maximumAnswerLength()) + .setPlaceholder("Enter your answer here") + .build(); + + EmojiUnion emoji = selectOption.getEmoji(); + String roleDisplayName; + + if (emoji == null) { + roleDisplayName = selectOption.getLabel(); + } else { + roleDisplayName = "%s %s".formatted(emoji.getFormatted(), selectOption.getLabel()); + } + + Modal modal = Modal + .create(generateComponentId(event.getUser().getId(), roleDisplayName), + String.format("Application form - %s", selectOption.getLabel())) + .addActionRow(ActionRow.of(body).getComponents()) + .build(); + + event.replyModal(modal).queue(); + } + + /** + * Checks a given list of passed arguments (from a user) and calculates how many roles have + * missing data. + * + * @param args the list of passed arguments + * @return the amount of roles with missing data + */ + private static long getIncorrectRoleArgsCount(final List args) { + final Map frequencyMap = new HashMap<>(); + + args.stream() + .map(OptionMapping::getName) + .map(name -> name.split(VALUE_DELIMITER)[1]) + .forEach(number -> frequencyMap.merge(number, 1, Integer::sum)); + + return frequencyMap.values().stream().filter(value -> value != 3).count(); + } + + /** + * Populates a {@link StringSelectMenu.Builder} with application roles. + * + * @param menuBuilder the menu builder to populate + * @param args the arguments which contain data about the roles + */ + private void addRolesToMenu(StringSelectMenu.Builder menuBuilder, + final List args) { + final Map roles = new HashMap<>(); + + for (int i = 0; i < args.size(); i += ARG_COUNT) { + OptionMapping optionTitle = args.get(i); + OptionMapping optionDescription = args.get(i + 1); + OptionMapping optionEmoji = args.get(i + 2); + + roles.put(i, + new MenuRole(optionTitle.getAsString(), + generateComponentId(ROLE_COMPONENT_ID_HEADER, + optionTitle.getAsString()), + optionDescription.getAsString(), + Emoji.fromFormatted(optionEmoji.getAsString()))); + } + + roles.values() + .forEach(role -> menuBuilder.addOption(role.title(), role.value(), role.description(), + role.emoji())); + } + + private boolean handleHasPermissions(SlashCommandInteractionEvent event) { + Member member = event.getMember(); + Guild guild = event.getGuild(); + + if (member == null || guild == null) { + return false; + } + + if (!member.hasPermission(Permission.MANAGE_ROLES)) { + event.reply("You do not have the required manage role permission to use this command") + .setEphemeral(true) + .queue(); + return false; + } + + Member selfMember = guild.getSelfMember(); + if (!selfMember.hasPermission(Permission.MANAGE_ROLES)) { + event.reply( + "Sorry, but I was not set up correctly. I need the manage role permissions for this.") + .setEphemeral(true) + .queue(); + logger.error("The bot requires the manage role permissions for /{}.", getName()); + return false; + } + + return true; + } + + /** + * Sends the initial embed and a button which displays role openings. + * + * @param event the command interaction event triggering the menu + */ + private void sendMenu(final CommandInteraction event) { + MessageEmbed embed = createApplicationEmbed(); + + StringSelectMenu.Builder menuBuilder = StringSelectMenu + .create(generateComponentId(Lifespan.PERMANENT, event.getUser().getId())) + .setPlaceholder("Select role to apply for") + .setRequiredRange(1, 1); + + addRolesToMenu(menuBuilder, event.getOptions()); + + event.replyEmbeds(embed).addActionRow(menuBuilder.build()).queue(); + } + + private static MessageEmbed createApplicationEmbed() { + return new EmbedBuilder().setTitle("Apply for roles") + .setDescription( + """ + We are always looking for community members that want to contribute to our community \ + and take charge. If you are interested, you can apply for various positions here!""") + .setColor(AMBIENT_COLOR) + .build(); + } + + public ApplicationApplyHandler getApplicationApplyHandler() { + return applicationApplyHandler; + } + + @Override + public void onModalSubmitted(ModalInteractionEvent event, List args) { + getApplicationApplyHandler().submitApplicationFromModalInteraction(event, args); + } + + /** + * Wrapper class which represents a menu role for the application create command. + *

+ * The reason this exists is due to the fact that {@link StringSelectMenu.Builder} does not have + * a method which takes emojis as input as of writing this, so we have to elegantly pass in + * custom data from this POJO. + */ + private record MenuRole(String title, String value, String description, @Nullable Emoji emoji) { + + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/package-info.java b/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/package-info.java new file mode 100644 index 0000000000..ac6ed5b52b --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/package-info.java @@ -0,0 +1,12 @@ +/** + * This packages offers all the functionality for the application-create command as well as the + * application system. The core class is + * {@link org.togetherjava.tjbot.features.roleapplication.ApplicationCreateCommand}. + */ +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault +package org.togetherjava.tjbot.features.roleapplication; + +import org.togetherjava.tjbot.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; From 8e4e8a94fa3d3495fa8b0d9a9fee90f8a091cb72 Mon Sep 17 00:00:00 2001 From: christolis Date: Thu, 17 Oct 2024 23:23:09 +0300 Subject: [PATCH 3/8] fix(handle-permissions): remove unnecessary check This removes the permission check for the bot to have the `MANAGE_ROLES` permission in order to execute the command. --- .../roleapplication/ApplicationCreateCommand.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/ApplicationCreateCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/ApplicationCreateCommand.java index 56d66b3fd6..87e1abbd53 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/ApplicationCreateCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/ApplicationCreateCommand.java @@ -234,16 +234,6 @@ private boolean handleHasPermissions(SlashCommandInteractionEvent event) { return false; } - Member selfMember = guild.getSelfMember(); - if (!selfMember.hasPermission(Permission.MANAGE_ROLES)) { - event.reply( - "Sorry, but I was not set up correctly. I need the manage role permissions for this.") - .setEphemeral(true) - .queue(); - logger.error("The bot requires the manage role permissions for /{}.", getName()); - return false; - } - return true; } From e4f019f0a7177e397f15897c8e9e4a76d39caefd Mon Sep 17 00:00:00 2001 From: christolis Date: Thu, 17 Oct 2024 23:34:24 +0300 Subject: [PATCH 4/8] refactor: make cooldown minutes check into separate method --- .../ApplicationApplyHandler.java | 21 ++++++++++--- .../ApplicationCreateCommand.java | 31 +++++++------------ 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/ApplicationApplyHandler.java b/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/ApplicationApplyHandler.java index 10f6ec37ee..84639f06b8 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/ApplicationApplyHandler.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/ApplicationApplyHandler.java @@ -37,13 +37,14 @@ public class ApplicationApplyHandler { /** * Constructs a new {@code ApplicationApplyHandler} instance. * - * @param roleApplicationSystemConfig the configuration that contains the details for the application form - * including the cooldown duration and channel pattern. + * @param roleApplicationSystemConfig the configuration that contains the details for the + * application form including the cooldown duration and channel pattern. */ public ApplicationApplyHandler(RoleApplicationSystemConfig roleApplicationSystemConfig) { this.roleApplicationSystemConfig = roleApplicationSystemConfig; this.applicationChannelPattern = - Pattern.compile(roleApplicationSystemConfig.submissionsChannelPattern()).asMatchPredicate(); + Pattern.compile(roleApplicationSystemConfig.submissionsChannelPattern()) + .asMatchPredicate(); final Duration applicationSubmitCooldownDuration = Duration.ofMinutes(roleApplicationSystemConfig.applicationSubmitCooldownMinutes()); @@ -85,8 +86,8 @@ protected void sendApplicationResult(final ModalInteractionEvent event, List args) { SelectOption selectOption = event.getSelectedOptions().getFirst(); + Member member = event.getMember(); - if (selectOption == null) { + if (selectOption == null || member == null) { return; } - OffsetDateTime timeSentCache = applicationApplyHandler.getApplicationSubmitCooldown() - .getIfPresent(event.getMember()); - if (timeSentCache != null) { - Duration duration = Duration.between(timeSentCache, OffsetDateTime.now()); - long remainingMinutes = - roleApplicationSystemConfig.applicationSubmitCooldownMinutes() - duration.toMinutes(); - - if (duration.toMinutes() < roleApplicationSystemConfig.applicationSubmitCooldownMinutes()) { - event - .reply("Please wait %d minutes before sending a new application form." - .formatted(remainingMinutes)) - .setEphemeral(true) - .queue(); - return; - } + long remainingMinutes = applicationApplyHandler.getMemberCooldownMinutes(member); + if (remainingMinutes > 0) { + event + .reply("Please wait %d minutes before sending a new application form." + .formatted(remainingMinutes)) + .setEphemeral(true) + .queue(); + return; } String questionLabel = roleApplicationSystemConfig.defaultQuestion(); @@ -151,7 +143,8 @@ public void onStringSelectSelection(StringSelectInteractionEvent event, List Date: Thu, 17 Oct 2024 23:40:31 +0300 Subject: [PATCH 5/8] feat: move substring of label into config record --- application/config.json.template | 2 +- .../tjbot/config/RoleApplicationSystemConfig.java | 5 +++++ .../features/roleapplication/ApplicationCreateCommand.java | 7 +------ 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/application/config.json.template b/application/config.json.template index 79d0d2cfea..876d30714b 100644 --- a/application/config.json.template +++ b/application/config.json.template @@ -117,7 +117,7 @@ }, "roleApplicationSystem": { "submissionsChannelPattern": "staff-applications", - "defaultQuestion": "What makes you a valuable addition to the team? 😎", + "defaultQuestion": "What makes you a good addition to the team?", "minimumAnswerLength": 50, "maximumAnswerLength": 500, "applicationSubmitCooldownMinutes": 5 diff --git a/application/src/main/java/org/togetherjava/tjbot/config/RoleApplicationSystemConfig.java b/application/src/main/java/org/togetherjava/tjbot/config/RoleApplicationSystemConfig.java index 1e3644c149..e8e20e0c85 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/RoleApplicationSystemConfig.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/RoleApplicationSystemConfig.java @@ -1,6 +1,7 @@ package org.togetherjava.tjbot.config; import com.fasterxml.jackson.annotation.JsonProperty; +import net.dv8tion.jda.api.interactions.components.text.TextInput; import java.util.Objects; @@ -26,5 +27,9 @@ public record RoleApplicationSystemConfig( public RoleApplicationSystemConfig { Objects.requireNonNull(submissionsChannelPattern); Objects.requireNonNull(defaultQuestion); + + if (defaultQuestion.length() > TextInput.MAX_LABEL_LENGTH) { + throw new IllegalArgumentException("defaultQuestion length is too long! Cannot be greater than %d".formatted(TextInput.MAX_LABEL_LENGTH)); + } } } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/ApplicationCreateCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/ApplicationCreateCommand.java index bae54a5cb5..3d7b0fea6a 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/ApplicationCreateCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/ApplicationCreateCommand.java @@ -134,13 +134,8 @@ public void onStringSelectSelection(StringSelectInteractionEvent event, List TextInput.MAX_LABEL_LENGTH) { - questionLabel = questionLabel.substring(0, TextInput.MAX_LABEL_LENGTH); - } - TextInput body = TextInput - .create(generateComponentId(event.getUser().getId()), questionLabel, + .create(generateComponentId(event.getUser().getId()), roleApplicationSystemConfig.defaultQuestion(), TextInputStyle.PARAGRAPH) .setRequired(true) .setRequiredRange(roleApplicationSystemConfig.minimumAnswerLength(), From 7b42e6a604ed6bb5143e7f67285b3cb9971cd908 Mon Sep 17 00:00:00 2001 From: christolis Date: Thu, 17 Oct 2024 23:47:39 +0300 Subject: [PATCH 6/8] docs: rewrite documentation for `RoleApplicationSystemConfig` --- .../tjbot/config/RoleApplicationSystemConfig.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/config/RoleApplicationSystemConfig.java b/application/src/main/java/org/togetherjava/tjbot/config/RoleApplicationSystemConfig.java index e8e20e0c85..1ff493e5a1 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/RoleApplicationSystemConfig.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/RoleApplicationSystemConfig.java @@ -8,6 +8,12 @@ /** * Represents the configuration for an application form, including roles and application channel * pattern. + * + * @param submissionsChannelPattern the pattern used to identify the submissions channel where applications are sent + * @param defaultQuestion the default question that will be asked in the role application form + * @param minimumAnswerLength the minimum number of characters required for the applicant's answer + * @param maximumAnswerLength the maximum number of characters allowed for the applicant's answer + * @param applicationSubmitCooldownMinutes the cooldown time in minutes before the user can submit another application */ public record RoleApplicationSystemConfig( @JsonProperty(value = "submissionsChannelPattern", @@ -20,9 +26,9 @@ public record RoleApplicationSystemConfig( /** * Constructs an instance of {@link RoleApplicationSystemConfig} with the provided parameters. - * - * @param submissionsChannelPattern the pattern used to identify the application channel - * @param defaultQuestion the default question for the form + *

+ * This constructor ensures that {@code submissionsChannelPattern} and {@code defaultQuestion} + * are not null and that the length of the {@code defaultQuestion} does not exceed the maximum allowed length. */ public RoleApplicationSystemConfig { Objects.requireNonNull(submissionsChannelPattern); From 0291813e72842441e5082925569d2f27e0227ae3 Mon Sep 17 00:00:00 2001 From: christolis Date: Thu, 17 Oct 2024 23:55:16 +0300 Subject: [PATCH 7/8] style: run spotlessApply gradle task --- .../tjbot/config/RoleApplicationSystemConfig.java | 13 +++++++++---- .../roleapplication/ApplicationApplyHandler.java | 1 - .../roleapplication/ApplicationCreateCommand.java | 8 ++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/config/RoleApplicationSystemConfig.java b/application/src/main/java/org/togetherjava/tjbot/config/RoleApplicationSystemConfig.java index 1ff493e5a1..8446484585 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/RoleApplicationSystemConfig.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/RoleApplicationSystemConfig.java @@ -9,11 +9,13 @@ * Represents the configuration for an application form, including roles and application channel * pattern. * - * @param submissionsChannelPattern the pattern used to identify the submissions channel where applications are sent + * @param submissionsChannelPattern the pattern used to identify the submissions channel where + * applications are sent * @param defaultQuestion the default question that will be asked in the role application form * @param minimumAnswerLength the minimum number of characters required for the applicant's answer * @param maximumAnswerLength the maximum number of characters allowed for the applicant's answer - * @param applicationSubmitCooldownMinutes the cooldown time in minutes before the user can submit another application + * @param applicationSubmitCooldownMinutes the cooldown time in minutes before the user can submit + * another application */ public record RoleApplicationSystemConfig( @JsonProperty(value = "submissionsChannelPattern", @@ -28,14 +30,17 @@ public record RoleApplicationSystemConfig( * Constructs an instance of {@link RoleApplicationSystemConfig} with the provided parameters. *

* This constructor ensures that {@code submissionsChannelPattern} and {@code defaultQuestion} - * are not null and that the length of the {@code defaultQuestion} does not exceed the maximum allowed length. + * are not null and that the length of the {@code defaultQuestion} does not exceed the maximum + * allowed length. */ public RoleApplicationSystemConfig { Objects.requireNonNull(submissionsChannelPattern); Objects.requireNonNull(defaultQuestion); if (defaultQuestion.length() > TextInput.MAX_LABEL_LENGTH) { - throw new IllegalArgumentException("defaultQuestion length is too long! Cannot be greater than %d".formatted(TextInput.MAX_LABEL_LENGTH)); + throw new IllegalArgumentException( + "defaultQuestion length is too long! Cannot be greater than %d" + .formatted(TextInput.MAX_LABEL_LENGTH)); } } } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/ApplicationApplyHandler.java b/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/ApplicationApplyHandler.java index 84639f06b8..0c8b9443d5 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/ApplicationApplyHandler.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/ApplicationApplyHandler.java @@ -29,7 +29,6 @@ * users to prevent spamming. */ public class ApplicationApplyHandler { - private final Cache applicationSubmitCooldown; private final Predicate applicationChannelPattern; private final RoleApplicationSystemConfig roleApplicationSystemConfig; diff --git a/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/ApplicationCreateCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/ApplicationCreateCommand.java index 3d7b0fea6a..d74a25af96 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/ApplicationCreateCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/ApplicationCreateCommand.java @@ -20,8 +20,6 @@ import net.dv8tion.jda.api.interactions.components.text.TextInput; import net.dv8tion.jda.api.interactions.components.text.TextInputStyle; import net.dv8tion.jda.api.interactions.modals.Modal; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.config.Config; import org.togetherjava.tjbot.config.RoleApplicationSystemConfig; @@ -44,8 +42,6 @@ * guild. */ public class ApplicationCreateCommand extends SlashCommandAdapter { - private static final Logger logger = LoggerFactory.getLogger(ApplicationCreateCommand.class); - protected static final Color AMBIENT_COLOR = new Color(24, 221, 136, 255); private static final int OPTIONAL_ROLES_AMOUNT = 5; private static final String ROLE_COMPONENT_ID_HEADER = "application-create"; @@ -135,8 +131,8 @@ public void onStringSelectSelection(StringSelectInteractionEvent event, List Date: Sat, 19 Oct 2024 22:43:32 +0300 Subject: [PATCH 8/8] feat(cooldown): use the proper word depending on count --- .../ApplicationCreateCommand.java | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/ApplicationCreateCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/ApplicationCreateCommand.java index d74a25af96..f0460beb1e 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/ApplicationCreateCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/roleapplication/ApplicationCreateCommand.java @@ -121,10 +121,12 @@ public void onStringSelectSelection(StringSelectInteractionEvent event, List 0) { event - .reply("Please wait %d minutes before sending a new application form." - .formatted(remainingMinutes)) + .reply("Please wait %d %s before sending a new application form." + .formatted(remainingMinutes, correctMinutesWord)) .setEphemeral(true) .queue(); return; @@ -157,6 +159,23 @@ public void onStringSelectSelection(StringSelectInteractionEvent event, List