Browse Source

Merge branch 'dev' into patch-C995E81D11

pull/2447/head
Quin Lynch GitHub 2 years ago
parent
commit
22d8198067
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
100 changed files with 2032 additions and 687 deletions
  1. +2
    -0
      .github/FUNDING.yml
  2. +10
    -2
      .github/ISSUE_TEMPLATE/bugreport.yml
  3. +3
    -0
      .gitmodules
  4. +162
    -0
      CHANGELOG.md
  5. +1
    -2
      CONTRIBUTING.md
  6. +16
    -1
      Discord.Net.sln
  7. +6
    -3
      Discord.Net.targets
  8. +54
    -32
      README.md
  9. +1
    -0
      azure/deploy.yml
  10. +1
    -1
      docs/docfx.json
  11. +1
    -1
      docs/faq/basics/client-basics.md
  12. +41
    -0
      docs/faq/build_overrides/what-are-they.md
  13. +2
    -0
      docs/faq/toc.yml
  14. +69
    -0
      docs/guides/dependency_injection/basics.md
  15. BIN
      docs/guides/dependency_injection/images/manager.png
  16. +44
    -0
      docs/guides/dependency_injection/injection.md
  17. +9
    -0
      docs/guides/dependency_injection/samples/access-activator.cs
  18. +13
    -0
      docs/guides/dependency_injection/samples/collection.cs
  19. +14
    -0
      docs/guides/dependency_injection/samples/ctor-injecting.cs
  20. +18
    -0
      docs/guides/dependency_injection/samples/enumeration.cs
  21. +12
    -0
      docs/guides/dependency_injection/samples/implicit-registration.cs
  22. +16
    -0
      docs/guides/dependency_injection/samples/modules.cs
  23. +24
    -0
      docs/guides/dependency_injection/samples/program.cs
  24. +9
    -0
      docs/guides/dependency_injection/samples/property-injecting.cs
  25. +26
    -0
      docs/guides/dependency_injection/samples/provider.cs
  26. +17
    -0
      docs/guides/dependency_injection/samples/runasync.cs
  27. +6
    -0
      docs/guides/dependency_injection/samples/scoped.cs
  28. +21
    -0
      docs/guides/dependency_injection/samples/service-registration.cs
  29. +9
    -0
      docs/guides/dependency_injection/samples/services.cs
  30. +6
    -0
      docs/guides/dependency_injection/samples/singleton.cs
  31. +6
    -0
      docs/guides/dependency_injection/samples/transient.cs
  32. +39
    -0
      docs/guides/dependency_injection/scaling.md
  33. +48
    -0
      docs/guides/dependency_injection/services.md
  34. +52
    -0
      docs/guides/dependency_injection/types.md
  35. +7
    -1
      docs/guides/deployment/deployment.md
  36. +1
    -1
      docs/guides/getting_started/first-bot.md
  37. +14
    -6
      docs/guides/getting_started/installing.md
  38. +0
    -30
      docs/guides/getting_started/labs.md
  39. +8
    -4
      docs/guides/getting_started/terminology.md
  40. +1
    -1
      docs/guides/int_basics/application-commands/slash-commands/choice-slash-command.md
  41. +3
    -1
      docs/guides/int_basics/application-commands/slash-commands/parameters.md
  42. +1
    -1
      docs/guides/int_basics/modals/intro.md
  43. +2
    -0
      docs/guides/int_framework/autocompletion.md
  44. +0
    -13
      docs/guides/int_framework/dependency-injection.md
  45. +93
    -5
      docs/guides/int_framework/intro.md
  46. +59
    -0
      docs/guides/int_framework/permissions.md
  47. +20
    -0
      docs/guides/int_framework/samples/autocompletion/autocomplete-example.cs
  48. +15
    -3
      docs/guides/int_framework/samples/intro/autocomplete.cs
  49. +37
    -0
      docs/guides/int_framework/samples/intro/complexparams.cs
  50. +14
    -0
      docs/guides/int_framework/samples/intro/event.cs
  51. +5
    -3
      docs/guides/int_framework/samples/intro/groupattribute.cs
  52. +26
    -0
      docs/guides/int_framework/samples/intro/groupmodule.cs
  53. +10
    -3
      docs/guides/int_framework/samples/intro/modal.cs
  54. +6
    -0
      docs/guides/int_framework/samples/permissions/guild-only.cs
  55. +7
    -0
      docs/guides/int_framework/samples/permissions/guild-perms.cs
  56. +16
    -0
      docs/guides/int_framework/samples/permissions/perm-nesting.cs
  57. +4
    -0
      docs/guides/int_framework/samples/permissions/perm-stacking.cs
  58. +1
    -1
      docs/guides/int_framework/samples/postexecution/error_review.cs
  59. BIN
      docs/guides/other_libs/images/mediatr_output.png
  60. +70
    -0
      docs/guides/other_libs/mediatr.md
  61. +1
    -0
      docs/guides/other_libs/samples/MediatrConfiguringDI.cs
  62. +16
    -0
      docs/guides/other_libs/samples/MediatrCreatingMessageNotification.cs
  63. +46
    -0
      docs/guides/other_libs/samples/MediatrDiscordEventListener.cs
  64. +17
    -0
      docs/guides/other_libs/samples/MediatrMessageReceivedHandler.cs
  65. +4
    -0
      docs/guides/other_libs/samples/MediatrStartListener.cs
  66. +0
    -51
      docs/guides/text_commands/dependency-injection.md
  67. +1
    -1
      docs/guides/text_commands/intro.md
  68. +0
    -65
      docs/guides/text_commands/samples/dependency-injection/dependency_map_setup.cs
  69. +0
    -37
      docs/guides/text_commands/samples/dependency-injection/dependency_module.cs
  70. +0
    -29
      docs/guides/text_commands/samples/dependency-injection/dependency_module_noinject.cs
  71. +16
    -7
      docs/guides/toc.yml
  72. +1
    -1
      docs/guides/v2_v3_guide/v2_to_v3_guide.md
  73. +2
    -4
      docs/guides/voice/sending-voice.md
  74. +1
    -1
      docs/index.md
  75. +278
    -0
      experiment/Discord.Net.BuildOverrides/BuildOverrides.cs
  76. +20
    -0
      experiment/Discord.Net.BuildOverrides/Discord.Net.BuildOverrides.csproj
  77. +34
    -0
      experiment/Discord.Net.BuildOverrides/IOverride.cs
  78. +30
    -0
      experiment/Discord.Net.BuildOverrides/OverrideContext.cs
  79. +1
    -0
      overrides/Discord.Net.BuildOverrides
  80. +3
    -3
      samples/BasicBot/_BasicBot.csproj
  81. +0
    -152
      samples/InteractionFramework/CommandHandler.cs
  82. +20
    -0
      samples/InteractionFramework/Enums/ExampleEnum.cs
  83. +0
    -10
      samples/InteractionFramework/ExampleEnum.cs
  84. +81
    -0
      samples/InteractionFramework/InteractionHandler.cs
  85. +0
    -18
      samples/InteractionFramework/Modules/ComponentModule.cs
  86. +49
    -37
      samples/InteractionFramework/Modules/ExampleModule.cs
  87. +0
    -30
      samples/InteractionFramework/Modules/MessageCommandModule.cs
  88. +0
    -51
      samples/InteractionFramework/Modules/SlashCommandModule.cs
  89. +0
    -17
      samples/InteractionFramework/Modules/UserCommandModule.cs
  90. +33
    -42
      samples/InteractionFramework/Program.cs
  91. +2
    -8
      samples/InteractionFramework/_InteractionFramework.csproj
  92. +48
    -0
      samples/MediatRSample/MediatRSample/DiscordEventListener.cs
  93. +14
    -0
      samples/MediatRSample/MediatRSample/Handlers/MessageReceivedHandler.cs
  94. +20
    -0
      samples/MediatRSample/MediatRSample/MediatRSample.csproj
  95. +14
    -0
      samples/MediatRSample/MediatRSample/Notifications/MessageReceivedNotification.cs
  96. +13
    -0
      samples/MediatRSample/MediatRSample/Notifications/ReadyNotification.cs
  97. +73
    -0
      samples/MediatRSample/MediatRSample/Program.cs
  98. +1
    -1
      samples/ShardedClient/Modules/InteractionModule.cs
  99. +5
    -2
      samples/ShardedClient/Program.cs
  100. +10
    -5
      samples/ShardedClient/Services/InteractionHandlingService.cs

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

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

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

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

