Browse Source

Implement checks for interaction respond times and multiple interaction responses. closes #236, #235

pull/1923/head
quin lynch 3 years ago
parent
commit
4c9b396fcc
4 changed files with 131 additions and 19 deletions
  1. +12
    -0
      src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs
  2. +76
    -9
      src/Discord.Net.WebSocket/Entities/Interaction/Message Components/SocketMessageComponent.cs
  3. +39
    -3
      src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs
  4. +4
    -7
      src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs

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

@@ -11,7 +11,19 @@ namespace Discord.Rest
{ {
internal static class InteractionHelper internal static class InteractionHelper
{ {
public const double ResponseTimeLimit = 3;
public const double ResponseAndFollowupLimit = 15;

#region InteractionHelper #region InteractionHelper
public static bool CanSendResponse(IDiscordInteraction interaction)
{
return (DateTime.UtcNow - interaction.CreatedAt).TotalSeconds < ResponseTimeLimit;
}
public static bool CanRespondOrFollowup(IDiscordInteraction interaction)
{
return (DateTime.UtcNow - interaction.CreatedAt).TotalMinutes <= ResponseAndFollowupLimit;
}

public static Task DeleteAllGuildCommandsAsync(BaseDiscordClient client, ulong guildId, RequestOptions options = null) public static Task DeleteAllGuildCommandsAsync(BaseDiscordClient client, ulong guildId, RequestOptions options = null)
{ {
return client.ApiClient.BulkOverwriteGuildApplicationCommandsAsync(guildId, Array.Empty<CreateApplicationCommandParams>(), options); return client.ApiClient.BulkOverwriteGuildApplicationCommandsAsync(guildId, Array.Empty<CreateApplicationCommandParams>(), options);


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

@@ -25,6 +25,9 @@ namespace Discord.WebSocket
/// </summary> /// </summary>
public SocketUserMessage Message { get; private set; } public SocketUserMessage Message { get; private set; }


private object _lock = new object();
internal override bool _hasResponded { get; set; } = false;

internal SocketMessageComponent(DiscordSocketClient client, Model model, ISocketMessageChannel channel) internal SocketMessageComponent(DiscordSocketClient client, Model model, ISocketMessageChannel channel)
: base(client, model.Id, channel) : base(client, model.Id, channel)
{ {
@@ -82,6 +85,9 @@ namespace Discord.WebSocket
if (!IsValidToken) if (!IsValidToken)
throw new InvalidOperationException("Interaction token is no longer valid"); throw new InvalidOperationException("Interaction token is no longer valid");


if (!InteractionHelper.CanSendResponse(this))
throw new TimeoutException($"Cannot respond to an interaction after {InteractionHelper.ResponseTimeLimit} seconds!");

embeds ??= Array.Empty<Embed>(); embeds ??= Array.Empty<Embed>();
if (embed != null) if (embed != null)
embeds = new[] { embed }.Concat(embeds).ToArray(); embeds = new[] { embed }.Concat(embeds).ToArray();
@@ -122,7 +128,20 @@ namespace Discord.WebSocket
if (ephemeral) if (ephemeral)
response.Data.Value.Flags = MessageFlags.Ephemeral; response.Data.Value.Flags = MessageFlags.Ephemeral;


await InteractionHelper.SendInteractionResponseAsync(Discord, response, Id, Token, options);
lock (_lock)
{
if (_hasResponded)
{
throw new InvalidOperationException("Cannot respond, update, or defer twice to the same interaction");
}
}

await InteractionHelper.SendInteractionResponseAsync(Discord, response, Id, Token, options).ConfigureAwait(false);

lock (_lock)
{
_hasResponded = true;
}
} }


/// <summary> /// <summary>
@@ -139,6 +158,9 @@ namespace Discord.WebSocket
if (!IsValidToken) if (!IsValidToken)
throw new InvalidOperationException("Interaction token is no longer valid"); throw new InvalidOperationException("Interaction token is no longer valid");


if (!InteractionHelper.CanSendResponse(this))
throw new TimeoutException($"Cannot respond to an interaction after {InteractionHelper.ResponseTimeLimit} seconds!");

if (args.AllowedMentions.IsSpecified) if (args.AllowedMentions.IsSpecified)
{ {
var allowedMentions = args.AllowedMentions.Value; var allowedMentions = args.AllowedMentions.Value;
@@ -201,7 +223,20 @@ namespace Discord.WebSocket
} }
}; };


await InteractionHelper.SendInteractionResponseAsync(Discord, response, Id, Token, options);
lock (_lock)
{
if (_hasResponded)
{
throw new InvalidOperationException("Cannot respond, update, or defer twice to the same interaction");
}
}

await InteractionHelper.SendInteractionResponseAsync(Discord, response, Id, Token, options).ConfigureAwait(false);

lock (_lock)
{
_hasResponded = true;
}
} }


/// <inheritdoc/> /// <inheritdoc/>
@@ -238,7 +273,7 @@ namespace Discord.WebSocket
if (ephemeral) if (ephemeral)
args.Flags = MessageFlags.Ephemeral; args.Flags = MessageFlags.Ephemeral;


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


/// <inheritdoc/> /// <inheritdoc/>
@@ -280,7 +315,7 @@ namespace Discord.WebSocket
if (ephemeral) if (ephemeral)
args.Flags = MessageFlags.Ephemeral; args.Flags = MessageFlags.Ephemeral;


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


/// <inheritdoc/> /// <inheritdoc/>
@@ -321,7 +356,7 @@ namespace Discord.WebSocket
if (ephemeral) if (ephemeral)
args.Flags = MessageFlags.Ephemeral; args.Flags = MessageFlags.Ephemeral;


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


/// <summary> /// <summary>
@@ -332,27 +367,59 @@ namespace Discord.WebSocket
/// <returns> /// <returns>
/// A task that represents the asynchronous operation of acknowledging the interaction. /// A task that represents the asynchronous operation of acknowledging the interaction.
/// </returns> /// </returns>
public Task DeferLoadingAsync(bool ephemeral = false, RequestOptions options = null)
public async Task DeferLoadingAsync(bool ephemeral = false, RequestOptions options = null)
{ {
if (!InteractionHelper.CanSendResponse(this))
throw new TimeoutException($"Cannot defer an interaction after {InteractionHelper.ResponseTimeLimit} seconds of no response/acknowledgement");

var response = new API.InteractionResponse var response = new API.InteractionResponse
{ {
Type = InteractionResponseType.DeferredChannelMessageWithSource, Type = InteractionResponseType.DeferredChannelMessageWithSource,
Data = ephemeral ? new API.InteractionCallbackData { Flags = MessageFlags.Ephemeral } : Optional<API.InteractionCallbackData>.Unspecified Data = ephemeral ? new API.InteractionCallbackData { Flags = MessageFlags.Ephemeral } : Optional<API.InteractionCallbackData>.Unspecified
}; };


return Discord.Rest.ApiClient.CreateInteractionResponseAsync(response, Id, Token, options);
lock (_lock)
{
if (_hasResponded)
{
throw new InvalidOperationException("Cannot respond or defer twice to the same interaction");
}
}

await Discord.Rest.ApiClient.CreateInteractionResponseAsync(response, Id, Token, options).ConfigureAwait(false);

lock (_lock)
{
_hasResponded = true;
}
} }


/// <inheritdoc/> /// <inheritdoc/>
public override Task DeferAsync(bool ephemeral = false, RequestOptions options = null)
public override async Task DeferAsync(bool ephemeral = false, RequestOptions options = null)
{ {
if (!InteractionHelper.CanSendResponse(this))
throw new TimeoutException($"Cannot defer an interaction after {InteractionHelper.ResponseTimeLimit} seconds of no response/acknowledgement");

var response = new API.InteractionResponse var response = new API.InteractionResponse
{ {
Type = InteractionResponseType.DeferredUpdateMessage, Type = InteractionResponseType.DeferredUpdateMessage,
Data = ephemeral ? new API.InteractionCallbackData { Flags = MessageFlags.Ephemeral } : Optional<API.InteractionCallbackData>.Unspecified Data = ephemeral ? new API.InteractionCallbackData { Flags = MessageFlags.Ephemeral } : Optional<API.InteractionCallbackData>.Unspecified
}; };


return Discord.Rest.ApiClient.CreateInteractionResponseAsync(response, Id, Token, options);
lock (_lock)
{
if (_hasResponded)
{
throw new InvalidOperationException("Cannot respond or defer twice to the same interaction");
}
}

await Discord.Rest.ApiClient.CreateInteractionResponseAsync(response, Id, Token, options).ConfigureAwait(false);

lock (_lock)
{
_hasResponded = true;
}
} }
} }
} }

