Using rate limits¶
Rate limits lets you reject interactions when they are used too much in a time span.
For more details, check out the docs of Bucket4J
Defining a rate limit¶
Rate limits can be defined by implementing RateLimitProvider,
the overridden function will run before registering commands, so you can use them anywhere.
Defining bucket configurations¶
A BucketConfiguration defines what the limits are, you can supply different configurations based on the context,
by implementing a BucketConfigurationSupplier, or you can create a configuration with the factories in Buckets,
or with the BucketConfiguration builder.
Tip
If you want to give the same configuration regardless of context,
you can use BucketConfigurationSupplier.constant(BucketConfiguration) (or BucketConfiguration#toSupplier() for Kotlin users)
Example
// 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
)
// Lets the user use the command 5 times in an hour, but also 2 times in 2 minutes to prevent spam
final var bucketConfiguration = Buckets.createSpikeProtected(
5, // 5 uses
Duration.ofHours(1), // Gives 5 tokens gradually, during an hour (1 token every 12 minutes)
2, // 2 uses
Duration.ofMinutes(2) // Give 2 tokens every 2 minutes
);
Creating a rate limiter¶
The default rate limiters should provide you ready-made implementations for both in-memory and proxied buckets, refer to the example attached to them.
Example
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()
)
final var rateLimiter = RateLimiter.createDefault(
// Apply to each user, regardless of channel/guild
RateLimitScope.USER,
// Give our constant bucket configuration
BucketConfigurationSupplier.constant(bucketConfiguration),
// Delete the message telling the user about the remaining rate limit after it expires
true
);
However, you can also create a custom one by implementing RateLimiter, which is the combination of:
BucketAccessor: Retrieves aBucketbased on the contextRateLimitHandler: Handles when an interaction has been rate limited (often to tell the user about it)
Tip
When making a custom rate limiter, you can delegate one of the default implementations to avoid reimplementing existing behavior.
You can also use BucketKeySupplier to help you define functions
returning a bucket key (an identifier basically), based on the execution context.
Registering the rate limiter¶
You can now register using RateLimitManager#rateLimit(String, RateLimiter),
the group (name of the rate limiter) must be unique.
Full example
@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"
}
}
@BService
public class WikiRateLimitProvider implements RateLimitProvider {
// The name of the rate limit, so you can reference it in your commands/components
public static final String RATE_LIMIT_GROUP = "Wiki";
@Override
public void declareRateLimit(@NotNull RateLimitManager rateLimitManager) {
// Lets the user use the command 5 times in an hour, but also 2 times in 2 minutes to prevent spam
final var bucketConfiguration = Buckets.createSpikeProtected(
5, // 5 uses
Duration.ofHours(1), // Gives 5 tokens gradually, during an hour (1 token every 12 minutes)
2, // 2 uses
Duration.ofMinutes(2) // Give 2 tokens every 2 minutes
);
final var rateLimiter = RateLimiter.createDefault(
// Apply to each user, regardless of channel/guild
RateLimitScope.USER,
// Give our constant bucket configuration
BucketConfigurationSupplier.constant(bucketConfiguration),
// Delete the message telling the user about the remaining rate limit after it expires
true
);
// Register
rateLimitManager.rateLimit(RATE_LIMIT_GROUP, rateLimiter);
}
}
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.
Example
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();
}