From c8f175e11abd900fc6e95a660def46648fd44dc8 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Wed, 9 Feb 2022 00:17:56 -0400 Subject: [PATCH] Feature: Implement modals (#2087) * Implement Modals (#428) * Socket Modal Support * fix shareded client support * Properly use `HasResponded` instead of `_hasResponded` * `ModalBuilder` and `TextInputBuilder` validation. * make orginisation more consistant. * Rest Modals. * Docs + add missing methods * fix message signatures and missing abstract members * modal changes * um????? * update modal docs * update docs - again for some reason * cleanup * fix message signatures * add modal commands support to interaction service * Fix _hasResponded * update to new unsupported standard. * Sending modals with Interaction service. * fix spelling in ComponentBuilder * sending IModals when responding to interactions * interaction service modals * fix rest modals * spelling and minor improvements. * improve interaction service modal proformance * use precompiled lambda for interaction service modals * respect user compiled lambda choice * changes to modals in the interaction service (more) * support compiled lambdas in modal properties. * modal interactions tweaks * fix inline doc * more modal docs * configure responce to faild modal component * init * solve runtime errors * solve build errors * add default value parsing * make modal info caching static * make ModalUtils static * add inline docs * fix build errors * code cleanup * Introduce Required and Label properties as seperate attributes. * replace internal dictionary of ModalInfo with a list * change input building logic of modals * update RespondWithModalAsync method * add initial value parameter back to ModalTextInput and fix optional modal field * add missing inline docs * dispose the reference modal instance after building * code cleanup on modalcommandbuilder * Update docs/guides/int_basics/message-components/text-input.md Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update docs/guides/int_basics/message-components/text-input.md Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update docs/guides/int_basics/modals/intro.md Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update docs/guides/int_basics/modals/intro.md Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update docs/guides/int_basics/modals/intro.md Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update docs/guides/int_basics/modals/intro.md Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update docs/guides/int_basics/modals/intro.md Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update docs/guides/int_basics/modals/intro.md Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update docs/guides/int_basics/modals/intro.md Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update docs/guides/int_framework/intro.md Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update docs/guides/int_framework/intro.md Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update docs/guides/int_framework/samples/intro/modal.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteractionData.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Interactions/MessageComponents/TextInputComponent.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Interactions/Modals/IModalInteraction.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.Interactions/Attributes/Commands/ModalInteractionAttribute.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.Interactions/Attributes/Modals/RequiredInputAttribute.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.Interactions/InteractionServiceConfig.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponentData.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModalData.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * update interaction service modal docs * implements ExitOnMissingmModalField config option and adds Type field to modal info * Add WithValue to text input builders * Fix rare NRE on component enumeration * Fix RequestOptions being required in some methods * Use 'OfType' instead of 'Where' * Remove android unsported warning * Change publicity of properties in IInputComponeontBuilder.cs Co-authored-by: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Remove complex parameter ref Co-authored-by: CottageDwellingCat <80918250+CottageDwellingCat@users.noreply.github.com> Co-authored-by: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> --- Discord.Net.code-workspace | 4 +- .../message-components/images/image7.png | Bin 0 -> 20937 bytes .../message-components/images/image8.png | Bin 0 -> 1801 bytes .../message-components/images/image9.png | Bin 0 -> 10913 bytes .../message-components/text-input.md | 46 ++ .../int_basics/modals/images/image1.png | Bin 0 -> 36021 bytes .../int_basics/modals/images/image2.png | Bin 0 -> 25568 bytes .../int_basics/modals/images/image3.png | Bin 0 -> 22409 bytes .../int_basics/modals/images/image4.png | Bin 0 -> 23802 bytes docs/guides/int_basics/modals/intro.md | 135 ++++++ docs/guides/int_framework/intro.md | 12 + .../int_framework/samples/intro/modal.cs | 36 ++ docs/guides/toc.yml | 6 + .../Interactions/IDiscordInteraction.cs | 8 + .../Interactions/InteractionResponseType.cs | 7 +- .../Entities/Interactions/InteractionType.cs | 7 +- .../MessageComponents/ComponentBuilder.cs | 249 +++++++++++ .../MessageComponents/ComponentType.cs | 12 +- .../IComponentInteractionData.cs | 7 +- .../MessageComponents/TextInputComponent.cs | 62 +++ .../MessageComponents/TextInputStyle.cs | 14 + .../Interactions/Modals/IModalInteraction.cs | 13 + .../Modals/IModalInteractionData.cs | 20 + .../Entities/Interactions/Modals/Modal.cs | 37 ++ .../Interactions/Modals/ModalBuilder.cs | 268 ++++++++++++ .../Interactions/Modals/ModalComponent.cs | 20 + .../Commands/ModalInteractionAttribute.cs | 44 ++ .../Attributes/Modals/InputLabelAttribute.cs | 25 ++ .../Attributes/Modals/ModalInputAttribute.cs | 32 ++ .../Modals/ModalTextInputAttribute.cs | 55 +++ .../Modals/RequiredInputAttribute.cs | 25 ++ .../Builders/Commands/ModalCommandBuilder.cs | 44 ++ .../Modals/Inputs/IInputComponentBuilder.cs | 105 +++++ .../Modals/Inputs/InputComponentBuilder.cs | 164 +++++++ .../Inputs/TextInputComponentBuilder.cs | 109 +++++ .../Builders/Modals/ModalBuilder.cs | 81 ++++ .../Builders/ModuleBuilder.cs | 24 +- .../Builders/ModuleClassBuilder.cs | 143 ++++++- .../ModalCommandParameterBuilder.cs | 45 ++ .../Entities/IModal.cs | 13 + .../IDiscordInteractionExtensions.cs | 37 ++ .../Info/Commands/ComponentCommandInfo.cs | 2 +- .../Info/Commands/ModalCommandInfo.cs | 81 ++++ .../InputComponents/InputComponentInfo.cs | 64 +++ .../InputComponents/TextInputComponentInfo.cs | 42 ++ .../Info/ModalInfo.cs | 90 ++++ .../Info/ModuleInfo.cs | 13 + .../Parameters/ModalCommandParameterInfo.cs | 28 ++ .../InteractionModuleBase.cs | 7 + .../InteractionService.cs | 60 ++- .../InteractionServiceConfig.cs | 8 + .../Utilities/ModalUtils.cs | 51 +++ .../Utilities/ReflectionUtils.cs | 72 +++- .../API/Common/ActionRowComponent.cs | 1 + .../API/Common/InteractionCallbackData.cs | 6 + .../Common/MessageComponentInteractionData.cs | 3 + .../API/Common/ModalInteractionData.cs | 13 + .../API/Common/SelectMenuComponent.cs | 2 + .../API/Common/TextInputComponent.cs | 49 +++ .../CommandBase/RestCommandBase.cs | 40 ++ .../MessageComponents/RestMessageComponent.cs | 40 ++ .../RestMessageComponentData.cs | 15 + .../Entities/Interactions/Modals/RestModal.cs | 402 ++++++++++++++++++ .../Interactions/Modals/RestModalData.cs | 45 ++ .../Entities/Interactions/RestInteraction.cs | 9 + .../Interactions/RestPingInteraction.cs | 1 + .../RestAutocompleteInteraction.cs | 3 +- .../Net/Converters/InteractionConverter.cs | 7 + .../Converters/MessageComponentConverter.cs | 3 + .../BaseSocketClient.Events.cs | 9 + .../DiscordShardedClient.cs | 1 + .../DiscordSocketApiClient.cs | 4 +- .../DiscordSocketClient.cs | 3 + .../SocketMessageComponent.cs | 35 ++ .../SocketMessageComponentData.cs | 20 + .../Interaction/Modals/SocketModal.cs | 302 +++++++++++++ .../Interaction/Modals/SocketModalData.cs | 36 ++ .../SocketAutocompleteInteraction.cs | 4 + .../SocketBaseCommand/SocketCommandBase.cs | 37 +- .../Entities/Interaction/SocketInteraction.cs | 10 + 80 files changed, 3502 insertions(+), 25 deletions(-) create mode 100644 docs/guides/int_basics/message-components/images/image7.png create mode 100644 docs/guides/int_basics/message-components/images/image8.png create mode 100644 docs/guides/int_basics/message-components/images/image9.png create mode 100644 docs/guides/int_basics/message-components/text-input.md create mode 100644 docs/guides/int_basics/modals/images/image1.png create mode 100644 docs/guides/int_basics/modals/images/image2.png create mode 100644 docs/guides/int_basics/modals/images/image3.png create mode 100644 docs/guides/int_basics/modals/images/image4.png create mode 100644 docs/guides/int_basics/modals/intro.md create mode 100644 docs/guides/int_framework/samples/intro/modal.cs create mode 100644 src/Discord.Net.Core/Entities/Interactions/MessageComponents/TextInputComponent.cs create mode 100644 src/Discord.Net.Core/Entities/Interactions/MessageComponents/TextInputStyle.cs create mode 100644 src/Discord.Net.Core/Entities/Interactions/Modals/IModalInteraction.cs create mode 100644 src/Discord.Net.Core/Entities/Interactions/Modals/IModalInteractionData.cs create mode 100644 src/Discord.Net.Core/Entities/Interactions/Modals/Modal.cs create mode 100644 src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs create mode 100644 src/Discord.Net.Core/Entities/Interactions/Modals/ModalComponent.cs create mode 100644 src/Discord.Net.Interactions/Attributes/Commands/ModalInteractionAttribute.cs create mode 100644 src/Discord.Net.Interactions/Attributes/Modals/InputLabelAttribute.cs create mode 100644 src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs create mode 100644 src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs create mode 100644 src/Discord.Net.Interactions/Attributes/Modals/RequiredInputAttribute.cs create mode 100644 src/Discord.Net.Interactions/Builders/Commands/ModalCommandBuilder.cs create mode 100644 src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs create mode 100644 src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs create mode 100644 src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs create mode 100644 src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs create mode 100644 src/Discord.Net.Interactions/Builders/Parameters/ModalCommandParameterBuilder.cs create mode 100644 src/Discord.Net.Interactions/Entities/IModal.cs create mode 100644 src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs create mode 100644 src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs create mode 100644 src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs create mode 100644 src/Discord.Net.Interactions/Info/InputComponents/TextInputComponentInfo.cs create mode 100644 src/Discord.Net.Interactions/Info/ModalInfo.cs create mode 100644 src/Discord.Net.Interactions/Info/Parameters/ModalCommandParameterInfo.cs create mode 100644 src/Discord.Net.Interactions/Utilities/ModalUtils.cs create mode 100644 src/Discord.Net.Rest/API/Common/ModalInteractionData.cs create mode 100644 src/Discord.Net.Rest/API/Common/TextInputComponent.cs create mode 100644 src/Discord.Net.Rest/Entities/Interactions/Modals/RestModal.cs create mode 100644 src/Discord.Net.Rest/Entities/Interactions/Modals/RestModalData.cs create mode 100644 src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModal.cs create mode 100644 src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModalData.cs diff --git a/Discord.Net.code-workspace b/Discord.Net.code-workspace index 709eb0e95..b40453473 100644 --- a/Discord.Net.code-workspace +++ b/Discord.Net.code-workspace @@ -8,16 +8,16 @@ "editor.rulers": [ 120 ], + "editor.insertSpaces": true, "files.exclude": { "**/.git": true, "**/.svn": true, "**/.hg": true, "**/CVS": true, "**/.DS_Store": true, - "docs/": true, "**/obj": true, "**/bin": true, "samples/": true, } } -} \ No newline at end of file +} diff --git a/docs/guides/int_basics/message-components/images/image7.png b/docs/guides/int_basics/message-components/images/image7.png new file mode 100644 index 0000000000000000000000000000000000000000..5ff55a550e24cfeda5145e266ace31b95309c1ea GIT binary patch literal 20937 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 literal 0 HcmV?d00001 diff --git a/docs/guides/int_basics/message-components/images/image8.png b/docs/guides/int_basics/message-components/images/image8.png new file mode 100644 index 0000000000000000000000000000000000000000..0268313d58050df42694fec2fad0a7fbd11c7e60 GIT binary patch literal 1801 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 IdCS-T0p3n83IG5A literal 0 HcmV?d00001 diff --git a/docs/guides/int_basics/message-components/text-input.md b/docs/guides/int_basics/message-components/text-input.md new file mode 100644 index 000000000..37f5b4937 --- /dev/null +++ b/docs/guides/int_basics/message-components/text-input.md @@ -0,0 +1,46 @@ +--- +uid: Guides.MessageComponents.TextInputs +title: Text Input Components +--- + +# Text Input Components + +> [!WARNING] +> Text input components can only be used in +> [modals](../modals/intro.md). + +Text input components are a type of MessageComponents that can only be +used in modals. Texts inputs can be longer (the `Paragraph`) style or +shorter (the `Short` style). Text inputs have a variable min and max +length. + +![A modal with short and paragraph text inputs](images/image7.png) + +## Creating text inputs +Text input components can be built using the `TextInputBuilder`. +The simplest text input can built with: +```cs +var tb = new TextInputBuilder() + .WithLabel("My Text") + .WithCustomId("text_input"); +``` + +and would produce a component that looks like: + +![basic text input component](images/image8.png) + +Additional options can be specified to control the placeholder, style, +and min/max length of the input: +```cs +var tb = new TextInputBuilder() + .WithLabel("Labeled") + .WithCustomId("text_input") + .WithStyle(TextInputStyle.Paragraph) + .WithMinLength(6); + .WithMaxLength(42) + .WithRequired(true) + .WithPlaceholder("Consider this place held."); +``` + +![more advanced text input](images/image9.png) + diff --git a/docs/guides/int_basics/modals/images/image1.png b/docs/guides/int_basics/modals/images/image1.png new file mode 100644 index 0000000000000000000000000000000000000000..779bf78b5cc0b1a2ff8e6bd828fa123dc6b3c507 GIT binary patch literal 36021 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 JnbZg0{{jcrI#~b! literal 0 HcmV?d00001 diff --git a/docs/guides/int_basics/modals/images/image3.png b/docs/guides/int_basics/modals/images/image3.png new file mode 100644 index 0000000000000000000000000000000000000000..49ca61cbd90875048bc20551b235924355a2a95c GIT binary patch literal 22409 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 L>uWeekH`NB`Re}B literal 0 HcmV?d00001 diff --git a/docs/guides/int_basics/modals/images/image4.png b/docs/guides/int_basics/modals/images/image4.png new file mode 100644 index 0000000000000000000000000000000000000000..453b2eee5c0977f3c179a250fc0f6456757a2b59 GIT binary patch literal 23802 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 literal 0 HcmV?d00001 diff --git a/docs/guides/int_basics/modals/intro.md b/docs/guides/int_basics/modals/intro.md new file mode 100644 index 000000000..3212019ae --- /dev/null +++ b/docs/guides/int_basics/modals/intro.md @@ -0,0 +1,135 @@ +--- +uid: Guides.Modals.Intro +title: Getting Started with Modals +--- +# Modals + +## Getting started with modals +This guide will show you how to use modals and give a few examples of +valid use cases. If your question is not covered by this guide ask in the +[Discord.Net Discord Server](https://discord.gg/dnet). + +### What is a modal? +Modals are forms bots can send when responding to interactions. Modals +are sent to Discord as an array of message components and converted +into the form layout by user's clients. Modals are required to have a +custom id, title, and at least one component. + +![Screenshot of a modal](images/image2.png) + +When users submit modals, your client fires the ModalSubmitted event. +You can get the components of the modal from the `Data.Components` property +on the SocketModal: + +![Screenshot of modal data](images/image1.png) + +### Using modals + +Lets create a simple modal with an entry field for users to +tell us their favorite food. We can start by creating a slash +command that will respond with the modal. +```cs +[SlashCommand("food", "Tell us about your favorite food!")] +public async Task FoodPreference() +{ + // send a modal +} +``` + +Now that we have our command set up, we need to build a modal. +We can use the aptly named `ModalBuilder` for that: + +| Method | Description | +| --------------- | ----------------------------------------- | +| `WithTitle` | Sets the modal's title. | +| `WithCustomId` | Sets the modal's custom id. | +| `AddTextInput` | Adds a `TextInputBuilder` to the modal. | +| `AddComponents` | Adds multiple components to the modal. | +| `Build` | Builds the `ModalBuilder` into a `Modal`. | + +We know we need to add a text input to the modal, so let's look at that +method's parameters. + +| Parameter | Description | +| ------------- | ------------------------------------------ | +| `label` | Sets the input's label. | +| `customId` | Sets the input's custom id. | +| `style` | Sets the input's style. | +| `placeholder` | Sets the input's placeholder. | +| `minLength` | Sets the minimum input length. | +| `maxLength` | Sets the maximum input length. | +| `required` | Sets whether or not the modal is required. | +| `value` | Sets the input's default value. | + +To make a basic text input we would only need to set the `label` and +`customId`, but in this example we will also use the `placeholder` +parameter. Next we can build our modal: + +```cs +var mb = new ModalBuilder() + .WithTitle("Fav Food") + .WithCustomId("food_menu") + .AddTextInput("What??", "food_name", placeholder:"Pizza") + .AddTextInput("Why??", "food_reason", TextInputStyle.Paragraph, + "Kus it's so tasty"); +``` + +Now that we have a ModalBuilder we can update our command to respond +with the modal. + +```cs +[SlashCommand("food", "Tell us about your favorite food!")] +public async Task FoodPreference() +{ + var mb = new ModalBuilder() + .WithTitle("Fav Food") + .WithCustomId("food_menu") + .AddTextInput("What??", "food_name", placeholder:"Pizza") + .AddTextInput("Why??", "food_reason", TextInputStyle.Paragraph, + "Kus it's so tasty"); + + await Context.Interaction.RespondWithModalAsync(mb.Build()); +} +``` + +When we run the command, our modal should pop up: + +![screenshot of the above modal](images/image3.png) + +### Respond to modals + +> [!WARNING] +> Modals can not be sent when respoding to a modal. + +Once a user has submitted the modal, we need to let everyone know what +their favorite food is. We can start by hooking a task to the client's +`ModalSubmitted` event. +```cs +_client.ModalSubmitted += async modal => +{ + // Get the values of components. + List components = + modal.Data.Components.ToList(); + string food = components + .Where(x => x.CustomId == "food_name").First().Value; + string reason = components + .Where(x => x.CustomId == "food_reason").First().Value; + + // Build the message to send. + string message = "hey @everyone; I just learned " + + $"{modal.User.Mention}'s favorite food is " + + $"{food} because {reason}."; + + // Specify the AllowedMentions so we don't actually ping everyone. + AllowedMentions mentions = new AllowedMentions(); + mentions.AllowedTypes = AllowedMentionTypes.Users; + + // Respond to the modal. + await modal.RespondAsync(message, allowedMentions:mentions); +} +``` + +Now responding to the modal should inform everyone of our tasty +choices. + +![Response of the modal submitted event](images/image4.png) diff --git a/docs/guides/int_framework/intro.md b/docs/guides/int_framework/intro.md index 7dfd7ac6e..0a5cc19f1 100644 --- a/docs/guides/int_framework/intro.md +++ b/docs/guides/int_framework/intro.md @@ -198,6 +198,18 @@ Autocomplete commands must be parameterless methods. A valid Autocomplete comman Alternatively, you can use the [AutocompleteHandlers] to simplify this workflow. +## Modals + +Modal commands last parameter must be an implementation of `IModal`. +A Modal implementation would look like this: + +[!code-csharp[Modal Command](samples/intro/modal.cs)] + +> [!NOTE] +> If you are using Modals in the interaction service it is **highly +> recommended** that you enable `PreCompiledLambdas` in your config +> to prevent performance issues. + ## Interaction Context Every command module provides its commands with an execution context. diff --git a/docs/guides/int_framework/samples/intro/modal.cs b/docs/guides/int_framework/samples/intro/modal.cs new file mode 100644 index 000000000..af72fe04e --- /dev/null +++ b/docs/guides/int_framework/samples/intro/modal.cs @@ -0,0 +1,36 @@ +// Registers a command that will respond with a modal. +[SlashCommand("food", "Tell us about your favorite food.")] +public async Task Command() + => await Context.Interaction.RespondWithModalAsync("food_menu"); + +// Defines the modal that will be sent. +public class FoodModal : IModal +{ + public string Title => "Fav Food"; + // Strings with the ModalTextInput attribute will automatically become components. + [InputLabel("What??")] + [ModalTextInput("food_name", placeholder: "Pizza", maxLength: 20)] + public string Food { get; set; } + + // Additional paremeters can be specified to further customize the input. + [InputLabel("Why??")] + [ModalTextInput("food_reason", TextInputStyle.Paragraph, "Kuz it's tasty", maxLength: 500)] + public string Reason { get; set; } +} + +// Responds to the modal. +[ModalInteraction("food_menu")] +public async Task ModalResponce(FoodModal modal) +{ + // Build the message to send. + string message = "hey @everyone, I just learned " + + $"{Context.User.Mention}'s favorite food is " + + $"{modal.Food} because {modal.Reason}."; + + // Specify the AllowedMentions so we don't actually ping everyone. + AllowedMentions mentions = new(); + mentions.AllowedTypes = AllowedMentionTypes.Users; + + // Respond to the modal. + await RespondAsync(message, allowedMentions: mentions, ephemeral: true); +} \ No newline at end of file diff --git a/docs/guides/toc.yml b/docs/guides/toc.yml index d4f2984f8..1616363b7 100644 --- a/docs/guides/toc.yml +++ b/docs/guides/toc.yml @@ -91,8 +91,14 @@ topicUid: Guides.MessageComponents.Buttons - name: Select menus topicUid: Guides.MessageComponents.SelectMenus + - name: Text Input + topicUid: Guides.MessageComponents.TextInputs - name: Advanced Concepts topicUid: Guides.MessageComponents.Advanced +- name: Modal Basics + items: + - name: Introduction + topicUid: Guides.Modals.Intro - name: Guild Events items: - name: Introduction diff --git a/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs index 8ac08f842..66ff6c6d0 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs @@ -332,5 +332,13 @@ namespace Discord /// A task that represents the asynchronous operation of deferring the interaction. /// Task DeferAsync(bool ephemeral = false, RequestOptions options = null); + + ///

+ /// Responds to the interaction with a modal. + /// + /// The modal to respond with. + /// The request options for this request. + /// A task that represents the asynchronous operation of responding to the interaction. + Task RespondWithModalAsync(Modal modal, RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Interactions/InteractionResponseType.cs b/src/Discord.Net.Core/Entities/Interactions/InteractionResponseType.cs index ebdf29781..b0c2384e7 100644 --- a/src/Discord.Net.Core/Entities/Interactions/InteractionResponseType.cs +++ b/src/Discord.Net.Core/Entities/Interactions/InteractionResponseType.cs @@ -41,6 +41,11 @@ namespace Discord /// /// Respond with a set of choices to a autocomplete interaction. /// - ApplicationCommandAutocompleteResult = 8 + ApplicationCommandAutocompleteResult = 8, + + /// + /// Respond by showing the user a modal. + /// + Modal = 9, } } diff --git a/src/Discord.Net.Core/Entities/Interactions/InteractionType.cs b/src/Discord.Net.Core/Entities/Interactions/InteractionType.cs index e09c906b5..811c8c7c7 100644 --- a/src/Discord.Net.Core/Entities/Interactions/InteractionType.cs +++ b/src/Discord.Net.Core/Entities/Interactions/InteractionType.cs @@ -23,6 +23,11 @@ namespace Discord /// /// An autocomplete request sent from discord. /// - ApplicationCommandAutocomplete = 4 + ApplicationCommandAutocomplete = 4, + + /// + /// A modal sent from discord. + /// + ModalSubmit = 5, } } diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs index b086535f7..0fa8189c1 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs @@ -276,6 +276,11 @@ namespace Discord /// A that can be sent with . public MessageComponent Build() { + if (_actionRows?.SelectMany(x => x.Components)?.Any(x => x.Type == ComponentType.TextInput) ?? false) + 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 ? new MessageComponent(_actionRows.Select(x => x.Build()).ToList()) : MessageComponent.Empty; @@ -1093,4 +1098,248 @@ namespace Discord return new SelectMenuOption(Label, Value, Description, Emote, IsDefault); } } + + public class TextInputBuilder + { + public const int LargestMaxLength = 4000; + + /// + /// Gets or sets the custom id of the current text input. + /// + /// length exceeds + /// length subceeds 1. + public string CustomId + { + get => _customId; + set => _customId = value?.Length switch + { + > ComponentBuilder.MaxCustomIdLength => throw new ArgumentOutOfRangeException(nameof(value), $"Custom Id length must be less or equal to {ComponentBuilder.MaxCustomIdLength}."), + 0 => throw new ArgumentOutOfRangeException(nameof(value), "Custom Id length must be at least 1."), + _ => value + }; + } + + /// + /// Gets or sets the style of the current text input. + /// + public TextInputStyle Style { get; set; } = TextInputStyle.Short; + + /// + /// Gets or sets the label of the current text input. + /// + public string Label { get; set; } + + /// + /// Gets or sets the placeholder of the current text input. + /// + /// is longer than 100 characters + public string Placeholder + { + get => _placeholder; + set => _placeholder = (value?.Length ?? 0) <= 100 + ? value + : throw new ArgumentException("Placeholder cannot have more than 100 characters."); + } + + /// + /// Gets or sets the minimum length of the current text input. + /// + /// is less than 0. + /// is greater than . + /// is greater than . + public int? MinLength + { + get => _minLength; + set + { + if (value < 0) + throw new ArgumentOutOfRangeException(nameof(value), $"MinLength must not be less than 0"); + if (value > LargestMaxLength) + throw new ArgumentOutOfRangeException(nameof(value), $"MinLength must not be greater than {LargestMaxLength}"); + if (value > (MaxLength ?? LargestMaxLength)) + throw new ArgumentOutOfRangeException(nameof(value), $"MinLength must be less than MaxLength"); + _minLength = value; + } + } + + /// + /// Gets or sets the maximum length of the current text input. + /// + /// is less than 0. + /// is greater than . + /// is less than . + public int? MaxLength + { + get => _maxLength; + set + { + if (value < 0) + throw new ArgumentOutOfRangeException(nameof(value), $"MaxLength must not be less than 0"); + if (value > LargestMaxLength) + throw new ArgumentOutOfRangeException(nameof(value), $"MaxLength most not be greater than {LargestMaxLength}"); + if (value < (MinLength ?? -1)) + throw new ArgumentOutOfRangeException(nameof(value), $"MaxLength must be greater than MinLength ({MinLength})"); + _maxLength = value; + } + } + + /// + /// Gets or sets whether the user is required to input text. + /// + public bool? Required { get; set; } + + /// + /// Gets or sets the default value of the text input. + /// + /// is less than 0. + /// + /// is greater than or . + /// + public string Value + { + get => _value; + set + { + if (value?.Length > (MaxLength ?? LargestMaxLength)) + throw new ArgumentOutOfRangeException(nameof(value), $"Value must not be longer than {MaxLength ?? LargestMaxLength}."); + if (value?.Length < (MinLength ?? 0)) + throw new ArgumentOutOfRangeException(nameof(value), $"Value must not be shorter than {MinLength}"); + _value = value; + } + } + + private string _customId; + private int? _maxLength; + private int? _minLength; + private string _placeholder; + private string _value; + + /// + /// Creates a new instance of a . + /// + /// The text input's label. + /// The text input's style. + /// The text input's custom id. + /// The text input's placeholder. + /// The text input's minimum length. + /// The text input's maximum length. + /// The text input's required value. + public TextInputBuilder (string label, string customId, TextInputStyle style = TextInputStyle.Short, string placeholder = null, + int? minLength = null, int? maxLength = null, bool? required = null, string value = null) + { + Label = label; + Style = style; + CustomId = customId; + Placeholder = placeholder; + MinLength = minLength; + MaxLength = maxLength; + Required = required; + Value = value; + } + + /// + /// Creates a new instance of a . + /// + public TextInputBuilder() + { + + } + + /// + /// Sets the label of the current builder. + /// + /// The value to set. + /// The current builder. + public TextInputBuilder WithLabel(string label) + { + Label = label; + return this; + } + + /// + /// Sets the style of the current builder. + /// + /// The value to set. + /// The current builder. + public TextInputBuilder WithStyle(TextInputStyle style) + { + Style = style; + return this; + } + + /// + /// Sets the custom id of the current builder. + /// + /// The value to set. + /// The current builder. + public TextInputBuilder WithCustomId(string customId) + { + CustomId = customId; + return this; + } + + /// + /// Sets the placeholder of the current builder. + /// + /// The value to set. + /// The current builder. + public TextInputBuilder WithPlaceholder(string placeholder) + { + Placeholder = placeholder; + return this; + } + + /// + /// Sets the value of the current builder. + /// + /// The value to set + /// The current builder. + public TextInputBuilder WithValue(string value) + { + Value = value; + return this; + } + + /// + /// Sets the minimum length of the current builder. + /// + /// The value to set. + /// The current builder. + public TextInputBuilder WithMinLength(int minLength) + { + MinLength = minLength; + return this; + } + + /// + /// Sets the maximum length of the current builder. + /// + /// The value to set. + /// The current builder. + public TextInputBuilder WithMaxLength(int maxLength) + { + MaxLength = maxLength; + return this; + } + + /// + /// Sets the required value of the current builder. + /// + /// The value to set. + /// The current builder. + public TextInputBuilder WithRequired(bool required) + { + Required = required; + return this; + } + + public TextInputComponent Build() + { + if (string.IsNullOrEmpty(CustomId)) + throw new ArgumentException("TextInputComponents must have a custom id.", nameof(CustomId)); + if (string.IsNullOrWhiteSpace(Label)) + throw new ArgumentException("TextInputComponents must have a label.", nameof(Label)); + return new TextInputComponent(CustomId, Label, Placeholder, MinLength, MaxLength, Style, Required, Value); + } + } } diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentType.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentType.cs index 70bc1f301..1d63ee829 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentType.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentType.cs @@ -18,6 +18,16 @@ namespace Discord /// /// A select menu for picking from choices. /// - SelectMenu = 3 + SelectMenu = 3, + + /// + /// A box for entering text. + /// + TextInput = 4, + + /// + /// An interaction sent when a model is submitted. + /// + ModalSubmit = 5, } } diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteractionData.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteractionData.cs index 99b9b6f6c..039b6b41f 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteractionData.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteractionData.cs @@ -8,7 +8,7 @@ namespace Discord public interface IComponentInteractionData : IDiscordInteractionData { /// - /// Gets the components Custom Id that was clicked. + /// Gets the component's Custom Id that was clicked. /// string CustomId { get; } @@ -21,5 +21,10 @@ namespace Discord /// Gets the value(s) of a interaction response. /// IReadOnlyCollection Values { get; } + + /// + /// Gets the value of a interaction response. + /// + public string Value { get; } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/TextInputComponent.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/TextInputComponent.cs new file mode 100644 index 000000000..d159df071 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/TextInputComponent.cs @@ -0,0 +1,62 @@ +namespace Discord +{ + /// + /// Respresents a text input. + /// + public class TextInputComponent : IMessageComponent + { + /// + public ComponentType Type => ComponentType.TextInput; + + /// + public string CustomId { get; } + + /// + /// Gets the label of the component; this is the text shown above it. + /// + public string Label { get; } + + /// + /// Gets the placeholder of the component. + /// + public string Placeholder { get; } + + /// + /// Gets the minimum length of the inputted text. + /// + public int? MinLength { get; } + + /// + /// Gets the maximum length of the inputted text. + /// + public int? MaxLength { get; } + + /// + /// Gets the style of the component. + /// + public TextInputStyle Style { get; } + + /// + /// Gets whether users are required to input text. + /// + public bool? Required { get; } + + /// + /// Gets the default value of the component. + /// + public string Value { get; } + + internal TextInputComponent(string customId, string label, string placeholder, int? minLength, int? maxLength, + TextInputStyle style, bool? required, string value) + { + CustomId = customId; + Label = label; + Placeholder = placeholder; + MinLength = minLength; + MaxLength = maxLength; + Style = style; + Required = required; + Value = value; + } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/TextInputStyle.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/TextInputStyle.cs new file mode 100644 index 000000000..72ea59b22 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/TextInputStyle.cs @@ -0,0 +1,14 @@ +namespace Discord +{ + public enum TextInputStyle + { + /// + /// Intended for short, single-line text. + /// + Short = 1, + /// + /// Intended for longer or multiline text. + /// + Paragraph = 2, + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/Modals/IModalInteraction.cs b/src/Discord.Net.Core/Entities/Interactions/Modals/IModalInteraction.cs new file mode 100644 index 000000000..5ce153845 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/Modals/IModalInteraction.cs @@ -0,0 +1,13 @@ +namespace Discord +{ + /// + /// Represents an interaction type for Modals. + /// + public interface IModalInteraction : IDiscordInteraction + { + /// + /// Gets the data received with this interaction; contains the clicked button. + /// + new IModalInteractionData Data { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/Modals/IModalInteractionData.cs b/src/Discord.Net.Core/Entities/Interactions/Modals/IModalInteractionData.cs new file mode 100644 index 000000000..767dd5df7 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/Modals/IModalInteractionData.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace Discord +{ + /// + /// Represents the data sent with the . + /// + public interface IModalInteractionData : IDiscordInteractionData + { + /// + /// Gets the 's Custom Id. + /// + string CustomId { get; } + + /// + /// Gets the components submitted by the user. + /// + IReadOnlyCollection Components { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/Modals/Modal.cs b/src/Discord.Net.Core/Entities/Interactions/Modals/Modal.cs new file mode 100644 index 000000000..a0fde5ea3 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/Modals/Modal.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a modal interaction. + /// + public class Modal : IMessageComponent + { + /// + public ComponentType Type => ComponentType.ModalSubmit; + + /// + /// Gets the title of the modal. + /// + public string Title { get; set; } + + /// + public string CustomId { get; set; } + + /// + /// Gets the components in the modal. + /// + public ModalComponent Component { get; set; } + + internal Modal(string title, string customId, ModalComponent components) + { + Title = title; + CustomId = customId; + Component = components; + } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs new file mode 100644 index 000000000..3a3e3cc49 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs @@ -0,0 +1,268 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public class ModalBuilder + { + /// + /// Gets or sets the components of the current modal. + /// + public ModalComponentBuilder Components { get; set; } = new(); + + /// + /// Gets or sets the title of the current modal. + /// + public string Title { get; set; } + + /// + /// Gets or sets the custom id of the current modal. + /// + public string CustomId + { + get => _customId; + set => _customId = value?.Length switch + { + > ComponentBuilder.MaxCustomIdLength => throw new ArgumentOutOfRangeException(nameof(value), $"Custom Id length must be less or equal to {ComponentBuilder.MaxCustomIdLength}."), + 0 => throw new ArgumentOutOfRangeException(nameof(value), "Custom Id length must be at least 1."), + _ => value + }; + } + + private string _customId; + + public ModalBuilder() { } + + /// + /// Creates a new instance of a + /// + /// The modal's title. + /// The modal's customId. + /// The modal's components. + /// Only TextInputComponents are allowed. + public ModalBuilder(string title, string customId, ModalComponentBuilder components = null) + { + Title = title; + CustomId = customId; + Components = components ?? new(); + } + + /// + /// Sets the title of the current modal. + /// + /// The value to set the title to. + /// The current builder. + public ModalBuilder WithTitle(string title) + { + Title = title; + return this; + } + + /// + /// Sets the custom id of the current modal. + /// + /// The value to set the custom id to. + /// The current builder. + public ModalBuilder WithCustomId(string customId) + { + CustomId = customId; + return this; + } + + /// + /// Adds a component to the current builder. + /// + /// The component to add. + /// The current builder. + public ModalBuilder AddTextInput(TextInputBuilder component) + { + Components.WithTextInput(component); + return this; + } + + /// + /// Adds a to the current builder. + /// + /// The input's custom id. + /// The input's label. + /// The input's placeholder text. + /// The input's minimum length. + /// The input's maximum length. + /// The input's style. + /// The current builder. + public ModalBuilder AddTextInput(string label, string customId, TextInputStyle style = TextInputStyle.Short, + string placeholder = "", int? minLength = null, int? maxLength = null, bool? required = null, string value = null) + => AddTextInput(new(label, customId, style, placeholder, minLength, maxLength, required, value)); + + /// + /// Adds multiple components to the current builder. + /// + /// The components to add. + /// The current builder + public ModalBuilder AddComponents(List components, int row) + { + components.ForEach(x => Components.AddComponent(x, row)); + return this; + } + + /// + /// Builds this builder into a . + /// + /// A with the same values as this builder. + /// Only TextInputComponents are allowed. + /// Modals must have a custom id. + /// Modals must have a title. + public Modal Build() + { + if (string.IsNullOrEmpty(CustomId)) + throw new ArgumentException("Modals must have a custom id.", nameof(CustomId)); + if (string.IsNullOrWhiteSpace(Title)) + throw new ArgumentException("Modals must have a title.", nameof(Title)); + if (Components.ActionRows?.SelectMany(x => x.Components).Any(x => x.Type != ComponentType.TextInput) ?? false) + throw new ArgumentException($"Only TextInputComponents are allowed.", nameof(Components)); + + return new(Title, CustomId, Components.Build()); + } + } + + /// + /// Represents a builder for creating a . + /// + public class ModalComponentBuilder + { + /// + /// The max length of a . + /// + public const int MaxCustomIdLength = 100; + + /// + /// The max amount of rows a can have. + /// + public const int MaxActionRowCount = 5; + + /// + /// Gets or sets the Action Rows for this Component Builder. + /// + /// cannot be null. + /// count exceeds . + public List ActionRows + { + get => _actionRows; + set + { + if (value == null) + throw new ArgumentNullException(nameof(value), $"{nameof(ActionRows)} cannot be null."); + if (value.Count > MaxActionRowCount) + throw new ArgumentOutOfRangeException(nameof(value), $"Action row count must be less than or equal to {MaxActionRowCount}."); + _actionRows = value; + } + } + + private List _actionRows; + + /// + /// Creates a new builder from the provided list of components. + /// + /// The components to create the builder from. + /// The newly created builder. + public static ComponentBuilder FromComponents(IReadOnlyCollection components) + { + var builder = new ComponentBuilder(); + for (int i = 0; i != components.Count; i++) + { + var component = components.ElementAt(i); + builder.AddComponent(component, i); + } + return builder; + } + + internal void AddComponent(IMessageComponent component, int row) + { + switch (component) + { + case TextInputComponent text: + WithTextInput(text.Label, text.CustomId, text.Style, text.Placeholder, text.MinLength, text.MaxLength, row); + break; + case ActionRowComponent actionRow: + foreach (var cmp in actionRow.Components) + AddComponent(cmp, row); + break; + } + } + + /// + /// Adds a to the at the specific row. + /// If the row cannot accept the component then it will add it to a row that can. + /// + /// The input's custom id. + /// The input's label. + /// The input's placeholder text. + /// The input's minimum length. + /// The input's maximum length. + /// The input's style. + /// The current builder. + public ModalComponentBuilder WithTextInput(string label, string customId, TextInputStyle style = TextInputStyle.Short, + string placeholder = null, int? minLength = null, int? maxLength = null, int row = 0, bool? required = null, + string value = null) + => WithTextInput(new(label, customId, style, placeholder, minLength, maxLength, required, value), row); + + /// + /// Adds a to the at the specific row. + /// If the row cannot accept the component then it will add it to a row that can. + /// + /// The to add. + /// The row to add the text input. + /// There are no more rows to add a text input to. + /// must be less than . + /// The current builder. + public ModalComponentBuilder WithTextInput(TextInputBuilder text, int row = 0) + { + Preconditions.LessThan(row, MaxActionRowCount, nameof(row)); + + var builtButton = text.Build(); + + if (_actionRows == null) + { + _actionRows = new List + { + new ActionRowBuilder().AddComponent(builtButton) + }; + } + else + { + if (_actionRows.Count == row) + _actionRows.Add(new ActionRowBuilder().AddComponent(builtButton)); + else + { + ActionRowBuilder actionRow; + if (_actionRows.Count > row) + actionRow = _actionRows.ElementAt(row); + else + { + actionRow = new ActionRowBuilder(); + _actionRows.Add(actionRow); + } + + if (actionRow.CanTakeComponent(builtButton)) + actionRow.AddComponent(builtButton); + else if (row < MaxActionRowCount) + WithTextInput(text, row + 1); + else + throw new InvalidOperationException($"There are no more rows to add {nameof(text)} to."); + } + } + + return this; + } + + /// + /// Get a representing the builder. + /// + /// A representing the builder. + public ModalComponent Build() + => new (ActionRows?.Select(x => x.Build()).ToList()); + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/Modals/ModalComponent.cs b/src/Discord.Net.Core/Entities/Interactions/Modals/ModalComponent.cs new file mode 100644 index 000000000..ecc90720f --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/Modals/ModalComponent.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace Discord +{ + /// + /// Represents a component object used in s. + /// + public class ModalComponent + { + /// + /// Gets the components to be used in a modal. + /// + public IReadOnlyCollection Components { get; } + + internal ModalComponent(List components) + { + Components = components; + } + } +} diff --git a/src/Discord.Net.Interactions/Attributes/Commands/ModalInteractionAttribute.cs b/src/Discord.Net.Interactions/Attributes/Commands/ModalInteractionAttribute.cs new file mode 100644 index 000000000..a0ce91cda --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/Commands/ModalInteractionAttribute.cs @@ -0,0 +1,44 @@ +using System; + +namespace Discord.Interactions +{ + /// + /// Create a Modal interaction handler. CustomId represents + /// the CustomId of the Modal that will be handled. + /// + /// + /// s will add prefixes to this command if is set to + /// CustomID supports a Wild Card pattern where you can use the to match a set of CustomIDs. + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public sealed class ModalInteractionAttribute : Attribute + { + /// + /// Gets the string to compare the Modal CustomIDs with. + /// + public string CustomId { get; } + + /// + /// Gets if s will be ignored while creating this command and this method will be treated as a top level command. + /// + public bool IgnoreGroupNames { get; } + + /// + /// Gets the run mode this command gets executed with. + /// + public RunMode RunMode { get; } + + /// + /// Create a command for modal interaction handling. + /// + /// String to compare the modal CustomIDs with. + /// If s will be ignored while creating this command and this method will be treated as a top level command. + /// Set the run mode of the command. + public ModalInteractionAttribute(string customId, bool ignoreGroupNames = false, RunMode runMode = RunMode.Default) + { + CustomId = customId; + IgnoreGroupNames = ignoreGroupNames; + RunMode = runMode; + } + } +} diff --git a/src/Discord.Net.Interactions/Attributes/Modals/InputLabelAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/InputLabelAttribute.cs new file mode 100644 index 000000000..fdeb8c414 --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/Modals/InputLabelAttribute.cs @@ -0,0 +1,25 @@ +using System; + +namespace Discord.Interactions +{ + /// + /// Creates a custom label for an modal input. + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public class InputLabelAttribute : Attribute + { + /// + /// Gets the label of the input. + /// + public string Label { get; } + + /// + /// Creates a custom label for an modal input. + /// + /// The label of the input. + public InputLabelAttribute(string label) + { + Label = label; + } + } +} diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs new file mode 100644 index 000000000..d611b574d --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs @@ -0,0 +1,32 @@ +using System; + +namespace Discord.Interactions +{ + /// + /// Mark an property as a modal input field. + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + public abstract class ModalInputAttribute : Attribute + { + /// + /// Gets the custom id of the text input. + /// + public string CustomId { get; } + + /// + /// Gets the type of the component. + /// + public abstract ComponentType ComponentType { get; } + + /// + /// Create a new . + /// + /// The label of the input. + /// The custom id of the input. + /// Whether the user is required to input a value.> + protected ModalInputAttribute(string customId) + { + CustomId = customId; + } + } +} diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs new file mode 100644 index 000000000..35121cd6b --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs @@ -0,0 +1,55 @@ +namespace Discord.Interactions +{ + /// + /// Marks a property as a text input. + /// + public sealed class ModalTextInputAttribute : ModalInputAttribute + { + /// + public override ComponentType ComponentType => ComponentType.TextInput; + + /// + /// Gets the style of the text input. + /// + public TextInputStyle Style { get; } + + /// + /// Gets the placeholder of the text input. + /// + public string Placeholder { get; } + + /// + /// Gets the minimum length of the text input. + /// + public int MinLength { get; } + + /// + /// Gets the maximum length of the text input. + /// + public int MaxLength { get; } + + /// + /// Gets the initial value to be displayed by this input. + /// + public string InitialValue { get; } + + /// + /// Create a new . + /// + /// + /// The style of the text input. + /// The placeholder of the text input. + /// The minimum length of the text input's content. + /// The maximum length of the text input's content. + /// The initial value to be displayed by this input. + public ModalTextInputAttribute(string customId, TextInputStyle style = TextInputStyle.Short, string placeholder = null, int minLength = 1, int maxLength = 4000, string initValue = null) + : base(customId) + { + Style = style; + Placeholder = placeholder; + MinLength = minLength; + MaxLength = maxLength; + InitialValue = initValue; + } + } +} diff --git a/src/Discord.Net.Interactions/Attributes/Modals/RequiredInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/RequiredInputAttribute.cs new file mode 100644 index 000000000..e3cab3340 --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/Modals/RequiredInputAttribute.cs @@ -0,0 +1,25 @@ +using System; + +namespace Discord.Interactions +{ + /// + /// Sets the input as required or optional. + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public class RequiredInputAttribute : Attribute + { + /// + /// Gets whether or not user input is required for this input. + /// + public bool IsRequired { get; } + + /// + /// Sets the input as required or optinal. + /// + /// Whether or not user input is required for this input. + public RequiredInputAttribute(bool isRequired = true) + { + IsRequired = isRequired; + } + } +} diff --git a/src/Discord.Net.Interactions/Builders/Commands/ModalCommandBuilder.cs b/src/Discord.Net.Interactions/Builders/Commands/ModalCommandBuilder.cs new file mode 100644 index 000000000..dfc76c686 --- /dev/null +++ b/src/Discord.Net.Interactions/Builders/Commands/ModalCommandBuilder.cs @@ -0,0 +1,44 @@ +using System; + +namespace Discord.Interactions.Builders +{ + /// + /// Represents a builder for creating a . + /// + public class ModalCommandBuilder : CommandBuilder + { + protected override ModalCommandBuilder Instance => this; + + /// + /// Initializes a new . + /// + /// Parent module of this modal. + public ModalCommandBuilder(ModuleBuilder module) : base(module) { } + + /// + /// Initializes a new . + /// + /// Parent module of this modal. + /// Name of this modal. + /// Execution callback of this modal. + public ModalCommandBuilder(ModuleBuilder module, string name, ExecuteCallback callback) : base(module, name, callback) { } + + /// + /// Adds a modal parameter to the parameters collection. + /// + /// factory. + /// + /// The builder instance. + /// + public override ModalCommandBuilder AddParameter(Action configure) + { + var parameter = new ModalCommandParameterBuilder(this); + configure(parameter); + AddParameters(parameter); + return this; + } + + internal override ModalCommandInfo Build(ModuleInfo module, InteractionService commandService) => + new(this, module, commandService); + } +} diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs new file mode 100644 index 000000000..37cd861c4 --- /dev/null +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; + +namespace Discord.Interactions.Builders +{ + /// + /// Represent a builder for creating . + /// + public interface IInputComponentBuilder + { + /// + /// Gets the parent modal of this input component. + /// + ModalBuilder Modal { get; } + + /// + /// Gets the custom id of this input component. + /// + string CustomId { get; } + + /// + /// Gets the label of this input component. + /// + string Label { get; } + + /// + /// Gets whether this input component is required. + /// + bool IsRequired { get; } + + /// + /// Gets the component type of this input component. + /// + ComponentType ComponentType { get; } + + /// + /// Get the reference type of this input component. + /// + Type Type { get; } + + /// + /// Gets the default value of this input component. + /// + object DefaultValue { get; } + + /// + /// Gets a collection of the attributes of this component. + /// + IReadOnlyCollection Attributes { get; } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + IInputComponentBuilder WithCustomId(string customId); + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + IInputComponentBuilder WithLabel(string label); + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + IInputComponentBuilder SetIsRequired(bool isRequired); + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + IInputComponentBuilder WithType(Type type); + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + IInputComponentBuilder SetDefaultValue(object value); + + /// + /// Adds attributes to . + /// + /// New attributes to be added to . + /// + /// The builder instance. + /// + IInputComponentBuilder WithAttributes(params Attribute[] attributes); + } +} diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs new file mode 100644 index 000000000..c2b9b0645 --- /dev/null +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; + +namespace Discord.Interactions.Builders +{ + /// + /// Represents the base builder class for creating . + /// + /// The this builder yields when built. + /// Inherited type. + public abstract class InputComponentBuilder : IInputComponentBuilder + where TInfo : InputComponentInfo + where TBuilder : InputComponentBuilder + { + private readonly List _attributes; + protected abstract TBuilder Instance { get; } + + /// + public ModalBuilder Modal { get; } + + /// + public string CustomId { get; set; } + + /// + public string Label { get; set; } + + /// + public bool IsRequired { get; set; } = true; + + /// + public ComponentType ComponentType { get; internal set; } + + /// + public Type Type { get; private set; } + + /// + public object DefaultValue { get; set; } + + /// + public IReadOnlyCollection Attributes => _attributes; + + /// + /// Creates an instance of + /// + /// Parent modal of this input component. + public InputComponentBuilder(ModalBuilder modal) + { + Modal = modal; + _attributes = new(); + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TBuilder WithCustomId(string customId) + { + CustomId = customId; + return Instance; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TBuilder WithLabel(string label) + { + Label = label; + return Instance; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TBuilder SetIsRequired(bool isRequired) + { + IsRequired = isRequired; + return Instance; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TBuilder WithComponentType(ComponentType componentType) + { + ComponentType = componentType; + return Instance; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TBuilder WithType(Type type) + { + Type = type; + return Instance; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TBuilder SetDefaultValue(object value) + { + DefaultValue = value; + return Instance; + } + + /// + /// Adds attributes to . + /// + /// New attributes to be added to . + /// + /// The builder instance. + /// + public TBuilder WithAttributes(params Attribute[] attributes) + { + _attributes.AddRange(attributes); + return Instance; + } + + internal abstract TInfo Build(ModalInfo modal); + + //IInputComponentBuilder + /// + IInputComponentBuilder IInputComponentBuilder.WithCustomId(string customId) => WithCustomId(customId); + + /// + IInputComponentBuilder IInputComponentBuilder.WithLabel(string label) => WithCustomId(label); + + /// + IInputComponentBuilder IInputComponentBuilder.WithType(Type type) => WithType(type); + + /// + IInputComponentBuilder IInputComponentBuilder.SetDefaultValue(object value) => SetDefaultValue(value); + + /// + IInputComponentBuilder IInputComponentBuilder.WithAttributes(params Attribute[] attributes) => WithAttributes(attributes); + + /// + IInputComponentBuilder IInputComponentBuilder.SetIsRequired(bool isRequired) => SetIsRequired(isRequired); + } +} diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs new file mode 100644 index 000000000..340119ddd --- /dev/null +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs @@ -0,0 +1,109 @@ +namespace Discord.Interactions.Builders +{ + /// + /// Represents a builder for creating . + /// + public class TextInputComponentBuilder : InputComponentBuilder + { + protected override TextInputComponentBuilder Instance => this; + + /// + /// Gets and sets the style of the text input. + /// + public TextInputStyle Style { get; set; } + + /// + /// Gets and sets the placeholder of the text input. + /// + public string Placeholder { get; set; } + + /// + /// Gets and sets the minimum length of the text input. + /// + public int MinLength { get; set; } + + /// + /// Gets and sets the maximum length of the text input. + /// + public int MaxLength { get; set; } + + /// + /// Gets and sets the initial value to be displayed by this input. + /// + public string InitialValue { get; set; } + + /// + /// Initializes a new . + /// + /// Parent modal of this component. + public TextInputComponentBuilder(ModalBuilder modal) : base(modal) { } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TextInputComponentBuilder WithStyle(TextInputStyle style) + { + Style = style; + return this; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TextInputComponentBuilder WithPlaceholder(string placeholder) + { + Placeholder = placeholder; + return this; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TextInputComponentBuilder WithMinLenght(int minLenght) + { + MinLength = minLenght; + return this; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TextInputComponentBuilder WithMaxLenght(int maxLenght) + { + MaxLength = maxLenght; + return this; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TextInputComponentBuilder WithInitialValue(string value) + { + InitialValue = value; + return this; + } + + internal override TextInputComponentInfo Build(ModalInfo modal) => + new(this, modal); + } +} diff --git a/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs new file mode 100644 index 000000000..e120e78be --- /dev/null +++ b/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Discord.Interactions.Builders +{ + /// + /// Represents a builder for creating . + /// + public class ModalBuilder + { + internal readonly List _components; + + /// + /// Gets the initialization delegate for this modal. + /// + public ModalInitializer ModalInitializer { get; internal set; } + + /// + /// Gets the title of this modal. + /// + public string Title { get; set; } + + /// + /// Gets the implementation used to initialize this object. + /// + public Type Type { get; } + + /// + /// Gets a collection of the components of this modal. + /// + public IReadOnlyCollection Components => _components; + + internal ModalBuilder(Type type) + { + if (!typeof(IModal).IsAssignableFrom(type)) + throw new ArgumentException($"Must be an implementation of {nameof(IModal)}", nameof(type)); + + _components = new(); + } + + /// + /// Initializes a new + /// + /// The initialization delegate for this modal. + public ModalBuilder(Type type, ModalInitializer modalInitializer) : this(type) + { + ModalInitializer = modalInitializer; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public ModalBuilder WithTitle(string title) + { + Title = title; + return this; + } + + /// + /// Adds text components to . + /// + /// Text Component builder factory. + /// + /// The builder instance. + /// + public ModalBuilder AddTextComponent(Action configure) + { + var builder = new TextInputComponentBuilder(this); + configure(builder); + _components.Add(builder); + return this; + } + + internal ModalInfo Build() => new(this); + } +} diff --git a/src/Discord.Net.Interactions/Builders/ModuleBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleBuilder.cs index 036964778..40c263643 100644 --- a/src/Discord.Net.Interactions/Builders/ModuleBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/ModuleBuilder.cs @@ -16,6 +16,7 @@ namespace Discord.Interactions.Builders private readonly List _contextCommands; private readonly List _componentCommands; private readonly List _autocompleteCommands; + private readonly List _modalCommands; /// /// Gets the underlying Interaction Service. @@ -92,6 +93,11 @@ namespace Discord.Interactions.Builders /// public IReadOnlyList AutocompleteCommands => _autocompleteCommands; + /// + /// Gets a collection of the Modal Commands of this module. + /// + public IReadOnlyList ModalCommands => _modalCommands; + internal TypeInfo TypeInfo { get; set; } internal ModuleBuilder (InteractionService interactionService, ModuleBuilder parent = null) @@ -105,6 +111,7 @@ namespace Discord.Interactions.Builders _contextCommands = new List(); _componentCommands = new List(); _autocompleteCommands = new List(); + _modalCommands = new List (); _preconditions = new List(); } @@ -152,7 +159,7 @@ namespace Discord.Interactions.Builders /// /// The builder instance. /// - public ModuleBuilder WithDefaultPermision (bool permission) + public ModuleBuilder WithDefaultPermission (bool permission) { DefaultPermission = permission; return this; @@ -310,6 +317,21 @@ namespace Discord.Interactions.Builders configure(command); _autocompleteCommands.Add(command); return this; + + } + + /// Adds a modal command builder to . + /// + /// factory. + /// + /// The builder instance. + /// + public ModuleBuilder AddModalCommand(Action configure) + { + var command = new ModalCommandBuilder(this); + configure(command); + _modalCommands.Add(command); + return this; } /// diff --git a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs index 071c68349..6615f131c 100644 --- a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs @@ -103,6 +103,7 @@ namespace Discord.Interactions.Builders var validContextCommands = methods.Where(IsValidContextCommandDefinition); var validInteractions = methods.Where(IsValidComponentCommandDefinition); var validAutocompleteCommands = methods.Where(IsValidAutocompleteCommandDefinition); + var validModalCommands = methods.Where(IsValidModalCommanDefinition); Func createInstance = commandService._useCompiledLambda ? ReflectionUtils.CreateLambdaBuilder(typeInfo, commandService) : ReflectionUtils.CreateBuilder(typeInfo, commandService); @@ -118,6 +119,9 @@ namespace Discord.Interactions.Builders foreach(var method in validAutocompleteCommands) builder.AddAutocompleteCommand(x => BuildAutocompleteCommand(x, createInstance, method, commandService, services)); + + foreach(var method in validModalCommands) + builder.AddModalCommand(x => BuildModalCommand(x, createInstance, method, commandService, services)); } private static void BuildSubModules (ModuleBuilder parent, IEnumerable subModules, IList builtTypes, InteractionService commandService, @@ -298,6 +302,47 @@ namespace Discord.Interactions.Builders builder.Callback = CreateCallback(createInstance, methodInfo, commandService); } + private static void BuildModalCommand(ModalCommandBuilder builder, Func createInstance, MethodInfo methodInfo, + InteractionService commandService, IServiceProvider services) + { + var parameters = methodInfo.GetParameters(); + + if (parameters.Count(x => typeof(IModal).IsAssignableFrom(x.ParameterType)) > 1) + throw new InvalidOperationException($"A modal command can only have one {nameof(IModal)} parameter."); + + if (!parameters.All(x => x.ParameterType == typeof(string) || typeof(IModal).IsAssignableFrom(x.ParameterType))) + throw new InvalidOperationException($"All parameters of a modal command must be either a string or an implementation of {nameof(IModal)}"); + + var attributes = methodInfo.GetCustomAttributes(); + + builder.MethodName = methodInfo.Name; + + foreach (var attribute in attributes) + { + switch (attribute) + { + case ModalInteractionAttribute modal: + { + builder.Name = modal.CustomId; + builder.RunMode = modal.RunMode; + builder.IgnoreGroupNames = modal.IgnoreGroupNames; + } + break; + case PreconditionAttribute precondition: + builder.WithPreconditions(precondition); + break; + default: + builder.WithAttributes(attribute); + break; + } + } + + foreach (var parameter in parameters) + builder.AddParameter(x => BuildParameter(x, parameter)); + + builder.Callback = CreateCallback(createInstance, methodInfo, commandService); + } + private static ExecuteCallback CreateCallback (Func createInstance, MethodInfo methodInfo, InteractionService commandService) { @@ -400,7 +445,9 @@ namespace Discord.Interactions.Builders builder.Name = Regex.Replace(builder.Name, "(?<=[a-z])(?=[A-Z])", "-").ToLower(); } - private static void BuildParameter (CommandParameterBuilder builder, ParameterInfo paramInfo) + private static void BuildParameter (ParameterBuilder builder, ParameterInfo paramInfo) + where TInfo : class, IParameterInfo + where TBuilder : ParameterBuilder { var attributes = paramInfo.GetCustomAttributes(); var paramType = paramInfo.ParameterType; @@ -428,6 +475,84 @@ namespace Discord.Interactions.Builders } #endregion + #region Modals + public static ModalInfo BuildModalInfo(Type modalType) + { + if (!typeof(IModal).IsAssignableFrom(modalType)) + throw new InvalidOperationException($"{modalType.FullName} isn't an implementation of {typeof(IModal).FullName}"); + + var instance = Activator.CreateInstance(modalType, false) as IModal; + + try + { + var builder = new ModalBuilder(modalType) + { + Title = instance.Title + }; + + var inputs = modalType.GetProperties().Where(IsValidModalInputDefinition); + + foreach (var prop in inputs) + { + var componentType = prop.GetCustomAttribute()?.ComponentType; + + switch (componentType) + { + case ComponentType.TextInput: + builder.AddTextComponent(x => BuildTextInput(x, prop, prop.GetValue(instance))); + break; + case null: + throw new InvalidOperationException($"{prop.Name} of {prop.DeclaringType.Name} isn't a valid modal input field."); + default: + throw new InvalidOperationException($"Component type {componentType} cannot be used in modals."); + } + } + + var memberInit = ReflectionUtils.CreateLambdaMemberInit(modalType.GetTypeInfo(), modalType.GetConstructor(Type.EmptyTypes), x => x.IsDefined(typeof(ModalInputAttribute))); + builder.ModalInitializer = (args) => memberInit(Array.Empty(), args); + return builder.Build(); + } + finally + { + (instance as IDisposable)?.Dispose(); + } + } + + private static void BuildTextInput(TextInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) + { + var attributes = propertyInfo.GetCustomAttributes(); + + builder.Label = propertyInfo.Name; + builder.DefaultValue = defaultValue; + builder.WithType(propertyInfo.PropertyType); + + foreach(var attribute in attributes) + { + switch (attribute) + { + case ModalTextInputAttribute textInput: + builder.CustomId = textInput.CustomId; + builder.ComponentType = textInput.ComponentType; + builder.Style = textInput.Style; + builder.Placeholder = textInput.Placeholder; + builder.MaxLength = textInput.MaxLength; + builder.MinLength = textInput.MinLength; + builder.InitialValue = textInput.InitialValue; + break; + case RequiredInputAttribute requiredInput: + builder.IsRequired = requiredInput.IsRequired; + break; + case InputLabelAttribute inputLabel: + builder.Label = inputLabel.Label; + break; + default: + builder.WithAttributes(attribute); + break; + } + } + } + #endregion + internal static bool IsValidModuleDefinition (TypeInfo typeInfo) { return ModuleTypeInfo.IsAssignableFrom(typeInfo) && @@ -467,5 +592,21 @@ namespace Discord.Interactions.Builders !methodInfo.IsGenericMethod && methodInfo.GetParameters().Length == 0; } + + private static bool IsValidModalCommanDefinition(MethodInfo methodInfo) + { + return methodInfo.IsDefined(typeof(ModalInteractionAttribute)) && + (methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task)) && + !methodInfo.IsStatic && + !methodInfo.IsGenericMethod && + typeof(IModal).IsAssignableFrom(methodInfo.GetParameters().Last().ParameterType); + } + + private static bool IsValidModalInputDefinition(PropertyInfo propertyInfo) + { + return propertyInfo.SetMethod?.IsPublic == true && + propertyInfo.SetMethod?.IsStatic == false && + propertyInfo.IsDefined(typeof(ModalInputAttribute)); + } } } diff --git a/src/Discord.Net.Interactions/Builders/Parameters/ModalCommandParameterBuilder.cs b/src/Discord.Net.Interactions/Builders/Parameters/ModalCommandParameterBuilder.cs new file mode 100644 index 000000000..a0315e1ea --- /dev/null +++ b/src/Discord.Net.Interactions/Builders/Parameters/ModalCommandParameterBuilder.cs @@ -0,0 +1,45 @@ +using System; + +namespace Discord.Interactions.Builders +{ + + /// + /// Represents a builder for creating . + /// + public class ModalCommandParameterBuilder : ParameterBuilder + { + protected override ModalCommandParameterBuilder Instance => this; + + /// + /// Gets the built class for this parameter, if is . + /// + public ModalInfo Modal { get; private set; } + + /// + /// Gets whether or not this parameter is an . + /// + public bool IsModalParameter => Modal is not null; + + internal ModalCommandParameterBuilder(ICommandBuilder command) : base(command) { } + + /// + /// Initializes a new . + /// + /// Parent command of this parameter. + /// Name of this command. + /// Type of this parameter. + public ModalCommandParameterBuilder(ICommandBuilder command, string name, Type type) : base(command, name, type) { } + + /// + public override ModalCommandParameterBuilder SetParameterType(Type type) + { + if (typeof(IModal).IsAssignableFrom(type)) + Modal = ModalUtils.GetOrAdd(type); + + return base.SetParameterType(type); + } + + internal override ModalCommandParameterInfo Build(ICommandInfo command) => + new(this, command); + } +} diff --git a/src/Discord.Net.Interactions/Entities/IModal.cs b/src/Discord.Net.Interactions/Entities/IModal.cs new file mode 100644 index 000000000..572a88033 --- /dev/null +++ b/src/Discord.Net.Interactions/Entities/IModal.cs @@ -0,0 +1,13 @@ +namespace Discord.Interactions +{ + /// + /// Represents a generic for use with the interaction service. + /// + public interface IModal + { + /// + /// Gets the modal's title. + /// + string Title { get; } + } +} diff --git a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs new file mode 100644 index 000000000..5c379cf42 --- /dev/null +++ b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs @@ -0,0 +1,37 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + public static class IDiscordInteractionExtentions + { + /// + /// Respond to an interaction with a . + /// + /// Type of the implementation. + /// The interaction to respond to. + /// The request options for this request. + /// A task that represents the asynchronous operation of responding to the interaction. + public static async Task RespondWithModalAsync(this IDiscordInteraction interaction, string customId, RequestOptions options = null) + where T : class, IModal + { + if (!ModalUtils.TryGet(out var modalInfo)) + throw new ArgumentException($"{typeof(T).FullName} isn't referenced by any registered Modal Interaction Command and doesn't have a cached {typeof(ModalInfo)}"); + + var builder = new ModalBuilder(modalInfo.Title, customId); + + foreach(var input in modalInfo.Components) + switch (input) + { + case TextInputComponentInfo textComponent: + builder.AddTextInput(textComponent.Label, textComponent.CustomId, textComponent.Style, textComponent.Placeholder, textComponent.IsRequired ? textComponent.MinLength : null, + textComponent.MaxLength, textComponent.IsRequired, textComponent.InitialValue); + break; + default: + throw new InvalidOperationException($"{input.GetType().FullName} isn't a valid component info class"); + } + + await interaction.RespondWithModalAsync(builder.Build(), options).ConfigureAwait(false); + } + } +} diff --git a/src/Discord.Net.Interactions/Info/Commands/ComponentCommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/ComponentCommandInfo.cs index 91fe2dbf9..0e43af3a8 100644 --- a/src/Discord.Net.Interactions/Info/Commands/ComponentCommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/Commands/ComponentCommandInfo.cs @@ -35,7 +35,7 @@ namespace Discord.Interactions /// Services that will be used while initializing the . /// Provide additional string parameters to the method along with the auto generated parameters. /// - /// A task representing the asyncronous command execution process. + /// A task representing the asynchronous command execution process. /// public async Task ExecuteAsync(IInteractionContext context, IServiceProvider services, params string[] additionalArgs) { diff --git a/src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs new file mode 100644 index 000000000..a750603fc --- /dev/null +++ b/src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +namespace Discord.Interactions +{ + /// + /// Represents the info class of an attribute based method for handling Modal Interaction events. + /// + public class ModalCommandInfo : CommandInfo + { + /// + /// Gets the class for this commands parameter. + /// + public ModalInfo Modal { get; } + + /// + public override bool SupportsWildCards => true; + + /// + public override IReadOnlyCollection Parameters { get; } + + internal ModalCommandInfo(Builders.ModalCommandBuilder builder, ModuleInfo module, InteractionService commandService) : base(builder, module, commandService) + { + Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray(); + Modal = Parameters.Last().Modal; + } + + /// + public override async Task ExecuteAsync(IInteractionContext context, IServiceProvider services) + => await ExecuteAsync(context, services, null).ConfigureAwait(false); + + /// + /// Execute this command using dependency injection. + /// + /// Context that will be injected to the . + /// Services that will be used while initializing the . + /// Provide additional string parameters to the method along with the auto generated parameters. + /// + /// A task representing the asynchronous command execution process. + /// + public async Task ExecuteAsync(IInteractionContext context, IServiceProvider services, params string[] additionalArgs) + { + if (context.Interaction is not IModalInteraction modalInteraction) + return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Modal Interaction."); + + try + { + var args = new List(); + + if (additionalArgs is not null) + args.AddRange(additionalArgs); + + var modal = Modal.CreateModal(modalInteraction, Module.CommandService._exitOnMissingModalField); + args.Add(modal); + + return await RunAsync(context, args.ToArray(), services); + } + catch (Exception ex) + { + var result = ExecuteResult.FromError(ex); + await InvokeModuleEvent(context, result).ConfigureAwait(false); + return result; + } + } + + /// + protected override Task InvokeModuleEvent(IInteractionContext context, IResult result) + => CommandService._modalCommandExecutedEvent.InvokeAsync(this, context, result); + + /// + protected override string GetLogString(IInteractionContext context) + { + if (context.Guild != null) + return $"Modal Command: \"{base.ToString()}\" for {context.User} in {context.Guild}/{context.Channel}"; + else + return $"Modal Command: \"{base.ToString()}\" for {context.User} in {context.Channel}"; + } + } +} diff --git a/src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs b/src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs new file mode 100644 index 000000000..790838ad9 --- /dev/null +++ b/src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Discord.Interactions +{ + /// + /// Represents the base info class for input components. + /// + public abstract class InputComponentInfo + { + /// + /// Gets the parent modal of this component. + /// + public ModalInfo Modal { get; } + + /// + /// Gets the custom id of this component. + /// + public string CustomId { get; } + + /// + /// Gets the label of this component. + /// + public string Label { get; } + + /// + /// Gets whether or not this component requires a user input. + /// + public bool IsRequired { get; } + + /// + /// Gets the type of this component. + /// + public ComponentType ComponentType { get; } + + /// + /// Gets the reference type of this component. + /// + public Type Type { get; } + + /// + /// Gets the default value of this component. + /// + public object DefaultValue { get; } + + /// + /// Gets a collection of the attributes of this command. + /// + public IReadOnlyCollection Attributes { get; } + + protected InputComponentInfo(Builders.IInputComponentBuilder builder, ModalInfo modal) + { + Modal = modal; + CustomId = builder.CustomId; + Label = builder.Label; + IsRequired = builder.IsRequired; + ComponentType = builder.ComponentType; + Type = builder.Type; + DefaultValue = builder.DefaultValue; + Attributes = builder.Attributes.ToImmutableArray(); + } + } +} diff --git a/src/Discord.Net.Interactions/Info/InputComponents/TextInputComponentInfo.cs b/src/Discord.Net.Interactions/Info/InputComponents/TextInputComponentInfo.cs new file mode 100644 index 000000000..613549fe8 --- /dev/null +++ b/src/Discord.Net.Interactions/Info/InputComponents/TextInputComponentInfo.cs @@ -0,0 +1,42 @@ +namespace Discord.Interactions +{ + /// + /// Represents the class for type. + /// + public class TextInputComponentInfo : InputComponentInfo + { + /// + /// Gets the style of the text input. + /// + public TextInputStyle Style { get; } + + /// + /// Gets the placeholder of the text input. + /// + public string Placeholder { get; } + + /// + /// Gets the minimum length of the text input. + /// + public int MinLength { get; } + + /// + /// Gets the maximum length of the text input. + /// + public int MaxLength { get; } + + /// + /// Gets the initial value to be displayed by this input. + /// + public string InitialValue { get; } + + internal TextInputComponentInfo(Builders.TextInputComponentBuilder builder, ModalInfo modal) : base(builder, modal) + { + Style = builder.Style; + Placeholder = builder.Placeholder; + MinLength = builder.MinLength; + MaxLength = builder.MaxLength; + InitialValue = builder.InitialValue; + } + } +} diff --git a/src/Discord.Net.Interactions/Info/ModalInfo.cs b/src/Discord.Net.Interactions/Info/ModalInfo.cs new file mode 100644 index 000000000..edc31373e --- /dev/null +++ b/src/Discord.Net.Interactions/Info/ModalInfo.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Discord.Interactions +{ + /// + /// Represents a cached object initialization delegate. + /// + /// Property arguments array. + /// + /// Returns the constructed object. + /// + public delegate IModal ModalInitializer(object[] args); + + /// + /// Represents the info class of an form. + /// + public class ModalInfo + { + internal readonly ModalInitializer _initializer; + + /// + /// Gets the title of this modal. + /// + public string Title { get; } + + /// + /// Gets the implementation used to initialize this object. + /// + public Type Type { get; } + + /// + /// Gets a collection of the components of this modal. + /// + public IReadOnlyCollection Components { get; } + + /// + /// Gets a collection of the text components of this modal. + /// + public IReadOnlyCollection TextComponents { get; } + + internal ModalInfo(Builders.ModalBuilder builder) + { + Title = builder.Title; + Type = builder.Type; + Components = builder.Components.Select(x => x switch + { + Builders.TextInputComponentBuilder textComponent => textComponent.Build(this), + _ => throw new InvalidOperationException($"{x.GetType().FullName} isn't a supported modal input component builder type.") + }).ToImmutableArray(); + + TextComponents = Components.OfType().ToImmutableArray(); + + _initializer = builder.ModalInitializer; + } + + /// + /// Creates an and fills it with provided message components. + /// + /// that will be injected into the modal. + /// + /// A filled with the provided components. + /// + public IModal CreateModal(IModalInteraction modalInteraction, bool throwOnMissingField = false) + { + var args = new object[Components.Count]; + var components = modalInteraction.Data.Components.ToList(); + + for (var i = 0; i < Components.Count; i++) + { + var input = Components.ElementAt(i); + var component = components.Find(x => x.CustomId == input.CustomId); + + if (component is null) + { + if (!throwOnMissingField) + args[i] = input.DefaultValue; + else + throw new InvalidOperationException($"Modal interaction is missing the required field: {input.CustomId}"); + } + else + args[i] = component.Value; + } + + return _initializer(args); + } + } +} diff --git a/src/Discord.Net.Interactions/Info/ModuleInfo.cs b/src/Discord.Net.Interactions/Info/ModuleInfo.cs index 4388ea722..321e0bfa9 100644 --- a/src/Discord.Net.Interactions/Info/ModuleInfo.cs +++ b/src/Discord.Net.Interactions/Info/ModuleInfo.cs @@ -68,6 +68,8 @@ namespace Discord.Interactions /// public IReadOnlyCollection AutocompleteCommands { get; } + public IReadOnlyCollection ModalCommands { get; } + /// /// Gets the declaring type of this module, if is . /// @@ -112,6 +114,7 @@ namespace Discord.Interactions ContextCommands = BuildContextCommands(builder).ToImmutableArray(); ComponentCommands = BuildComponentCommands(builder).ToImmutableArray(); AutocompleteCommands = BuildAutocompleteCommands(builder).ToImmutableArray(); + ModalCommands = BuildModalCommands(builder).ToImmutableArray(); SubModules = BuildSubModules(builder, commandService, services).ToImmutableArray(); Attributes = BuildAttributes(builder).ToImmutableArray(); Preconditions = BuildPreconditions(builder).ToImmutableArray(); @@ -171,6 +174,16 @@ namespace Discord.Interactions return result; } + private IEnumerable BuildModalCommands(ModuleBuilder builder) + { + var result = new List(); + + foreach (var commandBuilder in builder.ModalCommands) + result.Add(commandBuilder.Build(this, CommandService)); + + return result; + } + private IEnumerable BuildAttributes (ModuleBuilder builder) { var result = new List(); diff --git a/src/Discord.Net.Interactions/Info/Parameters/ModalCommandParameterInfo.cs b/src/Discord.Net.Interactions/Info/Parameters/ModalCommandParameterInfo.cs new file mode 100644 index 000000000..28162e109 --- /dev/null +++ b/src/Discord.Net.Interactions/Info/Parameters/ModalCommandParameterInfo.cs @@ -0,0 +1,28 @@ +using Discord.Interactions.Builders; + +namespace Discord.Interactions +{ + /// + /// Represents the base parameter info class for modals. + /// + public class ModalCommandParameterInfo : CommandParameterInfo + { + /// + /// Gets the class for this parameter if is true. + /// + public ModalInfo Modal { get; private set; } + + /// + /// Gets whether this parameter is an + /// + public bool IsModalParameter => Modal is not null; + + /// + public new ModalCommandInfo Command => base.Command as ModalCommandInfo; + + internal ModalCommandParameterInfo(ModalCommandParameterBuilder builder, ICommandInfo command) : base(builder, command) + { + Modal = builder.Modal; + } + } +} diff --git a/src/Discord.Net.Interactions/InteractionModuleBase.cs b/src/Discord.Net.Interactions/InteractionModuleBase.cs index 997542a2e..873f4c173 100644 --- a/src/Discord.Net.Interactions/InteractionModuleBase.cs +++ b/src/Discord.Net.Interactions/InteractionModuleBase.cs @@ -114,6 +114,13 @@ namespace Discord.Interactions var response = await Context.Interaction.GetOriginalResponseAsync().ConfigureAwait(false); await response.DeleteAsync().ConfigureAwait(false); } + + /// + protected virtual async Task RespondWithModalAsync(Modal modal, RequestOptions options = null) => await Context.Interaction.RespondWithModalAsync(modal); + + /// + protected virtual async Task RespondWithModalAsync(string customId, RequestOptions options = null) where T : class, IModal + => await Context.Interaction.RespondWithModalAsync(customId, options); //IInteractionModuleBase diff --git a/src/Discord.Net.Interactions/InteractionService.cs b/src/Discord.Net.Interactions/InteractionService.cs index 475622f0b..c1291bd6b 100644 --- a/src/Discord.Net.Interactions/InteractionService.cs +++ b/src/Discord.Net.Interactions/InteractionService.cs @@ -53,21 +53,29 @@ namespace Discord.Interactions public event Func AutocompleteHandlerExecuted { add { _autocompleteHandlerExecutedEvent.Add(value); } remove { _autocompleteHandlerExecutedEvent.Remove(value); } } internal readonly AsyncEvent> _autocompleteHandlerExecutedEvent = new(); + /// + /// Occurs when a Modal command is executed. + /// + public event Func ModalCommandExecuted { add { _modalCommandExecutedEvent.Add(value); } remove { _modalCommandExecutedEvent.Remove(value); } } + internal readonly AsyncEvent> _modalCommandExecutedEvent = new(); + private readonly ConcurrentDictionary _typedModuleDefs; private readonly CommandMap _slashCommandMap; private readonly ConcurrentDictionary> _contextCommandMaps; private readonly CommandMap _componentCommandMap; private readonly CommandMap _autocompleteCommandMap; + private readonly CommandMap _modalCommandMap; private readonly HashSet _moduleDefs; private readonly ConcurrentDictionary _typeConverters; private readonly ConcurrentDictionary _genericTypeConverters; private readonly ConcurrentDictionary _autocompleteHandlers = new(); + private readonly ConcurrentDictionary _modalInfos = new(); private readonly SemaphoreSlim _lock; internal readonly Logger _cmdLogger; internal readonly LogManager _logManager; internal readonly Func _getRestClient; - internal readonly bool _throwOnError, _useCompiledLambda, _enableAutocompleteHandlers, _autoServiceScopes; + internal readonly bool _throwOnError, _useCompiledLambda, _enableAutocompleteHandlers, _autoServiceScopes, _exitOnMissingModalField; internal readonly string _wildCardExp; internal readonly RunMode _runMode; internal readonly RestResponseCallback _restResponseCallback; @@ -97,6 +105,16 @@ namespace Discord.Interactions /// public IReadOnlyCollection ComponentCommands => _moduleDefs.SelectMany(x => x.ComponentCommands).ToList(); + /// + /// Represents all Modal Commands loaded within . + /// + public IReadOnlyCollection ModalCommands => _moduleDefs.SelectMany(x => x.ModalCommands).ToList(); + + /// + /// Gets a collection of the cached classes that are referenced in registered s. + /// + public IReadOnlyCollection Modals => ModalUtils.Modals; + /// /// Initialize a with provided configurations. /// @@ -145,6 +163,7 @@ namespace Discord.Interactions _contextCommandMaps = new ConcurrentDictionary>(); _componentCommandMap = new CommandMap(this, config.InteractionCustomIdDelimiters); _autocompleteCommandMap = new CommandMap(this); + _modalCommandMap = new CommandMap(this, config.InteractionCustomIdDelimiters); _getRestClient = getRestClient; @@ -155,6 +174,7 @@ namespace Discord.Interactions _throwOnError = config.ThrowOnError; _wildCardExp = config.WildCardExpression; _useCompiledLambda = config.UseCompiledLambda; + _exitOnMissingModalField = config.ExitOnMissingModalField; _enableAutocompleteHandlers = config.EnableAutocompleteHandlers; _autoServiceScopes = config.AutoServiceScopes; _restResponseCallback = config.RestResponseCallback; @@ -509,6 +529,9 @@ namespace Discord.Interactions foreach (var command in module.AutocompleteCommands) _autocompleteCommandMap.AddCommand(command.GetCommandKeywords(), command); + foreach (var command in module.ModalCommands) + _modalCommandMap.AddCommand(command, command.IgnoreGroupNames); + foreach (var subModule in module.SubModules) LoadModuleInternal(subModule); } @@ -654,7 +677,7 @@ namespace Discord.Interactions public async Task ExecuteCommandAsync (IInteractionContext context, IServiceProvider services) { var interaction = context.Interaction; - + return interaction switch { ISlashCommandInteraction slashCommand => await ExecuteSlashCommandAsync(context, slashCommand, services).ConfigureAwait(false), @@ -662,6 +685,7 @@ namespace Discord.Interactions IUserCommandInteraction userCommand => await ExecuteContextCommandAsync(context, userCommand.Data.Name, ApplicationCommandType.User, services).ConfigureAwait(false), IMessageCommandInteraction messageCommand => await ExecuteContextCommandAsync(context, messageCommand.Data.Name, ApplicationCommandType.Message, services).ConfigureAwait(false), IAutocompleteInteraction autocomplete => await ExecuteAutocompleteAsync(context, autocomplete, services).ConfigureAwait(false), + IModalInteraction modalCommand => await ExecuteModalCommandAsync(context, modalCommand.Data.CustomId, services).ConfigureAwait(false), _ => throw new InvalidOperationException($"{interaction.Type} interaction type cannot be executed by the Interaction service"), }; } @@ -745,6 +769,20 @@ namespace Discord.Interactions return await commandResult.Command.ExecuteAsync(context, services).ConfigureAwait(false); } + private async Task ExecuteModalCommandAsync(IInteractionContext context, string input, IServiceProvider services) + { + var result = _modalCommandMap.GetCommand(input); + + if (!result.IsSuccess) + { + await _cmdLogger.DebugAsync($"Unknown custom interaction id, skipping execution ({input.ToUpper()})"); + + await _componentCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false); + return result; + } + return await result.Command.ExecuteAsync(context, services, result.RegexCaptureGroups).ConfigureAwait(false); + } + internal TypeConverter GetTypeConverter (Type type, IServiceProvider services = null) { if (_typeConverters.TryGetValue(type, out var specific)) @@ -819,6 +857,24 @@ namespace Discord.Interactions _genericTypeConverters[targetType] = converterType; } + /// + /// Loads and caches an for the provided . + /// + /// Type of to be loaded. + /// + /// The built instance. + /// + /// + public ModalInfo AddModalInfo() where T : class, IModal + { + var type = typeof(T); + + if (_modalInfos.ContainsKey(type)) + throw new InvalidOperationException($"Modal type {type.FullName} already exists."); + + return ModalUtils.GetOrAdd(type); + } + internal IAutocompleteHandler GetAutocompleteHandler(Type autocompleteHandlerType, IServiceProvider services = null) { services ??= EmptyServiceProvider.Instance; diff --git a/src/Discord.Net.Interactions/InteractionServiceConfig.cs b/src/Discord.Net.Interactions/InteractionServiceConfig.cs index a1583a124..136cba24c 100644 --- a/src/Discord.Net.Interactions/InteractionServiceConfig.cs +++ b/src/Discord.Net.Interactions/InteractionServiceConfig.cs @@ -36,6 +36,9 @@ namespace Discord.Interactions /// /// Gets or sets the option to use compiled lambda expressions to create module instances and execute commands. This method improves performance at the cost of memory. /// + /// + /// For performance reasons, if you frequently use s with the service, it is highly recommended that you enable compiled lambdas. + /// public bool UseCompiledLambda { get; set; } = false; /// @@ -56,6 +59,11 @@ namespace Discord.Interactions /// Gets or sets delegate to be used by the when responding to a Rest based interaction. /// public RestResponseCallback RestResponseCallback { get; set; } = (ctx, str) => Task.CompletedTask; + + /// + /// Gets or sets whether a command execution should exit when a modal command encounters a missing modal component value. + /// + public bool ExitOnMissingModalField { get; set; } = false; } /// diff --git a/src/Discord.Net.Interactions/Utilities/ModalUtils.cs b/src/Discord.Net.Interactions/Utilities/ModalUtils.cs new file mode 100644 index 000000000..d42cc2fe9 --- /dev/null +++ b/src/Discord.Net.Interactions/Utilities/ModalUtils.cs @@ -0,0 +1,51 @@ +using Discord.Interactions.Builders; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace Discord.Interactions +{ + internal static class ModalUtils + { + private static ConcurrentDictionary _modalInfos = new(); + + public static IReadOnlyCollection Modals => _modalInfos.Values.ToReadOnlyCollection(); + + public static ModalInfo GetOrAdd(Type type) + { + if (!typeof(IModal).IsAssignableFrom(type)) + throw new ArgumentException($"Must be an implementation of {nameof(IModal)}", nameof(type)); + + return _modalInfos.GetOrAdd(type, ModuleClassBuilder.BuildModalInfo(type)); + } + + public static ModalInfo GetOrAdd() where T : class, IModal + => GetOrAdd(typeof(T)); + + public static bool TryGet(Type type, out ModalInfo modalInfo) + { + if (!typeof(IModal).IsAssignableFrom(type)) + throw new ArgumentException($"Must be an implementation of {nameof(IModal)}", nameof(type)); + + return _modalInfos.TryGetValue(type, out modalInfo); + } + + public static bool TryGet(out ModalInfo modalInfo) where T : class, IModal + => TryGet(typeof(T), out modalInfo); + + public static bool TryRemove(Type type, out ModalInfo modalInfo) + { + if (!typeof(IModal).IsAssignableFrom(type)) + throw new ArgumentException($"Must be an implementation of {nameof(IModal)}", nameof(type)); + + return _modalInfos.TryRemove(type, out modalInfo); + } + + public static bool TryRemove(out ModalInfo modalInfo) where T : class, IModal + => TryRemove(typeof(T), out modalInfo); + + public static void Clear() => _modalInfos.Clear(); + + public static int Count() => _modalInfos.Count; + } +} diff --git a/src/Discord.Net.Interactions/Utilities/ReflectionUtils.cs b/src/Discord.Net.Interactions/Utilities/ReflectionUtils.cs index b15662bfb..5d3da4c5c 100644 --- a/src/Discord.Net.Interactions/Utilities/ReflectionUtils.cs +++ b/src/Discord.Net.Interactions/Utilities/ReflectionUtils.cs @@ -112,6 +112,67 @@ namespace Discord.Interactions var parameters = constructor.GetParameters(); var properties = GetProperties(typeInfo); + var lambda = CreateLambdaMemberInit(typeInfo, constructor); + + return (services) => + { + var args = new object[parameters.Length]; + var props = new object[properties.Length]; + + for (int i = 0; i < parameters.Length; i++) + args[i] = GetMember(commandService, services, parameters[i].ParameterType, typeInfo); + + for (int i = 0; i < properties.Length; i++) + props[i] = GetMember(commandService, services, properties[i].PropertyType, typeInfo); + + var instance = lambda(args, props); + + return instance; + }; + } + + internal static Func CreateLambdaConstructorInvoker(TypeInfo typeInfo) + { + var constructor = GetConstructor(typeInfo); + var parameters = constructor.GetParameters(); + + var argsExp = Expression.Parameter(typeof(object[]), "args"); + + var parameterExps = new Expression[parameters.Length]; + + for (var i = 0; i < parameters.Length; i++) + { + var indexExp = Expression.Constant(i); + var accessExp = Expression.ArrayIndex(argsExp, indexExp); + parameterExps[i] = Expression.Convert(accessExp, parameters[i].ParameterType); + } + + var newExp = Expression.New(constructor, parameterExps); + + return Expression.Lambda>(newExp, argsExp).Compile(); + } + + /// + /// Create a compiled lambda property setter. + /// + internal static Action CreateLambdaPropertySetter(PropertyInfo propertyInfo) + { + var instanceParam = Expression.Parameter(typeof(T), "instance"); + var valueParam = Expression.Parameter(typeof(object), "value"); + + var prop = Expression.Property(instanceParam, propertyInfo); + var assign = Expression.Assign(prop, Expression.Convert(valueParam, propertyInfo.PropertyType)); + + return Expression.Lambda>(assign, instanceParam, valueParam).Compile(); + } + + internal static Func CreateLambdaMemberInit(TypeInfo typeInfo, ConstructorInfo constructor, Predicate propertySelect = null) + { + propertySelect ??= x => true; + + var parameters = constructor.GetParameters(); + var properties = GetProperties(typeInfo).Where(x => propertySelect(x)).ToArray(); + var argsExp = Expression.Parameter(typeof(object[]), "args"); var propsExp = Expression.Parameter(typeof(object[]), "props"); @@ -137,17 +198,8 @@ namespace Discord.Interactions var memberInit = Expression.MemberInit(newExp, memberExps); var lambda = Expression.Lambda>(memberInit, argsExp, propsExp).Compile(); - return (services) => + return (args, props) => { - var args = new object[parameters.Length]; - var props = new object[properties.Length]; - - for (int i = 0; i < parameters.Length; i++) - args[i] = GetMember(commandService, services, parameters[i].ParameterType, typeInfo); - - for (int i = 0; i < properties.Length; i++) - props[i] = GetMember(commandService, services, properties[i].PropertyType, typeInfo); - var instance = lambda(args, props); return instance; diff --git a/src/Discord.Net.Rest/API/Common/ActionRowComponent.cs b/src/Discord.Net.Rest/API/Common/ActionRowComponent.cs index 9dede7e03..9a7eb80dd 100644 --- a/src/Discord.Net.Rest/API/Common/ActionRowComponent.cs +++ b/src/Discord.Net.Rest/API/Common/ActionRowComponent.cs @@ -21,6 +21,7 @@ namespace Discord.API { ComponentType.Button => new ButtonComponent(x as Discord.ButtonComponent), ComponentType.SelectMenu => new SelectMenuComponent(x as Discord.SelectMenuComponent), + ComponentType.TextInput => new TextInputComponent(x as Discord.TextInputComponent), _ => null }; }).ToArray(); diff --git a/src/Discord.Net.Rest/API/Common/InteractionCallbackData.cs b/src/Discord.Net.Rest/API/Common/InteractionCallbackData.cs index b07ebff49..3685d7a99 100644 --- a/src/Discord.Net.Rest/API/Common/InteractionCallbackData.cs +++ b/src/Discord.Net.Rest/API/Common/InteractionCallbackData.cs @@ -24,5 +24,11 @@ namespace Discord.API [JsonProperty("choices")] public Optional Choices { get; set; } + + [JsonProperty("title")] + public Optional Title { get; set; } + + [JsonProperty("custom_id")] + public Optional CustomId { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/MessageComponentInteractionData.cs b/src/Discord.Net.Rest/API/Common/MessageComponentInteractionData.cs index a7760911c..4633fc25a 100644 --- a/src/Discord.Net.Rest/API/Common/MessageComponentInteractionData.cs +++ b/src/Discord.Net.Rest/API/Common/MessageComponentInteractionData.cs @@ -12,5 +12,8 @@ namespace Discord.API [JsonProperty("values")] public Optional Values { get; set; } + + [JsonProperty("value")] + public Optional Value { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/ModalInteractionData.cs b/src/Discord.Net.Rest/API/Common/ModalInteractionData.cs new file mode 100644 index 000000000..182fa53b2 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ModalInteractionData.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class ModalInteractionData : IDiscordInteractionData + { + [JsonProperty("custom_id")] + public string CustomId { get; set; } + + [JsonProperty("components")] + public API.ActionRowComponent[] Components { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs b/src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs index 0886a8fe9..25ac476c5 100644 --- a/src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs +++ b/src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs @@ -26,6 +26,8 @@ namespace Discord.API [JsonProperty("disabled")] public bool Disabled { get; set; } + [JsonProperty("values")] + public Optional Values { get; set; } public SelectMenuComponent() { } public SelectMenuComponent(Discord.SelectMenuComponent component) diff --git a/src/Discord.Net.Rest/API/Common/TextInputComponent.cs b/src/Discord.Net.Rest/API/Common/TextInputComponent.cs new file mode 100644 index 000000000..a475345fc --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/TextInputComponent.cs @@ -0,0 +1,49 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class TextInputComponent : IMessageComponent + { + [JsonProperty("type")] + public ComponentType Type { get; set; } + + [JsonProperty("style")] + public TextInputStyle Style { get; set; } + + [JsonProperty("custom_id")] + public string CustomId { get; set; } + + [JsonProperty("label")] + public string Label { get; set; } + + [JsonProperty("placeholder")] + public Optional Placeholder { get; set; } + + [JsonProperty("min_length")] + public Optional MinLength { get; set; } + + [JsonProperty("max_length")] + public Optional MaxLength { get; set; } + + [JsonProperty("value")] + public Optional Value { get; set; } + + [JsonProperty("required")] + public Optional Required { get; set; } + + public TextInputComponent() { } + + public TextInputComponent(Discord.TextInputComponent component) + { + Type = component.Type; + Style = component.Style; + CustomId = component.CustomId; + Label = component.Label; + Placeholder = component.Placeholder; + MinLength = component.MinLength ?? Optional.Unspecified; + MaxLength = component.MaxLength ?? Optional.Unspecified; + Required = component.Required ?? Optional.Unspecified; + Value = component.Value ?? Optional.Unspecified; + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs index 2069b9913..bb2e2c27d 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs @@ -316,5 +316,45 @@ namespace Discord.Rest return SerializePayload(response); } + + /// + /// Responds to the interaction with a modal. + /// + /// The modal to respond with. + /// The request options for this request. + /// A string that contains json to write back to the incoming http request. + /// + /// + public override string RespondWithModal(Modal modal, RequestOptions options = null) + { + if (!InteractionHelper.CanSendResponse(this)) + throw new TimeoutException($"Cannot defer an interaction after {InteractionHelper.ResponseTimeLimit} seconds of no response/acknowledgement"); + + var response = new API.InteractionResponse + { + Type = InteractionResponseType.Modal, + Data = new API.InteractionCallbackData + { + CustomId = modal.CustomId, + Title = modal.Title, + Components = modal.Component.Components.Select(x => new Discord.API.ActionRowComponent(x)).ToArray() + } + }; + + lock (_lock) + { + if (HasResponded) + { + throw new InvalidOperationException("Cannot respond or defer twice to the same interaction"); + } + } + + lock (_lock) + { + HasResponded = true; + } + + return SerializePayload(response); + } } } diff --git a/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs index d9643079e..359b92249 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs @@ -446,6 +446,46 @@ namespace Discord.Rest return SerializePayload(response); } + /// + /// Responds to the interaction with a modal. + /// + /// The modal to respond with. + /// The request options for this request. + /// A string that contains json to write back to the incoming http request. + /// + /// + public override string RespondWithModal(Modal modal, RequestOptions options = null) + { + if (!InteractionHelper.CanSendResponse(this)) + throw new TimeoutException($"Cannot defer an interaction after {InteractionHelper.ResponseTimeLimit} seconds of no response/acknowledgement"); + + var response = new API.InteractionResponse + { + Type = InteractionResponseType.Modal, + Data = new API.InteractionCallbackData + { + CustomId = modal.CustomId, + Title = modal.Title, + Components = modal.Component.Components.Select(x => new Discord.API.ActionRowComponent(x)).ToArray() + } + }; + + lock (_lock) + { + if (HasResponded) + { + throw new InvalidOperationException("Cannot respond or defer twice to the same interaction."); + } + } + + lock (_lock) + { + HasResponded = true; + } + + return SerializePayload(response); + } + //IComponentInteraction /// IComponentInteractionData IComponentInteraction.Data => Data; diff --git a/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponentData.cs b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponentData.cs index e865c208c..d065b258f 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponentData.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponentData.cs @@ -27,11 +27,26 @@ namespace Discord.Rest /// public IReadOnlyCollection Values { get; } + /// + public string Value { get; } + internal RestMessageComponentData(Model model) { CustomId = model.CustomId; Type = model.ComponentType; Values = model.Values.GetValueOrDefault(); } + + internal RestMessageComponentData(IMessageComponent component) + { + CustomId = component.CustomId; + Type = component.Type; + + if (component is API.TextInputComponent textInput) + Value = textInput.Value.Value; + + if (component is API.SelectMenuComponent select) + Values = select.Values.Value; + } } } diff --git a/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModal.cs b/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModal.cs new file mode 100644 index 000000000..5f54fe051 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModal.cs @@ -0,0 +1,402 @@ +using Discord.Net.Rest; +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using DataModel = Discord.API.ModalInteractionData; +using ModelBase = Discord.API.Interaction; + +namespace Discord.Rest +{ + /// + /// Represents a user submitted . + /// + public class RestModal : RestInteraction, IDiscordInteraction, IModalInteraction + { + internal RestModal(DiscordRestClient client, ModelBase model) + : base(client, model.Id) + { + var dataModel = model.Data.IsSpecified + ? (DataModel)model.Data.Value + : null; + + Data = new RestModalData(dataModel); + } + + internal new static async Task CreateAsync(DiscordRestClient client, ModelBase model) + { + var entity = new RestModal(client, model); + await entity.UpdateAsync(client, model); + return entity; + } + + private object _lock = new object(); + + /// + /// Acknowledges this interaction with the . + /// + /// + /// A string that contains json to write back to the incoming http request. + /// + public override string Defer(bool ephemeral = false, RequestOptions options = null) + { + if (!InteractionHelper.CanSendResponse(this)) + throw new TimeoutException($"Cannot defer an interaction after {InteractionHelper.ResponseTimeLimit} seconds!"); + + var response = new API.InteractionResponse + { + Type = InteractionResponseType.DeferredChannelMessageWithSource, + Data = new API.InteractionCallbackData + { + Flags = ephemeral ? MessageFlags.Ephemeral : Optional.Unspecified + } + }; + + lock (_lock) + { + if (HasResponded) + { + throw new InvalidOperationException("Cannot respond or defer twice to the same interaction"); + } + } + + lock (_lock) + { + HasResponded = true; + } + + return SerializePayload(response); + } + + /// + /// Sends a followup message for this interaction. + /// + /// The text of the message to be sent. + /// A array of embeds to send with this response. Max 10. + /// if the message should be read out by a text-to-speech reader, otherwise . + /// if the response should be hidden to everyone besides the invoker of the command, otherwise . + /// The allowed mentions for this response. + /// The request options for this response. + /// A to be sent with this response. + /// A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored. + /// + /// The sent message. + /// + public override async Task FollowupAsync( + string text = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + MessageComponent component = null, + Embed embed = null, + RequestOptions options = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + embeds ??= Array.Empty(); + if (embed != null) + embeds = new[] { embed }.Concat(embeds).ToArray(); + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + + var args = new API.Rest.CreateWebhookMessageParams + { + Content = text, + AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, + IsTTS = isTTS, + Embeds = embeds.Select(x => x.ToModel()).ToArray(), + Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified + }; + + if (ephemeral) + args.Flags = MessageFlags.Ephemeral; + + return await InteractionHelper.SendFollowupAsync(Discord, args, Token, Channel, options); + } + + /// + /// Sends a followup message for this interaction. + /// + /// The text of the message to be sent. + /// The file to upload. + /// The file name of the attachment. + /// A array of embeds to send with this response. Max 10. + /// if the message should be read out by a text-to-speech reader, otherwise . + /// if the response should be hidden to everyone besides the invoker of the command, otherwise . + /// The allowed mentions for this response. + /// The request options for this response. + /// A to be sent with this response. + /// A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored. + /// + /// The sent message. + /// + public override async Task FollowupWithFileAsync( + Stream fileStream, + string fileName, + string text = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + MessageComponent component = null, + Embed embed = null, + RequestOptions options = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + embeds ??= Array.Empty(); + if (embed != null) + embeds = new[] { embed }.Concat(embeds).ToArray(); + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + Preconditions.NotNull(fileStream, nameof(fileStream), "File Stream must have data"); + Preconditions.NotNullOrEmpty(fileName, nameof(fileName), "File Name must not be empty or null"); + + var args = new API.Rest.CreateWebhookMessageParams + { + Content = text, + AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, + IsTTS = isTTS, + Embeds = embeds.Select(x => x.ToModel()).ToArray(), + Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + File = fileStream is not null ? new MultipartFile(fileStream, fileName) : Optional.Unspecified + }; + + if (ephemeral) + args.Flags = MessageFlags.Ephemeral; + + return await InteractionHelper.SendFollowupAsync(Discord, args, Token, Channel, options); + } + + /// + /// Sends a followup message for this interaction. + /// + /// The text of the message to be sent. + /// The file to upload. + /// The file name of the attachment. + /// A array of embeds to send with this response. Max 10. + /// if the message should be read out by a text-to-speech reader, otherwise . + /// if the response should be hidden to everyone besides the invoker of the command, otherwise . + /// The allowed mentions for this response. + /// The request options for this response. + /// A to be sent with this response. + /// A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored. + /// + /// The sent message. + /// + public override async Task FollowupWithFileAsync( + string filePath, + string text = null, + string fileName = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + MessageComponent component = null, + Embed embed = null, + RequestOptions options = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + embeds ??= Array.Empty(); + if (embed != null) + embeds = new[] { embed }.Concat(embeds).ToArray(); + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + Preconditions.NotNullOrEmpty(filePath, nameof(filePath), "Path must exist"); + + fileName ??= Path.GetFileName(filePath); + Preconditions.NotNullOrEmpty(fileName, nameof(fileName), "File Name must not be empty or null"); + + var args = new API.Rest.CreateWebhookMessageParams + { + Content = text, + AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, + IsTTS = isTTS, + Embeds = embeds.Select(x => x.ToModel()).ToArray(), + Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + File = !string.IsNullOrEmpty(filePath) ? new MultipartFile(new MemoryStream(File.ReadAllBytes(filePath), false), fileName) : Optional.Unspecified + }; + + if (ephemeral) + args.Flags = MessageFlags.Ephemeral; + + return await InteractionHelper.SendFollowupAsync(Discord, args, Token, Channel, options); + } + + /// + /// Responds to an Interaction with type . + /// + /// The text of the message to be sent. + /// A array of embeds to send with this response. Max 10. + /// if the message should be read out by a text-to-speech reader, otherwise . + /// if the response should be hidden to everyone besides the invoker of the command, otherwise . + /// The allowed mentions for this response. + /// The request options for this response. + /// A to be sent with this response. + /// A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored. + /// Message content is too long, length must be less or equal to . + /// The parameters provided were invalid or the token was invalid. + /// + /// A string that contains json to write back to the incoming http request. + /// + public override string Respond( + string text = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + MessageComponent component = null, + Embed embed = null, + RequestOptions options = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + if (!InteractionHelper.CanSendResponse(this)) + throw new TimeoutException($"Cannot respond to an interaction after {InteractionHelper.ResponseTimeLimit} seconds!"); + + embeds ??= Array.Empty(); + if (embed != null) + embeds = new[] { embed }.Concat(embeds).ToArray(); + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + + // check that user flag and user Id list are exclusive, same with role flag and role Id list + if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) + { + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) && + allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + { + throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions)); + } + + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) && + allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + { + throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions)); + } + } + + var response = new API.InteractionResponse + { + Type = InteractionResponseType.ChannelMessageWithSource, + Data = new API.InteractionCallbackData + { + Content = text, + AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, + Embeds = embeds.Select(x => x.ToModel()).ToArray(), + TTS = isTTS, + Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + Flags = ephemeral ? MessageFlags.Ephemeral : Optional.Unspecified + } + }; + + lock (_lock) + { + if (HasResponded) + { + throw new InvalidOperationException("Cannot respond twice to the same interaction"); + } + } + + lock (_lock) + { + HasResponded = true; + } + + return SerializePayload(response); + } + + /// + public override async Task FollowupWithFilesAsync( + IEnumerable attachments, + string text = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + MessageComponent components = null, + Embed embed = null, + RequestOptions options = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + embeds ??= Array.Empty(); + if (embed != null) + embeds = new[] { embed }.Concat(embeds).ToArray(); + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + + foreach (var attachment in attachments) + { + Preconditions.NotNullOrEmpty(attachment.FileName, nameof(attachment.FileName), "File Name must not be empty or null"); + } + + // check that user flag and user Id list are exclusive, same with role flag and role Id list + if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) + { + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) && + allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + { + throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions)); + } + + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) && + allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + { + throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions)); + } + } + + var flags = MessageFlags.None; + + if (ephemeral) + flags |= MessageFlags.Ephemeral; + + var args = new API.Rest.UploadWebhookFileParams(attachments.ToArray()) { Flags = flags, Content = text, IsTTS = isTTS, Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, MessageComponents = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified }; + return await InteractionHelper.SendFollowupAsync(Discord, args, Token, Channel, options).ConfigureAwait(false); + } + + /// + public override Task FollowupWithFileAsync( + FileAttachment attachment, + string text = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + MessageComponent components = null, + Embed embed = null, + RequestOptions options = null) + { + return FollowupWithFilesAsync(new FileAttachment[] { attachment }, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options); + } + + /// + public override string RespondWithModal(Modal modal, RequestOptions requestOptions = null) + => throw new NotSupportedException("Modal interactions cannot have modal responces!"); + + public new RestModalData Data { get; set; } + + IModalInteractionData IModalInteraction.Data => Data; + } +} diff --git a/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModalData.cs b/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModalData.cs new file mode 100644 index 000000000..22460ae51 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModalData.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Linq; +using System; +using Model = Discord.API.ModalInteractionData; +using InterationModel = Discord.API.Interaction; +using DataModel = Discord.API.MessageComponentInteractionData; + +namespace Discord.Rest +{ + /// + /// Represents data sent from a Interaction. + /// + public class RestModalData : IComponentInteractionData, IModalInteractionData + { + /// + public string CustomId { get; } + + /// + /// Represents the s components submitted by the user. + /// + public IReadOnlyCollection Components { get; } + + /// + public ComponentType Type => ComponentType.ModalSubmit; + + /// + public IReadOnlyCollection Values + => throw new NotSupportedException("Modal interactions do not have values!"); + + /// + public string Value + => throw new NotSupportedException("Modal interactions do not have value!"); + + IReadOnlyCollection IModalInteractionData.Components => Components; + + internal RestModalData(Model model) + { + CustomId = model.CustomId; + Components = model.Components + .SelectMany(x => x.Components) + .Select(x => new RestMessageComponentData(x)) + .ToArray(); + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs b/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs index 566d60d14..5894ee264 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs @@ -100,6 +100,9 @@ namespace Discord.Rest if (model.Type == InteractionType.ApplicationCommandAutocomplete) return await RestAutocompleteInteraction.CreateAsync(client, model).ConfigureAwait(false); + if (model.Type == InteractionType.ModalSubmit) + return await RestModal.CreateAsync(client, model).ConfigureAwait(false); + return null; } @@ -180,6 +183,9 @@ namespace Discord.Rest var model = await InteractionHelper.ModifyInteractionResponseAsync(Discord, Token, func, options); return RestInteractionMessage.Create(Discord, model, Token, Channel); } + /// + public abstract string RespondWithModal(Modal modal, RequestOptions options = null); + /// public abstract string Respond(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null); @@ -294,6 +300,9 @@ namespace Discord.Rest Task IDiscordInteraction.DeferAsync(bool ephemeral, RequestOptions options) => Task.FromResult(Defer(ephemeral, options)); /// + Task IDiscordInteraction.RespondWithModalAsync(Modal modal, RequestOptions options) + => Task.FromResult(RespondWithModal(modal, options)); + /// async Task IDiscordInteraction.FollowupAsync(string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options) => await FollowupAsync(text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false); diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestPingInteraction.cs b/src/Discord.Net.Rest/Entities/Interactions/RestPingInteraction.cs index 71d5a588c..bd15bc2d3 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestPingInteraction.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestPingInteraction.cs @@ -36,6 +36,7 @@ namespace Discord.Rest } public override string Defer(bool ephemeral = false, RequestOptions options = null) => throw new NotSupportedException(); + public override string RespondWithModal(Modal modal, RequestOptions options = null) => throw new NotSupportedException(); public override string Respond(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null) => throw new NotSupportedException(); public override Task FollowupAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null) => throw new NotSupportedException(); public override Task FollowupWithFileAsync(Stream fileStream, string fileName, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null) => throw new NotSupportedException(); diff --git a/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestAutocompleteInteraction.cs b/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestAutocompleteInteraction.cs index 44d0dc6ff..24dbae37a 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestAutocompleteInteraction.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestAutocompleteInteraction.cs @@ -112,7 +112,8 @@ namespace Discord.Rest => throw new NotSupportedException("Autocomplete interactions don't support this method!"); public override Task FollowupWithFilesAsync(IEnumerable attachments, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null) => throw new NotSupportedException("Autocomplete interactions don't support this method!"); - + public override string RespondWithModal(Modal modal, RequestOptions options = null) + => throw new NotSupportedException("Autocomplete interactions don't support this method!"); //IAutocompleteInteraction /// diff --git a/src/Discord.Net.Rest/Net/Converters/InteractionConverter.cs b/src/Discord.Net.Rest/Net/Converters/InteractionConverter.cs index f7235841d..4c4e3444d 100644 --- a/src/Discord.Net.Rest/Net/Converters/InteractionConverter.cs +++ b/src/Discord.Net.Rest/Net/Converters/InteractionConverter.cs @@ -56,6 +56,13 @@ namespace Discord.Net.Converters interaction.Data = autocompleteData; } break; + case InteractionType.ModalSubmit: + { + var modalData = new API.ModalInteractionData(); + serializer.Populate(result.CreateReader(), modalData); + interaction.Data = modalData; + } + break; } } else diff --git a/src/Discord.Net.Rest/Net/Converters/MessageComponentConverter.cs b/src/Discord.Net.Rest/Net/Converters/MessageComponentConverter.cs index 0bf11a369..36542d83b 100644 --- a/src/Discord.Net.Rest/Net/Converters/MessageComponentConverter.cs +++ b/src/Discord.Net.Rest/Net/Converters/MessageComponentConverter.cs @@ -32,6 +32,9 @@ namespace Discord.Net.Converters case ComponentType.SelectMenu: messageComponent = new API.SelectMenuComponent(); break; + case ComponentType.TextInput: + messageComponent = new API.TextInputComponent(); + break; } serializer.Populate(jsonObject.CreateReader(), messageComponent); return messageComponent; diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs index 29e13a2a1..134f8136b 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs @@ -634,6 +634,15 @@ namespace Discord.WebSocket remove => _autocompleteExecuted.Remove(value); } internal readonly AsyncEvent> _autocompleteExecuted = new AsyncEvent>(); + /// + /// Fired when a modal is submitted. + /// + public event Func ModalSubmitted + { + add => _modalSubmitted.Add(value); + remove => _modalSubmitted.Remove(value); + } + internal readonly AsyncEvent> _modalSubmitted = new AsyncEvent>(); /// /// Fired when a guild application command is created. diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index e573a2593..51c6d3c34 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -468,6 +468,7 @@ namespace Discord.WebSocket client.UserCommandExecuted += (arg) => _userCommandExecuted.InvokeAsync(arg); client.MessageCommandExecuted += (arg) => _messageCommandExecuted.InvokeAsync(arg); client.AutocompleteExecuted += (arg) => _autocompleteExecuted.InvokeAsync(arg); + client.ModalSubmitted += (arg) => _modalSubmitted.InvokeAsync(arg); client.ThreadUpdated += (thread1, thread2) => _threadUpdated.InvokeAsync(thread1, thread2); client.ThreadCreated += (thread) => _threadCreated.InvokeAsync(thread); diff --git a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs index cad6e5daa..21594fed7 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs @@ -78,7 +78,7 @@ namespace Discord.API if (msg != null) { #if DEBUG_PACKETS - Console.WriteLine($"<- {(GatewayOpCode)msg.Operation} [{msg.Type ?? "none"}] : {(msg.Payload as Newtonsoft.Json.Linq.JToken)?.ToString().Length}"); + Console.WriteLine($"<- {(GatewayOpCode)msg.Operation} [{msg.Type ?? "none"}] : {(msg.Payload as Newtonsoft.Json.Linq.JToken)}"); #endif await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); @@ -95,7 +95,7 @@ namespace Discord.API if (msg != null) { #if DEBUG_PACKETS - Console.WriteLine($"<- {(GatewayOpCode)msg.Operation} [{msg.Type ?? "none"}] : {(msg.Payload as Newtonsoft.Json.Linq.JToken)?.ToString().Length}"); + Console.WriteLine($"<- {(GatewayOpCode)msg.Operation} [{msg.Type ?? "none"}] : {(msg.Payload as Newtonsoft.Json.Linq.JToken)}"); #endif await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index dab07d3e2..e7f9b10ee 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -2274,6 +2274,9 @@ namespace Discord.WebSocket case SocketAutocompleteInteraction autocomplete: await TimedInvokeAsync(_autocompleteExecuted, nameof(AutocompleteExecuted), autocomplete).ConfigureAwait(false); break; + case SocketModal modal: + await TimedInvokeAsync(_modalSubmitted, nameof(ModalSubmitted), modal).ConfigureAwait(false); + break; } } break; diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs index 862c792a8..17a5e0209 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs @@ -438,6 +438,41 @@ namespace Discord.WebSocket HasResponded = true; } + /// + public override async Task RespondWithModalAsync(Modal modal, RequestOptions options = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + if (!InteractionHelper.CanSendResponse(this)) + throw new TimeoutException($"Cannot respond to an interaction after {InteractionHelper.ResponseTimeLimit} seconds!"); + + var response = new API.InteractionResponse + { + Type = InteractionResponseType.Modal, + Data = new API.InteractionCallbackData + { + CustomId = modal.CustomId, + Title = modal.Title, + Components = modal.Component.Components.Select(x => new Discord.API.ActionRowComponent(x)).ToArray() + } + }; + + lock (_lock) + { + if (HasResponded) + { + throw new InvalidOperationException("Cannot respond twice to the same interaction"); + } + } + + await InteractionHelper.SendInteractionResponseAsync(Discord, response, this, Channel, options).ConfigureAwait(false); + + lock (_lock) + { + HasResponded = true; + } + } //IComponentInteraction /// IComponentInteractionData IComponentInteraction.Data => Data; diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponentData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponentData.cs index 71e1d0395..c7f6c5106 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponentData.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponentData.cs @@ -23,11 +23,31 @@ namespace Discord.WebSocket /// public IReadOnlyCollection Values { get; } + /// + /// Gets the value of a interaction response. + /// + public string Value { get; } + internal SocketMessageComponentData(Model model) { CustomId = model.CustomId; Type = model.ComponentType; Values = model.Values.GetValueOrDefault(); + Value = model.Value.GetValueOrDefault(); + } + + internal SocketMessageComponentData(IMessageComponent component) + { + CustomId = component.CustomId; + Type = component.Type; + + Value = component.Type == ComponentType.TextInput + ? (component as API.TextInputComponent).Value.Value + : null; + + Values = component.Type == ComponentType.SelectMenu + ? (component as API.SelectMenuComponent).Values.Value + : null; } } } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModal.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModal.cs new file mode 100644 index 000000000..197882dae --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModal.cs @@ -0,0 +1,302 @@ +using Discord.Net.Rest; +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using DataModel = Discord.API.ModalInteractionData; +using ModelBase = Discord.API.Interaction; + +namespace Discord.WebSocket +{ + /// + /// Represents a user submitted received via GateWay. + /// + public class SocketModal : SocketInteraction, IDiscordInteraction, IModalInteraction + { + /// + /// The data for this interaction. + /// + /// + public new SocketModalData Data { get; set; } + + internal SocketModal(DiscordSocketClient client, ModelBase model, ISocketMessageChannel channel) + : base(client, model.Id, channel) + { + var dataModel = model.Data.IsSpecified + ? (DataModel)model.Data.Value + : null; + + Data = new SocketModalData(dataModel); + } + + internal new static SocketModal Create(DiscordSocketClient client, ModelBase model, ISocketMessageChannel channel) + { + var entity = new SocketModal(client, model, channel); + entity.Update(model); + return entity; + } + + /// + public override bool HasResponded { get; internal set; } + private object _lock = new object(); + + /// + public override async Task RespondWithFilesAsync( + IEnumerable attachments, + string text = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + MessageComponent components = null, + Embed embed = null, + RequestOptions options = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + if (!InteractionHelper.CanSendResponse(this)) + throw new TimeoutException($"Cannot respond to an interaction after {InteractionHelper.ResponseTimeLimit} seconds!"); + + embeds ??= Array.Empty(); + if (embed != null) + embeds = new[] { embed }.Concat(embeds).ToArray(); + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + + // check that user flag and user Id list are exclusive, same with role flag and role Id list + if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) + { + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) && + allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + { + throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions)); + } + + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) && + allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + { + throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions)); + } + } + + var response = new API.Rest.UploadInteractionFileParams(attachments?.ToArray()) + { + Type = InteractionResponseType.ChannelMessageWithSource, + Content = text ?? Optional.Unspecified, + AllowedMentions = allowedMentions != null ? allowedMentions?.ToModel() : Optional.Unspecified, + Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, + IsTTS = isTTS, + MessageComponents = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + Flags = ephemeral ? MessageFlags.Ephemeral : Optional.Unspecified + }; + + lock (_lock) + { + if (HasResponded) + { + throw new InvalidOperationException("Cannot respond, update, or defer the same interaction twice"); + } + } + + await InteractionHelper.SendInteractionResponseAsync(Discord, response, this, Channel, options).ConfigureAwait(false); + HasResponded = true; + } + + /// + public override async Task RespondAsync( + string text = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + MessageComponent components = null, + Embed embed = null, + RequestOptions options = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + if (!InteractionHelper.CanSendResponse(this)) + throw new TimeoutException($"Cannot respond to an interaction after {InteractionHelper.ResponseTimeLimit} seconds!"); + + embeds ??= Array.Empty(); + if (embed != null) + embeds = new[] { embed }.Concat(embeds).ToArray(); + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + + // check that user flag and user Id list are exclusive, same with role flag and role Id list + if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) + { + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) && + allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + { + throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions)); + } + + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) && + allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + { + throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions)); + } + } + + var response = new API.InteractionResponse + { + Type = InteractionResponseType.ChannelMessageWithSource, + Data = new API.InteractionCallbackData + { + Content = text ?? Optional.Unspecified, + AllowedMentions = allowedMentions?.ToModel(), + Embeds = embeds.Select(x => x.ToModel()).ToArray(), + TTS = isTTS, + Flags = ephemeral ? MessageFlags.Ephemeral : Optional.Unspecified, + Components = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified + } + }; + + lock (_lock) + { + if (HasResponded) + { + throw new InvalidOperationException("Cannot respond, update, or defer twice to the same interaction"); + } + } + + await InteractionHelper.SendInteractionResponseAsync(Discord, response, this, Channel, options).ConfigureAwait(false); + HasResponded = true; + } + + /// + public override async Task FollowupAsync( + string text = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + MessageComponent components = null, + Embed embed = null, + RequestOptions options = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + embeds ??= Array.Empty(); + if (embed != null) + embeds = new[] { embed }.Concat(embeds).ToArray(); + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + + var args = new API.Rest.CreateWebhookMessageParams + { + Content = text, + AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, + IsTTS = isTTS, + Embeds = embeds.Select(x => x.ToModel()).ToArray(), + Components = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified + }; + + if (ephemeral) + args.Flags = MessageFlags.Ephemeral; + + return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options); + } + + /// + public override async Task FollowupWithFilesAsync( + IEnumerable attachments, + string text = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + MessageComponent components = null, + Embed embed = null, + RequestOptions options = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + embeds ??= Array.Empty(); + if (embed != null) + embeds = new[] { embed }.Concat(embeds).ToArray(); + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + + foreach (var attachment in attachments) + { + Preconditions.NotNullOrEmpty(attachment.FileName, nameof(attachment.FileName), "File Name must not be empty or null"); + } + + // check that user flag and user Id list are exclusive, same with role flag and role Id list + if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) + { + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) && + allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + { + throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions)); + } + + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) && + allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + { + throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions)); + } + } + + var flags = MessageFlags.None; + + if (ephemeral) + flags |= MessageFlags.Ephemeral; + + var args = new API.Rest.UploadWebhookFileParams(attachments.ToArray()) { Flags = flags, Content = text, IsTTS = isTTS, Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, MessageComponents = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified }; + return await InteractionHelper.SendFollowupAsync(Discord, args, Token, Channel, options).ConfigureAwait(false); + } + + /// + public override async Task DeferAsync(bool ephemeral = false, RequestOptions options = null) + { + if (!InteractionHelper.CanSendResponse(this)) + throw new TimeoutException($"Cannot defer an interaction after {InteractionHelper.ResponseTimeLimit} seconds of no response/acknowledgement"); + + var response = new API.InteractionResponse + { + Type = InteractionResponseType.DeferredUpdateMessage, + Data = ephemeral ? new API.InteractionCallbackData { Flags = MessageFlags.Ephemeral } : Optional.Unspecified + }; + + lock (_lock) + { + if (HasResponded) + { + throw new InvalidOperationException("Cannot respond or defer twice to the same interaction"); + } + } + + await Discord.Rest.ApiClient.CreateInteractionResponseAsync(response, Id, Token, options).ConfigureAwait(false); + + lock (_lock) + { + HasResponded = true; + } + } + + /// + public override Task RespondWithModalAsync(Modal modal, RequestOptions options = null) + => throw new NotSupportedException("You cannot respond to a modal with a modal!"); + + IModalInteractionData IModalInteraction.Data => Data; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModalData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModalData.cs new file mode 100644 index 000000000..df8be2fe8 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModalData.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Linq; +using System; +using Model = Discord.API.ModalInteractionData; +using InterationModel = Discord.API.Interaction; +using DataModel = Discord.API.MessageComponentInteractionData; + +namespace Discord.WebSocket +{ + /// + /// Represents data sent from a . + /// + public class SocketModalData : IDiscordInteractionData, IModalInteractionData + { + /// + /// Gets the 's Custom Id. + /// + public string CustomId { get; } + + /// + /// Gets the 's components submitted by the user. + /// + public IReadOnlyCollection Components { get; } + + internal SocketModalData(Model model) + { + CustomId = model.CustomId; + Components = model.Components + .SelectMany(x => x.Components) + .Select(x => new SocketMessageComponentData(x)) + .ToArray(); + } + + IReadOnlyCollection IModalInteractionData.Components => Components; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketAutocompleteInteraction.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketAutocompleteInteraction.cs index 6058bdafd..d4cdc9cc1 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketAutocompleteInteraction.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketAutocompleteInteraction.cs @@ -100,6 +100,10 @@ namespace Discord.WebSocket public override Task RespondWithFilesAsync(IEnumerable attachments, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null) => throw new NotSupportedException("Autocomplete interactions don't support this method!"); + /// + public override Task RespondWithModalAsync(Modal modal, RequestOptions requestOptions = null) + => throw new NotSupportedException("Autocomplete interactions cannot have normal responces!"); + //IAutocompleteInteraction /// IAutocompleteInteractionData IAutocompleteInteraction.Data => Data; diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs index 330d6d7a4..bc3ece20c 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs @@ -1,4 +1,3 @@ -using Discord.Net.Rest; using Discord.Rest; using System; using System.Collections.Generic; @@ -135,6 +134,42 @@ namespace Discord.WebSocket HasResponded = true; } + /// + public override async Task RespondWithModalAsync(Modal modal, RequestOptions options = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + if (!InteractionHelper.CanSendResponse(this)) + throw new TimeoutException($"Cannot respond to an interaction after {InteractionHelper.ResponseTimeLimit} seconds!"); + + var response = new API.InteractionResponse + { + Type = InteractionResponseType.Modal, + Data = new API.InteractionCallbackData + { + CustomId = modal.CustomId, + Title = modal.Title, + Components = modal.Component.Components.Select(x => new Discord.API.ActionRowComponent(x)).ToArray() + } + }; + + lock (_lock) + { + if (HasResponded) + { + throw new InvalidOperationException("Cannot respond twice to the same interaction"); + } + } + + await InteractionHelper.SendInteractionResponseAsync(Discord, response, this, Channel, options).ConfigureAwait(false); + + lock (_lock) + { + HasResponded = true; + } + } + public override async Task RespondWithFilesAsync( IEnumerable attachments, string text = null, diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs index 985e8e0d9..1c3563ab0 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs @@ -108,6 +108,9 @@ namespace Discord.WebSocket if (model.Type == InteractionType.ApplicationCommandAutocomplete) return SocketAutocompleteInteraction.Create(client, model, channel); + if (model.Type == InteractionType.ModalSubmit) + return SocketModal.Create(client, model, channel); + return null; } @@ -387,6 +390,13 @@ namespace Discord.WebSocket /// public abstract Task DeferAsync(bool ephemeral = false, RequestOptions options = null); + /// + /// Responds to this interaction with a . + /// + /// The to respond with. + /// The request options for this request. + /// A task that represents the asynchronous operation of responding to the interaction. + public abstract Task RespondWithModalAsync(Modal modal, RequestOptions options = null); #endregion #region IDiscordInteraction