Browse Source

Working version of Interactions. All public classes/members now have correct documentation. Added AlwaysAcknowledgeInteractions to the socket client config

pull/1717/head
quin lynch 4 years ago
parent
commit
670bd92383
23 changed files with 372 additions and 136 deletions
  1. +9
    -9
      src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs
  2. +11
    -1
      src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs
  3. +11
    -8
      src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs
  4. +4
    -4
      src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionData.cs
  5. +8
    -5
      src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionDataOption.cs
  6. +8
    -8
      src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs
  7. +3
    -3
      src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs
  8. +10
    -22
      src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs
  9. +6
    -6
      src/Discord.Net.Core/Entities/Interactions/InteractionResponseType.cs
  10. +3
    -3
      src/Discord.Net.Core/Entities/Interactions/InteractionType.cs
  11. +7
    -0
      src/Discord.Net.Core/Entities/Messages/MessageType.cs
  12. +1
    -1
      src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionDataOption.cs
  13. +8
    -1
      src/Discord.Net.Rest/API/Common/InteractionApplicationCommandCallbackData.cs
  14. +41
    -15
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  15. +0
    -33
      src/Discord.Net.Rest/Entities/Interactions/ApplicationCommands/ApplicationCommandHelper.cs
  16. +21
    -0
      src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs
  17. +25
    -4
      src/Discord.Net.WebSocket/BaseSocketClient.Events.cs
  18. +2
    -0
      src/Discord.Net.WebSocket/DiscordShardedClient.cs
  19. +8
    -5
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  20. +23
    -0
      src/Discord.Net.WebSocket/DiscordSocketConfig.cs
  21. +160
    -5
      src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs
  22. +1
    -1
      src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionData.cs
  23. +2
    -2
      src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionDataOption.cs

+ 9
- 9
src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs View File

@@ -7,47 +7,47 @@ using System.Threading.Tasks;
namespace Discord namespace Discord
{ {
/// <summary> /// <summary>
/// The option type of the Slash command parameter, See <see href="https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoptiontype"/>
/// The option type of the Slash command parameter, See <see href="https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoptiontype">the discord docs</see>.
/// </summary> /// </summary>
public enum ApplicationCommandOptionType : byte public enum ApplicationCommandOptionType : byte
{ {
/// <summary> /// <summary>
/// A sub command
/// A sub command.
/// </summary> /// </summary>
SubCommand = 1, SubCommand = 1,


/// <summary> /// <summary>
/// A group of sub commands
/// A group of sub commands.
/// </summary> /// </summary>
SubCommandGroup = 2, SubCommandGroup = 2,


/// <summary> /// <summary>
/// A <see langword="string"/> of text
/// A <see langword="string"/> of text.
/// </summary> /// </summary>
String = 3, String = 3,


/// <summary> /// <summary>
/// An <see langword="int"/>
/// An <see langword="int"/>.
/// </summary> /// </summary>
Integer = 4, Integer = 4,


/// <summary> /// <summary>
/// A <see langword="bool"/>
/// A <see langword="bool"/>.
/// </summary> /// </summary>
Boolean = 5, Boolean = 5,


/// <summary> /// <summary>
/// A <see cref="IGuildUser"/>
/// A <see cref="IGuildUser"/>.
/// </summary> /// </summary>
User = 6, User = 6,


/// <summary> /// <summary>
/// A <see cref="IGuildChannel"/>
/// A <see cref="IGuildChannel"/>.
/// </summary> /// </summary>
Channel = 7, Channel = 7,


/// <summary> /// <summary>
/// A <see cref="IRole"/>
/// A <see cref="IRole"/>.
/// </summary> /// </summary>
Role = 8 Role = 8
} }


+ 11
- 1
src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs View File

@@ -9,11 +9,21 @@ namespace Discord
/// <summary> /// <summary>
/// Provides properties that are used to modify a <see cref="IApplicationCommand" /> with the specified changes. /// Provides properties that are used to modify a <see cref="IApplicationCommand" /> with the specified changes.
/// </summary> /// </summary>
/// <see cref="Ia"/>
public class ApplicationCommandProperties public class ApplicationCommandProperties
{ {
/// <summary>
/// Gets or sets the name of this command.
/// </summary>
public string Name { get; set; } public string Name { get; set; }

/// <summary>
/// Gets or sets the discription of this command.
/// </summary>
public string Description { get; set; } public string Description { get; set; }

/// <summary>
/// Gets or sets the options for this command.
/// </summary>
public Optional<IEnumerable<IApplicationCommandOption>> Options { get; set; } public Optional<IEnumerable<IApplicationCommandOption>> Options { get; set; }
} }
} }

