Browse Source

Merge branch 'dev' into scheduled-events-in-audit-log

pull/2437/head
SaculRennorb GitHub 2 years ago
parent
commit
26d229079c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 1533 additions and 204 deletions
  1. +38
    -0
      CHANGELOG.md
  2. +1
    -1
      Discord.Net.targets
  3. +1
    -1
      docs/docfx.json
  4. +41
    -0
      docs/guides/int_framework/intro.md
  5. +5
    -0
      src/Discord.Net.Commands/CommandService.cs
  6. +1
    -1
      src/Discord.Net.Core/DiscordConfig.cs
  7. +24
    -0
      src/Discord.Net.Core/DiscordErrorCode.cs
  8. +6
    -1
      src/Discord.Net.Core/Entities/Guilds/IGuild.cs
  9. +75
    -17
      src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs
  10. +33
    -0
      src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs
  11. +52
    -0
      src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs
  12. +70
    -0
      src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs
  13. +71
    -1
      src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs
  14. +26
    -0
      src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs
  15. +26
    -0
      src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs
  16. +15
    -0
      src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs
  17. +291
    -40
      src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs
  18. +6
    -0
      src/Discord.Net.Core/Entities/Messages/IMessage.cs
  19. +15
    -0
      src/Discord.Net.Core/Extensions/GenericCollectionExtensions.cs
  20. +9
    -2
      src/Discord.Net.Core/GatewayIntents.cs
  21. +3
    -1
      src/Discord.Net.Core/IDiscordClient.cs
  22. +7
    -3
      src/Discord.Net.Core/Net/Rest/IRestClient.cs
  23. +8
    -4
      src/Discord.Net.Core/RequestOptions.cs
  24. +5
    -5
      src/Discord.Net.Core/Utils/Preconditions.cs
  25. +3
    -3
      src/Discord.Net.Interactions/Attributes/MaxLengthAttribute.cs
  26. +3
    -3
      src/Discord.Net.Interactions/Attributes/MinLengthAttribute.cs
  27. +6
    -6
      src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs
  28. +6
    -0
      src/Discord.Net.Interactions/InteractionService.cs
  29. +5
    -0
      src/Discord.Net.Interactions/InteractionServiceConfig.cs
  30. +32
    -0
      src/Discord.Net.Interactions/LocalizationManagers/ILocalizationManager.cs
  31. +72
    -0
      src/Discord.Net.Interactions/LocalizationManagers/JsonLocalizationManager.cs
  32. +55
    -0
      src/Discord.Net.Interactions/LocalizationManagers/ResxLocalizationManager.cs
  33. +25
    -0
      src/Discord.Net.Interactions/LocalizationTarget.cs
  34. +2
    -21
      src/Discord.Net.Interactions/Map/CommandMap.cs
  35. +71
    -17
      src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs
  36. +53
    -0
      src/Discord.Net.Interactions/Utilities/CommandHierarchy.cs
  37. +13
    -0
      src/Discord.Net.Rest/API/Common/ApplicationCommand.cs
  38. +21
    -0
      src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs
  39. +7
    -0
      src/Discord.Net.Rest/API/Common/ApplicationCommandOptionChoice.cs
  40. +14
    -1
      src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs
  41. +7
    -0
      src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs
  42. +1
    -1
      src/Discord.Net.Rest/BaseDiscordClient.cs
  43. +6
    -6
      src/Discord.Net.Rest/ClientHelper.cs
  44. +28
    -5
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  45. +7
    -7
      src/Discord.Net.Rest/DiscordRestClient.cs
  46. +3
    -3
      src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs
  47. +10
    -6
      src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
  48. +15
    -3
      src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs
  49. +35
    -0
      src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs
  50. +17
    -0
      src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandChoice.cs
  51. +36
    -1
      src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs
  52. +16
    -4
      src/Discord.Net.Rest/Net/DefaultRestClient.cs
  53. +4
    -1
      src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs
  54. +6
    -4
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  55. +7
    -4
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
  56. +35
    -0
      src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs
  57. +17
    -0
      src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandChoice.cs
  58. +35
    -0
      src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandOption.cs
  59. +31
    -31
      src/Discord.Net/Discord.Net.nuspec

+ 38
- 0
CHANGELOG.md View File

@@ -1,4 +1,42 @@
# Changelog

## [3.8.0] - 2022-08-27
### Added
- #2384 Added support for the WEBHOOKS_UPDATED event (010e8e8)
- #2370 Add async callbacks for IModuleBase (503fa75)
- #2367 Added DeleteMessagesAsync for TIV and added remaining rate limit in client log (f178660)
- #2379 Added Max/Min length fields for ApplicationCommandOption (e551431)
- #2369 Added support for using `RespondWithModalAsync<IModal>()` without prior IModal declaration (500e7b4)
- #2347 Added Embed field comparison operators (89a8ea1)
- #2359 Added support for creating lottie stickers (32b03c8)
- #2395 Added App Command localization support and `ILocalizationManager` to IF (39bbd29)

### Fixed
- #2425 Fix missing Fact attribute in ColorTests (92215b1)
- #2424 Fix IGuild.GetBansAsync() (b7b7964)
- #2416 Fix role icon & emoji assignment (b6b5e95)
- #2414 Fix NRE on RestCommandBase Data (02bc3b7)
- #2421 Fix placeholder length being hardcoded (8dfe19f)
- #2352 Fix issues related to the absence of bot scope (1eb42c6)
- #2346 Fix IGuild.DisconnectAsync(IUser) not disconnecting users (ba02416)
- #2404 Fix range of issues presented by 3rd party analyzer (902326d)
- #2409 Removes GroupContext from requirecontext (b0b8167)

### Misc
- #2366 Fixed typo in ChannelUpdatedEvent's documentation (cfd2662)
- #2408 Fix sharding sample throwing at appcommand registration (519deda)
- #2420 Fix broken code snippet in dependency injection docs (ddcf68a)
- #2430 Add a note about DontAutoRegisterAttribute (917118d)
- #2418 Update xmldocs to reflect the ConnectedUsers split (65b98f8)
- #2415 Adds missing DI entries in TOC (c49d483)
- #2407 Introduces high quality dependency injection documentation (6fdcf98)
- #2348 Added `RequiredInput` attribute to example in int.framework intro (ee6e0ad)
- #2385 Add ServerStarter.Host to deployment.md (06ed995)
- #2405 Add a note about `IgnoreGroupNames` to IF docs (cf25acd)
- #2356 Makes voice section about precompiled binaries more visible (e0d68d4 )
- #2405 IF intro docs improvements (246282d)
- #2406 Labs deprecation & readme/docs edits (bf493ea)

## [3.7.2] - 2022-06-02
### Added
- #2328 Add method overloads to InteractionService (0fad3e8)


+ 1
- 1
Discord.Net.targets View File

@@ -1,6 +1,6 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VersionPrefix>3.7.2</VersionPrefix>
<VersionPrefix>3.8.0</VersionPrefix>
<LangVersion>latest</LangVersion>
<Authors>Discord.Net Contributors</Authors>
<PackageTags>discord;discordapp</PackageTags>


+ 1
- 1
docs/docfx.json View File

