Browse Source

Merge branch 'dev' into feature/forum-channels

pull/2316/head
Quin Lynch 3 years ago
parent
commit
2d6dc933d5
100 changed files with 1127 additions and 180 deletions
  1. +2
    -0
      .github/FUNDING.yml
  2. +2
    -2
      .github/ISSUE_TEMPLATE/bugreport.yml
  3. +23
    -0
      CHANGELOG.md
  4. +1
    -1
      Discord.Net.targets
  5. +1
    -2
      README.md
  6. +1
    -1
      docs/docfx.json
  7. +1
    -1
      docs/guides/getting_started/first-bot.md
  8. +3
    -1
      docs/guides/int_basics/application-commands/slash-commands/parameters.md
  9. +2
    -0
      docs/guides/int_framework/autocompletion.md
  10. +11
    -0
      docs/guides/int_framework/intro.md
  11. +59
    -0
      docs/guides/int_framework/permissions.md
  12. +20
    -0
      docs/guides/int_framework/samples/autocompletion/autocomplete-example.cs
  13. +15
    -3
      docs/guides/int_framework/samples/intro/autocomplete.cs
  14. +21
    -0
      docs/guides/int_framework/samples/intro/groupmodule.cs
  15. +6
    -0
      docs/guides/int_framework/samples/permissions/guild-only.cs
  16. +7
    -0
      docs/guides/int_framework/samples/permissions/guild-perms.cs
  17. +16
    -0
      docs/guides/int_framework/samples/permissions/perm-nesting.cs
  18. +4
    -0
      docs/guides/int_framework/samples/permissions/perm-stacking.cs
  19. +2
    -2
      docs/guides/other_libs/samples/ModifyLogMethod.cs
  20. +2
    -0
      docs/guides/toc.yml
  21. +3
    -3
      samples/BasicBot/_BasicBot.csproj
  22. +1
    -1
      samples/InteractionFramework/Modules/ExampleModule.cs
  23. +2
    -8
      samples/InteractionFramework/_InteractionFramework.csproj
  24. +0
    -16
      samples/MediatRSample/MediatRSample.sln
  25. +2
    -7
      samples/ShardedClient/_ShardedClient.csproj
  26. +3
    -6
      samples/TextCommandFramework/_TextCommandFramework.csproj
  27. +2
    -2
      samples/WebhookClient/_WebhookClient.csproj
  28. +2
    -0
      src/Discord.Net.Commands/Discord.Net.Commands.csproj
  29. +3
    -3
      src/Discord.Net.Commands/Results/MatchResult.cs
  30. +2
    -0
      src/Discord.Net.Core/Discord.Net.Core.csproj
  31. +5
    -0
      src/Discord.Net.Core/DiscordErrorCode.cs
  32. +11
    -0
      src/Discord.Net.Core/Entities/Channels/ITextChannel.cs
  33. +1
    -1
      src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs
  34. +0
    -1
      src/Discord.Net.Core/Entities/Guilds/IGuild.cs
  35. +1
    -1
      src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs
  36. +1
    -1
      src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs
  37. +10
    -0
      src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs
  38. +35
    -1
      src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs
  39. +35
    -1
      src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs
  40. +13
    -0
      src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs
  41. +1
    -1
      src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs
  42. +102
    -8
      src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs
  43. +4
    -4
      src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs
  44. +42
    -19
      src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs
  45. +50
    -0
      src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs
  46. +13
    -2
      src/Discord.Net.Core/Entities/Messages/MessageReference.cs
  47. +1
    -1
      src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs
  48. +3
    -3
      src/Discord.Net.Core/Entities/Users/IGuildUser.cs
  49. +3
    -2
      src/Discord.Net.Core/Format.cs
  50. +24
    -0
      src/Discord.Net.Core/Interactions/IRouteMatchContainer.cs
  51. +16
    -0
      src/Discord.Net.Core/Interactions/IRouteSegmentMatch.cs
  52. +16
    -0
      src/Discord.Net.Core/Interactions/RouteSegmentMatch.cs
  53. +17
    -0
      src/Discord.Net.Core/Utils/Preconditions.cs
  54. +1
    -1
      src/Discord.Net.Core/Utils/UrlValidation.cs
  55. +1
    -1
      src/Discord.Net.Examples/Discord.Net.Examples.csproj
  56. +3
    -3
      src/Discord.Net.Interactions/Attributes/AutocompleteAttribute.cs
  57. +25
    -0
      src/Discord.Net.Interactions/Attributes/DefaultMemberPermissionAttribute.cs
  58. +1
    -0
      src/Discord.Net.Interactions/Attributes/DefaultPermissionAttribute.cs
  59. +25
    -0
      src/Discord.Net.Interactions/Attributes/EnabledInDmAttribute.cs
  60. +0
    -2
      src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs
  61. +1
    -1
      src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs
  62. +2
    -2
      src/Discord.Net.Interactions/Attributes/Preconditions/RequireUserPermissionAttribute.cs
  63. +38
    -0
      src/Discord.Net.Interactions/Builders/Commands/ContextCommandBuilder.cs
  64. +39
    -1
      src/Discord.Net.Interactions/Builders/Commands/SlashCommandBuilder.cs
  65. +1
    -1
      src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs
  66. +1
    -1
      src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs
  67. +40
    -1
      src/Discord.Net.Interactions/Builders/ModuleBuilder.cs
  68. +30
    -0
      src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs
  69. +1
    -1
      src/Discord.Net.Interactions/Builders/Parameters/ParameterBuilder.cs
  70. +3
    -1
      src/Discord.Net.Interactions/Discord.Net.Interactions.csproj
  71. +8
    -0
      src/Discord.Net.Interactions/Info/Commands/ContextCommands/ContextCommandInfo.cs
  72. +8
    -0
      src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs
  73. +13
    -0
      src/Discord.Net.Interactions/Info/IApplicationCommandInfo.cs
  74. +28
    -0
      src/Discord.Net.Interactions/Info/ModuleInfo.cs
  75. +14
    -3
      src/Discord.Net.Interactions/InteractionContext.cs
  76. +6
    -6
      src/Discord.Net.Interactions/InteractionModuleBase.cs
  77. +34
    -8
      src/Discord.Net.Interactions/InteractionService.cs
  78. +2
    -2
      src/Discord.Net.Interactions/RestInteractionModuleBase.cs
  79. +1
    -1
      src/Discord.Net.Interactions/Results/TypeConverterResult.cs
  80. +23
    -0
      src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/NullableComponentConverter.cs
  81. +23
    -0
      src/Discord.Net.Interactions/TypeReaders/NullableReader.cs
  82. +21
    -3
      src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs
  83. +7
    -0
      src/Discord.Net.Rest/API/Common/ApplicationCommand.cs
  84. +2
    -1
      src/Discord.Net.Rest/API/Common/Channel.cs
  85. +3
    -0
      src/Discord.Net.Rest/API/Common/MessageReference.cs
  86. +6
    -0
      src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs
  87. +1
    -0
      src/Discord.Net.Rest/AssemblyInfo.cs
  88. +2
    -0
      src/Discord.Net.Rest/Discord.Net.Rest.csproj
  89. +38
    -18
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  90. +15
    -7
      src/Discord.Net.Rest/DiscordRestClient.cs
  91. +2
    -0
      src/Discord.Net.Rest/DiscordRestConfig.cs
  92. +4
    -1
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs
  93. +4
    -1
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs
  94. +4
    -1
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs
  95. +4
    -1
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs
  96. +5
    -2
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/KickAuditLogData.cs
  97. +1
    -1
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs
  98. +4
    -1
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs
  99. +5
    -1
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs
  100. +4
    -1
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.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


