diff --git a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs index f1b1e67bd..44ee7ac22 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs @@ -11,7 +11,19 @@ namespace Discord.Rest { internal static class InteractionHelper { + public const double ResponseTimeLimit = 3; + public const double ResponseAndFollowupLimit = 15; + #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) { return client.ApiClient.BulkOverwriteGuildApplicationCommandsAsync(guildId, Array.Empty(), options); diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Message Components/SocketMessageComponent.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Message Components/SocketMessageComponent.cs index 624861182..926563c4c 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/Message Components/SocketMessageComponent.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/Message Components/SocketMessageComponent.cs @@ -25,6 +25,9 @@ namespace Discord.WebSocket /// 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) : base(client, model.Id, channel) { @@ -82,6 +85,9 @@ namespace Discord.WebSocket if (!IsValidToken) 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(); if (embed != null) embeds = new[] { embed }.Concat(embeds).ToArray(); @@ -122,7 +128,20 @@ namespace Discord.WebSocket if (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; + } } /// @@ -139,6 +158,9 @@ namespace Discord.WebSocket if (!IsValidToken) 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) { 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; + } } /// @@ -238,7 +273,7 @@ namespace Discord.WebSocket if (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); } /// @@ -280,7 +315,7 @@ namespace Discord.WebSocket if (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); } /// @@ -321,7 +356,7 @@ namespace Discord.WebSocket if (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); } /// @@ -332,27 +367,59 @@ namespace Discord.WebSocket /// /// A task that represents the asynchronous operation of acknowledging the interaction. /// - 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 { Type = InteractionResponseType.DeferredChannelMessageWithSource, Data = ephemeral ? new API.InteractionCallbackData { Flags = MessageFlags.Ephemeral } : Optional.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; + } } /// - 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 { Type = InteractionResponseType.DeferredUpdateMessage, Data = ephemeral ? new API.InteractionCallbackData { Flags = MessageFlags.Ephemeral } : Optional.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; + } } } } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs index 586b785f5..66d10fd3b 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs @@ -31,6 +31,10 @@ namespace Discord.WebSocket /// internal new SocketCommandBaseData Data { get; } + internal override bool _hasResponded { get; set; } + + private object _lock = new object(); + internal SocketCommandBase(DiscordSocketClient client, Model model, ISocketMessageChannel channel) : base(client, model.Id, channel) { @@ -77,6 +81,9 @@ namespace Discord.WebSocket if (!IsValidToken) 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(); if (embed != null) 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; + } } /// @@ -247,8 +267,11 @@ namespace Discord.WebSocket /// /// A task that represents the asynchronous operation of acknowledging the interaction. /// - 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 { 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; + } } } } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs index 2863e772d..20d8fa0f5 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs @@ -47,11 +47,13 @@ namespace Discord.WebSocket public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + internal abstract bool _hasResponded { get; set; } + /// /// if the token is valid for replying to, otherwise . /// public bool IsValidToken - => CheckToken(); + => InteractionHelper.CanRespondOrFollowup(this); internal SocketInteraction(DiscordSocketClient client, ulong id, ISocketMessageChannel channel) : base(client, id) @@ -210,12 +212,7 @@ namespace Discord.WebSocket /// A task that represents the asynchronous operation of acknowledging the interaction. /// 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 #region IDiscordInteraction