@@ -60,7 +60,7 @@
"overwrite": "_overwrites/**/**.md",
"globalMetadata": {
"_appTitle": "Discord.Net Documentation",
"_appFooter": "Discord.Net (c) 2015-2022 3.7.2",
"_appFooter": "Discord.Net (c) 2015-2022 3.8.0",
"_enableSearch": true,
"_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg",
"_appFaviconPath": "favicon.ico"


+ 41
- 0
docs/guides/int_framework/intro.md View File

@@ -376,6 +376,47 @@ respond to the Interactions within your command modules you need to perform the
delegate can be used to create HTTP responses from a deserialized json object string.
- Use the interaction endpoints of the module base instead of the interaction object (ie. `RespondAsync()`, `FollowupAsync()`...).

## Localization

Discord Slash Commands support name/description localization. Localization is available for names and descriptions of Slash Command Groups ([GroupAttribute]), Slash Commands ([SlashCommandAttribute]), Slash Command parameters and Slash Command Parameter Choices. Interaction Service can be initialized with an `ILocalizationManager` instance in its config which is used to create the necessary localization dictionaries on command registration. Interaction Service has two built-in `ILocalizationManager` implementations: `ResxLocalizationManager` and `JsonLocalizationManager`.

### ResXLocalizationManager

`ResxLocalizationManager` uses `.` delimited key names to traverse the resource files and get the localized strings (`group1.group2.command.parameter.name`). A `ResxLocalizationManager` instance must be initialized with a base resource name, a target assembly and a collection of `CultureInfo`s. Every key path must end with either `.name` or `.description`, including parameter choice strings. [Discord.Tools.LocalizationTemplate.Resx](https://www.nuget.org/packages/Discord.Tools.LocalizationTemplate.Resx) dotnet tool can be used to create localization file templates.

### JsonLocalizationManager

`JsonLocaliationManager` uses a nested data structure similar to Discord's Application Commands schema. You can get the Json schema [here](https://gist.github.com/Cenngo/d46a881de24823302f66c3c7e2f7b254). `JsonLocalizationManager` accepts a base path and a base file name and automatically discovers every resource file ( \basePath\fileName.locale.json ). A Json resource file should have a structure similar to:

```json
{
"command_1":{
"name": "localized_name",
"description": "localized_description",
"parameter_1":{
"name": "localized_name",
"description": "localized_description"
}
},
"group_1":{
"name": "localized_name",
"description": "localized_description",
"command_1":{
"name": "localized_name",
"description": "localized_description",
"parameter_1":{
"name": "localized_name",
"description": "localized_description"
},
"parameter_2":{
"name": "localized_name",
"description": "localized_description"
}
}
}
}
```

[AutocompleteHandlers]: xref:Guides.IntFw.AutoCompletion
[DependencyInjection]: xref:Guides.DI.Intro



+ 5
- 0
src/Discord.Net.Commands/CommandService.cs View File

@@ -270,6 +270,11 @@ namespace Discord.Commands
await _moduleLock.WaitAsync().ConfigureAwait(false);
try
{
var typeModulePair = _typedModuleDefs.FirstOrDefault(x => x.Value.Equals(module));

if (!typeModulePair.Equals(default(KeyValuePair<Type, ModuleInfo>)))
_typedModuleDefs.TryRemove(typeModulePair.Key, out var _);

return RemoveModuleInternal(module);
}
finally


+ 1
- 1
src/Discord.Net.Core/DiscordConfig.cs View File

@@ -18,7 +18,7 @@ namespace Discord
/// <see href="https://discord.com/developers/docs/reference#api-versioning">Discord API documentation</see>
/// .</para>
/// </returns>
public const int APIVersion = 9;
public const int APIVersion = 10;
/// <summary>
/// Returns the Voice API version Discord.Net uses.
/// </summary>


+ 24
- 0
src/Discord.Net.Core/DiscordErrorCode.cs View File

@@ -66,6 +66,7 @@ namespace Discord
ActionSlowmode = 20016,
OnlyOwnerAction = 20018,
AnnouncementEditRatelimit = 20022,
UnderMinimumAge = 20024,
ChannelWriteRatelimit = 20028,
WriteRatelimitReached = 20029,
WordsNotAllowed = 20031,
@@ -88,7 +89,9 @@ namespace Discord
MaximumServerMembersReached = 30019,
MaximumServerCategoriesReached = 30030,
GuildTemplateAlreadyExists = 30031,
MaximumNumberOfApplicationCommandsReached = 30032,
MaximumThreadMembersReached = 30033,
MaxNumberOfDailyApplicationCommandCreatesHasBeenReached = 30034,
MaximumBansForNonGuildMembersReached = 30035,
MaximumBanFetchesReached = 30037,
MaximumUncompleteGuildScheduledEvents = 30038,
@@ -98,6 +101,7 @@ namespace Discord
#endregion

#region General Request Errors (40XXX)
BitrateIsTooHighForChannelOfThisType = 30052,
MaximumNumberOfEditsReached = 30046,
MaximumNumberOfPinnedThreadsInAForumChannelReached = 30047,
MaximumNumberOfTagsInAForumChannelReached = 30048,
@@ -108,12 +112,17 @@ namespace Discord
RequestEntityTooLarge = 40005,
FeatureDisabled = 40006,
UserBanned = 40007,
ConnectionHasBeenRevoked = 40012,
TargetUserNotInVoice = 40032,
MessageAlreadyCrossposted = 40033,
ApplicationNameAlreadyExists = 40041,
#endregion

#region Action Preconditions/Checks (50XXX)
ApplicationInteractionFailedToSend = 40043,
CannotSendAMessageInAForumChannel = 40058,
ThereAreNoTagsAvailableThatCanBeSetByNonModerators = 40066,
ATagIsRequiredToCreateAForumPostInThisChannel = 40067,
InteractionHasAlreadyBeenAcknowledged = 40060,
TagNamesMustBeUnique = 40061,
MissingPermissions = 50001,
@@ -132,6 +141,7 @@ namespace Discord
InvalidAuthenticationToken = 50014,
NoteTooLong = 50015,
ProvidedMessageDeleteCountOutOfBounds = 50016,
InvalidMFALevel = 50017,
InvalidPinChannel = 50019,
InvalidInvite = 50020,
CannotExecuteOnSystemMessage = 50021,
@@ -165,6 +175,9 @@ namespace Discord
#endregion

#region 2FA (60XXX)
OwnershipCannotBeTransferredToABotUser = 50132,
AssetResizeBelowTheMaximumSize= 50138,
UploadedFileNotFound = 50146,
MissingPermissionToSendThisSticker = 50600,
Requires2FA = 60003,
#endregion
@@ -178,6 +191,7 @@ namespace Discord
#endregion

#region API Status (130XXX)
ApplicationNotYetAvailable = 110001,
APIOverloaded = 130000,
#endregion

@@ -207,5 +221,15 @@ namespace Discord
CannotUpdateFinishedEvent = 180000,
FailedStageCreation = 180002,
#endregion
#region Forum & Automod
MessageWasBlockedByAutomaticModeration = 200000,
TitleWasBlockedByAutomaticModeration = 200001,
WebhooksPostedToForumChannelsMustHaveAThreadNameOrThreadId = 220001,
WebhooksPostedToForumChannelsCannotHaveBothAThreadNameAndThreadId = 220002,
WebhooksCanOnlyCreateThreadsInForumChannels = 220003,
WebhookServicesCannotBeUsedInForumChannels = 220004,
MessageBlockedByHarmfulLinksFilter = 240000,
#endregion
}
}

+ 6
- 1
src/Discord.Net.Core/Entities/Guilds/IGuild.cs View File

@@ -1194,12 +1194,17 @@ 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="locale">The target locale of the localized name and description fields. Sets the <c>X-Discord-Locale</c> header, which takes precedence over <c>Accept-Language</c>.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <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, string locale = null, RequestOptions options = null);

/// <summary>
/// Gets an application command within this guild with the specified id.


+ 75
- 17
src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs View File

@@ -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>
@@ -105,5 +96,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.");
}
}
}
}

+ 33
- 0
src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs View File

@@ -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;
}
}
}
}

+ 52
- 0
src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs View File

@@ -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>


+ 70
- 0
src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs View File

@@ -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">The localization dictionary to use for the name field of this command.</param>
/// <returns></returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="nameLocalizations"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown if any dictionary key is an invalid locale string.</exception>
public MessageCommandBuilder WithNameLocalizations(IDictionary<string, string> nameLocalizations)
{
if (nameLocalizations is null)
throw new ArgumentNullException(nameof(nameLocalizations));

foreach (var (locale, name) in nameLocalizations)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

EnsureValidCommandName(name);
}

_nameLocalizations = new Dictionary<string, string>(nameLocalizations);
return this;
}

/// <summary>
/// Sets whether or not this command can be used in dms
/// </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>


+ 71
- 1
src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs View File

@@ -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">The localization dictionary to use for the name field of this command.</param>
/// <returns>The current builder.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="nameLocalizations"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown if any dictionary key is an invalid locale string.</exception>
public UserCommandBuilder WithNameLocalizations(IDictionary<string, string> nameLocalizations)
{
if (nameLocalizations is null)
throw new ArgumentNullException(nameof(nameLocalizations));

foreach (var (locale, name) in nameLocalizations)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

EnsureValidCommandName(name);
}

_nameLocalizations = new Dictionary<string, string>(nameLocalizations);
return this;
}
/// <summary>
/// Sets whether or not this command can be used in dms
/// </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>


+ 26
- 0
src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs View File

@@ -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>


+ 26
- 0
src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs View File

@@ -71,5 +71,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 option.
/// </summary>
IReadOnlyDictionary<string, string> NameLocalizations { get; }

/// <summary>
/// Gets the localization dictionary for the description field of this command option.
/// </summary>
IReadOnlyDictionary<string, string> DescriptionLocalizations { get; }

/// <summary>
/// Gets the localized name of this command option.
/// </summary>
/// <remarks>
/// Only returned when the `withLocalizations` query parameter is set to <see langword="false"/> when requesting the command.
/// </remarks>
string NameLocalized { get; }

/// <summary>
/// Gets the localized description of this command option.
/// </summary>
/// <remarks>
/// Only returned when the `withLocalizations` query parameter is set to true when requesting the command.
/// </remarks>
string DescriptionLocalized { get; }
}
}

