| @@ -0,0 +1,76 @@ | |||
| # Changelog | |||
| ## [Unreleased] | |||
| ### Added | |||
| - #747: `CommandService` now has a `CommandExecuted` event (e991715) | |||
| - #765: Parameters may have a name specified via `NameAttribute` (9c81ab9) | |||
| - #773: Both socket clients inherit from `BaseSocketClient` (9b7afec) | |||
| - #785: Primitives now automatically load a NullableTypeReader (cb0ff78) | |||
| - #819: Support for Welcome Message channels (30e867a) | |||
| - #835: Emoji may now be managed from a bot (b4bf046) | |||
| - #843: Webhooks may now be managed from a bot (7b2ddd0) | |||
| - #863: An embed may be converted to an `EmbedBuilder` using the `.ToEmbedBuilder()` method (5218e6b) | |||
| - #877: Support for reading rich presences (34b4e5a) | |||
| - #888: Users may now opt-in to using a proxy (678a723) | |||
| - #906: API Analyzers to assist users when writing their bot (f69ef2a) | |||
| - #907: Full support for channel categories (030422f) | |||
| - #913: Animated emoji may be read and written (a19ff18) | |||
| - #915: Unused parameters may be discarded, rather than failing the command (5f46aef) | |||
| - #929: Standard EqualityComparers for use in LINQ operations with the library's entities (b5e7548) | |||
| - 'html' variant added to the `EmbedType` enum (42c879c) | |||
| ### Fixed | |||
| - #742: `DiscordShardedClient#GetGuildFor` will now direct null guilds to Shard 0 (d5e9d6f) | |||
| - #743: Various issues with permissions and inheritance of permissions (f996338) | |||
| - #755: `IRole.Mention` will correctly tag the @everyone role (6b5a6e7) | |||
| - #768: `CreateGuildAsync` will include the icon stream (865080a) | |||
| - #866: Revised permissions constants and behavior (dec7cb2) | |||
| - #872: Bulk message deletion should no longer fail for incomplete batch sizes (804d918) | |||
| - #923: A null value should properly reset a user's nickname (227f61a) | |||
| - #938: The reconnect handler should no longer deadlock during Discord outages (73ac9d7) | |||
| - Ignore messages with no ID in bulk delete (676be40) | |||
| ### Changed | |||
| - #731: `IUserMessage#GetReactionUsersAsync` now takes an `IEmote` instead of a `string` (5d7f2fc) | |||
| - #744: IAsyncEnumerable has been redesigned (5bbd9bb) | |||
| - #777: `IGuild#DefaultChannel` will now resolve the first accessible channel, per changes to Discord (1ffcd4b) | |||
| - #781: Attempting to add or remove a member's EveryoneRole will throw (506a6c9) | |||
| - #801: `EmbedBuilder` will no longer implicitly convert to `Embed`, you must build manually (94f7dd2) | |||
| - #804: Command-related tasks will have the 'async' suffix (14fbe40) | |||
| - #812: The WebSocket4Net provider has been bumped to version 0.15, allowing support for .NET Standard apps (e25054b) | |||
| - #829: DeleteMessagesAsync moved from IMessageChannel to ITextChannel (e00f17f) | |||
| - #853: WebSocket will now use `zlib-stream` compression (759db34) | |||
| - #874: The `ReadMessages` permission is moving to `ViewChannel` (edfbd05) | |||
| - #877: Refactored Games into Activities (34b4e5a) | |||
| - `IGuildChannel#Nsfw` moved to `ITextChannel`, now maps to the API property (608bc35) | |||
| - Preemptive ratelimits are now logged under verbose, rather than warning. (3c1e766) | |||
| - The default InviteAge when creating Invites is now 24 hours (9979a02) | |||
| ### Removed | |||
| - #790: Redundant overloads for `AddField` removed from EmbedBuilder (479361b) | |||
| - #925: RPC is no longer being maintained nor packaged (b30af57) | |||
| - User logins (including selfbots) are no longer supported (fc5adca) | |||
| ### Misc | |||
| - This project is now licensed to the Discord.Net contributors (710e182) | |||
| - #786: Unit tests for the Color structure (22b969c) | |||
| - #828: We now include a contributing guide (cd82a0f) | |||
| - #876: We now include a standard editorconfig (5c8c784) | |||
| ## [1.0.2] - 2017-09-09 | |||
| ### Fixed | |||
| - Guilds utilizing Channel Categories will no longer crash bots on the `READY` event. | |||
| ## [1.0.1] - 2017-07-05 | |||
| ### Fixed | |||
| - #732: Fixed parameter preconditions not being loaded from class-based modules (b6dcc9e) | |||
| - #726: Fixed CalculateScore throwing an ArgumentException for missing parameters (7597cf5) | |||
| - EmbedBuilder URI validation should no longer throw NullReferenceExceptions in certain edge cases (d89804d) | |||
| - Fixed module auto-detection for nested modules (d2afb06) | |||
| ### Changed | |||
| - ShardedCommandContext now inherits from SocketCommandContext (8cd99be) | |||
| @@ -26,10 +26,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Analyzers", "sr | |||
| EndProject | |||
| Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}" | |||
| EndProject | |||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "01_basic_ping_bot", "samples\01_basic_ping_bot\01_basic_ping_bot.csproj", "{F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}" | |||
| Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "01_basic_ping_bot", "samples\01_basic_ping_bot\01_basic_ping_bot.csproj", "{F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}" | |||
| EndProject | |||
| Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "02_commands_framework", "samples\02_commands_framework\02_commands_framework.csproj", "{4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}" | |||
| EndProject | |||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "03_sharded_client", "samples\03_sharded_client\03_sharded_client.csproj", "{9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}" | |||
| EndProject | |||
| Global | |||
| GlobalSection(SolutionConfigurationPlatforms) = preSolution | |||
| Debug|Any CPU = Debug|Any CPU | |||
| @@ -160,6 +162,18 @@ Global | |||
| {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Release|x64.Build.0 = Release|Any CPU | |||
| {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Release|x86.ActiveCfg = Release|Any CPU | |||
| {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Release|x86.Build.0 = Release|Any CPU | |||
| {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | |||
| {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Debug|Any CPU.Build.0 = Debug|Any CPU | |||
| {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Debug|x64.ActiveCfg = Debug|Any CPU | |||
| {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Debug|x64.Build.0 = Debug|Any CPU | |||
| {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Debug|x86.ActiveCfg = Debug|Any CPU | |||
| {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Debug|x86.Build.0 = Debug|Any CPU | |||
| {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Release|Any CPU.ActiveCfg = Release|Any CPU | |||
| {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Release|Any CPU.Build.0 = Release|Any CPU | |||
| {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Release|x64.ActiveCfg = Release|Any CPU | |||
| {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Release|x64.Build.0 = Release|Any CPU | |||
| {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Release|x86.ActiveCfg = Release|Any CPU | |||
| {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Release|x86.Build.0 = Release|Any CPU | |||
| EndGlobalSection | |||
| GlobalSection(SolutionProperties) = preSolution | |||
| HideSolutionNode = FALSE | |||
| @@ -173,6 +187,7 @@ Global | |||
| {BBA8E7FB-C834-40DC-822F-B112CB7F0140} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A} | |||
| {F2FF84FB-F6AD-47E5-9EE5-18206CAE136E} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B} | |||
| {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B} | |||
| {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B} | |||
| EndGlobalSection | |||
| GlobalSection(ExtensibilityGlobals) = postSolution | |||
| SolutionGuid = {D2404771-EEC8-45F2-9D71-F3373F6C1495} | |||
| @@ -63,4 +63,32 @@ use the cached message entity. Read more about it [here](xref:Guides.Concepts.Ev | |||
| [MessageCacheSize]: xref:Discord.WebSocket.DiscordSocketConfig.MessageCacheSize | |||
| [DiscordSocketConfig]: xref:Discord.WebSocket.DiscordSocketConfig | |||
| [MessageUpdated]: xref:Discord.WebSocket.BaseSocketClient.MessageUpdated | |||
| [MessageUpdated]: xref:Discord.WebSocket.BaseSocketClient.MessageUpdated | |||
| ## What is a shard/sharded client, and how is it different from the `DiscordSocketClient`? | |||
| As your bot grows in popularity, it is recommended that you should section your bot off into separate processes. | |||
| The [DiscordShardedClient] is essentially a class that allows you to easily create and manage multiple [DiscordSocketClient] | |||
| instances, with each one serving a different amount of guilds. | |||
| There are very few differences from the [DiscordSocketClient] class, and it is very straightforward | |||
| to modify your existing code to use a [DiscordShardedClient] when necessary. | |||
| 1. You need to specify the total amount of shards, or shard ids, via [DiscordShardedClient]'s constructors. | |||
| 2. The [Connected], [Disconnected], [Ready], and [LatencyUpdated] events | |||
| are replaced with [ShardConnected], [ShardDisconnected], [ShardReady], and [ShardLatencyUpdated]. | |||
| 3. Every event handler you apply/remove to the [DiscordShardedClient] is applied/removed to each shard. | |||
| If you wish to control a specific shard's events, you can access an individual shard through the `Shards` property. | |||
| If you do not wish to use the [DiscordShardedClient] and instead reuse the same [DiscordSocketClient] code and manually shard them, | |||
| you can do so by specifying the [ShardId] for the [DiscordSocketConfig] and pass that to the [DiscordSocketClient]'s constructor. | |||
| [DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient | |||
| [DiscordShardedClient]: xref:Discord.WebSocket.DiscordShardedClient | |||
| [Connected]: xref:Discord.WebSocket.DiscordSocketClient.Connected | |||
| [Disconnected]: xref:Discord.WebSocket.DiscordSocketClient.Disconnected | |||
| [LatencyUpdated]: xref:Discord.WebSocket.DiscordSocketClient.LatencyUpdated | |||
| [ShardConnected]: xref:Discord.WebSocket.DiscordShardedClient.ShardConnected | |||
| [ShardDisconnected]: xref:Discord.WebSocket.DiscordShardedClient.ShardDisconnected | |||
| [ShardReady]: xref:Discord.WebSocket.DiscordShardedClient.ShardReady | |||
| [ShardLatencyUpdated]: xref:Discord.WebSocket.DiscordShardedClient.ShardLatencyUpdated | |||
| [ShardId]: xref:Discord.WebSocket.DiscordSocketConfig.ShardId | |||
| @@ -35,9 +35,10 @@ public class CommandHandler | |||
| // Create a number to track where the prefix ends and the command begins | |||
| int argPos = 0; | |||
| // Determine if the message is a command based on the prefix | |||
| // Determine if the message is a command based on the prefix and make sure no bots trigger commands | |||
| if (!(message.HasCharPrefix('!', ref argPos) || | |||
| message.HasMentionPrefix(_client.CurrentUser, ref argPos))) | |||
| message.HasMentionPrefix(_client.CurrentUser, ref argPos)) || | |||
| message.Author.IsBot) | |||
| return; | |||
| // Create a WebSocket-based command context based on the message | |||
| @@ -60,4 +61,4 @@ public class CommandHandler | |||
| // if (!result.IsSuccess) | |||
| // await context.Channel.SendMessageAsync(result.ErrorReason); | |||
| } | |||
| } | |||
| } | |||
| @@ -27,7 +27,7 @@ public async Task HandleCommandAsync(SocketMessage msg) | |||
| var message = messageParam as SocketUserMessage; | |||
| if (message == null) return; | |||
| int argPos = 0; | |||
| if (!(message.HasCharPrefix('!', ref argPos) || message.HasMentionPrefix(_client.CurrentUser, ref argPos))) return; | |||
| if (!(message.HasCharPrefix('!', ref argPos) || message.HasMentionPrefix(_client.CurrentUser, ref argPos)) || message.Author.IsBot) return; | |||
| var context = new SocketCommandContext(_client, message); | |||
| var result = await _commands.ExecuteAsync(context, argPos, _services); | |||
| // Optionally, you may pass the result manually into your | |||
| @@ -35,4 +35,4 @@ public async Task HandleCommandAsync(SocketMessage msg) | |||
| // precondition failures in the same method. | |||
| // await OnCommandExecutedAsync(null, context, result); | |||
| } | |||
| } | |||
| @@ -1,4 +1,5 @@ | |||
| [Command("join")] | |||
| // The command's Run Mode MUST be set to RunMode.Async, otherwise, being connected to a voice channel will block the gateway thread. | |||
| [Command("join", RunMode = RunMode.Async)] | |||
| public async Task JoinChannel(IVoiceChannel channel = null) | |||
| { | |||
| // Get the audio channel | |||
| @@ -11,16 +11,16 @@ Information is not guaranteed to be accurate. | |||
| ## Installing | |||
| Audio requires two native libraries, `libsodium` and `opus`. | |||
| Both of these libraries must be placed in the runtime directory of your | |||
| bot. (When developing on .NET Framework, this would be `bin/debug`, | |||
| when developing on .NET Core, this is where you execute `dotnet run` | |||
| Audio requires two native libraries, `libsodium` and `opus`. | |||
| Both of these libraries must be placed in the runtime directory of your | |||
| bot. (When developing on .NET Framework, this would be `bin/debug`, | |||
| when developing on .NET Core, this is where you execute `dotnet run` | |||
| from; typically the same directory as your csproj). | |||
| For Windows Users, precompiled binaries are available for your | |||
| For Windows Users, precompiled binaries are available for your | |||
| convienence [here](https://discord.foxbot.me/binaries/). | |||
| For Linux Users, you will need to compile [Sodium] and [Opus] from | |||
| For Linux Users, you will need to compile [Sodium] and [Opus] from | |||
| source, or install them from your package manager. | |||
| [Sodium]: https://download.libsodium.org/libsodium/releases/ | |||
| @@ -28,7 +28,7 @@ source, or install them from your package manager. | |||
| ## Joining a Channel | |||
| Joining a channel is the first step to sending audio, and will return | |||
| Joining a channel is the first step to sending audio, and will return | |||
| an [IAudioClient] to send data with. | |||
| To join a channel, simply await [ConnectAsync] on any instance of an | |||
| @@ -36,67 +36,76 @@ To join a channel, simply await [ConnectAsync] on any instance of an | |||
| [!code-csharp[Joining a Channel](samples/joining_audio.cs)] | |||
| The client will sustain a connection to this channel until it is | |||
| >[!WARNING] | |||
| >Commands which mutate voice states, such as those where you join/leave | |||
| >an audio channel, or send audio, should use [RunMode.Async]. RunMode.Async | |||
| >is necessary to prevent a feedback loop which will deadlock clients | |||
| >in their default configuration. If you know that you're running your | |||
| >commands in a different task than the gateway task, RunMode.Async is | |||
| >not required. | |||
| The client will sustain a connection to this channel until it is | |||
| kicked, disconnected from Discord, or told to disconnect. | |||
| It should be noted that voice connections are created on a per-guild | |||
| basis; only one audio connection may be open by the bot in a single | |||
| guild. To switch channels within a guild, invoke [ConnectAsync] on | |||
| It should be noted that voice connections are created on a per-guild | |||
| basis; only one audio connection may be open by the bot in a single | |||
| guild. To switch channels within a guild, invoke [ConnectAsync] on | |||
| another voice channel in the guild. | |||
| [IAudioClient]: xref:Discord.Audio.IAudioClient | |||
| [ConnectAsync]: xref:Discord.IAudioChannel.ConnectAsync* | |||
| [RunMode.Async]: xref:Discord.Commands.RunMode | |||
| ## Transmitting Audio | |||
| ### With FFmpeg | |||
| [FFmpeg] is an open source, highly versatile AV-muxing tool. This is | |||
| [FFmpeg] is an open source, highly versatile AV-muxing tool. This is | |||
| the recommended method of transmitting audio. | |||
| Before you begin, you will need to have a version of FFmpeg downloaded | |||
| and placed somewhere in your PATH (or alongside the bot, in the same | |||
| location as libsodium and opus). Windows binaries are available on | |||
| Before you begin, you will need to have a version of FFmpeg downloaded | |||
| and placed somewhere in your PATH (or alongside the bot, in the same | |||
| location as libsodium and opus). Windows binaries are available on | |||
| [FFmpeg's download page]. | |||
| [FFmpeg]: https://ffmpeg.org/ | |||
| [FFmpeg's download page]: https://ffmpeg.org/download.html | |||
| First, you will need to create a Process that starts FFmpeg. An | |||
| example of how to do this is included below, though it is important | |||
| First, you will need to create a Process that starts FFmpeg. An | |||
| example of how to do this is included below, though it is important | |||
| that you return PCM at 48000hz. | |||
| >[!NOTE] | |||
| >As of the time of this writing, Discord.Audio struggles significantly | |||
| >with processing audio that is already opus-encoded; you will need to | |||
| >As of the time of this writing, Discord.Audio struggles significantly | |||
| >with processing audio that is already opus-encoded; you will need to | |||
| >use the PCM write streams. | |||
| [!code-csharp[Creating FFmpeg](samples/audio_create_ffmpeg.cs)] | |||
| Next, to transmit audio from FFmpeg to Discord, you will need to | |||
| pull an [AudioOutStream] from your [IAudioClient]. Since we're using | |||
| Next, to transmit audio from FFmpeg to Discord, you will need to | |||
| pull an [AudioOutStream] from your [IAudioClient]. Since we're using | |||
| PCM audio, use [IAudioClient.CreatePCMStream]. | |||
| The sample rate argument doesn't particularly matter, so long as it is | |||
| a valid rate (120, 240, 480, 960, 1920, or 2880). For the sake of | |||
| The sample rate argument doesn't particularly matter, so long as it is | |||
| a valid rate (120, 240, 480, 960, 1920, or 2880). For the sake of | |||
| simplicity, I recommend using 1920. | |||
| Channels should be left at `2`, unless you specified a different value | |||
| Channels should be left at `2`, unless you specified a different value | |||
| for `-ac 2` when creating FFmpeg. | |||
| [AudioOutStream]: xref:Discord.Audio.AudioOutStream | |||
| [IAudioClient.CreatePCMStream]: xref:Discord.Audio.IAudioClient#Discord_Audio_IAudioClient_CreateDirectPCMStream_Discord_Audio_AudioApplication_System_Nullable_System_Int32__System_Int32_ | |||
| Finally, audio will need to be piped from FFmpeg's stdout into your | |||
| AudioOutStream. This step can be as complex as you'd like it to be, but | |||
| for the majority of cases, you can just use [Stream.CopyToAsync], as | |||
| Finally, audio will need to be piped from FFmpeg's stdout into your | |||
| AudioOutStream. This step can be as complex as you'd like it to be, but | |||
| for the majority of cases, you can just use [Stream.CopyToAsync], as | |||
| shown below. | |||
| [Stream.CopyToAsync]: https://msdn.microsoft.com/en-us/library/hh159084(v=vs.110).aspx | |||
| If you are implementing a queue for sending songs, it's likely that | |||
| you will want to wait for audio to stop playing before continuing on | |||
| to the next song. You can await `AudioOutStream.FlushAsync` to wait for | |||
| If you are implementing a queue for sending songs, it's likely that | |||
| you will want to wait for audio to stop playing before continuing on | |||
| to the next song. You can await `AudioOutStream.FlushAsync` to wait for | |||
| the audio client's internal buffer to clear out. | |||
| [!code-csharp[Sending Audio](samples/audio_ffmpeg.cs)] | |||
| @@ -20,6 +20,8 @@ namespace _02_commands_framework.Services | |||
| _discord = services.GetRequiredService<DiscordSocketClient>(); | |||
| _services = services; | |||
| _commands.CommandExecuted += CommandExecutedAsync; | |||
| _commands.Log += LogAsync; | |||
| _discord.MessageReceived += MessageReceivedAsync; | |||
| } | |||
| @@ -39,11 +41,28 @@ namespace _02_commands_framework.Services | |||
| if (!message.HasMentionPrefix(_discord.CurrentUser, ref argPos)) return; | |||
| var context = new SocketCommandContext(_discord, message); | |||
| var result = await _commands.ExecuteAsync(context, argPos, _services); | |||
| await _commands.ExecuteAsync(context, argPos, _services); // we will handle the result in CommandExecutedAsync | |||
| } | |||
| public async Task CommandExecutedAsync(Optional<CommandInfo> command, ICommandContext context, IResult result) | |||
| { | |||
| // command is unspecified when there was a search failure (command not found); we don't care about these errors | |||
| if (!command.IsSpecified) | |||
| return; | |||
| // the command was succesful, we don't care about this result, unless we want to log that a command succeeded. | |||
| if (result.IsSuccess) | |||
| return; | |||
| // the command failed, let's notify the user that something happened. | |||
| await context.Channel.SendMessageAsync($"error: {result.ToString()}"); | |||
| } | |||
| private Task LogAsync(LogMessage log) | |||
| { | |||
| Console.WriteLine(log.ToString()); | |||
| if (result.Error.HasValue && | |||
| result.Error.Value != CommandError.UnknownCommand) // it's bad practice to send 'unknown command' errors | |||
| await context.Channel.SendMessageAsync(result.ToString()); | |||
| return Task.CompletedTask; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,14 @@ | |||
| <Project Sdk="Microsoft.NET.Sdk"> | |||
| <PropertyGroup> | |||
| <OutputType>Exe</OutputType> | |||
| <TargetFramework>netcoreapp2.1</TargetFramework> | |||
| <RootNamespace>_03_sharded_client</RootNamespace> | |||
| </PropertyGroup> | |||
| <ItemGroup> | |||
| <ProjectReference Include="..\..\src\Discord.Net.Commands\Discord.Net.Commands.csproj" /> | |||
| <ProjectReference Include="..\..\src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" /> | |||
| </ItemGroup> | |||
| </Project> | |||
| @@ -0,0 +1,17 @@ | |||
| using System.Threading.Tasks; | |||
| using Discord.Commands; | |||
| namespace _03_sharded_client.Modules | |||
| { | |||
| // Remember to make your module reference the ShardedCommandContext | |||
| public class PublicModule : ModuleBase<ShardedCommandContext> | |||
| { | |||
| [Command("info")] | |||
| public async Task InfoAsync() | |||
| { | |||
| var msg = $@"Hi {Context.User}! There are currently {Context.Client.Shards} shards! | |||
| This guild is being served by shard number {Context.Client.GetShardFor(Context.Guild).ShardId}"; | |||
| await ReplyAsync(msg); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,69 @@ | |||
| using System; | |||
| using System.Threading.Tasks; | |||
| using _03_sharded_client.Services; | |||
| using Discord; | |||
| using Discord.Commands; | |||
| using Discord.WebSocket; | |||
| using Microsoft.Extensions.DependencyInjection; | |||
| namespace _03_sharded_client | |||
| { | |||
| // This is a minimal example of using Discord.Net's Sharded Client | |||
| // The provided DiscordShardedClient class simplifies having multiple | |||
| // DiscordSocketClient instances (or shards) to serve a large number of guilds. | |||
| class Program | |||
| { | |||
| private DiscordShardedClient _client; | |||
| static void Main(string[] args) | |||
| => new Program().MainAsync().GetAwaiter().GetResult(); | |||
| public async Task MainAsync() | |||
| { | |||
| // You specify the amount of shards you'd like to have with the | |||
| // DiscordSocketConfig. Generally, it's recommended to | |||
| // have 1 shard per 1500-2000 guilds your bot is in. | |||
| var config = new DiscordSocketConfig | |||
| { | |||
| TotalShards = 2 | |||
| }; | |||
| _client = new DiscordShardedClient(config); | |||
| var services = ConfigureServices(); | |||
| // The Sharded Client does not have a Ready event. | |||
| // The ShardReady event is used instead, allowing for individual | |||
| // control per shard. | |||
| _client.ShardReady += ReadyAsync; | |||
| _client.Log += LogAsync; | |||
| await services.GetRequiredService<CommandHandlingService>().InitializeAsync(); | |||
| await _client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token")); | |||
| await _client.StartAsync(); | |||
| await Task.Delay(-1); | |||
| } | |||
| private IServiceProvider ConfigureServices() | |||
| { | |||
| return new ServiceCollection() | |||
| .AddSingleton(_client) | |||
| .AddSingleton<CommandService>() | |||
| .AddSingleton<CommandHandlingService>() | |||
| .BuildServiceProvider(); | |||
| } | |||
| private Task ReadyAsync(DiscordSocketClient shard) | |||
| { | |||
| Console.WriteLine($"Shard Number {shard.ShardId} is connected and ready!"); | |||
| return Task.CompletedTask; | |||
| } | |||
| private Task LogAsync(LogMessage log) | |||
| { | |||
| Console.WriteLine(log.ToString()); | |||
| return Task.CompletedTask; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,72 @@ | |||
| using System; | |||
| using System.Reflection; | |||
| using System.Threading.Tasks; | |||
| using Microsoft.Extensions.DependencyInjection; | |||
| using Discord; | |||
| using Discord.Commands; | |||
| using Discord.WebSocket; | |||
| namespace _03_sharded_client.Services | |||
| { | |||
| public class CommandHandlingService | |||
| { | |||
| private readonly CommandService _commands; | |||
| private readonly DiscordShardedClient _discord; | |||
| private readonly IServiceProvider _services; | |||
| public CommandHandlingService(IServiceProvider services) | |||
| { | |||
| _commands = services.GetRequiredService<CommandService>(); | |||
| _discord = services.GetRequiredService<DiscordShardedClient>(); | |||
| _services = services; | |||
| _commands.CommandExecuted += CommandExecutedAsync; | |||
| _commands.Log += LogAsync; | |||
| _discord.MessageReceived += MessageReceivedAsync; | |||
| } | |||
| public async Task InitializeAsync() | |||
| { | |||
| await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); | |||
| } | |||
| public async Task MessageReceivedAsync(SocketMessage rawMessage) | |||
| { | |||
| // Ignore system messages, or messages from other bots | |||
| if (!(rawMessage is SocketUserMessage message)) | |||
| return; | |||
| if (message.Source != MessageSource.User) | |||
| return; | |||
| // This value holds the offset where the prefix ends | |||
| var argPos = 0; | |||
| if (!message.HasMentionPrefix(_discord.CurrentUser, ref argPos)) | |||
| return; | |||
| // A new kind of command context, ShardedCommandContext can be utilized with the commands framework | |||
| var context = new ShardedCommandContext(_discord, message); | |||
| await _commands.ExecuteAsync(context, argPos, _services); | |||
| } | |||
| public async Task CommandExecutedAsync(Optional<CommandInfo> command, ICommandContext context, IResult result) | |||
| { | |||
| // command is unspecified when there was a search failure (command not found); we don't care about these errors | |||
| if (!command.IsSpecified) | |||
| return; | |||
| // the command was succesful, we don't care about this result, unless we want to log that a command succeeded. | |||
| if (result.IsSuccess) | |||
| return; | |||
| // the command failed, let's notify the user that something happened. | |||
| await context.Channel.SendMessageAsync($"error: {result.ToString()}"); | |||
| } | |||
| private Task LogAsync(LogMessage log) | |||
| { | |||
| Console.WriteLine(log.ToString()); | |||
| return Task.CompletedTask; | |||
| } | |||
| } | |||
| } | |||
| @@ -49,8 +49,8 @@ namespace Discord.Commands | |||
| /// Should the command encounter any of the aforementioned error, this event will not be raised. | |||
| /// </para> | |||
| /// </remarks> | |||
| public event Func<CommandInfo, ICommandContext, IResult, Task> CommandExecuted { add { _commandExecutedEvent.Add(value); } remove { _commandExecutedEvent.Remove(value); } } | |||
| internal readonly AsyncEvent<Func<CommandInfo, ICommandContext, IResult, Task>> _commandExecutedEvent = new AsyncEvent<Func<CommandInfo, ICommandContext, IResult, Task>>(); | |||
| public event Func<Optional<CommandInfo>, ICommandContext, IResult, Task> CommandExecuted { add { _commandExecutedEvent.Add(value); } remove { _commandExecutedEvent.Remove(value); } } | |||
| internal readonly AsyncEvent<Func<Optional<CommandInfo>, ICommandContext, IResult, Task>> _commandExecutedEvent = new AsyncEvent<Func<Optional<CommandInfo>, ICommandContext, IResult, Task>>(); | |||
| private readonly SemaphoreSlim _moduleLock; | |||
| private readonly ConcurrentDictionary<Type, ModuleInfo> _typedModuleDefs; | |||
| @@ -512,7 +512,11 @@ namespace Discord.Commands | |||
| var searchResult = Search(input); | |||
| if (!searchResult.IsSuccess) | |||
| { | |||
| await _commandExecutedEvent.InvokeAsync(Optional.Create<CommandInfo>(), context, searchResult).ConfigureAwait(false); | |||
| return searchResult; | |||
| } | |||
| var commands = searchResult.Commands; | |||
| var preconditionResults = new Dictionary<CommandMatch, PreconditionResult>(); | |||
| @@ -532,6 +536,8 @@ namespace Discord.Commands | |||
| var bestCandidate = preconditionResults | |||
| .OrderByDescending(x => x.Key.Command.Priority) | |||
| .FirstOrDefault(x => !x.Value.IsSuccess); | |||
| await _commandExecutedEvent.InvokeAsync(bestCandidate.Key.Command, context, bestCandidate.Value).ConfigureAwait(false); | |||
| return bestCandidate.Value; | |||
| } | |||
| @@ -589,12 +595,17 @@ namespace Discord.Commands | |||
| //All parses failed, return the one from the highest priority command, using score as a tie breaker | |||
| var bestMatch = parseResults | |||
| .FirstOrDefault(x => !x.Value.IsSuccess); | |||
| await _commandExecutedEvent.InvokeAsync(bestMatch.Key.Command, context, bestMatch.Value).ConfigureAwait(false); | |||
| return bestMatch.Value; | |||
| } | |||
| //If we get this far, at least one parse was successful. Execute the most likely overload. | |||
| var chosenOverload = successfulParses[0]; | |||
| return await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, services).ConfigureAwait(false); | |||
| var result = await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, services).ConfigureAwait(false); | |||
| if (!result.IsSuccess) // succesful results raise the event in CommandInfo#ExecuteInternalAsync (have to raise it there b/c deffered execution) | |||
| await _commandExecutedEvent.InvokeAsync(chosenOverload.Key.Command, context, result); | |||
| return result; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,46 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.Commands | |||
| { | |||
| public static class CommandServiceExtensions | |||
| { | |||
| public static async Task<IReadOnlyCollection<CommandInfo>> GetExecutableCommandsAsync(this ICollection<CommandInfo> commands, ICommandContext context, IServiceProvider provider) | |||
| { | |||
| var executableCommands = new List<CommandInfo>(); | |||
| var tasks = commands.Select(async c => | |||
| { | |||
| var result = await c.CheckPreconditionsAsync(context, provider).ConfigureAwait(false); | |||
| return new { Command = c, PreconditionResult = result }; | |||
| }); | |||
| var results = await Task.WhenAll(tasks).ConfigureAwait(false); | |||
| foreach (var result in results) | |||
| { | |||
| if (result.PreconditionResult.IsSuccess) | |||
| executableCommands.Add(result.Command); | |||
| } | |||
| return executableCommands; | |||
| } | |||
| public static Task<IReadOnlyCollection<CommandInfo>> GetExecutableCommandsAsync(this CommandService commandService, ICommandContext context, IServiceProvider provider) | |||
| => GetExecutableCommandsAsync(commandService.Commands.ToArray(), context, provider); | |||
| public static async Task<IReadOnlyCollection<CommandInfo>> GetExecutableCommandsAsync(this ModuleInfo module, ICommandContext context, IServiceProvider provider) | |||
| { | |||
| var executableCommands = new List<CommandInfo>(); | |||
| executableCommands.AddRange(await module.Commands.ToArray().GetExecutableCommandsAsync(context, provider).ConfigureAwait(false)); | |||
| var tasks = module.Submodules.Select(async s => await s.GetExecutableCommandsAsync(context, provider).ConfigureAwait(false)); | |||
| var results = await Task.WhenAll(tasks).ConfigureAwait(false); | |||
| executableCommands.AddRange(results.SelectMany(c => c)); | |||
| return executableCommands; | |||
| } | |||
| } | |||
| } | |||
| @@ -272,6 +272,10 @@ namespace Discord.Commands | |||
| var wrappedEx = new CommandException(this, context, ex); | |||
| await Module.Service._cmdLogger.ErrorAsync(wrappedEx).ConfigureAwait(false); | |||
| var result = ExecuteResult.FromError(ex); | |||
| await Module.Service._commandExecutedEvent.InvokeAsync(this, context, result).ConfigureAwait(false); | |||
| if (Module.Service._throwOnError) | |||
| { | |||
| if (ex == originalEx) | |||
| @@ -280,7 +284,7 @@ namespace Discord.Commands | |||
| ExceptionDispatchInfo.Capture(ex).Throw(); | |||
| } | |||
| return ExecuteResult.FromError(CommandError.Exception, ex.Message); | |||
| return result; | |||
| } | |||
| finally | |||
| { | |||
| @@ -25,5 +25,10 @@ namespace Discord | |||
| /// representing the parent of this channel; <c>null</c> if none is set. | |||
| /// </returns> | |||
| Task<ICategoryChannel> GetCategoryAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); | |||
| /// <summary> | |||
| /// Syncs the permissions of this nested channel with its parent's. | |||
| /// </summary> | |||
| Task SyncPermissionsAsync(RequestOptions options = null); | |||
| } | |||
| } | |||
| @@ -0,0 +1,13 @@ | |||
| namespace Discord | |||
| { | |||
| public enum ExplicitContentFilterLevel | |||
| { | |||
| /// <summary> No messages will be scanned. </summary> | |||
| Disabled = 0, | |||
| /// <summary> Scans messages from all guild members that do not have a role. </summary> | |||
| /// <remarks> Recommented option for servers that use roles for trusted membership. </remarks> | |||
| MembersWithoutRoles = 1, | |||
| /// <summary> Scan messages sent by all guild members. </summary> | |||
| AllMembers = 2 | |||
| } | |||
| } | |||
| @@ -66,5 +66,9 @@ namespace Discord | |||
| /// Gets or sets the ID of the owner of this guild. | |||
| /// </summary> | |||
| public Optional<ulong> OwnerId { get; set; } | |||
| /// <summary> | |||
| /// Gets or sets the explicit content filter level of this guild. | |||
| /// </summary> | |||
| public Optional<ExplicitContentFilterLevel> ExplicitContentFilter { get; set; } | |||
| } | |||
| } | |||
| @@ -53,6 +53,13 @@ namespace Discord | |||
| /// </returns> | |||
| VerificationLevel VerificationLevel { get; } | |||
| /// <summary> | |||
| /// Gets the level of content filtering applied to user's content in a Guild. | |||
| /// </summary> | |||
| /// <returns> | |||
| /// The level of explicit content filtering. | |||
| /// </returns> | |||
| ExplicitContentFilterLevel ExplicitContentFilter { get; } | |||
| /// <summary> | |||
| /// Gets the ID of this guild's icon. | |||
| /// </summary> | |||
| /// <returns> | |||
| @@ -141,6 +148,13 @@ namespace Discord | |||
| /// </returns> | |||
| ulong OwnerId { get; } | |||
| /// <summary> | |||
| /// Gets the application ID of the guild creator if it is bot-created. | |||
| /// </summary> | |||
| /// <returns> | |||
| /// A <see cref="ulong"/> representing the snowflake identifier of the application ID that created this guild, or <c>null</c> if it was not bot-created. | |||
| /// </returns> | |||
| ulong? ApplicationId { get; } | |||
| /// <summary> | |||
| /// Gets the ID of the region hosting this guild's voice channels. | |||
| /// </summary> | |||
| /// <returns> | |||
| @@ -100,5 +100,25 @@ namespace Discord | |||
| /// A read-only collection of user IDs. | |||
| /// </returns> | |||
| IReadOnlyCollection<ulong> MentionedUserIds { get; } | |||
| /// <summary> | |||
| /// Returns the Activity associated with a message. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// Sent with Rich Presence-related chat embeds. | |||
| /// </remarks> | |||
| /// <returns> | |||
| /// A message's activity, if any is associated. | |||
| /// </returns> | |||
| MessageActivity Activity { get; } | |||
| /// <summary> | |||
| /// Returns the Application associated with a messsage. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// Sent with Rich-Presence-related chat embeds. | |||
| /// </remarks> | |||
| /// <returns> | |||
| /// A message's application, if any is associated. | |||
| /// </returns> | |||
| MessageApplication Application { get; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,27 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Diagnostics; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Discord | |||
| { | |||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
| public class MessageActivity | |||
| { | |||
| /// <summary> | |||
| /// Gets the type of activity of this message. | |||
| /// </summary> | |||
| public MessageActivityType Type { get; internal set; } | |||
| /// <summary> | |||
| /// Gets the party ID of this activity, if any. | |||
| /// </summary> | |||
| public string PartyId { get; internal set; } | |||
| private string DebuggerDisplay | |||
| => $"{Type}{(string.IsNullOrWhiteSpace(PartyId) ? "" : $" {PartyId}")}"; | |||
| public override string ToString() => DebuggerDisplay; | |||
| } | |||
| } | |||
| @@ -0,0 +1,16 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Discord | |||
| { | |||
| public enum MessageActivityType | |||
| { | |||
| Join = 1, | |||
| Spectate = 2, | |||
| Listen = 3, | |||
| JoinRequest = 5 | |||
| } | |||
| } | |||
| @@ -0,0 +1,43 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Diagnostics; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Discord | |||
| { | |||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
| public class MessageApplication | |||
| { | |||
| /// <summary> | |||
| /// Gets the snowflake ID of the application. | |||
| /// </summary> | |||
| public ulong Id { get; internal set; } | |||
| /// <summary> | |||
| /// Gets the ID of the embed's image asset. | |||
| /// </summary> | |||
| public string CoverImage { get; internal set; } | |||
| /// <summary> | |||
| /// Gets the application's description. | |||
| /// </summary> | |||
| public string Description { get; internal set; } | |||
| /// <summary> | |||
| /// Gets the ID of the application's icon. | |||
| /// </summary> | |||
| public string Icon { get; internal set; } | |||
| /// <summary> | |||
| /// Gets the Url of the application's icon. | |||
| /// </summary> | |||
| public string IconUrl | |||
| => $"https://cdn.discordapp.com/app-icons/{Id}/{Icon}"; | |||
| /// <summary> | |||
| /// Gets the name of the application. | |||
| /// </summary> | |||
| public string Name { get; internal set; } | |||
| private string DebuggerDisplay | |||
| => $"{Name} ({Id}): {Description}"; | |||
| public override string ToString() | |||
| => DebuggerDisplay; | |||
| } | |||
| } | |||
| @@ -1,4 +1,6 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| namespace Discord | |||
| { | |||
| @@ -65,5 +67,15 @@ namespace Discord | |||
| return builder; | |||
| } | |||
| public static EmbedBuilder WithFields(this EmbedBuilder builder, IEnumerable<EmbedFieldBuilder> fields) | |||
| { | |||
| foreach (var field in fields) | |||
| builder.AddField(field); | |||
| return builder; | |||
| } | |||
| public static EmbedBuilder WithFields(this EmbedBuilder builder, params EmbedFieldBuilder[] fields) | |||
| => WithFields(builder, fields.AsEnumerable()); | |||
| } | |||
| } | |||
| @@ -1,4 +1,4 @@ | |||
| #pragma warning disable CS1591 | |||
| #pragma warning disable CS1591 | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API | |||
| @@ -25,10 +25,12 @@ namespace Discord.API | |||
| public bool EmbedEnabled { get; set; } | |||
| [JsonProperty("embed_channel_id")] | |||
| public ulong? EmbedChannelId { get; set; } | |||
| [JsonProperty("system_channel_id")] | |||
| public ulong? SystemChannelId { get; set; } | |||
| [JsonProperty("verification_level")] | |||
| public VerificationLevel VerificationLevel { get; set; } | |||
| [JsonProperty("default_message_notifications")] | |||
| public DefaultMessageNotifications DefaultMessageNotifications { get; set; } | |||
| [JsonProperty("explicit_content_filter")] | |||
| public ExplicitContentFilterLevel ExplicitContentFilter { get; set; } | |||
| [JsonProperty("voice_states")] | |||
| public VoiceState[] VoiceStates { get; set; } | |||
| [JsonProperty("roles")] | |||
| @@ -39,7 +41,9 @@ namespace Discord.API | |||
| public string[] Features { get; set; } | |||
| [JsonProperty("mfa_level")] | |||
| public MfaLevel MfaLevel { get; set; } | |||
| [JsonProperty("default_message_notifications")] | |||
| public DefaultMessageNotifications DefaultMessageNotifications { get; set; } | |||
| [JsonProperty("application_id")] | |||
| public ulong? ApplicationId { get; set; } | |||
| [JsonProperty("system_channel_id")] | |||
| public ulong? SystemChannelId { get; set; } | |||
| } | |||
| } | |||
| @@ -44,5 +44,11 @@ namespace Discord.API | |||
| public Optional<bool> Pinned { get; set; } | |||
| [JsonProperty("reactions")] | |||
| public Optional<Reaction[]> Reactions { get; set; } | |||
| // sent with Rich Presence-related chat embeds | |||
| [JsonProperty("activity")] | |||
| public Optional<MessageActivity> Activity { get; set; } | |||
| // sent with Rich Presence-related chat embeds | |||
| [JsonProperty("application")] | |||
| public Optional<MessageApplication> Application { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,17 @@ | |||
| using Newtonsoft.Json; | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.API | |||
| { | |||
| public class MessageActivity | |||
| { | |||
| [JsonProperty("type")] | |||
| public Optional<MessageActivityType> Type { get; set; } | |||
| [JsonProperty("party_id")] | |||
| public Optional<string> PartyId { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,38 @@ | |||
| using Newtonsoft.Json; | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.API | |||
| { | |||
| public class MessageApplication | |||
| { | |||
| /// <summary> | |||
| /// Gets the snowflake ID of the application. | |||
| /// </summary> | |||
| [JsonProperty("id")] | |||
| public ulong Id { get; set; } | |||
| /// <summary> | |||
| /// Gets the ID of the embed's image asset. | |||
| /// </summary> | |||
| [JsonProperty("cover_image")] | |||
| public string CoverImage { get; set; } | |||
| /// <summary> | |||
| /// Gets the application's description. | |||
| /// </summary> | |||
| [JsonProperty("description")] | |||
| public string Description { get; set; } | |||
| /// <summary> | |||
| /// Gets the ID of the application's icon. | |||
| /// </summary> | |||
| [JsonProperty("icon")] | |||
| public string Icon { get; set; } | |||
| /// <summary> | |||
| /// Gets the name of the application. | |||
| /// </summary> | |||
| [JsonProperty("name")] | |||
| public string Name { get; set; } | |||
| } | |||
| } | |||
| @@ -1,4 +1,4 @@ | |||
| #pragma warning disable CS1591 | |||
| #pragma warning disable CS1591 | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API | |||
| @@ -1,4 +1,4 @@ | |||
| #pragma warning disable CS1591 | |||
| #pragma warning disable CS1591 | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API.Rest | |||
| @@ -12,5 +12,7 @@ namespace Discord.API.Rest | |||
| public Optional<int> Position { get; set; } | |||
| [JsonProperty("parent_id")] | |||
| public Optional<ulong?> CategoryId { get; set; } | |||
| [JsonProperty("permission_overwrites")] | |||
| public Optional<Overwrite[]> Overwrites { get; set; } | |||
| } | |||
| } | |||
| @@ -1,4 +1,4 @@ | |||
| #pragma warning disable CS1591 | |||
| #pragma warning disable CS1591 | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API.Rest | |||
| @@ -28,5 +28,7 @@ namespace Discord.API.Rest | |||
| public Optional<ulong?> AfkChannelId { get; set; } | |||
| [JsonProperty("owner_id")] | |||
| public Optional<ulong> OwnerId { get; set; } | |||
| [JsonProperty("explicit_content_filter")] | |||
| public Optional<ExplicitContentFilterLevel> ExplicitContentFilter { get; set; } | |||
| } | |||
| } | |||
| @@ -1,3 +1,4 @@ | |||
| #pragma warning disable CS1591 | |||
| using Discord.API.Rest; | |||
| using Discord.Net; | |||
| @@ -977,6 +978,8 @@ namespace Discord.API | |||
| Preconditions.NotNull(args, nameof(args)); | |||
| Preconditions.AtLeast(args.MaxAge, 0, nameof(args.MaxAge)); | |||
| Preconditions.AtLeast(args.MaxUses, 0, nameof(args.MaxUses)); | |||
| Preconditions.AtMost(args.MaxAge, 86400, nameof(args.MaxAge), | |||
| "The maximum age of an invite must be less than or equal to a day (86400 seconds)."); | |||
| options = RequestOptions.CreateOrClone(options); | |||
| var ids = new BucketIds(channelId: channelId); | |||
| @@ -1426,8 +1429,11 @@ namespace Discord.API | |||
| lastIndex = rightIndex + 1; | |||
| } | |||
| if (builder[builder.Length - 1] == '/') | |||
| builder.Remove(builder.Length - 1, 1); | |||
| format = builder.ToString(); | |||
| return x => string.Format(format, x.ToArray()); | |||
| } | |||
| catch (Exception ex) | |||
| @@ -76,9 +76,13 @@ namespace Discord.Rest | |||
| public static async Task<RestInviteMetadata> CreateInviteAsync(IGuildChannel channel, BaseDiscordClient client, | |||
| int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) | |||
| { | |||
| var args = new CreateChannelInviteParams { IsTemporary = isTemporary, IsUnique = isUnique }; | |||
| args.MaxAge = maxAge.GetValueOrDefault(0); | |||
| args.MaxUses = maxUses.GetValueOrDefault(0); | |||
| var args = new API.Rest.CreateChannelInviteParams | |||
| { | |||
| IsTemporary = isTemporary, | |||
| IsUnique = isUnique, | |||
| MaxAge = maxAge ?? 0, | |||
| MaxUses = maxUses ?? 0 | |||
| }; | |||
| var model = await client.ApiClient.CreateChannelInviteAsync(channel.Id, args, options).ConfigureAwait(false); | |||
| return RestInviteMetadata.Create(client, null, channel, model); | |||
| } | |||
| @@ -348,6 +352,23 @@ namespace Discord.Rest | |||
| var model = await client.ApiClient.GetChannelAsync(channel.CategoryId.Value, options).ConfigureAwait(false); | |||
| return RestCategoryChannel.Create(client, model) as ICategoryChannel; | |||
| } | |||
| public static async Task SyncPermissionsAsync(INestedChannel channel, BaseDiscordClient client, RequestOptions options) | |||
| { | |||
| var category = await GetCategoryAsync(channel, client, options).ConfigureAwait(false); | |||
| if (category == null) throw new InvalidOperationException("This channel does not have a parent channel."); | |||
| var apiArgs = new ModifyGuildChannelParams | |||
| { | |||
| Overwrites = category.PermissionOverwrites | |||
| .Select(overwrite => new API.Overwrite{ | |||
| TargetId = overwrite.TargetId, | |||
| TargetType = overwrite.TargetType, | |||
| Allow = overwrite.Permissions.AllowValue, | |||
| Deny = overwrite.Permissions.DenyValue | |||
| }).ToArray() | |||
| }; | |||
| await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); | |||
| } | |||
| //Helpers | |||
| private static IUser GetAuthor(BaseDiscordClient client, IGuild guild, UserModel model, ulong? webhookId) | |||
| @@ -201,6 +201,8 @@ namespace Discord.Rest | |||
| /// </returns> | |||
| public Task<ICategoryChannel> GetCategoryAsync(RequestOptions options = null) | |||
| => ChannelHelper.GetCategoryAsync(this, Discord, options); | |||
| public Task SyncPermissionsAsync(RequestOptions options = null) | |||
| => ChannelHelper.SyncPermissionsAsync(this, Discord, options); | |||
| private string DebuggerDisplay => $"{Name} ({Id}, Text)"; | |||
| @@ -57,6 +57,8 @@ namespace Discord.Rest | |||
| /// </returns> | |||
| public Task<ICategoryChannel> GetCategoryAsync(RequestOptions options = null) | |||
| => ChannelHelper.GetCategoryAsync(this, Discord, options); | |||
| public Task SyncPermissionsAsync(RequestOptions options = null) | |||
| => ChannelHelper.SyncPermissionsAsync(this, Discord, options); | |||
| private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; | |||
| @@ -32,7 +32,8 @@ namespace Discord.Rest | |||
| Icon = args.Icon.IsSpecified ? args.Icon.Value?.ToModel() : Optional.Create<ImageModel?>(), | |||
| Name = args.Name, | |||
| Splash = args.Splash.IsSpecified ? args.Splash.Value?.ToModel() : Optional.Create<ImageModel?>(), | |||
| VerificationLevel = args.VerificationLevel | |||
| VerificationLevel = args.VerificationLevel, | |||
| ExplicitContentFilter = args.ExplicitContentFilter | |||
| }; | |||
| if (args.AfkChannel.IsSpecified) | |||
| @@ -60,6 +61,9 @@ namespace Discord.Rest | |||
| if (!apiArgs.Icon.IsSpecified && guild.IconId != null) | |||
| apiArgs.Icon = new ImageModel(guild.IconId); | |||
| if (args.ExplicitContentFilter.IsSpecified) | |||
| apiArgs.ExplicitContentFilter = args.ExplicitContentFilter.Value; | |||
| return await client.ApiClient.ModifyGuildAsync(guild.Id, apiArgs, options).ConfigureAwait(false); | |||
| } | |||
| /// <exception cref="ArgumentNullException"><paramref name="func"/> is <c>null</c>.</exception> | |||
| @@ -32,6 +32,8 @@ namespace Discord.Rest | |||
| public MfaLevel MfaLevel { get; private set; } | |||
| /// <inheritdoc /> | |||
| public DefaultMessageNotifications DefaultMessageNotifications { get; private set; } | |||
| /// <inheritdoc /> | |||
| public ExplicitContentFilterLevel ExplicitContentFilter { get; private set; } | |||
| /// <inheritdoc /> | |||
| public ulong? AFKChannelId { get; private set; } | |||
| @@ -48,6 +50,8 @@ namespace Discord.Rest | |||
| /// <inheritdoc /> | |||
| public string SplashId { get; private set; } | |||
| internal bool Available { get; private set; } | |||
| /// <inheritdoc /> | |||
| public ulong? ApplicationId { get; private set; } | |||
| /// <inheritdoc /> | |||
| public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | |||
| @@ -98,6 +102,8 @@ namespace Discord.Rest | |||
| VerificationLevel = model.VerificationLevel; | |||
| MfaLevel = model.MfaLevel; | |||
| DefaultMessageNotifications = model.DefaultMessageNotifications; | |||
| ExplicitContentFilter = model.ExplicitContentFilter; | |||
| ApplicationId = model.ApplicationId; | |||
| if (model.Emojis != null) | |||
| { | |||
| @@ -55,6 +55,10 @@ namespace Discord.Rest | |||
| /// <inheritdoc /> | |||
| public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); | |||
| /// <inheritdoc /> | |||
| public MessageActivity Activity { get; private set; } | |||
| /// <inheritdoc /> | |||
| public MessageApplication Application { get; private set; } | |||
| internal RestMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author, MessageSource source) | |||
| : base(discord, id) | |||
| @@ -77,6 +81,29 @@ namespace Discord.Rest | |||
| if (model.Content.IsSpecified) | |||
| Content = model.Content.Value; | |||
| if (model.Application.IsSpecified) | |||
| { | |||
| // create a new Application from the API model | |||
| Application = new MessageApplication() | |||
| { | |||
| Id = model.Application.Value.Id, | |||
| CoverImage = model.Application.Value.CoverImage, | |||
| Description = model.Application.Value.Description, | |||
| Icon = model.Application.Value.Icon, | |||
| Name = model.Application.Value.Name | |||
| }; | |||
| } | |||
| if (model.Activity.IsSpecified) | |||
| { | |||
| // create a new Activity from the API model | |||
| Activity = new MessageActivity() | |||
| { | |||
| Type = model.Activity.Value.Type.Value, | |||
| PartyId = model.Activity.Value.PartyId.Value | |||
| }; | |||
| } | |||
| } | |||
| /// <inheritdoc /> | |||
| @@ -31,6 +31,8 @@ namespace Discord.WebSocket | |||
| /// </returns> | |||
| public ICategoryChannel Category | |||
| => CategoryId.HasValue ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel : null; | |||
| public Task SyncPermissionsAsync(RequestOptions options = null) | |||
| => ChannelHelper.SyncPermissionsAsync(this, Discord, options); | |||
| private bool _nsfw; | |||
| /// <inheritdoc /> | |||
| @@ -30,6 +30,8 @@ namespace Discord.WebSocket | |||
| /// </returns> | |||
| public ICategoryChannel Category | |||
| => CategoryId.HasValue ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel : null; | |||
| public Task SyncPermissionsAsync(RequestOptions options = null) | |||
| => ChannelHelper.SyncPermissionsAsync(this, Discord, options); | |||
| /// <inheritdoc /> | |||
| public override IReadOnlyCollection<SocketGuildUser> Users | |||
| @@ -50,6 +50,8 @@ namespace Discord.WebSocket | |||
| public MfaLevel MfaLevel { get; private set; } | |||
| /// <inheritdoc /> | |||
| public DefaultMessageNotifications DefaultMessageNotifications { get; private set; } | |||
| /// <inheritdoc /> | |||
| public ExplicitContentFilterLevel ExplicitContentFilter { get; private set; } | |||
| /// <summary> | |||
| /// Gets the number of members. | |||
| /// </summary> | |||
| @@ -73,6 +75,8 @@ namespace Discord.WebSocket | |||
| internal bool IsAvailable { get; private set; } | |||
| /// <summary> Indicates whether the client is connected to this guild. </summary> | |||
| public bool IsConnected { get; internal set; } | |||
| /// <inheritdoc /> | |||
| public ulong? ApplicationId { get; internal set; } | |||
| internal ulong? AFKChannelId { get; private set; } | |||
| internal ulong? EmbedChannelId { get; private set; } | |||
| @@ -346,6 +350,8 @@ namespace Discord.WebSocket | |||
| VerificationLevel = model.VerificationLevel; | |||
| MfaLevel = model.MfaLevel; | |||
| DefaultMessageNotifications = model.DefaultMessageNotifications; | |||
| ExplicitContentFilter = model.ExplicitContentFilter; | |||
| ApplicationId = model.ApplicationId; | |||
| if (model.Emojis != null) | |||
| { | |||
| @@ -43,6 +43,13 @@ namespace Discord.WebSocket | |||
| public virtual bool IsPinned => false; | |||
| /// <inheritdoc /> | |||
| public virtual DateTimeOffset? EditedTimestamp => null; | |||
| /// <inheritdoc /> | |||
| public MessageActivity Activity { get; private set; } | |||
| /// <inheritdoc /> | |||
| public MessageApplication Application { get; private set; } | |||
| /// <summary> | |||
| /// Returns all attachments included in this message. | |||
| /// </summary> | |||
| @@ -105,6 +112,29 @@ namespace Discord.WebSocket | |||
| if (model.Content.IsSpecified) | |||
| Content = model.Content.Value; | |||
| if (model.Application.IsSpecified) | |||
| { | |||
| // create a new Application from the API model | |||
| Application = new MessageApplication() | |||
| { | |||
| Id = model.Application.Value.Id, | |||
| CoverImage = model.Application.Value.CoverImage, | |||
| Description = model.Application.Value.Description, | |||
| Icon = model.Application.Value.Icon, | |||
| Name = model.Application.Value.Name | |||
| }; | |||
| } | |||
| if (model.Activity.IsSpecified) | |||
| { | |||
| // create a new Activity from the API model | |||
| Activity = new MessageActivity() | |||
| { | |||
| Type = model.Activity.Value.Type.Value, | |||
| PartyId = model.Activity.Value.PartyId.Value | |||
| }; | |||
| } | |||
| } | |||
| /// <inheritdoc /> | |||
| @@ -1,7 +1,10 @@ | |||
| using Discord.Rest; | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Collections.Immutable; | |||
| using System.Diagnostics; | |||
| using System.Linq; | |||
| using System.Threading.Tasks; | |||
| using Discord.Rest; | |||
| using Model = Discord.API.User; | |||
| namespace Discord.WebSocket | |||
| @@ -35,6 +38,8 @@ namespace Discord.WebSocket | |||
| public IActivity Activity => Presence.Activity; | |||
| /// <inheritdoc /> | |||
| public UserStatus Status => Presence.Status; | |||
| public IReadOnlyCollection<SocketGuild> MutualGuilds | |||
| => Discord.Guilds.Where(g => g.Users.Any(u => u.Id == Id)).ToImmutableArray(); | |||
| internal SocketUser(DiscordSocketClient discord, ulong id) | |||
| : base(discord, id) | |||
| @@ -5,8 +5,25 @@ using Xunit; | |||
| namespace Discord | |||
| { | |||
| public class GuidPermissionsTests | |||
| public partial class Tests | |||
| { | |||
| /// <summary> | |||
| /// Tests the behavior of modifying the ExplicitContentFilter property of a Guild. | |||
| /// </summary> | |||
| [Fact] | |||
| public async Task TestExplicitContentFilter() | |||
| { | |||
| foreach (var level in Enum.GetValues(typeof(ExplicitContentFilterLevel))) | |||
| { | |||
| await _guild.ModifyAsync(x => x.ExplicitContentFilter = (ExplicitContentFilterLevel)level); | |||
| await _guild.UpdateAsync(); | |||
| Assert.Equal(level, _guild.ExplicitContentFilter); | |||
| } | |||
| } | |||
| /// <summary> | |||
| /// Tests the behavior of the GuildPermissions class. | |||
| /// </summary> | |||
| [Fact] | |||
| public Task TestGuildPermission() | |||
| { | |||