+ 11
- 8
src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs View File

@@ -7,32 +7,37 @@ using System.Threading.Tasks;
namespace Discord namespace Discord
{ {
/// <summary> /// <summary>
/// The base command model that belongs to an application. see <see href="https://discord.com/developers/docs/interactions/slash-commands#applicationcommand"/>
/// The base command model that belongs to an application. see <see href="https://discord.com/developers/docs/interactions/slash-commands#applicationcommand"/>
/// </summary> /// </summary>
public interface IApplicationCommand : ISnowflakeEntity public interface IApplicationCommand : ISnowflakeEntity
{ {
/// <summary> /// <summary>
/// Gets the unique id of the command
/// Gets the unique id of the command.
/// </summary> /// </summary>
ulong Id { get; } ulong Id { get; }


/// <summary> /// <summary>
/// Gets the unique id of the parent application
/// Gets the unique id of the parent application.
/// </summary> /// </summary>
ulong ApplicationId { get; } ulong ApplicationId { get; }


/// <summary> /// <summary>
/// The name of the command
/// The name of the command.
/// </summary> /// </summary>
string Name { get; } string Name { get; }


/// <summary> /// <summary>
/// The description of the command
/// The description of the command.
/// </summary> /// </summary>
string Description { get; } string Description { get; }


/// <summary> /// <summary>
/// Modifies this command
/// If the option is a subcommand or subcommand group type, this nested options will be the parameters.
/// </summary>
IEnumerable<IApplicationCommandOption>? Options { get; }

/// <summary>
/// Modifies this command.
/// </summary> /// </summary>
/// <param name="func">The delegate containing the properties to modify the command with.</param> /// <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> /// <param name="options">The options to be used when sending the request.</param>
@@ -40,7 +45,5 @@ namespace Discord
/// A task that represents the asynchronous modification operation. /// A task that represents the asynchronous modification operation.
/// </returns> /// </returns>
Task ModifyAsync(Action<ApplicationCommandProperties> func, RequestOptions options = null); Task ModifyAsync(Action<ApplicationCommandProperties> func, RequestOptions options = null);

IEnumerable<IApplicationCommandOption>? Options { get; }
} }
} }

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

