From 39bbd298c37a7e2766f9eacb65e768930dee67a9 Mon Sep 17 00:00:00 2001 From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Date: Fri, 26 Aug 2022 18:45:27 +0300 Subject: [PATCH 1/9] Interactions Command Localization (#2395) * Request headers (#2394) * add support for per-request headers * remove unnecessary usings * Revert "remove unnecessary usings" This reverts commit 8d674fe4faf985b117f143fae3877a1698170ad2. * remove nullable strings from RequestOptions * Add Localization Support to Interaction Service (#2211) * add json and resx localization managers * add utils class for getting command paths * update json regex to make langage code optional * remove IServiceProvider from ILocalizationManager method params * replace the command path method in command map * add localization fields to rest and websocket application command entity implementations * move deconstruct extensions method to extensions folder * add withLocalizations parameter to rest methods * fix build error * add rest conversions to interaction service * add localization to the rest methods * add inline docs * fix implementation bugs * add missing inline docs * inline docs correction (Name/Description Localized properties) * add choice localization * fix conflicts * fix conflicts * add missing command props fields to ToApplicationCommandProps methods * add locale parameter to Get*ApplicationCommandsAsync methods for fetching localized command names/descriptions * Apply suggestions from code review Co-authored-by: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Guilds/IGuild.cs Co-authored-by: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> * add inline docs to LocalizationTarget * fix upstream merge errors * fix command parsing for context command names with space char * fix command parsing for context command names with space char * fix failed to generate buket id * fix get guild commands endpoint * update rexs localization manager to use single-file pattern * Upstream Merge Localization Branch (#2434) * fix ci/cd error (#2428) * Fix role icon & emoji assignment. (#2416) * Fix IGuild.GetBansAsync() (#2424) fix the problem of not being able to get more than 1000 bans * [DOCS] Add a note about `DontAutoRegisterAttribute` (#2430) * add a note about `DontAutoRegisterAttribute` * Remove "to to" and add punctuation Co-authored-by: MrCakeSlayer <13650699+MrCakeSlayer@users.noreply.github.com> * fix: Missing Fact attribute in ColorTests (#2425) * feat: Embed comparison (#2347) * Fix broken code snippet in dependency injection docs (#2420) * Fixed markdown formatting to show code snippet * Fixed constructor injection code snippet pointer * Added support for lottie stickers (#2359) Co-authored-by: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Co-authored-by: BokuNoPasya <49203428+1NieR@users.noreply.github.com> Co-authored-by: Misha133 <61027276+Misha-133@users.noreply.github.com> Co-authored-by: MrCakeSlayer <13650699+MrCakeSlayer@users.noreply.github.com> Co-authored-by: Ge Co-authored-by: Charlie U <52503242+cpurules@users.noreply.github.com> Co-authored-by: Kuba_Z2 <77853483+KubaZ2@users.noreply.github.com> * remove unnecassary fields from ResxLocalizationManager * update int framework guides * remove space character tokenization from ResxLocalizationManager Co-authored-by: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Co-authored-by: BokuNoPasya <49203428+1NieR@users.noreply.github.com> Co-authored-by: Misha133 <61027276+Misha-133@users.noreply.github.com> Co-authored-by: MrCakeSlayer <13650699+MrCakeSlayer@users.noreply.github.com> Co-authored-by: Ge Co-authored-by: Charlie U <52503242+cpurules@users.noreply.github.com> Co-authored-by: Kuba_Z2 <77853483+KubaZ2@users.noreply.github.com> --- docs/guides/int_framework/intro.md | 41 +++ .../Entities/Guilds/IGuild.cs | 7 +- .../Interactions/ApplicationCommandOption.cs | 92 ++++- .../ApplicationCommandOptionChoice.cs | 33 ++ .../ApplicationCommandProperties.cs | 52 +++ .../ContextMenus/MessageCommandBuilder.cs | 70 ++++ .../ContextMenus/UserCommandBuilder.cs | 72 +++- .../Interactions/IApplicationCommand.cs | 26 ++ .../Interactions/IApplicationCommandOption.cs | 26 ++ .../IApplicationCommandOptionChoice.cs | 15 + .../SlashCommands/SlashCommandBuilder.cs | 325 ++++++++++++++++-- .../Extensions/GenericCollectionExtensions.cs | 15 + src/Discord.Net.Core/IDiscordClient.cs | 4 +- src/Discord.Net.Core/Net/Rest/IRestClient.cs | 10 +- src/Discord.Net.Core/RequestOptions.cs | 12 +- src/Discord.Net.Core/Utils/Preconditions.cs | 10 +- .../InteractionService.cs | 6 + .../InteractionServiceConfig.cs | 5 + .../ILocalizationManager.cs | 32 ++ .../JsonLocalizationManager.cs | 72 ++++ .../ResxLocalizationManager.cs | 55 +++ .../LocalizationTarget.cs | 25 ++ .../Map/CommandMap.cs | 23 +- .../Utilities/ApplicationCommandRestUtil.cs | 88 ++++- .../Utilities/CommandHierarchy.cs | 53 +++ .../API/Common/ApplicationCommand.cs | 13 + .../API/Common/ApplicationCommandOption.cs | 21 ++ .../Common/ApplicationCommandOptionChoice.cs | 7 + .../Rest/CreateApplicationCommandParams.cs | 15 +- .../Rest/ModifyApplicationCommandParams.cs | 7 + src/Discord.Net.Rest/BaseDiscordClient.cs | 2 +- src/Discord.Net.Rest/ClientHelper.cs | 12 +- src/Discord.Net.Rest/DiscordRestApiClient.cs | 31 +- src/Discord.Net.Rest/DiscordRestClient.cs | 14 +- .../Entities/Guilds/GuildHelper.cs | 6 +- .../Entities/Guilds/RestGuild.cs | 16 +- .../Interactions/InteractionHelper.cs | 20 +- .../Interactions/RestApplicationCommand.cs | 35 ++ .../RestApplicationCommandChoice.cs | 17 + .../RestApplicationCommandOption.cs | 37 +- src/Discord.Net.Rest/Net/DefaultRestClient.cs | 20 +- .../Net/Queue/Requests/RestRequest.cs | 5 +- .../DiscordSocketClient.cs | 10 +- .../Entities/Guilds/SocketGuild.cs | 11 +- .../SocketApplicationCommand.cs | 35 ++ .../SocketApplicationCommandChoice.cs | 17 + .../SocketApplicationCommandOption.cs | 35 ++ 47 files changed, 1403 insertions(+), 152 deletions(-) create mode 100644 src/Discord.Net.Core/Extensions/GenericCollectionExtensions.cs create mode 100644 src/Discord.Net.Interactions/LocalizationManagers/ILocalizationManager.cs create mode 100644 src/Discord.Net.Interactions/LocalizationManagers/JsonLocalizationManager.cs create mode 100644 src/Discord.Net.Interactions/LocalizationManagers/ResxLocalizationManager.cs create mode 100644 src/Discord.Net.Interactions/LocalizationTarget.cs create mode 100644 src/Discord.Net.Interactions/Utilities/CommandHierarchy.cs diff --git a/docs/guides/int_framework/intro.md b/docs/guides/int_framework/intro.md index 37c579159..21ea365de 100644 --- a/docs/guides/int_framework/intro.md +++ b/docs/guides/int_framework/intro.md @@ -376,6 +376,47 @@ respond to the Interactions within your command modules you need to perform the delegate can be used to create HTTP responses from a deserialized json object string. - Use the interaction endpoints of the module base instead of the interaction object (ie. `RespondAsync()`, `FollowupAsync()`...). +## Localization + +Discord Slash Commands support name/description localization. Localization is available for names and descriptions of Slash Command Groups ([GroupAttribute]), Slash Commands ([SlashCommandAttribute]), Slash Command parameters and Slash Command Parameter Choices. Interaction Service can be initialized with an `ILocalizationManager` instance in its config which is used to create the necessary localization dictionaries on command registration. Interaction Service has two built-in `ILocalizationManager` implementations: `ResxLocalizationManager` and `JsonLocalizationManager`. + +### ResXLocalizationManager + +`ResxLocalizationManager` uses `.` delimited key names to traverse the resource files and get the localized strings (`group1.group2.command.parameter.name`). A `ResxLocalizationManager` instance must be initialized with a base resource name, a target assembly and a collection of `CultureInfo`s. Every key path must end with either `.name` or `.description`, including parameter choice strings. [Discord.Tools.LocalizationTemplate.Resx](https://www.nuget.org/packages/Discord.Tools.LocalizationTemplate.Resx) dotnet tool can be used to create localization file templates. + +### JsonLocalizationManager + +`JsonLocaliationManager` uses a nested data structure similar to Discord's Application Commands schema. You can get the Json schema [here](https://gist.github.com/Cenngo/d46a881de24823302f66c3c7e2f7b254). `JsonLocalizationManager` accepts a base path and a base file name and automatically discovers every resource file ( \basePath\fileName.locale.json ). A Json resource file should have a structure similar to: + +```json +{ + "command_1":{ + "name": "localized_name", + "description": "localized_description", + "parameter_1":{ + "name": "localized_name", + "description": "localized_description" + } + }, + "group_1":{ + "name": "localized_name", + "description": "localized_description", + "command_1":{ + "name": "localized_name", + "description": "localized_description", + "parameter_1":{ + "name": "localized_name", + "description": "localized_description" + }, + "parameter_2":{ + "name": "localized_name", + "description": "localized_description" + } + } + } +} +``` + [AutocompleteHandlers]: xref:Guides.IntFw.AutoCompletion [DependencyInjection]: xref:Guides.DI.Intro diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 775ff9e65..34a08f1e7 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -1194,12 +1194,17 @@ namespace Discord /// /// Gets this guilds application commands. /// + /// + /// Whether to include full localization dictionaries in the returned objects, + /// instead of the localized name and description fields. + /// + /// The target locale of the localized name and description fields. Sets the X-Discord-Locale header, which takes precedence over Accept-Language. /// The options to be used when sending the request. /// /// A task that represents the asynchronous get operation. The task result contains a read-only collection /// of application commands found within the guild. /// - Task> GetApplicationCommandsAsync(RequestOptions options = null); + Task> GetApplicationCommandsAsync(bool withLocalizations = false, string locale = null, RequestOptions options = null); /// /// Gets an application command within this guild with the specified id. diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs index 5e4f6a81d..bceefda32 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; @@ -12,6 +13,8 @@ namespace Discord { private string _name; private string _description; + private IDictionary _nameLocalizations = new Dictionary(); + private IDictionary _descriptionLocalizations = new Dictionary(); /// /// Gets or sets the name of this option. @@ -21,18 +24,7 @@ namespace Discord get => _name; set { - if (value == null) - throw new ArgumentNullException(nameof(value), $"{nameof(Name)} cannot be null."); - - if (value.Length > 32) - throw new ArgumentOutOfRangeException(nameof(value), "Name length must be less than or equal to 32."); - - if (!Regex.IsMatch(value, @"^[\w-]{1,32}$")) - throw new FormatException($"{nameof(value)} must match the regex ^[\\w-]{{1,32}}$"); - - if (value.Any(x => char.IsUpper(x))) - throw new FormatException("Name cannot contain any uppercase characters."); - + EnsureValidOptionName(value); _name = value; } } @@ -43,12 +35,11 @@ namespace Discord public string Description { get => _description; - set => _description = value?.Length switch + set { - > 100 => throw new ArgumentOutOfRangeException(nameof(value), "Description length must be less than or equal to 100."), - 0 => throw new ArgumentOutOfRangeException(nameof(value), "Description length must be at least 1."), - _ => value - }; + EnsureValidOptionDescription(value); + _description = value; + } } /// @@ -105,5 +96,72 @@ namespace Discord /// Gets or sets the allowed channel types for this option. /// public List ChannelTypes { get; set; } + + /// + /// Gets or sets the localization dictionary for the name field of this option. + /// + /// Thrown when any of the dictionary keys is an invalid locale. + public IDictionary NameLocalizations + { + get => _nameLocalizations; + set + { + foreach (var (locale, name) in value) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidOptionName(name); + } + _nameLocalizations = value; + } + } + + /// + /// Gets or sets the localization dictionary for the description field of this option. + /// + /// Thrown when any of the dictionary keys is an invalid locale. + public IDictionary DescriptionLocalizations + { + get => _descriptionLocalizations; + set + { + foreach (var (locale, description) in value) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidOptionDescription(description); + } + _descriptionLocalizations = value; + } + } + + private static void EnsureValidOptionName(string name) + { + if (name == null) + throw new ArgumentNullException(nameof(name), $"{nameof(Name)} cannot be null."); + + if (name.Length > 32) + throw new ArgumentOutOfRangeException(nameof(name), "Name length must be less than or equal to 32."); + + if (!Regex.IsMatch(name, @"^[\w-]{1,32}$")) + throw new FormatException($"{nameof(name)} must match the regex ^[\\w-]{{1,32}}$"); + + if (name.Any(x => char.IsUpper(x))) + throw new FormatException("Name cannot contain any uppercase characters."); + } + + private static void EnsureValidOptionDescription(string description) + { + switch (description.Length) + { + case > 100: + throw new ArgumentOutOfRangeException(nameof(description), + "Description length must be less than or equal to 100."); + case 0: + throw new ArgumentOutOfRangeException(nameof(description), "Description length must at least 1."); + } + } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs index 6a908b075..8f1ecc6d2 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs @@ -1,4 +1,8 @@ using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; namespace Discord { @@ -9,6 +13,7 @@ namespace Discord { private string _name; private object _value; + private IDictionary _nameLocalizations = new Dictionary(); /// /// Gets or sets the name of this choice. @@ -40,5 +45,33 @@ namespace Discord _value = value; } } + + /// + /// Gets or sets the localization dictionary for the name field of this choice. + /// + /// Thrown when any of the dictionary keys is an invalid locale. + public IDictionary NameLocalizations + { + get => _nameLocalizations; + set + { + foreach (var (locale, name) in value) + { + if (!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException("Key values of the dictionary must be valid language codes."); + + switch (name.Length) + { + case > 100: + throw new ArgumentOutOfRangeException(nameof(value), + "Name length must be less than or equal to 100."); + case 0: + throw new ArgumentOutOfRangeException(nameof(value), "Name length must at least 1."); + } + } + + _nameLocalizations = value; + } + } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs index 9b3ac8453..7ca16a27d 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs @@ -1,3 +1,10 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text.RegularExpressions; + namespace Discord { /// @@ -5,6 +12,9 @@ namespace Discord /// public abstract class ApplicationCommandProperties { + private IReadOnlyDictionary _nameLocalizations; + private IReadOnlyDictionary _descriptionLocalizations; + internal abstract ApplicationCommandType Type { get; } /// @@ -17,6 +27,48 @@ namespace Discord /// public Optional IsDefaultPermission { get; set; } + /// + /// Gets or sets the localization dictionary for the name field of this command. + /// + public IReadOnlyDictionary NameLocalizations + { + get => _nameLocalizations; + set + { + foreach (var (locale, name) in value) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + Preconditions.AtLeast(name.Length, 1, nameof(name)); + Preconditions.AtMost(name.Length, SlashCommandBuilder.MaxNameLength, nameof(name)); + if (!Regex.IsMatch(name, @"^[\w-]{1,32}$")) + throw new ArgumentException("Option name cannot contain any special characters or whitespaces!", nameof(name)); + } + _nameLocalizations = value; + } + } + + /// + /// Gets or sets the localization dictionary for the description field of this command. + /// + public IReadOnlyDictionary DescriptionLocalizations + { + get => _descriptionLocalizations; + set + { + foreach (var (locale, description) in value) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + Preconditions.AtLeast(description.Length, 1, nameof(description)); + Preconditions.AtMost(description.Length, SlashCommandBuilder.MaxDescriptionLength, nameof(description)); + } + _descriptionLocalizations = value; + } + } + /// /// Gets or sets whether or not this command can be used in DMs. /// diff --git a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs index 59040dd4e..ed49c685d 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs @@ -1,3 +1,8 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + namespace Discord { /// @@ -31,6 +36,11 @@ namespace Discord /// public bool IsDefaultPermission { get; set; } = true; + /// + /// Gets the localization dictionary for the name field of this command. + /// + public IReadOnlyDictionary NameLocalizations => _nameLocalizations; + /// /// Gets or sets whether or not this command can be used in DMs. /// @@ -42,6 +52,7 @@ namespace Discord public GuildPermission? DefaultMemberPermissions { get; set; } private string _name; + private Dictionary _nameLocalizations; /// /// Build the current builder into a class. @@ -86,6 +97,30 @@ namespace Discord return this; } + /// + /// Sets the collection. + /// + /// The localization dictionary to use for the name field of this command. + /// + /// Thrown if is null. + /// Thrown if any dictionary key is an invalid locale string. + public MessageCommandBuilder WithNameLocalizations(IDictionary nameLocalizations) + { + if (nameLocalizations is null) + throw new ArgumentNullException(nameof(nameLocalizations)); + + foreach (var (locale, name) in nameLocalizations) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandName(name); + } + + _nameLocalizations = new Dictionary(nameLocalizations); + return this; + } + /// /// Sets whether or not this command can be used in dms /// @@ -97,6 +132,41 @@ namespace Discord return this; } + /// + /// Adds a new entry to the collection. + /// + /// Locale of the entry. + /// Localized string for the name field. + /// The current builder. + /// Thrown if is an invalid locale string. + public MessageCommandBuilder AddNameLocalization(string locale, string name) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandName(name); + + _nameLocalizations ??= new(); + _nameLocalizations.Add(locale, name); + + return this; + } + + private static void EnsureValidCommandName(string name) + { + Preconditions.NotNullOrEmpty(name, nameof(name)); + Preconditions.AtLeast(name.Length, 1, nameof(name)); + Preconditions.AtMost(name.Length, MaxNameLength, nameof(name)); + + // Discord updated the docs, this regex prevents special characters like @!$%(... etc, + // https://discord.com/developers/docs/interactions/slash-commands#applicationcommand + if (!Regex.IsMatch(name, @"^[\w-]{1,32}$")) + throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(name)); + + if (name.Any(x => char.IsUpper(x))) + throw new FormatException("Name cannot contain any uppercase characters."); + } + /// /// Sets the default member permissions required to use this application command. /// diff --git a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs index 7c82dce55..d8bb2e056 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs @@ -1,3 +1,8 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + namespace Discord { /// @@ -5,7 +10,7 @@ namespace Discord /// public class UserCommandBuilder { - /// + /// /// Returns the maximum length a commands name allowed by Discord. /// public const int MaxNameLength = 32; @@ -31,6 +36,11 @@ namespace Discord /// public bool IsDefaultPermission { get; set; } = true; + /// + /// Gets the localization dictionary for the name field of this command. + /// + public IReadOnlyDictionary NameLocalizations => _nameLocalizations; + /// /// Gets or sets whether or not this command can be used in DMs. /// @@ -42,6 +52,7 @@ namespace Discord public GuildPermission? DefaultMemberPermissions { get; set; } private string _name; + private Dictionary _nameLocalizations; /// /// Build the current builder into a class. @@ -84,6 +95,30 @@ namespace Discord return this; } + /// + /// Sets the collection. + /// + /// The localization dictionary to use for the name field of this command. + /// The current builder. + /// Thrown if is null. + /// Thrown if any dictionary key is an invalid locale string. + public UserCommandBuilder WithNameLocalizations(IDictionary nameLocalizations) + { + if (nameLocalizations is null) + throw new ArgumentNullException(nameof(nameLocalizations)); + + foreach (var (locale, name) in nameLocalizations) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandName(name); + } + + _nameLocalizations = new Dictionary(nameLocalizations); + return this; + } + /// /// Sets whether or not this command can be used in dms /// @@ -95,6 +130,41 @@ namespace Discord return this; } + /// + /// Adds a new entry to the collection. + /// + /// Locale of the entry. + /// Localized string for the name field. + /// The current builder. + /// Thrown if is an invalid locale string. + public UserCommandBuilder AddNameLocalization(string locale, string name) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandName(name); + + _nameLocalizations ??= new(); + _nameLocalizations.Add(locale, name); + + return this; + } + + private static void EnsureValidCommandName(string name) + { + Preconditions.NotNullOrEmpty(name, nameof(name)); + Preconditions.AtLeast(name.Length, 1, nameof(name)); + Preconditions.AtMost(name.Length, MaxNameLength, nameof(name)); + + // Discord updated the docs, this regex prevents special characters like @!$%(... etc, + // https://discord.com/developers/docs/interactions/slash-commands#applicationcommand + if (!Regex.IsMatch(name, @"^[\w-]{1,32}$")) + throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(name)); + + if (name.Any(x => char.IsUpper(x))) + throw new FormatException("Name cannot contain any uppercase characters."); + } + /// /// Sets the default member permissions required to use this application command. /// diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs index 58a002649..6f9ce7a45 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs @@ -52,6 +52,32 @@ namespace Discord /// IReadOnlyCollection Options { get; } + /// + /// Gets the localization dictionary for the name field of this command. + /// + IReadOnlyDictionary NameLocalizations { get; } + + /// + /// Gets the localization dictionary for the description field of this command. + /// + IReadOnlyDictionary DescriptionLocalizations { get; } + + /// + /// Gets the localized name of this command. + /// + /// + /// Only returned when the `withLocalizations` query parameter is set to when requesting the command. + /// + string NameLocalized { get; } + + /// + /// Gets the localized description of this command. + /// + /// + /// Only returned when the `withLocalizations` query parameter is set to when requesting the command. + /// + string DescriptionLocalized { get; } + /// /// Modifies the current application command. /// diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs index c0a752fdc..fb179b661 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs @@ -71,5 +71,31 @@ namespace Discord /// Gets the allowed channel types for this option. /// IReadOnlyCollection ChannelTypes { get; } + + /// + /// Gets the localization dictionary for the name field of this command option. + /// + IReadOnlyDictionary NameLocalizations { get; } + + /// + /// Gets the localization dictionary for the description field of this command option. + /// + IReadOnlyDictionary DescriptionLocalizations { get; } + + /// + /// Gets the localized name of this command option. + /// + /// + /// Only returned when the `withLocalizations` query parameter is set to when requesting the command. + /// + string NameLocalized { get; } + + /// + /// Gets the localized description of this command option. + /// + /// + /// Only returned when the `withLocalizations` query parameter is set to true when requesting the command. + /// + string DescriptionLocalized { get; } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs index 631706c6f..3f76bae72 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + namespace Discord { /// @@ -14,5 +16,18 @@ namespace Discord /// Gets the value of the choice. /// object Value { get; } + + /// + /// Gets the localization dictionary for the name field of this command option. + /// + IReadOnlyDictionary NameLocalizations { get; } + + /// + /// Gets the localized name of this command option. + /// + /// + /// Only returned when the `withLocalizations` query parameter is set to when requesting the command. + /// + string NameLocalized { get; } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs index bf22d4e3a..579289304 100644 --- a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs @@ -1,6 +1,9 @@ using System; +using System.Collections; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; +using System.Net.Sockets; using System.Text.RegularExpressions; namespace Discord @@ -31,18 +34,7 @@ namespace Discord get => _name; set { - Preconditions.NotNullOrEmpty(value, nameof(value)); - Preconditions.AtLeast(value.Length, 1, nameof(value)); - Preconditions.AtMost(value.Length, MaxNameLength, nameof(value)); - - // Discord updated the docs, this regex prevents special characters like @!$%(... etc, - // https://discord.com/developers/docs/interactions/slash-commands#applicationcommand - if (!Regex.IsMatch(value, @"^[\w-]{1,32}$")) - throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(value)); - - if (value.Any(x => char.IsUpper(x))) - throw new FormatException("Name cannot contain any uppercase characters."); - + EnsureValidCommandName(value); _name = value; } } @@ -55,10 +47,7 @@ namespace Discord get => _description; set { - Preconditions.NotNullOrEmpty(value, nameof(Description)); - Preconditions.AtLeast(value.Length, 1, nameof(Description)); - Preconditions.AtMost(value.Length, MaxDescriptionLength, nameof(Description)); - + EnsureValidCommandDescription(value); _description = value; } } @@ -76,6 +65,16 @@ namespace Discord } } + /// + /// Gets the localization dictionary for the name field of this command. + /// + public IReadOnlyDictionary NameLocalizations => _nameLocalizations; + + /// + /// Gets the localization dictionary for the description field of this command. + /// + public IReadOnlyDictionary DescriptionLocalizations => _descriptionLocalizations; + /// /// Gets or sets whether the command is enabled by default when the app is added to a guild /// @@ -93,6 +92,8 @@ namespace Discord private string _name; private string _description; + private Dictionary _nameLocalizations; + private Dictionary _descriptionLocalizations; private List _options; /// @@ -106,6 +107,8 @@ namespace Discord Name = Name, Description = Description, IsDefaultPermission = IsDefaultPermission, + NameLocalizations = _nameLocalizations, + DescriptionLocalizations = _descriptionLocalizations, IsDMEnabled = IsDMEnabled, DefaultMemberPermissions = DefaultMemberPermissions ?? Optional.Unspecified }; @@ -190,13 +193,17 @@ namespace Discord /// If this option is set to autocomplete. /// The options of the option to add. /// The allowed channel types for this option. + /// Localization dictionary for the name field of this command. + /// Localization dictionary for the description field of this command. /// The choices of this option. /// The smallest number value the user can input. /// The largest number value the user can input. /// The current builder. public SlashCommandBuilder AddOption(string name, ApplicationCommandOptionType type, string description, bool? isRequired = null, bool? isDefault = null, bool isAutocomplete = false, double? minValue = null, double? maxValue = null, - int? minLength = null, int? maxLength = null, List options = null, List channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices) + List options = null, List channelTypes = null, IDictionary nameLocalizations = null, + IDictionary descriptionLocalizations = null, + int? minLength = null, int? maxLength = null, params ApplicationCommandOptionChoiceProperties[] choices) { Preconditions.Options(name, description); @@ -226,6 +233,12 @@ namespace Discord MaxLength = maxLength, }; + if (nameLocalizations is not null) + option.WithNameLocalizations(nameLocalizations); + + if (descriptionLocalizations is not null) + option.WithDescriptionLocalizations(descriptionLocalizations); + return AddOption(option); } @@ -268,6 +281,116 @@ namespace Discord Options.AddRange(options); return this; } + + /// + /// Sets the collection. + /// + /// The localization dictionary to use for the name field of this command. + /// + /// Thrown if is null. + /// Thrown if any dictionary key is an invalid locale string. + public SlashCommandBuilder WithNameLocalizations(IDictionary nameLocalizations) + { + if (nameLocalizations is null) + throw new ArgumentNullException(nameof(nameLocalizations)); + + foreach (var (locale, name) in nameLocalizations) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandName(name); + } + + _nameLocalizations = new Dictionary(nameLocalizations); + return this; + } + + /// + /// Sets the collection. + /// + /// The localization dictionary to use for the description field of this command. + /// + /// Thrown if is null. + /// Thrown if any dictionary key is an invalid locale string. + public SlashCommandBuilder WithDescriptionLocalizations(IDictionary descriptionLocalizations) + { + if (descriptionLocalizations is null) + throw new ArgumentNullException(nameof(descriptionLocalizations)); + + foreach (var (locale, description) in descriptionLocalizations) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandDescription(description); + } + + _descriptionLocalizations = new Dictionary(descriptionLocalizations); + return this; + } + + /// + /// Adds a new entry to the collection. + /// + /// Locale of the entry. + /// Localized string for the name field. + /// The current builder. + /// Thrown if is an invalid locale string. + public SlashCommandBuilder AddNameLocalization(string locale, string name) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandName(name); + + _nameLocalizations ??= new(); + _nameLocalizations.Add(locale, name); + + return this; + } + + /// + /// Adds a new entry to the collection. + /// + /// Locale of the entry. + /// Localized string for the description field. + /// The current builder. + /// Thrown if is an invalid locale string. + public SlashCommandBuilder AddDescriptionLocalization(string locale, string description) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandDescription(description); + + _descriptionLocalizations ??= new(); + _descriptionLocalizations.Add(locale, description); + + return this; + } + + internal static void EnsureValidCommandName(string name) + { + Preconditions.NotNullOrEmpty(name, nameof(name)); + Preconditions.AtLeast(name.Length, 1, nameof(name)); + Preconditions.AtMost(name.Length, MaxNameLength, nameof(name)); + + // Discord updated the docs, this regex prevents special characters like @!$%(... etc, + // https://discord.com/developers/docs/interactions/slash-commands#applicationcommand + if (!Regex.IsMatch(name, @"^[\w-]{1,32}$")) + throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(name)); + + if (name.Any(x => char.IsUpper(x))) + throw new FormatException("Name cannot contain any uppercase characters."); + } + + internal static void EnsureValidCommandDescription(string description) + { + Preconditions.NotNullOrEmpty(description, nameof(description)); + Preconditions.AtLeast(description.Length, 1, nameof(description)); + Preconditions.AtMost(description.Length, MaxDescriptionLength, nameof(description)); + } } /// @@ -287,6 +410,8 @@ namespace Discord private string _name; private string _description; + private Dictionary _nameLocalizations; + private Dictionary _descriptionLocalizations; /// /// Gets or sets the name of this option. @@ -298,10 +423,7 @@ namespace Discord { if (value != null) { - Preconditions.AtLeast(value.Length, 1, nameof(value)); - Preconditions.AtMost(value.Length, SlashCommandBuilder.MaxNameLength, nameof(value)); - if (!Regex.IsMatch(value, @"^[\w-]{1,32}$")) - throw new ArgumentException("Option name cannot contain any special characters or whitespaces!", nameof(value)); + EnsureValidCommandOptionName(value); } _name = value; @@ -318,8 +440,7 @@ namespace Discord { if (value != null) { - Preconditions.AtLeast(value.Length, 1, nameof(value)); - Preconditions.AtMost(value.Length, SlashCommandBuilder.MaxDescriptionLength, nameof(value)); + EnsureValidCommandOptionDescription(value); } _description = value; @@ -381,6 +502,16 @@ namespace Discord /// public List ChannelTypes { get; set; } + /// + /// Gets the localization dictionary for the name field of this command. + /// + public IReadOnlyDictionary NameLocalizations => _nameLocalizations; + + /// + /// Gets the localization dictionary for the description field of this command. + /// + public IReadOnlyDictionary DescriptionLocalizations => _descriptionLocalizations; + /// /// Builds the current option. /// @@ -424,6 +555,8 @@ namespace Discord ChannelTypes = ChannelTypes, MinValue = MinValue, MaxValue = MaxValue, + NameLocalizations = _nameLocalizations, + DescriptionLocalizations = _descriptionLocalizations, MinLength = MinLength, MaxLength = MaxLength, }; @@ -440,13 +573,17 @@ namespace Discord /// If this option supports autocomplete. /// The options of the option to add. /// The allowed channel types for this option. + /// Localization dictionary for the description field of this command. + /// Localization dictionary for the description field of this command. /// The choices of this option. /// The smallest number value the user can input. /// The largest number value the user can input. /// The current builder. public SlashCommandOptionBuilder AddOption(string name, ApplicationCommandOptionType type, string description, bool? isRequired = null, bool isDefault = false, bool isAutocomplete = false, double? minValue = null, double? maxValue = null, - int? minLength = null, int? maxLength = null, List options = null, List channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices) + List options = null, List channelTypes = null, IDictionary nameLocalizations = null, + IDictionary descriptionLocalizations = null, + int? minLength = null, int? maxLength = null, params ApplicationCommandOptionChoiceProperties[] choices) { Preconditions.Options(name, description); @@ -473,9 +610,15 @@ namespace Discord Options = options, Type = type, Choices = (choices ?? Array.Empty()).ToList(), - ChannelTypes = channelTypes + ChannelTypes = channelTypes, }; + if(nameLocalizations is not null) + option.WithNameLocalizations(nameLocalizations); + + if(descriptionLocalizations is not null) + option.WithDescriptionLocalizations(descriptionLocalizations); + return AddOption(option); } /// @@ -522,10 +665,11 @@ namespace Discord /// /// The name of the choice. /// The value of the choice. + /// The localization dictionary for to use the name field of this command option choice. /// The current builder. - public SlashCommandOptionBuilder AddChoice(string name, int value) + public SlashCommandOptionBuilder AddChoice(string name, int value, IDictionary nameLocalizations = null) { - return AddChoiceInternal(name, value); + return AddChoiceInternal(name, value, nameLocalizations); } /// @@ -533,10 +677,11 @@ namespace Discord /// /// The name of the choice. /// The value of the choice. + /// The localization dictionary for to use the name field of this command option choice. /// The current builder. - public SlashCommandOptionBuilder AddChoice(string name, string value) + public SlashCommandOptionBuilder AddChoice(string name, string value, IDictionary nameLocalizations = null) { - return AddChoiceInternal(name, value); + return AddChoiceInternal(name, value, nameLocalizations); } /// @@ -544,10 +689,11 @@ namespace Discord /// /// The name of the choice. /// The value of the choice. + /// Localization dictionary for the description field of this command. /// The current builder. - public SlashCommandOptionBuilder AddChoice(string name, double value) + public SlashCommandOptionBuilder AddChoice(string name, double value, IDictionary nameLocalizations = null) { - return AddChoiceInternal(name, value); + return AddChoiceInternal(name, value, nameLocalizations); } /// @@ -555,10 +701,11 @@ namespace Discord /// /// The name of the choice. /// The value of the choice. + /// The localization dictionary to use for the name field of this command option choice. /// The current builder. - public SlashCommandOptionBuilder AddChoice(string name, float value) + public SlashCommandOptionBuilder AddChoice(string name, float value, IDictionary nameLocalizations = null) { - return AddChoiceInternal(name, value); + return AddChoiceInternal(name, value, nameLocalizations); } /// @@ -566,13 +713,14 @@ namespace Discord /// /// The name of the choice. /// The value of the choice. + /// The localization dictionary to use for the name field of this command option choice. /// The current builder. - public SlashCommandOptionBuilder AddChoice(string name, long value) + public SlashCommandOptionBuilder AddChoice(string name, long value, IDictionary nameLocalizations = null) { - return AddChoiceInternal(name, value); + return AddChoiceInternal(name, value, nameLocalizations); } - private SlashCommandOptionBuilder AddChoiceInternal(string name, object value) + private SlashCommandOptionBuilder AddChoiceInternal(string name, object value, IDictionary nameLocalizations = null) { Choices ??= new List(); @@ -594,7 +742,8 @@ namespace Discord Choices.Add(new ApplicationCommandOptionChoiceProperties { Name = name, - Value = value + Value = value, + NameLocalizations = nameLocalizations }); return this; @@ -724,5 +873,107 @@ namespace Discord Type = type; return this; } + + /// + /// Sets the collection. + /// + /// The localization dictionary to use for the name field of this command option. + /// The current builder. + /// Thrown if is null. + /// Thrown if any dictionary key is an invalid locale string. + public SlashCommandOptionBuilder WithNameLocalizations(IDictionary nameLocalizations) + { + if (nameLocalizations is null) + throw new ArgumentNullException(nameof(nameLocalizations)); + + foreach (var (locale, name) in nameLocalizations) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandOptionName(name); + } + + _nameLocalizations = new Dictionary(nameLocalizations); + return this; + } + + /// + /// Sets the collection. + /// + /// The localization dictionary to use for the description field of this command option. + /// The current builder. + /// Thrown if is null. + /// Thrown if any dictionary key is an invalid locale string. + public SlashCommandOptionBuilder WithDescriptionLocalizations(IDictionary descriptionLocalizations) + { + if (descriptionLocalizations is null) + throw new ArgumentNullException(nameof(descriptionLocalizations)); + + foreach (var (locale, description) in _descriptionLocalizations) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandOptionDescription(description); + } + + _descriptionLocalizations = new Dictionary(descriptionLocalizations); + return this; + } + + /// + /// Adds a new entry to the collection. + /// + /// Locale of the entry. + /// Localized string for the name field. + /// The current builder. + /// Thrown if is an invalid locale string. + public SlashCommandOptionBuilder AddNameLocalization(string locale, string name) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandOptionName(name); + + _descriptionLocalizations ??= new(); + _nameLocalizations.Add(locale, name); + + return this; + } + + /// + /// Adds a new entry to the collection. + /// + /// Locale of the entry. + /// Localized string for the description field. + /// The current builder. + /// Thrown if is an invalid locale string. + public SlashCommandOptionBuilder AddDescriptionLocalization(string locale, string description) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandOptionDescription(description); + + _descriptionLocalizations ??= new(); + _descriptionLocalizations.Add(locale, description); + + return this; + } + + private static void EnsureValidCommandOptionName(string name) + { + Preconditions.AtLeast(name.Length, 1, nameof(name)); + Preconditions.AtMost(name.Length, SlashCommandBuilder.MaxNameLength, nameof(name)); + if (!Regex.IsMatch(name, @"^[\w-]{1,32}$")) + throw new ArgumentException("Option name cannot contain any special characters or whitespaces!", nameof(name)); + } + + private static void EnsureValidCommandOptionDescription(string description) + { + Preconditions.AtLeast(description.Length, 1, nameof(description)); + Preconditions.AtMost(description.Length, SlashCommandBuilder.MaxDescriptionLength, nameof(description)); + } } } diff --git a/src/Discord.Net.Core/Extensions/GenericCollectionExtensions.cs b/src/Discord.Net.Core/Extensions/GenericCollectionExtensions.cs new file mode 100644 index 000000000..75d81d292 --- /dev/null +++ b/src/Discord.Net.Core/Extensions/GenericCollectionExtensions.cs @@ -0,0 +1,15 @@ +using System.Linq; + +namespace System.Collections.Generic; + +internal static class GenericCollectionExtensions +{ + public static void Deconstruct(this KeyValuePair kvp, out T1 value1, out T2 value2) + { + value1 = kvp.Key; + value2 = kvp.Value; + } + + public static Dictionary ToDictionary(this IEnumerable> kvp) => + kvp.ToDictionary(x => x.Key, x => x.Value); +} diff --git a/src/Discord.Net.Core/IDiscordClient.cs b/src/Discord.Net.Core/IDiscordClient.cs index 14e156769..dd1da3ae3 100644 --- a/src/Discord.Net.Core/IDiscordClient.cs +++ b/src/Discord.Net.Core/IDiscordClient.cs @@ -155,12 +155,14 @@ namespace Discord /// /// Gets a collection of all global commands. /// + /// Whether to include full localization dictionaries in the returned objects, instead of the name localized and description localized fields. + /// The target locale of the localized name and description fields. Sets X-Discord-Locale header, which takes precedence over Accept-Language. /// The options to be used when sending the request. /// /// A task that represents the asynchronous get operation. The task result contains a read-only collection of global /// application commands. /// - Task> GetGlobalApplicationCommandsAsync(RequestOptions options = null); + Task> GetGlobalApplicationCommandsAsync(bool withLocalizations = false, string locale = null, RequestOptions options = null); /// /// Creates a global application command. diff --git a/src/Discord.Net.Core/Net/Rest/IRestClient.cs b/src/Discord.Net.Core/Net/Rest/IRestClient.cs index 71010f70d..d28fb707e 100644 --- a/src/Discord.Net.Core/Net/Rest/IRestClient.cs +++ b/src/Discord.Net.Core/Net/Rest/IRestClient.cs @@ -30,9 +30,13 @@ namespace Discord.Net.Rest /// The cancellation token used to cancel the task. /// Indicates whether to send the header only. /// The audit log reason. + /// Additional headers to be sent with the request. /// - Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly = false, string reason = null); - Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly = false, string reason = null); - Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly = false, string reason = null); + Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly = false, string reason = null, + IEnumerable>> requestHeaders = null); + Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly = false, string reason = null, + IEnumerable>> requestHeaders = null); + Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly = false, string reason = null, + IEnumerable>> requestHeaders = null); } } diff --git a/src/Discord.Net.Core/RequestOptions.cs b/src/Discord.Net.Core/RequestOptions.cs index 46aa2681f..ef8dbf756 100644 --- a/src/Discord.Net.Core/RequestOptions.cs +++ b/src/Discord.Net.Core/RequestOptions.cs @@ -1,5 +1,6 @@ using Discord.Net; using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -19,7 +20,7 @@ namespace Discord /// Gets or sets the maximum time to wait for this request to complete. /// /// - /// Gets or set the max time, in milliseconds, to wait for this request to complete. If + /// Gets or set the max time, in milliseconds, to wait for this request to complete. If /// null, a request will not time out. If a rate limit has been triggered for this request's bucket /// and will not be unpaused in time, this request will fail immediately. /// @@ -53,7 +54,7 @@ namespace Discord /// /// /// This property can also be set in . - /// On a per-request basis, the system clock should only be disabled + /// On a per-request basis, the system clock should only be disabled /// when millisecond precision is especially important, and the /// hosting system is known to have a desynced clock. /// @@ -70,8 +71,10 @@ namespace Discord internal bool IsReactionBucket { get; set; } internal bool IsGatewayBucket { get; set; } + internal IDictionary> RequestHeaders { get; } + internal static RequestOptions CreateOrClone(RequestOptions options) - { + { if (options == null) return new RequestOptions(); else @@ -96,8 +99,9 @@ namespace Discord public RequestOptions() { Timeout = DiscordConfig.DefaultRequestTimeout; + RequestHeaders = new Dictionary>(); } - + public RequestOptions Clone() => MemberwiseClone() as RequestOptions; } } diff --git a/src/Discord.Net.Core/Utils/Preconditions.cs b/src/Discord.Net.Core/Utils/Preconditions.cs index 2f24e660d..fb855f925 100644 --- a/src/Discord.Net.Core/Utils/Preconditions.cs +++ b/src/Discord.Net.Core/Utils/Preconditions.cs @@ -55,7 +55,7 @@ namespace Discord if (obj.Value == null) throw CreateNotNullException(name, msg); if (obj.Value.Trim().Length == 0) throw CreateNotEmptyException(name, msg); } - } + } private static ArgumentException CreateNotEmptyException(string name, string msg) => new ArgumentException(message: msg ?? "Argument cannot be blank.", paramName: name); @@ -129,7 +129,7 @@ namespace Discord private static ArgumentException CreateNotEqualException(string name, string msg, T value) => new ArgumentException(message: msg ?? $"Value may not be equal to {value}.", paramName: name); - + /// Value must be at least . public static void AtLeast(sbyte obj, sbyte value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } /// Value must be at least . @@ -165,7 +165,7 @@ namespace Discord private static ArgumentException CreateAtLeastException(string name, string msg, T value) => new ArgumentException(message: msg ?? $"Value must be at least {value}.", paramName: name); - + /// Value must be greater than . public static void GreaterThan(sbyte obj, sbyte value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } /// Value must be greater than . @@ -201,7 +201,7 @@ namespace Discord private static ArgumentException CreateGreaterThanException(string name, string msg, T value) => new ArgumentException(message: msg ?? $"Value must be greater than {value}.", paramName: name); - + /// Value must be at most . public static void AtMost(sbyte obj, sbyte value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } /// Value must be at most . @@ -237,7 +237,7 @@ namespace Discord private static ArgumentException CreateAtMostException(string name, string msg, T value) => new ArgumentException(message: msg ?? $"Value must be at most {value}.", paramName: name); - + /// Value must be less than . public static void LessThan(sbyte obj, sbyte value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } /// Value must be less than . diff --git a/src/Discord.Net.Interactions/InteractionService.cs b/src/Discord.Net.Interactions/InteractionService.cs index 793d89cdc..50c1f5546 100644 --- a/src/Discord.Net.Interactions/InteractionService.cs +++ b/src/Discord.Net.Interactions/InteractionService.cs @@ -83,6 +83,11 @@ namespace Discord.Interactions public event Func ModalCommandExecuted { add { _modalCommandExecutedEvent.Add(value); } remove { _modalCommandExecutedEvent.Remove(value); } } internal readonly AsyncEvent> _modalCommandExecutedEvent = new(); + /// + /// Get the used by this Interaction Service instance to localize strings. + /// + public ILocalizationManager LocalizationManager { get; set; } + private readonly ConcurrentDictionary _typedModuleDefs; private readonly CommandMap _slashCommandMap; private readonly ConcurrentDictionary> _contextCommandMaps; @@ -203,6 +208,7 @@ namespace Discord.Interactions _enableAutocompleteHandlers = config.EnableAutocompleteHandlers; _autoServiceScopes = config.AutoServiceScopes; _restResponseCallback = config.RestResponseCallback; + LocalizationManager = config.LocalizationManager; _typeConverterMap = new TypeMap(this, new ConcurrentDictionary { diff --git a/src/Discord.Net.Interactions/InteractionServiceConfig.cs b/src/Discord.Net.Interactions/InteractionServiceConfig.cs index b6576a49f..b9102bc5f 100644 --- a/src/Discord.Net.Interactions/InteractionServiceConfig.cs +++ b/src/Discord.Net.Interactions/InteractionServiceConfig.cs @@ -64,6 +64,11 @@ namespace Discord.Interactions /// Gets or sets whether a command execution should exit when a modal command encounters a missing modal component value. /// public bool ExitOnMissingModalField { get; set; } = false; + + /// + /// Localization provider to be used when registering application commands. + /// + public ILocalizationManager LocalizationManager { get; set; } } /// diff --git a/src/Discord.Net.Interactions/LocalizationManagers/ILocalizationManager.cs b/src/Discord.Net.Interactions/LocalizationManagers/ILocalizationManager.cs new file mode 100644 index 000000000..13b155292 --- /dev/null +++ b/src/Discord.Net.Interactions/LocalizationManagers/ILocalizationManager.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + /// + /// Respresents a localization provider for Discord Application Commands. + /// + public interface ILocalizationManager + { + /// + /// Get every the resource name for every available locale. + /// + /// Location of the resource. + /// Type of the resource. + /// + /// A dictionary containing every available locale and the resource name. + /// + IDictionary GetAllNames(IList key, LocalizationTarget destinationType); + + /// + /// Get every the resource description for every available locale. + /// + /// Location of the resource. + /// Type of the resource. + /// + /// A dictionary containing every available locale and the resource name. + /// + IDictionary GetAllDescriptions(IList key, LocalizationTarget destinationType); + } +} diff --git a/src/Discord.Net.Interactions/LocalizationManagers/JsonLocalizationManager.cs b/src/Discord.Net.Interactions/LocalizationManagers/JsonLocalizationManager.cs new file mode 100644 index 000000000..010fb3bdd --- /dev/null +++ b/src/Discord.Net.Interactions/LocalizationManagers/JsonLocalizationManager.cs @@ -0,0 +1,72 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + /// + /// The default localization provider for Json resource files. + /// + public sealed class JsonLocalizationManager : ILocalizationManager + { + private const string NameIdentifier = "name"; + private const string DescriptionIdentifier = "description"; + private const string SpaceToken = "~"; + + private readonly string _basePath; + private readonly string _fileName; + private readonly Regex _localeParserRegex = new Regex(@"\w+.(?\w{2}(?:-\w{2})?).json", RegexOptions.Compiled | RegexOptions.Singleline); + + /// + /// Initializes a new instance of the class. + /// + /// Base path of the Json file. + /// Name of the Json file. + public JsonLocalizationManager(string basePath, string fileName) + { + _basePath = basePath; + _fileName = fileName; + } + + /// + public IDictionary GetAllDescriptions(IList key, LocalizationTarget destinationType) => + GetValues(key, DescriptionIdentifier); + + /// + public IDictionary GetAllNames(IList key, LocalizationTarget destinationType) => + GetValues(key, NameIdentifier); + + private string[] GetAllFiles() => + Directory.GetFiles(_basePath, $"{_fileName}.*.json", SearchOption.TopDirectoryOnly); + + private IDictionary GetValues(IList key, string identifier) + { + var result = new Dictionary(); + var files = GetAllFiles(); + + foreach (var file in files) + { + var match = _localeParserRegex.Match(Path.GetFileName(file)); + if (!match.Success) + continue; + + var locale = match.Groups["locale"].Value; + + using var sr = new StreamReader(file); + using var jr = new JsonTextReader(sr); + var obj = JObject.Load(jr); + var token = string.Join(".", key.Select(x => $"['{x}']")) + $".{identifier}"; + var value = (string)obj.SelectToken(token); + if (value is not null) + result[locale] = value; + } + + return result; + } + } +} diff --git a/src/Discord.Net.Interactions/LocalizationManagers/ResxLocalizationManager.cs b/src/Discord.Net.Interactions/LocalizationManagers/ResxLocalizationManager.cs new file mode 100644 index 000000000..a110602f2 --- /dev/null +++ b/src/Discord.Net.Interactions/LocalizationManagers/ResxLocalizationManager.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; +using System.Resources; + +namespace Discord.Interactions +{ + /// + /// The default localization provider for Resx files. + /// + public sealed class ResxLocalizationManager : ILocalizationManager + { + private const string NameIdentifier = "name"; + private const string DescriptionIdentifier = "description"; + + private readonly ResourceManager _resourceManager; + private readonly IEnumerable _supportedLocales; + + /// + /// Initializes a new instance of the class. + /// + /// Name of the base resource. + /// The main assembly for the resources. + /// Cultures the should search for. + public ResxLocalizationManager(string baseResource, Assembly assembly, params CultureInfo[] supportedLocales) + { + _supportedLocales = supportedLocales; + _resourceManager = new ResourceManager(baseResource, assembly); + } + + /// + public IDictionary GetAllDescriptions(IList key, LocalizationTarget destinationType) => + GetValues(key, DescriptionIdentifier); + + /// + public IDictionary GetAllNames(IList key, LocalizationTarget destinationType) => + GetValues(key, NameIdentifier); + + private IDictionary GetValues(IList key, string identifier) + { + var entryKey = (string.Join(".", key) + "." + identifier); + + var result = new Dictionary(); + + foreach (var locale in _supportedLocales) + { + var value = _resourceManager.GetString(entryKey, locale); + if (value is not null) + result[locale.Name] = value; + } + + return result; + } + } +} diff --git a/src/Discord.Net.Interactions/LocalizationTarget.cs b/src/Discord.Net.Interactions/LocalizationTarget.cs new file mode 100644 index 000000000..cf54d3375 --- /dev/null +++ b/src/Discord.Net.Interactions/LocalizationTarget.cs @@ -0,0 +1,25 @@ +namespace Discord.Interactions +{ + /// + /// Resource targets for localization. + /// + public enum LocalizationTarget + { + /// + /// Target is a tagged with a . + /// + Group, + /// + /// Target is an application command method. + /// + Command, + /// + /// Target is a Slash Command parameter. + /// + Parameter, + /// + /// Target is a Slash Command parameter choice. + /// + Choice + } +} diff --git a/src/Discord.Net.Interactions/Map/CommandMap.cs b/src/Discord.Net.Interactions/Map/CommandMap.cs index 2e7bf5368..336e2b1ec 100644 --- a/src/Discord.Net.Interactions/Map/CommandMap.cs +++ b/src/Discord.Net.Interactions/Map/CommandMap.cs @@ -42,7 +42,7 @@ namespace Discord.Interactions public void RemoveCommand(T command) { - var key = ParseCommandName(command); + var key = CommandHierarchy.GetCommandPath(command); _root.RemoveCommand(key, 0); } @@ -60,28 +60,9 @@ namespace Discord.Interactions private void AddCommand(T command) { - var key = ParseCommandName(command); + var key = CommandHierarchy.GetCommandPath(command); _root.AddCommand(key, 0, command); } - - private IList ParseCommandName(T command) - { - var keywords = new List() { command.Name }; - - var currentParent = command.Module; - - while (currentParent != null) - { - if (!string.IsNullOrEmpty(currentParent.SlashGroupName)) - keywords.Add(currentParent.SlashGroupName); - - currentParent = currentParent.Parent; - } - - keywords.Reverse(); - - return keywords; - } } } diff --git a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs index 409c0e796..9b507f1bb 100644 --- a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs +++ b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; namespace Discord.Interactions @@ -9,6 +10,9 @@ namespace Discord.Interactions #region Parameters public static ApplicationCommandOptionProperties ToApplicationCommandOptionProps(this SlashCommandParameterInfo parameterInfo) { + var localizationManager = parameterInfo.Command.Module.CommandService.LocalizationManager; + var parameterPath = parameterInfo.GetParameterPath(); + var props = new ApplicationCommandOptionProperties { Name = parameterInfo.Name, @@ -18,12 +22,15 @@ namespace Discord.Interactions Choices = parameterInfo.Choices?.Select(x => new ApplicationCommandOptionChoiceProperties { Name = x.Name, - Value = x.Value + Value = x.Value, + NameLocalizations = localizationManager?.GetAllNames(parameterInfo.GetChoicePath(x), LocalizationTarget.Choice) ?? ImmutableDictionary.Empty })?.ToList(), ChannelTypes = parameterInfo.ChannelTypes?.ToList(), IsAutocomplete = parameterInfo.IsAutocomplete, MaxValue = parameterInfo.MaxValue, MinValue = parameterInfo.MinValue, + NameLocalizations = localizationManager?.GetAllNames(parameterPath, LocalizationTarget.Parameter) ?? ImmutableDictionary.Empty, + DescriptionLocalizations = localizationManager?.GetAllDescriptions(parameterPath, LocalizationTarget.Parameter) ?? ImmutableDictionary.Empty, MinLength = parameterInfo.MinLength, MaxLength = parameterInfo.MaxLength, }; @@ -38,13 +45,19 @@ namespace Discord.Interactions public static SlashCommandProperties ToApplicationCommandProps(this SlashCommandInfo commandInfo) { + var commandPath = commandInfo.GetCommandPath(); + var localizationManager = commandInfo.Module.CommandService.LocalizationManager; + var props = new SlashCommandBuilder() { Name = commandInfo.Name, Description = commandInfo.Description, + IsDefaultPermission = commandInfo.DefaultPermission, IsDMEnabled = commandInfo.IsEnabledInDm, DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), - }.Build(); + }.WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary.Empty) + .WithDescriptionLocalizations(localizationManager?.GetAllDescriptions(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary.Empty) + .Build(); if (commandInfo.Parameters.Count > SlashCommandBuilder.MaxOptionsCount) throw new InvalidOperationException($"Slash Commands cannot have more than {SlashCommandBuilder.MaxOptionsCount} command parameters"); @@ -54,18 +67,30 @@ namespace Discord.Interactions return props; } - public static ApplicationCommandOptionProperties ToApplicationCommandOptionProps(this SlashCommandInfo commandInfo) => - new ApplicationCommandOptionProperties + public static ApplicationCommandOptionProperties ToApplicationCommandOptionProps(this SlashCommandInfo commandInfo) + { + var localizationManager = commandInfo.Module.CommandService.LocalizationManager; + var commandPath = commandInfo.GetCommandPath(); + + return new ApplicationCommandOptionProperties { Name = commandInfo.Name, Description = commandInfo.Description, Type = ApplicationCommandOptionType.SubCommand, IsRequired = false, - Options = commandInfo.FlattenedParameters?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() + Options = commandInfo.FlattenedParameters?.Select(x => x.ToApplicationCommandOptionProps()) + ?.ToList(), + NameLocalizations = localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary.Empty, + DescriptionLocalizations = localizationManager?.GetAllDescriptions(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary.Empty }; + } public static ApplicationCommandProperties ToApplicationCommandProps(this ContextCommandInfo commandInfo) - => commandInfo.CommandType switch + { + var localizationManager = commandInfo.Module.CommandService.LocalizationManager; + var commandPath = commandInfo.GetCommandPath(); + + return commandInfo.CommandType switch { ApplicationCommandType.Message => new MessageCommandBuilder { @@ -73,16 +98,21 @@ namespace Discord.Interactions IsDefaultPermission = commandInfo.DefaultPermission, DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), IsDMEnabled = commandInfo.IsEnabledInDm - }.Build(), + } + .WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary.Empty) + .Build(), ApplicationCommandType.User => new UserCommandBuilder { Name = commandInfo.Name, IsDefaultPermission = commandInfo.DefaultPermission, DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), IsDMEnabled = commandInfo.IsEnabledInDm - }.Build(), + } + .WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary.Empty) + .Build(), _ => throw new InvalidOperationException($"{commandInfo.CommandType} isn't a supported command type.") }; + } #endregion #region Modules @@ -123,6 +153,9 @@ namespace Discord.Interactions options.AddRange(moduleInfo.SubModules?.SelectMany(x => x.ParseSubModule(args, ignoreDontRegister))); + var localizationManager = moduleInfo.CommandService.LocalizationManager; + var modulePath = moduleInfo.GetModulePath(); + var props = new SlashCommandBuilder { Name = moduleInfo.SlashGroupName, @@ -130,7 +163,10 @@ namespace Discord.Interactions IsDefaultPermission = moduleInfo.DefaultPermission, IsDMEnabled = moduleInfo.IsEnabledInDm, DefaultMemberPermissions = moduleInfo.DefaultMemberPermissions - }.Build(); + } + .WithNameLocalizations(localizationManager?.GetAllNames(modulePath, LocalizationTarget.Group) ?? ImmutableDictionary.Empty) + .WithDescriptionLocalizations(localizationManager?.GetAllDescriptions(modulePath, LocalizationTarget.Group) ?? ImmutableDictionary.Empty) + .Build(); if (options.Count > SlashCommandBuilder.MaxOptionsCount) throw new InvalidOperationException($"Slash Commands cannot have more than {SlashCommandBuilder.MaxOptionsCount} command parameters"); @@ -168,7 +204,11 @@ namespace Discord.Interactions Name = moduleInfo.SlashGroupName, Description = moduleInfo.Description, Type = ApplicationCommandOptionType.SubCommandGroup, - Options = options + Options = options, + NameLocalizations = moduleInfo.CommandService.LocalizationManager?.GetAllNames(moduleInfo.GetModulePath(), LocalizationTarget.Group) + ?? ImmutableDictionary.Empty, + DescriptionLocalizations = moduleInfo.CommandService.LocalizationManager?.GetAllDescriptions(moduleInfo.GetModulePath(), LocalizationTarget.Group) + ?? ImmutableDictionary.Empty, } }; } @@ -183,17 +223,29 @@ namespace Discord.Interactions Name = command.Name, Description = command.Description, IsDefaultPermission = command.IsDefaultPermission, - Options = command.Options?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional>.Unspecified + DefaultMemberPermissions = (GuildPermission)command.DefaultMemberPermissions.RawValue, + IsDMEnabled = command.IsEnabledInDm, + Options = command.Options?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional>.Unspecified, + NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary.Empty, + DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary.Empty, }, ApplicationCommandType.User => new UserCommandProperties { Name = command.Name, - IsDefaultPermission = command.IsDefaultPermission + IsDefaultPermission = command.IsDefaultPermission, + DefaultMemberPermissions = (GuildPermission)command.DefaultMemberPermissions.RawValue, + IsDMEnabled = command.IsEnabledInDm, + NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary.Empty, + DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary.Empty }, ApplicationCommandType.Message => new MessageCommandProperties { Name = command.Name, - IsDefaultPermission = command.IsDefaultPermission + IsDefaultPermission = command.IsDefaultPermission, + DefaultMemberPermissions = (GuildPermission)command.DefaultMemberPermissions.RawValue, + IsDMEnabled = command.IsEnabledInDm, + NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary.Empty, + DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary.Empty }, _ => throw new InvalidOperationException($"Cannot create command properties for command type {command.Type}"), }; @@ -206,18 +258,20 @@ namespace Discord.Interactions Description = commandOption.Description, Type = commandOption.Type, IsRequired = commandOption.IsRequired, + ChannelTypes = commandOption.ChannelTypes?.ToList(), + IsAutocomplete = commandOption.IsAutocomplete.GetValueOrDefault(), + MinValue = commandOption.MinValue, + MaxValue = commandOption.MaxValue, Choices = commandOption.Choices?.Select(x => new ApplicationCommandOptionChoiceProperties { Name = x.Name, Value = x.Value }).ToList(), Options = commandOption.Options?.Select(x => x.ToApplicationCommandOptionProps()).ToList(), + NameLocalizations = commandOption.NameLocalizations?.ToImmutableDictionary(), + DescriptionLocalizations = commandOption.DescriptionLocalizations?.ToImmutableDictionary(), MaxLength = commandOption.MaxLength, MinLength = commandOption.MinLength, - MaxValue = commandOption.MaxValue, - MinValue = commandOption.MinValue, - IsAutocomplete = commandOption.IsAutocomplete.GetValueOrDefault(), - ChannelTypes = commandOption.ChannelTypes.ToList(), }; public static Modal ToModal(this ModalInfo modalInfo, string customId, Action modifyModal = null) diff --git a/src/Discord.Net.Interactions/Utilities/CommandHierarchy.cs b/src/Discord.Net.Interactions/Utilities/CommandHierarchy.cs new file mode 100644 index 000000000..a4554eaef --- /dev/null +++ b/src/Discord.Net.Interactions/Utilities/CommandHierarchy.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; + +namespace Discord.Interactions +{ + internal static class CommandHierarchy + { + public const char EscapeChar = '$'; + + public static IList GetModulePath(this ModuleInfo moduleInfo) + { + var result = new List(); + + var current = moduleInfo; + while (current is not null) + { + if (current.IsSlashGroup) + result.Insert(0, current.SlashGroupName); + + current = current.Parent; + } + + return result; + } + + public static IList GetCommandPath(this ICommandInfo commandInfo) + { + if (commandInfo.IgnoreGroupNames) + return new string[] { commandInfo.Name }; + + var path = commandInfo.Module.GetModulePath(); + path.Add(commandInfo.Name); + return path; + } + + public static IList GetParameterPath(this IParameterInfo parameterInfo) + { + var path = parameterInfo.Command.GetCommandPath(); + path.Add(parameterInfo.Name); + return path; + } + + public static IList GetChoicePath(this IParameterInfo parameterInfo, ParameterChoice choice) + { + var path = parameterInfo.GetParameterPath(); + path.Add(choice.Name); + return path; + } + + public static IList GetTypePath(Type type) => + new string[] { EscapeChar + type.FullName }; + } +} diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs index 8b84149dd..e46369277 100644 --- a/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using System.Collections.Generic; namespace Discord.API { @@ -25,6 +26,18 @@ namespace Discord.API [JsonProperty("default_permission")] public Optional DefaultPermissions { get; set; } + [JsonProperty("name_localizations")] + public Optional> NameLocalizations { get; set; } + + [JsonProperty("description_localizations")] + public Optional> DescriptionLocalizations { get; set; } + + [JsonProperty("name_localized")] + public Optional NameLocalized { get; set; } + + [JsonProperty("description_localized")] + public Optional DescriptionLocalized { get; set; } + // V2 Permissions [JsonProperty("dm_permission")] public Optional DmPermission { get; set; } diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs index fff5730f4..fb64d5ebe 100644 --- a/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using System.Collections.Generic; using System.Linq; namespace Discord.API @@ -38,6 +39,18 @@ namespace Discord.API [JsonProperty("channel_types")] public Optional ChannelTypes { get; set; } + [JsonProperty("name_localizations")] + public Optional> NameLocalizations { get; set; } + + [JsonProperty("description_localizations")] + public Optional> DescriptionLocalizations { get; set; } + + [JsonProperty("name_localized")] + public Optional NameLocalized { get; set; } + + [JsonProperty("description_localized")] + public Optional DescriptionLocalized { get; set; } + [JsonProperty("min_length")] public Optional MinLength { get; set; } @@ -69,6 +82,11 @@ namespace Discord.API Name = cmd.Name; Type = cmd.Type; Description = cmd.Description; + + NameLocalizations = cmd.NameLocalizations?.ToDictionary() ?? Optional>.Unspecified; + DescriptionLocalizations = cmd.DescriptionLocalizations?.ToDictionary() ?? Optional>.Unspecified; + NameLocalized = cmd.NameLocalized; + DescriptionLocalized = cmd.DescriptionLocalized; } public ApplicationCommandOption(ApplicationCommandOptionProperties option) { @@ -94,6 +112,9 @@ namespace Discord.API Type = option.Type; Description = option.Description; Autocomplete = option.IsAutocomplete; + + NameLocalizations = option.NameLocalizations?.ToDictionary() ?? Optional>.Unspecified; + DescriptionLocalizations = option.DescriptionLocalizations?.ToDictionary() ?? Optional>.Unspecified; } } } diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommandOptionChoice.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommandOptionChoice.cs index 6f84437f6..966405cc9 100644 --- a/src/Discord.Net.Rest/API/Common/ApplicationCommandOptionChoice.cs +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommandOptionChoice.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using System.Collections.Generic; namespace Discord.API { @@ -9,5 +10,11 @@ namespace Discord.API [JsonProperty("value")] public object Value { get; set; } + + [JsonProperty("name_localizations")] + public Optional> NameLocalizations { get; set; } + + [JsonProperty("name_localized")] + public Optional NameLocalized { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs b/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs index 7ae8718b6..2257d4b97 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs @@ -1,4 +1,8 @@ using Newtonsoft.Json; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; namespace Discord.API.Rest { @@ -19,6 +23,12 @@ namespace Discord.API.Rest [JsonProperty("default_permission")] public Optional DefaultPermission { get; set; } + [JsonProperty("name_localizations")] + public Optional> NameLocalizations { get; set; } + + [JsonProperty("description_localizations")] + public Optional> DescriptionLocalizations { get; set; } + [JsonProperty("dm_permission")] public Optional DmPermission { get; set; } @@ -26,12 +36,15 @@ namespace Discord.API.Rest public Optional DefaultMemberPermission { get; set; } public CreateApplicationCommandParams() { } - public CreateApplicationCommandParams(string name, string description, ApplicationCommandType type, ApplicationCommandOption[] options = null) + public CreateApplicationCommandParams(string name, string description, ApplicationCommandType type, ApplicationCommandOption[] options = null, + IDictionary nameLocalizations = null, IDictionary descriptionLocalizations = null) { Name = name; Description = description; Options = Optional.Create(options); Type = type; + NameLocalizations = nameLocalizations?.ToDictionary(x => x.Key, x => x.Value) ?? Optional>.Unspecified; + DescriptionLocalizations = descriptionLocalizations?.ToDictionary(x => x.Key, x => x.Value) ?? Optional>.Unspecified; } } } diff --git a/src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs index 5891c2c28..f49a3f33d 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using System.Collections.Generic; namespace Discord.API.Rest { @@ -15,5 +16,11 @@ namespace Discord.API.Rest [JsonProperty("default_permission")] public Optional DefaultPermission { get; set; } + + [JsonProperty("name_localizations")] + public Optional> NameLocalizations { get; set; } + + [JsonProperty("description_localizations")] + public Optional> DescriptionLocalizations { get; set; } } } diff --git a/src/Discord.Net.Rest/BaseDiscordClient.cs b/src/Discord.Net.Rest/BaseDiscordClient.cs index af43e9f4e..686c7b030 100644 --- a/src/Discord.Net.Rest/BaseDiscordClient.cs +++ b/src/Discord.Net.Rest/BaseDiscordClient.cs @@ -243,7 +243,7 @@ namespace Discord.Rest => Task.FromResult(null); /// - Task> IDiscordClient.GetGlobalApplicationCommandsAsync(RequestOptions options) + Task> IDiscordClient.GetGlobalApplicationCommandsAsync(bool withLocalizations, string locale, RequestOptions options) => Task.FromResult>(ImmutableArray.Create()); Task IDiscordClient.CreateGlobalApplicationCommand(ApplicationCommandProperties properties, RequestOptions options) => Task.FromResult(null); diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs index c6ad6a9fb..0c8f8c42f 100644 --- a/src/Discord.Net.Rest/ClientHelper.cs +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -194,10 +194,10 @@ namespace Discord.Rest }; } - public static async Task> GetGlobalApplicationCommandsAsync(BaseDiscordClient client, - RequestOptions options = null) + public static async Task> GetGlobalApplicationCommandsAsync(BaseDiscordClient client, bool withLocalizations = false, + string locale = null, RequestOptions options = null) { - var response = await client.ApiClient.GetGlobalApplicationCommandsAsync(options).ConfigureAwait(false); + var response = await client.ApiClient.GetGlobalApplicationCommandsAsync(withLocalizations, locale, options).ConfigureAwait(false); if (!response.Any()) return Array.Empty(); @@ -212,10 +212,10 @@ namespace Discord.Rest return model != null ? RestGlobalCommand.Create(client, model) : null; } - public static async Task> GetGuildApplicationCommandsAsync(BaseDiscordClient client, ulong guildId, - RequestOptions options = null) + public static async Task> GetGuildApplicationCommandsAsync(BaseDiscordClient client, ulong guildId, bool withLocalizations = false, + string locale = null, RequestOptions options = null) { - var response = await client.ApiClient.GetGuildApplicationCommandsAsync(guildId, options).ConfigureAwait(false); + var response = await client.ApiClient.GetGuildApplicationCommandsAsync(guildId, withLocalizations, locale, options).ConfigureAwait(false); if (!response.Any()) return ImmutableArray.Create(); diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index c5b075103..eb1737c6f 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -8,6 +8,7 @@ using Newtonsoft.Json; using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.ComponentModel.Design; using System.Diagnostics; using System.Globalization; using System.IO; @@ -1212,11 +1213,22 @@ namespace Discord.API #endregion #region Interactions - public async Task GetGlobalApplicationCommandsAsync(RequestOptions options = null) + public async Task GetGlobalApplicationCommandsAsync(bool withLocalizations = false, string locale = null, RequestOptions options = null) { options = RequestOptions.CreateOrClone(options); - return await SendAsync("GET", () => $"applications/{CurrentApplicationId}/commands", new BucketIds(), options: options).ConfigureAwait(false); + if (locale is not null) + { + if (!System.Text.RegularExpressions.Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"{locale} is not a valid locale.", nameof(locale)); + + options.RequestHeaders["X-Discord-Locale"] = new[] { locale }; + } + + //with_localizations=false doesnt return localized names and descriptions + var query = withLocalizations ? "?with_localizations=true" : string.Empty; + return await SendAsync("GET", () => $"applications/{CurrentApplicationId}/commands{query}", + new BucketIds(), options: options).ConfigureAwait(false); } public async Task GetGlobalApplicationCommandAsync(ulong id, RequestOptions options = null) @@ -1281,13 +1293,24 @@ namespace Discord.API return await SendJsonAsync("PUT", () => $"applications/{CurrentApplicationId}/commands", commands, new BucketIds(), options: options).ConfigureAwait(false); } - public async Task GetGuildApplicationCommandsAsync(ulong guildId, RequestOptions options = null) + public async Task GetGuildApplicationCommandsAsync(ulong guildId, bool withLocalizations = false, string locale = null, RequestOptions options = null) { options = RequestOptions.CreateOrClone(options); var bucket = new BucketIds(guildId: guildId); - return await SendAsync("GET", () => $"applications/{CurrentApplicationId}/guilds/{guildId}/commands", bucket, options: options).ConfigureAwait(false); + if (locale is not null) + { + if (!System.Text.RegularExpressions.Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"{locale} is not a valid locale.", nameof(locale)); + + options.RequestHeaders["X-Discord-Locale"] = new[] { locale }; + } + + //with_localizations=false doesnt return localized names and descriptions + var query = withLocalizations ? "?with_localizations=true" : string.Empty; + return await SendAsync("GET", () => $"applications/{CurrentApplicationId}/guilds/{guildId}/commands{query}", + bucket, options: options).ConfigureAwait(false); } public async Task GetGuildApplicationCommandAsync(ulong guildId, ulong commandId, RequestOptions options = null) diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index daf7287c7..ddd38c5be 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -25,7 +25,7 @@ namespace Discord.Rest /// Gets the logged-in user. /// public new RestSelfUser CurrentUser { get => base.CurrentUser as RestSelfUser; internal set => base.CurrentUser = value; } - + /// public DiscordRestClient() : this(new DiscordRestConfig()) { } /// @@ -205,10 +205,10 @@ namespace Discord.Rest => ClientHelper.CreateGlobalApplicationCommandAsync(this, properties, options); public Task CreateGuildCommand(ApplicationCommandProperties properties, ulong guildId, RequestOptions options = null) => ClientHelper.CreateGuildApplicationCommandAsync(this, guildId, properties, options); - public Task> GetGlobalApplicationCommands(RequestOptions options = null) - => ClientHelper.GetGlobalApplicationCommandsAsync(this, options); - public Task> GetGuildApplicationCommands(ulong guildId, RequestOptions options = null) - => ClientHelper.GetGuildApplicationCommandsAsync(this, guildId, options); + public Task> GetGlobalApplicationCommands(bool withLocalizations = false, string locale = null, RequestOptions options = null) + => ClientHelper.GetGlobalApplicationCommandsAsync(this, withLocalizations, locale, options); + public Task> GetGuildApplicationCommands(ulong guildId, bool withLocalizations = false, string locale = null, RequestOptions options = null) + => ClientHelper.GetGuildApplicationCommandsAsync(this, guildId, withLocalizations, locale, options); public Task> BulkOverwriteGlobalCommands(ApplicationCommandProperties[] commandProperties, RequestOptions options = null) => ClientHelper.BulkOverwriteGlobalApplicationCommandAsync(this, commandProperties, options); public Task> BulkOverwriteGuildCommands(ApplicationCommandProperties[] commandProperties, ulong guildId, RequestOptions options = null) @@ -319,8 +319,8 @@ namespace Discord.Rest => await GetWebhookAsync(id, options).ConfigureAwait(false); /// - async Task> IDiscordClient.GetGlobalApplicationCommandsAsync(RequestOptions options) - => await GetGlobalApplicationCommands(options).ConfigureAwait(false); + async Task> IDiscordClient.GetGlobalApplicationCommandsAsync(bool withLocalizations, string locale, RequestOptions options) + => await GetGlobalApplicationCommands(withLocalizations, locale, options).ConfigureAwait(false); /// async Task IDiscordClient.GetGlobalApplicationCommandAsync(ulong id, RequestOptions options) => await ClientHelper.GetGlobalApplicationCommandAsync(this, id, options).ConfigureAwait(false); diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 8195a2cea..c4e3764d1 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -362,10 +362,10 @@ namespace Discord.Rest #endregion #region Interactions - public static async Task> GetSlashCommandsAsync(IGuild guild, BaseDiscordClient client, - RequestOptions options) + public static async Task> GetSlashCommandsAsync(IGuild guild, BaseDiscordClient client, bool withLocalizations, + string locale, RequestOptions options) { - var models = await client.ApiClient.GetGuildApplicationCommandsAsync(guild.Id, options); + var models = await client.ApiClient.GetGuildApplicationCommandsAsync(guild.Id, withLocalizations, locale, options); return models.Select(x => RestGuildCommand.Create(client, x, guild.Id)).ToImmutableArray(); } public static async Task GetSlashCommandAsync(IGuild guild, ulong id, BaseDiscordClient client, diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 3e0ad1840..eb3254619 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -311,13 +311,15 @@ namespace Discord.Rest /// /// Gets a collection of slash commands created by the current user in this guild. /// + /// Whether to include full localization dictionaries in the returned objects, instead of the name localized and description localized fields. + /// The target locale of the localized name and description fields. Sets X-Discord-Locale header, which takes precedence over Accept-Language. /// The options to be used when sending the request. /// /// A task that represents the asynchronous get operation. The task result contains a read-only collection of /// slash commands created by the current user. /// - public Task> GetSlashCommandsAsync(RequestOptions options = null) - => GuildHelper.GetSlashCommandsAsync(this, Discord, options); + public Task> GetSlashCommandsAsync(bool withLocalizations = false, string locale = null, RequestOptions options = null) + => GuildHelper.GetSlashCommandsAsync(this, Discord, withLocalizations, locale, options); /// /// Gets a slash command in the current guild. @@ -928,13 +930,15 @@ namespace Discord.Rest /// /// Gets this guilds slash commands /// + /// Whether to include full localization dictionaries in the returned objects, instead of the name localized and description localized fields. + /// The target locale of the localized name and description fields. Sets X-Discord-Locale header, which takes precedence over Accept-Language. /// The options to be used when sending the request. /// /// A task that represents the asynchronous get operation. The task result contains a read-only collection /// of application commands found within the guild. /// - public async Task> GetApplicationCommandsAsync (RequestOptions options = null) - => await ClientHelper.GetGuildApplicationCommandsAsync(Discord, Id, options).ConfigureAwait(false); + public async Task> GetApplicationCommandsAsync (bool withLocalizations = false, string locale = null, RequestOptions options = null) + => await ClientHelper.GetGuildApplicationCommandsAsync(Discord, Id, withLocalizations, locale, options).ConfigureAwait(false); /// /// Gets an application command within this guild with the specified id. /// @@ -1467,8 +1471,8 @@ namespace Discord.Rest async Task> IGuild.GetWebhooksAsync(RequestOptions options) => await GetWebhooksAsync(options).ConfigureAwait(false); /// - async Task> IGuild.GetApplicationCommandsAsync (RequestOptions options) - => await GetApplicationCommandsAsync(options).ConfigureAwait(false); + async Task> IGuild.GetApplicationCommandsAsync (bool withLocalizations, string locale, RequestOptions options) + => await GetApplicationCommandsAsync(withLocalizations, locale, options).ConfigureAwait(false); /// async Task IGuild.CreateStickerAsync(string name, string description, IEnumerable tags, Image image, RequestOptions options) => await CreateStickerAsync(name, description, tags, image, options); diff --git a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs index 522c098e6..de1ef3149 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs @@ -3,6 +3,7 @@ using Discord.API.Rest; using Discord.Net; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Net; using System.Threading.Tasks; @@ -101,11 +102,12 @@ namespace Discord.Rest DefaultPermission = arg.IsDefaultPermission.IsSpecified ? arg.IsDefaultPermission.Value : Optional.Unspecified, + NameLocalizations = arg.NameLocalizations?.ToDictionary(), + DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary(), // TODO: better conversion to nullable optionals DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(), DmPermission = arg.IsDMEnabled.ToNullable() - }; if (arg is SlashCommandProperties slashProps) @@ -140,12 +142,16 @@ namespace Discord.Rest DefaultPermission = arg.IsDefaultPermission.IsSpecified ? arg.IsDefaultPermission.Value : Optional.Unspecified, + NameLocalizations = arg.NameLocalizations?.ToDictionary(), + DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary(), // TODO: better conversion to nullable optionals DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(), DmPermission = arg.IsDMEnabled.ToNullable() }; + Console.WriteLine("Locales:" + string.Join(",", arg.NameLocalizations.Keys)); + if (arg is SlashCommandProperties slashProps) { Preconditions.NotNullOrEmpty(slashProps.Description, nameof(slashProps.Description)); @@ -181,6 +187,8 @@ namespace Discord.Rest DefaultPermission = arg.IsDefaultPermission.IsSpecified ? arg.IsDefaultPermission.Value : Optional.Unspecified, + NameLocalizations = arg.NameLocalizations?.ToDictionary(), + DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary(), // TODO: better conversion to nullable optionals DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(), @@ -244,7 +252,9 @@ namespace Discord.Rest Name = args.Name, DefaultPermission = args.IsDefaultPermission.IsSpecified ? args.IsDefaultPermission.Value - : Optional.Unspecified + : Optional.Unspecified, + NameLocalizations = args.NameLocalizations?.ToDictionary(), + DescriptionLocalizations = args.DescriptionLocalizations?.ToDictionary() }; if (args is SlashCommandProperties slashProps) @@ -299,6 +309,8 @@ namespace Discord.Rest DefaultPermission = arg.IsDefaultPermission.IsSpecified ? arg.IsDefaultPermission.Value : Optional.Unspecified, + NameLocalizations = arg.NameLocalizations?.ToDictionary(), + DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary(), // TODO: better conversion to nullable optionals DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(), @@ -335,7 +347,9 @@ namespace Discord.Rest Name = arg.Name, DefaultPermission = arg.IsDefaultPermission.IsSpecified ? arg.IsDefaultPermission.Value - : Optional.Unspecified + : Optional.Unspecified, + NameLocalizations = arg.NameLocalizations?.ToDictionary(), + DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary() }; if (arg is SlashCommandProperties slashProps) diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs index 667609ef4..468d10712 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs @@ -38,6 +38,32 @@ namespace Discord.Rest /// public IReadOnlyCollection Options { get; private set; } + /// + /// Gets the localization dictionary for the name field of this command. + /// + public IReadOnlyDictionary NameLocalizations { get; private set; } + + /// + /// Gets the localization dictionary for the description field of this command. + /// + public IReadOnlyDictionary DescriptionLocalizations { get; private set; } + + /// + /// Gets the localized name of this command. + /// + /// + /// Only returned when the `withLocalizations` query parameter is set to when requesting the command. + /// + public string NameLocalized { get; private set; } + + /// + /// Gets the localized description of this command. + /// + /// + /// Only returned when the `withLocalizations` query parameter is set to when requesting the command. + /// + public string DescriptionLocalized { get; private set; } + /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); @@ -64,6 +90,15 @@ namespace Discord.Rest ? model.Options.Value.Select(RestApplicationCommandOption.Create).ToImmutableArray() : ImmutableArray.Create(); + NameLocalizations = model.NameLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ?? + ImmutableDictionary.Empty; + + DescriptionLocalizations = model.DescriptionLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ?? + ImmutableDictionary.Empty; + + NameLocalized = model.NameLocalized.GetValueOrDefault(); + DescriptionLocalized = model.DescriptionLocalized.GetValueOrDefault(); + IsEnabledInDm = model.DmPermission.GetValueOrDefault(true).GetValueOrDefault(true); DefaultMemberPermissions = new GuildPermissions((ulong)model.DefaultMemberPermission.GetValueOrDefault(0).GetValueOrDefault(0)); } diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandChoice.cs b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandChoice.cs index a40491a2c..b736c435d 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandChoice.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandChoice.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; +using System.Collections.Immutable; using Model = Discord.API.ApplicationCommandOptionChoice; namespace Discord.Rest @@ -13,10 +15,25 @@ namespace Discord.Rest /// public object Value { get; } + /// + /// Gets the localization dictionary for the name field of this command option choice. + /// + public IReadOnlyDictionary NameLocalizations { get; } + + /// + /// Gets the localized name of this command option choice. + /// + /// + /// Only returned when the `withLocalizations` query parameter is set to when requesting the command. + /// + public string NameLocalized { get; } + internal RestApplicationCommandChoice(Model model) { Name = model.Name; Value = model.Value; + NameLocalizations = model.NameLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary(); + NameLocalized = model.NameLocalized.GetValueOrDefault(null); } } } diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs index c47080be7..3ac15e695 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs @@ -27,7 +27,7 @@ namespace Discord.Rest public bool? IsRequired { get; private set; } /// - public bool? IsAutocomplete { get; private set; } + public bool? IsAutocomplete { get; private set; } /// public double? MinValue { get; private set; } @@ -54,6 +54,32 @@ namespace Discord.Rest /// public IReadOnlyCollection ChannelTypes { get; private set; } + /// + /// Gets the localization dictionary for the name field of this command option. + /// + public IReadOnlyDictionary NameLocalizations { get; private set; } + + /// + /// Gets the localization dictionary for the description field of this command option. + /// + public IReadOnlyDictionary DescriptionLocalizations { get; private set; } + + /// + /// Gets the localized name of this command option. + /// + /// + /// Only returned when the `withLocalizations` query parameter is set to when requesting the command. + /// + public string NameLocalized { get; private set; } + + /// + /// Gets the localized description of this command option. + /// + /// + /// Only returned when the `withLocalizations` query parameter is set to when requesting the command. + /// + public string DescriptionLocalized { get; private set; } + internal RestApplicationCommandOption() { } internal static RestApplicationCommandOption Create(Model model) @@ -98,6 +124,15 @@ namespace Discord.Rest ChannelTypes = model.ChannelTypes.IsSpecified ? model.ChannelTypes.Value.ToImmutableArray() : ImmutableArray.Create(); + + NameLocalizations = model.NameLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ?? + ImmutableDictionary.Empty; + + DescriptionLocalizations = model.DescriptionLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ?? + ImmutableDictionary.Empty; + + NameLocalized = model.NameLocalized.GetValueOrDefault(); + DescriptionLocalized = model.DescriptionLocalized.GetValueOrDefault(); } #endregion diff --git a/src/Discord.Net.Rest/Net/DefaultRestClient.cs b/src/Discord.Net.Rest/Net/DefaultRestClient.cs index 721c7009d..97872ee6a 100644 --- a/src/Discord.Net.Rest/Net/DefaultRestClient.cs +++ b/src/Discord.Net.Rest/Net/DefaultRestClient.cs @@ -66,33 +66,45 @@ namespace Discord.Net.Rest _cancelToken = cancelToken; } - public async Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly, string reason = null) + public async Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly, string reason = null, + IEnumerable>> requestHeaders = null) { string uri = Path.Combine(_baseUrl, endpoint); using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) { if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); + if (requestHeaders != null) + foreach (var header in requestHeaders) + restRequest.Headers.Add(header.Key, header.Value); return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); } } - public async Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly, string reason = null) + public async Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly, string reason = null, + IEnumerable>> requestHeaders = null) { string uri = Path.Combine(_baseUrl, endpoint); using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) { if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); + if (requestHeaders != null) + foreach (var header in requestHeaders) + restRequest.Headers.Add(header.Key, header.Value); restRequest.Content = new StringContent(json, Encoding.UTF8, "application/json"); return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); } } /// Unsupported param type. - public async Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly, string reason = null) + public async Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly, string reason = null, + IEnumerable>> requestHeaders = null) { string uri = Path.Combine(_baseUrl, endpoint); using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) { if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); + if (requestHeaders != null) + foreach (var header in requestHeaders) + restRequest.Headers.Add(header.Key, header.Value); var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture)); MemoryStream memoryStream = null; if (multipartParams != null) @@ -126,7 +138,7 @@ namespace Discord.Net.Rest content.Add(streamContent, p.Key, fileValue.Filename); #pragma warning restore IDISP004 - + continue; } default: diff --git a/src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs b/src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs index bb5840ce2..e5cab831e 100644 --- a/src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs +++ b/src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs @@ -1,5 +1,8 @@ using Discord.Net.Rest; using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; using System.IO; using System.Threading.Tasks; @@ -28,7 +31,7 @@ namespace Discord.Net.Queue public virtual async Task SendAsync() { - return await Client.SendAsync(Method, Endpoint, Options.CancelToken, Options.HeaderOnly, Options.AuditLogReason).ConfigureAwait(false); + return await Client.SendAsync(Method, Endpoint, Options.CancelToken, Options.HeaderOnly, Options.AuditLogReason, Options.RequestHeaders).ConfigureAwait(false); } } } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index f0b50aa8f..670ed4567 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -450,14 +450,16 @@ namespace Discord.WebSocket /// /// Gets a collection of all global commands. /// + /// Whether to include full localization dictionaries in the returned objects, instead of the name localized and description localized fields. + /// The target locale of the localized name and description fields. Sets X-Discord-Locale header, which takes precedence over Accept-Language. /// The options to be used when sending the request. /// /// A task that represents the asynchronous get operation. The task result contains a read-only collection of global /// application commands. /// - public async Task> GetGlobalApplicationCommandsAsync(RequestOptions options = null) + public async Task> GetGlobalApplicationCommandsAsync(bool withLocalizations = false, string locale = null, RequestOptions options = null) { - var commands = (await ApiClient.GetGlobalApplicationCommandsAsync(options)).Select(x => SocketApplicationCommand.Create(this, x)); + var commands = (await ApiClient.GetGlobalApplicationCommandsAsync(withLocalizations, locale, options)).Select(x => SocketApplicationCommand.Create(this, x)); foreach(var command in commands) { @@ -3236,8 +3238,8 @@ namespace Discord.WebSocket async Task IDiscordClient.GetGlobalApplicationCommandAsync(ulong id, RequestOptions options) => await GetGlobalApplicationCommandAsync(id, options); /// - async Task> IDiscordClient.GetGlobalApplicationCommandsAsync(RequestOptions options) - => await GetGlobalApplicationCommandsAsync(options); + async Task> IDiscordClient.GetGlobalApplicationCommandsAsync(bool withLocalizations, string locale, RequestOptions options) + => await GetGlobalApplicationCommandsAsync(withLocalizations, locale, options); /// async Task IDiscordClient.StartAsync() diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 78fb33206..55f098b2f 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -874,14 +874,17 @@ namespace Discord.WebSocket /// /// Gets a collection of slash commands created by the current user in this guild. /// + /// Whether to include full localization dictionaries in the returned objects, instead of the name localized and description localized fields. + /// The target locale of the localized name and description fields. Sets X-Discord-Locale header, which takes precedence over Accept-Language. /// The options to be used when sending the request. /// /// A task that represents the asynchronous get operation. The task result contains a read-only collection of /// slash commands created by the current user. /// - public async Task> GetApplicationCommandsAsync(RequestOptions options = null) + public async Task> GetApplicationCommandsAsync(bool withLocalizations = false, string locale = null, RequestOptions options = null) { - var commands = (await Discord.ApiClient.GetGuildApplicationCommandsAsync(Id, options)).Select(x => SocketApplicationCommand.Create(Discord, x, Id)); + var commands = (await Discord.ApiClient.GetGuildApplicationCommandsAsync(Id, withLocalizations, locale, options)) + .Select(x => SocketApplicationCommand.Create(Discord, x, Id)); foreach (var command in commands) { @@ -1977,8 +1980,8 @@ namespace Discord.WebSocket async Task> IGuild.GetWebhooksAsync(RequestOptions options) => await GetWebhooksAsync(options).ConfigureAwait(false); /// - async Task> IGuild.GetApplicationCommandsAsync (RequestOptions options) - => await GetApplicationCommandsAsync(options).ConfigureAwait(false); + async Task> IGuild.GetApplicationCommandsAsync (bool withLocalizations, string locale, RequestOptions options) + => await GetApplicationCommandsAsync(withLocalizations, locale, options).ConfigureAwait(false); /// async Task IGuild.CreateStickerAsync(string name, string description, IEnumerable tags, Image image, RequestOptions options) => await CreateStickerAsync(name, description, tags, image, options); diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs index f6b3f9699..b0ddd0012 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs @@ -50,6 +50,32 @@ namespace Discord.WebSocket /// public IReadOnlyCollection Options { get; private set; } + /// + /// Gets the localization dictionary for the name field of this command. + /// + public IReadOnlyDictionary NameLocalizations { get; private set; } + + /// + /// Gets the localization dictionary for the description field of this command. + /// + public IReadOnlyDictionary DescriptionLocalizations { get; private set; } + + /// + /// Gets the localized name of this command. + /// + /// + /// Only returned when the `withLocalizations` query parameter is set to when requesting the command. + /// + public string NameLocalized { get; private set; } + + /// + /// Gets the localized description of this command. + /// + /// + /// Only returned when the `withLocalizations` query parameter is set to when requesting the command. + /// + public string DescriptionLocalized { get; private set; } + /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); @@ -93,6 +119,15 @@ namespace Discord.WebSocket ? model.Options.Value.Select(SocketApplicationCommandOption.Create).ToImmutableArray() : ImmutableArray.Create(); + NameLocalizations = model.NameLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ?? + ImmutableDictionary.Empty; + + DescriptionLocalizations = model.DescriptionLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ?? + ImmutableDictionary.Empty; + + NameLocalized = model.NameLocalized.GetValueOrDefault(); + DescriptionLocalized = model.DescriptionLocalized.GetValueOrDefault(); + IsEnabledInDm = model.DmPermission.GetValueOrDefault(true).GetValueOrDefault(true); DefaultMemberPermissions = new GuildPermissions((ulong)model.DefaultMemberPermission.GetValueOrDefault(0).GetValueOrDefault(0)); } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandChoice.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandChoice.cs index e70efa27b..4da1eaadb 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandChoice.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandChoice.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; +using System.Collections.Immutable; using Model = Discord.API.ApplicationCommandOptionChoice; namespace Discord.WebSocket @@ -13,6 +15,19 @@ namespace Discord.WebSocket /// public object Value { get; private set; } + /// + /// Gets the localization dictionary for the name field of this command option choice. + /// + public IReadOnlyDictionary NameLocalizations { get; private set; } + + /// + /// Gets the localized name of this command option choice. + /// + /// + /// Only returned when the `withLocalizations` query parameter is set to when requesting the command. + /// + public string NameLocalized { get; private set; } + internal SocketApplicationCommandChoice() { } internal static SocketApplicationCommandChoice Create(Model model) { @@ -24,6 +39,8 @@ namespace Discord.WebSocket { Name = model.Name; Value = model.Value; + NameLocalizations = model.NameLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary(); + NameLocalized = model.NameLocalized.GetValueOrDefault(null); } } } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandOption.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandOption.cs index 478c7cb54..78bb45141 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandOption.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandOption.cs @@ -54,6 +54,32 @@ namespace Discord.WebSocket /// public IReadOnlyCollection ChannelTypes { get; private set; } + /// + /// Gets the localization dictionary for the name field of this command option. + /// + public IReadOnlyDictionary NameLocalizations { get; private set; } + + /// + /// Gets the localization dictionary for the description field of this command option. + /// + public IReadOnlyDictionary DescriptionLocalizations { get; private set; } + + /// + /// Gets the localized name of this command option. + /// + /// + /// Only returned when the `withLocalizations` query parameter is set to when requesting the command. + /// + public string NameLocalized { get; private set; } + + /// + /// Gets the localized description of this command option. + /// + /// + /// Only returned when the `withLocalizations` query parameter is set to when requesting the command. + /// + public string DescriptionLocalized { get; private set; } + internal SocketApplicationCommandOption() { } internal static SocketApplicationCommandOption Create(Model model) { @@ -92,6 +118,15 @@ namespace Discord.WebSocket ChannelTypes = model.ChannelTypes.IsSpecified ? model.ChannelTypes.Value.ToImmutableArray() : ImmutableArray.Create(); + + NameLocalizations = model.NameLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ?? + ImmutableDictionary.Empty; + + DescriptionLocalizations = model.DescriptionLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ?? + ImmutableDictionary.Empty; + + NameLocalized = model.NameLocalized.GetValueOrDefault(); + DescriptionLocalized = model.DescriptionLocalized.GetValueOrDefault(); } IReadOnlyCollection IApplicationCommandOption.Choices => Choices; From adf012d1dd620ed2e4d78a7e6700bbce69a64d38 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Sun, 28 Aug 2022 12:04:08 -0300 Subject: [PATCH 2/9] meta: 3.8.0 (#2441) --- CHANGELOG.md | 38 ++++++++++++++++++ Discord.Net.targets | 2 +- docs/docfx.json | 2 +- src/Discord.Net/Discord.Net.nuspec | 62 +++++++++++++++--------------- 4 files changed, 71 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4022e1b6..7385ab38a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,42 @@ # Changelog + +## [3.8.0] - 2022-08-27 +### Added +- #2384 Added support for the WEBHOOKS_UPDATED event (010e8e8) +- #2370 Add async callbacks for IModuleBase (503fa75) +- #2367 Added DeleteMessagesAsync for TIV and added remaining rate limit in client log (f178660) +- #2379 Added Max/Min length fields for ApplicationCommandOption (e551431) +- #2369 Added support for using `RespondWithModalAsync()` without prior IModal declaration (500e7b4) +- #2347 Added Embed field comparison operators (89a8ea1) +- #2359 Added support for creating lottie stickers (32b03c8) +- #2395 Added App Command localization support and `ILocalizationManager` to IF (39bbd29) + +### Fixed +- #2425 Fix missing Fact attribute in ColorTests (92215b1) +- #2424 Fix IGuild.GetBansAsync() (b7b7964) +- #2416 Fix role icon & emoji assignment (b6b5e95) +- #2414 Fix NRE on RestCommandBase Data (02bc3b7) +- #2421 Fix placeholder length being hardcoded (8dfe19f) +- #2352 Fix issues related to the absence of bot scope (1eb42c6) +- #2346 Fix IGuild.DisconnectAsync(IUser) not disconnecting users (ba02416) +- #2404 Fix range of issues presented by 3rd party analyzer (902326d) +- #2409 Removes GroupContext from requirecontext (b0b8167) + +### Misc +- #2366 Fixed typo in ChannelUpdatedEvent's documentation (cfd2662) +- #2408 Fix sharding sample throwing at appcommand registration (519deda) +- #2420 Fix broken code snippet in dependency injection docs (ddcf68a) +- #2430 Add a note about DontAutoRegisterAttribute (917118d) +- #2418 Update xmldocs to reflect the ConnectedUsers split (65b98f8) +- #2415 Adds missing DI entries in TOC (c49d483) +- #2407 Introduces high quality dependency injection documentation (6fdcf98) +- #2348 Added `RequiredInput` attribute to example in int.framework intro (ee6e0ad) +- #2385 Add ServerStarter.Host to deployment.md (06ed995) +- #2405 Add a note about `IgnoreGroupNames` to IF docs (cf25acd) +- #2356 Makes voice section about precompiled binaries more visible (e0d68d4 ) +- #2405 IF intro docs improvements (246282d) +- #2406 Labs deprecation & readme/docs edits (bf493ea) + ## [3.7.2] - 2022-06-02 ### Added - #2328 Add method overloads to InteractionService (0fad3e8) diff --git a/Discord.Net.targets b/Discord.Net.targets index 8cedb40e7..d9ec415f9 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,6 +1,6 @@ - 3.7.2 + 3.8.0 latest Discord.Net Contributors discord;discordapp diff --git a/docs/docfx.json b/docs/docfx.json index 5dd1e640d..64a20ecb2 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -60,7 +60,7 @@ "overwrite": "_overwrites/**/**.md", "globalMetadata": { "_appTitle": "Discord.Net Documentation", - "_appFooter": "Discord.Net (c) 2015-2022 3.7.2", + "_appFooter": "Discord.Net (c) 2015-2022 3.8.0", "_enableSearch": true, "_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg", "_appFaviconPath": "favicon.ico" diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index 1a61ff97a..63a288dc3 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,7 +2,7 @@ Discord.Net - 3.7.2$suffix$ + 3.8.0$suffix$ Discord.Net Discord.Net Contributors foxbot @@ -14,44 +14,44 @@ https://github.com/discord-net/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + From 0aa381d4683694ab85e300337aef4be42a171e33 Mon Sep 17 00:00:00 2001 From: Kuba_Z2 <77853483+KubaZ2@users.noreply.github.com> Date: Mon, 29 Aug 2022 11:24:32 +0200 Subject: [PATCH 3/9] Fix typos of word `length` (#2443) --- .../SlashCommands/SlashCommandBuilder.cs | 6 +++--- .../Attributes/MaxLengthAttribute.cs | 6 +++--- .../Attributes/MinLengthAttribute.cs | 6 +++--- .../Modals/Inputs/TextInputComponentBuilder.cs | 12 ++++++------ 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs index 579289304..b443c4468 100644 --- a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs @@ -855,11 +855,11 @@ namespace Discord /// /// Sets the current builders max length field. /// - /// The value to set. + /// The value to set. /// The current builder. - public SlashCommandOptionBuilder WithMaxLength(int lenght) + public SlashCommandOptionBuilder WithMaxLength(int length) { - MaxLength = lenght; + MaxLength = length; return this; } diff --git a/src/Discord.Net.Interactions/Attributes/MaxLengthAttribute.cs b/src/Discord.Net.Interactions/Attributes/MaxLengthAttribute.cs index 1099e7d92..2172886d2 100644 --- a/src/Discord.Net.Interactions/Attributes/MaxLengthAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/MaxLengthAttribute.cs @@ -16,10 +16,10 @@ namespace Discord.Interactions /// /// Sets the maximum length allowed for a string type parameter. /// - /// Maximum string length allowed. - public MaxLengthAttribute(int lenght) + /// Maximum string length allowed. + public MaxLengthAttribute(int length) { - Length = lenght; + Length = length; } } } diff --git a/src/Discord.Net.Interactions/Attributes/MinLengthAttribute.cs b/src/Discord.Net.Interactions/Attributes/MinLengthAttribute.cs index 7d0b0fd63..8050f992a 100644 --- a/src/Discord.Net.Interactions/Attributes/MinLengthAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/MinLengthAttribute.cs @@ -16,10 +16,10 @@ namespace Discord.Interactions /// /// Sets the minimum length allowed for a string type parameter. /// - /// Minimum string length allowed. - public MinLengthAttribute(int lenght) + /// Minimum string length allowed. + public MinLengthAttribute(int length) { - Length = lenght; + Length = length; } } } diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs index 8dd2c4004..728b97a7a 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs @@ -67,26 +67,26 @@ namespace Discord.Interactions.Builders /// /// Sets . /// - /// New value of the . + /// New value of the . /// /// The builder instance. /// - public TextInputComponentBuilder WithMinLenght(int minLenght) + public TextInputComponentBuilder WithMinLength(int minLength) { - MinLength = minLenght; + MinLength = minLength; return this; } /// /// Sets . /// - /// New value of the . + /// New value of the . /// /// The builder instance. /// - public TextInputComponentBuilder WithMaxLenght(int maxLenght) + public TextInputComponentBuilder WithMaxLength(int maxLength) { - MaxLength = maxLenght; + MaxLength = maxLength; return this; } From 9feb703a82d6bd3c15118237c260320c8e873269 Mon Sep 17 00:00:00 2001 From: Viktor Chernikov <96922685+Polybroo@users.noreply.github.com> Date: Mon, 29 Aug 2022 11:25:13 +0200 Subject: [PATCH 4/9] Wrong symbol fix (#2438) --- src/Discord.Net.Rest/DiscordRestApiClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index eb1737c6f..615e5ac12 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -862,7 +862,7 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(webhookId: webhookId); - await SendJsonAsync("PATCH", () => $"webhooks/{webhookId}/{AuthToken}/messages/{messageId}${WebhookQuery(false, threadId)}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + await SendJsonAsync("PATCH", () => $"webhooks/{webhookId}/{AuthToken}/messages/{messageId}?{WebhookQuery(false, threadId)}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } /// This operation may only be called with a token. From fbc5ad414f87b2d4678b697f05ed06d70d8b9b16 Mon Sep 17 00:00:00 2001 From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Date: Mon, 29 Aug 2022 15:24:33 +0300 Subject: [PATCH 5/9] fix BulkOverwriteCommands NRE (#2444) --- src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs index de1ef3149..deca00b72 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs @@ -150,8 +150,6 @@ namespace Discord.Rest DmPermission = arg.IsDMEnabled.ToNullable() }; - Console.WriteLine("Locales:" + string.Join(",", arg.NameLocalizations.Keys)); - if (arg is SlashCommandProperties slashProps) { Preconditions.NotNullOrEmpty(slashProps.Description, nameof(slashProps.Description)); From 370bdfa3c630152bfcd851c918f5d49ab6a3116d Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Thu, 1 Sep 2022 20:48:00 +0200 Subject: [PATCH 6/9] Bump to Discord API v10 (#2448) * Bump, add messagecontent intent * Update comments Co-authored-by: Rozen --- src/Discord.Net.Core/DiscordConfig.cs | 2 +- src/Discord.Net.Core/Entities/Messages/IMessage.cs | 6 ++++++ src/Discord.Net.Core/GatewayIntents.cs | 11 +++++++++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net.Core/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs index 2db802f1e..ebca0120c 100644 --- a/src/Discord.Net.Core/DiscordConfig.cs +++ b/src/Discord.Net.Core/DiscordConfig.cs @@ -18,7 +18,7 @@ namespace Discord /// Discord API documentation /// . /// - public const int APIVersion = 9; + public const int APIVersion = 10; /// /// Returns the Voice API version Discord.Net uses. /// diff --git a/src/Discord.Net.Core/Entities/Messages/IMessage.cs b/src/Discord.Net.Core/Entities/Messages/IMessage.cs index f5f2ca007..48db4fdf0 100644 --- a/src/Discord.Net.Core/Entities/Messages/IMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IMessage.cs @@ -48,6 +48,9 @@ namespace Discord /// /// Gets the content for this message. /// + /// + /// This will be empty if the privileged is disabled. + /// /// /// A string that contains the body of the message; note that this field may be empty if there is an embed. /// @@ -55,6 +58,9 @@ namespace Discord /// /// Gets the clean content for this message. /// + /// + /// This will be empty if the privileged is disabled. + /// /// /// A string that contains the body of the message stripped of mentions, markdown, emojis and pings; note that this field may be empty if there is an embed. /// diff --git a/src/Discord.Net.Core/GatewayIntents.cs b/src/Discord.Net.Core/GatewayIntents.cs index f2a99e44c..e9dd8f814 100644 --- a/src/Discord.Net.Core/GatewayIntents.cs +++ b/src/Discord.Net.Core/GatewayIntents.cs @@ -39,7 +39,14 @@ namespace Discord DirectMessageReactions = 1 << 13, /// This intent includes TYPING_START DirectMessageTyping = 1 << 14, - /// This intent includes GUILD_SCHEDULED_EVENT_CREATE, GUILD_SCHEDULED_EVENT_UPDATE, GUILD_SCHEDULED_EVENT_DELETE, GUILD_SCHEDULED_EVENT_USER_ADD, GUILD_SCHEDULED_EVENT_USER_REMOVE + /// + /// This intent defines if the content within messages received by MESSAGE_CREATE is available or not. + /// This is a privileged intent and needs to be enabled in the developer portal. + /// + MessageContent = 1 << 15, + /// + /// This intent includes GUILD_SCHEDULED_EVENT_CREATE, GUILD_SCHEDULED_EVENT_UPDATE, GUILD_SCHEDULED_EVENT_DELETE, GUILD_SCHEDULED_EVENT_USER_ADD, GUILD_SCHEDULED_EVENT_USER_REMOVE + /// GuildScheduledEvents = 1 << 16, /// /// This intent includes all but and @@ -51,6 +58,6 @@ namespace Discord /// /// This intent includes all of them, including privileged ones. /// - All = AllUnprivileged | GuildMembers | GuildPresences + All = AllUnprivileged | GuildMembers | GuildPresences | MessageContent } } From 376a812b6a3b435566a2abbb011968dafb1db3d5 Mon Sep 17 00:00:00 2001 From: Damian Kraaijeveld Date: Fri, 2 Sep 2022 23:04:53 +0200 Subject: [PATCH 7/9] Return a list instead of an array (#2451) --- src/Discord.Net.Interactions/Utilities/CommandHierarchy.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Interactions/Utilities/CommandHierarchy.cs b/src/Discord.Net.Interactions/Utilities/CommandHierarchy.cs index a4554eaef..da7ef22e0 100644 --- a/src/Discord.Net.Interactions/Utilities/CommandHierarchy.cs +++ b/src/Discord.Net.Interactions/Utilities/CommandHierarchy.cs @@ -26,7 +26,7 @@ namespace Discord.Interactions public static IList GetCommandPath(this ICommandInfo commandInfo) { if (commandInfo.IgnoreGroupNames) - return new string[] { commandInfo.Name }; + return new List { commandInfo.Name }; var path = commandInfo.Module.GetModulePath(); path.Add(commandInfo.Name); @@ -48,6 +48,6 @@ namespace Discord.Interactions } public static IList GetTypePath(Type type) => - new string[] { EscapeChar + type.FullName }; + new List { EscapeChar + type.FullName }; } } From b967e6907c3ff204705bd60e50f7e31d6071cc95 Mon Sep 17 00:00:00 2001 From: Discord-NET-Robot <95661365+Discord-NET-Robot@users.noreply.github.com> Date: Fri, 2 Sep 2022 18:14:16 -0300 Subject: [PATCH 8/9] [Robot] Add missing json error (#2447) * Add 20024, 30032, 30034, 30052, 40012, 40043, 40058, 40066, 40067, 50017, 50132, 50138, 50146, 110001, 200000, 200001, 220001, 220002, 220003, 220004, 240000 Error codes * Update src/Discord.Net.Core/DiscordErrorCode.cs * Apply suggestions from code review Co-authored-by: Discord.Net Robot Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> --- src/Discord.Net.Core/DiscordErrorCode.cs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/Discord.Net.Core/DiscordErrorCode.cs b/src/Discord.Net.Core/DiscordErrorCode.cs index b444614e4..262252eab 100644 --- a/src/Discord.Net.Core/DiscordErrorCode.cs +++ b/src/Discord.Net.Core/DiscordErrorCode.cs @@ -66,6 +66,7 @@ namespace Discord ActionSlowmode = 20016, OnlyOwnerAction = 20018, AnnouncementEditRatelimit = 20022, + UnderMinimumAge = 20024, ChannelWriteRatelimit = 20028, WriteRatelimitReached = 20029, WordsNotAllowed = 20031, @@ -88,7 +89,9 @@ namespace Discord MaximumServerMembersReached = 30019, MaximumServerCategoriesReached = 30030, GuildTemplateAlreadyExists = 30031, + MaximumNumberOfApplicationCommandsReached = 30032, MaximumThreadMembersReached = 30033, + MaxNumberOfDailyApplicationCommandCreatesHasBeenReached = 30034, MaximumBansForNonGuildMembersReached = 30035, MaximumBanFetchesReached = 30037, MaximumUncompleteGuildScheduledEvents = 30038, @@ -98,6 +101,7 @@ namespace Discord #endregion #region General Request Errors (40XXX) + BitrateIsTooHighForChannelOfThisType = 30052, MaximumNumberOfEditsReached = 30046, MaximumNumberOfPinnedThreadsInAForumChannelReached = 30047, MaximumNumberOfTagsInAForumChannelReached = 30048, @@ -108,12 +112,17 @@ namespace Discord RequestEntityTooLarge = 40005, FeatureDisabled = 40006, UserBanned = 40007, + ConnectionHasBeenRevoked = 40012, TargetUserNotInVoice = 40032, MessageAlreadyCrossposted = 40033, ApplicationNameAlreadyExists = 40041, #endregion #region Action Preconditions/Checks (50XXX) + ApplicationInteractionFailedToSend = 40043, + CannotSendAMessageInAForumChannel = 40058, + ThereAreNoTagsAvailableThatCanBeSetByNonModerators = 40066, + ATagIsRequiredToCreateAForumPostInThisChannel = 40067, InteractionHasAlreadyBeenAcknowledged = 40060, TagNamesMustBeUnique = 40061, MissingPermissions = 50001, @@ -132,6 +141,7 @@ namespace Discord InvalidAuthenticationToken = 50014, NoteTooLong = 50015, ProvidedMessageDeleteCountOutOfBounds = 50016, + InvalidMFALevel = 50017, InvalidPinChannel = 50019, InvalidInvite = 50020, CannotExecuteOnSystemMessage = 50021, @@ -165,6 +175,9 @@ namespace Discord #endregion #region 2FA (60XXX) + OwnershipCannotBeTransferredToABotUser = 50132, + AssetResizeBelowTheMaximumSize= 50138, + UploadedFileNotFound = 50146, MissingPermissionToSendThisSticker = 50600, Requires2FA = 60003, #endregion @@ -178,6 +191,7 @@ namespace Discord #endregion #region API Status (130XXX) + ApplicationNotYetAvailable = 110001, APIOverloaded = 130000, #endregion @@ -207,5 +221,15 @@ namespace Discord CannotUpdateFinishedEvent = 180000, FailedStageCreation = 180002, #endregion + + #region Forum & Automod + MessageWasBlockedByAutomaticModeration = 200000, + TitleWasBlockedByAutomaticModeration = 200001, + WebhooksPostedToForumChannelsMustHaveAThreadNameOrThreadId = 220001, + WebhooksPostedToForumChannelsCannotHaveBothAThreadNameAndThreadId = 220002, + WebhooksCanOnlyCreateThreadsInForumChannels = 220003, + WebhookServicesCannotBeUsedInForumChannels = 220004, + MessageBlockedByHarmfulLinksFilter = 240000, + #endregion } } From fca9c6b618715baaeeb5602d70fb8acfd91f6fe3 Mon Sep 17 00:00:00 2001 From: Julian <54598714+akaJuliaan@users.noreply.github.com> Date: Fri, 2 Sep 2022 23:31:51 +0200 Subject: [PATCH 9/9] Fix: remove Module from _typedModuleDefs (#2417) --- src/Discord.Net.Commands/CommandService.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 57e0e430e..29bf6a428 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -270,6 +270,11 @@ namespace Discord.Commands await _moduleLock.WaitAsync().ConfigureAwait(false); try { + var typeModulePair = _typedModuleDefs.FirstOrDefault(x => x.Value.Equals(module)); + + if (!typeModulePair.Equals(default(KeyValuePair))) + _typedModuleDefs.TryRemove(typeModulePair.Key, out var _); + return RemoveModuleInternal(module); } finally