diff --git a/.github/ISSUE_TEMPLATE/bugreport.yml b/.github/ISSUE_TEMPLATE/bugreport.yml index d130991bf..e2c154130 100644 --- a/.github/ISSUE_TEMPLATE/bugreport.yml +++ b/.github/ISSUE_TEMPLATE/bugreport.yml @@ -76,3 +76,11 @@ body: ``` validations: required: false + - type: textarea + id: packages + attributes: + label: Packages + description: Please list all 3rd party packages in use if applicable, including their versions. + placeholder: Discord.Addons.Hosting V5.1.0, Discord.InteractivityAddon V2.4.0, etc. + validations: + required: true diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..71d50ed3a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "overrides/Discord.Net.BuildOverrides"] + path = overrides/Discord.Net.BuildOverrides + url = https://github.com/discord-net/Discord.Net.BuildOverrides diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ba61ed83..3e4de065c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,68 @@ # Changelog +## [3.5.0] - 2022-04-05 + +### Added +- #2204 Added config option for bidirectional formatting of usernames (e38104b) +- #2210 Add a way to remove type readers from the interaction/command service. (7339945) +- #2213 Add global interaction post execution event. (a744948) +- #2223 Add ban pagination support (d8757a5) +- #2201 Add missing interface methods to IComponentInteraction (741ed80) +- #2226 Add an action delegate parameter to `RespondWithModalAsync()` for modifying the modal (d2118f0) +- #2227 Add RespondWithModal methods to RestInteractinModuleBase (1c680db) + +### Fixed +- #2168 Fix Integration model from GuildIntegration and added INTEGRATION gateway events (305d7f9) +- #2187 Fix modal response failing (d656722) +- #2188 Fix serialization error on thread creation timestamp. (d48a7bd) +- #2209 Fix GuildPermissions.All not including newer permissions (91d8fab) +- #2219 Fix ShardedClients not pushing PresenceUpdates (c4131cf) +- #2225 Fix GuildMemberUpdated cacheable `before` entity being incorrect (bfd0d9b) +- #2217 Fix gateway interactions not running without bot scope. (8522447) + +### Misc +- #2193 Update GuildMemberUpdated comment regarding presence (82473bc) +- #2206 Fixed typo (c286b99) +- #2216 Fix small typo in modal example (0439437) +- #2228 Correct minor typo (d1cf1bf) + +## [3.4.1] - 2022-03-9 + +### Added +- #2169 Component TypeConverters and CustomID TypeReaders (fb4250b) +- #2180 Attachment description and content type (765c0c5) +- #2162 Add configuration toggle to suppress Unknown dispatch warnings (1ba96d6) +- #2178 Add 10065 Error code (cc6918d) + +### Fixed +- #2179 Logging out sharded client throws (24b7bb5) +- #2182 Thread owner always returns null (25aaa49) +- #2165 Fix error with flag params when uploading files. (a5d3add) +- #2181 Fix ambiguous reference for creating roles (f8ec3c7) + +## [3.4.0] - 2022-3-2 + +### Added +- #2146 Add FromDateTimeOffset in TimestampTag (553055b) +- #2062 Add return statement to precondition handling (3e52fab) +- #2131 Add support for sending Message Flags (1fb62de) +- #2137 Add self_video to VoiceState (8bcd3da) +- #2151 Add Image property to Guild Scheduled Events (1dc473c) +- #2152 Add missing json error codes (202554f) +- #2153 Add IsInvitable and CreatedAt to threads (6bf5818) +- #2155 Add Interaction Service Complex Parameters (9ba64f6) +- #2156 Add Display name support for enum type converter (c800674) + +### Fixed +- #2117 Fix stream access exception when ratelimited (a1cfa41) +- #2128 Fix context menu comand message type (f601e9b) +- #2135 Fix NRE when ratelimmited requests don't return a body (b95b942) +- #2154 Fix usage of CacheMode.AllowDownload in channels (b3370c3) + +### Misc +- #2149 Clarify Users property on SocketGuildChannel (5594739) +- #2157 Enforce valid button styles (507a18d) + ## [3.3.2] - 2022-02-16 ### Fixed diff --git a/Discord.Net.sln b/Discord.Net.sln index fc68eb71c..544283b8b 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -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} diff --git a/Discord.Net.targets b/Discord.Net.targets index 1b6a19c72..e50e6eceb 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,6 +1,6 @@ - 3.3.2 + 3.5.0 latest Discord.Net Contributors discord;discordapp diff --git a/azure/deploy.yml b/azure/deploy.yml index 4742da3c8..d3460ad6c 100644 --- a/azure/deploy.yml +++ b/azure/deploy.yml @@ -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 diff --git a/docs/docfx.json b/docs/docfx.json index c0821ce5d..2a4ee2867 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -60,7 +60,7 @@ "overwrite": "_overwrites/**/**.md", "globalMetadata": { "_appTitle": "Discord.Net Documentation", - "_appFooter": "Discord.Net (c) 2015-2022 3.3.2", + "_appFooter": "Discord.Net (c) 2015-2022 3.5.0", "_enableSearch": true, "_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg", "_appFaviconPath": "favicon.ico" diff --git a/docs/faq/build_overrides/what-are-they.md b/docs/faq/build_overrides/what-are-they.md new file mode 100644 index 000000000..f76fd6ddb --- /dev/null +++ b/docs/faq/build_overrides/what-are-they.md @@ -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). \ No newline at end of file diff --git a/docs/faq/toc.yml b/docs/faq/toc.yml index 97e327aba..b727f5117 100644 --- a/docs/faq/toc.yml +++ b/docs/faq/toc.yml @@ -22,3 +22,5 @@ topicUid: FAQ.TextCommands.General - name: Legacy or Upgrade topicUid: FAQ.Legacy +- name: Build Overrides + topicUid: FAQ.BuildOverrides.WhatAreThey diff --git a/docs/guides/int_basics/application-commands/slash-commands/choice-slash-command.md b/docs/guides/int_basics/application-commands/slash-commands/choice-slash-command.md index 3951e1141..46805eb7f 100644 --- a/docs/guides/int_basics/application-commands/slash-commands/choice-slash-command.md +++ b/docs/guides/int_basics/application-commands/slash-commands/choice-slash-command.md @@ -27,7 +27,7 @@ private async Task Client_Ready() .AddChoice("Lovely", 4) .AddChoice("Excellent!", 5) .WithType(ApplicationCommandOptionType.Integer) - ).Build(); + ); try { diff --git a/docs/guides/int_framework/intro.md b/docs/guides/int_framework/intro.md index 0a5cc19f1..abea2a735 100644 --- a/docs/guides/int_framework/intro.md +++ b/docs/guides/int_framework/intro.md @@ -143,6 +143,21 @@ In this case, user can only input Stage Channels and Text Channels to this param You can specify the permitted max/min value for a number type parameter using the [MaxValueAttribute] and [MinValueAttribute]. +#### Complex Parameters + +This allows users to create slash command options using an object's constructor allowing complex objects to be created which cannot be infered from only one input value. +Constructor methods support every attribute type that can be used with the regular slash commands ([Autocomplete], [Summary] etc. ). +Preferred constructor of a Type can be specified either by passing a `Type[]` to the `[ComplexParameterAttribute]` or tagging a type constructor with the `[ComplexParameterCtorAttribute]`. If nothing is specified, the InteractionService defaults to the only public constructor of the type. +TypeConverter pattern is used to parse the constructor methods objects. + +[!code-csharp[Complex Parameter](samples/intro/complexparams.cs)] + +Interaction service complex parameter constructors are prioritized in the following order: + +1. Constructor matching the signature provided in the `[ComplexParameter(Type[])]` overload. +2. Constuctor tagged with `[ComplexParameterCtor]`. +3. Type's only public constuctor. + ## User Commands A valid User Command must have the following structure: diff --git a/docs/guides/int_framework/samples/intro/complexparams.cs b/docs/guides/int_framework/samples/intro/complexparams.cs new file mode 100644 index 000000000..72c0616cc --- /dev/null +++ b/docs/guides/int_framework/samples/intro/complexparams.cs @@ -0,0 +1,37 @@ +public class Vector3 +{ + public int X {get;} + public int Y {get;} + public int Z {get;} + + public Vector3() + { + X = 0; + Y = 0; + Z = 0; + } + + [ComplexParameterCtor] + public Vector3(int x, int y, int z) + { + X = x; + Y = y; + Z = z; + } +} + +// Both of the commands below are displayed to the users identically. + +// With complex parameter +[SlashCommand("create-vector", "Create a 3D vector.")] +public async Task CreateVector([ComplexParameter]Vector3 vector3) +{ + ... +} + +// Without complex parameter +[SlashCommand("create-vector", "Create a 3D vector.")] +public async Task CreateVector(int x, int y, int z) +{ + ... +} \ No newline at end of file diff --git a/docs/guides/int_framework/samples/intro/groupattribute.cs b/docs/guides/int_framework/samples/intro/groupattribute.cs index 86a492c31..99d6cd67b 100644 --- a/docs/guides/int_framework/samples/intro/groupattribute.cs +++ b/docs/guides/int_framework/samples/intro/groupattribute.cs @@ -1,16 +1,18 @@ [SlashCommand("blep", "Send a random adorable animal photo")] -public async Task Blep([Choice("Dog", "dog"), Choice("Cat", "cat"), Choice("Penguin", "penguin")] string animal) +public async Task Blep([Choice("Dog", "dog"), Choice("Cat", "cat"), Choice("Guinea pig", "GuineaPig")] string animal) { ... } -// In most cases, you can use an enum to replace the seperate choice attributes in a command. +// In most cases, you can use an enum to replace the separate choice attributes in a command. public enum Animal { Cat, Dog, - Penguin + // You can also use the ChoiceDisplay attribute to change how they appear in the choice menu. + [ChoiceDisplay("Guinea pig")] + GuineaPig } [SlashCommand("blep", "Send a random adorable animal photo")] diff --git a/docs/guides/int_framework/samples/intro/modal.cs b/docs/guides/int_framework/samples/intro/modal.cs index af72fe04e..65cc81abf 100644 --- a/docs/guides/int_framework/samples/intro/modal.cs +++ b/docs/guides/int_framework/samples/intro/modal.cs @@ -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 " + diff --git a/docs/guides/int_framework/samples/postexecution/error_review.cs b/docs/guides/int_framework/samples/postexecution/error_review.cs index dd397b2c9..d5f8a9cb1 100644 --- a/docs/guides/int_framework/samples/postexecution/error_review.cs +++ b/docs/guides/int_framework/samples/postexecution/error_review.cs @@ -16,7 +16,7 @@ async Task SlashCommandExecuted(SlashCommandInfo arg1, Discord.IInteractionConte await arg2.Interaction.RespondAsync("Invalid number or arguments"); break; case InteractionCommandError.Exception: - await arg2.Interaction.RespondAsync("Command exception:{arg3.ErrorReason}"); + await arg2.Interaction.RespondAsync($"Command exception: {arg3.ErrorReason}"); break; case InteractionCommandError.Unsuccessful: await arg2.Interaction.RespondAsync("Command could not be executed"); diff --git a/docs/guides/other_libs/images/mediatr_output.png b/docs/guides/other_libs/images/mediatr_output.png new file mode 100644 index 000000000..801809313 Binary files /dev/null and b/docs/guides/other_libs/images/mediatr_output.png differ diff --git a/docs/guides/other_libs/mediatr.md b/docs/guides/other_libs/mediatr.md new file mode 100644 index 000000000..832628738 --- /dev/null +++ b/docs/guides/other_libs/mediatr.md @@ -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. diff --git a/docs/guides/other_libs/samples/MediatrConfiguringDI.cs b/docs/guides/other_libs/samples/MediatrConfiguringDI.cs new file mode 100644 index 000000000..3bef7bd76 --- /dev/null +++ b/docs/guides/other_libs/samples/MediatrConfiguringDI.cs @@ -0,0 +1 @@ +.AddMediatR(typeof(Bot)) diff --git a/docs/guides/other_libs/samples/MediatrCreatingMessageNotification.cs b/docs/guides/other_libs/samples/MediatrCreatingMessageNotification.cs new file mode 100644 index 000000000..449c96eb4 --- /dev/null +++ b/docs/guides/other_libs/samples/MediatrCreatingMessageNotification.cs @@ -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; } +} diff --git a/docs/guides/other_libs/samples/MediatrDiscordEventListener.cs b/docs/guides/other_libs/samples/MediatrDiscordEventListener.cs new file mode 100644 index 000000000..09583c3e9 --- /dev/null +++ b/docs/guides/other_libs/samples/MediatrDiscordEventListener.cs @@ -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(); + } + } + + public async Task StartAsync() + { + _client.MessageReceived += OnMessageReceivedAsync; + + await Task.CompletedTask; + } + + private Task OnMessageReceivedAsync(SocketMessage arg) + { + return Mediator.Publish(new MessageReceivedNotification(arg), _cancellationToken); + } +} diff --git a/docs/guides/other_libs/samples/MediatrMessageReceivedHandler.cs b/docs/guides/other_libs/samples/MediatrMessageReceivedHandler.cs new file mode 100644 index 000000000..1ab2491e2 --- /dev/null +++ b/docs/guides/other_libs/samples/MediatrMessageReceivedHandler.cs @@ -0,0 +1,17 @@ +// MessageReceivedHandler.cs + +using System; +using MediatR; +using MediatRSample.Notifications; + +namespace MediatRSample; + +public class MessageReceivedHandler : INotificationHandler +{ + public async Task Handle(MessageReceivedNotification notification, CancellationToken cancellationToken) + { + Console.WriteLine($"MediatR works! (Received a message by {notification.Message.Author.Username})"); + + // Your implementation + } +} diff --git a/docs/guides/other_libs/samples/MediatrStartListener.cs b/docs/guides/other_libs/samples/MediatrStartListener.cs new file mode 100644 index 000000000..72a54bf25 --- /dev/null +++ b/docs/guides/other_libs/samples/MediatrStartListener.cs @@ -0,0 +1,4 @@ +// Program.cs + +var listener = services.GetRequiredService(); +await listener.StartAsync(); diff --git a/docs/guides/toc.yml b/docs/guides/toc.yml index b1a6b4721..af0a8e2b4 100644 --- a/docs/guides/toc.yml +++ b/docs/guides/toc.yml @@ -115,6 +115,8 @@ topicUid: Guides.OtherLibs.Serilog - name: EFCore topicUid: Guides.OtherLibs.EFCore + - name: MediatR + topicUid: Guides.OtherLibs.MediatR - name: Emoji topicUid: Guides.Emoji - name: Voice diff --git a/docs/index.md b/docs/index.md index 1dfc41688..3e18513c2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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. diff --git a/experiment/Discord.Net.BuildOverrides/BuildOverrides.cs b/experiment/Discord.Net.BuildOverrides/BuildOverrides.cs new file mode 100644 index 000000000..54b56cc60 --- /dev/null +++ b/experiment/Discord.Net.BuildOverrides/BuildOverrides.cs @@ -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 +{ + /// + /// Represents an override that can be loaded. + /// + public sealed class Override + { + /// + /// Gets the ID of the override. + /// + public Guid Id { get; internal set; } + + /// + /// Gets the name of the override. + /// + public string Name { get; internal set; } + + /// + /// Gets the description of the override. + /// + public string Description { get; internal set; } + + /// + /// Gets the date this override was created. + /// + public DateTimeOffset CreatedAt { get; internal set; } + + /// + /// Gets the date the override was last modified. + /// + 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(); + result.Name = obj["name"].ToObject(); + result.Description = obj["description"].ToObject(); + result.CreatedAt = obj["created_at"].ToObject(); + result.LastUpdated = obj["last_updated"].ToObject(); + } + + return result; + } + } + + /// + /// Represents a loaded override instance. + /// + public sealed class LoadedOverride + { + /// + /// Gets the aseembly containing the overrides definition. + /// + public Assembly Assembly { get; internal set; } + + /// + /// Gets an instance of the override. + /// + public IOverride Instance { get; internal set; } + + /// + /// Gets the overrides type. + /// + public Type Type { get; internal set; } + } + + public sealed class BuildOverrides + { + /// + /// Fired when an override logs a message. + /// + public static event Func Log + { + add => _logEvents.Add(value); + remove => _logEvents.Remove(value); + + } + + /// + /// Gets a read-only dictionary containing the currently loaded overrides. + /// + public IReadOnlyDictionary> LoadedOverrides + => _loadedOverrides.Select(x => new KeyValuePair> (x.Key, x.Value)).ToDictionary(x => x.Key, x => x.Value); + + private static AssemblyLoadContext _overrideDomain; + private static List> _logEvents = new(); + private static ConcurrentDictionary> _loadedOverrides = new ConcurrentDictionary>(); + + private const string ApiUrl = "https://overrides.discordnet.dev"; + + static BuildOverrides() + { + _overrideDomain = new AssemblyLoadContext("Discord.Net.Overrides.Runtime"); + + _overrideDomain.Resolving += _overrideDomain_Resolving; + } + + /// + /// Gets details about a specific override. + /// + /// + /// Note: This method does not load an override, it simply retrives the info about it. + /// + /// The name of the override to get. + /// + /// A task representing the asynchronous get operation. The tasks result is an + /// if it exists; otherwise . + /// + public static async Task 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; + } + } + + /// + /// Adds an override to the current Discord.Net instance. + /// + /// + /// The override initialization is non-blocking, any errors that occor within + /// the overrides initialization procedure will be sent in the event. + /// + /// The name of the override to add. + /// + /// A task representing the asynchronous add operaton. The tasks result is a boolean + /// determining if the add operation was successful. + /// + public static async Task AddOverrideAsync(string name) + { + var ovrride = await GetOverrideAsync(name); + + if (ovrride == null) + return false; + + return await AddOverrideAsync(ovrride); + } + + /// + /// Adds an override to the current Discord.Net instance. + /// + /// + /// The override initialization is non-blocking, any errors that occor within + /// the overrides initialization procedure will be sent in the event. + /// + /// The override to add. + /// + /// A task representing the asynchronous add operaton. The tasks result is a boolean + /// determining if the add operation was successful. + /// + public static async Task 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 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 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); + } + } + } + } +} diff --git a/experiment/Discord.Net.BuildOverrides/Discord.Net.BuildOverrides.csproj b/experiment/Discord.Net.BuildOverrides/Discord.Net.BuildOverrides.csproj new file mode 100644 index 000000000..25b1c40b0 --- /dev/null +++ b/experiment/Discord.Net.BuildOverrides/Discord.Net.BuildOverrides.csproj @@ -0,0 +1,20 @@ + + + + 9.0 + Discord.Net.BuildOverrides + Discord.BuildOverrides + A Discord.Net extension adding a way to add build overrides for testing. + net6.0;net5.0; + net6.0;net5.0; + + + + + + + + + + + diff --git a/experiment/Discord.Net.BuildOverrides/IOverride.cs b/experiment/Discord.Net.BuildOverrides/IOverride.cs new file mode 100644 index 000000000..17327ae2c --- /dev/null +++ b/experiment/Discord.Net.BuildOverrides/IOverride.cs @@ -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 +{ + /// + /// Represents a generic build override for Discord.Net + /// + public interface IOverride + { + /// + /// Initializes the override. + /// + /// + /// This method is called by the class + /// and should not be called externally from it. + /// + /// Context used by an override to initialize. + /// + /// A task representing the asynchronous initialization operation. + /// + Task InitializeAsync(OverrideContext context); + + /// + /// Registers a callback to load a dependency for this override. + /// + /// The callback to load an external dependency. + void RegisterPackageLookupHandler(Func> func); + } +} diff --git a/experiment/Discord.Net.BuildOverrides/OverrideContext.cs b/experiment/Discord.Net.BuildOverrides/OverrideContext.cs new file mode 100644 index 000000000..1e88be74a --- /dev/null +++ b/experiment/Discord.Net.BuildOverrides/OverrideContext.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.Overrides +{ + /// + /// Represents context thats passed to an override in the initialization step. + /// + public sealed class OverrideContext + { + /// + /// A callback used to log messages. + /// + public Action Log { get; private set; } + + /// + /// The info about the override. + /// + public Override Info { get; private set; } + + internal OverrideContext(Action log, Override info) + { + Log = log; + Info = info; + } + } +} diff --git a/overrides/Discord.Net.BuildOverrides b/overrides/Discord.Net.BuildOverrides new file mode 160000 index 000000000..9b2be5597 --- /dev/null +++ b/overrides/Discord.Net.BuildOverrides @@ -0,0 +1 @@ +Subproject commit 9b2be5597468329090015fa1b2775815b20be440 diff --git a/samples/InteractionFramework/CommandHandler.cs b/samples/InteractionFramework/CommandHandler.cs deleted file mode 100644 index 9a505246f..000000000 --- a/samples/InteractionFramework/CommandHandler.cs +++ /dev/null @@ -1,152 +0,0 @@ -using Discord; -using Discord.Interactions; -using Discord.WebSocket; -using System; -using System.Reflection; -using System.Threading.Tasks; - -namespace InteractionFramework -{ - public class CommandHandler - { - private readonly DiscordSocketClient _client; - private readonly InteractionService _commands; - private readonly IServiceProvider _services; - - public CommandHandler(DiscordSocketClient client, InteractionService commands, IServiceProvider services) - { - _client = client; - _commands = commands; - _services = services; - } - - public async Task InitializeAsync ( ) - { - // Add the public modules that inherit InteractionModuleBase to the InteractionService - await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); - // Another approach to get the assembly of a specific type is: - // typeof(CommandHandler).Assembly - - - // Process the InteractionCreated payloads to execute Interactions commands - _client.InteractionCreated += HandleInteraction; - - // Process the command execution results - _commands.SlashCommandExecuted += SlashCommandExecuted; - _commands.ContextCommandExecuted += ContextCommandExecuted; - _commands.ComponentCommandExecuted += ComponentCommandExecuted; - } - - # region Error Handling - - private Task ComponentCommandExecuted (ComponentCommandInfo arg1, Discord.IInteractionContext arg2, IResult arg3) - { - if (!arg3.IsSuccess) - { - switch (arg3.Error) - { - case InteractionCommandError.UnmetPrecondition: - // implement - break; - case InteractionCommandError.UnknownCommand: - // implement - break; - case InteractionCommandError.BadArgs: - // implement - break; - case InteractionCommandError.Exception: - // implement - break; - case InteractionCommandError.Unsuccessful: - // implement - break; - default: - break; - } - } - - return Task.CompletedTask; - } - - private Task ContextCommandExecuted (ContextCommandInfo arg1, Discord.IInteractionContext arg2, IResult arg3) - { - if (!arg3.IsSuccess) - { - switch (arg3.Error) - { - case InteractionCommandError.UnmetPrecondition: - // implement - break; - case InteractionCommandError.UnknownCommand: - // implement - break; - case InteractionCommandError.BadArgs: - // implement - break; - case InteractionCommandError.Exception: - // implement - break; - case InteractionCommandError.Unsuccessful: - // implement - break; - default: - break; - } - } - - return Task.CompletedTask; - } - - private Task SlashCommandExecuted (SlashCommandInfo arg1, Discord.IInteractionContext arg2, IResult arg3) - { - if (!arg3.IsSuccess) - { - switch (arg3.Error) - { - case InteractionCommandError.UnmetPrecondition: - // implement - break; - case InteractionCommandError.UnknownCommand: - // implement - break; - case InteractionCommandError.BadArgs: - // implement - break; - case InteractionCommandError.Exception: - // implement - break; - case InteractionCommandError.Unsuccessful: - // implement - break; - default: - break; - } - } - - return Task.CompletedTask; - } - # endregion - - # region Execution - - private async Task HandleInteraction (SocketInteraction arg) - { - try - { - // Create an execution context that matches the generic type parameter of your InteractionModuleBase modules - var ctx = new SocketInteractionContext(_client, arg); - await _commands.ExecuteCommandAsync(ctx, _services); - } - catch (Exception ex) - { - Console.WriteLine(ex); - - // If a Slash Command execution fails it is most likely that the original interaction acknowledgement will persist. It is a good idea to delete the original - // response, or at least let the user know that something went wrong during the command execution. - if(arg.Type == InteractionType.ApplicationCommand) - await arg.GetOriginalResponseAsync().ContinueWith(async (msg) => await msg.Result.DeleteAsync()); - } - } - # endregion - } -} diff --git a/samples/InteractionFramework/Enums/ExampleEnum.cs b/samples/InteractionFramework/Enums/ExampleEnum.cs new file mode 100644 index 000000000..a70dd49a9 --- /dev/null +++ b/samples/InteractionFramework/Enums/ExampleEnum.cs @@ -0,0 +1,20 @@ +using Discord.Interactions; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace InteractionFramework +{ + public enum ExampleEnum + { + First, + Second, + Third, + Fourth, + [ChoiceDisplay("Twenty First")] + TwentyFirst + } +} diff --git a/samples/InteractionFramework/ExampleEnum.cs b/samples/InteractionFramework/ExampleEnum.cs deleted file mode 100644 index 755f33d17..000000000 --- a/samples/InteractionFramework/ExampleEnum.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace InteractionFramework -{ - public enum ExampleEnum - { - First, - Second, - Third, - Fourth - } -} diff --git a/samples/InteractionFramework/InteractionHandler.cs b/samples/InteractionFramework/InteractionHandler.cs new file mode 100644 index 000000000..bc6f47285 --- /dev/null +++ b/samples/InteractionFramework/InteractionHandler.cs @@ -0,0 +1,81 @@ +using Discord; +using Discord.Interactions; +using Discord.WebSocket; +using Microsoft.Extensions.Configuration; +using System; +using System.Reflection; +using System.Threading.Tasks; + +namespace InteractionFramework +{ + public class InteractionHandler + { + private readonly DiscordSocketClient _client; + private readonly InteractionService _handler; + private readonly IServiceProvider _services; + private readonly IConfiguration _configuration; + + public InteractionHandler(DiscordSocketClient client, InteractionService handler, IServiceProvider services, IConfiguration config) + { + _client = client; + _handler = handler; + _services = services; + _configuration = config; + } + + public async Task InitializeAsync() + { + // Process when the client is ready, so we can register our commands. + _client.Ready += ReadyAsync; + _handler.Log += LogAsync; + + // Add the public modules that inherit InteractionModuleBase to the InteractionService + await _handler.AddModulesAsync(Assembly.GetEntryAssembly(), _services); + + // Process the InteractionCreated payloads to execute Interactions commands + _client.InteractionCreated += HandleInteraction; + } + + private async Task LogAsync(LogMessage log) + => Console.WriteLine(log); + + private async Task ReadyAsync() + { + // Context & Slash commands can be automatically registered, but this process needs to happen after the client enters the READY state. + // Since Global Commands take around 1 hour to register, we should use a test guild to instantly update and test our commands. + if (Program.IsDebug()) + await _handler.RegisterCommandsToGuildAsync(_configuration.GetValue("testGuild"), true); + else + await _handler.RegisterCommandsGloballyAsync(true); + } + + private async Task HandleInteraction(SocketInteraction interaction) + { + try + { + // Create an execution context that matches the generic type parameter of your InteractionModuleBase modules. + var context = new SocketInteractionContext(_client, interaction); + + // Execute the incoming command. + var result = await _handler.ExecuteCommandAsync(context, _services); + + if (!result.IsSuccess) + switch (result.Error) + { + case InteractionCommandError.UnmetPrecondition: + // implement + break; + default: + break; + } + } + catch + { + // If Slash Command execution fails it is most likely that the original interaction acknowledgement will persist. It is a good idea to delete the original + // response, or at least let the user know that something went wrong during the command execution. + if (interaction.Type is InteractionType.ApplicationCommand) + await interaction.GetOriginalResponseAsync().ContinueWith(async (msg) => await msg.Result.DeleteAsync()); + } + } + } +} diff --git a/samples/InteractionFramework/Modules/ComponentModule.cs b/samples/InteractionFramework/Modules/ComponentModule.cs deleted file mode 100644 index 643004ded..000000000 --- a/samples/InteractionFramework/Modules/ComponentModule.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Discord.Interactions; -using Discord.WebSocket; -using InteractionFramework.Attributes; -using System.Threading.Tasks; - -namespace InteractionFramework -{ - // As with all other modules, we create the context by defining what type of interaction this module is supposed to target. - internal class ComponentModule : InteractionModuleBase> - { - // With the Attribute DoUserCheck you can make sure that only the user this button targets can click it. This is defined by the first wildcard: *. - // See Attributes/DoUserCheckAttribute.cs for elaboration. - [DoUserCheck] - [ComponentInteraction("myButton:*")] - public async Task ClickButtonAsync(string userId) - => await RespondAsync(text: ":thumbsup: Clicked!"); - } -} diff --git a/samples/InteractionFramework/Modules/GeneralModule.cs b/samples/InteractionFramework/Modules/ExampleModule.cs similarity index 54% rename from samples/InteractionFramework/Modules/GeneralModule.cs rename to samples/InteractionFramework/Modules/ExampleModule.cs index 78740a960..21064bbe3 100644 --- a/samples/InteractionFramework/Modules/GeneralModule.cs +++ b/samples/InteractionFramework/Modules/ExampleModule.cs @@ -1,32 +1,25 @@ using Discord; using Discord.Interactions; +using InteractionFramework.Attributes; +using System; using System.Threading.Tasks; namespace InteractionFramework.Modules { // Interation modules must be public and inherit from an IInterationModuleBase - public class GeneralModule : InteractionModuleBase + public class ExampleModule : InteractionModuleBase { // Dependencies can be accessed through Property injection, public properties with public setters will be set by the service provider public InteractionService Commands { get; set; } - private CommandHandler _handler; + private InteractionHandler _handler; - // Constructor injection is also a valid way to access the dependecies - public GeneralModule(CommandHandler handler) + // Constructor injection is also a valid way to access the dependencies + public ExampleModule(InteractionHandler handler) { _handler = handler; } - // Slash Commands are declared using the [SlashCommand], you need to provide a name and a description, both following the Discord guidelines - [SlashCommand("ping", "Recieve a pong")] - // By setting the DefaultPermission to false, you can disable the command by default. No one can use the command until you give them permission - [DefaultPermission(false)] - public async Task Ping ( ) - { - await RespondAsync("pong"); - } - // You can use a number of parameter types in you Slash Command handlers (string, int, double, bool, IUser, IChannel, IMentionable, IRole, Enums) by default. Optionally, // you can implement your own TypeConverters to support a wider range of parameter types. For more information, refer to the library documentation. // Optional method parameters(parameters with a default value) also will be displayed as optional on Discord. @@ -34,9 +27,15 @@ namespace InteractionFramework.Modules // [Summary] lets you customize the name and the description of a parameter [SlashCommand("echo", "Repeat the input")] public async Task Echo(string echo, [Summary(description: "mention the user")]bool mention = false) - { - await RespondAsync(echo + (mention ? Context.User.Mention : string.Empty)); - } + => await RespondAsync(echo + (mention ? Context.User.Mention : string.Empty)); + + [SlashCommand("ping", "Pings the bot and returns its latency.")] + public async Task GreetUserAsync() + => await RespondAsync(text: $":ping_pong: It took me {Context.Client.Latency}ms to respond to you!", ephemeral: true); + + [SlashCommand("bitrate", "Gets the bitrate of a specific voice channel.")] + public async Task GetBitrateAsync([ChannelTypes(ChannelType.Voice, ChannelType.Stage)] IChannel channel) + => await RespondAsync(text: $"This voice channel has a bitrate of {(channel as IVoiceChannel).Bitrate}"); // [Group] will create a command group. [SlashCommand]s and [ComponentInteraction]s will be registered with the group prefix [Group("test_group", "This is a command group")] @@ -46,25 +45,7 @@ namespace InteractionFramework.Modules // choice option [SlashCommand("choice_example", "Enums create choices")] public async Task ChoiceExample(ExampleEnum input) - { - await RespondAsync(input.ToString()); - } - } - - // User Commands can only have one parameter, which must be a type of SocketUser - [UserCommand("SayHello")] - public async Task SayHello(IUser user) - { - await RespondAsync($"Hello, {user.Mention}"); - } - - // Message Commands can only have one parameter, which must be a type of SocketMessage - [MessageCommand("Delete")] - [Attributes.RequireOwner] - public async Task DeleteMesage(IMessage message) - { - await message.DeleteAsync(); - await RespondAsync("Deleted message."); + => await RespondAsync(input.ToString()); } // Use [ComponentInteraction] to handle message component interactions. Message component interaction with the matching customId will be executed. @@ -80,9 +61,40 @@ namespace InteractionFramework.Modules // Select Menu interactions, contain ids of the menu options that were selected by the user. You can access the option ids from the method parameters. // You can also use the wild card pattern with Select Menus, in that case, the wild card captures will be passed on to the method first, followed by the option ids. [ComponentInteraction("roleSelect")] - public async Task RoleSelect(params string[] selections) + public async Task RoleSelect(string[] selections) + { + throw new NotImplementedException(); + } + + // With the Attribute DoUserCheck you can make sure that only the user this button targets can click it. This is defined by the first wildcard: *. + // See Attributes/DoUserCheckAttribute.cs for elaboration. + [DoUserCheck] + [ComponentInteraction("myButton:*")] + public async Task ClickButtonAsync(string userId) + => await RespondAsync(text: ":thumbsup: Clicked!"); + + // This command will greet target user in the channel this was executed in. + [UserCommand("greet")] + public async Task GreetUserAsync(IUser user) + => await RespondAsync(text: $":wave: {Context.User} said hi to you, <@{user.Id}>!"); + + // Pins a message in the channel it is in. + [MessageCommand("pin")] + public async Task PinMessageAsync(IMessage message) { - // implement + // make a safety cast to check if the message is ISystem- or IUserMessage + if (message is not IUserMessage userMessage) + await RespondAsync(text: ":x: You cant pin system messages!"); + + // if the pins in this channel are equal to or above 50, no more messages can be pinned. + else if ((await Context.Channel.GetPinnedMessagesAsync()).Count >= 50) + await RespondAsync(text: ":x: You cant pin any more messages, the max has already been reached in this channel!"); + + else + { + await userMessage.PinAsync(); + await RespondAsync(":white_check_mark: Successfully pinned message!"); + } } } } diff --git a/samples/InteractionFramework/Modules/MessageCommandModule.cs b/samples/InteractionFramework/Modules/MessageCommandModule.cs deleted file mode 100644 index d07d276f5..000000000 --- a/samples/InteractionFramework/Modules/MessageCommandModule.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Discord; -using Discord.Interactions; -using Discord.WebSocket; -using System.Threading.Tasks; - -namespace InteractionFramework.Modules -{ - // A transient module for executing commands. This module will NOT keep any information after the command is executed. - internal class MessageCommandModule : InteractionModuleBase> - { - // Pins a message in the channel it is in. - [MessageCommand("pin")] - public async Task PinMessageAsync(IMessage message) - { - // make a safety cast to check if the message is ISystem- or IUserMessage - if (message is not IUserMessage userMessage) - await RespondAsync(text: ":x: You cant pin system messages!"); - - // if the pins in this channel are equal to or above 50, no more messages can be pinned. - else if ((await Context.Channel.GetPinnedMessagesAsync()).Count >= 50) - await RespondAsync(text: ":x: You cant pin any more messages, the max has already been reached in this channel!"); - - else - { - await userMessage.PinAsync(); - await RespondAsync(":white_check_mark: Successfully pinned message!"); - } - } - } -} diff --git a/samples/InteractionFramework/Modules/SlashCommandModule.cs b/samples/InteractionFramework/Modules/SlashCommandModule.cs deleted file mode 100644 index a066ea18c..000000000 --- a/samples/InteractionFramework/Modules/SlashCommandModule.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Discord; -using Discord.Interactions; -using Discord.WebSocket; -using System; -using System.Threading.Tasks; - -namespace InteractionFramework.Modules -{ - public enum Hobby - { - Gaming, - - Art, - - Reading - } - - // A transient module for executing commands. This module will NOT keep any information after the command is executed. - class SlashCommandModule : InteractionModuleBase> - { - // Will be called before execution. Here you can populate several entities you may want to retrieve before executing a command. - // I.E. database objects - public override void BeforeExecute(ICommandInfo command) - { - // Anything - throw new NotImplementedException(); - } - - // Will be called after execution - public override void AfterExecute(ICommandInfo command) - { - // Anything - throw new NotImplementedException(); - } - - [SlashCommand("ping", "Pings the bot and returns its latency.")] - public async Task GreetUserAsync() - => await RespondAsync(text: $":ping_pong: It took me {Context.Client.Latency}ms to respond to you!", ephemeral: true); - - [SlashCommand("hobby", "Choose your hobby from the list!")] - public async Task ChooseAsync(Hobby hobby) - => await RespondAsync(text: $":thumbsup: Your hobby is: {hobby}."); - - [SlashCommand("bitrate", "Gets the bitrate of a specific voice channel.")] - public async Task GetBitrateAsync([ChannelTypes(ChannelType.Voice, ChannelType.Stage)] IChannel channel) - { - var voiceChannel = channel as IVoiceChannel; - await RespondAsync(text: $"This voice channel has a bitrate of {voiceChannel.Bitrate}"); - } - } -} diff --git a/samples/InteractionFramework/Modules/UserCommandModule.cs b/samples/InteractionFramework/Modules/UserCommandModule.cs deleted file mode 100644 index 60c5246ce..000000000 --- a/samples/InteractionFramework/Modules/UserCommandModule.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Discord; -using Discord.Interactions; -using Discord.WebSocket; -using System.Threading.Tasks; - -namespace InteractionFramework.Modules -{ - // A transient module for executing commands. This module will NOT keep any information after the command is executed. - class UserCommandModule : InteractionModuleBase> - { - // This command will greet target user in the channel this was executed in. - [UserCommand("greet")] - public async Task GreetUserAsync(IUser user) - => await RespondAsync(text: $":wave: {Context.User} said hi to you, <@{user.Id}>!"); - } -} - diff --git a/samples/InteractionFramework/Program.cs b/samples/InteractionFramework/Program.cs index 49db29714..b9c4697af 100644 --- a/samples/InteractionFramework/Program.cs +++ b/samples/InteractionFramework/Program.cs @@ -9,69 +9,60 @@ using System.Threading.Tasks; namespace InteractionFramework { - class Program + public class Program { - // Entry point of the program. - static void Main ( string[] args ) + private readonly IConfiguration _configuration; + private readonly IServiceProvider _services; + + private readonly DiscordSocketConfig _socketConfig = new() + { + GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.GuildMembers, + AlwaysDownloadUsers = true, + }; + + public Program() { - // One of the more flexable ways to access the configuration data is to use the Microsoft's Configuration model, - // this way we can avoid hard coding the environment secrets. I opted to use the Json and environment variable providers here. - IConfiguration config = new ConfigurationBuilder() + _configuration = new ConfigurationBuilder() .AddEnvironmentVariables(prefix: "DC_") .AddJsonFile("appsettings.json", optional: true) .Build(); - RunAsync(config).GetAwaiter().GetResult(); + _services = new ServiceCollection() + .AddSingleton(_configuration) + .AddSingleton(_socketConfig) + .AddSingleton() + .AddSingleton(x => new InteractionService(x.GetRequiredService())) + .AddSingleton() + .BuildServiceProvider(); } - static async Task RunAsync (IConfiguration configuration) - { - // Dependency injection is a key part of the Interactions framework but it needs to be disposed at the end of the app's lifetime. - using var services = ConfigureServices(configuration); + static void Main(string[] args) + => new Program().RunAsync() + .GetAwaiter() + .GetResult(); - var client = services.GetRequiredService(); - var commands = services.GetRequiredService(); + public async Task RunAsync() + { + var client = _services.GetRequiredService(); client.Log += LogAsync; - commands.Log += LogAsync; - - // Slash Commands and Context Commands are can be automatically registered, but this process needs to happen after the client enters the READY state. - // Since Global Commands take around 1 hour to register, we should use a test guild to instantly update and test our commands. To determine the method we should - // register the commands with, we can check whether we are in a DEBUG environment and if we are, we can register the commands to a predetermined test guild. - client.Ready += async ( ) => - { - if (IsDebug()) - // Id of the test guild can be provided from the Configuration object - await commands.RegisterCommandsToGuildAsync(configuration.GetValue("testGuild"), true); - else - await commands.RegisterCommandsGloballyAsync(true); - }; // Here we can initialize the service that will register and execute our commands - await services.GetRequiredService().InitializeAsync(); + await _services.GetRequiredService() + .InitializeAsync(); // Bot token can be provided from the Configuration object we set up earlier - await client.LoginAsync(TokenType.Bot, configuration["token"]); + await client.LoginAsync(TokenType.Bot, _configuration["token"]); await client.StartAsync(); + // Never quit the program until manually forced to. await Task.Delay(Timeout.Infinite); } - static Task LogAsync(LogMessage message) - { - Console.WriteLine(message.ToString()); - return Task.CompletedTask; - } - - static ServiceProvider ConfigureServices ( IConfiguration configuration ) - => new ServiceCollection() - .AddSingleton(configuration) - .AddSingleton() - .AddSingleton(x => new InteractionService(x.GetRequiredService())) - .AddSingleton() - .BuildServiceProvider(); + private async Task LogAsync(LogMessage message) + => Console.WriteLine(message.ToString()); - static bool IsDebug ( ) + public static bool IsDebug() { #if DEBUG return true; diff --git a/samples/MediatRSample/MediatRSample.sln b/samples/MediatRSample/MediatRSample.sln new file mode 100644 index 000000000..d0599ae26 --- /dev/null +++ b/samples/MediatRSample/MediatRSample.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediatRSample", "MediatRSample\MediatRSample.csproj", "{CE066EE5-7ED1-42A0-8DB2-862D44F40EA7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CE066EE5-7ED1-42A0-8DB2-862D44F40EA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE066EE5-7ED1-42A0-8DB2-862D44F40EA7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE066EE5-7ED1-42A0-8DB2-862D44F40EA7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE066EE5-7ED1-42A0-8DB2-862D44F40EA7}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/samples/MediatRSample/MediatRSample/DiscordEventListener.cs b/samples/MediatRSample/MediatRSample/DiscordEventListener.cs new file mode 100644 index 000000000..dec342773 --- /dev/null +++ b/samples/MediatRSample/MediatRSample/DiscordEventListener.cs @@ -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(); + } + } + + 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); + } +} \ No newline at end of file diff --git a/samples/MediatRSample/MediatRSample/Handlers/MessageReceivedHandler.cs b/samples/MediatRSample/MediatRSample/Handlers/MessageReceivedHandler.cs new file mode 100644 index 000000000..5cae3f267 --- /dev/null +++ b/samples/MediatRSample/MediatRSample/Handlers/MessageReceivedHandler.cs @@ -0,0 +1,14 @@ +using MediatR; +using MediatRSample.Notifications; + +namespace MediatRSample.Handlers; + +public class MessageReceivedHandler : INotificationHandler +{ + public async Task Handle(MessageReceivedNotification notification, CancellationToken cancellationToken) + { + Console.WriteLine($"MediatR works! (Received a message by {notification.Message.Author.Username})"); + + // Your implementation + } +} \ No newline at end of file diff --git a/samples/MediatRSample/MediatRSample/MediatRSample.csproj b/samples/MediatRSample/MediatRSample/MediatRSample.csproj new file mode 100644 index 000000000..4e9d01c8c --- /dev/null +++ b/samples/MediatRSample/MediatRSample/MediatRSample.csproj @@ -0,0 +1,20 @@ + + + + Exe + net6.0 + enable + enable + Linux + + + + + + + + + + + + diff --git a/samples/MediatRSample/MediatRSample/Notifications/MessageReceivedNotification.cs b/samples/MediatRSample/MediatRSample/Notifications/MessageReceivedNotification.cs new file mode 100644 index 000000000..610b4a0a5 --- /dev/null +++ b/samples/MediatRSample/MediatRSample/Notifications/MessageReceivedNotification.cs @@ -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; } +} \ No newline at end of file diff --git a/samples/MediatRSample/MediatRSample/Notifications/ReadyNotification.cs b/samples/MediatRSample/MediatRSample/Notifications/ReadyNotification.cs new file mode 100644 index 000000000..bafa6c10b --- /dev/null +++ b/samples/MediatRSample/MediatRSample/Notifications/ReadyNotification.cs @@ -0,0 +1,13 @@ +using MediatR; + +namespace MediatRSample.Notifications; + +public class ReadyNotification : INotification +{ + public static readonly ReadyNotification Default + = new(); + + private ReadyNotification() + { + } +} \ No newline at end of file diff --git a/samples/MediatRSample/MediatRSample/Program.cs b/samples/MediatRSample/MediatRSample/Program.cs new file mode 100644 index 000000000..96b393e5d --- /dev/null +++ b/samples/MediatRSample/MediatRSample/Program.cs @@ -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() + .AddSingleton(x => new InteractionService(x.GetRequiredService())) + .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(); + client.Log += LogAsync; + + var listener = services.GetRequiredService(); + 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; + } +} diff --git a/samples/ShardedClient/Modules/InteractionModule.cs b/samples/ShardedClient/Modules/InteractionModule.cs index 089328e7d..6c2f0e940 100644 --- a/samples/ShardedClient/Modules/InteractionModule.cs +++ b/samples/ShardedClient/Modules/InteractionModule.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; namespace ShardedClient.Modules { // A display of portability, which shows how minimal the difference between the 2 frameworks is. - public class InteractionModule : InteractionModuleBase> + public class InteractionModule : InteractionModuleBase { [SlashCommand("info", "Information about this shard.")] public async Task InfoAsync() diff --git a/samples/ShardedClient/Program.cs b/samples/ShardedClient/Program.cs index 717ce1d80..2b8f49edb 100644 --- a/samples/ShardedClient/Program.cs +++ b/samples/ShardedClient/Program.cs @@ -45,8 +45,11 @@ namespace ShardedClient client.ShardReady += ReadyAsync; client.Log += LogAsync; - await services.GetRequiredService().InitializeAsync(); - await services.GetRequiredService().InitializeAsync(); + await services.GetRequiredService() + .InitializeAsync(); + + await services.GetRequiredService() + .InitializeAsync(); // Tokens should be considered secret data, and never hard-coded. await client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token")); diff --git a/samples/ShardedClient/Services/InteractionHandlingService.cs b/samples/ShardedClient/Services/InteractionHandlingService.cs index 59b479361..3c41d7f33 100644 --- a/samples/ShardedClient/Services/InteractionHandlingService.cs +++ b/samples/ShardedClient/Services/InteractionHandlingService.cs @@ -31,9 +31,9 @@ namespace ShardedClient.Services { await _service.AddModulesAsync(typeof(InteractionHandlingService).Assembly, _provider); #if DEBUG - await _service.AddCommandsToGuildAsync(_client.Guilds.First(x => x.Id == 1)); + await _service.RegisterCommandsToGuildAsync(1 /* implement */); #else - await _service.AddCommandsGloballyAsync(); + await _service.RegisterCommandsGloballyAsync(); #endif } diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index d6dfc2fb7..57e0e430e 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -403,6 +403,41 @@ namespace Discord.Commands AddNullableTypeReader(type, reader); } } + + /// + /// Removes a type reader from the list of type readers. + /// + /// + /// Removing a from the will not dereference the from the loaded module/command instances. + /// You need to reload the modules for the changes to take effect. + /// + /// The type to remove the readers from. + /// if the default readers for should be removed; otherwise . + /// The removed collection of type readers. + /// if the remove operation was successful; otherwise . + public bool TryRemoveTypeReader(Type type, bool isDefaultTypeReader, out IDictionary readers) + { + readers = new Dictionary(); + + if (isDefaultTypeReader) + { + var isSuccess = _defaultTypeReaders.TryRemove(type, out var result); + if (isSuccess) + readers.Add(result?.GetType(), result); + + return isSuccess; + } + else + { + var isSuccess = _typeReaders.TryRemove(type, out var result); + + if (isSuccess) + readers = result; + + return isSuccess; + } + } + internal bool HasDefaultTypeReader(Type type) { if (_defaultTypeReaders.ContainsKey(type)) diff --git a/src/Discord.Net.Core/CDN.cs b/src/Discord.Net.Core/CDN.cs index d6535a4f1..1a8795101 100644 --- a/src/Discord.Net.Core/CDN.cs +++ b/src/Discord.Net.Core/CDN.cs @@ -208,6 +208,18 @@ namespace Discord public static string GetStickerUrl(ulong stickerId, StickerFormatType format = StickerFormatType.Png) => $"{DiscordConfig.CDNUrl}stickers/{stickerId}.{FormatToExtension(format)}"; + /// + /// Returns an events cover image url. + /// + /// The guild id that the event is in. + /// The id of the event. + /// The id of the cover image asset. + /// The format of the image. + /// The size of the image. + /// + public static string GetEventCoverImageUrl(ulong guildId, ulong eventId, string assetId, ImageFormat format = ImageFormat.Auto, ushort size = 1024) + => $"{DiscordConfig.CDNUrl}guild-events/{guildId}/{eventId}/{assetId}.{FormatToExtension(format, assetId)}?size={size}"; + private static string FormatToExtension(StickerFormatType format) { return format switch diff --git a/src/Discord.Net.Core/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs index 34bfc5e62..067c55225 100644 --- a/src/Discord.Net.Core/DiscordConfig.cs +++ b/src/Discord.Net.Core/DiscordConfig.cs @@ -97,6 +97,13 @@ namespace Discord /// public const int MaxUsersPerBatch = 1000; /// + /// Returns the max bans allowed to be in a request. + /// + /// + /// The maximum number of bans that can be gotten per-batch. + /// + public const int MaxBansPerBatch = 1000; + /// /// Returns the max users allowed to be in a request for guild event users. /// /// @@ -187,5 +194,15 @@ namespace Discord /// This will still require a stable clock on your system. /// public bool UseInteractionSnowflakeDate { get; set; } = true; + + /// + /// Gets or sets if the Rest/Socket user override formats the string in respect to bidirectional unicode. + /// + /// + /// By default, the returned value will be "?Discord?#1234", to work with bidirectional usernames. + ///
+ /// If set to , this value will be "Discord#1234". + ///
+ public bool FormatUsersInBidirectionalUnicode { get; set; } = true; } } diff --git a/src/Discord.Net.Core/DiscordErrorCode.cs b/src/Discord.Net.Core/DiscordErrorCode.cs index 0e481f245..51fd736f6 100644 --- a/src/Discord.Net.Core/DiscordErrorCode.cs +++ b/src/Discord.Net.Core/DiscordErrorCode.cs @@ -48,6 +48,7 @@ namespace Discord UnknownSticker = 10060, UnknownInteraction = 10062, UnknownApplicationCommand = 10063, + UnknownVoiceState = 10065, UnknownApplicationCommandPermissions = 10066, UnknownStageInstance = 10067, UnknownGuildMemberVerificationForm = 10068, @@ -97,11 +98,13 @@ namespace Discord #endregion #region General Request Errors (40XXX) + MaximumNumberOfEditsReached = 30046, MaximumNumberOfPinnedThreadsInAForumChannelReached = 30047, MaximumNumberOfTagsInAForumChannelReached = 30048, TokenUnauthorized = 40001, InvalidVerification = 40002, OpeningDMTooFast = 40003, + SendMessagesHasBeenTemporarilyDisabled = 40004, RequestEntityTooLarge = 40005, FeatureDisabled = 40006, UserBanned = 40007, @@ -111,6 +114,7 @@ namespace Discord #endregion #region Action Preconditions/Checks (50XXX) + InteractionHasAlreadyBeenAcknowledged = 40060, TagNamesMustBeUnique = 40061, MissingPermissions = 50001, InvalidAccountType = 50002, @@ -145,12 +149,14 @@ namespace Discord InvalidFileUpload = 50046, CannotSelfRedeemGift = 50054, InvalidGuild = 50055, + InvalidMessageType = 50068, PaymentSourceRequiredForGift = 50070, CannotDeleteRequiredCommunityChannel = 50074, InvalidSticker = 50081, CannotExecuteOnArchivedThread = 50083, InvalidThreadNotificationSettings = 50084, BeforeValueEarlierThanThreadCreation = 50085, + CommunityServerChannelsMustBeTextChannels = 50086, ServerLocaleUnavailable = 50095, ServerRequiresMonetization = 50097, ServerRequiresBoosts = 50101, diff --git a/src/Discord.Net.Core/Entities/Channels/Direction.cs b/src/Discord.Net.Core/Entities/Channels/Direction.cs index efdf4ff42..4149617d8 100644 --- a/src/Discord.Net.Core/Entities/Channels/Direction.cs +++ b/src/Discord.Net.Core/Entities/Channels/Direction.cs @@ -1,10 +1,10 @@ namespace Discord { /// - /// 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. /// /// - /// This enum is used to specify the direction for retrieving messages. + /// This enum is used to specify the direction for retrieving entities. /// /// At the time of writing, is not yet implemented into /// . @@ -15,15 +15,15 @@ namespace Discord public enum Direction { /// - /// The message(s) should be retrieved before a message. + /// The entity(s) should be retrieved before an entity. /// Before, /// - /// The message(s) should be retrieved after a message. + /// The entity(s) should be retrieved after an entity. /// After, /// - /// The message(s) should be retrieved around a message. + /// The entity(s) should be retrieved around an entity. /// Around } diff --git a/src/Discord.Net.Core/Entities/Channels/IChannel.cs b/src/Discord.Net.Core/Entities/Channels/IChannel.cs index e2df86f2a..6d58486f8 100644 --- a/src/Discord.Net.Core/Entities/Channels/IChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IChannel.cs @@ -21,7 +21,7 @@ namespace Discord /// /// /// - /// The returned collection is an asynchronous enumerable object; one must call + /// The returned collection is an asynchronous enumerable object; one must call /// to access the individual messages as a /// collection. /// diff --git a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs index 00ec38746..60a7c7575 100644 --- a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs @@ -31,11 +31,12 @@ namespace Discord /// The message components to be included with this message. Used for interactions. /// A collection of stickers to send with the message. /// A array of s to send with this response. Max 10. + /// A message flag to be applied to the sent message, only is permitted. /// /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. /// - Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null); + Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); /// /// Sends a file to this message channel with an optional caption. /// @@ -71,11 +72,12 @@ namespace Discord /// The message components to be included with this message. Used for interactions. /// A collection of stickers to send with the file. /// A array of s to send with this response. Max 10. + /// A message flag to be applied to the sent message, only is permitted. /// /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. /// - Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null); + Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); /// /// Sends a file to this message channel with an optional caption. /// @@ -108,11 +110,12 @@ namespace Discord /// The message components to be included with this message. Used for interactions. /// A collection of stickers to send with the file. /// A array of s to send with this response. Max 10. + /// A message flag to be applied to the sent message, only is permitted. /// /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. /// - Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null); + Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); /// /// Sends a file to this message channel with an optional caption. /// @@ -137,11 +140,12 @@ namespace Discord /// The message components to be included with this message. Used for interactions. /// A collection of stickers to send with the file. /// A array of s to send with this response. Max 10. + /// A message flag to be applied to the sent message, only is permitted. /// /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. /// - Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null); + Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); /// /// Sends a collection of files to this message channel. /// @@ -166,11 +170,12 @@ namespace Discord /// The message components to be included with this message. Used for interactions. /// A collection of stickers to send with the file. /// A array of s to send with this response. Max 10. + /// A message flag to be applied to the sent message, only is permitted. /// /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. /// - Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null); + Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); /// /// Gets a message from this message channel. diff --git a/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs b/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs index 50e46efa6..f03edbbf9 100644 --- a/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs @@ -48,6 +48,23 @@ namespace Discord /// int MessageCount { get; } + /// + /// Gets whether non-moderators can add other non-moderators to a thread. + /// + /// + /// This property is only available on private threads. + /// + bool? IsInvitable { get; } + + /// + /// Gets when the thread was created. + /// + /// + /// This property is only populated for threads created after 2022-01-09, hence the default date of this + /// property will be that date. + /// + new DateTimeOffset CreatedAt { get; } + /// /// Joins the current thread. /// diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildIntegrationProperties.cs b/src/Discord.Net.Core/Entities/Guilds/GuildIntegrationProperties.cs deleted file mode 100644 index 2ca19b50a..000000000 --- a/src/Discord.Net.Core/Entities/Guilds/GuildIntegrationProperties.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Discord -{ - /// - /// Provides properties used to modify an with the specified changes. - /// - public class GuildIntegrationProperties - { - /// - /// Gets or sets the behavior when an integration subscription lapses. - /// - public Optional ExpireBehavior { get; set; } - /// - /// Gets or sets the period (in seconds) where the integration will ignore lapsed subscriptions. - /// - public Optional ExpireGracePeriod { get; set; } - /// - /// Gets or sets whether emoticons should be synced for this integration. - /// - public Optional EnableEmoticons { get; set; } - } -} diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventsProperties.cs b/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventsProperties.cs index a3fd729e5..d3be8b784 100644 --- a/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventsProperties.cs +++ b/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventsProperties.cs @@ -54,5 +54,10 @@ namespace Discord /// Gets or sets the status of the event. /// public Optional Status { get; set; } + + /// + /// Gets or sets the banner image of the event. + /// + public Optional CoverImage { get; set; } } } diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index ae1b2d67d..4706b629e 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -409,17 +409,70 @@ namespace Discord /// A task that represents the asynchronous leave operation. ///
Task LeaveAsync(RequestOptions options = null); - /// - /// Gets a collection of all users banned in this guild. + /// Gets amount of bans from the guild ordered by user ID. /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the individual messages as a + /// collection. + /// + /// + /// 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! + /// + /// + /// The amount of bans to get from the guild. /// The options to be used when sending the request. /// - /// 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. /// - Task> GetBansAsync(RequestOptions options = null); + IAsyncEnumerable> GetBansAsync(int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null); + /// + /// Gets amount of bans from the guild starting at the provided ordered by user ID. + /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the individual messages as a + /// collection. + /// + /// + /// 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! + /// + /// + /// The ID of the user to start to get bans from. + /// The direction of the bans to be gotten. + /// The number of bans to get. + /// The options to be used when sending the request. + /// + /// A paged collection of bans. + /// + IAsyncEnumerable> GetBansAsync(ulong fromUserId, Direction dir, int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null); + /// + /// Gets amount of bans from the guild starting at the provided ordered by user ID. + /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the individual messages as a + /// collection. + /// + /// + /// 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! + /// + /// + /// The user to start to get bans from. + /// The direction of the bans to be gotten. + /// The number of bans to get. + /// The options to be used when sending the request. + /// + /// A paged collection of bans. + /// + IAsyncEnumerable> GetBansAsync(IUser fromUser, Direction dir, int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null); /// /// Gets a ban object for a banned user. /// @@ -718,8 +771,25 @@ namespace Discord /// Task> GetVoiceRegionsAsync(RequestOptions options = null); - Task> GetIntegrationsAsync(RequestOptions options = null); - Task CreateIntegrationAsync(ulong id, string type, RequestOptions options = null); + /// + /// Gets a collection of all the integrations this guild contains. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// integrations the guild can has. + /// + Task> GetIntegrationsAsync(RequestOptions options = null); + + /// + /// Deletes an integration. + /// + /// The id for the integration. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous removal operation. + /// + Task DeleteIntegrationAsync(ulong id, RequestOptions options = null); /// /// Gets a collection of all invites in this guild. @@ -1105,6 +1175,7 @@ namespace Discord /// /// A collection of speakers for the event. /// The location of the event; links are supported + /// The optional banner image for the event. /// The options to be used when sending the request. /// /// A task that represents the asynchronous create operation. @@ -1118,6 +1189,7 @@ namespace Discord DateTimeOffset? endTime = null, ulong? channelId = null, string location = null, + Image? coverImage = null, RequestOptions options = null); /// diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs b/src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs index e50f4cc2b..4b2fa3bee 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs @@ -39,6 +39,11 @@ namespace Discord /// string Description { get; } + /// + /// Gets the banner asset id of the event. + /// + string CoverImageId { get; } + /// /// Gets the start time of the event. /// @@ -80,6 +85,14 @@ namespace Discord /// int? UserCount { get; } + /// + /// Gets this events banner image url. + /// + /// The format to return. + /// The size of the image to return in. This can be any power of two between 16 and 2048. + /// The cover images url. + string GetCoverImageUrl(ImageFormat format = ImageFormat.Auto, ushort size = 1024); + /// /// Starts the event. /// diff --git a/src/Discord.Net.Core/Entities/Guilds/IntegrationAccount.cs b/src/Discord.Net.Core/Entities/Guilds/IntegrationAccount.cs deleted file mode 100644 index 340115fde..000000000 --- a/src/Discord.Net.Core/Entities/Guilds/IntegrationAccount.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Diagnostics; - -namespace Discord -{ - [DebuggerDisplay("{DebuggerDisplay,nq}")] - public struct IntegrationAccount - { - /// Gets the ID of the account. - /// A unique identifier of this integration account. - public string Id { get; } - /// Gets the name of the account. - /// A string containing the name of this integration account. - public string Name { get; private set; } - - public override string ToString() => Name; - private string DebuggerDisplay => $"{Name} ({Id})"; - } -} diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuildIntegration.cs b/src/Discord.Net.Core/Entities/Integrations/IIntegration.cs similarity index 61% rename from src/Discord.Net.Core/Entities/Guilds/IGuildIntegration.cs rename to src/Discord.Net.Core/Entities/Integrations/IIntegration.cs index 6fe3f7b55..304d58792 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuildIntegration.cs +++ b/src/Discord.Net.Core/Entities/Integrations/IIntegration.cs @@ -3,15 +3,16 @@ using System; namespace Discord { /// - /// Holds information for a guild integration feature. + /// Holds information for an integration feature. + /// Nullable fields not provided for Discord bot integrations, but are for Twitch etc. /// - public interface IGuildIntegration + public interface IIntegration { /// /// Gets the integration ID. /// /// - /// An representing the unique identifier value of this integration. + /// A representing the unique identifier value of this integration. /// ulong Id { get; } /// @@ -45,30 +46,52 @@ namespace Discord /// /// true if this integration is syncing; otherwise false. /// - bool IsSyncing { get; } + bool? IsSyncing { get; } /// /// Gets the ID that this integration uses for "subscribers". /// - ulong ExpireBehavior { get; } + ulong? RoleId { get; } + /// + /// Gets whether emoticons should be synced for this integration (twitch only currently). + /// + bool? HasEnabledEmoticons { get; } + /// + /// Gets the behavior of expiring subscribers. + /// + IntegrationExpireBehavior? ExpireBehavior { get; } /// /// Gets the grace period before expiring "subscribers". /// - ulong ExpireGracePeriod { get; } + int? ExpireGracePeriod { get; } + /// + /// Gets the user for this integration. + /// + IUser User { get; } + /// + /// Gets integration account information. + /// + IIntegrationAccount Account { get; } /// /// Gets when this integration was last synced. /// /// /// A containing a date and time of day when the integration was last synced. /// - DateTimeOffset SyncedAt { get; } + DateTimeOffset? SyncedAt { get; } /// - /// Gets integration account information. + /// Gets how many subscribers this integration has. /// - IntegrationAccount Account { get; } - + int? SubscriberCount { get; } + /// + /// Gets whether this integration been revoked. + /// + bool? IsRevoked { get; } + /// + /// Gets the bot/OAuth2 application for a discord integration. + /// + IIntegrationApplication Application { get; } + IGuild Guild { get; } ulong GuildId { get; } - ulong RoleId { get; } - IUser User { get; } } } diff --git a/src/Discord.Net.Core/Entities/Integrations/IIntegrationAccount.cs b/src/Discord.Net.Core/Entities/Integrations/IIntegrationAccount.cs new file mode 100644 index 000000000..322ffa5c2 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Integrations/IIntegrationAccount.cs @@ -0,0 +1,23 @@ +namespace Discord +{ + /// + /// Provides the account information for an . + /// + public interface IIntegrationAccount + { + /// + /// Gets the ID of the account. + /// + /// + /// A unique identifier of this integration account. + /// + string Id { get; } + /// + /// Gets the name of the account. + /// + /// + /// A string containing the name of this integration account. + /// + string Name { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Integrations/IIntegrationApplication.cs b/src/Discord.Net.Core/Entities/Integrations/IIntegrationApplication.cs new file mode 100644 index 000000000..9085ae686 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Integrations/IIntegrationApplication.cs @@ -0,0 +1,33 @@ +namespace Discord +{ + /// + /// Provides the bot/OAuth2 application for an . + /// + public interface IIntegrationApplication + { + /// + /// Gets the id of the app. + /// + ulong Id { get; } + /// + /// Gets the name of the app. + /// + string Name { get; } + /// + /// Gets the icon hash of the app. + /// + string Icon { get; } + /// + /// Gets the description of the app. + /// + string Description { get; } + /// + /// Gets the summary of the app. + /// + string Summary { get; } + /// + /// Gets the bot associated with this application. + /// + IUser Bot { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Integrations/IntegrationExpireBehavior.cs b/src/Discord.Net.Core/Entities/Integrations/IntegrationExpireBehavior.cs new file mode 100644 index 000000000..642e247eb --- /dev/null +++ b/src/Discord.Net.Core/Entities/Integrations/IntegrationExpireBehavior.cs @@ -0,0 +1,17 @@ +namespace Discord +{ + /// + /// The behavior of expiring subscribers for an . + /// + public enum IntegrationExpireBehavior + { + /// + /// Removes a role from an expired subscriber. + /// + RemoveRole = 0, + /// + /// Kicks an expired subscriber from the guild. + /// + Kick = 1 + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs index 0fa8189c1..7becca0e0 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs @@ -613,6 +613,9 @@ namespace Discord if (!(string.IsNullOrEmpty(Url) ^ string.IsNullOrEmpty(CustomId))) throw new InvalidOperationException("A button must contain either a URL or a CustomId, but not both!"); + if (Style == 0) + throw new ArgumentException("A button must have a style.", nameof(Style)); + if (Style == ButtonStyle.Link) { if (string.IsNullOrEmpty(Url)) diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteraction.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteraction.cs index 2a46e8f18..299ee795d 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteraction.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteraction.cs @@ -1,3 +1,6 @@ +using System; +using System.Threading.Tasks; + namespace Discord { /// @@ -6,7 +9,7 @@ namespace Discord public interface IComponentInteraction : IDiscordInteraction { /// - /// Gets the data received with this interaction, contains the button that was clicked. + /// Gets the data received with this component interaction. /// new IComponentInteractionData Data { get; } @@ -14,5 +17,21 @@ namespace Discord /// Gets the message that contained the trigger for this interaction. /// IUserMessage Message { get; } + + /// + /// Updates the message which this component resides in with the type + /// + /// A delegate containing the properties to modify the message with. + /// The options to be used when sending the request. + /// A task that represents the asynchronous operation of updating the message. + Task UpdateAsync(Action func, RequestOptions options = null); + + /// + /// Defers an interaction with the response type 5 (). + /// + /// to defer ephemerally, otherwise . + /// The options to be used when sending the request. + /// A task that represents the asynchronous operation of acknowledging the interaction. + Task DeferLoadingAsync(bool ephemeral = false, RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Messages/FileAttachment.cs b/src/Discord.Net.Core/Entities/Messages/FileAttachment.cs index 35252693b..7470a72cd 100644 --- a/src/Discord.Net.Core/Entities/Messages/FileAttachment.cs +++ b/src/Discord.Net.Core/Entities/Messages/FileAttachment.cs @@ -7,13 +7,29 @@ using System.Threading.Tasks; namespace Discord { + /// + /// Represents an outgoing file attachment used to send a file to discord. + /// public struct FileAttachment : IDisposable { + /// + /// Gets or sets the filename. + /// public string FileName { get; set; } + /// + /// Gets or sets the description of the file. + /// public string Description { get; set; } + + /// + /// Gets or sets whether this file should be marked as a spoiler. + /// public bool IsSpoiler { get; set; } #pragma warning disable IDISP008 + /// + /// Gets the stream containing the file content. + /// public Stream Stream { get; } #pragma warning restore IDISP008 diff --git a/src/Discord.Net.Core/Entities/Messages/IAttachment.cs b/src/Discord.Net.Core/Entities/Messages/IAttachment.cs index e94e9f97c..277c06291 100644 --- a/src/Discord.Net.Core/Entities/Messages/IAttachment.cs +++ b/src/Discord.Net.Core/Entities/Messages/IAttachment.cs @@ -62,5 +62,13 @@ namespace Discord /// if the attachment is ephemeral; otherwise . /// bool Ephemeral { get; } + /// + /// Gets the description of the attachment; or if there is none set. + /// + string Description { get; } + /// + /// Gets the media's MIME type if present; otherwise . + /// + string ContentType { get; } } } diff --git a/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs index 649944ede..4c3125907 100644 --- a/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs @@ -13,7 +13,7 @@ namespace Discord /// Gets a that grants all guild permissions for webhook users. public static readonly GuildPermissions Webhook = new GuildPermissions(0b0_00000_0000000_0000000_0001101100000_000000); /// Gets a that grants all guild permissions. - public static readonly GuildPermissions All = new GuildPermissions(0b1_11111_1111111_1111111_1111111111111_111111); + public static readonly GuildPermissions All = new GuildPermissions(ulong.MaxValue); /// Gets a packed value representing all the permissions in this . public ulong RawValue { get; } diff --git a/src/Discord.Net.Core/Entities/Users/ConnectionVisibility.cs b/src/Discord.Net.Core/Entities/Users/ConnectionVisibility.cs new file mode 100644 index 000000000..ed041c9f9 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Users/ConnectionVisibility.cs @@ -0,0 +1,17 @@ +namespace Discord +{ + /// + /// The visibility of the connected account. + /// + public enum ConnectionVisibility + { + /// + /// Invisible to everyone except the user themselves. + /// + None = 0, + /// + /// Visible to everyone. + /// + Everyone = 1 + } +} diff --git a/src/Discord.Net.Core/Entities/Users/IConnection.cs b/src/Discord.Net.Core/Entities/Users/IConnection.cs index 1e65d971f..94b23a4b5 100644 --- a/src/Discord.Net.Core/Entities/Users/IConnection.cs +++ b/src/Discord.Net.Core/Entities/Users/IConnection.cs @@ -4,24 +4,53 @@ namespace Discord { public interface IConnection { - /// Gets the ID of the connection account. - /// A representing the unique identifier value of this connection. + /// + /// Gets the ID of the connection account. + /// + /// + /// A representing the unique identifier value of this connection. + /// string Id { get; } - /// Gets the service of the connection (twitch, youtube). - /// A string containing the name of this type of connection. - string Type { get; } - /// Gets the username of the connection account. - /// A string containing the name of this connection. + /// + /// Gets the username of the connection account. + /// + /// + /// A string containing the name of this connection. + /// string Name { get; } - /// Gets whether the connection is revoked. - /// A value which if true indicates that this connection has been revoked, otherwise false. - bool IsRevoked { get; } - - /// Gets a of integration IDs. + /// + /// Gets the service of the connection (twitch, youtube). + /// + /// + /// A string containing the name of this type of connection. + /// + string Type { get; } + /// + /// Gets whether the connection is revoked. + /// /// - /// An containing - /// representations of unique identifier values of integrations. + /// A value which if true indicates that this connection has been revoked, otherwise false. /// - IReadOnlyCollection IntegrationIds { get; } + bool? IsRevoked { get; } + /// + /// Gets a of integration parials. + /// + IReadOnlyCollection Integrations { get; } + /// + /// Gets whether the connection is verified. + /// + bool Verified { get; } + /// + /// Gets whether friend sync is enabled for this connection. + /// + bool FriendSync { get; } + /// + /// Gets whether activities related to this connection will be shown in presence updates. + /// + bool ShowActivity { get; } + /// + /// Visibility of this connection. + /// + ConnectionVisibility Visibility { get; } } } diff --git a/src/Discord.Net.Core/Format.cs b/src/Discord.Net.Core/Format.cs index a5951aa73..dc2a06540 100644 --- a/src/Discord.Net.Core/Format.cs +++ b/src/Discord.Net.Core/Format.cs @@ -107,13 +107,16 @@ namespace Discord } /// - /// Formats a user's username + discriminator while maintaining bidirectional unicode + /// Formats a user's username + discriminator. /// + /// To format the string in bidirectional unicode or not /// The user whos username and discriminator to format /// The username + discriminator - 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}"; } } } diff --git a/src/Discord.Net.Interactions/Attributes/ComplexParameterAttribute.cs b/src/Discord.Net.Interactions/Attributes/ComplexParameterAttribute.cs new file mode 100644 index 000000000..952ca06a4 --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/ComplexParameterAttribute.cs @@ -0,0 +1,30 @@ +using System; + +namespace Discord.Interactions +{ + /// + /// Registers a parameter as a complex parameter. + /// + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] + public class ComplexParameterAttribute : Attribute + { + /// + /// Gets the parameter array of the constructor method that should be prioritized. + /// + public Type[] PrioritizedCtorSignature { get; } + + /// + /// Registers a slash command parameter as a complex parameter. + /// + public ComplexParameterAttribute() { } + + /// + /// Registers a slash command parameter as a complex parameter with a specified constructor signature. + /// + /// Type array of the preferred constructor parameters. + public ComplexParameterAttribute(Type[] types) + { + PrioritizedCtorSignature = types; + } + } +} diff --git a/src/Discord.Net.Interactions/Attributes/ComplexParameterCtorAttribute.cs b/src/Discord.Net.Interactions/Attributes/ComplexParameterCtorAttribute.cs new file mode 100644 index 000000000..59ee3377b --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/ComplexParameterCtorAttribute.cs @@ -0,0 +1,10 @@ +using System; + +namespace Discord.Interactions +{ + /// + /// Tag a type constructor as the preferred Complex command constructor. + /// + [AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = true)] + public class ComplexParameterCtorAttribute : Attribute { } +} diff --git a/src/Discord.Net.Interactions/Attributes/EnumChoiceAttribute.cs b/src/Discord.Net.Interactions/Attributes/EnumChoiceAttribute.cs new file mode 100644 index 000000000..c7f83b6cd --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/EnumChoiceAttribute.cs @@ -0,0 +1,25 @@ +using System; + +namespace Discord.Interactions +{ + /// + /// Customize the displayed value of a slash command choice enum. Only works with the default enum type converter. + /// + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)] + public class ChoiceDisplayAttribute : Attribute + { + /// + /// Gets the name of the parameter. + /// + public string Name { get; } = null; + + /// + /// Modify the default name and description values of a Slash Command parameter. + /// + /// Name of the parameter. + public ChoiceDisplayAttribute(string name) + { + Name = name; + } + } +} diff --git a/src/Discord.Net.Interactions/Builders/Commands/ComponentCommandBuilder.cs b/src/Discord.Net.Interactions/Builders/Commands/ComponentCommandBuilder.cs index e42dfabce..dd857498c 100644 --- a/src/Discord.Net.Interactions/Builders/Commands/ComponentCommandBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Commands/ComponentCommandBuilder.cs @@ -5,7 +5,7 @@ namespace Discord.Interactions.Builders /// /// Represents a builder for creating . /// - public sealed class ComponentCommandBuilder : CommandBuilder + public sealed class ComponentCommandBuilder : CommandBuilder { protected override ComponentCommandBuilder Instance => this; @@ -26,9 +26,9 @@ namespace Discord.Interactions.Builders /// /// The builder instance. /// - public override ComponentCommandBuilder AddParameter (Action configure) + public override ComponentCommandBuilder AddParameter (Action configure) { - var parameter = new CommandParameterBuilder(this); + var parameter = new ComponentCommandParameterBuilder(this); configure(parameter); AddParameters(parameter); return this; diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs index 37cd861c4..ad2f07c73 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs @@ -38,6 +38,11 @@ namespace Discord.Interactions.Builders /// Type Type { get; } + /// + /// Get the assigned to this input. + /// + ComponentTypeConverter TypeConverter { get; } + /// /// Gets the default value of this input component. /// diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs index c2b9b0645..7d1d96712 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs @@ -33,6 +33,9 @@ namespace Discord.Interactions.Builders /// public Type Type { get; private set; } + /// + public ComponentTypeConverter TypeConverter { get; private set; } + /// public object DefaultValue { get; set; } @@ -111,6 +114,7 @@ namespace Discord.Interactions.Builders public TBuilder WithType(Type type) { Type = type; + TypeConverter = Modal._interactionService.GetComponentTypeConverter(type); return Instance; } diff --git a/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs index e120e78be..fc1dbdc0e 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs @@ -9,6 +9,7 @@ namespace Discord.Interactions.Builders /// public class ModalBuilder { + internal readonly InteractionService _interactionService; internal readonly List _components; /// @@ -31,11 +32,12 @@ namespace Discord.Interactions.Builders /// public IReadOnlyCollection Components => _components; - internal ModalBuilder(Type type) + internal ModalBuilder(Type type, InteractionService interactionService) { if (!typeof(IModal).IsAssignableFrom(type)) throw new ArgumentException($"Must be an implementation of {nameof(IModal)}", nameof(type)); + _interactionService = interactionService; _components = new(); } @@ -43,7 +45,7 @@ namespace Discord.Interactions.Builders /// Initializes a new /// /// The initialization delegate for this modal. - public ModalBuilder(Type type, ModalInitializer modalInitializer) : this(type) + public ModalBuilder(Type type, ModalInitializer modalInitializer, InteractionService interactionService) : this(type, interactionService) { ModalInitializer = modalInitializer; } diff --git a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs index 6615f131c..b2317d1f3 100644 --- a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs @@ -231,9 +231,6 @@ namespace Discord.Interactions.Builders private static void BuildComponentCommand (ComponentCommandBuilder builder, Func createInstance, MethodInfo methodInfo, InteractionService commandService, IServiceProvider services) { - if (!methodInfo.GetParameters().All(x => x.ParameterType == typeof(string) || x.ParameterType == typeof(string[]))) - throw new InvalidOperationException($"Interaction method parameters all must be types of {typeof(string).Name} or {typeof(string[]).Name}"); - var attributes = methodInfo.GetCustomAttributes(); builder.MethodName = methodInfo.Name; @@ -260,8 +257,10 @@ namespace Discord.Interactions.Builders var parameters = methodInfo.GetParameters(); + var wildCardCount = Regex.Matches(Regex.Escape(builder.Name), Regex.Escape(commandService._wildCardExp)).Count; + foreach (var parameter in parameters) - builder.AddParameter(x => BuildParameter(x, parameter)); + builder.AddParameter(x => BuildComponentParameter(x, parameter, parameter.Position >= wildCardCount)); builder.Callback = CreateCallback(createInstance, methodInfo, commandService); } @@ -310,8 +309,8 @@ namespace Discord.Interactions.Builders if (parameters.Count(x => typeof(IModal).IsAssignableFrom(x.ParameterType)) > 1) throw new InvalidOperationException($"A modal command can only have one {nameof(IModal)} parameter."); - if (!parameters.All(x => x.ParameterType == typeof(string) || typeof(IModal).IsAssignableFrom(x.ParameterType))) - throw new InvalidOperationException($"All parameters of a modal command must be either a string or an implementation of {nameof(IModal)}"); + if (!typeof(IModal).IsAssignableFrom(parameters.Last().ParameterType)) + throw new InvalidOperationException($"Last parameter of a modal command must be an implementation of {nameof(IModal)}"); var attributes = methodInfo.GetCustomAttributes(); @@ -397,7 +396,6 @@ namespace Discord.Interactions.Builders builder.Description = paramInfo.Name; builder.IsRequired = !paramInfo.IsOptional; builder.DefaultValue = paramInfo.DefaultValue; - builder.SetParameterType(paramType, services); foreach (var attribute in attributes) { @@ -435,16 +433,42 @@ namespace Discord.Interactions.Builders case MinValueAttribute minValue: builder.MinValue = minValue.Value; break; + case ComplexParameterAttribute complexParameter: + { + builder.IsComplexParameter = true; + ConstructorInfo ctor = GetComplexParameterConstructor(paramInfo.ParameterType.GetTypeInfo(), complexParameter); + + foreach (var parameter in ctor.GetParameters()) + { + if (parameter.IsDefined(typeof(ComplexParameterAttribute))) + throw new InvalidOperationException("You cannot create nested complex parameters."); + + builder.AddComplexParameterField(fieldBuilder => BuildSlashParameter(fieldBuilder, parameter, services)); + } + + var initializer = builder.Command.Module.InteractionService._useCompiledLambda ? + ReflectionUtils.CreateLambdaConstructorInvoker(paramInfo.ParameterType.GetTypeInfo()) : ctor.Invoke; + builder.ComplexParameterInitializer = args => initializer(args); + } + break; default: builder.AddAttributes(attribute); break; } } + builder.SetParameterType(paramType, services); + // Replace pascal casings with '-' builder.Name = Regex.Replace(builder.Name, "(?<=[a-z])(?=[A-Z])", "-").ToLower(); } + private static void BuildComponentParameter(ComponentCommandParameterBuilder builder, ParameterInfo paramInfo, bool isComponentParam) + { + builder.SetIsRouteSegment(!isComponentParam); + BuildParameter(builder, paramInfo); + } + private static void BuildParameter (ParameterBuilder builder, ParameterInfo paramInfo) where TInfo : class, IParameterInfo where TBuilder : ParameterBuilder @@ -476,7 +500,7 @@ namespace Discord.Interactions.Builders #endregion #region Modals - public static ModalInfo BuildModalInfo(Type modalType) + public static ModalInfo BuildModalInfo(Type modalType, InteractionService interactionService) { if (!typeof(IModal).IsAssignableFrom(modalType)) throw new InvalidOperationException($"{modalType.FullName} isn't an implementation of {typeof(IModal).FullName}"); @@ -485,7 +509,7 @@ namespace Discord.Interactions.Builders try { - var builder = new ModalBuilder(modalType) + var builder = new ModalBuilder(modalType, interactionService) { Title = instance.Title }; @@ -608,5 +632,41 @@ namespace Discord.Interactions.Builders propertyInfo.SetMethod?.IsStatic == false && propertyInfo.IsDefined(typeof(ModalInputAttribute)); } + + private static ConstructorInfo GetComplexParameterConstructor(TypeInfo typeInfo, ComplexParameterAttribute complexParameter) + { + var ctors = typeInfo.GetConstructors(); + + if (ctors.Length == 0) + throw new InvalidOperationException($"No constructor found for \"{typeInfo.FullName}\"."); + + if (complexParameter.PrioritizedCtorSignature is not null) + { + var ctor = typeInfo.GetConstructor(complexParameter.PrioritizedCtorSignature); + + if (ctor is null) + throw new InvalidOperationException($"No constructor was found with the signature: {string.Join(",", complexParameter.PrioritizedCtorSignature.Select(x => x.Name))}"); + + return ctor; + } + + var prioritizedCtors = ctors.Where(x => x.IsDefined(typeof(ComplexParameterCtorAttribute), true)); + + switch (prioritizedCtors.Count()) + { + case > 1: + throw new InvalidOperationException($"{nameof(ComplexParameterCtorAttribute)} can only be used once in a type."); + case 1: + return prioritizedCtors.First(); + } + + switch (ctors.Length) + { + case > 1: + throw new InvalidOperationException($"Multiple constructors found for \"{typeInfo.FullName}\"."); + default: + return ctors.First(); + } + } } } diff --git a/src/Discord.Net.Interactions/Builders/Parameters/ComponentCommandParameterBuilder.cs b/src/Discord.Net.Interactions/Builders/Parameters/ComponentCommandParameterBuilder.cs new file mode 100644 index 000000000..d9f1463c3 --- /dev/null +++ b/src/Discord.Net.Interactions/Builders/Parameters/ComponentCommandParameterBuilder.cs @@ -0,0 +1,77 @@ +using System; + +namespace Discord.Interactions.Builders +{ + /// + /// Represents a builder for creating . + /// + public class ComponentCommandParameterBuilder : ParameterBuilder + { + /// + /// Get the assigned to this parameter, if is . + /// + public ComponentTypeConverter TypeConverter { get; private set; } + + /// + /// Get the assigned to this parameter, if is . + /// + public TypeReader TypeReader { get; private set; } + + /// + /// Gets whether this parameter is a CustomId segment or a Component value parameter. + /// + public bool IsRouteSegmentParameter { get; private set; } + + /// + protected override ComponentCommandParameterBuilder Instance => this; + + internal ComponentCommandParameterBuilder(ICommandBuilder command) : base(command) { } + + /// + /// Initializes a new . + /// + /// Parent command of this parameter. + /// Name of this command. + /// Type of this parameter. + public ComponentCommandParameterBuilder(ICommandBuilder command, string name, Type type) : base(command, name, type) { } + + /// + public override ComponentCommandParameterBuilder SetParameterType(Type type) => SetParameterType(type, null); + + /// + /// Sets . + /// + /// New value of the . + /// Service container to be used to resolve the dependencies of this parameters . + /// + /// The builder instance. + /// + public ComponentCommandParameterBuilder SetParameterType(Type type, IServiceProvider services) + { + base.SetParameterType(type); + + if (IsRouteSegmentParameter) + TypeReader = Command.Module.InteractionService.GetTypeReader(type); + else + TypeConverter = Command.Module.InteractionService.GetComponentTypeConverter(ParameterType, services); + + return this; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public ComponentCommandParameterBuilder SetIsRouteSegment(bool isRouteSegment) + { + IsRouteSegmentParameter = isRouteSegment; + return this; + } + + internal override ComponentCommandParameterInfo Build(ICommandInfo command) + => new(this, command); + } +} diff --git a/src/Discord.Net.Interactions/Builders/Parameters/ModalCommandParameterBuilder.cs b/src/Discord.Net.Interactions/Builders/Parameters/ModalCommandParameterBuilder.cs index a0315e1ea..8cb9b3ab9 100644 --- a/src/Discord.Net.Interactions/Builders/Parameters/ModalCommandParameterBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Parameters/ModalCommandParameterBuilder.cs @@ -20,6 +20,11 @@ namespace Discord.Interactions.Builders /// public bool IsModalParameter => Modal is not null; + /// + /// Gets the assigned to this parameter, if is . + /// + public TypeReader TypeReader { get; private set; } + internal ModalCommandParameterBuilder(ICommandBuilder command) : base(command) { } /// @@ -34,7 +39,9 @@ namespace Discord.Interactions.Builders public override ModalCommandParameterBuilder SetParameterType(Type type) { if (typeof(IModal).IsAssignableFrom(type)) - Modal = ModalUtils.GetOrAdd(type); + Modal = ModalUtils.GetOrAdd(type, Command.Module.InteractionService); + else + TypeReader = Command.Module.InteractionService.GetTypeReader(type); return base.SetParameterType(type); } diff --git a/src/Discord.Net.Interactions/Builders/Parameters/SlashCommandParameterBuilder.cs b/src/Discord.Net.Interactions/Builders/Parameters/SlashCommandParameterBuilder.cs index c208a4b0e..d600c9cc7 100644 --- a/src/Discord.Net.Interactions/Builders/Parameters/SlashCommandParameterBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Parameters/SlashCommandParameterBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; namespace Discord.Interactions.Builders { @@ -10,6 +11,7 @@ namespace Discord.Interactions.Builders { private readonly List _choices = new(); private readonly List _channelTypes = new(); + private readonly List _complexParameterFields = new(); /// /// Gets or sets the description of this parameter. @@ -36,6 +38,11 @@ namespace Discord.Interactions.Builders /// public IReadOnlyCollection ChannelTypes => _channelTypes; + /// + /// Gets the constructor parameters of this parameter, if is . + /// + public IReadOnlyCollection ComplexParameterFields => _complexParameterFields; + /// /// Gets or sets whether this parameter should be configured for Autocomplete Interactions. /// @@ -46,6 +53,16 @@ namespace Discord.Interactions.Builders /// public TypeConverter TypeConverter { get; private set; } + /// + /// Gets whether this type should be treated as a complex parameter. + /// + public bool IsComplexParameter { get; internal set; } + + /// + /// Gets the initializer delegate for this parameter, if is . + /// + public ComplexParameterInitializer ComplexParameterInitializer { get; internal set; } + /// /// Gets or sets the of this parameter. /// @@ -60,7 +77,14 @@ namespace Discord.Interactions.Builders /// Parent command of this parameter. /// Name of this command. /// Type of this parameter. - public SlashCommandParameterBuilder(ICommandBuilder command, string name, Type type) : base(command, name, type) { } + public SlashCommandParameterBuilder(ICommandBuilder command, string name, Type type, ComplexParameterInitializer complexParameterInitializer = null) + : base(command, name, type) + { + ComplexParameterInitializer = complexParameterInitializer; + + if (complexParameterInitializer is not null) + IsComplexParameter = true; + } /// /// Sets . @@ -168,7 +192,47 @@ namespace Discord.Interactions.Builders public SlashCommandParameterBuilder SetParameterType(Type type, IServiceProvider services = null) { base.SetParameterType(type); - TypeConverter = Command.Module.InteractionService.GetTypeConverter(ParameterType, services); + + if(!IsComplexParameter) + TypeConverter = Command.Module.InteractionService.GetTypeConverter(ParameterType, services); + + return this; + } + + /// + /// Adds a parameter builders to . + /// + /// factory. + /// + /// The builder instance. + /// + /// Thrown if the added field has a . + public SlashCommandParameterBuilder AddComplexParameterField(Action configure) + { + SlashCommandParameterBuilder builder = new(Command); + configure(builder); + + if(builder.IsComplexParameter) + throw new InvalidOperationException("You cannot create nested complex parameters."); + + _complexParameterFields.Add(builder); + return this; + } + + /// + /// Adds parameter builders to . + /// + /// New parameter builders to be added to . + /// + /// The builder instance. + /// + /// Thrown if the added field has a . + public SlashCommandParameterBuilder AddComplexParameterFields(params SlashCommandParameterBuilder[] fields) + { + if(fields.Any(x => x.IsComplexParameter)) + throw new InvalidOperationException("You cannot create nested complex parameters."); + + _complexParameterFields.AddRange(fields); return this; } diff --git a/src/Discord.Net.Interactions/Entities/ITypeConverter.cs b/src/Discord.Net.Interactions/Entities/ITypeConverter.cs new file mode 100644 index 000000000..c692b29cb --- /dev/null +++ b/src/Discord.Net.Interactions/Entities/ITypeConverter.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + internal interface ITypeConverter + { + public bool CanConvertTo(Type type); + + public Task ReadAsync(IInteractionContext context, T option, IServiceProvider services); + } +} diff --git a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs index 5c379cf42..8f0987661 100644 --- a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs +++ b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs @@ -10,9 +10,10 @@ namespace Discord.Interactions /// /// Type of the implementation. /// The interaction to respond to. + /// Delegate that can be used to modify the modal. /// The request options for this request. /// A task that represents the asynchronous operation of responding to the interaction. - public static async Task RespondWithModalAsync(this IDiscordInteraction interaction, string customId, RequestOptions options = null) + public static async Task RespondWithModalAsync(this IDiscordInteraction interaction, string customId, RequestOptions options = null, Action modifyModal = null) where T : class, IModal { if (!ModalUtils.TryGet(out var modalInfo)) @@ -31,6 +32,9 @@ namespace Discord.Interactions throw new InvalidOperationException($"{input.GetType().FullName} isn't a valid component info class"); } + if (modifyModal is not null) + modifyModal(builder); + await interaction.RespondWithModalAsync(builder.Build(), options).ConfigureAwait(false); } } diff --git a/src/Discord.Net.Interactions/Extensions/RestExtensions.cs b/src/Discord.Net.Interactions/Extensions/RestExtensions.cs new file mode 100644 index 000000000..2641617e0 --- /dev/null +++ b/src/Discord.Net.Interactions/Extensions/RestExtensions.cs @@ -0,0 +1,25 @@ +using Discord.Interactions; +using System; + +namespace Discord.Rest +{ + public static class RestExtensions + { + /// + /// Respond to an interaction with a . + /// + /// Type of the implementation. + /// The interaction to respond to. + /// The request options for this request. + /// Serialized payload to be used to create a HTTP response. + public static string RespondWithModal(this RestInteraction interaction, string customId, RequestOptions options = null, Action modifyModal = null) + where T : class, IModal + { + if (!ModalUtils.TryGet(out var modalInfo)) + throw new ArgumentException($"{typeof(T).FullName} isn't referenced by any registered Modal Interaction Command and doesn't have a cached {typeof(ModalInfo)}"); + + var modal = modalInfo.ToModal(customId, modifyModal); + return interaction.RespondWithModal(modal, options); + } + } +} diff --git a/src/Discord.Net.Interactions/Info/Commands/AutocompleteCommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/AutocompleteCommandInfo.cs index 712b058a3..9e30c55f4 100644 --- a/src/Discord.Net.Interactions/Info/Commands/AutocompleteCommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/Commands/AutocompleteCommandInfo.cs @@ -41,14 +41,7 @@ namespace Discord.Interactions if (context.Interaction is not IAutocompleteInteraction) return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Autocomplete Interaction"); - try - { - return await RunAsync(context, Array.Empty(), services).ConfigureAwait(false); - } - catch (Exception ex) - { - return ExecuteResult.FromError(ex); - } + return await RunAsync(context, Array.Empty(), services).ConfigureAwait(false); } /// diff --git a/src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs index cf1a2dfa1..ea5ded11c 100644 --- a/src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs @@ -31,6 +31,8 @@ namespace Discord.Interactions private readonly ExecuteCallback _action; private readonly ILookup _groupedPreconditions; + internal IReadOnlyDictionary _parameterDictionary { get; } + /// public ModuleInfo Module { get; } @@ -79,6 +81,7 @@ namespace Discord.Interactions _action = builder.Callback; _groupedPreconditions = builder.Preconditions.ToLookup(x => x.Group, x => x, StringComparer.Ordinal); + _parameterDictionary = Parameters?.ToDictionary(x => x.Name, x => x).ToImmutableDictionary(); } /// @@ -120,10 +123,7 @@ namespace Discord.Interactions return moduleResult; var commandResult = await CheckGroups(_groupedPreconditions, "Command").ConfigureAwait(false); - if (!commandResult.IsSuccess) - return commandResult; - - return PreconditionResult.FromSuccess(); + return !commandResult.IsSuccess ? commandResult : PreconditionResult.FromSuccess(); } protected async Task RunAsync(IInteractionContext context, object[] args, IServiceProvider services) @@ -137,8 +137,8 @@ namespace Discord.Interactions using var scope = services?.CreateScope(); return await ExecuteInternalAsync(context, args, scope?.ServiceProvider ?? EmptyServiceProvider.Instance).ConfigureAwait(false); } - else - return await ExecuteInternalAsync(context, args, services).ConfigureAwait(false); + + return await ExecuteInternalAsync(context, args, services).ConfigureAwait(false); } case RunMode.Async: _ = Task.Run(async () => @@ -167,20 +167,14 @@ namespace Discord.Interactions { var preconditionResult = await CheckPreconditionsAsync(context, services).ConfigureAwait(false); if (!preconditionResult.IsSuccess) - { - await InvokeModuleEvent(context, preconditionResult).ConfigureAwait(false); - return preconditionResult; - } + return await InvokeEventAndReturn(context, preconditionResult).ConfigureAwait(false); var index = 0; foreach (var parameter in Parameters) { var result = await parameter.CheckPreconditionsAsync(context, args[index++], services).ConfigureAwait(false); if (!result.IsSuccess) - { - await InvokeModuleEvent(context, result).ConfigureAwait(false); - return result; - } + return await InvokeEventAndReturn(context, result).ConfigureAwait(false); } var task = _action(context, args, services, this); @@ -189,20 +183,16 @@ namespace Discord.Interactions { var result = await resultTask.ConfigureAwait(false); await InvokeModuleEvent(context, result).ConfigureAwait(false); - if (result is RuntimeResult || result is ExecuteResult) + if (result is RuntimeResult or ExecuteResult) return result; } else { await task.ConfigureAwait(false); - var result = ExecuteResult.FromSuccess(); - await InvokeModuleEvent(context, result).ConfigureAwait(false); - return result; + return await InvokeEventAndReturn(context, ExecuteResult.FromSuccess()).ConfigureAwait(false); } - var failResult = ExecuteResult.FromError(InteractionCommandError.Unsuccessful, "Command execution failed for an unknown reason"); - await InvokeModuleEvent(context, failResult).ConfigureAwait(false); - return failResult; + return await InvokeEventAndReturn(context, ExecuteResult.FromError(InteractionCommandError.Unsuccessful, "Command execution failed for an unknown reason")).ConfigureAwait(false); } catch (Exception ex) { @@ -231,6 +221,12 @@ namespace Discord.Interactions } } + protected async ValueTask InvokeEventAndReturn(IInteractionContext context, IResult result) + { + await InvokeModuleEvent(context, result).ConfigureAwait(false); + return result; + } + private static bool CheckTopLevel(ModuleInfo parent) { var currentParent = parent; diff --git a/src/Discord.Net.Interactions/Info/Commands/ComponentCommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/ComponentCommandInfo.cs index 0e43af3a8..22d6aba6c 100644 --- a/src/Discord.Net.Interactions/Info/Commands/ComponentCommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/Commands/ComponentCommandInfo.cs @@ -1,5 +1,4 @@ using Discord.Interactions.Builders; -using Discord.WebSocket; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -11,10 +10,10 @@ namespace Discord.Interactions /// /// Represents the info class of an attribute based method for handling Component Interaction events. /// - public class ComponentCommandInfo : CommandInfo + public class ComponentCommandInfo : CommandInfo { /// - public override IReadOnlyCollection Parameters { get; } + public override IReadOnlyCollection Parameters { get; } /// public override bool SupportsWildCards => true; @@ -42,80 +41,46 @@ namespace Discord.Interactions if (context.Interaction is not IComponentInteraction componentInteraction) return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Message Component Interaction"); - var args = new List(); - - if (additionalArgs is not null) - args.AddRange(additionalArgs); - - if (componentInteraction.Data?.Values is not null) - args.AddRange(componentInteraction.Data.Values); - - return await ExecuteAsync(context, Parameters, args, services); + return await ExecuteAsync(context, Parameters, additionalArgs, componentInteraction.Data, services); } /// - public async Task ExecuteAsync(IInteractionContext context, IEnumerable paramList, IEnumerable values, + public async Task ExecuteAsync(IInteractionContext context, IEnumerable paramList, IEnumerable wildcardCaptures, IComponentInteractionData data, IServiceProvider services) { + var paramCount = paramList.Count(); + var captureCount = wildcardCaptures?.Count() ?? 0; + if (context.Interaction is not IComponentInteraction messageComponent) return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Component Command Interaction"); try { - var strCount = Parameters.Count(x => x.ParameterType == typeof(string)); + var args = new object[paramCount]; + + for (var i = 0; i < paramCount; i++) + { + var parameter = Parameters.ElementAt(i); + var isCapture = i < captureCount; - if (strCount > values?.Count()) - return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command was invoked with too few parameters"); + if (isCapture ^ parameter.IsRouteSegmentParameter) + return await InvokeEventAndReturn(context, ExecuteResult.FromError(InteractionCommandError.BadArgs, "Argument type and parameter type didn't match (Wild Card capture/Component value)")).ConfigureAwait(false); - var componentValues = messageComponent.Data?.Values; + var readResult = isCapture ? await parameter.TypeReader.ReadAsync(context, wildcardCaptures.ElementAt(i), services).ConfigureAwait(false) : + await parameter.TypeConverter.ReadAsync(context, data, services).ConfigureAwait(false); - var args = new object[Parameters.Count]; + if (!readResult.IsSuccess) + return await InvokeEventAndReturn(context, readResult).ConfigureAwait(false); - if (componentValues is not null) - { - if (Parameters.Last().ParameterType == typeof(string[])) - args[args.Length - 1] = componentValues.ToArray(); - else - return ExecuteResult.FromError(InteractionCommandError.BadArgs, $"Select Menu Interaction handlers must accept a {typeof(string[]).FullName} as its last parameter"); + args[i] = readResult.Value; } - for (var i = 0; i < strCount; i++) - args[i] = values.ElementAt(i); - return await RunAsync(context, args, services).ConfigureAwait(false); } catch (Exception ex) { - return ExecuteResult.FromError(ex); - } - } - - private static object[] GenerateArgs(IEnumerable paramList, IEnumerable argList) - { - var result = new object[paramList.Count()]; - - for (var i = 0; i < paramList.Count(); i++) - { - var parameter = paramList.ElementAt(i); - - if (argList?.ElementAt(i) == null) - { - if (!parameter.IsRequired) - result[i] = parameter.DefaultValue; - else - throw new InvalidOperationException($"Component Interaction handler is executed with too few args."); - } - else if (parameter.IsParameterArray) - { - string[] paramArray = new string[argList.Count() - i]; - argList.ToArray().CopyTo(paramArray, i); - result[i] = paramArray; - } - else - result[i] = argList?.ElementAt(i); + return await InvokeEventAndReturn(context, ExecuteResult.FromError(ex)).ConfigureAwait(false); } - - return result; } protected override Task InvokeModuleEvent(IInteractionContext context, IResult result) diff --git a/src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs index a750603fc..4866bd1da 100644 --- a/src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics.Tracing; using System.Linq; using System.Threading.Tasks; namespace Discord.Interactions @@ -47,21 +48,38 @@ namespace Discord.Interactions try { - var args = new List(); + var args = new object[Parameters.Count]; + var captureCount = additionalArgs?.Length ?? 0; - if (additionalArgs is not null) - args.AddRange(additionalArgs); + for(var i = 0; i < Parameters.Count; i++) + { + var parameter = Parameters.ElementAt(i); - var modal = Modal.CreateModal(modalInteraction, Module.CommandService._exitOnMissingModalField); - args.Add(modal); + if(i < captureCount) + { + var readResult = await parameter.TypeReader.ReadAsync(context, additionalArgs[i], services).ConfigureAwait(false); + if (!readResult.IsSuccess) + return await InvokeEventAndReturn(context, readResult).ConfigureAwait(false); - return await RunAsync(context, args.ToArray(), services); + args[i] = readResult.Value; + } + else + { + var modalResult = await Modal.CreateModalAsync(context, services, Module.CommandService._exitOnMissingModalField).ConfigureAwait(false); + if (!modalResult.IsSuccess) + return await InvokeEventAndReturn(context, modalResult).ConfigureAwait(false); + + if (modalResult is not ParseResult parseResult) + return await InvokeEventAndReturn(context, ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command parameter parsing failed for an unknown reason.")); + + args[i] = parseResult.Value; + } + } + return await RunAsync(context, args, services); } catch (Exception ex) { - var result = ExecuteResult.FromError(ex); - await InvokeModuleEvent(context, result).ConfigureAwait(false); - return result; + return await InvokeEventAndReturn(context, ExecuteResult.FromError(ex)).ConfigureAwait(false); } } diff --git a/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs index 116a07ab4..a123ac183 100644 --- a/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs @@ -13,6 +13,8 @@ namespace Discord.Interactions /// public class SlashCommandInfo : CommandInfo, IApplicationCommandInfo { + internal IReadOnlyDictionary _flattenedParameterDictionary { get; } + /// /// Gets the command description that will be displayed on Discord. /// @@ -30,11 +32,23 @@ namespace Discord.Interactions /// public override bool SupportsWildCards => false; + /// + /// Gets the flattened collection of command parameters and complex parameter fields. + /// + public IReadOnlyCollection FlattenedParameters { get; } + internal SlashCommandInfo (Builders.SlashCommandBuilder builder, ModuleInfo module, InteractionService commandService) : base(builder, module, commandService) { Description = builder.Description; DefaultPermission = builder.DefaultPermission; Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray(); + FlattenedParameters = FlattenParameters(Parameters).ToImmutableArray(); + + for (var i = 0; i < FlattenedParameters.Count - 1; i++) + if (!FlattenedParameters.ElementAt(i).IsRequired && FlattenedParameters.ElementAt(i + 1).IsRequired) + throw new InvalidOperationException("Optional parameters must appear after all required parameters, ComplexParameters with optional parameters must be located at the end."); + + _flattenedParameterDictionary = FlattenedParameters?.ToDictionary(x => x.Name, x => x).ToImmutableDictionary(); } /// @@ -56,46 +70,65 @@ namespace Discord.Interactions { try { - if (paramList?.Count() < argList?.Count()) - return ExecuteResult.FromError(InteractionCommandError.BadArgs ,"Command was invoked with too many parameters"); + var slashCommandParameterInfos = paramList.ToList(); + var args = new object[slashCommandParameterInfos.Count]; - var args = new object[paramList.Count()]; - - for (var i = 0; i < paramList.Count(); i++) + for (var i = 0; i < slashCommandParameterInfos.Count; i++) { - var parameter = paramList.ElementAt(i); - - var arg = argList?.Find(x => string.Equals(x.Name, parameter.Name, StringComparison.OrdinalIgnoreCase)); - - if (arg == default) - { - if (parameter.IsRequired) - return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command was invoked with too few parameters"); - else - args[i] = parameter.DefaultValue; - } - else - { - var typeConverter = parameter.TypeConverter; - - var readResult = await typeConverter.ReadAsync(context, arg, services).ConfigureAwait(false); - - if (!readResult.IsSuccess) - { - await InvokeModuleEvent(context, readResult).ConfigureAwait(false); - return readResult; - } - - args[i] = readResult.Value; - } - } + var parameter = slashCommandParameterInfos[i]; + var result = await ParseArgument(parameter, context, argList, services).ConfigureAwait(false); + if (!result.IsSuccess) + return await InvokeEventAndReturn(context, result).ConfigureAwait(false); + + if (result is not ParseResult parseResult) + return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command parameter parsing failed for an unknown reason."); + + args[i] = parseResult.Value; + } return await RunAsync(context, args, services).ConfigureAwait(false); } - catch (Exception ex) + catch(Exception ex) + { + return await InvokeEventAndReturn(context, ExecuteResult.FromError(ex)).ConfigureAwait(false); + } + } + + private async Task ParseArgument(SlashCommandParameterInfo parameterInfo, IInteractionContext context, List argList, + IServiceProvider services) + { + if (parameterInfo.IsComplexParameter) { - return ExecuteResult.FromError(ex); + var ctorArgs = new object[parameterInfo.ComplexParameterFields.Count]; + + for (var i = 0; i < ctorArgs.Length; i++) + { + var result = await ParseArgument(parameterInfo.ComplexParameterFields.ElementAt(i), context, argList, services).ConfigureAwait(false); + + if (!result.IsSuccess) + return result; + + if (result is not ParseResult parseResult) + return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Complex command parsing failed for an unknown reason."); + + ctorArgs[i] = parseResult.Value; + } + + return ParseResult.FromSuccess(parameterInfo._complexParameterInitializer(ctorArgs)); } + + var arg = argList?.Find(x => string.Equals(x.Name, parameterInfo.Name, StringComparison.OrdinalIgnoreCase)); + + if (arg == default) + return parameterInfo.IsRequired ? ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command was invoked with too few parameters") : + ParseResult.FromSuccess(parameterInfo.DefaultValue); + + var typeConverter = parameterInfo.TypeConverter; + var readResult = await typeConverter.ReadAsync(context, arg, services).ConfigureAwait(false); + if (!readResult.IsSuccess) + return readResult; + + return ParseResult.FromSuccess(readResult.Value); } protected override Task InvokeModuleEvent (IInteractionContext context, IResult result) @@ -108,5 +141,15 @@ namespace Discord.Interactions else return $"Slash Command: \"{base.ToString()}\" for {context.User} in {context.Channel}"; } + + private static IEnumerable FlattenParameters(IEnumerable parameters) + { + foreach (var parameter in parameters) + if (!parameter.IsComplexParameter) + yield return parameter; + else + foreach(var complexParameterField in parameter.ComplexParameterFields) + yield return complexParameterField; + } } } diff --git a/src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs b/src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs index 790838ad9..05695f862 100644 --- a/src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs +++ b/src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs @@ -39,6 +39,11 @@ namespace Discord.Interactions /// public Type Type { get; } + /// + /// Gets the assigned to this component. + /// + public ComponentTypeConverter TypeConverter { get; } + /// /// Gets the default value of this component. /// @@ -57,6 +62,7 @@ namespace Discord.Interactions IsRequired = builder.IsRequired; ComponentType = builder.ComponentType; Type = builder.Type; + TypeConverter = builder.TypeConverter; DefaultValue = builder.DefaultValue; Attributes = builder.Attributes.ToImmutableArray(); } diff --git a/src/Discord.Net.Interactions/Info/ModalInfo.cs b/src/Discord.Net.Interactions/Info/ModalInfo.cs index edc31373e..5130c26a1 100644 --- a/src/Discord.Net.Interactions/Info/ModalInfo.cs +++ b/src/Discord.Net.Interactions/Info/ModalInfo.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Threading.Tasks; namespace Discord.Interactions { @@ -19,6 +20,7 @@ namespace Discord.Interactions /// public class ModalInfo { + internal readonly InteractionService _interactionService; internal readonly ModalInitializer _initializer; /// @@ -53,16 +55,18 @@ namespace Discord.Interactions TextComponents = Components.OfType().ToImmutableArray(); + _interactionService = builder._interactionService; _initializer = builder.ModalInitializer; } /// /// Creates an and fills it with provided message components. /// - /// that will be injected into the modal. + /// that will be injected into the modal. /// /// A filled with the provided components. /// + [Obsolete("This method is no longer supported with the introduction of Component TypeConverters, please use the CreateModalAsync method.")] public IModal CreateModal(IModalInteraction modalInteraction, bool throwOnMissingField = false) { var args = new object[Components.Count]; @@ -86,5 +90,50 @@ namespace Discord.Interactions return _initializer(args); } + + /// + /// Creates an and fills it with provided message components. + /// + /// Context of the that will be injected into the modal. + /// Services to be passed onto the s of the modal fiels. + /// Wheter or not this method should exit on encountering a missing modal field. + /// + /// A if a type conversion has failed, else a . + /// + public async Task CreateModalAsync(IInteractionContext context, IServiceProvider services = null, bool throwOnMissingField = false) + { + if (context.Interaction is not IModalInteraction modalInteraction) + return ParseResult.FromError(InteractionCommandError.Unsuccessful, "Provided context doesn't belong to a Modal Interaction."); + + services ??= EmptyServiceProvider.Instance; + + var args = new object[Components.Count]; + var components = modalInteraction.Data.Components.ToList(); + + for (var i = 0; i < Components.Count; i++) + { + var input = Components.ElementAt(i); + var component = components.Find(x => x.CustomId == input.CustomId); + + if (component is null) + { + if (!throwOnMissingField) + args[i] = input.DefaultValue; + else + return ParseResult.FromError(InteractionCommandError.BadArgs, $"Modal interaction is missing the required field: {input.CustomId}"); + } + else + { + var readResult = await input.TypeConverter.ReadAsync(context, component, services).ConfigureAwait(false); + + if (!readResult.IsSuccess) + return readResult; + + args[i] = readResult.Value; + } + } + + return ParseResult.FromSuccess(_initializer(args)); + } } } diff --git a/src/Discord.Net.Interactions/Info/Parameters/ComponentCommandParameterInfo.cs b/src/Discord.Net.Interactions/Info/Parameters/ComponentCommandParameterInfo.cs new file mode 100644 index 000000000..36b75ddb7 --- /dev/null +++ b/src/Discord.Net.Interactions/Info/Parameters/ComponentCommandParameterInfo.cs @@ -0,0 +1,34 @@ +using Discord.Interactions.Builders; + +namespace Discord.Interactions +{ + /// + /// Represents the parameter info class for commands. + /// + public class ComponentCommandParameterInfo : CommandParameterInfo + { + /// + /// Gets the that will be used to convert a message component value into + /// , if is false. + /// + public ComponentTypeConverter TypeConverter { get; } + + /// + /// Gets the that will be used to convert a CustomId segment value into + /// , if is . + /// + public TypeReader TypeReader { get; } + + /// + /// Gets whether this parameter is a CustomId segment or a component value parameter. + /// + public bool IsRouteSegmentParameter { get; } + + internal ComponentCommandParameterInfo(ComponentCommandParameterBuilder builder, ICommandInfo command) : base(builder, command) + { + TypeConverter = builder.TypeConverter; + TypeReader = builder.TypeReader; + IsRouteSegmentParameter = builder.IsRouteSegmentParameter; + } + } +} diff --git a/src/Discord.Net.Interactions/Info/Parameters/ModalCommandParameterInfo.cs b/src/Discord.Net.Interactions/Info/Parameters/ModalCommandParameterInfo.cs index 28162e109..cafb0b7f5 100644 --- a/src/Discord.Net.Interactions/Info/Parameters/ModalCommandParameterInfo.cs +++ b/src/Discord.Net.Interactions/Info/Parameters/ModalCommandParameterInfo.cs @@ -15,7 +15,12 @@ namespace Discord.Interactions /// /// Gets whether this parameter is an /// - public bool IsModalParameter => Modal is not null; + public bool IsModalParameter { get; } + + /// + /// Gets the assigned to this parameter, if is . + /// + public TypeReader TypeReader { get; } /// public new ModalCommandInfo Command => base.Command as ModalCommandInfo; @@ -23,6 +28,8 @@ namespace Discord.Interactions internal ModalCommandParameterInfo(ModalCommandParameterBuilder builder, ICommandInfo command) : base(builder, command) { Modal = builder.Modal; + IsModalParameter = builder.IsModalParameter; + TypeReader = builder.TypeReader; } } } diff --git a/src/Discord.Net.Interactions/Info/Parameters/SlashCommandParameterInfo.cs b/src/Discord.Net.Interactions/Info/Parameters/SlashCommandParameterInfo.cs index 68b63c806..8702d69f7 100644 --- a/src/Discord.Net.Interactions/Info/Parameters/SlashCommandParameterInfo.cs +++ b/src/Discord.Net.Interactions/Info/Parameters/SlashCommandParameterInfo.cs @@ -1,13 +1,25 @@ using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; namespace Discord.Interactions { + /// + /// Represents a cached argument constructor delegate. + /// + /// Method arguments array. + /// + /// Returns the constructed object. + /// + public delegate object ComplexParameterInitializer(object[] args); + /// /// Represents the parameter info class for commands. /// public class SlashCommandParameterInfo : CommandParameterInfo { + internal readonly ComplexParameterInitializer _complexParameterInitializer; + /// public new SlashCommandInfo Command => base.Command as SlashCommandInfo; @@ -43,9 +55,14 @@ namespace Discord.Interactions public bool IsAutocomplete { get; } /// - /// Gets the Discord option type this parameter represents. + /// Gets whether this type should be treated as a complex parameter. /// - public ApplicationCommandOptionType DiscordOptionType => TypeConverter.GetDiscordType(); + public bool IsComplexParameter { get; } + + /// + /// Gets the Discord option type this parameter represents. If the parameter is not a complex parameter. + /// + public ApplicationCommandOptionType? DiscordOptionType => TypeConverter?.GetDiscordType(); /// /// Gets the parameter choices of this Slash Application Command parameter. @@ -57,6 +74,11 @@ namespace Discord.Interactions /// public IReadOnlyCollection ChannelTypes { get; } + /// + /// Gets the constructor parameters of this parameter, if is . + /// + public IReadOnlyCollection ComplexParameterFields { get; } + internal SlashCommandParameterInfo(Builders.SlashCommandParameterBuilder builder, SlashCommandInfo command) : base(builder, command) { TypeConverter = builder.TypeConverter; @@ -64,9 +86,13 @@ namespace Discord.Interactions Description = builder.Description; MaxValue = builder.MaxValue; MinValue = builder.MinValue; + IsComplexParameter = builder.IsComplexParameter; IsAutocomplete = builder.Autocomplete; Choices = builder.Choices.ToImmutableArray(); ChannelTypes = builder.ChannelTypes.ToImmutableArray(); + ComplexParameterFields = builder.ComplexParameterFields?.Select(x => x.Build(command)).ToImmutableArray(); + + _complexParameterInitializer = builder.ComplexParameterInitializer; } } } diff --git a/src/Discord.Net.Interactions/InteractionService.cs b/src/Discord.Net.Interactions/InteractionService.cs index c1291bd6b..01fb8cc9d 100644 --- a/src/Discord.Net.Interactions/InteractionService.cs +++ b/src/Discord.Net.Interactions/InteractionService.cs @@ -3,6 +3,7 @@ using Discord.Logging; using Discord.Rest; using Discord.WebSocket; using System; +using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -23,6 +24,29 @@ namespace Discord.Interactions public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } internal readonly AsyncEvent> _logEvent = new (); + /// + /// Occurs when any type of interaction is executed. + /// + public event Func InteractionExecuted + { + add + { + SlashCommandExecuted += value; + ContextCommandExecuted += value; + ComponentCommandExecuted += value; + AutocompleteCommandExecuted += value; + ModalCommandExecuted += value; + } + remove + { + SlashCommandExecuted -= value; + ContextCommandExecuted -= value; + ComponentCommandExecuted -= value; + AutocompleteCommandExecuted -= value; + ModalCommandExecuted -= value; + } + } + /// /// Occurs when a Slash Command is executed. /// @@ -66,8 +90,9 @@ namespace Discord.Interactions private readonly CommandMap _autocompleteCommandMap; private readonly CommandMap _modalCommandMap; private readonly HashSet _moduleDefs; - private readonly ConcurrentDictionary _typeConverters; - private readonly ConcurrentDictionary _genericTypeConverters; + private readonly TypeMap _typeConverterMap; + private readonly TypeMap _compTypeConverterMap; + private readonly TypeMap _typeReaderMap; private readonly ConcurrentDictionary _autocompleteHandlers = new(); private readonly ConcurrentDictionary _modalInfos = new(); private readonly SemaphoreSlim _lock; @@ -179,22 +204,38 @@ namespace Discord.Interactions _autoServiceScopes = config.AutoServiceScopes; _restResponseCallback = config.RestResponseCallback; - _genericTypeConverters = new ConcurrentDictionary - { - [typeof(IChannel)] = typeof(DefaultChannelConverter<>), - [typeof(IRole)] = typeof(DefaultRoleConverter<>), - [typeof(IAttachment)] = typeof(DefaultAttachmentConverter<>), - [typeof(IUser)] = typeof(DefaultUserConverter<>), - [typeof(IMentionable)] = typeof(DefaultMentionableConverter<>), - [typeof(IConvertible)] = typeof(DefaultValueConverter<>), - [typeof(Enum)] = typeof(EnumConverter<>), - [typeof(Nullable<>)] = typeof(NullableConverter<>), - }; + _typeConverterMap = new TypeMap(this, new ConcurrentDictionary + { + [typeof(TimeSpan)] = new TimeSpanConverter() + }, new ConcurrentDictionary + { + [typeof(IChannel)] = typeof(DefaultChannelConverter<>), + [typeof(IRole)] = typeof(DefaultRoleConverter<>), + [typeof(IAttachment)] = typeof(DefaultAttachmentConverter<>), + [typeof(IUser)] = typeof(DefaultUserConverter<>), + [typeof(IMentionable)] = typeof(DefaultMentionableConverter<>), + [typeof(IConvertible)] = typeof(DefaultValueConverter<>), + [typeof(Enum)] = typeof(EnumConverter<>), + [typeof(Nullable<>)] = typeof(NullableConverter<>) + }); + + _compTypeConverterMap = new TypeMap(this, new ConcurrentDictionary(), + new ConcurrentDictionary + { + [typeof(Array)] = typeof(DefaultArrayComponentConverter<>), + [typeof(IConvertible)] = typeof(DefaultValueComponentConverter<>) + }); - _typeConverters = new ConcurrentDictionary - { - [typeof(TimeSpan)] = new TimeSpanConverter() - }; + _typeReaderMap = new TypeMap(this, new ConcurrentDictionary(), + new ConcurrentDictionary + { + [typeof(IChannel)] = typeof(DefaultChannelReader<>), + [typeof(IRole)] = typeof(DefaultRoleReader<>), + [typeof(IUser)] = typeof(DefaultUserReader<>), + [typeof(IMessage)] = typeof(DefaultMessageReader<>), + [typeof(IConvertible)] = typeof(DefaultValueReader<>), + [typeof(Enum)] = typeof(EnumReader<>) + }); } /// @@ -293,7 +334,7 @@ namespace Discord.Interactions public async Task AddModuleAsync (Type type, IServiceProvider services) { if (!typeof(IInteractionModuleBase).IsAssignableFrom(type)) - throw new ArgumentException("Type parameter must be a type of Slash Module", "T"); + throw new ArgumentException("Type parameter must be a type of Slash Module", nameof(type)); services ??= EmptyServiceProvider.Instance; @@ -326,7 +367,7 @@ namespace Discord.Interactions } /// - /// Register Application Commands from and to a guild. + /// Register Application Commands from and to a guild. /// /// Id of the target guild. /// If , this operation will not delete the commands that are missing from . @@ -422,7 +463,7 @@ namespace Discord.Interactions } /// - /// Register Application Commands from modules provided in to a guild. + /// Register Application Commands from modules provided in to a guild. /// /// The target guild. /// Modules to be registered to Discord. @@ -449,7 +490,7 @@ namespace Discord.Interactions } /// - /// Register Application Commands from modules provided in as global commands. + /// Register Application Commands from modules provided in as global commands. /// /// Modules to be registered to Discord. /// @@ -677,7 +718,7 @@ namespace Discord.Interactions public async Task ExecuteCommandAsync (IInteractionContext context, IServiceProvider services) { var interaction = context.Interaction; - + return interaction switch { ISlashCommandInteraction slashCommand => await ExecuteSlashCommandAsync(context, slashCommand, services).ConfigureAwait(false), @@ -747,9 +788,7 @@ namespace Discord.Interactions if(autocompleteHandlerResult.IsSuccess) { - var parameter = autocompleteHandlerResult.Command.Parameters.FirstOrDefault(x => string.Equals(x.Name, interaction.Data.Current.Name, StringComparison.Ordinal)); - - if(parameter?.AutocompleteHandler is not null) + if (autocompleteHandlerResult.Command._flattenedParameterDictionary.TryGetValue(interaction.Data.Current.Name, out var parameter) && parameter?.AutocompleteHandler is not null) return await parameter.AutocompleteHandler.ExecuteAsync(context, interaction, parameter, services).ConfigureAwait(false); } } @@ -783,47 +822,24 @@ namespace Discord.Interactions return await result.Command.ExecuteAsync(context, services, result.RegexCaptureGroups).ConfigureAwait(false); } - internal TypeConverter GetTypeConverter (Type type, IServiceProvider services = null) - { - if (_typeConverters.TryGetValue(type, out var specific)) - return specific; - else if (_genericTypeConverters.Any(x => x.Key.IsAssignableFrom(type) - || (x.Key.IsGenericTypeDefinition && type.IsGenericType && x.Key.GetGenericTypeDefinition() == type.GetGenericTypeDefinition()))) - { - services ??= EmptyServiceProvider.Instance; - - var converterType = GetMostSpecificTypeConverter(type); - var converter = ReflectionUtils.CreateObject(converterType.MakeGenericType(type).GetTypeInfo(), this, services); - _typeConverters[type] = converter; - return converter; - } - - else if (_typeConverters.Any(x => x.Value.CanConvertTo(type))) - return _typeConverters.First(x => x.Value.CanConvertTo(type)).Value; - - throw new ArgumentException($"No type {nameof(TypeConverter)} is defined for this {type.FullName}", "type"); - } + internal TypeConverter GetTypeConverter(Type type, IServiceProvider services = null) + => _typeConverterMap.Get(type, services); /// /// Add a concrete type . /// /// Primary target of the . /// The instance. - public void AddTypeConverter (TypeConverter converter) => - AddTypeConverter(typeof(T), converter); + public void AddTypeConverter(TypeConverter converter) => + _typeConverterMap.AddConcrete(converter); /// /// Add a concrete type . /// /// Primary target of the . /// The instance. - public void AddTypeConverter (Type type, TypeConverter converter) - { - if (!converter.CanConvertTo(type)) - throw new ArgumentException($"This {converter.GetType().FullName} cannot read {type.FullName} and cannot be registered as its {nameof(TypeConverter)}"); - - _typeConverters[type] = converter; - } + public void AddTypeConverter(Type type, TypeConverter converter) => + _typeConverterMap.AddConcrete(type, converter); /// /// Add a generic type . @@ -831,30 +847,173 @@ namespace Discord.Interactions /// Generic Type constraint of the of the . /// Type of the . - public void AddGenericTypeConverter (Type converterType) => - AddGenericTypeConverter(typeof(T), converterType); + public void AddGenericTypeConverter(Type converterType) => + _typeConverterMap.AddGeneric(converterType); /// /// Add a generic type . /// /// Generic Type constraint of the of the . /// Type of the . - public void AddGenericTypeConverter (Type targetType, Type converterType) - { - if (!converterType.IsGenericTypeDefinition) - throw new ArgumentException($"{converterType.FullName} is not generic."); + public void AddGenericTypeConverter(Type targetType, Type converterType) => + _typeConverterMap.AddGeneric(targetType, converterType); + + internal ComponentTypeConverter GetComponentTypeConverter(Type type, IServiceProvider services = null) => + _compTypeConverterMap.Get(type, services); + + /// + /// Add a concrete type . + /// + /// Primary target of the . + /// The instance. + public void AddComponentTypeConverter(ComponentTypeConverter converter) => + AddComponentTypeConverter(typeof(T), converter); + + /// + /// Add a concrete type . + /// + /// Primary target of the . + /// The instance. + public void AddComponentTypeConverter(Type type, ComponentTypeConverter converter) => + _compTypeConverterMap.AddConcrete(type, converter); + + /// + /// Add a generic type . + /// + /// Generic Type constraint of the of the . + /// Type of the . + public void AddGenericComponentTypeConverter(Type converterType) => + AddGenericComponentTypeConverter(typeof(T), converterType); + + /// + /// Add a generic type . + /// + /// Generic Type constraint of the of the . + /// Type of the . + public void AddGenericComponentTypeConverter(Type targetType, Type converterType) => + _compTypeConverterMap.AddGeneric(targetType, converterType); + + internal TypeReader GetTypeReader(Type type, IServiceProvider services = null) => + _typeReaderMap.Get(type, services); + + /// + /// Add a concrete type . + /// + /// Primary target of the . + /// The instance. + public void AddTypeReader(TypeReader reader) => + AddTypeReader(typeof(T), reader); + + /// + /// Add a concrete type . + /// + /// Primary target of the . + /// The instance. + public void AddTypeReader(Type type, TypeReader reader) => + _typeReaderMap.AddConcrete(type, reader); + + /// + /// Add a generic type . + /// + /// Generic Type constraint of the of the . + /// Type of the . + public void AddGenericTypeReader(Type readerType) => + AddGenericTypeReader(typeof(T), readerType); + + /// + /// Add a generic type . + /// + /// Generic Type constraint of the of the . + /// Type of the . + public void AddGenericTypeReader(Type targetType, Type readerType) => + _typeReaderMap.AddGeneric(targetType, readerType); + + /// + /// Removes a type reader for the type . + /// + /// The type to remove the readers from. + /// The reader if the resulting remove operation was successful. + /// if the remove operation was successful; otherwise . + public bool TryRemoveTypeReader(out TypeReader reader) + => TryRemoveTypeReader(typeof(T), out reader); + + /// + /// Removes a type reader for the given type. + /// + /// + /// Removing a from the will not dereference the from the loaded module/command instances. + /// You need to reload the modules for the changes to take effect. + /// + /// The type to remove the reader from. + /// The reader if the resulting remove operation was successful. + /// if the remove operation was successful; otherwise . + public bool TryRemoveTypeReader(Type type, out TypeReader reader) + => _typeReaderMap.TryRemoveConcrete(type, out reader); + + /// + /// Removes a generic type reader from the type . + /// + /// + /// Removing a from the will not dereference the from the loaded module/command instances. + /// You need to reload the modules for the changes to take effect. + /// + /// The type to remove the readers from. + /// The removed readers type. + /// if the remove operation was successful; otherwise . + public bool TryRemoveGenericTypeReader(out Type readerType) + => TryRemoveGenericTypeReader(typeof(T), out readerType); - var genericArguments = converterType.GetGenericArguments(); + /// + /// Removes a generic type reader from the given type. + /// + /// + /// Removing a from the will not dereference the from the loaded module/command instances. + /// You need to reload the modules for the changes to take effect. + /// + /// The type to remove the reader from. + /// The readers type if the remove operation was successful. + /// if the remove operation was successful; otherwise . + public bool TryRemoveGenericTypeReader(Type type, out Type readerType) + => _typeReaderMap.TryRemoveGeneric(type, out readerType); - if (genericArguments.Count() > 1) - throw new InvalidOperationException($"Valid generic {converterType.FullName}s cannot have more than 1 generic type parameter"); + /// + /// Serialize an object using a into a to be placed in a Component CustomId. + /// + /// + /// Removing a from the will not dereference the from the loaded module/command instances. + /// You need to reload the modules for the changes to take effect. + /// + /// Type of the object to be serialized. + /// Object to be serialized. + /// Services that will be passed on to the . + /// + /// A task representing the conversion process. The task result contains the result of the conversion. + /// + public Task SerializeValueAsync(T obj, IServiceProvider services) => + _typeReaderMap.Get(typeof(T), services).SerializeAsync(obj, services); - var constraints = genericArguments.SelectMany(x => x.GetGenericParameterConstraints()); + /// + /// Serialize and format multiple objects into a Custom Id string. + /// + /// A composite format string. + /// >Services that will be passed on to the s. + /// Objects to be serialized. + /// + /// A task representing the conversion process. The task result contains the result of the conversion. + /// + public async Task GenerateCustomIdStringAsync(string format, IServiceProvider services, params object[] args) + { + var serializedValues = new string[args.Length]; - if (!constraints.Any(x => x.IsAssignableFrom(targetType))) - throw new InvalidOperationException($"This generic class does not support type {targetType.FullName}"); + for(var i = 0; i < args.Length; i++) + { + var arg = args[i]; + var typeReader = _typeReaderMap.Get(arg.GetType(), null); + var result = await typeReader.SerializeAsync(arg, services).ConfigureAwait(false); + serializedValues[i] = result; + } - _genericTypeConverters[targetType] = converterType; + return string.Format(format, serializedValues); } /// @@ -872,7 +1031,7 @@ namespace Discord.Interactions if (_modalInfos.ContainsKey(type)) throw new InvalidOperationException($"Modal type {type.FullName} already exists."); - return ModalUtils.GetOrAdd(type); + return ModalUtils.GetOrAdd(type, this); } internal IAutocompleteHandler GetAutocompleteHandler(Type autocompleteHandlerType, IServiceProvider services = null) @@ -1018,7 +1177,7 @@ namespace Discord.Interactions public ModuleInfo GetModuleInfo ( ) where TModule : class { if (!typeof(IInteractionModuleBase).IsAssignableFrom(typeof(TModule))) - throw new ArgumentException("Type parameter must be a type of Slash Module", "TModule"); + throw new ArgumentException("Type parameter must be a type of Slash Module", nameof(TModule)); var module = _typedModuleDefs[typeof(TModule)]; @@ -1034,21 +1193,6 @@ namespace Discord.Interactions _lock.Dispose(); } - private Type GetMostSpecificTypeConverter (Type type) - { - if (_genericTypeConverters.TryGetValue(type, out var matching)) - return matching; - - if (type.IsGenericType && _genericTypeConverters.TryGetValue(type.GetGenericTypeDefinition(), out var genericDefinition)) - return genericDefinition; - - var typeInterfaces = type.GetInterfaces(); - var candidates = _genericTypeConverters.Where(x => x.Key.IsAssignableFrom(type)) - .OrderByDescending(x => typeInterfaces.Count(y => y.IsAssignableFrom(x.Key))); - - return candidates.First().Value; - } - private void EnsureClientReady() { if (RestClient?.CurrentUser is null || RestClient?.CurrentUser?.Id == 0) diff --git a/src/Discord.Net.Interactions/InteractionServiceConfig.cs b/src/Discord.Net.Interactions/InteractionServiceConfig.cs index 136cba24c..b6576a49f 100644 --- a/src/Discord.Net.Interactions/InteractionServiceConfig.cs +++ b/src/Discord.Net.Interactions/InteractionServiceConfig.cs @@ -31,7 +31,7 @@ namespace Discord.Interactions /// /// Gets or sets the string expression that will be treated as a wild card. /// - public string WildCardExpression { get; set; } + public string WildCardExpression { get; set; } = "*"; /// /// Gets or sets the option to use compiled lambda expressions to create module instances and execute commands. This method improves performance at the cost of memory. diff --git a/src/Discord.Net.Interactions/Map/TypeMap.cs b/src/Discord.Net.Interactions/Map/TypeMap.cs new file mode 100644 index 000000000..520ed7231 --- /dev/null +++ b/src/Discord.Net.Interactions/Map/TypeMap.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Discord.Interactions +{ + internal class TypeMap + where TConverter : class, ITypeConverter + { + private readonly ConcurrentDictionary _concretes; + private readonly ConcurrentDictionary _generics; + private readonly InteractionService _interactionService; + + public TypeMap(InteractionService interactionService, IDictionary concretes = null, IDictionary generics = null) + { + _interactionService = interactionService; + _concretes = concretes is not null ? new(concretes) : new(); + _generics = generics is not null ? new(generics) : new(); + } + + internal TConverter Get(Type type, IServiceProvider services = null) + { + if (_concretes.TryGetValue(type, out var specific)) + return specific; + + if (_generics.Any(x => x.Key.IsAssignableFrom(type) + || x.Key.IsGenericTypeDefinition && type.IsGenericType && x.Key.GetGenericTypeDefinition() == type.GetGenericTypeDefinition())) + { + services ??= EmptyServiceProvider.Instance; + + var converterType = GetMostSpecific(type); + var converter = ReflectionUtils.CreateObject(converterType.MakeGenericType(type).GetTypeInfo(), _interactionService, services); + _concretes[type] = converter; + return converter; + } + + if (_concretes.Any(x => x.Value.CanConvertTo(type))) + return _concretes.First(x => x.Value.CanConvertTo(type)).Value; + + throw new ArgumentException($"No type {typeof(TConverter).Name} is defined for this {type.FullName}", nameof(type)); + } + + public void AddConcrete(TConverter converter) => + AddConcrete(typeof(TTarget), converter); + + public void AddConcrete(Type type, TConverter converter) + { + if (!converter.CanConvertTo(type)) + throw new ArgumentException($"This {converter.GetType().FullName} cannot read {type.FullName} and cannot be registered as its {nameof(TypeConverter)}"); + + _concretes[type] = converter; + } + + public void AddGeneric(Type converterType) => + AddGeneric(typeof(TTarget), converterType); + + public void AddGeneric(Type targetType, Type converterType) + { + if (!converterType.IsGenericTypeDefinition) + throw new ArgumentException($"{converterType.FullName} is not generic."); + + var genericArguments = converterType.GetGenericArguments(); + + if (genericArguments.Length > 1) + throw new InvalidOperationException($"Valid generic {converterType.FullName}s cannot have more than 1 generic type parameter"); + + var constraints = genericArguments.SelectMany(x => x.GetGenericParameterConstraints()); + + if (!constraints.Any(x => x.IsAssignableFrom(targetType))) + throw new InvalidOperationException($"This generic class does not support type {targetType.FullName}"); + + _generics[targetType] = converterType; + } + + public bool TryRemoveConcrete(out TConverter converter) + => TryRemoveConcrete(typeof(TTarget), out converter); + + public bool TryRemoveConcrete(Type type, out TConverter converter) + => _concretes.TryRemove(type, out converter); + + public bool TryRemoveGeneric(out Type converterType) + => TryRemoveGeneric(typeof(TTarget), out converterType); + + public bool TryRemoveGeneric(Type targetType, out Type converterType) + => _generics.TryRemove(targetType, out converterType); + + private Type GetMostSpecific(Type type) + { + if (_generics.TryGetValue(type, out var matching)) + return matching; + + if (type.IsGenericType && _generics.TryGetValue(type.GetGenericTypeDefinition(), out var genericDefinition)) + return genericDefinition; + + var typeInterfaces = type.GetInterfaces(); + var candidates = _generics.Where(x => x.Key.IsAssignableFrom(type)) + .OrderByDescending(x => typeInterfaces.Count(y => y.IsAssignableFrom(x.Key))); + + return candidates.First().Value; + } + } +} diff --git a/src/Discord.Net.Interactions/RestInteractionModuleBase.cs b/src/Discord.Net.Interactions/RestInteractionModuleBase.cs index a07614f7f..e83c91fef 100644 --- a/src/Discord.Net.Interactions/RestInteractionModuleBase.cs +++ b/src/Discord.Net.Interactions/RestInteractionModuleBase.cs @@ -65,5 +65,39 @@ namespace Discord.Interactions else await InteractionService._restResponseCallback(Context, payload).ConfigureAwait(false); } + + /// + /// Responds to the interaction with a modal. + /// + /// The modal to respond with. + /// The request options for this request. + /// A string that contains json to write back to the incoming http request. + /// + /// + protected override async Task RespondWithModalAsync(Modal modal, RequestOptions options = null) + { + if (Context.Interaction is not RestInteraction restInteraction) + throw new InvalidOperationException($"Invalid interaction type. Interaction must be a type of {nameof(RestInteraction)} in order to execute this method"); + + var payload = restInteraction.RespondWithModal(modal, options); + + if (Context is IRestInteractionContext restContext && restContext.InteractionResponseCallback != null) + await restContext.InteractionResponseCallback.Invoke(payload).ConfigureAwait(false); + else + await InteractionService._restResponseCallback(Context, payload).ConfigureAwait(false); + } + + protected override async Task RespondWithModalAsync(string customId, RequestOptions options = null) + { + if (Context.Interaction is not RestInteraction restInteraction) + throw new InvalidOperationException($"Invalid interaction type. Interaction must be a type of {nameof(RestInteraction)} in order to execute this method"); + + var payload = restInteraction.RespondWithModal(customId, options); + + if (Context is IRestInteractionContext restContext && restContext.InteractionResponseCallback != null) + await restContext.InteractionResponseCallback.Invoke(payload).ConfigureAwait(false); + else + await InteractionService._restResponseCallback(Context, payload).ConfigureAwait(false); + } } } diff --git a/src/Discord.Net.Interactions/Results/ParseResult.cs b/src/Discord.Net.Interactions/Results/ParseResult.cs new file mode 100644 index 000000000..dfc6a57fe --- /dev/null +++ b/src/Discord.Net.Interactions/Results/ParseResult.cs @@ -0,0 +1,36 @@ +using System; + +namespace Discord.Interactions +{ + internal struct ParseResult : IResult + { + public object Value { get; } + + public InteractionCommandError? Error { get; } + + public string ErrorReason { get; } + + public bool IsSuccess => !Error.HasValue; + + private ParseResult(object value, InteractionCommandError? error, string reason) + { + Value = value; + Error = error; + ErrorReason = reason; + } + + public static ParseResult FromSuccess(object value) => + new ParseResult(value, null, null); + + public static ParseResult FromError(Exception exception) => + new ParseResult(null, InteractionCommandError.Exception, exception.Message); + + public static ParseResult FromError(InteractionCommandError error, string reason) => + new ParseResult(null, error, reason); + + public static ParseResult FromError(IResult result) => + new ParseResult(null, result.Error, result.ErrorReason); + + public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; + } +} diff --git a/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/ComponentTypeConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/ComponentTypeConverter.cs new file mode 100644 index 000000000..e406d4a26 --- /dev/null +++ b/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/ComponentTypeConverter.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + /// + /// Base class for creating Component TypeConverters. uses TypeConverters to interface with Slash Command parameters. + /// + public abstract class ComponentTypeConverter : ITypeConverter + { + /// + /// Will be used to search for alternative TypeConverters whenever the Command Service encounters an unknown parameter type. + /// + /// An object type. + /// + /// The boolean result. + /// + public abstract bool CanConvertTo(Type type); + + /// + /// Will be used to read the incoming payload before executing the method body. + /// + /// Command exexution context. + /// Recieved option payload. + /// Service provider that will be used to initialize the command module. + /// + /// The result of the read process. + /// + public abstract Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services); + } + + /// + public abstract class ComponentTypeConverter : ComponentTypeConverter + { + /// + public sealed override bool CanConvertTo(Type type) => + typeof(T).IsAssignableFrom(type); + } +} diff --git a/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultArrayComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultArrayComponentConverter.cs new file mode 100644 index 000000000..87fc431c5 --- /dev/null +++ b/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultArrayComponentConverter.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + internal sealed class DefaultArrayComponentConverter : ComponentTypeConverter + { + private readonly TypeReader _typeReader; + private readonly Type _underlyingType; + + public DefaultArrayComponentConverter(InteractionService interactionService) + { + var type = typeof(T); + + if (!type.IsArray) + throw new InvalidOperationException($"{nameof(DefaultArrayComponentConverter)} cannot be used to convert a non-array type."); + + _underlyingType = typeof(T).GetElementType(); + _typeReader = interactionService.GetTypeReader(_underlyingType); + } + + public override async Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) + { + var results = new List(); + + foreach (var value in option.Values) + { + var result = await _typeReader.ReadAsync(context, value, services).ConfigureAwait(false); + + if (!result.IsSuccess) + return result; + + results.Add(result); + } + + var destination = Array.CreateInstance(_underlyingType, results.Count); + + for (var i = 0; i < results.Count; i++) + destination.SetValue(results[i].Value, i); + + return TypeConverterResult.FromSuccess(destination); + } + } +} diff --git a/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultValueComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultValueComponentConverter.cs new file mode 100644 index 000000000..9ed82c6ed --- /dev/null +++ b/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultValueComponentConverter.cs @@ -0,0 +1,26 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + internal sealed class DefaultValueComponentConverter : ComponentTypeConverter + where T : IConvertible + { + public override Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) + { + try + { + return option.Type switch + { + ComponentType.SelectMenu => Task.FromResult(TypeConverterResult.FromSuccess(Convert.ChangeType(string.Join(",", option.Values), typeof(T)))), + ComponentType.TextInput => Task.FromResult(TypeConverterResult.FromSuccess(Convert.ChangeType(option.Value, typeof(T)))), + _ => Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"{option.Type} doesn't have a convertible value.")) + }; + } + catch (InvalidCastException castEx) + { + return Task.FromResult(TypeConverterResult.FromError(castEx)); + } + } + } +} diff --git a/src/Discord.Net.Interactions/TypeConverters/DefaultEntityTypeConverter.cs b/src/Discord.Net.Interactions/TypeConverters/SlashCommands/DefaultEntityTypeConverter.cs similarity index 100% rename from src/Discord.Net.Interactions/TypeConverters/DefaultEntityTypeConverter.cs rename to src/Discord.Net.Interactions/TypeConverters/SlashCommands/DefaultEntityTypeConverter.cs diff --git a/src/Discord.Net.Interactions/TypeConverters/DefaultValueConverter.cs b/src/Discord.Net.Interactions/TypeConverters/SlashCommands/DefaultValueConverter.cs similarity index 100% rename from src/Discord.Net.Interactions/TypeConverters/DefaultValueConverter.cs rename to src/Discord.Net.Interactions/TypeConverters/SlashCommands/DefaultValueConverter.cs diff --git a/src/Discord.Net.Interactions/TypeConverters/EnumConverter.cs b/src/Discord.Net.Interactions/TypeConverters/SlashCommands/EnumConverter.cs similarity index 90% rename from src/Discord.Net.Interactions/TypeConverters/EnumConverter.cs rename to src/Discord.Net.Interactions/TypeConverters/SlashCommands/EnumConverter.cs index a06c70ec4..1406c6f1a 100644 --- a/src/Discord.Net.Interactions/TypeConverters/EnumConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/SlashCommands/EnumConverter.cs @@ -2,6 +2,7 @@ using Discord.WebSocket; using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Threading.Tasks; namespace Discord.Interactions @@ -27,12 +28,14 @@ namespace Discord.Interactions var choices = new List(); foreach (var member in members) + { + var displayValue = member.GetCustomAttribute()?.Name ?? member.Name; choices.Add(new ApplicationCommandOptionChoiceProperties { - Name = member.Name, + Name = displayValue, Value = member.Name }); - + } properties.Choices = choices; } } diff --git a/src/Discord.Net.Interactions/TypeConverters/NullableConverter.cs b/src/Discord.Net.Interactions/TypeConverters/SlashCommands/NullableConverter.cs similarity index 100% rename from src/Discord.Net.Interactions/TypeConverters/NullableConverter.cs rename to src/Discord.Net.Interactions/TypeConverters/SlashCommands/NullableConverter.cs diff --git a/src/Discord.Net.Interactions/TypeConverters/TimeSpanConverter.cs b/src/Discord.Net.Interactions/TypeConverters/SlashCommands/TimeSpanConverter.cs similarity index 100% rename from src/Discord.Net.Interactions/TypeConverters/TimeSpanConverter.cs rename to src/Discord.Net.Interactions/TypeConverters/SlashCommands/TimeSpanConverter.cs diff --git a/src/Discord.Net.Interactions/TypeConverters/TypeConverter.cs b/src/Discord.Net.Interactions/TypeConverters/SlashCommands/TypeConverter.cs similarity index 95% rename from src/Discord.Net.Interactions/TypeConverters/TypeConverter.cs rename to src/Discord.Net.Interactions/TypeConverters/SlashCommands/TypeConverter.cs index 360b6ce4a..09cbc56d4 100644 --- a/src/Discord.Net.Interactions/TypeConverters/TypeConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/SlashCommands/TypeConverter.cs @@ -6,7 +6,7 @@ namespace Discord.Interactions /// /// Base class for creating TypeConverters. uses TypeConverters to interface with Slash Command parameters. /// - public abstract class TypeConverter + public abstract class TypeConverter : ITypeConverter { /// /// Will be used to search for alternative TypeConverters whenever the Command Service encounters an unknown parameter type. diff --git a/src/Discord.Net.Interactions/TypeReaders/DefaultSnowflakeReader.cs b/src/Discord.Net.Interactions/TypeReaders/DefaultSnowflakeReader.cs new file mode 100644 index 000000000..e2ac1efbd --- /dev/null +++ b/src/Discord.Net.Interactions/TypeReaders/DefaultSnowflakeReader.cs @@ -0,0 +1,48 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + internal abstract class DefaultSnowflakeReader : TypeReader + where T : class, ISnowflakeEntity + { + protected abstract Task GetEntity(ulong id, IInteractionContext ctx); + + public override async Task ReadAsync(IInteractionContext context, string option, IServiceProvider services) + { + if (!ulong.TryParse(option, out var snowflake)) + return TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"{option} isn't a valid snowflake thus cannot be converted into {typeof(T).Name}"); + + var result = await GetEntity(snowflake, context).ConfigureAwait(false); + + return result is not null ? + TypeConverterResult.FromSuccess(result) : TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"{option} must be a valid {typeof(T).Name} snowflake to be parsed."); + } + + public override Task SerializeAsync(object obj, IServiceProvider services) => Task.FromResult((obj as ISnowflakeEntity)?.Id.ToString()); + } + + internal sealed class DefaultUserReader : DefaultSnowflakeReader + where T : class, IUser + { + protected override async Task GetEntity(ulong id, IInteractionContext ctx) => await ctx.Client.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T; + } + + internal sealed class DefaultChannelReader : DefaultSnowflakeReader + where T : class, IChannel + { + protected override async Task GetEntity(ulong id, IInteractionContext ctx) => await ctx.Client.GetChannelAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T; + } + + internal sealed class DefaultRoleReader : DefaultSnowflakeReader + where T : class, IRole + { + protected override Task GetEntity(ulong id, IInteractionContext ctx) => Task.FromResult(ctx.Guild?.GetRole(id) as T); + } + + internal sealed class DefaultMessageReader : DefaultSnowflakeReader + where T : class, IMessage + { + protected override async Task GetEntity(ulong id, IInteractionContext ctx) => await ctx.Channel.GetMessageAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T; + } +} diff --git a/src/Discord.Net.Interactions/TypeReaders/DefaultValueReader.cs b/src/Discord.Net.Interactions/TypeReaders/DefaultValueReader.cs new file mode 100644 index 000000000..e833382a6 --- /dev/null +++ b/src/Discord.Net.Interactions/TypeReaders/DefaultValueReader.cs @@ -0,0 +1,22 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + internal sealed class DefaultValueReader : TypeReader + where T : IConvertible + { + public override Task ReadAsync(IInteractionContext context, string option, IServiceProvider services) + { + try + { + var converted = Convert.ChangeType(option, typeof(T)); + return Task.FromResult(TypeConverterResult.FromSuccess(converted)); + } + catch (InvalidCastException castEx) + { + return Task.FromResult(TypeConverterResult.FromError(castEx)); + } + } + } +} diff --git a/src/Discord.Net.Interactions/TypeReaders/EnumReader.cs b/src/Discord.Net.Interactions/TypeReaders/EnumReader.cs new file mode 100644 index 000000000..df6f2ac33 --- /dev/null +++ b/src/Discord.Net.Interactions/TypeReaders/EnumReader.cs @@ -0,0 +1,25 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + internal sealed class EnumReader : TypeReader + where T : struct, Enum + { + public override Task ReadAsync(IInteractionContext context, string option, IServiceProvider services) + { + return Task.FromResult(Enum.TryParse(option, out var result) ? + TypeConverterResult.FromSuccess(result) : TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"Value {option} cannot be converted to {nameof(T)}")); + } + + public override Task SerializeAsync(object obj, IServiceProvider services) + { + var name = Enum.GetName(typeof(T), obj); + + if (name is null) + throw new ArgumentException($"Enum name cannot be parsed from {obj}"); + + return Task.FromResult(name); + } + } +} diff --git a/src/Discord.Net.Interactions/TypeReaders/TypeReader.cs b/src/Discord.Net.Interactions/TypeReaders/TypeReader.cs new file mode 100644 index 000000000..e518e0208 --- /dev/null +++ b/src/Discord.Net.Interactions/TypeReaders/TypeReader.cs @@ -0,0 +1,46 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + /// + /// Base class for creating TypeConverters. uses TypeConverters to interface with Slash Command parameters. + /// + public abstract class TypeReader : ITypeConverter + { + /// + /// Will be used to search for alternative TypeReaders whenever the Command Service encounters an unknown parameter type. + /// + /// An object type. + /// + /// The boolean result. + /// + public abstract bool CanConvertTo(Type type); + + /// + /// Will be used to read the incoming payload before executing the method body. + /// + /// Command execution context. + /// Received option payload. + /// Service provider that will be used to initialize the command module. + /// The result of the read process. + public abstract Task ReadAsync(IInteractionContext context, string option, IServiceProvider services); + + /// + /// Will be used to serialize objects into strings. + /// + /// Object to be serialized. + /// + /// A task representing the conversion process. The result of the task contains the conversion result. + /// + public virtual Task SerializeAsync(object obj, IServiceProvider services) => Task.FromResult(obj.ToString()); + } + + /// + public abstract class TypeReader : TypeReader + { + /// + public sealed override bool CanConvertTo(Type type) => + typeof(T).IsAssignableFrom(type); + } +} diff --git a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs index 48b6e44e7..c2052b7c7 100644 --- a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs +++ b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs @@ -13,7 +13,7 @@ namespace Discord.Interactions { Name = parameterInfo.Name, Description = parameterInfo.Description, - Type = parameterInfo.DiscordOptionType, + Type = parameterInfo.DiscordOptionType.Value, IsRequired = parameterInfo.IsRequired, Choices = parameterInfo.Choices?.Select(x => new ApplicationCommandOptionChoiceProperties { @@ -46,7 +46,7 @@ namespace Discord.Interactions if (commandInfo.Parameters.Count > SlashCommandBuilder.MaxOptionsCount) throw new InvalidOperationException($"Slash Commands cannot have more than {SlashCommandBuilder.MaxOptionsCount} command parameters"); - props.Options = commandInfo.Parameters.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional>.Unspecified; + props.Options = commandInfo.FlattenedParameters.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional>.Unspecified; return props; } @@ -58,7 +58,7 @@ namespace Discord.Interactions Description = commandInfo.Description, Type = ApplicationCommandOptionType.SubCommand, IsRequired = false, - Options = commandInfo.Parameters?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() + Options = commandInfo.FlattenedParameters?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() }; public static ApplicationCommandProperties ToApplicationCommandProps(this ContextCommandInfo commandInfo) @@ -196,5 +196,26 @@ namespace Discord.Interactions }).ToList(), Options = commandOption.Options?.Select(x => x.ToApplicationCommandOptionProps()).ToList() }; + + public static Modal ToModal(this ModalInfo modalInfo, string customId, Action modifyModal = null) + { + var builder = new ModalBuilder(modalInfo.Title, customId); + + foreach (var input in modalInfo.Components) + switch (input) + { + case TextInputComponentInfo textComponent: + builder.AddTextInput(textComponent.Label, textComponent.CustomId, textComponent.Style, textComponent.Placeholder, textComponent.IsRequired ? textComponent.MinLength : null, + textComponent.MaxLength, textComponent.IsRequired, textComponent.InitialValue); + break; + default: + throw new InvalidOperationException($"{input.GetType().FullName} isn't a valid component info class"); + } + + if(modifyModal is not null) + modifyModal(builder); + + return builder.Build(); + } } } diff --git a/src/Discord.Net.Interactions/Utilities/ModalUtils.cs b/src/Discord.Net.Interactions/Utilities/ModalUtils.cs index d42cc2fe9..e2d028e1f 100644 --- a/src/Discord.Net.Interactions/Utilities/ModalUtils.cs +++ b/src/Discord.Net.Interactions/Utilities/ModalUtils.cs @@ -7,20 +7,20 @@ namespace Discord.Interactions { internal static class ModalUtils { - private static ConcurrentDictionary _modalInfos = new(); + private static readonly ConcurrentDictionary _modalInfos = new(); public static IReadOnlyCollection Modals => _modalInfos.Values.ToReadOnlyCollection(); - public static ModalInfo GetOrAdd(Type type) + public static ModalInfo GetOrAdd(Type type, InteractionService interactionService) { if (!typeof(IModal).IsAssignableFrom(type)) throw new ArgumentException($"Must be an implementation of {nameof(IModal)}", nameof(type)); - return _modalInfos.GetOrAdd(type, ModuleClassBuilder.BuildModalInfo(type)); + return _modalInfos.GetOrAdd(type, ModuleClassBuilder.BuildModalInfo(type, interactionService)); } - public static ModalInfo GetOrAdd() where T : class, IModal - => GetOrAdd(typeof(T)); + public static ModalInfo GetOrAdd(InteractionService interactionService) where T : class, IModal + => GetOrAdd(typeof(T), interactionService); public static bool TryGet(Type type, out ModalInfo modalInfo) { diff --git a/src/Discord.Net.Rest/API/Common/Connection.cs b/src/Discord.Net.Rest/API/Common/Connection.cs index bd8de3902..0a9940e23 100644 --- a/src/Discord.Net.Rest/API/Common/Connection.cs +++ b/src/Discord.Net.Rest/API/Common/Connection.cs @@ -7,14 +7,22 @@ namespace Discord.API { [JsonProperty("id")] public string Id { get; set; } - [JsonProperty("type")] - public string Type { get; set; } [JsonProperty("name")] public string Name { get; set; } + [JsonProperty("type")] + public string Type { get; set; } [JsonProperty("revoked")] - public bool Revoked { get; set; } - + public Optional Revoked { get; set; } [JsonProperty("integrations")] - public IReadOnlyCollection Integrations { get; set; } + public Optional> Integrations { get; set; } + [JsonProperty("verified")] + public bool Verified { get; set; } + [JsonProperty("friend_sync")] + public bool FriendSync { get; set; } + [JsonProperty("show_activity")] + public bool ShowActivity { get; set; } + [JsonProperty("visibility")] + public ConnectionVisibility Visibility { get; set; } + } } diff --git a/src/Discord.Net.Rest/API/Common/GuildScheduledEvent.cs b/src/Discord.Net.Rest/API/Common/GuildScheduledEvent.cs index 338c24dc9..94c53e779 100644 --- a/src/Discord.Net.Rest/API/Common/GuildScheduledEvent.cs +++ b/src/Discord.Net.Rest/API/Common/GuildScheduledEvent.cs @@ -39,5 +39,7 @@ namespace Discord.API public Optional Creator { get; set; } [JsonProperty("user_count")] public Optional UserCount { get; set; } + [JsonProperty("image")] + public string Image { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/Integration.cs b/src/Discord.Net.Rest/API/Common/Integration.cs index 47d67e149..5a2b00001 100644 --- a/src/Discord.Net.Rest/API/Common/Integration.cs +++ b/src/Discord.Net.Rest/API/Common/Integration.cs @@ -5,6 +5,9 @@ namespace Discord.API { internal class Integration { + [JsonProperty("guild_id")] + public Optional GuildId { get; set; } + [JsonProperty("id")] public ulong Id { get; set; } [JsonProperty("name")] @@ -14,18 +17,26 @@ namespace Discord.API [JsonProperty("enabled")] public bool Enabled { get; set; } [JsonProperty("syncing")] - public bool Syncing { get; set; } + public Optional Syncing { get; set; } [JsonProperty("role_id")] - public ulong RoleId { get; set; } + public Optional RoleId { get; set; } + [JsonProperty("enable_emoticons")] + public Optional EnableEmoticons { get; set; } [JsonProperty("expire_behavior")] - public ulong ExpireBehavior { get; set; } + public Optional ExpireBehavior { get; set; } [JsonProperty("expire_grace_period")] - public ulong ExpireGracePeriod { get; set; } + public Optional ExpireGracePeriod { get; set; } [JsonProperty("user")] - public User User { get; set; } + public Optional User { get; set; } [JsonProperty("account")] - public IntegrationAccount Account { get; set; } + public Optional Account { get; set; } [JsonProperty("synced_at")] - public DateTimeOffset SyncedAt { get; set; } + public Optional SyncedAt { get; set; } + [JsonProperty("subscriber_count")] + public Optional SubscriberAccount { get; set; } + [JsonProperty("revoked")] + public Optional Revoked { get; set; } + [JsonProperty("application")] + public Optional Application { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/IntegrationAccount.cs b/src/Discord.Net.Rest/API/Common/IntegrationAccount.cs index a8d33931c..6b8328074 100644 --- a/src/Discord.Net.Rest/API/Common/IntegrationAccount.cs +++ b/src/Discord.Net.Rest/API/Common/IntegrationAccount.cs @@ -5,7 +5,7 @@ namespace Discord.API internal class IntegrationAccount { [JsonProperty("id")] - public ulong Id { get; set; } + public string Id { get; set; } [JsonProperty("name")] public string Name { get; set; } } diff --git a/src/Discord.Net.Rest/API/Common/IntegrationApplication.cs b/src/Discord.Net.Rest/API/Common/IntegrationApplication.cs new file mode 100644 index 000000000..4e07398b8 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/IntegrationApplication.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class IntegrationApplication + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("icon")] + public Optional Icon { get; set; } + [JsonProperty("description")] + public string Description { get; set; } + [JsonProperty("summary")] + public string Summary { get; set; } + [JsonProperty("bot")] + public Optional Bot { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/ThreadMetadata.cs b/src/Discord.Net.Rest/API/Common/ThreadMetadata.cs index 39e9bd13e..6735504c8 100644 --- a/src/Discord.Net.Rest/API/Common/ThreadMetadata.cs +++ b/src/Discord.Net.Rest/API/Common/ThreadMetadata.cs @@ -16,5 +16,11 @@ namespace Discord.API [JsonProperty("locked")] public Optional Locked { get; set; } + + [JsonProperty("invitable")] + public Optional Invitable { get; set; } + + [JsonProperty("create_timestamp")] + public Optional CreatedAt { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/CreateGuildScheduledEventParams.cs b/src/Discord.Net.Rest/API/Rest/CreateGuildScheduledEventParams.cs index a207d3374..2ccd06fe6 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateGuildScheduledEventParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateGuildScheduledEventParams.cs @@ -25,5 +25,7 @@ namespace Discord.API.Rest public Optional Description { get; set; } [JsonProperty("entity_type")] public GuildScheduledEventType Type { get; set; } + [JsonProperty("image")] + public Optional Image { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs b/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs index 5996c7e83..466ad41e3 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs @@ -28,6 +28,9 @@ namespace Discord.API.Rest [JsonProperty("sticker_ids")] public Optional Stickers { get; set; } + + [JsonProperty("flags")] + public Optional Flags { get; set; } public CreateMessageParams(string content) { diff --git a/src/Discord.Net.Rest/API/Rest/GetGuildBansParams.cs b/src/Discord.Net.Rest/API/Rest/GetGuildBansParams.cs new file mode 100644 index 000000000..6a1e430c3 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/GetGuildBansParams.cs @@ -0,0 +1,9 @@ +namespace Discord.API.Rest +{ + internal class GetGuildBansParams + { + public Optional Limit { get; set; } + public Optional RelativeDirection { get; set; } + public Optional RelativeUserId { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildScheduledEventParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildScheduledEventParams.cs index 3d191a0b3..1179ddcbe 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyGuildScheduledEventParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildScheduledEventParams.cs @@ -27,5 +27,7 @@ namespace Discord.API.Rest public Optional Type { get; set; } [JsonProperty("status")] public Optional Status { get; set; } + [JsonProperty("image")] + public Optional Image { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs index 6340c3e38..67a690e4d 100644 --- a/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs +++ b/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs @@ -51,7 +51,7 @@ namespace Discord.API.Rest if (Stickers.IsSpecified) payload["sticker_ids"] = Stickers.Value; if (Flags.IsSpecified) - payload["flags"] = Flags; + payload["flags"] = Flags.Value; List attachments = new(); diff --git a/src/Discord.Net.Rest/BaseDiscordClient.cs b/src/Discord.Net.Rest/BaseDiscordClient.cs index 2bf08e3c7..75f477c7c 100644 --- a/src/Discord.Net.Rest/BaseDiscordClient.cs +++ b/src/Discord.Net.Rest/BaseDiscordClient.cs @@ -36,6 +36,7 @@ namespace Discord.Rest /// public TokenType TokenType => ApiClient.AuthTokenType; internal bool UseInteractionSnowflakeDate { get; private set; } + internal bool FormatUsersInBidirectionalUnicode { get; private set; } /// Creates a new REST-only Discord client. internal BaseDiscordClient(DiscordRestConfig config, API.DiscordRestApiClient client) @@ -49,6 +50,7 @@ namespace Discord.Rest _isFirstLogin = config.DisplayInitialLog; UseInteractionSnowflakeDate = config.UseInteractionSnowflakeDate; + FormatUsersInBidirectionalUnicode = config.FormatUsersInBidirectionalUnicode; ApiClient.RequestQueue.RateLimitTriggered += async (id, info, endpoint) => { diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs index 5debea27e..c6ad6a9fb 100644 --- a/src/Discord.Net.Rest/ClientHelper.cs +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -49,7 +49,7 @@ namespace Discord.Rest public static async Task> GetConnectionsAsync(BaseDiscordClient client, RequestOptions options) { var models = await client.ApiClient.GetMyConnectionsAsync(options).ConfigureAwait(false); - return models.Select(RestConnection.Create).ToImmutableArray(); + return models.Select(model => RestConnection.Create(client, model)).ToImmutableArray(); } public static async Task GetInviteAsync(BaseDiscordClient client, diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index f6d579d79..3b829ee17 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -1545,13 +1545,29 @@ namespace Discord.API #endregion #region Guild Bans - public async Task> GetGuildBansAsync(ulong guildId, RequestOptions options = null) + public async Task> GetGuildBansAsync(ulong guildId, GetGuildBansParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.AtLeast(args.Limit, 0, nameof(args.Limit)); + Preconditions.AtMost(args.Limit, DiscordConfig.MaxBansPerBatch, nameof(args.Limit)); options = RequestOptions.CreateOrClone(options); + int limit = args.Limit.GetValueOrDefault(DiscordConfig.MaxBansPerBatch); + ulong? relativeId = args.RelativeUserId.IsSpecified ? args.RelativeUserId.Value : (ulong?)null; + var relativeDir = args.RelativeDirection.GetValueOrDefault(Direction.Before) switch + { + Direction.After => "after", + Direction.Around => "around", + _ => "before", + }; var ids = new BucketIds(guildId: guildId); - return await SendAsync>("GET", () => $"guilds/{guildId}/bans", ids, options: options).ConfigureAwait(false); + Expression> endpoint; + if (relativeId != null) + endpoint = () => $"guilds/{guildId}/bans?limit={limit}&{relativeDir}={relativeId}"; + else + endpoint = () => $"guilds/{guildId}/bans?limit={limit}"; + return await SendAsync>("GET", endpoint, ids, options: options).ConfigureAwait(false); } public async Task GetGuildBanAsync(ulong guildId, ulong userId, RequestOptions options) { @@ -1626,7 +1642,7 @@ namespace Discord.API #region Guild Integrations /// must not be equal to zero. - public async Task> GetGuildIntegrationsAsync(ulong guildId, RequestOptions options = null) + public async Task> GetIntegrationsAsync(ulong guildId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); options = RequestOptions.CreateOrClone(options); @@ -1634,47 +1650,14 @@ namespace Discord.API var ids = new BucketIds(guildId: guildId); return await SendAsync>("GET", () => $"guilds/{guildId}/integrations", ids, options: options).ConfigureAwait(false); } - /// and must not be equal to zero. - /// must not be . - public async Task CreateGuildIntegrationAsync(ulong guildId, CreateGuildIntegrationParams args, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - Preconditions.NotNull(args, nameof(args)); - Preconditions.NotEqual(args.Id, 0, nameof(args.Id)); - options = RequestOptions.CreateOrClone(options); - - var ids = new BucketIds(guildId: guildId); - return await SendAsync("POST", () => $"guilds/{guildId}/integrations", ids, options: options).ConfigureAwait(false); - } - public async Task DeleteGuildIntegrationAsync(ulong guildId, ulong integrationId, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - Preconditions.NotEqual(integrationId, 0, nameof(integrationId)); - options = RequestOptions.CreateOrClone(options); - - var ids = new BucketIds(guildId: guildId); - return await SendAsync("DELETE", () => $"guilds/{guildId}/integrations/{integrationId}", ids, options: options).ConfigureAwait(false); - } - public async Task ModifyGuildIntegrationAsync(ulong guildId, ulong integrationId, Rest.ModifyGuildIntegrationParams args, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - Preconditions.NotEqual(integrationId, 0, nameof(integrationId)); - Preconditions.NotNull(args, nameof(args)); - Preconditions.AtLeast(args.ExpireBehavior, 0, nameof(args.ExpireBehavior)); - Preconditions.AtLeast(args.ExpireGracePeriod, 0, nameof(args.ExpireGracePeriod)); - options = RequestOptions.CreateOrClone(options); - - var ids = new BucketIds(guildId: guildId); - return await SendJsonAsync("PATCH", () => $"guilds/{guildId}/integrations/{integrationId}", args, ids, options: options).ConfigureAwait(false); - } - public async Task SyncGuildIntegrationAsync(ulong guildId, ulong integrationId, RequestOptions options = null) + public async Task DeleteIntegrationAsync(ulong guildId, ulong integrationId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(integrationId, 0, nameof(integrationId)); options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(guildId: guildId); - return await SendAsync("POST", () => $"guilds/{guildId}/integrations/{integrationId}/sync", ids, options: options).ConfigureAwait(false); + await SendAsync("DELETE", () => $"guilds/{guildId}/integrations/{integrationId}", ids, options: options).ConfigureAwait(false); } #endregion diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index b5087cd2f..d66fd5e51 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -266,8 +266,10 @@ namespace Discord.Rest } /// Message content is too long, length must be less or equal to . + /// The only valid are and . public static async Task SendMessageAsync(IMessageChannel channel, BaseDiscordClient client, - string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, RequestOptions options, Embed[] embeds) + string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, RequestOptions options, Embed[] embeds, MessageFlags flags) { embeds ??= Array.Empty(); if (embed != null) @@ -298,6 +300,10 @@ namespace Discord.Rest Preconditions.AtMost(stickers.Length, 3, nameof(stickers), "A max of 3 stickers are allowed."); } + + if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds) + throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds and none.", nameof(flags)); + var args = new CreateMessageParams(text) { IsTTS = isTTS, @@ -305,7 +311,8 @@ namespace Discord.Rest AllowedMentions = allowedMentions?.ToModel(), MessageReference = messageReference?.ToModel(), Components = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, - Stickers = stickers?.Any() ?? false ? stickers.Select(x => x.Id).ToArray() : Optional.Unspecified + Stickers = stickers?.Any() ?? false ? stickers.Select(x => x.Id).ToArray() : Optional.Unspecified, + Flags = flags }; var model = await client.ApiClient.CreateMessageAsync(channel.Id, args, options).ConfigureAwait(false); return RestUserMessage.Create(client, channel, client.CurrentUser, model); @@ -335,29 +342,44 @@ namespace Discord.Rest /// is in an invalid format. /// An I/O error occurred while opening the file. /// Message content is too long, length must be less or equal to . + /// The only valid are and . public static async Task SendFileAsync(IMessageChannel channel, BaseDiscordClient client, - string filePath, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, RequestOptions options, bool isSpoiler, Embed[] embeds) + string filePath, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, + MessageReference messageReference, MessageComponent components, ISticker[] stickers, RequestOptions options, + bool isSpoiler, Embed[] embeds, MessageFlags flags = MessageFlags.None) { string filename = Path.GetFileName(filePath); using (var file = File.OpenRead(filePath)) - return await SendFileAsync(channel, client, file, filename, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds).ConfigureAwait(false); + return await SendFileAsync(channel, client, file, filename, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, isSpoiler, embeds, flags).ConfigureAwait(false); } /// Message content is too long, length must be less or equal to . + /// The only valid are and . public static async Task SendFileAsync(IMessageChannel channel, BaseDiscordClient client, - Stream stream, string filename, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, RequestOptions options, bool isSpoiler, Embed[] embeds) + Stream stream, string filename, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, + MessageReference messageReference, MessageComponent components, ISticker[] stickers, RequestOptions options, + bool isSpoiler, Embed[] embeds, MessageFlags flags = MessageFlags.None) { using (var file = new FileAttachment(stream, filename, isSpoiler: isSpoiler)) - return await SendFileAsync(channel, client, file, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds).ConfigureAwait(false); + return await SendFileAsync(channel, client, file, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags).ConfigureAwait(false); } /// Message content is too long, length must be less or equal to . + /// The only valid are and . public static Task SendFileAsync(IMessageChannel channel, BaseDiscordClient client, - FileAttachment attachment, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, RequestOptions options, Embed[] embeds) - => SendFilesAsync(channel, client, new[] { attachment }, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + FileAttachment attachment, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, + MessageReference messageReference, MessageComponent components, ISticker[] stickers, RequestOptions options, + Embed[] embeds, MessageFlags flags = MessageFlags.None) + => SendFilesAsync(channel, client, new[] { attachment }, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); + /// The only valid are and . public static async Task SendFilesAsync(IMessageChannel channel, BaseDiscordClient client, - IEnumerable attachments, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, RequestOptions options, Embed[] embeds) + IEnumerable attachments, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, + MessageReference messageReference, MessageComponent components, ISticker[] stickers, RequestOptions options, + Embed[] embeds, MessageFlags flags) { embeds ??= Array.Empty(); if (embed != null) @@ -366,7 +388,7 @@ namespace Discord.Rest Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); - + foreach(var attachment in attachments) { Preconditions.NotNullOrEmpty(attachment.FileName, nameof(attachment.FileName), "File Name must not be empty or null"); @@ -398,12 +420,26 @@ namespace Discord.Rest } } + if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds) + throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds and none.", nameof(flags)); + if (stickers != null) { Preconditions.AtMost(stickers.Length, 3, nameof(stickers), "A max of 3 stickers are allowed."); } - var args = new UploadFileParams(attachments.ToArray()) { Content = text, IsTTS = isTTS, Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, MessageReference = messageReference?.ToModel() ?? Optional.Unspecified, MessageComponent = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, Stickers = stickers?.Any() ?? false ? stickers.Select(x => x.Id).ToArray() : Optional.Unspecified }; + var args = new UploadFileParams(attachments.ToArray()) + { + Content = text, + IsTTS = isTTS, + Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, + AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, + MessageReference = messageReference?.ToModel() ?? Optional.Unspecified, + MessageComponent = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + Stickers = stickers?.Any() ?? false ? stickers.Select(x => x.Id).ToArray() : Optional.Unspecified, + Flags = flags + }; + var model = await client.ApiClient.UploadFileAsync(channel.Id, args, options).ConfigureAwait(false); return RestUserMessage.Create(client, channel, client.CurrentUser, model); } diff --git a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs index 1af936a57..0cf92bb04 100644 --- a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs @@ -9,84 +9,14 @@ namespace Discord.Rest /// public interface IRestMessageChannel : IMessageChannel { - /// - /// Sends a message to this message channel. - /// - /// - /// This method follows the same behavior as described in . - /// Please visit its documentation for more details on this method. - /// - /// The message to be sent. - /// Determines whether the message should be read aloud by Discord or not. - /// The to be sent. - /// The options to be used when sending the request. - /// - /// Specifies if notifications are sent for mentioned users and roles in the message . - /// If null, all mentioned roles and users will be notified. - /// - /// The message references to be included. Used to reply to specific messages. - /// The message components to be included with this message. Used for interactions. - /// A collection of stickers to send with the message. - /// A array of s to send with this response. Max 10. - /// - /// A task that represents an asynchronous send operation for delivering the message. The task result - /// contains the sent message. - /// - new Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null); - /// - /// Sends a file to this message channel with an optional caption. - /// - /// - /// This method follows the same behavior as described in - /// . Please visit - /// its documentation for more details on this method. - /// - /// The file path of the file. - /// The message to be sent. - /// Whether the message should be read aloud by Discord or not. - /// The to be sent. - /// The options to be used when sending the request. - /// Whether the message attachment should be hidden as a spoiler. - /// - /// Specifies if notifications are sent for mentioned users and roles in the message . - /// If null, all mentioned roles and users will be notified. - /// - /// The message references to be included. Used to reply to specific messages. - /// The message components to be included with this message. Used for interactions. - /// A collection of stickers to send with the message. - /// A array of s to send with this response. Max 10. - /// - /// A task that represents an asynchronous send operation for delivering the message. The task result - /// contains the sent message. - /// - new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null); - /// - /// Sends a file to this message channel with an optional caption. - /// - /// - /// This method follows the same behavior as described in . - /// Please visit its documentation for more details on this method. - /// - /// The of the file to be sent. - /// The name of the attachment. - /// The message to be sent. - /// Whether the message should be read aloud by Discord or not. - /// The to be sent. - /// The options to be used when sending the request. - /// Whether the message attachment should be hidden as a spoiler. - /// - /// Specifies if notifications are sent for mentioned users and roles in the message . - /// If null, all mentioned roles and users will be notified. - /// - /// The message references to be included. Used to reply to specific messages. - /// The message components to be included with this message. Used for interactions. - /// A collection of stickers to send with the message. - /// A array of s to send with this response. Max 10. - /// - /// A task that represents an asynchronous send operation for delivering the message. The task result - /// contains the sent message. - /// - new Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null); + /// + new Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); + + /// + new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); + + /// + new Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); /// /// Gets a message from this message channel. diff --git a/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs index 83c6d8bfb..c730596c7 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs @@ -13,7 +13,7 @@ namespace Discord.Rest { #region RestChannel /// - public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + public virtual DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); internal RestChannel(BaseDiscordClient discord, ulong id) : base(discord, id) diff --git a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs index 36b190e56..3bf43a594 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs @@ -94,8 +94,12 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, + RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); /// /// @@ -122,22 +126,39 @@ namespace Discord.Rest /// is in an invalid format. /// An I/O error occurred while opening the file. /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); + /// The only valid are and . + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, + RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); /// public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) @@ -219,20 +240,38 @@ namespace Discord.Rest async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); /// - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, + RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, + Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, + Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, + stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, + bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, + AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, + ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags).ConfigureAwait(false); + #endregion #region IChannel diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs index 03858fbbe..d21852f93 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs @@ -104,8 +104,12 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, + RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); /// /// @@ -132,20 +136,40 @@ namespace Discord.Rest /// is in an invalid format. /// An I/O error occurred while opening the file. /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); + /// The only valid are and . + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, + RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); + /// The only valid are and . + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, embeds, flags); /// public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); @@ -197,17 +221,41 @@ namespace Discord.Rest async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); - - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); - async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); - async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + /// + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, + RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); + + /// + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, + Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); + + /// + async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, + Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, + stickers, embeds, flags).ConfigureAwait(false); + + /// + async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, + bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, + stickers, embeds, flags).ConfigureAwait(false); + + /// + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, + AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, + ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, + stickers, embeds, flags).ConfigureAwait(false); + #endregion #region IAudioChannel diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs index bc9d4110a..fa2362854 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs @@ -227,7 +227,7 @@ namespace Discord.Rest /// IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => AsyncEnumerable.Empty>(); //Overridden //Overridden in Text/Voice + => AsyncEnumerable.Empty>(); //Overridden in Text/Voice /// Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); //Overridden in Text/Voice diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index f1bdee65c..76c75ab6e 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -103,8 +103,12 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, + RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); /// /// @@ -131,23 +135,42 @@ namespace Discord.Rest /// is in an invalid format. /// An I/O error occurred while opening the file. /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); + /// The only valid are and . + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, + RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); + /// The only valid are and . + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds, flags); /// public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) @@ -261,7 +284,7 @@ namespace Discord.Rest /// The duration on which this thread archives after. /// /// Note: Options and - /// are only available for guilds that are boosted. You can check in the to see if the + /// are only available for guilds that are boosted. You can check in the to see if the /// guild has the THREE_DAY_THREAD_ARCHIVE and SEVEN_DAY_THREAD_ARCHIVE. /// /// @@ -332,24 +355,37 @@ namespace Discord.Rest => await GetPinnedMessagesAsync(options).ConfigureAwait(false); /// - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, + RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); /// - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, + Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); /// - async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, + Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, + stickers, embeds, flags).ConfigureAwait(false); /// - async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, + bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags).ConfigureAwait(false); /// - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, + AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, + ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags).ConfigureAwait(false); #endregion #region IGuildChannel @@ -364,10 +400,9 @@ namespace Discord.Rest /// IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) { - if (mode == CacheMode.AllowDownload) - return GetUsersAsync(options); - else - return AsyncEnumerable.Empty>(); + return mode == CacheMode.AllowDownload + ? GetUsersAsync(options) + : AsyncEnumerable.Empty>(); } #endregion diff --git a/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs index 63071b9a5..c763a6660 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs @@ -34,17 +34,26 @@ namespace Discord.Rest /// public int MessageCount { get; private set; } + /// + public bool? IsInvitable { get; private set; } + + /// + public override DateTimeOffset CreatedAt { get; } + /// /// Gets the parent text channel id. /// public ulong ParentChannelId { get; private set; } - internal RestThreadChannel(BaseDiscordClient discord, IGuild guild, ulong id) - : base(discord, guild, id) { } + internal RestThreadChannel(BaseDiscordClient discord, IGuild guild, ulong id, DateTimeOffset? createdAt) + : base(discord, guild, id) + { + CreatedAt = createdAt ?? new DateTimeOffset(2022, 1, 9, 0, 0, 0, TimeSpan.Zero); + } internal new static RestThreadChannel Create(BaseDiscordClient discord, IGuild guild, Model model) { - var entity = new RestThreadChannel(discord, guild, model.Id); + var entity = new RestThreadChannel(discord, guild, model.Id, model.ThreadMetadata.GetValueOrDefault()?.CreatedAt.GetValueOrDefault()); entity.Update(model); return entity; } @@ -57,6 +66,7 @@ namespace Discord.Rest if (model.ThreadMetadata.IsSpecified) { + IsInvitable = model.ThreadMetadata.Value.Invitable.ToNullable(); IsArchived = model.ThreadMetadata.Value.Archived; AutoArchiveDuration = model.ThreadMetadata.Value.AutoArchiveDuration; ArchiveTimestamp = model.ThreadMetadata.Value.ArchiveTimestamp; diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 874d3c2cd..469e93db4 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -142,12 +142,54 @@ namespace Discord.Rest #endregion #region Bans - public static async Task> GetBansAsync(IGuild guild, BaseDiscordClient client, - RequestOptions options) + public static IAsyncEnumerable> GetBansAsync(IGuild guild, BaseDiscordClient client, + ulong? fromUserId, Direction dir, int limit, RequestOptions options) { - var models = await client.ApiClient.GetGuildBansAsync(guild.Id, options).ConfigureAwait(false); - return models.Select(x => RestBan.Create(client, x)).ToImmutableArray(); + if (dir == Direction.Around && limit > DiscordConfig.MaxBansPerBatch) + { + int around = limit / 2; + if (fromUserId.HasValue) + return GetBansAsync(guild, client, fromUserId.Value + 1, Direction.Before, around + 1, options) + .Concat(GetBansAsync(guild, client, fromUserId.Value, Direction.After, around, options)); + else + return GetBansAsync(guild, client, null, Direction.Before, around + 1, options); + } + + return new PagedAsyncEnumerable( + DiscordConfig.MaxBansPerBatch, + async (info, ct) => + { + var args = new GetGuildBansParams + { + RelativeDirection = dir, + Limit = info.PageSize + }; + if (info.Position != null) + args.RelativeUserId = info.Position.Value; + + var models = await client.ApiClient.GetGuildBansAsync(guild.Id, args, options).ConfigureAwait(false); + var builder = ImmutableArray.CreateBuilder(); + + foreach (var model in models) + builder.Add(RestBan.Create(client, model)); + + return builder.ToImmutable(); + }, + nextPage: (info, lastPage) => + { + if (lastPage.Count != DiscordConfig.MaxMessagesPerBatch) + return false; + if (dir == Direction.Before) + info.Position = lastPage.Min(x => x.User.Id); + else + info.Position = lastPage.Max(x => x.User.Id); + return true; + }, + start: fromUserId, + count: limit + ); } + public static async Task GetBanAsync(IGuild guild, BaseDiscordClient client, ulong userId, RequestOptions options) { var model = await client.ApiClient.GetGuildBanAsync(guild.Id, userId, options).ConfigureAwait(false); @@ -305,19 +347,15 @@ namespace Discord.Rest #endregion #region Integrations - public static async Task> GetIntegrationsAsync(IGuild guild, BaseDiscordClient client, + public static async Task> GetIntegrationsAsync(IGuild guild, BaseDiscordClient client, RequestOptions options) { - var models = await client.ApiClient.GetGuildIntegrationsAsync(guild.Id, options).ConfigureAwait(false); - return models.Select(x => RestGuildIntegration.Create(client, guild, x)).ToImmutableArray(); - } - public static async Task CreateIntegrationAsync(IGuild guild, BaseDiscordClient client, - ulong id, string type, RequestOptions options) - { - var args = new CreateGuildIntegrationParams(id, type); - var model = await client.ApiClient.CreateGuildIntegrationAsync(guild.Id, args, options).ConfigureAwait(false); - return RestGuildIntegration.Create(client, guild, model); + var models = await client.ApiClient.GetIntegrationsAsync(guild.Id, options).ConfigureAwait(false); + return models.Select(x => RestIntegration.Create(client, guild, x)).ToImmutableArray(); } + public static async Task DeleteIntegrationAsync(IGuild guild, BaseDiscordClient client, ulong id, + RequestOptions options) => + await client.ApiClient.DeleteIntegrationAsync(guild.Id, id, options).ConfigureAwait(false); #endregion #region Interactions @@ -799,7 +837,12 @@ namespace Discord.Rest PrivacyLevel = args.PrivacyLevel, StartTime = args.StartTime, Status = args.Status, - Type = args.Type + Type = args.Type, + Image = args.CoverImage.IsSpecified + ? args.CoverImage.Value.HasValue + ? args.CoverImage.Value.Value.ToModel() + : null + : Optional.Unspecified }; if(args.Location.IsSpecified) @@ -839,6 +882,7 @@ namespace Discord.Rest DateTimeOffset? endTime = null, ulong? channelId = null, string location = null, + Image? bannerImage = null, RequestOptions options = null) { if(location != null) @@ -864,6 +908,7 @@ namespace Discord.Rest if (endTime != null && endTime <= startTime) throw new ArgumentOutOfRangeException(nameof(endTime), $"{nameof(endTime)} cannot be before the start time"); + var apiArgs = new CreateGuildScheduledEventParams() { ChannelId = channelId ?? Optional.Unspecified, @@ -872,7 +917,8 @@ namespace Discord.Rest Name = name, PrivacyLevel = privacyLevel, StartTime = startTime, - Type = type + Type = type, + Image = bannerImage.HasValue ? bannerImage.Value.ToModel() : Optional.Unspecified }; if(location != null) diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index d90372636..92d598466 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -333,17 +333,18 @@ namespace Discord.Rest #endregion #region Bans - /// - /// Gets a collection of all users banned in this guild. - /// - /// The options to be used when sending the request. - /// - /// 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. - /// - public Task> GetBansAsync(RequestOptions options = null) - => GuildHelper.GetBansAsync(this, Discord, options); + + /// + public IAsyncEnumerable> GetBansAsync(int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null) + => GuildHelper.GetBansAsync(this, Discord, null, Direction.Before, limit, options); + + /// + public IAsyncEnumerable> GetBansAsync(ulong fromUserId, Direction dir, int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null) + => GuildHelper.GetBansAsync(this, Discord, fromUserId, dir, limit, options); + + /// + public IAsyncEnumerable> GetBansAsync(IUser fromUser, Direction dir, int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null) + => GuildHelper.GetBansAsync(this, Discord, fromUser.Id, dir, limit, options); /// /// Gets a ban object for a banned user. /// @@ -720,10 +721,10 @@ namespace Discord.Rest #endregion #region Integrations - public Task> GetIntegrationsAsync(RequestOptions options = null) + public Task> GetIntegrationsAsync(RequestOptions options = null) => GuildHelper.GetIntegrationsAsync(this, Discord, options); - public Task CreateIntegrationAsync(ulong id, string type, RequestOptions options = null) - => GuildHelper.CreateIntegrationAsync(this, Discord, id, type, options); + public Task DeleteIntegrationAsync(ulong id, RequestOptions options = null) + => GuildHelper.DeleteIntegrationAsync(this, Discord, id, options); #endregion #region Invites @@ -763,11 +764,6 @@ namespace Discord.Rest return null; } - /// - public Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), - bool isHoisted = false, RequestOptions options = null) - => CreateRoleAsync(name, permissions, color, isHoisted, false, options); - /// /// Creates a new role with the provided name. /// @@ -1167,6 +1163,7 @@ namespace Discord.Rest /// /// A collection of speakers for the event. /// The location of the event; links are supported + /// The optional banner image for the event. /// The options to be used when sending the request. /// /// A task that represents the asynchronous create operation. @@ -1180,8 +1177,9 @@ namespace Discord.Rest DateTimeOffset? endTime = null, ulong? channelId = null, string location = null, + Image? coverImage = null, RequestOptions options = null) - => GuildHelper.CreateGuildEventAsync(Discord, this, name, privacyLevel, startTime, type, description, endTime, channelId, location, options); + => GuildHelper.CreateGuildEventAsync(Discord, this, name, privacyLevel, startTime, type, description, endTime, channelId, location, coverImage, options); #endregion @@ -1196,22 +1194,24 @@ namespace Discord.Rest IReadOnlyCollection IGuild.Roles => Roles; IReadOnlyCollection IGuild.Stickers => Stickers; - /// - async Task IGuild.CreateEventAsync(string name, DateTimeOffset startTime, GuildScheduledEventType type, GuildScheduledEventPrivacyLevel privacyLevel, string description, DateTimeOffset? endTime, ulong? channelId, string location, RequestOptions options) - => await CreateEventAsync(name, startTime, type, privacyLevel, description, endTime, channelId, location, options).ConfigureAwait(false); - + async Task IGuild.CreateEventAsync(string name, DateTimeOffset startTime, GuildScheduledEventType type, GuildScheduledEventPrivacyLevel privacyLevel, string description, DateTimeOffset? endTime, ulong? channelId, string location, Image? coverImage, RequestOptions options) + => await CreateEventAsync(name, startTime, type, privacyLevel, description, endTime, channelId, location, coverImage, options).ConfigureAwait(false); /// async Task IGuild.GetEventAsync(ulong id, RequestOptions options) => await GetEventAsync(id, options).ConfigureAwait(false); - /// async Task> IGuild.GetEventsAsync(RequestOptions options) => await GetEventsAsync(options).ConfigureAwait(false); - /// - async Task> IGuild.GetBansAsync(RequestOptions options) - => await GetBansAsync(options).ConfigureAwait(false); + IAsyncEnumerable> IGuild.GetBansAsync(int limit, RequestOptions options) + => GetBansAsync(limit, options); + /// + IAsyncEnumerable> IGuild.GetBansAsync(ulong fromUserId, Direction dir, int limit, RequestOptions options) + => GetBansAsync(fromUserId, dir, limit, options); + /// + IAsyncEnumerable> IGuild.GetBansAsync(IUser fromUser, Direction dir, int limit, RequestOptions options) + => GetBansAsync(fromUser, dir, limit, options); /// async Task IGuild.GetBanAsync(IUser user, RequestOptions options) => await GetBanAsync(user, options).ConfigureAwait(false); @@ -1373,11 +1373,11 @@ namespace Discord.Rest => await GetVoiceRegionsAsync(options).ConfigureAwait(false); /// - async Task> IGuild.GetIntegrationsAsync(RequestOptions options) + async Task> IGuild.GetIntegrationsAsync(RequestOptions options) => await GetIntegrationsAsync(options).ConfigureAwait(false); /// - async Task IGuild.CreateIntegrationAsync(ulong id, string type, RequestOptions options) - => await CreateIntegrationAsync(id, type, options).ConfigureAwait(false); + async Task IGuild.DeleteIntegrationAsync(ulong id, RequestOptions options) + => await DeleteIntegrationAsync(id, options).ConfigureAwait(false); /// async Task> IGuild.GetInvitesAsync(RequestOptions options) diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuildEvent.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuildEvent.cs index d3ec11fc6..0b02e60ba 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuildEvent.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuildEvent.cs @@ -28,6 +28,9 @@ namespace Discord.Rest /// public string Description { get; private set; } + /// + public string CoverImageId { get; private set; } + /// public DateTimeOffset StartTime { get; private set; } @@ -98,8 +101,13 @@ namespace Discord.Rest EntityId = model.EntityId; Location = model.EntityMetadata?.Location.GetValueOrDefault(); UserCount = model.UserCount.ToNullable(); + CoverImageId = model.Image; } + /// + public string GetCoverImageUrl(ImageFormat format = ImageFormat.Auto, ushort size = 1024) + => CDN.GetEventCoverImageUrl(Guild.Id, Id, CoverImageId, format, size); + /// public Task StartAsync(RequestOptions options = null) => ModifyAsync(x => x.Status = GuildScheduledEventStatus.Active); diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs deleted file mode 100644 index 9759e64d2..000000000 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using System.Diagnostics; -using System.Threading.Tasks; -using Model = Discord.API.Integration; - -namespace Discord.Rest -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class RestGuildIntegration : RestEntity, IGuildIntegration - { - private long _syncedAtTicks; - - /// - public string Name { get; private set; } - /// - public string Type { get; private set; } - /// - public bool IsEnabled { get; private set; } - /// - public bool IsSyncing { get; private set; } - /// - public ulong ExpireBehavior { get; private set; } - /// - public ulong ExpireGracePeriod { get; private set; } - /// - public ulong GuildId { get; private set; } - /// - public ulong RoleId { get; private set; } - public RestUser User { get; private set; } - /// - public IntegrationAccount Account { get; private set; } - internal IGuild Guild { get; private set; } - - /// - public DateTimeOffset SyncedAt => DateTimeUtils.FromTicks(_syncedAtTicks); - - internal RestGuildIntegration(BaseDiscordClient discord, IGuild guild, ulong id) - : base(discord, id) - { - Guild = guild; - } - internal static RestGuildIntegration Create(BaseDiscordClient discord, IGuild guild, Model model) - { - var entity = new RestGuildIntegration(discord, guild, model.Id); - entity.Update(model); - return entity; - } - - internal void Update(Model model) - { - Name = model.Name; - Type = model.Type; - IsEnabled = model.Enabled; - IsSyncing = model.Syncing; - ExpireBehavior = model.ExpireBehavior; - ExpireGracePeriod = model.ExpireGracePeriod; - _syncedAtTicks = model.SyncedAt.UtcTicks; - - RoleId = model.RoleId; - User = RestUser.Create(Discord, model.User); - } - - public async Task DeleteAsync() - { - await Discord.ApiClient.DeleteGuildIntegrationAsync(GuildId, Id).ConfigureAwait(false); - } - public async Task ModifyAsync(Action func) - { - if (func == null) throw new NullReferenceException(nameof(func)); - - var args = new GuildIntegrationProperties(); - func(args); - var apiArgs = new API.Rest.ModifyGuildIntegrationParams - { - EnableEmoticons = args.EnableEmoticons, - ExpireBehavior = args.ExpireBehavior, - ExpireGracePeriod = args.ExpireGracePeriod - }; - var model = await Discord.ApiClient.ModifyGuildIntegrationAsync(GuildId, Id, apiArgs).ConfigureAwait(false); - - Update(model); - } - public async Task SyncAsync() - { - await Discord.ApiClient.SyncGuildIntegrationAsync(GuildId, Id).ConfigureAwait(false); - } - - public override string ToString() => Name; - private string DebuggerDisplay => $"{Name} ({Id}{(IsEnabled ? ", Enabled" : "")})"; - - /// - IGuild IGuildIntegration.Guild - { - get - { - if (Guild != null) - return Guild; - throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); - } - } - /// - IUser IGuildIntegration.User => User; - } -} diff --git a/src/Discord.Net.Rest/Entities/Integrations/RestIntegration.cs b/src/Discord.Net.Rest/Entities/Integrations/RestIntegration.cs new file mode 100644 index 000000000..e92ecdded --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Integrations/RestIntegration.cs @@ -0,0 +1,102 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.Integration; + +namespace Discord.Rest +{ + /// + /// Represents a Rest-based implementation of . + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestIntegration : RestEntity, IIntegration + { + private long? _syncedAtTicks; + + /// + public string Name { get; private set; } + /// + public string Type { get; private set; } + /// + public bool IsEnabled { get; private set; } + /// + public bool? IsSyncing { get; private set; } + /// + public ulong? RoleId { get; private set; } + /// + public bool? HasEnabledEmoticons { get; private set; } + /// + public IntegrationExpireBehavior? ExpireBehavior { get; private set; } + /// + public int? ExpireGracePeriod { get; private set; } + /// + IUser IIntegration.User => User; + /// + public IIntegrationAccount Account { get; private set; } + /// + public DateTimeOffset? SyncedAt => DateTimeUtils.FromTicks(_syncedAtTicks); + /// + public int? SubscriberCount { get; private set; } + /// + public bool? IsRevoked { get; private set; } + /// + public IIntegrationApplication Application { get; private set; } + + internal IGuild Guild { get; private set; } + public RestUser User { get; private set; } + + internal RestIntegration(BaseDiscordClient discord, IGuild guild, ulong id) + : base(discord, id) + { + Guild = guild; + } + internal static RestIntegration Create(BaseDiscordClient discord, IGuild guild, Model model) + { + var entity = new RestIntegration(discord, guild, model.Id); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + Name = model.Name; + Type = model.Type; + IsEnabled = model.Enabled; + + IsSyncing = model.Syncing.IsSpecified ? model.Syncing.Value : null; + RoleId = model.RoleId.IsSpecified ? model.RoleId.Value : null; + HasEnabledEmoticons = model.EnableEmoticons.IsSpecified ? model.EnableEmoticons.Value : null; + ExpireBehavior = model.ExpireBehavior.IsSpecified ? model.ExpireBehavior.Value : null; + ExpireGracePeriod = model.ExpireGracePeriod.IsSpecified ? model.ExpireGracePeriod.Value : null; + User = model.User.IsSpecified ? RestUser.Create(Discord, model.User.Value) : null; + Account = model.Account.IsSpecified ? RestIntegrationAccount.Create(model.Account.Value) : null; + SubscriberCount = model.SubscriberAccount.IsSpecified ? model.SubscriberAccount.Value : null; + IsRevoked = model.Revoked.IsSpecified ? model.Revoked.Value : null; + Application = model.Application.IsSpecified ? RestIntegrationApplication.Create(Discord, model.Application.Value) : null; + + _syncedAtTicks = model.SyncedAt.IsSpecified ? model.SyncedAt.Value.UtcTicks : null; + } + + public async Task DeleteAsync() + { + await Discord.ApiClient.DeleteIntegrationAsync(GuildId, Id).ConfigureAwait(false); + } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id}{(IsEnabled ? ", Enabled" : "")})"; + + /// + public ulong GuildId { get; private set; } + + /// + IGuild IIntegration.Guild + { + get + { + if (Guild != null) + return Guild; + throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); + } + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Integrations/RestIntegrationAccount.cs b/src/Discord.Net.Rest/Entities/Integrations/RestIntegrationAccount.cs new file mode 100644 index 000000000..6d83aa1f0 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Integrations/RestIntegrationAccount.cs @@ -0,0 +1,29 @@ +using Model = Discord.API.IntegrationAccount; + +namespace Discord.Rest +{ + /// + /// Represents a Rest-based implementation of . + /// + public class RestIntegrationAccount : IIntegrationAccount + { + internal RestIntegrationAccount() { } + + public string Id { get; private set; } + + public string Name { get; private set; } + + internal static RestIntegrationAccount Create(Model model) + { + var entity = new RestIntegrationAccount(); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + model.Name = Name; + model.Id = Id; + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Integrations/RestIntegrationApplication.cs b/src/Discord.Net.Rest/Entities/Integrations/RestIntegrationApplication.cs new file mode 100644 index 000000000..e532ac970 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Integrations/RestIntegrationApplication.cs @@ -0,0 +1,39 @@ +using Model = Discord.API.IntegrationApplication; + +namespace Discord.Rest +{ + /// + /// Represents a Rest-based implementation of . + /// + public class RestIntegrationApplication : RestEntity, IIntegrationApplication + { + public string Name { get; private set; } + + public string Icon { get; private set; } + + public string Description { get; private set; } + + public string Summary { get; private set; } + + public IUser Bot { get; private set; } + + internal RestIntegrationApplication(BaseDiscordClient discord, ulong id) + : base(discord, id) { } + + internal static RestIntegrationApplication Create(BaseDiscordClient discord, Model model) + { + var entity = new RestIntegrationApplication(discord, model.Id); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + Name = model.Name; + Icon = model.Icon.IsSpecified ? model.Icon.Value : null; + Description = model.Description; + Summary = model.Summary; + Bot = RestUser.Create(Discord, model.Bot.Value); + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs index 359b92249..002510eac 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs @@ -492,5 +492,13 @@ namespace Discord.Rest /// IUserMessage IComponentInteraction.Message => Message; + + /// + Task IComponentInteraction.UpdateAsync(Action func, RequestOptions options) + => Task.FromResult(Update(func, options)); + + /// + Task IComponentInteraction.DeferLoadingAsync(bool ephemeral, RequestOptions options) + => Task.FromResult(DeferLoading(ephemeral, options)); } } diff --git a/src/Discord.Net.Rest/Entities/Messages/Attachment.cs b/src/Discord.Net.Rest/Entities/Messages/Attachment.cs index 4e4849c51..a5b83fb7b 100644 --- a/src/Discord.Net.Rest/Entities/Messages/Attachment.cs +++ b/src/Discord.Net.Rest/Entities/Messages/Attachment.cs @@ -23,8 +23,13 @@ namespace Discord public int? Width { get; } /// public bool Ephemeral { get; } + /// + public string Description { get; } + /// + public string ContentType { get; } - internal Attachment(ulong id, string filename, string url, string proxyUrl, int size, int? height, int? width, bool? ephemeral) + internal Attachment(ulong id, string filename, string url, string proxyUrl, int size, int? height, int? width, + bool? ephemeral, string description, string contentType) { Id = id; Filename = filename; @@ -34,13 +39,16 @@ namespace Discord Height = height; Width = width; Ephemeral = ephemeral.GetValueOrDefault(false); + Description = description; + ContentType = contentType; } internal static Attachment Create(Model model) { return new Attachment(model.Id, model.Filename, model.Url, model.ProxyUrl, model.Size, model.Height.IsSpecified ? model.Height.Value : (int?)null, model.Width.IsSpecified ? model.Width.Value : (int?)null, - model.Ephemeral.ToNullable()); + model.Ephemeral.ToNullable(), model.Description.GetValueOrDefault(), + model.ContentType.GetValueOrDefault()); } /// diff --git a/src/Discord.Net.Rest/Entities/Users/RestConnection.cs b/src/Discord.Net.Rest/Entities/Users/RestConnection.cs index 1afb813c0..496279727 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestConnection.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestConnection.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; using System.Collections.Immutable; +using System.Collections.ObjectModel; using System.Diagnostics; +using System.Linq; using Model = Discord.API.Connection; namespace Discord.Rest @@ -9,28 +11,49 @@ namespace Discord.Rest public class RestConnection : IConnection { /// - public string Id { get; } + public string Id { get; private set; } /// - public string Type { get; } + public string Name { get; private set; } /// - public string Name { get; } + public string Type { get; private set; } /// - public bool IsRevoked { get; } + public bool? IsRevoked { get; private set; } /// - public IReadOnlyCollection IntegrationIds { get; } + public IReadOnlyCollection Integrations { get; private set; } + /// + public bool Verified { get; private set; } + /// + public bool FriendSync { get; private set; } + /// + public bool ShowActivity { get; private set; } + /// + public ConnectionVisibility Visibility { get; private set; } - internal RestConnection(string id, string type, string name, bool isRevoked, IReadOnlyCollection integrationIds) - { - Id = id; - Type = type; - Name = name; - IsRevoked = isRevoked; + internal BaseDiscordClient Discord { get; } - IntegrationIds = integrationIds; + internal RestConnection(BaseDiscordClient discord) { + Discord = discord; } - internal static RestConnection Create(Model model) + + internal static RestConnection Create(BaseDiscordClient discord, Model model) + { + var entity = new RestConnection(discord); + entity.Update(model); + return entity; + } + + internal void Update(Model model) { - return new RestConnection(model.Id, model.Type, model.Name, model.Revoked, model.Integrations.ToImmutableArray()); + Id = model.Id; + Name = model.Name; + Type = model.Type; + IsRevoked = model.Revoked.IsSpecified ? model.Revoked.Value : null; + Integrations = model.Integrations.IsSpecified ?model.Integrations.Value + .Select(intergration => RestIntegration.Create(Discord, null, intergration)).ToImmutableArray() : null; + Verified = model.Verified; + FriendSync = model.FriendSync; + ShowActivity = model.ShowActivity; + Visibility = model.Visibility; } /// @@ -40,6 +63,6 @@ namespace Discord.Rest /// Name of the connection. /// public override string ToString() => Name; - private string DebuggerDisplay => $"{Name} ({Id}, {Type}{(IsRevoked ? ", Revoked" : "")})"; + private string DebuggerDisplay => $"{Name} ({Id}, {Type}{(IsRevoked.GetValueOrDefault() ? ", Revoked" : "")})"; } } diff --git a/src/Discord.Net.Rest/Entities/Users/RestUser.cs b/src/Discord.Net.Rest/Entities/Users/RestUser.cs index 70f990fe7..dfdb53815 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -129,8 +129,10 @@ namespace Discord.Rest /// /// A string that resolves to Username#Discriminator of the user. /// - public override string ToString() => Format.UsernameAndDiscriminator(this); - private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this)} ({Id}{(IsBot ? ", Bot" : "")})"; + public override string ToString() + => Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode); + + private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode)} ({Id}{(IsBot ? ", Bot" : "")})"; #endregion #region IUser diff --git a/src/Discord.Net.WebSocket/API/Gateway/IntegrationDeletedEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/IntegrationDeletedEvent.cs new file mode 100644 index 000000000..cf6e70ca6 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Gateway/IntegrationDeletedEvent.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class IntegrationDeletedEvent + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("application_id")] + public Optional ApplicationID { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs index 134f8136b..c47591418 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs @@ -415,6 +415,32 @@ namespace Discord.WebSocket #endregion + #region Integrations + /// Fired when an integration is created. + public event Func IntegrationCreated + { + add { _integrationCreated.Add(value); } + remove { _integrationCreated.Remove(value); } + } + internal readonly AsyncEvent> _integrationCreated = new AsyncEvent>(); + + /// Fired when an integration is updated. + public event Func IntegrationUpdated + { + add { _integrationUpdated.Add(value); } + remove { _integrationUpdated.Remove(value); } + } + internal readonly AsyncEvent> _integrationUpdated = new AsyncEvent>(); + + /// Fired when an integration is deleted. + public event Func, Task> IntegrationDeleted + { + add { _integrationDeleted.Add(value); } + remove { _integrationDeleted.Remove(value); } + } + internal readonly AsyncEvent, Task>> _integrationDeleted = new AsyncEvent, Task>>(); + #endregion + #region Users /// Fired when a user joins a guild. public event Func UserJoined @@ -451,7 +477,7 @@ namespace Discord.WebSocket remove { _userUpdatedEvent.Remove(value); } } internal readonly AsyncEvent> _userUpdatedEvent = new AsyncEvent>(); - /// Fired when a guild member is updated, or a member presence is updated. + /// Fired when a guild member is updated. public event Func, SocketGuildUser, Task> GuildMemberUpdated { add { _guildMemberUpdatedEvent.Add(value); } diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.cs b/src/Discord.Net.WebSocket/BaseSocketClient.cs index 20acd85dd..bb2d489b4 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.cs @@ -209,7 +209,7 @@ namespace Discord.WebSocket /// Sets the of the logged-in user. /// /// - /// This method sets the of the user. + /// This method sets the of the user. /// /// Discord will only accept setting of name and the type of activity. /// @@ -219,7 +219,7 @@ namespace Discord.WebSocket /// /// /// Rich Presence cannot be set via this method or client. Rich Presence is strictly limited to RPC - /// clients only. + /// clients only. /// /// /// The activity to be set. @@ -240,7 +240,7 @@ namespace Discord.WebSocket /// Creates a guild for the logged-in user who is in less than 10 active guilds. /// /// - /// This method creates a new guild on behalf of the logged-in user. + /// This method creates a new guild on behalf of the logged-in user. /// /// Due to Discord's limitation, this method will only work for users that are in less than 10 guilds. /// @@ -317,8 +317,15 @@ namespace Discord.WebSocket => await CreateGuildAsync(name, region, jpegIcon, options).ConfigureAwait(false); /// - Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetUser(id)); + async Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + var user = GetUser(id); + if (user is not null || mode == CacheMode.CacheOnly) + return user; + + return await Rest.GetUserAsync(id, options).ConfigureAwait(false); + } + /// Task IDiscordClient.GetUserAsync(string username, string discriminator, RequestOptions options) => Task.FromResult(GetUser(username, discriminator)); diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index 51c6d3c34..3a14692e0 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -178,7 +178,6 @@ namespace Discord.WebSocket await _shards[i].LogoutAsync(); } - CurrentUser = null; if (_automaticShards) { _shardIds = new int[0]; @@ -450,6 +449,7 @@ namespace Discord.WebSocket client.UserBanned += (user, guild) => _userBannedEvent.InvokeAsync(user, guild); client.UserUnbanned += (user, guild) => _userUnbannedEvent.InvokeAsync(user, guild); client.UserUpdated += (oldUser, newUser) => _userUpdatedEvent.InvokeAsync(oldUser, newUser); + client.PresenceUpdated += (user, oldPresence, newPresence) => _presenceUpdated.InvokeAsync(user, oldPresence, newPresence); client.GuildMemberUpdated += (oldUser, newUser) => _guildMemberUpdatedEvent.InvokeAsync(oldUser, newUser); client.UserVoiceStateUpdated += (user, oldVoiceState, newVoiceState) => _userVoiceStateUpdatedEvent.InvokeAsync(user, oldVoiceState, newVoiceState); client.VoiceServerUpdated += (server) => _voiceServerUpdatedEvent.InvokeAsync(server); @@ -533,8 +533,15 @@ namespace Discord.WebSocket => await CreateGuildAsync(name, region, jpegIcon).ConfigureAwait(false); /// - Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetUser(id)); + async Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + var user = GetUser(id); + if (user is not null || mode == CacheMode.CacheOnly) + return user; + + return await Rest.GetUserAsync(id, options).ConfigureAwait(false); + } + /// Task IDiscordClient.GetUserAsync(string username, string discriminator, RequestOptions options) => Task.FromResult(GetUser(username, discriminator)); diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index b0215d9ef..aaef4656a 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -78,6 +78,7 @@ namespace Discord.WebSocket internal bool AlwaysDownloadDefaultStickers { get; private set; } internal bool AlwaysResolveStickers { get; private set; } internal bool LogGatewayIntentWarnings { get; private set; } + internal bool SuppressUnknownDispatchWarnings { get; private set; } internal new DiscordSocketApiClient ApiClient => base.ApiClient; /// public override IReadOnlyCollection Guilds => State.Guilds; @@ -150,6 +151,7 @@ namespace Discord.WebSocket AlwaysDownloadDefaultStickers = config.AlwaysDownloadDefaultStickers; AlwaysResolveStickers = config.AlwaysResolveStickers; LogGatewayIntentWarnings = config.LogGatewayIntentWarnings; + SuppressUnknownDispatchWarnings = config.SuppressUnknownDispatchWarnings; HandlerTimeout = config.HandlerTimeout; State = new ClientState(0, 0); Rest = new DiscordSocketRestClient(config, ApiClient); @@ -543,7 +545,7 @@ namespace Discord.WebSocket if(model == null) return null; - + if (model.GuildId.IsSpecified) { var guild = State.GetGuild(model.GuildId.Value); @@ -1309,7 +1311,7 @@ namespace Discord.WebSocket else { user = guild.AddOrUpdateUser(data); - var cacheableBefore = new Cacheable(user, user.Id, true, () => null); + var cacheableBefore = new Cacheable(null, user.Id, false, () => null); await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false); } } @@ -2015,6 +2017,92 @@ namespace Discord.WebSocket break; #endregion + #region Integrations + case "INTEGRATION_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (INTEGRATION_CREATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + // Integrations from Gateway should always have guild IDs specified. + if (!data.GuildId.IsSpecified) + return; + + var guild = State.GetGuild(data.GuildId.Value); + + if (guild != null) + { + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + await TimedInvokeAsync(_integrationCreated, nameof(IntegrationCreated), RestIntegration.Create(this, guild, data)).ConfigureAwait(false); + } + else + { + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } + } + break; + case "INTEGRATION_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (INTEGRATION_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + // Integrations from Gateway should always have guild IDs specified. + if (!data.GuildId.IsSpecified) + return; + + var guild = State.GetGuild(data.GuildId.Value); + + if (guild != null) + { + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + await TimedInvokeAsync(_integrationUpdated, nameof(IntegrationUpdated), RestIntegration.Create(this, guild, data)).ConfigureAwait(false); + } + else + { + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } + } + break; + case "INTEGRATION_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (INTEGRATION_DELETE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + + if (guild != null) + { + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + await TimedInvokeAsync(_integrationDeleted, nameof(IntegrationDeleted), guild, data.Id, data.ApplicationID).ConfigureAwait(false); + } + else + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + } + break; + #endregion + #region Users case "USER_UPDATE": { @@ -2128,7 +2216,7 @@ namespace Discord.WebSocket { await TimedInvokeAsync(_speakerRemoved, nameof(SpeakerRemoved), stage, guildUser); } - } + } } await TimedInvokeAsync(_userVoiceStateUpdatedEvent, nameof(UserVoiceStateUpdated), user, before, after).ConfigureAwait(false); @@ -2243,7 +2331,7 @@ namespace Discord.WebSocket SocketUser user = data.User.IsSpecified ? State.GetOrAddUser(data.User.Value.Id, (_) => SocketGlobalUser.Create(this, State, data.User.Value)) - : guild.AddOrUpdateUser(data.Member.Value); + : guild?.AddOrUpdateUser(data.Member.Value); // null if the bot scope isn't set, so the guild cannot be retrieved. SocketChannel channel = null; if(data.ChannelId.IsSpecified) @@ -2258,8 +2346,12 @@ namespace Discord.WebSocket } else { - await UnknownChannelAsync(type, data.ChannelId.Value).ConfigureAwait(false); - return; + if (guild != null) // The guild id is set, but the guild cannot be found as the bot scope is not set. + { + await UnknownChannelAsync(type, data.ChannelId.Value).ConfigureAwait(false); + return; + } + // The channel isnt required when responding to an interaction, so we can leave the channel null. } } } @@ -2520,7 +2612,7 @@ namespace Discord.WebSocket } break; - case "THREAD_MEMBERS_UPDATE": + case "THREAD_MEMBERS_UPDATE": { await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_MEMBERS_UPDATE)").ConfigureAwait(false); @@ -2771,7 +2863,7 @@ namespace Discord.WebSocket #region Others default: - await _gatewayLogger.WarningAsync($"Unknown Dispatch ({type})").ConfigureAwait(false); + if(!SuppressUnknownDispatchWarnings) await _gatewayLogger.WarningAsync($"Unknown Dispatch ({type})").ConfigureAwait(false); break; #endregion } @@ -3113,7 +3205,14 @@ namespace Discord.WebSocket /// async Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => mode == CacheMode.AllowDownload ? await GetUserAsync(id, options).ConfigureAwait(false) : GetUser(id); + { + var user = GetUser(id); + if (user is not null || mode == CacheMode.CacheOnly) + return user; + + return await Rest.GetUserAsync(id, options).ConfigureAwait(false); + } + /// Task IDiscordClient.GetUserAsync(string username, string discriminator, RequestOptions options) => Task.FromResult(GetUser(username, discriminator)); diff --git a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs index f0e6dc857..4cd64dbc2 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs @@ -188,6 +188,11 @@ namespace Discord.WebSocket /// public bool LogGatewayIntentWarnings { get; set; } = true; + /// + /// Gets or sets whether or not Unknown Dispatch event messages should be logged. + /// + public bool SuppressUnknownDispatchWarnings { get; set; } = true; + /// /// Initializes a new instance of the class with the default configuration. /// diff --git a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs index 3e9b635de..b632bcb60 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs @@ -18,83 +18,22 @@ namespace Discord.WebSocket /// IReadOnlyCollection CachedMessages { get; } - /// - /// Sends a message to this message channel. - /// - /// - /// This method follows the same behavior as described in . - /// Please visit its documentation for more details on this method. - /// - /// The message to be sent. - /// Determines whether the message should be read aloud by Discord or not. - /// The to be sent. - /// The options to be used when sending the request. - /// - /// Specifies if notifications are sent for mentioned users and roles in the message . - /// If null, all mentioned roles and users will be notified. - /// - /// The message references to be included. Used to reply to specific messages. - /// The message components to be included with this message. Used for interactions. - /// A collection of stickers to send with the message. - /// A array of s to send with this response. Max 10. - /// - /// A task that represents an asynchronous send operation for delivering the message. The task result - /// contains the sent message. - /// - new Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null); - /// - /// Sends a file to this message channel with an optional caption. - /// - /// - /// This method follows the same behavior as described in . - /// Please visit its documentation for more details on this method. - /// - /// The file path of the file. - /// The message to be sent. - /// Whether the message should be read aloud by Discord or not. - /// The to be sent. - /// The options to be used when sending the request. - /// Whether the message attachment should be hidden as a spoiler. - /// - /// Specifies if notifications are sent for mentioned users and roles in the message . - /// If null, all mentioned roles and users will be notified. - /// - /// The message references to be included. Used to reply to specific messages. - /// The message components to be included with this message. Used for interactions. - /// A collection of stickers to send with the file. - /// A array of s to send with this response. Max 10. - /// - /// A task that represents an asynchronous send operation for delivering the message. The task result - /// contains the sent message. - /// - new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null); - /// - /// Sends a file to this message channel with an optional caption. - /// - /// - /// This method follows the same behavior as described in . - /// Please visit its documentation for more details on this method. - /// - /// The of the file to be sent. - /// The name of the attachment. - /// The message to be sent. - /// Whether the message should be read aloud by Discord or not. - /// The to be sent. - /// The options to be used when sending the request. - /// Whether the message attachment should be hidden as a spoiler. - /// - /// Specifies if notifications are sent for mentioned users and roles in the message . - /// If null, all mentioned roles and users will be notified. - /// - /// The message references to be included. Used to reply to specific messages. - /// The message components to be included with this message. Used for interactions. - /// A collection of stickers to send with the file. - /// A array of s to send with this response. Max 10. - /// - /// A task that represents an asynchronous send operation for delivering the message. The task result - /// contains the sent message. - /// - new Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null); + /// + new Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, + RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); + + /// + new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, + RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None); + + /// + new Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, + Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None); /// /// Gets a cached message from this channel. diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs index 9c7dd4fbd..43f23de1a 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; +using Discord.Rest; using Model = Discord.API.Channel; namespace Discord.WebSocket @@ -64,21 +65,44 @@ namespace Discord.WebSocket #endregion #region IGuildChannel + /// - IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, + RequestOptions options) + { + return mode == CacheMode.AllowDownload + ? ChannelHelper.GetUsersAsync(this, Guild, Discord, null, null, options) + : ImmutableArray.Create>(Users).ToAsyncEnumerable(); + } /// - Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetUser(id)); + async Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + var user = GetUser(id); + if (user is not null || mode == CacheMode.CacheOnly) + return user; + + return await ChannelHelper.GetUserAsync(this, Guild, Discord, id, options).ConfigureAwait(false); + } #endregion #region IChannel + /// IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + { + return mode == CacheMode.AllowDownload + ? ChannelHelper.GetUsersAsync(this, Guild, Discord, null, null, options) + : ImmutableArray.Create>(Users).ToAsyncEnumerable(); + } /// - Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetUser(id)); + async Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + var user = GetUser(id); + if (user is not null || mode == CacheMode.CacheOnly) + return user; + + return await ChannelHelper.GetUserAsync(this, Guild, Discord, id, options).ConfigureAwait(false); + } #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs index 758ee9271..c30b3d254 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs @@ -17,7 +17,7 @@ namespace Discord.WebSocket /// /// Gets when the channel is created. /// - public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + public virtual DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); /// /// Gets a collection of users from the WebSocket cache. /// diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs index f4fe12755..17ab4ebe3 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs @@ -139,24 +139,48 @@ namespace Discord.WebSocket /// /// Message content is too long, length must be less or equal to . - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, + RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); /// - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); + /// The only valid are and . + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, + RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); + /// The only valid are and . + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, embeds, flags); /// public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) @@ -255,20 +279,37 @@ namespace Discord.WebSocket async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); /// - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, + RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, + Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, + Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, + stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, + bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, + AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, + ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags).ConfigureAwait(false); #endregion #region IChannel diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index c8137784f..4f068cf81 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -178,24 +178,48 @@ namespace Discord.WebSocket /// /// Message content is too long, length must be less or equal to . - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, + RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); /// - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); + /// The only valid are and . + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, + RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); + /// The only valid are and . + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, embeds, flags); /// public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) @@ -327,21 +351,37 @@ namespace Discord.WebSocket => await GetPinnedMessagesAsync(options).ConfigureAwait(false); /// - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, + RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, + Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, + Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, + stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, + bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags).ConfigureAwait(false); /// - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, + AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, + ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags).ConfigureAwait(false); #endregion #region IAudioChannel @@ -352,7 +392,7 @@ namespace Discord.WebSocket Task IAudioChannel.ModifyAsync(Action func, RequestOptions options) { throw new NotSupportedException(); } #endregion - #region IChannel + #region IChannel /// Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs index 45eb28444..79f02fe1c 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -214,10 +214,10 @@ namespace Discord.WebSocket /// IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + => ImmutableArray.Create>(Users).ToAsyncEnumerable(); //Overridden in Text/Voice /// Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetUser(id)); + => Task.FromResult(GetUser(id)); //Overridden in Text/Voice #endregion #region IChannel diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index dbf238625..e4a299edc 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -103,7 +103,7 @@ namespace Discord.WebSocket /// The duration on which this thread archives after. /// /// Note: Options and - /// are only available for guilds that are boosted. You can check in the to see if the + /// are only available for guilds that are boosted. You can check in the to see if the /// guild has the THREE_DAY_THREAD_ARCHIVE and SEVEN_DAY_THREAD_ARCHIVE. /// /// @@ -212,27 +212,48 @@ namespace Discord.WebSocket /// /// Message content is too long, length must be less or equal to . - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); - - /// - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); - + /// The only valid are and . + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, + RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); + + /// + /// The only valid are and . + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, + RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); - + /// The only valid are and . + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); - + /// The only valid are and . + public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, embeds, flags); /// public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) @@ -355,11 +376,22 @@ namespace Discord.WebSocket #region IGuildChannel /// - Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetUser(id)); + async Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + var user = GetUser(id); + if (user is not null || mode == CacheMode.CacheOnly) + return user; + + return await ChannelHelper.GetUserAsync(this, Guild, Discord, id, options).ConfigureAwait(false); + } /// IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + { + return mode == CacheMode.AllowDownload + ? ChannelHelper.GetUsersAsync(this, Guild, Discord, null, null, options) + : ImmutableArray.Create>(Users).ToAsyncEnumerable(); + } + #endregion #region IMessageChannel @@ -385,20 +417,38 @@ namespace Discord.WebSocket => await GetPinnedMessagesAsync(options).ConfigureAwait(false); /// - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, + RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, + Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, + Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, + stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, + bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, + AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, + ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags).ConfigureAwait(false); + #endregion #region INestedChannel diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs index 7fcafc14a..78462b062 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs @@ -24,7 +24,29 @@ namespace Discord.WebSocket /// /// Gets the owner of the current thread. /// - public SocketThreadUser Owner { get; private set; } + public SocketThreadUser Owner + { + get + { + lock (_ownerLock) + { + var user = GetUser(_ownerId); + + if (user == null) + { + var guildMember = Guild.GetUser(_ownerId); + if (guildMember == null) + return null; + + user = SocketThreadUser.Create(Guild, this, guildMember); + _members[user.Id] = user; + return user; + } + else + return user; + } + } + } /// /// Gets the current users within this thread. @@ -44,7 +66,7 @@ namespace Discord.WebSocket /// /// Gets the parent channel this thread resides in. /// - public SocketTextChannel ParentChannel { get; private set; } + public SocketGuildChannel ParentChannel { get; private set; } /// public int MessageCount { get; private set; } @@ -64,6 +86,12 @@ namespace Discord.WebSocket /// public bool IsLocked { get; private set; } + /// + public bool? IsInvitable { get; private set; } + + /// + public override DateTimeOffset CreatedAt { get; } + /// /// Gets a collection of cached users within this thread. /// @@ -77,18 +105,23 @@ namespace Discord.WebSocket private bool _usersDownloaded; private readonly object _downloadLock = new object(); + private readonly object _ownerLock = new object(); + + private ulong _ownerId; - internal SocketThreadChannel(DiscordSocketClient discord, SocketGuild guild, ulong id, SocketTextChannel parent) + internal SocketThreadChannel(DiscordSocketClient discord, SocketGuild guild, ulong id, SocketGuildChannel parent, + DateTimeOffset? createdAt) : base(discord, id, guild) { ParentChannel = parent; _members = new ConcurrentDictionary(); + CreatedAt = createdAt ?? new DateTimeOffset(2022, 1, 9, 0, 0, 0, TimeSpan.Zero); } internal new static SocketThreadChannel Create(SocketGuild guild, ClientState state, Model model) { - var parent = (SocketTextChannel)guild.GetChannel(model.CategoryId.Value); - var entity = new SocketThreadChannel(guild.Discord, guild, model.Id, parent); + var parent = guild.GetChannel(model.CategoryId.Value); + var entity = new SocketThreadChannel(guild.Discord, guild, model.Id, parent, model.ThreadMetadata.GetValueOrDefault()?.CreatedAt.GetValueOrDefault(null)); entity.Update(state, model); return entity; } @@ -103,6 +136,7 @@ namespace Discord.WebSocket if (model.ThreadMetadata.IsSpecified) { + IsInvitable = model.ThreadMetadata.Value.Invitable.ToNullable(); IsArchived = model.ThreadMetadata.Value.Archived; ArchiveTimestamp = model.ThreadMetadata.Value.ArchiveTimestamp; AutoArchiveDuration = model.ThreadMetadata.Value.AutoArchiveDuration; @@ -111,7 +145,7 @@ namespace Discord.WebSocket if (model.OwnerId.IsSpecified) { - Owner = GetUser(model.OwnerId.Value); + _ownerId = model.OwnerId.Value; } HasJoined = model.ThreadMember.IsSpecified; diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 4a7d4fafb..8b376b3ed 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -372,7 +372,7 @@ namespace Discord.WebSocket /// This field is based off of caching alone, since there is no events returned on the guild model. /// /// - /// A read-only collection of guild events found within this guild. + /// A read-only collection of guild events found within this guild. /// public IReadOnlyCollection Events => _events.ToReadOnlyCollection(); @@ -621,17 +621,19 @@ namespace Discord.WebSocket #endregion #region Bans - /// - /// Gets a collection of all users banned in this guild. - /// - /// The options to be used when sending the request. - /// - /// 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. - /// - public Task> GetBansAsync(RequestOptions options = null) - => GuildHelper.GetBansAsync(this, Discord, options); + + /// + public IAsyncEnumerable> GetBansAsync(int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null) + => GuildHelper.GetBansAsync(this, Discord, null, Direction.Before, limit, options); + + /// + public IAsyncEnumerable> GetBansAsync(ulong fromUserId, Direction dir, int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null) + => GuildHelper.GetBansAsync(this, Discord, fromUserId, dir, limit, options); + + /// + public IAsyncEnumerable> GetBansAsync(IUser fromUser, Direction dir, int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null) + => GuildHelper.GetBansAsync(this, Discord, fromUser.Id, dir, limit, options); + /// /// Gets a ban object for a banned user. /// @@ -847,10 +849,10 @@ namespace Discord.WebSocket #endregion #region Integrations - public Task> GetIntegrationsAsync(RequestOptions options = null) + public Task> GetIntegrationsAsync(RequestOptions options = null) => GuildHelper.GetIntegrationsAsync(this, Discord, options); - public Task CreateIntegrationAsync(ulong id, string type, RequestOptions options = null) - => GuildHelper.CreateIntegrationAsync(this, Discord, id, type, options); + public Task DeleteIntegrationAsync(ulong id, RequestOptions options = null) + => GuildHelper.DeleteIntegrationAsync(this, Discord, id, options); #endregion #region Interactions @@ -999,10 +1001,6 @@ namespace Discord.WebSocket return null; } - /// - public Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), - bool isHoisted = false, RequestOptions options = null) - => GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, false, options); /// /// Creates a new role with the provided name. /// @@ -1295,6 +1293,7 @@ namespace Discord.WebSocket /// /// A collection of speakers for the event. /// The location of the event; links are supported + /// The optional banner image for the event. /// The options to be used when sending the request. /// /// A task that represents the asynchronous create operation. @@ -1308,6 +1307,7 @@ namespace Discord.WebSocket DateTimeOffset? endTime = null, ulong? channelId = null, string location = null, + Image? coverImage = null, RequestOptions options = null) { // requirements taken from https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-permissions-requirements @@ -1324,7 +1324,7 @@ namespace Discord.WebSocket break; } - return GuildHelper.CreateGuildEventAsync(Discord, this, name, privacyLevel, startTime, type, description, endTime, channelId, location, options); + return GuildHelper.CreateGuildEventAsync(Discord, this, name, privacyLevel, startTime, type, description, endTime, channelId, location, coverImage, options); } @@ -1781,7 +1781,7 @@ namespace Discord.WebSocket /// ulong? IGuild.AFKChannelId => AFKChannelId; /// - IAudioClient IGuild.AudioClient => null; + IAudioClient IGuild.AudioClient => AudioClient; /// bool IGuild.Available => true; /// @@ -1803,8 +1803,8 @@ namespace Discord.WebSocket /// IReadOnlyCollection IGuild.Stickers => Stickers; /// - async Task IGuild.CreateEventAsync(string name, DateTimeOffset startTime, GuildScheduledEventType type, GuildScheduledEventPrivacyLevel privacyLevel, string description, DateTimeOffset? endTime, ulong? channelId, string location, RequestOptions options) - => await CreateEventAsync(name, startTime, type, privacyLevel, description, endTime, channelId, location, options).ConfigureAwait(false); + async Task IGuild.CreateEventAsync(string name, DateTimeOffset startTime, GuildScheduledEventType type, GuildScheduledEventPrivacyLevel privacyLevel, string description, DateTimeOffset? endTime, ulong? channelId, string location, Image? coverImage, RequestOptions options) + => await CreateEventAsync(name, startTime, type, privacyLevel, description, endTime, channelId, location, coverImage, options).ConfigureAwait(false); /// async Task IGuild.GetEventAsync(ulong id, RequestOptions options) => await GetEventAsync(id, options).ConfigureAwait(false); @@ -1812,8 +1812,14 @@ namespace Discord.WebSocket async Task> IGuild.GetEventsAsync(RequestOptions options) => await GetEventsAsync(options).ConfigureAwait(false); /// - async Task> IGuild.GetBansAsync(RequestOptions options) - => await GetBansAsync(options).ConfigureAwait(false); + IAsyncEnumerable> IGuild.GetBansAsync(int limit, RequestOptions options) + => GetBansAsync(limit, options); + /// + IAsyncEnumerable> IGuild.GetBansAsync(ulong fromUserId, Direction dir, int limit, RequestOptions options) + => GetBansAsync(fromUserId, dir, limit, options); + /// + IAsyncEnumerable> IGuild.GetBansAsync(IUser fromUser, Direction dir, int limit, RequestOptions options) + => GetBansAsync(fromUser, dir, limit, options); /// async Task IGuild.GetBanAsync(IUser user, RequestOptions options) => await GetBanAsync(user, options).ConfigureAwait(false); @@ -1890,11 +1896,11 @@ namespace Discord.WebSocket => await GetVoiceRegionsAsync(options).ConfigureAwait(false); /// - async Task> IGuild.GetIntegrationsAsync(RequestOptions options) + async Task> IGuild.GetIntegrationsAsync(RequestOptions options) => await GetIntegrationsAsync(options).ConfigureAwait(false); /// - async Task IGuild.CreateIntegrationAsync(ulong id, string type, RequestOptions options) - => await CreateIntegrationAsync(id, type, options).ConfigureAwait(false); + async Task IGuild.DeleteIntegrationAsync(ulong id, RequestOptions options) + => await DeleteIntegrationAsync(id, options).ConfigureAwait(false); /// async Task> IGuild.GetInvitesAsync(RequestOptions options) @@ -1926,8 +1932,15 @@ namespace Discord.WebSocket async Task IGuild.AddGuildUserAsync(ulong userId, string accessToken, Action func, RequestOptions options) => await AddGuildUserAsync(userId, accessToken, func, options); /// - Task IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetUser(id)); + async Task IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + var user = GetUser(id); + if (user is not null || mode == CacheMode.CacheOnly) + return user; + + return await GuildHelper.GetUserAsync(this, Discord, id, options).ConfigureAwait(false); + } + /// Task IGuild.GetCurrentUserAsync(CacheMode mode, RequestOptions options) => Task.FromResult(CurrentUser); diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs index df619e4ca..a86aafadf 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs @@ -35,6 +35,9 @@ namespace Discord.WebSocket /// public string Description { get; private set; } + /// + public string CoverImageId { get; private set; } + /// public DateTimeOffset StartTime { get; private set; } @@ -109,8 +112,13 @@ namespace Discord.WebSocket StartTime = model.ScheduledStartTime; Status = model.Status; UserCount = model.UserCount.ToNullable(); + CoverImageId = model.Image; } + /// + public string GetCoverImageUrl(ImageFormat format = ImageFormat.Auto, ushort size = 1024) + => CDN.GetEventCoverImageUrl(Guild.Id, Id, CoverImageId, format, size); + /// public Task DeleteAsync(RequestOptions options = null) => GuildHelper.DeleteEventAsync(Discord, this, options); diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs index b06979381..aeff465bd 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs @@ -202,12 +202,7 @@ namespace Discord.WebSocket HasResponded = true; } - /// - /// Updates the message which this component resides in with the type - /// - /// A delegate containing the properties to modify the message with. - /// The request options for this request. - /// A task that represents the asynchronous operation of updating the message. + /// public async Task UpdateAsync(Action func, RequestOptions options = null) { var args = new MessageProperties(); @@ -383,14 +378,7 @@ namespace Discord.WebSocket return await InteractionHelper.SendFollowupAsync(Discord, args, Token, Channel, options).ConfigureAwait(false); } - /// - /// Defers an interaction and responds with type 5 () - /// - /// to send this message ephemerally, otherwise . - /// The request options for this request. - /// - /// A task that represents the asynchronous operation of acknowledging the interaction. - /// + /// public async Task DeferLoadingAsync(bool ephemeral = false, RequestOptions options = null) { if (!InteractionHelper.CanSendResponse(this)) diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs index 8b5bd9c32..5b2da04f5 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs @@ -19,13 +19,25 @@ namespace Discord.WebSocket /// Gets the this interaction was used in. /// /// - /// If the channel isn't cached or the bot doesn't have access to it then + /// If the channel isn't cached, the bot scope isn't used, or the bot doesn't have access to it then /// this property will be . /// public ISocketMessageChannel Channel { get; private set; } + /// + /// Gets the ID of the channel this interaction was used in. + /// + /// + /// This property is exposed in cases where the bot scope is not provided, so the channel entity cannot be retrieved. + ///
+ /// To get the channel, you can call + /// as this method makes a request for a if nothing was found in cache. + ///
+ public ulong? ChannelId { get; private set; } + /// /// Gets the who triggered this interaction. + /// This property will be if the bot scope isn't used. /// public SocketUser User { get; private set; } @@ -62,8 +74,6 @@ namespace Discord.WebSocket /// public bool IsDMInteraction { get; private set; } - private ulong? _channelId; - internal SocketInteraction(DiscordSocketClient client, ulong id, ISocketMessageChannel channel, SocketUser user) : base(client, id) { @@ -111,7 +121,7 @@ namespace Discord.WebSocket { IsDMInteraction = !model.GuildId.IsSpecified; - _channelId = model.ChannelId.ToNullable(); + ChannelId = model.ChannelId.ToNullable(); Data = model.Data.IsSpecified ? model.Data.Value @@ -396,12 +406,12 @@ namespace Discord.WebSocket if (Channel != null) return Channel; - if (!_channelId.HasValue) + if (!ChannelId.HasValue) return null; try { - return (IMessageChannel)await Discord.GetChannelAsync(_channelId.Value, options).ConfigureAwait(false); + return (IMessageChannel)await Discord.GetChannelAsync(ChannelId.Value, options).ConfigureAwait(false); } catch(HttpException ex) when (ex.DiscordCode == DiscordErrorCode.MissingPermissions) { return null; } // bot can't view that channel, return null instead of throwing. } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs index 025d34d0f..6eddd876d 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs @@ -147,6 +147,17 @@ namespace Discord.WebSocket return entity; } + internal static SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, SocketGuildUser owner) + { + // this is used for creating the owner of the thread. + var entity = new SocketThreadUser(guild, thread, owner, owner.Id); + entity.Update(new Model + { + JoinTimestamp = thread.CreatedAt, + }); + return entity; + } + internal void Update(Model model) { ThreadJoinedAt = model.JoinTimestamp; diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index 35121d666..d70e61739 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -117,8 +117,8 @@ namespace Discord.WebSocket /// /// The full name of the user. /// - public override string ToString() => Format.UsernameAndDiscriminator(this); - private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this)} ({Id}{(IsBot ? ", Bot" : "")})"; + public override string ToString() => Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode); + private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode)} ({Id}{(IsBot ? ", Bot" : "")})"; internal SocketUser Clone() => MemberwiseClone() as SocketUser; } } diff --git a/src/Discord.Net.Webhook/DiscordWebhookClient.cs b/src/Discord.Net.Webhook/DiscordWebhookClient.cs index f7bc38587..405100f89 100644 --- a/src/Discord.Net.Webhook/DiscordWebhookClient.cs +++ b/src/Discord.Net.Webhook/DiscordWebhookClient.cs @@ -87,8 +87,9 @@ namespace Discord.Webhook /// Sends a message to the channel for this webhook. /// Returns the ID of the created message. public Task SendMessageAsync(string text = null, bool isTTS = false, IEnumerable embeds = null, - string username = null, string avatarUrl = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null) - => WebhookClientHelper.SendMessageAsync(this, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, components); + string username = null, string avatarUrl = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageComponent components = null, MessageFlags flags = MessageFlags.None) + => WebhookClientHelper.SendMessageAsync(this, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, components, flags); /// /// Modifies a message posted using this webhook. @@ -124,33 +125,35 @@ namespace Discord.Webhook public Task SendFileAsync(string filePath, string text, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, - MessageComponent components = null) + MessageComponent components = null, MessageFlags flags = MessageFlags.None) => WebhookClientHelper.SendFileAsync(this, filePath, text, isTTS, embeds, username, avatarUrl, - allowedMentions, options, isSpoiler, components); + allowedMentions, options, isSpoiler, components, flags); /// Sends a message to the channel for this webhook with an attachment. /// Returns the ID of the created message. public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, - MessageComponent components = null) + MessageComponent components = null, MessageFlags flags = MessageFlags.None) => WebhookClientHelper.SendFileAsync(this, stream, filename, text, isTTS, embeds, username, - avatarUrl, allowedMentions, options, isSpoiler, components); + avatarUrl, allowedMentions, options, isSpoiler, components, flags); /// Sends a message to the channel for this webhook with an attachment. /// Returns the ID of the created message. public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, - RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null) + RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, + MessageFlags flags = MessageFlags.None) => WebhookClientHelper.SendFileAsync(this, attachment, text, isTTS, embeds, username, - avatarUrl, allowedMentions, components, options); + avatarUrl, allowedMentions, components, options, flags); /// Sends a message to the channel for this webhook with an attachment. /// Returns the ID of the created message. public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, - RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null) + RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, + MessageFlags flags = MessageFlags.None) => WebhookClientHelper.SendFilesAsync(this, attachments, text, isTTS, embeds, username, avatarUrl, - allowedMentions, components, options); + allowedMentions, components, options, flags); /// Modifies the properties of this webhook. diff --git a/src/Discord.Net.Webhook/WebhookClientHelper.cs b/src/Discord.Net.Webhook/WebhookClientHelper.cs index a9d5a25da..0a974a9d9 100644 --- a/src/Discord.Net.Webhook/WebhookClientHelper.cs +++ b/src/Discord.Net.Webhook/WebhookClientHelper.cs @@ -21,12 +21,14 @@ namespace Discord.Webhook return RestInternalWebhook.Create(client, model); } public static async Task SendMessageAsync(DiscordWebhookClient client, - string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, MessageComponent components) + string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, + AllowedMentions allowedMentions, RequestOptions options, MessageComponent components, MessageFlags flags) { var args = new CreateWebhookMessageParams { Content = text, - IsTTS = isTTS + IsTTS = isTTS, + Flags = flags }; if (embeds != null) @@ -40,6 +42,9 @@ namespace Discord.Webhook if (components != null) args.Components = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray(); + if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds) + throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds and none.", nameof(flags)); + var model = await client.ApiClient.CreateWebhookMessageAsync(client.Webhook.Id, args, options: options).ConfigureAwait(false); return model.Id; } @@ -97,22 +102,27 @@ namespace Discord.Webhook await client.ApiClient.DeleteWebhookMessageAsync(client.Webhook.Id, messageId, options).ConfigureAwait(false); } public static async Task SendFileAsync(DiscordWebhookClient client, string filePath, string text, bool isTTS, - IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, bool isSpoiler, MessageComponent components) + IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, + bool isSpoiler, MessageComponent components, MessageFlags flags = MessageFlags.None) { string filename = Path.GetFileName(filePath); using (var file = File.OpenRead(filePath)) - return await SendFileAsync(client, file, filename, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, isSpoiler, components).ConfigureAwait(false); + return await SendFileAsync(client, file, filename, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, isSpoiler, components, flags).ConfigureAwait(false); } public static Task SendFileAsync(DiscordWebhookClient client, Stream stream, string filename, string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, bool isSpoiler, - MessageComponent components) - => SendFileAsync(client, new FileAttachment(stream, filename, isSpoiler: isSpoiler), text, isTTS, embeds, username, avatarUrl, allowedMentions, components, options); + MessageComponent components, MessageFlags flags) + => SendFileAsync(client, new FileAttachment(stream, filename, isSpoiler: isSpoiler), text, isTTS, embeds, username, avatarUrl, allowedMentions, components, options, flags); - public static Task SendFileAsync(DiscordWebhookClient client, FileAttachment attachment, string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, MessageComponent components, RequestOptions options) - => SendFilesAsync(client, new FileAttachment[] { attachment }, text, isTTS, embeds, username, avatarUrl, allowedMentions, components, options); + public static Task SendFileAsync(DiscordWebhookClient client, FileAttachment attachment, string text, bool isTTS, + IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, + MessageComponent components, RequestOptions options, MessageFlags flags) + => SendFilesAsync(client, new FileAttachment[] { attachment }, text, isTTS, embeds, username, avatarUrl, allowedMentions, components, options, flags); public static async Task SendFilesAsync(DiscordWebhookClient client, - IEnumerable attachments, string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, MessageComponent components, RequestOptions options) + IEnumerable attachments, string text, bool isTTS, IEnumerable embeds, string username, + string avatarUrl, AllowedMentions allowedMentions, MessageComponent components, RequestOptions options, + MessageFlags flags) { embeds ??= Array.Empty(); @@ -141,7 +151,19 @@ namespace Discord.Webhook } } - var args = new UploadWebhookFileParams(attachments.ToArray()) {AvatarUrl = avatarUrl, Username = username, Content = text, IsTTS = isTTS, Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, MessageComponents = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified }; + if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds) + throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds and none.", nameof(flags)); + + var args = new UploadWebhookFileParams(attachments.ToArray()) + { + AvatarUrl = avatarUrl, + Username = username, Content = text, + IsTTS = isTTS, + Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, + AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, + MessageComponents = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + Flags = flags + }; var msg = await client.ApiClient.UploadWebhookFileAsync(client.Webhook.Id, args, options).ConfigureAwait(false); return msg.Id; } diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index dec25413c..d79e9a24a 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,7 +2,7 @@ Discord.Net - 3.3.2$suffix$ + 3.5.0$suffix$ Discord.Net Discord.Net Contributors foxbot @@ -14,44 +14,44 @@ https://github.com/RogueException/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs index 519bab4d9..2a7f8065a 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs @@ -83,10 +83,10 @@ namespace Discord throw new NotImplementedException(); } - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); - public Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); - public Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); - public Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); - public Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); } } diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs index 9c94efffa..b7f98f572 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs @@ -93,17 +93,17 @@ namespace Discord throw new NotImplementedException(); } - public Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + public Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) { throw new NotImplementedException(); } - public Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + public Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) { throw new NotImplementedException(); } - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) { throw new NotImplementedException(); } @@ -113,7 +113,7 @@ namespace Discord throw new NotImplementedException(); } - public Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); - public Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); + public Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); } } diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs index ad0af04b2..0dfcab7a5 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs @@ -176,17 +176,17 @@ namespace Discord throw new NotImplementedException(); } - public Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + public Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) { throw new NotImplementedException(); } - public Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + public Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) { throw new NotImplementedException(); } - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) { throw new NotImplementedException(); } @@ -211,9 +211,10 @@ namespace Discord throw new NotImplementedException(); } - public Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); - public Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); - public Task CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, bool? invitable = null, int? slowmode = null, RequestOptions options = null) => throw new NotImplementedException(); + public Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, bool? invitable = null, int? slowmode = null, RequestOptions options = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); public Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => throw new NotImplementedException(); + public Task CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, bool? invitable = null, int? slowmode = null, RequestOptions options = null) => throw new NotImplementedException(); } }