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>
@@ -8,13 +8,13 @@
|
|||||||
"editor.rulers": [
|
"editor.rulers": [
|
||||||
120
|
120
|
||||||
],
|
],
|
||||||
|
"editor.insertSpaces": true,
|
||||||
"files.exclude": {
|
"files.exclude": {
|
||||||
"**/.git": true,
|
"**/.git": true,
|
||||||
"**/.svn": true,
|
"**/.svn": true,
|
||||||
"**/.hg": true,
|
"**/.hg": true,
|
||||||
"**/CVS": true,
|
"**/CVS": true,
|
||||||
"**/.DS_Store": true,
|
"**/.DS_Store": true,
|
||||||
"docs/": true,
|
|
||||||
"**/obj": true,
|
"**/obj": true,
|
||||||
"**/bin": true,
|
"**/bin": true,
|
||||||
"samples/": true,
|
"samples/": true,
|
||||||
|
|||||||
BIN
docs/guides/int_basics/message-components/images/image7.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
docs/guides/int_basics/message-components/images/image8.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
docs/guides/int_basics/message-components/images/image9.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
46
docs/guides/int_basics/message-components/text-input.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
uid: Guides.MessageComponents.TextInputs
|
||||||
|
title: Text Input Components
|
||||||
|
---
|
||||||
|
|
||||||
|
# Text Input Components
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> Text input components can only be used in
|
||||||
|
> [modals](../modals/intro.md).
|
||||||
|
|
||||||
|
Text input components are a type of MessageComponents that can only be
|
||||||
|
used in modals. Texts inputs can be longer (the `Paragraph`) style or
|
||||||
|
shorter (the `Short` style). Text inputs have a variable min and max
|
||||||
|
length.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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.");
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
BIN
docs/guides/int_basics/modals/images/image1.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
docs/guides/int_basics/modals/images/image2.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
docs/guides/int_basics/modals/images/image3.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
docs/guides/int_basics/modals/images/image4.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
135
docs/guides/int_basics/modals/intro.md
Normal file
@@ -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.
|
||||||
|
|
||||||
|

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

|
||||||
|
|
||||||
|
### 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:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 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<SocketMessageComponentData> 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.
|
||||||
|
|
||||||
|

|
||||||
@@ -198,6 +198,18 @@ Autocomplete commands must be parameterless methods. A valid Autocomplete comman
|
|||||||
|
|
||||||
Alternatively, you can use the [AutocompleteHandlers] to simplify this workflow.
|
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
|
## Interaction Context
|
||||||
|
|
||||||
Every command module provides its commands with an execution context.
|
Every command module provides its commands with an execution context.
|
||||||
|
|||||||
36
docs/guides/int_framework/samples/intro/modal.cs
Normal file
@@ -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<FoodModal>("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);
|
||||||
|
}
|
||||||
@@ -91,8 +91,14 @@
|
|||||||
topicUid: Guides.MessageComponents.Buttons
|
topicUid: Guides.MessageComponents.Buttons
|
||||||
- name: Select menus
|
- name: Select menus
|
||||||
topicUid: Guides.MessageComponents.SelectMenus
|
topicUid: Guides.MessageComponents.SelectMenus
|
||||||
|
- name: Text Input
|
||||||
|
topicUid: Guides.MessageComponents.TextInputs
|
||||||
- name: Advanced Concepts
|
- name: Advanced Concepts
|
||||||
topicUid: Guides.MessageComponents.Advanced
|
topicUid: Guides.MessageComponents.Advanced
|
||||||
|
- name: Modal Basics
|
||||||
|
items:
|
||||||
|
- name: Introduction
|
||||||
|
topicUid: Guides.Modals.Intro
|
||||||
- name: Guild Events
|
- name: Guild Events
|
||||||
items:
|
items:
|
||||||
- name: Introduction
|
- name: Introduction
|
||||||
|
|||||||
@@ -332,5 +332,13 @@ namespace Discord
|
|||||||
/// A task that represents the asynchronous operation of deferring the interaction.
|
/// A task that represents the asynchronous operation of deferring the interaction.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
Task DeferAsync(bool ephemeral = false, RequestOptions options = null);
|
Task DeferAsync(bool ephemeral = false, RequestOptions options = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Responds to the interaction with a modal.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="modal">The modal to respond with.</param>
|
||||||
|
/// <param name="options">The request options for this <see langword="async"/> request.</param>
|
||||||
|
/// <returns>A task that represents the asynchronous operation of responding to the interaction.</returns>
|
||||||
|
Task RespondWithModalAsync(Modal modal, RequestOptions options = null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ namespace Discord
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Respond with a set of choices to a autocomplete interaction.
|
/// Respond with a set of choices to a autocomplete interaction.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
ApplicationCommandAutocompleteResult = 8
|
ApplicationCommandAutocompleteResult = 8,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Respond by showing the user a modal.
|
||||||
|
/// </summary>
|
||||||
|
Modal = 9,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ namespace Discord
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// An autocomplete request sent from discord.
|
/// An autocomplete request sent from discord.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
ApplicationCommandAutocomplete = 4
|
ApplicationCommandAutocomplete = 4,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A modal sent from discord.
|
||||||
|
/// </summary>
|
||||||
|
ModalSubmit = 5,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -276,6 +276,11 @@ namespace Discord
|
|||||||
/// <returns>A <see cref="MessageComponent"/> that can be sent with <see cref="IMessageChannel.SendMessageAsync"/>.</returns>
|
/// <returns>A <see cref="MessageComponent"/> that can be sent with <see cref="IMessageChannel.SendMessageAsync"/>.</returns>
|
||||||
public MessageComponent Build()
|
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
|
return _actionRows != null
|
||||||
? new MessageComponent(_actionRows.Select(x => x.Build()).ToList())
|
? new MessageComponent(_actionRows.Select(x => x.Build()).ToList())
|
||||||
: MessageComponent.Empty;
|
: MessageComponent.Empty;
|
||||||
@@ -1093,4 +1098,248 @@ namespace Discord
|
|||||||
return new SelectMenuOption(Label, Value, Description, Emote, IsDefault);
|
return new SelectMenuOption(Label, Value, Description, Emote, IsDefault);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class TextInputBuilder
|
||||||
|
{
|
||||||
|
public const int LargestMaxLength = 4000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the custom id of the current text input.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="ArgumentException" accessor="set"><see cref="CustomId"/> length exceeds <see cref="ComponentBuilder.MaxCustomIdLength"/></exception>
|
||||||
|
/// <exception cref="ArgumentException" accessor="set"><see cref="CustomId"/> length subceeds 1.</exception>
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the style of the current text input.
|
||||||
|
/// </summary>
|
||||||
|
public TextInputStyle Style { get; set; } = TextInputStyle.Short;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the label of the current text input.
|
||||||
|
/// </summary>
|
||||||
|
public string Label { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the placeholder of the current text input.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="ArgumentException"><see cref="Placeholder"/> is longer than 100 characters</exception>
|
||||||
|
public string Placeholder
|
||||||
|
{
|
||||||
|
get => _placeholder;
|
||||||
|
set => _placeholder = (value?.Length ?? 0) <= 100
|
||||||
|
? value
|
||||||
|
: throw new ArgumentException("Placeholder cannot have more than 100 characters.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the minimum length of the current text input.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="ArgumentOutOfRangeException"><see cref="MinLength"/> is less than 0.</exception>
|
||||||
|
/// <exception cref="ArgumentOutOfRangeException"><see cref="MinLength"/> is greater than <see cref="LargestMaxLength"/>.</exception>
|
||||||
|
/// <exception cref="ArgumentOutOfRangeException"><see cref="MinLength"/> is greater than <see cref="MaxLength"/>.</exception>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the maximum length of the current text input.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="ArgumentOutOfRangeException"><see cref="MaxLength"/> is less than 0.</exception>
|
||||||
|
/// <exception cref="ArgumentOutOfRangeException"><see cref="MaxLength"/> is greater than <see cref="LargestMaxLength"/>.</exception>
|
||||||
|
/// <exception cref="ArgumentOutOfRangeException"><see cref="MaxLength"/> is less than <see cref="MinLength"/>.</exception>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether the user is required to input text.
|
||||||
|
/// </summary>
|
||||||
|
public bool? Required { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the default value of the text input.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="ArgumentOutOfRangeException"><see cref="Value.Length"/> is less than 0.</exception>
|
||||||
|
/// <exception cref="ArgumentOutOfRangeException">
|
||||||
|
/// <see cref="Value.Length"/> is greater than <see cref="LargestMaxLength"/> or <see cref="MaxLength"/>.
|
||||||
|
/// </exception>
|
||||||
|
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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of a <see cref="TextInputBuilder"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="label">The text input's label.</param>
|
||||||
|
/// <param name="style">The text input's style.</param>
|
||||||
|
/// <param name="customId">The text input's custom id.</param>
|
||||||
|
/// <param name="placeholder">The text input's placeholder.</param>
|
||||||
|
/// <param name="minLength">The text input's minimum length.</param>
|
||||||
|
/// <param name="maxLength">The text input's maximum length.</param>
|
||||||
|
/// <param name="required">The text input's required value.</param>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of a <see cref="TextInputBuilder"/>.
|
||||||
|
/// </summary>
|
||||||
|
public TextInputBuilder()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the label of the current builder.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="label">The value to set.</param>
|
||||||
|
/// <returns>The current builder. </returns>
|
||||||
|
public TextInputBuilder WithLabel(string label)
|
||||||
|
{
|
||||||
|
Label = label;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the style of the current builder.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="style">The value to set.</param>
|
||||||
|
/// <returns>The current builder. </returns>
|
||||||
|
public TextInputBuilder WithStyle(TextInputStyle style)
|
||||||
|
{
|
||||||
|
Style = style;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the custom id of the current builder.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="customId">The value to set.</param>
|
||||||
|
/// <returns>The current builder. </returns>
|
||||||
|
public TextInputBuilder WithCustomId(string customId)
|
||||||
|
{
|
||||||
|
CustomId = customId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the placeholder of the current builder.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="placeholder">The value to set.</param>
|
||||||
|
/// <returns>The current builder. </returns>
|
||||||
|
public TextInputBuilder WithPlaceholder(string placeholder)
|
||||||
|
{
|
||||||
|
Placeholder = placeholder;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the value of the current builder.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The value to set</param>
|
||||||
|
/// <returns>The current builder.</returns>
|
||||||
|
public TextInputBuilder WithValue(string value)
|
||||||
|
{
|
||||||
|
Value = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the minimum length of the current builder.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="placeholder">The value to set.</param>
|
||||||
|
/// <returns>The current builder. </returns>
|
||||||
|
public TextInputBuilder WithMinLength(int minLength)
|
||||||
|
{
|
||||||
|
MinLength = minLength;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the maximum length of the current builder.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="placeholder">The value to set.</param>
|
||||||
|
/// <returns>The current builder. </returns>
|
||||||
|
public TextInputBuilder WithMaxLength(int maxLength)
|
||||||
|
{
|
||||||
|
MaxLength = maxLength;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the required value of the current builder.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="required">The value to set.</param>
|
||||||
|
/// <returns>The current builder. </returns>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,16 @@ namespace Discord
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A select menu for picking from choices.
|
/// A select menu for picking from choices.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
SelectMenu = 3
|
SelectMenu = 3,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A box for entering text.
|
||||||
|
/// </summary>
|
||||||
|
TextInput = 4,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An interaction sent when a model is submitted.
|
||||||
|
/// </summary>
|
||||||
|
ModalSubmit = 5,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace Discord
|
|||||||
public interface IComponentInteractionData : IDiscordInteractionData
|
public interface IComponentInteractionData : IDiscordInteractionData
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the components Custom Id that was clicked.
|
/// Gets the component's Custom Id that was clicked.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
string CustomId { get; }
|
string CustomId { get; }
|
||||||
|
|
||||||
@@ -21,5 +21,10 @@ namespace Discord
|
|||||||
/// Gets the value(s) of a <see cref="SelectMenuComponent"/> interaction response.
|
/// Gets the value(s) of a <see cref="SelectMenuComponent"/> interaction response.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IReadOnlyCollection<string> Values { get; }
|
IReadOnlyCollection<string> Values { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the value of a <see cref="TextInputComponent"/> interaction response.
|
||||||
|
/// </summary>
|
||||||
|
public string Value { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
namespace Discord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Respresents a <see cref="IMessageComponent"/> text input.
|
||||||
|
/// </summary>
|
||||||
|
public class TextInputComponent : IMessageComponent
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public ComponentType Type => ComponentType.TextInput;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string CustomId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the label of the component; this is the text shown above it.
|
||||||
|
/// </summary>
|
||||||
|
public string Label { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the placeholder of the component.
|
||||||
|
/// </summary>
|
||||||
|
public string Placeholder { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the minimum length of the inputted text.
|
||||||
|
/// </summary>
|
||||||
|
public int? MinLength { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the maximum length of the inputted text.
|
||||||
|
/// </summary>
|
||||||
|
public int? MaxLength { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the style of the component.
|
||||||
|
/// </summary>
|
||||||
|
public TextInputStyle Style { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether users are required to input text.
|
||||||
|
/// </summary>
|
||||||
|
public bool? Required { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the default value of the component.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace Discord
|
||||||
|
{
|
||||||
|
public enum TextInputStyle
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Intended for short, single-line text.
|
||||||
|
/// </summary>
|
||||||
|
Short = 1,
|
||||||
|
/// <summary>
|
||||||
|
/// Intended for longer or multiline text.
|
||||||
|
/// </summary>
|
||||||
|
Paragraph = 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace Discord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an interaction type for Modals.
|
||||||
|
/// </summary>
|
||||||
|
public interface IModalInteraction : IDiscordInteraction
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the data received with this interaction; contains the clicked button.
|
||||||
|
/// </summary>
|
||||||
|
new IModalInteractionData Data { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Discord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the data sent with the <see cref="IModalInteraction"/>.
|
||||||
|
/// </summary>
|
||||||
|
public interface IModalInteractionData : IDiscordInteractionData
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="Modal"/>'s Custom Id.
|
||||||
|
/// </summary>
|
||||||
|
string CustomId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="Modal"/> components submitted by the user.
|
||||||
|
/// </summary>
|
||||||
|
IReadOnlyCollection<IComponentInteractionData> Components { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/Discord.Net.Core/Entities/Interactions/Modals/Modal.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a modal interaction.
|
||||||
|
/// </summary>
|
||||||
|
public class Modal : IMessageComponent
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public ComponentType Type => ComponentType.ModalSubmit;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the title of the modal.
|
||||||
|
/// </summary>
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string CustomId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the components in the modal.
|
||||||
|
/// </summary>
|
||||||
|
public ModalComponent Component { get; set; }
|
||||||
|
|
||||||
|
internal Modal(string title, string customId, ModalComponent components)
|
||||||
|
{
|
||||||
|
Title = title;
|
||||||
|
CustomId = customId;
|
||||||
|
Component = components;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the components of the current modal.
|
||||||
|
/// </summary>
|
||||||
|
public ModalComponentBuilder Components { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the title of the current modal.
|
||||||
|
/// </summary>
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the custom id of the current modal.
|
||||||
|
/// </summary>
|
||||||
|
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() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of a <see cref="ModalBuilder"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="title">The modal's title.</param>
|
||||||
|
/// <param name="customId">The modal's customId.</param>
|
||||||
|
/// <param name="components">The modal's components.</param>
|
||||||
|
/// <exception cref="ArgumentException">Only TextInputComponents are allowed.</exception>
|
||||||
|
public ModalBuilder(string title, string customId, ModalComponentBuilder components = null)
|
||||||
|
{
|
||||||
|
Title = title;
|
||||||
|
CustomId = customId;
|
||||||
|
Components = components ?? new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the title of the current modal.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="title">The value to set the title to.</param>
|
||||||
|
/// <returns>The current builder.</returns>
|
||||||
|
public ModalBuilder WithTitle(string title)
|
||||||
|
{
|
||||||
|
Title = title;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the custom id of the current modal.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="title">The value to set the custom id to.</param>
|
||||||
|
/// <returns>The current builder.</returns>
|
||||||
|
public ModalBuilder WithCustomId(string customId)
|
||||||
|
{
|
||||||
|
CustomId = customId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a component to the current builder.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="title">The component to add.</param>
|
||||||
|
/// <returns>The current builder.</returns>
|
||||||
|
public ModalBuilder AddTextInput(TextInputBuilder component)
|
||||||
|
{
|
||||||
|
Components.WithTextInput(component);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a <see cref="TextInputBuilder"/> to the current builder.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="customId">The input's custom id.</param>
|
||||||
|
/// <param name="label">The input's label.</param>
|
||||||
|
/// <param name="placeholder">The input's placeholder text.</param>
|
||||||
|
/// <param name="minLength">The input's minimum length.</param>
|
||||||
|
/// <param name="maxLength">The input's maximum length.</param>
|
||||||
|
/// <param name="style">The input's style.</param>
|
||||||
|
/// <returns>The current builder.</returns>
|
||||||
|
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));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds multiple components to the current builder.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="components">The components to add.</param>
|
||||||
|
/// <returns>The current builder</returns>
|
||||||
|
public ModalBuilder AddComponents(List<IMessageComponent> components, int row)
|
||||||
|
{
|
||||||
|
components.ForEach(x => Components.AddComponent(x, row));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds this builder into a <see cref="Modal"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A <see cref="Modal"/> with the same values as this builder.</returns>
|
||||||
|
/// <exception cref="ArgumentException">Only TextInputComponents are allowed.</exception>
|
||||||
|
/// <exception cref="ArgumentException">Modals must have a custom id.</exception>
|
||||||
|
/// <exception cref="ArgumentException">Modals must have a title.</exception>
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a builder for creating a <see cref="ModalComponent"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class ModalComponentBuilder
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The max length of a <see cref="IMessageComponent.CustomId"/>.
|
||||||
|
/// </summary>
|
||||||
|
public const int MaxCustomIdLength = 100;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The max amount of rows a <see cref="ModalComponent"/> can have.
|
||||||
|
/// </summary>
|
||||||
|
public const int MaxActionRowCount = 5;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the Action Rows for this Component Builder.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="ArgumentNullException" accessor="set"><see cref="ActionRows"/> cannot be null.</exception>
|
||||||
|
/// <exception cref="ArgumentException" accessor="set"><see cref="ActionRows"/> count exceeds <see cref="MaxActionRowCount"/>.</exception>
|
||||||
|
public List<ActionRowBuilder> 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<ActionRowBuilder> _actionRows;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new builder from the provided list of components.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="components">The components to create the builder from.</param>
|
||||||
|
/// <returns>The newly created builder.</returns>
|
||||||
|
public static ComponentBuilder FromComponents(IReadOnlyCollection<IMessageComponent> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a <see cref="TextInputBuilder"/> to the <see cref="ComponentBuilder"/> at the specific row.
|
||||||
|
/// If the row cannot accept the component then it will add it to a row that can.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="customId">The input's custom id.</param>
|
||||||
|
/// <param name="label">The input's label.</param>
|
||||||
|
/// <param name="placeholder">The input's placeholder text.</param>
|
||||||
|
/// <param name="minLength">The input's minimum length.</param>
|
||||||
|
/// <param name="maxLength">The input's maximum length.</param>
|
||||||
|
/// <param name="style">The input's style.</param>
|
||||||
|
/// <returns>The current builder.</returns>
|
||||||
|
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);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a <see cref="TextInputBuilder"/> to the <see cref="ModalComponentBuilder"/> at the specific row.
|
||||||
|
/// If the row cannot accept the component then it will add it to a row that can.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">The <see cref="TextInputBuilder"> to add.</param>
|
||||||
|
/// <param name="row">The row to add the text input.</param>
|
||||||
|
/// <exception cref="InvalidOperationException">There are no more rows to add a text input to.</exception>
|
||||||
|
/// <exception cref="ArgumentException"><paramref name="row"/> must be less than <see cref="MaxActionRowCount"/>.</exception>
|
||||||
|
/// <returns>The current builder.</returns>
|
||||||
|
public ModalComponentBuilder WithTextInput(TextInputBuilder text, int row = 0)
|
||||||
|
{
|
||||||
|
Preconditions.LessThan(row, MaxActionRowCount, nameof(row));
|
||||||
|
|
||||||
|
var builtButton = text.Build();
|
||||||
|
|
||||||
|
if (_actionRows == null)
|
||||||
|
{
|
||||||
|
_actionRows = new List<ActionRowBuilder>
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a <see cref="ModalComponent"/> representing the builder.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A <see cref="ModalComponent"/> representing the builder.</returns>
|
||||||
|
public ModalComponent Build()
|
||||||
|
=> new (ActionRows?.Select(x => x.Build()).ToList());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Discord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a component object used in <see cref="Modal"/>s.
|
||||||
|
/// </summary>
|
||||||
|
public class ModalComponent
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the components to be used in a modal.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<ActionRowComponent> Components { get; }
|
||||||
|
|
||||||
|
internal ModalComponent(List<ActionRowComponent> components)
|
||||||
|
{
|
||||||
|
Components = components;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Discord.Interactions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Create a Modal interaction handler. CustomId represents
|
||||||
|
/// the CustomId of the Modal that will be handled.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <see cref="GroupAttribute"/>s will add prefixes to this command if <see cref="IgnoreGroupNames"/> is set to <see langword="false"/>
|
||||||
|
/// CustomID supports a Wild Card pattern where you can use the <see cref="InteractionServiceConfig.WildCardExpression"/> to match a set of CustomIDs.
|
||||||
|
/// </remarks>
|
||||||
|
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
|
||||||
|
public sealed class ModalInteractionAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the string to compare the Modal CustomIDs with.
|
||||||
|
/// </summary>
|
||||||
|
public string CustomId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets <see langword="true"/> if <see cref="GroupAttribute"/>s will be ignored while creating this command and this method will be treated as a top level command.
|
||||||
|
/// </summary>
|
||||||
|
public bool IgnoreGroupNames { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the run mode this command gets executed with.
|
||||||
|
/// </summary>
|
||||||
|
public RunMode RunMode { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a command for modal interaction handling.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="customId">String to compare the modal CustomIDs with.</param>
|
||||||
|
/// <param name="ignoreGroupNames">If <see langword="true"/> <see cref="GroupAttribute"/>s will be ignored while creating this command and this method will be treated as a top level command.</param>
|
||||||
|
/// <param name="runMode">Set the run mode of the command.</param>
|
||||||
|
public ModalInteractionAttribute(string customId, bool ignoreGroupNames = false, RunMode runMode = RunMode.Default)
|
||||||
|
{
|
||||||
|
CustomId = customId;
|
||||||
|
IgnoreGroupNames = ignoreGroupNames;
|
||||||
|
RunMode = runMode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Discord.Interactions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a custom label for an modal input.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
|
||||||
|
public class InputLabelAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the label of the input.
|
||||||
|
/// </summary>
|
||||||
|
public string Label { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a custom label for an modal input.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="label">The label of the input.</param>
|
||||||
|
public InputLabelAttribute(string label)
|
||||||
|
{
|
||||||
|
Label = label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Discord.Interactions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Mark an <see cref="IModal"/> property as a modal input field.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
|
||||||
|
public abstract class ModalInputAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the custom id of the text input.
|
||||||
|
/// </summary>
|
||||||
|
public string CustomId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the type of the component.
|
||||||
|
/// </summary>
|
||||||
|
public abstract ComponentType ComponentType { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="ModalInputAttribute"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="label">The label of the input.</param>
|
||||||
|
/// <param name="customId">The custom id of the input.</param>
|
||||||
|
/// <param name="required">Whether the user is required to input a value.></param>
|
||||||
|
protected ModalInputAttribute(string customId)
|
||||||
|
{
|
||||||
|
CustomId = customId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
namespace Discord.Interactions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Marks a <see cref="IModal"/> property as a text input.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ModalTextInputAttribute : ModalInputAttribute
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override ComponentType ComponentType => ComponentType.TextInput;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the style of the text input.
|
||||||
|
/// </summary>
|
||||||
|
public TextInputStyle Style { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the placeholder of the text input.
|
||||||
|
/// </summary>
|
||||||
|
public string Placeholder { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the minimum length of the text input.
|
||||||
|
/// </summary>
|
||||||
|
public int MinLength { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the maximum length of the text input.
|
||||||
|
/// </summary>
|
||||||
|
public int MaxLength { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the initial value to be displayed by this input.
|
||||||
|
/// </summary>
|
||||||
|
public string InitialValue { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="ModalTextInputAttribute"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="customId"The custom id of the text input.></param>
|
||||||
|
/// <param name="style">The style of the text input.</param>
|
||||||
|
/// <param name="placeholder">The placeholder of the text input.</param>
|
||||||
|
/// <param name="minLength">The minimum length of the text input's content.</param>
|
||||||
|
/// <param name="maxLength">The maximum length of the text input's content.</param>
|
||||||
|
/// <param name="initValue">The initial value to be displayed by this input.</param>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Discord.Interactions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the input as required or optional.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
|
||||||
|
public class RequiredInputAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether or not user input is required for this input.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsRequired { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the input as required or optinal.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="isRequired">Whether or not user input is required for this input.</param>
|
||||||
|
public RequiredInputAttribute(bool isRequired = true)
|
||||||
|
{
|
||||||
|
IsRequired = isRequired;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Discord.Interactions.Builders
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a builder for creating a <see cref="ModalCommandInfo"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class ModalCommandBuilder : CommandBuilder<ModalCommandInfo, ModalCommandBuilder, ModalCommandParameterBuilder>
|
||||||
|
{
|
||||||
|
protected override ModalCommandBuilder Instance => this;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new <see cref="ModalCommandBuilder"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="module">Parent module of this modal.</param>
|
||||||
|
public ModalCommandBuilder(ModuleBuilder module) : base(module) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new <see cref="ModalCommandBuilder"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="module">Parent module of this modal.</param>
|
||||||
|
/// <param name="name">Name of this modal.</param>
|
||||||
|
/// <param name="callback">Execution callback of this modal.</param>
|
||||||
|
public ModalCommandBuilder(ModuleBuilder module, string name, ExecuteCallback callback) : base(module, name, callback) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a modal parameter to the parameters collection.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configure"><see cref="ModalCommandParameterBuilder"/> factory.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public override ModalCommandBuilder AddParameter(Action<ModalCommandParameterBuilder> configure)
|
||||||
|
{
|
||||||
|
var parameter = new ModalCommandParameterBuilder(this);
|
||||||
|
configure(parameter);
|
||||||
|
AddParameters(parameter);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal override ModalCommandInfo Build(ModuleInfo module, InteractionService commandService) =>
|
||||||
|
new(this, module, commandService);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Discord.Interactions.Builders
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represent a builder for creating <see cref="InputComponentInfo"/>.
|
||||||
|
/// </summary>
|
||||||
|
public interface IInputComponentBuilder
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the parent modal of this input component.
|
||||||
|
/// </summary>
|
||||||
|
ModalBuilder Modal { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the custom id of this input component.
|
||||||
|
/// </summary>
|
||||||
|
string CustomId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the label of this input component.
|
||||||
|
/// </summary>
|
||||||
|
string Label { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether this input component is required.
|
||||||
|
/// </summary>
|
||||||
|
bool IsRequired { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the component type of this input component.
|
||||||
|
/// </summary>
|
||||||
|
ComponentType ComponentType { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the reference type of this input component.
|
||||||
|
/// </summary>
|
||||||
|
Type Type { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the default value of this input component.
|
||||||
|
/// </summary>
|
||||||
|
object DefaultValue { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a collection of the attributes of this component.
|
||||||
|
/// </summary>
|
||||||
|
IReadOnlyCollection<Attribute> Attributes { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets <see cref="CustomId"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="customId">New value of the <see cref="CustomId"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
IInputComponentBuilder WithCustomId(string customId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets <see cref="Label"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="label">New value of the <see cref="Label"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
IInputComponentBuilder WithLabel(string label);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets <see cref="IsRequired"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="isRequired">New value of the <see cref="IsRequired"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
IInputComponentBuilder SetIsRequired(bool isRequired);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets <see cref="Type"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">New value of the <see cref="Type"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
IInputComponentBuilder WithType(Type type);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets <see cref="DefaultValue"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">New value of the <see cref="DefaultValue"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
IInputComponentBuilder SetDefaultValue(object value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds attributes to <see cref="Attributes"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="attributes">New attributes to be added to <see cref="Attributes"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
IInputComponentBuilder WithAttributes(params Attribute[] attributes);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Discord.Interactions.Builders
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the base builder class for creating <see cref="InputComponentInfo"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TInfo">The <see cref="InputComponentInfo"/> this builder yields when built.</typeparam>
|
||||||
|
/// <typeparam name="TBuilder">Inherited <see cref="InputComponentBuilder{TInfo, TBuilder}"/> type.</typeparam>
|
||||||
|
public abstract class InputComponentBuilder<TInfo, TBuilder> : IInputComponentBuilder
|
||||||
|
where TInfo : InputComponentInfo
|
||||||
|
where TBuilder : InputComponentBuilder<TInfo, TBuilder>
|
||||||
|
{
|
||||||
|
private readonly List<Attribute> _attributes;
|
||||||
|
protected abstract TBuilder Instance { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public ModalBuilder Modal { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string CustomId { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string Label { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool IsRequired { get; set; } = true;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public ComponentType ComponentType { get; internal set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public Type Type { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public object DefaultValue { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public IReadOnlyCollection<Attribute> Attributes => _attributes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an instance of <see cref="InputComponentBuilder{TInfo, TBuilder}"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="modal">Parent modal of this input component.</param>
|
||||||
|
public InputComponentBuilder(ModalBuilder modal)
|
||||||
|
{
|
||||||
|
Modal = modal;
|
||||||
|
_attributes = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets <see cref="CustomId"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="customId">New value of the <see cref="CustomId"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public TBuilder WithCustomId(string customId)
|
||||||
|
{
|
||||||
|
CustomId = customId;
|
||||||
|
return Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets <see cref="Label"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="label">New value of the <see cref="Label"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public TBuilder WithLabel(string label)
|
||||||
|
{
|
||||||
|
Label = label;
|
||||||
|
return Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets <see cref="IsRequired"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="isRequired">New value of the <see cref="IsRequired"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public TBuilder SetIsRequired(bool isRequired)
|
||||||
|
{
|
||||||
|
IsRequired = isRequired;
|
||||||
|
return Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets <see cref="ComponentType"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="componentType">New value of the <see cref="ComponentType"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public TBuilder WithComponentType(ComponentType componentType)
|
||||||
|
{
|
||||||
|
ComponentType = componentType;
|
||||||
|
return Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets <see cref="Type"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">New value of the <see cref="Type"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public TBuilder WithType(Type type)
|
||||||
|
{
|
||||||
|
Type = type;
|
||||||
|
return Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets <see cref="DefaultValue"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">New value of the <see cref="DefaultValue"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public TBuilder SetDefaultValue(object value)
|
||||||
|
{
|
||||||
|
DefaultValue = value;
|
||||||
|
return Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds attributes to <see cref="Attributes"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="attributes">New attributes to be added to <see cref="Attributes"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public TBuilder WithAttributes(params Attribute[] attributes)
|
||||||
|
{
|
||||||
|
_attributes.AddRange(attributes);
|
||||||
|
return Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal abstract TInfo Build(ModalInfo modal);
|
||||||
|
|
||||||
|
//IInputComponentBuilder
|
||||||
|
/// <inheritdoc/>
|
||||||
|
IInputComponentBuilder IInputComponentBuilder.WithCustomId(string customId) => WithCustomId(customId);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
IInputComponentBuilder IInputComponentBuilder.WithLabel(string label) => WithCustomId(label);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
IInputComponentBuilder IInputComponentBuilder.WithType(Type type) => WithType(type);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
IInputComponentBuilder IInputComponentBuilder.SetDefaultValue(object value) => SetDefaultValue(value);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
IInputComponentBuilder IInputComponentBuilder.WithAttributes(params Attribute[] attributes) => WithAttributes(attributes);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
IInputComponentBuilder IInputComponentBuilder.SetIsRequired(bool isRequired) => SetIsRequired(isRequired);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
namespace Discord.Interactions.Builders
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a builder for creating <see cref="TextInputComponentInfo"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class TextInputComponentBuilder : InputComponentBuilder<TextInputComponentInfo, TextInputComponentBuilder>
|
||||||
|
{
|
||||||
|
protected override TextInputComponentBuilder Instance => this;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets and sets the style of the text input.
|
||||||
|
/// </summary>
|
||||||
|
public TextInputStyle Style { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets and sets the placeholder of the text input.
|
||||||
|
/// </summary>
|
||||||
|
public string Placeholder { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets and sets the minimum length of the text input.
|
||||||
|
/// </summary>
|
||||||
|
public int MinLength { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets and sets the maximum length of the text input.
|
||||||
|
/// </summary>
|
||||||
|
public int MaxLength { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets and sets the initial value to be displayed by this input.
|
||||||
|
/// </summary>
|
||||||
|
public string InitialValue { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new <see cref="TextInputComponentBuilder"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="modal">Parent modal of this component.</param>
|
||||||
|
public TextInputComponentBuilder(ModalBuilder modal) : base(modal) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets <see cref="Style"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="style">New value of the <see cref="SetValue(string)"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public TextInputComponentBuilder WithStyle(TextInputStyle style)
|
||||||
|
{
|
||||||
|
Style = style;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets <see cref="Placeholder"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="placeholder">New value of the <see cref="Placeholder"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public TextInputComponentBuilder WithPlaceholder(string placeholder)
|
||||||
|
{
|
||||||
|
Placeholder = placeholder;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets <see cref="MinLength"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="minLenght">New value of the <see cref="MinLength"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public TextInputComponentBuilder WithMinLenght(int minLenght)
|
||||||
|
{
|
||||||
|
MinLength = minLenght;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets <see cref="MaxLength"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="maxLenght">New value of the <see cref="MaxLength"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public TextInputComponentBuilder WithMaxLenght(int maxLenght)
|
||||||
|
{
|
||||||
|
MaxLength = maxLenght;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets <see cref="InitialValue"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">New value of the <see cref="InitialValue"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public TextInputComponentBuilder WithInitialValue(string value)
|
||||||
|
{
|
||||||
|
InitialValue = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal override TextInputComponentInfo Build(ModalInfo modal) =>
|
||||||
|
new(this, modal);
|
||||||
|
}
|
||||||
|
}
|
||||||
81
src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Discord.Interactions.Builders
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a builder for creating <see cref="ModalInfo"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class ModalBuilder
|
||||||
|
{
|
||||||
|
internal readonly List<IInputComponentBuilder> _components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the initialization delegate for this modal.
|
||||||
|
/// </summary>
|
||||||
|
public ModalInitializer ModalInitializer { get; internal set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the title of this modal.
|
||||||
|
/// </summary>
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="IModal"/> implementation used to initialize this object.
|
||||||
|
/// </summary>
|
||||||
|
public Type Type { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a collection of the components of this modal.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<IInputComponentBuilder> 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new <see cref="ModalBuilder"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="modalInitializer">The initialization delegate for this modal.</param>
|
||||||
|
public ModalBuilder(Type type, ModalInitializer modalInitializer) : this(type)
|
||||||
|
{
|
||||||
|
ModalInitializer = modalInitializer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets <see cref="Title"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="title">New value of the <see cref="Title"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public ModalBuilder WithTitle(string title)
|
||||||
|
{
|
||||||
|
Title = title;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds text components to <see cref="TextComponents"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configure">Text Component builder factory.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public ModalBuilder AddTextComponent(Action<TextInputComponentBuilder> configure)
|
||||||
|
{
|
||||||
|
var builder = new TextInputComponentBuilder(this);
|
||||||
|
configure(builder);
|
||||||
|
_components.Add(builder);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal ModalInfo Build() => new(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ namespace Discord.Interactions.Builders
|
|||||||
private readonly List<ContextCommandBuilder> _contextCommands;
|
private readonly List<ContextCommandBuilder> _contextCommands;
|
||||||
private readonly List<ComponentCommandBuilder> _componentCommands;
|
private readonly List<ComponentCommandBuilder> _componentCommands;
|
||||||
private readonly List<AutocompleteCommandBuilder> _autocompleteCommands;
|
private readonly List<AutocompleteCommandBuilder> _autocompleteCommands;
|
||||||
|
private readonly List<ModalCommandBuilder> _modalCommands;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the underlying Interaction Service.
|
/// Gets the underlying Interaction Service.
|
||||||
@@ -92,6 +93,11 @@ namespace Discord.Interactions.Builders
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyList<AutocompleteCommandBuilder> AutocompleteCommands => _autocompleteCommands;
|
public IReadOnlyList<AutocompleteCommandBuilder> AutocompleteCommands => _autocompleteCommands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a collection of the Modal Commands of this module.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<ModalCommandBuilder> ModalCommands => _modalCommands;
|
||||||
|
|
||||||
internal TypeInfo TypeInfo { get; set; }
|
internal TypeInfo TypeInfo { get; set; }
|
||||||
|
|
||||||
internal ModuleBuilder (InteractionService interactionService, ModuleBuilder parent = null)
|
internal ModuleBuilder (InteractionService interactionService, ModuleBuilder parent = null)
|
||||||
@@ -105,6 +111,7 @@ namespace Discord.Interactions.Builders
|
|||||||
_contextCommands = new List<ContextCommandBuilder>();
|
_contextCommands = new List<ContextCommandBuilder>();
|
||||||
_componentCommands = new List<ComponentCommandBuilder>();
|
_componentCommands = new List<ComponentCommandBuilder>();
|
||||||
_autocompleteCommands = new List<AutocompleteCommandBuilder>();
|
_autocompleteCommands = new List<AutocompleteCommandBuilder>();
|
||||||
|
_modalCommands = new List<ModalCommandBuilder> ();
|
||||||
_preconditions = new List<PreconditionAttribute>();
|
_preconditions = new List<PreconditionAttribute>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +159,7 @@ namespace Discord.Interactions.Builders
|
|||||||
/// <returns>
|
/// <returns>
|
||||||
/// The builder instance.
|
/// The builder instance.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
public ModuleBuilder WithDefaultPermision (bool permission)
|
public ModuleBuilder WithDefaultPermission (bool permission)
|
||||||
{
|
{
|
||||||
DefaultPermission = permission;
|
DefaultPermission = permission;
|
||||||
return this;
|
return this;
|
||||||
@@ -310,6 +317,21 @@ namespace Discord.Interactions.Builders
|
|||||||
configure(command);
|
configure(command);
|
||||||
_autocompleteCommands.Add(command);
|
_autocompleteCommands.Add(command);
|
||||||
return this;
|
return this;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a modal command builder to <see cref="ModalCommands"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configure"><see cref="ModalCommands"/> factory.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The builder instance.
|
||||||
|
/// </returns>
|
||||||
|
public ModuleBuilder AddModalCommand(Action<ModalCommandBuilder> configure)
|
||||||
|
{
|
||||||
|
var command = new ModalCommandBuilder(this);
|
||||||
|
configure(command);
|
||||||
|
_modalCommands.Add(command);
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ namespace Discord.Interactions.Builders
|
|||||||
var validContextCommands = methods.Where(IsValidContextCommandDefinition);
|
var validContextCommands = methods.Where(IsValidContextCommandDefinition);
|
||||||
var validInteractions = methods.Where(IsValidComponentCommandDefinition);
|
var validInteractions = methods.Where(IsValidComponentCommandDefinition);
|
||||||
var validAutocompleteCommands = methods.Where(IsValidAutocompleteCommandDefinition);
|
var validAutocompleteCommands = methods.Where(IsValidAutocompleteCommandDefinition);
|
||||||
|
var validModalCommands = methods.Where(IsValidModalCommanDefinition);
|
||||||
|
|
||||||
Func<IServiceProvider, IInteractionModuleBase> createInstance = commandService._useCompiledLambda ?
|
Func<IServiceProvider, IInteractionModuleBase> createInstance = commandService._useCompiledLambda ?
|
||||||
ReflectionUtils<IInteractionModuleBase>.CreateLambdaBuilder(typeInfo, commandService) : ReflectionUtils<IInteractionModuleBase>.CreateBuilder(typeInfo, commandService);
|
ReflectionUtils<IInteractionModuleBase>.CreateLambdaBuilder(typeInfo, commandService) : ReflectionUtils<IInteractionModuleBase>.CreateBuilder(typeInfo, commandService);
|
||||||
@@ -118,6 +119,9 @@ namespace Discord.Interactions.Builders
|
|||||||
|
|
||||||
foreach(var method in validAutocompleteCommands)
|
foreach(var method in validAutocompleteCommands)
|
||||||
builder.AddAutocompleteCommand(x => BuildAutocompleteCommand(x, createInstance, method, commandService, services));
|
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<TypeInfo> subModules, IList<TypeInfo> builtTypes, InteractionService commandService,
|
private static void BuildSubModules (ModuleBuilder parent, IEnumerable<TypeInfo> subModules, IList<TypeInfo> builtTypes, InteractionService commandService,
|
||||||
@@ -298,6 +302,47 @@ namespace Discord.Interactions.Builders
|
|||||||
builder.Callback = CreateCallback(createInstance, methodInfo, commandService);
|
builder.Callback = CreateCallback(createInstance, methodInfo, commandService);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void BuildModalCommand(ModalCommandBuilder builder, Func<IServiceProvider, IInteractionModuleBase> 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<IServiceProvider, IInteractionModuleBase> createInstance,
|
private static ExecuteCallback CreateCallback (Func<IServiceProvider, IInteractionModuleBase> createInstance,
|
||||||
MethodInfo methodInfo, InteractionService commandService)
|
MethodInfo methodInfo, InteractionService commandService)
|
||||||
{
|
{
|
||||||
@@ -400,7 +445,9 @@ namespace Discord.Interactions.Builders
|
|||||||
builder.Name = Regex.Replace(builder.Name, "(?<=[a-z])(?=[A-Z])", "-").ToLower();
|
builder.Name = Regex.Replace(builder.Name, "(?<=[a-z])(?=[A-Z])", "-").ToLower();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void BuildParameter (CommandParameterBuilder builder, ParameterInfo paramInfo)
|
private static void BuildParameter<TInfo, TBuilder> (ParameterBuilder<TInfo, TBuilder> builder, ParameterInfo paramInfo)
|
||||||
|
where TInfo : class, IParameterInfo
|
||||||
|
where TBuilder : ParameterBuilder<TInfo, TBuilder>
|
||||||
{
|
{
|
||||||
var attributes = paramInfo.GetCustomAttributes();
|
var attributes = paramInfo.GetCustomAttributes();
|
||||||
var paramType = paramInfo.ParameterType;
|
var paramType = paramInfo.ParameterType;
|
||||||
@@ -428,6 +475,84 @@ namespace Discord.Interactions.Builders
|
|||||||
}
|
}
|
||||||
#endregion
|
#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<ModalInputAttribute>()?.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<IModal>.CreateLambdaMemberInit(modalType.GetTypeInfo(), modalType.GetConstructor(Type.EmptyTypes), x => x.IsDefined(typeof(ModalInputAttribute)));
|
||||||
|
builder.ModalInitializer = (args) => memberInit(Array.Empty<object>(), 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)
|
internal static bool IsValidModuleDefinition (TypeInfo typeInfo)
|
||||||
{
|
{
|
||||||
return ModuleTypeInfo.IsAssignableFrom(typeInfo) &&
|
return ModuleTypeInfo.IsAssignableFrom(typeInfo) &&
|
||||||
@@ -467,5 +592,21 @@ namespace Discord.Interactions.Builders
|
|||||||
!methodInfo.IsGenericMethod &&
|
!methodInfo.IsGenericMethod &&
|
||||||
methodInfo.GetParameters().Length == 0;
|
methodInfo.GetParameters().Length == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsValidModalCommanDefinition(MethodInfo methodInfo)
|
||||||
|
{
|
||||||
|
return methodInfo.IsDefined(typeof(ModalInteractionAttribute)) &&
|
||||||
|
(methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task<RuntimeResult>)) &&
|
||||||
|
!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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Discord.Interactions.Builders
|
||||||
|
{
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a builder for creating <see cref="ModalCommandBuilder"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class ModalCommandParameterBuilder : ParameterBuilder<ModalCommandParameterInfo, ModalCommandParameterBuilder>
|
||||||
|
{
|
||||||
|
protected override ModalCommandParameterBuilder Instance => this;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the built <see cref="ModalInfo"/> class for this parameter, if <see cref="IsModalParameter"/> is <see langword="true"/>.
|
||||||
|
/// </summary>
|
||||||
|
public ModalInfo Modal { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether or not this parameter is an <see cref="IModal"/>.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsModalParameter => Modal is not null;
|
||||||
|
|
||||||
|
internal ModalCommandParameterBuilder(ICommandBuilder command) : base(command) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new <see cref="ModalCommandParameterBuilder"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="command">Parent command of this parameter.</param>
|
||||||
|
/// <param name="name">Name of this command.</param>
|
||||||
|
/// <param name="type">Type of this parameter.</param>
|
||||||
|
public ModalCommandParameterBuilder(ICommandBuilder command, string name, Type type) : base(command, name, type) { }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/Discord.Net.Interactions/Entities/IModal.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace Discord.Interactions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a generic <see cref="Modal"/> for use with the interaction service.
|
||||||
|
/// </summary>
|
||||||
|
public interface IModal
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the modal's title.
|
||||||
|
/// </summary>
|
||||||
|
string Title { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord.Interactions
|
||||||
|
{
|
||||||
|
public static class IDiscordInteractionExtentions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Respond to an interaction with a <see cref="IModal"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Type of the <see cref="IModal"/> implementation.</typeparam>
|
||||||
|
/// <param name="interaction">The interaction to respond to.</param>
|
||||||
|
/// <param name="options">The request options for this <see langword="async"/> request.</param>
|
||||||
|
/// <returns>A task that represents the asynchronous operation of responding to the interaction.</returns>
|
||||||
|
public static async Task RespondWithModalAsync<T>(this IDiscordInteraction interaction, string customId, RequestOptions options = null)
|
||||||
|
where T : class, IModal
|
||||||
|
{
|
||||||
|
if (!ModalUtils.TryGet<T>(out var modalInfo))
|
||||||
|
throw new ArgumentException($"{typeof(T).FullName} isn't referenced by any registered Modal Interaction Command and doesn't have a cached {typeof(ModalInfo)}");
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,7 +35,7 @@ namespace Discord.Interactions
|
|||||||
/// <param name="services">Services that will be used while initializing the <see cref="InteractionModuleBase{T}"/>.</param>
|
/// <param name="services">Services that will be used while initializing the <see cref="InteractionModuleBase{T}"/>.</param>
|
||||||
/// <param name="additionalArgs">Provide additional string parameters to the method along with the auto generated parameters.</param>
|
/// <param name="additionalArgs">Provide additional string parameters to the method along with the auto generated parameters.</param>
|
||||||
/// <returns>
|
/// <returns>
|
||||||
/// A task representing the asyncronous command execution process.
|
/// A task representing the asynchronous command execution process.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
public async Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services, params string[] additionalArgs)
|
public async Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services, params string[] additionalArgs)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
namespace Discord.Interactions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the info class of an attribute based method for handling Modal Interaction events.
|
||||||
|
/// </summary>
|
||||||
|
public class ModalCommandInfo : CommandInfo<ModalCommandParameterInfo>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="ModalInfo"/> class for this commands <see cref="IModal"/> parameter.
|
||||||
|
/// </summary>
|
||||||
|
public ModalInfo Modal { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override bool SupportsWildCards => true;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override IReadOnlyCollection<ModalCommandParameterInfo> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override async Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services)
|
||||||
|
=> await ExecuteAsync(context, services, null).ConfigureAwait(false);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Execute this command using dependency injection.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">Context that will be injected to the <see cref="InteractionModuleBase{T}"/>.</param>
|
||||||
|
/// <param name="services">Services that will be used while initializing the <see cref="InteractionModuleBase{T}"/>.</param>
|
||||||
|
/// <param name="additionalArgs">Provide additional string parameters to the method along with the auto generated parameters.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A task representing the asynchronous command execution process.
|
||||||
|
/// </returns>
|
||||||
|
public async Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services, params string[] additionalArgs)
|
||||||
|
{
|
||||||
|
if (context.Interaction is not IModalInteraction modalInteraction)
|
||||||
|
return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Modal Interaction.");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var args = new List<object>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
protected override Task InvokeModuleEvent(IInteractionContext context, IResult result)
|
||||||
|
=> CommandService._modalCommandExecutedEvent.InvokeAsync(this, context, result);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
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}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
|
||||||
|
namespace Discord.Interactions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the base info class for <see cref="IModal"/> input components.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class InputComponentInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the parent modal of this component.
|
||||||
|
/// </summary>
|
||||||
|
public ModalInfo Modal { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the custom id of this component.
|
||||||
|
/// </summary>
|
||||||
|
public string CustomId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the label of this component.
|
||||||
|
/// </summary>
|
||||||
|
public string Label { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether or not this component requires a user input.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsRequired { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the type of this component.
|
||||||
|
/// </summary>
|
||||||
|
public ComponentType ComponentType { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the reference type of this component.
|
||||||
|
/// </summary>
|
||||||
|
public Type Type { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the default value of this component.
|
||||||
|
/// </summary>
|
||||||
|
public object DefaultValue { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a collection of the attributes of this command.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<Attribute> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
namespace Discord.Interactions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the <see cref="InputComponentInfo"/> class for <see cref="ComponentType.TextInput"/> type.
|
||||||
|
/// </summary>
|
||||||
|
public class TextInputComponentInfo : InputComponentInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the style of the text input.
|
||||||
|
/// </summary>
|
||||||
|
public TextInputStyle Style { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the placeholder of the text input.
|
||||||
|
/// </summary>
|
||||||
|
public string Placeholder { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the minimum length of the text input.
|
||||||
|
/// </summary>
|
||||||
|
public int MinLength { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the maximum length of the text input.
|
||||||
|
/// </summary>
|
||||||
|
public int MaxLength { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the initial value to be displayed by this input.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/Discord.Net.Interactions/Info/ModalInfo.cs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Discord.Interactions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a cached object initialization delegate.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">Property arguments array.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// Returns the constructed object.
|
||||||
|
/// </returns>
|
||||||
|
public delegate IModal ModalInitializer(object[] args);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the info class of an <see cref="IModal"/> form.
|
||||||
|
/// </summary>
|
||||||
|
public class ModalInfo
|
||||||
|
{
|
||||||
|
internal readonly ModalInitializer _initializer;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the title of this modal.
|
||||||
|
/// </summary>
|
||||||
|
public string Title { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="IModal"/> implementation used to initialize this object.
|
||||||
|
/// </summary>
|
||||||
|
public Type Type { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a collection of the components of this modal.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<InputComponentInfo> Components { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a collection of the text components of this modal.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<TextInputComponentInfo> 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<TextInputComponentInfo>().ToImmutableArray();
|
||||||
|
|
||||||
|
_initializer = builder.ModalInitializer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an <see cref="IModal"/> and fills it with provided message components.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="components"><see cref="IModalInteraction"/> that will be injected into the modal.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="IModal"/> filled with the provided components.
|
||||||
|
/// </returns>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -68,6 +68,8 @@ namespace Discord.Interactions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyCollection<AutocompleteCommandInfo> AutocompleteCommands { get; }
|
public IReadOnlyCollection<AutocompleteCommandInfo> AutocompleteCommands { get; }
|
||||||
|
|
||||||
|
public IReadOnlyCollection<ModalCommandInfo> ModalCommands { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the declaring type of this module, if <see cref="IsSubModule"/> is <see langword="true"/>.
|
/// Gets the declaring type of this module, if <see cref="IsSubModule"/> is <see langword="true"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -112,6 +114,7 @@ namespace Discord.Interactions
|
|||||||
ContextCommands = BuildContextCommands(builder).ToImmutableArray();
|
ContextCommands = BuildContextCommands(builder).ToImmutableArray();
|
||||||
ComponentCommands = BuildComponentCommands(builder).ToImmutableArray();
|
ComponentCommands = BuildComponentCommands(builder).ToImmutableArray();
|
||||||
AutocompleteCommands = BuildAutocompleteCommands(builder).ToImmutableArray();
|
AutocompleteCommands = BuildAutocompleteCommands(builder).ToImmutableArray();
|
||||||
|
ModalCommands = BuildModalCommands(builder).ToImmutableArray();
|
||||||
SubModules = BuildSubModules(builder, commandService, services).ToImmutableArray();
|
SubModules = BuildSubModules(builder, commandService, services).ToImmutableArray();
|
||||||
Attributes = BuildAttributes(builder).ToImmutableArray();
|
Attributes = BuildAttributes(builder).ToImmutableArray();
|
||||||
Preconditions = BuildPreconditions(builder).ToImmutableArray();
|
Preconditions = BuildPreconditions(builder).ToImmutableArray();
|
||||||
@@ -171,6 +174,16 @@ namespace Discord.Interactions
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IEnumerable<ModalCommandInfo> BuildModalCommands(ModuleBuilder builder)
|
||||||
|
{
|
||||||
|
var result = new List<ModalCommandInfo>();
|
||||||
|
|
||||||
|
foreach (var commandBuilder in builder.ModalCommands)
|
||||||
|
result.Add(commandBuilder.Build(this, CommandService));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private IEnumerable<Attribute> BuildAttributes (ModuleBuilder builder)
|
private IEnumerable<Attribute> BuildAttributes (ModuleBuilder builder)
|
||||||
{
|
{
|
||||||
var result = new List<Attribute>();
|
var result = new List<Attribute>();
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using Discord.Interactions.Builders;
|
||||||
|
|
||||||
|
namespace Discord.Interactions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the base parameter info class for <see cref="InteractionService"/> modals.
|
||||||
|
/// </summary>
|
||||||
|
public class ModalCommandParameterInfo : CommandParameterInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="ModalInfo"/> class for this parameter if <see cref="IsModalParameter"/> is true.
|
||||||
|
/// </summary>
|
||||||
|
public ModalInfo Modal { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether this parameter is an <see cref="IModal"/>
|
||||||
|
/// </summary>
|
||||||
|
public bool IsModalParameter => Modal is not null;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public new ModalCommandInfo Command => base.Command as ModalCommandInfo;
|
||||||
|
|
||||||
|
internal ModalCommandParameterInfo(ModalCommandParameterBuilder builder, ICommandInfo command) : base(builder, command)
|
||||||
|
{
|
||||||
|
Modal = builder.Modal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -115,6 +115,13 @@ namespace Discord.Interactions
|
|||||||
await response.DeleteAsync().ConfigureAwait(false);
|
await response.DeleteAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="IDiscordInteraction.RespondWithModalAsync(Modal, RequestOptions)"/>
|
||||||
|
protected virtual async Task RespondWithModalAsync(Modal modal, RequestOptions options = null) => await Context.Interaction.RespondWithModalAsync(modal);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="IDiscordInteractionExtentions.RespondWithModalAsync(IDiscordInteraction, IModal, RequestOptions)"/>
|
||||||
|
protected virtual async Task RespondWithModalAsync<T>(string customId, RequestOptions options = null) where T : class, IModal
|
||||||
|
=> await Context.Interaction.RespondWithModalAsync<T>(customId, options);
|
||||||
|
|
||||||
//IInteractionModuleBase
|
//IInteractionModuleBase
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
|
|||||||
@@ -53,21 +53,29 @@ namespace Discord.Interactions
|
|||||||
public event Func<IAutocompleteHandler, IInteractionContext, IResult, Task> AutocompleteHandlerExecuted { add { _autocompleteHandlerExecutedEvent.Add(value); } remove { _autocompleteHandlerExecutedEvent.Remove(value); } }
|
public event Func<IAutocompleteHandler, IInteractionContext, IResult, Task> AutocompleteHandlerExecuted { add { _autocompleteHandlerExecutedEvent.Add(value); } remove { _autocompleteHandlerExecutedEvent.Remove(value); } }
|
||||||
internal readonly AsyncEvent<Func<IAutocompleteHandler, IInteractionContext, IResult, Task>> _autocompleteHandlerExecutedEvent = new();
|
internal readonly AsyncEvent<Func<IAutocompleteHandler, IInteractionContext, IResult, Task>> _autocompleteHandlerExecutedEvent = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Occurs when a Modal command is executed.
|
||||||
|
/// </summary>
|
||||||
|
public event Func<ModalCommandInfo, IInteractionContext, IResult, Task> ModalCommandExecuted { add { _modalCommandExecutedEvent.Add(value); } remove { _modalCommandExecutedEvent.Remove(value); } }
|
||||||
|
internal readonly AsyncEvent<Func<ModalCommandInfo, IInteractionContext, IResult, Task>> _modalCommandExecutedEvent = new();
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<Type, ModuleInfo> _typedModuleDefs;
|
private readonly ConcurrentDictionary<Type, ModuleInfo> _typedModuleDefs;
|
||||||
private readonly CommandMap<SlashCommandInfo> _slashCommandMap;
|
private readonly CommandMap<SlashCommandInfo> _slashCommandMap;
|
||||||
private readonly ConcurrentDictionary<ApplicationCommandType, CommandMap<ContextCommandInfo>> _contextCommandMaps;
|
private readonly ConcurrentDictionary<ApplicationCommandType, CommandMap<ContextCommandInfo>> _contextCommandMaps;
|
||||||
private readonly CommandMap<ComponentCommandInfo> _componentCommandMap;
|
private readonly CommandMap<ComponentCommandInfo> _componentCommandMap;
|
||||||
private readonly CommandMap<AutocompleteCommandInfo> _autocompleteCommandMap;
|
private readonly CommandMap<AutocompleteCommandInfo> _autocompleteCommandMap;
|
||||||
|
private readonly CommandMap<ModalCommandInfo> _modalCommandMap;
|
||||||
private readonly HashSet<ModuleInfo> _moduleDefs;
|
private readonly HashSet<ModuleInfo> _moduleDefs;
|
||||||
private readonly ConcurrentDictionary<Type, TypeConverter> _typeConverters;
|
private readonly ConcurrentDictionary<Type, TypeConverter> _typeConverters;
|
||||||
private readonly ConcurrentDictionary<Type, Type> _genericTypeConverters;
|
private readonly ConcurrentDictionary<Type, Type> _genericTypeConverters;
|
||||||
private readonly ConcurrentDictionary<Type, IAutocompleteHandler> _autocompleteHandlers = new();
|
private readonly ConcurrentDictionary<Type, IAutocompleteHandler> _autocompleteHandlers = new();
|
||||||
|
private readonly ConcurrentDictionary<Type, ModalInfo> _modalInfos = new();
|
||||||
private readonly SemaphoreSlim _lock;
|
private readonly SemaphoreSlim _lock;
|
||||||
internal readonly Logger _cmdLogger;
|
internal readonly Logger _cmdLogger;
|
||||||
internal readonly LogManager _logManager;
|
internal readonly LogManager _logManager;
|
||||||
internal readonly Func<DiscordRestClient> _getRestClient;
|
internal readonly Func<DiscordRestClient> _getRestClient;
|
||||||
|
|
||||||
internal readonly bool _throwOnError, _useCompiledLambda, _enableAutocompleteHandlers, _autoServiceScopes;
|
internal readonly bool _throwOnError, _useCompiledLambda, _enableAutocompleteHandlers, _autoServiceScopes, _exitOnMissingModalField;
|
||||||
internal readonly string _wildCardExp;
|
internal readonly string _wildCardExp;
|
||||||
internal readonly RunMode _runMode;
|
internal readonly RunMode _runMode;
|
||||||
internal readonly RestResponseCallback _restResponseCallback;
|
internal readonly RestResponseCallback _restResponseCallback;
|
||||||
@@ -97,6 +105,16 @@ namespace Discord.Interactions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyCollection<ComponentCommandInfo> ComponentCommands => _moduleDefs.SelectMany(x => x.ComponentCommands).ToList();
|
public IReadOnlyCollection<ComponentCommandInfo> ComponentCommands => _moduleDefs.SelectMany(x => x.ComponentCommands).ToList();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents all Modal Commands loaded within <see cref="InteractionService"/>.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<ModalCommandInfo> ModalCommands => _moduleDefs.SelectMany(x => x.ModalCommands).ToList();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a collection of the cached <see cref="ModalInfo"/> classes that are referenced in registered <see cref="ModalCommandInfo"/>s.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<ModalInfo> Modals => ModalUtils.Modals;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initialize a <see cref="InteractionService"/> with provided configurations.
|
/// Initialize a <see cref="InteractionService"/> with provided configurations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -145,6 +163,7 @@ namespace Discord.Interactions
|
|||||||
_contextCommandMaps = new ConcurrentDictionary<ApplicationCommandType, CommandMap<ContextCommandInfo>>();
|
_contextCommandMaps = new ConcurrentDictionary<ApplicationCommandType, CommandMap<ContextCommandInfo>>();
|
||||||
_componentCommandMap = new CommandMap<ComponentCommandInfo>(this, config.InteractionCustomIdDelimiters);
|
_componentCommandMap = new CommandMap<ComponentCommandInfo>(this, config.InteractionCustomIdDelimiters);
|
||||||
_autocompleteCommandMap = new CommandMap<AutocompleteCommandInfo>(this);
|
_autocompleteCommandMap = new CommandMap<AutocompleteCommandInfo>(this);
|
||||||
|
_modalCommandMap = new CommandMap<ModalCommandInfo>(this, config.InteractionCustomIdDelimiters);
|
||||||
|
|
||||||
_getRestClient = getRestClient;
|
_getRestClient = getRestClient;
|
||||||
|
|
||||||
@@ -155,6 +174,7 @@ namespace Discord.Interactions
|
|||||||
_throwOnError = config.ThrowOnError;
|
_throwOnError = config.ThrowOnError;
|
||||||
_wildCardExp = config.WildCardExpression;
|
_wildCardExp = config.WildCardExpression;
|
||||||
_useCompiledLambda = config.UseCompiledLambda;
|
_useCompiledLambda = config.UseCompiledLambda;
|
||||||
|
_exitOnMissingModalField = config.ExitOnMissingModalField;
|
||||||
_enableAutocompleteHandlers = config.EnableAutocompleteHandlers;
|
_enableAutocompleteHandlers = config.EnableAutocompleteHandlers;
|
||||||
_autoServiceScopes = config.AutoServiceScopes;
|
_autoServiceScopes = config.AutoServiceScopes;
|
||||||
_restResponseCallback = config.RestResponseCallback;
|
_restResponseCallback = config.RestResponseCallback;
|
||||||
@@ -509,6 +529,9 @@ namespace Discord.Interactions
|
|||||||
foreach (var command in module.AutocompleteCommands)
|
foreach (var command in module.AutocompleteCommands)
|
||||||
_autocompleteCommandMap.AddCommand(command.GetCommandKeywords(), command);
|
_autocompleteCommandMap.AddCommand(command.GetCommandKeywords(), command);
|
||||||
|
|
||||||
|
foreach (var command in module.ModalCommands)
|
||||||
|
_modalCommandMap.AddCommand(command, command.IgnoreGroupNames);
|
||||||
|
|
||||||
foreach (var subModule in module.SubModules)
|
foreach (var subModule in module.SubModules)
|
||||||
LoadModuleInternal(subModule);
|
LoadModuleInternal(subModule);
|
||||||
}
|
}
|
||||||
@@ -662,6 +685,7 @@ namespace Discord.Interactions
|
|||||||
IUserCommandInteraction userCommand => await ExecuteContextCommandAsync(context, userCommand.Data.Name, ApplicationCommandType.User, services).ConfigureAwait(false),
|
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),
|
IMessageCommandInteraction messageCommand => await ExecuteContextCommandAsync(context, messageCommand.Data.Name, ApplicationCommandType.Message, services).ConfigureAwait(false),
|
||||||
IAutocompleteInteraction autocomplete => await ExecuteAutocompleteAsync(context, autocomplete, 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"),
|
_ => 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);
|
return await commandResult.Command.ExecuteAsync(context, services).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<IResult> 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)
|
internal TypeConverter GetTypeConverter (Type type, IServiceProvider services = null)
|
||||||
{
|
{
|
||||||
if (_typeConverters.TryGetValue(type, out var specific))
|
if (_typeConverters.TryGetValue(type, out var specific))
|
||||||
@@ -819,6 +857,24 @@ namespace Discord.Interactions
|
|||||||
_genericTypeConverters[targetType] = converterType;
|
_genericTypeConverters[targetType] = converterType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads and caches an <see cref="ModalInfo"/> for the provided <see cref="IModal"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Type of <see cref="IModal"/> to be loaded.</typeparam>
|
||||||
|
/// <returns>
|
||||||
|
/// The built <see cref="ModalInfo"/> instance.
|
||||||
|
/// </returns>
|
||||||
|
/// <exception cref="InvalidOperationException"></exception>
|
||||||
|
public ModalInfo AddModalInfo<T>() 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)
|
internal IAutocompleteHandler GetAutocompleteHandler(Type autocompleteHandlerType, IServiceProvider services = null)
|
||||||
{
|
{
|
||||||
services ??= EmptyServiceProvider.Instance;
|
services ??= EmptyServiceProvider.Instance;
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ namespace Discord.Interactions
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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.
|
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// For performance reasons, if you frequently use <see cref="Modal"/>s with the service, it is highly recommended that you enable compiled lambdas.
|
||||||
|
/// </remarks>
|
||||||
public bool UseCompiledLambda { get; set; } = false;
|
public bool UseCompiledLambda { get; set; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -56,6 +59,11 @@ namespace Discord.Interactions
|
|||||||
/// Gets or sets delegate to be used by the <see cref="InteractionService"/> when responding to a Rest based interaction.
|
/// Gets or sets delegate to be used by the <see cref="InteractionService"/> when responding to a Rest based interaction.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public RestResponseCallback RestResponseCallback { get; set; } = (ctx, str) => Task.CompletedTask;
|
public RestResponseCallback RestResponseCallback { get; set; } = (ctx, str) => Task.CompletedTask;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether a command execution should exit when a modal command encounters a missing modal component value.
|
||||||
|
/// </summary>
|
||||||
|
public bool ExitOnMissingModalField { get; set; } = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
51
src/Discord.Net.Interactions/Utilities/ModalUtils.cs
Normal file
@@ -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<Type, ModalInfo> _modalInfos = new();
|
||||||
|
|
||||||
|
public static IReadOnlyCollection<ModalInfo> 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<T>() 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<T>(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<T>(out ModalInfo modalInfo) where T : class, IModal
|
||||||
|
=> TryRemove(typeof(T), out modalInfo);
|
||||||
|
|
||||||
|
public static void Clear() => _modalInfos.Clear();
|
||||||
|
|
||||||
|
public static int Count() => _modalInfos.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -112,6 +112,67 @@ namespace Discord.Interactions
|
|||||||
var parameters = constructor.GetParameters();
|
var parameters = constructor.GetParameters();
|
||||||
var properties = GetProperties(typeInfo);
|
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<object[], T> 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<Func<object[], T>>(newExp, argsExp).Compile();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a compiled lambda property setter.
|
||||||
|
/// </summary>
|
||||||
|
internal static Action<T, object> 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<Action<T, object>>(assign, instanceParam, valueParam).Compile();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Func<object[], object[], T> CreateLambdaMemberInit(TypeInfo typeInfo, ConstructorInfo constructor, Predicate<PropertyInfo> 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 argsExp = Expression.Parameter(typeof(object[]), "args");
|
||||||
var propsExp = Expression.Parameter(typeof(object[]), "props");
|
var propsExp = Expression.Parameter(typeof(object[]), "props");
|
||||||
|
|
||||||
@@ -137,17 +198,8 @@ namespace Discord.Interactions
|
|||||||
var memberInit = Expression.MemberInit(newExp, memberExps);
|
var memberInit = Expression.MemberInit(newExp, memberExps);
|
||||||
var lambda = Expression.Lambda<Func<object[], object[], T>>(memberInit, argsExp, propsExp).Compile();
|
var lambda = Expression.Lambda<Func<object[], object[], T>>(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);
|
var instance = lambda(args, props);
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ namespace Discord.API
|
|||||||
{
|
{
|
||||||
ComponentType.Button => new ButtonComponent(x as Discord.ButtonComponent),
|
ComponentType.Button => new ButtonComponent(x as Discord.ButtonComponent),
|
||||||
ComponentType.SelectMenu => new SelectMenuComponent(x as Discord.SelectMenuComponent),
|
ComponentType.SelectMenu => new SelectMenuComponent(x as Discord.SelectMenuComponent),
|
||||||
|
ComponentType.TextInput => new TextInputComponent(x as Discord.TextInputComponent),
|
||||||
_ => null
|
_ => null
|
||||||
};
|
};
|
||||||
}).ToArray();
|
}).ToArray();
|
||||||
|
|||||||
@@ -24,5 +24,11 @@ namespace Discord.API
|
|||||||
|
|
||||||
[JsonProperty("choices")]
|
[JsonProperty("choices")]
|
||||||
public Optional<ApplicationCommandOptionChoice[]> Choices { get; set; }
|
public Optional<ApplicationCommandOptionChoice[]> Choices { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("title")]
|
||||||
|
public Optional<string> Title { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("custom_id")]
|
||||||
|
public Optional<string> CustomId { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,5 +12,8 @@ namespace Discord.API
|
|||||||
|
|
||||||
[JsonProperty("values")]
|
[JsonProperty("values")]
|
||||||
public Optional<string[]> Values { get; set; }
|
public Optional<string[]> Values { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("value")]
|
||||||
|
public Optional<string> Value { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/Discord.Net.Rest/API/Common/ModalInteractionData.cs
Normal file
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,8 @@ namespace Discord.API
|
|||||||
[JsonProperty("disabled")]
|
[JsonProperty("disabled")]
|
||||||
public bool Disabled { get; set; }
|
public bool Disabled { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("values")]
|
||||||
|
public Optional<string[]> Values { get; set; }
|
||||||
public SelectMenuComponent() { }
|
public SelectMenuComponent() { }
|
||||||
|
|
||||||
public SelectMenuComponent(Discord.SelectMenuComponent component)
|
public SelectMenuComponent(Discord.SelectMenuComponent component)
|
||||||
|
|||||||
49
src/Discord.Net.Rest/API/Common/TextInputComponent.cs
Normal file
@@ -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<string> Placeholder { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("min_length")]
|
||||||
|
public Optional<int> MinLength { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("max_length")]
|
||||||
|
public Optional<int> MaxLength { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("value")]
|
||||||
|
public Optional<string> Value { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("required")]
|
||||||
|
public Optional<bool> 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<int>.Unspecified;
|
||||||
|
MaxLength = component.MaxLength ?? Optional<int>.Unspecified;
|
||||||
|
Required = component.Required ?? Optional<bool>.Unspecified;
|
||||||
|
Value = component.Value ?? Optional<string>.Unspecified;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -316,5 +316,45 @@ namespace Discord.Rest
|
|||||||
|
|
||||||
return SerializePayload(response);
|
return SerializePayload(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Responds to the interaction with a modal.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="modal">The modal to respond with.</param>
|
||||||
|
/// <param name="options">The request options for this <see langword="async"/> request.</param>
|
||||||
|
/// <returns>A string that contains json to write back to the incoming http request.</returns>
|
||||||
|
/// <exception cref="TimeoutException"></exception>
|
||||||
|
/// <exception cref="InvalidOperationException"></exception>
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -446,6 +446,46 @@ namespace Discord.Rest
|
|||||||
return SerializePayload(response);
|
return SerializePayload(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Responds to the interaction with a modal.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="modal">The modal to respond with.</param>
|
||||||
|
/// <param name="options">The request options for this <see langword="async"/> request.</param>
|
||||||
|
/// <returns>A string that contains json to write back to the incoming http request.</returns>
|
||||||
|
/// <exception cref="TimeoutException"></exception>
|
||||||
|
/// <exception cref="InvalidOperationException"></exception>
|
||||||
|
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
|
//IComponentInteraction
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
IComponentInteractionData IComponentInteraction.Data => Data;
|
IComponentInteractionData IComponentInteraction.Data => Data;
|
||||||
|
|||||||
@@ -27,11 +27,26 @@ namespace Discord.Rest
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyCollection<string> Values { get; }
|
public IReadOnlyCollection<string> Values { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string Value { get; }
|
||||||
|
|
||||||
internal RestMessageComponentData(Model model)
|
internal RestMessageComponentData(Model model)
|
||||||
{
|
{
|
||||||
CustomId = model.CustomId;
|
CustomId = model.CustomId;
|
||||||
Type = model.ComponentType;
|
Type = model.ComponentType;
|
||||||
Values = model.Values.GetValueOrDefault();
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
402
src/Discord.Net.Rest/Entities/Interactions/Modals/RestModal.cs
Normal file
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a user submitted <see cref="Modal"/>.
|
||||||
|
/// </summary>
|
||||||
|
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<RestModal> CreateAsync(DiscordRestClient client, ModelBase model)
|
||||||
|
{
|
||||||
|
var entity = new RestModal(client, model);
|
||||||
|
await entity.UpdateAsync(client, model);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private object _lock = new object();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Acknowledges this interaction with the <see cref="InteractionResponseType.DeferredChannelMessageWithSource"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A string that contains json to write back to the incoming http request.
|
||||||
|
/// </returns>
|
||||||
|
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<MessageFlags>.Unspecified
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (HasResponded)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Cannot respond or defer twice to the same interaction");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
HasResponded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SerializePayload(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends a followup message for this interaction.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">The text of the message to be sent.</param>
|
||||||
|
/// <param name="embeds">A array of embeds to send with this response. Max 10.</param>
|
||||||
|
/// <param name="isTTS"><see langword="true"/> if the message should be read out by a text-to-speech reader, otherwise <see langword="false"/>.</param>
|
||||||
|
/// <param name="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</param>
|
||||||
|
/// <param name="allowedMentions">The allowed mentions for this response.</param>
|
||||||
|
/// <param name="options">The request options for this response.</param>
|
||||||
|
/// <param name="component">A <see cref="MessageComponent"/> to be sent with this response.</param>
|
||||||
|
/// <param name="embed">A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The sent message.
|
||||||
|
/// </returns>
|
||||||
|
public override async Task<RestFollowupMessage> 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<Embed>();
|
||||||
|
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<API.AllowedMentions>.Unspecified,
|
||||||
|
IsTTS = isTTS,
|
||||||
|
Embeds = embeds.Select(x => x.ToModel()).ToArray(),
|
||||||
|
Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional<API.ActionRowComponent[]>.Unspecified
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ephemeral)
|
||||||
|
args.Flags = MessageFlags.Ephemeral;
|
||||||
|
|
||||||
|
return await InteractionHelper.SendFollowupAsync(Discord, args, Token, Channel, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends a followup message for this interaction.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">The text of the message to be sent.</param>
|
||||||
|
/// <param name="fileStream">The file to upload.</param>
|
||||||
|
/// <param name="fileName">The file name of the attachment.</param>
|
||||||
|
/// <param name="embeds">A array of embeds to send with this response. Max 10.</param>
|
||||||
|
/// <param name="isTTS"><see langword="true"/> if the message should be read out by a text-to-speech reader, otherwise <see langword="false"/>.</param>
|
||||||
|
/// <param name="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</param>
|
||||||
|
/// <param name="allowedMentions">The allowed mentions for this response.</param>
|
||||||
|
/// <param name="options">The request options for this response.</param>
|
||||||
|
/// <param name="component">A <see cref="MessageComponent"/> to be sent with this response.</param>
|
||||||
|
/// <param name="embed">A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The sent message.
|
||||||
|
/// </returns>
|
||||||
|
public override async Task<RestFollowupMessage> 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<Embed>();
|
||||||
|
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<API.AllowedMentions>.Unspecified,
|
||||||
|
IsTTS = isTTS,
|
||||||
|
Embeds = embeds.Select(x => x.ToModel()).ToArray(),
|
||||||
|
Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional<API.ActionRowComponent[]>.Unspecified,
|
||||||
|
File = fileStream is not null ? new MultipartFile(fileStream, fileName) : Optional<MultipartFile>.Unspecified
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ephemeral)
|
||||||
|
args.Flags = MessageFlags.Ephemeral;
|
||||||
|
|
||||||
|
return await InteractionHelper.SendFollowupAsync(Discord, args, Token, Channel, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends a followup message for this interaction.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">The text of the message to be sent.</param>
|
||||||
|
/// <param name="filePath">The file to upload.</param>
|
||||||
|
/// <param name="fileName">The file name of the attachment.</param>
|
||||||
|
/// <param name="embeds">A array of embeds to send with this response. Max 10.</param>
|
||||||
|
/// <param name="isTTS"><see langword="true"/> if the message should be read out by a text-to-speech reader, otherwise <see langword="false"/>.</param>
|
||||||
|
/// <param name="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</param>
|
||||||
|
/// <param name="allowedMentions">The allowed mentions for this response.</param>
|
||||||
|
/// <param name="options">The request options for this response.</param>
|
||||||
|
/// <param name="component">A <see cref="MessageComponent"/> to be sent with this response.</param>
|
||||||
|
/// <param name="embed">A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The sent message.
|
||||||
|
/// </returns>
|
||||||
|
public override async Task<RestFollowupMessage> 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<Embed>();
|
||||||
|
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<API.AllowedMentions>.Unspecified,
|
||||||
|
IsTTS = isTTS,
|
||||||
|
Embeds = embeds.Select(x => x.ToModel()).ToArray(),
|
||||||
|
Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional<API.ActionRowComponent[]>.Unspecified,
|
||||||
|
File = !string.IsNullOrEmpty(filePath) ? new MultipartFile(new MemoryStream(File.ReadAllBytes(filePath), false), fileName) : Optional<MultipartFile>.Unspecified
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ephemeral)
|
||||||
|
args.Flags = MessageFlags.Ephemeral;
|
||||||
|
|
||||||
|
return await InteractionHelper.SendFollowupAsync(Discord, args, Token, Channel, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Responds to an Interaction with type <see cref="InteractionResponseType.ChannelMessageWithSource"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">The text of the message to be sent.</param>
|
||||||
|
/// <param name="embeds">A array of embeds to send with this response. Max 10.</param>
|
||||||
|
/// <param name="isTTS"><see langword="true"/> if the message should be read out by a text-to-speech reader, otherwise <see langword="false"/>.</param>
|
||||||
|
/// <param name="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</param>
|
||||||
|
/// <param name="allowedMentions">The allowed mentions for this response.</param>
|
||||||
|
/// <param name="options">The request options for this response.</param>
|
||||||
|
/// <param name="component">A <see cref="MessageComponent"/> to be sent with this response.</param>
|
||||||
|
/// <param name="embed">A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored.</param>
|
||||||
|
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">The parameters provided were invalid or the token was invalid.</exception>
|
||||||
|
/// <returns>
|
||||||
|
/// A string that contains json to write back to the incoming http request.
|
||||||
|
/// </returns>
|
||||||
|
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<Embed>();
|
||||||
|
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<API.AllowedMentions>.Unspecified,
|
||||||
|
Embeds = embeds.Select(x => x.ToModel()).ToArray(),
|
||||||
|
TTS = isTTS,
|
||||||
|
Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional<API.ActionRowComponent[]>.Unspecified,
|
||||||
|
Flags = ephemeral ? MessageFlags.Ephemeral : Optional<MessageFlags>.Unspecified
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (HasResponded)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Cannot respond twice to the same interaction");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
HasResponded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SerializePayload(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override async Task<RestFollowupMessage> FollowupWithFilesAsync(
|
||||||
|
IEnumerable<FileAttachment> 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<Embed>();
|
||||||
|
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<API.Embed[]>.Unspecified, AllowedMentions = allowedMentions?.ToModel() ?? Optional<API.AllowedMentions>.Unspecified, MessageComponents = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional<API.ActionRowComponent[]>.Unspecified };
|
||||||
|
return await InteractionHelper.SendFollowupAsync(Discord, args, Token, Channel, options).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override Task<RestFollowupMessage> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents data sent from a <see cref="InteractionType.ModalSubmit"/> Interaction.
|
||||||
|
/// </summary>
|
||||||
|
public class RestModalData : IComponentInteractionData, IModalInteractionData
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string CustomId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the <see cref="Modal"/>s components submitted by the user.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<RestMessageComponentData> Components { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public ComponentType Type => ComponentType.ModalSubmit;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public IReadOnlyCollection<string> Values
|
||||||
|
=> throw new NotSupportedException("Modal interactions do not have values!");
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string Value
|
||||||
|
=> throw new NotSupportedException("Modal interactions do not have value!");
|
||||||
|
|
||||||
|
IReadOnlyCollection<IComponentInteractionData> IModalInteractionData.Components => Components;
|
||||||
|
|
||||||
|
internal RestModalData(Model model)
|
||||||
|
{
|
||||||
|
CustomId = model.CustomId;
|
||||||
|
Components = model.Components
|
||||||
|
.SelectMany(x => x.Components)
|
||||||
|
.Select(x => new RestMessageComponentData(x))
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -100,6 +100,9 @@ namespace Discord.Rest
|
|||||||
if (model.Type == InteractionType.ApplicationCommandAutocomplete)
|
if (model.Type == InteractionType.ApplicationCommandAutocomplete)
|
||||||
return await RestAutocompleteInteraction.CreateAsync(client, model).ConfigureAwait(false);
|
return await RestAutocompleteInteraction.CreateAsync(client, model).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (model.Type == InteractionType.ModalSubmit)
|
||||||
|
return await RestModal.CreateAsync(client, model).ConfigureAwait(false);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,6 +183,9 @@ namespace Discord.Rest
|
|||||||
var model = await InteractionHelper.ModifyInteractionResponseAsync(Discord, Token, func, options);
|
var model = await InteractionHelper.ModifyInteractionResponseAsync(Discord, Token, func, options);
|
||||||
return RestInteractionMessage.Create(Discord, model, Token, Channel);
|
return RestInteractionMessage.Create(Discord, model, Token, Channel);
|
||||||
}
|
}
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public abstract string RespondWithModal(Modal modal, RequestOptions options = null);
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
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);
|
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 IDiscordInteraction.DeferAsync(bool ephemeral, RequestOptions options)
|
||||||
=> Task.FromResult(Defer(ephemeral, options));
|
=> Task.FromResult(Defer(ephemeral, options));
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
|
Task IDiscordInteraction.RespondWithModalAsync(Modal modal, RequestOptions options)
|
||||||
|
=> Task.FromResult(RespondWithModal(modal, options));
|
||||||
|
/// <inheritdoc/>
|
||||||
async Task<IUserMessage> IDiscordInteraction.FollowupAsync(string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions,
|
async Task<IUserMessage> IDiscordInteraction.FollowupAsync(string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions,
|
||||||
MessageComponent components, Embed embed, RequestOptions options)
|
MessageComponent components, Embed embed, RequestOptions options)
|
||||||
=> await FollowupAsync(text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false);
|
=> await FollowupAsync(text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ namespace Discord.Rest
|
|||||||
}
|
}
|
||||||
|
|
||||||
public override string Defer(bool ephemeral = false, RequestOptions options = null) => throw new NotSupportedException();
|
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 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<RestFollowupMessage> 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<RestFollowupMessage> 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<RestFollowupMessage> 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();
|
public override Task<RestFollowupMessage> 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();
|
||||||
|
|||||||
@@ -112,7 +112,8 @@ namespace Discord.Rest
|
|||||||
=> throw new NotSupportedException("Autocomplete interactions don't support this method!");
|
=> throw new NotSupportedException("Autocomplete interactions don't support this method!");
|
||||||
public override Task<RestFollowupMessage> FollowupWithFilesAsync(IEnumerable<FileAttachment> 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)
|
public override Task<RestFollowupMessage> FollowupWithFilesAsync(IEnumerable<FileAttachment> 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!");
|
=> 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
|
//IAutocompleteInteraction
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
|
|||||||
@@ -56,6 +56,13 @@ namespace Discord.Net.Converters
|
|||||||
interaction.Data = autocompleteData;
|
interaction.Data = autocompleteData;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case InteractionType.ModalSubmit:
|
||||||
|
{
|
||||||
|
var modalData = new API.ModalInteractionData();
|
||||||
|
serializer.Populate(result.CreateReader(), modalData);
|
||||||
|
interaction.Data = modalData;
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ namespace Discord.Net.Converters
|
|||||||
case ComponentType.SelectMenu:
|
case ComponentType.SelectMenu:
|
||||||
messageComponent = new API.SelectMenuComponent();
|
messageComponent = new API.SelectMenuComponent();
|
||||||
break;
|
break;
|
||||||
|
case ComponentType.TextInput:
|
||||||
|
messageComponent = new API.TextInputComponent();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
serializer.Populate(jsonObject.CreateReader(), messageComponent);
|
serializer.Populate(jsonObject.CreateReader(), messageComponent);
|
||||||
return messageComponent;
|
return messageComponent;
|
||||||
|
|||||||
@@ -634,6 +634,15 @@ namespace Discord.WebSocket
|
|||||||
remove => _autocompleteExecuted.Remove(value);
|
remove => _autocompleteExecuted.Remove(value);
|
||||||
}
|
}
|
||||||
internal readonly AsyncEvent<Func<SocketAutocompleteInteraction, Task>> _autocompleteExecuted = new AsyncEvent<Func<SocketAutocompleteInteraction, Task>>();
|
internal readonly AsyncEvent<Func<SocketAutocompleteInteraction, Task>> _autocompleteExecuted = new AsyncEvent<Func<SocketAutocompleteInteraction, Task>>();
|
||||||
|
/// <summary>
|
||||||
|
/// Fired when a modal is submitted.
|
||||||
|
/// </summary>
|
||||||
|
public event Func<SocketModal, Task> ModalSubmitted
|
||||||
|
{
|
||||||
|
add => _modalSubmitted.Add(value);
|
||||||
|
remove => _modalSubmitted.Remove(value);
|
||||||
|
}
|
||||||
|
internal readonly AsyncEvent<Func<SocketModal, Task>> _modalSubmitted = new AsyncEvent<Func<SocketModal, Task>>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fired when a guild application command is created.
|
/// Fired when a guild application command is created.
|
||||||
|
|||||||
@@ -468,6 +468,7 @@ namespace Discord.WebSocket
|
|||||||
client.UserCommandExecuted += (arg) => _userCommandExecuted.InvokeAsync(arg);
|
client.UserCommandExecuted += (arg) => _userCommandExecuted.InvokeAsync(arg);
|
||||||
client.MessageCommandExecuted += (arg) => _messageCommandExecuted.InvokeAsync(arg);
|
client.MessageCommandExecuted += (arg) => _messageCommandExecuted.InvokeAsync(arg);
|
||||||
client.AutocompleteExecuted += (arg) => _autocompleteExecuted.InvokeAsync(arg);
|
client.AutocompleteExecuted += (arg) => _autocompleteExecuted.InvokeAsync(arg);
|
||||||
|
client.ModalSubmitted += (arg) => _modalSubmitted.InvokeAsync(arg);
|
||||||
|
|
||||||
client.ThreadUpdated += (thread1, thread2) => _threadUpdated.InvokeAsync(thread1, thread2);
|
client.ThreadUpdated += (thread1, thread2) => _threadUpdated.InvokeAsync(thread1, thread2);
|
||||||
client.ThreadCreated += (thread) => _threadCreated.InvokeAsync(thread);
|
client.ThreadCreated += (thread) => _threadCreated.InvokeAsync(thread);
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ namespace Discord.API
|
|||||||
if (msg != null)
|
if (msg != null)
|
||||||
{
|
{
|
||||||
#if DEBUG_PACKETS
|
#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
|
#endif
|
||||||
|
|
||||||
await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false);
|
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 (msg != null)
|
||||||
{
|
{
|
||||||
#if DEBUG_PACKETS
|
#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
|
#endif
|
||||||
|
|
||||||
await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false);
|
await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -2274,6 +2274,9 @@ namespace Discord.WebSocket
|
|||||||
case SocketAutocompleteInteraction autocomplete:
|
case SocketAutocompleteInteraction autocomplete:
|
||||||
await TimedInvokeAsync(_autocompleteExecuted, nameof(AutocompleteExecuted), autocomplete).ConfigureAwait(false);
|
await TimedInvokeAsync(_autocompleteExecuted, nameof(AutocompleteExecuted), autocomplete).ConfigureAwait(false);
|
||||||
break;
|
break;
|
||||||
|
case SocketModal modal:
|
||||||
|
await TimedInvokeAsync(_modalSubmitted, nameof(ModalSubmitted), modal).ConfigureAwait(false);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -438,6 +438,41 @@ namespace Discord.WebSocket
|
|||||||
HasResponded = true;
|
HasResponded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
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
|
//IComponentInteraction
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
IComponentInteractionData IComponentInteraction.Data => Data;
|
IComponentInteractionData IComponentInteraction.Data => Data;
|
||||||
|
|||||||
@@ -23,11 +23,31 @@ namespace Discord.WebSocket
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyCollection<string> Values { get; }
|
public IReadOnlyCollection<string> Values { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the value of a <see cref="TextInputComponent"/> interaction response.
|
||||||
|
/// </summary>
|
||||||
|
public string Value { get; }
|
||||||
|
|
||||||
internal SocketMessageComponentData(Model model)
|
internal SocketMessageComponentData(Model model)
|
||||||
{
|
{
|
||||||
CustomId = model.CustomId;
|
CustomId = model.CustomId;
|
||||||
Type = model.ComponentType;
|
Type = model.ComponentType;
|
||||||
Values = model.Values.GetValueOrDefault();
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a user submitted <see cref="Discord.Modal"/> received via GateWay.
|
||||||
|
/// </summary>
|
||||||
|
public class SocketModal : SocketInteraction, IDiscordInteraction, IModalInteraction
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The data for this <see cref="Modal"/> interaction.
|
||||||
|
/// </summary>
|
||||||
|
/// <value></value>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override bool HasResponded { get; internal set; }
|
||||||
|
private object _lock = new object();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override async Task RespondWithFilesAsync(
|
||||||
|
IEnumerable<FileAttachment> 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<Embed>();
|
||||||
|
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<string>.Unspecified,
|
||||||
|
AllowedMentions = allowedMentions != null ? allowedMentions?.ToModel() : Optional<API.AllowedMentions>.Unspecified,
|
||||||
|
Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional<API.Embed[]>.Unspecified,
|
||||||
|
IsTTS = isTTS,
|
||||||
|
MessageComponents = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional<API.ActionRowComponent[]>.Unspecified,
|
||||||
|
Flags = ephemeral ? MessageFlags.Ephemeral : Optional<MessageFlags>.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
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<Embed>();
|
||||||
|
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<string>.Unspecified,
|
||||||
|
AllowedMentions = allowedMentions?.ToModel(),
|
||||||
|
Embeds = embeds.Select(x => x.ToModel()).ToArray(),
|
||||||
|
TTS = isTTS,
|
||||||
|
Flags = ephemeral ? MessageFlags.Ephemeral : Optional<MessageFlags>.Unspecified,
|
||||||
|
Components = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional<API.ActionRowComponent[]>.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override async Task<RestFollowupMessage> 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<Embed>();
|
||||||
|
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<API.AllowedMentions>.Unspecified,
|
||||||
|
IsTTS = isTTS,
|
||||||
|
Embeds = embeds.Select(x => x.ToModel()).ToArray(),
|
||||||
|
Components = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional<API.ActionRowComponent[]>.Unspecified
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ephemeral)
|
||||||
|
args.Flags = MessageFlags.Ephemeral;
|
||||||
|
|
||||||
|
return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override async Task<RestFollowupMessage> FollowupWithFilesAsync(
|
||||||
|
IEnumerable<FileAttachment> 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<Embed>();
|
||||||
|
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<API.Embed[]>.Unspecified, AllowedMentions = allowedMentions?.ToModel() ?? Optional<API.AllowedMentions>.Unspecified, MessageComponents = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional<API.ActionRowComponent[]>.Unspecified };
|
||||||
|
return await InteractionHelper.SendFollowupAsync(Discord, args, Token, Channel, options).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
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<API.InteractionCallbackData>.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents data sent from a <see cref="InteractionType.ModalSubmit"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class SocketModalData : IDiscordInteractionData, IModalInteractionData
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="Modal"/>'s Custom Id.
|
||||||
|
/// </summary>
|
||||||
|
public string CustomId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="Modal"/>'s components submitted by the user.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<SocketMessageComponentData> Components { get; }
|
||||||
|
|
||||||
|
internal SocketModalData(Model model)
|
||||||
|
{
|
||||||
|
CustomId = model.CustomId;
|
||||||
|
Components = model.Components
|
||||||
|
.SelectMany(x => x.Components)
|
||||||
|
.Select(x => new SocketMessageComponentData(x))
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
IReadOnlyCollection<IComponentInteractionData> IModalInteractionData.Components => Components;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -100,6 +100,10 @@ namespace Discord.WebSocket
|
|||||||
public override Task RespondWithFilesAsync(IEnumerable<FileAttachment> 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)
|
public override Task RespondWithFilesAsync(IEnumerable<FileAttachment> 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!");
|
=> throw new NotSupportedException("Autocomplete interactions don't support this method!");
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override Task RespondWithModalAsync(Modal modal, RequestOptions requestOptions = null)
|
||||||
|
=> throw new NotSupportedException("Autocomplete interactions cannot have normal responces!");
|
||||||
|
|
||||||
//IAutocompleteInteraction
|
//IAutocompleteInteraction
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
IAutocompleteInteractionData IAutocompleteInteraction.Data => Data;
|
IAutocompleteInteractionData IAutocompleteInteraction.Data => Data;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
using Discord.Net.Rest;
|
|
||||||
using Discord.Rest;
|
using Discord.Rest;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -135,6 +134,42 @@ namespace Discord.WebSocket
|
|||||||
HasResponded = true;
|
HasResponded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
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(
|
public override async Task RespondWithFilesAsync(
|
||||||
IEnumerable<FileAttachment> attachments,
|
IEnumerable<FileAttachment> attachments,
|
||||||
string text = null,
|
string text = null,
|
||||||
|
|||||||
@@ -108,6 +108,9 @@ namespace Discord.WebSocket
|
|||||||
if (model.Type == InteractionType.ApplicationCommandAutocomplete)
|
if (model.Type == InteractionType.ApplicationCommandAutocomplete)
|
||||||
return SocketAutocompleteInteraction.Create(client, model, channel);
|
return SocketAutocompleteInteraction.Create(client, model, channel);
|
||||||
|
|
||||||
|
if (model.Type == InteractionType.ModalSubmit)
|
||||||
|
return SocketModal.Create(client, model, channel);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,6 +390,13 @@ namespace Discord.WebSocket
|
|||||||
/// </returns>
|
/// </returns>
|
||||||
public abstract Task DeferAsync(bool ephemeral = false, RequestOptions options = null);
|
public abstract Task DeferAsync(bool ephemeral = false, RequestOptions options = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Responds to this interaction with a <see cref="Modal"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="modal">The <see cref="Modal"/> to respond with.</param>
|
||||||
|
/// <param name="options">The request options for this <see langword="async"/> request.</param>
|
||||||
|
/// <returns>A task that represents the asynchronous operation of responding to the interaction.</returns>
|
||||||
|
public abstract Task RespondWithModalAsync(Modal modal, RequestOptions options = null);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region IDiscordInteraction
|
#region IDiscordInteraction
|
||||||
|
|||||||