Browse Source

Merge branch 'dev' into patch-327D243D29

pull/2237/head
Quin Lynch GitHub 3 years ago
parent
commit
2e4fd81c5b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
100 changed files with 2369 additions and 696 deletions
  1. +8
    -0
      .github/ISSUE_TEMPLATE/bugreport.yml
  2. +3
    -0
      .gitmodules
  3. +63
    -0
      CHANGELOG.md
  4. +16
    -1
      Discord.Net.sln
  5. +1
    -1
      Discord.Net.targets
  6. +1
    -0
      azure/deploy.yml
  7. +1
    -1
      docs/docfx.json
  8. +41
    -0
      docs/faq/build_overrides/what-are-they.md
  9. +2
    -0
      docs/faq/toc.yml
  10. +1
    -1
      docs/guides/int_basics/application-commands/slash-commands/choice-slash-command.md
  11. +15
    -0
      docs/guides/int_framework/intro.md
  12. +37
    -0
      docs/guides/int_framework/samples/intro/complexparams.cs
  13. +5
    -3
      docs/guides/int_framework/samples/intro/groupattribute.cs
  14. +1
    -1
      docs/guides/int_framework/samples/intro/modal.cs
  15. +1
    -1
      docs/guides/int_framework/samples/postexecution/error_review.cs
  16. BIN
      docs/guides/other_libs/images/mediatr_output.png
  17. +70
    -0
      docs/guides/other_libs/mediatr.md
  18. +1
    -0
      docs/guides/other_libs/samples/MediatrConfiguringDI.cs
  19. +16
    -0
      docs/guides/other_libs/samples/MediatrCreatingMessageNotification.cs
  20. +46
    -0
      docs/guides/other_libs/samples/MediatrDiscordEventListener.cs
  21. +17
    -0
      docs/guides/other_libs/samples/MediatrMessageReceivedHandler.cs
  22. +4
    -0
      docs/guides/other_libs/samples/MediatrStartListener.cs
  23. +2
    -0
      docs/guides/toc.yml
  24. +1
    -1
      docs/index.md
  25. +278
    -0
      experiment/Discord.Net.BuildOverrides/BuildOverrides.cs
  26. +20
    -0
      experiment/Discord.Net.BuildOverrides/Discord.Net.BuildOverrides.csproj
  27. +34
    -0
      experiment/Discord.Net.BuildOverrides/IOverride.cs
  28. +30
    -0
      experiment/Discord.Net.BuildOverrides/OverrideContext.cs
  29. +1
    -0
      overrides/Discord.Net.BuildOverrides
  30. +0
    -152
      samples/InteractionFramework/CommandHandler.cs
  31. +20
    -0
      samples/InteractionFramework/Enums/ExampleEnum.cs
  32. +0
    -10
      samples/InteractionFramework/ExampleEnum.cs
  33. +81
    -0
      samples/InteractionFramework/InteractionHandler.cs
  34. +0
    -18
      samples/InteractionFramework/Modules/ComponentModule.cs
  35. +49
    -37
      samples/InteractionFramework/Modules/ExampleModule.cs
  36. +0
    -30
      samples/InteractionFramework/Modules/MessageCommandModule.cs
  37. +0
    -51
      samples/InteractionFramework/Modules/SlashCommandModule.cs
  38. +0
    -17
      samples/InteractionFramework/Modules/UserCommandModule.cs
  39. +33
    -42
      samples/InteractionFramework/Program.cs
  40. +16
    -0
      samples/MediatRSample/MediatRSample.sln
  41. +48
    -0
      samples/MediatRSample/MediatRSample/DiscordEventListener.cs
  42. +14
    -0
      samples/MediatRSample/MediatRSample/Handlers/MessageReceivedHandler.cs
  43. +20
    -0
      samples/MediatRSample/MediatRSample/MediatRSample.csproj
  44. +14
    -0
      samples/MediatRSample/MediatRSample/Notifications/MessageReceivedNotification.cs
  45. +13
    -0
      samples/MediatRSample/MediatRSample/Notifications/ReadyNotification.cs
  46. +73
    -0
      samples/MediatRSample/MediatRSample/Program.cs
  47. +1
    -1
      samples/ShardedClient/Modules/InteractionModule.cs
  48. +5
    -2
      samples/ShardedClient/Program.cs
  49. +2
    -2
      samples/ShardedClient/Services/InteractionHandlingService.cs
  50. +35
    -0
      src/Discord.Net.Commands/CommandService.cs
  51. +12
    -0
      src/Discord.Net.Core/CDN.cs
  52. +17
    -0
      src/Discord.Net.Core/DiscordConfig.cs
  53. +6
    -0
      src/Discord.Net.Core/DiscordErrorCode.cs
  54. +5
    -5
      src/Discord.Net.Core/Entities/Channels/Direction.cs
  55. +1
    -1
      src/Discord.Net.Core/Entities/Channels/IChannel.cs
  56. +10
    -5
      src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs
  57. +17
    -0
      src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs
  58. +0
    -21
      src/Discord.Net.Core/Entities/Guilds/GuildIntegrationProperties.cs
  59. +5
    -0
      src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventsProperties.cs
  60. +80
    -8
      src/Discord.Net.Core/Entities/Guilds/IGuild.cs
  61. +13
    -0
      src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs
  62. +0
    -18
      src/Discord.Net.Core/Entities/Guilds/IntegrationAccount.cs
  63. +35
    -12
      src/Discord.Net.Core/Entities/Integrations/IIntegration.cs
  64. +23
    -0
      src/Discord.Net.Core/Entities/Integrations/IIntegrationAccount.cs
  65. +33
    -0
      src/Discord.Net.Core/Entities/Integrations/IIntegrationApplication.cs
  66. +17
    -0
      src/Discord.Net.Core/Entities/Integrations/IntegrationExpireBehavior.cs
  67. +3
    -0
      src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs
  68. +20
    -1
      src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteraction.cs
  69. +16
    -0
      src/Discord.Net.Core/Entities/Messages/FileAttachment.cs
  70. +8
    -0
      src/Discord.Net.Core/Entities/Messages/IAttachment.cs
  71. +1
    -1
      src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs
  72. +17
    -0
      src/Discord.Net.Core/Entities/Users/ConnectionVisibility.cs
  73. +44
    -15
      src/Discord.Net.Core/Entities/Users/IConnection.cs
  74. +6
    -3
      src/Discord.Net.Core/Format.cs
  75. +30
    -0
      src/Discord.Net.Interactions/Attributes/ComplexParameterAttribute.cs
  76. +10
    -0
      src/Discord.Net.Interactions/Attributes/ComplexParameterCtorAttribute.cs
  77. +25
    -0
      src/Discord.Net.Interactions/Attributes/EnumChoiceAttribute.cs
  78. +3
    -3
      src/Discord.Net.Interactions/Builders/Commands/ComponentCommandBuilder.cs
  79. +5
    -0
      src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs
  80. +4
    -0
      src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs
  81. +4
    -2
      src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs
  82. +69
    -9
      src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs
  83. +77
    -0
      src/Discord.Net.Interactions/Builders/Parameters/ComponentCommandParameterBuilder.cs
  84. +8
    -1
      src/Discord.Net.Interactions/Builders/Parameters/ModalCommandParameterBuilder.cs
  85. +66
    -2
      src/Discord.Net.Interactions/Builders/Parameters/SlashCommandParameterBuilder.cs
  86. +12
    -0
      src/Discord.Net.Interactions/Entities/ITypeConverter.cs
  87. +5
    -1
      src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs
  88. +25
    -0
      src/Discord.Net.Interactions/Extensions/RestExtensions.cs
  89. +1
    -8
      src/Discord.Net.Interactions/Info/Commands/AutocompleteCommandInfo.cs
  90. +17
    -21
      src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs
  91. +21
    -56
      src/Discord.Net.Interactions/Info/Commands/ComponentCommandInfo.cs
  92. +27
    -9
      src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs
  93. +76
    -33
      src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs
  94. +6
    -0
      src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs
  95. +50
    -1
      src/Discord.Net.Interactions/Info/ModalInfo.cs
  96. +34
    -0
      src/Discord.Net.Interactions/Info/Parameters/ComponentCommandParameterInfo.cs
  97. +8
    -1
      src/Discord.Net.Interactions/Info/Parameters/ModalCommandParameterInfo.cs
  98. +28
    -2
      src/Discord.Net.Interactions/Info/Parameters/SlashCommandParameterInfo.cs
  99. +228
    -84
      src/Discord.Net.Interactions/InteractionService.cs
  100. +1
    -1
      src/Discord.Net.Interactions/InteractionServiceConfig.cs

+ 8
- 0
.github/ISSUE_TEMPLATE/bugreport.yml View File

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

+ 3
- 0
.gitmodules View File

@@ -0,0 +1,3 @@
[submodule "overrides/Discord.Net.BuildOverrides"]
path = overrides/Discord.Net.BuildOverrides
url = https://github.com/discord-net/Discord.Net.BuildOverrides

+ 63
- 0
CHANGELOG.md View File

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

## [3.5.0] - 2022-04-05

### Added
- #2204 Added config option for bidirectional formatting of usernames (e38104b)
- #2210 Add a way to remove type readers from the interaction/command service. (7339945)
- #2213 Add global interaction post execution event. (a744948)
- #2223 Add ban pagination support (d8757a5)
- #2201 Add missing interface methods to IComponentInteraction (741ed80)
- #2226 Add an action delegate parameter to `RespondWithModalAsync<T>()` for modifying the modal (d2118f0)
- #2227 Add RespondWithModal methods to RestInteractinModuleBase (1c680db)

### Fixed
- #2168 Fix Integration model from GuildIntegration and added INTEGRATION gateway events (305d7f9)
- #2187 Fix modal response failing (d656722)
- #2188 Fix serialization error on thread creation timestamp. (d48a7bd)
- #2209 Fix GuildPermissions.All not including newer permissions (91d8fab)
- #2219 Fix ShardedClients not pushing PresenceUpdates (c4131cf)
- #2225 Fix GuildMemberUpdated cacheable `before` entity being incorrect (bfd0d9b)
- #2217 Fix gateway interactions not running without bot scope. (8522447)

### Misc
- #2193 Update GuildMemberUpdated comment regarding presence (82473bc)
- #2206 Fixed typo (c286b99)
- #2216 Fix small typo in modal example (0439437)
- #2228 Correct minor typo (d1cf1bf)

## [3.4.1] - 2022-03-9

### Added
- #2169 Component TypeConverters and CustomID TypeReaders (fb4250b)
- #2180 Attachment description and content type (765c0c5)
- #2162 Add configuration toggle to suppress Unknown dispatch warnings (1ba96d6)
- #2178 Add 10065 Error code (cc6918d)

### Fixed
- #2179 Logging out sharded client throws (24b7bb5)
- #2182 Thread owner always returns null (25aaa49)
- #2165 Fix error with flag params when uploading files. (a5d3add)
- #2181 Fix ambiguous reference for creating roles (f8ec3c7)

