Browse Source

Merge branch 'dev' into customId-segments-context

pull/2136/head
Cenk Ergen GitHub 3 years ago
parent
commit
ded89a7458
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
100 changed files with 1795 additions and 351 deletions
  1. +10
    -1
      .github/ISSUE_TEMPLATE/bugreport.yml
  2. +48
    -0
      CHANGELOG.md
  3. +1
    -1
      Discord.Net.targets
  4. +1
    -1
      docs/docfx.json
  5. +5
    -13
      docs/faq/basics/dependency-injection.md
  6. +26
    -12
      docs/faq/basics/getting-started.md
  7. BIN
      docs/faq/basics/images/link.png
  8. BIN
      docs/faq/basics/images/permissions.png
  9. BIN
      docs/faq/basics/images/scopes.png
  10. +0
    -0
      docs/faq/basics/samples/DI.cs
  11. +6
    -3
      docs/faq/basics/samples/missing-dep.cs
  12. +31
    -12
      docs/faq/int_framework/framework.md
  13. +14
    -35
      docs/faq/int_framework/general.md
  14. +0
    -0
      docs/faq/int_framework/images/scope.png
  15. +45
    -0
      docs/faq/int_framework/manual.md
  16. +6
    -0
      docs/faq/int_framework/samples/interactionsyncing.cs
  17. +8
    -0
      docs/faq/int_framework/samples/propertyinjection.cs
  18. +0
    -0
      docs/faq/int_framework/samples/registerint.cs
  19. +19
    -2
      docs/faq/misc/legacy.md
  20. +6
    -11
      docs/faq/text_commands/general.md
  21. +0
    -0
      docs/faq/text_commands/samples/Remainder.cs
  22. +0
    -0
      docs/faq/text_commands/samples/runmode-cmdattrib.cs
  23. +0
    -0
      docs/faq/text_commands/samples/runmode-cmdconfig.cs
  24. +13
    -9
      docs/faq/toc.yml
  25. +1
    -1
      docs/guides/int_basics/application-commands/slash-commands/choice-slash-command.md
  26. +2
    -2
      docs/guides/int_basics/application-commands/slash-commands/creating-slash-commands.md
  27. +1
    -1
      docs/guides/int_basics/message-components/advanced.md
  28. +5
    -5
      docs/guides/int_basics/message-components/text-input.md
  29. +15
    -0
      docs/guides/int_framework/intro.md
  30. +37
    -0
      docs/guides/int_framework/samples/intro/complexparams.cs
  31. +1
    -1
      docs/guides/int_framework/samples/intro/context.cs
  32. +5
    -3
      docs/guides/int_framework/samples/intro/groupattribute.cs
  33. +61
    -0
      docs/guides/other_libs/efcore.md
  34. BIN
      docs/guides/other_libs/images/serilog_output.png
  35. +36
    -0
      docs/guides/other_libs/samples/ConfiguringSerilog.cs
  36. +9
    -0
      docs/guides/other_libs/samples/DbContextDepInjection.cs
  37. +19
    -0
      docs/guides/other_libs/samples/DbContextSample.cs
  38. +20
    -0
      docs/guides/other_libs/samples/InteractionModuleDISample.cs
  39. +1
    -0
      docs/guides/other_libs/samples/LogDebugSample.cs
  40. +15
    -0
      docs/guides/other_libs/samples/ModifyLogMethod.cs
  41. +45
    -0
      docs/guides/other_libs/serilog.md
  42. +7
    -1
      docs/guides/toc.yml
  43. +11
    -1
      samples/InteractionFramework/ExampleEnum.cs
  44. +1
    -0
      src/Discord.Net.Commands/CommandService.cs
  45. +12
    -0
      src/Discord.Net.Core/CDN.cs
  46. +6
    -0
      src/Discord.Net.Core/DiscordErrorCode.cs
  47. +1
    -1
      src/Discord.Net.Core/Entities/Channels/IChannel.cs
  48. +10
    -5
      src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs
  49. +17
    -0
      src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs
  50. +5
    -0
      src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventsProperties.cs
  51. +2
    -0
      src/Discord.Net.Core/Entities/Guilds/IGuild.cs
  52. +13
    -0
      src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs
  53. +3
    -0
      src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs
  54. +19
    -4
      src/Discord.Net.Core/Entities/Messages/TimestampTag.cs
  55. +31
    -3
      src/Discord.Net.Core/Entities/Users/IGuildUser.cs
  56. +7
    -0
      src/Discord.Net.Core/Entities/Users/IVoiceState.cs
  57. +30
    -0
      src/Discord.Net.Interactions/Attributes/ComplexParameterAttribute.cs
  58. +10
    -0
      src/Discord.Net.Interactions/Attributes/ComplexParameterCtorAttribute.cs
  59. +25
    -0
      src/Discord.Net.Interactions/Attributes/EnumChoiceAttribute.cs
  60. +3
    -3
      src/Discord.Net.Interactions/Builders/Commands/ComponentCommandBuilder.cs
  61. +5
    -0
      src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs
  62. +4
    -0
      src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs
  63. +4
    -2
      src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs
  64. +69
    -9
      src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs
  65. +77
    -0
      src/Discord.Net.Interactions/Builders/Parameters/ComponentCommandParameterBuilder.cs
  66. +8
    -1
      src/Discord.Net.Interactions/Builders/Parameters/ModalCommandParameterBuilder.cs
  67. +66
    -2
      src/Discord.Net.Interactions/Builders/Parameters/SlashCommandParameterBuilder.cs
  68. +12
    -0
      src/Discord.Net.Interactions/Entities/ITypeConverter.cs
  69. +1
    -8
      src/Discord.Net.Interactions/Info/Commands/AutocompleteCommandInfo.cs
  70. +17
    -21
      src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs
  71. +21
    -56
      src/Discord.Net.Interactions/Info/Commands/ComponentCommandInfo.cs
  72. +27
    -9
      src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs
  73. +76
    -33
      src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs
  74. +6
    -0
      src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs
  75. +50
    -1
      src/Discord.Net.Interactions/Info/ModalInfo.cs
  76. +34
    -0
      src/Discord.Net.Interactions/Info/Parameters/ComponentCommandParameterInfo.cs
  77. +8
    -1
      src/Discord.Net.Interactions/Info/Parameters/ModalCommandParameterInfo.cs
  78. +28
    -2
      src/Discord.Net.Interactions/Info/Parameters/SlashCommandParameterInfo.cs
  79. +153
    -63
      src/Discord.Net.Interactions/InteractionService.cs
  80. +1
    -1
      src/Discord.Net.Interactions/InteractionServiceConfig.cs
  81. +92
    -0
      src/Discord.Net.Interactions/Map/TypeMap.cs
  82. +36
    -0
      src/Discord.Net.Interactions/Results/ParseResult.cs
  83. +39
    -0
      src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/ComponentTypeConverter.cs
  84. +45
    -0
      src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultArrayComponentConverter.cs
  85. +26
    -0
      src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultValueComponentConverter.cs
  86. +0
    -0
      src/Discord.Net.Interactions/TypeConverters/SlashCommands/DefaultEntityTypeConverter.cs
  87. +0
    -0
      src/Discord.Net.Interactions/TypeConverters/SlashCommands/DefaultValueConverter.cs
  88. +5
    -2
      src/Discord.Net.Interactions/TypeConverters/SlashCommands/EnumConverter.cs
  89. +0
    -0
      src/Discord.Net.Interactions/TypeConverters/SlashCommands/NullableConverter.cs
  90. +0
    -0
      src/Discord.Net.Interactions/TypeConverters/SlashCommands/TimeSpanConverter.cs
  91. +1
    -1
      src/Discord.Net.Interactions/TypeConverters/SlashCommands/TypeConverter.cs
  92. +48
    -0
      src/Discord.Net.Interactions/TypeReaders/DefaultSnowflakeReader.cs
  93. +22
    -0
      src/Discord.Net.Interactions/TypeReaders/DefaultValueReader.cs
  94. +25
    -0
      src/Discord.Net.Interactions/TypeReaders/EnumReader.cs
  95. +46
    -0
      src/Discord.Net.Interactions/TypeReaders/TypeReader.cs
  96. +3
    -3
      src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs
  97. +5
    -5
      src/Discord.Net.Interactions/Utilities/ModalUtils.cs
  98. +2
    -0
      src/Discord.Net.Rest/API/Common/GuildScheduledEvent.cs
  99. +6
    -0
      src/Discord.Net.Rest/API/Common/ThreadMetadata.cs
  100. +2
    -0
      src/Discord.Net.Rest/API/Common/VoiceState.cs

+ 10
- 1
.github/ISSUE_TEMPLATE/bugreport.yml View File

