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>
This commit is contained in:
Quin Lynch
2022-02-09 00:17:56 -04:00
committed by GitHub
parent 33efd8981d
commit c8f175e11a
80 changed files with 3502 additions and 25 deletions

View File

@@ -21,6 +21,7 @@ namespace Discord.API
{
ComponentType.Button => new ButtonComponent(x as Discord.ButtonComponent),
ComponentType.SelectMenu => new SelectMenuComponent(x as Discord.SelectMenuComponent),
ComponentType.TextInput => new TextInputComponent(x as Discord.TextInputComponent),
_ => null
};
}).ToArray();

View File

@@ -24,5 +24,11 @@ namespace Discord.API
[JsonProperty("choices")]
public Optional<ApplicationCommandOptionChoice[]> Choices { get; set; }
[JsonProperty("title")]
public Optional<string> Title { get; set; }
[JsonProperty("custom_id")]
public Optional<string> CustomId { get; set; }
}
}

View File

@@ -12,5 +12,8 @@ namespace Discord.API
[JsonProperty("values")]
public Optional<string[]> Values { get; set; }
[JsonProperty("value")]
public Optional<string> Value { get; set; }
}
}

View 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; }
}
}

View File

@@ -26,6 +26,8 @@ namespace Discord.API
[JsonProperty("disabled")]
public bool Disabled { get; set; }
[JsonProperty("values")]
public Optional<string[]> Values { get; set; }
public SelectMenuComponent() { }
public SelectMenuComponent(Discord.SelectMenuComponent component)

View 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;
}
}
}

View File

@@ -316,5 +316,45 @@ namespace Discord.Rest
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);
}
}
}

View File

@@ -446,6 +446,46 @@ namespace Discord.Rest
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
/// <inheritdoc/>
IComponentInteractionData IComponentInteraction.Data => Data;

View File

@@ -27,11 +27,26 @@ namespace Discord.Rest
/// </summary>
public IReadOnlyCollection<string> Values { get; }
/// <inheritdoc/>
public string Value { get; }
internal RestMessageComponentData(Model model)
{
CustomId = model.CustomId;
Type = model.ComponentType;
Values = model.Values.GetValueOrDefault();
}
internal RestMessageComponentData(IMessageComponent component)
{
CustomId = component.CustomId;
Type = component.Type;
if (component is API.TextInputComponent textInput)
Value = textInput.Value.Value;
if (component is API.SelectMenuComponent select)
Values = select.Values.Value;
}
}
}

View 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;
}
}

View File

@@ -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();
}
}
}

View File

@@ -100,6 +100,9 @@ namespace Discord.Rest
if (model.Type == InteractionType.ApplicationCommandAutocomplete)
return await RestAutocompleteInteraction.CreateAsync(client, model).ConfigureAwait(false);
if (model.Type == InteractionType.ModalSubmit)
return await RestModal.CreateAsync(client, model).ConfigureAwait(false);
return null;
}
@@ -180,6 +183,9 @@ namespace Discord.Rest
var model = await InteractionHelper.ModifyInteractionResponseAsync(Discord, Token, func, options);
return RestInteractionMessage.Create(Discord, model, Token, Channel);
}
/// <inheritdoc/>
public abstract string RespondWithModal(Modal modal, RequestOptions options = null);
/// <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);
@@ -294,6 +300,9 @@ namespace Discord.Rest
Task IDiscordInteraction.DeferAsync(bool ephemeral, RequestOptions options)
=> Task.FromResult(Defer(ephemeral, options));
/// <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,
MessageComponent components, Embed embed, RequestOptions options)
=> await FollowupAsync(text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false);

View File

@@ -36,6 +36,7 @@ namespace Discord.Rest
}
public override string Defer(bool ephemeral = false, RequestOptions options = null) => throw new NotSupportedException();
public override string RespondWithModal(Modal modal, RequestOptions options = null) => throw new NotSupportedException();
public override string Respond(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null) => throw new NotSupportedException();
public override Task<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();

View File

@@ -112,7 +112,8 @@ namespace Discord.Rest
=> 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)
=> 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
/// <inheritdoc/>

View File

@@ -56,6 +56,13 @@ namespace Discord.Net.Converters
interaction.Data = autocompleteData;
}
break;
case InteractionType.ModalSubmit:
{
var modalData = new API.ModalInteractionData();
serializer.Populate(result.CreateReader(), modalData);
interaction.Data = modalData;
}
break;
}
}
else

View File

@@ -32,6 +32,9 @@ namespace Discord.Net.Converters
case ComponentType.SelectMenu:
messageComponent = new API.SelectMenuComponent();
break;
case ComponentType.TextInput:
messageComponent = new API.TextInputComponent();
break;
}
serializer.Populate(jsonObject.CreateReader(), messageComponent);
return messageComponent;