+ 23
- 0
CHANGELOG.md View File

@@ -1,5 +1,28 @@
# Changelog

## [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
- #2136 Passing CustomId matches into contexts (4ce1801)
- #2222 V2 Permissions (d98b3cc)

### Fixed
- #2260 Guarding against empty descriptions in `SlashCommandBuilder`/`SlashCommandOptionBuilder` (0554ac2)
- #2248 Fix SocketGuild not returning the AudioClient (daba58c)
- #2254 Fix browser property (275b833)

## [3.5.0] - 2022-04-05

### Added


+ 1
- 1
Discord.Net.targets View File

@@ -1,6 +1,6 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VersionPrefix>3.5.0</VersionPrefix>
<VersionPrefix>3.6.1</VersionPrefix>
<LangVersion>latest</LangVersion>
<Authors>Discord.Net Contributors</Authors>
<PackageTags>discord;discordapp</PackageTags>


+ 1
- 2
README.md View File

@@ -1,4 +1,3 @@
# Discord.Net
<p align="center">
<a href="https://discordnet.dev/" title="Click to visit the documentation!">
<img src="https://raw.githubusercontent.com/discord-net/Discord.Net/dev/docs/marketing/logo/SVG/Combinationmark%20White%20Border.svg" alt="Logo">
@@ -18,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.5.0",
"_appFooter": "Discord.Net (c) 2015-2022 3.6.1",
"_enableSearch": true,
"_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg",
"_appFaviconPath": "favicon.ico"


+ 1
- 1
docs/guides/getting_started/first-bot.md View File

@@ -202,7 +202,7 @@ online in Discord.

To create commands for your bot, you may choose from a variety of
command processors available. Throughout the guides, we will be using
the one that Discord.Net ships with. @Guides.Commands.Intro will
the one that Discord.Net ships with. @Guides.TextCommands.Intro will
guide you through how to setup a program that is ready for
[CommandService].



+ 3
- 1
docs/guides/int_basics/application-commands/slash-commands/parameters.md View File

@@ -15,9 +15,10 @@ Slash commands can have a bunch of parameters, each their own type. Let's first
| Integer | A number. |
| Boolean | True or False. |
| User | A user |
| Channel | A channel, this includes voice text and categories |
| Role | A role. |
| Channel | A channel, this includes voice text and categories |
| Mentionable | A role or a user. |
| File | A file |

Each one of the parameter types has its own DNET type in the `SocketSlashCommandDataOption`'s Value field:
| Name | C# Type |
@@ -31,6 +32,7 @@ Each one of the parameter types has its own DNET type in the `SocketSlashCommand
| Role | `SocketRole` |
| Channel | `SocketChannel` |
| Mentionable | `SocketUser`, `SocketGuildUser`, or `SocketRole` |
| File | `IAttachment` |

Let's start by making a command that takes in a user and lists their roles.



+ 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.


+ 11
- 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`
@@ -158,6 +159,14 @@ Interaction service complex parameter constructors are prioritized in the follow
2. Constuctor tagged with `[ComplexParameterCtor]`.
3. Type's only public constuctor.

#### DM Permissions

You can use the [EnabledInDmAttribute] to configure whether a globally-scoped top level command should be enabled in Dms or not. Only works on top level commands.

#### Default Member Permissions

[DefaultMemberPermissionsAttribute] can be used when creating a command to set the permissions a user must have to use the command. Permission overwrites can be configured from the Integrations page of Guild Settings. [DefaultMemberPermissionsAttribute] cumulatively propagates down the class hierarchy until it reaches a top level command. This attribute can be only used on top level commands and will not work on commands that are nested in command groups.

## User Commands

A valid User Command must have the following structure:
@@ -282,6 +291,8 @@ By nesting commands inside a module that is tagged with [GroupAttribute] you can
> Although creating nested module stuctures are allowed,
> you are not permitted to use more than 2 [GroupAttribute]'s in module hierarchy.

[!code-csharp[Command Group Example](samples/intro/groupmodule.cs)]

## Executing Commands

Any of the following socket events can be used to execute commands:


+ 59
- 0
docs/guides/int_framework/permissions.md View File

@@ -0,0 +1,59 @@
---
uid: Guides.IntFw.Perms
title: How to handle permissions.
---

# Permissions

This page covers everything to know about setting up permissions for Slash & context commands.

Application command (Slash, User & Message) permissions are set up at creation.
When you add your commands to a guild or globally, the permissions will be set up from the attributes you defined.

Commands that are added will only show up for members that meet the required permissions.
There is no further internal handling, as Discord deals with this on its own.

> [!WARNING]
> Permissions can only be configured at top level commands. Not in subcommands.

## Disallowing commands in DM

Commands can be blocked from being executed in DM if a guild is required to execute them in as followed:

[!code-csharp[no-DM permission](samples/permissions/guild-only.cs)]

> [!TIP]
> This attribute only works on global-level commands. Commands that are registered in guilds alone do not have a need for it.

## Server permissions

As previously shown, a command like ban can be blocked from being executed inside DMs,
as there are no members to ban inside of a DM. However, for a command like this,
we'll also want to make block it from being used by members that do not have the [permissions].
To do this, we can use the `DefaultMemberPermissions` attribute:

[!code-csharp[Server permissions](samples/permissions/guild-perms.cs)]

### Stacking permissions

If you want a user to have multiple [permissions] in order to execute a command, you can use the `|` operator, just like with setting up intents:

[!code-csharp[Permission stacking](samples/permissions/perm-stacking.cs)]