## [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


+ 16
- 1
Discord.Net.sln View File

@@ -34,7 +34,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_InteractionFramework", "sa
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_WebhookClient", "samples\WebhookClient\_WebhookClient.csproj", "{B61AAE66-15CC-40E4-873A-C23E697C3411}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IDN", "samples\idn\idn.csproj", "{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "idn", "samples\idn\idn.csproj", "{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{C7CF5621-7D36-433B-B337-5A2E3C101A71}"
EndProject
@@ -44,6 +44,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions",
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.BuildOverrides", "experiment\Discord.Net.BuildOverrides\Discord.Net.BuildOverrides.csproj", "{115F4921-B44D-4F69-996B-69796959C99D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -258,6 +260,18 @@ Global
{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x64.Build.0 = Release|Any CPU
{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x86.ActiveCfg = Release|Any CPU
{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x86.Build.0 = Release|Any CPU
{115F4921-B44D-4F69-996B-69796959C99D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{115F4921-B44D-4F69-996B-69796959C99D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{115F4921-B44D-4F69-996B-69796959C99D}.Debug|x64.ActiveCfg = Debug|Any CPU
{115F4921-B44D-4F69-996B-69796959C99D}.Debug|x64.Build.0 = Debug|Any CPU
{115F4921-B44D-4F69-996B-69796959C99D}.Debug|x86.ActiveCfg = Debug|Any CPU
{115F4921-B44D-4F69-996B-69796959C99D}.Debug|x86.Build.0 = Debug|Any CPU
{115F4921-B44D-4F69-996B-69796959C99D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{115F4921-B44D-4F69-996B-69796959C99D}.Release|Any CPU.Build.0 = Release|Any CPU
{115F4921-B44D-4F69-996B-69796959C99D}.Release|x64.ActiveCfg = Release|Any CPU
{115F4921-B44D-4F69-996B-69796959C99D}.Release|x64.Build.0 = Release|Any CPU
{115F4921-B44D-4F69-996B-69796959C99D}.Release|x86.ActiveCfg = Release|Any CPU
{115F4921-B44D-4F69-996B-69796959C99D}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -279,6 +293,7 @@ Global
{A23E46D2-1610-4AE5-820F-422D34810887} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}
{B61AAE66-15CC-40E4-873A-C23E697C3411} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}
{4A03840B-9EBE-47E3-89AB-E0914DF21AFB} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}
{115F4921-B44D-4F69-996B-69796959C99D} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D2404771-EEC8-45F2-9D71-F3373F6C1495}


+ 1
- 1
Discord.Net.targets View File

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


+ 1
- 0
azure/deploy.yml View File

@@ -8,6 +8,7 @@ steps:
dotnet pack "src\Discord.Net.Providers.WS4Net\Discord.Net.Providers.WS4Net.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "$(Build.ArtifactStagingDirectory)" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag)
dotnet pack "src\Discord.Net.Analyzers\Discord.Net.Analyzers.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "$(Build.ArtifactStagingDirectory)" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag)
dotnet pack "src\Discord.Net.Interactions\Discord.Net.Interactions.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "$(Build.ArtifactStagingDirectory)" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag)
dotnet pack "experiment\Discord.Net.BuildOverrides\Discord.Net.BuildOverrides.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "$(Build.ArtifactStagingDirectory)" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag)
displayName: Pack projects

- task: NuGetCommand@2


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


+ 41
- 0
docs/faq/build_overrides/what-are-they.md View File

@@ -0,0 +1,41 @@
---
uid: FAQ.BuildOverrides.WhatAreThey
title: Build Overrides, What are they?
---

# Build Overrides

Build overrides are a way for library developers to override the default behavior of the library on the fly. Adding them to your code is really simple.

## Installing the package

The build override package can be installed on nuget [here](TODO) or by using the package manager

```
PM> Install-Package Discord.Net.BuildOverrides
```

## Adding an override

```cs
public async Task MainAsync()
{
// hook into the log function
BuildOverrides.Log += (buildOverride, message) =>
{
Console.WriteLine($"{buildOverride.Name}: {message}");
return Task.CompletedTask;
};

// add your overrides
await BuildOverrides.AddOverrideAsync("example-override-name");
}

```

Overrides are normally built for specific problems, for example if someone is having an issue and we think we might have a fix then we can create a build override for them to test out the fix.

## Security and Transparency

Overrides can only be created and updated by library developers, you should only apply an override if a library developer askes you to.
Code for the overrides server and the overrides themselves can be found [here](https://github.com/discord-net/Discord.Net.BuildOverrides).

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

@@ -22,3 +22,5 @@
topicUid: FAQ.TextCommands.General
- name: Legacy or Upgrade
topicUid: FAQ.Legacy
- name: Build Overrides
topicUid: FAQ.BuildOverrides.WhatAreThey

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


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

+ 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")]


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

@@ -20,7 +20,7 @@ public class FoodModal : IModal

// Responds to the modal.
[ModalInteraction("food_menu")]
public async Task ModalResponce(FoodModal modal)
public async Task ModalResponse(FoodModal modal)
{
// Build the message to send.
string message = "hey @everyone, I just learned " +


+ 1
- 1
docs/guides/int_framework/samples/postexecution/error_review.cs View File

@@ -16,7 +16,7 @@ async Task SlashCommandExecuted(SlashCommandInfo arg1, Discord.IInteractionConte
await arg2.Interaction.RespondAsync("Invalid number or arguments");
break;
case InteractionCommandError.Exception:
await arg2.Interaction.RespondAsync("Command exception:{arg3.ErrorReason}");
await arg2.Interaction.RespondAsync($"Command exception: {arg3.ErrorReason}");
break;
case InteractionCommandError.Unsuccessful:
await arg2.Interaction.RespondAsync("Command could not be executed");


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

Before After
Width: 428  |  Height: 166  |  Size: 24 KiB

+ 70
- 0
docs/guides/other_libs/mediatr.md View File

@@ -0,0 +1,70 @@
---
uid: Guides.OtherLibs.MediatR
title: MediatR
---

# Configuring MediatR

## Prerequisites

- A simple bot with dependency injection configured

## 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|
|--|--|
| `MediatR` | [link](https://www.nuget.org/packages/MediatR) |
| `MediatR.Extensions.Microsoft.DependencyInjection` | [link](https://www.nuget.org/packages/MediatR.Extensions.Microsoft.DependencyInjection)|

## Adding MediatR to your dependency injection container

Adding MediatR to your dependency injection is made easy by the `MediatR.Extensions.Microsoft.DependencyInjection` package. You can use the following piece of code to configure it. The parameter of `.AddMediatR()` can be any type that is inside of the assembly you will have your event handlers in.

[!code-csharp[Configuring MediatR](samples/MediatrConfiguringDI.cs)]

## Creating notifications

The way MediatR publishes events throughout your applications is through notifications and notification handlers. For this guide we will create a notification to handle the `MessageReceived` event on the `DiscordSocketClient`.

[!code-csharp[Creating a notification](samples/MediatrCreatingMessageNotification.cs)]

## Creating the notification publisher / event listener

For MediatR to actually publish the events we need a way to listen for them. We will create a class to listen for discord events like so:

[!code-csharp[Creating an event listener](samples/MediatrDiscordEventListener.cs)]

The code above does a couple of things. First it receives the DiscordSocketClient from the dependency injection container. It can then use this client to register events. In this guide we will be focusing on the MessageReceived event. You register the event like any ordinary event, but inside of the handler method we will use MediatR to publish our event to all of our notification handlers.

## Adding the event listener to your dependency injection container

To start the listener we have to call the `StartAsync()` method on our `DiscordEventListener` class from inside of our main function. To do this, first register the `DiscordEventListener` class in your dependency injection container and get a reference to it in your main method.

[!code-csharp[Starting the event listener](samples/MediatrStartListener.cs)]

## Creating your notification handler

MediatR publishes notifications to all of your notification handlers that are listening for a specific notification. We will create a handler for our newly created `MessageReceivedNotification` like this:

[!code-csharp[Creating an event listener](samples/MediatrMessageReceivedHandler.cs)]

The code above implements the `INotificationHandler<>` interface provided by MediatR, this tells MediatR to dispatch `MessageReceivedNotification` notifications to this handler class.

> [!NOTE]
> You can create as many notification handlers for the same notification as you desire. That's the beauty of MediatR!

## Testing

To test if we have successfully implemented MediatR, we can start up the bot and send a message to a server the bot is in. It should print out the message we defined earlier in our `MessageReceivedHandler`.

![MediatR output](images/mediatr_output.png)

## Adding more event types

To add more event types you can follow these steps:

1. Create a new notification class for the event. it should contain all of the parameters that the event would send. (Ex: the `MessageReceived` event takes one `SocketMessage` as an argument. The notification class should also map this argument)
2. Register the event in your `DiscordEventListener` class.
3. Create a notification handler for your new notification.

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

@@ -0,0 +1 @@
.AddMediatR(typeof(Bot))

+ 16
- 0
docs/guides/other_libs/samples/MediatrCreatingMessageNotification.cs View File

@@ -0,0 +1,16 @@
// MessageReceivedNotification.cs

using Discord.WebSocket;
using MediatR;

namespace MediatRSample.Notifications;

public class MessageReceivedNotification : INotification
{
public MessageReceivedNotification(SocketMessage message)
{
Message = message ?? throw new ArgumentNullException(nameof(message));
}

public SocketMessage Message { get; }
}

+ 46
- 0
docs/guides/other_libs/samples/MediatrDiscordEventListener.cs View File

@@ -0,0 +1,46 @@
// DiscordEventListener.cs

using Discord.WebSocket;
using MediatR;
using MediatRSample.Notifications;
using Microsoft.Extensions.DependencyInjection;
using System.Threading;
using System.Threading.Tasks;

namespace MediatRSample;

public class DiscordEventListener
{
private readonly CancellationToken _cancellationToken;

private readonly DiscordSocketClient _client;
private readonly IServiceScopeFactory _serviceScope;

public DiscordEventListener(DiscordSocketClient client, IServiceScopeFactory serviceScope)
{
_client = client;
_serviceScope = serviceScope;
_cancellationToken = new CancellationTokenSource().Token;
}

private IMediator Mediator
{
get
{
var scope = _serviceScope.CreateScope();
return scope.ServiceProvider.GetRequiredService<IMediator>();
}
}

public async Task StartAsync()
{
_client.MessageReceived += OnMessageReceivedAsync;

await Task.CompletedTask;
}

private Task OnMessageReceivedAsync(SocketMessage arg)
{
return Mediator.Publish(new MessageReceivedNotification(arg), _cancellationToken);
}
}

+ 17
- 0
docs/guides/other_libs/samples/MediatrMessageReceivedHandler.cs View File

@@ -0,0 +1,17 @@
// MessageReceivedHandler.cs

using System;
using MediatR;
using MediatRSample.Notifications;

namespace MediatRSample;

public class MessageReceivedHandler : INotificationHandler<MessageReceivedNotification>
{
public async Task Handle(MessageReceivedNotification notification, CancellationToken cancellationToken)
{
Console.WriteLine($"MediatR works! (Received a message by {notification.Message.Author.Username})");

// Your implementation
}
}

+ 4
- 0
docs/guides/other_libs/samples/MediatrStartListener.cs View File

@@ -0,0 +1,4 @@
// Program.cs

var listener = services.GetRequiredService<DiscordEventListener>();
await listener.StartAsync();

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

@@ -115,6 +115,8 @@
topicUid: Guides.OtherLibs.Serilog
- name: EFCore
topicUid: Guides.OtherLibs.EFCore
- name: MediatR
topicUid: Guides.OtherLibs.MediatR
- name: Emoji
topicUid: Guides.Emoji
- name: Voice


+ 1
- 1
docs/index.md View File

@@ -67,7 +67,7 @@ Being interactions, they are handled as SocketInteractions. Creating and receivi
- Find out more about slash commands in the
[Slash Command Guides](xref:Guides.SlashCommands.Intro)

#### Context Message & User Ccommands
#### Context Message & User Commands

These commands can be pointed at messages and users, in custom application tabs.
Being interactions as well, they are able to be handled just like slash commands. They do not have options however.


+ 278
- 0
experiment/Discord.Net.BuildOverrides/BuildOverrides.cs View File

@@ -0,0 +1,278 @@
using Discord.Overrides;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Runtime.Loader;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
/// <summary>
/// Represents an override that can be loaded.
/// </summary>
public sealed class Override
{
/// <summary>
/// Gets the ID of the override.
/// </summary>
public Guid Id { get; internal set; }

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

/// <summary>
/// Gets the description of the override.
/// </summary>
public string Description { get; internal set; }

/// <summary>
/// Gets the date this override was created.
/// </summary>
public DateTimeOffset CreatedAt { get; internal set; }

/// <summary>
/// Gets the date the override was last modified.
/// </summary>
public DateTimeOffset LastUpdated { get; internal set; }

internal static Override FromJson(string json)
{
var result = new Override();

using(var textReader = new StringReader(json))
using(var reader = new JsonTextReader(textReader))
{
var obj = JObject.ReadFrom(reader);
result.Id = obj["id"].ToObject<Guid>();
result.Name = obj["name"].ToObject<string>();
result.Description = obj["description"].ToObject<string>();
result.CreatedAt = obj["created_at"].ToObject<DateTimeOffset>();
result.LastUpdated = obj["last_updated"].ToObject<DateTimeOffset>();
}

return result;
}
}

/// <summary>
/// Represents a loaded override instance.
/// </summary>
public sealed class LoadedOverride
{
/// <summary>
/// Gets the aseembly containing the overrides definition.
/// </summary>
public Assembly Assembly { get; internal set; }

/// <summary>
/// Gets an instance of the override.
/// </summary>
public IOverride Instance { get; internal set; }

/// <summary>
/// Gets the overrides type.
/// </summary>
public Type Type { get; internal set; }
}

public sealed class BuildOverrides
{
/// <summary>
/// Fired when an override logs a message.
/// </summary>
public static event Func<Override, string, Task> Log
{
add => _logEvents.Add(value);
remove => _logEvents.Remove(value);

}

/// <summary>
/// Gets a read-only dictionary containing the currently loaded overrides.
/// </summary>
public IReadOnlyDictionary<Override, IReadOnlyCollection<LoadedOverride>> LoadedOverrides
=> _loadedOverrides.Select(x => new KeyValuePair<Override, IReadOnlyCollection<LoadedOverride>> (x.Key, x.Value)).ToDictionary(x => x.Key, x => x.Value);

private static AssemblyLoadContext _overrideDomain;
private static List<Func<Override, string, Task>> _logEvents = new();
private static ConcurrentDictionary<Override, List<LoadedOverride>> _loadedOverrides = new ConcurrentDictionary<Override, List<LoadedOverride>>();

private const string ApiUrl = "https://overrides.discordnet.dev";
static BuildOverrides()
{
_overrideDomain = new AssemblyLoadContext("Discord.Net.Overrides.Runtime");

_overrideDomain.Resolving += _overrideDomain_Resolving;
}

/// <summary>
/// Gets details about a specific override.
/// </summary>
/// <remarks>
/// <b>Note:</b> This method does not load an override, it simply retrives the info about it.
/// </remarks>
/// <param name="name">The name of the override to get.</param>
/// <returns>
/// A task representing the asynchronous get operation. The tasks result is an <see cref="Override"/>
/// if it exists; otherwise <see langword="null"/>.
/// </returns>
public static async Task<Override> GetOverrideAsync(string name)
{
using (var client = new HttpClient())
{
var result = await client.GetAsync($"{ApiUrl}/overrides/{name}");

if (result.IsSuccessStatusCode)
{
var content = await result.Content.ReadAsStringAsync();

return Override.FromJson(content);
}
else
return null;
}
}

/// <summary>
/// Adds an override to the current Discord.Net instance.
/// </summary>
/// <remarks>
/// The override initialization is non-blocking, any errors that occor within
/// the overrides initialization procedure will be sent in the <see cref="Log"/> event.
/// </remarks>
/// <param name="name">The name of the override to add.</param>
/// <returns>
/// A task representing the asynchronous add operaton. The tasks result is a boolean
/// determining if the add operation was successful.
/// </returns>
public static async Task<bool> AddOverrideAsync(string name)
{
var ovrride = await GetOverrideAsync(name);

if (ovrride == null)
return false;

return await AddOverrideAsync(ovrride);
}

/// <summary>
/// Adds an override to the current Discord.Net instance.
/// </summary>
/// <remarks>
/// The override initialization is non-blocking, any errors that occor within
/// the overrides initialization procedure will be sent in the <see cref="Log"/> event.
/// </remarks>
/// <param name="ovrride">The override to add.</param>
/// <returns>
/// A task representing the asynchronous add operaton. The tasks result is a boolean
/// determining if the add operation was successful.
/// </returns>
public static async Task<bool> AddOverrideAsync(Override ovrride)
{
// download it
var ms = new MemoryStream();

using (var client = new HttpClient())
{
var result = await client.GetAsync($"{ApiUrl}/overrides/download/{ovrride.Id}");

if (!result.IsSuccessStatusCode)
return false;

await (await result.Content.ReadAsStreamAsync()).CopyToAsync(ms);
}

ms.Position = 0;

// load the assembly
//var test = Assembly.Load(ms.ToArray());
var asm = _overrideDomain.LoadFromStream(ms);

// find out IOverride
var overrides = asm.GetTypes().Where(x => x.GetInterfaces().Any(x => x == typeof(IOverride)));

List<LoadedOverride> loaded = new();

var context = new OverrideContext((m) => HandleLog(ovrride, m), ovrride);

foreach (var ovr in overrides)
{
var inst = (IOverride)Activator.CreateInstance(ovr);

inst.RegisterPackageLookupHandler((s) =>
{
return GetDependencyAsync(ovrride.Id, s);
});

_ = Task.Run(async () =>
{
try
{
await inst.InitializeAsync(context);
}
catch (Exception x)
{
HandleLog(ovrride, $"Failed to initialize build override: {x}");
}
});

loaded.Add(new LoadedOverride()
{
Assembly = asm,
Instance = inst,
Type = ovr
});
}

return _loadedOverrides.AddOrUpdate(ovrride, loaded, (_, __) => loaded) != null;
}

internal static void HandleLog(Override ovr, string msg)
{
_ = Task.Run(async () =>
{
foreach (var item in _logEvents)
{
await item.Invoke(ovr, msg).ConfigureAwait(false);
}
});
}

private static Assembly _overrideDomain_Resolving(AssemblyLoadContext arg1, AssemblyName arg2)
{
// resolve the override id
var v = _loadedOverrides.FirstOrDefault(x => x.Value.Any(x => x.Assembly.FullName == arg1.Assemblies.FirstOrDefault().FullName));

return GetDependencyAsync(v.Key.Id, $"{arg2}").GetAwaiter().GetResult();
}

private static async Task<Assembly> GetDependencyAsync(Guid id, string name)
{
using(var client = new HttpClient())
{
var result = await client.PostAsync($"{ApiUrl}/overrides/{id}/dependency", new StringContent($"{{ \"info\": \"{name}\"}}", Encoding.UTF8, "application/json"));

if (!result.IsSuccessStatusCode)
throw new Exception("Failed to get dependency");

using(var ms = new MemoryStream())
{
var innerStream = await result.Content.ReadAsStreamAsync();
await innerStream.CopyToAsync(ms);
ms.Position = 0;
return _overrideDomain.LoadFromStream(ms);
}
}
}
}
}

+ 20
- 0
experiment/Discord.Net.BuildOverrides/Discord.Net.BuildOverrides.csproj View File

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

<PropertyGroup>
<LangVersion>9.0</LangVersion>
<AssemblyName>Discord.Net.BuildOverrides</AssemblyName>
<RootNamespace>Discord.BuildOverrides</RootNamespace>
<Description>A Discord.Net extension adding a way to add build overrides for testing.</Description>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">net6.0;net5.0;</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">net6.0;net5.0;</TargetFrameworks>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'net461' ">
<Reference Include="System.Net.Http" />
</ItemGroup>

</Project>

+ 34
- 0
experiment/Discord.Net.BuildOverrides/IOverride.cs View File

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

namespace Discord.Overrides
{
/// <summary>
/// Represents a generic build override for Discord.Net
/// </summary>
public interface IOverride
{
/// <summary>
/// Initializes the override.
/// </summary>
/// <remarks>
/// This method is called by the <see cref="BuildOverrides"/> class
/// and should not be called externally from it.
/// </remarks>
/// <param name="context">Context used by an override to initialize.</param>
/// <returns>
/// A task representing the asynchronous initialization operation.
/// </returns>
Task InitializeAsync(OverrideContext context);

/// <summary>
/// Registers a callback to load a dependency for this override.
/// </summary>
/// <param name="func">The callback to load an external dependency.</param>
void RegisterPackageLookupHandler(Func<string, Task<Assembly>> func);
}
}

+ 30
- 0
experiment/Discord.Net.BuildOverrides/OverrideContext.cs View File

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

namespace Discord.Overrides
{
/// <summary>
/// Represents context thats passed to an override in the initialization step.
/// </summary>
public sealed class OverrideContext
{
/// <summary>
/// A callback used to log messages.
/// </summary>
public Action<string> Log { get; private set; }

/// <summary>
/// The info about the override.
/// </summary>
public Override Info { get; private set; }

internal OverrideContext(Action<string> log, Override info)
{
Log = log;
Info = info;
}
}
}

+ 1
- 0
overrides/Discord.Net.BuildOverrides

@@ -0,0 +1 @@
Subproject commit 9b2be5597468329090015fa1b2775815b20be440

+ 0
- 152
samples/InteractionFramework/CommandHandler.cs View File

@@ -1,152 +0,0 @@
using Discord;
using Discord.Interactions;
using Discord.WebSocket;
using System;
using System.Reflection;
using System.Threading.Tasks;

namespace InteractionFramework
{
public class CommandHandler
{
private readonly DiscordSocketClient _client;
private readonly InteractionService _commands;
private readonly IServiceProvider _services;

public CommandHandler(DiscordSocketClient client, InteractionService commands, IServiceProvider services)
{
_client = client;
_commands = commands;
_services = services;
}

public async Task InitializeAsync ( )
{
// Add the public modules that inherit InteractionModuleBase<T> to the InteractionService
await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services);
// Another approach to get the assembly of a specific type is:
// typeof(CommandHandler).Assembly


// Process the InteractionCreated payloads to execute Interactions commands
_client.InteractionCreated += HandleInteraction;

// Process the command execution results
_commands.SlashCommandExecuted += SlashCommandExecuted;
_commands.ContextCommandExecuted += ContextCommandExecuted;
_commands.ComponentCommandExecuted += ComponentCommandExecuted;
}

# region Error Handling

private Task ComponentCommandExecuted (ComponentCommandInfo arg1, Discord.IInteractionContext arg2, IResult arg3)
{
if (!arg3.IsSuccess)
{
switch (arg3.Error)
{
case InteractionCommandError.UnmetPrecondition:
// implement
break;
case InteractionCommandError.UnknownCommand:
// implement
break;
case InteractionCommandError.BadArgs:
// implement
break;
case InteractionCommandError.Exception:
// implement
break;
case InteractionCommandError.Unsuccessful:
// implement
break;
default:
break;
}
}

return Task.CompletedTask;
}

private Task ContextCommandExecuted (ContextCommandInfo arg1, Discord.IInteractionContext arg2, IResult arg3)
{
if (!arg3.IsSuccess)
{
switch (arg3.Error)
{
case InteractionCommandError.UnmetPrecondition:
// implement
break;
case InteractionCommandError.UnknownCommand:
// implement
break;
case InteractionCommandError.BadArgs:
// implement
break;
case InteractionCommandError.Exception:
// implement
break;
case InteractionCommandError.Unsuccessful:
// implement
break;
default:
break;
}
}

return Task.CompletedTask;
}

private Task SlashCommandExecuted (SlashCommandInfo arg1, Discord.IInteractionContext arg2, IResult arg3)
{
if (!arg3.IsSuccess)
{
switch (arg3.Error)
{
case InteractionCommandError.UnmetPrecondition:
// implement
break;
case InteractionCommandError.UnknownCommand:
// implement
break;
case InteractionCommandError.BadArgs:
// implement
break;
case InteractionCommandError.Exception:
// implement
break;
case InteractionCommandError.Unsuccessful:
// implement
break;
default:
break;
}
}

return Task.CompletedTask;
}
# endregion

# region Execution

private async Task HandleInteraction (SocketInteraction arg)
{
try
{
// Create an execution context that matches the generic type parameter of your InteractionModuleBase<T> modules
var ctx = new SocketInteractionContext(_client, arg);
await _commands.ExecuteCommandAsync(ctx, _services);
}
catch (Exception ex)
{
Console.WriteLine(ex);

// If a Slash Command execution fails it is most likely that the original interaction acknowledgement will persist. It is a good idea to delete the original
// response, or at least let the user know that something went wrong during the command execution.
if(arg.Type == InteractionType.ApplicationCommand)
await arg.GetOriginalResponseAsync().ContinueWith(async (msg) => await msg.Result.DeleteAsync());
}
}
# endregion
}
}

+ 20
- 0
samples/InteractionFramework/Enums/ExampleEnum.cs View File

@@ -0,0 +1,20 @@
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
{
First,
Second,
Third,
Fourth,
[ChoiceDisplay("Twenty First")]
TwentyFirst
}
}

+ 0
- 10
samples/InteractionFramework/ExampleEnum.cs View File

@@ -1,10 +0,0 @@
namespace InteractionFramework
{
public enum ExampleEnum
{
First,
Second,
Third,
Fourth
}
}

+ 81
- 0
samples/InteractionFramework/InteractionHandler.cs View File

@@ -0,0 +1,81 @@
using Discord;
using Discord.Interactions;
using Discord.WebSocket;
using Microsoft.Extensions.Configuration;
using System;
using System.Reflection;
using System.Threading.Tasks;

namespace InteractionFramework
{
public class InteractionHandler
{
private readonly DiscordSocketClient _client;
private readonly InteractionService _handler;
private readonly IServiceProvider _services;
private readonly IConfiguration _configuration;

public InteractionHandler(DiscordSocketClient client, InteractionService handler, IServiceProvider services, IConfiguration config)
{
_client = client;
_handler = handler;
_services = services;
_configuration = config;
}

public async Task InitializeAsync()
{
// Process when the client is ready, so we can register our commands.
_client.Ready += ReadyAsync;
_handler.Log += LogAsync;

// Add the public modules that inherit InteractionModuleBase<T> to the InteractionService
await _handler.AddModulesAsync(Assembly.GetEntryAssembly(), _services);

// Process the InteractionCreated payloads to execute Interactions commands
_client.InteractionCreated += HandleInteraction;
}

private async Task LogAsync(LogMessage log)
=> Console.WriteLine(log);

private async Task ReadyAsync()
{
// Context & Slash commands can be automatically registered, but this process needs to happen after the client enters the READY state.
// Since Global Commands take around 1 hour to register, we should use a test guild to instantly update and test our commands.
if (Program.IsDebug())
await _handler.RegisterCommandsToGuildAsync(_configuration.GetValue<ulong>("testGuild"), true);
else
await _handler.RegisterCommandsGloballyAsync(true);
}

private async Task HandleInteraction(SocketInteraction interaction)
{
try
{
// Create an execution context that matches the generic type parameter of your InteractionModuleBase<T> modules.
var context = new SocketInteractionContext(_client, interaction);

// Execute the incoming command.
var result = await _handler.ExecuteCommandAsync(context, _services);

if (!result.IsSuccess)
switch (result.Error)
{
case InteractionCommandError.UnmetPrecondition:
// implement
break;
default:
break;
}
}
catch
{
// If Slash Command execution fails it is most likely that the original interaction acknowledgement will persist. It is a good idea to delete the original
// response, or at least let the user know that something went wrong during the command execution.
if (interaction.Type is InteractionType.ApplicationCommand)
await interaction.GetOriginalResponseAsync().ContinueWith(async (msg) => await msg.Result.DeleteAsync());
}
}
}
}

+ 0
- 18
samples/InteractionFramework/Modules/ComponentModule.cs View File

@@ -1,18 +0,0 @@
using Discord.Interactions;
using Discord.WebSocket;
using InteractionFramework.Attributes;
using System.Threading.Tasks;

namespace InteractionFramework
{
// As with all other modules, we create the context by defining what type of interaction this module is supposed to target.
internal class ComponentModule : InteractionModuleBase<SocketInteractionContext<SocketMessageComponent>>
{
// With the Attribute DoUserCheck you can make sure that only the user this button targets can click it. This is defined by the first wildcard: *.
// See Attributes/DoUserCheckAttribute.cs for elaboration.
[DoUserCheck]
[ComponentInteraction("myButton:*")]
public async Task ClickButtonAsync(string userId)
=> await RespondAsync(text: ":thumbsup: Clicked!");
}
}

samples/InteractionFramework/Modules/GeneralModule.cs → samples/InteractionFramework/Modules/ExampleModule.cs View File

@@ -1,32 +1,25 @@
using Discord;
using Discord.Interactions;
using InteractionFramework.Attributes;
using System;
using System.Threading.Tasks;

namespace InteractionFramework.Modules
{
// Interation modules must be public and inherit from an IInterationModuleBase
public class GeneralModule : InteractionModuleBase<SocketInteractionContext>
public class ExampleModule : InteractionModuleBase<SocketInteractionContext>
{
// Dependencies can be accessed through Property injection, public properties with public setters will be set by the service provider
public InteractionService Commands { get; set; }

private CommandHandler _handler;
private InteractionHandler _handler;

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

// Slash Commands are declared using the [SlashCommand], you need to provide a name and a description, both following the Discord guidelines
[SlashCommand("ping", "Recieve a pong")]
// By setting the DefaultPermission to false, you can disable the command by default. No one can use the command until you give them permission
[DefaultPermission(false)]
public async Task Ping ( )
{
await RespondAsync("pong");
}

// You can use a number of parameter types in you Slash Command handlers (string, int, double, bool, IUser, IChannel, IMentionable, IRole, Enums) by default. Optionally,
// you can implement your own TypeConverters to support a wider range of parameter types. For more information, refer to the library documentation.
// Optional method parameters(parameters with a default value) also will be displayed as optional on Discord.
@@ -34,9 +27,15 @@ namespace InteractionFramework.Modules
// [Summary] lets you customize the name and the description of a parameter
[SlashCommand("echo", "Repeat the input")]
public async Task Echo(string echo, [Summary(description: "mention the user")]bool mention = false)
{
await RespondAsync(echo + (mention ? Context.User.Mention : string.Empty));
}
=> await RespondAsync(echo + (mention ? Context.User.Mention : string.Empty));

[SlashCommand("ping", "Pings the bot and returns its latency.")]
public async Task GreetUserAsync()
=> await RespondAsync(text: $":ping_pong: It took me {Context.Client.Latency}ms to respond to you!", ephemeral: true);

[SlashCommand("bitrate", "Gets the bitrate of a specific voice channel.")]
public async Task GetBitrateAsync([ChannelTypes(ChannelType.Voice, ChannelType.Stage)] IChannel channel)
=> await RespondAsync(text: $"This voice channel has a bitrate of {(channel as IVoiceChannel).Bitrate}");

// [Group] will create a command group. [SlashCommand]s and [ComponentInteraction]s will be registered with the group prefix
[Group("test_group", "This is a command group")]
@@ -46,25 +45,7 @@ namespace InteractionFramework.Modules
// choice option
[SlashCommand("choice_example", "Enums create choices")]
public async Task ChoiceExample(ExampleEnum input)
{
await RespondAsync(input.ToString());
}
}

// User Commands can only have one parameter, which must be a type of SocketUser
[UserCommand("SayHello")]
public async Task SayHello(IUser user)
{
await RespondAsync($"Hello, {user.Mention}");
}

// Message Commands can only have one parameter, which must be a type of SocketMessage
[MessageCommand("Delete")]
[Attributes.RequireOwner]
public async Task DeleteMesage(IMessage message)
{
await message.DeleteAsync();
await RespondAsync("Deleted message.");
=> await RespondAsync(input.ToString());
}

// Use [ComponentInteraction] to handle message component interactions. Message component interaction with the matching customId will be executed.
@@ -80,9 +61,40 @@ namespace InteractionFramework.Modules
// Select Menu interactions, contain ids of the menu options that were selected by the user. You can access the option ids from the method parameters.
// You can also use the wild card pattern with Select Menus, in that case, the wild card captures will be passed on to the method first, followed by the option ids.
[ComponentInteraction("roleSelect")]
public async Task RoleSelect(params string[] selections)
public async Task RoleSelect(string[] selections)
{
throw new NotImplementedException();
}

// With the Attribute DoUserCheck you can make sure that only the user this button targets can click it. This is defined by the first wildcard: *.
// See Attributes/DoUserCheckAttribute.cs for elaboration.
[DoUserCheck]
[ComponentInteraction("myButton:*")]
public async Task ClickButtonAsync(string userId)
=> await RespondAsync(text: ":thumbsup: Clicked!");

// This command will greet target user in the channel this was executed in.
[UserCommand("greet")]
public async Task GreetUserAsync(IUser user)
=> await RespondAsync(text: $":wave: {Context.User} said hi to you, <@{user.Id}>!");

// Pins a message in the channel it is in.
[MessageCommand("pin")]
public async Task PinMessageAsync(IMessage message)
{
// implement
// make a safety cast to check if the message is ISystem- or IUserMessage
if (message is not IUserMessage userMessage)
await RespondAsync(text: ":x: You cant pin system messages!");

// if the pins in this channel are equal to or above 50, no more messages can be pinned.
else if ((await Context.Channel.GetPinnedMessagesAsync()).Count >= 50)
await RespondAsync(text: ":x: You cant pin any more messages, the max has already been reached in this channel!");

else
{
await userMessage.PinAsync();
await RespondAsync(":white_check_mark: Successfully pinned message!");
}
}
}
}

+ 0
- 30
samples/InteractionFramework/Modules/MessageCommandModule.cs View File

@@ -1,30 +0,0 @@
using Discord;
using Discord.Interactions;
using Discord.WebSocket;
using System.Threading.Tasks;

namespace InteractionFramework.Modules
{
// A transient module for executing commands. This module will NOT keep any information after the command is executed.
internal class MessageCommandModule : InteractionModuleBase<SocketInteractionContext<SocketMessageCommand>>
{
// Pins a message in the channel it is in.
[MessageCommand("pin")]
public async Task PinMessageAsync(IMessage message)
{
// make a safety cast to check if the message is ISystem- or IUserMessage
if (message is not IUserMessage userMessage)
await RespondAsync(text: ":x: You cant pin system messages!");

// if the pins in this channel are equal to or above 50, no more messages can be pinned.
else if ((await Context.Channel.GetPinnedMessagesAsync()).Count >= 50)
await RespondAsync(text: ":x: You cant pin any more messages, the max has already been reached in this channel!");

else
{
await userMessage.PinAsync();
await RespondAsync(":white_check_mark: Successfully pinned message!");
}
}
}
}

+ 0
- 51
samples/InteractionFramework/Modules/SlashCommandModule.cs View File

@@ -1,51 +0,0 @@
using Discord;
using Discord.Interactions;
using Discord.WebSocket;
using System;
using System.Threading.Tasks;

namespace InteractionFramework.Modules
{
public enum Hobby
{
Gaming,

Art,

Reading
}

// A transient module for executing commands. This module will NOT keep any information after the command is executed.
class SlashCommandModule : InteractionModuleBase<SocketInteractionContext<SocketSlashCommand>>
{
// Will be called before execution. Here you can populate several entities you may want to retrieve before executing a command.
// I.E. database objects
public override void BeforeExecute(ICommandInfo command)
{
// Anything
throw new NotImplementedException();
}

// Will be called after execution
public override void AfterExecute(ICommandInfo command)
{
// Anything
throw new NotImplementedException();
}

[SlashCommand("ping", "Pings the bot and returns its latency.")]
public async Task GreetUserAsync()
=> await RespondAsync(text: $":ping_pong: It took me {Context.Client.Latency}ms to respond to you!", ephemeral: true);

[SlashCommand("hobby", "Choose your hobby from the list!")]
public async Task ChooseAsync(Hobby hobby)
=> await RespondAsync(text: $":thumbsup: Your hobby is: {hobby}.");

[SlashCommand("bitrate", "Gets the bitrate of a specific voice channel.")]
public async Task GetBitrateAsync([ChannelTypes(ChannelType.Voice, ChannelType.Stage)] IChannel channel)
{
var voiceChannel = channel as IVoiceChannel;
await RespondAsync(text: $"This voice channel has a bitrate of {voiceChannel.Bitrate}");
}
}
}

+ 0
- 17
samples/InteractionFramework/Modules/UserCommandModule.cs View File

@@ -1,17 +0,0 @@
using Discord;
using Discord.Interactions;
using Discord.WebSocket;
using System.Threading.Tasks;

namespace InteractionFramework.Modules
{
// A transient module for executing commands. This module will NOT keep any information after the command is executed.
class UserCommandModule : InteractionModuleBase<SocketInteractionContext<SocketUserCommand>>
{
// This command will greet target user in the channel this was executed in.
[UserCommand("greet")]
public async Task GreetUserAsync(IUser user)
=> await RespondAsync(text: $":wave: {Context.User} said hi to you, <@{user.Id}>!");
}
}


+ 33
- 42
samples/InteractionFramework/Program.cs View File

@@ -9,69 +9,60 @@ using System.Threading.Tasks;

namespace InteractionFramework
{
class Program
public class Program
{
// Entry point of the program.
static void Main ( string[] args )
private readonly IConfiguration _configuration;
private readonly IServiceProvider _services;

private readonly DiscordSocketConfig _socketConfig = new()
{
GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.GuildMembers,
AlwaysDownloadUsers = true,
};

public Program()
{
// One of the more flexable ways to access the configuration data is to use the Microsoft's Configuration model,
// this way we can avoid hard coding the environment secrets. I opted to use the Json and environment variable providers here.
IConfiguration config = new ConfigurationBuilder()
_configuration = new ConfigurationBuilder()
.AddEnvironmentVariables(prefix: "DC_")
.AddJsonFile("appsettings.json", optional: true)
.Build();

RunAsync(config).GetAwaiter().GetResult();
_services = new ServiceCollection()
.AddSingleton(_configuration)
.AddSingleton(_socketConfig)
.AddSingleton<DiscordSocketClient>()
.AddSingleton(x => new InteractionService(x.GetRequiredService<DiscordSocketClient>()))
.AddSingleton<InteractionHandler>()
.BuildServiceProvider();
}

static async Task RunAsync (IConfiguration configuration)
{
// Dependency injection is a key part of the Interactions framework but it needs to be disposed at the end of the app's lifetime.
using var services = ConfigureServices(configuration);
static void Main(string[] args)
=> new Program().RunAsync()
.GetAwaiter()
.GetResult();

var client = services.GetRequiredService<DiscordSocketClient>();
var commands = services.GetRequiredService<InteractionService>();
public async Task RunAsync()
{
var client = _services.GetRequiredService<DiscordSocketClient>();

client.Log += LogAsync;
commands.Log += LogAsync;

// Slash Commands and Context Commands are can be automatically registered, but this process needs to happen after the client enters the READY state.
// Since Global Commands take around 1 hour to register, we should use a test guild to instantly update and test our commands. To determine the method we should
// register the commands with, we can check whether we are in a DEBUG environment and if we are, we can register the commands to a predetermined test guild.
client.Ready += async ( ) =>
{
if (IsDebug())
// Id of the test guild can be provided from the Configuration object
await commands.RegisterCommandsToGuildAsync(configuration.GetValue<ulong>("testGuild"), true);
else
await commands.RegisterCommandsGloballyAsync(true);
};

// Here we can initialize the service that will register and execute our commands
await services.GetRequiredService<CommandHandler>().InitializeAsync();
await _services.GetRequiredService<InteractionHandler>()
.InitializeAsync();

// Bot token can be provided from the Configuration object we set up earlier
await client.LoginAsync(TokenType.Bot, configuration["token"]);
await client.LoginAsync(TokenType.Bot, _configuration["token"]);
await client.StartAsync();

// Never quit the program until manually forced to.
await Task.Delay(Timeout.Infinite);
}

static Task LogAsync(LogMessage message)
{
Console.WriteLine(message.ToString());
return Task.CompletedTask;
}

static ServiceProvider ConfigureServices ( IConfiguration configuration )
=> new ServiceCollection()
.AddSingleton(configuration)
.AddSingleton<DiscordSocketClient>()
.AddSingleton(x => new InteractionService(x.GetRequiredService<DiscordSocketClient>()))
.AddSingleton<CommandHandler>()
.BuildServiceProvider();
private async Task LogAsync(LogMessage message)
=> Console.WriteLine(message.ToString());

static bool IsDebug ( )
public static bool IsDebug()
{
#if DEBUG
return true;


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

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

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

+ 48
- 0
samples/MediatRSample/MediatRSample/DiscordEventListener.cs View File

@@ -0,0 +1,48 @@
using Discord.WebSocket;
using MediatR;
using MediatRSample.Notifications;
using Microsoft.Extensions.DependencyInjection;

namespace MediatRSample;

public class DiscordEventListener
{
private readonly CancellationToken _cancellationToken;

private readonly DiscordSocketClient _client;
private readonly IServiceScopeFactory _serviceScope;

public DiscordEventListener(DiscordSocketClient client, IServiceScopeFactory serviceScope)
{
_client = client;
_serviceScope = serviceScope;
_cancellationToken = new CancellationTokenSource().Token;
}

private IMediator Mediator
{
get
{
var scope = _serviceScope.CreateScope();
return scope.ServiceProvider.GetRequiredService<IMediator>();
}
}

public Task StartAsync()
{
_client.Ready += OnReadyAsync;
_client.MessageReceived += OnMessageReceivedAsync;

return Task.CompletedTask;
}

private Task OnMessageReceivedAsync(SocketMessage arg)
{
return Mediator.Publish(new MessageReceivedNotification(arg), _cancellationToken);
}
private Task OnReadyAsync()
{
return Mediator.Publish(ReadyNotification.Default, _cancellationToken);
}
}

+ 14
- 0
samples/MediatRSample/MediatRSample/Handlers/MessageReceivedHandler.cs View File

@@ -0,0 +1,14 @@
using MediatR;
using MediatRSample.Notifications;

namespace MediatRSample.Handlers;

public class MessageReceivedHandler : INotificationHandler<MessageReceivedNotification>
{
public async Task Handle(MessageReceivedNotification notification, CancellationToken cancellationToken)
{
Console.WriteLine($"MediatR works! (Received a message by {notification.Message.Author.Username})");
// Your implementation
}
}

+ 20
- 0
samples/MediatRSample/MediatRSample/MediatRSample.csproj View File

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

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Discord.Net" Version="3.4.1" />
<PackageReference Include="MediatR" Version="10.0.1" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
</ItemGroup>

</Project>

+ 14
- 0
samples/MediatRSample/MediatRSample/Notifications/MessageReceivedNotification.cs View File

@@ -0,0 +1,14 @@
using Discord.WebSocket;
using MediatR;

namespace MediatRSample.Notifications;

public class MessageReceivedNotification : INotification
{
public MessageReceivedNotification(SocketMessage message)
{
Message = message ?? throw new ArgumentNullException(nameof(message));
}

public SocketMessage Message { get; }
}

+ 13
- 0
samples/MediatRSample/MediatRSample/Notifications/ReadyNotification.cs View File

@@ -0,0 +1,13 @@
using MediatR;

namespace MediatRSample.Notifications;

public class ReadyNotification : INotification
{
public static readonly ReadyNotification Default
= new();

private ReadyNotification()
{
}
}

+ 73
- 0
samples/MediatRSample/MediatRSample/Program.cs View File

@@ -0,0 +1,73 @@
using Discord;
using Discord.Interactions;
using Discord.WebSocket;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using Serilog.Events;

namespace MediatRSample;

public class Bot
{
private static ServiceProvider ConfigureServices()
{
return new ServiceCollection()
.AddMediatR(typeof(Bot))
.AddSingleton(new DiscordSocketClient(new DiscordSocketConfig
{
AlwaysDownloadUsers = true,
MessageCacheSize = 100,
GatewayIntents = GatewayIntents.AllUnprivileged,
LogLevel = LogSeverity.Info
}))
.AddSingleton<DiscordEventListener>()
.AddSingleton(x => new InteractionService(x.GetRequiredService<DiscordSocketClient>()))
.BuildServiceProvider();
}

public static async Task Main()
{
await new Bot().RunAsync();
}

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

await using var services = ConfigureServices();

var client = services.GetRequiredService<DiscordSocketClient>();
client.Log += LogAsync;

var listener = services.GetRequiredService<DiscordEventListener>();
await listener.StartAsync();

await client.LoginAsync(TokenType.Bot, "YOUR_TOKEN_HERE");
await client.StartAsync();

await Task.Delay(Timeout.Infinite);
}

private static 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);

return Task.CompletedTask;
}
}

+ 1
- 1
samples/ShardedClient/Modules/InteractionModule.cs View File

@@ -5,7 +5,7 @@ using System.Threading.Tasks;
namespace ShardedClient.Modules
{
// A display of portability, which shows how minimal the difference between the 2 frameworks is.
public class InteractionModule : InteractionModuleBase<ShardedInteractionContext<SocketSlashCommand>>
public class InteractionModule : InteractionModuleBase<ShardedInteractionContext>
{
[SlashCommand("info", "Information about this shard.")]
public async Task InfoAsync()


+ 5
- 2
samples/ShardedClient/Program.cs View File

@@ -45,8 +45,11 @@ namespace ShardedClient
client.ShardReady += ReadyAsync;
client.Log += LogAsync;

await services.GetRequiredService<InteractionHandlingService>().InitializeAsync();
await services.GetRequiredService<CommandHandlingService>().InitializeAsync();
await services.GetRequiredService<InteractionHandlingService>()
.InitializeAsync();

await services.GetRequiredService<CommandHandlingService>()
.InitializeAsync();

// Tokens should be considered secret data, and never hard-coded.
await client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token"));


+ 2
- 2
samples/ShardedClient/Services/InteractionHandlingService.cs View File

@@ -31,9 +31,9 @@ namespace ShardedClient.Services
{
await _service.AddModulesAsync(typeof(InteractionHandlingService).Assembly, _provider);
#if DEBUG
await _service.AddCommandsToGuildAsync(_client.Guilds.First(x => x.Id == 1));
await _service.RegisterCommandsToGuildAsync(1 /* implement */);
#else
await _service.AddCommandsGloballyAsync();
await _service.RegisterCommandsGloballyAsync();
#endif
}



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

@@ -403,6 +403,41 @@ namespace Discord.Commands
AddNullableTypeReader(type, reader);
}
}

/// <summary>
/// Removes a type reader from the list of type readers.
/// </summary>
/// <remarks>
/// Removing a <see cref="TypeReader"/> from the <see cref="CommandService"/> will not dereference the <see cref="TypeReader"/> from the loaded module/command instances.
/// You need to reload the modules for the changes to take effect.
/// </remarks>
/// <param name="type">The type to remove the readers from.</param>
/// <param name="isDefaultTypeReader"><see langword="true"/> if the default readers for <paramref name="type"/> should be removed; otherwise <see langword="false"/>.</param>
/// <param name="readers">The removed collection of type readers.</param>
/// <returns><see langword="true"/> if the remove operation was successful; otherwise <see langword="false"/>.</returns>
public bool TryRemoveTypeReader(Type type, bool isDefaultTypeReader, out IDictionary<Type, TypeReader> readers)
{
readers = new Dictionary<Type, TypeReader>();

if (isDefaultTypeReader)
{
var isSuccess = _defaultTypeReaders.TryRemove(type, out var result);
if (isSuccess)
readers.Add(result?.GetType(), result);

return isSuccess;
}
else
{
var isSuccess = _typeReaders.TryRemove(type, out var result);

if (isSuccess)
readers = result;

return isSuccess;
}
}

internal bool HasDefaultTypeReader(Type type)
{
if (_defaultTypeReaders.ContainsKey(type))


+ 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


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

@@ -97,6 +97,13 @@ namespace Discord
/// </returns>
public const int MaxUsersPerBatch = 1000;
/// <summary>
/// Returns the max bans allowed to be in a request.
/// </summary>
/// <returns>
/// The maximum number of bans that can be gotten per-batch.
/// </returns>
public const int MaxBansPerBatch = 1000;
/// <summary>
/// Returns the max users allowed to be in a request for guild event users.
/// </summary>
/// <returns>
@@ -187,5 +194,15 @@ namespace Discord
/// <b>This will still require a stable clock on your system.</b>
/// </remarks>
public bool UseInteractionSnowflakeDate { get; set; } = true;

/// <summary>
/// Gets or sets if the Rest/Socket user <see cref="object.ToString"/> override formats the string in respect to bidirectional unicode.
/// </summary>
/// <remarks>
/// By default, the returned value will be "?Discord?#1234", to work with bidirectional usernames.
/// <br/>
/// If set to <see langword="false"/>, this value will be "Discord#1234".
/// </remarks>
public bool FormatUsersInBidirectionalUnicode { get; set; } = true;
}
}

+ 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,
@@ -97,11 +98,13 @@ namespace Discord
#endregion

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

#region Action Preconditions/Checks (50XXX)
InteractionHasAlreadyBeenAcknowledged = 40060,
TagNamesMustBeUnique = 40061,
MissingPermissions = 50001,
InvalidAccountType = 50002,
@@ -145,12 +149,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,


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

@@ -1,10 +1,10 @@
namespace Discord
{
/// <summary>
/// Specifies the direction of where message(s) should be retrieved from.
/// Specifies the direction of where entities (e.g. bans/messages) should be retrieved from.
/// </summary>
/// <remarks>
/// This enum is used to specify the direction for retrieving messages.
/// This enum is used to specify the direction for retrieving entities.
/// <note type="important">
/// At the time of writing, <see cref="Around"/> is not yet implemented into
/// <see cref="IMessageChannel.GetMessagesAsync(int, CacheMode, RequestOptions)"/>.
@@ -15,15 +15,15 @@ namespace Discord
public enum Direction
{
/// <summary>
/// The message(s) should be retrieved before a message.
/// The entity(s) should be retrieved before an entity.
/// </summary>
Before,
/// <summary>
/// The message(s) should be retrieved after a message.
/// The entity(s) should be retrieved after an entity.
/// </summary>
After,
/// <summary>
/// The message(s) should be retrieved around a message.
/// The entity(s) should be retrieved around an entity.
/// </summary>
Around
}


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


+ 0
- 21
src/Discord.Net.Core/Entities/Guilds/GuildIntegrationProperties.cs View File

@@ -1,21 +0,0 @@
namespace Discord
{
/// <summary>
/// Provides properties used to modify an <see cref="IGuildIntegration" /> with the specified changes.
/// </summary>
public class GuildIntegrationProperties
{
/// <summary>
/// Gets or sets the behavior when an integration subscription lapses.
/// </summary>
public Optional<int> ExpireBehavior { get; set; }
/// <summary>
/// Gets or sets the period (in seconds) where the integration will ignore lapsed subscriptions.
/// </summary>
public Optional<int> ExpireGracePeriod { get; set; }
/// <summary>
/// Gets or sets whether emoticons should be synced for this integration.
/// </summary>
public Optional<bool> EnableEmoticons { get; set; }
}
}

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

+ 80
- 8
src/Discord.Net.Core/Entities/Guilds/IGuild.cs View File

@@ -409,17 +409,70 @@ namespace Discord
/// A task that represents the asynchronous leave operation.
/// </returns>
Task LeaveAsync(RequestOptions options = null);

/// <summary>
/// Gets a collection of all users banned in this guild.
/// Gets <paramref name="limit"/> amount of bans from the guild ordered by user ID.
/// </summary>
/// <remarks>
/// <note type="important">
/// 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>
/// <note type="warning">
/// Do not fetch too many bans at once! This may cause unwanted preemptive rate limit or even actual
/// rate limit, causing your bot to freeze!
/// </note>
/// </remarks>
/// <param name="limit">The amount of bans to get from the guild.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains a read-only collection of
/// ban objects that this guild currently possesses, with each object containing the user banned and reason
/// behind the ban.
/// A paged collection of bans.
/// </returns>
Task<IReadOnlyCollection<IBan>> GetBansAsync(RequestOptions options = null);
IAsyncEnumerable<IReadOnlyCollection<IBan>> GetBansAsync(int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null);
/// <summary>
/// Gets <paramref name="limit"/> amount of bans from the guild starting at the provided <paramref name="fromUserId"/> ordered by user ID.
/// </summary>
/// <remarks>
/// <note type="important">
/// 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>
/// <note type="warning">
/// Do not fetch too many bans at once! This may cause unwanted preemptive rate limit or even actual
/// rate limit, causing your bot to freeze!
/// </note>
/// </remarks>
/// <param name="fromUserId">The ID of the user to start to get bans from.</param>
/// <param name="dir">The direction of the bans to be gotten.</param>
/// <param name="limit">The number of bans to get.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A paged collection of bans.
/// </returns>
IAsyncEnumerable<IReadOnlyCollection<IBan>> GetBansAsync(ulong fromUserId, Direction dir, int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null);
/// <summary>
/// Gets <paramref name="limit"/> amount of bans from the guild starting at the provided <paramref name="fromUser"/> ordered by user ID.
/// </summary>
/// <remarks>
/// <note type="important">
/// 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>
/// <note type="warning">
/// Do not fetch too many bans at once! This may cause unwanted preemptive rate limit or even actual
/// rate limit, causing your bot to freeze!
/// </note>
/// </remarks>
/// <param name="fromUser">The user to start to get bans from.</param>
/// <param name="dir">The direction of the bans to be gotten.</param>
/// <param name="limit">The number of bans to get.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A paged collection of bans.
/// </returns>
IAsyncEnumerable<IReadOnlyCollection<IBan>> GetBansAsync(IUser fromUser, Direction dir, int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null);
/// <summary>
/// Gets a ban object for a banned user.
/// </summary>
@@ -718,8 +771,25 @@ namespace Discord
/// </returns>
Task<IReadOnlyCollection<IVoiceRegion>> GetVoiceRegionsAsync(RequestOptions options = null);

Task<IReadOnlyCollection<IGuildIntegration>> GetIntegrationsAsync(RequestOptions options = null);
Task<IGuildIntegration> CreateIntegrationAsync(ulong id, string type, RequestOptions options = null);
/// <summary>
/// Gets a collection of all the integrations this guild contains.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains a read-only collection of
/// integrations the guild can has.
/// </returns>
Task<IReadOnlyCollection<IIntegration>> GetIntegrationsAsync(RequestOptions options = null);

/// <summary>
/// Deletes an integration.
/// </summary>
/// <param name="id">The id for the integration.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous removal operation.
/// </returns>
Task DeleteIntegrationAsync(ulong id, RequestOptions options = null);

/// <summary>
/// Gets a collection of all invites in this guild.
@@ -1105,6 +1175,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 +1189,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>


+ 0
- 18
src/Discord.Net.Core/Entities/Guilds/IntegrationAccount.cs View File

@@ -1,18 +0,0 @@
using System.Diagnostics;

namespace Discord
{
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public struct IntegrationAccount
{
/// <summary> Gets the ID of the account. </summary>
/// <returns> A <see cref="string"/> unique identifier of this integration account. </returns>
public string Id { get; }
/// <summary> Gets the name of the account. </summary>
/// <returns> A string containing the name of this integration account. </returns>
public string Name { get; private set; }

public override string ToString() => Name;
private string DebuggerDisplay => $"{Name} ({Id})";
}
}

src/Discord.Net.Core/Entities/Guilds/IGuildIntegration.cs → src/Discord.Net.Core/Entities/Integrations/IIntegration.cs View File

@@ -3,15 +3,16 @@ using System;
namespace Discord
{
/// <summary>
/// Holds information for a guild integration feature.
/// Holds information for an integration feature.
/// Nullable fields not provided for Discord bot integrations, but are for Twitch etc.
/// </summary>
public interface IGuildIntegration
public interface IIntegration
{
/// <summary>
/// Gets the integration ID.
/// </summary>
/// <returns>
/// An <see cref="UInt64"/> representing the unique identifier value of this integration.
/// A <see cref="ulong"/> representing the unique identifier value of this integration.
/// </returns>
ulong Id { get; }
/// <summary>
@@ -45,30 +46,52 @@ namespace Discord
/// <returns>
/// <c>true</c> if this integration is syncing; otherwise <c>false</c>.
/// </returns>
bool IsSyncing { get; }
bool? IsSyncing { get; }
/// <summary>
/// Gets the ID that this integration uses for "subscribers".
/// </summary>
ulong ExpireBehavior { get; }
ulong? RoleId { get; }
/// <summary>
/// Gets whether emoticons should be synced for this integration (twitch only currently).
/// </summary>
bool? HasEnabledEmoticons { get; }
/// <summary>
/// Gets the behavior of expiring subscribers.
/// </summary>
IntegrationExpireBehavior? ExpireBehavior { get; }
/// <summary>
/// Gets the grace period before expiring "subscribers".
/// </summary>
ulong ExpireGracePeriod { get; }
int? ExpireGracePeriod { get; }
/// <summary>
/// Gets the user for this integration.
/// </summary>
IUser User { get; }
/// <summary>
/// Gets integration account information.
/// </summary>
IIntegrationAccount Account { get; }
/// <summary>
/// Gets when this integration was last synced.
/// </summary>
/// <returns>
/// A <see cref="DateTimeOffset"/> containing a date and time of day when the integration was last synced.
/// </returns>
DateTimeOffset SyncedAt { get; }
DateTimeOffset? SyncedAt { get; }
/// <summary>
/// Gets integration account information.
/// Gets how many subscribers this integration has.
/// </summary>
IntegrationAccount Account { get; }

int? SubscriberCount { get; }
/// <summary>
/// Gets whether this integration been revoked.
/// </summary>
bool? IsRevoked { get; }
/// <summary>
/// Gets the bot/OAuth2 application for a discord integration.
/// </summary>
IIntegrationApplication Application { get; }
IGuild Guild { get; }
ulong GuildId { get; }
ulong RoleId { get; }
IUser User { get; }
}
}

+ 23
- 0
src/Discord.Net.Core/Entities/Integrations/IIntegrationAccount.cs View File

@@ -0,0 +1,23 @@
namespace Discord
{
/// <summary>
/// Provides the account information for an <see cref="IIntegration" />.
/// </summary>
public interface IIntegrationAccount
{
/// <summary>
/// Gets the ID of the account.
/// </summary>
/// <returns>
/// A <see cref="string"/> unique identifier of this integration account.
/// </returns>
string Id { get; }
/// <summary>
/// Gets the name of the account.
/// </summary>
/// <returns>
/// A string containing the name of this integration account.
/// </returns>
string Name { get; }
}
}

+ 33
- 0
src/Discord.Net.Core/Entities/Integrations/IIntegrationApplication.cs View File

@@ -0,0 +1,33 @@
namespace Discord
{
/// <summary>
/// Provides the bot/OAuth2 application for an <see cref="IIntegration" />.
/// </summary>
public interface IIntegrationApplication
{
/// <summary>
/// Gets the id of the app.
/// </summary>
ulong Id { get; }
/// <summary>
/// Gets the name of the app.
/// </summary>
string Name { get; }
/// <summary>
/// Gets the icon hash of the app.
/// </summary>
string Icon { get; }
/// <summary>
/// Gets the description of the app.
/// </summary>
string Description { get; }
/// <summary>
/// Gets the summary of the app.
/// </summary>
string Summary { get; }
/// <summary>
/// Gets the bot associated with this application.
/// </summary>
IUser Bot { get; }
}
}

+ 17
- 0
src/Discord.Net.Core/Entities/Integrations/IntegrationExpireBehavior.cs View File

@@ -0,0 +1,17 @@
namespace Discord
{
/// <summary>
/// The behavior of expiring subscribers for an <see cref="IIntegration" />.
/// </summary>
public enum IntegrationExpireBehavior
{
/// <summary>
/// Removes a role from an expired subscriber.
/// </summary>
RemoveRole = 0,
/// <summary>
/// Kicks an expired subscriber from the guild.
/// </summary>
Kick = 1
}
}

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


+ 20
- 1
src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteraction.cs View File

@@ -1,3 +1,6 @@
using System;
using System.Threading.Tasks;

namespace Discord
{
/// <summary>
@@ -6,7 +9,7 @@ namespace Discord
public interface IComponentInteraction : IDiscordInteraction
{
/// <summary>
/// Gets the data received with this interaction, contains the button that was clicked.
/// Gets the data received with this component interaction.
/// </summary>
new IComponentInteractionData Data { get; }

@@ -14,5 +17,21 @@ namespace Discord
/// Gets the message that contained the trigger for this interaction.
/// </summary>
IUserMessage Message { get; }

/// <summary>
/// Updates the message which this component resides in with the type <see cref="InteractionResponseType.UpdateMessage"/>
/// </summary>
/// <param name="func">A delegate containing the properties to modify the message with.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>A task that represents the asynchronous operation of updating the message.</returns>
Task UpdateAsync(Action<MessageProperties> func, RequestOptions options = null);

/// <summary>
/// Defers an interaction with the response type 5 (<see cref="InteractionResponseType.DeferredChannelMessageWithSource"/>).
/// </summary>
/// <param name="ephemeral"><see langword="true"/> to defer ephemerally, otherwise <see langword="false"/>.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>A task that represents the asynchronous operation of acknowledging the interaction.</returns>
Task DeferLoadingAsync(bool ephemeral = false, RequestOptions options = null);
}
}

+ 16
- 0
src/Discord.Net.Core/Entities/Messages/FileAttachment.cs View File

@@ -7,13 +7,29 @@ using System.Threading.Tasks;

namespace Discord
{
/// <summary>
/// Represents an outgoing file attachment used to send a file to discord.
/// </summary>
public struct FileAttachment : IDisposable
{
/// <summary>
/// Gets or sets the filename.
/// </summary>
public string FileName { get; set; }
/// <summary>
/// Gets or sets the description of the file.
/// </summary>
public string Description { get; set; }

/// <summary>
/// Gets or sets whether this file should be marked as a spoiler.
/// </summary>
public bool IsSpoiler { get; set; }

#pragma warning disable IDISP008
/// <summary>
/// Gets the stream containing the file content.
/// </summary>
public Stream Stream { get; }
#pragma warning restore IDISP008



+ 8
- 0
src/Discord.Net.Core/Entities/Messages/IAttachment.cs View File

@@ -62,5 +62,13 @@ namespace Discord
/// <see langword="true"/> if the attachment is ephemeral; otherwise <see langword="false"/>.
/// </returns>
bool Ephemeral { get; }
/// <summary>
/// Gets the description of the attachment; or <see langword="null"/> if there is none set.
/// </summary>
string Description { get; }
/// <summary>
/// Gets the media's <see href="https://en.wikipedia.org/wiki/Media_type">MIME type</see> if present; otherwise <see langword="null"/>.
/// </summary>
string ContentType { get; }
}
}

+ 1
- 1
src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs View File

@@ -13,7 +13,7 @@ namespace Discord
/// <summary> Gets a <see cref="GuildPermissions"/> that grants all guild permissions for webhook users. </summary>
public static readonly GuildPermissions Webhook = new GuildPermissions(0b0_00000_0000000_0000000_0001101100000_000000);
/// <summary> Gets a <see cref="GuildPermissions"/> that grants all guild permissions. </summary>
public static readonly GuildPermissions All = new GuildPermissions(0b1_11111_1111111_1111111_1111111111111_111111);
public static readonly GuildPermissions All = new GuildPermissions(ulong.MaxValue);

/// <summary> Gets a packed value representing all the permissions in this <see cref="GuildPermissions"/>. </summary>
public ulong RawValue { get; }


+ 17
- 0
src/Discord.Net.Core/Entities/Users/ConnectionVisibility.cs View File

@@ -0,0 +1,17 @@
namespace Discord
{
/// <summary>
/// The visibility of the connected account.
/// </summary>
public enum ConnectionVisibility
{
/// <summary>
/// Invisible to everyone except the user themselves.
/// </summary>
None = 0,
/// <summary>
/// Visible to everyone.
/// </summary>
Everyone = 1
}
}

+ 44
- 15
src/Discord.Net.Core/Entities/Users/IConnection.cs View File

@@ -4,24 +4,53 @@ namespace Discord
{
public interface IConnection
{
/// <summary> Gets the ID of the connection account. </summary>
/// <returns> A <see cref="string"/> representing the unique identifier value of this connection. </returns>
/// <summary>
/// Gets the ID of the connection account.
/// </summary>
/// <returns>
/// A <see cref="string"/> representing the unique identifier value of this connection.
/// </returns>
string Id { get; }
/// <summary> Gets the service of the connection (twitch, youtube). </summary>
/// <returns> A string containing the name of this type of connection. </returns>
string Type { get; }
/// <summary> Gets the username of the connection account. </summary>
/// <returns> A string containing the name of this connection. </returns>
/// <summary>
/// Gets the username of the connection account.
/// </summary>
/// <returns>
/// A string containing the name of this connection.
/// </returns>
string Name { get; }
/// <summary> Gets whether the connection is revoked. </summary>
/// <returns> A value which if true indicates that this connection has been revoked, otherwise false. </returns>
bool IsRevoked { get; }

/// <summary> Gets a <see cref="IReadOnlyCollection{T}"/> of integration IDs. </summary>
/// <summary>
/// Gets the service of the connection (twitch, youtube).
/// </summary>
/// <returns>
/// A string containing the name of this type of connection.
/// </returns>
string Type { get; }
/// <summary>
/// Gets whether the connection is revoked.
/// </summary>
/// <returns>
/// An <see cref="IReadOnlyCollection{T}"/> containing <see cref="ulong"/>
/// representations of unique identifier values of integrations.
/// A value which if true indicates that this connection has been revoked, otherwise false.
/// </returns>
IReadOnlyCollection<ulong> IntegrationIds { get; }
bool? IsRevoked { get; }
/// <summary>
/// Gets a <see cref="IReadOnlyCollection{T}"/> of integration parials.
/// </summary>
IReadOnlyCollection<IIntegration> Integrations { get; }
/// <summary>
/// Gets whether the connection is verified.
/// </summary>
bool Verified { get; }
/// <summary>
/// Gets whether friend sync is enabled for this connection.
/// </summary>
bool FriendSync { get; }
/// <summary>
/// Gets whether activities related to this connection will be shown in presence updates.
/// </summary>
bool ShowActivity { get; }
/// <summary>
/// Visibility of this connection.
/// </summary>
ConnectionVisibility Visibility { get; }
}
}

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

@@ -107,13 +107,16 @@ namespace Discord
}

/// <summary>
/// Formats a user's username + discriminator while maintaining bidirectional unicode
/// Formats a user's username + discriminator.
/// </summary>
/// <param name="doBidirectional">To format the string in bidirectional unicode or not</param>
/// <param name="user">The user whos username and discriminator to format</param>
/// <returns>The username + discriminator</returns>
public static string UsernameAndDiscriminator(IUser user)
public static string UsernameAndDiscriminator(IUser user, bool doBidirectional)
{
return $"\u2066{user.Username}\u2069#{user.Discriminator}";
return doBidirectional
? $"\u2066{user.Username}\u2069#{user.Discriminator}"
: $"{user.Username}#{user.Discriminator}";
}
}
}

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

+ 5
- 1
src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs View File

@@ -10,9 +10,10 @@ namespace Discord.Interactions
/// </summary>
/// <typeparam name="T">Type of the <see cref="IModal"/> implementation.</typeparam>
/// <param name="interaction">The interaction to respond to.</param>
/// <param name="modifyModal">Delegate that can be used to modify the modal.</param>
/// <param name="options">The request options for this <see langword="async"/> request.</param>
/// <returns>A task that represents the asynchronous operation of responding to the interaction.</returns>
public static async Task RespondWithModalAsync<T>(this IDiscordInteraction interaction, string customId, RequestOptions options = null)
public static async Task RespondWithModalAsync<T>(this IDiscordInteraction interaction, string customId, RequestOptions options = null, Action<ModalBuilder> modifyModal = null)
where T : class, IModal
{
if (!ModalUtils.TryGet<T>(out var modalInfo))
@@ -31,6 +32,9 @@ namespace Discord.Interactions
throw new InvalidOperationException($"{input.GetType().FullName} isn't a valid component info class");
}

if (modifyModal is not null)
modifyModal(builder);

await interaction.RespondWithModalAsync(builder.Build(), options).ConfigureAwait(false);
}
}


+ 25
- 0
src/Discord.Net.Interactions/Extensions/RestExtensions.cs View File

@@ -0,0 +1,25 @@
using Discord.Interactions;
using System;

namespace Discord.Rest
{
public static class RestExtensions
{
/// <summary>
/// Respond to an interaction with a <see cref="IModal"/>.
/// </summary>
/// <typeparam name="T">Type of the <see cref="IModal"/> implementation.</typeparam>
/// <param name="interaction">The interaction to respond to.</param>
/// <param name="options">The request options for this <see langword="async"/> request.</param>
/// <returns>Serialized payload to be used to create a HTTP response.</returns>
public static string RespondWithModal<T>(this RestInteraction interaction, string customId, RequestOptions options = null, Action<ModalBuilder> modifyModal = null)
where T : class, IModal
{
if (!ModalUtils.TryGet<T>(out var modalInfo))
throw new ArgumentException($"{typeof(T).FullName} isn't referenced by any registered Modal Interaction Command and doesn't have a cached {typeof(ModalInfo)}");

var modal = modalInfo.ToModal(customId, modifyModal);
return interaction.RespondWithModal(modal, options);
}
}
}

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

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

+ 228
- 84
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;
@@ -23,6 +24,29 @@ namespace Discord.Interactions
public event Func<LogMessage, Task> Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } }
internal readonly AsyncEvent<Func<LogMessage, Task>> _logEvent = new ();

