* Request headers (#2394)
* add support for per-request headers
* remove unnecessary usings
* Revert "remove unnecessary usings"
This reverts commit 8d674fe4fa
.
* 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 <gehongyan1996@126.com>
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 <gehongyan1996@126.com>
Co-authored-by: Charlie U <52503242+cpurules@users.noreply.github.com>
Co-authored-by: Kuba_Z2 <77853483+KubaZ2@users.noreply.github.com>
pull/2441/head
@@ -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. | 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()`...). | - 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 | [AutocompleteHandlers]: xref:Guides.IntFw.AutoCompletion | ||||
[DependencyInjection]: xref:Guides.DI.Intro | [DependencyInjection]: xref:Guides.DI.Intro | ||||
@@ -1194,12 +1194,17 @@ namespace Discord | |||||
/// <summary> | /// <summary> | ||||
/// Gets this guilds application commands. | /// Gets this guilds application commands. | ||||
/// </summary> | /// </summary> | ||||
/// <param name="withLocalizations"> | |||||
/// Whether to include full localization dictionaries in the returned objects, | |||||
/// instead of the localized name and description fields. | |||||
/// </param> | |||||
/// <param name="locale">The target locale of the localized name and description fields. Sets the <c>X-Discord-Locale</c> header, which takes precedence over <c>Accept-Language</c>.</param> | |||||
/// <param name="options">The options to be used when sending the request.</param> | /// <param name="options">The options to be used when sending the request.</param> | ||||
/// <returns> | /// <returns> | ||||
/// A task that represents the asynchronous get operation. The task result contains a read-only collection | /// A task that represents the asynchronous get operation. The task result contains a read-only collection | ||||
/// of application commands found within the guild. | /// of application commands found within the guild. | ||||
/// </returns> | /// </returns> | ||||
Task<IReadOnlyCollection<IApplicationCommand>> GetApplicationCommandsAsync(RequestOptions options = null); | |||||
Task<IReadOnlyCollection<IApplicationCommand>> GetApplicationCommandsAsync(bool withLocalizations = false, string locale = null, RequestOptions options = null); | |||||
/// <summary> | /// <summary> | ||||
/// Gets an application command within this guild with the specified id. | /// Gets an application command within this guild with the specified id. | ||||
@@ -1,4 +1,5 @@ | |||||
using System; | using System; | ||||
using System.Collections; | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Linq; | using System.Linq; | ||||
using System.Text.RegularExpressions; | using System.Text.RegularExpressions; | ||||
@@ -12,6 +13,8 @@ namespace Discord | |||||
{ | { | ||||
private string _name; | private string _name; | ||||
private string _description; | private string _description; | ||||
private IDictionary<string, string> _nameLocalizations = new Dictionary<string, string>(); | |||||
private IDictionary<string, string> _descriptionLocalizations = new Dictionary<string, string>(); | |||||
/// <summary> | /// <summary> | ||||
/// Gets or sets the name of this option. | /// Gets or sets the name of this option. | ||||
@@ -21,18 +24,7 @@ namespace Discord | |||||
get => _name; | get => _name; | ||||
set | 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; | _name = value; | ||||
} | } | ||||
} | } | ||||
@@ -43,12 +35,11 @@ namespace Discord | |||||
public string Description | public string Description | ||||
{ | { | ||||
get => _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; | |||||
} | |||||
} | } | ||||
/// <summary> | /// <summary> | ||||
@@ -105,5 +96,72 @@ namespace Discord | |||||
/// Gets or sets the allowed channel types for this option. | /// Gets or sets the allowed channel types for this option. | ||||
/// </summary> | /// </summary> | ||||
public List<ChannelType> ChannelTypes { get; set; } | public List<ChannelType> ChannelTypes { get; set; } | ||||
/// <summary> | |||||
/// Gets or sets the localization dictionary for the name field of this option. | |||||
/// </summary> | |||||
/// <exception cref="ArgumentException">Thrown when any of the dictionary keys is an invalid locale.</exception> | |||||
public IDictionary<string, string> 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; | |||||
} | |||||
} | |||||
/// <summary> | |||||
/// Gets or sets the localization dictionary for the description field of this option. | |||||
/// </summary> | |||||
/// <exception cref="ArgumentException">Thrown when any of the dictionary keys is an invalid locale.</exception> | |||||
public IDictionary<string, string> 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."); | |||||
} | |||||
} | |||||
} | } | ||||
} | } |
@@ -1,4 +1,8 @@ | |||||
using System; | using System; | ||||
using System.Collections; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text.RegularExpressions; | |||||
namespace Discord | namespace Discord | ||||
{ | { | ||||
@@ -9,6 +13,7 @@ namespace Discord | |||||
{ | { | ||||
private string _name; | private string _name; | ||||
private object _value; | private object _value; | ||||
private IDictionary<string, string> _nameLocalizations = new Dictionary<string, string>(); | |||||
/// <summary> | /// <summary> | ||||
/// Gets or sets the name of this choice. | /// Gets or sets the name of this choice. | ||||
@@ -40,5 +45,33 @@ namespace Discord | |||||
_value = value; | _value = value; | ||||
} | } | ||||
} | } | ||||
/// <summary> | |||||
/// Gets or sets the localization dictionary for the name field of this choice. | |||||
/// </summary> | |||||
/// <exception cref="ArgumentException">Thrown when any of the dictionary keys is an invalid locale.</exception> | |||||
public IDictionary<string, string> 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; | |||||
} | |||||
} | |||||
} | } | ||||
} | } |
@@ -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 | namespace Discord | ||||
{ | { | ||||
/// <summary> | /// <summary> | ||||
@@ -5,6 +12,9 @@ namespace Discord | |||||
/// </summary> | /// </summary> | ||||
public abstract class ApplicationCommandProperties | public abstract class ApplicationCommandProperties | ||||
{ | { | ||||
private IReadOnlyDictionary<string, string> _nameLocalizations; | |||||
private IReadOnlyDictionary<string, string> _descriptionLocalizations; | |||||
internal abstract ApplicationCommandType Type { get; } | internal abstract ApplicationCommandType Type { get; } | ||||
/// <summary> | /// <summary> | ||||
@@ -18,6 +28,48 @@ namespace Discord | |||||
public Optional<bool> IsDefaultPermission { get; set; } | public Optional<bool> IsDefaultPermission { get; set; } | ||||
/// <summary> | /// <summary> | ||||
/// Gets or sets the localization dictionary for the name field of this command. | |||||
/// </summary> | |||||
public IReadOnlyDictionary<string, string> 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; | |||||
} | |||||
} | |||||
/// <summary> | |||||
/// Gets or sets the localization dictionary for the description field of this command. | |||||
/// </summary> | |||||
public IReadOnlyDictionary<string, string> 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; | |||||
} | |||||
} | |||||
/// <summary> | |||||
/// Gets or sets whether or not this command can be used in DMs. | /// Gets or sets whether or not this command can be used in DMs. | ||||
/// </summary> | /// </summary> | ||||
public Optional<bool> IsDMEnabled { get; set; } | public Optional<bool> IsDMEnabled { get; set; } | ||||
@@ -1,3 +1,8 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text.RegularExpressions; | |||||
namespace Discord | namespace Discord | ||||
{ | { | ||||
/// <summary> | /// <summary> | ||||
@@ -32,6 +37,11 @@ namespace Discord | |||||
public bool IsDefaultPermission { get; set; } = true; | public bool IsDefaultPermission { get; set; } = true; | ||||
/// <summary> | /// <summary> | ||||
/// Gets the localization dictionary for the name field of this command. | |||||
/// </summary> | |||||
public IReadOnlyDictionary<string, string> NameLocalizations => _nameLocalizations; | |||||
/// <summary> | |||||
/// Gets or sets whether or not this command can be used in DMs. | /// Gets or sets whether or not this command can be used in DMs. | ||||
/// </summary> | /// </summary> | ||||
public bool IsDMEnabled { get; set; } = true; | public bool IsDMEnabled { get; set; } = true; | ||||
@@ -42,6 +52,7 @@ namespace Discord | |||||
public GuildPermission? DefaultMemberPermissions { get; set; } | public GuildPermission? DefaultMemberPermissions { get; set; } | ||||
private string _name; | private string _name; | ||||
private Dictionary<string, string> _nameLocalizations; | |||||
/// <summary> | /// <summary> | ||||
/// Build the current builder into a <see cref="MessageCommandProperties"/> class. | /// Build the current builder into a <see cref="MessageCommandProperties"/> class. | ||||
@@ -87,6 +98,30 @@ namespace Discord | |||||
} | } | ||||
/// <summary> | /// <summary> | ||||
/// Sets the <see cref="NameLocalizations"/> collection. | |||||
/// </summary> | |||||
/// <param name="nameLocalizations">The localization dictionary to use for the name field of this command.</param> | |||||
/// <returns></returns> | |||||
/// <exception cref="ArgumentNullException">Thrown if <paramref name="nameLocalizations"/> is null.</exception> | |||||
/// <exception cref="ArgumentException">Thrown if any dictionary key is an invalid locale string.</exception> | |||||
public MessageCommandBuilder WithNameLocalizations(IDictionary<string, string> 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<string, string>(nameLocalizations); | |||||
return this; | |||||
} | |||||
/// <summary> | |||||
/// Sets whether or not this command can be used in dms | /// Sets whether or not this command can be used in dms | ||||
/// </summary> | /// </summary> | ||||
/// <param name="permission"><see langword="true"/> if the command is available in dms, otherwise <see langword="false"/>.</param> | /// <param name="permission"><see langword="true"/> if the command is available in dms, otherwise <see langword="false"/>.</param> | ||||
@@ -98,6 +133,41 @@ namespace Discord | |||||
} | } | ||||
/// <summary> | /// <summary> | ||||
/// Adds a new entry to the <see cref="NameLocalizations"/> collection. | |||||
/// </summary> | |||||
/// <param name="locale">Locale of the entry.</param> | |||||
/// <param name="name">Localized string for the name field.</param> | |||||
/// <returns>The current builder.</returns> | |||||
/// <exception cref="ArgumentException">Thrown if <paramref name="locale"/> is an invalid locale string.</exception> | |||||
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."); | |||||
} | |||||
/// <summary> | |||||
/// Sets the default member permissions required to use this application command. | /// Sets the default member permissions required to use this application command. | ||||
/// </summary> | /// </summary> | ||||
/// <param name="permissions">The permissions required to use this command.</param> | /// <param name="permissions">The permissions required to use this command.</param> | ||||
@@ -1,3 +1,8 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text.RegularExpressions; | |||||
namespace Discord | namespace Discord | ||||
{ | { | ||||
/// <summary> | /// <summary> | ||||
@@ -5,7 +10,7 @@ namespace Discord | |||||
/// </summary> | /// </summary> | ||||
public class UserCommandBuilder | public class UserCommandBuilder | ||||
{ | { | ||||
/// <summary> | |||||
/// <summary> | |||||
/// Returns the maximum length a commands name allowed by Discord. | /// Returns the maximum length a commands name allowed by Discord. | ||||
/// </summary> | /// </summary> | ||||
public const int MaxNameLength = 32; | public const int MaxNameLength = 32; | ||||
@@ -32,6 +37,11 @@ namespace Discord | |||||
public bool IsDefaultPermission { get; set; } = true; | public bool IsDefaultPermission { get; set; } = true; | ||||
/// <summary> | /// <summary> | ||||
/// Gets the localization dictionary for the name field of this command. | |||||
/// </summary> | |||||
public IReadOnlyDictionary<string, string> NameLocalizations => _nameLocalizations; | |||||
/// <summary> | |||||
/// Gets or sets whether or not this command can be used in DMs. | /// Gets or sets whether or not this command can be used in DMs. | ||||
/// </summary> | /// </summary> | ||||
public bool IsDMEnabled { get; set; } = true; | public bool IsDMEnabled { get; set; } = true; | ||||
@@ -42,6 +52,7 @@ namespace Discord | |||||
public GuildPermission? DefaultMemberPermissions { get; set; } | public GuildPermission? DefaultMemberPermissions { get; set; } | ||||
private string _name; | private string _name; | ||||
private Dictionary<string, string> _nameLocalizations; | |||||
/// <summary> | /// <summary> | ||||
/// Build the current builder into a <see cref="UserCommandProperties"/> class. | /// Build the current builder into a <see cref="UserCommandProperties"/> class. | ||||
@@ -85,6 +96,30 @@ namespace Discord | |||||
} | } | ||||
/// <summary> | /// <summary> | ||||
/// Sets the <see cref="NameLocalizations"/> collection. | |||||
/// </summary> | |||||
/// <param name="nameLocalizations">The localization dictionary to use for the name field of this command.</param> | |||||
/// <returns>The current builder.</returns> | |||||
/// <exception cref="ArgumentNullException">Thrown if <paramref name="nameLocalizations"/> is null.</exception> | |||||
/// <exception cref="ArgumentException">Thrown if any dictionary key is an invalid locale string.</exception> | |||||
public UserCommandBuilder WithNameLocalizations(IDictionary<string, string> 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<string, string>(nameLocalizations); | |||||
return this; | |||||
} | |||||
/// <summary> | |||||
/// Sets whether or not this command can be used in dms | /// Sets whether or not this command can be used in dms | ||||
/// </summary> | /// </summary> | ||||
/// <param name="permission"><see langword="true"/> if the command is available in dms, otherwise <see langword="false"/>.</param> | /// <param name="permission"><see langword="true"/> if the command is available in dms, otherwise <see langword="false"/>.</param> | ||||
@@ -96,6 +131,41 @@ namespace Discord | |||||
} | } | ||||
/// <summary> | /// <summary> | ||||
/// Adds a new entry to the <see cref="NameLocalizations"/> collection. | |||||
/// </summary> | |||||
/// <param name="locale">Locale of the entry.</param> | |||||
/// <param name="name">Localized string for the name field.</param> | |||||
/// <returns>The current builder.</returns> | |||||
/// <exception cref="ArgumentException">Thrown if <paramref name="locale"/> is an invalid locale string.</exception> | |||||
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."); | |||||
} | |||||
/// <summary> | |||||
/// Sets the default member permissions required to use this application command. | /// Sets the default member permissions required to use this application command. | ||||
/// </summary> | /// </summary> | ||||
/// <param name="permissions">The permissions required to use this command.</param> | /// <param name="permissions">The permissions required to use this command.</param> | ||||
@@ -53,6 +53,32 @@ namespace Discord | |||||
IReadOnlyCollection<IApplicationCommandOption> Options { get; } | IReadOnlyCollection<IApplicationCommandOption> Options { get; } | ||||
/// <summary> | /// <summary> | ||||
/// Gets the localization dictionary for the name field of this command. | |||||
/// </summary> | |||||
IReadOnlyDictionary<string, string> NameLocalizations { get; } | |||||
/// <summary> | |||||
/// Gets the localization dictionary for the description field of this command. | |||||
/// </summary> | |||||
IReadOnlyDictionary<string, string> DescriptionLocalizations { get; } | |||||
/// <summary> | |||||
/// Gets the localized name of this command. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// Only returned when the `withLocalizations` query parameter is set to <see langword="false"/> when requesting the command. | |||||
/// </remarks> | |||||
string NameLocalized { get; } | |||||
/// <summary> | |||||
/// Gets the localized description of this command. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// Only returned when the `withLocalizations` query parameter is set to <see langword="false"/> when requesting the command. | |||||
/// </remarks> | |||||
string DescriptionLocalized { get; } | |||||
/// <summary> | |||||
/// Modifies the current application command. | /// Modifies the current application command. | ||||
/// </summary> | /// </summary> | ||||
/// <param name="func">The new properties to use when modifying the command.</param> | /// <param name="func">The new properties to use when modifying the command.</param> | ||||
@@ -71,5 +71,31 @@ namespace Discord | |||||
/// Gets the allowed channel types for this option. | /// Gets the allowed channel types for this option. | ||||
/// </summary> | /// </summary> | ||||
IReadOnlyCollection<ChannelType> ChannelTypes { get; } | IReadOnlyCollection<ChannelType> ChannelTypes { get; } | ||||
/// <summary> | |||||
/// Gets the localization dictionary for the name field of this command option. | |||||
/// </summary> | |||||
IReadOnlyDictionary<string, string> NameLocalizations { get; } | |||||
/// <summary> | |||||
/// Gets the localization dictionary for the description field of this command option. | |||||
/// </summary> | |||||
IReadOnlyDictionary<string, string> DescriptionLocalizations { get; } | |||||
/// <summary> | |||||
/// Gets the localized name of this command option. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// Only returned when the `withLocalizations` query parameter is set to <see langword="false"/> when requesting the command. | |||||
/// </remarks> | |||||
string NameLocalized { get; } | |||||
/// <summary> | |||||
/// Gets the localized description of this command option. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// Only returned when the `withLocalizations` query parameter is set to true when requesting the command. | |||||
/// </remarks> | |||||
string DescriptionLocalized { get; } | |||||
} | } | ||||
} | } |
@@ -1,3 +1,5 @@ | |||||
using System.Collections.Generic; | |||||
namespace Discord | namespace Discord | ||||
{ | { | ||||
/// <summary> | /// <summary> | ||||
@@ -14,5 +16,18 @@ namespace Discord | |||||
/// Gets the value of the choice. | /// Gets the value of the choice. | ||||
/// </summary> | /// </summary> | ||||
object Value { get; } | object Value { get; } | ||||
/// <summary> | |||||
/// Gets the localization dictionary for the name field of this command option. | |||||
/// </summary> | |||||
IReadOnlyDictionary<string, string> NameLocalizations { get; } | |||||
/// <summary> | |||||
/// Gets the localized name of this command option. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// Only returned when the `withLocalizations` query parameter is set to <see langword="false"/> when requesting the command. | |||||
/// </remarks> | |||||
string NameLocalized { get; } | |||||
} | } | ||||
} | } |
@@ -1,6 +1,9 @@ | |||||
using System; | using System; | ||||
using System.Collections; | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Collections.Immutable; | |||||
using System.Linq; | using System.Linq; | ||||
using System.Net.Sockets; | |||||
using System.Text.RegularExpressions; | using System.Text.RegularExpressions; | ||||
namespace Discord | namespace Discord | ||||
@@ -31,18 +34,7 @@ namespace Discord | |||||
get => _name; | get => _name; | ||||
set | 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; | _name = value; | ||||
} | } | ||||
} | } | ||||
@@ -55,10 +47,7 @@ namespace Discord | |||||
get => _description; | get => _description; | ||||
set | set | ||||
{ | { | ||||
Preconditions.NotNullOrEmpty(value, nameof(Description)); | |||||
Preconditions.AtLeast(value.Length, 1, nameof(Description)); | |||||
Preconditions.AtMost(value.Length, MaxDescriptionLength, nameof(Description)); | |||||
EnsureValidCommandDescription(value); | |||||
_description = value; | _description = value; | ||||
} | } | ||||
} | } | ||||
@@ -77,6 +66,16 @@ namespace Discord | |||||
} | } | ||||
/// <summary> | /// <summary> | ||||
/// Gets the localization dictionary for the name field of this command. | |||||
/// </summary> | |||||
public IReadOnlyDictionary<string, string> NameLocalizations => _nameLocalizations; | |||||
/// <summary> | |||||
/// Gets the localization dictionary for the description field of this command. | |||||
/// </summary> | |||||
public IReadOnlyDictionary<string, string> DescriptionLocalizations => _descriptionLocalizations; | |||||
/// <summary> | |||||
/// Gets or sets whether the command is enabled by default when the app is added to a guild | /// Gets or sets whether the command is enabled by default when the app is added to a guild | ||||
/// </summary> | /// </summary> | ||||
public bool IsDefaultPermission { get; set; } = true; | public bool IsDefaultPermission { get; set; } = true; | ||||
@@ -93,6 +92,8 @@ namespace Discord | |||||
private string _name; | private string _name; | ||||
private string _description; | private string _description; | ||||
private Dictionary<string, string> _nameLocalizations; | |||||
private Dictionary<string, string> _descriptionLocalizations; | |||||
private List<SlashCommandOptionBuilder> _options; | private List<SlashCommandOptionBuilder> _options; | ||||
/// <summary> | /// <summary> | ||||
@@ -106,6 +107,8 @@ namespace Discord | |||||
Name = Name, | Name = Name, | ||||
Description = Description, | Description = Description, | ||||
IsDefaultPermission = IsDefaultPermission, | IsDefaultPermission = IsDefaultPermission, | ||||
NameLocalizations = _nameLocalizations, | |||||
DescriptionLocalizations = _descriptionLocalizations, | |||||
IsDMEnabled = IsDMEnabled, | IsDMEnabled = IsDMEnabled, | ||||
DefaultMemberPermissions = DefaultMemberPermissions ?? Optional<GuildPermission>.Unspecified | DefaultMemberPermissions = DefaultMemberPermissions ?? Optional<GuildPermission>.Unspecified | ||||
}; | }; | ||||
@@ -190,13 +193,17 @@ namespace Discord | |||||
/// <param name="isAutocomplete">If this option is set to autocomplete.</param> | /// <param name="isAutocomplete">If this option is set to autocomplete.</param> | ||||
/// <param name="options">The options of the option to add.</param> | /// <param name="options">The options of the option to add.</param> | ||||
/// <param name="channelTypes">The allowed channel types for this option.</param> | /// <param name="channelTypes">The allowed channel types for this option.</param> | ||||
/// <param name="nameLocalizations">Localization dictionary for the name field of this command.</param> | |||||
/// <param name="descriptionLocalizations">Localization dictionary for the description field of this command.</param> | |||||
/// <param name="choices">The choices of this option.</param> | /// <param name="choices">The choices of this option.</param> | ||||
/// <param name="minValue">The smallest number value the user can input.</param> | /// <param name="minValue">The smallest number value the user can input.</param> | ||||
/// <param name="maxValue">The largest number value the user can input.</param> | /// <param name="maxValue">The largest number value the user can input.</param> | ||||
/// <returns>The current builder.</returns> | /// <returns>The current builder.</returns> | ||||
public SlashCommandBuilder AddOption(string name, ApplicationCommandOptionType type, | public SlashCommandBuilder AddOption(string name, ApplicationCommandOptionType type, | ||||
string description, bool? isRequired = null, bool? isDefault = null, bool isAutocomplete = false, double? minValue = null, double? maxValue = null, | string description, bool? isRequired = null, bool? isDefault = null, bool isAutocomplete = false, double? minValue = null, double? maxValue = null, | ||||
int? minLength = null, int? maxLength = null, List<SlashCommandOptionBuilder> options = null, List<ChannelType> channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices) | |||||
List<SlashCommandOptionBuilder> options = null, List<ChannelType> channelTypes = null, IDictionary<string, string> nameLocalizations = null, | |||||
IDictionary<string, string> descriptionLocalizations = null, | |||||
int? minLength = null, int? maxLength = null, params ApplicationCommandOptionChoiceProperties[] choices) | |||||
{ | { | ||||
Preconditions.Options(name, description); | Preconditions.Options(name, description); | ||||
@@ -226,6 +233,12 @@ namespace Discord | |||||
MaxLength = maxLength, | MaxLength = maxLength, | ||||
}; | }; | ||||
if (nameLocalizations is not null) | |||||
option.WithNameLocalizations(nameLocalizations); | |||||
if (descriptionLocalizations is not null) | |||||
option.WithDescriptionLocalizations(descriptionLocalizations); | |||||
return AddOption(option); | return AddOption(option); | ||||
} | } | ||||
@@ -268,6 +281,116 @@ namespace Discord | |||||
Options.AddRange(options); | Options.AddRange(options); | ||||
return this; | return this; | ||||
} | } | ||||
/// <summary> | |||||
/// Sets the <see cref="NameLocalizations"/> collection. | |||||
/// </summary> | |||||
/// <param name="nameLocalizations">The localization dictionary to use for the name field of this command.</param> | |||||
/// <returns></returns> | |||||
/// <exception cref="ArgumentNullException">Thrown if <paramref name="nameLocalizations"/> is null.</exception> | |||||
/// <exception cref="ArgumentException">Thrown if any dictionary key is an invalid locale string.</exception> | |||||
public SlashCommandBuilder WithNameLocalizations(IDictionary<string, string> 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<string, string>(nameLocalizations); | |||||
return this; | |||||
} | |||||
/// <summary> | |||||
/// Sets the <see cref="DescriptionLocalizations"/> collection. | |||||
/// </summary> | |||||
/// <param name="descriptionLocalizations">The localization dictionary to use for the description field of this command.</param> | |||||
/// <returns></returns> | |||||
/// <exception cref="ArgumentNullException">Thrown if <paramref name="descriptionLocalizations"/> is null.</exception> | |||||
/// <exception cref="ArgumentException">Thrown if any dictionary key is an invalid locale string.</exception> | |||||
public SlashCommandBuilder WithDescriptionLocalizations(IDictionary<string, string> 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<string, string>(descriptionLocalizations); | |||||
return this; | |||||
} | |||||
/// <summary> | |||||
/// Adds a new entry to the <see cref="NameLocalizations"/> collection. | |||||
/// </summary> | |||||
/// <param name="locale">Locale of the entry.</param> | |||||
/// <param name="name">Localized string for the name field.</param> | |||||
/// <returns>The current builder.</returns> | |||||
/// <exception cref="ArgumentException">Thrown if <paramref name="locale"/> is an invalid locale string.</exception> | |||||
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; | |||||
} | |||||
/// <summary> | |||||
/// Adds a new entry to the <see cref="Description"/> collection. | |||||
/// </summary> | |||||
/// <param name="locale">Locale of the entry.</param> | |||||
/// <param name="description">Localized string for the description field.</param> | |||||
/// <returns>The current builder.</returns> | |||||
/// <exception cref="ArgumentException">Thrown if <paramref name="locale"/> is an invalid locale string.</exception> | |||||
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)); | |||||
} | |||||
} | } | ||||
/// <summary> | /// <summary> | ||||
@@ -287,6 +410,8 @@ namespace Discord | |||||
private string _name; | private string _name; | ||||
private string _description; | private string _description; | ||||
private Dictionary<string, string> _nameLocalizations; | |||||
private Dictionary<string, string> _descriptionLocalizations; | |||||
/// <summary> | /// <summary> | ||||
/// Gets or sets the name of this option. | /// Gets or sets the name of this option. | ||||
@@ -298,10 +423,7 @@ namespace Discord | |||||
{ | { | ||||
if (value != null) | 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; | _name = value; | ||||
@@ -318,8 +440,7 @@ namespace Discord | |||||
{ | { | ||||
if (value != null) | if (value != null) | ||||
{ | { | ||||
Preconditions.AtLeast(value.Length, 1, nameof(value)); | |||||
Preconditions.AtMost(value.Length, SlashCommandBuilder.MaxDescriptionLength, nameof(value)); | |||||
EnsureValidCommandOptionDescription(value); | |||||
} | } | ||||
_description = value; | _description = value; | ||||
@@ -382,6 +503,16 @@ namespace Discord | |||||
public List<ChannelType> ChannelTypes { get; set; } | public List<ChannelType> ChannelTypes { get; set; } | ||||
/// <summary> | /// <summary> | ||||
/// Gets the localization dictionary for the name field of this command. | |||||
/// </summary> | |||||
public IReadOnlyDictionary<string, string> NameLocalizations => _nameLocalizations; | |||||
/// <summary> | |||||
/// Gets the localization dictionary for the description field of this command. | |||||
/// </summary> | |||||
public IReadOnlyDictionary<string, string> DescriptionLocalizations => _descriptionLocalizations; | |||||
/// <summary> | |||||
/// Builds the current option. | /// Builds the current option. | ||||
/// </summary> | /// </summary> | ||||
/// <returns>The built version of this option.</returns> | /// <returns>The built version of this option.</returns> | ||||
@@ -424,6 +555,8 @@ namespace Discord | |||||
ChannelTypes = ChannelTypes, | ChannelTypes = ChannelTypes, | ||||
MinValue = MinValue, | MinValue = MinValue, | ||||
MaxValue = MaxValue, | MaxValue = MaxValue, | ||||
NameLocalizations = _nameLocalizations, | |||||
DescriptionLocalizations = _descriptionLocalizations, | |||||
MinLength = MinLength, | MinLength = MinLength, | ||||
MaxLength = MaxLength, | MaxLength = MaxLength, | ||||
}; | }; | ||||
@@ -440,13 +573,17 @@ namespace Discord | |||||
/// <param name="isAutocomplete">If this option supports autocomplete.</param> | /// <param name="isAutocomplete">If this option supports autocomplete.</param> | ||||
/// <param name="options">The options of the option to add.</param> | /// <param name="options">The options of the option to add.</param> | ||||
/// <param name="channelTypes">The allowed channel types for this option.</param> | /// <param name="channelTypes">The allowed channel types for this option.</param> | ||||
/// <param name="nameLocalizations">Localization dictionary for the description field of this command.</param> | |||||
/// <param name="descriptionLocalizations">Localization dictionary for the description field of this command.</param> | |||||
/// <param name="choices">The choices of this option.</param> | /// <param name="choices">The choices of this option.</param> | ||||
/// <param name="minValue">The smallest number value the user can input.</param> | /// <param name="minValue">The smallest number value the user can input.</param> | ||||
/// <param name="maxValue">The largest number value the user can input.</param> | /// <param name="maxValue">The largest number value the user can input.</param> | ||||
/// <returns>The current builder.</returns> | /// <returns>The current builder.</returns> | ||||
public SlashCommandOptionBuilder AddOption(string name, ApplicationCommandOptionType type, | public SlashCommandOptionBuilder AddOption(string name, ApplicationCommandOptionType type, | ||||
string description, bool? isRequired = null, bool isDefault = false, bool isAutocomplete = false, double? minValue = null, double? maxValue = null, | string description, bool? isRequired = null, bool isDefault = false, bool isAutocomplete = false, double? minValue = null, double? maxValue = null, | ||||
int? minLength = null, int? maxLength = null, List<SlashCommandOptionBuilder> options = null, List<ChannelType> channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices) | |||||
List<SlashCommandOptionBuilder> options = null, List<ChannelType> channelTypes = null, IDictionary<string, string> nameLocalizations = null, | |||||
IDictionary<string, string> descriptionLocalizations = null, | |||||
int? minLength = null, int? maxLength = null, params ApplicationCommandOptionChoiceProperties[] choices) | |||||
{ | { | ||||
Preconditions.Options(name, description); | Preconditions.Options(name, description); | ||||
@@ -473,9 +610,15 @@ namespace Discord | |||||
Options = options, | Options = options, | ||||
Type = type, | Type = type, | ||||
Choices = (choices ?? Array.Empty<ApplicationCommandOptionChoiceProperties>()).ToList(), | Choices = (choices ?? Array.Empty<ApplicationCommandOptionChoiceProperties>()).ToList(), | ||||
ChannelTypes = channelTypes | |||||
ChannelTypes = channelTypes, | |||||
}; | }; | ||||
if(nameLocalizations is not null) | |||||
option.WithNameLocalizations(nameLocalizations); | |||||
if(descriptionLocalizations is not null) | |||||
option.WithDescriptionLocalizations(descriptionLocalizations); | |||||
return AddOption(option); | return AddOption(option); | ||||
} | } | ||||
/// <summary> | /// <summary> | ||||
@@ -522,10 +665,11 @@ namespace Discord | |||||
/// </summary> | /// </summary> | ||||
/// <param name="name">The name of the choice.</param> | /// <param name="name">The name of the choice.</param> | ||||
/// <param name="value">The value of the choice.</param> | /// <param name="value">The value of the choice.</param> | ||||
/// <param name="nameLocalizations">The localization dictionary for to use the name field of this command option choice.</param> | |||||
/// <returns>The current builder.</returns> | /// <returns>The current builder.</returns> | ||||
public SlashCommandOptionBuilder AddChoice(string name, int value) | |||||
public SlashCommandOptionBuilder AddChoice(string name, int value, IDictionary<string, string> nameLocalizations = null) | |||||
{ | { | ||||
return AddChoiceInternal(name, value); | |||||
return AddChoiceInternal(name, value, nameLocalizations); | |||||
} | } | ||||
/// <summary> | /// <summary> | ||||
@@ -533,10 +677,11 @@ namespace Discord | |||||
/// </summary> | /// </summary> | ||||
/// <param name="name">The name of the choice.</param> | /// <param name="name">The name of the choice.</param> | ||||
/// <param name="value">The value of the choice.</param> | /// <param name="value">The value of the choice.</param> | ||||
/// <param name="nameLocalizations">The localization dictionary for to use the name field of this command option choice.</param> | |||||
/// <returns>The current builder.</returns> | /// <returns>The current builder.</returns> | ||||
public SlashCommandOptionBuilder AddChoice(string name, string value) | |||||
public SlashCommandOptionBuilder AddChoice(string name, string value, IDictionary<string, string> nameLocalizations = null) | |||||
{ | { | ||||
return AddChoiceInternal(name, value); | |||||
return AddChoiceInternal(name, value, nameLocalizations); | |||||
} | } | ||||
/// <summary> | /// <summary> | ||||
@@ -544,10 +689,11 @@ namespace Discord | |||||
/// </summary> | /// </summary> | ||||
/// <param name="name">The name of the choice.</param> | /// <param name="name">The name of the choice.</param> | ||||
/// <param name="value">The value of the choice.</param> | /// <param name="value">The value of the choice.</param> | ||||
/// <param name="nameLocalizations">Localization dictionary for the description field of this command.</param> | |||||
/// <returns>The current builder.</returns> | /// <returns>The current builder.</returns> | ||||
public SlashCommandOptionBuilder AddChoice(string name, double value) | |||||
public SlashCommandOptionBuilder AddChoice(string name, double value, IDictionary<string, string> nameLocalizations = null) | |||||
{ | { | ||||
return AddChoiceInternal(name, value); | |||||
return AddChoiceInternal(name, value, nameLocalizations); | |||||
} | } | ||||
/// <summary> | /// <summary> | ||||
@@ -555,10 +701,11 @@ namespace Discord | |||||
/// </summary> | /// </summary> | ||||
/// <param name="name">The name of the choice.</param> | /// <param name="name">The name of the choice.</param> | ||||
/// <param name="value">The value of the choice.</param> | /// <param name="value">The value of the choice.</param> | ||||
/// <param name="nameLocalizations">The localization dictionary to use for the name field of this command option choice.</param> | |||||
/// <returns>The current builder.</returns> | /// <returns>The current builder.</returns> | ||||
public SlashCommandOptionBuilder AddChoice(string name, float value) | |||||
public SlashCommandOptionBuilder AddChoice(string name, float value, IDictionary<string, string> nameLocalizations = null) | |||||
{ | { | ||||
return AddChoiceInternal(name, value); | |||||
return AddChoiceInternal(name, value, nameLocalizations); | |||||
} | } | ||||
/// <summary> | /// <summary> | ||||
@@ -566,13 +713,14 @@ namespace Discord | |||||
/// </summary> | /// </summary> | ||||
/// <param name="name">The name of the choice.</param> | /// <param name="name">The name of the choice.</param> | ||||
/// <param name="value">The value of the choice.</param> | /// <param name="value">The value of the choice.</param> | ||||
/// <param name="nameLocalizations">The localization dictionary to use for the name field of this command option choice.</param> | |||||
/// <returns>The current builder.</returns> | /// <returns>The current builder.</returns> | ||||
public SlashCommandOptionBuilder AddChoice(string name, long value) | |||||
public SlashCommandOptionBuilder AddChoice(string name, long value, IDictionary<string, string> 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<string, string> nameLocalizations = null) | |||||
{ | { | ||||
Choices ??= new List<ApplicationCommandOptionChoiceProperties>(); | Choices ??= new List<ApplicationCommandOptionChoiceProperties>(); | ||||
@@ -594,7 +742,8 @@ namespace Discord | |||||
Choices.Add(new ApplicationCommandOptionChoiceProperties | Choices.Add(new ApplicationCommandOptionChoiceProperties | ||||
{ | { | ||||
Name = name, | Name = name, | ||||
Value = value | |||||
Value = value, | |||||
NameLocalizations = nameLocalizations | |||||
}); | }); | ||||
return this; | return this; | ||||
@@ -724,5 +873,107 @@ namespace Discord | |||||
Type = type; | Type = type; | ||||
return this; | return this; | ||||
} | } | ||||
/// <summary> | |||||
/// Sets the <see cref="NameLocalizations"/> collection. | |||||
/// </summary> | |||||
/// <param name="nameLocalizations">The localization dictionary to use for the name field of this command option.</param> | |||||
/// <returns>The current builder.</returns> | |||||
/// <exception cref="ArgumentNullException">Thrown if <paramref name="nameLocalizations"/> is null.</exception> | |||||
/// <exception cref="ArgumentException">Thrown if any dictionary key is an invalid locale string.</exception> | |||||
public SlashCommandOptionBuilder WithNameLocalizations(IDictionary<string, string> 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<string, string>(nameLocalizations); | |||||
return this; | |||||
} | |||||
/// <summary> | |||||
/// Sets the <see cref="DescriptionLocalizations"/> collection. | |||||
/// </summary> | |||||
/// <param name="descriptionLocalizations">The localization dictionary to use for the description field of this command option.</param> | |||||
/// <returns>The current builder.</returns> | |||||
/// <exception cref="ArgumentNullException">Thrown if <paramref name="descriptionLocalizations"/> is null.</exception> | |||||
/// <exception cref="ArgumentException">Thrown if any dictionary key is an invalid locale string.</exception> | |||||
public SlashCommandOptionBuilder WithDescriptionLocalizations(IDictionary<string, string> 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<string, string>(descriptionLocalizations); | |||||
return this; | |||||
} | |||||
/// <summary> | |||||
/// Adds a new entry to the <see cref="NameLocalizations"/> collection. | |||||
/// </summary> | |||||
/// <param name="locale">Locale of the entry.</param> | |||||
/// <param name="name">Localized string for the name field.</param> | |||||
/// <returns>The current builder.</returns> | |||||
/// <exception cref="ArgumentException">Thrown if <paramref name="locale"/> is an invalid locale string.</exception> | |||||
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; | |||||
} | |||||
/// <summary> | |||||
/// Adds a new entry to the <see cref="DescriptionLocalizations"/> collection. | |||||
/// </summary> | |||||
/// <param name="locale">Locale of the entry.</param> | |||||
/// <param name="description">Localized string for the description field.</param> | |||||
/// <returns>The current builder.</returns> | |||||
/// <exception cref="ArgumentException">Thrown if <paramref name="locale"/> is an invalid locale string.</exception> | |||||
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)); | |||||
} | |||||
} | } | ||||
} | } |
@@ -0,0 +1,15 @@ | |||||
using System.Linq; | |||||
namespace System.Collections.Generic; | |||||
internal static class GenericCollectionExtensions | |||||
{ | |||||
public static void Deconstruct<T1, T2>(this KeyValuePair<T1, T2> kvp, out T1 value1, out T2 value2) | |||||
{ | |||||
value1 = kvp.Key; | |||||
value2 = kvp.Value; | |||||
} | |||||
public static Dictionary<T1, T2> ToDictionary<T1, T2>(this IEnumerable<KeyValuePair<T1, T2>> kvp) => | |||||
kvp.ToDictionary(x => x.Key, x => x.Value); | |||||
} |
@@ -155,12 +155,14 @@ namespace Discord | |||||
/// <summary> | /// <summary> | ||||
/// Gets a collection of all global commands. | /// Gets a collection of all global commands. | ||||
/// </summary> | /// </summary> | ||||
/// <param name="withLocalizations">Whether to include full localization dictionaries in the returned objects, instead of the name localized and description localized fields.</param> | |||||
/// <param name="locale">The target locale of the localized name and description fields. Sets <c>X-Discord-Locale</c> header, which takes precedence over <c>Accept-Language</c>.</param> | |||||
/// <param name="options">The options to be used when sending the request.</param> | /// <param name="options">The options to be used when sending the request.</param> | ||||
/// <returns> | /// <returns> | ||||
/// A task that represents the asynchronous get operation. The task result contains a read-only collection of global | /// A task that represents the asynchronous get operation. The task result contains a read-only collection of global | ||||
/// application commands. | /// application commands. | ||||
/// </returns> | /// </returns> | ||||
Task<IReadOnlyCollection<IApplicationCommand>> GetGlobalApplicationCommandsAsync(RequestOptions options = null); | |||||
Task<IReadOnlyCollection<IApplicationCommand>> GetGlobalApplicationCommandsAsync(bool withLocalizations = false, string locale = null, RequestOptions options = null); | |||||
/// <summary> | /// <summary> | ||||
/// Creates a global application command. | /// Creates a global application command. | ||||
@@ -30,9 +30,13 @@ namespace Discord.Net.Rest | |||||
/// <param name="cancelToken">The cancellation token used to cancel the task.</param> | /// <param name="cancelToken">The cancellation token used to cancel the task.</param> | ||||
/// <param name="headerOnly">Indicates whether to send the header only.</param> | /// <param name="headerOnly">Indicates whether to send the header only.</param> | ||||
/// <param name="reason">The audit log reason.</param> | /// <param name="reason">The audit log reason.</param> | ||||
/// <param name="requestHeaders">Additional headers to be sent with the request.</param> | |||||
/// <returns></returns> | /// <returns></returns> | ||||
Task<RestResponse> SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly = false, string reason = null); | |||||
Task<RestResponse> SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly = false, string reason = null); | |||||
Task<RestResponse> SendAsync(string method, string endpoint, IReadOnlyDictionary<string, object> multipartParams, CancellationToken cancelToken, bool headerOnly = false, string reason = null); | |||||
Task<RestResponse> SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly = false, string reason = null, | |||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>> requestHeaders = null); | |||||
Task<RestResponse> SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly = false, string reason = null, | |||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>> requestHeaders = null); | |||||
Task<RestResponse> SendAsync(string method, string endpoint, IReadOnlyDictionary<string, object> multipartParams, CancellationToken cancelToken, bool headerOnly = false, string reason = null, | |||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>> requestHeaders = null); | |||||
} | } | ||||
} | } |
@@ -1,5 +1,6 @@ | |||||
using Discord.Net; | using Discord.Net; | ||||
using System; | using System; | ||||
using System.Collections.Generic; | |||||
using System.Threading; | using System.Threading; | ||||
using System.Threading.Tasks; | 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 sets the maximum time to wait for this request to complete. | ||||
/// </summary> | /// </summary> | ||||
/// <remarks> | /// <remarks> | ||||
/// 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 | |||||
/// <c>null</c>, a request will not time out. If a rate limit has been triggered for this request's bucket | /// <c>null</c>, 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. | /// and will not be unpaused in time, this request will fail immediately. | ||||
/// </remarks> | /// </remarks> | ||||
@@ -53,7 +54,7 @@ namespace Discord | |||||
/// </summary> | /// </summary> | ||||
/// <remarks> | /// <remarks> | ||||
/// This property can also be set in <see cref="DiscordConfig"/>. | /// This property can also be set in <see cref="DiscordConfig"/>. | ||||
/// 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 | /// when millisecond precision is especially important, and the | ||||
/// hosting system is known to have a desynced clock. | /// hosting system is known to have a desynced clock. | ||||
/// </remarks> | /// </remarks> | ||||
@@ -70,8 +71,10 @@ namespace Discord | |||||
internal bool IsReactionBucket { get; set; } | internal bool IsReactionBucket { get; set; } | ||||
internal bool IsGatewayBucket { get; set; } | internal bool IsGatewayBucket { get; set; } | ||||
internal IDictionary<string, IEnumerable<string>> RequestHeaders { get; } | |||||
internal static RequestOptions CreateOrClone(RequestOptions options) | internal static RequestOptions CreateOrClone(RequestOptions options) | ||||
{ | |||||
{ | |||||
if (options == null) | if (options == null) | ||||
return new RequestOptions(); | return new RequestOptions(); | ||||
else | else | ||||
@@ -96,8 +99,9 @@ namespace Discord | |||||
public RequestOptions() | public RequestOptions() | ||||
{ | { | ||||
Timeout = DiscordConfig.DefaultRequestTimeout; | Timeout = DiscordConfig.DefaultRequestTimeout; | ||||
RequestHeaders = new Dictionary<string, IEnumerable<string>>(); | |||||
} | } | ||||
public RequestOptions Clone() => MemberwiseClone() as RequestOptions; | public RequestOptions Clone() => MemberwiseClone() as RequestOptions; | ||||
} | } | ||||
} | } |
@@ -55,7 +55,7 @@ namespace Discord | |||||
if (obj.Value == null) throw CreateNotNullException(name, msg); | if (obj.Value == null) throw CreateNotNullException(name, msg); | ||||
if (obj.Value.Trim().Length == 0) throw CreateNotEmptyException(name, msg); | if (obj.Value.Trim().Length == 0) throw CreateNotEmptyException(name, msg); | ||||
} | } | ||||
} | |||||
} | |||||
private static ArgumentException CreateNotEmptyException(string name, string msg) | private static ArgumentException CreateNotEmptyException(string name, string msg) | ||||
=> new ArgumentException(message: msg ?? "Argument cannot be blank.", paramName: name); | => new ArgumentException(message: msg ?? "Argument cannot be blank.", paramName: name); | ||||
@@ -129,7 +129,7 @@ namespace Discord | |||||
private static ArgumentException CreateNotEqualException<T>(string name, string msg, T value) | private static ArgumentException CreateNotEqualException<T>(string name, string msg, T value) | ||||
=> new ArgumentException(message: msg ?? $"Value may not be equal to {value}.", paramName: name); | => new ArgumentException(message: msg ?? $"Value may not be equal to {value}.", paramName: name); | ||||
/// <exception cref="ArgumentException">Value must be at least <paramref name="value"/>.</exception> | /// <exception cref="ArgumentException">Value must be at least <paramref name="value"/>.</exception> | ||||
public static void AtLeast(sbyte obj, sbyte value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } | public static void AtLeast(sbyte obj, sbyte value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } | ||||
/// <exception cref="ArgumentException">Value must be at least <paramref name="value"/>.</exception> | /// <exception cref="ArgumentException">Value must be at least <paramref name="value"/>.</exception> | ||||
@@ -165,7 +165,7 @@ namespace Discord | |||||
private static ArgumentException CreateAtLeastException<T>(string name, string msg, T value) | private static ArgumentException CreateAtLeastException<T>(string name, string msg, T value) | ||||
=> new ArgumentException(message: msg ?? $"Value must be at least {value}.", paramName: name); | => new ArgumentException(message: msg ?? $"Value must be at least {value}.", paramName: name); | ||||
/// <exception cref="ArgumentException">Value must be greater than <paramref name="value"/>.</exception> | /// <exception cref="ArgumentException">Value must be greater than <paramref name="value"/>.</exception> | ||||
public static void GreaterThan(sbyte obj, sbyte value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } | public static void GreaterThan(sbyte obj, sbyte value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } | ||||
/// <exception cref="ArgumentException">Value must be greater than <paramref name="value"/>.</exception> | /// <exception cref="ArgumentException">Value must be greater than <paramref name="value"/>.</exception> | ||||
@@ -201,7 +201,7 @@ namespace Discord | |||||
private static ArgumentException CreateGreaterThanException<T>(string name, string msg, T value) | private static ArgumentException CreateGreaterThanException<T>(string name, string msg, T value) | ||||
=> new ArgumentException(message: msg ?? $"Value must be greater than {value}.", paramName: name); | => new ArgumentException(message: msg ?? $"Value must be greater than {value}.", paramName: name); | ||||
/// <exception cref="ArgumentException">Value must be at most <paramref name="value"/>.</exception> | /// <exception cref="ArgumentException">Value must be at most <paramref name="value"/>.</exception> | ||||
public static void AtMost(sbyte obj, sbyte value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } | public static void AtMost(sbyte obj, sbyte value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } | ||||
/// <exception cref="ArgumentException">Value must be at most <paramref name="value"/>.</exception> | /// <exception cref="ArgumentException">Value must be at most <paramref name="value"/>.</exception> | ||||
@@ -237,7 +237,7 @@ namespace Discord | |||||
private static ArgumentException CreateAtMostException<T>(string name, string msg, T value) | private static ArgumentException CreateAtMostException<T>(string name, string msg, T value) | ||||
=> new ArgumentException(message: msg ?? $"Value must be at most {value}.", paramName: name); | => new ArgumentException(message: msg ?? $"Value must be at most {value}.", paramName: name); | ||||
/// <exception cref="ArgumentException">Value must be less than <paramref name="value"/>.</exception> | /// <exception cref="ArgumentException">Value must be less than <paramref name="value"/>.</exception> | ||||
public static void LessThan(sbyte obj, sbyte value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } | public static void LessThan(sbyte obj, sbyte value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } | ||||
/// <exception cref="ArgumentException">Value must be less than <paramref name="value"/>.</exception> | /// <exception cref="ArgumentException">Value must be less than <paramref name="value"/>.</exception> | ||||
@@ -83,6 +83,11 @@ namespace Discord.Interactions | |||||
public event Func<ModalCommandInfo, IInteractionContext, IResult, Task> ModalCommandExecuted { add { _modalCommandExecutedEvent.Add(value); } remove { _modalCommandExecutedEvent.Remove(value); } } | public event Func<ModalCommandInfo, IInteractionContext, IResult, Task> ModalCommandExecuted { add { _modalCommandExecutedEvent.Add(value); } remove { _modalCommandExecutedEvent.Remove(value); } } | ||||
internal readonly AsyncEvent<Func<ModalCommandInfo, IInteractionContext, IResult, Task>> _modalCommandExecutedEvent = new(); | internal readonly AsyncEvent<Func<ModalCommandInfo, IInteractionContext, IResult, Task>> _modalCommandExecutedEvent = new(); | ||||
/// <summary> | |||||
/// Get the <see cref="ILocalizationManager"/> used by this Interaction Service instance to localize strings. | |||||
/// </summary> | |||||
public ILocalizationManager LocalizationManager { get; set; } | |||||
private readonly ConcurrentDictionary<Type, ModuleInfo> _typedModuleDefs; | private readonly ConcurrentDictionary<Type, ModuleInfo> _typedModuleDefs; | ||||
private readonly CommandMap<SlashCommandInfo> _slashCommandMap; | private readonly CommandMap<SlashCommandInfo> _slashCommandMap; | ||||
private readonly ConcurrentDictionary<ApplicationCommandType, CommandMap<ContextCommandInfo>> _contextCommandMaps; | private readonly ConcurrentDictionary<ApplicationCommandType, CommandMap<ContextCommandInfo>> _contextCommandMaps; | ||||
@@ -203,6 +208,7 @@ namespace Discord.Interactions | |||||
_enableAutocompleteHandlers = config.EnableAutocompleteHandlers; | _enableAutocompleteHandlers = config.EnableAutocompleteHandlers; | ||||
_autoServiceScopes = config.AutoServiceScopes; | _autoServiceScopes = config.AutoServiceScopes; | ||||
_restResponseCallback = config.RestResponseCallback; | _restResponseCallback = config.RestResponseCallback; | ||||
LocalizationManager = config.LocalizationManager; | |||||
_typeConverterMap = new TypeMap<TypeConverter, IApplicationCommandInteractionDataOption>(this, new ConcurrentDictionary<Type, TypeConverter> | _typeConverterMap = new TypeMap<TypeConverter, IApplicationCommandInteractionDataOption>(this, new ConcurrentDictionary<Type, TypeConverter> | ||||
{ | { | ||||
@@ -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. | /// Gets or sets whether a command execution should exit when a modal command encounters a missing modal component value. | ||||
/// </summary> | /// </summary> | ||||
public bool ExitOnMissingModalField { get; set; } = false; | public bool ExitOnMissingModalField { get; set; } = false; | ||||
/// <summary> | |||||
/// Localization provider to be used when registering application commands. | |||||
/// </summary> | |||||
public ILocalizationManager LocalizationManager { get; set; } | |||||
} | } | ||||
/// <summary> | /// <summary> | ||||
@@ -0,0 +1,32 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.Interactions | |||||
{ | |||||
/// <summary> | |||||
/// Respresents a localization provider for Discord Application Commands. | |||||
/// </summary> | |||||
public interface ILocalizationManager | |||||
{ | |||||
/// <summary> | |||||
/// Get every the resource name for every available locale. | |||||
/// </summary> | |||||
/// <param name="key">Location of the resource.</param> | |||||
/// <param name="destinationType">Type of the resource.</param> | |||||
/// <returns> | |||||
/// A dictionary containing every available locale and the resource name. | |||||
/// </returns> | |||||
IDictionary<string, string> GetAllNames(IList<string> key, LocalizationTarget destinationType); | |||||
/// <summary> | |||||
/// Get every the resource description for every available locale. | |||||
/// </summary> | |||||
/// <param name="key">Location of the resource.</param> | |||||
/// <param name="destinationType">Type of the resource.</param> | |||||
/// <returns> | |||||
/// A dictionary containing every available locale and the resource name. | |||||
/// </returns> | |||||
IDictionary<string, string> GetAllDescriptions(IList<string> key, LocalizationTarget destinationType); | |||||
} | |||||
} |
@@ -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 | |||||
{ | |||||
/// <summary> | |||||
/// The default localization provider for Json resource files. | |||||
/// </summary> | |||||
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+.(?<locale>\w{2}(?:-\w{2})?).json", RegexOptions.Compiled | RegexOptions.Singleline); | |||||
/// <summary> | |||||
/// Initializes a new instance of the <see cref="JsonLocalizationManager"/> class. | |||||
/// </summary> | |||||
/// <param name="basePath">Base path of the Json file.</param> | |||||
/// <param name="fileName">Name of the Json file.</param> | |||||
public JsonLocalizationManager(string basePath, string fileName) | |||||
{ | |||||
_basePath = basePath; | |||||
_fileName = fileName; | |||||
} | |||||
/// <inheritdoc /> | |||||
public IDictionary<string, string> GetAllDescriptions(IList<string> key, LocalizationTarget destinationType) => | |||||
GetValues(key, DescriptionIdentifier); | |||||
/// <inheritdoc /> | |||||
public IDictionary<string, string> GetAllNames(IList<string> key, LocalizationTarget destinationType) => | |||||
GetValues(key, NameIdentifier); | |||||
private string[] GetAllFiles() => | |||||
Directory.GetFiles(_basePath, $"{_fileName}.*.json", SearchOption.TopDirectoryOnly); | |||||
private IDictionary<string, string> GetValues(IList<string> key, string identifier) | |||||
{ | |||||
var result = new Dictionary<string, string>(); | |||||
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; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,55 @@ | |||||
using System.Collections.Generic; | |||||
using System.Globalization; | |||||
using System.Reflection; | |||||
using System.Resources; | |||||
namespace Discord.Interactions | |||||
{ | |||||
/// <summary> | |||||
/// The default localization provider for Resx files. | |||||
/// </summary> | |||||
public sealed class ResxLocalizationManager : ILocalizationManager | |||||
{ | |||||
private const string NameIdentifier = "name"; | |||||
private const string DescriptionIdentifier = "description"; | |||||
private readonly ResourceManager _resourceManager; | |||||
private readonly IEnumerable<CultureInfo> _supportedLocales; | |||||
/// <summary> | |||||
/// Initializes a new instance of the <see cref="ResxLocalizationManager"/> class. | |||||
/// </summary> | |||||
/// <param name="baseResource">Name of the base resource.</param> | |||||
/// <param name="assembly">The main assembly for the resources.</param> | |||||
/// <param name="supportedLocales">Cultures the <see cref="ResxLocalizationManager"/> should search for.</param> | |||||
public ResxLocalizationManager(string baseResource, Assembly assembly, params CultureInfo[] supportedLocales) | |||||
{ | |||||
_supportedLocales = supportedLocales; | |||||
_resourceManager = new ResourceManager(baseResource, assembly); | |||||
} | |||||
/// <inheritdoc /> | |||||
public IDictionary<string, string> GetAllDescriptions(IList<string> key, LocalizationTarget destinationType) => | |||||
GetValues(key, DescriptionIdentifier); | |||||
/// <inheritdoc /> | |||||
public IDictionary<string, string> GetAllNames(IList<string> key, LocalizationTarget destinationType) => | |||||
GetValues(key, NameIdentifier); | |||||
private IDictionary<string, string> GetValues(IList<string> key, string identifier) | |||||
{ | |||||
var entryKey = (string.Join(".", key) + "." + identifier); | |||||
var result = new Dictionary<string, string>(); | |||||
foreach (var locale in _supportedLocales) | |||||
{ | |||||
var value = _resourceManager.GetString(entryKey, locale); | |||||
if (value is not null) | |||||
result[locale.Name] = value; | |||||
} | |||||
return result; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,25 @@ | |||||
namespace Discord.Interactions | |||||
{ | |||||
/// <summary> | |||||
/// Resource targets for localization. | |||||
/// </summary> | |||||
public enum LocalizationTarget | |||||
{ | |||||
/// <summary> | |||||
/// Target is a <see cref="IInteractionModuleBase"/> tagged with a <see cref="GroupAttribute"/>. | |||||
/// </summary> | |||||
Group, | |||||
/// <summary> | |||||
/// Target is an application command method. | |||||
/// </summary> | |||||
Command, | |||||
/// <summary> | |||||
/// Target is a Slash Command parameter. | |||||
/// </summary> | |||||
Parameter, | |||||
/// <summary> | |||||
/// Target is a Slash Command parameter choice. | |||||
/// </summary> | |||||
Choice | |||||
} | |||||
} |
@@ -42,7 +42,7 @@ namespace Discord.Interactions | |||||
public void RemoveCommand(T command) | public void RemoveCommand(T command) | ||||
{ | { | ||||
var key = ParseCommandName(command); | |||||
var key = CommandHierarchy.GetCommandPath(command); | |||||
_root.RemoveCommand(key, 0); | _root.RemoveCommand(key, 0); | ||||
} | } | ||||
@@ -60,28 +60,9 @@ namespace Discord.Interactions | |||||
private void AddCommand(T command) | private void AddCommand(T command) | ||||
{ | { | ||||
var key = ParseCommandName(command); | |||||
var key = CommandHierarchy.GetCommandPath(command); | |||||
_root.AddCommand(key, 0, command); | _root.AddCommand(key, 0, command); | ||||
} | } | ||||
private IList<string> ParseCommandName(T command) | |||||
{ | |||||
var keywords = new List<string>() { 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; | |||||
} | |||||
} | } | ||||
} | } |
@@ -1,5 +1,6 @@ | |||||
using System; | using System; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Collections.Immutable; | |||||
using System.Linq; | using System.Linq; | ||||
namespace Discord.Interactions | namespace Discord.Interactions | ||||
@@ -9,6 +10,9 @@ namespace Discord.Interactions | |||||
#region Parameters | #region Parameters | ||||
public static ApplicationCommandOptionProperties ToApplicationCommandOptionProps(this SlashCommandParameterInfo parameterInfo) | public static ApplicationCommandOptionProperties ToApplicationCommandOptionProps(this SlashCommandParameterInfo parameterInfo) | ||||
{ | { | ||||
var localizationManager = parameterInfo.Command.Module.CommandService.LocalizationManager; | |||||
var parameterPath = parameterInfo.GetParameterPath(); | |||||
var props = new ApplicationCommandOptionProperties | var props = new ApplicationCommandOptionProperties | ||||
{ | { | ||||
Name = parameterInfo.Name, | Name = parameterInfo.Name, | ||||
@@ -18,12 +22,15 @@ namespace Discord.Interactions | |||||
Choices = parameterInfo.Choices?.Select(x => new ApplicationCommandOptionChoiceProperties | Choices = parameterInfo.Choices?.Select(x => new ApplicationCommandOptionChoiceProperties | ||||
{ | { | ||||
Name = x.Name, | Name = x.Name, | ||||
Value = x.Value | |||||
Value = x.Value, | |||||
NameLocalizations = localizationManager?.GetAllNames(parameterInfo.GetChoicePath(x), LocalizationTarget.Choice) ?? ImmutableDictionary<string, string>.Empty | |||||
})?.ToList(), | })?.ToList(), | ||||
ChannelTypes = parameterInfo.ChannelTypes?.ToList(), | ChannelTypes = parameterInfo.ChannelTypes?.ToList(), | ||||
IsAutocomplete = parameterInfo.IsAutocomplete, | IsAutocomplete = parameterInfo.IsAutocomplete, | ||||
MaxValue = parameterInfo.MaxValue, | MaxValue = parameterInfo.MaxValue, | ||||
MinValue = parameterInfo.MinValue, | MinValue = parameterInfo.MinValue, | ||||
NameLocalizations = localizationManager?.GetAllNames(parameterPath, LocalizationTarget.Parameter) ?? ImmutableDictionary<string, string>.Empty, | |||||
DescriptionLocalizations = localizationManager?.GetAllDescriptions(parameterPath, LocalizationTarget.Parameter) ?? ImmutableDictionary<string, string>.Empty, | |||||
MinLength = parameterInfo.MinLength, | MinLength = parameterInfo.MinLength, | ||||
MaxLength = parameterInfo.MaxLength, | MaxLength = parameterInfo.MaxLength, | ||||
}; | }; | ||||
@@ -38,13 +45,19 @@ namespace Discord.Interactions | |||||
public static SlashCommandProperties ToApplicationCommandProps(this SlashCommandInfo commandInfo) | public static SlashCommandProperties ToApplicationCommandProps(this SlashCommandInfo commandInfo) | ||||
{ | { | ||||
var commandPath = commandInfo.GetCommandPath(); | |||||
var localizationManager = commandInfo.Module.CommandService.LocalizationManager; | |||||
var props = new SlashCommandBuilder() | var props = new SlashCommandBuilder() | ||||
{ | { | ||||
Name = commandInfo.Name, | Name = commandInfo.Name, | ||||
Description = commandInfo.Description, | Description = commandInfo.Description, | ||||
IsDefaultPermission = commandInfo.DefaultPermission, | |||||
IsDMEnabled = commandInfo.IsEnabledInDm, | IsDMEnabled = commandInfo.IsEnabledInDm, | ||||
DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), | DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), | ||||
}.Build(); | |||||
}.WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary<string, string>.Empty) | |||||
.WithDescriptionLocalizations(localizationManager?.GetAllDescriptions(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary<string, string>.Empty) | |||||
.Build(); | |||||
if (commandInfo.Parameters.Count > SlashCommandBuilder.MaxOptionsCount) | if (commandInfo.Parameters.Count > SlashCommandBuilder.MaxOptionsCount) | ||||
throw new InvalidOperationException($"Slash Commands cannot have more than {SlashCommandBuilder.MaxOptionsCount} command parameters"); | throw new InvalidOperationException($"Slash Commands cannot have more than {SlashCommandBuilder.MaxOptionsCount} command parameters"); | ||||
@@ -54,18 +67,30 @@ namespace Discord.Interactions | |||||
return props; | 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, | Name = commandInfo.Name, | ||||
Description = commandInfo.Description, | Description = commandInfo.Description, | ||||
Type = ApplicationCommandOptionType.SubCommand, | Type = ApplicationCommandOptionType.SubCommand, | ||||
IsRequired = false, | 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<string, string>.Empty, | |||||
DescriptionLocalizations = localizationManager?.GetAllDescriptions(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary<string, string>.Empty | |||||
}; | }; | ||||
} | |||||
public static ApplicationCommandProperties ToApplicationCommandProps(this ContextCommandInfo commandInfo) | 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 | ApplicationCommandType.Message => new MessageCommandBuilder | ||||
{ | { | ||||
@@ -73,16 +98,21 @@ namespace Discord.Interactions | |||||
IsDefaultPermission = commandInfo.DefaultPermission, | IsDefaultPermission = commandInfo.DefaultPermission, | ||||
DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), | DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), | ||||
IsDMEnabled = commandInfo.IsEnabledInDm | IsDMEnabled = commandInfo.IsEnabledInDm | ||||
}.Build(), | |||||
} | |||||
.WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary<string, string>.Empty) | |||||
.Build(), | |||||
ApplicationCommandType.User => new UserCommandBuilder | ApplicationCommandType.User => new UserCommandBuilder | ||||
{ | { | ||||
Name = commandInfo.Name, | Name = commandInfo.Name, | ||||
IsDefaultPermission = commandInfo.DefaultPermission, | IsDefaultPermission = commandInfo.DefaultPermission, | ||||
DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), | DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), | ||||
IsDMEnabled = commandInfo.IsEnabledInDm | IsDMEnabled = commandInfo.IsEnabledInDm | ||||
}.Build(), | |||||
} | |||||
.WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary<string, string>.Empty) | |||||
.Build(), | |||||
_ => throw new InvalidOperationException($"{commandInfo.CommandType} isn't a supported command type.") | _ => throw new InvalidOperationException($"{commandInfo.CommandType} isn't a supported command type.") | ||||
}; | }; | ||||
} | |||||
#endregion | #endregion | ||||
#region Modules | #region Modules | ||||
@@ -123,6 +153,9 @@ namespace Discord.Interactions | |||||
options.AddRange(moduleInfo.SubModules?.SelectMany(x => x.ParseSubModule(args, ignoreDontRegister))); | options.AddRange(moduleInfo.SubModules?.SelectMany(x => x.ParseSubModule(args, ignoreDontRegister))); | ||||
var localizationManager = moduleInfo.CommandService.LocalizationManager; | |||||
var modulePath = moduleInfo.GetModulePath(); | |||||
var props = new SlashCommandBuilder | var props = new SlashCommandBuilder | ||||
{ | { | ||||
Name = moduleInfo.SlashGroupName, | Name = moduleInfo.SlashGroupName, | ||||
@@ -130,7 +163,10 @@ namespace Discord.Interactions | |||||
IsDefaultPermission = moduleInfo.DefaultPermission, | IsDefaultPermission = moduleInfo.DefaultPermission, | ||||
IsDMEnabled = moduleInfo.IsEnabledInDm, | IsDMEnabled = moduleInfo.IsEnabledInDm, | ||||
DefaultMemberPermissions = moduleInfo.DefaultMemberPermissions | DefaultMemberPermissions = moduleInfo.DefaultMemberPermissions | ||||
}.Build(); | |||||
} | |||||
.WithNameLocalizations(localizationManager?.GetAllNames(modulePath, LocalizationTarget.Group) ?? ImmutableDictionary<string, string>.Empty) | |||||
.WithDescriptionLocalizations(localizationManager?.GetAllDescriptions(modulePath, LocalizationTarget.Group) ?? ImmutableDictionary<string, string>.Empty) | |||||
.Build(); | |||||
if (options.Count > SlashCommandBuilder.MaxOptionsCount) | if (options.Count > SlashCommandBuilder.MaxOptionsCount) | ||||
throw new InvalidOperationException($"Slash Commands cannot have more than {SlashCommandBuilder.MaxOptionsCount} command parameters"); | throw new InvalidOperationException($"Slash Commands cannot have more than {SlashCommandBuilder.MaxOptionsCount} command parameters"); | ||||
@@ -168,7 +204,11 @@ namespace Discord.Interactions | |||||
Name = moduleInfo.SlashGroupName, | Name = moduleInfo.SlashGroupName, | ||||
Description = moduleInfo.Description, | Description = moduleInfo.Description, | ||||
Type = ApplicationCommandOptionType.SubCommandGroup, | Type = ApplicationCommandOptionType.SubCommandGroup, | ||||
Options = options | |||||
Options = options, | |||||
NameLocalizations = moduleInfo.CommandService.LocalizationManager?.GetAllNames(moduleInfo.GetModulePath(), LocalizationTarget.Group) | |||||
?? ImmutableDictionary<string, string>.Empty, | |||||
DescriptionLocalizations = moduleInfo.CommandService.LocalizationManager?.GetAllDescriptions(moduleInfo.GetModulePath(), LocalizationTarget.Group) | |||||
?? ImmutableDictionary<string, string>.Empty, | |||||
} }; | } }; | ||||
} | } | ||||
@@ -183,17 +223,29 @@ namespace Discord.Interactions | |||||
Name = command.Name, | Name = command.Name, | ||||
Description = command.Description, | Description = command.Description, | ||||
IsDefaultPermission = command.IsDefaultPermission, | IsDefaultPermission = command.IsDefaultPermission, | ||||
Options = command.Options?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional<List<ApplicationCommandOptionProperties>>.Unspecified | |||||
DefaultMemberPermissions = (GuildPermission)command.DefaultMemberPermissions.RawValue, | |||||
IsDMEnabled = command.IsEnabledInDm, | |||||
Options = command.Options?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional<List<ApplicationCommandOptionProperties>>.Unspecified, | |||||
NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty, | |||||
DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty, | |||||
}, | }, | ||||
ApplicationCommandType.User => new UserCommandProperties | ApplicationCommandType.User => new UserCommandProperties | ||||
{ | { | ||||
Name = command.Name, | Name = command.Name, | ||||
IsDefaultPermission = command.IsDefaultPermission | |||||
IsDefaultPermission = command.IsDefaultPermission, | |||||
DefaultMemberPermissions = (GuildPermission)command.DefaultMemberPermissions.RawValue, | |||||
IsDMEnabled = command.IsEnabledInDm, | |||||
NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty, | |||||
DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty | |||||
}, | }, | ||||
ApplicationCommandType.Message => new MessageCommandProperties | ApplicationCommandType.Message => new MessageCommandProperties | ||||
{ | { | ||||
Name = command.Name, | Name = command.Name, | ||||
IsDefaultPermission = command.IsDefaultPermission | |||||
IsDefaultPermission = command.IsDefaultPermission, | |||||
DefaultMemberPermissions = (GuildPermission)command.DefaultMemberPermissions.RawValue, | |||||
IsDMEnabled = command.IsEnabledInDm, | |||||
NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty, | |||||
DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty | |||||
}, | }, | ||||
_ => throw new InvalidOperationException($"Cannot create command properties for command type {command.Type}"), | _ => throw new InvalidOperationException($"Cannot create command properties for command type {command.Type}"), | ||||
}; | }; | ||||
@@ -206,18 +258,20 @@ namespace Discord.Interactions | |||||
Description = commandOption.Description, | Description = commandOption.Description, | ||||
Type = commandOption.Type, | Type = commandOption.Type, | ||||
IsRequired = commandOption.IsRequired, | IsRequired = commandOption.IsRequired, | ||||
ChannelTypes = commandOption.ChannelTypes?.ToList(), | |||||
IsAutocomplete = commandOption.IsAutocomplete.GetValueOrDefault(), | |||||
MinValue = commandOption.MinValue, | |||||
MaxValue = commandOption.MaxValue, | |||||
Choices = commandOption.Choices?.Select(x => new ApplicationCommandOptionChoiceProperties | Choices = commandOption.Choices?.Select(x => new ApplicationCommandOptionChoiceProperties | ||||
{ | { | ||||
Name = x.Name, | Name = x.Name, | ||||
Value = x.Value | Value = x.Value | ||||
}).ToList(), | }).ToList(), | ||||
Options = commandOption.Options?.Select(x => x.ToApplicationCommandOptionProps()).ToList(), | Options = commandOption.Options?.Select(x => x.ToApplicationCommandOptionProps()).ToList(), | ||||
NameLocalizations = commandOption.NameLocalizations?.ToImmutableDictionary(), | |||||
DescriptionLocalizations = commandOption.DescriptionLocalizations?.ToImmutableDictionary(), | |||||
MaxLength = commandOption.MaxLength, | MaxLength = commandOption.MaxLength, | ||||
MinLength = commandOption.MinLength, | 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<ModalBuilder> modifyModal = null) | public static Modal ToModal(this ModalInfo modalInfo, string customId, Action<ModalBuilder> modifyModal = null) | ||||
@@ -0,0 +1,53 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
namespace Discord.Interactions | |||||
{ | |||||
internal static class CommandHierarchy | |||||
{ | |||||
public const char EscapeChar = '$'; | |||||
public static IList<string> GetModulePath(this ModuleInfo moduleInfo) | |||||
{ | |||||
var result = new List<string>(); | |||||
var current = moduleInfo; | |||||
while (current is not null) | |||||
{ | |||||
if (current.IsSlashGroup) | |||||
result.Insert(0, current.SlashGroupName); | |||||
current = current.Parent; | |||||
} | |||||
return result; | |||||
} | |||||
public static IList<string> 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<string> GetParameterPath(this IParameterInfo parameterInfo) | |||||
{ | |||||
var path = parameterInfo.Command.GetCommandPath(); | |||||
path.Add(parameterInfo.Name); | |||||
return path; | |||||
} | |||||
public static IList<string> GetChoicePath(this IParameterInfo parameterInfo, ParameterChoice choice) | |||||
{ | |||||
var path = parameterInfo.GetParameterPath(); | |||||
path.Add(choice.Name); | |||||
return path; | |||||
} | |||||
public static IList<string> GetTypePath(Type type) => | |||||
new string[] { EscapeChar + type.FullName }; | |||||
} | |||||
} |
@@ -1,4 +1,5 @@ | |||||
using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
using System.Collections.Generic; | |||||
namespace Discord.API | namespace Discord.API | ||||
{ | { | ||||
@@ -25,6 +26,18 @@ namespace Discord.API | |||||
[JsonProperty("default_permission")] | [JsonProperty("default_permission")] | ||||
public Optional<bool> DefaultPermissions { get; set; } | public Optional<bool> DefaultPermissions { get; set; } | ||||
[JsonProperty("name_localizations")] | |||||
public Optional<Dictionary<string, string>> NameLocalizations { get; set; } | |||||
[JsonProperty("description_localizations")] | |||||
public Optional<Dictionary<string, string>> DescriptionLocalizations { get; set; } | |||||
[JsonProperty("name_localized")] | |||||
public Optional<string> NameLocalized { get; set; } | |||||
[JsonProperty("description_localized")] | |||||
public Optional<string> DescriptionLocalized { get; set; } | |||||
// V2 Permissions | // V2 Permissions | ||||
[JsonProperty("dm_permission")] | [JsonProperty("dm_permission")] | ||||
public Optional<bool?> DmPermission { get; set; } | public Optional<bool?> DmPermission { get; set; } | ||||
@@ -1,4 +1,5 @@ | |||||
using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
using System.Collections.Generic; | |||||
using System.Linq; | using System.Linq; | ||||
namespace Discord.API | namespace Discord.API | ||||
@@ -38,6 +39,18 @@ namespace Discord.API | |||||
[JsonProperty("channel_types")] | [JsonProperty("channel_types")] | ||||
public Optional<ChannelType[]> ChannelTypes { get; set; } | public Optional<ChannelType[]> ChannelTypes { get; set; } | ||||
[JsonProperty("name_localizations")] | |||||
public Optional<Dictionary<string, string>> NameLocalizations { get; set; } | |||||
[JsonProperty("description_localizations")] | |||||
public Optional<Dictionary<string, string>> DescriptionLocalizations { get; set; } | |||||
[JsonProperty("name_localized")] | |||||
public Optional<string> NameLocalized { get; set; } | |||||
[JsonProperty("description_localized")] | |||||
public Optional<string> DescriptionLocalized { get; set; } | |||||
[JsonProperty("min_length")] | [JsonProperty("min_length")] | ||||
public Optional<int> MinLength { get; set; } | public Optional<int> MinLength { get; set; } | ||||
@@ -69,6 +82,11 @@ namespace Discord.API | |||||
Name = cmd.Name; | Name = cmd.Name; | ||||
Type = cmd.Type; | Type = cmd.Type; | ||||
Description = cmd.Description; | Description = cmd.Description; | ||||
NameLocalizations = cmd.NameLocalizations?.ToDictionary() ?? Optional<Dictionary<string, string>>.Unspecified; | |||||
DescriptionLocalizations = cmd.DescriptionLocalizations?.ToDictionary() ?? Optional<Dictionary<string, string>>.Unspecified; | |||||
NameLocalized = cmd.NameLocalized; | |||||
DescriptionLocalized = cmd.DescriptionLocalized; | |||||
} | } | ||||
public ApplicationCommandOption(ApplicationCommandOptionProperties option) | public ApplicationCommandOption(ApplicationCommandOptionProperties option) | ||||
{ | { | ||||
@@ -94,6 +112,9 @@ namespace Discord.API | |||||
Type = option.Type; | Type = option.Type; | ||||
Description = option.Description; | Description = option.Description; | ||||
Autocomplete = option.IsAutocomplete; | Autocomplete = option.IsAutocomplete; | ||||
NameLocalizations = option.NameLocalizations?.ToDictionary() ?? Optional<Dictionary<string, string>>.Unspecified; | |||||
DescriptionLocalizations = option.DescriptionLocalizations?.ToDictionary() ?? Optional<Dictionary<string, string>>.Unspecified; | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -1,4 +1,5 @@ | |||||
using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
using System.Collections.Generic; | |||||
namespace Discord.API | namespace Discord.API | ||||
{ | { | ||||
@@ -9,5 +10,11 @@ namespace Discord.API | |||||
[JsonProperty("value")] | [JsonProperty("value")] | ||||
public object Value { get; set; } | public object Value { get; set; } | ||||
[JsonProperty("name_localizations")] | |||||
public Optional<Dictionary<string, string>> NameLocalizations { get; set; } | |||||
[JsonProperty("name_localized")] | |||||
public Optional<string> NameLocalized { get; set; } | |||||
} | } | ||||
} | } |
@@ -1,4 +1,8 @@ | |||||
using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
using System.Collections; | |||||
using System.Collections.Generic; | |||||
using System.Collections.Immutable; | |||||
using System.Linq; | |||||
namespace Discord.API.Rest | namespace Discord.API.Rest | ||||
{ | { | ||||
@@ -19,6 +23,12 @@ namespace Discord.API.Rest | |||||
[JsonProperty("default_permission")] | [JsonProperty("default_permission")] | ||||
public Optional<bool> DefaultPermission { get; set; } | public Optional<bool> DefaultPermission { get; set; } | ||||
[JsonProperty("name_localizations")] | |||||
public Optional<Dictionary<string, string>> NameLocalizations { get; set; } | |||||
[JsonProperty("description_localizations")] | |||||
public Optional<Dictionary<string, string>> DescriptionLocalizations { get; set; } | |||||
[JsonProperty("dm_permission")] | [JsonProperty("dm_permission")] | ||||
public Optional<bool?> DmPermission { get; set; } | public Optional<bool?> DmPermission { get; set; } | ||||
@@ -26,12 +36,15 @@ namespace Discord.API.Rest | |||||
public Optional<GuildPermission?> DefaultMemberPermission { get; set; } | public Optional<GuildPermission?> DefaultMemberPermission { get; set; } | ||||
public CreateApplicationCommandParams() { } | 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<string, string> nameLocalizations = null, IDictionary<string, string> descriptionLocalizations = null) | |||||
{ | { | ||||
Name = name; | Name = name; | ||||
Description = description; | Description = description; | ||||
Options = Optional.Create(options); | Options = Optional.Create(options); | ||||
Type = type; | Type = type; | ||||
NameLocalizations = nameLocalizations?.ToDictionary(x => x.Key, x => x.Value) ?? Optional<Dictionary<string, string>>.Unspecified; | |||||
DescriptionLocalizations = descriptionLocalizations?.ToDictionary(x => x.Key, x => x.Value) ?? Optional<Dictionary<string, string>>.Unspecified; | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -1,4 +1,5 @@ | |||||
using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
using System.Collections.Generic; | |||||
namespace Discord.API.Rest | namespace Discord.API.Rest | ||||
{ | { | ||||
@@ -15,5 +16,11 @@ namespace Discord.API.Rest | |||||
[JsonProperty("default_permission")] | [JsonProperty("default_permission")] | ||||
public Optional<bool> DefaultPermission { get; set; } | public Optional<bool> DefaultPermission { get; set; } | ||||
[JsonProperty("name_localizations")] | |||||
public Optional<Dictionary<string, string>> NameLocalizations { get; set; } | |||||
[JsonProperty("description_localizations")] | |||||
public Optional<Dictionary<string, string>> DescriptionLocalizations { get; set; } | |||||
} | } | ||||
} | } |
@@ -243,7 +243,7 @@ namespace Discord.Rest | |||||
=> Task.FromResult<IApplicationCommand>(null); | => Task.FromResult<IApplicationCommand>(null); | ||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
Task<IReadOnlyCollection<IApplicationCommand>> IDiscordClient.GetGlobalApplicationCommandsAsync(RequestOptions options) | |||||
Task<IReadOnlyCollection<IApplicationCommand>> IDiscordClient.GetGlobalApplicationCommandsAsync(bool withLocalizations, string locale, RequestOptions options) | |||||
=> Task.FromResult<IReadOnlyCollection<IApplicationCommand>>(ImmutableArray.Create<IApplicationCommand>()); | => Task.FromResult<IReadOnlyCollection<IApplicationCommand>>(ImmutableArray.Create<IApplicationCommand>()); | ||||
Task<IApplicationCommand> IDiscordClient.CreateGlobalApplicationCommand(ApplicationCommandProperties properties, RequestOptions options) | Task<IApplicationCommand> IDiscordClient.CreateGlobalApplicationCommand(ApplicationCommandProperties properties, RequestOptions options) | ||||
=> Task.FromResult<IApplicationCommand>(null); | => Task.FromResult<IApplicationCommand>(null); | ||||
@@ -194,10 +194,10 @@ namespace Discord.Rest | |||||
}; | }; | ||||
} | } | ||||
public static async Task<IReadOnlyCollection<RestGlobalCommand>> GetGlobalApplicationCommandsAsync(BaseDiscordClient client, | |||||
RequestOptions options = null) | |||||
public static async Task<IReadOnlyCollection<RestGlobalCommand>> 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()) | if (!response.Any()) | ||||
return Array.Empty<RestGlobalCommand>(); | return Array.Empty<RestGlobalCommand>(); | ||||
@@ -212,10 +212,10 @@ namespace Discord.Rest | |||||
return model != null ? RestGlobalCommand.Create(client, model) : null; | return model != null ? RestGlobalCommand.Create(client, model) : null; | ||||
} | } | ||||
public static async Task<IReadOnlyCollection<RestGuildCommand>> GetGuildApplicationCommandsAsync(BaseDiscordClient client, ulong guildId, | |||||
RequestOptions options = null) | |||||
public static async Task<IReadOnlyCollection<RestGuildCommand>> 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()) | if (!response.Any()) | ||||
return ImmutableArray.Create<RestGuildCommand>(); | return ImmutableArray.Create<RestGuildCommand>(); | ||||
@@ -8,6 +8,7 @@ using Newtonsoft.Json; | |||||
using System; | using System; | ||||
using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.ComponentModel.Design; | |||||
using System.Diagnostics; | using System.Diagnostics; | ||||
using System.Globalization; | using System.Globalization; | ||||
using System.IO; | using System.IO; | ||||
@@ -1212,11 +1213,22 @@ namespace Discord.API | |||||
#endregion | #endregion | ||||
#region Interactions | #region Interactions | ||||
public async Task<ApplicationCommand[]> GetGlobalApplicationCommandsAsync(RequestOptions options = null) | |||||
public async Task<ApplicationCommand[]> GetGlobalApplicationCommandsAsync(bool withLocalizations = false, string locale = null, RequestOptions options = null) | |||||
{ | { | ||||
options = RequestOptions.CreateOrClone(options); | options = RequestOptions.CreateOrClone(options); | ||||
return await SendAsync<ApplicationCommand[]>("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<ApplicationCommand[]>("GET", () => $"applications/{CurrentApplicationId}/commands{query}", | |||||
new BucketIds(), options: options).ConfigureAwait(false); | |||||
} | } | ||||
public async Task<ApplicationCommand> GetGlobalApplicationCommandAsync(ulong id, RequestOptions options = null) | public async Task<ApplicationCommand> GetGlobalApplicationCommandAsync(ulong id, RequestOptions options = null) | ||||
@@ -1281,13 +1293,24 @@ namespace Discord.API | |||||
return await SendJsonAsync<ApplicationCommand[]>("PUT", () => $"applications/{CurrentApplicationId}/commands", commands, new BucketIds(), options: options).ConfigureAwait(false); | return await SendJsonAsync<ApplicationCommand[]>("PUT", () => $"applications/{CurrentApplicationId}/commands", commands, new BucketIds(), options: options).ConfigureAwait(false); | ||||
} | } | ||||
public async Task<ApplicationCommand[]> GetGuildApplicationCommandsAsync(ulong guildId, RequestOptions options = null) | |||||
public async Task<ApplicationCommand[]> GetGuildApplicationCommandsAsync(ulong guildId, bool withLocalizations = false, string locale = null, RequestOptions options = null) | |||||
{ | { | ||||
options = RequestOptions.CreateOrClone(options); | options = RequestOptions.CreateOrClone(options); | ||||
var bucket = new BucketIds(guildId: guildId); | var bucket = new BucketIds(guildId: guildId); | ||||
return await SendAsync<ApplicationCommand[]>("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<ApplicationCommand[]>("GET", () => $"applications/{CurrentApplicationId}/guilds/{guildId}/commands{query}", | |||||
bucket, options: options).ConfigureAwait(false); | |||||
} | } | ||||
public async Task<ApplicationCommand> GetGuildApplicationCommandAsync(ulong guildId, ulong commandId, RequestOptions options = null) | public async Task<ApplicationCommand> GetGuildApplicationCommandAsync(ulong guildId, ulong commandId, RequestOptions options = null) | ||||
@@ -25,7 +25,7 @@ namespace Discord.Rest | |||||
/// Gets the logged-in user. | /// Gets the logged-in user. | ||||
/// </summary> | /// </summary> | ||||
public new RestSelfUser CurrentUser { get => base.CurrentUser as RestSelfUser; internal set => base.CurrentUser = value; } | public new RestSelfUser CurrentUser { get => base.CurrentUser as RestSelfUser; internal set => base.CurrentUser = value; } | ||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public DiscordRestClient() : this(new DiscordRestConfig()) { } | public DiscordRestClient() : this(new DiscordRestConfig()) { } | ||||
/// <summary> | /// <summary> | ||||
@@ -205,10 +205,10 @@ namespace Discord.Rest | |||||
=> ClientHelper.CreateGlobalApplicationCommandAsync(this, properties, options); | => ClientHelper.CreateGlobalApplicationCommandAsync(this, properties, options); | ||||
public Task<RestGuildCommand> CreateGuildCommand(ApplicationCommandProperties properties, ulong guildId, RequestOptions options = null) | public Task<RestGuildCommand> CreateGuildCommand(ApplicationCommandProperties properties, ulong guildId, RequestOptions options = null) | ||||
=> ClientHelper.CreateGuildApplicationCommandAsync(this, guildId, properties, options); | => ClientHelper.CreateGuildApplicationCommandAsync(this, guildId, properties, options); | ||||
public Task<IReadOnlyCollection<RestGlobalCommand>> GetGlobalApplicationCommands(RequestOptions options = null) | |||||
=> ClientHelper.GetGlobalApplicationCommandsAsync(this, options); | |||||
public Task<IReadOnlyCollection<RestGuildCommand>> GetGuildApplicationCommands(ulong guildId, RequestOptions options = null) | |||||
=> ClientHelper.GetGuildApplicationCommandsAsync(this, guildId, options); | |||||
public Task<IReadOnlyCollection<RestGlobalCommand>> GetGlobalApplicationCommands(bool withLocalizations = false, string locale = null, RequestOptions options = null) | |||||
=> ClientHelper.GetGlobalApplicationCommandsAsync(this, withLocalizations, locale, options); | |||||
public Task<IReadOnlyCollection<RestGuildCommand>> GetGuildApplicationCommands(ulong guildId, bool withLocalizations = false, string locale = null, RequestOptions options = null) | |||||
=> ClientHelper.GetGuildApplicationCommandsAsync(this, guildId, withLocalizations, locale, options); | |||||
public Task<IReadOnlyCollection<RestGlobalCommand>> BulkOverwriteGlobalCommands(ApplicationCommandProperties[] commandProperties, RequestOptions options = null) | public Task<IReadOnlyCollection<RestGlobalCommand>> BulkOverwriteGlobalCommands(ApplicationCommandProperties[] commandProperties, RequestOptions options = null) | ||||
=> ClientHelper.BulkOverwriteGlobalApplicationCommandAsync(this, commandProperties, options); | => ClientHelper.BulkOverwriteGlobalApplicationCommandAsync(this, commandProperties, options); | ||||
public Task<IReadOnlyCollection<RestGuildCommand>> BulkOverwriteGuildCommands(ApplicationCommandProperties[] commandProperties, ulong guildId, RequestOptions options = null) | public Task<IReadOnlyCollection<RestGuildCommand>> BulkOverwriteGuildCommands(ApplicationCommandProperties[] commandProperties, ulong guildId, RequestOptions options = null) | ||||
@@ -319,8 +319,8 @@ namespace Discord.Rest | |||||
=> await GetWebhookAsync(id, options).ConfigureAwait(false); | => await GetWebhookAsync(id, options).ConfigureAwait(false); | ||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
async Task<IReadOnlyCollection<IApplicationCommand>> IDiscordClient.GetGlobalApplicationCommandsAsync(RequestOptions options) | |||||
=> await GetGlobalApplicationCommands(options).ConfigureAwait(false); | |||||
async Task<IReadOnlyCollection<IApplicationCommand>> IDiscordClient.GetGlobalApplicationCommandsAsync(bool withLocalizations, string locale, RequestOptions options) | |||||
=> await GetGlobalApplicationCommands(withLocalizations, locale, options).ConfigureAwait(false); | |||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
async Task<IApplicationCommand> IDiscordClient.GetGlobalApplicationCommandAsync(ulong id, RequestOptions options) | async Task<IApplicationCommand> IDiscordClient.GetGlobalApplicationCommandAsync(ulong id, RequestOptions options) | ||||
=> await ClientHelper.GetGlobalApplicationCommandAsync(this, id, options).ConfigureAwait(false); | => await ClientHelper.GetGlobalApplicationCommandAsync(this, id, options).ConfigureAwait(false); | ||||
@@ -362,10 +362,10 @@ namespace Discord.Rest | |||||
#endregion | #endregion | ||||
#region Interactions | #region Interactions | ||||
public static async Task<IReadOnlyCollection<RestGuildCommand>> GetSlashCommandsAsync(IGuild guild, BaseDiscordClient client, | |||||
RequestOptions options) | |||||
public static async Task<IReadOnlyCollection<RestGuildCommand>> 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(); | return models.Select(x => RestGuildCommand.Create(client, x, guild.Id)).ToImmutableArray(); | ||||
} | } | ||||
public static async Task<RestGuildCommand> GetSlashCommandAsync(IGuild guild, ulong id, BaseDiscordClient client, | public static async Task<RestGuildCommand> GetSlashCommandAsync(IGuild guild, ulong id, BaseDiscordClient client, | ||||
@@ -311,13 +311,15 @@ namespace Discord.Rest | |||||
/// <summary> | /// <summary> | ||||
/// Gets a collection of slash commands created by the current user in this guild. | /// Gets a collection of slash commands created by the current user in this guild. | ||||
/// </summary> | /// </summary> | ||||
/// <param name="withLocalizations">Whether to include full localization dictionaries in the returned objects, instead of the name localized and description localized fields.</param> | |||||
/// <param name="locale">The target locale of the localized name and description fields. Sets <c>X-Discord-Locale</c> header, which takes precedence over <c>Accept-Language</c>.</param> | |||||
/// <param name="options">The options to be used when sending the request.</param> | /// <param name="options">The options to be used when sending the request.</param> | ||||
/// <returns> | /// <returns> | ||||
/// A task that represents the asynchronous get operation. The task result contains a read-only collection of | /// A task that represents the asynchronous get operation. The task result contains a read-only collection of | ||||
/// slash commands created by the current user. | /// slash commands created by the current user. | ||||
/// </returns> | /// </returns> | ||||
public Task<IReadOnlyCollection<RestGuildCommand>> GetSlashCommandsAsync(RequestOptions options = null) | |||||
=> GuildHelper.GetSlashCommandsAsync(this, Discord, options); | |||||
public Task<IReadOnlyCollection<RestGuildCommand>> GetSlashCommandsAsync(bool withLocalizations = false, string locale = null, RequestOptions options = null) | |||||
=> GuildHelper.GetSlashCommandsAsync(this, Discord, withLocalizations, locale, options); | |||||
/// <summary> | /// <summary> | ||||
/// Gets a slash command in the current guild. | /// Gets a slash command in the current guild. | ||||
@@ -928,13 +930,15 @@ namespace Discord.Rest | |||||
/// <summary> | /// <summary> | ||||
/// Gets this guilds slash commands | /// Gets this guilds slash commands | ||||
/// </summary> | /// </summary> | ||||
/// <param name="withLocalizations">Whether to include full localization dictionaries in the returned objects, instead of the name localized and description localized fields.</param> | |||||
/// <param name="locale">The target locale of the localized name and description fields. Sets <c>X-Discord-Locale</c> header, which takes precedence over <c>Accept-Language</c>.</param> | |||||
/// <param name="options">The options to be used when sending the request.</param> | /// <param name="options">The options to be used when sending the request.</param> | ||||
/// <returns> | /// <returns> | ||||
/// A task that represents the asynchronous get operation. The task result contains a read-only collection | /// A task that represents the asynchronous get operation. The task result contains a read-only collection | ||||
/// of application commands found within the guild. | /// of application commands found within the guild. | ||||
/// </returns> | /// </returns> | ||||
public async Task<IReadOnlyCollection<RestGuildCommand>> GetApplicationCommandsAsync (RequestOptions options = null) | |||||
=> await ClientHelper.GetGuildApplicationCommandsAsync(Discord, Id, options).ConfigureAwait(false); | |||||
public async Task<IReadOnlyCollection<RestGuildCommand>> GetApplicationCommandsAsync (bool withLocalizations = false, string locale = null, RequestOptions options = null) | |||||
=> await ClientHelper.GetGuildApplicationCommandsAsync(Discord, Id, withLocalizations, locale, options).ConfigureAwait(false); | |||||
/// <summary> | /// <summary> | ||||
/// Gets an application command within this guild with the specified id. | /// Gets an application command within this guild with the specified id. | ||||
/// </summary> | /// </summary> | ||||
@@ -1467,8 +1471,8 @@ namespace Discord.Rest | |||||
async Task<IReadOnlyCollection<IWebhook>> IGuild.GetWebhooksAsync(RequestOptions options) | async Task<IReadOnlyCollection<IWebhook>> IGuild.GetWebhooksAsync(RequestOptions options) | ||||
=> await GetWebhooksAsync(options).ConfigureAwait(false); | => await GetWebhooksAsync(options).ConfigureAwait(false); | ||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
async Task<IReadOnlyCollection<IApplicationCommand>> IGuild.GetApplicationCommandsAsync (RequestOptions options) | |||||
=> await GetApplicationCommandsAsync(options).ConfigureAwait(false); | |||||
async Task<IReadOnlyCollection<IApplicationCommand>> IGuild.GetApplicationCommandsAsync (bool withLocalizations, string locale, RequestOptions options) | |||||
=> await GetApplicationCommandsAsync(withLocalizations, locale, options).ConfigureAwait(false); | |||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
async Task<ICustomSticker> IGuild.CreateStickerAsync(string name, string description, IEnumerable<string> tags, Image image, RequestOptions options) | async Task<ICustomSticker> IGuild.CreateStickerAsync(string name, string description, IEnumerable<string> tags, Image image, RequestOptions options) | ||||
=> await CreateStickerAsync(name, description, tags, image, options); | => await CreateStickerAsync(name, description, tags, image, options); | ||||
@@ -3,6 +3,7 @@ using Discord.API.Rest; | |||||
using Discord.Net; | using Discord.Net; | ||||
using System; | using System; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Collections.Immutable; | |||||
using System.Linq; | using System.Linq; | ||||
using System.Net; | using System.Net; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
@@ -101,11 +102,12 @@ namespace Discord.Rest | |||||
DefaultPermission = arg.IsDefaultPermission.IsSpecified | DefaultPermission = arg.IsDefaultPermission.IsSpecified | ||||
? arg.IsDefaultPermission.Value | ? arg.IsDefaultPermission.Value | ||||
: Optional<bool>.Unspecified, | : Optional<bool>.Unspecified, | ||||
NameLocalizations = arg.NameLocalizations?.ToDictionary(), | |||||
DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary(), | |||||
// TODO: better conversion to nullable optionals | // TODO: better conversion to nullable optionals | ||||
DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(), | DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(), | ||||
DmPermission = arg.IsDMEnabled.ToNullable() | DmPermission = arg.IsDMEnabled.ToNullable() | ||||
}; | }; | ||||
if (arg is SlashCommandProperties slashProps) | if (arg is SlashCommandProperties slashProps) | ||||
@@ -140,12 +142,16 @@ namespace Discord.Rest | |||||
DefaultPermission = arg.IsDefaultPermission.IsSpecified | DefaultPermission = arg.IsDefaultPermission.IsSpecified | ||||
? arg.IsDefaultPermission.Value | ? arg.IsDefaultPermission.Value | ||||
: Optional<bool>.Unspecified, | : Optional<bool>.Unspecified, | ||||
NameLocalizations = arg.NameLocalizations?.ToDictionary(), | |||||
DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary(), | |||||
// TODO: better conversion to nullable optionals | // TODO: better conversion to nullable optionals | ||||
DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(), | DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(), | ||||
DmPermission = arg.IsDMEnabled.ToNullable() | DmPermission = arg.IsDMEnabled.ToNullable() | ||||
}; | }; | ||||
Console.WriteLine("Locales:" + string.Join(",", arg.NameLocalizations.Keys)); | |||||
if (arg is SlashCommandProperties slashProps) | if (arg is SlashCommandProperties slashProps) | ||||
{ | { | ||||
Preconditions.NotNullOrEmpty(slashProps.Description, nameof(slashProps.Description)); | Preconditions.NotNullOrEmpty(slashProps.Description, nameof(slashProps.Description)); | ||||
@@ -181,6 +187,8 @@ namespace Discord.Rest | |||||
DefaultPermission = arg.IsDefaultPermission.IsSpecified | DefaultPermission = arg.IsDefaultPermission.IsSpecified | ||||
? arg.IsDefaultPermission.Value | ? arg.IsDefaultPermission.Value | ||||
: Optional<bool>.Unspecified, | : Optional<bool>.Unspecified, | ||||
NameLocalizations = arg.NameLocalizations?.ToDictionary(), | |||||
DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary(), | |||||
// TODO: better conversion to nullable optionals | // TODO: better conversion to nullable optionals | ||||
DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(), | DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(), | ||||
@@ -244,7 +252,9 @@ namespace Discord.Rest | |||||
Name = args.Name, | Name = args.Name, | ||||
DefaultPermission = args.IsDefaultPermission.IsSpecified | DefaultPermission = args.IsDefaultPermission.IsSpecified | ||||
? args.IsDefaultPermission.Value | ? args.IsDefaultPermission.Value | ||||
: Optional<bool>.Unspecified | |||||
: Optional<bool>.Unspecified, | |||||
NameLocalizations = args.NameLocalizations?.ToDictionary(), | |||||
DescriptionLocalizations = args.DescriptionLocalizations?.ToDictionary() | |||||
}; | }; | ||||
if (args is SlashCommandProperties slashProps) | if (args is SlashCommandProperties slashProps) | ||||
@@ -299,6 +309,8 @@ namespace Discord.Rest | |||||
DefaultPermission = arg.IsDefaultPermission.IsSpecified | DefaultPermission = arg.IsDefaultPermission.IsSpecified | ||||
? arg.IsDefaultPermission.Value | ? arg.IsDefaultPermission.Value | ||||
: Optional<bool>.Unspecified, | : Optional<bool>.Unspecified, | ||||
NameLocalizations = arg.NameLocalizations?.ToDictionary(), | |||||
DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary(), | |||||
// TODO: better conversion to nullable optionals | // TODO: better conversion to nullable optionals | ||||
DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(), | DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(), | ||||
@@ -335,7 +347,9 @@ namespace Discord.Rest | |||||
Name = arg.Name, | Name = arg.Name, | ||||
DefaultPermission = arg.IsDefaultPermission.IsSpecified | DefaultPermission = arg.IsDefaultPermission.IsSpecified | ||||
? arg.IsDefaultPermission.Value | ? arg.IsDefaultPermission.Value | ||||
: Optional<bool>.Unspecified | |||||
: Optional<bool>.Unspecified, | |||||
NameLocalizations = arg.NameLocalizations?.ToDictionary(), | |||||
DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary() | |||||
}; | }; | ||||
if (arg is SlashCommandProperties slashProps) | if (arg is SlashCommandProperties slashProps) | ||||
@@ -38,6 +38,32 @@ namespace Discord.Rest | |||||
/// </summary> | /// </summary> | ||||
public IReadOnlyCollection<RestApplicationCommandOption> Options { get; private set; } | public IReadOnlyCollection<RestApplicationCommandOption> Options { get; private set; } | ||||
/// <summary> | |||||
/// Gets the localization dictionary for the name field of this command. | |||||
/// </summary> | |||||
public IReadOnlyDictionary<string, string> NameLocalizations { get; private set; } | |||||
/// <summary> | |||||
/// Gets the localization dictionary for the description field of this command. | |||||
/// </summary> | |||||
public IReadOnlyDictionary<string, string> DescriptionLocalizations { get; private set; } | |||||
/// <summary> | |||||
/// Gets the localized name of this command. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// Only returned when the `withLocalizations` query parameter is set to <see langword="false"/> when requesting the command. | |||||
/// </remarks> | |||||
public string NameLocalized { get; private set; } | |||||
/// <summary> | |||||
/// Gets the localized description of this command. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// Only returned when the `withLocalizations` query parameter is set to <see langword="false"/> when requesting the command. | |||||
/// </remarks> | |||||
public string DescriptionLocalized { get; private set; } | |||||
/// <inheritdoc/> | /// <inheritdoc/> | ||||
public DateTimeOffset CreatedAt | public DateTimeOffset CreatedAt | ||||
=> SnowflakeUtils.FromSnowflake(Id); | => SnowflakeUtils.FromSnowflake(Id); | ||||
@@ -64,6 +90,15 @@ namespace Discord.Rest | |||||
? model.Options.Value.Select(RestApplicationCommandOption.Create).ToImmutableArray() | ? model.Options.Value.Select(RestApplicationCommandOption.Create).ToImmutableArray() | ||||
: ImmutableArray.Create<RestApplicationCommandOption>(); | : ImmutableArray.Create<RestApplicationCommandOption>(); | ||||
NameLocalizations = model.NameLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ?? | |||||
ImmutableDictionary<string, string>.Empty; | |||||
DescriptionLocalizations = model.DescriptionLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ?? | |||||
ImmutableDictionary<string, string>.Empty; | |||||
NameLocalized = model.NameLocalized.GetValueOrDefault(); | |||||
DescriptionLocalized = model.DescriptionLocalized.GetValueOrDefault(); | |||||
IsEnabledInDm = model.DmPermission.GetValueOrDefault(true).GetValueOrDefault(true); | IsEnabledInDm = model.DmPermission.GetValueOrDefault(true).GetValueOrDefault(true); | ||||
DefaultMemberPermissions = new GuildPermissions((ulong)model.DefaultMemberPermission.GetValueOrDefault(0).GetValueOrDefault(0)); | DefaultMemberPermissions = new GuildPermissions((ulong)model.DefaultMemberPermission.GetValueOrDefault(0).GetValueOrDefault(0)); | ||||
} | } | ||||
@@ -1,3 +1,5 @@ | |||||
using System.Collections.Generic; | |||||
using System.Collections.Immutable; | |||||
using Model = Discord.API.ApplicationCommandOptionChoice; | using Model = Discord.API.ApplicationCommandOptionChoice; | ||||
namespace Discord.Rest | namespace Discord.Rest | ||||
@@ -13,10 +15,25 @@ namespace Discord.Rest | |||||
/// <inheritdoc/> | /// <inheritdoc/> | ||||
public object Value { get; } | public object Value { get; } | ||||
/// <summary> | |||||
/// Gets the localization dictionary for the name field of this command option choice. | |||||
/// </summary> | |||||
public IReadOnlyDictionary<string, string> NameLocalizations { get; } | |||||
/// <summary> | |||||
/// Gets the localized name of this command option choice. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// Only returned when the `withLocalizations` query parameter is set to <see langword="false"/> when requesting the command. | |||||
/// </remarks> | |||||
public string NameLocalized { get; } | |||||
internal RestApplicationCommandChoice(Model model) | internal RestApplicationCommandChoice(Model model) | ||||
{ | { | ||||
Name = model.Name; | Name = model.Name; | ||||
Value = model.Value; | Value = model.Value; | ||||
NameLocalizations = model.NameLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary(); | |||||
NameLocalized = model.NameLocalized.GetValueOrDefault(null); | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -27,7 +27,7 @@ namespace Discord.Rest | |||||
public bool? IsRequired { get; private set; } | public bool? IsRequired { get; private set; } | ||||
/// <inheritdoc/> | /// <inheritdoc/> | ||||
public bool? IsAutocomplete { get; private set; } | |||||
public bool? IsAutocomplete { get; private set; } | |||||
/// <inheritdoc/> | /// <inheritdoc/> | ||||
public double? MinValue { get; private set; } | public double? MinValue { get; private set; } | ||||
@@ -54,6 +54,32 @@ namespace Discord.Rest | |||||
/// <inheritdoc/> | /// <inheritdoc/> | ||||
public IReadOnlyCollection<ChannelType> ChannelTypes { get; private set; } | public IReadOnlyCollection<ChannelType> ChannelTypes { get; private set; } | ||||
/// <summary> | |||||
/// Gets the localization dictionary for the name field of this command option. | |||||
/// </summary> | |||||
public IReadOnlyDictionary<string, string> NameLocalizations { get; private set; } | |||||
/// <summary> | |||||
/// Gets the localization dictionary for the description field of this command option. | |||||
/// </summary> | |||||
public IReadOnlyDictionary<string, string> DescriptionLocalizations { get; private set; } | |||||
/// <summary> | |||||
/// Gets the localized name of this command option. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// Only returned when the `withLocalizations` query parameter is set to <see langword="false"/> when requesting the command. | |||||
/// </remarks> | |||||
public string NameLocalized { get; private set; } | |||||
/// <summary> | |||||
/// Gets the localized description of this command option. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// Only returned when the `withLocalizations` query parameter is set to <see langword="false"/> when requesting the command. | |||||
/// </remarks> | |||||
public string DescriptionLocalized { get; private set; } | |||||
internal RestApplicationCommandOption() { } | internal RestApplicationCommandOption() { } | ||||
internal static RestApplicationCommandOption Create(Model model) | internal static RestApplicationCommandOption Create(Model model) | ||||
@@ -98,6 +124,15 @@ namespace Discord.Rest | |||||
ChannelTypes = model.ChannelTypes.IsSpecified | ChannelTypes = model.ChannelTypes.IsSpecified | ||||
? model.ChannelTypes.Value.ToImmutableArray() | ? model.ChannelTypes.Value.ToImmutableArray() | ||||
: ImmutableArray.Create<ChannelType>(); | : ImmutableArray.Create<ChannelType>(); | ||||
NameLocalizations = model.NameLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ?? | |||||
ImmutableDictionary<string, string>.Empty; | |||||
DescriptionLocalizations = model.DescriptionLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ?? | |||||
ImmutableDictionary<string, string>.Empty; | |||||
NameLocalized = model.NameLocalized.GetValueOrDefault(); | |||||
DescriptionLocalized = model.DescriptionLocalized.GetValueOrDefault(); | |||||
} | } | ||||
#endregion | #endregion | ||||
@@ -66,33 +66,45 @@ namespace Discord.Net.Rest | |||||
_cancelToken = cancelToken; | _cancelToken = cancelToken; | ||||
} | } | ||||
public async Task<RestResponse> SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly, string reason = null) | |||||
public async Task<RestResponse> SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly, string reason = null, | |||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>> requestHeaders = null) | |||||
{ | { | ||||
string uri = Path.Combine(_baseUrl, endpoint); | string uri = Path.Combine(_baseUrl, endpoint); | ||||
using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) | using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) | ||||
{ | { | ||||
if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); | 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); | return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); | ||||
} | } | ||||
} | } | ||||
public async Task<RestResponse> SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly, string reason = null) | |||||
public async Task<RestResponse> SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly, string reason = null, | |||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>> requestHeaders = null) | |||||
{ | { | ||||
string uri = Path.Combine(_baseUrl, endpoint); | string uri = Path.Combine(_baseUrl, endpoint); | ||||
using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) | using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) | ||||
{ | { | ||||
if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); | 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"); | restRequest.Content = new StringContent(json, Encoding.UTF8, "application/json"); | ||||
return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); | return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); | ||||
} | } | ||||
} | } | ||||
/// <exception cref="InvalidOperationException">Unsupported param type.</exception> | /// <exception cref="InvalidOperationException">Unsupported param type.</exception> | ||||
public async Task<RestResponse> SendAsync(string method, string endpoint, IReadOnlyDictionary<string, object> multipartParams, CancellationToken cancelToken, bool headerOnly, string reason = null) | |||||
public async Task<RestResponse> SendAsync(string method, string endpoint, IReadOnlyDictionary<string, object> multipartParams, CancellationToken cancelToken, bool headerOnly, string reason = null, | |||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>> requestHeaders = null) | |||||
{ | { | ||||
string uri = Path.Combine(_baseUrl, endpoint); | string uri = Path.Combine(_baseUrl, endpoint); | ||||
using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) | using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) | ||||
{ | { | ||||
if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); | 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)); | var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture)); | ||||
MemoryStream memoryStream = null; | MemoryStream memoryStream = null; | ||||
if (multipartParams != null) | if (multipartParams != null) | ||||
@@ -126,7 +138,7 @@ namespace Discord.Net.Rest | |||||
content.Add(streamContent, p.Key, fileValue.Filename); | content.Add(streamContent, p.Key, fileValue.Filename); | ||||
#pragma warning restore IDISP004 | #pragma warning restore IDISP004 | ||||
continue; | continue; | ||||
} | } | ||||
default: | default: | ||||
@@ -1,5 +1,8 @@ | |||||
using Discord.Net.Rest; | using Discord.Net.Rest; | ||||
using System; | using System; | ||||
using System.Collections; | |||||
using System.Collections.Generic; | |||||
using System.Collections.Immutable; | |||||
using System.IO; | using System.IO; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
@@ -28,7 +31,7 @@ namespace Discord.Net.Queue | |||||
public virtual async Task<RestResponse> SendAsync() | public virtual async Task<RestResponse> 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); | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -450,14 +450,16 @@ namespace Discord.WebSocket | |||||
/// <summary> | /// <summary> | ||||
/// Gets a collection of all global commands. | /// Gets a collection of all global commands. | ||||
/// </summary> | /// </summary> | ||||
/// <param name="withLocalizations">Whether to include full localization dictionaries in the returned objects, instead of the name localized and description localized fields.</param> | |||||
/// <param name="locale">The target locale of the localized name and description fields. Sets <c>X-Discord-Locale</c> header, which takes precedence over <c>Accept-Language</c>.</param> | |||||
/// <param name="options">The options to be used when sending the request.</param> | /// <param name="options">The options to be used when sending the request.</param> | ||||
/// <returns> | /// <returns> | ||||
/// A task that represents the asynchronous get operation. The task result contains a read-only collection of global | /// A task that represents the asynchronous get operation. The task result contains a read-only collection of global | ||||
/// application commands. | /// application commands. | ||||
/// </returns> | /// </returns> | ||||
public async Task<IReadOnlyCollection<SocketApplicationCommand>> GetGlobalApplicationCommandsAsync(RequestOptions options = null) | |||||
public async Task<IReadOnlyCollection<SocketApplicationCommand>> 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) | foreach(var command in commands) | ||||
{ | { | ||||
@@ -3236,8 +3238,8 @@ namespace Discord.WebSocket | |||||
async Task<IApplicationCommand> IDiscordClient.GetGlobalApplicationCommandAsync(ulong id, RequestOptions options) | async Task<IApplicationCommand> IDiscordClient.GetGlobalApplicationCommandAsync(ulong id, RequestOptions options) | ||||
=> await GetGlobalApplicationCommandAsync(id, options); | => await GetGlobalApplicationCommandAsync(id, options); | ||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
async Task<IReadOnlyCollection<IApplicationCommand>> IDiscordClient.GetGlobalApplicationCommandsAsync(RequestOptions options) | |||||
=> await GetGlobalApplicationCommandsAsync(options); | |||||
async Task<IReadOnlyCollection<IApplicationCommand>> IDiscordClient.GetGlobalApplicationCommandsAsync(bool withLocalizations, string locale, RequestOptions options) | |||||
=> await GetGlobalApplicationCommandsAsync(withLocalizations, locale, options); | |||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
async Task IDiscordClient.StartAsync() | async Task IDiscordClient.StartAsync() | ||||
@@ -874,14 +874,17 @@ namespace Discord.WebSocket | |||||
/// <summary> | /// <summary> | ||||
/// Gets a collection of slash commands created by the current user in this guild. | /// Gets a collection of slash commands created by the current user in this guild. | ||||
/// </summary> | /// </summary> | ||||
/// <param name="withLocalizations">Whether to include full localization dictionaries in the returned objects, instead of the name localized and description localized fields.</param> | |||||
/// <param name="locale">The target locale of the localized name and description fields. Sets <c>X-Discord-Locale</c> header, which takes precedence over <c>Accept-Language</c>.</param> | |||||
/// <param name="options">The options to be used when sending the request.</param> | /// <param name="options">The options to be used when sending the request.</param> | ||||
/// <returns> | /// <returns> | ||||
/// A task that represents the asynchronous get operation. The task result contains a read-only collection of | /// A task that represents the asynchronous get operation. The task result contains a read-only collection of | ||||
/// slash commands created by the current user. | /// slash commands created by the current user. | ||||
/// </returns> | /// </returns> | ||||
public async Task<IReadOnlyCollection<SocketApplicationCommand>> GetApplicationCommandsAsync(RequestOptions options = null) | |||||
public async Task<IReadOnlyCollection<SocketApplicationCommand>> 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) | foreach (var command in commands) | ||||
{ | { | ||||
@@ -1977,8 +1980,8 @@ namespace Discord.WebSocket | |||||
async Task<IReadOnlyCollection<IWebhook>> IGuild.GetWebhooksAsync(RequestOptions options) | async Task<IReadOnlyCollection<IWebhook>> IGuild.GetWebhooksAsync(RequestOptions options) | ||||
=> await GetWebhooksAsync(options).ConfigureAwait(false); | => await GetWebhooksAsync(options).ConfigureAwait(false); | ||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
async Task<IReadOnlyCollection<IApplicationCommand>> IGuild.GetApplicationCommandsAsync (RequestOptions options) | |||||
=> await GetApplicationCommandsAsync(options).ConfigureAwait(false); | |||||
async Task<IReadOnlyCollection<IApplicationCommand>> IGuild.GetApplicationCommandsAsync (bool withLocalizations, string locale, RequestOptions options) | |||||
=> await GetApplicationCommandsAsync(withLocalizations, locale, options).ConfigureAwait(false); | |||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
async Task<ICustomSticker> IGuild.CreateStickerAsync(string name, string description, IEnumerable<string> tags, Image image, RequestOptions options) | async Task<ICustomSticker> IGuild.CreateStickerAsync(string name, string description, IEnumerable<string> tags, Image image, RequestOptions options) | ||||
=> await CreateStickerAsync(name, description, tags, image, options); | => await CreateStickerAsync(name, description, tags, image, options); | ||||
@@ -50,6 +50,32 @@ namespace Discord.WebSocket | |||||
/// </remarks> | /// </remarks> | ||||
public IReadOnlyCollection<SocketApplicationCommandOption> Options { get; private set; } | public IReadOnlyCollection<SocketApplicationCommandOption> Options { get; private set; } | ||||
/// <summary> | |||||
/// Gets the localization dictionary for the name field of this command. | |||||
/// </summary> | |||||
public IReadOnlyDictionary<string, string> NameLocalizations { get; private set; } | |||||
/// <summary> | |||||
/// Gets the localization dictionary for the description field of this command. | |||||
/// </summary> | |||||
public IReadOnlyDictionary<string, string> DescriptionLocalizations { get; private set; } | |||||
/// <summary> | |||||
/// Gets the localized name of this command. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// Only returned when the `withLocalizations` query parameter is set to <see langword="false"/> when requesting the command. | |||||
/// </remarks> | |||||
public string NameLocalized { get; private set; } | |||||
/// <summary> | |||||
/// Gets the localized description of this command. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// Only returned when the `withLocalizations` query parameter is set to <see langword="false"/> when requesting the command. | |||||
/// </remarks> | |||||
public string DescriptionLocalized { get; private set; } | |||||
/// <inheritdoc/> | /// <inheritdoc/> | ||||
public DateTimeOffset CreatedAt | public DateTimeOffset CreatedAt | ||||
=> SnowflakeUtils.FromSnowflake(Id); | => SnowflakeUtils.FromSnowflake(Id); | ||||
@@ -93,6 +119,15 @@ namespace Discord.WebSocket | |||||
? model.Options.Value.Select(SocketApplicationCommandOption.Create).ToImmutableArray() | ? model.Options.Value.Select(SocketApplicationCommandOption.Create).ToImmutableArray() | ||||
: ImmutableArray.Create<SocketApplicationCommandOption>(); | : ImmutableArray.Create<SocketApplicationCommandOption>(); | ||||
NameLocalizations = model.NameLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ?? | |||||
ImmutableDictionary<string, string>.Empty; | |||||
DescriptionLocalizations = model.DescriptionLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ?? | |||||
ImmutableDictionary<string, string>.Empty; | |||||
NameLocalized = model.NameLocalized.GetValueOrDefault(); | |||||
DescriptionLocalized = model.DescriptionLocalized.GetValueOrDefault(); | |||||
IsEnabledInDm = model.DmPermission.GetValueOrDefault(true).GetValueOrDefault(true); | IsEnabledInDm = model.DmPermission.GetValueOrDefault(true).GetValueOrDefault(true); | ||||
DefaultMemberPermissions = new GuildPermissions((ulong)model.DefaultMemberPermission.GetValueOrDefault(0).GetValueOrDefault(0)); | DefaultMemberPermissions = new GuildPermissions((ulong)model.DefaultMemberPermission.GetValueOrDefault(0).GetValueOrDefault(0)); | ||||
} | } | ||||
@@ -1,3 +1,5 @@ | |||||
using System.Collections.Generic; | |||||
using System.Collections.Immutable; | |||||
using Model = Discord.API.ApplicationCommandOptionChoice; | using Model = Discord.API.ApplicationCommandOptionChoice; | ||||
namespace Discord.WebSocket | namespace Discord.WebSocket | ||||
@@ -13,6 +15,19 @@ namespace Discord.WebSocket | |||||
/// <inheritdoc/> | /// <inheritdoc/> | ||||
public object Value { get; private set; } | public object Value { get; private set; } | ||||
/// <summary> | |||||
/// Gets the localization dictionary for the name field of this command option choice. | |||||
/// </summary> | |||||
public IReadOnlyDictionary<string, string> NameLocalizations { get; private set; } | |||||
/// <summary> | |||||
/// Gets the localized name of this command option choice. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// Only returned when the `withLocalizations` query parameter is set to <see langword="false"/> when requesting the command. | |||||
/// </remarks> | |||||
public string NameLocalized { get; private set; } | |||||
internal SocketApplicationCommandChoice() { } | internal SocketApplicationCommandChoice() { } | ||||
internal static SocketApplicationCommandChoice Create(Model model) | internal static SocketApplicationCommandChoice Create(Model model) | ||||
{ | { | ||||
@@ -24,6 +39,8 @@ namespace Discord.WebSocket | |||||
{ | { | ||||
Name = model.Name; | Name = model.Name; | ||||
Value = model.Value; | Value = model.Value; | ||||
NameLocalizations = model.NameLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary(); | |||||
NameLocalized = model.NameLocalized.GetValueOrDefault(null); | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -54,6 +54,32 @@ namespace Discord.WebSocket | |||||
/// </summary> | /// </summary> | ||||
public IReadOnlyCollection<ChannelType> ChannelTypes { get; private set; } | public IReadOnlyCollection<ChannelType> ChannelTypes { get; private set; } | ||||
/// <summary> | |||||
/// Gets the localization dictionary for the name field of this command option. | |||||
/// </summary> | |||||
public IReadOnlyDictionary<string, string> NameLocalizations { get; private set; } | |||||
/// <summary> | |||||
/// Gets the localization dictionary for the description field of this command option. | |||||
/// </summary> | |||||
public IReadOnlyDictionary<string, string> DescriptionLocalizations { get; private set; } | |||||
/// <summary> | |||||
/// Gets the localized name of this command option. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// Only returned when the `withLocalizations` query parameter is set to <see langword="false"/> when requesting the command. | |||||
/// </remarks> | |||||
public string NameLocalized { get; private set; } | |||||
/// <summary> | |||||
/// Gets the localized description of this command option. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// Only returned when the `withLocalizations` query parameter is set to <see langword="false"/> when requesting the command. | |||||
/// </remarks> | |||||
public string DescriptionLocalized { get; private set; } | |||||
internal SocketApplicationCommandOption() { } | internal SocketApplicationCommandOption() { } | ||||
internal static SocketApplicationCommandOption Create(Model model) | internal static SocketApplicationCommandOption Create(Model model) | ||||
{ | { | ||||
@@ -92,6 +118,15 @@ namespace Discord.WebSocket | |||||
ChannelTypes = model.ChannelTypes.IsSpecified | ChannelTypes = model.ChannelTypes.IsSpecified | ||||
? model.ChannelTypes.Value.ToImmutableArray() | ? model.ChannelTypes.Value.ToImmutableArray() | ||||
: ImmutableArray.Create<ChannelType>(); | : ImmutableArray.Create<ChannelType>(); | ||||
NameLocalizations = model.NameLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ?? | |||||
ImmutableDictionary<string, string>.Empty; | |||||
DescriptionLocalizations = model.DescriptionLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ?? | |||||
ImmutableDictionary<string, string>.Empty; | |||||
NameLocalized = model.NameLocalized.GetValueOrDefault(); | |||||
DescriptionLocalized = model.DescriptionLocalized.GetValueOrDefault(); | |||||
} | } | ||||
IReadOnlyCollection<IApplicationCommandOptionChoice> IApplicationCommandOption.Choices => Choices; | IReadOnlyCollection<IApplicationCommandOptionChoice> IApplicationCommandOption.Choices => Choices; | ||||