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) + { + + } + } +}