diff --git a/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs index 3979e8835..5c409e837 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs @@ -26,9 +26,9 @@ namespace Discord InteractionType Type { get; } /// - /// The command data payload. + /// Represents the data sent within this interaction. /// - IApplicationCommandInteractionData? Data { get; } + object Data { get; } /// /// A continuation token for responding to the interaction. diff --git a/src/Discord.Net.Core/Entities/Interactions/InteractionResponseType.cs b/src/Discord.Net.Core/Entities/Interactions/InteractionResponseType.cs index eca4fdff6..1b103f491 100644 --- a/src/Discord.Net.Core/Entities/Interactions/InteractionResponseType.cs +++ b/src/Discord.Net.Core/Entities/Interactions/InteractionResponseType.cs @@ -42,6 +42,16 @@ namespace Discord /// /// ACK an interaction and edit a response later, the user sees a loading state. /// - DeferredChannelMessageWithSource = 5 + DeferredChannelMessageWithSource = 5, + + /// + /// for components: ACK an interaction and edit the original message later; the user does not see a loading state + /// + DeferredUpdateMessage = 6, + + /// + /// for components: edit the message the component was attached to + /// + UpdateMessage = 7 } } diff --git a/src/Discord.Net.Core/Entities/Interactions/InteractionType.cs b/src/Discord.Net.Core/Entities/Interactions/InteractionType.cs index f95960586..4da39b58e 100644 --- a/src/Discord.Net.Core/Entities/Interactions/InteractionType.cs +++ b/src/Discord.Net.Core/Entities/Interactions/InteractionType.cs @@ -17,8 +17,13 @@ namespace Discord Ping = 1, /// - /// An sent from discord. + /// A sent from discord. /// - ApplicationCommand = 2 + ApplicationCommand = 2, + + /// + /// A sent from discord. + /// + MessageComponent = 3, } } diff --git a/src/Discord.Net.Core/Entities/Interactions/Message Components/ActionRowComponent.cs b/src/Discord.Net.Core/Entities/Interactions/Message Components/ActionRowComponent.cs index 6fd74a6f9..7151cc5af 100644 --- a/src/Discord.Net.Core/Entities/Interactions/Message Components/ActionRowComponent.cs +++ b/src/Discord.Net.Core/Entities/Interactions/Message Components/ActionRowComponent.cs @@ -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 Components { get; internal set; } internal ActionRowComponent() { } diff --git a/src/Discord.Net.Core/Entities/Interactions/Message Components/ButtonComponent.cs b/src/Discord.Net.Core/Entities/Interactions/Message Components/ButtonComponent.cs index 514f45a62..2e2b98f98 100644 --- a/src/Discord.Net.Core/Entities/Interactions/Message Components/ButtonComponent.cs +++ b/src/Discord.Net.Core/Entities/Interactions/Message Components/ButtonComponent.cs @@ -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) diff --git a/src/Discord.Net.Rest/API/Common/InteractionApplicationCommandCallbackData.cs b/src/Discord.Net.Rest/API/Common/InteractionApplicationCommandCallbackData.cs index 24da5bf7e..f5ce75250 100644 --- a/src/Discord.Net.Rest/API/Common/InteractionApplicationCommandCallbackData.cs +++ b/src/Discord.Net.Rest/API/Common/InteractionApplicationCommandCallbackData.cs @@ -25,6 +25,9 @@ namespace Discord.API [JsonProperty("flags")] public Optional Flags { get; set; } + [JsonProperty("components")] + public Optional Components { get; set; } + public InteractionApplicationCommandCallbackData() { } public InteractionApplicationCommandCallbackData(string text) { diff --git a/src/Discord.Net.Rest/API/Common/MessageComponentInteractionData.cs b/src/Discord.Net.Rest/API/Common/MessageComponentInteractionData.cs new file mode 100644 index 000000000..cdb4e7d5c --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/MessageComponentInteractionData.cs @@ -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; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs b/src/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs index 3e201d8eb..c668ee484 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs @@ -30,6 +30,9 @@ namespace Discord.API.Rest [JsonProperty("flags")] public Optional Flags { get; set; } + [JsonProperty("components")] + public Optional Components { get; set; } + public CreateWebhookMessageParams(string content) { Content = content; diff --git a/src/Discord.Net.WebSocket/API/Gateway/InteractionCreated.cs b/src/Discord.Net.WebSocket/API/Gateway/InteractionCreated.cs index 8fb0cbd58..8c451a552 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/InteractionCreated.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/InteractionCreated.cs @@ -17,7 +17,7 @@ namespace Discord.API.Gateway public InteractionType Type { get; set; } [JsonProperty("data")] - public Optional Data { get; set; } + public Optional Data { get; set; } [JsonProperty("guild_id")] public ulong GuildId { get; set; } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Message Components/SocketMessageComponent.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Message Components/SocketMessageComponent.cs new file mode 100644 index 000000000..a05fae3cc --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/Message Components/SocketMessageComponent.cs @@ -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() + : 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; + } + + /// + /// Responds to an Interaction. + /// + /// If you have set to , You should use + /// instead. + /// + /// + /// The text of the message to be sent. + /// if the message should be read out by a text-to-speech reader, otherwise . + /// A to send with this response. + /// The type of response to this Interaction. + /// if the response should be hidden to everyone besides the invoker of the command, otherwise . + /// The allowed mentions for this response. + /// The request options for this response. + /// A to be sent with this response + /// + /// The sent as the response. If this is the first acknowledgement, it will return null. + /// + /// Message content is too long, length must be less or equal to . + /// The parameters provided were invalid or the token was invalid. + + public override async Task 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.Unspecified, + TTS = isTTS, + Components = component?.ToModel() ?? Optional.Unspecified + } + }; + + if (ephemeral) + response.Data.Value.Flags = 64; + + return await InteractionHelper.SendInteractionResponse(this.Discord, this.Channel, response, this.Id, Token, options); + } + + /// + /// Sends a followup message for this interaction. + /// + /// The text of the message to be sent + /// if the message should be read out by a text-to-speech reader, otherwise . + /// A to send with this response. + /// The type of response to this Interaction. + /// /// if the response should be hidden to everyone besides the invoker of the command, otherwise . + /// The allowed mentions for this response. + /// The request options for this response. + /// A to be sent with this response + /// + /// The sent message. + /// + public override async Task 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.Unspecified, + Components = component?.ToModel() ?? Optional.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); + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Message Components/SocketMessageComponentData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Message Components/SocketMessageComponentData.cs new file mode 100644 index 000000000..fad959e1d --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/Message Components/SocketMessageComponentData.cs @@ -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 + { + /// + /// The components Custom Id that was clicked + /// + public string CustomId { get; } + + /// + /// The type of the component clicked + /// + public ComponentType Type { get; } + + internal SocketMessageComponentData(Model model) + { + this.CustomId = model.CustomId; + this.Type = model.ComponentType; + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketApplicationCommand.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketApplicationCommand.cs similarity index 100% rename from src/Discord.Net.WebSocket/Entities/Interaction/SocketApplicationCommand.cs rename to src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketApplicationCommand.cs diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketApplicationCommandChoice.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketApplicationCommandChoice.cs similarity index 100% rename from src/Discord.Net.WebSocket/Entities/Interaction/SocketApplicationCommandChoice.cs rename to src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketApplicationCommandChoice.cs diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketApplicationCommandOption.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketApplicationCommandOption.cs similarity index 100% rename from src/Discord.Net.WebSocket/Entities/Interaction/SocketApplicationCommandOption.cs rename to src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketApplicationCommandOption.cs diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommand.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommand.cs new file mode 100644 index 000000000..da94c4625 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommand.cs @@ -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 + { + /// + /// The data associated with this interaction. + /// + 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() + : 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() + : null; + + this.Data.Update(data, this.Guild.Id); + + base.Update(model); + } + + /// + /// Responds to an Interaction. + /// + /// If you have set to , You should use + /// instead. + /// + /// + /// The text of the message to be sent. + /// if the message should be read out by a text-to-speech reader, otherwise . + /// A to send with this response. + /// The type of response to this Interaction. + /// if the response should be hidden to everyone besides the invoker of the command, otherwise . + /// The allowed mentions for this response. + /// The request options for this response. + /// + /// The sent as the response. If this is the first acknowledgement, it will return null. + /// + /// Message content is too long, length must be less or equal to . + /// The parameters provided were invalid or the token was invalid. + + public override async Task 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.Unspecified, + TTS = isTTS, + Components = component?.ToModel() ?? Optional.Unspecified + } + }; + + if (ephemeral) + response.Data.Value.Flags = 64; + + return await InteractionHelper.SendInteractionResponse(this.Discord, this.Channel, response, this.Id, Token, options); + } + + /// + /// Sends a followup message for this interaction. + /// + /// The text of the message to be sent + /// if the message should be read out by a text-to-speech reader, otherwise . + /// A to send with this response. + /// The type of response to this Interaction. + /// /// if the response should be hidden to everyone besides the invoker of the command, otherwise . + /// The allowed mentions for this response. + /// The request options for this response. + /// + /// The sent message. + /// + public override async Task 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.Unspecified, + Components = component?.ToModel() ?? Optional.Unspecified + }; + + if (ephemeral) + args.Flags = 64; + + return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options); + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommandData.cs similarity index 57% rename from src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionData.cs rename to src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommandData.cs index eef7e5ab4..098d81b86 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionData.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommandData.cs @@ -8,27 +8,27 @@ using Model = Discord.API.ApplicationCommandInteractionData; namespace Discord.WebSocket { - public class SocketInteractionData : SocketEntity, IApplicationCommandInteractionData + public class SocketSlashCommandData : SocketEntity, IApplicationCommandInteractionData { /// public string Name { get; private set; } /// - /// The 's recieved with this interaction. + /// The 's recieved with this interaction. /// - public IReadOnlyCollection Options { get; private set; } + public IReadOnlyCollection 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; } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionDataOption.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommandDataOption.cs similarity index 71% rename from src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionDataOption.cs rename to src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommandDataOption.cs index c4908bc4d..89f1443be 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionDataOption.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommandDataOption.cs @@ -11,7 +11,7 @@ namespace Discord.WebSocket /// /// Represents a Websocket-based recieved by the gateway /// - public class SocketInteractionDataOption : IApplicationCommandInteractionDataOption + public class SocketSlashCommandDataOption : IApplicationCommandInteractionDataOption { /// public string Name { get; private set; } @@ -22,13 +22,13 @@ namespace Discord.WebSocket /// /// The sub command options recieved for this sub command group. /// - public IReadOnlyCollection Options { get; private set; } + public IReadOnlyCollection 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) { diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs index 92bc679d8..96f0bca10 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs @@ -11,7 +11,7 @@ namespace Discord.WebSocket /// /// Represents an Interaction recieved over the gateway. /// - public class SocketInteraction : SocketEntity, IDiscordInteraction + public abstract class SocketInteraction : SocketEntity, IDiscordInteraction { /// /// The this interaction was used in. @@ -36,14 +36,14 @@ namespace Discord.WebSocket public InteractionType Type { get; private set; } /// - /// The data associated with this interaction. + /// The token used to respond to this interaction. /// - public SocketInteractionData Data { get; private set; } + public string Token { get; private set; } /// - /// The token used to respond to this interaction. + /// The data sent with this interaction. /// - public string Token { get; private set; } + public object Data { get; private set; } /// /// 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; - } /// - /// Responds to an Interaction. + /// Responds to an Interaction. /// /// If you have set to , You should use /// instead. @@ -110,63 +108,16 @@ namespace Discord.WebSocket /// if the response should be hidden to everyone besides the invoker of the command, otherwise . /// The allowed mentions for this response. /// The request options for this response. + /// A to be sent with this response /// /// The sent as the response. If this is the first acknowledgement, it will return null. /// /// Message content is too long, length must be less or equal to . /// The parameters provided were invalid or the token was invalid. - public async Task 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.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 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; } /// /// Sends a followup message for this interaction. @@ -174,36 +125,18 @@ namespace Discord.WebSocket /// The text of the message to be sent /// if the message should be read out by a text-to-speech reader, otherwise . /// A to send with this response. - /// The type of response to this Interaction. + /// The type of response to this Interaction. /// /// if the response should be hidden to everyone besides the invoker of the command, otherwise . /// The allowed mentions for this response. /// The request options for this response. + /// A to be sent with this response /// /// The sent message. /// - public async Task 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.Unspecified, - }; - - if (ephemeral) - args.Flags = 64; - - return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options); - } + public virtual Task 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; } /// /// Acknowledges this interaction with the . @@ -211,16 +144,20 @@ namespace Discord.WebSocket /// /// A task that represents the asynchronous operation of acknowledging the interaction. /// - 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; + } } }