Browse Source

Merge branch 'dev' into resume-gateway-url

pull/2423/head
Quin Lynch GitHub 2 years ago
parent
commit
42b785dc05
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 1966 additions and 226 deletions
  1. +38
    -0
      CHANGELOG.md
  2. +1
    -1
      Discord.Net.targets
  3. +1
    -1
      docs/docfx.json
  4. +1
    -1
      docs/guides/dependency_injection/injection.md
  5. +47
    -2
      docs/guides/int_framework/intro.md
  6. +3
    -1
      src/Discord.Net.Core/Discord.Net.Core.csproj
  7. +1
    -1
      src/Discord.Net.Core/DiscordConfig.cs
  8. +24
    -0
      src/Discord.Net.Core/DiscordErrorCode.cs
  9. +6
    -1
      src/Discord.Net.Core/Entities/Guilds/IGuild.cs
  10. +75
    -17
      src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs
  11. +33
    -0
      src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs
  12. +52
    -0
      src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs
  13. +70
    -0
      src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs
  14. +71
    -1
      src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs
  15. +26
    -0
      src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs
  16. +26
    -0
      src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs
  17. +15
    -0
      src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs
  18. +7
    -3
      src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs
  19. +291
    -40
      src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs
  20. +39
    -0
      src/Discord.Net.Core/Entities/Messages/Embed.cs
  21. +31
    -0
      src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs
  22. +141
    -0
      src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs
  23. +31
    -0
      src/Discord.Net.Core/Entities/Messages/EmbedField.cs
  24. +31
    -0
      src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs
  25. +31
    -0
      src/Discord.Net.Core/Entities/Messages/EmbedImage.cs
  26. +31
    -0
      src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs
  27. +31
    -0
      src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs
  28. +31
    -0
      src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs
  29. +6
    -0
      src/Discord.Net.Core/Entities/Messages/IMessage.cs
  30. +15
    -0
      src/Discord.Net.Core/Extensions/GenericCollectionExtensions.cs
  31. +9
    -2
      src/Discord.Net.Core/GatewayIntents.cs
  32. +3
    -1
      src/Discord.Net.Core/IDiscordClient.cs
  33. +7
    -3
      src/Discord.Net.Core/Net/Rest/IRestClient.cs
  34. +8
    -4
      src/Discord.Net.Core/RequestOptions.cs
  35. +5
    -5
      src/Discord.Net.Core/Utils/Preconditions.cs
  36. +3
    -3
      src/Discord.Net.Interactions/Attributes/MaxLengthAttribute.cs
  37. +3
    -3
      src/Discord.Net.Interactions/Attributes/MinLengthAttribute.cs
  38. +6
    -6
      src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs
  39. +6
    -0
      src/Discord.Net.Interactions/InteractionService.cs
  40. +5
    -0
      src/Discord.Net.Interactions/InteractionServiceConfig.cs
  41. +32
    -0
      src/Discord.Net.Interactions/LocalizationManagers/ILocalizationManager.cs
  42. +72
    -0
      src/Discord.Net.Interactions/LocalizationManagers/JsonLocalizationManager.cs
  43. +55
    -0
      src/Discord.Net.Interactions/LocalizationManagers/ResxLocalizationManager.cs
  44. +25
    -0
      src/Discord.Net.Interactions/LocalizationTarget.cs
  45. +2
    -21
      src/Discord.Net.Interactions/Map/CommandMap.cs
  46. +71
    -17
      src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs
  47. +53
    -0
      src/Discord.Net.Interactions/Utilities/CommandHierarchy.cs
  48. +13
    -0
      src/Discord.Net.Rest/API/Common/ApplicationCommand.cs
  49. +21
    -0
      src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs
  50. +7
    -0
      src/Discord.Net.Rest/API/Common/ApplicationCommandOptionChoice.cs
  51. +14
    -1
      src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs
  52. +13
    -5
      src/Discord.Net.Rest/API/Rest/CreateStickerParams.cs
  53. +7
    -0
      src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs
  54. +1
    -1
      src/Discord.Net.Rest/BaseDiscordClient.cs
  55. +6
    -6
      src/Discord.Net.Rest/ClientHelper.cs
  56. +28
    -5
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  57. +7
    -7
      src/Discord.Net.Rest/DiscordRestClient.cs
  58. +4
    -4
      src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs
  59. +10
    -6
      src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
  60. +15
    -3
      src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs
  61. +35
    -0
      src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs
  62. +17
    -0
      src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandChoice.cs
  63. +36
    -1
      src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs
  64. +7
    -7
      src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs
  65. +16
    -4
      src/Discord.Net.Rest/Net/DefaultRestClient.cs
  66. +4
    -1
      src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs
  67. +6
    -4
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  68. +2
    -2
      src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs
  69. +7
    -4
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
  70. +35
    -0
      src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs
  71. +17
    -0
      src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandChoice.cs
  72. +35
    -0
      src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandOption.cs
  73. +31
    -31
      src/Discord.Net/Discord.Net.nuspec
  74. +1
    -0
      test/Discord.Net.Tests.Unit/ColorTests.cs