+ 15
- 0
src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs View File

@@ -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 option.
/// </summary>
IReadOnlyDictionary<string, string> NameLocalizations { get; }

/// <summary>
/// Gets the localized name of this command option.
/// </summary>
/// <remarks>
/// Only returned when the `withLocalizations` query parameter is set to <see langword="false"/> when requesting the command.
/// </remarks>
string NameLocalized { get; }
}
}

+ 291
- 40
src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs View File

@@ -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,17 @@ 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,
int? minLength = null, int? maxLength = null, List<SlashCommandOptionBuilder> options = null, List<ChannelType> channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices)
List<SlashCommandOptionBuilder> options = null, List<ChannelType> channelTypes = null, IDictionary<string, string> nameLocalizations = null,
IDictionary<string, string> descriptionLocalizations = null,
int? minLength = null, int? maxLength = null, params ApplicationCommandOptionChoiceProperties[] choices)
{
Preconditions.Options(name, description);

@@ -226,6 +233,12 @@ namespace Discord
MaxLength = maxLength,
};

if (nameLocalizations is not null)
option.WithNameLocalizations(nameLocalizations);

if (descriptionLocalizations is not null)
option.WithDescriptionLocalizations(descriptionLocalizations);

return AddOption(option);
}

@@ -268,6 +281,116 @@ namespace Discord
Options.AddRange(options);
return this;
}

/// <summary>
/// Sets the <see cref="NameLocalizations"/> collection.
/// </summary>
/// <param name="nameLocalizations">The localization dictionary to use for the name field of this command.</param>
/// <returns></returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="nameLocalizations"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown if any dictionary key is an invalid locale string.</exception>
public SlashCommandBuilder WithNameLocalizations(IDictionary<string, string> nameLocalizations)
{
if (nameLocalizations is null)
throw new ArgumentNullException(nameof(nameLocalizations));

foreach (var (locale, name) in nameLocalizations)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

EnsureValidCommandName(name);
}

_nameLocalizations = new Dictionary<string, string>(nameLocalizations);
return this;
}

/// <summary>
/// Sets the <see cref="DescriptionLocalizations"/> collection.
/// </summary>
/// <param name="descriptionLocalizations">The localization dictionary to use for the description field of this command.</param>
/// <returns></returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="descriptionLocalizations"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown if any dictionary key is an invalid locale string.</exception>
public SlashCommandBuilder WithDescriptionLocalizations(IDictionary<string, string> descriptionLocalizations)
{
if (descriptionLocalizations is null)
throw new ArgumentNullException(nameof(descriptionLocalizations));

foreach (var (locale, description) in descriptionLocalizations)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

EnsureValidCommandDescription(description);
}

_descriptionLocalizations = new Dictionary<string, string>(descriptionLocalizations);
return this;
}

/// <summary>
/// Adds a new entry to the <see cref="NameLocalizations"/> collection.
/// </summary>
/// <param name="locale">Locale of the entry.</param>
/// <param name="name">Localized string for the name field.</param>
/// <returns>The current builder.</returns>
/// <exception cref="ArgumentException">Thrown if <paramref name="locale"/> is an invalid locale string.</exception>
public SlashCommandBuilder AddNameLocalization(string locale, string name)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

EnsureValidCommandName(name);

_nameLocalizations ??= new();
_nameLocalizations.Add(locale, name);

return this;
}

/// <summary>
/// Adds a new entry to the <see cref="Description"/> collection.
/// </summary>
/// <param name="locale">Locale of the entry.</param>
/// <param name="description">Localized string for the description field.</param>
/// <returns>The current builder.</returns>
/// <exception cref="ArgumentException">Thrown if <paramref name="locale"/> is an invalid locale string.</exception>
public SlashCommandBuilder AddDescriptionLocalization(string locale, string description)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

EnsureValidCommandDescription(description);

_descriptionLocalizations ??= new();
_descriptionLocalizations.Add(locale, description);

return this;
}

internal static void EnsureValidCommandName(string name)
{
Preconditions.NotNullOrEmpty(name, nameof(name));
Preconditions.AtLeast(name.Length, 1, nameof(name));
Preconditions.AtMost(name.Length, MaxNameLength, nameof(name));

// Discord updated the docs, this regex prevents special characters like @!$%(... etc,
// https://discord.com/developers/docs/interactions/slash-commands#applicationcommand
if (!Regex.IsMatch(name, @"^[\w-]{1,32}$"))
throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(name));

if (name.Any(x => char.IsUpper(x)))
throw new FormatException("Name cannot contain any uppercase characters.");
}

internal static void EnsureValidCommandDescription(string description)
{
Preconditions.NotNullOrEmpty(description, nameof(description));
Preconditions.AtLeast(description.Length, 1, nameof(description));
Preconditions.AtMost(description.Length, MaxDescriptionLength, nameof(description));
}
}

/// <summary>
@@ -287,6 +410,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.
@@ -298,10 +423,7 @@ namespace Discord
{
if (value != null)
{
Preconditions.AtLeast(value.Length, 1, nameof(value));
Preconditions.AtMost(value.Length, SlashCommandBuilder.MaxNameLength, nameof(value));
if (!Regex.IsMatch(value, @"^[\w-]{1,32}$"))
throw new ArgumentException("Option name cannot contain any special characters or whitespaces!", nameof(value));
EnsureValidCommandOptionName(value);
}

_name = value;
@@ -318,8 +440,7 @@ namespace Discord
{
if (value != null)
{
Preconditions.AtLeast(value.Length, 1, nameof(value));
Preconditions.AtMost(value.Length, SlashCommandBuilder.MaxDescriptionLength, nameof(value));
EnsureValidCommandOptionDescription(value);
}

_description = value;
@@ -381,6 +502,16 @@ namespace Discord
/// </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>
@@ -424,6 +555,8 @@ namespace Discord
ChannelTypes = ChannelTypes,
MinValue = MinValue,
MaxValue = MaxValue,
NameLocalizations = _nameLocalizations,
DescriptionLocalizations = _descriptionLocalizations,
MinLength = MinLength,
MaxLength = MaxLength,
};
@@ -440,13 +573,17 @@ namespace Discord
/// <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,
int? minLength = null, int? maxLength = null, List<SlashCommandOptionBuilder> options = null, List<ChannelType> channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices)
List<SlashCommandOptionBuilder> options = null, List<ChannelType> channelTypes = null, IDictionary<string, string> nameLocalizations = null,
IDictionary<string, string> descriptionLocalizations = null,
int? minLength = null, int? maxLength = null, params ApplicationCommandOptionChoiceProperties[] choices)
{
Preconditions.Options(name, description);

@@ -473,9 +610,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>
@@ -522,10 +665,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">The localization dictionary for to use the name field of this command option choice.</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>
@@ -533,10 +677,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">The localization dictionary for to use the name field of this command option choice.</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>
@@ -544,10 +689,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>
@@ -555,10 +701,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">The localization dictionary to use for the name field of this command option choice.</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>
@@ -566,13 +713,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">The localization dictionary to use for the name field of this command option choice.</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>();

@@ -594,7 +742,8 @@ namespace Discord
Choices.Add(new ApplicationCommandOptionChoiceProperties
{
Name = name,
Value = value
Value = value,
NameLocalizations = nameLocalizations
});

return this;
@@ -706,11 +855,11 @@ namespace Discord
/// <summary>
/// Sets the current builders max length field.
/// </summary>
/// <param name="lenght">The value to set.</param>
/// <param name="length">The value to set.</param>
/// <returns>The current builder.</returns>
public SlashCommandOptionBuilder WithMaxLength(int lenght)
public SlashCommandOptionBuilder WithMaxLength(int length)
{
MaxLength = lenght;
MaxLength = length;
return this;
}

@@ -724,5 +873,107 @@ namespace Discord
Type = type;
return this;
}

/// <summary>
/// Sets the <see cref="NameLocalizations"/> collection.
/// </summary>
/// <param name="nameLocalizations">The localization dictionary to use for the name field of this command option.</param>
/// <returns>The current builder.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="nameLocalizations"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown if any dictionary key is an invalid locale string.</exception>
public SlashCommandOptionBuilder WithNameLocalizations(IDictionary<string, string> nameLocalizations)
{
if (nameLocalizations is null)
throw new ArgumentNullException(nameof(nameLocalizations));

foreach (var (locale, name) in nameLocalizations)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

EnsureValidCommandOptionName(name);
}

_nameLocalizations = new Dictionary<string, string>(nameLocalizations);
return this;
}

