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)}"; }