+ 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"


+ 1
- 1
docs/guides/dependency_injection/injection.md View File

@@ -16,7 +16,7 @@ This can be done through property or constructor.
Services can be injected from the constructor of the class.
This is the preferred approach, because it automatically locks the readonly field in place with the provided service and isn't accessible outside of the class.

[!code-csharp[Property Injection(samples/property-injecting.cs)]]
[!code-csharp[Constructor Injection](samples/ctor-injecting.cs)]

## Injecting through properties



+ 47
- 2
docs/guides/int_framework/intro.md View File

@@ -294,7 +294,7 @@ By nesting commands inside a module that is tagged with [GroupAttribute] you can
> [!NOTE]
> To not use the command group's name as a prefix for component or modal interaction's custom id set `ignoreGroupNames` parameter to `true` in classes with [GroupAttribute]
>
> However, you have to be careful to prevent overlapping ids of buttons and modals
> However, you have to be careful to prevent overlapping ids of buttons and modals.

[!code-csharp[Command Group Example](samples/intro/groupmodule.cs)]

@@ -346,10 +346,13 @@ Command registration methods can only be used after the gateway client is ready
Methods like `AddModulesToGuildAsync()`, `AddCommandsToGuildAsync()`, `AddModulesGloballyAsync()` and `AddCommandsGloballyAsync()`
can be used to register cherry picked modules or commands to global/guild scopes.

> [!NOTE]
> [DontAutoRegisterAttribute] can be used on module classes to prevent `RegisterCommandsGloballyAsync()` and `RegisterCommandsToGuildAsync()` from registering them to the Discord.

> [!NOTE]
> In debug environment, since Global commands can take up to 1 hour to register/update,
> it is adviced to register your commands to a test guild for your changes to take effect immediately.
> You can use preprocessor directives to create a simple logic for registering commands as seen above
> You can use preprocessor directives to create a simple logic for registering commands as seen above.

## Interaction Utility

@@ -373,10 +376,52 @@ 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

[GroupAttribute]: xref:Discord.Interactions.GroupAttribute
[DontAutoRegisterAttribute]: xref:Discord.Interactions.DontAutoRegisterAttribute
[InteractionService]: xref:Discord.Interactions.InteractionService
[InteractionServiceConfig]: xref:Discord.Interactions.InteractionServiceConfig
[InteractionModuleBase]: xref:Discord.Interactions.InteractionModuleBase


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

@@ -16,7 +16,6 @@
<PackageReference Include="IDisposableAnalyzers" Version="3.4.15">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.1' ">
<PackageReference Include="System.Collections.Immutable" Version="1.3.1" />
@@ -27,4 +26,7 @@
<ItemGroup Condition=" '$(TargetFramework)' == 'net461' ">
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' != 'net461'">
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
</ItemGroup>
</Project>

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

+ 7
- 3
src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs View File

@@ -1198,6 +1198,10 @@ namespace Discord

