@@ -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 | EndProject | ||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}" | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}" | ||||
EndProject | 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 | EndProject | ||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "02_commands_framework", "samples\02_commands_framework\02_commands_framework.csproj", "{4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}" | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "02_commands_framework", "samples\02_commands_framework\02_commands_framework.csproj", "{4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}" | ||||
EndProject | 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 | Global | ||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution | GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||||
Debug|Any CPU = Debug|Any CPU | 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|x64.Build.0 = Release|Any CPU | ||||
{4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Release|x86.ActiveCfg = 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 | {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 | EndGlobalSection | ||||
GlobalSection(SolutionProperties) = preSolution | GlobalSection(SolutionProperties) = preSolution | ||||
HideSolutionNode = FALSE | HideSolutionNode = FALSE | ||||
@@ -173,6 +187,7 @@ Global | |||||
{BBA8E7FB-C834-40DC-822F-B112CB7F0140} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A} | {BBA8E7FB-C834-40DC-822F-B112CB7F0140} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A} | ||||
{F2FF84FB-F6AD-47E5-9EE5-18206CAE136E} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B} | {F2FF84FB-F6AD-47E5-9EE5-18206CAE136E} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B} | ||||
{4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76} = {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 | EndGlobalSection | ||||
GlobalSection(ExtensibilityGlobals) = postSolution | GlobalSection(ExtensibilityGlobals) = postSolution | ||||
SolutionGuid = {D2404771-EEC8-45F2-9D71-F3373F6C1495} | 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 | [MessageCacheSize]: xref:Discord.WebSocket.DiscordSocketConfig.MessageCacheSize | ||||
[DiscordSocketConfig]: xref:Discord.WebSocket.DiscordSocketConfig | [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 | // Create a number to track where the prefix ends and the command begins | ||||
int argPos = 0; | 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) || | if (!(message.HasCharPrefix('!', ref argPos) || | ||||
message.HasMentionPrefix(_client.CurrentUser, ref argPos))) | |||||
message.HasMentionPrefix(_client.CurrentUser, ref argPos)) || | |||||
message.Author.IsBot) | |||||
return; | return; | ||||
// Create a WebSocket-based command context based on the message | // Create a WebSocket-based command context based on the message | ||||
@@ -60,4 +61,4 @@ public class CommandHandler | |||||
// if (!result.IsSuccess) | // if (!result.IsSuccess) | ||||
// await context.Channel.SendMessageAsync(result.ErrorReason); | // await context.Channel.SendMessageAsync(result.ErrorReason); | ||||
} | } | ||||
} | |||||
} |
@@ -27,7 +27,7 @@ public async Task HandleCommandAsync(SocketMessage msg) | |||||
var message = messageParam as SocketUserMessage; | var message = messageParam as SocketUserMessage; | ||||
if (message == null) return; | if (message == null) return; | ||||
int argPos = 0; | 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 context = new SocketCommandContext(_client, message); | ||||
var result = await _commands.ExecuteAsync(context, argPos, _services); | var result = await _commands.ExecuteAsync(context, argPos, _services); | ||||
// Optionally, you may pass the result manually into your | // 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. | // precondition failures in the same method. | ||||
// await OnCommandExecutedAsync(null, context, result); | // await OnCommandExecutedAsync(null, context, result); | ||||
} | |||||
} |
@@ -1,9 +1,10 @@ | |||||
[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) | public async Task JoinChannel(IVoiceChannel channel = null) | ||||
{ | { | ||||
// Get the audio channel | // Get the audio channel | ||||
channel = channel ?? (msg.Author as IGuildUser)?.VoiceChannel; | |||||
if (channel == null) { await msg.Channel.SendMessageAsync("User must be in a voice channel, or a voice channel must be passed as an argument."); return; } | |||||
channel = channel ?? (Context.User as IGuildUser)?.VoiceChannel; | |||||
if (channel == null) { await Context.Channel.SendMessageAsync("User must be in a voice channel, or a voice channel must be passed as an argument."); return; } | |||||
// For the next step with transmitting audio, you would want to pass this Audio Client in to a service. | // For the next step with transmitting audio, you would want to pass this Audio Client in to a service. | ||||
var audioClient = await channel.ConnectAsync(); | var audioClient = await channel.ConnectAsync(); | ||||
@@ -11,16 +11,16 @@ Information is not guaranteed to be accurate. | |||||
## Installing | ## 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). | 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/). | 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. | source, or install them from your package manager. | ||||
[Sodium]: https://download.libsodium.org/libsodium/releases/ | [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 | ||||
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. | an [IAudioClient] to send data with. | ||||
To join a channel, simply await [ConnectAsync] on any instance of an | 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)] | [!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. | 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. | another voice channel in the guild. | ||||
[IAudioClient]: xref:Discord.Audio.IAudioClient | [IAudioClient]: xref:Discord.Audio.IAudioClient | ||||
[ConnectAsync]: xref:Discord.IAudioChannel.ConnectAsync* | [ConnectAsync]: xref:Discord.IAudioChannel.ConnectAsync* | ||||
[RunMode.Async]: xref:Discord.Commands.RunMode | |||||
## Transmitting Audio | ## Transmitting Audio | ||||
### With FFmpeg | ### 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. | 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's download page]. | ||||
[FFmpeg]: https://ffmpeg.org/ | [FFmpeg]: https://ffmpeg.org/ | ||||
[FFmpeg's download page]: https://ffmpeg.org/download.html | [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. | that you return PCM at 48000hz. | ||||
>[!NOTE] | >[!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. | >use the PCM write streams. | ||||
[!code-csharp[Creating FFmpeg](samples/audio_create_ffmpeg.cs)] | [!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]. | 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. | 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. | for `-ac 2` when creating FFmpeg. | ||||
[AudioOutStream]: xref:Discord.Audio.AudioOutStream | [AudioOutStream]: xref:Discord.Audio.AudioOutStream | ||||
[IAudioClient.CreatePCMStream]: xref:Discord.Audio.IAudioClient#Discord_Audio_IAudioClient_CreateDirectPCMStream_Discord_Audio_AudioApplication_System_Nullable_System_Int32__System_Int32_ | [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. | shown below. | ||||
[Stream.CopyToAsync]: https://msdn.microsoft.com/en-us/library/hh159084(v=vs.110).aspx | [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. | the audio client's internal buffer to clear out. | ||||
[!code-csharp[Sending Audio](samples/audio_ffmpeg.cs)] | [!code-csharp[Sending Audio](samples/audio_ffmpeg.cs)] |
@@ -20,6 +20,8 @@ namespace _02_commands_framework.Services | |||||
_discord = services.GetRequiredService<DiscordSocketClient>(); | _discord = services.GetRequiredService<DiscordSocketClient>(); | ||||
_services = services; | _services = services; | ||||
_commands.CommandExecuted += CommandExecutedAsync; | |||||
_commands.Log += LogAsync; | |||||
_discord.MessageReceived += MessageReceivedAsync; | _discord.MessageReceived += MessageReceivedAsync; | ||||
} | } | ||||
@@ -39,11 +41,28 @@ namespace _02_commands_framework.Services | |||||
if (!message.HasMentionPrefix(_discord.CurrentUser, ref argPos)) return; | if (!message.HasMentionPrefix(_discord.CurrentUser, ref argPos)) return; | ||||
var context = new SocketCommandContext(_discord, message); | 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; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,11 @@ | |||||
using System; | |||||
namespace Discord.Commands | |||||
{ | |||||
/// <summary> | |||||
/// Instructs the command system to treat command paramters of this type | |||||
/// as a collection of named arguments matching to its properties. | |||||
/// </summary> | |||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] | |||||
public sealed class NamedArgumentTypeAttribute : Attribute { } | |||||
} |
@@ -1,5 +1,4 @@ | |||||
using System; | using System; | ||||
using System.Reflection; | using System.Reflection; | ||||
namespace Discord.Commands | namespace Discord.Commands | ||||
@@ -27,8 +26,8 @@ namespace Discord.Commands | |||||
/// => ReplyAsync(time); | /// => ReplyAsync(time); | ||||
/// </code> | /// </code> | ||||
/// </example> | /// </example> | ||||
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] | |||||
public class OverrideTypeReaderAttribute : Attribute | |||||
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] | |||||
public sealed class OverrideTypeReaderAttribute : Attribute | |||||
{ | { | ||||
private static readonly TypeInfo TypeReaderTypeInfo = typeof(TypeReader).GetTypeInfo(); | private static readonly TypeInfo TypeReaderTypeInfo = typeof(TypeReader).GetTypeInfo(); | ||||
@@ -280,7 +280,7 @@ namespace Discord.Commands | |||||
} | } | ||||
} | } | ||||
private static TypeReader GetTypeReader(CommandService service, Type paramType, Type typeReaderType, IServiceProvider services) | |||||
internal static TypeReader GetTypeReader(CommandService service, Type paramType, Type typeReaderType, IServiceProvider services) | |||||
{ | { | ||||
var readers = service.GetTypeReaders(paramType); | var readers = service.GetTypeReaders(paramType); | ||||
TypeReader reader = null; | TypeReader reader = null; | ||||
@@ -56,11 +56,36 @@ namespace Discord.Commands.Builders | |||||
private TypeReader GetReader(Type type) | private TypeReader GetReader(Type type) | ||||
{ | { | ||||
var readers = Command.Module.Service.GetTypeReaders(type); | |||||
var commands = Command.Module.Service; | |||||
if (type.GetTypeInfo().GetCustomAttribute<NamedArgumentTypeAttribute>() != null) | |||||
{ | |||||
IsRemainder = true; | |||||
var reader = commands.GetTypeReaders(type)?.FirstOrDefault().Value; | |||||
if (reader == null) | |||||
{ | |||||
Type readerType; | |||||
try | |||||
{ | |||||
readerType = typeof(NamedArgumentTypeReader<>).MakeGenericType(new[] { type }); | |||||
} | |||||
catch (ArgumentException ex) | |||||
{ | |||||
throw new InvalidOperationException($"Parameter type '{type.Name}' for command '{Command.Name}' must be a class with a public parameterless constructor to use as a NamedArgumentType.", ex); | |||||
} | |||||
reader = (TypeReader)Activator.CreateInstance(readerType, new[] { commands }); | |||||
commands.AddTypeReader(type, reader); | |||||
} | |||||
return reader; | |||||
} | |||||
var readers = commands.GetTypeReaders(type); | |||||
if (readers != null) | if (readers != null) | ||||
return readers.FirstOrDefault().Value; | return readers.FirstOrDefault().Value; | ||||
else | else | ||||
return Command.Module.Service.GetDefaultTypeReader(type); | |||||
return commands.GetDefaultTypeReader(type); | |||||
} | } | ||||
public ParameterBuilder WithSummary(string summary) | public ParameterBuilder WithSummary(string summary) | ||||
@@ -49,8 +49,8 @@ namespace Discord.Commands | |||||
/// Should the command encounter any of the aforementioned error, this event will not be raised. | /// Should the command encounter any of the aforementioned error, this event will not be raised. | ||||
/// </para> | /// </para> | ||||
/// </remarks> | /// </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 SemaphoreSlim _moduleLock; | ||||
private readonly ConcurrentDictionary<Type, ModuleInfo> _typedModuleDefs; | private readonly ConcurrentDictionary<Type, ModuleInfo> _typedModuleDefs; | ||||
@@ -512,7 +512,11 @@ namespace Discord.Commands | |||||
var searchResult = Search(input); | var searchResult = Search(input); | ||||
if (!searchResult.IsSuccess) | if (!searchResult.IsSuccess) | ||||
{ | |||||
await _commandExecutedEvent.InvokeAsync(Optional.Create<CommandInfo>(), context, searchResult).ConfigureAwait(false); | |||||
return searchResult; | return searchResult; | ||||
} | |||||
var commands = searchResult.Commands; | var commands = searchResult.Commands; | ||||
var preconditionResults = new Dictionary<CommandMatch, PreconditionResult>(); | var preconditionResults = new Dictionary<CommandMatch, PreconditionResult>(); | ||||
@@ -532,6 +536,8 @@ namespace Discord.Commands | |||||
var bestCandidate = preconditionResults | var bestCandidate = preconditionResults | ||||
.OrderByDescending(x => x.Key.Command.Priority) | .OrderByDescending(x => x.Key.Command.Priority) | ||||
.FirstOrDefault(x => !x.Value.IsSuccess); | .FirstOrDefault(x => !x.Value.IsSuccess); | ||||
await _commandExecutedEvent.InvokeAsync(bestCandidate.Key.Command, context, bestCandidate.Value).ConfigureAwait(false); | |||||
return bestCandidate.Value; | 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 | //All parses failed, return the one from the highest priority command, using score as a tie breaker | ||||
var bestMatch = parseResults | var bestMatch = parseResults | ||||
.FirstOrDefault(x => !x.Value.IsSuccess); | .FirstOrDefault(x => !x.Value.IsSuccess); | ||||
await _commandExecutedEvent.InvokeAsync(bestMatch.Key.Command, context, bestMatch.Value).ConfigureAwait(false); | |||||
return bestMatch.Value; | return bestMatch.Value; | ||||
} | } | ||||
//If we get this far, at least one parse was successful. Execute the most likely overload. | //If we get this far, at least one parse was successful. Execute the most likely overload. | ||||
var chosenOverload = successfulParses[0]; | 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 && !(result is RuntimeResult)) // 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); | var wrappedEx = new CommandException(this, context, ex); | ||||
await Module.Service._cmdLogger.ErrorAsync(wrappedEx).ConfigureAwait(false); | 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 (Module.Service._throwOnError) | ||||
{ | { | ||||
if (ex == originalEx) | if (ex == originalEx) | ||||
@@ -280,7 +284,7 @@ namespace Discord.Commands | |||||
ExceptionDispatchInfo.Capture(ex).Throw(); | ExceptionDispatchInfo.Capture(ex).Throw(); | ||||
} | } | ||||
return ExecuteResult.FromError(CommandError.Exception, ex.Message); | |||||
return result; | |||||
} | } | ||||
finally | finally | ||||
{ | { | ||||
@@ -0,0 +1,191 @@ | |||||
using System; | |||||
using System.Collections; | |||||
using System.Collections.Generic; | |||||
using System.Collections.Immutable; | |||||
using System.Linq; | |||||
using System.Reflection; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.Commands | |||||
{ | |||||
internal sealed class NamedArgumentTypeReader<T> : TypeReader | |||||
where T : class, new() | |||||
{ | |||||
private static readonly IReadOnlyDictionary<string, PropertyInfo> _tProps = typeof(T).GetTypeInfo().DeclaredProperties | |||||
.Where(p => p.SetMethod != null && p.SetMethod.IsPublic && !p.SetMethod.IsStatic) | |||||
.ToImmutableDictionary(p => p.Name, StringComparer.OrdinalIgnoreCase); | |||||
private readonly CommandService _commands; | |||||
public NamedArgumentTypeReader(CommandService commands) | |||||
{ | |||||
_commands = commands; | |||||
} | |||||
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services) | |||||
{ | |||||
var result = new T(); | |||||
var state = ReadState.LookingForParameter; | |||||
int beginRead = 0, currentRead = 0; | |||||
while (state != ReadState.End) | |||||
{ | |||||
try | |||||
{ | |||||
var prop = Read(out var arg); | |||||
var propVal = await ReadArgumentAsync(prop, arg).ConfigureAwait(false); | |||||
if (propVal != null) | |||||
prop.SetMethod.Invoke(result, new[] { propVal }); | |||||
else | |||||
return TypeReaderResult.FromError(CommandError.ParseFailed, $"Could not parse the argument for the parameter '{prop.Name}' as type '{prop.PropertyType}'."); | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
//TODO: use the Exception overload after a rebase on latest | |||||
return TypeReaderResult.FromError(CommandError.Exception, ex.Message); | |||||
} | |||||
} | |||||
return TypeReaderResult.FromSuccess(result); | |||||
PropertyInfo Read(out string arg) | |||||
{ | |||||
string currentParam = null; | |||||
char match = '\0'; | |||||
for (; currentRead < input.Length; currentRead++) | |||||
{ | |||||
var currentChar = input[currentRead]; | |||||
switch (state) | |||||
{ | |||||
case ReadState.LookingForParameter: | |||||
if (Char.IsWhiteSpace(currentChar)) | |||||
continue; | |||||
else | |||||
{ | |||||
beginRead = currentRead; | |||||
state = ReadState.InParameter; | |||||
} | |||||
break; | |||||
case ReadState.InParameter: | |||||
if (currentChar != ':') | |||||
continue; | |||||
else | |||||
{ | |||||
currentParam = input.Substring(beginRead, currentRead - beginRead); | |||||
state = ReadState.LookingForArgument; | |||||
} | |||||
break; | |||||
case ReadState.LookingForArgument: | |||||
if (Char.IsWhiteSpace(currentChar)) | |||||
continue; | |||||
else | |||||
{ | |||||
beginRead = currentRead; | |||||
state = (QuotationAliasUtils.GetDefaultAliasMap.TryGetValue(currentChar, out match)) | |||||
? ReadState.InQuotedArgument | |||||
: ReadState.InArgument; | |||||
} | |||||
break; | |||||
case ReadState.InArgument: | |||||
if (!Char.IsWhiteSpace(currentChar)) | |||||
continue; | |||||
else | |||||
return GetPropAndValue(out arg); | |||||
case ReadState.InQuotedArgument: | |||||
if (currentChar != match) | |||||
continue; | |||||
else | |||||
return GetPropAndValue(out arg); | |||||
} | |||||
} | |||||
if (currentParam == null) | |||||
throw new InvalidOperationException("No parameter name was read."); | |||||
return GetPropAndValue(out arg); | |||||
PropertyInfo GetPropAndValue(out string argv) | |||||
{ | |||||
bool quoted = state == ReadState.InQuotedArgument; | |||||
state = (currentRead == (quoted ? input.Length - 1 : input.Length)) | |||||
? ReadState.End | |||||
: ReadState.LookingForParameter; | |||||
if (quoted) | |||||
{ | |||||
argv = input.Substring(beginRead + 1, currentRead - beginRead - 1).Trim(); | |||||
currentRead++; | |||||
} | |||||
else | |||||
argv = input.Substring(beginRead, currentRead - beginRead); | |||||
return _tProps[currentParam]; | |||||
} | |||||
} | |||||
async Task<object> ReadArgumentAsync(PropertyInfo prop, string arg) | |||||
{ | |||||
var elemType = prop.PropertyType; | |||||
bool isCollection = false; | |||||
if (elemType.GetTypeInfo().IsGenericType && elemType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) | |||||
{ | |||||
elemType = prop.PropertyType.GenericTypeArguments[0]; | |||||
isCollection = true; | |||||
} | |||||
var overridden = prop.GetCustomAttribute<OverrideTypeReaderAttribute>(); | |||||
var reader = (overridden != null) | |||||
? ModuleClassBuilder.GetTypeReader(_commands, elemType, overridden.TypeReader, services) | |||||
: (_commands.GetDefaultTypeReader(elemType) | |||||
?? _commands.GetTypeReaders(elemType).FirstOrDefault().Value); | |||||
if (reader != null) | |||||
{ | |||||
if (isCollection) | |||||
{ | |||||
var method = _readMultipleMethod.MakeGenericMethod(elemType); | |||||
var task = (Task<IEnumerable>)method.Invoke(null, new object[] { reader, context, arg.Split(','), services }); | |||||
return await task.ConfigureAwait(false); | |||||
} | |||||
else | |||||
return await ReadSingle(reader, context, arg, services).ConfigureAwait(false); | |||||
} | |||||
return null; | |||||
} | |||||
} | |||||
private static async Task<object> ReadSingle(TypeReader reader, ICommandContext context, string arg, IServiceProvider services) | |||||
{ | |||||
var readResult = await reader.ReadAsync(context, arg, services).ConfigureAwait(false); | |||||
return (readResult.IsSuccess) | |||||
? readResult.BestMatch | |||||
: null; | |||||
} | |||||
private static async Task<IEnumerable> ReadMultiple<TObj>(TypeReader reader, ICommandContext context, IEnumerable<string> args, IServiceProvider services) | |||||
{ | |||||
var objs = new List<TObj>(); | |||||
foreach (var arg in args) | |||||
{ | |||||
var read = await ReadSingle(reader, context, arg.Trim(), services).ConfigureAwait(false); | |||||
if (read != null) | |||||
objs.Add((TObj)read); | |||||
} | |||||
return objs.ToImmutableArray(); | |||||
} | |||||
private static readonly MethodInfo _readMultipleMethod = typeof(NamedArgumentTypeReader<T>) | |||||
.GetTypeInfo() | |||||
.DeclaredMethods | |||||
.Single(m => m.IsPrivate && m.IsStatic && m.Name == nameof(ReadMultiple)); | |||||
private enum ReadState | |||||
{ | |||||
LookingForParameter, | |||||
InParameter, | |||||
LookingForArgument, | |||||
InArgument, | |||||
InQuotedArgument, | |||||
End | |||||
} | |||||
} | |||||
} |
@@ -25,5 +25,10 @@ namespace Discord | |||||
/// representing the parent of this channel; <c>null</c> if none is set. | /// representing the parent of this channel; <c>null</c> if none is set. | ||||
/// </returns> | /// </returns> | ||||
Task<ICategoryChannel> GetCategoryAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); | 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. | /// Gets or sets the ID of the owner of this guild. | ||||
/// </summary> | /// </summary> | ||||
public Optional<ulong> OwnerId { get; set; } | 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> | /// </returns> | ||||
VerificationLevel VerificationLevel { get; } | VerificationLevel VerificationLevel { get; } | ||||
/// <summary> | /// <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. | /// Gets the ID of this guild's icon. | ||||
/// </summary> | /// </summary> | ||||
/// <returns> | /// <returns> | ||||
@@ -141,6 +148,13 @@ namespace Discord | |||||
/// </returns> | /// </returns> | ||||
ulong OwnerId { get; } | ulong OwnerId { get; } | ||||
/// <summary> | /// <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. | /// Gets the ID of the region hosting this guild's voice channels. | ||||
/// </summary> | /// </summary> | ||||
/// <returns> | /// <returns> | ||||
@@ -521,6 +535,18 @@ namespace Discord | |||||
/// </returns> | /// </returns> | ||||
Task<IRole> CreateRoleAsync(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false, RequestOptions options = null); | Task<IRole> CreateRoleAsync(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false, RequestOptions options = null); | ||||
/// <summary> | |||||
/// Adds a user to this guild. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// This method requires you have an OAuth2 access token for the user, requested with the guilds.join scope, and that the bot have the MANAGE_INVITES permission in the guild. | |||||
/// </remarks> | |||||
/// <param name="id">The snowflake identifier of the user.</param> | |||||
/// <param name="accessToken">The OAuth2 access token for the user, requested with the guilds.join scope.</param> | |||||
/// <param name="func">The delegate containing the properties to be applied to the user upon being added to the guild.</param> | |||||
/// <param name="options">The options to be used when sending the request.</param> | |||||
/// <returns>A guild user associated with the specified <paramref name="id" />; <c>null</c> if the user is already in the guild.</returns> | |||||
Task<IGuildUser> AddGuildUserAsync(ulong userId, string accessToken, Action<AddGuildUserProperties> func = null, RequestOptions options = null); | |||||
/// <summary> | /// <summary> | ||||
/// Gets a collection of all users in this guild. | /// Gets a collection of all users in this guild. | ||||
/// </summary> | /// </summary> | ||||
@@ -100,5 +100,25 @@ namespace Discord | |||||
/// A read-only collection of user IDs. | /// A read-only collection of user IDs. | ||||
/// </returns> | /// </returns> | ||||
IReadOnlyCollection<ulong> MentionedUserIds { get; } | 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; | |||||
} | |||||
} |
@@ -0,0 +1,62 @@ | |||||
using System.Collections.Generic; | |||||
namespace Discord | |||||
{ | |||||
/// <summary> | |||||
/// Properties that are used to add a new <see cref="IGuildUser"/> to the guild with the following parameters. | |||||
/// </summary> | |||||
/// <seealso cref="IGuild.AddGuildUserAsync" /> | |||||
public class AddGuildUserProperties | |||||
{ | |||||
/// <summary> | |||||
/// Gets or sets the user's nickname. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// To clear the user's nickname, this value can be set to <c>null</c> or | |||||
/// <see cref="string.Empty"/>. | |||||
/// </remarks> | |||||
public Optional<string> Nickname { get; set; } | |||||
/// <summary> | |||||
/// Gets or sets whether the user should be muted in a voice channel. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// If this value is set to <c>true</c>, no user will be able to hear this user speak in the guild. | |||||
/// </remarks> | |||||
public Optional<bool> Mute { get; set; } | |||||
/// <summary> | |||||
/// Gets or sets whether the user should be deafened in a voice channel. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// If this value is set to <c>true</c>, this user will not be able to hear anyone speak in the guild. | |||||
/// </remarks> | |||||
public Optional<bool> Deaf { get; set; } | |||||
/// <summary> | |||||
/// Gets or sets the roles the user should have. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// <para> | |||||
/// To add a role to a user: | |||||
/// <see cref="IGuildUser.AddRolesAsync(IEnumerable{IRole},RequestOptions)" /> | |||||
/// </para> | |||||
/// <para> | |||||
/// To remove a role from a user: | |||||
/// <see cref="IGuildUser.RemoveRolesAsync(IEnumerable{IRole},RequestOptions)" /> | |||||
/// </para> | |||||
/// </remarks> | |||||
public Optional<IEnumerable<IRole>> Roles { get; set; } | |||||
/// <summary> | |||||
/// Gets or sets the roles the user should have. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// <para> | |||||
/// To add a role to a user: | |||||
/// <see cref="IGuildUser.AddRolesAsync(IEnumerable{IRole},RequestOptions)" /> | |||||
/// </para> | |||||
/// <para> | |||||
/// To remove a role from a user: | |||||
/// <see cref="IGuildUser.RemoveRolesAsync(IEnumerable{IRole},RequestOptions)" /> | |||||
/// </para> | |||||
/// </remarks> | |||||
public Optional<IEnumerable<ulong>> RoleIds { get; set; } | |||||
} | |||||
} |
@@ -1,4 +1,6 @@ | |||||
using System; | using System; | ||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
namespace Discord | namespace Discord | ||||
{ | { | ||||
@@ -65,5 +67,15 @@ namespace Discord | |||||
return builder; | 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,17 +0,0 @@ | |||||
using System; | |||||
namespace Discord | |||||
{ | |||||
public class RpcException : Exception | |||||
{ | |||||
public int ErrorCode { get; } | |||||
public string Reason { get; } | |||||
public RpcException(int errorCode, string reason = null) | |||||
: base($"The server sent error {errorCode}{(reason != null ? $": \"{reason}\"" : "")}") | |||||
{ | |||||
ErrorCode = errorCode; | |||||
Reason = reason; | |||||
} | |||||
} | |||||
} |
@@ -1,4 +1,4 @@ | |||||
#pragma warning disable CS1591 | |||||
#pragma warning disable CS1591 | |||||
using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
namespace Discord.API | namespace Discord.API | ||||
@@ -25,10 +25,12 @@ namespace Discord.API | |||||
public bool EmbedEnabled { get; set; } | public bool EmbedEnabled { get; set; } | ||||
[JsonProperty("embed_channel_id")] | [JsonProperty("embed_channel_id")] | ||||
public ulong? EmbedChannelId { get; set; } | public ulong? EmbedChannelId { get; set; } | ||||
[JsonProperty("system_channel_id")] | |||||
public ulong? SystemChannelId { get; set; } | |||||
[JsonProperty("verification_level")] | [JsonProperty("verification_level")] | ||||
public VerificationLevel VerificationLevel { get; set; } | 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")] | [JsonProperty("voice_states")] | ||||
public VoiceState[] VoiceStates { get; set; } | public VoiceState[] VoiceStates { get; set; } | ||||
[JsonProperty("roles")] | [JsonProperty("roles")] | ||||
@@ -39,7 +41,9 @@ namespace Discord.API | |||||
public string[] Features { get; set; } | public string[] Features { get; set; } | ||||
[JsonProperty("mfa_level")] | [JsonProperty("mfa_level")] | ||||
public MfaLevel MfaLevel { get; set; } | 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; } | public Optional<bool> Pinned { get; set; } | ||||
[JsonProperty("reactions")] | [JsonProperty("reactions")] | ||||
public Optional<Reaction[]> Reactions { get; set; } | 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; | using Newtonsoft.Json; | ||||
namespace Discord.API | namespace Discord.API | ||||
@@ -0,0 +1,19 @@ | |||||
using Newtonsoft.Json; | |||||
namespace Discord.API.Rest | |||||
{ | |||||
[JsonObject(MemberSerialization = MemberSerialization.OptIn)] | |||||
internal class AddGuildMemberParams | |||||
{ | |||||
[JsonProperty("access_token")] | |||||
public string AccessToken { get; set; } | |||||
[JsonProperty("nick")] | |||||
public Optional<string> Nickname { get; set; } | |||||
[JsonProperty("roles")] | |||||
public Optional<ulong[]> RoleIds { get; set; } | |||||
[JsonProperty("mute")] | |||||
public Optional<bool> IsMuted { get; set; } | |||||
[JsonProperty("deaf")] | |||||
public Optional<bool> IsDeafened { get; set; } | |||||
} | |||||
} |
@@ -1,4 +1,4 @@ | |||||
#pragma warning disable CS1591 | |||||
#pragma warning disable CS1591 | |||||
using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
namespace Discord.API.Rest | namespace Discord.API.Rest | ||||
@@ -12,5 +12,7 @@ namespace Discord.API.Rest | |||||
public Optional<int> Position { get; set; } | public Optional<int> Position { get; set; } | ||||
[JsonProperty("parent_id")] | [JsonProperty("parent_id")] | ||||
public Optional<ulong?> CategoryId { get; set; } | 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; | using Newtonsoft.Json; | ||||
namespace Discord.API.Rest | namespace Discord.API.Rest | ||||
@@ -28,5 +28,7 @@ namespace Discord.API.Rest | |||||
public Optional<ulong?> AfkChannelId { get; set; } | public Optional<ulong?> AfkChannelId { get; set; } | ||||
[JsonProperty("owner_id")] | [JsonProperty("owner_id")] | ||||
public Optional<ulong> OwnerId { get; set; } | public Optional<ulong> OwnerId { get; set; } | ||||
[JsonProperty("explicit_content_filter")] | |||||
public Optional<ExplicitContentFilterLevel> ExplicitContentFilter { get; set; } | |||||
} | } | ||||
} | } |
@@ -1,12 +1,17 @@ | |||||
#pragma warning disable CS1591 | |||||
#pragma warning disable CS1591 | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.IO; | using System.IO; | ||||
using System.Text; | |||||
using Discord.Net.Converters; | |||||
using Discord.Net.Rest; | using Discord.Net.Rest; | ||||
using Newtonsoft.Json; | |||||
namespace Discord.API.Rest | namespace Discord.API.Rest | ||||
{ | { | ||||
internal class UploadWebhookFileParams | internal class UploadWebhookFileParams | ||||
{ | { | ||||
private static JsonSerializer _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; | |||||
public Stream File { get; } | public Stream File { get; } | ||||
public Optional<string> Filename { get; set; } | public Optional<string> Filename { get; set; } | ||||
@@ -27,18 +32,27 @@ namespace Discord.API.Rest | |||||
var d = new Dictionary<string, object>(); | var d = new Dictionary<string, object>(); | ||||
d["file"] = new MultipartFile(File, Filename.GetValueOrDefault("unknown.dat")); | d["file"] = new MultipartFile(File, Filename.GetValueOrDefault("unknown.dat")); | ||||
var payload = new Dictionary<string, object>(); | |||||
if (Content.IsSpecified) | if (Content.IsSpecified) | ||||
d["content"] = Content.Value; | |||||
payload["content"] = Content.Value; | |||||
if (IsTTS.IsSpecified) | if (IsTTS.IsSpecified) | ||||
d["tts"] = IsTTS.Value.ToString(); | |||||
payload["tts"] = IsTTS.Value.ToString(); | |||||
if (Nonce.IsSpecified) | if (Nonce.IsSpecified) | ||||
d["nonce"] = Nonce.Value; | |||||
payload["nonce"] = Nonce.Value; | |||||
if (Username.IsSpecified) | if (Username.IsSpecified) | ||||
d["username"] = Username.Value; | |||||
payload["username"] = Username.Value; | |||||
if (AvatarUrl.IsSpecified) | if (AvatarUrl.IsSpecified) | ||||
d["avatar_url"] = AvatarUrl.Value; | |||||
payload["avatar_url"] = AvatarUrl.Value; | |||||
if (Embeds.IsSpecified) | if (Embeds.IsSpecified) | ||||
d["embeds"] = Embeds.Value; | |||||
payload["embeds"] = Embeds.Value; | |||||
var json = new StringBuilder(); | |||||
using (var text = new StringWriter(json)) | |||||
using (var writer = new JsonTextWriter(text)) | |||||
_serializer.Serialize(writer, payload); | |||||
d["payload_json"] = json.ToString(); | |||||
return d; | return d; | ||||
} | } | ||||
} | } | ||||
@@ -1,3 +1,4 @@ | |||||
#pragma warning disable CS1591 | #pragma warning disable CS1591 | ||||
using Discord.API.Rest; | using Discord.API.Rest; | ||||
using Discord.Net; | using Discord.Net; | ||||
@@ -977,6 +978,8 @@ namespace Discord.API | |||||
Preconditions.NotNull(args, nameof(args)); | Preconditions.NotNull(args, nameof(args)); | ||||
Preconditions.AtLeast(args.MaxAge, 0, nameof(args.MaxAge)); | Preconditions.AtLeast(args.MaxAge, 0, nameof(args.MaxAge)); | ||||
Preconditions.AtLeast(args.MaxUses, 0, nameof(args.MaxUses)); | 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); | options = RequestOptions.CreateOrClone(options); | ||||
var ids = new BucketIds(channelId: channelId); | var ids = new BucketIds(channelId: channelId); | ||||
@@ -991,6 +994,25 @@ namespace Discord.API | |||||
} | } | ||||
//Guild Members | //Guild Members | ||||
public async Task<GuildMember> AddGuildMemberAsync(ulong guildId, ulong userId, AddGuildMemberParams args, RequestOptions options = null) | |||||
{ | |||||
Preconditions.NotEqual(guildId, 0, nameof(guildId)); | |||||
Preconditions.NotEqual(userId, 0, nameof(userId)); | |||||
Preconditions.NotNull(args, nameof(args)); | |||||
Preconditions.NotNullOrWhitespace(args.AccessToken, nameof(args.AccessToken)); | |||||
if (args.RoleIds.IsSpecified) | |||||
{ | |||||
foreach (var roleId in args.RoleIds.Value) | |||||
Preconditions.NotEqual(roleId, 0, nameof(roleId)); | |||||
} | |||||
options = RequestOptions.CreateOrClone(options); | |||||
var ids = new BucketIds(guildId: guildId); | |||||
return await SendJsonAsync<GuildMember>("PUT", () => $"guilds/{guildId}/members/{userId}", args, ids, options: options); | |||||
} | |||||
public async Task<GuildMember> GetGuildMemberAsync(ulong guildId, ulong userId, RequestOptions options = null) | public async Task<GuildMember> GetGuildMemberAsync(ulong guildId, ulong userId, RequestOptions options = null) | ||||
{ | { | ||||
Preconditions.NotEqual(guildId, 0, nameof(guildId)); | Preconditions.NotEqual(guildId, 0, nameof(guildId)); | ||||
@@ -1426,8 +1448,11 @@ namespace Discord.API | |||||
lastIndex = rightIndex + 1; | lastIndex = rightIndex + 1; | ||||
} | } | ||||
if (builder[builder.Length - 1] == '/') | |||||
builder.Remove(builder.Length - 1, 1); | |||||
format = builder.ToString(); | format = builder.ToString(); | ||||
return x => string.Format(format, x.ToArray()); | return x => string.Format(format, x.ToArray()); | ||||
} | } | ||||
catch (Exception ex) | catch (Exception ex) | ||||
@@ -76,9 +76,13 @@ namespace Discord.Rest | |||||
public static async Task<RestInviteMetadata> CreateInviteAsync(IGuildChannel channel, BaseDiscordClient client, | public static async Task<RestInviteMetadata> CreateInviteAsync(IGuildChannel channel, BaseDiscordClient client, | ||||
int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) | 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); | var model = await client.ApiClient.CreateChannelInviteAsync(channel.Id, args, options).ConfigureAwait(false); | ||||
return RestInviteMetadata.Create(client, null, channel, model); | 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); | var model = await client.ApiClient.GetChannelAsync(channel.CategoryId.Value, options).ConfigureAwait(false); | ||||
return RestCategoryChannel.Create(client, model) as ICategoryChannel; | 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 | //Helpers | ||||
private static IUser GetAuthor(BaseDiscordClient client, IGuild guild, UserModel model, ulong? webhookId) | private static IUser GetAuthor(BaseDiscordClient client, IGuild guild, UserModel model, ulong? webhookId) | ||||
@@ -201,6 +201,8 @@ namespace Discord.Rest | |||||
/// </returns> | /// </returns> | ||||
public Task<ICategoryChannel> GetCategoryAsync(RequestOptions options = null) | public Task<ICategoryChannel> GetCategoryAsync(RequestOptions options = null) | ||||
=> ChannelHelper.GetCategoryAsync(this, Discord, options); | => ChannelHelper.GetCategoryAsync(this, Discord, options); | ||||
public Task SyncPermissionsAsync(RequestOptions options = null) | |||||
=> ChannelHelper.SyncPermissionsAsync(this, Discord, options); | |||||
private string DebuggerDisplay => $"{Name} ({Id}, Text)"; | private string DebuggerDisplay => $"{Name} ({Id}, Text)"; | ||||
@@ -57,6 +57,8 @@ namespace Discord.Rest | |||||
/// </returns> | /// </returns> | ||||
public Task<ICategoryChannel> GetCategoryAsync(RequestOptions options = null) | public Task<ICategoryChannel> GetCategoryAsync(RequestOptions options = null) | ||||
=> ChannelHelper.GetCategoryAsync(this, Discord, options); | => ChannelHelper.GetCategoryAsync(this, Discord, options); | ||||
public Task SyncPermissionsAsync(RequestOptions options = null) | |||||
=> ChannelHelper.SyncPermissionsAsync(this, Discord, options); | |||||
private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; | private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; | ||||
@@ -1,110 +0,0 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Diagnostics; | |||||
using System.IO; | |||||
using System.Linq; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.Rest | |||||
{ | |||||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||||
internal class RestVirtualMessageChannel : RestEntity<ulong>, IMessageChannel | |||||
{ | |||||
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | |||||
public string Mention => MentionUtils.MentionChannel(Id); | |||||
internal RestVirtualMessageChannel(BaseDiscordClient discord, ulong id) | |||||
: base(discord, id) | |||||
{ | |||||
} | |||||
internal static RestVirtualMessageChannel Create(BaseDiscordClient discord, ulong id) | |||||
{ | |||||
return new RestVirtualMessageChannel(discord, id); | |||||
} | |||||
public Task<RestMessage> GetMessageAsync(ulong id, RequestOptions options = null) | |||||
=> ChannelHelper.GetMessageAsync(this, Discord, id, options); | |||||
public IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) | |||||
=> ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, options); | |||||
public IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) | |||||
=> ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, options); | |||||
public IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) | |||||
=> ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, options); | |||||
public Task<IReadOnlyCollection<RestMessage>> GetPinnedMessagesAsync(RequestOptions options = null) | |||||
=> ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); | |||||
public Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) | |||||
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); | |||||
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) | |||||
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options); | |||||
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) | |||||
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options); | |||||
/// <inheritdoc /> | |||||
public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) | |||||
=> ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options); | |||||
/// <inheritdoc /> | |||||
public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) | |||||
=> ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options); | |||||
/// <inheritdoc /> | |||||
public Task TriggerTypingAsync(RequestOptions options = null) | |||||
=> ChannelHelper.TriggerTypingAsync(this, Discord, options); | |||||
/// <inheritdoc /> | |||||
public IDisposable EnterTypingState(RequestOptions options = null) | |||||
=> ChannelHelper.EnterTypingState(this, Discord, options); | |||||
private string DebuggerDisplay => $"({Id}, Text)"; | |||||
//IMessageChannel | |||||
async Task<IMessage> IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) | |||||
{ | |||||
if (mode == CacheMode.AllowDownload) | |||||
return await GetMessageAsync(id, options).ConfigureAwait(false); | |||||
else | |||||
return null; | |||||
} | |||||
IAsyncEnumerable<IReadOnlyCollection<IMessage>> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) | |||||
{ | |||||
if (mode == CacheMode.AllowDownload) | |||||
return GetMessagesAsync(limit, options); | |||||
else | |||||
return AsyncEnumerable.Empty<IReadOnlyCollection<IMessage>>(); | |||||
} | |||||
IAsyncEnumerable<IReadOnlyCollection<IMessage>> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) | |||||
{ | |||||
if (mode == CacheMode.AllowDownload) | |||||
return GetMessagesAsync(fromMessageId, dir, limit, options); | |||||
else | |||||
return AsyncEnumerable.Empty<IReadOnlyCollection<IMessage>>(); | |||||
} | |||||
IAsyncEnumerable<IReadOnlyCollection<IMessage>> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) | |||||
{ | |||||
if (mode == CacheMode.AllowDownload) | |||||
return GetMessagesAsync(fromMessage, dir, limit, options); | |||||
else | |||||
return AsyncEnumerable.Empty<IReadOnlyCollection<IMessage>>(); | |||||
} | |||||
async Task<IReadOnlyCollection<IMessage>> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) | |||||
=> await GetPinnedMessagesAsync(options).ConfigureAwait(false); | |||||
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options) | |||||
=> await SendFileAsync(filePath, text, isTTS, embed, options); | |||||
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options) | |||||
=> await SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false); | |||||
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) | |||||
=> await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); | |||||
//IChannel | |||||
string IChannel.Name => | |||||
throw new NotSupportedException(); | |||||
IAsyncEnumerable<IReadOnlyCollection<IUser>> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => | |||||
throw new NotSupportedException(); | |||||
Task<IUser> IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => | |||||
throw new NotSupportedException(); | |||||
} | |||||
} |
@@ -32,7 +32,8 @@ namespace Discord.Rest | |||||
Icon = args.Icon.IsSpecified ? args.Icon.Value?.ToModel() : Optional.Create<ImageModel?>(), | Icon = args.Icon.IsSpecified ? args.Icon.Value?.ToModel() : Optional.Create<ImageModel?>(), | ||||
Name = args.Name, | Name = args.Name, | ||||
Splash = args.Splash.IsSpecified ? args.Splash.Value?.ToModel() : Optional.Create<ImageModel?>(), | Splash = args.Splash.IsSpecified ? args.Splash.Value?.ToModel() : Optional.Create<ImageModel?>(), | ||||
VerificationLevel = args.VerificationLevel | |||||
VerificationLevel = args.VerificationLevel, | |||||
ExplicitContentFilter = args.ExplicitContentFilter | |||||
}; | }; | ||||
if (args.AfkChannel.IsSpecified) | if (args.AfkChannel.IsSpecified) | ||||
@@ -60,6 +61,9 @@ namespace Discord.Rest | |||||
if (!apiArgs.Icon.IsSpecified && guild.IconId != null) | if (!apiArgs.Icon.IsSpecified && guild.IconId != null) | ||||
apiArgs.Icon = new ImageModel(guild.IconId); | 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); | return await client.ApiClient.ModifyGuildAsync(guild.Id, apiArgs, options).ConfigureAwait(false); | ||||
} | } | ||||
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <c>null</c>.</exception> | /// <exception cref="ArgumentNullException"><paramref name="func"/> is <c>null</c>.</exception> | ||||
@@ -253,6 +257,34 @@ namespace Discord.Rest | |||||
} | } | ||||
//Users | //Users | ||||
public static async Task<RestGuildUser> AddGuildUserAsync(IGuild guild, BaseDiscordClient client, ulong userId, string accessToken, | |||||
Action<AddGuildUserProperties> func, RequestOptions options) | |||||
{ | |||||
var args = new AddGuildUserProperties(); | |||||
func?.Invoke(args); | |||||
if (args.Roles.IsSpecified) | |||||
{ | |||||
var ids = args.Roles.Value.Select(r => r.Id); | |||||
if (args.RoleIds.IsSpecified) | |||||
args.RoleIds.Value.Concat(ids); | |||||
else | |||||
args.RoleIds = Optional.Create(ids); | |||||
} | |||||
var apiArgs = new AddGuildMemberParams | |||||
{ | |||||
AccessToken = accessToken, | |||||
Nickname = args.Nickname, | |||||
IsDeafened = args.Deaf, | |||||
IsMuted = args.Mute, | |||||
RoleIds = args.RoleIds.IsSpecified ? args.RoleIds.Value.Distinct().ToArray() : Optional.Create<ulong[]>() | |||||
}; | |||||
var model = await client.ApiClient.AddGuildMemberAsync(guild.Id, userId, apiArgs, options); | |||||
return model is null ? null : RestGuildUser.Create(client, guild, model); | |||||
} | |||||
public static async Task<RestGuildUser> GetUserAsync(IGuild guild, BaseDiscordClient client, | public static async Task<RestGuildUser> GetUserAsync(IGuild guild, BaseDiscordClient client, | ||||
ulong id, RequestOptions options) | ulong id, RequestOptions options) | ||||
{ | { | ||||
@@ -32,6 +32,8 @@ namespace Discord.Rest | |||||
public MfaLevel MfaLevel { get; private set; } | public MfaLevel MfaLevel { get; private set; } | ||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public DefaultMessageNotifications DefaultMessageNotifications { get; private set; } | public DefaultMessageNotifications DefaultMessageNotifications { get; private set; } | ||||
/// <inheritdoc /> | |||||
public ExplicitContentFilterLevel ExplicitContentFilter { get; private set; } | |||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public ulong? AFKChannelId { get; private set; } | public ulong? AFKChannelId { get; private set; } | ||||
@@ -48,6 +50,8 @@ namespace Discord.Rest | |||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public string SplashId { get; private set; } | public string SplashId { get; private set; } | ||||
internal bool Available { get; private set; } | internal bool Available { get; private set; } | ||||
/// <inheritdoc /> | |||||
public ulong? ApplicationId { get; private set; } | |||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | ||||
@@ -98,6 +102,8 @@ namespace Discord.Rest | |||||
VerificationLevel = model.VerificationLevel; | VerificationLevel = model.VerificationLevel; | ||||
MfaLevel = model.MfaLevel; | MfaLevel = model.MfaLevel; | ||||
DefaultMessageNotifications = model.DefaultMessageNotifications; | DefaultMessageNotifications = model.DefaultMessageNotifications; | ||||
ExplicitContentFilter = model.ExplicitContentFilter; | |||||
ApplicationId = model.ApplicationId; | |||||
if (model.Emojis != null) | if (model.Emojis != null) | ||||
{ | { | ||||
@@ -531,6 +537,10 @@ namespace Discord.Rest | |||||
public IAsyncEnumerable<IReadOnlyCollection<RestGuildUser>> GetUsersAsync(RequestOptions options = null) | public IAsyncEnumerable<IReadOnlyCollection<RestGuildUser>> GetUsersAsync(RequestOptions options = null) | ||||
=> GuildHelper.GetUsersAsync(this, Discord, null, null, options); | => GuildHelper.GetUsersAsync(this, Discord, null, null, options); | ||||
/// <inheritdoc /> | |||||
public Task<RestGuildUser> AddGuildUserAsync(ulong id, string accessToken, Action<AddGuildUserProperties> func = null, RequestOptions options = null) | |||||
=> GuildHelper.AddGuildUserAsync(this, Discord, id, accessToken, func, options); | |||||
/// <summary> | /// <summary> | ||||
/// Gets a user from this guild. | /// Gets a user from this guild. | ||||
/// </summary> | /// </summary> | ||||
@@ -794,6 +804,10 @@ namespace Discord.Rest | |||||
async Task<IRole> IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted, RequestOptions options) | async Task<IRole> IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted, RequestOptions options) | ||||
=> await CreateRoleAsync(name, permissions, color, isHoisted, options).ConfigureAwait(false); | => await CreateRoleAsync(name, permissions, color, isHoisted, options).ConfigureAwait(false); | ||||
/// <inheritdoc /> | |||||
async Task<IGuildUser> IGuild.AddGuildUserAsync(ulong userId, string accessToken, Action<AddGuildUserProperties> func, RequestOptions options) | |||||
=> await AddGuildUserAsync(userId, accessToken, func, options); | |||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
async Task<IGuildUser> IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) | async Task<IGuildUser> IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) | ||||
{ | { | ||||
@@ -1,7 +1,7 @@ | |||||
using System.Diagnostics; | using System.Diagnostics; | ||||
using Model = Discord.API.GuildEmbed; | using Model = Discord.API.GuildEmbed; | ||||
namespace Discord | |||||
namespace Discord.Rest | |||||
{ | { | ||||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
public struct RestGuildEmbed | public struct RestGuildEmbed | ||||
@@ -2,7 +2,7 @@ using Discord.Rest; | |||||
using System.Diagnostics; | using System.Diagnostics; | ||||
using Model = Discord.API.VoiceRegion; | using Model = Discord.API.VoiceRegion; | ||||
namespace Discord | |||||
namespace Discord.Rest | |||||
{ | { | ||||
/// <summary> | /// <summary> | ||||
/// Represents a REST-based voice region. | /// Represents a REST-based voice region. | ||||
@@ -55,6 +55,10 @@ namespace Discord.Rest | |||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); | 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) | internal RestMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author, MessageSource source) | ||||
: base(discord, id) | : base(discord, id) | ||||
@@ -77,6 +81,29 @@ namespace Discord.Rest | |||||
if (model.Content.IsSpecified) | if (model.Content.IsSpecified) | ||||
Content = model.Content.Value; | 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 /> | /// <inheritdoc /> | ||||
@@ -3,7 +3,7 @@ using System.Collections.Immutable; | |||||
using System.Diagnostics; | using System.Diagnostics; | ||||
using Model = Discord.API.Connection; | using Model = Discord.API.Connection; | ||||
namespace Discord | |||||
namespace Discord.Rest | |||||
{ | { | ||||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
public class RestConnection : IConnection | public class RestConnection : IConnection | ||||
@@ -1158,7 +1158,11 @@ namespace Discord.WebSocket | |||||
if (author == null) | if (author == null) | ||||
{ | { | ||||
if (guild != null) | if (guild != null) | ||||
author = guild.AddOrUpdateUser(data.Member.Value); //per g250k, we can create an entire member now | |||||
{ | |||||
author = data.Member.IsSpecified // member isn't always included, but use it when we can | |||||
? guild.AddOrUpdateUser(data.Member.Value) | |||||
: guild.AddOrUpdateUser(data.Author.Value); // user has no guild-specific data | |||||
} | |||||
else if (channel is SocketGroupChannel) | else if (channel is SocketGroupChannel) | ||||
author = (channel as SocketGroupChannel).GetOrAddUser(data.Author.Value); | author = (channel as SocketGroupChannel).GetOrAddUser(data.Author.Value); | ||||
else | else | ||||
@@ -31,6 +31,8 @@ namespace Discord.WebSocket | |||||
/// </returns> | /// </returns> | ||||
public ICategoryChannel Category | public ICategoryChannel Category | ||||
=> CategoryId.HasValue ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel : null; | => CategoryId.HasValue ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel : null; | ||||
public Task SyncPermissionsAsync(RequestOptions options = null) | |||||
=> ChannelHelper.SyncPermissionsAsync(this, Discord, options); | |||||
private bool _nsfw; | private bool _nsfw; | ||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
@@ -30,6 +30,8 @@ namespace Discord.WebSocket | |||||
/// </returns> | /// </returns> | ||||
public ICategoryChannel Category | public ICategoryChannel Category | ||||
=> CategoryId.HasValue ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel : null; | => CategoryId.HasValue ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel : null; | ||||
public Task SyncPermissionsAsync(RequestOptions options = null) | |||||
=> ChannelHelper.SyncPermissionsAsync(this, Discord, options); | |||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public override IReadOnlyCollection<SocketGuildUser> Users | public override IReadOnlyCollection<SocketGuildUser> Users | ||||
@@ -50,6 +50,8 @@ namespace Discord.WebSocket | |||||
public MfaLevel MfaLevel { get; private set; } | public MfaLevel MfaLevel { get; private set; } | ||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public DefaultMessageNotifications DefaultMessageNotifications { get; private set; } | public DefaultMessageNotifications DefaultMessageNotifications { get; private set; } | ||||
/// <inheritdoc /> | |||||
public ExplicitContentFilterLevel ExplicitContentFilter { get; private set; } | |||||
/// <summary> | /// <summary> | ||||
/// Gets the number of members. | /// Gets the number of members. | ||||
/// </summary> | /// </summary> | ||||
@@ -73,6 +75,8 @@ namespace Discord.WebSocket | |||||
internal bool IsAvailable { get; private set; } | internal bool IsAvailable { get; private set; } | ||||
/// <summary> Indicates whether the client is connected to this guild. </summary> | /// <summary> Indicates whether the client is connected to this guild. </summary> | ||||
public bool IsConnected { get; internal set; } | public bool IsConnected { get; internal set; } | ||||
/// <inheritdoc /> | |||||
public ulong? ApplicationId { get; internal set; } | |||||
internal ulong? AFKChannelId { get; private set; } | internal ulong? AFKChannelId { get; private set; } | ||||
internal ulong? EmbedChannelId { get; private set; } | internal ulong? EmbedChannelId { get; private set; } | ||||
@@ -346,6 +350,8 @@ namespace Discord.WebSocket | |||||
VerificationLevel = model.VerificationLevel; | VerificationLevel = model.VerificationLevel; | ||||
MfaLevel = model.MfaLevel; | MfaLevel = model.MfaLevel; | ||||
DefaultMessageNotifications = model.DefaultMessageNotifications; | DefaultMessageNotifications = model.DefaultMessageNotifications; | ||||
ExplicitContentFilter = model.ExplicitContentFilter; | |||||
ApplicationId = model.ApplicationId; | |||||
if (model.Emojis != null) | if (model.Emojis != null) | ||||
{ | { | ||||
@@ -663,6 +669,10 @@ namespace Discord.WebSocket | |||||
} | } | ||||
//Users | //Users | ||||
/// <inheritdoc /> | |||||
public Task<RestGuildUser> AddGuildUserAsync(ulong id, string accessToken, Action<AddGuildUserProperties> func = null, RequestOptions options = null) | |||||
=> GuildHelper.AddGuildUserAsync(this, Discord, id, accessToken, func, options); | |||||
/// <summary> | /// <summary> | ||||
/// Gets a user from this guild. | /// Gets a user from this guild. | ||||
/// </summary> | /// </summary> | ||||
@@ -1090,6 +1100,10 @@ namespace Discord.WebSocket | |||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
Task<IReadOnlyCollection<IGuildUser>> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options) | Task<IReadOnlyCollection<IGuildUser>> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options) | ||||
=> Task.FromResult<IReadOnlyCollection<IGuildUser>>(Users); | => Task.FromResult<IReadOnlyCollection<IGuildUser>>(Users); | ||||
/// <inheritdoc /> | |||||
async Task<IGuildUser> IGuild.AddGuildUserAsync(ulong userId, string accessToken, Action<AddGuildUserProperties> func, RequestOptions options) | |||||
=> await AddGuildUserAsync(userId, accessToken, func, options); | |||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
Task<IGuildUser> IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) | Task<IGuildUser> IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) | ||||
=> Task.FromResult<IGuildUser>(GetUser(id)); | => Task.FromResult<IGuildUser>(GetUser(id)); | ||||
@@ -43,6 +43,13 @@ namespace Discord.WebSocket | |||||
public virtual bool IsPinned => false; | public virtual bool IsPinned => false; | ||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public virtual DateTimeOffset? EditedTimestamp => null; | public virtual DateTimeOffset? EditedTimestamp => null; | ||||
/// <inheritdoc /> | |||||
public MessageActivity Activity { get; private set; } | |||||
/// <inheritdoc /> | |||||
public MessageApplication Application { get; private set; } | |||||
/// <summary> | /// <summary> | ||||
/// Returns all attachments included in this message. | /// Returns all attachments included in this message. | ||||
/// </summary> | /// </summary> | ||||
@@ -105,6 +112,29 @@ namespace Discord.WebSocket | |||||
if (model.Content.IsSpecified) | if (model.Content.IsSpecified) | ||||
Content = model.Content.Value; | 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 /> | /// <inheritdoc /> | ||||
@@ -1,7 +1,10 @@ | |||||
using Discord.Rest; | |||||
using System; | using System; | ||||
using System.Collections.Generic; | |||||
using System.Collections.Immutable; | |||||
using System.Diagnostics; | using System.Diagnostics; | ||||
using System.Linq; | |||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
using Discord.Rest; | |||||
using Model = Discord.API.User; | using Model = Discord.API.User; | ||||
namespace Discord.WebSocket | namespace Discord.WebSocket | ||||
@@ -35,6 +38,8 @@ namespace Discord.WebSocket | |||||
public IActivity Activity => Presence.Activity; | public IActivity Activity => Presence.Activity; | ||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public UserStatus Status => Presence.Status; | 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) | internal SocketUser(DiscordSocketClient discord, ulong id) | ||||
: base(discord, id) | : base(discord, id) | ||||
@@ -3,6 +3,7 @@ | |||||
<OutputType>Exe</OutputType> | <OutputType>Exe</OutputType> | ||||
<RootNamespace>Discord</RootNamespace> | <RootNamespace>Discord</RootNamespace> | ||||
<TargetFramework>netcoreapp1.1</TargetFramework> | <TargetFramework>netcoreapp1.1</TargetFramework> | ||||
<DebugType>portable</DebugType> | |||||
<PackageTargetFallback>$(PackageTargetFallback);portable-net45+win8+wp8+wpa81</PackageTargetFallback> | <PackageTargetFallback>$(PackageTargetFallback);portable-net45+win8+wp8+wpa81</PackageTargetFallback> | ||||
</PropertyGroup> | </PropertyGroup> | ||||
<ItemGroup> | <ItemGroup> | ||||
@@ -23,8 +24,8 @@ | |||||
<PackageReference Include="Akavache" Version="5.0.0" /> | <PackageReference Include="Akavache" Version="5.0.0" /> | ||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.2" /> | <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.2" /> | ||||
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" /> | <PackageReference Include="Newtonsoft.Json" Version="11.0.2" /> | ||||
<PackageReference Include="xunit" Version="2.3.1" /> | |||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" /> | |||||
<PackageReference Include="xunit.runner.reporters" Version="2.3.1" /> | |||||
<PackageReference Include="xunit" Version="2.4.0" /> | |||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" /> | |||||
<PackageReference Include="xunit.runner.reporters" Version="2.4.0" /> | |||||
</ItemGroup> | </ItemGroup> | ||||
</Project> | </Project> |
@@ -5,8 +5,25 @@ using Xunit; | |||||
namespace Discord | 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] | [Fact] | ||||
public Task TestGuildPermission() | public Task TestGuildPermission() | ||||
{ | { |
@@ -0,0 +1,133 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Threading.Tasks; | |||||
using Discord.Commands; | |||||
using Xunit; | |||||
namespace Discord | |||||
{ | |||||
public sealed class TypeReaderTests | |||||
{ | |||||
[Fact] | |||||
public async Task TestNamedArgumentReader() | |||||
{ | |||||
var commands = new CommandService(); | |||||
var module = await commands.AddModuleAsync<TestModule>(null); | |||||
Assert.NotNull(module); | |||||
Assert.NotEmpty(module.Commands); | |||||
var cmd = module.Commands[0]; | |||||
Assert.NotNull(cmd); | |||||
Assert.NotEmpty(cmd.Parameters); | |||||
var param = cmd.Parameters[0]; | |||||
Assert.NotNull(param); | |||||
Assert.True(param.IsRemainder); | |||||
var result = await param.ParseAsync(null, "bar: hello foo: 42"); | |||||
Assert.True(result.IsSuccess); | |||||
var m = result.BestMatch as ArgumentType; | |||||
Assert.NotNull(m); | |||||
Assert.Equal(expected: 42, actual: m.Foo); | |||||
Assert.Equal(expected: "hello", actual: m.Bar); | |||||
} | |||||
[Fact] | |||||
public async Task TestQuotedArgumentValue() | |||||
{ | |||||
var commands = new CommandService(); | |||||
var module = await commands.AddModuleAsync<TestModule>(null); | |||||
Assert.NotNull(module); | |||||
Assert.NotEmpty(module.Commands); | |||||
var cmd = module.Commands[0]; | |||||
Assert.NotNull(cmd); | |||||
Assert.NotEmpty(cmd.Parameters); | |||||
var param = cmd.Parameters[0]; | |||||
Assert.NotNull(param); | |||||
Assert.True(param.IsRemainder); | |||||
var result = await param.ParseAsync(null, "foo: 42 bar: 《hello》"); | |||||
Assert.True(result.IsSuccess); | |||||
var m = result.BestMatch as ArgumentType; | |||||
Assert.NotNull(m); | |||||
Assert.Equal(expected: 42, actual: m.Foo); | |||||
Assert.Equal(expected: "hello", actual: m.Bar); | |||||
} | |||||
[Fact] | |||||
public async Task TestNonPatternInput() | |||||
{ | |||||
var commands = new CommandService(); | |||||
var module = await commands.AddModuleAsync<TestModule>(null); | |||||
Assert.NotNull(module); | |||||
Assert.NotEmpty(module.Commands); | |||||
var cmd = module.Commands[0]; | |||||
Assert.NotNull(cmd); | |||||
Assert.NotEmpty(cmd.Parameters); | |||||
var param = cmd.Parameters[0]; | |||||
Assert.NotNull(param); | |||||
Assert.True(param.IsRemainder); | |||||
var result = await param.ParseAsync(null, "foobar"); | |||||
Assert.False(result.IsSuccess); | |||||
Assert.Equal(expected: CommandError.Exception, actual: result.Error); | |||||
} | |||||
[Fact] | |||||
public async Task TestMultiple() | |||||
{ | |||||
var commands = new CommandService(); | |||||
var module = await commands.AddModuleAsync<TestModule>(null); | |||||
Assert.NotNull(module); | |||||
Assert.NotEmpty(module.Commands); | |||||
var cmd = module.Commands[0]; | |||||
Assert.NotNull(cmd); | |||||
Assert.NotEmpty(cmd.Parameters); | |||||
var param = cmd.Parameters[0]; | |||||
Assert.NotNull(param); | |||||
Assert.True(param.IsRemainder); | |||||
var result = await param.ParseAsync(null, "manyints: \"1, 2, 3, 4, 5, 6, 7\""); | |||||
Assert.True(result.IsSuccess); | |||||
var m = result.BestMatch as ArgumentType; | |||||
Assert.NotNull(m); | |||||
Assert.Equal(expected: new int[] { 1, 2, 3, 4, 5, 6, 7 }, actual: m.ManyInts); | |||||
} | |||||
} | |||||
[NamedArgumentType] | |||||
public sealed class ArgumentType | |||||
{ | |||||
public int Foo { get; set; } | |||||
[OverrideTypeReader(typeof(CustomTypeReader))] | |||||
public string Bar { get; set; } | |||||
public IEnumerable<int> ManyInts { get; set; } | |||||
} | |||||
public sealed class CustomTypeReader : TypeReader | |||||
{ | |||||
public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services) | |||||
=> Task.FromResult(TypeReaderResult.FromSuccess(input)); | |||||
} | |||||
public sealed class TestModule : ModuleBase | |||||
{ | |||||
[Command("test")] | |||||
public Task TestCommand(ArgumentType arg) => Task.Delay(0); | |||||
} | |||||
} |