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