Browse Source

Merge branch 'feature/safe-responseWithModal' of https://github.com/Cenngo/Discord.Net into feature/safe-responseWithModal

pull/2369/head
Cenngo 3 years ago
parent
commit
cbc597b082
100 changed files with 1765 additions and 327 deletions
  1. +2
    -0
      .github/FUNDING.yml
  2. +2
    -2
      .github/ISSUE_TEMPLATE/bugreport.yml
  3. +51
    -0
      CHANGELOG.md
  4. +6
    -3
      Discord.Net.targets
  5. +1
    -1
      README.md
  6. +1
    -1
      docs/docfx.json
  7. +1
    -1
      docs/guides/int_basics/modals/intro.md
  8. +2
    -0
      docs/guides/int_framework/autocompletion.md
  9. +1
    -0
      docs/guides/int_framework/intro.md
  10. +20
    -0
      docs/guides/int_framework/samples/autocompletion/autocomplete-example.cs
  11. +15
    -3
      docs/guides/int_framework/samples/intro/autocomplete.cs
  12. +3
    -3
      samples/BasicBot/_BasicBot.csproj
  13. +2
    -8
      samples/InteractionFramework/_InteractionFramework.csproj
  14. +2
    -7
      samples/ShardedClient/_ShardedClient.csproj
  15. +3
    -6
      samples/TextCommandFramework/_TextCommandFramework.csproj
  16. +2
    -2
      samples/WebhookClient/_WebhookClient.csproj
  17. +2
    -0
      src/Discord.Net.Commands/Discord.Net.Commands.csproj
  18. +3
    -3
      src/Discord.Net.Commands/Results/MatchResult.cs
  19. +2
    -0
      src/Discord.Net.Core/Discord.Net.Core.csproj
  20. +10
    -0
      src/Discord.Net.Core/DiscordConfig.cs
  21. +2
    -0
      src/Discord.Net.Core/DiscordErrorCode.cs
  22. +3
    -1
      src/Discord.Net.Core/Entities/Channels/ChannelType.cs
  23. +216
    -0
      src/Discord.Net.Core/Entities/Channels/IForumChannel.cs
  24. +11
    -0
      src/Discord.Net.Core/Entities/Channels/ITextChannel.cs
  25. +1
    -1
      src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs
  26. +42
    -0
      src/Discord.Net.Core/Entities/ForumTag.cs
  27. +0
    -1
      src/Discord.Net.Core/Entities/Guilds/IGuild.cs
  28. +1
    -1
      src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs
  29. +1
    -1
      src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs
  30. +25
    -1
      src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs
  31. +102
    -8
      src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs
  32. +4
    -4
      src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs
  33. +22
    -5
      src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs
  34. +50
    -0
      src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs
  35. +13
    -2
      src/Discord.Net.Core/Entities/Messages/MessageReference.cs
  36. +42
    -17
      src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs
  37. +1
    -1
      src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs
  38. +3
    -3
      src/Discord.Net.Core/Entities/Users/IGuildUser.cs
  39. +3
    -2
      src/Discord.Net.Core/Format.cs
  40. +1
    -1
      src/Discord.Net.Core/Utils/UrlValidation.cs
  41. +1
    -1
      src/Discord.Net.Examples/Discord.Net.Examples.csproj
  42. +3
    -3
      src/Discord.Net.Interactions/Attributes/AutocompleteAttribute.cs
  43. +0
    -2
      src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs
  44. +1
    -1
      src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs
  45. +2
    -2
      src/Discord.Net.Interactions/Attributes/Preconditions/RequireUserPermissionAttribute.cs
  46. +1
    -1
      src/Discord.Net.Interactions/Builders/Commands/SlashCommandBuilder.cs
  47. +1
    -1
      src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs
  48. +1
    -1
      src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs
  49. +2
    -1
      src/Discord.Net.Interactions/Builders/ModuleBuilder.cs
  50. +1
    -1
      src/Discord.Net.Interactions/Builders/Parameters/ParameterBuilder.cs
  51. +3
    -1
      src/Discord.Net.Interactions/Discord.Net.Interactions.csproj
  52. +1
    -1
      src/Discord.Net.Interactions/Info/ModuleInfo.cs
  53. +1
    -2
      src/Discord.Net.Interactions/InteractionContext.cs
  54. +6
    -6
      src/Discord.Net.Interactions/InteractionModuleBase.cs
  55. +140
    -36
      src/Discord.Net.Interactions/InteractionService.cs
  56. +2
    -2
      src/Discord.Net.Interactions/RestInteractionModuleBase.cs
  57. +1
    -1
      src/Discord.Net.Interactions/Results/TypeConverterResult.cs
  58. +23
    -0
      src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/NullableComponentConverter.cs
  59. +23
    -0
      src/Discord.Net.Interactions/TypeReaders/NullableReader.cs
  60. +6
    -3
      src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs
  61. +7
    -0
      src/Discord.Net.Rest/API/Common/Channel.cs
  62. +0
    -3
      src/Discord.Net.Rest/API/Common/ChannelThreads.cs
  63. +21
    -0
      src/Discord.Net.Rest/API/Common/ForumTags.cs
  64. +33
    -0
      src/Discord.Net.Rest/API/Common/ForumThreadMessage.cs
  65. +3
    -0
      src/Discord.Net.Rest/API/Common/MessageReference.cs
  66. +96
    -0
      src/Discord.Net.Rest/API/Rest/CreateMultipartPostAsync.cs
  67. +25
    -0
      src/Discord.Net.Rest/API/Rest/CreatePostParams.cs
  68. +1
    -1
      src/Discord.Net.Rest/API/Rest/UploadFileParams.cs
  69. +1
    -1
      src/Discord.Net.Rest/API/Rest/UploadInteractionFileParams.cs
  70. +1
    -1
      src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs
  71. +1
    -0
      src/Discord.Net.Rest/AssemblyInfo.cs
  72. +2
    -0
      src/Discord.Net.Rest/Discord.Net.Rest.csproj
  73. +60
    -22
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  74. +15
    -7
      src/Discord.Net.Rest/DiscordRestClient.cs
  75. +2
    -0
      src/Discord.Net.Rest/DiscordRestConfig.cs
  76. +4
    -1
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs
  77. +4
    -1
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs
  78. +4
    -1
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs
  79. +4
    -1
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs
  80. +5
    -2
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/KickAuditLogData.cs
  81. +1
    -1
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs
  82. +4
    -1
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs
  83. +5
    -1
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs
  84. +4
    -1
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs
  85. +4
    -1
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs
  86. +2
    -2
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/UnbanAuditLogData.cs
  87. +131
    -0
      src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs
  88. +1
    -0
      src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs
  89. +5
    -3
      src/Discord.Net.Rest/Entities/Channels/RestStageChannel.cs
  90. +57
    -50
      src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs
  91. +181
    -37
      src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs
  92. +138
    -0
      src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs
  93. +9
    -6
      src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs
  94. +0
    -1
      src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
  95. +4
    -4
      src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs
  96. +4
    -4
      src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBaseData.cs
  97. +20
    -6
      src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestResolvableData.cs
  98. +5
    -5
      src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommand.cs
  99. +3
    -3
      src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommandData.cs
  100. +5
    -5
      src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/UserCommands/RestUserCommand.cs

+ 2
- 0
.github/FUNDING.yml View File

@@ -1 +1,3 @@
github: quinchs
open_collective: discordnet
custom: https://paypal.me/quinchs

+ 2
- 2
.github/ISSUE_TEMPLATE/bugreport.yml View File

@@ -38,7 +38,7 @@ body:
id: description
attributes:
label: Description
description: A brief explination of the bug.
description: A brief explanation of the bug.
placeholder: When I start a DiscordSocketClient without stopping it, the gateway thread gets blocked.
validations:
required: true
@@ -62,7 +62,7 @@ body:
id: logs
attributes:
label: Logs
description: Add applicable logs and/or a stacktrace here.
description: Add applicable logs and/or a stack trace here.
validations:
required: true
- type: textarea


+ 51
- 0
CHANGELOG.md View File

@@ -1,4 +1,55 @@
# Changelog
## [3.7.2] - 2022-06-02
### Added
- #2328 Add method overloads to InteractionService (0fad3e8)
- #2336 Add support for attachments on interaction response type 7 (35db22e)
- #2338 AddOptions no longer has an uneeded restriction, added AddOptions to SlashCommandOptionBuilder (3a37f89)

### Fixed
- #2342 Disable TIV restrictions for rollout of TIV (7adf516)

## [3.7.1] - 2022-05-27
### Added
- #2325 Add missing interaction properties (d3a693a)
- #2330 Add better call control in ParseHttpInteraction (a890de9)

### Fixed
- #2329 Voice perms not retaining text perms. (712a4ae)
- #2331 NRE with Cacheable.DownloadAsync() (e1f9b76)

## [3.7.0] - 2022-05-24
### Added
- #2269 Text-In-Voice (23656e8)
- #2281 Optional API calling to RestInteraction (a24dde4)
- #2283 Support FailIfNotExists on MessageReference (0ec8938)
- #2284 Add Parse & TryParse to EmbedBuilder & Add ToJsonString extension (cea59b5)
- #2289 Add UpdateAsync to SocketModal (b333de2)
- #2291 Webhook support for threads (b0a3b65)
- #2295 Add DefaultArchiveDuration to ITextChannel (1f01881)
- #2296 Add `.With` methods to ActionRowBuilder (13ccc7c)
- #2307 Add Nullable ComponentTypeConverter and TypeReader (6fbd396)
- #2316 Forum channels (7a07fd6)

### Fixed
- #2290 Possible NRE in Sanitize (20ffa64)
- #2293 Application commands are disabled to everyone except admins by default (b465d60)
- #2299 Close-stage bucketId being null (725d255)
- #2313 Upload file size limit being incorrectly calculated (54a5af7)
- #2319 Use `IDiscordClient.GetUserAsync` impl in `DiscordSocketClient` (f47f319)
- #2320 NRE with bot scope and user parameters (88f6168)

## [3.6.1] - 2022-04-30
### Added
- #2272 add 50080 Error code (503e720)

### Fixed
- #2267 Permissions v2 Invalid Operation Exception (a8f6075)
- #2271 null user on interaction without bot scope (f2bb55e)
- #2274 Implement fix for Custom Id Segments NRE (0d74c5c)

### Misc
- 3.6.0 (27226f0)


## [3.6.0] - 2022-04-28
### Added


+ 6
- 3
Discord.Net.targets View File

@@ -1,12 +1,12 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VersionPrefix>3.6.0</VersionPrefix>
<VersionPrefix>3.7.2</VersionPrefix>
<LangVersion>latest</LangVersion>
<Authors>Discord.Net Contributors</Authors>
<PackageTags>discord;discordapp</PackageTags>
<PackageProjectUrl>https://github.com/Discord-Net/Discord.Net</PackageProjectUrl>
<PackageLicenseUrl>http://opensource.org/licenses/MIT</PackageLicenseUrl>
<PackageIconUrl>https://github.com/Discord-Net/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png</PackageIconUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>PackageLogo.png</PackageIcon>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>git://github.com/Discord-Net/Discord.Net</RepositoryUrl>
</PropertyGroup>
@@ -23,4 +23,7 @@
<WarningsAsErrors>true</WarningsAsErrors>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<None Include="../../docs/marketing/logo/PackageLogo.png" Pack="true" PackagePath=""/>
</ItemGroup>
</Project>

+ 1
- 1
README.md View File

