From c4ed90da26bc58c7575537deb670bb1c15c76d7d Mon Sep 17 00:00:00 2001 From: christolis Date: Fri, 8 Mar 2024 15:55:00 +0200 Subject: [PATCH 01/14] feat(ApplicationCreateCommand): base code --- application/config.json.template | 10 + .../tjbot/config/ApplicationFormConfig.java | 17 ++ .../tjbot/config/ApplyRoleConfig.java | 15 ++ .../org/togetherjava/tjbot/config/Config.java | 15 +- .../togetherjava/tjbot/features/Features.java | 2 + .../basic/ApplicationCreateCommand.java | 226 ++++++++++++++++++ 6 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 application/src/main/java/org/togetherjava/tjbot/config/ApplicationFormConfig.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/config/ApplyRoleConfig.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java diff --git a/application/config.json.template b/application/config.json.template index a1aec8f470..e2cf86a91e 100644 --- a/application/config.json.template +++ b/application/config.json.template @@ -115,5 +115,15 @@ "fallbackChannelPattern": "java-news-and-changes", "pollIntervalInMinutes": 10 }, + "applicationForm": { + "applicationChannelPattern": "applications-log", + "roles": [ + { + "description": "Lorem ipsum", + "name": "Test role", + "formattedEmoji": ":joy:" + } + ] + } "memberCountCategoryPattern": "Info" } diff --git a/application/src/main/java/org/togetherjava/tjbot/config/ApplicationFormConfig.java b/application/src/main/java/org/togetherjava/tjbot/config/ApplicationFormConfig.java new file mode 100644 index 0000000000..791ec06f94 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/config/ApplicationFormConfig.java @@ -0,0 +1,17 @@ +package org.togetherjava.tjbot.config; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; +import java.util.Objects; + +public record ApplicationFormConfig( + @JsonProperty(value = "roles", required = true) List applyRoleConfig, + @JsonProperty(value = "applicationChannelPattern", + required = true) String applicationChannelPattern) { + + public ApplicationFormConfig { + Objects.requireNonNull(applyRoleConfig); + Objects.requireNonNull(applicationChannelPattern); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/config/ApplyRoleConfig.java b/application/src/main/java/org/togetherjava/tjbot/config/ApplyRoleConfig.java new file mode 100644 index 0000000000..2f3441ae90 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/config/ApplyRoleConfig.java @@ -0,0 +1,15 @@ +package org.togetherjava.tjbot.config; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +public record ApplyRoleConfig(@JsonProperty(value = "name", required = true) String name, + @JsonProperty(value = "description", required = true) String description, + @JsonProperty(value = "formattedEmoji") String emoji) { + + public ApplyRoleConfig { + Objects.requireNonNull(name); + Objects.requireNonNull(description); + } +} 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..42dd46fdaa 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/Config.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/Config.java @@ -45,6 +45,7 @@ public final class Config { private final FeatureBlacklistConfig featureBlacklistConfig; private final RSSFeedsConfig rssFeedsConfig; private final String selectRolesChannelPattern; + private final ApplicationFormConfig applicationFormConfig; private final String memberCountCategoryPattern; @SuppressWarnings("ConstructorWithTooManyParameters") @@ -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 = "applicationForm", + required = true) ApplicationFormConfig applicationFormConfig) { 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.applicationFormConfig = applicationFormConfig; } /** @@ -401,6 +405,15 @@ public String getSelectRolesChannelPattern() { return selectRolesChannelPattern; } + /** + * The configuration related to the application form. + * + * @return the application form config + */ + public ApplicationFormConfig getApplicationFormConfig() { + return applicationFormConfig; + } + /** * Gets the pattern matching the category that is used to display the total member count. * 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..f0e4702577 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/Features.java @@ -6,6 +6,7 @@ import org.togetherjava.tjbot.config.FeatureBlacklist; import org.togetherjava.tjbot.config.FeatureBlacklistConfig; import org.togetherjava.tjbot.db.Database; +import org.togetherjava.tjbot.features.basic.ApplicationCreateCommand; import org.togetherjava.tjbot.features.basic.MemberCountDisplayRoutine; import org.togetherjava.tjbot.features.basic.PingCommand; import org.togetherjava.tjbot.features.basic.RoleSelectCommand; @@ -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/basic/ApplicationCreateCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java new file mode 100644 index 0000000000..2ac445452c --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java @@ -0,0 +1,226 @@ +package org.togetherjava.tjbot.features.basic; + +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.User; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +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.ButtonInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.SelectMenuInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.CommandInteraction; +import net.dv8tion.jda.api.interactions.components.ActionRow; +import net.dv8tion.jda.api.interactions.components.Modal; +import net.dv8tion.jda.api.interactions.components.buttons.Button; +import net.dv8tion.jda.api.interactions.components.selections.SelectMenu; +import net.dv8tion.jda.api.interactions.components.selections.SelectOption; +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.ModalMapping; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.togetherjava.tjbot.config.ApplicationFormConfig; +import org.togetherjava.tjbot.config.ApplyRoleConfig; +import org.togetherjava.tjbot.config.Config; +import org.togetherjava.tjbot.features.CommandVisibility; +import org.togetherjava.tjbot.features.SlashCommandAdapter; +import org.togetherjava.tjbot.features.componentids.Lifespan; + +import java.awt.Color; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +public class ApplicationCreateCommand extends SlashCommandAdapter { + private static final Logger logger = LoggerFactory.getLogger(ApplicationCreateCommand.class); + + private static final Color AMBIENT_COLOR = new Color(24, 221, 136, 255); + private static final int MIN_REASON_LENGTH = 50; + private static final int MAX_REASON_LENGTH = 500; + private static final String DEFAULT_QUESTION = + "What makes you a valuable addition to the team? 😎"; + private final Predicate applicationChannelPattern; + private final ApplicationFormConfig config; + + public ApplicationCreateCommand(Config config) { + super("application-form", "Generates an application form for members to apply for roles.", + CommandVisibility.GUILD); + + this.config = config.getApplicationFormConfig(); + this.applicationChannelPattern = + Pattern.compile(this.config.applicationChannelPattern()).asMatchPredicate(); + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event) { + if (!handleHasPermissions(event)) { + return; + } + + sendMenu(event); + } + + @Override + public void onButtonClick(ButtonInteractionEvent event, List args) { + User user = event.getUser(); + SelectMenu.Builder menu = + SelectMenu.create(generateComponentId(Lifespan.REGULAR, event.getUser().getId())) + .setPlaceholder("Select role to apply for"); + + config.applyRoleConfig() + .stream() + .map(option -> mapToSelectOption(user, option)) + .forEach(menu::addOptions); + + event.reply("").addActionRow(menu.build()).setEphemeral(true).queue(); + } + + private SelectOption mapToSelectOption(User user, ApplyRoleConfig option) { + return SelectOption.of(option.name(), generateComponentId(user.getId(), option.name())) + .withDescription(option.description()) + .withEmoji(Emoji.fromFormatted(option.emoji())); + } + + @Override + public void onSelectMenuSelection(SelectMenuInteractionEvent event, List args) { + SelectOption selectOption = event.getSelectedOptions().getFirst(); + + if (selectOption == null) { + return; + } + + TextInput body = TextInput + .create(generateComponentId(event.getUser().getId()), "Question", + TextInputStyle.PARAGRAPH) + .setRequired(true) + .setRequiredRange(MIN_REASON_LENGTH, MAX_REASON_LENGTH) + .setPlaceholder(DEFAULT_QUESTION) + .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.getHook().deleteOriginal().queue(); + event.replyModal(modal).queue(); + } + + @Override + public void onModalSubmitted(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(); + } + + 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(); + } + + private boolean handleHasPermissions(SlashCommandInteractionEvent event) { + Member member = event.getMember(); + + if (member == null) { + return false; + } + + if (!event.getMember().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 = event.getGuild().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; + } + + private 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(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(DEFAULT_QUESTION, answer, false); + embed.addField(answerField); + + applicationChannel.get().sendMessageEmbeds(embed.build()).queue(); + } + + private void sendMenu(final CommandInteraction event) { + MessageEmbed embed = createApplicationEmbed(); + + String buttonComponentId = generateComponentId(Lifespan.PERMANENT, event.getUser().getId()); + Button button = Button.primary(buttonComponentId, "Check openings"); + + event.replyEmbeds(embed).addActionRow(button).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(); + } +} From 23dc6b8605c6ada402b48dd0287237930d01be10 Mon Sep 17 00:00:00 2001 From: christolis Date: Sat, 9 Mar 2024 02:29:31 +0200 Subject: [PATCH 02/14] style: add JavaDocs where necessary --- .../tjbot/config/ApplicationFormConfig.java | 11 +++++ .../tjbot/config/ApplyRoleConfig.java | 10 ++++ .../basic/ApplicationCreateCommand.java | 47 +++++++++++++++++++ 3 files changed, 68 insertions(+) diff --git a/application/src/main/java/org/togetherjava/tjbot/config/ApplicationFormConfig.java b/application/src/main/java/org/togetherjava/tjbot/config/ApplicationFormConfig.java index 791ec06f94..e0216735b0 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/ApplicationFormConfig.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/ApplicationFormConfig.java @@ -5,11 +5,22 @@ import java.util.List; import java.util.Objects; +/** + * Represents the configuration for an application form, including roles and application channel + * pattern. + */ public record ApplicationFormConfig( @JsonProperty(value = "roles", required = true) List applyRoleConfig, @JsonProperty(value = "applicationChannelPattern", required = true) String applicationChannelPattern) { + /** + * Constructs an instance of {@link ApplicationFormConfig} with the provided parameters. + * + * @param applyRoleConfig the list of ApplyRoleConfig objects defining roles for the application + * form + * @param applicationChannelPattern the pattern used to identify the application channel + */ public ApplicationFormConfig { Objects.requireNonNull(applyRoleConfig); Objects.requireNonNull(applicationChannelPattern); diff --git a/application/src/main/java/org/togetherjava/tjbot/config/ApplyRoleConfig.java b/application/src/main/java/org/togetherjava/tjbot/config/ApplyRoleConfig.java index 2f3441ae90..14128eee0f 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/ApplyRoleConfig.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/ApplyRoleConfig.java @@ -4,10 +4,20 @@ import java.util.Objects; +/** + * Represents the configuration for applying a role. + */ public record ApplyRoleConfig(@JsonProperty(value = "name", required = true) String name, @JsonProperty(value = "description", required = true) String description, @JsonProperty(value = "formattedEmoji") String emoji) { + /** + * Constructs an instance of ApplyRoleConfig with the given parameters. + * + * @param name the name of the role + * @param description the description of the role + * @param emoji the emoji associated with the role + */ public ApplyRoleConfig { Objects.requireNonNull(name); Objects.requireNonNull(description); diff --git a/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java index 2ac445452c..e29185a82a 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java @@ -39,6 +39,12 @@ import java.util.function.Predicate; import java.util.regex.Pattern; +/** + * 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); @@ -50,6 +56,13 @@ public class ApplicationCreateCommand extends SlashCommandAdapter { private final Predicate applicationChannelPattern; private final ApplicationFormConfig config; + /** + * Constructs a new {@code 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); @@ -83,6 +96,17 @@ public void onButtonClick(ButtonInteractionEvent event, List args) { event.reply("").addActionRow(menu.build()).setEphemeral(true).queue(); } + /** + * Maps a user and an {@link ApplyRoleConfig} option to a SelectOption object. + *

+ * This method is used to create a SelectOption object that represents a role configuration + * option for a user, including a unique component ID generated based on the user's ID and the + * option's name, a description, and an emoji. + * + * @param user the user for whom the role configuration option is being mapped + * @param option the {@link ApplyRoleConfig} option to be mapped to a SelectOption + * @return a {@link SelectOption} object with the specified details + */ private SelectOption mapToSelectOption(User user, ApplyRoleConfig option) { return SelectOption.of(option.name(), generateComponentId(user.getId(), option.name())) .withDescription(option.description()) @@ -140,6 +164,13 @@ public void onModalSubmitted(ModalInteractionEvent event, List args) { .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 {@code 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() @@ -176,6 +207,17 @@ private boolean handleHasPermissions(SlashCommandInteractionEvent event) { return true; } + /** + * 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 + */ private void sendApplicationResult(final ModalInteractionEvent event, List args, String answer) { Guild guild = event.getGuild(); @@ -205,6 +247,11 @@ private void sendApplicationResult(final ModalInteractionEvent event, List Date: Sat, 9 Mar 2024 12:02:50 +0200 Subject: [PATCH 03/14] feat: add application submit cooldown --- .../basic/ApplicationCreateCommand.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java index e29185a82a..54993cb1b9 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java @@ -1,5 +1,7 @@ package org.togetherjava.tjbot.features.basic; +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.Permission; import net.dv8tion.jda.api.entities.Guild; @@ -33,9 +35,12 @@ import org.togetherjava.tjbot.features.componentids.Lifespan; import java.awt.Color; +import java.time.Duration; import java.time.Instant; +import java.time.OffsetDateTime; import java.util.List; import java.util.Optional; +import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import java.util.regex.Pattern; @@ -51,8 +56,11 @@ public class ApplicationCreateCommand extends SlashCommandAdapter { private static final Color AMBIENT_COLOR = new Color(24, 221, 136, 255); private static final int MIN_REASON_LENGTH = 50; private static final int MAX_REASON_LENGTH = 500; + private static final int APPLICATION_SUBMIT_COOLDOWN = 5; private static final String DEFAULT_QUESTION = "What makes you a valuable addition to the team? 😎"; + + private final Cache applicationSubmitCooldown; private final Predicate applicationChannelPattern; private final ApplicationFormConfig config; @@ -70,6 +78,10 @@ public ApplicationCreateCommand(Config config) { this.config = config.getApplicationFormConfig(); this.applicationChannelPattern = Pattern.compile(this.config.applicationChannelPattern()).asMatchPredicate(); + + this.applicationSubmitCooldown = Caffeine.newBuilder() + .expireAfterWrite(APPLICATION_SUBMIT_COOLDOWN, TimeUnit.MINUTES) + .build(); } @Override @@ -121,6 +133,18 @@ public void onSelectMenuSelection(SelectMenuInteractionEvent event, List return; } + OffsetDateTime timeSentCache = applicationSubmitCooldown.getIfPresent(event.getMember()); + if (timeSentCache != null) { + Duration duration = Duration.between(timeSentCache, OffsetDateTime.now()); + + if (duration.toMinutes() < APPLICATION_SUBMIT_COOLDOWN) { + event.reply("Please wait before sending a new application form.") + .setEphemeral(true) + .queue(); + return; + } + } + TextInput body = TextInput .create(generateComponentId(event.getUser().getId()), "Question", TextInputStyle.PARAGRAPH) @@ -162,6 +186,8 @@ public void onModalSubmitted(ModalInteractionEvent event, List args) { event.reply("Your application has been submitted. Thank you for applying! 😎") .setEphemeral(true) .queue(); + + applicationSubmitCooldown.put(event.getMember(), OffsetDateTime.now()); } /** From c07d87b2fac3411832c9cf97a25e219abc554706 Mon Sep 17 00:00:00 2001 From: christolis Date: Sat, 9 Mar 2024 12:09:54 +0200 Subject: [PATCH 04/14] fix: handle possible NPEs --- .../tjbot/features/basic/ApplicationCreateCommand.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java index 54993cb1b9..f20af0edd4 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java @@ -208,19 +208,20 @@ private Optional getApplicationChannel(Guild guild) { private boolean handleHasPermissions(SlashCommandInteractionEvent event) { Member member = event.getMember(); + Guild guild = event.getGuild(); - if (member == null) { + if (member == null || guild == null) { return false; } - if (!event.getMember().hasPermission(Permission.MANAGE_ROLES)) { + 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 = event.getGuild().getSelfMember(); + 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.") From e10bdec179ce9473de03704b8498611f87210c04 Mon Sep 17 00:00:00 2001 From: christolis Date: Sat, 9 Mar 2024 12:16:41 +0200 Subject: [PATCH 05/14] docs: fix some JavaDocs --- .../features/basic/ApplicationCreateCommand.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java index f20af0edd4..54ffdd1777 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java @@ -65,7 +65,7 @@ public class ApplicationCreateCommand extends SlashCommandAdapter { private final ApplicationFormConfig config; /** - * Constructs a new {@code ApplicationCreateCommand} with the specified configuration. + * Constructs a new {@link ApplicationCreateCommand} with the specified configuration. *

* This command is designed to generate an application form for members to apply for roles. * @@ -111,12 +111,12 @@ public void onButtonClick(ButtonInteractionEvent event, List args) { /** * Maps a user and an {@link ApplyRoleConfig} option to a SelectOption object. *

- * This method is used to create a SelectOption object that represents a role configuration - * option for a user, including a unique component ID generated based on the user's ID and the - * option's name, a description, and an emoji. + * This method is used to create a {@link SelectOption} object that represents a role + * configuration option for a user, including a unique component ID generated based on the + * user's ID and the option's name, a description, and an emoji. * * @param user the user for whom the role configuration option is being mapped - * @param option the {@link ApplyRoleConfig} option to be mapped to a SelectOption + * @param option the {@link ApplyRoleConfig} option to be mapped to a {@link SelectOption} * @return a {@link SelectOption} object with the specified details */ private SelectOption mapToSelectOption(User user, ApplyRoleConfig option) { @@ -194,7 +194,7 @@ public void onModalSubmitted(ModalInteractionEvent event, List args) { * 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 {@code TextChannel} representing the application + * @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) { From 0be12caa472f0217f935b4cbc59d636042a4e945 Mon Sep 17 00:00:00 2001 From: christolis Date: Wed, 13 Mar 2024 17:36:05 +0200 Subject: [PATCH 06/14] resolve merge conflicts --- .../org/togetherjava/tjbot/config/Config.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) 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 42dd46fdaa..f00a298318 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/Config.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/Config.java @@ -45,8 +45,8 @@ public final class Config { private final FeatureBlacklistConfig featureBlacklistConfig; private final RSSFeedsConfig rssFeedsConfig; private final String selectRolesChannelPattern; - private final ApplicationFormConfig applicationFormConfig; private final String memberCountCategoryPattern; + private final ApplicationFormConfig applicationFormConfig; @SuppressWarnings("ConstructorWithTooManyParameters") @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) @@ -406,21 +406,21 @@ public String getSelectRolesChannelPattern() { } /** - * The configuration related to the application form. + * Gets the pattern matching the category that is used to display the total member count. * - * @return the application form config + * @return the categories name types */ - public ApplicationFormConfig getApplicationFormConfig() { - return applicationFormConfig; + public String getMemberCountCategoryPattern() { + return memberCountCategoryPattern; } /** - * Gets the pattern matching the category that is used to display the total member count. + * The configuration related to the application form. * - * @return the categories name types + * @return the application form config */ - public String getMemberCountCategoryPattern() { - return memberCountCategoryPattern; + public ApplicationFormConfig getApplicationFormConfig() { + return applicationFormConfig; } /** From 46973c663b9d66f5f0a492822d175f8c63525c29 Mon Sep 17 00:00:00 2001 From: christolis Date: Thu, 4 Apr 2024 15:49:29 +0300 Subject: [PATCH 07/14] feat: use latest JDA API --- .../features/basic/ApplicationCreateCommand.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java index 54ffdd1777..3961a93980 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java @@ -14,15 +14,15 @@ 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.ButtonInteractionEvent; -import net.dv8tion.jda.api.events.interaction.component.SelectMenuInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent; import net.dv8tion.jda.api.interactions.commands.CommandInteraction; import net.dv8tion.jda.api.interactions.components.ActionRow; -import net.dv8tion.jda.api.interactions.components.Modal; import net.dv8tion.jda.api.interactions.components.buttons.Button; -import net.dv8tion.jda.api.interactions.components.selections.SelectMenu; 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 net.dv8tion.jda.api.interactions.modals.ModalMapping; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -96,9 +96,9 @@ public void onSlashCommand(SlashCommandInteractionEvent event) { @Override public void onButtonClick(ButtonInteractionEvent event, List args) { User user = event.getUser(); - SelectMenu.Builder menu = - SelectMenu.create(generateComponentId(Lifespan.REGULAR, event.getUser().getId())) - .setPlaceholder("Select role to apply for"); + StringSelectMenu.Builder menu = StringSelectMenu + .create(generateComponentId(Lifespan.REGULAR, event.getUser().getId())) + .setPlaceholder("Select role to apply for"); config.applyRoleConfig() .stream() @@ -126,7 +126,7 @@ private SelectOption mapToSelectOption(User user, ApplyRoleConfig option) { } @Override - public void onSelectMenuSelection(SelectMenuInteractionEvent event, List args) { + public void onStringSelectSelection(StringSelectInteractionEvent event, List args) { SelectOption selectOption = event.getSelectedOptions().getFirst(); if (selectOption == null) { From 557d92a4421695fb3b6d16346cbd11c319a47778 Mon Sep 17 00:00:00 2001 From: christolis Date: Thu, 4 Apr 2024 15:56:14 +0300 Subject: [PATCH 08/14] fix: use proper formatted emoji in config.json.template --- application/config.json.template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/config.json.template b/application/config.json.template index e2cf86a91e..e29f260c6f 100644 --- a/application/config.json.template +++ b/application/config.json.template @@ -121,7 +121,7 @@ { "description": "Lorem ipsum", "name": "Test role", - "formattedEmoji": ":joy:" + "formattedEmoji": "🔥" } ] } From e0b925f79bfb806c082e420f44091886231b7ae4 Mon Sep 17 00:00:00 2001 From: christolis Date: Thu, 4 Apr 2024 23:18:39 +0300 Subject: [PATCH 09/14] feat: infer roles from command parameters After a discussion with the rest of the team, we decided that there are plenty of flaws with having application roles stored in the configuration file, primarily due to the fact that if somebody were to want to change the roles displayed on the dropdown menu, they would have to perform the tedious work of updating the configuration and then restarting the server, but we can do better. With this commit, all the application roles are inferred straight from the arguments that are passed from the member executing the command. A restart will not be needed in case somebody wants to change the available roles or add a new one. --- application/config.json.template | 9 +- .../tjbot/config/ApplicationFormConfig.java | 10 +- .../basic/ApplicationCreateCommand.java | 159 +++++++++++++++--- 3 files changed, 142 insertions(+), 36 deletions(-) diff --git a/application/config.json.template b/application/config.json.template index e29f260c6f..6c92cb1cbf 100644 --- a/application/config.json.template +++ b/application/config.json.template @@ -116,14 +116,7 @@ "pollIntervalInMinutes": 10 }, "applicationForm": { - "applicationChannelPattern": "applications-log", - "roles": [ - { - "description": "Lorem ipsum", - "name": "Test role", - "formattedEmoji": "🔥" - } - ] + "applicationChannelPattern": "applications-log" } "memberCountCategoryPattern": "Info" } diff --git a/application/src/main/java/org/togetherjava/tjbot/config/ApplicationFormConfig.java b/application/src/main/java/org/togetherjava/tjbot/config/ApplicationFormConfig.java index e0216735b0..b12c5df418 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/ApplicationFormConfig.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/ApplicationFormConfig.java @@ -2,27 +2,21 @@ import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.List; import java.util.Objects; /** * Represents the configuration for an application form, including roles and application channel * pattern. */ -public record ApplicationFormConfig( - @JsonProperty(value = "roles", required = true) List applyRoleConfig, - @JsonProperty(value = "applicationChannelPattern", - required = true) String applicationChannelPattern) { +public record ApplicationFormConfig(@JsonProperty(value = "applicationChannelPattern", + required = true) String applicationChannelPattern) { /** * Constructs an instance of {@link ApplicationFormConfig} with the provided parameters. * - * @param applyRoleConfig the list of ApplyRoleConfig objects defining roles for the application - * form * @param applicationChannelPattern the pattern used to identify the application channel */ public ApplicationFormConfig { - Objects.requireNonNull(applyRoleConfig); Objects.requireNonNull(applicationChannelPattern); } } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java index 3961a93980..ad0a86bc0a 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java @@ -13,11 +13,12 @@ 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.ButtonInteractionEvent; 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.buttons.Button; 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; @@ -38,11 +39,14 @@ import java.time.Duration; import java.time.Instant; import java.time.OffsetDateTime; +import java.util.HashMap; 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.regex.Pattern; +import java.util.stream.IntStream; /** * Represents a command to create an application form for members to apply for roles. @@ -59,6 +63,8 @@ public class ApplicationCreateCommand extends SlashCommandAdapter { private static final int APPLICATION_SUBMIT_COOLDOWN = 5; private static final String DEFAULT_QUESTION = "What makes you a valuable addition to the team? 😎"; + private static final int OPTIONAL_ROLES_AMOUNT = 5; + private static final String ROLE_COMPONENT_ID_HEADER = "application-create"; private final Cache applicationSubmitCooldown; private final Predicate applicationChannelPattern; @@ -82,6 +88,24 @@ public ApplicationCreateCommand(Config config) { this.applicationSubmitCooldown = Caffeine.newBuilder() .expireAfterWrite(APPLICATION_SUBMIT_COOLDOWN, TimeUnit.MINUTES) .build(); + + generateRoleOptions(getData()); + } + + /** + * Populates a {@link SlashCommandData} object with the proper arguments. + * + * @param data the object to populate + */ + private void generateRoleOptions(SlashCommandData data) { + IntStream.range(0, OPTIONAL_ROLES_AMOUNT).forEach(index -> { + int renderNumber = index + 1; + + data.addOption(OptionType.STRING, "title" + renderNumber, "The title of the role"); + data.addOption(OptionType.STRING, "description" + renderNumber, + "The description of the role"); + data.addOption(OptionType.STRING, "emoji" + renderNumber, "The emoji of the role"); + }); } @Override @@ -90,22 +114,15 @@ public void onSlashCommand(SlashCommandInteractionEvent event) { return; } - sendMenu(event); - } - - @Override - public void onButtonClick(ButtonInteractionEvent event, List args) { - User user = event.getUser(); - StringSelectMenu.Builder menu = StringSelectMenu - .create(generateComponentId(Lifespan.REGULAR, event.getUser().getId())) - .setPlaceholder("Select role to apply for"); - - config.applyRoleConfig() - .stream() - .map(option -> mapToSelectOption(user, option)) - .forEach(menu::addOptions); + long incorrectArgsCount = getIncorrectRoleArgsCount(event.getInteraction().getOptions()); + if (incorrectArgsCount > 0) { + event.reply("Missing information for %d roles.".formatted(incorrectArgsCount)) + .setEphemeral(true) + .queue(); + return; + } - event.reply("").addActionRow(menu.build()).setEphemeral(true).queue(); + sendMenu(event); } /** @@ -172,6 +189,58 @@ public void onStringSelectSelection(StringSelectInteractionEvent event, List args) { + final Map frequencyMap = new HashMap<>(); + + args.stream() + .map(OptionMapping::getName) + .map(name -> name.charAt(name.length() - 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<>(); + + args.forEach(arg -> { + final String name = arg.getName(); + final String argValue = arg.getAsString(); + final char roleId = name.charAt(name.length() - 1); + MenuRole role = roles.computeIfAbsent(roleId, k -> new MenuRole()); + + if (name.startsWith("title")) { + String value = generateComponentId(ROLE_COMPONENT_ID_HEADER, argValue); + + role.setValue(value); + role.setLabel(argValue); + } else if (name.startsWith("description")) { + role.setDescription(argValue); + } else if (name.startsWith("emoji")) { + role.setEmoji(Emoji.fromFormatted(argValue)); + } + }); + + roles.values().forEach(role -> { + menuBuilder.addOption(role.getLabel(), role.getValue(), role.getDescription(), + role.getEmoji()); + }); + } + @Override public void onModalSubmitted(ModalInteractionEvent event, List args) { Guild guild = event.getGuild(); @@ -282,10 +351,14 @@ private void sendApplicationResult(final ModalInteractionEvent event, List + * 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 static class MenuRole { + private String label; + private String value; + private String description; + private Emoji emoji; + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Emoji getEmoji() { + return emoji; + } + + public void setEmoji(Emoji emoji) { + this.emoji = emoji; + } + } } From 54ee1b0604a11fc5c1165a8fc035169859f4ae64 Mon Sep 17 00:00:00 2001 From: christolis Date: Fri, 5 Apr 2024 00:24:13 +0300 Subject: [PATCH 10/14] feat: add delimiter in dropdown menu option IDs This way we are able to add more roles in the future without running into any character limits. Getting the last character of the menu option ID is not the most effective way to go about this. Adding the delimiter in the menu option IDs allows us to treat the ID of the role better and in a safer way. --- .../basic/ApplicationCreateCommand.java | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java index ad0a86bc0a..16ae33384a 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java @@ -65,6 +65,7 @@ public class ApplicationCreateCommand extends SlashCommandAdapter { "What makes you a valuable addition to the team? 😎"; 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 final Cache applicationSubmitCooldown; private final Predicate applicationChannelPattern; @@ -101,13 +102,19 @@ private void generateRoleOptions(SlashCommandData data) { IntStream.range(0, OPTIONAL_ROLES_AMOUNT).forEach(index -> { int renderNumber = index + 1; - data.addOption(OptionType.STRING, "title" + renderNumber, "The title of the role"); - data.addOption(OptionType.STRING, "description" + renderNumber, + data.addOption(OptionType.STRING, generateOptionId("title", renderNumber), + "The title of the role"); + data.addOption(OptionType.STRING, generateOptionId("description", renderNumber), "The description of the role"); - data.addOption(OptionType.STRING, "emoji" + renderNumber, "The emoji of the role"); + data.addOption(OptionType.STRING, generateOptionId("emoji", renderNumber), + "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)) { @@ -197,11 +204,11 @@ public void onStringSelectSelection(StringSelectInteractionEvent event, List args) { - final Map frequencyMap = new HashMap<>(); + final Map frequencyMap = new HashMap<>(); args.stream() .map(OptionMapping::getName) - .map(name -> name.charAt(name.length() - 1)) + .map(name -> name.split(VALUE_DELIMITER)[1]) .forEach(number -> frequencyMap.merge(number, 1, Integer::sum)); return frequencyMap.values().stream().filter(value -> value != 3).count(); @@ -215,12 +222,12 @@ private static long getIncorrectRoleArgsCount(final List args) { */ private void addRolesToMenu(StringSelectMenu.Builder menuBuilder, final List args) { - final Map roles = new HashMap<>(); + final Map roles = new HashMap<>(); args.forEach(arg -> { final String name = arg.getName(); final String argValue = arg.getAsString(); - final char roleId = name.charAt(name.length() - 1); + final String roleId = name.split(VALUE_DELIMITER)[1]; MenuRole role = roles.computeIfAbsent(roleId, k -> new MenuRole()); if (name.startsWith("title")) { From 5be1e1559ed0499bb9154a4221474cbd95c69458 Mon Sep 17 00:00:00 2001 From: christolis Date: Fri, 5 Apr 2024 01:11:51 +0300 Subject: [PATCH 11/14] fix: sonarlint errors --- .../basic/ApplicationCreateCommand.java | 30 ++++--------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java index 16ae33384a..62594f65d6 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java @@ -29,7 +29,6 @@ import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.config.ApplicationFormConfig; -import org.togetherjava.tjbot.config.ApplyRoleConfig; import org.togetherjava.tjbot.config.Config; import org.togetherjava.tjbot.features.CommandVisibility; import org.togetherjava.tjbot.features.SlashCommandAdapter; @@ -69,7 +68,6 @@ public class ApplicationCreateCommand extends SlashCommandAdapter { private final Cache applicationSubmitCooldown; private final Predicate applicationChannelPattern; - private final ApplicationFormConfig config; /** * Constructs a new {@link ApplicationCreateCommand} with the specified configuration. @@ -82,9 +80,9 @@ public ApplicationCreateCommand(Config config) { super("application-form", "Generates an application form for members to apply for roles.", CommandVisibility.GUILD); - this.config = config.getApplicationFormConfig(); + final ApplicationFormConfig formConfig = config.getApplicationFormConfig(); this.applicationChannelPattern = - Pattern.compile(this.config.applicationChannelPattern()).asMatchPredicate(); + Pattern.compile(formConfig.applicationChannelPattern()).asMatchPredicate(); this.applicationSubmitCooldown = Caffeine.newBuilder() .expireAfterWrite(APPLICATION_SUBMIT_COOLDOWN, TimeUnit.MINUTES) @@ -132,23 +130,6 @@ public void onSlashCommand(SlashCommandInteractionEvent event) { sendMenu(event); } - /** - * Maps a user and an {@link ApplyRoleConfig} option to a SelectOption object. - *

- * This method is used to create a {@link SelectOption} object that represents a role - * configuration option for a user, including a unique component ID generated based on the - * user's ID and the option's name, a description, and an emoji. - * - * @param user the user for whom the role configuration option is being mapped - * @param option the {@link ApplyRoleConfig} option to be mapped to a {@link SelectOption} - * @return a {@link SelectOption} object with the specified details - */ - private SelectOption mapToSelectOption(User user, ApplyRoleConfig option) { - return SelectOption.of(option.name(), generateComponentId(user.getId(), option.name())) - .withDescription(option.description()) - .withEmoji(Emoji.fromFormatted(option.emoji())); - } - @Override public void onStringSelectSelection(StringSelectInteractionEvent event, List args) { SelectOption selectOption = event.getSelectedOptions().getFirst(); @@ -242,10 +223,9 @@ private void addRolesToMenu(StringSelectMenu.Builder menuBuilder, } }); - roles.values().forEach(role -> { - menuBuilder.addOption(role.getLabel(), role.getValue(), role.getDescription(), - role.getEmoji()); - }); + roles.values() + .forEach(role -> menuBuilder.addOption(role.getLabel(), role.getValue(), + role.getDescription(), role.getEmoji())); } @Override From 29c8ee6df73f8e15b8ce650a49f9a9c7b1f1e3fd Mon Sep 17 00:00:00 2001 From: christolis Date: Mon, 20 May 2024 21:26:21 +0300 Subject: [PATCH 12/14] fix: do not delete application form once used We want members to use the same application form when applying for roles instead of removing it once it gets used. The command to generate a form should be used once. --- .../tjbot/features/basic/ApplicationCreateCommand.java | 1 - 1 file changed, 1 deletion(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java index 62594f65d6..23e53f9c5a 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java @@ -173,7 +173,6 @@ public void onStringSelectSelection(StringSelectInteractionEvent event, List Date: Sun, 6 Oct 2024 15:48:17 +0300 Subject: [PATCH 13/14] fix(application-form): give menu permanent lifespan Since application menus where users are going to apply from are going to be permanent, we need their component IDs to not expire by the cache. Previously there was a problem while testing where the already-existing menus would expire and the Discord client would complain. Refs: #1024 --- .../tjbot/features/basic/ApplicationCreateCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java index 23e53f9c5a..fd202ed880 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java @@ -338,7 +338,7 @@ private void sendMenu(final CommandInteraction event) { MessageEmbed embed = createApplicationEmbed(); StringSelectMenu.Builder menuBuilder = StringSelectMenu - .create(generateComponentId(Lifespan.REGULAR, event.getUser().getId())) + .create(generateComponentId(Lifespan.PERMANENT, event.getUser().getId())) .setPlaceholder("Select role to apply for") .setRequiredRange(1, 1); From 304fc66a286c94b828d883a6c8a86606cd66727b Mon Sep 17 00:00:00 2001 From: christolis Date: Sun, 6 Oct 2024 15:54:49 +0300 Subject: [PATCH 14/14] fix(application-form): handle no options provided Users with the right permissions could create a menu with no roles, and that would cause an error. Refs: #1024 --- .../tjbot/features/basic/ApplicationCreateCommand.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java index fd202ed880..f5dfe9d3e8 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java @@ -119,7 +119,13 @@ public void onSlashCommand(SlashCommandInteractionEvent event) { return; } - long incorrectArgsCount = getIncorrectRoleArgsCount(event.getInteraction().getOptions()); + 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)