Skip to content

Commit

Permalink
feat: custom cooldown responder
Browse files Browse the repository at this point in the history
  • Loading branch information
Lulalaby committed Jan 17, 2024
1 parent a88661b commit 5da2d94
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 23 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
Expand All @@ -19,8 +23,9 @@ namespace DisCatSharp.ApplicationCommands.Attributes;
/// <param name="maxUses">Number of times the command can be used before triggering a cooldown.</param>
/// <param name="resetAfter">Number of seconds after which the cooldown is reset.</param>
/// <param name="bucketType">Type of cooldown bucket. This allows controlling whether the bucket will be cooled down per user, guild, member, channel, and/or globally.</param>
/// <param name="cooldownResponderType">The responder type used to respond to cooldown ratelimit hits.</param>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public sealed class ContextMenuCooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType) : ApplicationCommandCheckBaseAttribute, ICooldown<BaseContext, CooldownBucket>
public sealed class ContextMenuCooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType, Type? cooldownResponderType = null) : ApplicationCommandCheckBaseAttribute, ICooldown<BaseContext, CooldownBucket>
{
/// <summary>
/// Gets the maximum number of uses before this command triggers a cooldown for its bucket.
Expand All @@ -37,6 +42,11 @@ public sealed class ContextMenuCooldownAttribute(int maxUses, double resetAfter,
/// </summary>
public CooldownBucketType BucketType { get; } = bucketType;

/// <summary>
/// Gets the responder type.
/// </summary>
public Type? ResponderType { get; } = cooldownResponderType;

/// <summary>
/// Gets a cooldown bucket for given command context.
/// </summary>
Expand Down Expand Up @@ -117,10 +127,19 @@ public async Task<bool> 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,8 +20,9 @@ namespace DisCatSharp.ApplicationCommands.Attributes;
/// <param name="maxUses">Number of times the command can be used before triggering a cooldown.</param>
/// <param name="resetAfter">Number of seconds after which the cooldown is reset.</param>
/// <param name="bucketType">Type of cooldown bucket. This allows controlling whether the bucket will be cooled down per user, guild, member, channel, and/or globally.</param>
/// <param name="cooldownResponderType">The responder type used to respond to cooldown ratelimit hits.</param>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public sealed class SlashCommandCooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType) : ApplicationCommandCheckBaseAttribute, ICooldown<BaseContext, CooldownBucket>
public sealed class SlashCommandCooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType, Type? cooldownResponderType = null) : ApplicationCommandCheckBaseAttribute, ICooldown<BaseContext, CooldownBucket>
{
/// <summary>
/// Gets the maximum number of uses before this command triggers a cooldown for its bucket.
Expand All @@ -37,6 +39,11 @@ public sealed class SlashCommandCooldownAttribute(int maxUses, double resetAfter
/// </summary>
public CooldownBucketType BucketType { get; } = bucketType;

/// <summary>
/// Gets the responder type.
/// </summary>
public Type? ResponderType { get; } = cooldownResponderType;

/// <summary>
/// Gets a cooldown bucket for given command context.
/// </summary>
Expand Down Expand Up @@ -117,10 +124,19 @@ public async Task<bool> 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;
}
Expand Down
17 changes: 17 additions & 0 deletions DisCatSharp.ApplicationCommands/Entities/ICooldownResponder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.Threading.Tasks;

using DisCatSharp.ApplicationCommands.Context;

namespace DisCatSharp.ApplicationCommands.Entities;

/// <summary>
/// The cooldown responder.
/// </summary>
public interface ICooldownResponder
{
/// <summary>
/// Responds to cooldown ratelimit hits with given response.
/// </summary>
/// <param name="context">The context.</param>
Task Responder(BaseContext context);
}
30 changes: 23 additions & 7 deletions DisCatSharp.CommandsNext/Attributes/CooldownAttribute.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -17,8 +18,9 @@ namespace DisCatSharp.CommandsNext.Attributes;
/// <param name="maxUses">Number of times the command can be used before triggering a cooldown.</param>
/// <param name="resetAfter">Number of seconds after which the cooldown is reset.</param>
/// <param name="bucketType">Type of cooldown bucket. This allows controlling whether the bucket will be cooled down per user, guild, channel, or globally.</param>
/// <param name="cooldownResponderType">The responder type used to respond to cooldown ratelimit hits.</param>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public sealed class CooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType) : CheckBaseAttribute, ICooldown<CommandContext, CooldownBucket>
public sealed class CooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType, Type? cooldownResponderType = null) : CheckBaseAttribute, ICooldown<CommandContext, CooldownBucket>
{
/// <summary>
/// Gets the maximum number of uses before this command triggers a cooldown for its bucket.
Expand All @@ -35,6 +37,11 @@ public sealed class CooldownAttribute(int maxUses, double resetAfter, CooldownBu
/// </summary>
public CooldownBucketType BucketType { get; } = bucketType;

/// <summary>
/// Gets the responder type.
/// </summary>
public Type? ResponderType { get; } = cooldownResponderType;

/// <summary>
/// Gets a cooldown bucket for given command context.
/// </summary>
Expand Down Expand Up @@ -116,15 +123,24 @@ public async Task<bool> 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;
}
}
16 changes: 16 additions & 0 deletions DisCatSharp.CommandsNext/Entities/ICooldownResponder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Threading.Tasks;

namespace DisCatSharp.CommandsNext.Entities;

/// <summary>
/// The cooldown responder.
/// </summary>
public interface ICooldownResponder
{
/// <summary>
/// <para>Responds to cooldown ratelimit hits with given actions.</para>
/// <para>For example you could respond with a reaction or send a dm.</para>
/// </summary>
/// <param name="context">The context.</param>
Task Responder(CommandContext context);
}
11 changes: 6 additions & 5 deletions DisCatSharp/Entities/Application/DiscordApplicationCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ public DiscordApplicationCommandLocalization? NameLocalizations
/// <summary>
/// Gets the description of this command.
/// </summary>
[JsonProperty("description")]
public string Description { get; internal set; }
[JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)]
public string? Description { get; internal set; }

/// <summary>
/// Sets the description localizations.
Expand Down Expand Up @@ -110,7 +110,8 @@ public DiscordApplicationCommandLocalization? DescriptionLocalizations
/// Gets the mention for this command.
/// </summary>
[JsonIgnore]
public string Mention => this.Type == ApplicationCommandType.ChatInput ? $"</{this.Name}:{this.Id}>" : this.Name;
public string Mention
=> this.Type == ApplicationCommandType.ChatInput ? $"</{this.Name}:{this.Id}>" : this.Name;

/// <summary>
/// Creates a new instance of a <see cref="DiscordApplicationCommand"/>.
Expand All @@ -128,7 +129,7 @@ public DiscordApplicationCommandLocalization? DescriptionLocalizations
/// <param name="integrationTypes">The allowed integration types.</param>
public DiscordApplicationCommand(
string name,
string description,
string? description,
IEnumerable<DiscordApplicationCommandOption>? options = null,
ApplicationCommandType type = ApplicationCommandType.ChatInput,
DiscordApplicationCommandLocalization? nameLocalizations = null,
Expand All @@ -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));
Expand Down

0 comments on commit 5da2d94

Please sign in to comment.