### Nesting permissions

Alternatively, permissions can also be nested.
It will look for all uses of `DefaultMemberPermissions` up until the highest level class.
The `EnabledInDm` attribute can be defined at top level as well,
and will be set up for all of the commands & nested modules inside this class.

[!code-csharp[Permission stacking](samples/permissions/perm-nesting.cs)]

The amount of nesting you can do is realistically endless.

> [!NOTE]
> If the nested class is marked with `Group`, as required for setting up subcommands, this example will not work.
> As mentioned before, subcommands cannot have seperate permissions from the top level command.

[permissions]: xref:Discord.GuildPermission


+ 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}");

+ 21
- 0
docs/guides/int_framework/samples/intro/groupmodule.cs View File

@@ -0,0 +1,21 @@
// You can put commands in groups
[Group("group-name", "Group description")]
public class CommandGroupModule : InteractionModuleBase<SocketInteractionContext>
{
// This command will look like
// group-name ping
[SlashCommand("ping", "Get a pong")]
public async Task PongSubcommand()
=> await RespondAsync("Pong!");
// And even in sub-command groups
[Group("subcommand-group-name", "Subcommand group description")]
public class SubСommandGroupModule : InteractionModuleBase<SocketInteractionContext>
{
// This command will look like
// group-name subcommand-group-name echo
[SlashCommand("echo", "Echo an input")]
public async Task EchoSubcommand(string input)
=> await RespondAsync(input);
}
}

+ 6
- 0
docs/guides/int_framework/samples/permissions/guild-only.cs View File

@@ -0,0 +1,6 @@
[EnabledInDm(false)]
[SlashCommand("ban", "Bans a user in this guild")]
public async Task BanAsync(...)
{
...
}

+ 7
- 0
docs/guides/int_framework/samples/permissions/guild-perms.cs View File

@@ -0,0 +1,7 @@
[EnabledInDm(false)]
[DefaultMemberPermissions(GuildPermission.BanMembers)]
[SlashCommand("ban", "Bans a user in this guild")]
public async Task BanAsync(...)
{
...
}

+ 16
- 0
docs/guides/int_framework/samples/permissions/perm-nesting.cs View File

@@ -0,0 +1,16 @@
[EnabledInDm(true)]
[DefaultMemberPermissions(GuildPermission.ViewChannels)]
public class Module : InteractionModuleBase<SocketInteractionContext>
{
[DefaultMemberPermissions(GuildPermission.SendMessages)]
public class NestedModule : InteractionModuleBase<SocketInteractionContext>
{
// While looking for more permissions, it has found 'ViewChannels' and 'SendMessages'. The result of this lookup will be:
// ViewChannels + SendMessages + ManageMessages.
// If these together are not found for target user, the command will not show up for them.
[DefaultMemberPermissions(GuildPermission.ManageMessages)]
[SlashCommand("ping", "Pong!")]
public async Task Ping()
=> await RespondAsync("pong");
}
}

+ 4
- 0
docs/guides/int_framework/samples/permissions/perm-stacking.cs View File

@@ -0,0 +1,4 @@
[DefaultMemberPermissions(GuildPermission.SendMessages | GuildPermission.ViewChannels)]
[SlashCommand("ping", "Pong!")]
public async Task Ping()
=> await RespondAsync("pong");

+ 2
- 2
docs/guides/other_libs/samples/ModifyLogMethod.cs View File

@@ -6,8 +6,8 @@ private static async Task LogAsync(LogMessage message)
LogSeverity.Error => LogEventLevel.Error,
LogSeverity.Warning => LogEventLevel.Warning,
LogSeverity.Info => LogEventLevel.Information,
LogSeverity.Verbose => LogEventLevel.Debug,
LogSeverity.Debug => LogEventLevel.Verbose,
LogSeverity.Verbose => LogEventLevel.Verbose,
LogSeverity.Debug => LogEventLevel.Debug,
_ => LogEventLevel.Information
};
Log.Write(severity, message.Exception, "[{Source}] {Message}", message.Source, message.Message);


+ 2
- 0
docs/guides/toc.yml View File

@@ -57,6 +57,8 @@
topicUid: Guides.IntFw.DI
- name: Post-execution Handling
topicUid: Guides.IntFw.PostExecution
- name: Permissions
topicUid: Guides.IntFw.Perms
- name: Slash Command Basics
items:
- name: Introduction


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

+ 1
- 1
samples/InteractionFramework/Modules/ExampleModule.cs View File

@@ -14,7 +14,7 @@ namespace InteractionFramework.Modules

private InteractionHandler _handler;

// Constructor injection is also a valid way to access the dependecies
// Constructor injection is also a valid way to access the dependencies
public ExampleModule(InteractionHandler handler)
{
_handler = handler;


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

+ 0
- 16
samples/MediatRSample/MediatRSample.sln View File

@@ -1,16 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediatRSample", "MediatRSample\MediatRSample.csproj", "{CE066EE5-7ED1-42A0-8DB2-862D44F40EA7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{CE066EE5-7ED1-42A0-8DB2-862D44F40EA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CE066EE5-7ED1-42A0-8DB2-862D44F40EA7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CE066EE5-7ED1-42A0-8DB2-862D44F40EA7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CE066EE5-7ED1-42A0-8DB2-862D44F40EA7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

+ 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" />


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

@@ -58,6 +58,7 @@ namespace Discord
#endregion

#region General Actions (20XXX)
UnknownTag = 10087,
BotsCannotUse = 20001,
OnlyBotsCanUse = 20002,
CannotSendExplicitContent = 20009,
@@ -98,6 +99,8 @@ namespace Discord

#region General Request Errors (40XXX)
MaximumNumberOfEditsReached = 30046,
MaximumNumberOfPinnedThreadsInAForumChannelReached = 30047,
MaximumNumberOfTagsInAForumChannelReached = 30048,
TokenUnauthorized = 40001,
InvalidVerification = 40002,
OpeningDMTooFast = 40003,
@@ -112,6 +115,7 @@ namespace Discord

#region Action Preconditions/Checks (50XXX)
InteractionHasAlreadyBeenAcknowledged = 40060,
TagNamesMustBeUnique = 40061,
MissingPermissions = 50001,
InvalidAccountType = 50002,
CannotExecuteForDM = 50003,
@@ -148,6 +152,7 @@ namespace Discord
InvalidMessageType = 50068,
PaymentSourceRequiredForGift = 50070,
CannotDeleteRequiredCommunityChannel = 50074,
CannotEditStickersWithinAMessage = 50080,
InvalidSticker = 50081,
CannotExecuteOnArchivedThread = 50083,
InvalidThreadNotificationSettings = 50084,


+ 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.


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


+ 10
- 0
src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs View File

@@ -17,6 +17,16 @@ namespace Discord
/// </summary>
public Optional<bool> IsDefaultPermission { get; set; }

/// <summary>
/// Gets or sets whether or not this command can be used in DMs.
/// </summary>
public Optional<bool> IsDMEnabled { get; set; }

/// <summary>
/// Gets or sets the default permissions required by a user to execute this application command.
/// </summary>
public Optional<GuildPermission> DefaultMemberPermissions { get; set; }

internal ApplicationCommandProperties() { }
}
}

+ 35
- 1
src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs View File

@@ -31,6 +31,16 @@ namespace Discord
/// </summary>
public bool IsDefaultPermission { get; set; } = true;

/// <summary>
/// Gets or sets whether or not this command can be used in DMs.
/// </summary>
public bool IsDMEnabled { get; set; } = true;

/// <summary>
/// Gets or sets the default permission required to use this slash command.
/// </summary>
public GuildPermission? DefaultMemberPermissions { get; set; }

private string _name;

/// <summary>
@@ -44,7 +54,9 @@ namespace Discord
var props = new MessageCommandProperties
{
Name = Name,
IsDefaultPermission = IsDefaultPermission
IsDefaultPermission = IsDefaultPermission,
IsDMEnabled = IsDMEnabled,
DefaultMemberPermissions = DefaultMemberPermissions ?? Optional<GuildPermission>.Unspecified
};

return props;
@@ -73,5 +85,27 @@ namespace Discord
IsDefaultPermission = isDefaultPermission;
return this;
}

