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