/// <summary>
/// Sets the <see cref="DescriptionLocalizations"/> collection.
/// </summary>
/// <param name="descriptionLocalizations">The localization dictionary to use for the description field of this command option.</param>
/// <returns>The current builder.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="descriptionLocalizations"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown if any dictionary key is an invalid locale string.</exception>
public SlashCommandOptionBuilder WithDescriptionLocalizations(IDictionary<string, string> descriptionLocalizations)
{
if (descriptionLocalizations is null)
throw new ArgumentNullException(nameof(descriptionLocalizations));

foreach (var (locale, description) in _descriptionLocalizations)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

EnsureValidCommandOptionDescription(description);
}

_descriptionLocalizations = new Dictionary<string, string>(descriptionLocalizations);
return this;
}

/// <summary>
/// Adds a new entry to the <see cref="NameLocalizations"/> collection.
/// </summary>
/// <param name="locale">Locale of the entry.</param>
/// <param name="name">Localized string for the name field.</param>
/// <returns>The current builder.</returns>
/// <exception cref="ArgumentException">Thrown if <paramref name="locale"/> is an invalid locale string.</exception>
public SlashCommandOptionBuilder AddNameLocalization(string locale, string name)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

EnsureValidCommandOptionName(name);

_descriptionLocalizations ??= new();
_nameLocalizations.Add(locale, name);

return this;
}

/// <summary>
/// Adds a new entry to the <see cref="DescriptionLocalizations"/> collection.
/// </summary>
/// <param name="locale">Locale of the entry.</param>
/// <param name="description">Localized string for the description field.</param>
/// <returns>The current builder.</returns>
/// <exception cref="ArgumentException">Thrown if <paramref name="locale"/> is an invalid locale string.</exception>
public SlashCommandOptionBuilder AddDescriptionLocalization(string locale, string description)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

EnsureValidCommandOptionDescription(description);

_descriptionLocalizations ??= new();
_descriptionLocalizations.Add(locale, description);

return this;
}

private static void EnsureValidCommandOptionName(string name)
{
Preconditions.AtLeast(name.Length, 1, nameof(name));
Preconditions.AtMost(name.Length, SlashCommandBuilder.MaxNameLength, nameof(name));
if (!Regex.IsMatch(name, @"^[\w-]{1,32}$"))
throw new ArgumentException("Option name cannot contain any special characters or whitespaces!", nameof(name));
}

private static void EnsureValidCommandOptionDescription(string description)
{
Preconditions.AtLeast(description.Length, 1, nameof(description));
Preconditions.AtMost(description.Length, SlashCommandBuilder.MaxDescriptionLength, nameof(description));
}
}
}

+ 6
- 0
src/Discord.Net.Core/Entities/Messages/IMessage.cs View File

@@ -48,6 +48,9 @@ namespace Discord
/// <summary>
/// Gets the content for this message.
/// </summary>
/// <remarks>
/// This will be empty if the privileged <see cref="GatewayIntents.MessageContent"/> is disabled.
/// </remarks>
/// <returns>
/// A string that contains the body of the message; note that this field may be empty if there is an embed.
/// </returns>
@@ -55,6 +58,9 @@ namespace Discord
/// <summary>
/// Gets the clean content for this message.
/// </summary>
/// <remarks>
/// This will be empty if the privileged <see cref="GatewayIntents.MessageContent"/> is disabled.
/// </remarks>
/// <returns>
/// A string that contains the body of the message stripped of mentions, markdown, emojis and pings; note that this field may be empty if there is an embed.
/// </returns>


+ 15
- 0
src/Discord.Net.Core/Extensions/GenericCollectionExtensions.cs View File

@@ -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);
}

+ 9
- 2
src/Discord.Net.Core/GatewayIntents.cs View File

@@ -39,7 +39,14 @@ namespace Discord
DirectMessageReactions = 1 << 13,
/// <summary> This intent includes TYPING_START </summary>
DirectMessageTyping = 1 << 14,
/// <summary> This intent includes GUILD_SCHEDULED_EVENT_CREATE, GUILD_SCHEDULED_EVENT_UPDATE, GUILD_SCHEDULED_EVENT_DELETE, GUILD_SCHEDULED_EVENT_USER_ADD, GUILD_SCHEDULED_EVENT_USER_REMOVE </summary>
/// <summary>
/// This intent defines if the content within messages received by MESSAGE_CREATE is available or not.
/// This is a privileged intent and needs to be enabled in the developer portal.
/// </summary>
MessageContent = 1 << 15,
/// <summary>
/// This intent includes GUILD_SCHEDULED_EVENT_CREATE, GUILD_SCHEDULED_EVENT_UPDATE, GUILD_SCHEDULED_EVENT_DELETE, GUILD_SCHEDULED_EVENT_USER_ADD, GUILD_SCHEDULED_EVENT_USER_REMOVE
/// </summary>
GuildScheduledEvents = 1 << 16,
/// <summary>
/// This intent includes all but <see cref="GuildMembers"/> and <see cref="GuildPresences"/>
@@ -51,6 +58,6 @@ namespace Discord
/// <summary>
/// This intent includes all of them, including privileged ones.
/// </summary>
All = AllUnprivileged | GuildMembers | GuildPresences
All = AllUnprivileged | GuildMembers | GuildPresences | MessageContent
}
}

+ 3
- 1
src/Discord.Net.Core/IDiscordClient.cs View File

@@ -155,12 +155,14 @@ 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="locale">The target locale of the localized name and description fields. Sets <c>X-Discord-Locale</c> header, which takes precedence over <c>Accept-Language</c>.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <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, string locale = null, RequestOptions options = null);

/// <summary>
/// Creates a global application command.


+ 7
- 3
src/Discord.Net.Core/Net/Rest/IRestClient.cs View File

@@ -30,9 +30,13 @@ namespace Discord.Net.Rest
/// <param name="cancelToken">The cancellation token used to cancel the task.</param>
/// <param name="headerOnly">Indicates whether to send the header only.</param>
/// <param name="reason">The audit log reason.</param>
/// <param name="requestHeaders">Additional headers to be sent with the request.</param>
/// <returns></returns>
Task<RestResponse> SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly = false, string reason = null);
Task<RestResponse> SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly = false, string reason = null);
Task<RestResponse> SendAsync(string method, string endpoint, IReadOnlyDictionary<string, object> multipartParams, CancellationToken cancelToken, bool headerOnly = false, string reason = null);
Task<RestResponse> SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly = false, string reason = null,
IEnumerable<KeyValuePair<string, IEnumerable<string>>> requestHeaders = null);
Task<RestResponse> SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly = false, string reason = null,
IEnumerable<KeyValuePair<string, IEnumerable<string>>> requestHeaders = null);
Task<RestResponse> SendAsync(string method, string endpoint, IReadOnlyDictionary<string, object> multipartParams, CancellationToken cancelToken, bool headerOnly = false, string reason = null,
IEnumerable<KeyValuePair<string, IEnumerable<string>>> requestHeaders = null);
}
}

+ 8
- 4
src/Discord.Net.Core/RequestOptions.cs View File

@@ -1,5 +1,6 @@
using Discord.Net;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

@@ -19,7 +20,7 @@ namespace Discord
/// Gets or sets the maximum time to wait for this request to complete.
/// </summary>
/// <remarks>
/// Gets or set the max time, in milliseconds, to wait for this request to complete. If
/// Gets or set the max time, in milliseconds, to wait for this request to complete. If
/// <c>null</c>, a request will not time out. If a rate limit has been triggered for this request's bucket
/// and will not be unpaused in time, this request will fail immediately.
/// </remarks>
@@ -53,7 +54,7 @@ namespace Discord
/// </summary>
/// <remarks>
/// This property can also be set in <see cref="DiscordConfig"/>.
/// On a per-request basis, the system clock should only be disabled
/// On a per-request basis, the system clock should only be disabled
/// when millisecond precision is especially important, and the
/// hosting system is known to have a desynced clock.
/// </remarks>
@@ -70,8 +71,10 @@ namespace Discord
internal bool IsReactionBucket { get; set; }
internal bool IsGatewayBucket { get; set; }

internal IDictionary<string, IEnumerable<string>> RequestHeaders { get; }

internal static RequestOptions CreateOrClone(RequestOptions options)
{
{
if (options == null)
return new RequestOptions();
else
@@ -96,8 +99,9 @@ namespace Discord
public RequestOptions()
{
Timeout = DiscordConfig.DefaultRequestTimeout;
RequestHeaders = new Dictionary<string, IEnumerable<string>>();
}
public RequestOptions Clone() => MemberwiseClone() as RequestOptions;
}
}

+ 5
- 5
src/Discord.Net.Core/Utils/Preconditions.cs View File

@@ -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>


+ 3
- 3
src/Discord.Net.Interactions/Attributes/MaxLengthAttribute.cs View File