/// <summary>
/// Sets whether or not this command can be used in dms
/// </summary>
/// <param name="permission"><see langword="true"/> if the command is available in dms, otherwise <see langword="false"/>.</param>
/// <returns>The current builder.</returns>
public MessageCommandBuilder WithDMPermission(bool permission)
{
IsDMEnabled = permission;
return this;
}

/// <summary>
/// Sets the default member permissions required to use this application command.
/// </summary>
/// <param name="permissions">The permissions required to use this command.</param>
/// <returns>The current builder.</returns>
public MessageCommandBuilder WithDefaultMemberPermissions(GuildPermission? permissions)
{
DefaultMemberPermissions = permissions;
return this;
}
}
}

+ 35
- 1
src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs View File

@@ -31,6 +31,16 @@ namespace Discord
/// </summary>
public bool IsDefaultPermission { get; set; } = true;

/// <summary>
/// Gets or sets whether or not this command can be used in DMs.
/// </summary>
public bool IsDMEnabled { get; set; } = true;

/// <summary>
/// Gets or sets the default permission required to use this slash command.
/// </summary>
public GuildPermission? DefaultMemberPermissions { get; set; }

private string _name;

/// <summary>
@@ -42,7 +52,9 @@ namespace Discord
var props = new UserCommandProperties
{
Name = Name,
IsDefaultPermission = IsDefaultPermission
IsDefaultPermission = IsDefaultPermission,
IsDMEnabled = IsDMEnabled,
DefaultMemberPermissions = DefaultMemberPermissions ?? Optional<GuildPermission>.Unspecified
};

return props;
@@ -71,5 +83,27 @@ namespace Discord
IsDefaultPermission = isDefaultPermission;
return this;
}

/// <summary>
/// Sets whether or not this command can be used in dms
/// </summary>
/// <param name="permission"><see langword="true"/> if the command is available in dms, otherwise <see langword="false"/>.</param>
/// <returns>The current builder.</returns>
public UserCommandBuilder WithDMPermission(bool permission)
{
IsDMEnabled = permission;
return this;
}

/// <summary>
/// Sets the default member permissions required to use this application command.
/// </summary>
/// <param name="permissions">The permissions required to use this command.</param>
/// <returns>The current builder.</returns>
public UserCommandBuilder WithDefaultMemberPermissions(GuildPermission? permissions)
{
DefaultMemberPermissions = permissions;
return this;
}
}
}

+ 13
- 0
src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs View File

@@ -34,6 +34,19 @@ namespace Discord
/// </summary>
bool IsDefaultPermission { get; }

/// <summary>
/// Indicates whether the command is available in DMs with the app.
/// </summary>
/// <remarks>
/// Only for globally-scoped commands.
/// </remarks>
bool IsEnabledInDm { get; }

/// <summary>
/// Set of default <see cref="GuildPermission"/> required to invoke the command.
/// </summary>
GuildPermissions DefaultMemberPermissions { get; }

/// <summary>
/// Gets a collection of options for this application command.
/// </summary>


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

@@ -55,7 +55,7 @@ namespace Discord
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.


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


+ 42
- 19
src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs View File

@@ -81,6 +81,16 @@ namespace Discord
/// </summary>
public bool IsDefaultPermission { get; set; } = true;

/// <summary>
/// Gets or sets whether or not this command can be used in DMs.
/// </summary>
public bool IsDMEnabled { get; set; } = true;

/// <summary>
/// Gets or sets the default permission required to use this slash command.
/// </summary>
public GuildPermission? DefaultMemberPermissions { get; set; }

private string _name;
private string _description;
private List<SlashCommandOptionBuilder> _options;
@@ -96,6 +106,8 @@ namespace Discord
Name = Name,
Description = Description,
IsDefaultPermission = IsDefaultPermission,
IsDMEnabled = IsDMEnabled,
DefaultMemberPermissions = DefaultMemberPermissions ?? Optional<GuildPermission>.Unspecified
};

if (Options != null && Options.Any())
@@ -145,6 +157,28 @@ namespace Discord
return this;
}

/// <summary>
/// Sets whether or not this command can be used in dms
/// </summary>
/// <param name="permission"><see langword="true"/> if the command is available in dms, otherwise <see langword="false"/>.</param>
/// <returns>The current builder.</returns>
public SlashCommandBuilder WithDMPermission(bool permission)
{
IsDMEnabled = permission;
return this;
}

/// <summary>
/// Sets the default member permissions required to use this application command.
/// </summary>
/// <param name="permissions">The permissions required to use this command.</param>
/// <returns>The current builder.</returns>
public SlashCommandBuilder WithDefaultMemberPermissions(GuildPermission? permissions)
{
DefaultMemberPermissions = permissions;
return this;
}