/// <summary>
/// Occurs when any type of interaction is executed.
/// </summary>
public event Func<ICommandInfo, IInteractionContext, IResult, Task> InteractionExecuted
{
add
{
SlashCommandExecuted += value;
ContextCommandExecuted += value;
ComponentCommandExecuted += value;
AutocompleteCommandExecuted += value;
ModalCommandExecuted += value;
}
remove
{
SlashCommandExecuted -= value;
ContextCommandExecuted -= value;
ComponentCommandExecuted -= value;
AutocompleteCommandExecuted -= value;
ModalCommandExecuted -= value;
}
}

/// <summary>
/// Occurs when a Slash Command is executed.
/// </summary>
@@ -66,8 +90,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 +204,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 +334,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 +367,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 +463,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 +490,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>
@@ -677,7 +718,7 @@ namespace Discord.Interactions
public async Task<IResult> ExecuteCommandAsync (IInteractionContext context, IServiceProvider services)
{
var interaction = context.Interaction;
return interaction switch
{
ISlashCommandInteraction slashCommand => await ExecuteSlashCommandAsync(context, slashCommand, services).ConfigureAwait(false),
@@ -747,9 +788,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);
}
}
@@ -783,47 +822,24 @@ namespace Discord.Interactions
return await result.Command.ExecuteAsync(context, services, result.RegexCaptureGroups).ConfigureAwait(false);
}