public class TextInputBuilder
{
/// <summary>
/// The max length of a <see cref="TextInputComponent.Placeholder"/>.
/// </summary>
public const int MaxPlaceholderLength = 100;
public const int LargestMaxLength = 4000;

/// <summary>
@@ -1229,13 +1233,13 @@ namespace Discord
/// <summary>
/// Gets or sets the placeholder of the current text input.
/// </summary>
/// <exception cref="ArgumentException"><see cref="Placeholder"/> is longer than 100 characters</exception>
/// <exception cref="ArgumentException"><see cref="Placeholder"/> is longer than <see cref="MaxPlaceholderLength"/> characters</exception>
public string Placeholder
{
get => _placeholder;
set => _placeholder = (value?.Length ?? 0) <= 100
set => _placeholder = (value?.Length ?? 0) <= MaxPlaceholderLength
? value
: throw new ArgumentException("Placeholder cannot have more than 100 characters.");
: throw new ArgumentException($"Placeholder cannot have more than {MaxPlaceholderLength} characters.");
}

/// <summary>


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

+ 39
- 0
src/Discord.Net.Core/Entities/Messages/Embed.cs View File

@@ -94,5 +94,44 @@ namespace Discord
/// </summary>
public override string ToString() => Title;
private string DebuggerDisplay => $"{Title} ({Type})";

public static bool operator ==(Embed left, Embed right)
=> left is null ? right is null
: left.Equals(right);

public static bool operator !=(Embed left, Embed right)
=> !(left == right);

/// <summary>
/// Determines whether the specified object is equal to the current <see cref="Embed"/>.
/// </summary>
/// <remarks>
/// If the object passes is an <see cref="Embed"/>, <see cref="Equals(Embed)"/> will be called to compare the 2 instances
/// </remarks>
/// <param name="obj">The object to compare with the current <see cref="Embed"/></param>
/// <returns></returns>
public override bool Equals(object obj)
=> obj is Embed embed && Equals(embed);

/// <summary>
/// Determines whether the specified <see cref="Embed"/> is equal to the current <see cref="Embed"/>
/// </summary>
/// <param name="embed">The <see cref="Embed"/> to compare with the current <see cref="Embed"/></param>
/// <returns></returns>
public bool Equals(Embed embed)
=> GetHashCode() == embed?.GetHashCode();

/// <inheritdoc />
public override int GetHashCode()
{
unchecked
{
var hash = 17;
hash = hash * 23 + (Type, Title, Description, Timestamp, Color, Image, Video, Author, Footer, Provider, Thumbnail).GetHashCode();
foreach(var field in Fields)
hash = hash * 23 + field.GetHashCode();
return hash;
}
}
}
}

+ 31
- 0
src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs View File

@@ -1,3 +1,4 @@
using System;
using System.Diagnostics;

namespace Discord
@@ -41,5 +42,35 @@ namespace Discord
///
/// </returns>
public override string ToString() => Name;

public static bool operator ==(EmbedAuthor? left, EmbedAuthor? right)
=> left is null ? right is null
: left.Equals(right);

public static bool operator !=(EmbedAuthor? left, EmbedAuthor? right)
=> !(left == right);

/// <summary>
/// Determines whether the specified object is equal to the current <see cref="EmbedAuthor"/>.
/// </summary>
/// <remarks>
/// If the object passes is an <see cref="EmbedAuthor"/>, <see cref="Equals(EmbedAuthor?)"/> will be called to compare the 2 instances
/// </remarks>
/// <param name="obj">The object to compare with the current <see cref="EmbedAuthor"/></param>
/// <returns></returns>
public override bool Equals(object obj)
=> obj is EmbedAuthor embedAuthor && Equals(embedAuthor);

/// <summary>
/// Determines whether the specified <see cref="EmbedAuthor"/> is equal to the current <see cref="EmbedAuthor"/>
/// </summary>
/// <param name="embedAuthor">The <see cref="EmbedAuthor"/> to compare with the current <see cref="EmbedAuthor"/></param>
/// <returns></returns>
public bool Equals(EmbedAuthor? embedAuthor)
=> GetHashCode() == embedAuthor?.GetHashCode();

/// <inheritdoc />
public override int GetHashCode()
=> (Name, Url, IconUrl).GetHashCode();
}
}

+ 141
- 0
src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs View File

@@ -481,6 +481,55 @@ namespace Discord

return new Embed(EmbedType.Rich, Title, Description, Url, Timestamp, Color, _image, null, Author?.Build(), Footer?.Build(), null, _thumbnail, fields.ToImmutable());
}

public static bool operator ==(EmbedBuilder left, EmbedBuilder right)
=> left is null ? right is null
: left.Equals(right);

