Browse Source

Component alpha build!

pull/1923/head
quin lynch 4 years ago
parent
commit
80ab30e12f
18 changed files with 450 additions and 118 deletions
  1. +2
    -2
      src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs
  2. +11
    -1
      src/Discord.Net.Core/Entities/Interactions/InteractionResponseType.cs
  3. +7
    -2
      src/Discord.Net.Core/Entities/Interactions/InteractionType.cs
  4. +3
    -0
      src/Discord.Net.Core/Entities/Interactions/Message Components/ActionRowComponent.cs
  5. +8
    -0
      src/Discord.Net.Core/Entities/Interactions/Message Components/ButtonComponent.cs
  6. +3
    -0
      src/Discord.Net.Rest/API/Common/InteractionApplicationCommandCallbackData.cs
  7. +18
    -0
      src/Discord.Net.Rest/API/Common/MessageComponentInteractionData.cs
  8. +3
    -0
      src/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs
  9. +1
    -1
      src/Discord.Net.WebSocket/API/Gateway/InteractionCreated.cs
  10. +156
    -0
      src/Discord.Net.WebSocket/Entities/Interaction/Message Components/SocketMessageComponent.cs
  11. +28
    -0
      src/Discord.Net.WebSocket/Entities/Interaction/Message Components/SocketMessageComponentData.cs
  12. +0
    -0
      src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketApplicationCommand.cs
  13. +0
    -0
      src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketApplicationCommandChoice.cs
  14. +0
    -0
      src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketApplicationCommandOption.cs
  15. +161
    -0
      src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommand.cs
  16. +7
    -7
      src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommandData.cs
  17. +11
    -11
      src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommandDataOption.cs
  18. +31
    -94
      src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs

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

@@ -26,9 +26,9 @@ namespace Discord
InteractionType Type { get; }

/// <summary>
/// The command data payload.
/// Represents the data sent within this interaction.
/// </summary>
IApplicationCommandInteractionData? Data { get; }
object Data { get; }

/// <summary>
/// A continuation token for responding to the interaction.


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

@@ -42,6 +42,16 @@ namespace Discord
/// <summary>
/// ACK an interaction and edit a response later, the user sees a loading state.
/// </summary>
DeferredChannelMessageWithSource = 5
DeferredChannelMessageWithSource = 5,

/// <summary>
/// for components: ACK an interaction and edit the original message later; the user does not see a loading state
/// </summary>
DeferredUpdateMessage = 6,

/// <summary>
/// for components: edit the message the component was attached to
/// </summary>
UpdateMessage = 7
}
}

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

@@ -17,8 +17,13 @@ namespace Discord
Ping = 1,

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

/// <summary>
/// A <see cref="IMessageComponent"/> sent from discord.
/// </summary>
MessageComponent = 3,
}
}

+ 3
- 0
src/Discord.Net.Core/Entities/Interactions/Message Components/ActionRowComponent.cs View File

