Skip to content

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((List<UserSnowflake>) (List<? extends UserSnowflake>) 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.

The aggregated object
// 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()}"
}
The aggregated parameter declaration
@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()
    }
}