diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs index 3a03e028b..97ab54d3d 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs @@ -7,47 +7,47 @@ using System.Threading.Tasks; namespace Discord { /// - /// The option type of the Slash command parameter, See + /// The option type of the Slash command parameter, See the discord docs. /// public enum ApplicationCommandOptionType : byte { /// - /// A sub command + /// A sub command. /// SubCommand = 1, /// - /// A group of sub commands + /// A group of sub commands. /// SubCommandGroup = 2, /// - /// A of text + /// A of text. /// String = 3, /// - /// An + /// An . /// Integer = 4, /// - /// A + /// A . /// Boolean = 5, /// - /// A + /// A . /// User = 6, /// - /// A + /// A . /// Channel = 7, /// - /// A + /// A . /// Role = 8 } diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs index 08406f844..7fa28c8d0 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs @@ -9,11 +9,21 @@ namespace Discord /// /// Provides properties that are used to modify a with the specified changes. /// - /// public class ApplicationCommandProperties { + /// + /// Gets or sets the name of this command. + /// public string Name { get; set; } + + /// + /// Gets or sets the discription of this command. + /// public string Description { get; set; } + + /// + /// Gets or sets the options for this command. + /// public Optional> Options { get; set; } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs index cd119cf4b..122f2c599 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs @@ -7,32 +7,37 @@ using System.Threading.Tasks; namespace Discord { /// - /// The base command model that belongs to an application. see + /// The base command model that belongs to an application. see /// public interface IApplicationCommand : ISnowflakeEntity { /// - /// Gets the unique id of the command + /// Gets the unique id of the command. /// ulong Id { get; } /// - /// Gets the unique id of the parent application + /// Gets the unique id of the parent application. /// ulong ApplicationId { get; } /// - /// The name of the command + /// The name of the command. /// string Name { get; } /// - /// The description of the command + /// The description of the command. /// string Description { get; } /// - /// Modifies this command + /// If the option is a subcommand or subcommand group type, this nested options will be the parameters. + /// + IEnumerable? Options { get; } + + /// + /// Modifies this command. /// /// The delegate containing the properties to modify the command with. /// The options to be used when sending the request. @@ -40,7 +45,5 @@ namespace Discord /// A task that represents the asynchronous modification operation. /// Task ModifyAsync(Action func, RequestOptions options = null); - - IEnumerable? Options { get; } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionData.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionData.cs index c2b00ac4a..6f700d898 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionData.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionData.cs @@ -7,22 +7,22 @@ using System.Threading.Tasks; namespace Discord { /// - /// Represents data of an Interaction Command, see + /// Represents data of an Interaction Command, see /// public interface IApplicationCommandInteractionData { /// - /// The snowflake id of this command + /// The snowflake id of this command /// ulong Id { get; } /// - /// The name of this command + /// The name of this command /// string Name { get; } /// - /// The params + values from the user + /// The params + values from the user /// IReadOnlyCollection Options { get; } } diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionDataOption.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionDataOption.cs index 931c12f2a..871eb4ae6 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionDataOption.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionDataOption.cs @@ -7,22 +7,25 @@ using System.Threading.Tasks; namespace Discord { /// - /// Represents a option group for a command, see + /// Represents a option group for a command, see /// public interface IApplicationCommandInteractionDataOption { /// - /// The name of the parameter + /// The name of the parameter. /// string Name { get; } /// - /// The value of the pair + /// The value of the pair. + /// + /// This objects type can be any one of the option types in + /// /// - ApplicationCommandOptionType? Value { get; } + object? Value { get; } /// - /// Present if this option is a group or subcommand + /// Present if this option is a group or subcommand. /// IReadOnlyCollection Options { get; } diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs index 466c2a3fc..14e25a26e 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs @@ -7,42 +7,42 @@ using System.Threading.Tasks; namespace Discord { /// - /// Options for the , see + /// Options for the , see The docs. /// public interface IApplicationCommandOption { /// - /// The type of this + /// The type of this . /// ApplicationCommandOptionType Type { get; } /// - /// The name of this command option, 1-32 character name. + /// The name of this command option, 1-32 character name. /// string Name { get; } /// - /// The discription of this command option, 1-100 character description + /// The discription of this command option, 1-100 character description. /// string Description { get; } /// - /// the first required option for the user to complete--only one option can be default + /// The first required option for the user to complete--only one option can be default. /// bool? Default { get; } /// - /// if the parameter is required or optional--default + /// If the parameter is required or optional, default is . /// bool? Required { get; } /// - /// choices for string and int types for the user to pick from + /// Choices for string and int types for the user to pick from. /// IEnumerable? Choices { get; } /// - /// if the option is a subcommand or subcommand group type, this nested options will be the parameters + /// if the option is a subcommand or subcommand group type, this nested options will be the parameters. /// IEnumerable? Options { get; } } diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs index d7d81ab0d..3b4c7c249 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs @@ -7,17 +7,17 @@ using System.Threading.Tasks; namespace Discord { /// - /// Specifies choices for command group + /// Specifies choices for command group. /// public interface IApplicationCommandOptionChoice { /// - /// 1-100 character choice name + /// 1-100 character choice name. /// string Name { get; } /// - /// value of the choice + /// value of the choice. /// string Value { get; } diff --git a/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs index 80b0e9153..2cc0faf4f 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs @@ -7,48 +7,36 @@ using System.Threading.Tasks; namespace Discord { /// - /// An interaction is the base "thing" that is sent when a user invokes a command, and is the same for Slash Commands and other future interaction types. - /// see + /// Represents a discord interaction + /// + /// An interaction is the base "thing" that is sent when a user invokes a command, and is the same for Slash Commands + /// and other future interaction types. see . + /// /// public interface IDiscordInteraction : ISnowflakeEntity { /// - /// id of the interaction + /// The id of the interaction. /// ulong Id { get; } /// - /// The type of this + /// The type of this . /// InteractionType Type { get; } /// - /// The command data payload + /// The command data payload. /// IApplicationCommandInteractionData? Data { get; } /// - /// The guild it was sent from - /// - ulong GuildId { get; } - - /// - /// The channel it was sent from - /// - ulong ChannelId { get; } - - /// - /// Guild member id for the invoking user - /// - ulong MemberId { get; } - - /// - /// A continuation token for responding to the interaction + /// A continuation token for responding to the interaction. /// string Token { get; } /// - /// read-only property, always 1 + /// read-only property, always 1. /// int Version { get; } } diff --git a/src/Discord.Net.Core/Entities/Interactions/InteractionResponseType.cs b/src/Discord.Net.Core/Entities/Interactions/InteractionResponseType.cs index afb1ff13c..99a228358 100644 --- a/src/Discord.Net.Core/Entities/Interactions/InteractionResponseType.cs +++ b/src/Discord.Net.Core/Entities/Interactions/InteractionResponseType.cs @@ -7,32 +7,32 @@ using System.Threading.Tasks; namespace Discord { /// - /// The response type for an + /// The response type for an . /// public enum InteractionResponseType : byte { /// - /// ACK a Ping + /// ACK a Ping. /// Pong = 1, /// - /// ACK a command without sending a message, eating the user's input + /// ACK a command without sending a message, eating the user's input. /// Acknowledge = 2, /// - /// Respond with a message, eating the user's input + /// Respond with a message, eating the user's input. /// ChannelMessage = 3, /// - /// respond with a message, showing the user's input + /// Respond with a message, showing the user's input. /// ChannelMessageWithSource = 4, /// - /// ACK a command without sending a message, showing the user's input + /// ACK a command without sending a message, showing the user's input. /// ACKWithSource = 5 } diff --git a/src/Discord.Net.Core/Entities/Interactions/InteractionType.cs b/src/Discord.Net.Core/Entities/Interactions/InteractionType.cs index 0b5f32f1f..f95960586 100644 --- a/src/Discord.Net.Core/Entities/Interactions/InteractionType.cs +++ b/src/Discord.Net.Core/Entities/Interactions/InteractionType.cs @@ -7,17 +7,17 @@ using System.Threading.Tasks; namespace Discord { /// - /// Represents a type of Interaction from discord. + /// Represents a type of Interaction from discord. /// public enum InteractionType : byte { /// - /// A ping from discord + /// A ping from discord. /// Ping = 1, /// - /// An sent from discord + /// An sent from discord. /// ApplicationCommand = 2 } diff --git a/src/Discord.Net.Core/Entities/Messages/MessageType.cs b/src/Discord.Net.Core/Entities/Messages/MessageType.cs index ad1f3a3cd..e6a117ba5 100644 --- a/src/Discord.Net.Core/Entities/Messages/MessageType.cs +++ b/src/Discord.Net.Core/Entities/Messages/MessageType.cs @@ -64,5 +64,12 @@ namespace Discord /// Only available in API v8. /// Reply = 19, + /// + /// The message is an Application Command + /// + /// + /// Only available in API v8 + /// + ApplicationCommand = 20 } } diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionDataOption.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionDataOption.cs index 1d1592bb8..84f2e3f7a 100644 --- a/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionDataOption.cs +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionDataOption.cs @@ -13,7 +13,7 @@ namespace Discord.API public string Name { get; set; } [JsonProperty("value")] - public Optional Value { get; set; } + public Optional Value { get; set; } [JsonProperty("options")] public Optional> Options { get; set; } diff --git a/src/Discord.Net.Rest/API/Common/InteractionApplicationCommandCallbackData.cs b/src/Discord.Net.Rest/API/Common/InteractionApplicationCommandCallbackData.cs index 3d9434d94..6637c176c 100644 --- a/src/Discord.Net.Rest/API/Common/InteractionApplicationCommandCallbackData.cs +++ b/src/Discord.Net.Rest/API/Common/InteractionApplicationCommandCallbackData.cs @@ -13,12 +13,19 @@ namespace Discord.API public Optional TTS { get; set; } [JsonProperty("content")] - public string Content { get; set; } + public Optional Content { get; set; } [JsonProperty("embeds")] public Optional Embeds { get; set; } [JsonProperty("allowed_mentions")] public Optional AllowedMentions { get; set; } + + public InteractionApplicationCommandCallbackData() { } + public InteractionApplicationCommandCallbackData(string text) + { + this.Content = text; + } + } } diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 60a82d799..d9da6503a 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -787,13 +787,7 @@ namespace Discord.API //Interactions public async Task GetGlobalApplicationCommandsAsync(RequestOptions options = null) - { - try - { - return await SendAsync("GET", $"applications/{this.CurrentUserId}/commands", options: options).ConfigureAwait(false); - } - catch (HttpException ex) { return null; } - } + => await SendAsync("GET", $"applications/{this.CurrentUserId}/commands", options: options).ConfigureAwait(false); public async Task CreateGlobalApplicationCommandAsync(ApplicationCommandParams command, RequestOptions options = null) { @@ -844,21 +838,53 @@ namespace Discord.API => await SendAsync("DELETE", $"applications/{this.CurrentUserId}/guilds/{guildId}/commands/{commandId}", options: options).ConfigureAwait(false); //Interaction Responses - public async Task CreateInteractionResponse(InteractionResponse response, string interactionId, string interactionToken, RequestOptions options = null) + public async Task CreateInteractionResponse(InteractionResponse response, ulong interactionId, string interactionToken, RequestOptions options = null) { - if(response.Data.IsSpecified) - Preconditions.AtMost(response.Data.Value.Content.Length, 2000, nameof(response.Data.Value.Content)); - - await SendJsonAsync("POST", $"/interactions/{interactionId}/{interactionToken}/callback", response, options: options); + if(response.Data.IsSpecified && response.Data.Value.Content.IsSpecified) + Preconditions.AtMost(response.Data.Value.Content.Value.Length, 2000, nameof(response.Data.Value.Content)); + + options = RequestOptions.CreateOrClone(options); + + await SendJsonAsync("POST", () => $"interactions/{interactionId}/{interactionToken}/callback", response, new BucketIds(), options: options); } public async Task ModifyInteractionResponse(ModifyInteractionResponseParams args, string interactionToken, RequestOptions options = null) - => await SendJsonAsync("POST", $"/webhooks/{this.CurrentUserId}/{interactionToken}/messages/@original", args, options: options); + => await SendJsonAsync("POST", $"webhooks/{this.CurrentUserId}/{interactionToken}/messages/@original", args, options: options); public async Task DeleteInteractionResponse(string interactionToken, RequestOptions options = null) - => await SendAsync("DELETE", $"/webhooks/{this.CurrentUserId}/{interactionToken}/messages/@original", options: options); + => await SendAsync("DELETE", $"webhooks/{this.CurrentUserId}/{interactionToken}/messages/@original", options: options); - public async Task CreateInteractionFollowupMessage() + public async Task CreateInteractionFollowupMessage(CreateWebhookMessageParams args, string token, RequestOptions options = null) { + if (!args.Embeds.IsSpecified || args.Embeds.Value == null || args.Embeds.Value.Length == 0) + Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); + + if (args.Content?.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentException(message: $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", paramName: nameof(args.Content)); + + options = RequestOptions.CreateOrClone(options); + + return await SendJsonAsync("POST", $"webhooks/{CurrentUserId}/{token}?wait=true", args, options: options).ConfigureAwait(false); + } + + public async Task ModifyInteractionFollowupMessage(CreateWebhookMessageParams args, ulong id, string token, RequestOptions options = null) + { + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotEqual(id, 0, nameof(id)); + + if (args.Content?.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentException(message: $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", paramName: nameof(args.Content)); + + options = RequestOptions.CreateOrClone(options); + + return await SendJsonAsync("PATCH", $"webhooks/{CurrentUserId}/{token}/messages/{id}", args, options: options).ConfigureAwait(false); + } + + public async Task DeleteInteractionFollowupMessage(ulong id, string token, RequestOptions options = null) + { + Preconditions.NotEqual(id, 0, nameof(id)); + + options = RequestOptions.CreateOrClone(options); + await SendAsync("DELETE", $"webhooks/{CurrentUserId}/{token}/messages/{id}", options: options).ConfigureAwait(false); } //Guilds diff --git a/src/Discord.Net.Rest/Entities/Interactions/ApplicationCommands/ApplicationCommandHelper.cs b/src/Discord.Net.Rest/Entities/Interactions/ApplicationCommands/ApplicationCommandHelper.cs deleted file mode 100644 index 40dd1f6fc..000000000 --- a/src/Discord.Net.Rest/Entities/Interactions/ApplicationCommands/ApplicationCommandHelper.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Model = Discord.API.ApplicationCommand; - -namespace Discord.Rest -{ - internal static class ApplicationCommandHelper - { - public static async Task ModifyAsync(IApplicationCommand command, BaseDiscordClient client, - Action func, RequestOptions options) - { - if (func == null) - throw new ArgumentNullException(nameof(func)); - - var args = new ApplicationCommandProperties(); - func(args); - - var apiArgs = new Discord.API.Rest.ApplicationCommandParams() - { - Description = args.Description, - Name = args.Name, - Options = args.Options.IsSpecified - ? args.Options.Value.Select(x => new API.ApplicationCommandOption(x)).ToArray() - : Optional.Unspecified, - }; - - return await client.ApiClient.ModifyGlobalApplicationCommandAsync(apiArgs, command.Id, options); - } - } -} diff --git a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs new file mode 100644 index 000000000..a275a7d0a --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs @@ -0,0 +1,21 @@ +using Discord.API; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.Rest +{ + internal static class InteractionHelper + { + internal static async Task SendFollowupAsync(BaseDiscordClient client, API.Rest.CreateWebhookMessageParams args, + string token, IMessageChannel channel, RequestOptions options = null) + { + var model = await client.ApiClient.CreateInteractionFollowupMessage(args, token, options); + + var entity = RestUserMessage.Create(client, channel, client.CurrentUser, model); + return entity; + } + } +} diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs index 966aec7fa..7b8a245cc 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs @@ -23,7 +23,7 @@ namespace Discord.WebSocket /// /// - public event Func ChannelCreated + public event Func ChannelCreated { add { _channelCreatedEvent.Add(value); } remove { _channelCreatedEvent.Remove(value); } @@ -70,7 +70,7 @@ namespace Discord.WebSocket public event Func ChannelUpdated { add { _channelUpdatedEvent.Add(value); } remove { _channelUpdatedEvent.Remove(value); } - } + } internal readonly AsyncEvent> _channelUpdatedEvent = new AsyncEvent>(); //Messages @@ -351,7 +351,7 @@ namespace Discord.WebSocket add { _guildMemberUpdatedEvent.Add(value); } remove { _guildMemberUpdatedEvent.Remove(value); } } - internal readonly AsyncEvent> _guildMemberUpdatedEvent = new AsyncEvent>(); + internal readonly AsyncEvent> _guildMemberUpdatedEvent = new AsyncEvent>(); /// Fired when a user joins, leaves, or moves voice channels. public event Func UserVoiceStateUpdated { add { _userVoiceStateUpdatedEvent.Add(value); } @@ -361,7 +361,7 @@ namespace Discord.WebSocket /// Fired when the bot connects to a Discord voice server. public event Func VoiceServerUpdated { - add { _voiceServerUpdatedEvent.Add(value); } + add { _voiceServerUpdatedEvent.Add(value); } remove { _voiceServerUpdatedEvent.Remove(value); } } internal readonly AsyncEvent> _voiceServerUpdatedEvent = new AsyncEvent>(); @@ -431,5 +431,26 @@ namespace Discord.WebSocket remove { _inviteDeletedEvent.Remove(value); } } internal readonly AsyncEvent> _inviteDeletedEvent = new AsyncEvent>(); + + //Interactions + /// + /// Fired when an Interaction is created. + /// + /// + /// + /// This event is fired when an interaction is created. The event handler must return a + /// and accept a as its parameter. + /// + /// + /// The interaction created will be passed into the parameter. + /// + /// + public event Func InteractionCreated + { + add { _interactionCreatedEvent.Add(value); } + remove { _interactionCreatedEvent.Remove(value); } + } + internal readonly AsyncEvent> _interactionCreatedEvent = new AsyncEvent>(); + } } diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index a2c89d4e5..7d62596f7 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -378,6 +378,8 @@ namespace Discord.WebSocket client.InviteCreated += (invite) => _inviteCreatedEvent.InvokeAsync(invite); client.InviteDeleted += (channel, invite) => _inviteDeletedEvent.InvokeAsync(channel, invite); + + client.InteractionCreated += (interaction) => _interactionCreatedEvent.InvokeAsync(interaction); } //IDiscordClient diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 67e24e616..e2029e471 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -73,6 +73,7 @@ namespace Discord.WebSocket internal bool AlwaysDownloadUsers { get; private set; } internal int? HandlerTimeout { get; private set; } internal bool? ExclusiveBulkDelete { get; private set; } + internal bool AlwaysAcknowledgeInteractions { get; private set; } internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; /// @@ -134,6 +135,7 @@ namespace Discord.WebSocket UdpSocketProvider = config.UdpSocketProvider; WebSocketProvider = config.WebSocketProvider; AlwaysDownloadUsers = config.AlwaysDownloadUsers; + AlwaysAcknowledgeInteractions = config.AlwaysAcknowledgeInteractions; HandlerTimeout = config.HandlerTimeout; ExclusiveBulkDelete = config.ExclusiveBulkDelete; State = new ClientState(0, 0); @@ -1792,11 +1794,12 @@ namespace Discord.WebSocket return; } - if(data.Type == InteractionType.ApplicationCommand) - { - // TODO: call command - - } + var interaction = SocketInteraction.Create(this, data); + + if (this.AlwaysAcknowledgeInteractions) + await interaction.AcknowledgeAsync().ConfigureAwait(false); + + await TimedInvokeAsync(_interactionCreatedEvent, nameof(InteractionCreated), interaction).ConfigureAwait(false); } else { diff --git a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs index a45d4f5be..b93f6d33d 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs @@ -105,6 +105,29 @@ namespace Discord.WebSocket /// public bool AlwaysDownloadUsers { get; set; } = false; + /// + /// Gets or sets whether or not interactions are acknowledge with source. + /// + /// + /// + /// Discord interactions will not go thru in chat until the client responds to them. With this option set to + /// the client will automatically acknowledge the interaction with . + /// see the docs on + /// responding to interactions for more info + /// + /// + /// With this option set to you will have to acknowledge the interaction with + /// , + /// only after the interaction is captured the origional slash command message will be visible. + /// + /// + /// Please note that manually acknowledging the interaction with a message reply will not provide any return data. + /// By autmatically acknowledging the interaction without sending the message will allow for follow up responses to + /// be used, follow up responses return the message data sent. + /// + /// + public bool AlwaysAcknowledgeInteractions { get; set; } = true; + /// /// Gets or sets the timeout for event handlers, in milliseconds, after which a warning will be logged. /// Setting this property to nulldisables this check. diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs index dbf2c369b..1eb8fdca0 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs @@ -1,3 +1,4 @@ +using Discord.Rest; using System; using System.Collections.Generic; using System.Linq; @@ -5,28 +6,63 @@ using System.Text; using System.Threading.Tasks; using Model = Discord.API.Gateway.InteractionCreated; -namespace Discord.WebSocket.Entities.Interaction +namespace Discord.WebSocket { + /// + /// Represents an Interaction recieved over the gateway + /// public class SocketInteraction : SocketEntity, IDiscordInteraction { + /// + /// The this interaction was used in + /// public SocketGuild Guild => Discord.GetGuild(GuildId); + + /// + /// The this interaction was used in + /// public SocketTextChannel Channel => Guild.GetTextChannel(ChannelId); + + /// + /// The who triggered this interaction + /// public SocketGuildUser Member => Guild.GetUser(MemberId); + /// + /// The type of this interaction + /// public InteractionType Type { get; private set; } + + /// + /// The data associated with this interaction + /// public IApplicationCommandInteractionData Data { get; private set; } + + /// + /// The token used to respond to this interaction + /// public string Token { get; private set; } + + /// + /// The version of this interaction + /// public int Version { get; private set; } + public DateTimeOffset CreatedAt { get; } - public ulong GuildId { get; private set; } - public ulong ChannelId { get; private set; } - public ulong MemberId { get; private set; } + /// + /// if the token is valid for replying to, otherwise + /// + public bool IsValidToken + => CheckToken(); + + private ulong GuildId { get; set; } + private ulong ChannelId { get; set; } + private ulong MemberId { get; set; } - internal SocketInteraction(DiscordSocketClient client, ulong id) : base(client, id) { @@ -52,6 +88,125 @@ namespace Discord.WebSocket.Entities.Interaction this.MemberId = model.Member.User.Id; this.Type = model.Type; } + private bool CheckToken() + { + // Tokens last for 15 minutes according to https://discord.com/developers/docs/interactions/slash-commands#responding-to-an-interaction + return (DateTime.UtcNow - this.CreatedAt.UtcDateTime).TotalMinutes >= 15d; + } + + /// + /// Responds to an Interaction, eating its input + /// + /// If you have set to , this method + /// will be obsolete and will use + /// + /// + /// The text of the message to be sent + /// if the message should be read out by a text-to-speech reader, otherwise + /// A to send with this response + /// The type of response to this Interaction + /// The allowed mentions for this response + /// The request options for this response + /// + /// The sent as the response. If this is the first acknowledgement, it will return null; + /// + /// Message content is too long, length must be less or equal to . + + public async Task RespondAsync(string text = null, bool isTTS = false, Embed embed = null, InteractionResponseType Type = InteractionResponseType.ChannelMessageWithSource, AllowedMentions allowedMentions = null, RequestOptions options = null) + { + if (Type == InteractionResponseType.ACKWithSource || Type == InteractionResponseType.ACKWithSource || 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(); + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + // check that user flag and user Id list are exclusive, same with role flag and role Id list + if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) + { + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) && + allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + { + throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions)); + } + + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) && + allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + { + throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions)); + } + } + + + var response = new API.InteractionResponse() + { + Type = Type, + Data = new API.InteractionApplicationCommandCallbackData(text) + { + AllowedMentions = allowedMentions?.ToModel(), + Embeds = embed != null + ? new API.Embed[] { embed.ToModel() } + : Optional.Unspecified, + TTS = isTTS + } + }; + + await Discord.Rest.ApiClient.CreateInteractionResponse(response, this.Id, Token, options); + return null; + } + + /// + /// Sends a followup message for this interaction + /// + /// The text of the message to be sent + /// if the message should be read out by a text-to-speech reader, otherwise + /// A to send with this response + /// The type of response to this Interaction + /// The allowed mentions for this response + /// The request options for this response + /// + /// The sent message + /// + public async Task FollowupAsync(string text = null, bool isTTS = false, Embed embed = null, InteractionResponseType Type = InteractionResponseType.ChannelMessageWithSource, + AllowedMentions allowedMentions = null, RequestOptions options = null) + { + if (Type == InteractionResponseType.ACKWithSource || Type == InteractionResponseType.ACKWithSource || Type == InteractionResponseType.Pong) + throw new InvalidOperationException($"Cannot use {Type} on a send message function"); + + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + var args = new API.Rest.CreateWebhookMessageParams(text) + { + IsTTS = isTTS, + Embeds = embed != null + ? new API.Embed[] { embed.ToModel() } + : Optional.Unspecified, + }; + + return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options); + + } + + /// + /// Acknowledges this interaction with the + /// + /// + /// A task that represents the asynchronous operation of acknowledging the interaction + /// + public async Task AcknowledgeAsync(RequestOptions options = null) + { + var response = new API.InteractionResponse() + { + Type = InteractionResponseType.ACKWithSource, + }; + + await Discord.Rest.ApiClient.CreateInteractionResponse(response, this.Id, Token, options).ConfigureAwait(false); + } } } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionData.cs index 8e2ae3bf5..4ff582480 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionData.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionData.cs @@ -6,7 +6,7 @@ using System.Text; using System.Threading.Tasks; using Model = Discord.API.ApplicationCommandInteractionData; -namespace Discord.WebSocket.Entities.Interaction +namespace Discord.WebSocket { public class SocketInteractionData : SocketEntity, IApplicationCommandInteractionData { diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionDataOption.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionDataOption.cs index 086ef1b87..26a7b9659 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionDataOption.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionDataOption.cs @@ -6,12 +6,12 @@ using System.Text; using System.Threading.Tasks; using Model = Discord.API.ApplicationCommandInteractionDataOption; -namespace Discord.WebSocket.Entities.Interaction +namespace Discord.WebSocket { public class SocketInteractionDataOption : IApplicationCommandInteractionDataOption { public string Name { get; private set; } - public ApplicationCommandOptionType? Value { get; private set; } + public object? Value { get; private set; } public IReadOnlyCollection Options { get; private set; }