Browse Source

Merge branch 'dev' into patches/nested-channel-invite

pull/1172/head
Christopher F GitHub 6 years ago
parent
commit
2146dc1da6
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 796 additions and 60 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. +2
    -1
      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. +14
    -3
      src/Discord.Net.Commands/CommandService.cs
  14. +46
    -0
      src/Discord.Net.Commands/Extensions/CommandServiceExtensions.cs
  15. +5
    -1
      src/Discord.Net.Commands/Info/CommandInfo.cs
  16. +5
    -0
      src/Discord.Net.Core/Entities/Channels/INestedChannel.cs
  17. +13
    -0
      src/Discord.Net.Core/Entities/Guilds/ExplicitContentFilterLevel.cs
  18. +4
    -0
      src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs
  19. +14
    -0
      src/Discord.Net.Core/Entities/Guilds/IGuild.cs
  20. +20
    -0
      src/Discord.Net.Core/Entities/Messages/IMessage.cs
  21. +27
    -0
      src/Discord.Net.Core/Entities/Messages/MessageActivity.cs
  22. +16
    -0
      src/Discord.Net.Core/Entities/Messages/MessageActivityType.cs
  23. +43
    -0
      src/Discord.Net.Core/Entities/Messages/MessageApplication.cs
  24. +12
    -0
      src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs
  25. +9
    -5
      src/Discord.Net.Rest/API/Common/Guild.cs
  26. +6
    -0
      src/Discord.Net.Rest/API/Common/Message.cs
  27. +17
    -0
      src/Discord.Net.Rest/API/Common/MessageActivity.cs
  28. +38
    -0
      src/Discord.Net.Rest/API/Common/MessageApplication.cs
  29. +1
    -1
      src/Discord.Net.Rest/API/Common/Overwrite.cs
  30. +3
    -1
      src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs
  31. +3
    -1
      src/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs
  32. +6
    -0
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  33. +24
    -3
      src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs
  34. +2
    -0
      src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs
  35. +3
    -0
      src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs
  36. +5
    -1
      src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs
  37. +6
    -0
      src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
  38. +27
    -0
      src/Discord.Net.Rest/Entities/Messages/RestMessage.cs
  39. +2
    -0
      src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs
  40. +2
    -0
      src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs
  41. +6
    -0
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
  42. +30
    -0
      src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs
  43. +6
    -1
      src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs
  44. +18
    -1
      test/Discord.Net.Tests/Tests.Guilds.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
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "01_basic_ping_bot", "samples\01_basic_ping_bot\01_basic_ping_bot.csproj", "{F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "01_basic_ping_bot", "samples\01_basic_ping_bot\01_basic_ping_bot.csproj", "{F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "02_commands_framework", "samples\02_commands_framework\02_commands_framework.csproj", "{4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "03_sharded_client", "samples\03_sharded_client\03_sharded_client.csproj", "{9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -160,6 +162,18 @@ Global
{4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Release|x64.Build.0 = Release|Any CPU
{4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Release|x86.ActiveCfg = Release|Any CPU
{4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Release|x86.Build.0 = Release|Any CPU
{9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Debug|x64.ActiveCfg = Debug|Any CPU
{9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Debug|x64.Build.0 = Debug|Any CPU
{9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Debug|x86.ActiveCfg = Debug|Any CPU
{9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Debug|x86.Build.0 = Debug|Any CPU
{9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Release|Any CPU.Build.0 = Release|Any CPU
{9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Release|x64.ActiveCfg = Release|Any CPU
{9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Release|x64.Build.0 = Release|Any CPU
{9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Release|x86.ActiveCfg = Release|Any CPU
{9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -173,6 +187,7 @@ Global
{BBA8E7FB-C834-40DC-822F-B112CB7F0140} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A}
{F2FF84FB-F6AD-47E5-9EE5-18206CAE136E} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}
{4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}
{9B4C4AFB-3D15-44C6-9E36-12ED625AAA26} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D2404771-EEC8-45F2-9D71-F3373F6C1495}


+ 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
[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
int argPos = 0;

// Determine if the message is a command based on the prefix
// Determine if the message is a command based on the prefix and make sure no bots trigger commands
if (!(message.HasCharPrefix('!', ref argPos) ||
message.HasMentionPrefix(_client.CurrentUser, ref argPos)))
message.HasMentionPrefix(_client.CurrentUser, ref argPos)) ||
message.Author.IsBot)
return;

// Create a WebSocket-based command context based on the message
@@ -60,4 +61,4 @@ public class CommandHandler
// if (!result.IsSuccess)
// await context.Channel.SendMessageAsync(result.ErrorReason);
}
}
}

+ 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;
if (message == null) return;
int argPos = 0;
if (!(message.HasCharPrefix('!', ref argPos) || message.HasMentionPrefix(_client.CurrentUser, ref argPos))) return;
if (!(message.HasCharPrefix('!', ref argPos) || message.HasMentionPrefix(_client.CurrentUser, ref argPos)) || message.Author.IsBot) return;
var context = new SocketCommandContext(_client, message);
var result = await _commands.ExecuteAsync(context, argPos, _services);
// Optionally, you may pass the result manually into your
@@ -35,4 +35,4 @@ public async Task HandleCommandAsync(SocketMessage msg)
// precondition failures in the same method.

// await OnCommandExecutedAsync(null, context, result);
}
}

+ 2
- 1
docs/guides/voice/samples/joining_audio.cs View File

@@ -1,4 +1,5 @@
[Command("join")]
// The command's Run Mode MUST be set to RunMode.Async, otherwise, being connected to a voice channel will block the gateway thread.
[Command("join", RunMode = RunMode.Async)]
public async Task JoinChannel(IVoiceChannel channel = null)
{
// Get the audio channel


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

@@ -11,16 +11,16 @@ Information is not guaranteed to be accurate.

## Installing

Audio requires two native libraries, `libsodium` and `opus`.
Both of these libraries must be placed in the runtime directory of your
bot. (When developing on .NET Framework, this would be `bin/debug`,
when developing on .NET Core, this is where you execute `dotnet run`
Audio requires two native libraries, `libsodium` and `opus`.
Both of these libraries must be placed in the runtime directory of your
bot. (When developing on .NET Framework, this would be `bin/debug`,
when developing on .NET Core, this is where you execute `dotnet run`
from; typically the same directory as your csproj).

For Windows Users, precompiled binaries are available for your
For Windows Users, precompiled binaries are available for your
convienence [here](https://discord.foxbot.me/binaries/).

For Linux Users, you will need to compile [Sodium] and [Opus] from
For Linux Users, you will need to compile [Sodium] and [Opus] from
source, or install them from your package manager.

[Sodium]: https://download.libsodium.org/libsodium/releases/
@@ -28,7 +28,7 @@ source, or install them from your package manager.

## Joining a Channel

Joining a channel is the first step to sending audio, and will return
Joining a channel is the first step to sending audio, and will return
an [IAudioClient] to send data with.

To join a channel, simply await [ConnectAsync] on any instance of an
@@ -36,67 +36,76 @@ To join a channel, simply await [ConnectAsync] on any instance of an

[!code-csharp[Joining a Channel](samples/joining_audio.cs)]

The client will sustain a connection to this channel until it is
>[!WARNING]
>Commands which mutate voice states, such as those where you join/leave
>an audio channel, or send audio, should use [RunMode.Async]. RunMode.Async
>is necessary to prevent a feedback loop which will deadlock clients
>in their default configuration. If you know that you're running your
>commands in a different task than the gateway task, RunMode.Async is
>not required.

The client will sustain a connection to this channel until it is
kicked, disconnected from Discord, or told to disconnect.

It should be noted that voice connections are created on a per-guild
basis; only one audio connection may be open by the bot in a single
guild. To switch channels within a guild, invoke [ConnectAsync] on
It should be noted that voice connections are created on a per-guild
basis; only one audio connection may be open by the bot in a single
guild. To switch channels within a guild, invoke [ConnectAsync] on
another voice channel in the guild.

[IAudioClient]: xref:Discord.Audio.IAudioClient
[ConnectAsync]: xref:Discord.IAudioChannel.ConnectAsync*
[RunMode.Async]: xref:Discord.Commands.RunMode

## Transmitting Audio

### With FFmpeg

[FFmpeg] is an open source, highly versatile AV-muxing tool. This is
[FFmpeg] is an open source, highly versatile AV-muxing tool. This is
the recommended method of transmitting audio.

Before you begin, you will need to have a version of FFmpeg downloaded
and placed somewhere in your PATH (or alongside the bot, in the same
location as libsodium and opus). Windows binaries are available on
Before you begin, you will need to have a version of FFmpeg downloaded
and placed somewhere in your PATH (or alongside the bot, in the same
location as libsodium and opus). Windows binaries are available on
[FFmpeg's download page].

[FFmpeg]: https://ffmpeg.org/
[FFmpeg's download page]: https://ffmpeg.org/download.html

First, you will need to create a Process that starts FFmpeg. An
example of how to do this is included below, though it is important
First, you will need to create a Process that starts FFmpeg. An
example of how to do this is included below, though it is important
that you return PCM at 48000hz.

>[!NOTE]
>As of the time of this writing, Discord.Audio struggles significantly
>with processing audio that is already opus-encoded; you will need to
>As of the time of this writing, Discord.Audio struggles significantly
>with processing audio that is already opus-encoded; you will need to
>use the PCM write streams.

[!code-csharp[Creating FFmpeg](samples/audio_create_ffmpeg.cs)]

Next, to transmit audio from FFmpeg to Discord, you will need to
pull an [AudioOutStream] from your [IAudioClient]. Since we're using
Next, to transmit audio from FFmpeg to Discord, you will need to
pull an [AudioOutStream] from your [IAudioClient]. Since we're using
PCM audio, use [IAudioClient.CreatePCMStream].

The sample rate argument doesn't particularly matter, so long as it is
a valid rate (120, 240, 480, 960, 1920, or 2880). For the sake of
The sample rate argument doesn't particularly matter, so long as it is
a valid rate (120, 240, 480, 960, 1920, or 2880). For the sake of
simplicity, I recommend using 1920.

Channels should be left at `2`, unless you specified a different value
Channels should be left at `2`, unless you specified a different value
for `-ac 2` when creating FFmpeg.

[AudioOutStream]: xref:Discord.Audio.AudioOutStream
[IAudioClient.CreatePCMStream]: xref:Discord.Audio.IAudioClient#Discord_Audio_IAudioClient_CreateDirectPCMStream_Discord_Audio_AudioApplication_System_Nullable_System_Int32__System_Int32_

Finally, audio will need to be piped from FFmpeg's stdout into your
AudioOutStream. This step can be as complex as you'd like it to be, but
for the majority of cases, you can just use [Stream.CopyToAsync], as
Finally, audio will need to be piped from FFmpeg's stdout into your
AudioOutStream. This step can be as complex as you'd like it to be, but
for the majority of cases, you can just use [Stream.CopyToAsync], as
shown below.

[Stream.CopyToAsync]: https://msdn.microsoft.com/en-us/library/hh159084(v=vs.110).aspx

If you are implementing a queue for sending songs, it's likely that
you will want to wait for audio to stop playing before continuing on
to the next song. You can await `AudioOutStream.FlushAsync` to wait for
If you are implementing a queue for sending songs, it's likely that
you will want to wait for audio to stop playing before continuing on
to the next song. You can await `AudioOutStream.FlushAsync` to wait for
the audio client's internal buffer to clear out.

[!code-csharp[Sending Audio](samples/audio_ffmpeg.cs)]

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

@@ -20,6 +20,8 @@ namespace _02_commands_framework.Services
_discord = services.GetRequiredService<DiscordSocketClient>();
_services = services;

_commands.CommandExecuted += CommandExecutedAsync;
_commands.Log += LogAsync;
_discord.MessageReceived += MessageReceivedAsync;
}

@@ -39,11 +41,28 @@ namespace _02_commands_framework.Services
if (!message.HasMentionPrefix(_discord.CurrentUser, ref argPos)) return;

var context = new SocketCommandContext(_discord, message);
var result = await _commands.ExecuteAsync(context, argPos, _services);
await _commands.ExecuteAsync(context, argPos, _services); // we will handle the result in CommandExecutedAsync
}

public async Task CommandExecutedAsync(Optional<CommandInfo> command, ICommandContext context, IResult result)
{
// command is unspecified when there was a search failure (command not found); we don't care about these errors
if (!command.IsSpecified)
return;

// the command was succesful, we don't care about this result, unless we want to log that a command succeeded.
if (result.IsSuccess)
return;

// the command failed, let's notify the user that something happened.
await context.Channel.SendMessageAsync($"error: {result.ToString()}");
}

private Task LogAsync(LogMessage log)
{
Console.WriteLine(log.ToString());

if (result.Error.HasValue &&
result.Error.Value != CommandError.UnknownCommand) // it's bad practice to send 'unknown command' errors
await context.Channel.SendMessageAsync(result.ToString());
return Task.CompletedTask;
}
}
}

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

+ 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.
/// </para>
/// </remarks>
public event Func<CommandInfo, ICommandContext, IResult, Task> CommandExecuted { add { _commandExecutedEvent.Add(value); } remove { _commandExecutedEvent.Remove(value); } }
internal readonly AsyncEvent<Func<CommandInfo, ICommandContext, IResult, Task>> _commandExecutedEvent = new AsyncEvent<Func<CommandInfo, ICommandContext, IResult, Task>>();
public event Func<Optional<CommandInfo>, ICommandContext, IResult, Task> CommandExecuted { add { _commandExecutedEvent.Add(value); } remove { _commandExecutedEvent.Remove(value); } }
internal readonly AsyncEvent<Func<Optional<CommandInfo>, ICommandContext, IResult, Task>> _commandExecutedEvent = new AsyncEvent<Func<Optional<CommandInfo>, ICommandContext, IResult, Task>>();

private readonly SemaphoreSlim _moduleLock;
private readonly ConcurrentDictionary<Type, ModuleInfo> _typedModuleDefs;
@@ -512,7 +512,11 @@ namespace Discord.Commands

var searchResult = Search(input);
if (!searchResult.IsSuccess)
{
await _commandExecutedEvent.InvokeAsync(Optional.Create<CommandInfo>(), context, searchResult).ConfigureAwait(false);
return searchResult;
}

var commands = searchResult.Commands;
var preconditionResults = new Dictionary<CommandMatch, PreconditionResult>();
@@ -532,6 +536,8 @@ namespace Discord.Commands
var bestCandidate = preconditionResults
.OrderByDescending(x => x.Key.Command.Priority)
.FirstOrDefault(x => !x.Value.IsSuccess);

await _commandExecutedEvent.InvokeAsync(bestCandidate.Key.Command, context, bestCandidate.Value).ConfigureAwait(false);
return bestCandidate.Value;
}

@@ -589,12 +595,17 @@ namespace Discord.Commands
//All parses failed, return the one from the highest priority command, using score as a tie breaker
var bestMatch = parseResults
.FirstOrDefault(x => !x.Value.IsSuccess);

await _commandExecutedEvent.InvokeAsync(bestMatch.Key.Command, context, bestMatch.Value).ConfigureAwait(false);
return bestMatch.Value;
}

//If we get this far, at least one parse was successful. Execute the most likely overload.
var chosenOverload = successfulParses[0];
return await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, services).ConfigureAwait(false);
var result = await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, services).ConfigureAwait(false);
if (!result.IsSuccess) // succesful results raise the event in CommandInfo#ExecuteInternalAsync (have to raise it there b/c deffered execution)
await _commandExecutedEvent.InvokeAsync(chosenOverload.Key.Command, context, result);
return result;
}
}
}