/// <summary>
/// Adds an option to the current slash command.
/// </summary>
@@ -164,21 +198,13 @@ namespace Discord
string description, bool? isRequired = null, bool? isDefault = null, bool isAutocomplete = false, double? minValue = null, double? maxValue = null,
List<SlashCommandOptionBuilder> options = null, List<ChannelType> channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices)
{
// Make sure the name matches the requirements from discord
Preconditions.NotNullOrEmpty(name, nameof(name));
Preconditions.AtLeast(name.Length, 1, nameof(name));
Preconditions.AtMost(name.Length, MaxNameLength, nameof(name));
Preconditions.Options(name, description);

// Discord updated the docs, this regex prevents special characters like @!$%( and s p a c e s.. etc,
// https://discord.com/developers/docs/interactions/slash-commands#applicationcommand
if (!Regex.IsMatch(name, @"^[\w-]{1,32}$"))
throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(name));

// same with description
Preconditions.NotNullOrEmpty(description, nameof(description));
Preconditions.AtLeast(description.Length, 1, nameof(description));
Preconditions.AtMost(description.Length, MaxDescriptionLength, nameof(description));

// make sure theres only one option with default set to true
if (isDefault == true && Options?.Any(x => x.IsDefault == true) == true)
throw new ArgumentException("There can only be one command option with default set to true!", nameof(isDefault));
@@ -214,6 +240,7 @@ namespace Discord
throw new InvalidOperationException($"Cannot have more than {MaxOptionsCount} options!");

Preconditions.NotNull(option, nameof(option));
Preconditions.Options(option.Name, option.Description); // this is a double-check when this method is called via AddOption(string name... )

Options.Add(option);
return this;
@@ -236,6 +263,9 @@ namespace Discord
if (Options.Count + options.Length > MaxOptionsCount)
throw new ArgumentOutOfRangeException(nameof(options), $"Cannot have more than {MaxOptionsCount} options!");

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

Options.AddRange(options);
return this;
}
@@ -379,7 +409,7 @@ namespace Discord
MinValue = MinValue,
MaxValue = MaxValue
};
}
}

/// <summary>
/// Adds an option to the current slash command.
@@ -400,21 +430,13 @@ namespace Discord
string description, bool? isRequired = null, bool isDefault = false, bool isAutocomplete = false, double? minValue = null, double? maxValue = null,
List<SlashCommandOptionBuilder> options = null, List<ChannelType> channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices)
{
// Make sure the name matches the requirements from discord
Preconditions.NotNullOrEmpty(name, nameof(name));
Preconditions.AtLeast(name.Length, 1, nameof(name));
Preconditions.AtMost(name.Length, SlashCommandBuilder.MaxNameLength, nameof(name));
Preconditions.Options(name, description);

// Discord updated the docs, this regex prevents special characters like @!$%( and s p a c e s.. etc,
// https://discord.com/developers/docs/interactions/slash-commands#applicationcommand
if (!Regex.IsMatch(name, @"^[\w-]{1,32}$"))
throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(name));

// same with description
Preconditions.NotNullOrEmpty(description, nameof(description));
Preconditions.AtLeast(description.Length, 1, nameof(description));
Preconditions.AtMost(description.Length, SlashCommandBuilder.MaxDescriptionLength, nameof(description));

// make sure theres only one option with default set to true
if (isDefault && Options?.Any(x => x.IsDefault == true) == true)
throw new ArgumentException("There can only be one command option with default set to true!", nameof(isDefault));
@@ -449,6 +471,7 @@ namespace Discord
throw new InvalidOperationException($"There can only be {SlashCommandBuilder.MaxOptionsCount} options per sub command group!");

Preconditions.NotNull(option, nameof(option));
Preconditions.Options(option.Name, option.Description); // double check again

Options.Add(option);
return this;


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


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



+ 24
- 0
src/Discord.Net.Core/Interactions/IRouteMatchContainer.cs View File

@@ -0,0 +1,24 @@
using System.Collections.Generic;

namespace Discord
{
/// <summary>
/// Represents a container for temporarily storing CustomId wild card matches of a component.
/// </summary>
public interface IRouteMatchContainer
{
/// <summary>
/// Gets the collection of captured route segments in this container.
/// </summary>
/// <returns>
/// A collection of captured route segments.
///</returns>
IEnumerable<IRouteSegmentMatch> SegmentMatches { get; }

/// <summary>
/// Sets the <see cref="SegmentMatches"/> property of this container.
/// </summary>
/// <param name="segmentMatches">The collection of captured route segments.</param>
void SetSegmentMatches(IEnumerable<IRouteSegmentMatch> segmentMatches);
}
}

+ 16
- 0
src/Discord.Net.Core/Interactions/IRouteSegmentMatch.cs View File

@@ -0,0 +1,16 @@
namespace Discord
{
/// <summary>
/// Represents an object for storing a CustomId wild card match.
/// </summary>
public interface IRouteSegmentMatch
{
/// <summary>
/// Gets the captured value of this wild card match.
/// </summary>
/// <returns>
/// The value of this wild card.
/// </returns>
string Value { get; }
}
}

+ 16
- 0
src/Discord.Net.Core/Interactions/RouteSegmentMatch.cs View File

@@ -0,0 +1,16 @@
namespace Discord
{
/// <summary>
/// Represents an object for storing a CustomId wild card match.
/// </summary>
internal record RouteSegmentMatch : IRouteSegmentMatch
{
/// <inheritdoc/>
public string Value { get; }

public RouteSegmentMatch(string value)
{
Value = value;
}
}
}

+ 17
- 0
src/Discord.Net.Core/Utils/Preconditions.cs View File

@@ -297,5 +297,22 @@ namespace Discord
}
}
#endregion

#region SlashCommandOptions

/// <exception cref="ArgumentNullException"><paramref name="description"/> or <paramref name="name"/> is null.</exception>
/// <exception cref="ArgumentException"><paramref name="description"/> or <paramref name="name"/> are either empty or their length exceed limits.</exception>
public static void Options(string name, string description)
{
// Make sure the name matches the requirements from discord
NotNullOrEmpty(name, nameof(name));
NotNullOrEmpty(description, nameof(description));
AtLeast(name.Length, 1, nameof(name));
AtMost(name.Length, SlashCommandBuilder.MaxNameLength, nameof(name));
AtLeast(description.Length, 1, nameof(description));
AtMost(description.Length, SlashCommandBuilder.MaxDescriptionLength, nameof(description));
}

#endregion
}
}

+ 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() { }
}


+ 25
- 0
src/Discord.Net.Interactions/Attributes/DefaultMemberPermissionAttribute.cs View File

@@ -0,0 +1,25 @@
using System;

