Browse Source

Merge pull request #1 from RogueException/dev

Updating my fork
pull/1196/head
Nathan Solomon GitHub 6 years ago
parent
commit
d501cfa599
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 1351 additions and 209 deletions
  1. +76
    -0
      CHANGELOG.md
  2. +16
    -1
      Discord.Net.sln
  3. +29
    -1
      docs/faq/basics/client-basics.md
  4. +4
    -3
      docs/guides/commands/samples/intro/command_handler.cs
  5. +2
    -2
      docs/guides/commands/samples/post-execution/command_executed_demo.cs
  6. +4
    -3
      docs/guides/voice/samples/joining_audio.cs
  7. +39
    -30
      docs/guides/voice/sending-voice.md
  8. +23
    -4
      samples/02_commands_framework/Services/CommandHandlingService.cs
  9. +14
    -0
      samples/03_sharded_client/03_sharded_client.csproj
  10. +17
    -0
      samples/03_sharded_client/Modules/PublicModule.cs
  11. +69
    -0
      samples/03_sharded_client/Program.cs
  12. +72
    -0
      samples/03_sharded_client/Services/CommandHandlingService.cs
  13. +11
    -0
      src/Discord.Net.Commands/Attributes/NamedArgumentTypeAttribute.cs
  14. +2
    -3
      src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs
  15. +1
    -1
      src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs
  16. +27
    -2
      src/Discord.Net.Commands/Builders/ParameterBuilder.cs
  17. +14
    -3
      src/Discord.Net.Commands/CommandService.cs
  18. +46
    -0
      src/Discord.Net.Commands/Extensions/CommandServiceExtensions.cs
  19. +5
    -1
      src/Discord.Net.Commands/Info/CommandInfo.cs
  20. +191
    -0
      src/Discord.Net.Commands/Readers/NamedArgumentTypeReader.cs
  21. +5
    -0
      src/Discord.Net.Core/Entities/Channels/INestedChannel.cs
  22. +13
    -0
      src/Discord.Net.Core/Entities/Guilds/ExplicitContentFilterLevel.cs
  23. +4
    -0
      src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs
  24. +26
    -0
      src/Discord.Net.Core/Entities/Guilds/IGuild.cs
  25. +20
    -0
      src/Discord.Net.Core/Entities/Messages/IMessage.cs
  26. +27
    -0
      src/Discord.Net.Core/Entities/Messages/MessageActivity.cs
  27. +16
    -0
      src/Discord.Net.Core/Entities/Messages/MessageActivityType.cs
  28. +43
    -0
      src/Discord.Net.Core/Entities/Messages/MessageApplication.cs
  29. +62
    -0
      src/Discord.Net.Core/Entities/Users/AddGuildUserProperties.cs
  30. +12
    -0
      src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs
  31. +0
    -17
      src/Discord.Net.Core/Net/RpcException.cs
  32. +9
    -5
      src/Discord.Net.Rest/API/Common/Guild.cs
  33. +6
    -0
      src/Discord.Net.Rest/API/Common/Message.cs
  34. +17
    -0
      src/Discord.Net.Rest/API/Common/MessageActivity.cs
  35. +38
    -0
      src/Discord.Net.Rest/API/Common/MessageApplication.cs
  36. +1
    -1
      src/Discord.Net.Rest/API/Common/Overwrite.cs
  37. +19
    -0
      src/Discord.Net.Rest/API/Rest/AddGuildMemberParams.cs
  38. +3
    -1
      src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs
  39. +3
    -1
      src/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs
  40. +21
    -7
      src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs
  41. +25
    -0
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  42. +24
    -3
      src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs
  43. +2
    -0
      src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs
  44. +2
    -0
      src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs
  45. +0
    -110
      src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs
  46. +33
    -1
      src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs
  47. +14
    -0
      src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
  48. +1
    -1
      src/Discord.Net.Rest/Entities/Guilds/RestGuildEmbed.cs
  49. +1
    -1
      src/Discord.Net.Rest/Entities/Guilds/RestVoiceRegion.cs
  50. +27
    -0
      src/Discord.Net.Rest/Entities/Messages/RestMessage.cs
  51. +1
    -1
      src/Discord.Net.Rest/Entities/Users/RestConnection.cs
  52. +5
    -1
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  53. +2
    -0
      src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs
  54. +2
    -0
      src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs
  55. +14
    -0
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
  56. +30
    -0
      src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs
  57. +6
    -1
      src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs
  58. +4
    -3
      test/Discord.Net.Tests/Discord.Net.Tests.csproj
  59. +18
    -1
      test/Discord.Net.Tests/Tests.Guilds.cs
  60. +133
    -0
      test/Discord.Net.Tests/Tests.TypeReaders.cs

+ 76
- 0
CHANGELOG.md View File

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

+ 16
- 1
Discord.Net.sln View File

@@ -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}


+ 29
- 1
docs/faq/basics/client-basics.md View File

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

