Skip to content

Commit

Permalink
Add Max Delay Limits, Delay Hook, and RateLimitInfo (#1144)
Browse files Browse the repository at this point in the history
Add max delay limits, delay hook, and rate limit info.
This commit limits the maximum amount of rate limit delayed invocations.
By setting this limit to 0, all invocations will error instantly. Otherwise, they will only error once the limit is hit.
Once a delay will be suggested, the bucket will hook an optional function.
This function may inform the user about the limit.
The rate limit info will be provided in the dispatch error.
It contains information about the duration, the active delays, maximum delays, and whether this is the first try.
  • Loading branch information
Lakelezz committed Dec 25, 2020
1 parent d4caf12 commit aed3886
Show file tree
Hide file tree
Showing 3 changed files with 184 additions and 53 deletions.
42 changes: 31 additions & 11 deletions examples/e05_command_framework/src/main.rs
Expand Up @@ -135,6 +135,7 @@ async fn my_help(
let _ = help_commands::with_embeds(context, msg, args, help_options, groups, owners).await;
Ok(())
}

#[hook]
async fn before(ctx: &Context, msg: &Message, command_name: &str) -> bool {
println!("Got command '{}' by user '{}'", command_name, msg.author.name);
Expand Down Expand Up @@ -168,14 +169,23 @@ async fn normal_message(_ctx: &Context, msg: &Message) {
println!("Message is not a command '{}'", msg.content);
}

#[hook]
async fn delay_action(ctx: &Context, msg: &Message) {
// You may want to handle a Discord rate limit if this fails.
let _ = msg.react(ctx, '⏱').await;
}

#[hook]
async fn dispatch_error(ctx: &Context, msg: &Message, error: DispatchError) {
if let DispatchError::Ratelimited(duration) = error {
let _ = msg
.channel_id
.say(&ctx.http, &format!("Try this again in {} seconds.", duration.as_secs()))
.await;
if let DispatchError::Ratelimited(info) = error {

// We notify them only once.
if info.is_first_try {
let _ = msg
.channel_id
.say(&ctx.http, &format!("Try this again in {} seconds.", info.as_secs()))
.await;
}
}
}

Expand All @@ -184,11 +194,14 @@ async fn dispatch_error(ctx: &Context, msg: &Message, error: DispatchError) {
use serenity::{futures::future::BoxFuture, FutureExt};
fn _dispatch_error_no_macro<'fut>(ctx: &'fut mut Context, msg: &'fut Message, error: DispatchError) -> BoxFuture<'fut, ()> {
async move {
if let DispatchError::Ratelimited(duration) = error {
let _ = msg
.channel_id
.say(&ctx.http, &format!("Try this again in {} seconds.", duration.as_secs()))
.await;
if let DispatchError::Ratelimited(info) = error {

if info.is_first_try {
let _ = msg
.channel_id
.say(&ctx.http, &format!("Try this again in {} seconds.", info.as_secs()))
.await;
}
};
}.boxed()
}
Expand Down Expand Up @@ -261,7 +274,14 @@ async fn main() {
// Can't be used more than 2 times per 30 seconds, with a 5 second delay applying per channel.
// Optionally `await_ratelimits` will delay until the command can be executed instead of
// cancelling the command invocation.
.bucket("complicated", |b| b.delay(5).time_span(30).limit(2).limit_for(LimitedFor::Channel).await_ratelimits()).await
.bucket("complicated", |b| b.limit(2).time_span(30).delay(5)
// The target each bucket will apply to.
.limit_for(LimitedFor::Channel)
// The maximum amount of command invocations that can be delayed per target.
// Setting this to 0 (default) will never await/delay commands and cancel the invocation.
.await_ratelimits(1)
// A function to call when a rate limit leads to a delay.
.delay_action(delay_action)).await
// The `#[group]` macro generates `static` instances of the options set for the group.
// They're made in the pattern: `#name_GROUP` for the group instance and `#name_GROUP_OPTIONS`.
// #name is turned all uppercase
Expand Down
19 changes: 9 additions & 10 deletions src/framework/standard/mod.rs
Expand Up @@ -12,13 +12,13 @@ pub use args::{Args, Delimiter, Error as ArgError, Iter, RawArguments};
pub use configuration::{Configuration, WithWhiteSpace};
pub use structures::*;

use structures::buckets::{Bucket, BucketAction};
use structures::buckets::{Bucket, RateLimitAction};
pub use structures::buckets::BucketBuilder;

use parse::{ParseError, Invoke};
use parse::map::{CommandMap, GroupMap, Map};

use self::buckets::RevertBucket;
use self::buckets::{RateLimitInfo, RevertBucket};

use super::Framework;
use crate::client::Context;
Expand All @@ -29,7 +29,6 @@ use crate::model::{

use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;

use tokio::sync::Mutex;
use futures::future::BoxFuture;
Expand All @@ -53,9 +52,8 @@ use crate::model::{guild::Role, id::RoleId};
pub enum DispatchError {
/// When a custom function check has failed.
CheckFailed(&'static str, Reason),
/// When the command requester has exceeded a ratelimit bucket. The attached
/// value is the time a requester has to wait to run the command again.
Ratelimited(Duration),
/// When the command caller has exceeded a ratelimit bucket.
Ratelimited(RateLimitInfo),
/// When the requested command is disabled in bot configuration.
CommandDisabled(String),
/// When the user is blocked in bot configuration.
Expand Down Expand Up @@ -287,11 +285,12 @@ impl StandardFramework {

if let Some(ref mut bucket) = command.bucket.as_ref().and_then(|b| buckets.get_mut(*b)) {

if let Some(bucket_action) = bucket.take(ctx, msg).await {
if let Some(rate_limit_info) = bucket.take(ctx, msg).await {

duration = match bucket_action {
BucketAction::CancelWith(duration) => return Some(DispatchError::Ratelimited(duration)),
BucketAction::DelayFor(duration) => Some(duration),
duration = match rate_limit_info.action {
RateLimitAction::Cancelled | RateLimitAction::FailedDelay =>
return Some(DispatchError::Ratelimited(rate_limit_info)),
RateLimitAction::Delayed => Some(rate_limit_info.rate_limit),
};
}
}
Expand Down

0 comments on commit aed3886

Please sign in to comment.