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:
@@ -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();
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,5 +12,8 @@ namespace Discord.API
|
||||
|
||||
[JsonProperty("values")]
|
||||
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
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")]
|
||||
public bool Disabled { get; set; }
|
||||
|
||||
[JsonProperty("values")]
|
||||
public Optional<string[]> Values { get; set; }
|
||||
public SelectMenuComponent() { }
|
||||
|
||||
public SelectMenuComponent(Discord.SelectMenuComponent component)
|
||||
|
||||
49
src/Discord.Net.Rest/API/Common/TextInputComponent.cs
Normal file
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);
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
|
||||
/// <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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
402
src/Discord.Net.Rest/Entities/Interactions/Modals/RestModal.cs
Normal file
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)
|
||||
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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user