@@ -16,10 +16,10 @@ namespace Discord.Interactions
/// <summary>
/// Sets the maximum length allowed for a string type parameter.
/// </summary>
/// <param name="lenght">Maximum string length allowed.</param>
public MaxLengthAttribute(int lenght)
/// <param name="length">Maximum string length allowed.</param>
public MaxLengthAttribute(int length)
{
Length = lenght;
Length = length;
}
}
}

+ 3
- 3
src/Discord.Net.Interactions/Attributes/MinLengthAttribute.cs View File

@@ -16,10 +16,10 @@ namespace Discord.Interactions
/// <summary>
/// Sets the minimum length allowed for a string type parameter.
/// </summary>
/// <param name="lenght">Minimum string length allowed.</param>
public MinLengthAttribute(int lenght)
/// <param name="length">Minimum string length allowed.</param>
public MinLengthAttribute(int length)
{
Length = lenght;
Length = length;
}
}
}

+ 6
- 6
src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs View File

@@ -67,26 +67,26 @@ namespace Discord.Interactions.Builders
/// <summary>
/// Sets <see cref="MinLength"/>.
/// </summary>
/// <param name="minLenght">New value of the <see cref="MinLength"/>.</param>
/// <param name="minLength">New value of the <see cref="MinLength"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public TextInputComponentBuilder WithMinLenght(int minLenght)
public TextInputComponentBuilder WithMinLength(int minLength)
{
MinLength = minLenght;
MinLength = minLength;
return this;
}

/// <summary>
/// Sets <see cref="MaxLength"/>.
/// </summary>
/// <param name="maxLenght">New value of the <see cref="MaxLength"/>.</param>
/// <param name="maxLength">New value of the <see cref="MaxLength"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public TextInputComponentBuilder WithMaxLenght(int maxLenght)
public TextInputComponentBuilder WithMaxLength(int maxLength)
{
MaxLength = maxLenght;
MaxLength = maxLength;
return this;
}



+ 6
- 0
src/Discord.Net.Interactions/InteractionService.cs View File

@@ -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>
{


+ 5
- 0
src/Discord.Net.Interactions/InteractionServiceConfig.cs View File

@@ -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>


+ 32
- 0
src/Discord.Net.Interactions/LocalizationManagers/ILocalizationManager.cs View File

@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Discord.Interactions
{
/// <summary>
/// Respresents a localization provider for Discord Application Commands.
/// </summary>
public interface ILocalizationManager
{
/// <summary>
/// Get every the resource name for every available locale.
/// </summary>
/// <param name="key">Location of the resource.</param>
/// <param name="destinationType">Type of the resource.</param>
/// <returns>
/// A dictionary containing every available locale and the resource name.
/// </returns>
IDictionary<string, string> GetAllNames(IList<string> key, LocalizationTarget destinationType);

/// <summary>
/// Get every the resource description for every available locale.
/// </summary>
/// <param name="key">Location of the resource.</param>
/// <param name="destinationType">Type of the resource.</param>
/// <returns>
/// A dictionary containing every available locale and the resource name.
/// </returns>
IDictionary<string, string> GetAllDescriptions(IList<string> key, LocalizationTarget destinationType);
}
}

+ 72
- 0
src/Discord.Net.Interactions/LocalizationManagers/JsonLocalizationManager.cs View File

@@ -0,0 +1,72 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace Discord.Interactions
{
/// <summary>
/// The default localization provider for Json resource files.
/// </summary>
public sealed class JsonLocalizationManager : ILocalizationManager
{
private const string NameIdentifier = "name";
private const string DescriptionIdentifier = "description";
private const string SpaceToken = "~";

private readonly string _basePath;
private readonly string _fileName;
private readonly Regex _localeParserRegex = new Regex(@"\w+.(?<locale>\w{2}(?:-\w{2})?).json", RegexOptions.Compiled | RegexOptions.Singleline);

/// <summary>
/// Initializes a new instance of the <see cref="JsonLocalizationManager"/> class.
/// </summary>
/// <param name="basePath">Base path of the Json file.</param>
/// <param name="fileName">Name of the Json file.</param>
public JsonLocalizationManager(string basePath, string fileName)
{
_basePath = basePath;
_fileName = fileName;
}

/// <inheritdoc />
public IDictionary<string, string> GetAllDescriptions(IList<string> key, LocalizationTarget destinationType) =>
GetValues(key, DescriptionIdentifier);

/// <inheritdoc />
public IDictionary<string, string> GetAllNames(IList<string> key, LocalizationTarget destinationType) =>
GetValues(key, NameIdentifier);

private string[] GetAllFiles() =>
Directory.GetFiles(_basePath, $"{_fileName}.*.json", SearchOption.TopDirectoryOnly);

private IDictionary<string, string> GetValues(IList<string> key, string identifier)
{
var result = new Dictionary<string, string>();
var files = GetAllFiles();

foreach (var file in files)
{
var match = _localeParserRegex.Match(Path.GetFileName(file));
if (!match.Success)
continue;

var locale = match.Groups["locale"].Value;

using var sr = new StreamReader(file);
using var jr = new JsonTextReader(sr);
var obj = JObject.Load(jr);
var token = string.Join(".", key.Select(x => $"['{x}']")) + $".{identifier}";
var value = (string)obj.SelectToken(token);
if (value is not null)
result[locale] = value;
}

return result;
}
}
}

+ 55
- 0
src/Discord.Net.Interactions/LocalizationManagers/ResxLocalizationManager.cs View File

@@ -0,0 +1,55 @@
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using System.Resources;

namespace Discord.Interactions
{
/// <summary>
/// The default localization provider for Resx files.
/// </summary>
public sealed class ResxLocalizationManager : ILocalizationManager
{
private const string NameIdentifier = "name";
private const string DescriptionIdentifier = "description";

private readonly ResourceManager _resourceManager;
private readonly IEnumerable<CultureInfo> _supportedLocales;

/// <summary>
/// Initializes a new instance of the <see cref="ResxLocalizationManager"/> class.
/// </summary>
/// <param name="baseResource">Name of the base resource.</param>
/// <param name="assembly">The main assembly for the resources.</param>
/// <param name="supportedLocales">Cultures the <see cref="ResxLocalizationManager"/> should search for.</param>
public ResxLocalizationManager(string baseResource, Assembly assembly, params CultureInfo[] supportedLocales)
{
_supportedLocales = supportedLocales;
_resourceManager = new ResourceManager(baseResource, assembly);
}

/// <inheritdoc />
public IDictionary<string, string> GetAllDescriptions(IList<string> key, LocalizationTarget destinationType) =>
GetValues(key, DescriptionIdentifier);

/// <inheritdoc />
public IDictionary<string, string> GetAllNames(IList<string> key, LocalizationTarget destinationType) =>
GetValues(key, NameIdentifier);

private IDictionary<string, string> GetValues(IList<string> key, string identifier)
{
var entryKey = (string.Join(".", key) + "." + identifier);

var result = new Dictionary<string, string>();

foreach (var locale in _supportedLocales)
{
var value = _resourceManager.GetString(entryKey, locale);
if (value is not null)
result[locale.Name] = value;
}

return result;
}
}
}

+ 25
- 0
src/Discord.Net.Interactions/LocalizationTarget.cs View File

@@ -0,0 +1,25 @@
namespace Discord.Interactions
{
/// <summary>
/// Resource targets for localization.
/// </summary>
public enum LocalizationTarget
{
/// <summary>
/// Target is a <see cref="IInteractionModuleBase"/> tagged with a <see cref="GroupAttribute"/>.
/// </summary>
Group,
/// <summary>
/// Target is an application command method.
/// </summary>
Command,
/// <summary>
/// Target is a Slash Command parameter.
/// </summary>
Parameter,
/// <summary>
/// Target is a Slash Command parameter choice.
/// </summary>
Choice
}
}

+ 2
- 21
src/Discord.Net.Interactions/Map/CommandMap.cs View File

@@ -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;
}
}
}

+ 71
- 17
src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs View File

@@ -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,
NameLocalizations = localizationManager?.GetAllNames(parameterPath, LocalizationTarget.Parameter) ?? ImmutableDictionary<string, string>.Empty,
DescriptionLocalizations = localizationManager?.GetAllDescriptions(parameterPath, LocalizationTarget.Parameter) ?? ImmutableDictionary<string, string>.Empty,
MinLength = parameterInfo.MinLength,
MaxLength = parameterInfo.MaxLength,
};
@@ -38,13 +45,19 @@ namespace Discord.Interactions

public static SlashCommandProperties ToApplicationCommandProps(this SlashCommandInfo commandInfo)
{
var commandPath = commandInfo.GetCommandPath();
var localizationManager = commandInfo.Module.CommandService.LocalizationManager;

var props = new SlashCommandBuilder()
{
Name = commandInfo.Name,
Description = commandInfo.Description,
IsDefaultPermission = commandInfo.DefaultPermission,
IsDMEnabled = commandInfo.IsEnabledInDm,
DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(),
}.Build();
}.WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary<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");
@@ -54,18 +67,30 @@ namespace Discord.Interactions
return props;
}

