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