+ 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);
await Module.Service._cmdLogger.ErrorAsync(wrappedEx).ConfigureAwait(false);

var result = ExecuteResult.FromError(ex);
await Module.Service._commandExecutedEvent.InvokeAsync(this, context, result).ConfigureAwait(false);

if (Module.Service._throwOnError)
{
if (ex == originalEx)
@@ -280,7 +284,7 @@ namespace Discord.Commands
ExceptionDispatchInfo.Capture(ex).Throw();
}

return ExecuteResult.FromError(CommandError.Exception, ex.Message);
return result;
}
finally
{


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

@@ -26,6 +26,11 @@ namespace Discord
/// representing the parent of this channel; <c>null</c> if none is set.
/// </returns>
Task<ICategoryChannel> GetCategoryAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Syncs the permissions of this nested channel with its parent's.
/// </summary>
Task SyncPermissionsAsync(RequestOptions options = null);

/// <summary>
/// Creates a new invite to this channel.


+ 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.
/// </summary>
public Optional<ulong> OwnerId { get; set; }
/// <summary>
/// Gets or sets the explicit content filter level of this guild.
/// </summary>
public Optional<ExplicitContentFilterLevel> ExplicitContentFilter { get; set; }
}
}

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

@@ -53,6 +53,13 @@ namespace Discord
/// </returns>
VerificationLevel VerificationLevel { get; }
/// <summary>
/// Gets the level of content filtering applied to user's content in a Guild.
/// </summary>
/// <returns>
/// The level of explicit content filtering.
/// </returns>
ExplicitContentFilterLevel ExplicitContentFilter { get; }
/// <summary>
/// Gets the ID of this guild's icon.
/// </summary>
/// <returns>
@@ -141,6 +148,13 @@ namespace Discord
/// </returns>
ulong OwnerId { get; }
/// <summary>
/// Gets the application ID of the guild creator if it is bot-created.
/// </summary>
/// <returns>
/// A <see cref="ulong"/> representing the snowflake identifier of the application ID that created this guild, or <c>null</c> if it was not bot-created.
/// </returns>
ulong? ApplicationId { get; }
/// <summary>
/// Gets the ID of the region hosting this guild's voice channels.
/// </summary>
/// <returns>