namespace Discord.Interactions
{
/// <summary>
/// Sets the <see cref="IApplicationCommandInfo.DefaultMemberPermissions"/> of an application command or module.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class DefaultMemberPermissionsAttribute : Attribute
{
/// <summary>
/// Gets the default permission required to use this command.
/// </summary>
public GuildPermission Permissions { get; }

/// <summary>
/// Sets the <see cref="IApplicationCommandInfo.DefaultMemberPermissions"/> of an application command or module.
/// </summary>
/// <param name="permissions">The default permission required to use this command.</param>
public DefaultMemberPermissionsAttribute(GuildPermission permissions)
{
Permissions = permissions;
}
}
}

+ 1
- 0
src/Discord.Net.Interactions/Attributes/DefaultPermissionAttribute.cs View File

@@ -6,6 +6,7 @@ namespace Discord.Interactions
/// Set the "Default Permission" property of an Application Command.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
[Obsolete($"Soon to be deprecated, use Permissions-v2 attributes like {nameof(EnabledInDmAttribute)} and {nameof(DefaultMemberPermissionsAttribute)}")]
public class DefaultPermissionAttribute : Attribute
{
/// <summary>


+ 25
- 0
src/Discord.Net.Interactions/Attributes/EnabledInDmAttribute.cs View File

@@ -0,0 +1,25 @@
using System;

namespace Discord.Interactions
{
/// <summary>
/// Sets the <see cref="IApplicationCommandInfo.IsEnabledInDm"/> property of an application command or module.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class EnabledInDmAttribute : Attribute
{
/// <summary>
/// Gets whether or not this command can be used in DMs.
/// </summary>
public bool IsEnabled { get; }

/// <summary>
/// Sets the <see cref="IApplicationCommandInfo.IsEnabledInDm"/> property of an application command or module.
/// </summary>
/// <param name="isEnabled">Whether or not this command can be used in DMs.</param>
public EnabledInDmAttribute(bool isEnabled)
{
IsEnabled = isEnabled;
}
}
}

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


+ 38
- 0
src/Discord.Net.Interactions/Builders/Commands/ContextCommandBuilder.cs View File

@@ -17,8 +17,19 @@ namespace Discord.Interactions.Builders
/// <summary>
/// Gets the default permission of this command.
/// </summary>
[Obsolete($"To be deprecated soon, use {nameof(IsEnabledInDm)} and {nameof(DefaultMemberPermissions)} instead.")]
public bool DefaultPermission { get; set; } = true;

/// <summary>
/// Gets whether this command can be used in DMs.
/// </summary>
public bool IsEnabledInDm { get; set; } = true;

/// <summary>
/// Gets the default permissions needed for executing this command.
/// </summary>
public GuildPermission? DefaultMemberPermissions { get; set; } = null;

internal ContextCommandBuilder (ModuleBuilder module) : base(module) { }

/// <summary>
@@ -49,6 +60,7 @@ namespace Discord.Interactions.Builders
/// <returns>
/// The builder instance.
/// </returns>
[Obsolete($"To be deprecated soon, use {nameof(SetEnabledInDm)} and {nameof(WithDefaultMemberPermissions)} instead.")]
public ContextCommandBuilder SetDefaultPermission (bool defaultPermision)
{
DefaultPermission = defaultPermision;
@@ -70,6 +82,32 @@ namespace Discord.Interactions.Builders
return this;
}

/// <summary>
/// Sets <see cref="IsEnabledInDm"/>.
/// </summary>
/// <param name="isEnabled">New value of the <see cref="IsEnabledInDm"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public ContextCommandBuilder SetEnabledInDm(bool isEnabled)
{
IsEnabledInDm = isEnabled;
return this;
}

/// <summary>
/// Sets <see cref="DefaultMemberPermissions"/>.
/// </summary>
/// <param name="permissions">New value of the <see cref="DefaultMemberPermissions"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public ContextCommandBuilder WithDefaultMemberPermissions(GuildPermission permissions)
{
DefaultMemberPermissions = permissions;
return this;
}

internal override ContextCommandInfo Build (ModuleInfo module, InteractionService commandService) =>
ContextCommandInfo.Create(this, module, commandService);
}


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

@@ -17,8 +17,19 @@ namespace Discord.Interactions.Builders
/// <summary>
/// Gets and sets the default permission of this command.
/// </summary>
[Obsolete($"To be deprecated soon, use {nameof(IsEnabledInDm)} and {nameof(DefaultMemberPermissions)} instead.")]
public bool DefaultPermission { get; set; } = true;

/// <summary>
/// Gets whether this command can be used in DMs.
/// </summary>
public bool IsEnabledInDm { get; set; } = true;

/// <summary>
/// Gets the default permissions needed for executing this command.
/// </summary>
public GuildPermission? DefaultMemberPermissions { get; set; } = null;

internal SlashCommandBuilder (ModuleBuilder module) : base(module) { }

/// <summary>
@@ -45,10 +56,11 @@ 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>
[Obsolete($"To be deprecated soon, use {nameof(SetEnabledInDm)} and {nameof(WithDefaultMemberPermissions)} instead.")]
public SlashCommandBuilder WithDefaultPermission (bool permission)
{
DefaultPermission = permission;
@@ -70,6 +82,32 @@ namespace Discord.Interactions.Builders
return this;
}

/// <summary>
/// Sets <see cref="IsEnabledInDm"/>.
/// </summary>
/// <param name="isEnabled">New value of the <see cref="IsEnabledInDm"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public SlashCommandBuilder SetEnabledInDm(bool isEnabled)
{
IsEnabledInDm = isEnabled;
return this;
}

/// <summary>
/// Sets <see cref="DefaultMemberPermissions"/>.
/// </summary>
/// <param name="permissions">New value of the <see cref="DefaultMemberPermissions"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public SlashCommandBuilder WithDefaultMemberPermissions(GuildPermission permissions)
{
DefaultMemberPermissions = permissions;
return this;
}

internal override SlashCommandInfo Build (ModuleInfo module, InteractionService commandService) =>
new SlashCommandInfo(this, module, commandService);
}


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


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

@@ -51,8 +51,19 @@ namespace Discord.Interactions.Builders
/// <summary>
/// Gets and sets the default permission of this module.
/// </summary>
[Obsolete($"To be deprecated soon, use {nameof(IsEnabledInDm)} and {nameof(DefaultMemberPermissions)} instead.")]
public bool DefaultPermission { get; set; } = true;

/// <summary>
/// Gets whether this command can be used in DMs.
/// </summary>
public bool IsEnabledInDm { get; set; } = true;

/// <summary>
/// Gets the default permissions needed for executing this command.
/// </summary>
public GuildPermission? DefaultMemberPermissions { get; set; } = null;