+ 4
- 3
docs/guides/commands/samples/intro/command_handler.cs View File

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

+ 2
- 2
docs/guides/commands/samples/post-execution/command_executed_demo.cs View File

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

+ 4
- 3
docs/guides/voice/samples/joining_audio.cs View File

@@ -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();


+ 39
- 30
docs/guides/voice/sending-voice.md View File

@@ -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)]

+ 23
- 4
samples/02_commands_framework/Services/CommandHandlingService.cs View File

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

+ 14
- 0
samples/03_sharded_client/03_sharded_client.csproj View File

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

+ 17
- 0
samples/03_sharded_client/Modules/PublicModule.cs View File

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

+ 69
- 0
samples/03_sharded_client/Program.cs View File

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

+ 72
- 0
samples/03_sharded_client/Services/CommandHandlingService.cs View File

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

+ 11
- 0
src/Discord.Net.Commands/Attributes/NamedArgumentTypeAttribute.cs View File

@@ -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 { }
}

+ 2
- 3
src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs View File

@@ -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();




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

@@ -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;


+ 27
- 2
src/Discord.Net.Commands/Builders/ParameterBuilder.cs View File

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


+ 14
- 3
src/Discord.Net.Commands/CommandService.cs View File

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

+ 46
- 0
src/Discord.Net.Commands/Extensions/CommandServiceExtensions.cs View File

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

+ 5
- 1
src/Discord.Net.Commands/Info/CommandInfo.cs View File

@@ -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
{ {


+ 191
- 0
src/Discord.Net.Commands/Readers/NamedArgumentTypeReader.cs View File

@@ -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
}
}
}

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

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

+ 13
- 0
src/Discord.Net.Core/Entities/Guilds/ExplicitContentFilterLevel.cs View File

@@ -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
}
}

+ 4
- 0
src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs View File

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

+ 26
- 0
src/Discord.Net.Core/Entities/Guilds/IGuild.cs View File

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


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

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

+ 27
- 0
src/Discord.Net.Core/Entities/Messages/MessageActivity.cs View File

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

+ 16
- 0
src/Discord.Net.Core/Entities/Messages/MessageActivityType.cs View File

@@ -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
}
}

+ 43
- 0
src/Discord.Net.Core/Entities/Messages/MessageApplication.cs View File

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

+ 62
- 0
src/Discord.Net.Core/Entities/Users/AddGuildUserProperties.cs View File

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

+ 12
- 0
src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs View File

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

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

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

+ 9
- 5
src/Discord.Net.Rest/API/Common/Guild.cs View File

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

+ 6
- 0
src/Discord.Net.Rest/API/Common/Message.cs View File

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

+ 17
- 0
src/Discord.Net.Rest/API/Common/MessageActivity.cs View File

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

+ 38
- 0
src/Discord.Net.Rest/API/Common/MessageApplication.cs View File

@@ -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
- 1
src/Discord.Net.Rest/API/Common/Overwrite.cs View File

@@ -1,4 +1,4 @@
#pragma warning disable CS1591
#pragma warning disable CS1591
using Newtonsoft.Json; using Newtonsoft.Json;


namespace Discord.API namespace Discord.API


+ 19
- 0
src/Discord.Net.Rest/API/Rest/AddGuildMemberParams.cs View File

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

+ 3
- 1
src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs View File

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

+ 3
- 1
src/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs View File

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

+ 21
- 7
src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs View File

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


+ 25
- 0
src/Discord.Net.Rest/DiscordRestApiClient.cs View File

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


+ 24
- 3
src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs View File

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


+ 2
- 0
src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs View File

@@ -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)";




+ 2
- 0
src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs View File

@@ -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)";




+ 0
- 110
src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs View File

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

+ 33
- 1
src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs View File

@@ -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)
{ {


+ 14
- 0
src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs View File

@@ -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
- 1
src/Discord.Net.Rest/Entities/Guilds/RestGuildEmbed.cs View File

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


+ 1
- 1
src/Discord.Net.Rest/Entities/Guilds/RestVoiceRegion.cs View File

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


+ 27
- 0
src/Discord.Net.Rest/Entities/Messages/RestMessage.cs View File

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


+ 1
- 1
src/Discord.Net.Rest/Entities/Users/RestConnection.cs View File

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


+ 5
- 1
src/Discord.Net.WebSocket/DiscordSocketClient.cs View File

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


+ 2
- 0
src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs View File

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


+ 2
- 0
src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs View File

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


+ 14
- 0
src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs View File

@@ -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));


+ 30
- 0
src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs View File

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


+ 6
- 1
src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs View File

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


+ 4
- 3
test/Discord.Net.Tests/Discord.Net.Tests.csproj View File

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

test/Discord.Net.Tests/Tests.GuildPermissions.cs → test/Discord.Net.Tests/Tests.Guilds.cs View File

@@ -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()
{ {

+ 133
- 0
test/Discord.Net.Tests/Tests.TypeReaders.cs View File

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

Loading…
Cancel
Save