Skip to content

Commit

Permalink
feat: move cooldown to discatsharp base package
Browse files Browse the repository at this point in the history
  • Loading branch information
Lulalaby committed Jan 16, 2024
1 parent a8ad5f3 commit 99e344c
Show file tree
Hide file tree
Showing 19 changed files with 257 additions and 424 deletions.
34 changes: 21 additions & 13 deletions DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
using DisCatSharp.Common.Utilities;
using DisCatSharp.Entities;
using DisCatSharp.Enums;
using DisCatSharp.Enums.Core;
using DisCatSharp.EventArgs;
using DisCatSharp.Exceptions;

Expand Down Expand Up @@ -85,16 +86,6 @@ public sealed class ApplicationCommandsExtension : BaseExtension
/// </summary>
private static bool s_errored { get; set; }

/// <summary>
/// Gets the cooldown buckets for slash commands.
/// </summary>
public static ConcurrentDictionary<string, SlashCommandCooldownBucket> SlashCommandBuckets { get; } = [];

/// <summary>
/// Gets the cooldown buckets for context menu commands.
/// </summary>
public static ConcurrentDictionary<string, ContextMenuCooldownBucket> ContextMenuCommandBuckets { get; } = [];

/// <summary>
/// Gets a list of registered commands. The key is the guild id (null if global).
/// </summary>
Expand Down Expand Up @@ -1091,7 +1082,11 @@ private Task InteractionHandler(DiscordClient client, InteractionCreateEventArgs
GuildLocale = e.Interaction.GuildLocale,
AppPermissions = e.Interaction.AppPermissions,
Entitlements = e.Interaction.Entitlements,
EntitlementSkuIds = e.Interaction.EntitlementSkuIds
EntitlementSkuIds = e.Interaction.EntitlementSkuIds,
UserId = e.Interaction.User.Id,
GuildId = e.Interaction.GuildId,
MemberId = e.Interaction.GuildId is not null ? e.Interaction.User.Id : null,
ChannelId = e.Interaction.ChannelId
};