/// <summary>
/// Gets and sets whether this has a <see cref="DontAutoRegisterAttribute"/>.
/// </summary>
@@ -159,12 +170,39 @@ namespace Discord.Interactions.Builders
/// <returns>
/// The builder instance.
/// </returns>
[Obsolete($"To be deprecated soon, use {nameof(SetEnabledInDm)} and {nameof(WithDefaultMemberPermissions)} instead.")]
public ModuleBuilder WithDefaultPermission (bool permission)
{
DefaultPermission = permission;
return this;
}

/// <summary>
/// Sets <see cref="IsEnabledInDm"/>.
/// </summary>
/// <param name="isEnabled">New value of the <see cref="IsEnabledInDm"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public ModuleBuilder SetEnabledInDm(bool isEnabled)
{
IsEnabledInDm = isEnabled;
return this;
}

/// <summary>
/// Sets <see cref="DefaultMemberPermissions"/>.
/// </summary>
/// <param name="permissions">New value of the <see cref="DefaultMemberPermissions"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public ModuleBuilder WithDefaultMemberPermissions(GuildPermission permissions)
{
DefaultMemberPermissions = permissions;
return this;
}

/// <summary>
/// Adds attributes to <see cref="Attributes"/>.
/// </summary>
@@ -319,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>


+ 30
- 0
src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs View File

@@ -85,6 +85,16 @@ namespace Discord.Interactions.Builders
builder.DefaultPermission = defPermission.IsDefaultPermission;
}
break;
case EnabledInDmAttribute enabledInDm:
{
builder.IsEnabledInDm = enabledInDm.IsEnabled;
}
break;
case DefaultMemberPermissionsAttribute memberPermission:
{
builder.DefaultMemberPermissions = memberPermission.Permissions;
}
break;
case PreconditionAttribute precondition:
builder.AddPreconditions(precondition);
break;
@@ -169,6 +179,16 @@ namespace Discord.Interactions.Builders
builder.DefaultPermission = defaultPermission.IsDefaultPermission;
}
break;
case EnabledInDmAttribute enabledInDm:
{
builder.IsEnabledInDm = enabledInDm.IsEnabled;
}
break;
case DefaultMemberPermissionsAttribute memberPermission:
{
builder.DefaultMemberPermissions = memberPermission.Permissions;
}
break;
case PreconditionAttribute precondition:
builder.WithPreconditions(precondition);
break;
@@ -211,6 +231,16 @@ namespace Discord.Interactions.Builders
builder.DefaultPermission = defaultPermission.IsDefaultPermission;
}
break;
case EnabledInDmAttribute enabledInDm:
{
builder.IsEnabledInDm = enabledInDm.IsEnabled;
}
break;
case DefaultMemberPermissionsAttribute memberPermission:
{
builder.DefaultMemberPermissions = memberPermission.Permissions;
}
break;
case PreconditionAttribute precondition:
builder.WithPreconditions(precondition);
break;


+ 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" />


+ 8
- 0
src/Discord.Net.Interactions/Info/Commands/ContextCommands/ContextCommandInfo.cs View File

@@ -17,6 +17,12 @@ namespace Discord.Interactions
/// <inheritdoc/>
public bool DefaultPermission { get; }

/// <inheritdoc/>
public bool IsEnabledInDm { get; }

/// <inheritdoc/>
public GuildPermission? DefaultMemberPermissions { get; }

/// <inheritdoc/>
public override IReadOnlyCollection<CommandParameterInfo> Parameters { get; }

@@ -31,6 +37,8 @@ namespace Discord.Interactions
{
CommandType = builder.CommandType;
DefaultPermission = builder.DefaultPermission;
IsEnabledInDm = builder.IsEnabledInDm;
DefaultMemberPermissions = builder.DefaultMemberPermissions;
Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray();
}



+ 8
- 0
src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs View File

@@ -26,6 +26,12 @@ namespace Discord.Interactions
/// <inheritdoc/>
public bool DefaultPermission { get; }

/// <inheritdoc/>
public bool IsEnabledInDm { get; }

/// <inheritdoc/>
public GuildPermission? DefaultMemberPermissions { get; }

/// <inheritdoc/>
public override IReadOnlyCollection<SlashCommandParameterInfo> Parameters { get; }

@@ -41,6 +47,8 @@ namespace Discord.Interactions
{
Description = builder.Description;
DefaultPermission = builder.DefaultPermission;
IsEnabledInDm = builder.IsEnabledInDm;
DefaultMemberPermissions = builder.DefaultMemberPermissions;
Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray();
FlattenedParameters = FlattenParameters(Parameters).ToImmutableArray();



+ 13
- 0
src/Discord.Net.Interactions/Info/IApplicationCommandInfo.cs View File

@@ -1,3 +1,5 @@
using System;

namespace Discord.Interactions
{
/// <summary>
@@ -18,6 +20,17 @@ namespace Discord.Interactions
/// <summary>
/// Gets the DefaultPermission of this command.
/// </summary>
[Obsolete($"To be deprecated soon, use {nameof(IsEnabledInDm)} and {nameof(DefaultMemberPermissions)} instead.")]
bool DefaultPermission { get; }

/// <summary>
/// Gets whether this command can be used in DMs.
/// </summary>
public bool IsEnabledInDm { get; }

/// <summary>
/// Gets the default permissions needed for executing this command.
/// </summary>
public GuildPermission? DefaultMemberPermissions { get; }
}
}

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

@@ -41,8 +41,19 @@ namespace Discord.Interactions
/// <summary>
/// Gets the default Permission of this module.
/// </summary>
[Obsolete($"To be deprecated soon, use {nameof(IsEnabledInDm)} and {nameof(DefaultMemberPermissions)} instead.")]
public bool DefaultPermission { get; }

/// <summary>
/// Gets whether this command can be used in DMs.
/// </summary>
public bool IsEnabledInDm { get; }

/// <summary>
/// Gets the default permissions needed for executing this command.
/// </summary>
public GuildPermission? DefaultMemberPermissions { get; }

/// <summary>
/// Gets the collection of Sub Modules of this module.
/// </summary>
@@ -110,6 +121,8 @@ namespace Discord.Interactions
Description = builder.Description;
Parent = parent;
DefaultPermission = builder.DefaultPermission;
IsEnabledInDm = builder.IsEnabledInDm;
DefaultMemberPermissions = BuildDefaultMemberPermissions(builder);
SlashCommands = BuildSlashCommands(builder).ToImmutableArray();
ContextCommands = BuildContextCommands(builder).ToImmutableArray();
ComponentCommands = BuildComponentCommands(builder).ToImmutableArray();
@@ -226,5 +239,20 @@ namespace Discord.Interactions
}
return true;
}