public static bool operator !=(EmbedBuilder left, EmbedBuilder right)
=> !(left == right);

/// <summary>
/// Determines whether the specified object is equal to the current <see cref="EmbedBuilder"/>.
/// </summary>
/// <remarks>
/// If the object passes is an <see cref="EmbedBuilder"/>, <see cref="Equals(EmbedBuilder)"/> will be called to compare the 2 instances
/// </remarks>
/// <param name="obj">The object to compare with the current <see cref="EmbedBuilder"/></param>
/// <returns></returns>
public override bool Equals(object obj)
=> obj is EmbedBuilder embedBuilder && Equals(embedBuilder);

/// <summary>
/// Determines whether the specified <see cref="EmbedBuilder"/> is equal to the current <see cref="EmbedBuilder"/>
/// </summary>
/// <param name="embedBuilder">The <see cref="EmbedBuilder"/> to compare with the current <see cref="EmbedBuilder"/></param>
/// <returns></returns>
public bool Equals(EmbedBuilder embedBuilder)
{
if (embedBuilder is null)
return false;

if (Fields.Count != embedBuilder.Fields.Count)
return false;

for (var i = 0; i < _fields.Count; i++)
if (_fields[i] != embedBuilder._fields[i])
return false;

return _title == embedBuilder?._title
&& _description == embedBuilder?._description
&& _image == embedBuilder?._image
&& _thumbnail == embedBuilder?._thumbnail
&& Timestamp == embedBuilder?.Timestamp
&& Color == embedBuilder?.Color
&& Author == embedBuilder?.Author
&& Footer == embedBuilder?.Footer
&& Url == embedBuilder?.Url;
}

/// <inheritdoc />
public override int GetHashCode() => base.GetHashCode();
}

/// <summary>
@@ -597,6 +646,37 @@ namespace Discord
/// </exception>
public EmbedField Build()
=> new EmbedField(Name, Value.ToString(), IsInline);

public static bool operator ==(EmbedFieldBuilder left, EmbedFieldBuilder right)
=> left is null ? right is null
: left.Equals(right);

public static bool operator !=(EmbedFieldBuilder left, EmbedFieldBuilder right)
=> !(left == right);

/// <summary>
/// Determines whether the specified object is equal to the current <see cref="EmbedFieldBuilder"/>.
/// </summary>
/// <remarks>
/// If the object passes is an <see cref="EmbedFieldBuilder"/>, <see cref="Equals(EmbedFieldBuilder)"/> will be called to compare the 2 instances
/// </remarks>
/// <param name="obj">The object to compare with the current <see cref="EmbedFieldBuilder"/></param>
/// <returns></returns>
public override bool Equals(object obj)
=> obj is EmbedFieldBuilder embedFieldBuilder && Equals(embedFieldBuilder);

/// <summary>
/// Determines whether the specified <see cref="EmbedFieldBuilder"/> is equal to the current <see cref="EmbedFieldBuilder"/>
/// </summary>
/// <param name="embedFieldBuilder">The <see cref="EmbedFieldBuilder"/> to compare with the current <see cref="EmbedFieldBuilder"/></param>
/// <returns></returns>
public bool Equals(EmbedFieldBuilder embedFieldBuilder)
=> _name == embedFieldBuilder?._name
&& _value == embedFieldBuilder?._value
&& IsInline == embedFieldBuilder?.IsInline;

/// <inheritdoc />
public override int GetHashCode() => base.GetHashCode();
}

/// <summary>
@@ -697,6 +777,37 @@ namespace Discord
/// </returns>
public EmbedAuthor Build()
=> new EmbedAuthor(Name, Url, IconUrl, null);

public static bool operator ==(EmbedAuthorBuilder left, EmbedAuthorBuilder right)
=> left is null ? right is null
: left.Equals(right);

public static bool operator !=(EmbedAuthorBuilder left, EmbedAuthorBuilder right)
=> !(left == right);

