From a0f9646235fb0aff3e7b4ed14c976ef7f0c4b814 Mon Sep 17 00:00:00 2001 From: quin lynch Date: Sat, 19 Dec 2020 14:44:43 -0400 Subject: [PATCH] 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)