Using rate limiters in commands¶
In this page, we'll focus on slash commands, but it works the same for text commands and context menu commands.
Annotated commands¶
Using an (anonymous) rate limiter¶
Use @RateLimit or @Cooldown to define one on an application command, it will define a rate limiter automatically, but cannot be referenced anywhere else.
@Command
class SlashRateLimit {
// A rate limit for this slash command only
@RateLimit(
// Apply to each user, regardless of channel/guild
scope = RateLimitScope.USER,
// Delete the message telling the user about the remaining rate limit after it expires
deleteOnRefill = true,
// At least one of those needs to be empty for the command to be rejected
bandwidths = [
// 5 uses, 5 tokens gets added gradually over an hour (so, 1 token every 12 minutes)
Bandwidth(
capacity = 5, // 5 uses
Refill(
type = RefillType.GREEDY, // Gradually
tokens = 5, // gives 5 tokens
period = 1, // every 1
periodUnit = ChronoUnit.HOURS // hour
)
),
// 2 uses, 2 tokens gets added at once after 2 minutes
// This is to prevent the user from spamming the command
Bandwidth(
capacity = 2, // 2 uses
Refill(
type = RefillType.INTERVAL, // At once,
tokens = 2, // give 2 tokens
period = 2, // every 2
periodUnit = ChronoUnit.MINUTES // minutes
)
),
]
)
@JDASlashCommand(name = "rate_limit")
suspend fun onSlashRateLimit(event: GuildSlashEvent) {
event.reply("Hello world!").await()
}
}
@Command
public class SlashRateLimit {
// A rate limit for this slash command only
@RateLimit(
// Apply to each user, regardless of channel/guild
scope = RateLimitScope.USER,
// Delete the message telling the user about the remaining rate limit after it expires
deleteOnRefill = true,
// At least one of those needs to be empty for the command to be rejected
bandwidths = {
// 5 uses, 5 tokens gets added gradually over an hour (so, 1 token every 12 minutes)
@Bandwidth(
capacity = 5, // 5 uses
refill = @Refill(
type = RefillType.GREEDY, // Gradually
tokens = 5, // gives 5 tokens
period = 1, // every 1
periodUnit = ChronoUnit.HOURS // hour
)
),
// 2 uses, 2 tokens gets added at once after 2 minutes
// This is to prevent the user from spamming the command
@Bandwidth(
capacity = 2, // 2 uses
refill = @Refill(
type = RefillType.INTERVAL, // At once,
tokens = 2, // give 2 tokens
period = 2, // every 2
periodUnit = ChronoUnit.MINUTES // minutes
)
),
}
)
@JDASlashCommand(name = "rate_limit")
public void onSlashRateLimit(GuildSlashEvent event) {
event.reply("Hello world!").queue();
}
}
Cooldown¶
A cooldown can be used as @Cooldown(5, ChronoUnit.SECONDS /* also scope and deleteOnRefill */).
Using an existing rate limiter¶
Nothing as simple as using @RateLimitReference with the group of a rate limiter defined by a RateLimitProvider.
We'll be using the same one as in "Registering the rate limiter".
@Command
class SlashRateLimitExisting {
// A rate limit for this slash command only
@RateLimitReference(WikiRateLimitProvider.RATE_LIMIT_GROUP)
@JDASlashCommand(name = "rate_limit_existing")
suspend fun onSlashRateLimit(event: GuildSlashEvent) {
event.reply("Hello world!").await()
}
}
@Command
public class SlashRateLimitExisting {
// A rate limit for this slash command only
@RateLimitReference(WikiRateLimitProvider.RATE_LIMIT_GROUP)
@JDASlashCommand(name = "rate_limit_existing")
public void onSlashRateLimit(GuildSlashEvent event) {
event.reply("Hello world!").queue();
}
}
Declarative commands¶
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)
}
}
}
Cooldown¶
A cooldown can be used as cooldown(5.seconds /* also scope and deleteOnRefill */), instead of the last rateLimit call.
Using an existing rate limiter¶
Nothing as simple as using rateLimitReference with the group of a rate limiter defined by a RateLimitProvider.
We'll be using the same one as in "Registering the rate limiter".
@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)
}
}
}
Cancelling rate limits¶
If your interaction does an early return, you can also return the token with cancelRateLimit(),
so the user won't get penalized for this interaction.
Cancelling when not in a voice channel
suspend fun onSlashRateLimit(event: GuildSlashEvent) {
// Assuming we have voice states cached
if (!event.member.voiceState!!.inAudioChannel()) {
// Note that this would be easier done using a filter,
// as no token would be used, and would also be cleaner.
event.cancelRateLimit()
return event.reply_("You must be in a voice channel").awaitUnit()
}
event.reply("Hello world!").await()
}
public void onSlashRateLimit(GuildSlashEvent event) {
// Assuming we have voice states cached
if (!event.getMember().getVoiceState().inAudioChannel()) {
// Note that this would be easier done using a filter,
// as no token would be used, and would also be cleaner.
event.cancelRateLimit();
event.reply("You must be in a voice channel").queue();
return;
}
event.reply("Hello world!").queue();
}