Skip to content

Commit e83db20

Browse files
committed
feat(ApplicationCreateCommand): base code
1 parent 6c3cc81 commit e83db20

File tree

6 files changed

+286
-3
lines changed

6 files changed

+286
-3
lines changed

application/config.json.template

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,16 @@
110110
"special": [
111111
]
112112
},
113-
"memberCountCategoryPattern": "Info",
114-
"selectRolesChannelPattern": "select-your-roles"
113+
"applicationForm": {
114+
"applicationChannelPattern": "applications-log"
115+
"roles": [
116+
{
117+
"description": "Lorem ipsum",
118+
"name": "Test role",
119+
"formattedEmoji": ":joy:"
120+
}
121+
]
122+
}
123+
"selectRolesChannelPattern": "select-your-roles",
124+
"memberCountCategoryPattern": "Info"
115125
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package org.togetherjava.tjbot.config;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
5+
import java.util.List;
6+
import java.util.Objects;
7+
8+
public record ApplicationFormConfig(
9+
@JsonProperty(value = "roles", required = true) List<ApplyRoleConfig> applyRoleConfig,
10+
@JsonProperty(value = "applicationChannelPattern",
11+
required = true) String applicationChannelPattern) {
12+
13+
public ApplicationFormConfig {
14+
Objects.requireNonNull(applyRoleConfig);
15+
Objects.requireNonNull(applicationChannelPattern);
16+
}
17+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.togetherjava.tjbot.config;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
5+
import java.util.Objects;
6+
7+
public record ApplyRoleConfig(@JsonProperty(value = "name", required = true) String name,
8+
@JsonProperty(value = "description", required = true) String description,
9+
@JsonProperty(value = "formattedEmoji") String emoji) {
10+
11+
public ApplyRoleConfig {
12+
Objects.requireNonNull(name);
13+
Objects.requireNonNull(description);
14+
}
15+
}

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public final class Config {
4444
private final HelperPruneConfig helperPruneConfig;
4545
private final FeatureBlacklistConfig featureBlacklistConfig;
4646
private final String selectRolesChannelPattern;
47+
private final ApplicationFormConfig applicationFormConfig;
4748
private final String memberCountCategoryPattern;
4849

4950
@SuppressWarnings("ConstructorWithTooManyParameters")
@@ -94,7 +95,9 @@ private Config(@JsonProperty(value = "token", required = true) String token,
9495
@JsonProperty(value = "featureBlacklist",
9596
required = true) FeatureBlacklistConfig featureBlacklistConfig,
9697
@JsonProperty(value = "selectRolesChannelPattern",
97-
required = true) String selectRolesChannelPattern) {
98+
required = true) String selectRolesChannelPattern,
99+
@JsonProperty(value = "applicationForm",
100+
required = true) ApplicationFormConfig applicationFormConfig) {
98101
this.token = Objects.requireNonNull(token);
99102
this.githubApiKey = Objects.requireNonNull(githubApiKey);
100103
this.databasePath = Objects.requireNonNull(databasePath);
@@ -127,6 +130,7 @@ private Config(@JsonProperty(value = "token", required = true) String token,
127130
this.helperPruneConfig = Objects.requireNonNull(helperPruneConfig);
128131
this.featureBlacklistConfig = Objects.requireNonNull(featureBlacklistConfig);
129132
this.selectRolesChannelPattern = Objects.requireNonNull(selectRolesChannelPattern);
133+
this.applicationFormConfig = applicationFormConfig;
130134
}
131135

132136
/**
@@ -410,6 +414,15 @@ public String getSelectRolesChannelPattern() {
410414
return selectRolesChannelPattern;
411415
}
412416

417+
/**
418+
* The configuration related to the application form.
419+
*
420+
* @return the application form config
421+
*/
422+
public ApplicationFormConfig getApplicationFormConfig() {
423+
return applicationFormConfig;
424+
}
425+
413426
/**
414427
* Gets the pattern matching the category that is used to display the total member count.
415428
*

application/src/main/java/org/togetherjava/tjbot/features/Features.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.togetherjava.tjbot.config.FeatureBlacklist;
99
import org.togetherjava.tjbot.config.FeatureBlacklistConfig;
1010
import org.togetherjava.tjbot.db.Database;
11+
import org.togetherjava.tjbot.features.basic.ApplicationCreateCommand;
1112
import org.togetherjava.tjbot.features.basic.MemberCountDisplayRoutine;
1213
import org.togetherjava.tjbot.features.basic.PingCommand;
1314
import org.togetherjava.tjbot.features.basic.RoleSelectCommand;
@@ -166,6 +167,7 @@ public static Collection<Feature> createFeatures(JDA jda, Database database, Con
166167
features.add(new BookmarksCommand(bookmarksSystem));
167168
features.add(new ChatGptCommand(chatGptService, helpSystemHelper));
168169
features.add(new JShellCommand(jshellEval));
170+
features.add(new ApplicationCreateCommand(config));
169171

170172
FeatureBlacklist<Class<?>> blacklist = blacklistConfig.normal();
171173
return blacklist.filterStream(features.stream(), Object::getClass).toList();
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
package org.togetherjava.tjbot.features.basic;
2+
3+
import net.dv8tion.jda.api.EmbedBuilder;
4+
import net.dv8tion.jda.api.Permission;
5+
import net.dv8tion.jda.api.entities.Guild;
6+
import net.dv8tion.jda.api.entities.Member;
7+
import net.dv8tion.jda.api.entities.MessageEmbed;
8+
import net.dv8tion.jda.api.entities.User;
9+
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
10+
import net.dv8tion.jda.api.entities.emoji.Emoji;
11+
import net.dv8tion.jda.api.entities.emoji.EmojiUnion;
12+
import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent;
13+
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
14+
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
15+
import net.dv8tion.jda.api.events.interaction.component.SelectMenuInteractionEvent;
16+
import net.dv8tion.jda.api.interactions.commands.CommandInteraction;
17+
import net.dv8tion.jda.api.interactions.components.ActionRow;
18+
import net.dv8tion.jda.api.interactions.components.Modal;
19+
import net.dv8tion.jda.api.interactions.components.buttons.Button;
20+
import net.dv8tion.jda.api.interactions.components.selections.SelectMenu;
21+
import net.dv8tion.jda.api.interactions.components.selections.SelectOption;
22+
import net.dv8tion.jda.api.interactions.components.text.TextInput;
23+
import net.dv8tion.jda.api.interactions.components.text.TextInputStyle;
24+
import net.dv8tion.jda.api.interactions.modals.ModalMapping;
25+
import org.slf4j.Logger;
26+
import org.slf4j.LoggerFactory;
27+
28+
import org.togetherjava.tjbot.config.ApplicationFormConfig;
29+
import org.togetherjava.tjbot.config.ApplyRoleConfig;
30+
import org.togetherjava.tjbot.config.Config;
31+
import org.togetherjava.tjbot.features.CommandVisibility;
32+
import org.togetherjava.tjbot.features.SlashCommandAdapter;
33+
import org.togetherjava.tjbot.features.componentids.Lifespan;
34+
35+
import java.awt.Color;
36+
import java.time.Instant;
37+
import java.util.List;
38+
import java.util.Optional;
39+
import java.util.function.Predicate;
40+
import java.util.regex.Pattern;
41+
42+
public class ApplicationCreateCommand extends SlashCommandAdapter {
43+
private static final Logger logger = LoggerFactory.getLogger(ApplicationCreateCommand.class);
44+
45+
private static final Color AMBIENT_COLOR = new Color(24, 221, 136, 255);
46+
private static final int MIN_REASON_LENGTH = 50;
47+
private static final int MAX_REASON_LENGTH = 500;
48+
private static final String DEFAULT_QUESTION =
49+
"What makes you a valuable addition to the team? 😎";
50+
private final Predicate<String> applicationChannelPattern;
51+
private final ApplicationFormConfig config;
52+
53+
public ApplicationCreateCommand(Config config) {
54+
super("application-form", "Generates an application form for members to apply for roles.",
55+
CommandVisibility.GUILD);
56+
57+
this.config = config.getApplicationFormConfig();
58+
this.applicationChannelPattern =
59+
Pattern.compile(this.config.applicationChannelPattern()).asMatchPredicate();
60+
}
61+
62+
@Override
63+
public void onSlashCommand(SlashCommandInteractionEvent event) {
64+
if (!handleHasPermissions(event)) {
65+
return;
66+
}
67+
68+
sendMenu(event);
69+
}
70+
71+
@Override
72+
public void onButtonClick(ButtonInteractionEvent event, List<String> args) {
73+
User user = event.getUser();
74+
SelectMenu.Builder menu =
75+
SelectMenu.create(generateComponentId(Lifespan.REGULAR, event.getUser().getId()))
76+
.setPlaceholder("Select role to apply for");
77+
78+
config.applyRoleConfig()
79+
.stream()
80+
.map(option -> mapToSelectOption(user, option))
81+
.forEach(menu::addOptions);
82+
83+
event.reply("").addActionRow(menu.build()).setEphemeral(true).queue();
84+
}
85+
86+
private SelectOption mapToSelectOption(User user, ApplyRoleConfig option) {
87+
return SelectOption.of(option.name(), generateComponentId(user.getId(), option.name()))
88+
.withDescription(option.description())
89+
.withEmoji(Emoji.fromFormatted(option.emoji()));
90+
}
91+
92+
@Override
93+
public void onSelectMenuSelection(SelectMenuInteractionEvent event, List<String> args) {
94+
SelectOption selectOption = event.getSelectedOptions().getFirst();
95+
96+
if (selectOption == null) {
97+
return;
98+
}
99+
100+
TextInput body = TextInput
101+
.create(generateComponentId(event.getUser().getId()), "Question",
102+
TextInputStyle.PARAGRAPH)
103+
.setRequired(true)
104+
.setRequiredRange(MIN_REASON_LENGTH, MAX_REASON_LENGTH)
105+
.setPlaceholder(DEFAULT_QUESTION)
106+
.build();
107+
108+
EmojiUnion emoji = selectOption.getEmoji();
109+
String roleDisplayName;
110+
111+
if (emoji == null) {
112+
roleDisplayName = selectOption.getLabel();
113+
} else {
114+
roleDisplayName = "%s %s".formatted(emoji.getFormatted(), selectOption.getLabel());
115+
}
116+
117+
Modal modal = Modal
118+
.create(generateComponentId(event.getUser().getId(), roleDisplayName),
119+
String.format("Application form - %s", selectOption.getLabel()))
120+
.addActionRow(ActionRow.of(body).getComponents())
121+
.build();
122+
123+
event.getHook().deleteOriginal().queue();
124+
event.replyModal(modal).queue();
125+
}
126+
127+
@Override
128+
public void onModalSubmitted(ModalInteractionEvent event, List<String> args) {
129+
Guild guild = event.getGuild();
130+
131+
if (guild == null) {
132+
return;
133+
}
134+
135+
ModalMapping modalAnswer = event.getValues().getFirst();
136+
137+
sendApplicationResult(event, args, modalAnswer.getAsString());
138+
event.reply("Your application has been submitted. Thank you for applying! 😎")
139+
.setEphemeral(true)
140+
.queue();
141+
}
142+
143+
private Optional<TextChannel> getApplicationChannel(Guild guild) {
144+
return guild.getChannels()
145+
.stream()
146+
.filter(channel -> applicationChannelPattern.test(channel.getName()))
147+
.filter(channel -> channel.getType().isMessage())
148+
.map(TextChannel.class::cast)
149+
.findFirst();
150+
}
151+
152+
private boolean handleHasPermissions(SlashCommandInteractionEvent event) {
153+
Member member = event.getMember();
154+
155+
if (member == null) {
156+
return false;
157+
}
158+
159+
if (!event.getMember().hasPermission(Permission.MANAGE_ROLES)) {
160+
event.reply("You do not have the required manage role permission to use this command")
161+
.setEphemeral(true)
162+
.queue();
163+
return false;
164+
}
165+
166+
Member selfMember = event.getGuild().getSelfMember();
167+
if (!selfMember.hasPermission(Permission.MANAGE_ROLES)) {
168+
event.reply(
169+
"Sorry, but I was not set up correctly. I need the manage role permissions for this.")
170+
.setEphemeral(true)
171+
.queue();
172+
logger.error("The bot requires the manage role permissions for /{}.", getName());
173+
return false;
174+
}
175+
176+
return true;
177+
}
178+
179+
private void sendApplicationResult(final ModalInteractionEvent event, List<String> args,
180+
String answer) {
181+
Guild guild = event.getGuild();
182+
if (args.size() != 2 || guild == null) {
183+
return;
184+
}
185+
186+
Optional<TextChannel> applicationChannel = getApplicationChannel(guild);
187+
if (applicationChannel.isEmpty()) {
188+
return;
189+
}
190+
191+
User applicant = event.getUser();
192+
EmbedBuilder embed =
193+
new EmbedBuilder().setAuthor(applicant.getName(), null, applicant.getAvatarUrl())
194+
.setColor(AMBIENT_COLOR)
195+
.setTimestamp(Instant.now())
196+
.setFooter("Submitted at");
197+
198+
String roleString = args.getLast();
199+
MessageEmbed.Field roleField = new MessageEmbed.Field("Role", roleString, false);
200+
embed.addField(roleField);
201+
202+
MessageEmbed.Field answerField = new MessageEmbed.Field(DEFAULT_QUESTION, answer, false);
203+
embed.addField(answerField);
204+
205+
applicationChannel.get().sendMessageEmbeds(embed.build()).queue();
206+
}
207+
208+
private void sendMenu(final CommandInteraction event) {
209+
MessageEmbed embed = createApplicationEmbed();
210+
211+
String buttonComponentId = generateComponentId(Lifespan.PERMANENT, event.getUser().getId());
212+
Button button = Button.primary(buttonComponentId, "Check openings");
213+
214+
event.replyEmbeds(embed).addActionRow(button).queue();
215+
}
216+
217+
private static MessageEmbed createApplicationEmbed() {
218+
return new EmbedBuilder().setTitle("Apply for roles")
219+
.setDescription(
220+
"""
221+
We are always looking for community members that want to contribute to our community \
222+
and take charge. If you are interested, you can apply for various positions here!""")
223+
.setColor(AMBIENT_COLOR)
224+
.build();
225+
}
226+
}

0 commit comments

Comments
 (0)