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:
@@ -634,6 +634,15 @@ namespace Discord.WebSocket
|
||||
remove => _autocompleteExecuted.Remove(value);
|
||||
}
|
||||
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>
|
||||
/// Fired when a guild application command is created.
|
||||
|
||||
@@ -468,6 +468,7 @@ namespace Discord.WebSocket
|
||||
client.UserCommandExecuted += (arg) => _userCommandExecuted.InvokeAsync(arg);
|
||||
client.MessageCommandExecuted += (arg) => _messageCommandExecuted.InvokeAsync(arg);
|
||||
client.AutocompleteExecuted += (arg) => _autocompleteExecuted.InvokeAsync(arg);
|
||||
client.ModalSubmitted += (arg) => _modalSubmitted.InvokeAsync(arg);
|
||||
|
||||
client.ThreadUpdated += (thread1, thread2) => _threadUpdated.InvokeAsync(thread1, thread2);
|
||||
client.ThreadCreated += (thread) => _threadCreated.InvokeAsync(thread);
|
||||
|
||||
@@ -78,7 +78,7 @@ namespace Discord.API
|
||||
if (msg != null)
|
||||
{
|
||||
#if DEBUG_PACKETS
|
||||
Console.WriteLine($"<- {(GatewayOpCode)msg.Operation} [{msg.Type ?? "none"}] : {(msg.Payload as Newtonsoft.Json.Linq.JToken)?.ToString().Length}");
|
||||
Console.WriteLine($"<- {(GatewayOpCode)msg.Operation} [{msg.Type ?? "none"}] : {(msg.Payload as Newtonsoft.Json.Linq.JToken)}");
|
||||
#endif
|
||||
|
||||
await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false);
|
||||
@@ -95,7 +95,7 @@ namespace Discord.API
|
||||
if (msg != null)
|
||||
{
|
||||
#if DEBUG_PACKETS
|
||||
Console.WriteLine($"<- {(GatewayOpCode)msg.Operation} [{msg.Type ?? "none"}] : {(msg.Payload as Newtonsoft.Json.Linq.JToken)?.ToString().Length}");
|
||||
Console.WriteLine($"<- {(GatewayOpCode)msg.Operation} [{msg.Type ?? "none"}] : {(msg.Payload as Newtonsoft.Json.Linq.JToken)}");
|
||||
#endif
|
||||
|
||||
await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false);
|
||||
|
||||
@@ -2274,6 +2274,9 @@ namespace Discord.WebSocket
|
||||
case SocketAutocompleteInteraction autocomplete:
|
||||
await TimedInvokeAsync(_autocompleteExecuted, nameof(AutocompleteExecuted), autocomplete).ConfigureAwait(false);
|
||||
break;
|
||||
case SocketModal modal:
|
||||
await TimedInvokeAsync(_modalSubmitted, nameof(ModalSubmitted), modal).ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -438,6 +438,41 @@ namespace Discord.WebSocket
|
||||
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
|
||||
/// <inheritdoc/>
|
||||
IComponentInteractionData IComponentInteraction.Data => Data;
|
||||
|
||||
@@ -23,11 +23,31 @@ namespace Discord.WebSocket
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
CustomId = model.CustomId;
|
||||
Type = model.ComponentType;
|
||||
Values = model.Values.GetValueOrDefault();
|
||||
Value = model.Value.GetValueOrDefault();
|
||||
}
|
||||
|
||||
internal SocketMessageComponentData(IMessageComponent component)
|
||||
{
|
||||
CustomId = component.CustomId;
|
||||
Type = component.Type;
|
||||
|
||||
Value = component.Type == ComponentType.TextInput
|
||||
? (component as API.TextInputComponent).Value.Value
|
||||
: null;
|
||||
|
||||
Values = component.Type == ComponentType.SelectMenu
|
||||
? (component as API.SelectMenuComponent).Values.Value
|
||||
: null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
=> 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
|
||||
/// <inheritdoc/>
|
||||
IAutocompleteInteractionData IAutocompleteInteraction.Data => Data;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using Discord.Net.Rest;
|
||||
using Discord.Rest;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -135,6 +134,42 @@ namespace Discord.WebSocket
|
||||
HasResponded = true;
|
||||
}
|
||||
|
||||
/// <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(
|
||||
IEnumerable<FileAttachment> attachments,
|
||||
string text = null,
|
||||
|
||||
@@ -108,6 +108,9 @@ namespace Discord.WebSocket
|
||||
if (model.Type == InteractionType.ApplicationCommandAutocomplete)
|
||||
return SocketAutocompleteInteraction.Create(client, model, channel);
|
||||
|
||||
if (model.Type == InteractionType.ModalSubmit)
|
||||
return SocketModal.Create(client, model, channel);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -387,6 +390,13 @@ namespace Discord.WebSocket
|
||||
/// </returns>
|
||||
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
|
||||
|
||||
#region IDiscordInteraction
|
||||
|
||||
Reference in New Issue
Block a user