@@ -18,7 +18,8 @@ body:
attributes:
label: Verify Issue Source
description: If your issue is related to an exception make sure the error was thrown by Discord.Net, and not your code or another library.
If you get an `HttpException` with the error code `401`, then the error is caused by your bot's permissions, not dnet.
If you get an `HttpException` with the error code `401`, then the error is caused by your bot's permissions, not dnet.
If you have a issue that does directly relate to an API bug, feel free to open a [Q&A Discussion](https://github.com/discord-net/Discord.Net/discussions)
options:
- label: I verified the issue was caused by Discord.Net.
required: true
@@ -75,3 +76,11 @@ body:
```
validations:
required: false
- type: textarea
id: packages
attributes:
label: Packages
description: Please list all 3rd party packages in use if applicable, including their versions.
placeholder: Discord.Addons.Hosting V5.1.0, Discord.InteractivityAddon V2.4.0, etc.
validations:
required: true

+ 48
- 0
CHANGELOG.md View File

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

## [3.4.0] - 2022-3-2

## Added
- #2146 Add FromDateTimeOffset in TimestampTag (553055b)
- #2062 Add return statement to precondition handling (3e52fab)
- #2131 Add support for sending Message Flags (1fb62de)
- #2137 Add self_video to VoiceState (8bcd3da)
- #2151 Add Image property to Guild Scheduled Events (1dc473c)
- #2152 Add missing json error codes (202554f)
- #2153 Add IsInvitable and CreatedAt to threads (6bf5818)
- #2155 Add Interaction Service Complex Parameters (9ba64f6)
- #2156 Add Display name support for enum type converter (c800674)

## Fixed
- #2117 Fix stream access exception when ratelimited (a1cfa41)
- #2128 Fix context menu comand message type (f601e9b)
- #2135 Fix NRE when ratelimmited requests don't return a body (b95b942)
- #2154 Fix usage of CacheMode.AllowDownload in channels (b3370c3)

## Misc
- #2149 Clarify Users property on SocketGuildChannel (5594739)
- #2157 Enforce valid button styles (507a18d)

## [3.3.2] - 2022-02-16

### Fixed

- #2116 Fix null rest client in shards

## [3.3.1] - 2022-02-16

### Added

- #2107 Add DisplayName property to IGuildUser. (abfba3c)

### Fixed

- #2110 Fix incorrect ratelimit handles for 429's (b2598d3)
- #2094 Fix ToString() on CommandInfo (01735c8)
- #2098 Fix channel being null in DMs on Interactions (7e1b8c9)
- #2100 Fix crosspost ratelimits (fad217e)
- #2108 Fix being unable to modify AllowedMentions with no embeds set. (169d54f)
- #2109 Fix unused creation of REST clients for DiscordShardedClient shards. (6039378)

### Misc

- #2099 Update interaction summaries (503d32a)

## [3.3.0] - 2022-02-09

### Added


+ 1
- 1
Discord.Net.targets View File

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


+ 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.3.0",
"_appFooter": "Discord.Net (c) 2015-2022 3.4.0",
"_enableSearch": true,
"_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg",
"_appFaviconPath": "favicon.ico"


docs/faq/commands/dependency-injection.md → docs/faq/basics/dependency-injection.md View File

@@ -1,12 +1,12 @@
---
uid: FAQ.Commands.DI
title: Questions about Dependency Injection with Commands
uid: FAQ.Basics.DI
title: Questions about Dependency Injection.
---

# Dependency-injection-related Questions

In the following section, you will find common questions and answers
to utilizing dependency injection with @Discord.Commands, as well as
to utilizing dependency injection with @Discord.Commands and @Discord.Interactions, as well as
common troubleshooting steps regarding DI.

## What is a service? Why does my module not hold any data after execution?
@@ -22,8 +22,7 @@ Service is often used to hold data externally so that they persist
throughout execution. Think of it like a chest that holds
whatever you throw at it that won't be affected by anything unless
you want it to. Note that you should also learn Microsoft's
implementation of [Dependency Injection] \([video]) before proceeding,
as well as how it works in [Discord.Net](xref:Guides.TextCommands.DI#usage-in-modules).
implementation of [Dependency Injection] \([video]) before proceeding.

A brief example of service and dependency injection can be seen below.

@@ -32,18 +31,12 @@ A brief example of service and dependency injection can be seen below.
[Dependency Injection]: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection
[video]: https://www.youtube.com/watch?v=QtDTfn8YxXg

## Why is my `CommandService` complaining about a missing dependency?
## Why is my Command/Interaction Service complaining about a missing dependency?

If you encounter an error similar to `Failed to create MyModule,
dependency MyExternalDependency was not found.`, you may have
forgotten to add the external dependency to the dependency container.

Starting from Discord.Net 2.0, all dependencies required by each
module must be present when the module is loaded into the
[CommandService]. This means when loading the module, you must pass a
valid [IServiceProvider] with the dependency loaded before the module
can be successfully registered.

For example, if your module, `MyModule`, requests a `DatabaseService`
in its constructor, the `DatabaseService` must be present in the
[IServiceProvider] when registering `MyModule`.
@@ -51,4 +44,3 @@ in its constructor, the `DatabaseService` must be present in the
[!code-csharp[Missing Dependencies](samples/missing-dep.cs)]

[IServiceProvider]: xref:System.IServiceProvider
[CommandService]: xref:Discord.Commands.CommandService

+ 26
- 12
docs/faq/basics/getting-started.md View File

@@ -11,18 +11,32 @@ introduction to the Discord API ecosystem.

## How do I add my bot to my server/guild?

You can do so by using the [permission calculator] provided
by [FiniteReality].
This tool allows you to set permissions that the bot will be assigned
with, and invite the bot into your guild. With this method, bots will
also be assigned a unique role that a regular user cannot use; this
is what we call a `Managed` role. Because you cannot assign this
role to any other users, it is much safer than creating a single
role which, intentionally or not, can be applied to other users
to escalate their privilege.

[FiniteReality]: https://github.com/FiniteReality/permissions-calculator
[permission calculator]: https://finitereality.github.io/permissions-calculator
Inviting your bot can be done by using the OAuth2 url generator provided by the [Discord Developer Portal].

Permissions can be granted by selecting the `bot` scope in the scopes section.

![Scopes](images/scopes.png)

A permissions tab will appear below the scope selection,
from which you can pick any permissions your bot may require to function.
When invited, the role this bot is granted will include these permissions.
If you grant no permissions, no role will be created for your bot upon invitation as there is no need for one.

![Permissions](images/permissions.png)

When done selecting permissions, you can use the link below in your browser to invite the bot
to servers where you have the `Manage Server` permission.

![Invite](images/link.png)

If you are planning to play around with slash/context commands,
make sure to check the `application commands` scope before inviting your bot!

> [!NOTE]
> You do not have to kick and reinvite your bot to update permissions/scopes later on.
> Simply reusing the invite link with provided scopes/perms will update it accordingly.

[Discord Developer Portal]: https://discord.com/developers/applications/

## What is a token?



BIN
docs/faq/basics/images/link.png View File

Before After
Width: 1236  |  Height: 72  |  Size: 5.1 KiB

BIN
docs/faq/basics/images/permissions.png View File

Before After
Width: 1242  |  Height: 606  |  Size: 59 KiB

BIN
docs/faq/basics/images/scopes.png View File

Before After
Width: 1240  |  Height: 321  |  Size: 28 KiB

docs/faq/commands/samples/DI.cs → docs/faq/basics/samples/DI.cs View File


docs/faq/commands/samples/missing-dep.cs → docs/faq/basics/samples/missing-dep.cs View File

@@ -11,8 +11,8 @@ public class CommandHandler
public CommandHandler(DiscordSocketClient client)
{
_services = new ServiceCollection()
.AddService<CommandService>()
.AddService(client)
.AddSingleton<CommandService>()
.AddSingleton(client)
// We are missing DatabaseService!
.BuildServiceProvider();
}
@@ -25,5 +25,8 @@ public class CommandHandler
// registered in this instance of _services.
await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services);
// ...

// The same approach applies to the interaction service.
// Make sure to resolve these issues!
}
}
}

docs/faq/commands/interaction.md → docs/faq/int_framework/framework.md View File

@@ -1,34 +1,54 @@
---
uid: FAQ.Commands.Interactions
title: Interaction service
uid: FAQ.Interactions.Framework
title: Interaction Framework
---

# Interaction commands in services
# The Interaction Framework

A chapter talking about the interaction service framework.
For questions about interactions in general, refer to the [Interactions FAQ]
Common misconceptions and questions about the Interaction Framework.

## How can I restrict some of my commands so only specific users can execute them?

Based on how you want to implement the restrictions, you can use the
built-in `RequireUserPermission` precondition, which allows you to
restrict the command based on the user's current permissions in the
guild or channel (*e.g., `GuildPermission.Administrator`,
`ChannelPermission.ManageMessages`*).

[RequireUserPermission]: xref:Discord.Commands.RequireUserPermissionAttribute

> [!NOTE]
> There are many more preconditions to use, including being able to make some yourself.
> Examples on self-made preconditions can be found
> [here](https://github.com/discord-net/Discord.Net/blob/dev/samples/InteractionFramework/Attributes/RequireOwnerAttribute.cs)

## Why do preconditions not hide my commands?

In the current permission design by Discord,
it is not very straight forward to limit vision of slash/context commands to users.
If you want to hide commands, you should take a look at the commands' `DefaultPermissions` parameter.

## Module dependencies aren't getting populated by Property Injection?

Make sure the properties are publicly accessible and publicly settable.

## How do I use this * interaction specific method/property?

If your interaction context holds a down-casted version of the interaction object, you need to up-cast it.
Ideally, use pattern matching to make sure its the type of interaction you are expecting it to be.
[!code-csharp[Property Injection](samples/propertyinjection.cs)]

## `InteractionService.ExecuteAsync()` always returns a successful result, how do i access the failed command execution results?

If you are using `RunMode.Async` you need to setup your post-execution pipeline around `CommandExecuted` events.
If you are using `RunMode.Async` you need to setup your post-execution pipeline around
`..Executed` events exposed by the Interaction Service.

## How do I check if the executing user has * permission?

Refer to the [documentation about preconditions]

[documentation about preconditions]: xref:Guides.ChatCommands.Preconditions

## How do I send the HTTP Response from inside the command modules.

Set the `RestResponseCallback` property of [InteractionServiceConfig] with a delegate for handling HTTP Responses and use
`RestInteractionModuleBase` to create your command modules. `RespondAsync()` and `DeferAsync()` methods of this module base will use the
`RestInteractionModuleBase` to create your command modules. `RespondWithModalAsync()`, `RespondAsync()` and `DeferAsync()` methods of this module base will use the
`RestResponseCallback` to create interaction responses.

## Is there a cleaner way of creating parameter choices other than using `[Choice]`?
@@ -49,4 +69,3 @@ It compares the _target base type_ key of the
[TypeConverter]: xref:Discord.Interactions.TypeConverter
[Interactions FAQ]: xref: FAQ.Basics.Interactions
[InteractionServiceConfig]: xref:Discord.Interactions.InteractionServiceConfig
[documentation about preconditions]: xref: Guides.ChatCommands.Preconditions

docs/faq/basics/interactions.md → docs/faq/int_framework/general.md View File

@@ -1,11 +1,13 @@
---
uid: FAQ.Basics.InteractionBasics
title: Basics of interactions, common practice
uid: FAQ.Interactions.General
title: Interactions
---

# Interactions basics, where to get started
# Interaction basics

This section answers basic questions and common mistakes in handling application commands, and responding to them.
This chapter mostly refers to interactions in general,
and will include questions that are common among users of the Interaction Framework
as well as users that register and handle commands manually.

## What's the difference between RespondAsync, DeferAsync and FollowupAsync?

@@ -24,33 +26,20 @@ DeferAsync will not send out a response, RespondAsync will.

## Im getting System.TimeoutException: 'Cannot respond to an interaction after 3 seconds!'

This happens because your computers clock is out of sync or your trying to respond after 3 seconds. If your clock is out of sync and you cant fix it, you can set the `UseInteractionSnowflakeDate` to false in the config.
This happens because your computer's clock is out of sync or you're trying to respond after 3 seconds.
If your clock is out of sync and you can't fix it, you can set the `UseInteractionSnowflakeDate` to false in the [DiscordSocketConfig].

## Bad form Exception when I try to create my commands, why do I get this?
[!code-csharp[Interaction Sync](samples/interactionsyncing.cs)]

Bad form exceptions are thrown if the slash, user or message command builder has invalid values.
The following options could resolve your error.
[DiscordClientConfig]: xref:Discord.WebSocket.DiscordSocketConfig

#### Is your command name lowercase?
## How do I use this * interaction specific method/property?

If your command name is not lowercase, it is not seen as a valid command entry.
`Avatar` is invalid; `avatar` is valid.

#### Are your values below or above the required amount? (This also applies to message components)

Discord expects all values to be below maximum allowed.
Going over this maximum amount of characters causes an exception.
If your interaction context holds a down-casted version of the interaction object, you need to up-cast it.
Ideally, use pattern matching to make sure its the type of interaction you are expecting it to be.

> [!NOTE]
> All maximum and minimum value requirements can be found in the [Discord Developer Docs].
> For components, structure documentation is found [here].

[Discord Developer Docs]: https://discord.com/developers/docs/interactions/application-commands#application-commands
[here]: https://discord.com/developers/docs/interactions/message-components#message-components

#### Is your subcommand branching correct?

Branching structure is covered properly here: xref:Guides.SlashCommands.SubCommand
> Further documentation on pattern matching can be found [here](xref:Guides.Entities.Casting).

## My interaction commands are not showing up?

@@ -65,16 +54,6 @@ Did you register a guild command (should be instant), or waited more than an hou

- Do you have the application commands scope checked when adding your bot to guilds?

![Scope check](images/scope.png)

## There are many options for creating commands, which do I use?

[!code-csharp[Register examples](samples/registerint.cs)]

> [!NOTE]
> You can use bulkoverwrite even if there are no commands in guild, nor globally.
> The bulkoverwrite method disposes the old set of commands and replaces it with the new.

## Do I need to create commands on startup?

If you are registering your commands for the first time, it is required to create them once.

docs/faq/basics/images/scope.png → docs/faq/int_framework/images/scope.png View File


+ 45
- 0
docs/faq/int_framework/manual.md View File

@@ -0,0 +1,45 @@
---
uid: FAQ.Interactions.Manual
title: Manual handling
---

# Manually handing interactions.

This section talks about the manual building and responding to interactions.
If you are using the interaction framework (highly recommended) this section does not apply to you.

## Bad form Exception when I try to create my commands, why do I get this?

Bad form exceptions are thrown if the slash, user or message command builder has invalid values.
The following options could resolve your error.

#### Is your command name lowercase?

If your command name is not lowercase, it is not seen as a valid command entry.
`Avatar` is invalid; `avatar` is valid.

#### Are your values below or above the required amount? (This also applies to message components)

Discord expects all values to be below maximum allowed.
Going over this maximum amount of characters causes an exception.

> [!NOTE]
> All maximum and minimum value requirements can be found in the [Discord Developer Docs].
> For components, structure documentation is found [here].

[Discord Developer Docs]: https://discord.com/developers/docs/interactions/application-commands#application-commands
[here]: https://discord.com/developers/docs/interactions/message-components#message-components

#### Is your subcommand branching correct?

Branching structure is covered properly here: xref:Guides.SlashCommands.SubCommand

![Scope check](images/scope.png)

## There are many options for creating commands, which do I use?

[!code-csharp[Register examples](samples/registerint.cs)]

> [!NOTE]
> You can use bulkoverwrite even if there are no commands in guild, nor globally.
> The bulkoverwrite method disposes the old set of commands and replaces it with the new.

+ 6
- 0
docs/faq/int_framework/samples/interactionsyncing.cs View File

@@ -0,0 +1,6 @@
DiscordSocketConfig config = new()
{
UseInteractionSnowflakeDate = false
};

DiscordSocketclient client = new(config);

+ 8
- 0
docs/faq/int_framework/samples/propertyinjection.cs View File

@@ -0,0 +1,8 @@
public class MyModule
{
// Intended.
public InteractionService Service { get; set; }

// Will not work. A private setter cannot be accessed by the serviceprovider.
private InteractionService Service { get; private set; }
}

docs/faq/basics/samples/registerint.cs → docs/faq/int_framework/samples/registerint.cs View File


+ 19
- 2
docs/faq/misc/legacy.md View File

@@ -8,15 +8,32 @@ title: Questions about Legacy Versions
This section refers to legacy library-related questions that do not
apply to the latest or recent version of the Discord.Net library.

## Migrating your commands to application commands.

The new interaction service was designed to act like the previous service for text-based commands.
Your pre-existing code will continue to work, but you will need to migrate your modules and response functions to use the new
interaction service methods. Documentation on this can be found in the [Guides](xref:Guides.IntFw.Intro).

## Gateway event parameters changed, why?

With 3.0, a higher focus on [Cacheable]'s was introduced.
[Cacheable]'s get an entity from cache, rather than making an API call to retrieve it's data.
The entity can be retrieved from cache by calling `GetOrDownloadAsync()` on the [Cacheable] type.

> [!NOTE]
> GetOrDownloadAsync will download the entity if its not available directly from the cache.

[Cacheable]: xref:Discord.Cacheable

## X, Y, Z does not work! It doesn't return a valid value anymore.

If you are currently using an older version of the stable branch,
please upgrade to the latest pre-release version to ensure maximum
please upgrade to the latest release version to ensure maximum
compatibility. Several features may be broken in older
versions and will likely not be fixed in the version branch due to
their breaking nature.

Visit the repo's [release tag] to see the latest public pre-release.
Visit the repo's [release tag] to see the latest public release.

[release tag]: https://github.com/discord-net/Discord.Net/releases



docs/faq/commands/general.md → docs/faq/text_commands/general.md View File

@@ -1,6 +1,6 @@
---
uid: FAQ.Commands.General
title: General Questions about chat Commands
uid: FAQ.TextCommands.General
title: General Questions about Text Commands
---

# Chat Command-related Questions
@@ -10,21 +10,16 @@ answered regarding general command usage when using @Discord.Commands.

## How can I restrict some of my commands so only specific users can execute them?

Based on how you want to implement the restrictions, you can use the
built-in [RequireUserPermission] precondition, which allows you to
You can use the built-in `RequireUserPermission` precondition, which allows you to
restrict the command based on the user's current permissions in the
guild or channel (*e.g., `GuildPermission.Administrator`,
`ChannelPermission.ManageMessages`*).

If, however, you wish to restrict the commands based on the user's
role, you can either create your custom precondition or use
Joe4evr's [Preconditions Addons] that provides a few custom
preconditions that aren't provided in the stock library.
Its source can also be used as an example for creating your
custom preconditions.
> [!NOTE]
> There are many more preconditions to use, including being able to make some yourself.
> Precondition documentation is covered [here](xref:Guides.TextCommands.Preconditions)

[RequireUserPermission]: xref:Discord.Commands.RequireUserPermissionAttribute
[Preconditions Addons]: https://github.com/Joe4evr/Discord.Addons/tree/master/src/Discord.Addons.Preconditions

## Why am I getting an error about `Assembly.GetEntryAssembly`?


docs/faq/commands/samples/Remainder.cs → docs/faq/text_commands/samples/Remainder.cs View File


docs/faq/commands/samples/runmode-cmdattrib.cs → docs/faq/text_commands/samples/runmode-cmdattrib.cs View File


docs/faq/commands/samples/runmode-cmdconfig.cs → docs/faq/text_commands/samples/runmode-cmdconfig.cs View File


+ 13
- 9
docs/faq/toc.yml View File

@@ -6,15 +6,19 @@
topicUid: FAQ.Basics.BasicOp
- name: Client Basics
topicUid: FAQ.Basics.ClientBasics
- name: Interactions
topicUid: FAQ.Basics.InteractionBasics
- name: Commands
items:
- name: String commands
topicUid: FAQ.Commands.General
- name: Interaction commands
topicUid: FAQ.Commands.Interactions
- name: Dependency Injection
topicUid: FAQ.Commands.DI
topicUid: FAQ.Basics.DI
- name: Interactions
items:
- name: Starting out
topicUid: FAQ.Interactions.General
- name: Interaction Service/Framework
topicUid: FAQ.Interactions.Framework
- name: Manual handling
topicUid: FAQ.Interactions.Manual
- name: Text Commands
items:
- name: Text Command basics
topicUid: FAQ.TextCommands.General
- name: Legacy or Upgrade
topicUid: FAQ.Legacy

+ 1
- 1
docs/guides/int_basics/application-commands/slash-commands/choice-slash-command.md View File

@@ -27,7 +27,7 @@ private async Task Client_Ready()
.AddChoice("Lovely", 4)
.AddChoice("Excellent!", 5)
.WithType(ApplicationCommandOptionType.Integer)
).Build();
);

try
{


+ 2
- 2
docs/guides/int_basics/application-commands/slash-commands/creating-slash-commands.md View File

@@ -70,14 +70,14 @@ public async Task Client_Ready()
// Let's do our global command
var globalCommand = new SlashCommandBuilder();
globalCommand.WithName("first-global-command");
globalCommand.WithDescription("This is my frist global slash command");
globalCommand.WithDescription("This is my first global slash command");

try
{
// Now that we have our builder, we can call the CreateApplicationCommandAsync method to make our slash command.
await guild.CreateApplicationCommandAsync(guildCommand.Build());

// With global commands we dont need the guild.
// With global commands we don't need the guild.
await client.CreateGlobalApplicationCommandAsync(globalCommand.Build());
// Using the ready event is a simple implementation for the sake of the example. Suitable for testing and development.
// For a production bot, it is recommended to only run the CreateGlobalApplicationCommandAsync() once for each command.


+ 1
- 1
docs/guides/int_basics/message-components/advanced.md View File

@@ -43,7 +43,7 @@ var components = new ComponentBuilder()
.WithSelectMenu(menu);


await arg.RespondAsync("On a scale of one to five, how gaming is this?", component: componBuild(), ephemeral: true);
await arg.RespondAsync("On a scale of one to five, how gaming is this?", component: components.Build(), ephemeral: true);
break;
```



+ 5
- 5
docs/guides/int_basics/message-components/text-input.md View File

@@ -35,11 +35,11 @@ and min/max length of the input:
var tb = new TextInputBuilder()
.WithLabel("Labeled")
.WithCustomId("text_input")
.WithStyle(TextInputStyle.Paragraph)
.WithMinLength(6);
.WithMaxLength(42)
.WithRequired(true)
.WithPlaceholder("Consider this place held.");
.WithStyle(TextInputStyle.Paragraph)
.WithMinLength(6)
.WithMaxLength(42)
.WithRequired(true)
.WithPlaceholder("Consider this place held.");
```

![more advanced text input](images/image9.png)


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

@@ -143,6 +143,21 @@ In this case, user can only input Stage Channels and Text Channels to this param

You can specify the permitted max/min value for a number type parameter using the [MaxValueAttribute] and [MinValueAttribute].

#### Complex Parameters

This allows users to create slash command options using an object's constructor allowing complex objects to be created which cannot be infered from only one input value.
Constructor methods support every attribute type that can be used with the regular slash commands ([Autocomplete], [Summary] etc. ).
Preferred constructor of a Type can be specified either by passing a `Type[]` to the `[ComplexParameterAttribute]` or tagging a type constructor with the `[ComplexParameterCtorAttribute]`. If nothing is specified, the InteractionService defaults to the only public constructor of the type.
TypeConverter pattern is used to parse the constructor methods objects.

[!code-csharp[Complex Parameter](samples/intro/complexparams.cs)]

Interaction service complex parameter constructors are prioritized in the following order:

1. Constructor matching the signature provided in the `[ComplexParameter(Type[])]` overload.
2. Constuctor tagged with `[ComplexParameterCtor]`.
3. Type's only public constuctor.

## User Commands

A valid User Command must have the following structure:


+ 37
- 0
docs/guides/int_framework/samples/intro/complexparams.cs View File

@@ -0,0 +1,37 @@
public class Vector3
{
public int X {get;}
public int Y {get;}
public int Z {get;}

public Vector3()
{
X = 0;
Y = 0;
Z = 0;
}

[ComplexParameterCtor]
public Vector3(int x, int y, int z)
{
X = x;
Y = y;
Z = z;
}
}

// Both of the commands below are displayed to the users identically.

// With complex parameter
[SlashCommand("create-vector", "Create a 3D vector.")]
public async Task CreateVector([ComplexParameter]Vector3 vector3)
{
...
}

// Without complex parameter
[SlashCommand("create-vector", "Create a 3D vector.")]
public async Task CreateVector(int x, int y, int z)
{
...
}

+ 1
- 1
docs/guides/int_framework/samples/intro/context.cs View File

@@ -1,7 +1,7 @@
discordClient.ButtonExecuted += async (interaction) =>
{
var ctx = new SocketInteractionContext<SocketMessageComponent>(discordClient, interaction);
await _interactionService.ExecuteAsync(ctx, serviceProvider);
await _interactionService.ExecuteCommandAsync(ctx, serviceProvider);
};

public class MessageComponentModule : InteractionModuleBase<SocketInteractionContext<SocketMessageComponent>>


+ 5
- 3
docs/guides/int_framework/samples/intro/groupattribute.cs View File

@@ -1,16 +1,18 @@
[SlashCommand("blep", "Send a random adorable animal photo")]
public async Task Blep([Choice("Dog", "dog"), Choice("Cat", "cat"), Choice("Penguin", "penguin")] string animal)
public async Task Blep([Choice("Dog", "dog"), Choice("Cat", "cat"), Choice("Guinea pig", "GuineaPig")] string animal)
{
...
}

// In most cases, you can use an enum to replace the seperate choice attributes in a command.
// In most cases, you can use an enum to replace the separate choice attributes in a command.

public enum Animal
{
Cat,
Dog,
Penguin
// You can also use the ChoiceDisplay attribute to change how they appear in the choice menu.
[ChoiceDisplay("Guinea pig")]
GuineaPig
}

[SlashCommand("blep", "Send a random adorable animal photo")]


+ 61
- 0
docs/guides/other_libs/efcore.md View File

@@ -0,0 +1,61 @@
---
uid: Guides.OtherLibs.EFCore
title: EFCore
---

# Entity Framework Core

In this guide we will set up EFCore with a PostgreSQL database. Information on other databases will be at the bottom of this page.

## Prerequisites

- A simple bot with dependency injection configured
- A running PostgreSQL instance
- [EFCore CLI tools](https://docs.microsoft.com/en-us/ef/core/cli/dotnet#installing-the-tools)

## Downloading the required packages

You can install the following packages through your IDE or go to the nuget link to grab the dotnet cli command.

|Name|Link|
|--|--|
| `Microsoft.EntityFrameworkCore` | [link](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore) |
| `Npgsql.EntityFrameworkCore.PostgreSQL` | [link](https://www.nuget.org/packages/Npgsql.EntityFrameworkCore.PostgreSQL)|

## Configuring the DbContext

To use EFCore, you need a DbContext to access everything in your database. The DbContext will look like this. Here is an example entity to show you how you can add more entities yourself later on.

[!code-csharp[DBContext Sample](samples/DbContextSample.cs)]

> [!NOTE]
> To learn more about creating the EFCore model, visit the following [link](https://docs.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs=netcore-cli#create-the-model)

## Adding the DbContext to your Dependency Injection container

To add your newly created DbContext to your Dependency Injection container, simply use the extension method provided by EFCore to add the context to your container. It should look something like this

[!code-csharp[DBContext Dependency Injection](samples/DbContextDepInjection.cs)]

> [!NOTE]
> You can find out how to get your connection string [here](https://www.connectionstrings.com/npgsql/standard/)

## Migrations

Before you can start using your DbContext, you have to migrate the changes you've made in your code to your actual database.
To learn more about migrations, visit the official Microsoft documentation [here](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli)

## Using the DbContext

You can now use the DbContext wherever you can inject it. Here's an example on injecting it into an interaction command module.

[!code-csharp[DBContext injected into interaction module](samples/InteractionModuleDISample.cs)]

## Using a different database provider

Here's a couple of popular database providers for EFCore and links to tutorials on how to set them up. The only thing that usually changes is the provider inside of your `DbContextOptions`

| Provider | Link |
|--|--|
| MySQL | [link](https://dev.mysql.com/doc/connector-net/en/connector-net-entityframework-core-example.html) |
| SQLite | [link](https://docs.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs=netcore-cli) |

BIN
docs/guides/other_libs/images/serilog_output.png View File

Before After
Width: 1456  |  Height: 141  |  Size: 40 KiB

+ 36
- 0
docs/guides/other_libs/samples/ConfiguringSerilog.cs View File

@@ -0,0 +1,36 @@
using Discord;
using Serilog;
using Serilog.Events;

public class Program
{
static void Main(string[] args) => new Program().MainAsync().GetAwaiter().GetResult();

public async Task MainAsync()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Verbose()
.Enrich.FromLogContext()
.WriteTo.Console()
.CreateLogger();

_client = new DiscordSocketClient();

_client.Log += LogAsync;

// You can assign your bot token to a string, and pass that in to connect.
// This is, however, insecure, particularly if you plan to have your code hosted in a public repository.
var token = "token";

// Some alternative options would be to keep your token in an Environment Variable or a standalone file.
// var token = Environment.GetEnvironmentVariable("NameOfYourEnvironmentVariable");
// var token = File.ReadAllText("token.txt");
// var token = JsonConvert.DeserializeObject<AConfigurationClass>(File.ReadAllText("config.json")).Token;

await _client.LoginAsync(TokenType.Bot, token);
await _client.StartAsync();

// Block this task until the program is closed.
await Task.Delay(Timeout.Infinite);
}
}

+ 9
- 0
docs/guides/other_libs/samples/DbContextDepInjection.cs View File

@@ -0,0 +1,9 @@
private static ServiceProvider ConfigureServices()
{
return new ServiceCollection()
.AddDbContext<ApplicationDbContext>(
options => options.UseNpgsql("Your connection string")
)
[...]
.BuildServiceProvider();
}

+ 19
- 0
docs/guides/other_libs/samples/DbContextSample.cs View File

@@ -0,0 +1,19 @@
// ApplicationDbContext.cs
using Microsoft.EntityFrameworkCore;

public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{

}

public DbSet<UserEntity> Users { get; set; } = null!;
}

// UserEntity.cs
public class UserEntity
{
public ulong Id { get; set; }
public string Name { get; set; }
}

+ 20
- 0
docs/guides/other_libs/samples/InteractionModuleDISample.cs View File

@@ -0,0 +1,20 @@
using Discord;

public class SampleModule : InteractionModuleBase<SocketInteractionContext>
{
private readonly ApplicationDbContext _db;

public SampleModule(ApplicationDbContext db)
{
_db = db;
}

[SlashCommand("sample", "sample")]
public async Task Sample()
{
// Do stuff with your injected DbContext
var user = _db.Users.FirstOrDefault(x => x.Id == Context.User.Id);

...
}
}

+ 1
- 0
docs/guides/other_libs/samples/LogDebugSample.cs View File

@@ -0,0 +1 @@
Log.Debug("Your log message, with {Variables}!", 10); // This will output "[21:51:00 DBG] Your log message, with 10!"

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

@@ -0,0 +1,15 @@
private static async Task LogAsync(LogMessage message)
{
var severity = message.Severity switch
{
LogSeverity.Critical => LogEventLevel.Fatal,
LogSeverity.Error => LogEventLevel.Error,
LogSeverity.Warning => LogEventLevel.Warning,
LogSeverity.Info => LogEventLevel.Information,
LogSeverity.Verbose => LogEventLevel.Verbose,
LogSeverity.Debug => LogEventLevel.Debug,
_ => LogEventLevel.Information
};
Log.Write(severity, message.Exception, "[{Source}] {Message}", message.Source, message.Message);
await Task.CompletedTask;
}

+ 45
- 0
docs/guides/other_libs/serilog.md View File

@@ -0,0 +1,45 @@
---
uid: Guides.OtherLibs.Serilog
title: Serilog
---

# Configuring serilog

## Prerequisites

- A basic working bot with a logging method as described in [Creating your first bot](xref:Guides.GettingStarted.FirstBot)

## Installing the Serilog package

You can install the following packages through your IDE or go to the nuget link to grab the dotnet cli command.

|Name|Link|
|--|--|
|`Serilog.Extensions.Logging`| [link](https://www.nuget.org/packages/Serilog.Extensions.Logging)|
|`Serilog.Sinks.Console`| [link](https://www.nuget.org/packages/Serilog.Sinks.Console)|

## Configuring Serilog

Serilog will be configured at the top of your async Main method, it looks like this

[!code-csharp[Configuring serilog](samples/ConfiguringSerilog.cs)]

## Modifying your logging method

For Serilog to log Discord events correctly, we have to map the Discord `LogSeverity` to the Serilog `LogEventLevel`. You can modify your log method to look like this.

[!code-csharp[Modifying your log method](samples/ModifyLogMethod.cs)]

## Testing

If you run your application now, you should see something similar to this
![Serilog output](images/serilog_output.png)

## Using your new logger in other places

Now that you have set up Serilog, you can use it everywhere in your application by simply calling

[!code-csharp[Log debug sample](samples/LogDebugSample.cs)]

> [!NOTE]
> Depending on your configured log level, the log messages may or may not show up in your console. Refer to [Serilog's github page](https://github.com/serilog/serilog/wiki/Configuration-Basics#minimum-level) for more information about log levels.

+ 7
- 1
docs/guides/toc.yml View File

@@ -95,7 +95,7 @@
topicUid: Guides.MessageComponents.TextInputs
- name: Advanced Concepts
topicUid: Guides.MessageComponents.Advanced
- name: Modal Basics
- name: Modal Basics
items:
- name: Introduction
topicUid: Guides.Modals.Intro
@@ -109,6 +109,12 @@
topicUid: Guides.GuildEvents.GettingUsers
- name: Modifying Events
topicUid: Guides.GuildEvents.Modifying
- name: Working with other libraries
items:
- name: Serilog
topicUid: Guides.OtherLibs.Serilog
- name: EFCore
topicUid: Guides.OtherLibs.EFCore
- name: Emoji
topicUid: Guides.Emoji
- name: Voice


+ 11
- 1
samples/InteractionFramework/ExampleEnum.cs View File

@@ -1,3 +1,11 @@
using Discord.Interactions;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace InteractionFramework
{
public enum ExampleEnum
@@ -5,6 +13,8 @@ namespace InteractionFramework
First,
Second,
Third,
Fourth
Fourth,
[ChoiceDisplay("Twenty First")]
TwentyFirst
}
}

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

@@ -557,6 +557,7 @@ namespace Discord.Commands
if (matchResult.Pipeline is PreconditionResult preconditionResult)
{
await _commandExecutedEvent.InvokeAsync(matchResult.Match.Value.Command, context, preconditionResult).ConfigureAwait(false);
return preconditionResult;
}

return matchResult;


+ 12
- 0
src/Discord.Net.Core/CDN.cs View File

@@ -208,6 +208,18 @@ namespace Discord
public static string GetStickerUrl(ulong stickerId, StickerFormatType format = StickerFormatType.Png)
=> $"{DiscordConfig.CDNUrl}stickers/{stickerId}.{FormatToExtension(format)}";

/// <summary>
/// Returns an events cover image url.
/// </summary>
/// <param name="guildId">The guild id that the event is in.</param>
/// <param name="eventId">The id of the event.</param>
/// <param name="assetId">The id of the cover image asset.</param>
/// <param name="format">The format of the image.</param>
/// <param name="size">The size of the image.</param>
/// <returns></returns>
public static string GetEventCoverImageUrl(ulong guildId, ulong eventId, string assetId, ImageFormat format = ImageFormat.Auto, ushort size = 1024)
=> $"{DiscordConfig.CDNUrl}guild-events/{guildId}/{eventId}/{assetId}.{FormatToExtension(format, assetId)}?size={size}";

private static string FormatToExtension(StickerFormatType format)
{
return format switch


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

@@ -48,6 +48,7 @@ namespace Discord
UnknownSticker = 10060,
UnknownInteraction = 10062,
UnknownApplicationCommand = 10063,
UnknownVoiceState = 10065,
UnknownApplicationCommandPermissions = 10066,
UnknownStageInstance = 10067,
UnknownGuildMemberVerificationForm = 10068,
@@ -96,9 +97,11 @@ namespace Discord
#endregion

#region General Request Errors (40XXX)
MaximumNumberOfEditsReached = 30046,
TokenUnauthorized = 40001,
InvalidVerification = 40002,
OpeningDMTooFast = 40003,
SendMessagesHasBeenTemporarilyDisabled = 40004,
RequestEntityTooLarge = 40005,
FeatureDisabled = 40006,
UserBanned = 40007,
@@ -108,6 +111,7 @@ namespace Discord
#endregion

#region Action Preconditions/Checks (50XXX)
InteractionHasAlreadyBeenAcknowledged = 40060,
MissingPermissions = 50001,
InvalidAccountType = 50002,
CannotExecuteForDM = 50003,
@@ -141,12 +145,14 @@ namespace Discord
InvalidFileUpload = 50046,
CannotSelfRedeemGift = 50054,
InvalidGuild = 50055,
InvalidMessageType = 50068,
PaymentSourceRequiredForGift = 50070,
CannotDeleteRequiredCommunityChannel = 50074,
InvalidSticker = 50081,
CannotExecuteOnArchivedThread = 50083,
InvalidThreadNotificationSettings = 50084,
BeforeValueEarlierThanThreadCreation = 50085,
CommunityServerChannelsMustBeTextChannels = 50086,
ServerLocaleUnavailable = 50095,
ServerRequiresMonetization = 50097,
ServerRequiresBoosts = 50101,


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

@@ -21,7 +21,7 @@ namespace Discord
/// </summary>
/// <remarks>
/// <note type="important">
/// The returned collection is an asynchronous enumerable object; one must call
/// The returned collection is an asynchronous enumerable object; one must call
/// <see cref="AsyncEnumerableExtensions.FlattenAsync{T}"/> to access the individual messages as a
/// collection.
/// </note>


+ 10
- 5
src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs View File

@@ -31,11 +31,12 @@ namespace Discord
/// <param name="components">The message components to be included with this message. Used for interactions.</param>
/// <param name="stickers">A collection of stickers to send with the message.</param>
/// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param>
/// <param name="flags">A message flag to be applied to the sent message, only <see cref="MessageFlags.SuppressEmbeds"/> is permitted.</param>
/// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
Task<IUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null);
Task<IUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None);
/// <summary>
/// Sends a file to this message channel with an optional caption.
/// </summary>
@@ -71,11 +72,12 @@ namespace Discord
/// <param name="components">The message components to be included with this message. Used for interactions.</param>
/// <param name="stickers">A collection of stickers to send with the file.</param>
/// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param>
/// <param name="flags">A message flag to be applied to the sent message, only <see cref="MessageFlags.SuppressEmbeds"/> is permitted.</param>
/// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
Task<IUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null);
Task<IUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None);
/// <summary>
/// Sends a file to this message channel with an optional caption.
/// </summary>
@@ -108,11 +110,12 @@ namespace Discord
/// <param name="components">The message components to be included with this message. Used for interactions.</param>
/// <param name="stickers">A collection of stickers to send with the file.</param>
/// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param>
/// <param name="flags">A message flag to be applied to the sent message, only <see cref="MessageFlags.SuppressEmbeds"/> is permitted.</param>
/// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
Task<IUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null);
Task<IUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None);
/// <summary>
/// Sends a file to this message channel with an optional caption.
/// </summary>
@@ -137,11 +140,12 @@ namespace Discord
/// <param name="components">The message components to be included with this message. Used for interactions.</param>
/// <param name="stickers">A collection of stickers to send with the file.</param>
/// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param>
/// <param name="flags">A message flag to be applied to the sent message, only <see cref="MessageFlags.SuppressEmbeds"/> is permitted.</param>
/// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
Task<IUserMessage> SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null);
Task<IUserMessage> SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None);
/// <summary>
/// Sends a collection of files to this message channel.
/// </summary>
@@ -166,11 +170,12 @@ namespace Discord
/// <param name="components">The message components to be included with this message. Used for interactions.</param>
/// <param name="stickers">A collection of stickers to send with the file.</param>
/// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param>
/// <param name="flags">A message flag to be applied to the sent message, only <see cref="MessageFlags.SuppressEmbeds"/> is permitted.</param>
/// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
Task<IUserMessage> SendFilesAsync(IEnumerable<FileAttachment> attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null);
Task<IUserMessage> SendFilesAsync(IEnumerable<FileAttachment> attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None);

/// <summary>
/// Gets a message from this message channel.


+ 17
- 0
src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs View File

@@ -48,6 +48,23 @@ namespace Discord
/// </summary>
int MessageCount { get; }

/// <summary>
/// Gets whether non-moderators can add other non-moderators to a thread.
/// </summary>
/// <remarks>
/// This property is only available on private threads.
/// </remarks>
bool? IsInvitable { get; }

/// <summary>
/// Gets when the thread was created.
/// </summary>
/// <remarks>
/// This property is only populated for threads created after 2022-01-09, hence the default date of this
/// property will be that date.
/// </remarks>
new DateTimeOffset CreatedAt { get; }

/// <summary>
/// Joins the current thread.
/// </summary>


+ 5
- 0
src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventsProperties.cs View File

@@ -54,5 +54,10 @@ namespace Discord
/// Gets or sets the status of the event.
/// </summary>
public Optional<GuildScheduledEventStatus> Status { get; set; }

/// <summary>
/// Gets or sets the banner image of the event.
/// </summary>
public Optional<Image?> CoverImage { get; set; }
}
}

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

@@ -1105,6 +1105,7 @@ namespace Discord
/// </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>
/// <returns>
/// A task that represents the asynchronous create operation.
@@ -1118,6 +1119,7 @@ namespace Discord
DateTimeOffset? endTime = null,
ulong? channelId = null,
string location = null,
Image? coverImage = null,
RequestOptions options = null);

/// <summary>


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

@@ -39,6 +39,11 @@ namespace Discord
/// </remarks>
string Description { get; }

/// <summary>
/// Gets the banner asset id of the event.
/// </summary>
string CoverImageId { get; }

/// <summary>
/// Gets the start time of the event.
/// </summary>
@@ -80,6 +85,14 @@ namespace Discord
/// </summary>
int? UserCount { get; }

/// <summary>
/// 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.
/// <returns>The cover images url.</returns>
string GetCoverImageUrl(ImageFormat format = ImageFormat.Auto, ushort size = 1024);

/// <summary>
/// Starts the event.
/// </summary>


+ 3
- 0
src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs View File

@@ -613,6 +613,9 @@ namespace Discord
if (!(string.IsNullOrEmpty(Url) ^ string.IsNullOrEmpty(CustomId)))
throw new InvalidOperationException("A button must contain either a URL or a CustomId, but not both!");

if (Style == 0)
throw new ArgumentException("A button must have a style.", nameof(Style));

if (Style == ButtonStyle.Link)
{
if (string.IsNullOrEmpty(Url))


+ 19
- 4
src/Discord.Net.Core/Entities/Messages/TimestampTag.cs View File

@@ -15,7 +15,7 @@ namespace Discord
/// <summary>
/// Gets or sets the time for this timestamp tag.
/// </summary>
public DateTime Time { get; set; }
public DateTimeOffset Time { get; set; }

/// <summary>
/// Converts the current timestamp tag to the string representation supported by discord.
@@ -26,11 +26,11 @@ namespace Discord
/// <returns>A string that is compatible in a discord message, ex: <code>&lt;t:1625944201:f&gt;</code></returns>
public override string ToString()
{
return $"<t:{((DateTimeOffset)Time).ToUnixTimeSeconds()}:{(char)Style}>";
return $"<t:{Time.ToUnixTimeSeconds()}:{(char)Style}>";
}

/// <summary>
/// Creates a new timestamp tag with the specified datetime object.
/// Creates a new timestamp tag with the specified <see cref="DateTime"/> object.
/// </summary>
/// <param name="time">The time of this timestamp tag.</param>
/// <param name="style">The style for this timestamp tag.</param>
@@ -43,5 +43,20 @@ namespace Discord
Time = time
};
}

/// <summary>
/// Creates a new timestamp tag with the specified <see cref="DateTimeOffset"/> object.
/// </summary>
/// <param name="time">The time of this timestamp tag.</param>
/// <param name="style">The style for this timestamp tag.</param>
/// <returns>The newly create timestamp tag.</returns>
public static TimestampTag FromDateTimeOffset(DateTimeOffset time, TimestampTagStyles style = TimestampTagStyles.ShortDateTime)
{
return new TimestampTag
{
Style = style,
Time = time
};
}
}
}
}

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

@@ -18,6 +18,13 @@ namespace Discord
/// </returns>
DateTimeOffset? JoinedAt { get; }
/// <summary>
/// Gets the displayed name for this user.
/// </summary>
/// <returns>
/// A string representing the display name of the user; If the nickname is null, this will be the username.
/// </returns>
string DisplayName { get; }
/// <summary>
/// Gets the nickname for this user.
/// </summary>
/// <returns>
@@ -25,7 +32,15 @@ namespace Discord
/// </returns>
string Nickname { get; }
/// <summary>
/// Gets the guild specific avatar for this users.
/// Gets the displayed avatar for this user.
/// </summary>
/// <returns>
/// The users displayed avatar hash. If the user does not have a guild avatar, this will be the regular avatar.
/// If the user also does not have a regular avatar, this will be <see langword="null"/>.
/// </returns>
string DisplayAvatarId { get; }
/// <summary>
/// Gets the guild specific avatar for this user.
/// </summary>
/// <returns>
/// The users guild avatar hash if they have one; otherwise <see langword="null"/>.
@@ -119,16 +134,29 @@ namespace Discord
/// </summary>
/// <remarks>
/// This property retrieves a URL for this guild user's guild specific avatar. In event that the user does not have a valid guild avatar
/// (i.e. their avatar identifier is not set), this method will return <c>null</c>.
/// (i.e. their avatar identifier is not set), this method will return <see langword="null"/>.
/// </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>
/// <returns>
/// A string representing the user's avatar URL; <c>null</c> if the user does not have an avatar in place.
/// A string representing the user's avatar URL; <see langword="null"/> if the user does not have an avatar in place.
/// </returns>
string GetGuildAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128);
/// <summary>
/// Gets the display avatar URL for this user.
/// </summary>
/// <remarks>
/// This property retrieves an URL for this guild user's displayed avatar.
/// 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.
/// <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>
string GetDisplayAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128);
/// <summary>
/// Kicks this user from this guild.
/// </summary>
/// <param name="reason">The reason for the kick which will be recorded in the audit log.</param>


+ 7
- 0
src/Discord.Net.Core/Entities/Users/IVoiceState.cs View File

@@ -65,6 +65,13 @@ namespace Discord
/// </returns>
bool IsStreaming { get; }
/// <summary>
/// Gets a value that indicates if the user is videoing in a voice channel.
/// </summary>
/// <returns>
/// <c>true</c> if the user has their camera turned on; otherwise <c>false</c>.
/// </returns>
bool IsVideoing { get; }
/// <summary>
/// Gets the time on which the user requested to speak.
/// </summary>
DateTimeOffset? RequestToSpeakTimestamp { get; }


+ 30
- 0
src/Discord.Net.Interactions/Attributes/ComplexParameterAttribute.cs View File

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

namespace Discord.Interactions
{
/// <summary>
/// Registers a parameter as a complex parameter.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public class ComplexParameterAttribute : Attribute
{
/// <summary>
/// Gets the parameter array of the constructor method that should be prioritized.
/// </summary>
public Type[] PrioritizedCtorSignature { get; }

/// <summary>
/// Registers a slash command parameter as a complex parameter.
/// </summary>
public ComplexParameterAttribute() { }

/// <summary>
/// Registers a slash command parameter as a complex parameter with a specified constructor signature.
/// </summary>
/// <param name="types">Type array of the preferred constructor parameters.</param>
public ComplexParameterAttribute(Type[] types)
{
PrioritizedCtorSignature = types;
}
}
}

+ 10
- 0
src/Discord.Net.Interactions/Attributes/ComplexParameterCtorAttribute.cs View File

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

namespace Discord.Interactions
{
/// <summary>
/// Tag a type constructor as the preferred Complex command constructor.
/// </summary>
[AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = true)]
public class ComplexParameterCtorAttribute : Attribute { }
}

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

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

namespace Discord.Interactions
{
/// <summary>
/// Customize the displayed value of a slash command choice enum. Only works with the default enum type converter.
/// </summary>
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
public class ChoiceDisplayAttribute : Attribute
{
/// <summary>
/// Gets the name of the parameter.
/// </summary>
public string Name { get; } = null;

/// <summary>
/// Modify the default name and description values of a Slash Command parameter.
/// </summary>
/// <param name="name">Name of the parameter.</param>
public ChoiceDisplayAttribute(string name)
{
Name = name;
}
}
}

+ 3
- 3
src/Discord.Net.Interactions/Builders/Commands/ComponentCommandBuilder.cs View File

@@ -5,7 +5,7 @@ namespace Discord.Interactions.Builders
/// <summary>
/// Represents a builder for creating <see cref="ComponentCommandInfo"/>.
/// </summary>
public sealed class ComponentCommandBuilder : CommandBuilder<ComponentCommandInfo, ComponentCommandBuilder, CommandParameterBuilder>
public sealed class ComponentCommandBuilder : CommandBuilder<ComponentCommandInfo, ComponentCommandBuilder, ComponentCommandParameterBuilder>
{
protected override ComponentCommandBuilder Instance => this;

@@ -26,9 +26,9 @@ namespace Discord.Interactions.Builders
/// <returns>
/// The builder instance.
/// </returns>
public override ComponentCommandBuilder AddParameter (Action<CommandParameterBuilder> configure)
public override ComponentCommandBuilder AddParameter (Action<ComponentCommandParameterBuilder> configure)
{
var parameter = new CommandParameterBuilder(this);
var parameter = new ComponentCommandParameterBuilder(this);
configure(parameter);
AddParameters(parameter);
return this;


+ 5
- 0
src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs View File

@@ -38,6 +38,11 @@ namespace Discord.Interactions.Builders
/// </summary>
Type Type { get; }

/// <summary>
/// Get the <see cref="ComponentTypeConverter"/> assigned to this input.
/// </summary>
ComponentTypeConverter TypeConverter { get; }

/// <summary>
/// Gets the default value of this input component.
/// </summary>


+ 4
- 0
src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs View File

@@ -33,6 +33,9 @@ namespace Discord.Interactions.Builders
/// <inheritdoc/>
public Type Type { get; private set; }

/// <inheritdoc/>
public ComponentTypeConverter TypeConverter { get; private set; }

/// <inheritdoc/>
public object DefaultValue { get; set; }

@@ -111,6 +114,7 @@ namespace Discord.Interactions.Builders
public TBuilder WithType(Type type)
{
Type = type;
TypeConverter = Modal._interactionService.GetComponentTypeConverter(type);
return Instance;
}



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

@@ -9,6 +9,7 @@ namespace Discord.Interactions.Builders
/// </summary>
public class ModalBuilder
{
internal readonly InteractionService _interactionService;
internal readonly List<IInputComponentBuilder> _components;

/// <summary>
@@ -31,11 +32,12 @@ namespace Discord.Interactions.Builders
/// </summary>
public IReadOnlyCollection<IInputComponentBuilder> Components => _components;

internal ModalBuilder(Type type)
internal ModalBuilder(Type type, InteractionService interactionService)
{
if (!typeof(IModal).IsAssignableFrom(type))
throw new ArgumentException($"Must be an implementation of {nameof(IModal)}", nameof(type));

_interactionService = interactionService;
_components = new();
}

@@ -43,7 +45,7 @@ namespace Discord.Interactions.Builders
/// Initializes a new <see cref="ModalBuilder"/>
/// </summary>
/// <param name="modalInitializer">The initialization delegate for this modal.</param>
public ModalBuilder(Type type, ModalInitializer modalInitializer) : this(type)
public ModalBuilder(Type type, ModalInitializer modalInitializer, InteractionService interactionService) : this(type, interactionService)
{
ModalInitializer = modalInitializer;
}


+ 69
- 9
src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs View File

@@ -231,9 +231,6 @@ namespace Discord.Interactions.Builders
private static void BuildComponentCommand (ComponentCommandBuilder builder, Func<IServiceProvider, IInteractionModuleBase> createInstance, MethodInfo methodInfo,
InteractionService commandService, IServiceProvider services)
{
if (!methodInfo.GetParameters().All(x => x.ParameterType == typeof(string) || x.ParameterType == typeof(string[])))
throw new InvalidOperationException($"Interaction method parameters all must be types of {typeof(string).Name} or {typeof(string[]).Name}");

var attributes = methodInfo.GetCustomAttributes();

builder.MethodName = methodInfo.Name;
@@ -260,8 +257,10 @@ namespace Discord.Interactions.Builders

var parameters = methodInfo.GetParameters();

var wildCardCount = Regex.Matches(Regex.Escape(builder.Name), Regex.Escape(commandService._wildCardExp)).Count;

foreach (var parameter in parameters)
builder.AddParameter(x => BuildParameter(x, parameter));
builder.AddParameter(x => BuildComponentParameter(x, parameter, parameter.Position >= wildCardCount));

builder.Callback = CreateCallback(createInstance, methodInfo, commandService);
}
@@ -310,8 +309,8 @@ namespace Discord.Interactions.Builders
if (parameters.Count(x => typeof(IModal).IsAssignableFrom(x.ParameterType)) > 1)
throw new InvalidOperationException($"A modal command can only have one {nameof(IModal)} parameter.");

if (!parameters.All(x => x.ParameterType == typeof(string) || typeof(IModal).IsAssignableFrom(x.ParameterType)))
throw new InvalidOperationException($"All parameters of a modal command must be either a string or an implementation of {nameof(IModal)}");
if (!typeof(IModal).IsAssignableFrom(parameters.Last().ParameterType))
throw new InvalidOperationException($"Last parameter of a modal command must be an implementation of {nameof(IModal)}");

var attributes = methodInfo.GetCustomAttributes();

@@ -397,7 +396,6 @@ namespace Discord.Interactions.Builders
builder.Description = paramInfo.Name;
builder.IsRequired = !paramInfo.IsOptional;
builder.DefaultValue = paramInfo.DefaultValue;
builder.SetParameterType(paramType, services);

foreach (var attribute in attributes)
{
@@ -435,16 +433,42 @@ namespace Discord.Interactions.Builders
case MinValueAttribute minValue:
builder.MinValue = minValue.Value;
break;
case ComplexParameterAttribute complexParameter:
{
builder.IsComplexParameter = true;
ConstructorInfo ctor = GetComplexParameterConstructor(paramInfo.ParameterType.GetTypeInfo(), complexParameter);

foreach (var parameter in ctor.GetParameters())
{
if (parameter.IsDefined(typeof(ComplexParameterAttribute)))
throw new InvalidOperationException("You cannot create nested complex parameters.");

builder.AddComplexParameterField(fieldBuilder => BuildSlashParameter(fieldBuilder, parameter, services));
}

var initializer = builder.Command.Module.InteractionService._useCompiledLambda ?
ReflectionUtils<object>.CreateLambdaConstructorInvoker(paramInfo.ParameterType.GetTypeInfo()) : ctor.Invoke;
builder.ComplexParameterInitializer = args => initializer(args);
}
break;
default:
builder.AddAttributes(attribute);
break;
}
}

builder.SetParameterType(paramType, services);

// Replace pascal casings with '-'
builder.Name = Regex.Replace(builder.Name, "(?<=[a-z])(?=[A-Z])", "-").ToLower();
}

private static void BuildComponentParameter(ComponentCommandParameterBuilder builder, ParameterInfo paramInfo, bool isComponentParam)
{
builder.SetIsRouteSegment(!isComponentParam);
BuildParameter(builder, paramInfo);
}

private static void BuildParameter<TInfo, TBuilder> (ParameterBuilder<TInfo, TBuilder> builder, ParameterInfo paramInfo)
where TInfo : class, IParameterInfo
where TBuilder : ParameterBuilder<TInfo, TBuilder>
@@ -476,7 +500,7 @@ namespace Discord.Interactions.Builders
#endregion

#region Modals
public static ModalInfo BuildModalInfo(Type modalType)
public static ModalInfo BuildModalInfo(Type modalType, InteractionService interactionService)
{
if (!typeof(IModal).IsAssignableFrom(modalType))
throw new InvalidOperationException($"{modalType.FullName} isn't an implementation of {typeof(IModal).FullName}");
@@ -485,7 +509,7 @@ namespace Discord.Interactions.Builders

try
{
var builder = new ModalBuilder(modalType)
var builder = new ModalBuilder(modalType, interactionService)
{
Title = instance.Title
};
@@ -608,5 +632,41 @@ namespace Discord.Interactions.Builders
propertyInfo.SetMethod?.IsStatic == false &&
propertyInfo.IsDefined(typeof(ModalInputAttribute));
}
private static ConstructorInfo GetComplexParameterConstructor(TypeInfo typeInfo, ComplexParameterAttribute complexParameter)
{
var ctors = typeInfo.GetConstructors();

if (ctors.Length == 0)
throw new InvalidOperationException($"No constructor found for \"{typeInfo.FullName}\".");

if (complexParameter.PrioritizedCtorSignature is not null)
{
var ctor = typeInfo.GetConstructor(complexParameter.PrioritizedCtorSignature);

if (ctor is null)
throw new InvalidOperationException($"No constructor was found with the signature: {string.Join(",", complexParameter.PrioritizedCtorSignature.Select(x => x.Name))}");

return ctor;
}

var prioritizedCtors = ctors.Where(x => x.IsDefined(typeof(ComplexParameterCtorAttribute), true));

switch (prioritizedCtors.Count())
{
case > 1:
throw new InvalidOperationException($"{nameof(ComplexParameterCtorAttribute)} can only be used once in a type.");
case 1:
return prioritizedCtors.First();
}

switch (ctors.Length)
{
case > 1:
throw new InvalidOperationException($"Multiple constructors found for \"{typeInfo.FullName}\".");
default:
return ctors.First();
}
}
}
}

+ 77
- 0
src/Discord.Net.Interactions/Builders/Parameters/ComponentCommandParameterBuilder.cs View File

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

namespace Discord.Interactions.Builders
{
/// <summary>
/// Represents a builder for creating <see cref="ComponentCommandParameterInfo"/>.
/// </summary>
public class ComponentCommandParameterBuilder : ParameterBuilder<ComponentCommandParameterInfo, ComponentCommandParameterBuilder>
{
/// <summary>
/// Get the <see cref="ComponentTypeConverter"/> assigned to this parameter, if <see cref="IsRouteSegmentParameter"/> is <see langword="false"/>.
/// </summary>
public ComponentTypeConverter TypeConverter { get; private set; }

/// <summary>
/// Get the <see cref="Discord.Interactions.TypeReader"/> assigned to this parameter, if <see cref="IsRouteSegmentParameter"/> is <see langword="true"/>.
/// </summary>
public TypeReader TypeReader { get; private set; }

/// <summary>
/// Gets whether this parameter is a CustomId segment or a Component value parameter.
/// </summary>
public bool IsRouteSegmentParameter { get; private set; }

/// <inheritdoc/>
protected override ComponentCommandParameterBuilder Instance => this;

internal ComponentCommandParameterBuilder(ICommandBuilder command) : base(command) { }

/// <summary>
/// Initializes a new <see cref="ComponentCommandParameterBuilder"/>.
/// </summary>
/// <param name="command">Parent command of this parameter.</param>
/// <param name="name">Name of this command.</param>
/// <param name="type">Type of this parameter.</param>
public ComponentCommandParameterBuilder(ICommandBuilder command, string name, Type type) : base(command, name, type) { }

/// <inheritdoc/>
public override ComponentCommandParameterBuilder SetParameterType(Type type) => SetParameterType(type, null);

/// <summary>
/// Sets <see cref="ParameterBuilder{TInfo, TBuilder}.ParameterType"/>.
/// </summary>
/// <param name="type">New value of the <see cref="ParameterBuilder{TInfo, TBuilder}.ParameterType"/>.</param>
/// <param name="services">Service container to be used to resolve the dependencies of this parameters <see cref="Interactions.TypeConverter"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public ComponentCommandParameterBuilder SetParameterType(Type type, IServiceProvider services)
{
base.SetParameterType(type);

if (IsRouteSegmentParameter)
TypeReader = Command.Module.InteractionService.GetTypeReader(type);
else
TypeConverter = Command.Module.InteractionService.GetComponentTypeConverter(ParameterType, services);

return this;
}

/// <summary>
/// Sets <see cref="IsRouteSegmentParameter"/>.
/// </summary>
/// <param name="isRouteSegment">New value of the <see cref="IsRouteSegmentParameter"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public ComponentCommandParameterBuilder SetIsRouteSegment(bool isRouteSegment)
{
IsRouteSegmentParameter = isRouteSegment;
return this;
}

internal override ComponentCommandParameterInfo Build(ICommandInfo command)
=> new(this, command);
}
}

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

@@ -20,6 +20,11 @@ namespace Discord.Interactions.Builders
/// </summary>
public bool IsModalParameter => Modal is not null;

/// <summary>
/// Gets the <see cref="TypeReader"/> assigned to this parameter, if <see cref="IsModalParameter"/> is <see langword="true"/>.
/// </summary>
public TypeReader TypeReader { get; private set; }

internal ModalCommandParameterBuilder(ICommandBuilder command) : base(command) { }

/// <summary>
@@ -34,7 +39,9 @@ namespace Discord.Interactions.Builders
public override ModalCommandParameterBuilder SetParameterType(Type type)
{
if (typeof(IModal).IsAssignableFrom(type))
Modal = ModalUtils.GetOrAdd(type);
Modal = ModalUtils.GetOrAdd(type, Command.Module.InteractionService);
else
TypeReader = Command.Module.InteractionService.GetTypeReader(type);

return base.SetParameterType(type);
}


+ 66
- 2
src/Discord.Net.Interactions/Builders/Parameters/SlashCommandParameterBuilder.cs View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace Discord.Interactions.Builders
{
@@ -10,6 +11,7 @@ namespace Discord.Interactions.Builders
{
private readonly List<ParameterChoice> _choices = new();
private readonly List<ChannelType> _channelTypes = new();
private readonly List<SlashCommandParameterBuilder> _complexParameterFields = new();

/// <summary>
/// Gets or sets the description of this parameter.
@@ -36,6 +38,11 @@ namespace Discord.Interactions.Builders
/// </summary>
public IReadOnlyCollection<ChannelType> ChannelTypes => _channelTypes;

/// <summary>
/// Gets the constructor parameters of this parameter, if <see cref="IsComplexParameter"/> is <see langword="true"/>.
/// </summary>
public IReadOnlyCollection<SlashCommandParameterBuilder> ComplexParameterFields => _complexParameterFields;

/// <summary>
/// Gets or sets whether this parameter should be configured for Autocomplete Interactions.
/// </summary>
@@ -46,6 +53,16 @@ namespace Discord.Interactions.Builders
/// </summary>
public TypeConverter TypeConverter { get; private set; }

/// <summary>
/// Gets whether this type should be treated as a complex parameter.
/// </summary>
public bool IsComplexParameter { get; internal set; }

/// <summary>
/// Gets the initializer delegate for this parameter, if <see cref="IsComplexParameter"/> is <see langword="true"/>.
/// </summary>
public ComplexParameterInitializer ComplexParameterInitializer { get; internal set; }

/// <summary>
/// Gets or sets the <see cref="IAutocompleteHandler"/> of this parameter.
/// </summary>
@@ -60,7 +77,14 @@ namespace Discord.Interactions.Builders
/// <param name="command">Parent command of this parameter.</param>
/// <param name="name">Name of this command.</param>
/// <param name="type">Type of this parameter.</param>
public SlashCommandParameterBuilder(ICommandBuilder command, string name, Type type) : base(command, name, type) { }
public SlashCommandParameterBuilder(ICommandBuilder command, string name, Type type, ComplexParameterInitializer complexParameterInitializer = null)
: base(command, name, type)
{
ComplexParameterInitializer = complexParameterInitializer;

if (complexParameterInitializer is not null)
IsComplexParameter = true;
}

/// <summary>
/// Sets <see cref="Description"/>.
@@ -168,7 +192,47 @@ namespace Discord.Interactions.Builders
public SlashCommandParameterBuilder SetParameterType(Type type, IServiceProvider services = null)
{
base.SetParameterType(type);
TypeConverter = Command.Module.InteractionService.GetTypeConverter(ParameterType, services);

if(!IsComplexParameter)
TypeConverter = Command.Module.InteractionService.GetTypeConverter(ParameterType, services);

return this;
}

/// <summary>
/// Adds a parameter builders to <see cref="ComplexParameterFields"/>.
/// </summary>
/// <param name="configure"><see cref="SlashCommandParameterBuilder"/> factory.</param>
/// <returns>
/// The builder instance.
/// </returns>
/// <exception cref="InvalidOperationException">Thrown if the added field has a <see cref="ComplexParameterAttribute"/>.</exception>
public SlashCommandParameterBuilder AddComplexParameterField(Action<SlashCommandParameterBuilder> configure)
{
SlashCommandParameterBuilder builder = new(Command);
configure(builder);

if(builder.IsComplexParameter)
throw new InvalidOperationException("You cannot create nested complex parameters.");

_complexParameterFields.Add(builder);
return this;
}

/// <summary>
/// Adds parameter builders to <see cref="ComplexParameterFields"/>.
/// </summary>
/// <param name="fields">New parameter builders to be added to <see cref="ComplexParameterFields"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
/// <exception cref="InvalidOperationException">Thrown if the added field has a <see cref="ComplexParameterAttribute"/>.</exception>
public SlashCommandParameterBuilder AddComplexParameterFields(params SlashCommandParameterBuilder[] fields)
{
if(fields.Any(x => x.IsComplexParameter))
throw new InvalidOperationException("You cannot create nested complex parameters.");

_complexParameterFields.AddRange(fields);
return this;
}



+ 12
- 0
src/Discord.Net.Interactions/Entities/ITypeConverter.cs View File

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

namespace Discord.Interactions
{
internal interface ITypeConverter<T>
{
public bool CanConvertTo(Type type);

public Task<TypeConverterResult> ReadAsync(IInteractionContext context, T option, IServiceProvider services);
}
}

+ 1
- 8
src/Discord.Net.Interactions/Info/Commands/AutocompleteCommandInfo.cs View File

@@ -41,14 +41,7 @@ namespace Discord.Interactions
if (context.Interaction is not IAutocompleteInteraction)
return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Autocomplete Interaction");

try
{
return await RunAsync(context, Array.Empty<object>(), services).ConfigureAwait(false);
}
catch (Exception ex)
{
return ExecuteResult.FromError(ex);
}
return await RunAsync(context, Array.Empty<object>(), services).ConfigureAwait(false);
}

/// <inheritdoc/>


+ 17
- 21
src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs View File

@@ -31,6 +31,8 @@ namespace Discord.Interactions
private readonly ExecuteCallback _action;
private readonly ILookup<string, PreconditionAttribute> _groupedPreconditions;

internal IReadOnlyDictionary<string, TParameter> _parameterDictionary { get; }

/// <inheritdoc/>
public ModuleInfo Module { get; }

@@ -79,6 +81,7 @@ namespace Discord.Interactions

_action = builder.Callback;
_groupedPreconditions = builder.Preconditions.ToLookup(x => x.Group, x => x, StringComparer.Ordinal);
_parameterDictionary = Parameters?.ToDictionary(x => x.Name, x => x).ToImmutableDictionary();
}

/// <inheritdoc/>
@@ -120,10 +123,7 @@ namespace Discord.Interactions
return moduleResult;

var commandResult = await CheckGroups(_groupedPreconditions, "Command").ConfigureAwait(false);
if (!commandResult.IsSuccess)
return commandResult;

return PreconditionResult.FromSuccess();
return !commandResult.IsSuccess ? commandResult : PreconditionResult.FromSuccess();
}

protected async Task<IResult> RunAsync(IInteractionContext context, object[] args, IServiceProvider services)
@@ -137,8 +137,8 @@ namespace Discord.Interactions
using var scope = services?.CreateScope();
return await ExecuteInternalAsync(context, args, scope?.ServiceProvider ?? EmptyServiceProvider.Instance).ConfigureAwait(false);
}
else
return await ExecuteInternalAsync(context, args, services).ConfigureAwait(false);
return await ExecuteInternalAsync(context, args, services).ConfigureAwait(false);
}
case RunMode.Async:
_ = Task.Run(async () =>
@@ -167,20 +167,14 @@ namespace Discord.Interactions
{
var preconditionResult = await CheckPreconditionsAsync(context, services).ConfigureAwait(false);
if (!preconditionResult.IsSuccess)
{
await InvokeModuleEvent(context, preconditionResult).ConfigureAwait(false);
return preconditionResult;
}
return await InvokeEventAndReturn(context, preconditionResult).ConfigureAwait(false);

var index = 0;
foreach (var parameter in Parameters)
{
var result = await parameter.CheckPreconditionsAsync(context, args[index++], services).ConfigureAwait(false);
if (!result.IsSuccess)
{
await InvokeModuleEvent(context, result).ConfigureAwait(false);
return result;
}
return await InvokeEventAndReturn(context, result).ConfigureAwait(false);
}

var task = _action(context, args, services, this);
@@ -189,20 +183,16 @@ namespace Discord.Interactions
{
var result = await resultTask.ConfigureAwait(false);
await InvokeModuleEvent(context, result).ConfigureAwait(false);
if (result is RuntimeResult || result is ExecuteResult)
if (result is RuntimeResult or ExecuteResult)
return result;
}
else
{
await task.ConfigureAwait(false);
var result = ExecuteResult.FromSuccess();
await InvokeModuleEvent(context, result).ConfigureAwait(false);
return result;
return await InvokeEventAndReturn(context, ExecuteResult.FromSuccess()).ConfigureAwait(false);
}

var failResult = ExecuteResult.FromError(InteractionCommandError.Unsuccessful, "Command execution failed for an unknown reason");
await InvokeModuleEvent(context, failResult).ConfigureAwait(false);
return failResult;
return await InvokeEventAndReturn(context, ExecuteResult.FromError(InteractionCommandError.Unsuccessful, "Command execution failed for an unknown reason")).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -231,6 +221,12 @@ namespace Discord.Interactions
}
}

protected async ValueTask<IResult> InvokeEventAndReturn(IInteractionContext context, IResult result)
{
await InvokeModuleEvent(context, result).ConfigureAwait(false);
return result;
}

private static bool CheckTopLevel(ModuleInfo parent)
{
var currentParent = parent;


+ 21
- 56
src/Discord.Net.Interactions/Info/Commands/ComponentCommandInfo.cs View File

@@ -1,5 +1,4 @@
using Discord.Interactions.Builders;
using Discord.WebSocket;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -11,10 +10,10 @@ namespace Discord.Interactions
/// <summary>
/// Represents the info class of an attribute based method for handling Component Interaction events.
/// </summary>
public class ComponentCommandInfo : CommandInfo<CommandParameterInfo>
public class ComponentCommandInfo : CommandInfo<ComponentCommandParameterInfo>
{
/// <inheritdoc/>
public override IReadOnlyCollection<CommandParameterInfo> Parameters { get; }
public override IReadOnlyCollection<ComponentCommandParameterInfo> Parameters { get; }

/// <inheritdoc/>
public override bool SupportsWildCards => true;
@@ -42,80 +41,46 @@ namespace Discord.Interactions
if (context.Interaction is not IComponentInteraction componentInteraction)
return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Message Component Interaction");

var args = new List<string>();

if (additionalArgs is not null)
args.AddRange(additionalArgs);

if (componentInteraction.Data?.Values is not null)
args.AddRange(componentInteraction.Data.Values);

return await ExecuteAsync(context, Parameters, args, services);
return await ExecuteAsync(context, Parameters, additionalArgs, componentInteraction.Data, services);
}

/// <inheritdoc/>
public async Task<IResult> ExecuteAsync(IInteractionContext context, IEnumerable<CommandParameterInfo> paramList, IEnumerable<string> values,
public async Task<IResult> ExecuteAsync(IInteractionContext context, IEnumerable<CommandParameterInfo> paramList, IEnumerable<string> wildcardCaptures, IComponentInteractionData data,
IServiceProvider services)
{
var paramCount = paramList.Count();
var captureCount = wildcardCaptures?.Count() ?? 0;

if (context.Interaction is not IComponentInteraction messageComponent)
return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Component Command Interaction");

try
{
var strCount = Parameters.Count(x => x.ParameterType == typeof(string));
var args = new object[paramCount];

for (var i = 0; i < paramCount; i++)
{
var parameter = Parameters.ElementAt(i);
var isCapture = i < captureCount;

if (strCount > values?.Count())
return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command was invoked with too few parameters");
if (isCapture ^ parameter.IsRouteSegmentParameter)
return await InvokeEventAndReturn(context, ExecuteResult.FromError(InteractionCommandError.BadArgs, "Argument type and parameter type didn't match (Wild Card capture/Component value)")).ConfigureAwait(false);

var componentValues = messageComponent.Data?.Values;
var readResult = isCapture ? await parameter.TypeReader.ReadAsync(context, wildcardCaptures.ElementAt(i), services).ConfigureAwait(false) :
await parameter.TypeConverter.ReadAsync(context, data, services).ConfigureAwait(false);

var args = new object[Parameters.Count];
if (!readResult.IsSuccess)
return await InvokeEventAndReturn(context, readResult).ConfigureAwait(false);

if (componentValues is not null)
{
if (Parameters.Last().ParameterType == typeof(string[]))
args[args.Length - 1] = componentValues.ToArray();
else
return ExecuteResult.FromError(InteractionCommandError.BadArgs, $"Select Menu Interaction handlers must accept a {typeof(string[]).FullName} as its last parameter");
args[i] = readResult.Value;
}

for (var i = 0; i < strCount; i++)
args[i] = values.ElementAt(i);

return await RunAsync(context, args, services).ConfigureAwait(false);
}
catch (Exception ex)
{
return ExecuteResult.FromError(ex);
}
}

private static object[] GenerateArgs(IEnumerable<CommandParameterInfo> paramList, IEnumerable<string> argList)
{
var result = new object[paramList.Count()];

for (var i = 0; i < paramList.Count(); i++)
{
var parameter = paramList.ElementAt(i);

if (argList?.ElementAt(i) == null)
{
if (!parameter.IsRequired)
result[i] = parameter.DefaultValue;
else
throw new InvalidOperationException($"Component Interaction handler is executed with too few args.");
}
else if (parameter.IsParameterArray)
{
string[] paramArray = new string[argList.Count() - i];
argList.ToArray().CopyTo(paramArray, i);
result[i] = paramArray;
}
else
result[i] = argList?.ElementAt(i);
return await InvokeEventAndReturn(context, ExecuteResult.FromError(ex)).ConfigureAwait(false);
}

return result;
}

protected override Task InvokeModuleEvent(IInteractionContext context, IResult result)


+ 27
- 9
src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.Tracing;
using System.Linq;
using System.Threading.Tasks;
namespace Discord.Interactions
@@ -47,21 +48,38 @@ namespace Discord.Interactions

try
{
var args = new List<object>();
var args = new object[Parameters.Count];
var captureCount = additionalArgs.Length;

if (additionalArgs is not null)
args.AddRange(additionalArgs);
for(var i = 0; i < Parameters.Count; i++)
{
var parameter = Parameters.ElementAt(i);

var modal = Modal.CreateModal(modalInteraction, Module.CommandService._exitOnMissingModalField);
args.Add(modal);
if(i < captureCount)
{
var readResult = await parameter.TypeReader.ReadAsync(context, additionalArgs[i], services).ConfigureAwait(false);
if (!readResult.IsSuccess)
return await InvokeEventAndReturn(context, readResult).ConfigureAwait(false);

return await RunAsync(context, args.ToArray(), services);
args[i] = readResult.Value;
}
else
{
var modalResult = await Modal.CreateModalAsync(context, services, Module.CommandService._exitOnMissingModalField).ConfigureAwait(false);
if (!modalResult.IsSuccess)
return await InvokeEventAndReturn(context, modalResult).ConfigureAwait(false);

if (modalResult is not ParseResult parseResult)
return await InvokeEventAndReturn(context, ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command parameter parsing failed for an unknown reason."));

args[i] = parseResult.Value;
}
}
return await RunAsync(context, args, services);
}
catch (Exception ex)
{
var result = ExecuteResult.FromError(ex);
await InvokeModuleEvent(context, result).ConfigureAwait(false);
return result;
return await InvokeEventAndReturn(context, ExecuteResult.FromError(ex)).ConfigureAwait(false);
}
}



+ 76
- 33
src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs View File

@@ -13,6 +13,8 @@ namespace Discord.Interactions
/// </summary>
public class SlashCommandInfo : CommandInfo<SlashCommandParameterInfo>, IApplicationCommandInfo
{
internal IReadOnlyDictionary<string, SlashCommandParameterInfo> _flattenedParameterDictionary { get; }

/// <summary>
/// Gets the command description that will be displayed on Discord.
/// </summary>
@@ -30,11 +32,23 @@ namespace Discord.Interactions
/// <inheritdoc/>
public override bool SupportsWildCards => false;

/// <summary>
/// Gets the flattened collection of command parameters and complex parameter fields.
/// </summary>
public IReadOnlyCollection<SlashCommandParameterInfo> FlattenedParameters { get; }

internal SlashCommandInfo (Builders.SlashCommandBuilder builder, ModuleInfo module, InteractionService commandService) : base(builder, module, commandService)
{
Description = builder.Description;
DefaultPermission = builder.DefaultPermission;
Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray();
FlattenedParameters = FlattenParameters(Parameters).ToImmutableArray();

for (var i = 0; i < FlattenedParameters.Count - 1; i++)
if (!FlattenedParameters.ElementAt(i).IsRequired && FlattenedParameters.ElementAt(i + 1).IsRequired)
throw new InvalidOperationException("Optional parameters must appear after all required parameters, ComplexParameters with optional parameters must be located at the end.");

_flattenedParameterDictionary = FlattenedParameters?.ToDictionary(x => x.Name, x => x).ToImmutableDictionary();
}

/// <inheritdoc/>
@@ -56,46 +70,65 @@ namespace Discord.Interactions
{
try
{
if (paramList?.Count() < argList?.Count())
return ExecuteResult.FromError(InteractionCommandError.BadArgs ,"Command was invoked with too many parameters");
var slashCommandParameterInfos = paramList.ToList();
var args = new object[slashCommandParameterInfos.Count];

var args = new object[paramList.Count()];

for (var i = 0; i < paramList.Count(); i++)
for (var i = 0; i < slashCommandParameterInfos.Count; i++)
{
var parameter = paramList.ElementAt(i);

var arg = argList?.Find(x => string.Equals(x.Name, parameter.Name, StringComparison.OrdinalIgnoreCase));

if (arg == default)
{
if (parameter.IsRequired)
return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command was invoked with too few parameters");
else
args[i] = parameter.DefaultValue;
}
else
{
var typeConverter = parameter.TypeConverter;

var readResult = await typeConverter.ReadAsync(context, arg, services).ConfigureAwait(false);

if (!readResult.IsSuccess)
{
await InvokeModuleEvent(context, readResult).ConfigureAwait(false);
return readResult;
}

args[i] = readResult.Value;
}
}
var parameter = slashCommandParameterInfos[i];
var result = await ParseArgument(parameter, context, argList, services).ConfigureAwait(false);

if (!result.IsSuccess)
return await InvokeEventAndReturn(context, result).ConfigureAwait(false);

if (result is not ParseResult parseResult)
return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command parameter parsing failed for an unknown reason.");

args[i] = parseResult.Value;
}
return await RunAsync(context, args, services).ConfigureAwait(false);
}
catch (Exception ex)
catch(Exception ex)
{
return await InvokeEventAndReturn(context, ExecuteResult.FromError(ex)).ConfigureAwait(false);
}
}

private async Task<IResult> ParseArgument(SlashCommandParameterInfo parameterInfo, IInteractionContext context, List<IApplicationCommandInteractionDataOption> argList,
IServiceProvider services)
{
if (parameterInfo.IsComplexParameter)
{
return ExecuteResult.FromError(ex);
var ctorArgs = new object[parameterInfo.ComplexParameterFields.Count];

for (var i = 0; i < ctorArgs.Length; i++)
{
var result = await ParseArgument(parameterInfo.ComplexParameterFields.ElementAt(i), context, argList, services).ConfigureAwait(false);

if (!result.IsSuccess)
return result;

if (result is not ParseResult parseResult)
return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Complex command parsing failed for an unknown reason.");

ctorArgs[i] = parseResult.Value;
}

return ParseResult.FromSuccess(parameterInfo._complexParameterInitializer(ctorArgs));
}

var arg = argList?.Find(x => string.Equals(x.Name, parameterInfo.Name, StringComparison.OrdinalIgnoreCase));

if (arg == default)
return parameterInfo.IsRequired ? ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command was invoked with too few parameters") :
ParseResult.FromSuccess(parameterInfo.DefaultValue);

var typeConverter = parameterInfo.TypeConverter;
var readResult = await typeConverter.ReadAsync(context, arg, services).ConfigureAwait(false);
if (!readResult.IsSuccess)
return readResult;

return ParseResult.FromSuccess(readResult.Value);
}

protected override Task InvokeModuleEvent (IInteractionContext context, IResult result)
@@ -108,5 +141,15 @@ namespace Discord.Interactions
else
return $"Slash Command: \"{base.ToString()}\" for {context.User} in {context.Channel}";
}

private static IEnumerable<SlashCommandParameterInfo> FlattenParameters(IEnumerable<SlashCommandParameterInfo> parameters)
{
foreach (var parameter in parameters)
if (!parameter.IsComplexParameter)
yield return parameter;
else
foreach(var complexParameterField in parameter.ComplexParameterFields)
yield return complexParameterField;
}
}
}

+ 6
- 0
src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs View File

@@ -39,6 +39,11 @@ namespace Discord.Interactions
/// </summary>
public Type Type { get; }

/// <summary>
/// Gets the <see cref="ComponentTypeConverter"/> assigned to this component.
/// </summary>
public ComponentTypeConverter TypeConverter { get; }

/// <summary>
/// Gets the default value of this component.
/// </summary>
@@ -57,6 +62,7 @@ namespace Discord.Interactions
IsRequired = builder.IsRequired;
ComponentType = builder.ComponentType;
Type = builder.Type;
TypeConverter = builder.TypeConverter;
DefaultValue = builder.DefaultValue;
Attributes = builder.Attributes.ToImmutableArray();
}


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

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;

namespace Discord.Interactions
{
@@ -19,6 +20,7 @@ namespace Discord.Interactions
/// </summary>
public class ModalInfo
{
internal readonly InteractionService _interactionService;
internal readonly ModalInitializer _initializer;

/// <summary>
@@ -53,16 +55,18 @@ namespace Discord.Interactions

TextComponents = Components.OfType<TextInputComponentInfo>().ToImmutableArray();

_interactionService = builder._interactionService;
_initializer = builder.ModalInitializer;
}

/// <summary>
/// Creates an <see cref="IModal"/> and fills it with provided message components.
/// </summary>
/// <param name="components"><see cref="IModalInteraction"/> that will be injected into the modal.</param>
/// <param name="modalInteraction"><see cref="IModalInteraction"/> that will be injected into the modal.</param>
/// <returns>
/// A <see cref="IModal"/> filled with the provided components.
/// </returns>
[Obsolete("This method is no longer supported with the introduction of Component TypeConverters, please use the CreateModalAsync method.")]
public IModal CreateModal(IModalInteraction modalInteraction, bool throwOnMissingField = false)
{
var args = new object[Components.Count];
@@ -86,5 +90,50 @@ namespace Discord.Interactions

return _initializer(args);
}

/// <summary>
/// Creates an <see cref="IModal"/> and fills it with provided message components.
/// </summary>
/// <param name="context">Context of the <see cref="IModalInteraction"/> that will be injected into the modal.</param>
/// <param name="services">Services to be passed onto the <see cref="ComponentTypeConverter"/>s of the modal fiels.</param>
/// <param name="throwOnMissingField">Wheter or not this method should exit on encountering a missing modal field.</param>
/// <returns>
/// A <see cref="TypeConverterResult"/> if a type conversion has failed, else a <see cref="ParseResult"/>.
/// </returns>
public async Task<IResult> CreateModalAsync(IInteractionContext context, IServiceProvider services = null, bool throwOnMissingField = false)
{
if (context.Interaction is not IModalInteraction modalInteraction)
return ParseResult.FromError(InteractionCommandError.Unsuccessful, "Provided context doesn't belong to a Modal Interaction.");

services ??= EmptyServiceProvider.Instance;

var args = new object[Components.Count];
var components = modalInteraction.Data.Components.ToList();

for (var i = 0; i < Components.Count; i++)
{
var input = Components.ElementAt(i);
var component = components.Find(x => x.CustomId == input.CustomId);

if (component is null)
{
if (!throwOnMissingField)
args[i] = input.DefaultValue;
else
return ParseResult.FromError(InteractionCommandError.BadArgs, $"Modal interaction is missing the required field: {input.CustomId}");
}
else
{
var readResult = await input.TypeConverter.ReadAsync(context, component, services).ConfigureAwait(false);

if (!readResult.IsSuccess)
return readResult;

args[i] = readResult.Value;
}
}

return ParseResult.FromSuccess(_initializer(args));
}
}
}

+ 34
- 0
src/Discord.Net.Interactions/Info/Parameters/ComponentCommandParameterInfo.cs View File

@@ -0,0 +1,34 @@
using Discord.Interactions.Builders;

namespace Discord.Interactions
{
/// <summary>
/// Represents the parameter info class for <see cref="ComponentCommandInfo"/> commands.
/// </summary>
public class ComponentCommandParameterInfo : CommandParameterInfo
{
/// <summary>
/// Gets the <see cref="ComponentTypeConverter"/> that will be used to convert a message component value into
/// <see cref="CommandParameterInfo.ParameterType"/>, if <see cref="IsRouteSegmentParameter"/> is false.
/// </summary>
public ComponentTypeConverter TypeConverter { get; }

/// <summary>
/// Gets the <see cref="TypeReader"/> that will be used to convert a CustomId segment value into
/// <see cref="CommandParameterInfo.ParameterType"/>, if <see cref="IsRouteSegmentParameter"/> is <see langword="true"/>.
/// </summary>
public TypeReader TypeReader { get; }

/// <summary>
/// Gets whether this parameter is a CustomId segment or a component value parameter.
/// </summary>
public bool IsRouteSegmentParameter { get; }

internal ComponentCommandParameterInfo(ComponentCommandParameterBuilder builder, ICommandInfo command) : base(builder, command)
{
TypeConverter = builder.TypeConverter;
TypeReader = builder.TypeReader;
IsRouteSegmentParameter = builder.IsRouteSegmentParameter;
}
}
}

+ 8
- 1
src/Discord.Net.Interactions/Info/Parameters/ModalCommandParameterInfo.cs View File

@@ -15,7 +15,12 @@ namespace Discord.Interactions
/// <summary>
/// Gets whether this parameter is an <see cref="IModal"/>
/// </summary>
public bool IsModalParameter => Modal is not null;
public bool IsModalParameter { get; }

/// <summary>
/// Gets the <see cref="TypeReader"/> assigned to this parameter, if <see cref="IsModalParameter"/> is <see langword="true"/>.
/// </summary>
public TypeReader TypeReader { get; }

/// <inheritdoc/>
public new ModalCommandInfo Command => base.Command as ModalCommandInfo;
@@ -23,6 +28,8 @@ namespace Discord.Interactions
internal ModalCommandParameterInfo(ModalCommandParameterBuilder builder, ICommandInfo command) : base(builder, command)
{
Modal = builder.Modal;
IsModalParameter = builder.IsModalParameter;
TypeReader = builder.TypeReader;
}
}
}

+ 28
- 2
src/Discord.Net.Interactions/Info/Parameters/SlashCommandParameterInfo.cs View File

@@ -1,13 +1,25 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;

namespace Discord.Interactions
{
/// <summary>
/// Represents a cached argument constructor delegate.
/// </summary>
/// <param name="args">Method arguments array.</param>
/// <returns>
/// Returns the constructed object.
/// </returns>
public delegate object ComplexParameterInitializer(object[] args);

/// <summary>
/// Represents the parameter info class for <see cref="SlashCommandInfo"/> commands.
/// </summary>
public class SlashCommandParameterInfo : CommandParameterInfo
{
internal readonly ComplexParameterInitializer _complexParameterInitializer;

/// <inheritdoc/>
public new SlashCommandInfo Command => base.Command as SlashCommandInfo;

@@ -43,9 +55,14 @@ namespace Discord.Interactions
public bool IsAutocomplete { get; }

/// <summary>
/// Gets the Discord option type this parameter represents.
/// Gets whether this type should be treated as a complex parameter.
/// </summary>
public ApplicationCommandOptionType DiscordOptionType => TypeConverter.GetDiscordType();
public bool IsComplexParameter { get; }

/// <summary>
/// Gets the Discord option type this parameter represents. If the parameter is not a complex parameter.
/// </summary>
public ApplicationCommandOptionType? DiscordOptionType => TypeConverter?.GetDiscordType();

/// <summary>
/// Gets the parameter choices of this Slash Application Command parameter.
@@ -57,6 +74,11 @@ namespace Discord.Interactions
/// </summary>
public IReadOnlyCollection<ChannelType> ChannelTypes { get; }

/// <summary>
/// Gets the constructor parameters of this parameter, if <see cref="IsComplexParameter"/> is <see langword="true"/>.
/// </summary>
public IReadOnlyCollection<SlashCommandParameterInfo> ComplexParameterFields { get; }

internal SlashCommandParameterInfo(Builders.SlashCommandParameterBuilder builder, SlashCommandInfo command) : base(builder, command)
{
TypeConverter = builder.TypeConverter;
@@ -64,9 +86,13 @@ namespace Discord.Interactions
Description = builder.Description;
MaxValue = builder.MaxValue;
MinValue = builder.MinValue;
IsComplexParameter = builder.IsComplexParameter;
IsAutocomplete = builder.Autocomplete;
Choices = builder.Choices.ToImmutableArray();
ChannelTypes = builder.ChannelTypes.ToImmutableArray();
ComplexParameterFields = builder.ComplexParameterFields?.Select(x => x.Build(command)).ToImmutableArray();

_complexParameterInitializer = builder.ComplexParameterInitializer;
}
}
}

+ 153
- 63
src/Discord.Net.Interactions/InteractionService.cs View File

@@ -3,6 +3,7 @@ using Discord.Logging;
using Discord.Rest;
using Discord.WebSocket;
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
@@ -66,8 +67,9 @@ namespace Discord.Interactions
private readonly CommandMap<AutocompleteCommandInfo> _autocompleteCommandMap;
private readonly CommandMap<ModalCommandInfo> _modalCommandMap;
private readonly HashSet<ModuleInfo> _moduleDefs;
private readonly ConcurrentDictionary<Type, TypeConverter> _typeConverters;
private readonly ConcurrentDictionary<Type, Type> _genericTypeConverters;
private readonly TypeMap<TypeConverter, IApplicationCommandInteractionDataOption> _typeConverterMap;
private readonly TypeMap<ComponentTypeConverter, IComponentInteractionData> _compTypeConverterMap;
private readonly TypeMap<TypeReader, string> _typeReaderMap;
private readonly ConcurrentDictionary<Type, IAutocompleteHandler> _autocompleteHandlers = new();
private readonly ConcurrentDictionary<Type, ModalInfo> _modalInfos = new();
private readonly SemaphoreSlim _lock;
@@ -179,22 +181,38 @@ namespace Discord.Interactions
_autoServiceScopes = config.AutoServiceScopes;
_restResponseCallback = config.RestResponseCallback;

_genericTypeConverters = new ConcurrentDictionary<Type, Type>
{
[typeof(IChannel)] = typeof(DefaultChannelConverter<>),
[typeof(IRole)] = typeof(DefaultRoleConverter<>),
[typeof(IAttachment)] = typeof(DefaultAttachmentConverter<>),
[typeof(IUser)] = typeof(DefaultUserConverter<>),
[typeof(IMentionable)] = typeof(DefaultMentionableConverter<>),
[typeof(IConvertible)] = typeof(DefaultValueConverter<>),
[typeof(Enum)] = typeof(EnumConverter<>),
[typeof(Nullable<>)] = typeof(NullableConverter<>),
};
_typeConverterMap = new TypeMap<TypeConverter, IApplicationCommandInteractionDataOption>(this, new ConcurrentDictionary<Type, TypeConverter>
{
[typeof(TimeSpan)] = new TimeSpanConverter()
}, new ConcurrentDictionary<Type, Type>
{
[typeof(IChannel)] = typeof(DefaultChannelConverter<>),
[typeof(IRole)] = typeof(DefaultRoleConverter<>),
[typeof(IAttachment)] = typeof(DefaultAttachmentConverter<>),
[typeof(IUser)] = typeof(DefaultUserConverter<>),
[typeof(IMentionable)] = typeof(DefaultMentionableConverter<>),
[typeof(IConvertible)] = typeof(DefaultValueConverter<>),
[typeof(Enum)] = typeof(EnumConverter<>),
[typeof(Nullable<>)] = typeof(NullableConverter<>)
});

_compTypeConverterMap = new TypeMap<ComponentTypeConverter, IComponentInteractionData>(this, new ConcurrentDictionary<Type, ComponentTypeConverter>(),
new ConcurrentDictionary<Type, Type>
{
[typeof(Array)] = typeof(DefaultArrayComponentConverter<>),
[typeof(IConvertible)] = typeof(DefaultValueComponentConverter<>)
});

_typeConverters = new ConcurrentDictionary<Type, TypeConverter>
{
[typeof(TimeSpan)] = new TimeSpanConverter()
};
_typeReaderMap = new TypeMap<TypeReader, string>(this, new ConcurrentDictionary<Type, TypeReader>(),
new ConcurrentDictionary<Type, Type>
{
[typeof(IChannel)] = typeof(DefaultChannelReader<>),
[typeof(IRole)] = typeof(DefaultRoleReader<>),
[typeof(IUser)] = typeof(DefaultUserReader<>),
[typeof(IMessage)] = typeof(DefaultMessageReader<>),
[typeof(IConvertible)] = typeof(DefaultValueReader<>),
[typeof(Enum)] = typeof(EnumReader<>)
});
}

/// <summary>
@@ -293,7 +311,7 @@ namespace Discord.Interactions
public async Task<ModuleInfo> AddModuleAsync (Type type, IServiceProvider services)
{
if (!typeof(IInteractionModuleBase).IsAssignableFrom(type))
throw new ArgumentException("Type parameter must be a type of Slash Module", "T");
throw new ArgumentException("Type parameter must be a type of Slash Module", nameof(type));

services ??= EmptyServiceProvider.Instance;

@@ -326,7 +344,7 @@ namespace Discord.Interactions
}

/// <summary>
/// Register Application Commands from <see cref="ContextCommands"/> and <see cref="SlashCommands"/> to a guild.
/// Register Application Commands from <see cref="ContextCommands"/> and <see cref="SlashCommands"/> to a guild.
/// </summary>
/// <param name="guildId">Id of the target guild.</param>
/// <param name="deleteMissing">If <see langword="false"/>, this operation will not delete the commands that are missing from <see cref="InteractionService"/>.</param>
@@ -422,7 +440,7 @@ namespace Discord.Interactions
}

/// <summary>
/// Register Application Commands from modules provided in <paramref name="modules"/> to a guild.
/// Register Application Commands from modules provided in <paramref name="modules"/> to a guild.
/// </summary>
/// <param name="guild">The target guild.</param>
/// <param name="modules">Modules to be registered to Discord.</param>
@@ -449,7 +467,7 @@ namespace Discord.Interactions
}

/// <summary>
/// Register Application Commands from modules provided in <paramref name="modules"/> as global commands.
/// Register Application Commands from modules provided in <paramref name="modules"/> as global commands.
/// </summary>
/// <param name="modules">Modules to be registered to Discord.</param>
/// <returns>
@@ -750,9 +768,7 @@ namespace Discord.Interactions

if(autocompleteHandlerResult.IsSuccess)
{
var parameter = autocompleteHandlerResult.Command.Parameters.FirstOrDefault(x => string.Equals(x.Name, interaction.Data.Current.Name, StringComparison.Ordinal));

if(parameter?.AutocompleteHandler is not null)
if (autocompleteHandlerResult.Command._flattenedParameterDictionary.TryGetValue(interaction.Data.Current.Name, out var parameter) && parameter?.AutocompleteHandler is not null)
return await parameter.AutocompleteHandler.ExecuteAsync(context, interaction, parameter, services).ConfigureAwait(false);
}
}
@@ -823,26 +839,24 @@ namespace Discord.Interactions
throw new ArgumentException($"No type {nameof(TypeConverter)} is defined for this {type.FullName}", "type");
}

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

/// <summary>
/// Add a concrete type <see cref="TypeConverter"/>.
/// </summary>
/// <typeparam name="T">Primary target <see cref="Type"/> of the <see cref="TypeConverter"/>.</typeparam>
/// <param name="converter">The <see cref="TypeConverter"/> instance.</param>
public void AddTypeConverter<T> (TypeConverter converter) =>
AddTypeConverter(typeof(T), converter);
public void AddTypeConverter<T>(TypeConverter converter) =>
_typeConverterMap.AddConcrete<T>(converter);

/// <summary>
/// Add a concrete type <see cref="TypeConverter"/>.
/// </summary>
/// <param name="type">Primary target <see cref="Type"/> of the <see cref="TypeConverter"/>.</param>
/// <param name="converter">The <see cref="TypeConverter"/> instance.</param>
public void AddTypeConverter (Type type, TypeConverter converter)
{
if (!converter.CanConvertTo(type))
throw new ArgumentException($"This {converter.GetType().FullName} cannot read {type.FullName} and cannot be registered as its {nameof(TypeConverter)}");

_typeConverters[type] = converter;
}
public void AddTypeConverter(Type type, TypeConverter converter) =>
_typeConverterMap.AddConcrete(type, converter);

/// <summary>
/// Add a generic type <see cref="TypeConverter{T}"/>.
@@ -850,30 +864,121 @@ namespace Discord.Interactions
/// <typeparam name="T">Generic Type constraint of the <see cref="Type"/> of the <see cref="TypeConverter{T}"/>.</typeparam>
/// <param name="converterType">Type of the <see cref="TypeConverter{T}"/>.</param>

public void AddGenericTypeConverter<T> (Type converterType) =>
AddGenericTypeConverter(typeof(T), converterType);
public void AddGenericTypeConverter<T>(Type converterType) =>
_typeConverterMap.AddGeneric<T>(converterType);

/// <summary>
/// Add a generic type <see cref="TypeConverter{T}"/>.
/// </summary>
/// <param name="targetType">Generic Type constraint of the <see cref="Type"/> of the <see cref="TypeConverter{T}"/>.</param>
/// <param name="converterType">Type of the <see cref="TypeConverter{T}"/>.</param>
public void AddGenericTypeConverter (Type targetType, Type converterType)
{
if (!converterType.IsGenericTypeDefinition)
throw new ArgumentException($"{converterType.FullName} is not generic.");
public void AddGenericTypeConverter(Type targetType, Type converterType) =>
_typeConverterMap.AddGeneric(targetType, converterType);

internal ComponentTypeConverter GetComponentTypeConverter(Type type, IServiceProvider services = null) =>
_compTypeConverterMap.Get(type, services);

/// <summary>
/// Add a concrete type <see cref="ComponentTypeConverter"/>.
/// </summary>
/// <typeparam name="T">Primary target <see cref="Type"/> of the <see cref="ComponentTypeConverter"/>.</typeparam>
/// <param name="converter">The <see cref="ComponentTypeConverter"/> instance.</param>
public void AddComponentTypeConverter<T>(ComponentTypeConverter converter) =>
AddComponentTypeConverter(typeof(T), converter);

/// <summary>
/// Add a concrete type <see cref="ComponentTypeConverter"/>.
/// </summary>
/// <param name="type">Primary target <see cref="Type"/> of the <see cref="ComponentTypeConverter"/>.</param>
/// <param name="converter">The <see cref="ComponentTypeConverter"/> instance.</param>
public void AddComponentTypeConverter(Type type, ComponentTypeConverter converter) =>
_compTypeConverterMap.AddConcrete(type, converter);

/// <summary>
/// Add a generic type <see cref="ComponentTypeConverter{T}"/>.
/// </summary>
/// <typeparam name="T">Generic Type constraint of the <see cref="Type"/> of the <see cref="ComponentTypeConverter{T}"/>.</typeparam>
/// <param name="converterType">Type of the <see cref="ComponentTypeConverter{T}"/>.</param>
public void AddGenericComponentTypeConverter<T>(Type converterType) =>
AddGenericComponentTypeConverter(typeof(T), converterType);

/// <summary>
/// Add a generic type <see cref="ComponentTypeConverter{T}"/>.
/// </summary>
/// <param name="targetType">Generic Type constraint of the <see cref="Type"/> of the <see cref="ComponentTypeConverter{T}"/>.</param>
/// <param name="converterType">Type of the <see cref="ComponentTypeConverter{T}"/>.</param>
public void AddGenericComponentTypeConverter(Type targetType, Type converterType) =>
_compTypeConverterMap.AddGeneric(targetType, converterType);

internal TypeReader GetTypeReader(Type type, IServiceProvider services = null) =>
_typeReaderMap.Get(type, services);

var genericArguments = converterType.GetGenericArguments();
/// <summary>
/// Add a concrete type <see cref="TypeReader"/>.
/// </summary>
/// <typeparam name="T">Primary target <see cref="Type"/> of the <see cref="TypeReader"/>.</typeparam>
/// <param name="reader">The <see cref="TypeReader"/> instance.</param>
public void AddTypeReader<T>(TypeReader reader) =>
AddTypeReader(typeof(T), reader);

/// <summary>
/// Add a concrete type <see cref="TypeReader"/>.
/// </summary>
/// <param name="type">Primary target <see cref="Type"/> of the <see cref="TypeReader"/>.</param>
/// <param name="reader">The <see cref="TypeReader"/> instance.</param>
public void AddTypeReader(Type type, TypeReader reader) =>
_typeReaderMap.AddConcrete(type, reader);

/// <summary>
/// Add a generic type <see cref="TypeReader{T}"/>.
/// </summary>
/// <typeparam name="T">Generic Type constraint of the <see cref="Type"/> of the <see cref="TypeReader{T}"/>.</typeparam>
/// <param name="readerType">Type of the <see cref="TypeReader{T}"/>.</param>
public void AddGenericTypeReader<T>(Type readerType) =>
AddGenericTypeReader(typeof(T), readerType);

/// <summary>
/// Add a generic type <see cref="TypeReader{T}"/>.
/// </summary>
/// <param name="targetType">Generic Type constraint of the <see cref="Type"/> of the <see cref="TypeReader{T}"/>.</param>
/// <param name="readerType">Type of the <see cref="TypeReader{T}"/>.</param>
public void AddGenericTypeReader(Type targetType, Type readerType) =>
_typeReaderMap.AddGeneric(targetType, readerType);

if (genericArguments.Count() > 1)
throw new InvalidOperationException($"Valid generic {converterType.FullName}s cannot have more than 1 generic type parameter");
/// <summary>
/// Serialize an object using a <see cref="TypeReader"/> into a <see cref="string"/> to be placed in a Component CustomId.
/// </summary>
/// <typeparam name="T">Type of the object to be serialized.</typeparam>
/// <param name="obj">Object to be serialized.</param>
/// <param name="services">Services that will be passed on to the <see cref="TypeReader"/>.</param>
/// <returns>
/// A task representing the conversion process. The task result contains the result of the conversion.
/// </returns>
public Task<string> SerializeValueAsync<T>(T obj, IServiceProvider services) =>
_typeReaderMap.Get(typeof(T), services).SerializeAsync(obj, services);

var constraints = genericArguments.SelectMany(x => x.GetGenericParameterConstraints());
/// <summary>
/// Serialize and format multiple objects into a Custom Id string.
/// </summary>
/// <param name="format">A composite format string.</param>
/// <param name="services">>Services that will be passed on to the <see cref="TypeReader"/>s.</param>
/// <param name="args">Objects to be serialized.</param>
/// <returns>
/// A task representing the conversion process. The task result contains the result of the conversion.
/// </returns>
public async Task<string> GenerateCustomIdStringAsync(string format, IServiceProvider services, params object[] args)
{
var serializedValues = new string[args.Length];

if (!constraints.Any(x => x.IsAssignableFrom(targetType)))
throw new InvalidOperationException($"This generic class does not support type {targetType.FullName}");
for(var i = 0; i < args.Length; i++)
{
var arg = args[i];
var typeReader = _typeReaderMap.Get(arg.GetType(), null);
var result = await typeReader.SerializeAsync(arg, services).ConfigureAwait(false);
serializedValues[i] = result;
}

_genericTypeConverters[targetType] = converterType;
return string.Format(format, serializedValues);
}

/// <summary>
@@ -891,7 +996,7 @@ namespace Discord.Interactions
if (_modalInfos.ContainsKey(type))
throw new InvalidOperationException($"Modal type {type.FullName} already exists.");

return ModalUtils.GetOrAdd(type);
return ModalUtils.GetOrAdd(type, this);
}

internal IAutocompleteHandler GetAutocompleteHandler(Type autocompleteHandlerType, IServiceProvider services = null)
@@ -1037,7 +1142,7 @@ namespace Discord.Interactions
public ModuleInfo GetModuleInfo<TModule> ( ) where TModule : class
{
if (!typeof(IInteractionModuleBase).IsAssignableFrom(typeof(TModule)))
throw new ArgumentException("Type parameter must be a type of Slash Module", "TModule");
throw new ArgumentException("Type parameter must be a type of Slash Module", nameof(TModule));

var module = _typedModuleDefs[typeof(TModule)];

@@ -1053,21 +1158,6 @@ namespace Discord.Interactions
_lock.Dispose();
}

private Type GetMostSpecificTypeConverter (Type type)
{
if (_genericTypeConverters.TryGetValue(type, out var matching))
return matching;

if (type.IsGenericType && _genericTypeConverters.TryGetValue(type.GetGenericTypeDefinition(), out var genericDefinition))
return genericDefinition;

var typeInterfaces = type.GetInterfaces();
var candidates = _genericTypeConverters.Where(x => x.Key.IsAssignableFrom(type))
.OrderByDescending(x => typeInterfaces.Count(y => y.IsAssignableFrom(x.Key)));

return candidates.First().Value;
}

private void EnsureClientReady()
{
if (RestClient?.CurrentUser is null || RestClient?.CurrentUser?.Id == 0)


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

@@ -31,7 +31,7 @@ namespace Discord.Interactions
/// <summary>
/// Gets or sets the string expression that will be treated as a wild card.
/// </summary>
public string WildCardExpression { get; set; }
public string WildCardExpression { get; set; } = "*";

/// <summary>
/// Gets or sets the option to use compiled lambda expressions to create module instances and execute commands. This method improves performance at the cost of memory.


+ 92
- 0
src/Discord.Net.Interactions/Map/TypeMap.cs View File

@@ -0,0 +1,92 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace Discord.Interactions
{
internal class TypeMap<TConverter, TData>
where TConverter : class, ITypeConverter<TData>
{
private readonly ConcurrentDictionary<Type, TConverter> _concretes;
private readonly ConcurrentDictionary<Type, Type> _generics;
private readonly InteractionService _interactionService;

public TypeMap(InteractionService interactionService, IDictionary<Type, TConverter> concretes = null, IDictionary<Type, Type> generics = null)
{
_interactionService = interactionService;
_concretes = concretes is not null ? new(concretes) : new();
_generics = generics is not null ? new(generics) : new();
}

internal TConverter Get(Type type, IServiceProvider services = null)
{
if (_concretes.TryGetValue(type, out var specific))
return specific;

if (_generics.Any(x => x.Key.IsAssignableFrom(type)
|| x.Key.IsGenericTypeDefinition && type.IsGenericType && x.Key.GetGenericTypeDefinition() == type.GetGenericTypeDefinition()))
{
services ??= EmptyServiceProvider.Instance;

var converterType = GetMostSpecific(type);
var converter = ReflectionUtils<TConverter>.CreateObject(converterType.MakeGenericType(type).GetTypeInfo(), _interactionService, services);
_concretes[type] = converter;
return converter;
}

if (_concretes.Any(x => x.Value.CanConvertTo(type)))
return _concretes.First(x => x.Value.CanConvertTo(type)).Value;

throw new ArgumentException($"No type {typeof(TConverter).Name} is defined for this {type.FullName}", nameof(type));
}

public void AddConcrete<TTarget>(TConverter converter) =>
AddConcrete(typeof(TTarget), converter);

public void AddConcrete(Type type, TConverter converter)
{
if (!converter.CanConvertTo(type))
throw new ArgumentException($"This {converter.GetType().FullName} cannot read {type.FullName} and cannot be registered as its {nameof(TypeConverter)}");

_concretes[type] = converter;
}

public void AddGeneric<TTarget>(Type converterType) =>
AddGeneric(typeof(TTarget), converterType);

public void AddGeneric(Type targetType, Type converterType)
{
if (!converterType.IsGenericTypeDefinition)
throw new ArgumentException($"{converterType.FullName} is not generic.");

var genericArguments = converterType.GetGenericArguments();

if (genericArguments.Length > 1)
throw new InvalidOperationException($"Valid generic {converterType.FullName}s cannot have more than 1 generic type parameter");

var constraints = genericArguments.SelectMany(x => x.GetGenericParameterConstraints());

if (!constraints.Any(x => x.IsAssignableFrom(targetType)))
throw new InvalidOperationException($"This generic class does not support type {targetType.FullName}");

_generics[targetType] = converterType;
}

private Type GetMostSpecific(Type type)
{
if (_generics.TryGetValue(type, out var matching))
return matching;

if (type.IsGenericType && _generics.TryGetValue(type.GetGenericTypeDefinition(), out var genericDefinition))
return genericDefinition;

var typeInterfaces = type.GetInterfaces();
var candidates = _generics.Where(x => x.Key.IsAssignableFrom(type))
.OrderByDescending(x => typeInterfaces.Count(y => y.IsAssignableFrom(x.Key)));

return candidates.First().Value;
}
}
}

+ 36
- 0
src/Discord.Net.Interactions/Results/ParseResult.cs View File

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

namespace Discord.Interactions
{
internal struct ParseResult : IResult
{
public object Value { get; }

public InteractionCommandError? Error { get; }

public string ErrorReason { get; }

public bool IsSuccess => !Error.HasValue;

private ParseResult(object value, InteractionCommandError? error, string reason)
{
Value = value;
Error = error;
ErrorReason = reason;
}

public static ParseResult FromSuccess(object value) =>
new ParseResult(value, null, null);

public static ParseResult FromError(Exception exception) =>
new ParseResult(null, InteractionCommandError.Exception, exception.Message);

public static ParseResult FromError(InteractionCommandError error, string reason) =>
new ParseResult(null, error, reason);

public static ParseResult FromError(IResult result) =>
new ParseResult(null, result.Error, result.ErrorReason);

public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}";
}
}

+ 39
- 0
src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/ComponentTypeConverter.cs View File

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

namespace Discord.Interactions
{
/// <summary>
/// Base class for creating Component TypeConverters. <see cref="InteractionService"/> uses TypeConverters to interface with Slash Command parameters.
/// </summary>
public abstract class ComponentTypeConverter : ITypeConverter<IComponentInteractionData>
{
/// <summary>
/// Will be used to search for alternative TypeConverters whenever the Command Service encounters an unknown parameter type.
/// </summary>
/// <param name="type">An object type.</param>
/// <returns>
/// The boolean result.
/// </returns>
public abstract bool CanConvertTo(Type type);

/// <summary>
/// Will be used to read the incoming payload before executing the method body.
/// </summary>
/// <param name="context">Command exexution context.</param>
/// <param name="option">Recieved option payload.</param>
/// <param name="services">Service provider that will be used to initialize the command module.</param>
/// <returns>
/// The result of the read process.
/// </returns>
public abstract Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services);
}

/// <inheritdoc/>
public abstract class ComponentTypeConverter<T> : ComponentTypeConverter
{
/// <inheritdoc/>
public sealed override bool CanConvertTo(Type type) =>
typeof(T).IsAssignableFrom(type);
}
}

+ 45
- 0
src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultArrayComponentConverter.cs View File

@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Discord.Interactions
{
internal sealed class DefaultArrayComponentConverter<T> : ComponentTypeConverter<T>
{
private readonly TypeReader _typeReader;
private readonly Type _underlyingType;

public DefaultArrayComponentConverter(InteractionService interactionService)
{
var type = typeof(T);

if (!type.IsArray)
throw new InvalidOperationException($"{nameof(DefaultArrayComponentConverter<T>)} cannot be used to convert a non-array type.");

_underlyingType = typeof(T).GetElementType();
_typeReader = interactionService.GetTypeReader(_underlyingType);
}

public override async Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
{
var results = new List<TypeConverterResult>();

foreach (var value in option.Values)
{
var result = await _typeReader.ReadAsync(context, value, services).ConfigureAwait(false);

if (!result.IsSuccess)
return result;

results.Add(result);
}

var destination = Array.CreateInstance(_underlyingType, results.Count);

for (var i = 0; i < results.Count; i++)
destination.SetValue(results[i].Value, i);

return TypeConverterResult.FromSuccess(destination);
}
}
}

+ 26
- 0
src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultValueComponentConverter.cs View File

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

namespace Discord.Interactions
{
internal sealed class DefaultValueComponentConverter<T> : ComponentTypeConverter<T>
where T : IConvertible
{
public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
{
try
{
return option.Type switch
{
ComponentType.SelectMenu => Task.FromResult(TypeConverterResult.FromSuccess(Convert.ChangeType(string.Join(",", option.Values), typeof(T)))),
ComponentType.TextInput => Task.FromResult(TypeConverterResult.FromSuccess(Convert.ChangeType(option.Value, typeof(T)))),
_ => Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"{option.Type} doesn't have a convertible value."))
};
}
catch (InvalidCastException castEx)
{
return Task.FromResult(TypeConverterResult.FromError(castEx));
}
}
}
}

src/Discord.Net.Interactions/TypeConverters/DefaultEntityTypeConverter.cs → src/Discord.Net.Interactions/TypeConverters/SlashCommands/DefaultEntityTypeConverter.cs View File


src/Discord.Net.Interactions/TypeConverters/DefaultValueConverter.cs → src/Discord.Net.Interactions/TypeConverters/SlashCommands/DefaultValueConverter.cs View File


src/Discord.Net.Interactions/TypeConverters/EnumConverter.cs → src/Discord.Net.Interactions/TypeConverters/SlashCommands/EnumConverter.cs View File

@@ -2,6 +2,7 @@ using Discord.WebSocket;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;

namespace Discord.Interactions
@@ -27,12 +28,14 @@ namespace Discord.Interactions
var choices = new List<ApplicationCommandOptionChoiceProperties>();

foreach (var member in members)
{
var displayValue = member.GetCustomAttribute<ChoiceDisplayAttribute>()?.Name ?? member.Name;
choices.Add(new ApplicationCommandOptionChoiceProperties
{
Name = member.Name,
Name = displayValue,
Value = member.Name
});
}
properties.Choices = choices;
}
}

src/Discord.Net.Interactions/TypeConverters/NullableConverter.cs → src/Discord.Net.Interactions/TypeConverters/SlashCommands/NullableConverter.cs View File


src/Discord.Net.Interactions/TypeConverters/TimeSpanConverter.cs → src/Discord.Net.Interactions/TypeConverters/SlashCommands/TimeSpanConverter.cs View File


src/Discord.Net.Interactions/TypeConverters/TypeConverter.cs → src/Discord.Net.Interactions/TypeConverters/SlashCommands/TypeConverter.cs View File

@@ -6,7 +6,7 @@ namespace Discord.Interactions
/// <summary>
/// Base class for creating TypeConverters. <see cref="InteractionService"/> uses TypeConverters to interface with Slash Command parameters.
/// </summary>
public abstract class TypeConverter
public abstract class TypeConverter : ITypeConverter<IApplicationCommandInteractionDataOption>
{
/// <summary>
/// Will be used to search for alternative TypeConverters whenever the Command Service encounters an unknown parameter type.

+ 48
- 0
src/Discord.Net.Interactions/TypeReaders/DefaultSnowflakeReader.cs View File

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

namespace Discord.Interactions
{
internal abstract class DefaultSnowflakeReader<T> : TypeReader<T>
where T : class, ISnowflakeEntity
{
protected abstract Task<T> GetEntity(ulong id, IInteractionContext ctx);

public override async Task<TypeConverterResult> ReadAsync(IInteractionContext context, string option, IServiceProvider services)
{
if (!ulong.TryParse(option, out var snowflake))
return TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"{option} isn't a valid snowflake thus cannot be converted into {typeof(T).Name}");

var result = await GetEntity(snowflake, context).ConfigureAwait(false);

return result is not null ?
TypeConverterResult.FromSuccess(result) : TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"{option} must be a valid {typeof(T).Name} snowflake to be parsed.");
}

public override Task<string> SerializeAsync(object obj, IServiceProvider services) => Task.FromResult((obj as ISnowflakeEntity)?.Id.ToString());
}

internal sealed class DefaultUserReader<T> : DefaultSnowflakeReader<T>
where T : class, IUser
{
protected override async Task<T> GetEntity(ulong id, IInteractionContext ctx) => await ctx.Client.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T;
}

internal sealed class DefaultChannelReader<T> : DefaultSnowflakeReader<T>
where T : class, IChannel
{
protected override async Task<T> GetEntity(ulong id, IInteractionContext ctx) => await ctx.Client.GetChannelAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T;
}

internal sealed class DefaultRoleReader<T> : DefaultSnowflakeReader<T>
where T : class, IRole
{
protected override Task<T> GetEntity(ulong id, IInteractionContext ctx) => Task.FromResult(ctx.Guild?.GetRole(id) as T);
}

internal sealed class DefaultMessageReader<T> : DefaultSnowflakeReader<T>
where T : class, IMessage
{
protected override async Task<T> GetEntity(ulong id, IInteractionContext ctx) => await ctx.Channel.GetMessageAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T;
}
}

+ 22
- 0
src/Discord.Net.Interactions/TypeReaders/DefaultValueReader.cs View File

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

namespace Discord.Interactions
{
internal sealed class DefaultValueReader<T> : TypeReader<T>
where T : IConvertible
{
public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, string option, IServiceProvider services)
{
try
{
var converted = Convert.ChangeType(option, typeof(T));
return Task.FromResult(TypeConverterResult.FromSuccess(converted));
}
catch (InvalidCastException castEx)
{
return Task.FromResult(TypeConverterResult.FromError(castEx));
}
}
}
}

+ 25
- 0
src/Discord.Net.Interactions/TypeReaders/EnumReader.cs View File

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

namespace Discord.Interactions
{
internal sealed class EnumReader<T> : TypeReader<T>
where T : struct, Enum
{
public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, string option, IServiceProvider services)
{
return Task.FromResult(Enum.TryParse<T>(option, out var result) ?
TypeConverterResult.FromSuccess(result) : TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"Value {option} cannot be converted to {nameof(T)}"));
}

public override Task<string> SerializeAsync(object obj, IServiceProvider services)
{
var name = Enum.GetName(typeof(T), obj);

if (name is null)
throw new ArgumentException($"Enum name cannot be parsed from {obj}");

return Task.FromResult(name);
}
}
}

+ 46
- 0
src/Discord.Net.Interactions/TypeReaders/TypeReader.cs View File

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

namespace Discord.Interactions
{
/// <summary>
/// Base class for creating TypeConverters. <see cref="InteractionService"/> uses TypeConverters to interface with Slash Command parameters.
/// </summary>
public abstract class TypeReader : ITypeConverter<string>
{
/// <summary>
/// Will be used to search for alternative TypeReaders whenever the Command Service encounters an unknown parameter type.
/// </summary>
/// <param name="type">An object type.</param>
/// <returns>
/// The boolean result.
/// </returns>
public abstract bool CanConvertTo(Type type);

/// <summary>
/// Will be used to read the incoming payload before executing the method body.
/// </summary>
/// <param name="context">Command execution context.</param>
/// <param name="option">Received option payload.</param>
/// <param name="services">Service provider that will be used to initialize the command module.</param>
/// <returns>The result of the read process.</returns>
public abstract Task<TypeConverterResult> ReadAsync(IInteractionContext context, string option, IServiceProvider services);

/// <summary>
/// Will be used to serialize objects into strings.
/// </summary>
/// <param name="obj">Object to be serialized.</param>
/// <returns>
/// A task representing the conversion process. The result of the task contains the conversion result.
/// </returns>
public virtual Task<string> SerializeAsync(object obj, IServiceProvider services) => Task.FromResult(obj.ToString());
}

/// <inheritdoc/>
public abstract class TypeReader<T> : TypeReader
{
/// <inheritdoc/>
public sealed override bool CanConvertTo(Type type) =>
typeof(T).IsAssignableFrom(type);
}
}

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

@@ -13,7 +13,7 @@ namespace Discord.Interactions
{
Name = parameterInfo.Name,
Description = parameterInfo.Description,
Type = parameterInfo.DiscordOptionType,
Type = parameterInfo.DiscordOptionType.Value,
IsRequired = parameterInfo.IsRequired,
Choices = parameterInfo.Choices?.Select(x => new ApplicationCommandOptionChoiceProperties
{
@@ -46,7 +46,7 @@ namespace Discord.Interactions
if (commandInfo.Parameters.Count > SlashCommandBuilder.MaxOptionsCount)
throw new InvalidOperationException($"Slash Commands cannot have more than {SlashCommandBuilder.MaxOptionsCount} command parameters");

props.Options = commandInfo.Parameters.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional<List<ApplicationCommandOptionProperties>>.Unspecified;
props.Options = commandInfo.FlattenedParameters.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional<List<ApplicationCommandOptionProperties>>.Unspecified;

return props;
}
@@ -58,7 +58,7 @@ namespace Discord.Interactions
Description = commandInfo.Description,
Type = ApplicationCommandOptionType.SubCommand,
IsRequired = false,
Options = commandInfo.Parameters?.Select(x => x.ToApplicationCommandOptionProps())?.ToList()
Options = commandInfo.FlattenedParameters?.Select(x => x.ToApplicationCommandOptionProps())?.ToList()
};

public static ApplicationCommandProperties ToApplicationCommandProps(this ContextCommandInfo commandInfo)


+ 5
- 5
src/Discord.Net.Interactions/Utilities/ModalUtils.cs View File

@@ -7,20 +7,20 @@ namespace Discord.Interactions
{
internal static class ModalUtils
{
private static ConcurrentDictionary<Type, ModalInfo> _modalInfos = new();
private static readonly ConcurrentDictionary<Type, ModalInfo> _modalInfos = new();

public static IReadOnlyCollection<ModalInfo> Modals => _modalInfos.Values.ToReadOnlyCollection();

public static ModalInfo GetOrAdd(Type type)
public static ModalInfo GetOrAdd(Type type, InteractionService interactionService)
{
if (!typeof(IModal).IsAssignableFrom(type))
throw new ArgumentException($"Must be an implementation of {nameof(IModal)}", nameof(type));

return _modalInfos.GetOrAdd(type, ModuleClassBuilder.BuildModalInfo(type));
return _modalInfos.GetOrAdd(type, ModuleClassBuilder.BuildModalInfo(type, interactionService));
}

public static ModalInfo GetOrAdd<T>() where T : class, IModal
=> GetOrAdd(typeof(T));
public static ModalInfo GetOrAdd<T>(InteractionService interactionService) where T : class, IModal
=> GetOrAdd(typeof(T), interactionService);

public static bool TryGet(Type type, out ModalInfo modalInfo)
{


+ 2
- 0
src/Discord.Net.Rest/API/Common/GuildScheduledEvent.cs View File

@@ -39,5 +39,7 @@ namespace Discord.API
public Optional<User> Creator { get; set; }
[JsonProperty("user_count")]
public Optional<int> UserCount { get; set; }
[JsonProperty("image")]
public string Image { get; set; }
}
}

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

@@ -16,5 +16,11 @@ namespace Discord.API

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

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

[JsonProperty("create_timestamp")]
public Optional<DateTimeOffset> CreatedAt { get; set; }
}
}

+ 2
- 0
src/Discord.Net.Rest/API/Common/VoiceState.cs View File

@@ -28,6 +28,8 @@ namespace Discord.API
public bool Suppress { get; set; }
[JsonProperty("self_stream")]
public bool SelfStream { get; set; }
[JsonProperty("self_video")]
public bool SelfVideo { get; set; }
[JsonProperty("request_to_speak_timestamp")]
public Optional<DateTimeOffset?> RequestToSpeakTimestamp { get; set; }
}


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

Loading…
Cancel
Save