@@ -17,7 +17,7 @@
<img src="https://discord.com/api/guilds/848176216011046962/widget.png" alt="Discord">
</a>
</p>
Discord NET is an unofficial .NET API Wrapper for the Discord client (https://discord.com).
Discord.Net is an unofficial .NET API Wrapper for the Discord client (https://discord.com).

## Documentation



+ 1
- 1
docs/docfx.json View File

@@ -60,7 +60,7 @@
"overwrite": "_overwrites/**/**.md",
"globalMetadata": {
"_appTitle": "Discord.Net Documentation",
"_appFooter": "Discord.Net (c) 2015-2022 3.6.0",
"_appFooter": "Discord.Net (c) 2015-2022 3.7.2",
"_enableSearch": true,
"_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg",
"_appFaviconPath": "favicon.ico"


+ 1
- 1
docs/guides/int_basics/modals/intro.md View File

@@ -99,7 +99,7 @@ When we run the command, our modal should pop up:
### Respond to modals

> [!WARNING]
> Modals can not be sent when respoding to a modal.
> Modals can not be sent when responding to a modal.

Once a user has submitted the modal, we need to let everyone know what
their favorite food is. We can start by hooking a task to the client's


+ 2
- 0
docs/guides/int_framework/autocompletion.md View File

@@ -18,6 +18,8 @@ AutocompleteHandlers raise the `AutocompleteHandlerExecuted` event on execution.

A valid AutocompleteHandlers must inherit [AutocompleteHandler] base type and implement all of its abstract methods.

[!code-csharp[Autocomplete Command Example](samples/autocompletion/autocomplete-example.cs)]

### GenerateSuggestionsAsync()

The Interactions Service uses this method to generate a response of an Autocomplete Interaction.


+ 1
- 0
docs/guides/int_framework/intro.md View File

@@ -86,6 +86,7 @@ By default, your methods can feature the following parameter types:
- Implementations of [IChannel]
- Implementations of [IRole]
- Implementations of [IMentionable]
- Implementations of [IAttachment]
- `string`
- `float`, `double`, `decimal`
- `bool`


+ 20
- 0
docs/guides/int_framework/samples/autocompletion/autocomplete-example.cs View File

@@ -0,0 +1,20 @@
// you need to add `Autocomplete` attribute before parameter to add autocompletion to it
[SlashCommand("command_name", "command_description")]
public async Task ExampleCommand([Summary("parameter_name"), Autocomplete(typeof(ExampleAutocompleteHandler))] string parameterWithAutocompletion)
=> await RespondAsync($"Your choice: {parameterWithAutocompletion}");

public class ExampleAutocompleteHandler : AutocompleteHandler
{
public override async Task<AutocompletionResult> GenerateSuggestionsAsync(IInteractionContext context, IAutocompleteInteraction autocompleteInteraction, IParameterInfo parameter, IServiceProvider services)
{
// Create a collection with suggestions for autocomplete
IEnumerable<AutocompleteResult> results = new[]
{
new AutocompleteResult("Name1", "value111"),
new AutocompleteResult("Name2", "value2")
};

// max - 25 suggestions at a time (API limit)
return AutocompletionResult.FromSuccess(results.Take(25));
}
}

+ 15
- 3
docs/guides/int_framework/samples/intro/autocomplete.cs View File

@@ -1,9 +1,21 @@
[AutocompleteCommand("parameter_name", "command_name")]
public async Task Autocomplete()
{
IEnumerable<AutocompleteResult> results;
string userInput = (Context.Interaction as SocketAutocompleteInteraction).Data.Current.Value.ToString();

...
IEnumerable<AutocompleteResult> results = new[]
{
new AutocompleteResult("foo", "foo_value"),
new AutocompleteResult("bar", "bar_value"),
new AutocompleteResult("baz", "baz_value"),
}.Where(x => x.Name.StartsWith(userInput, StringComparison.InvariantCultureIgnoreCase)); // only send suggestions that starts with user's input; use case insensitive matching

await (Context.Interaction as SocketAutocompleteInteraction).RespondAsync(results);

// max - 25 suggestions at a time
await (Context.Interaction as SocketAutocompleteInteraction).RespondAsync(results.Take(25));
}

// you need to add `Autocomplete` attribute before parameter to add autocompletion to it
[SlashCommand("command_name", "command_description")]
public async Task ExampleCommand([Summary("parameter_name"), Autocomplete] string parameterWithAutocompletion)
=> await RespondAsync($"Your choice: {parameterWithAutocompletion}");

+ 3
- 3
samples/BasicBot/_BasicBot.csproj View File

@@ -1,12 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" />
<PackageReference Include="Discord.Net.WebSocket" Version="3.6.1"/>
</ItemGroup>

</Project>

+ 2
- 8
samples/InteractionFramework/_InteractionFramework.csproj View File

@@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>InteractionFramework</RootNamespace>
<StartupObject></StartupObject>
</PropertyGroup>
@@ -13,13 +13,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Discord.Net.Core\Discord.Net.Core.csproj" />
<ProjectReference Include="..\..\src\Discord.Net.Interactions\Discord.Net.Interactions.csproj" />
<ProjectReference Include="..\..\src\Discord.Net.Rest\Discord.Net.Rest.csproj" />
<ProjectReference Include="..\..\src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" />
<PackageReference Include="Discord.Net.Interactions" Version="3.6.1" />
</ItemGroup>

</Project>

+ 2
- 7
samples/ShardedClient/_ShardedClient.csproj View File

@@ -2,18 +2,13 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>ShardedClient</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Discord.Net.Commands\Discord.Net.Commands.csproj" />
<ProjectReference Include="..\..\src\Discord.Net.Interactions\Discord.Net.Interactions.csproj" />
<ProjectReference Include="..\..\src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" />
<PackageReference Include="Discord.Net" Version="3.6.1" />
</ItemGroup>

</Project>

+ 3
- 6
samples/TextCommandFramework/_TextCommandFramework.csproj View File

@@ -2,17 +2,14 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>TextCommandFramework</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Discord.Net.Commands\Discord.Net.Commands.csproj" />
<ProjectReference Include="..\..\src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" />
<PackageReference Include="Discord.Net.Commands" Version="3.6.1" />
<PackageReference Include="Discord.Net.Websocket" Version="3.6.1" />
</ItemGroup>

</Project>

+ 2
- 2
samples/WebhookClient/_WebhookClient.csproj View File

@@ -2,12 +2,12 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>WebHookClient</RootNamespace>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Discord.Net.Webhook\Discord.Net.Webhook.csproj" />
<PackageReference Include="Discord.Net.Webhook" Version="3.6.1" />
</ItemGroup>

</Project>

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

@@ -7,6 +7,8 @@
<Description>A Discord.Net extension adding support for bot commands.</Description>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">net6.0;net5.0;net461;netstandard2.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">net6.0;net5.0;netstandard2.0;netstandard2.1</TargetFrameworks>
<WarningLevel>5</WarningLevel>
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" />


+ 3
- 3
src/Discord.Net.Commands/Results/MatchResult.cs View File

@@ -1,4 +1,4 @@
using System;
using System;

namespace Discord.Commands
{
@@ -12,7 +12,7 @@ namespace Discord.Commands
/// <summary>
/// Gets on which pipeline stage the command may have matched or failed.
/// </summary>
public IResult? Pipeline { get; }
public IResult Pipeline { get; }

/// <inheritdoc />
public CommandError? Error { get; }
@@ -21,7 +21,7 @@ namespace Discord.Commands
/// <inheritdoc />
public bool IsSuccess => !Error.HasValue;

private MatchResult(CommandMatch? match, IResult? pipeline, CommandError? error, string errorReason)
private MatchResult(CommandMatch? match, IResult pipeline, CommandError? error, string errorReason)
{
Match = match;
Error = error;


+ 2
- 0
src/Discord.Net.Core/Discord.Net.Core.csproj View File

@@ -7,6 +7,8 @@
<Description>The core components for the Discord.Net library.</Description>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">net6.0;net5.0;net461;netstandard2.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">net6.0;net5.0;netstandard2.0;netstandard2.1</TargetFrameworks>
<WarningLevel>5</WarningLevel>
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />


+ 10
- 0
src/Discord.Net.Core/DiscordConfig.cs View File

@@ -132,6 +132,16 @@ namespace Discord
/// </returns>
public const int MaxAuditLogEntriesPerBatch = 100;

/// <summary>
/// Returns the max number of stickers that can be sent with a message.
/// </summary>
public const int MaxStickersPerMessage = 3;

/// <summary>
/// Returns the max number of embeds that can be sent with a message.
/// </summary>
public const int MaxEmbedsPerMessage = 10;

/// <summary>
/// Gets or sets how a request should act in the case of an error, by default.
/// </summary>


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

@@ -152,6 +152,7 @@ namespace Discord
InvalidMessageType = 50068,
PaymentSourceRequiredForGift = 50070,
CannotDeleteRequiredCommunityChannel = 50074,
CannotEditStickersWithinAMessage = 50080,
InvalidSticker = 50081,
CannotExecuteOnArchivedThread = 50083,
InvalidThreadNotificationSettings = 50084,
@@ -164,6 +165,7 @@ namespace Discord
#endregion

#region 2FA (60XXX)
MissingPermissionToSendThisSticker = 50600,
Requires2FA = 60003,
#endregion



+ 3
- 1
src/Discord.Net.Core/Entities/Channels/ChannelType.cs View File

@@ -26,6 +26,8 @@ namespace Discord
/// <summary> The channel is a stage voice channel. </summary>
Stage = 13,
/// <summary> The channel is a guild directory used in hub servers. (Unreleased)</summary>
GuildDirectory = 14
GuildDirectory = 14,
/// <summary> The channel is a forum channel containing multiple threads. </summary>
Forum = 15
}
}

+ 216
- 0
src/Discord.Net.Core/Entities/Channels/IForumChannel.cs View File

@@ -0,0 +1,216 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
public interface IForumChannel : IGuildChannel, IMentionable
{
/// <summary>
/// Gets a value that indicates whether the channel is NSFW.
/// </summary>
/// <returns>
/// <c>true</c> if the channel has the NSFW flag enabled; otherwise <c>false</c>.
/// </returns>
bool IsNsfw { get; }

/// <summary>
/// Gets the current topic for this text channel.
/// </summary>
/// <returns>
/// A string representing the topic set in the channel; <c>null</c> if none is set.
/// </returns>
string Topic { get; }

/// <summary>
/// Gets the default archive duration for a newly created post.
/// </summary>
ThreadArchiveDuration DefaultAutoArchiveDuration { get; }

/// <summary>
/// Gets a collection of tags inside of this forum channel.
/// </summary>
IReadOnlyCollection<ForumTag> Tags { get; }

/// <summary>
/// Creates a new post (thread) within the forum.
/// </summary>
/// <param name="title">The title of the post.</param>
/// <param name="archiveDuration">The archive duration of the post.</param>
/// <param name="slowmode">The slowmode for the posts thread.</param>
/// <param name="text">The message to be sent.</param>
/// <param name="embed">The <see cref="Discord.EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <param name="allowedMentions">
/// Specifies if notifications are sent for mentioned users and roles in the message <paramref name="text"/>.
/// If <c>null</c>, all mentioned roles and users will be notified.
/// </param>
/// <param name="components">The message components to be included with this message. Used for interactions.</param>
/// <param name="stickers">A collection of stickers to send with the message.</param>
/// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param>
/// <param name="flags">A message flag to be applied to the sent message, only <see cref="MessageFlags.SuppressEmbeds"/> is permitted.</param>
/// <returns>
/// A task that represents the asynchronous creation operation.
/// </returns>
Task<IThreadChannel> CreatePostAsync(string title, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null,
string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null,
MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None);

/// <summary>
/// Creates a new post (thread) within the forum.
/// </summary>
/// <param name="title">The title of the post.</param>
/// <param name="archiveDuration">The archive duration of the post.</param>
/// <param name="slowmode">The slowmode for the posts thread.</param>
/// <param name="filePath">The file path of the file.</param>
/// <param name="text">The message to be sent.</param>
/// <param name="embed">The <see cref="Discord.EmbedType.Rich" /> <see cref="Embed" /> to be sent.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <param name="isSpoiler">Whether the message attachment should be hidden as a spoiler.</param>
/// <param name="allowedMentions">
/// Specifies if notifications are sent for mentioned users and roles in the message <paramref name="text"/>.
/// If <c>null</c>, all mentioned roles and users will be notified.
/// </param>
/// <param name="components">The message components to be included with this message. Used for interactions.</param>
/// <param name="stickers">A collection of stickers to send with the file.</param>
/// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param>
/// <param name="flags">A message flag to be applied to the sent message, only <see cref="MessageFlags.SuppressEmbeds"/> is permitted.</param>
/// <returns>
/// A task that represents the asynchronous creation operation.
/// </returns>
Task<IThreadChannel> CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay,
int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false,
AllowedMentions allowedMentions = null, MessageComponent components = null,
ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None);

/// <summary>
/// Creates a new post (thread) within the forum.
/// </summary>
/// <param name="title">The title of the post.</param>
/// <param name="stream">The <see cref="Stream" /> of the file to be sent.</param>
/// <param name="filename">The name of the attachment.</param>
/// <param name="archiveDuration">The archive duration of the post.</param>
/// <param name="slowmode">The slowmode for the posts thread.</param>
/// <param name="text">The message to be sent.</param>
/// <param name="embed">The <see cref="Discord.EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <param name="isSpoiler">Whether the message attachment should be hidden as a spoiler.</param>
/// <param name="allowedMentions">
/// Specifies if notifications are sent for mentioned users and roles in the message <paramref name="text"/>.
/// If <c>null</c>, all mentioned roles and users will be notified.
/// </param>
/// <param name="components">The message components to be included with this message. Used for interactions.</param>
/// <param name="stickers">A collection of stickers to send with the file.</param>
/// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param>
/// <param name="flags">A message flag to be applied to the sent message, only <see cref="MessageFlags.SuppressEmbeds"/> is permitted.</param>
/// <returns>
/// A task that represents the asynchronous creation operation.
/// </returns>
public Task<IThreadChannel> CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay,
int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false,
AllowedMentions allowedMentions = null, MessageComponent components = null,
ISticker[] stickers = null, Embed[] embeds = null,MessageFlags flags = MessageFlags.None);

/// <summary>
/// Creates a new post (thread) within the forum.
/// </summary>
/// <param name="title">The title of the post.</param>
/// <param name="attachment">The attachment containing the file and description.</param>
/// <param name="archiveDuration">The archive duration of the post.</param>
/// <param name="slowmode">The slowmode for the posts thread.</param>
/// <param name="text">The message to be sent.</param>
/// <param name="embed">The <see cref="Discord.EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <param name="allowedMentions">
/// Specifies if notifications are sent for mentioned users and roles in the message <paramref name="text"/>.
/// If <c>null</c>, all mentioned roles and users will be notified.
/// </param>
/// <param name="components">The message components to be included with this message. Used for interactions.</param>
/// <param name="stickers">A collection of stickers to send with the file.</param>
/// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param>
/// <param name="flags">A message flag to be applied to the sent message, only <see cref="MessageFlags.SuppressEmbeds"/> is permitted.</param>
/// <returns>
/// A task that represents the asynchronous creation operation.
/// </returns>
public Task<IThreadChannel> CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay,
int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null,
MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None);

/// <summary>
/// Creates a new post (thread) within the forum.
/// </summary>
/// <param name="title">The title of the post.</param>
/// <param name="attachments">A collection of attachments to upload.</param>
/// <param name="archiveDuration">The archive duration of the post.</param>
/// <param name="slowmode">The slowmode for the posts thread.</param>
/// <param name="text">The message to be sent.</param>
/// <param name="embed">The <see cref="Discord.EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <param name="allowedMentions">
/// Specifies if notifications are sent for mentioned users and roles in the message <paramref name="text"/>.
/// If <c>null</c>, all mentioned roles and users will be notified.
/// </param>
/// <param name="components">The message components to be included with this message. Used for interactions.</param>
/// <param name="stickers">A collection of stickers to send with the file.</param>
/// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param>
/// <param name="flags">A message flag to be applied to the sent message, only <see cref="MessageFlags.SuppressEmbeds"/> is permitted.</param>
/// <returns>
/// A task that represents the asynchronous creation operation.
/// </returns>
public Task<IThreadChannel> CreatePostWithFilesAsync(string title, IEnumerable<FileAttachment> attachments, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay,
int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null,
MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None);

/// <summary>
/// Gets a collection of active threads within this forum channel.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents an asynchronous get operation for retrieving the threads. The task result contains
/// a collection of active threads.
/// </returns>
Task<IReadOnlyCollection<IThreadChannel>> GetActiveThreadsAsync(RequestOptions options = null);

/// <summary>
/// Gets a collection of publicly archived threads within this forum channel.
/// </summary>
/// <param name="limit">The optional limit of how many to get.</param>
/// <param name="before">The optional date to return threads created before this timestamp.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents an asynchronous get operation for retrieving the threads. The task result contains
/// a collection of publicly archived threads.
/// </returns>
Task<IReadOnlyCollection<IThreadChannel>> GetPublicArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null);

/// <summary>
/// Gets a collection of privately archived threads within this forum channel.
/// </summary>
/// <remarks>
/// The bot requires the <see cref="GuildPermission.ManageThreads"/> permission in order to execute this request.
/// </remarks>
/// <param name="limit">The optional limit of how many to get.</param>
/// <param name="before">The optional date to return threads created before this timestamp.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents an asynchronous get operation for retrieving the threads. The task result contains
/// a collection of privately archived threads.
/// </returns>
Task<IReadOnlyCollection<IThreadChannel>> GetPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null);

/// <summary>
/// Gets a collection of privately archived threads that the current bot has joined within this forum channel.
/// </summary>
/// <param name="limit">The optional limit of how many to get.</param>
/// <param name="before">The optional date to return threads created before this timestamp.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents an asynchronous get operation for retrieving the threads. The task result contains
/// a collection of privately archived threads.
/// </returns>
Task<IReadOnlyCollection<IThreadChannel>> GetJoinedPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null);
}
}

+ 11
- 0
src/Discord.Net.Core/Entities/Channels/ITextChannel.cs View File

@@ -35,6 +35,17 @@ namespace Discord
/// </returns>
int SlowModeInterval { get; }

/// <summary>
/// Gets the default auto-archive duration for client-created threads in this channel.
/// </summary>
/// <remarks>
/// The value of this property does not affect API thread creation, it will not respect this value.
/// </remarks>
/// <returns>
/// The default auto-archive duration for thread creation in this channel.
/// </returns>
ThreadArchiveDuration DefaultArchiveDuration { get; }

/// <summary>
/// Bulk-deletes multiple messages.
/// </summary>


+ 1
- 1
src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs View File

@@ -6,7 +6,7 @@ namespace Discord
/// <summary>
/// Represents a generic voice channel in a guild.
/// </summary>
public interface IVoiceChannel : INestedChannel, IAudioChannel, IMentionable
public interface IVoiceChannel : IMessageChannel, INestedChannel, IAudioChannel, IMentionable
{
/// <summary>
/// Gets the bit-rate that the clients in this voice channel are requested to use.


+ 42
- 0
src/Discord.Net.Core/Entities/ForumTag.cs View File

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

namespace Discord
{
/// <summary>
/// A struct representing a forum channel tag.
/// </summary>
public struct ForumTag
{
/// <summary>
/// Gets the Id of the tag.
/// </summary>
public ulong Id { get; }

/// <summary>
/// Gets the name of the tag.
/// </summary>
public string Name { get; }

/// <summary>
/// Gets the emoji of the tag or <see langword="null"/> if none is set.
/// </summary>
public IEmote Emoji { get; }

internal ForumTag(ulong id, string name, ulong? emojiId, string emojiName)
{
if (emojiId.HasValue && emojiId.Value != 0)
Emoji = new Emote(emojiId.Value, emojiName, false);
else if (emojiName != null)
Emoji = new Emoji(name);
else
Emoji = null;

Id = id;
Name = name;
}
}
}

+ 0
- 1
src/Discord.Net.Core/Entities/Guilds/IGuild.cs View File

@@ -1173,7 +1173,6 @@ namespace Discord
/// in order to use this property.
/// </remarks>
/// </param>
/// <param name="speakers">A collection of speakers for the event.</param>
/// <param name="location">The location of the event; links are supported</param>
/// <param name="coverImage">The optional banner image for the event.</param>
/// <param name="options">The options to be used when sending the request.</param>


+ 1
- 1
src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs View File

@@ -89,7 +89,7 @@ namespace Discord
/// Gets this events banner image url.
/// </summary>
/// <param name="format">The format to return.</param>
/// <param name="size">The size of the image to return in. This can be any power of two between 16 and 2048.
/// <param name="size">The size of the image to return in. This can be any power of two between 16 and 2048.</param>
/// <returns>The cover images url.</returns>
string GetCoverImageUrl(ImageFormat format = ImageFormat.Auto, ushort size = 1024);



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

@@ -56,7 +56,7 @@ namespace Discord
Number = 10,

/// <summary>
/// A <see cref="Discord.Attachment"/>.
/// A <see cref="IAttachment"/>.
/// </summary>
Attachment = 11
}


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

@@ -52,10 +52,13 @@ namespace Discord
/// <summary>
/// Gets the preferred locale of the invoking User.
/// </summary>
/// <remarks>
/// This property returns <see langword="null"/> if the interaction is a REST ping interaction.
/// </remarks>
string UserLocale { get; }

/// <summary>
/// Gets the preferred locale of the guild this interaction was executed in. <see cref="null"/> if not executed in a guild.
/// Gets the preferred locale of the guild this interaction was executed in. <see langword="null"/> if not executed in a guild.
/// </summary>
/// <remarks>
/// Non-community guilds (With no locale setting available) will have en-US as the default value sent by Discord.
@@ -67,6 +70,27 @@ namespace Discord
/// </summary>
bool IsDMInteraction { get; }

/// <summary>
/// Gets the ID of the channel this interaction was executed in.
/// </summary>
/// <remarks>
/// This property returns <see langword="null"/> if the interaction is a REST ping interaction.
/// </remarks>
ulong? ChannelId { get; }

/// <summary>
/// Gets the ID of the guild this interaction was executed in.
/// </summary>
/// <remarks>
/// This property returns <see langword="null"/> if the interaction was not executed in a guild.
/// </remarks>
ulong? GuildId { get; }

/// <summary>
/// Gets the ID of the application this interaction is for.
/// </summary>
ulong ApplicationId { get; }

/// <summary>
/// Responds to an Interaction with type <see cref="InteractionResponseType.ChannelMessageWithSource"/>.
/// </summary>


+ 102
- 8
src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs View File

@@ -195,7 +195,7 @@ namespace Discord
/// </summary>
/// <param name="button">The button to add.</param>
/// <param name="row">The row to add the button.</param>
/// <exception cref="InvalidOperationException">There is no more row to add a menu.</exception>
/// <exception cref="InvalidOperationException">There is no more row to add a button.</exception>
/// <exception cref="ArgumentException"><paramref name="row"/> must be less than <see cref="MaxActionRowCount"/>.</exception>
/// <returns>The current builder.</returns>
public ComponentBuilder WithButton(ButtonBuilder button, int row = 0)
@@ -348,6 +348,100 @@ namespace Discord
return this;
}

/// <summary>
/// Adds a <see cref="SelectMenuBuilder"/> to the <see cref="ActionRowBuilder"/>.
/// </summary>
/// <param name="customId">The custom id of the menu.</param>
/// <param name="options">The options of the menu.</param>
/// <param name="placeholder">The placeholder of the menu.</param>
/// <param name="minValues">The min values of the placeholder.</param>
/// <param name="maxValues">The max values of the placeholder.</param>
/// <param name="disabled">Whether or not the menu is disabled.</param>
/// <returns>The current builder.</returns>
public ActionRowBuilder WithSelectMenu(string customId, List<SelectMenuOptionBuilder> options,
string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false)
{
return WithSelectMenu(new SelectMenuBuilder()
.WithCustomId(customId)
.WithOptions(options)
.WithPlaceholder(placeholder)
.WithMaxValues(maxValues)
.WithMinValues(minValues)
.WithDisabled(disabled));
}

/// <summary>
/// Adds a <see cref="SelectMenuBuilder"/> to the <see cref="ActionRowBuilder"/>.
/// </summary>
/// <param name="menu">The menu to add.</param>
/// <exception cref="InvalidOperationException">A Select Menu cannot exist in a pre-occupied ActionRow.</exception>
/// <returns>The current builder.</returns>
public ActionRowBuilder WithSelectMenu(SelectMenuBuilder menu)
{
if (menu.Options.Distinct().Count() != menu.Options.Count)
throw new InvalidOperationException("Please make sure that there is no duplicates values.");

var builtMenu = menu.Build();

if (Components.Count != 0)
throw new InvalidOperationException($"A Select Menu cannot exist in a pre-occupied ActionRow.");

AddComponent(builtMenu);

return this;
}

/// <summary>
/// Adds a <see cref="ButtonBuilder"/> with specified parameters to the <see cref="ActionRowBuilder"/>.
/// </summary>
/// <param name="label">The label text for the newly added button.</param>
/// <param name="style">The style of this newly added button.</param>
/// <param name="emote">A <see cref="IEmote"/> to be used with this button.</param>
/// <param name="customId">The custom id of the newly added button.</param>
/// <param name="url">A URL to be used only if the <see cref="ButtonStyle"/> is a Link.</param>
/// <param name="disabled">Whether or not the newly created button is disabled.</param>
/// <returns>The current builder.</returns>
public ActionRowBuilder WithButton(
string label = null,
string customId = null,
ButtonStyle style = ButtonStyle.Primary,
IEmote emote = null,
string url = null,
bool disabled = false)
{
var button = new ButtonBuilder()
.WithLabel(label)
.WithStyle(style)
.WithEmote(emote)
.WithCustomId(customId)
.WithUrl(url)
.WithDisabled(disabled);

return WithButton(button);
}

/// <summary>
/// Adds a <see cref="ButtonBuilder"/> to the <see cref="ActionRowBuilder"/>.
/// </summary>
/// <param name="button">The button to add.</param>
/// <exception cref="InvalidOperationException">Components count reached <see cref="MaxChildCount"/>.</exception>
/// <exception cref="InvalidOperationException">A button cannot be added to a row with a SelectMenu.</exception>
/// <returns>The current builder.</returns>
public ActionRowBuilder WithButton(ButtonBuilder button)
{
var builtButton = button.Build();

if(Components.Count >= 5)
throw new InvalidOperationException($"Components count reached {MaxChildCount}");

if (Components.Any(x => x.Type == ComponentType.SelectMenu))
throw new InvalidOperationException($"A button cannot be added to a row with a SelectMenu");

AddComponent(builtButton);

return this;
}

