Browse Source

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.
pull/1717/head
quin lynch 4 years ago
parent
commit
a0f9646235
15 changed files with 434 additions and 27 deletions
  1. +25
    -3
      src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs
  2. +4
    -7
      src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs
  3. +2
    -2
      src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs
  4. +7
    -1
      src/Discord.Net.Rest/API/Common/ApplicationCommand.cs
  5. +19
    -0
      src/Discord.Net.Rest/ClientHelper.cs
  6. +8
    -0
      src/Discord.Net.Rest/DiscordRestClient.cs
  7. +123
    -1
      src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs
  8. +58
    -0
      src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs
  9. +22
    -0
      src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandChoice.cs
  10. +57
    -0
      src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs
  11. +14
    -0
      src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandType.cs
  12. +41
    -0
      src/Discord.Net.Rest/Entities/Interactions/RestGlobalCommand.cs
  13. +40
    -0
      src/Discord.Net.Rest/Entities/Interactions/RestGuildCommand.cs
  14. +9
    -9
      src/Discord.Net.WebSocket/DiscordSocketConfig.cs
  15. +5
    -4
      src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs

+ 25
- 3
src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs View File

@@ -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; }
}
}

+ 4
- 7
src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs View File

@@ -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);
}
}

+ 2
- 2
src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs View File

@@ -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; }
}
}

+ 7
- 1
src/Discord.Net.Rest/API/Common/ApplicationCommand.cs View File

@@ -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; }
}
}

+ 19
- 0
src/Discord.Net.Rest/ClientHelper.cs View File

@@ -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();
}
}
}

+ 8
- 0
src/Discord.Net.Rest/DiscordRestClient.cs View File

@@ -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 />


+ 123
- 1
src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs View File

@@ -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);
}
}
}

+ 58
- 0
src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs View File

@@ -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();
}
}

+ 22
- 0
src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandChoice.cs View File

@@ -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;
}
}
}

+ 57
- 0
src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs View File

@@ -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;
}
}
}

+ 14
- 0
src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandType.cs View File

@@ -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
}
}

+ 41
- 0
src/Discord.Net.Rest/Entities/Interactions/RestGlobalCommand.cs View File

@@ -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);
}
}

+ 40
- 0
src/Discord.Net.Rest/Entities/Interactions/RestGuildCommand.cs View File

@@ -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);
}
}

+ 9
- 9
src/Discord.Net.WebSocket/DiscordSocketConfig.cs View File

@@ -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;


+ 5
- 4
src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs View File

@@ -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)


Loading…
Cancel
Save