| @@ -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,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> | |||
| @@ -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" | |||
| @@ -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 | |||
| @@ -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 | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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 | |||
| } | |||
| } | |||
| @@ -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. | |||
| @@ -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."); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -1,4 +1,8 @@ | |||
| using System; | |||
| using System.Collections; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text.RegularExpressions; | |||
| namespace Discord | |||
| { | |||
| @@ -9,6 +13,7 @@ namespace Discord | |||
| { | |||
| private string _name; | |||
| private object _value; | |||
| private IDictionary<string, string> _nameLocalizations = new Dictionary<string, string>(); | |||
| /// <summary> | |||
| /// Gets or sets the name of this choice. | |||
| @@ -40,5 +45,33 @@ namespace Discord | |||
| _value = value; | |||
| } | |||
| } | |||
| /// <summary> | |||
| /// Gets or sets the localization dictionary for the name field of this choice. | |||
| /// </summary> | |||
| /// <exception cref="ArgumentException">Thrown when any of the dictionary keys is an invalid locale.</exception> | |||
| public IDictionary<string, string> NameLocalizations | |||
| { | |||
| get => _nameLocalizations; | |||
| set | |||
| { | |||
| foreach (var (locale, name) in value) | |||
| { | |||
| if (!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) | |||
| throw new ArgumentException("Key values of the dictionary must be valid language codes."); | |||
| switch (name.Length) | |||
| { | |||
| case > 100: | |||
| throw new ArgumentOutOfRangeException(nameof(value), | |||
| "Name length must be less than or equal to 100."); | |||
| case 0: | |||
| throw new ArgumentOutOfRangeException(nameof(value), "Name length must at least 1."); | |||
| } | |||
| } | |||
| _nameLocalizations = value; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -1,3 +1,10 @@ | |||
| using System; | |||
| using System.Collections; | |||
| using System.Collections.Generic; | |||
| using System.Collections.Immutable; | |||
| using System.Linq; | |||
| using System.Text.RegularExpressions; | |||
| namespace Discord | |||
| { | |||
| /// <summary> | |||
| @@ -5,6 +12,9 @@ namespace Discord | |||
| /// </summary> | |||
| public abstract class ApplicationCommandProperties | |||
| { | |||
| private IReadOnlyDictionary<string, string> _nameLocalizations; | |||
| private IReadOnlyDictionary<string, string> _descriptionLocalizations; | |||
| internal abstract ApplicationCommandType Type { get; } | |||
| /// <summary> | |||
| @@ -17,6 +27,48 @@ namespace Discord | |||
| /// </summary> | |||
| public Optional<bool> IsDefaultPermission { get; set; } | |||
| /// <summary> | |||
| /// Gets or sets the localization dictionary for the name field of this command. | |||
| /// </summary> | |||
| public IReadOnlyDictionary<string, string> NameLocalizations | |||
| { | |||
| get => _nameLocalizations; | |||
| set | |||
| { | |||
| foreach (var (locale, name) in value) | |||
| { | |||
| if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) | |||
| throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); | |||
| Preconditions.AtLeast(name.Length, 1, nameof(name)); | |||
| Preconditions.AtMost(name.Length, SlashCommandBuilder.MaxNameLength, nameof(name)); | |||
| if (!Regex.IsMatch(name, @"^[\w-]{1,32}$")) | |||
| throw new ArgumentException("Option name cannot contain any special characters or whitespaces!", nameof(name)); | |||
| } | |||
| _nameLocalizations = value; | |||
| } | |||
| } | |||
| /// <summary> | |||
| /// Gets or sets the localization dictionary for the description field of this command. | |||
| /// </summary> | |||
| public IReadOnlyDictionary<string, string> DescriptionLocalizations | |||
| { | |||
| get => _descriptionLocalizations; | |||
| set | |||
| { | |||
| foreach (var (locale, description) in value) | |||
| { | |||
| if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) | |||
| throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); | |||
| Preconditions.AtLeast(description.Length, 1, nameof(description)); | |||
| Preconditions.AtMost(description.Length, SlashCommandBuilder.MaxDescriptionLength, nameof(description)); | |||
| } | |||
| _descriptionLocalizations = value; | |||
| } | |||
| } | |||
| /// <summary> | |||
| /// Gets or sets whether or not this command can be used in DMs. | |||
| /// </summary> | |||
| @@ -1,3 +1,8 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text.RegularExpressions; | |||
| namespace Discord | |||
| { | |||
| /// <summary> | |||
| @@ -31,6 +36,11 @@ namespace Discord | |||
| /// </summary> | |||
| public bool IsDefaultPermission { get; set; } = true; | |||
| /// <summary> | |||
| /// Gets the localization dictionary for the name field of this command. | |||
| /// </summary> | |||
| public IReadOnlyDictionary<string, string> NameLocalizations => _nameLocalizations; | |||
| /// <summary> | |||
| /// Gets or sets whether or not this command can be used in DMs. | |||
| /// </summary> | |||
| @@ -42,6 +52,7 @@ namespace Discord | |||
| public GuildPermission? DefaultMemberPermissions { get; set; } | |||
| private string _name; | |||
| private Dictionary<string, string> _nameLocalizations; | |||
| /// <summary> | |||
| /// Build the current builder into a <see cref="MessageCommandProperties"/> class. | |||
| @@ -86,6 +97,30 @@ namespace Discord | |||
| return this; | |||
| } | |||
| /// <summary> | |||
| /// Sets the <see cref="NameLocalizations"/> collection. | |||
| /// </summary> | |||
| /// <param name="nameLocalizations">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> | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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; } | |||
| } | |||
| } | |||
| @@ -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; } | |||
| } | |||
| } | |||
| @@ -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> | |||
| @@ -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)); | |||
| } | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -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(); | |||
| } | |||
| } | |||
| @@ -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(); | |||
| } | |||
| } | |||
| @@ -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(); | |||
| } | |||
| } | |||
| @@ -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(); | |||
| } | |||
| } | |||
| @@ -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(); | |||
| } | |||
| } | |||
| @@ -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(); | |||
| } | |||
| } | |||
| @@ -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(); | |||
| } | |||
| } | |||
| @@ -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(); | |||
| } | |||
| } | |||
| @@ -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> | |||
| @@ -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); | |||
| } | |||
| @@ -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 | |||
| } | |||
| } | |||
| @@ -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. | |||
| @@ -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); | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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> | |||
| @@ -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; | |||
| } | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| @@ -19,9 +19,36 @@ namespace Discord.Interactions | |||
| if (!ModalUtils.TryGet<T>(out var modalInfo)) | |||
| throw new ArgumentException($"{typeof(T).FullName} isn't referenced by any registered Modal Interaction Command and doesn't have a cached {typeof(ModalInfo)}"); | |||
| await SendModalResponseAsync(interaction, customId, modalInfo, options, modifyModal); | |||
| } | |||
| /// <summary> | |||
| /// Respond to an interaction with a <see cref="IModal"/>. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// This method overload uses the <paramref name="interactionService"/> parameter to create a new <see cref="ModalInfo"/> | |||
| /// if there isn't a built one already in cache. | |||
| /// </remarks> | |||
| /// <typeparam name="T">Type of the <see cref="IModal"/> implementation.</typeparam> | |||
| /// <param name="interaction">The interaction to respond to.</param> | |||
| /// <param name="interactionService">Interaction service instance that should be used to build <see cref="ModalInfo"/>s.</param> | |||
| /// <param name="options">The request options for this <see langword="async"/> request.</param> | |||
| /// <param name="modifyModal">Delegate that can be used to modify the modal.</param> | |||
| /// <returns></returns> | |||
| public static async Task RespondWithModalAsync<T>(this IDiscordInteraction interaction, string customId, InteractionService interactionService, | |||
| RequestOptions options = null, Action<ModalBuilder> modifyModal = null) | |||
| where T : class, IModal | |||
| { | |||
| var modalInfo = ModalUtils.GetOrAdd<T>(interactionService); | |||
| await SendModalResponseAsync(interaction, customId, modalInfo, options, modifyModal); | |||
| } | |||
| private static async Task SendModalResponseAsync(IDiscordInteraction interaction, string customId, ModalInfo modalInfo, RequestOptions options = null, Action<ModalBuilder> modifyModal = null) | |||
| { | |||
| var builder = new ModalBuilder(modalInfo.Title, customId); | |||
| foreach(var input in modalInfo.Components) | |||
| foreach (var input in modalInfo.Components) | |||
| switch (input) | |||
| { | |||
| case TextInputComponentInfo textComponent: | |||
| @@ -83,6 +83,11 @@ namespace Discord.Interactions | |||
| public event Func<ModalCommandInfo, IInteractionContext, IResult, Task> ModalCommandExecuted { add { _modalCommandExecutedEvent.Add(value); } remove { _modalCommandExecutedEvent.Remove(value); } } | |||
| internal readonly AsyncEvent<Func<ModalCommandInfo, IInteractionContext, IResult, Task>> _modalCommandExecutedEvent = new(); | |||
| /// <summary> | |||
| /// Get the <see cref="ILocalizationManager"/> used by this Interaction Service instance to localize strings. | |||
| /// </summary> | |||
| public ILocalizationManager LocalizationManager { get; set; } | |||
| private readonly ConcurrentDictionary<Type, ModuleInfo> _typedModuleDefs; | |||
| private readonly CommandMap<SlashCommandInfo> _slashCommandMap; | |||
| private readonly ConcurrentDictionary<ApplicationCommandType, CommandMap<ContextCommandInfo>> _contextCommandMaps; | |||
| @@ -203,6 +208,7 @@ namespace Discord.Interactions | |||
| _enableAutocompleteHandlers = config.EnableAutocompleteHandlers; | |||
| _autoServiceScopes = config.AutoServiceScopes; | |||
| _restResponseCallback = config.RestResponseCallback; | |||
| LocalizationManager = config.LocalizationManager; | |||
| _typeConverterMap = new TypeMap<TypeConverter, IApplicationCommandInteractionDataOption>(this, new ConcurrentDictionary<Type, TypeConverter> | |||
| { | |||
| @@ -64,6 +64,11 @@ namespace Discord.Interactions | |||
| /// Gets or sets whether a command execution should exit when a modal command encounters a missing modal component value. | |||
| /// </summary> | |||
| public bool ExitOnMissingModalField { get; set; } = false; | |||
| /// <summary> | |||
| /// Localization provider to be used when registering application commands. | |||
| /// </summary> | |||
| public ILocalizationManager LocalizationManager { get; set; } | |||
| } | |||
| /// <summary> | |||
| @@ -0,0 +1,32 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.Interactions | |||
| { | |||
| /// <summary> | |||
| /// Respresents a localization provider for Discord Application Commands. | |||
| /// </summary> | |||
| public interface ILocalizationManager | |||
| { | |||
| /// <summary> | |||
| /// Get every the resource name for every available locale. | |||
| /// </summary> | |||
| /// <param name="key">Location of the resource.</param> | |||
| /// <param name="destinationType">Type of the resource.</param> | |||
| /// <returns> | |||
| /// A dictionary containing every available locale and the resource name. | |||
| /// </returns> | |||
| IDictionary<string, string> GetAllNames(IList<string> key, LocalizationTarget destinationType); | |||
| /// <summary> | |||
| /// Get every the resource description for every available locale. | |||
| /// </summary> | |||
| /// <param name="key">Location of the resource.</param> | |||
| /// <param name="destinationType">Type of the resource.</param> | |||
| /// <returns> | |||
| /// A dictionary containing every available locale and the resource name. | |||
| /// </returns> | |||
| IDictionary<string, string> GetAllDescriptions(IList<string> key, LocalizationTarget destinationType); | |||
| } | |||
| } | |||
| @@ -0,0 +1,72 @@ | |||
| using Newtonsoft.Json; | |||
| using Newtonsoft.Json.Linq; | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.IO; | |||
| using System.Linq; | |||
| using System.Text.RegularExpressions; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.Interactions | |||
| { | |||
| /// <summary> | |||
| /// The default localization provider for Json resource files. | |||
| /// </summary> | |||
| public sealed class JsonLocalizationManager : ILocalizationManager | |||
| { | |||
| private const string NameIdentifier = "name"; | |||
| private const string DescriptionIdentifier = "description"; | |||
| private const string SpaceToken = "~"; | |||
| private readonly string _basePath; | |||
| private readonly string _fileName; | |||
| private readonly Regex _localeParserRegex = new Regex(@"\w+.(?<locale>\w{2}(?:-\w{2})?).json", RegexOptions.Compiled | RegexOptions.Singleline); | |||
| /// <summary> | |||
| /// Initializes a new instance of the <see cref="JsonLocalizationManager"/> class. | |||
| /// </summary> | |||
| /// <param name="basePath">Base path of the Json file.</param> | |||
| /// <param name="fileName">Name of the Json file.</param> | |||
| public JsonLocalizationManager(string basePath, string fileName) | |||
| { | |||
| _basePath = basePath; | |||
| _fileName = fileName; | |||
| } | |||
| /// <inheritdoc /> | |||
| public IDictionary<string, string> GetAllDescriptions(IList<string> key, LocalizationTarget destinationType) => | |||
| GetValues(key, DescriptionIdentifier); | |||
| /// <inheritdoc /> | |||
| public IDictionary<string, string> GetAllNames(IList<string> key, LocalizationTarget destinationType) => | |||
| GetValues(key, NameIdentifier); | |||
| private string[] GetAllFiles() => | |||
| Directory.GetFiles(_basePath, $"{_fileName}.*.json", SearchOption.TopDirectoryOnly); | |||
| private IDictionary<string, string> GetValues(IList<string> key, string identifier) | |||
| { | |||
| var result = new Dictionary<string, string>(); | |||
| var files = GetAllFiles(); | |||
| foreach (var file in files) | |||
| { | |||
| var match = _localeParserRegex.Match(Path.GetFileName(file)); | |||
| if (!match.Success) | |||
| continue; | |||
| var locale = match.Groups["locale"].Value; | |||
| using var sr = new StreamReader(file); | |||
| using var jr = new JsonTextReader(sr); | |||
| var obj = JObject.Load(jr); | |||
| var token = string.Join(".", key.Select(x => $"['{x}']")) + $".{identifier}"; | |||
| var value = (string)obj.SelectToken(token); | |||
| if (value is not null) | |||
| result[locale] = value; | |||
| } | |||
| return result; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,55 @@ | |||
| using System.Collections.Generic; | |||
| using System.Globalization; | |||
| using System.Reflection; | |||
| using System.Resources; | |||
| namespace Discord.Interactions | |||
| { | |||
| /// <summary> | |||
| /// The default localization provider for Resx files. | |||
| /// </summary> | |||
| public sealed class ResxLocalizationManager : ILocalizationManager | |||
| { | |||
| private const string NameIdentifier = "name"; | |||
| private const string DescriptionIdentifier = "description"; | |||
| private readonly ResourceManager _resourceManager; | |||
| private readonly IEnumerable<CultureInfo> _supportedLocales; | |||
| /// <summary> | |||
| /// Initializes a new instance of the <see cref="ResxLocalizationManager"/> class. | |||
| /// </summary> | |||
| /// <param name="baseResource">Name of the base resource.</param> | |||
| /// <param name="assembly">The main assembly for the resources.</param> | |||
| /// <param name="supportedLocales">Cultures the <see cref="ResxLocalizationManager"/> should search for.</param> | |||
| public ResxLocalizationManager(string baseResource, Assembly assembly, params CultureInfo[] supportedLocales) | |||
| { | |||
| _supportedLocales = supportedLocales; | |||
| _resourceManager = new ResourceManager(baseResource, assembly); | |||
| } | |||
| /// <inheritdoc /> | |||
| public IDictionary<string, string> GetAllDescriptions(IList<string> key, LocalizationTarget destinationType) => | |||
| GetValues(key, DescriptionIdentifier); | |||
| /// <inheritdoc /> | |||
| public IDictionary<string, string> GetAllNames(IList<string> key, LocalizationTarget destinationType) => | |||
| GetValues(key, NameIdentifier); | |||
| private IDictionary<string, string> GetValues(IList<string> key, string identifier) | |||
| { | |||
| var entryKey = (string.Join(".", key) + "." + identifier); | |||
| var result = new Dictionary<string, string>(); | |||
| foreach (var locale in _supportedLocales) | |||
| { | |||
| var value = _resourceManager.GetString(entryKey, locale); | |||
| if (value is not null) | |||
| result[locale.Name] = value; | |||
| } | |||
| return result; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,25 @@ | |||
| namespace Discord.Interactions | |||
| { | |||
| /// <summary> | |||
| /// Resource targets for localization. | |||
| /// </summary> | |||
| public enum LocalizationTarget | |||
| { | |||
| /// <summary> | |||
| /// Target is a <see cref="IInteractionModuleBase"/> tagged with a <see cref="GroupAttribute"/>. | |||
| /// </summary> | |||
| Group, | |||
| /// <summary> | |||
| /// Target is an application command method. | |||
| /// </summary> | |||
| Command, | |||
| /// <summary> | |||
| /// Target is a Slash Command parameter. | |||
| /// </summary> | |||
| Parameter, | |||
| /// <summary> | |||
| /// Target is a Slash Command parameter choice. | |||
| /// </summary> | |||
| Choice | |||
| } | |||
| } | |||
| @@ -42,7 +42,7 @@ namespace Discord.Interactions | |||
| public void RemoveCommand(T command) | |||
| { | |||
| var key = ParseCommandName(command); | |||
| var key = CommandHierarchy.GetCommandPath(command); | |||
| _root.RemoveCommand(key, 0); | |||
| } | |||
| @@ -60,28 +60,9 @@ namespace Discord.Interactions | |||
| private void AddCommand(T command) | |||
| { | |||
| var key = ParseCommandName(command); | |||
| var key = CommandHierarchy.GetCommandPath(command); | |||
| _root.AddCommand(key, 0, command); | |||
| } | |||
| private IList<string> ParseCommandName(T command) | |||
| { | |||
| var keywords = new List<string>() { command.Name }; | |||
| var currentParent = command.Module; | |||
| while (currentParent != null) | |||
| { | |||
| if (!string.IsNullOrEmpty(currentParent.SlashGroupName)) | |||
| keywords.Add(currentParent.SlashGroupName); | |||
| currentParent = currentParent.Parent; | |||
| } | |||
| keywords.Reverse(); | |||
| return keywords; | |||
| } | |||
| } | |||
| } | |||
| @@ -1,5 +1,6 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Collections.Immutable; | |||
| using System.Linq; | |||
| namespace Discord.Interactions | |||
| @@ -9,6 +10,9 @@ namespace Discord.Interactions | |||
| #region Parameters | |||
| public static ApplicationCommandOptionProperties ToApplicationCommandOptionProps(this SlashCommandParameterInfo parameterInfo) | |||
| { | |||
| var localizationManager = parameterInfo.Command.Module.CommandService.LocalizationManager; | |||
| var parameterPath = parameterInfo.GetParameterPath(); | |||
| var props = new ApplicationCommandOptionProperties | |||
| { | |||
| Name = parameterInfo.Name, | |||
| @@ -18,12 +22,15 @@ namespace Discord.Interactions | |||
| Choices = parameterInfo.Choices?.Select(x => new ApplicationCommandOptionChoiceProperties | |||
| { | |||
| Name = x.Name, | |||
| Value = x.Value | |||
| Value = x.Value, | |||
| NameLocalizations = localizationManager?.GetAllNames(parameterInfo.GetChoicePath(x), LocalizationTarget.Choice) ?? ImmutableDictionary<string, string>.Empty | |||
| })?.ToList(), | |||
| ChannelTypes = parameterInfo.ChannelTypes?.ToList(), | |||
| IsAutocomplete = parameterInfo.IsAutocomplete, | |||
| MaxValue = parameterInfo.MaxValue, | |||
| MinValue = parameterInfo.MinValue, | |||
| 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) | |||
| @@ -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 }; | |||
| } | |||
| } | |||
| @@ -1,4 +1,5 @@ | |||
| using Newtonsoft.Json; | |||
| using System.Collections.Generic; | |||
| namespace Discord.API | |||
| { | |||
| @@ -25,6 +26,18 @@ namespace Discord.API | |||
| [JsonProperty("default_permission")] | |||
| public Optional<bool> DefaultPermissions { get; set; } | |||
| [JsonProperty("name_localizations")] | |||
| public Optional<Dictionary<string, string>> NameLocalizations { get; set; } | |||
| [JsonProperty("description_localizations")] | |||
| public Optional<Dictionary<string, string>> DescriptionLocalizations { get; set; } | |||
| [JsonProperty("name_localized")] | |||
| public Optional<string> NameLocalized { get; set; } | |||
| [JsonProperty("description_localized")] | |||
| public Optional<string> DescriptionLocalized { get; set; } | |||
| // V2 Permissions | |||
| [JsonProperty("dm_permission")] | |||
| public Optional<bool?> DmPermission { get; set; } | |||
| @@ -1,4 +1,5 @@ | |||
| using Newtonsoft.Json; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| namespace Discord.API | |||
| @@ -38,6 +39,18 @@ namespace Discord.API | |||
| [JsonProperty("channel_types")] | |||
| public Optional<ChannelType[]> ChannelTypes { get; set; } | |||
| [JsonProperty("name_localizations")] | |||
| public Optional<Dictionary<string, string>> NameLocalizations { get; set; } | |||
| [JsonProperty("description_localizations")] | |||
| public Optional<Dictionary<string, string>> DescriptionLocalizations { get; set; } | |||
| [JsonProperty("name_localized")] | |||
| public Optional<string> NameLocalized { get; set; } | |||
| [JsonProperty("description_localized")] | |||
| public Optional<string> DescriptionLocalized { get; set; } | |||
| [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; | |||
| } | |||
| } | |||
| } | |||
| @@ -1,4 +1,5 @@ | |||
| using Newtonsoft.Json; | |||
| using System.Collections.Generic; | |||
| namespace Discord.API | |||
| { | |||
| @@ -9,5 +10,11 @@ namespace Discord.API | |||
| [JsonProperty("value")] | |||
| public object Value { get; set; } | |||
| [JsonProperty("name_localizations")] | |||
| public Optional<Dictionary<string, string>> NameLocalizations { get; set; } | |||
| [JsonProperty("name_localized")] | |||
| public Optional<string> NameLocalized { get; set; } | |||
| } | |||
| } | |||
| @@ -1,4 +1,8 @@ | |||
| using Newtonsoft.Json; | |||
| using System.Collections; | |||
| using System.Collections.Generic; | |||
| using System.Collections.Immutable; | |||
| using System.Linq; | |||
| namespace Discord.API.Rest | |||
| { | |||
| @@ -19,6 +23,12 @@ namespace Discord.API.Rest | |||
| [JsonProperty("default_permission")] | |||
| public Optional<bool> DefaultPermission { get; set; } | |||
| [JsonProperty("name_localizations")] | |||
| public Optional<Dictionary<string, string>> NameLocalizations { get; set; } | |||
| [JsonProperty("description_localizations")] | |||
| public Optional<Dictionary<string, string>> DescriptionLocalizations { get; set; } | |||
| [JsonProperty("dm_permission")] | |||
| public Optional<bool?> DmPermission { get; set; } | |||
| @@ -26,12 +36,15 @@ namespace Discord.API.Rest | |||
| public Optional<GuildPermission?> DefaultMemberPermission { get; set; } | |||
| public CreateApplicationCommandParams() { } | |||
| public CreateApplicationCommandParams(string name, string description, ApplicationCommandType type, ApplicationCommandOption[] options = null) | |||
| public CreateApplicationCommandParams(string name, string description, ApplicationCommandType type, ApplicationCommandOption[] options = null, | |||
| IDictionary<string, string> nameLocalizations = null, IDictionary<string, string> descriptionLocalizations = null) | |||
| { | |||
| Name = name; | |||
| Description = description; | |||
| Options = Optional.Create(options); | |||
| Type = type; | |||
| NameLocalizations = nameLocalizations?.ToDictionary(x => x.Key, x => x.Value) ?? Optional<Dictionary<string, string>>.Unspecified; | |||
| DescriptionLocalizations = descriptionLocalizations?.ToDictionary(x => x.Key, x => x.Value) ?? Optional<Dictionary<string, string>>.Unspecified; | |||
| } | |||
| } | |||
| } | |||
| @@ -1,4 +1,5 @@ | |||
| using 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; | |||
| } | |||
| @@ -1,4 +1,5 @@ | |||
| using Newtonsoft.Json; | |||
| using System.Collections.Generic; | |||
| namespace Discord.API.Rest | |||
| { | |||
| @@ -15,5 +16,11 @@ namespace Discord.API.Rest | |||
| [JsonProperty("default_permission")] | |||
| public Optional<bool> DefaultPermission { get; set; } | |||
| [JsonProperty("name_localizations")] | |||
| public Optional<Dictionary<string, string>> NameLocalizations { get; set; } | |||
| [JsonProperty("description_localizations")] | |||
| public Optional<Dictionary<string, string>> DescriptionLocalizations { get; set; } | |||
| } | |||
| } | |||
| @@ -243,7 +243,7 @@ namespace Discord.Rest | |||
| => Task.FromResult<IApplicationCommand>(null); | |||
| /// <inheritdoc /> | |||
| Task<IReadOnlyCollection<IApplicationCommand>> IDiscordClient.GetGlobalApplicationCommandsAsync(RequestOptions options) | |||
| Task<IReadOnlyCollection<IApplicationCommand>> IDiscordClient.GetGlobalApplicationCommandsAsync(bool withLocalizations, string locale, RequestOptions options) | |||
| => Task.FromResult<IReadOnlyCollection<IApplicationCommand>>(ImmutableArray.Create<IApplicationCommand>()); | |||
| Task<IApplicationCommand> IDiscordClient.CreateGlobalApplicationCommand(ApplicationCommandProperties properties, RequestOptions options) | |||
| => Task.FromResult<IApplicationCommand>(null); | |||
| @@ -194,10 +194,10 @@ namespace Discord.Rest | |||
| }; | |||
| } | |||
| public static async Task<IReadOnlyCollection<RestGlobalCommand>> GetGlobalApplicationCommandsAsync(BaseDiscordClient client, | |||
| RequestOptions options = null) | |||
| public static async Task<IReadOnlyCollection<RestGlobalCommand>> GetGlobalApplicationCommandsAsync(BaseDiscordClient client, bool withLocalizations = false, | |||
| string locale = null, RequestOptions options = null) | |||
| { | |||
| var response = await client.ApiClient.GetGlobalApplicationCommandsAsync(options).ConfigureAwait(false); | |||
| var response = await client.ApiClient.GetGlobalApplicationCommandsAsync(withLocalizations, locale, options).ConfigureAwait(false); | |||
| if (!response.Any()) | |||
| 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>(); | |||
| @@ -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) | |||
| @@ -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); | |||
| @@ -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, | |||
| @@ -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); | |||
| @@ -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) | |||
| @@ -38,6 +38,32 @@ namespace Discord.Rest | |||
| /// </summary> | |||
| public IReadOnlyCollection<RestApplicationCommandOption> Options { get; private set; } | |||
| /// <summary> | |||
| /// Gets the localization dictionary for the name field of this command. | |||
| /// </summary> | |||
| public IReadOnlyDictionary<string, string> NameLocalizations { get; private set; } | |||
| /// <summary> | |||
| /// Gets the localization dictionary for the description field of this command. | |||
| /// </summary> | |||
| public IReadOnlyDictionary<string, string> DescriptionLocalizations { get; private set; } | |||
| /// <summary> | |||
| /// Gets the localized name of this command. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// Only returned when the `withLocalizations` query parameter is set to <see langword="false"/> when requesting the command. | |||
| /// </remarks> | |||
| public string NameLocalized { get; private set; } | |||
| /// <summary> | |||
| /// Gets the localized description of this command. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// Only returned when the `withLocalizations` query parameter is set to <see langword="false"/> when requesting the command. | |||
| /// </remarks> | |||
| public string DescriptionLocalized { get; private set; } | |||
| /// <inheritdoc/> | |||
| public DateTimeOffset CreatedAt | |||
| => SnowflakeUtils.FromSnowflake(Id); | |||
| @@ -64,6 +90,15 @@ namespace Discord.Rest | |||
| ? model.Options.Value.Select(RestApplicationCommandOption.Create).ToImmutableArray() | |||
| : ImmutableArray.Create<RestApplicationCommandOption>(); | |||
| NameLocalizations = model.NameLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ?? | |||
| ImmutableDictionary<string, string>.Empty; | |||
| DescriptionLocalizations = model.DescriptionLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ?? | |||
| ImmutableDictionary<string, string>.Empty; | |||
| NameLocalized = model.NameLocalized.GetValueOrDefault(); | |||
| DescriptionLocalized = model.DescriptionLocalized.GetValueOrDefault(); | |||
| IsEnabledInDm = model.DmPermission.GetValueOrDefault(true).GetValueOrDefault(true); | |||
| DefaultMemberPermissions = new GuildPermissions((ulong)model.DefaultMemberPermission.GetValueOrDefault(0).GetValueOrDefault(0)); | |||
| } | |||
| @@ -1,3 +1,5 @@ | |||
| using System.Collections.Generic; | |||
| using System.Collections.Immutable; | |||
| using Model = Discord.API.ApplicationCommandOptionChoice; | |||
| namespace Discord.Rest | |||
| @@ -13,10 +15,25 @@ namespace Discord.Rest | |||
| /// <inheritdoc/> | |||
| public object Value { get; } | |||
| /// <summary> | |||
| /// Gets the localization dictionary for the name field of this command option choice. | |||
| /// </summary> | |||
| public IReadOnlyDictionary<string, string> NameLocalizations { get; } | |||
| /// <summary> | |||
| /// Gets the localized name of this command option choice. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// Only returned when the `withLocalizations` query parameter is set to <see langword="false"/> when requesting the command. | |||
| /// </remarks> | |||
| public string NameLocalized { get; } | |||
| internal RestApplicationCommandChoice(Model model) | |||
| { | |||
| Name = model.Name; | |||
| Value = model.Value; | |||
| NameLocalizations = model.NameLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary(); | |||
| NameLocalized = model.NameLocalized.GetValueOrDefault(null); | |||
| } | |||
| } | |||
| } | |||
| @@ -27,7 +27,7 @@ namespace Discord.Rest | |||
| public bool? IsRequired { get; private set; } | |||
| /// <inheritdoc/> | |||
| public bool? IsAutocomplete { get; private set; } | |||
| public bool? IsAutocomplete { get; private set; } | |||
| /// <inheritdoc/> | |||
| public double? MinValue { get; private set; } | |||
| @@ -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 | |||
| @@ -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); | |||
| @@ -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: | |||
| @@ -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); | |||
| } | |||
| } | |||
| } | |||
| @@ -450,14 +450,16 @@ namespace Discord.WebSocket | |||
| /// <summary> | |||
| /// Gets a collection of all global commands. | |||
| /// </summary> | |||
| /// <param name="withLocalizations">Whether to include full localization dictionaries in the returned objects, instead of the name localized and description localized fields.</param> | |||
| /// <param name="locale">The target locale of the localized name and description fields. Sets <c>X-Discord-Locale</c> header, which takes precedence over <c>Accept-Language</c>.</param> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <returns> | |||
| /// A task that represents the asynchronous get operation. The task result contains a read-only collection of global | |||
| /// application commands. | |||
| /// </returns> | |||
| public async Task<IReadOnlyCollection<SocketApplicationCommand>> GetGlobalApplicationCommandsAsync(RequestOptions options = null) | |||
| public async Task<IReadOnlyCollection<SocketApplicationCommand>> GetGlobalApplicationCommandsAsync(bool withLocalizations = false, string locale = null, RequestOptions options = null) | |||
| { | |||
| var commands = (await ApiClient.GetGlobalApplicationCommandsAsync(options)).Select(x => SocketApplicationCommand.Create(this, x)); | |||
| var commands = (await ApiClient.GetGlobalApplicationCommandsAsync(withLocalizations, locale, options)).Select(x => SocketApplicationCommand.Create(this, x)); | |||
| foreach(var command in commands) | |||
| { | |||
| @@ -3236,8 +3238,8 @@ namespace Discord.WebSocket | |||
| async Task<IApplicationCommand> IDiscordClient.GetGlobalApplicationCommandAsync(ulong id, RequestOptions options) | |||
| => await GetGlobalApplicationCommandAsync(id, options); | |||
| /// <inheritdoc /> | |||
| async Task<IReadOnlyCollection<IApplicationCommand>> IDiscordClient.GetGlobalApplicationCommandsAsync(RequestOptions options) | |||
| => await GetGlobalApplicationCommandsAsync(options); | |||
| async Task<IReadOnlyCollection<IApplicationCommand>> IDiscordClient.GetGlobalApplicationCommandsAsync(bool withLocalizations, string locale, RequestOptions options) | |||
| => await GetGlobalApplicationCommandsAsync(withLocalizations, locale, options); | |||
| /// <inheritdoc /> | |||
| async Task IDiscordClient.StartAsync() | |||
| @@ -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). | |||
| @@ -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); | |||
| @@ -50,6 +50,32 @@ namespace Discord.WebSocket | |||
| /// </remarks> | |||
| public IReadOnlyCollection<SocketApplicationCommandOption> Options { get; private set; } | |||
| /// <summary> | |||
| /// Gets the localization dictionary for the name field of this command. | |||
| /// </summary> | |||
| public IReadOnlyDictionary<string, string> NameLocalizations { get; private set; } | |||
| /// <summary> | |||
| /// Gets the localization dictionary for the description field of this command. | |||
| /// </summary> | |||
| public IReadOnlyDictionary<string, string> DescriptionLocalizations { get; private set; } | |||
| /// <summary> | |||
| /// Gets the localized name of this command. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// Only returned when the `withLocalizations` query parameter is set to <see langword="false"/> when requesting the command. | |||
| /// </remarks> | |||
| public string NameLocalized { get; private set; } | |||
| /// <summary> | |||
| /// Gets the localized description of this command. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// Only returned when the `withLocalizations` query parameter is set to <see langword="false"/> when requesting the command. | |||
| /// </remarks> | |||
| public string DescriptionLocalized { get; private set; } | |||
| /// <inheritdoc/> | |||
| public DateTimeOffset CreatedAt | |||
| => SnowflakeUtils.FromSnowflake(Id); | |||
| @@ -93,6 +119,15 @@ namespace Discord.WebSocket | |||
| ? model.Options.Value.Select(SocketApplicationCommandOption.Create).ToImmutableArray() | |||
| : ImmutableArray.Create<SocketApplicationCommandOption>(); | |||
| NameLocalizations = model.NameLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ?? | |||
| ImmutableDictionary<string, string>.Empty; | |||
| DescriptionLocalizations = model.DescriptionLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ?? | |||
| ImmutableDictionary<string, string>.Empty; | |||
| NameLocalized = model.NameLocalized.GetValueOrDefault(); | |||
| DescriptionLocalized = model.DescriptionLocalized.GetValueOrDefault(); | |||
| IsEnabledInDm = model.DmPermission.GetValueOrDefault(true).GetValueOrDefault(true); | |||
| DefaultMemberPermissions = new GuildPermissions((ulong)model.DefaultMemberPermission.GetValueOrDefault(0).GetValueOrDefault(0)); | |||
| } | |||
| @@ -1,3 +1,5 @@ | |||
| using System.Collections.Generic; | |||
| using System.Collections.Immutable; | |||
| using Model = Discord.API.ApplicationCommandOptionChoice; | |||
| namespace Discord.WebSocket | |||
| @@ -13,6 +15,19 @@ namespace Discord.WebSocket | |||
| /// <inheritdoc/> | |||
| public object Value { get; private set; } | |||
| /// <summary> | |||
| /// Gets the localization dictionary for the name field of this command option choice. | |||
| /// </summary> | |||
| public IReadOnlyDictionary<string, string> NameLocalizations { get; private set; } | |||
| /// <summary> | |||
| /// Gets the localized name of this command option choice. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// Only returned when the `withLocalizations` query parameter is set to <see langword="false"/> when requesting the command. | |||
| /// </remarks> | |||
| public string NameLocalized { get; private set; } | |||
| internal SocketApplicationCommandChoice() { } | |||
| internal static SocketApplicationCommandChoice Create(Model model) | |||
| { | |||
| @@ -24,6 +39,8 @@ namespace Discord.WebSocket | |||
| { | |||
| Name = model.Name; | |||
| Value = model.Value; | |||
| NameLocalizations = model.NameLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary(); | |||
| NameLocalized = model.NameLocalized.GetValueOrDefault(null); | |||
| } | |||
| } | |||
| } | |||
| @@ -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; | |||
| @@ -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> | |||
| @@ -10,6 +10,7 @@ namespace Discord | |||
| /// </summary> | |||
| public class ColorTests | |||
| { | |||
| [Fact] | |||
| public void Color_New() | |||
| { | |||
| Assert.Equal(0u, new Color().RawValue); | |||