From d09d89b1854f47103e150fcc8da27e562b5f12a8 Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Wed, 17 Jan 2024 15:05:28 +0100 Subject: [PATCH] feat: Implement cooldowns for application commands (#431) * feat: make cooldowns work feat: move cooldown to discatsharp base package chore: some experimental stuff fix: commandsnext cooldowns * [ci skip] chore: resharper disable for sealed on CooldownBucket * fix: try fixing translations (while i'm on it) * fix: really fix translation export * fix: fixed translations (while i'm on it) * feat: add by discord added allowed locales * [ci skip] chore: update release notes * fix: fix registration of applicaiton commands when commands cleared or non existent in advance * feat: custom cooldown responder * docs: fix space -> tab * Update RELEASENOTES.md * fix: fix registration of translations for subcommands * fix: nre * chore: remove command grouping type --------- Co-authored-by: Mira <56395159+TheXorog@users.noreply.github.com> --- .../ApplicationCommandsExtension.cs | 74 +++-- .../ContextMenuCooldownAttribute.cs | 115 ++++---- .../SlashCommandCooldownAttribute.cs | 118 ++++---- .../Context/BaseContext.cs | 38 +-- .../Context/ContextMenuContext.cs | 5 +- .../Context/InteractionContext.cs | 3 +- .../Entities/ChoiceTranslator.cs | 17 +- .../Entities/CommandTranslator.cs | 14 +- .../Entities/FakeApplicationCommandObjects.cs | 34 ++- .../Entities/GroupTranslator.cs | 12 +- .../Entities/ICooldownResponder.cs | 17 ++ .../Entities/OptionTranslator.cs | 34 ++- .../Entities/SubGroupTranslator.cs | 12 +- .../GlobalSuppressions.cs | 2 - .../Workers/ApplicationCommandWorker.cs | 27 +- .../Workers/RegistrationWorker.cs | 13 +- .../Attributes/CooldownAttribute.cs | 272 ++++-------------- .../CommandsNextExtension.cs | 21 +- .../Entities/CommandGroup.cs | 6 +- .../Entities/ICooldownResponder.cs | 16 ++ .../EventArgs/CommandContext.cs | 17 +- .../GlobalSuppressions.cs | 1 - .../translations/reference.md | 103 +++---- .../translations/using.md | 6 +- DisCatSharp/Clients/DiscordClient.cs | 8 + .../Application/DiscordApplicationCommand.cs | 25 +- .../DiscordApplicationCommandLocalization.cs | 16 +- .../DiscordApplicationCommandOptionChoice.cs | 4 +- .../Entities/Core}/CooldownBucket.cs | 65 +++-- .../Core/DisCatSharpCommandContext.cs | 73 +++++ .../Entities/Core}/IBucket.cs | 3 +- .../Entities/Core}/ICooldown.cs | 18 +- DisCatSharp/Entities/DCS/DisCatSharpTeam.cs | 183 ------------ .../Entities/DCS/DisCatSharpTeamMember.cs | 71 ----- DisCatSharp/Entities/DCS/GitHubRelease.cs | 165 ----------- .../Interaction/DiscordInteraction.cs | 6 + .../Enums/Core}/CooldownBucketType.cs | 16 +- .../Enums/Core/DisCatSharpCommandType.cs | 32 +++ DisCatSharp/GlobalSuppressions.cs | 10 - .../Rest/RestApplicationCommandPayloads.cs | 20 +- DisCatSharp/Net/Rest/DiscordApiClient.cs | 82 +++--- RELEASENOTES.md | 70 ++--- 42 files changed, 746 insertions(+), 1098 deletions(-) create mode 100644 DisCatSharp.ApplicationCommands/Entities/ICooldownResponder.cs create mode 100644 DisCatSharp.CommandsNext/Entities/ICooldownResponder.cs rename {DisCatSharp.ApplicationCommands/Entities => DisCatSharp/Entities/Core}/CooldownBucket.cs (58%) 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 (67%) 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/DisCatSharpCommandType.cs diff --git a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs index dab858f433..f04ba35050 100644 --- a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs +++ b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs @@ -13,11 +13,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; @@ -661,39 +661,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))!)); } } @@ -733,12 +731,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!); + } + } } } @@ -804,7 +810,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 { @@ -911,7 +917,7 @@ private async Task RegisterCommands(List RegisteredCommands = GlobalCommandsInternal }).ConfigureAwait(false); - await this.CheckRegistrationStartup(translation, groupTranslation); + await this.CheckRegistrationStartup(translation, groupTranslation, guildId); } catch (NullReferenceException ex) { @@ -965,7 +971,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) { @@ -973,7 +980,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); @@ -991,7 +998,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); @@ -1030,6 +1037,8 @@ private async Task CheckStartupFinishAsync(ApplicationCommandsExtension sender, GuildsWithoutScope = s_missingScopeGuildIdsGlobal }).ConfigureAwait(false); FinishFired = true; + if (Configuration.GenerateTranslationFilesOnly) + Environment.Exit(0); } args.Handled = false; @@ -1081,7 +1090,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 @@ -1340,7 +1353,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 +1377,11 @@ 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, + 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 c8371902fd..76a34f584f 100644 --- a/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuCooldownAttribute.cs +++ b/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuCooldownAttribute.cs @@ -1,63 +1,62 @@ using System; -using System.Collections.Concurrent; +using System.Collections.Generic; +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; + +using Sentry; 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. +/// The responder type used to respond to cooldown ratelimit hits. [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, Type? cooldownResponderType = null) : 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; } - - /// - /// Gets the cooldown buckets for this command. - /// - internal readonly ConcurrentDictionary Buckets; + public CooldownBucketType BucketType { get; } = bucketType; /// - /// 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. + /// Gets the responder type. /// - /// Number of times the command can be used before triggering a cooldown. - /// Number of seconds after which the cooldown is reset. - /// Type of cooldown bucket. This allows controlling whether the bucket will be cooled down per user, guild, channel, or globally. - public ContextMenuCooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType) - { - this.MaxUses = maxUses; - this.Reset = TimeSpan.FromSeconds(resetAfter); - this.BucketType = bucketType; - this.Buckets = new(); - } + public Type? ResponderType { get; } = cooldownResponderType; /// /// Gets a cooldown bucket for given command context. /// /// Command context to get cooldown bucket for. /// Requested cooldown bucket, or null if one wasn't present. - public ContextMenuCooldownBucket GetBucket(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 +67,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 +81,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 +92,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 +111,36 @@ 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); - } + 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 bucket.DecrementUseAsync().ConfigureAwait(false); + 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 (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 64ec59a68f..5a6d14efef 100644 --- a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs +++ b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs @@ -1,63 +1,59 @@ 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. /// +/// +/// 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. +/// The responder type used to respond to cooldown ratelimit hits. [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, Type? cooldownResponderType = null) : 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; } + public CooldownBucketType BucketType { get; } = bucketType; /// - /// Gets the cooldown buckets for this command. + /// Gets the responder type. /// - internal readonly ConcurrentDictionary Buckets; - - /// - /// Defines a cooldown for this command. This means that users will be able to use the command a specific number of times before they have to wait to use it again. - /// - /// Number of times the command can be used before triggering a cooldown. - /// Number of seconds after which the cooldown is reset. - /// Type of cooldown bucket. This allows controlling whether the bucket will be cooled down per user, guild, channel, or globally. - public SlashCommandCooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType) - { - this.MaxUses = maxUses; - this.Reset = TimeSpan.FromSeconds(resetAfter); - this.BucketType = bucketType; - this.Buckets = new(); - } + public Type? ResponderType { get; } = cooldownResponderType; /// /// Gets a cooldown bucket for given command context. /// /// Command context to get cooldown bucket for. /// Requested cooldown bucket, or null if one wasn't present. - public SlashCommandCooldownBucket GetBucket(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,36 @@ 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); - } + 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); - return await bucket.DecrementUseAsync().ConfigureAwait(false); + 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 (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/Context/BaseContext.cs b/DisCatSharp.ApplicationCommands/Context/BaseContext.cs index d63fac1474..290544e3ff 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,14 @@ public string FullCommandName /// public IServiceProvider Services { get; internal set; } = new ServiceCollection().BuildServiceProvider(true); + /// + /// Initializes a new instance of the class. + /// + /// The command type. + internal BaseContext(DisCatSharpCommandType type) + : base(type) + { } + /// /// 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 7b03989840..fb405d7294 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 1000b43014..c425cb79e6 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.ApplicationCommands/Entities/ChoiceTranslator.cs b/DisCatSharp.ApplicationCommands/Entities/ChoiceTranslator.cs index faa879592b..14f99ad00f 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 4093f63f72..93f73f0170 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 f2ca93afbb..41dd1d9488 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 beaafa1934..de6b8a0fe5 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/ICooldownResponder.cs b/DisCatSharp.ApplicationCommands/Entities/ICooldownResponder.cs new file mode 100644 index 0000000000..da4e140241 --- /dev/null +++ b/DisCatSharp.ApplicationCommands/Entities/ICooldownResponder.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; + +using DisCatSharp.ApplicationCommands.Context; + +namespace DisCatSharp.ApplicationCommands.Entities; + +/// +/// The cooldown responder. +/// +public interface ICooldownResponder +{ + /// + /// Responds to cooldown ratelimit hits with given response. + /// + /// The context. + Task Responder(BaseContext context); +} diff --git a/DisCatSharp.ApplicationCommands/Entities/OptionTranslator.cs b/DisCatSharp.ApplicationCommands/Entities/OptionTranslator.cs index 5380ed2050..0b786d0bc6 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 48a108f0ea..70856841e0 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 e523816e89..b2219dfa7d 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.ApplicationCommands/Workers/ApplicationCommandWorker.cs b/DisCatSharp.ApplicationCommands/Workers/ApplicationCommandWorker.cs index 0684a19d6b..08fbb8d293 100644 --- a/DisCatSharp.ApplicationCommands/Workers/ApplicationCommandWorker.cs +++ b/DisCatSharp.ApplicationCommands/Workers/ApplicationCommandWorker.cs @@ -233,20 +233,23 @@ 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; diff --git a/DisCatSharp.ApplicationCommands/Workers/RegistrationWorker.cs b/DisCatSharp.ApplicationCommands/Workers/RegistrationWorker.cs index 91687126b5..5166a2eb7f 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))); diff --git a/DisCatSharp.CommandsNext/Attributes/CooldownAttribute.cs b/DisCatSharp.CommandsNext/Attributes/CooldownAttribute.cs index 699764051e..796dbec21b 100644 --- a/DisCatSharp.CommandsNext/Attributes/CooldownAttribute.cs +++ b/DisCatSharp.CommandsNext/Attributes/CooldownAttribute.cs @@ -1,61 +1,57 @@ using System; -using System.Collections.Concurrent; -using System.Globalization; -using System.Threading; using System.Threading.Tasks; +using DisCatSharp.CommandsNext.Entities; +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 /// +/// +/// 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. +/// The responder type used to respond to cooldown ratelimit hits. [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = false)] -public sealed class CooldownAttribute : CheckBaseAttribute +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. /// - 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; } - - /// - /// Gets the cooldown buckets for this command. - /// - private readonly ConcurrentDictionary _buckets; + public CooldownBucketType BucketType { get; } = bucketType; /// - /// 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. + /// Gets the responder type. /// - /// 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; - this._buckets = new(); - } + public Type? ResponderType { get; } = cooldownResponderType; /// /// Gets a cooldown bucket for given command context. /// /// Command context to get cooldown bucket for. /// Requested cooldown bucket, or null if one wasn't present. - public 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,39 @@ 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); + if (ctx.Client.CommandCooldownBuckets.TryGetValue(bid, out var bucket)) + return await this.RespondRatelimitHitAsync(ctx, await bucket.DecrementUseAsync(ctx), bucket); - private int _remainingUses; + bucket = new(this.MaxUses, this.Reset, ctx.Command.QualifiedName, "text", usr, chn, gld); + ctx.Client.CommandCooldownBuckets.AddOrUpdate(bid, bucket, (k, v) => 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; - - /// - /// 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 we're past reset time... - var now = DateTimeOffset.UtcNow; - if (now >= this.ResetsAt) - { - // ...do the reset and set a new reset time - Interlocked.Exchange(ref this._remainingUses, this.MaxUses); - this.ResetsAt = now + this.Reset; - } + if (noHit) + return true; - // check if we have any uses left, if we do... - var success = false; - if (this.RemainingUses > 0) + if (this.ResponderType is null) { - // ...decrement, and return success... - Interlocked.Decrement(ref this._remainingUses); - success = true; + try + { + await ctx.Message.CreateReactionAsync(DiscordEmoji.FromName(ctx.Client, ":x:", false)); + } + catch (UnauthorizedException) + { + // ignore + } + + return false; } - // ...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; + var providerMethod = this.ResponderType.GetMethod(nameof(ICooldownResponder.Responder)); + var providerInstance = Activator.CreateInstance(this.ResponderType); + await ((Task)providerMethod.Invoke(providerInstance, [ctx])).ConfigureAwait(false); - 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 ae3d322615..f052de5bc7 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 5930de264f..063fc1039c 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/Entities/ICooldownResponder.cs b/DisCatSharp.CommandsNext/Entities/ICooldownResponder.cs new file mode 100644 index 0000000000..8465c45bb8 --- /dev/null +++ b/DisCatSharp.CommandsNext/Entities/ICooldownResponder.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; + +namespace DisCatSharp.CommandsNext.Entities; + +/// +/// The cooldown responder. +/// +public interface ICooldownResponder +{ + /// + /// Responds to cooldown ratelimit hits with given actions. + /// For example you could respond with a reaction or send a dm. + /// + /// The context. + Task Responder(CommandContext context); +} diff --git a/DisCatSharp.CommandsNext/EventArgs/CommandContext.cs b/DisCatSharp.CommandsNext/EventArgs/CommandContext.cs index 1d4fa6bb04..0eeadcb679 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.CommandsNext/GlobalSuppressions.cs b/DisCatSharp.CommandsNext/GlobalSuppressions.cs index 21e46756fe..0c4423d196 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.Docs/articles/modules/application_commands/translations/reference.md b/DisCatSharp.Docs/articles/modules/application_commands/translations/reference.md index 91c08e74ed..3e3add5738 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 08eae63f48..1fadf068fc 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/Clients/DiscordClient.cs b/DisCatSharp/Clients/DiscordClient.cs index f42f01344c..b531722c6c 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 a446297986..5ca8cfdd46 100644 --- a/DisCatSharp/Entities/Application/DiscordApplicationCommand.cs +++ b/DisCatSharp/Entities/Application/DiscordApplicationCommand.cs @@ -36,33 +36,33 @@ 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. /// - [JsonProperty("description")] - public string Description { get; internal set; } + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + public string? Description { get; internal set; } /// /// 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. @@ -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, @@ -145,9 +146,9 @@ 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) + 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)); diff --git a/DisCatSharp/Entities/Application/DiscordApplicationCommandLocalization.cs b/DisCatSharp/Entities/Application/DiscordApplicationCommandLocalization.cs index 3475fbba84..d5a5576724 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", "id", "es-419"]; /// /// 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 3c5f41e5a0..852e7ff427 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.ApplicationCommands/Entities/CooldownBucket.cs b/DisCatSharp/Entities/Core/CooldownBucket.cs similarity index 58% rename from DisCatSharp.ApplicationCommands/Entities/CooldownBucket.cs rename to DisCatSharp/Entities/Core/CooldownBucket.cs index 43eca62e7d..24cc57bd4a 100644 --- a/DisCatSharp.ApplicationCommands/Entities/CooldownBucket.cs +++ b/DisCatSharp/Entities/Core/CooldownBucket.cs @@ -3,8 +3,14 @@ using System.Threading; using System.Threading.Tasks; -namespace DisCatSharp.ApplicationCommands.Entities; +using Microsoft.Extensions.Logging; +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 { /// @@ -18,10 +24,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 +41,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 +64,9 @@ public class CooldownBucket : IBucket, IEquatable /// internal readonly SemaphoreSlim UsageSemaphore; + /// + /// Gets the remaining uses for this bucket. + /// internal int RemainingUsesInternal; /// @@ -59,10 +74,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 +89,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 +134,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 +158,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 +178,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 0000000000..e01687974e --- /dev/null +++ b/DisCatSharp/Entities/Core/DisCatSharpCommandContext.cs @@ -0,0 +1,73 @@ +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; } + + /// + /// Initializes a new instance of the class. + /// + /// The command type. + internal DisCatSharpCommandContext(DisCatSharpCommandType type) + { + this.CommandType = type; + } +} 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 fa10df5bb7..3ff9bd923c 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 67% rename from DisCatSharp.ApplicationCommands/Entities/ICooldown.cs rename to DisCatSharp/Entities/Core/ICooldown.cs index ae3747f725..a73a9ca795 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,12 @@ 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 hit. + /// The cooldown bucket. + 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 094ec2173c..0000000000 --- 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 367e6a71f0..0000000000 --- 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 ed54c37a73..0000000000 --- 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/Entities/Interaction/DiscordInteraction.cs b/DisCatSharp/Entities/Interaction/DiscordInteraction.cs index 1cb3822562..e5d3c1fb18 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.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 de5e750dce..f50fecab0d 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/DisCatSharpCommandType.cs b/DisCatSharp/Enums/Core/DisCatSharpCommandType.cs new file mode 100644 index 0000000000..22619fd78f --- /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/GlobalSuppressions.cs b/DisCatSharp/GlobalSuppressions.cs index 39440d9bd9..bd0eb64b22 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/Abstractions/Rest/RestApplicationCommandPayloads.cs b/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs index fae7e13eb7..2c5d079cc9 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 7f74ea06a2..db031457cc 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) { @@ -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, @@ -6596,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); diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b8f8f83e91..d2ce6239dd 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,105 +1,67 @@ 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 + - Fix first-time command registration 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