/// <summary>
/// Builds the current builder to a <see cref="ActionRowComponent"/> that can be used within a <see cref="ComponentBuilder"/>
/// </summary>
@@ -1194,9 +1288,9 @@ namespace Discord
/// <summary>
/// Gets or sets the default value of the text input.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException"><see cref="Value.Length"/> is less than 0.</exception>
/// <exception cref="ArgumentOutOfRangeException"><see cref="Value"/>.Length is less than 0.</exception>
/// <exception cref="ArgumentOutOfRangeException">
/// <see cref="Value.Length"/> is greater than <see cref="LargestMaxLength"/> or <see cref="MaxLength"/>.
/// <see cref="Value"/>.Length is greater than <see cref="LargestMaxLength"/> or <see cref="MaxLength"/>.
/// </exception>
public string Value
{
@@ -1227,7 +1321,7 @@ namespace Discord
/// <param name="minLength">The text input's minimum length.</param>
/// <param name="maxLength">The text input's maximum length.</param>
/// <param name="required">The text input's required value.</param>
public TextInputBuilder (string label, string customId, TextInputStyle style = TextInputStyle.Short, string placeholder = null,
public TextInputBuilder(string label, string customId, TextInputStyle style = TextInputStyle.Short, string placeholder = null,
int? minLength = null, int? maxLength = null, bool? required = null, string value = null)
{
Label = label;
@@ -1291,7 +1385,7 @@ namespace Discord
Placeholder = placeholder;
return this;
}
/// <summary>
/// Sets the value of the current builder.
/// </summary>
@@ -1306,18 +1400,18 @@ namespace Discord
/// <summary>
/// Sets the minimum length of the current builder.
/// </summary>
/// <param name="placeholder">The value to set.</param>
/// <param name="minLength">The value to set.</param>
/// <returns>The current builder. </returns>
public TextInputBuilder WithMinLength(int minLength)
{
MinLength = minLength;
return this;
}
/// <summary>
/// Sets the maximum length of the current builder.
/// </summary>
/// <param name="placeholder">The value to set.</param>
/// <param name="maxLength">The value to set.</param>
/// <returns>The current builder. </returns>
public TextInputBuilder WithMaxLength(int maxLength)
{


+ 4
- 4
src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs View File

@@ -64,18 +64,18 @@ namespace Discord
/// <summary>
/// Sets the custom id of the current modal.
/// </summary>
/// <param name="title">The value to set the custom id to.</param>
/// <param name="customId">The value to set the custom id to.</param>
/// <returns>The current builder.</returns>
public ModalBuilder WithCustomId(string customId)
{
CustomId = customId;
return this;
}
/// <summary>
/// Adds a component to the current builder.
/// </summary>
/// <param name="title">The component to add.</param>
/// <param name="component">The component to add.</param>
/// <returns>The current builder.</returns>
public ModalBuilder AddTextInput(TextInputBuilder component)
{
@@ -213,7 +213,7 @@ namespace Discord
/// Adds a <see cref="TextInputBuilder"/> to the <see cref="ModalComponentBuilder"/> at the specific row.
/// If the row cannot accept the component then it will add it to a row that can.
/// </summary>
/// <param name="text">The <see cref="TextInputBuilder"> to add.</param>
/// <param name="text">The <see cref="TextInputBuilder"/> to add.</param>
/// <param name="row">The row to add the text input.</param>
/// <exception cref="InvalidOperationException">There are no more rows to add a text input to.</exception>
/// <exception cref="ArgumentException"><paramref name="row"/> must be less than <see cref="MaxActionRowCount"/>.</exception>


+ 22
- 5
src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs View File

@@ -255,9 +255,6 @@ namespace Discord
if (options == null)
throw new ArgumentNullException(nameof(options), "Options cannot be null!");

if (options.Length == 0)
throw new ArgumentException("Options cannot be empty!", nameof(options));

Options ??= new List<SlashCommandOptionBuilder>();

if (Options.Count + options.Length > MaxOptionsCount)
@@ -409,7 +406,7 @@ namespace Discord
MinValue = MinValue,
MaxValue = MaxValue
};
}
}

/// <summary>
/// Adds an option to the current slash command.
@@ -477,6 +474,26 @@ namespace Discord
return this;
}

/// <summary>
/// Adds a collection of options to the current option.
/// </summary>
/// <param name="options">The collection of options to add.</param>
/// <returns>The current builder.</returns>
public SlashCommandOptionBuilder AddOptions(params SlashCommandOptionBuilder[] options)
{
if (options == null)
throw new ArgumentNullException(nameof(options), "Options cannot be null!");

if ((Options?.Count ?? 0) + options.Length > SlashCommandBuilder.MaxOptionsCount)
throw new ArgumentOutOfRangeException(nameof(options), $"There can only be {SlashCommandBuilder.MaxOptionsCount} options per sub command group!");

foreach (var option in options)
Preconditions.Options(option.Name, option.Description);

Options.AddRange(options);
return this;
}

/// <summary>
/// Adds a choice to the current option.
/// </summary>
@@ -640,7 +657,7 @@ namespace Discord
MinValue = value;
return this;
}
/// <summary>
/// Sets the current builders max value field.
/// </summary>


+ 50
- 0
src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Discord.Utils;
using Newtonsoft.Json;

namespace Discord
{
@@ -155,6 +156,55 @@ namespace Discord
}
}

/// <summary>
/// Tries to parse a string into an <see cref="EmbedBuilder"/>.
/// </summary>
/// <param name="json">The json string to parse.</param>
/// <param name="builder">The <see cref="EmbedBuilder"/> with populated values. An empty instance if method returns <see langword="false"/>.</param>
/// <returns><see langword="true"/> if <paramref name="json"/> was succesfully parsed. <see langword="false"/> if not.</returns>
public static bool TryParse(string json, out EmbedBuilder builder)
{
builder = new EmbedBuilder();
try
{
var model = JsonConvert.DeserializeObject<Embed>(json);

if (model is not null)
{
builder = model.ToEmbedBuilder();
return true;
}
return false;
}
catch
{
return false;
}
}

/// <summary>
/// Parses a string into an <see cref="EmbedBuilder"/>.
/// </summary>
/// <param name="json">The json string to parse.</param>
/// <returns>An <see cref="EmbedBuilder"/> with populated values from the passed <paramref name="json"/>.</returns>
/// <exception cref="InvalidOperationException">Thrown if the string passed is not valid json.</exception>
public static EmbedBuilder Parse(string json)
{
try
{
var model = JsonConvert.DeserializeObject<Embed>(json);

if (model is not null)
return model.ToEmbedBuilder();

return new EmbedBuilder();
}
catch
{
throw;
}
}

/// <summary>
/// Sets the title of an <see cref="Embed"/>.
/// </summary>


+ 13
- 2
src/Discord.Net.Core/Entities/Messages/MessageReference.cs View File

@@ -27,6 +27,12 @@ namespace Discord
/// </summary>
public Optional<ulong> GuildId { get; internal set; }

/// <summary>
/// Gets whether to error if the referenced message doesn't exist instead of sending as a normal (non-reply) message
/// Defaults to true.
/// </summary>
public Optional<bool> FailIfNotExists { get; internal set; }

/// <summary>
/// Initializes a new instance of the <see cref="MessageReference"/> class.
/// </summary>
@@ -39,16 +45,21 @@ namespace Discord
/// <param name="guildId">
/// The ID of the guild that will be referenced. It will be validated if sent.
/// </param>
public MessageReference(ulong? messageId = null, ulong? channelId = null, ulong? guildId = null)
/// <param name="failIfNotExists">
/// Whether to error if the referenced message doesn't exist instead of sending as a normal (non-reply) message. Defaults to true.
/// </param>
public MessageReference(ulong? messageId = null, ulong? channelId = null, ulong? guildId = null, bool? failIfNotExists = null)
{
MessageId = messageId ?? Optional.Create<ulong>();
InternalChannelId = channelId ?? Optional.Create<ulong>();
GuildId = guildId ?? Optional.Create<ulong>();
FailIfNotExists = failIfNotExists ?? Optional.Create<bool>();
}

private string DebuggerDisplay
=> $"Channel ID: ({ChannelId}){(GuildId.IsSpecified ? $", Guild ID: ({GuildId.Value})" : "")}" +
$"{(MessageId.IsSpecified ? $", Message ID: ({MessageId.Value})" : "")}";
$"{(MessageId.IsSpecified ? $", Message ID: ({MessageId.Value})" : "")}" +
$"{(FailIfNotExists.IsSpecified ? $", FailIfNotExists: ({FailIfNotExists.Value})" : "")}";

public override string ToString()
=> DebuggerDisplay;


+ 42
- 17
src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs View File

@@ -7,30 +7,55 @@ namespace Discord
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public struct ChannelPermissions
{
/// <summary> Gets a blank <see cref="ChannelPermissions"/> that grants no permissions.</summary>
/// <returns> A <see cref="ChannelPermissions"/> structure that does not contain any set permissions.</returns>
public static readonly ChannelPermissions None = new ChannelPermissions();
/// <summary> Gets a <see cref="ChannelPermissions"/> that grants all permissions for text channels.</summary>
public static readonly ChannelPermissions Text = new ChannelPermissions(0b0_11111_0101100_0000000_1111111110001_010001);
/// <summary> Gets a <see cref="ChannelPermissions"/> that grants all permissions for voice channels.</summary>
public static readonly ChannelPermissions Voice = new ChannelPermissions(0b1_00000_0000100_1111110_0000000011100_010001);
/// <summary> Gets a <see cref="ChannelPermissions"/> that grants all permissions for stage channels.</summary>
public static readonly ChannelPermissions Stage = new ChannelPermissions(0b0_00000_1000100_0111010_0000000010000_010001);
/// <summary> Gets a <see cref="ChannelPermissions"/> that grants all permissions for category channels.</summary>
public static readonly ChannelPermissions Category = new ChannelPermissions(0b01100_1111110_1111111110001_010001);
/// <summary> Gets a <see cref="ChannelPermissions"/> that grants all permissions for direct message channels.</summary>
public static readonly ChannelPermissions DM = new ChannelPermissions(0b00000_1000110_1011100110001_000000);
/// <summary> Gets a <see cref="ChannelPermissions"/> that grants all permissions for group channels.</summary>
public static readonly ChannelPermissions Group = new ChannelPermissions(0b00000_1000110_0001101100000_000000);
/// <summary> Gets a <see cref="ChannelPermissions"/> that grants all permissions for a given channel type.</summary>
/// <summary>
/// Gets a blank <see cref="ChannelPermissions"/> that grants no permissions.
/// </summary>
/// <returns>
/// A <see cref="ChannelPermissions"/> structure that does not contain any set permissions.
/// </returns>
public static readonly ChannelPermissions None = new();

/// <summary>
/// Gets a <see cref="ChannelPermissions"/> that grants all permissions for text channels.
/// </summary>
public static readonly ChannelPermissions Text = new(0b0_11111_0101100_0000000_1111111110001_010001);

/// <summary>
/// Gets a <see cref="ChannelPermissions"/> that grants all permissions for voice channels.
/// </summary>
public static readonly ChannelPermissions Voice = new(0b1_11111_0101100_1111110_1111111111101_010001); // (0b1_00000_0000100_1111110_0000000011100_010001 (<- voice only perms) |= Text)

/// <summary>
/// Gets a <see cref="ChannelPermissions"/> that grants all permissions for stage channels.
/// </summary>
public static readonly ChannelPermissions Stage = new(0b0_00000_1000100_0111010_0000000010000_010001);

/// <summary>
/// Gets a <see cref="ChannelPermissions"/> that grants all permissions for category channels.
/// </summary>
public static readonly ChannelPermissions Category = new(0b01100_1111110_1111111110001_010001);

/// <summary>
/// Gets a <see cref="ChannelPermissions"/> that grants all permissions for direct message channels.
/// </summary>
public static readonly ChannelPermissions DM = new(0b00000_1000110_1011100110001_000000);

/// <summary>
/// Gets a <see cref="ChannelPermissions"/> that grants all permissions for group channels.
/// </summary>
public static readonly ChannelPermissions Group = new(0b00000_1000110_0001101100000_000000);

/// <summary>
/// Gets a <see cref="ChannelPermissions"/> that grants all permissions for a given channel type.
/// </summary>
/// <exception cref="ArgumentException">Unknown channel type.</exception>
public static ChannelPermissions All(IChannel channel)
{
return channel switch
{
ITextChannel _ => Text,
IStageChannel _ => Stage,
IVoiceChannel _ => Voice,
ITextChannel _ => Text,
ICategoryChannel _ => Category,
IDMChannel _ => DM,
IGroupChannel _ => Group,


+ 1
- 1
src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs View File

@@ -79,7 +79,7 @@ namespace Discord
/// Sets a timestamp how long a user should be timed out for.
/// </summary>
/// <remarks>
/// <see cref="null"/> or a time in the past to clear a currently existing timeout.
/// <see langword="null"/> or a time in the past to clear a currently existing timeout.
/// </remarks>
public Optional<DateTimeOffset?> TimedOutUntil { get; set; }
}


+ 3
- 3
src/Discord.Net.Core/Entities/Users/IGuildUser.cs View File

@@ -104,7 +104,7 @@ namespace Discord
/// Gets the date and time that indicates if and for how long a user has been timed out.
/// </summary>
/// <remarks>
/// <see cref="null"/> or a timestamp in the past if the user is not timed out.
/// <see langword="null"/> or a timestamp in the past if the user is not timed out.
/// </remarks>
/// <returns>
/// A <see cref="DateTimeOffset"/> indicating how long the user will be timed out for.
@@ -116,7 +116,7 @@ namespace Discord
/// </summary>
/// <example>
/// <para>The following example checks if the current user has the ability to send a message with attachment in
/// this channel; if so, uploads a file via <see cref="IMessageChannel.SendFileAsync(string, string, bool, Embed, RequestOptions, bool, AllowedMentions, MessageReference)"/>.</para>
/// this channel; if so, uploads a file via <see cref="IMessageChannel.SendFileAsync(string, string, bool, Embed, RequestOptions, bool, AllowedMentions, MessageReference, MessageComponent, ISticker[], Embed[], MessageFlags)"/>.</para>
/// <code language="cs">
/// if (currentUser?.GetPermissions(targetChannel)?.AttachFiles)
/// await targetChannel.SendFileAsync("fortnite.png");
@@ -151,7 +151,7 @@ namespace Discord
/// If the user does not have a guild avatar, this will be the user's regular avatar.
/// </remarks>
/// <param name="format">The format to return.</param>
/// <param name="size">The size of the image to return in. This can be any power of two between 16 and 2048.
/// <param name="size">The size of the image to return in. This can be any power of two between 16 and 2048.</param>
/// <returns>
/// A string representing the URL of the displayed avatar for this user. <see langword="null"/> if the user does not have an avatar in place.
/// </returns>


+ 3
- 2
src/Discord.Net.Core/Format.cs View File

@@ -37,8 +37,9 @@ namespace Discord
/// <summary> Sanitizes the string, safely escaping any Markdown sequences. </summary>
public static string Sanitize(string text)
{
foreach (string unsafeChar in SensitiveCharacters)
text = text.Replace(unsafeChar, $"\\{unsafeChar}");
if (text != null)
foreach (string unsafeChar in SensitiveCharacters)
text = text.Replace(unsafeChar, $"\\{unsafeChar}");
return text;
}



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

@@ -23,7 +23,7 @@ namespace Discord.Utils

/// <summary>
/// Not full URL validation right now. Just Ensures the protocol is either http, https, or discord
/// <see cref="Validate(string)"/> should be used everything other than url buttons.
/// <see cref="Validate(string, bool)"/> should be used everything other than url buttons.
/// </summary>
/// <param name="url">The URL to validate before sending to discord.</param>
/// <exception cref="InvalidOperationException">A URL must include a protocol (either http, https, or discord).</exception>


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

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>

<ItemGroup>


+ 3
- 3
src/Discord.Net.Interactions/Attributes/AutocompleteAttribute.cs View File

@@ -3,7 +3,7 @@ using System;
namespace Discord.Interactions
{
/// <summary>
/// Set the <see cref="ApplicationCommandOptionProperties.Autocomplete"/> to <see langword="true"/>.
/// Set the <see cref="ApplicationCommandOptionProperties.IsAutocomplete"/> to <see langword="true"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public class AutocompleteAttribute : Attribute
@@ -14,7 +14,7 @@ namespace Discord.Interactions
public Type AutocompleteHandlerType { get; }

/// <summary>
/// Set the <see cref="ApplicationCommandOptionProperties.Autocomplete"/> to <see langword="true"/> and define a <see cref="AutocompleteHandler"/> to handle
/// Set the <see cref="ApplicationCommandOptionProperties.IsAutocomplete"/> to <see langword="true"/> and define a <see cref="AutocompleteHandler"/> to handle
/// Autocomplete interactions targeting the parameter this <see cref="Attribute"/> is applied to.
/// </summary>
/// <remarks>
@@ -29,7 +29,7 @@ namespace Discord.Interactions
}

/// <summary>
/// Set the <see cref="ApplicationCommandOptionProperties.Autocomplete"/> to <see langword="true"/> without specifying a <see cref="AutocompleteHandler"/>.
/// Set the <see cref="ApplicationCommandOptionProperties.IsAutocomplete"/> to <see langword="true"/> without specifying a <see cref="AutocompleteHandler"/>.
/// </summary>
public AutocompleteAttribute() { }
}


+ 0
- 2
src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs View File

@@ -21,9 +21,7 @@ namespace Discord.Interactions
/// <summary>
/// Create a new <see cref="ModalInputAttribute"/>.
/// </summary>
/// <param name="label">The label of the input.</param>
/// <param name="customId">The custom id of the input.</param>
/// <param name="required">Whether the user is required to input a value.></param>
protected ModalInputAttribute(string customId)
{
CustomId = customId;


+ 1
- 1
src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs View File

@@ -36,7 +36,7 @@ namespace Discord.Interactions
/// <summary>
/// Create a new <see cref="ModalTextInputAttribute"/>.
/// </summary>
/// <param name="customId"The custom id of the text input.></param>
/// <param name="customId">The custom id of the text input.></param>
/// <param name="style">The style of the text input.</param>
/// <param name="placeholder">The placeholder of the text input.</param>
/// <param name="minLength">The minimum length of the text input's content.</param>


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

@@ -29,7 +29,7 @@ namespace Discord.Interactions
/// <remarks>
/// This precondition will always fail if the command is being invoked in a <see cref="IPrivateChannel"/>.
/// </remarks>
/// <param name="permission">
/// <param name="guildPermission">
/// The <see cref="Discord.GuildPermission" /> that the user must have. Multiple permissions can be
/// specified by ORing the permissions together.
/// </param>
@@ -41,7 +41,7 @@ namespace Discord.Interactions
/// <summary>
/// Requires that the user invoking the command to have a specific <see cref="Discord.ChannelPermission"/>.
/// </summary>
/// <param name="permission">
/// <param name="channelPermission">
/// The <see cref="Discord.ChannelPermission"/> that the user must have. Multiple permissions can be
/// specified by ORing the permissions together.
/// </param>


+ 1
- 1
src/Discord.Net.Interactions/Builders/Commands/SlashCommandBuilder.cs View File

@@ -56,7 +56,7 @@ namespace Discord.Interactions.Builders
/// <summary>
/// Sets <see cref="DefaultPermission"/>.
/// </summary>
/// <param name="defaultPermision">New value of the <see cref="DefaultPermission"/>.</param>
/// <param name="permission">New value of the <see cref="DefaultPermission"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>


+ 1
- 1
src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs View File

@@ -41,7 +41,7 @@ namespace Discord.Interactions.Builders
/// <summary>
/// Sets <see cref="Style"/>.
/// </summary>
/// <param name="style">New value of the <see cref="SetValue(string)"/>.</param>
/// <param name="style">New value of the <see cref="Style"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>


+ 1
- 1
src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs View File

@@ -64,7 +64,7 @@ namespace Discord.Interactions.Builders
}

/// <summary>
/// Adds text components to <see cref="TextComponents"/>.
/// Adds text components to <see cref="Components"/>.
/// </summary>
/// <param name="configure">Text Component builder factory.</param>
/// <returns>


+ 2
- 1
src/Discord.Net.Interactions/Builders/ModuleBuilder.cs View File

@@ -357,7 +357,8 @@ namespace Discord.Interactions.Builders
return this;

}

