From 5da2d94e87dee6e2966b9d67d816ca8d5df739df Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Wed, 17 Jan 2024 14:02:05 +0100 Subject: [PATCH] feat: custom cooldown responder --- .../ApplicationCommandsExtension.cs | 1 - .../ContextMenuCooldownAttribute.cs | 29 ++++++++++++++---- .../SlashCommandCooldownAttribute.cs | 26 ++++++++++++---- .../Entities/ICooldownResponder.cs | 17 +++++++++++ .../Attributes/CooldownAttribute.cs | 30 ++++++++++++++----- .../Entities/ICooldownResponder.cs | 16 ++++++++++ .../Application/DiscordApplicationCommand.cs | 11 +++---- 7 files changed, 107 insertions(+), 23 deletions(-) create mode 100644 DisCatSharp.ApplicationCommands/Entities/ICooldownResponder.cs create mode 100644 DisCatSharp.CommandsNext/Entities/ICooldownResponder.cs diff --git a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs index 0c8dc8f11e..980c20c53f 100644 --- a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs +++ b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; diff --git a/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuCooldownAttribute.cs b/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuCooldownAttribute.cs index 7c62db5aa3..76a34f584f 100644 --- a/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuCooldownAttribute.cs +++ b/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuCooldownAttribute.cs @@ -1,13 +1,17 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.Threading.Tasks; using DisCatSharp.ApplicationCommands.Context; +using DisCatSharp.ApplicationCommands.Entities; using DisCatSharp.Entities; using DisCatSharp.Entities.Core; using DisCatSharp.Enums; using DisCatSharp.Enums.Core; +using Sentry; + namespace DisCatSharp.ApplicationCommands.Attributes; /// @@ -19,8 +23,9 @@ namespace DisCatSharp.ApplicationCommands.Attributes; /// 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, member, channel, and/or globally. +/// The responder type used to respond to cooldown ratelimit hits. [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = false)] -public sealed class ContextMenuCooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType) : ApplicationCommandCheckBaseAttribute, ICooldown +public sealed class ContextMenuCooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType, Type? cooldownResponderType = null) : ApplicationCommandCheckBaseAttribute, ICooldown { /// /// Gets the maximum number of uses before this command triggers a cooldown for its bucket. @@ -37,6 +42,11 @@ public sealed class ContextMenuCooldownAttribute(int maxUses, double resetAfter, /// public CooldownBucketType BucketType { get; } = bucketType; + /// + /// Gets the responder type. + /// + public Type? ResponderType { get; } = cooldownResponderType; + /// /// Gets a cooldown bucket for given command context. /// @@ -117,10 +127,19 @@ public async Task RespondRatelimitHitAsync(BaseContext ctx, bool noHit, Co if (noHit) return true; - if (ApplicationCommandsExtension.Configuration.AutoDefer) - await ctx.EditResponseAsync(new DiscordWebhookBuilder().WithContent($"Error: Ratelimit hit\nTry again {bucket.ResetsAt.Timestamp()}")); - else - await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent($"Error: Ratelimit hit\nTry again {bucket.ResetsAt.Timestamp()}").AsEphemeral()); + if (this.ResponderType is null) + { + if (ApplicationCommandsExtension.Configuration.AutoDefer) + await ctx.EditResponseAsync(new DiscordWebhookBuilder().WithContent($"Error: Ratelimit hit\nTry again {bucket.ResetsAt.Timestamp()}")); + else + await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent($"Error: Ratelimit hit\nTry again {bucket.ResetsAt.Timestamp()}").AsEphemeral()); + + return false; + } + + var providerMethod = this.ResponderType.GetMethod(nameof(ICooldownResponder.Responder)); + var providerInstance = Activator.CreateInstance(this.ResponderType); + await ((Task)providerMethod.Invoke(providerInstance, [ctx])).ConfigureAwait(false); return false; } diff --git a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs index 5ec4917cac..5a6d14efef 100644 --- a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs +++ b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using DisCatSharp.ApplicationCommands.Context; +using DisCatSharp.ApplicationCommands.Entities; using DisCatSharp.Entities; using DisCatSharp.Entities.Core; using DisCatSharp.Enums; @@ -19,8 +20,9 @@ namespace DisCatSharp.ApplicationCommands.Attributes; /// 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, member, channel, and/or globally. +/// The responder type used to respond to cooldown ratelimit hits. [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = false)] -public sealed class SlashCommandCooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType) : ApplicationCommandCheckBaseAttribute, ICooldown +public sealed class SlashCommandCooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType, Type? cooldownResponderType = null) : ApplicationCommandCheckBaseAttribute, ICooldown { /// /// Gets the maximum number of uses before this command triggers a cooldown for its bucket. @@ -37,6 +39,11 @@ public sealed class SlashCommandCooldownAttribute(int maxUses, double resetAfter /// public CooldownBucketType BucketType { get; } = bucketType; + /// + /// Gets the responder type. + /// + public Type? ResponderType { get; } = cooldownResponderType; + /// /// Gets a cooldown bucket for given command context. /// @@ -117,10 +124,19 @@ public async Task RespondRatelimitHitAsync(BaseContext ctx, bool noHit, Co if (noHit) return true; - if (ApplicationCommandsExtension.Configuration.AutoDefer) - await ctx.EditResponseAsync(new DiscordWebhookBuilder().WithContent($"Error: Ratelimit hit\nTry again {bucket.ResetsAt.Timestamp()}")); - else - await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent($"Error: Ratelimit hit\nTry again {bucket.ResetsAt.Timestamp()}").AsEphemeral()); + if (this.ResponderType is null) + { + if (ApplicationCommandsExtension.Configuration.AutoDefer) + await ctx.EditResponseAsync(new DiscordWebhookBuilder().WithContent($"Error: Ratelimit hit\nTry again {bucket.ResetsAt.Timestamp()}")); + else + await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent($"Error: Ratelimit hit\nTry again {bucket.ResetsAt.Timestamp()}").AsEphemeral()); + + return false; + } + + var providerMethod = this.ResponderType.GetMethod(nameof(ICooldownResponder.Responder)); + var providerInstance = Activator.CreateInstance(this.ResponderType); + await ((Task)providerMethod.Invoke(providerInstance, [ctx])).ConfigureAwait(false); return false; } diff --git a/DisCatSharp.ApplicationCommands/Entities/ICooldownResponder.cs b/DisCatSharp.ApplicationCommands/Entities/ICooldownResponder.cs new file mode 100644 index 0000000000..da4e140241 --- /dev/null +++ b/DisCatSharp.ApplicationCommands/Entities/ICooldownResponder.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; + +using DisCatSharp.ApplicationCommands.Context; + +namespace DisCatSharp.ApplicationCommands.Entities; + +/// +/// The cooldown responder. +/// +public interface ICooldownResponder +{ + /// + /// Responds to cooldown ratelimit hits with given response. + /// + /// The context. + Task Responder(BaseContext context); +} diff --git a/DisCatSharp.CommandsNext/Attributes/CooldownAttribute.cs b/DisCatSharp.CommandsNext/Attributes/CooldownAttribute.cs index 90990f0538..796dbec21b 100644 --- a/DisCatSharp.CommandsNext/Attributes/CooldownAttribute.cs +++ b/DisCatSharp.CommandsNext/Attributes/CooldownAttribute.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; +using DisCatSharp.CommandsNext.Entities; using DisCatSharp.Entities; using DisCatSharp.Entities.Core; using DisCatSharp.Enums.Core; @@ -17,8 +18,9 @@ namespace DisCatSharp.CommandsNext.Attributes; /// 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. +/// The responder type used to respond to cooldown ratelimit hits. [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = false)] -public sealed class CooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType) : CheckBaseAttribute, ICooldown +public sealed class CooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType, Type? cooldownResponderType = null) : CheckBaseAttribute, ICooldown { /// /// Gets the maximum number of uses before this command triggers a cooldown for its bucket. @@ -35,6 +37,11 @@ public sealed class CooldownAttribute(int maxUses, double resetAfter, CooldownBu /// public CooldownBucketType BucketType { get; } = bucketType; + /// + /// Gets the responder type. + /// + public Type? ResponderType { get; } = cooldownResponderType; + /// /// Gets a cooldown bucket for given command context. /// @@ -116,15 +123,24 @@ public async Task RespondRatelimitHitAsync(CommandContext ctx, bool noHit, if (noHit) return true; - try + if (this.ResponderType is null) { - await ctx.Message.CreateReactionAsync(DiscordEmoji.FromName(ctx.Client, ":x:", false)); - } - catch (UnauthorizedException) - { - // ignore + try + { + await ctx.Message.CreateReactionAsync(DiscordEmoji.FromName(ctx.Client, ":x:", false)); + } + catch (UnauthorizedException) + { + // ignore + } + + return false; } + var providerMethod = this.ResponderType.GetMethod(nameof(ICooldownResponder.Responder)); + var providerInstance = Activator.CreateInstance(this.ResponderType); + await ((Task)providerMethod.Invoke(providerInstance, [ctx])).ConfigureAwait(false); + return false; } } diff --git a/DisCatSharp.CommandsNext/Entities/ICooldownResponder.cs b/DisCatSharp.CommandsNext/Entities/ICooldownResponder.cs new file mode 100644 index 0000000000..8465c45bb8 --- /dev/null +++ b/DisCatSharp.CommandsNext/Entities/ICooldownResponder.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; + +namespace DisCatSharp.CommandsNext.Entities; + +/// +/// The cooldown responder. +/// +public interface ICooldownResponder +{ + /// + /// Responds to cooldown ratelimit hits with given actions. + /// For example you could respond with a reaction or send a dm. + /// + /// The context. + Task Responder(CommandContext context); +} diff --git a/DisCatSharp/Entities/Application/DiscordApplicationCommand.cs b/DisCatSharp/Entities/Application/DiscordApplicationCommand.cs index c71ad1f8ac..5ca8cfdd46 100644 --- a/DisCatSharp/Entities/Application/DiscordApplicationCommand.cs +++ b/DisCatSharp/Entities/Application/DiscordApplicationCommand.cs @@ -48,8 +48,8 @@ public DiscordApplicationCommandLocalization? NameLocalizations /// /// Gets the description of this command. /// - [JsonProperty("description")] - public string Description { get; internal set; } + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + public string? Description { get; internal set; } /// /// Sets the description localizations. @@ -110,7 +110,8 @@ public DiscordApplicationCommandLocalization? DescriptionLocalizations /// Gets the mention for this command. /// [JsonIgnore] - public string Mention => this.Type == ApplicationCommandType.ChatInput ? $"" : this.Name; + public string Mention + => this.Type == ApplicationCommandType.ChatInput ? $"" : this.Name; /// /// Creates a new instance of a . @@ -128,7 +129,7 @@ public DiscordApplicationCommandLocalization? DescriptionLocalizations /// The allowed integration types. public DiscordApplicationCommand( string name, - string description, + string? description, IEnumerable? options = null, ApplicationCommandType type = ApplicationCommandType.ChatInput, DiscordApplicationCommandLocalization? nameLocalizations = null, @@ -147,7 +148,7 @@ public DiscordApplicationCommand( throw new ArgumentException("Invalid slash command name specified. It must be below 32 characters and not contain any whitespace.", nameof(name)); if (name.Any(char.IsUpper)) throw new ArgumentException("Slash command name cannot have any upper case characters.", nameof(name)); - if (description.Length > 100) + if (description?.Length > 100) throw new ArgumentException("Slash command description cannot exceed 100 characters.", nameof(description)); if (string.IsNullOrWhiteSpace(description)) throw new ArgumentException("Slash commands need a description.", nameof(description));