@@ -7,47 +7,47 @@ using System.Threading.Tasks; | |||
namespace Discord | |||
{ | |||
/// <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> | |||
public enum ApplicationCommandOptionType : byte | |||
{ | |||
/// <summary> | |||
/// A sub command | |||
/// A sub command. | |||
/// </summary> | |||
SubCommand = 1, | |||
/// <summary> | |||
/// A group of sub commands | |||
/// A group of sub commands. | |||
/// </summary> | |||
SubCommandGroup = 2, | |||
/// <summary> | |||
/// A <see langword="string"/> of text | |||
/// A <see langword="string"/> of text. | |||
/// </summary> | |||
String = 3, | |||
/// <summary> | |||
/// An <see langword="int"/> | |||
/// An <see langword="int"/>. | |||
/// </summary> | |||
Integer = 4, | |||
/// <summary> | |||
/// A <see langword="bool"/> | |||
/// A <see langword="bool"/>. | |||
/// </summary> | |||
Boolean = 5, | |||
/// <summary> | |||
/// A <see cref="IGuildUser"/> | |||
/// A <see cref="IGuildUser"/>. | |||
/// </summary> | |||
User = 6, | |||
/// <summary> | |||
/// A <see cref="IGuildChannel"/> | |||
/// A <see cref="IGuildChannel"/>. | |||
/// </summary> | |||
Channel = 7, | |||
/// <summary> | |||
/// A <see cref="IRole"/> | |||
/// A <see cref="IRole"/>. | |||
/// </summary> | |||
Role = 8 | |||
} | |||
@@ -9,11 +9,21 @@ namespace Discord | |||
/// <summary> | |||
/// Provides properties that are used to modify a <see cref="IApplicationCommand" /> with the specified changes. | |||
/// </summary> | |||
/// <see cref="Ia"/> | |||
public class ApplicationCommandProperties | |||
{ | |||
/// <summary> | |||
/// Gets or sets the name of this command. | |||
/// </summary> | |||
public string Name { get; set; } | |||
/// <summary> | |||
/// Gets or sets the discription of this command. | |||
/// </summary> | |||
public string Description { get; set; } | |||
/// <summary> | |||
/// Gets or sets the options for this command. | |||
/// </summary> | |||
public Optional<IEnumerable<IApplicationCommandOption>> Options { get; set; } | |||
} | |||
} |
@@ -7,32 +7,37 @@ using System.Threading.Tasks; | |||
namespace Discord | |||
{ | |||
/// <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> | |||
public interface IApplicationCommand : ISnowflakeEntity | |||
{ | |||
/// <summary> | |||
/// Gets the unique id of the command | |||
/// Gets the unique id of the command. | |||
/// </summary> | |||
ulong Id { get; } | |||
/// <summary> | |||
/// Gets the unique id of the parent application | |||
/// Gets the unique id of the parent application. | |||
/// </summary> | |||
ulong ApplicationId { get; } | |||
/// <summary> | |||
/// The name of the command | |||
/// The name of the command. | |||
/// </summary> | |||
string Name { get; } | |||
/// <summary> | |||
/// The description of the command | |||
/// The description of the command. | |||
/// </summary> | |||
string Description { get; } | |||
/// <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> | |||
/// <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> | |||
@@ -40,7 +45,5 @@ namespace Discord | |||
/// A task that represents the asynchronous modification operation. | |||
/// </returns> | |||
Task ModifyAsync(Action<ApplicationCommandProperties> func, RequestOptions options = null); | |||
IEnumerable<IApplicationCommandOption>? Options { get; } | |||
} | |||
} |
@@ -7,22 +7,22 @@ using System.Threading.Tasks; | |||
namespace Discord | |||
{ | |||
/// <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> | |||
public interface IApplicationCommandInteractionData | |||
{ | |||
/// <summary> | |||
/// The snowflake id of this command | |||
/// The snowflake id of this command | |||
/// </summary> | |||
ulong Id { get; } | |||
/// <summary> | |||
/// The name of this command | |||
/// The name of this command | |||
/// </summary> | |||
string Name { get; } | |||
/// <summary> | |||
/// The params + values from the user | |||
/// The params + values from the user | |||
/// </summary> | |||
IReadOnlyCollection<IApplicationCommandInteractionDataOption> Options { get; } | |||
} | |||
@@ -7,22 +7,25 @@ using System.Threading.Tasks; | |||
namespace Discord | |||
{ | |||
/// <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> | |||
public interface IApplicationCommandInteractionDataOption | |||
{ | |||
/// <summary> | |||
/// The name of the parameter | |||
/// The name of the parameter. | |||
/// </summary> | |||
string Name { get; } | |||
/// <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> | |||
ApplicationCommandOptionType? Value { get; } | |||
object? Value { get; } | |||
/// <summary> | |||
/// Present if this option is a group or subcommand | |||
/// Present if this option is a group or subcommand. | |||
/// </summary> | |||
IReadOnlyCollection<IApplicationCommandInteractionDataOption> Options { get; } | |||
@@ -7,42 +7,42 @@ using System.Threading.Tasks; | |||
namespace Discord | |||
{ | |||
/// <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> | |||
public interface IApplicationCommandOption | |||
{ | |||
/// <summary> | |||
/// The type of this <see cref="IApplicationCommandOption"/> | |||
/// The type of this <see cref="IApplicationCommandOption"/>. | |||
/// </summary> | |||
ApplicationCommandOptionType Type { get; } | |||
/// <summary> | |||
/// The name of this command option, 1-32 character name. | |||
/// The name of this command option, 1-32 character name. | |||
/// </summary> | |||
string Name { get; } | |||
/// <summary> | |||
/// The discription of this command option, 1-100 character description | |||
/// The discription of this command option, 1-100 character description. | |||
/// </summary> | |||
string Description { get; } | |||
/// <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> | |||
bool? Default { get; } | |||
/// <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> | |||
bool? Required { get; } | |||
/// <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> | |||
IEnumerable<IApplicationCommandOptionChoice>? Choices { get; } | |||
/// <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> | |||
IEnumerable<IApplicationCommandOption>? Options { get; } | |||
} | |||
@@ -7,17 +7,17 @@ using System.Threading.Tasks; | |||
namespace Discord | |||
{ | |||
/// <summary> | |||
/// Specifies choices for command group | |||
/// Specifies choices for command group. | |||
/// </summary> | |||
public interface IApplicationCommandOptionChoice | |||
{ | |||
/// <summary> | |||
/// 1-100 character choice name | |||
/// 1-100 character choice name. | |||
/// </summary> | |||
string Name { get; } | |||
/// <summary> | |||
/// value of the choice | |||
/// value of the choice. | |||
/// </summary> | |||
string Value { get; } | |||
@@ -7,48 +7,36 @@ using System.Threading.Tasks; | |||
namespace Discord | |||
{ | |||
/// <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> | |||
public interface IDiscordInteraction : ISnowflakeEntity | |||
{ | |||
/// <summary> | |||
/// id of the interaction | |||
/// The id of the interaction. | |||
/// </summary> | |||
ulong Id { get; } | |||
/// <summary> | |||
/// The type of this <see cref="IDiscordInteraction"/> | |||
/// The type of this <see cref="IDiscordInteraction"/>. | |||
/// </summary> | |||
InteractionType Type { get; } | |||
/// <summary> | |||
/// The command data payload | |||
/// The command data payload. | |||
/// </summary> | |||
IApplicationCommandInteractionData? Data { get; } | |||
/// <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> | |||
string Token { get; } | |||
/// <summary> | |||
/// read-only property, always 1 | |||
/// read-only property, always 1. | |||
/// </summary> | |||
int Version { get; } | |||
} | |||
@@ -7,32 +7,32 @@ using System.Threading.Tasks; | |||
namespace Discord | |||
{ | |||
/// <summary> | |||
/// The response type for an <see cref="IDiscordInteraction"/> | |||
/// The response type for an <see cref="IDiscordInteraction"/>. | |||
/// </summary> | |||
public enum InteractionResponseType : byte | |||
{ | |||
/// <summary> | |||
/// ACK a Ping | |||
/// ACK a Ping. | |||
/// </summary> | |||
Pong = 1, | |||
/// <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> | |||
Acknowledge = 2, | |||
/// <summary> | |||
/// Respond with a message, eating the user's input | |||
/// Respond with a message, eating the user's input. | |||
/// </summary> | |||
ChannelMessage = 3, | |||
/// <summary> | |||
/// respond with a message, showing the user's input | |||
/// Respond with a message, showing the user's input. | |||
/// </summary> | |||
ChannelMessageWithSource = 4, | |||
/// <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> | |||
ACKWithSource = 5 | |||
} | |||
@@ -7,17 +7,17 @@ using System.Threading.Tasks; | |||
namespace Discord | |||
{ | |||
/// <summary> | |||
/// Represents a type of Interaction from discord. | |||
/// Represents a type of Interaction from discord. | |||
/// </summary> | |||
public enum InteractionType : byte | |||
{ | |||
/// <summary> | |||
/// A ping from discord | |||
/// A ping from discord. | |||
/// </summary> | |||
Ping = 1, | |||
/// <summary> | |||
/// An <see cref="IApplicationCommand"/> sent from discord | |||
/// An <see cref="IApplicationCommand"/> sent from discord. | |||
/// </summary> | |||
ApplicationCommand = 2 | |||
} | |||
@@ -64,5 +64,12 @@ namespace Discord | |||
/// Only available in API v8. | |||
/// </remarks> | |||
Reply = 19, | |||
/// <summary> | |||
/// The message is an Application Command | |||
/// </summary> | |||
/// <remarks> | |||
/// Only available in API v8 | |||
/// </remarks> | |||
ApplicationCommand = 20 | |||
} | |||
} |
@@ -13,7 +13,7 @@ namespace Discord.API | |||
public string Name { get; set; } | |||
[JsonProperty("value")] | |||
public Optional<ApplicationCommandOptionType> Value { get; set; } | |||
public Optional<object> Value { get; set; } | |||
[JsonProperty("options")] | |||
public Optional<IEnumerable<ApplicationCommandInteractionDataOption>> Options { get; set; } | |||
@@ -13,12 +13,19 @@ namespace Discord.API | |||
public Optional<bool> TTS { get; set; } | |||
[JsonProperty("content")] | |||
public string Content { get; set; } | |||
public Optional<string> Content { get; set; } | |||
[JsonProperty("embeds")] | |||
public Optional<Embed[]> Embeds { get; set; } | |||
[JsonProperty("allowed_mentions")] | |||
public Optional<AllowedMentions> AllowedMentions { get; set; } | |||
public InteractionApplicationCommandCallbackData() { } | |||
public InteractionApplicationCommandCallbackData(string text) | |||
{ | |||
this.Content = text; | |||
} | |||
} | |||
} |
@@ -787,13 +787,7 @@ namespace Discord.API | |||
//Interactions | |||
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) | |||
{ | |||
@@ -844,21 +838,53 @@ namespace Discord.API | |||
=> await SendAsync<ApplicationCommand>("DELETE", $"applications/{this.CurrentUserId}/guilds/{guildId}/commands/{commandId}", options: options).ConfigureAwait(false); | |||
//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) | |||
=> 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) | |||
=> 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 | |||
@@ -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); | |||
} | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
} |
@@ -23,7 +23,7 @@ namespace Discord.WebSocket | |||
/// <code language="cs" region="ChannelCreated" | |||
/// source="..\Discord.Net.Examples\WebSocket\BaseSocketClient.Events.Examples.cs"/> | |||
/// </example> | |||
public event Func<SocketChannel, Task> ChannelCreated | |||
public event Func<SocketChannel, Task> ChannelCreated | |||
{ | |||
add { _channelCreatedEvent.Add(value); } | |||
remove { _channelCreatedEvent.Remove(value); } | |||
@@ -70,7 +70,7 @@ namespace Discord.WebSocket | |||
public event Func<SocketChannel, SocketChannel, Task> ChannelUpdated { | |||
add { _channelUpdatedEvent.Add(value); } | |||
remove { _channelUpdatedEvent.Remove(value); } | |||
} | |||
} | |||
internal readonly AsyncEvent<Func<SocketChannel, SocketChannel, Task>> _channelUpdatedEvent = new AsyncEvent<Func<SocketChannel, SocketChannel, Task>>(); | |||
//Messages | |||
@@ -351,7 +351,7 @@ namespace Discord.WebSocket | |||
add { _guildMemberUpdatedEvent.Add(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> | |||
public event Func<SocketUser, SocketVoiceState, SocketVoiceState, Task> UserVoiceStateUpdated { | |||
add { _userVoiceStateUpdatedEvent.Add(value); } | |||
@@ -361,7 +361,7 @@ namespace Discord.WebSocket | |||
/// <summary> Fired when the bot connects to a Discord voice server. </summary> | |||
public event Func<SocketVoiceServer, Task> VoiceServerUpdated | |||
{ | |||
add { _voiceServerUpdatedEvent.Add(value); } | |||
add { _voiceServerUpdatedEvent.Add(value); } | |||
remove { _voiceServerUpdatedEvent.Remove(value); } | |||
} | |||
internal readonly AsyncEvent<Func<SocketVoiceServer, Task>> _voiceServerUpdatedEvent = new AsyncEvent<Func<SocketVoiceServer, Task>>(); | |||
@@ -431,5 +431,26 @@ namespace Discord.WebSocket | |||
remove { _inviteDeletedEvent.Remove(value); } | |||
} | |||
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>>(); | |||
} | |||
} |
@@ -378,6 +378,8 @@ namespace Discord.WebSocket | |||
client.InviteCreated += (invite) => _inviteCreatedEvent.InvokeAsync(invite); | |||
client.InviteDeleted += (channel, invite) => _inviteDeletedEvent.InvokeAsync(channel, invite); | |||
client.InteractionCreated += (interaction) => _interactionCreatedEvent.InvokeAsync(interaction); | |||
} | |||
//IDiscordClient | |||
@@ -73,6 +73,7 @@ namespace Discord.WebSocket | |||
internal bool AlwaysDownloadUsers { get; private set; } | |||
internal int? HandlerTimeout { get; private set; } | |||
internal bool? ExclusiveBulkDelete { get; private set; } | |||
internal bool AlwaysAcknowledgeInteractions { get; private set; } | |||
internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; | |||
/// <inheritdoc /> | |||
@@ -134,6 +135,7 @@ namespace Discord.WebSocket | |||
UdpSocketProvider = config.UdpSocketProvider; | |||
WebSocketProvider = config.WebSocketProvider; | |||
AlwaysDownloadUsers = config.AlwaysDownloadUsers; | |||
AlwaysAcknowledgeInteractions = config.AlwaysAcknowledgeInteractions; | |||
HandlerTimeout = config.HandlerTimeout; | |||
ExclusiveBulkDelete = config.ExclusiveBulkDelete; | |||
State = new ClientState(0, 0); | |||
@@ -1792,11 +1794,12 @@ namespace Discord.WebSocket | |||
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 | |||
{ | |||
@@ -105,6 +105,29 @@ namespace Discord.WebSocket | |||
/// </remarks> | |||
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> | |||
/// 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. | |||
@@ -1,3 +1,4 @@ | |||
using Discord.Rest; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
@@ -5,28 +6,63 @@ using System.Text; | |||
using System.Threading.Tasks; | |||
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 | |||
{ | |||
/// <summary> | |||
/// The <see cref="SocketGuild"/> this interaction was used in | |||
/// </summary> | |||
public SocketGuild Guild | |||
=> Discord.GetGuild(GuildId); | |||
/// <summary> | |||
/// The <see cref="SocketTextChannel"/> this interaction was used in | |||
/// </summary> | |||
public SocketTextChannel Channel | |||
=> Guild.GetTextChannel(ChannelId); | |||
/// <summary> | |||
/// The <see cref="SocketGuildUser"/> who triggered this interaction | |||
/// </summary> | |||
public SocketGuildUser Member | |||
=> Guild.GetUser(MemberId); | |||
/// <summary> | |||
/// The type of this interaction | |||
/// </summary> | |||
public InteractionType Type { get; private set; } | |||
/// <summary> | |||
/// The data associated with this interaction | |||
/// </summary> | |||
public IApplicationCommandInteractionData Data { get; private set; } | |||
/// <summary> | |||
/// The token used to respond to this interaction | |||
/// </summary> | |||
public string Token { get; private set; } | |||
/// <summary> | |||
/// The version of this interaction | |||
/// </summary> | |||
public int Version { get; private set; } | |||
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) | |||
: base(client, id) | |||
{ | |||
@@ -52,6 +88,125 @@ namespace Discord.WebSocket.Entities.Interaction | |||
this.MemberId = model.Member.User.Id; | |||
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); | |||
} | |||
} | |||
} |
@@ -6,7 +6,7 @@ using System.Text; | |||
using System.Threading.Tasks; | |||
using Model = Discord.API.ApplicationCommandInteractionData; | |||
namespace Discord.WebSocket.Entities.Interaction | |||
namespace Discord.WebSocket | |||
{ | |||
public class SocketInteractionData : SocketEntity<ulong>, IApplicationCommandInteractionData | |||
{ | |||
@@ -6,12 +6,12 @@ using System.Text; | |||
using System.Threading.Tasks; | |||
using Model = Discord.API.ApplicationCommandInteractionDataOption; | |||
namespace Discord.WebSocket.Entities.Interaction | |||
namespace Discord.WebSocket | |||
{ | |||
public class SocketInteractionDataOption : IApplicationCommandInteractionDataOption | |||
{ | |||
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; } | |||