internal TypeConverter GetTypeConverter (Type type, IServiceProvider services = null)
{
if (_typeConverters.TryGetValue(type, out var specific))
return specific;
else if (_genericTypeConverters.Any(x => x.Key.IsAssignableFrom(type)
|| (x.Key.IsGenericTypeDefinition && type.IsGenericType && x.Key.GetGenericTypeDefinition() == type.GetGenericTypeDefinition())))
{
services ??= EmptyServiceProvider.Instance;

var converterType = GetMostSpecificTypeConverter(type);
var converter = ReflectionUtils<TypeConverter>.CreateObject(converterType.MakeGenericType(type).GetTypeInfo(), this, services);
_typeConverters[type] = converter;
return converter;
}

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

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}"/>.
@@ -831,30 +847,173 @@ 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);

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

/// <summary>
/// Removes a type reader for the type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type to remove the readers from.</typeparam>
/// <param name="reader">The reader if the resulting remove operation was successful.</param>
/// <returns><see langword="true"/> if the remove operation was successful; otherwise <see langword="false"/>.</returns>
public bool TryRemoveTypeReader<T>(out TypeReader reader)
=> TryRemoveTypeReader(typeof(T), out reader);

/// <summary>
/// Removes a type reader for the given type.
/// </summary>
/// <remarks>
/// Removing a <see cref="TypeReader"/> from the <see cref="CommandService"/> will not dereference the <see cref="TypeReader"/> from the loaded module/command instances.
/// You need to reload the modules for the changes to take effect.
/// </remarks>
/// <param name="type">The type to remove the reader from.</param>
/// <param name="reader">The reader if the resulting remove operation was successful.</param>
/// <returns><see langword="true"/> if the remove operation was successful; otherwise <see langword="false"/>.</returns>
public bool TryRemoveTypeReader(Type type, out TypeReader reader)
=> _typeReaderMap.TryRemoveConcrete(type, out reader);

