diff --git a/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuCooldownAttribute.cs b/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuCooldownAttribute.cs
index bd88a20f2..d64b82204 100644
--- a/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuCooldownAttribute.cs
+++ b/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuCooldownAttribute.cs
@@ -1,305 +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.Globalization;
using System.Threading;
using System.Threading.Tasks;
using DisCatSharp.ApplicationCommands.Context;
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;
///
/// 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 = ContextMenuCooldownBucket.MakeId(userId, channelId, guildId);
+ 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 : IEquatable
+public sealed class ContextMenuCooldownBucket : CooldownBucket
{
- ///
- /// Gets the ID of the user with whom this cooldown is associated.
- ///
- public ulong UserId { get; }
-
- ///
- /// Gets the ID of the channel with which this cooldown is associated.
- ///
- public ulong ChannelId { get; }
-
- ///
- /// Gets the ID of the guild with which this cooldown is associated.
- ///
- public ulong GuildId { get; }
-
- ///
- /// Gets the ID of the bucket. This is used to distinguish between cooldown buckets.
- ///
- public string BucketId { get; }
-
- ///
- /// Gets the remaining number of uses before the cooldown is triggered.
- ///
- public int RemainingUses
- => Volatile.Read(ref this._remainingUses);
-
- private int _remainingUses;
-
- ///
- /// Gets the maximum number of times this command can be used in given timespan.
- ///
- public int MaxUses { get; }
-
- ///
- /// Gets the date and time at which the cooldown resets.
- ///
- public DateTimeOffset ResetsAt { get; internal set; }
-
- ///
- /// Gets the time after which this cooldown resets.
- ///
- public TimeSpan Reset { get; internal set; }
-
- ///
- /// Gets the semaphore used to lock the use value.
- ///
- private readonly SemaphoreSlim _usageSemaphore;
-
- ///
- /// 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 ContextMenuCooldownBucket(int maxUses, TimeSpan resetAfter, ulong userId = 0, ulong channelId = 0, ulong guildId = 0)
+ : base(maxUses, resetAfter, userId, channelId, guildId)
{
- 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 SemaphoreSlim(1, 1);
- }
-
- ///
- /// Decrements the remaining use counter.
- ///
- /// Whether decrement succeeded or not.
- internal async Task DecrementUseAsync()
- {
- await this._usageSemaphore.WaitAsync().ConfigureAwait(false);
-
- // 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;
- }
-
- // ...otherwise just fail
- this._usageSemaphore.Release();
- return success;
}
///
/// 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}";
-
- ///
- /// 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 ContextMenuCooldownBucket);
-
- ///
- /// Checks whether this is equal to another .
- ///
- /// to compare to.
- /// Whether the is equal to this .
- public bool Equals(ContextMenuCooldownBucket 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 ==(ContextMenuCooldownBucket bucket1, ContextMenuCooldownBucket 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 !=(ContextMenuCooldownBucket bucket1, ContextMenuCooldownBucket 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)}";
}
diff --git a/DisCatSharp.ApplicationCommands/Attributes/CooldownBucket.cs b/DisCatSharp.ApplicationCommands/Attributes/CooldownBucket.cs
new file mode 100644
index 000000000..5045d1e1f
--- /dev/null
+++ b/DisCatSharp.ApplicationCommands/Attributes/CooldownBucket.cs
@@ -0,0 +1,154 @@
+// 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.Attributes;
+
+public class CooldownBucket : IBucket, IEquatable
+{
+ public ulong UserId { get; }
+ public ulong ChannelId { get; }
+ public ulong GuildId { get; }
+ public string BucketId { get; }
+ public int RemainingUses => Volatile.Read(ref this._remainingUses);
+ public int MaxUses { get; }
+ public DateTimeOffset ResetsAt { get; internal set; }
+ public TimeSpan Reset { get; internal set; }
+
+ ///
+ /// Gets the semaphore used to lock the use value.
+ ///
+ private readonly SemaphoreSlim _usageSemaphore;
+
+ private 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);
+
+ // 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;
+ }
+
+ // ...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)}";
+
+
+}
diff --git a/DisCatSharp.ApplicationCommands/Attributes/ICooldown.cs b/DisCatSharp.ApplicationCommands/Attributes/IBucket.cs
similarity index 55%
copy from DisCatSharp.ApplicationCommands/Attributes/ICooldown.cs
copy to DisCatSharp.ApplicationCommands/Attributes/IBucket.cs
index 03fc52845..f1c89c52c 100644
--- a/DisCatSharp.ApplicationCommands/Attributes/ICooldown.cs
+++ b/DisCatSharp.ApplicationCommands/Attributes/IBucket.cs
@@ -1,67 +1,73 @@
// 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 DisCatSharp.ApplicationCommands.Context;
-using DisCatSharp.ApplicationCommands.Enums;
-
namespace DisCatSharp.ApplicationCommands.Attributes;
///
-/// Cooldown feature contract
+/// Defines the standard contract for bucket feature
///
-/// Type of in which this cooldown handles
-/// Type of Cooldown bucket
-public interface ICooldown
- where TContextType : BaseContext
- where TBucketType : IEquatable
+public interface IBucket
{
///
- /// Gets the maximum number of uses before this command triggers a cooldown for its bucket.
+ /// Gets the ID of the user whom this cooldown is associated
///
- int MaxUses { get; }
+ ulong UserId { get; }
///
- /// Gets the time after which the cooldown is reset.
+ /// Gets the ID of the channel with which this cooldown is associated
///
- TimeSpan Reset { get; }
+ ulong ChannelId { get; }
+
+ ///
+ /// Gets the ID of the guild with which this cooldown is associated
+ ///
+ ulong GuildId { get; }
+
+ ///
+ /// Gets the ID of the bucket. This is used to distinguish between cooldown buckets
+ ///
+ string BucketId { get; }
+
+ ///
+ /// Gets the remaining number of uses before the cooldown is triggered
+ ///
+ int RemainingUses { get; }
///
- /// Gets the type of the cooldown bucket. This determines how a cooldown is applied.
+ /// Gets the maximum number of times this command can be used in a given timespan
///
- CooldownBucketType BucketType { get; }
+ int MaxUses { get; }
///
- /// Calculates the cooldown remaining for given context.
+ /// Gets the date and time at which the cooldown resets
///
- /// Context for which to calculate the cooldown.
- /// Remaining cooldown, or zero if no cooldown is active
- TimeSpan GetRemainingCooldown(TContextType ctx);
+ DateTimeOffset ResetsAt { get; }
///
- /// Gets a cooldown bucket for given context
+ /// Get the time after which this cooldown resets
///
- /// Command context to get cooldown bucket for.
- /// Requested cooldown bucket, or null if one wasn't present
- TBucketType GetBucket(TContextType ctx);
+ TimeSpan Reset { get; }
+
+
}
diff --git a/DisCatSharp.ApplicationCommands/Attributes/ICooldown.cs b/DisCatSharp.ApplicationCommands/Attributes/ICooldown.cs
index 03fc52845..8ff80b2f6 100644
--- a/DisCatSharp.ApplicationCommands/Attributes/ICooldown.cs
+++ b/DisCatSharp.ApplicationCommands/Attributes/ICooldown.cs
@@ -1,67 +1,67 @@
// 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 DisCatSharp.ApplicationCommands.Context;
using DisCatSharp.ApplicationCommands.Enums;
namespace DisCatSharp.ApplicationCommands.Attributes;
///
/// Cooldown feature contract
///
/// Type of in which this cooldown handles
/// Type of Cooldown bucket
public interface ICooldown
where TContextType : BaseContext
- where TBucketType : IEquatable
+ where TBucketType : CooldownBucket
{
///
/// Gets the maximum number of uses before this command triggers a cooldown for its bucket.
///
int MaxUses { get; }
///
/// Gets the time after which the cooldown is reset.
///
TimeSpan Reset { get; }
///
/// Gets the type of the cooldown bucket. This determines how a cooldown is applied.
///
CooldownBucketType BucketType { get; }
///
/// Calculates the cooldown remaining for given context.
///
/// Context for which to calculate the cooldown.
/// Remaining cooldown, or zero if no cooldown is active
TimeSpan GetRemainingCooldown(TContextType ctx);
///
/// Gets a cooldown bucket for given context
///
/// Command context to get cooldown bucket for.
/// Requested cooldown bucket, or null if one wasn't present
TBucketType GetBucket(TContextType ctx);
}
diff --git a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs
index f4da2088a..202c2f11b 100644
--- a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs
+++ b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs
@@ -1,305 +1,158 @@
// 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.Globalization;
using System.Threading;
using System.Threading.Tasks;
using DisCatSharp.ApplicationCommands.Context;
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;
///
/// 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 = SlashCommandCooldownBucket.MakeId(userId, channelId, guildId);
+ 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 : IEquatable
+public sealed class SlashCommandCooldownBucket : CooldownBucket
{
- ///
- /// Gets the ID of the user with whom this cooldown is associated.
- ///
- public ulong UserId { get; }
-
- ///
- /// Gets the ID of the channel with which this cooldown is associated.
- ///
- public ulong ChannelId { get; }
-
- ///
- /// Gets the ID of the guild with which this cooldown is associated.
- ///
- public ulong GuildId { get; }
-
- ///
- /// Gets the ID of the bucket. This is used to distinguish between cooldown buckets.
- ///
- public string BucketId { get; }
-
- ///
- /// Gets the remaining number of uses before the cooldown is triggered.
- ///
- public int RemainingUses
- => Volatile.Read(ref this._remainingUses);
-
- private int _remainingUses;
-
- ///
- /// Gets the maximum number of times this command can be used in given timespan.
- ///
- public int MaxUses { get; }
-
- ///
- /// Gets the date and time at which the cooldown resets.
- ///
- public DateTimeOffset ResetsAt { get; internal set; }
-
- ///
- /// Gets the time after which this cooldown resets.
- ///
- public TimeSpan Reset { get; internal set; }
-
- ///
- /// Gets the semaphore used to lock the use value.
- ///
- private readonly SemaphoreSlim _usageSemaphore;
-
- ///
- /// 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 SlashCommandCooldownBucket(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 SemaphoreSlim(1, 1);
- }
-
- ///
- /// Decrements the remaining use counter.
- ///
- /// Whether decrement succeeded or not.
- internal async Task DecrementUseAsync()
- {
- await this._usageSemaphore.WaitAsync().ConfigureAwait(false);
-
- // 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;
- }
-
- // ...otherwise just fail
- this._usageSemaphore.Release();
- return success;
- }
-
///
/// 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}";
- ///
- /// 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 SlashCommandCooldownBucket);
-
- ///
- /// Checks whether this is equal to another .
- ///
- /// to compare to.
- /// Whether the is equal to this .
- public bool Equals(SlashCommandCooldownBucket 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 ==(SlashCommandCooldownBucket bucket1, SlashCommandCooldownBucket bucket2)
+ internal SlashCommandCooldownBucket(int maxUses, TimeSpan resetAfter, ulong userId = 0, ulong channelId = 0, ulong guildId = 0)
+ : base(maxUses, resetAfter, userId, channelId, guildId)
{
- 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 !=(SlashCommandCooldownBucket bucket1, SlashCommandCooldownBucket 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)}";
}