From c5029d8f1f7bdf1412e82f05843e6bd9e46100fd Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Mon, 15 Jan 2024 09:48:21 +0100 Subject: [PATCH 01/14] feat: make cooldowns work feat: move cooldown to discatsharp base package chore: some experimental stuff fix: commandsnext cooldowns --- .../ApplicationCommandsExtension.cs | 26 +- .../ContextMenuCooldownAttribute.cs | 83 +++---- .../SlashCommandCooldownAttribute.cs | 89 ++++--- .../Context/BaseContext.cs | 39 ++- .../Context/ContextMenuContext.cs | 5 +- .../Context/InteractionContext.cs | 3 +- .../Attributes/CooldownAttribute.cs | 235 +++--------------- .../CommandsNextExtension.cs | 21 +- .../Entities/CommandGroup.cs | 6 +- .../EventArgs/CommandContext.cs | 17 +- DisCatSharp/Clients/DiscordClient.cs | 8 + .../Application/DiscordApplicationCommand.cs | 14 +- .../Entities/Core}/CooldownBucket.cs | 64 +++-- .../Core/DisCatSharpCommandContext.cs | 80 ++++++ .../Entities/Core}/IBucket.cs | 3 +- .../Entities/Core}/ICooldown.cs | 17 +- DisCatSharp/Entities/DCS/DisCatSharpTeam.cs | 183 -------------- .../Entities/DCS/DisCatSharpTeamMember.cs | 71 ------ DisCatSharp/Entities/DCS/GitHubRelease.cs | 165 ------------ .../Enums/Core}/CooldownBucketType.cs | 16 +- .../Core/DisCatSharpCommandGroupingType.cs | 27 ++ .../Enums/Core/DisCatSharpCommandType.cs | 32 +++ .../Rest/RestApplicationCommandPayloads.cs | 20 +- DisCatSharp/Net/Rest/DiscordApiClient.cs | 78 +++--- 24 files changed, 455 insertions(+), 847 deletions(-) rename {DisCatSharp.ApplicationCommands/Entities => DisCatSharp/Entities/Core}/CooldownBucket.cs (59%) create mode 100644 DisCatSharp/Entities/Core/DisCatSharpCommandContext.cs rename {DisCatSharp.ApplicationCommands/Entities => DisCatSharp/Entities/Core}/IBucket.cs (94%) rename {DisCatSharp.ApplicationCommands/Entities => DisCatSharp/Entities/Core}/ICooldown.cs (69%) delete mode 100644 DisCatSharp/Entities/DCS/DisCatSharpTeam.cs delete mode 100644 DisCatSharp/Entities/DCS/DisCatSharpTeamMember.cs delete mode 100644 DisCatSharp/Entities/DCS/GitHubRelease.cs rename {DisCatSharp.ApplicationCommands/Enums => DisCatSharp/Enums/Core}/CooldownBucketType.cs (68%) create mode 100644 DisCatSharp/Enums/Core/DisCatSharpCommandGroupingType.cs create mode 100644 DisCatSharp/Enums/Core/DisCatSharpCommandType.cs diff --git a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs index dab858f43..c55d67a48 100644 --- a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs +++ b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; @@ -13,11 +14,11 @@ using DisCatSharp.ApplicationCommands.EventArgs; using DisCatSharp.ApplicationCommands.Exceptions; using DisCatSharp.ApplicationCommands.Workers; -using DisCatSharp.Attributes; using DisCatSharp.Common; using DisCatSharp.Common.Utilities; using DisCatSharp.Entities; using DisCatSharp.Enums; +using DisCatSharp.Enums.Core; using DisCatSharp.EventArgs; using DisCatSharp.Exceptions; @@ -1081,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 @@ -1108,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); @@ -1121,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); @@ -1136,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); @@ -1340,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, @@ -1359,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 diff --git a/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuCooldownAttribute.cs b/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuCooldownAttribute.cs index c8371902f..b4a1d9052 100644 --- a/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuCooldownAttribute.cs +++ b/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuCooldownAttribute.cs @@ -1,10 +1,12 @@ 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; @@ -12,7 +14,7 @@ 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 : ApplicationCommandCheckBaseAttribute, ICooldown +public sealed class ContextMenuCooldownAttribute : ApplicationCommandCheckBaseAttribute, ICooldown { /// /// Gets the maximum number of uses before this command triggers a cooldown for its bucket. @@ -29,23 +31,17 @@ public sealed class ContextMenuCooldownAttribute : ApplicationCommandCheckBaseAt /// public CooldownBucketType BucketType { get; } - /// - /// Gets the cooldown buckets for this command. - /// - 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. + /// Type of cooldown bucket. This allows controlling whether the bucket will be cooled down per user, guild, member, channel, and/or globally. public ContextMenuCooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType) { this.MaxUses = maxUses; this.Reset = TimeSpan.FromSeconds(resetAfter); this.BucketType = bucketType; - this.Buckets = new(); } /// @@ -53,11 +49,11 @@ public ContextMenuCooldownAttribute(int maxUses, double resetAfter, CooldownBuck /// /// Command context to get cooldown bucket for. /// Requested cooldown bucket, or null if one wasn't present. - public ContextMenuCooldownBucket GetBucket(BaseContext ctx) + public CooldownBucket GetBucket(BaseContext ctx) { - var bid = this.GetBucketId(ctx, out _, out _, out _); - this.Buckets.TryGetValue(bid, out var bucket); - return bucket; + var bid = this.GetBucketId(ctx, out _, out _, out _, out _); + ctx.Client.CommandCooldownBuckets.TryGetValue(bid, out var bucket); + return bucket!; } /// @@ -68,7 +64,7 @@ public ContextMenuCooldownBucket GetBucket(BaseContext ctx) public TimeSpan GetRemainingCooldown(BaseContext ctx) { var bucket = this.GetBucket(ctx); - return bucket == null + return bucket == null! ? TimeSpan.Zero : bucket.RemainingUses > 0 ? TimeSpan.Zero @@ -82,8 +78,9 @@ public TimeSpan GetRemainingCooldown(BaseContext ctx) /// 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. + /// ID of the member with which this bucket is associated. /// Calculated bucket ID. - private string GetBucketId(BaseContext ctx, out ulong userId, out ulong channelId, out ulong guildId) + private string GetBucketId(BaseContext ctx, out ulong userId, out ulong channelId, out ulong guildId, out ulong memberId) { userId = 0ul; if ((this.BucketType & CooldownBucketType.User) != 0) @@ -92,14 +89,16 @@ private string GetBucketId(BaseContext ctx, out ulong userId, out ulong channelI 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) + if (ctx.Guild is not null && (this.BucketType & CooldownBucketType.Guild) != 0) guildId = ctx.Guild.Id; - var bid = CooldownBucket.MakeId(userId, channelId, guildId); + memberId = 0ul; + 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.FullCommandName, ctx.Interaction.Data.Id.ToString(CultureInfo.InvariantCulture), userId, channelId, guildId, memberId); return bid; } @@ -109,29 +108,27 @@ private string GetBucketId(BaseContext ctx, out ulong userId, out ulong channelI /// The command context. public override async Task ExecuteChecksAsync(BaseContext 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(this.MaxUses, this.Reset, usr, chn, gld); - this.Buckets.AddOrUpdate(bid, bucket, (k, v) => bucket); - } - - return await bucket.DecrementUseAsync().ConfigureAwait(false); + var bid = this.GetBucketId(ctx, out var usr, out var chn, out var gld, out var mem); + 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.FullCommandName, ctx.Interaction.Data.Id.ToString(CultureInfo.InvariantCulture), usr, chn, gld, mem); + ctx.Client.CommandCooldownBuckets.AddOrUpdate(bid, bucket, (k, v) => bucket); + + return await this.RespondRatelimitHitAsync(ctx, await bucket.DecrementUseAsync(ctx), bucket); } -} -/// -/// 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) - { } + /// + public async Task RespondRatelimitHitAsync(BaseContext ctx, bool noHit, CooldownBucket bucket) + { + if (noHit) + return true; - /// - /// 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}"; + 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; + } } diff --git a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs index 64ec59a68..ec60d6062 100644 --- a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs +++ b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs @@ -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; /// -/// Defines a cooldown for this command. This allows you to define how many times can users execute a specific command +/// 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 : ApplicationCommandCheckBaseAttribute, ICooldown +public sealed class SlashCommandCooldownAttribute : ApplicationCommandCheckBaseAttribute, ICooldown { /// /// Gets the maximum number of uses before this command triggers a cooldown for its bucket. @@ -29,23 +31,17 @@ public sealed class SlashCommandCooldownAttribute : ApplicationCommandCheckBaseA /// public CooldownBucketType BucketType { get; } - /// - /// Gets the cooldown buckets for this command. - /// - 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. + /// Type of cooldown bucket. This allows controlling whether the bucket will be cooled down per user, guild, member, channel, and/or globally. public SlashCommandCooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType) { this.MaxUses = maxUses; this.Reset = TimeSpan.FromSeconds(resetAfter); this.BucketType = bucketType; - this.Buckets = new(); } /// @@ -53,11 +49,11 @@ public SlashCommandCooldownAttribute(int maxUses, double resetAfter, CooldownBuc /// /// Command context to get cooldown bucket for. /// Requested cooldown bucket, or null if one wasn't present. - public SlashCommandCooldownBucket GetBucket(BaseContext ctx) + public CooldownBucket GetBucket(BaseContext ctx) { - var bid = this.GetBucketId(ctx, out _, out _, out _); - this.Buckets.TryGetValue(bid, out var bucket); - return bucket; + var bid = this.GetBucketId(ctx, out _, out _, out _, out _); + ctx.Client.CommandCooldownBuckets.TryGetValue(bid, out var bucket); + return bucket!; } /// @@ -68,7 +64,7 @@ public SlashCommandCooldownBucket GetBucket(BaseContext ctx) public TimeSpan GetRemainingCooldown(BaseContext ctx) { var bucket = this.GetBucket(ctx); - return bucket == null + return bucket is null ? TimeSpan.Zero : bucket.RemainingUses > 0 ? TimeSpan.Zero @@ -82,24 +78,27 @@ public TimeSpan GetRemainingCooldown(BaseContext ctx) /// 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. + /// ID of the member with which this bucket is associated. /// Calculated bucket ID. - private string GetBucketId(BaseContext ctx, out ulong userId, out ulong channelId, out ulong guildId) + private string GetBucketId(BaseContext ctx, out ulong userId, out ulong channelId, out ulong guildId, out ulong memberId) { userId = 0ul; - if ((this.BucketType & CooldownBucketType.User) != 0) + if (this.BucketType.HasFlag(CooldownBucketType.User)) 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) + if (this.BucketType.HasFlag(CooldownBucketType.Channel)) channelId = ctx.Channel.Id; guildId = 0ul; - if (ctx.Guild != null && (this.BucketType & CooldownBucketType.Guild) != 0) + if (ctx.Guild is not null && this.BucketType.HasFlag(CooldownBucketType.Guild)) guildId = ctx.Guild.Id; - var bid = CooldownBucket.MakeId(userId, channelId, guildId); + memberId = 0ul; + 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.FullCommandName, ctx.Interaction.Data.Id.ToString(CultureInfo.InvariantCulture), userId, channelId, guildId, memberId); return bid; } @@ -109,29 +108,27 @@ private string GetBucketId(BaseContext ctx, out ulong userId, out ulong channelI /// The command context. public override async Task ExecuteChecksAsync(BaseContext 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(this.MaxUses, this.Reset, usr, chn, gld); - this.Buckets.AddOrUpdate(bid, bucket, (k, v) => bucket); - } - - return await bucket.DecrementUseAsync().ConfigureAwait(false); + var bid = this.GetBucketId(ctx, out var usr, out var chn, out var gld, out var mem); + 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.FullCommandName, ctx.Interaction.Data.Id.ToString(CultureInfo.InvariantCulture), usr, chn, gld, mem); + ctx.Client.CommandCooldownBuckets.AddOrUpdate(bid, bucket, (k, v) => bucket); + + return await this.RespondRatelimitHitAsync(ctx, await bucket.DecrementUseAsync(ctx), bucket); } -} -/// -/// 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}"; + /// + public async Task RespondRatelimitHitAsync(BaseContext ctx, bool noHit, CooldownBucket bucket) + { + if (noHit) + return true; - internal SlashCommandCooldownBucket(int maxUses, TimeSpan resetAfter, ulong userId = 0, ulong channelId = 0, ulong guildId = 0) - : base(maxUses, resetAfter, userId, channelId, guildId) - { } + 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; + } } diff --git a/DisCatSharp.ApplicationCommands/Context/BaseContext.cs b/DisCatSharp.ApplicationCommands/Context/BaseContext.cs index d63fac147..1c60ddd27 100644 --- a/DisCatSharp.ApplicationCommands/Context/BaseContext.cs +++ b/DisCatSharp.ApplicationCommands/Context/BaseContext.cs @@ -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; @@ -13,22 +15,17 @@ namespace DisCatSharp.ApplicationCommands.Context; /// /// Represents a base context for application command contexts. /// -public class BaseContext +public class BaseContext : DisCatSharpCommandContext { /// /// Gets the interaction that was created. /// public DiscordInteraction Interaction { get; internal init; } - /// - /// Gets the client for this interaction. - /// - public DiscordClient Client { get; internal init; } - /// /// Gets the guild this interaction was executed in. /// - public DiscordGuild Guild { get; internal init; } + public DiscordGuild? Guild { get; internal init; } /// /// Gets the channel this interaction was executed in. @@ -43,7 +40,7 @@ public class BaseContext /// /// Gets the member which executed this interaction, or null if the command is in a DM. /// - public DiscordMember Member + public DiscordMember? Member => this.User is DiscordMember member ? member : null; /// @@ -61,25 +58,10 @@ public DiscordMember Member /// public ulong InteractionId { get; internal set; } - /// - /// Gets the name of the command. - /// - public string CommandName { get; internal init; } - - /// - /// Gets the name of the sub command. - /// - public string? SubCommandName { get; internal set; } - - /// - /// Gets the name of the sub command. - /// - public string? SubSubCommandName { get; internal set; } - /// /// Gets the full command string, including the subcommand. /// - public string FullCommandName + public override string FullCommandName => $"{this.CommandName}{(string.IsNullOrWhiteSpace(this.SubCommandName) ? "" : $" {this.SubCommandName}")}{(string.IsNullOrWhiteSpace(this.SubSubCommandName) ? "" : $" {this.SubSubCommandName}")}"; /// @@ -125,6 +107,15 @@ public string FullCommandName /// public IServiceProvider Services { get; internal set; } = new ServiceCollection().BuildServiceProvider(true); + /// + /// Initializes a new instance of the class. + /// + /// The command type. + /// The command grouping type. + internal BaseContext(DisCatSharpCommandType type, DisCatSharpCommandGroupingType groupingType = DisCatSharpCommandGroupingType.Command) + : base(type, groupingType) + { } + /// /// Creates a response to this interaction. /// 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 at the start, and edit the response later. diff --git a/DisCatSharp.ApplicationCommands/Context/ContextMenuContext.cs b/DisCatSharp.ApplicationCommands/Context/ContextMenuContext.cs index 7b0398984..fb405d729 100644 --- a/DisCatSharp.ApplicationCommands/Context/ContextMenuContext.cs +++ b/DisCatSharp.ApplicationCommands/Context/ContextMenuContext.cs @@ -1,11 +1,12 @@ using DisCatSharp.Entities; +using DisCatSharp.Enums.Core; namespace DisCatSharp.ApplicationCommands.Context; /// /// Represents a context for a context menu. /// -public sealed class ContextMenuContext : BaseContext +public sealed class ContextMenuContext(DisCatSharpCommandType type) : BaseContext(type) { /// /// The user this command targets, if applicable. @@ -15,7 +16,7 @@ public sealed class ContextMenuContext : BaseContext /// /// The member this command targets, if applicable. /// - public DiscordMember TargetMember + public DiscordMember? TargetMember => this.TargetUser is DiscordMember member ? member : null; /// diff --git a/DisCatSharp.ApplicationCommands/Context/InteractionContext.cs b/DisCatSharp.ApplicationCommands/Context/InteractionContext.cs index 1000b4301..c425cb79e 100644 --- a/DisCatSharp.ApplicationCommands/Context/InteractionContext.cs +++ b/DisCatSharp.ApplicationCommands/Context/InteractionContext.cs @@ -1,13 +1,14 @@ using System.Collections.Generic; using DisCatSharp.Entities; +using DisCatSharp.Enums.Core; namespace DisCatSharp.ApplicationCommands.Context; /// /// Represents a context for an interaction. /// -public sealed class InteractionContext : BaseContext +public sealed class InteractionContext() : BaseContext(DisCatSharpCommandType.SlashCommand) { /// /// Gets the users mentioned in the command parameters. diff --git a/DisCatSharp.CommandsNext/Attributes/CooldownAttribute.cs b/DisCatSharp.CommandsNext/Attributes/CooldownAttribute.cs index 699764051..3bcd913e5 100644 --- a/DisCatSharp.CommandsNext/Attributes/CooldownAttribute.cs +++ b/DisCatSharp.CommandsNext/Attributes/CooldownAttribute.cs @@ -1,16 +1,18 @@ using System; -using System.Collections.Concurrent; -using System.Globalization; -using System.Threading; using System.Threading.Tasks; +using DisCatSharp.Entities; +using DisCatSharp.Entities.Core; +using DisCatSharp.Enums.Core; +using DisCatSharp.Exceptions; + namespace DisCatSharp.CommandsNext.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 CooldownAttribute : CheckBaseAttribute +public sealed class CooldownAttribute : CheckBaseAttribute, ICooldown { /// /// Gets the maximum number of uses before this command triggers a cooldown for its bucket. @@ -27,11 +29,6 @@ public sealed class CooldownAttribute : CheckBaseAttribute /// 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. /// @@ -43,7 +40,6 @@ public CooldownAttribute(int maxUses, double resetAfter, CooldownBucketType buck this.MaxUses = maxUses; this.Reset = TimeSpan.FromSeconds(resetAfter); this.BucketType = bucketType; - this._buckets = new(); } /// @@ -51,11 +47,11 @@ public CooldownAttribute(int maxUses, double resetAfter, CooldownBucketType buck /// /// Command context to get cooldown bucket for. /// Requested cooldown bucket, or null if one wasn't present. - public CommandCooldownBucket GetBucket(CommandContext ctx) + public CooldownBucket GetBucket(CommandContext ctx) { var bid = this.GetBucketId(ctx, out _, out _, out _); - this._buckets.TryGetValue(bid, out var bucket); - return bucket; + ctx.Client.CommandCooldownBuckets.TryGetValue(bid, out var bucket); + return bucket!; } /// @@ -66,7 +62,7 @@ public CommandCooldownBucket GetBucket(CommandContext ctx) public TimeSpan GetRemainingCooldown(CommandContext ctx) { var bucket = this.GetBucket(ctx); - return bucket == null + return bucket is null ? TimeSpan.Zero : bucket.RemainingUses > 0 ? TimeSpan.Zero @@ -84,20 +80,20 @@ public TimeSpan GetRemainingCooldown(CommandContext ctx) private string GetBucketId(CommandContext ctx, out ulong userId, out ulong channelId, out ulong guildId) { userId = 0ul; - if ((this.BucketType & CooldownBucketType.User) != 0) + if (this.BucketType.HasFlag(CooldownBucketType.User)) userId = ctx.User.Id; channelId = 0ul; - if ((this.BucketType & CooldownBucketType.Channel) != 0) + if (this.BucketType.HasFlag(CooldownBucketType.Channel)) channelId = ctx.Channel.Id; - if ((this.BucketType & CooldownBucketType.Guild) != 0 && ctx.Guild == null) + if (this.BucketType.HasFlag(CooldownBucketType.Guild) && ctx.Guild is null) channelId = ctx.Channel.Id; guildId = 0ul; - if (ctx.Guild != null && (this.BucketType & CooldownBucketType.Guild) != 0) + if (ctx.Guild is not null && this.BucketType.HasFlag(CooldownBucketType.Guild)) guildId = ctx.Guild.Id; - var bid = CommandCooldownBucket.MakeId(userId, channelId, guildId); + var bid = CooldownBucket.MakeId(ctx.Command.QualifiedName, "text", userId, channelId, guildId); return bid; } @@ -112,203 +108,30 @@ public override async Task ExecuteCheckAsync(CommandContext ctx, bool help return true; var bid = this.GetBucketId(ctx, out var usr, out var chn, out var gld); - if (!this._buckets.TryGetValue(bid, out var bucket)) - { - bucket = new(this.MaxUses, this.Reset, usr, chn, gld); - this._buckets.AddOrUpdate(bid, bucket, (k, v) => bucket); - } - - return await bucket.DecrementUseAsync().ConfigureAwait(false); - } -} - -/// -/// Defines how are command cooldowns applied. -/// -public enum CooldownBucketType -{ - /// - /// Denotes that the command will have its cooldown applied per-user. - /// - User = 1, - - /// - /// Denotes that the command will have its cooldown applied per-channel. - /// - Channel = 2, - - /// - /// Denotes that the command will have its cooldown applied per-guild. In DMs, this applies the cooldown per-channel. - /// - Guild = 4, - - /// - /// Denotes that the command will have its cooldown applied globally. - /// - Global = 0 -} - -/// -/// Represents a cooldown bucket for commands. -/// -public sealed class CommandCooldownBucket : IEquatable -{ - /// - /// 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; + if (ctx.Client.CommandCooldownBuckets.TryGetValue(bid, out var bucket)) + return await this.RespondRatelimitHitAsync(ctx, await bucket.DecrementUseAsync(ctx), bucket); - /// - /// 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; + bucket = new(this.MaxUses, this.Reset, ctx.Command.QualifiedName, "text", usr, chn, gld); + ctx.Client.CommandCooldownBuckets.AddOrUpdate(bid, bucket, (k, v) => bucket); - /// - /// 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 CommandCooldownBucket(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); + return await this.RespondRatelimitHitAsync(ctx, await bucket.DecrementUseAsync(ctx), bucket); } - /// - /// Decrements the remaining use counter. - /// - /// Whether decrement succeeded or not. - internal async Task DecrementUseAsync() + /// + public async Task RespondRatelimitHitAsync(CommandContext ctx, bool noHit, CooldownBucket bucket) { - await this._usageSemaphore.WaitAsync().ConfigureAwait(false); + if (noHit) + return true; - // if we're past reset time... - var now = DateTimeOffset.UtcNow; - if (now >= this.ResetsAt) + try { - // ...do the reset and set a new reset time - Interlocked.Exchange(ref this._remainingUses, this.MaxUses); - this.ResetsAt = now + this.Reset; + await ctx.Message.CreateReactionAsync(DiscordEmoji.FromName(ctx.Client, ":x:", false)); } - - // check if we have any uses left, if we do... - var success = false; - if (this.RemainingUses > 0) + catch (UnauthorizedException) { - // ...decrement, and return success... - Interlocked.Decrement(ref this._remainingUses); - success = true; + // ignore } - // ...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() => $"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 CommandCooldownBucket); - - /// - /// Checks whether this is equal to another . - /// - /// to compare to. - /// Whether the is equal to this . - public bool Equals(CommandCooldownBucket 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 ==(CommandCooldownBucket bucket1, CommandCooldownBucket bucket2) - { - var null1 = bucket1 is null; - var null2 = bucket2 is null; - - return (null1 && null2) || (null1 == null2 && null1.Equals(null2)); + return false; } - - /// - /// 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 !=(CommandCooldownBucket bucket1, CommandCooldownBucket 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.CommandsNext/CommandsNextExtension.cs b/DisCatSharp.CommandsNext/CommandsNextExtension.cs index ae3d32261..f052de5bc 100644 --- a/DisCatSharp.CommandsNext/CommandsNextExtension.cs +++ b/DisCatSharp.CommandsNext/CommandsNextExtension.cs @@ -258,7 +258,7 @@ private async Task HandleCommandsAsync(DiscordClient sender, MessageCreateEventA /// Qualified name of the command, optionally with arguments. /// Separated arguments. /// Found command or null if none was found. - public Command FindCommand(string commandString, out string rawArguments) + public Command FindCommand(string commandString, out string? rawArguments) { rawArguments = null; @@ -323,7 +323,7 @@ public Command FindCommand(string commandString, out string rawArguments) /// Command to execute. /// Raw arguments to pass to command. /// Created command execution context. - public CommandContext CreateContext(DiscordMessage msg, string prefix, Command cmd, string rawArguments = null) + public CommandContext CreateContext(DiscordMessage msg, string prefix, Command cmd, string? rawArguments = null) { var ctx = new CommandContext { @@ -334,7 +334,11 @@ public CommandContext CreateContext(DiscordMessage msg, string prefix, Command c RawArgumentString = rawArguments ?? "", Prefix = prefix, CommandsNext = this, - Services = this.Services + Services = this.Services, + UserId = msg.Author.Id, + GuildId = msg.GuildId, + MemberId = msg.GuildId is not null ? msg.Author.Id : null, + ChannelId = msg.ChannelId }; if (cmd != null && (cmd.Module is TransientCommandModule || cmd.Module == null)) @@ -839,7 +843,8 @@ public CommandContext CreateFakeContext(DiscordUser actor, DiscordChannel channe AttachmentsInternal = [], EmbedsInternal = [], TimestampRaw = now.ToString("yyyy-MM-ddTHH:mm:sszzz"), - ReactionsInternal = [] + ReactionsInternal = [], + GuildId = channel.GuildId }; var mentionedUsers = new List(); @@ -871,10 +876,14 @@ public CommandContext CreateFakeContext(DiscordUser actor, DiscordChannel channe RawArgumentString = rawArguments ?? "", Prefix = prefix, CommandsNext = this, - Services = this.Services + Services = this.Services, + UserId = msg.Author.Id, + GuildId = msg.GuildId, + MemberId = msg.GuildId is not null ? msg.Author.Id : null, + ChannelId = msg.ChannelId }; - if (cmd != null && (cmd.Module is TransientCommandModule || cmd.Module == null)) + if (cmd != null && cmd.Module is TransientCommandModule or null) { var scope = ctx.Services.CreateScope(); ctx.ServiceScopeContext = new(ctx.Services, scope); diff --git a/DisCatSharp.CommandsNext/Entities/CommandGroup.cs b/DisCatSharp.CommandsNext/Entities/CommandGroup.cs index 5930de264..063fc1039 100644 --- a/DisCatSharp.CommandsNext/Entities/CommandGroup.cs +++ b/DisCatSharp.CommandsNext/Entities/CommandGroup.cs @@ -56,7 +56,11 @@ public override async Task ExecuteAsync(CommandContext ctx) RawArgumentString = ctx.RawArgumentString[findPos..], Prefix = ctx.Prefix, CommandsNext = ctx.CommandsNext, - Services = ctx.Services + Services = ctx.Services, + UserId = ctx.Message.Author.Id, + GuildId = ctx.Message.GuildId, + MemberId = ctx.Message.GuildId is not null ? ctx.Message.Author.Id : null, + ChannelId = ctx.Message.ChannelId }; var fchecks = await cmd.RunChecksAsync(xctx, false).ConfigureAwait(false); diff --git a/DisCatSharp.CommandsNext/EventArgs/CommandContext.cs b/DisCatSharp.CommandsNext/EventArgs/CommandContext.cs index 1d4fa6bb0..0eeadcb67 100644 --- a/DisCatSharp.CommandsNext/EventArgs/CommandContext.cs +++ b/DisCatSharp.CommandsNext/EventArgs/CommandContext.cs @@ -3,6 +3,8 @@ using System.Threading.Tasks; using DisCatSharp.Entities; +using DisCatSharp.Entities.Core; +using DisCatSharp.Enums.Core; using Microsoft.Extensions.DependencyInjection; @@ -11,13 +13,8 @@ namespace DisCatSharp.CommandsNext; /// /// Represents a context in which a command is executed. /// -public sealed class CommandContext +public sealed class CommandContext : DisCatSharpCommandContext { - /// - /// Gets the client which received the message. - /// - public DiscordClient Client { get; internal set; } - /// /// Gets the message that triggered the execution. /// @@ -32,7 +29,7 @@ public DiscordChannel Channel /// /// Gets the guild in which the execution was triggered. This property is null for commands sent over direct messages. /// - public DiscordGuild Guild + public DiscordGuild? Guild => this.Message.GuildId.HasValue ? this.Message.Guild : null; /// @@ -98,8 +95,9 @@ public DiscordMember Member /// Initializes a new instance of the class. /// internal CommandContext() + : base(DisCatSharpCommandType.TextCommand) { - this._lazyMember = new(() => this.Guild != null && this.Guild.Members.TryGetValue(this.User.Id, out var member) ? member : this.Guild?.GetMemberAsync(this.User.Id).ConfigureAwait(false).GetAwaiter().GetResult()); + this._lazyMember = new(() => this.Guild is not null && this.Guild.Members.TryGetValue(this.User.Id, out var member) ? member : this.Guild?.GetMemberAsync(this.User.Id).ConfigureAwait(false).GetAwaiter().GetResult()); } /// @@ -182,6 +180,7 @@ public ServiceContext(IServiceProvider services, IServiceScope scope) /// /// Disposes the command context. /// - public void Dispose() => this.Scope?.Dispose(); + public void Dispose() + => this.Scope?.Dispose(); } } diff --git a/DisCatSharp/Clients/DiscordClient.cs b/DisCatSharp/Clients/DiscordClient.cs index f42f01344..b531722c6 100644 --- a/DisCatSharp/Clients/DiscordClient.cs +++ b/DisCatSharp/Clients/DiscordClient.cs @@ -11,6 +11,7 @@ using DisCatSharp.Attributes; using DisCatSharp.Entities; +using DisCatSharp.Entities.Core; using DisCatSharp.Enums; using DisCatSharp.Exceptions; using DisCatSharp.Net; @@ -135,6 +136,11 @@ public IReadOnlyDictionary EmbeddedActivities internal Dictionary EmbeddedActivitiesInternal = []; private Lazy> _embeddedActivitiesLazy; + /// + /// Gets the cooldown buckets for commands. + /// + public ConcurrentDictionary CommandCooldownBuckets { get; } = []; + #endregion #region Constructor/Internal Setup @@ -1697,6 +1703,8 @@ public override void Dispose() this.ApiClient.Rest.Dispose(); this.CurrentUser = null; + this.CommandCooldownBuckets.Clear(); + var extensions = this._extensions; // prevent _extensions being modified during dispose this._extensions.Clear(); foreach (var extension in extensions) diff --git a/DisCatSharp/Entities/Application/DiscordApplicationCommand.cs b/DisCatSharp/Entities/Application/DiscordApplicationCommand.cs index a44629798..c71ad1f8a 100644 --- a/DisCatSharp/Entities/Application/DiscordApplicationCommand.cs +++ b/DisCatSharp/Entities/Application/DiscordApplicationCommand.cs @@ -36,14 +36,14 @@ public class DiscordApplicationCommand : SnowflakeObject, IEquatable [JsonProperty("name_localizations", NullValueHandling = NullValueHandling.Ignore)] - internal Dictionary RawNameLocalizations { get; set; } + internal Dictionary? RawNameLocalizations { get; set; } /// /// Gets the name localizations. /// [JsonIgnore] - public DiscordApplicationCommandLocalization NameLocalizations - => new(this.RawNameLocalizations); + public DiscordApplicationCommandLocalization? NameLocalizations + => this.RawNameLocalizations != null ? new(this.RawNameLocalizations) : null; /// /// Gets the description of this command. @@ -55,14 +55,14 @@ public DiscordApplicationCommandLocalization NameLocalizations /// Sets the description localizations. /// [JsonProperty("description_localizations", NullValueHandling = NullValueHandling.Ignore)] - internal Dictionary RawDescriptionLocalizations { get; set; } + internal Dictionary? RawDescriptionLocalizations { get; set; } /// /// Gets the description localizations. /// [JsonIgnore] - public DiscordApplicationCommandLocalization DescriptionLocalizations - => new(this.RawDescriptionLocalizations); + public DiscordApplicationCommandLocalization? DescriptionLocalizations + => this.RawDescriptionLocalizations != null ? new(this.RawDescriptionLocalizations) : null; /// /// Gets the potential parameters for this command. @@ -145,7 +145,7 @@ public DiscordApplicationCommand( { if (!Utilities.IsValidSlashCommandName(name)) throw new ArgumentException("Invalid slash command name specified. It must be below 32 characters and not contain any whitespace.", nameof(name)); - if (name.Any(ch => char.IsUpper(ch))) + if (name.Any(char.IsUpper)) throw new ArgumentException("Slash command name cannot have any upper case characters.", nameof(name)); if (description.Length > 100) throw new ArgumentException("Slash command description cannot exceed 100 characters.", nameof(description)); diff --git a/DisCatSharp.ApplicationCommands/Entities/CooldownBucket.cs b/DisCatSharp/Entities/Core/CooldownBucket.cs similarity index 59% rename from DisCatSharp.ApplicationCommands/Entities/CooldownBucket.cs rename to DisCatSharp/Entities/Core/CooldownBucket.cs index 43eca62e7..de21ef767 100644 --- a/DisCatSharp.ApplicationCommands/Entities/CooldownBucket.cs +++ b/DisCatSharp/Entities/Core/CooldownBucket.cs @@ -3,8 +3,13 @@ using System.Threading; using System.Threading.Tasks; -namespace DisCatSharp.ApplicationCommands.Entities; +using Microsoft.Extensions.Logging; +namespace DisCatSharp.Entities.Core; + +/// +/// Represents a cooldown bucket. +/// public class CooldownBucket : IBucket, IEquatable { /// @@ -18,10 +23,15 @@ public class CooldownBucket : IBucket, IEquatable public ulong ChannelId { get; } /// - /// The guild id for this bucket. + /// The guild id for this bucket. /// public ulong GuildId { get; } + /// + /// The member id for this bucket. + /// + public ulong MemberId { get; } + /// /// The id for this bucket. /// @@ -30,7 +40,8 @@ public class CooldownBucket : IBucket, IEquatable /// /// The remaining uses for this bucket. /// - public int RemainingUses => Volatile.Read(ref this.RemainingUsesInternal); + public int RemainingUses + => Volatile.Read(ref this.RemainingUsesInternal); /// /// The max uses for this bucket. @@ -52,6 +63,9 @@ public class CooldownBucket : IBucket, IEquatable /// internal readonly SemaphoreSlim UsageSemaphore; + /// + /// Gets the remaining uses for this bucket. + /// internal int RemainingUsesInternal; /// @@ -59,10 +73,13 @@ public class CooldownBucket : IBucket, IEquatable /// /// Maximum number of uses for this bucket. /// Time after which this bucket resets. + /// ID of the command + /// Name of the command. /// 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) + /// ID of the member with which this cooldown is associated. + internal CooldownBucket(int maxUses, TimeSpan resetAfter, string commandName, string commandId, ulong userId = 0, ulong channelId = 0, ulong guildId = 0, ulong memberId = 0) { this.RemainingUsesInternal = maxUses; this.MaxUses = maxUses; @@ -71,40 +88,43 @@ internal CooldownBucket(int maxUses, TimeSpan resetAfter, ulong userId = 0, ulon this.UserId = userId; this.ChannelId = channelId; this.GuildId = guildId; - this.BucketId = MakeId(userId, channelId, guildId); + this.MemberId = memberId; + this.BucketId = MakeId(commandId, commandName, userId, channelId, guildId, memberId); this.UsageSemaphore = new(1, 1); } /// /// Decrements the remaining use counter. /// + /// The context. /// Whether decrement succeeded or not. - internal async Task DecrementUseAsync() + internal async Task DecrementUseAsync(DisCatSharpCommandContext ctx) { await this.UsageSemaphore.WaitAsync().ConfigureAwait(false); - 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}"); + ctx.Client.Logger.LogDebug($"[Cooldown::prev_check({ctx.FullCommandName})]:\n\tRemaining: {this.RemainingUses}/{this.MaxUses}\n\tResets: {this.ResetsAt}\n\tNow: {DateTimeOffset.UtcNow}\n\tVars[u,c,g,m]: {this.UserId} {this.ChannelId} {this.GuildId} {this.MemberId}\n\tId: {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.RemainingUsesInternal, this.MaxUses); this.ResetsAt = now + this.Reset; } - // check if we have any uses left, if we do... + ctx.Client.Logger.LogDebug($"[Cooldown::check({ctx.FullCommandName})]:\n\tRemaining: {this.RemainingUses}/{this.MaxUses}\n\tResets: {this.ResetsAt}\n\tNow: {DateTimeOffset.UtcNow}\n\tVars[u,c,g,m]: {this.UserId} {this.ChannelId} {this.GuildId} {this.MemberId}\n\tId: {this.BucketId}"); + var success = false; if (this.RemainingUses > 0) { - // ...decrement, and return success... Interlocked.Decrement(ref this.RemainingUsesInternal); success = true; } - 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(); + if (success) + return success; + + ctx.Client.Logger.LogWarning($"[Cooldown::hit({ctx.FullCommandName})]:\n\tRemaining: {this.RemainingUses}/{this.MaxUses}\n\tResets: {this.ResetsAt}\n\tNow: {DateTimeOffset.UtcNow}\n\tVars[u,c,g,m]: {this.UserId} {this.ChannelId} {this.GuildId} {this.MemberId}\n\tId: {this.BucketId}"); + return success; } @@ -113,20 +133,23 @@ internal async Task DecrementUseAsync() /// /// Object to compare to. /// Whether the object is equal to this . - public override bool Equals(object obj) => this.Equals(obj as CooldownBucket); + 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)); + public bool Equals(CooldownBucket? other) + => other is not null && (ReferenceEquals(this, other) || (this.UserId == other.UserId && this.ChannelId == other.ChannelId && this.GuildId == other.GuildId && this.MemberId == other.MemberId)); /// /// Gets the hash code for this . /// /// The hash code for this . - public override int GetHashCode() => HashCode.Combine(this.UserId, this.ChannelId, this.GuildId); + public override int GetHashCode() + => HashCode.Combine(this.UserId, this.ChannelId, this.GuildId, this.MemberId); /// /// Gets whether the two objects are equal. @@ -134,7 +157,7 @@ internal async Task DecrementUseAsync() /// First bucket to compare. /// Second bucket to compare. /// Whether the two buckets are equal. - public static bool operator ==(CooldownBucket bucket1, CooldownBucket bucket2) + public static bool operator ==(CooldownBucket? bucket1, CooldownBucket? bucket2) { var null1 = bucket1 is null; var null2 = bucket2 is null; @@ -154,10 +177,13 @@ internal async Task DecrementUseAsync() /// /// Creates a bucket ID from given bucket parameters. /// + /// ID of the command + /// Name of the command. /// 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. + /// ID of the member 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)}"; + public static string MakeId(string commandId, string commandName, ulong userId = 0, ulong channelId = 0, ulong guildId = 0, ulong memberId = 0) + => $"{commandId}:{commandName}::{userId.ToString(CultureInfo.InvariantCulture)}:{channelId.ToString(CultureInfo.InvariantCulture)}:{guildId.ToString(CultureInfo.InvariantCulture)}:{memberId.ToString(CultureInfo.InvariantCulture)}"; } diff --git a/DisCatSharp/Entities/Core/DisCatSharpCommandContext.cs b/DisCatSharp/Entities/Core/DisCatSharpCommandContext.cs new file mode 100644 index 000000000..cc5300e6b --- /dev/null +++ b/DisCatSharp/Entities/Core/DisCatSharpCommandContext.cs @@ -0,0 +1,80 @@ +using DisCatSharp.Enums.Core; + +namespace DisCatSharp.Entities.Core; + +/// +/// Interface for various command types like slash commands, user commands, message commands, text commands, etc. +/// +public class DisCatSharpCommandContext +{ + /// + /// Gets the client. + /// + public DiscordClient Client { get; internal init; } + + /// + /// Gets the id of the user who executes this command. + /// + public ulong UserId { get; internal set; } + + /// + /// Gets the id of the channel this command gets executed in. + /// + public ulong ChannelId { get; internal set; } + + /// + /// Gets the id of the guild this command gets executed in. + /// + public ulong? GuildId { get; internal set; } + + /// + /// Gets the id of the member who executes this command. + /// + public ulong? MemberId { get; internal set; } + + /// + /// Gets the id of the command. + /// + public ulong? CommandId { get; internal set; } + + /// + /// Gets the name of the command. + /// + public string CommandName { get; internal set; } = string.Empty; + + /// + /// Gets the name of the sub command. + /// + public string? SubCommandName { get; internal set; } + + /// + /// Gets the name of the sub command within a sub group. + /// + public string? SubSubCommandName { get; internal set; } + + /// + /// Gets the fully qualified name of the command. + /// + public virtual string FullCommandName { get; internal set; } = string.Empty; + + /// + /// Gets the type of the command. + /// + public DisCatSharpCommandType CommandType { get; internal set; } + + /// + /// Gets the command grouping type of the command. + /// + public DisCatSharpCommandGroupingType CommandGroupingType { get; internal set; } + + /// + /// Initializes a new instance of the class. + /// + /// The command type. + /// The command grouping type. + internal DisCatSharpCommandContext(DisCatSharpCommandType type, DisCatSharpCommandGroupingType groupingType = DisCatSharpCommandGroupingType.None) + { + this.CommandType = type; + this.CommandGroupingType = groupingType; + } +} diff --git a/DisCatSharp.ApplicationCommands/Entities/IBucket.cs b/DisCatSharp/Entities/Core/IBucket.cs similarity index 94% rename from DisCatSharp.ApplicationCommands/Entities/IBucket.cs rename to DisCatSharp/Entities/Core/IBucket.cs index fa10df5bb..3ff9bd923 100644 --- a/DisCatSharp.ApplicationCommands/Entities/IBucket.cs +++ b/DisCatSharp/Entities/Core/IBucket.cs @@ -1,6 +1,7 @@ using System; +using System.Threading.Tasks; -namespace DisCatSharp.ApplicationCommands.Entities; +namespace DisCatSharp.Entities.Core; /// /// Defines the standard contract for bucket feature diff --git a/DisCatSharp.ApplicationCommands/Entities/ICooldown.cs b/DisCatSharp/Entities/Core/ICooldown.cs similarity index 69% rename from DisCatSharp.ApplicationCommands/Entities/ICooldown.cs rename to DisCatSharp/Entities/Core/ICooldown.cs index ae3747f72..0b9c7dde8 100644 --- a/DisCatSharp.ApplicationCommands/Entities/ICooldown.cs +++ b/DisCatSharp/Entities/Core/ICooldown.cs @@ -1,17 +1,17 @@ using System; +using System.Threading.Tasks; -using DisCatSharp.ApplicationCommands.Context; -using DisCatSharp.ApplicationCommands.Enums; +using DisCatSharp.Enums.Core; -namespace DisCatSharp.ApplicationCommands.Entities; +namespace DisCatSharp.Entities.Core; /// /// Cooldown feature contract /// -/// Type of in which this cooldown handles +/// Type of in which this cooldown handles /// Type of Cooldown bucket public interface ICooldown - where TContextType : BaseContext + where TContextType : DisCatSharpCommandContext where TBucketType : CooldownBucket { /// @@ -42,4 +42,11 @@ public interface ICooldown /// Command context to get cooldown bucket for. /// Requested cooldown bucket, or null if one wasn't present TBucketType GetBucket(TContextType ctx); + + /// + /// Responds to a ratelimit hit. + /// + /// The command context. + /// Whether the ratelimit wasn't it. + Task RespondRatelimitHitAsync(TContextType ctx, bool noHit, CooldownBucket bucket); } diff --git a/DisCatSharp/Entities/DCS/DisCatSharpTeam.cs b/DisCatSharp/Entities/DCS/DisCatSharpTeam.cs deleted file mode 100644 index 094ec2173..000000000 --- a/DisCatSharp/Entities/DCS/DisCatSharpTeam.cs +++ /dev/null @@ -1,183 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; - -using DisCatSharp.Enums; -using DisCatSharp.Net; -using DisCatSharp.Net.Abstractions; - -using Microsoft.Extensions.Logging; - -using Newtonsoft.Json; - -namespace DisCatSharp.Entities; - -/// -/// The DisCatSharp team. -/// -public sealed class DisCatSharpTeam : SnowflakeObject -{ - /// - /// Gets the team's name. - /// - public string TeamName { get; internal set; } - - /// - /// Gets the overall owner. - /// - public string MainOwner - => "Lala Sabathil"; - - /// - /// Gets the team's icon. - /// - public string Icon - => !string.IsNullOrWhiteSpace(this.IconHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.TEAM_ICONS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.IconHash}.png?size=1024" : null; - - /// - /// Gets the team's icon's hash. - /// - public string IconHash { get; internal set; } - - /// - /// Gets the team's logo. - /// - public string Logo - => !string.IsNullOrWhiteSpace(this.LogoHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.ICONS}/{this.GuildId.ToString(CultureInfo.InvariantCulture)}/{this.LogoHash}.png?size=1024" : null; - - /// - /// Gets the team's logo's hash. - /// - public string LogoHash { get; internal set; } - - /// - /// Gets the team's banner. - /// - public string Banner - => !string.IsNullOrWhiteSpace(this.BannerHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.BANNERS}/{this.GuildId.ToString(CultureInfo.InvariantCulture)}/{this.BannerHash}.png?size=1024" : null; - - /// - /// Gets the team's banner's hash. - /// - public string BannerHash { get; internal set; } - - /// - /// Gets the team's docs url. - /// - public string DocsUrl { get; internal set; } - - /// - /// Gets the team's repo url. - /// - public string RepoUrl { get; internal set; } - - /// - /// Gets the team's terms of service url. - /// - public string TermsOfServiceUrl { get; internal set; } - - /// - /// Gets the team's privacy policy url. - /// - public string PrivacyPolicyUrl { get; internal set; } - - /// - /// Get's the team's guild id - /// - public ulong GuildId { get; internal set; } - - /// - /// Gets the team's developers. - /// - public IReadOnlyList Developers { get; internal set; } - - /// - /// Gets the team's owner. - /// - public DisCatSharpTeamMember Owner { get; internal set; } - - /// - /// Gets the team's guild. - /// - public DiscordGuild Guild { get; internal set; } - - /// - /// Gets the team's support invite. - /// - public DiscordInvite SupportInvite { get; internal set; } - - /// - /// Initializes a new instance of the class. - /// - internal static async Task Get(HttpClient http, ILogger logger, DiscordApiClient apiClient) - { - try - { - var dcs = await http.GetStringAsync(new Uri("https://dcs.aitsys.dev/api/devs/")).ConfigureAwait(false); - var dcsGuild = await http.GetStringAsync(new Uri("https://dcs.aitsys.dev/api/guild/")).ConfigureAwait(false); - - var app = JsonConvert.DeserializeObject(dcs); - var guild = JsonConvert.DeserializeObject(dcsGuild); - - var dcst = new DisCatSharpTeam - { - IconHash = app.Team.IconHash, - TeamName = app.Team.Name, - PrivacyPolicyUrl = app.PrivacyPolicyUrl, - TermsOfServiceUrl = app.TermsOfServiceUrl, - RepoUrl = "https://github.com/Aiko-IT-Systems/DisCatSharp", - DocsUrl = "https://docs.dcs.aitsys.dev", - Id = app.Team.Id, - BannerHash = guild.BannerHash, - LogoHash = guild.IconHash, - GuildId = guild.Id, - Guild = guild, - SupportInvite = await apiClient.GetInviteAsync("GGYSywkxwN", true, true, null).ConfigureAwait(false) - }; - List team = []; - DisCatSharpTeamMember owner = new(); - foreach (var mb in app.Team.Members.OrderBy(m => m.User.Username)) - { - var tuser = await apiClient.GetUserAsync(mb.User.Id).ConfigureAwait(false); - var user = mb.User; - if (mb.User.Id == 856780995629154305) - { - owner.Id = user.Id; - owner.Username = user.Username; - owner.Discriminator = user.Discriminator; - owner.AvatarHash = user.AvatarHash; - owner.BannerHash = tuser.BannerHash; - owner.BannerColorInternal = tuser.BannerColorInternal; - team.Add(owner); - } - else - team.Add(new() - { - Id = user.Id, - Username = user.Username, - Discriminator = user.Discriminator, - AvatarHash = user.AvatarHash, - BannerHash = tuser.BannerHash, - BannerColorInternal = tuser.BannerColorInternal - }); - } - - dcst.Owner = owner; - dcst.Developers = team; - - return dcst; - } - catch (Exception ex) - { - logger.LogDebug(ex.Message); - logger.LogDebug(ex.StackTrace); - return null; - } - } - - private DisCatSharpTeam() - { } -} diff --git a/DisCatSharp/Entities/DCS/DisCatSharpTeamMember.cs b/DisCatSharp/Entities/DCS/DisCatSharpTeamMember.cs deleted file mode 100644 index 367e6a71f..000000000 --- a/DisCatSharp/Entities/DCS/DisCatSharpTeamMember.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Globalization; - -using DisCatSharp.Enums; -using DisCatSharp.Net; - -namespace DisCatSharp.Entities; - -/// -/// Represents a DisCatSharp team member. -/// -public sealed class DisCatSharpTeamMember : SnowflakeObject -{ - /// - /// Gets this user's username. - /// - public string Username { get; internal set; } - - /// - /// Gets the user's 4-digit discriminator. - /// - public string Discriminator { get; internal set; } - - /// - /// Gets the discriminator integer. - /// - internal int DiscriminatorInt - => int.Parse(this.Discriminator, NumberStyles.Integer, CultureInfo.InvariantCulture); - - /// - /// Gets the user's banner color, if set. Mutually exclusive with . - /// - public DiscordColor? BannerColor - => !this.BannerColorInternal.HasValue ? null : new DiscordColor(this.BannerColorInternal.Value); - - internal int? BannerColorInternal; - - /// - /// Gets the user's banner url - /// - public string BannerUrl - => string.IsNullOrWhiteSpace(this.BannerHash) ? null : $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.BANNERS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.BannerHash}.{(this.BannerHash.StartsWith("a_", StringComparison.Ordinal) ? "gif" : "png")}?size=4096"; - - /// - /// Gets the user's profile banner hash. Mutually exclusive with . - /// - public string BannerHash { get; internal set; } - - /// - /// Gets the user's avatar hash. - /// - public string AvatarHash { get; internal set; } - - /// - /// Gets the user's avatar URL. - /// - public string AvatarUrl - => string.IsNullOrWhiteSpace(this.AvatarHash) ? this.DefaultAvatarUrl : $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.AVATARS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.AvatarHash}.{(this.AvatarHash.StartsWith("a_", StringComparison.Ordinal) ? "gif" : "png")}?size=1024"; - - /// - /// Gets the URL of default avatar for this user. - /// - public string DefaultAvatarUrl - => $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.EMBED}{Endpoints.AVATARS}/{(this.DiscriminatorInt % 5).ToString(CultureInfo.InvariantCulture)}.png?size=1024"; - - /// - /// Initializes a new instance of the class. - /// - internal DisCatSharpTeamMember() - { } -} diff --git a/DisCatSharp/Entities/DCS/GitHubRelease.cs b/DisCatSharp/Entities/DCS/GitHubRelease.cs deleted file mode 100644 index ed54c37a7..000000000 --- a/DisCatSharp/Entities/DCS/GitHubRelease.cs +++ /dev/null @@ -1,165 +0,0 @@ -using System; -using System.Collections.Generic; - -using Newtonsoft.Json; - -namespace DisCatSharp.Entities.DCS; - -internal class GitHubRelease -{ - [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] - public Uri Url { get; set; } - - [JsonProperty("assets_url", NullValueHandling = NullValueHandling.Ignore)] - public Uri AssetsUrl { get; set; } - - [JsonProperty("upload_url", NullValueHandling = NullValueHandling.Ignore)] - public Uri UploadUrl { get; set; } - - [JsonProperty("html_url", NullValueHandling = NullValueHandling.Ignore)] - public Uri HtmlUrl { get; set; } - - [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] - public int? Id { get; set; } - - [JsonProperty("author", NullValueHandling = NullValueHandling.Ignore)] - public GitHubUser Author { get; set; } - - [JsonProperty("node_id", NullValueHandling = NullValueHandling.Ignore)] - public string NodeId { get; set; } - - [JsonProperty("tag_name", NullValueHandling = NullValueHandling.Ignore)] - public string TagName { get; set; } - - [JsonProperty("target_commitish", NullValueHandling = NullValueHandling.Ignore)] - public string TargetCommitish { get; set; } - - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; set; } - - [JsonProperty("draft", NullValueHandling = NullValueHandling.Ignore)] - public bool? Draft { get; set; } - - [JsonProperty("prerelease", NullValueHandling = NullValueHandling.Ignore)] - public bool? Prerelease { get; set; } - - [JsonProperty("created_at", NullValueHandling = NullValueHandling.Ignore)] - public DateTime? CreatedAt { get; set; } - - [JsonProperty("published_at", NullValueHandling = NullValueHandling.Ignore)] - public DateTime? PublishedAt { get; set; } - - [JsonProperty("assets", NullValueHandling = NullValueHandling.Ignore)] - public List Assets { get; set; } - - [JsonProperty("tarball_url", NullValueHandling = NullValueHandling.Ignore)] - public Uri TarballUrl { get; set; } - - [JsonProperty("zipball_url", NullValueHandling = NullValueHandling.Ignore)] - public Uri ZipballUrl { get; set; } - - [JsonProperty("body", NullValueHandling = NullValueHandling.Ignore)] - public string Body { get; set; } - - [JsonProperty("mentions_count", NullValueHandling = NullValueHandling.Ignore)] - public int? MentionsCount { get; set; } - - public class Asset - { - [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] - public Uri Url { get; set; } - - [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] - public int? Id { get; set; } - - [JsonProperty("node_id", NullValueHandling = NullValueHandling.Ignore)] - public string NodeId { get; set; } - - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; set; } - - [JsonProperty("label", NullValueHandling = NullValueHandling.Ignore)] - public string? Label { get; set; } - - [JsonProperty("uploader", NullValueHandling = NullValueHandling.Ignore)] - public GitHubUser Uploader { get; set; } - - [JsonProperty("content_type", NullValueHandling = NullValueHandling.Ignore)] - public string ContentType { get; set; } - - [JsonProperty("state", NullValueHandling = NullValueHandling.Ignore)] - public string State { get; set; } - - [JsonProperty("size", NullValueHandling = NullValueHandling.Ignore)] - public int? Size { get; set; } - - [JsonProperty("download_count", NullValueHandling = NullValueHandling.Ignore)] - public int? DownloadCount { get; set; } - - [JsonProperty("created_at", NullValueHandling = NullValueHandling.Ignore)] - public DateTime? CreatedAt { get; set; } - - [JsonProperty("updated_at", NullValueHandling = NullValueHandling.Ignore)] - public DateTime? UpdatedAt { get; set; } - - [JsonProperty("browser_download_url", NullValueHandling = NullValueHandling.Ignore)] - public Uri BrowserDownloadUrl { get; set; } - } - - public class GitHubUser - { - [JsonProperty("login", NullValueHandling = NullValueHandling.Ignore)] - public string Login { get; set; } - - [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] - public int? Id { get; set; } - - [JsonProperty("node_id", NullValueHandling = NullValueHandling.Ignore)] - public string NodeId { get; set; } - - [JsonProperty("avatar_url", NullValueHandling = NullValueHandling.Ignore)] - public Uri AvatarUrl { get; set; } - - [JsonProperty("gravatar_id", NullValueHandling = NullValueHandling.Ignore)] - public string GravatarId { get; set; } - - [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] - public Uri Url { get; set; } - - [JsonProperty("html_url", NullValueHandling = NullValueHandling.Ignore)] - public Uri HtmlUrl { get; set; } - - [JsonProperty("followers_url", NullValueHandling = NullValueHandling.Ignore)] - public Uri FollowersUrl { get; set; } - - [JsonProperty("following_url", NullValueHandling = NullValueHandling.Ignore)] - public Uri FollowingUrl { get; set; } - - [JsonProperty("gists_url", NullValueHandling = NullValueHandling.Ignore)] - public Uri GistsUrl { get; set; } - - [JsonProperty("starred_url", NullValueHandling = NullValueHandling.Ignore)] - public Uri StarredUrl { get; set; } - - [JsonProperty("subscriptions_url", NullValueHandling = NullValueHandling.Ignore)] - public Uri SubscriptionsUrl { get; set; } - - [JsonProperty("organizations_url", NullValueHandling = NullValueHandling.Ignore)] - public Uri OrganizationsUrl { get; set; } - - [JsonProperty("repos_url", NullValueHandling = NullValueHandling.Ignore)] - public Uri ReposUrl { get; set; } - - [JsonProperty("events_url", NullValueHandling = NullValueHandling.Ignore)] - public Uri EventsUrl { get; set; } - - [JsonProperty("received_events_url", NullValueHandling = NullValueHandling.Ignore)] - public Uri ReceivedEventsUrl { get; set; } - - [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] - public string Type { get; set; } - - [JsonProperty("site_admin", NullValueHandling = NullValueHandling.Ignore)] - public bool? SiteAdmin { get; set; } - } -} diff --git a/DisCatSharp.ApplicationCommands/Enums/CooldownBucketType.cs b/DisCatSharp/Enums/Core/CooldownBucketType.cs similarity index 68% rename from DisCatSharp.ApplicationCommands/Enums/CooldownBucketType.cs rename to DisCatSharp/Enums/Core/CooldownBucketType.cs index de5e750dc..f50fecab0 100644 --- a/DisCatSharp.ApplicationCommands/Enums/CooldownBucketType.cs +++ b/DisCatSharp/Enums/Core/CooldownBucketType.cs @@ -1,8 +1,11 @@ -namespace DisCatSharp.ApplicationCommands.Enums; +using System; + +namespace DisCatSharp.Enums.Core; /// /// Defines how are command cooldowns applied. /// +[Flags] public enum CooldownBucketType { /// @@ -11,7 +14,7 @@ public enum CooldownBucketType Global = 0, /// - /// Denotes that the command will have its cooldown applied per-user. + /// Denotes that the command will have its cooldown applied per-user globally. /// User = 1, @@ -21,7 +24,12 @@ public enum CooldownBucketType Channel = 2, /// - /// Denotes that the command will have its cooldown applied per-guild. In DMs, this applies the cooldown per-channel. + /// Denotes that the command will have its cooldown applied per-guild. Skipped for DMs. + /// + Guild = 3, + + /// + /// Denotes that the command will have its cooldown applied per-member per-guild. Skipped for DMs. /// - Guild = 4 + Member = 4 } diff --git a/DisCatSharp/Enums/Core/DisCatSharpCommandGroupingType.cs b/DisCatSharp/Enums/Core/DisCatSharpCommandGroupingType.cs new file mode 100644 index 000000000..6debe497b --- /dev/null +++ b/DisCatSharp/Enums/Core/DisCatSharpCommandGroupingType.cs @@ -0,0 +1,27 @@ +namespace DisCatSharp.Enums.Core; + +/// +/// Represents the grouping type of a command. +/// +public enum DisCatSharpCommandGroupingType +{ + /// + /// This is a special type and not a command. + /// + None, + + /// + /// The command is not part of a group. + /// + Command, + + /// + /// The command is a sub command. + /// + SubCommand, + + /// + /// The command is a sub group command. + /// + SubGroupCommand +} diff --git a/DisCatSharp/Enums/Core/DisCatSharpCommandType.cs b/DisCatSharp/Enums/Core/DisCatSharpCommandType.cs new file mode 100644 index 000000000..22619fd78 --- /dev/null +++ b/DisCatSharp/Enums/Core/DisCatSharpCommandType.cs @@ -0,0 +1,32 @@ +namespace DisCatSharp.Enums.Core; + +/// +/// Represents the type of a command. +/// +public enum DisCatSharpCommandType +{ + /// + /// A text command. + /// + TextCommand, + + /// + /// A slash command. + /// + SlashCommand, + + /// + /// A user context menu command. + /// + UserCommand, + + /// + /// A message context menu command. + /// + MessageCommand, + + /// + /// A special type. + /// + Special +} diff --git a/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs b/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs index fae7e13eb..2c5d079cc 100644 --- a/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs +++ b/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs @@ -28,25 +28,25 @@ internal sealed class RestApplicationCommandCreatePayload : ObservableApiObject /// Gets the name localizations. /// [JsonProperty("name_localizations", NullValueHandling = NullValueHandling.Ignore)] - public Optional> NameLocalizations { get; set; } + public Optional?> NameLocalizations { get; set; } /// /// Gets the description. /// [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public string Description { get; set; } + public string? Description { get; set; } /// /// Gets the description localizations. /// [JsonProperty("description_localizations", NullValueHandling = NullValueHandling.Ignore)] - public Optional> DescriptionLocalizations { get; set; } + public Optional?> DescriptionLocalizations { get; set; } /// /// Gets the options. /// [JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable Options { get; set; } + public IEnumerable? Options { get; set; } /// /// Whether the command is allowed for everyone. @@ -99,26 +99,26 @@ internal sealed class RestApplicationCommandEditPayload : ObservableApiObject /// /// Gets the name localizations. /// - [JsonProperty("name_localizations")] - public Optional> NameLocalizations { get; set; } + [JsonProperty("name_localizations", NullValueHandling = NullValueHandling.Ignore)] + public Optional?> NameLocalizations { get; set; } /// /// Gets the description. /// [JsonProperty("description")] - public Optional Description { get; set; } + public Optional Description { get; set; } /// /// Gets the description localizations. /// - [JsonProperty("description_localizations")] - public Optional> DescriptionLocalizations { get; set; } + [JsonProperty("description_localizations", NullValueHandling = NullValueHandling.Ignore)] + public Optional?> DescriptionLocalizations { get; set; } /// /// Gets the options. /// [JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)] - public Optional> Options { get; set; } + public Optional?> Options { get; set; } /// /// The command needed permissions. diff --git a/DisCatSharp/Net/Rest/DiscordApiClient.cs b/DisCatSharp/Net/Rest/DiscordApiClient.cs index 7f74ea06a..ca6f72df9 100644 --- a/DisCatSharp/Net/Rest/DiscordApiClient.cs +++ b/DisCatSharp/Net/Rest/DiscordApiClient.cs @@ -5946,21 +5946,20 @@ internal async Task> BulkOverwriteGloba { var pld = new List(); if (commands.Any()) - foreach (var command in commands) - pld.Add(new() - { - Type = command.Type, - Name = command.Name, - Description = command.Type == ApplicationCommandType.ChatInput ? command.Description : null, - Options = command.Options, - NameLocalizations = command.NameLocalizations?.GetKeyValuePairs(), - DescriptionLocalizations = command.DescriptionLocalizations?.GetKeyValuePairs(), - DefaultMemberPermission = command.DefaultMemberPermissions, - DmPermission = command.DmPermission, - Nsfw = command.IsNsfw, - AllowedContexts = command.AllowedContexts, - IntegrationTypes = command.IntegrationTypes - }); + pld.AddRange(commands.Select(command => new RestApplicationCommandCreatePayload() + { + Type = command.Type, + Name = command.Name, + Description = command.Type is ApplicationCommandType.ChatInput ? command.Description : null, + Options = command.Options, + NameLocalizations = command.NameLocalizations?.GetKeyValuePairs(), + DescriptionLocalizations = command.DescriptionLocalizations?.GetKeyValuePairs(), + DefaultMemberPermission = command.DefaultMemberPermissions, + DmPermission = command.DmPermission, + Nsfw = command.IsNsfw, + AllowedContexts = command.AllowedContexts, + IntegrationTypes = command.IntegrationTypes + })); var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new @@ -5986,10 +5985,10 @@ internal async Task CreateGlobalApplicationCommandAsy { Type = command.Type, Name = command.Name, - Description = command.Type == ApplicationCommandType.ChatInput ? command.Description : null, + Description = command.Type is ApplicationCommandType.ChatInput ? command.Description : null, Options = command.Options, - NameLocalizations = command.NameLocalizations.GetKeyValuePairs(), - DescriptionLocalizations = command.DescriptionLocalizations.GetKeyValuePairs(), + NameLocalizations = command.NameLocalizations?.GetKeyValuePairs(), + DescriptionLocalizations = command.DescriptionLocalizations?.GetKeyValuePairs(), DefaultMemberPermission = command.DefaultMemberPermissions, DmPermission = command.DmPermission, Nsfw = command.IsNsfw, @@ -6054,10 +6053,10 @@ internal async Task EditGlobalApplicationCommandAsync ulong applicationId, ulong commandId, Optional name, - Optional description, + Optional description, Optional?> options, - Optional nameLocalization, - Optional descriptionLocalization, + Optional nameLocalization, + Optional descriptionLocalization, Optional defaultMemberPermission, Optional dmPermission, Optional isNsfw, @@ -6149,20 +6148,19 @@ internal async Task> BulkOverwriteGuild { var pld = new List(); if (commands.Any()) - foreach (var command in commands) - pld.Add(new() - { - Type = command.Type, - Name = command.Name, - Description = command.Type == ApplicationCommandType.ChatInput ? command.Description : null, - Options = command.Options, - NameLocalizations = command.NameLocalizations?.GetKeyValuePairs(), - DescriptionLocalizations = command.DescriptionLocalizations?.GetKeyValuePairs(), - DefaultMemberPermission = command.DefaultMemberPermissions, - DmPermission = command.DmPermission, - Nsfw = command.IsNsfw, - AllowedContexts = command.AllowedContexts - }); + pld.AddRange(commands.Select(command => new RestApplicationCommandCreatePayload() + { + Type = command.Type, + Name = command.Name, + Description = command.Type is ApplicationCommandType.ChatInput ? command.Description : null, + Options = command.Options, + NameLocalizations = command.NameLocalizations?.GetKeyValuePairs(), + DescriptionLocalizations = command.DescriptionLocalizations?.GetKeyValuePairs(), + DefaultMemberPermission = command.DefaultMemberPermissions, + DmPermission = command.DmPermission, + Nsfw = command.IsNsfw, + AllowedContexts = command.AllowedContexts + })); var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new @@ -6192,8 +6190,8 @@ internal async Task CreateGuildApplicationCommandAsyn Name = command.Name, Description = command.Type == ApplicationCommandType.ChatInput ? command.Description : null, Options = command.Options, - NameLocalizations = command.NameLocalizations.GetKeyValuePairs(), - DescriptionLocalizations = command.DescriptionLocalizations.GetKeyValuePairs(), + NameLocalizations = command.NameLocalizations?.GetKeyValuePairs(), + DescriptionLocalizations = command.DescriptionLocalizations?.GetKeyValuePairs(), DefaultMemberPermission = command.DefaultMemberPermissions, DmPermission = command.DmPermission, Nsfw = command.IsNsfw, @@ -6262,10 +6260,10 @@ internal async Task EditGuildApplicationCommandAsync( ulong guildId, ulong commandId, Optional name, - Optional description, + Optional description, Optional?> options, - Optional nameLocalization, - Optional descriptionLocalization, + Optional nameLocalization, + Optional descriptionLocalization, Optional defaultMemberPermission, Optional dmPermission, Optional isNsfw, From abb2e701069753c22d54b746bf03cbeda21a7c35 Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Tue, 16 Jan 2024 21:36:23 +0100 Subject: [PATCH 02/14] [ci skip] chore: resharper disable for sealed on CooldownBucket --- DisCatSharp/Entities/Core/CooldownBucket.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/DisCatSharp/Entities/Core/CooldownBucket.cs b/DisCatSharp/Entities/Core/CooldownBucket.cs index de21ef767..24cc57bd4 100644 --- a/DisCatSharp/Entities/Core/CooldownBucket.cs +++ b/DisCatSharp/Entities/Core/CooldownBucket.cs @@ -10,6 +10,7 @@ namespace DisCatSharp.Entities.Core; /// /// Represents a cooldown bucket. /// +// ReSharper disable once ClassCanBeSealed.Global "This class can be inherited from by developers." public class CooldownBucket : IBucket, IEquatable { /// From 0d4d471c3696e1eef321e43e7190c5f70585f542 Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Tue, 16 Jan 2024 23:41:44 +0100 Subject: [PATCH 03/14] fix: try fixing translations (while i'm on it) --- .../ApplicationCommandsExtension.cs | 20 +++++++------- .../ContextMenuCooldownAttribute.cs | 27 +++++++------------ .../SlashCommandCooldownAttribute.cs | 27 +++++++------------ .../Entities/FakeApplicationCommandObjects.cs | 8 ++++-- .../GlobalSuppressions.cs | 2 -- .../Attributes/CooldownAttribute.cs | 27 +++++++------------ .../GlobalSuppressions.cs | 1 - DisCatSharp/Entities/Core/ICooldown.cs | 3 ++- DisCatSharp/GlobalSuppressions.cs | 10 ------- DisCatSharp/Net/Rest/DiscordApiClient.cs | 4 +-- 10 files changed, 49 insertions(+), 80 deletions(-) diff --git a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs index c55d67a48..64f1b75af 100644 --- a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs +++ b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs @@ -662,12 +662,13 @@ private async Task RegisterCommands(List if (Configuration.GenerateTranslationFilesOnly) { var cgwsgs = new List(); - var cgs2 = new List(); foreach (var cmd in slashGroupsTuple.applicationCommands) if (cmd.Type is ApplicationCommandType.ChatInput) { var cgs = new List(); + var cs2 = new List(); if (cmd.Options is not null) + { foreach (var scg in cmd.Options.Where(x => x.Type is ApplicationCommandOptionType.SubCommandGroup)) { var cs = new List(); @@ -680,21 +681,18 @@ private async Task RegisterCommands(List cgs.Add(new(scg.Name, scg.Description, cs, null)); } - cgwsgs.Add(new(cmd.Name, cmd.Description, cgs, cmd.Type)); + foreach (var sc2 in cmd.Options.Where(x => x.Type is ApplicationCommandOptionType.SubCommand)) + if (sc2.Options == null || sc2.Options.Count == 0) + cs2.Add(new(sc2.Name, sc2.Description, null, null)); + else + cs2.Add(new(sc2.Name, sc2.Description, [.. sc2.Options], null)); + } - var cs2 = new List(); - foreach (var sc2 in cmd.Options.Where(x => x.Type is ApplicationCommandOptionType.SubCommand)) - if (sc2.Options == null || sc2.Options.Count == 0) - cs2.Add(new(sc2.Name, sc2.Description, null, null)); - else - cs2.Add(new(sc2.Name, sc2.Description, [.. sc2.Options], null)); - cgs2.Add(new(cmd.Name, cmd.Description, cs2, cmd.Type)); + cgwsgs.Add(new(cmd.Name, cmd.Description, cgs, cs2, cmd.Type)); } if (cgwsgs.Count is not 0) groupTranslation.AddRange(cgwsgs.Select(cgwsg => JsonConvert.DeserializeObject(JsonConvert.SerializeObject(cgwsg))!)); - if (cgs2.Count is not 0) - groupTranslation.AddRange(cgs2.Select(cg2 => JsonConvert.DeserializeObject(JsonConvert.SerializeObject(cg2))!)); } } diff --git a/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuCooldownAttribute.cs b/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuCooldownAttribute.cs index b4a1d9052..7c62db5aa 100644 --- a/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuCooldownAttribute.cs +++ b/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuCooldownAttribute.cs @@ -13,36 +13,29 @@ namespace DisCatSharp.ApplicationCommands.Attributes; /// /// Defines a cooldown for this command. This allows you to define how many times can users execute a specific command /// +/// +/// 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, member, channel, and/or globally. [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = false)] -public sealed class ContextMenuCooldownAttribute : ApplicationCommandCheckBaseAttribute, ICooldown +public sealed class ContextMenuCooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType) : ApplicationCommandCheckBaseAttribute, ICooldown { /// /// Gets the maximum number of uses before this command triggers a cooldown for its bucket. /// - public int MaxUses { get; } + public int MaxUses { get; } = maxUses; /// /// Gets the time after which the cooldown is reset. /// - public TimeSpan Reset { get; } + public TimeSpan Reset { get; } = TimeSpan.FromSeconds(resetAfter); /// /// Gets the type of the cooldown bucket. This determines how cooldowns are applied. /// - public CooldownBucketType BucketType { get; } - - /// - /// 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, member, channel, and/or globally. - public ContextMenuCooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType) - { - this.MaxUses = maxUses; - this.Reset = TimeSpan.FromSeconds(resetAfter); - this.BucketType = bucketType; - } + public CooldownBucketType BucketType { get; } = bucketType; /// /// Gets a cooldown bucket for given command context. diff --git a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs index ec60d6062..5ec4917ca 100644 --- a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs +++ b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs @@ -13,36 +13,29 @@ namespace DisCatSharp.ApplicationCommands.Attributes; /// /// Defines a cooldown for this command. This allows you to define how many times can users execute a specific command. /// +/// +/// 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, member, channel, and/or globally. [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = false)] -public sealed class SlashCommandCooldownAttribute : ApplicationCommandCheckBaseAttribute, ICooldown +public sealed class SlashCommandCooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType) : ApplicationCommandCheckBaseAttribute, ICooldown { /// /// Gets the maximum number of uses before this command triggers a cooldown for its bucket. /// - public int MaxUses { get; } + public int MaxUses { get; } = maxUses; /// /// Gets the time after which the cooldown is reset. /// - public TimeSpan Reset { get; } + public TimeSpan Reset { get; } = TimeSpan.FromSeconds(resetAfter); /// /// Gets the type of the cooldown bucket. This determines how cooldowns are applied. /// - public CooldownBucketType BucketType { get; } - - /// - /// 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, member, channel, and/or globally. - public SlashCommandCooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType) - { - this.MaxUses = maxUses; - this.Reset = TimeSpan.FromSeconds(resetAfter); - this.BucketType = bucketType; - } + public CooldownBucketType BucketType { get; } = bucketType; /// /// Gets a cooldown bucket for given command context. diff --git a/DisCatSharp.ApplicationCommands/Entities/FakeApplicationCommandObjects.cs b/DisCatSharp.ApplicationCommands/Entities/FakeApplicationCommandObjects.cs index f2ca93afb..e6940dbb1 100644 --- a/DisCatSharp.ApplicationCommands/Entities/FakeApplicationCommandObjects.cs +++ b/DisCatSharp.ApplicationCommands/Entities/FakeApplicationCommandObjects.cs @@ -12,10 +12,14 @@ internal sealed class CommandGroupWithSubGroups : BaseCommand [JsonProperty("groups")] internal List SubGroups { get; set; } - internal CommandGroupWithSubGroups(string name, string description, List commands, ApplicationCommandType type) + [JsonProperty("commands")] + internal List Commands { get; set; } + + internal CommandGroupWithSubGroups(string name, string description, List subGroups, List commands, ApplicationCommandType type) : base(name, description, type) { - this.SubGroups = commands; + this.SubGroups = subGroups; + this.Commands = commands; } } diff --git a/DisCatSharp.ApplicationCommands/GlobalSuppressions.cs b/DisCatSharp.ApplicationCommands/GlobalSuppressions.cs index e523816e8..b2219dfa7 100644 --- a/DisCatSharp.ApplicationCommands/GlobalSuppressions.cs +++ b/DisCatSharp.ApplicationCommands/GlobalSuppressions.cs @@ -35,9 +35,7 @@ [assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.Attributes.ChannelTypesAttribute.#ctor(DisCatSharp.Enums.ChannelType[])")] [assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.Attributes.ChoiceNameAttribute.#ctor(System.String)")] [assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.Attributes.ChoiceProviderAttribute.#ctor(System.Type)")] -[assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.Attributes.ContextMenuCooldownAttribute.#ctor(System.Int32,System.Double,DisCatSharp.ApplicationCommands.Enums.CooldownBucketType)")] [assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.Attributes.RequireAnyPermissionsAttribute.#ctor(DisCatSharp.Enums.Permissions[])")] -[assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.Attributes.SlashCommandCooldownAttribute.#ctor(System.Int32,System.Double,DisCatSharp.ApplicationCommands.Enums.CooldownBucketType)")] [assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.EventArgs.ContextMenuErrorEventArgs.#ctor(System.IServiceProvider)")] [assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.EventArgs.ContextMenuExecutedEventArgs.#ctor(System.IServiceProvider)")] [assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.EventArgs.SlashCommandErrorEventArgs.#ctor(System.IServiceProvider)")] diff --git a/DisCatSharp.CommandsNext/Attributes/CooldownAttribute.cs b/DisCatSharp.CommandsNext/Attributes/CooldownAttribute.cs index 3bcd913e5..90990f053 100644 --- a/DisCatSharp.CommandsNext/Attributes/CooldownAttribute.cs +++ b/DisCatSharp.CommandsNext/Attributes/CooldownAttribute.cs @@ -11,36 +11,29 @@ namespace DisCatSharp.CommandsNext.Attributes; /// /// Defines a cooldown for this command. This allows you to define how many times can users execute a specific command /// +/// +/// 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. [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = false)] -public sealed class CooldownAttribute : CheckBaseAttribute, ICooldown +public sealed class CooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType) : CheckBaseAttribute, ICooldown { /// /// Gets the maximum number of uses before this command triggers a cooldown for its bucket. /// - public int MaxUses { get; } + public int MaxUses { get; } = maxUses; /// /// Gets the time after which the cooldown is reset. /// - public TimeSpan Reset { get; } + public TimeSpan Reset { get; } = TimeSpan.FromSeconds(resetAfter); /// /// Gets the type of the cooldown bucket. This determines how cooldowns are applied. /// - public CooldownBucketType BucketType { get; } - - /// - /// 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 CooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType) - { - this.MaxUses = maxUses; - this.Reset = TimeSpan.FromSeconds(resetAfter); - this.BucketType = bucketType; - } + public CooldownBucketType BucketType { get; } = bucketType; /// /// Gets a cooldown bucket for given command context. diff --git a/DisCatSharp.CommandsNext/GlobalSuppressions.cs b/DisCatSharp.CommandsNext/GlobalSuppressions.cs index 21e46756f..0c4423d19 100644 --- a/DisCatSharp.CommandsNext/GlobalSuppressions.cs +++ b/DisCatSharp.CommandsNext/GlobalSuppressions.cs @@ -15,7 +15,6 @@ [assembly: SuppressMessage("Performance", "CA1860:Avoid using 'Enumerable.Any()' extension method", Justification = "", Scope = "member", Target = "~M:DisCatSharp.CommandsNext.Attributes.RequirePrefixesAttribute.#ctor(System.String[])")] [assembly: SuppressMessage("Performance", "CA1860:Avoid using 'Enumerable.Any()' extension method", Justification = "", Scope = "member", Target = "~M:DisCatSharp.CommandsNext.CommandsNextExtension.HandleCommandsAsync(DisCatSharp.DiscordClient,DisCatSharp.EventArgs.MessageCreateEventArgs)~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Performance", "CA1862:Use the 'StringComparison' method overloads to perform case-insensitive string comparisons", Justification = "", Scope = "member", Target = "~M:DisCatSharp.CommandsNext.CommandsNextExtension.FindCommand(System.String,System.String@)~DisCatSharp.CommandsNext.Command")] -[assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.CommandsNext.Attributes.CooldownAttribute.#ctor(System.Int32,System.Double,DisCatSharp.CommandsNext.Attributes.CooldownBucketType)")] [assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.CommandsNext.Attributes.DescriptionAttribute.#ctor(System.String)")] [assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.CommandsNext.Attributes.ModuleLifespanAttribute.#ctor(DisCatSharp.CommandsNext.Attributes.ModuleLifespan)")] [assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.CommandsNext.Attributes.PriorityAttribute.#ctor(System.Int32)")] diff --git a/DisCatSharp/Entities/Core/ICooldown.cs b/DisCatSharp/Entities/Core/ICooldown.cs index 0b9c7dde8..a73a9ca79 100644 --- a/DisCatSharp/Entities/Core/ICooldown.cs +++ b/DisCatSharp/Entities/Core/ICooldown.cs @@ -47,6 +47,7 @@ public interface ICooldown /// Responds to a ratelimit hit. /// /// The command context. - /// Whether the ratelimit wasn't it. + /// Whether the ratelimit wasn't hit. + /// The cooldown bucket. Task RespondRatelimitHitAsync(TContextType ctx, bool noHit, CooldownBucket bucket); } diff --git a/DisCatSharp/GlobalSuppressions.cs b/DisCatSharp/GlobalSuppressions.cs index 39440d9bd..bd0eb64b2 100644 --- a/DisCatSharp/GlobalSuppressions.cs +++ b/DisCatSharp/GlobalSuppressions.cs @@ -26,7 +26,6 @@ [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordClient.WsSendAsync(System.String)~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordShardedClient.EventErrorHandler``1(DisCatSharp.Common.Utilities.AsyncEvent{DisCatSharp.DiscordClient,``0},System.Exception,DisCatSharp.Common.Utilities.AsyncEventHandler{DisCatSharp.DiscordClient,``0},DisCatSharp.DiscordClient,``0)")] [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.RestClient.BuildRequest(DisCatSharp.Net.BaseRestRequest)~System.Net.Http.HttpRequestMessage")] -[assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.RestClient.ExecuteRequestAsync(DisCatSharp.Net.BaseRestRequest,DisCatSharp.Net.RateLimitBucket,System.Threading.Tasks.TaskCompletionSource{System.Boolean})~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordClient.OnEmbeddedActivityUpdateAsync(Newtonsoft.Json.Linq.JObject,DisCatSharp.Entities.DiscordGuild,System.UInt64,Newtonsoft.Json.Linq.JArray,System.UInt64)~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.Abstractions.GamePartySizeConverter.ReadArrayObject(Newtonsoft.Json.JsonReader,Newtonsoft.Json.JsonSerializer)~Newtonsoft.Json.Linq.JArray")] [assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.Abstractions.ShardInfoConverter.ReadArrayObject(Newtonsoft.Json.JsonReader,Newtonsoft.Json.JsonSerializer)~Newtonsoft.Json.Linq.JArray")] @@ -43,11 +42,9 @@ [assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordClient.Goof``2(DisCatSharp.Common.Utilities.AsyncEvent{``0,``1},System.Exception,DisCatSharp.Common.Utilities.AsyncEventHandler{``0,``1},``0,``1)")] [assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordClient.ConnectAsync(DisCatSharp.Entities.DiscordActivity,System.Nullable{DisCatSharp.Entities.UserStatus},System.Nullable{System.DateTimeOffset})~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.RestClient.CleanupBucketsAsync~System.Threading.Tasks.Task")] -[assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.RestClient.ExecuteRequestAsync(DisCatSharp.Net.BaseRestRequest,DisCatSharp.Net.RateLimitBucket,System.Threading.Tasks.TaskCompletionSource{System.Boolean})~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.RestClient.UpdateHashCaches(DisCatSharp.Net.BaseRestRequest,DisCatSharp.Net.RateLimitBucket,System.String)")] [assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.RestClient.WaitForInitialRateLimit(DisCatSharp.Net.RateLimitBucket)~System.Threading.Tasks.Task{System.Threading.Tasks.TaskCompletionSource{System.Boolean}}")] [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordShardedClient.GetGatewayInfoAsync~System.Threading.Tasks.Task{DisCatSharp.Net.GatewayInfo}")] -[assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Entities.DisCatSharpTeam.Get(System.Net.Http.HttpClient,Microsoft.Extensions.Logging.ILogger,DisCatSharp.Net.DiscordApiClient)~System.Threading.Tasks.Task{DisCatSharp.Entities.DisCatSharpTeam}")] [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordShardedClient.StartAsync~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordShardedClient.ConnectShardAsync(System.Int32)~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordShardedClient.EventErrorHandler``1(DisCatSharp.Common.Utilities.AsyncEvent{DisCatSharp.DiscordClient,``0},System.Exception,DisCatSharp.Common.Utilities.AsyncEventHandler{DisCatSharp.DiscordClient,``0},DisCatSharp.DiscordClient,``0)")] @@ -102,13 +99,6 @@ [assembly: SuppressMessage("Performance", "CA1860:Avoid using 'Enumerable.Any()' extension method", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.DiscordApiClient.CreateMessageAsync(System.UInt64,DisCatSharp.Entities.DiscordMessageBuilder)~System.Threading.Tasks.Task{DisCatSharp.Entities.DiscordMessage}")] [assembly: SuppressMessage("Performance", "CA1862:Use the 'StringComparison' method overloads to perform case-insensitive string comparisons", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.RestClient.UpdateBucket(DisCatSharp.Net.BaseRestRequest,DisCatSharp.Net.RestResponse,System.Threading.Tasks.TaskCompletionSource{System.Boolean})")] [assembly: SuppressMessage("Performance", "CA1862:Use the 'StringComparison' method overloads to perform case-insensitive string comparisons", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.RestClient.Handle429(DisCatSharp.Net.RestResponse,System.Threading.Tasks.Task@,System.Boolean@)")] - -/* Unmerged change from project 'DisCatSharp (net8.0)' -Added: -[assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Entities.RoleMention.#ctor(System.UInt64)")] -[assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Entities.UserMention.#ctor(System.UInt64)")] -*/ -[assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Ascii.ImageSharpImageSource.#ctor(SixLabors.ImageSharp.Image{SixLabors.ImageSharp.PixelFormats.Rgba32})")] [assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.CompositeDefaultLogger.#ctor(System.Collections.Generic.IEnumerable{Microsoft.Extensions.Logging.ILoggerProvider})")] [assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Entities.DiscordApplicationRoleConnectionMetadata.#ctor(DisCatSharp.Enums.ApplicationRoleConnectionMetadataType,System.String,System.String,System.String,DisCatSharp.Entities.DiscordApplicationCommandLocalization,DisCatSharp.Entities.DiscordApplicationCommandLocalization)")] [assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Entities.ForumReactionEmoji.#ctor(System.Nullable{System.UInt64},System.String)")] diff --git a/DisCatSharp/Net/Rest/DiscordApiClient.cs b/DisCatSharp/Net/Rest/DiscordApiClient.cs index ca6f72df9..db031457c 100644 --- a/DisCatSharp/Net/Rest/DiscordApiClient.cs +++ b/DisCatSharp/Net/Rest/DiscordApiClient.cs @@ -4672,7 +4672,7 @@ internal async Task ExecuteWebhookAsync(ulong webhookId, string }; if (builder.Mentions != null) - pld.Mentions = new(builder.Mentions, builder.Mentions.Any()); + pld.Mentions = new(builder.Mentions, builder.Mentions.Count is not 0); if (builder.Files?.Count > 0) { @@ -6594,7 +6594,7 @@ internal async Task CreateFollowupMessageAsync(ulong application } if (builder.Mentions != null) - pld.Mentions = new(builder.Mentions, builder.Mentions.Any()); + pld.Mentions = new(builder.Mentions, builder.Mentions.Count is not 0); if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count > 0 || builder.IsTts == true || builder.Mentions != null || builder.Files?.Count > 0 || builder.Components?.Count > 0) values["payload_json"] = DiscordJson.SerializeObject(pld); From 59ad839f8a1947d00b5ca5346d4f53343484709c Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Wed, 17 Jan 2024 00:05:42 +0100 Subject: [PATCH 04/14] fix: really fix translation export --- .../ApplicationCommandsExtension.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs index 64f1b75af..72d86d168 100644 --- a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs +++ b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs @@ -803,7 +803,7 @@ private async Task RegisterCommands(List { updateList = updateList.DistinctBy(x => x.Name).ToList(); if (Configuration.GenerateTranslationFilesOnly) - await this.CheckRegistrationStartup(translation, groupTranslation); + await this.CheckRegistrationStartup(translation, groupTranslation, guildId); else try { @@ -964,7 +964,8 @@ private async Task RegisterCommands(List /// /// The optional translations. /// The optional group translations. - private async Task CheckRegistrationStartup(List? translation = null, List? groupTranslation = null) + /// The optional guild id. + private async Task CheckRegistrationStartup(List? translation = null, List? groupTranslation = null, ulong? guildId = null) { if (Configuration.GenerateTranslationFilesOnly) { @@ -972,7 +973,7 @@ private async Task CheckRegistrationStartup(List? translation { if (translation is not null && translation.Count is not 0) { - var fileName = $"translation_generator_export-shard{this.Client.ShardId}-SINGLE.json"; + var fileName = $"translation_generator_export-shard{this.Client.ShardId}-SINGLE-{(guildId.HasValue ? guildId.Value : "global")}.json"; var fs = File.Create(fileName); var ms = new MemoryStream(); var writer = new StreamWriter(ms); @@ -990,7 +991,7 @@ private async Task CheckRegistrationStartup(List? translation if (groupTranslation is not null && groupTranslation.Count is not 0) { - var fileName = $"translation_generator_export-shard{this.Client.ShardId}-GROUP.json"; + var fileName = $"translation_generator_export-shard{this.Client.ShardId}-GROUP-{(guildId.HasValue ? guildId.Value : "global")}.json"; var fs = File.Create(fileName); var ms = new MemoryStream(); var writer = new StreamWriter(ms); @@ -1029,6 +1030,8 @@ private async Task CheckStartupFinishAsync(ApplicationCommandsExtension sender, GuildsWithoutScope = s_missingScopeGuildIdsGlobal }).ConfigureAwait(false); FinishFired = true; + if (Configuration.GenerateTranslationFilesOnly) + Environment.Exit(0); } args.Handled = false; From 1ffb5420d5a9645de4f21ffda7c9b4b417cedcdd Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Tue, 16 Jan 2024 23:41:44 +0100 Subject: [PATCH 05/14] fix: fixed translations (while i'm on it) --- .../ApplicationCommandsExtension.cs | 53 +++++++++++-------- .../ContextMenuCooldownAttribute.cs | 27 ++++------ .../SlashCommandCooldownAttribute.cs | 27 ++++------ .../Entities/ChoiceTranslator.cs | 17 ++++-- .../Entities/CommandTranslator.cs | 14 ++--- .../Entities/FakeApplicationCommandObjects.cs | 34 ++++++++---- .../Entities/GroupTranslator.cs | 12 ++--- .../Entities/OptionTranslator.cs | 34 +++++++++--- .../Entities/SubGroupTranslator.cs | 12 ++--- .../GlobalSuppressions.cs | 2 - .../Attributes/CooldownAttribute.cs | 27 ++++------ .../GlobalSuppressions.cs | 1 - .../DiscordApplicationCommandLocalization.cs | 16 +++--- .../DiscordApplicationCommandOptionChoice.cs | 4 +- DisCatSharp/Entities/Core/ICooldown.cs | 3 +- .../Interaction/DiscordInteraction.cs | 6 +++ DisCatSharp/GlobalSuppressions.cs | 10 ---- DisCatSharp/Net/Rest/DiscordApiClient.cs | 4 +- 18 files changed, 165 insertions(+), 138 deletions(-) diff --git a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs index c55d67a48..0c8dc8f11 100644 --- a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs +++ b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs @@ -662,39 +662,37 @@ private async Task RegisterCommands(List if (Configuration.GenerateTranslationFilesOnly) { var cgwsgs = new List(); - var cgs2 = new List(); foreach (var cmd in slashGroupsTuple.applicationCommands) if (cmd.Type is ApplicationCommandType.ChatInput) { var cgs = new List(); + var cs2 = new List(); if (cmd.Options is not null) + { foreach (var scg in cmd.Options.Where(x => x.Type is ApplicationCommandOptionType.SubCommandGroup)) { var cs = new List(); if (scg.Options is not null) foreach (var sc in scg.Options) if (sc.Options is null || sc.Options.Count is 0) - cs.Add(new(sc.Name, sc.Description, null, null)); + cs.Add(new(sc.Name, sc.Description, null, null, sc.RawNameLocalizations, sc.RawDescriptionLocalizations)); else - cs.Add(new(sc.Name, sc.Description, [.. sc.Options], null)); - cgs.Add(new(scg.Name, scg.Description, cs, null)); + cs.Add(new(sc.Name, sc.Description, [.. sc.Options], null, sc.RawNameLocalizations, sc.RawDescriptionLocalizations)); + cgs.Add(new(scg.Name, scg.Description, cs, null, scg.RawNameLocalizations, scg.RawDescriptionLocalizations)); } - cgwsgs.Add(new(cmd.Name, cmd.Description, cgs, cmd.Type)); + foreach (var sc2 in cmd.Options.Where(x => x.Type is ApplicationCommandOptionType.SubCommand)) + if (sc2.Options == null || sc2.Options.Count == 0) + cs2.Add(new(sc2.Name, sc2.Description, null, null, sc2.RawNameLocalizations, sc2.RawDescriptionLocalizations)); + else + cs2.Add(new(sc2.Name, sc2.Description, [.. sc2.Options], null, sc2.RawNameLocalizations, sc2.RawDescriptionLocalizations)); + } - var cs2 = new List(); - foreach (var sc2 in cmd.Options.Where(x => x.Type is ApplicationCommandOptionType.SubCommand)) - if (sc2.Options == null || sc2.Options.Count == 0) - cs2.Add(new(sc2.Name, sc2.Description, null, null)); - else - cs2.Add(new(sc2.Name, sc2.Description, [.. sc2.Options], null)); - cgs2.Add(new(cmd.Name, cmd.Description, cs2, cmd.Type)); + cgwsgs.Add(new(cmd.Name, cmd.Description, cgs, cs2, cmd.Type, cmd.RawNameLocalizations, cmd.RawDescriptionLocalizations)); } if (cgwsgs.Count is not 0) groupTranslation.AddRange(cgwsgs.Select(cgwsg => JsonConvert.DeserializeObject(JsonConvert.SerializeObject(cgwsg))!)); - if (cgs2.Count is not 0) - groupTranslation.AddRange(cgs2.Select(cg2 => JsonConvert.DeserializeObject(JsonConvert.SerializeObject(cg2))!)); } } @@ -734,12 +732,20 @@ private async Task RegisterCommands(List var cs = new List(); foreach (var cmd in slashCommands.applicationCommands.Where(cmd => cmd.Type is ApplicationCommandType.ChatInput && (cmd.Options is null || !cmd.Options.Any(x => x.Type is ApplicationCommandOptionType.SubCommand or ApplicationCommandOptionType.SubCommandGroup)))) if (cmd.Options == null || cmd.Options.Count == 0) - cs.Add(new(cmd.Name, cmd.Description, null, ApplicationCommandType.ChatInput)); + cs.Add(new(cmd.Name, cmd.Description, null, ApplicationCommandType.ChatInput, cmd.RawNameLocalizations, cmd.RawDescriptionLocalizations)); else - cs.Add(new(cmd.Name, cmd.Description, [.. cmd.Options], ApplicationCommandType.ChatInput)); + cs.Add(new(cmd.Name, cmd.Description, [.. cmd.Options], ApplicationCommandType.ChatInput, cmd.RawNameLocalizations, cmd.RawDescriptionLocalizations)); if (cs.Count is not 0) - translation.AddRange(cs.Select(c => JsonConvert.DeserializeObject(JsonConvert.SerializeObject(c))!)); + //translation.AddRange(cs.Select(c => JsonConvert.DeserializeObject(JsonConvert.SerializeObject(c))!)); + { + foreach (var c in cs) + { + var json = JsonConvert.SerializeObject(c); + var obj = JsonConvert.DeserializeObject(json); + translation.Add(obj!); + } + } } } @@ -805,7 +811,7 @@ private async Task RegisterCommands(List { updateList = updateList.DistinctBy(x => x.Name).ToList(); if (Configuration.GenerateTranslationFilesOnly) - await this.CheckRegistrationStartup(translation, groupTranslation); + await this.CheckRegistrationStartup(translation, groupTranslation, guildId); else try { @@ -912,7 +918,7 @@ private async Task RegisterCommands(List RegisteredCommands = GlobalCommandsInternal }).ConfigureAwait(false); - await this.CheckRegistrationStartup(translation, groupTranslation); + await this.CheckRegistrationStartup(translation, groupTranslation, guildId); } catch (NullReferenceException ex) { @@ -966,7 +972,8 @@ private async Task RegisterCommands(List /// /// The optional translations. /// The optional group translations. - private async Task CheckRegistrationStartup(List? translation = null, List? groupTranslation = null) + /// The optional guild id. + private async Task CheckRegistrationStartup(List? translation = null, List? groupTranslation = null, ulong? guildId = null) { if (Configuration.GenerateTranslationFilesOnly) { @@ -974,7 +981,7 @@ private async Task CheckRegistrationStartup(List? translation { if (translation is not null && translation.Count is not 0) { - var fileName = $"translation_generator_export-shard{this.Client.ShardId}-SINGLE.json"; + var fileName = $"translation_generator_export-shard{this.Client.ShardId}-SINGLE-{(guildId.HasValue ? guildId.Value : "global")}.json"; var fs = File.Create(fileName); var ms = new MemoryStream(); var writer = new StreamWriter(ms); @@ -992,7 +999,7 @@ private async Task CheckRegistrationStartup(List? translation if (groupTranslation is not null && groupTranslation.Count is not 0) { - var fileName = $"translation_generator_export-shard{this.Client.ShardId}-GROUP.json"; + var fileName = $"translation_generator_export-shard{this.Client.ShardId}-GROUP-{(guildId.HasValue ? guildId.Value : "global")}.json"; var fs = File.Create(fileName); var ms = new MemoryStream(); var writer = new StreamWriter(ms); @@ -1031,6 +1038,8 @@ private async Task CheckStartupFinishAsync(ApplicationCommandsExtension sender, GuildsWithoutScope = s_missingScopeGuildIdsGlobal }).ConfigureAwait(false); FinishFired = true; + if (Configuration.GenerateTranslationFilesOnly) + Environment.Exit(0); } args.Handled = false; diff --git a/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuCooldownAttribute.cs b/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuCooldownAttribute.cs index b4a1d9052..7c62db5aa 100644 --- a/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuCooldownAttribute.cs +++ b/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuCooldownAttribute.cs @@ -13,36 +13,29 @@ namespace DisCatSharp.ApplicationCommands.Attributes; /// /// Defines a cooldown for this command. This allows you to define how many times can users execute a specific command /// +/// +/// 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, member, channel, and/or globally. [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = false)] -public sealed class ContextMenuCooldownAttribute : ApplicationCommandCheckBaseAttribute, ICooldown +public sealed class ContextMenuCooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType) : ApplicationCommandCheckBaseAttribute, ICooldown { /// /// Gets the maximum number of uses before this command triggers a cooldown for its bucket. /// - public int MaxUses { get; } + public int MaxUses { get; } = maxUses; /// /// Gets the time after which the cooldown is reset. /// - public TimeSpan Reset { get; } + public TimeSpan Reset { get; } = TimeSpan.FromSeconds(resetAfter); /// /// Gets the type of the cooldown bucket. This determines how cooldowns are applied. /// - public CooldownBucketType BucketType { get; } - - /// - /// 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, member, channel, and/or globally. - public ContextMenuCooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType) - { - this.MaxUses = maxUses; - this.Reset = TimeSpan.FromSeconds(resetAfter); - this.BucketType = bucketType; - } + public CooldownBucketType BucketType { get; } = bucketType; /// /// Gets a cooldown bucket for given command context. diff --git a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs index ec60d6062..5ec4917ca 100644 --- a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs +++ b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs @@ -13,36 +13,29 @@ namespace DisCatSharp.ApplicationCommands.Attributes; /// /// Defines a cooldown for this command. This allows you to define how many times can users execute a specific command. /// +/// +/// 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, member, channel, and/or globally. [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = false)] -public sealed class SlashCommandCooldownAttribute : ApplicationCommandCheckBaseAttribute, ICooldown +public sealed class SlashCommandCooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType) : ApplicationCommandCheckBaseAttribute, ICooldown { /// /// Gets the maximum number of uses before this command triggers a cooldown for its bucket. /// - public int MaxUses { get; } + public int MaxUses { get; } = maxUses; /// /// Gets the time after which the cooldown is reset. /// - public TimeSpan Reset { get; } + public TimeSpan Reset { get; } = TimeSpan.FromSeconds(resetAfter); /// /// Gets the type of the cooldown bucket. This determines how cooldowns are applied. /// - public CooldownBucketType BucketType { get; } - - /// - /// 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, member, channel, and/or globally. - public SlashCommandCooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType) - { - this.MaxUses = maxUses; - this.Reset = TimeSpan.FromSeconds(resetAfter); - this.BucketType = bucketType; - } + public CooldownBucketType BucketType { get; } = bucketType; /// /// Gets a cooldown bucket for given command context. diff --git a/DisCatSharp.ApplicationCommands/Entities/ChoiceTranslator.cs b/DisCatSharp.ApplicationCommands/Entities/ChoiceTranslator.cs index faa879592..14f99ad00 100644 --- a/DisCatSharp.ApplicationCommands/Entities/ChoiceTranslator.cs +++ b/DisCatSharp.ApplicationCommands/Entities/ChoiceTranslator.cs @@ -21,9 +21,20 @@ internal sealed class ChoiceTranslator /// Gets the choice name translations. /// [JsonProperty("name_translations")] - internal Dictionary NameTranslationsDictionary { get; set; } + internal Dictionary? NameTranslationsDictionary { get; set; } [JsonIgnore] - public DiscordApplicationCommandLocalization NameTranslations - => new(this.NameTranslationsDictionary); + public DiscordApplicationCommandLocalization? NameTranslations + => this.NameTranslationsDictionary is not null ? new(this.NameTranslationsDictionary) : null; + + internal static ChoiceTranslator FromApplicationCommandChoice(DiscordApplicationCommandOptionChoice option) + { + var translator = new ChoiceTranslator + { + Name = option.Name, + NameTranslationsDictionary = option.RawNameLocalizations + }; + + return translator; + } } diff --git a/DisCatSharp.ApplicationCommands/Entities/CommandTranslator.cs b/DisCatSharp.ApplicationCommands/Entities/CommandTranslator.cs index 4093f63f7..93f73f017 100644 --- a/DisCatSharp.ApplicationCommands/Entities/CommandTranslator.cs +++ b/DisCatSharp.ApplicationCommands/Entities/CommandTranslator.cs @@ -35,25 +35,25 @@ internal sealed class CommandTranslator /// Gets the command name translations. /// [JsonProperty("name_translations")] - internal Dictionary NameTranslationDictionary { get; set; } + internal Dictionary? NameTranslationsDictionary { get; set; } [JsonIgnore] - public DiscordApplicationCommandLocalization NameTranslations - => new(this.NameTranslationDictionary); + public DiscordApplicationCommandLocalization? NameTranslations + => this.NameTranslationsDictionary is not null ? new(this.NameTranslationsDictionary) : null; /// /// Gets the command description translations. /// [JsonProperty("description_translations")] - internal Dictionary DescriptionTranslationDictionary { get; set; } + internal Dictionary? DescriptionTranslationsDictionary { get; set; } [JsonIgnore] - public DiscordApplicationCommandLocalization DescriptionTranslations - => new(this.DescriptionTranslationDictionary); + public DiscordApplicationCommandLocalization? DescriptionTranslations + => this.DescriptionTranslationsDictionary is not null ? new(this.DescriptionTranslationsDictionary) : null; /// /// Gets the option translators, if applicable. /// [JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)] - public List Options { get; set; } + public List? Options { get; set; } } diff --git a/DisCatSharp.ApplicationCommands/Entities/FakeApplicationCommandObjects.cs b/DisCatSharp.ApplicationCommands/Entities/FakeApplicationCommandObjects.cs index f2ca93afb..41dd1d948 100644 --- a/DisCatSharp.ApplicationCommands/Entities/FakeApplicationCommandObjects.cs +++ b/DisCatSharp.ApplicationCommands/Entities/FakeApplicationCommandObjects.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using DisCatSharp.Entities; using DisCatSharp.Enums; @@ -12,10 +13,14 @@ internal sealed class CommandGroupWithSubGroups : BaseCommand [JsonProperty("groups")] internal List SubGroups { get; set; } - internal CommandGroupWithSubGroups(string name, string description, List commands, ApplicationCommandType type) - : base(name, description, type) + [JsonProperty("commands")] + internal List Commands { get; set; } + + internal CommandGroupWithSubGroups(string name, string description, List subGroups, List commands, ApplicationCommandType type, Dictionary? nameTranslations = null, Dictionary? descriptionTranslations = null) + : base(name, description, type, nameTranslations, descriptionTranslations) { - this.SubGroups = commands; + this.SubGroups = subGroups; + this.Commands = commands; } } @@ -24,8 +29,8 @@ internal sealed class CommandGroup : BaseCommand [JsonProperty("commands")] internal List Commands { get; set; } - internal CommandGroup(string name, string description, List commands, ApplicationCommandType? type = null) - : base(name, description, type) + internal CommandGroup(string name, string description, List commands, ApplicationCommandType? type = null, Dictionary? nameTranslations = null, Dictionary? descriptionTranslations = null) + : base(name, description, type, nameTranslations, descriptionTranslations) { this.Commands = commands; } @@ -34,12 +39,13 @@ internal CommandGroup(string name, string description, List commands, A internal sealed class Command : BaseCommand { [JsonProperty("options")] - internal List? Options { get; set; } + internal List? Options { get; set; } - internal Command(string name, string? description = null, List? options = null, ApplicationCommandType? type = null) - : base(name, description, type) + internal Command(string name, string? description = null, List? options = null, ApplicationCommandType? type = null, Dictionary? nameTranslations = null, Dictionary? descriptionTranslations = null) + : base(name, description, type, nameTranslations, descriptionTranslations) { - this.Options = options; + if (options is not null) + this.Options = options.Select(OptionTranslator.FromApplicationCommandOption).ToList(); } } @@ -48,16 +54,24 @@ internal class BaseCommand [JsonProperty("name")] internal string Name { get; set; } + [JsonProperty("name_translations")] + internal Dictionary? NameTranslations { get; set; } + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] internal string? Description { get; set; } + [JsonProperty("description_translations", NullValueHandling = NullValueHandling.Ignore)] + internal Dictionary? DescriptionTranslations { get; set; } + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] internal ApplicationCommandType? Type { get; set; } - internal BaseCommand(string name, string? description = null, ApplicationCommandType? type = null) + internal BaseCommand(string name, string? description = null, ApplicationCommandType? type = null, Dictionary? nameTranslations = null, Dictionary? descriptionTranslations = null) { this.Name = name; this.Type = type; this.Description = description; + this.NameTranslations = nameTranslations; + this.DescriptionTranslations = descriptionTranslations; } } diff --git a/DisCatSharp.ApplicationCommands/Entities/GroupTranslator.cs b/DisCatSharp.ApplicationCommands/Entities/GroupTranslator.cs index beaafa193..de6b8a0fe 100644 --- a/DisCatSharp.ApplicationCommands/Entities/GroupTranslator.cs +++ b/DisCatSharp.ApplicationCommands/Entities/GroupTranslator.cs @@ -35,21 +35,21 @@ internal sealed class GroupTranslator /// Gets the group name translations. /// [JsonProperty("name_translations")] - internal Dictionary NameTranslationsDictionary { get; set; } + internal Dictionary? NameTranslationsDictionary { get; set; } [JsonIgnore] - public DiscordApplicationCommandLocalization NameTranslations - => new(this.NameTranslationsDictionary); + public DiscordApplicationCommandLocalization? NameTranslations + => this.NameTranslationsDictionary is not null ? new(this.NameTranslationsDictionary) : null; /// /// Gets the group description translations. /// [JsonProperty("description_translations")] - internal Dictionary DescriptionTranslationsDictionary { get; set; } + internal Dictionary? DescriptionTranslationsDictionary { get; set; } [JsonIgnore] - public DiscordApplicationCommandLocalization DescriptionTranslations - => new(this.DescriptionTranslationsDictionary); + public DiscordApplicationCommandLocalization? DescriptionTranslations + => this.DescriptionTranslationsDictionary is not null ? new(this.DescriptionTranslationsDictionary) : null; /// /// Gets the sub group translators, if applicable. diff --git a/DisCatSharp.ApplicationCommands/Entities/OptionTranslator.cs b/DisCatSharp.ApplicationCommands/Entities/OptionTranslator.cs index 5380ed205..0b786d0bc 100644 --- a/DisCatSharp.ApplicationCommands/Entities/OptionTranslator.cs +++ b/DisCatSharp.ApplicationCommands/Entities/OptionTranslator.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using DisCatSharp.Entities; using DisCatSharp.Enums; @@ -28,31 +29,48 @@ internal sealed class OptionTranslator /// Gets the option type /// [JsonProperty("type")] - public ApplicationCommandOptionType? Type { get; set; } + public ApplicationCommandOptionType Type { get; set; } /// /// Gets the option name translations. /// [JsonProperty("name_translations")] - internal Dictionary NameTranslationsDictionary { get; set; } + internal Dictionary? NameTranslationsDictionary { get; set; } [JsonIgnore] - public DiscordApplicationCommandLocalization NameTranslations - => new(this.NameTranslationsDictionary); + public DiscordApplicationCommandLocalization? NameTranslations + => this.NameTranslationsDictionary is not null ? new(this.NameTranslationsDictionary) : null; /// /// Gets the option description translations. /// [JsonProperty("description_translations")] - internal Dictionary DescriptionTranslationsDictionary { get; set; } + internal Dictionary? DescriptionTranslationsDictionary { get; set; } [JsonIgnore] - public DiscordApplicationCommandLocalization DescriptionTranslations - => new(this.DescriptionTranslationsDictionary); + public DiscordApplicationCommandLocalization? DescriptionTranslations + => this.DescriptionTranslationsDictionary is not null ? new(this.DescriptionTranslationsDictionary) : null; /// /// Gets the choice translators, if applicable. /// [JsonProperty("choices", NullValueHandling = NullValueHandling.Ignore)] - public List Choices { get; set; } + public List? Choices { get; set; } + + internal static OptionTranslator FromApplicationCommandOption(DiscordApplicationCommandOption option) + { + var optionTranslator = new OptionTranslator + { + Name = option.Name, + Description = option.Description, + Type = option.Type, + NameTranslationsDictionary = option.RawNameLocalizations, + DescriptionTranslationsDictionary = option.RawDescriptionLocalizations + }; + + if (option.Choices is not null) + optionTranslator.Choices = option.Choices.Select(ChoiceTranslator.FromApplicationCommandChoice).ToList(); + + return optionTranslator; + } } diff --git a/DisCatSharp.ApplicationCommands/Entities/SubGroupTranslator.cs b/DisCatSharp.ApplicationCommands/Entities/SubGroupTranslator.cs index 48a108f0e..70856841e 100644 --- a/DisCatSharp.ApplicationCommands/Entities/SubGroupTranslator.cs +++ b/DisCatSharp.ApplicationCommands/Entities/SubGroupTranslator.cs @@ -27,21 +27,21 @@ internal sealed class SubGroupTranslator /// Gets the sub group name translations. /// [JsonProperty("name_translations")] - internal Dictionary NameTranslationsDictionary { get; set; } + internal Dictionary? NameTranslationsDictionary { get; set; } [JsonIgnore] - public DiscordApplicationCommandLocalization NameTranslations - => new(this.NameTranslationsDictionary); + public DiscordApplicationCommandLocalization? NameTranslations + => this.NameTranslationsDictionary is not null ? new(this.NameTranslationsDictionary) : null; /// /// Gets the sub group description translations. /// [JsonProperty("description_translations")] - internal Dictionary DescriptionTranslationsDictionary { get; set; } + internal Dictionary? DescriptionTranslationsDictionary { get; set; } [JsonIgnore] - public DiscordApplicationCommandLocalization DescriptionTranslations - => new(this.DescriptionTranslationsDictionary); + public DiscordApplicationCommandLocalization? DescriptionTranslations + => this.DescriptionTranslationsDictionary is not null ? new(this.DescriptionTranslationsDictionary) : null; /// /// Gets the command translators. diff --git a/DisCatSharp.ApplicationCommands/GlobalSuppressions.cs b/DisCatSharp.ApplicationCommands/GlobalSuppressions.cs index e523816e8..b2219dfa7 100644 --- a/DisCatSharp.ApplicationCommands/GlobalSuppressions.cs +++ b/DisCatSharp.ApplicationCommands/GlobalSuppressions.cs @@ -35,9 +35,7 @@ [assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.Attributes.ChannelTypesAttribute.#ctor(DisCatSharp.Enums.ChannelType[])")] [assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.Attributes.ChoiceNameAttribute.#ctor(System.String)")] [assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.Attributes.ChoiceProviderAttribute.#ctor(System.Type)")] -[assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.Attributes.ContextMenuCooldownAttribute.#ctor(System.Int32,System.Double,DisCatSharp.ApplicationCommands.Enums.CooldownBucketType)")] [assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.Attributes.RequireAnyPermissionsAttribute.#ctor(DisCatSharp.Enums.Permissions[])")] -[assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.Attributes.SlashCommandCooldownAttribute.#ctor(System.Int32,System.Double,DisCatSharp.ApplicationCommands.Enums.CooldownBucketType)")] [assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.EventArgs.ContextMenuErrorEventArgs.#ctor(System.IServiceProvider)")] [assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.EventArgs.ContextMenuExecutedEventArgs.#ctor(System.IServiceProvider)")] [assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.EventArgs.SlashCommandErrorEventArgs.#ctor(System.IServiceProvider)")] diff --git a/DisCatSharp.CommandsNext/Attributes/CooldownAttribute.cs b/DisCatSharp.CommandsNext/Attributes/CooldownAttribute.cs index 3bcd913e5..90990f053 100644 --- a/DisCatSharp.CommandsNext/Attributes/CooldownAttribute.cs +++ b/DisCatSharp.CommandsNext/Attributes/CooldownAttribute.cs @@ -11,36 +11,29 @@ namespace DisCatSharp.CommandsNext.Attributes; /// /// Defines a cooldown for this command. This allows you to define how many times can users execute a specific command /// +/// +/// 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. [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = false)] -public sealed class CooldownAttribute : CheckBaseAttribute, ICooldown +public sealed class CooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType) : CheckBaseAttribute, ICooldown { /// /// Gets the maximum number of uses before this command triggers a cooldown for its bucket. /// - public int MaxUses { get; } + public int MaxUses { get; } = maxUses; /// /// Gets the time after which the cooldown is reset. /// - public TimeSpan Reset { get; } + public TimeSpan Reset { get; } = TimeSpan.FromSeconds(resetAfter); /// /// Gets the type of the cooldown bucket. This determines how cooldowns are applied. /// - public CooldownBucketType BucketType { get; } - - /// - /// 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 CooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType) - { - this.MaxUses = maxUses; - this.Reset = TimeSpan.FromSeconds(resetAfter); - this.BucketType = bucketType; - } + public CooldownBucketType BucketType { get; } = bucketType; /// /// Gets a cooldown bucket for given command context. diff --git a/DisCatSharp.CommandsNext/GlobalSuppressions.cs b/DisCatSharp.CommandsNext/GlobalSuppressions.cs index 21e46756f..0c4423d19 100644 --- a/DisCatSharp.CommandsNext/GlobalSuppressions.cs +++ b/DisCatSharp.CommandsNext/GlobalSuppressions.cs @@ -15,7 +15,6 @@ [assembly: SuppressMessage("Performance", "CA1860:Avoid using 'Enumerable.Any()' extension method", Justification = "", Scope = "member", Target = "~M:DisCatSharp.CommandsNext.Attributes.RequirePrefixesAttribute.#ctor(System.String[])")] [assembly: SuppressMessage("Performance", "CA1860:Avoid using 'Enumerable.Any()' extension method", Justification = "", Scope = "member", Target = "~M:DisCatSharp.CommandsNext.CommandsNextExtension.HandleCommandsAsync(DisCatSharp.DiscordClient,DisCatSharp.EventArgs.MessageCreateEventArgs)~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Performance", "CA1862:Use the 'StringComparison' method overloads to perform case-insensitive string comparisons", Justification = "", Scope = "member", Target = "~M:DisCatSharp.CommandsNext.CommandsNextExtension.FindCommand(System.String,System.String@)~DisCatSharp.CommandsNext.Command")] -[assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.CommandsNext.Attributes.CooldownAttribute.#ctor(System.Int32,System.Double,DisCatSharp.CommandsNext.Attributes.CooldownBucketType)")] [assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.CommandsNext.Attributes.DescriptionAttribute.#ctor(System.String)")] [assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.CommandsNext.Attributes.ModuleLifespanAttribute.#ctor(DisCatSharp.CommandsNext.Attributes.ModuleLifespan)")] [assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.CommandsNext.Attributes.PriorityAttribute.#ctor(System.Int32)")] diff --git a/DisCatSharp/Entities/Application/DiscordApplicationCommandLocalization.cs b/DisCatSharp/Entities/Application/DiscordApplicationCommandLocalization.cs index 3475fbba8..c35213380 100644 --- a/DisCatSharp/Entities/Application/DiscordApplicationCommandLocalization.cs +++ b/DisCatSharp/Entities/Application/DiscordApplicationCommandLocalization.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; namespace DisCatSharp.Entities; @@ -16,7 +17,7 @@ public sealed class DiscordApplicationCommandLocalization /// /// Gets valid [locales](xref:modules_application_commands_translations_reference#valid-locales) for Discord. /// - internal List ValidLocales = ["ru", "fi", "hr", "de", "hu", "sv-SE", "cs", "fr", "it", "en-GB", "pt-BR", "ja", "tr", "en-US", "es-ES", "uk", "hi", "th", "el", "no", "ro", "ko", "zh-TW", "vi", "zh-CN", "pl", "bg", "da", "nl", "lt"]; + internal readonly List ValidLocales = ["ru", "fi", "hr", "de", "hu", "sv-SE", "cs", "fr", "it", "en-GB", "pt-BR", "ja", "tr", "en-US", "es-ES", "uk", "hi", "th", "el", "no", "ro", "ko", "zh-TW", "vi", "zh-CN", "pl", "bg", "da", "nl", "lt"]; /// /// Adds a localization. @@ -49,13 +50,14 @@ public DiscordApplicationCommandLocalization() /// Initializes a new instance of . /// /// Localizations. - public DiscordApplicationCommandLocalization(Dictionary localizations) + public DiscordApplicationCommandLocalization(Dictionary? localizations) { - if (localizations != null) - foreach (var locale in localizations.Keys) - if (!this.Validate(locale)) - throw new NotSupportedException($"The provided locale \"{locale}\" is not valid for Discord.\n" + - $"Valid locales: {string.Join(", ", this.ValidLocales)}"); + if (localizations == null) + return; + + foreach (var locale in localizations.Keys.Where(locale => !this.Validate(locale))) + throw new NotSupportedException($"The provided locale \"{locale}\" is not valid for Discord.\n" + + $"Valid locales: {string.Join(", ", this.ValidLocales)}"); this.Localizations = localizations; } diff --git a/DisCatSharp/Entities/Application/DiscordApplicationCommandOptionChoice.cs b/DisCatSharp/Entities/Application/DiscordApplicationCommandOptionChoice.cs index 3c5f41e5a..852e7ff42 100644 --- a/DisCatSharp/Entities/Application/DiscordApplicationCommandOptionChoice.cs +++ b/DisCatSharp/Entities/Application/DiscordApplicationCommandOptionChoice.cs @@ -20,7 +20,7 @@ public sealed class DiscordApplicationCommandOptionChoice /// Sets the name localizations. /// [JsonProperty("name_localizations", NullValueHandling = NullValueHandling.Ignore)] - internal Dictionary RawNameLocalizations { get; set; } + internal Dictionary? RawNameLocalizations { get; set; } /// /// Gets the name localizations. @@ -43,7 +43,7 @@ public DiscordApplicationCommandLocalization NameLocalizations /// The localizations of the parameter choice name. public DiscordApplicationCommandOptionChoice(string name, object value, DiscordApplicationCommandLocalization nameLocalizations = null) { - if (!(value is string || value is long || value is int || value is double)) + if (value is not (string or long or int or double)) throw new InvalidOperationException($"Only {typeof(string)}, {typeof(long)}, {typeof(double)} or {typeof(int)} types may be passed to a command option choice."); if (name.Length > 100) diff --git a/DisCatSharp/Entities/Core/ICooldown.cs b/DisCatSharp/Entities/Core/ICooldown.cs index 0b9c7dde8..a73a9ca79 100644 --- a/DisCatSharp/Entities/Core/ICooldown.cs +++ b/DisCatSharp/Entities/Core/ICooldown.cs @@ -47,6 +47,7 @@ public interface ICooldown /// Responds to a ratelimit hit. /// /// The command context. - /// Whether the ratelimit wasn't it. + /// Whether the ratelimit wasn't hit. + /// The cooldown bucket. Task RespondRatelimitHitAsync(TContextType ctx, bool noHit, CooldownBucket bucket); } diff --git a/DisCatSharp/Entities/Interaction/DiscordInteraction.cs b/DisCatSharp/Entities/Interaction/DiscordInteraction.cs index 1cb382256..e5d3c1fb1 100644 --- a/DisCatSharp/Entities/Interaction/DiscordInteraction.cs +++ b/DisCatSharp/Entities/Interaction/DiscordInteraction.cs @@ -134,6 +134,12 @@ public DiscordChannel Channel [JsonProperty("authorizing_integration_owners", NullValueHandling = NullValueHandling.Ignore)] public AuthorizingIntegrationOwners? AuthorizingIntegrationOwners { get; internal set; } + /// + /// Gets the interaction's calling context. + /// + [JsonProperty("context", NullValueHandling = NullValueHandling.Ignore)] + public ApplicationCommandContexts Context { get; internal set; } + /// /// Creates a response to this interaction. /// diff --git a/DisCatSharp/GlobalSuppressions.cs b/DisCatSharp/GlobalSuppressions.cs index 39440d9bd..bd0eb64b2 100644 --- a/DisCatSharp/GlobalSuppressions.cs +++ b/DisCatSharp/GlobalSuppressions.cs @@ -26,7 +26,6 @@ [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordClient.WsSendAsync(System.String)~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordShardedClient.EventErrorHandler``1(DisCatSharp.Common.Utilities.AsyncEvent{DisCatSharp.DiscordClient,``0},System.Exception,DisCatSharp.Common.Utilities.AsyncEventHandler{DisCatSharp.DiscordClient,``0},DisCatSharp.DiscordClient,``0)")] [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.RestClient.BuildRequest(DisCatSharp.Net.BaseRestRequest)~System.Net.Http.HttpRequestMessage")] -[assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.RestClient.ExecuteRequestAsync(DisCatSharp.Net.BaseRestRequest,DisCatSharp.Net.RateLimitBucket,System.Threading.Tasks.TaskCompletionSource{System.Boolean})~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordClient.OnEmbeddedActivityUpdateAsync(Newtonsoft.Json.Linq.JObject,DisCatSharp.Entities.DiscordGuild,System.UInt64,Newtonsoft.Json.Linq.JArray,System.UInt64)~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.Abstractions.GamePartySizeConverter.ReadArrayObject(Newtonsoft.Json.JsonReader,Newtonsoft.Json.JsonSerializer)~Newtonsoft.Json.Linq.JArray")] [assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.Abstractions.ShardInfoConverter.ReadArrayObject(Newtonsoft.Json.JsonReader,Newtonsoft.Json.JsonSerializer)~Newtonsoft.Json.Linq.JArray")] @@ -43,11 +42,9 @@ [assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordClient.Goof``2(DisCatSharp.Common.Utilities.AsyncEvent{``0,``1},System.Exception,DisCatSharp.Common.Utilities.AsyncEventHandler{``0,``1},``0,``1)")] [assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordClient.ConnectAsync(DisCatSharp.Entities.DiscordActivity,System.Nullable{DisCatSharp.Entities.UserStatus},System.Nullable{System.DateTimeOffset})~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.RestClient.CleanupBucketsAsync~System.Threading.Tasks.Task")] -[assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.RestClient.ExecuteRequestAsync(DisCatSharp.Net.BaseRestRequest,DisCatSharp.Net.RateLimitBucket,System.Threading.Tasks.TaskCompletionSource{System.Boolean})~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.RestClient.UpdateHashCaches(DisCatSharp.Net.BaseRestRequest,DisCatSharp.Net.RateLimitBucket,System.String)")] [assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.RestClient.WaitForInitialRateLimit(DisCatSharp.Net.RateLimitBucket)~System.Threading.Tasks.Task{System.Threading.Tasks.TaskCompletionSource{System.Boolean}}")] [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordShardedClient.GetGatewayInfoAsync~System.Threading.Tasks.Task{DisCatSharp.Net.GatewayInfo}")] -[assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Entities.DisCatSharpTeam.Get(System.Net.Http.HttpClient,Microsoft.Extensions.Logging.ILogger,DisCatSharp.Net.DiscordApiClient)~System.Threading.Tasks.Task{DisCatSharp.Entities.DisCatSharpTeam}")] [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordShardedClient.StartAsync~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordShardedClient.ConnectShardAsync(System.Int32)~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordShardedClient.EventErrorHandler``1(DisCatSharp.Common.Utilities.AsyncEvent{DisCatSharp.DiscordClient,``0},System.Exception,DisCatSharp.Common.Utilities.AsyncEventHandler{DisCatSharp.DiscordClient,``0},DisCatSharp.DiscordClient,``0)")] @@ -102,13 +99,6 @@ [assembly: SuppressMessage("Performance", "CA1860:Avoid using 'Enumerable.Any()' extension method", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.DiscordApiClient.CreateMessageAsync(System.UInt64,DisCatSharp.Entities.DiscordMessageBuilder)~System.Threading.Tasks.Task{DisCatSharp.Entities.DiscordMessage}")] [assembly: SuppressMessage("Performance", "CA1862:Use the 'StringComparison' method overloads to perform case-insensitive string comparisons", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.RestClient.UpdateBucket(DisCatSharp.Net.BaseRestRequest,DisCatSharp.Net.RestResponse,System.Threading.Tasks.TaskCompletionSource{System.Boolean})")] [assembly: SuppressMessage("Performance", "CA1862:Use the 'StringComparison' method overloads to perform case-insensitive string comparisons", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.RestClient.Handle429(DisCatSharp.Net.RestResponse,System.Threading.Tasks.Task@,System.Boolean@)")] - -/* Unmerged change from project 'DisCatSharp (net8.0)' -Added: -[assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Entities.RoleMention.#ctor(System.UInt64)")] -[assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Entities.UserMention.#ctor(System.UInt64)")] -*/ -[assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Ascii.ImageSharpImageSource.#ctor(SixLabors.ImageSharp.Image{SixLabors.ImageSharp.PixelFormats.Rgba32})")] [assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.CompositeDefaultLogger.#ctor(System.Collections.Generic.IEnumerable{Microsoft.Extensions.Logging.ILoggerProvider})")] [assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Entities.DiscordApplicationRoleConnectionMetadata.#ctor(DisCatSharp.Enums.ApplicationRoleConnectionMetadataType,System.String,System.String,System.String,DisCatSharp.Entities.DiscordApplicationCommandLocalization,DisCatSharp.Entities.DiscordApplicationCommandLocalization)")] [assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Entities.ForumReactionEmoji.#ctor(System.Nullable{System.UInt64},System.String)")] diff --git a/DisCatSharp/Net/Rest/DiscordApiClient.cs b/DisCatSharp/Net/Rest/DiscordApiClient.cs index ca6f72df9..db031457c 100644 --- a/DisCatSharp/Net/Rest/DiscordApiClient.cs +++ b/DisCatSharp/Net/Rest/DiscordApiClient.cs @@ -4672,7 +4672,7 @@ internal async Task ExecuteWebhookAsync(ulong webhookId, string }; if (builder.Mentions != null) - pld.Mentions = new(builder.Mentions, builder.Mentions.Any()); + pld.Mentions = new(builder.Mentions, builder.Mentions.Count is not 0); if (builder.Files?.Count > 0) { @@ -6594,7 +6594,7 @@ internal async Task CreateFollowupMessageAsync(ulong application } if (builder.Mentions != null) - pld.Mentions = new(builder.Mentions, builder.Mentions.Any()); + pld.Mentions = new(builder.Mentions, builder.Mentions.Count is not 0); if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count > 0 || builder.IsTts == true || builder.Mentions != null || builder.Files?.Count > 0 || builder.Components?.Count > 0) values["payload_json"] = DiscordJson.SerializeObject(pld); From 042abe042bf01564d1df7c56a7f5ff085ecbe16a Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Wed, 17 Jan 2024 02:02:01 +0100 Subject: [PATCH 06/14] feat: add by discord added allowed locales --- .../translations/reference.md | 103 +++++++++--------- .../translations/using.md | 6 +- .../DiscordApplicationCommandLocalization.cs | 2 +- 3 files changed, 57 insertions(+), 54 deletions(-) diff --git a/DisCatSharp.Docs/articles/modules/application_commands/translations/reference.md b/DisCatSharp.Docs/articles/modules/application_commands/translations/reference.md index 91c08e74e..3e3add573 100644 --- a/DisCatSharp.Docs/articles/modules/application_commands/translations/reference.md +++ b/DisCatSharp.Docs/articles/modules/application_commands/translations/reference.md @@ -10,14 +10,14 @@ title: Translation Reference ## Command Object -| Key | Value | Description | -| ------------------------ | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | -| name | string | name of the application command | -| description? | string | description of the application command | -| type | int | [type](#application-command-type) of application command, used to map command types | -| name_translations | array of [Translation KVPs](#translation-kvp) | array of translation key-value-pairs for the application command name | -| description_translations | array of [Translation KVPs](#translation-kvp) | array of translation key-value-pairs for the application command description, only valid for slash commands | -| options | array of [Option Objects](#option-object) | array of option objects containing translations | +| Key | Value | Description | +| ------------------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | +| name | string | name of the application command | +| description? | string | description of the application command | +| type | int | [type](#application-command-type) of application command, used to map command types, not valid for options | +| name_translations | array of [Translation KVPs](#translation-kvp) | array of translation key-value-pairs for the application command name | +| description_translations? | array of [Translation KVPs](#translation-kvp) | array of translation key-value-pairs for the application command description, only valid for slash commands | +| options | array of [Option Objects](#option-object) | array of option objects containing translations | ### Application Command Type @@ -29,21 +29,22 @@ title: Translation Reference ## Command Group Object -| Key | Value | Description | -| ------------------------ | --------------------------------------------------------------- | ---------------------------------------------------------------------------------- | -| name | string | name of the application command group | -| description? | string | description of the application command group | -| name_translations | array of [Translation KVPs](#translation-kvp) | array of translation key-value-pairs for the application command group name | -| description_translations | array of [Translation KVPs](#translation-kvp) | array of translation key-value-pairs for the application command group description | -| commands | array of [Command Objects](#command-object) | array of command objects containing translations | -| groups | array of [Sub Command Group Objects](#sub-command-group-object) | array of sub command group objects containing translations | +| Key | Value | Description | +| ------------------------ | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| name | string | name of the application command group | +| description | string | description of the application command group | +| type | int | [type](#application-command-type) of application command, used to map command types, not valid for options | +| name_translations | array of [Translation KVPs](#translation-kvp) | array of translation key-value-pairs for the application command group name | +| description_translations | array of [Translation KVPs](#translation-kvp) | array of translation key-value-pairs for the application command group description | +| commands | array of [Command Objects](#command-object) | array of command objects containing translations | +| groups | array of [Sub Command Group Objects](#sub-command-group-object) | array of sub command group objects containing translations | ## Sub Command Group Object | Key | Value | Description | | ------------------------ | --------------------------------------------- | -------------------------------------------------------------------------------------- | | name | string | name of the application command sub group | -| description? | string | description of the application command group | +| description | string | description of the application command sub group | | name_translations | array of [Translation KVPs](#translation-kvp) | array of translation key-value-pairs for the application command sub group name | | description_translations | array of [Translation KVPs](#translation-kvp) | array of translation key-value-pairs for the application command sub group description | | commands | array of [Command Objects](#command-object) | array of command objects containing translations | @@ -53,7 +54,7 @@ title: Translation Reference | Key | Value | Description | | ------------------------ | ------------------------------------------------------- | ----------------------------------------------------------------------------------- | | name | string | name of the application command option | -| description? | string | description of the application command group | +| description | string | description of the application command group | | name_translations | array of [Translation KVPs](#translation-kvp) | array of translation key-value-pairs for the application command option name | | description_translations | array of [Translation KVPs](#translation-kvp) | array of translation key-value-pairs for the application command option description | | choices | array of [Option Choice Objects](#option-choice-object) | array of option choice objects containing translations | @@ -80,35 +81,37 @@ A translation object is a key-value-pair of `"locale": "value"`. ## Valid Locales -| Locale | Language | -| ------ | --------------------- | -| da | Danish | -| de | German | -| en-GB | English, UK | -| en-US | English, US | -| es-ES | Spanish | -| fr | French | -| hr | Croatian | -| it | Italian | -| lt | Lithuanian | -| hu | Hungarian | -| nl | Dutch | -| no | Norwegian | -| pl | Polish | -| pt-BR | Portuguese, Brazilian | -| ro | Romanian, Romania | -| fi | Finnish | -| sv-SE | Swedish | -| vi | Vietnamese | -| tr | Turkish | -| cs | Czech | -| el | Greek | -| bg | Bulgarian | -| ru | Russian | -| uk | Ukrainian | -| hi | Hindi | -| th | Thai | -| zh-CN | Chinese, China | -| ja | Japanese | -| zh-TW | Chinese, Taiwan | -| ko | Korean | +| Locale | Language | +| ------ | ---------------------- | +| id | Indonesian | +| da | Danish | +| de | German | +| en-GB | English, UK | +| en-US | English, US | +| es-ES | Spanish | +| es-419 | Spanish, Latin America | +| fr | French | +| hr | Croatian | +| it | Italian | +| lt | Lithuanian | +| hu | Hungarian | +| nl | Dutch | +| no | Norwegian | +| pl | Polish | +| pt-BR | Portuguese, Brazilian | +| ro | Romanian, Romania | +| fi | Finnish | +| sv-SE | Swedish | +| vi | Vietnamese | +| tr | Turkish | +| cs | Czech | +| el | Greek | +| bg | Bulgarian | +| ru | Russian | +| uk | Ukrainian | +| hi | Hindi | +| th | Thai | +| zh-CN | Chinese, China | +| ja | Japanese | +| zh-TW | Chinese, Taiwan | +| ko | Korean | diff --git a/DisCatSharp.Docs/articles/modules/application_commands/translations/using.md b/DisCatSharp.Docs/articles/modules/application_commands/translations/using.md index 08eae63f4..0c7bbfd1f 100644 --- a/DisCatSharp.Docs/articles/modules/application_commands/translations/using.md +++ b/DisCatSharp.Docs/articles/modules/application_commands/translations/using.md @@ -79,6 +79,7 @@ A correct translation json for english and german would look like that: { "name": "my_command", "description": "This is description of the command group.", + "type": 1, "name_translations": { "en-US": "my_command", "de": "mein_befehl" @@ -87,11 +88,11 @@ A correct translation json for english and german would look like that: "en-US": "This is description of the command group.", "de": "Das ist die description der Befehl Gruppe." }, + "groups": [], "commands": [ { "name": "first", "description": "First", - "type": 1, // Type 1 for slash command "name_translations": { "en-US": "first", "de": "erste" @@ -104,7 +105,6 @@ A correct translation json for english and german would look like that: { "name": "second", "description": "Second", - "type": 1, // Type 1 for slash command "name_translations": { "en-US": "second", "de": "zweite" @@ -117,6 +117,7 @@ A correct translation json for english and german would look like that: { "name": "value", "description": "Some string value.", + "type": 3, "name_translations": { "en-US": "value", "de": "wert" @@ -182,7 +183,6 @@ A correct json for this example would look like that: }, { "name": "My Command", - "description": null, "type": 2, // Type 2 for user context menu command "name_translations": { "en-US": "My Command", diff --git a/DisCatSharp/Entities/Application/DiscordApplicationCommandLocalization.cs b/DisCatSharp/Entities/Application/DiscordApplicationCommandLocalization.cs index c35213380..d5a557672 100644 --- a/DisCatSharp/Entities/Application/DiscordApplicationCommandLocalization.cs +++ b/DisCatSharp/Entities/Application/DiscordApplicationCommandLocalization.cs @@ -17,7 +17,7 @@ public sealed class DiscordApplicationCommandLocalization /// /// Gets valid [locales](xref:modules_application_commands_translations_reference#valid-locales) for Discord. /// - internal readonly List ValidLocales = ["ru", "fi", "hr", "de", "hu", "sv-SE", "cs", "fr", "it", "en-GB", "pt-BR", "ja", "tr", "en-US", "es-ES", "uk", "hi", "th", "el", "no", "ro", "ko", "zh-TW", "vi", "zh-CN", "pl", "bg", "da", "nl", "lt"]; + internal readonly List ValidLocales = ["ru", "fi", "hr", "de", "hu", "sv-SE", "cs", "fr", "it", "en-GB", "pt-BR", "ja", "tr", "en-US", "es-ES", "uk", "hi", "th", "el", "no", "ro", "ko", "zh-TW", "vi", "zh-CN", "pl", "bg", "da", "nl", "lt", "id", "es-419"]; /// /// Adds a localization. From bd85f562b369c507f0f28d9275af5e9b7b4b2315 Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Wed, 17 Jan 2024 12:15:55 +0100 Subject: [PATCH 07/14] [ci skip] chore: update release notes --- RELEASENOTES.md | 69 +++++++++++-------------------------------------- 1 file changed, 15 insertions(+), 54 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b8f8f83e9..e0e77f8ba 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,105 +1,66 @@ DisCatSharp Release Notes Important fix: - - Apparently the built-in c# method for building uris broke. The gateway uri included never the gateway version, encoding and compression. This is fixed now! - - Breaking changes: - - Dropped support for .NET 6 - - Removed previously deprecated fields and methods + - Applying the proxy configuration to the sharded client startup (it was missing) Notable Changes - - Full support for onboarding - - Custom status support - - Full support for Application Subscriptions aka. Premium Apps - - DiscordOAuth2Client: Allows bots to request and use access tokens for the Discord API. - - Support for default select menu values (THANKS MAISY FOR ADDING IT TO DISCORD) - - DisCatSharp can now check for new releases on startup. Including support for extensions + - Added support for building and using cooldowns DisCatSharp.Attributes Release Notes - Breaking changes: - - Dropped support for .NET 6 - - Removed previously deprecated fields and methods - - Added new required feature enums to notate feature usage + None DisCatSharp.ApplicationCommands Release Notes - Important fix: - - Changed the timing when and how commands are registered to fix some issues - Breaking changes: - - Dropped support for .NET 6 - - Removed previously deprecated fields and methods + - Fixed problems with translation generation and usage - Contains a rework for command registration (Kinda wacky tho with translation-enabled commands) - Fixed a major issue with application commands. Upgrade mandatory + Notable changes + - Added cooldowns for application commands DisCatSharp.CommandsNext Release Notes - Breaking changes: - - Dropped support for .NET 6 - - Removed previously deprecated fields and methods + Notable changes: + - Fixed cooldowns DisCatSharp.Interactivity Release Notes - Breaking changes: - - Dropped support for .NET 6 - - Removed previously deprecated fields and methods - - Contains important bug fixes for interactions and pagination + None DisCatSharp.Common Release Notes - Breaking changes: - - Dropped support for .NET 6 - - Removed previously deprecated fields and methods - - We added all of our regexes to the Common package as GenereicRegexes + None DisCatSharp.Lavalink Release Notes - Breaking changes: - - Dropped support for .NET 6 - - Removed previously deprecated fields and methods - - Lavalink got a complete rework for V4. - - Visit the documentation for more information: https://docs.dcs.aitsys.dev/articles/modules/audio/lavalink_v4/intro + None DisCatSharp.VoiceNext Release Notes - Breaking changes: - - Dropped support for .NET 6 - - Removed previously deprecated fields and methods - Will be deprecated soon and replaced by DisCatSharp.Voice DisCatSharp.Experimental Release Notes - Breaking changes: - - Dropped support for .NET 6 + None DisCatSharp.Configuration Release Notes - Breaking changes: - - Dropped support for .NET 6 + None DisCatSharp.Hosting Release Notes - Breaking changes: - - Dropped support for .NET 6 + None DisCatSharp.Hosting.DependencyInjection Release Notes - Breaking changes: - - Dropped support for .NET 6 - + None From f6af29e9f5ad6cf4b85e1bb3cfc16ff3de66c4a5 Mon Sep 17 00:00:00 2001 From: Mira <56395159+TheXorog@users.noreply.github.com> Date: Wed, 17 Jan 2024 13:59:40 +0100 Subject: [PATCH 08/14] fix: fix registration of applicaiton commands when commands cleared or non existent in advance --- .../Workers/RegistrationWorker.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/DisCatSharp.ApplicationCommands/Workers/RegistrationWorker.cs b/DisCatSharp.ApplicationCommands/Workers/RegistrationWorker.cs index 91687126b..5166a2eb7 100644 --- a/DisCatSharp.ApplicationCommands/Workers/RegistrationWorker.cs +++ b/DisCatSharp.ApplicationCommands/Workers/RegistrationWorker.cs @@ -356,14 +356,13 @@ internal class RegistrationWorker /// private static List? BuildGuildCreateList(DiscordClient client, ulong guildId, List? updateList = null) { - if (ApplicationCommandsExtension.GuildDiscordCommands.Count is 0 - || updateList is null || !ApplicationCommandsExtension.GuildDiscordCommands!.TryGetFirstValueByKey(guildId, out var discord) - ) + if (updateList is null) return null; + var success = ApplicationCommandsExtension.GuildDiscordCommands.TryGetFirstValueByKey(guildId, out var discord); List newCommands = []; - if (discord is null) + if (!success || discord is null || discord.Count is 0) return updateList; newCommands.AddRange(updateList.Where(cmd => discord.All(d => d.Name != cmd.Name))); @@ -450,14 +449,14 @@ private static ( /// A list of commands. private static List? BuildGlobalCreateList(DiscordClient client, List? updateList = null) { - if (ApplicationCommandsExtension.GlobalDiscordCommands.Count is 0 || updateList is null) - return updateList; + if (updateList is null) + return null; var discord = ApplicationCommandsExtension.GlobalDiscordCommands; List newCommands = []; - if (discord is null) + if (discord is null || discord.Count is 0) return updateList; newCommands.AddRange(updateList.Where(cmd => discord.All(d => d.Name != cmd.Name))); From 5da2d94e87dee6e2966b9d67d816ca8d5df739df Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Wed, 17 Jan 2024 14:02:05 +0100 Subject: [PATCH 09/14] 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 0c8dc8f11..980c20c53 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 7c62db5aa..76a34f584 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 5ec4917ca..5a6d14efe 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 000000000..da4e14024 --- /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 90990f053..796dbec21 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 000000000..8465c45bb --- /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 c71ad1f8a..5ca8cfdd4 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)); From c6ada21c11b84ce98ff7d6acca6af2d2bba9e7f2 Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Wed, 17 Jan 2024 14:24:15 +0100 Subject: [PATCH 10/14] docs: fix space -> tab --- .../modules/application_commands/translations/using.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DisCatSharp.Docs/articles/modules/application_commands/translations/using.md b/DisCatSharp.Docs/articles/modules/application_commands/translations/using.md index 0c7bbfd1f..1fadf068f 100644 --- a/DisCatSharp.Docs/articles/modules/application_commands/translations/using.md +++ b/DisCatSharp.Docs/articles/modules/application_commands/translations/using.md @@ -79,7 +79,7 @@ A correct translation json for english and german would look like that: { "name": "my_command", "description": "This is description of the command group.", - "type": 1, + "type": 1, "name_translations": { "en-US": "my_command", "de": "mein_befehl" @@ -88,7 +88,7 @@ A correct translation json for english and german would look like that: "en-US": "This is description of the command group.", "de": "Das ist die description der Befehl Gruppe." }, - "groups": [], + "groups": [], "commands": [ { "name": "first", @@ -117,7 +117,7 @@ A correct translation json for english and german would look like that: { "name": "value", "description": "Some string value.", - "type": 3, + "type": 3, "name_translations": { "en-US": "value", "de": "wert" From c742a187fadb7cfe97fff7c765aad3de9fbbd09d Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Wed, 17 Jan 2024 14:27:10 +0100 Subject: [PATCH 11/14] Update RELEASENOTES.md --- RELEASENOTES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e0e77f8ba..d2ce6239d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -19,6 +19,7 @@ DisCatSharp.ApplicationCommands Release Notes Notable changes - Added cooldowns for application commands + - Fix first-time command registration DisCatSharp.CommandsNext Release Notes From ba9f46b71a732fbdc9b81ccd61b29e4c32600042 Mon Sep 17 00:00:00 2001 From: Mira <56395159+TheXorog@users.noreply.github.com> Date: Wed, 17 Jan 2024 14:43:06 +0100 Subject: [PATCH 12/14] fix: fix registration of translations for subcommands --- .../Workers/ApplicationCommandWorker.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DisCatSharp.ApplicationCommands/Workers/ApplicationCommandWorker.cs b/DisCatSharp.ApplicationCommands/Workers/ApplicationCommandWorker.cs index 0684a19d6..9df31568b 100644 --- a/DisCatSharp.ApplicationCommands/Workers/ApplicationCommandWorker.cs +++ b/DisCatSharp.ApplicationCommands/Workers/ApplicationCommandWorker.cs @@ -249,10 +249,10 @@ bool withLocalization )); } - subNameLocalizations = subCommandTranslation.NameTranslations; - subDescriptionLocalizations = subCommandTranslation.DescriptionTranslations; } + subNameLocalizations = subCommandTranslation.NameTranslations; + subDescriptionLocalizations = subCommandTranslation.DescriptionTranslations; //Creates the subcommand and adds it to the main command var subpayload = new DiscordApplicationCommandOption(commandAttribute.Name, commandAttribute.Description, ApplicationCommandOptionType.SubCommand, false, null, localizedOptions ?? options, nameLocalizations: subNameLocalizations, descriptionLocalizations: subDescriptionLocalizations); payload = new(payload.Name, payload.Description, payload.Options?.Append(subpayload) ?? new[] { subpayload }, nameLocalizations: payload.NameLocalizations, descriptionLocalizations: payload.DescriptionLocalizations, defaultMemberPermissions: payload.DefaultMemberPermissions, dmPermission: payload.DmPermission ?? true, isNsfw: payload.IsNsfw, allowedContexts: payload.AllowedContexts, integrationTypes: payload.IntegrationTypes); From c4a571ffa0c98e76999b1de7c65cbf993f740e52 Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Wed, 17 Jan 2024 14:47:43 +0100 Subject: [PATCH 13/14] fix: nre --- .../Workers/ApplicationCommandWorker.cs | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/DisCatSharp.ApplicationCommands/Workers/ApplicationCommandWorker.cs b/DisCatSharp.ApplicationCommands/Workers/ApplicationCommandWorker.cs index 9df31568b..08fbb8d29 100644 --- a/DisCatSharp.ApplicationCommands/Workers/ApplicationCommandWorker.cs +++ b/DisCatSharp.ApplicationCommands/Workers/ApplicationCommandWorker.cs @@ -233,26 +233,29 @@ bool withLocalization var commandTranslation = translator?.Single(c => c.Name == payload.Name); var subCommandTranslation = commandTranslation?.Commands?.Single(sc => sc.Name == commandAttribute.Name); - if (subCommandTranslation?.Options is not null) + if (subCommandTranslation is not null) { - localizedOptions = new(options.Count); - foreach (var option in options) + if (subCommandTranslation.Options is not null) { - var choices = option.Choices is not null ? new List(option.Choices.Count) : null; - if (option.Choices is not null && choices is not null) - choices.AddRange(option.Choices.Select(choice => new DiscordApplicationCommandOptionChoice(choice.Name, choice.Value, subCommandTranslation.Options.Single(o => o.Name == option.Name).Choices.Single(c => c.Name == choice.Name).NameTranslations))); - - localizedOptions.Add(new(option.Name, option.Description, option.Type, option.Required, - choices, option.Options, option.ChannelTypes, option.AutoComplete, option.MinimumValue, option.MaximumValue, - subCommandTranslation.Options.Single(o => o.Name == option.Name).NameTranslations, subCommandTranslation.Options.Single(o => o.Name == option.Name).DescriptionTranslations, - option.MinimumLength, option.MaximumLength - )); + localizedOptions = new(options.Count); + foreach (var option in options) + { + var choices = option.Choices is not null ? new List(option.Choices.Count) : null; + if (option.Choices is not null && choices is not null) + choices.AddRange(option.Choices.Select(choice => new DiscordApplicationCommandOptionChoice(choice.Name, choice.Value, subCommandTranslation.Options.Single(o => o.Name == option.Name).Choices.Single(c => c.Name == choice.Name).NameTranslations))); + + localizedOptions.Add(new(option.Name, option.Description, option.Type, option.Required, + choices, option.Options, option.ChannelTypes, option.AutoComplete, option.MinimumValue, option.MaximumValue, + subCommandTranslation.Options.Single(o => o.Name == option.Name).NameTranslations, subCommandTranslation.Options.Single(o => o.Name == option.Name).DescriptionTranslations, + option.MinimumLength, option.MaximumLength + )); + } } + subNameLocalizations = subCommandTranslation.NameTranslations; + subDescriptionLocalizations = subCommandTranslation.DescriptionTranslations; } - subNameLocalizations = subCommandTranslation.NameTranslations; - subDescriptionLocalizations = subCommandTranslation.DescriptionTranslations; //Creates the subcommand and adds it to the main command var subpayload = new DiscordApplicationCommandOption(commandAttribute.Name, commandAttribute.Description, ApplicationCommandOptionType.SubCommand, false, null, localizedOptions ?? options, nameLocalizations: subNameLocalizations, descriptionLocalizations: subDescriptionLocalizations); payload = new(payload.Name, payload.Description, payload.Options?.Append(subpayload) ?? new[] { subpayload }, nameLocalizations: payload.NameLocalizations, descriptionLocalizations: payload.DescriptionLocalizations, defaultMemberPermissions: payload.DefaultMemberPermissions, dmPermission: payload.DmPermission ?? true, isNsfw: payload.IsNsfw, allowedContexts: payload.AllowedContexts, integrationTypes: payload.IntegrationTypes); From a5f0d99f1fdb28d29efa7d9f20544451f1a03cfd Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Wed, 17 Jan 2024 14:55:39 +0100 Subject: [PATCH 14/14] chore: remove command grouping type --- .../ApplicationCommandsExtension.cs | 4 --- .../Context/BaseContext.cs | 5 ++-- .../Core/DisCatSharpCommandContext.cs | 9 +------ .../Core/DisCatSharpCommandGroupingType.cs | 27 ------------------- 4 files changed, 3 insertions(+), 42 deletions(-) delete mode 100644 DisCatSharp/Enums/Core/DisCatSharpCommandGroupingType.cs diff --git a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs index 980c20c53..f04ba3505 100644 --- a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs +++ b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs @@ -1121,7 +1121,6 @@ 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); @@ -1135,7 +1134,6 @@ 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); @@ -1151,7 +1149,6 @@ 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); @@ -1381,7 +1378,6 @@ private Task ContextMenuHandler(DiscordClient client, ContextMenuInteractionCrea AppPermissions = e.Interaction.AppPermissions, Entitlements = e.Interaction.Entitlements, 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, diff --git a/DisCatSharp.ApplicationCommands/Context/BaseContext.cs b/DisCatSharp.ApplicationCommands/Context/BaseContext.cs index 1c60ddd27..290544e3f 100644 --- a/DisCatSharp.ApplicationCommands/Context/BaseContext.cs +++ b/DisCatSharp.ApplicationCommands/Context/BaseContext.cs @@ -111,9 +111,8 @@ public override string FullCommandName /// Initializes a new instance of the class. /// /// The command type. - /// The command grouping type. - internal BaseContext(DisCatSharpCommandType type, DisCatSharpCommandGroupingType groupingType = DisCatSharpCommandGroupingType.Command) - : base(type, groupingType) + internal BaseContext(DisCatSharpCommandType type) + : base(type) { } /// diff --git a/DisCatSharp/Entities/Core/DisCatSharpCommandContext.cs b/DisCatSharp/Entities/Core/DisCatSharpCommandContext.cs index cc5300e6b..e01687974 100644 --- a/DisCatSharp/Entities/Core/DisCatSharpCommandContext.cs +++ b/DisCatSharp/Entities/Core/DisCatSharpCommandContext.cs @@ -62,19 +62,12 @@ public class DisCatSharpCommandContext /// public DisCatSharpCommandType CommandType { get; internal set; } - /// - /// Gets the command grouping type of the command. - /// - public DisCatSharpCommandGroupingType CommandGroupingType { get; internal set; } - /// /// Initializes a new instance of the class. /// /// The command type. - /// The command grouping type. - internal DisCatSharpCommandContext(DisCatSharpCommandType type, DisCatSharpCommandGroupingType groupingType = DisCatSharpCommandGroupingType.None) + internal DisCatSharpCommandContext(DisCatSharpCommandType type) { this.CommandType = type; - this.CommandGroupingType = groupingType; } } diff --git a/DisCatSharp/Enums/Core/DisCatSharpCommandGroupingType.cs b/DisCatSharp/Enums/Core/DisCatSharpCommandGroupingType.cs deleted file mode 100644 index 6debe497b..000000000 --- a/DisCatSharp/Enums/Core/DisCatSharpCommandGroupingType.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace DisCatSharp.Enums.Core; - -/// -/// Represents the grouping type of a command. -/// -public enum DisCatSharpCommandGroupingType -{ - /// - /// This is a special type and not a command. - /// - None, - - /// - /// The command is not part of a group. - /// - Command, - - /// - /// The command is a sub command. - /// - SubCommand, - - /// - /// The command is a sub group command. - /// - SubGroupCommand -}