/// <summary>
/// Determines whether the specified object is equal to the current <see cref="EmbedAuthorBuilder"/>.
/// </summary>
/// <remarks>
/// If the object passes is an <see cref="EmbedAuthorBuilder"/>, <see cref="Equals(EmbedAuthorBuilder)"/> will be called to compare the 2 instances
/// </remarks>
/// <param name="obj">The object to compare with the current <see cref="EmbedAuthorBuilder"/></param>
/// <returns></returns>
public override bool Equals(object obj)
=> obj is EmbedAuthorBuilder embedAuthorBuilder && Equals(embedAuthorBuilder);

/// <summary>
/// Determines whether the specified <see cref="EmbedAuthorBuilder"/> is equals to the current <see cref="EmbedAuthorBuilder"/>
/// </summary>
/// <param name="embedAuthorBuilder">The <see cref="EmbedAuthorBuilder"/> to compare with the current <see cref="EmbedAuthorBuilder"/></param>
/// <returns></returns>
public bool Equals(EmbedAuthorBuilder embedAuthorBuilder)
=> _name == embedAuthorBuilder?._name
&& Url == embedAuthorBuilder?.Url
&& IconUrl == embedAuthorBuilder?.IconUrl;

/// <inheritdoc />
public override int GetHashCode() => base.GetHashCode();
}

/// <summary>
@@ -777,5 +888,35 @@ namespace Discord
/// </returns>
public EmbedFooter Build()
=> new EmbedFooter(Text, IconUrl, null);

public static bool operator ==(EmbedFooterBuilder left, EmbedFooterBuilder right)
=> left is null ? right is null
: left.Equals(right);

public static bool operator !=(EmbedFooterBuilder left, EmbedFooterBuilder right)
=> !(left == right);

/// <summary>
/// Determines whether the specified object is equal to the current <see cref="EmbedFooterBuilder"/>.
/// </summary>
/// <remarks>
/// If the object passes is an <see cref="EmbedFooterBuilder"/>, <see cref="Equals(EmbedFooterBuilder)"/> will be called to compare the 2 instances
/// </remarks>
/// <param name="obj">The object to compare with the current <see cref="EmbedFooterBuilder"/></param>
/// <returns></returns>
public override bool Equals(object obj)
=> obj is EmbedFooterBuilder embedFooterBuilder && Equals(embedFooterBuilder);

/// <summary>
/// Determines whether the specified <see cref="EmbedFooterBuilder"/> is equal to the current <see cref="EmbedFooterBuilder"/>
/// </summary>
/// <param name="embedFooterBuilder">The <see cref="EmbedFooterBuilder"/> to compare with the current <see cref="EmbedFooterBuilder"/></param>
/// <returns></returns>
public bool Equals(EmbedFooterBuilder embedFooterBuilder)
=> _text == embedFooterBuilder?._text
&& IconUrl == embedFooterBuilder?.IconUrl;

/// <inheritdoc />
public override int GetHashCode() => base.GetHashCode();
}
}

+ 31
- 0
src/Discord.Net.Core/Entities/Messages/EmbedField.cs View File

@@ -1,3 +1,4 @@
using System;
using System.Diagnostics;

namespace Discord
@@ -36,5 +37,35 @@ namespace Discord
/// A string that resolves to <see cref="EmbedField.Name"/>.
/// </returns>
public override string ToString() => Name;

public static bool operator ==(EmbedField? left, EmbedField? right)
=> left is null ? right is null
: left.Equals(right);

public static bool operator !=(EmbedField? left, EmbedField? right)
=> !(left == right);

/// <summary>
/// Determines whether the specified object is equal to the current <see cref="EmbedField"/>.
/// </summary>
/// <remarks>
/// If the object passes is an <see cref="EmbedField"/>, <see cref="Equals(EmbedField?)"/> will be called to compare the 2 instances
/// </remarks>
/// <param name="obj">The object to compare with the current object</param>
/// <returns></returns>
public override bool Equals(object obj)
=> obj is EmbedField embedField && Equals(embedField);

/// <summary>
/// Determines whether the specified <see cref="EmbedField"/> is equal to the current <see cref="EmbedField"/>
/// </summary>
/// <param name="embedField"></param>
/// <returns></returns>
public bool Equals(EmbedField? embedField)
=> GetHashCode() == embedField?.GetHashCode();

/// <inheritdoc />
public override int GetHashCode()
=> (Name, Value, Inline).GetHashCode();
}
}

+ 31
- 0
src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs View File

@@ -1,3 +1,4 @@
using System;
using System.Diagnostics;

