Browse Source

Merge branch 'feature/command-localization' into feature/command-localization

pull/2211/head
Cenk Ergen GitHub 2 years ago
parent
commit
f6420ee864
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
100 changed files with 2088 additions and 146 deletions
  1. +2
    -0
      .github/FUNDING.yml
  2. +2
    -2
      .github/ISSUE_TEMPLATE/bugreport.yml
  3. +3
    -0
      .gitmodules
  4. +87
    -0
      CHANGELOG.md
  5. +16
    -1
      Discord.Net.sln
  6. +6
    -3
      Discord.Net.targets
  7. +1
    -2
      README.md
  8. +1
    -0
      azure/deploy.yml
  9. +1
    -1
      docs/docfx.json
  10. +41
    -0
      docs/faq/build_overrides/what-are-they.md
  11. +2
    -0
      docs/faq/toc.yml
  12. +1
    -1
      docs/guides/getting_started/first-bot.md
  13. +3
    -1
      docs/guides/int_basics/application-commands/slash-commands/parameters.md
  14. +1
    -1
      docs/guides/int_basics/modals/intro.md
  15. +2
    -0
      docs/guides/int_framework/autocompletion.md
  16. +11
    -0
      docs/guides/int_framework/intro.md
  17. +59
    -0
      docs/guides/int_framework/permissions.md
  18. +20
    -0
      docs/guides/int_framework/samples/autocompletion/autocomplete-example.cs
  19. +15
    -3
      docs/guides/int_framework/samples/intro/autocomplete.cs
  20. +21
    -0
      docs/guides/int_framework/samples/intro/groupmodule.cs
  21. +1
    -1
      docs/guides/int_framework/samples/intro/modal.cs
  22. +6
    -0
      docs/guides/int_framework/samples/permissions/guild-only.cs
  23. +7
    -0
      docs/guides/int_framework/samples/permissions/guild-perms.cs
  24. +16
    -0
      docs/guides/int_framework/samples/permissions/perm-nesting.cs
  25. +4
    -0
      docs/guides/int_framework/samples/permissions/perm-stacking.cs
  26. BIN
      docs/guides/other_libs/images/mediatr_output.png
  27. +70
    -0
      docs/guides/other_libs/mediatr.md
  28. +1
    -0
      docs/guides/other_libs/samples/MediatrConfiguringDI.cs
  29. +16
    -0
      docs/guides/other_libs/samples/MediatrCreatingMessageNotification.cs
  30. +46
    -0
      docs/guides/other_libs/samples/MediatrDiscordEventListener.cs
  31. +17
    -0
      docs/guides/other_libs/samples/MediatrMessageReceivedHandler.cs
  32. +4
    -0
      docs/guides/other_libs/samples/MediatrStartListener.cs
  33. +2
    -2
      docs/guides/other_libs/samples/ModifyLogMethod.cs
  34. +4
    -0
      docs/guides/toc.yml
  35. +1
    -1
      docs/index.md
  36. +278
    -0
      experiment/Discord.Net.BuildOverrides/BuildOverrides.cs
  37. +20
    -0
      experiment/Discord.Net.BuildOverrides/Discord.Net.BuildOverrides.csproj
  38. +34
    -0
      experiment/Discord.Net.BuildOverrides/IOverride.cs
  39. +30
    -0
      experiment/Discord.Net.BuildOverrides/OverrideContext.cs
  40. +1
    -0
      overrides/Discord.Net.BuildOverrides
  41. +3
    -3
      samples/BasicBot/_BasicBot.csproj
  42. +1
    -1
      samples/InteractionFramework/Modules/ExampleModule.cs
  43. +2
    -8
      samples/InteractionFramework/_InteractionFramework.csproj
  44. +48
    -0
      samples/MediatRSample/MediatRSample/DiscordEventListener.cs
  45. +14
    -0
      samples/MediatRSample/MediatRSample/Handlers/MessageReceivedHandler.cs
  46. +20
    -0
      samples/MediatRSample/MediatRSample/MediatRSample.csproj
  47. +14
    -0
      samples/MediatRSample/MediatRSample/Notifications/MessageReceivedNotification.cs
  48. +13
    -0
      samples/MediatRSample/MediatRSample/Notifications/ReadyNotification.cs
  49. +73
    -0
      samples/MediatRSample/MediatRSample/Program.cs
  50. +2
    -7
      samples/ShardedClient/_ShardedClient.csproj
  51. +3
    -6
      samples/TextCommandFramework/_TextCommandFramework.csproj
  52. +2
    -2
      samples/WebhookClient/_WebhookClient.csproj
  53. +2
    -0
      src/Discord.Net.Commands/Discord.Net.Commands.csproj
  54. +3
    -3
      src/Discord.Net.Commands/Results/MatchResult.cs
  55. +2
    -0
      src/Discord.Net.Core/Discord.Net.Core.csproj
  56. +27
    -0
      src/Discord.Net.Core/DiscordConfig.cs
  57. +6
    -0
      src/Discord.Net.Core/DiscordErrorCode.cs
  58. +3
    -1
      src/Discord.Net.Core/Entities/Channels/ChannelType.cs
  59. +5
    -5
      src/Discord.Net.Core/Entities/Channels/Direction.cs
  60. +216
    -0
      src/Discord.Net.Core/Entities/Channels/IForumChannel.cs
  61. +11
    -0
      src/Discord.Net.Core/Entities/Channels/ITextChannel.cs
  62. +1
    -1
      src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs
  63. +42
    -0
      src/Discord.Net.Core/Entities/ForumTag.cs
  64. +59
    -7
      src/Discord.Net.Core/Entities/Guilds/IGuild.cs
  65. +1
    -1
      src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs
  66. +1
    -1
      src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs
  67. +9
    -0
      src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs
  68. +34
    -1
      src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs
  69. +34
    -1
      src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs
  70. +13
    -0
      src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs
  71. +25
    -1
      src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs
  72. +102
    -8
      src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs
  73. +4
    -4
      src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs
  74. +61
    -21
      src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs
  75. +50
    -0
      src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs
  76. +13
    -2
      src/Discord.Net.Core/Entities/Messages/MessageReference.cs
  77. +42
    -17
      src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs
  78. +1
    -1
      src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs
  79. +3
    -3
      src/Discord.Net.Core/Entities/Users/IGuildUser.cs
  80. +9
    -5
      src/Discord.Net.Core/Format.cs
  81. +24
    -0
      src/Discord.Net.Core/Interactions/IRouteMatchContainer.cs
  82. +16
    -0
      src/Discord.Net.Core/Interactions/IRouteSegmentMatch.cs
  83. +16
    -0
      src/Discord.Net.Core/Interactions/RouteSegmentMatch.cs
  84. +7
    -3
      src/Discord.Net.Core/Net/Rest/IRestClient.cs
  85. +3
    -0
      src/Discord.Net.Core/RequestOptions.cs
  86. +17
    -0
      src/Discord.Net.Core/Utils/Preconditions.cs
  87. +1
    -1
      src/Discord.Net.Core/Utils/UrlValidation.cs
  88. +1
    -1
      src/Discord.Net.Examples/Discord.Net.Examples.csproj
  89. +3
    -3
      src/Discord.Net.Interactions/Attributes/AutocompleteAttribute.cs
  90. +25
    -0
      src/Discord.Net.Interactions/Attributes/DefaultMemberPermissionAttribute.cs
  91. +1
    -0
      src/Discord.Net.Interactions/Attributes/DefaultPermissionAttribute.cs
  92. +25
    -0
      src/Discord.Net.Interactions/Attributes/EnabledInDmAttribute.cs
  93. +0
    -2
      src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs
  94. +1
    -1
      src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs
  95. +2
    -2
      src/Discord.Net.Interactions/Attributes/Preconditions/RequireUserPermissionAttribute.cs
  96. +38
    -0
      src/Discord.Net.Interactions/Builders/Commands/ContextCommandBuilder.cs
  97. +39
    -1
      src/Discord.Net.Interactions/Builders/Commands/SlashCommandBuilder.cs
  98. +1
    -1
      src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs
  99. +1
    -1
      src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs
  100. +40
    -1
      src/Discord.Net.Interactions/Builders/ModuleBuilder.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