+ 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.
/// </returns>
IReadOnlyCollection<ulong> MentionedUserIds { get; }
/// <summary>
/// Returns the Activity associated with a message.
/// </summary>
/// <remarks>
/// Sent with Rich Presence-related chat embeds.
/// </remarks>
/// <returns>
/// A message's activity, if any is associated.
/// </returns>
MessageActivity Activity { get; }
/// <summary>
/// Returns the Application associated with a messsage.
/// </summary>
/// <remarks>
/// Sent with Rich-Presence-related chat embeds.
/// </remarks>
/// <returns>
/// A message's application, if any is associated.
/// </returns>
MessageApplication Application { get; }
}
}

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

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

@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace Discord
{
@@ -65,5 +67,15 @@ namespace Discord

return builder;
}

public static EmbedBuilder WithFields(this EmbedBuilder builder, IEnumerable<EmbedFieldBuilder> fields)
{
foreach (var field in fields)
builder.AddField(field);

return builder;
}
public static EmbedBuilder WithFields(this EmbedBuilder builder, params EmbedFieldBuilder[] fields)
=> WithFields(builder, fields.AsEnumerable());
}
}

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

namespace Discord.API
@@ -25,10 +25,12 @@ namespace Discord.API
public bool EmbedEnabled { get; set; }
[JsonProperty("embed_channel_id")]
public ulong? EmbedChannelId { get; set; }
[JsonProperty("system_channel_id")]
public ulong? SystemChannelId { get; set; }
[JsonProperty("verification_level")]
public VerificationLevel VerificationLevel { get; set; }
[JsonProperty("default_message_notifications")]
public DefaultMessageNotifications DefaultMessageNotifications { get; set; }
[JsonProperty("explicit_content_filter")]
public ExplicitContentFilterLevel ExplicitContentFilter { get; set; }
[JsonProperty("voice_states")]
public VoiceState[] VoiceStates { get; set; }
[JsonProperty("roles")]
@@ -39,7 +41,9 @@ namespace Discord.API
public string[] Features { get; set; }
[JsonProperty("mfa_level")]
public MfaLevel MfaLevel { get; set; }
[JsonProperty("default_message_notifications")]
public DefaultMessageNotifications DefaultMessageNotifications { get; set; }
[JsonProperty("application_id")]
public ulong? ApplicationId { get; set; }
[JsonProperty("system_channel_id")]
public ulong? SystemChannelId { get; set; }
}
}