public static ApplicationCommandOptionProperties ToApplicationCommandOptionProps(this SlashCommandInfo commandInfo) =>
new ApplicationCommandOptionProperties
public static ApplicationCommandOptionProperties ToApplicationCommandOptionProps(this SlashCommandInfo commandInfo)
{
var localizationManager = commandInfo.Module.CommandService.LocalizationManager;
var commandPath = commandInfo.GetCommandPath();

return new ApplicationCommandOptionProperties
{
Name = commandInfo.Name,
Description = commandInfo.Description,
Type = ApplicationCommandOptionType.SubCommand,
IsRequired = false,
Options = commandInfo.FlattenedParameters?.Select(x => x.ToApplicationCommandOptionProps())?.ToList()
Options = commandInfo.FlattenedParameters?.Select(x => x.ToApplicationCommandOptionProps())
?.ToList(),
NameLocalizations = localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary<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
{
@@ -73,16 +98,21 @@ namespace Discord.Interactions
IsDefaultPermission = commandInfo.DefaultPermission,
DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(),
IsDMEnabled = commandInfo.IsEnabledInDm
}.Build(),
}
.WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary<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
@@ -123,6 +153,9 @@ namespace Discord.Interactions

options.AddRange(moduleInfo.SubModules?.SelectMany(x => x.ParseSubModule(args, ignoreDontRegister)));

var localizationManager = moduleInfo.CommandService.LocalizationManager;
var modulePath = moduleInfo.GetModulePath();

var props = new SlashCommandBuilder
{
Name = moduleInfo.SlashGroupName,
@@ -130,7 +163,10 @@ namespace Discord.Interactions
IsDefaultPermission = moduleInfo.DefaultPermission,
IsDMEnabled = moduleInfo.IsEnabledInDm,
DefaultMemberPermissions = moduleInfo.DefaultMemberPermissions
}.Build();
}
.WithNameLocalizations(localizationManager?.GetAllNames(modulePath, LocalizationTarget.Group) ?? ImmutableDictionary<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");
@@ -168,7 +204,11 @@ namespace Discord.Interactions
Name = moduleInfo.SlashGroupName,
Description = moduleInfo.Description,
Type = ApplicationCommandOptionType.SubCommandGroup,
Options = options
Options = options,
NameLocalizations = moduleInfo.CommandService.LocalizationManager?.GetAllNames(moduleInfo.GetModulePath(), LocalizationTarget.Group)
?? ImmutableDictionary<string, string>.Empty,
DescriptionLocalizations = moduleInfo.CommandService.LocalizationManager?.GetAllDescriptions(moduleInfo.GetModulePath(), LocalizationTarget.Group)
?? ImmutableDictionary<string, string>.Empty,
} };
}

@@ -183,17 +223,29 @@ namespace Discord.Interactions
Name = command.Name,
Description = command.Description,
IsDefaultPermission = command.IsDefaultPermission,
Options = command.Options?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional<List<ApplicationCommandOptionProperties>>.Unspecified
DefaultMemberPermissions = (GuildPermission)command.DefaultMemberPermissions.RawValue,
IsDMEnabled = command.IsEnabledInDm,
Options = command.Options?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional<List<ApplicationCommandOptionProperties>>.Unspecified,
NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty,
DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty,
},
ApplicationCommandType.User => new UserCommandProperties
{
Name = command.Name,
IsDefaultPermission = command.IsDefaultPermission
IsDefaultPermission = command.IsDefaultPermission,
DefaultMemberPermissions = (GuildPermission)command.DefaultMemberPermissions.RawValue,
IsDMEnabled = command.IsEnabledInDm,
NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty,
DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty
},
ApplicationCommandType.Message => new MessageCommandProperties
{
Name = command.Name,
IsDefaultPermission = command.IsDefaultPermission
IsDefaultPermission = command.IsDefaultPermission,
DefaultMemberPermissions = (GuildPermission)command.DefaultMemberPermissions.RawValue,
IsDMEnabled = command.IsEnabledInDm,
NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty,
DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty
},
_ => throw new InvalidOperationException($"Cannot create command properties for command type {command.Type}"),
};
@@ -206,18 +258,20 @@ namespace Discord.Interactions
Description = commandOption.Description,
Type = commandOption.Type,
IsRequired = commandOption.IsRequired,
ChannelTypes = commandOption.ChannelTypes?.ToList(),
IsAutocomplete = commandOption.IsAutocomplete.GetValueOrDefault(),
MinValue = commandOption.MinValue,
MaxValue = commandOption.MaxValue,
Choices = commandOption.Choices?.Select(x => new ApplicationCommandOptionChoiceProperties
{
Name = x.Name,
Value = x.Value
}).ToList(),
Options = commandOption.Options?.Select(x => x.ToApplicationCommandOptionProps()).ToList(),
NameLocalizations = commandOption.NameLocalizations?.ToImmutableDictionary(),
DescriptionLocalizations = commandOption.DescriptionLocalizations?.ToImmutableDictionary(),
MaxLength = commandOption.MaxLength,
MinLength = commandOption.MinLength,
MaxValue = commandOption.MaxValue,
MinValue = commandOption.MinValue,
IsAutocomplete = commandOption.IsAutocomplete.GetValueOrDefault(),
ChannelTypes = commandOption.ChannelTypes.ToList(),
};

public static Modal ToModal(this ModalInfo modalInfo, string customId, Action<ModalBuilder> modifyModal = null)


+ 53
- 0
src/Discord.Net.Interactions/Utilities/CommandHierarchy.cs View File

@@ -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 List<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 List<string> { EscapeChar + type.FullName };
}
}

+ 13
- 0
src/Discord.Net.Rest/API/Common/ApplicationCommand.cs View File

@@ -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; }


+ 21
- 0
src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs View File

@@ -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; }

[JsonProperty("min_length")]
public Optional<int> MinLength { get; set; }

@@ -69,6 +82,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)
{
@@ -94,6 +112,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;
}
}
}

+ 7
- 0
src/Discord.Net.Rest/API/Common/ApplicationCommandOptionChoice.cs View File

@@ -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; }
}
}

+ 14
- 1
src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs View File

@@ -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;
}
}
}

+ 7
- 0
src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs View File

@@ -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; }
}
}

+ 1
- 1
src/Discord.Net.Rest/BaseDiscordClient.cs View File

@@ -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, string locale, RequestOptions options)
=> Task.FromResult<IReadOnlyCollection<IApplicationCommand>>(ImmutableArray.Create<IApplicationCommand>());
Task<IApplicationCommand> IDiscordClient.CreateGlobalApplicationCommand(ApplicationCommandProperties properties, RequestOptions options)
=> Task.FromResult<IApplicationCommand>(null);


+ 6
- 6
src/Discord.Net.Rest/ClientHelper.cs View File

@@ -194,10 +194,10 @@ namespace Discord.Rest
};
}