+ 3
- 0
.gitmodules View File

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

+ 87
- 0
CHANGELOG.md View File

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

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

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

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

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

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

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

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

### Misc
- 3.6.0 (27226f0)


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

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

## [3.5.0] - 2022-04-05

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

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

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

## [3.4.1] - 2022-03-9



+ 16
- 1
Discord.Net.sln View File

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


+ 6
- 3
Discord.Net.targets View File

@@ -1,12 +1,12 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VersionPrefix>3.4.1</VersionPrefix>
<VersionPrefix>3.7.2</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>

+ 1
- 2
README.md View File

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

## Documentation



+ 1
- 0
azure/deploy.yml View File

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

- task: NuGetCommand@2


+ 1
- 1
docs/docfx.json View File

@@ -60,7 +60,7 @@
"overwrite": "_overwrites/**/**.md",
"globalMetadata": {
"_appTitle": "Discord.Net Documentation",
"_appFooter": "Discord.Net (c) 2015-2022 3.4.1",
"_appFooter": "Discord.Net (c) 2015-2022 3.7.2",
"_enableSearch": true,
"_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg",
"_appFaviconPath": "favicon.ico"


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

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

# Build Overrides

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

## Installing the package

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

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

## Adding an override

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

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

```

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

## Security and Transparency

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

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

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

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

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

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



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

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

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

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



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


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

@@ -86,6 +86,7 @@ By default, your methods can feature the following parameter types:
- Implementations of [IChannel]
- Implementations of [IRole]
- Implementations of [IMentionable]
- Implementations of [IAttachment]
- `string`
- `float`, `double`, `decimal`
- `bool`
@@ -158,6 +159,14 @@ Interaction service complex parameter constructors are prioritized in the follow
2. Constuctor tagged with `[ComplexParameterCtor]`.
3. Type's only public constuctor.

#### DM Permissions

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

#### Default Member Permissions

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

## User Commands

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

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

## Executing Commands

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


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

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

# Permissions

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

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

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

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

## Disallowing commands in DM

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

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

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

## Server permissions

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

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

### Stacking permissions

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

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

### Nesting permissions

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

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

The amount of nesting you can do is realistically endless.

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

[permissions]: xref:Discord.GuildPermission


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

# Configuring MediatR

## Prerequisites

- A simple bot with dependency injection configured

## Downloading the required packages

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

|Name|Link|
|--|--|
| `MediatR` | [link](https://www.nuget.org/packages/MediatR) |
| `MediatR.Extensions.Microsoft.DependencyInjection` | [link](https://www.nuget.org/packages/MediatR.Extensions.Microsoft.DependencyInjection)|

## Adding MediatR to your dependency injection container

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

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

## Creating notifications

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

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

## Creating the notification publisher / event listener

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

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

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

## Adding the event listener to your dependency injection container

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

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

## Creating your notification handler

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

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

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

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

## Testing

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

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

## Adding more event types

To add more event types you can follow these steps:

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

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

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

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

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

using Discord.WebSocket;
using MediatR;

namespace MediatRSample.Notifications;

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

public SocketMessage Message { get; }
}

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

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

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

namespace MediatRSample;

public class DiscordEventListener
{
private readonly CancellationToken _cancellationToken;

private readonly DiscordSocketClient _client;
private readonly IServiceScopeFactory _serviceScope;

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

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

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

await Task.CompletedTask;
}

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

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

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

using System;
using MediatR;
using MediatRSample.Notifications;

namespace MediatRSample;

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

// Your implementation
}
}

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

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

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

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

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


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

@@ -57,6 +57,8 @@
topicUid: Guides.IntFw.DI
- name: Post-execution Handling
topicUid: Guides.IntFw.PostExecution
- name: Permissions
topicUid: Guides.IntFw.Perms
- name: Slash Command Basics
items:
- name: Introduction
@@ -115,6 +117,8 @@
topicUid: Guides.OtherLibs.Serilog
- name: EFCore
topicUid: Guides.OtherLibs.EFCore
- name: MediatR
topicUid: Guides.OtherLibs.MediatR
- name: Emoji
topicUid: Guides.Emoji
- name: Voice


+ 1
- 1
docs/index.md View File

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

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

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


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

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

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

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

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

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

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

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

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

return result;
}
}

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

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

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

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

}

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

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

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

_overrideDomain.Resolving += _overrideDomain_Resolving;
}

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

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

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

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

if (ovrride == null)
return false;

return await AddOverrideAsync(ovrride);
}

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

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

if (!result.IsSuccessStatusCode)
return false;

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

ms.Position = 0;

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

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

List<LoadedOverride> loaded = new();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

</Project>

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

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

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

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

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

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

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

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

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

+ 1
- 0
overrides/Discord.Net.BuildOverrides

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

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

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

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

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

</Project>

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

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

private InteractionHandler _handler;

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


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

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

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

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

</Project>

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

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

namespace MediatRSample;

public class DiscordEventListener
{
private readonly CancellationToken _cancellationToken;

private readonly DiscordSocketClient _client;
private readonly IServiceScopeFactory _serviceScope;

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

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

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

return Task.CompletedTask;
}

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

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

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

namespace MediatRSample.Handlers;

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

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

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

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

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

</Project>

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

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

namespace MediatRSample.Notifications;

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

public SocketMessage Message { get; }
}

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

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

namespace MediatRSample.Notifications;

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

private ReadyNotification()
{
}
}

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

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

namespace MediatRSample;

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

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

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

await using var services = ConfigureServices();

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

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

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

await Task.Delay(Timeout.Infinite);
}

private static Task LogAsync(LogMessage message)
{
var severity = message.Severity switch
{
LogSeverity.Critical => LogEventLevel.Fatal,
LogSeverity.Error => LogEventLevel.Error,
LogSeverity.Warning => LogEventLevel.Warning,
LogSeverity.Info => LogEventLevel.Information,
LogSeverity.Verbose => LogEventLevel.Verbose,
LogSeverity.Debug => LogEventLevel.Debug,
_ => LogEventLevel.Information
};

Log.Write(severity, message.Exception, "[{Source}] {Message}", message.Source, message.Message);

return Task.CompletedTask;
}
}

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

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

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

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

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

</Project>

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

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

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

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

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

</Project>

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

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

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

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

</Project>

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

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


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

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

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

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

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


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

@@ -7,6 +7,8 @@
<Description>The core components for the Discord.Net library.</Description>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">net6.0;net5.0;net461;netstandard2.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">net6.0;net5.0;netstandard2.0;netstandard2.1</TargetFrameworks>
<WarningLevel>5</WarningLevel>
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />


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

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

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

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

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

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

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

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

#region 2FA (60XXX)
MissingPermissionToSendThisSticker = 50600,
Requires2FA = 60003,
#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
}
}

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

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


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


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

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


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

+ 59
- 7
src/Discord.Net.Core/Entities/Guilds/IGuild.cs View File

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

/// <summary>
/// Gets a collection of all users banned in this guild.
/// Gets <paramref name="limit"/> amount of bans from the guild ordered by user ID.
/// </summary>
/// <remarks>
/// <note type="important">
/// The returned collection is an asynchronous enumerable object; one must call
/// <see cref="AsyncEnumerableExtensions.FlattenAsync{T}"/> to access the individual messages as a
/// collection.
/// </note>
/// <note type="warning">
/// Do not fetch too many bans at once! This may cause unwanted preemptive rate limit or even actual
/// rate limit, causing your bot to freeze!
/// </note>
/// </remarks>
/// <param name="limit">The amount of bans to get from the guild.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains a read-only collection of
/// ban objects that this guild currently possesses, with each object containing the user banned and reason
/// behind the ban.
/// A paged collection of bans.
/// </returns>
IAsyncEnumerable<IReadOnlyCollection<IBan>> GetBansAsync(int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null);
/// <summary>
/// Gets <paramref name="limit"/> amount of bans from the guild starting at the provided <paramref name="fromUserId"/> ordered by user ID.
/// </summary>
/// <remarks>
/// <note type="important">
/// The returned collection is an asynchronous enumerable object; one must call
/// <see cref="AsyncEnumerableExtensions.FlattenAsync{T}"/> to access the individual messages as a
/// collection.
/// </note>
/// <note type="warning">
/// Do not fetch too many bans at once! This may cause unwanted preemptive rate limit or even actual
/// rate limit, causing your bot to freeze!
/// </note>
/// </remarks>
/// <param name="fromUserId">The ID of the user to start to get bans from.</param>
/// <param name="dir">The direction of the bans to be gotten.</param>
/// <param name="limit">The number of bans to get.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A paged collection of bans.
/// </returns>
IAsyncEnumerable<IReadOnlyCollection<IBan>> GetBansAsync(ulong fromUserId, Direction dir, int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null);
/// <summary>
/// Gets <paramref name="limit"/> amount of bans from the guild starting at the provided <paramref name="fromUser"/> ordered by user ID.
/// </summary>
/// <remarks>
/// <note type="important">
/// The returned collection is an asynchronous enumerable object; one must call
/// <see cref="AsyncEnumerableExtensions.FlattenAsync{T}"/> to access the individual messages as a
/// collection.
/// </note>
/// <note type="warning">
/// Do not fetch too many bans at once! This may cause unwanted preemptive rate limit or even actual
/// rate limit, causing your bot to freeze!
/// </note>
/// </remarks>
/// <param name="fromUser">The user to start to get bans from.</param>
/// <param name="dir">The direction of the bans to be gotten.</param>
/// <param name="limit">The number of bans to get.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A paged collection of bans.
/// </returns>
Task<IReadOnlyCollection<IBan>> GetBansAsync(RequestOptions options = null);
IAsyncEnumerable<IReadOnlyCollection<IBan>> GetBansAsync(IUser fromUser, Direction dir, int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null);
/// <summary>
/// Gets a ban object for a banned user.
/// </summary>
@@ -1120,7 +1173,6 @@ namespace Discord
/// in order to use this property.
/// </remarks>
/// </param>
/// <param name="speakers">A collection of speakers for the event.</param>
/// <param name="location">The location of the event; links are supported</param>
/// <param name="coverImage">The optional banner image for the event.</param>
/// <param name="options">The options to be used when sending the request.</param>


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

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



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

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

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


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

@@ -69,6 +69,15 @@ namespace Discord
}
}

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

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

internal ApplicationCommandProperties() { }
}
}

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

@@ -41,6 +41,16 @@ namespace Discord
/// </summary>
public IReadOnlyDictionary<string, string> NameLocalizations => _nameLocalizations;

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

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

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

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

return props;
@@ -106,6 +118,16 @@ namespace Discord
}

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

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

@@ -143,5 +165,16 @@ namespace Discord
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>
/// <param name="permissions">The permissions required to use this command.</param>
/// <returns>The current builder.</returns>
public MessageCommandBuilder WithDefaultMemberPermissions(GuildPermission? permissions)
{
DefaultMemberPermissions = permissions;
return this;
}
}
}

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

@@ -41,6 +41,16 @@ namespace Discord
/// </summary>
public IReadOnlyDictionary<string, string> NameLocalizations => _nameLocalizations;

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

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

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

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

return props;
@@ -104,6 +116,16 @@ namespace Discord
}

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

@@ -141,5 +163,16 @@ namespace Discord
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>
/// <param name="permissions">The permissions required to use this command.</param>
/// <returns>The current builder.</returns>
public UserCommandBuilder WithDefaultMemberPermissions(GuildPermission? permissions)
{
DefaultMemberPermissions = permissions;
return this;
}
}
}

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

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

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

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

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


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


+ 102
- 8
src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs View File

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

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

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

var builtMenu = menu.Build();

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

AddComponent(builtMenu);

return this;
}

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

return WithButton(button);
}

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

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

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

AddComponent(builtButton);

return this;
}

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


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

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


+ 61
- 21
src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs View File

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

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

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

private string _name;
private string _description;
private Dictionary<string, string> _nameLocalizations;
@@ -99,6 +109,8 @@ namespace Discord
IsDefaultPermission = IsDefaultPermission,
NameLocalizations = _nameLocalizations,
DescriptionLocalizations = _descriptionLocalizations,
IsDMEnabled = IsDMEnabled,
DefaultMemberPermissions = DefaultMemberPermissions ?? Optional<GuildPermission>.Unspecified
};

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

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

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

/// <summary>
/// Adds an option to the current slash command.
/// </summary>
@@ -170,21 +204,13 @@ namespace Discord
List<SlashCommandOptionBuilder> options = null, List<ChannelType> channelTypes = null, IDictionary<string, string> nameLocalizations = null,
IDictionary<string, string> descriptionLocalizations = null, params ApplicationCommandOptionChoiceProperties[] choices)
{
// Make sure the name matches the requirements from discord
Preconditions.NotNullOrEmpty(name, nameof(name));
Preconditions.AtLeast(name.Length, 1, nameof(name));
Preconditions.AtMost(name.Length, MaxNameLength, nameof(name));
Preconditions.Options(name, description);

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

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

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

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

Options.Add(option);
return this;
@@ -240,14 +267,14 @@ 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)
throw new ArgumentOutOfRangeException(nameof(options), $"Cannot have more than {MaxOptionsCount} options!");

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

Options.AddRange(options);
return this;
}
@@ -535,21 +562,13 @@ namespace Discord
List<SlashCommandOptionBuilder> options = null, List<ChannelType> channelTypes = null, IDictionary<string, string> nameLocalizations = null,
IDictionary<string, string> descriptionLocalizations = null, params ApplicationCommandOptionChoiceProperties[] choices)
{
// Make sure the name matches the requirements from discord
Preconditions.NotNullOrEmpty(name, nameof(name));
Preconditions.AtLeast(name.Length, 1, nameof(name));
Preconditions.AtMost(name.Length, SlashCommandBuilder.MaxNameLength, nameof(name));
Preconditions.Options(name, description);

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

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

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

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

Options.Add(option);
return this;
}

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


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

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

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

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

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

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

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

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

/// <summary>
/// Sets the title of an <see cref="Embed"/>.
/// </summary>


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

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

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

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

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

public override string ToString()
=> DebuggerDisplay;


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


+ 9
- 5
src/Discord.Net.Core/Format.cs View File

@@ -37,8 +37,9 @@ namespace Discord
/// <summary> Sanitizes the string, safely escaping any Markdown sequences. </summary>
public static string Sanitize(string text)
{
foreach (string unsafeChar in SensitiveCharacters)
text = text.Replace(unsafeChar, $"\\{unsafeChar}");
if (text != null)
foreach (string unsafeChar in SensitiveCharacters)
text = text.Replace(unsafeChar, $"\\{unsafeChar}");
return text;
}

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

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

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

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

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

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

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

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

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

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

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

+ 7
- 3
src/Discord.Net.Core/Net/Rest/IRestClient.cs View File

@@ -30,9 +30,13 @@ namespace Discord.Net.Rest
/// <param name="cancelToken">The cancellation token used to cancel the task.</param>
/// <param name="headerOnly">Indicates whether to send the header only.</param>
/// <param name="reason">The audit log reason.</param>
/// <param name="requestHeaders">Additional headers to be sent with the request.</param>
/// <returns></returns>
Task<RestResponse> SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly = false, string reason = null);
Task<RestResponse> SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly = false, string reason = null);
Task<RestResponse> SendAsync(string method, string endpoint, IReadOnlyDictionary<string, object> multipartParams, CancellationToken cancelToken, bool headerOnly = false, string reason = null);
Task<RestResponse> SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly = false, string reason = null,
IEnumerable<KeyValuePair<string, IEnumerable<string>>> requestHeaders = null);
Task<RestResponse> SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly = false, string reason = null,
IEnumerable<KeyValuePair<string, IEnumerable<string>>> requestHeaders = null);
Task<RestResponse> SendAsync(string method, string endpoint, IReadOnlyDictionary<string, object> multipartParams, CancellationToken cancelToken, bool headerOnly = false, string reason = null,
IEnumerable<KeyValuePair<string, IEnumerable<string>>> requestHeaders = null);
}
}

+ 3
- 0
src/Discord.Net.Core/RequestOptions.cs View File

@@ -73,6 +73,8 @@ namespace Discord
internal bool IsReactionBucket { get; set; }
internal bool IsGatewayBucket { get; set; }

internal IDictionary<string, IEnumerable<string>> RequestHeaders { get; }

internal static RequestOptions CreateOrClone(RequestOptions options)
{
if (options == null)
@@ -99,6 +101,7 @@ namespace Discord
public RequestOptions()
{
Timeout = DiscordConfig.DefaultRequestTimeout;
RequestHeaders = new Dictionary<string, IEnumerable<string>>();
}

public RequestOptions Clone() => MemberwiseClone() as RequestOptions;


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

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

#region SlashCommandOptions

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

#endregion
}
}

+ 1
- 1
src/Discord.Net.Core/Utils/UrlValidation.cs View File

@@ -23,7 +23,7 @@ namespace Discord.Utils

/// <summary>
/// Not full URL validation right now. Just Ensures the protocol is either http, https, or discord
/// <see cref="Validate(string)"/> should be used everything other than url buttons.
/// <see cref="Validate(string, bool)"/> should be used everything other than url buttons.
/// </summary>
/// <param name="url">The URL to validate before sending to discord.</param>
/// <exception cref="InvalidOperationException">A URL must include a protocol (either http, https, or discord).</exception>


+ 1
- 1
src/Discord.Net.Examples/Discord.Net.Examples.csproj View File

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

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

<ItemGroup>


+ 3
- 3
src/Discord.Net.Interactions/Attributes/AutocompleteAttribute.cs View File

@@ -3,7 +3,7 @@ using System;
namespace Discord.Interactions
{
/// <summary>
/// Set the <see cref="ApplicationCommandOptionProperties.Autocomplete"/> to <see langword="true"/>.
/// Set the <see cref="ApplicationCommandOptionProperties.IsAutocomplete"/> to <see langword="true"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public class AutocompleteAttribute : Attribute
@@ -14,7 +14,7 @@ namespace Discord.Interactions
public Type AutocompleteHandlerType { get; }

/// <summary>
/// Set the <see cref="ApplicationCommandOptionProperties.Autocomplete"/> to <see langword="true"/> and define a <see cref="AutocompleteHandler"/> to handle
/// Set the <see cref="ApplicationCommandOptionProperties.IsAutocomplete"/> to <see langword="true"/> and define a <see cref="AutocompleteHandler"/> to handle
/// Autocomplete interactions targeting the parameter this <see cref="Attribute"/> is applied to.
/// </summary>
/// <remarks>
@@ -29,7 +29,7 @@ namespace Discord.Interactions
}

/// <summary>
/// Set the <see cref="ApplicationCommandOptionProperties.Autocomplete"/> to <see langword="true"/> without specifying a <see cref="AutocompleteHandler"/>.
/// Set the <see cref="ApplicationCommandOptionProperties.IsAutocomplete"/> to <see langword="true"/> without specifying a <see cref="AutocompleteHandler"/>.
/// </summary>
public AutocompleteAttribute() { }
}


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

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

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

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

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

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


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

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

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

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

+ 0
- 2
src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs View File

@@ -21,9 +21,7 @@ namespace Discord.Interactions
/// <summary>
/// Create a new <see cref="ModalInputAttribute"/>.
/// </summary>
/// <param name="label">The label of the input.</param>
/// <param name="customId">The custom id of the input.</param>
/// <param name="required">Whether the user is required to input a value.></param>
protected ModalInputAttribute(string customId)
{
CustomId = customId;


+ 1
- 1
src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs View File

@@ -36,7 +36,7 @@ namespace Discord.Interactions
/// <summary>
/// Create a new <see cref="ModalTextInputAttribute"/>.
/// </summary>
/// <param name="customId"The custom id of the text input.></param>
/// <param name="customId">The custom id of the text input.></param>
/// <param name="style">The style of the text input.</param>
/// <param name="placeholder">The placeholder of the text input.</param>
/// <param name="minLength">The minimum length of the text input's content.</param>


+ 2
- 2
src/Discord.Net.Interactions/Attributes/Preconditions/RequireUserPermissionAttribute.cs View File

@@ -29,7 +29,7 @@ namespace Discord.Interactions
/// <remarks>
/// This precondition will always fail if the command is being invoked in a <see cref="IPrivateChannel"/>.
/// </remarks>
/// <param name="permission">
/// <param name="guildPermission">
/// The <see cref="Discord.GuildPermission" /> that the user must have. Multiple permissions can be
/// specified by ORing the permissions together.
/// </param>
@@ -41,7 +41,7 @@ namespace Discord.Interactions
/// <summary>
/// Requires that the user invoking the command to have a specific <see cref="Discord.ChannelPermission"/>.
/// </summary>
/// <param name="permission">
/// <param name="channelPermission">
/// The <see cref="Discord.ChannelPermission"/> that the user must have. Multiple permissions can be
/// specified by ORing the permissions together.
/// </param>


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

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

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

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

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

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

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

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

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


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

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

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

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

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

/// <summary>
@@ -45,10 +56,11 @@ namespace Discord.Interactions.Builders
/// <summary>
/// Sets <see cref="DefaultPermission"/>.
/// </summary>
/// <param name="defaultPermision">New value of the <see cref="DefaultPermission"/>.</param>
/// <param name="permission">New value of the <see cref="DefaultPermission"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
[Obsolete($"To be deprecated soon, use {nameof(SetEnabledInDm)} and {nameof(WithDefaultMemberPermissions)} instead.")]
public SlashCommandBuilder WithDefaultPermission (bool permission)
{
DefaultPermission = permission;
@@ -70,6 +82,32 @@ namespace Discord.Interactions.Builders
return this;
}

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

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

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


+ 1
- 1
src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs View File

@@ -41,7 +41,7 @@ namespace Discord.Interactions.Builders
/// <summary>
/// Sets <see cref="Style"/>.
/// </summary>
/// <param name="style">New value of the <see cref="SetValue(string)"/>.</param>
/// <param name="style">New value of the <see cref="Style"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>


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

@@ -64,7 +64,7 @@ namespace Discord.Interactions.Builders
}

/// <summary>
/// Adds text components to <see cref="TextComponents"/>.
/// Adds text components to <see cref="Components"/>.
/// </summary>
/// <param name="configure">Text Component builder factory.</param>
/// <returns>


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

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

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

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

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

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

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

/// <summary>
/// Adds attributes to <see cref="Attributes"/>.
/// </summary>
@@ -319,7 +357,8 @@ namespace Discord.Interactions.Builders
return this;

}

/// <summary>
/// Adds a modal command builder to <see cref="ModalCommands"/>.
/// </summary>
/// <param name="configure"><see cref="ModalCommands"/> factory.</param>


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

Loading…
Cancel
Save