/// <summary>
/// Adds a modal command builder to <see cref="ModalCommands"/>.
/// </summary>
/// <param name="configure"><see cref="ModalCommands"/> factory.</param>


+ 1
- 1
src/Discord.Net.Interactions/Builders/Parameters/ParameterBuilder.cs View File

@@ -122,7 +122,7 @@ namespace Discord.Interactions.Builders
/// <summary>
/// Adds preconditions to <see cref="Preconditions"/>
/// </summary>
/// <param name="preconditions">New attributes to be added to <see cref="Preconditions"/>.</param>
/// <param name="attributes">New attributes to be added to <see cref="Preconditions"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>


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

@@ -7,8 +7,10 @@
<RootNamespace>Discord.Interactions</RootNamespace>
<AssemblyName>Discord.Net.Interactions</AssemblyName>
<Description>A Discord.Net extension adding support for Application Commands.</Description>
<WarningLevel>5</WarningLevel>
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" />
<ProjectReference Include="..\Discord.Net.Rest\Discord.Net.Rest.csproj" />


+ 1
- 1
src/Discord.Net.Interactions/Info/ModuleInfo.cs View File

@@ -248,7 +248,7 @@ namespace Discord.Interactions

while (parent != null)
{
permissions = (permissions ?? 0) | (parent.DefaultMemberPermissions ?? 0);
permissions = (permissions ?? 0) | (parent.DefaultMemberPermissions ?? 0).SanitizeGuildPermissions();
parent = parent.Parent;
}



+ 1
- 2
src/Discord.Net.Interactions/InteractionContext.cs View File

@@ -24,8 +24,7 @@ namespace Discord.Interactions
/// </summary>
/// <param name="client">The underlying client.</param>
/// <param name="interaction">The underlying interaction.</param>
/// <param name="user"><see cref="IUser"/> who executed the command.</param>
/// <param name="channel"><see cref="ISocketMessageChannel"/> the command originated from.</param>
/// <param name="channel"><see cref="IMessageChannel"/> the command originated from.</param>
public InteractionContext(IDiscordClient client, IDiscordInteraction interaction, IMessageChannel channel = null)
{
Client = client;


+ 6
- 6
src/Discord.Net.Interactions/InteractionModuleBase.cs View File

@@ -45,7 +45,7 @@ namespace Discord.Interactions
protected virtual async Task DeferAsync(bool ephemeral = false, RequestOptions options = null) =>
await Context.Interaction.DeferAsync(ephemeral, options).ConfigureAwait(false);

/// <inheritdoc cref="IDiscordInteraction.RespondAsync(string, Embed[], bool, bool, AllowedMentions, RequestOptions, MessageComponent, Embed)"/>
/// <inheritdoc cref="IDiscordInteraction.RespondAsync(string, Embed[], bool, bool, AllowedMentions, MessageComponent, Embed, RequestOptions)"/>
protected virtual async Task RespondAsync (string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false,
AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent components = null, Embed embed = null) =>
await Context.Interaction.RespondAsync(text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false);
@@ -70,7 +70,7 @@ namespace Discord.Interactions
AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null)
=> Context.Interaction.RespondWithFilesAsync(attachments, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options);

/// <inheritdoc cref="IDiscordInteraction.FollowupAsync(string, Embed[], bool, bool, AllowedMentions, RequestOptions, MessageComponent, Embed)"/>
/// <inheritdoc cref="IDiscordInteraction.FollowupAsync(string, Embed[], bool, bool, AllowedMentions, MessageComponent, Embed, RequestOptions)"/>
protected virtual async Task<IUserMessage> FollowupAsync (string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false,
AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent components = null, Embed embed = null) =>
await Context.Interaction.FollowupAsync(text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false);
@@ -95,7 +95,7 @@ namespace Discord.Interactions
AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null)
=> Context.Interaction.FollowupWithFilesAsync(attachments, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options);

/// <inheritdoc cref="IMessageChannel.SendMessageAsync(string, bool, Embed, RequestOptions, AllowedMentions, MessageReference, MessageComponent, ISticker[], Embed[])"/>
/// <inheritdoc cref="IMessageChannel.SendMessageAsync(string, bool, Embed, RequestOptions, AllowedMentions, MessageReference, MessageComponent, ISticker[], Embed[], MessageFlags)"/>
protected virtual async Task<IUserMessage> ReplyAsync (string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null,
AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null) =>
await Context.Channel.SendMessageAsync(text, false, embed, options, allowedMentions, messageReference, components).ConfigureAwait(false);
@@ -118,9 +118,9 @@ namespace Discord.Interactions
/// <inheritdoc cref="IDiscordInteraction.RespondWithModalAsync(Modal, RequestOptions)"/>
protected virtual async Task RespondWithModalAsync(Modal modal, RequestOptions options = null) => await Context.Interaction.RespondWithModalAsync(modal);
/// <inheritdoc cref="IDiscordInteractionExtentions.RespondWithModalAsync(IDiscordInteraction, IModal, RequestOptions)"/>
protected virtual async Task RespondWithModalAsync<T>(string customId, RequestOptions options = null) where T : class, IModal
=> await Context.Interaction.RespondWithModalAsync<T>(customId, options);
/// <inheritdoc cref="IDiscordInteractionExtentions.RespondWithModalAsync{T}(IDiscordInteraction, string, RequestOptions, Action{ModalBuilder})"/>
protected virtual async Task RespondWithModalAsync<TModal>(string customId, RequestOptions options = null) where TModal : class, IModal
=> await Context.Interaction.RespondWithModalAsync<TModal>(customId, options);

//IInteractionModuleBase



+ 140
- 36
src/Discord.Net.Interactions/InteractionService.cs View File

@@ -223,7 +223,8 @@ namespace Discord.Interactions
new ConcurrentDictionary<Type, Type>
{
[typeof(Array)] = typeof(DefaultArrayComponentConverter<>),
[typeof(IConvertible)] = typeof(DefaultValueComponentConverter<>)
[typeof(IConvertible)] = typeof(DefaultValueComponentConverter<>),
[typeof(Nullable<>)] = typeof(NullableComponentConverter<>)
});

_typeReaderMap = new TypeMap<TypeReader, string>(this, new ConcurrentDictionary<Type, TypeReader>(),
@@ -234,7 +235,8 @@ namespace Discord.Interactions
[typeof(IUser)] = typeof(DefaultUserReader<>),
[typeof(IMessage)] = typeof(DefaultMessageReader<>),
[typeof(IConvertible)] = typeof(DefaultValueReader<>),
[typeof(Enum)] = typeof(EnumReader<>)
[typeof(Enum)] = typeof(EnumReader<>),
[typeof(Nullable<>)] = typeof(NullableReader<>)
});
}

@@ -421,20 +423,39 @@ namespace Discord.Interactions
/// </summary>
/// <remarks>
/// Commands will be registered as standalone commands, if you want the <see cref="GroupAttribute"/> to take effect,
/// use <see cref="AddModulesToGuildAsync(IGuild, ModuleInfo[])"/>. Registering a commands without group names might cause the command traversal to fail.
/// use <see cref="AddModulesToGuildAsync(IGuild, bool, ModuleInfo[])"/>. Registering a commands without group names might cause the command traversal to fail.
/// </remarks>
/// <param name="guild">The target guild.</param>
/// <param name="deleteMissing">If <see langword="false"/>, this operation will not delete the commands that are missing from <see cref="InteractionService"/>.</param>
/// <param name="commands">Commands to be registered to Discord.</param>
/// <returns>
/// A task representing the command registration process. The task result contains the active application commands of the target guild.
/// </returns>
public async Task<IReadOnlyCollection<RestGuildCommand>> AddCommandsToGuildAsync(IGuild guild, bool deleteMissing = false, params ICommandInfo[] commands)
{
EnsureClientReady();

if (guild is null)
throw new ArgumentNullException(nameof(guild));

return await AddCommandsToGuildAsync(guild.Id, deleteMissing, commands).ConfigureAwait(false);
}

/// <summary>
/// Register Application Commands from <paramref name="commands"/> to a guild.
/// </summary>
/// <remarks>
/// Commands will be registered as standalone commands, if you want the <see cref="GroupAttribute"/> to take effect,
/// use <see cref="AddModulesToGuildAsync(ulong, bool, ModuleInfo[])"/>. Registering a commands without group names might cause the command traversal to fail.
/// </remarks>
/// <param name="guildId">The target guild ID.</param>
/// <param name="deleteMissing">If <see langword="false"/>, this operation will not delete the commands that are missing from <see cref="InteractionService"/>.</param>
/// <param name="commands">Commands to be registered to Discord.</param>
/// <returns>
/// A task representing the command registration process. The task result contains the active application commands of the target guild.
/// </returns>
public async Task<IReadOnlyCollection<RestGuildCommand>> AddCommandsToGuildAsync(ulong guildId, bool deleteMissing = false, params ICommandInfo[] commands)
{
EnsureClientReady();

var props = new List<ApplicationCommandProperties>();

foreach (var command in commands)
@@ -454,44 +475,60 @@ namespace Discord.Interactions

if (!deleteMissing)
{
var existing = await RestClient.GetGuildApplicationCommands(guild.Id).ConfigureAwait(false);
var existing = await RestClient.GetGuildApplicationCommands(guildId).ConfigureAwait(false);
var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name));
props.AddRange(missing.Select(x => x.ToApplicationCommandProps()));
}

return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guild.Id).ConfigureAwait(false);
return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guildId).ConfigureAwait(false);
}

/// <summary>
/// Register Application Commands from modules provided in <paramref name="modules"/> to a guild.
/// </summary>
/// <param name="guild">The target guild.</param>
/// <param name="deleteMissing">If <see langword="false"/>, this operation will not delete the commands that are missing from <see cref="InteractionService"/>.</param>
/// <param name="modules">Modules to be registered to Discord.</param>
/// <returns>
/// A task representing the command registration process. The task result contains the active application commands of the target guild.
/// </returns>
public async Task<IReadOnlyCollection<RestGuildCommand>> AddModulesToGuildAsync(IGuild guild, bool deleteMissing = false, params ModuleInfo[] modules)
{
EnsureClientReady();

if (guild is null)
throw new ArgumentNullException(nameof(guild));

return await AddModulesToGuildAsync(guild.Id, deleteMissing, modules).ConfigureAwait(false);
}

/// <summary>
/// Register Application Commands from modules provided in <paramref name="modules"/> to a guild.
/// </summary>
/// <param name="guildId">The target guild ID.</param>
/// <param name="deleteMissing">If <see langword="false"/>, this operation will not delete the commands that are missing from <see cref="InteractionService"/>.</param>
/// <param name="modules">Modules to be registered to Discord.</param>
/// <returns>
/// A task representing the command registration process. The task result contains the active application commands of the target guild.
/// </returns>
public async Task<IReadOnlyCollection<RestGuildCommand>> AddModulesToGuildAsync(ulong guildId, bool deleteMissing = false, params ModuleInfo[] modules)
{
EnsureClientReady();

var props = modules.SelectMany(x => x.ToApplicationCommandProps(true)).ToList();

if (!deleteMissing)
{
var existing = await RestClient.GetGuildApplicationCommands(guild.Id).ConfigureAwait(false);
var existing = await RestClient.GetGuildApplicationCommands(guildId).ConfigureAwait(false);
var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name));
props.AddRange(missing.Select(x => x.ToApplicationCommandProps()));
}

return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guild.Id).ConfigureAwait(false);
return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guildId).ConfigureAwait(false);
}

/// <summary>
/// Register Application Commands from modules provided in <paramref name="modules"/> as global commands.
/// </summary>
/// <param name="deleteMissing">If <see langword="false"/>, this operation will not delete the commands that are missing from <see cref="InteractionService"/>.</param>
/// <param name="modules">Modules to be registered to Discord.</param>
/// <returns>
/// A task representing the command registration process. The task result contains the active application commands of the target guild.
@@ -517,8 +554,9 @@ namespace Discord.Interactions
/// </summary>
/// <remarks>
/// Commands will be registered as standalone commands, if you want the <see cref="GroupAttribute"/> to take effect,
/// use <see cref="AddModulesToGuildAsync(IGuild, ModuleInfo[])"/>. Registering a commands without group names might cause the command traversal to fail.
/// use <see cref="AddModulesToGuildAsync(IGuild, bool, ModuleInfo[])"/>. Registering a commands without group names might cause the command traversal to fail.
/// </remarks>
/// <param name="deleteMissing">If <see langword="false"/>, this operation will not delete the commands that are missing from <see cref="InteractionService"/>.</param>
/// <param name="commands">Commands to be registered to Discord.</param>
/// <returns>
/// A task representing the command registration process. The task result contains the active application commands of the target guild.
@@ -834,11 +872,16 @@ namespace Discord.Interactions
if (!searchResult.Command.SupportsWildCards || context is not IRouteMatchContainer matchContainer)
return;

var matches = new RouteSegmentMatch[searchResult.RegexCaptureGroups.Length];
for (var i = 0; i < searchResult.RegexCaptureGroups.Length; i++)
matches[i] = new RouteSegmentMatch(searchResult.RegexCaptureGroups[i]);
if (searchResult.RegexCaptureGroups?.Length > 0)
{
var matches = new RouteSegmentMatch[searchResult.RegexCaptureGroups.Length];
for (var i = 0; i < searchResult.RegexCaptureGroups.Length; i++)
matches[i] = new RouteSegmentMatch(searchResult.RegexCaptureGroups[i]);

matchContainer.SetSegmentMatches(matches);
matchContainer.SetSegmentMatches(matches);
}
else
matchContainer.SetSegmentMatches(Array.Empty<RouteSegmentMatch>());
}