+ 162
- 0
CHANGELOG.md View File

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

## [3.8.0] - 2022-08-27
### Added
- #2384 Added support for the WEBHOOKS_UPDATED event (010e8e8)
- #2370 Add async callbacks for IModuleBase (503fa75)
- #2367 Added DeleteMessagesAsync for TIV and added remaining rate limit in client log (f178660)
- #2379 Added Max/Min length fields for ApplicationCommandOption (e551431)
- #2369 Added support for using `RespondWithModalAsync<IModal>()` without prior IModal declaration (500e7b4)
- #2347 Added Embed field comparison operators (89a8ea1)
- #2359 Added support for creating lottie stickers (32b03c8)
- #2395 Added App Command localization support and `ILocalizationManager` to IF (39bbd29)

### Fixed
- #2425 Fix missing Fact attribute in ColorTests (92215b1)
- #2424 Fix IGuild.GetBansAsync() (b7b7964)
- #2416 Fix role icon & emoji assignment (b6b5e95)
- #2414 Fix NRE on RestCommandBase Data (02bc3b7)
- #2421 Fix placeholder length being hardcoded (8dfe19f)
- #2352 Fix issues related to the absence of bot scope (1eb42c6)
- #2346 Fix IGuild.DisconnectAsync(IUser) not disconnecting users (ba02416)
- #2404 Fix range of issues presented by 3rd party analyzer (902326d)
- #2409 Removes GroupContext from requirecontext (b0b8167)

### Misc
- #2366 Fixed typo in ChannelUpdatedEvent's documentation (cfd2662)
- #2408 Fix sharding sample throwing at appcommand registration (519deda)
- #2420 Fix broken code snippet in dependency injection docs (ddcf68a)
- #2430 Add a note about DontAutoRegisterAttribute (917118d)
- #2418 Update xmldocs to reflect the ConnectedUsers split (65b98f8)
- #2415 Adds missing DI entries in TOC (c49d483)
- #2407 Introduces high quality dependency injection documentation (6fdcf98)
- #2348 Added `RequiredInput` attribute to example in int.framework intro (ee6e0ad)
- #2385 Add ServerStarter.Host to deployment.md (06ed995)
- #2405 Add a note about `IgnoreGroupNames` to IF docs (cf25acd)
- #2356 Makes voice section about precompiled binaries more visible (e0d68d4 )
- #2405 IF intro docs improvements (246282d)
- #2406 Labs deprecation & readme/docs edits (bf493ea)

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

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

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

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

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

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

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

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

### Misc
- 3.6.0 (27226f0)


## [3.6.0] - 2022-04-28
### Added
- #2136 Passing CustomId matches into contexts (4ce1801)
- #2222 V2 Permissions (d98b3cc)

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

## [3.5.0] - 2022-04-05

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


+ 1
- 2
CONTRIBUTING.md View File

@@ -7,8 +7,7 @@ following guidelines when possible:
## Development Cycle