public static async Task<IReadOnlyCollection<RestGlobalCommand>> GetGlobalApplicationCommandsAsync(BaseDiscordClient client,
RequestOptions options = null)
public static async Task<IReadOnlyCollection<RestGlobalCommand>> GetGlobalApplicationCommandsAsync(BaseDiscordClient client, bool withLocalizations = false,
string locale = null, RequestOptions options = null)
{
var response = await client.ApiClient.GetGlobalApplicationCommandsAsync(options).ConfigureAwait(false);
var response = await client.ApiClient.GetGlobalApplicationCommandsAsync(withLocalizations, locale, options).ConfigureAwait(false);

if (!response.Any())
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,
RequestOptions options = null)
public static async Task<IReadOnlyCollection<RestGuildCommand>> GetGuildApplicationCommandsAsync(BaseDiscordClient client, ulong guildId, bool withLocalizations = false,
string locale = null, RequestOptions options = null)
{
var response = await client.ApiClient.GetGuildApplicationCommandsAsync(guildId, options).ConfigureAwait(false);
var response = await client.ApiClient.GetGuildApplicationCommandsAsync(guildId, withLocalizations, locale, options).ConfigureAwait(false);

if (!response.Any())
return ImmutableArray.Create<RestGuildCommand>();


+ 28
- 5
src/Discord.Net.Rest/DiscordRestApiClient.cs View File

@@ -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;
@@ -861,7 +862,7 @@ namespace Discord.API
options = RequestOptions.CreateOrClone(options);

var ids = new BucketIds(webhookId: webhookId);
await SendJsonAsync<Message>("PATCH", () => $"webhooks/{webhookId}/{AuthToken}/messages/{messageId}${WebhookQuery(false, threadId)}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false);
await SendJsonAsync<Message>("PATCH", () => $"webhooks/{webhookId}/{AuthToken}/messages/{messageId}?{WebhookQuery(false, threadId)}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false);
}

/// <exception cref="InvalidOperationException">This operation may only be called with a <see cref="TokenType.Webhook"/> token.</exception>
@@ -1212,11 +1213,22 @@ namespace Discord.API
#endregion

#region Interactions
public async Task<ApplicationCommand[]> GetGlobalApplicationCommandsAsync(RequestOptions options = null)
public async Task<ApplicationCommand[]> GetGlobalApplicationCommandsAsync(bool withLocalizations = false, string locale = null, RequestOptions options = null)
{
options = RequestOptions.CreateOrClone(options);

return await SendAsync<ApplicationCommand[]>("GET", () => $"applications/{CurrentApplicationId}/commands", new BucketIds(), options: options).ConfigureAwait(false);
if (locale is not null)
{
if (!System.Text.RegularExpressions.Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"{locale} is not a valid locale.", nameof(locale));

options.RequestHeaders["X-Discord-Locale"] = new[] { locale };
}

//with_localizations=false doesnt return localized names and descriptions
var query = withLocalizations ? "?with_localizations=true" : string.Empty;
return await SendAsync<ApplicationCommand[]>("GET", () => $"applications/{CurrentApplicationId}/commands{query}",
new BucketIds(), options: options).ConfigureAwait(false);
}

public async Task<ApplicationCommand> GetGlobalApplicationCommandAsync(ulong id, RequestOptions options = null)
@@ -1281,13 +1293,24 @@ 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, string locale = null, 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);
if (locale is not null)
{
if (!System.Text.RegularExpressions.Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"{locale} is not a valid locale.", nameof(locale));

options.RequestHeaders["X-Discord-Locale"] = new[] { locale };
}

//with_localizations=false doesnt return localized names and descriptions
var query = withLocalizations ? "?with_localizations=true" : string.Empty;
return await SendAsync<ApplicationCommand[]>("GET", () => $"applications/{CurrentApplicationId}/guilds/{guildId}/commands{query}",
bucket, options: options).ConfigureAwait(false);
}

public async Task<ApplicationCommand> GetGuildApplicationCommandAsync(ulong guildId, ulong commandId, RequestOptions options = null)


+ 7
- 7
src/Discord.Net.Rest/DiscordRestClient.cs View File

@@ -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, string locale = null, RequestOptions options = null)
=> ClientHelper.GetGlobalApplicationCommandsAsync(this, withLocalizations, locale, options);
public Task<IReadOnlyCollection<RestGuildCommand>> GetGuildApplicationCommands(ulong guildId, bool withLocalizations = false, string locale = null, RequestOptions options = null)
=> ClientHelper.GetGuildApplicationCommandsAsync(this, guildId, withLocalizations, locale, options);
public Task<IReadOnlyCollection<RestGlobalCommand>> BulkOverwriteGlobalCommands(ApplicationCommandProperties[] commandProperties, RequestOptions options = null)
=> 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, string locale, RequestOptions options)
=> await GetGlobalApplicationCommands(withLocalizations, locale, options).ConfigureAwait(false);
/// <inheritdoc />
async Task<IApplicationCommand> IDiscordClient.GetGlobalApplicationCommandAsync(ulong id, RequestOptions options)
=> await ClientHelper.GetGlobalApplicationCommandAsync(this, id, options).ConfigureAwait(false);


+ 3
- 3
src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs View File

@@ -362,10 +362,10 @@ namespace Discord.Rest
#endregion

#region Interactions
public static async Task<IReadOnlyCollection<RestGuildCommand>> GetSlashCommandsAsync(IGuild guild, BaseDiscordClient client,
RequestOptions options)
public static async Task<IReadOnlyCollection<RestGuildCommand>> GetSlashCommandsAsync(IGuild guild, BaseDiscordClient client, bool withLocalizations,
string locale, RequestOptions options)
{
var models = await client.ApiClient.GetGuildApplicationCommandsAsync(guild.Id, options);
var models = await client.ApiClient.GetGuildApplicationCommandsAsync(guild.Id, withLocalizations, locale, options);
return models.Select(x => RestGuildCommand.Create(client, x, guild.Id)).ToImmutableArray();
}
public static async Task<RestGuildCommand> GetSlashCommandAsync(IGuild guild, ulong id, BaseDiscordClient client,


+ 10
- 6
src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs View File

@@ -311,13 +311,15 @@ namespace Discord.Rest
/// <summary>
/// Gets a collection of slash commands created by the current user in this guild.
/// </summary>
/// <param name="withLocalizations">Whether to include full localization dictionaries in the returned objects, instead of the name localized and description localized fields.</param>
/// <param name="locale">The target locale of the localized name and description fields. Sets <c>X-Discord-Locale</c> header, which takes precedence over <c>Accept-Language</c>.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// 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, string locale = null, RequestOptions options = null)
=> GuildHelper.GetSlashCommandsAsync(this, Discord, withLocalizations, locale, options);

/// <summary>
/// Gets a slash command in the current guild.
@@ -928,13 +930,15 @@ namespace Discord.Rest
/// <summary>
/// Gets this guilds slash 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="locale">The target locale of the localized name and description fields. Sets <c>X-Discord-Locale</c> header, which takes precedence over <c>Accept-Language</c>.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <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>
public async Task<IReadOnlyCollection<RestGuildCommand>> GetApplicationCommandsAsync (RequestOptions options = null)
=> await ClientHelper.GetGuildApplicationCommandsAsync(Discord, Id, options).ConfigureAwait(false);
public async Task<IReadOnlyCollection<RestGuildCommand>> GetApplicationCommandsAsync (bool withLocalizations = false, string locale = null, RequestOptions options = null)
=> await ClientHelper.GetGuildApplicationCommandsAsync(Discord, Id, withLocalizations, locale, options).ConfigureAwait(false);
/// <summary>
/// Gets an application command within this guild with the specified id.
/// </summary>
@@ -1467,8 +1471,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, string locale, RequestOptions options)
=> await GetApplicationCommandsAsync(withLocalizations, locale, 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);


+ 15
- 3
src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs View File

@@ -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,6 +142,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(),
@@ -181,6 +185,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 +250,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 +307,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 +345,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)


+ 35
- 0
src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs View File

@@ -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));
}


+ 17
- 0
src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandChoice.cs View File

@@ -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);
}
}
}

+ 36
- 1
src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs View File

@@ -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; }
@@ -54,6 +54,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)
@@ -98,6 +124,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



+ 16
- 4
src/Discord.Net.Rest/Net/DefaultRestClient.cs View File

@@ -66,33 +66,45 @@ namespace Discord.Net.Rest
_cancelToken = cancelToken;
}

public async Task<RestResponse> SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly, string reason = null)
public async Task<RestResponse> SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly, string reason = null,
IEnumerable<KeyValuePair<string, IEnumerable<string>>> requestHeaders = null)
{
string uri = Path.Combine(_baseUrl, endpoint);
using (var restRequest = new HttpRequestMessage(GetMethod(method), uri))
{
if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason));
if (requestHeaders != null)
foreach (var header in requestHeaders)
restRequest.Headers.Add(header.Key, header.Value);
return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false);
}
}
public async Task<RestResponse> SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly, string reason = null)
public async Task<RestResponse> SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly, string reason = null,
IEnumerable<KeyValuePair<string, IEnumerable<string>>> requestHeaders = null)
{
string uri = Path.Combine(_baseUrl, endpoint);
using (var restRequest = new HttpRequestMessage(GetMethod(method), uri))
{
if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason));
if (requestHeaders != null)
foreach (var header in requestHeaders)
restRequest.Headers.Add(header.Key, header.Value);
restRequest.Content = new StringContent(json, Encoding.UTF8, "application/json");
return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false);
}
}

/// <exception cref="InvalidOperationException">Unsupported param type.</exception>
public async Task<RestResponse> SendAsync(string method, string endpoint, IReadOnlyDictionary<string, object> multipartParams, CancellationToken cancelToken, bool headerOnly, string reason = null)
public async Task<RestResponse> SendAsync(string method, string endpoint, IReadOnlyDictionary<string, object> multipartParams, CancellationToken cancelToken, bool headerOnly, string reason = null,
IEnumerable<KeyValuePair<string, IEnumerable<string>>> requestHeaders = null)
{
string uri = Path.Combine(_baseUrl, endpoint);
using (var restRequest = new HttpRequestMessage(GetMethod(method), uri))
{
if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason));
if (requestHeaders != null)
foreach (var header in requestHeaders)
restRequest.Headers.Add(header.Key, header.Value);
var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture));
MemoryStream memoryStream = null;
if (multipartParams != null)
@@ -126,7 +138,7 @@ namespace Discord.Net.Rest

