Browse Source

Merge branch 'dev' into fix/typeconverter-service-scope

pull/2306/head
Quin Lynch GitHub 2 years ago
parent
commit
c2db6526a6
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
100 changed files with 2648 additions and 487 deletions
  1. +2
    -0
      .github/FUNDING.yml
  2. +2
    -2
      .github/ISSUE_TEMPLATE/bugreport.yml
  3. +89
    -0
      CHANGELOG.md
  4. +1
    -2
      CONTRIBUTING.md
  5. +6
    -3
      Discord.Net.targets
  6. +54
    -31
      README.md
  7. +1
    -1
      docs/docfx.json
  8. +1
    -1
      docs/faq/basics/client-basics.md
  9. +69
    -0
      docs/guides/dependency_injection/basics.md
  10. BIN
      docs/guides/dependency_injection/images/manager.png
  11. +44
    -0
      docs/guides/dependency_injection/injection.md
  12. +9
    -0
      docs/guides/dependency_injection/samples/access-activator.cs
  13. +13
    -0
      docs/guides/dependency_injection/samples/collection.cs
  14. +14
    -0
      docs/guides/dependency_injection/samples/ctor-injecting.cs
  15. +18
    -0
      docs/guides/dependency_injection/samples/enumeration.cs
  16. +12
    -0
      docs/guides/dependency_injection/samples/implicit-registration.cs
  17. +16
    -0
      docs/guides/dependency_injection/samples/modules.cs
  18. +24
    -0
      docs/guides/dependency_injection/samples/program.cs
  19. +9
    -0
      docs/guides/dependency_injection/samples/property-injecting.cs
  20. +26
    -0
      docs/guides/dependency_injection/samples/provider.cs
  21. +17
    -0
      docs/guides/dependency_injection/samples/runasync.cs
  22. +6
    -0
      docs/guides/dependency_injection/samples/scoped.cs
  23. +21
    -0
      docs/guides/dependency_injection/samples/service-registration.cs
  24. +9
    -0
      docs/guides/dependency_injection/samples/services.cs
  25. +6
    -0
      docs/guides/dependency_injection/samples/singleton.cs
  26. +6
    -0
      docs/guides/dependency_injection/samples/transient.cs
  27. +39
    -0
      docs/guides/dependency_injection/scaling.md
  28. +48
    -0
      docs/guides/dependency_injection/services.md
  29. +52
    -0
      docs/guides/dependency_injection/types.md
  30. +7
    -1
      docs/guides/deployment/deployment.md
  31. +14
    -6
      docs/guides/getting_started/installing.md
  32. +0
    -30
      docs/guides/getting_started/labs.md
  33. +8
    -4
      docs/guides/getting_started/terminology.md
  34. +1
    -1
      docs/guides/int_basics/modals/intro.md
  35. +2
    -0
      docs/guides/int_framework/autocompletion.md
  36. +0
    -13
      docs/guides/int_framework/dependency-injection.md
  37. +68
    -5
      docs/guides/int_framework/intro.md
  38. +20
    -0
      docs/guides/int_framework/samples/autocompletion/autocomplete-example.cs
  39. +15
    -3
      docs/guides/int_framework/samples/intro/autocomplete.cs
  40. +14
    -0
      docs/guides/int_framework/samples/intro/event.cs
  41. +6
    -1
      docs/guides/int_framework/samples/intro/groupmodule.cs
  42. +9
    -2
      docs/guides/int_framework/samples/intro/modal.cs
  43. +0
    -51
      docs/guides/text_commands/dependency-injection.md
  44. +1
    -1
      docs/guides/text_commands/intro.md
  45. +0
    -65
      docs/guides/text_commands/samples/dependency-injection/dependency_map_setup.cs
  46. +0
    -37
      docs/guides/text_commands/samples/dependency-injection/dependency_module.cs
  47. +0
    -29
      docs/guides/text_commands/samples/dependency-injection/dependency_module_noinject.cs
  48. +12
    -7
      docs/guides/toc.yml
  49. +1
    -1
      docs/guides/v2_v3_guide/v2_to_v3_guide.md
  50. +2
    -4
      docs/guides/voice/sending-voice.md
  51. +1
    -1
      experiment/Discord.Net.BuildOverrides/BuildOverrides.cs
  52. +3
    -3
      samples/BasicBot/_BasicBot.csproj
  53. +2
    -8
      samples/InteractionFramework/_InteractionFramework.csproj
  54. +10
    -5
      samples/ShardedClient/Services/InteractionHandlingService.cs
  55. +2
    -7
      samples/ShardedClient/_ShardedClient.csproj
  56. +3
    -6
      samples/TextCommandFramework/_TextCommandFramework.csproj
  57. +2
    -2
      samples/WebhookClient/_WebhookClient.csproj
  58. +2
    -0
      src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs
  59. +2
    -0
      src/Discord.Net.Commands/Discord.Net.Commands.csproj
  60. +13
    -0
      src/Discord.Net.Commands/IModuleBase.cs
  61. +12
    -0
      src/Discord.Net.Commands/ModuleBase.cs
  62. +3
    -3
      src/Discord.Net.Commands/Results/MatchResult.cs
  63. +5
    -1
      src/Discord.Net.Core/Discord.Net.Core.csproj
  64. +11
    -1
      src/Discord.Net.Core/DiscordConfig.cs
  65. +26
    -0
      src/Discord.Net.Core/DiscordErrorCode.cs
  66. +3
    -1
      src/Discord.Net.Core/Entities/Channels/ChannelType.cs
  67. +216
    -0
      src/Discord.Net.Core/Entities/Channels/IForumChannel.cs
  68. +11
    -0
      src/Discord.Net.Core/Entities/Channels/ITextChannel.cs
  69. +40
    -1
      src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs
  70. +42
    -0
      src/Discord.Net.Core/Entities/ForumTag.cs
  71. +43
    -43
      src/Discord.Net.Core/Entities/Guilds/GuildFeature.cs
  72. +6
    -2
      src/Discord.Net.Core/Entities/Guilds/IGuild.cs
  73. +1
    -1
      src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs
  74. +85
    -17
      src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs
  75. +33
    -0
      src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs
  76. +1
    -1
      src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs
  77. +52
    -0
      src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs
  78. +70
    -0
      src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs
  79. +71
    -1
      src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs
  80. +26
    -0
      src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs
  81. +36
    -0
      src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs
  82. +15
    -0
      src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs
  83. +25
    -1
      src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs
  84. +109
    -11
      src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs
  85. +4
    -4
      src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs
  86. +356
    -43
      src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs
  87. +39
    -0
      src/Discord.Net.Core/Entities/Messages/Embed.cs
  88. +31
    -0
      src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs
  89. +191
    -0
      src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs
  90. +31
    -0
      src/Discord.Net.Core/Entities/Messages/EmbedField.cs
  91. +31
    -0
      src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs
  92. +31
    -0
      src/Discord.Net.Core/Entities/Messages/EmbedImage.cs
  93. +31
    -0
      src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs
  94. +31
    -0
      src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs
  95. +31
    -0
      src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs
  96. +6
    -0
      src/Discord.Net.Core/Entities/Messages/IMessage.cs
  97. +13
    -2
      src/Discord.Net.Core/Entities/Messages/MessageReference.cs
  98. +42
    -17
      src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs
  99. +1
    -1
      src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs
  100. +3
    -3
      src/Discord.Net.Core/Entities/Users/IGuildUser.cs

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

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

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

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


+ 89
- 0
CHANGELOG.md View File

@@ -1,5 +1,94 @@
# 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)


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


+ 6
- 3
Discord.Net.targets View File

@@ -1,12 +1,12 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VersionPrefix>3.6.0</VersionPrefix>
<VersionPrefix>3.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
- 31
README.md View File

@@ -17,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)

@@ -33,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
- 1
docs/docfx.json View File