+ 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; }
[JsonProperty("reactions")]
public Optional<Reaction[]> Reactions { get; set; }
// sent with Rich Presence-related chat embeds
[JsonProperty("activity")]
public Optional<MessageActivity> Activity { get; set; }
// sent with Rich Presence-related chat embeds
[JsonProperty("application")]
public Optional<MessageApplication> Application { get; set; }
}
}

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

namespace Discord.API


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

namespace Discord.API.Rest
@@ -12,5 +12,7 @@ namespace Discord.API.Rest
public Optional<int> Position { get; set; }
[JsonProperty("parent_id")]
public Optional<ulong?> CategoryId { get; set; }
[JsonProperty("permission_overwrites")]
public Optional<Overwrite[]> Overwrites { get; set; }
}
}

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

namespace Discord.API.Rest
@@ -28,5 +28,7 @@ namespace Discord.API.Rest
public Optional<ulong?> AfkChannelId { get; set; }
[JsonProperty("owner_id")]
public Optional<ulong> OwnerId { get; set; }
[JsonProperty("explicit_content_filter")]
public Optional<ExplicitContentFilterLevel> ExplicitContentFilter { get; set; }
}
}

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

@@ -1,3 +1,4 @@

#pragma warning disable CS1591
using Discord.API.Rest;
using Discord.Net;
@@ -977,6 +978,8 @@ namespace Discord.API
Preconditions.NotNull(args, nameof(args));
Preconditions.AtLeast(args.MaxAge, 0, nameof(args.MaxAge));
Preconditions.AtLeast(args.MaxUses, 0, nameof(args.MaxUses));
Preconditions.AtMost(args.MaxAge, 86400, nameof(args.MaxAge),
"The maximum age of an invite must be less than or equal to a day (86400 seconds).");
options = RequestOptions.CreateOrClone(options);

