use std::collections::HashMap;
use std::fmt;
use std::time::{Duration, Instant};
use futures::future::BoxFuture;
use crate::client::Context;
use crate::internal::tokio::spawn_named;
use crate::model::channel::Message;
type Check = for<'fut> fn(&'fut Context, &'fut Message) -> BoxFuture<'fut, bool>;
type DelayHook = for<'fut> fn(&'fut Context, &'fut Message) -> BoxFuture<'fut, ()>;
pub(crate) struct Ratelimit {
pub delay: Duration,
pub limit: Option<(Duration, u32)>,
}
pub(crate) struct UnitRatelimit {
pub last_time: Option<Instant>,
pub set_time: Instant,
pub tickets: u32,
pub awaiting: u32,
pub is_first_try: bool,
}
impl UnitRatelimit {
fn new(creation_time: Instant) -> Self {
Self {
last_time: None,
set_time: creation_time,
tickets: 0,
awaiting: 0,
is_first_try: true,
}
}
}
pub(crate) enum Bucket {
Global(TicketCounter),
User(TicketCounter),
Guild(TicketCounter),
Channel(TicketCounter),
#[cfg(feature = "cache")]
Category(TicketCounter),
}
impl Bucket {
#[inline]
pub async fn take(&mut self, ctx: &Context, msg: &Message) -> Option<RateLimitInfo> {
match self {
Self::Global(counter) => counter.take(ctx, msg, 0).await,
Self::User(counter) => counter.take(ctx, msg, msg.author.id.0).await,
Self::Guild(counter) => {
if let Some(guild_id) = msg.guild_id {
counter.take(ctx, msg, guild_id.0).await
} else {
None
}
},
Self::Channel(counter) => counter.take(ctx, msg, msg.channel_id.0).await,
#[cfg(feature = "cache")]
Self::Category(counter) => {
if let Some(category_id) = msg.category_id(ctx) {
counter.take(ctx, msg, category_id.0).await
} else {
None
}
},
}
}
#[inline]
pub async fn give(&mut self, ctx: &Context, msg: &Message) {
match self {
Self::Global(counter) => counter.give(ctx, msg, 0).await,
Self::User(counter) => counter.give(ctx, msg, msg.author.id.0).await,
Self::Guild(counter) => {
if let Some(guild_id) = msg.guild_id {
counter.give(ctx, msg, guild_id.0).await;
}
},
Self::Channel(counter) => counter.give(ctx, msg, msg.channel_id.0).await,
#[cfg(feature = "cache")]
Self::Category(counter) => {
if let Some(category_id) = msg.category_id(ctx) {
counter.give(ctx, msg, category_id.0).await;
}
},
}
}
}
pub(crate) struct TicketCounter {
pub ratelimit: Ratelimit,
pub tickets_for: HashMap<u64, UnitRatelimit>,
pub check: Option<Check>,
pub delay_action: Option<DelayHook>,
pub await_ratelimits: u32,
}
#[derive(Debug)]
pub struct RateLimitInfo {
pub rate_limit: Duration,
pub active_delays: u32,
pub max_delays: u32,
pub is_first_try: bool,
pub action: RateLimitAction,
}
#[derive(Debug)]
pub enum RateLimitAction {
Delayed,
FailedDelay,
Cancelled,
}
impl RateLimitInfo {
#[inline]
#[must_use]
pub fn as_secs(&self) -> u64 {
self.rate_limit.as_secs()
}
#[inline]
#[must_use]
pub fn as_millis(&self) -> u128 {
self.rate_limit.as_millis()
}
#[inline]
#[must_use]
pub fn as_micros(&self) -> u128 {
self.rate_limit.as_micros()
}
}
impl TicketCounter {
pub async fn take(&mut self, ctx: &Context, msg: &Message, id: u64) -> Option<RateLimitInfo> {
if let Some(ref check) = self.check {
if !(check)(ctx, msg).await {
return None;
}
}
let now = Instant::now();
let Self {
tickets_for,
ratelimit,
..
} = self;
let ticket_owner = tickets_for.entry(id).or_insert_with(|| UnitRatelimit::new(now));
if let Some((timespan, limit)) = ratelimit.limit {
if (ticket_owner.tickets + 1) > limit {
if let Some(ratelimit) =
(ticket_owner.set_time + timespan).checked_duration_since(now)
{
let was_first_try = ticket_owner.is_first_try;
let action = if self.await_ratelimits > ticket_owner.awaiting {
ticket_owner.awaiting += 1;
if let Some(delay_action) = self.delay_action {
let ctx = ctx.clone();
let msg = msg.clone();
spawn_named("buckets::delay_action", async move {
delay_action(&ctx, &msg).await;
});
}
RateLimitAction::Delayed
} else if self.await_ratelimits > 0 {
ticket_owner.is_first_try = false;
RateLimitAction::FailedDelay
} else {
ticket_owner.is_first_try = false;
RateLimitAction::Cancelled
};
return Some(RateLimitInfo {
rate_limit: ratelimit,
active_delays: ticket_owner.awaiting,
max_delays: self.await_ratelimits,
action,
is_first_try: was_first_try,
});
}
ticket_owner.tickets = 0;
ticket_owner.set_time = now;
}
}
if let Some(ratelimit) =
ticket_owner.last_time.and_then(|x| (x + ratelimit.delay).checked_duration_since(now))
{
let was_first_try = ticket_owner.is_first_try;
let action = if self.await_ratelimits > ticket_owner.awaiting {
ticket_owner.awaiting += 1;
if let Some(delay_action) = self.delay_action {
let ctx = ctx.clone();
let msg = msg.clone();
spawn_named("buckets::delay_action", async move {
delay_action(&ctx, &msg).await;
});
}
RateLimitAction::Delayed
} else if self.await_ratelimits > 0 {
ticket_owner.is_first_try = false;
RateLimitAction::FailedDelay
} else {
RateLimitAction::Cancelled
};
return Some(RateLimitInfo {
rate_limit: ratelimit,
active_delays: ticket_owner.awaiting,
max_delays: self.await_ratelimits,
action,
is_first_try: was_first_try,
});
}
ticket_owner.awaiting = ticket_owner.awaiting.saturating_sub(1);
ticket_owner.tickets += 1;
ticket_owner.is_first_try = true;
ticket_owner.last_time = Some(now);
None
}
pub async fn give(&mut self, ctx: &Context, msg: &Message, id: u64) {
if let Some(ref check) = self.check {
if !(check)(ctx, msg).await {
return;
}
}
if let Some(ticket_owner) = self.tickets_for.get_mut(&id) {
if ticket_owner.tickets > 0 {
ticket_owner.tickets -= 1;
}
let delay = self.ratelimit.delay;
ticket_owner.last_time = ticket_owner.last_time.and_then(|i| i.checked_sub(delay));
}
}
}
#[derive(Debug)]
pub struct RevertBucket;
impl fmt::Display for RevertBucket {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("RevertBucket")
}
}
impl std::error::Error for RevertBucket {}
#[derive(Debug)]
pub enum LimitedFor {
Global,
User,
Guild,
Channel,
#[cfg(feature = "cache")]
Category,
}
impl Default for LimitedFor {
fn default() -> Self {
Self::User
}
}
pub struct BucketBuilder {
pub(crate) delay: Duration,
pub(crate) time_span: Duration,
pub(crate) limit: u32,
pub(crate) check: Option<Check>,
pub(crate) delay_action: Option<DelayHook>,
pub(crate) limited_for: LimitedFor,
pub(crate) await_ratelimits: u32,
}
impl Default for BucketBuilder {
fn default() -> Self {
Self {
delay: Duration::default(),
time_span: Duration::default(),
limit: 1,
check: None,
delay_action: None,
limited_for: LimitedFor::default(),
await_ratelimits: 0,
}
}
}
impl BucketBuilder {
#[must_use]
pub fn new_global() -> Self {
Self {
limited_for: LimitedFor::Global,
..Default::default()
}
}
#[must_use]
pub fn new_user() -> Self {
Self {
limited_for: LimitedFor::User,
..Default::default()
}
}
#[must_use]
pub fn new_guild() -> Self {
Self {
limited_for: LimitedFor::Guild,
..Default::default()
}
}
#[must_use]
pub fn new_channel() -> Self {
Self {
limited_for: LimitedFor::Channel,
..Default::default()
}
}
#[cfg(feature = "cache")]
#[must_use]
pub fn new_category() -> Self {
Self {
limited_for: LimitedFor::Category,
..Default::default()
}
}
#[inline]
pub fn delay(&mut self, secs: u64) -> &mut Self {
self.delay = Duration::from_secs(secs);
self
}
#[inline]
pub fn time_span(&mut self, secs: u64) -> &mut Self {
self.time_span = Duration::from_secs(secs);
self
}
#[inline]
pub fn limit(&mut self, n: u32) -> &mut Self {
self.limit = n;
self
}
#[inline]
pub fn check(&mut self, check: Check) -> &mut Self {
self.check = Some(check);
self
}
#[inline]
pub fn delay_action(&mut self, action: DelayHook) -> &mut Self {
self.delay_action = Some(action);
self
}
#[inline]
pub fn limit_for(&mut self, target: LimitedFor) -> &mut Self {
self.limited_for = target;
self
}
#[inline]
pub fn await_ratelimits(&mut self, amount: u32) -> &mut Self {
self.await_ratelimits = amount;
self
}
#[inline]
pub(crate) fn construct(self) -> Bucket {
let counter = TicketCounter {
ratelimit: Ratelimit {
delay: self.delay,
limit: Some((self.time_span, self.limit)),
},
tickets_for: HashMap::new(),
check: self.check,
delay_action: self.delay_action,
await_ratelimits: self.await_ratelimits,
};
match self.limited_for {
LimitedFor::User => Bucket::User(counter),
LimitedFor::Guild => Bucket::Guild(counter),
LimitedFor::Channel => Bucket::Channel(counter),
#[cfg(feature = "cache")]
LimitedFor::Category => Bucket::Category(counter),
LimitedFor::Global => Bucket::Global(counter),
}
}
}