content.Add(streamContent, p.Key, fileValue.Filename);
#pragma warning restore IDISP004
continue;
}
default:


+ 4
- 1
src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs View File

@@ -1,5 +1,8 @@
using Discord.Net.Rest;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Threading.Tasks;

@@ -28,7 +31,7 @@ namespace Discord.Net.Queue

public virtual async Task<RestResponse> SendAsync()
{
return await Client.SendAsync(Method, Endpoint, Options.CancelToken, Options.HeaderOnly, Options.AuditLogReason).ConfigureAwait(false);
return await Client.SendAsync(Method, Endpoint, Options.CancelToken, Options.HeaderOnly, Options.AuditLogReason, Options.RequestHeaders).ConfigureAwait(false);
}
}
}

+ 6
- 4
src/Discord.Net.WebSocket/DiscordSocketClient.cs View File

@@ -450,14 +450,16 @@ namespace Discord.WebSocket
/// <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="locale">The target locale of the localized name and description fields. Sets <c>X-Discord-Locale</c> header, which takes precedence over <c>Accept-Language</c>.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// 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, string locale = null, RequestOptions options = null)
{
var commands = (await ApiClient.GetGlobalApplicationCommandsAsync(options)).Select(x => SocketApplicationCommand.Create(this, x));
var commands = (await ApiClient.GetGlobalApplicationCommandsAsync(withLocalizations, locale, options)).Select(x => SocketApplicationCommand.Create(this, x));

foreach(var command in commands)
{
@@ -3236,8 +3238,8 @@ namespace Discord.WebSocket
async Task<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, string locale, RequestOptions options)
=> await GetGlobalApplicationCommandsAsync(withLocalizations, locale, options);

/// <inheritdoc />
async Task IDiscordClient.StartAsync()


+ 7
- 4
src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs View File

@@ -874,14 +874,17 @@ namespace Discord.WebSocket
/// <summary>
/// Gets a collection of slash commands created by the current user in this guild.
/// </summary>
/// <param name="withLocalizations">Whether to include full localization dictionaries in the returned objects, instead of the name localized and description localized fields.</param>
/// <param name="locale">The target locale of the localized name and description fields. Sets <c>X-Discord-Locale</c> header, which takes precedence over <c>Accept-Language</c>.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// 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, string locale = null, RequestOptions options = null)
{
var commands = (await Discord.ApiClient.GetGuildApplicationCommandsAsync(Id, options)).Select(x => SocketApplicationCommand.Create(Discord, x, Id));
var commands = (await Discord.ApiClient.GetGuildApplicationCommandsAsync(Id, withLocalizations, locale, options))
.Select(x => SocketApplicationCommand.Create(Discord, x, Id));

foreach (var command in commands)
{
@@ -1977,8 +1980,8 @@ namespace Discord.WebSocket
async Task<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, string locale, RequestOptions options)
=> await GetApplicationCommandsAsync(withLocalizations, locale, 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);


+ 35
- 0
src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs View File

@@ -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));
}


+ 17
- 0
src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandChoice.cs View File

@@ -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);
}
}
}

+ 35
- 0
src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandOption.cs View File

@@ -54,6 +54,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)
{
@@ -92,6 +118,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;


+ 31
- 31
src/Discord.Net/Discord.Net.nuspec View File

@@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Discord.Net</id>
<version>3.7.2$suffix$</version>
<version>3.8.0$suffix$</version>
<title>Discord.Net</title>
<authors>Discord.Net Contributors</authors>
<owners>foxbot</owners>
@@ -14,44 +14,44 @@
<iconUrl>https://github.com/discord-net/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png</iconUrl>
<dependencies>
<group targetFramework="net6.0">
<dependency id="Discord.Net.Core" version="3.7.2$suffix$" />
<dependency id="Discord.Net.Rest" version="3.7.2$suffix$" />
<dependency id="Discord.Net.WebSocket" version="3.7.2$suffix$" />
<dependency id="Discord.Net.Commands" version="3.7.2$suffix$" />
<dependency id="Discord.Net.Webhook" version="3.7.2$suffix$" />
<dependency id="Discord.Net.Interactions" version="3.7.2$suffix$" />
<dependency id="Discord.Net.Core" version="3.8.0$suffix$" />
<dependency id="Discord.Net.Rest" version="3.8.0$suffix$" />
<dependency id="Discord.Net.WebSocket" version="3.8.0$suffix$" />
<dependency id="Discord.Net.Commands" version="3.8.0$suffix$" />
<dependency id="Discord.Net.Webhook" version="3.8.0$suffix$" />
<dependency id="Discord.Net.Interactions" version="3.8.0$suffix$" />
</group>
<group targetFramework="net5.0">
<dependency id="Discord.Net.Core" version="3.7.2$suffix$" />
<dependency id="Discord.Net.Rest" version="3.7.2$suffix$" />
<dependency id="Discord.Net.WebSocket" version="3.7.2$suffix$" />
<dependency id="Discord.Net.Commands" version="3.7.2$suffix$" />
<dependency id="Discord.Net.Webhook" version="3.7.2$suffix$" />
<dependency id="Discord.Net.Interactions" version="3.7.2$suffix$" />
<dependency id="Discord.Net.Core" version="3.8.0$suffix$" />
<dependency id="Discord.Net.Rest" version="3.8.0$suffix$" />
<dependency id="Discord.Net.WebSocket" version="3.8.0$suffix$" />
<dependency id="Discord.Net.Commands" version="3.8.0$suffix$" />
<dependency id="Discord.Net.Webhook" version="3.8.0$suffix$" />
<dependency id="Discord.Net.Interactions" version="3.8.0$suffix$" />
</group>
<group targetFramework="net461">
<dependency id="Discord.Net.Core" version="3.7.2$suffix$" />
<dependency id="Discord.Net.Rest" version="3.7.2$suffix$" />
<dependency id="Discord.Net.WebSocket" version="3.7.2$suffix$" />
<dependency id="Discord.Net.Commands" version="3.7.2$suffix$" />
<dependency id="Discord.Net.Webhook" version="3.7.2$suffix$" />
<dependency id="Discord.Net.Interactions" version="3.7.2$suffix$" />
<dependency id="Discord.Net.Core" version="3.8.0$suffix$" />
<dependency id="Discord.Net.Rest" version="3.8.0$suffix$" />
<dependency id="Discord.Net.WebSocket" version="3.8.0$suffix$" />
<dependency id="Discord.Net.Commands" version="3.8.0$suffix$" />
<dependency id="Discord.Net.Webhook" version="3.8.0$suffix$" />
<dependency id="Discord.Net.Interactions" version="3.8.0$suffix$" />
</group>
<group targetFramework="netstandard2.0">
<dependency id="Discord.Net.Core" version="3.7.2$suffix$" />
<dependency id="Discord.Net.Rest" version="3.7.2$suffix$" />
<dependency id="Discord.Net.WebSocket" version="3.7.2$suffix$" />
<dependency id="Discord.Net.Commands" version="3.7.2$suffix$" />
<dependency id="Discord.Net.Webhook" version="3.7.2$suffix$" />
<dependency id="Discord.Net.Interactions" version="3.7.2$suffix$" />
<dependency id="Discord.Net.Core" version="3.8.0$suffix$" />
<dependency id="Discord.Net.Rest" version="3.8.0$suffix$" />
<dependency id="Discord.Net.WebSocket" version="3.8.0$suffix$" />
<dependency id="Discord.Net.Commands" version="3.8.0$suffix$" />
<dependency id="Discord.Net.Webhook" version="3.8.0$suffix$" />
<dependency id="Discord.Net.Interactions" version="3.8.0$suffix$" />
</group>
<group targetFramework="netstandard2.1">
<dependency id="Discord.Net.Core" version="3.7.2$suffix$" />
<dependency id="Discord.Net.Rest" version="3.7.2$suffix$" />
<dependency id="Discord.Net.WebSocket" version="3.7.2$suffix$" />
<dependency id="Discord.Net.Commands" version="3.7.2$suffix$" />
<dependency id="Discord.Net.Webhook" version="3.7.2$suffix$" />
<dependency id="Discord.Net.Interactions" version="3.7.2$suffix$" />
<dependency id="Discord.Net.Core" version="3.8.0$suffix$" />
<dependency id="Discord.Net.Rest" version="3.8.0$suffix$" />
<dependency id="Discord.Net.WebSocket" version="3.8.0$suffix$" />
<dependency id="Discord.Net.Commands" version="3.8.0$suffix$" />
<dependency id="Discord.Net.Webhook" version="3.8.0$suffix$" />
<dependency id="Discord.Net.Interactions" version="3.8.0$suffix$" />
</group>
</dependencies>
</metadata>


Loading…
Cancel
Save