We prefer all changes to the library to be discussed beforehand,
either in a GitHub issue, or in a discussion in our Discord channel
with library regulars or other contributors.
either in a GitHub issue, or in a discussion in our [Discord server](https://discord.gg/dnet)

Issues that are tagged as "up for grabs" are free to be picked up by
any member of the community.


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


+ 6
- 3
Discord.Net.targets View File

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

+ 54
- 32
README.md View File

@@ -1,4 +1,3 @@
# Discord.Net
<p align="center">
<a href="https://discordnet.dev/" title="Click to visit the documentation!">
<img src="https://raw.githubusercontent.com/discord-net/Discord.Net/dev/docs/marketing/logo/SVG/Combinationmark%20White%20Border.svg" alt="Logo">
@@ -18,13 +17,13 @@
<img src="https://discord.com/api/guilds/848176216011046962/widget.png" alt="Discord">
</a>
</p>
Discord NET is an unofficial .NET API Wrapper for the Discord client (https://discord.com).
Discord.Net is an unofficial .NET API Wrapper for the Discord client (https://discord.com).

## Documentation
## 📄 Documentation

- [Nightly](https://discordnet.dev)
- https://discordnet.dev

## Installation
## 📥 Installation

### Stable (NuGet)

@@ -34,55 +33,78 @@ Our stable builds available from NuGet through the Discord.Net metapackage:

The individual components may also be installed from NuGet:

- [Discord.Net.Commands](https://www.nuget.org/packages/Discord.Net.Commands/)
- [Discord.Net.Rest](https://www.nuget.org/packages/Discord.Net.Rest/)
- [Discord.Net.WebSocket](https://www.nuget.org/packages/Discord.Net.WebSocket/)
- [Discord.Net.Webhook](https://www.nuget.org/packages/Discord.Net.Webhook/)
- _Webhooks_
- [Discord.Net.Webhook](https://www.nuget.org/packages/Discord.Net.Webhook/)

### Unstable (MyGet)
- _Text-Command & Interaction services._
- [Discord.Net.Commands](https://www.nuget.org/packages/Discord.Net.Commands/)
- [Discord.Net.Interactions](https://www.nuget.org/packages/Discord.Net.Interactions/)

Nightly builds are available through our MyGet feed (`https://www.myget.org/F/discord-net/api/v3/index.json`).
- _Complete API coverage._
- [Discord.Net.WebSocket](https://www.nuget.org/packages/Discord.Net.WebSocket/)
- [Discord.Net.Rest](https://www.nuget.org/packages/Discord.Net.Rest/)

### Unstable (Labs)
- _The API core. Implements only entities and barebones functionality._
- [Discord.Net.Core](https://www.nuget.org/packages/Discord.Net.Core/)

Labs builds are available on nuget (`https://www.nuget.org/packages/Discord.Net.Labs/`) and myget (`https://www.myget.org/F/discord-net-labs/api/v3/index.json`).
### Unstable

## Compiling
Nightly builds are available through our MyGet feed (`https://www.myget.org/F/discord-net/api/v3/index.json`).
These builds target the dev branch.

In order to compile Discord.Net, you require the following:
## 🛑 Known Issues

### Using Visual Studio
### WebSockets (Win7 and earlier)

- [Visual Studio 2017](https://www.microsoft.com/net/core#windowsvs2017)
- [.NET Core SDK](https://www.microsoft.com/net/download/core)
.NET Core 1.1 does not support WebSockets on Win7 and earlier.
This issue has been fixed since the release of .NET Core 2.1.
It is recommended to target .NET Core 2.1 or above for your project if you wish to run your bot on legacy platforms;
alternatively, you may choose to install the
[Discord.Net.Providers.WS4Net](https://www.nuget.org/packages/Discord.Net.Providers.WS4Net/) package.

The .NET Core workload must be selected during Visual Studio installation.
### TLS on .NET Framework.

### Using Command Line
Discord supports only TLS1.2+ on all their websites including the API since 07/19/2022.
.NET Framework does not support this protocol by default.
If you depend on .NET Framework, it is suggested to upgrade your project to `net6-windows`.
This framework supports most of the windows-only features introduced by fx, and resolves startup errors from the TLS protocol mismatch.

- [.NET Core SDK](https://www.microsoft.com/net/download/core)
## 🗃️ Versioning Guarantees

## Known Issues
This library generally abides by [Semantic Versioning](https://semver.org). Packages are published in `MAJOR.MINOR.PATCH` version format.

### WebSockets (Win7 and earlier)
### Patch component

.NET Core 1.1 does not support WebSockets on Win7 and earlier. This issue has been fixed since the release of .NET Core 2.1. It is recommended to target .NET Core 2.1 or above for your project if you wish to run your bot on legacy platforms; alternatively, you may choose to install the [Discord.Net.Providers.WS4Net](https://www.nuget.org/packages/Discord.Net.Providers.WS4Net/) package.
An increment of the **PATCH** component always indicates that an internal-only change was made, generally a bugfix. These changes will not affect the public-facing API in any way, and are always guaranteed to be forward- and backwards-compatible with your codebase, any pre-compiled dependencies of your codebase.

## Versioning Guarantees
### Minor component

This library generally abides by [Semantic Versioning](https://semver.org). Packages are published in MAJOR.MINOR.PATCH version format.
An increment of the **MINOR** component indicates that some addition was made to the library,
and this addition is not backwards-compatible with prior versions.
However, Discord.Net **does not guarantee forward-compatibility** on minor additions.
In other words, we permit a limited set of breaking changes on a minor version bump.

An increment of the PATCH component always indicates that an internal-only change was made, generally a bugfix. These changes will not affect the public-facing API in any way, and are always guaranteed to be forward- and backwards-compatible with your codebase, any pre-compiled dependencies of your codebase.
Due to the nature of the Discord API, we will oftentimes need to add a property to an entity to support the latest API changes.
Discord.Net provides interfaces as a method of consuming entities; and as such, introducing a new field to an entity is technically a breaking change.
Major version bumps generally indicate some major change to the library,
and as such we are hesitant to bump the major version for every minor addition to the library.
To compromise, we have decided that interfaces should be treated as **consumable only**,
and your applications should typically not be implementing interfaces.

An increment of the MINOR component indicates that some addition was made to the library, and this addition is not backwards-compatible with prior versions. However, Discord.Net **does not guarantee forward-compatibility** on minor additions. In other words, we permit a limited set of breaking changes on a minor version bump.
> For applications where interfaces are implemented, such as in test mocks, we apologize for this inconsistency with SemVer.

Due to the nature of the Discord API, we will oftentimes need to add a property to an entity to support the latest API changes. Discord.Net provides interfaces as a method of consuming entities; and as such, introducing a new field to an entity is technically a breaking change. Major version bumps generally indicate some major change to the library, and as such we are hesitant to bump the major version for every minor addition to the library. To compromise, we have decided that interfaces should be treated as **consumable only**, and your applications should typically not be implementing interfaces. (For applications where interfaces are implemented, such as in test mocks, we apologize for this inconsistency with SemVer).
While we will never break the API (outside of interface changes) on minor builds,
we will occasionally need to break the ABI, by introducing parameters to a method to match changes upstream with Discord.
As such, a minor version increment may require you to recompile your code, and dependencies,
such as addons, may also need to be recompiled and republished on the newer version.
When a binary breaking change is made, the change will be noted in the release notes.

Furthermore, while we will never break the API (outside of interface changes) on minor builds, we will occasionally need to break the ABI, by introducing parameters to a method to match changes upstream with Discord. As such, a minor version increment may require you to recompile your code, and dependencies, such as addons, may also need to be recompiled and republished on the newer version. When a binary breaking change is made, the change will be noted in the release notes.
### Major component

An increment of the MAJOR component indicates that breaking changes have been made to the library; consumers should check the release notes to determine what changes need to be made.
An increment of the **MAJOR** component indicates that breaking changes have been made to the library;
consumers should check the release notes to determine what changes need to be made.

## Branches
## 📚 Branches

### Release/X.X



+ 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.8.0",
"_enableSearch": true,
"_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg",
"_appFaviconPath": "favicon.ico"


+ 1
- 1
docs/faq/basics/client-basics.md View File

@@ -36,7 +36,7 @@ _client = new DiscordSocketClient(config);
This includes intents that receive messages such as: `GatewayIntents.GuildMessages, GatewayIntents.DirectMessages`
- GuildMembers: An intent disabled by default, as you need to enable it in the [developer portal].
- GuildPresences: Also disabled by default, this intent together with `GuildMembers` are the only intents not included in `AllUnprivileged`.
- All: All intents, it is ill adviced to use this without care, as it *can* cause a memory leak from presence.
- All: All intents, it is ill advised to use this without care, as it _can_ cause a memory leak from presence.
The library will give responsive warnings if you specify unnecessary intents.




+ 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

+ 69
- 0
docs/guides/dependency_injection/basics.md View File

@@ -0,0 +1,69 @@
---
uid: Guides.DI.Intro
title: Introduction
---

# Dependency Injection

Dependency injection is a feature not required in Discord.Net, but makes it a lot easier to use.
It can be combined with a large number of other libraries, and gives you better control over your application.

> Further into the documentation, Dependency Injection will be referred to as 'DI'.

## Installation

DI is not native to .NET. You need to install the extension packages to your project in order to use it:

- [Meta](https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection/).
- [Abstractions](https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection.Abstractions/).

> [!WARNING]
> Downloading the abstractions package alone will not give you access to required classes to use DI properly.
> Please install both packages, or choose to only install the meta package to implicitly install both.

### Visual Package Manager:

[Installing](images/manager.png)

### Command Line:

`PM> Install-Package Microsoft.Extensions.DependencyInjection`.

> [!TIP]
> ASP.NET already comes packed with all the necessary assemblies in its framework.
> You do not require to install any additional NuGet packages to make full use of all features of DI in ASP.NET projects.

## Getting started

First of all, you will need to create an application based around dependency injection,
which in order will be able to access and inject them across the project.

[!code-csharp[Building the Program](samples/program.cs)]

In order to freely pass around your dependencies in different classes,
you will need to register them to a new `ServiceCollection` and build them into an `IServiceProvider` as seen above.
The IServiceProvider then needs to be accessible by the startup file, so you can access your provider and manage them.

[!code-csharp[Building the Collection](samples/collection.cs)]

As shown above, an instance of `DiscordSocketConfig` is created, and added **before** the client itself is.
Because the collection will prefer to create the highest populated constructor available with the services already present,
it will prefer the constructor with the configuration, because you already added it.

## Using your dependencies

After building your provider in the Program class constructor, the provider is now available inside the instance you're actively using.
Through the provider, we can ask for the DiscordSocketClient we registered earlier.

[!code-csharp[Applying DI in RunAsync](samples/runasync.cs)]

> [!WARNING]
> Service constructors are not activated until the service is **first requested**.
> An 'endpoint' service will have to be requested from the provider before it is activated.
> If a service is requested with dependencies, its dependencies (if not already active) will be activated before the service itself is.

## Injecting dependencies

You can not only directly access the provider from a field or property, but you can also pass around instances to classes registered in the provider.
There are multiple ways to do this. Please refer to the
[Injection Documentation](Guides.DI.Injection) for further information.

BIN
docs/guides/dependency_injection/images/manager.png View File

Before After
Width: 777  |  Height: 142  |  Size: 12 KiB

+ 44
- 0
docs/guides/dependency_injection/injection.md View File

@@ -0,0 +1,44 @@
---
uid: Guides.DI.Injection
title: Injection
---

# Injecting instances within the provider

You can inject registered services into any class that is registered to the `IServiceProvider`.
This can be done through property or constructor.

> [!NOTE]
> As mentioned above, the dependency *and* the target class have to be registered in order for the serviceprovider to resolve it.

## Injecting through a constructor

Services can be injected from the constructor of the class.
This is the preferred approach, because it automatically locks the readonly field in place with the provided service and isn't accessible outside of the class.

[!code-csharp[Constructor Injection](samples/ctor-injecting.cs)]

## Injecting through properties

Injecting through properties is also allowed as follows.

[!code-csharp[Property Injection](samples/property-injecting.cs)]

> [!WARNING]
> Dependency Injection will not resolve missing services in property injection, and it will not pick a constructor instead.
> If a publically accessible property is attempted to be injected and its service is missing, the application will throw an error.

## Using the provider itself

You can also access the provider reference itself from injecting it into a class. There are multiple use cases for this:

- Allowing libraries (Like Discord.Net) to access your provider internally.
- Injecting optional dependencies.
- Calling methods on the provider itself if necessary, this is often done for creating scopes.

[!code-csharp[Provider Injection](samples/provider.cs)]

> [!NOTE]
> It is important to keep in mind that the provider will pick the 'biggest' available constructor.
> If you choose to introduce multiple constructors,
> keep in mind that services missing from one constructor may have the provider pick another one that *is* available instead of throwing an exception.

+ 9
- 0
docs/guides/dependency_injection/samples/access-activator.cs View File

@@ -0,0 +1,9 @@
async Task RunAsync()
{
//...

await _serviceProvider.GetRequiredService<ServiceActivator>()
.ActivateAsync();

//...
}

+ 13
- 0
docs/guides/dependency_injection/samples/collection.cs View File

@@ -0,0 +1,13 @@
static IServiceProvider CreateServices()
{
var config = new DiscordSocketConfig()
{
//...
};

var collection = new ServiceCollection()
.AddSingleton(config)
.AddSingleton<DiscordSocketClient>();

return collection.BuildServiceProvider();
}

+ 14
- 0
docs/guides/dependency_injection/samples/ctor-injecting.cs View File

@@ -0,0 +1,14 @@
public class ClientHandler
{
private readonly DiscordSocketClient _client;

public ClientHandler(DiscordSocketClient client)
{
_client = client;
}

public async Task ConfigureAsync()
{
//...
}
}

+ 18
- 0
docs/guides/dependency_injection/samples/enumeration.cs View File

@@ -0,0 +1,18 @@
public class ServiceActivator
{
// This contains *all* registered services of serviceType IService
private readonly IEnumerable<IService> _services;

public ServiceActivator(IEnumerable<IService> services)
{
_services = services;
}

public async Task ActivateAsync()
{
foreach(var service in _services)
{
await service.StartAsync();
}
}
}

+ 12
- 0
docs/guides/dependency_injection/samples/implicit-registration.cs View File

@@ -0,0 +1,12 @@
public static ServiceCollection RegisterImplicitServices(this ServiceCollection collection, Type interfaceType, Type activatorType)
{
// Get all types in the executing assembly. There are many ways to do this, but this is fastest.
foreach (var type in typeof(Program).Assembly.GetTypes())
{
if (interfaceType.IsAssignableFrom(type) && !type.IsAbstract)
collection.AddSingleton(interfaceType, type);
}

// Register the activator so you can activate the instances.
collection.AddSingleton(activatorType);
}

+ 16
- 0
docs/guides/dependency_injection/samples/modules.cs View File

@@ -0,0 +1,16 @@
public class MyModule : InteractionModuleBase
{
private readonly MyService _service;

public MyModule(MyService service)
{
_service = service;
}

[SlashCommand("things", "Shows things")]
public async Task ThingsAsync()
{
var str = string.Join("\n", _service.Things)
await RespondAsync(str);
}
}

+ 24
- 0
docs/guides/dependency_injection/samples/program.cs View File

@@ -0,0 +1,24 @@
public class Program
{
private readonly IServiceProvider _serviceProvider;

public Program()
{
_serviceProvider = CreateProvider();
}

static void Main(string[] args)
=> new Program().RunAsync(args).GetAwaiter().GetResult();

static IServiceProvider CreateProvider()
{
var collection = new ServiceCollection();
//...
return collection.BuildServiceProvider();
}

async Task RunAsync(string[] args)
{
//...
}
}

+ 9
- 0
docs/guides/dependency_injection/samples/property-injecting.cs View File

@@ -0,0 +1,9 @@
public class ClientHandler
{
public DiscordSocketClient Client { get; set; }

public async Task ConfigureAsync()
{
//...
}
}

+ 26
- 0
docs/guides/dependency_injection/samples/provider.cs View File

@@ -0,0 +1,26 @@
public class UtilizingProvider
{
private readonly IServiceProvider _provider;
private readonly AnyService _service;

// This service is allowed to be null because it is only populated if the service is actually available in the provider.
private readonly AnyOtherService? _otherService;

// This constructor injects only the service provider,
// and uses it to populate the other dependencies.
public UtilizingProvider(IServiceProvider provider)
{
_provider = provider;
_service = provider.GetRequiredService<AnyService>();
_otherService = provider.GetService<AnyOtherService>();
}

// This constructor injects the service provider, and AnyService,
// making sure that AnyService is not null without having to call GetRequiredService
public UtilizingProvider(IServiceProvider provider, AnyService service)
{
_provider = provider;
_service = service;
_otherService = provider.GetService<AnyOtherService>();
}
}

+ 17
- 0
docs/guides/dependency_injection/samples/runasync.cs View File

@@ -0,0 +1,17 @@
async Task RunAsync(string[] args)
{
// Request the instance from the client.
// Because we're requesting it here first, its targetted constructor will be called and we will receive an active instance.
var client = _services.GetRequiredService<DiscordSocketClient>();

client.Log += async (msg) =>
{
await Task.CompletedTask;
Console.WriteLine(msg);
}

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

await Task.Delay(Timeout.Infinite);
}

+ 6
- 0
docs/guides/dependency_injection/samples/scoped.cs View File

@@ -0,0 +1,6 @@

// With serviceType:
collection.AddScoped<IScopedService, ScopedService>();

// Without serviceType:
collection.AddScoped<ScopedService>();

+ 21
- 0
docs/guides/dependency_injection/samples/service-registration.cs View File

@@ -0,0 +1,21 @@
static IServiceProvider CreateServices()
{
var config = new DiscordSocketConfig()
{
//...
};

// X represents either Interaction or Command, as it functions the exact same for both types.
var servConfig = new XServiceConfig()
{
//...
}

var collection = new ServiceCollection()
.AddSingleton(config)
.AddSingleton<DiscordSocketClient>()
.AddSingleton(servConfig)
.AddSingleton<XService>();

return collection.BuildServiceProvider();
}

+ 9
- 0
docs/guides/dependency_injection/samples/services.cs View File

@@ -0,0 +1,9 @@
public class MyService
{
public List<string> Things { get; }

public MyService()
{
Things = new();
}
}

+ 6
- 0
docs/guides/dependency_injection/samples/singleton.cs View File

@@ -0,0 +1,6 @@

// With serviceType:
collection.AddSingleton<ISingletonService, SingletonService>();

// Without serviceType:
collection.AddSingleton<SingletonService>();

+ 6
- 0
docs/guides/dependency_injection/samples/transient.cs View File

@@ -0,0 +1,6 @@

// With serviceType:
collection.AddTransient<ITransientService, TransientService>();

// Without serviceType:
collection.AddTransient<TransientService>();

+ 39
- 0
docs/guides/dependency_injection/scaling.md View File

@@ -0,0 +1,39 @@
---
uid: Guides.DI.Scaling
title: Scaling your DI
---

# Scaling your DI

Dependency injection has a lot of use cases, and is very suitable for scaled applications.
There are a few ways to make registering & using services easier in large amounts.

## Using a range of services.

If you have a lot of services that all have the same use such as handling an event or serving a module,
you can register and inject them all at once by some requirements:

- All classes need to inherit a single interface or abstract type.
- While not required, it is preferred if the interface and types share a method to call on request.
- You need to register a class that all the types can be injected into.

### Registering implicitly

Registering all the types is done through getting all types in the assembly and checking if they inherit the target interface.

[!code-csharp[Registering](samples/implicit-registration.cs)]

> [!NOTE]
> As seen above, the interfaceType and activatorType are undefined. For our usecase below, these are `IService` and `ServiceActivator` in order.

### Using implicit dependencies

In order to use the implicit dependencies, you have to get access to the activator you registered earlier.

[!code-csharp[Accessing the activator](samples/access-activator.cs)]

When the activator is accessed and the `ActivateAsync()` method is called, the following code will be executed:

[!code-csharp[Executing the activator](samples/enumeration.cs)]

As a result of this, all the services that were registered with `IService` as its implementation type will execute their starting code, and start up.

+ 48
- 0
docs/guides/dependency_injection/services.md View File

@@ -0,0 +1,48 @@
---
uid: Guides.DI.Services
title: Using DI in Interaction & Command Frameworks
---

# DI in the Interaction- & Command Service

For both the Interaction- and Command Service modules, DI is quite straight-forward to use.

You can inject any service into modules without the modules having to be registered to the provider.
Discord.Net resolves your dependencies internally.

> [!WARNING]
> The way DI is used in the Interaction- & Command Service are nearly identical, except for one detail:
> [Resolving Module Dependencies](xref:Guides.IntFw.Intro#resolving-module-dependencies)

## Registering the Service

Thanks to earlier described behavior of allowing already registered members as parameters of the available ctors,
The socket client & configuration will automatically be acknowledged and the XService(client, config) overload will be used.

[!code-csharp[Service Registration](samples/service-registration.cs)]

## Usage in modules

In the constructor of your module, any parameters will be filled in by
the @System.IServiceProvider that you've passed.

Any publicly settable properties will also be filled in the same
manner.

[!code-csharp[Module Injection](samples/modules.cs)]

If you accept `Command/InteractionService` or `IServiceProvider` as a parameter in your constructor or as an injectable property,
these entries will be filled by the `Command/InteractionService` that the module is loaded from and the `IServiceProvider` that is passed into it respectively.

> [!NOTE]
> Annotating a property with a [DontInjectAttribute] attribute will
> prevent the property from being injected.

## Services

Because modules are transient of nature and will reinstantiate on every request,
it is suggested to create a singleton service behind it to hold values across multiple command executions.

[!code-csharp[Services](samples/services.cs)]



+ 52
- 0
docs/guides/dependency_injection/types.md View File

@@ -0,0 +1,52 @@
---
uid: Guides.DI.Dependencies
title: Types of Dependencies
---

# Dependency Types

There are 3 types of dependencies to learn to use. Several different usecases apply for each.

> [!WARNING]
> When registering types with a serviceType & implementationType,
> only the serviceType will be available for injection, and the implementationType will be used for the underlying instance.

## Singleton

A singleton service creates a single instance when first requested, and maintains that instance across the lifetime of the application.
Any values that are changed within a singleton will be changed across all instances that depend on it, as they all have the same reference to it.

### Registration:

[!code-csharp[Singleton Example](samples/singleton.cs)]

> [!NOTE]
> Types like the Discord client and Interaction/Command services are intended to be singleton,
> as they should last across the entire app and share their state with all references to the object.

## Scoped

A scoped service creates a new instance every time a new service is requested, but is kept across the 'scope'.
As long as the service is in view for the created scope, the same instance is used for all references to the type.
This means that you can reuse the same instance during execution, and keep the services' state for as long as the request is active.

### Registration:

[!code-csharp[Scoped Example](samples/scoped.cs)]

> [!NOTE]
> Without using HTTP or libraries like EFCORE, scopes are often unused in Discord bots.
> They are most commonly used for handling HTTP and database requests.

## Transient

A transient service is created every time it is requested, and does not share its state between references within the target service.
It is intended for lightweight types that require little state, to be disposed quickly after execution.

### Registration:

[!code-csharp[Transient Example](samples/transient.cs)]

> [!NOTE]
> Discord.Net modules behave exactly as transient types, and are intended to only last as long as the command execution takes.
> This is why it is suggested for apps to use singleton services to keep track of cross-execution data.

+ 7
- 1
docs/guides/deployment/deployment.md View File

@@ -47,6 +47,12 @@ enough. Here is a list of recommended VPS provider.
* Location(s):
* Europe: Lithuania
* Based in: Europe
* [ServerStarter.Host](https://serverstarter.host/clients/store/discord-bots)
* Description: Bot hosting with a panel for quick deployment and
no Linux knowledge required.
* Location(s):
* America: United States
* Based in: United States

## .NET Core Deployment

@@ -100,4 +106,4 @@ Windows 10 x64 based machine:
* `dotnet publish -c Release -r win10-x64`

[.NET Core application deployment]: https://docs.microsoft.com/en-us/dotnet/core/deploying/
[Runtime ID]: https://docs.microsoft.com/en-us/dotnet/core/rid-catalog
[Runtime ID]: https://docs.microsoft.com/en-us/dotnet/core/rid-catalog

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

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

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



+ 14
- 6
docs/guides/getting_started/installing.md View File

@@ -30,17 +30,25 @@ other limitations, you may also consider targeting [.NET Framework]
[.net framework]: https://docs.microsoft.com/en-us/dotnet/framework/get-started/
[additional steps]: #installing-on-net-standard-11

## Installing with NuGet
## Installing

Release builds of Discord.Net will be published to the
[official NuGet feed].

Development builds of Discord.Net, as well as add-ons, will be
published to our [MyGet feed]. See
@Guides.GettingStarted.Installation.Nightlies to learn more.
### Experimental/Development

[official nuget feed]: https://nuget.org
[myget feed]: https://www.myget.org/feed/Packages/discord-net
Development builds of Discord.Net will be
published to our [MyGet feed]. The MyGet feed can be used to run the latest dev branch builds.
It is not advised to use MyGet packages in a production environment, as changes may be made that negatively affect certain library functions.

### Labs

This exterior branch of Discord.Net has been deprecated and is no longer supported.
If you have used Discord.Net-Labs in the past, you are advised to update to the latest version of Discord.Net.
All features in Labs are implemented in the main repository.

[official NuGet feed]: https://nuget.org
[MyGet feed]: https://www.myget.org/feed/Packages/discord-net

### [Using Visual Studio](#tab/vs-install)



+ 0
- 30
docs/guides/getting_started/labs.md View File

@@ -1,30 +0,0 @@
---
uid: Guides.GettingStarted.Installation.Labs
title: Installing Labs builds
---

# Installing Discord.NET Labs

Discord.NET Labs is the experimental repository that introduces new features & chips away at all bugs until ready for merging into Discord.NET.
Are you looking to test or play with new features?

> [!IMPORTANT]
> It is very ill advised to use Discord.NET Labs in a production environment normally,
> considering it can include bugs that have not been discovered yet, as features are freshly added.
> However if approached correctly, will work as a pre-release to Discord.NET.
> Make sure to report any bugs at the Labs [repository] or on [Discord]

[Discord]: https://discord.gg/dnet
[repository]: https://github.com/Discord-Net-Labs/Discord.Net-Labs

## Installation:

[NuGet] - This only includes releases, on which features are ready to test.

> [!NOTE]
> Installing NuGet packages is covered fully at [Installing Discord NET](xref:Guides.GettingStarted.Installation)

[MyGet] - Available for current builds and unreleased features.

[NuGet]: https://www.nuget.org/packages/Discord.Net.Labs/
[MyGet]: https://www.myget.org/feed/Packages/discord-net-labs

+ 8
- 4
docs/guides/getting_started/terminology.md View File

@@ -8,18 +8,22 @@ title: Terminology
## Preface

Most terms for objects remain the same between 0.9 and 1.0 and above.
The major difference is that the ``Server`` is now called ``Guild``
The major difference is that the `Server` is now called `Guild`
to stay in line with Discord internally.

## Implementation Specific Entities

Discord.Net is split into a core library and two different
implementations - `Discord.Net.Core`, `Discord.Net.Rest`, and
`Discord.Net.WebSockets`.
`Discord.Net.WebSocket`.

As a bot developer, you will only need to use `Discord.Net.WebSockets`,
You will typically only need to use `Discord.Net.WebSocket`,
but you should be aware of the differences between them.

> [!TIP]
> If you are looking to implement Rest based interactions, or handle calls over REST in any other way,
> `Discord.Net.Rest` is the resource most applicable to you.

`Discord.Net.Core` provides a set of interfaces that models Discord's
API. These interfaces are consistent throughout all implementations of
Discord.Net, and if you are writing an implementation-agnostic library
@@ -33,4 +37,4 @@ implementation are prefixed with `Rest` (e.g., `RestChannel`).
`Discord.Net.WebSocket` provides a set of concrete classes that are
used primarily with Discord's WebSocket API or entities that are kept
in cache. When developing bots, you will be using this implementation.
All entities are prefixed with `Socket` (e.g., `SocketChannel`).
All entities are prefixed with `Socket` (e.g., `SocketChannel`).

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


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

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

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

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



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

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

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

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


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

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

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

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

### GenerateSuggestionsAsync()

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


+ 0
- 13
docs/guides/int_framework/dependency-injection.md View File

@@ -1,13 +0,0 @@
---
uid: Guides.IntFw.DI
title: Dependency Injection
---

# Dependency Injection

Dependency injection in the Interaction Service is mostly based on that of the Text-based command service,
for which further information is found [here](xref:Guides.TextCommands.DI).

> [!NOTE]
> The 2 are nearly identical, except for one detail:
> [Resolving Module Dependencies](xref:Guides.IntFw.Intro#resolving-module-dependencies)

+ 93
- 5
docs/guides/int_framework/intro.md View File

@@ -86,6 +86,7 @@ By default, your methods can feature the following parameter types:
- Implementations of [IChannel]
- Implementations of [IRole]
- Implementations of [IMentionable]
- Implementations of [IAttachment]
- `string`
- `float`, `double`, `decimal`
- `bool`
@@ -143,6 +144,29 @@ 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.

#### DM Permissions

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

#### Default Member Permissions

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

## User Commands

A valid User Command must have the following structure:
@@ -255,8 +279,8 @@ Meaning, the constructor parameters and public settable properties of a module w
For more information on dependency injection, read the [DependencyInjection] guides.

> [!NOTE]
> On every command execution, module dependencies are resolved using a new service scope which allows you to utilize scoped service instances, just like in Asp.Net.
> Including the precondition checks, every module method is executed using the same service scope and service scopes are disposed right after the `AfterExecute` method returns.
> On every command execution, if the 'AutoServiceScopes' option is enabled in the config , module dependencies are resolved using a new service scope which allows you to utilize scoped service instances, just like in Asp.Net.
> Including the precondition checks, every module method is executed using the same service scope and service scopes are disposed right after the `AfterExecute` method returns. This doesn't apply to methods other than `ExecuteAsync()`.

## Module Groups

@@ -267,6 +291,13 @@ By nesting commands inside a module that is tagged with [GroupAttribute] you can
> Although creating nested module stuctures are allowed,
> you are not permitted to use more than 2 [GroupAttribute]'s in module hierarchy.

> [!NOTE]
> To not use the command group's name as a prefix for component or modal interaction's custom id set `ignoreGroupNames` parameter to `true` in classes with [GroupAttribute]
>
> However, you have to be careful to prevent overlapping ids of buttons and modals.

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

## Executing Commands

Any of the following socket events can be used to execute commands:
@@ -277,8 +308,19 @@ Any of the following socket events can be used to execute commands:
- [AutocompleteExecuted]
- [UserCommandExecuted]
- [MessageCommandExecuted]
- [ModalExecuted]

These events will trigger for the specific type of interaction they inherit their name from. The [InteractionCreated] event will trigger for all.
An example of executing a command from an event can be seen here:

[!code-csharp[Command Event Example](samples/intro/event.cs)]

Commands can be either executed on the gateway thread or on a seperate thread from the thread pool.
This behaviour can be configured by changing the `RunMode` property of `InteractionServiceConfig` or by setting the *runMode* parameter of a command attribute.

Commands can be either executed on the gateway thread or on a seperate thread from the thread pool. This behaviour can be configured by changing the *RunMode* property of `InteractionServiceConfig` or by setting the *runMode* parameter of a command attribute.
> [!WARNING]
> In the example above, no form of post-execution is presented.
> Please carefully read the [Post Execution Documentation] for the best approach in resolving the result based on your `RunMode`.

You can also configure the way [InteractionService] executes the commands.
By default, commands are executed using `ConstructorInfo.Invoke()` to create module instances and
@@ -304,10 +346,13 @@ Command registration methods can only be used after the gateway client is ready
Methods like `AddModulesToGuildAsync()`, `AddCommandsToGuildAsync()`, `AddModulesGloballyAsync()` and `AddCommandsGloballyAsync()`
can be used to register cherry picked modules or commands to global/guild scopes.

> [!NOTE]
> [DontAutoRegisterAttribute] can be used on module classes to prevent `RegisterCommandsGloballyAsync()` and `RegisterCommandsToGuildAsync()` from registering them to the Discord.

> [!NOTE]
> In debug environment, since Global commands can take up to 1 hour to register/update,
> it is adviced to register your commands to a test guild for your changes to take effect immediately.
> You can use preprocessor directives to create a simple logic for registering commands as seen above
> You can use preprocessor directives to create a simple logic for registering commands as seen above.

## Interaction Utility

@@ -331,10 +376,52 @@ respond to the Interactions within your command modules you need to perform the
delegate can be used to create HTTP responses from a deserialized json object string.
- Use the interaction endpoints of the module base instead of the interaction object (ie. `RespondAsync()`, `FollowupAsync()`...).

## Localization

Discord Slash Commands support name/description localization. Localization is available for names and descriptions of Slash Command Groups ([GroupAttribute]), Slash Commands ([SlashCommandAttribute]), Slash Command parameters and Slash Command Parameter Choices. Interaction Service can be initialized with an `ILocalizationManager` instance in its config which is used to create the necessary localization dictionaries on command registration. Interaction Service has two built-in `ILocalizationManager` implementations: `ResxLocalizationManager` and `JsonLocalizationManager`.

### ResXLocalizationManager

`ResxLocalizationManager` uses `.` delimited key names to traverse the resource files and get the localized strings (`group1.group2.command.parameter.name`). A `ResxLocalizationManager` instance must be initialized with a base resource name, a target assembly and a collection of `CultureInfo`s. Every key path must end with either `.name` or `.description`, including parameter choice strings. [Discord.Tools.LocalizationTemplate.Resx](https://www.nuget.org/packages/Discord.Tools.LocalizationTemplate.Resx) dotnet tool can be used to create localization file templates.

### JsonLocalizationManager

`JsonLocaliationManager` uses a nested data structure similar to Discord's Application Commands schema. You can get the Json schema [here](https://gist.github.com/Cenngo/d46a881de24823302f66c3c7e2f7b254). `JsonLocalizationManager` accepts a base path and a base file name and automatically discovers every resource file ( \basePath\fileName.locale.json ). A Json resource file should have a structure similar to:

```json
{
"command_1":{
"name": "localized_name",
"description": "localized_description",
"parameter_1":{
"name": "localized_name",
"description": "localized_description"
}
},
"group_1":{
"name": "localized_name",
"description": "localized_description",
"command_1":{
"name": "localized_name",
"description": "localized_description",
"parameter_1":{
"name": "localized_name",
"description": "localized_description"
},
"parameter_2":{
"name": "localized_name",
"description": "localized_description"
}
}
}
}
```

[AutocompleteHandlers]: xref:Guides.IntFw.AutoCompletion
[DependencyInjection]: xref:Guides.TextCommands.DI
[DependencyInjection]: xref:Guides.DI.Intro

[GroupAttribute]: xref:Discord.Interactions.GroupAttribute
[DontAutoRegisterAttribute]: xref:Discord.Interactions.DontAutoRegisterAttribute
[InteractionService]: xref:Discord.Interactions.InteractionService
[InteractionServiceConfig]: xref:Discord.Interactions.InteractionServiceConfig
[InteractionModuleBase]: xref:Discord.Interactions.InteractionModuleBase
@@ -345,6 +432,7 @@ delegate can be used to create HTTP responses from a deserialized json object st
[AutocompleteExecuted]: xref:Discord.WebSocket.BaseSocketClient
[UserCommandExecuted]: xref:Discord.WebSocket.BaseSocketClient
[MessageCommandExecuted]: xref:Discord.WebSocket.BaseSocketClient
[ModalExecuted]: xref:Discord.WebSocket.BaseSocketClient
[DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient
[DiscordRestClient]: xref:Discord.Rest.DiscordRestClient
[SocketInteractionContext]: xref:Discord.Interactions.SocketInteractionContext


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

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

# Permissions

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

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

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

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

## Disallowing commands in DM

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

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

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

## Server permissions

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

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

### Stacking permissions

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

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

### Nesting permissions

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

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

The amount of nesting you can do is realistically endless.

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

[permissions]: xref:Discord.GuildPermission


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

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

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

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

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

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

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

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

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

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

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

+ 14
- 0
docs/guides/int_framework/samples/intro/event.cs View File

@@ -0,0 +1,14 @@
// Theres multiple ways to subscribe to the event, depending on your application. Please use the approach fit to your type of client.
// DiscordSocketClient:
_socketClient.InteractionCreated += async (x) =>
{
var ctx = new SocketInteractionContext(_socketClient, x);
await _interactionService.ExecuteCommandAsync(ctx, _serviceProvider);
}

// DiscordShardedClient:
_shardedClient.InteractionCreated += async (x) =>
{
var ctx = new ShardedInteractionContext(_shardedClient, x);
await _interactionService.ExecuteCommandAsync(ctx, _serviceProvider);
}

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


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

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

// Component interaction with ignoreGroupNames set to true
[ComponentInteraction("echoButton_*", true)]
public async Task EchoButton(string input)
=> await RespondAsync(input);
}
}

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

@@ -12,7 +12,9 @@ public class FoodModal : IModal
[ModalTextInput("food_name", placeholder: "Pizza", maxLength: 20)]
public string Food { get; set; }

// Additional paremeters can be specified to further customize the input.
// Additional paremeters can be specified to further customize the input.
// Parameters can be optional
[RequiredInput(false)]
[InputLabel("Why??")]
[ModalTextInput("food_reason", TextInputStyle.Paragraph, "Kuz it's tasty", maxLength: 500)]
public string Reason { get; set; }
@@ -20,12 +22,17 @@ public class FoodModal : IModal

// Responds to the modal.
[ModalInteraction("food_menu")]
public async Task ModalResponce(FoodModal modal)
public async Task ModalResponse(FoodModal modal)
{
// Check if "Why??" field is populated
string reason = string.IsNullOrWhiteSpace(modal.Reason)
? "."
: $" because {modal.Reason}";

// Build the message to send.
string message = "hey @everyone, I just learned " +
$"{Context.User.Mention}'s favorite food is " +
$"{modal.Food} because {modal.Reason}.";
$"{modal.Food}{reason}";

// Specify the AllowedMentions so we don't actually ping everyone.
AllowedMentions mentions = new();


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

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

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

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

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

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

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

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

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

+ 0
- 51
docs/guides/text_commands/dependency-injection.md View File

@@ -1,51 +0,0 @@
---
uid: Guides.TextCommands.DI
title: Dependency Injection
---

# Dependency Injection

The Text Command Service is bundled with a very barebone Dependency
Injection service for your convenience. It is recommended that you use
DI when writing your modules.

> [!WARNING]
> If you were brought here from the Interaction Service guides,
> make sure to replace all namespaces that imply `Discord.Commands` with `Discord.Interactions`

## Setup

1. Create a @Microsoft.Extensions.DependencyInjection.ServiceCollection.
2. Add the dependencies to the service collection that you wish
to use in the modules.
3. Build the service collection into a service provider.
4. Pass the service collection into @Discord.Commands.CommandService.AddModulesAsync* / @Discord.Commands.CommandService.AddModuleAsync* , @Discord.Commands.CommandService.ExecuteAsync* .

### Example - Setting up Injection

[!code-csharp[IServiceProvider Setup](samples/dependency-injection/dependency_map_setup.cs)]

## Usage in Modules

In the constructor of your module, any parameters will be filled in by
the @System.IServiceProvider that you've passed.

Any publicly settable properties will also be filled in the same
manner.

> [!NOTE]
> Annotating a property with a [DontInjectAttribute] attribute will
> prevent the property from being injected.

> [!NOTE]
> If you accept `CommandService` or `IServiceProvider` as a parameter
> in your constructor or as an injectable property, these entries will
> be filled by the `CommandService` that the module is loaded from and
> the `IServiceProvider` that is passed into it respectively.

### Example - Injection in Modules

[!code-csharp[Injection Modules](samples/dependency-injection/dependency_module.cs)]
[!code-csharp[Disallow Dependency Injection](samples/dependency-injection/dependency_module_noinject.cs)]

[DontInjectAttribute]: xref:Discord.Commands.DontInjectAttribute

+ 1
- 1
docs/guides/text_commands/intro.md View File

@@ -187,7 +187,7 @@ service provider.

### Module Constructors

Modules are constructed using [Dependency Injection](xref:Guides.TextCommands.DI). Any parameters
Modules are constructed using [Dependency Injection](xref:Guides.DI.Intro). Any parameters
that are placed in the Module's constructor must be injected into an
@System.IServiceProvider first.



+ 0
- 65
docs/guides/text_commands/samples/dependency-injection/dependency_map_setup.cs View File

@@ -1,65 +0,0 @@
public class Initialize
{
private readonly CommandService _commands;
private readonly DiscordSocketClient _client;

// Ask if there are existing CommandService and DiscordSocketClient
// instance. If there are, we retrieve them and add them to the
// DI container; if not, we create our own.
public Initialize(CommandService commands = null, DiscordSocketClient client = null)
{
_commands = commands ?? new CommandService();
_client = client ?? new DiscordSocketClient();
}

public IServiceProvider BuildServiceProvider() => new ServiceCollection()
.AddSingleton(_client)
.AddSingleton(_commands)
// You can pass in an instance of the desired type
.AddSingleton(new NotificationService())
// ...or by using the generic method.
//
// The benefit of using the generic method is that
// ASP.NET DI will attempt to inject the required
// dependencies that are specified under the constructor
// for us.
.AddSingleton<DatabaseService>()
.AddSingleton<CommandHandler>()
.BuildServiceProvider();
}
public class CommandHandler
{
private readonly DiscordSocketClient _client;
private readonly CommandService _commands;
private readonly IServiceProvider _services;

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

public async Task InitializeAsync()
{
// Pass the service provider to the second parameter of
// AddModulesAsync to inject dependencies to all modules
// that may require them.
await _commands.AddModulesAsync(
assembly: Assembly.GetEntryAssembly(),
services: _services);
_client.MessageReceived += HandleCommandAsync;
}

public async Task HandleCommandAsync(SocketMessage msg)
{
// ...
// Pass the service provider to the ExecuteAsync method for
// precondition checks.
await _commands.ExecuteAsync(
context: context,
argPos: argPos,
services: _services);
// ...
}
}

+ 0
- 37
docs/guides/text_commands/samples/dependency-injection/dependency_module.cs View File

@@ -1,37 +0,0 @@
// After setting up dependency injection, modules will need to request
// the dependencies to let the library know to pass
// them along during execution.

// Dependency can be injected in two ways with Discord.Net.
// You may inject any required dependencies via...
// the module constructor
// -or-
// public settable properties

// Injection via constructor
public class DatabaseModule : ModuleBase<SocketCommandContext>
{
private readonly DatabaseService _database;
public DatabaseModule(DatabaseService database)
{
_database = database;
}

[Command("read")]
public async Task ReadFromDbAsync()
{
await ReplyAsync(_database.GetData());
}
}

// Injection via public settable properties
public class DatabaseModule : ModuleBase<SocketCommandContext>
{
public DatabaseService DbService { get; set; }

[Command("read")]
public async Task ReadFromDbAsync()
{
await ReplyAsync(DbService.GetData());
}
}

+ 0
- 29
docs/guides/text_commands/samples/dependency-injection/dependency_module_noinject.cs View File

@@ -1,29 +0,0 @@
// Sometimes injecting dependencies automatically with the provided
// methods in the prior example may not be desired.

// You may explicitly tell Discord.Net to **not** inject the properties
// by either...
// restricting the access modifier
// -or-
// applying DontInjectAttribute to the property

// Restricting the access modifier of the property
public class ImageModule : ModuleBase<SocketCommandContext>
{
public ImageService ImageService { get; }
public ImageModule()
{
ImageService = new ImageService();
}
}

// Applying DontInjectAttribute
public class ImageModule : ModuleBase<SocketCommandContext>
{
[DontInject]
public ImageService ImageService { get; set; }
public ImageModule()
{
ImageService = new ImageService();
}
}

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

@@ -6,9 +6,6 @@
items:
- name: Installation
topicUid: Guides.GettingStarted.Installation
items:
- name: Nightly builds
topicUid: Guides.GettingStarted.Installation.Labs
- name: Your First Bot
topicUid: Guides.GettingStarted.FirstBot
- name: Terminology
@@ -29,6 +26,18 @@
topicUid: Guides.Entities.Casting
- name: Glossary & Flowcharts
topicUid: Guides.Entities.Glossary
- name: Dependency Injection
items:
- name: Introduction
topicUid: Guides.DI.Intro
- name: Injection
topicUid: Guides.DI.Injection
- name: Command- & Interaction Services
topicUid: Guides.DI.Services
- name: Service Types
topicUid: Guides.DI.Dependencies
- name: Scaling your Application
topicUid: Guides.DI.Scaling
- name: Working with Text-based Commands
items:
- name: Introduction
@@ -39,8 +48,6 @@
topicUid: Guides.TextCommands.NamedArguments
- name: Preconditions
topicUid: Guides.TextCommands.Preconditions
- name: Dependency Injection
topicUid: Guides.TextCommands.DI
- name: Post-execution Handling
topicUid: Guides.TextCommands.PostExecution
- name: Working with the Interaction Framework
@@ -53,10 +60,10 @@
topicUid: Guides.IntFw.TypeConverters
- name: Preconditions
topicUid: Guides.IntFw.Preconditions
- name: Dependency Injection
topicUid: Guides.IntFw.DI
- name: Post-execution Handling
topicUid: Guides.IntFw.PostExecution
- name: Permissions
topicUid: Guides.IntFw.Perms
- name: Slash Command Basics
items:
- name: Introduction
@@ -115,6 +122,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/guides/v2_v3_guide/v2_to_v3_guide.md View File

@@ -38,7 +38,7 @@ _client = new DiscordSocketClient(config);
This includes intents that receive messages such as: `GatewayIntents.GuildMessages, GatewayIntents.DirectMessages`
- GuildMembers: An intent disabled by default, as you need to enable it in the [developer portal].
- GuildPresences: Also disabled by default, this intent together with `GuildMembers` are the only intents not included in `AllUnprivileged`.
- All: All intents, it is ill adviced to use this without care, as it _can_ cause a memory leak from presence.
- All: All intents, it is ill advised to use this without care, as it _can_ cause a memory leak from presence.
The library will give responsive warnings if you specify unnecessary intents.

> [!NOTE]


+ 2
- 4
docs/guides/voice/sending-voice.md View File

@@ -17,11 +17,9 @@ bot. (When developing on .NET Framework, this would be `bin/debug`,
when developing on .NET Core, this is where you execute `dotnet run`
from; typically the same directory as your csproj).

For Windows Users, precompiled binaries are available for your
convienence [here](https://github.com/discord-net/Discord.Net/tree/dev/voice-natives).
**For Windows users, precompiled binaries are available for your convienence [here](https://github.com/discord-net/Discord.Net/tree/dev/voice-natives).**

For Linux Users, you will need to compile [Sodium] and [Opus] from
source, or install them from your package manager.
**For Linux users, you will need to compile [Sodium] and [Opus] from source, or install them from your package manager.**

[Sodium]: https://download.libsodium.org/libsodium/releases/
[Opus]: http://downloads.xiph.org/releases/opus/


+ 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.First().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

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

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

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

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

</Project>

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


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

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

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

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

</Project>

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


+ 10
- 5
samples/ShardedClient/Services/InteractionHandlingService.cs View File

@@ -22,6 +22,7 @@ namespace ShardedClient.Services

_service.Log += LogAsync;
_client.InteractionCreated += OnInteractionAsync;
_client.ShardReady += ReadyAsync;
// For examples on how to handle post execution,
// see the InteractionFramework samples.
}
@@ -30,11 +31,6 @@ namespace ShardedClient.Services
public async Task InitializeAsync()
{
await _service.AddModulesAsync(typeof(InteractionHandlingService).Assembly, _provider);
#if DEBUG
await _service.AddCommandsToGuildAsync(_client.Guilds.First(x => x.Id == 1));
#else
await _service.AddCommandsGloballyAsync();
#endif
}

private async Task OnInteractionAsync(SocketInteraction interaction)
@@ -53,5 +49,14 @@ namespace ShardedClient.Services

return Task.CompletedTask;
}

private async Task ReadyAsync(DiscordSocketClient _)
{
#if DEBUG
await _service.RegisterCommandsToGuildAsync(1 /* implement */);
#else
await _service.RegisterCommandsGloballyAsync();
#endif
}
}
}

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

Loading…
Cancel
Save