var ids = new BucketIds(channelId: channelId);
@@ -1426,8 +1429,11 @@ namespace Discord.API

lastIndex = rightIndex + 1;
}
if (builder[builder.Length - 1] == '/')
builder.Remove(builder.Length - 1, 1);

format = builder.ToString();

return x => string.Format(format, x.ToArray());
}
catch (Exception ex)


+ 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,
int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options)
{
var args = new CreateChannelInviteParams { IsTemporary = isTemporary, IsUnique = isUnique };
args.MaxAge = maxAge.GetValueOrDefault(0);
args.MaxUses = maxUses.GetValueOrDefault(0);
var args = new API.Rest.CreateChannelInviteParams
{
IsTemporary = isTemporary,
IsUnique = isUnique,
MaxAge = maxAge ?? 0,
MaxUses = maxUses ?? 0
};
var model = await client.ApiClient.CreateChannelInviteAsync(channel.Id, args, options).ConfigureAwait(false);
return RestInviteMetadata.Create(client, null, channel, model);
}
@@ -348,6 +352,23 @@ namespace Discord.Rest
var model = await client.ApiClient.GetChannelAsync(channel.CategoryId.Value, options).ConfigureAwait(false);
return RestCategoryChannel.Create(client, model) as ICategoryChannel;
}
public static async Task SyncPermissionsAsync(INestedChannel channel, BaseDiscordClient client, RequestOptions options)
{
var category = await GetCategoryAsync(channel, client, options).ConfigureAwait(false);
if (category == null) throw new InvalidOperationException("This channel does not have a parent channel.");

var apiArgs = new ModifyGuildChannelParams
{
Overwrites = category.PermissionOverwrites
.Select(overwrite => new API.Overwrite{
TargetId = overwrite.TargetId,
TargetType = overwrite.TargetType,
Allow = overwrite.Permissions.AllowValue,
Deny = overwrite.Permissions.DenyValue
}).ToArray()
};
await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false);
}

