From 6c7502da685a95d5c855665b99af0f6de4d0eec1 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Sun, 19 Dec 2021 03:41:58 -0400 Subject: [PATCH] Update to Labs 3.5.0 (#1971) * Merge https://github.com/Discord-Net-Labs/Discord.Net-Labs into patch/labs3.5.0 * Add missing periods --- docs/faq/basics/basic-operations.md | 24 +-- docs/guides/getting_started/installing.md | 4 +- docs/guides/v2_v3_guide/v2_to_v3_guide.md | 21 ++- docs/index.md | 8 +- src/Discord.Net.Commands/CommandService.cs | 4 +- .../Discord.Net.Commands.csproj | 5 +- src/Discord.Net.Core/Discord.Net.Core.csproj | 17 +- src/Discord.Net.Core/DiscordErrorCode.cs | 2 +- src/Discord.Net.Core/Entities/IApplication.cs | 13 +- .../Interactions/IDiscordInteraction.cs | 155 ++++++++++++++++-- .../Entities/Messages/FileAttachment.cs | 8 +- .../Interactions/IRestInteractionContext.cs | 19 +++ .../RequireBotPermissionAttribute.cs | 84 ++++++++++ .../Preconditions/RequireContextAttribute.cs | 72 ++++++++ .../Preconditions/RequireNsfwAttribute.cs | 42 +++++ .../Preconditions/RequireOwnerAttribute.cs | 36 ++++ .../Preconditions/RequireRoleAttribute.cs | 71 ++++++++ .../RequireUserPermissionAttribute.cs | 81 +++++++++ .../AutocompleteHandler.cs | 6 +- .../Discord.Net.Interactions.csproj | 1 - .../RestInteractionModuleBase.cs | 14 +- .../API/Common/Application.cs | 7 +- .../API/Rest/UploadInteractionFileParams.cs | 99 +++++++++++ src/Discord.Net.Rest/Discord.Net.Rest.csproj | 2 +- src/Discord.Net.Rest/DiscordRestApiClient.cs | 15 +- .../Entities/Channels/ChannelHelper.cs | 3 +- .../Interactions/InteractionHelper.cs | 12 +- .../Entities/Interactions/RestInteraction.cs | 21 ++- .../Messages/RestInteractionMessage.cs | 14 -- .../Entities/RestApplication.cs | 8 +- .../Interactions/RestInteractionContext.cs | 42 ++++- src/Discord.Net.Rest/Net/DefaultRestClient.cs | 3 +- .../Net/Queue/RequestQueueBucket.cs | 1 + .../BaseSocketClient.Events.cs | 8 +- .../DiscordShardedClient.cs | 2 +- .../DiscordSocketClient.cs | 12 +- .../SocketMessageComponent.cs | 155 ++++++++---------- .../SocketAutocompleteInteraction.cs | 10 +- .../SocketBaseCommand/SocketCommandBase.cs | 117 +++++++------ .../Entities/Interaction/SocketInteraction.cs | 129 +++++++++++++-- .../Discord.Net.Webhook.csproj | 2 +- 41 files changed, 1087 insertions(+), 262 deletions(-) create mode 100644 src/Discord.Net.Core/Interactions/IRestInteractionContext.cs create mode 100644 src/Discord.Net.Interactions/Attributes/Preconditions/RequireBotPermissionAttribute.cs create mode 100644 src/Discord.Net.Interactions/Attributes/Preconditions/RequireContextAttribute.cs create mode 100644 src/Discord.Net.Interactions/Attributes/Preconditions/RequireNsfwAttribute.cs create mode 100644 src/Discord.Net.Interactions/Attributes/Preconditions/RequireOwnerAttribute.cs create mode 100644 src/Discord.Net.Interactions/Attributes/Preconditions/RequireRoleAttribute.cs create mode 100644 src/Discord.Net.Interactions/Attributes/Preconditions/RequireUserPermissionAttribute.cs create mode 100644 src/Discord.Net.Rest/API/Rest/UploadInteractionFileParams.cs diff --git a/docs/faq/basics/basic-operations.md b/docs/faq/basics/basic-operations.md index ee2edef94..d6121dbb0 100644 --- a/docs/faq/basics/basic-operations.md +++ b/docs/faq/basics/basic-operations.md @@ -109,15 +109,15 @@ reactions. Unfortunately, not at the moment. See [#401](https://github.com/discord-net/Discord.Net/issues/401). -[ichannel]: xref:Discord.IChannel -[icategorychannel]: xref:Discord.ICategoryChannel -[iguildchannel]: xref:Discord.IGuildChannel -[itextchannel]: xref:Discord.ITextChannel -[iguild]: xref:Discord.IGuild -[ivoicechannel]: xref:Discord.IVoiceChannel -[iguilduser]: xref:Discord.IGuildUser -[imessagechannel]: xref:Discord.IMessageChannel -[iusermessage]: xref:Discord.IUserMessage -[iemote]: xref:Discord.IEmote -[emote]: xref:Discord.Emote -[emoji]: xref:Discord.Emoji +[IChannel]: xref:Discord.IChannel +[ICategoryChannel]: xref:Discord.ICategoryChannel +[IGuildChannel]: xref:Discord.IGuildChannel +[ITextChannel]: xref:Discord.ITextChannel +[IGuild]: xref:Discord.IGuild +[IVoiceChannel]: xref:Discord.IVoiceChannel +[IGuildUser]: xref:Discord.IGuildUser +[IMessageChannel]: xref:Discord.IMessageChannel +[IUserMessage]: xref:Discord.IUserMessage +[IEmote]: xref:Discord.IEmote +[Emote]: xref:Discord.Emote +[Emoji]: xref:Discord.Emoji diff --git a/docs/guides/getting_started/installing.md b/docs/guides/getting_started/installing.md index 09dc32837..0dde72380 100644 --- a/docs/guides/getting_started/installing.md +++ b/docs/guides/getting_started/installing.md @@ -100,7 +100,7 @@ installation. ### Using Command Line -- [.NET 5 SDK] +* [.NET 5 SDK] ## Additional Information @@ -143,4 +143,4 @@ by installing one or more custom packages as listed below. --- -[.net 5 sdk]: https://dotnet.microsoft.com/download +[.NET 5 SDK]: https://dotnet.microsoft.com/download diff --git a/docs/guides/v2_v3_guide/v2_to_v3_guide.md b/docs/guides/v2_v3_guide/v2_to_v3_guide.md index 2bd914e17..2a47b42bb 100644 --- a/docs/guides/v2_v3_guide/v2_to_v3_guide.md +++ b/docs/guides/v2_v3_guide/v2_to_v3_guide.md @@ -5,26 +5,29 @@ title: V2 -> V3 Guide # V2 to V3 Guide -V3 is designed to be a more feature complete, more reliable, and more flexible library than any previous version. +V3 is designed to be a more feature complete, more reliable, +and more flexible library than any previous version. Below are the most notable breaking changes that you would need to update your code to work with V3. -### GuildMemberUpdated Event - -The guild member updated event now passes a `Cacheable` for the first argument instead of a normal `SocketGuildUser`. This new cacheable type allows you to download a `RestGuildUser` if the user isn't cached. - ### ReactionAdded Event -The reaction added event has been changed to have both parameters cacheable. This allows you to download the channel and message if they aren't cached instead of them being null. +The reaction added event has been changed to have both parameters cacheable. +This allows you to download the channel and message if they aren't cached instead of them being null. ### UserIsTyping Event -THe user is typing event has been changed to have both parameters cacheable. This allows you to download the user and channel if they aren't cached instead of them being null. +The user is typing event has been changed to have both parameters cacheable. +This allows you to download the user and channel if they aren't cached instead of them being null. ### Presence -There is a new event called `PresenceUpdated` that is called when a user's presence changes, instead of `GuildMemberUpdated` or `UserUpdated`. If your code relied on these events to get presence data then you need to update it to work with the new event. +There is a new event called `PresenceUpdated` that is called when a user's presence changes, +instead of `GuildMemberUpdated` or `UserUpdated`. +If your code relied on these events to get presence data then you need to update it to work with the new event. ## Migrating your commands to slash command -The new InteractionService was designed to act like the previous service for text-based commands. Your pre-existing code will continue to work, but you will need to migrate your modules and response functions to use the new InteractionService methods. Docs on this can be found [here](xref:Guides.IntFw.Intro) +The new InteractionService was designed to act like the previous service for text-based commands. +Your pre-existing code will continue to work, but you will need to migrate your modules and response functions to use the new +InteractionService methods. Docs on this can be found in the Guides section. diff --git a/docs/index.md b/docs/index.md index 8a87963ea..32b0498ea 100644 --- a/docs/index.md +++ b/docs/index.md @@ -37,14 +37,14 @@ for testing and development to eventually get merged into Discord.NET. [Installing Discord.NET Labs](xref:Guides.GettingStarted.Installation.Labs) -[discord.net labs]: https://github.com/Discord-Net-Labs/Discord.Net-Labs +[Discord.NET Labs]: https://github.com/Discord-Net-Labs/Discord.Net-Labs ## Questions? -Frequently asked questions are covered in the -FAQ. Read it thoroughly because most common questions are already answered there. +Frequently asked questions are covered in the +FAQ. Read it thoroughly because most common questions are already answered there. -If you still have unanswered questions after reading the [FAQ](xref:FAQ.Basics.GetStarted), further support is available on +If you still have unanswered questions after reading the [FAQ](xref:FAQ.Basics.GetStarted), further support is available on [Discord](https://discord.gg/dnet). ## Commonly used features diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index f9552ef4b..e2bfeeb44 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -643,12 +643,12 @@ namespace Discord.Commands var bestMatch = parseResults .FirstOrDefault(x => !x.Value.IsSuccess); - return MatchResult.FromSuccess(bestMatch.Key,bestMatch.Value); + return MatchResult.FromSuccess(bestMatch.Key, bestMatch.Value); } var chosenOverload = successfulParses[0]; - return MatchResult.FromSuccess(chosenOverload.Key,chosenOverload.Value); + return MatchResult.FromSuccess(chosenOverload.Key, chosenOverload.Value); } #endregion diff --git a/src/Discord.Net.Commands/Discord.Net.Commands.csproj b/src/Discord.Net.Commands/Discord.Net.Commands.csproj index 811a0470e..fea719016 100644 --- a/src/Discord.Net.Commands/Discord.Net.Commands.csproj +++ b/src/Discord.Net.Commands/Discord.Net.Commands.csproj @@ -1,6 +1,6 @@ - + Discord.Net.Commands Discord.Commands @@ -11,5 +11,4 @@ - - \ No newline at end of file + diff --git a/src/Discord.Net.Core/Discord.Net.Core.csproj b/src/Discord.Net.Core/Discord.Net.Core.csproj index bfbff6f5c..783565e04 100644 --- a/src/Discord.Net.Core/Discord.Net.Core.csproj +++ b/src/Discord.Net.Core/Discord.Net.Core.csproj @@ -9,11 +9,20 @@ net6.0;net5.0;netstandard2.0;netstandard2.1 - - - - + + + all + + + + + + + + + + \ No newline at end of file diff --git a/src/Discord.Net.Core/DiscordErrorCode.cs b/src/Discord.Net.Core/DiscordErrorCode.cs index 61516a0ff..03f8b19e9 100644 --- a/src/Discord.Net.Core/DiscordErrorCode.cs +++ b/src/Discord.Net.Core/DiscordErrorCode.cs @@ -150,7 +150,7 @@ namespace Discord ServerLocaleUnavailable = 50095, ServerRequiresMonetization = 50097, ServerRequiresBoosts = 50101, - + RequestBodyContainsInvalidJSON = 50109, #endregion #region 2FA (60XXX) diff --git a/src/Discord.Net.Core/Entities/IApplication.cs b/src/Discord.Net.Core/Entities/IApplication.cs index 9f9881340..d25e82c4b 100644 --- a/src/Discord.Net.Core/Entities/IApplication.cs +++ b/src/Discord.Net.Core/Entities/IApplication.cs @@ -19,6 +19,9 @@ namespace Discord /// Gets the RPC origins of the application. /// IReadOnlyCollection RPCOrigins { get; } + /// + /// Gets the application's public flags. + /// ApplicationFlags Flags { get; } /// /// Gets a collection of install parameters for this application. @@ -44,10 +47,18 @@ namespace Discord /// Gets the team associated with this application if there is one. /// ITeam Team { get; } - /// /// Gets the partial user object containing info on the owner of the application. /// IUser Owner { get; } + /// + /// Gets the url of the app's terms of service. + /// + public string TermsOfService { get; } + /// + /// Gets the the url of the app's privacy policy. + /// + public string PrivacyPolicy { get; } + } } diff --git a/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs index 77971b9f3..9988e238c 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs @@ -48,17 +48,119 @@ namespace Discord /// if the message should be read out by a text-to-speech reader, otherwise . /// if the response should be hidden to everyone besides the invoker of the command, otherwise . /// The allowed mentions for this response. - /// The request options for this response. /// A to be sent with this response. /// A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored. + /// The request options for this response. /// /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. /// - Task RespondAsync(string text = null, Embed[] embeds = null, bool isTTS = false, + Task RespondAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null); /// + /// Responds to this interaction with a file attachment. + /// + /// The file to upload. + /// The file name of the attachment. + /// The text of the message to be sent. + /// A array of embeds to send with this response. Max 10. + /// if the message should be read out by a text-to-speech reader, otherwise . + /// if the response should be hidden to everyone besides the invoker of the command, otherwise . + /// The allowed mentions for this response. + /// A to be sent with this response. + /// A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored. + /// The request options for this response. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// +#if NETCOREAPP3_0_OR_GREATER + async Task RespondWithFileAsync(Stream fileStream, string fileName, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null) + { + using(var file = new FileAttachment(fileStream, fileName)) + { + await RespondWithFileAsync(file, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false); + } + } +#else + Task RespondWithFileAsync(Stream fileStream, string fileName, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null); +#endif + /// + /// Responds to this interaction with a file attachment. + /// + /// The file to upload. + /// The file name of the attachment. + /// The text of the message to be sent. + /// A array of embeds to send with this response. Max 10. + /// if the message should be read out by a text-to-speech reader, otherwise . + /// if the response should be hidden to everyone besides the invoker of the command, otherwise . + /// The allowed mentions for this response. + /// The request options for this response. + /// A to be sent with this response. + /// A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// +#if NETCOREAPP3_0_OR_GREATER + async Task RespondWithFileAsync(string filePath, string fileName = null, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null) + { + using (var file = new FileAttachment(filePath, fileName)) + { + await RespondWithFileAsync(file, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false); + } + } +#else + Task RespondWithFileAsync(string filePath, string fileName = null, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null); +#endif + /// + /// Responds to this interaction with a file attachment. + /// + /// The attachment containing the file and description. + /// The text of the message to be sent. + /// A array of embeds to send with this response. Max 10. + /// if the message should be read out by a text-to-speech reader, otherwise . + /// if the response should be hidden to everyone besides the invoker of the command, otherwise . + /// The allowed mentions for this response. + /// The request options for this response. + /// A to be sent with this response. + /// A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// +#if NETCOREAPP3_0_OR_GREATER + Task RespondWithFileAsync(FileAttachment attachment, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null) + => RespondWithFilesAsync(new FileAttachment[] { attachment }, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options); +#else + Task RespondWithFileAsync(FileAttachment attachment, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null); +#endif + /// + /// Responds to this interaction with a collection of file attachments. + /// + /// A collection of attachments to upload. + /// The text of the message to be sent. + /// A array of embeds to send with this response. Max 10. + /// if the message should be read out by a text-to-speech reader, otherwise . + /// if the response should be hidden to everyone besides the invoker of the command, otherwise . + /// The allowed mentions for this response. + /// The request options for this response. + /// A to be sent with this response. + /// A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + Task RespondWithFilesAsync(IEnumerable attachments, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null); + /// /// Sends a followup message for this interaction. /// /// The text of the message to be sent. @@ -75,13 +177,12 @@ namespace Discord /// Task FollowupAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null); - /// /// Sends a followup message for this interaction. /// - /// The text of the message to be sent. /// The file to upload. /// The file name of the attachment. + /// The text of the message to be sent. /// A array of embeds to send with this response. Max 10. /// if the message should be read out by a text-to-speech reader, otherwise . /// if the response should be hidden to everyone besides the invoker of the command, otherwise . @@ -93,15 +194,25 @@ namespace Discord /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. /// - public Task FollowupWithFileAsync(Stream fileStream, string fileName, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, +#if NETCOREAPP3_0_OR_GREATER + async Task FollowupWithFileAsync(Stream fileStream, string fileName, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null) + { + using(var file = new FileAttachment(fileStream, fileName)) + { + return await FollowupWithFileAsync(file, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false); + } + } +#else + Task FollowupWithFileAsync(Stream fileStream, string fileName, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null); - +#endif /// /// Sends a followup message for this interaction. /// - /// The text of the message to be sent. /// The file to upload. /// The file name of the attachment. + /// The text of the message to be sent. /// A array of embeds to send with this response. Max 10. /// if the message should be read out by a text-to-speech reader, otherwise . /// if the response should be hidden to everyone besides the invoker of the command, otherwise . @@ -113,8 +224,19 @@ namespace Discord /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. /// - public Task FollowupWithFileAsync(string filePath, string fileName = null, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, +#if NETCOREAPP3_0_OR_GREATER + async Task FollowupWithFileAsync(string filePath, string fileName = null, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null) + { + using (var file = new FileAttachment(filePath, fileName)) + { + return await FollowupWithFileAsync(file, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false); + } + } +#else + Task FollowupWithFileAsync(string filePath, string fileName = null, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null); +#endif /// /// Sends a followup message for this interaction. /// @@ -131,8 +253,14 @@ namespace Discord /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. /// +#if NETCOREAPP3_0_OR_GREATER + Task FollowupWithFileAsync(FileAttachment attachment, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null) + => FollowupWithFilesAsync(new FileAttachment[] { attachment }, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options); +#else Task FollowupWithFileAsync(FileAttachment attachment, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null); +#endif /// /// Sends a followup message for this interaction. /// @@ -151,14 +279,12 @@ namespace Discord /// Task FollowupWithFilesAsync(IEnumerable attachments, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null); - /// /// Gets the original response for this interaction. /// /// The request options for this request. /// A that represents the initial response. Task GetOriginalResponseAsync(RequestOptions options = null); - /// /// Edits original response for this interaction. /// @@ -169,7 +295,14 @@ namespace Discord /// contains the updated message. /// Task ModifyOriginalResponseAsync(Action func, RequestOptions options = null); - + /// + /// Deletes the original response to this interaction. + /// + /// The request options for this request. + /// + /// A task that represents an asynchronous deletion operation. + /// + Task DeleteOriginalResponseAsync(RequestOptions options = null); /// /// Acknowledges this interaction. /// diff --git a/src/Discord.Net.Core/Entities/Messages/FileAttachment.cs b/src/Discord.Net.Core/Entities/Messages/FileAttachment.cs index dc5437861..582f5c2e7 100644 --- a/src/Discord.Net.Core/Entities/Messages/FileAttachment.cs +++ b/src/Discord.Net.Core/Entities/Messages/FileAttachment.cs @@ -25,6 +25,7 @@ namespace Discord /// The stream to create the attachment from. /// The name of the attachment. /// The description of the attachment. + /// Whether or not the attachment is a spoiler. public FileAttachment(Stream stream, string fileName, string description = null, bool isSpoiler = false) { _isDisposed = false; @@ -42,6 +43,9 @@ namespace Discord /// . /// /// The path to the file. + /// The name of the attachment. + /// The description of the attachment. + /// Whether or not the attachment is a spoiler. /// /// is a zero-length string, contains only white space, or contains one or more invalid /// characters as defined by . @@ -62,11 +66,11 @@ namespace Discord /// The file specified in was not found. /// /// An I/O error occurred while opening the file. - public FileAttachment(string path, string description = null, bool isSpoiler = false) + public FileAttachment(string path, string fileName = null, string description = null, bool isSpoiler = false) { _isDisposed = false; Stream = File.OpenRead(path); - FileName = Path.GetFileName(path); + FileName = fileName ?? Path.GetFileName(path); Description = description; IsSpoiler = isSpoiler; } diff --git a/src/Discord.Net.Core/Interactions/IRestInteractionContext.cs b/src/Discord.Net.Core/Interactions/IRestInteractionContext.cs new file mode 100644 index 000000000..2aa5156b4 --- /dev/null +++ b/src/Discord.Net.Core/Interactions/IRestInteractionContext.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IRestInteractionContext : IInteractionContext + { + /// + /// Gets or sets the callback to use when the service has outgoing json for the rest webhook. + /// + /// + /// If this property is the default callback will be used. + /// + Func InteractionResponseCallback { get; } + } +} diff --git a/src/Discord.Net.Interactions/Attributes/Preconditions/RequireBotPermissionAttribute.cs b/src/Discord.Net.Interactions/Attributes/Preconditions/RequireBotPermissionAttribute.cs new file mode 100644 index 000000000..1dd3092ff --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/Preconditions/RequireBotPermissionAttribute.cs @@ -0,0 +1,84 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + /// + /// Requires the bot to have a specific permission in the channel a command is invoked in. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] + public class RequireBotPermissionAttribute : PreconditionAttribute + { + /// + /// Gets the specified of the precondition. + /// + public GuildPermission? GuildPermission { get; } + /// + /// Gets the specified of the precondition. + /// + public ChannelPermission? ChannelPermission { get; } + /// + /// Gets or sets the error message if the precondition + /// fails due to being run outside of a Guild channel. + /// + public string NotAGuildErrorMessage { get; set; } + + /// + /// Requires the bot account to have a specific . + /// + /// + /// This precondition will always fail if the command is being invoked in a . + /// + /// + /// The that the bot must have. Multiple permissions can be specified + /// by ORing the permissions together. + /// + public RequireBotPermissionAttribute(GuildPermission permission) + { + GuildPermission = permission; + ChannelPermission = null; + } + /// + /// Requires that the bot account to have a specific . + /// + /// + /// The that the bot must have. Multiple permissions can be + /// specified by ORing the permissions together. + /// + public RequireBotPermissionAttribute(ChannelPermission permission) + { + ChannelPermission = permission; + GuildPermission = null; + } + + /// + public override async Task CheckRequirementsAsync(IInteractionContext context, ICommandInfo command, IServiceProvider services) + { + IGuildUser guildUser = null; + if (context.Guild != null) + guildUser = await context.Guild.GetCurrentUserAsync().ConfigureAwait(false); + + if (GuildPermission.HasValue) + { + if (guildUser == null) + return PreconditionResult.FromError(NotAGuildErrorMessage ?? "Command must be used in a guild channel."); + if (!guildUser.GuildPermissions.Has(GuildPermission.Value)) + return PreconditionResult.FromError(ErrorMessage ?? $"Bot requires guild permission {GuildPermission.Value}."); + } + + if (ChannelPermission.HasValue) + { + ChannelPermissions perms; + if (context.Channel is IGuildChannel guildChannel) + perms = guildUser.GetPermissions(guildChannel); + else + perms = ChannelPermissions.All(context.Channel); + + if (!perms.Has(ChannelPermission.Value)) + return PreconditionResult.FromError(ErrorMessage ?? $"Bot requires channel permission {ChannelPermission.Value}."); + } + + return PreconditionResult.FromSuccess(); + } + } +} diff --git a/src/Discord.Net.Interactions/Attributes/Preconditions/RequireContextAttribute.cs b/src/Discord.Net.Interactions/Attributes/Preconditions/RequireContextAttribute.cs new file mode 100644 index 000000000..2f1b1df0d --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/Preconditions/RequireContextAttribute.cs @@ -0,0 +1,72 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + /// + /// Defines the type of command context (i.e. where the command is being executed). + /// + [Flags] + public enum ContextType + { + /// + /// Specifies the command to be executed within a guild. + /// + Guild = 0x01, + /// + /// Specifies the command to be executed within a DM. + /// + DM = 0x02, + /// + /// Specifies the command to be executed within a group. + /// + Group = 0x04 + } + + /// + /// Requires the command to be invoked in a specified context (e.g. in guild, DM). + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] + public class RequireContextAttribute : PreconditionAttribute + { + /// + /// Gets the context required to execute the command. + /// + public ContextType Contexts { get; } + + /// Requires the command to be invoked in the specified context. + /// The type of context the command can be invoked in. Multiple contexts can be specified by ORing the contexts together. + /// + /// + /// [Command("secret")] + /// [RequireContext(ContextType.DM | ContextType.Group)] + /// public Task PrivateOnlyAsync() + /// { + /// return ReplyAsync("shh, this command is a secret"); + /// } + /// + /// + public RequireContextAttribute(ContextType contexts) + { + Contexts = contexts; + } + + /// + public override Task CheckRequirementsAsync(IInteractionContext context, ICommandInfo command, IServiceProvider services) + { + bool isValid = false; + + if ((Contexts & ContextType.Guild) != 0) + isValid = context.Channel is IGuildChannel; + if ((Contexts & ContextType.DM) != 0) + isValid = isValid || context.Channel is IDMChannel; + if ((Contexts & ContextType.Group) != 0) + isValid = isValid || context.Channel is IGroupChannel; + + if (isValid) + return Task.FromResult(PreconditionResult.FromSuccess()); + else + return Task.FromResult(PreconditionResult.FromError(ErrorMessage ?? $"Invalid context for command; accepted contexts: {Contexts}.")); + } + } +} diff --git a/src/Discord.Net.Interactions/Attributes/Preconditions/RequireNsfwAttribute.cs b/src/Discord.Net.Interactions/Attributes/Preconditions/RequireNsfwAttribute.cs new file mode 100644 index 000000000..f943ae080 --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/Preconditions/RequireNsfwAttribute.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + /// + /// Requires the command to be invoked in a channel marked NSFW. + /// + /// + /// The precondition will restrict the access of the command or module to be accessed within a guild channel + /// that has been marked as mature or NSFW. If the channel is not of type or the + /// channel is not marked as NSFW, the precondition will fail with an erroneous . + /// + /// + /// The following example restricts the command too-cool to an NSFW-enabled channel only. + /// + /// public class DankModule : ModuleBase + /// { + /// [Command("cool")] + /// public Task CoolAsync() + /// => ReplyAsync("I'm cool for everyone."); + /// + /// [RequireNsfw] + /// [Command("too-cool")] + /// public Task TooCoolAsync() + /// => ReplyAsync("You can only see this if you're cool enough."); + /// } + /// + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] + public class RequireNsfwAttribute : PreconditionAttribute + { + /// + public override Task CheckRequirementsAsync(IInteractionContext context, ICommandInfo command, IServiceProvider services) + { + if (context.Channel is ITextChannel text && text.IsNsfw) + return Task.FromResult(PreconditionResult.FromSuccess()); + else + return Task.FromResult(PreconditionResult.FromError(ErrorMessage ?? "This command may only be invoked in an NSFW channel.")); + } + } +} diff --git a/src/Discord.Net.Interactions/Attributes/Preconditions/RequireOwnerAttribute.cs b/src/Discord.Net.Interactions/Attributes/Preconditions/RequireOwnerAttribute.cs new file mode 100644 index 000000000..827ede239 --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/Preconditions/RequireOwnerAttribute.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + /// + /// Requires the command to be invoked by the owner of the bot. + /// + /// + /// This precondition will restrict the access of the command or module to the owner of the Discord application. + /// If the precondition fails to be met, an erroneous will be returned with the + /// message "Command can only be run by the owner of the bot." + /// + /// This precondition will only work if the account has a of + /// ;otherwise, this precondition will always fail. + /// + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] + public class RequireOwnerAttribute : PreconditionAttribute + { + /// + public override async Task CheckRequirementsAsync(IInteractionContext context, ICommandInfo command, IServiceProvider services) + { + switch (context.Client.TokenType) + { + case TokenType.Bot: + var application = await context.Client.GetApplicationInfoAsync().ConfigureAwait(false); + if (context.User.Id != application.Owner.Id) + return PreconditionResult.FromError(ErrorMessage ?? "Command can only be run by the owner of the bot."); + return PreconditionResult.FromSuccess(); + default: + return PreconditionResult.FromError($"{nameof(RequireOwnerAttribute)} is not supported by this {nameof(TokenType)}."); + } + } + } +} diff --git a/src/Discord.Net.Interactions/Attributes/Preconditions/RequireRoleAttribute.cs b/src/Discord.Net.Interactions/Attributes/Preconditions/RequireRoleAttribute.cs new file mode 100644 index 000000000..69a49c7fa --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/Preconditions/RequireRoleAttribute.cs @@ -0,0 +1,71 @@ +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + /// + /// Requires the user invoking the command to have a specified role. + /// + public class RequireRoleAttribute : PreconditionAttribute + { + /// + /// Gets the specified Role name of the precondition. + /// + public string RoleName { get; } + + /// + /// Gets the specified Role ID of the precondition. + /// + public ulong? RoleId { get; } + + /// + /// Gets or sets the error message if the precondition + /// fails due to being run outside of a Guild channel. + /// + public string NotAGuildErrorMessage { get; set; } + + /// + /// Requires that the user invoking the command to have a specific Role. + /// + /// Id of the role that the user must have. + public RequireRoleAttribute(ulong roleId) + { + RoleId = roleId; + } + + /// + /// Requires that the user invoking the command to have a specific Role. + /// + /// Name of the role that the user must have. + public RequireRoleAttribute(string roleName) + { + RoleName = roleName; + } + + /// + public override Task CheckRequirementsAsync(IInteractionContext context, ICommandInfo commandInfo, IServiceProvider services) + { + if (context.User is not IGuildUser guildUser) + return Task.FromResult(PreconditionResult.FromError(NotAGuildErrorMessage ?? "Command must be used in a guild channel.")); + + if (RoleId.HasValue) + { + if (guildUser.RoleIds.Contains(RoleId.Value)) + return Task.FromResult(PreconditionResult.FromSuccess()); + else + Task.FromResult(PreconditionResult.FromError(ErrorMessage ?? $"User requires guild role {context.Guild.GetRole(RoleId.Value).Name}.")); + } + + if (!string.IsNullOrEmpty(RoleName)) + { + if (guildUser.Guild.Roles.Any(x => x.Name == RoleName)) + return Task.FromResult(PreconditionResult.FromSuccess()); + else + Task.FromResult(PreconditionResult.FromError(ErrorMessage ?? $"User requires guild role {RoleName}.")); + } + + return Task.FromResult(PreconditionResult.FromSuccess()); + } + } +} diff --git a/src/Discord.Net.Interactions/Attributes/Preconditions/RequireUserPermissionAttribute.cs b/src/Discord.Net.Interactions/Attributes/Preconditions/RequireUserPermissionAttribute.cs new file mode 100644 index 000000000..77d6e8f25 --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/Preconditions/RequireUserPermissionAttribute.cs @@ -0,0 +1,81 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + /// + /// Requires the user invoking the command to have a specified permission. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] + public class RequireUserPermissionAttribute : PreconditionAttribute + { + /// + /// Gets the specified of the precondition. + /// + public GuildPermission? GuildPermission { get; } + /// + /// Gets the specified of the precondition. + /// + public ChannelPermission? ChannelPermission { get; } + /// + /// Gets or sets the error message if the precondition + /// fails due to being run outside of a Guild channel. + /// + public string NotAGuildErrorMessage { get; set; } + + /// + /// Requires that the user invoking the command to have a specific . + /// + /// + /// This precondition will always fail if the command is being invoked in a . + /// + /// + /// The that the user must have. Multiple permissions can be + /// specified by ORing the permissions together. + /// + public RequireUserPermissionAttribute(GuildPermission guildPermission) + { + GuildPermission = guildPermission; + } + + /// + /// Requires that the user invoking the command to have a specific . + /// + /// + /// The that the user must have. Multiple permissions can be + /// specified by ORing the permissions together. + /// + public RequireUserPermissionAttribute(ChannelPermission channelPermission) + { + ChannelPermission = channelPermission; + } + + /// + public override Task CheckRequirementsAsync(IInteractionContext context, ICommandInfo commandInfo, IServiceProvider services) + { + var guildUser = context.User as IGuildUser; + + if (GuildPermission.HasValue) + { + if (guildUser == null) + return Task.FromResult(PreconditionResult.FromError(NotAGuildErrorMessage ?? "Command must be used in a guild channel.")); + if (!guildUser.GuildPermissions.Has(GuildPermission.Value)) + return Task.FromResult(PreconditionResult.FromError(ErrorMessage ?? $"User requires guild permission {GuildPermission.Value}.")); + } + + if (ChannelPermission.HasValue) + { + ChannelPermissions perms; + if (context.Channel is IGuildChannel guildChannel) + perms = guildUser.GetPermissions(guildChannel); + else + perms = ChannelPermissions.All(context.Channel); + + if (!perms.Has(ChannelPermission.Value)) + return Task.FromResult(PreconditionResult.FromError(ErrorMessage ?? $"User requires channel permission {ChannelPermission.Value}.")); + } + + return Task.FromResult(PreconditionResult.FromSuccess()); + } + } +} diff --git a/src/Discord.Net.Interactions/AutocompleteHandlers/AutocompleteHandler.cs b/src/Discord.Net.Interactions/AutocompleteHandlers/AutocompleteHandler.cs index fd0bc83de..1dd3cf2bf 100644 --- a/src/Discord.Net.Interactions/AutocompleteHandlers/AutocompleteHandler.cs +++ b/src/Discord.Net.Interactions/AutocompleteHandlers/AutocompleteHandler.cs @@ -60,7 +60,11 @@ namespace Discord.Interactions { case RestAutocompleteInteraction restAutocomplete: var payload = restAutocomplete.Respond(result.Suggestions); - await InteractionService._restResponseCallback(context, payload).ConfigureAwait(false); + + if (context is IRestInteractionContext restContext && restContext.InteractionResponseCallback != null) + await restContext.InteractionResponseCallback.Invoke(payload).ConfigureAwait(false); + else + await InteractionService._restResponseCallback(context, payload).ConfigureAwait(false); break; case SocketAutocompleteInteraction socketAutocomplete: await socketAutocomplete.RespondAsync(result.Suggestions).ConfigureAwait(false); diff --git a/src/Discord.Net.Interactions/Discord.Net.Interactions.csproj b/src/Discord.Net.Interactions/Discord.Net.Interactions.csproj index e56459e44..c617eff61 100644 --- a/src/Discord.Net.Interactions/Discord.Net.Interactions.csproj +++ b/src/Discord.Net.Interactions/Discord.Net.Interactions.csproj @@ -6,7 +6,6 @@ net6.0;net5.0;netstandard2.0;netstandard2.1 Discord.Interactions Discord.Net.Interactions - Discord.Net.Interactions A Discord.Net extension adding support for Application Commands. diff --git a/src/Discord.Net.Interactions/RestInteractionModuleBase.cs b/src/Discord.Net.Interactions/RestInteractionModuleBase.cs index fe184fc9a..a07614f7f 100644 --- a/src/Discord.Net.Interactions/RestInteractionModuleBase.cs +++ b/src/Discord.Net.Interactions/RestInteractionModuleBase.cs @@ -30,7 +30,12 @@ namespace Discord.Interactions if (Context.Interaction is not RestInteraction restInteraction) throw new InvalidOperationException($"Invalid interaction type. Interaction must be a type of {nameof(RestInteraction)} in order to execute this method"); - await InteractionService._restResponseCallback(Context, restInteraction.Defer(ephemeral, options)).ConfigureAwait(false); + var payload = restInteraction.Defer(ephemeral, options); + + if (Context is IRestInteractionContext restContext && restContext.InteractionResponseCallback != null) + await restContext.InteractionResponseCallback.Invoke(payload).ConfigureAwait(false); + else + await InteractionService._restResponseCallback(Context, payload).ConfigureAwait(false); } /// @@ -53,7 +58,12 @@ namespace Discord.Interactions if (Context.Interaction is not RestInteraction restInteraction) throw new InvalidOperationException($"Invalid interaction type. Interaction must be a type of {nameof(RestInteraction)} in order to execute this method"); - await InteractionService._restResponseCallback(Context, restInteraction.Respond(text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options)).ConfigureAwait(false); + var payload = restInteraction.Respond(text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options); + + if (Context is IRestInteractionContext restContext && restContext.InteractionResponseCallback != null) + await restContext.InteractionResponseCallback.Invoke(payload).ConfigureAwait(false); + else + await InteractionService._restResponseCallback(Context, payload).ConfigureAwait(false); } } } diff --git a/src/Discord.Net.Rest/API/Common/Application.cs b/src/Discord.Net.Rest/API/Common/Application.cs index 4ef6940a2..62a1d9688 100644 --- a/src/Discord.Net.Rest/API/Common/Application.cs +++ b/src/Discord.Net.Rest/API/Common/Application.cs @@ -20,14 +20,17 @@ namespace Discord.API public bool BotRequiresCodeGrant { get; set; } [JsonProperty("install_params")] public Optional InstallParams { get; set; } - [JsonProperty("team")] public Team Team { get; set; } - [JsonProperty("flags"), Int53] public Optional Flags { get; set; } [JsonProperty("owner")] public Optional Owner { get; set; } + [JsonProperty("tags")] public Optional Tags { get; set; } + [JsonProperty("terms_of_service_url")] + public string TermsOfService { get; set; } + [JsonProperty("privacy_policy_url")] + public string PrivacyPolicy { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/UploadInteractionFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadInteractionFileParams.cs new file mode 100644 index 000000000..f004dec82 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/UploadInteractionFileParams.cs @@ -0,0 +1,99 @@ +using Discord.Net.Converters; +using Discord.Net.Rest; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API.Rest +{ + internal class UploadInteractionFileParams + { + private static JsonSerializer _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; + + public FileAttachment[] Files { get; } + + public InteractionResponseType Type { get; set; } + public Optional Content { get; set; } + public Optional IsTTS { get; set; } + public Optional Embeds { get; set; } + public Optional AllowedMentions { get; set; } + public Optional MessageComponents { get; set; } + public Optional Flags { get; set; } + + public bool HasData + => Content.IsSpecified || + IsTTS.IsSpecified || + Embeds.IsSpecified || + AllowedMentions.IsSpecified || + MessageComponents.IsSpecified || + Flags.IsSpecified || + Files.Any(); + + public UploadInteractionFileParams(params FileAttachment[] files) + { + Files = files; + } + + public IReadOnlyDictionary ToDictionary() + { + var d = new Dictionary(); + + + var payload = new Dictionary(); + payload["type"] = Type; + + var data = new Dictionary(); + if (Content.IsSpecified) + data["content"] = Content.Value; + if (IsTTS.IsSpecified) + data["tts"] = IsTTS.Value.ToString(); + if (MessageComponents.IsSpecified) + data["components"] = MessageComponents.Value; + if (Embeds.IsSpecified) + data["embeds"] = Embeds.Value; + if (AllowedMentions.IsSpecified) + data["allowed_mentions"] = AllowedMentions.Value; + if (Flags.IsSpecified) + data["flags"] = Flags.Value; + + List attachments = new(); + + for (int n = 0; n != Files.Length; n++) + { + var attachment = Files[n]; + + var filename = attachment.FileName ?? "unknown.dat"; + if (attachment.IsSpoiler && !filename.StartsWith(AttachmentExtensions.SpoilerPrefix)) + filename = filename.Insert(0, AttachmentExtensions.SpoilerPrefix); + d[$"files[{n}]"] = new MultipartFile(attachment.Stream, filename); + + attachments.Add(new + { + id = (ulong)n, + filename = filename, + description = attachment.Description ?? Optional.Unspecified + }); + } + + data["attachments"] = attachments; + + payload["data"] = data; + + + if (data.Any()) + { + var json = new StringBuilder(); + using (var text = new StringWriter(json)) + using (var writer = new JsonTextWriter(text)) + _serializer.Serialize(writer, payload); + d["payload_json"] = json.ToString(); + } + + return d; + } + } +} diff --git a/src/Discord.Net.Rest/Discord.Net.Rest.csproj b/src/Discord.Net.Rest/Discord.Net.Rest.csproj index a1c6892ca..98692998f 100644 --- a/src/Discord.Net.Rest/Discord.Net.Rest.csproj +++ b/src/Discord.Net.Rest/Discord.Net.Rest.csproj @@ -1,6 +1,6 @@ - + Discord.Net.Rest Discord.Rest diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index d905eee05..c2f2fbc99 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -1309,7 +1309,20 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); - await SendJsonAsync("POST", () => $"interactions/{interactionId}/{interactionToken}/callback", response, new BucketIds(), options: options); + await SendJsonAsync("POST", () => $"interactions/{interactionId}/{interactionToken}/callback", response, new BucketIds(), options: options); + } + public async Task CreateInteractionResponseAsync(UploadInteractionFileParams response, ulong interactionId, string interactionToken, RequestOptions options = null) + { + if ((!response.Embeds.IsSpecified || response.Embeds.Value == null || response.Embeds.Value.Length == 0) && !response.Files.Any()) + Preconditions.NotNullOrEmpty(response.Content, nameof(response.Content)); + + if (response.Content.IsSpecified && response.Content.Value.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentException(message: $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", paramName: nameof(response.Content)); + + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(); + await SendMultipartAsync("POST", () => $"interactions/{interactionId}/{interactionToken}/callback", response.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } public async Task GetInteractionResponseAsync(string interactionToken, RequestOptions options = null) { diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index 40328cbd6..45535746d 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -347,7 +347,8 @@ namespace Discord.Rest public static Task SendFileAsync(IMessageChannel channel, BaseDiscordClient client, Stream stream, string filename, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, RequestOptions options, bool isSpoiler, Embed[] embeds) { - return SendFileAsync(channel, client, new FileAttachment(stream, filename, isSpoiler: isSpoiler), text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + using var file = new FileAttachment(stream, filename, isSpoiler: isSpoiler); + return SendFileAsync(channel, client, file, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); } /// Message content is too long, length must be less or equal to . diff --git a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs index b20cfe2ed..0cac07577 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs @@ -34,11 +34,16 @@ namespace Discord.Rest return client.ApiClient.BulkOverwriteGlobalApplicationCommandsAsync(Array.Empty(), options); } - public static async Task SendInteractionResponseAsync(BaseDiscordClient client, InteractionResponse response, + public static async Task SendInteractionResponseAsync(BaseDiscordClient client, InteractionResponse response, + IDiscordInteraction interaction, IMessageChannel channel = null, RequestOptions options = null) + { + await client.ApiClient.CreateInteractionResponseAsync(response, interaction.Id, interaction.Token, options).ConfigureAwait(false); + } + + public static async Task SendInteractionResponseAsync(BaseDiscordClient client, UploadInteractionFileParams response, IDiscordInteraction interaction, IMessageChannel channel = null, RequestOptions options = null) { await client.ApiClient.CreateInteractionResponseAsync(response, interaction.Id, interaction.Token, options).ConfigureAwait(false); - return RestInteractionMessage.Create(client, response, interaction, channel); } public static async Task GetOriginalResponseAsync(BaseDiscordClient client, IMessageChannel channel, @@ -434,6 +439,9 @@ namespace Discord.Rest public static async Task DeleteInteractionResponseAsync(BaseDiscordClient client, RestInteractionMessage message, RequestOptions options = null) => await client.ApiClient.DeleteInteractionFollowupMessageAsync(message.Id, message.Token, options); + public static async Task DeleteInteractionResponseAsync(BaseDiscordClient client, IDiscordInteraction interaction, RequestOptions options = null) + => await client.ApiClient.DeleteInteractionFollowupMessageAsync(interaction.Id, interaction.Token, options); + public static Task SendAutocompleteResultAsync(BaseDiscordClient client, IEnumerable result, ulong interactionId, string interactionToken, RequestOptions options) { diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs b/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs index 5f551ba0c..0011b9b62 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs @@ -260,15 +260,17 @@ namespace Discord.Rest public abstract Task FollowupWithFilesAsync(IEnumerable attachments, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null); + /// + public Task DeleteOriginalResponseAsync(RequestOptions options = null) + => InteractionHelper.DeleteInteractionResponseAsync(Discord, this, options); + #region IDiscordInteraction /// IUser IDiscordInteraction.User => User; /// - Task IDiscordInteraction.RespondAsync(string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options) - { - return Task.FromResult(null); - } + Task IDiscordInteraction.RespondAsync(string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options) + => Task.FromResult(Respond(text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options)); /// Task IDiscordInteraction.DeferAsync(bool ephemeral, RequestOptions options) => Task.FromResult(Defer(ephemeral, options)); @@ -296,6 +298,17 @@ namespace Discord.Rest /// async Task IDiscordInteraction.FollowupWithFilesAsync(IEnumerable attachments, string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options) => await FollowupWithFilesAsync(attachments, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false); + /// + Task IDiscordInteraction.RespondWithFilesAsync(IEnumerable attachments, string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options) => throw new NotSupportedException("REST-Based interactions don't support files."); + /// +#if NETCOREAPP3_0_OR_GREATER != true + /// + Task IDiscordInteraction.RespondWithFileAsync(Stream fileStream, string fileName, string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options) => throw new NotSupportedException("REST-Based interactions don't support files."); + /// + Task IDiscordInteraction.RespondWithFileAsync(string filePath, string fileName, string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options) => throw new NotSupportedException("REST-Based interactions don't support files."); + /// + Task IDiscordInteraction.RespondWithFileAsync(FileAttachment attachment, string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options) => throw new NotSupportedException("REST-Based interactions don't support files."); +#endif #endregion } } diff --git a/src/Discord.Net.Rest/Entities/Messages/RestInteractionMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestInteractionMessage.cs index 815f1953f..a055a69b4 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestInteractionMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestInteractionMessage.cs @@ -1,7 +1,6 @@ using System; using System.Threading.Tasks; using MessageModel = Discord.API.Message; -using Model = Discord.API.InteractionResponse; namespace Discord.Rest { @@ -26,24 +25,11 @@ namespace Discord.Rest return entity; } - internal static RestInteractionMessage Create(BaseDiscordClient discord, Model model, IDiscordInteraction interaction, IMessageChannel channel) - { - var entity = new RestInteractionMessage(discord, interaction.Id, discord.CurrentUser, interaction.Token, channel); - entity.Update(model, interaction); - return entity; - } - internal new void Update(MessageModel model) { base.Update(model); } - internal void Update(Model model, IDiscordInteraction interaction) - { - ResponseType = model.Type; - base.Update(model.ToMessage(interaction)); - } - /// /// Deletes this object and all of it's children. /// diff --git a/src/Discord.Net.Rest/Entities/RestApplication.cs b/src/Discord.Net.Rest/Entities/RestApplication.cs index beec52433..8347a70da 100644 --- a/src/Discord.Net.Rest/Entities/RestApplication.cs +++ b/src/Discord.Net.Rest/Entities/RestApplication.cs @@ -29,10 +29,12 @@ namespace Discord.Rest public bool BotRequiresCodeGrant { get; private set; } /// public ITeam Team { get; private set; } - /// public IUser Owner { get; private set; } - + /// + public string TermsOfService { get; private set; } + /// + public string PrivacyPolicy { get; private set; } /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); /// @@ -61,6 +63,8 @@ namespace Discord.Rest IsBotPublic = model.IsBotPublic; BotRequiresCodeGrant = model.BotRequiresCodeGrant; Tags = model.Tags.GetValueOrDefault(null)?.ToImmutableArray() ?? ImmutableArray.Empty; + PrivacyPolicy = model.PrivacyPolicy; + TermsOfService = model.TermsOfService; var installParams = model.InstallParams.GetValueOrDefault(null); InstallParams = new ApplicationInstallParams(installParams?.Scopes ?? new string[0], (GuildPermission?)installParams?.Permission ?? null); diff --git a/src/Discord.Net.Rest/Interactions/RestInteractionContext.cs b/src/Discord.Net.Rest/Interactions/RestInteractionContext.cs index 7e73caa4f..196c6133b 100644 --- a/src/Discord.Net.Rest/Interactions/RestInteractionContext.cs +++ b/src/Discord.Net.Rest/Interactions/RestInteractionContext.cs @@ -1,9 +1,12 @@ +using System; +using System.Threading.Tasks; + namespace Discord.Rest { /// /// Represents a Rest based context of an . /// - public class RestInteractionContext : IInteractionContext + public class RestInteractionContext : IRestInteractionContext where TInteraction : RestInteraction { /// @@ -34,6 +37,14 @@ namespace Discord.Rest /// public TInteraction Interaction { get; } + /// + /// Gets or sets the callback to use when the service has outgoing json for the rest webhook. + /// + /// + /// If this property is the default callback will be used. + /// + public Func InteractionResponseCallback { get; set; } + /// /// Initializes a new . /// @@ -48,6 +59,18 @@ namespace Discord.Rest Interaction = interaction; } + /// + /// Initializes a new . + /// + /// The underlying client. + /// The underlying interaction. + /// The callback for outgoing json. + public RestInteractionContext(DiscordRestClient client, TInteraction interaction, Func interactionResponseCallback) + : this(client, interaction) + { + InteractionResponseCallback = interactionResponseCallback; + } + // IInterationContext /// IDiscordClient IInteractionContext.Client => Client; @@ -66,15 +89,24 @@ namespace Discord.Rest } /// - /// Represents a Rest based context of an + /// Represents a Rest based context of an . /// public class RestInteractionContext : RestInteractionContext { /// - /// Initializes a new + /// Initializes a new . /// - /// The underlying client - /// The underlying interaction + /// The underlying client. + /// The underlying interaction. public RestInteractionContext(DiscordRestClient client, RestInteraction interaction) : base(client, interaction) { } + + /// + /// Initializes a new . + /// + /// The underlying client. + /// The underlying interaction. + /// The callback for outgoing json. + public RestInteractionContext(DiscordRestClient client, RestInteraction interaction, Func interactionResponseCallback) + : base(client, interaction, interactionResponseCallback) { } } } diff --git a/src/Discord.Net.Rest/Net/DefaultRestClient.cs b/src/Discord.Net.Rest/Net/DefaultRestClient.cs index 1db743609..ce32b085b 100644 --- a/src/Discord.Net.Rest/Net/DefaultRestClient.cs +++ b/src/Discord.Net.Rest/Net/DefaultRestClient.cs @@ -129,7 +129,8 @@ namespace Discord.Net.Rest continue; } - default: throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\"."); + default: + throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\"."); } } } diff --git a/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs b/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs index 2921678f7..408c8bbdb 100644 --- a/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs +++ b/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs @@ -1,3 +1,4 @@ +using Discord.API; using Newtonsoft.Json; using System; #if DEBUG_LIMITS diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs index 0a42b403e..29e13a2a1 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs @@ -424,12 +424,12 @@ namespace Discord.WebSocket } internal readonly AsyncEvent> _userJoinedEvent = new AsyncEvent>(); /// Fired when a user leaves a guild. - public event Func UserLeft + public event Func UserLeft { add { _userLeftEvent.Add(value); } remove { _userLeftEvent.Remove(value); } } - internal readonly AsyncEvent> _userLeftEvent = new AsyncEvent>(); + internal readonly AsyncEvent> _userLeftEvent = new AsyncEvent>(); /// Fired when a user is banned from a guild. public event Func UserBanned { @@ -452,12 +452,12 @@ namespace Discord.WebSocket } internal readonly AsyncEvent> _userUpdatedEvent = new AsyncEvent>(); /// Fired when a guild member is updated, or a member presence is updated. - public event Func, SocketGuildUser, Task> GuildMemberUpdated + public event Func, SocketGuildUser, Task> GuildMemberUpdated { add { _guildMemberUpdatedEvent.Add(value); } remove { _guildMemberUpdatedEvent.Remove(value); } } - internal readonly AsyncEvent, SocketGuildUser, Task>> _guildMemberUpdatedEvent = new AsyncEvent, SocketGuildUser, Task>>(); + internal readonly AsyncEvent, SocketGuildUser, Task>> _guildMemberUpdatedEvent = new AsyncEvent, SocketGuildUser, Task>>(); /// Fired when a user joins, leaves, or moves voice channels. public event Func UserVoiceStateUpdated { diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index 1e71ce853..9a13c8ff8 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -445,7 +445,7 @@ namespace Discord.WebSocket client.GuildUpdated += (oldGuild, newGuild) => _guildUpdatedEvent.InvokeAsync(oldGuild, newGuild); client.UserJoined += (user) => _userJoinedEvent.InvokeAsync(user); - client.UserLeft += (user) => _userLeftEvent.InvokeAsync(user); + client.UserLeft += (guild, user) => _userLeftEvent.InvokeAsync(guild, user); client.UserBanned += (user, guild) => _userBannedEvent.InvokeAsync(user, guild); client.UserUnbanned += (user, guild) => _userUnbannedEvent.InvokeAsync(user, guild); client.UserUpdated += (oldUser, newUser) => _userUpdatedEvent.InvokeAsync(oldUser, newUser); diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 7c37cb1fa..b107fbbea 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1275,13 +1275,13 @@ namespace Discord.WebSocket var before = user.Clone(); user.Update(State, data); - var cacheableBefore = new Cacheable(null, user.Id, false, () => Rest.GetGuildUserAsync(guild.Id, user.Id)); + var cacheableBefore = new Cacheable(before, user.Id, true, () => null); await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false); } else { user = guild.AddOrUpdateUser(data); - var cacheableBefore = new Cacheable(null, user.Id, false, () => Rest.GetGuildUserAsync(guild.Id, user.Id)); + var cacheableBefore = new Cacheable(user, user.Id, true, () => null); await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false); } } @@ -1309,10 +1309,14 @@ namespace Discord.WebSocket return; } - if(user == null) + user = State.GetUser(data.User.Id); + + if (user != null) + user.Update(State, data.User); + else user = SocketGlobalUser.Create(this, State, data.User); - await TimedInvokeAsync(_userLeftEvent, nameof(UserLeft), user).ConfigureAwait(false); + await TimedInvokeAsync(_userLeftEvent, nameof(UserLeft), guild, user).ConfigureAwait(false); } else { diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs index 17038b23d..4fbab38b0 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs @@ -71,8 +71,72 @@ namespace Discord.WebSocket } } } + public override async Task RespondWithFilesAsync( + IEnumerable attachments, + string text = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + MessageComponent components = null, + Embed embed = null, + RequestOptions options = null) + { + 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(); + + 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."); + Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds 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.Rest.UploadInteractionFileParams(attachments?.ToArray()) + { + Type = InteractionResponseType.ChannelMessageWithSource, + Content = text ?? Optional.Unspecified, + AllowedMentions = allowedMentions != null ? allowedMentions?.ToModel() : Optional.Unspecified, + Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, + IsTTS = isTTS, + MessageComponents = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + Flags = ephemeral ? MessageFlags.Ephemeral : Optional.Unspecified + }; + + lock (_lock) + { + if (HasResponded) + { + throw new InvalidOperationException("Cannot respond, update, or defer the same interaction twice"); + } + } + + await InteractionHelper.SendInteractionResponseAsync(Discord, response, this, Channel, options).ConfigureAwait(false); + HasResponded = true; + } + /// - public override async Task RespondAsync( + public override async Task RespondAsync( string text = null, Embed[] embeds = null, bool isTTS = false, @@ -121,13 +185,11 @@ namespace Discord.WebSocket AllowedMentions = allowedMentions?.ToModel(), Embeds = embeds.Select(x => x.ToModel()).ToArray(), TTS = isTTS, + Flags = ephemeral ? MessageFlags.Ephemeral : Optional.Unspecified, Components = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified } }; - if (ephemeral) - response.Data.Value.Flags = MessageFlags.Ephemeral; - lock (_lock) { if (HasResponded) @@ -136,17 +198,8 @@ namespace Discord.WebSocket } } - try - { - return await InteractionHelper.SendInteractionResponseAsync(Discord, response, this, Channel, options).ConfigureAwait(false); - } - finally - { - lock (_lock) - { - HasResponded = true; - } - } + await InteractionHelper.SendInteractionResponseAsync(Discord, response, this, Channel, options).ConfigureAwait(false); + HasResponded = true; } /// @@ -155,7 +208,7 @@ namespace Discord.WebSocket /// A delegate containing the properties to modify the message with. /// The request options for this request. /// A task that represents the asynchronous operation of updating the message. - public async Task UpdateAsync(Action func, RequestOptions options = null) + public async Task UpdateAsync(Action func, RequestOptions options = null) { var args = new MessageProperties(); func(args); @@ -236,12 +289,8 @@ namespace Discord.WebSocket } } - return await InteractionHelper.SendInteractionResponseAsync(Discord, response, this, Channel, options).ConfigureAwait(false); - - lock (_lock) - { - HasResponded = true; - } + await InteractionHelper.SendInteractionResponseAsync(Discord, response, this, Channel, options).ConfigureAwait(false); + HasResponded = true; } /// @@ -281,68 +330,6 @@ namespace Discord.WebSocket return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options); } - /// - public override Task FollowupWithFileAsync( - Stream fileStream, - string fileName, - string text = null, - Embed[] embeds = null, - bool isTTS = false, - bool ephemeral = false, - AllowedMentions allowedMentions = null, - MessageComponent components = null, - Embed embed = null, - RequestOptions options = null) - { - if (!IsValidToken) - throw new InvalidOperationException("Interaction token is no longer valid"); - - embeds ??= Array.Empty(); - if (embed != null) - embeds = new[] { embed }.Concat(embeds).ToArray(); - - Preconditions.NotNull(fileStream, nameof(fileStream), "File Stream must have data"); - Preconditions.NotNullOrEmpty(fileName, nameof(fileName), "File Name must not be empty or null"); - - return FollowupWithFileAsync(new FileAttachment(fileStream, fileName), text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options); - } - - /// - public override Task FollowupWithFileAsync( - string filePath, - string fileName = null, - string text = null, - Embed[] embeds = null, - bool isTTS = false, - bool ephemeral = false, - AllowedMentions allowedMentions = null, - MessageComponent components = null, - Embed embed = null, - RequestOptions options = null) - { - Preconditions.NotNullOrEmpty(filePath, nameof(filePath), "Path must exist"); - - fileName ??= Path.GetFileName(filePath); - Preconditions.NotNullOrEmpty(fileName, nameof(fileName), "File Name must not be empty or null"); - - return FollowupWithFileAsync(new FileAttachment(File.OpenRead(filePath), fileName), text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options); - } - - /// - public override Task FollowupWithFileAsync( - FileAttachment attachment, - string text = null, - Embed[] embeds = null, - bool isTTS = false, - bool ephemeral = false, - AllowedMentions allowedMentions = null, - MessageComponent components = null, - Embed embed = null, - RequestOptions options = null) - { - return FollowupWithFilesAsync(new FileAttachment[] { attachment }, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options); - } - /// public override async Task FollowupWithFilesAsync( IEnumerable attachments, diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketAutocompleteInteraction.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketAutocompleteInteraction.cs index 955d7d53f..6058bdafd 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketAutocompleteInteraction.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketAutocompleteInteraction.cs @@ -89,20 +89,16 @@ namespace Discord.WebSocket /// public Task RespondAsync(RequestOptions options = null, params AutocompleteResult[] result) => RespondAsync(result, options); - public override Task RespondAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null) + public override Task RespondAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null) => throw new NotSupportedException("Autocomplete interactions don't support this method!"); public override Task FollowupAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null) => throw new NotSupportedException("Autocomplete interactions don't support this method!"); - public override Task FollowupWithFileAsync(Stream fileStream, string fileName, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null) - => throw new NotSupportedException("Autocomplete interactions don't support this method!"); - public override Task FollowupWithFileAsync(string filePath, string fileName = null, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null) - => throw new NotSupportedException("Autocomplete interactions don't support this method!"); - public override Task FollowupWithFileAsync(FileAttachment attachment, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null) - => throw new NotSupportedException("Autocomplete interactions don't support this method!"); public override Task FollowupWithFilesAsync(IEnumerable attachments, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null) => throw new NotSupportedException("Autocomplete interactions don't support this method!"); public override Task DeferAsync(bool ephemeral = false, RequestOptions options = null) => throw new NotSupportedException("Autocomplete interactions don't support this method!"); + public override Task RespondWithFilesAsync(IEnumerable attachments, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null) + => throw new NotSupportedException("Autocomplete interactions don't support this method!"); //IAutocompleteInteraction /// diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs index 0a97bcd48..439200d30 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs @@ -69,7 +69,7 @@ namespace Discord.WebSocket } /// - public override async Task RespondAsync( + public override async Task RespondAsync( string text = null, Embed[] embeds = null, bool isTTS = false, @@ -131,21 +131,12 @@ namespace Discord.WebSocket } } - try - { - return await InteractionHelper.SendInteractionResponseAsync(Discord, response, this, Channel, options).ConfigureAwait(false); - } - finally - { - lock (_lock) - { - HasResponded = true; - } - } + await InteractionHelper.SendInteractionResponseAsync(Discord, response, this, Channel, options).ConfigureAwait(false); + HasResponded = true; } - /// - public override async Task FollowupAsync( + public override async Task RespondWithFilesAsync( + IEnumerable attachments, string text = null, Embed[] embeds = null, bool isTTS = false, @@ -158,6 +149,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(); @@ -166,25 +160,47 @@ namespace Discord.WebSocket Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); - var args = new API.Rest.CreateWebhookMessageParams + // 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.Rest.UploadInteractionFileParams(attachments?.ToArray()) + { + Type = InteractionResponseType.ChannelMessageWithSource, Content = text ?? Optional.Unspecified, - AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, + AllowedMentions = allowedMentions != null ? allowedMentions?.ToModel() : Optional.Unspecified, + Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, IsTTS = isTTS, - Embeds = embeds.Select(x => x.ToModel()).ToArray(), - Components = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified + Flags = ephemeral ? MessageFlags.Ephemeral : Optional.Unspecified, + MessageComponents = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified }; - if (ephemeral) - args.Flags = MessageFlags.Ephemeral; + lock (_lock) + { + if (HasResponded) + { + throw new InvalidOperationException("Cannot respond, update, or defer the same interaction twice"); + } + } - return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options); + await InteractionHelper.SendInteractionResponseAsync(Discord, response, this, Channel, options).ConfigureAwait(false); + HasResponded = true; } /// - public override Task FollowupWithFileAsync( - Stream fileStream, - string fileName, + public override async Task FollowupAsync( string text = null, Embed[] embeds = null, bool isTTS = false, @@ -201,48 +217,25 @@ namespace Discord.WebSocket if (embed != null) embeds = new[] { embed }.Concat(embeds).ToArray(); - Preconditions.NotNull(fileStream, nameof(fileStream), "File Stream must have data"); - Preconditions.NotNullOrEmpty(fileName, nameof(fileName), "File Name must not be empty or null"); - - return FollowupWithFileAsync(new FileAttachment(fileStream, fileName), text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options); - } - - /// - public override Task FollowupWithFileAsync( - string filePath, - string fileName = null, - string text = null, - Embed[] embeds = null, - bool isTTS = false, - bool ephemeral = false, - AllowedMentions allowedMentions = null, - MessageComponent components = null, - Embed embed = null, - RequestOptions options = null) - { - Preconditions.NotNullOrEmpty(filePath, nameof(filePath), "Path must exist"); + 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."); + Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); - fileName ??= Path.GetFileName(filePath); - Preconditions.NotNullOrEmpty(fileName, nameof(fileName), "File Name must not be empty or null"); + var args = new API.Rest.CreateWebhookMessageParams + { + Content = text ?? Optional.Unspecified, + AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, + IsTTS = isTTS, + Embeds = embeds.Select(x => x.ToModel()).ToArray(), + Components = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified + }; - return FollowupWithFileAsync(new FileAttachment(File.OpenRead(filePath), fileName), text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options); - } + if (ephemeral) + args.Flags = MessageFlags.Ephemeral; - /// - public override Task FollowupWithFileAsync( - FileAttachment attachment, - string text = null, - Embed[] embeds = null, - bool isTTS = false, - bool ephemeral = false, - AllowedMentions allowedMentions = null, - MessageComponent components = null, - Embed embed = null, - RequestOptions options = null) - { - return FollowupWithFilesAsync(new FileAttachment[] { attachment }, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options); + return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options); } - + /// public override async Task FollowupWithFilesAsync( IEnumerable attachments, diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs index 1bfd77479..b11a78464 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs @@ -136,9 +136,97 @@ namespace Discord.WebSocket /// The request options for this response. /// Message content is too long, length must be less or equal to . /// The parameters provided were invalid or the token was invalid. - public abstract Task RespondAsync(string text = null, Embed[] embeds = null, bool isTTS = false, + public abstract Task RespondAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null); + /// + /// Responds to this interaction with a file attachment. + /// + /// The file to upload. + /// The file name of the attachment. + /// The text of the message to be sent. + /// A array of embeds to send with this response. Max 10. + /// if the message should be read out by a text-to-speech reader, otherwise . + /// if the response should be hidden to everyone besides the invoker of the command, otherwise . + /// The allowed mentions for this response. + /// A to be sent with this response. + /// A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored. + /// The request options for this response. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + public async Task RespondWithFileAsync(Stream fileStream, string fileName, string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options) + { + using (var file = new FileAttachment(fileStream, fileName)) + { + await RespondWithFileAsync(file, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false); + } + } + + /// + /// Responds to this interaction with a file attachment. + /// + /// The file to upload. + /// The file name of the attachment. + /// The text of the message to be sent. + /// A array of embeds to send with this response. Max 10. + /// if the message should be read out by a text-to-speech reader, otherwise . + /// if the response should be hidden to everyone besides the invoker of the command, otherwise . + /// The allowed mentions for this response. + /// The request options for this response. + /// A to be sent with this response. + /// A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + public async Task RespondWithFileAsync(string filePath, string fileName, string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options) + { + using (var file = new FileAttachment(filePath, fileName)) + { + await RespondWithFileAsync(file, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false); + } + } + + /// + /// Responds to this interaction with a file attachment. + /// + /// The attachment containing the file and description. + /// The text of the message to be sent. + /// A array of embeds to send with this response. Max 10. + /// if the message should be read out by a text-to-speech reader, otherwise . + /// if the response should be hidden to everyone besides the invoker of the command, otherwise . + /// The allowed mentions for this response. + /// The request options for this response. + /// A to be sent with this response. + /// A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + public Task RespondWithFileAsync(FileAttachment attachment, string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options) + => RespondWithFilesAsync(new FileAttachment[] { attachment }, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options); + + /// + /// Responds to this interaction with a collection of file attachments. + /// + /// A collection of attachments to upload. + /// The text of the message to be sent. + /// A array of embeds to send with this response. Max 10. + /// if the message should be read out by a text-to-speech reader, otherwise . + /// if the response should be hidden to everyone besides the invoker of the command, otherwise . + /// The allowed mentions for this response. + /// The request options for this response. + /// A to be sent with this response. + /// A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + public abstract Task RespondWithFilesAsync(IEnumerable attachments, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null); + /// /// Sends a followup message for this interaction. /// @@ -172,8 +260,14 @@ namespace Discord.WebSocket /// /// The sent message. /// - public abstract Task FollowupWithFileAsync(Stream fileStream, string fileName, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null); + public async Task FollowupWithFileAsync(Stream fileStream, string fileName, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null) + { + using (var file = new FileAttachment(fileStream, fileName)) + { + return await FollowupWithFileAsync(file, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false); + } + } /// /// Sends a followup message for this interaction. @@ -191,8 +285,14 @@ namespace Discord.WebSocket /// /// The sent message. /// - public abstract Task FollowupWithFileAsync(string filePath, string fileName = null, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null); + public async Task FollowupWithFileAsync(string filePath, string fileName = null, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null) + { + using (var file = new FileAttachment(filePath, fileName)) + { + return await FollowupWithFileAsync(file, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false); + } + } /// /// Sends a followup message for this interaction. @@ -210,8 +310,9 @@ namespace Discord.WebSocket /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. /// - public abstract Task FollowupWithFileAsync(FileAttachment attachment, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null); + public Task FollowupWithFileAsync(FileAttachment attachment, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null) + => FollowupWithFilesAsync(new FileAttachment[] { attachment }, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options); /// /// Sends a followup message for this interaction. @@ -252,6 +353,10 @@ namespace Discord.WebSocket return RestInteractionMessage.Create(Discord, model, Token, Channel); } + /// + public Task DeleteOriginalResponseAsync(RequestOptions options = null) + => InteractionHelper.DeleteInteractionResponseAsync(Discord, this, options); + /// /// Acknowledges this interaction. /// @@ -275,12 +380,16 @@ namespace Discord.WebSocket async Task IDiscordInteraction.ModifyOriginalResponseAsync(Action func, RequestOptions options) => await ModifyOriginalResponseAsync(func, options).ConfigureAwait(false); /// - async Task IDiscordInteraction.RespondAsync(string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options) + async Task IDiscordInteraction.RespondAsync(string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options) => await RespondAsync(text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false); /// async Task IDiscordInteraction.FollowupAsync(string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options) => await FollowupAsync(text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false); /// + async Task IDiscordInteraction.FollowupWithFilesAsync(IEnumerable attachments, string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options) + => await FollowupWithFilesAsync(attachments, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false); +#if NETCOREAPP3_0_OR_GREATER != true + /// async Task IDiscordInteraction.FollowupWithFileAsync(Stream fileStream, string fileName, string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options) => await FollowupWithFileAsync(fileStream, fileName, text, embeds, isTTS, ephemeral, allowedMentions, components, embed).ConfigureAwait(false); /// @@ -289,9 +398,7 @@ namespace Discord.WebSocket /// async Task IDiscordInteraction.FollowupWithFileAsync(FileAttachment attachment, string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options) => await FollowupWithFileAsync(attachment, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false); - /// - async Task IDiscordInteraction.FollowupWithFilesAsync(IEnumerable attachments, string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options) - => await FollowupWithFilesAsync(attachments, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false); +#endif #endregion } } diff --git a/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj b/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj index 175b486d2..df920b7dc 100644 --- a/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj +++ b/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj @@ -1,6 +1,6 @@ - + Discord.Net.Webhook Discord.Webhook