| @@ -26,9 +26,9 @@ namespace Discord | |||||
| InteractionType Type { get; } | InteractionType Type { get; } | ||||
| /// <summary> | /// <summary> | ||||
| /// The command data payload. | |||||
| /// Represents the data sent within this interaction. | |||||
| /// </summary> | /// </summary> | ||||
| IApplicationCommandInteractionData? Data { get; } | |||||
| object Data { get; } | |||||
| /// <summary> | /// <summary> | ||||
| /// A continuation token for responding to the interaction. | /// A continuation token for responding to the interaction. | ||||
| @@ -42,6 +42,16 @@ namespace Discord | |||||
| /// <summary> | /// <summary> | ||||
| /// ACK an interaction and edit a response later, the user sees a loading state. | /// ACK an interaction and edit a response later, the user sees a loading state. | ||||
| /// </summary> | /// </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, | Ping = 1, | ||||
| /// <summary> | /// <summary> | ||||
| /// An <see cref="IApplicationCommand"/> sent from discord. | |||||
| /// A <see cref="IApplicationCommand"/> sent from discord. | |||||
| /// </summary> | /// </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; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Linq; | using System.Linq; | ||||
| @@ -8,8 +9,10 @@ namespace Discord | |||||
| { | { | ||||
| public class ActionRowComponent : IMessageComponent | public class ActionRowComponent : IMessageComponent | ||||
| { | { | ||||
| [JsonProperty("type")] | |||||
| public ComponentType Type { get; } = ComponentType.ActionRow; | public ComponentType Type { get; } = ComponentType.ActionRow; | ||||
| [JsonProperty("components")] | |||||
| public IReadOnlyCollection<IMessageComponent> Components { get; internal set; } | public IReadOnlyCollection<IMessageComponent> Components { get; internal set; } | ||||
| internal ActionRowComponent() { } | internal ActionRowComponent() { } | ||||
| @@ -1,3 +1,4 @@ | |||||
| using Newtonsoft.Json; | |||||
| using System; | using System; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Linq; | using System.Linq; | ||||
| @@ -8,18 +9,25 @@ namespace Discord | |||||
| { | { | ||||
| public class ButtonComponent : IMessageComponent | public class ButtonComponent : IMessageComponent | ||||
| { | { | ||||
| [JsonProperty("type")] | |||||
| public ComponentType Type { get; } = ComponentType.Button; | public ComponentType Type { get; } = ComponentType.Button; | ||||
| [JsonProperty("style")] | |||||
| public ButtonStyle Style { get; } | public ButtonStyle Style { get; } | ||||
| [JsonProperty("label")] | |||||
| public string Label { get; } | public string Label { get; } | ||||
| [JsonProperty("emoji")] | |||||
| public IEmote Emote { get; } | public IEmote Emote { get; } | ||||
| [JsonProperty("custom_id")] | |||||
| public string CustomId { get; } | public string CustomId { get; } | ||||
| [JsonProperty("url")] | |||||
| public string Url { get; } | public string Url { get; } | ||||
| [JsonProperty("disabled")] | |||||
| public bool Disabled { get; } | public bool Disabled { get; } | ||||
| internal ButtonComponent(ButtonStyle style, string label, IEmote emote, string customId, string url, bool disabled) | internal ButtonComponent(ButtonStyle style, string label, IEmote emote, string customId, string url, bool disabled) | ||||
| @@ -25,6 +25,9 @@ namespace Discord.API | |||||
| [JsonProperty("flags")] | [JsonProperty("flags")] | ||||
| public Optional<int> Flags { get; set; } | public Optional<int> Flags { get; set; } | ||||
| [JsonProperty("components")] | |||||
| public Optional<IMessageComponent[]> Components { get; set; } | |||||
| public InteractionApplicationCommandCallbackData() { } | public InteractionApplicationCommandCallbackData() { } | ||||
| public InteractionApplicationCommandCallbackData(string text) | 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")] | [JsonProperty("flags")] | ||||
| public Optional<int> Flags { get; set; } | public Optional<int> Flags { get; set; } | ||||
| [JsonProperty("components")] | |||||
| public Optional<IMessageComponent[]> Components { get; set; } | |||||
| public CreateWebhookMessageParams(string content) | public CreateWebhookMessageParams(string content) | ||||
| { | { | ||||
| Content = content; | Content = content; | ||||
| @@ -17,7 +17,7 @@ namespace Discord.API.Gateway | |||||
| public InteractionType Type { get; set; } | public InteractionType Type { get; set; } | ||||
| [JsonProperty("data")] | [JsonProperty("data")] | ||||
| public Optional<ApplicationCommandInteractionData> Data { get; set; } | |||||
| public Optional<object> Data { get; set; } | |||||
| [JsonProperty("guild_id")] | [JsonProperty("guild_id")] | ||||
| public ulong GuildId { get; set; } | 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 | namespace Discord.WebSocket | ||||
| { | { | ||||
| public class SocketInteractionData : SocketEntity<ulong>, IApplicationCommandInteractionData | |||||
| public class SocketSlashCommandData : SocketEntity<ulong>, IApplicationCommandInteractionData | |||||
| { | { | ||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public string Name { get; private set; } | public string Name { get; private set; } | ||||
| /// <summary> | /// <summary> | ||||
| /// The <see cref="SocketInteractionDataOption"/>'s recieved with this interaction. | |||||
| /// The <see cref="SocketSlashCommandDataOption"/>'s recieved with this interaction. | |||||
| /// </summary> | /// </summary> | ||||
| public IReadOnlyCollection<SocketInteractionDataOption> Options { get; private set; } | |||||
| public IReadOnlyCollection<SocketSlashCommandDataOption> Options { get; private set; } | |||||
| private ulong guildId; | private ulong guildId; | ||||
| internal SocketInteractionData(DiscordSocketClient client, ulong id) | |||||
| internal SocketSlashCommandData(DiscordSocketClient client, ulong id) | |||||
| : base(client, 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); | entity.Update(model, guildId); | ||||
| return entity; | return entity; | ||||
| } | } | ||||
| @@ -38,7 +38,7 @@ namespace Discord.WebSocket | |||||
| this.guildId = guildId; | this.guildId = guildId; | ||||
| this.Options = model.Options.IsSpecified | 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; | : null; | ||||
| } | } | ||||
| @@ -11,7 +11,7 @@ namespace Discord.WebSocket | |||||
| /// <summary> | /// <summary> | ||||
| /// Represents a Websocket-based <see cref="IApplicationCommandInteractionDataOption"/> recieved by the gateway | /// Represents a Websocket-based <see cref="IApplicationCommandInteractionDataOption"/> recieved by the gateway | ||||
| /// </summary> | /// </summary> | ||||
| public class SocketInteractionDataOption : IApplicationCommandInteractionDataOption | |||||
| public class SocketSlashCommandDataOption : IApplicationCommandInteractionDataOption | |||||
| { | { | ||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public string Name { get; private set; } | public string Name { get; private set; } | ||||
| @@ -22,13 +22,13 @@ namespace Discord.WebSocket | |||||
| /// <summary> | /// <summary> | ||||
| /// The sub command options recieved for this sub command group. | /// The sub command options recieved for this sub command group. | ||||
| /// </summary> | /// </summary> | ||||
| public IReadOnlyCollection<SocketInteractionDataOption> Options { get; private set; } | |||||
| public IReadOnlyCollection<SocketSlashCommandDataOption> Options { get; private set; } | |||||
| private DiscordSocketClient discord; | private DiscordSocketClient discord; | ||||
| private ulong guild; | 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.Name = model.Name; | ||||
| this.Value = model.Value.IsSpecified ? model.Value.Value : null; | this.Value = model.Value.IsSpecified ? model.Value.Value : null; | ||||
| @@ -36,19 +36,19 @@ namespace Discord.WebSocket | |||||
| this.guild = guild; | this.guild = guild; | ||||
| this.Options = model.Options.IsSpecified | 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; | : null; | ||||
| } | } | ||||
| // Converters | // Converters | ||||
| public static explicit operator bool(SocketInteractionDataOption option) | |||||
| public static explicit operator bool(SocketSlashCommandDataOption option) | |||||
| => (bool)option.Value; | => (bool)option.Value; | ||||
| public static explicit operator int(SocketInteractionDataOption option) | |||||
| public static explicit operator int(SocketSlashCommandDataOption option) | |||||
| => (int)option.Value; | => (int)option.Value; | ||||
| public static explicit operator string(SocketInteractionDataOption option) | |||||
| public static explicit operator string(SocketSlashCommandDataOption option) | |||||
| => option.Value.ToString(); | => option.Value.ToString(); | ||||
| public static explicit operator SocketGuildChannel(SocketInteractionDataOption option) | |||||
| public static explicit operator SocketGuildChannel(SocketSlashCommandDataOption option) | |||||
| { | { | ||||
| if (option.Value is ulong id) | if (option.Value is ulong id) | ||||
| { | { | ||||
| @@ -63,7 +63,7 @@ namespace Discord.WebSocket | |||||
| return null; | return null; | ||||
| } | } | ||||
| public static explicit operator SocketRole(SocketInteractionDataOption option) | |||||
| public static explicit operator SocketRole(SocketSlashCommandDataOption option) | |||||
| { | { | ||||
| if (option.Value is ulong id) | if (option.Value is ulong id) | ||||
| { | { | ||||
| @@ -78,7 +78,7 @@ namespace Discord.WebSocket | |||||
| return null; | return null; | ||||
| } | } | ||||
| public static explicit operator SocketGuildUser(SocketInteractionDataOption option) | |||||
| public static explicit operator SocketGuildUser(SocketSlashCommandDataOption option) | |||||
| { | { | ||||
| if(option.Value is ulong id) | if(option.Value is ulong id) | ||||
| { | { | ||||
| @@ -11,7 +11,7 @@ namespace Discord.WebSocket | |||||
| /// <summary> | /// <summary> | ||||
| /// Represents an Interaction recieved over the gateway. | /// Represents an Interaction recieved over the gateway. | ||||
| /// </summary> | /// </summary> | ||||
| public class SocketInteraction : SocketEntity<ulong>, IDiscordInteraction | |||||
| public abstract class SocketInteraction : SocketEntity<ulong>, IDiscordInteraction | |||||
| { | { | ||||
| /// <summary> | /// <summary> | ||||
| /// The <see cref="SocketGuild"/> this interaction was used in. | /// The <see cref="SocketGuild"/> this interaction was used in. | ||||
| @@ -36,14 +36,14 @@ namespace Discord.WebSocket | |||||
| public InteractionType Type { get; private set; } | public InteractionType Type { get; private set; } | ||||
| /// <summary> | /// <summary> | ||||
| /// The data associated with this interaction. | |||||
| /// The token used to respond to this interaction. | |||||
| /// </summary> | /// </summary> | ||||
| public SocketInteractionData Data { get; private set; } | |||||
| public string Token { get; private set; } | |||||
| /// <summary> | /// <summary> | ||||
| /// The token used to respond to this interaction. | |||||
| /// The data sent with this interaction. | |||||
| /// </summary> | /// </summary> | ||||
| public string Token { get; private set; } | |||||
| public object Data { get; private set; } | |||||
| /// <summary> | /// <summary> | ||||
| /// The version of this interaction. | /// The version of this interaction. | ||||
| @@ -69,15 +69,18 @@ namespace Discord.WebSocket | |||||
| internal static SocketInteraction Create(DiscordSocketClient client, Model model) | 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 | this.Data = model.Data.IsSpecified | ||||
| ? SocketInteractionData.Create(this.Discord, model.Data.Value, model.GuildId) | |||||
| ? model.Data.Value | |||||
| : null; | : null; | ||||
| this.GuildId = model.GuildId; | this.GuildId = model.GuildId; | ||||
| @@ -90,14 +93,9 @@ namespace Discord.WebSocket | |||||
| if (this.User == null) | if (this.User == null) | ||||
| this.User = SocketGuildUser.Create(this.Guild, Discord.State, model.Member); // Change from getter. | 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> | /// <summary> | ||||
| /// Responds to an Interaction. | |||||
| /// Responds to an Interaction. | |||||
| /// <para> | /// <para> | ||||
| /// If you have <see cref="DiscordSocketConfig.AlwaysAcknowledgeInteractions"/> set to <see langword="true"/>, You should use | /// 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. | /// <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="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="allowedMentions">The allowed mentions for this response.</param> | ||||
| /// <param name="options">The request options 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> | /// <returns> | ||||
| /// The <see cref="IMessage"/> sent as the response. If this is the first acknowledgement, it will return null. | /// The <see cref="IMessage"/> sent as the response. If this is the first acknowledgement, it will return null. | ||||
| /// </returns> | /// </returns> | ||||
| /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> | /// <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> | /// <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> | /// <summary> | ||||
| /// Sends a followup message for this interaction. | /// 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="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="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="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="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="allowedMentions">The allowed mentions for this response.</param> | ||||
| /// <param name="options">The request options 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> | /// <returns> | ||||
| /// The sent message. | /// The sent message. | ||||
| /// </returns> | /// </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> | /// <summary> | ||||
| /// Acknowledges this interaction with the <see cref="InteractionResponseType.DeferredChannelMessageWithSource"/>. | /// Acknowledges this interaction with the <see cref="InteractionResponseType.DeferredChannelMessageWithSource"/>. | ||||
| @@ -211,16 +144,20 @@ namespace Discord.WebSocket | |||||
| /// <returns> | /// <returns> | ||||
| /// A task that represents the asynchronous operation of acknowledging the interaction. | /// A task that represents the asynchronous operation of acknowledging the interaction. | ||||
| /// </returns> | /// </returns> | ||||
| public async Task AcknowledgeAsync(RequestOptions options = null) | |||||
| public virtual Task AcknowledgeAsync(RequestOptions options = null) | |||||
| { | { | ||||
| var response = new API.InteractionResponse() | var response = new API.InteractionResponse() | ||||
| { | { | ||||
| Type = InteractionResponseType.DeferredChannelMessageWithSource, | 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; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||