-
-
Notifications
You must be signed in to change notification settings - Fork 91
Feat: Add slash command to generate application form for various community roles #1049
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
6d8ceee
a42a428
dcb241c
375ae45
8651807
0a04d90
8ab2f40
f46c983
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
package org.togetherjava.tjbot.config; | ||
|
||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
import net.dv8tion.jda.api.interactions.components.text.TextInput; | ||
|
||
import java.util.Objects; | ||
|
||
/** | ||
* 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( | ||
tj-wazei marked this conversation as resolved.
Show resolved
Hide resolved
|
||
@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. | ||
* <p> | ||
* 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); | ||
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)); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
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. | ||
* <p> | ||
* 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the feature should be renamed to match the package name. |
||
private final Cache<Member, OffsetDateTime> applicationSubmitCooldown; | ||
private final Predicate<String> 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. | ||
* <p> | ||
* 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<String> args, | ||
String answer) { | ||
Guild guild = event.getGuild(); | ||
if (args.size() != 2 || guild == null) { | ||
return; | ||
} | ||
Comment on lines
+68
to
+70
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sounds like this would be an error case that should emit a log message instead of silent failure? |
||
|
||
Optional<TextChannel> applicationChannel = getApplicationChannel(guild); | ||
if (applicationChannel.isEmpty()) { | ||
return; | ||
} | ||
Comment on lines
+73
to
+75
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. warning instead of silent failure? |
||
|
||
User applicant = event.getUser(); | ||
EmbedBuilder embed = | ||
new EmbedBuilder().setAuthor(applicant.getName(), null, applicant.getAvatarUrl()) | ||
.setColor(ApplicationCreateCommand.AMBIENT_COLOR) | ||
.setTimestamp(Instant.now()) | ||
.setFooter("Submitted at"); | ||
Comment on lines
+81
to
+82
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. prefer reordering these two calls so it tells the story better "Submitted at ... Instant.now()" |
||
|
||
String roleString = args.getLast(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. prefer extracting the args at the beginning, right after ur length check. and id prefer explicit indices here to make sure the thing is at the index u expected. so |
||
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<TextChannel> 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<Member, OffsetDateTime> getApplicationSubmitCooldown() { | ||
return applicationSubmitCooldown; | ||
} | ||
|
||
protected void submitApplicationFromModalInteraction(ModalInteractionEvent event, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why protected? this class doesnt make use of inheritance. id remove all the protected and make the class |
||
List<String> 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()); | ||
} | ||
|
||
protected long getMemberCooldownMinutes(Member member) { | ||
OffsetDateTime timeSentCache = getApplicationSubmitCooldown().getIfPresent(member); | ||
if (timeSentCache != null) { | ||
Duration duration = Duration.between(timeSentCache, OffsetDateTime.now()); | ||
return roleApplicationSystemConfig.applicationSubmitCooldownMinutes() | ||
- duration.toMinutes(); | ||
} | ||
return 0L; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these 3 can be hardcoded. not needed in the config