| @@ -1,5 +1,33 @@ | |||||
| # Changelog | # Changelog | ||||
| ## [3.9.0] - 2022-12-23 | |||||
| ### Added | |||||
| - #2469 Add missing properties in forum & thread channels (01ae904) | |||||
| - #2501 Add new discord stuff (ed38635) | |||||
| - #2521 Add missing property & new stuff (82b772a) | |||||
| - #2520 Implemented ClientDisconnect event for audio client. (4cad546) | |||||
| - #2509 Add SendFiles to UserExtensions ( 4cad546) | |||||
| - #2528 Implement wildcard lenght quantifiers, TreatAsRegex property and solve catastrpohic backtracking (25cfb88) | |||||
| - #2531 Add Age restricted (NSFW) application commands support (60956c7) | |||||
| ### Fixed | |||||
| - #2500 Fix duplicated members of DiscordErrorCode (6712ef4) | |||||
| - #2468 Fix TimestampTag being sadge (bc89d3c) | |||||
| - #2497 Avoid throwing on missing Application (7077c44) | |||||
| - #2485 Fixed an oversight clearing session data upon any disconnect. (c7ac59d) | |||||
| - #2526 Fix `GetActiveThreadsAsync` & add it to `ITextChannel` (bd2f719) | |||||
| - #2535 Fix deploy.yml (20d8fdf) | |||||
| ### Misc | |||||
| - #2471 Update samples to use `MessageContent` intent & update `v2 => v3 guide` (a4d34f6) | |||||
| - #2505 Update events.cs (ea039b8) | |||||
| - #2467 Update license and icon nuspec props (11ed0ff) | |||||
| - #2306 Command execution code rework & TypeConverters auto-scope fix (6869817) | |||||
| - #2534 Fully qualify SlashCommandBuilder namespace ( 3b107c2) | |||||
| ## [3.8.1] - 2022-09-12 | ## [3.8.1] - 2022-09-12 | ||||
| ### Added | ### Added | ||||
| @@ -1,6 +1,6 @@ | |||||
| <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | ||||
| <PropertyGroup> | <PropertyGroup> | ||||
| <VersionPrefix>3.8.1</VersionPrefix> | |||||
| <VersionPrefix>3.9.0</VersionPrefix> | |||||
| <LangVersion>latest</LangVersion> | <LangVersion>latest</LangVersion> | ||||
| <Authors>Discord.Net Contributors</Authors> | <Authors>Discord.Net Contributors</Authors> | ||||
| <PackageTags>discord;discordapp</PackageTags> | <PackageTags>discord;discordapp</PackageTags> | ||||
| @@ -11,6 +11,11 @@ steps: | |||||
| dotnet pack "experiment\Discord.Net.BuildOverrides\Discord.Net.BuildOverrides.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "$(Build.ArtifactStagingDirectory)" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag) | dotnet pack "experiment\Discord.Net.BuildOverrides\Discord.Net.BuildOverrides.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "$(Build.ArtifactStagingDirectory)" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag) | ||||
| displayName: Pack projects | displayName: Pack projects | ||||
| - task: NuGetToolInstaller@1 | |||||
| displayName: Download and Cache Nuget.exe | |||||
| inputs: | |||||
| versionSpec: 6.4.0 | |||||
| - task: NuGetCommand@2 | - task: NuGetCommand@2 | ||||
| displayName: Pack metapackage (release mode) | displayName: Pack metapackage (release mode) | ||||
| condition: eq(variables['buildTag'], True) | condition: eq(variables['buildTag'], True) | ||||
| @@ -60,7 +60,7 @@ | |||||
| "overwrite": "_overwrites/**/**.md", | "overwrite": "_overwrites/**/**.md", | ||||
| "globalMetadata": { | "globalMetadata": { | ||||
| "_appTitle": "Discord.Net Documentation", | "_appTitle": "Discord.Net Documentation", | ||||
| "_appFooter": "Discord.Net (c) 2015-2022 3.8.1", | |||||
| "_appFooter": "Discord.Net (c) 2015-2022 3.9.0", | |||||
| "_enableSearch": true, | "_enableSearch": true, | ||||
| "_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg", | "_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg", | ||||
| "_appFaviconPath": "favicon.ico" | "_appFaviconPath": "favicon.ico" | ||||
| @@ -22,7 +22,7 @@ public class Program | |||||
| { | { | ||||
| Console.WriteLine("Bot is connected!"); | Console.WriteLine("Bot is connected!"); | ||||
| return Task.CompletedTask; | return Task.CompletedTask; | ||||
| } | |||||
| }; | |||||
| await Task.Delay(-1); | await Task.Delay(-1); | ||||
| } | } | ||||
| @@ -45,7 +45,7 @@ public async Task Client_Ready() | |||||
| { | { | ||||
| ulong guildId = 848176216011046962; | ulong guildId = 848176216011046962; | ||||
| var guildCommand = new SlashCommandBuilder() | |||||
| var guildCommand = new Discord.SlashCommandBuilder() | |||||
| .WithName("list-roles") | .WithName("list-roles") | ||||
| .WithDescription("Lists all roles of a user.") | .WithDescription("Lists all roles of a user.") | ||||
| .AddOption("user", ApplicationCommandOptionType.User, "The users whos roles you want to be listed", isRequired: true); | .AddOption("user", ApplicationCommandOptionType.User, "The users whos roles you want to be listed", isRequired: true); | ||||
| @@ -7,7 +7,7 @@ title: Command Autocompletion | |||||
| [Autocompleters] provide a similar pattern to TypeConverters. | [Autocompleters] provide a similar pattern to TypeConverters. | ||||
| [Autocompleters] are cached, singleton services and they are used by the | [Autocompleters] are cached, singleton services and they are used by the | ||||
| Interaction Service to handle Autocomplete Interations targeted to a specific Slash Command parameter. | |||||
| Interaction Service to handle Autocomplete Interactions targeted to a specific Slash Command parameter. | |||||
| To start using AutocompleteHandlers, use the `[AutocompleteAttribute(Type type)]` overload of the [AutocompleteAttribute]. | To start using AutocompleteHandlers, use the `[AutocompleteAttribute(Type type)]` overload of the [AutocompleteAttribute]. | ||||
| This will dynamically link the parameter to the [AutocompleteHandler] type. | This will dynamically link the parameter to the [AutocompleteHandler] type. | ||||
| @@ -208,6 +208,9 @@ You may use as many wild card characters as you want. | |||||
| Unlike button interactions, select menu interactions also contain the values of the selected menu items. | Unlike button interactions, select menu interactions also contain the values of the selected menu items. | ||||
| In this case, you should structure your method to accept a string array. | In this case, you should structure your method to accept a string array. | ||||
| > [!NOTE] | |||||
| > Use arrays of `IUser`, `IChannel`, `IRole`, `IMentionable` or their implementations to get data from a select menu with respective type. | |||||
| [!code-csharp[Dropdown](samples/intro/dropdown.cs)] | [!code-csharp[Dropdown](samples/intro/dropdown.cs)] | ||||
| > [!NOTE] | > [!NOTE] | ||||
| @@ -55,5 +55,10 @@ The amount of nesting you can do is realistically endless. | |||||
| > If the nested class is marked with `Group`, as required for setting up subcommands, this example will not work. | > If the nested class is marked with `Group`, as required for setting up subcommands, this example will not work. | ||||
| > As mentioned before, subcommands cannot have seperate permissions from the top level command. | > As mentioned before, subcommands cannot have seperate permissions from the top level command. | ||||
| ### NSFW Commands | |||||
| Commands can be limited to only age restricted channels and DMs: | |||||
| [!code-csharp[Nsfw-Permissions](samples/permissions/nsfw-permissions.cs)] | |||||
| [permissions]: xref:Discord.GuildPermission | [permissions]: xref:Discord.GuildPermission | ||||
| @@ -0,0 +1,6 @@ | |||||
| [NsfwCommand(true)] | |||||
| [SlashCommand("beautiful-code", "Get an image of perfect code")] | |||||
| public async Task BeautifulCodeAsync(...) | |||||
| { | |||||
| ... | |||||
| } | |||||
| @@ -13,6 +13,7 @@ namespace Discord.Audio | |||||
| event Func<ulong, AudioInStream, Task> StreamCreated; | event Func<ulong, AudioInStream, Task> StreamCreated; | ||||
| event Func<ulong, Task> StreamDestroyed; | event Func<ulong, Task> StreamDestroyed; | ||||
| event Func<ulong, bool, Task> SpeakingUpdated; | event Func<ulong, bool, Task> SpeakingUpdated; | ||||
| event Func<ulong, Task> ClientDisconnected; | |||||
| /// <summary> Gets the current connection state of this client. </summary> | /// <summary> Gets the current connection state of this client. </summary> | ||||
| ConnectionState ConnectionState { get; } | ConnectionState ConnectionState { get; } | ||||
| @@ -94,10 +94,10 @@ namespace Discord | |||||
| MaxNumberOfDailyApplicationCommandCreatesHasBeenReached = 30034, | MaxNumberOfDailyApplicationCommandCreatesHasBeenReached = 30034, | ||||
| MaximumBansForNonGuildMembersReached = 30035, | MaximumBansForNonGuildMembersReached = 30035, | ||||
| MaximumBanFetchesReached = 30037, | MaximumBanFetchesReached = 30037, | ||||
| MaximumUncompleteGuildScheduledEvents = 30038, | |||||
| MaximumUncompletedGuildScheduledEvents = 30038, | |||||
| MaximumStickersReached = 30039, | MaximumStickersReached = 30039, | ||||
| MaximumPruneRequestReached = 30040, | MaximumPruneRequestReached = 30040, | ||||
| MaximumGuildWigitsReached = 30042, | |||||
| MaximumGuildWidgetsReached = 30042, | |||||
| #endregion | #endregion | ||||
| #region General Request Errors (40XXX) | #region General Request Errors (40XXX) | ||||
| @@ -116,24 +116,24 @@ namespace Discord | |||||
| TargetUserNotInVoice = 40032, | TargetUserNotInVoice = 40032, | ||||
| MessageAlreadyCrossposted = 40033, | MessageAlreadyCrossposted = 40033, | ||||
| ApplicationNameAlreadyExists = 40041, | ApplicationNameAlreadyExists = 40041, | ||||
| #endregion | |||||
| #region Action Preconditions/Checks (50XXX) | |||||
| ApplicationInteractionFailedToSend = 40043, | ApplicationInteractionFailedToSend = 40043, | ||||
| CannotSendAMessageInAForumChannel = 40058, | CannotSendAMessageInAForumChannel = 40058, | ||||
| ThereAreNoTagsAvailableThatCanBeSetByNonModerators = 40066, | ThereAreNoTagsAvailableThatCanBeSetByNonModerators = 40066, | ||||
| ATagIsRequiredToCreateAForumPostInThisChannel = 40067, | ATagIsRequiredToCreateAForumPostInThisChannel = 40067, | ||||
| InteractionHasAlreadyBeenAcknowledged = 40060, | InteractionHasAlreadyBeenAcknowledged = 40060, | ||||
| TagNamesMustBeUnique = 40061, | TagNamesMustBeUnique = 40061, | ||||
| #endregion | |||||
| #region Action Preconditions/Checks (50XXX) | |||||
| MissingPermissions = 50001, | MissingPermissions = 50001, | ||||
| InvalidAccountType = 50002, | InvalidAccountType = 50002, | ||||
| CannotExecuteForDM = 50003, | CannotExecuteForDM = 50003, | ||||
| GuildWigitDisabled = 50004, | |||||
| GuildWidgetDisabled = 50004, | |||||
| CannotEditOtherUsersMessage = 50005, | CannotEditOtherUsersMessage = 50005, | ||||
| CannotSendEmptyMessage = 50006, | CannotSendEmptyMessage = 50006, | ||||
| CannotSendMessageToUser = 50007, | CannotSendMessageToUser = 50007, | ||||
| CannotSendMessageToVoiceChannel = 50008, | CannotSendMessageToVoiceChannel = 50008, | ||||
| ChannelVerificationTooHight = 50009, | |||||
| ChannelVerificationTooHigh = 50009, | |||||
| OAuth2ApplicationDoesntHaveBot = 50010, | OAuth2ApplicationDoesntHaveBot = 50010, | ||||
| OAuth2ApplicationLimitReached = 50011, | OAuth2ApplicationLimitReached = 50011, | ||||
| InvalidOAuth2State = 50012, | InvalidOAuth2State = 50012, | ||||
| @@ -154,6 +154,7 @@ namespace Discord | |||||
| BulkDeleteMessageTooOld = 50034, | BulkDeleteMessageTooOld = 50034, | ||||
| InvalidFormBody = 50035, | InvalidFormBody = 50035, | ||||
| InviteAcceptedForGuildThatBotIsntIn = 50036, | InviteAcceptedForGuildThatBotIsntIn = 50036, | ||||
| InvalidActivityAction = 50039, | |||||
| InvalidAPIVersion = 50041, | InvalidAPIVersion = 50041, | ||||
| FileUploadTooBig = 50045, | FileUploadTooBig = 50045, | ||||
| InvalidFileUpload = 50046, | InvalidFileUpload = 50046, | ||||
| @@ -161,6 +162,7 @@ namespace Discord | |||||
| InvalidGuild = 50055, | InvalidGuild = 50055, | ||||
| InvalidMessageType = 50068, | InvalidMessageType = 50068, | ||||
| PaymentSourceRequiredForGift = 50070, | PaymentSourceRequiredForGift = 50070, | ||||
| CannotModifySystemWebhook = 50073, | |||||
| CannotDeleteRequiredCommunityChannel = 50074, | CannotDeleteRequiredCommunityChannel = 50074, | ||||
| CannotEditStickersWithinAMessage = 50080, | CannotEditStickersWithinAMessage = 50080, | ||||
| InvalidSticker = 50081, | InvalidSticker = 50081, | ||||
| @@ -172,10 +174,10 @@ namespace Discord | |||||
| ServerRequiresMonetization = 50097, | ServerRequiresMonetization = 50097, | ||||
| ServerRequiresBoosts = 50101, | ServerRequiresBoosts = 50101, | ||||
| RequestBodyContainsInvalidJSON = 50109, | RequestBodyContainsInvalidJSON = 50109, | ||||
| FailedToResizeAssetBelowTheMaximumSize = 50138, | |||||
| OwnershipCannotBeTransferredToABotUser = 50132, | OwnershipCannotBeTransferredToABotUser = 50132, | ||||
| AssetResizeBelowTheMaximumSize= 50138, | |||||
| FailedToResizeAssetBelowTheMaximumSize = 50138, | |||||
| UploadedFileNotFound = 50146, | UploadedFileNotFound = 50146, | ||||
| FeatureInProcessOfRollingOut = 50155, | |||||
| MissingPermissionToSendThisSticker = 50600, | MissingPermissionToSendThisSticker = 50600, | ||||
| #endregion | #endregion | ||||
| @@ -213,8 +215,8 @@ namespace Discord | |||||
| LottieCantContainRasters = 170002, | LottieCantContainRasters = 170002, | ||||
| StickerMaximumFramerateExceeded = 170003, | StickerMaximumFramerateExceeded = 170003, | ||||
| StickerMaximumFrameCountExceeded = 170004, | StickerMaximumFrameCountExceeded = 170004, | ||||
| LottieMaximumDimentionsExceeded = 170005, | |||||
| StickerFramerateBoundsExceeed = 170006, | |||||
| LottieMaximumDimensionsExceeded = 170005, | |||||
| StickerFramerateBoundsExceeded = 170006, | |||||
| StickerAnimationDurationTooLong = 170007, | StickerAnimationDurationTooLong = 170007, | ||||
| #endregion | #endregion | ||||
| @@ -222,7 +224,7 @@ namespace Discord | |||||
| CannotUpdateFinishedEvent = 180000, | CannotUpdateFinishedEvent = 180000, | ||||
| FailedStageCreation = 180002, | FailedStageCreation = 180002, | ||||
| #endregion | #endregion | ||||
| #region Forum & Automod | #region Forum & Automod | ||||
| MessageWasBlockedByAutomaticModeration = 200000, | MessageWasBlockedByAutomaticModeration = 200000, | ||||
| TitleWasBlockedByAutomaticModeration = 200001, | TitleWasBlockedByAutomaticModeration = 200001, | ||||
| @@ -4,20 +4,31 @@ using System.Linq; | |||||
| using System.Text; | using System.Text; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| namespace Discord | |||||
| namespace Discord; | |||||
| /// <summary> | |||||
| /// Represents public flags for an application. | |||||
| /// </summary> | |||||
| public enum ApplicationFlags | |||||
| { | { | ||||
| /// <summary> | |||||
| /// Represents public flags for an application. | |||||
| /// </summary> | |||||
| public enum ApplicationFlags | |||||
| { | |||||
| GatewayPresence = 1 << 12, | |||||
| GatewayPresenceLimited = 1 << 13, | |||||
| GatewayGuildMembers = 1 << 14, | |||||
| GatewayGuildMembersLimited = 1 << 15, | |||||
| VerificationPendingGuildLimit = 1 << 16, | |||||
| Embedded = 1 << 17, | |||||
| GatewayMessageContent = 1 << 18, | |||||
| GatewayMessageContentLimited = 1 << 19 | |||||
| } | |||||
| GatewayPresence = 1 << 12, | |||||
| GatewayPresenceLimited = 1 << 13, | |||||
| GatewayGuildMembers = 1 << 14, | |||||
| GatewayGuildMembersLimited = 1 << 15, | |||||
| VerificationPendingGuildLimit = 1 << 16, | |||||
| Embedded = 1 << 17, | |||||
| GatewayMessageContent = 1 << 18, | |||||
| GatewayMessageContentLimited = 1 << 19, | |||||
| ApplicationCommandBadge = 1 << 23, | |||||
| ActiveApplication = 1 << 24 | |||||
| } | } | ||||
| @@ -0,0 +1,22 @@ | |||||
| namespace Discord; | |||||
| /// <summary> | |||||
| /// Represents public flags for a channel. | |||||
| /// </summary> | |||||
| public enum ChannelFlags | |||||
| { | |||||
| /// <summary> | |||||
| /// Default value for flags, when none are given to a channel. | |||||
| /// </summary> | |||||
| None = 0, | |||||
| /// <summary> | |||||
| /// Flag given to a thread channel pinned on top of parent forum channel. | |||||
| /// </summary> | |||||
| Pinned = 1 << 1, | |||||
| /// <summary> | |||||
| /// Flag given to a forum channel that requires people to select tags when posting. | |||||
| /// </summary> | |||||
| RequireTag = 1 << 4 | |||||
| } | |||||
| @@ -0,0 +1,65 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| namespace Discord; | |||||
| public class ForumChannelProperties : TextChannelProperties | |||||
| { | |||||
| /// <summary> | |||||
| /// Gets or sets the topic of the channel. | |||||
| /// </summary> | |||||
| /// <remarks> | |||||
| /// Not available in forum channels. | |||||
| /// </remarks> | |||||
| public new Optional<int> SlowModeInterval { get; } | |||||
| /// <summary> | |||||
| /// Gets or sets rate limit on creating posts in this forum channel. | |||||
| /// </summary> | |||||
| /// <remarks> | |||||
| /// Setting this value to anything above zero will require each user to wait X seconds before | |||||
| /// creating another thread; setting this value to <c>0</c> will disable rate limits for this channel. | |||||
| /// <note> | |||||
| /// Users with <see cref="Discord.ChannelPermission.ManageMessages"/> or | |||||
| /// <see cref="ChannelPermission.ManageChannels"/> will be exempt from rate limits. | |||||
| /// </note> | |||||
| /// </remarks> | |||||
| /// <exception cref="ArgumentOutOfRangeException">Thrown if the value does not fall within [0, 21600].</exception> | |||||
| public Optional<int> ThreadCreationInterval { get; set; } | |||||
| /// <summary> | |||||
| /// Gets or sets the default slow-mode for threads in this channel. | |||||
| /// </summary> | |||||
| /// <remarks> | |||||
| /// Setting this value to anything above zero will require each user to wait X seconds before | |||||
| /// sending another message; setting this value to <c>0</c> will disable slow-mode for child threads. | |||||
| /// <note> | |||||
| /// Users with <see cref="Discord.ChannelPermission.ManageMessages"/> or | |||||
| /// <see cref="ChannelPermission.ManageChannels"/> will be exempt from slow-mode. | |||||
| /// </note> | |||||
| /// </remarks> | |||||
| /// <exception cref="ArgumentOutOfRangeException">Thrown if the value does not fall within [0, 21600].</exception> | |||||
| public Optional<int> DefaultSlowModeInterval { get; set; } | |||||
| /// <summary> | |||||
| /// Gets or sets a collection of tags inside of this forum channel. | |||||
| /// </summary> | |||||
| public Optional<IEnumerable<ForumTagProperties>> Tags { get; set; } | |||||
| /// <summary> | |||||
| /// Gets or sets a new default reaction emoji in this forum channel. | |||||
| /// </summary> | |||||
| public Optional<IEmote> DefaultReactionEmoji { get; set; } | |||||
| /// <summary> | |||||
| /// Gets or sets the rule used to order posts in forum channels. | |||||
| /// </summary> | |||||
| public Optional<ForumSortOrder> DefaultSortOrder { get; set; } | |||||
| /// <summary> | |||||
| /// Gets or sets the rule used to display posts in a forum channel. | |||||
| /// </summary> | |||||
| public Optional<ForumLayout> DefaultLayout { get; set; } | |||||
| } | |||||
| @@ -0,0 +1,22 @@ | |||||
| namespace Discord; | |||||
| /// <summary> | |||||
| /// Represents the layout type used to display posts in a forum channel. | |||||
| /// </summary> | |||||
| public enum ForumLayout | |||||
| { | |||||
| /// <summary> | |||||
| /// A preferred forum layout hasn't been set by a server admin | |||||
| /// </summary> | |||||
| Default = 0, | |||||
| /// <summary> | |||||
| /// List View: display forum posts in a text-focused list | |||||
| /// </summary> | |||||
| List = 1, | |||||
| /// <summary> | |||||
| /// Gallery View: display forum posts in a media-focused gallery | |||||
| /// </summary> | |||||
| Grid = 2 | |||||
| } | |||||
| @@ -0,0 +1,17 @@ | |||||
| namespace Discord; | |||||
| /// <summary> | |||||
| /// Defines the rule used to order posts in forum channels. | |||||
| /// </summary> | |||||
| public enum ForumSortOrder | |||||
| { | |||||
| /// <summary> | |||||
| /// Sort forum posts by activity. | |||||
| /// </summary> | |||||
| LatestActivity = 0, | |||||
| /// <summary> | |||||
| /// Sort forum posts by creation time (from most recent to oldest). | |||||
| /// </summary> | |||||
| CreationDate = 1 | |||||
| } | |||||
| @@ -36,5 +36,10 @@ namespace Discord | |||||
| /// Gets or sets the permission overwrites for this channel. | /// Gets or sets the permission overwrites for this channel. | ||||
| /// </summary> | /// </summary> | ||||
| public Optional<IEnumerable<Overwrite>> PermissionOverwrites { get; set; } | public Optional<IEnumerable<Overwrite>> PermissionOverwrites { get; set; } | ||||
| /// <summary> | |||||
| /// Gets or sets the flags of the channel. | |||||
| /// </summary> | |||||
| public Optional<ChannelFlags> Flags { get; set; } | |||||
| } | } | ||||
| } | } | ||||
| @@ -7,7 +7,7 @@ using System.Threading.Tasks; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| public interface IForumChannel : IGuildChannel, IMentionable | |||||
| public interface IForumChannel : IGuildChannel, IMentionable, INestedChannel | |||||
| { | { | ||||
| /// <summary> | /// <summary> | ||||
| /// Gets a value that indicates whether the channel is NSFW. | /// Gets a value that indicates whether the channel is NSFW. | ||||
| @@ -35,6 +35,60 @@ namespace Discord | |||||
| /// </summary> | /// </summary> | ||||
| IReadOnlyCollection<ForumTag> Tags { get; } | IReadOnlyCollection<ForumTag> Tags { get; } | ||||
| /// <summary> | |||||
| /// Gets the current rate limit on creating posts in this forum channel. | |||||
| /// </summary> | |||||
| /// <returns> | |||||
| /// An <see cref="int"/> representing the time in seconds required before the user can send another | |||||
| /// message; <c>0</c> if disabled. | |||||
| /// </returns> | |||||
| int ThreadCreationInterval { get; } | |||||
| /// <summary> | |||||
| /// Gets the current default slow-mode delay for threads in this forum channel. | |||||
| /// </summary> | |||||
| /// <returns> | |||||
| /// An <see cref="int"/> representing the time in seconds required before the user can send another | |||||
| /// message; <c>0</c> if disabled. | |||||
| /// </returns> | |||||
| int DefaultSlowModeInterval { get; } | |||||
| /// <summary> | |||||
| /// Gets the emoji to show in the add reaction button on a thread in a forum channel | |||||
| /// </summary> | |||||
| /// <remarks> | |||||
| /// If the emoji is <see cref="Emote"/> only the <see cref="Emote.Id"/> will be populated. | |||||
| /// Use <see cref="IGuild.GetEmoteAsync"/> to get the emoji. | |||||
| /// </remarks> | |||||
| IEmote DefaultReactionEmoji { get; } | |||||
| /// <summary> | |||||
| /// Gets the rule used to order posts in forum channels. | |||||
| /// </summary> | |||||
| /// <remarks> | |||||
| /// Defaults to null, which indicates a preferred sort order hasn't been set | |||||
| /// </remarks> | |||||
| ForumSortOrder? DefaultSortOrder { get; } | |||||
| /// <summary> | |||||
| /// Gets the rule used to display posts in a forum channel. | |||||
| /// </summary> | |||||
| ForumLayout DefaultLayout { get; } | |||||
| /// <summary> | |||||
| /// Modifies this forum channel. | |||||
| /// </summary> | |||||
| /// <remarks> | |||||
| /// This method modifies the current forum channel with the specified properties. To see an example of this | |||||
| /// method and what properties are available, please refer to <see cref="ForumChannelProperties"/>. | |||||
| /// </remarks> | |||||
| /// <param name="func">The delegate containing the properties to modify the channel with.</param> | |||||
| /// <param name="options">The options to be used when sending the request.</param> | |||||
| /// <returns> | |||||
| /// A task that represents the asynchronous modification operation. | |||||
| /// </returns> | |||||
| Task ModifyAsync(Action<ForumChannelProperties> func, RequestOptions options = null); | |||||
| /// <summary> | /// <summary> | ||||
| /// Creates a new post (thread) within the forum. | /// Creates a new post (thread) within the forum. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -52,12 +106,13 @@ namespace Discord | |||||
| /// <param name="stickers">A collection of stickers to send with the message.</param> | /// <param name="stickers">A collection of stickers to send with the message.</param> | ||||
| /// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param> | /// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param> | ||||
| /// <param name="flags">A message flag to be applied to the sent message, only <see cref="MessageFlags.SuppressEmbeds"/> is permitted.</param> | /// <param name="flags">A message flag to be applied to the sent message, only <see cref="MessageFlags.SuppressEmbeds"/> is permitted.</param> | ||||
| /// <param name="tags">An array of <see cref="ForumTag"/> to be applied to the post.</param> | |||||
| /// <returns> | /// <returns> | ||||
| /// A task that represents the asynchronous creation operation. | /// A task that represents the asynchronous creation operation. | ||||
| /// </returns> | /// </returns> | ||||
| Task<IThreadChannel> CreatePostAsync(string title, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, | Task<IThreadChannel> CreatePostAsync(string title, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, | ||||
| string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, | string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, | ||||
| MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); | |||||
| MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null); | |||||
| /// <summary> | /// <summary> | ||||
| /// Creates a new post (thread) within the forum. | /// Creates a new post (thread) within the forum. | ||||
| @@ -78,13 +133,14 @@ namespace Discord | |||||
| /// <param name="stickers">A collection of stickers to send with the file.</param> | /// <param name="stickers">A collection of stickers to send with the file.</param> | ||||
| /// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param> | /// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param> | ||||
| /// <param name="flags">A message flag to be applied to the sent message, only <see cref="MessageFlags.SuppressEmbeds"/> is permitted.</param> | /// <param name="flags">A message flag to be applied to the sent message, only <see cref="MessageFlags.SuppressEmbeds"/> is permitted.</param> | ||||
| /// <param name="tags">An array of <see cref="ForumTag"/> to be applied to the post.</param> | |||||
| /// <returns> | /// <returns> | ||||
| /// A task that represents the asynchronous creation operation. | /// A task that represents the asynchronous creation operation. | ||||
| /// </returns> | /// </returns> | ||||
| Task<IThreadChannel> CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, | Task<IThreadChannel> CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, | ||||
| int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, | int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, | ||||
| AllowedMentions allowedMentions = null, MessageComponent components = null, | AllowedMentions allowedMentions = null, MessageComponent components = null, | ||||
| ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); | |||||
| ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null); | |||||
| /// <summary> | /// <summary> | ||||
| /// Creates a new post (thread) within the forum. | /// Creates a new post (thread) within the forum. | ||||
| @@ -106,13 +162,14 @@ namespace Discord | |||||
| /// <param name="stickers">A collection of stickers to send with the file.</param> | /// <param name="stickers">A collection of stickers to send with the file.</param> | ||||
| /// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param> | /// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param> | ||||
| /// <param name="flags">A message flag to be applied to the sent message, only <see cref="MessageFlags.SuppressEmbeds"/> is permitted.</param> | /// <param name="flags">A message flag to be applied to the sent message, only <see cref="MessageFlags.SuppressEmbeds"/> is permitted.</param> | ||||
| /// <param name="tags">An array of <see cref="ForumTag"/> to be applied to the post.</param> | |||||
| /// <returns> | /// <returns> | ||||
| /// A task that represents the asynchronous creation operation. | /// A task that represents the asynchronous creation operation. | ||||
| /// </returns> | /// </returns> | ||||
| public Task<IThreadChannel> CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, | public Task<IThreadChannel> CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, | ||||
| int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, | int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, | ||||
| AllowedMentions allowedMentions = null, MessageComponent components = null, | AllowedMentions allowedMentions = null, MessageComponent components = null, | ||||
| ISticker[] stickers = null, Embed[] embeds = null,MessageFlags flags = MessageFlags.None); | |||||
| ISticker[] stickers = null, Embed[] embeds = null,MessageFlags flags = MessageFlags.None, ForumTag[] tags = null); | |||||
| /// <summary> | /// <summary> | ||||
| /// Creates a new post (thread) within the forum. | /// Creates a new post (thread) within the forum. | ||||
| @@ -132,12 +189,13 @@ namespace Discord | |||||
| /// <param name="stickers">A collection of stickers to send with the file.</param> | /// <param name="stickers">A collection of stickers to send with the file.</param> | ||||
| /// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param> | /// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param> | ||||
| /// <param name="flags">A message flag to be applied to the sent message, only <see cref="MessageFlags.SuppressEmbeds"/> is permitted.</param> | /// <param name="flags">A message flag to be applied to the sent message, only <see cref="MessageFlags.SuppressEmbeds"/> is permitted.</param> | ||||
| /// <param name="tags">An array of <see cref="ForumTag"/> to be applied to the post.</param> | |||||
| /// <returns> | /// <returns> | ||||
| /// A task that represents the asynchronous creation operation. | /// A task that represents the asynchronous creation operation. | ||||
| /// </returns> | /// </returns> | ||||
| public Task<IThreadChannel> CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, | public Task<IThreadChannel> CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, | ||||
| int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, | int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, | ||||
| MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); | |||||
| MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null); | |||||
| /// <summary> | /// <summary> | ||||
| /// Creates a new post (thread) within the forum. | /// Creates a new post (thread) within the forum. | ||||
| @@ -155,14 +213,15 @@ namespace Discord | |||||
| /// </param> | /// </param> | ||||
| /// <param name="components">The message components to be included with this message. Used for interactions.</param> | /// <param name="components">The message components to be included with this message. Used for interactions.</param> | ||||
| /// <param name="stickers">A collection of stickers to send with the file.</param> | /// <param name="stickers">A collection of stickers to send with the file.</param> | ||||
| /// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param> | |||||
| /// <param name="embeds">An array of <see cref="Embed"/>s to send with this response. Max 10.</param> | |||||
| /// <param name="flags">A message flag to be applied to the sent message, only <see cref="MessageFlags.SuppressEmbeds"/> is permitted.</param> | /// <param name="flags">A message flag to be applied to the sent message, only <see cref="MessageFlags.SuppressEmbeds"/> is permitted.</param> | ||||
| /// <param name="tags">An array of <see cref="ForumTag"/> to be applied to the post.</param> | |||||
| /// <returns> | /// <returns> | ||||
| /// A task that represents the asynchronous creation operation. | /// A task that represents the asynchronous creation operation. | ||||
| /// </returns> | /// </returns> | ||||
| public Task<IThreadChannel> CreatePostWithFilesAsync(string title, IEnumerable<FileAttachment> attachments, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, | public Task<IThreadChannel> CreatePostWithFilesAsync(string title, IEnumerable<FileAttachment> attachments, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, | ||||
| int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, | int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, | ||||
| MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); | |||||
| MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null); | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets a collection of active threads within this forum channel. | /// Gets a collection of active threads within this forum channel. | ||||
| @@ -21,6 +21,17 @@ namespace Discord | |||||
| /// </returns> | /// </returns> | ||||
| int Position { get; } | int Position { get; } | ||||
| /// <summary> | |||||
| /// Gets the flags related to this channel. | |||||
| /// </summary> | |||||
| /// <remarks> | |||||
| /// This value is determined by bitwise OR-ing <see cref="ChannelFlags"/> values together. | |||||
| /// </remarks> | |||||
| /// <returns> | |||||
| /// A channel's flags, if any is associated. | |||||
| /// </returns> | |||||
| ChannelFlags Flags { get; } | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the guild associated with this channel. | /// Gets the guild associated with this channel. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -160,5 +160,15 @@ namespace Discord | |||||
| /// </returns> | /// </returns> | ||||
| Task<IThreadChannel> CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, | Task<IThreadChannel> CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, | ||||
| IMessage message = null, bool? invitable = null, int? slowmode = null, RequestOptions options = null); | IMessage message = null, bool? invitable = null, int? slowmode = null, RequestOptions options = null); | ||||
| /// <summary> | |||||
| /// Gets a collection of active threads within this channel. | |||||
| /// </summary> | |||||
| /// <param name="options">The options to be used when sending the request.</param> | |||||
| /// <returns> | |||||
| /// A task that represents an asynchronous get operation for retrieving the threads. The task result contains | |||||
| /// a collection of active threads. | |||||
| /// </returns> | |||||
| Task<IReadOnlyCollection<IThreadChannel>> GetActiveThreadsAsync(RequestOptions options = null); | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,4 +1,5 @@ | |||||
| using System; | using System; | ||||
| using System.Collections.Generic; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| namespace Discord | namespace Discord | ||||
| @@ -56,6 +57,14 @@ namespace Discord | |||||
| /// </remarks> | /// </remarks> | ||||
| bool? IsInvitable { get; } | bool? IsInvitable { get; } | ||||
| /// <summary> | |||||
| /// Gets ids of tags applied to a forum thread | |||||
| /// </summary> | |||||
| /// <remarks> | |||||
| /// This property is only available on forum threads. | |||||
| /// </remarks> | |||||
| IReadOnlyCollection<ulong> AppliedTags { get; } | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets when the thread was created. | /// Gets when the thread was created. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -65,6 +74,11 @@ namespace Discord | |||||
| /// </remarks> | /// </remarks> | ||||
| new DateTimeOffset CreatedAt { get; } | new DateTimeOffset CreatedAt { get; } | ||||
| /// <summary> | |||||
| /// Gets the id of the creator of the thread. | |||||
| /// </summary> | |||||
| ulong OwnerId { get; } | |||||
| /// <summary> | /// <summary> | ||||
| /// Joins the current thread. | /// Joins the current thread. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -102,5 +116,16 @@ namespace Discord | |||||
| /// A task that represents the asynchronous operation of removing a user from this thread. | /// A task that represents the asynchronous operation of removing a user from this thread. | ||||
| /// </returns> | /// </returns> | ||||
| Task RemoveUserAsync(IGuildUser user, RequestOptions options = null); | Task RemoveUserAsync(IGuildUser user, RequestOptions options = null); | ||||
| /// <summary> | |||||
| /// Modifies this thread channel. | |||||
| /// </summary> | |||||
| /// <param name="func">The delegate containing the properties to modify the channel with.</param> | |||||
| /// <param name="options">The options to be used when sending the request.</param> | |||||
| /// <returns> | |||||
| /// A task that represents the asynchronous modification operation. | |||||
| /// </returns> | |||||
| /// <seealso cref="ThreadChannelProperties"/> | |||||
| Task ModifyAsync(Action<ThreadChannelProperties> func, RequestOptions options = null); | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,4 +1,5 @@ | |||||
| using System; | using System; | ||||
| using System.Collections.Generic; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| @@ -39,20 +40,10 @@ namespace Discord | |||||
| /// <exception cref="ArgumentOutOfRangeException">Thrown if the value does not fall within [0, 21600].</exception> | /// <exception cref="ArgumentOutOfRangeException">Thrown if the value does not fall within [0, 21600].</exception> | ||||
| public Optional<int> SlowModeInterval { get; set; } | public Optional<int> SlowModeInterval { get; set; } | ||||
| /// <summary> | |||||
| /// Gets or sets whether or not the thread is archived. | |||||
| /// </summary> | |||||
| public Optional<bool> Archived { get; set; } | |||||
| /// <summary> | |||||
| /// Gets or sets whether or not the thread is locked. | |||||
| /// </summary> | |||||
| public Optional<bool> Locked { get; set; } | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets or sets the auto archive duration. | /// Gets or sets the auto archive duration. | ||||
| /// </summary> | /// </summary> | ||||
| public Optional<ThreadArchiveDuration> AutoArchiveDuration { get; set; } | public Optional<ThreadArchiveDuration> AutoArchiveDuration { get; set; } | ||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,26 @@ | |||||
| using System.Collections.Generic; | |||||
| namespace Discord; | |||||
| /// <summary> | |||||
| /// Provides properties that are used to modify an <see cref="IThreadChannel"/> with the specified changes. | |||||
| /// </summary> | |||||
| /// <seealso cref="IThreadChannel.ModifyAsync(System.Action{ThreadChannelProperties}, RequestOptions)"/> | |||||
| public class ThreadChannelProperties : TextChannelProperties | |||||
| { | |||||
| /// <summary> | |||||
| /// Gets or sets the tags applied to a forum thread | |||||
| /// </summary> | |||||
| public Optional<IEnumerable<ulong>> AppliedTags { get; set; } | |||||
| /// <summary> | |||||
| /// Gets or sets whether or not the thread is locked. | |||||
| /// </summary> | |||||
| public Optional<bool> Locked { get; set; } | |||||
| /// <summary> | |||||
| /// Gets or sets whether or not the thread is archived. | |||||
| /// </summary> | |||||
| public Optional<bool> Archived { get; set; } | |||||
| } | |||||
| @@ -1,42 +0,0 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| /// <summary> | |||||
| /// A struct representing a forum channel tag. | |||||
| /// </summary> | |||||
| public struct ForumTag | |||||
| { | |||||
| /// <summary> | |||||
| /// Gets the Id of the tag. | |||||
| /// </summary> | |||||
| public ulong Id { get; } | |||||
| /// <summary> | |||||
| /// Gets the name of the tag. | |||||
| /// </summary> | |||||
| public string Name { get; } | |||||
| /// <summary> | |||||
| /// Gets the emoji of the tag or <see langword="null"/> if none is set. | |||||
| /// </summary> | |||||
| public IEmote Emoji { get; } | |||||
| internal ForumTag(ulong id, string name, ulong? emojiId, string emojiName) | |||||
| { | |||||
| if (emojiId.HasValue && emojiId.Value != 0) | |||||
| Emoji = new Emote(emojiId.Value, emojiName, false); | |||||
| else if (emojiName != null) | |||||
| Emoji = new Emoji(name); | |||||
| else | |||||
| Emoji = null; | |||||
| Id = id; | |||||
| Name = name; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,67 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| #nullable enable | |||||
| namespace Discord | |||||
| { | |||||
| /// <summary> | |||||
| /// A struct representing a forum channel tag. | |||||
| /// </summary> | |||||
| public struct ForumTag : ISnowflakeEntity, IForumTag | |||||
| { | |||||
| /// <summary> | |||||
| /// Gets the Id of the tag. | |||||
| /// </summary> | |||||
| public ulong Id { get; } | |||||
| /// <inheritdoc/> | |||||
| public string Name { get; } | |||||
| /// <inheritdoc/> | |||||
| public IEmote? Emoji { get; } | |||||
| /// <inheritdoc/> | |||||
| public bool IsModerated { get; } | |||||
| /// <inheritdoc/> | |||||
| public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | |||||
| internal ForumTag(ulong id, string name, ulong? emojiId = null, string? emojiName = null, bool moderated = false) | |||||
| { | |||||
| if (emojiId.HasValue && emojiId.Value != 0) | |||||
| Emoji = new Emote(emojiId.Value, null, false); | |||||
| else if (emojiName != null) | |||||
| Emoji = new Emoji(emojiName); | |||||
| else | |||||
| Emoji = null; | |||||
| Id = id; | |||||
| Name = name; | |||||
| IsModerated = moderated; | |||||
| } | |||||
| public override int GetHashCode() => (Id, Name, Emoji, IsModerated).GetHashCode(); | |||||
| public override bool Equals(object? obj) | |||||
| => obj is ForumTag tag && Equals(tag); | |||||
| /// <summary> | |||||
| /// Gets whether supplied tag is equals to the current one. | |||||
| /// </summary> | |||||
| public bool Equals(ForumTag tag) | |||||
| => Id == tag.Id && | |||||
| Name == tag.Name && | |||||
| (Emoji is Emoji emoji && tag.Emoji is Emoji otherEmoji && emoji.Equals(otherEmoji) || | |||||
| Emoji is Emote emote && tag.Emoji is Emote otherEmote && emote.Equals(otherEmote)) && | |||||
| IsModerated == tag.IsModerated; | |||||
| public static bool operator ==(ForumTag? left, ForumTag? right) | |||||
| => left?.Equals(right) ?? right is null; | |||||
| public static bool operator !=(ForumTag? left, ForumTag? right) => !(left == right); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,191 @@ | |||||
| #nullable enable | |||||
| using System; | |||||
| namespace Discord; | |||||
| public class ForumTagBuilder | |||||
| { | |||||
| private string? _name; | |||||
| private IEmote? _emoji; | |||||
| private bool _moderated; | |||||
| private ulong? _id; | |||||
| /// <summary> | |||||
| /// Returns the maximum length of name allowed by Discord. | |||||
| /// </summary> | |||||
| public const int MaxNameLength = 20; | |||||
| /// <summary> | |||||
| /// Gets or sets the snowflake Id of the tag. | |||||
| /// </summary> | |||||
| /// <remarks> | |||||
| /// If set this will update existing tag or will create a new one otherwise. | |||||
| /// </remarks> | |||||
| public ulong? Id | |||||
| { | |||||
| get { return _id; } | |||||
| set { _id = value; } | |||||
| } | |||||
| /// <summary> | |||||
| /// Gets or sets the name of the tag. | |||||
| /// </summary> | |||||
| /// <exception cref="ArgumentException">Name length must be less than or equal to <see cref="MaxNameLength"/>.</exception> | |||||
| public string? Name | |||||
| { | |||||
| get { return _name; } | |||||
| set | |||||
| { | |||||
| if (value?.Length > MaxNameLength) | |||||
| throw new ArgumentException(message: $"Name length must be less than or equal to {MaxNameLength}.", paramName: nameof(Name)); | |||||
| _name = value; | |||||
| } | |||||
| } | |||||
| /// <summary> | |||||
| /// Gets or sets the emoji of the tag. | |||||
| /// </summary> | |||||
| public IEmote? Emoji | |||||
| { | |||||
| get { return _emoji; } | |||||
| set { _emoji = value; } | |||||
| } | |||||
| /// <summary> | |||||
| /// Gets or sets whether this tag can only be added to or removed from threads by a member | |||||
| /// with the <see cref="GuildPermissions.ManageThreads"/> permission | |||||
| /// </summary> | |||||
| public bool IsModerated | |||||
| { | |||||
| get { return _moderated; } | |||||
| set { _moderated = value; } | |||||
| } | |||||
| /// <summary> | |||||
| /// Initializes a new <see cref="ForumTagBuilder"/> class. | |||||
| /// </summary> | |||||
| public ForumTagBuilder() | |||||
| { | |||||
| } | |||||
| /// <summary> | |||||
| /// Initializes a new <see cref="ForumTagBuilder"/> class with values | |||||
| /// </summary> | |||||
| /// <param name="id"> If set existing tag will be updated or a new one will be created otherwise.</param> | |||||
| /// <param name="name"> Name of the tag.</param> | |||||
| /// <param name="isModerated"> Sets whether this tag can only be added to or removed from threads by a member | |||||
| /// with the <see cref="GuildPermissions.ManageThreads"/> permission. </param> | |||||
| public ForumTagBuilder(string name, ulong? id = null, bool isModerated = false) | |||||
| { | |||||
| Name = name; | |||||
| IsModerated = isModerated; | |||||
| Id = id; | |||||
| } | |||||
| /// <summary> | |||||
| /// Initializes a new <see cref="ForumTagBuilder"/> class with values | |||||
| /// </summary> | |||||
| /// <param name="name"> Name of the tag.</param> | |||||
| /// <param name="id"> If set existing tag will be updated or a new one will be created otherwise.</param> | |||||
| /// <param name="emoji"> Display emoji of the tag.</param> | |||||
| /// <param name="isModerated"> Sets whether this tag can only be added to or removed from threads by a member | |||||
| /// with the <see cref="GuildPermissions.ManageThreads"/> permission. </param> | |||||
| public ForumTagBuilder(string name, ulong? id = null, bool isModerated = false, IEmote? emoji = null) | |||||
| { | |||||
| Name = name; | |||||
| Emoji = emoji; | |||||
| IsModerated = isModerated; | |||||
| Id = id; | |||||
| } | |||||
| /// <summary> | |||||
| /// Initializes a new <see cref="ForumTagBuilder"/> class with values | |||||
| /// </summary> | |||||
| /// /// <param name="name"> Name of the tag.</param> | |||||
| /// <param name="id"> If set existing tag will be updated or a new one will be created otherwise.</param> | |||||
| /// <param name="emoteId"> The id of custom Display emoji of the tag.</param> | |||||
| /// <param name="isModerated"> Sets whether this tag can only be added to or removed from threads by a member | |||||
| /// with the <see cref="GuildPermissions.ManageThreads"/> permission </param> | |||||
| public ForumTagBuilder(string name, ulong? id = null, bool isModerated = false, ulong? emoteId = null) | |||||
| { | |||||
| Name = name; | |||||
| if(emoteId is not null) | |||||
| Emoji = new Emote(emoteId.Value, null, false); | |||||
| IsModerated = isModerated; | |||||
| Id = id; | |||||
| } | |||||
| /// <summary> | |||||
| /// Builds the Tag. | |||||
| /// </summary> | |||||
| /// <returns>An instance of <see cref="ForumTagProperties"/></returns> | |||||
| /// <exception cref="ArgumentNullException">"Name must be set to build the tag"</exception> | |||||
| public ForumTagProperties Build() | |||||
| { | |||||
| if (_name is null) | |||||
| throw new ArgumentNullException(nameof(Name), "Name must be set to build the tag"); | |||||
| return new ForumTagProperties(_name!, _emoji, _moderated); | |||||
| } | |||||
| /// <summary> | |||||
| /// Sets the name of the tag. | |||||
| /// </summary> | |||||
| /// <exception cref="ArgumentException">Name length must be less than or equal to <see cref="MaxNameLength"/>.</exception> | |||||
| public ForumTagBuilder WithName(string name) | |||||
| { | |||||
| Name = name; | |||||
| return this; | |||||
| } | |||||
| /// <summary> | |||||
| /// Sets the id of the tag. | |||||
| /// </summary> | |||||
| /// <param name="id"> If set existing tag will be updated or a new one will be created otherwise.</param> | |||||
| /// <exception cref="ArgumentException">Name length must be less than or equal to <see cref="MaxNameLength"/>.</exception> | |||||
| public ForumTagBuilder WithId(ulong? id) | |||||
| { | |||||
| Id = id; | |||||
| return this; | |||||
| } | |||||
| /// <summary> | |||||
| /// Sets the emoji of the tag. | |||||
| /// </summary> | |||||
| public ForumTagBuilder WithEmoji(IEmote? emoji) | |||||
| { | |||||
| Emoji = emoji; | |||||
| return this; | |||||
| } | |||||
| /// <summary> | |||||
| /// Sets whether this tag can only be added to or removed from threads by a member | |||||
| /// with the <see cref="GuildPermissions.ManageThreads"/> permission | |||||
| /// </summary> | |||||
| public ForumTagBuilder WithModerated(bool moderated) | |||||
| { | |||||
| IsModerated = moderated; | |||||
| return this; | |||||
| } | |||||
| public override int GetHashCode() => base.GetHashCode(); | |||||
| public override bool Equals(object? obj) | |||||
| => obj is ForumTagBuilder builder && Equals(builder); | |||||
| /// <summary> | |||||
| /// Gets whether supplied tag builder is equals to the current one. | |||||
| /// </summary> | |||||
| public bool Equals(ForumTagBuilder? builder) | |||||
| => builder is not null && | |||||
| Id == builder.Id && | |||||
| Name == builder.Name && | |||||
| (Emoji is Emoji emoji && builder.Emoji is Emoji otherEmoji && emoji.Equals(otherEmoji) || | |||||
| Emoji is Emote emote && builder.Emoji is Emote otherEmote && emote.Equals(otherEmote)) && | |||||
| IsModerated == builder.IsModerated; | |||||
| public static bool operator ==(ForumTagBuilder? left, ForumTagBuilder? right) | |||||
| => left?.Equals(right) ?? right is null ; | |||||
| public static bool operator !=(ForumTagBuilder? left, ForumTagBuilder? right) => !(left == right); | |||||
| } | |||||
| @@ -0,0 +1,11 @@ | |||||
| namespace Discord; | |||||
| public static class ForumTagBuilderExtensions | |||||
| { | |||||
| public static ForumTagBuilder ToForumTagBuilder(this ForumTag tag) | |||||
| => new ForumTagBuilder(tag.Name, tag.Id, tag.IsModerated, tag.Emoji); | |||||
| public static ForumTagBuilder ToForumTagBuilder(this ForumTagProperties tag) | |||||
| => new ForumTagBuilder(tag.Name, tag.Id, tag.IsModerated, tag.Emoji); | |||||
| } | |||||
| @@ -0,0 +1,48 @@ | |||||
| namespace Discord; | |||||
| #nullable enable | |||||
| public class ForumTagProperties : IForumTag | |||||
| { | |||||
| /// <summary> | |||||
| /// Gets the Id of the tag. | |||||
| /// </summary> | |||||
| public ulong Id { get; } | |||||
| /// <inheritdoc/> | |||||
| public string Name { get; } | |||||
| /// <inheritdoc/> | |||||
| public IEmote? Emoji { get; } | |||||
| /// <inheritdoc/> | |||||
| public bool IsModerated { get; } | |||||
| internal ForumTagProperties(string name, IEmote? emoji = null, bool isMmoderated = false) | |||||
| { | |||||
| Name = name; | |||||
| Emoji = emoji; | |||||
| IsModerated = isMmoderated; | |||||
| } | |||||
| public override int GetHashCode() => (Id, Name, Emoji, IsModerated).GetHashCode(); | |||||
| public override bool Equals(object? obj) | |||||
| => obj is ForumTagProperties tag && Equals(tag); | |||||
| /// <summary> | |||||
| /// Gets whether supplied tag is equals to the current one. | |||||
| /// </summary> | |||||
| public bool Equals(ForumTagProperties? tag) | |||||
| => tag is not null && | |||||
| Id == tag.Id && | |||||
| Name == tag.Name && | |||||
| (Emoji is Emoji emoji && tag.Emoji is Emoji otherEmoji && emoji.Equals(otherEmoji) || | |||||
| Emoji is Emote emote && tag.Emoji is Emote otherEmote && emote.Equals(otherEmote)) && | |||||
| IsModerated == tag.IsModerated; | |||||
| public static bool operator ==(ForumTagProperties? left, ForumTagProperties? right) | |||||
| => left?.Equals(right) ?? right is null; | |||||
| public static bool operator !=(ForumTagProperties? left, ForumTagProperties? right) => !(left == right); | |||||
| } | |||||
| @@ -0,0 +1,29 @@ | |||||
| namespace Discord; | |||||
| #nullable enable | |||||
| /// <summary> | |||||
| /// Represents a Discord forum tag | |||||
| /// </summary> | |||||
| public interface IForumTag | |||||
| { | |||||
| /// <summary> | |||||
| /// Gets the name of the tag. | |||||
| /// </summary> | |||||
| string Name { get; } | |||||
| /// <summary> | |||||
| /// Gets the emoji of the tag or <see langword="null"/> if none is set. | |||||
| /// </summary> | |||||
| /// <remarks> | |||||
| /// If the emoji is <see cref="Emote"/> only the <see cref="Emote.Id"/> will be populated. | |||||
| /// Use <see cref="IGuild.GetEmoteAsync"/> to get the emoji. | |||||
| /// </remarks> | |||||
| IEmote? Emoji { get; } | |||||
| /// <summary> | |||||
| /// Gets whether this tag can only be added to or removed from threads by a member | |||||
| /// with the <see cref="GuildPermissions.ManageThreads"/> permission | |||||
| /// </summary> | |||||
| bool IsModerated { get; } | |||||
| } | |||||
| @@ -181,5 +181,9 @@ namespace Discord | |||||
| /// The guild has enabled the welcome screen. | /// The guild has enabled the welcome screen. | ||||
| /// </summary> | /// </summary> | ||||
| WelcomeScreenEnabled = 1L << 41, | WelcomeScreenEnabled = 1L << 41, | ||||
| /// <summary> | |||||
| /// The guild has been set as a support server on the App Directory. | |||||
| /// </summary> | |||||
| DeveloperSupportServer = 1L << 42, | |||||
| } | } | ||||
| } | } | ||||
| @@ -761,6 +761,18 @@ namespace Discord | |||||
| /// </returns> | /// </returns> | ||||
| Task<ICategoryChannel> CreateCategoryAsync(string name, Action<GuildChannelProperties> func = null, RequestOptions options = null); | Task<ICategoryChannel> CreateCategoryAsync(string name, Action<GuildChannelProperties> func = null, RequestOptions options = null); | ||||
| /// <summary> | |||||
| /// Creates a new channel forum in this guild. | |||||
| /// </summary> | |||||
| /// <param name="name">The new name for the forum.</param> | |||||
| /// <param name="func">The delegate containing the properties to be applied to the channel upon its creation.</param> | |||||
| /// <param name="options">The options to be used when sending the request.</param> | |||||
| /// <returns> | |||||
| /// A task that represents the asynchronous creation operation. The task result contains the newly created | |||||
| /// forum channel. | |||||
| /// </returns> | |||||
| Task<IForumChannel> CreateForumChannelAsync(string name, Action<ForumChannelProperties> func = null, RequestOptions options = null); | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets a collection of all the voice regions this guild can access. | /// Gets a collection of all the voice regions this guild can access. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -1239,5 +1251,21 @@ namespace Discord | |||||
| /// </returns> | /// </returns> | ||||
| Task<IReadOnlyCollection<IApplicationCommand>> BulkOverwriteApplicationCommandsAsync(ApplicationCommandProperties[] properties, | Task<IReadOnlyCollection<IApplicationCommand>> BulkOverwriteApplicationCommandsAsync(ApplicationCommandProperties[] properties, | ||||
| RequestOptions options = null); | RequestOptions options = null); | ||||
| /// <summary> | |||||
| /// Gets the welcome screen of the guild. Returns <see langword="null"/> if the welcome channel is not set. | |||||
| /// </summary> | |||||
| /// <returns> | |||||
| /// A task that represents the asynchronous creation operation. The task result contains a <see cref="WelcomeScreen"/>. | |||||
| /// </returns> | |||||
| Task<WelcomeScreen> GetWelcomeScreenAsync(RequestOptions options = null); | |||||
| /// <summary> | |||||
| /// Modifies the welcome screen of the guild. Returns <see langword="null"/> if welcome screen is removed. | |||||
| /// </summary> | |||||
| /// <returns> | |||||
| /// A task that represents the asynchronous creation operation. The task result contains a <see cref="WelcomeScreen"/>. | |||||
| /// </returns> | |||||
| Task<WelcomeScreen> ModifyWelcomeScreenAsync(bool enabled, WelcomeScreenChannelProperties[] channels, string description = null, RequestOptions options = null); | |||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,24 @@ | |||||
| using System.Collections.Generic; | |||||
| using System.Collections.Immutable; | |||||
| namespace Discord; | |||||
| public class WelcomeScreen | |||||
| { | |||||
| /// <summary> | |||||
| /// Gets the server description shown in the welcome screen. <see langword="null"/> if not set. | |||||
| /// </summary> | |||||
| public string Description { get; } | |||||
| /// <summary> | |||||
| /// Gets the channels shown in the welcome screen, up to 5 channels. | |||||
| /// </summary> | |||||
| public IReadOnlyCollection<WelcomeScreenChannel> Channels { get; } | |||||
| internal WelcomeScreen(string description, IReadOnlyCollection<WelcomeScreenChannel> channels) | |||||
| { | |||||
| Description = description; | |||||
| Channels = channels.ToImmutableArray(); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,41 @@ | |||||
| using System; | |||||
| namespace Discord; | |||||
| public class WelcomeScreenChannel : ISnowflakeEntity | |||||
| { | |||||
| /// <summary> | |||||
| /// Gets the channel's id. | |||||
| /// </summary> | |||||
| public ulong Id { get; } | |||||
| /// <summary> | |||||
| /// Gets the description shown for the channel. | |||||
| /// </summary> | |||||
| public string Description { get; } | |||||
| /// <summary> | |||||
| /// Gets the emoji for this channel. <see cref="Emoji"/> if it is unicode emoji, <see cref="Emote"/> if it is a custom one and <see langword="null"/> if none is set. | |||||
| /// </summary> | |||||
| /// <remarks> | |||||
| /// If the emoji is <see cref="Emote"/> only the <see cref="Emote.Id"/> will be populated. | |||||
| /// Use <see cref="IGuild.GetEmoteAsync"/> to get the emoji. | |||||
| /// </remarks> | |||||
| public IEmote Emoji { get; } | |||||
| /// <inheritdoc/> | |||||
| public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | |||||
| internal WelcomeScreenChannel(ulong id, string description, string emojiName = null, ulong? emoteId = null) | |||||
| { | |||||
| Id = id; | |||||
| Description = description; | |||||
| if (emoteId.HasValue && emoteId.Value != 0) | |||||
| Emoji = new Emote(emoteId.Value, emojiName, false); | |||||
| else if (emojiName != null) | |||||
| Emoji = new Emoji(emojiName); | |||||
| else | |||||
| Emoji = null; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,54 @@ | |||||
| using System; | |||||
| using System.Xml.Linq; | |||||
| namespace Discord; | |||||
| public class WelcomeScreenChannelProperties : ISnowflakeEntity | |||||
| { | |||||
| /// <summary> | |||||
| /// Gets or sets the channel's id. | |||||
| /// </summary> | |||||
| public ulong Id { get; set; } | |||||
| /// <summary> | |||||
| /// Gets or sets the description shown for the channel. | |||||
| /// </summary> | |||||
| public string Description { get; set; } | |||||
| /// <summary> | |||||
| /// Gets or sets the emoji for this channel. <see cref="Emoji"/> if it is unicode emoji, <see cref="Emote"/> if it is a custom one and <see langword="null"/> if none is set. | |||||
| /// </summary> | |||||
| /// <remarks> | |||||
| /// If the emoji is <see cref="Emote"/> only the <see cref="Emote.Id"/> will be populated. | |||||
| /// Use <see cref="IGuild.GetEmoteAsync"/> to get the emoji. | |||||
| /// </remarks> | |||||
| public IEmote Emoji { get; set; } | |||||
| /// <inheritdoc/> | |||||
| public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | |||||
| /// <summary> | |||||
| /// Initializes a new instance of <see cref="WelcomeScreenChannelProperties"/>. | |||||
| /// </summary> | |||||
| /// <param name="id">Id if a channel.</param> | |||||
| /// <param name="description">Description for the channel in the welcome screen.</param> | |||||
| /// <param name="emoji">The emoji for the channel in the welcome screen.</param> | |||||
| public WelcomeScreenChannelProperties(ulong id, string description, IEmote emoji = null) | |||||
| { | |||||
| Id = id; | |||||
| Description = description; | |||||
| Emoji = emoji; | |||||
| } | |||||
| /// <summary> | |||||
| /// Initializes a new instance of <see cref="WelcomeScreenChannelProperties"/>. | |||||
| /// </summary> | |||||
| public WelcomeScreenChannelProperties() { } | |||||
| /// <summary> | |||||
| /// Initializes a new instance of <see cref="WelcomeScreenChannelProperties"/>. | |||||
| /// </summary> | |||||
| /// <param name="channel">A welcome screen channel to modify.</param> | |||||
| /// <returns>A new instance of <see cref="WelcomeScreenChannelProperties"/>.</returns> | |||||
| public static WelcomeScreenChannelProperties FromWelcomeScreenChannel(WelcomeScreenChannel channel) | |||||
| => new (channel.Id, channel.Description, channel.Emoji); | |||||
| } | |||||
| @@ -21,7 +21,7 @@ namespace Discord | |||||
| String = 3, | String = 3, | ||||
| /// <summary> | /// <summary> | ||||
| /// An <see langword="int"/>. | |||||
| /// An <see langword="long"/>. | |||||
| /// </summary> | /// </summary> | ||||
| Integer = 4, | Integer = 4, | ||||
| @@ -83,6 +83,11 @@ namespace Discord | |||||
| /// </summary> | /// </summary> | ||||
| public Optional<bool> IsDMEnabled { get; set; } | public Optional<bool> IsDMEnabled { get; set; } | ||||
| /// <summary> | |||||
| /// Gets or sets whether or not this command is age restricted. | |||||
| /// </summary> | |||||
| public Optional<bool> IsNsfw { get; set; } | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets or sets the default permissions required by a user to execute this application command. | /// Gets or sets the default permissions required by a user to execute this application command. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -46,6 +46,11 @@ namespace Discord | |||||
| /// </summary> | /// </summary> | ||||
| public bool IsDMEnabled { get; set; } = true; | public bool IsDMEnabled { get; set; } = true; | ||||
| /// <summary> | |||||
| /// Gets or sets whether or not this command is age restricted. | |||||
| /// </summary> | |||||
| public bool IsNsfw{ get; set; } = false; | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets or sets the default permission required to use this slash command. | /// Gets or sets the default permission required to use this slash command. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -68,7 +73,8 @@ namespace Discord | |||||
| IsDefaultPermission = IsDefaultPermission, | IsDefaultPermission = IsDefaultPermission, | ||||
| IsDMEnabled = IsDMEnabled, | IsDMEnabled = IsDMEnabled, | ||||
| DefaultMemberPermissions = DefaultMemberPermissions ?? Optional<GuildPermission>.Unspecified, | DefaultMemberPermissions = DefaultMemberPermissions ?? Optional<GuildPermission>.Unspecified, | ||||
| NameLocalizations = NameLocalizations | |||||
| NameLocalizations = NameLocalizations, | |||||
| IsNsfw = IsNsfw, | |||||
| }; | }; | ||||
| return props; | return props; | ||||
| @@ -123,7 +129,7 @@ namespace Discord | |||||
| } | } | ||||
| /// <summary> | /// <summary> | ||||
| /// Sets whether or not this command can be used in dms | |||||
| /// Sets whether or not this command can be used in dms. | |||||
| /// </summary> | /// </summary> | ||||
| /// <param name="permission"><see langword="true"/> if the command is available in dms, otherwise <see langword="false"/>.</param> | /// <param name="permission"><see langword="true"/> if the command is available in dms, otherwise <see langword="false"/>.</param> | ||||
| /// <returns>The current builder.</returns> | /// <returns>The current builder.</returns> | ||||
| @@ -133,6 +139,17 @@ namespace Discord | |||||
| return this; | return this; | ||||
| } | } | ||||
| /// <summary> | |||||
| /// Sets whether or not this command is age restricted. | |||||
| /// </summary> | |||||
| /// <param name="permission"><see langword="true"/> if the command is age restricted, otherwise <see langword="false"/>.</param> | |||||
| /// <returns>The current builder.</returns> | |||||
| public MessageCommandBuilder WithNsfw(bool permission) | |||||
| { | |||||
| IsNsfw = permission; | |||||
| return this; | |||||
| } | |||||
| /// <summary> | /// <summary> | ||||
| /// Adds a new entry to the <see cref="NameLocalizations"/> collection. | /// Adds a new entry to the <see cref="NameLocalizations"/> collection. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -46,6 +46,11 @@ namespace Discord | |||||
| /// </summary> | /// </summary> | ||||
| public bool IsDMEnabled { get; set; } = true; | public bool IsDMEnabled { get; set; } = true; | ||||
| /// <summary> | |||||
| /// Gets or sets whether or not this command is age restricted. | |||||
| /// </summary> | |||||
| public bool IsNsfw { get; set; } = false; | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets or sets the default permission required to use this slash command. | /// Gets or sets the default permission required to use this slash command. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -66,7 +71,8 @@ namespace Discord | |||||
| IsDefaultPermission = IsDefaultPermission, | IsDefaultPermission = IsDefaultPermission, | ||||
| IsDMEnabled = IsDMEnabled, | IsDMEnabled = IsDMEnabled, | ||||
| DefaultMemberPermissions = DefaultMemberPermissions ?? Optional<GuildPermission>.Unspecified, | DefaultMemberPermissions = DefaultMemberPermissions ?? Optional<GuildPermission>.Unspecified, | ||||
| NameLocalizations = NameLocalizations | |||||
| NameLocalizations = NameLocalizations, | |||||
| IsNsfw = IsNsfw, | |||||
| }; | }; | ||||
| return props; | return props; | ||||
| @@ -121,7 +127,7 @@ namespace Discord | |||||
| } | } | ||||
| /// <summary> | /// <summary> | ||||
| /// Sets whether or not this command can be used in dms | |||||
| /// Sets whether or not this command can be used in dms. | |||||
| /// </summary> | /// </summary> | ||||
| /// <param name="permission"><see langword="true"/> if the command is available in dms, otherwise <see langword="false"/>.</param> | /// <param name="permission"><see langword="true"/> if the command is available in dms, otherwise <see langword="false"/>.</param> | ||||
| /// <returns>The current builder.</returns> | /// <returns>The current builder.</returns> | ||||
| @@ -131,6 +137,17 @@ namespace Discord | |||||
| return this; | return this; | ||||
| } | } | ||||
| /// <summary> | |||||
| /// Sets whether or not this command is age restricted. | |||||
| /// </summary> | |||||
| /// <param name="permission"><see langword="true"/> if the command is age restricted, otherwise <see langword="false"/>.</param> | |||||
| /// <returns>The current builder.</returns> | |||||
| public UserCommandBuilder WithNsfw(bool permission) | |||||
| { | |||||
| IsNsfw = permission; | |||||
| return this; | |||||
| } | |||||
| /// <summary> | /// <summary> | ||||
| /// Adds a new entry to the <see cref="NameLocalizations"/> collection. | /// Adds a new entry to the <see cref="NameLocalizations"/> collection. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -42,6 +42,11 @@ namespace Discord | |||||
| /// </remarks> | /// </remarks> | ||||
| bool IsEnabledInDm { get; } | bool IsEnabledInDm { get; } | ||||
| /// <summary> | |||||
| /// Indicates whether the command is age restricted. | |||||
| /// </summary> | |||||
| bool IsNsfw { get; } | |||||
| /// <summary> | /// <summary> | ||||
| /// Set of default <see cref="GuildPermission"/> required to invoke the command. | /// Set of default <see cref="GuildPermission"/> required to invoke the command. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -1,7 +1,7 @@ | |||||
| using Discord.Utils; | |||||
| using System; | using System; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Linq; | using System.Linq; | ||||
| using Discord.Utils; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| @@ -92,9 +92,11 @@ namespace Discord | |||||
| /// <param name="maxValues">The max values of the placeholder.</param> | /// <param name="maxValues">The max values of the placeholder.</param> | ||||
| /// <param name="disabled">Whether or not the menu is disabled.</param> | /// <param name="disabled">Whether or not the menu is disabled.</param> | ||||
| /// <param name="row">The row to add the menu to.</param> | /// <param name="row">The row to add the menu to.</param> | ||||
| /// <param name="type">The type of the select menu.</param> | |||||
| /// <param name="channelTypes">Menus valid channel types (only for <see cref="ComponentType.ChannelSelect"/>)</param> | |||||
| /// <returns></returns> | /// <returns></returns> | ||||
| public ComponentBuilder WithSelectMenu(string customId, List<SelectMenuOptionBuilder> options, | |||||
| string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false, int row = 0) | |||||
| public ComponentBuilder WithSelectMenu(string customId, List<SelectMenuOptionBuilder> options = null, | |||||
| string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false, int row = 0, ComponentType type = ComponentType.SelectMenu, ChannelType[] channelTypes = null) | |||||
| { | { | ||||
| return WithSelectMenu(new SelectMenuBuilder() | return WithSelectMenu(new SelectMenuBuilder() | ||||
| .WithCustomId(customId) | .WithCustomId(customId) | ||||
| @@ -102,7 +104,9 @@ namespace Discord | |||||
| .WithPlaceholder(placeholder) | .WithPlaceholder(placeholder) | ||||
| .WithMaxValues(maxValues) | .WithMaxValues(maxValues) | ||||
| .WithMinValues(minValues) | .WithMinValues(minValues) | ||||
| .WithDisabled(disabled), | |||||
| .WithDisabled(disabled) | |||||
| .WithType(type) | |||||
| .WithChannelTypes(channelTypes), | |||||
| row); | row); | ||||
| } | } | ||||
| @@ -118,7 +122,7 @@ namespace Discord | |||||
| public ComponentBuilder WithSelectMenu(SelectMenuBuilder menu, int row = 0) | public ComponentBuilder WithSelectMenu(SelectMenuBuilder menu, int row = 0) | ||||
| { | { | ||||
| Preconditions.LessThan(row, MaxActionRowCount, nameof(row)); | Preconditions.LessThan(row, MaxActionRowCount, nameof(row)); | ||||
| if (menu.Options.Distinct().Count() != menu.Options.Count) | |||||
| if (menu.Options is not null && menu.Options.Distinct().Count() != menu.Options.Count) | |||||
| throw new InvalidOperationException("Please make sure that there is no duplicates values."); | throw new InvalidOperationException("Please make sure that there is no duplicates values."); | ||||
| var builtMenu = menu.Build(); | var builtMenu = menu.Build(); | ||||
| @@ -278,9 +282,7 @@ namespace Discord | |||||
| { | { | ||||
| if (_actionRows?.SelectMany(x => x.Components)?.Any(x => x.Type == ComponentType.TextInput) ?? false) | if (_actionRows?.SelectMany(x => x.Components)?.Any(x => x.Type == ComponentType.TextInput) ?? false) | ||||
| throw new ArgumentException("TextInputComponents are not allowed in messages.", nameof(ActionRows)); | throw new ArgumentException("TextInputComponents are not allowed in messages.", nameof(ActionRows)); | ||||
| if (_actionRows?.SelectMany(x => x.Components)?.Any(x => x.Type == ComponentType.ModalSubmit) ?? false) | |||||
| throw new ArgumentException("ModalSubmit components are not allowed in messages.", nameof(ActionRows)); | |||||
| return _actionRows != null | return _actionRows != null | ||||
| ? new MessageComponent(_actionRows.Select(x => x.Build()).ToList()) | ? new MessageComponent(_actionRows.Select(x => x.Build()).ToList()) | ||||
| : MessageComponent.Empty; | : MessageComponent.Empty; | ||||
| @@ -356,10 +358,13 @@ namespace Discord | |||||
| /// <param name="placeholder">The placeholder of the menu.</param> | /// <param name="placeholder">The placeholder of the menu.</param> | ||||
| /// <param name="minValues">The min values of the placeholder.</param> | /// <param name="minValues">The min values of the placeholder.</param> | ||||
| /// <param name="maxValues">The max values of the placeholder.</param> | /// <param name="maxValues">The max values of the placeholder.</param> | ||||
| /// <param name="disabled">Whether or not the menu is disabled.</param> | |||||
| /// <returns>The current builder.</returns> | |||||
| public ActionRowBuilder WithSelectMenu(string customId, List<SelectMenuOptionBuilder> options, | |||||
| string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false) | |||||
| /// <param name="disabled">Whether or not the menu is disabled.</param> | |||||
| /// <param name="type">The type of the select menu.</param> | |||||
| /// <param name="channelTypes">Menus valid channel types (only for <see cref="ComponentType.ChannelSelect"/>)</param> | |||||
| /// <returns>The current builder.</returns> | |||||
| public ActionRowBuilder WithSelectMenu(string customId, List<SelectMenuOptionBuilder> options = null, | |||||
| string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false, | |||||
| ComponentType type = ComponentType.SelectMenu, ChannelType[] channelTypes = null) | |||||
| { | { | ||||
| return WithSelectMenu(new SelectMenuBuilder() | return WithSelectMenu(new SelectMenuBuilder() | ||||
| .WithCustomId(customId) | .WithCustomId(customId) | ||||
| @@ -367,7 +372,9 @@ namespace Discord | |||||
| .WithPlaceholder(placeholder) | .WithPlaceholder(placeholder) | ||||
| .WithMaxValues(maxValues) | .WithMaxValues(maxValues) | ||||
| .WithMinValues(minValues) | .WithMinValues(minValues) | ||||
| .WithDisabled(disabled)); | |||||
| .WithDisabled(disabled) | |||||
| .WithType(type) | |||||
| .WithChannelTypes(channelTypes)); | |||||
| } | } | ||||
| /// <summary> | /// <summary> | ||||
| @@ -378,7 +385,7 @@ namespace Discord | |||||
| /// <returns>The current builder.</returns> | /// <returns>The current builder.</returns> | ||||
| public ActionRowBuilder WithSelectMenu(SelectMenuBuilder menu) | public ActionRowBuilder WithSelectMenu(SelectMenuBuilder menu) | ||||
| { | { | ||||
| if (menu.Options.Distinct().Count() != menu.Options.Count) | |||||
| if (menu.Options is not null && menu.Options.Distinct().Count() != menu.Options.Count) | |||||
| throw new InvalidOperationException("Please make sure that there is no duplicates values."); | throw new InvalidOperationException("Please make sure that there is no duplicates values."); | ||||
| var builtMenu = menu.Build(); | var builtMenu = menu.Build(); | ||||
| @@ -431,10 +438,10 @@ namespace Discord | |||||
| { | { | ||||
| var builtButton = button.Build(); | var builtButton = button.Build(); | ||||
| if(Components.Count >= 5) | |||||
| if (Components.Count >= 5) | |||||
| throw new InvalidOperationException($"Components count reached {MaxChildCount}"); | throw new InvalidOperationException($"Components count reached {MaxChildCount}"); | ||||
| if (Components.Any(x => x.Type == ComponentType.SelectMenu)) | |||||
| if (Components.Any(x => x.Type.IsSelectType())) | |||||
| throw new InvalidOperationException($"A button cannot be added to a row with a SelectMenu"); | throw new InvalidOperationException($"A button cannot be added to a row with a SelectMenu"); | ||||
| AddComponent(builtButton); | AddComponent(builtButton); | ||||
| @@ -458,11 +465,15 @@ namespace Discord | |||||
| case ComponentType.ActionRow: | case ComponentType.ActionRow: | ||||
| return false; | return false; | ||||
| case ComponentType.Button: | case ComponentType.Button: | ||||
| if (Components.Any(x => x.Type == ComponentType.SelectMenu)) | |||||
| if (Components.Any(x => x.Type.IsSelectType())) | |||||
| return false; | return false; | ||||
| else | else | ||||
| return Components.Count < 5; | return Components.Count < 5; | ||||
| case ComponentType.SelectMenu: | case ComponentType.SelectMenu: | ||||
| case ComponentType.ChannelSelect: | |||||
| case ComponentType.MentionableSelect: | |||||
| case ComponentType.RoleSelect: | |||||
| case ComponentType.UserSelect: | |||||
| return Components.Count == 0; | return Components.Count == 0; | ||||
| default: | default: | ||||
| return false; | return false; | ||||
| @@ -759,6 +770,18 @@ namespace Discord | |||||
| }; | }; | ||||
| } | } | ||||
| /// <summary> | |||||
| /// Gets or sets the type of the current select menu. | |||||
| /// </summary> | |||||
| /// <exception cref="ArgumentException">Type must be a select menu type.</exception> | |||||
| public ComponentType Type | |||||
| { | |||||
| get => _type; | |||||
| set => _type = value.IsSelectType() | |||||
| ? value | |||||
| : throw new ArgumentException("Type must be a select menu type.", nameof(value)); | |||||
| } | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets or sets the placeholder text of the current select menu. | /// Gets or sets the placeholder text of the current select menu. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -815,8 +838,6 @@ namespace Discord | |||||
| { | { | ||||
| if (value != null) | if (value != null) | ||||
| Preconditions.AtMost(value.Count, MaxOptionCount, nameof(Options)); | Preconditions.AtMost(value.Count, MaxOptionCount, nameof(Options)); | ||||
| else | |||||
| throw new ArgumentNullException(nameof(value), $"{nameof(Options)} cannot be null."); | |||||
| _options = value; | _options = value; | ||||
| } | } | ||||
| @@ -827,11 +848,17 @@ namespace Discord | |||||
| /// </summary> | /// </summary> | ||||
| public bool IsDisabled { get; set; } | public bool IsDisabled { get; set; } | ||||
| /// <summary> | |||||
| /// Gets or sets the menu's channel types (only valid on <see cref="ComponentType.ChannelSelect"/>s). | |||||
| /// </summary> | |||||
| public List<ChannelType> ChannelTypes { get; set; } | |||||
| private List<SelectMenuOptionBuilder> _options = new List<SelectMenuOptionBuilder>(); | private List<SelectMenuOptionBuilder> _options = new List<SelectMenuOptionBuilder>(); | ||||
| private int _minValues = 1; | private int _minValues = 1; | ||||
| private int _maxValues = 1; | private int _maxValues = 1; | ||||
| private string _placeholder; | private string _placeholder; | ||||
| private string _customId; | private string _customId; | ||||
| private ComponentType _type = ComponentType.SelectMenu; | |||||
| /// <summary> | /// <summary> | ||||
| /// Creates a new instance of a <see cref="SelectMenuBuilder"/>. | /// Creates a new instance of a <see cref="SelectMenuBuilder"/>. | ||||
| @@ -862,7 +889,9 @@ namespace Discord | |||||
| /// <param name="maxValues">The max values of this select menu.</param> | /// <param name="maxValues">The max values of this select menu.</param> | ||||
| /// <param name="minValues">The min values of this select menu.</param> | /// <param name="minValues">The min values of this select menu.</param> | ||||
| /// <param name="isDisabled">Disabled this select menu or not.</param> | /// <param name="isDisabled">Disabled this select menu or not.</param> | ||||
| public SelectMenuBuilder(string customId, List<SelectMenuOptionBuilder> options, string placeholder = null, int maxValues = 1, int minValues = 1, bool isDisabled = false) | |||||
| /// <param name="type">The <see cref="ComponentType"/> of this select menu.</param> | |||||
| /// <param name="channelTypes">The types of channels this menu can select (only valid on <see cref="ComponentType.ChannelSelect"/>s)</param> | |||||
| public SelectMenuBuilder(string customId, List<SelectMenuOptionBuilder> options = null, string placeholder = null, int maxValues = 1, int minValues = 1, bool isDisabled = false, ComponentType type = ComponentType.SelectMenu, List<ChannelType> channelTypes = null) | |||||
| { | { | ||||
| CustomId = customId; | CustomId = customId; | ||||
| Options = options; | Options = options; | ||||
| @@ -870,6 +899,8 @@ namespace Discord | |||||
| IsDisabled = isDisabled; | IsDisabled = isDisabled; | ||||
| MaxValues = maxValues; | MaxValues = maxValues; | ||||
| MinValues = minValues; | MinValues = minValues; | ||||
| Type = type; | |||||
| ChannelTypes = channelTypes ?? new(); | |||||
| } | } | ||||
| /// <summary> | /// <summary> | ||||
| @@ -990,6 +1021,47 @@ namespace Discord | |||||
| return this; | return this; | ||||
| } | } | ||||
| /// <summary> | |||||
| /// Sets the menu's current type. | |||||
| /// </summary> | |||||
| /// <param name="type">The type of the menu.</param> | |||||
| /// <returns> | |||||
| /// The current builder. | |||||
| /// </returns> | |||||
| public SelectMenuBuilder WithType(ComponentType type) | |||||
| { | |||||
| Type = type; | |||||
| return this; | |||||
| } | |||||
| /// <summary> | |||||
| /// Sets the menus valid channel types (only for <see cref="ComponentType.ChannelSelect"/>s). | |||||
| /// </summary> | |||||
| /// <param name="channelTypes">The valid channel types of the menu.</param> | |||||
| /// <returns> | |||||
| /// The current builder. | |||||
| /// </returns> | |||||
| public SelectMenuBuilder WithChannelTypes(List<ChannelType> channelTypes) | |||||
| { | |||||
| ChannelTypes = channelTypes; | |||||
| return this; | |||||
| } | |||||
| /// <summary> | |||||
| /// Sets the menus valid channel types (only for <see cref="ComponentType.ChannelSelect"/>s). | |||||
| /// </summary> | |||||
| /// <param name="channelTypes">The valid channel types of the menu.</param> | |||||
| /// <returns> | |||||
| /// The current builder. | |||||
| /// </returns> | |||||
| public SelectMenuBuilder WithChannelTypes(params ChannelType[] channelTypes) | |||||
| { | |||||
| ChannelTypes = channelTypes is null | |||||
| ? ChannelTypeUtils.AllChannelTypes() | |||||
| : channelTypes.ToList(); | |||||
| return this; | |||||
| } | |||||
| /// <summary> | /// <summary> | ||||
| /// Builds a <see cref="SelectMenuComponent"/> | /// Builds a <see cref="SelectMenuComponent"/> | ||||
| /// </summary> | /// </summary> | ||||
| @@ -998,7 +1070,7 @@ namespace Discord | |||||
| { | { | ||||
| var options = Options?.Select(x => x.Build()).ToList(); | var options = Options?.Select(x => x.Build()).ToList(); | ||||
| return new SelectMenuComponent(CustomId, options, Placeholder, MinValues, MaxValues, IsDisabled); | |||||
| return new SelectMenuComponent(CustomId, options, Placeholder, MinValues, MaxValues, IsDisabled, Type, ChannelTypes); | |||||
| } | } | ||||
| } | } | ||||
| @@ -26,8 +26,23 @@ namespace Discord | |||||
| TextInput = 4, | TextInput = 4, | ||||
| /// <summary> | /// <summary> | ||||
| /// An interaction sent when a model is submitted. | |||||
| /// A select menu for picking from users. | |||||
| /// </summary> | /// </summary> | ||||
| ModalSubmit = 5, | |||||
| UserSelect = 5, | |||||
| /// <summary> | |||||
| /// A select menu for picking from roles. | |||||
| /// </summary> | |||||
| RoleSelect = 6, | |||||
| /// <summary> | |||||
| /// A select menu for picking from roles and users. | |||||
| /// </summary> | |||||
| MentionableSelect = 7, | |||||
| /// <summary> | |||||
| /// A select menu for picking from channels. | |||||
| /// </summary> | |||||
| ChannelSelect = 8, | |||||
| } | } | ||||
| } | } | ||||
| @@ -18,12 +18,32 @@ namespace Discord | |||||
| ComponentType Type { get; } | ComponentType Type { get; } | ||||
| /// <summary> | /// <summary> | ||||
| /// Gets the value(s) of a <see cref="SelectMenuComponent"/> interaction response. | |||||
| /// Gets the value(s) of a <see cref="ComponentType.SelectMenu"/> interaction response. <see langword="null"/> if select type is different. | |||||
| /// </summary> | /// </summary> | ||||
| IReadOnlyCollection<string> Values { get; } | IReadOnlyCollection<string> Values { get; } | ||||
| /// <summary> | /// <summary> | ||||
| /// Gets the value of a <see cref="TextInputComponent"/> interaction response. | |||||
| /// Gets the channels(s) of a <see cref="ComponentType.ChannelSelect"/> interaction response. <see langword="null"/> if select type is different. | |||||
| /// </summary> | |||||
| IReadOnlyCollection<IChannel> Channels { get; } | |||||
| /// <summary> | |||||
| /// Gets the user(s) of a <see cref="ComponentType.UserSelect"/> or <see cref="ComponentType.MentionableSelect"/> interaction response. <see langword="null"/> if select type is different. | |||||
| /// </summary> | |||||
| IReadOnlyCollection<IUser> Users { get; } | |||||
| /// <summary> | |||||
| /// Gets the roles(s) of a <see cref="ComponentType.RoleSelect"/> or <see cref="ComponentType.MentionableSelect"/> interaction response. <see langword="null"/> if select type is different. | |||||
| /// </summary> | |||||
| IReadOnlyCollection<IRole> Roles { get; } | |||||
| /// <summary> | |||||
| /// Gets the guild member(s) of a <see cref="ComponentType.UserSelect"/> or <see cref="ComponentType.MentionableSelect"/> interaction response. <see langword="null"/> if type select is different. | |||||
| /// </summary> | |||||
| IReadOnlyCollection<IGuildUser> Members { get; } | |||||
| /// <summary> | |||||
| /// Gets the value of a <see cref="ComponentType.TextInput"/> interaction response. | |||||
| /// </summary> | /// </summary> | ||||
| public string Value { get; } | public string Value { get; } | ||||
| } | } | ||||
| @@ -1,3 +1,4 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Linq; | using System.Linq; | ||||
| @@ -9,7 +10,7 @@ namespace Discord | |||||
| public class SelectMenuComponent : IMessageComponent | public class SelectMenuComponent : IMessageComponent | ||||
| { | { | ||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public ComponentType Type => ComponentType.SelectMenu; | |||||
| public ComponentType Type { get; } | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public string CustomId { get; } | public string CustomId { get; } | ||||
| @@ -39,6 +40,11 @@ namespace Discord | |||||
| /// </summary> | /// </summary> | ||||
| public bool IsDisabled { get; } | public bool IsDisabled { get; } | ||||
| /// <summary> | |||||
| /// Gets the allowed channel types for this modal | |||||
| /// </summary> | |||||
| public IReadOnlyCollection<ChannelType> ChannelTypes { get; } | |||||
| /// <summary> | /// <summary> | ||||
| /// Turns this select menu into a builder. | /// Turns this select menu into a builder. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -52,9 +58,9 @@ namespace Discord | |||||
| Placeholder, | Placeholder, | ||||
| MaxValues, | MaxValues, | ||||
| MinValues, | MinValues, | ||||
| IsDisabled); | |||||
| IsDisabled, Type, ChannelTypes.ToList()); | |||||
| internal SelectMenuComponent(string customId, List<SelectMenuOption> options, string placeholder, int minValues, int maxValues, bool disabled) | |||||
| internal SelectMenuComponent(string customId, List<SelectMenuOption> options, string placeholder, int minValues, int maxValues, bool disabled, ComponentType type, IEnumerable<ChannelType> channelTypes = null) | |||||
| { | { | ||||
| CustomId = customId; | CustomId = customId; | ||||
| Options = options; | Options = options; | ||||
| @@ -62,6 +68,8 @@ namespace Discord | |||||
| MinValues = minValues; | MinValues = minValues; | ||||
| MaxValues = maxValues; | MaxValues = maxValues; | ||||
| IsDisabled = disabled; | IsDisabled = disabled; | ||||
| Type = type; | |||||
| ChannelTypes = channelTypes?.ToArray() ?? Array.Empty<ChannelType>(); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -7,12 +7,12 @@ using System.Threading.Tasks; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| /// <summary> | /// <summary> | ||||
| /// Represents a modal interaction. | |||||
| /// Represents a modal interaction. | |||||
| /// </summary> | /// </summary> | ||||
| public class Modal : IMessageComponent | public class Modal : IMessageComponent | ||||
| { | { | ||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public ComponentType Type => ComponentType.ModalSubmit; | |||||
| public ComponentType Type => throw new NotSupportedException("Modals do not have a component type."); | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the title of the modal. | /// Gets the title of the modal. | ||||
| @@ -84,6 +84,11 @@ namespace Discord | |||||
| /// Gets or sets whether or not this command can be used in DMs. | /// Gets or sets whether or not this command can be used in DMs. | ||||
| /// </summary> | /// </summary> | ||||
| public bool IsDMEnabled { get; set; } = true; | public bool IsDMEnabled { get; set; } = true; | ||||
| /// <summary> | |||||
| /// Gets or sets whether or not this command is age restricted. | |||||
| /// </summary> | |||||
| public bool IsNsfw { get; set; } = false; | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets or sets the default permission required to use this slash command. | /// Gets or sets the default permission required to use this slash command. | ||||
| @@ -110,7 +115,8 @@ namespace Discord | |||||
| NameLocalizations = _nameLocalizations, | NameLocalizations = _nameLocalizations, | ||||
| DescriptionLocalizations = _descriptionLocalizations, | DescriptionLocalizations = _descriptionLocalizations, | ||||
| IsDMEnabled = IsDMEnabled, | IsDMEnabled = IsDMEnabled, | ||||
| DefaultMemberPermissions = DefaultMemberPermissions ?? Optional<GuildPermission>.Unspecified | |||||
| DefaultMemberPermissions = DefaultMemberPermissions ?? Optional<GuildPermission>.Unspecified, | |||||
| IsNsfw = IsNsfw, | |||||
| }; | }; | ||||
| if (Options != null && Options.Any()) | if (Options != null && Options.Any()) | ||||
| @@ -161,7 +167,7 @@ namespace Discord | |||||
| } | } | ||||
| /// <summary> | /// <summary> | ||||
| /// Sets whether or not this command can be used in dms | |||||
| /// Sets whether or not this command can be used in dms. | |||||
| /// </summary> | /// </summary> | ||||
| /// <param name="permission"><see langword="true"/> if the command is available in dms, otherwise <see langword="false"/>.</param> | /// <param name="permission"><see langword="true"/> if the command is available in dms, otherwise <see langword="false"/>.</param> | ||||
| /// <returns>The current builder.</returns> | /// <returns>The current builder.</returns> | ||||
| @@ -171,6 +177,17 @@ namespace Discord | |||||
| return this; | return this; | ||||
| } | } | ||||
| /// <summary> | |||||
| /// Sets whether or not this command is age restricted. | |||||
| /// </summary> | |||||
| /// <param name="permission"><see langword="true"/> if the command is age restricted, otherwise <see langword="false"/>.</param> | |||||
| /// <returns>The current builder.</returns> | |||||
| public SlashCommandBuilder WithNsfw(bool permission) | |||||
| { | |||||
| IsNsfw = permission; | |||||
| return this; | |||||
| } | |||||
| /// <summary> | /// <summary> | ||||
| /// Sets the default member permissions required to use this application command. | /// Sets the default member permissions required to use this application command. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -60,6 +60,7 @@ namespace Discord | |||||
| /// A guild object representing the guild that the invite points to. | /// A guild object representing the guild that the invite points to. | ||||
| /// </returns> | /// </returns> | ||||
| IGuild Guild { get; } | IGuild Guild { get; } | ||||
| /// <summary> | /// <summary> | ||||
| /// Gets the ID of the guild this invite is linked to. | /// Gets the ID of the guild this invite is linked to. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -0,0 +1,156 @@ | |||||
| using System; | |||||
| namespace Discord; | |||||
| public class InviteGuild : ISnowflakeEntity | |||||
| { | |||||
| /// <inheritdoc /> | |||||
| public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | |||||
| /// <inheritdoc/> | |||||
| public ulong Id { get; private set; } | |||||
| /// <summary> | |||||
| /// Gets the name of this guild. | |||||
| /// </summary> | |||||
| /// <returns> | |||||
| /// A string containing the name of this guild. | |||||
| /// </returns> | |||||
| public string Name { get; private set; } | |||||
| /// <summary> | |||||
| /// Gets the description for the guild. | |||||
| /// </summary> | |||||
| /// <returns> | |||||
| /// The description for the guild; <see langword="null" /> if none is set. | |||||
| /// </returns> | |||||
| public string Description { get; private set; } | |||||
| /// <summary> | |||||
| /// Gets the ID of this guild's splash image. | |||||
| /// </summary> | |||||
| /// <returns> | |||||
| /// An identifier for the splash image; <see langword="null" /> if none is set. | |||||
| /// </returns> | |||||
| public string SplashId { get; private set; } | |||||
| /// <summary> | |||||
| /// Gets the URL of this guild's splash image. | |||||
| /// </summary> | |||||
| /// <returns> | |||||
| /// A URL pointing to the guild's splash image; <see langword="null" /> if none is set. | |||||
| /// </returns> | |||||
| public string SplashUrl => CDN.GetGuildSplashUrl(Id, SplashId); | |||||
| /// <summary> | |||||
| /// Gets the identifier for this guilds banner image. | |||||
| /// </summary> | |||||
| /// <returns> | |||||
| /// An identifier for the banner image; <see langword="null" /> if none is set. | |||||
| /// </returns> | |||||
| public string BannerId { get; private set; } | |||||
| /// <summary> | |||||
| /// Gets the URL of this guild's banner image. | |||||
| /// </summary> | |||||
| /// <returns> | |||||
| /// A URL pointing to the guild's banner image; <see langword="null" /> if none is set. | |||||
| /// </returns> | |||||
| public string BannerUrl => CDN.GetGuildBannerUrl(Id, BannerId, ImageFormat.Auto); | |||||
| /// <summary> | |||||
| /// Gets the features for this guild. | |||||
| /// </summary> | |||||
| /// <returns> | |||||
| /// A flags enum containing all the features for the guild. | |||||
| /// </returns> | |||||
| public GuildFeatures Features { get; private set; } | |||||
| /// <summary> | |||||
| /// Gets the ID of this guild's icon. | |||||
| /// </summary> | |||||
| /// <returns> | |||||
| /// An identifier for the splash image; <see langword="null" /> if none is set. | |||||
| /// </returns> | |||||
| public string IconId { get; private set; } | |||||
| /// <summary> | |||||
| /// Gets the URL of this guild's icon. | |||||
| /// </summary> | |||||
| /// <returns> | |||||
| /// A URL pointing to the guild's icon; <see langword="null" /> if none is set. | |||||
| /// </returns> | |||||
| public string IconUrl => CDN.GetGuildIconUrl(Id, IconId); | |||||
| /// <summary> | |||||
| /// | |||||
| /// Gets the level of requirements a user must fulfill before being allowed to post messages in this guild. | |||||
| /// </summary> | |||||
| /// <returns> | |||||
| /// The level of requirements. | |||||
| /// </returns> | |||||
| public VerificationLevel VerificationLevel { get; private set; } | |||||
| /// <summary> | |||||
| /// Gets the code for this guild's vanity invite URL. | |||||
| /// </summary> | |||||
| /// <returns> | |||||
| /// A string containing the vanity invite code for this guild; <see langword="null" /> if none is set. | |||||
| /// </returns> | |||||
| public string VanityURLCode { get; private set; } | |||||
| /// <summary> | |||||
| /// Gets the number of premium subscribers of this guild. | |||||
| /// </summary> | |||||
| /// <remarks> | |||||
| /// This is the number of users who have boosted this guild. | |||||
| /// </remarks> | |||||
| /// <returns> | |||||
| /// The number of premium subscribers of this guild; | |||||
| /// </returns> | |||||
| public int PremiumSubscriptionCount { get; private set; } | |||||
| /// <summary> | |||||
| /// Gets the NSFW level of this guild. | |||||
| /// </summary> | |||||
| /// <returns> | |||||
| /// The NSFW level of this guild. | |||||
| /// </returns> | |||||
| public NsfwLevel NsfwLevel { get; private set; } | |||||
| /// <summary> | |||||
| /// Gets the Welcome Screen of this guild | |||||
| /// </summary> | |||||
| /// <returns> | |||||
| /// The welcome screen of this guild. <see langword="null" /> if none is set. | |||||
| /// </returns> | |||||
| public WelcomeScreen WelcomeScreen { get; private set; } | |||||
| internal InviteGuild( | |||||
| ulong id, | |||||
| string name, | |||||
| string description, | |||||
| string splashId, | |||||
| string bannerId, | |||||
| GuildFeatures features, | |||||
| string iconId, | |||||
| VerificationLevel verificationLevel, | |||||
| string vanityURLCode, | |||||
| int premiumSubscriptionCount, | |||||
| NsfwLevel nsfwLevel, | |||||
| WelcomeScreen welcomeScreen) | |||||
| { | |||||
| Id = id; | |||||
| Name = name; | |||||
| Description = description; | |||||
| SplashId = splashId; | |||||
| BannerId = bannerId; | |||||
| Features = features; | |||||
| IconId = iconId; | |||||
| VerificationLevel = verificationLevel; | |||||
| VanityURLCode = vanityURLCode; | |||||
| PremiumSubscriptionCount = premiumSubscriptionCount; | |||||
| NsfwLevel = nsfwLevel; | |||||
| WelcomeScreen = welcomeScreen; | |||||
| } | |||||
| } | |||||
| @@ -16,6 +16,11 @@ namespace Discord | |||||
| /// <summary> | /// <summary> | ||||
| /// Nitro subscription. Includes app perks as well as the games subscription service. | /// Nitro subscription. Includes app perks as well as the games subscription service. | ||||
| /// </summary> | /// </summary> | ||||
| Nitro = 2 | |||||
| Nitro = 2, | |||||
| /// <summary> | |||||
| /// Nitro Basic subscription. Includes app perks like video backgrounds, sending bigger files. | |||||
| /// </summary> | |||||
| NitroBasic = 3 | |||||
| } | } | ||||
| } | } | ||||
| @@ -69,5 +69,10 @@ namespace Discord | |||||
| /// Flag given to bots that use only outgoing webhooks, exclusively. | /// Flag given to bots that use only outgoing webhooks, exclusively. | ||||
| /// </summary> | /// </summary> | ||||
| BotHTTPInteractions = 1 << 19, | BotHTTPInteractions = 1 << 19, | ||||
| /// <summary> | |||||
| /// Flag given to users that are active developers. | |||||
| /// </summary> | |||||
| ActiveDeveloper = 1 << 22 | |||||
| } | } | ||||
| } | } | ||||
| @@ -46,6 +46,9 @@ namespace Discord | |||||
| case ITextChannel: | case ITextChannel: | ||||
| return ChannelType.Text; | return ChannelType.Text; | ||||
| case IForumChannel: | |||||
| return ChannelType.Forum; | |||||
| } | } | ||||
| return null; | return null; | ||||
| @@ -1,5 +1,6 @@ | |||||
| using System; | using System; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using System.Collections.Generic; | |||||
| using System.IO; | using System.IO; | ||||
| namespace Discord | namespace Discord | ||||
| @@ -163,6 +164,96 @@ namespace Discord | |||||
| return await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(filePath, text, isTTS, embed, options, components: components, embeds: embeds).ConfigureAwait(false); | return await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(filePath, text, isTTS, embed, options, components: components, embeds: embeds).ConfigureAwait(false); | ||||
| } | } | ||||
| /// <summary> | |||||
| /// Sends a file via DM with an optional caption. | |||||
| /// </summary> | |||||
| /// <remarks> | |||||
| /// This method attempts to send an attachment as a direct-message to the user. | |||||
| /// <note type="warning"> | |||||
| /// <para> | |||||
| /// Please note that this method <strong>will</strong> throw an <see cref="Discord.Net.HttpException"/> | |||||
| /// if the user cannot receive DMs due to privacy reasons or if the user has the sender blocked. | |||||
| /// </para> | |||||
| /// <para> | |||||
| /// You may want to consider catching for <see cref="Discord.Net.HttpException.DiscordCode"/> | |||||
| /// <c>50007</c> when using this method. | |||||
| /// </para> | |||||
| /// </note> | |||||
| /// <note> | |||||
| /// If you wish to upload an image and have it embedded in a <see cref="Discord.EmbedType.Rich"/> embed, | |||||
| /// you may upload the file and refer to the file with "attachment://filename.ext" in the | |||||
| /// <see cref="Discord.EmbedBuilder.ImageUrl"/>. See the example section for its usage. | |||||
| /// </note> | |||||
| /// </remarks> | |||||
| /// <param name="user">The user to send the DM to.</param> | |||||
| /// <param name="attachment">The attachment containing the file and description.</param> | |||||
| /// <param name="text">The message to be sent.</param> | |||||
| /// <param name="isTTS">Whether the message should be read aloud by Discord or not.</param> | |||||
| /// <param name="embed">The <see cref="EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param> | |||||
| /// <param name="options">The options to be used when sending the request.</param> | |||||
| /// <param name="components">The message component to be included with this message. Used for interactions.</param> | |||||
| /// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param> | |||||
| /// <returns> | |||||
| /// A task that represents an asynchronous send operation for delivering the message. The task result | |||||
| /// contains the sent message. | |||||
| /// </returns> | |||||
| public static async Task<IUserMessage> SendFileAsync(this IUser user, | |||||
| FileAttachment attachment, | |||||
| string text = null, | |||||
| bool isTTS = false, | |||||
| Embed embed = null, | |||||
| RequestOptions options = null, | |||||
| MessageComponent components = null, | |||||
| Embed[] embeds = null) | |||||
| { | |||||
| return await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(attachment, text, isTTS, embed, options, components: components, embeds: embeds).ConfigureAwait(false); | |||||
| } | |||||
| /// <summary> | |||||
| /// Sends a collection of files via DM. | |||||
| /// </summary> | |||||
| /// <remarks> | |||||
| /// This method attempts to send an attachments as a direct-message to the user. | |||||
| /// <note type="warning"> | |||||
| /// <para> | |||||
| /// Please note that this method <strong>will</strong> throw an <see cref="Discord.Net.HttpException"/> | |||||
| /// if the user cannot receive DMs due to privacy reasons or if the user has the sender blocked. | |||||
| /// </para> | |||||
| /// <para> | |||||
| /// You may want to consider catching for <see cref="Discord.Net.HttpException.DiscordCode"/> | |||||
| /// <c>50007</c> when using this method. | |||||
| /// </para> | |||||
| /// </note> | |||||
| /// <note> | |||||
| /// If you wish to upload an image and have it embedded in a <see cref="Discord.EmbedType.Rich"/> embed, | |||||
| /// you may upload the file and refer to the file with "attachment://filename.ext" in the | |||||
| /// <see cref="Discord.EmbedBuilder.ImageUrl"/>. See the example section for its usage. | |||||
| /// </note> | |||||
| /// </remarks> | |||||
| /// <param name="user">The user to send the DM to.</param> | |||||
| /// <param name="attachments">A collection of attachments to upload.</param> | |||||
| /// <param name="text">The message to be sent.</param> | |||||
| /// <param name="isTTS">Whether the message should be read aloud by Discord or not.</param> | |||||
| /// <param name="embed">The <see cref="EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param> | |||||
| /// <param name="options">The options to be used when sending the request.</param> | |||||
| /// <param name="components">The message component to be included with this message. Used for interactions.</param> | |||||
| /// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param> | |||||
| /// <returns> | |||||
| /// A task that represents an asynchronous send operation for delivering the message. The task result | |||||
| /// contains the sent message. | |||||
| /// </returns> | |||||
| public static async Task<IUserMessage> SendFilesAsync(this IUser user, | |||||
| IEnumerable<FileAttachment> attachments, | |||||
| string text = null, | |||||
| bool isTTS = false, | |||||
| Embed embed = null, | |||||
| RequestOptions options = null, | |||||
| MessageComponent components = null, | |||||
| Embed[] embeds = null) | |||||
| { | |||||
| return await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendFilesAsync(attachments, text, isTTS, embed, options, components: components, embeds: embeds).ConfigureAwait(false); | |||||
| } | |||||
| /// <summary> | /// <summary> | ||||
| /// Bans the user from the guild and optionally prunes their recent messages. | /// Bans the user from the guild and optionally prunes their recent messages. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -0,0 +1,14 @@ | |||||
| using System.Collections.Generic; | |||||
| namespace Discord.Utils; | |||||
| public static class ChannelTypeUtils | |||||
| { | |||||
| public static List<ChannelType> AllChannelTypes() | |||||
| => new List<ChannelType>() | |||||
| { | |||||
| ChannelType.Forum, ChannelType.Category, ChannelType.DM, ChannelType.Group, ChannelType.GuildDirectory, | |||||
| ChannelType.News, ChannelType.NewsThread, ChannelType.PrivateThread, ChannelType.PublicThread, | |||||
| ChannelType.Stage, ChannelType.Store, ChannelType.Text, ChannelType.Voice | |||||
| }; | |||||
| } | |||||
| @@ -0,0 +1,8 @@ | |||||
| namespace Discord.Utils; | |||||
| public static class ComponentTypeUtils | |||||
| { | |||||
| public static bool IsSelectType(this ComponentType type) => type is ComponentType.ChannelSelect | |||||
| or ComponentType.SelectMenu or ComponentType.RoleSelect or ComponentType.UserSelect | |||||
| or ComponentType.MentionableSelect; | |||||
| } | |||||
| @@ -1,4 +1,5 @@ | |||||
| using System; | using System; | ||||
| using System.Runtime.CompilerServices; | |||||
| namespace Discord.Interactions | namespace Discord.Interactions | ||||
| { | { | ||||
| @@ -28,6 +29,11 @@ namespace Discord.Interactions | |||||
| /// </summary> | /// </summary> | ||||
| public RunMode RunMode { get; } | public RunMode RunMode { get; } | ||||
| /// <summary> | |||||
| /// Gets or sets whether the <see cref="CustomId"/> should be treated as a raw Regex pattern. | |||||
| /// </summary> | |||||
| public bool TreatAsRegex { get; set; } = false; | |||||
| /// <summary> | /// <summary> | ||||
| /// Create a command for component interaction handling. | /// Create a command for component interaction handling. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -28,6 +28,11 @@ namespace Discord.Interactions | |||||
| /// </summary> | /// </summary> | ||||
| public RunMode RunMode { get; } | public RunMode RunMode { get; } | ||||
| /// <summary> | |||||
| /// Gets or sets whether the <see cref="CustomId"/> should be treated as a raw Regex pattern. | |||||
| /// </summary> | |||||
| public bool TreatAsRegex { get; set; } = false; | |||||
| /// <summary> | /// <summary> | ||||
| /// Create a command for modal interaction handling. | /// Create a command for modal interaction handling. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -0,0 +1,25 @@ | |||||
| using System; | |||||
| namespace Discord.Interactions | |||||
| { | |||||
| /// <summary> | |||||
| /// Sets the <see cref="IApplicationCommandInfo.IsNsfw"/> property of an application command or module. | |||||
| /// </summary> | |||||
| [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] | |||||
| public class NsfwCommandAttribute : Attribute | |||||
| { | |||||
| /// <summary> | |||||
| /// Gets whether or not this command is age restricted. | |||||
| /// </summary> | |||||
| public bool IsNsfw { get; } | |||||
| /// <summary> | |||||
| /// Sets the <see cref="IApplicationCommandInfo.IsNsfw"/> property of an application command or module. | |||||
| /// </summary> | |||||
| /// <param name="isNsfw">Whether or not this command is age restricted.</param> | |||||
| public NsfwCommandAttribute(bool isNsfw) | |||||
| { | |||||
| IsNsfw = isNsfw; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -35,6 +35,9 @@ namespace Discord.Interactions.Builders | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public bool IgnoreGroupNames { get; set; } | public bool IgnoreGroupNames { get; set; } | ||||
| /// <inheritdoc/> | |||||
| public bool TreatNameAsRegex { get; set; } | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public RunMode RunMode { get; set; } | public RunMode RunMode { get; set; } | ||||
| @@ -117,6 +120,19 @@ namespace Discord.Interactions.Builders | |||||
| return Instance; | return Instance; | ||||
| } | } | ||||
| /// <summary> | |||||
| /// Sets <see cref="TreatNameAsRegex"/>. | |||||
| /// </summary> | |||||
| /// <param name="value">New value of the <see cref="TreatNameAsRegex"/>.</param> | |||||
| /// <returns> | |||||
| /// The builder instance. | |||||
| /// </returns> | |||||
| public TBuilder WithNameAsRegex (bool value) | |||||
| { | |||||
| TreatNameAsRegex = value; | |||||
| return Instance; | |||||
| } | |||||
| /// <summary> | /// <summary> | ||||
| /// Adds parameter builders to <see cref="Parameters"/>. | /// Adds parameter builders to <see cref="Parameters"/>. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -163,6 +179,10 @@ namespace Discord.Interactions.Builders | |||||
| ICommandBuilder ICommandBuilder.SetRunMode (RunMode runMode) => | ICommandBuilder ICommandBuilder.SetRunMode (RunMode runMode) => | ||||
| SetRunMode(runMode); | SetRunMode(runMode); | ||||
| /// <inheritdoc/> | |||||
| ICommandBuilder ICommandBuilder.WithNameAsRegex(bool value) => | |||||
| WithNameAsRegex(value); | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| ICommandBuilder ICommandBuilder.AddParameters (params IParameterBuilder[] parameters) => | ICommandBuilder ICommandBuilder.AddParameters (params IParameterBuilder[] parameters) => | ||||
| AddParameters(parameters as TParamBuilder); | AddParameters(parameters as TParamBuilder); | ||||
| @@ -25,6 +25,11 @@ namespace Discord.Interactions.Builders | |||||
| /// </summary> | /// </summary> | ||||
| public bool IsEnabledInDm { get; set; } = true; | public bool IsEnabledInDm { get; set; } = true; | ||||
| /// <summary> | |||||
| /// Gets whether this command is age restricted. | |||||
| /// </summary> | |||||
| public bool IsNsfw { get; set; } = false; | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the default permissions needed for executing this command. | /// Gets the default permissions needed for executing this command. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -95,6 +100,19 @@ namespace Discord.Interactions.Builders | |||||
| return this; | return this; | ||||
| } | } | ||||
| /// <summary> | |||||
| /// Sets <see cref="IsNsfw"/>. | |||||
| /// </summary> | |||||
| /// <param name="isNsfw">New value of the <see cref="IsNsfw"/>.</param> | |||||
| /// <returns> | |||||
| /// The builder instance. | |||||
| /// </returns> | |||||
| public ContextCommandBuilder SetNsfw(bool isNsfw) | |||||
| { | |||||
| IsNsfw = isNsfw; | |||||
| return this; | |||||
| } | |||||
| /// <summary> | /// <summary> | ||||
| /// Sets <see cref="DefaultMemberPermissions"/>. | /// Sets <see cref="DefaultMemberPermissions"/>. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -34,6 +34,11 @@ namespace Discord.Interactions.Builders | |||||
| /// </summary> | /// </summary> | ||||
| bool IgnoreGroupNames { get; set; } | bool IgnoreGroupNames { get; set; } | ||||
| /// <summary> | |||||
| /// Gets or sets whether the <see cref="Name"/> should be directly used as a Regex pattern. | |||||
| /// </summary> | |||||
| bool TreatNameAsRegex { get; set; } | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets or sets the run mode this command gets executed with. | /// Gets or sets the run mode this command gets executed with. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -90,6 +95,15 @@ namespace Discord.Interactions.Builders | |||||
| /// </returns> | /// </returns> | ||||
| ICommandBuilder SetRunMode (RunMode runMode); | ICommandBuilder SetRunMode (RunMode runMode); | ||||
| /// <summary> | |||||
| /// Sets <see cref="TreatNameAsRegex"/>. | |||||
| /// </summary> | |||||
| /// <param name="value">New value of the <see cref="TreatNameAsRegex"/>.</param> | |||||
| /// <returns> | |||||
| /// The builder instance. | |||||
| /// </returns> | |||||
| ICommandBuilder WithNameAsRegex(bool value); | |||||
| /// <summary> | /// <summary> | ||||
| /// Adds parameter builders to <see cref="Parameters"/>. | /// Adds parameter builders to <see cref="Parameters"/>. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -25,6 +25,11 @@ namespace Discord.Interactions.Builders | |||||
| /// </summary> | /// </summary> | ||||
| public bool IsEnabledInDm { get; set; } = true; | public bool IsEnabledInDm { get; set; } = true; | ||||
| /// <summary> | |||||
| /// Gets whether this command is age restricted. | |||||
| /// </summary> | |||||
| public bool IsNsfw { get; set; } = false; | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the default permissions needed for executing this command. | /// Gets the default permissions needed for executing this command. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -95,6 +100,19 @@ namespace Discord.Interactions.Builders | |||||
| return this; | return this; | ||||
| } | } | ||||
| /// <summary> | |||||
| /// Sets <see cref="IsNsfw"/>. | |||||
| /// </summary> | |||||
| /// <param name="isNsfw">New value of the <see cref="IsNsfw"/>.</param> | |||||
| /// <returns> | |||||
| /// The builder instance. | |||||
| /// </returns> | |||||
| public SlashCommandBuilder SetNsfw(bool isNsfw) | |||||
| { | |||||
| IsNsfw = isNsfw; | |||||
| return this; | |||||
| } | |||||
| /// <summary> | /// <summary> | ||||
| /// Sets <see cref="DefaultMemberPermissions"/>. | /// Sets <see cref="DefaultMemberPermissions"/>. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -59,6 +59,11 @@ namespace Discord.Interactions.Builders | |||||
| /// </summary> | /// </summary> | ||||
| public bool IsEnabledInDm { get; set; } = true; | public bool IsEnabledInDm { get; set; } = true; | ||||
| /// <summary> | |||||
| /// Gets whether this command is age restricted. | |||||
| /// </summary> | |||||
| public bool IsNsfw { get; set; } = false; | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the default permissions needed for executing this command. | /// Gets the default permissions needed for executing this command. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -190,6 +195,19 @@ namespace Discord.Interactions.Builders | |||||
| return this; | return this; | ||||
| } | } | ||||
| /// <summary> | |||||
| /// Sets <see cref="IsNsfw"/>. | |||||
| /// </summary> | |||||
| /// <param name="isNsfw">New value of the <see cref="IsNsfw"/>.</param> | |||||
| /// <returns> | |||||
| /// The builder instance. | |||||
| /// </returns> | |||||
| public ModuleBuilder SetNsfw(bool isNsfw) | |||||
| { | |||||
| IsNsfw = isNsfw; | |||||
| return this; | |||||
| } | |||||
| /// <summary> | /// <summary> | ||||
| /// Sets <see cref="DefaultMemberPermissions"/>. | /// Sets <see cref="DefaultMemberPermissions"/>. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -101,6 +101,9 @@ namespace Discord.Interactions.Builders | |||||
| case DontAutoRegisterAttribute dontAutoRegister: | case DontAutoRegisterAttribute dontAutoRegister: | ||||
| builder.DontAutoRegister = true; | builder.DontAutoRegister = true; | ||||
| break; | break; | ||||
| case NsfwCommandAttribute nsfwCommand: | |||||
| builder.SetNsfw(nsfwCommand.IsNsfw); | |||||
| break; | |||||
| default: | default: | ||||
| builder.AddAttributes(attribute); | builder.AddAttributes(attribute); | ||||
| break; | break; | ||||
| @@ -192,6 +195,9 @@ namespace Discord.Interactions.Builders | |||||
| case PreconditionAttribute precondition: | case PreconditionAttribute precondition: | ||||
| builder.WithPreconditions(precondition); | builder.WithPreconditions(precondition); | ||||
| break; | break; | ||||
| case NsfwCommandAttribute nsfwCommand: | |||||
| builder.SetNsfw(nsfwCommand.IsNsfw); | |||||
| break; | |||||
| default: | default: | ||||
| builder.WithAttributes(attribute); | builder.WithAttributes(attribute); | ||||
| break; | break; | ||||
| @@ -244,6 +250,9 @@ namespace Discord.Interactions.Builders | |||||
| case PreconditionAttribute precondition: | case PreconditionAttribute precondition: | ||||
| builder.WithPreconditions(precondition); | builder.WithPreconditions(precondition); | ||||
| break; | break; | ||||
| case NsfwCommandAttribute nsfwCommand: | |||||
| builder.SetNsfw(nsfwCommand.IsNsfw); | |||||
| break; | |||||
| default: | default: | ||||
| builder.WithAttributes(attribute); | builder.WithAttributes(attribute); | ||||
| break; | break; | ||||
| @@ -274,6 +283,7 @@ namespace Discord.Interactions.Builders | |||||
| builder.Name = interaction.CustomId; | builder.Name = interaction.CustomId; | ||||
| builder.RunMode = interaction.RunMode; | builder.RunMode = interaction.RunMode; | ||||
| builder.IgnoreGroupNames = interaction.IgnoreGroupNames; | builder.IgnoreGroupNames = interaction.IgnoreGroupNames; | ||||
| builder.TreatNameAsRegex = interaction.TreatAsRegex; | |||||
| } | } | ||||
| break; | break; | ||||
| case PreconditionAttribute precondition: | case PreconditionAttribute precondition: | ||||
| @@ -287,7 +297,7 @@ namespace Discord.Interactions.Builders | |||||
| var parameters = methodInfo.GetParameters(); | var parameters = methodInfo.GetParameters(); | ||||
| var wildCardCount = Regex.Matches(Regex.Escape(builder.Name), Regex.Escape(commandService._wildCardExp)).Count; | |||||
| var wildCardCount = RegexUtils.GetWildCardCount(builder.Name, commandService._wildCardExp); | |||||
| foreach (var parameter in parameters) | foreach (var parameter in parameters) | ||||
| builder.AddParameter(x => BuildComponentParameter(x, parameter, parameter.Position >= wildCardCount)); | builder.AddParameter(x => BuildComponentParameter(x, parameter, parameter.Position >= wildCardCount)); | ||||
| @@ -355,6 +365,7 @@ namespace Discord.Interactions.Builders | |||||
| builder.Name = modal.CustomId; | builder.Name = modal.CustomId; | ||||
| builder.RunMode = modal.RunMode; | builder.RunMode = modal.RunMode; | ||||
| builder.IgnoreGroupNames = modal.IgnoreGroupNames; | builder.IgnoreGroupNames = modal.IgnoreGroupNames; | ||||
| builder.TreatNameAsRegex = modal.TreatAsRegex; | |||||
| } | } | ||||
| break; | break; | ||||
| case PreconditionAttribute precondition: | case PreconditionAttribute precondition: | ||||
| @@ -23,7 +23,7 @@ namespace Discord.Interactions | |||||
| public string CommandName { get; } | public string CommandName { get; } | ||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public override IReadOnlyCollection<CommandParameterInfo> Parameters { get; } | |||||
| public override IReadOnlyList<CommandParameterInfo> Parameters { get; } | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public override bool SupportsWildCards => false; | public override bool SupportsWildCards => false; | ||||
| @@ -41,9 +41,12 @@ namespace Discord.Interactions | |||||
| if (context.Interaction is not IAutocompleteInteraction) | if (context.Interaction is not IAutocompleteInteraction) | ||||
| return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Autocomplete Interaction"); | return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Autocomplete Interaction"); | ||||
| return await RunAsync(context, Array.Empty<object>(), services).ConfigureAwait(false); | |||||
| return await base.ExecuteAsync(context, services).ConfigureAwait(false); | |||||
| } | } | ||||
| protected override Task<IResult> ParseArgumentsAsync(IInteractionContext context, IServiceProvider services) | |||||
| => Task.FromResult(ParseResult.FromSuccess(Array.Empty<object>()) as IResult); | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| protected override Task InvokeModuleEvent(IInteractionContext context, IResult result) => | protected override Task InvokeModuleEvent(IInteractionContext context, IResult result) => | ||||
| CommandService._autocompleteCommandExecutedEvent.InvokeAsync(this, context, result); | CommandService._autocompleteCommandExecutedEvent.InvokeAsync(this, context, result); | ||||
| @@ -64,7 +64,9 @@ namespace Discord.Interactions | |||||
| public IReadOnlyCollection<PreconditionAttribute> Preconditions { get; } | public IReadOnlyCollection<PreconditionAttribute> Preconditions { get; } | ||||
| /// <inheritdoc cref="ICommandInfo.Parameters"/> | /// <inheritdoc cref="ICommandInfo.Parameters"/> | ||||
| public abstract IReadOnlyCollection<TParameter> Parameters { get; } | |||||
| public abstract IReadOnlyList<TParameter> Parameters { get; } | |||||
| public bool TreatNameAsRegex { get; } | |||||
| internal CommandInfo(Builders.ICommandBuilder builder, ModuleInfo module, InteractionService commandService) | internal CommandInfo(Builders.ICommandBuilder builder, ModuleInfo module, InteractionService commandService) | ||||
| { | { | ||||
| @@ -78,6 +80,7 @@ namespace Discord.Interactions | |||||
| RunMode = builder.RunMode != RunMode.Default ? builder.RunMode : commandService._runMode; | RunMode = builder.RunMode != RunMode.Default ? builder.RunMode : commandService._runMode; | ||||
| Attributes = builder.Attributes.ToImmutableArray(); | Attributes = builder.Attributes.ToImmutableArray(); | ||||
| Preconditions = builder.Preconditions.ToImmutableArray(); | Preconditions = builder.Preconditions.ToImmutableArray(); | ||||
| TreatNameAsRegex = builder.TreatNameAsRegex && SupportsWildCards; | |||||
| _action = builder.Callback; | _action = builder.Callback; | ||||
| _groupedPreconditions = builder.Preconditions.ToLookup(x => x.Group, x => x, StringComparer.Ordinal); | _groupedPreconditions = builder.Preconditions.ToLookup(x => x.Group, x => x, StringComparer.Ordinal); | ||||
| @@ -85,71 +88,16 @@ namespace Discord.Interactions | |||||
| } | } | ||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public abstract Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services); | |||||
| protected abstract Task InvokeModuleEvent(IInteractionContext context, IResult result); | |||||
| protected abstract string GetLogString(IInteractionContext context); | |||||
| /// <inheritdoc/> | |||||
| public async Task<PreconditionResult> CheckPreconditionsAsync(IInteractionContext context, IServiceProvider services) | |||||
| { | |||||
| async Task<PreconditionResult> CheckGroups(ILookup<string, PreconditionAttribute> preconditions, string type) | |||||
| { | |||||
| foreach (IGrouping<string, PreconditionAttribute> preconditionGroup in preconditions) | |||||
| { | |||||
| if (preconditionGroup.Key == null) | |||||
| { | |||||
| foreach (PreconditionAttribute precondition in preconditionGroup) | |||||
| { | |||||
| var result = await precondition.CheckRequirementsAsync(context, this, services).ConfigureAwait(false); | |||||
| if (!result.IsSuccess) | |||||
| return result; | |||||
| } | |||||
| } | |||||
| else | |||||
| { | |||||
| var results = new List<PreconditionResult>(); | |||||
| foreach (PreconditionAttribute precondition in preconditionGroup) | |||||
| results.Add(await precondition.CheckRequirementsAsync(context, this, services).ConfigureAwait(false)); | |||||
| if (!results.Any(p => p.IsSuccess)) | |||||
| return PreconditionGroupResult.FromError($"{type} precondition group {preconditionGroup.Key} failed.", results); | |||||
| } | |||||
| } | |||||
| return PreconditionGroupResult.FromSuccess(); | |||||
| } | |||||
| var moduleResult = await CheckGroups(Module.GroupedPreconditions, "Module").ConfigureAwait(false); | |||||
| if (!moduleResult.IsSuccess) | |||||
| return moduleResult; | |||||
| var commandResult = await CheckGroups(_groupedPreconditions, "Command").ConfigureAwait(false); | |||||
| return !commandResult.IsSuccess ? commandResult : PreconditionResult.FromSuccess(); | |||||
| } | |||||
| protected async Task<IResult> RunAsync(IInteractionContext context, object[] args, IServiceProvider services) | |||||
| public virtual async Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services) | |||||
| { | { | ||||
| switch (RunMode) | switch (RunMode) | ||||
| { | { | ||||
| case RunMode.Sync: | case RunMode.Sync: | ||||
| { | |||||
| if (CommandService._autoServiceScopes) | |||||
| { | |||||
| using var scope = services?.CreateScope(); | |||||
| return await ExecuteInternalAsync(context, args, scope?.ServiceProvider ?? EmptyServiceProvider.Instance).ConfigureAwait(false); | |||||
| } | |||||
| return await ExecuteInternalAsync(context, args, services).ConfigureAwait(false); | |||||
| } | |||||
| return await ExecuteInternalAsync(context, services).ConfigureAwait(false); | |||||
| case RunMode.Async: | case RunMode.Async: | ||||
| _ = Task.Run(async () => | _ = Task.Run(async () => | ||||
| { | { | ||||
| if (CommandService._autoServiceScopes) | |||||
| { | |||||
| using var scope = services?.CreateScope(); | |||||
| await ExecuteInternalAsync(context, args, scope?.ServiceProvider ?? EmptyServiceProvider.Instance).ConfigureAwait(false); | |||||
| } | |||||
| else | |||||
| await ExecuteInternalAsync(context, args, services).ConfigureAwait(false); | |||||
| await ExecuteInternalAsync(context, services).ConfigureAwait(false); | |||||
| }); | }); | ||||
| break; | break; | ||||
| default: | default: | ||||
| @@ -159,16 +107,33 @@ namespace Discord.Interactions | |||||
| return ExecuteResult.FromSuccess(); | return ExecuteResult.FromSuccess(); | ||||
| } | } | ||||
| private async Task<IResult> ExecuteInternalAsync(IInteractionContext context, object[] args, IServiceProvider services) | |||||
| protected abstract Task<IResult> ParseArgumentsAsync(IInteractionContext context, IServiceProvider services); | |||||
| private async Task<IResult> ExecuteInternalAsync(IInteractionContext context, IServiceProvider services) | |||||
| { | { | ||||
| await CommandService._cmdLogger.DebugAsync($"Executing {GetLogString(context)}").ConfigureAwait(false); | await CommandService._cmdLogger.DebugAsync($"Executing {GetLogString(context)}").ConfigureAwait(false); | ||||
| using var scope = services?.CreateScope(); | |||||
| if (CommandService._autoServiceScopes) | |||||
| services = scope?.ServiceProvider ?? EmptyServiceProvider.Instance; | |||||
| try | try | ||||
| { | { | ||||
| var preconditionResult = await CheckPreconditionsAsync(context, services).ConfigureAwait(false); | var preconditionResult = await CheckPreconditionsAsync(context, services).ConfigureAwait(false); | ||||
| if (!preconditionResult.IsSuccess) | if (!preconditionResult.IsSuccess) | ||||
| return await InvokeEventAndReturn(context, preconditionResult).ConfigureAwait(false); | return await InvokeEventAndReturn(context, preconditionResult).ConfigureAwait(false); | ||||
| var argsResult = await ParseArgumentsAsync(context, services).ConfigureAwait(false); | |||||
| if (!argsResult.IsSuccess) | |||||
| return await InvokeEventAndReturn(context, argsResult).ConfigureAwait(false); | |||||
| if(argsResult is not ParseResult parseResult) | |||||
| return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Complex command parsing failed for an unknown reason."); | |||||
| var args = parseResult.Args; | |||||
| var index = 0; | var index = 0; | ||||
| foreach (var parameter in Parameters) | foreach (var parameter in Parameters) | ||||
| { | { | ||||
| @@ -221,7 +186,47 @@ namespace Discord.Interactions | |||||
| } | } | ||||
| } | } | ||||
| protected async ValueTask<IResult> InvokeEventAndReturn(IInteractionContext context, IResult result) | |||||
| protected abstract Task InvokeModuleEvent(IInteractionContext context, IResult result); | |||||
| protected abstract string GetLogString(IInteractionContext context); | |||||
| /// <inheritdoc/> | |||||
| public async Task<PreconditionResult> CheckPreconditionsAsync(IInteractionContext context, IServiceProvider services) | |||||
| { | |||||
| async Task<PreconditionResult> CheckGroups(ILookup<string, PreconditionAttribute> preconditions, string type) | |||||
| { | |||||
| foreach (IGrouping<string, PreconditionAttribute> preconditionGroup in preconditions) | |||||
| { | |||||
| if (preconditionGroup.Key == null) | |||||
| { | |||||
| foreach (PreconditionAttribute precondition in preconditionGroup) | |||||
| { | |||||
| var result = await precondition.CheckRequirementsAsync(context, this, services).ConfigureAwait(false); | |||||
| if (!result.IsSuccess) | |||||
| return result; | |||||
| } | |||||
| } | |||||
| else | |||||
| { | |||||
| var results = new List<PreconditionResult>(); | |||||
| foreach (PreconditionAttribute precondition in preconditionGroup) | |||||
| results.Add(await precondition.CheckRequirementsAsync(context, this, services).ConfigureAwait(false)); | |||||
| if (!results.Any(p => p.IsSuccess)) | |||||
| return PreconditionGroupResult.FromError($"{type} precondition group {preconditionGroup.Key} failed.", results); | |||||
| } | |||||
| } | |||||
| return PreconditionGroupResult.FromSuccess(); | |||||
| } | |||||
| var moduleResult = await CheckGroups(Module.GroupedPreconditions, "Module").ConfigureAwait(false); | |||||
| if (!moduleResult.IsSuccess) | |||||
| return moduleResult; | |||||
| var commandResult = await CheckGroups(_groupedPreconditions, "Command").ConfigureAwait(false); | |||||
| return !commandResult.IsSuccess ? commandResult : PreconditionResult.FromSuccess(); | |||||
| } | |||||
| protected async Task<T> InvokeEventAndReturn<T>(IInteractionContext context, T result) where T : IResult | |||||
| { | { | ||||
| await InvokeModuleEvent(context, result).ConfigureAwait(false); | await InvokeModuleEvent(context, result).ConfigureAwait(false); | ||||
| return result; | return result; | ||||
| @@ -13,7 +13,7 @@ namespace Discord.Interactions | |||||
| public class ComponentCommandInfo : CommandInfo<ComponentCommandParameterInfo> | public class ComponentCommandInfo : CommandInfo<ComponentCommandParameterInfo> | ||||
| { | { | ||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public override IReadOnlyCollection<ComponentCommandParameterInfo> Parameters { get; } | |||||
| public override IReadOnlyList<ComponentCommandParameterInfo> Parameters { get; } | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public override bool SupportsWildCards => true; | public override bool SupportsWildCards => true; | ||||
| @@ -25,48 +25,32 @@ namespace Discord.Interactions | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public override async Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services) | public override async Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services) | ||||
| => await ExecuteAsync(context, services, null).ConfigureAwait(false); | |||||
| /// <summary> | |||||
| /// Execute this command using dependency injection. | |||||
| /// </summary> | |||||
| /// <param name="context">Context that will be injected to the <see cref="InteractionModuleBase{T}"/>.</param> | |||||
| /// <param name="services">Services that will be used while initializing the <see cref="InteractionModuleBase{T}"/>.</param> | |||||
| /// <param name="additionalArgs">Provide additional string parameters to the method along with the auto generated parameters.</param> | |||||
| /// <returns> | |||||
| /// A task representing the asynchronous command execution process. | |||||
| /// </returns> | |||||
| public async Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services, params string[] additionalArgs) | |||||
| { | { | ||||
| if (context.Interaction is not IComponentInteraction componentInteraction) | |||||
| if (context.Interaction is not IComponentInteraction) | |||||
| return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Message Component Interaction"); | return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Message Component Interaction"); | ||||
| return await ExecuteAsync(context, Parameters, additionalArgs, componentInteraction.Data, services); | |||||
| return await base.ExecuteAsync(context, services).ConfigureAwait(false); | |||||
| } | } | ||||
| /// <inheritdoc/> | |||||
| public async Task<IResult> ExecuteAsync(IInteractionContext context, IEnumerable<CommandParameterInfo> paramList, IEnumerable<string> wildcardCaptures, IComponentInteractionData data, | |||||
| IServiceProvider services) | |||||
| protected override async Task<IResult> ParseArgumentsAsync(IInteractionContext context, IServiceProvider services) | |||||
| { | { | ||||
| var paramCount = paramList.Count(); | |||||
| var captureCount = wildcardCaptures?.Count() ?? 0; | |||||
| if (context.Interaction is not IComponentInteraction messageComponent) | |||||
| return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Component Command Interaction"); | |||||
| var captures = (context as IRouteMatchContainer)?.SegmentMatches?.ToList(); | |||||
| var captureCount = captures?.Count() ?? 0; | |||||
| try | try | ||||
| { | { | ||||
| var args = new object[paramCount]; | |||||
| var data = (context.Interaction as IComponentInteraction).Data; | |||||
| var args = new object[Parameters.Count]; | |||||
| for (var i = 0; i < paramCount; i++) | |||||
| for(var i = 0; i < Parameters.Count; i++) | |||||
| { | { | ||||
| var parameter = Parameters.ElementAt(i); | |||||
| var parameter = Parameters[i]; | |||||
| var isCapture = i < captureCount; | var isCapture = i < captureCount; | ||||
| if (isCapture ^ parameter.IsRouteSegmentParameter) | if (isCapture ^ parameter.IsRouteSegmentParameter) | ||||
| return await InvokeEventAndReturn(context, ExecuteResult.FromError(InteractionCommandError.BadArgs, "Argument type and parameter type didn't match (Wild Card capture/Component value)")).ConfigureAwait(false); | return await InvokeEventAndReturn(context, ExecuteResult.FromError(InteractionCommandError.BadArgs, "Argument type and parameter type didn't match (Wild Card capture/Component value)")).ConfigureAwait(false); | ||||
| var readResult = isCapture ? await parameter.TypeReader.ReadAsync(context, wildcardCaptures.ElementAt(i), services).ConfigureAwait(false) : | |||||
| var readResult = isCapture ? await parameter.TypeReader.ReadAsync(context, captures[i].Value, services).ConfigureAwait(false) : | |||||
| await parameter.TypeConverter.ReadAsync(context, data, services).ConfigureAwait(false); | await parameter.TypeConverter.ReadAsync(context, data, services).ConfigureAwait(false); | ||||
| if (!readResult.IsSuccess) | if (!readResult.IsSuccess) | ||||
| @@ -75,7 +59,7 @@ namespace Discord.Interactions | |||||
| args[i] = readResult.Value; | args[i] = readResult.Value; | ||||
| } | } | ||||
| return await RunAsync(context, args, services).ConfigureAwait(false); | |||||
| return ParseResult.FromSuccess(args); | |||||
| } | } | ||||
| catch (Exception ex) | catch (Exception ex) | ||||
| { | { | ||||
| @@ -20,11 +20,14 @@ namespace Discord.Interactions | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public bool IsEnabledInDm { get; } | public bool IsEnabledInDm { get; } | ||||
| /// <inheritdoc/> | |||||
| public bool IsNsfw { get; } | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public GuildPermission? DefaultMemberPermissions { get; } | public GuildPermission? DefaultMemberPermissions { get; } | ||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public override IReadOnlyCollection<CommandParameterInfo> Parameters { get; } | |||||
| public override IReadOnlyList<CommandParameterInfo> Parameters { get; } | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public override bool SupportsWildCards => false; | public override bool SupportsWildCards => false; | ||||
| @@ -37,6 +40,7 @@ namespace Discord.Interactions | |||||
| { | { | ||||
| CommandType = builder.CommandType; | CommandType = builder.CommandType; | ||||
| DefaultPermission = builder.DefaultPermission; | DefaultPermission = builder.DefaultPermission; | ||||
| IsNsfw = builder.IsNsfw; | |||||
| IsEnabledInDm = builder.IsEnabledInDm; | IsEnabledInDm = builder.IsEnabledInDm; | ||||
| DefaultMemberPermissions = builder.DefaultMemberPermissions; | DefaultMemberPermissions = builder.DefaultMemberPermissions; | ||||
| Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray(); | Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray(); | ||||
| @@ -14,18 +14,23 @@ namespace Discord.Interactions | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public override async Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services) | public override async Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services) | ||||
| { | { | ||||
| if (context.Interaction is not IMessageCommandInteraction messageCommand) | |||||
| if (context.Interaction is not IMessageCommandInteraction) | |||||
| return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Message Command Interation"); | return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Message Command Interation"); | ||||
| return await base.ExecuteAsync(context, services).ConfigureAwait(false); | |||||
| } | |||||
| protected override Task<IResult> ParseArgumentsAsync(IInteractionContext context, IServiceProvider services) | |||||
| { | |||||
| try | try | ||||
| { | { | ||||
| object[] args = new object[1] { messageCommand.Data.Message }; | |||||
| object[] args = new object[1] { (context.Interaction as IMessageCommandInteraction).Data.Message }; | |||||
| return await RunAsync(context, args, services).ConfigureAwait(false); | |||||
| return Task.FromResult(ParseResult.FromSuccess(args) as IResult); | |||||
| } | } | ||||
| catch (Exception ex) | catch (Exception ex) | ||||
| { | { | ||||
| return ExecuteResult.FromError(ex); | |||||
| return Task.FromResult(ParseResult.FromError(ex) as IResult); | |||||
| } | } | ||||
| } | } | ||||
| @@ -17,15 +17,20 @@ namespace Discord.Interactions | |||||
| if (context.Interaction is not IUserCommandInteraction userCommand) | if (context.Interaction is not IUserCommandInteraction userCommand) | ||||
| return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Message Command Interation"); | return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Message Command Interation"); | ||||
| return await base.ExecuteAsync(context, services).ConfigureAwait(false); | |||||
| } | |||||
| protected override Task<IResult> ParseArgumentsAsync(IInteractionContext context, IServiceProvider services) | |||||
| { | |||||
| try | try | ||||
| { | { | ||||
| object[] args = new object[1] { userCommand.Data.User }; | |||||
| object[] args = new object[1] { (context.Interaction as IUserCommandInteraction).Data.User }; | |||||
| return await RunAsync(context, args, services).ConfigureAwait(false); | |||||
| return Task.FromResult(ParseResult.FromSuccess(args) as IResult); | |||||
| } | } | ||||
| catch (Exception ex) | catch (Exception ex) | ||||
| { | { | ||||
| return ExecuteResult.FromError(ex); | |||||
| return Task.FromResult(ParseResult.FromError(ex) as IResult); | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,7 +1,6 @@ | |||||
| using System; | using System; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
| using System.Diagnostics.Tracing; | |||||
| using System.Linq; | using System.Linq; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| namespace Discord.Interactions | namespace Discord.Interactions | ||||
| @@ -20,7 +19,7 @@ namespace Discord.Interactions | |||||
| public override bool SupportsWildCards => true; | public override bool SupportsWildCards => true; | ||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public override IReadOnlyCollection<ModalCommandParameterInfo> Parameters { get; } | |||||
| public override IReadOnlyList<ModalCommandParameterInfo> Parameters { get; } | |||||
| internal ModalCommandInfo(Builders.ModalCommandBuilder builder, ModuleInfo module, InteractionService commandService) : base(builder, module, commandService) | internal ModalCommandInfo(Builders.ModalCommandBuilder builder, ModuleInfo module, InteractionService commandService) : base(builder, module, commandService) | ||||
| { | { | ||||
| @@ -30,34 +29,29 @@ namespace Discord.Interactions | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public override async Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services) | public override async Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services) | ||||
| => await ExecuteAsync(context, services, null).ConfigureAwait(false); | |||||
| /// <summary> | |||||
| /// Execute this command using dependency injection. | |||||
| /// </summary> | |||||
| /// <param name="context">Context that will be injected to the <see cref="InteractionModuleBase{T}"/>.</param> | |||||
| /// <param name="services">Services that will be used while initializing the <see cref="InteractionModuleBase{T}"/>.</param> | |||||
| /// <param name="additionalArgs">Provide additional string parameters to the method along with the auto generated parameters.</param> | |||||
| /// <returns> | |||||
| /// A task representing the asynchronous command execution process. | |||||
| /// </returns> | |||||
| public async Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services, params string[] additionalArgs) | |||||
| { | { | ||||
| if (context.Interaction is not IModalInteraction modalInteraction) | if (context.Interaction is not IModalInteraction modalInteraction) | ||||
| return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Modal Interaction."); | return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Modal Interaction."); | ||||
| return await base.ExecuteAsync(context, services).ConfigureAwait(false); | |||||
| } | |||||
| protected override async Task<IResult> ParseArgumentsAsync(IInteractionContext context, IServiceProvider services) | |||||
| { | |||||
| var captures = (context as IRouteMatchContainer)?.SegmentMatches?.ToList(); | |||||
| var captureCount = captures?.Count() ?? 0; | |||||
| try | try | ||||
| { | { | ||||
| var args = new object[Parameters.Count]; | var args = new object[Parameters.Count]; | ||||
| var captureCount = additionalArgs?.Length ?? 0; | |||||
| for(var i = 0; i < Parameters.Count; i++) | |||||
| for (var i = 0; i < Parameters.Count; i++) | |||||
| { | { | ||||
| var parameter = Parameters.ElementAt(i); | var parameter = Parameters.ElementAt(i); | ||||
| if(i < captureCount) | |||||
| if (i < captureCount) | |||||
| { | { | ||||
| var readResult = await parameter.TypeReader.ReadAsync(context, additionalArgs[i], services).ConfigureAwait(false); | |||||
| var readResult = await parameter.TypeReader.ReadAsync(context, captures[i].Value, services).ConfigureAwait(false); | |||||
| if (!readResult.IsSuccess) | if (!readResult.IsSuccess) | ||||
| return await InvokeEventAndReturn(context, readResult).ConfigureAwait(false); | return await InvokeEventAndReturn(context, readResult).ConfigureAwait(false); | ||||
| @@ -69,13 +63,14 @@ namespace Discord.Interactions | |||||
| if (!modalResult.IsSuccess) | if (!modalResult.IsSuccess) | ||||
| return await InvokeEventAndReturn(context, modalResult).ConfigureAwait(false); | return await InvokeEventAndReturn(context, modalResult).ConfigureAwait(false); | ||||
| if (modalResult is not ParseResult parseResult) | |||||
| if (modalResult is not TypeConverterResult converterResult) | |||||
| return await InvokeEventAndReturn(context, ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command parameter parsing failed for an unknown reason.")); | return await InvokeEventAndReturn(context, ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command parameter parsing failed for an unknown reason.")); | ||||
| args[i] = parseResult.Value; | |||||
| args[i] = converterResult.Value; | |||||
| } | } | ||||
| } | } | ||||
| return await RunAsync(context, args, services); | |||||
| return ParseResult.FromSuccess(args); | |||||
| } | } | ||||
| catch (Exception ex) | catch (Exception ex) | ||||
| { | { | ||||
| @@ -29,11 +29,14 @@ namespace Discord.Interactions | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public bool IsEnabledInDm { get; } | public bool IsEnabledInDm { get; } | ||||
| /// <inheritdoc/> | |||||
| public bool IsNsfw { get; } | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public GuildPermission? DefaultMemberPermissions { get; } | public GuildPermission? DefaultMemberPermissions { get; } | ||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public override IReadOnlyCollection<SlashCommandParameterInfo> Parameters { get; } | |||||
| public override IReadOnlyList<SlashCommandParameterInfo> Parameters { get; } | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public override bool SupportsWildCards => false; | public override bool SupportsWildCards => false; | ||||
| @@ -41,13 +44,14 @@ namespace Discord.Interactions | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the flattened collection of command parameters and complex parameter fields. | /// Gets the flattened collection of command parameters and complex parameter fields. | ||||
| /// </summary> | /// </summary> | ||||
| public IReadOnlyCollection<SlashCommandParameterInfo> FlattenedParameters { get; } | |||||
| public IReadOnlyList<SlashCommandParameterInfo> FlattenedParameters { get; } | |||||
| internal SlashCommandInfo (Builders.SlashCommandBuilder builder, ModuleInfo module, InteractionService commandService) : base(builder, module, commandService) | |||||
| internal SlashCommandInfo(Builders.SlashCommandBuilder builder, ModuleInfo module, InteractionService commandService) : base(builder, module, commandService) | |||||
| { | { | ||||
| Description = builder.Description; | Description = builder.Description; | ||||
| DefaultPermission = builder.DefaultPermission; | DefaultPermission = builder.DefaultPermission; | ||||
| IsEnabledInDm = builder.IsEnabledInDm; | IsEnabledInDm = builder.IsEnabledInDm; | ||||
| IsNsfw = builder.IsNsfw; | |||||
| DefaultMemberPermissions = builder.DefaultMemberPermissions; | DefaultMemberPermissions = builder.DefaultMemberPermissions; | ||||
| Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray(); | Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray(); | ||||
| FlattenedParameters = FlattenParameters(Parameters).ToImmutableArray(); | FlattenedParameters = FlattenParameters(Parameters).ToImmutableArray(); | ||||
| @@ -60,49 +64,45 @@ namespace Discord.Interactions | |||||
| } | } | ||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public override async Task<IResult> ExecuteAsync (IInteractionContext context, IServiceProvider services) | |||||
| public override async Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services) | |||||
| { | { | ||||
| if(context.Interaction is not ISlashCommandInteraction slashCommand) | |||||
| if (context.Interaction is not ISlashCommandInteraction) | |||||
| return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Slash Command Interaction"); | return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Slash Command Interaction"); | ||||
| var options = slashCommand.Data.Options; | |||||
| while (options != null && options.Any(x => x.Type == ApplicationCommandOptionType.SubCommand || x.Type == ApplicationCommandOptionType.SubCommandGroup)) | |||||
| options = options.ElementAt(0)?.Options; | |||||
| return await ExecuteAsync(context, Parameters, options?.ToList(), services); | |||||
| return await base.ExecuteAsync(context, services); | |||||
| } | } | ||||
| private async Task<IResult> ExecuteAsync (IInteractionContext context, IEnumerable<SlashCommandParameterInfo> paramList, | |||||
| List<IApplicationCommandInteractionDataOption> argList, IServiceProvider services) | |||||
| protected override async Task<IResult> ParseArgumentsAsync(IInteractionContext context, IServiceProvider services) | |||||
| { | { | ||||
| try | |||||
| List<IApplicationCommandInteractionDataOption> GetOptions() | |||||
| { | { | ||||
| var slashCommandParameterInfos = paramList.ToList(); | |||||
| var args = new object[slashCommandParameterInfos.Count]; | |||||
| for (var i = 0; i < slashCommandParameterInfos.Count; i++) | |||||
| { | |||||
| var parameter = slashCommandParameterInfos[i]; | |||||
| var result = await ParseArgument(parameter, context, argList, services).ConfigureAwait(false); | |||||
| if (!result.IsSuccess) | |||||
| return await InvokeEventAndReturn(context, result).ConfigureAwait(false); | |||||
| var options = (context.Interaction as ISlashCommandInteraction).Data.Options; | |||||
| if (result is not ParseResult parseResult) | |||||
| return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command parameter parsing failed for an unknown reason."); | |||||
| while (options != null && options.Any(x => x.Type == ApplicationCommandOptionType.SubCommand || x.Type == ApplicationCommandOptionType.SubCommandGroup)) | |||||
| options = options.ElementAt(0)?.Options; | |||||
| args[i] = parseResult.Value; | |||||
| } | |||||
| return await RunAsync(context, args, services).ConfigureAwait(false); | |||||
| return options.ToList(); | |||||
| } | } | ||||
| catch(Exception ex) | |||||
| var options = GetOptions(); | |||||
| var args = new object[Parameters.Count]; | |||||
| for(var i = 0; i < Parameters.Count; i++) | |||||
| { | { | ||||
| return await InvokeEventAndReturn(context, ExecuteResult.FromError(ex)).ConfigureAwait(false); | |||||
| var parameter = Parameters[i]; | |||||
| var result = await ParseArgumentAsync(parameter, context, options, services).ConfigureAwait(false); | |||||
| if (!result.IsSuccess) | |||||
| return await InvokeEventAndReturn(context, ParseResult.FromError(result)).ConfigureAwait(false); | |||||
| if (result is not TypeConverterResult converterResult) | |||||
| return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Complex command parsing failed for an unknown reason."); | |||||
| args[i] = converterResult.Value; | |||||
| } | } | ||||
| return ParseResult.FromSuccess(args); | |||||
| } | } | ||||
| private async Task<IResult> ParseArgument(SlashCommandParameterInfo parameterInfo, IInteractionContext context, List<IApplicationCommandInteractionDataOption> argList, | |||||
| private async ValueTask<IResult> ParseArgumentAsync(SlashCommandParameterInfo parameterInfo, IInteractionContext context, List<IApplicationCommandInteractionDataOption> argList, | |||||
| IServiceProvider services) | IServiceProvider services) | ||||
| { | { | ||||
| if (parameterInfo.IsComplexParameter) | if (parameterInfo.IsComplexParameter) | ||||
| @@ -111,32 +111,29 @@ namespace Discord.Interactions | |||||
| for (var i = 0; i < ctorArgs.Length; i++) | for (var i = 0; i < ctorArgs.Length; i++) | ||||
| { | { | ||||
| var result = await ParseArgument(parameterInfo.ComplexParameterFields.ElementAt(i), context, argList, services).ConfigureAwait(false); | |||||
| var result = await ParseArgumentAsync(parameterInfo.ComplexParameterFields.ElementAt(i), context, argList, services).ConfigureAwait(false); | |||||
| if (!result.IsSuccess) | if (!result.IsSuccess) | ||||
| return result; | return result; | ||||
| if (result is not ParseResult parseResult) | |||||
| if (result is not TypeConverterResult converterResult) | |||||
| return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Complex command parsing failed for an unknown reason."); | return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Complex command parsing failed for an unknown reason."); | ||||
| ctorArgs[i] = parseResult.Value; | |||||
| ctorArgs[i] = converterResult.Value; | |||||
| } | } | ||||
| return ParseResult.FromSuccess(parameterInfo._complexParameterInitializer(ctorArgs)); | |||||
| return TypeConverterResult.FromSuccess(parameterInfo._complexParameterInitializer(ctorArgs)); | |||||
| } | } | ||||
| var arg = argList?.Find(x => string.Equals(x.Name, parameterInfo.Name, StringComparison.OrdinalIgnoreCase)); | var arg = argList?.Find(x => string.Equals(x.Name, parameterInfo.Name, StringComparison.OrdinalIgnoreCase)); | ||||
| if (arg == default) | if (arg == default) | ||||
| return parameterInfo.IsRequired ? ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command was invoked with too few parameters") : | return parameterInfo.IsRequired ? ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command was invoked with too few parameters") : | ||||
| ParseResult.FromSuccess(parameterInfo.DefaultValue); | |||||
| TypeConverterResult.FromSuccess(parameterInfo.DefaultValue); | |||||
| var typeConverter = parameterInfo.TypeConverter; | var typeConverter = parameterInfo.TypeConverter; | ||||
| var readResult = await typeConverter.ReadAsync(context, arg, services).ConfigureAwait(false); | var readResult = await typeConverter.ReadAsync(context, arg, services).ConfigureAwait(false); | ||||
| if (!readResult.IsSuccess) | |||||
| return readResult; | |||||
| return ParseResult.FromSuccess(readResult.Value); | |||||
| return readResult; | |||||
| } | } | ||||
| protected override Task InvokeModuleEvent (IInteractionContext context, IResult result) | protected override Task InvokeModuleEvent (IInteractionContext context, IResult result) | ||||
| @@ -28,6 +28,11 @@ namespace Discord.Interactions | |||||
| /// </summary> | /// </summary> | ||||
| public bool IsEnabledInDm { get; } | public bool IsEnabledInDm { get; } | ||||
| /// <summary> | |||||
| /// Gets whether this command can is age restricted. | |||||
| /// </summary> | |||||
| public bool IsNsfw { get; } | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the default permissions needed for executing this command. | /// Gets the default permissions needed for executing this command. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -65,6 +65,8 @@ namespace Discord.Interactions | |||||
| /// </summary> | /// </summary> | ||||
| IReadOnlyCollection<IParameterInfo> Parameters { get; } | IReadOnlyCollection<IParameterInfo> Parameters { get; } | ||||
| bool TreatNameAsRegex { get; } | |||||
| /// <summary> | /// <summary> | ||||
| /// Executes the command with the provided context. | /// Executes the command with the provided context. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -103,7 +103,7 @@ namespace Discord.Interactions | |||||
| public async Task<IResult> CreateModalAsync(IInteractionContext context, IServiceProvider services = null, bool throwOnMissingField = false) | public async Task<IResult> CreateModalAsync(IInteractionContext context, IServiceProvider services = null, bool throwOnMissingField = false) | ||||
| { | { | ||||
| if (context.Interaction is not IModalInteraction modalInteraction) | if (context.Interaction is not IModalInteraction modalInteraction) | ||||
| return ParseResult.FromError(InteractionCommandError.Unsuccessful, "Provided context doesn't belong to a Modal Interaction."); | |||||
| return TypeConverterResult.FromError(InteractionCommandError.Unsuccessful, "Provided context doesn't belong to a Modal Interaction."); | |||||
| services ??= EmptyServiceProvider.Instance; | services ??= EmptyServiceProvider.Instance; | ||||
| @@ -120,7 +120,7 @@ namespace Discord.Interactions | |||||
| if (!throwOnMissingField) | if (!throwOnMissingField) | ||||
| args[i] = input.DefaultValue; | args[i] = input.DefaultValue; | ||||
| else | else | ||||
| return ParseResult.FromError(InteractionCommandError.BadArgs, $"Modal interaction is missing the required field: {input.CustomId}"); | |||||
| return TypeConverterResult.FromError(InteractionCommandError.BadArgs, $"Modal interaction is missing the required field: {input.CustomId}"); | |||||
| } | } | ||||
| else | else | ||||
| { | { | ||||
| @@ -133,7 +133,7 @@ namespace Discord.Interactions | |||||
| } | } | ||||
| } | } | ||||
| return ParseResult.FromSuccess(_initializer(args)); | |||||
| return TypeConverterResult.FromSuccess(_initializer(args)); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -49,6 +49,11 @@ namespace Discord.Interactions | |||||
| /// </summary> | /// </summary> | ||||
| public bool IsEnabledInDm { get; } | public bool IsEnabledInDm { get; } | ||||
| /// <summary> | |||||
| /// Gets whether this command is age restricted. | |||||
| /// </summary> | |||||
| public bool IsNsfw { get; } | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the default permissions needed for executing this command. | /// Gets the default permissions needed for executing this command. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -121,6 +126,7 @@ namespace Discord.Interactions | |||||
| Description = builder.Description; | Description = builder.Description; | ||||
| Parent = parent; | Parent = parent; | ||||
| DefaultPermission = builder.DefaultPermission; | DefaultPermission = builder.DefaultPermission; | ||||
| IsNsfw = builder.IsNsfw; | |||||
| IsEnabledInDm = builder.IsEnabledInDm; | IsEnabledInDm = builder.IsEnabledInDm; | ||||
| DefaultMemberPermissions = BuildDefaultMemberPermissions(builder); | DefaultMemberPermissions = BuildDefaultMemberPermissions(builder); | ||||
| SlashCommands = BuildSlashCommands(builder).ToImmutableArray(); | SlashCommands = BuildSlashCommands(builder).ToImmutableArray(); | ||||
| @@ -822,7 +822,7 @@ namespace Discord.Interactions | |||||
| SetMatchesIfApplicable(context, result); | SetMatchesIfApplicable(context, result); | ||||
| return await result.Command.ExecuteAsync(context, services, result.RegexCaptureGroups).ConfigureAwait(false); | |||||
| return await result.Command.ExecuteAsync(context, services).ConfigureAwait(false); | |||||
| } | } | ||||
| private async Task<IResult> ExecuteAutocompleteAsync (IInteractionContext context, IAutocompleteInteraction interaction, IServiceProvider services ) | private async Task<IResult> ExecuteAutocompleteAsync (IInteractionContext context, IAutocompleteInteraction interaction, IServiceProvider services ) | ||||
| @@ -869,7 +869,7 @@ namespace Discord.Interactions | |||||
| SetMatchesIfApplicable(context, result); | SetMatchesIfApplicable(context, result); | ||||
| return await result.Command.ExecuteAsync(context, services, result.RegexCaptureGroups).ConfigureAwait(false); | |||||
| return await result.Command.ExecuteAsync(context, services).ConfigureAwait(false); | |||||
| } | } | ||||
| private static void SetMatchesIfApplicable<T>(IInteractionContext context, SearchResult<T> searchResult) | private static void SetMatchesIfApplicable<T>(IInteractionContext context, SearchResult<T> searchResult) | ||||
| @@ -2,14 +2,13 @@ using System; | |||||
| using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Linq; | using System.Linq; | ||||
| using System.Text; | |||||
| using System.Text.RegularExpressions; | using System.Text.RegularExpressions; | ||||
| namespace Discord.Interactions | namespace Discord.Interactions | ||||
| { | { | ||||
| internal class CommandMapNode<T> where T : class, ICommandInfo | internal class CommandMapNode<T> where T : class, ICommandInfo | ||||
| { | |||||
| private const string RegexWildCardExp = "(\\S+)?"; | |||||
| { | |||||
| private readonly string _wildCardStr = "*"; | private readonly string _wildCardStr = "*"; | ||||
| private readonly ConcurrentDictionary<string, CommandMapNode<T>> _nodes; | private readonly ConcurrentDictionary<string, CommandMapNode<T>> _nodes; | ||||
| private readonly ConcurrentDictionary<string, T> _commands; | private readonly ConcurrentDictionary<string, T> _commands; | ||||
| @@ -35,10 +34,8 @@ namespace Discord.Interactions | |||||
| { | { | ||||
| if (keywords.Count == index + 1) | if (keywords.Count == index + 1) | ||||
| { | { | ||||
| if (commandInfo.SupportsWildCards && commandInfo.Name.Contains(_wildCardStr)) | |||||
| if (commandInfo.SupportsWildCards && RegexUtils.TryBuildRegexPattern(commandInfo, _wildCardStr, out var patternStr)) | |||||
| { | { | ||||
| var escapedStr = RegexUtils.EscapeExcluding(commandInfo.Name, _wildCardStr.ToArray()); | |||||
| var patternStr = "\\A" + escapedStr.Replace(_wildCardStr, RegexWildCardExp) + "\\Z"; | |||||
| var regex = new Regex(patternStr, RegexOptions.Singleline | RegexOptions.Compiled); | var regex = new Regex(patternStr, RegexOptions.Singleline | RegexOptions.Compiled); | ||||
| if (!_wildCardCommands.TryAdd(regex, commandInfo)) | if (!_wildCardCommands.TryAdd(regex, commandInfo)) | ||||
| @@ -2,9 +2,9 @@ using System; | |||||
| namespace Discord.Interactions | namespace Discord.Interactions | ||||
| { | { | ||||
| internal struct ParseResult : IResult | |||||
| public struct ParseResult : IResult | |||||
| { | { | ||||
| public object Value { get; } | |||||
| public object[] Args { get; } | |||||
| public InteractionCommandError? Error { get; } | public InteractionCommandError? Error { get; } | ||||
| @@ -12,15 +12,15 @@ namespace Discord.Interactions | |||||
| public bool IsSuccess => !Error.HasValue; | public bool IsSuccess => !Error.HasValue; | ||||
| private ParseResult(object value, InteractionCommandError? error, string reason) | |||||
| private ParseResult(object[] args, InteractionCommandError? error, string reason) | |||||
| { | { | ||||
| Value = value; | |||||
| Args = args; | |||||
| Error = error; | Error = error; | ||||
| ErrorReason = reason; | ErrorReason = reason; | ||||
| } | } | ||||
| public static ParseResult FromSuccess(object value) => | |||||
| new ParseResult(value, null, null); | |||||
| public static ParseResult FromSuccess(object[] args) => | |||||
| new ParseResult(args, null, null); | |||||
| public static ParseResult FromError(Exception exception) => | public static ParseResult FromError(Exception exception) => | ||||
| new ParseResult(null, InteractionCommandError.Exception, exception.Message); | new ParseResult(null, InteractionCommandError.Exception, exception.Message); | ||||
| @@ -1,5 +1,7 @@ | |||||
| using System; | using System; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Diagnostics.CodeAnalysis; | |||||
| using System.Linq; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| namespace Discord.Interactions | namespace Discord.Interactions | ||||
| @@ -17,27 +19,56 @@ namespace Discord.Interactions | |||||
| throw new InvalidOperationException($"{nameof(DefaultArrayComponentConverter<T>)} cannot be used to convert a non-array type."); | throw new InvalidOperationException($"{nameof(DefaultArrayComponentConverter<T>)} cannot be used to convert a non-array type."); | ||||
| _underlyingType = typeof(T).GetElementType(); | _underlyingType = typeof(T).GetElementType(); | ||||
| _typeReader = interactionService.GetTypeReader(_underlyingType); | |||||
| _typeReader = true switch | |||||
| { | |||||
| _ when typeof(IUser).IsAssignableFrom(_underlyingType) | |||||
| || typeof(IChannel).IsAssignableFrom(_underlyingType) | |||||
| || typeof(IMentionable).IsAssignableFrom(_underlyingType) | |||||
| || typeof(IRole).IsAssignableFrom(_underlyingType) => null, | |||||
| _ => interactionService.GetTypeReader(_underlyingType) | |||||
| }; | |||||
| } | } | ||||
| public override async Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) | public override async Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) | ||||
| { | { | ||||
| var results = new List<TypeConverterResult>(); | |||||
| var objs = new List<object>(); | |||||
| if(_typeReader is not null && option.Values.Count > 0) | |||||
| foreach (var value in option.Values) | |||||
| { | |||||
| var result = await _typeReader.ReadAsync(context, value, services).ConfigureAwait(false); | |||||
| if (!result.IsSuccess) | |||||
| return result; | |||||
| foreach (var value in option.Values) | |||||
| objs.Add(result.Value); | |||||
| } | |||||
| else | |||||
| { | { | ||||
| var result = await _typeReader.ReadAsync(context, value, services).ConfigureAwait(false); | |||||
| var users = new Dictionary<ulong, IUser>(); | |||||
| if (option.Users is not null) | |||||
| foreach (var user in option.Users) | |||||
| users[user.Id] = user; | |||||
| if(option.Members is not null) | |||||
| foreach(var member in option.Members) | |||||
| users[member.Id] = member; | |||||
| objs.AddRange(users.Values); | |||||
| if (!result.IsSuccess) | |||||
| return result; | |||||
| if(option.Roles is not null) | |||||
| objs.AddRange(option.Roles); | |||||
| results.Add(result); | |||||
| if (option.Channels is not null) | |||||
| objs.AddRange(option.Channels); | |||||
| } | } | ||||
| var destination = Array.CreateInstance(_underlyingType, results.Count); | |||||
| var destination = Array.CreateInstance(_underlyingType, objs.Count); | |||||
| for (var i = 0; i < results.Count; i++) | |||||
| destination.SetValue(results[i].Value, i); | |||||
| for (var i = 0; i < objs.Count; i++) | |||||
| destination.SetValue(objs[i], i); | |||||
| return TypeConverterResult.FromSuccess(destination); | return TypeConverterResult.FromSuccess(destination); | ||||
| } | } | ||||
| @@ -54,6 +54,7 @@ namespace Discord.Interactions | |||||
| Description = commandInfo.Description, | Description = commandInfo.Description, | ||||
| IsDefaultPermission = commandInfo.DefaultPermission, | IsDefaultPermission = commandInfo.DefaultPermission, | ||||
| IsDMEnabled = commandInfo.IsEnabledInDm, | IsDMEnabled = commandInfo.IsEnabledInDm, | ||||
| IsNsfw = commandInfo.IsNsfw, | |||||
| DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), | DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), | ||||
| }.WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary<string, string>.Empty) | }.WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary<string, string>.Empty) | ||||
| .WithDescriptionLocalizations(localizationManager?.GetAllDescriptions(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary<string, string>.Empty) | .WithDescriptionLocalizations(localizationManager?.GetAllDescriptions(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary<string, string>.Empty) | ||||
| @@ -97,7 +98,8 @@ namespace Discord.Interactions | |||||
| Name = commandInfo.Name, | Name = commandInfo.Name, | ||||
| IsDefaultPermission = commandInfo.DefaultPermission, | IsDefaultPermission = commandInfo.DefaultPermission, | ||||
| DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), | DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), | ||||
| IsDMEnabled = commandInfo.IsEnabledInDm | |||||
| IsDMEnabled = commandInfo.IsEnabledInDm, | |||||
| IsNsfw = commandInfo.IsNsfw, | |||||
| } | } | ||||
| .WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary<string, string>.Empty) | .WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary<string, string>.Empty) | ||||
| .Build(), | .Build(), | ||||
| @@ -106,6 +108,7 @@ namespace Discord.Interactions | |||||
| Name = commandInfo.Name, | Name = commandInfo.Name, | ||||
| IsDefaultPermission = commandInfo.DefaultPermission, | IsDefaultPermission = commandInfo.DefaultPermission, | ||||
| DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), | DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), | ||||
| IsNsfw = commandInfo.IsNsfw, | |||||
| IsDMEnabled = commandInfo.IsEnabledInDm | IsDMEnabled = commandInfo.IsEnabledInDm | ||||
| } | } | ||||
| .WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary<string, string>.Empty) | .WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary<string, string>.Empty) | ||||
| @@ -162,6 +165,7 @@ namespace Discord.Interactions | |||||
| Description = moduleInfo.Description, | Description = moduleInfo.Description, | ||||
| IsDefaultPermission = moduleInfo.DefaultPermission, | IsDefaultPermission = moduleInfo.DefaultPermission, | ||||
| IsDMEnabled = moduleInfo.IsEnabledInDm, | IsDMEnabled = moduleInfo.IsEnabledInDm, | ||||
| IsNsfw = moduleInfo.IsNsfw, | |||||
| DefaultMemberPermissions = moduleInfo.DefaultMemberPermissions | DefaultMemberPermissions = moduleInfo.DefaultMemberPermissions | ||||
| } | } | ||||
| .WithNameLocalizations(localizationManager?.GetAllNames(modulePath, LocalizationTarget.Group) ?? ImmutableDictionary<string, string>.Empty) | .WithNameLocalizations(localizationManager?.GetAllNames(modulePath, LocalizationTarget.Group) ?? ImmutableDictionary<string, string>.Empty) | ||||
| @@ -225,6 +229,7 @@ namespace Discord.Interactions | |||||
| IsDefaultPermission = command.IsDefaultPermission, | IsDefaultPermission = command.IsDefaultPermission, | ||||
| DefaultMemberPermissions = (GuildPermission)command.DefaultMemberPermissions.RawValue, | DefaultMemberPermissions = (GuildPermission)command.DefaultMemberPermissions.RawValue, | ||||
| IsDMEnabled = command.IsEnabledInDm, | IsDMEnabled = command.IsEnabledInDm, | ||||
| IsNsfw = command.IsNsfw, | |||||
| Options = command.Options?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional<List<ApplicationCommandOptionProperties>>.Unspecified, | Options = command.Options?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional<List<ApplicationCommandOptionProperties>>.Unspecified, | ||||
| NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty, | NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty, | ||||
| DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty, | DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty, | ||||
| @@ -234,6 +239,7 @@ namespace Discord.Interactions | |||||
| Name = command.Name, | Name = command.Name, | ||||
| IsDefaultPermission = command.IsDefaultPermission, | IsDefaultPermission = command.IsDefaultPermission, | ||||
| DefaultMemberPermissions = (GuildPermission)command.DefaultMemberPermissions.RawValue, | DefaultMemberPermissions = (GuildPermission)command.DefaultMemberPermissions.RawValue, | ||||
| IsNsfw = command.IsNsfw, | |||||
| IsDMEnabled = command.IsEnabledInDm, | IsDMEnabled = command.IsEnabledInDm, | ||||
| NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty, | NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty, | ||||
| DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty | DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty | ||||
| @@ -243,6 +249,7 @@ namespace Discord.Interactions | |||||
| Name = command.Name, | Name = command.Name, | ||||
| IsDefaultPermission = command.IsDefaultPermission, | IsDefaultPermission = command.IsDefaultPermission, | ||||
| DefaultMemberPermissions = (GuildPermission)command.DefaultMemberPermissions.RawValue, | DefaultMemberPermissions = (GuildPermission)command.DefaultMemberPermissions.RawValue, | ||||
| IsNsfw = command.IsNsfw, | |||||
| IsDMEnabled = command.IsEnabledInDm, | IsDMEnabled = command.IsEnabledInDm, | ||||
| NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty, | NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty, | ||||
| DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty | DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty | ||||
| @@ -1,3 +1,4 @@ | |||||
| using Discord.Interactions; | |||||
| using System; | using System; | ||||
| using System.Linq; | using System.Linq; | ||||
| @@ -81,5 +82,37 @@ namespace System.Text.RegularExpressions | |||||
| { | { | ||||
| return (ch <= '|' && _category[ch] >= E); | return (ch <= '|' && _category[ch] >= E); | ||||
| } | } | ||||
| internal static int GetWildCardCount(string input, string wildCardExpression) | |||||
| { | |||||
| var escapedWildCard = Regex.Escape(wildCardExpression); | |||||
| var match = Regex.Matches(input, $@"(?<!\\){escapedWildCard}|(?<!\\){{[0-9]+(?:,[0-9]*)?(?<!\\)}}"); | |||||
| return match.Count; | |||||
| } | |||||
| internal static bool TryBuildRegexPattern<T>(T commandInfo, string wildCardStr, out string pattern) where T: class, ICommandInfo | |||||
| { | |||||
| if (commandInfo.TreatNameAsRegex) | |||||
| { | |||||
| pattern = commandInfo.Name; | |||||
| return true; | |||||
| } | |||||
| if (GetWildCardCount(commandInfo.Name, wildCardStr) == 0) | |||||
| { | |||||
| pattern = null; | |||||
| return false; | |||||
| } | |||||
| var escapedWildCard = Regex.Escape(wildCardStr); | |||||
| var unquantified = Regex.Replace(commandInfo.Name, $@"(?<!\\){escapedWildCard}(?<delimiter>[^{escapedWildCard}]?)", | |||||
| @"([^\n\t${delimiter}]+)${delimiter}"); | |||||
| var quantified = Regex.Replace(unquantified, $@"(?<!\\){{(?<start>[0-9]+)(?<end>,[0-9]*)?(?<!\\)}}(?<delimiter>[^{escapedWildCard}]?)", | |||||
| @"([^\n\t${delimiter}]{${start}${end}})${delimiter}"); | |||||
| pattern = "\\A" + quantified + "\\Z"; | |||||
| return true; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -21,6 +21,10 @@ namespace Discord.API | |||||
| { | { | ||||
| ComponentType.Button => new ButtonComponent(x as Discord.ButtonComponent), | ComponentType.Button => new ButtonComponent(x as Discord.ButtonComponent), | ||||
| ComponentType.SelectMenu => new SelectMenuComponent(x as Discord.SelectMenuComponent), | ComponentType.SelectMenu => new SelectMenuComponent(x as Discord.SelectMenuComponent), | ||||
| ComponentType.ChannelSelect => new SelectMenuComponent(x as Discord.SelectMenuComponent), | |||||
| ComponentType.UserSelect => new SelectMenuComponent(x as Discord.SelectMenuComponent), | |||||
| ComponentType.RoleSelect => new SelectMenuComponent(x as Discord.SelectMenuComponent), | |||||
| ComponentType.MentionableSelect => new SelectMenuComponent(x as Discord.SelectMenuComponent), | |||||
| ComponentType.TextInput => new TextInputComponent(x as Discord.TextInputComponent), | ComponentType.TextInput => new TextInputComponent(x as Discord.TextInputComponent), | ||||
| _ => null | _ => null | ||||
| }; | }; | ||||
| @@ -44,5 +44,8 @@ namespace Discord.API | |||||
| [JsonProperty("default_member_permissions")] | [JsonProperty("default_member_permissions")] | ||||
| public Optional<GuildPermission?> DefaultMemberPermission { get; set; } | public Optional<GuildPermission?> DefaultMemberPermission { get; set; } | ||||
| [JsonProperty("nsfw")] | |||||
| public Optional<bool?> Nsfw { get; set; } | |||||
| } | } | ||||
| } | } | ||||
| @@ -70,8 +70,27 @@ namespace Discord.API | |||||
| //ForumChannel | //ForumChannel | ||||
| [JsonProperty("available_tags")] | [JsonProperty("available_tags")] | ||||
| public Optional<ForumTags[]> ForumTags { get; set; } | public Optional<ForumTags[]> ForumTags { get; set; } | ||||
| [JsonProperty("applied_tags")] | |||||
| public Optional<ulong[]> AppliedTags { get; set; } | |||||
| [JsonProperty("default_auto_archive_duration")] | [JsonProperty("default_auto_archive_duration")] | ||||
| public Optional<ThreadArchiveDuration> AutoArchiveDuration { get; set; } | public Optional<ThreadArchiveDuration> AutoArchiveDuration { get; set; } | ||||
| [JsonProperty("default_thread_rate_limit_per_user")] | |||||
| public Optional<int> ThreadRateLimitPerUser { get; set; } | |||||
| [JsonProperty("flags")] | |||||
| public Optional<ChannelFlags> Flags { get; set; } | |||||
| [JsonProperty("default_sort_order")] | |||||
| public Optional<ForumSortOrder?> DefaultSortOrder { get; set; } | |||||
| [JsonProperty("default_reaction_emoji")] | |||||
| public Optional<ForumReactionEmoji> DefaultReactionEmoji { get; set; } | |||||
| [JsonProperty("default_forum_layout")] | |||||
| public Optional<ForumLayout> DefaultForumLayout { get; set; } | |||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,12 @@ | |||||
| using Newtonsoft.Json; | |||||
| namespace Discord.API; | |||||
| public class ForumReactionEmoji | |||||
| { | |||||
| [JsonProperty("emoji_id")] | |||||
| public ulong? EmojiId { get; set; } | |||||
| [JsonProperty("emoji_name")] | |||||
| public Optional<string> EmojiName { get; set; } | |||||
| } | |||||
| @@ -17,5 +17,8 @@ namespace Discord.API | |||||
| public Optional<ulong?> EmojiId { get; set; } | public Optional<ulong?> EmojiId { get; set; } | ||||
| [JsonProperty("emoji_name")] | [JsonProperty("emoji_name")] | ||||
| public Optional<string> EmojiName { get; set; } | public Optional<string> EmojiName { get; set; } | ||||
| [JsonProperty("moderated")] | |||||
| public bool Moderated { get; set; } | |||||
| } | } | ||||
| } | } | ||||
| @@ -83,5 +83,8 @@ namespace Discord.API | |||||
| public Sticker[] Stickers { get; set; } | public Sticker[] Stickers { get; set; } | ||||
| [JsonProperty("premium_progress_bar_enabled")] | [JsonProperty("premium_progress_bar_enabled")] | ||||
| public Optional<bool> IsBoostProgressBarEnabled { get; set; } | public Optional<bool> IsBoostProgressBarEnabled { get; set; } | ||||
| [JsonProperty("welcome_screen")] | |||||
| public Optional<WelcomeScreen> WelcomeScreen { get; set; } | |||||
| } | } | ||||
| } | } | ||||
| @@ -6,9 +6,41 @@ namespace Discord.API | |||||
| { | { | ||||
| [JsonProperty("id")] | [JsonProperty("id")] | ||||
| public ulong Id { get; set; } | public ulong Id { get; set; } | ||||
| [JsonProperty("name")] | [JsonProperty("name")] | ||||
| public string Name { get; set; } | public string Name { get; set; } | ||||
| [JsonProperty("splash_hash")] | |||||
| public string SplashHash { get; set; } | |||||
| [JsonProperty("splash")] | |||||
| public Optional<string> Splash { get; set; } | |||||
| [JsonProperty("banner")] | |||||
| public Optional<string> BannerHash { get; set; } | |||||
| [JsonProperty("description")] | |||||
| public Optional<string> Description { get; set; } | |||||
| [JsonProperty("icon")] | |||||
| public Optional<string> IconHash { get; set; } | |||||
| [JsonProperty("features")] | |||||
| public GuildFeatures Features { get; set; } | |||||
| [JsonProperty("verification_level")] | |||||
| public VerificationLevel VerificationLevel { get; set; } | |||||
| [JsonProperty("vanity_url_code")] | |||||
| public Optional<string> VanityUrlCode { get; set; } | |||||
| [JsonProperty("premium_subscription_count")] | |||||
| public Optional<int> PremiumSubscriptionCount { get; set; } | |||||
| [JsonProperty("nsfw")] | |||||
| public Optional<bool?> Nsfw { get; set; } | |||||
| [JsonProperty("nsfw_level")] | |||||
| public NsfwLevel NsfwLevel { get; set; } | |||||
| [JsonProperty("welcome_screen")] | |||||
| public Optional<WelcomeScreen> WelcomeScreen { get; set; } | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,4 +1,5 @@ | |||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| using System.Collections.Generic; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| @@ -15,5 +16,8 @@ namespace Discord.API | |||||
| [JsonProperty("value")] | [JsonProperty("value")] | ||||
| public Optional<string> Value { get; set; } | public Optional<string> Value { get; set; } | ||||
| [JsonProperty("resolved")] | |||||
| public Optional<MessageComponentInteractionDataResolved> Resolved { get; set; } | |||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,19 @@ | |||||
| using Newtonsoft.Json; | |||||
| using System.Collections.Generic; | |||||
| namespace Discord.API; | |||||
| internal class MessageComponentInteractionDataResolved | |||||
| { | |||||
| [JsonProperty("users")] | |||||
| public Optional<Dictionary<string, User>> Users { get; set; } | |||||
| [JsonProperty("members")] | |||||
| public Optional<Dictionary<string, GuildMember>> Members { get; set; } | |||||
| [JsonProperty("channels")] | |||||
| public Optional<Dictionary<string, Channel>> Channels { get; set; } | |||||
| [JsonProperty("roles")] | |||||
| public Optional<Dictionary<string, Role>> Roles { get; set; } | |||||
| } | |||||
| @@ -26,6 +26,12 @@ namespace Discord.API | |||||
| [JsonProperty("disabled")] | [JsonProperty("disabled")] | ||||
| public bool Disabled { get; set; } | public bool Disabled { get; set; } | ||||
| [JsonProperty("channel_types")] | |||||
| public Optional<ChannelType[]> ChannelTypes { get; set; } | |||||
| [JsonProperty("resolved")] | |||||
| public Optional<MessageComponentInteractionDataResolved> Resolved { get; set; } | |||||
| [JsonProperty("values")] | [JsonProperty("values")] | ||||
| public Optional<string[]> Values { get; set; } | public Optional<string[]> Values { get; set; } | ||||
| public SelectMenuComponent() { } | public SelectMenuComponent() { } | ||||
| @@ -34,11 +40,12 @@ namespace Discord.API | |||||
| { | { | ||||
| Type = component.Type; | Type = component.Type; | ||||
| CustomId = component.CustomId; | CustomId = component.CustomId; | ||||
| Options = component.Options.Select(x => new SelectMenuOption(x)).ToArray(); | |||||
| Options = component.Options?.Select(x => new SelectMenuOption(x)).ToArray(); | |||||
| Placeholder = component.Placeholder; | Placeholder = component.Placeholder; | ||||
| MinValues = component.MinValues; | MinValues = component.MinValues; | ||||
| MaxValues = component.MaxValues; | MaxValues = component.MaxValues; | ||||
| Disabled = component.IsDisabled; | Disabled = component.IsDisabled; | ||||
| ChannelTypes = component.ChannelTypes.ToArray(); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,12 @@ | |||||
| using Newtonsoft.Json; | |||||
| namespace Discord.API; | |||||
| internal class WelcomeScreen | |||||
| { | |||||
| [JsonProperty("description")] | |||||
| public Optional<string> Description { get; set; } | |||||
| [JsonProperty("welcome_channels")] | |||||
| public WelcomeScreenChannel[] WelcomeChannels { get; set; } | |||||
| } | |||||
| @@ -0,0 +1,18 @@ | |||||
| using Newtonsoft.Json; | |||||
| namespace Discord.API; | |||||
| internal class WelcomeScreenChannel | |||||
| { | |||||
| [JsonProperty("channel_id")] | |||||
| public ulong ChannelId { get; set; } | |||||
| [JsonProperty("description")] | |||||
| public string Description { get; set; } | |||||
| [JsonProperty("emoji_id")] | |||||
| public Optional<ulong?> EmojiId { get; set; } | |||||
| [JsonProperty("emoji_name")] | |||||
| public Optional<string> EmojiName{ get; set; } | |||||
| } | |||||
| @@ -35,9 +35,12 @@ namespace Discord.API.Rest | |||||
| [JsonProperty("default_member_permissions")] | [JsonProperty("default_member_permissions")] | ||||
| public Optional<GuildPermission?> DefaultMemberPermission { get; set; } | public Optional<GuildPermission?> DefaultMemberPermission { get; set; } | ||||
| [JsonProperty("nsfw")] | |||||
| public Optional<bool> Nsfw { get; set; } | |||||
| public CreateApplicationCommandParams() { } | public CreateApplicationCommandParams() { } | ||||
| public CreateApplicationCommandParams(string name, string description, ApplicationCommandType type, ApplicationCommandOption[] options = null, | public CreateApplicationCommandParams(string name, string description, ApplicationCommandType type, ApplicationCommandOption[] options = null, | ||||
| IDictionary<string, string> nameLocalizations = null, IDictionary<string, string> descriptionLocalizations = null) | |||||
| IDictionary<string, string> nameLocalizations = null, IDictionary<string, string> descriptionLocalizations = null, bool nsfw = false) | |||||
| { | { | ||||
| Name = name; | Name = name; | ||||
| Description = description; | Description = description; | ||||
| @@ -45,6 +48,7 @@ namespace Discord.API.Rest | |||||
| Type = type; | Type = type; | ||||
| NameLocalizations = nameLocalizations?.ToDictionary(x => x.Key, x => x.Value) ?? Optional<Dictionary<string, string>>.Unspecified; | 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; | DescriptionLocalizations = descriptionLocalizations?.ToDictionary(x => x.Key, x => x.Value) ?? Optional<Dictionary<string, string>>.Unspecified; | ||||
| Nsfw = nsfw; | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -23,6 +23,8 @@ namespace Discord.API.Rest | |||||
| public Optional<bool> IsNsfw { get; set; } | public Optional<bool> IsNsfw { get; set; } | ||||
| [JsonProperty("rate_limit_per_user")] | [JsonProperty("rate_limit_per_user")] | ||||
| public Optional<int> SlowModeInterval { get; set; } | public Optional<int> SlowModeInterval { get; set; } | ||||
| [JsonProperty("default_auto_archive_duration")] | |||||
| public Optional<ThreadArchiveDuration> DefaultAutoArchiveDuration { get; set; } | |||||
| //Voice channels | //Voice channels | ||||
| [JsonProperty("bitrate")] | [JsonProperty("bitrate")] | ||||
| @@ -30,6 +32,16 @@ namespace Discord.API.Rest | |||||
| [JsonProperty("user_limit")] | [JsonProperty("user_limit")] | ||||
| public Optional<int?> UserLimit { get; set; } | public Optional<int?> UserLimit { get; set; } | ||||
| //Forum channels | |||||
| [JsonProperty("default_reaction_emoji")] | |||||
| public Optional<ModifyForumReactionEmojiParams> DefaultReactionEmoji { get; set; } | |||||
| [JsonProperty("default_thread_rate_limit_per_user")] | |||||
| public Optional<int> ThreadRateLimitPerUser { get; set; } | |||||
| [JsonProperty("available_tags")] | |||||
| public Optional<ModifyForumTagParams[]> AvailableTags { get; set; } | |||||
| [JsonProperty("default_sort_order")] | |||||
| public Optional<ForumSortOrder?> DefaultSortOrder { get; set; } | |||||
| public CreateGuildChannelParams(string name, ChannelType type) | public CreateGuildChannelParams(string name, ChannelType type) | ||||
| { | { | ||||
| Name = name; | Name = name; | ||||
| @@ -27,6 +27,7 @@ namespace Discord.API.Rest | |||||
| public Optional<ActionRowComponent[]> MessageComponent { get; set; } | public Optional<ActionRowComponent[]> MessageComponent { get; set; } | ||||
| public Optional<MessageFlags?> Flags { get; set; } | public Optional<MessageFlags?> Flags { get; set; } | ||||
| public Optional<ulong[]> Stickers { get; set; } | public Optional<ulong[]> Stickers { get; set; } | ||||
| public Optional<ulong[]> TagIds { get; set; } | |||||
| public CreateMultipartPostAsync(params FileAttachment[] attachments) | public CreateMultipartPostAsync(params FileAttachment[] attachments) | ||||
| { | { | ||||
| @@ -59,6 +60,8 @@ namespace Discord.API.Rest | |||||
| message["sticker_ids"] = Stickers.Value; | message["sticker_ids"] = Stickers.Value; | ||||
| if (Flags.IsSpecified) | if (Flags.IsSpecified) | ||||
| message["flags"] = Flags.Value; | message["flags"] = Flags.Value; | ||||
| if (TagIds.IsSpecified) | |||||
| message["applied_tags"] = TagIds.Value; | |||||
| List<object> attachments = new(); | List<object> attachments = new(); | ||||
| @@ -21,5 +21,8 @@ namespace Discord.API.Rest | |||||
| [JsonProperty("message")] | [JsonProperty("message")] | ||||
| public ForumThreadMessage Message { get; set; } | public ForumThreadMessage Message { get; set; } | ||||
| [JsonProperty("applied_tags")] | |||||
| public Optional<ulong[]> Tags { get; set; } | |||||
| } | } | ||||
| } | } | ||||
| @@ -17,6 +17,12 @@ namespace Discord.API.Rest | |||||
| [JsonProperty("default_permission")] | [JsonProperty("default_permission")] | ||||
| public Optional<bool> DefaultPermission { get; set; } | public Optional<bool> DefaultPermission { get; set; } | ||||
| [JsonProperty("nsfw")] | |||||
| public Optional<bool> Nsfw { get; set; } | |||||
| [JsonProperty("default_member_permissions")] | |||||
| public Optional<GuildPermission?> DefaultMemberPermission { get; set; } | |||||
| [JsonProperty("name_localizations")] | [JsonProperty("name_localizations")] | ||||
| public Optional<Dictionary<string, string>> NameLocalizations { get; set; } | public Optional<Dictionary<string, string>> NameLocalizations { get; set; } | ||||
| @@ -0,0 +1,26 @@ | |||||
| using Newtonsoft.Json; | |||||
| namespace Discord.API.Rest; | |||||
| [JsonObject(MemberSerialization = MemberSerialization.OptIn)] | |||||
| internal class ModifyForumChannelParams : ModifyTextChannelParams | |||||
| { | |||||
| [JsonProperty("available_tags")] | |||||
| public Optional<ModifyForumTagParams[]> Tags { get; set; } | |||||
| [JsonProperty("default_thread_rate_limit_per_user")] | |||||
| public Optional<int> DefaultSlowModeInterval { get; set; } | |||||
| [JsonProperty("rate_limit_per_user")] | |||||
| public Optional<int> ThreadCreationInterval { get; set; } | |||||
| [JsonProperty("default_reaction_emoji")] | |||||
| public Optional<ModifyForumReactionEmojiParams> DefaultReactionEmoji { get; set; } | |||||
| [JsonProperty("default_sort_order")] | |||||
| public Optional<ForumSortOrder> DefaultSortOrder { get; set; } | |||||
| [JsonProperty("default_forum_layout")] | |||||
| public Optional<ForumLayout> DefaultLayout { get; set; } | |||||
| } | |||||
| @@ -0,0 +1,15 @@ | |||||
| using Newtonsoft.Json; | |||||
| namespace Discord.API; | |||||
| [JsonObject(MemberSerialization = MemberSerialization.OptIn)] | |||||
| public class ModifyForumReactionEmojiParams | |||||
| { | |||||
| [JsonProperty("emoji_id")] | |||||
| public Optional<ulong?> EmojiId { get; set; } | |||||
| [JsonProperty("emoji_name")] | |||||
| public Optional<string> EmojiName { get; set; } | |||||
| } | |||||