New Rest entities: RestApplicationCommand,RestGlobalCommand, RestGuildCommand, RestApplicationCommandOption, RestApplicationCommandChoice, RestApplicationCommandType. Added public methods to the RestClient to fetch/create/edit interactions.pull/1717/head
@@ -11,19 +11,41 @@ namespace Discord | |||
/// </summary> | |||
public class ApplicationCommandProperties | |||
{ | |||
private string _name { get; set; } | |||
private string _description { get; set; } | |||
/// <summary> | |||
/// Gets or sets the name of this command. | |||
/// </summary> | |||
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; | |||
} | |||
} | |||
/// <summary> | |||
/// Gets or sets the discription of this command. | |||
/// </summary> | |||
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; | |||
} | |||
} | |||
/// <summary> | |||
/// Gets or sets the options for this command. | |||
/// </summary> | |||
public Optional<IEnumerable<IApplicationCommandOption>> Options { get; set; } | |||
public Optional<List<IApplicationCommandOption>> Options { get; set; } | |||
} | |||
} |
@@ -34,16 +34,13 @@ namespace Discord | |||
/// <summary> | |||
/// If the option is a subcommand or subcommand group type, this nested options will be the parameters. | |||
/// </summary> | |||
IEnumerable<IApplicationCommandOption>? Options { get; } | |||
IReadOnlyCollection<IApplicationCommandOption> Options { get; } | |||
/// <summary> | |||
/// Modifies this command. | |||
/// Deletes this command | |||
/// </summary> | |||
/// <param name="func">The delegate containing the properties to modify the command with.</param> | |||
/// <param name="options">The options to be used when sending the request.</param> | |||
/// <returns> | |||
/// A task that represents the asynchronous modification operation. | |||
/// </returns> | |||
Task ModifyAsync(Action<ApplicationCommandProperties> func, RequestOptions options = null); | |||
/// <returns></returns> | |||
Task DeleteAsync(RequestOptions options = null); | |||
} | |||
} |
@@ -39,11 +39,11 @@ namespace Discord | |||
/// <summary> | |||
/// Choices for string and int types for the user to pick from. | |||
/// </summary> | |||
IEnumerable<IApplicationCommandOptionChoice>? Choices { get; } | |||
IReadOnlyCollection<IApplicationCommandOptionChoice>? Choices { get; } | |||
/// <summary> | |||
/// if the option is a subcommand or subcommand group type, this nested options will be the parameters. | |||
/// </summary> | |||
IEnumerable<IApplicationCommandOption>? Options { get; } | |||
IReadOnlyCollection<IApplicationCommandOption>? Options { get; } | |||
} | |||
} |
@@ -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<ApplicationCommandOption[]> Options { get; set; } | |||
} | |||
} |
@@ -201,5 +201,24 @@ namespace Discord.Rest | |||
} | |||
}; | |||
} | |||
public static async Task<RestGlobalCommand[]> 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<RestGuildCommand[]> 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(); | |||
} | |||
} | |||
} |
@@ -107,6 +107,14 @@ namespace Discord.Rest | |||
=> ClientHelper.GetVoiceRegionAsync(this, id, options); | |||
public Task<RestWebhook> GetWebhookAsync(ulong id, RequestOptions options = null) | |||
=> ClientHelper.GetWebhookAsync(this, id, options); | |||
public Task<RestGlobalCommand> CreateGobalCommand(Action<ApplicationCommandProperties> func, RequestOptions options = null) | |||
=> InteractionHelper.CreateGlobalCommand(this, func, options); | |||
public Task<RestGuildCommand> CreateGuildCommand(Action<ApplicationCommandProperties> func, ulong guildId, RequestOptions options = null) | |||
=> InteractionHelper.CreateGuildCommand(this, guildId, func, options); | |||
public Task<RestGlobalCommand[]> GetGlobalApplicationCommands(RequestOptions options = null) | |||
=> ClientHelper.GetGlobalApplicationCommands(this, options); | |||
public Task<RestGuildCommand[]> GetGuildApplicationCommands(ulong guildId, RequestOptions options = null) | |||
=> ClientHelper.GetGuildApplicationCommands(this, guildId, options); | |||
//IDiscordClient | |||
/// <inheritdoc /> | |||
@@ -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<RestUserMessage> 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<RestGlobalCommand> CreateGlobalCommand(BaseDiscordClient client, | |||
Action<ApplicationCommandProperties> 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<ApplicationCommandOption[]>.Unspecified | |||
}; | |||
var cmd = await client.ApiClient.CreateGlobalApplicationCommandAsync(model, options).ConfigureAwait(false); | |||
return RestGlobalCommand.Create(client, cmd); | |||
} | |||
internal static async Task<RestGlobalCommand> ModifyGlobalCommand(BaseDiscordClient client, RestGlobalCommand command, | |||
Action<ApplicationCommandProperties> 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<ApplicationCommandOption[]>.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<RestGuildCommand> CreateGuildCommand(BaseDiscordClient client, ulong guildId, | |||
Action<ApplicationCommandProperties> 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<ApplicationCommandOption[]>.Unspecified | |||
}; | |||
var cmd = await client.ApiClient.CreateGuildApplicationCommandAsync(model, guildId, options).ConfigureAwait(false); | |||
return RestGuildCommand.Create(client, cmd, guildId); | |||
} | |||
internal static async Task<RestGuildCommand> ModifyGuildCommand(BaseDiscordClient client, RestGuildCommand command, | |||
Action<ApplicationCommandProperties> 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<ApplicationCommandOption[]>.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); | |||
} | |||
} | |||
} |
@@ -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 | |||
{ | |||
/// <summary> | |||
/// Represents a rest implementation of the <see cref="IApplicationCommand"/> | |||
/// </summary> | |||
public abstract class RestApplicationCommand : RestEntity<ulong>, IApplicationCommand | |||
{ | |||
public ulong ApplicationId { get; private set; } | |||
public string Name { get; private set; } | |||
public string Description { get; private set; } | |||
public IReadOnlyCollection<IApplicationCommandOption> 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(); | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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<IApplicationCommandOptionChoice> Choices { get; private set; } | |||
public IReadOnlyCollection<IApplicationCommandOption> 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; | |||
} | |||
} | |||
} |
@@ -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 | |||
} | |||
} |
@@ -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 | |||
{ | |||
/// <summary> | |||
/// Represents a global Slash command | |||
/// </summary> | |||
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); | |||
/// <summary> | |||
/// Modifies this <see cref="RestApplicationCommand"/>. | |||
/// </summary> | |||
/// <param name="func">The delegate containing the properties to modify the command with.</param> | |||
/// <param name="options">The options to be used when sending the request.</param> | |||
/// <returns> | |||
/// The modified command | |||
/// </returns> | |||
public async Task<RestGlobalCommand> ModifyAsync(Action<ApplicationCommandProperties> func, RequestOptions options = null) | |||
=> await InteractionHelper.ModifyGlobalCommand(Discord, this, func, options).ConfigureAwait(false); | |||
} | |||
} |
@@ -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); | |||
/// <summary> | |||
/// Modifies this <see cref="RestApplicationCommand"/>. | |||
/// </summary> | |||
/// <param name="func">The delegate containing the properties to modify the command with.</param> | |||
/// <param name="options">The options to be used when sending the request.</param> | |||
/// <returns> | |||
/// The modified command | |||
/// </returns> | |||
public async Task<RestGuildCommand> ModifyAsync(Action<ApplicationCommandProperties> func, RequestOptions options = null) | |||
=> await InteractionHelper.ModifyGuildCommand(Discord, this, func, options).ConfigureAwait(false); | |||
} | |||
} |
@@ -110,20 +110,20 @@ namespace Discord.WebSocket | |||
/// </summary> | |||
/// <remarks> | |||
/// <para> | |||
/// Discord interactions will not go thru in chat until the client responds to them. With this option set to | |||
/// <see langword="true"/> the client will automatically acknowledge the interaction with <see cref="InteractionResponseType.ACKWithSource"/>. | |||
/// see <see href="https://discord.com/developers/docs/interactions/slash-commands#interaction-interactionresponsetype">the docs</see> 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 | |||
/// <see langword="true"/>, the client will automatically acknowledge the interaction with <see cref="InteractionResponseType.ACKWithSource"/>. | |||
/// See <see href="https://discord.com/developers/docs/interactions/slash-commands#interaction-interactionresponsetype">the docs</see> on | |||
/// responding to interactions for more info. | |||
/// </para> | |||
/// <para> | |||
/// With this option set to <see langword="false"/> you will have to acknowledge the interaction with | |||
/// <see cref="SocketInteraction.RespondAsync(string, bool, Embed, InteractionResponseType, AllowedMentions, RequestOptions)"/>, | |||
/// only after the interaction is captured the origional slash command message will be visible. | |||
/// With this option set to <see langword="false"/>, you will have to acknowledge the interaction with | |||
/// <see cref="SocketInteraction.RespondAsync(string, bool, Embed, InteractionResponseType, AllowedMentions, RequestOptions)"/>. | |||
/// Only after the interaction is acknowledged, the origional slash command message will be visible. | |||
/// </para> | |||
/// <note> | |||
/// 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. | |||
/// </note> | |||
/// </remarks> | |||
public bool AlwaysAcknowledgeInteractions { get; set; } = true; | |||
@@ -95,10 +95,10 @@ namespace Discord.WebSocket | |||
} | |||
/// <summary> | |||
/// Responds to an Interaction, eating its input | |||
/// Responds to an Interaction. | |||
/// <para> | |||
/// If you have <see cref="DiscordSocketConfig.AlwaysAcknowledgeInteractions"/> set to <see langword="true"/>, this method | |||
/// will be obsolete and will use <see cref="FollowupAsync(string, bool, Embed, InteractionResponseType, AllowedMentions, RequestOptions)"/> | |||
/// If you have <see cref="DiscordSocketConfig.AlwaysAcknowledgeInteractions"/> set to <see langword="true"/>, You should use | |||
/// <see cref="FollowupAsync(string, bool, Embed, InteractionResponseType, AllowedMentions, RequestOptions)"/> instead. | |||
/// </para> | |||
/// </summary> | |||
/// <param name="text">The text of the message to be sent</param> | |||
@@ -111,10 +111,11 @@ namespace Discord.WebSocket | |||
/// The <see cref="IMessage"/> sent as the response. If this is the first acknowledgement, it will return null; | |||
/// </returns> | |||
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> | |||
/// <exception cref="InvalidOperationException">The parameters provided were invalid or the token was invalid</exception> | |||
public async Task<IMessage> 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) | |||