Skip to content

Commit 797340a

Browse files
committed
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.
1 parent c027a57 commit 797340a

File tree

3 files changed

+142
-36
lines changed

3 files changed

+142
-36
lines changed

application/config.json.template

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -104,14 +104,7 @@
104104
]
105105
},
106106
"applicationForm": {
107-
"applicationChannelPattern": "applications-log",
108-
"roles": [
109-
{
110-
"description": "Lorem ipsum",
111-
"name": "Test role",
112-
"formattedEmoji": "🔥"
113-
}
114-
]
107+
"applicationChannelPattern": "applications-log"
115108
}
116109
"selectRolesChannelPattern": "select-your-roles",
117110
"memberCountCategoryPattern": "Info"

application/src/main/java/org/togetherjava/tjbot/config/ApplicationFormConfig.java

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,21 @@
22

33
import com.fasterxml.jackson.annotation.JsonProperty;
44

5-
import java.util.List;
65
import java.util.Objects;
76

87
/**
98
* Represents the configuration for an application form, including roles and application channel
109
* pattern.
1110
*/
12-
public record ApplicationFormConfig(
13-
@JsonProperty(value = "roles", required = true) List<ApplyRoleConfig> applyRoleConfig,
14-
@JsonProperty(value = "applicationChannelPattern",
15-
required = true) String applicationChannelPattern) {
11+
public record ApplicationFormConfig(@JsonProperty(value = "applicationChannelPattern",
12+
required = true) String applicationChannelPattern) {
1613

1714
/**
1815
* Constructs an instance of {@link ApplicationFormConfig} with the provided parameters.
1916
*
20-
* @param applyRoleConfig the list of ApplyRoleConfig objects defining roles for the application
21-
* form
2217
* @param applicationChannelPattern the pattern used to identify the application channel
2318
*/
2419
public ApplicationFormConfig {
25-
Objects.requireNonNull(applyRoleConfig);
2620
Objects.requireNonNull(applicationChannelPattern);
2721
}
2822
}

application/src/main/java/org/togetherjava/tjbot/features/basic/ApplicationCreateCommand.java

Lines changed: 139 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@
1313
import net.dv8tion.jda.api.entities.emoji.EmojiUnion;
1414
import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent;
1515
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
16-
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
1716
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
1817
import net.dv8tion.jda.api.interactions.commands.CommandInteraction;
18+
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
19+
import net.dv8tion.jda.api.interactions.commands.OptionType;
20+
import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData;
1921
import net.dv8tion.jda.api.interactions.components.ActionRow;
20-
import net.dv8tion.jda.api.interactions.components.buttons.Button;
2122
import net.dv8tion.jda.api.interactions.components.selections.SelectOption;
2223
import net.dv8tion.jda.api.interactions.components.selections.StringSelectMenu;
2324
import net.dv8tion.jda.api.interactions.components.text.TextInput;
@@ -38,11 +39,14 @@
3839
import java.time.Duration;
3940
import java.time.Instant;
4041
import java.time.OffsetDateTime;
42+
import java.util.HashMap;
4143
import java.util.List;
44+
import java.util.Map;
4245
import java.util.Optional;
4346
import java.util.concurrent.TimeUnit;
4447
import java.util.function.Predicate;
4548
import java.util.regex.Pattern;
49+
import java.util.stream.IntStream;
4650

4751
/**
4852
* Represents a command to create an application form for members to apply for roles.
@@ -59,6 +63,8 @@ public class ApplicationCreateCommand extends SlashCommandAdapter {
5963
private static final int APPLICATION_SUBMIT_COOLDOWN = 5;
6064
private static final String DEFAULT_QUESTION =
6165
"What makes you a valuable addition to the team? 😎";
66+
private static final int OPTIONAL_ROLES_AMOUNT = 5;
67+
private static final String ROLE_COMPONENT_ID_HEADER = "application-create";
6268

6369
private final Cache<Member, OffsetDateTime> applicationSubmitCooldown;
6470
private final Predicate<String> applicationChannelPattern;
@@ -82,6 +88,24 @@ public ApplicationCreateCommand(Config config) {
8288
this.applicationSubmitCooldown = Caffeine.newBuilder()
8389
.expireAfterWrite(APPLICATION_SUBMIT_COOLDOWN, TimeUnit.MINUTES)
8490
.build();
91+
92+
generateRoleOptions(getData());
93+
}
94+
95+
/**
96+
* Populates a {@link SlashCommandData} object with the proper arguments.
97+
*
98+
* @param data the object to populate
99+
*/
100+
private void generateRoleOptions(SlashCommandData data) {
101+
IntStream.range(0, OPTIONAL_ROLES_AMOUNT).forEach(index -> {
102+
int renderNumber = index + 1;
103+
104+
data.addOption(OptionType.STRING, "title" + renderNumber, "The title of the role");
105+
data.addOption(OptionType.STRING, "description" + renderNumber,
106+
"The description of the role");
107+
data.addOption(OptionType.STRING, "emoji" + renderNumber, "The emoji of the role");
108+
});
85109
}
86110

87111
@Override
@@ -90,22 +114,15 @@ public void onSlashCommand(SlashCommandInteractionEvent event) {
90114
return;
91115
}
92116

93-
sendMenu(event);
94-
}
95-
96-
@Override
97-
public void onButtonClick(ButtonInteractionEvent event, List<String> args) {
98-
User user = event.getUser();
99-
StringSelectMenu.Builder menu = StringSelectMenu
100-
.create(generateComponentId(Lifespan.REGULAR, event.getUser().getId()))
101-
.setPlaceholder("Select role to apply for");
102-
103-
config.applyRoleConfig()
104-
.stream()
105-
.map(option -> mapToSelectOption(user, option))
106-
.forEach(menu::addOptions);
117+
long incorrectArgsCount = getIncorrectRoleArgsCount(event.getInteraction().getOptions());
118+
if (incorrectArgsCount > 0) {
119+
event.reply("Missing information for %d roles.".formatted(incorrectArgsCount))
120+
.setEphemeral(true)
121+
.queue();
122+
return;
123+
}
107124

108-
event.reply("").addActionRow(menu.build()).setEphemeral(true).queue();
125+
sendMenu(event);
109126
}
110127

