Declarative slash commands¶
Declarative commands must be declared with Kotlin, but the command itself may be in any language.
Declarative commands allow you full control over the declaration, implement at least one of them:
GlobalApplicationCommandProvider(for everyone at once)GuildApplicationCommandProvider(on each guild)
You can then use the slashCommand method on the manager, give it the command name, the command method,
and then configure your command.
Tip
You are free to skip registration of any command/subcommand/group, for example,
if the guild in GuildApplicationCommandManager isn't a guild you want the command to appear in.
Example
@Command
class SlashPing : GlobalApplicationCommandProvider {
suspend fun onSlashPing(event: GuildSlashEvent) {
event.deferReply(true).queue()
val ping = event.jda.getRestPing().await()
event.hook.editOriginal("Pong! $ping ms").await()
}
override fun declareGlobalApplicationCommands(manager: GlobalApplicationCommandManager) {
// Default scope is global, guild-only (GUILD_NO_DM)
manager.slashCommand("ping", function = ::onSlashPing) {
description = "Pong!"
}
}
}
Subcommands¶
As top-level commands cannot be made alongside subcommands, the top-level function must be null.
You can then add a subcommand by using subcommand, where each subcommand is its own function.
Example
@Command
class SlashTag : GlobalApplicationCommandProvider {
fun onSlashTagCreate(event: GuildSlashEvent) {
// ...
}
fun onSlashTagDelete(event: GuildSlashEvent) {
// ...
}
override fun declareGlobalApplicationCommands(manager: GlobalApplicationCommandManager) {
// Pass a null function as this is not a top-level command
manager.slashCommand("tag", function = null) {
description = "Manage tags"
subcommand("create", ::onSlashTagCreate) {
description = "Creates a tag"
}
subcommand("delete", ::onSlashTagDelete) {
description = "Deletes a tag"
}
}
}
}
Info
You can still create both subcommands, and subcommand groups containg subcommands.
Adding options¶
Options can be added with a parameter and declaring it using option in your command builder,
where the declaredName is the name of your parameter, the block will let you change the description, choices, etc.
All supported types are documented under ParameterResolver, and other types can be added.
Example
@Command
class SlashSay : GlobalApplicationCommandProvider {
suspend fun onSlashSay(event: GuildSlashEvent, content: String) {
event.reply(content).await()
}
override fun declareGlobalApplicationCommands(manager: GlobalApplicationCommandManager) {
manager.slashCommand("say", function = ::onSlashSay) {
description = "Says something"
option("content") {
description = "What to say"
}
}
}
}
Tip
You can override the option name by setting optionName in the option declaration:
option("content", optionName = "sentence") {
...
}
Using choices¶
Adding choices is very straight forward, you only have to give a list of choices to the choice property.
Example
@Command
class SlashConvert : GlobalApplicationCommandProvider {
suspend fun onSlashConvert(event: GuildSlashEvent, time: Long, from: TimeUnit, to: TimeUnit) {
event.reply("${to.convert(time, from)} ${to.name.lowercase()}").await()
}
override fun declareGlobalApplicationCommands(manager: GlobalApplicationCommandManager) {
manager.slashCommand("convert", function = ::onSlashConvert) {
description = "Convert time to another unit"
option("time") {
description = "The time to convert"
}
option("from") {
description = "The unit to convert from"
choices = listOf(TimeUnit.SECONDS, TimeUnit.MINUTES, TimeUnit.HOURS, TimeUnit.DAYS)
// The Resolvers class helps us by providing resolvers for any enum type.
// We're just using the helper method to change an enum value to a more natural name.
.map { Choice(Resolvers.toHumanName(it), it.name) }
}
option("to") {
description = "The unit to convert to"
choices = listOf(TimeUnit.SECONDS, TimeUnit.MINUTES, TimeUnit.HOURS, TimeUnit.DAYS)
// The Resolvers class helps us by providing resolvers for any enum type.
// We're just using the helper method to change an enum value to a more natural name.
.map { Choice(Resolvers.toHumanName(it), it.name) }
}
}
}
}
As you can see, despite the short choice list, this causes duplications with multiple commands. This issue is solved with predefined choices.
Using autocomplete¶
Learn how to create an autocomplete handler here
Enabling autocompletion for an option is done by referencing an existing handler:
- (recommended) Reference autocomplete handlers by their function with
autocompleteByFunction - Reference named autocomplete handlers with
autocompleteByName
Example
Using the autocomplete handler we made in "Creating autocomplete handlers":
@Command
class SlashWord : GlobalApplicationCommandProvider {
suspend fun onSlashWord(event: GuildSlashEvent, word: String) {
event.reply_("Your word was $word", ephemeral = true).await()
}
override fun declareGlobalApplicationCommands(manager: GlobalApplicationCommandManager) {
manager.slashCommand("word", function = ::onSlashWord) {
description = "Autocompletes a word"
option("word") {
description = "The word"
// Use an existing autocomplete declaration
autocompleteByFunction(SlashWordAutocomplete::onWordAutocomplete)
}
}
}
}
Rate limiting¶
This lets you reject application commands if the user tries to use them too often.
Learn how to create a rate limiter with "Defining a rate limit"
Using an (anonymous) rate limiter¶
@Command
class SlashRateLimit : GlobalApplicationCommandProvider {
suspend fun onSlashRateLimit(event: GuildSlashEvent) {
event.reply("Hello world!").await()
}
override fun declareGlobalApplicationCommands(manager: GlobalApplicationCommandManager) {
manager.slashCommand("rate_limit", function = ::onSlashRateLimit) {
// Lets the user use the command 5 times in an hour, but also 2 times in 2 minutes to prevent spam
val bucketConfiguration = Buckets.spikeProtected(
capacity = 5, // 5 uses
duration = 1.hours, // Gives 5 tokens gradually, during an hour (1 token every 12 minutes)
spikeCapacity = 2, // 2 uses
spikeDuration = 2.minutes // Give 2 tokens every 2 minutes
)
val rateLimiter = RateLimiter.createDefault(
// Apply to each user, regardless of channel/guild
RateLimitScope.USER,
// Delete the message telling the user about the remaining rate limit after it expires
deleteOnRefill = true,
// Give our constant bucket configuration
configurationSupplier = bucketConfiguration.toSupplier()
)
rateLimit(rateLimiter)
}
}
}
Using an existing rate limiter¶
Nothing as simple as using rateLimitReference with the group of a rate limiter defined in a RateLimitProvider.
@BService
class WikiRateLimitProvider : RateLimitProvider {
override fun declareRateLimit(manager: RateLimitManager) {
// Lets the user use the command 5 times in an hour, but also 2 times in 2 minutes to prevent spam
val bucketConfiguration = Buckets.spikeProtected(
capacity = 5, // 5 uses
duration = 1.hours, // Gives 5 tokens gradually, during an hour (1 token every 12 minutes)
spikeCapacity = 2, // 2 uses
spikeDuration = 2.minutes // Give 2 tokens every 2 minutes
)
val rateLimiter = RateLimiter.createDefault(
// Apply to each user, regardless of channel/guild
RateLimitScope.USER,
// Delete the message telling the user about the remaining rate limit after it expires
deleteOnRefill = true,
// Give our constant bucket configuration
configurationSupplier = bucketConfiguration.toSupplier()
)
// Register, any command can use it
manager.rateLimit(RATE_LIMIT_GROUP, rateLimiter)
}
companion object {
// The name of the rate limit, so you can reference it in your commands/components
const val RATE_LIMIT_GROUP = "Wiki"
}
}
@Command
class SlashRateLimitExisting : GlobalApplicationCommandProvider {
suspend fun onSlashRateLimit(event: GuildSlashEvent) {
event.reply("Hello world!").await()
}
override fun declareGlobalApplicationCommands(manager: GlobalApplicationCommandManager) {
manager.slashCommand("rate_limit_existing", function = ::onSlashRateLimit) {
// Use the rate limiter we defined in [[WikiRateLimitProvider]]
rateLimitReference(WikiRateLimitProvider.RATE_LIMIT_GROUP)
}
}
}
Cooldown¶
A cooldown is a rate limit, but with fewer parameters, it can be used as cooldown(5.seconds /* also scope and deleteOnRefill */).
Generated values¶
Generated values are a command parameter that gets their values computed by the given block everytime the command run.
Contrary to the annotated commands, no checks are required, as this is tied to the currently built command.
Example
@Command
class SlashCreateTime : GlobalApplicationCommandProvider {
suspend fun onSlashCreateTime(event: GuildSlashEvent, timestamp: Instant) {
event.reply("I was created on ${TimeFormat.DATE_TIME_SHORT.format(timestamp)}").await()
}
override fun declareGlobalApplicationCommands(manager: GlobalApplicationCommandManager) {
manager.slashCommand("create_time", function = ::onSlashCreateTime) {
description = "Shows the creation time of this command"
// Create a snapshot of the instant the command was created
val now = Instant.now()
generatedOption("timestamp") {
// Give back the instant snapshot, as this will be called every time the command is run
return@generatedOption now
}
}
}
}