@@ -26,9 +26,9 @@ namespace Discord | |||
InteractionType Type { get; } | |||
/// <summary> | |||
/// The command data payload. | |||
/// Represents the data sent within this interaction. | |||
/// </summary> | |||
IApplicationCommandInteractionData? Data { get; } | |||
object Data { get; } | |||
/// <summary> | |||
/// A continuation token for responding to the interaction. | |||
@@ -42,6 +42,16 @@ namespace Discord | |||
/// <summary> | |||
/// ACK an interaction and edit a response later, the user sees a loading state. | |||
/// </summary> | |||
DeferredChannelMessageWithSource = 5 | |||
DeferredChannelMessageWithSource = 5, | |||
/// <summary> | |||
/// for components: ACK an interaction and edit the original message later; the user does not see a loading state | |||
/// </summary> | |||
DeferredUpdateMessage = 6, | |||
/// <summary> | |||
/// for components: edit the message the component was attached to | |||
/// </summary> | |||
UpdateMessage = 7 | |||
} | |||
} |
@@ -17,8 +17,13 @@ namespace Discord | |||
Ping = 1, | |||
/// <summary> | |||
/// An <see cref="IApplicationCommand"/> sent from discord. | |||
/// A <see cref="IApplicationCommand"/> sent from discord. | |||
/// </summary> | |||
ApplicationCommand = 2 | |||
ApplicationCommand = 2, | |||
/// <summary> | |||
/// A <see cref="IMessageComponent"/> sent from discord. | |||
/// </summary> | |||
MessageComponent = 3, | |||
} | |||
} |
@@ -1,3 +1,4 @@ | |||
using Newtonsoft.Json; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
@@ -8,8 +9,10 @@ namespace Discord | |||
{ | |||
public class ActionRowComponent : IMessageComponent | |||
{ | |||
[JsonProperty("type")] | |||
public ComponentType Type { get; } = ComponentType.ActionRow; | |||
[JsonProperty("components")] | |||
public IReadOnlyCollection<IMessageComponent> Components { get; internal set; } | |||
internal ActionRowComponent() { } | |||
@@ -1,3 +1,4 @@ | |||
using Newtonsoft.Json; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
@@ -8,18 +9,25 @@ namespace Discord | |||
{ | |||
public class ButtonComponent : IMessageComponent | |||
{ | |||
[JsonProperty("type")] | |||
public ComponentType Type { get; } = ComponentType.Button; | |||
[JsonProperty("style")] | |||
public ButtonStyle Style { get; } | |||
[JsonProperty("label")] | |||
public string Label { get; } | |||
[JsonProperty("emoji")] | |||
public IEmote Emote { get; } | |||
[JsonProperty("custom_id")] | |||
public string CustomId { get; } | |||
[JsonProperty("url")] | |||
public string Url { get; } | |||
[JsonProperty("disabled")] | |||
public bool Disabled { get; } | |||
internal ButtonComponent(ButtonStyle style, string label, IEmote emote, string customId, string url, bool disabled) | |||
@@ -25,6 +25,9 @@ namespace Discord.API | |||
[JsonProperty("flags")] | |||
public Optional<int> Flags { get; set; } | |||
[JsonProperty("components")] | |||
public Optional<IMessageComponent[]> Components { get; set; } | |||
public InteractionApplicationCommandCallbackData() { } | |||
public InteractionApplicationCommandCallbackData(string text) | |||
{ | |||
@@ -0,0 +1,18 @@ | |||
using Newtonsoft.Json; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Discord.API | |||
{ | |||
internal class MessageComponentInteractionData | |||
{ | |||
[JsonProperty("custom_id")] | |||
public string CustomId { get; set; } | |||
[JsonProperty("component_type")] | |||
public ComponentType ComponentType { get; set; } | |||
} | |||
} |
@@ -30,6 +30,9 @@ namespace Discord.API.Rest | |||
[JsonProperty("flags")] | |||
public Optional<int> Flags { get; set; } | |||
[JsonProperty("components")] | |||
public Optional<IMessageComponent[]> Components { get; set; } | |||
public CreateWebhookMessageParams(string content) | |||
{ | |||
Content = content; | |||
@@ -17,7 +17,7 @@ namespace Discord.API.Gateway | |||
public InteractionType Type { get; set; } | |||
[JsonProperty("data")] | |||
public Optional<ApplicationCommandInteractionData> Data { get; set; } | |||
public Optional<object> Data { get; set; } | |||
[JsonProperty("guild_id")] | |||
public ulong GuildId { get; set; } | |||
@@ -0,0 +1,156 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
using Model = Discord.API.Gateway.InteractionCreated; | |||
using DataModel = Discord.API.MessageComponentInteractionData; | |||
using Newtonsoft.Json.Linq; | |||
using Discord.Rest; | |||
namespace Discord.WebSocket | |||
{ | |||
public class SocketMessageComponent : SocketInteraction | |||
{ | |||
new public SocketMessageComponentData Data { get; } | |||
internal SocketMessageComponent(DiscordSocketClient client, Model model) | |||
: base(client, model.Id) | |||
{ | |||
var dataModel = model.Data.IsSpecified ? | |||
(model.Data.Value as JToken).ToObject<DataModel>() | |||
: null; | |||
this.Data = new SocketMessageComponentData(dataModel); | |||
} | |||
new internal static SocketMessageComponent Create(DiscordSocketClient client, Model model) | |||
{ | |||
var entity = new SocketMessageComponent(client, model); | |||
entity.Update(model); | |||
return entity; | |||
} | |||
/// <summary> | |||
/// Responds to an Interaction. | |||
/// <para> | |||
/// If you have <see cref="DiscordSocketConfig.AlwaysAcknowledgeInteractions"/> set to <see langword="true"/>, You should use | |||
/// <see cref="FollowupAsync(string, bool, Embed, InteractionResponseType, AllowedMentions, RequestOptions)"/> instead. | |||
/// </para> | |||
/// </summary> | |||
/// <param name="text">The text of the message to be sent.</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="embed">A <see cref="Embed"/> to send with this response.</param> | |||
/// <param name="type">The type of response to this Interaction.</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> | |||
/// <returns> | |||
/// The <see cref="IMessage"/> sent as the response. If this is the first acknowledgement, it will return null. | |||
/// </returns> | |||
/// <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> | |||
public override async Task<RestUserMessage> RespondAsync(string text = null, bool isTTS = false, Embed embed = null, InteractionResponseType type = InteractionResponseType.ChannelMessageWithSource, | |||
bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null) | |||
{ | |||
if (type == InteractionResponseType.Pong) | |||
throw new InvalidOperationException($"Cannot use {Type} on a send message function"); | |||
if (!IsValidToken) | |||
throw new InvalidOperationException("Interaction token is no longer valid"); | |||
if (Discord.AlwaysAcknowledgeInteractions) | |||
return await FollowupAsync(text, isTTS, embed, ephemeral, type, allowedMentions, options); | |||
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."); | |||
// 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 = type, | |||
Data = new API.InteractionApplicationCommandCallbackData(text) | |||
{ | |||
AllowedMentions = allowedMentions?.ToModel(), | |||
Embeds = embed != null | |||
? new API.Embed[] { embed.ToModel() } | |||
: Optional<API.Embed[]>.Unspecified, | |||
TTS = isTTS, | |||
Components = component?.ToModel() ?? Optional<IMessageComponent[]>.Unspecified | |||
} | |||
}; | |||
if (ephemeral) | |||
response.Data.Value.Flags = 64; | |||
return await InteractionHelper.SendInteractionResponse(this.Discord, this.Channel, response, this.Id, Token, options); | |||
} | |||
/// <summary> | |||
/// Sends a followup message for this interaction. | |||
/// </summary> | |||
/// <param name="text">The text of the message to be sent</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="embed">A <see cref="Embed"/> to send with this response.</param> | |||
/// <param name="type">The type of response to this Interaction.</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> | |||
/// <returns> | |||
/// The sent message. | |||
/// </returns> | |||
public override async Task<RestFollowupMessage> FollowupAsync(string text = null, bool isTTS = false, Embed embed = null, bool ephemeral = false, | |||
InteractionResponseType type = InteractionResponseType.ChannelMessageWithSource, | |||
AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null) | |||
{ | |||
if (type == InteractionResponseType.DeferredChannelMessageWithSource || type == InteractionResponseType.DeferredChannelMessageWithSource || type == InteractionResponseType.Pong) | |||
throw new InvalidOperationException($"Cannot use {type} on a slash command!"); | |||
if (!IsValidToken) | |||
throw new InvalidOperationException("Interaction token is no longer valid"); | |||
var args = new API.Rest.CreateWebhookMessageParams(text) | |||
{ | |||
IsTTS = isTTS, | |||
Embeds = embed != null | |||
? new API.Embed[] { embed.ToModel() } | |||
: Optional<API.Embed[]>.Unspecified, | |||
Components = component?.ToModel() ?? Optional<IMessageComponent[]>.Unspecified | |||
}; | |||
if (ephemeral) | |||
args.Flags = 64; | |||
return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options); | |||
} | |||
public override Task AcknowledgeAsync(RequestOptions options = null) | |||
{ | |||
var response = new API.InteractionResponse() | |||
{ | |||
Type = InteractionResponseType.DeferredUpdateMessage, | |||
}; | |||
return Discord.Rest.ApiClient.CreateInteractionResponse(response, this.Id, this.Token, options); | |||
} | |||
} | |||
} |
@@ -0,0 +1,28 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
using Model = Discord.API.MessageComponentInteractionData; | |||
namespace Discord.WebSocket | |||
{ | |||
public class SocketMessageComponentData | |||
{ | |||
/// <summary> | |||
/// The components Custom Id that was clicked | |||
/// </summary> | |||
public string CustomId { get; } | |||
/// <summary> | |||
/// The type of the component clicked | |||
/// </summary> | |||
public ComponentType Type { get; } | |||
internal SocketMessageComponentData(Model model) | |||
{ | |||
this.CustomId = model.CustomId; | |||
this.Type = model.ComponentType; | |||
} | |||
} | |||
} |
@@ -0,0 +1,161 @@ | |||
using Discord.Rest; | |||
using Newtonsoft.Json.Linq; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
using Model = Discord.API.Gateway.InteractionCreated; | |||
using DataModel = Discord.API.ApplicationCommandInteractionData; | |||
namespace Discord.WebSocket | |||
{ | |||
public class SocketSlashCommand : SocketInteraction | |||
{ | |||
/// <summary> | |||
/// The data associated with this interaction. | |||
/// </summary> | |||
new public SocketSlashCommandData Data { get; private set; } | |||
internal SocketSlashCommand(DiscordSocketClient client, Model model) | |||
: base(client, model.Id) | |||
{ | |||
var dataModel = model.Data.IsSpecified ? | |||
(model.Data.Value as JToken).ToObject<DataModel>() | |||
: null; | |||
Data = SocketSlashCommandData.Create(client, dataModel, model.GuildId); | |||
} | |||
new internal static SocketInteraction Create(DiscordSocketClient client, Model model) | |||
{ | |||
var entity = new SocketSlashCommand(client, model); | |||
entity.Update(model); | |||
return entity; | |||
} | |||
internal override void Update(Model model) | |||
{ | |||
var data = model.Data.IsSpecified ? | |||
(model.Data.Value as JToken).ToObject<DataModel>() | |||
: null; | |||
this.Data.Update(data, this.Guild.Id); | |||
base.Update(model); | |||
} | |||
/// <summary> | |||
/// Responds to an Interaction. | |||
/// <para> | |||
/// If you have <see cref="DiscordSocketConfig.AlwaysAcknowledgeInteractions"/> set to <see langword="true"/>, You should use | |||
/// <see cref="FollowupAsync(string, bool, Embed, InteractionResponseType, AllowedMentions, RequestOptions)"/> instead. | |||
/// </para> | |||
/// </summary> | |||
/// <param name="text">The text of the message to be sent.</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="embed">A <see cref="Embed"/> to send with this response.</param> | |||
/// <param name="type">The type of response to this Interaction.</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> | |||
/// <returns> | |||
/// The <see cref="IMessage"/> sent as the response. If this is the first acknowledgement, it will return null. | |||
/// </returns> | |||
/// <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> | |||
public override async Task<RestUserMessage> RespondAsync(string text = null, bool isTTS = false, Embed embed = null, InteractionResponseType type = InteractionResponseType.ChannelMessageWithSource, | |||
bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null) | |||
{ | |||
if (type == InteractionResponseType.Pong) | |||
throw new InvalidOperationException($"Cannot use {Type} on a send message function"); | |||
if(type == InteractionResponseType.DeferredUpdateMessage || type == InteractionResponseType.UpdateMessage) | |||
throw new InvalidOperationException($"Cannot use {Type} on a slash command!"); | |||
if (!IsValidToken) | |||
throw new InvalidOperationException("Interaction token is no longer valid"); | |||
if (Discord.AlwaysAcknowledgeInteractions) | |||
return await FollowupAsync(text, isTTS, embed, ephemeral, type, allowedMentions, options); // The arguments should be passed? What was i thinking... | |||
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."); | |||
// 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 = type, | |||
Data = new API.InteractionApplicationCommandCallbackData(text) | |||
{ | |||
AllowedMentions = allowedMentions?.ToModel(), | |||
Embeds = embed != null | |||
? new API.Embed[] { embed.ToModel() } | |||
: Optional<API.Embed[]>.Unspecified, | |||
TTS = isTTS, | |||
Components = component?.ToModel() ?? Optional<IMessageComponent[]>.Unspecified | |||
} | |||
}; | |||
if (ephemeral) | |||
response.Data.Value.Flags = 64; | |||
return await InteractionHelper.SendInteractionResponse(this.Discord, this.Channel, response, this.Id, Token, options); | |||
} | |||
/// <summary> | |||
/// Sends a followup message for this interaction. | |||
/// </summary> | |||
/// <param name="text">The text of the message to be sent</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="embed">A <see cref="Embed"/> to send with this response.</param> | |||
/// <param name="type">The type of response to this Interaction.</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> | |||
/// <returns> | |||
/// The sent message. | |||
/// </returns> | |||
public override async Task<RestFollowupMessage> FollowupAsync(string text = null, bool isTTS = false, Embed embed = null, bool ephemeral = false, | |||
InteractionResponseType type = InteractionResponseType.ChannelMessageWithSource, | |||
AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null) | |||
{ | |||
if (type == InteractionResponseType.DeferredChannelMessageWithSource || type == InteractionResponseType.DeferredChannelMessageWithSource || type == InteractionResponseType.Pong || type == InteractionResponseType.DeferredUpdateMessage || type == InteractionResponseType.UpdateMessage) | |||
throw new InvalidOperationException($"Cannot use {type} on a slash command!"); | |||
if (!IsValidToken) | |||
throw new InvalidOperationException("Interaction token is no longer valid"); | |||
var args = new API.Rest.CreateWebhookMessageParams(text) | |||
{ | |||
IsTTS = isTTS, | |||
Embeds = embed != null | |||
? new API.Embed[] { embed.ToModel() } | |||
: Optional<API.Embed[]>.Unspecified, | |||
Components = component?.ToModel() ?? Optional<IMessageComponent[]>.Unspecified | |||
}; | |||
if (ephemeral) | |||
args.Flags = 64; | |||
return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options); | |||
} | |||
} | |||
} |
@@ -8,27 +8,27 @@ using Model = Discord.API.ApplicationCommandInteractionData; | |||
namespace Discord.WebSocket | |||
{ | |||
public class SocketInteractionData : SocketEntity<ulong>, IApplicationCommandInteractionData | |||
public class SocketSlashCommandData : SocketEntity<ulong>, IApplicationCommandInteractionData | |||
{ | |||
/// <inheritdoc/> | |||
public string Name { get; private set; } | |||
/// <summary> | |||
/// The <see cref="SocketInteractionDataOption"/>'s recieved with this interaction. | |||
/// The <see cref="SocketSlashCommandDataOption"/>'s recieved with this interaction. | |||
/// </summary> | |||
public IReadOnlyCollection<SocketInteractionDataOption> Options { get; private set; } | |||
public IReadOnlyCollection<SocketSlashCommandDataOption> Options { get; private set; } | |||
private ulong guildId; | |||
internal SocketInteractionData(DiscordSocketClient client, ulong id) | |||
internal SocketSlashCommandData(DiscordSocketClient client, ulong id) | |||
: base(client, id) | |||
{ | |||
} | |||
internal static SocketInteractionData Create(DiscordSocketClient client, Model model, ulong guildId) | |||
internal static SocketSlashCommandData Create(DiscordSocketClient client, Model model, ulong guildId) | |||
{ | |||
var entity = new SocketInteractionData(client, model.Id); | |||
var entity = new SocketSlashCommandData(client, model.Id); | |||
entity.Update(model, guildId); | |||
return entity; | |||
} | |||
@@ -38,7 +38,7 @@ namespace Discord.WebSocket | |||
this.guildId = guildId; | |||
this.Options = model.Options.IsSpecified | |||
? model.Options.Value.Select(x => new SocketInteractionDataOption(x, this.Discord, guildId)).ToImmutableArray() | |||
? model.Options.Value.Select(x => new SocketSlashCommandDataOption(x, this.Discord, guildId)).ToImmutableArray() | |||
: null; | |||
} | |||
@@ -11,7 +11,7 @@ namespace Discord.WebSocket | |||
/// <summary> | |||
/// Represents a Websocket-based <see cref="IApplicationCommandInteractionDataOption"/> recieved by the gateway | |||
/// </summary> | |||
public class SocketInteractionDataOption : IApplicationCommandInteractionDataOption | |||
public class SocketSlashCommandDataOption : IApplicationCommandInteractionDataOption | |||
{ | |||
/// <inheritdoc/> | |||
public string Name { get; private set; } | |||
@@ -22,13 +22,13 @@ namespace Discord.WebSocket | |||
/// <summary> | |||
/// The sub command options recieved for this sub command group. | |||
/// </summary> | |||
public IReadOnlyCollection<SocketInteractionDataOption> Options { get; private set; } | |||
public IReadOnlyCollection<SocketSlashCommandDataOption> Options { get; private set; } | |||
private DiscordSocketClient discord; | |||
private ulong guild; | |||
internal SocketInteractionDataOption() { } | |||
internal SocketInteractionDataOption(Model model, DiscordSocketClient discord, ulong guild) | |||
internal SocketSlashCommandDataOption() { } | |||
internal SocketSlashCommandDataOption(Model model, DiscordSocketClient discord, ulong guild) | |||
{ | |||
this.Name = model.Name; | |||
this.Value = model.Value.IsSpecified ? model.Value.Value : null; | |||
@@ -36,19 +36,19 @@ namespace Discord.WebSocket | |||
this.guild = guild; | |||
this.Options = model.Options.IsSpecified | |||
? model.Options.Value.Select(x => new SocketInteractionDataOption(x, discord, guild)).ToImmutableArray() | |||
? model.Options.Value.Select(x => new SocketSlashCommandDataOption(x, discord, guild)).ToImmutableArray() | |||
: null; | |||
} | |||
// Converters | |||
public static explicit operator bool(SocketInteractionDataOption option) | |||
public static explicit operator bool(SocketSlashCommandDataOption option) | |||
=> (bool)option.Value; | |||
public static explicit operator int(SocketInteractionDataOption option) | |||
public static explicit operator int(SocketSlashCommandDataOption option) | |||
=> (int)option.Value; | |||
public static explicit operator string(SocketInteractionDataOption option) | |||
public static explicit operator string(SocketSlashCommandDataOption option) | |||
=> option.Value.ToString(); | |||
public static explicit operator SocketGuildChannel(SocketInteractionDataOption option) | |||
public static explicit operator SocketGuildChannel(SocketSlashCommandDataOption option) | |||
{ | |||
if (option.Value is ulong id) | |||
{ | |||
@@ -63,7 +63,7 @@ namespace Discord.WebSocket | |||
return null; | |||
} | |||
public static explicit operator SocketRole(SocketInteractionDataOption option) | |||
public static explicit operator SocketRole(SocketSlashCommandDataOption option) | |||
{ | |||
if (option.Value is ulong id) | |||
{ | |||
@@ -78,7 +78,7 @@ namespace Discord.WebSocket | |||
return null; | |||
} | |||
public static explicit operator SocketGuildUser(SocketInteractionDataOption option) | |||
public static explicit operator SocketGuildUser(SocketSlashCommandDataOption option) | |||
{ | |||
if(option.Value is ulong id) | |||
{ |
@@ -11,7 +11,7 @@ namespace Discord.WebSocket | |||
/// <summary> | |||
/// Represents an Interaction recieved over the gateway. | |||
/// </summary> | |||
public class SocketInteraction : SocketEntity<ulong>, IDiscordInteraction | |||
public abstract class SocketInteraction : SocketEntity<ulong>, IDiscordInteraction | |||
{ | |||
/// <summary> | |||
/// The <see cref="SocketGuild"/> this interaction was used in. | |||
@@ -36,14 +36,14 @@ namespace Discord.WebSocket | |||
public InteractionType Type { get; private set; } | |||
/// <summary> | |||
/// The data associated with this interaction. | |||
/// The token used to respond to this interaction. | |||
/// </summary> | |||
public SocketInteractionData Data { get; private set; } | |||
public string Token { get; private set; } | |||
/// <summary> | |||
/// The token used to respond to this interaction. | |||
/// The data sent with this interaction. | |||
/// </summary> | |||
public string Token { get; private set; } | |||
public object Data { get; private set; } | |||
/// <summary> | |||
/// The version of this interaction. | |||
@@ -69,15 +69,18 @@ namespace Discord.WebSocket | |||
internal static SocketInteraction Create(DiscordSocketClient client, Model model) | |||
{ | |||
var entitiy = new SocketInteraction(client, model.Id); | |||
entitiy.Update(model); | |||
return entitiy; | |||
if (model.Type == InteractionType.ApplicationCommand) | |||
return SocketSlashCommand.Create(client, model); | |||
if (model.Type == InteractionType.MessageComponent) | |||
return SocketMessageComponent.Create(client, model); | |||
else | |||
return null; | |||
} | |||
internal void Update(Model model) | |||
internal virtual void Update(Model model) | |||
{ | |||
this.Data = model.Data.IsSpecified | |||
? SocketInteractionData.Create(this.Discord, model.Data.Value, model.GuildId) | |||
? model.Data.Value | |||
: null; | |||
this.GuildId = model.GuildId; | |||
@@ -90,14 +93,9 @@ namespace Discord.WebSocket | |||
if (this.User == null) | |||
this.User = SocketGuildUser.Create(this.Guild, Discord.State, model.Member); // Change from getter. | |||
} | |||
private bool CheckToken() | |||
{ | |||
// Tokens last for 15 minutes according to https://discord.com/developers/docs/interactions/slash-commands#responding-to-an-interaction | |||
return (DateTime.UtcNow - this.CreatedAt.UtcDateTime).TotalMinutes >= 15d; | |||
} | |||
/// <summary> | |||
/// Responds to an Interaction. | |||
/// Responds to an Interaction. | |||
/// <para> | |||
/// If you have <see cref="DiscordSocketConfig.AlwaysAcknowledgeInteractions"/> set to <see langword="true"/>, You should use | |||
/// <see cref="FollowupAsync(string, bool, Embed, InteractionResponseType, AllowedMentions, RequestOptions)"/> instead. | |||
@@ -110,63 +108,16 @@ namespace Discord.WebSocket | |||
/// <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> | |||
/// <returns> | |||
/// The <see cref="IMessage"/> sent as the response. If this is the first acknowledgement, it will return null. | |||
/// </returns> | |||
/// <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> | |||
public async Task<IMessage> RespondAsync(string text = null, bool isTTS = false, Embed embed = null, InteractionResponseType type = InteractionResponseType.ChannelMessageWithSource, | |||
bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null) | |||
{ | |||
if (type == InteractionResponseType.Pong) | |||
throw new InvalidOperationException($"Cannot use {Type} on a send message function"); | |||
if (!IsValidToken) | |||
throw new InvalidOperationException("Interaction token is no longer valid"); | |||
if (Discord.AlwaysAcknowledgeInteractions) | |||
return await FollowupAsync(text, isTTS, embed, ephemeral, type, allowedMentions, options); // The arguments should be passed? What was i thinking... | |||
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."); | |||
// 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 = type, | |||
Data = new API.InteractionApplicationCommandCallbackData(text) | |||
{ | |||
AllowedMentions = allowedMentions?.ToModel(), | |||
Embeds = embed != null | |||
? new API.Embed[] { embed.ToModel() } | |||
: Optional<API.Embed[]>.Unspecified, | |||
TTS = isTTS, | |||
} | |||
}; | |||
if (ephemeral) | |||
response.Data.Value.Flags = 64; | |||
await Discord.Rest.ApiClient.CreateInteractionResponse(response, this.Id, Token, options); | |||
return null; | |||
} | |||
public virtual Task<RestUserMessage> RespondAsync(string text = null, bool isTTS = false, Embed embed = null, InteractionResponseType type = InteractionResponseType.ChannelMessageWithSource, | |||
bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null) | |||
{ return null; } | |||
/// <summary> | |||
/// Sends a followup message for this interaction. | |||
@@ -174,36 +125,18 @@ namespace Discord.WebSocket | |||
/// <param name="text">The text of the message to be sent</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="embed">A <see cref="Embed"/> to send with this response.</param> | |||
/// <param name="Type">The type of response to this Interaction.</param> | |||
/// <param name="type">The type of response to this Interaction.</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> | |||
/// <returns> | |||
/// The sent message. | |||
/// </returns> | |||
public async Task<IMessage> FollowupAsync(string text = null, bool isTTS = false, Embed embed = null, bool ephemeral = false, | |||
InteractionResponseType Type = InteractionResponseType.ChannelMessageWithSource, | |||
AllowedMentions allowedMentions = null, RequestOptions options = null) | |||
{ | |||
if (Type == InteractionResponseType.DeferredChannelMessageWithSource || Type == InteractionResponseType.DeferredChannelMessageWithSource || Type == InteractionResponseType.Pong) | |||
throw new InvalidOperationException($"Cannot use {Type} on a send message function"); | |||
if (!IsValidToken) | |||
throw new InvalidOperationException("Interaction token is no longer valid"); | |||
var args = new API.Rest.CreateWebhookMessageParams(text) | |||
{ | |||
IsTTS = isTTS, | |||
Embeds = embed != null | |||
? new API.Embed[] { embed.ToModel() } | |||
: Optional<API.Embed[]>.Unspecified, | |||
}; | |||
if (ephemeral) | |||
args.Flags = 64; | |||
return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options); | |||
} | |||
public virtual Task<RestFollowupMessage> FollowupAsync(string text = null, bool isTTS = false, Embed embed = null, bool ephemeral = false, | |||
InteractionResponseType type = InteractionResponseType.ChannelMessageWithSource, | |||
AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null) | |||
{ return null; } | |||
/// <summary> | |||
/// Acknowledges this interaction with the <see cref="InteractionResponseType.DeferredChannelMessageWithSource"/>. | |||
@@ -211,16 +144,20 @@ namespace Discord.WebSocket | |||
/// <returns> | |||
/// A task that represents the asynchronous operation of acknowledging the interaction. | |||
/// </returns> | |||
public async Task AcknowledgeAsync(RequestOptions options = null) | |||
public virtual Task AcknowledgeAsync(RequestOptions options = null) | |||
{ | |||
var response = new API.InteractionResponse() | |||
{ | |||
Type = InteractionResponseType.DeferredChannelMessageWithSource, | |||
}; | |||
await Discord.Rest.ApiClient.CreateInteractionResponse(response, this.Id, Token, options).ConfigureAwait(false); | |||
return Discord.Rest.ApiClient.CreateInteractionResponse(response, this.Id, this.Token, options); | |||
} | |||
IApplicationCommandInteractionData IDiscordInteraction.Data => Data; | |||
private bool CheckToken() | |||
{ | |||
// Tokens last for 15 minutes according to https://discord.com/developers/docs/interactions/slash-commands#responding-to-an-interaction | |||
return (DateTime.UtcNow - this.CreatedAt.UtcDateTime).TotalMinutes >= 15d; | |||
} | |||
} | |||
} |