@@ -7,22 +7,22 @@ using System.Threading.Tasks;
namespace Discord namespace Discord
{ {
/// <summary> /// <summary>
/// Represents data of an Interaction Command, see <see href="https://discord.com/developers/docs/interactions/slash-commands#interaction-applicationcommandinteractiondata"/>
/// Represents data of an Interaction Command, see <see href="https://discord.com/developers/docs/interactions/slash-commands#interaction-applicationcommandinteractiondata"/>
/// </summary> /// </summary>
public interface IApplicationCommandInteractionData public interface IApplicationCommandInteractionData
{ {
/// <summary> /// <summary>
/// The snowflake id of this command
/// The snowflake id of this command
/// </summary> /// </summary>
ulong Id { get; } ulong Id { get; }


/// <summary> /// <summary>
/// The name of this command
/// The name of this command
/// </summary> /// </summary>
string Name { get; } string Name { get; }


/// <summary> /// <summary>
/// The params + values from the user
/// The params + values from the user
/// </summary> /// </summary>
IReadOnlyCollection<IApplicationCommandInteractionDataOption> Options { get; } IReadOnlyCollection<IApplicationCommandInteractionDataOption> Options { get; }
} }


+ 8
- 5
src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionDataOption.cs View File

@@ -7,22 +7,25 @@ using System.Threading.Tasks;
namespace Discord namespace Discord
{ {
/// <summary> /// <summary>
/// Represents a option group for a command, see <see href="https://discord.com/developers/docs/interactions/slash-commands#interaction-applicationcommandinteractiondataoption"/>
/// Represents a option group for a command, see <see href="https://discord.com/developers/docs/interactions/slash-commands#interaction-applicationcommandinteractiondataoption"/>
/// </summary> /// </summary>
public interface IApplicationCommandInteractionDataOption public interface IApplicationCommandInteractionDataOption
{ {
/// <summary> /// <summary>
/// The name of the parameter
/// The name of the parameter.
/// </summary> /// </summary>
string Name { get; } string Name { get; }


/// <summary> /// <summary>
/// The value of the pair
/// The value of the pair.
/// <note>
/// This objects type can be any one of the option types in <see cref="ApplicationCommandOptionType"/>
/// </note>
/// </summary> /// </summary>
ApplicationCommandOptionType? Value { get; }
object? Value { get; }


/// <summary> /// <summary>
/// Present if this option is a group or subcommand
/// Present if this option is a group or subcommand.
/// </summary> /// </summary>
IReadOnlyCollection<IApplicationCommandInteractionDataOption> Options { get; } IReadOnlyCollection<IApplicationCommandInteractionDataOption> Options { get; }




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

@@ -7,42 +7,42 @@ using System.Threading.Tasks;
namespace Discord namespace Discord
{ {
/// <summary> /// <summary>
/// Options for the <see cref="IApplicationCommand"/>, see <see href="https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoption"/>
/// Options for the <see cref="IApplicationCommand"/>, see <see href="https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoption"/>The docs</see>.
/// </summary> /// </summary>
public interface IApplicationCommandOption public interface IApplicationCommandOption
{ {
/// <summary> /// <summary>
/// The type of this <see cref="IApplicationCommandOption"/>
/// The type of this <see cref="IApplicationCommandOption"/>.
/// </summary> /// </summary>
ApplicationCommandOptionType Type { get; } ApplicationCommandOptionType Type { get; }


/// <summary> /// <summary>
/// The name of this command option, 1-32 character name.
/// The name of this command option, 1-32 character name.
/// </summary> /// </summary>
string Name { get; } string Name { get; }


/// <summary> /// <summary>
/// The discription of this command option, 1-100 character description
/// The discription of this command option, 1-100 character description.
/// </summary> /// </summary>
string Description { get; } string Description { get; }


/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
bool? Default { get; } bool? Default { get; }


/// <summary> /// <summary>
/// if the parameter is required or optional--default <see langword="false"/>
/// If the parameter is required or optional, default is <see langword="false"/>.
/// </summary> /// </summary>
bool? Required { get; } bool? Required { get; }


/// <summary> /// <summary>
/// choices for string and int types for the user to pick from
/// Choices for string and int types for the user to pick from.
/// </summary> /// </summary>
IEnumerable<IApplicationCommandOptionChoice>? Choices { get; } IEnumerable<IApplicationCommandOptionChoice>? Choices { get; }


/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
IEnumerable<IApplicationCommandOption>? Options { get; } IEnumerable<IApplicationCommandOption>? Options { get; }
} }


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

@@ -7,17 +7,17 @@ using System.Threading.Tasks;
namespace Discord namespace Discord
{ {
/// <summary> /// <summary>
/// Specifies choices for command group
/// Specifies choices for command group.
/// </summary> /// </summary>
public interface IApplicationCommandOptionChoice public interface IApplicationCommandOptionChoice
{ {
/// <summary> /// <summary>
/// 1-100 character choice name
/// 1-100 character choice name.
/// </summary> /// </summary>
string Name { get; } string Name { get; }


/// <summary> /// <summary>
/// value of the choice
/// value of the choice.
/// </summary> /// </summary>
string Value { get; } string Value { get; }




+ 10
- 22
src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs View File

@@ -7,48 +7,36 @@ using System.Threading.Tasks;
namespace Discord namespace Discord
{ {
/// <summary> /// <summary>
/// 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 <see href="https://discord.com/developers/docs/interactions/slash-commands#interaction"/>
/// Represents a discord interaction
/// <para>
/// 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 <see href="https://discord.com/developers/docs/interactions/slash-commands#interaction"/>.
/// </para>
/// </summary> /// </summary>
public interface IDiscordInteraction : ISnowflakeEntity public interface IDiscordInteraction : ISnowflakeEntity
{ {
/// <summary> /// <summary>
/// id of the interaction
/// The id of the interaction.
/// </summary> /// </summary>
ulong Id { get; } ulong Id { get; }


/// <summary> /// <summary>
/// The type of this <see cref="IDiscordInteraction"/>
/// The type of this <see cref="IDiscordInteraction"/>.
/// </summary> /// </summary>
InteractionType Type { get; } InteractionType Type { get; }


/// <summary> /// <summary>
/// The command data payload
/// The command data payload.
/// </summary> /// </summary>
IApplicationCommandInteractionData? Data { get; } IApplicationCommandInteractionData? Data { get; }


/// <summary> /// <summary>
/// The guild it was sent from
/// </summary>
ulong GuildId { get; }

/// <summary>
/// The channel it was sent from
/// </summary>
ulong ChannelId { get; }

/// <summary>
/// Guild member id for the invoking user
/// </summary>
ulong MemberId { get; }

/// <summary>
/// A continuation token for responding to the interaction
/// A continuation token for responding to the interaction.
/// </summary> /// </summary>
string Token { get; } string Token { get; }


/// <summary> /// <summary>
/// read-only property, always 1
/// read-only property, always 1.
/// </summary> /// </summary>
int Version { get; } int Version { get; }
} }


+ 6
- 6
src/Discord.Net.Core/Entities/Interactions/InteractionResponseType.cs View File

@@ -7,32 +7,32 @@ using System.Threading.Tasks;
namespace Discord namespace Discord
{ {
/// <summary> /// <summary>
/// The response type for an <see cref="IDiscordInteraction"/>
/// The response type for an <see cref="IDiscordInteraction"/>.
/// </summary> /// </summary>
public enum InteractionResponseType : byte public enum InteractionResponseType : byte
{ {
/// <summary> /// <summary>
/// ACK a Ping
/// ACK a Ping.
/// </summary> /// </summary>
Pong = 1, Pong = 1,


/// <summary> /// <summary>
/// ACK a command without sending a message, eating the user's input
/// ACK a command without sending a message, eating the user's input.
/// </summary> /// </summary>
Acknowledge = 2, Acknowledge = 2,


/// <summary> /// <summary>
/// Respond with a message, eating the user's input
/// Respond with a message, eating the user's input.
/// </summary> /// </summary>
ChannelMessage = 3, ChannelMessage = 3,


/// <summary> /// <summary>
/// respond with a message, showing the user's input
/// Respond with a message, showing the user's input.
/// </summary> /// </summary>
ChannelMessageWithSource = 4, ChannelMessageWithSource = 4,


/// <summary> /// <summary>
/// ACK a command without sending a message, showing the user's input
/// ACK a command without sending a message, showing the user's input.
/// </summary> /// </summary>
ACKWithSource = 5 ACKWithSource = 5
} }


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

@@ -7,17 +7,17 @@ using System.Threading.Tasks;
namespace Discord namespace Discord
{ {
/// <summary> /// <summary>
/// Represents a type of Interaction from discord.
/// Represents a type of Interaction from discord.
/// </summary> /// </summary>
public enum InteractionType : byte public enum InteractionType : byte
{ {
/// <summary> /// <summary>
/// A ping from discord
/// A ping from discord.
/// </summary> /// </summary>
Ping = 1, Ping = 1,


/// <summary> /// <summary>
/// An <see cref="IApplicationCommand"/> sent from discord
/// An <see cref="IApplicationCommand"/> sent from discord.
/// </summary> /// </summary>
ApplicationCommand = 2 ApplicationCommand = 2
} }


+ 7
- 0
src/Discord.Net.Core/Entities/Messages/MessageType.cs View File

@@ -64,5 +64,12 @@ namespace Discord
/// Only available in API v8. /// Only available in API v8.
/// </remarks> /// </remarks>
Reply = 19, Reply = 19,
/// <summary>
/// The message is an Application Command
/// </summary>
/// <remarks>
/// Only available in API v8
/// </remarks>
ApplicationCommand = 20
} }
} }

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

@@ -13,7 +13,7 @@ namespace Discord.API
public string Name { get; set; } public string Name { get; set; }


[JsonProperty("value")] [JsonProperty("value")]
public Optional<ApplicationCommandOptionType> Value { get; set; }
public Optional<object> Value { get; set; }


[JsonProperty("options")] [JsonProperty("options")]
public Optional<IEnumerable<ApplicationCommandInteractionDataOption>> Options { get; set; } public Optional<IEnumerable<ApplicationCommandInteractionDataOption>> Options { get; set; }


+ 8
- 1
src/Discord.Net.Rest/API/Common/InteractionApplicationCommandCallbackData.cs View File

@@ -13,12 +13,19 @@ namespace Discord.API
public Optional<bool> TTS { get; set; } public Optional<bool> TTS { get; set; }


[JsonProperty("content")] [JsonProperty("content")]
public string Content { get; set; }
public Optional<string> Content { get; set; }


[JsonProperty("embeds")] [JsonProperty("embeds")]
public Optional<Embed[]> Embeds { get; set; } public Optional<Embed[]> Embeds { get; set; }


[JsonProperty("allowed_mentions")] [JsonProperty("allowed_mentions")]
public Optional<AllowedMentions> AllowedMentions { get; set; } public Optional<AllowedMentions> AllowedMentions { get; set; }

public InteractionApplicationCommandCallbackData() { }
public InteractionApplicationCommandCallbackData(string text)
{
this.Content = text;
}
} }
} }

+ 41
- 15
src/Discord.Net.Rest/DiscordRestApiClient.cs View File

@@ -787,13 +787,7 @@ namespace Discord.API


//Interactions //Interactions
public async Task<ApplicationCommand[]> GetGlobalApplicationCommandsAsync(RequestOptions options = null) public async Task<ApplicationCommand[]> GetGlobalApplicationCommandsAsync(RequestOptions options = null)
{
try
{
return await SendAsync<ApplicationCommand[]>("GET", $"applications/{this.CurrentUserId}/commands", options: options).ConfigureAwait(false);
}
catch (HttpException ex) { return null; }
}
=> await SendAsync<ApplicationCommand[]>("GET", $"applications/{this.CurrentUserId}/commands", options: options).ConfigureAwait(false);


public async Task<ApplicationCommand> CreateGlobalApplicationCommandAsync(ApplicationCommandParams command, RequestOptions options = null) public async Task<ApplicationCommand> CreateGlobalApplicationCommandAsync(ApplicationCommandParams command, RequestOptions options = null)
{ {
@@ -844,21 +838,53 @@ namespace Discord.API
=> await SendAsync<ApplicationCommand>("DELETE", $"applications/{this.CurrentUserId}/guilds/{guildId}/commands/{commandId}", options: options).ConfigureAwait(false); => await SendAsync<ApplicationCommand>("DELETE", $"applications/{this.CurrentUserId}/guilds/{guildId}/commands/{commandId}", options: options).ConfigureAwait(false);


//Interaction Responses //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) 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) 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<Message> 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<Message>("POST", $"webhooks/{CurrentUserId}/{token}?wait=true", args, options: options).ConfigureAwait(false);
}

public async Task<Message> 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<Message>("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 //Guilds


+ 0
- 33
src/Discord.Net.Rest/Entities/Interactions/ApplicationCommands/ApplicationCommandHelper.cs View File

@@ -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<Model> ModifyAsync(IApplicationCommand command, BaseDiscordClient client,
Action<ApplicationCommandProperties> 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<API.ApplicationCommandOption[]>.Unspecified,
};

return await client.ApiClient.ModifyGlobalApplicationCommandAsync(apiArgs, command.Id, options);
}
}
}

+ 21
- 0
src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs View File

@@ -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<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 entity = RestUserMessage.Create(client, channel, client.CurrentUser, model);
return entity;
}
}
}

+ 25
- 4
src/Discord.Net.WebSocket/BaseSocketClient.Events.cs View File

@@ -23,7 +23,7 @@ namespace Discord.WebSocket
/// <code language="cs" region="ChannelCreated" /// <code language="cs" region="ChannelCreated"
/// source="..\Discord.Net.Examples\WebSocket\BaseSocketClient.Events.Examples.cs"/> /// source="..\Discord.Net.Examples\WebSocket\BaseSocketClient.Events.Examples.cs"/>
/// </example> /// </example>
public event Func<SocketChannel, Task> ChannelCreated
public event Func<SocketChannel, Task> ChannelCreated
{ {
add { _channelCreatedEvent.Add(value); } add { _channelCreatedEvent.Add(value); }
remove { _channelCreatedEvent.Remove(value); } remove { _channelCreatedEvent.Remove(value); }
@@ -70,7 +70,7 @@ namespace Discord.WebSocket
public event Func<SocketChannel, SocketChannel, Task> ChannelUpdated { public event Func<SocketChannel, SocketChannel, Task> ChannelUpdated {
add { _channelUpdatedEvent.Add(value); } add { _channelUpdatedEvent.Add(value); }
remove { _channelUpdatedEvent.Remove(value); } remove { _channelUpdatedEvent.Remove(value); }
}
}
internal readonly AsyncEvent<Func<SocketChannel, SocketChannel, Task>> _channelUpdatedEvent = new AsyncEvent<Func<SocketChannel, SocketChannel, Task>>(); internal readonly AsyncEvent<Func<SocketChannel, SocketChannel, Task>> _channelUpdatedEvent = new AsyncEvent<Func<SocketChannel, SocketChannel, Task>>();


//Messages //Messages
@@ -351,7 +351,7 @@ namespace Discord.WebSocket
add { _guildMemberUpdatedEvent.Add(value); } add { _guildMemberUpdatedEvent.Add(value); }
remove { _guildMemberUpdatedEvent.Remove(value); } remove { _guildMemberUpdatedEvent.Remove(value); }
} }
internal readonly AsyncEvent<Func<SocketGuildUser, SocketGuildUser, Task>> _guildMemberUpdatedEvent = new AsyncEvent<Func<SocketGuildUser, SocketGuildUser, Task>>();
internal readonly AsyncEvent<Func<SocketGuildUser, SocketGuildUser, Task>> _guildMemberUpdatedEvent = new AsyncEvent<Func<SocketGuildUser, SocketGuildUser, Task>>();
/// <summary> Fired when a user joins, leaves, or moves voice channels. </summary> /// <summary> Fired when a user joins, leaves, or moves voice channels. </summary>
public event Func<SocketUser, SocketVoiceState, SocketVoiceState, Task> UserVoiceStateUpdated { public event Func<SocketUser, SocketVoiceState, SocketVoiceState, Task> UserVoiceStateUpdated {
add { _userVoiceStateUpdatedEvent.Add(value); } add { _userVoiceStateUpdatedEvent.Add(value); }
@@ -361,7 +361,7 @@ namespace Discord.WebSocket
/// <summary> Fired when the bot connects to a Discord voice server. </summary> /// <summary> Fired when the bot connects to a Discord voice server. </summary>
public event Func<SocketVoiceServer, Task> VoiceServerUpdated public event Func<SocketVoiceServer, Task> VoiceServerUpdated
{ {
add { _voiceServerUpdatedEvent.Add(value); }
add { _voiceServerUpdatedEvent.Add(value); }
remove { _voiceServerUpdatedEvent.Remove(value); } remove { _voiceServerUpdatedEvent.Remove(value); }
} }
internal readonly AsyncEvent<Func<SocketVoiceServer, Task>> _voiceServerUpdatedEvent = new AsyncEvent<Func<SocketVoiceServer, Task>>(); internal readonly AsyncEvent<Func<SocketVoiceServer, Task>> _voiceServerUpdatedEvent = new AsyncEvent<Func<SocketVoiceServer, Task>>();
@@ -431,5 +431,26 @@ namespace Discord.WebSocket
remove { _inviteDeletedEvent.Remove(value); } remove { _inviteDeletedEvent.Remove(value); }
} }
internal readonly AsyncEvent<Func<SocketGuildChannel, string, Task>> _inviteDeletedEvent = new AsyncEvent<Func<SocketGuildChannel, string, Task>>(); internal readonly AsyncEvent<Func<SocketGuildChannel, string, Task>> _inviteDeletedEvent = new AsyncEvent<Func<SocketGuildChannel, string, Task>>();

//Interactions
/// <summary>
/// Fired when an Interaction is created.
/// </summary>
/// <remarks>
/// <para>
/// This event is fired when an interaction is created. The event handler must return a
/// <see cref="Task"/> and accept a <see cref="SocketInteraction"/> as its parameter.
/// </para>
/// <para>
/// The interaction created will be passed into the <see cref="SocketInteraction"/> parameter.
/// </para>
/// </remarks>
public event Func<SocketInteraction, Task> InteractionCreated
{
add { _interactionCreatedEvent.Add(value); }
remove { _interactionCreatedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketInteraction, Task>> _interactionCreatedEvent = new AsyncEvent<Func<SocketInteraction, Task>>();

} }
} }

+ 2
- 0
src/Discord.Net.WebSocket/DiscordShardedClient.cs View File

@@ -378,6 +378,8 @@ namespace Discord.WebSocket


client.InviteCreated += (invite) => _inviteCreatedEvent.InvokeAsync(invite); client.InviteCreated += (invite) => _inviteCreatedEvent.InvokeAsync(invite);
client.InviteDeleted += (channel, invite) => _inviteDeletedEvent.InvokeAsync(channel, invite); client.InviteDeleted += (channel, invite) => _inviteDeletedEvent.InvokeAsync(channel, invite);

client.InteractionCreated += (interaction) => _interactionCreatedEvent.InvokeAsync(interaction);
} }


//IDiscordClient //IDiscordClient


+ 8
- 5
src/Discord.Net.WebSocket/DiscordSocketClient.cs View File

@@ -73,6 +73,7 @@ namespace Discord.WebSocket
internal bool AlwaysDownloadUsers { get; private set; } internal bool AlwaysDownloadUsers { get; private set; }
internal int? HandlerTimeout { get; private set; } internal int? HandlerTimeout { get; private set; }
internal bool? ExclusiveBulkDelete { get; private set; } internal bool? ExclusiveBulkDelete { get; private set; }
internal bool AlwaysAcknowledgeInteractions { get; private set; }


internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient;
/// <inheritdoc /> /// <inheritdoc />
@@ -134,6 +135,7 @@ namespace Discord.WebSocket
UdpSocketProvider = config.UdpSocketProvider; UdpSocketProvider = config.UdpSocketProvider;
WebSocketProvider = config.WebSocketProvider; WebSocketProvider = config.WebSocketProvider;
AlwaysDownloadUsers = config.AlwaysDownloadUsers; AlwaysDownloadUsers = config.AlwaysDownloadUsers;
AlwaysAcknowledgeInteractions = config.AlwaysAcknowledgeInteractions;
HandlerTimeout = config.HandlerTimeout; HandlerTimeout = config.HandlerTimeout;
ExclusiveBulkDelete = config.ExclusiveBulkDelete; ExclusiveBulkDelete = config.ExclusiveBulkDelete;
State = new ClientState(0, 0); State = new ClientState(0, 0);
@@ -1792,11 +1794,12 @@ namespace Discord.WebSocket
return; 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 else
{ {


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

@@ -105,6 +105,29 @@ namespace Discord.WebSocket
/// </remarks> /// </remarks>
public bool AlwaysDownloadUsers { get; set; } = false; public bool AlwaysDownloadUsers { get; set; } = false;


/// <summary>
/// Gets or sets whether or not interactions are acknowledge with source.
/// </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
/// </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.
/// </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.
/// </note>
/// </remarks>
public bool AlwaysAcknowledgeInteractions { get; set; } = true;

/// <summary> /// <summary>
/// Gets or sets the timeout for event handlers, in milliseconds, after which a warning will be logged. /// Gets or sets the timeout for event handlers, in milliseconds, after which a warning will be logged.
/// Setting this property to <c>null</c>disables this check. /// Setting this property to <c>null</c>disables this check.


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

@@ -1,3 +1,4 @@
using Discord.Rest;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -5,28 +6,63 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Model = Discord.API.Gateway.InteractionCreated; using Model = Discord.API.Gateway.InteractionCreated;


namespace Discord.WebSocket.Entities.Interaction
namespace Discord.WebSocket
{ {
/// <summary>
/// Represents an Interaction recieved over the gateway
/// </summary>
public class SocketInteraction : SocketEntity<ulong>, IDiscordInteraction public class SocketInteraction : SocketEntity<ulong>, IDiscordInteraction
{ {
/// <summary>
/// The <see cref="SocketGuild"/> this interaction was used in
/// </summary>
public SocketGuild Guild public SocketGuild Guild
=> Discord.GetGuild(GuildId); => Discord.GetGuild(GuildId);

/// <summary>
/// The <see cref="SocketTextChannel"/> this interaction was used in
/// </summary>
public SocketTextChannel Channel public SocketTextChannel Channel
=> Guild.GetTextChannel(ChannelId); => Guild.GetTextChannel(ChannelId);

/// <summary>
/// The <see cref="SocketGuildUser"/> who triggered this interaction
/// </summary>
public SocketGuildUser Member public SocketGuildUser Member
=> Guild.GetUser(MemberId); => Guild.GetUser(MemberId);


/// <summary>
/// The type of this interaction
/// </summary>
public InteractionType Type { get; private set; } public InteractionType Type { get; private set; }

/// <summary>
/// The data associated with this interaction
/// </summary>
public IApplicationCommandInteractionData Data { get; private set; } public IApplicationCommandInteractionData Data { get; private set; }

/// <summary>
/// The token used to respond to this interaction
/// </summary>
public string Token { get; private set; } public string Token { get; private set; }

/// <summary>
/// The version of this interaction
/// </summary>
public int Version { get; private set; } public int Version { get; private set; }

public DateTimeOffset CreatedAt { get; } public DateTimeOffset CreatedAt { get; }


public ulong GuildId { get; private set; }
public ulong ChannelId { get; private set; }
public ulong MemberId { get; private set; }
/// <summary>
/// <see langword="true"/> if the token is valid for replying to, otherwise <see langword="false"/>
/// </summary>
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) internal SocketInteraction(DiscordSocketClient client, ulong id)
: base(client, id) : base(client, id)
{ {
@@ -52,6 +88,125 @@ namespace Discord.WebSocket.Entities.Interaction
this.MemberId = model.Member.User.Id; this.MemberId = model.Member.User.Id;
this.Type = model.Type; 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;
}

/// <summary>
/// Responds to an Interaction, eating its input
/// <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)"/>
/// </para>
/// </summary>
/// <param name="text">The text of the message to be sent</param>
/// <param name="isTTS"><see langword="true"/> if the message should be read out by a text-to-speech reader, otherwise <see langword="false"/></param>
/// <param name="embed">A <see cref="Embed"/> to send with this response</param>
/// <param name="Type">The type of response to this Interaction</param>
/// <param name="allowedMentions">The allowed mentions for this response</param>
/// <param name="options">The request options for this response</param>
/// <returns>
/// 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>

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)
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<API.Embed[]>.Unspecified,
TTS = isTTS
}
};

await Discord.Rest.ApiClient.CreateInteractionResponse(response, this.Id, Token, options);
return null;
}

/// <summary>
/// Sends a followup message for this interaction
/// </summary>
/// <param name="text">The text of the message to be sent</param>
/// <param name="isTTS"><see langword="true"/> if the message should be read out by a text-to-speech reader, otherwise <see langword="false"/></param>
/// <param name="embed">A <see cref="Embed"/> to send with this response</param>
/// <param name="Type">The type of response to this Interaction</param>
/// <param name="allowedMentions">The allowed mentions for this response</param>
/// <param name="options">The request options for this response</param>
/// <returns>
/// The sent message
/// </returns>
public async Task<IMessage> 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<API.Embed[]>.Unspecified,
};
return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options);
}

/// <summary>
/// Acknowledges this interaction with the <see cref="InteractionResponseType.ACKWithSource"/>
/// </summary>
/// <returns>
/// A task that represents the asynchronous operation of acknowledging the interaction
/// </returns>
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);
}
} }
} }