+ 39
- 3
src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs View File

@@ -31,6 +31,10 @@ namespace Discord.WebSocket
/// </summary> /// </summary>
internal new SocketCommandBaseData Data { get; } internal new SocketCommandBaseData Data { get; }


internal override bool _hasResponded { get; set; }

private object _lock = new object();

internal SocketCommandBase(DiscordSocketClient client, Model model, ISocketMessageChannel channel) internal SocketCommandBase(DiscordSocketClient client, Model model, ISocketMessageChannel channel)
: base(client, model.Id, channel) : base(client, model.Id, channel)
{ {
@@ -77,6 +81,9 @@ namespace Discord.WebSocket
if (!IsValidToken) if (!IsValidToken)
throw new InvalidOperationException("Interaction token is no longer valid"); throw new InvalidOperationException("Interaction token is no longer valid");


if (!InteractionHelper.CanSendResponse(this))
throw new TimeoutException($"Cannot respond to an interaction after {InteractionHelper.ResponseTimeLimit} seconds!");

embeds ??= Array.Empty<Embed>(); embeds ??= Array.Empty<Embed>();
if (embed != null) if (embed != null)
embeds = new[] { embed }.Concat(embeds).ToArray(); embeds = new[] { embed }.Concat(embeds).ToArray();
@@ -115,7 +122,20 @@ namespace Discord.WebSocket
} }
}; };