internal TypeConverter GetTypeConverter(Type type, IServiceProvider services = null)
@@ -960,7 +1003,7 @@ namespace Discord.Interactions
/// Removes a type reader for the given type.
/// </summary>
/// <remarks>
/// Removing a <see cref="TypeReader"/> from the <see cref="CommandService"/> will not dereference the <see cref="TypeReader"/> from the loaded module/command instances.
/// Removing a <see cref="TypeReader"/> from the <see cref="InteractionService"/> will not dereference the <see cref="TypeReader"/> from the loaded module/command instances.
/// You need to reload the modules for the changes to take effect.
/// </remarks>
/// <param name="type">The type to remove the reader from.</param>
@@ -973,7 +1016,7 @@ namespace Discord.Interactions
/// Removes a generic type reader from the type <typeparamref name="T"/>.
/// </summary>
/// <remarks>
/// Removing a <see cref="TypeReader"/> from the <see cref="CommandService"/> will not dereference the <see cref="TypeReader"/> from the loaded module/command instances.
/// Removing a <see cref="TypeReader"/> from the <see cref="InteractionService"/> will not dereference the <see cref="TypeReader"/> from the loaded module/command instances.
/// You need to reload the modules for the changes to take effect.
/// </remarks>
/// <typeparam name="T">The type to remove the readers from.</typeparam>
@@ -986,7 +1029,7 @@ namespace Discord.Interactions
/// Removes a generic type reader from the given type.
/// </summary>
/// <remarks>
/// Removing a <see cref="TypeReader"/> from the <see cref="CommandService"/> will not dereference the <see cref="TypeReader"/> from the loaded module/command instances.
/// Removing a <see cref="TypeReader"/> from the <see cref="InteractionService"/> will not dereference the <see cref="TypeReader"/> from the loaded module/command instances.
/// You need to reload the modules for the changes to take effect.
/// </remarks>
/// <param name="type">The type to remove the reader from.</param>
@@ -999,7 +1042,7 @@ namespace Discord.Interactions
/// Serialize an object using a <see cref="TypeReader"/> into a <see cref="string"/> to be placed in a Component CustomId.
/// </summary>
/// <remarks>
/// Removing a <see cref="TypeReader"/> from the <see cref="CommandService"/> will not dereference the <see cref="TypeReader"/> from the loaded module/command instances.
/// Removing a <see cref="TypeReader"/> from the <see cref="InteractionService"/> will not dereference the <see cref="TypeReader"/> from the loaded module/command instances.
/// You need to reload the modules for the changes to take effect.
/// </remarks>
/// <typeparam name="T">Type of the object to be serialized.</typeparam>
@@ -1079,19 +1122,40 @@ namespace Discord.Interactions
/// <returns>
/// The active command permissions after the modification.
/// </returns>
public async Task<GuildApplicationCommandPermission> ModifySlashCommandPermissionsAsync (ModuleInfo module, IGuild guild,
public async Task<GuildApplicationCommandPermission> ModifySlashCommandPermissionsAsync(ModuleInfo module, IGuild guild,
params ApplicationCommandPermission[] permissions)
{
if (module is null)
throw new ArgumentNullException(nameof(module));

if (guild is null)
throw new ArgumentNullException(nameof(guild));

return await ModifySlashCommandPermissionsAsync(module, guild.Id, permissions).ConfigureAwait(false);
}

/// <summary>
/// Modify the command permissions of the matching Discord Slash Command.
/// </summary>
/// <param name="module">Module representing the top level Slash Command.</param>
/// <param name="guildId">Target guild ID.</param>
/// <param name="permissions">New permission values.</param>
/// <returns>
/// The active command permissions after the modification.
/// </returns>
public async Task<GuildApplicationCommandPermission> ModifySlashCommandPermissionsAsync(ModuleInfo module, ulong guildId,
params ApplicationCommandPermission[] permissions)
{
if (module is null)
throw new ArgumentNullException(nameof(module));

if (!module.IsSlashGroup)
throw new InvalidOperationException($"This module does not have a {nameof(GroupAttribute)} and does not represent an Application Command");

if (!module.IsTopLevelGroup)
throw new InvalidOperationException("This module is not a top level application command. You cannot change its permissions");

if (guild is null)
throw new ArgumentNullException("guild");

var commands = await RestClient.GetGuildApplicationCommands(guild.Id).ConfigureAwait(false);
var commands = await RestClient.GetGuildApplicationCommands(guildId).ConfigureAwait(false);
var appCommand = commands.First(x => x.Name == module.SlashGroupName);

return await appCommand.ModifyCommandPermissions(permissions).ConfigureAwait(false);
@@ -1106,9 +1170,29 @@ namespace Discord.Interactions
/// <returns>
/// The active command permissions after the modification.
/// </returns>
public async Task<GuildApplicationCommandPermission> ModifySlashCommandPermissionsAsync (SlashCommandInfo command, IGuild guild,
params ApplicationCommandPermission[] permissions) =>
await ModifyApplicationCommandPermissionsAsync(command, guild, permissions).ConfigureAwait(false);
public async Task<GuildApplicationCommandPermission> ModifySlashCommandPermissionsAsync(SlashCommandInfo command, IGuild guild,
params ApplicationCommandPermission[] permissions)
{
if (command is null)
throw new ArgumentNullException(nameof(command));

if (guild is null)
throw new ArgumentNullException(nameof(guild));

return await ModifyApplicationCommandPermissionsAsync(command, guild.Id, permissions).ConfigureAwait(false);
}

/// <summary>
/// Modify the command permissions of the matching Discord Slash Command.
/// </summary>
/// <param name="command">The Slash Command.</param>
/// <param name="guildId">Target guild ID.</param>
/// <param name="permissions">New permission values.</param>
/// <returns>
/// The active command permissions after the modification.
/// </returns>
public async Task<GuildApplicationCommandPermission> ModifySlashCommandPermissionsAsync(SlashCommandInfo command, ulong guildId,
params ApplicationCommandPermission[] permissions) => await ModifyApplicationCommandPermissionsAsync(command, guildId, permissions).ConfigureAwait(false);

/// <summary>
/// Modify the command permissions of the matching Discord Slash Command.
@@ -1119,20 +1203,40 @@ namespace Discord.Interactions
/// <returns>
/// The active command permissions after the modification.
/// </returns>
public async Task<GuildApplicationCommandPermission> ModifyContextCommandPermissionsAsync (ContextCommandInfo command, IGuild guild,
params ApplicationCommandPermission[] permissions) =>
await ModifyApplicationCommandPermissionsAsync(command, guild, permissions).ConfigureAwait(false);
public async Task<GuildApplicationCommandPermission> ModifyContextCommandPermissionsAsync(ContextCommandInfo command, IGuild guild,
params ApplicationCommandPermission[] permissions)
{
if (command is null)
throw new ArgumentNullException(nameof(command));

if (guild is null)
throw new ArgumentNullException(nameof(guild));

return await ModifyApplicationCommandPermissionsAsync(command, guild.Id, permissions).ConfigureAwait(false);
}

private async Task<GuildApplicationCommandPermission> ModifyApplicationCommandPermissionsAsync<T> (T command, IGuild guild,
/// <summary>
/// Modify the command permissions of the matching Discord Slash Command.
/// </summary>
/// <param name="command">The Context Command.</param>
/// <param name="guildId">Target guild ID.</param>
/// <param name="permissions">New permission values.</param>
/// <returns>
/// The active command permissions after the modification.
/// </returns>
public async Task<GuildApplicationCommandPermission> ModifyContextCommandPermissionsAsync(ContextCommandInfo command, ulong guildId,
params ApplicationCommandPermission[] permissions) => await ModifyApplicationCommandPermissionsAsync(command, guildId, permissions).ConfigureAwait(false);

private async Task<GuildApplicationCommandPermission> ModifyApplicationCommandPermissionsAsync<T> (T command, ulong guildId,
params ApplicationCommandPermission[] permissions) where T : class, IApplicationCommandInfo, ICommandInfo
{
if (command is null)
throw new ArgumentNullException(nameof(command));

if (!command.IsTopLevelCommand)
throw new InvalidOperationException("This command is not a top level application command. You cannot change its permissions");

if (guild is null)
throw new ArgumentNullException("guild");

var commands = await RestClient.GetGuildApplicationCommands(guild.Id).ConfigureAwait(false);
var commands = await RestClient.GetGuildApplicationCommands(guildId).ConfigureAwait(false);
var appCommand = commands.First(x => x.Name == ( command as IApplicationCommandInfo ).Name);

return await appCommand.ModifyCommandPermissions(permissions).ConfigureAwait(false);


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

@@ -87,12 +87,12 @@ namespace Discord.Interactions
await InteractionService._restResponseCallback(Context, payload).ConfigureAwait(false);
}

protected override async Task RespondWithModalAsync<T>(string customId, RequestOptions options = null)
protected override async Task RespondWithModalAsync<TModal>(string customId, RequestOptions options = null)
{
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");

var payload = restInteraction.RespondWithModal<T>(customId, options);
var payload = restInteraction.RespondWithModal<TModal>(customId, options);

if (Context is IRestInteractionContext restContext && restContext.InteractionResponseCallback != null)
await restContext.InteractionResponseCallback.Invoke(payload).ConfigureAwait(false);


+ 1
- 1
src/Discord.Net.Interactions/Results/TypeConverterResult.cs View File

@@ -3,7 +3,7 @@ using System;
namespace Discord.Interactions
{
/// <summary>
/// Represents a result type for <see cref="TypeConverter.ReadAsync(IInteractionContext, WebSocket.SocketSlashCommandDataOption, IServiceProvider)"/>.
/// Represents a result type for <see cref="TypeConverter.ReadAsync(IInteractionContext, IApplicationCommandInteractionDataOption, IServiceProvider)"/>.
/// </summary>
public struct TypeConverterResult : IResult
{


+ 23
- 0
src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/NullableComponentConverter.cs View File

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

namespace Discord.Interactions
{
internal class NullableComponentConverter<T> : ComponentTypeConverter<T>
{
private readonly ComponentTypeConverter _typeConverter;

public NullableComponentConverter(InteractionService interactionService, IServiceProvider services)
{
var type = Nullable.GetUnderlyingType(typeof(T));

if (type is null)
throw new ArgumentException($"No type {nameof(TypeConverter)} is defined for this {type.FullName}", "type");

_typeConverter = interactionService.GetComponentTypeConverter(type, services);
}

public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
=> string.IsNullOrEmpty(option.Value) ? Task.FromResult(TypeConverterResult.FromSuccess(null)) : _typeConverter.ReadAsync(context, option, services);
}
}

+ 23
- 0
src/Discord.Net.Interactions/TypeReaders/NullableReader.cs View File

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

namespace Discord.Interactions
{
internal class NullableReader<T> : TypeReader<T>
{
private readonly TypeReader _typeReader;

public NullableReader(InteractionService interactionService, IServiceProvider services)
{
var type = Nullable.GetUnderlyingType(typeof(T));

if (type is null)
throw new ArgumentException($"No type {nameof(TypeConverter)} is defined for this {type.FullName}", "type");

_typeReader = interactionService.GetTypeReader(type, services);
}

public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, string option, IServiceProvider services)
=> string.IsNullOrEmpty(option) ? Task.FromResult(TypeConverterResult.FromSuccess(null)) : _typeReader.ReadAsync(context, option, services);
}
}

+ 6
- 3
src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs View File

@@ -41,7 +41,7 @@ namespace Discord.Interactions
Name = commandInfo.Name,
Description = commandInfo.Description,
IsDMEnabled = commandInfo.IsEnabledInDm,
DefaultMemberPermissions = (commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)
DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(),
}.Build();

if (commandInfo.Parameters.Count > SlashCommandBuilder.MaxOptionsCount)
@@ -69,14 +69,14 @@ namespace Discord.Interactions
{
Name = commandInfo.Name,
IsDefaultPermission = commandInfo.DefaultPermission,
DefaultMemberPermissions = (commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0),
DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(),
IsDMEnabled = commandInfo.IsEnabledInDm
}.Build(),
ApplicationCommandType.User => new UserCommandBuilder
{
Name = commandInfo.Name,
IsDefaultPermission = commandInfo.DefaultPermission,
DefaultMemberPermissions = (commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0),
DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(),
IsDMEnabled = commandInfo.IsEnabledInDm
}.Build(),
_ => throw new InvalidOperationException($"{commandInfo.CommandType} isn't a supported command type.")
@@ -232,5 +232,8 @@ namespace Discord.Interactions

return builder.Build();
}

public static GuildPermission? SanitizeGuildPermissions(this GuildPermission permissions) =>
permissions == 0 ? null : permissions;
}
}

+ 7
- 0
src/Discord.Net.Rest/API/Common/Channel.cs View File

@@ -66,5 +66,12 @@ namespace Discord.API

[JsonProperty("member_count")]
public Optional<int> MemberCount { get; set; }

//ForumChannel
[JsonProperty("available_tags")]
public Optional<ForumTags[]> ForumTags { get; set; }
[JsonProperty("default_auto_archive_duration")]
public Optional<ThreadArchiveDuration> AutoArchiveDuration { get; set; }
}
}

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

@@ -9,8 +9,5 @@ namespace Discord.API.Rest

[JsonProperty("members")]
public ThreadMember[] Members { get; set; }

[JsonProperty("has_more")]
public bool HasMore { get; set; }
}
}

+ 21
- 0
src/Discord.Net.Rest/API/Common/ForumTags.cs View File

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

namespace Discord.API
{
internal class ForumTags
{
[JsonProperty("id")]
public ulong Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("emoji_id")]
public Optional<ulong?> EmojiId { get; set; }
[JsonProperty("emoji_name")]
public Optional<string> EmojiName { get; set; }
}
}

+ 33
- 0
src/Discord.Net.Rest/API/Common/ForumThreadMessage.cs View File

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

namespace Discord.API
{
internal class ForumThreadMessage
{
[JsonProperty("content")]
public Optional<string> Content { get; set; }

[JsonProperty("nonce")]
public Optional<string> Nonce { get; set; }

[JsonProperty("embeds")]
public Optional<Embed[]> Embeds { get; set; }

[JsonProperty("allowed_mentions")]
public Optional<AllowedMentions> AllowedMentions { get; set; }

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

[JsonProperty("sticker_ids")]
public Optional<ulong[]> Stickers { get; set; }

[JsonProperty("flags")]
public Optional<MessageFlags> Flags { get; set; }
}
}

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

@@ -12,5 +12,8 @@ namespace Discord.API

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

[JsonProperty("fail_if_not_exists")]
public Optional<bool> FailIfNotExists { get; set; }
}
}

+ 96
- 0
src/Discord.Net.Rest/API/Rest/CreateMultipartPostAsync.cs View File

@@ -0,0 +1,96 @@
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 CreateMultipartPostAsync
{
private static JsonSerializer _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() };

public FileAttachment[] Files { get; }

public string Title { get; set; }
public ThreadArchiveDuration ArchiveDuration { get; set; }
public Optional<int?> Slowmode { get; set; }


public Optional<string> Content { get; set; }
public Optional<Embed[]> Embeds { get; set; }
public Optional<AllowedMentions> AllowedMentions { get; set; }
public Optional<ActionRowComponent[]> MessageComponent { get; set; }
public Optional<MessageFlags?> Flags { get; set; }
public Optional<ulong[]> Stickers { get; set; }

public CreateMultipartPostAsync(params FileAttachment[] attachments)
{
Files = attachments;
}

public IReadOnlyDictionary<string, object> ToDictionary()
{
var d = new Dictionary<string, object>();

var payload = new Dictionary<string, object>();
var message = new Dictionary<string, object>();

payload["name"] = Title;
payload["auto_archive_duration"] = ArchiveDuration;

if (Slowmode.IsSpecified)
payload["rate_limit_per_user"] = Slowmode.Value;

// message
if (Content.IsSpecified)
message["content"] = Content.Value;
if (Embeds.IsSpecified)
message["embeds"] = Embeds.Value;
if (AllowedMentions.IsSpecified)
message["allowed_mentions"] = AllowedMentions.Value;
if (MessageComponent.IsSpecified)
message["components"] = MessageComponent.Value;
if (Stickers.IsSpecified)
message["sticker_ids"] = Stickers.Value;
if (Flags.IsSpecified)
message["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
});
}

message["attachments"] = attachments;

payload["message"] = message;

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

+ 25
- 0
src/Discord.Net.Rest/API/Rest/CreatePostParams.cs View File

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

namespace Discord.API.Rest
{
internal class CreatePostParams
{
// thread
[JsonProperty("name")]
public string Title { get; set; }

[JsonProperty("auto_archive_duration")]
public ThreadArchiveDuration ArchiveDuration { get; set; }

[JsonProperty("rate_limit_per_user")]
public Optional<int?> Slowmode { get; set; }

[JsonProperty("message")]
public ForumThreadMessage Message { get; set; }
}
}

+ 1
- 1
src/Discord.Net.Rest/API/Rest/UploadFileParams.cs View File

@@ -37,7 +37,7 @@ namespace Discord.API.Rest
if (Content.IsSpecified)
payload["content"] = Content.Value;
if (IsTTS.IsSpecified)
payload["tts"] = IsTTS.Value.ToString();
payload["tts"] = IsTTS.Value;
if (Nonce.IsSpecified)
payload["nonce"] = Nonce.Value;
if (Embeds.IsSpecified)


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

@@ -50,7 +50,7 @@ namespace Discord.API.Rest
if (Content.IsSpecified)
data["content"] = Content.Value;
if (IsTTS.IsSpecified)
data["tts"] = IsTTS.Value.ToString();
data["tts"] = IsTTS.Value;
if (MessageComponents.IsSpecified)
data["components"] = MessageComponents.Value;
if (Embeds.IsSpecified)


+ 1
- 1
src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs View File

@@ -36,7 +36,7 @@ namespace Discord.API.Rest
if (Content.IsSpecified)
payload["content"] = Content.Value;
if (IsTTS.IsSpecified)
payload["tts"] = IsTTS.Value.ToString();
payload["tts"] = IsTTS.Value;
if (Nonce.IsSpecified)
payload["nonce"] = Nonce.Value;
if (Username.IsSpecified)


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

@@ -6,6 +6,7 @@ using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Discord.Net.Commands")]
[assembly: InternalsVisibleTo("Discord.Net.Tests")]
[assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")]
[assembly: InternalsVisibleTo("Discord.Net.Tests.Integration")]
[assembly: InternalsVisibleTo("Discord.Net.Interactions")]

[assembly: TypeForwardedTo(typeof(Discord.Embed))]


+ 2
- 0
src/Discord.Net.Rest/Discord.Net.Rest.csproj View File

@@ -7,6 +7,8 @@
<Description>A core Discord.Net library containing the REST client and models.</Description>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">net6.0;net5.0;net461;netstandard2.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">net6.0;net5.0;netstandard2.0;netstandard2.1</TargetFrameworks>
<WarningLevel>5</WarningLevel>
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" />


+ 60
- 22
src/Discord.Net.Rest/DiscordRestApiClient.cs View File

@@ -173,10 +173,12 @@ namespace Discord.API
private async Task LogoutInternalAsync()
{
//An exception here will lock the client into the unusable LoggingOut state, but that's probably fine since our client is in an undefined state too.
if (LoginState == LoginState.LoggedOut) return;
if (LoginState == LoginState.LoggedOut)
return;
LoginState = LoginState.LoggingOut;

try { _loginCancelToken?.Cancel(false); }
try
{ _loginCancelToken?.Cancel(false); }
catch { }

await DisconnectInternalAsync(null).ConfigureAwait(false);
@@ -398,7 +400,7 @@ namespace Discord.API
Preconditions.AtLeast(args.Position, 0, nameof(args.Position));
Preconditions.NotNullOrWhitespace(args.Name, nameof(args.Name));

if(args.Name.IsSpecified)
if (args.Name.IsSpecified)
Preconditions.AtMost(args.Name.Value.Length, 100, nameof(args.Name));

options = RequestOptions.CreateOrClone(options);
@@ -414,9 +416,9 @@ namespace Discord.API
Preconditions.AtLeast(args.Position, 0, nameof(args.Position));
Preconditions.NotNullOrWhitespace(args.Name, nameof(args.Name));

if(args.Name.IsSpecified)
if (args.Name.IsSpecified)
Preconditions.AtMost(args.Name.Value.Length, 100, nameof(args.Name));
if(args.Topic.IsSpecified)
if (args.Topic.IsSpecified)
Preconditions.AtMost(args.Topic.Value.Length, 1024, nameof(args.Name));

Preconditions.AtLeast(args.SlowModeInterval, 0, nameof(args.SlowModeInterval));
@@ -464,6 +466,24 @@ namespace Discord.API
#endregion

#region Threads
public async Task<Channel> CreatePostAsync(ulong channelId, CreatePostParams args, RequestOptions options = null)
{
Preconditions.NotEqual(channelId, 0, nameof(channelId));

var bucket = new BucketIds(channelId: channelId);

return await SendJsonAsync<Channel>("POST", () => $"channels/{channelId}/threads", args, bucket, options: options);
}

public async Task<Channel> CreatePostAsync(ulong channelId, CreateMultipartPostAsync args, RequestOptions options = null)
{
Preconditions.NotEqual(channelId, 0, nameof(channelId));

var bucket = new BucketIds(channelId: channelId);

return await SendMultipartAsync<Channel>("POST", () => $"channels/{channelId}/threads", args.ToDictionary(), bucket, options: options);
}

public async Task<Channel> ModifyThreadAsync(ulong channelId, ModifyThreadParams args, RequestOptions options = null)
{
Preconditions.NotEqual(channelId, 0, nameof(channelId));
@@ -564,15 +584,15 @@ namespace Discord.API
return await SendAsync<ThreadMember>("GET", () => $"channels/{channelId}/thread-members/{userId}", bucket, options: options).ConfigureAwait(false);
}

public async Task<ChannelThreads> GetActiveThreadsAsync(ulong channelId, RequestOptions options = null)
public async Task<ChannelThreads> GetActiveThreadsAsync(ulong guildId, RequestOptions options = null)
{
Preconditions.NotEqual(channelId, 0, nameof(channelId));
Preconditions.NotEqual(guildId, 0, nameof(guildId));

options = RequestOptions.CreateOrClone(options);

var bucket = new BucketIds(channelId: channelId);
var bucket = new BucketIds(guildId: guildId);

return await SendAsync<ChannelThreads>("GET", () => $"channels/{channelId}/threads/active", bucket, options: options);
return await SendAsync<ChannelThreads>("GET", () => $"guilds/{guildId}/threads/active", bucket, options: options);
}

public async Task<ChannelThreads> GetPublicArchivedThreadsAsync(ulong channelId, DateTimeOffset? before = null, int? limit = null, RequestOptions options = null)
@@ -671,9 +691,11 @@ namespace Discord.API

options = RequestOptions.CreateOrClone(options);

var bucket = new BucketIds(channelId: channelId);

try
{
await SendAsync("DELETE", $"stage-instances/{channelId}", options: options).ConfigureAwait(false);
await SendAsync("DELETE", () => $"stage-instances/{channelId}", bucket, options: options).ConfigureAwait(false);
}
catch (HttpException httpEx) when (httpEx.HttpCode == HttpStatusCode.NotFound) { }
}
@@ -798,9 +820,11 @@ namespace Discord.API
var ids = new BucketIds(channelId: channelId);
return await SendJsonAsync<Message>("POST", () => $"channels/{channelId}/messages", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false);
}