private static GuildPermission? BuildDefaultMemberPermissions(ModuleBuilder builder)
{
var permissions = builder.DefaultMemberPermissions;

var parent = builder.Parent;

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

return permissions;
}
}
}

+ 14
- 3
src/Discord.Net.Interactions/InteractionContext.cs View File

@@ -1,7 +1,10 @@
using System.Collections.Generic;
using System.Collections.Immutable;

namespace Discord.Interactions
{
/// <inheritdoc cref="IInteractionContext"/>
public class InteractionContext : IInteractionContext
public class InteractionContext : IInteractionContext, IRouteMatchContainer
{
/// <inheritdoc/>
public IDiscordClient Client { get; }
@@ -13,14 +16,15 @@ namespace Discord.Interactions
public IUser User { get; }
/// <inheritdoc/>
public IDiscordInteraction Interaction { get; }
/// <inheritdoc cref="IRouteMatchContainer.SegmentMatches"/>
public IReadOnlyCollection<IRouteSegmentMatch> SegmentMatches { get; private set; }

/// <summary>
/// Initializes a new <see cref="SocketInteractionContext{TInteraction}"/>.
/// </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;
@@ -30,5 +34,12 @@ namespace Discord.Interactions
User = interaction.User;
Interaction = interaction;
}

/// <inheritdoc/>
public void SetSegmentMatches(IEnumerable<IRouteSegmentMatch> segmentMatches) => SegmentMatches = segmentMatches.ToImmutableArray();

//IRouteMatchContainer
/// <inheritdoc/>
IEnumerable<IRouteSegmentMatch> IRouteMatchContainer.SegmentMatches => SegmentMatches;
}
}

+ 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



+ 34
- 8
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,7 +423,7 @@ 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="commands">Commands to be registered to Discord.</param>
@@ -517,7 +519,7 @@ 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="commands">Commands to be registered to Discord.</param>
/// <returns>
@@ -775,6 +777,9 @@ namespace Discord.Interactions
await _componentCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false);
return result;
}

SetMatchesIfApplicable(context, result);

return await result.Command.ExecuteAsync(context, services, result.RegexCaptureGroups).ConfigureAwait(false);
}

@@ -819,9 +824,30 @@ namespace Discord.Interactions
await _componentCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false);
return result;
}

SetMatchesIfApplicable(context, result);

return await result.Command.ExecuteAsync(context, services, result.RegexCaptureGroups).ConfigureAwait(false);
}

private static void SetMatchesIfApplicable<T>(IInteractionContext context, SearchResult<T> searchResult)
where T : class, ICommandInfo
{
if (!searchResult.Command.SupportsWildCards || context is not IRouteMatchContainer matchContainer)
return;

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);
}
else
matchContainer.SetSegmentMatches(Array.Empty<RouteSegmentMatch>());
}

internal TypeConverter GetTypeConverter(Type type, IServiceProvider services = null)
=> _typeConverterMap.Get(type, services);

@@ -941,7 +967,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>
@@ -954,7 +980,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>
@@ -967,7 +993,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>
@@ -980,7 +1006,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>


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

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

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

if (commandInfo.Parameters.Count > SlashCommandBuilder.MaxOptionsCount)
@@ -64,8 +65,20 @@ namespace Discord.Interactions
public static ApplicationCommandProperties ToApplicationCommandProps(this ContextCommandInfo commandInfo)
=> commandInfo.CommandType switch
{
ApplicationCommandType.Message => new MessageCommandBuilder { Name = commandInfo.Name, IsDefaultPermission = commandInfo.DefaultPermission}.Build(),
ApplicationCommandType.User => new UserCommandBuilder { Name = commandInfo.Name, IsDefaultPermission=commandInfo.DefaultPermission}.Build(),
ApplicationCommandType.Message => new MessageCommandBuilder
{
Name = commandInfo.Name,
IsDefaultPermission = commandInfo.DefaultPermission,
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)).SanitizeGuildPermissions(),
IsDMEnabled = commandInfo.IsEnabledInDm
}.Build(),
_ => throw new InvalidOperationException($"{commandInfo.CommandType} isn't a supported command type.")
};
#endregion
@@ -113,6 +126,8 @@ namespace Discord.Interactions
Name = moduleInfo.SlashGroupName,
Description = moduleInfo.Description,
IsDefaultPermission = moduleInfo.DefaultPermission,
IsDMEnabled = moduleInfo.IsEnabledInDm,
DefaultMemberPermissions = moduleInfo.DefaultMemberPermissions
}.Build();

if (options.Count > SlashCommandBuilder.MaxOptionsCount)
@@ -217,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/ApplicationCommand.cs View File

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

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

// V2 Permissions
[JsonProperty("dm_permission")]
public Optional<bool?> DmPermission { get; set; }

[JsonProperty("default_member_permissions")]
public Optional<GuildPermission?> DefaultMemberPermission { get; set; }
}
}

+ 2
- 1
src/Discord.Net.Rest/API/Common/Channel.cs View File

@@ -70,7 +70,8 @@ namespace Discord.API
//ForumChannel
[JsonProperty("available_tags")]
public Optional<ForumTags[]> ForumTags { get; set; }
[JsonProperty("default_auto_archive_duration")]
public Optional<ThreadArchiveDuration> DefaultAutoArchiveDuration { get; set; }
public Optional<ThreadArchiveDuration> AutoArchiveDuration { 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; }
}
}

+ 6
- 0
src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs View File

@@ -19,6 +19,12 @@ namespace Discord.API.Rest
[JsonProperty("default_permission")]
public Optional<bool> DefaultPermission { get; set; }

[JsonProperty("dm_permission")]
public Optional<bool?> DmPermission { get; set; }

[JsonProperty("default_member_permissions")]
public Optional<GuildPermission?> DefaultMemberPermission { get; set; }

public CreateApplicationCommandParams() { }
public CreateApplicationCommandParams(string name, string description, ApplicationCommandType type, ApplicationCommandOption[] options = null)
{


+ 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" />


+ 38
- 18
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));
@@ -689,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) { }
}
@@ -816,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.");
@@ -834,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.");
@@ -855,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.");
@@ -870,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>
@@ -891,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.");
@@ -911,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)
{
@@ -1398,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);
@@ -1418,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);
}
@@ -1747,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);

@@ -2432,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<InteractionType, 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<InteractionType, 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 != null ? doApiCallOnCreation(model.Type) : _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>


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

Loading…
Cancel
Save