//Helpers
private static IUser GetAuthor(BaseDiscordClient client, IGuild guild, UserModel model, ulong? webhookId)


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

@@ -201,6 +201,8 @@ namespace Discord.Rest
/// </returns>
public Task<ICategoryChannel> GetCategoryAsync(RequestOptions options = null)
=> ChannelHelper.GetCategoryAsync(this, Discord, options);
public Task SyncPermissionsAsync(RequestOptions options = null)
=> ChannelHelper.SyncPermissionsAsync(this, Discord, options);

//Invites
/// <inheritdoc />


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

@@ -57,6 +57,9 @@ namespace Discord.Rest
/// </returns>
public Task<ICategoryChannel> GetCategoryAsync(RequestOptions options = null)
=> ChannelHelper.GetCategoryAsync(this, Discord, options);
public Task SyncPermissionsAsync(RequestOptions options = null)
=> ChannelHelper.SyncPermissionsAsync(this, Discord, options);
//Invites
/// <inheritdoc />


+ 5
- 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?>(),
Name = args.Name,
Splash = args.Splash.IsSpecified ? args.Splash.Value?.ToModel() : Optional.Create<ImageModel?>(),
VerificationLevel = args.VerificationLevel
VerificationLevel = args.VerificationLevel,
ExplicitContentFilter = args.ExplicitContentFilter
};