/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
/// <exception cref="InvalidOperationException">This operation may only be called with a <see cref="TokenType.Webhook"/> token.</exception>
public async Task<Message> CreateWebhookMessageAsync(ulong webhookId, CreateWebhookMessageParams args, RequestOptions options = null)
public async Task<Message> CreateWebhookMessageAsync(ulong webhookId, CreateWebhookMessageParams args, RequestOptions options = null, ulong? threadId = null)
{
if (AuthTokenType != TokenType.Webhook)
throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token.");
@@ -816,12 +840,12 @@ namespace Discord.API
options = RequestOptions.CreateOrClone(options);

var ids = new BucketIds(webhookId: webhookId);
return await SendJsonAsync<Message>("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false);
return await SendJsonAsync<Message>("POST", () => $"webhooks/{webhookId}/{AuthToken}?{WebhookQuery(true, threadId)}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false);
}

/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
/// <exception cref="InvalidOperationException">This operation may only be called with a <see cref="TokenType.Webhook"/> token.</exception>
public async Task ModifyWebhookMessageAsync(ulong webhookId, ulong messageId, ModifyWebhookMessageParams args, RequestOptions options = null)
public async Task ModifyWebhookMessageAsync(ulong webhookId, ulong messageId, ModifyWebhookMessageParams args, RequestOptions options = null, ulong? threadId = null)
{
if (AuthTokenType != TokenType.Webhook)
throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token.");
@@ -837,11 +861,11 @@ namespace Discord.API
options = RequestOptions.CreateOrClone(options);

var ids = new BucketIds(webhookId: webhookId);
await SendJsonAsync<Message>("PATCH", () => $"webhooks/{webhookId}/{AuthToken}/messages/{messageId}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false);
await SendJsonAsync<Message>("PATCH", () => $"webhooks/{webhookId}/{AuthToken}/messages/{messageId}${WebhookQuery(false, threadId)}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false);
}

/// <exception cref="InvalidOperationException">This operation may only be called with a <see cref="TokenType.Webhook"/> token.</exception>
public async Task DeleteWebhookMessageAsync(ulong webhookId, ulong messageId, RequestOptions options = null)
public async Task DeleteWebhookMessageAsync(ulong webhookId, ulong messageId, RequestOptions options = null, ulong? threadId = null)
{
if (AuthTokenType != TokenType.Webhook)
throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token.");
@@ -852,7 +876,7 @@ namespace Discord.API
options = RequestOptions.CreateOrClone(options);

var ids = new BucketIds(webhookId: webhookId);
await SendAsync("DELETE", () => $"webhooks/{webhookId}/{AuthToken}/messages/{messageId}", ids, options: options).ConfigureAwait(false);
await SendAsync("DELETE", () => $"webhooks/{webhookId}/{AuthToken}/messages/{messageId}?{WebhookQuery(false, threadId)}", ids, options: options).ConfigureAwait(false);
}

/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
@@ -873,7 +897,7 @@ namespace Discord.API

/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
/// <exception cref="InvalidOperationException">This operation may only be called with a <see cref="TokenType.Webhook"/> token.</exception>
public async Task<Message> UploadWebhookFileAsync(ulong webhookId, UploadWebhookFileParams args, RequestOptions options = null)
public async Task<Message> UploadWebhookFileAsync(ulong webhookId, UploadWebhookFileParams args, RequestOptions options = null, ulong? threadId = null)
{
if (AuthTokenType != TokenType.Webhook)
throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token.");
@@ -893,7 +917,7 @@ namespace Discord.API
}

var ids = new BucketIds(webhookId: webhookId);
return await SendMultipartAsync<Message>("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false);
return await SendMultipartAsync<Message>("POST", () => $"webhooks/{webhookId}/{AuthToken}?{WebhookQuery(true, threadId)}", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false);
}
public async Task DeleteMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null)
{
@@ -1380,7 +1404,7 @@ namespace Discord.API
if ((!args.Embeds.IsSpecified || args.Embeds.Value == null || args.Embeds.Value.Length == 0) && !args.File.IsSpecified)
Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content));

if(args.Content.IsSpecified && args.Content.Value?.Length > DiscordConfig.MaxMessageSize)
if (args.Content.IsSpecified && args.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(args.Content));

options = RequestOptions.CreateOrClone(options);
@@ -1400,7 +1424,7 @@ namespace Discord.API
throw new ArgumentException(message: $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", paramName: nameof(args.Content));

options = RequestOptions.CreateOrClone(options);
var ids = new BucketIds();
return await SendMultipartAsync<Message>("POST", () => $"webhooks/{CurrentApplicationId}/{token}?wait=true", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false);
}
@@ -1729,8 +1753,10 @@ namespace Discord.API
if (args.TargetType.IsSpecified)
{
Preconditions.NotEqual((int)args.TargetType.Value, (int)TargetUserType.Undefined, nameof(args.TargetType));
if (args.TargetType.Value == TargetUserType.Stream) Preconditions.GreaterThan(args.TargetUserId, 0, nameof(args.TargetUserId));
if (args.TargetType.Value == TargetUserType.EmbeddedApplication) Preconditions.GreaterThan(args.TargetApplicationId, 0, nameof(args.TargetUserId));
if (args.TargetType.Value == TargetUserType.Stream)
Preconditions.GreaterThan(args.TargetUserId, 0, nameof(args.TargetUserId));
if (args.TargetType.Value == TargetUserType.EmbeddedApplication)
Preconditions.GreaterThan(args.TargetApplicationId, 0, nameof(args.TargetUserId));
}
options = RequestOptions.CreateOrClone(options);

@@ -2414,6 +2440,18 @@ namespace Discord.API

return (expr as MemberExpression).Member.Name;
}

private static string WebhookQuery(bool wait = false, ulong? threadId = null)
{
List<string> querys = new List<string>() { };
if (wait)
querys.Add("wait=true");
if (threadId.HasValue)
querys.Add($"thread_id={threadId}");

return $"{string.Join("&", querys)}";
}

#endregion
}
}

+ 15
- 7
src/Discord.Net.Rest/DiscordRestClient.cs View File

@@ -32,9 +32,15 @@ namespace Discord.Rest
/// Initializes a new <see cref="DiscordRestClient"/> with the provided configuration.
/// </summary>
/// <param name="config">The configuration to be used with the client.</param>
public DiscordRestClient(DiscordRestConfig config) : base(config, CreateApiClient(config)) { }
public DiscordRestClient(DiscordRestConfig config) : base(config, CreateApiClient(config))
{
_apiOnCreation = config.APIOnRestInteractionCreation;
}
// used for socket client rest access
internal DiscordRestClient(DiscordRestConfig config, API.DiscordRestApiClient api) : base(config, api) { }
internal DiscordRestClient(DiscordRestConfig config, API.DiscordRestApiClient api) : base(config, api)
{
_apiOnCreation = config.APIOnRestInteractionCreation;
}

private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config)
=> new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent, serializer: Serializer, useSystemClock: config.UseSystemClock, defaultRatelimitCallback: config.DefaultRatelimitCallback);
@@ -82,6 +88,8 @@ namespace Discord.Rest

#region Rest interactions

private readonly bool _apiOnCreation;

public bool IsValidHttpInteraction(string publicKey, string signature, string timestamp, string body)
=> IsValidHttpInteraction(publicKey, signature, timestamp, Encoding.UTF8.GetBytes(body));
public bool IsValidHttpInteraction(string publicKey, string signature, string timestamp, byte[] body)
@@ -113,8 +121,8 @@ namespace Discord.Rest
/// A <see cref="RestInteraction"/> that represents the incoming http interaction.
/// </returns>
/// <exception cref="BadSignatureException">Thrown when the signature doesn't match the public key.</exception>
public Task<RestInteraction> ParseHttpInteractionAsync(string publicKey, string signature, string timestamp, string body)
=> ParseHttpInteractionAsync(publicKey, signature, timestamp, Encoding.UTF8.GetBytes(body));
public Task<RestInteraction> ParseHttpInteractionAsync(string publicKey, string signature, string timestamp, string body, Func<InteractionProperties, bool> doApiCallOnCreation = null)
=> ParseHttpInteractionAsync(publicKey, signature, timestamp, Encoding.UTF8.GetBytes(body), doApiCallOnCreation);

/// <summary>
/// Creates a <see cref="RestInteraction"/> from a http message.
@@ -127,7 +135,7 @@ namespace Discord.Rest
/// A <see cref="RestInteraction"/> that represents the incoming http interaction.
/// </returns>
/// <exception cref="BadSignatureException">Thrown when the signature doesn't match the public key.</exception>
public async Task<RestInteraction> ParseHttpInteractionAsync(string publicKey, string signature, string timestamp, byte[] body)
public async Task<RestInteraction> ParseHttpInteractionAsync(string publicKey, string signature, string timestamp, byte[] body, Func<InteractionProperties, bool> doApiCallOnCreation = null)
{
if (!IsValidHttpInteraction(publicKey, signature, timestamp, body))
{
@@ -138,12 +146,12 @@ namespace Discord.Rest
using (var jsonReader = new JsonTextReader(textReader))
{
var model = Serializer.Deserialize<API.Interaction>(jsonReader);
return await RestInteraction.CreateAsync(this, model);
return await RestInteraction.CreateAsync(this, model, doApiCallOnCreation is not null ? doApiCallOnCreation(new InteractionProperties(model)) : _apiOnCreation);
}
}

#endregion
public async Task<RestApplication> GetApplicationInfoAsync(RequestOptions options = null)
{
return _applicationInfo ??= await ClientHelper.GetApplicationInfoAsync(this, options).ConfigureAwait(false);


+ 2
- 0
src/Discord.Net.Rest/DiscordRestConfig.cs View File

@@ -9,5 +9,7 @@ namespace Discord.Rest
{
/// <summary> Gets or sets the provider used to generate new REST connections. </summary>
public RestClientProvider RestClientProvider { get; set; } = DefaultRestClientProvider.Instance;

public bool APIOnRestInteractionCreation { get; set; } = true;
}
}

+ 4
- 1
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs View File

@@ -18,12 +18,15 @@ namespace Discord.Rest
internal static BanAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId);
return new BanAuditLogData(RestUser.Create(discord, userInfo));
return new BanAuditLogData((userInfo != null) ? RestUser.Create(discord, userInfo) : null);
}

/// <summary>
/// Gets the user that was banned.
/// </summary>
/// <remarks>
/// Will be <see langword="null"/> if the user is a 'Deleted User#....' because Discord does send user data for deleted users.
/// </remarks>
/// <returns>
/// A user object representing the banned user.
/// </returns>


+ 4
- 1
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs View File

@@ -18,12 +18,15 @@ namespace Discord.Rest
internal static BotAddAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId);
return new BotAddAuditLogData(RestUser.Create(discord, userInfo));
return new BotAddAuditLogData((userInfo != null) ? RestUser.Create(discord, userInfo) : null);
}

/// <summary>
/// Gets the bot that was added.
/// </summary>
/// <remarks>
/// Will be <see langword="null"/> if the bot is a 'Deleted User#....' because Discord does send user data for deleted users.
/// </remarks>
/// <returns>
/// A user object representing the bot.
/// </returns>


+ 4
- 1
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs View File

@@ -45,7 +45,7 @@ namespace Discord.Rest
{
var inviterId = inviterIdModel.NewValue.ToObject<ulong>(discord.ApiClient.Serializer);
var inviterInfo = log.Users.FirstOrDefault(x => x.Id == inviterId);
inviter = RestUser.Create(discord, inviterInfo);
inviter = (inviterInfo != null) ? RestUser.Create(discord, inviterInfo) : null;
}

return new InviteCreateAuditLogData(maxAge, code, temporary, inviter, channelId, uses, maxUses);
@@ -76,6 +76,9 @@ namespace Discord.Rest
/// <summary>
/// Gets the user that created this invite if available.
/// </summary>
/// <remarks>
/// Will be <see langword="null"/> if the user is a 'Deleted User#....' because Discord does send user data for deleted users.
/// </remarks>
/// <returns>
/// A user that created this invite or <see langword="null"/>.
/// </returns>


+ 4
- 1
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs View File

@@ -45,7 +45,7 @@ namespace Discord.Rest
{
var inviterId = inviterIdModel.OldValue.ToObject<ulong>(discord.ApiClient.Serializer);
var inviterInfo = log.Users.FirstOrDefault(x => x.Id == inviterId);
inviter = RestUser.Create(discord, inviterInfo);
inviter = (inviterInfo != null) ? RestUser.Create(discord, inviterInfo) : null;
}

return new InviteDeleteAuditLogData(maxAge, code, temporary, inviter, channelId, uses, maxUses);
@@ -76,6 +76,9 @@ namespace Discord.Rest
/// <summary>
/// Gets the user that created this invite if available.
/// </summary>
/// <remarks>
/// Will be <see langword="null"/> if the user is a 'Deleted User#....' because Discord does send user data for deleted users.
/// </remarks>
/// <returns>
/// A user that created this invite or <see langword="null"/>.
/// </returns>


+ 5
- 2
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/KickAuditLogData.cs View File

@@ -1,4 +1,4 @@
using System.Linq;
using System.Linq;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;
@@ -18,12 +18,15 @@ namespace Discord.Rest
internal static KickAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId);
return new KickAuditLogData(RestUser.Create(discord, userInfo));
return new KickAuditLogData((userInfo != null) ? RestUser.Create(discord, userInfo) : null);
}

/// <summary>
/// Gets the user that was kicked.
/// </summary>
/// <remarks>
/// Will be <see langword="null"/> if the user is a 'Deleted User#....' because Discord does send user data for deleted users.
/// </remarks>
/// <returns>
/// A user object representing the kicked user.
/// </returns>


+ 1
- 1
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs View File

@@ -27,7 +27,7 @@ namespace Discord.Rest
.ToList();

var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId);
var user = RestUser.Create(discord, userInfo);
RestUser user = (userInfo != null) ? RestUser.Create(discord, userInfo) : null;

return new MemberRoleAuditLogData(roleInfos.ToReadOnlyCollection(), user);
}


+ 4
- 1
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs View File

@@ -33,7 +33,7 @@ namespace Discord.Rest
newMute = muteModel?.NewValue?.ToObject<bool>(discord.ApiClient.Serializer);

var targetInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId);
var user = RestUser.Create(discord, targetInfo);
RestUser user = (targetInfo != null) ? RestUser.Create(discord, targetInfo) : null;

var before = new MemberInfo(oldNick, oldDeaf, oldMute);
var after = new MemberInfo(newNick, newDeaf, newMute);
@@ -44,6 +44,9 @@ namespace Discord.Rest
/// <summary>
/// Gets the user that the changes were performed on.
/// </summary>
/// <remarks>
/// Will be <see langword="null"/> if the user is a 'Deleted User#....' because Discord does send user data for deleted users.
/// </remarks>
/// <returns>
/// A user object representing the user who the changes were performed on.
/// </returns>


+ 5
- 1
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs View File

@@ -2,6 +2,7 @@ using System.Linq;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;
using System;

namespace Discord.Rest
{
@@ -20,7 +21,7 @@ namespace Discord.Rest
internal static MessageDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId);
return new MessageDeleteAuditLogData(entry.Options.ChannelId.Value, entry.Options.Count.Value, RestUser.Create(discord, userInfo));
return new MessageDeleteAuditLogData(entry.Options.ChannelId.Value, entry.Options.Count.Value, userInfo != null ? RestUser.Create(discord, userInfo) : null);
}

/// <summary>
@@ -41,6 +42,9 @@ namespace Discord.Rest
/// <summary>
/// Gets the user of the messages that were deleted.
/// </summary>
/// <remarks>
/// Will be <see langword="null"/> if the user is a 'Deleted User#....' because Discord does send user data for deleted users.
/// </remarks>
/// <returns>
/// A user object representing the user that created the deleted messages.
/// </returns>


+ 4
- 1
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs View File

@@ -23,7 +23,7 @@ namespace Discord.Rest
if (entry.TargetId.HasValue)
{
var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId);
user = RestUser.Create(discord, userInfo);
user = (userInfo != null) ? RestUser.Create(discord, userInfo) : null;
}

return new MessagePinAuditLogData(entry.Options.MessageId.Value, entry.Options.ChannelId.Value, user);
@@ -46,6 +46,9 @@ namespace Discord.Rest
/// <summary>
/// Gets the user of the message that was pinned if available.
/// </summary>
/// <remarks>
/// Will be <see langword="null"/> if the user is a 'Deleted User#....' because Discord does send user data for deleted users.
/// </remarks>
/// <returns>
/// A user object representing the user that created the pinned message or <see langword="null"/>.
/// </returns>


+ 4
- 1
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs View File

@@ -23,7 +23,7 @@ namespace Discord.Rest
if (entry.TargetId.HasValue)
{
var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId);
user = RestUser.Create(discord, userInfo);
user = (userInfo != null) ? RestUser.Create(discord, userInfo) : null;
}

return new MessageUnpinAuditLogData(entry.Options.MessageId.Value, entry.Options.ChannelId.Value, user);
@@ -46,6 +46,9 @@ namespace Discord.Rest
/// <summary>
/// Gets the user of the message that was unpinned if available.
/// </summary>
/// <remarks>
/// Will be <see langword="null"/> if the user is a 'Deleted User#....' because Discord does send user data for deleted users.
/// </remarks>
/// <returns>
/// A user object representing the user that created the unpinned message or <see langword="null"/>.
/// </returns>


+ 2
- 2
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/UnbanAuditLogData.cs View File

@@ -1,4 +1,4 @@
using System.Linq;
using System.Linq;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;
@@ -18,7 +18,7 @@ namespace Discord.Rest
internal static UnbanAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId);
return new UnbanAuditLogData(RestUser.Create(discord, userInfo));
return new UnbanAuditLogData((userInfo != null) ? RestUser.Create(discord, userInfo) : null);
}

/// <summary>


+ 131
- 0
src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs View File

@@ -0,0 +1,131 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Model = Discord.API.Channel;

