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 aBucket
based 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();
}