+ 1
- 1
src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionData.cs View File

@@ -6,7 +6,7 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Model = Discord.API.ApplicationCommandInteractionData; using Model = Discord.API.ApplicationCommandInteractionData;


namespace Discord.WebSocket.Entities.Interaction
namespace Discord.WebSocket
{ {
public class SocketInteractionData : SocketEntity<ulong>, IApplicationCommandInteractionData public class SocketInteractionData : SocketEntity<ulong>, IApplicationCommandInteractionData
{ {


+ 2
- 2
src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionDataOption.cs View File

@@ -6,12 +6,12 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Model = Discord.API.ApplicationCommandInteractionDataOption; using Model = Discord.API.ApplicationCommandInteractionDataOption;


namespace Discord.WebSocket.Entities.Interaction
namespace Discord.WebSocket
{ {
public class SocketInteractionDataOption : IApplicationCommandInteractionDataOption public class SocketInteractionDataOption : IApplicationCommandInteractionDataOption
{ {
public string Name { get; private set; } public string Name { get; private set; }
public ApplicationCommandOptionType? Value { get; private set; }
public object? Value { get; private set; }


public IReadOnlyCollection<IApplicationCommandInteractionDataOption> Options { get; private set; } public IReadOnlyCollection<IApplicationCommandInteractionDataOption> Options { get; private set; }




Loading…
Cancel
Save