namespace Discord.Rest
{
/// <summary>
/// Represents a REST-based forum channel in a guild.
/// </summary>
public class RestForumChannel : RestGuildChannel, IForumChannel
{
/// <inheritdoc/>
public bool IsNsfw { get; private set; }

/// <inheritdoc/>
public string Topic { get; private set; }

/// <inheritdoc/>
public ThreadArchiveDuration DefaultAutoArchiveDuration { get; private set; }

/// <inheritdoc/>
public IReadOnlyCollection<ForumTag> Tags { get; private set; }

/// <inheritdoc/>
public string Mention => MentionUtils.MentionChannel(Id);

internal RestForumChannel(BaseDiscordClient client, IGuild guild, ulong id)
: base(client, guild, id)
{

}

internal new static RestStageChannel Create(BaseDiscordClient discord, IGuild guild, Model model)
{
var entity = new RestStageChannel(discord, guild, model.Id);
entity.Update(model);
return entity;
}

internal override void Update(Model model)
{
base.Update(model);
IsNsfw = model.Nsfw.GetValueOrDefault(false);
Topic = model.Topic.GetValueOrDefault();
DefaultAutoArchiveDuration = model.AutoArchiveDuration.GetValueOrDefault(ThreadArchiveDuration.OneDay);

Tags = model.ForumTags.GetValueOrDefault(Array.Empty<API.ForumTags>()).Select(
x => new ForumTag(x.Id, x.Name, x.EmojiId.GetValueOrDefault(null), x.EmojiName.GetValueOrDefault())
).ToImmutableArray();
}

/// <inheritdoc cref="IForumChannel.CreatePostAsync(string, ThreadArchiveDuration, int?, string, Embed, RequestOptions, AllowedMentions, MessageComponent, ISticker[], Embed[], MessageFlags)"/>
public Task<RestThreadChannel> CreatePostAsync(string title, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None)
=> ThreadHelper.CreatePostAsync(this, Discord, title, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags);

/// <inheritdoc cref="IForumChannel.CreatePostWithFileAsync(string, string, ThreadArchiveDuration, int?, string, Embed, RequestOptions, bool, AllowedMentions, MessageComponent, ISticker[], Embed[], MessageFlags)"/>
public async Task<RestThreadChannel> CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay,
int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false,
AllowedMentions allowedMentions = null, MessageComponent components = null,
ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None)
{
using var file = new FileAttachment(filePath, isSpoiler: isSpoiler);
return await ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { file }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false);
}

/// <inheritdoc cref="IForumChannel.CreatePostWithFileAsync(string, Stream, string, ThreadArchiveDuration, int?, string, Embed, RequestOptions, bool, AllowedMentions, MessageComponent, ISticker[], Embed[], MessageFlags)"/>
public async Task<RestThreadChannel> CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay,
int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false,
AllowedMentions allowedMentions = null, MessageComponent components = null,
ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None)
{
using var file = new FileAttachment(stream, filename, isSpoiler: isSpoiler);
return await ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { file }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false);
}

/// <inheritdoc cref="IForumChannel.CreatePostWithFileAsync(string, FileAttachment, ThreadArchiveDuration, int?, string, Embed, RequestOptions, AllowedMentions, MessageComponent, ISticker[], Embed[], MessageFlags)"/>
public Task<RestThreadChannel> CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay,
int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null,
MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None)
=> ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { attachment }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags);

/// <inheritdoc cref="IForumChannel.CreatePostWithFilesAsync(string, IEnumerable{FileAttachment}, ThreadArchiveDuration, int?, string, Embed, RequestOptions, AllowedMentions, MessageComponent, ISticker[], Embed[], MessageFlags)"/>
public Task<RestThreadChannel> CreatePostWithFilesAsync(string title, IEnumerable<FileAttachment> attachments, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay,
int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null,
MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None)
=> ThreadHelper.CreatePostAsync(this, Discord, title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags);

/// <inheritdoc cref="IForumChannel.GetActiveThreadsAsync(RequestOptions)"/>
public Task<IReadOnlyCollection<RestThreadChannel>> GetActiveThreadsAsync(RequestOptions options = null)
=> ThreadHelper.GetActiveThreadsAsync(Guild, Discord, options);

/// <inheritdoc cref="IForumChannel.GetJoinedPrivateArchivedThreadsAsync(int?, DateTimeOffset?, RequestOptions)"/>
public Task<IReadOnlyCollection<RestThreadChannel>> GetJoinedPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null)
=> ThreadHelper.GetJoinedPrivateArchivedThreadsAsync(this, Discord, limit, before, options);

/// <inheritdoc cref="IForumChannel.GetPrivateArchivedThreadsAsync(int?, DateTimeOffset?, RequestOptions)"/>
public Task<IReadOnlyCollection<RestThreadChannel>> GetPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null)
=> ThreadHelper.GetPrivateArchivedThreadsAsync(this, Discord, limit, before, options);

/// <inheritdoc cref="IForumChannel.GetPublicArchivedThreadsAsync(int?, DateTimeOffset?, RequestOptions)"/>
public Task<IReadOnlyCollection<RestThreadChannel>> GetPublicArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null)
=> ThreadHelper.GetPublicArchivedThreadsAsync(this, Discord, limit, before, options);

#region IForumChannel
async Task<IReadOnlyCollection<IThreadChannel>> IForumChannel.GetActiveThreadsAsync(RequestOptions options)
=> await GetActiveThreadsAsync(options).ConfigureAwait(false);
async Task<IReadOnlyCollection<IThreadChannel>> IForumChannel.GetPublicArchivedThreadsAsync(int? limit, DateTimeOffset? before, RequestOptions options)
=> await GetPublicArchivedThreadsAsync(limit, before, options).ConfigureAwait(false);
async Task<IReadOnlyCollection<IThreadChannel>> IForumChannel.GetPrivateArchivedThreadsAsync(int? limit, DateTimeOffset? before, RequestOptions options)
=> await GetPrivateArchivedThreadsAsync(limit, before, options).ConfigureAwait(false);
async Task<IReadOnlyCollection<IThreadChannel>> IForumChannel.GetJoinedPrivateArchivedThreadsAsync(int? limit, DateTimeOffset? before, RequestOptions options)
=> await GetJoinedPrivateArchivedThreadsAsync(limit, before, options).ConfigureAwait(false);
async Task<IThreadChannel> IForumChannel.CreatePostAsync(string title, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags)
=> await CreatePostAsync(title, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false);
async Task<IThreadChannel> IForumChannel.CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags)
=> await CreatePostWithFileAsync(title, filePath, archiveDuration, slowmode, text, embed, options, isSpoiler, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false);
async Task<IThreadChannel> IForumChannel.CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags)
=> await CreatePostWithFileAsync(title, stream, filename, archiveDuration, slowmode, text, embed, options, isSpoiler, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false);
async Task<IThreadChannel> IForumChannel.CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags)
=> await CreatePostWithFileAsync(title, attachment, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false);
async Task<IThreadChannel> IForumChannel.CreatePostWithFilesAsync(string title, IEnumerable<FileAttachment> attachments, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags)
=> await CreatePostWithFilesAsync(title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags);

#endregion
}
}

+ 1
- 0
src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs View File

@@ -39,6 +39,7 @@ namespace Discord.Rest
ChannelType.Text => RestTextChannel.Create(discord, guild, model),
ChannelType.Voice => RestVoiceChannel.Create(discord, guild, model),
ChannelType.Stage => RestStageChannel.Create(discord, guild, model),
ChannelType.Forum => RestForumChannel.Create(discord, guild, model),
ChannelType.Category => RestCategoryChannel.Create(discord, guild, model),
ChannelType.PublicThread or ChannelType.PrivateThread or ChannelType.NewsThread => RestThreadChannel.Create(discord, guild, model),
_ => new RestGuildChannel(discord, guild, model.Id),


+ 5
- 3
src/Discord.Net.Rest/Entities/Channels/RestStageChannel.cs View File

@@ -12,7 +12,11 @@ namespace Discord.Rest
public class RestStageChannel : RestVoiceChannel, IStageChannel
{
/// <inheritdoc/>
public string Topic { get; private set; }
/// <remarks>
/// This field is always false for stage channels.
/// </remarks>
public override bool IsTextInVoice
=> false;

/// <inheritdoc/>
public StagePrivacyLevel? PrivacyLevel { get; private set; }
@@ -37,13 +41,11 @@ namespace Discord.Rest
IsLive = isLive;
if(isLive)
{
Topic = model.Topic;
PrivacyLevel = model.PrivacyLevel;
IsDiscoverableDisabled = model.DiscoverableDisabled;
}
else
{
Topic = null;
PrivacyLevel = null;
IsDiscoverableDisabled = null;
}


+ 57
- 50
src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs View File

@@ -21,11 +21,12 @@ namespace Discord.Rest
public virtual int SlowModeInterval { get; private set; }
/// <inheritdoc />
public ulong? CategoryId { get; private set; }

/// <inheritdoc />
public string Mention => MentionUtils.MentionChannel(Id);
/// <inheritdoc />
public bool IsNsfw { get; private set; }
/// <inheritdoc />
public ThreadArchiveDuration DefaultArchiveDuration { get; private set; }

internal RestTextChannel(BaseDiscordClient discord, IGuild guild, ulong id)
: base(discord, guild, id)
@@ -46,6 +47,12 @@ namespace Discord.Rest
if (model.SlowMode.IsSpecified)
SlowModeInterval = model.SlowMode.Value;
IsNsfw = model.Nsfw.GetValueOrDefault();

if (model.AutoArchiveDuration.IsSpecified)
DefaultArchiveDuration = model.AutoArchiveDuration.Value;
else
DefaultArchiveDuration = ThreadArchiveDuration.OneDay;
// basic value at channel creation. Shouldn't be called since guild text channels always have this property
}

/// <inheritdoc />
@@ -86,25 +93,25 @@ namespace Discord.Rest
=> ChannelHelper.GetUsersAsync(this, Guild, Discord, null, null, options);

/// <inheritdoc />
public Task<RestMessage> GetMessageAsync(ulong id, RequestOptions options = null)
public virtual Task<RestMessage> GetMessageAsync(ulong id, RequestOptions options = null)
=> ChannelHelper.GetMessageAsync(this, Discord, id, options);
/// <inheritdoc />
public IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null)
public virtual IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null)
=> ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, options);
/// <inheritdoc />
public IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null)
public virtual IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null)
=> ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, options);
/// <inheritdoc />
public IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null)
public virtual IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null)
=> ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, options);
/// <inheritdoc />
public Task<IReadOnlyCollection<RestMessage>> GetPinnedMessagesAsync(RequestOptions options = null)
public virtual Task<IReadOnlyCollection<RestMessage>> GetPinnedMessagesAsync(RequestOptions options = null)
=> ChannelHelper.GetPinnedMessagesAsync(this, Discord, options);

/// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
/// <exception cref="ArgumentException">The only valid <see cref="MessageFlags"/> are <see cref="MessageFlags.SuppressEmbeds"/> and <see cref="MessageFlags.None"/>.</exception>
public Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null,
public virtual Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null,
RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null,
MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None)
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference,
@@ -136,7 +143,7 @@ namespace Discord.Rest
/// <exception cref="IOException">An I/O error occurred while opening the file.</exception>
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
/// <exception cref="ArgumentException">The only valid <see cref="MessageFlags"/> are <see cref="MessageFlags.SuppressEmbeds"/> and <see cref="MessageFlags.None"/>.</exception>
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null,
public virtual Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null,
RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null,
MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null,
Embed[] embeds = null, MessageFlags flags = MessageFlags.None)
@@ -146,7 +153,7 @@ namespace Discord.Rest
/// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
/// <exception cref="ArgumentException">The only valid <see cref="MessageFlags"/> are <see cref="MessageFlags.SuppressEmbeds"/> and <see cref="MessageFlags.None"/>.</exception>
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false,
public virtual Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false,
Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null,
MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null,
Embed[] embeds = null, MessageFlags flags = MessageFlags.None)
@@ -156,7 +163,7 @@ namespace Discord.Rest
/// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
/// <exception cref="ArgumentException">The only valid <see cref="MessageFlags"/> are <see cref="MessageFlags.SuppressEmbeds"/> and <see cref="MessageFlags.None"/>.</exception>
public Task<RestUserMessage> SendFileAsync(FileAttachment attachment, string text, bool isTTS = false,
public virtual Task<RestUserMessage> SendFileAsync(FileAttachment attachment, string text, bool isTTS = false,
Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null,
MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null,
Embed[] embeds = null, MessageFlags flags = MessageFlags.None)
@@ -166,35 +173,35 @@ namespace Discord.Rest
/// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
/// <exception cref="ArgumentException">The only valid <see cref="MessageFlags"/> are <see cref="MessageFlags.SuppressEmbeds"/> and <see cref="MessageFlags.None"/>.</exception>
public Task<RestUserMessage> SendFilesAsync(IEnumerable<FileAttachment> attachments, string text, bool isTTS = false,
public virtual Task<RestUserMessage> SendFilesAsync(IEnumerable<FileAttachment> attachments, string text, bool isTTS = false,
Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null,
MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null,
Embed[] embeds = null, MessageFlags flags = MessageFlags.None)
=> ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds, flags);

/// <inheritdoc />
public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null)
public virtual Task DeleteMessageAsync(ulong messageId, RequestOptions options = null)
=> ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options);
/// <inheritdoc />
public Task DeleteMessageAsync(IMessage message, RequestOptions options = null)
public virtual Task DeleteMessageAsync(IMessage message, RequestOptions options = null)
=> ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options);

/// <inheritdoc />
public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null)
public virtual Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null)
=> ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options);
/// <inheritdoc />
public Task DeleteMessagesAsync(IEnumerable<ulong> messageIds, RequestOptions options = null)
public virtual Task DeleteMessagesAsync(IEnumerable<ulong> messageIds, RequestOptions options = null)
=> ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options);

/// <inheritdoc />
public async Task<IUserMessage> ModifyMessageAsync(ulong messageId, Action<MessageProperties> func, RequestOptions options = null)
public virtual async Task<IUserMessage> ModifyMessageAsync(ulong messageId, Action<MessageProperties> func, RequestOptions options = null)
=> await ChannelHelper.ModifyMessageAsync(this, messageId, func, Discord, options).ConfigureAwait(false);

/// <inheritdoc />
public Task TriggerTypingAsync(RequestOptions options = null)
public virtual Task TriggerTypingAsync(RequestOptions options = null)
=> ChannelHelper.TriggerTypingAsync(this, Discord, options);
/// <inheritdoc />
public IDisposable EnterTypingState(RequestOptions options = null)
public virtual IDisposable EnterTypingState(RequestOptions options = null)
=> ChannelHelper.EnterTypingState(this, Discord, options);

/// <summary>
@@ -231,38 +238,6 @@ namespace Discord.Rest
public virtual Task<IReadOnlyCollection<RestWebhook>> GetWebhooksAsync(RequestOptions options = null)
=> ChannelHelper.GetWebhooksAsync(this, Discord, options);

/// <summary>
/// Gets the parent (category) channel of this channel.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the category channel
/// representing the parent of this channel; <c>null</c> if none is set.
/// </returns>
public virtual Task<ICategoryChannel> GetCategoryAsync(RequestOptions options = null)
=> ChannelHelper.GetCategoryAsync(this, Discord, options);
/// <inheritdoc />
public Task SyncPermissionsAsync(RequestOptions options = null)
=> ChannelHelper.SyncPermissionsAsync(this, Discord, options);
#endregion

#region Invites
/// <inheritdoc />
public virtual async Task<IInviteMetadata> CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false);
public virtual async Task<IInviteMetadata> CreateInviteToApplicationAsync(ulong applicationId, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options);
/// <inheritdoc />
public virtual async Task<IInviteMetadata> CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options);
public virtual Task<IInviteMetadata> CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> throw new NotImplementedException();
/// <inheritdoc />
public virtual async Task<IReadOnlyCollection<IInviteMetadata>> GetInvitesAsync(RequestOptions options = null)
=> await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false);

private string DebuggerDisplay => $"{Name} ({Id}, Text)";

/// <summary>
/// Creates a thread within this <see cref="ITextChannel"/>.
/// </summary>
@@ -299,6 +274,38 @@ namespace Discord.Rest
var model = await ThreadHelper.CreateThreadAsync(Discord, this, name, type, autoArchiveDuration, message, invitable, slowmode, options);
return RestThreadChannel.Create(Discord, Guild, model);
}

/// <summary>
/// Gets the parent (category) channel of this channel.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the category channel
/// representing the parent of this channel; <c>null</c> if none is set.
/// </returns>
public virtual Task<ICategoryChannel> GetCategoryAsync(RequestOptions options = null)
=> ChannelHelper.GetCategoryAsync(this, Discord, options);
/// <inheritdoc />
public Task SyncPermissionsAsync(RequestOptions options = null)
=> ChannelHelper.SyncPermissionsAsync(this, Discord, options);
#endregion

#region Invites
/// <inheritdoc />
public virtual async Task<IInviteMetadata> CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false);
public virtual async Task<IInviteMetadata> CreateInviteToApplicationAsync(ulong applicationId, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options);
/// <inheritdoc />
public virtual async Task<IInviteMetadata> CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options);
public virtual Task<IInviteMetadata> CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> throw new NotImplementedException();
/// <inheritdoc />
public virtual async Task<IReadOnlyCollection<IInviteMetadata>> GetInvitesAsync(RequestOptions options = null)
=> await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false);

private string DebuggerDisplay => $"{Name} ({Id}, Text)";
#endregion

#region ITextChannel


+ 181
- 37
src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs View File

@@ -2,6 +2,7 @@ using Discord.Audio;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Model = Discord.API.Channel;
@@ -12,21 +13,21 @@ namespace Discord.Rest
/// Represents a REST-based voice channel in a guild.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class RestVoiceChannel : RestGuildChannel, IVoiceChannel, IRestAudioChannel
public class RestVoiceChannel : RestTextChannel, IVoiceChannel, IRestAudioChannel
{
#region RestVoiceChannel
/// <summary>
/// Gets whether or not the guild has Text-In-Voice enabled and the voice channel is a TiV channel.
/// </summary>
public virtual bool IsTextInVoice
=> Guild.Features.HasTextInVoice;
/// <inheritdoc />
public int Bitrate { get; private set; }
/// <inheritdoc />
public int? UserLimit { get; private set; }
/// <inheritdoc />
public ulong? CategoryId { get; private set; }
/// <inheritdoc/>
public string RTCRegion { get; private set; }

/// <inheritdoc />
public string Mention => MentionUtils.MentionChannel(Id);

internal RestVoiceChannel(BaseDiscordClient discord, IGuild guild, ulong id)
: base(discord, guild, id)
{
@@ -41,7 +42,6 @@ namespace Discord.Rest
internal override void Update(Model model)
{
base.Update(model);
CategoryId = model.CategoryId;

if(model.Bitrate.IsSpecified)
Bitrate = model.Bitrate.Value;
@@ -59,41 +59,185 @@ namespace Discord.Rest
Update(model);
}

/// <summary>
/// Gets the parent (category) channel of this channel.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the category channel
/// representing the parent of this channel; <c>null</c> if none is set.
/// </returns>
public Task<ICategoryChannel> GetCategoryAsync(RequestOptions options = null)
=> ChannelHelper.GetCategoryAsync(this, Discord, options);
/// <inheritdoc />
public Task SyncPermissionsAsync(RequestOptions options = null)
=> ChannelHelper.SyncPermissionsAsync(this, Discord, options);
#endregion
/// <inheritdoc/>
/// <exception cref="InvalidOperationException">Cannot modify text channel properties of a voice channel.</exception>
public override Task ModifyAsync(Action<TextChannelProperties> func, RequestOptions options = null)
=> throw new InvalidOperationException("Cannot modify text channel properties of a voice channel");

#region Invites
/// <inheritdoc />
public async Task<IInviteMetadata> CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false);
/// <inheritdoc />
public async Task<IInviteMetadata> CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options).ConfigureAwait(false);
/// <inheritdoc />
public virtual async Task<IInviteMetadata> CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options);
/// <inheritdoc />
public async Task<IInviteMetadata> CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteToStreamAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, user, options).ConfigureAwait(false);
/// <inheritdoc />
public async Task<IReadOnlyCollection<IInviteMetadata>> GetInvitesAsync(RequestOptions options = null)
=> await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false);
/// <inheritdoc/>
/// <exception cref="InvalidOperationException">Cannot create a thread within a voice channel.</exception>
public override Task<RestThreadChannel> CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, bool? invitable = null, int? slowmode = null, RequestOptions options = null)
=> throw new InvalidOperationException("Cannot create a thread within a voice channel");

