Browse Source

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
tags/3.0.0
Quin Lynch GitHub 3 years ago
parent
commit
6c7502da68
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 1087 additions and 262 deletions
  1. +12
    -12
      docs/faq/basics/basic-operations.md
  2. +2
    -2
      docs/guides/getting_started/installing.md
  3. +12
    -9
      docs/guides/v2_v3_guide/v2_to_v3_guide.md
  4. +4
    -4
      docs/index.md
  5. +2
    -2
      src/Discord.Net.Commands/CommandService.cs
  6. +2
    -3
      src/Discord.Net.Commands/Discord.Net.Commands.csproj
  7. +13
    -4
      src/Discord.Net.Core/Discord.Net.Core.csproj
  8. +1
    -1
      src/Discord.Net.Core/DiscordErrorCode.cs
  9. +12
    -1
      src/Discord.Net.Core/Entities/IApplication.cs
  10. +144
    -11
      src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs
  11. +6
    -2
      src/Discord.Net.Core/Entities/Messages/FileAttachment.cs
  12. +19
    -0
      src/Discord.Net.Core/Interactions/IRestInteractionContext.cs
  13. +84
    -0
      src/Discord.Net.Interactions/Attributes/Preconditions/RequireBotPermissionAttribute.cs
  14. +72
    -0
      src/Discord.Net.Interactions/Attributes/Preconditions/RequireContextAttribute.cs
  15. +42
    -0
      src/Discord.Net.Interactions/Attributes/Preconditions/RequireNsfwAttribute.cs
  16. +36
    -0
      src/Discord.Net.Interactions/Attributes/Preconditions/RequireOwnerAttribute.cs
  17. +71
    -0
      src/Discord.Net.Interactions/Attributes/Preconditions/RequireRoleAttribute.cs
  18. +81
    -0
      src/Discord.Net.Interactions/Attributes/Preconditions/RequireUserPermissionAttribute.cs
  19. +5
    -1
      src/Discord.Net.Interactions/AutocompleteHandlers/AutocompleteHandler.cs
  20. +0
    -1
      src/Discord.Net.Interactions/Discord.Net.Interactions.csproj
  21. +12
    -2
      src/Discord.Net.Interactions/RestInteractionModuleBase.cs
  22. +5
    -2
      src/Discord.Net.Rest/API/Common/Application.cs
  23. +99
    -0
      src/Discord.Net.Rest/API/Rest/UploadInteractionFileParams.cs
  24. +1
    -1
      src/Discord.Net.Rest/Discord.Net.Rest.csproj
  25. +14
    -1
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  26. +2
    -1
      src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs
  27. +10
    -2
      src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs
  28. +17
    -4
      src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs
  29. +0
    -14
      src/Discord.Net.Rest/Entities/Messages/RestInteractionMessage.cs
  30. +6
    -2
      src/Discord.Net.Rest/Entities/RestApplication.cs
  31. +37
    -5
      src/Discord.Net.Rest/Interactions/RestInteractionContext.cs
  32. +2
    -1
      src/Discord.Net.Rest/Net/DefaultRestClient.cs
  33. +1
    -0
      src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs
  34. +4
    -4
      src/Discord.Net.WebSocket/BaseSocketClient.Events.cs
  35. +1
    -1
      src/Discord.Net.WebSocket/DiscordShardedClient.cs
  36. +8
    -4
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  37. +71
    -84
      src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs
  38. +3
    -7
      src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketAutocompleteInteraction.cs
  39. +55
    -62
      src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs
  40. +118
    -11
      src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs
  41. +1
    -1
      src/Discord.Net.Webhook/Discord.Net.Webhook.csproj

+ 12
- 12
docs/faq/basics/basic-operations.md View File

@@ -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

+ 2
- 2
docs/guides/getting_started/installing.md View File

@@ -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

+ 12
- 9
docs/guides/v2_v3_guide/v2_to_v3_guide.md View File

