diff --git a/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuCooldownAttribute.cs b/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuCooldownAttribute.cs
index fa9c71c4c..a5475c6f9 100644
--- a/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuCooldownAttribute.cs
+++ b/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuCooldownAttribute.cs
@@ -1,156 +1,156 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using DisCatSharp.ApplicationCommands.Context;
using DisCatSharp.ApplicationCommands.Entities;
using DisCatSharp.ApplicationCommands.Enums;
namespace DisCatSharp.ApplicationCommands.Attributes;
///
/// Defines a cooldown for this command. This allows you to define how many times can users execute a specific command
///
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public sealed class ContextMenuCooldownAttribute : ContextMenuCheckBaseAttribute, ICooldown
{
///
/// Gets the maximum number of uses before this command triggers a cooldown for its bucket.
///
public int MaxUses { get; }
///
/// Gets the time after which the cooldown is reset.
///
public TimeSpan Reset { get; }
///
/// Gets the type of the cooldown bucket. This determines how cooldowns are applied.
///
public CooldownBucketType BucketType { get; }
///
/// Gets the cooldown buckets for this command.
///
- private readonly ConcurrentDictionary _buckets;
+ internal readonly ConcurrentDictionary _buckets;
///
/// Defines a cooldown for this command. This means that users will be able to use the command a specific number of times before they have to wait to use it again.
///
/// Number of times the command can be used before triggering a cooldown.
/// Number of seconds after which the cooldown is reset.
/// Type of cooldown bucket. This allows controlling whether the bucket will be cooled down per user, guild, channel, or globally.
public ContextMenuCooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType)
{
this.MaxUses = maxUses;
this.Reset = TimeSpan.FromSeconds(resetAfter);
this.BucketType = bucketType;
this._buckets = new ConcurrentDictionary();
}
///
/// Gets a cooldown bucket for given command context.
///
/// Command context to get cooldown bucket for.
/// Requested cooldown bucket, or null if one wasn't present.
public ContextMenuCooldownBucket GetBucket(ContextMenuContext ctx)
{
var bid = this.GetBucketId(ctx, out _, out _, out _);
this._buckets.TryGetValue(bid, out var bucket);
return bucket;
}
///
/// Calculates the cooldown remaining for given command context.
///
/// Context for which to calculate the cooldown.
/// Remaining cooldown, or zero if no cooldown is active.
public TimeSpan GetRemainingCooldown(ContextMenuContext ctx)
{
var bucket = this.GetBucket(ctx);
return bucket == null ? TimeSpan.Zero : bucket.RemainingUses > 0 ? TimeSpan.Zero : bucket.ResetsAt - DateTimeOffset.UtcNow;
}
///
/// Calculates bucket ID for given command context.
///
/// Context for which to calculate bucket ID for.
/// ID of the user with which this bucket is associated.
/// ID of the channel with which this bucket is associated.
/// ID of the guild with which this bucket is associated.
/// Calculated bucket ID.
private string GetBucketId(ContextMenuContext ctx, out ulong userId, out ulong channelId, out ulong guildId)
{
userId = 0ul;
if ((this.BucketType & CooldownBucketType.User) != 0)
userId = ctx.User.Id;
channelId = 0ul;
if ((this.BucketType & CooldownBucketType.Channel) != 0)
channelId = ctx.Channel.Id;
if ((this.BucketType & CooldownBucketType.Guild) != 0 && ctx.Guild == null)
channelId = ctx.Channel.Id;
guildId = 0ul;
if (ctx.Guild != null && (this.BucketType & CooldownBucketType.Guild) != 0)
guildId = ctx.Guild.Id;
var bid = CooldownBucket.MakeId(userId, channelId, guildId);
return bid;
}
///
/// Executes a check.
///
/// The command context.
public override async Task ExecuteChecksAsync(ContextMenuContext ctx)
{
var bid = this.GetBucketId(ctx, out var usr, out var chn, out var gld);
if (!this._buckets.TryGetValue(bid, out var bucket))
{
bucket = new ContextMenuCooldownBucket(this.MaxUses, this.Reset, usr, chn, gld);
this._buckets.AddOrUpdate(bid, bucket, (k, v) => bucket);
}
return await bucket.DecrementUseAsync().ConfigureAwait(false);
}
}
///
/// Represents a cooldown bucket for commands.
///
public sealed class ContextMenuCooldownBucket : CooldownBucket
{
internal ContextMenuCooldownBucket(int maxUses, TimeSpan resetAfter, ulong userId = 0, ulong channelId = 0, ulong guildId = 0)
: base(maxUses, resetAfter, userId, channelId, guildId)
{
}
///
/// Returns a string representation of this command cooldown bucket.
///
/// String representation of this command cooldown bucket.
public override string ToString() => $"Context Menu Command bucket {this.BucketId}";
}
diff --git a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs
index 8485e5514..2d679d87f 100644
--- a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs
+++ b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs
@@ -1,157 +1,157 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using DisCatSharp.ApplicationCommands.Context;
using DisCatSharp.ApplicationCommands.Entities;
using DisCatSharp.ApplicationCommands.Enums;
namespace DisCatSharp.ApplicationCommands.Attributes;
///
/// Defines a cooldown for this command. This allows you to define how many times can users execute a specific command
///
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public sealed class SlashCommandCooldownAttribute : SlashCheckBaseAttribute, ICooldown
{
///
/// Gets the maximum number of uses before this command triggers a cooldown for its bucket.
///
public int MaxUses { get; }
///
/// Gets the time after which the cooldown is reset.
///
public TimeSpan Reset { get; }
///
/// Gets the type of the cooldown bucket. This determines how cooldowns are applied.
///
public CooldownBucketType BucketType { get; }
///
/// Gets the cooldown buckets for this command.
///
- private readonly ConcurrentDictionary _buckets;
+ internal readonly ConcurrentDictionary _buckets;
///
/// Defines a cooldown for this command. This means that users will be able to use the command a specific number of times before they have to wait to use it again.
///
/// Number of times the command can be used before triggering a cooldown.
/// Number of seconds after which the cooldown is reset.
/// Type of cooldown bucket. This allows controlling whether the bucket will be cooled down per user, guild, channel, or globally.
public SlashCommandCooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType)
{
this.MaxUses = maxUses;
this.Reset = TimeSpan.FromSeconds(resetAfter);
this.BucketType = bucketType;
this._buckets = new ConcurrentDictionary();
}
///
/// Gets a cooldown bucket for given command context.
///
/// Command context to get cooldown bucket for.
/// Requested cooldown bucket, or null if one wasn't present.
public SlashCommandCooldownBucket GetBucket(InteractionContext ctx)
{
var bid = this.GetBucketId(ctx, out _, out _, out _);
this._buckets.TryGetValue(bid, out var bucket);
return bucket;
}
///
/// Calculates the cooldown remaining for given command context.
///
/// Context for which to calculate the cooldown.
/// Remaining cooldown, or zero if no cooldown is active.
public TimeSpan GetRemainingCooldown(InteractionContext ctx)
{
var bucket = this.GetBucket(ctx);
return bucket == null ? TimeSpan.Zero : bucket.RemainingUses > 0 ? TimeSpan.Zero : bucket.ResetsAt - DateTimeOffset.UtcNow;
}
///
/// Calculates bucket ID for given command context.
///
/// Context for which to calculate bucket ID for.
/// ID of the user with which this bucket is associated.
/// ID of the channel with which this bucket is associated.
/// ID of the guild with which this bucket is associated.
/// Calculated bucket ID.
private string GetBucketId(InteractionContext ctx, out ulong userId, out ulong channelId, out ulong guildId)
{
userId = 0ul;
if ((this.BucketType & CooldownBucketType.User) != 0)
userId = ctx.User.Id;
channelId = 0ul;
if ((this.BucketType & CooldownBucketType.Channel) != 0)
channelId = ctx.Channel.Id;
if ((this.BucketType & CooldownBucketType.Guild) != 0 && ctx.Guild == null)
channelId = ctx.Channel.Id;
guildId = 0ul;
if (ctx.Guild != null && (this.BucketType & CooldownBucketType.Guild) != 0)
guildId = ctx.Guild.Id;
var bid = CooldownBucket.MakeId(userId, channelId, guildId);
return bid;
}
///
/// Executes a check.
///
/// The command context.
public override async Task ExecuteChecksAsync(InteractionContext ctx)
{
var bid = this.GetBucketId(ctx, out var usr, out var chn, out var gld);
if (!this._buckets.TryGetValue(bid, out var bucket))
{
bucket = new SlashCommandCooldownBucket(this.MaxUses, this.Reset, usr, chn, gld);
this._buckets.AddOrUpdate(bid, bucket, (k, v) => bucket);
}
return await bucket.DecrementUseAsync().ConfigureAwait(false);
}
}
///
/// Represents a cooldown bucket for commands.
///
public sealed class SlashCommandCooldownBucket : CooldownBucket
{
///
/// Returns a string representation of this command cooldown bucket.
///
/// String representation of this command cooldown bucket.
public override string ToString() => $"Slash Command bucket {this.BucketId}";
internal SlashCommandCooldownBucket(int maxUses, TimeSpan resetAfter, ulong userId = 0, ulong channelId = 0, ulong guildId = 0)
: base(maxUses, resetAfter, userId, channelId, guildId)
{
}
}
diff --git a/DisCatSharp.ApplicationCommands/Entities/CooldownBucket.cs b/DisCatSharp.ApplicationCommands/Entities/CooldownBucket.cs
index dcc905856..b43cef26e 100644
--- a/DisCatSharp.ApplicationCommands/Entities/CooldownBucket.cs
+++ b/DisCatSharp.ApplicationCommands/Entities/CooldownBucket.cs
@@ -1,186 +1,186 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
namespace DisCatSharp.ApplicationCommands.Entities;
public class CooldownBucket : IBucket, IEquatable
{
///
/// The user id for this bucket.
///
public ulong UserId { get; }
///
/// The channel id for this bucket.
///
public ulong ChannelId { get; }
///
/// The guild id for this bucket.
///
public ulong GuildId { get; }
///
/// The id for this bucket.
///
public string BucketId { get; }
///
/// The remaining uses for this bucket.
///
public int RemainingUses => Volatile.Read(ref this._remainingUses);
///
/// The max uses for this bucket.
///
public int MaxUses { get; }
///
/// The datetime offset when this bucket resets.
///
public DateTimeOffset ResetsAt { get; internal set; }
///
/// The timespan when this bucket resets.
///
public TimeSpan Reset { get; internal set; }
///
/// Gets the semaphore used to lock the use value.
///
- private readonly SemaphoreSlim _usageSemaphore;
+ internal readonly SemaphoreSlim _usageSemaphore;
- private int _remainingUses;
+ internal int _remainingUses;
///
/// Creates a new command cooldown bucket.
///
/// Maximum number of uses for this bucket.
/// Time after which this bucket resets.
/// ID of the user with which this cooldown is associated.
/// ID of the channel with which this cooldown is associated.
/// ID of the guild with which this cooldown is associated.
internal CooldownBucket(int maxUses, TimeSpan resetAfter, ulong userId = 0, ulong channelId = 0, ulong guildId = 0)
{
this._remainingUses = maxUses;
this.MaxUses = maxUses;
this.ResetsAt = DateTimeOffset.UtcNow + resetAfter;
this.Reset = resetAfter;
this.UserId = userId;
this.ChannelId = channelId;
this.GuildId = guildId;
this.BucketId = MakeId(userId, channelId, guildId);
this._usageSemaphore = new(1, 1);
}
///
/// Decrements the remaining use counter.
///
/// Whether decrement succeeded or not.
internal async Task DecrementUseAsync()
{
await this._usageSemaphore.WaitAsync().ConfigureAwait(false);
- Console.WriteLine($"[DecrementUseAsync]: Remaining: {this.RemainingUses}/{this.MaxUses} Resets: {this.ResetsAt} Vars[u,c,g]: {this.UserId} {this.ChannelId} {this.GuildId} Id: {this.BucketId}");
+ Console.WriteLine($"[DecrementUseAsync]: Remaining: {this.RemainingUses}/{this.MaxUses} Resets: {this.ResetsAt} Now: {DateTimeOffset.UtcNow} Vars[u,c,g]: {this.UserId} {this.ChannelId} {this.GuildId} Id: {this.BucketId}");
// if we're past reset time...
var now = DateTimeOffset.UtcNow;
if (now >= this.ResetsAt)
{
// ...do the reset and set a new reset time
Interlocked.Exchange(ref this._remainingUses, this.MaxUses);
this.ResetsAt = now + this.Reset;
}
// check if we have any uses left, if we do...
var success = false;
if (this.RemainingUses > 0)
{
// ...decrement, and return success...
Interlocked.Decrement(ref this._remainingUses);
success = true;
}
- Console.WriteLine($"[DecrementUseAsync]: Remaining: {this.RemainingUses}/{this.MaxUses} Resets: {this.ResetsAt} Vars[u,c,g]: {this.UserId} {this.ChannelId} {this.GuildId} Id: {this.BucketId}");
+ Console.WriteLine($"[DecrementUseAsync]: Remaining: {this.RemainingUses}/{this.MaxUses} Resets: {this.ResetsAt} Now: {DateTimeOffset.UtcNow} Vars[u,c,g]: {this.UserId} {this.ChannelId} {this.GuildId} Id: {this.BucketId}");
// ...otherwise just fail
this._usageSemaphore.Release();
return success;
}
///
/// Checks whether this is equal to another object.
///
/// Object to compare to.
/// Whether the object is equal to this .
public override bool Equals(object obj) => this.Equals(obj as CooldownBucket);
///
/// Checks whether this is equal to another .
///
/// to compare to.
/// Whether the is equal to this .
public bool Equals(CooldownBucket other) => other is not null && (ReferenceEquals(this, other) || (this.UserId == other.UserId && this.ChannelId == other.ChannelId && this.GuildId == other.GuildId));
///
/// Gets the hash code for this .
///
/// The hash code for this .
public override int GetHashCode() => HashCode.Combine(this.UserId, this.ChannelId, this.GuildId);
///
/// Gets whether the two objects are equal.
///
/// First bucket to compare.
/// Second bucket to compare.
/// Whether the two buckets are equal.
public static bool operator ==(CooldownBucket bucket1, CooldownBucket bucket2)
{
var null1 = bucket1 is null;
var null2 = bucket2 is null;
return (null1 && null2) || (null1 == null2 && null1.Equals(null2));
}
///
/// Gets whether the two objects are not equal.
///
/// First bucket to compare.
/// Second bucket to compare.
/// Whether the two buckets are not equal.
public static bool operator !=(CooldownBucket bucket1, CooldownBucket bucket2)
=> !(bucket1 == bucket2);
///
/// Creates a bucket ID from given bucket parameters.
///
/// ID of the user with which this cooldown is associated.
/// ID of the channel with which this cooldown is associated.
/// ID of the guild with which this cooldown is associated.
/// Generated bucket ID.
public static string MakeId(ulong userId = 0, ulong channelId = 0, ulong guildId = 0)
=> $"{userId.ToString(CultureInfo.InvariantCulture)}:{channelId.ToString(CultureInfo.InvariantCulture)}:{guildId.ToString(CultureInfo.InvariantCulture)}";
}