#endregion

private string DebuggerDisplay => $"{Name} ({Id}, Voice)";

#region TextOverrides

/// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception>
public override Task<RestMessage> GetMessageAsync(ulong id, RequestOptions options = null)
{
if (!IsTextInVoice)
throw new NotSupportedException("This function is only supported in Text-In-Voice channels");
return base.GetMessageAsync(id, options);
}

/// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception>
public override Task DeleteMessageAsync(IMessage message, RequestOptions options = null)
{
if (!IsTextInVoice)
throw new NotSupportedException("This function is only supported in Text-In-Voice channels");
return base.DeleteMessageAsync(message, options);
}

/// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception>
public override Task DeleteMessageAsync(ulong messageId, RequestOptions options = null)
{
if (!IsTextInVoice)
throw new NotSupportedException("This function is only supported in Text-In-Voice channels");
return base.DeleteMessageAsync(messageId, options);
}

/// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception>
public override Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null)
{
if (!IsTextInVoice)
throw new NotSupportedException("This function is only supported in Text-In-Voice channels");
return base.DeleteMessagesAsync(messages, options);
}

/// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception>
public override Task DeleteMessagesAsync(IEnumerable<ulong> messageIds, RequestOptions options = null)
{
if (!IsTextInVoice)
throw new NotSupportedException("This function is only supported in Text-In-Voice channels");
return base.DeleteMessagesAsync(messageIds, options);
}

/// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception>
public override IDisposable EnterTypingState(RequestOptions options = null)
{
if (!IsTextInVoice)
throw new NotSupportedException("This function is only supported in Text-In-Voice channels");
return base.EnterTypingState(options);
}

/// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception>
public override IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = 100, RequestOptions options = null)
{
if (!IsTextInVoice)
throw new NotSupportedException("This function is only supported in Text-In-Voice channels");
return base.GetMessagesAsync(fromMessage, dir, limit, options);
}

/// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception>
public override IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(int limit = 100, RequestOptions options = null)
{
if (!IsTextInVoice)
throw new NotSupportedException("This function is only supported in Text-In-Voice channels");
return base.GetMessagesAsync(limit, options);
}

/// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception>
public override IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = 100, RequestOptions options = null)
{
if (!IsTextInVoice)
throw new NotSupportedException("This function is only supported in Text-In-Voice channels");
return base.GetMessagesAsync(fromMessageId, dir, limit, options);
}

/// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception>
public override Task<IReadOnlyCollection<RestMessage>> GetPinnedMessagesAsync(RequestOptions options = null)
{
if (!IsTextInVoice)
throw new NotSupportedException("This function is only supported in Text-In-Voice channels");
return base.GetPinnedMessagesAsync(options);
}

/// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception>
public override Task<RestWebhook> GetWebhookAsync(ulong id, RequestOptions options = null)
{
if (!IsTextInVoice)
throw new NotSupportedException("This function is only supported in Text-In-Voice channels");
return base.GetWebhookAsync(id, options);
}

/// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception>
public override Task<IReadOnlyCollection<RestWebhook>> GetWebhooksAsync(RequestOptions options = null)
{
if (!IsTextInVoice)
throw new NotSupportedException("This function is only supported in Text-In-Voice channels");
return base.GetWebhooksAsync(options);
}

/// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception>
public override Task<RestWebhook> CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null)
{
if (!IsTextInVoice)
throw new NotSupportedException("This function is only supported in Text-In-Voice channels");
return base.CreateWebhookAsync(name, avatar, options);
}

/// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception>
public override Task<IUserMessage> ModifyMessageAsync(ulong messageId, Action<MessageProperties> func, RequestOptions options = null)
{
if (!IsTextInVoice)
throw new NotSupportedException("This function is only supported in Text-In-Voice channels");
return base.ModifyMessageAsync(messageId, func, options);
}

/// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception>
public override Task<RestUserMessage> SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None)
{
if (!IsTextInVoice)
throw new NotSupportedException("This function is only supported in Text-In-Voice channels");
return base.SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags);
}

/// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception>
public override Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None)
{
if (!IsTextInVoice)
throw new NotSupportedException("This function is only supported in Text-In-Voice channels");
return base.SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds, flags);
}

/// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception>
public override Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None)
{
if (!IsTextInVoice)
throw new NotSupportedException("This function is only supported in Text-In-Voice channels");
return base.SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds, flags);
}

/// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception>
public override Task<RestUserMessage> SendFilesAsync(IEnumerable<FileAttachment> attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None)
{
if (!IsTextInVoice)
throw new NotSupportedException("This function is only supported in Text-In-Voice channels");
return base.SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags);
}

/// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception>
public override Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None)
{
if (!IsTextInVoice)
throw new NotSupportedException("This function is only supported in Text-In-Voice channels");
return base.SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags);
}

/// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception>
public override Task TriggerTypingAsync(RequestOptions options = null)
{
if (!IsTextInVoice)
throw new NotSupportedException("This function is only supported in Text-In-Voice channels");
return base.TriggerTypingAsync(options);
}

#endregion


#region IAudioChannel
/// <inheritdoc />
/// <exception cref="NotSupportedException">Connecting to a REST-based channel is not supported.</exception>


+ 138
- 0
src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs View File

@@ -1,5 +1,7 @@
using Discord.API.Rest;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Model = Discord.API.Channel;
@@ -60,6 +62,33 @@ namespace Discord.Rest
return await client.ApiClient.ModifyThreadAsync(channel.Id, apiArgs, options).ConfigureAwait(false);
}

public static async Task<IReadOnlyCollection<RestThreadChannel>> GetActiveThreadsAsync(IGuild guild, BaseDiscordClient client, RequestOptions options)
{
var result = await client.ApiClient.GetActiveThreadsAsync(guild.Id, options).ConfigureAwait(false);
return result.Threads.Select(x => RestThreadChannel.Create(client, guild, x)).ToImmutableArray();
}

public static async Task<IReadOnlyCollection<RestThreadChannel>> GetPublicArchivedThreadsAsync(IGuildChannel channel, BaseDiscordClient client, int? limit = null,
DateTimeOffset? before = null, RequestOptions options = null)
{
var result = await client.ApiClient.GetPublicArchivedThreadsAsync(channel.Id, before, limit, options);
return result.Threads.Select(x => RestThreadChannel.Create(client, channel.Guild, x)).ToImmutableArray();
}

public static async Task<IReadOnlyCollection<RestThreadChannel>> GetPrivateArchivedThreadsAsync(IGuildChannel channel, BaseDiscordClient client, int? limit = null,
DateTimeOffset? before = null, RequestOptions options = null)
{
var result = await client.ApiClient.GetPrivateArchivedThreadsAsync(channel.Id, before, limit, options);
return result.Threads.Select(x => RestThreadChannel.Create(client, channel.Guild, x)).ToImmutableArray();
}

public static async Task<IReadOnlyCollection<RestThreadChannel>> GetJoinedPrivateArchivedThreadsAsync(IGuildChannel channel, BaseDiscordClient client, int? limit = null,
DateTimeOffset? before = null, RequestOptions options = null)
{
var result = await client.ApiClient.GetJoinedPrivateArchivedThreadsAsync(channel.Id, before, limit, options);
return result.Threads.Select(x => RestThreadChannel.Create(client, channel.Guild, x)).ToImmutableArray();
}

public static async Task<RestThreadUser[]> GetUsersAsync(IThreadChannel channel, BaseDiscordClient client, RequestOptions options = null)
{
var users = await client.ApiClient.ListThreadMembersAsync(channel.Id, options);
@@ -73,5 +102,114 @@ namespace Discord.Rest

return RestThreadUser.Create(client, channel.Guild, model, channel);
}

public static async Task<RestThreadChannel> CreatePostAsync(IForumChannel channel, BaseDiscordClient client, string title, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None)
{
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));
}
}

if (stickers != null)
{
Preconditions.AtMost(stickers.Length, 3, nameof(stickers), "A max of 3 stickers are allowed.");
}


if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds)
throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds and none.", nameof(flags));

var args = new CreatePostParams()
{
Title = title,
ArchiveDuration = archiveDuration,
Slowmode = slowmode,
Message = new()
{
AllowedMentions = allowedMentions.ToModel(),
Content = text,
Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional<API.Embed[]>.Unspecified,
Flags = flags,
Components = components?.Components?.Any() ?? false ? components.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional<API.ActionRowComponent[]>.Unspecified,
Stickers = stickers?.Any() ?? false ? stickers.Select(x => x.Id).ToArray() : Optional<ulong[]>.Unspecified,
}
};

var model = await client.ApiClient.CreatePostAsync(channel.Id, args, options).ConfigureAwait(false);

return RestThreadChannel.Create(client, channel.Guild, model);
}

public static async Task<RestThreadChannel> CreatePostAsync(IForumChannel channel, BaseDiscordClient client, string title, IEnumerable<FileAttachment> attachments, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags)
{
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));
}
}

if (stickers != null)
{
Preconditions.AtMost(stickers.Length, 3, nameof(stickers), "A max of 3 stickers are allowed.");
}


if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds)
throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds and none.", nameof(flags));

var args = new CreateMultipartPostAsync(attachments.ToArray())
{
AllowedMentions = allowedMentions.ToModel(),
ArchiveDuration = archiveDuration,
Content = text,
Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional<API.Embed[]>.Unspecified,
Flags = flags,
MessageComponent = components?.Components?.Any() ?? false ? components.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional<API.ActionRowComponent[]>.Unspecified,
Slowmode = slowmode,
Stickers = stickers?.Any() ?? false ? stickers.Select(x => x.Id).ToArray() : Optional<ulong[]>.Unspecified,
Title = title
};

var model = await client.ApiClient.CreatePostAsync(channel.Id, args, options);

return RestThreadChannel.Create(client, channel.Guild, model);
}
}
}

+ 9
- 6
src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs View File

@@ -132,12 +132,15 @@ namespace Discord.Rest
}
public static ulong GetUploadLimit(IGuild guild)
{
return guild.PremiumTier switch
var tierFactor = guild.PremiumTier switch
{
PremiumTier.Tier2 => 50ul * 1000000,
PremiumTier.Tier3 => 100ul * 1000000,
_ => 8ul * 1000000
PremiumTier.Tier2 => 50,
PremiumTier.Tier3 => 100,
_ => 8
};

var mebibyte = Math.Pow(2, 20);
return (ulong) (tierFactor * mebibyte);
}
#endregion

@@ -151,7 +154,7 @@ namespace Discord.Rest
if (fromUserId.HasValue)
return GetBansAsync(guild, client, fromUserId.Value + 1, Direction.Before, around + 1, options)
.Concat(GetBansAsync(guild, client, fromUserId.Value, Direction.After, around, options));
else
else
return GetBansAsync(guild, client, null, Direction.Before, around + 1, options);
}

@@ -908,7 +911,7 @@ namespace Discord.Rest
if (endTime != null && endTime <= startTime)
throw new ArgumentOutOfRangeException(nameof(endTime), $"{nameof(endTime)} cannot be before the start time");

var apiArgs = new CreateGuildScheduledEventParams()
{
ChannelId = channelId ?? Optional<ulong>.Unspecified,


+ 0
- 1
src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs View File

@@ -1161,7 +1161,6 @@ namespace Discord.Rest
/// in order to use this property.
/// </remarks>
/// </param>
/// <param name="speakers">A collection of speakers for the event.</param>
/// <param name="location">The location of the event; links are supported</param>
/// <param name="coverImage">The optional banner image for the event.</param>
/// <param name="options">The options to be used when sending the request.</param>


+ 4
- 4
src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs View File

@@ -39,16 +39,16 @@ namespace Discord.Rest
{
}

internal new static async Task<RestCommandBase> CreateAsync(DiscordRestClient client, Model model)
internal new static async Task<RestCommandBase> CreateAsync(DiscordRestClient client, Model model, bool doApiCall)
{
var entity = new RestCommandBase(client, model);
await entity.UpdateAsync(client, model).ConfigureAwait(false);
await entity.UpdateAsync(client, model, doApiCall).ConfigureAwait(false);
return entity;
}

internal override async Task UpdateAsync(DiscordRestClient client, Model model)
internal override async Task UpdateAsync(DiscordRestClient client, Model model, bool doApiCall)
{
await base.UpdateAsync(client, model).ConfigureAwait(false);
await base.UpdateAsync(client, model, doApiCall).ConfigureAwait(false);
}

/// <summary>


+ 4
- 4
src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBaseData.cs View File

@@ -27,20 +27,20 @@ namespace Discord.Rest
{
}

internal static async Task<RestCommandBaseData> CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel)
internal static async Task<RestCommandBaseData> CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel, bool doApiCall)
{
var entity = new RestCommandBaseData(client, model);
await entity.UpdateAsync(client, model, guild, channel).ConfigureAwait(false);
await entity.UpdateAsync(client, model, guild, channel, doApiCall).ConfigureAwait(false);
return entity;
}

internal virtual async Task UpdateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel)
internal virtual async Task UpdateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel, bool doApiCall)
{
Name = model.Name;
if (model.Resolved.IsSpecified && ResolvableData == null)
{
ResolvableData = new RestResolvableData<Model>();
await ResolvableData.PopulateAsync(client, guild, channel, model).ConfigureAwait(false);
await ResolvableData.PopulateAsync(client, guild, channel, model, doApiCall).ConfigureAwait(false);
}
}



+ 20
- 6
src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestResolvableData.cs View File

@@ -22,7 +22,7 @@ namespace Discord.Rest
internal readonly Dictionary<ulong, Attachment> Attachments
= new Dictionary<ulong, Attachment>();

internal async Task PopulateAsync(DiscordRestClient discord, RestGuild guild, IRestMessageChannel channel, T model)
internal async Task PopulateAsync(DiscordRestClient discord, RestGuild guild, IRestMessageChannel channel, T model, bool doApiCall)
{
var resolved = model.Resolved.Value;

@@ -38,15 +38,26 @@ namespace Discord.Rest

if (resolved.Channels.IsSpecified)
{
var channels = await guild.GetChannelsAsync().ConfigureAwait(false);
var channels = doApiCall ? await guild.GetChannelsAsync().ConfigureAwait(false) : null;

foreach (var channelModel in resolved.Channels.Value)
{
var restChannel = channels.FirstOrDefault(x => x.Id == channelModel.Value.Id);
if (channels != null)
{
var guildChannel = channels.FirstOrDefault(x => x.Id == channelModel.Value.Id);

restChannel.Update(channelModel.Value);
guildChannel.Update(channelModel.Value);

Channels.Add(ulong.Parse(channelModel.Key), restChannel);
Channels.Add(ulong.Parse(channelModel.Key), guildChannel);
}
else
{
var restChannel = RestChannel.Create(discord, channelModel.Value);

restChannel.Update(channelModel.Value);

Channels.Add(ulong.Parse(channelModel.Key), restChannel);
}
}
}

@@ -76,7 +87,10 @@ namespace Discord.Rest
{
foreach (var msg in resolved.Messages.Value)
{
channel ??= (IRestMessageChannel)(Channels.FirstOrDefault(x => x.Key == msg.Value.ChannelId).Value ?? await discord.GetChannelAsync(msg.Value.ChannelId).ConfigureAwait(false));
channel ??= (IRestMessageChannel)(Channels.FirstOrDefault(x => x.Key == msg.Value.ChannelId).Value
?? (doApiCall
? await discord.GetChannelAsync(msg.Value.ChannelId).ConfigureAwait(false)
: null));

RestUser author;



+ 5
- 5
src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommand.cs View File

@@ -20,22 +20,22 @@ namespace Discord.Rest
}

internal new static async Task<RestMessageCommand> CreateAsync(DiscordRestClient client, Model model)
internal new static async Task<RestMessageCommand> CreateAsync(DiscordRestClient client, Model model, bool doApiCall)
{
var entity = new RestMessageCommand(client, model);
await entity.UpdateAsync(client, model).ConfigureAwait(false);
await entity.UpdateAsync(client, model, doApiCall).ConfigureAwait(false);
return entity;
}

internal override async Task UpdateAsync(DiscordRestClient client, Model model)
internal override async Task UpdateAsync(DiscordRestClient client, Model model, bool doApiCall)
{
await base.UpdateAsync(client, model).ConfigureAwait(false);
await base.UpdateAsync(client, model, doApiCall).ConfigureAwait(false);

var dataModel = model.Data.IsSpecified
? (DataModel)model.Data.Value
: null;
Data = await RestMessageCommandData.CreateAsync(client, dataModel, Guild, Channel).ConfigureAwait(false);
Data = await RestMessageCommandData.CreateAsync(client, dataModel, Guild, Channel, doApiCall).ConfigureAwait(false);
}

//IMessageCommandInteraction


+ 3
- 3
src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommandData.cs View File

@@ -23,15 +23,15 @@ namespace Discord.Rest
/// <b>Note</b> Not implemented for <see cref="RestMessageCommandData"/>
/// </remarks>
public override IReadOnlyCollection<IApplicationCommandInteractionDataOption> Options
=> throw new System.NotImplementedException();
=> throw new NotImplementedException();

internal RestMessageCommandData(DiscordRestClient client, Model model)
: base(client, model) { }

internal new static async Task<RestMessageCommandData> CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel)
internal new static async Task<RestMessageCommandData> CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel, bool doApiCall)
{
var entity = new RestMessageCommandData(client, model);
await entity.UpdateAsync(client, model, guild, channel).ConfigureAwait(false);
await entity.UpdateAsync(client, model, guild, channel, doApiCall).ConfigureAwait(false);
return entity;
}



+ 5
- 5
src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/UserCommands/RestUserCommand.cs View File

@@ -23,22 +23,22 @@ namespace Discord.Rest
{
}

internal new static async Task<RestUserCommand> CreateAsync(DiscordRestClient client, Model model)
internal new static async Task<RestUserCommand> CreateAsync(DiscordRestClient client, Model model, bool doApiCall)
{
var entity = new RestUserCommand(client, model);
await entity.UpdateAsync(client, model).ConfigureAwait(false);
await entity.UpdateAsync(client, model, doApiCall).ConfigureAwait(false);
return entity;
}

internal override async Task UpdateAsync(DiscordRestClient client, Model model)
internal override async Task UpdateAsync(DiscordRestClient client, Model model, bool doApiCall)
{
await base.UpdateAsync(client, model).ConfigureAwait(false);
await base.UpdateAsync(client, model, doApiCall).ConfigureAwait(false);

var dataModel = model.Data.IsSpecified
? (DataModel)model.Data.Value
: null;

Data = await RestUserCommandData.CreateAsync(client, dataModel, Guild, Channel).ConfigureAwait(false);
Data = await RestUserCommandData.CreateAsync(client, dataModel, Guild, Channel, doApiCall).ConfigureAwait(false);
}

//IUserCommandInteractionData


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save