@@ -1,3 +1,4 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -8,8 +9,10 @@ namespace Discord
{
public class ActionRowComponent : IMessageComponent
{
[JsonProperty("type")]
public ComponentType Type { get; } = ComponentType.ActionRow;

[JsonProperty("components")]
public IReadOnlyCollection<IMessageComponent> Components { get; internal set; }

internal ActionRowComponent() { }


+ 8
- 0
src/Discord.Net.Core/Entities/Interactions/Message Components/ButtonComponent.cs View File

@@ -1,3 +1,4 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -8,18 +9,25 @@ namespace Discord
{
public class ButtonComponent : IMessageComponent
{
[JsonProperty("type")]
public ComponentType Type { get; } = ComponentType.Button;

[JsonProperty("style")]
public ButtonStyle Style { get; }

[JsonProperty("label")]
public string Label { get; }

[JsonProperty("emoji")]
public IEmote Emote { get; }

[JsonProperty("custom_id")]
public string CustomId { get; }

[JsonProperty("url")]
public string Url { get; }

[JsonProperty("disabled")]
public bool Disabled { get; }

internal ButtonComponent(ButtonStyle style, string label, IEmote emote, string customId, string url, bool disabled)


+ 3
- 0
src/Discord.Net.Rest/API/Common/InteractionApplicationCommandCallbackData.cs View File

@@ -25,6 +25,9 @@ namespace Discord.API
[JsonProperty("flags")]
public Optional<int> Flags { get; set; }

[JsonProperty("components")]
public Optional<IMessageComponent[]> Components { get; set; }

public InteractionApplicationCommandCallbackData() { }
public InteractionApplicationCommandCallbackData(string text)
{


+ 18
- 0
src/Discord.Net.Rest/API/Common/MessageComponentInteractionData.cs View File

@@ -0,0 +1,18 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord.API
{
internal class MessageComponentInteractionData
{
[JsonProperty("custom_id")]
public string CustomId { get; set; }

[JsonProperty("component_type")]
public ComponentType ComponentType { get; set; }
}
}

+ 3
- 0
src/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs View File

@@ -30,6 +30,9 @@ namespace Discord.API.Rest
[JsonProperty("flags")]
public Optional<int> Flags { get; set; }

[JsonProperty("components")]
public Optional<IMessageComponent[]> Components { get; set; }

public CreateWebhookMessageParams(string content)
{
Content = content;


+ 1
- 1
src/Discord.Net.WebSocket/API/Gateway/InteractionCreated.cs View File

@@ -17,7 +17,7 @@ namespace Discord.API.Gateway
public InteractionType Type { get; set; }

[JsonProperty("data")]
public Optional<ApplicationCommandInteractionData> Data { get; set; }
public Optional<object> Data { get; set; }

[JsonProperty("guild_id")]
public ulong GuildId { get; set; }


+ 156
- 0
src/Discord.Net.WebSocket/Entities/Interaction/Message Components/SocketMessageComponent.cs View File

@@ -0,0 +1,156 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Model = Discord.API.Gateway.InteractionCreated;
using DataModel = Discord.API.MessageComponentInteractionData;
using Newtonsoft.Json.Linq;
using Discord.Rest;

namespace Discord.WebSocket
{
public class SocketMessageComponent : SocketInteraction
{
new public SocketMessageComponentData Data { get; }

internal SocketMessageComponent(DiscordSocketClient client, Model model)
: base(client, model.Id)
{
var dataModel = model.Data.IsSpecified ?
(model.Data.Value as JToken).ToObject<DataModel>()
: null;

this.Data = new SocketMessageComponentData(dataModel);
}

new internal static SocketMessageComponent Create(DiscordSocketClient client, Model model)
{
var entity = new SocketMessageComponent(client, model);
entity.Update(model);
return entity;
}

/// <summary>
/// Responds to an Interaction.
/// <para>
/// 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>
/// <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="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</param>
/// <param name="allowedMentions">The allowed mentions for this response.</param>
/// <param name="options">The request options for this response.</param>
/// <param name="component">A <see cref="MessageComponent"/> to be sent with 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>
/// <exception cref="InvalidOperationException">The parameters provided were invalid or the token was invalid.</exception>

public override async Task<RestUserMessage> RespondAsync(string text = null, bool isTTS = false, Embed embed = null, InteractionResponseType type = InteractionResponseType.ChannelMessageWithSource,
bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null)
{
if (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(text, isTTS, embed, ephemeral, type, allowedMentions, options);

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,
Components = component?.ToModel() ?? Optional<IMessageComponent[]>.Unspecified
}
};

if (ephemeral)
response.Data.Value.Flags = 64;

return await InteractionHelper.SendInteractionResponse(this.Discord, this.Channel, response, this.Id, Token, options);
}

/// <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="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</param>
/// <param name="allowedMentions">The allowed mentions for this response.</param>
/// <param name="options">The request options for this response.</param>
/// <param name="component">A <see cref="MessageComponent"/> to be sent with this response</param>
/// <returns>
/// The sent message.
/// </returns>
public override async Task<RestFollowupMessage> FollowupAsync(string text = null, bool isTTS = false, Embed embed = null, bool ephemeral = false,
InteractionResponseType type = InteractionResponseType.ChannelMessageWithSource,
AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null)
{
if (type == InteractionResponseType.DeferredChannelMessageWithSource || type == InteractionResponseType.DeferredChannelMessageWithSource || type == InteractionResponseType.Pong)
throw new InvalidOperationException($"Cannot use {type} on a slash command!");

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,
Components = component?.ToModel() ?? Optional<IMessageComponent[]>.Unspecified
};

if (ephemeral)
args.Flags = 64;

return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options);
}

public override Task AcknowledgeAsync(RequestOptions options = null)
{
var response = new API.InteractionResponse()
{
Type = InteractionResponseType.DeferredUpdateMessage,
};

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

+ 28
- 0
src/Discord.Net.WebSocket/Entities/Interaction/Message Components/SocketMessageComponentData.cs View File

@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Model = Discord.API.MessageComponentInteractionData;

namespace Discord.WebSocket
{
public class SocketMessageComponentData
{
/// <summary>
/// The components Custom Id that was clicked
/// </summary>
public string CustomId { get; }

/// <summary>
/// The type of the component clicked
/// </summary>
public ComponentType Type { get; }

internal SocketMessageComponentData(Model model)
{
this.CustomId = model.CustomId;
this.Type = model.ComponentType;
}
}
}

src/Discord.Net.WebSocket/Entities/Interaction/SocketApplicationCommand.cs → src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketApplicationCommand.cs View File


src/Discord.Net.WebSocket/Entities/Interaction/SocketApplicationCommandChoice.cs → src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketApplicationCommandChoice.cs View File


src/Discord.Net.WebSocket/Entities/Interaction/SocketApplicationCommandOption.cs → src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketApplicationCommandOption.cs View File


+ 161
- 0
src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommand.cs View File

@@ -0,0 +1,161 @@
using Discord.Rest;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Model = Discord.API.Gateway.InteractionCreated;
using DataModel = Discord.API.ApplicationCommandInteractionData;

namespace Discord.WebSocket
{
public class SocketSlashCommand : SocketInteraction
{
/// <summary>
/// The data associated with this interaction.
/// </summary>
new public SocketSlashCommandData Data { get; private set; }

internal SocketSlashCommand(DiscordSocketClient client, Model model)
: base(client, model.Id)
{
var dataModel = model.Data.IsSpecified ?
(model.Data.Value as JToken).ToObject<DataModel>()
: null;

Data = SocketSlashCommandData.Create(client, dataModel, model.GuildId);
}

new internal static SocketInteraction Create(DiscordSocketClient client, Model model)
{
var entity = new SocketSlashCommand(client, model);
entity.Update(model);
return entity;
}

internal override void Update(Model model)
{
var data = model.Data.IsSpecified ?
(model.Data.Value as JToken).ToObject<DataModel>()
: null;

this.Data.Update(data, this.Guild.Id);

base.Update(model);
}

/// <summary>
/// Responds to an Interaction.
/// <para>
/// 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>
/// <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="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</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>
/// <exception cref="InvalidOperationException">The parameters provided were invalid or the token was invalid.</exception>

public override async Task<RestUserMessage> RespondAsync(string text = null, bool isTTS = false, Embed embed = null, InteractionResponseType type = InteractionResponseType.ChannelMessageWithSource,
bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null)
{
if (type == InteractionResponseType.Pong)
throw new InvalidOperationException($"Cannot use {Type} on a send message function");

if(type == InteractionResponseType.DeferredUpdateMessage || type == InteractionResponseType.UpdateMessage)
throw new InvalidOperationException($"Cannot use {Type} on a slash command!");

if (!IsValidToken)
throw new InvalidOperationException("Interaction token is no longer valid");

if (Discord.AlwaysAcknowledgeInteractions)
return await FollowupAsync(text, isTTS, embed, ephemeral, type, allowedMentions, options); // The arguments should be passed? What was i thinking...

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,
Components = component?.ToModel() ?? Optional<IMessageComponent[]>.Unspecified
}
};

if (ephemeral)
response.Data.Value.Flags = 64;

return await InteractionHelper.SendInteractionResponse(this.Discord, this.Channel, response, this.Id, Token, options);
}

/// <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="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</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 override async Task<RestFollowupMessage> FollowupAsync(string text = null, bool isTTS = false, Embed embed = null, bool ephemeral = false,
InteractionResponseType type = InteractionResponseType.ChannelMessageWithSource,
AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null)
{
if (type == InteractionResponseType.DeferredChannelMessageWithSource || type == InteractionResponseType.DeferredChannelMessageWithSource || type == InteractionResponseType.Pong || type == InteractionResponseType.DeferredUpdateMessage || type == InteractionResponseType.UpdateMessage)
throw new InvalidOperationException($"Cannot use {type} on a slash command!");

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,
Components = component?.ToModel() ?? Optional<IMessageComponent[]>.Unspecified
};

if (ephemeral)
args.Flags = 64;

return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options);
}
}
}

src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionData.cs → src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommandData.cs View File

@@ -8,27 +8,27 @@ using Model = Discord.API.ApplicationCommandInteractionData;

namespace Discord.WebSocket
{
public class SocketInteractionData : SocketEntity<ulong>, IApplicationCommandInteractionData
public class SocketSlashCommandData : SocketEntity<ulong>, IApplicationCommandInteractionData
{
/// <inheritdoc/>
public string Name { get; private set; }

/// <summary>
/// The <see cref="SocketInteractionDataOption"/>'s recieved with this interaction.
/// The <see cref="SocketSlashCommandDataOption"/>'s recieved with this interaction.
/// </summary>
public IReadOnlyCollection<SocketInteractionDataOption> Options { get; private set; }
public IReadOnlyCollection<SocketSlashCommandDataOption> Options { get; private set; }

private ulong guildId;

internal SocketInteractionData(DiscordSocketClient client, ulong id)
internal SocketSlashCommandData(DiscordSocketClient client, ulong id)
: base(client, id)
{

}

internal static SocketInteractionData Create(DiscordSocketClient client, Model model, ulong guildId)
internal static SocketSlashCommandData Create(DiscordSocketClient client, Model model, ulong guildId)
{
var entity = new SocketInteractionData(client, model.Id);
var entity = new SocketSlashCommandData(client, model.Id);
entity.Update(model, guildId);
return entity;
}
@@ -38,7 +38,7 @@ namespace Discord.WebSocket
this.guildId = guildId;

this.Options = model.Options.IsSpecified
? model.Options.Value.Select(x => new SocketInteractionDataOption(x, this.Discord, guildId)).ToImmutableArray()
? model.Options.Value.Select(x => new SocketSlashCommandDataOption(x, this.Discord, guildId)).ToImmutableArray()
: null;
}


src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionDataOption.cs → src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommandDataOption.cs View File

@@ -11,7 +11,7 @@ namespace Discord.WebSocket
/// <summary>
/// Represents a Websocket-based <see cref="IApplicationCommandInteractionDataOption"/> recieved by the gateway
/// </summary>
public class SocketInteractionDataOption : IApplicationCommandInteractionDataOption
public class SocketSlashCommandDataOption : IApplicationCommandInteractionDataOption
{
/// <inheritdoc/>
public string Name { get; private set; }
@@ -22,13 +22,13 @@ namespace Discord.WebSocket
/// <summary>
/// The sub command options recieved for this sub command group.
/// </summary>
public IReadOnlyCollection<SocketInteractionDataOption> Options { get; private set; }
public IReadOnlyCollection<SocketSlashCommandDataOption> Options { get; private set; }

private DiscordSocketClient discord;
private ulong guild;

internal SocketInteractionDataOption() { }
internal SocketInteractionDataOption(Model model, DiscordSocketClient discord, ulong guild)
internal SocketSlashCommandDataOption() { }
internal SocketSlashCommandDataOption(Model model, DiscordSocketClient discord, ulong guild)
{
this.Name = model.Name;
this.Value = model.Value.IsSpecified ? model.Value.Value : null;
@@ -36,19 +36,19 @@ namespace Discord.WebSocket
this.guild = guild;

this.Options = model.Options.IsSpecified
? model.Options.Value.Select(x => new SocketInteractionDataOption(x, discord, guild)).ToImmutableArray()
? model.Options.Value.Select(x => new SocketSlashCommandDataOption(x, discord, guild)).ToImmutableArray()
: null;
}

// Converters
public static explicit operator bool(SocketInteractionDataOption option)
public static explicit operator bool(SocketSlashCommandDataOption option)
=> (bool)option.Value;
public static explicit operator int(SocketInteractionDataOption option)
public static explicit operator int(SocketSlashCommandDataOption option)
=> (int)option.Value;
public static explicit operator string(SocketInteractionDataOption option)
public static explicit operator string(SocketSlashCommandDataOption option)
=> option.Value.ToString();

public static explicit operator SocketGuildChannel(SocketInteractionDataOption option)
public static explicit operator SocketGuildChannel(SocketSlashCommandDataOption option)
{
if (option.Value is ulong id)
{
@@ -63,7 +63,7 @@ namespace Discord.WebSocket
return null;
}

public static explicit operator SocketRole(SocketInteractionDataOption option)
public static explicit operator SocketRole(SocketSlashCommandDataOption option)
{
if (option.Value is ulong id)
{
@@ -78,7 +78,7 @@ namespace Discord.WebSocket
return null;
}

public static explicit operator SocketGuildUser(SocketInteractionDataOption option)
public static explicit operator SocketGuildUser(SocketSlashCommandDataOption option)
{
if(option.Value is ulong id)
{

+ 31
- 94
src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs View File

@@ -11,7 +11,7 @@ namespace Discord.WebSocket
/// <summary>
/// Represents an Interaction recieved over the gateway.
/// </summary>
public class SocketInteraction : SocketEntity<ulong>, IDiscordInteraction
public abstract class SocketInteraction : SocketEntity<ulong>, IDiscordInteraction
{
/// <summary>
/// The <see cref="SocketGuild"/> this interaction was used in.
@@ -36,14 +36,14 @@ namespace Discord.WebSocket
public InteractionType Type { get; private set; }

/// <summary>
/// The data associated with this interaction.
/// The token used to respond to this interaction.
/// </summary>
public SocketInteractionData Data { get; private set; }
public string Token { get; private set; }

/// <summary>
/// The token used to respond to this interaction.
/// The data sent with this interaction.
/// </summary>
public string Token { get; private set; }
public object Data { get; private set; }

/// <summary>
/// The version of this interaction.
@@ -69,15 +69,18 @@ namespace Discord.WebSocket

internal static SocketInteraction Create(DiscordSocketClient client, Model model)
{
var entitiy = new SocketInteraction(client, model.Id);
entitiy.Update(model);
return entitiy;
if (model.Type == InteractionType.ApplicationCommand)
return SocketSlashCommand.Create(client, model);
if (model.Type == InteractionType.MessageComponent)
return SocketMessageComponent.Create(client, model);
else
return null;
}

internal void Update(Model model)
internal virtual void Update(Model model)
{
this.Data = model.Data.IsSpecified
? SocketInteractionData.Create(this.Discord, model.Data.Value, model.GuildId)
? model.Data.Value
: null;

this.GuildId = model.GuildId;
@@ -90,14 +93,9 @@ namespace Discord.WebSocket
if (this.User == null)
this.User = SocketGuildUser.Create(this.Guild, Discord.State, model.Member); // Change from getter.
}
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.
/// Responds to an Interaction.
/// <para>
/// 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.
@@ -110,63 +108,16 @@ namespace Discord.WebSocket
/// <param name="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</param>
/// <param name="allowedMentions">The allowed mentions for this response.</param>
/// <param name="options">The request options for this response.</param>
/// <param name="component">A <see cref="MessageComponent"/> to be sent with 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>
/// <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,
bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null)
{
if (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(text, isTTS, embed, ephemeral, type, allowedMentions, options); // The arguments should be passed? What was i thinking...

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

if (ephemeral)
response.Data.Value.Flags = 64;

await Discord.Rest.ApiClient.CreateInteractionResponse(response, this.Id, Token, options);
return null;
}
public virtual Task<RestUserMessage> RespondAsync(string text = null, bool isTTS = false, Embed embed = null, InteractionResponseType type = InteractionResponseType.ChannelMessageWithSource,
bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null)
{ return null; }

/// <summary>
/// Sends a followup message for this interaction.
@@ -174,36 +125,18 @@ namespace Discord.WebSocket
/// <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="type">The type of response to this Interaction.</param>
/// /// <param name="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</param>
/// <param name="allowedMentions">The allowed mentions for this response.</param>
/// <param name="options">The request options for this response.</param>
/// <param name="component">A <see cref="MessageComponent"/> to be sent with this response</param>
/// <returns>
/// The sent message.
/// </returns>
public async Task<IMessage> FollowupAsync(string text = null, bool isTTS = false, Embed embed = null, bool ephemeral = false,
InteractionResponseType Type = InteractionResponseType.ChannelMessageWithSource,
AllowedMentions allowedMentions = null, RequestOptions options = null)
{
if (Type == InteractionResponseType.DeferredChannelMessageWithSource || Type == InteractionResponseType.DeferredChannelMessageWithSource || 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,
};

if (ephemeral)
args.Flags = 64;

return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options);
}
public virtual Task<RestFollowupMessage> FollowupAsync(string text = null, bool isTTS = false, Embed embed = null, bool ephemeral = false,
InteractionResponseType type = InteractionResponseType.ChannelMessageWithSource,
AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null)
{ return null; }

/// <summary>
/// Acknowledges this interaction with the <see cref="InteractionResponseType.DeferredChannelMessageWithSource"/>.
@@ -211,16 +144,20 @@ namespace Discord.WebSocket
/// <returns>
/// A task that represents the asynchronous operation of acknowledging the interaction.
/// </returns>
public async Task AcknowledgeAsync(RequestOptions options = null)
public virtual Task AcknowledgeAsync(RequestOptions options = null)
{
var response = new API.InteractionResponse()
{
Type = InteractionResponseType.DeferredChannelMessageWithSource,
};

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

IApplicationCommandInteractionData IDiscordInteraction.Data => Data;
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;
}
}
}

Loading…
Cancel
Save