namespace Discord
@@ -43,5 +44,35 @@ namespace Discord
/// A string that resolves to <see cref="Discord.EmbedFooter.Text"/>.
/// </returns>
public override string ToString() => Text;

public static bool operator ==(EmbedFooter? left, EmbedFooter? right)
=> left is null ? right is null
: left.Equals(right);

public static bool operator !=(EmbedFooter? left, EmbedFooter? right)
=> !(left == right);

/// <summary>
/// Determines whether the specified object is equal to the current <see cref="EmbedFooter"/>.
/// </summary>
/// <remarks>
/// If the object passes is an <see cref="EmbedFooter"/>, <see cref="Equals(EmbedFooter?)"/> will be called to compare the 2 instances
/// </remarks>
/// <param name="obj">The object to compare with the current <see cref="EmbedFooter"/></param>
/// <returns></returns>
public override bool Equals(object obj)
=> obj is EmbedFooter embedFooter && Equals(embedFooter);

/// <summary>
/// Determines whether the specified <see cref="EmbedFooter"/> is equal to the current <see cref="EmbedFooter"/>
/// </summary>
/// <param name="embedFooter">The <see cref="EmbedFooter"/> to compare with the current <see cref="EmbedFooter"/></param>
/// <returns></returns>
public bool Equals(EmbedFooter? embedFooter)
=> GetHashCode() == embedFooter?.GetHashCode();

/// <inheritdoc />
public override int GetHashCode()
=> (Text, IconUrl, ProxyUrl).GetHashCode();
}
}

+ 31
- 0
src/Discord.Net.Core/Entities/Messages/EmbedImage.cs View File

@@ -1,3 +1,4 @@
using System;
using System.Diagnostics;

namespace Discord
@@ -53,5 +54,35 @@ namespace Discord
/// A string that resolves to <see cref="Discord.EmbedImage.Url"/> .
/// </returns>
public override string ToString() => Url;

public static bool operator ==(EmbedImage? left, EmbedImage? right)
=> left is null ? right is null
: left.Equals(right);

public static bool operator !=(EmbedImage? left, EmbedImage? right)
=> !(left == right);

/// <summary>
/// Determines whether the specified object is equal to the current <see cref="EmbedImage"/>.
/// </summary>
/// <remarks>
/// If the object passes is an <see cref="EmbedImage"/>, <see cref="Equals(EmbedImage?)"/> will be called to compare the 2 instances
/// </remarks>
/// <param name="obj">The object to compare with the current <see cref="EmbedImage"/></param>
/// <returns></returns>
public override bool Equals(object obj)
=> obj is EmbedImage embedImage && Equals(embedImage);

/// <summary>
/// Determines whether the specified <see cref="EmbedImage"/> is equal to the current <see cref="EmbedImage"/>
/// </summary>
/// <param name="embedImage">The <see cref="EmbedImage"/> to compare with the current <see cref="EmbedImage"/></param>
/// <returns></returns>
public bool Equals(EmbedImage? embedImage)
=> GetHashCode() == embedImage?.GetHashCode();

/// <inheritdoc />
public override int GetHashCode()
=> (Height, Width, Url, ProxyUrl).GetHashCode();
}
}

+ 31
- 0
src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs View File

@@ -1,3 +1,4 @@
using System;
using System.Diagnostics;

namespace Discord
@@ -35,5 +36,35 @@ namespace Discord
/// A string that resolves to <see cref="Discord.EmbedProvider.Name" />.
/// </returns>
public override string ToString() => Name;

public static bool operator ==(EmbedProvider? left, EmbedProvider? right)
=> left is null ? right is null
: left.Equals(right);

public static bool operator !=(EmbedProvider? left, EmbedProvider? right)
=> !(left == right);

/// <summary>
/// Determines whether the specified object is equal to the current <see cref="EmbedProvider"/>.
/// </summary>
/// <remarks>
/// If the object passes is an <see cref="EmbedProvider"/>, <see cref="Equals(EmbedProvider?)"/> will be called to compare the 2 instances
/// </remarks>
/// <param name="obj">The object to compare with the current <see cref="EmbedProvider"/></param>
/// <returns></returns>
public override bool Equals(object obj)
=> obj is EmbedProvider embedProvider && Equals(embedProvider);