111128
/**
@@ -172,6 +189,58 @@ public void onStringSelectSelection(StringSelectInteractionEvent event, List<Str
172189
event.replyModal(modal).queue();
173190
}
174191

192+
/**
193+
* Checks a given list of passed arguments (from a user) and calculates how many roles have
194+
* missing data.
195+
*
196+
* @param args the list of passed arguments
197+
* @return the amount of roles with missing data
198+
*/
199+
private static long getIncorrectRoleArgsCount(final List<OptionMapping> args) {
200+
final Map<Character, Integer> frequencyMap = new HashMap<>();
201+
202+
args.stream()
203+
.map(OptionMapping::getName)
204+
.map(name -> name.charAt(name.length() - 1))
205+
.forEach(number -> frequencyMap.merge(number, 1, Integer::sum));
206+
207+
return frequencyMap.values().stream().filter(value -> value != 3).count();
208+
}
209+
210+
/**
211+
* Populates a {@link StringSelectMenu.Builder} with application roles.
212+
*
213+
* @param menuBuilder the menu builder to populate
214+
* @param args the arguments which contain data about the roles
215+
*/
216+
private void addRolesToMenu(StringSelectMenu.Builder menuBuilder,
217+
final List<OptionMapping> args) {
218+
final Map<Character, MenuRole> roles = new HashMap<>();
219+
220+
args.forEach(arg -> {
221+
final String name = arg.getName();
222+
final String argValue = arg.getAsString();
223+
final char roleId = name.charAt(name.length() - 1);
224+
MenuRole role = roles.computeIfAbsent(roleId, k -> new MenuRole());
225+
226+
if (name.startsWith("title")) {
227+
String value = generateComponentId(ROLE_COMPONENT_ID_HEADER, argValue);
228+
229+
role.setValue(value);
230+
role.setLabel(argValue);
231+
} else if (name.startsWith("description")) {
232+
role.setDescription(argValue);
233+
} else if (name.startsWith("emoji")) {
234+
role.setEmoji(Emoji.fromFormatted(argValue));
235+
}
236+
});
237+
238+
roles.values().forEach(role -> {
239+
menuBuilder.addOption(role.getLabel(), role.getValue(), role.getDescription(),
240+
role.getEmoji());
241+
});
242+
}
243+
175244
@Override
176245
public void onModalSubmitted(ModalInteractionEvent event, List<String> args) {
177246
Guild guild = event.getGuild();
@@ -282,10 +351,14 @@ private void sendApplicationResult(final ModalInteractionEvent event, List<Strin
282351
private void sendMenu(final CommandInteraction event) {
283352
MessageEmbed embed = createApplicationEmbed();
284353

285-
String buttonComponentId = generateComponentId(Lifespan.PERMANENT, event.getUser().getId());
286-
Button button = Button.primary(buttonComponentId, "Check openings");
354+
StringSelectMenu.Builder menuBuilder = StringSelectMenu
355+
.create(generateComponentId(Lifespan.REGULAR, event.getUser().getId()))
356+
.setPlaceholder("Select role to apply for")
357+
.setRequiredRange(1, 1);
358+
359+
addRolesToMenu(menuBuilder, event.getOptions());
287360

288-
event.replyEmbeds(embed).addActionRow(button).queue();
361+
event.replyEmbeds(embed).addActionRow(menuBuilder.build()).queue();
289362
}
290363

291364
private static MessageEmbed createApplicationEmbed() {
@@ -297,4 +370,50 @@ private static MessageEmbed createApplicationEmbed() {
297370
.setColor(AMBIENT_COLOR)
298371
.build();
299372
}
373+
374+
/**
375+
* Wrapper class which represents a menu role for the application create command.
376+
* <p>
377+
* The reason this exists is due to the fact that {@link StringSelectMenu.Builder} does not have
378+
* a method which takes emojis as input as of writing this, so we have to elegantly pass in
379+
* custom data from this POJO.
380+
*/
381+
private static class MenuRole {
382+
private String label;
383+
private String value;
384+
private String description;
385+
private Emoji emoji;
386+
387+
public String getLabel() {
388+
return label;
389+
}
390+
391+
public void setLabel(String label) {
392+
this.label = label;
393+
}
394+
395+
public String getValue() {
396+
return value;
397+
}
398+
399+
public void setValue(String value) {
400+
this.value = value;
401+
}
402+
403+
public String getDescription() {
404+
return description;
405+
}
406+
407+
public void setDescription(String description) {
408+
this.description = description;
409+
}
410+
411+
public Emoji getEmoji() {
412+
return emoji;
413+
}
414+
415+
public void setEmoji(Emoji emoji) {
416+
this.emoji = emoji;
417+
}
418+
}
300419
}

0 commit comments

Comments
 (0)