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 .../int_basics/message-components/text-input.md | 46 +++ docs/guides/int_basics/modals/images/image1.png | Bin 0 -> 36021 bytes docs/guides/int_basics/modals/images/image2.png | Bin 0 -> 25568 bytes docs/guides/int_basics/modals/images/image3.png | Bin 0 -> 22409 bytes docs/guides/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 + docs/guides/int_framework/samples/intro/modal.cs | 36 ++ docs/guides/toc.yml | 6 + .../Entities/Interactions/IDiscordInteraction.cs | 8 + .../Interactions/InteractionResponseType.cs | 7 +- .../Entities/Interactions/InteractionType.cs | 7 +- .../MessageComponents/ComponentBuilder.cs | 249 +++++++++++++ .../MessageComponents/ComponentType.cs | 12 +- .../MessageComponents/IComponentInteractionData.cs | 7 +- .../MessageComponents/TextInputComponent.cs | 62 ++++ .../MessageComponents/TextInputStyle.cs | 14 + .../Interactions/Modals/IModalInteraction.cs | 13 + .../Interactions/Modals/IModalInteractionData.cs | 20 + .../Entities/Interactions/Modals/Modal.cs | 37 ++ .../Entities/Interactions/Modals/ModalBuilder.cs | 268 ++++++++++++++ .../Entities/Interactions/Modals/ModalComponent.cs | 20 + .../Commands/ModalInteractionAttribute.cs | 44 +++ .../Attributes/Modals/InputLabelAttribute.cs | 25 ++ .../Attributes/Modals/ModalInputAttribute.cs | 32 ++ .../Attributes/Modals/ModalTextInputAttribute.cs | 55 +++ .../Attributes/Modals/RequiredInputAttribute.cs | 25 ++ .../Builders/Commands/ModalCommandBuilder.cs | 44 +++ .../Modals/Inputs/IInputComponentBuilder.cs | 105 ++++++ .../Modals/Inputs/InputComponentBuilder.cs | 164 +++++++++ .../Modals/Inputs/TextInputComponentBuilder.cs | 109 ++++++ .../Builders/Modals/ModalBuilder.cs | 81 +++++ .../Builders/ModuleBuilder.cs | 24 +- .../Builders/ModuleClassBuilder.cs | 143 +++++++- .../Parameters/ModalCommandParameterBuilder.cs | 45 +++ src/Discord.Net.Interactions/Entities/IModal.cs | 13 + .../Extensions/IDiscordInteractionExtensions.cs | 37 ++ .../Info/Commands/ComponentCommandInfo.cs | 2 +- .../Info/Commands/ModalCommandInfo.cs | 81 +++++ .../Info/InputComponents/InputComponentInfo.cs | 64 ++++ .../Info/InputComponents/TextInputComponentInfo.cs | 42 +++ src/Discord.Net.Interactions/Info/ModalInfo.cs | 90 +++++ src/Discord.Net.Interactions/Info/ModuleInfo.cs | 13 + .../Info/Parameters/ModalCommandParameterInfo.cs | 28 ++ .../InteractionModuleBase.cs | 7 + src/Discord.Net.Interactions/InteractionService.cs | 60 ++- .../InteractionServiceConfig.cs | 8 + .../Utilities/ModalUtils.cs | 51 +++ .../Utilities/ReflectionUtils.cs | 72 +++- .../API/Common/ActionRowComponent.cs | 1 + .../API/Common/InteractionCallbackData.cs | 6 + .../API/Common/MessageComponentInteractionData.cs | 3 + .../API/Common/ModalInteractionData.cs | 13 + .../API/Common/SelectMenuComponent.cs | 2 + .../API/Common/TextInputComponent.cs | 49 +++ .../Interactions/CommandBase/RestCommandBase.cs | 40 ++ .../MessageComponents/RestMessageComponent.cs | 40 ++ .../MessageComponents/RestMessageComponentData.cs | 15 + .../Entities/Interactions/Modals/RestModal.cs | 402 +++++++++++++++++++++ .../Entities/Interactions/Modals/RestModalData.cs | 45 +++ .../Entities/Interactions/RestInteraction.cs | 9 + .../Entities/Interactions/RestPingInteraction.cs | 1 + .../SlashCommands/RestAutocompleteInteraction.cs | 3 +- .../Net/Converters/InteractionConverter.cs | 7 + .../Net/Converters/MessageComponentConverter.cs | 3 + .../BaseSocketClient.Events.cs | 9 + src/Discord.Net.WebSocket/DiscordShardedClient.cs | 1 + .../DiscordSocketApiClient.cs | 4 +- src/Discord.Net.WebSocket/DiscordSocketClient.cs | 3 + .../MessageComponents/SocketMessageComponent.cs | 35 ++ .../SocketMessageComponentData.cs | 20 + .../Entities/Interaction/Modals/SocketModal.cs | 302 ++++++++++++++++ .../Entities/Interaction/Modals/SocketModalData.cs | 36 ++ .../SlashCommands/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 KcmV+b0RR6000031 diff --git a/docs/guides/int_basics/message-components/images/image9.png b/docs/guides/int_basics/message-components/images/image9.png new file mode 100644 index 0000000000000000000000000000000000000000..6a9850fb3a60645116393de4c1fc31b68423ee84 GIT binary patch literal 10913 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 [!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 zcmV*lj7oEfP) zaB^>EX>4U6ba`-PAZ2)IW&i+q+Fg#z4Z|=DL+_fRBj{&J9w!JAARBc2%CXU)eFupY z1cG$DuajN_VJB~q=l~bqz3WyYd1B0>EMwQ4tZhQ z*tOF2FuSK1)BQEVMwR{qZMl^F-SO=Wpp57WoKz_JtFVBzak)MWIZBRHBB&0a6@HiX-Il< zWm9@iRAYK;V>x6ZKRyaPcsh7(aBXFDVIX*IY;JQpcs(LvWN%_+I&O11ZDn*}FCrj# zI(ThxbZb2#SWQqMLvM0rAT%yCE-)=ccxiSrB0dTrJaS}aI#NVNAb4$TZgVgOb#y%V>lA~Z5IA_^cNAZ2)I zW;#%5cx7x^L}_hhZgXjGZapG2GBP3xARr)gX=Y|RNo`?gWmjorbZ9*yG%_?I3LqdL zbZKU0I!SF|XJtrbX=iA3Jt8zRG9n5fARu&UW@b81a%p95bYXO9Z*DyzF(NNM3Lrc$ za%5&YQba~R3Op}(I(ThxZDn*}J_;ZpARr(hARr(hARr(hARr(hARr(hARr(hARr(h zARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(h zARr(hARr(hARr(hARr(hARr1LARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(h zARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(h zARr(hARr(hARr(hARr(LARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(h zARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(h zARr(hARr(hARr(h3LqdLARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(h zARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(h zARr(hARr(hAPOKLARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(h zARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(h zARr(hARr1LARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(h zARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(h zARr(LARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(h zARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(h z3LqdLARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(h zARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hAPOKL zARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(h zARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr1LARr(h zARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(h zARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(LARr(hARr(h zARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(h zARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(h3LqdLARr(hARr(h zARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(h zARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hAPOKLARr(hARr(hARr(h zARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(h zARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr1LARr(hARr(hARr(hARr(h zARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(h zARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(LARr(hARr(hARr(hARr(hARr(h zARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(h zARr(hARr(hARr(hARr(hARr(hARr(hARr(h3LqdLARr(hARr(hARr(hARr(hARr(h zARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(h zARr(hARr(hARr(hARr(hARr(hARr(hAPOKLARr(hARr(hARr(hARr(hARr(hARr(h zARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(h zARr(hARr(hARr(hARr(hARr(hARr1LARr(hARr(hARr(hARr(hARr(hARr(hARr(h zARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(h zARr(hARr(hARr(hARr(hARr(LARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(h zARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(h zARr(hARr(hARr(hARr(h3LqdLARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(h zARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(hARr(h zARr(hARr(hARr(hAPOKLARr(hARr(hARr(hARr(hARr(hARr(hAPPJ`cyM83Yh`pG zWo~3WB6lJ`KKF}XivR!s1am@3R0s$N2z&@+hyVZ}07*naRCwC#y?K08^}YZ9JagvE znwiN=_LV>agg_9&s^G4m6x-Tbti@J(uXeGE{od-m^|tnRYum4FuU)ja_E!6Cu~%wq zZ%b=&!39teSrZ@-5|SbNLNZxqCbOP7bAEr!5P}3mM8x`eJ|1}_ndL0+^ZC4A=l%Y? z&*vjuGnj+ zVnz~1C-p6z?A?EeWH?H7egPYQ_c%EQoohaH1Dp2jnQ$ACY&+hI3OO<(kG^6TRwS(c z$Z7Yh%My})>i^#2v)b<4wV^J7ZaB9#n{YqIaz6gCV!9jpiJj(i?d9`u1_qGy{vZE>Mts!sC|T*SC3G2pbyn7pLW0T3^RuoPa6B* zU+U)Q^xSiPevUwt1ur%+cWW!TeL)V+DI}6*duI;YU|`5$PB}=kk*Fn=A@+2PU>_Jg zJ8?P-8+Kh(L?}1oJhT!s=8V%y*q~bka7ur)D$NJBXvl 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 AJOBUy literal 0 KcmV+b0RR6000031 diff --git a/docs/guides/int_basics/modals/images/image2.png b/docs/guides/int_basics/modals/images/image2.png new file mode 100644 index 0000000000000000000000000000000000000000..7c1c325d3bb5f2653929fff29bc0aa13766a6a8a GIT binary patch literal 25568 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
sv&@)ZCqzH|XYQ&Tv0 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 literal 0 KcmV+b0RR6000031 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; @@ -98,6 +106,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. /// /// The discord client. @@ -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; } @@ -181,6 +184,9 @@ namespace Discord.Rest 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