/// <summary>
/// Determines whether the specified <see cref="EmbedProvider"/> is equal to the current <see cref="EmbedProvider"/>
/// </summary>
/// <param name="embedProvider">The <see cref="EmbedProvider"/> to compare with the current <see cref="EmbedProvider"/></param>
/// <returns></returns>
public bool Equals(EmbedProvider? embedProvider)
=> GetHashCode() == embedProvider?.GetHashCode();

/// <inheritdoc />
public override int GetHashCode()
=> (Name, Url).GetHashCode();
}
}

+ 31
- 0
src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs View File

@@ -1,3 +1,4 @@
using System;
using System.Diagnostics;

namespace Discord
@@ -53,5 +54,35 @@ namespace Discord
/// A string that resolves to <see cref="Discord.EmbedThumbnail.Url" />.
/// </returns>
public override string ToString() => Url;

public static bool operator ==(EmbedThumbnail? left, EmbedThumbnail? right)
=> left is null ? right is null
: left.Equals(right);

public static bool operator !=(EmbedThumbnail? left, EmbedThumbnail? right)
=> !(left == right);

/// <summary>
/// Determines whether the specified object is equal to the current <see cref="EmbedThumbnail"/>.
/// </summary>
/// <remarks>
/// If the object passes is an <see cref="EmbedThumbnail"/>, <see cref="Equals(EmbedThumbnail?)"/> will be called to compare the 2 instances
/// </remarks>
/// <param name="obj">The object to compare with the current <see cref="EmbedThumbnail"/></param>
/// <returns></returns>
public override bool Equals(object obj)
=> obj is EmbedThumbnail embedThumbnail && Equals(embedThumbnail);

/// <summary>
/// Determines whether the specified <see cref="EmbedThumbnail"/> is equal to the current <see cref="EmbedThumbnail"/>
/// </summary>
/// <param name="embedThumbnail">The <see cref="EmbedThumbnail"/> to compare with the current <see cref="EmbedThumbnail"/></param>
/// <returns></returns>
public bool Equals(EmbedThumbnail? embedThumbnail)
=> GetHashCode() == embedThumbnail?.GetHashCode();

/// <inheritdoc />
public override int GetHashCode()
=> (Width, Height, Url, ProxyUrl).GetHashCode();
}
}

+ 31
- 0
src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs View File

@@ -1,3 +1,4 @@
using System;
using System.Diagnostics;

namespace Discord
@@ -47,5 +48,35 @@ namespace Discord
/// A string that resolves to <see cref="Url"/>.
/// </returns>
public override string ToString() => Url;

public static bool operator ==(EmbedVideo? left, EmbedVideo? right)
=> left is null ? right is null
: left.Equals(right);

public static bool operator !=(EmbedVideo? left, EmbedVideo? right)
=> !(left == right);

/// <summary>
/// Determines whether the specified object is equal to the current <see cref="EmbedVideo"/>.
/// </summary>
/// <remarks>
/// If the object passes is an <see cref="EmbedVideo"/>, <see cref="Equals(EmbedVideo?)"/> will be called to compare the 2 instances
/// </remarks>
/// <param name="obj">The object to compare with the current <see cref="EmbedVideo"/></param>
/// <returns></returns>
public override bool Equals(object obj)
=> obj is EmbedVideo embedVideo && Equals(embedVideo);

/// <summary>
/// Determines whether the specified <see cref="EmbedVideo"/> is equal to the current <see cref="EmbedVideo"/>
/// </summary>
/// <param name="embedVideo">The <see cref="EmbedVideo"/> to compare with the current <see cref="EmbedVideo"/></param>
/// <returns></returns>
public bool Equals(EmbedVideo? embedVideo)
=> GetHashCode() == embedVideo?.GetHashCode();

/// <inheritdoc />
public override int GetHashCode()
=> (Width, Height, Url).GetHashCode();
}
}

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

+ 13
- 5
src/Discord.Net.Rest/API/Rest/CreateStickerParams.cs View File

@@ -1,4 +1,5 @@
using Discord.Net.Rest;

using System.Collections.Generic;
using System.IO;
namespace Discord.API.Rest
@@ -20,14 +21,21 @@ namespace Discord.API.Rest
["tags"] = Tags
};

