From d35ba91312fccfb2718bde5c2737eef53c7886cb Mon Sep 17 00:00:00 2001 From: quin lynch Date: Wed, 16 Dec 2020 18:49:43 -0400 Subject: [PATCH 1/8] Rest features for ApplicationCommands --- .../Discord.Net.Commands.csproj | 2 +- .../ApplicationCommandOptionType.cs | 23 +++++ .../ApplicationCommandProperties.cs | 15 +++ .../Interactions/IApplicationCommand.cs | 36 ++++++++ .../IApplicationCommandInteractionData.cs | 15 +++ ...ApplicationCommandInteractionDataOption.cs | 16 ++++ .../Interactions/IApplicationCommandOption.cs | 49 ++++++++++ .../IApplicationCommandOptionChoice.cs | 22 +++++ .../Interactions/IDiscordInteraction.cs | 27 ++++++ .../Entities/Interactions/InteractionType.cs | 14 +++ .../API/Common/ApplicationCommand.cs | 17 ++++ .../ApplicationCommandInteractionData.cs | 21 +++++ ...ApplicationCommandInteractionDataOption.cs | 21 +++++ .../API/Common/ApplicationCommandOption.cs | 57 ++++++++++++ .../Common/ApplicationCommandOptionChoice.cs | 18 ++++ .../API/Rest/ApplicationCommandParams.cs | 30 ++++++ src/Discord.Net.Rest/Discord.Net.Rest.csproj | 2 +- src/Discord.Net.Rest/DiscordRestApiClient.cs | 92 ++++++++++++++++++- .../ApplicationCommandHelper.cs | 33 +++++++ .../API/Gateway/InteractionCreated.cs | 37 ++++++++ .../Discord.Net.WebSocket.csproj | 2 +- .../DiscordSocketClient.cs | 26 ++++++ .../Entities/Interaction/SocketInteraction.cs | 34 +++++++ 23 files changed, 605 insertions(+), 4 deletions(-) create mode 100644 src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs create mode 100644 src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs create mode 100644 src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs create mode 100644 src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionData.cs create mode 100644 src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionDataOption.cs create mode 100644 src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs create mode 100644 src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs create mode 100644 src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs create mode 100644 src/Discord.Net.Core/Entities/Interactions/InteractionType.cs create mode 100644 src/Discord.Net.Rest/API/Common/ApplicationCommand.cs create mode 100644 src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionData.cs create mode 100644 src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionDataOption.cs create mode 100644 src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs create mode 100644 src/Discord.Net.Rest/API/Common/ApplicationCommandOptionChoice.cs create mode 100644 src/Discord.Net.Rest/API/Rest/ApplicationCommandParams.cs create mode 100644 src/Discord.Net.Rest/Entities/Interactions/ApplicationCommands/ApplicationCommandHelper.cs create mode 100644 src/Discord.Net.WebSocket/API/Gateway/InteractionCreated.cs create mode 100644 src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs diff --git a/src/Discord.Net.Commands/Discord.Net.Commands.csproj b/src/Discord.Net.Commands/Discord.Net.Commands.csproj index 21869d91c..d64678d7c 100644 --- a/src/Discord.Net.Commands/Discord.Net.Commands.csproj +++ b/src/Discord.Net.Commands/Discord.Net.Commands.csproj @@ -1,6 +1,6 @@ - + Discord.Net.Commands Discord.Commands diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs new file mode 100644 index 000000000..9d876a3bf --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// The option type of the Slash command parameter, See + /// + public enum ApplicationCommandOptionType : byte + { + SubCommand = 1, + SubCommandGroup = 2, + String = 3, + Integer = 4, + Boolean = 5, + User = 6, + Channel = 7, + Role = 8 + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs new file mode 100644 index 000000000..5b0c0ecf6 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public class ApplicationCommandProperties + { + public string Name { get; set; } + public string Description { get; set; } + 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 new file mode 100644 index 000000000..903da5bd4 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// The base command model that belongs to an application. see + /// + public interface IApplicationCommand : ISnowflakeEntity + { + /// + /// Gets the unique id of the command + /// + ulong Id { get; } + + /// + /// Gets the unique id of the parent application + /// + ulong ApplicationId { get; } + + /// + /// The name of the command + /// + string Name { get; } + + /// + /// The description of the command + /// + string Description { get; } + + IEnumerable? Options { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionData.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionData.cs new file mode 100644 index 000000000..73d468fe0 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionData.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IApplicationCommandInteractionData + { + ulong Id { get; } + string Name { get; } + IEnumerable Options { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionDataOption.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionDataOption.cs new file mode 100644 index 000000000..7454e0c99 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionDataOption.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IApplicationCommandInteractionDataOption + { + string Name { get; } + ApplicationCommandOptionType Value { get; } + IEnumerable Options { get; } + + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs new file mode 100644 index 000000000..466c2a3fc --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Options for the , see + /// + public interface IApplicationCommandOption + { + /// + /// The type of this + /// + ApplicationCommandOptionType Type { get; } + + /// + /// The name of this command option, 1-32 character name. + /// + string Name { get; } + + /// + /// 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 + /// + bool? Default { get; } + + /// + /// if the parameter is required or optional--default + /// + bool? Required { get; } + + /// + /// 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 + /// + IEnumerable? Options { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs new file mode 100644 index 000000000..7e8e7668d --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IApplicationCommandOptionChoice + { + /// + /// 1-100 character choice name + /// + string Name { get; } + + /// + /// 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 new file mode 100644 index 000000000..02398a190 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +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 + /// + public interface IDiscordInteraction : ISnowflakeEntity + { + /// + /// id of the interaction + /// + ulong Id { get; } + InteractionType Type { get; } + IApplicationCommandInteractionData? Data { get; } + ulong GuildId { get; } + ulong ChannelId { get; } + IGuildUser Member { get; } + string Token { get; } + int Version { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/InteractionType.cs b/src/Discord.Net.Core/Entities/Interactions/InteractionType.cs new file mode 100644 index 000000000..19a5eb829 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/InteractionType.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public enum InteractionType : byte + { + Ping = 1, + ApplicationCommand = 2 + } +} diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs new file mode 100644 index 000000000..8ef109b64 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class ApplicationCommand + { + public ulong Id { get; set; } + public ulong ApplicationId { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public IEnumerable + } +} diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionData.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionData.cs new file mode 100644 index 000000000..65c1403f3 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionData.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class ApplicationCommandInteractionData + { + [JsonProperty("id")] + public ulong Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("options")] + public Optional> + } +} diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionDataOption.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionDataOption.cs new file mode 100644 index 000000000..1d1592bb8 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionDataOption.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class ApplicationCommandInteractionDataOption + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("value")] + public Optional Value { get; set; } + + [JsonProperty("options")] + public Optional> Options { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs new file mode 100644 index 000000000..ad7748594 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs @@ -0,0 +1,57 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class ApplicationCommandOption + { + [JsonProperty("type")] + public ApplicationCommandOptionType Type { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("default")] + public Optional Default { get; set; } + + [JsonProperty("required")] + public Optional Required { get; set; } + + [JsonProperty("choices")] + public Optional Choices { get; set; } + + [JsonProperty("options")] + public Optional Options { get; set; } + + public ApplicationCommandOption() { } + + public ApplicationCommandOption(IApplicationCommandOption cmd) + { + this.Choices = cmd.Choices.Select(x => new ApplicationCommandOptionChoice() + { + Name = x.Name, + Value = x.Value + }).ToArray(); + + this.Options = cmd.Options.Select(x => new ApplicationCommandOption(x)).ToArray(); + + this.Required = cmd.Required.HasValue + ? cmd.Required.Value + : Optional.Unspecified; + this.Default = cmd.Default.HasValue + ? cmd.Default.Value + : Optional.Unspecified; + + this.Name = cmd.Name; + this.Type = cmd.Type; + this.Description = cmd.Description; + } + } +} diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommandOptionChoice.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommandOptionChoice.cs new file mode 100644 index 000000000..00179dde1 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommandOptionChoice.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class ApplicationCommandOptionChoice + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("value")] + public string Value { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/ApplicationCommandParams.cs b/src/Discord.Net.Rest/API/Rest/ApplicationCommandParams.cs new file mode 100644 index 000000000..a674093c5 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ApplicationCommandParams.cs @@ -0,0 +1,30 @@ +using Discord.API; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API.Rest +{ + internal class ApplicationCommandParams + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("options")] + public Optional Options { get; set; } + + public ApplicationCommandParams() { } + public ApplicationCommandParams(string name, string description, ApplicationCommandOption[] options = null) + { + this.Name = name; + this.Description = description; + this.Options = Optional.Create(options); + } + } +} diff --git a/src/Discord.Net.Rest/Discord.Net.Rest.csproj b/src/Discord.Net.Rest/Discord.Net.Rest.csproj index 8407abfd6..2737ec0d4 100644 --- a/src/Discord.Net.Rest/Discord.Net.Rest.csproj +++ b/src/Discord.Net.Rest/Discord.Net.Rest.csproj @@ -1,6 +1,6 @@ - + Discord.Net.Rest Discord.Rest diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 592ad7e92..29e8dca0e 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -47,7 +47,6 @@ namespace Discord.API internal ulong? CurrentUserId { get; set; } public RateLimitPrecision RateLimitPrecision { get; private set; } internal bool UseSystemClock { get; set; } - internal JsonSerializer Serializer => _serializer; /// Unknown OAuth token type. @@ -786,6 +785,97 @@ namespace Discord.API await SendAsync("DELETE", () => $"channels/{channelId}/recipients/{userId}", ids, options: options).ConfigureAwait(false); } + //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; } + } + + public async Task CreateGlobalApplicationCommandAsync(ApplicationCommandParams command, RequestOptions options = null) + { + Preconditions.NotNull(command, nameof(command)); + Preconditions.AtMost(command.Name.Length, 32, nameof(command.Name)); + Preconditions.AtLeast(command.Name.Length, 3, nameof(command.Name)); + Preconditions.AtMost(command.Description.Length, 100, nameof(command.Description)); + Preconditions.AtLeast(command.Description.Length, 1, nameof(command.Description)); + + try + { + return await SendJsonAsync("POST", $"applications/{this.CurrentUserId}/commands", command, options: options).ConfigureAwait(false); + } + catch (HttpException ex) { return null; } + } + public async Task EditGlobalApplicationCommandAsync(ApplicationCommandParams command, ulong commandId, RequestOptions options = null) + { + Preconditions.NotNull(command, nameof(command)); + Preconditions.AtMost(command.Name.Length, 32, nameof(command.Name)); + Preconditions.AtLeast(command.Name.Length, 3, nameof(command.Name)); + Preconditions.AtMost(command.Description.Length, 100, nameof(command.Description)); + Preconditions.AtLeast(command.Description.Length, 1, nameof(command.Description)); + + try + { + return await SendJsonAsync("PATCH", $"applications/{this.CurrentUserId}/commands/{commandId}", command, options: options).ConfigureAwait(false); + } + catch (HttpException ex) { return null; } + } + public async Task DeleteGlobalApplicationCommandAsync(ulong commandId, RequestOptions options = null) + { + try + { + await SendAsync("DELETE", $"applications/{this.CurrentUserId}/commands/{commandId}", options: options).ConfigureAwait(false); + } + catch (HttpException ex) { return; } + } + public async Task GetGuildApplicationCommandAsync(ulong guildId, RequestOptions options = null) + { + try + { + return await SendAsync("GET", $"applications/{this.CurrentUserId}/guilds/{guildId}/commands", options: options).ConfigureAwait(false); + } + catch (HttpException ex) { return null; } + } + public async Task CreateGuildApplicationCommandAsync(ApplicationCommandParams command, ulong guildId, RequestOptions options = null) + { + Preconditions.NotNull(command, nameof(command)); + Preconditions.AtMost(command.Name.Length, 32, nameof(command.Name)); + Preconditions.AtLeast(command.Name.Length, 3, nameof(command.Name)); + Preconditions.AtMost(command.Description.Length, 100, nameof(command.Description)); + Preconditions.AtLeast(command.Description.Length, 1, nameof(command.Description)); + + try + { + return await SendJsonAsync("POST", $"applications/{this.CurrentUserId}/guilds/{guildId}/commands", command, options: options).ConfigureAwait(false); + } + catch (HttpException ex) { return null; } + } + public async Task EditGuildApplicationCommandAsync(ApplicationCommandParams command, ulong guildId, ulong commandId, RequestOptions options = null) + { + Preconditions.NotNull(command, nameof(command)); + Preconditions.AtMost(command.Name.Length, 32, nameof(command.Name)); + Preconditions.AtLeast(command.Name.Length, 3, nameof(command.Name)); + Preconditions.AtMost(command.Description.Length, 100, nameof(command.Description)); + Preconditions.AtLeast(command.Description.Length, 1, nameof(command.Description)); + + try + { + return await SendJsonAsync("PATCH", $"applications/{this.CurrentUserId}/guilds/{guildId}/commands/{commandId}", command, options: options).ConfigureAwait(false); + } + catch (HttpException ex) { return null; } + } + public async Task DeleteGuildApplicationCommandAsync(ulong guildId, ulong commandId, RequestOptions options = null) + { + try + { + await SendAsync("DELETE", $"applications/{this.CurrentUserId}/guilds/{guildId}/commands/{commandId}", options: options).ConfigureAwait(false); + } + catch (HttpException ex) { return; } + } + //Guilds public async Task GetGuildAsync(ulong guildId, bool withCounts, RequestOptions options = null) { diff --git a/src/Discord.Net.Rest/Entities/Interactions/ApplicationCommands/ApplicationCommandHelper.cs b/src/Discord.Net.Rest/Entities/Interactions/ApplicationCommands/ApplicationCommandHelper.cs new file mode 100644 index 000000000..dd1141d99 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/ApplicationCommands/ApplicationCommandHelper.cs @@ -0,0 +1,33 @@ +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.EditGlobalApplicationCommandAsync(apiArgs, command.Id, options); + } + } +} diff --git a/src/Discord.Net.WebSocket/API/Gateway/InteractionCreated.cs b/src/Discord.Net.WebSocket/API/Gateway/InteractionCreated.cs new file mode 100644 index 000000000..8fb0cbd58 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Gateway/InteractionCreated.cs @@ -0,0 +1,37 @@ +using Discord.API; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API.Gateway +{ + internal class InteractionCreated + { + [JsonProperty("id")] + public ulong Id { get; set; } + + [JsonProperty("type")] + public InteractionType Type { get; set; } + + [JsonProperty("data")] + public Optional Data { get; set; } + + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + + [JsonProperty("member")] + public GuildMember Member { get; set; } + + [JsonProperty("token")] + public string Token { get; set; } + + [JsonProperty("version")] + public int Version { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj index 01aece130..1f65d0a15 100644 --- a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj +++ b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj @@ -1,6 +1,6 @@ - + Discord.Net.WebSocket Discord.WebSocket diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 0a2123ef2..0d78b405d 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1778,7 +1778,33 @@ namespace Discord.WebSocket } } break; + case "INTERACTION_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (INVITE_DELETE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + if (State.GetChannel(data.ChannelId) is SocketGuildChannel channel) + { + var guild = channel.Guild; + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + if(data.Type == InteractionType.ApplicationCommand) + { + // TODO: call command + + } + } + else + { + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + return; + } + } + break; //Ignored (User only) case "CHANNEL_PINS_ACK": await _gatewayLogger.DebugAsync("Ignored Dispatch (CHANNEL_PINS_ACK)").ConfigureAwait(false); diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs new file mode 100644 index 000000000..7786ce339 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.WebSocket.Entities.Interaction +{ + public class SocketInteraction : SocketEntity, IDiscordInteraction + { + public ulong Id { get; } + + public InteractionType Type { get; } + + public IApplicationCommandInteractionData Data { get; } + + public ulong GuildId { get; } + + public ulong ChannelId { get; } + + public IGuildUser Member { get; } + + public string Token { get; } + + public int Version { get; } + + public DateTimeOffset CreatedAt { get; } + public SocketInteraction(DiscordSocketClient client, ulong id) + : base(client, id) + { + + } + } +} From 27efb7d30d60775c097d32726b40b2d9829be8c0 Mon Sep 17 00:00:00 2001 From: quin lynch Date: Wed, 16 Dec 2020 18:58:40 -0400 Subject: [PATCH 2/8] Fixed couple of unfinished properties --- src/Discord.Net.Rest/API/Common/ApplicationCommand.cs | 2 +- .../API/Common/ApplicationCommandInteractionData.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs index 8ef109b64..9ae240e43 100644 --- a/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs @@ -12,6 +12,6 @@ namespace Discord.API public ulong ApplicationId { get; set; } public string Name { get; set; } public string Description { get; set; } - public IEnumerable + public ApplicationCommand[] Options { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionData.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionData.cs index 65c1403f3..b421edf6f 100644 --- a/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionData.cs +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionData.cs @@ -16,6 +16,6 @@ namespace Discord.API public string Name { get; set; } [JsonProperty("options")] - public Optional> + public Optional Options { get; set; } } } From fae3fbeda89ef43af501f6f8912e952c3d8ddd05 Mon Sep 17 00:00:00 2001 From: quin lynch Date: Thu, 17 Dec 2020 16:48:45 -0400 Subject: [PATCH 3/8] Worked on more rest routes for responding to application commands. Worked on SocketInteraction --- StyleAnalyzer.targets | 4 +- .../ApplicationCommandOptionType.cs | 31 +++++++++ .../ApplicationCommandProperties.cs | 4 ++ .../Interactions/IApplicationCommand.cs | 10 +++ .../IApplicationCommandInteractionData.cs | 16 ++++- ...ApplicationCommandInteractionDataOption.cs | 18 ++++- .../IApplicationCommandOptionChoice.cs | 3 + .../Interactions/IDiscordInteraction.cs | 30 ++++++++- .../Interactions/InteractionResponseType.cs | 39 +++++++++++ .../Entities/Interactions/InteractionType.cs | 10 +++ ...teractionApplicationCommandCallbackData.cs | 24 +++++++ .../API/Common/InteractionFollowupMessage.cs | 20 ++++++ .../API/Common/InteractionResponse.cs | 18 +++++ .../Rest/ModifyInteractionResponseParams.cs | 21 ++++++ src/Discord.Net.Rest/DiscordRestApiClient.cs | 67 +++++++------------ .../ApplicationCommandHelper.cs | 2 +- .../DiscordSocketClient.cs | 2 +- .../Entities/Interaction/SocketInteraction.cs | 55 ++++++++++----- .../Interaction/SocketInteractionData.cs | 37 ++++++++++ .../SocketInteractionDataOption.cs | 28 ++++++++ 20 files changed, 374 insertions(+), 65 deletions(-) create mode 100644 src/Discord.Net.Core/Entities/Interactions/InteractionResponseType.cs create mode 100644 src/Discord.Net.Rest/API/Common/InteractionApplicationCommandCallbackData.cs create mode 100644 src/Discord.Net.Rest/API/Common/InteractionFollowupMessage.cs create mode 100644 src/Discord.Net.Rest/API/Common/InteractionResponse.cs create mode 100644 src/Discord.Net.Rest/API/Rest/ModifyInteractionResponseParams.cs create mode 100644 src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionData.cs create mode 100644 src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionDataOption.cs diff --git a/StyleAnalyzer.targets b/StyleAnalyzer.targets index bbb90b800..2df86122a 100644 --- a/StyleAnalyzer.targets +++ b/StyleAnalyzer.targets @@ -1,9 +1,9 @@ - + diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs index 9d876a3bf..3a03e028b 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs @@ -11,13 +11,44 @@ namespace Discord /// public enum ApplicationCommandOptionType : byte { + /// + /// A sub command + /// SubCommand = 1, + + /// + /// A group of sub commands + /// SubCommandGroup = 2, + + /// + /// A of text + /// String = 3, + + /// + /// An + /// Integer = 4, + + /// + /// A + /// Boolean = 5, + + /// + /// A + /// User = 6, + + /// + /// A + /// Channel = 7, + + /// + /// A + /// Role = 8 } } diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs index 5b0c0ecf6..08406f844 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs @@ -6,6 +6,10 @@ using System.Threading.Tasks; namespace Discord { + /// + /// Provides properties that are used to modify a with the specified changes. + /// + /// public class ApplicationCommandProperties { public string Name { get; set; } diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs index 903da5bd4..cd119cf4b 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs @@ -31,6 +31,16 @@ namespace Discord /// string Description { get; } + /// + /// Modifies this command + /// + /// The delegate containing the properties to modify the command with. + /// The options to be used when sending the request. + /// + /// 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 73d468fe0..c2b00ac4a 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionData.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionData.cs @@ -6,10 +6,24 @@ using System.Threading.Tasks; namespace Discord { + /// + /// Represents data of an Interaction Command, see + /// public interface IApplicationCommandInteractionData { + /// + /// The snowflake id of this command + /// ulong Id { get; } + + /// + /// The name of this command + /// string Name { get; } - IEnumerable Options { get; } + + /// + /// 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 7454e0c99..931c12f2a 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionDataOption.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionDataOption.cs @@ -6,11 +6,25 @@ using System.Threading.Tasks; namespace Discord { + /// + /// Represents a option group for a command, see + /// public interface IApplicationCommandInteractionDataOption { + /// + /// The name of the parameter + /// string Name { get; } - ApplicationCommandOptionType Value { get; } - IEnumerable Options { get; } + + /// + /// The value of the pair + /// + ApplicationCommandOptionType? Value { get; } + + /// + /// Present if this option is a group or subcommand + /// + IReadOnlyCollection Options { get; } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs index 7e8e7668d..d7d81ab0d 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs @@ -6,6 +6,9 @@ using System.Threading.Tasks; namespace Discord { + /// + /// Specifies choices for command group + /// public interface IApplicationCommandOptionChoice { /// diff --git a/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs index 02398a190..80b0e9153 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs @@ -16,12 +16,40 @@ namespace Discord /// id of the interaction /// ulong Id { get; } + + /// + /// The type of this + /// InteractionType Type { get; } + + /// + /// 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; } - IGuildUser Member { get; } + + /// + /// Guild member id for the invoking user + /// + ulong MemberId { get; } + + /// + /// A continuation token for responding to the interaction + /// string Token { get; } + + /// + /// 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 new file mode 100644 index 000000000..afb1ff13c --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/InteractionResponseType.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// The response type for an + /// + public enum InteractionResponseType : byte + { + /// + /// ACK a Ping + /// + Pong = 1, + + /// + /// ACK a command without sending a message, eating the user's input + /// + Acknowledge = 2, + + /// + /// Respond with a message, eating the user's input + /// + ChannelMessage = 3, + + /// + /// respond with a message, showing the user's input + /// + ChannelMessageWithSource = 4, + + /// + /// 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 19a5eb829..0b5f32f1f 100644 --- a/src/Discord.Net.Core/Entities/Interactions/InteractionType.cs +++ b/src/Discord.Net.Core/Entities/Interactions/InteractionType.cs @@ -6,9 +6,19 @@ using System.Threading.Tasks; namespace Discord { + /// + /// Represents a type of Interaction from discord. + /// public enum InteractionType : byte { + /// + /// A ping from discord + /// Ping = 1, + + /// + /// An sent from discord + /// ApplicationCommand = 2 } } diff --git a/src/Discord.Net.Rest/API/Common/InteractionApplicationCommandCallbackData.cs b/src/Discord.Net.Rest/API/Common/InteractionApplicationCommandCallbackData.cs new file mode 100644 index 000000000..3d9434d94 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/InteractionApplicationCommandCallbackData.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class InteractionApplicationCommandCallbackData + { + [JsonProperty("tts")] + public Optional TTS { get; set; } + + [JsonProperty("content")] + public string Content { get; set; } + + [JsonProperty("embeds")] + public Optional Embeds { get; set; } + + [JsonProperty("allowed_mentions")] + public Optional AllowedMentions { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/InteractionFollowupMessage.cs b/src/Discord.Net.Rest/API/Common/InteractionFollowupMessage.cs new file mode 100644 index 000000000..28de67ee6 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/InteractionFollowupMessage.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class InteractionFollowupMessage + { + public string Content { get; set; } + public Optional Username { get; set; } + public Optional AvatarUrl { get; set; } + public Optional TTS { get; set; } + public Optional File { get; set; } + public Embed[] Embeds { get; set; } + + } +} diff --git a/src/Discord.Net.Rest/API/Common/InteractionResponse.cs b/src/Discord.Net.Rest/API/Common/InteractionResponse.cs new file mode 100644 index 000000000..6be48340b --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/InteractionResponse.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class InteractionResponse + { + [JsonProperty("type")] + public InteractionResponseType Type { get; set; } + + [JsonProperty("data")] + public Optional Data { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/ModifyInteractionResponseParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyInteractionResponseParams.cs new file mode 100644 index 000000000..4012debd7 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ModifyInteractionResponseParams.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API.Rest +{ + internal class ModifyInteractionResponseParams + { + [JsonProperty("content")] + public string Content { get; set; } + + [JsonProperty("embeds")] + public Optional Embeds { get; set; } + + [JsonProperty("allowed_mentions")] + public Optional AllowedMentions { get; set; } + } +} diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 29e8dca0e..60a82d799 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -803,13 +803,9 @@ namespace Discord.API Preconditions.AtMost(command.Description.Length, 100, nameof(command.Description)); Preconditions.AtLeast(command.Description.Length, 1, nameof(command.Description)); - try - { - return await SendJsonAsync("POST", $"applications/{this.CurrentUserId}/commands", command, options: options).ConfigureAwait(false); - } - catch (HttpException ex) { return null; } + return await SendJsonAsync("POST", $"applications/{this.CurrentUserId}/commands", command, options: options).ConfigureAwait(false); } - public async Task EditGlobalApplicationCommandAsync(ApplicationCommandParams command, ulong commandId, RequestOptions options = null) + public async Task ModifyGlobalApplicationCommandAsync(ApplicationCommandParams command, ulong commandId, RequestOptions options = null) { Preconditions.NotNull(command, nameof(command)); Preconditions.AtMost(command.Name.Length, 32, nameof(command.Name)); @@ -817,28 +813,13 @@ namespace Discord.API Preconditions.AtMost(command.Description.Length, 100, nameof(command.Description)); Preconditions.AtLeast(command.Description.Length, 1, nameof(command.Description)); - try - { - return await SendJsonAsync("PATCH", $"applications/{this.CurrentUserId}/commands/{commandId}", command, options: options).ConfigureAwait(false); - } - catch (HttpException ex) { return null; } + return await SendJsonAsync("PATCH", $"applications/{this.CurrentUserId}/commands/{commandId}", command, options: options).ConfigureAwait(false); } public async Task DeleteGlobalApplicationCommandAsync(ulong commandId, RequestOptions options = null) - { - try - { - await SendAsync("DELETE", $"applications/{this.CurrentUserId}/commands/{commandId}", options: options).ConfigureAwait(false); - } - catch (HttpException ex) { return; } - } + => await SendAsync("DELETE", $"applications/{this.CurrentUserId}/commands/{commandId}", options: options).ConfigureAwait(false); + public async Task GetGuildApplicationCommandAsync(ulong guildId, RequestOptions options = null) - { - try - { - return await SendAsync("GET", $"applications/{this.CurrentUserId}/guilds/{guildId}/commands", options: options).ConfigureAwait(false); - } - catch (HttpException ex) { return null; } - } + => await SendAsync("GET", $"applications/{this.CurrentUserId}/guilds/{guildId}/commands", options: options).ConfigureAwait(false); public async Task CreateGuildApplicationCommandAsync(ApplicationCommandParams command, ulong guildId, RequestOptions options = null) { Preconditions.NotNull(command, nameof(command)); @@ -847,13 +828,9 @@ namespace Discord.API Preconditions.AtMost(command.Description.Length, 100, nameof(command.Description)); Preconditions.AtLeast(command.Description.Length, 1, nameof(command.Description)); - try - { - return await SendJsonAsync("POST", $"applications/{this.CurrentUserId}/guilds/{guildId}/commands", command, options: options).ConfigureAwait(false); - } - catch (HttpException ex) { return null; } + return await SendJsonAsync("POST", $"applications/{this.CurrentUserId}/guilds/{guildId}/commands", command, options: options).ConfigureAwait(false); } - public async Task EditGuildApplicationCommandAsync(ApplicationCommandParams command, ulong guildId, ulong commandId, RequestOptions options = null) + public async Task ModifyGuildApplicationCommandAsync(ApplicationCommandParams command, ulong guildId, ulong commandId, RequestOptions options = null) { Preconditions.NotNull(command, nameof(command)); Preconditions.AtMost(command.Name.Length, 32, nameof(command.Name)); @@ -861,19 +838,27 @@ namespace Discord.API Preconditions.AtMost(command.Description.Length, 100, nameof(command.Description)); Preconditions.AtLeast(command.Description.Length, 1, nameof(command.Description)); - try - { - return await SendJsonAsync("PATCH", $"applications/{this.CurrentUserId}/guilds/{guildId}/commands/{commandId}", command, options: options).ConfigureAwait(false); - } - catch (HttpException ex) { return null; } + return await SendJsonAsync("PATCH", $"applications/{this.CurrentUserId}/guilds/{guildId}/commands/{commandId}", command, options: options).ConfigureAwait(false); } public async Task DeleteGuildApplicationCommandAsync(ulong guildId, ulong commandId, RequestOptions options = null) + => 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) { - try - { - await SendAsync("DELETE", $"applications/{this.CurrentUserId}/guilds/{guildId}/commands/{commandId}", options: options).ConfigureAwait(false); - } - catch (HttpException ex) { return; } + 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); + } + public async Task ModifyInteractionResponse(ModifyInteractionResponseParams args, string interactionToken, RequestOptions options = null) + => 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); + + public async Task CreateInteractionFollowupMessage() + { + } //Guilds diff --git a/src/Discord.Net.Rest/Entities/Interactions/ApplicationCommands/ApplicationCommandHelper.cs b/src/Discord.Net.Rest/Entities/Interactions/ApplicationCommands/ApplicationCommandHelper.cs index dd1141d99..40dd1f6fc 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/ApplicationCommands/ApplicationCommandHelper.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/ApplicationCommands/ApplicationCommandHelper.cs @@ -27,7 +27,7 @@ namespace Discord.Rest : Optional.Unspecified, }; - return await client.ApiClient.EditGlobalApplicationCommandAsync(apiArgs, command.Id, options); + return await client.ApiClient.ModifyGlobalApplicationCommandAsync(apiArgs, command.Id, options); } } } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 0d78b405d..67e24e616 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1780,7 +1780,7 @@ namespace Discord.WebSocket break; case "INTERACTION_CREATE": { - await _gatewayLogger.DebugAsync("Received Dispatch (INVITE_DELETE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (INTERACTION_CREATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); if (State.GetChannel(data.ChannelId) is SocketGuildChannel channel) diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs index 7786ce339..dbf2c369b 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs @@ -3,32 +3,55 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Model = Discord.API.Gateway.InteractionCreated; namespace Discord.WebSocket.Entities.Interaction { public class SocketInteraction : SocketEntity, IDiscordInteraction { - public ulong Id { get; } - - public InteractionType Type { get; } - - public IApplicationCommandInteractionData Data { get; } - - public ulong GuildId { get; } - - public ulong ChannelId { get; } - - public IGuildUser Member { get; } - - public string Token { get; } + public SocketGuild Guild + => Discord.GetGuild(GuildId); + public SocketTextChannel Channel + => Guild.GetTextChannel(ChannelId); + public SocketGuildUser Member + => Guild.GetUser(MemberId); + + public InteractionType Type { get; private set; } + public IApplicationCommandInteractionData Data { get; private set; } + public string Token { get; private set; } + public int Version { get; private set; } + public DateTimeOffset CreatedAt { get; } - public int Version { get; } + public ulong GuildId { get; private set; } + public ulong ChannelId { get; private set; } + public ulong MemberId { get; private set; } - public DateTimeOffset CreatedAt { get; } - public SocketInteraction(DiscordSocketClient client, ulong id) + + internal SocketInteraction(DiscordSocketClient client, ulong id) : base(client, id) { + } + internal static SocketInteraction Create(DiscordSocketClient client, Model model) + { + var entitiy = new SocketInteraction(client, model.Id); + entitiy.Update(model); + return entitiy; } + + internal void Update(Model model) + { + this.Data = model.Data.IsSpecified + ? SocketInteractionData.Create(this.Discord, model.Data.Value) + : null; + + this.GuildId = model.GuildId; + this.ChannelId = model.ChannelId; + this.Token = model.Token; + this.Version = model.Version; + this.MemberId = model.Member.User.Id; + this.Type = model.Type; + } + } } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionData.cs new file mode 100644 index 000000000..8e2ae3bf5 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionData.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.ApplicationCommandInteractionData; + +namespace Discord.WebSocket.Entities.Interaction +{ + public class SocketInteractionData : SocketEntity, IApplicationCommandInteractionData + { + public string Name { get; private set; } + public IReadOnlyCollection Options { get; private set; } + + internal SocketInteractionData(DiscordSocketClient client, ulong id) + : base(client, id) + { + + } + + internal static SocketInteractionData Create(DiscordSocketClient client, Model model) + { + var entity = new SocketInteractionData(client, model.Id); + entity.Update(model); + return entity; + } + internal void Update(Model model) + { + this.Name = model.Name; + this.Options = model.Options.IsSpecified + ? model.Options.Value.Select(x => new SocketInteractionDataOption(x)).ToImmutableArray() + : null; + + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionDataOption.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionDataOption.cs new file mode 100644 index 000000000..086ef1b87 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionDataOption.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.ApplicationCommandInteractionDataOption; + +namespace Discord.WebSocket.Entities.Interaction +{ + public class SocketInteractionDataOption : IApplicationCommandInteractionDataOption + { + public string Name { get; private set; } + public ApplicationCommandOptionType? Value { get; private set; } + + public IReadOnlyCollection Options { get; private set; } + + internal SocketInteractionDataOption(Model model) + { + this.Name = Name; + this.Value = model.Value.IsSpecified ? model.Value.Value : null; + + this.Options = model.Options.IsSpecified + ? model.Options.Value.Select(x => new SocketInteractionDataOption(x)).ToImmutableArray() + : null; + } + } +} From 670bd923834e7721041301bab4e305bb4e8a89e0 Mon Sep 17 00:00:00 2001 From: quin lynch Date: Fri, 18 Dec 2020 18:56:22 -0400 Subject: [PATCH 4/8] Working version of Interactions. All public classes/members now have correct documentation. Added AlwaysAcknowledgeInteractions to the socket client config --- .../ApplicationCommandOptionType.cs | 18 +- .../ApplicationCommandProperties.cs | 12 +- .../Interactions/IApplicationCommand.cs | 19 +- .../IApplicationCommandInteractionData.cs | 8 +- ...ApplicationCommandInteractionDataOption.cs | 13 +- .../Interactions/IApplicationCommandOption.cs | 16 +- .../IApplicationCommandOptionChoice.cs | 6 +- .../Interactions/IDiscordInteraction.cs | 32 ++-- .../Interactions/InteractionResponseType.cs | 12 +- .../Entities/Interactions/InteractionType.cs | 6 +- .../Entities/Messages/MessageType.cs | 7 + ...ApplicationCommandInteractionDataOption.cs | 2 +- ...teractionApplicationCommandCallbackData.cs | 9 +- src/Discord.Net.Rest/DiscordRestApiClient.cs | 56 ++++-- .../ApplicationCommandHelper.cs | 33 ---- .../Interactions/InteractionHelper.cs | 21 +++ .../BaseSocketClient.Events.cs | 29 ++- .../DiscordShardedClient.cs | 2 + .../DiscordSocketClient.cs | 13 +- .../DiscordSocketConfig.cs | 23 +++ .../Entities/Interaction/SocketInteraction.cs | 165 +++++++++++++++++- .../Interaction/SocketInteractionData.cs | 2 +- .../SocketInteractionDataOption.cs | 4 +- 23 files changed, 372 insertions(+), 136 deletions(-) delete mode 100644 src/Discord.Net.Rest/Entities/Interactions/ApplicationCommands/ApplicationCommandHelper.cs create mode 100644 src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs 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; } From a0f9646235fb0aff3e7b4ed14c976ef7f0c4b814 Mon Sep 17 00:00:00 2001 From: quin lynch Date: Sat, 19 Dec 2020 14:44:43 -0400 Subject: [PATCH 5/8] Get, modify, and delete Guild/Global commands, New Rest entities: RestApplicationCommand,RestGlobalCommand, RestGuildCommand, RestApplicationCommandOption, RestApplicationCommandChoice, RestApplicationCommandType. Added public methods to the RestClient to fetch/create/edit interactions. --- .../ApplicationCommandProperties.cs | 28 +++- .../Interactions/IApplicationCommand.cs | 11 +- .../Interactions/IApplicationCommandOption.cs | 4 +- .../API/Common/ApplicationCommand.cs | 8 +- src/Discord.Net.Rest/ClientHelper.cs | 19 +++ src/Discord.Net.Rest/DiscordRestClient.cs | 8 ++ .../Interactions/InteractionHelper.cs | 124 +++++++++++++++++- .../Interactions/RestApplicationCommand.cs | 58 ++++++++ .../RestApplicationCommandChoice.cs | 22 ++++ .../RestApplicationCommandOption.cs | 57 ++++++++ .../RestApplicationCommandType.cs | 14 ++ .../Interactions/RestGlobalCommand.cs | 41 ++++++ .../Entities/Interactions/RestGuildCommand.cs | 40 ++++++ .../DiscordSocketConfig.cs | 18 +-- .../Entities/Interaction/SocketInteraction.cs | 9 +- 15 files changed, 434 insertions(+), 27 deletions(-) create mode 100644 src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs create mode 100644 src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandChoice.cs create mode 100644 src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs create mode 100644 src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandType.cs create mode 100644 src/Discord.Net.Rest/Entities/Interactions/RestGlobalCommand.cs create mode 100644 src/Discord.Net.Rest/Entities/Interactions/RestGuildCommand.cs diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs index 7fa28c8d0..990fe2e5e 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs @@ -11,19 +11,41 @@ namespace Discord /// public class ApplicationCommandProperties { + private string _name { get; set; } + private string _description { get; set; } + /// /// Gets or sets the name of this command. /// - public string Name { get; set; } + public string Name + { + get => _name; + set + { + if(value.Length > 32) + throw new ArgumentException("Name length must be less than or equal to 32"); + _name = value; + } + } /// /// Gets or sets the discription of this command. /// - public string Description { get; set; } + public string Description + { + get => _description; + set + { + if (value.Length > 100) + throw new ArgumentException("Description length must be less than or equal to 100"); + _description = value; + } + } + /// /// Gets or sets the options for this command. /// - public Optional> Options { get; set; } + 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 122f2c599..c094efbd8 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs @@ -34,16 +34,13 @@ namespace Discord /// /// If the option is a subcommand or subcommand group type, this nested options will be the parameters. /// - IEnumerable? Options { get; } + IReadOnlyCollection Options { get; } /// - /// Modifies this command. + /// Deletes this command /// - /// The delegate containing the properties to modify the command with. /// The options to be used when sending the request. - /// - /// A task that represents the asynchronous modification operation. - /// - Task ModifyAsync(Action func, RequestOptions options = null); + /// + Task DeleteAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs index 14e25a26e..960d1571d 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs @@ -39,11 +39,11 @@ namespace Discord /// /// Choices for string and int types for the user to pick from. /// - IEnumerable? Choices { get; } + IReadOnlyCollection? Choices { get; } /// /// if the option is a subcommand or subcommand group type, this nested options will be the parameters. /// - IEnumerable? Options { get; } + IReadOnlyCollection? Options { get; } } } diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs index 9ae240e43..f42837450 100644 --- a/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs @@ -1,3 +1,4 @@ +using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Linq; @@ -8,10 +9,15 @@ namespace Discord.API { internal class ApplicationCommand { + [JsonProperty("id")] public ulong Id { get; set; } + [JsonProperty("application_id")] public ulong ApplicationId { get; set; } + [JsonProperty("name")] public string Name { get; set; } + [JsonProperty("description")] public string Description { get; set; } - public ApplicationCommand[] Options { get; set; } + [JsonProperty("options")] + public Optional Options { get; set; } } } diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs index 8910e999a..d39fcd7a2 100644 --- a/src/Discord.Net.Rest/ClientHelper.cs +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -201,5 +201,24 @@ namespace Discord.Rest } }; } + + public static async Task GetGlobalApplicationCommands(BaseDiscordClient client, RequestOptions options) + { + var response = await client.ApiClient.GetGlobalApplicationCommandsAsync(options).ConfigureAwait(false); + + if (!response.Any()) + return null; + + return response.Select(x => RestGlobalCommand.Create(client, x)).ToArray(); + } + public static async Task GetGuildApplicationCommands(BaseDiscordClient client, ulong guildId, RequestOptions options) + { + var response = await client.ApiClient.GetGuildApplicationCommandAsync(guildId, options).ConfigureAwait(false); + + if (!response.Any()) + return null; + + return response.Select(x => RestGuildCommand.Create(client, x, guildId)).ToArray(); + } } } diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index 48c40fdfa..79fb1a5c2 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -107,6 +107,14 @@ namespace Discord.Rest => ClientHelper.GetVoiceRegionAsync(this, id, options); public Task GetWebhookAsync(ulong id, RequestOptions options = null) => ClientHelper.GetWebhookAsync(this, id, options); + public Task CreateGobalCommand(Action func, RequestOptions options = null) + => InteractionHelper.CreateGlobalCommand(this, func, options); + public Task CreateGuildCommand(Action func, ulong guildId, RequestOptions options = null) + => InteractionHelper.CreateGuildCommand(this, guildId, func, options); + public Task GetGlobalApplicationCommands(RequestOptions options = null) + => ClientHelper.GetGlobalApplicationCommands(this, options); + public Task GetGuildApplicationCommands(ulong guildId, RequestOptions options = null) + => ClientHelper.GetGuildApplicationCommands(this, guildId, options); //IDiscordClient /// diff --git a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs index a275a7d0a..341087cb4 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs @@ -1,4 +1,5 @@ using Discord.API; +using Discord.API.Rest; using System; using System.Collections.Generic; using System.Linq; @@ -12,10 +13,131 @@ namespace Discord.Rest 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 model = await client.ApiClient.CreateInteractionFollowupMessage(args, token, options).ConfigureAwait(false); var entity = RestUserMessage.Create(client, channel, client.CurrentUser, model); return entity; } + + // Global commands + internal static async Task CreateGlobalCommand(BaseDiscordClient client, + Action func, RequestOptions options = null) + { + var args = new ApplicationCommandProperties(); + func(args); + + if (args.Options.IsSpecified) + { + if (args.Options.Value.Count > 10) + throw new ArgumentException("Option count must be 10 or less"); + } + + var model = new ApplicationCommandParams() + { + Name = args.Name, + Description = args.Description, + Options = args.Options.IsSpecified + ? args.Options.Value.Select(x => new ApplicationCommandOption(x)).ToArray() + : Optional.Unspecified + }; + + var cmd = await client.ApiClient.CreateGlobalApplicationCommandAsync(model, options).ConfigureAwait(false); + return RestGlobalCommand.Create(client, cmd); + } + + internal static async Task ModifyGlobalCommand(BaseDiscordClient client, RestGlobalCommand command, + Action func, RequestOptions options = null) + { + ApplicationCommandProperties args = new ApplicationCommandProperties(); + func(args); + + if (args.Options.IsSpecified) + { + if (args.Options.Value.Count > 10) + throw new ArgumentException("Option count must be 10 or less"); + } + + var model = new Discord.API.Rest.ApplicationCommandParams() + { + Name = args.Name, + Description = args.Description, + Options = args.Options.IsSpecified + ? args.Options.Value.Select(x => new ApplicationCommandOption(x)).ToArray() + : Optional.Unspecified + }; + + var msg = await client.ApiClient.ModifyGlobalApplicationCommandAsync(model, command.Id, options).ConfigureAwait(false); + command.Update(msg); + return command; + } + + + internal static async Task DeleteGlobalCommand(BaseDiscordClient client, RestGlobalCommand command, RequestOptions options = null) + { + Preconditions.NotNull(command, nameof(command)); + Preconditions.NotEqual(command.Id, 0, nameof(command.Id)); + + await client.ApiClient.DeleteGlobalApplicationCommandAsync(command.Id, options).ConfigureAwait(false); + } + + // Guild Commands + internal static async Task CreateGuildCommand(BaseDiscordClient client, ulong guildId, + Action func, RequestOptions options = null) + { + var args = new ApplicationCommandProperties(); + func(args); + + if (args.Options.IsSpecified) + { + if (args.Options.Value.Count > 10) + throw new ArgumentException("Option count must be 10 or less"); + } + + var model = new ApplicationCommandParams() + { + Name = args.Name, + Description = args.Description, + Options = args.Options.IsSpecified + ? args.Options.Value.Select(x => new ApplicationCommandOption(x)).ToArray() + : Optional.Unspecified + }; + + var cmd = await client.ApiClient.CreateGuildApplicationCommandAsync(model, guildId, options).ConfigureAwait(false); + return RestGuildCommand.Create(client, cmd, guildId); + } + + internal static async Task ModifyGuildCommand(BaseDiscordClient client, RestGuildCommand command, + Action func, RequestOptions options = null) + { + ApplicationCommandProperties args = new ApplicationCommandProperties(); + func(args); + + if (args.Options.IsSpecified) + { + if (args.Options.Value.Count > 10) + throw new ArgumentException("Option count must be 10 or less"); + } + + var model = new Discord.API.Rest.ApplicationCommandParams() + { + Name = args.Name, + Description = args.Description, + Options = args.Options.IsSpecified + ? args.Options.Value.Select(x => new ApplicationCommandOption(x)).ToArray() + : Optional.Unspecified + }; + + var msg = await client.ApiClient.ModifyGuildApplicationCommandAsync(model, command.Id, command.GuildId, options).ConfigureAwait(false); + command.Update(msg); + return command; + } + + internal static async Task DeleteGuildCommand(BaseDiscordClient client, RestGuildCommand command, RequestOptions options = null) + { + Preconditions.NotNull(command, nameof(command)); + Preconditions.NotEqual(command.Id, 0, nameof(command.Id)); + + await client.ApiClient.DeleteGuildApplicationCommandAsync(command.Id, command.GuildId, options).ConfigureAwait(false); + } } } diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs new file mode 100644 index 000000000..c52d69619 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.ApplicationCommand; + +namespace Discord.Rest +{ + /// + /// Represents a rest implementation of the + /// + public abstract class RestApplicationCommand : RestEntity, IApplicationCommand + { + public ulong ApplicationId { get; private set; } + + public string Name { get; private set; } + + public string Description { get; private set; } + + public IReadOnlyCollection Options { get; private set; } + + public RestApplicationCommandType CommandType { get; private set; } + + public DateTimeOffset CreatedAt + => SnowflakeUtils.FromSnowflake(this.Id); + + internal RestApplicationCommand(BaseDiscordClient client, ulong id) + : base(client, id) + { + + } + + internal static RestApplicationCommand Create(BaseDiscordClient client, Model model, RestApplicationCommandType type, ulong guildId = 0) + { + if (type == RestApplicationCommandType.GlobalCommand) + return RestGlobalCommand.Create(client, model); + + if (type == RestApplicationCommandType.GuildCommand) + return RestGuildCommand.Create(client, model, guildId); + + return null; + } + + internal virtual void Update(Model model) + { + this.ApplicationId = model.ApplicationId; + this.Name = model.Name; + + this.Options = model.Options.IsSpecified + ? model.Options.Value.Select(x => RestApplicationCommandOption.Create(x)).ToImmutableArray() + : null; + } + + public virtual Task DeleteAsync(RequestOptions options = null) => throw new NotImplementedException(); + } +} diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandChoice.cs b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandChoice.cs new file mode 100644 index 000000000..211d2fe12 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandChoice.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.ApplicationCommandOptionChoice; + +namespace Discord.Rest +{ + public class RestApplicationCommandChoice : IApplicationCommandOptionChoice + { + public string Name { get; } + + public string Value { get; } + + internal RestApplicationCommandChoice(Model model) + { + this.Name = model.Name; + this.Value = model.Value; + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs new file mode 100644 index 000000000..946319112 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.ApplicationCommandOption; + +namespace Discord.Rest +{ + public class RestApplicationCommandOption : IApplicationCommandOption + { + public ApplicationCommandOptionType Type { get; private set; } + + public string Name { get; private set; } + + public string Description { get; private set; } + + public bool? Default { get; private set; } + + public bool? Required { get; private set; } + + public IReadOnlyCollection Choices { get; private set; } + + public IReadOnlyCollection Options { get; private set; } + + internal RestApplicationCommandOption() { } + + internal static RestApplicationCommandOption Create(Model model) + { + var options = new RestApplicationCommandOption(); + options.Update(model); + return options; + } + + internal void Update(Model model) + { + this.Type = model.Type; + this.Name = model.Name; + this.Description = model.Description; + + if (model.Default.IsSpecified) + this.Default = model.Default.Value; + + if (model.Required.IsSpecified) + this.Required = model.Required.Value; + + this.Options = model.Options.IsSpecified + ? model.Options.Value.Select(x => Create(x)).ToImmutableArray() + : null; + + this.Choices = model.Choices.IsSpecified + ? model.Choices.Value.Select(x => new RestApplicationCommandChoice(x)).ToImmutableArray() + : null; + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandType.cs b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandType.cs new file mode 100644 index 000000000..7ea7cd9f0 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandType.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.Rest +{ + public enum RestApplicationCommandType + { + GlobalCommand, + GuildCommand + } +} diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestGlobalCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/RestGlobalCommand.cs new file mode 100644 index 000000000..d44e6819d --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/RestGlobalCommand.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.ApplicationCommand; + +namespace Discord.Rest +{ + /// + /// Represents a global Slash command + /// + public class RestGlobalCommand : RestApplicationCommand + { + internal RestGlobalCommand(BaseDiscordClient client, ulong id) + : base(client, id) + { + + } + + internal static RestGlobalCommand Create(BaseDiscordClient client, Model model) + { + var entity = new RestGlobalCommand(client, model.Id); + entity.Update(model); + return entity; + } + public override async Task DeleteAsync(RequestOptions options = null) + => await InteractionHelper.DeleteGlobalCommand(Discord, this).ConfigureAwait(false); + + /// + /// Modifies this . + /// + /// The delegate containing the properties to modify the command with. + /// The options to be used when sending the request. + /// + /// The modified command + /// + public async Task ModifyAsync(Action func, RequestOptions options = null) + => await InteractionHelper.ModifyGlobalCommand(Discord, this, func, options).ConfigureAwait(false); + } +} diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestGuildCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/RestGuildCommand.cs new file mode 100644 index 000000000..5bf386051 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/RestGuildCommand.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.ApplicationCommand; + +namespace Discord.Rest +{ + public class RestGuildCommand : RestApplicationCommand + { + public ulong GuildId { get; set; } + internal RestGuildCommand(BaseDiscordClient client, ulong id, ulong guildId) + : base(client, id) + { + this.GuildId = guildId; + } + + internal static RestGuildCommand Create(BaseDiscordClient client, Model model, ulong guildId) + { + var entity = new RestGuildCommand(client, model.Id, guildId); + entity.Update(model); + return entity; + } + + public override async Task DeleteAsync(RequestOptions options = null) + => await InteractionHelper.DeleteGuildCommand(Discord, this).ConfigureAwait(false); + + /// + /// Modifies this . + /// + /// The delegate containing the properties to modify the command with. + /// The options to be used when sending the request. + /// + /// The modified command + /// + public async Task ModifyAsync(Action func, RequestOptions options = null) + => await InteractionHelper.ModifyGuildCommand(Discord, this, func, options).ConfigureAwait(false); + } +} diff --git a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs index b93f6d33d..171094ade 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs @@ -110,20 +110,20 @@ namespace Discord.WebSocket /// /// /// - /// 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 + /// Discord interactions will not appear 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. + /// With this option set to , you will have to acknowledge the interaction with + /// . + /// Only after the interaction is acknowledged, 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. + /// Automatically 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; diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs index 1eb8fdca0..f51d0a8f8 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs @@ -95,10 +95,10 @@ namespace Discord.WebSocket } /// - /// Responds to an Interaction, eating its input + /// Responds to an Interaction. /// - /// If you have set to , this method - /// will be obsolete and will use + /// If you have set to , You should use + /// instead. /// /// /// The text of the message to be sent @@ -111,10 +111,11 @@ namespace Discord.WebSocket /// The sent as the response. If this is the first acknowledgement, it will return null; /// /// Message content is too long, length must be less or equal to . + /// The parameters provided were invalid or the token was invalid public async Task RespondAsync(string text = null, bool isTTS = false, Embed embed = null, InteractionResponseType Type = InteractionResponseType.ChannelMessageWithSource, AllowedMentions allowedMentions = null, RequestOptions options = null) { - if (Type == InteractionResponseType.ACKWithSource || Type == InteractionResponseType.ACKWithSource || Type == InteractionResponseType.Pong) + if (Type == InteractionResponseType.Pong) throw new InvalidOperationException($"Cannot use {Type} on a send message function"); if (!IsValidToken) From 09f6f3439fdafb31e9c494d9960db5ebd18c4cc0 Mon Sep 17 00:00:00 2001 From: quin lynch Date: Sun, 20 Dec 2020 14:22:16 -0400 Subject: [PATCH 6/8] Added APPLICATION_COMMAND_DELETE, APPLICATION_COMMAND_UPDATE, and APPLICATION_COMMAND_CREATE gateway events. Added SocketApplicationCommands, Added method in SocketGuild to fetch that guilds ApplicationCommands. Tested all rest routes and fixed them accordingly. Did more testing and I think its ready to go --- .../Interactions/ApplicationCommandOption.cs | 72 +++++++++ .../ApplicationCommandOptionChoice.cs | 49 ++++++ .../ApplicationCommandProperties.cs | 27 +--- ...ApplicationCommandInteractionDataOption.cs | 2 +- .../Interactions/IApplicationCommandOption.cs | 2 +- .../IApplicationCommandOptionChoice.cs | 2 +- .../SlashCommandCreationProperties.cs | 30 ++++ .../API/Common/ApplicationCommandOption.cs | 23 +++ .../Common/ApplicationCommandOptionChoice.cs | 2 +- ...s.cs => CreateApplicationCommandParams.cs} | 6 +- .../Rest/ModifyApplicationCommandParams.cs | 21 +++ src/Discord.Net.Rest/ClientHelper.cs | 10 +- src/Discord.Net.Rest/DiscordRestApiClient.cs | 115 ++++++++++---- src/Discord.Net.Rest/DiscordRestClient.cs | 12 +- .../Interactions/InteractionHelper.cs | 61 ++++--- .../Interactions/RestApplicationCommand.cs | 7 +- .../RestApplicationCommandChoice.cs | 2 +- .../RestApplicationCommandOption.cs | 7 +- .../Interactions/RestGlobalCommand.cs | 2 +- .../Entities/Interactions/RestGuildCommand.cs | 1 + .../ApplicationCommandCreatedUpdatedEvent.cs | 30 ++++ .../BaseSocketClient.Events.cs | 149 ++++++++++++++---- .../DiscordSocketClient.cs | 56 ++++++- .../Entities/Guilds/SocketGuild.cs | 12 ++ .../Interaction/SocketApplicationCommand.cs | 55 +++++++ .../SocketApplicationCommandChoice.cs | 32 ++++ .../SocketApplicationCommandOption.cs | 70 ++++++++ .../Entities/Interaction/SocketInteraction.cs | 4 +- .../Interaction/SocketInteractionData.cs | 4 +- 29 files changed, 737 insertions(+), 128 deletions(-) create mode 100644 src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs create mode 100644 src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs create mode 100644 src/Discord.Net.Core/Entities/Interactions/SlashCommandCreationProperties.cs rename src/Discord.Net.Rest/API/Rest/{ApplicationCommandParams.cs => CreateApplicationCommandParams.cs} (74%) create mode 100644 src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs create mode 100644 src/Discord.Net.WebSocket/API/Gateway/ApplicationCommandCreatedUpdatedEvent.cs create mode 100644 src/Discord.Net.WebSocket/Entities/Interaction/SocketApplicationCommand.cs create mode 100644 src/Discord.Net.WebSocket/Entities/Interaction/SocketApplicationCommandChoice.cs create mode 100644 src/Discord.Net.WebSocket/Entities/Interaction/SocketApplicationCommandOption.cs diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs new file mode 100644 index 000000000..9b4e435cf --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a for making slash commands. + /// + public class ApplicationCommandOptionProperties + { + private string _name; + private string _description; + + /// + /// The name of this option. + /// + public string Name + { + get => _name; + set + { + if (value?.Length > 32) + throw new ArgumentException("Name length must be less than or equal to 32"); + _name = value; + } + } + + /// + /// The description of this option. + /// + public string Description + { + get => _description; + set + { + if (value?.Length > 100) + throw new ArgumentException("Name length must be less than or equal to 32"); + _description = value; + } + } + + /// + /// The type of this option. + /// + public ApplicationCommandOptionType Type { get; set; } + + /// + /// The first required option for the user to complete. only one option can be default. + /// + public bool? Default { get; set; } + + /// + /// if this option is required for this command, otherwise . + /// + public bool? Required { get; set; } + + /// + /// choices for string and int types for the user to pick from + /// + public List Choices { get; set; } + + /// + /// If the option is a subcommand or subcommand group type, this nested options will be the parameters. + /// + public List Options { get; set; } + + + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs new file mode 100644 index 000000000..99e3f66d6 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a choice for a . This class is used when making new commands + /// + public class ApplicationCommandOptionChoiceProperties + { + private string _name; + private object _value; + /// + /// The name of this choice + /// + public string Name + { + get => _name; + set + { + if(value?.Length > 100) + throw new ArgumentException("Name length must be less than or equal to 100"); + _name = value; + } + } + + // Note: discord allows strings & ints as values. how should that be handled? + // should we make this an object and then just type check it? + /// + /// The value of this choice + /// + public object Value + { + get => _value; + set + { + if(value != null) + { + if(!(value is int) && !(value is string)) + throw new ArgumentException("The value of a choice must be a string or int!"); + } + _value = value; + } + } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs index 990fe2e5e..594a0f370 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs @@ -11,41 +11,20 @@ namespace Discord /// public class ApplicationCommandProperties { - private string _name { get; set; } - private string _description { get; set; } - /// /// Gets or sets the name of this command. /// - public string Name - { - get => _name; - set - { - if(value.Length > 32) - throw new ArgumentException("Name length must be less than or equal to 32"); - _name = value; - } - } + public Optional Name { get; set; } /// /// Gets or sets the discription of this command. /// - public string Description - { - get => _description; - set - { - if (value.Length > 100) - throw new ArgumentException("Description length must be less than or equal to 100"); - _description = value; - } - } + public Optional Description { get; set; } /// /// Gets or sets the options for this command. /// - public Optional> Options { get; set; } + public Optional> Options { get; set; } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionDataOption.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionDataOption.cs index 871eb4ae6..8ea8378e7 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionDataOption.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionDataOption.cs @@ -22,7 +22,7 @@ namespace Discord /// This objects type can be any one of the option types in /// /// - object? Value { get; } + object Value { get; } /// /// Present if this option is a group or subcommand. diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs index 960d1571d..25e3c7dfc 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs @@ -42,7 +42,7 @@ namespace Discord IReadOnlyCollection? 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. /// IReadOnlyCollection? Options { get; } } diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs index 3b4c7c249..1f0540656 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs @@ -19,7 +19,7 @@ namespace Discord /// /// value of the choice. /// - string Value { get; } + object Value { get; } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/SlashCommandCreationProperties.cs b/src/Discord.Net.Core/Entities/Interactions/SlashCommandCreationProperties.cs new file mode 100644 index 000000000..0facc02a1 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/SlashCommandCreationProperties.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// A class used to create slash commands + /// + public class SlashCommandCreationProperties + { + /// + /// The name of this command. + /// + public string Name { get; set; } + + /// + /// 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.Rest/API/Common/ApplicationCommandOption.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs index ad7748594..789c5549d 100644 --- a/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs @@ -53,5 +53,28 @@ namespace Discord.API this.Type = cmd.Type; this.Description = cmd.Description; } + public ApplicationCommandOption(Discord.ApplicationCommandOptionProperties option) + { + this.Choices = option.Choices != null + ? option.Choices.Select(x => new ApplicationCommandOptionChoice() + { + Name = x.Name, + Value = x.Value + }).ToArray() + : Optional.Unspecified; + + this.Options = option.Options != null + ? option.Options.Select(x => new ApplicationCommandOption(x)).ToArray() + : Optional.Unspecified; + + this.Required = option.Required.Value; + this.Default = option.Default.HasValue + ? option.Default.Value + : Optional.Unspecified; + + this.Name = option.Name; + this.Type = option.Type; + this.Description = option.Description; + } } } diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommandOptionChoice.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommandOptionChoice.cs index 00179dde1..b847fceba 100644 --- a/src/Discord.Net.Rest/API/Common/ApplicationCommandOptionChoice.cs +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommandOptionChoice.cs @@ -13,6 +13,6 @@ namespace Discord.API public string Name { get; set; } [JsonProperty("value")] - public string Value { get; set; } + public object Value { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/ApplicationCommandParams.cs b/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs similarity index 74% rename from src/Discord.Net.Rest/API/Rest/ApplicationCommandParams.cs rename to src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs index a674093c5..4ab6f5a9d 100644 --- a/src/Discord.Net.Rest/API/Rest/ApplicationCommandParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; namespace Discord.API.Rest { - internal class ApplicationCommandParams + internal class CreateApplicationCommandParams { [JsonProperty("name")] public string Name { get; set; } @@ -19,8 +19,8 @@ namespace Discord.API.Rest [JsonProperty("options")] public Optional Options { get; set; } - public ApplicationCommandParams() { } - public ApplicationCommandParams(string name, string description, ApplicationCommandOption[] options = null) + public CreateApplicationCommandParams() { } + public CreateApplicationCommandParams(string name, string description, ApplicationCommandOption[] options = null) { this.Name = name; this.Description = description; diff --git a/src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs new file mode 100644 index 000000000..141932955 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API.Rest +{ + internal class ModifyApplicationCommandParams + { + [JsonProperty("name")] + public Optional Name { get; set; } + + [JsonProperty("description")] + public Optional Description { get; set; } + + [JsonProperty("options")] + public Optional Options { get; set; } + } +} diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs index d39fcd7a2..4666d645d 100644 --- a/src/Discord.Net.Rest/ClientHelper.cs +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -202,23 +202,23 @@ namespace Discord.Rest }; } - public static async Task GetGlobalApplicationCommands(BaseDiscordClient client, RequestOptions options) + public static async Task> GetGlobalApplicationCommands(BaseDiscordClient client, RequestOptions options) { var response = await client.ApiClient.GetGlobalApplicationCommandsAsync(options).ConfigureAwait(false); if (!response.Any()) - return null; + return new RestGlobalCommand[0]; return response.Select(x => RestGlobalCommand.Create(client, x)).ToArray(); } - public static async Task GetGuildApplicationCommands(BaseDiscordClient client, ulong guildId, RequestOptions options) + public static async Task> GetGuildApplicationCommands(BaseDiscordClient client, ulong guildId, RequestOptions options) { var response = await client.ApiClient.GetGuildApplicationCommandAsync(guildId, options).ConfigureAwait(false); if (!response.Any()) - return null; + return new RestGuildCommand[0].ToImmutableArray(); - return response.Select(x => RestGuildCommand.Create(client, x, guildId)).ToArray(); + return response.Select(x => RestGuildCommand.Create(client, x, guildId)).ToImmutableArray(); } } } diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index d9da6503a..af33360f4 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -46,7 +46,7 @@ namespace Discord.API internal IRestClient RestClient { get; private set; } internal ulong? CurrentUserId { get; set; } public RateLimitPrecision RateLimitPrecision { get; private set; } - internal bool UseSystemClock { get; set; } + internal bool UseSystemClock { get; set; } internal JsonSerializer Serializer => _serializer; /// Unknown OAuth token type. @@ -58,7 +58,7 @@ namespace Discord.API DefaultRetryMode = defaultRetryMode; _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; RateLimitPrecision = rateLimitPrecision; - UseSystemClock = useSystemClock; + UseSystemClock = useSystemClock; RequestQueue = new RequestQueue(); _stateLock = new SemaphoreSlim(1, 1); @@ -262,8 +262,8 @@ namespace Discord.API CheckState(); if (request.Options.RetryMode == null) request.Options.RetryMode = DefaultRetryMode; - if (request.Options.UseSystemClock == null) - request.Options.UseSystemClock = UseSystemClock; + if (request.Options.UseSystemClock == null) + request.Options.UseSystemClock = UseSystemClock; var stopwatch = Stopwatch.StartNew(); var responseStream = await RequestQueue.SendAsync(request).ConfigureAwait(false); @@ -787,9 +787,14 @@ namespace Discord.API //Interactions public async Task GetGlobalApplicationCommandsAsync(RequestOptions options = null) - => await SendAsync("GET", $"applications/{this.CurrentUserId}/commands", options: options).ConfigureAwait(false); + { + options = RequestOptions.CreateOrClone(options); + + return await SendAsync("GET", () => $"applications/{this.CurrentUserId}/commands", new BucketIds(), options: options).ConfigureAwait(false); + } - public async Task CreateGlobalApplicationCommandAsync(ApplicationCommandParams command, RequestOptions options = null) + + public async Task CreateGlobalApplicationCommandAsync(CreateApplicationCommandParams command, RequestOptions options = null) { Preconditions.NotNull(command, nameof(command)); Preconditions.AtMost(command.Name.Length, 32, nameof(command.Name)); @@ -797,24 +802,45 @@ namespace Discord.API Preconditions.AtMost(command.Description.Length, 100, nameof(command.Description)); Preconditions.AtLeast(command.Description.Length, 1, nameof(command.Description)); - return await SendJsonAsync("POST", $"applications/{this.CurrentUserId}/commands", command, options: options).ConfigureAwait(false); + options = RequestOptions.CreateOrClone(options); + + return await SendJsonAsync("POST", () => $"applications/{this.CurrentUserId}/commands", command, new BucketIds(), options: options).ConfigureAwait(false); } - public async Task ModifyGlobalApplicationCommandAsync(ApplicationCommandParams command, ulong commandId, RequestOptions options = null) + public async Task ModifyGlobalApplicationCommandAsync(ModifyApplicationCommandParams command, ulong commandId, RequestOptions options = null) { Preconditions.NotNull(command, nameof(command)); - Preconditions.AtMost(command.Name.Length, 32, nameof(command.Name)); - Preconditions.AtLeast(command.Name.Length, 3, nameof(command.Name)); - Preconditions.AtMost(command.Description.Length, 100, nameof(command.Description)); - Preconditions.AtLeast(command.Description.Length, 1, nameof(command.Description)); - return await SendJsonAsync("PATCH", $"applications/{this.CurrentUserId}/commands/{commandId}", command, options: options).ConfigureAwait(false); + if (command.Name.IsSpecified) + { + Preconditions.AtMost(command.Name.Value.Length, 32, nameof(command.Name)); + Preconditions.AtLeast(command.Name.Value.Length, 3, nameof(command.Name)); + } + if (command.Description.IsSpecified) + { + Preconditions.AtMost(command.Description.Value.Length, 100, nameof(command.Description)); + Preconditions.AtLeast(command.Description.Value.Length, 1, nameof(command.Description)); + } + + options = RequestOptions.CreateOrClone(options); + + return await SendJsonAsync("PATCH", () => $"applications/{this.CurrentUserId}/commands/{commandId}", command, new BucketIds(), options: options).ConfigureAwait(false); } public async Task DeleteGlobalApplicationCommandAsync(ulong commandId, RequestOptions options = null) - => await SendAsync("DELETE", $"applications/{this.CurrentUserId}/commands/{commandId}", options: options).ConfigureAwait(false); + { + options = RequestOptions.CreateOrClone(options); + + await SendAsync("DELETE", () => $"applications/{this.CurrentUserId}/commands/{commandId}", new BucketIds(), options: options).ConfigureAwait(false); + } public async Task GetGuildApplicationCommandAsync(ulong guildId, RequestOptions options = null) - => await SendAsync("GET", $"applications/{this.CurrentUserId}/guilds/{guildId}/commands", options: options).ConfigureAwait(false); - public async Task CreateGuildApplicationCommandAsync(ApplicationCommandParams command, ulong guildId, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + + var bucket = new BucketIds(guildId: guildId); + + return await SendAsync("GET", () => $"applications/{this.CurrentUserId}/guilds/{guildId}/commands", bucket, options: options).ConfigureAwait(false); + } + public async Task CreateGuildApplicationCommandAsync(CreateApplicationCommandParams command, ulong guildId, RequestOptions options = null) { Preconditions.NotNull(command, nameof(command)); Preconditions.AtMost(command.Name.Length, 32, nameof(command.Name)); @@ -822,20 +848,41 @@ namespace Discord.API Preconditions.AtMost(command.Description.Length, 100, nameof(command.Description)); Preconditions.AtLeast(command.Description.Length, 1, nameof(command.Description)); - return await SendJsonAsync("POST", $"applications/{this.CurrentUserId}/guilds/{guildId}/commands", command, options: options).ConfigureAwait(false); + options = RequestOptions.CreateOrClone(options); + + var bucket = new BucketIds(guildId: guildId); + + return await SendJsonAsync("POST", () => $"applications/{this.CurrentUserId}/guilds/{guildId}/commands", command, bucket, options: options).ConfigureAwait(false); } - public async Task ModifyGuildApplicationCommandAsync(ApplicationCommandParams command, ulong guildId, ulong commandId, RequestOptions options = null) + public async Task ModifyGuildApplicationCommandAsync(ModifyApplicationCommandParams command, ulong guildId, ulong commandId, RequestOptions options = null) { Preconditions.NotNull(command, nameof(command)); - Preconditions.AtMost(command.Name.Length, 32, nameof(command.Name)); - Preconditions.AtLeast(command.Name.Length, 3, nameof(command.Name)); - Preconditions.AtMost(command.Description.Length, 100, nameof(command.Description)); - Preconditions.AtLeast(command.Description.Length, 1, nameof(command.Description)); - return await SendJsonAsync("PATCH", $"applications/{this.CurrentUserId}/guilds/{guildId}/commands/{commandId}", command, options: options).ConfigureAwait(false); + if (command.Name.IsSpecified) + { + Preconditions.AtMost(command.Name.Value.Length, 32, nameof(command.Name)); + Preconditions.AtLeast(command.Name.Value.Length, 3, nameof(command.Name)); + } + if (command.Description.IsSpecified) + { + Preconditions.AtMost(command.Description.Value.Length, 100, nameof(command.Description)); + Preconditions.AtLeast(command.Description.Value.Length, 1, nameof(command.Description)); + } + + options = RequestOptions.CreateOrClone(options); + + var bucket = new BucketIds(guildId: guildId); + + return await SendJsonAsync("PATCH", () => $"applications/{this.CurrentUserId}/guilds/{guildId}/commands/{commandId}", command, bucket, options: options).ConfigureAwait(false); } public async Task DeleteGuildApplicationCommandAsync(ulong guildId, ulong commandId, RequestOptions options = null) - => await SendAsync("DELETE", $"applications/{this.CurrentUserId}/guilds/{guildId}/commands/{commandId}", options: options).ConfigureAwait(false); + { + options = RequestOptions.CreateOrClone(options); + + var bucket = new BucketIds(guildId: guildId); + + await SendAsync("DELETE", () => $"applications/{this.CurrentUserId}/guilds/{guildId}/commands/{commandId}", bucket, options: options).ConfigureAwait(false); + } //Interaction Responses public async Task CreateInteractionResponse(InteractionResponse response, ulong interactionId, string interactionToken, RequestOptions options = null) @@ -845,12 +892,20 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); - await SendJsonAsync("POST", () => $"interactions/{interactionId}/{interactionToken}/callback", response, new BucketIds(), options: 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); + { + options = RequestOptions.CreateOrClone(options); + + await SendJsonAsync("POST", () => $"webhooks/{this.CurrentUserId}/{interactionToken}/messages/@original", args, new BucketIds(), options: options); + } public async Task DeleteInteractionResponse(string interactionToken, RequestOptions options = null) - => await SendAsync("DELETE", $"webhooks/{this.CurrentUserId}/{interactionToken}/messages/@original", options: options); + { + options = RequestOptions.CreateOrClone(options); + + await SendAsync("DELETE", () => $"webhooks/{this.CurrentUserId}/{interactionToken}/messages/@original", new BucketIds(), options: options); + } public async Task CreateInteractionFollowupMessage(CreateWebhookMessageParams args, string token, RequestOptions options = null) { @@ -862,7 +917,7 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); - return await SendJsonAsync("POST", $"webhooks/{CurrentUserId}/{token}?wait=true", args, options: options).ConfigureAwait(false); + return await SendJsonAsync("POST", () => $"webhooks/{CurrentUserId}/{token}?wait=true", args, new BucketIds(), options: options).ConfigureAwait(false); } public async Task ModifyInteractionFollowupMessage(CreateWebhookMessageParams args, ulong id, string token, RequestOptions options = null) @@ -875,7 +930,7 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); - return await SendJsonAsync("PATCH", $"webhooks/{CurrentUserId}/{token}/messages/{id}", args, options: options).ConfigureAwait(false); + return await SendJsonAsync("PATCH", () => $"webhooks/{CurrentUserId}/{token}/messages/{id}", args, new BucketIds(), options: options).ConfigureAwait(false); } public async Task DeleteInteractionFollowupMessage(ulong id, string token, RequestOptions options = null) @@ -884,7 +939,7 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); - await SendAsync("DELETE", $"webhooks/{CurrentUserId}/{token}/messages/{id}", options: options).ConfigureAwait(false); + await SendAsync("DELETE", () => $"webhooks/{CurrentUserId}/{token}/messages/{id}", new BucketIds(), options: options).ConfigureAwait(false); } //Guilds diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index 79fb1a5c2..d62a83d2d 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -107,13 +107,17 @@ namespace Discord.Rest => ClientHelper.GetVoiceRegionAsync(this, id, options); public Task GetWebhookAsync(ulong id, RequestOptions options = null) => ClientHelper.GetWebhookAsync(this, id, options); - public Task CreateGobalCommand(Action func, RequestOptions options = null) + public Task CreateGlobalCommand(SlashCommandCreationProperties properties, RequestOptions options = null) + => InteractionHelper.CreateGlobalCommand(this, properties, options); + public Task CreateGlobalCommand(Action func, RequestOptions options = null) => InteractionHelper.CreateGlobalCommand(this, func, options); - public Task CreateGuildCommand(Action func, ulong guildId, RequestOptions options = null) + public Task CreateGuildCommand(SlashCommandCreationProperties properties, ulong guildId, RequestOptions options = null) + => InteractionHelper.CreateGuildCommand(this, guildId, properties, options); + public Task CreateGuildCommand(Action func, ulong guildId, RequestOptions options = null) => InteractionHelper.CreateGuildCommand(this, guildId, func, options); - public Task GetGlobalApplicationCommands(RequestOptions options = null) + public Task> GetGlobalApplicationCommands(RequestOptions options = null) => ClientHelper.GetGlobalApplicationCommands(this, options); - public Task GetGuildApplicationCommands(ulong guildId, RequestOptions options = null) + public Task> GetGuildApplicationCommands(ulong guildId, RequestOptions options = null) => ClientHelper.GetGuildApplicationCommands(this, guildId, options); //IDiscordClient diff --git a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs index 341087cb4..46d92f815 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs @@ -21,30 +21,35 @@ namespace Discord.Rest // Global commands internal static async Task CreateGlobalCommand(BaseDiscordClient client, - Action func, RequestOptions options = null) + Action func, RequestOptions options = null) { - var args = new ApplicationCommandProperties(); + var args = new SlashCommandCreationProperties(); func(args); - + return await CreateGlobalCommand(client, args, options).ConfigureAwait(false); + } + internal static async Task CreateGlobalCommand(BaseDiscordClient client, + SlashCommandCreationProperties args, RequestOptions options = null) + { if (args.Options.IsSpecified) { if (args.Options.Value.Count > 10) throw new ArgumentException("Option count must be 10 or less"); } - var model = new ApplicationCommandParams() + + + var model = new CreateApplicationCommandParams() { Name = args.Name, Description = args.Description, Options = args.Options.IsSpecified - ? args.Options.Value.Select(x => new ApplicationCommandOption(x)).ToArray() - : Optional.Unspecified + ? args.Options.Value.Select(x => new Discord.API.ApplicationCommandOption(x)).ToArray() + : Optional.Unspecified }; var cmd = await client.ApiClient.CreateGlobalApplicationCommandAsync(model, options).ConfigureAwait(false); return RestGlobalCommand.Create(client, cmd); } - internal static async Task ModifyGlobalCommand(BaseDiscordClient client, RestGlobalCommand command, Action func, RequestOptions options = null) { @@ -57,13 +62,13 @@ namespace Discord.Rest throw new ArgumentException("Option count must be 10 or less"); } - var model = new Discord.API.Rest.ApplicationCommandParams() + var model = new Discord.API.Rest.ModifyApplicationCommandParams() { Name = args.Name, Description = args.Description, Options = args.Options.IsSpecified - ? args.Options.Value.Select(x => new ApplicationCommandOption(x)).ToArray() - : Optional.Unspecified + ? args.Options.Value.Select(x => new Discord.API.ApplicationCommandOption(x)).ToArray() + : Optional.Unspecified }; var msg = await client.ApiClient.ModifyGlobalApplicationCommandAsync(model, command.Id, options).ConfigureAwait(false); @@ -82,30 +87,44 @@ namespace Discord.Rest // Guild Commands internal static async Task CreateGuildCommand(BaseDiscordClient client, ulong guildId, - Action func, RequestOptions options = null) + Action func, RequestOptions options = null) { - var args = new ApplicationCommandProperties(); + var args = new SlashCommandCreationProperties(); func(args); + return await CreateGuildCommand(client, guildId, args, options).ConfigureAwait(false); + } + internal static async Task CreateGuildCommand(BaseDiscordClient client, ulong guildId, + SlashCommandCreationProperties args, RequestOptions options = null) + { + Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name)); + Preconditions.NotNullOrEmpty(args.Description, nameof(args.Description)); + + if (args.Options.IsSpecified) { if (args.Options.Value.Count > 10) throw new ArgumentException("Option count must be 10 or less"); + + foreach(var item in args.Options.Value) + { + Preconditions.NotNullOrEmpty(item.Name, nameof(item.Name)); + Preconditions.NotNullOrEmpty(item.Description, nameof(item.Description)); + } } - var model = new ApplicationCommandParams() + var model = new CreateApplicationCommandParams() { Name = args.Name, Description = args.Description, Options = args.Options.IsSpecified - ? args.Options.Value.Select(x => new ApplicationCommandOption(x)).ToArray() - : Optional.Unspecified + ? args.Options.Value.Select(x => new Discord.API.ApplicationCommandOption(x)).ToArray() + : Optional.Unspecified }; var cmd = await client.ApiClient.CreateGuildApplicationCommandAsync(model, guildId, options).ConfigureAwait(false); return RestGuildCommand.Create(client, cmd, guildId); } - internal static async Task ModifyGuildCommand(BaseDiscordClient client, RestGuildCommand command, Action func, RequestOptions options = null) { @@ -118,16 +137,16 @@ namespace Discord.Rest throw new ArgumentException("Option count must be 10 or less"); } - var model = new Discord.API.Rest.ApplicationCommandParams() + var model = new Discord.API.Rest.ModifyApplicationCommandParams() { Name = args.Name, Description = args.Description, Options = args.Options.IsSpecified - ? args.Options.Value.Select(x => new ApplicationCommandOption(x)).ToArray() - : Optional.Unspecified + ? args.Options.Value.Select(x => new Discord.API.ApplicationCommandOption(x)).ToArray() + : Optional.Unspecified }; - var msg = await client.ApiClient.ModifyGuildApplicationCommandAsync(model, command.Id, command.GuildId, options).ConfigureAwait(false); + var msg = await client.ApiClient.ModifyGuildApplicationCommandAsync(model, command.GuildId, command.Id, options).ConfigureAwait(false); command.Update(msg); return command; } @@ -137,7 +156,7 @@ namespace Discord.Rest Preconditions.NotNull(command, nameof(command)); Preconditions.NotEqual(command.Id, 0, nameof(command.Id)); - await client.ApiClient.DeleteGuildApplicationCommandAsync(command.Id, command.GuildId, options).ConfigureAwait(false); + await client.ApiClient.DeleteGuildApplicationCommandAsync(command.GuildId, command.Id, options).ConfigureAwait(false); } } } diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs index c52d69619..5e0852b29 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs @@ -19,9 +19,9 @@ namespace Discord.Rest public string Description { get; private set; } - public IReadOnlyCollection Options { get; private set; } + public IReadOnlyCollection Options { get; private set; } - public RestApplicationCommandType CommandType { get; private set; } + public RestApplicationCommandType CommandType { get; internal set; } public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(this.Id); @@ -47,12 +47,15 @@ namespace Discord.Rest { this.ApplicationId = model.ApplicationId; this.Name = model.Name; + this.Description = model.Description; this.Options = model.Options.IsSpecified ? model.Options.Value.Select(x => RestApplicationCommandOption.Create(x)).ToImmutableArray() : null; } + IReadOnlyCollection IApplicationCommand.Options => Options; + public virtual Task DeleteAsync(RequestOptions options = null) => throw new NotImplementedException(); } } diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandChoice.cs b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandChoice.cs index 211d2fe12..f9ab50f60 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandChoice.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandChoice.cs @@ -11,7 +11,7 @@ namespace Discord.Rest { public string Name { get; } - public string Value { get; } + public object Value { get; } internal RestApplicationCommandChoice(Model model) { diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs index 946319112..120fe0c29 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs @@ -20,9 +20,9 @@ namespace Discord.Rest public bool? Required { get; private set; } - public IReadOnlyCollection Choices { get; private set; } + public IReadOnlyCollection Choices { get; private set; } - public IReadOnlyCollection Options { get; private set; } + public IReadOnlyCollection Options { get; private set; } internal RestApplicationCommandOption() { } @@ -53,5 +53,8 @@ namespace Discord.Rest ? model.Choices.Value.Select(x => new RestApplicationCommandChoice(x)).ToImmutableArray() : null; } + + IReadOnlyCollection IApplicationCommandOption.Options => Options; + IReadOnlyCollection IApplicationCommandOption.Choices => Choices; } } diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestGlobalCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/RestGlobalCommand.cs index d44e6819d..5223c069c 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestGlobalCommand.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestGlobalCommand.cs @@ -15,7 +15,7 @@ namespace Discord.Rest internal RestGlobalCommand(BaseDiscordClient client, ulong id) : base(client, id) { - + this.CommandType = RestApplicationCommandType.GlobalCommand; } internal static RestGlobalCommand Create(BaseDiscordClient client, Model model) diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestGuildCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/RestGuildCommand.cs index 5bf386051..73737dcd2 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestGuildCommand.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestGuildCommand.cs @@ -13,6 +13,7 @@ namespace Discord.Rest internal RestGuildCommand(BaseDiscordClient client, ulong id, ulong guildId) : base(client, id) { + this.CommandType = RestApplicationCommandType.GuildCommand; this.GuildId = guildId; } diff --git a/src/Discord.Net.WebSocket/API/Gateway/ApplicationCommandCreatedUpdatedEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/ApplicationCommandCreatedUpdatedEvent.cs new file mode 100644 index 000000000..94b3470e7 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Gateway/ApplicationCommandCreatedUpdatedEvent.cs @@ -0,0 +1,30 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API.Gateway +{ + internal class ApplicationCommandCreatedUpdatedEvent + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("id")] + public ulong Id { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("application_id")] + public ulong ApplicationId { get; set; } + + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + + [JsonProperty("options")] + public List Options { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs index 7b8a245cc..af1a9f147 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs @@ -45,7 +45,8 @@ namespace Discord.WebSocket /// /// - public event Func ChannelDestroyed { + public event Func ChannelDestroyed + { add { _channelDestroyedEvent.Add(value); } remove { _channelDestroyedEvent.Remove(value); } } @@ -67,7 +68,8 @@ namespace Discord.WebSocket /// /// - public event Func ChannelUpdated { + public event Func ChannelUpdated + { add { _channelUpdatedEvent.Add(value); } remove { _channelUpdatedEvent.Remove(value); } } @@ -92,7 +94,8 @@ namespace Discord.WebSocket /// /// - public event Func MessageReceived { + public event Func MessageReceived + { add { _messageReceivedEvent.Add(value); } remove { _messageReceivedEvent.Remove(value); } } @@ -124,7 +127,8 @@ namespace Discord.WebSocket /// /// - public event Func, ISocketMessageChannel, Task> MessageDeleted { + public event Func, ISocketMessageChannel, Task> MessageDeleted + { add { _messageDeletedEvent.Add(value); } remove { _messageDeletedEvent.Remove(value); } } @@ -182,7 +186,8 @@ namespace Discord.WebSocket /// parameter. /// /// - public event Func, SocketMessage, ISocketMessageChannel, Task> MessageUpdated { + public event Func, SocketMessage, ISocketMessageChannel, Task> MessageUpdated + { add { _messageUpdatedEvent.Add(value); } remove { _messageUpdatedEvent.Remove(value); } } @@ -217,19 +222,22 @@ namespace Discord.WebSocket /// /// - public event Func, ISocketMessageChannel, SocketReaction, Task> ReactionAdded { + public event Func, ISocketMessageChannel, SocketReaction, Task> ReactionAdded + { add { _reactionAddedEvent.Add(value); } remove { _reactionAddedEvent.Remove(value); } } internal readonly AsyncEvent, ISocketMessageChannel, SocketReaction, Task>> _reactionAddedEvent = new AsyncEvent, ISocketMessageChannel, SocketReaction, Task>>(); /// Fired when a reaction is removed from a message. - public event Func, ISocketMessageChannel, SocketReaction, Task> ReactionRemoved { + public event Func, ISocketMessageChannel, SocketReaction, Task> ReactionRemoved + { add { _reactionRemovedEvent.Add(value); } remove { _reactionRemovedEvent.Remove(value); } } internal readonly AsyncEvent, ISocketMessageChannel, SocketReaction, Task>> _reactionRemovedEvent = new AsyncEvent, ISocketMessageChannel, SocketReaction, Task>>(); /// Fired when all reactions to a message are cleared. - public event Func, ISocketMessageChannel, Task> ReactionsCleared { + public event Func, ISocketMessageChannel, Task> ReactionsCleared + { add { _reactionsClearedEvent.Add(value); } remove { _reactionsClearedEvent.Remove(value); } } @@ -259,19 +267,22 @@ namespace Discord.WebSocket //Roles /// Fired when a role is created. - public event Func RoleCreated { + public event Func RoleCreated + { add { _roleCreatedEvent.Add(value); } remove { _roleCreatedEvent.Remove(value); } } internal readonly AsyncEvent> _roleCreatedEvent = new AsyncEvent>(); /// Fired when a role is deleted. - public event Func RoleDeleted { + public event Func RoleDeleted + { add { _roleDeletedEvent.Add(value); } remove { _roleDeletedEvent.Remove(value); } } internal readonly AsyncEvent> _roleDeletedEvent = new AsyncEvent>(); /// Fired when a role is updated. - public event Func RoleUpdated { + public event Func RoleUpdated + { add { _roleUpdatedEvent.Add(value); } remove { _roleUpdatedEvent.Remove(value); } } @@ -279,37 +290,43 @@ namespace Discord.WebSocket //Guilds /// Fired when the connected account joins a guild. - public event Func JoinedGuild { + public event Func JoinedGuild + { add { _joinedGuildEvent.Add(value); } remove { _joinedGuildEvent.Remove(value); } } internal readonly AsyncEvent> _joinedGuildEvent = new AsyncEvent>(); /// Fired when the connected account leaves a guild. - public event Func LeftGuild { + public event Func LeftGuild + { add { _leftGuildEvent.Add(value); } remove { _leftGuildEvent.Remove(value); } } internal readonly AsyncEvent> _leftGuildEvent = new AsyncEvent>(); /// Fired when a guild becomes available. - public event Func GuildAvailable { + public event Func GuildAvailable + { add { _guildAvailableEvent.Add(value); } remove { _guildAvailableEvent.Remove(value); } } internal readonly AsyncEvent> _guildAvailableEvent = new AsyncEvent>(); /// Fired when a guild becomes unavailable. - public event Func GuildUnavailable { + public event Func GuildUnavailable + { add { _guildUnavailableEvent.Add(value); } remove { _guildUnavailableEvent.Remove(value); } } internal readonly AsyncEvent> _guildUnavailableEvent = new AsyncEvent>(); /// Fired when offline guild members are downloaded. - public event Func GuildMembersDownloaded { + public event Func GuildMembersDownloaded + { add { _guildMembersDownloadedEvent.Add(value); } remove { _guildMembersDownloadedEvent.Remove(value); } } internal readonly AsyncEvent> _guildMembersDownloadedEvent = new AsyncEvent>(); /// Fired when a guild is updated. - public event Func GuildUpdated { + public event Func GuildUpdated + { add { _guildUpdatedEvent.Add(value); } remove { _guildUpdatedEvent.Remove(value); } } @@ -317,43 +334,50 @@ namespace Discord.WebSocket //Users /// Fired when a user joins a guild. - public event Func UserJoined { + public event Func UserJoined + { add { _userJoinedEvent.Add(value); } remove { _userJoinedEvent.Remove(value); } } internal readonly AsyncEvent> _userJoinedEvent = new AsyncEvent>(); /// Fired when a user leaves a guild. - public event Func UserLeft { + public event Func UserLeft + { add { _userLeftEvent.Add(value); } remove { _userLeftEvent.Remove(value); } } internal readonly AsyncEvent> _userLeftEvent = new AsyncEvent>(); /// Fired when a user is banned from a guild. - public event Func UserBanned { + public event Func UserBanned + { add { _userBannedEvent.Add(value); } remove { _userBannedEvent.Remove(value); } } internal readonly AsyncEvent> _userBannedEvent = new AsyncEvent>(); /// Fired when a user is unbanned from a guild. - public event Func UserUnbanned { + public event Func UserUnbanned + { add { _userUnbannedEvent.Add(value); } remove { _userUnbannedEvent.Remove(value); } } internal readonly AsyncEvent> _userUnbannedEvent = new AsyncEvent>(); /// Fired when a user is updated. - public event Func UserUpdated { + public event Func UserUpdated + { add { _userUpdatedEvent.Add(value); } remove { _userUpdatedEvent.Remove(value); } } internal readonly AsyncEvent> _userUpdatedEvent = new AsyncEvent>(); /// Fired when a guild member is updated, or a member presence is updated. - public event Func GuildMemberUpdated { + public event Func GuildMemberUpdated + { add { _guildMemberUpdatedEvent.Add(value); } remove { _guildMemberUpdatedEvent.Remove(value); } } internal readonly AsyncEvent> _guildMemberUpdatedEvent = new AsyncEvent>(); /// Fired when a user joins, leaves, or moves voice channels. - public event Func UserVoiceStateUpdated { + public event Func UserVoiceStateUpdated + { add { _userVoiceStateUpdatedEvent.Add(value); } remove { _userVoiceStateUpdatedEvent.Remove(value); } } @@ -366,25 +390,29 @@ namespace Discord.WebSocket } internal readonly AsyncEvent> _voiceServerUpdatedEvent = new AsyncEvent>(); /// Fired when the connected account is updated. - public event Func CurrentUserUpdated { + public event Func CurrentUserUpdated + { add { _selfUpdatedEvent.Add(value); } remove { _selfUpdatedEvent.Remove(value); } } internal readonly AsyncEvent> _selfUpdatedEvent = new AsyncEvent>(); /// Fired when a user starts typing. - public event Func UserIsTyping { + public event Func UserIsTyping + { add { _userIsTypingEvent.Add(value); } remove { _userIsTypingEvent.Remove(value); } } internal readonly AsyncEvent> _userIsTypingEvent = new AsyncEvent>(); /// Fired when a user joins a group channel. - public event Func RecipientAdded { + public event Func RecipientAdded + { add { _recipientAddedEvent.Add(value); } remove { _recipientAddedEvent.Remove(value); } } internal readonly AsyncEvent> _recipientAddedEvent = new AsyncEvent>(); /// Fired when a user is removed from a group channel. - public event Func RecipientRemoved { + public event Func RecipientRemoved + { add { _recipientRemovedEvent.Add(value); } remove { _recipientRemovedEvent.Remove(value); } } @@ -452,5 +480,70 @@ namespace Discord.WebSocket } internal readonly AsyncEvent> _interactionCreatedEvent = new AsyncEvent>(); + /// + /// Fired when a guild application command is created. + /// + /// + /// + /// This event is fired when an application command is created. The event handler must return a + /// and accept a as its parameter. + /// + /// + /// The command that was deleted will be passed into the parameter. + /// + /// + /// This event is an undocumented discord event and may break at any time, its not recommended to rely on this event + /// + /// + public event Func ApplicationCommandCreated + { + add { _applicationCommandCreated.Add(value); } + remove { _applicationCommandCreated.Remove(value); } + } + internal readonly AsyncEvent> _applicationCommandCreated = new AsyncEvent>(); + + /// + /// Fired when a guild application command is updated. + /// + /// + /// + /// This event is fired when an application command is updated. The event handler must return a + /// and accept a as its parameter. + /// + /// + /// The command that was deleted will be passed into the parameter. + /// + /// + /// This event is an undocumented discord event and may break at any time, its not recommended to rely on this event + /// + /// + public event Func ApplicationCommandUpdated + { + add { _applicationCommandUpdated.Add(value); } + remove { _applicationCommandUpdated.Remove(value); } + } + internal readonly AsyncEvent> _applicationCommandUpdated = new AsyncEvent>(); + + /// + /// Fired when a guild application command is deleted. + /// + /// + /// + /// This event is fired when an application command is deleted. The event handler must return a + /// and accept a as its parameter. + /// + /// + /// The command that was deleted will be passed into the parameter. + /// + /// + /// This event is an undocumented discord event and may break at any time, its not recommended to rely on this event + /// + /// + public event Func ApplicationCommandDeleted + { + add { _applicationCommandDeleted.Add(value); } + remove { _applicationCommandDeleted.Remove(value); } + } + internal readonly AsyncEvent> _applicationCommandDeleted = new AsyncEvent>(); } } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index e2029e471..115367e6f 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -613,7 +613,7 @@ namespace Discord.WebSocket } else if (_connection.CancelToken.IsCancellationRequested) return; - + if (BaseConfig.AlwaysDownloadUsers) _ = DownloadUsersAsync(Guilds.Where(x => x.IsAvailable && !x.HasAllMembers)); @@ -1808,6 +1808,60 @@ namespace Discord.WebSocket } } break; + case "APPLICATION_COMMAND_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (APPLICATION_COMMAND_CREATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + if(guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + + var applicationCommand = SocketApplicationCommand.Create(this, data); + + await TimedInvokeAsync(_applicationCommandCreated, nameof(ApplicationCommandCreated), applicationCommand).ConfigureAwait(false); + } + break; + case "APPLICATION_COMMAND_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (APPLICATION_COMMAND_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + + var applicationCommand = SocketApplicationCommand.Create(this, data); + + await TimedInvokeAsync(_applicationCommandUpdated, nameof(ApplicationCommandUpdated), applicationCommand).ConfigureAwait(false); + } + break; + case "APPLICATION_COMMAND_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (APPLICATION_COMMAND_DELETE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + + var applicationCommand = SocketApplicationCommand.Create(this, data); + + await TimedInvokeAsync(_applicationCommandDeleted, nameof(ApplicationCommandDeleted), applicationCommand).ConfigureAwait(false); + } + break; //Ignored (User only) case "CHANNEL_PINS_ACK": await _gatewayLogger.DebugAsync("Ignored Dispatch (CHANNEL_PINS_ACK)").ConfigureAwait(false); diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index e9e535998..35797dc78 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -1006,6 +1006,18 @@ namespace Discord.WebSocket public Task> GetWebhooksAsync(RequestOptions options = null) => GuildHelper.GetWebhooksAsync(this, Discord, options); + //Interactions + /// + /// Gets this guilds slash commands commands + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of application commands found within the guild. + /// + public async Task> GetApplicationCommandsAsync(RequestOptions options = null) + => await Discord.Rest.GetGuildApplicationCommands(this.Id, options); + //Emotes /// public Task GetEmoteAsync(ulong id, RequestOptions options = null) diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketApplicationCommand.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketApplicationCommand.cs new file mode 100644 index 000000000..c8cc4d3a2 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketApplicationCommand.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.Gateway.ApplicationCommandCreatedUpdatedEvent; + +namespace Discord.WebSocket +{ + public class SocketApplicationCommand : SocketEntity, IApplicationCommand + { + public ulong ApplicationId { get; private set; } + + public string Name { get; private set; } + + public string Description { get; private set; } + + public IReadOnlyCollection Options { get; private set; } + + public DateTimeOffset CreatedAt + => SnowflakeUtils.FromSnowflake(this.Id); + + public SocketGuild Guild + => Discord.GetGuild(this.GuildId); + private ulong GuildId { get; set; } + + internal SocketApplicationCommand(DiscordSocketClient client, ulong id) + : base(client, id) + { + + } + internal static SocketApplicationCommand Create(DiscordSocketClient client, Model model) + { + var entity = new SocketApplicationCommand(client, model.Id); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + this.ApplicationId = model.ApplicationId; + this.Description = model.Description; + this.Name = model.Name; + this.GuildId = model.GuildId; + + this.Options = model.Options.Any() + ? model.Options.Select(x => SocketApplicationCommandOption.Create(x)).ToImmutableArray() + : new ImmutableArray(); + } + + public Task DeleteAsync(RequestOptions options = null) => throw new NotImplementedException(); + IReadOnlyCollection IApplicationCommand.Options => Options; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketApplicationCommandChoice.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketApplicationCommandChoice.cs new file mode 100644 index 000000000..f82f9e6c4 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketApplicationCommandChoice.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.ApplicationCommandOptionChoice; + +namespace Discord.WebSocket +{ + /// + /// Represents a choice for a + /// + public class SocketApplicationCommandChoice : IApplicationCommandOptionChoice + { + public string Name { get; private set; } + + public object Value { get; private set; } + + internal SocketApplicationCommandChoice() { } + internal static SocketApplicationCommandChoice Create(Model model) + { + var entity = new SocketApplicationCommandChoice(); + entity.Update(model); + return entity; + } + internal void Update(Model model) + { + this.Name = model.Name; + this.Value = model.Value; + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketApplicationCommandOption.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketApplicationCommandOption.cs new file mode 100644 index 000000000..e856bce42 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketApplicationCommandOption.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.ApplicationCommandOption; + +namespace Discord.WebSocket +{ + /// + /// Represents an option for a + /// + public class SocketApplicationCommandOption : IApplicationCommandOption + { + public string Name { get; private set; } + + public ApplicationCommandOptionType Type { get; private set; } + + public string Description { get; private set; } + + public bool? Default { get; private set; } + + public bool? Required { get; private set; } + + /// + /// Choices for string and int types for the user to pick from. + /// + public IReadOnlyCollection Choices { get; private set; } + + /// + /// If the option is a subcommand or subcommand group type, this nested options will be the parameters. + /// + public IReadOnlyCollection Options { get; private set; } + + internal SocketApplicationCommandOption() { } + internal static SocketApplicationCommandOption Create(Model model) + { + var entity = new SocketApplicationCommandOption(); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + this.Name = model.Name; + this.Type = model.Type; + this.Description = model.Description; + + this.Default = model.Default.IsSpecified + ? model.Default.Value + : null; + + this.Required = model.Required.IsSpecified + ? model.Required.Value + : null; + + this.Choices = model.Choices.IsSpecified + ? model.Choices.Value.Select(x => SocketApplicationCommandChoice.Create(x)).ToImmutableArray() + : new ImmutableArray(); + + this.Options = model.Options.IsSpecified + ? model.Options.Value.Select(x => SocketApplicationCommandOption.Create(x)).ToImmutableArray() + : new ImmutableArray(); + } + + IReadOnlyCollection IApplicationCommandOption.Choices => Choices; + IReadOnlyCollection IApplicationCommandOption.Options => Options; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs index f51d0a8f8..f8c2c62bd 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs @@ -39,7 +39,7 @@ namespace Discord.WebSocket /// /// The data associated with this interaction /// - public IApplicationCommandInteractionData Data { get; private set; } + public SocketInteractionData Data { get; private set; } /// /// The token used to respond to this interaction @@ -209,5 +209,7 @@ namespace Discord.WebSocket await Discord.Rest.ApiClient.CreateInteractionResponse(response, this.Id, Token, options).ConfigureAwait(false); } + + IApplicationCommandInteractionData IDiscordInteraction.Data => Data; } } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionData.cs index 4ff582480..b6dfd2f8e 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionData.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionData.cs @@ -11,7 +11,7 @@ namespace Discord.WebSocket public class SocketInteractionData : SocketEntity, IApplicationCommandInteractionData { public string Name { get; private set; } - public IReadOnlyCollection Options { get; private set; } + public IReadOnlyCollection Options { get; private set; } internal SocketInteractionData(DiscordSocketClient client, ulong id) : base(client, id) @@ -33,5 +33,7 @@ namespace Discord.WebSocket : null; } + + IReadOnlyCollection IApplicationCommandInteractionData.Options => Options; } } From 18ffc40e5434d230cc7623b9bab87dd1a25e8fba Mon Sep 17 00:00:00 2001 From: quinchs <49576606+quinchs@users.noreply.github.com> Date: Mon, 21 Dec 2020 14:27:55 -0400 Subject: [PATCH 7/8] Create application-commands.md --- docs/guides/commands/application-commands.md | 216 +++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 docs/guides/commands/application-commands.md diff --git a/docs/guides/commands/application-commands.md b/docs/guides/commands/application-commands.md new file mode 100644 index 000000000..a1247417a --- /dev/null +++ b/docs/guides/commands/application-commands.md @@ -0,0 +1,216 @@ +# Application commands + +Application commands are a new feature thats still a work in progress, this guide will show you how to make the best of em. + + + + +## Getting started + +### Configuring + +There is a new configuration setting for your DiscordSocketClient called `AlwaysAcknowledgeInteractions`, It's default value is true. +Interactions work off of the Recieve -> Respond pipeline, meaning if you dont acknowledge the interaction within 3 seconds its gone forever. +With `AlwaysAcknowledgeInteractions` set to true, the client will automatically acknowledge the interaction as its recieved, +letting you wait up to 15 minutes before responding with a message. + +With `AlwaysAcknowledgeInteractions` set to false you will have to acknowledge the interaction yourself via the `InteractionCreated` event + +### Registering commands + +While there is no "easy" way to register command right now, in the future I plan to write a command service to help with that, but right now you have to use the rest +client to create your command: + +```cs +_client.Ready += RegisterCommands + +... + +private async Task RegisterCommands() +{ + // Creating a global command + var myGlobalCommand = await _client.Rest.CreateGlobalCommand(new Discord.SlashCommandCreationProperties() + { + Name = "example", + Description = "Runs the example command", + Options = new List() + { + new ApplicationCommandOptionProperties() + { + Name = "Example option", + Required = false, + Description = "Option Description", + Type = Discord.ApplicationCommandOptionType.String, + } + } + }); + + // Creating a guild command + var myGuildCommand = await _client.Rest.CreateGuildCommand(new Discord.SlashCommandCreationProperties() + { + Name = "guildExample", + Description = "Runs the guild example command", + Options = new List() + { + new ApplicationCommandOptionProperties() + { + Name = "Guild example option", + Required = false, + Description = "Guild option description", + Type = Discord.ApplicationCommandOptionType.String, + } + } + }, 1234567890); // <- the guild id +} +``` +CreateGuildCommand returns a `RestGuildCommand` class which can be used to modify/delete your command on the fly, it also contains details about your command. +CreateGlobalCOmmand returns a `RestGlobalCommand` class which can be used to modify/delete your command on the fly, it also contains details about your command. + +### Getting a list of all your commands +You can fetch a list of all your global commands via rest: +```cs +var commands = _client.Rest.GetGlobalApplicationCommands(); +``` +This returns a `IReadOnlyCollection`. + +You can also fetch guild specific commands: +```cs +var commands = _client.Rest.GetGuildApplicationCommands(1234567890) +``` +This returns all the application commands in that guild. + +### Responding + +First thing we want to do is listen to the `InteractionCreated` event. This event is fired when a socket interaction is recieved via the gateway, It looks somthing like this +```cs +_client.InteractionCreated += MyEventHandler; + +... + +private async Task MyEventHandler(SocketInteraction arg) +{ + // handle the interaction here +} +``` + +A socket interaction is made up of these properties and methods: + +| Name | Description | +|--------|--------------| +| Guild | The `SocketGuild` this interaction was used in | +| Channel | The `SocketTextChannel` this interaction was used in | +| Member | The `SocketGuildUser` that executed the interaction | +| Type | The [InteractionType](https://discord.com/developers/docs/interactions/slash-commands#interaction-interactiontype) of this interaction | +| Data | The `SocketInteractionData` associated with this interaction | +| Token | The token used to respond to this interaction | +| Version | The version of this interaction | +| CreatedAt | The time this interaction was created | +| IsValidToken | Whether or not the token to respond to this interaction is still valid | +| RespondAsync | Responds to the interaction | +| FollowupAsync | Sends a followup message to the interaction | + + + +#### Whats the difference between `FollowupAsync` and `RespondAsync`? +RespondAsync is the initial responce to the interaction, its used to "capture" the interaction, while followup is used to send more messages to the interaction. +Basically, you want to first use `RespondAsync` to acknowledge the interaction, then if you need to send anything else regarding that interaction you would use `FollowupAsync` +If you have `AlwaysAcknowledgeInteractions` set to true in your client config then it will automatically acknowledge the interaction without sending a message, +in this case you can use either or to respond. + +#### Example ping pong command +```cs +_client.InteractionCreated += MyEventHandler; +_client.Ready += CreateCommands + +... + +private async Task CreateCommands() +{ + await _client.Rest.CreateGlobalCommand(new Discord.SlashCommandCreationProperties() + { + Name = "ping", + Description = "ping for a pong!", + }); +} + +private async Task MyEventHandler(SocketInteraction arg) +{ + switch(arg.Type) // We want to check the type of this interaction + { + case InteractionType.ApplicationCommand: // If it is a command + await MySlashCommandHandler(arg); // Handle the command somewhere + break; + default: // We dont support it + Console.WriteLine("Unsupported interaction type: " + arg.Type); + break; + } +} + +private async Task MySlashCommandHandler(SocketInteraction arg) +{ + switch(arg.Name) + { + case "ping": + await arg.RespondAsync("Pong!"); + break; + } +} +``` + +#### Example hug command +```cs +_client.InteractionCreated += MyEventHandler; +_client.Ready += CreateCommands; + +... + +private async Task CreateCommands() +{ + await _client.Rest.CreateGlobalCommand(new Discord.SlashCommandCreationProperties() + { + Name = "hug", + Description = "Hugs a user!", + Options = new List() + { + new ApplicationCommandOptionProperties() + { + Name = "User", + Required = true, + Description = "The user to hug", + Type = Discord.ApplicationCommandOptionType.User, + } + } + }); +} + +private async Task MyEventHandler(SocketInteraction arg) +{ + switch(arg.Type) // We want to check the type of this interaction + { + case InteractionType.ApplicationCommand: // If it is a command + await MySlashCommandHandler(arg); // Handle the command somewhere + break; + default: // We dont support it + Console.WriteLine("Unsupported interaction type: " + arg.Type); + break; + } +} + +private async Task MySlashCommandHandler(SocketInteraction arg) +{ + switch(arg.Name) + { + case "hug": + // Get the user argument + var option = arg.Data.Options.First(x => x.Name == "user"); + // We know that the options value must be a user + if(option.Value is SocketGuildUser user) + { + await arg.RespondAsync($"Hugged {user.Mention}"); + } + break; + } +} +``` + + From 21c2a7fa3a3876509e96035dd09eff7f98812e8b Mon Sep 17 00:00:00 2001 From: quin lynch Date: Sun, 27 Dec 2020 05:38:54 -0400 Subject: [PATCH 8/8] SlashCommandBuilder added --- .../Builders/SlashCommandBuilder.cs | 491 ++++++++++++++++++ 1 file changed, 491 insertions(+) create mode 100644 src/Discord.Net.Commands/Builders/SlashCommandBuilder.cs diff --git a/src/Discord.Net.Commands/Builders/SlashCommandBuilder.cs b/src/Discord.Net.Commands/Builders/SlashCommandBuilder.cs new file mode 100644 index 000000000..523137858 --- /dev/null +++ b/src/Discord.Net.Commands/Builders/SlashCommandBuilder.cs @@ -0,0 +1,491 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Discord.Commands.Builders +{ + /// + /// A class used to build slash commands. + /// + public class SlashCommandBuilder + { + /// + /// Returns the maximun length a commands name allowed by Discord + /// + public const int MaxNameLength = 32; + /// + /// Returns the maximum length of a commands description allowed by Discord. + /// + public const int MaxDescriptionLength = 100; + /// + /// Returns the maximum count of command options allowed by Discord + /// + public const int MaxOptionsCount = 10; + + /// + /// The name of this slash command. + /// + public string Name + { + get + { + return _name; + } + set + { + Preconditions.NotNullOrEmpty(value, nameof(Name)); + Preconditions.AtLeast(value.Length, 3, nameof(Name)); + Preconditions.AtMost(value.Length, MaxNameLength, nameof(Name)); + + // Discord updated the docs, this regex prevents special characters like @!$%(... etc, + // https://discord.com/developers/docs/interactions/slash-commands#applicationcommand + if (!Regex.IsMatch(value, @"^[\w-]{3,32}$")) + throw new ArgumentException("Command name cannot contian any special characters or whitespaces!"); + + _name = value; + } + } + + /// + /// A 1-100 length description of this slash command + /// + public string Description + { + get + { + return _description; + } + set + { + Preconditions.AtLeast(value.Length, 1, nameof(Description)); + Preconditions.AtMost(value.Length, MaxDescriptionLength, nameof(Description)); + + _description = value; + } + } + + public ulong GuildId + { + get + { + return _guildId ?? 0; + } + set + { + if (value == 0) + { + throw new ArgumentException("Guild ID cannot be 0!"); + } + + _guildId = value; + + if (isGlobal) + isGlobal = false; + } + } + /// + /// Gets or sets the options for this command. + /// + public List Options + { + get + { + return _options; + } + set + { + if (value != null) + if (value.Count > MaxOptionsCount) + throw new ArgumentException(message: $"Option count must be less than or equal to {MaxOptionsCount}.", paramName: nameof(Options)); + + _options = value; + } + } + + private ulong? _guildId { get; set; } + private string _name { get; set; } + private string _description { get; set; } + private List _options { get; set; } + + internal bool isGlobal { get; set; } + + + public SlashCommandCreationProperties Build() + { + SlashCommandCreationProperties props = new SlashCommandCreationProperties() + { + Name = this.Name, + Description = this.Description, + }; + + if(this.Options != null || this.Options.Any()) + { + var options = new List(); + + this.Options.ForEach(x => options.Add(x.Build())); + + props.Options = options; + } + + return props; + + } + + /// + /// Makes this command a global application command . + /// + /// The current builder. + public SlashCommandBuilder MakeGlobal() + { + this.isGlobal = true; + return this; + } + + /// + /// Makes this command a guild specific command. + /// + /// The Id of the target guild. + /// The current builder. + public SlashCommandBuilder ForGuild(ulong GuildId) + { + this.GuildId = GuildId; + return this; + } + + public SlashCommandBuilder WithName(string Name) + { + this.Name = Name; + return this; + } + + /// + /// Sets the description of the current command. + /// + /// The description of this command. + /// The current builder. + public SlashCommandBuilder WithDescription(string Description) + { + this.Description = Description; + return this; + } + + /// + /// Adds an option to the current slash command. + /// + /// The name of the option to add. + /// The type of this option. + /// The description of this option. + /// If this option is required for this command. + /// If this option is the default option. + /// The options of the option to add. + /// The choices of this option. + /// The current builder. + public SlashCommandBuilder AddOption(string Name, ApplicationCommandOptionType Type, + string Description, bool Required = true, bool Default = false, List Options = null, params ApplicationCommandOptionChoiceProperties[] Choices) + { + // Make sure the name matches the requirements from discord + Preconditions.NotNullOrEmpty(Name, nameof(Name)); + Preconditions.AtLeast(Name.Length, 3, nameof(Name)); + Preconditions.AtMost(Name.Length, MaxNameLength, nameof(Name)); + + // Discord updated the docs, this regex prevents special characters like @!$%( and s p a c e s.. etc, + // https://discord.com/developers/docs/interactions/slash-commands#applicationcommand + if (!Regex.IsMatch(Name, @"^[\w-]{3,32}$")) + throw new ArgumentException("Command name cannot contian any special characters or whitespaces!", nameof(Name)); + + // same with description + Preconditions.NotNullOrEmpty(Description, nameof(Description)); + Preconditions.AtLeast(Description.Length, 3, nameof(Description)); + Preconditions.AtMost(Description.Length, MaxDescriptionLength, nameof(Description)); + + // make sure theres only one option with default set to true + if (Default) + { + if (this.Options != null) + if (this.Options.Any(x => x.Default)) + throw new ArgumentException("There can only be one command option with default set to true!", nameof(Default)); + } + + SlashCommandOptionBuilder option = new SlashCommandOptionBuilder(); + option.Name = Name; + option.Description = Description; + option.Required = Required; + option.Default = Default; + option.Options = Options; + option.Choices = Choices != null ? new List(Choices) : null; + + return AddOption(option); + } + + /// + /// Adds an option to the current slash command. + /// + /// The name of the option to add. + /// The type of this option. + /// The description of this option. + /// If this option is required for this command. + /// If this option is the default option. + /// The choices of this option. + /// The current builder. + public SlashCommandBuilder AddOption(string Name, ApplicationCommandOptionType Type, + string Description, bool Required = true, bool Default = false, params ApplicationCommandOptionChoiceProperties[] Choices) + => AddOption(Name, Type, Description, Required, Default, null, Choices); + + /// + /// Adds an option to the current slash command. + /// + /// The name of the option to add. + /// The type of this option. + /// The sescription of this option. + /// The current builder. + public SlashCommandBuilder AddOption(string Name, ApplicationCommandOptionType Type, string Description) + => AddOption(Name, Type, Description, Options: null, Choices: null); + + /// + /// Adds an option to this slash command. + /// + /// The option to add. + /// The current builder. + public SlashCommandBuilder AddOption(SlashCommandOptionBuilder Option) + { + if (this.Options == null) + this.Options = new List(); + + if (this.Options.Count >= MaxOptionsCount) + throw new ArgumentOutOfRangeException(nameof(Options), $"Cannot have more than {MaxOptionsCount} options!"); + + if (Option == null) + throw new ArgumentNullException(nameof(Option), "Option cannot be null"); + + this.Options.Add(Option); + return this; + } + /// + /// Adds a collection of options to the current slash command. + /// + /// The collection of options to add. + /// The current builder. + public SlashCommandBuilder AddOptions(params SlashCommandOptionBuilder[] Options) + { + if (Options == null) + throw new ArgumentNullException(nameof(Options), "Options cannot be null!"); + + if (Options.Length == 0) + throw new ArgumentException(nameof(Options), "Options cannot be empty!"); + + if (this.Options == null) + this.Options = new List(); + + if (this.Options.Count + Options.Length > MaxOptionsCount) + throw new ArgumentOutOfRangeException(nameof(Options), $"Cannot have more than {MaxOptionsCount} options!"); + + this.Options.AddRange(Options); + return this; + } + } + + /// + /// Represents a class used to build options for the . + /// + public class SlashCommandOptionBuilder + { + /// + /// The max length of a choice's name allowed by Discord. + /// + public const int ChoiceNameMaxLength = 100; + + /// + /// The maximum number of choices allowed by Discord. + /// + public const int MaxChoiceCount = 10; + + private string _name; + private string _description; + + /// + /// The name of this option. + /// + public string Name + { + get => _name; + set + { + if (value?.Length > SlashCommandBuilder.MaxNameLength) + throw new ArgumentException("Name length must be less than or equal to 32"); + if(value?.Length < 3) + throw new ArgumentException("Name length must at least 3 characters in length"); + + // Discord updated the docs, this regex prevents special characters like @!$%(... etc, + // https://discord.com/developers/docs/interactions/slash-commands#applicationcommand + if (!Regex.IsMatch(value, @"^[\w-]{3,32}$")) + throw new ArgumentException("Option name cannot contian any special characters or whitespaces!"); + + _name = value; + } + } + + /// + /// The description of this option. + /// + public string Description + { + get => _description; + set + { + if (value?.Length > SlashCommandBuilder.MaxDescriptionLength) + throw new ArgumentException("Description length must be less than or equal to 100"); + if (value?.Length < 1) + throw new ArgumentException("Name length must at least 1 character in length"); + + _description = value; + } + } + + /// + /// The type of this option. + /// + public ApplicationCommandOptionType Type { get; set; } + + /// + /// The first required option for the user to complete. only one option can be default. + /// + public bool Default { get; set; } + + /// + /// if this option is required for this command, otherwise . + /// + public bool Required { get; set; } + + /// + /// choices for string and int types for the user to pick from. + /// + public List Choices { get; set; } + + /// + /// If the option is a subcommand or subcommand group type, this nested options will be the parameters. + /// + public List Options { get; set; } + + /// + /// Builds the current option. + /// + /// The build version of this option + public ApplicationCommandOptionProperties Build() + { + bool isSubType = this.Type == ApplicationCommandOptionType.SubCommand || this.Type == ApplicationCommandOptionType.SubCommandGroup; + + if (isSubType && (Options == null || !Options.Any())) + throw new ArgumentException(nameof(Options), "SubCommands/SubCommandGroups must have at least one option"); + + if (!isSubType && (Options != null && Options.Any())) + throw new ArgumentException(nameof(Options), $"Cannot have options on {Type} type"); + + return new ApplicationCommandOptionProperties() + { + Name = this.Name, + Description = this.Description, + Default = this.Default, + Required = this.Required, + Type = this.Type, + Options = new List(this.Options.Select(x => x.Build())), + Choices = this.Choices + }; + } + + /// + /// Adds a sub + /// + /// + /// + public SlashCommandOptionBuilder AddOption(SlashCommandOptionBuilder option) + { + if (this.Options == null) + this.Options = new List(); + + if (this.Options.Count >= SlashCommandBuilder.MaxOptionsCount) + throw new ArgumentOutOfRangeException(nameof(Choices), $"There can only be {SlashCommandBuilder.MaxOptionsCount} options per sub command group!"); + + if (option == null) + throw new ArgumentNullException(nameof(option), "Option cannot be null"); + + Options.Add(option); + return this; + } + + public SlashCommandOptionBuilder AddChoice(string Name, int Value) + { + if (Choices == null) + Choices = new List(); + + if (Choices.Count >= MaxChoiceCount) + throw new ArgumentOutOfRangeException(nameof(Choices), $"Cannot add more than {MaxChoiceCount} choices!"); + + Choices.Add(new ApplicationCommandOptionChoiceProperties() + { + Name = Name, + Value = Value + }); + + return this; + } + public SlashCommandOptionBuilder AddChoice(string Name, string Value) + { + if (Choices == null) + Choices = new List(); + + if (Choices.Count >= MaxChoiceCount) + throw new ArgumentOutOfRangeException(nameof(Choices), $"Cannot add more than {MaxChoiceCount} choices!"); + + Choices.Add(new ApplicationCommandOptionChoiceProperties() + { + Name = Name, + Value = Value + }); + + return this; + } + + public SlashCommandOptionBuilder WithName(string Name, int Value) + { + if (Choices == null) + Choices = new List(); + + if (Choices.Count >= MaxChoiceCount) + throw new ArgumentOutOfRangeException(nameof(Choices), $"Cannot add more than {MaxChoiceCount} choices!"); + + Choices.Add(new ApplicationCommandOptionChoiceProperties() + { + Name = Name, + Value = Value + }); + + return this; + } + + public SlashCommandOptionBuilder WithDescription(string Description) + { + this.Description = Description; + return this; + } + + public SlashCommandOptionBuilder WithRequired(bool value) + { + this.Required = value; + return this; + } + + public SlashCommandOptionBuilder WithDefault(bool value) + { + this.Default = value; + return this; + } + public SlashCommandOptionBuilder WithType(ApplicationCommandOptionType Type) + { + this.Type = Type; + return this; + } + } +}