if (args.AfkChannel.IsSpecified)
@@ -60,6 +61,9 @@ namespace Discord.Rest
if (!apiArgs.Icon.IsSpecified && guild.IconId != null)
apiArgs.Icon = new ImageModel(guild.IconId);

if (args.ExplicitContentFilter.IsSpecified)
apiArgs.ExplicitContentFilter = args.ExplicitContentFilter.Value;

return await client.ApiClient.ModifyGuildAsync(guild.Id, apiArgs, options).ConfigureAwait(false);
}
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <c>null</c>.</exception>


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

@@ -32,6 +32,8 @@ namespace Discord.Rest
public MfaLevel MfaLevel { get; private set; }
/// <inheritdoc />
public DefaultMessageNotifications DefaultMessageNotifications { get; private set; }
/// <inheritdoc />
public ExplicitContentFilterLevel ExplicitContentFilter { get; private set; }

/// <inheritdoc />
public ulong? AFKChannelId { get; private set; }
@@ -48,6 +50,8 @@ namespace Discord.Rest
/// <inheritdoc />
public string SplashId { get; private set; }
internal bool Available { get; private set; }
/// <inheritdoc />
public ulong? ApplicationId { get; private set; }

/// <inheritdoc />
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id);
@@ -98,6 +102,8 @@ namespace Discord.Rest
VerificationLevel = model.VerificationLevel;
MfaLevel = model.MfaLevel;
DefaultMessageNotifications = model.DefaultMessageNotifications;
ExplicitContentFilter = model.ExplicitContentFilter;
ApplicationId = model.ApplicationId;

if (model.Emojis != null)
{


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

@@ -55,6 +55,10 @@ namespace Discord.Rest

/// <inheritdoc />
public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks);
/// <inheritdoc />
public MessageActivity Activity { get; private set; }
/// <inheritdoc />
public MessageApplication Application { get; private set; }

internal RestMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author, MessageSource source)
: base(discord, id)
@@ -77,6 +81,29 @@ namespace Discord.Rest

if (model.Content.IsSpecified)
Content = model.Content.Value;

if (model.Application.IsSpecified)
{
// create a new Application from the API model
Application = new MessageApplication()
{
Id = model.Application.Value.Id,
CoverImage = model.Application.Value.CoverImage,
Description = model.Application.Value.Description,
Icon = model.Application.Value.Icon,
Name = model.Application.Value.Name
};
}

if (model.Activity.IsSpecified)
{
// create a new Activity from the API model
Activity = new MessageActivity()
{
Type = model.Activity.Value.Type.Value,
PartyId = model.Activity.Value.PartyId.Value
};
}
}

/// <inheritdoc />


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

@@ -31,6 +31,8 @@ namespace Discord.WebSocket
/// </returns>
public ICategoryChannel Category
=> CategoryId.HasValue ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel : null;
public Task SyncPermissionsAsync(RequestOptions options = null)
=> ChannelHelper.SyncPermissionsAsync(this, Discord, options);

private bool _nsfw;
/// <inheritdoc />


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