string contentType = "image/png";

string contentType;
if (File is FileStream fileStream)
contentType = $"image/{Path.GetExtension(fileStream.Name)}";
{
var extension = Path.GetExtension(fileStream.Name).TrimStart('.');
contentType = extension == "json" ? "application/json" : $"image/{extension}";
}
else if (FileName != null)
contentType = $"image/{Path.GetExtension(FileName)}";
{
var extension = Path.GetExtension(FileName).TrimStart('.');
contentType = extension == "json" ? "application/json" : $"image/{extension}";
}
else
contentType = "image/png";

d["file"] = new MultipartFile(File, FileName ?? "image", contentType.Replace(".", ""));
d["file"] = new MultipartFile(File, FileName ?? "image", contentType);

return d;
}


+ 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);


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

@@ -180,7 +180,7 @@ namespace Discord.Rest
},
nextPage: (info, lastPage) =>
{
if (lastPage.Count != DiscordConfig.MaxMessagesPerBatch)
if (lastPage.Count != DiscordConfig.MaxBansPerBatch)
return false;
if (dir == Direction.Before)
info.Position = lastPage.Min(x => x.User.Id);
@@ -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



+ 7
- 7
src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs View File

@@ -23,7 +23,7 @@ namespace Discord.Rest
{
role.Guild.Features.EnsureFeature(GuildFeature.RoleIcons);

if (args.Icon.IsSpecified && args.Emoji.IsSpecified)
if ((args.Icon.IsSpecified && args.Icon.Value != null) && (args.Emoji.IsSpecified && args.Emoji.Value != null))
{
throw new ArgumentException("Emoji and Icon properties cannot be present on a role at the same time.");
}
@@ -36,18 +36,18 @@ namespace Discord.Rest
Mentionable = args.Mentionable,
Name = args.Name,
Permissions = args.Permissions.IsSpecified ? args.Permissions.Value.RawValue.ToString() : Optional.Create<string>(),
Icon = args.Icon.IsSpecified ? args.Icon.Value.Value.ToModel() : Optional<API.Image?>.Unspecified,
Emoji = args.Emoji.GetValueOrDefault()?.Name ?? Optional<string>.Unspecified
Icon = args.Icon.IsSpecified ? args.Icon.Value?.ToModel() ?? null : Optional<API.Image?>.Unspecified,
Emoji = args.Emoji.IsSpecified ? args.Emoji.Value?.Name ?? "" : Optional.Create<string>(),
};

if (args.Icon.IsSpecified && role.Emoji != null)
if ((args.Icon.IsSpecified && args.Icon.Value != null) && role.Emoji != null)
{
apiArgs.Emoji = null;
apiArgs.Emoji = "";
}

if (args.Emoji.IsSpecified && !string.IsNullOrEmpty(role.Icon))
if ((args.Emoji.IsSpecified && args.Emoji.Value != null) && !string.IsNullOrEmpty(role.Icon))
{
apiArgs.Icon = null;
apiArgs.Icon = Optional<API.Image?>.Unspecified;
}

var model = await client.ApiClient.ModifyGuildRoleAsync(role.Guild.Id, role.Id, apiArgs, options).ConfigureAwait(false);


+ 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

@@ -453,14 +453,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)
{
@@ -3241,8 +3243,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()


+ 2
- 2
src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs View File

@@ -36,8 +36,8 @@ namespace Discord.WebSocket
/// Gets a collection of users that are able to view the channel.
/// </summary>
/// <remarks>
/// If this channel is a voice channel, a collection of users who are currently connected to this channel
/// is returned.
/// If this channel is a voice channel, use <see cref="SocketVoiceChannel.ConnectedUsers"/> to retrieve a
/// collection of users who are currently connected to this channel.
/// </remarks>
/// <returns>
/// A read-only collection of users that can access the channel (i.e. the users seen in the user list).


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


+ 1
- 0
test/Discord.Net.Tests.Unit/ColorTests.cs View File

@@ -10,6 +10,7 @@ namespace Discord
/// </summary>
public class ColorTests
{
[Fact]
public void Color_New()
{
Assert.Equal(0u, new Color().RawValue);


Loading…
Cancel
Save