@@ -60,7 +60,7 @@
"overwrite": "_overwrites/**/**.md",
"globalMetadata": {
"_appTitle": "Discord.Net Documentation",
"_appFooter": "Discord.Net (c) 2015-2022 3.6.0",
"_appFooter": "Discord.Net (c) 2015-2022 3.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.




+ 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

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

+ 68
- 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`
@@ -278,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

@@ -290,6 +291,11 @@ 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
@@ -302,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
@@ -329,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

@@ -356,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
@@ -370,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


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

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

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

@@ -16,6 +16,11 @@ public class CommandGroupModule : InteractionModuleBase<SocketInteractionContext
// group-name subcommand-group-name echo
[SlashCommand("echo", "Echo an input")]
public async Task EchoSubcommand(string input)
=> await RespondAsync(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);
}
}

+ 9
- 2
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; }
@@ -22,10 +24,15 @@ public class FoodModal : IModal
[ModalInteraction("food_menu")]
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();


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

+ 12
- 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,8 +60,6 @@
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


+ 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
experiment/Discord.Net.BuildOverrides/BuildOverrides.cs View File

@@ -251,7 +251,7 @@ namespace Discord
private static Assembly _overrideDomain_Resolving(AssemblyLoadContext arg1, AssemblyName arg2)
{
// resolve the override id
var v = _loadedOverrides.FirstOrDefault(x => x.Value.Any(x => x.Assembly.FullName == arg1.Assemblies.FirstOrDefault().FullName));
var v = _loadedOverrides.FirstOrDefault(x => x.Value.Any(x => x.Assembly.FullName == arg1.Assemblies.First().FullName));

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


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

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

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

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

</Project>

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

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

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

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

</Project>

+ 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.RegisterCommandsToGuildAsync(1 /* implement */);
#else
await _service.RegisterCommandsGloballyAsync();
#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
}
}
}

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

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

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

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

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

</Project>

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

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

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

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

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

</Project>

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

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

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

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

</Project>

+ 2
- 0
src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs View File

@@ -206,6 +206,7 @@ namespace Discord.Commands

try
{
await instance.BeforeExecuteAsync(cmd).ConfigureAwait(false);
instance.BeforeExecute(cmd);

var task = method.Invoke(instance, args) as Task ?? Task.Delay(0);
@@ -221,6 +222,7 @@ namespace Discord.Commands
}
finally
{
await instance.AfterExecuteAsync(cmd).ConfigureAwait(false);
instance.AfterExecute(cmd);
(instance as IDisposable)?.Dispose();
}


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

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


+ 13
- 0
src/Discord.Net.Commands/IModuleBase.cs View File

@@ -1,4 +1,5 @@
using Discord.Commands.Builders;
using System.Threading.Tasks;

namespace Discord.Commands
{
@@ -13,12 +14,24 @@ namespace Discord.Commands
/// <param name="context">The context to set.</param>
void SetContext(ICommandContext context);

/// <summary>
/// Executed asynchronously before a command is run in this module base.
/// </summary>
/// <param name="command">The command thats about to run.</param>
Task BeforeExecuteAsync(CommandInfo command);

/// <summary>
/// Executed before a command is run in this module base.
/// </summary>
/// <param name="command">The command thats about to run.</param>
void BeforeExecute(CommandInfo command);

/// <summary>
/// Executed asynchronously after a command is run in this module base.
/// </summary>
/// <param name="command">The command thats about to run.</param>
Task AfterExecuteAsync(CommandInfo command);

/// <summary>
/// Executed after a command is ran in this module base.
/// </summary>


+ 12
- 0
src/Discord.Net.Commands/ModuleBase.cs View File

@@ -46,6 +46,11 @@ namespace Discord.Commands
return await Context.Channel.SendMessageAsync(message, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false);
}
/// <summary>
/// The method to execute asynchronously before executing the command.
/// </summary>
/// <param name="command">The <see cref="CommandInfo"/> of the command to be executed.</param>
protected virtual Task BeforeExecuteAsync(CommandInfo command) => Task.CompletedTask;
/// <summary>
/// The method to execute before executing the command.
/// </summary>
/// <param name="command">The <see cref="CommandInfo"/> of the command to be executed.</param>
@@ -53,6 +58,11 @@ namespace Discord.Commands
{
}
/// <summary>
/// The method to execute asynchronously after executing the command.
/// </summary>
/// <param name="command">The <see cref="CommandInfo"/> of the command to be executed.</param>
protected virtual Task AfterExecuteAsync(CommandInfo command) => Task.CompletedTask;
/// <summary>
/// The method to execute after executing the command.
/// </summary>
/// <param name="command">The <see cref="CommandInfo"/> of the command to be executed.</param>
@@ -76,7 +86,9 @@ namespace Discord.Commands
var newValue = context as T;
Context = newValue ?? throw new InvalidOperationException($"Invalid context type. Expected {typeof(T).Name}, got {context.GetType().Name}.");
}
Task IModuleBase.BeforeExecuteAsync(CommandInfo command) => BeforeExecuteAsync(command);
void IModuleBase.BeforeExecute(CommandInfo command) => BeforeExecute(command);
Task IModuleBase.AfterExecuteAsync(CommandInfo command) => AfterExecuteAsync(command);
void IModuleBase.AfterExecute(CommandInfo command) => AfterExecute(command);
void IModuleBase.OnModuleBuilding(CommandService commandService, ModuleBuilder builder) => OnModuleBuilding(commandService, builder);
#endregion


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

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

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

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

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


+ 5
- 1
src/Discord.Net.Core/Discord.Net.Core.csproj View File

@@ -7,6 +7,8 @@
<Description>The core components for the Discord.Net library.</Description>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">net6.0;net5.0;net461;netstandard2.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">net6.0;net5.0;netstandard2.0;netstandard2.1</TargetFrameworks>
<WarningLevel>5</WarningLevel>
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
@@ -14,7 +16,6 @@
<PackageReference Include="IDisposableAnalyzers" Version="3.4.15">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.1' ">
<PackageReference Include="System.Collections.Immutable" Version="1.3.1" />
@@ -25,4 +26,7 @@
<ItemGroup Condition=" '$(TargetFramework)' == 'net461' ">
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' != 'net461'">
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
</ItemGroup>
</Project>

+ 11
- 1
src/Discord.Net.Core/DiscordConfig.cs View File

@@ -18,7 +18,7 @@ namespace Discord
/// <see href="https://discord.com/developers/docs/reference#api-versioning">Discord API documentation</see>
/// .</para>
/// </returns>
public const int APIVersion = 9;
public const int APIVersion = 10;
/// <summary>
/// Returns the Voice API version Discord.Net uses.
/// </summary>
@@ -132,6 +132,16 @@ namespace Discord
/// </returns>
public const int MaxAuditLogEntriesPerBatch = 100;

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

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

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


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

@@ -66,6 +66,7 @@ namespace Discord
ActionSlowmode = 20016,
OnlyOwnerAction = 20018,
AnnouncementEditRatelimit = 20022,
UnderMinimumAge = 20024,
ChannelWriteRatelimit = 20028,
WriteRatelimitReached = 20029,
WordsNotAllowed = 20031,
@@ -88,7 +89,9 @@ namespace Discord
MaximumServerMembersReached = 30019,
MaximumServerCategoriesReached = 30030,
GuildTemplateAlreadyExists = 30031,
MaximumNumberOfApplicationCommandsReached = 30032,
MaximumThreadMembersReached = 30033,
MaxNumberOfDailyApplicationCommandCreatesHasBeenReached = 30034,
MaximumBansForNonGuildMembersReached = 30035,
MaximumBanFetchesReached = 30037,
MaximumUncompleteGuildScheduledEvents = 30038,
@@ -98,6 +101,7 @@ namespace Discord
#endregion

#region General Request Errors (40XXX)
BitrateIsTooHighForChannelOfThisType = 30052,
MaximumNumberOfEditsReached = 30046,
MaximumNumberOfPinnedThreadsInAForumChannelReached = 30047,
MaximumNumberOfTagsInAForumChannelReached = 30048,
@@ -108,12 +112,17 @@ namespace Discord
RequestEntityTooLarge = 40005,
FeatureDisabled = 40006,
UserBanned = 40007,
ConnectionHasBeenRevoked = 40012,
TargetUserNotInVoice = 40032,
MessageAlreadyCrossposted = 40033,
ApplicationNameAlreadyExists = 40041,
#endregion

#region Action Preconditions/Checks (50XXX)
ApplicationInteractionFailedToSend = 40043,
CannotSendAMessageInAForumChannel = 40058,
ThereAreNoTagsAvailableThatCanBeSetByNonModerators = 40066,
ATagIsRequiredToCreateAForumPostInThisChannel = 40067,
InteractionHasAlreadyBeenAcknowledged = 40060,
TagNamesMustBeUnique = 40061,
MissingPermissions = 50001,
@@ -132,6 +141,7 @@ namespace Discord
InvalidAuthenticationToken = 50014,
NoteTooLong = 50015,
ProvidedMessageDeleteCountOutOfBounds = 50016,
InvalidMFALevel = 50017,
InvalidPinChannel = 50019,
InvalidInvite = 50020,
CannotExecuteOnSystemMessage = 50021,
@@ -152,6 +162,7 @@ namespace Discord
InvalidMessageType = 50068,
PaymentSourceRequiredForGift = 50070,
CannotDeleteRequiredCommunityChannel = 50074,
CannotEditStickersWithinAMessage = 50080,
InvalidSticker = 50081,
CannotExecuteOnArchivedThread = 50083,
InvalidThreadNotificationSettings = 50084,
@@ -164,6 +175,10 @@ namespace Discord
#endregion

#region 2FA (60XXX)
OwnershipCannotBeTransferredToABotUser = 50132,
AssetResizeBelowTheMaximumSize= 50138,
UploadedFileNotFound = 50146,
MissingPermissionToSendThisSticker = 50600,
Requires2FA = 60003,
#endregion

@@ -176,6 +191,7 @@ namespace Discord
#endregion

#region API Status (130XXX)
ApplicationNotYetAvailable = 110001,
APIOverloaded = 130000,
#endregion

@@ -205,5 +221,15 @@ namespace Discord
CannotUpdateFinishedEvent = 180000,
FailedStageCreation = 180002,
#endregion
#region Forum & Automod
MessageWasBlockedByAutomaticModeration = 200000,
TitleWasBlockedByAutomaticModeration = 200001,
WebhooksPostedToForumChannelsMustHaveAThreadNameOrThreadId = 220001,
WebhooksPostedToForumChannelsCannotHaveBothAThreadNameAndThreadId = 220002,
WebhooksCanOnlyCreateThreadsInForumChannels = 220003,
WebhookServicesCannotBeUsedInForumChannels = 220004,
MessageBlockedByHarmfulLinksFilter = 240000,
#endregion
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

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

/// <summary>
/// Bulk-deletes multiple messages.
/// </summary>
/// <example>
/// <para>The following example gets 250 messages from the channel and deletes them.</para>
/// <code language="cs">
/// var messages = await voiceChannel.GetMessagesAsync(250).FlattenAsync();
/// await voiceChannel.DeleteMessagesAsync(messages);
/// </code>
/// </example>
/// <remarks>
/// This method attempts to remove the messages specified in bulk.
/// <note type="important">
/// Due to the limitation set by Discord, this method can only remove messages that are posted within 14 days!
/// </note>
/// </remarks>
/// <param name="messages">The messages to be bulk-deleted.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous bulk-removal operation.
/// </returns>
Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null);
/// <summary>
/// Bulk-deletes multiple messages.
/// </summary>
/// <remarks>
/// This method attempts to remove the messages specified in bulk.
/// <note type="important">
/// Due to the limitation set by Discord, this method can only remove messages that are posted within 14 days!
/// </note>
/// </remarks>
/// <param name="messageIds">The snowflake identifier of the messages to be bulk-deleted.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous bulk-removal operation.
/// </returns>
Task DeleteMessagesAsync(IEnumerable<ulong> messageIds, RequestOptions options = null);

/// <summary>
/// Modifies this voice channel.
/// </summary>


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

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

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

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

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

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

Id = id;
Name = name;
}
}
}

+ 43
- 43
src/Discord.Net.Core/Entities/Guilds/GuildFeature.cs View File

@@ -12,174 +12,174 @@ namespace Discord
/// <summary>
/// The guild has no features.
/// </summary>
None = 0,
None = 0L,
/// <summary>
/// The guild has access to animated banners.
/// </summary>
AnimatedBanner = 1 << 0,
AnimatedBanner = 1L << 0,
/// <summary>
/// The guild has access to set an animated guild icon.
/// </summary>
AnimatedIcon = 1 << 1,
AnimatedIcon = 1L << 1,
/// <summary>
/// The guild has access to set a guild banner image.
/// </summary>
Banner = 1 << 2,
Banner = 1L << 2,
/// <summary>
/// The guild has access to channel banners.
/// </summary>
ChannelBanner = 1 << 3,
ChannelBanner = 1L << 3,
/// <summary>
/// The guild has access to use commerce features (i.e. create store channels).
/// </summary>
Commerce = 1 << 4,
Commerce = 1L << 4,
/// <summary>
/// The guild can enable welcome screen, Membership Screening, stage channels and discovery, and receives community updates.
/// </summary>
Community = 1 << 5,
Community = 1L << 5,
/// <summary>
/// The guild is able to be discovered in the directory.
/// </summary>
Discoverable = 1 << 6,
Discoverable = 1L << 6,
/// <summary>
/// The guild has discoverable disabled.
/// </summary>
DiscoverableDisabled = 1 << 7,
DiscoverableDisabled = 1L << 7,
/// <summary>
/// The guild has enabled discoverable before.
/// </summary>
EnabledDiscoverableBefore = 1 << 8,
EnabledDiscoverableBefore = 1L << 8,
/// <summary>
/// The guild is able to be featured in the directory.
/// </summary>
Featureable = 1 << 9,
Featureable = 1L << 9,
/// <summary>
/// The guild has a force relay.
/// </summary>
ForceRelay = 1 << 10,
ForceRelay = 1L << 10,
/// <summary>
/// The guild has a directory entry.
/// </summary>
HasDirectoryEntry = 1 << 11,
HasDirectoryEntry = 1L << 11,
/// <summary>
/// The guild is a hub.
/// </summary>
Hub = 1 << 12,
Hub = 1L << 12,
/// <summary>
/// You shouldn't be here...
/// </summary>
InternalEmployeeOnly = 1 << 13,
InternalEmployeeOnly = 1L << 13,
/// <summary>
/// The guild has access to set an invite splash background.
/// </summary>
InviteSplash = 1 << 14,
InviteSplash = 1L << 14,
/// <summary>
/// The guild is linked to a hub.
/// </summary>
LinkedToHub = 1 << 15,
LinkedToHub = 1L << 15,
/// <summary>
/// The guild has member profiles.
/// </summary>
MemberProfiles = 1 << 16,
MemberProfiles = 1L << 16,
/// <summary>
/// The guild has enabled <seealso href="https://discord.com/developers/docs/resources/guild#membership-screening-object">Membership Screening</seealso>.
/// </summary>
MemberVerificationGateEnabled = 1 << 17,
MemberVerificationGateEnabled = 1L << 17,
/// <summary>
/// The guild has enabled monetization.
/// </summary>
MonetizationEnabled = 1 << 18,
MonetizationEnabled = 1L << 18,
/// <summary>
/// The guild has more emojis.
/// </summary>
MoreEmoji = 1 << 19,
MoreEmoji = 1L << 19,
/// <summary>
/// The guild has increased custom sticker slots.
/// </summary>
MoreStickers = 1 << 20,
MoreStickers = 1L << 20,
/// <summary>
/// The guild has access to create news channels.
/// </summary>
News = 1 << 21,
News = 1L << 21,
/// <summary>
/// The guild has new thread permissions.
/// </summary>
NewThreadPermissions = 1 << 22,
NewThreadPermissions = 1L << 22,
/// <summary>
/// The guild is partnered.
/// </summary>
Partnered = 1 << 23,
Partnered = 1L << 23,
/// <summary>
/// The guild has a premium tier three override; guilds made by Discord usually have this.
/// </summary>
PremiumTier3Override = 1 << 24,
PremiumTier3Override = 1L << 24,
/// <summary>
/// The guild can be previewed before joining via Membership Screening or the directory.
/// </summary>
PreviewEnabled = 1 << 25,
PreviewEnabled = 1L << 25,
/// <summary>
/// The guild has access to create private threads.
/// </summary>
PrivateThreads = 1 << 26,
PrivateThreads = 1L << 26,
/// <summary>
/// The guild has relay enabled.
/// </summary>
RelayEnabled = 1 << 27,
RelayEnabled = 1L << 27,
/// <summary>
/// The guild is able to set role icons.
/// </summary>
RoleIcons = 1 << 28,
RoleIcons = 1L << 28,
/// <summary>
/// The guild has role subscriptions available for purchase.
/// </summary>
RoleSubscriptionsAvailableForPurchase = 1 << 29,
RoleSubscriptionsAvailableForPurchase = 1L << 29,
/// <summary>
/// The guild has role subscriptions enabled.
/// </summary>
RoleSubscriptionsEnabled = 1 << 30,
RoleSubscriptionsEnabled = 1L << 30,
/// <summary>
/// The guild has access to the seven day archive time for threads.
/// </summary>
SevenDayThreadArchive = 1 << 31,
SevenDayThreadArchive = 1L << 31,
/// <summary>
/// The guild has text in voice enabled.
/// </summary>
TextInVoiceEnabled = 1 << 32,
TextInVoiceEnabled = 1L << 32,
/// <summary>
/// The guild has threads enabled.
/// </summary>
ThreadsEnabled = 1 << 33,
ThreadsEnabled = 1L << 33,
/// <summary>
/// The guild has testing threads enabled.
/// </summary>
ThreadsEnabledTesting = 1 << 34,
ThreadsEnabledTesting = 1L << 34,
/// <summary>
/// The guild has the default thread auto archive.
/// </summary>
ThreadsDefaultAutoArchiveDuration = 1 << 35,
ThreadsDefaultAutoArchiveDuration = 1L << 35,
/// <summary>
/// The guild has access to the three day archive time for threads.
/// </summary>
ThreeDayThreadArchive = 1 << 36,
ThreeDayThreadArchive = 1L << 36,
/// <summary>
/// The guild has enabled ticketed events.
/// </summary>
TicketedEventsEnabled = 1 << 37,
TicketedEventsEnabled = 1L << 37,
/// <summary>
/// The guild has access to set a vanity URL.
/// </summary>
VanityUrl = 1 << 38,
VanityUrl = 1L << 38,
/// <summary>
/// The guild is verified.
/// </summary>
Verified = 1 << 39,
Verified = 1L << 39,
/// <summary>
/// The guild has access to set 384kbps bitrate in voice (previously VIP voice servers).
/// </summary>
VIPRegions = 1 << 40,
VIPRegions = 1L << 40,
/// <summary>
/// The guild has enabled the welcome screen.
/// </summary>
WelcomeScreenEnabled = 1 << 41,
WelcomeScreenEnabled = 1L << 41,
}
}

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

@@ -1173,7 +1173,6 @@ namespace Discord
/// in order to use this property.
/// </remarks>
/// </param>
/// <param name="speakers">A collection of speakers for the event.</param>
/// <param name="location">The location of the event; links are supported</param>
/// <param name="coverImage">The optional banner image for the event.</param>
/// <param name="options">The options to be used when sending the request.</param>
@@ -1195,12 +1194,17 @@ namespace Discord
/// <summary>
/// Gets this guilds application commands.
/// </summary>
/// <param name="withLocalizations">
/// Whether to include full localization dictionaries in the returned objects,
/// instead of the localized name and description fields.
/// </param>
/// <param name="locale">The target locale of the localized name and description fields. Sets the <c>X-Discord-Locale</c> header, which takes precedence over <c>Accept-Language</c>.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains a read-only collection
/// of application commands found within the guild.
/// </returns>
Task<IReadOnlyCollection<IApplicationCommand>> GetApplicationCommandsAsync(RequestOptions options = null);
Task<IReadOnlyCollection<IApplicationCommand>> GetApplicationCommandsAsync(bool withLocalizations = false, string locale = null, RequestOptions options = null);

/// <summary>
/// Gets an application command within this guild with the specified id.


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

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



+ 85
- 17
src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs View File

@@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
@@ -12,6 +13,8 @@ namespace Discord
{
private string _name;
private string _description;
private IDictionary<string, string> _nameLocalizations = new Dictionary<string, string>();
private IDictionary<string, string> _descriptionLocalizations = new Dictionary<string, string>();

/// <summary>
/// Gets or sets the name of this option.
@@ -21,18 +24,7 @@ namespace Discord
get => _name;
set
{
if (value == null)
throw new ArgumentNullException(nameof(value), $"{nameof(Name)} cannot be null.");

if (value.Length > 32)
throw new ArgumentOutOfRangeException(nameof(value), "Name length must be less than or equal to 32.");

if (!Regex.IsMatch(value, @"^[\w-]{1,32}$"))
throw new FormatException($"{nameof(value)} must match the regex ^[\\w-]{{1,32}}$");

if (value.Any(x => char.IsUpper(x)))
throw new FormatException("Name cannot contain any uppercase characters.");

EnsureValidOptionName(value);
_name = value;
}
}
@@ -43,12 +35,11 @@ namespace Discord
public string Description
{
get => _description;
set => _description = value?.Length switch
set
{
> 100 => throw new ArgumentOutOfRangeException(nameof(value), "Description length must be less than or equal to 100."),
0 => throw new ArgumentOutOfRangeException(nameof(value), "Description length must be at least 1."),
_ => value
};
EnsureValidOptionDescription(value);
_description = value;
}
}

/// <summary>
@@ -81,6 +72,16 @@ namespace Discord
/// </summary>
public double? MaxValue { get; set; }

/// <summary>
/// Gets or sets the minimum allowed length for a string input.
/// </summary>
public int? MinLength { get; set; }

/// <summary>
/// Gets or sets the maximum allowed length for a string input.
/// </summary>
public int? MaxLength { get; set; }

/// <summary>
/// Gets or sets the choices for string and int types for the user to pick from.
/// </summary>
@@ -95,5 +96,72 @@ namespace Discord
/// Gets or sets the allowed channel types for this option.
/// </summary>
public List<ChannelType> ChannelTypes { get; set; }

/// <summary>
/// Gets or sets the localization dictionary for the name field of this option.
/// </summary>
/// <exception cref="ArgumentException">Thrown when any of the dictionary keys is an invalid locale.</exception>
public IDictionary<string, string> NameLocalizations
{
get => _nameLocalizations;
set
{
foreach (var (locale, name) in value)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

EnsureValidOptionName(name);
}
_nameLocalizations = value;
}
}

/// <summary>
/// Gets or sets the localization dictionary for the description field of this option.
/// </summary>
/// <exception cref="ArgumentException">Thrown when any of the dictionary keys is an invalid locale.</exception>
public IDictionary<string, string> DescriptionLocalizations
{
get => _descriptionLocalizations;
set
{
foreach (var (locale, description) in value)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

EnsureValidOptionDescription(description);
}
_descriptionLocalizations = value;
}
}

private static void EnsureValidOptionName(string name)
{
if (name == null)
throw new ArgumentNullException(nameof(name), $"{nameof(Name)} cannot be null.");

if (name.Length > 32)
throw new ArgumentOutOfRangeException(nameof(name), "Name length must be less than or equal to 32.");

if (!Regex.IsMatch(name, @"^[\w-]{1,32}$"))
throw new FormatException($"{nameof(name)} must match the regex ^[\\w-]{{1,32}}$");

if (name.Any(x => char.IsUpper(x)))
throw new FormatException("Name cannot contain any uppercase characters.");
}

private static void EnsureValidOptionDescription(string description)
{
switch (description.Length)
{
case > 100:
throw new ArgumentOutOfRangeException(nameof(description),
"Description length must be less than or equal to 100.");
case 0:
throw new ArgumentOutOfRangeException(nameof(description), "Description length must at least 1.");
}
}
}
}

+ 33
- 0
src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs View File

@@ -1,4 +1,8 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace Discord
{
@@ -9,6 +13,7 @@ namespace Discord
{
private string _name;
private object _value;
private IDictionary<string, string> _nameLocalizations = new Dictionary<string, string>();

/// <summary>
/// Gets or sets the name of this choice.
@@ -40,5 +45,33 @@ namespace Discord
_value = value;
}
}

/// <summary>
/// Gets or sets the localization dictionary for the name field of this choice.
/// </summary>
/// <exception cref="ArgumentException">Thrown when any of the dictionary keys is an invalid locale.</exception>
public IDictionary<string, string> NameLocalizations
{
get => _nameLocalizations;
set
{
foreach (var (locale, name) in value)
{
if (!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException("Key values of the dictionary must be valid language codes.");

switch (name.Length)
{
case > 100:
throw new ArgumentOutOfRangeException(nameof(value),
"Name length must be less than or equal to 100.");
case 0:
throw new ArgumentOutOfRangeException(nameof(value), "Name length must at least 1.");
}
}

_nameLocalizations = value;
}
}
}
}

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

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

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


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

@@ -1,3 +1,10 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text.RegularExpressions;

namespace Discord
{
/// <summary>
@@ -5,6 +12,9 @@ namespace Discord
/// </summary>
public abstract class ApplicationCommandProperties
{
private IReadOnlyDictionary<string, string> _nameLocalizations;
private IReadOnlyDictionary<string, string> _descriptionLocalizations;

internal abstract ApplicationCommandType Type { get; }

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

/// <summary>
/// Gets or sets the localization dictionary for the name field of this command.
/// </summary>
public IReadOnlyDictionary<string, string> NameLocalizations
{
get => _nameLocalizations;
set
{
foreach (var (locale, name) in value)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

Preconditions.AtLeast(name.Length, 1, nameof(name));
Preconditions.AtMost(name.Length, SlashCommandBuilder.MaxNameLength, nameof(name));
if (!Regex.IsMatch(name, @"^[\w-]{1,32}$"))
throw new ArgumentException("Option name cannot contain any special characters or whitespaces!", nameof(name));
}
_nameLocalizations = value;
}
}

/// <summary>
/// Gets or sets the localization dictionary for the description field of this command.
/// </summary>
public IReadOnlyDictionary<string, string> DescriptionLocalizations
{
get => _descriptionLocalizations;
set
{
foreach (var (locale, description) in value)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

Preconditions.AtLeast(description.Length, 1, nameof(description));
Preconditions.AtMost(description.Length, SlashCommandBuilder.MaxDescriptionLength, nameof(description));
}
_descriptionLocalizations = value;
}
}

/// <summary>
/// Gets or sets whether or not this command can be used in DMs.
/// </summary>


+ 70
- 0
src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs View File

@@ -1,3 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

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

/// <summary>
/// Gets the localization dictionary for the name field of this command.
/// </summary>
public IReadOnlyDictionary<string, string> NameLocalizations => _nameLocalizations;

/// <summary>
/// Gets or sets whether or not this command can be used in DMs.
/// </summary>
@@ -42,6 +52,7 @@ namespace Discord
public GuildPermission? DefaultMemberPermissions { get; set; }

private string _name;
private Dictionary<string, string> _nameLocalizations;

/// <summary>
/// Build the current builder into a <see cref="MessageCommandProperties"/> class.
@@ -86,6 +97,30 @@ namespace Discord
return this;
}

/// <summary>
/// Sets the <see cref="NameLocalizations"/> collection.
/// </summary>
/// <param name="nameLocalizations">The localization dictionary to use for the name field of this command.</param>
/// <returns></returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="nameLocalizations"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown if any dictionary key is an invalid locale string.</exception>
public MessageCommandBuilder WithNameLocalizations(IDictionary<string, string> nameLocalizations)
{
if (nameLocalizations is null)
throw new ArgumentNullException(nameof(nameLocalizations));

foreach (var (locale, name) in nameLocalizations)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

EnsureValidCommandName(name);
}

_nameLocalizations = new Dictionary<string, string>(nameLocalizations);
return this;
}

/// <summary>
/// Sets whether or not this command can be used in dms
/// </summary>
@@ -97,6 +132,41 @@ namespace Discord
return this;
}

/// <summary>
/// Adds a new entry to the <see cref="NameLocalizations"/> collection.
/// </summary>
/// <param name="locale">Locale of the entry.</param>
/// <param name="name">Localized string for the name field.</param>
/// <returns>The current builder.</returns>
/// <exception cref="ArgumentException">Thrown if <paramref name="locale"/> is an invalid locale string.</exception>
public MessageCommandBuilder AddNameLocalization(string locale, string name)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

EnsureValidCommandName(name);

_nameLocalizations ??= new();
_nameLocalizations.Add(locale, name);

return this;
}

private static void EnsureValidCommandName(string name)
{
Preconditions.NotNullOrEmpty(name, nameof(name));
Preconditions.AtLeast(name.Length, 1, nameof(name));
Preconditions.AtMost(name.Length, MaxNameLength, nameof(name));

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

if (name.Any(x => char.IsUpper(x)))
throw new FormatException("Name cannot contain any uppercase characters.");
}

/// <summary>
/// Sets the default member permissions required to use this application command.
/// </summary>


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

@@ -1,3 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace Discord
{
/// <summary>
@@ -5,7 +10,7 @@ namespace Discord
/// </summary>
public class UserCommandBuilder
{
/// <summary>
/// <summary>
/// Returns the maximum length a commands name allowed by Discord.
/// </summary>
public const int MaxNameLength = 32;
@@ -31,6 +36,11 @@ namespace Discord
/// </summary>
public bool IsDefaultPermission { get; set; } = true;

/// <summary>
/// Gets the localization dictionary for the name field of this command.
/// </summary>
public IReadOnlyDictionary<string, string> NameLocalizations => _nameLocalizations;

/// <summary>
/// Gets or sets whether or not this command can be used in DMs.
/// </summary>
@@ -42,6 +52,7 @@ namespace Discord
public GuildPermission? DefaultMemberPermissions { get; set; }

private string _name;
private Dictionary<string, string> _nameLocalizations;

/// <summary>
/// Build the current builder into a <see cref="UserCommandProperties"/> class.
@@ -84,6 +95,30 @@ namespace Discord
return this;
}

/// <summary>
/// Sets the <see cref="NameLocalizations"/> collection.
/// </summary>
/// <param name="nameLocalizations">The localization dictionary to use for the name field of this command.</param>
/// <returns>The current builder.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="nameLocalizations"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown if any dictionary key is an invalid locale string.</exception>
public UserCommandBuilder WithNameLocalizations(IDictionary<string, string> nameLocalizations)
{
if (nameLocalizations is null)
throw new ArgumentNullException(nameof(nameLocalizations));

foreach (var (locale, name) in nameLocalizations)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

EnsureValidCommandName(name);
}

_nameLocalizations = new Dictionary<string, string>(nameLocalizations);
return this;
}
/// <summary>
/// Sets whether or not this command can be used in dms
/// </summary>
@@ -95,6 +130,41 @@ namespace Discord
return this;
}

/// <summary>
/// Adds a new entry to the <see cref="NameLocalizations"/> collection.
/// </summary>
/// <param name="locale">Locale of the entry.</param>
/// <param name="name">Localized string for the name field.</param>
/// <returns>The current builder.</returns>
/// <exception cref="ArgumentException">Thrown if <paramref name="locale"/> is an invalid locale string.</exception>
public UserCommandBuilder AddNameLocalization(string locale, string name)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

EnsureValidCommandName(name);

_nameLocalizations ??= new();
_nameLocalizations.Add(locale, name);

return this;
}

private static void EnsureValidCommandName(string name)
{
Preconditions.NotNullOrEmpty(name, nameof(name));
Preconditions.AtLeast(name.Length, 1, nameof(name));
Preconditions.AtMost(name.Length, MaxNameLength, nameof(name));

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

if (name.Any(x => char.IsUpper(x)))
throw new FormatException("Name cannot contain any uppercase characters.");
}

/// <summary>
/// Sets the default member permissions required to use this application command.
/// </summary>


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

@@ -52,6 +52,32 @@ namespace Discord
/// </summary>
IReadOnlyCollection<IApplicationCommandOption> Options { get; }

/// <summary>
/// Gets the localization dictionary for the name field of this command.
/// </summary>
IReadOnlyDictionary<string, string> NameLocalizations { get; }

/// <summary>
/// Gets the localization dictionary for the description field of this command.
/// </summary>
IReadOnlyDictionary<string, string> DescriptionLocalizations { get; }

/// <summary>
/// Gets the localized name of this command.
/// </summary>
/// <remarks>
/// Only returned when the `withLocalizations` query parameter is set to <see langword="false"/> when requesting the command.
/// </remarks>
string NameLocalized { get; }

/// <summary>
/// Gets the localized description of this command.
/// </summary>
/// <remarks>
/// Only returned when the `withLocalizations` query parameter is set to <see langword="false"/> when requesting the command.
/// </remarks>
string DescriptionLocalized { get; }

/// <summary>
/// Modifies the current application command.
/// </summary>


+ 36
- 0
src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs View File

@@ -47,6 +47,16 @@ namespace Discord
/// </summary>
double? MaxValue { get; }

/// <summary>
/// Gets the minimum allowed length for a string input.
/// </summary>
int? MinLength { get; }

/// <summary>
/// Gets the maximum allowed length for a string input.
/// </summary>
int? MaxLength { get; }

/// <summary>
/// Gets the choices for string and int types for the user to pick from.
/// </summary>
@@ -61,5 +71,31 @@ namespace Discord
/// Gets the allowed channel types for this option.
/// </summary>
IReadOnlyCollection<ChannelType> ChannelTypes { get; }

/// <summary>
/// Gets the localization dictionary for the name field of this command option.
/// </summary>
IReadOnlyDictionary<string, string> NameLocalizations { get; }

/// <summary>
/// Gets the localization dictionary for the description field of this command option.
/// </summary>
IReadOnlyDictionary<string, string> DescriptionLocalizations { get; }

/// <summary>
/// Gets the localized name of this command option.
/// </summary>
/// <remarks>
/// Only returned when the `withLocalizations` query parameter is set to <see langword="false"/> when requesting the command.
/// </remarks>
string NameLocalized { get; }

/// <summary>
/// Gets the localized description of this command option.
/// </summary>
/// <remarks>
/// Only returned when the `withLocalizations` query parameter is set to true when requesting the command.
/// </remarks>
string DescriptionLocalized { get; }
}
}

+ 15
- 0
src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs View File

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

namespace Discord
{
/// <summary>
@@ -14,5 +16,18 @@ namespace Discord
/// Gets the value of the choice.
/// </summary>
object Value { get; }

/// <summary>
/// Gets the localization dictionary for the name field of this command option.
/// </summary>
IReadOnlyDictionary<string, string> NameLocalizations { get; }

/// <summary>
/// Gets the localized name of this command option.
/// </summary>
/// <remarks>
/// Only returned when the `withLocalizations` query parameter is set to <see langword="false"/> when requesting the command.
/// </remarks>
string NameLocalized { get; }
}
}

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

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

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

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

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

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

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


+ 109
- 11
src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs View File

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

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

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

var builtMenu = menu.Build();

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

AddComponent(builtMenu);

return this;
}

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

return WithButton(button);
}

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

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

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

AddComponent(builtButton);

return this;
}

/// <summary>
/// Builds the current builder to a <see cref="ActionRowComponent"/> that can be used within a <see cref="ComponentBuilder"/>
/// </summary>
@@ -1104,6 +1198,10 @@ namespace Discord

public class TextInputBuilder
{
/// <summary>
/// The max length of a <see cref="TextInputComponent.Placeholder"/>.
/// </summary>
public const int MaxPlaceholderLength = 100;
public const int LargestMaxLength = 4000;

/// <summary>
@@ -1135,13 +1233,13 @@ namespace Discord
/// <summary>
/// Gets or sets the placeholder of the current text input.
/// </summary>
/// <exception cref="ArgumentException"><see cref="Placeholder"/> is longer than 100 characters</exception>
/// <exception cref="ArgumentException"><see cref="Placeholder"/> is longer than <see cref="MaxPlaceholderLength"/> characters</exception>
public string Placeholder
{
get => _placeholder;
set => _placeholder = (value?.Length ?? 0) <= 100
set => _placeholder = (value?.Length ?? 0) <= MaxPlaceholderLength
? value
: throw new ArgumentException("Placeholder cannot have more than 100 characters.");
: throw new ArgumentException($"Placeholder cannot have more than {MaxPlaceholderLength} characters.");
}

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


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

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


+ 356
- 43
src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs View File

@@ -1,6 +1,9 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Net.Sockets;
using System.Text.RegularExpressions;

namespace Discord
@@ -31,18 +34,7 @@ namespace Discord
get => _name;
set
{
Preconditions.NotNullOrEmpty(value, nameof(value));
Preconditions.AtLeast(value.Length, 1, nameof(value));
Preconditions.AtMost(value.Length, MaxNameLength, nameof(value));

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

if (value.Any(x => char.IsUpper(x)))
throw new FormatException("Name cannot contain any uppercase characters.");

EnsureValidCommandName(value);
_name = value;
}
}
@@ -55,10 +47,7 @@ namespace Discord
get => _description;
set
{
Preconditions.NotNullOrEmpty(value, nameof(Description));
Preconditions.AtLeast(value.Length, 1, nameof(Description));
Preconditions.AtMost(value.Length, MaxDescriptionLength, nameof(Description));

EnsureValidCommandDescription(value);
_description = value;
}
}
@@ -76,6 +65,16 @@ namespace Discord
}
}

/// <summary>
/// Gets the localization dictionary for the name field of this command.
/// </summary>
public IReadOnlyDictionary<string, string> NameLocalizations => _nameLocalizations;

/// <summary>
/// Gets the localization dictionary for the description field of this command.
/// </summary>
public IReadOnlyDictionary<string, string> DescriptionLocalizations => _descriptionLocalizations;

/// <summary>
/// Gets or sets whether the command is enabled by default when the app is added to a guild
/// </summary>
@@ -93,6 +92,8 @@ namespace Discord

private string _name;
private string _description;
private Dictionary<string, string> _nameLocalizations;
private Dictionary<string, string> _descriptionLocalizations;
private List<SlashCommandOptionBuilder> _options;

/// <summary>
@@ -106,6 +107,8 @@ namespace Discord
Name = Name,
Description = Description,
IsDefaultPermission = IsDefaultPermission,
NameLocalizations = _nameLocalizations,
DescriptionLocalizations = _descriptionLocalizations,
IsDMEnabled = IsDMEnabled,
DefaultMemberPermissions = DefaultMemberPermissions ?? Optional<GuildPermission>.Unspecified
};
@@ -190,13 +193,17 @@ namespace Discord
/// <param name="isAutocomplete">If this option is set to autocomplete.</param>
/// <param name="options">The options of the option to add.</param>
/// <param name="channelTypes">The allowed channel types for this option.</param>
/// <param name="nameLocalizations">Localization dictionary for the name field of this command.</param>
/// <param name="descriptionLocalizations">Localization dictionary for the description field of this command.</param>
/// <param name="choices">The choices of this option.</param>
/// <param name="minValue">The smallest number value the user can input.</param>
/// <param name="maxValue">The largest number value the user can input.</param>
/// <returns>The current builder.</returns>
public SlashCommandBuilder AddOption(string name, ApplicationCommandOptionType type,
string description, bool? isRequired = null, bool? isDefault = null, bool isAutocomplete = false, double? minValue = null, double? maxValue = null,
List<SlashCommandOptionBuilder> options = null, List<ChannelType> channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices)
List<SlashCommandOptionBuilder> options = null, List<ChannelType> channelTypes = null, IDictionary<string, string> nameLocalizations = null,
IDictionary<string, string> descriptionLocalizations = null,
int? minLength = null, int? maxLength = null, params ApplicationCommandOptionChoiceProperties[] choices)
{
Preconditions.Options(name, description);

@@ -222,8 +229,16 @@ namespace Discord
ChannelTypes = channelTypes,
MinValue = minValue,
MaxValue = maxValue,
MinLength = minLength,
MaxLength = maxLength,
};

if (nameLocalizations is not null)
option.WithNameLocalizations(nameLocalizations);

if (descriptionLocalizations is not null)
option.WithDescriptionLocalizations(descriptionLocalizations);

return AddOption(option);
}

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

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

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

if (Options.Count + options.Length > MaxOptionsCount)
@@ -269,6 +281,116 @@ namespace Discord
Options.AddRange(options);
return this;
}

/// <summary>
/// Sets the <see cref="NameLocalizations"/> collection.
/// </summary>
/// <param name="nameLocalizations">The localization dictionary to use for the name field of this command.</param>
/// <returns></returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="nameLocalizations"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown if any dictionary key is an invalid locale string.</exception>
public SlashCommandBuilder WithNameLocalizations(IDictionary<string, string> nameLocalizations)
{
if (nameLocalizations is null)
throw new ArgumentNullException(nameof(nameLocalizations));

foreach (var (locale, name) in nameLocalizations)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

EnsureValidCommandName(name);
}

_nameLocalizations = new Dictionary<string, string>(nameLocalizations);
return this;
}

/// <summary>
/// Sets the <see cref="DescriptionLocalizations"/> collection.
/// </summary>
/// <param name="descriptionLocalizations">The localization dictionary to use for the description field of this command.</param>
/// <returns></returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="descriptionLocalizations"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown if any dictionary key is an invalid locale string.</exception>
public SlashCommandBuilder WithDescriptionLocalizations(IDictionary<string, string> descriptionLocalizations)
{
if (descriptionLocalizations is null)
throw new ArgumentNullException(nameof(descriptionLocalizations));

foreach (var (locale, description) in descriptionLocalizations)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

EnsureValidCommandDescription(description);
}

_descriptionLocalizations = new Dictionary<string, string>(descriptionLocalizations);
return this;
}

/// <summary>
/// Adds a new entry to the <see cref="NameLocalizations"/> collection.
/// </summary>
/// <param name="locale">Locale of the entry.</param>
/// <param name="name">Localized string for the name field.</param>
/// <returns>The current builder.</returns>
/// <exception cref="ArgumentException">Thrown if <paramref name="locale"/> is an invalid locale string.</exception>
public SlashCommandBuilder AddNameLocalization(string locale, string name)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

EnsureValidCommandName(name);

_nameLocalizations ??= new();
_nameLocalizations.Add(locale, name);

return this;
}

/// <summary>
/// Adds a new entry to the <see cref="Description"/> collection.
/// </summary>
/// <param name="locale">Locale of the entry.</param>
/// <param name="description">Localized string for the description field.</param>
/// <returns>The current builder.</returns>
/// <exception cref="ArgumentException">Thrown if <paramref name="locale"/> is an invalid locale string.</exception>
public SlashCommandBuilder AddDescriptionLocalization(string locale, string description)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

EnsureValidCommandDescription(description);

_descriptionLocalizations ??= new();
_descriptionLocalizations.Add(locale, description);

return this;
}

internal static void EnsureValidCommandName(string name)
{
Preconditions.NotNullOrEmpty(name, nameof(name));
Preconditions.AtLeast(name.Length, 1, nameof(name));
Preconditions.AtMost(name.Length, MaxNameLength, nameof(name));

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

if (name.Any(x => char.IsUpper(x)))
throw new FormatException("Name cannot contain any uppercase characters.");
}

internal static void EnsureValidCommandDescription(string description)
{
Preconditions.NotNullOrEmpty(description, nameof(description));
Preconditions.AtLeast(description.Length, 1, nameof(description));
Preconditions.AtMost(description.Length, MaxDescriptionLength, nameof(description));
}
}

/// <summary>
@@ -288,6 +410,8 @@ namespace Discord

private string _name;
private string _description;
private Dictionary<string, string> _nameLocalizations;
private Dictionary<string, string> _descriptionLocalizations;

/// <summary>
/// Gets or sets the name of this option.
@@ -299,10 +423,7 @@ namespace Discord
{
if (value != null)
{
Preconditions.AtLeast(value.Length, 1, nameof(value));
Preconditions.AtMost(value.Length, SlashCommandBuilder.MaxNameLength, nameof(value));
if (!Regex.IsMatch(value, @"^[\w-]{1,32}$"))
throw new ArgumentException("Option name cannot contain any special characters or whitespaces!", nameof(value));
EnsureValidCommandOptionName(value);
}

_name = value;
@@ -319,8 +440,7 @@ namespace Discord
{
if (value != null)
{
Preconditions.AtLeast(value.Length, 1, nameof(value));
Preconditions.AtMost(value.Length, SlashCommandBuilder.MaxDescriptionLength, nameof(value));
EnsureValidCommandOptionDescription(value);
}

_description = value;
@@ -357,6 +477,16 @@ namespace Discord
/// </summary>
public double? MaxValue { get; set; }

/// <summary>
/// Gets or sets the minimum allowed length for a string input.
/// </summary>
public int? MinLength { get; set; }

/// <summary>
/// Gets or sets the maximum allowed length for a string input.
/// </summary>
public int? MaxLength { get; set; }

/// <summary>
/// Gets or sets the choices for string and int types for the user to pick from.
/// </summary>
@@ -372,6 +502,16 @@ namespace Discord
/// </summary>
public List<ChannelType> ChannelTypes { get; set; }

/// <summary>
/// Gets the localization dictionary for the name field of this command.
/// </summary>
public IReadOnlyDictionary<string, string> NameLocalizations => _nameLocalizations;

/// <summary>
/// Gets the localization dictionary for the description field of this command.
/// </summary>
public IReadOnlyDictionary<string, string> DescriptionLocalizations => _descriptionLocalizations;

/// <summary>
/// Builds the current option.
/// </summary>
@@ -380,6 +520,7 @@ namespace Discord
{
bool isSubType = Type == ApplicationCommandOptionType.SubCommandGroup;
bool isIntType = Type == ApplicationCommandOptionType.Integer;
bool isStrType = Type == ApplicationCommandOptionType.String;

if (isSubType && (Options == null || !Options.Any()))
throw new InvalidOperationException("SubCommands/SubCommandGroups must have at least one option");
@@ -393,6 +534,12 @@ namespace Discord
if (isIntType && MaxValue != null && MaxValue % 1 != 0)
throw new InvalidOperationException("MaxValue cannot have decimals on Integer command options.");

if(isStrType && MinLength is not null && MinLength < 0)
throw new InvalidOperationException("MinLength cannot be smaller than 0.");

if (isStrType && MaxLength is not null && MaxLength < 1)
throw new InvalidOperationException("MaxLength cannot be smaller than 1.");

return new ApplicationCommandOptionProperties
{
Name = Name,
@@ -407,9 +554,13 @@ namespace Discord
IsAutocomplete = IsAutocomplete,
ChannelTypes = ChannelTypes,
MinValue = MinValue,
MaxValue = MaxValue
MaxValue = MaxValue,
NameLocalizations = _nameLocalizations,
DescriptionLocalizations = _descriptionLocalizations,
MinLength = MinLength,
MaxLength = MaxLength,
};
}
}

/// <summary>
/// Adds an option to the current slash command.
@@ -422,13 +573,17 @@ namespace Discord
/// <param name="isAutocomplete">If this option supports autocomplete.</param>
/// <param name="options">The options of the option to add.</param>
/// <param name="channelTypes">The allowed channel types for this option.</param>
/// <param name="nameLocalizations">Localization dictionary for the description field of this command.</param>
/// <param name="descriptionLocalizations">Localization dictionary for the description field of this command.</param>
/// <param name="choices">The choices of this option.</param>
/// <param name="minValue">The smallest number value the user can input.</param>
/// <param name="maxValue">The largest number value the user can input.</param>
/// <returns>The current builder.</returns>
public SlashCommandOptionBuilder AddOption(string name, ApplicationCommandOptionType type,
string description, bool? isRequired = null, bool isDefault = false, bool isAutocomplete = false, double? minValue = null, double? maxValue = null,
List<SlashCommandOptionBuilder> options = null, List<ChannelType> channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices)
List<SlashCommandOptionBuilder> options = null, List<ChannelType> channelTypes = null, IDictionary<string, string> nameLocalizations = null,
IDictionary<string, string> descriptionLocalizations = null,
int? minLength = null, int? maxLength = null, params ApplicationCommandOptionChoiceProperties[] choices)
{
Preconditions.Options(name, description);

@@ -450,12 +605,20 @@ namespace Discord
IsAutocomplete = isAutocomplete,
MinValue = minValue,
MaxValue = maxValue,
MinLength = minLength,
MaxLength = maxLength,
Options = options,
Type = type,
Choices = (choices ?? Array.Empty<ApplicationCommandOptionChoiceProperties>()).ToList(),
ChannelTypes = channelTypes
ChannelTypes = channelTypes,
};

if(nameLocalizations is not null)
option.WithNameLocalizations(nameLocalizations);

if(descriptionLocalizations is not null)
option.WithDescriptionLocalizations(descriptionLocalizations);

return AddOption(option);
}
/// <summary>
@@ -477,15 +640,36 @@ namespace Discord
return this;
}

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

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

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

Options.AddRange(options);
return this;
}

/// <summary>
/// Adds a choice to the current option.
/// </summary>
/// <param name="name">The name of the choice.</param>
/// <param name="value">The value of the choice.</param>
/// <param name="nameLocalizations">The localization dictionary for to use the name field of this command option choice.</param>
/// <returns>The current builder.</returns>
public SlashCommandOptionBuilder AddChoice(string name, int value)
public SlashCommandOptionBuilder AddChoice(string name, int value, IDictionary<string, string> nameLocalizations = null)
{
return AddChoiceInternal(name, value);
return AddChoiceInternal(name, value, nameLocalizations);
}

/// <summary>
@@ -493,10 +677,11 @@ namespace Discord
/// </summary>
/// <param name="name">The name of the choice.</param>
/// <param name="value">The value of the choice.</param>
/// <param name="nameLocalizations">The localization dictionary for to use the name field of this command option choice.</param>
/// <returns>The current builder.</returns>
public SlashCommandOptionBuilder AddChoice(string name, string value)
public SlashCommandOptionBuilder AddChoice(string name, string value, IDictionary<string, string> nameLocalizations = null)
{
return AddChoiceInternal(name, value);
return AddChoiceInternal(name, value, nameLocalizations);
}

/// <summary>
@@ -504,10 +689,11 @@ namespace Discord
/// </summary>
/// <param name="name">The name of the choice.</param>
/// <param name="value">The value of the choice.</param>
/// <param name="nameLocalizations">Localization dictionary for the description field of this command.</param>
/// <returns>The current builder.</returns>
public SlashCommandOptionBuilder AddChoice(string name, double value)
public SlashCommandOptionBuilder AddChoice(string name, double value, IDictionary<string, string> nameLocalizations = null)
{
return AddChoiceInternal(name, value);
return AddChoiceInternal(name, value, nameLocalizations);
}

/// <summary>
@@ -515,10 +701,11 @@ namespace Discord
/// </summary>
/// <param name="name">The name of the choice.</param>
/// <param name="value">The value of the choice.</param>
/// <param name="nameLocalizations">The localization dictionary to use for the name field of this command option choice.</param>
/// <returns>The current builder.</returns>
public SlashCommandOptionBuilder AddChoice(string name, float value)
public SlashCommandOptionBuilder AddChoice(string name, float value, IDictionary<string, string> nameLocalizations = null)
{
return AddChoiceInternal(name, value);
return AddChoiceInternal(name, value, nameLocalizations);
}

/// <summary>
@@ -526,13 +713,14 @@ namespace Discord
/// </summary>
/// <param name="name">The name of the choice.</param>
/// <param name="value">The value of the choice.</param>
/// <param name="nameLocalizations">The localization dictionary to use for the name field of this command option choice.</param>
/// <returns>The current builder.</returns>
public SlashCommandOptionBuilder AddChoice(string name, long value)
public SlashCommandOptionBuilder AddChoice(string name, long value, IDictionary<string, string> nameLocalizations = null)
{
return AddChoiceInternal(name, value);
return AddChoiceInternal(name, value, nameLocalizations);
}

private SlashCommandOptionBuilder AddChoiceInternal(string name, object value)
private SlashCommandOptionBuilder AddChoiceInternal(string name, object value, IDictionary<string, string> nameLocalizations = null)
{
Choices ??= new List<ApplicationCommandOptionChoiceProperties>();

@@ -554,7 +742,8 @@ namespace Discord
Choices.Add(new ApplicationCommandOptionChoiceProperties
{
Name = name,
Value = value
Value = value,
NameLocalizations = nameLocalizations
});

return this;
@@ -640,7 +829,7 @@ namespace Discord
MinValue = value;
return this;
}
/// <summary>
/// Sets the current builders max value field.
/// </summary>
@@ -652,6 +841,28 @@ namespace Discord
return this;
}

/// <summary>
/// Sets the current builders min length field.
/// </summary>
/// <param name="length">The value to set.</param>
/// <returns>The current builder.</returns>
public SlashCommandOptionBuilder WithMinLength(int length)
{
MinLength = length;
return this;
}

/// <summary>
/// Sets the current builders max length field.
/// </summary>
/// <param name="length">The value to set.</param>
/// <returns>The current builder.</returns>
public SlashCommandOptionBuilder WithMaxLength(int length)
{
MaxLength = length;
return this;
}

/// <summary>
/// Sets the current type of this builder.
/// </summary>
@@ -662,5 +873,107 @@ namespace Discord
Type = type;
return this;
}

/// <summary>
/// Sets the <see cref="NameLocalizations"/> collection.
/// </summary>
/// <param name="nameLocalizations">The localization dictionary to use for the name field of this command option.</param>
/// <returns>The current builder.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="nameLocalizations"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown if any dictionary key is an invalid locale string.</exception>
public SlashCommandOptionBuilder WithNameLocalizations(IDictionary<string, string> nameLocalizations)
{
if (nameLocalizations is null)
throw new ArgumentNullException(nameof(nameLocalizations));

foreach (var (locale, name) in nameLocalizations)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

EnsureValidCommandOptionName(name);
}

_nameLocalizations = new Dictionary<string, string>(nameLocalizations);
return this;
}

/// <summary>
/// Sets the <see cref="DescriptionLocalizations"/> collection.
/// </summary>
/// <param name="descriptionLocalizations">The localization dictionary to use for the description field of this command option.</param>
/// <returns>The current builder.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="descriptionLocalizations"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown if any dictionary key is an invalid locale string.</exception>
public SlashCommandOptionBuilder WithDescriptionLocalizations(IDictionary<string, string> descriptionLocalizations)
{
if (descriptionLocalizations is null)
throw new ArgumentNullException(nameof(descriptionLocalizations));

foreach (var (locale, description) in _descriptionLocalizations)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

EnsureValidCommandOptionDescription(description);
}

_descriptionLocalizations = new Dictionary<string, string>(descriptionLocalizations);
return this;
}

/// <summary>
/// Adds a new entry to the <see cref="NameLocalizations"/> collection.
/// </summary>
/// <param name="locale">Locale of the entry.</param>
/// <param name="name">Localized string for the name field.</param>
/// <returns>The current builder.</returns>
/// <exception cref="ArgumentException">Thrown if <paramref name="locale"/> is an invalid locale string.</exception>
public SlashCommandOptionBuilder AddNameLocalization(string locale, string name)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

EnsureValidCommandOptionName(name);

_descriptionLocalizations ??= new();
_nameLocalizations.Add(locale, name);

return this;
}

/// <summary>
/// Adds a new entry to the <see cref="DescriptionLocalizations"/> collection.
/// </summary>
/// <param name="locale">Locale of the entry.</param>
/// <param name="description">Localized string for the description field.</param>
/// <returns>The current builder.</returns>
/// <exception cref="ArgumentException">Thrown if <paramref name="locale"/> is an invalid locale string.</exception>
public SlashCommandOptionBuilder AddDescriptionLocalization(string locale, string description)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

EnsureValidCommandOptionDescription(description);

_descriptionLocalizations ??= new();
_descriptionLocalizations.Add(locale, description);

return this;
}

private static void EnsureValidCommandOptionName(string name)
{
Preconditions.AtLeast(name.Length, 1, nameof(name));
Preconditions.AtMost(name.Length, SlashCommandBuilder.MaxNameLength, nameof(name));
if (!Regex.IsMatch(name, @"^[\w-]{1,32}$"))
throw new ArgumentException("Option name cannot contain any special characters or whitespaces!", nameof(name));
}

private static void EnsureValidCommandOptionDescription(string description)
{
Preconditions.AtLeast(description.Length, 1, nameof(description));
Preconditions.AtMost(description.Length, SlashCommandBuilder.MaxDescriptionLength, nameof(description));
}
}
}

+ 39
- 0
src/Discord.Net.Core/Entities/Messages/Embed.cs View File

@@ -94,5 +94,44 @@ namespace Discord
/// </summary>
public override string ToString() => Title;
private string DebuggerDisplay => $"{Title} ({Type})";

public static bool operator ==(Embed left, Embed right)
=> left is null ? right is null
: left.Equals(right);

public static bool operator !=(Embed left, Embed right)
=> !(left == right);

/// <summary>
/// Determines whether the specified object is equal to the current <see cref="Embed"/>.
/// </summary>
/// <remarks>
/// If the object passes is an <see cref="Embed"/>, <see cref="Equals(Embed)"/> will be called to compare the 2 instances
/// </remarks>
/// <param name="obj">The object to compare with the current <see cref="Embed"/></param>
/// <returns></returns>
public override bool Equals(object obj)
=> obj is Embed embed && Equals(embed);

/// <summary>
/// Determines whether the specified <see cref="Embed"/> is equal to the current <see cref="Embed"/>
/// </summary>
/// <param name="embed">The <see cref="Embed"/> to compare with the current <see cref="Embed"/></param>
/// <returns></returns>
public bool Equals(Embed embed)
=> GetHashCode() == embed?.GetHashCode();

/// <inheritdoc />
public override int GetHashCode()
{
unchecked
{
var hash = 17;
hash = hash * 23 + (Type, Title, Description, Timestamp, Color, Image, Video, Author, Footer, Provider, Thumbnail).GetHashCode();
foreach(var field in Fields)
hash = hash * 23 + field.GetHashCode();
return hash;
}
}
}
}

+ 31
- 0
src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs View File

@@ -1,3 +1,4 @@
using System;
using System.Diagnostics;

namespace Discord
@@ -41,5 +42,35 @@ namespace Discord
///
/// </returns>
public override string ToString() => Name;

public static bool operator ==(EmbedAuthor? left, EmbedAuthor? right)
=> left is null ? right is null
: left.Equals(right);

public static bool operator !=(EmbedAuthor? left, EmbedAuthor? right)
=> !(left == right);

/// <summary>
/// Determines whether the specified object is equal to the current <see cref="EmbedAuthor"/>.
/// </summary>
/// <remarks>
/// If the object passes is an <see cref="EmbedAuthor"/>, <see cref="Equals(EmbedAuthor?)"/> will be called to compare the 2 instances
/// </remarks>
/// <param name="obj">The object to compare with the current <see cref="EmbedAuthor"/></param>
/// <returns></returns>
public override bool Equals(object obj)
=> obj is EmbedAuthor embedAuthor && Equals(embedAuthor);

/// <summary>
/// Determines whether the specified <see cref="EmbedAuthor"/> is equal to the current <see cref="EmbedAuthor"/>
/// </summary>
/// <param name="embedAuthor">The <see cref="EmbedAuthor"/> to compare with the current <see cref="EmbedAuthor"/></param>
/// <returns></returns>
public bool Equals(EmbedAuthor? embedAuthor)
=> GetHashCode() == embedAuthor?.GetHashCode();

/// <inheritdoc />
public override int GetHashCode()
=> (Name, Url, IconUrl).GetHashCode();
}
}

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

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

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

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

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

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

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

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

/// <summary>
/// Sets the title of an <see cref="Embed"/>.
/// </summary>
@@ -431,6 +481,55 @@ namespace Discord

return new Embed(EmbedType.Rich, Title, Description, Url, Timestamp, Color, _image, null, Author?.Build(), Footer?.Build(), null, _thumbnail, fields.ToImmutable());
}

public static bool operator ==(EmbedBuilder left, EmbedBuilder right)
=> left is null ? right is null
: left.Equals(right);

public static bool operator !=(EmbedBuilder left, EmbedBuilder right)
=> !(left == right);

/// <summary>
/// Determines whether the specified object is equal to the current <see cref="EmbedBuilder"/>.
/// </summary>
/// <remarks>
/// If the object passes is an <see cref="EmbedBuilder"/>, <see cref="Equals(EmbedBuilder)"/> will be called to compare the 2 instances
/// </remarks>
/// <param name="obj">The object to compare with the current <see cref="EmbedBuilder"/></param>
/// <returns></returns>
public override bool Equals(object obj)
=> obj is EmbedBuilder embedBuilder && Equals(embedBuilder);

/// <summary>
/// Determines whether the specified <see cref="EmbedBuilder"/> is equal to the current <see cref="EmbedBuilder"/>
/// </summary>
/// <param name="embedBuilder">The <see cref="EmbedBuilder"/> to compare with the current <see cref="EmbedBuilder"/></param>
/// <returns></returns>
public bool Equals(EmbedBuilder embedBuilder)
{
if (embedBuilder is null)
return false;

if (Fields.Count != embedBuilder.Fields.Count)
return false;

for (var i = 0; i < _fields.Count; i++)
if (_fields[i] != embedBuilder._fields[i])
return false;

return _title == embedBuilder?._title
&& _description == embedBuilder?._description
&& _image == embedBuilder?._image
&& _thumbnail == embedBuilder?._thumbnail
&& Timestamp == embedBuilder?.Timestamp
&& Color == embedBuilder?.Color
&& Author == embedBuilder?.Author
&& Footer == embedBuilder?.Footer
&& Url == embedBuilder?.Url;
}

/// <inheritdoc />
public override int GetHashCode() => base.GetHashCode();
}

/// <summary>
@@ -547,6 +646,37 @@ namespace Discord
/// </exception>
public EmbedField Build()
=> new EmbedField(Name, Value.ToString(), IsInline);

public static bool operator ==(EmbedFieldBuilder left, EmbedFieldBuilder right)
=> left is null ? right is null
: left.Equals(right);

public static bool operator !=(EmbedFieldBuilder left, EmbedFieldBuilder right)
=> !(left == right);

/// <summary>
/// Determines whether the specified object is equal to the current <see cref="EmbedFieldBuilder"/>.
/// </summary>
/// <remarks>
/// If the object passes is an <see cref="EmbedFieldBuilder"/>, <see cref="Equals(EmbedFieldBuilder)"/> will be called to compare the 2 instances
/// </remarks>
/// <param name="obj">The object to compare with the current <see cref="EmbedFieldBuilder"/></param>
/// <returns></returns>
public override bool Equals(object obj)
=> obj is EmbedFieldBuilder embedFieldBuilder && Equals(embedFieldBuilder);

/// <summary>
/// Determines whether the specified <see cref="EmbedFieldBuilder"/> is equal to the current <see cref="EmbedFieldBuilder"/>
/// </summary>
/// <param name="embedFieldBuilder">The <see cref="EmbedFieldBuilder"/> to compare with the current <see cref="EmbedFieldBuilder"/></param>
/// <returns></returns>
public bool Equals(EmbedFieldBuilder embedFieldBuilder)
=> _name == embedFieldBuilder?._name
&& _value == embedFieldBuilder?._value
&& IsInline == embedFieldBuilder?.IsInline;

/// <inheritdoc />
public override int GetHashCode() => base.GetHashCode();
}

/// <summary>
@@ -647,6 +777,37 @@ namespace Discord
/// </returns>
public EmbedAuthor Build()
=> new EmbedAuthor(Name, Url, IconUrl, null);

public static bool operator ==(EmbedAuthorBuilder left, EmbedAuthorBuilder right)
=> left is null ? right is null
: left.Equals(right);

public static bool operator !=(EmbedAuthorBuilder left, EmbedAuthorBuilder right)
=> !(left == right);

/// <summary>
/// Determines whether the specified object is equal to the current <see cref="EmbedAuthorBuilder"/>.
/// </summary>
/// <remarks>
/// If the object passes is an <see cref="EmbedAuthorBuilder"/>, <see cref="Equals(EmbedAuthorBuilder)"/> will be called to compare the 2 instances
/// </remarks>
/// <param name="obj">The object to compare with the current <see cref="EmbedAuthorBuilder"/></param>
/// <returns></returns>
public override bool Equals(object obj)
=> obj is EmbedAuthorBuilder embedAuthorBuilder && Equals(embedAuthorBuilder);

/// <summary>
/// Determines whether the specified <see cref="EmbedAuthorBuilder"/> is equals to the current <see cref="EmbedAuthorBuilder"/>
/// </summary>
/// <param name="embedAuthorBuilder">The <see cref="EmbedAuthorBuilder"/> to compare with the current <see cref="EmbedAuthorBuilder"/></param>
/// <returns></returns>
public bool Equals(EmbedAuthorBuilder embedAuthorBuilder)
=> _name == embedAuthorBuilder?._name
&& Url == embedAuthorBuilder?.Url
&& IconUrl == embedAuthorBuilder?.IconUrl;

/// <inheritdoc />
public override int GetHashCode() => base.GetHashCode();
}

/// <summary>
@@ -727,5 +888,35 @@ namespace Discord
/// </returns>
public EmbedFooter Build()
=> new EmbedFooter(Text, IconUrl, null);

public static bool operator ==(EmbedFooterBuilder left, EmbedFooterBuilder right)
=> left is null ? right is null
: left.Equals(right);

public static bool operator !=(EmbedFooterBuilder left, EmbedFooterBuilder right)
=> !(left == right);

/// <summary>
/// Determines whether the specified object is equal to the current <see cref="EmbedFooterBuilder"/>.
/// </summary>
/// <remarks>
/// If the object passes is an <see cref="EmbedFooterBuilder"/>, <see cref="Equals(EmbedFooterBuilder)"/> will be called to compare the 2 instances
/// </remarks>
/// <param name="obj">The object to compare with the current <see cref="EmbedFooterBuilder"/></param>
/// <returns></returns>
public override bool Equals(object obj)
=> obj is EmbedFooterBuilder embedFooterBuilder && Equals(embedFooterBuilder);

/// <summary>
/// Determines whether the specified <see cref="EmbedFooterBuilder"/> is equal to the current <see cref="EmbedFooterBuilder"/>
/// </summary>
/// <param name="embedFooterBuilder">The <see cref="EmbedFooterBuilder"/> to compare with the current <see cref="EmbedFooterBuilder"/></param>
/// <returns></returns>
public bool Equals(EmbedFooterBuilder embedFooterBuilder)
=> _text == embedFooterBuilder?._text
&& IconUrl == embedFooterBuilder?.IconUrl;

/// <inheritdoc />
public override int GetHashCode() => base.GetHashCode();
}
}

+ 31
- 0
src/Discord.Net.Core/Entities/Messages/EmbedField.cs View File

@@ -1,3 +1,4 @@
using System;
using System.Diagnostics;

namespace Discord
@@ -36,5 +37,35 @@ namespace Discord
/// A string that resolves to <see cref="EmbedField.Name"/>.
/// </returns>
public override string ToString() => Name;

public static bool operator ==(EmbedField? left, EmbedField? right)
=> left is null ? right is null
: left.Equals(right);

public static bool operator !=(EmbedField? left, EmbedField? right)
=> !(left == right);

/// <summary>
/// Determines whether the specified object is equal to the current <see cref="EmbedField"/>.
/// </summary>
/// <remarks>
/// If the object passes is an <see cref="EmbedField"/>, <see cref="Equals(EmbedField?)"/> will be called to compare the 2 instances
/// </remarks>
/// <param name="obj">The object to compare with the current object</param>
/// <returns></returns>
public override bool Equals(object obj)
=> obj is EmbedField embedField && Equals(embedField);

/// <summary>
/// Determines whether the specified <see cref="EmbedField"/> is equal to the current <see cref="EmbedField"/>
/// </summary>
/// <param name="embedField"></param>
/// <returns></returns>
public bool Equals(EmbedField? embedField)
=> GetHashCode() == embedField?.GetHashCode();

/// <inheritdoc />
public override int GetHashCode()
=> (Name, Value, Inline).GetHashCode();
}
}

+ 31
- 0
src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs View File

@@ -1,3 +1,4 @@
using System;
using System.Diagnostics;

namespace Discord
@@ -43,5 +44,35 @@ namespace Discord
/// A string that resolves to <see cref="Discord.EmbedFooter.Text"/>.
/// </returns>
public override string ToString() => Text;

public static bool operator ==(EmbedFooter? left, EmbedFooter? right)
=> left is null ? right is null
: left.Equals(right);

public static bool operator !=(EmbedFooter? left, EmbedFooter? right)
=> !(left == right);

/// <summary>
/// Determines whether the specified object is equal to the current <see cref="EmbedFooter"/>.
/// </summary>
/// <remarks>
/// If the object passes is an <see cref="EmbedFooter"/>, <see cref="Equals(EmbedFooter?)"/> will be called to compare the 2 instances
/// </remarks>
/// <param name="obj">The object to compare with the current <see cref="EmbedFooter"/></param>
/// <returns></returns>
public override bool Equals(object obj)
=> obj is EmbedFooter embedFooter && Equals(embedFooter);

/// <summary>
/// Determines whether the specified <see cref="EmbedFooter"/> is equal to the current <see cref="EmbedFooter"/>
/// </summary>
/// <param name="embedFooter">The <see cref="EmbedFooter"/> to compare with the current <see cref="EmbedFooter"/></param>
/// <returns></returns>
public bool Equals(EmbedFooter? embedFooter)
=> GetHashCode() == embedFooter?.GetHashCode();

/// <inheritdoc />
public override int GetHashCode()
=> (Text, IconUrl, ProxyUrl).GetHashCode();
}
}

+ 31
- 0
src/Discord.Net.Core/Entities/Messages/EmbedImage.cs View File

@@ -1,3 +1,4 @@
using System;
using System.Diagnostics;

namespace Discord
@@ -53,5 +54,35 @@ namespace Discord
/// A string that resolves to <see cref="Discord.EmbedImage.Url"/> .
/// </returns>
public override string ToString() => Url;

public static bool operator ==(EmbedImage? left, EmbedImage? right)
=> left is null ? right is null
: left.Equals(right);

public static bool operator !=(EmbedImage? left, EmbedImage? right)
=> !(left == right);

/// <summary>
/// Determines whether the specified object is equal to the current <see cref="EmbedImage"/>.
/// </summary>
/// <remarks>
/// If the object passes is an <see cref="EmbedImage"/>, <see cref="Equals(EmbedImage?)"/> will be called to compare the 2 instances
/// </remarks>
/// <param name="obj">The object to compare with the current <see cref="EmbedImage"/></param>
/// <returns></returns>
public override bool Equals(object obj)
=> obj is EmbedImage embedImage && Equals(embedImage);

/// <summary>
/// Determines whether the specified <see cref="EmbedImage"/> is equal to the current <see cref="EmbedImage"/>
/// </summary>
/// <param name="embedImage">The <see cref="EmbedImage"/> to compare with the current <see cref="EmbedImage"/></param>
/// <returns></returns>
public bool Equals(EmbedImage? embedImage)
=> GetHashCode() == embedImage?.GetHashCode();

/// <inheritdoc />
public override int GetHashCode()
=> (Height, Width, Url, ProxyUrl).GetHashCode();
}
}

+ 31
- 0
src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs View File

@@ -1,3 +1,4 @@
using System;
using System.Diagnostics;

namespace Discord
@@ -35,5 +36,35 @@ namespace Discord
/// A string that resolves to <see cref="Discord.EmbedProvider.Name" />.
/// </returns>
public override string ToString() => Name;

public static bool operator ==(EmbedProvider? left, EmbedProvider? right)
=> left is null ? right is null
: left.Equals(right);

public static bool operator !=(EmbedProvider? left, EmbedProvider? right)
=> !(left == right);

/// <summary>
/// Determines whether the specified object is equal to the current <see cref="EmbedProvider"/>.
/// </summary>
/// <remarks>
/// If the object passes is an <see cref="EmbedProvider"/>, <see cref="Equals(EmbedProvider?)"/> will be called to compare the 2 instances
/// </remarks>
/// <param name="obj">The object to compare with the current <see cref="EmbedProvider"/></param>
/// <returns></returns>
public override bool Equals(object obj)
=> obj is EmbedProvider embedProvider && Equals(embedProvider);

/// <summary>
/// Determines whether the specified <see cref="EmbedProvider"/> is equal to the current <see cref="EmbedProvider"/>
/// </summary>
/// <param name="embedProvider">The <see cref="EmbedProvider"/> to compare with the current <see cref="EmbedProvider"/></param>
/// <returns></returns>
public bool Equals(EmbedProvider? embedProvider)
=> GetHashCode() == embedProvider?.GetHashCode();

/// <inheritdoc />
public override int GetHashCode()
=> (Name, Url).GetHashCode();
}
}

+ 31
- 0
src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs View File

@@ -1,3 +1,4 @@
using System;
using System.Diagnostics;

namespace Discord
@@ -53,5 +54,35 @@ namespace Discord
/// A string that resolves to <see cref="Discord.EmbedThumbnail.Url" />.
/// </returns>
public override string ToString() => Url;

public static bool operator ==(EmbedThumbnail? left, EmbedThumbnail? right)
=> left is null ? right is null
: left.Equals(right);

public static bool operator !=(EmbedThumbnail? left, EmbedThumbnail? right)
=> !(left == right);

/// <summary>
/// Determines whether the specified object is equal to the current <see cref="EmbedThumbnail"/>.
/// </summary>
/// <remarks>
/// If the object passes is an <see cref="EmbedThumbnail"/>, <see cref="Equals(EmbedThumbnail?)"/> will be called to compare the 2 instances
/// </remarks>
/// <param name="obj">The object to compare with the current <see cref="EmbedThumbnail"/></param>
/// <returns></returns>
public override bool Equals(object obj)
=> obj is EmbedThumbnail embedThumbnail && Equals(embedThumbnail);

/// <summary>
/// Determines whether the specified <see cref="EmbedThumbnail"/> is equal to the current <see cref="EmbedThumbnail"/>
/// </summary>
/// <param name="embedThumbnail">The <see cref="EmbedThumbnail"/> to compare with the current <see cref="EmbedThumbnail"/></param>
/// <returns></returns>
public bool Equals(EmbedThumbnail? embedThumbnail)
=> GetHashCode() == embedThumbnail?.GetHashCode();

/// <inheritdoc />
public override int GetHashCode()
=> (Width, Height, Url, ProxyUrl).GetHashCode();
}
}

+ 31
- 0
src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs View File

@@ -1,3 +1,4 @@
using System;
using System.Diagnostics;

namespace Discord
@@ -47,5 +48,35 @@ namespace Discord
/// A string that resolves to <see cref="Url"/>.
/// </returns>
public override string ToString() => Url;

public static bool operator ==(EmbedVideo? left, EmbedVideo? right)
=> left is null ? right is null
: left.Equals(right);

public static bool operator !=(EmbedVideo? left, EmbedVideo? right)
=> !(left == right);

/// <summary>
/// Determines whether the specified object is equal to the current <see cref="EmbedVideo"/>.
/// </summary>
/// <remarks>
/// If the object passes is an <see cref="EmbedVideo"/>, <see cref="Equals(EmbedVideo?)"/> will be called to compare the 2 instances
/// </remarks>
/// <param name="obj">The object to compare with the current <see cref="EmbedVideo"/></param>
/// <returns></returns>
public override bool Equals(object obj)
=> obj is EmbedVideo embedVideo && Equals(embedVideo);

/// <summary>
/// Determines whether the specified <see cref="EmbedVideo"/> is equal to the current <see cref="EmbedVideo"/>
/// </summary>
/// <param name="embedVideo">The <see cref="EmbedVideo"/> to compare with the current <see cref="EmbedVideo"/></param>
/// <returns></returns>
public bool Equals(EmbedVideo? embedVideo)
=> GetHashCode() == embedVideo?.GetHashCode();

/// <inheritdoc />
public override int GetHashCode()
=> (Width, Height, Url).GetHashCode();
}
}

+ 6
- 0
src/Discord.Net.Core/Entities/Messages/IMessage.cs View File

@@ -48,6 +48,9 @@ namespace Discord
/// <summary>
/// Gets the content for this message.
/// </summary>
/// <remarks>
/// This will be empty if the privileged <see cref="GatewayIntents.MessageContent"/> is disabled.
/// </remarks>
/// <returns>
/// A string that contains the body of the message; note that this field may be empty if there is an embed.
/// </returns>
@@ -55,6 +58,9 @@ namespace Discord
/// <summary>
/// Gets the clean content for this message.
/// </summary>
/// <remarks>
/// This will be empty if the privileged <see cref="GatewayIntents.MessageContent"/> is disabled.
/// </remarks>
/// <returns>
/// A string that contains the body of the message stripped of mentions, markdown, emojis and pings; note that this field may be empty if there is an embed.
/// </returns>


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

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

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

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

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

public override string ToString()
=> DebuggerDisplay;


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

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

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

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

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

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

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

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

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


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

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


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

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


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

Loading…
Cancel
Save