try
Expand All @@ -1118,6 +1113,7 @@ private Task InteractionHandler(DiscordClient client, InteractionCreateEventArgs
var method = methods.First().Method;
context.SubCommandName = null;
context.SubSubCommandName = null;
context.CommandGroupingType = DisCatSharpCommandGroupingType.Command;
if (DebugEnabled)
this.Client.Logger.LogDebug("Executing {cmd}", method.Name);
var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options).ConfigureAwait(false);
Expand All @@ -1131,6 +1127,7 @@ private Task InteractionHandler(DiscordClient client, InteractionCreateEventArgs
var method = groups.First().Methods.First(x => x.Key == command.Name).Value;
context.SubCommandName = command.Name;
context.SubSubCommandName = null;
context.CommandGroupingType = DisCatSharpCommandGroupingType.SubCommand;
if (DebugEnabled)
this.Client.Logger.LogDebug("Executing {cmd}", method.Name);
var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options[0].Options).ConfigureAwait(false);
Expand All @@ -1146,6 +1143,7 @@ private Task InteractionHandler(DiscordClient client, InteractionCreateEventArgs
var method = group.Methods.First(x => x.Key == command.Options[0].Name).Value;
context.SubCommandName = command.Name;
context.SubSubCommandName = command.Options[0].Name;
context.CommandGroupingType = DisCatSharpCommandGroupingType.SubGroupCommand;

if (DebugEnabled)
this.Client.Logger.LogDebug("Executing {cmd}", method.Name);
Expand Down Expand Up @@ -1350,7 +1348,12 @@ private Task ContextMenuHandler(DiscordClient client, ContextMenuInteractionCrea
_ = Task.Run(async () =>
{
//Creates the context
var context = new ContextMenuContext
var context = new ContextMenuContext(e.Type switch
{
ApplicationCommandType.User => DisCatSharpCommandType.UserCommand,
ApplicationCommandType.Message => DisCatSharpCommandType.MessageCommand,
_ => throw new ArgumentOutOfRangeException(nameof(e.Type), "Unknown context menu type")
})
{
Interaction = e.Interaction,
Channel = e.Interaction.Channel,
Expand All @@ -1369,7 +1372,12 @@ private Task ContextMenuHandler(DiscordClient client, ContextMenuInteractionCrea
GuildLocale = e.Interaction.GuildLocale,
AppPermissions = e.Interaction.AppPermissions,
Entitlements = e.Interaction.Entitlements,
EntitlementSkuIds = e.Interaction.EntitlementSkuIds
EntitlementSkuIds = e.Interaction.EntitlementSkuIds,
CommandGroupingType = DisCatSharpCommandGroupingType.Command,
UserId = e.Interaction.User.Id,
GuildId = e.Interaction.GuildId,
MemberId = e.Interaction.GuildId is not null ? e.Interaction.User.Id : null,
ChannelId = e.Interaction.ChannelId
};

try
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
using System;
using System.Collections.Concurrent;
using System.ComponentModel.Design;
using System.Globalization;
using System.Threading.Tasks;

using DisCatSharp.ApplicationCommands.Context;
using DisCatSharp.ApplicationCommands.Entities;
using DisCatSharp.ApplicationCommands.Enums;
using DisCatSharp.Entities;
using DisCatSharp.Entities.Core;
using DisCatSharp.Enums;
using DisCatSharp.Enums.Core;

namespace DisCatSharp.ApplicationCommands.Attributes;

/// <summary>
/// Defines a cooldown for this command. This allows you to define how many times can users execute a specific command
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public sealed class ContextMenuCooldownAttribute : ApplicationCommandCheckBaseAttribute, ICooldown<BaseContext, ContextMenuCooldownBucket>
public sealed class ContextMenuCooldownAttribute : ApplicationCommandCheckBaseAttribute, ICooldown<BaseContext, CooldownBucket>
{
/// <summary>
/// Gets the maximum number of uses before this command triggers a cooldown for its bucket.
Expand Down Expand Up @@ -48,10 +49,10 @@ public ContextMenuCooldownAttribute(int maxUses, double resetAfter, CooldownBuck
/// </summary>
/// <param name="ctx">Command context to get cooldown bucket for.</param>
/// <returns>Requested cooldown bucket, or null if one wasn't present.</returns>
public ContextMenuCooldownBucket GetBucket(BaseContext ctx)
public CooldownBucket GetBucket(BaseContext ctx)
{
var bid = this.GetBucketId(ctx, out _, out _, out _, out _);
ApplicationCommandsExtension.ContextMenuCommandBuckets.TryGetValue(bid, out var bucket);
ctx.Client.CommandCooldownBuckets.TryGetValue(bid, out var bucket);
return bucket!;
}

Expand Down Expand Up @@ -97,7 +98,7 @@ private string GetBucketId(BaseContext ctx, out ulong userId, out ulong channelI
if (ctx.Guild is not null && ctx.Member is not null && (this.BucketType & CooldownBucketType.Member) != 0)
memberId = ctx.Member.Id;

var bid = CooldownBucket.MakeId(ctx.Interaction.Data.Id, ctx.FullCommandName, userId, channelId, guildId, memberId);
var bid = CooldownBucket.MakeId(ctx.FullCommandName, ctx.Interaction.Data.Id.ToString(CultureInfo.InvariantCulture), userId, channelId, guildId, memberId);
return bid;
}

Expand All @@ -108,40 +109,26 @@ private string GetBucketId(BaseContext ctx, out ulong userId, out ulong channelI
public override async Task<bool> ExecuteChecksAsync(BaseContext ctx)
{
var bid = this.GetBucketId(ctx, out var usr, out var chn, out var gld, out var mem);
if (ApplicationCommandsExtension.ContextMenuCommandBuckets.TryGetValue(bid, out var bucket))
return await bucket.DecrementUseAsync(ctx).ConfigureAwait(false);
if (ctx.Client.CommandCooldownBuckets.TryGetValue(bid, out var bucket))
return await this.RespondRatelimitHitAsync(ctx, await bucket.DecrementUseAsync(ctx), bucket);

bucket = new(this.MaxUses, this.Reset, ctx.Interaction.Data.Id, ctx.FullCommandName, usr, chn, gld, mem);
ApplicationCommandsExtension.ContextMenuCommandBuckets.AddOrUpdate(bid, bucket, (k, v) => bucket);
bucket = new(this.MaxUses, this.Reset, ctx.FullCommandName, ctx.Interaction.Data.Id.ToString(CultureInfo.InvariantCulture), usr, chn, gld, mem);
ctx.Client.CommandCooldownBuckets.AddOrUpdate(bid, bucket, (k, v) => bucket);

return await bucket.DecrementUseAsync(ctx).ConfigureAwait(false);
return await this.RespondRatelimitHitAsync(ctx, await bucket.DecrementUseAsync(ctx), bucket);
}
}

/// <summary>
/// Represents a cooldown bucket for context menu commands.
/// </summary>
public sealed class ContextMenuCooldownBucket : CooldownBucket
{
/// <summary>
/// Creates a new context menu command cooldown bucket.
/// </summary>
/// <param name="maxUses">Maximum number of uses for this bucket.</param>
/// <param name="resetAfter">Time after which this bucket resets.</param>
/// <param name="commandId">ID of the command</param>
/// <param name="commandName">Name of the command.</param>
/// <param name="userId">ID of the user with which this cooldown is associated.</param>
/// <param name="channelId">ID of the channel with which this cooldown is associated.</param>
/// <param name="guildId">ID of the guild with which this cooldown is associated.</param>
/// <param name="memberId">ID of the member with which this cooldown is associated.</param>
internal ContextMenuCooldownBucket(int maxUses, TimeSpan resetAfter, ulong commandId, string commandName, ulong userId = 0, ulong channelId = 0, ulong guildId = 0, ulong memberId = 0)
: base(maxUses, resetAfter, commandId, commandName, userId, channelId, guildId, memberId)
{ }
/// <inheritdoc/>
public async Task<bool> RespondRatelimitHitAsync(BaseContext ctx, bool noHit, CooldownBucket bucket)
{
if (noHit)
return true;

/// <summary>
/// Returns a string representation of this command cooldown bucket.
/// </summary>
/// <returns>String representation of this command cooldown bucket.</returns>
public override string ToString()
=> $"Context Menu Command bucket {this.BucketId}";
if (ApplicationCommandsExtension.Configuration.AutoDefer)
await ctx.EditResponseAsync(new DiscordWebhookBuilder().WithContent($"Error: Ratelimit hit\nTry again in {bucket.ResetsAt.Timestamp()}"));
else
await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent($"Error: Ratelimit hit\nTry again in {bucket.ResetsAt.Timestamp()}").AsEphemeral());

return false;
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
using System;
using System.Collections.Concurrent;
using System.Globalization;
using System.Threading.Tasks;

using DisCatSharp.ApplicationCommands.Context;
using DisCatSharp.ApplicationCommands.Entities;
using DisCatSharp.ApplicationCommands.Enums;
using DisCatSharp.Entities;
using DisCatSharp.Entities.Core;
using DisCatSharp.Enums;
using DisCatSharp.Enums.Core;

namespace DisCatSharp.ApplicationCommands.Attributes;

/// <summary>
/// Defines a cooldown for this command. This allows you to define how many times can users execute a specific command.
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public sealed class SlashCommandCooldownAttribute : ApplicationCommandCheckBaseAttribute, ICooldown<BaseContext, SlashCommandCooldownBucket>
public sealed class SlashCommandCooldownAttribute : ApplicationCommandCheckBaseAttribute, ICooldown<BaseContext, CooldownBucket>
{
/// <summary>
/// Gets the maximum number of uses before this command triggers a cooldown for its bucket.
Expand Down Expand Up @@ -47,10 +49,10 @@ public SlashCommandCooldownAttribute(int maxUses, double resetAfter, CooldownBuc
/// </summary>
/// <param name="ctx">Command context to get cooldown bucket for.</param>
/// <returns>Requested cooldown bucket, or null if one wasn't present.</returns>
public SlashCommandCooldownBucket GetBucket(BaseContext ctx)
public CooldownBucket GetBucket(BaseContext ctx)
{
var bid = this.GetBucketId(ctx, out _, out _, out _, out _);
ApplicationCommandsExtension.SlashCommandBuckets.TryGetValue(bid, out var bucket);
ctx.Client.CommandCooldownBuckets.TryGetValue(bid, out var bucket);
return bucket!;
}

Expand Down Expand Up @@ -96,7 +98,7 @@ private string GetBucketId(BaseContext ctx, out ulong userId, out ulong channelI
if (ctx.Guild is not null && ctx.Member is not null && this.BucketType.HasFlag(CooldownBucketType.Member))
memberId = ctx.Member.Id;

var bid = CooldownBucket.MakeId(ctx.Interaction.Data.Id, ctx.FullCommandName, userId, channelId, guildId, memberId);
var bid = CooldownBucket.MakeId(ctx.FullCommandName, ctx.Interaction.Data.Id.ToString(CultureInfo.InvariantCulture), userId, channelId, guildId, memberId);
return bid;
}

Expand All @@ -107,40 +109,26 @@ private string GetBucketId(BaseContext ctx, out ulong userId, out ulong channelI
public override async Task<bool> ExecuteChecksAsync(BaseContext ctx)
{
var bid = this.GetBucketId(ctx, out var usr, out var chn, out var gld, out var mem);
if (ApplicationCommandsExtension.SlashCommandBuckets.TryGetValue(bid, out var bucket))
return await bucket.DecrementUseAsync(ctx).ConfigureAwait(false);
if (ctx.Client.CommandCooldownBuckets.TryGetValue(bid, out var bucket))
return await this.RespondRatelimitHitAsync(ctx, await bucket.DecrementUseAsync(ctx), bucket);

bucket = new(this.MaxUses, this.Reset, ctx.Interaction.Data.Id, ctx.FullCommandName, usr, chn, gld, mem);
ApplicationCommandsExtension.SlashCommandBuckets.AddOrUpdate(bid, bucket, (k, v) => bucket);
bucket = new(this.MaxUses, this.Reset, ctx.FullCommandName, ctx.Interaction.Data.Id.ToString(CultureInfo.InvariantCulture), usr, chn, gld, mem);
ctx.Client.CommandCooldownBuckets.AddOrUpdate(bid, bucket, (k, v) => bucket);

return await bucket.DecrementUseAsync(ctx).ConfigureAwait(false);
return await this.RespondRatelimitHitAsync(ctx, await bucket.DecrementUseAsync(ctx), bucket);
}
}

/// <summary>
/// Represents a cooldown bucket for slash commands.
/// </summary>
public sealed class SlashCommandCooldownBucket : CooldownBucket
{
/// <summary>
/// Creates a new slash command cooldown bucket.
/// </summary>
/// <param name="maxUses">Maximum number of uses for this bucket.</param>
/// <param name="resetAfter">Time after which this bucket resets.</param>
/// <param name="commandId">ID of the command</param>
/// <param name="commandName">Name of the command.</param>
/// <param name="userId">ID of the user with which this cooldown is associated.</param>
/// <param name="channelId">ID of the channel with which this cooldown is associated.</param>
/// <param name="guildId">ID of the guild with which this cooldown is associated.</param>
/// <param name="memberId">ID of the member with which this cooldown is associated.</param>
internal SlashCommandCooldownBucket(int maxUses, TimeSpan resetAfter, ulong commandId, string commandName, ulong userId = 0, ulong channelId = 0, ulong guildId = 0, ulong memberId = 0)
: base(maxUses, resetAfter, commandId, commandName, userId, channelId, guildId, memberId)
{ }
/// <inheritdoc/>
public async Task<bool> RespondRatelimitHitAsync(BaseContext ctx, bool noHit, CooldownBucket bucket)
{
if (noHit)
return true;

/// <summary>
/// Returns a string representation of this command cooldown bucket.
/// </summary>
/// <returns>String representation of this command cooldown bucket.</returns>
public override string ToString()
=> $"Slash Command bucket {this.BucketId}";
if (ApplicationCommandsExtension.Configuration.AutoDefer)
await ctx.EditResponseAsync(new DiscordWebhookBuilder().WithContent($"Error: Ratelimit hit\nTry again in {bucket.ResetsAt.Timestamp()}"));
else
await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent($"Error: Ratelimit hit\nTry again in {bucket.ResetsAt.Timestamp()}").AsEphemeral());

return false;
}
}
35 changes: 13 additions & 22 deletions DisCatSharp.ApplicationCommands/Context/BaseContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

using DisCatSharp.Attributes;
using DisCatSharp.Entities;
using DisCatSharp.Entities.Core;
using DisCatSharp.Enums;
using DisCatSharp.Enums.Core;

using Microsoft.Extensions.DependencyInjection;

Expand All @@ -13,18 +15,13 @@ namespace DisCatSharp.ApplicationCommands.Context;
/// <summary>
/// Represents a base context for application command contexts.
/// </summary>
public class BaseContext
public class BaseContext : DisCatSharpCommandContext
{
/// <summary>
/// Gets the interaction that was created.
/// </summary>
public DiscordInteraction Interaction { get; internal init; }

/// <summary>
/// Gets the client for this interaction.
/// </summary>
public DiscordClient Client { get; internal init; }

/// <summary>
/// Gets the guild this interaction was executed in.
/// </summary>
Expand Down Expand Up @@ -61,25 +58,10 @@ public DiscordMember? Member
/// </summary>
public ulong InteractionId { get; internal set; }

/// <summary>
/// Gets the name of the command.
/// </summary>
public string CommandName { get; internal init; }

/// <summary>
/// Gets the name of the sub command.
/// </summary>
public string? SubCommandName { get; internal set; }

/// <summary>
/// Gets the name of the sub command.
/// </summary>
public string? SubSubCommandName { get; internal set; }

/// <summary>
/// Gets the full command string, including the subcommand.
/// </summary>
public string FullCommandName
public override string FullCommandName
=> $"{this.CommandName}{(string.IsNullOrWhiteSpace(this.SubCommandName) ? "" : $" {this.SubCommandName}")}{(string.IsNullOrWhiteSpace(this.SubSubCommandName) ? "" : $" {this.SubSubCommandName}")}";

/// <summary>
Expand Down Expand Up @@ -125,6 +107,15 @@ public string FullCommandName
/// </summary>
public IServiceProvider Services { get; internal set; } = new ServiceCollection().BuildServiceProvider(true);

/// <summary>
/// Initializes a new instance of the <see cref="BaseContext"/> class.
/// </summary>
/// <param name="type">The command type.</param>
/// <param name="groupingType">The command grouping type.</param>
internal BaseContext(DisCatSharpCommandType type, DisCatSharpCommandGroupingType groupingType = DisCatSharpCommandGroupingType.Command)
: base(type, groupingType)
{ }

/// <summary>
/// Creates a response to this interaction.
/// <para>You must create a response within 3 seconds of this interaction being executed; if the command has the potential to take more than 3 seconds, create a <see cref="InteractionResponseType.DeferredChannelMessageWithSource"/> at the start, and edit the response later.</para>
Expand Down
5 changes: 3 additions & 2 deletions DisCatSharp.ApplicationCommands/Context/ContextMenuContext.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
using DisCatSharp.Entities;
using DisCatSharp.Enums.Core;

namespace DisCatSharp.ApplicationCommands.Context;

/// <summary>
/// Represents a context for a context menu.
/// </summary>
public sealed class ContextMenuContext : BaseContext
public sealed class ContextMenuContext(DisCatSharpCommandType type) : BaseContext(type)
{
/// <summary>
/// The user this command targets, if applicable.
Expand All @@ -15,7 +16,7 @@ public sealed class ContextMenuContext : BaseContext
/// <summary>
/// The member this command targets, if applicable.
/// </summary>
public DiscordMember TargetMember
public DiscordMember? TargetMember
=> this.TargetUser is DiscordMember member ? member : null;

/// <summary>
Expand Down
Loading

0 comments on commit 99e344c

Please sign in to comment.