@@ -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<SocketGuildUser, RestGuildUser, IGuildUser, ulong>` 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.

+ 4
- 4
docs/index.md View File

@@ -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


+ 2
- 2
src/Discord.Net.Commands/CommandService.cs View File

@@ -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



+ 2
- 3
src/Discord.Net.Commands/Discord.Net.Commands.csproj View File

@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../../Discord.Net.targets" />
<Import Project="../../StyleAnalyzer.targets"/>
<Import Project="../../StyleAnalyzer.targets" />
<PropertyGroup>
<AssemblyName>Discord.Net.Commands</AssemblyName>
<RootNamespace>Discord.Commands</RootNamespace>
@@ -11,5 +11,4 @@
<ItemGroup>
<ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" />
</ItemGroup>

</Project>
</Project>

+ 13
- 4
src/Discord.Net.Core/Discord.Net.Core.csproj View File

@@ -9,11 +9,20 @@
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">net6.0;net5.0;netstandard2.0;netstandard2.1</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="System.Collections.Immutable" Version="1.3.1" />
<PackageReference Include="System.Interactive.Async" Version="4.0.0" />
<PackageReference Include="IDisposableAnalyzers" Version="2.1.2">
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="System.Interactive.Async" Version="5.0.0" />
<PackageReference Include="IDisposableAnalyzers" Version="3.4.15">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.1' ">
<PackageReference Include="System.Collections.Immutable" Version="1.3.1" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' != 'netstandard2.1' ">
<PackageReference Include="System.Collections.Immutable" Version="5.0.0" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net461' ">
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
</ItemGroup>
</Project>

+ 1
- 1
src/Discord.Net.Core/DiscordErrorCode.cs View File

@@ -150,7 +150,7 @@ namespace Discord
ServerLocaleUnavailable = 50095,
ServerRequiresMonetization = 50097,
ServerRequiresBoosts = 50101,
RequestBodyContainsInvalidJSON = 50109,
#endregion

#region 2FA (60XXX)


+ 12
- 1
src/Discord.Net.Core/Entities/IApplication.cs View File

@@ -19,6 +19,9 @@ namespace Discord
/// Gets the RPC origins of the application.
/// </summary>
IReadOnlyCollection<string> RPCOrigins { get; }
/// <summary>
/// Gets the application's public flags.
/// </summary>
ApplicationFlags Flags { get; }
/// <summary>
/// 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.
/// </summary>
ITeam Team { get; }

/// <summary>
/// Gets the partial user object containing info on the owner of the application.
/// </summary>
IUser Owner { get; }
/// <summary>
/// Gets the url of the app's terms of service.
/// </summary>
public string TermsOfService { get; }
/// <summary>
/// Gets the the url of the app's privacy policy.
/// </summary>
public string PrivacyPolicy { get; }

}
}

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

@@ -48,17 +48,119 @@ namespace Discord
/// <param name="isTTS"><see langword="true"/> if the message should be read out by a text-to-speech reader, otherwise <see langword="false"/>.</param>
/// <param name="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</param>
/// <param name="allowedMentions">The allowed mentions for this response.</param>
/// <param name="options">The request options for this response.</param>
/// <param name="components">A <see cref="MessageComponent"/> to be sent with this response.</param>
/// <param name="embed">A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored.</param>
/// <param name="options">The request options for this response.</param>
/// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
Task<IUserMessage> 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);

/// <summary>
/// Responds to this interaction with a file attachment.
/// </summary>
/// <param name="fileStream">The file to upload.</param>
/// <param name="fileName">The file name of the attachment.</param>
/// <param name="text">The text of the message to be sent.</param>
/// <param name="embeds">A array of embeds to send with this response. Max 10.</param>
/// <param name="isTTS"><see langword="true"/> if the message should be read out by a text-to-speech reader, otherwise <see langword="false"/>.</param>
/// <param name="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</param>
/// <param name="allowedMentions">The allowed mentions for this response.</param>
/// <param name="components">A <see cref="MessageComponent"/> to be sent with this response.</param>
/// <param name="embed">A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored.</param>
/// <param name="options">The request options for this response.</param>
/// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
#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
/// <summary>
/// Responds to this interaction with a file attachment.
/// </summary>
/// <param name="filePath">The file to upload.</param>
/// <param name="fileName">The file name of the attachment.</param>
/// <param name="text">The text of the message to be sent.</param>
/// <param name="embeds">A array of embeds to send with this response. Max 10.</param>
/// <param name="isTTS"><see langword="true"/> if the message should be read out by a text-to-speech reader, otherwise <see langword="false"/>.</param>
/// <param name="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</param>
/// <param name="allowedMentions">The allowed mentions for this response.</param>
/// <param name="options">The request options for this response.</param>
/// <param name="components">A <see cref="MessageComponent"/> to be sent with this response.</param>
/// <param name="embed">A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored.</param>
/// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
#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
/// <summary>
/// Responds to this interaction with a file attachment.
/// </summary>
/// <param name="attachment">The attachment containing the file and description.</param>
/// <param name="text">The text of the message to be sent.</param>
/// <param name="embeds">A array of embeds to send with this response. Max 10.</param>
/// <param name="isTTS"><see langword="true"/> if the message should be read out by a text-to-speech reader, otherwise <see langword="false"/>.</param>
/// <param name="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</param>
/// <param name="allowedMentions">The allowed mentions for this response.</param>
/// <param name="options">The request options for this response.</param>
/// <param name="components">A <see cref="MessageComponent"/> to be sent with this response.</param>
/// <param name="embed">A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored.</param>
/// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
#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
/// <summary>
/// Responds to this interaction with a collection of file attachments.
/// </summary>
/// <param name="attachments">A collection of attachments to upload.</param>
/// <param name="text">The text of the message to be sent.</param>
/// <param name="embeds">A array of embeds to send with this response. Max 10.</param>
/// <param name="isTTS"><see langword="true"/> if the message should be read out by a text-to-speech reader, otherwise <see langword="false"/>.</param>
/// <param name="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</param>
/// <param name="allowedMentions">The allowed mentions for this response.</param>
/// <param name="options">The request options for this response.</param>
/// <param name="components">A <see cref="MessageComponent"/> to be sent with this response.</param>
/// <param name="embed">A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored.</param>
/// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
Task RespondWithFilesAsync(IEnumerable<FileAttachment> 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);
/// <summary>
/// Sends a followup message for this interaction.
/// </summary>
/// <param name="text">The text of the message to be sent.</param>
@@ -75,13 +177,12 @@ namespace Discord
/// </returns>
Task<IUserMessage> 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);

/// <summary>
/// Sends a followup message for this interaction.
/// </summary>
/// <param name="text">The text of the message to be sent.</param>
/// <param name="fileStream">The file to upload.</param>
/// <param name="fileName">The file name of the attachment.</param>
/// <param name="text">The text of the message to be sent.</param>
/// <param name="embeds">A array of embeds to send with this response. Max 10.</param>
/// <param name="isTTS"><see langword="true"/> if the message should be read out by a text-to-speech reader, otherwise <see langword="false"/>.</param>
/// <param name="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</param>
@@ -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.
/// </returns>
public Task<IUserMessage> FollowupWithFileAsync(Stream fileStream, string fileName, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false,
#if NETCOREAPP3_0_OR_GREATER
async Task<IUserMessage> 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<IUserMessage> 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
/// <summary>
/// Sends a followup message for this interaction.
/// </summary>
/// <param name="text">The text of the message to be sent.</param>
/// <param name="filePath">The file to upload.</param>
/// <param name="fileName">The file name of the attachment.</param>
/// <param name="text">The text of the message to be sent.</param>
/// <param name="embeds">A array of embeds to send with this response. Max 10.</param>
/// <param name="isTTS"><see langword="true"/> if the message should be read out by a text-to-speech reader, otherwise <see langword="false"/>.</param>
/// <param name="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</param>
@@ -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.
/// </returns>
public Task<IUserMessage> 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<IUserMessage> 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<IUserMessage> 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
/// <summary>
/// Sends a followup message for this interaction.
/// </summary>
@@ -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.
/// </returns>
#if NETCOREAPP3_0_OR_GREATER
Task<IUserMessage> 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<IUserMessage> 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
/// <summary>
/// Sends a followup message for this interaction.
/// </summary>
@@ -151,14 +279,12 @@ namespace Discord
/// </returns>
Task<IUserMessage> FollowupWithFilesAsync(IEnumerable<FileAttachment> 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);

/// <summary>
/// Gets the original response for this interaction.
/// </summary>
/// <param name="options">The request options for this <see langword="async"/> request.</param>
/// <returns>A <see cref="IUserMessage"/> that represents the initial response.</returns>
Task<IUserMessage> GetOriginalResponseAsync(RequestOptions options = null);

/// <summary>
/// Edits original response for this interaction.
/// </summary>
@@ -169,7 +295,14 @@ namespace Discord
/// contains the updated message.
/// </returns>
Task<IUserMessage> ModifyOriginalResponseAsync(Action<MessageProperties> func, RequestOptions options = null);

/// <summary>
/// Deletes the original response to this interaction.
/// </summary>
/// <param name="options">The request options for this <see langword="async"/> request.</param>
/// <returns>
/// A task that represents an asynchronous deletion operation.
/// </returns>
Task DeleteOriginalResponseAsync(RequestOptions options = null);
/// <summary>
/// Acknowledges this interaction.
/// </summary>


+ 6
- 2
src/Discord.Net.Core/Entities/Messages/FileAttachment.cs View File

@@ -25,6 +25,7 @@ namespace Discord
/// <param name="stream">The stream to create the attachment from.</param>
/// <param name="fileName">The name of the attachment.</param>
/// <param name="description">The description of the attachment.</param>
/// <param name="isSpoiler">Whether or not the attachment is a spoiler.</param>
public FileAttachment(Stream stream, string fileName, string description = null, bool isSpoiler = false)
{
_isDisposed = false;
@@ -42,6 +43,9 @@ namespace Discord
/// <see cref="File.OpenRead"/>.
/// </remarks>
/// <param name="path">The path to the file.</param>
/// <param name="fileName">The name of the attachment.</param>
/// <param name="description">The description of the attachment.</param>
/// <param name="isSpoiler">Whether or not the attachment is a spoiler.</param>
/// <exception cref="System.ArgumentException">
/// <paramref name="path" /> is a zero-length string, contains only white space, or contains one or more invalid
/// characters as defined by <see cref="Path.GetInvalidPathChars"/>.
@@ -62,11 +66,11 @@ namespace Discord
/// <exception cref="FileNotFoundException">The file specified in <paramref name="path" /> was not found.
/// </exception>
/// <exception cref="IOException">An I/O error occurred while opening the file. </exception>
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;
}


+ 19
- 0
src/Discord.Net.Core/Interactions/IRestInteractionContext.cs View File

@@ -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
{
/// <summary>
/// Gets or sets the callback to use when the service has outgoing json for the rest webhook.
/// </summary>
/// <remarks>
/// If this property is <see langword="null"/> the default callback will be used.
/// </remarks>
Func<string, Task> InteractionResponseCallback { get; }
}
}

+ 84
- 0
src/Discord.Net.Interactions/Attributes/Preconditions/RequireBotPermissionAttribute.cs View File

@@ -0,0 +1,84 @@
using System;
using System.Threading.Tasks;

namespace Discord.Interactions
{
/// <summary>
/// Requires the bot to have a specific permission in the channel a command is invoked in.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class RequireBotPermissionAttribute : PreconditionAttribute
{
/// <summary>
/// Gets the specified <see cref="Discord.GuildPermission" /> of the precondition.
/// </summary>
public GuildPermission? GuildPermission { get; }
/// <summary>
/// Gets the specified <see cref="Discord.ChannelPermission" /> of the precondition.
/// </summary>
public ChannelPermission? ChannelPermission { get; }
/// <summary>
/// Gets or sets the error message if the precondition
/// fails due to being run outside of a Guild channel.
/// </summary>
public string NotAGuildErrorMessage { get; set; }

/// <summary>
/// Requires the bot account to have a specific <see cref="Discord.GuildPermission"/>.
/// </summary>
/// <remarks>
/// This precondition will always fail if the command is being invoked in a <see cref="IPrivateChannel"/>.
/// </remarks>
/// <param name="permission">
/// The <see cref="Discord.GuildPermission"/> that the bot must have. Multiple permissions can be specified
/// by ORing the permissions together.
/// </param>
public RequireBotPermissionAttribute(GuildPermission permission)
{
GuildPermission = permission;
ChannelPermission = null;
}
/// <summary>
/// Requires that the bot account to have a specific <see cref="Discord.ChannelPermission"/>.
/// </summary>
/// <param name="permission">
/// The <see cref="Discord.ChannelPermission"/> that the bot must have. Multiple permissions can be
/// specified by ORing the permissions together.
/// </param>
public RequireBotPermissionAttribute(ChannelPermission permission)
{
ChannelPermission = permission;
GuildPermission = null;
}

/// <inheritdoc />
public override async Task<PreconditionResult> 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();
}
}
}

+ 72
- 0
src/Discord.Net.Interactions/Attributes/Preconditions/RequireContextAttribute.cs View File

@@ -0,0 +1,72 @@
using System;
using System.Threading.Tasks;

namespace Discord.Interactions
{
/// <summary>
/// Defines the type of command context (i.e. where the command is being executed).
/// </summary>
[Flags]
public enum ContextType
{
/// <summary>
/// Specifies the command to be executed within a guild.
/// </summary>
Guild = 0x01,
/// <summary>
/// Specifies the command to be executed within a DM.
/// </summary>
DM = 0x02,
/// <summary>
/// Specifies the command to be executed within a group.
/// </summary>
Group = 0x04
}

/// <summary>
/// Requires the command to be invoked in a specified context (e.g. in guild, DM).
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class RequireContextAttribute : PreconditionAttribute
{
/// <summary>
/// Gets the context required to execute the command.
/// </summary>
public ContextType Contexts { get; }

/// <summary> Requires the command to be invoked in the specified context. </summary>
/// <param name="contexts">The type of context the command can be invoked in. Multiple contexts can be specified by ORing the contexts together.</param>
/// <example>
/// <code language="cs">
/// [Command("secret")]
/// [RequireContext(ContextType.DM | ContextType.Group)]
/// public Task PrivateOnlyAsync()
/// {
/// return ReplyAsync("shh, this command is a secret");
/// }
/// </code>
/// </example>
public RequireContextAttribute(ContextType contexts)
{
Contexts = contexts;
}

/// <inheritdoc />
public override Task<PreconditionResult> 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}."));
}
}
}

+ 42
- 0
src/Discord.Net.Interactions/Attributes/Preconditions/RequireNsfwAttribute.cs View File

@@ -0,0 +1,42 @@
using System;
using System.Threading.Tasks;

namespace Discord.Interactions
{
/// <summary>
/// Requires the command to be invoked in a channel marked NSFW.
/// </summary>
/// <remarks>
/// 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 <see cref="ITextChannel"/> or the
/// channel is not marked as NSFW, the precondition will fail with an erroneous <see cref="PreconditionResult"/>.
/// </remarks>
/// <example>
/// The following example restricts the command <c>too-cool</c> to an NSFW-enabled channel only.
/// <code language="cs">
/// 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.");
/// }
/// </code>
/// </example>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class RequireNsfwAttribute : PreconditionAttribute
{
/// <inheritdoc />
public override Task<PreconditionResult> 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."));
}
}
}

+ 36
- 0
src/Discord.Net.Interactions/Attributes/Preconditions/RequireOwnerAttribute.cs View File

@@ -0,0 +1,36 @@
using System;
using System.Threading.Tasks;

namespace Discord.Interactions
{
/// <summary>
/// Requires the command to be invoked by the owner of the bot.
/// </summary>
/// <remarks>
/// 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 <see cref="PreconditionResult"/> will be returned with the
/// message "Command can only be run by the owner of the bot."
/// <note>
/// This precondition will only work if the account has a <see cref="TokenType"/> of <see cref="TokenType.Bot"/>
/// ;otherwise, this precondition will always fail.
/// </note>
/// </remarks>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class RequireOwnerAttribute : PreconditionAttribute
{
/// <inheritdoc />
public override async Task<PreconditionResult> 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)}.");
}
}
}
}

+ 71
- 0
src/Discord.Net.Interactions/Attributes/Preconditions/RequireRoleAttribute.cs View File

@@ -0,0 +1,71 @@
using System;
using System.Linq;
using System.Threading.Tasks;

namespace Discord.Interactions
{
/// <summary>
/// Requires the user invoking the command to have a specified role.
/// </summary>
public class RequireRoleAttribute : PreconditionAttribute
{
/// <summary>
/// Gets the specified Role name of the precondition.
/// </summary>
public string RoleName { get; }

/// <summary>
/// Gets the specified Role ID of the precondition.
/// </summary>
public ulong? RoleId { get; }

/// <summary>
/// Gets or sets the error message if the precondition
/// fails due to being run outside of a Guild channel.
/// </summary>
public string NotAGuildErrorMessage { get; set; }

/// <summary>
/// Requires that the user invoking the command to have a specific Role.
/// </summary>
/// <param name="roleId">Id of the role that the user must have.</param>
public RequireRoleAttribute(ulong roleId)
{
RoleId = roleId;
}

/// <summary>
/// Requires that the user invoking the command to have a specific Role.
/// </summary>
/// <param name="roleName">Name of the role that the user must have.</param>
public RequireRoleAttribute(string roleName)
{
RoleName = roleName;
}

/// <inheritdoc />
public override Task<PreconditionResult> 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());
}
}
}

+ 81
- 0
src/Discord.Net.Interactions/Attributes/Preconditions/RequireUserPermissionAttribute.cs View File

@@ -0,0 +1,81 @@
using System;
using System.Threading.Tasks;

namespace Discord.Interactions
{
/// <summary>
/// Requires the user invoking the command to have a specified permission.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class RequireUserPermissionAttribute : PreconditionAttribute
{
/// <summary>
/// Gets the specified <see cref="Discord.GuildPermission" /> of the precondition.
/// </summary>
public GuildPermission? GuildPermission { get; }
/// <summary>
/// Gets the specified <see cref="Discord.ChannelPermission" /> of the precondition.
/// </summary>
public ChannelPermission? ChannelPermission { get; }
/// <summary>
/// Gets or sets the error message if the precondition
/// fails due to being run outside of a Guild channel.
/// </summary>
public string NotAGuildErrorMessage { get; set; }

/// <summary>
/// Requires that the user invoking the command to have a specific <see cref="Discord.GuildPermission"/>.
/// </summary>
/// <remarks>
/// This precondition will always fail if the command is being invoked in a <see cref="IPrivateChannel"/>.
/// </remarks>
/// <param name="permission">
/// The <see cref="Discord.GuildPermission" /> that the user must have. Multiple permissions can be
/// specified by ORing the permissions together.
/// </param>
public RequireUserPermissionAttribute(GuildPermission guildPermission)
{
GuildPermission = guildPermission;
}

/// <summary>
/// Requires that the user invoking the command to have a specific <see cref="Discord.ChannelPermission"/>.
/// </summary>
/// <param name="permission">
/// The <see cref="Discord.ChannelPermission"/> that the user must have. Multiple permissions can be
/// specified by ORing the permissions together.
/// </param>
public RequireUserPermissionAttribute(ChannelPermission channelPermission)
{
ChannelPermission = channelPermission;
}

/// <inheritdoc />
public override Task<PreconditionResult> 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());
}
}
}

+ 5
- 1
src/Discord.Net.Interactions/AutocompleteHandlers/AutocompleteHandler.cs View File

@@ -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);


+ 0
- 1
src/Discord.Net.Interactions/Discord.Net.Interactions.csproj View File

@@ -6,7 +6,6 @@
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">net6.0;net5.0;netstandard2.0;netstandard2.1</TargetFrameworks>
<RootNamespace>Discord.Interactions</RootNamespace>
<AssemblyName>Discord.Net.Interactions</AssemblyName>
<PackageId>Discord.Net.Interactions</PackageId>
<Description>A Discord.Net extension adding support for Application Commands.</Description>
</PropertyGroup>



+ 12
- 2
src/Discord.Net.Interactions/RestInteractionModuleBase.cs View File

@@ -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);
}

/// <summary>
@@ -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);
}
}
}

+ 5
- 2
src/Discord.Net.Rest/API/Common/Application.cs View File

@@ -20,14 +20,17 @@ namespace Discord.API
public bool BotRequiresCodeGrant { get; set; }
[JsonProperty("install_params")]
public Optional<InstallParams> InstallParams { get; set; }

[JsonProperty("team")]
public Team Team { get; set; }

[JsonProperty("flags"), Int53]
public Optional<ApplicationFlags> Flags { get; set; }
[JsonProperty("owner")]
public Optional<User> Owner { get; set; }
[JsonProperty("tags")]
public Optional<string[]> Tags { get; set; }
[JsonProperty("terms_of_service_url")]
public string TermsOfService { get; set; }
[JsonProperty("privacy_policy_url")]
public string PrivacyPolicy { get; set; }
}
}

+ 99
- 0
src/Discord.Net.Rest/API/Rest/UploadInteractionFileParams.cs View File

@@ -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<string> Content { get; set; }
public Optional<bool> IsTTS { get; set; }
public Optional<Embed[]> Embeds { get; set; }
public Optional<AllowedMentions> AllowedMentions { get; set; }
public Optional<ActionRowComponent[]> MessageComponents { get; set; }
public Optional<MessageFlags> 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<string, object> ToDictionary()
{
var d = new Dictionary<string, object>();

var payload = new Dictionary<string, object>();
payload["type"] = Type;

var data = new Dictionary<string, object>();
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<object> 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<string>.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;
}
}
}

+ 1
- 1
src/Discord.Net.Rest/Discord.Net.Rest.csproj View File

@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../../Discord.Net.targets" />
<Import Project="../../StyleAnalyzer.targets"/>
<Import Project="../../StyleAnalyzer.targets" />
<PropertyGroup>
<AssemblyName>Discord.Net.Rest</AssemblyName>
<RootNamespace>Discord.Rest</RootNamespace>


+ 14
- 1
src/Discord.Net.Rest/DiscordRestApiClient.cs View File

@@ -1309,7 +1309,20 @@ namespace Discord.API

options = RequestOptions.CreateOrClone(options);

await SendJsonAsync<Message>("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<Message> GetInteractionResponseAsync(string interactionToken, RequestOptions options = null)
{


+ 2
- 1
src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs View File

@@ -347,7 +347,8 @@ namespace Discord.Rest
public static Task<RestUserMessage> 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);
}

/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>


+ 10
- 2
src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs View File

@@ -34,11 +34,16 @@ namespace Discord.Rest
return client.ApiClient.BulkOverwriteGlobalApplicationCommandsAsync(Array.Empty<CreateApplicationCommandParams>(), options);
}

public static async Task<RestInteractionMessage> 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<RestInteractionMessage> 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<AutocompleteResult> result, ulong interactionId,
string interactionToken, RequestOptions options)
{


+ 17
- 4
src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs View File

@@ -260,15 +260,17 @@ namespace Discord.Rest
public abstract Task<RestFollowupMessage> FollowupWithFilesAsync(IEnumerable<FileAttachment> 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);

/// <inheritdoc/>
public Task DeleteOriginalResponseAsync(RequestOptions options = null)
=> InteractionHelper.DeleteInteractionResponseAsync(Discord, this, options);

#region IDiscordInteraction
/// <inheritdoc/>
IUser IDiscordInteraction.User => User;

/// <inheritdoc/>
Task<IUserMessage> IDiscordInteraction.RespondAsync(string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options)
{
return Task.FromResult<IUserMessage>(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));
/// <inheritdoc/>
Task IDiscordInteraction.DeferAsync(bool ephemeral, RequestOptions options)
=> Task.FromResult(Defer(ephemeral, options));
@@ -296,6 +298,17 @@ namespace Discord.Rest
/// <inheritdoc/>
async Task<IUserMessage> IDiscordInteraction.FollowupWithFilesAsync(IEnumerable<FileAttachment> 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);
/// <inheritdoc/>
Task IDiscordInteraction.RespondWithFilesAsync(IEnumerable<FileAttachment> 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.");
/// <inheritdoc/>
#if NETCOREAPP3_0_OR_GREATER != true
/// <inheritdoc/>
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.");
/// <inheritdoc/>
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.");
/// <inheritdoc/>
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
}
}

+ 0
- 14
src/Discord.Net.Rest/Entities/Messages/RestInteractionMessage.cs View File

@@ -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));
}

/// <summary>
/// Deletes this object and all of it's children.
/// </summary>


+ 6
- 2
src/Discord.Net.Rest/Entities/RestApplication.cs View File

@@ -29,10 +29,12 @@ namespace Discord.Rest
public bool BotRequiresCodeGrant { get; private set; }
/// <inheritdoc />
public ITeam Team { get; private set; }

/// <inheritdoc />
public IUser Owner { get; private set; }

/// <inheritdoc />
public string TermsOfService { get; private set; }
/// <inheritdoc />
public string PrivacyPolicy { get; private set; }
/// <inheritdoc />
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id);
/// <inheritdoc />
@@ -61,6 +63,8 @@ namespace Discord.Rest
IsBotPublic = model.IsBotPublic;
BotRequiresCodeGrant = model.BotRequiresCodeGrant;
Tags = model.Tags.GetValueOrDefault(null)?.ToImmutableArray() ?? ImmutableArray<string>.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);



+ 37
- 5
src/Discord.Net.Rest/Interactions/RestInteractionContext.cs View File

@@ -1,9 +1,12 @@
using System;
using System.Threading.Tasks;

namespace Discord.Rest
{
/// <summary>
/// Represents a Rest based context of an <see cref="IDiscordInteraction"/>.
/// </summary>
public class RestInteractionContext<TInteraction> : IInteractionContext
public class RestInteractionContext<TInteraction> : IRestInteractionContext
where TInteraction : RestInteraction
{
/// <summary>
@@ -34,6 +37,14 @@ namespace Discord.Rest
/// </summary>
public TInteraction Interaction { get; }

/// <summary>
/// Gets or sets the callback to use when the service has outgoing json for the rest webhook.
/// </summary>
/// <remarks>
/// If this property is <see langword="null"/> the default callback will be used.
/// </remarks>
public Func<string, Task> InteractionResponseCallback { get; set; }

/// <summary>
/// Initializes a new <see cref="RestInteractionContext{TInteraction}"/>.
/// </summary>
@@ -48,6 +59,18 @@ namespace Discord.Rest
Interaction = interaction;
}

/// <summary>
/// Initializes a new <see cref="RestInteractionContext{TInteraction}"/>.
/// </summary>
/// <param name="client">The underlying client.</param>
/// <param name="interaction">The underlying interaction.</param>
/// <param name="interactionResponseCallback">The callback for outgoing json.</param>
public RestInteractionContext(DiscordRestClient client, TInteraction interaction, Func<string, Task> interactionResponseCallback)
: this(client, interaction)
{
InteractionResponseCallback = interactionResponseCallback;
}

// IInterationContext
/// <inheritdoc/>
IDiscordClient IInteractionContext.Client => Client;
@@ -66,15 +89,24 @@ namespace Discord.Rest
}

/// <summary>
/// Represents a Rest based context of an <see cref="IDiscordInteraction"/>
/// Represents a Rest based context of an <see cref="IDiscordInteraction"/>.
/// </summary>
public class RestInteractionContext : RestInteractionContext<RestInteraction>
{
/// <summary>
/// Initializes a new <see cref="RestInteractionContext"/>
/// Initializes a new <see cref="RestInteractionContext"/>.
/// </summary>
/// <param name="client">The underlying client</param>
/// <param name="interaction">The underlying interaction</param>
/// <param name="client">The underlying client.</param>
/// <param name="interaction">The underlying interaction.</param>
public RestInteractionContext(DiscordRestClient client, RestInteraction interaction) : base(client, interaction) { }

/// <summary>
/// Initializes a new <see cref="RestInteractionContext"/>.
/// </summary>
/// <param name="client">The underlying client.</param>
/// <param name="interaction">The underlying interaction.</param>
/// <param name="interactionResponseCallback">The callback for outgoing json.</param>
public RestInteractionContext(DiscordRestClient client, RestInteraction interaction, Func<string, Task> interactionResponseCallback)
: base(client, interaction, interactionResponseCallback) { }
}
}

+ 2
- 1
src/Discord.Net.Rest/Net/DefaultRestClient.cs View File

@@ -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}\".");
}
}
}


+ 1
- 0
src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs View File

@@ -1,3 +1,4 @@
using Discord.API;
using Newtonsoft.Json;
using System;
#if DEBUG_LIMITS


+ 4
- 4
src/Discord.Net.WebSocket/BaseSocketClient.Events.cs View File

@@ -424,12 +424,12 @@ namespace Discord.WebSocket
}
internal readonly AsyncEvent<Func<SocketGuildUser, Task>> _userJoinedEvent = new AsyncEvent<Func<SocketGuildUser, Task>>();
/// <summary> Fired when a user leaves a guild. </summary>
public event Func<SocketUser, Task> UserLeft
public event Func<SocketGuild, SocketUser, Task> UserLeft
{
add { _userLeftEvent.Add(value); }
remove { _userLeftEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketUser, Task>> _userLeftEvent = new AsyncEvent<Func<SocketUser, Task>>();
internal readonly AsyncEvent<Func<SocketGuild, SocketUser, Task>> _userLeftEvent = new AsyncEvent<Func<SocketGuild, SocketUser, Task>>();
/// <summary> Fired when a user is banned from a guild. </summary>
public event Func<SocketUser, SocketGuild, Task> UserBanned
{
@@ -452,12 +452,12 @@ namespace Discord.WebSocket
}
internal readonly AsyncEvent<Func<SocketUser, SocketUser, Task>> _userUpdatedEvent = new AsyncEvent<Func<SocketUser, SocketUser, Task>>();
/// <summary> Fired when a guild member is updated, or a member presence is updated. </summary>
public event Func<Cacheable<SocketGuildUser, RestGuildUser, IGuildUser, ulong>, SocketGuildUser, Task> GuildMemberUpdated
public event Func<Cacheable<SocketGuildUser, ulong>, SocketGuildUser, Task> GuildMemberUpdated
{
add { _guildMemberUpdatedEvent.Add(value); }
remove { _guildMemberUpdatedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<Cacheable<SocketGuildUser, RestGuildUser, IGuildUser, ulong>, SocketGuildUser, Task>> _guildMemberUpdatedEvent = new AsyncEvent<Func<Cacheable<SocketGuildUser, RestGuildUser, IGuildUser, ulong>, SocketGuildUser, Task>>();
internal readonly AsyncEvent<Func<Cacheable<SocketGuildUser, ulong>, SocketGuildUser, Task>> _guildMemberUpdatedEvent = new AsyncEvent<Func<Cacheable<SocketGuildUser, ulong>, SocketGuildUser, Task>>();
/// <summary> Fired when a user joins, leaves, or moves voice channels. </summary>
public event Func<SocketUser, SocketVoiceState, SocketVoiceState, Task> UserVoiceStateUpdated
{


+ 1
- 1
src/Discord.Net.WebSocket/DiscordShardedClient.cs View File

@@ -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);


+ 8
- 4
src/Discord.Net.WebSocket/DiscordSocketClient.cs View File

@@ -1275,13 +1275,13 @@ namespace Discord.WebSocket
var before = user.Clone();
user.Update(State, data);

var cacheableBefore = new Cacheable<SocketGuildUser, RestGuildUser, IGuildUser, ulong>(null, user.Id, false, () => Rest.GetGuildUserAsync(guild.Id, user.Id));
var cacheableBefore = new Cacheable<SocketGuildUser, ulong>(before, user.Id, true, () => null);
await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false);
}
else
{
user = guild.AddOrUpdateUser(data);
var cacheableBefore = new Cacheable<SocketGuildUser, RestGuildUser, IGuildUser, ulong>(null, user.Id, false, () => Rest.GetGuildUserAsync(guild.Id, user.Id));
var cacheableBefore = new Cacheable<SocketGuildUser, ulong>(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
{


+ 71
- 84
src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs View File

@@ -71,8 +71,72 @@ namespace Discord.WebSocket
}
}
}
public override async Task RespondWithFilesAsync(
IEnumerable<FileAttachment> 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<Embed>();
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<string>.Unspecified,
AllowedMentions = allowedMentions != null ? allowedMentions?.ToModel() : Optional<API.AllowedMentions>.Unspecified,
Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional<API.Embed[]>.Unspecified,
IsTTS = isTTS,
MessageComponents = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional<API.ActionRowComponent[]>.Unspecified,
Flags = ephemeral ? MessageFlags.Ephemeral : Optional<MessageFlags>.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;
}

/// <inheritdoc/>
public override async Task<RestInteractionMessage> 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<MessageFlags>.Unspecified,
Components = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional<API.ActionRowComponent[]>.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;
}

/// <summary>
@@ -155,7 +208,7 @@ namespace Discord.WebSocket
/// <param name="func">A delegate containing the properties to modify the message with.</param>
/// <param name="options">The request options for this <see langword="async"/> request.</param>
/// <returns>A task that represents the asynchronous operation of updating the message.</returns>
public async Task<RestInteractionMessage> UpdateAsync(Action<MessageProperties> func, RequestOptions options = null)
public async Task UpdateAsync(Action<MessageProperties> 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;
}

/// <inheritdoc/>
@@ -281,68 +330,6 @@ namespace Discord.WebSocket
return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options);
}

/// <inheritdoc/>
public override Task<RestFollowupMessage> 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<Embed>();
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);
}

/// <inheritdoc/>
public override Task<RestFollowupMessage> 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);
}

/// <inheritdoc/>
public override Task<RestFollowupMessage> 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);
}

/// <inheritdoc/>
public override async Task<RestFollowupMessage> FollowupWithFilesAsync(
IEnumerable<FileAttachment> attachments,


+ 3
- 7
src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketAutocompleteInteraction.cs View File

@@ -89,20 +89,16 @@ namespace Discord.WebSocket
/// </returns>
public Task RespondAsync(RequestOptions options = null, params AutocompleteResult[] result)
=> RespondAsync(result, options);
public override Task<RestInteractionMessage> 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<RestFollowupMessage> 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<RestFollowupMessage> 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<RestFollowupMessage> 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<RestFollowupMessage> 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<RestFollowupMessage> FollowupWithFilesAsync(IEnumerable<FileAttachment> 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<FileAttachment> 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
/// <inheritdoc/>


+ 55
- 62
src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs View File

@@ -69,7 +69,7 @@ namespace Discord.WebSocket
}

/// <inheritdoc/>
public override async Task<RestInteractionMessage> 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;
}

/// <inheritdoc/>
public override async Task<RestFollowupMessage> FollowupAsync(
public override async Task RespondWithFilesAsync(
IEnumerable<FileAttachment> 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<Embed>();
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<string>.Unspecified,
AllowedMentions = allowedMentions?.ToModel() ?? Optional<API.AllowedMentions>.Unspecified,
AllowedMentions = allowedMentions != null ? allowedMentions?.ToModel() : Optional<API.AllowedMentions>.Unspecified,
Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional<API.Embed[]>.Unspecified,
IsTTS = isTTS,
Embeds = embeds.Select(x => x.ToModel()).ToArray(),
Components = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional<API.ActionRowComponent[]>.Unspecified
Flags = ephemeral ? MessageFlags.Ephemeral : Optional<MessageFlags>.Unspecified,
MessageComponents = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional<API.ActionRowComponent[]>.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;
}

/// <inheritdoc/>
public override Task<RestFollowupMessage> FollowupWithFileAsync(
Stream fileStream,
string fileName,
public override async Task<RestFollowupMessage> 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);
}

/// <inheritdoc/>
public override Task<RestFollowupMessage> 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<string>.Unspecified,
AllowedMentions = allowedMentions?.ToModel() ?? Optional<API.AllowedMentions>.Unspecified,
IsTTS = isTTS,
Embeds = embeds.Select(x => x.ToModel()).ToArray(),
Components = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional<API.ActionRowComponent[]>.Unspecified
};

return FollowupWithFileAsync(new FileAttachment(File.OpenRead(filePath), fileName), text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options);
}
if (ephemeral)
args.Flags = MessageFlags.Ephemeral;

/// <inheritdoc/>
public override Task<RestFollowupMessage> 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);
}
/// <inheritdoc/>
public override async Task<RestFollowupMessage> FollowupWithFilesAsync(
IEnumerable<FileAttachment> attachments,


+ 118
- 11
src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs View File

@@ -136,9 +136,97 @@ namespace Discord.WebSocket
/// <param name="options">The request options for this response.</param>
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
/// <exception cref="InvalidOperationException">The parameters provided were invalid or the token was invalid.</exception>
public abstract Task<RestInteractionMessage> 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);

/// <summary>
/// Responds to this interaction with a file attachment.
/// </summary>
/// <param name="fileStream">The file to upload.</param>
/// <param name="fileName">The file name of the attachment.</param>
/// <param name="text">The text of the message to be sent.</param>
/// <param name="embeds">A array of embeds to send with this response. Max 10.</param>
/// <param name="isTTS"><see langword="true"/> if the message should be read out by a text-to-speech reader, otherwise <see langword="false"/>.</param>
/// <param name="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</param>
/// <param name="allowedMentions">The allowed mentions for this response.</param>
/// <param name="components">A <see cref="MessageComponent"/> to be sent with this response.</param>
/// <param name="embed">A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored.</param>
/// <param name="options">The request options for this response.</param>
/// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
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);
}
}

/// <summary>
/// Responds to this interaction with a file attachment.
/// </summary>
/// <param name="filePath">The file to upload.</param>
/// <param name="fileName">The file name of the attachment.</param>
/// <param name="text">The text of the message to be sent.</param>
/// <param name="embeds">A array of embeds to send with this response. Max 10.</param>
/// <param name="isTTS"><see langword="true"/> if the message should be read out by a text-to-speech reader, otherwise <see langword="false"/>.</param>
/// <param name="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</param>
/// <param name="allowedMentions">The allowed mentions for this response.</param>
/// <param name="options">The request options for this response.</param>
/// <param name="components">A <see cref="MessageComponent"/> to be sent with this response.</param>
/// <param name="embed">A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored.</param>
/// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
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);
}
}

/// <summary>
/// Responds to this interaction with a file attachment.
/// </summary>
/// <param name="attachment">The attachment containing the file and description.</param>
/// <param name="text">The text of the message to be sent.</param>
/// <param name="embeds">A array of embeds to send with this response. Max 10.</param>
/// <param name="isTTS"><see langword="true"/> if the message should be read out by a text-to-speech reader, otherwise <see langword="false"/>.</param>
/// <param name="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</param>
/// <param name="allowedMentions">The allowed mentions for this response.</param>
/// <param name="options">The request options for this response.</param>
/// <param name="components">A <see cref="MessageComponent"/> to be sent with this response.</param>
/// <param name="embed">A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored.</param>
/// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
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);

/// <summary>
/// Responds to this interaction with a collection of file attachments.
/// </summary>
/// <param name="attachments">A collection of attachments to upload.</param>
/// <param name="text">The text of the message to be sent.</param>
/// <param name="embeds">A array of embeds to send with this response. Max 10.</param>
/// <param name="isTTS"><see langword="true"/> if the message should be read out by a text-to-speech reader, otherwise <see langword="false"/>.</param>
/// <param name="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</param>
/// <param name="allowedMentions">The allowed mentions for this response.</param>
/// <param name="options">The request options for this response.</param>
/// <param name="components">A <see cref="MessageComponent"/> to be sent with this response.</param>
/// <param name="embed">A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored.</param>
/// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
public abstract Task RespondWithFilesAsync(IEnumerable<FileAttachment> 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);

/// <summary>
/// Sends a followup message for this interaction.
/// </summary>
@@ -172,8 +260,14 @@ namespace Discord.WebSocket
/// <returns>
/// The sent message.
/// </returns>
public abstract Task<RestFollowupMessage> 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<RestFollowupMessage> 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);
}
}

/// <summary>
/// Sends a followup message for this interaction.
@@ -191,8 +285,14 @@ namespace Discord.WebSocket
/// <returns>
/// The sent message.
/// </returns>
public abstract Task<RestFollowupMessage> 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<RestFollowupMessage> 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);
}
}

/// <summary>
/// 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.
/// </returns>
public abstract Task<RestFollowupMessage> 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<RestFollowupMessage> 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);

/// <summary>
/// Sends a followup message for this interaction.
@@ -252,6 +353,10 @@ namespace Discord.WebSocket
return RestInteractionMessage.Create(Discord, model, Token, Channel);
}

/// <inheritdoc/>
public Task DeleteOriginalResponseAsync(RequestOptions options = null)
=> InteractionHelper.DeleteInteractionResponseAsync(Discord, this, options);

/// <summary>
/// Acknowledges this interaction.
/// </summary>
@@ -275,12 +380,16 @@ namespace Discord.WebSocket
async Task<IUserMessage> IDiscordInteraction.ModifyOriginalResponseAsync(Action<MessageProperties> func, RequestOptions options)
=> await ModifyOriginalResponseAsync(func, options).ConfigureAwait(false);
/// <inheritdoc/>
async Task<IUserMessage> 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);
/// <inheritdoc/>
async Task<IUserMessage> 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);
/// <inheritdoc/>
async Task<IUserMessage> IDiscordInteraction.FollowupWithFilesAsync(IEnumerable<FileAttachment> 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
/// <inheritdoc/>
async Task<IUserMessage> 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);
/// <inheritdoc/>
@@ -289,9 +398,7 @@ namespace Discord.WebSocket
/// <inheritdoc/>
async Task<IUserMessage> 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);
/// <inheritdoc/>
async Task<IUserMessage> IDiscordInteraction.FollowupWithFilesAsync(IEnumerable<FileAttachment> 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
}
}

+ 1
- 1
src/Discord.Net.Webhook/Discord.Net.Webhook.csproj View File

@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../../Discord.Net.targets" />
<Import Project="../../StyleAnalyzer.targets"/>
<Import Project="../../StyleAnalyzer.targets" />
<PropertyGroup>
<AssemblyName>Discord.Net.Webhook</AssemblyName>
<RootNamespace>Discord.Webhook</RootNamespace>


Loading…
Cancel
Save