await InteractionHelper.SendInteractionResponseAsync(Discord, response, Id, Token, options);
lock (_lock)
{
if (_hasResponded)
{
throw new InvalidOperationException("Cannot respond twice to the same interaction");
}
}

await InteractionHelper.SendInteractionResponseAsync(Discord, response, Id, Token, options).ConfigureAwait(false);

lock (_lock)
{
_hasResponded = true;
}
} }


/// <inheritdoc/> /// <inheritdoc/>
@@ -247,8 +267,11 @@ namespace Discord.WebSocket
/// <returns> /// <returns>
/// A task that represents the asynchronous operation of acknowledging the interaction. /// A task that represents the asynchronous operation of acknowledging the interaction.
/// </returns> /// </returns>
public override Task DeferAsync(bool ephemeral = false, RequestOptions options = null)
public override async Task DeferAsync(bool ephemeral = false, RequestOptions options = null)
{ {
if (!InteractionHelper.CanSendResponse(this))
throw new TimeoutException($"Cannot defer an interaction after {InteractionHelper.ResponseTimeLimit} seconds!");

var response = new API.InteractionResponse var response = new API.InteractionResponse
{ {
Type = InteractionResponseType.DeferredChannelMessageWithSource, Type = InteractionResponseType.DeferredChannelMessageWithSource,
@@ -258,7 +281,20 @@ namespace Discord.WebSocket
} }
}; };


return Discord.Rest.ApiClient.CreateInteractionResponseAsync(response, Id, Token, options);
lock (_lock)
{
if (_hasResponded)
{
throw new InvalidOperationException("Cannot respond or defer twice to the same interaction");
}
}

await Discord.Rest.ApiClient.CreateInteractionResponseAsync(response, Id, Token, options).ConfigureAwait(false);

lock (_lock)
{
_hasResponded = true;
}
} }
} }
} }

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

@@ -47,11 +47,13 @@ namespace Discord.WebSocket
public DateTimeOffset CreatedAt public DateTimeOffset CreatedAt
=> SnowflakeUtils.FromSnowflake(Id); => SnowflakeUtils.FromSnowflake(Id);


internal abstract bool _hasResponded { get; set; }

/// <summary> /// <summary>
/// <see langword="true"/> if the token is valid for replying to, otherwise <see langword="false"/>. /// <see langword="true"/> if the token is valid for replying to, otherwise <see langword="false"/>.
/// </summary> /// </summary>
public bool IsValidToken public bool IsValidToken
=> CheckToken();
=> InteractionHelper.CanRespondOrFollowup(this);


internal SocketInteraction(DiscordSocketClient client, ulong id, ISocketMessageChannel channel) internal SocketInteraction(DiscordSocketClient client, ulong id, ISocketMessageChannel channel)
: base(client, id) : base(client, id)
@@ -210,12 +212,7 @@ namespace Discord.WebSocket
/// A task that represents the asynchronous operation of acknowledging the interaction. /// A task that represents the asynchronous operation of acknowledging the interaction.
/// </returns> /// </returns>
public abstract Task DeferAsync(bool ephemeral = false, RequestOptions options = null); public abstract Task DeferAsync(bool ephemeral = false, RequestOptions options = null);

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 - CreatedAt.UtcDateTime).TotalMinutes <= 15d;
}
#endregion #endregion


#region IDiscordInteraction #region IDiscordInteraction


Loading…
Cancel
Save