* 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 conflictspull/2395/head
@@ -1194,12 +1194,16 @@ namespace Discord | |||
/// <summary> | |||
/// Gets this guilds application commands. | |||
/// </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="options">The options to be used when sending the request.</param> | |||
/// <returns> | |||
/// A task that represents the asynchronous get operation. The task result contains a read-only collection | |||
/// of application commands found within the guild. | |||
/// </returns> | |||
Task<IReadOnlyCollection<IApplicationCommand>> GetApplicationCommandsAsync(RequestOptions options = null); | |||
Task<IReadOnlyCollection<IApplicationCommand>> GetApplicationCommandsAsync(bool withLocalizations = false, RequestOptions options = null); | |||
/// <summary> | |||
/// Gets an application command within this guild with the specified id. | |||
@@ -1,4 +1,5 @@ | |||
using System; | |||
using System.Collections; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text.RegularExpressions; | |||
@@ -12,6 +13,8 @@ namespace Discord | |||
{ | |||
private string _name; | |||
private string _description; | |||
private IDictionary<string, string> _nameLocalizations = new Dictionary<string, string>(); | |||
private IDictionary<string, string> _descriptionLocalizations = new Dictionary<string, string>(); | |||
/// <summary> | |||
/// Gets or sets the name of this option. | |||
@@ -21,18 +24,7 @@ namespace Discord | |||
get => _name; | |||
set | |||
{ | |||
if (value == null) | |||
throw new ArgumentNullException(nameof(value), $"{nameof(Name)} cannot be null."); | |||
if (value.Length > 32) | |||
throw new ArgumentOutOfRangeException(nameof(value), "Name length must be less than or equal to 32."); | |||
if (!Regex.IsMatch(value, @"^[\w-]{1,32}$")) | |||
throw new FormatException($"{nameof(value)} must match the regex ^[\\w-]{{1,32}}$"); | |||
if (value.Any(x => char.IsUpper(x))) | |||
throw new FormatException("Name cannot contain any uppercase characters."); | |||
EnsureValidOptionName(value); | |||
_name = value; | |||
} | |||
} | |||
@@ -43,12 +35,11 @@ namespace Discord | |||
public string Description | |||
{ | |||
get => _description; | |||
set => _description = value?.Length switch | |||
set | |||
{ | |||
> 100 => throw new ArgumentOutOfRangeException(nameof(value), "Description length must be less than or equal to 100."), | |||
0 => throw new ArgumentOutOfRangeException(nameof(value), "Description length must be at least 1."), | |||
_ => value | |||
}; | |||
EnsureValidOptionDescription(value); | |||
_description = value; | |||
} | |||
} | |||
/// <summary> | |||
@@ -95,5 +86,72 @@ namespace Discord | |||
/// Gets or sets the allowed channel types for this option. | |||
/// </summary> | |||
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.Collections; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text.RegularExpressions; | |||
namespace Discord | |||
{ | |||
@@ -9,6 +13,7 @@ namespace Discord | |||
{ | |||
private string _name; | |||
private object _value; | |||
private IDictionary<string, string> _nameLocalizations = new Dictionary<string, string>(); | |||
/// <summary> | |||
/// Gets or sets the name of this choice. | |||
@@ -40,5 +45,33 @@ namespace Discord | |||
_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 | |||
{ | |||
/// <summary> | |||
@@ -5,6 +12,9 @@ namespace Discord | |||
/// </summary> | |||
public abstract class ApplicationCommandProperties | |||
{ | |||
private IReadOnlyDictionary<string, string> _nameLocalizations; | |||
private IReadOnlyDictionary<string, string> _descriptionLocalizations; | |||
internal abstract ApplicationCommandType Type { get; } | |||
/// <summary> | |||
@@ -17,6 +27,48 @@ namespace Discord | |||
/// </summary> | |||
public Optional<bool> IsDefaultPermission { get; set; } | |||
/// <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. | |||
/// </summary> | |||
@@ -1,3 +1,8 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text.RegularExpressions; | |||
namespace Discord | |||
{ | |||
/// <summary> | |||
@@ -31,6 +36,11 @@ namespace Discord | |||
/// </summary> | |||
public bool IsDefaultPermission { get; set; } = true; | |||
/// <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. | |||
/// </summary> | |||
@@ -42,6 +52,7 @@ namespace Discord | |||
public GuildPermission? DefaultMemberPermissions { get; set; } | |||
private string _name; | |||
private Dictionary<string, string> _nameLocalizations; | |||
/// <summary> | |||
/// Build the current builder into a <see cref="MessageCommandProperties"/> class. | |||
@@ -86,6 +97,30 @@ namespace Discord | |||
return this; | |||
} | |||
/// <summary> | |||
/// Sets the <see cref="NameLocalizations"/> collection. | |||
/// </summary> | |||
/// <param name="nameLocalizations">Localization dictionary 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 | |||
/// </summary> | |||
@@ -97,6 +132,41 @@ namespace Discord | |||
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 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. | |||
/// </summary> | |||
@@ -1,3 +1,8 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text.RegularExpressions; | |||
namespace Discord | |||
{ | |||
/// <summary> | |||
@@ -5,7 +10,7 @@ namespace Discord | |||
/// </summary> | |||
public class UserCommandBuilder | |||
{ | |||
/// <summary> | |||
/// <summary> | |||
/// Returns the maximum length a commands name allowed by Discord. | |||
/// </summary> | |||
public const int MaxNameLength = 32; | |||
@@ -31,6 +36,11 @@ namespace Discord | |||
/// </summary> | |||
public bool IsDefaultPermission { get; set; } = true; | |||
/// <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. | |||
/// </summary> | |||
@@ -42,6 +52,7 @@ namespace Discord | |||
public GuildPermission? DefaultMemberPermissions { get; set; } | |||
private string _name; | |||
private Dictionary<string, string> _nameLocalizations; | |||
/// <summary> | |||
/// Build the current builder into a <see cref="UserCommandProperties"/> class. | |||
@@ -84,6 +95,30 @@ namespace Discord | |||
return this; | |||
} | |||
/// <summary> | |||
/// Sets the <see cref="NameLocalizations"/> collection. | |||
/// </summary> | |||
/// <param name="nameLocalizations">Localization dictionary 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 | |||
/// </summary> | |||
@@ -95,6 +130,41 @@ namespace Discord | |||
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 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. | |||
/// </summary> | |||
@@ -52,6 +52,32 @@ namespace Discord | |||
/// </summary> | |||
IReadOnlyCollection<IApplicationCommandOption> Options { get; } | |||
/// <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. | |||
/// </summary> | |||
@@ -61,5 +61,31 @@ namespace Discord | |||
/// Gets the allowed channel types for this option. | |||
/// </summary> | |||
IReadOnlyCollection<ChannelType> ChannelTypes { get; } | |||
/// <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 true when requesting the command. | |||
/// </remarks> | |||
string DescriptionLocalized { get; } | |||
} | |||
} |
@@ -1,3 +1,5 @@ | |||
using System.Collections.Generic; | |||
namespace Discord | |||
{ | |||
/// <summary> | |||
@@ -14,5 +16,18 @@ namespace Discord | |||
/// Gets the value of the choice. | |||
/// </summary> | |||
object Value { get; } | |||
/// <summary> | |||
/// Gets the localization dictionary for the name field of this command. | |||
/// </summary> | |||
IReadOnlyDictionary<string, string> NameLocalizations { 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; } | |||
} | |||
} |
@@ -1,6 +1,9 @@ | |||
using System; | |||
using System.Collections; | |||
using System.Collections.Generic; | |||
using System.Collections.Immutable; | |||
using System.Linq; | |||
using System.Net.Sockets; | |||
using System.Text.RegularExpressions; | |||
namespace Discord | |||
@@ -31,18 +34,7 @@ namespace Discord | |||
get => _name; | |||
set | |||
{ | |||
Preconditions.NotNullOrEmpty(value, nameof(value)); | |||
Preconditions.AtLeast(value.Length, 1, nameof(value)); | |||
Preconditions.AtMost(value.Length, MaxNameLength, nameof(value)); | |||
// Discord updated the docs, this regex prevents special characters like @!$%(... etc, | |||
// https://discord.com/developers/docs/interactions/slash-commands#applicationcommand | |||
if (!Regex.IsMatch(value, @"^[\w-]{1,32}$")) | |||
throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(value)); | |||
if (value.Any(x => char.IsUpper(x))) | |||
throw new FormatException("Name cannot contain any uppercase characters."); | |||
EnsureValidCommandName(value); | |||
_name = value; | |||
} | |||
} | |||
@@ -55,10 +47,7 @@ namespace Discord | |||
get => _description; | |||
set | |||
{ | |||
Preconditions.NotNullOrEmpty(value, nameof(Description)); | |||
Preconditions.AtLeast(value.Length, 1, nameof(Description)); | |||
Preconditions.AtMost(value.Length, MaxDescriptionLength, nameof(Description)); | |||
EnsureValidCommandDescription(value); | |||
_description = value; | |||
} | |||
} | |||
@@ -76,6 +65,16 @@ namespace Discord | |||
} | |||
} | |||
/// <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 | |||
/// </summary> | |||
@@ -93,6 +92,8 @@ namespace Discord | |||
private string _name; | |||
private string _description; | |||
private Dictionary<string, string> _nameLocalizations; | |||
private Dictionary<string, string> _descriptionLocalizations; | |||
private List<SlashCommandOptionBuilder> _options; | |||
/// <summary> | |||
@@ -106,6 +107,8 @@ namespace Discord | |||
Name = Name, | |||
Description = Description, | |||
IsDefaultPermission = IsDefaultPermission, | |||
NameLocalizations = _nameLocalizations, | |||
DescriptionLocalizations = _descriptionLocalizations, | |||
IsDMEnabled = IsDMEnabled, | |||
DefaultMemberPermissions = DefaultMemberPermissions ?? Optional<GuildPermission>.Unspecified | |||
}; | |||
@@ -190,13 +193,16 @@ namespace Discord | |||
/// <param name="isAutocomplete">If this option is set to autocomplete.</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="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="minValue">The smallest number value the user can input.</param> | |||
/// <param name="maxValue">The largest number value the user can input.</param> | |||
/// <returns>The current builder.</returns> | |||
public SlashCommandBuilder AddOption(string name, ApplicationCommandOptionType type, | |||
string description, bool? isRequired = null, bool? isDefault = null, bool isAutocomplete = false, double? minValue = null, double? maxValue = 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, params ApplicationCommandOptionChoiceProperties[] choices) | |||
{ | |||
Preconditions.Options(name, description); | |||
@@ -221,9 +227,15 @@ namespace Discord | |||
Choices = (choices ?? Array.Empty<ApplicationCommandOptionChoiceProperties>()).ToList(), | |||
ChannelTypes = channelTypes, | |||
MinValue = minValue, | |||
MaxValue = maxValue, | |||
MaxValue = maxValue | |||
}; | |||
if (nameLocalizations is not null) | |||
option.WithNameLocalizations(nameLocalizations); | |||
if (descriptionLocalizations is not null) | |||
option.WithDescriptionLocalizations(descriptionLocalizations); | |||
return AddOption(option); | |||
} | |||
@@ -266,6 +278,116 @@ namespace Discord | |||
Options.AddRange(options); | |||
return this; | |||
} | |||
/// <summary> | |||
/// Sets the <see cref="NameLocalizations"/> collection. | |||
/// </summary> | |||
/// <param name="nameLocalizations">Localization dictionary 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">Localization dictionary for the name 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> | |||
@@ -285,6 +407,8 @@ namespace Discord | |||
private string _name; | |||
private string _description; | |||
private Dictionary<string, string> _nameLocalizations; | |||
private Dictionary<string, string> _descriptionLocalizations; | |||
/// <summary> | |||
/// Gets or sets the name of this option. | |||
@@ -296,10 +420,7 @@ namespace Discord | |||
{ | |||
if (value != null) | |||
{ | |||
Preconditions.AtLeast(value.Length, 1, nameof(value)); | |||
Preconditions.AtMost(value.Length, SlashCommandBuilder.MaxNameLength, nameof(value)); | |||
if (!Regex.IsMatch(value, @"^[\w-]{1,32}$")) | |||
throw new ArgumentException("Option name cannot contain any special characters or whitespaces!", nameof(value)); | |||
EnsureValidCommandOptionName(value); | |||
} | |||
_name = value; | |||
@@ -316,8 +437,7 @@ namespace Discord | |||
{ | |||
if (value != null) | |||
{ | |||
Preconditions.AtLeast(value.Length, 1, nameof(value)); | |||
Preconditions.AtMost(value.Length, SlashCommandBuilder.MaxDescriptionLength, nameof(value)); | |||
EnsureValidCommandOptionDescription(value); | |||
} | |||
_description = value; | |||
@@ -369,6 +489,16 @@ namespace Discord | |||
/// </summary> | |||
public List<ChannelType> ChannelTypes { get; set; } | |||
/// <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. | |||
/// </summary> | |||
@@ -404,7 +534,9 @@ namespace Discord | |||
IsAutocomplete = IsAutocomplete, | |||
ChannelTypes = ChannelTypes, | |||
MinValue = MinValue, | |||
MaxValue = MaxValue | |||
MaxValue = MaxValue, | |||
NameLocalizations = _nameLocalizations, | |||
DescriptionLocalizations = _descriptionLocalizations | |||
}; | |||
} | |||
@@ -419,13 +551,16 @@ namespace Discord | |||
/// <param name="isAutocomplete">If this option supports autocomplete.</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="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="minValue">The smallest number value the user can input.</param> | |||
/// <param name="maxValue">The largest number value the user can input.</param> | |||
/// <returns>The current builder.</returns> | |||
public SlashCommandOptionBuilder AddOption(string name, ApplicationCommandOptionType type, | |||
string description, bool? isRequired = null, bool isDefault = false, bool isAutocomplete = false, double? minValue = null, double? maxValue = 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, params ApplicationCommandOptionChoiceProperties[] choices) | |||
{ | |||
Preconditions.Options(name, description); | |||
@@ -450,9 +585,15 @@ namespace Discord | |||
Options = options, | |||
Type = type, | |||
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); | |||
} | |||
/// <summary> | |||
@@ -499,10 +640,11 @@ namespace Discord | |||
/// </summary> | |||
/// <param name="name">The name 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> | |||
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> | |||
@@ -510,10 +652,11 @@ namespace Discord | |||
/// </summary> | |||
/// <param name="name">The name 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> | |||
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> | |||
@@ -521,10 +664,11 @@ namespace Discord | |||
/// </summary> | |||
/// <param name="name">The name 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> | |||
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> | |||
@@ -532,10 +676,11 @@ namespace Discord | |||
/// </summary> | |||
/// <param name="name">The name 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> | |||
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> | |||
@@ -543,13 +688,14 @@ namespace Discord | |||
/// </summary> | |||
/// <param name="name">The name 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> | |||
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>(); | |||
@@ -571,7 +717,8 @@ namespace Discord | |||
Choices.Add(new ApplicationCommandOptionChoiceProperties | |||
{ | |||
Name = name, | |||
Value = value | |||
Value = value, | |||
NameLocalizations = nameLocalizations | |||
}); | |||
return this; | |||
@@ -679,5 +826,107 @@ namespace Discord | |||
Type = type; | |||
return this; | |||
} | |||
/// <summary> | |||
/// Sets the <see cref="NameLocalizations"/> collection. | |||
/// </summary> | |||
/// <param name="nameLocalizations">Localization dictionary 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 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">Localization dictionary for the description field of this command.</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,13 @@ namespace Discord | |||
/// <summary> | |||
/// Gets a collection of all global commands. | |||
/// </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="options">The options to be used when sending the request.</param> | |||
/// <returns> | |||
/// A task that represents the asynchronous get operation. The task result contains a read-only collection of global | |||
/// application commands. | |||
/// </returns> | |||
Task<IReadOnlyCollection<IApplicationCommand>> GetGlobalApplicationCommandsAsync(RequestOptions options = null); | |||
Task<IReadOnlyCollection<IApplicationCommand>> GetGlobalApplicationCommandsAsync(bool withLocalizations = false, RequestOptions options = null); | |||
/// <summary> | |||
/// Creates a global application command. | |||
@@ -55,7 +55,7 @@ namespace Discord | |||
if (obj.Value == null) throw CreateNotNullException(name, msg); | |||
if (obj.Value.Trim().Length == 0) throw CreateNotEmptyException(name, msg); | |||
} | |||
} | |||
} | |||
private static ArgumentException CreateNotEmptyException(string name, string msg) | |||
=> new ArgumentException(message: msg ?? "Argument cannot be blank.", paramName: name); | |||
@@ -129,7 +129,7 @@ namespace Discord | |||
private static ArgumentException CreateNotEqualException<T>(string name, string msg, T value) | |||
=> 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> | |||
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> | |||
@@ -165,7 +165,7 @@ namespace Discord | |||
private static ArgumentException CreateAtLeastException<T>(string name, string msg, T value) | |||
=> new ArgumentException(message: msg ?? $"Value must be at least {value}.", paramName: name); | |||
/// <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); } | |||
/// <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) | |||
=> new ArgumentException(message: msg ?? $"Value must be greater than {value}.", paramName: name); | |||
/// <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); } | |||
/// <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) | |||
=> new ArgumentException(message: msg ?? $"Value must be at most {value}.", paramName: name); | |||
/// <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); } | |||
/// <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); } } | |||
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 CommandMap<SlashCommandInfo> _slashCommandMap; | |||
private readonly ConcurrentDictionary<ApplicationCommandType, CommandMap<ContextCommandInfo>> _contextCommandMaps; | |||
@@ -203,6 +208,7 @@ namespace Discord.Interactions | |||
_enableAutocompleteHandlers = config.EnableAutocompleteHandlers; | |||
_autoServiceScopes = config.AutoServiceScopes; | |||
_restResponseCallback = config.RestResponseCallback; | |||
LocalizationManager = config.LocalizationManager; | |||
_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. | |||
/// </summary> | |||
public bool ExitOnMissingModalField { get; set; } = false; | |||
/// <summary> | |||
/// Localization provider to be used when registering application commands. | |||
/// </summary> | |||
public ILocalizationManager LocalizationManager { get; set; } | |||
} | |||
/// <summary> | |||
@@ -0,0 +1,32 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Threading.Tasks; | |||
namespace Discord.Interactions | |||
{ | |||
/// <summary> | |||
/// Respresents 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,70 @@ | |||
using Newtonsoft.Json; | |||
using Newtonsoft.Json.Linq; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.IO; | |||
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 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) + $".{identifier}"; | |||
var value = (string)obj.SelectToken(token); | |||
if (value is not null) | |||
result[locale] = value; | |||
} | |||
return result; | |||
} | |||
} | |||
} |
@@ -0,0 +1,71 @@ | |||
using System; | |||
using System.Collections.Concurrent; | |||
using System.Collections.Generic; | |||
using System.Collections.Immutable; | |||
using System.Diagnostics; | |||
using System.Globalization; | |||
using System.Linq; | |||
using System.Reflection; | |||
using System.Resources; | |||
using System.Threading.Tasks; | |||
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 const string SpaceToken = "~"; | |||
private readonly string _baseResource; | |||
private readonly Assembly _assembly; | |||
private static readonly ConcurrentDictionary<string, ResourceManager> _localizerCache = new(); | |||
private readonly IEnumerable<CultureInfo> _supportedLocales; | |||
private readonly IEnumerable<string> _resourceNames; | |||
/// <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) | |||
{ | |||
_baseResource = baseResource; | |||
_assembly = assembly; | |||
_supportedLocales = supportedLocales; | |||
_resourceNames = assembly.GetManifestResourceNames(); | |||
} | |||
/// <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 resourceName = (_baseResource + "." + string.Join(".", key)).Replace(" ", SpaceToken); | |||
if (!_resourceNames.Any(x => string.Equals(resourceName + ".resources", x, StringComparison.OrdinalIgnoreCase))) | |||
return ImmutableDictionary<string, string>.Empty; | |||
var result = new Dictionary<string, string>(); | |||
var resourceManager = _localizerCache.GetOrAdd(resourceName, new ResourceManager(resourceName, _assembly)); | |||
foreach (var locale in _supportedLocales) | |||
{ | |||
var value = resourceManager.GetString(identifier, locale); | |||
if (value is not null) | |||
result[locale.Name] = value; | |||
} | |||
return result; | |||
} | |||
} | |||
} |
@@ -0,0 +1,13 @@ | |||
namespace Discord.Interactions | |||
{ | |||
/// <summary> | |||
/// Resource targets for localization. | |||
/// </summary> | |||
public enum LocalizationTarget | |||
{ | |||
Group, | |||
Command, | |||
Parameter, | |||
Choice | |||
} | |||
} |
@@ -42,7 +42,7 @@ namespace Discord.Interactions | |||
public void RemoveCommand(T command) | |||
{ | |||
var key = ParseCommandName(command); | |||
var key = CommandHierarchy.GetCommandPath(command); | |||
_root.RemoveCommand(key, 0); | |||
} | |||
@@ -60,28 +60,9 @@ namespace Discord.Interactions | |||
private void AddCommand(T command) | |||
{ | |||
var key = ParseCommandName(command); | |||
var key = CommandHierarchy.GetCommandPath(command); | |||
_root.AddCommand(key, 0, command); | |||
} | |||
private IList<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.Collections.Generic; | |||
using System.Collections.Immutable; | |||
using System.Linq; | |||
namespace Discord.Interactions | |||
@@ -9,6 +10,9 @@ namespace Discord.Interactions | |||
#region Parameters | |||
public static ApplicationCommandOptionProperties ToApplicationCommandOptionProps(this SlashCommandParameterInfo parameterInfo) | |||
{ | |||
var localizationManager = parameterInfo.Command.Module.CommandService.LocalizationManager; | |||
var parameterPath = parameterInfo.GetParameterPath(); | |||
var props = new ApplicationCommandOptionProperties | |||
{ | |||
Name = parameterInfo.Name, | |||
@@ -18,12 +22,15 @@ namespace Discord.Interactions | |||
Choices = parameterInfo.Choices?.Select(x => new ApplicationCommandOptionChoiceProperties | |||
{ | |||
Name = x.Name, | |||
Value = x.Value | |||
Value = x.Value, | |||
NameLocalizations = localizationManager?.GetAllNames(parameterInfo.GetChoicePath(x), LocalizationTarget.Choice) ?? ImmutableDictionary<string, string>.Empty | |||
})?.ToList(), | |||
ChannelTypes = parameterInfo.ChannelTypes?.ToList(), | |||
IsAutocomplete = parameterInfo.IsAutocomplete, | |||
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 | |||
}; | |||
parameterInfo.TypeConverter.Write(props, parameterInfo); | |||
@@ -36,13 +43,19 @@ namespace Discord.Interactions | |||
public static SlashCommandProperties ToApplicationCommandProps(this SlashCommandInfo commandInfo) | |||
{ | |||
var commandPath = commandInfo.GetCommandPath(); | |||
var localizationManager = commandInfo.Module.CommandService.LocalizationManager; | |||
var props = new SlashCommandBuilder() | |||
{ | |||
Name = commandInfo.Name, | |||
Description = commandInfo.Description, | |||
IsDefaultPermission = commandInfo.DefaultPermission, | |||
IsDMEnabled = commandInfo.IsEnabledInDm, | |||
DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), | |||
}.Build(); | |||
}.WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary<string, string>.Empty) | |||
.WithDescriptionLocalizations(localizationManager?.GetAllDescriptions(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary<string, string>.Empty) | |||
.Build(); | |||
if (commandInfo.Parameters.Count > SlashCommandBuilder.MaxOptionsCount) | |||
throw new InvalidOperationException($"Slash Commands cannot have more than {SlashCommandBuilder.MaxOptionsCount} command parameters"); | |||
@@ -52,18 +65,30 @@ namespace Discord.Interactions | |||
return props; | |||
} | |||
public static ApplicationCommandOptionProperties ToApplicationCommandOptionProps(this SlashCommandInfo commandInfo) => | |||
new ApplicationCommandOptionProperties | |||
public static ApplicationCommandOptionProperties ToApplicationCommandOptionProps(this SlashCommandInfo commandInfo) | |||
{ | |||
var localizationManager = commandInfo.Module.CommandService.LocalizationManager; | |||
var commandPath = commandInfo.GetCommandPath(); | |||
return new ApplicationCommandOptionProperties | |||
{ | |||
Name = commandInfo.Name, | |||
Description = commandInfo.Description, | |||
Type = ApplicationCommandOptionType.SubCommand, | |||
IsRequired = false, | |||
Options = commandInfo.FlattenedParameters?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() | |||
Options = commandInfo.FlattenedParameters?.Select(x => x.ToApplicationCommandOptionProps()) | |||
?.ToList(), | |||
NameLocalizations = localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary<string, string>.Empty, | |||
DescriptionLocalizations = localizationManager?.GetAllDescriptions(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary<string, string>.Empty | |||
}; | |||
} | |||
public static ApplicationCommandProperties ToApplicationCommandProps(this ContextCommandInfo commandInfo) | |||
=> commandInfo.CommandType switch | |||
{ | |||
var localizationManager = commandInfo.Module.CommandService.LocalizationManager; | |||
var commandPath = commandInfo.GetCommandPath(); | |||
return commandInfo.CommandType switch | |||
{ | |||
ApplicationCommandType.Message => new MessageCommandBuilder | |||
{ | |||
@@ -71,16 +96,21 @@ namespace Discord.Interactions | |||
IsDefaultPermission = commandInfo.DefaultPermission, | |||
DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), | |||
IsDMEnabled = commandInfo.IsEnabledInDm | |||
}.Build(), | |||
} | |||
.WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary<string, string>.Empty) | |||
.Build(), | |||
ApplicationCommandType.User => new UserCommandBuilder | |||
{ | |||
Name = commandInfo.Name, | |||
IsDefaultPermission = commandInfo.DefaultPermission, | |||
DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), | |||
IsDMEnabled = commandInfo.IsEnabledInDm | |||
}.Build(), | |||
} | |||
.WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary<string, string>.Empty) | |||
.Build(), | |||
_ => throw new InvalidOperationException($"{commandInfo.CommandType} isn't a supported command type.") | |||
}; | |||
} | |||
#endregion | |||
#region Modules | |||
@@ -121,6 +151,9 @@ namespace Discord.Interactions | |||
options.AddRange(moduleInfo.SubModules?.SelectMany(x => x.ParseSubModule(args, ignoreDontRegister))); | |||
var localizationManager = moduleInfo.CommandService.LocalizationManager; | |||
var modulePath = moduleInfo.GetModulePath(); | |||
var props = new SlashCommandBuilder | |||
{ | |||
Name = moduleInfo.SlashGroupName, | |||
@@ -128,7 +161,10 @@ namespace Discord.Interactions | |||
IsDefaultPermission = moduleInfo.DefaultPermission, | |||
IsDMEnabled = moduleInfo.IsEnabledInDm, | |||
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) | |||
throw new InvalidOperationException($"Slash Commands cannot have more than {SlashCommandBuilder.MaxOptionsCount} command parameters"); | |||
@@ -166,7 +202,11 @@ namespace Discord.Interactions | |||
Name = moduleInfo.SlashGroupName, | |||
Description = moduleInfo.Description, | |||
Type = ApplicationCommandOptionType.SubCommandGroup, | |||
Options = options | |||
Options = options, | |||
NameLocalizations = moduleInfo.CommandService.LocalizationManager?.GetAllNames(moduleInfo.GetModulePath(), LocalizationTarget.Group) | |||
?? ImmutableDictionary<string, string>.Empty, | |||
DescriptionLocalizations = moduleInfo.CommandService.LocalizationManager?.GetAllDescriptions(moduleInfo.GetModulePath(), LocalizationTarget.Group) | |||
?? ImmutableDictionary<string, string>.Empty, | |||
} }; | |||
} | |||
@@ -181,17 +221,23 @@ namespace Discord.Interactions | |||
Name = command.Name, | |||
Description = command.Description, | |||
IsDefaultPermission = command.IsDefaultPermission, | |||
Options = command.Options?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional<List<ApplicationCommandOptionProperties>>.Unspecified | |||
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 | |||
{ | |||
Name = command.Name, | |||
IsDefaultPermission = command.IsDefaultPermission | |||
IsDefaultPermission = command.IsDefaultPermission, | |||
NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty, | |||
DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty | |||
}, | |||
ApplicationCommandType.Message => new MessageCommandProperties | |||
{ | |||
Name = command.Name, | |||
IsDefaultPermission = command.IsDefaultPermission | |||
IsDefaultPermission = command.IsDefaultPermission, | |||
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}"), | |||
}; | |||
@@ -209,7 +255,9 @@ namespace Discord.Interactions | |||
Name = x.Name, | |||
Value = x.Value | |||
}).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() | |||
}; | |||
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 System.Collections.Generic; | |||
namespace Discord.API | |||
{ | |||
@@ -25,6 +26,18 @@ namespace Discord.API | |||
[JsonProperty("default_permission")] | |||
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 | |||
[JsonProperty("dm_permission")] | |||
public Optional<bool?> DmPermission { get; set; } | |||
@@ -1,4 +1,5 @@ | |||
using Newtonsoft.Json; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
namespace Discord.API | |||
@@ -38,6 +39,18 @@ namespace Discord.API | |||
[JsonProperty("channel_types")] | |||
public Optional<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; } | |||
public ApplicationCommandOption() { } | |||
public ApplicationCommandOption(IApplicationCommandOption cmd) | |||
@@ -61,6 +74,11 @@ namespace Discord.API | |||
Name = cmd.Name; | |||
Type = cmd.Type; | |||
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) | |||
{ | |||
@@ -84,6 +102,9 @@ namespace Discord.API | |||
Type = option.Type; | |||
Description = option.Description; | |||
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 System.Collections.Generic; | |||
namespace Discord.API | |||
{ | |||
@@ -9,5 +10,11 @@ namespace Discord.API | |||
[JsonProperty("value")] | |||
public object Value { get; set; } | |||
[JsonProperty("name_localizations")] | |||
public Optional<Dictionary<string, string>> NameLocalizations { get; set; } | |||
[JsonProperty("name_localized")] | |||
public Optional<string> NameLocalized { get; set; } | |||
} | |||
} |
@@ -1,4 +1,8 @@ | |||
using Newtonsoft.Json; | |||
using System.Collections; | |||
using System.Collections.Generic; | |||
using System.Collections.Immutable; | |||
using System.Linq; | |||
namespace Discord.API.Rest | |||
{ | |||
@@ -19,6 +23,12 @@ namespace Discord.API.Rest | |||
[JsonProperty("default_permission")] | |||
public Optional<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")] | |||
public Optional<bool?> DmPermission { get; set; } | |||
@@ -26,12 +36,15 @@ namespace Discord.API.Rest | |||
public Optional<GuildPermission?> DefaultMemberPermission { get; set; } | |||
public CreateApplicationCommandParams() { } | |||
public CreateApplicationCommandParams(string name, string description, ApplicationCommandType type, ApplicationCommandOption[] options = null) | |||
public CreateApplicationCommandParams(string name, string description, ApplicationCommandType type, ApplicationCommandOption[] options = null, | |||
IDictionary<string, string> nameLocalizations = null, IDictionary<string, string> descriptionLocalizations = null) | |||
{ | |||
Name = name; | |||
Description = description; | |||
Options = Optional.Create(options); | |||
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 System.Collections.Generic; | |||
namespace Discord.API.Rest | |||
{ | |||
@@ -15,5 +16,11 @@ namespace Discord.API.Rest | |||
[JsonProperty("default_permission")] | |||
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); | |||
/// <inheritdoc /> | |||
Task<IReadOnlyCollection<IApplicationCommand>> IDiscordClient.GetGlobalApplicationCommandsAsync(RequestOptions options) | |||
Task<IReadOnlyCollection<IApplicationCommand>> IDiscordClient.GetGlobalApplicationCommandsAsync(bool withLocalizations, RequestOptions options) | |||
=> Task.FromResult<IReadOnlyCollection<IApplicationCommand>>(ImmutableArray.Create<IApplicationCommand>()); | |||
Task<IApplicationCommand> IDiscordClient.CreateGlobalApplicationCommand(ApplicationCommandProperties properties, RequestOptions options) | |||
=> Task.FromResult<IApplicationCommand>(null); | |||
@@ -257,6 +257,6 @@ namespace Discord.Rest | |||
/// <inheritdoc /> | |||
Task IDiscordClient.StopAsync() | |||
=> Task.Delay(0); | |||
#endregion | |||
#endregion | |||
} | |||
} |
@@ -194,10 +194,10 @@ namespace Discord.Rest | |||
}; | |||
} | |||
public static async Task<IReadOnlyCollection<RestGlobalCommand>> GetGlobalApplicationCommandsAsync(BaseDiscordClient client, | |||
public static async Task<IReadOnlyCollection<RestGlobalCommand>> GetGlobalApplicationCommandsAsync(BaseDiscordClient client, bool withLocalizations = false, | |||
RequestOptions options = null) | |||
{ | |||
var response = await client.ApiClient.GetGlobalApplicationCommandsAsync(options).ConfigureAwait(false); | |||
var response = await client.ApiClient.GetGlobalApplicationCommandsAsync(withLocalizations, options).ConfigureAwait(false); | |||
if (!response.Any()) | |||
return Array.Empty<RestGlobalCommand>(); | |||
@@ -212,10 +212,10 @@ namespace Discord.Rest | |||
return model != null ? RestGlobalCommand.Create(client, model) : null; | |||
} | |||
public static async Task<IReadOnlyCollection<RestGuildCommand>> GetGuildApplicationCommandsAsync(BaseDiscordClient client, ulong guildId, | |||
public static async Task<IReadOnlyCollection<RestGuildCommand>> GetGuildApplicationCommandsAsync(BaseDiscordClient client, ulong guildId, bool withLocalizations = false, | |||
RequestOptions options = null) | |||
{ | |||
var response = await client.ApiClient.GetGuildApplicationCommandsAsync(guildId, options).ConfigureAwait(false); | |||
var response = await client.ApiClient.GetGuildApplicationCommandsAsync(guildId, withLocalizations, options).ConfigureAwait(false); | |||
if (!response.Any()) | |||
return ImmutableArray.Create<RestGuildCommand>(); | |||
@@ -8,6 +8,7 @@ using Newtonsoft.Json; | |||
using System; | |||
using System.Collections.Concurrent; | |||
using System.Collections.Generic; | |||
using System.ComponentModel.Design; | |||
using System.Diagnostics; | |||
using System.Globalization; | |||
using System.IO; | |||
@@ -1212,11 +1213,13 @@ namespace Discord.API | |||
#endregion | |||
#region Interactions | |||
public async Task<ApplicationCommand[]> GetGlobalApplicationCommandsAsync(RequestOptions options = null) | |||
public async Task<ApplicationCommand[]> GetGlobalApplicationCommandsAsync(bool withLocalizations = false, RequestOptions options = null) | |||
{ | |||
options = RequestOptions.CreateOrClone(options); | |||
return await SendAsync<ApplicationCommand[]>("GET", () => $"applications/{CurrentApplicationId}/commands", new BucketIds(), options: options).ConfigureAwait(false); | |||
//with_localizations=false doesnt return localized names and descriptions | |||
return await SendAsync<ApplicationCommand[]>("GET", () => $"applications/{CurrentApplicationId}/commands{(withLocalizations ? "?with_localizations=true" : string.Empty)}", | |||
new BucketIds(), options: options).ConfigureAwait(false); | |||
} | |||
public async Task<ApplicationCommand> GetGlobalApplicationCommandAsync(ulong id, RequestOptions options = null) | |||
@@ -1281,13 +1284,15 @@ namespace Discord.API | |||
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, RequestOptions options = null) | |||
{ | |||
options = RequestOptions.CreateOrClone(options); | |||
var bucket = new BucketIds(guildId: guildId); | |||
return await SendAsync<ApplicationCommand[]>("GET", () => $"applications/{CurrentApplicationId}/guilds/{guildId}/commands", bucket, options: options).ConfigureAwait(false); | |||
//with_localizations=false doesnt return localized names and descriptions | |||
return await SendAsync<ApplicationCommand[]>("GET", () => $"applications/{CurrentApplicationId}/commands{(withLocalizations ? "?with_localizations=true" : string.Empty)}", | |||
bucket, options: options).ConfigureAwait(false); | |||
} | |||
public async Task<ApplicationCommand> GetGuildApplicationCommandAsync(ulong guildId, ulong commandId, RequestOptions options = null) | |||
@@ -25,7 +25,7 @@ namespace Discord.Rest | |||
/// Gets the logged-in user. | |||
/// </summary> | |||
public new RestSelfUser CurrentUser { get => base.CurrentUser as RestSelfUser; internal set => base.CurrentUser = value; } | |||
/// <inheritdoc /> | |||
public DiscordRestClient() : this(new DiscordRestConfig()) { } | |||
/// <summary> | |||
@@ -205,10 +205,10 @@ namespace Discord.Rest | |||
=> ClientHelper.CreateGlobalApplicationCommandAsync(this, properties, options); | |||
public Task<RestGuildCommand> CreateGuildCommand(ApplicationCommandProperties properties, ulong guildId, RequestOptions options = null) | |||
=> 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, RequestOptions options = null) | |||
=> ClientHelper.GetGlobalApplicationCommandsAsync(this, withLocalizations, options); | |||
public Task<IReadOnlyCollection<RestGuildCommand>> GetGuildApplicationCommands(ulong guildId, bool withLocalizations = false, RequestOptions options = null) | |||
=> ClientHelper.GetGuildApplicationCommandsAsync(this, guildId, withLocalizations, options); | |||
public Task<IReadOnlyCollection<RestGlobalCommand>> BulkOverwriteGlobalCommands(ApplicationCommandProperties[] commandProperties, RequestOptions options = null) | |||
=> ClientHelper.BulkOverwriteGlobalApplicationCommandAsync(this, commandProperties, options); | |||
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); | |||
/// <inheritdoc /> | |||
async Task<IReadOnlyCollection<IApplicationCommand>> IDiscordClient.GetGlobalApplicationCommandsAsync(RequestOptions options) | |||
=> await GetGlobalApplicationCommands(options).ConfigureAwait(false); | |||
async Task<IReadOnlyCollection<IApplicationCommand>> IDiscordClient.GetGlobalApplicationCommandsAsync(bool withLocalizations, RequestOptions options) | |||
=> await GetGlobalApplicationCommands(withLocalizations, options).ConfigureAwait(false); | |||
/// <inheritdoc /> | |||
async Task<IApplicationCommand> IDiscordClient.GetGlobalApplicationCommandAsync(ulong id, RequestOptions options) | |||
=> await ClientHelper.GetGlobalApplicationCommandAsync(this, id, options).ConfigureAwait(false); | |||
@@ -362,10 +362,10 @@ namespace Discord.Rest | |||
#endregion | |||
#region Interactions | |||
public static async Task<IReadOnlyCollection<RestGuildCommand>> GetSlashCommandsAsync(IGuild guild, BaseDiscordClient client, | |||
public static async Task<IReadOnlyCollection<RestGuildCommand>> GetSlashCommandsAsync(IGuild guild, BaseDiscordClient client, bool withLocalizations, | |||
RequestOptions options) | |||
{ | |||
var models = await client.ApiClient.GetGuildApplicationCommandsAsync(guild.Id, options); | |||
var models = await client.ApiClient.GetGuildApplicationCommandsAsync(guild.Id, withLocalizations, options); | |||
return models.Select(x => RestGuildCommand.Create(client, x, guild.Id)).ToImmutableArray(); | |||
} | |||
public static async Task<RestGuildCommand> GetSlashCommandAsync(IGuild guild, ulong id, BaseDiscordClient client, | |||
@@ -316,8 +316,8 @@ namespace Discord.Rest | |||
/// A task that represents the asynchronous get operation. The task result contains a read-only collection of | |||
/// slash commands created by the current user. | |||
/// </returns> | |||
public Task<IReadOnlyCollection<RestGuildCommand>> GetSlashCommandsAsync(RequestOptions options = null) | |||
=> GuildHelper.GetSlashCommandsAsync(this, Discord, options); | |||
public Task<IReadOnlyCollection<RestGuildCommand>> GetSlashCommandsAsync(bool withLocalizations = false, RequestOptions options = null) | |||
=> GuildHelper.GetSlashCommandsAsync(this, Discord, withLocalizations, options); | |||
/// <summary> | |||
/// Gets a slash command in the current guild. | |||
@@ -933,8 +933,8 @@ namespace Discord.Rest | |||
/// A task that represents the asynchronous get operation. The task result contains a read-only collection | |||
/// of application commands found within the guild. | |||
/// </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, RequestOptions options = null) | |||
=> await ClientHelper.GetGuildApplicationCommandsAsync(Discord, Id, withLocalizations, options).ConfigureAwait(false); | |||
/// <summary> | |||
/// Gets an application command within this guild with the specified id. | |||
/// </summary> | |||
@@ -1467,8 +1467,8 @@ namespace Discord.Rest | |||
async Task<IReadOnlyCollection<IWebhook>> IGuild.GetWebhooksAsync(RequestOptions options) | |||
=> await GetWebhooksAsync(options).ConfigureAwait(false); | |||
/// <inheritdoc /> | |||
async Task<IReadOnlyCollection<IApplicationCommand>> IGuild.GetApplicationCommandsAsync (RequestOptions options) | |||
=> await GetApplicationCommandsAsync(options).ConfigureAwait(false); | |||
async Task<IReadOnlyCollection<IApplicationCommand>> IGuild.GetApplicationCommandsAsync (bool withLocalizations, RequestOptions options) | |||
=> await GetApplicationCommandsAsync(withLocalizations, options).ConfigureAwait(false); | |||
/// <inheritdoc /> | |||
async Task<ICustomSticker> IGuild.CreateStickerAsync(string name, string description, IEnumerable<string> tags, Image image, RequestOptions options) | |||
=> await CreateStickerAsync(name, description, tags, image, options); | |||
@@ -3,6 +3,7 @@ using Discord.API.Rest; | |||
using Discord.Net; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Collections.Immutable; | |||
using System.Linq; | |||
using System.Net; | |||
using System.Threading.Tasks; | |||
@@ -101,11 +102,12 @@ namespace Discord.Rest | |||
DefaultPermission = arg.IsDefaultPermission.IsSpecified | |||
? arg.IsDefaultPermission.Value | |||
: Optional<bool>.Unspecified, | |||
NameLocalizations = arg.NameLocalizations?.ToDictionary(), | |||
DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary(), | |||
// TODO: better conversion to nullable optionals | |||
DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(), | |||
DmPermission = arg.IsDMEnabled.ToNullable() | |||
}; | |||
if (arg is SlashCommandProperties slashProps) | |||
@@ -140,12 +142,16 @@ namespace Discord.Rest | |||
DefaultPermission = arg.IsDefaultPermission.IsSpecified | |||
? arg.IsDefaultPermission.Value | |||
: Optional<bool>.Unspecified, | |||
NameLocalizations = arg.NameLocalizations?.ToDictionary(), | |||
DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary(), | |||
// TODO: better conversion to nullable optionals | |||
DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(), | |||
DmPermission = arg.IsDMEnabled.ToNullable() | |||
}; | |||
Console.WriteLine("Locales:" + string.Join(",", arg.NameLocalizations.Keys)); | |||
if (arg is SlashCommandProperties slashProps) | |||
{ | |||
Preconditions.NotNullOrEmpty(slashProps.Description, nameof(slashProps.Description)); | |||
@@ -181,6 +187,8 @@ namespace Discord.Rest | |||
DefaultPermission = arg.IsDefaultPermission.IsSpecified | |||
? arg.IsDefaultPermission.Value | |||
: Optional<bool>.Unspecified, | |||
NameLocalizations = arg.NameLocalizations?.ToDictionary(), | |||
DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary(), | |||
// TODO: better conversion to nullable optionals | |||
DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(), | |||
@@ -244,7 +252,9 @@ namespace Discord.Rest | |||
Name = args.Name, | |||
DefaultPermission = args.IsDefaultPermission.IsSpecified | |||
? args.IsDefaultPermission.Value | |||
: Optional<bool>.Unspecified | |||
: Optional<bool>.Unspecified, | |||
NameLocalizations = args.NameLocalizations?.ToDictionary(), | |||
DescriptionLocalizations = args.DescriptionLocalizations?.ToDictionary() | |||
}; | |||
if (args is SlashCommandProperties slashProps) | |||
@@ -299,6 +309,8 @@ namespace Discord.Rest | |||
DefaultPermission = arg.IsDefaultPermission.IsSpecified | |||
? arg.IsDefaultPermission.Value | |||
: Optional<bool>.Unspecified, | |||
NameLocalizations = arg.NameLocalizations?.ToDictionary(), | |||
DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary(), | |||
// TODO: better conversion to nullable optionals | |||
DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(), | |||
@@ -335,7 +347,9 @@ namespace Discord.Rest | |||
Name = arg.Name, | |||
DefaultPermission = arg.IsDefaultPermission.IsSpecified | |||
? arg.IsDefaultPermission.Value | |||
: Optional<bool>.Unspecified | |||
: Optional<bool>.Unspecified, | |||
NameLocalizations = arg.NameLocalizations?.ToDictionary(), | |||
DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary() | |||
}; | |||
if (arg is SlashCommandProperties slashProps) | |||
@@ -38,6 +38,32 @@ namespace Discord.Rest | |||
/// </summary> | |||
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/> | |||
public DateTimeOffset CreatedAt | |||
=> SnowflakeUtils.FromSnowflake(Id); | |||
@@ -64,6 +90,15 @@ namespace Discord.Rest | |||
? model.Options.Value.Select(RestApplicationCommandOption.Create).ToImmutableArray() | |||
: 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); | |||
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; | |||
namespace Discord.Rest | |||
@@ -13,10 +15,25 @@ namespace Discord.Rest | |||
/// <inheritdoc/> | |||
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) | |||
{ | |||
Name = model.Name; | |||
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; } | |||
/// <inheritdoc/> | |||
public bool? IsAutocomplete { get; private set; } | |||
public bool? IsAutocomplete { get; private set; } | |||
/// <inheritdoc/> | |||
public double? MinValue { get; private set; } | |||
@@ -48,6 +48,32 @@ namespace Discord.Rest | |||
/// <inheritdoc/> | |||
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 static RestApplicationCommandOption Create(Model model) | |||
@@ -89,6 +115,15 @@ namespace Discord.Rest | |||
ChannelTypes = model.ChannelTypes.IsSpecified | |||
? model.ChannelTypes.Value.ToImmutableArray() | |||
: 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 | |||
@@ -455,9 +455,9 @@ namespace Discord.WebSocket | |||
/// A task that represents the asynchronous get operation. The task result contains a read-only collection of global | |||
/// application commands. | |||
/// </returns> | |||
public async Task<IReadOnlyCollection<SocketApplicationCommand>> GetGlobalApplicationCommandsAsync(RequestOptions options = null) | |||
public async Task<IReadOnlyCollection<SocketApplicationCommand>> GetGlobalApplicationCommandsAsync(bool withLocalizations = false, RequestOptions options = null) | |||
{ | |||
var commands = (await ApiClient.GetGlobalApplicationCommandsAsync(options)).Select(x => SocketApplicationCommand.Create(this, x)); | |||
var commands = (await ApiClient.GetGlobalApplicationCommandsAsync(withLocalizations, options)).Select(x => SocketApplicationCommand.Create(this, x)); | |||
foreach(var command in commands) | |||
{ | |||
@@ -3230,8 +3230,8 @@ namespace Discord.WebSocket | |||
async Task<IApplicationCommand> IDiscordClient.GetGlobalApplicationCommandAsync(ulong id, RequestOptions options) | |||
=> await GetGlobalApplicationCommandAsync(id, options); | |||
/// <inheritdoc /> | |||
async Task<IReadOnlyCollection<IApplicationCommand>> IDiscordClient.GetGlobalApplicationCommandsAsync(RequestOptions options) | |||
=> await GetGlobalApplicationCommandsAsync(options); | |||
async Task<IReadOnlyCollection<IApplicationCommand>> IDiscordClient.GetGlobalApplicationCommandsAsync(bool withLocalizations, RequestOptions options) | |||
=> await GetGlobalApplicationCommandsAsync(withLocalizations, options); | |||
/// <inheritdoc /> | |||
async Task IDiscordClient.StartAsync() | |||
@@ -882,9 +882,10 @@ namespace Discord.WebSocket | |||
/// A task that represents the asynchronous get operation. The task result contains a read-only collection of | |||
/// slash commands created by the current user. | |||
/// </returns> | |||
public async Task<IReadOnlyCollection<SocketApplicationCommand>> GetApplicationCommandsAsync(RequestOptions options = null) | |||
public async Task<IReadOnlyCollection<SocketApplicationCommand>> GetApplicationCommandsAsync(bool withLocalizations = false, 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, options)) | |||
.Select(x => SocketApplicationCommand.Create(Discord, x, Id)); | |||
foreach (var command in commands) | |||
{ | |||
@@ -1980,8 +1981,8 @@ namespace Discord.WebSocket | |||
async Task<IReadOnlyCollection<IWebhook>> IGuild.GetWebhooksAsync(RequestOptions options) | |||
=> await GetWebhooksAsync(options).ConfigureAwait(false); | |||
/// <inheritdoc /> | |||
async Task<IReadOnlyCollection<IApplicationCommand>> IGuild.GetApplicationCommandsAsync (RequestOptions options) | |||
=> await GetApplicationCommandsAsync(options).ConfigureAwait(false); | |||
async Task<IReadOnlyCollection<IApplicationCommand>> IGuild.GetApplicationCommandsAsync (bool withLocalizations, RequestOptions options) | |||
=> await GetApplicationCommandsAsync(withLocalizations, options).ConfigureAwait(false); | |||
/// <inheritdoc /> | |||
async Task<ICustomSticker> IGuild.CreateStickerAsync(string name, string description, IEnumerable<string> tags, Image image, RequestOptions options) | |||
=> await CreateStickerAsync(name, description, tags, image, options); | |||
@@ -50,6 +50,32 @@ namespace Discord.WebSocket | |||
/// </remarks> | |||
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/> | |||
public DateTimeOffset CreatedAt | |||
=> SnowflakeUtils.FromSnowflake(Id); | |||
@@ -93,6 +119,15 @@ namespace Discord.WebSocket | |||
? model.Options.Value.Select(SocketApplicationCommandOption.Create).ToImmutableArray() | |||
: 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); | |||
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; | |||
namespace Discord.WebSocket | |||
@@ -13,6 +15,19 @@ namespace Discord.WebSocket | |||
/// <inheritdoc/> | |||
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 static SocketApplicationCommandChoice Create(Model model) | |||
{ | |||
@@ -24,6 +39,8 @@ namespace Discord.WebSocket | |||
{ | |||
Name = model.Name; | |||
Value = model.Value; | |||
NameLocalizations = model.NameLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary(); | |||
NameLocalized = model.NameLocalized.GetValueOrDefault(null); | |||
} | |||
} | |||
} |
@@ -48,6 +48,32 @@ namespace Discord.WebSocket | |||
/// </summary> | |||
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 static SocketApplicationCommandOption Create(Model model) | |||
{ | |||
@@ -83,6 +109,15 @@ namespace Discord.WebSocket | |||
ChannelTypes = model.ChannelTypes.IsSpecified | |||
? model.ChannelTypes.Value.ToImmutableArray() | |||
: 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; | |||