@@ -30,6 +30,8 @@ namespace Discord.WebSocket
/// </returns>
public ICategoryChannel Category
=> CategoryId.HasValue ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel : null;
public Task SyncPermissionsAsync(RequestOptions options = null)
=> ChannelHelper.SyncPermissionsAsync(this, Discord, options);

/// <inheritdoc />
public override IReadOnlyCollection<SocketGuildUser> Users


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

@@ -50,6 +50,8 @@ namespace Discord.WebSocket
public MfaLevel MfaLevel { get; private set; }
/// <inheritdoc />
public DefaultMessageNotifications DefaultMessageNotifications { get; private set; }
/// <inheritdoc />
public ExplicitContentFilterLevel ExplicitContentFilter { get; private set; }
/// <summary>
/// Gets the number of members.
/// </summary>
@@ -73,6 +75,8 @@ namespace Discord.WebSocket
internal bool IsAvailable { get; private set; }
/// <summary> Indicates whether the client is connected to this guild. </summary>
public bool IsConnected { get; internal set; }
/// <inheritdoc />
public ulong? ApplicationId { get; internal set; }

internal ulong? AFKChannelId { get; private set; }
internal ulong? EmbedChannelId { get; private set; }
@@ -346,6 +350,8 @@ namespace Discord.WebSocket
VerificationLevel = model.VerificationLevel;
MfaLevel = model.MfaLevel;
DefaultMessageNotifications = model.DefaultMessageNotifications;
ExplicitContentFilter = model.ExplicitContentFilter;
ApplicationId = model.ApplicationId;

if (model.Emojis != null)
{


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

@@ -43,6 +43,13 @@ namespace Discord.WebSocket
public virtual bool IsPinned => false;
/// <inheritdoc />
public virtual DateTimeOffset? EditedTimestamp => null;

/// <inheritdoc />
public MessageActivity Activity { get; private set; }

/// <inheritdoc />
public MessageApplication Application { get; private set; }

/// <summary>
/// Returns all attachments included in this message.
/// </summary>
@@ -105,6 +112,29 @@ namespace Discord.WebSocket

if (model.Content.IsSpecified)
Content = model.Content.Value;

if (model.Application.IsSpecified)
{
// create a new Application from the API model
Application = new MessageApplication()
{
Id = model.Application.Value.Id,
CoverImage = model.Application.Value.CoverImage,
Description = model.Application.Value.Description,
Icon = model.Application.Value.Icon,
Name = model.Application.Value.Name
};
}

if (model.Activity.IsSpecified)
{
// create a new Activity from the API model
Activity = new MessageActivity()
{
Type = model.Activity.Value.Type.Value,
PartyId = model.Activity.Value.PartyId.Value
};
}
}

/// <inheritdoc />


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

@@ -1,7 +1,10 @@
using Discord.Rest;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Discord.Rest;
using Model = Discord.API.User;

namespace Discord.WebSocket
@@ -35,6 +38,8 @@ namespace Discord.WebSocket
public IActivity Activity => Presence.Activity;
/// <inheritdoc />
public UserStatus Status => Presence.Status;
public IReadOnlyCollection<SocketGuild> MutualGuilds
=> Discord.Guilds.Where(g => g.Users.Any(u => u.Id == Id)).ToImmutableArray();

internal SocketUser(DiscordSocketClient discord, ulong id)
: base(discord, id)


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

@@ -5,8 +5,25 @@ using Xunit;

namespace Discord
{
public class GuidPermissionsTests
public partial class Tests
{
/// <summary>
/// Tests the behavior of modifying the ExplicitContentFilter property of a Guild.
/// </summary>
[Fact]
public async Task TestExplicitContentFilter()
{
foreach (var level in Enum.GetValues(typeof(ExplicitContentFilterLevel)))
{
await _guild.ModifyAsync(x => x.ExplicitContentFilter = (ExplicitContentFilterLevel)level);
await _guild.UpdateAsync();
Assert.Equal(level, _guild.ExplicitContentFilter);
}
}

/// <summary>
/// Tests the behavior of the GuildPermissions class.
/// </summary>
[Fact]
public Task TestGuildPermission()
{

Loading…
Cancel
Save