/// <summary>
/// Removes a generic type reader from the type <typeparamref name="T"/>.
/// </summary>
/// <remarks>
/// Removing a <see cref="TypeReader"/> from the <see cref="CommandService"/> will not dereference the <see cref="TypeReader"/> from the loaded module/command instances.
/// You need to reload the modules for the changes to take effect.
/// </remarks>
/// <typeparam name="T">The type to remove the readers from.</typeparam>
/// <param name="readerType">The removed readers type.</param>
/// <returns><see langword="true"/> if the remove operation was successful; otherwise <see langword="false"/>.</returns>
public bool TryRemoveGenericTypeReader<T>(out Type readerType)
=> TryRemoveGenericTypeReader(typeof(T), out readerType);

var genericArguments = converterType.GetGenericArguments();
/// <summary>
/// Removes a generic type reader from the given type.
/// </summary>
/// <remarks>
/// Removing a <see cref="TypeReader"/> from the <see cref="CommandService"/> will not dereference the <see cref="TypeReader"/> from the loaded module/command instances.
/// You need to reload the modules for the changes to take effect.
/// </remarks>
/// <param name="type">The type to remove the reader from.</param>
/// <param name="readerType">The readers type if the remove operation was successful.</param>
/// <returns><see langword="true"/> if the remove operation was successful; otherwise <see langword="false"/>.</returns>
public bool TryRemoveGenericTypeReader(Type type, out Type readerType)
=> _typeReaderMap.TryRemoveGeneric(type, out 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>
/// <remarks>
/// Removing a <see cref="TypeReader"/> from the <see cref="CommandService"/> will not dereference the <see cref="TypeReader"/> from the loaded module/command instances.
/// You need to reload the modules for the changes to take effect.
/// </remarks>
/// <typeparam name="T">Type of the object to be serialized.</typeparam>
/// <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>
@@ -872,7 +1031,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)
@@ -1018,7 +1177,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)];

@@ -1034,21 +1193,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.


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

Loading…
Cancel
Save