Advanced options
Text and application command options can benefit from more complex option types, by combining multiple options into one parameter, such as varargs, mention strings and custom data structures.
Varargs¶
Varargs lets you generate options (up to 25 options per command) and put the values in a List
,
the number of required options is also configurable.
Annotation-declared commands¶
Use @VarArgs
on the parameter.
The drawback is that each option will be configured the same, name, description, etc...
Code-declared commands¶
Using optionVararg
or inlineClassOptionVararg
on your command builder lets you solve the above issues.
Example
fun onSlashCommand(event: GuildSlashEvent, names: List<String>) {
// ...
}
manager.slashCommand("command", ::onSlashCommand) {
optionVararg(
declaredName = "names", // Name of the method parameter
amount = 5, //How many options to generate
requiredAmount = 1, //How many of them are required
optionNameSupplier = { num -> "name_$num" } // Generate the name of each option
) { num ->
// This runs for each option
description = "Name N°$num"
}
}
Mention strings¶
You can use this annotation on both code-declared and annotation-declared commands
@MentionsString
is an annotation
that lets you retrieve as many mentions as a string option lets you type,
you must use it on a List
parameter with an element type supported by the annotation.
You can also use a List<IMentionable>
, where you can set the requested mention types.
Note
This won't restrict what the user can type on Discord, this only enables parsing mentions inside the string.
Bulk ban example
@Command
class SlashBulkBan : ApplicationCommand() {
@JDASlashCommand(name = "bulk_ban", description = "Ban users in bulk")
suspend fun onSlashBulkBan(
event: GuildSlashEvent,
@SlashOption(description = "Users to ban") @MentionsString users: List<InputUser>,
@SlashOption(description = "Time frame of messages to delete") timeframe: Long,
@SlashOption(description = "Unit of the time frame", usePredefinedChoices = true) unit: TimeUnit,
) {
// Check if any member cannot be banned
val higherMembers = users.mapNotNull { it.member }.filterNot { event.guild.selfMember.canInteract(it) }
if (higherMembers.isNotEmpty()) {
return event.reply_("Cannot ban ${higherMembers.joinToString { it.asMention }} as they have equal/higher roles", ephemeral = true).awaitUnit()
}
event.deferReply(true).queue()
event.guild.ban(users, timeframe.toDuration(unit.toDurationUnit())).awaitCatching()
// Make sure to use onSuccess first,
// as 'handle' will clear the result type
.onSuccess {
event.hook.send("Banned ${it.bannedUsers.size} users, ${it.failedUsers.size} failed").await()
}
.handle(ErrorResponse.MISSING_PERMISSIONS) {
event.hook.send("Could not ban users due to missing permissions").await()
}
.handle(ErrorResponse.FAILED_TO_BAN_USERS) {
event.hook.send("Could not ban anyone").await()
}
// Throw unhandled exceptions
.getOrThrow()
}
}
@Command
public class SlashBulkBan extends ApplicationCommand {
@JDASlashCommand(name = "bulk_ban", description = "Ban users in bulk")
public void onSlashBulkBan(
GuildSlashEvent event,
@SlashOption(description = "Users to ban") @MentionsString List<? extends InputUser> users,
@SlashOption(description = "Time frame of messages to delete") Long timeframe,
@SlashOption(description = "Unit of the time frame", usePredefinedChoices = true) TimeUnit unit
) {
// Check if any member cannot be banned
final var higherMembers = new ArrayList<Member>();
for (var user : users) {
final Member member = user.getMember();
if (member == null) continue;
if (!event.getGuild().getSelfMember().canInteract(member)) {
higherMembers.add(member);
}
}
if (!higherMembers.isEmpty()) {
final String mentions = higherMembers.stream().map(IMentionable::getAsMention).collect(Collectors.joining());
event.reply("Cannot ban " + mentions + " as they have equal/higher roles")
.setEphemeral(true)
.queue();
return;
}
event.deferReply(true).queue();
event.getGuild().ban(users, Duration.of(timeframe, unit.toChronoUnit()))
.queue(response -> {
event.getHook().sendMessageFormat("Banned %s users, %s failed", response.getBannedUsers().size(), response.getFailedUsers().size()).queue();
}, new ErrorHandler()
.handle(ErrorResponse.MISSING_PERMISSIONS, exception -> {
event.getHook().sendMessage("Could not ban users due to missing permissions").queue();
})
.handle(ErrorResponse.FAILED_TO_BAN_USERS, exception -> {
event.getHook().sendMessage("Could not ban anyone").queue();
})
);
}
}
Advanced code-declared options¶
The Kotlin DSL also lets you do more, for example, using loops to generate commands, or even options. It also allows you to create more complex options, such as having multiple options in one parameter.
Distinction between parameters and options
Method parameters are what you expect, a simple value in your method, but for the framework, parameters might be a complex object (composed of multiple options), or a single option, whether it's an injected service, a Discord option or a generated value.
i.e., A parameter might be a single or multiple options, but an option is always a single value.
Composite parameters¶
These are parameters composed of multiple options, of any type, which gets merged into one parameter by using an aggregator.
Tip
This is how varargs are implemented, they are a loop that generates N options, where X options are optional.
Creating an aggregated parameter
Here is how you can use aggregated parameters to create a message delete timeframe, out of a Long
and a TimeUnit
.
// This data class is practically pointless;
// this is just to demonstrate how you can group parameters together,
// so you can benefit from functions/backed properties limited to your parameters,
// without polluting classes with extensions
data class DeleteTimeframe(val time: Long, val unit: TimeUnit) {
override fun toString(): String = "$time ${unit.name.lowercase()}"
}
@Command
class SlashBan {
@AppDeclaration
fun onDeclare(manager: GlobalApplicationCommandManager) {
manager.slashCommand("ban", function = SlashBan::onSlashBan) {
...
aggregate(declaredName = "timeframe", aggregator = ::DeleteTimeframe) {
option(declaredName = "time") {
description = "The timeframe of messages to delete with the specified unit"
}
option(declaredName = "unit") {
description = "The unit of the delete timeframe"
usePredefinedChoices = true
}
}
}
}
}
The aggregating function can be a reference to the object's constructor, or a function taking the options and returning an object of the corresponding type.
Kotlin's inline classes¶
Input options as well as varargs can be encapsulated in an inline class,
allowing you to define simple computable properties and functions for types where defining an extension makes no sense.
(Like adding an extension, that's specific to only one command, on a String
)
Using inline classes
private val spaceDelimiter = Regex("""\s+""")
@JvmInline
value class Sentence(val value: String) {
val words: List<String> get() = spaceDelimiter.split(value)
}
@Command
class SlashInlineWords : ApplicationCommand() {
@JDASlashCommand(name = "words", description = "Extracts the words of a sentence")
suspend fun onSlashWords(event: GuildSlashEvent, @SlashOption(description = "Input sentence") sentence: Sentence) {
event.reply_("The words are: ${sentence.words}", ephemeral = true).await()
}
}