Browse Source

Merge branch 'dev' into NullCoalescingAssignments

pull/1391/head
Christopher F GitHub 5 years ago
parent
commit
73a44b7730
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
95 changed files with 1419 additions and 286 deletions
  1. +74
    -0
      CHANGELOG.md
  2. +15
    -0
      Discord.Net.sln
  3. +3
    -3
      Discord.Net.targets
  4. +29
    -0
      docs/_overwrites/Common/DiscordComparers.Overwrites.md
  5. +2
    -2
      docs/docfx.json
  6. +1
    -1
      docs/faq/basics/basic-operations.md
  7. +4
    -4
      docs/guides/commands/intro.md
  8. +79
    -0
      docs/guides/commands/namedarguments.md
  9. +1
    -0
      docs/guides/commands/samples/preconditions/require_role.cs
  10. +2
    -0
      docs/guides/toc.yml
  11. +2
    -1
      samples/01_basic_ping_bot/Program.cs
  12. +2
    -1
      samples/02_commands_framework/Program.cs
  13. +2
    -1
      samples/03_sharded_client/Program.cs
  14. +74
    -0
      samples/idn/Inspector.cs
  15. +152
    -0
      samples/idn/Program.cs
  16. +16
    -0
      samples/idn/idn.csproj
  17. +1
    -0
      samples/idn/logview.ps1
  18. +10
    -10
      src/Discord.Net.Commands/CommandService.cs
  19. +1
    -1
      src/Discord.Net.Commands/CommandServiceConfig.cs
  20. +6
    -2
      src/Discord.Net.Commands/ModuleBase.cs
  21. +5
    -1
      src/Discord.Net.Core/Entities/Activities/ActivityType.cs
  22. +40
    -0
      src/Discord.Net.Core/Entities/Activities/CustomStatusGame.cs
  23. +33
    -0
      src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs
  24. +25
    -1
      src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs
  25. +5
    -1
      src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs
  26. +4
    -4
      src/Discord.Net.Core/Entities/Channels/INestedChannel.cs
  27. +2
    -2
      src/Discord.Net.Core/Entities/Channels/ITextChannel.cs
  28. +4
    -0
      src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs
  29. +16
    -1
      src/Discord.Net.Core/Entities/Guilds/IGuild.cs
  30. +24
    -0
      src/Discord.Net.Core/Entities/Messages/AllowedMentionTypes.cs
  31. +64
    -0
      src/Discord.Net.Core/Entities/Messages/AllowedMentions.cs
  32. +34
    -7
      src/Discord.Net.Core/Entities/Messages/IMessage.cs
  33. +1
    -1
      src/Discord.Net.Core/Entities/Messages/IUserMessage.cs
  34. +33
    -0
      src/Discord.Net.Core/Entities/Messages/MessageReference.cs
  35. +20
    -19
      src/Discord.Net.Core/Entities/Roles/Color.cs
  36. +2
    -2
      src/Discord.Net.Core/Entities/Users/IGuildUser.cs
  37. +4
    -4
      src/Discord.Net.Core/Entities/Users/IUser.cs
  38. +2
    -2
      src/Discord.Net.Core/Extensions/CollectionExtensions.cs
  39. +3
    -3
      src/Discord.Net.Core/Extensions/MessageExtensions.cs
  40. +9
    -4
      src/Discord.Net.Core/Extensions/UserExtensions.cs
  41. +1
    -1
      src/Discord.Net.Core/IDiscordClient.cs
  42. +1
    -1
      src/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs
  43. +1
    -2
      src/Discord.Net.Core/RequestOptions.cs
  44. +5
    -5
      src/Discord.Net.Providers.WS4Net/WS4NetClient.cs
  45. +4
    -3
      src/Discord.Net.Rest/API/Common/AuditLogOptions.cs
  46. +6
    -0
      src/Discord.Net.Rest/API/Common/Game.cs
  47. +4
    -0
      src/Discord.Net.Rest/API/Common/Message.cs
  48. +16
    -0
      src/Discord.Net.Rest/API/Common/MessageReference.cs
  49. +3
    -1
      src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs
  50. +2
    -0
      src/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs
  51. +6
    -11
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  52. +6
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs
  53. +32
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs
  54. +7
    -14
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs
  55. +2
    -2
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs
  56. +2
    -2
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs
  57. +29
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberDisconnectAuditLogData.cs
  58. +37
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberMoveAuditLogData.cs
  59. +38
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageBulkDeleteAuditLogData.cs
  60. +9
    -6
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs
  61. +48
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs
  62. +48
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs
  63. +6
    -5
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs
  64. +21
    -2
      src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs
  65. +5
    -1
      src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs
  66. +4
    -4
      src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs
  67. +5
    -4
      src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs
  68. +0
    -24
      src/Discord.Net.Rest/Entities/Channels/RestNewsChannel.cs
  69. +4
    -4
      src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs
  70. +7
    -3
      src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs
  71. +12
    -3
      src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
  72. +15
    -0
      src/Discord.Net.Rest/Entities/Messages/AllowedMentions.cs
  73. +5
    -0
      src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs
  74. +13
    -0
      src/Discord.Net.Rest/Entities/Messages/RestMessage.cs
  75. +26
    -0
      src/Discord.Net.Rest/Extensions/EntityExtensions.cs
  76. +23
    -0
      src/Discord.Net.WebSocket/ClientState.cs
  77. +5
    -2
      src/Discord.Net.WebSocket/DiscordShardedClient.cs
  78. +6
    -12
      src/Discord.Net.WebSocket/DiscordSocketApiClient.cs
  79. +16
    -3
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  80. +5
    -1
      src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs
  81. +4
    -4
      src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs
  82. +4
    -4
      src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs
  83. +1
    -49
      src/Discord.Net.WebSocket/Entities/Channels/SocketNewsChannel.cs
  84. +4
    -4
      src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs
  85. +33
    -3
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
  86. +14
    -0
      src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs
  87. +1
    -1
      src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs
  88. +17
    -0
      src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs
  89. +22
    -0
      src/Discord.Net.WebSocket/GatewayReconnectException.cs
  90. +7
    -6
      src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs
  91. +1
    -1
      src/Discord.Net.Webhook/DiscordWebhookClient.cs
  92. +17
    -17
      src/Discord.Net/Discord.Net.nuspec
  93. +1
    -1
      test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs
  94. +1
    -1
      test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs
  95. +1
    -1
      test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs

+ 74
- 0
CHANGELOG.md View File

@@ -1,5 +1,79 @@
# Changelog

## [2.2.0] - 2020-04-16
### Added
- #1247 Implement Client Status Support (9da11b4)
- #1310 id overload for RemoveReactionAsync (c88b1da)
- #1319 BOOST (faf23de)
- #1326 Added a Rest property to DiscordShardedClient (9fede34)
- #1348 Add Quote Formatting (265da99)
- #1354 Add support for setting X-RateLimit-Precision (9482204)
- #1355 Provide ParameterInfo with error ParseResult (3755a02)
- #1357 add the "Stream" permission. (b00da3d)
- #1358 Add ChannelFollowAdd MessageType (794eba5)
- #1369 Add SelfStream voice state property (9bb08c9)
- #1372 support X-RateLimit-Reset-After (7b9029d)
- #1373 update audit log models (c54867f)
- #1377 Support filtering audit log entries on user, action type, and before entry id (68eb71c)
- #1386 support guild subscription opt-out (0d54207)
- #1387 #1381 Guild PreferredLocale support (a61adb0)
- #1406 CustomStatusGame Activity (79a0ea9)
- #1413 Implemented Message Reference Property (f86c39d)
- #1414 add StartedAt, EndsAt, Elapsed and Remaining to SpotifyGame. (2bba324)
- #1432 Add ability to modify the banner for guilds (d734ce0)
- suppress messages (cd28892)

### Fixed
- #1318 #1314 Don't parse tags within code blocks (c977f2e)
- #1333 Remove null coalescing on ToEmbedBuilder Color (120c0f7)
- #1337 Fixed attempting to access a non-present optional value (4edda5b)
- #1346 CommandExecuted event will fire when a parameter precondition fails like what happens when standard precondition fails. (e8cb031)
- #1371 Fix keys of guild update audit (b0a595b)
- #1375 Use double precision for X-Reset-After, set CultureInfo when parsing numeric types (606dac3)
- #1392 patch todo in NamedTypeReader (0bda8a4)
- #1405 add .NET Standard 2.1 support for Color (7f0c0c9)
- #1412 GetUsersAsync to use MaxUsersPerBatch const as limit instead of MaxMessagesPerBatch. (5439cba)
- #1416 false-positive detection of CustomStatusGame based on Id property (a484651)
- #1418 #1335 Add isMentionable parameter to CreateRoleAsync in non-breaking manner (1c63fd4)
- #1421 (3ff4e3d)
- include MessageFlags and SuppressEmbedParams (d6d4429)

### Changed
- #1368 Update ISystemMessage interface to allow reactions (07f4d5f)
- #1417 fix #1415 Re-add support for overwrite permissions for news channels (e627f07)
- use millisecond precision by default (bcb3534)

### Misc
- #1290 Split Unit and Integration tests into separate projects (a797be9)
- #1328 Fix #1327 Color.ToString returns wrong value (1e8aa08)
- #1329 Fix invalid cref values in docs (363d1c6)
- #1330 Fix spelling mistake in ExclusiveBulkDelete warning (c864f48)
- #1331 Change token explanation (0484fe8)
- #1349 Fixed a spelling error. (af79ed5)
- #1353 [ci skip] Removed duplicate "any" from the readme (15b2a36)
- #1359 Fixing GatewayEncoding comment (52565ed)
- #1379 September 2019 Documentation Update (fd3810e)
- #1382 Fix .NET Core 3.0 compatibility + Drop NS1.3 (d199d93)
- #1388 fix coercion error with DateTime/Offset (3d39704)
- #1393 Utilize ValueTuples (99d7135)
- #1400 Fix #1394 Misworded doc for command params args (1c6ee72)
- #1401 Fix package publishing in azure pipelines (a08d529)
- #1402 Fix packaging (65223a6)
- #1403 Cache regex instances in MessageHelper (007b011)
- #1424 Fix the Comparer descriptions not linking the type (911523d)
- #1426 Fix incorrect and missing colour values for Color fields (9ede6b9)
- #1470 Added System.Linq reference (adf823c)
- temporary sanity checking in SocketGuild (c870e67)
- build and deploy docs automatically (2981d6b)
- 2.2.0 (4b602b4)
- target the Process env-var scope (3c6b376)
- fix metapackage build (1794f95)
- copy only _site to docs-static (a8cdadc)
- do not exit on failed robocopy (fd204ee)
- add idn debugger (91aec9f)
- rename IsStream to IsStreaming (dcd9cdd)
- feature (40844b9)

## [2.1.1] - 2019-06-08
### Fixed
- #994: Remainder parameters now ignore character escaping, as there is no reason to escape characters here (2e95c49)


+ 15
- 0
Discord.Net.sln View File

@@ -40,6 +40,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Analyzers.Tests
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Examples", "src\Discord.Net.Examples\Discord.Net.Examples.csproj", "{47820065-3CFB-401C-ACEA-862BD564A404}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "idn", "samples\idn\idn.csproj", "{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -218,6 +220,18 @@ Global
{47820065-3CFB-401C-ACEA-862BD564A404}.Release|x64.Build.0 = Release|Any CPU
{47820065-3CFB-401C-ACEA-862BD564A404}.Release|x86.ActiveCfg = Release|Any CPU
{47820065-3CFB-401C-ACEA-862BD564A404}.Release|x86.Build.0 = Release|Any CPU
{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Debug|x64.ActiveCfg = Debug|Any CPU
{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Debug|x64.Build.0 = Debug|Any CPU
{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Debug|x86.ActiveCfg = Debug|Any CPU
{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Debug|x86.Build.0 = Debug|Any CPU
{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|Any CPU.Build.0 = Release|Any CPU
{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x64.ActiveCfg = Release|Any CPU
{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x64.Build.0 = Release|Any CPU
{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x86.ActiveCfg = Release|Any CPU
{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -236,6 +250,7 @@ Global
{E169E15A-E82C-45BF-8C24-C2CADB7093AA} = {C7CF5621-7D36-433B-B337-5A2E3C101A71}
{FC67057C-E92F-4E1C-98BE-46F839C8AD71} = {C7CF5621-7D36-433B-B337-5A2E3C101A71}
{47820065-3CFB-401C-ACEA-862BD564A404} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}
{4A03840B-9EBE-47E3-89AB-E0914DF21AFB} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D2404771-EEC8-45F2-9D71-F3373F6C1495}


+ 3
- 3
Discord.Net.targets View File

@@ -1,9 +1,9 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VersionPrefix>2.2.0</VersionPrefix>
<VersionPrefix>2.3.0</VersionPrefix>
<VersionSuffix>dev</VersionSuffix>
<Authors>RogueException</Authors>
<LangVersion>8.0</LangVersion>
<LangVersion>latest</LangVersion>
<Authors>Discord.Net Contributors</Authors>
<PackageTags>discord;discordapp</PackageTags>
<PackageProjectUrl>https://github.com/RogueException/Discord.Net</PackageProjectUrl>
<PackageLicenseUrl>http://opensource.org/licenses/MIT</PackageLicenseUrl>


+ 29
- 0
docs/_overwrites/Common/DiscordComparers.Overwrites.md View File

@@ -0,0 +1,29 @@
---
uid: Discord.DiscordComparers.ChannelComparer
summary: *content
---
Gets an [IEqualityComparer](xref:System.Collections.Generic.IEqualityComparer`1)<<xref:Discord.IChannel>> to be used to compare channels.

---
uid: Discord.DiscordComparers.GuildComparer
summary: *content
---
Gets an [IEqualityComparer](xref:System.Collections.Generic.IEqualityComparer`1)<<xref:Discord.IGuild>> to be used to compare guilds.

---
uid: Discord.DiscordComparers.MessageComparer
summary: *content
---
Gets an [IEqualityComparer](xref:System.Collections.Generic.IEqualityComparer`1)<<xref:Discord.IMessage>> to be used to compare messages.

---
uid: Discord.DiscordComparers.RoleComparer
summary: *content
---
Gets an [IEqualityComparer](xref:System.Collections.Generic.IEqualityComparer`1)<<xref:Discord.IRole>> to be used to compare roles.

---
uid: Discord.DiscordComparers.UserComparer
summary: *content
---
Gets an [IEqualityComparer](xref:System.Collections.Generic.IEqualityComparer`1)<<xref:Discord.IUser>> to be used to compare users.

+ 2
- 2
docs/docfx.json View File

@@ -9,7 +9,7 @@
"dest": "api",
"filter": "filterConfig.yml",
"properties": {
"TargetFramework": "netstandard1.3"
"TargetFramework": "netstandard2.0"
}
}],
"build": {
@@ -51,7 +51,7 @@
"overwrite": "_overwrites/**/**.md",
"globalMetadata": {
"_appTitle": "Discord.Net Documentation",
"_appFooter": "Discord.Net (c) 2015-2019 2.1.1",
"_appFooter": "Discord.Net (c) 2015-2020 2.2.0",
"_enableSearch": true,
"_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg",
"_appFaviconPath": "favicon.ico"


+ 1
- 1
docs/faq/basics/basic-operations.md View File

@@ -88,7 +88,7 @@ implement [IEmote] and are valid options.

***

[AddReactionAsync]: xref:Discord.IUserMessage.AddReactionAsync*
[AddReactionAsync]: xref:Discord.IMessage.AddReactionAsync*

## What is a "preemptive rate limit?"



+ 4
- 4
docs/guides/commands/intro.md View File

@@ -71,11 +71,11 @@ By now, your module should look like this:

> [!WARNING]
> **Avoid using long-running code** in your modules wherever possible.
> You should **not** be implementing very much logic into your
> modules, instead, outsource to a service for that.
> Long-running code, by default, within a command module
> can cause gateway thread to be blocked; therefore, interrupting
> the bot's connection to Discord.
>
> If you are unfamiliar with Inversion of Control, it is recommended
> to read the MSDN article on [IoC] and [Dependency Injection].
> You may read more about it in @FAQ.Commands.General .

The next step to creating commands is actually creating the commands.



+ 79
- 0
docs/guides/commands/namedarguments.md View File

@@ -0,0 +1,79 @@
---
uid: Guides.Commands.NamedArguments
title: Named Arguments
---

# Named Arguments

By default, arguments for commands are parsed positionally, meaning
that the order matters. But sometimes you may want to define a command
with many optional parameters, and it'd be easier for end-users
to only specify what they want to set, instead of needing them
to specify everything by hand.

## Setting up Named Arguments

In order to be able to specify different arguments by name, you have
to create a new class that contains all of the optional values that
the command will use, and apply an instance of
[NamedArgumentTypeAttribute] on it.

### Example - Creating a Named Arguments Type

```cs
[NamedArgumentType]
public class NamableArguments
{
public string First { get; set; }
public string Second { get; set; }
public string Third { get; set; }
public string Fourth { get; set; }
}
```

## Usage in a Command

The command where you want to use these values can be declared like so:
```cs
[Command("act")]
public async Task Act(int requiredArg, NamableArguments namedArgs)
```

The command can now be invoked as
`.act 42 first: Hello fourth: "A string with spaces must be wrapped in quotes" second: World`.

A TypeReader for the named arguments container type is
automatically registered.
It's important that any other arguments that would be required
are placed before the container type.

> [!IMPORTANT]
> A single command can have only __one__ parameter of a
> type annotated with [NamedArgumentTypeAttribute], and it
> **MUST** be the last parameter in the list.
> A command parameter of such an annotated type
> is automatically treated as if that parameter
> has [RemainderAttribute](xref:Discord.Commands.RemainderAttribute)
> applied.

## Complex Types

The TypeReader for Named Argument Types will look for a TypeReader
of every property type, meaning any other command parameter type
will work just the same.

You can also read multiple values into a single property
by making that property an `IEnumerable<T>`. So for example, if your
Named Argument Type has the following field,
```cs
public IEnumerable<int> Numbers { get; set; }
```
then the command can be invoked as
`.cmd numbers: "1, 2, 4, 8, 16, 32"`

## Additional Notes

The use of [`[OverrideTypeReader]`](xref:Discord.Commands.OverrideTypeReaderAttribute)
is also supported on the properties of a Named Argument Type.

[NamedArgumentTypeAttribute]: xref:Discord.Commands.NamedArgumentTypeAttribute

+ 1
- 0
docs/guides/commands/samples/preconditions/require_role.cs View File

@@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Discord.Commands;
using Discord.WebSocket;


+ 2
- 0
docs/guides/toc.yml View File

@@ -27,6 +27,8 @@
topicUid: Guides.Commands.Intro
- name: TypeReaders
topicUid: Guides.Commands.TypeReaders
- name: Named Arguments
topicUid: Guides.Commands.NamedArguments
- name: Preconditions
topicUid: Guides.Commands.Preconditions
- name: Dependency Injection


+ 2
- 1
samples/01_basic_ping_bot/Program.cs View File

@@ -1,4 +1,5 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;
@@ -43,7 +44,7 @@ namespace _01_basic_ping_bot
await _client.StartAsync();

// Block the program until it is closed.
await Task.Delay(-1);
await Task.Delay(Timeout.Infinite);
}

private Task LogAsync(LogMessage log)


+ 2
- 1
samples/02_commands_framework/Program.cs View File

@@ -1,5 +1,6 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Discord;
@@ -45,7 +46,7 @@ namespace _02_commands_framework
// Here we initialize the logic required to register our commands.
await services.GetRequiredService<CommandHandlingService>().InitializeAsync();

await Task.Delay(-1);
await Task.Delay(Timeout.Infinite);
}
}



+ 2
- 1
samples/03_sharded_client/Program.cs View File

@@ -1,4 +1,5 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using _03_sharded_client.Services;
using Discord;
@@ -45,7 +46,7 @@ namespace _03_sharded_client
await client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token"));
await client.StartAsync();

await Task.Delay(-1);
await Task.Delay(Timeout.Infinite);
}
}



+ 74
- 0
samples/idn/Inspector.cs View File

@@ -0,0 +1,74 @@
using System.Collections;
using System.Linq;
using System.Reflection;
using System.Text;

namespace idn
{
public static class Inspector
{
public static string Inspect(object value)
{
var builder = new StringBuilder();
if (value != null)
{
var type = value.GetType().GetTypeInfo();
builder.AppendLine($"[{type.Namespace}.{type.Name}]");
builder.AppendLine($"{InspectProperty(value)}");

if (value is IEnumerable)
{
var items = (value as IEnumerable).Cast<object>().ToArray();
if (items.Length > 0)
{
builder.AppendLine();
foreach (var item in items)
builder.AppendLine($"- {InspectProperty(item)}");
}
}
else
{
var groups = type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(x => x.GetIndexParameters().Length == 0)
.GroupBy(x => x.Name)
.OrderBy(x => x.Key)
.ToArray();
if (groups.Length > 0)
{
builder.AppendLine();
int pad = groups.Max(x => x.Key.Length) + 1;
foreach (var group in groups)
builder.AppendLine($"{group.Key.PadRight(pad, ' ')}{InspectProperty(group.First().GetValue(value))}");
}
}
}
else
builder.AppendLine("null");
return builder.ToString();
}

private static string InspectProperty(object obj)
{
if (obj == null)
return "null";

var type = obj.GetType();

var debuggerDisplay = type.GetProperty("DebuggerDisplay", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (debuggerDisplay != null)
return debuggerDisplay.GetValue(obj).ToString();

var toString = type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Where(x => x.Name == "ToString" && x.DeclaringType != typeof(object))
.FirstOrDefault();
if (toString != null)
return obj.ToString();

var count = type.GetProperty("Count", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (count != null)
return $"[{count.GetValue(obj)} Items]";

return obj.ToString();
}
}
}

+ 152
- 0
samples/idn/Program.cs View File

@@ -0,0 +1,152 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
using Discord;
using Discord.WebSocket;
using System.Collections.Concurrent;
using System.Threading;
using System.Text;
using System.Diagnostics;

namespace idn
{
public class Program
{
public static readonly string[] Imports =
{
"System",
"System.Collections.Generic",
"System.Linq",
"System.Threading.Tasks",
"System.Diagnostics",
"System.IO",
"Discord",
"Discord.Rest",
"Discord.WebSocket",
"idn"
};

static async Task Main(string[] args)
{
var token = File.ReadAllText("token.ignore");
var client = new DiscordSocketClient(new DiscordSocketConfig { LogLevel = LogSeverity.Debug });
var logQueue = new ConcurrentQueue<LogMessage>();
var logCancelToken = new CancellationTokenSource();
int presenceUpdates = 0;

client.Log += msg =>
{
logQueue.Enqueue(msg);
return Task.CompletedTask;
};
Console.CancelKeyPress += (_ev, _s) =>
{
logCancelToken.Cancel();
};

var logTask = Task.Run(async () =>
{
var fs = new FileStream("idn.log", FileMode.Append);
var logStringBuilder = new StringBuilder(200);
string logString = "";

byte[] helloBytes = Encoding.UTF8.GetBytes($"### new log session: {DateTime.Now} ###\n\n");
await fs.WriteAsync(helloBytes);

while (!logCancelToken.IsCancellationRequested)
{
if (logQueue.TryDequeue(out var msg))
{
if (msg.Message?.IndexOf("PRESENCE_UPDATE)") > 0)
{
presenceUpdates++;
continue;
}

_ = msg.ToString(builder: logStringBuilder);
logStringBuilder.AppendLine();
logString = logStringBuilder.ToString();

Debug.Write(logString, "DNET");
await fs.WriteAsync(Encoding.UTF8.GetBytes(logString));
}
await fs.FlushAsync();
try
{
await Task.Delay(100, logCancelToken.Token);
}
finally { }
}

byte[] goodbyeBytes = Encoding.UTF8.GetBytes($"#!! end log session: {DateTime.Now} !!#\n\n\n");
await fs.WriteAsync(goodbyeBytes);
await fs.DisposeAsync();
});

await client.LoginAsync(TokenType.Bot, token);
await client.StartAsync();

var options = ScriptOptions.Default
.AddReferences(GetAssemblies().ToArray())
.AddImports(Imports);

var globals = new ScriptGlobals
{
Client = client,
PUCount = -1,
};

while (true)
{
Console.Write("> ");
string input = Console.ReadLine();

if (input == "quit!")
{
break;
}

object eval;
try
{
globals.PUCount = presenceUpdates;
eval = await CSharpScript.EvaluateAsync(input, options, globals);
}
catch (Exception e)
{
eval = e;
}
Console.WriteLine(Inspector.Inspect(eval));
}

await client.StopAsync();
client.Dispose();
logCancelToken.Cancel();
try
{ await logTask; }
finally { Console.WriteLine("goodbye!"); }
}

static IEnumerable<Assembly> GetAssemblies()
{
var Assemblies = Assembly.GetEntryAssembly().GetReferencedAssemblies();
foreach (var a in Assemblies)
{
var asm = Assembly.Load(a);
yield return asm;
}
yield return Assembly.GetEntryAssembly();
}

public class ScriptGlobals
{
public DiscordSocketClient Client { get; set; }
public int PUCount { get; set; }
}
}
}

+ 16
- 0
samples/idn/idn.csproj View File

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="3.5.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" />
</ItemGroup>

</Project>

+ 1
- 0
samples/idn/logview.ps1 View File

@@ -0,0 +1 @@
Get-Content .\bin\Debug\netcoreapp3.1\idn.log -Tail 3 -Wait

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

@@ -36,7 +36,7 @@ namespace Discord.Commands
internal readonly AsyncEvent<Func<LogMessage, Task>> _logEvent = new AsyncEvent<Func<LogMessage, Task>>();

/// <summary>
/// Occurs when a command is successfully executed without any error.
/// Occurs when a command is executed.
/// </summary>
/// <remarks>
/// This event is fired when a command has been executed, successfully or not. When a command fails to
@@ -49,7 +49,7 @@ namespace Discord.Commands
private readonly ConcurrentDictionary<Type, ModuleInfo> _typedModuleDefs;
private readonly ConcurrentDictionary<Type, ConcurrentDictionary<Type, TypeReader>> _typeReaders;
private readonly ConcurrentDictionary<Type, TypeReader> _defaultTypeReaders;
private readonly ImmutableList<Tuple<Type, Type>> _entityTypeReaders; //TODO: Candidate for C#7 Tuple
private readonly ImmutableList<(Type EntityType, Type TypeReaderType)> _entityTypeReaders;
private readonly HashSet<ModuleInfo> _moduleDefs;
private readonly CommandMap _map;

@@ -124,11 +124,11 @@ namespace Discord.Commands
_defaultTypeReaders[typeof(string)] =
new PrimitiveTypeReader<string>((string x, out string y) => { y = x; return true; }, 0);

var entityTypeReaders = ImmutableList.CreateBuilder<Tuple<Type, Type>>();
entityTypeReaders.Add(new Tuple<Type, Type>(typeof(IMessage), typeof(MessageTypeReader<>)));
entityTypeReaders.Add(new Tuple<Type, Type>(typeof(IChannel), typeof(ChannelTypeReader<>)));
entityTypeReaders.Add(new Tuple<Type, Type>(typeof(IRole), typeof(RoleTypeReader<>)));
entityTypeReaders.Add(new Tuple<Type, Type>(typeof(IUser), typeof(UserTypeReader<>)));
var entityTypeReaders = ImmutableList.CreateBuilder<(Type, Type)>();
entityTypeReaders.Add((typeof(IMessage), typeof(MessageTypeReader<>)));
entityTypeReaders.Add((typeof(IChannel), typeof(ChannelTypeReader<>)));
entityTypeReaders.Add((typeof(IRole), typeof(RoleTypeReader<>)));
entityTypeReaders.Add((typeof(IUser), typeof(UserTypeReader<>)));
_entityTypeReaders = entityTypeReaders.ToImmutable();
}

@@ -408,7 +408,7 @@ namespace Discord.Commands
var typeInfo = type.GetTypeInfo();
if (typeInfo.IsEnum)
return true;
return _entityTypeReaders.Any(x => type == x.Item1 || typeInfo.ImplementedInterfaces.Contains(x.Item2));
return _entityTypeReaders.Any(x => type == x.EntityType || typeInfo.ImplementedInterfaces.Contains(x.TypeReaderType));
}
internal void AddNullableTypeReader(Type valueType, TypeReader valueTypeReader)
{
@@ -439,9 +439,9 @@ namespace Discord.Commands
//Is this an entity?
for (int i = 0; i < _entityTypeReaders.Count; i++)
{
if (type == _entityTypeReaders[i].Item1 || typeInfo.ImplementedInterfaces.Contains(_entityTypeReaders[i].Item1))
if (type == _entityTypeReaders[i].EntityType || typeInfo.ImplementedInterfaces.Contains(_entityTypeReaders[i].EntityType))
{
reader = Activator.CreateInstance(_entityTypeReaders[i].Item2.MakeGenericType(type)) as TypeReader;
reader = Activator.CreateInstance(_entityTypeReaders[i].TypeReaderType.MakeGenericType(type)) as TypeReader;
_defaultTypeReaders[type] = reader;
return reader;
}


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

@@ -44,7 +44,7 @@ namespace Discord.Commands
/// </summary>
/// <example>
/// <code language="cs">
/// QuotationMarkAliasMap = new Dictionary&lt;char, char%gt;()
/// QuotationMarkAliasMap = new Dictionary&lt;char, char&gt;()
/// {
/// {'\"', '\"' },
/// {'“', '”' },


+ 6
- 2
src/Discord.Net.Commands/ModuleBase.cs View File

@@ -31,9 +31,13 @@ namespace Discord.Commands
/// </param>
/// <param name="isTTS">Specifies if Discord should read this <paramref name="message"/> aloud using text-to-speech.</param>
/// <param name="embed">An embed to be displayed alongside the <paramref name="message"/>.</param>
protected virtual async Task<IUserMessage> ReplyAsync(string message = null, bool isTTS = false, Embed embed = null, RequestOptions options = null)
/// <param name="allowedMentions">
/// Specifies if notifications are sent for mentioned users and roles in the <paramref name="message"/>.
/// If <c>null</c>, all mentioned roles and users will be notified.
/// </param>
protected virtual async Task<IUserMessage> ReplyAsync(string message = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null)
{
return await Context.Channel.SendMessageAsync(message, isTTS, embed, options).ConfigureAwait(false);
return await Context.Channel.SendMessageAsync(message, isTTS, embed, options, allowedMentions).ConfigureAwait(false);
}
/// <summary>
/// The method to execute before executing the command.


+ 5
- 1
src/Discord.Net.Core/Entities/Activities/ActivityType.cs View File

@@ -20,6 +20,10 @@ namespace Discord
/// <summary>
/// The user is watching some form of media.
/// </summary>
Watching = 3
Watching = 3,
/// <summary>
/// The user has set a custom status.
/// </summary>
CustomStatus = 4,
}
}

+ 40
- 0
src/Discord.Net.Core/Entities/Activities/CustomStatusGame.cs View File

@@ -0,0 +1,40 @@
using System;
using System.Diagnostics;

namespace Discord
{
/// <summary>
/// A user's activity for their custom status.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class CustomStatusGame : Game
{
internal CustomStatusGame() { }

/// <summary>
/// Gets the emote, if it is set.
/// </summary>
/// <returns>
/// An <see cref="IEmote"/> containing the <see cref="Emoji"/> or <see cref="GuildEmote"/> set by the user.
/// </returns>
public IEmote Emote { get; internal set; }

/// <summary>
/// Gets the timestamp of when this status was created.
/// </summary>
/// <returns>
/// A <see cref="DateTimeOffset"/> containing the time when this status was created.
/// </returns>
public DateTimeOffset CreatedAt { get; internal set; }

/// <summary>
/// Gets the state of the status.
/// </summary>
public string State { get; internal set; }

public override string ToString()
=> $"{Emote} {State}";

private string DebuggerDisplay => $"{Name}";
}
}

+ 33
- 0
src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs View File

@@ -31,6 +31,23 @@ namespace Discord
/// A string containing the name of the song (e.g. <c>Lonely Together (feat. Rita Ora)</c>).
/// </returns>
public string TrackTitle { get; internal set; }

/// <summary>
/// Gets the date when the track started playing.
/// </summary>
/// <returns>
/// A <see cref="DateTimeOffset"/> containing the start timestamp of the song.
/// </returns>
public DateTimeOffset? StartedAt { get; internal set; }

/// <summary>
/// Gets the date when the track ends.
/// </summary>
/// <returns>
/// A <see cref="DateTimeOffset"/> containing the finish timestamp of the song.
/// </returns>
public DateTimeOffset? EndsAt { get; internal set; }

/// <summary>
/// Gets the duration of the song.
/// </summary>
@@ -39,6 +56,22 @@ namespace Discord
/// </returns>
public TimeSpan? Duration { get; internal set; }

/// <summary>
/// Gets the elapsed duration of the song.
/// </summary>
/// <returns>
/// A <see cref="TimeSpan"/> containing the elapsed duration of the song.
/// </returns>
public TimeSpan? Elapsed => DateTimeOffset.UtcNow - StartedAt;

/// <summary>
/// Gets the remaining duration of the song.
/// </summary>
/// <returns>
/// A <see cref="TimeSpan"/> containing the remaining duration of the song.
/// </returns>
public TimeSpan? Remaining => EndsAt - DateTimeOffset.UtcNow;

/// <summary>
/// Gets the track ID of the song.
/// </summary>


+ 25
- 1
src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs View File

@@ -61,6 +61,18 @@ namespace Discord
/// A guild member's role collection was updated.
/// </summary>
MemberRoleUpdated = 25,
/// <summary>
/// A guild member moved to a voice channel.
/// </summary>
MemberMoved = 26,
/// <summary>
/// A guild member disconnected from a voice channel.
/// </summary>
MemberDisconnected = 27,
/// <summary>
/// A bot was added to this guild.
/// </summary>
BotAdded = 28,

/// <summary>
/// A role was created in this guild.
@@ -117,6 +129,18 @@ namespace Discord
/// <summary>
/// A message was deleted from this guild.
/// </summary>
MessageDeleted = 72
MessageDeleted = 72,
/// <summary>
/// Multiple messages were deleted from this guild.
/// </summary>
MessageBulkDeleted = 73,
/// <summary>
/// A message was pinned from this guild.
/// </summary>
MessagePinned = 74,
/// <summary>
/// A message was unpinned from this guild.
/// </summary>
MessageUnpinned = 75,
}
}

+ 5
- 1
src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs View File

@@ -23,11 +23,15 @@ namespace Discord
/// <param name="isTTS">Determines whether the message should be read aloud by Discord or not.</param>
/// <param name="embed">The <see cref="Discord.EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <param name="allowedMentions">
/// Specifies if notifications are sent for mentioned users and roles in the message <paramref name="text"/>.
/// If <c>null</c>, all mentioned roles and users will be notified.
/// </param>
/// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
Task<IUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null);
Task<IUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null);
/// <summary>
/// Sends a file to this message channel with an optional caption.
/// </summary>


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

@@ -40,8 +40,8 @@ namespace Discord
/// Creates a new invite to this channel.
/// </summary>
/// <example>
/// The following example creates a new invite to this channel; the invite lasts for 12 hours and can only
/// be used 3 times throughout its lifespan.
/// <para>The following example creates a new invite to this channel; the invite lasts for 12 hours and can only
/// be used 3 times throughout its lifespan.</para>
/// <code language="cs">
/// await guildChannel.CreateInviteAsync(maxAge: 43200, maxUses: 3);
/// </code>
@@ -60,8 +60,8 @@ namespace Discord
/// Gets a collection of all invites to this channel.
/// </summary>B
/// <example>
/// The following example gets all of the invites that have been created in this channel and selects the
/// most used invite.
/// <para>The following example gets all of the invites that have been created in this channel and selects the
/// most used invite.</para>
/// <code language="cs">
/// var invites = await channel.GetInvitesAsync();
/// if (invites.Count == 0) return;


+ 2
- 2
src/Discord.Net.Core/Entities/Channels/ITextChannel.cs View File

@@ -30,7 +30,7 @@ namespace Discord
/// Gets the current slow-mode delay for this channel.
/// </summary>
/// <returns>
/// An <see cref="Int32"/> representing the time in seconds required before the user can send another
/// An <see cref="int"/> representing the time in seconds required before the user can send another
/// message; <c>0</c> if disabled.
/// </returns>
int SlowModeInterval { get; }
@@ -39,7 +39,7 @@ namespace Discord
/// Bulk-deletes multiple messages.
/// </summary>
/// <example>
/// The following example gets 250 messages from the channel and deletes them.
/// <para>The following example gets 250 messages from the channel and deletes them.</para>
/// <code language="cs">
/// var messages = await textChannel.GetMessagesAsync(250).FlattenAsync();
/// await textChannel.DeleteMessagesAsync(messages);


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

@@ -38,6 +38,10 @@ namespace Discord
/// </summary>
public Optional<Image?> Icon { get; set; }
/// <summary>
/// Gets or sets the banner of the guild.
/// </summary>
public Optional<Image?> Banner { get; set; }
/// <summary>
/// Gets or sets the guild's splash image.
/// </summary>
/// <remarks>


+ 16
- 1
src/Discord.Net.Core/Entities/Guilds/IGuild.cs View File

@@ -510,7 +510,7 @@ namespace Discord
/// Creates a new text channel in this guild.
/// </summary>
/// <example>
/// The following example creates a new text channel under an existing category named <c>Wumpus</c> with a set topic.
/// <para>The following example creates a new text channel under an existing category named <c>Wumpus</c> with a set topic.</para>
/// <code language="cs" region="CreateTextChannelAsync"
/// source="..\..\..\Discord.Net.Examples\Core\Entities\Guilds\IGuild.Examples.cs"/>
/// </example>
@@ -598,6 +598,21 @@ namespace Discord
/// role.
/// </returns>
Task<IRole> CreateRoleAsync(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false, RequestOptions options = null);
// TODO remove CreateRoleAsync overload that does not have isMentionable when breaking change is acceptable
/// <summary>
/// Creates a new role with the provided name.
/// </summary>
/// <param name="name">The new name for the role.</param>
/// <param name="permissions">The guild permission that the role should possess.</param>
/// <param name="color">The color of the role.</param>
/// <param name="isHoisted">Whether the role is separated from others on the sidebar.</param>
/// <param name="isMentionable">Whether the role can be mentioned.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous creation operation. The task result contains the newly created
/// role.
/// </returns>
Task<IRole> CreateRoleAsync(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false, bool isMentionable = false, RequestOptions options = null);

/// <summary>
/// Adds a user to this guild.


+ 24
- 0
src/Discord.Net.Core/Entities/Messages/AllowedMentionTypes.cs View File

@@ -0,0 +1,24 @@
using System;

namespace Discord
{
/// <summary>
/// Specifies the type of mentions that will be notified from the message content.
/// </summary>
[Flags]
public enum AllowedMentionTypes
{
/// <summary>
/// Controls role mentions.
/// </summary>
Roles,
/// <summary>
/// Controls user mentions.
/// </summary>
Users,
/// <summary>
/// Controls <code>@everyone</code> and <code>@here</code> mentions.
/// </summary>
Everyone,
}
}

+ 64
- 0
src/Discord.Net.Core/Entities/Messages/AllowedMentions.cs View File

@@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;

namespace Discord
{
/// <summary>
/// Defines which mentions and types of mentions that will notify users from the message content.
/// </summary>
public class AllowedMentions
{
private static readonly Lazy<AllowedMentions> none = new Lazy<AllowedMentions>(() => new AllowedMentions());
private static readonly Lazy<AllowedMentions> all = new Lazy<AllowedMentions>(() =>
new AllowedMentions(AllowedMentionTypes.Everyone | AllowedMentionTypes.Users | AllowedMentionTypes.Roles));

/// <summary>
/// Gets a value which indicates that no mentions in the message content should notify users.
/// </summary>
public static AllowedMentions None => none.Value;

/// <summary>
/// Gets a value which indicates that all mentions in the message content should notify users.
/// </summary>
public static AllowedMentions All => all.Value;

/// <summary>
/// Gets or sets the type of mentions that will be parsed from the message content.
/// </summary>
/// <remarks>
/// The <see cref="AllowedMentionTypes.Users"/> flag is mutually exclusive with the <see cref="UserIds"/>
/// property, and the <see cref="AllowedMentionTypes.Roles"/> flag is mutually exclusive with the
/// <see cref="RoleIds"/> property.
/// If <c>null</c>, only the ids specified in <see cref="UserIds"/> and <see cref="RoleIds"/> will be mentioned.
/// </remarks>
public AllowedMentionTypes? AllowedTypes { get; set; }

/// <summary>
/// Gets or sets the list of all role ids that will be mentioned.
/// This property is mutually exclusive with the <see cref="AllowedMentionTypes.Roles"/>
/// flag of the <see cref="AllowedTypes"/> property. If the flag is set, the value of this property
/// must be <c>null</c> or empty.
/// </summary>
public List<ulong> RoleIds { get; set; }

/// <summary>
/// Gets or sets the list of all user ids that will be mentioned.
/// This property is mutually exclusive with the <see cref="AllowedMentionTypes.Users"/>
/// flag of the <see cref="AllowedTypes"/> property. If the flag is set, the value of this property
/// must be <c>null</c> or empty.
/// </summary>
public List<ulong> UserIds { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="AllowedMentions"/> class.
/// </summary>
/// <param name="allowedTypes">
/// The types of mentions to parse from the message content.
/// If <c>null</c>, only the ids specified in <see cref="UserIds"/> and <see cref="RoleIds"/> will be mentioned.
/// </param>
public AllowedMentions(AllowedMentionTypes? allowedTypes = null)
{
AllowedTypes = allowedTypes;
}
}
}

+ 34
- 7
src/Discord.Net.Core/Entities/Messages/IMessage.cs View File

@@ -140,6 +140,18 @@ namespace Discord
/// </returns>
MessageApplication Application { get; }

/// <summary>
/// Gets the reference to the original message if it was crossposted.
/// </summary>
/// <remarks>
/// Sent with Cross-posted messages, meaning they were published from news channels
/// and received by subscriber channels.
/// </remarks>
/// <returns>
/// A message's reference, if any is associated.
/// </returns>
MessageReference Reference { get; }

/// <summary>
/// Gets all reactions included in this message.
/// </summary>
@@ -149,7 +161,7 @@ namespace Discord
/// Adds a reaction to this message.
/// </summary>
/// <example>
/// The following example adds the reaction, <c>💕</c>, to the message.
/// <para>The following example adds the reaction, <c>💕</c>, to the message.</para>
/// <code language="cs">
/// await msg.AddReactionAsync(new Emoji("\U0001f495"));
/// </code>
@@ -165,7 +177,7 @@ namespace Discord
/// Removes a reaction from message.
/// </summary>
/// <example>
/// The following example removes the reaction, <c>💕</c>, added by the message author from the message.
/// <para>The following example removes the reaction, <c>💕</c>, added by the message author from the message.</para>
/// <code language="cs">
/// await msg.RemoveReactionAsync(new Emoji("\U0001f495"), msg.Author);
/// </code>
@@ -182,7 +194,7 @@ namespace Discord
/// Removes a reaction from message.
/// </summary>
/// <example>
/// The following example removes the reaction, <c>💕</c>, added by the user with ID 84291986575613952 from the message.
/// <para>The following example removes the reaction, <c>💕</c>, added by the user with ID 84291986575613952 from the message.</para>
/// <code language="cs">
/// await msg.RemoveReactionAsync(new Emoji("\U0001f495"), 84291986575613952);
/// </code>
@@ -207,8 +219,25 @@ namespace Discord
/// <summary>
/// Gets all users that reacted to a message with a given emote.
/// </summary>
/// <remarks>
/// <note type="important">
/// The returned collection is an asynchronous enumerable object; one must call
/// <see cref="AsyncEnumerableExtensions.FlattenAsync{T}"/> to access the users as a
/// collection.
/// </note>
/// <note type="warning">
/// Do not fetch too many users at once! This may cause unwanted preemptive rate limit or even actual
/// rate limit, causing your bot to freeze!
/// </note>
/// This method will attempt to fetch the number of reactions specified under <paramref name="limit"/>.
/// The library will attempt to split up the requests according to your <paramref name="limit"/> and
/// <see cref="DiscordConfig.MaxUserReactionsPerBatch"/>. In other words, should the user request 500 reactions,
/// and the <see cref="Discord.DiscordConfig.MaxUserReactionsPerBatch"/> constant is <c>100</c>, the request will
/// be split into 5 individual requests; thus returning 5 individual asynchronous responses, hence the need
/// of flattening.
/// </remarks>
/// <example>
/// The following example gets the users that have reacted with the emoji <c>💕</c> to the message.
/// <para>The following example gets the users that have reacted with the emoji <c>💕</c> to the message.</para>
/// <code language="cs">
/// var emoji = new Emoji("\U0001f495");
/// var reactedUsers = await message.GetReactionUsersAsync(emoji, 100).FlattenAsync();
@@ -218,9 +247,7 @@ namespace Discord
/// <param name="limit">The number of users to request.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A paged collection containing a read-only collection of users that has reacted to this message.
/// Flattening the paginated response into a collection of users with
/// <see cref="AsyncEnumerableExtensions.FlattenAsync{T}"/> is required if you wish to access the users.
/// Paged collection of users.
/// </returns>
IAsyncEnumerable<IReadOnlyCollection<IUser>> GetReactionUsersAsync(IEmote emoji, int limit, RequestOptions options = null);
}


+ 1
- 1
src/Discord.Net.Core/Entities/Messages/IUserMessage.cs View File

@@ -17,7 +17,7 @@ namespace Discord
/// method and what properties are available, please refer to <see cref="MessageProperties"/>.
/// </remarks>
/// <example>
/// The following example replaces the content of the message with <c>Hello World!</c>.
/// <para>The following example replaces the content of the message with <c>Hello World!</c>.</para>
/// <code language="cs">
/// await msg.ModifyAsync(x =&gt; x.Content = "Hello World!");
/// </code>


+ 33
- 0
src/Discord.Net.Core/Entities/Messages/MessageReference.cs View File

@@ -0,0 +1,33 @@
using System.Diagnostics;

namespace Discord
{
/// <summary>
/// Contains the IDs sent from a crossposted message.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class MessageReference
{
/// <summary>
/// Gets the Message ID of the original message.
/// </summary>
public Optional<ulong> MessageId { get; internal set; }

/// <summary>
/// Gets the Channel ID of the original message.
/// </summary>
public ulong ChannelId { get; internal set; }

/// <summary>
/// Gets the Guild ID of the original message.
/// </summary>
public Optional<ulong> GuildId { get; internal set; }

private string DebuggerDisplay
=> $"Channel ID: ({ChannelId}){(GuildId.IsSpecified ? $", Guild ID: ({GuildId.Value})" : "")}" +
$"{(MessageId.IsSpecified ? $", Message ID: ({MessageId.Value})" : "")}";

public override string ToString()
=> DebuggerDisplay;
}
}

+ 20
- 19
src/Discord.Net.Core/Entities/Roles/Color.cs View File

@@ -16,60 +16,61 @@ namespace Discord
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/1ABC9C">1ABC9C</see>.</returns>
public static readonly Color Teal = new Color(0x1ABC9C);
/// <summary> Gets the dark teal color value. </summary>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/11806A">11806A</see>.</returns>
public static readonly Color DarkTeal = new Color(0x11806A);
/// <summary> Gets the green color value. </summary>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/11806A">11806A</see>.</returns>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/2ECC71">2ECC71</see>.</returns>
public static readonly Color Green = new Color(0x2ECC71);
/// <summary> Gets the dark green color value. </summary>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/2ECC71">2ECC71</see>.</returns>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/1F8B4C">1F8B4C</see>.</returns>
public static readonly Color DarkGreen = new Color(0x1F8B4C);
/// <summary> Gets the blue color value. </summary>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/1F8B4C">1F8B4C</see>.</returns>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/3498DB">3498DB</see>.</returns>
public static readonly Color Blue = new Color(0x3498DB);
/// <summary> Gets the dark blue color value. </summary>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/3498DB">3498DB</see>.</returns>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/206694">206694</see>.</returns>
public static readonly Color DarkBlue = new Color(0x206694);
/// <summary> Gets the purple color value. </summary>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/206694">206694</see>.</returns>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/9B59B6">9B59B6</see>.</returns>
public static readonly Color Purple = new Color(0x9B59B6);
/// <summary> Gets the dark purple color value. </summary>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/9B59B6">9B59B6</see>.</returns>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/71368A">71368A</see>.</returns>
public static readonly Color DarkPurple = new Color(0x71368A);
/// <summary> Gets the magenta color value. </summary>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/71368A">71368A</see>.</returns>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/E91E63">E91E63</see>.</returns>
public static readonly Color Magenta = new Color(0xE91E63);
/// <summary> Gets the dark magenta color value. </summary>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/E91E63">E91E63</see>.</returns>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/AD1457">AD1457</see>.</returns>
public static readonly Color DarkMagenta = new Color(0xAD1457);
/// <summary> Gets the gold color value. </summary>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/AD1457">AD1457</see>.</returns>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/F1C40F">F1C40F</see>.</returns>
public static readonly Color Gold = new Color(0xF1C40F);
/// <summary> Gets the light orange color value. </summary>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/F1C40F">F1C40F</see>.</returns>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/C27C0E">C27C0E</see>.</returns>
public static readonly Color LightOrange = new Color(0xC27C0E);
/// <summary> Gets the orange color value. </summary>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/C27C0E">C27C0E</see>.</returns>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/E67E22">E67E22</see>.</returns>
public static readonly Color Orange = new Color(0xE67E22);
/// <summary> Gets the dark orange color value. </summary>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/E67E22">E67E22</see>.</returns>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/A84300">A84300</see>.</returns>
public static readonly Color DarkOrange = new Color(0xA84300);
/// <summary> Gets the red color value. </summary>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/A84300">A84300</see>.</returns>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/E74C3C">E74C3C</see>.</returns>
public static readonly Color Red = new Color(0xE74C3C);
/// <summary> Gets the dark red color value. </summary>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/E74C3C">E74C3C</see>.</returns>
public static readonly Color DarkRed = new Color(0x992D22);
/// <summary> Gets the light grey color value. </summary>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/992D22">992D22</see>.</returns>
public static readonly Color DarkRed = new Color(0x992D22);
/// <summary> Gets the light grey color value. </summary>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/979C9F">979C9F</see>.</returns>
public static readonly Color LightGrey = new Color(0x979C9F);
/// <summary> Gets the lighter grey color value. </summary>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/979C9F">979C9F</see>.</returns>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/95A5A6">95A5A6</see>.</returns>
public static readonly Color LighterGrey = new Color(0x95A5A6);
/// <summary> Gets the dark grey color value. </summary>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/95A5A6">95A5A6</see>.</returns>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/607D8B">607D8B</see>.</returns>
public static readonly Color DarkGrey = new Color(0x607D8B);
/// <summary> Gets the darker grey color value. </summary>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/607D8B">607D8B</see>.</returns>
/// <returns> A color struct with the hex value of <see href="http://www.color-hex.com/color/546E7A">546E7A</see>.</returns>
public static readonly Color DarkerGrey = new Color(0x546E7A);

/// <summary> Gets the encoded value for this color. </summary>


+ 2
- 2
src/Discord.Net.Core/Entities/Users/IGuildUser.cs View File

@@ -72,8 +72,8 @@ namespace Discord
/// Gets the level permissions granted to this user to a given channel.
/// </summary>
/// <example>
/// The following example checks if the current user has the ability to send a message with attachment in
/// this channel; if so, uploads a file via <see cref="IMessageChannel.SendFileAsync(string, string, bool, Embed, RequestOptions, bool)"/>.
/// <para>The following example checks if the current user has the ability to send a message with attachment in
/// this channel; if so, uploads a file via <see cref="IMessageChannel.SendFileAsync(string, string, bool, Embed, RequestOptions, bool)"/>.</para>
/// <code language="cs">
/// if (currentUser?.GetPermissions(targetChannel)?.AttachFiles)
/// await targetChannel.SendFileAsync("fortnite.png");


+ 4
- 4
src/Discord.Net.Core/Entities/Users/IUser.cs View File

@@ -21,8 +21,8 @@ namespace Discord
/// example).
/// </remarks>
/// <example>
/// The following example attempts to retrieve the user's current avatar and send it to a channel; if one is
/// not set, a default avatar for this user will be returned instead.
/// <para>The following example attempts to retrieve the user's current avatar and send it to a channel; if one is
/// not set, a default avatar for this user will be returned instead.</para>
/// <code language="cs" region="GetAvatarUrl"
/// source="..\..\..\Discord.Net.Examples\Core\Entities\Users\IUser.Examples.cs"/>
/// </example>
@@ -90,8 +90,8 @@ namespace Discord
/// </note>
/// </remarks>
/// <example>
/// The following example attempts to send a direct message to the target user and logs the incident should
/// it fail.
/// <para>The following example attempts to send a direct message to the target user and logs the incident should
/// it fail.</para>
/// <code region="GetOrCreateDMChannelAsync" language="cs"
/// source="../../../Discord.Net.Examples/Core/Entities/Users/IUser.Examples.cs"/>
/// </example>


+ 2
- 2
src/Discord.Net.Core/Extensions/CollectionExtensions.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
@@ -15,7 +15,7 @@ namespace Discord
//public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> source)
// => new CollectionWrapper<TValue>(source.Select(x => x.Value), () => source.Count);
public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TKey, TValue>(this IDictionary<TKey, TValue> source)
=> new CollectionWrapper<TValue>(source.Select(x => x.Value), () => source.Count);
=> new CollectionWrapper<TValue>(source.Values, () => source.Count);
public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TValue, TSource>(this IEnumerable<TValue> query, IReadOnlyCollection<TSource> source)
=> new CollectionWrapper<TValue>(query, () => source.Count);
public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TValue>(this IEnumerable<TValue> query, Func<int> countFunc)


+ 3
- 3
src/Discord.Net.Core/Extensions/MessageExtensions.cs View File

@@ -39,7 +39,7 @@ namespace Discord
/// <returns>
/// A task that represents the asynchronous operation for adding a reaction to this message.
/// </returns>
/// <seealso cref="IUserMessage.AddReactionAsync(IEmote, RequestOptions)"/>
/// <seealso cref="IMessage.AddReactionAsync(IEmote, RequestOptions)"/>
/// <seealso cref="IEmote"/>
public static async Task AddReactionsAsync(this IUserMessage msg, IEmote[] reactions, RequestOptions options = null)
{
@@ -51,7 +51,7 @@ namespace Discord
/// </summary>
/// <remarks>
/// This method does not bulk remove reactions! If you want to clear reactions from a message,
/// <see cref="IUserMessage.RemoveAllReactionsAsync(RequestOptions)"/>
/// <see cref="IMessage.RemoveAllReactionsAsync(RequestOptions)"/>
/// </remarks>
/// <example>
/// <code language="cs">
@@ -64,7 +64,7 @@ namespace Discord
/// <returns>
/// A task that represents the asynchronous operation for removing a reaction to this message.
/// </returns>
/// <seealso cref="IUserMessage.RemoveReactionAsync(IEmote, IUser, RequestOptions)"/>
/// <seealso cref="IMessage.RemoveReactionAsync(IEmote, IUser, RequestOptions)"/>
/// <seealso cref="IEmote"/>
public static async Task RemoveReactionsAsync(this IUserMessage msg, IUser user, IEmote[] reactions, RequestOptions options = null)
{


+ 9
- 4
src/Discord.Net.Core/Extensions/UserExtensions.cs View File

@@ -28,6 +28,10 @@ namespace Discord
/// <param name="isTTS">Whether the message should be read aloud by Discord or not.</param>
/// <param name="embed">The <see cref="EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <param name="allowedMentions">
/// Specifies if notifications are sent for mentioned users and roles in the message <paramref name="text"/>.
/// If <c>null</c>, all mentioned roles and users will be notified.
/// </param>
/// <returns>
/// A task that represents the asynchronous send operation. The task result contains the sent message.
/// </returns>
@@ -35,17 +39,18 @@ namespace Discord
string text = null,
bool isTTS = false,
Embed embed = null,
RequestOptions options = null)
RequestOptions options = null,
AllowedMentions allowedMentions = null)
{
return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false);
return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false);
}

/// <summary>
/// Sends a file to this message channel with an optional caption.
/// </summary>
/// <example>
/// The following example uploads a streamed image that will be called <c>b1nzy.jpg</c> embedded inside a
/// rich embed to the channel.
/// <para>The following example uploads a streamed image that will be called <c>b1nzy.jpg</c> embedded inside a
/// rich embed to the channel.</para>
/// <code language="cs">
/// await channel.SendFileAsync(b1nzyStream, "b1nzy.jpg",
/// embed: new EmbedBuilder {ImageUrl = "attachment://b1nzy.jpg"}.Build());


+ 1
- 1
src/Discord.Net.Core/IDiscordClient.cs View File

@@ -270,7 +270,7 @@ namespace Discord
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains an <see cref="Int32"/>
/// A task that represents the asynchronous get operation. The task result contains an <see cref="int"/>
/// that represents the number of shards that should be used with this account.
/// </returns>
Task<int> GetRecommendedShardCountAsync(RequestOptions options = null);


+ 1
- 1
src/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs View File

@@ -14,7 +14,7 @@ namespace Discord.Net.WebSockets
void SetCancelToken(CancellationToken cancelToken);

Task ConnectAsync(string host);
Task DisconnectAsync();
Task DisconnectAsync(int closeCode = 1000);

Task SendAsync(byte[] data, int index, int count, bool isText);
}


+ 1
- 2
src/Discord.Net.Core/RequestOptions.cs View File

@@ -49,8 +49,7 @@ namespace Discord
/// clock for rate-limiting. Defaults to <c>true</c>.
/// </summary>
/// <remarks>
/// This property can also be set in <see cref="DiscordConfig">.
///
/// This property can also be set in <see cref="DiscordConfig"/>.
/// On a per-request basis, the system clock should only be disabled
/// when millisecond precision is especially important, and the
/// hosting system is known to have a desynced clock.


+ 5
- 5
src/Discord.Net.Providers.WS4Net/WS4NetClient.cs View File

@@ -40,7 +40,7 @@ namespace Discord.Net.Providers.WS4Net
{
if (disposing)
{
DisconnectInternalAsync(true).GetAwaiter().GetResult();
DisconnectInternalAsync(isDisposing: true).GetAwaiter().GetResult();
_lock?.Dispose();
_cancelTokenSource?.Dispose();
}
@@ -92,19 +92,19 @@ namespace Discord.Net.Providers.WS4Net
_waitUntilConnect.Wait(_cancelToken);
}

public async Task DisconnectAsync()
public async Task DisconnectAsync(int closeCode = 1000)
{
await _lock.WaitAsync().ConfigureAwait(false);
try
{
await DisconnectInternalAsync().ConfigureAwait(false);
await DisconnectInternalAsync(closeCode: closeCode).ConfigureAwait(false);
}
finally
{
_lock.Release();
}
}
private Task DisconnectInternalAsync(bool isDisposing = false)
private Task DisconnectInternalAsync(int closeCode = 1000, bool isDisposing = false)
{
_disconnectCancelTokenSource.Cancel();
if (_client == null)
@@ -112,7 +112,7 @@ namespace Discord.Net.Providers.WS4Net

if (_client.State == WebSocketState.Open)
{
try { _client.Close(1000, ""); }
try { _client.Close(closeCode, ""); }
catch { }
}



+ 4
- 3
src/Discord.Net.Rest/API/Common/AuditLogOptions.cs View File

@@ -4,11 +4,12 @@ namespace Discord.API
{
internal class AuditLogOptions
{
//Message delete
[JsonProperty("count")]
public int? MessageDeleteCount { get; set; }
public int? Count { get; set; }
[JsonProperty("channel_id")]
public ulong? MessageDeleteChannelId { get; set; }
public ulong? ChannelId { get; set; }
[JsonProperty("message_id")]
public ulong? MessageId { get; set; }

//Prune
[JsonProperty("delete_member_days")]


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

@@ -35,6 +35,12 @@ namespace Discord.API
public Optional<string> SessionId { get; set; }
[JsonProperty("Flags")]
public Optional<ActivityProperties> Flags { get; set; }
[JsonProperty("id")]
public Optional<string> Id { get; set; }
[JsonProperty("emoji")]
public Optional<Emoji> Emoji { get; set; }
[JsonProperty("created_at")]
public Optional<long> CreatedAt { get; set; }

[OnError]
internal void OnError(StreamingContext context, ErrorContext errorContext)


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

@@ -50,7 +50,11 @@ namespace Discord.API
// sent with Rich Presence-related chat embeds
[JsonProperty("application")]
public Optional<MessageApplication> Application { get; set; }
[JsonProperty("message_reference")]
public Optional<MessageReference> Reference { get; set; }
[JsonProperty("flags")]
public Optional<MessageFlags> Flags { get; set; }
[JsonProperty("allowed_mentions")]
public Optional<AllowedMentions> AllowedMentions { get; set; }
}
}

+ 16
- 0
src/Discord.Net.Rest/API/Common/MessageReference.cs View File

@@ -0,0 +1,16 @@
using Newtonsoft.Json;

namespace Discord.API
{
internal class MessageReference
{
[JsonProperty("message_id")]
public Optional<ulong> MessageId { get; set; }

[JsonProperty("channel_id")]
public ulong ChannelId { get; set; }

[JsonProperty("guild_id")]
public Optional<ulong> GuildId { get; set; }
}
}

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

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

namespace Discord.API.Rest
@@ -15,6 +15,8 @@ namespace Discord.API.Rest
public Optional<bool> IsTTS { get; set; }
[JsonProperty("embed")]
public Optional<Embed> Embed { get; set; }
[JsonProperty("allowed_mentions")]
public Optional<AllowedMentions> AllowedMentions { get; set; }

public CreateMessageParams(string content)
{


+ 2
- 0
src/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs View File

@@ -22,6 +22,8 @@ namespace Discord.API.Rest
public Optional<ulong?> SystemChannelId { get; set; }
[JsonProperty("icon")]
public Optional<Image?> Icon { get; set; }
[JsonProperty("banner")]
public Optional<Image?> Banner { get; set; }
[JsonProperty("splash")]
public Optional<Image?> Splash { get; set; }
[JsonProperty("afk_channel_id")]


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

@@ -47,7 +47,7 @@ namespace Discord.API
internal ulong? CurrentUserId { get; set; }
public RateLimitPrecision RateLimitPrecision { get; private set; }
internal bool UseSystemClock { get; set; }
internal JsonSerializer Serializer => _serializer;

/// <exception cref="ArgumentException">Unknown OAuth token type.</exception>
@@ -164,7 +164,7 @@ namespace Discord.API
try { _loginCancelToken?.Cancel(false); }
catch { }

await DisconnectInternalAsync().ConfigureAwait(false);
await DisconnectInternalAsync(null).ConfigureAwait(false);
await RequestQueue.ClearAsync().ConfigureAwait(false);

await RequestQueue.SetCancelTokenAsync(CancellationToken.None).ConfigureAwait(false);
@@ -175,7 +175,7 @@ namespace Discord.API
}

internal virtual Task ConnectInternalAsync() => Task.Delay(0);
internal virtual Task DisconnectInternalAsync() => Task.Delay(0);
internal virtual Task DisconnectInternalAsync(Exception ex = null) => Task.Delay(0);

//Core
internal Task SendAsync(string method, Expression<Func<string>> endpointExpr, BucketIds ids,
@@ -602,13 +602,8 @@ namespace Discord.API
Preconditions.NotEqual(channelId, 0, nameof(channelId));
Preconditions.NotEqual(messageId, 0, nameof(messageId));
Preconditions.NotNull(args, nameof(args));
if (args.Content.IsSpecified)
{
if (!args.Embed.IsSpecified)
Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content));
if (args.Content.Value?.Length > DiscordConfig.MaxMessageSize)
throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content));
}
if (args.Content.IsSpecified && args.Content.Value?.Length > DiscordConfig.MaxMessageSize)
throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content));
options = RequestOptions.CreateOrClone(options);

var ids = new BucketIds(channelId: channelId);
@@ -1062,7 +1057,7 @@ namespace Discord.API
{
foreach (var roleId in args.RoleIds.Value)
Preconditions.NotEqual(roleId, 0, nameof(roleId));
}
}

options = RequestOptions.CreateOrClone(options);



+ 6
- 0
src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs View File

@@ -27,6 +27,9 @@ namespace Discord.Rest
[ActionType.Unban] = UnbanAuditLogData.Create,
[ActionType.MemberUpdated] = MemberUpdateAuditLogData.Create,
[ActionType.MemberRoleUpdated] = MemberRoleAuditLogData.Create,
[ActionType.MemberMoved] = MemberMoveAuditLogData.Create,
[ActionType.MemberDisconnected] = MemberDisconnectAuditLogData.Create,
[ActionType.BotAdded] = BotAddAuditLogData.Create,

[ActionType.RoleCreated] = RoleCreateAuditLogData.Create,
[ActionType.RoleUpdated] = RoleUpdateAuditLogData.Create,
@@ -45,6 +48,9 @@ namespace Discord.Rest
[ActionType.EmojiDeleted] = EmoteDeleteAuditLogData.Create,

[ActionType.MessageDeleted] = MessageDeleteAuditLogData.Create,
[ActionType.MessageBulkDeleted] = MessageBulkDeleteAuditLogData.Create,
[ActionType.MessagePinned] = MessagePinAuditLogData.Create,
[ActionType.MessageUnpinned] = MessageUnpinAuditLogData.Create,
};

public static IAuditLogData CreateData(BaseDiscordClient discord, Model log, EntryModel entry)


+ 32
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs View File

@@ -0,0 +1,32 @@
using System.Linq;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
{
/// <summary>
/// Contains a piece of audit log data related to a adding a bot to a guild.
/// </summary>
public class BotAddAuditLogData : IAuditLogData
{
private BotAddAuditLogData(IUser bot)
{
Target = bot;
}

internal static BotAddAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId);
return new BotAddAuditLogData(RestUser.Create(discord, userInfo));
}

/// <summary>
/// Gets the bot that was added.
/// </summary>
/// <returns>
/// A user object representing the bot.
/// </returns>
public IUser Target { get; }
}
}

+ 7
- 14
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs View File

@@ -25,7 +25,6 @@ namespace Discord.Rest
internal static ChannelCreateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var changes = entry.Changes;
var overwrites = new List<Overwrite>();

var overwritesModel = changes.FirstOrDefault(x => x.ChangedProperty == "permission_overwrites");
var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type");
@@ -34,23 +33,17 @@ namespace Discord.Rest
var nsfwModel = changes.FirstOrDefault(x => x.ChangedProperty == "nsfw");
var bitrateModel = changes.FirstOrDefault(x => x.ChangedProperty == "bitrate");

var overwrites = overwritesModel.NewValue.ToObject<API.Overwrite[]>(discord.ApiClient.Serializer)
.Select(x => new Overwrite(x.TargetId, x.TargetType, new OverwritePermissions(x.Allow, x.Deny)))
.ToList();
var type = typeModel.NewValue.ToObject<ChannelType>(discord.ApiClient.Serializer);
var name = nameModel.NewValue.ToObject<string>(discord.ApiClient.Serializer);
int? rateLimitPerUser = rateLimitPerUserModel?.NewValue?.ToObject<int>(discord.ApiClient.Serializer);
bool? nsfw = nsfwModel?.NewValue?.ToObject<bool>(discord.ApiClient.Serializer);
int? bitrate = bitrateModel?.NewValue?.ToObject<int>(discord.ApiClient.Serializer);
var id = entry.TargetId.Value;

foreach (var overwrite in overwritesModel.NewValue)
{
var deny = overwrite["deny"].ToObject<ulong>(discord.ApiClient.Serializer);
var permType = overwrite["type"].ToObject<PermissionTarget>(discord.ApiClient.Serializer);
var id = overwrite["id"].ToObject<ulong>(discord.ApiClient.Serializer);
var allow = overwrite["allow"].ToObject<ulong>(discord.ApiClient.Serializer);

overwrites.Add(new Overwrite(id, permType, new OverwritePermissions(allow, deny)));
}

return new ChannelCreateAuditLogData(entry.TargetId.Value, name, type, rateLimitPerUser, nsfw, bitrate, overwrites.ToReadOnlyCollection());
return new ChannelCreateAuditLogData(id, name, type, rateLimitPerUser, nsfw, bitrate, overwrites.ToReadOnlyCollection());
}

/// <summary>
@@ -78,7 +71,7 @@ namespace Discord.Rest
/// Gets the current slow-mode delay of the created channel.
/// </summary>
/// <returns>
/// An <see cref="Int32"/> representing the time in seconds required before the user can send another
/// An <see cref="int"/> representing the time in seconds required before the user can send another
/// message; <c>0</c> if disabled.
/// <c>null</c> if this is not mentioned in this entry.
/// </returns>
@@ -95,7 +88,7 @@ namespace Discord.Rest
/// Gets the bit-rate that the clients in the created voice channel are requested to use.
/// </summary>
/// <returns>
/// An <see cref="Int32"/> representing the bit-rate (bps) that the created voice channel defines and requests the
/// An <see cref="int"/> representing the bit-rate (bps) that the created voice channel defines and requests the
/// client(s) to use.
/// <c>null</c> if this is not mentioned in this entry.
/// </returns>


+ 2
- 2
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs View File

@@ -71,7 +71,7 @@ namespace Discord.Rest
/// Gets the slow-mode delay of the deleted channel.
/// </summary>
/// <returns>
/// An <see cref="Int32"/> representing the time in seconds required before the user can send another
/// An <see cref="int"/> representing the time in seconds required before the user can send another
/// message; <c>0</c> if disabled.
/// <c>null</c> if this is not mentioned in this entry.
/// </returns>
@@ -88,7 +88,7 @@ namespace Discord.Rest
/// Gets the bit-rate of this channel if applicable.
/// </summary>
/// <returns>
/// An <see cref="Int32"/> representing the bit-rate set of the voice channel.
/// An <see cref="int"/> representing the bit-rate set of the voice channel.
/// <c>null</c> if this is not mentioned in this entry.
/// </returns>
public int? Bitrate { get; }


+ 2
- 2
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs View File

@@ -32,7 +32,7 @@ namespace Discord.Rest
/// Gets the current slow-mode delay of this channel.
/// </summary>
/// <returns>
/// An <see cref="Int32"/> representing the time in seconds required before the user can send another
/// An <see cref="int"/> representing the time in seconds required before the user can send another
/// message; <c>0</c> if disabled.
/// <c>null</c> if this is not mentioned in this entry.
/// </returns>
@@ -49,7 +49,7 @@ namespace Discord.Rest
/// Gets the bit-rate of this channel if applicable.
/// </summary>
/// <returns>
/// An <see cref="Int32"/> representing the bit-rate set for the voice channel;
/// An <see cref="int"/> representing the bit-rate set for the voice channel;
/// <c>null</c> if this is not mentioned in this entry.
/// </returns>
public int? Bitrate { get; }


+ 29
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberDisconnectAuditLogData.cs View File

@@ -0,0 +1,29 @@
using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
{
/// <summary>
/// Contains a piece of audit log data related to disconnecting members from voice channels.
/// </summary>
public class MemberDisconnectAuditLogData : IAuditLogData
{
private MemberDisconnectAuditLogData(int count)
{
MemberCount = count;
}

internal static MemberDisconnectAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
return new MemberDisconnectAuditLogData(entry.Options.Count.Value);
}

/// <summary>
/// Gets the number of members that were disconnected.
/// </summary>
/// <returns>
/// An <see cref="int"/> representing the number of members that were disconnected from a voice channel.
/// </returns>
public int MemberCount { get; }
}
}

+ 37
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberMoveAuditLogData.cs View File

@@ -0,0 +1,37 @@
using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
{
/// <summary>
/// Contains a piece of audit log data related to moving members between voice channels.
/// </summary>
public class MemberMoveAuditLogData : IAuditLogData
{
private MemberMoveAuditLogData(ulong channelId, int count)
{
ChannelId = channelId;
MemberCount = count;
}

internal static MemberMoveAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
return new MemberMoveAuditLogData(entry.Options.ChannelId.Value, entry.Options.Count.Value);
}

/// <summary>
/// Gets the ID of the channel that the members were moved to.
/// </summary>
/// <returns>
/// A <see cref="ulong"/> representing the snowflake identifier for the channel that the members were moved to.
/// </returns>
public ulong ChannelId { get; }
/// <summary>
/// Gets the number of members that were moved.
/// </summary>
/// <returns>
/// An <see cref="int"/> representing the number of members that were moved to another voice channel.
/// </returns>
public int MemberCount { get; }
}
}

+ 38
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageBulkDeleteAuditLogData.cs View File

@@ -0,0 +1,38 @@
using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
{
/// <summary>
/// Contains a piece of audit log data related to message deletion(s).
/// </summary>
public class MessageBulkDeleteAuditLogData : IAuditLogData
{
private MessageBulkDeleteAuditLogData(ulong channelId, int count)
{
ChannelId = channelId;
MessageCount = count;
}

internal static MessageBulkDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
return new MessageBulkDeleteAuditLogData(entry.TargetId.Value, entry.Options.Count.Value);
}

/// <summary>
/// Gets the ID of the channel that the messages were deleted from.
/// </summary>
/// <returns>
/// A <see cref="ulong"/> representing the snowflake identifier for the channel that the messages were
/// deleted from.
/// </returns>
public ulong ChannelId { get; }
/// <summary>
/// Gets the number of messages that were deleted.
/// </summary>
/// <returns>
/// An <see cref="int"/> representing the number of messages that were deleted from the channel.
/// </returns>
public int MessageCount { get; }
}
}

+ 9
- 6
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs View File

@@ -1,3 +1,5 @@
using System.Linq;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

@@ -8,16 +10,17 @@ namespace Discord.Rest
/// </summary>
public class MessageDeleteAuditLogData : IAuditLogData
{
private MessageDeleteAuditLogData(ulong channelId, int count, ulong authorId)
private MessageDeleteAuditLogData(ulong channelId, int count, IUser user)
{
ChannelId = channelId;
MessageCount = count;
AuthorId = authorId;
Target = user;
}

internal static MessageDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
return new MessageDeleteAuditLogData(entry.Options.MessageDeleteChannelId.Value, entry.Options.MessageDeleteCount.Value, entry.TargetId.Value);
var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId);
return new MessageDeleteAuditLogData(entry.Options.ChannelId.Value, entry.Options.Count.Value, RestUser.Create(discord, userInfo));
}

/// <summary>
@@ -36,11 +39,11 @@ namespace Discord.Rest
/// </returns>
public ulong ChannelId { get; }
/// <summary>
/// Gets the author of the messages that were deleted.
/// Gets the user of the messages that were deleted.
/// </summary>
/// <returns>
/// A <see cref="ulong"/> representing the snowflake identifier for the user that created the deleted messages.
/// A user object representing the user that created the deleted messages.
/// </returns>
public ulong AuthorId { get; }
public IUser Target { get; }
}
}

+ 48
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs View File

@@ -0,0 +1,48 @@
using System.Linq;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
{
/// <summary>
/// Contains a piece of audit log data related to a pinned message.
/// </summary>
public class MessagePinAuditLogData : IAuditLogData
{
private MessagePinAuditLogData(ulong messageId, ulong channelId, IUser user)
{
MessageId = messageId;
ChannelId = channelId;
Target = user;
}

internal static MessagePinAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId);
return new MessagePinAuditLogData(entry.Options.MessageId.Value, entry.Options.ChannelId.Value, RestUser.Create(discord, userInfo));
}

/// <summary>
/// Gets the ID of the messages that was pinned.
/// </summary>
/// <returns>
/// A <see cref="ulong"/> representing the snowflake identifier for the messages that was pinned.
/// </returns>
public ulong MessageId { get; }
/// <summary>
/// Gets the ID of the channel that the message was pinned from.
/// </summary>
/// <returns>
/// A <see cref="ulong"/> representing the snowflake identifier for the channel that the message was pinned from.
/// </returns>
public ulong ChannelId { get; }
/// <summary>
/// Gets the user of the message that was pinned.
/// </summary>
/// <returns>
/// A user object representing the user that created the pinned message.
/// </returns>
public IUser Target { get; }
}
}

+ 48
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs View File

@@ -0,0 +1,48 @@
using System.Linq;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
{
/// <summary>
/// Contains a piece of audit log data related to an unpinned message.
/// </summary>
public class MessageUnpinAuditLogData : IAuditLogData
{
private MessageUnpinAuditLogData(ulong messageId, ulong channelId, IUser user)
{
MessageId = messageId;
ChannelId = channelId;
Target = user;
}

internal static MessageUnpinAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId);
return new MessageUnpinAuditLogData(entry.Options.MessageId.Value, entry.Options.ChannelId.Value, RestUser.Create(discord, userInfo));
}

/// <summary>
/// Gets the ID of the messages that was unpinned.
/// </summary>
/// <returns>
/// A <see cref="ulong"/> representing the snowflake identifier for the messages that was unpinned.
/// </returns>
public ulong MessageId { get; }
/// <summary>
/// Gets the ID of the channel that the message was unpinned from.
/// </summary>
/// <returns>
/// A <see cref="ulong"/> representing the snowflake identifier for the channel that the message was unpinned from.
/// </returns>
public ulong ChannelId { get; }
/// <summary>
/// Gets the user of the message that was unpinned.
/// </summary>
/// <returns>
/// A user object representing the user that created the unpinned message.
/// </returns>
public IUser Target { get; }
}
}

+ 6
- 5
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs View File

@@ -21,16 +21,17 @@ namespace Discord.Rest
var changes = entry.Changes;

var denyModel = changes.FirstOrDefault(x => x.ChangedProperty == "deny");
var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type");
var idModel = changes.FirstOrDefault(x => x.ChangedProperty == "id");
var allowModel = changes.FirstOrDefault(x => x.ChangedProperty == "allow");

var deny = denyModel.OldValue.ToObject<ulong>(discord.ApiClient.Serializer);
var type = typeModel.OldValue.ToObject<PermissionTarget>(discord.ApiClient.Serializer);
var id = idModel.OldValue.ToObject<ulong>(discord.ApiClient.Serializer);
var allow = allowModel.OldValue.ToObject<ulong>(discord.ApiClient.Serializer);

return new OverwriteDeleteAuditLogData(entry.TargetId.Value, new Overwrite(id, type, new OverwritePermissions(allow, deny)));
var permissions = new OverwritePermissions(allow, deny);

var id = entry.Options.OverwriteTargetId.Value;
var type = entry.Options.OverwriteType;

return new OverwriteDeleteAuditLogData(entry.TargetId.Value, new Overwrite(id, type, permissions));
}

/// <summary>


+ 21
- 2
src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs View File

@@ -167,9 +167,28 @@ namespace Discord.Rest

/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public static async Task<RestUserMessage> SendMessageAsync(IMessageChannel channel, BaseDiscordClient client,
string text, bool isTTS, Embed embed, RequestOptions options)
string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, RequestOptions options)
{
var args = new CreateMessageParams(text) { IsTTS = isTTS, Embed = embed?.ToModel() };
Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed.");
Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed.");

// check that user flag and user Id list are exclusive, same with role flag and role Id list
if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue)
{
if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) &&
allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0)
{
throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions));
}

if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) &&
allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0)
{
throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions));
}
}

var args = new CreateMessageParams(text) { IsTTS = isTTS, Embed = embed?.ToModel(), AllowedMentions = allowedMentions?.ToModel() };
var model = await client.ApiClient.CreateMessageAsync(channel.Id, args, options).ConfigureAwait(false);
return RestUserMessage.Create(client, channel, client.CurrentUser, model);
}


+ 5
- 1
src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs View File

@@ -20,11 +20,15 @@ namespace Discord.Rest
/// <param name="isTTS">Determines whether the message should be read aloud by Discord or not.</param>
/// <param name="embed">The <see cref="Discord.EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <param name="allowedMentions">
/// Specifies if notifications are sent for mentioned users and roles in the message <paramref name="text"/>.
/// If <c>null</c>, all mentioned roles and users will be notified.
/// </param>
/// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
new Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null);
new Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null);
/// <summary>
/// Sends a file to this message channel with an optional caption.
/// </summary>


+ 4
- 4
src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs View File

@@ -93,8 +93,8 @@ namespace Discord.Rest

/// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
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> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options);

/// <inheritdoc />
/// <exception cref="ArgumentException">
@@ -206,8 +206,8 @@ namespace Discord.Rest
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler)
=> await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false);
/// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options)
=> await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false);
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions)
=> await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false);

//IChannel
/// <inheritdoc />


+ 5
- 4
src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs View File

@@ -95,8 +95,8 @@ namespace Discord.Rest

/// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
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> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options);

/// <inheritdoc />
/// <exception cref="ArgumentException">
@@ -183,8 +183,9 @@ namespace Discord.Rest

async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler)
=> await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false);
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options)
=> await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false);

async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions)
=> await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false);

//IAudioChannel
/// <inheritdoc />


+ 0
- 24
src/Discord.Net.Rest/Entities/Channels/RestNewsChannel.cs View File

@@ -25,29 +25,5 @@ namespace Discord.Rest
return entity;
}
public override int SlowModeInterval => throw new NotSupportedException("News channels do not support Slow Mode.");
public override Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null)
{
throw new NotSupportedException("News channels do not support Overwrite Permissions.");
}
public override Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null)
{
throw new NotSupportedException("News channels do not support Overwrite Permissions.");
}
public override OverwritePermissions? GetPermissionOverwrite(IRole role)
{
throw new NotSupportedException("News channels do not support Overwrite Permissions.");
}
public override OverwritePermissions? GetPermissionOverwrite(IUser user)
{
throw new NotSupportedException("News channels do not support Overwrite Permissions.");
}
public override Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null)
{
throw new NotSupportedException("News channels do not support Overwrite Permissions.");
}
public override Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null)
{
throw new NotSupportedException("News channels do not support Overwrite Permissions.");
}
}
}

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

@@ -101,8 +101,8 @@ namespace Discord.Rest

/// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
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> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options);

/// <inheritdoc />
/// <exception cref="ArgumentException">
@@ -273,8 +273,8 @@ namespace Discord.Rest
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler)
=> await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false);
/// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options)
=> await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false);
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions)
=> await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false);

//IGuildChannel
/// <inheritdoc />


+ 7
- 3
src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs View File

@@ -32,6 +32,7 @@ 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?>(),
Banner = args.Banner.IsSpecified ? args.Banner.Value?.ToModel() : Optional.Create<ImageModel?>(),
VerificationLevel = args.VerificationLevel,
ExplicitContentFilter = args.ExplicitContentFilter,
SystemChannelFlags = args.SystemChannelFlags
@@ -57,6 +58,8 @@ namespace Discord.Rest
else if (args.RegionId.IsSpecified)
apiArgs.RegionId = args.RegionId.Value;

if (!apiArgs.Banner.IsSpecified && guild.BannerId != null)
apiArgs.Banner = new ImageModel(guild.BannerId);
if (!apiArgs.Splash.IsSpecified && guild.SplashId != null)
apiArgs.Splash = new ImageModel(guild.SplashId);
if (!apiArgs.Icon.IsSpecified && guild.IconId != null)
@@ -257,7 +260,7 @@ namespace Discord.Rest
//Roles
/// <exception cref="ArgumentNullException"><paramref name="name"/> is <c>null</c>.</exception>
public static async Task<RestRole> CreateRoleAsync(IGuild guild, BaseDiscordClient client,
string name, GuildPermissions? permissions, Color? color, bool isHoisted, RequestOptions options)
string name, GuildPermissions? permissions, Color? color, bool isHoisted, bool isMentionable, RequestOptions options)
{
if (name == null) throw new ArgumentNullException(paramName: nameof(name));

@@ -270,6 +273,7 @@ namespace Discord.Rest
x.Permissions = (permissions ?? role.Permissions);
x.Color = (color ?? Color.Default);
x.Hoist = isHoisted;
x.Mentionable = isMentionable;
}, options).ConfigureAwait(false);

return role;
@@ -349,7 +353,7 @@ namespace Discord.Rest
ulong? fromUserId, int? limit, RequestOptions options)
{
return new PagedAsyncEnumerable<RestGuildUser>(
DiscordConfig.MaxMessagesPerBatch,
DiscordConfig.MaxUsersPerBatch,
async (info, ct) =>
{
var args = new GetGuildMembersParams
@@ -363,7 +367,7 @@ namespace Discord.Rest
},
nextPage: (info, lastPage) =>
{
if (lastPage.Count != DiscordConfig.MaxMessagesPerBatch)
if (lastPage.Count != DiscordConfig.MaxUsersPerBatch)
return false;
info.Position = lastPage.Max(x => x.Id);
return true;


+ 12
- 3
src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs View File

@@ -530,6 +530,11 @@ namespace Discord.Rest
return null;
}

/// <inheritdoc />
public Task<RestRole> CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?),
bool isHoisted = false, RequestOptions options = null)
=> CreateRoleAsync(name, permissions, color, isHoisted, false, options);

/// <summary>
/// Creates a new role with the provided name.
/// </summary>
@@ -538,14 +543,15 @@ namespace Discord.Rest
/// <param name="color">The color of the role.</param>
/// <param name="isHoisted">Whether the role is separated from others on the sidebar.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <param name="isMentionable">Whether the role can be mentioned.</param>
/// <returns>
/// A task that represents the asynchronous creation operation. The task result contains the newly created
/// role.
/// </returns>
public async Task<RestRole> CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?),
bool isHoisted = false, RequestOptions options = null)
bool isHoisted = false, bool isMentionable = false, RequestOptions options = null)
{
var role = await GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, options).ConfigureAwait(false);
var role = await GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, isMentionable, options).ConfigureAwait(false);
_roles = _roles.Add(role.Id, role);
return role;
}
@@ -833,7 +839,10 @@ namespace Discord.Rest
=> GetRole(id);
/// <inheritdoc />
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, false, options).ConfigureAwait(false);
/// <inheritdoc />
async Task<IRole> IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted, bool isMentionable, RequestOptions options)
=> await CreateRoleAsync(name, permissions, color, isHoisted, isMentionable, options).ConfigureAwait(false);

/// <inheritdoc />
async Task<IGuildUser> IGuild.AddGuildUserAsync(ulong userId, string accessToken, Action<AddGuildUserProperties> func, RequestOptions options)


+ 15
- 0
src/Discord.Net.Rest/Entities/Messages/AllowedMentions.cs View File

@@ -0,0 +1,15 @@
using Newtonsoft.Json;

namespace Discord.API
{
public class AllowedMentions
{
[JsonProperty("parse")]
public Optional<string[]> Parse { get; set; }
// Roles and Users have a max size of 100
[JsonProperty("roles")]
public Optional<ulong[]> Roles { get; set; }
[JsonProperty("users")]
public Optional<ulong[]> Users { get; set; }
}
}

+ 5
- 0
src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs View File

@@ -32,6 +32,11 @@ namespace Discord.Rest
var args = new MessageProperties();
func(args);

bool hasText = args.Content.IsSpecified ? !string.IsNullOrEmpty(args.Content.Value) : !string.IsNullOrEmpty(msg.Content);
bool hasEmbed = args.Embed.IsSpecified ? args.Embed.Value != null : msg.Embeds.Any();
if (!hasText && !hasEmbed)
Preconditions.NotNullOrEmpty(args.Content.IsSpecified ? args.Content.Value : string.Empty, nameof(args.Content));

var apiArgs = new API.Rest.ModifyMessageParams
{
Content = args.Content,


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

@@ -62,6 +62,8 @@ namespace Discord.Rest
public MessageActivity Activity { get; private set; }
/// <inheritdoc />
public MessageApplication Application { get; private set; }
/// <inheritdoc />
public MessageReference Reference { get; private set; }

internal RestMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author, MessageSource source)
: base(discord, id)
@@ -108,6 +110,17 @@ namespace Discord.Rest
};
}

if(model.Reference.IsSpecified)
{
// Creates a new Reference from the API model
Reference = new MessageReference
{
GuildId = model.Reference.Value.GuildId,
ChannelId = model.Reference.Value.ChannelId,
MessageId = model.Reference.Value.MessageId
};
}

if (model.Reactions.IsSpecified)
{
var value = model.Reactions.Value;


+ 26
- 0
src/Discord.Net.Rest/Extensions/EntityExtensions.cs View File

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

@@ -5,6 +6,13 @@ namespace Discord.Rest
{
internal static class EntityExtensions
{
public static IEmote ToIEmote(this API.Emoji model)
{
if (model.Id.HasValue)
return model.ToEntity();
return new Emoji(model.Name);
}

public static GuildEmote ToEntity(this API.Emoji model)
=> new GuildEmote(model.Id.Value,
model.Name,
@@ -53,6 +61,24 @@ namespace Discord.Rest
model.Video = entity.Video.Value.ToModel();
return model;
}
public static API.AllowedMentions ToModel(this AllowedMentions entity)
{
return new API.AllowedMentions()
{
Parse = entity.AllowedTypes?.EnumerateMentionTypes().ToArray(),
Roles = entity.RoleIds?.ToArray(),
Users = entity.UserIds?.ToArray(),
};
}
public static IEnumerable<string> EnumerateMentionTypes(this AllowedMentionTypes mentionTypes)
{
if (mentionTypes.HasFlag(AllowedMentionTypes.Everyone))
yield return "everyone";
if (mentionTypes.HasFlag(AllowedMentionTypes.Roles))
yield return "roles";
if (mentionTypes.HasFlag(AllowedMentionTypes.Users))
yield return "users";
}
public static EmbedAuthor ToEntity(this API.EmbedAuthor model)
{
return new EmbedAuthor(model.Name, model.Url, model.IconUrl, model.ProxyIconUrl);


+ 23
- 0
src/Discord.Net.WebSocket/ClientState.cs View File

@@ -82,6 +82,20 @@ namespace Discord.WebSocket
}
return null;
}
internal void PurgeAllChannels()
{
foreach (var guild in _guilds.Values)
guild.PurgeChannelCache(this);

PurgeDMChannels();
}
internal void PurgeDMChannels()
{
foreach (var channel in _dmChannels.Values)
_channels.TryRemove(channel.Id, out _);

_dmChannels.Clear();
}

internal SocketGuild GetGuild(ulong id)
{
@@ -96,7 +110,11 @@ namespace Discord.WebSocket
internal SocketGuild RemoveGuild(ulong id)
{
if (_guilds.TryRemove(id, out SocketGuild guild))
{
guild.PurgeChannelCache(this);
guild.PurgeGuildUserCache();
return guild;
}
return null;
}

@@ -116,5 +134,10 @@ namespace Discord.WebSocket
return user;
return null;
}
internal void PurgeUsers()
{
foreach (var guild in _guilds.Values)
guild.PurgeGuildUserCache();
}
}
}

+ 5
- 2
src/Discord.Net.WebSocket/DiscordShardedClient.cs View File

@@ -389,8 +389,11 @@ namespace Discord.WebSocket
{
if (disposing)
{
foreach (var client in _shards)
client?.Dispose();
if (_shards != null)
{
foreach (var client in _shards)
client?.Dispose();
}
_connectionGroupLock?.Dispose();
}



+ 6
- 12
src/Discord.Net.WebSocket/DiscordSocketApiClient.cs View File

@@ -164,26 +164,17 @@ namespace Discord.API
}
}

public async Task DisconnectAsync()
public async Task DisconnectAsync(Exception ex = null)
{
await _stateLock.WaitAsync().ConfigureAwait(false);
try
{
await DisconnectInternalAsync().ConfigureAwait(false);
}
finally { _stateLock.Release(); }
}
public async Task DisconnectAsync(Exception ex)
{
await _stateLock.WaitAsync().ConfigureAwait(false);
try
{
await DisconnectInternalAsync().ConfigureAwait(false);
await DisconnectInternalAsync(ex).ConfigureAwait(false);
}
finally { _stateLock.Release(); }
}
/// <exception cref="NotSupportedException">This client is not configured with WebSocket support.</exception>
internal override async Task DisconnectInternalAsync()
internal override async Task DisconnectInternalAsync(Exception ex = null)
{
if (WebSocketClient == null)
throw new NotSupportedException("This client is not configured with WebSocket support.");
@@ -194,6 +185,9 @@ namespace Discord.API
try { _connectCancelToken?.Cancel(false); }
catch { }

if (ex is GatewayReconnectException)
await WebSocketClient.DisconnectAsync(4000);
else
await WebSocketClient.DisconnectAsync().ConfigureAwait(false);

ConnectionState = ConnectionState.Disconnected;


+ 16
- 3
src/Discord.Net.WebSocket/DiscordSocketClient.cs View File

@@ -264,7 +264,7 @@ namespace Discord.WebSocket
{

await _gatewayLogger.DebugAsync("Disconnecting ApiClient").ConfigureAwait(false);
await ApiClient.DisconnectAsync().ConfigureAwait(false);
await ApiClient.DisconnectAsync(ex).ConfigureAwait(false);

//Wait for tasks to complete
await _gatewayLogger.DebugAsync("Waiting for heartbeater").ConfigureAwait(false);
@@ -306,6 +306,14 @@ namespace Discord.WebSocket
/// <inheritdoc />
public override SocketChannel GetChannel(ulong id)
=> State.GetChannel(id);
/// <summary>
/// Clears all cached channels from the client.
/// </summary>
public void PurgeChannelCache() => State.PurgeAllChannels();
/// <summary>
/// Clears cached DM channels from the client.
/// </summary>
public void PurgeDMChannelCache() => State.PurgeDMChannels();

/// <inheritdoc />
public override SocketUser GetUser(ulong id)
@@ -313,6 +321,10 @@ namespace Discord.WebSocket
/// <inheritdoc />
public override SocketUser GetUser(string username, string discriminator)
=> State.Users.FirstOrDefault(x => x.Discriminator == discriminator && x.Username == username);
/// <summary>
/// Clears cached users from the client.
/// </summary>
public void PurgeUserCache() => State.PurgeUsers();
internal SocketGlobalUser GetOrCreateUser(ClientState state, Discord.API.User model)
{
return state.GetOrAddUser(model.Id, x =>
@@ -511,7 +523,7 @@ namespace Discord.WebSocket
case GatewayOpCode.Reconnect:
{
await _gatewayLogger.DebugAsync("Received Reconnect").ConfigureAwait(false);
_connection.Error(new Exception("Server requested a reconnect"));
_connection.Error(new GatewayReconnectException("Server requested a reconnect"));
}
break;
case GatewayOpCode.Dispatch:
@@ -628,6 +640,7 @@ namespace Discord.WebSocket
if (guild != null)
{
await TimedInvokeAsync(_joinedGuildEvent, nameof(JoinedGuild), guild).ConfigureAwait(false);
await GuildAvailableAsync(guild).ConfigureAwait(false);
}
else
{
@@ -1689,7 +1702,7 @@ namespace Discord.WebSocket
{
if (ConnectionState == ConnectionState.Connected && (_guildDownloadTask?.IsCompleted ?? true))
{
_connection.Error(new Exception("Server missed last heartbeat"));
_connection.Error(new GatewayReconnectException("Server missed last heartbeat"));
return;
}
}


+ 5
- 1
src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs View File

@@ -29,11 +29,15 @@ namespace Discord.WebSocket
/// <param name="isTTS">Determines whether the message should be read aloud by Discord or not.</param>
/// <param name="embed">The <see cref="Discord.EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <param name="allowedMentions">
/// Specifies if notifications are sent for mentioned users and roles in the message <paramref name="text"/>.
/// If <c>null</c>, all mentioned roles and users will be notified.
/// </param>
/// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
new Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null);
new Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null);
/// <summary>
/// Sends a file to this message channel with an optional caption.
/// </summary>


+ 4
- 4
src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs View File

@@ -135,8 +135,8 @@ namespace Discord.WebSocket

/// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
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> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options);

/// <inheritdoc />
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false)
@@ -235,8 +235,8 @@ namespace Discord.WebSocket
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler)
=> await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false);
/// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options)
=> await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false);
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions)
=> await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false);

//IChannel
/// <inheritdoc />


+ 4
- 4
src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs View File

@@ -163,8 +163,8 @@ namespace Discord.WebSocket

/// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
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> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options);

/// <inheritdoc />
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false)
@@ -299,8 +299,8 @@ namespace Discord.WebSocket
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler)
=> await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false);
/// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options)
=> await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false);
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions)
=> await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false);

//IAudioChannel
/// <inheritdoc />


+ 1
- 49
src/Discord.Net.WebSocket/Entities/Channels/SocketNewsChannel.cs View File

@@ -11,7 +11,7 @@ namespace Discord.WebSocket
/// </summary>
/// <remarks>
/// <note type="warning">
/// Most of the properties and methods featured may not be supported due to the nature of the channel.
/// The <see cref="SlowModeInterval"/> property is not supported for news channels.
/// </note>
/// </remarks>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
@@ -35,53 +35,5 @@ namespace Discord.WebSocket
/// </remarks>
public override int SlowModeInterval
=> throw new NotSupportedException("News channels do not support Slow Mode.");
/// <inheritdoc />
/// <remarks>
/// <note type="important">
/// This method is not supported by this type. Attempting to use this method will result in a <see cref="NotSupportedException"/>.
/// </note>
/// </remarks>
public override Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null)
=> throw new NotSupportedException("News channels do not support Overwrite Permissions.");
/// <inheritdoc />
/// <remarks>
/// <note type="important">
/// This method is not supported by this type. Attempting to use this method will result in a <see cref="NotSupportedException"/>.
/// </note>
/// </remarks>
public override Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null)
=> throw new NotSupportedException("News channels do not support Overwrite Permissions.");
/// <inheritdoc />
/// <remarks>
/// <note type="important">
/// This property is not supported by this type. Attempting to use this property will result in a <see cref="NotSupportedException"/>.
/// </note>
/// </remarks>
public override IReadOnlyCollection<Overwrite> PermissionOverwrites
=> throw new NotSupportedException("News channels do not support Overwrite Permissions.");
/// <inheritdoc />
/// <remarks>
/// <note type="important">
/// This method is not supported by this type. Attempting to use this method will result in a <see cref="NotSupportedException"/>.
/// </note>
/// </remarks>
public override Task SyncPermissionsAsync(RequestOptions options = null)
=> throw new NotSupportedException("News channels do not support Overwrite Permissions.");
/// <inheritdoc />
/// <remarks>
/// <note type="important">
/// This method is not supported by this type. Attempting to use this method will result in a <see cref="NotSupportedException"/>.
/// </note>
/// </remarks>
public override Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null)
=> throw new NotSupportedException("News channels do not support Overwrite Permissions.");
/// <inheritdoc />
/// <remarks>
/// <note type="important">
/// This method is not supported by this type. Attempting to use this method will result in a <see cref="NotSupportedException"/>.
/// </note>
/// </remarks>
public override Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null)
=> throw new NotSupportedException("News channels do not support Overwrite Permissions.");
}
}

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

@@ -161,8 +161,8 @@ namespace Discord.WebSocket

/// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
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> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options);

/// <inheritdoc />
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false)
@@ -308,8 +308,8 @@ namespace Discord.WebSocket
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler)
=> await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false);
/// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options)
=> await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false);
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions)
=> await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false);

// INestedChannel
/// <inheritdoc />


+ 33
- 3
src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs View File

@@ -623,6 +623,13 @@ namespace Discord.WebSocket
return state.RemoveChannel(id) as SocketGuildChannel;
return null;
}
internal void PurgeChannelCache(ClientState state)
{
foreach (var channelId in _channels)
state.RemoveChannel(channelId);

_channels.Clear();
}

//Voice Regions
/// <summary>
@@ -679,6 +686,10 @@ namespace Discord.WebSocket
return null;
}

/// <inheritdoc />
public Task<RestRole> CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?),
bool isHoisted = false, RequestOptions options = null)
=> GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, false, options);
/// <summary>
/// Creates a new role with the provided name.
/// </summary>
@@ -686,6 +697,7 @@ namespace Discord.WebSocket
/// <param name="permissions">The guild permission that the role should possess.</param>
/// <param name="color">The color of the role.</param>
/// <param name="isHoisted">Whether the role is separated from others on the sidebar.</param>
/// <param name="isMentionable">Whether the role can be mentioned.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <exception cref="ArgumentNullException"><paramref name="name"/> is <c>null</c>.</exception>
/// <returns>
@@ -693,8 +705,8 @@ namespace Discord.WebSocket
/// role.
/// </returns>
public Task<RestRole> CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?),
bool isHoisted = false, RequestOptions options = null)
=> GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, options);
bool isHoisted = false, bool isMentionable = false, RequestOptions options = null)
=> GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, isMentionable, options);
internal SocketRole AddRole(RoleModel model)
{
var role = SocketRole.Create(this, Discord.State, model);
@@ -792,6 +804,21 @@ namespace Discord.WebSocket
}
return null;
}
internal void PurgeGuildUserCache()
{
var members = Users;
var self = CurrentUser;
_members.Clear();
_members.TryAdd(self.Id, self);

DownloadedMemberCount = _members.Count;

foreach (var member in members)
{
if (member.Id != self.Id)
member.GlobalUser.RemoveRef(Discord);
}
}

/// <inheritdoc />
public async Task DownloadUsersAsync()
@@ -1151,7 +1178,10 @@ namespace Discord.WebSocket
=> GetRole(id);
/// <inheritdoc />
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, false, options).ConfigureAwait(false);
/// <inheritdoc />
async Task<IRole> IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted, bool isMentionable, RequestOptions options)
=> await CreateRoleAsync(name, permissions, color, isHoisted, isMentionable, options).ConfigureAwait(false);

/// <inheritdoc />
Task<IReadOnlyCollection<IGuildUser>> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options)


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

@@ -53,6 +53,9 @@ namespace Discord.WebSocket
/// <inheritdoc />
public MessageApplication Application { get; private set; }

/// <inheritdoc />
public MessageReference Reference { get; private set; }

/// <summary>
/// Returns all attachments included in this message.
/// </summary>
@@ -140,6 +143,17 @@ namespace Discord.WebSocket
PartyId = model.Activity.Value.PartyId.Value
};
}

if (model.Reference.IsSpecified)
{
// Creates a new Reference from the API model
Reference = new MessageReference
{
GuildId = model.Reference.Value.GuildId,
ChannelId = model.Reference.Value.ChannelId,
MessageId = model.Reference.Value.MessageId
};
}
}

/// <inheritdoc />


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

@@ -40,7 +40,7 @@ namespace Discord.WebSocket
/// <inheritdoc />
public UserStatus Status => Presence.Status;
/// <inheritdoc />
public IImmutableSet<ClientType> ActiveClients => Presence.ActiveClients;
public IImmutableSet<ClientType> ActiveClients => Presence.ActiveClients ?? ImmutableHashSet<ClientType>.Empty;
/// <summary>
/// Gets mutual guilds shared with this user.
/// </summary>


+ 17
- 0
src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs View File

@@ -1,3 +1,5 @@
using Discord.Rest;
using System;
using System.Collections.Immutable;
using System.Linq;

@@ -7,6 +9,19 @@ namespace Discord.WebSocket
{
public static IActivity ToEntity(this API.Game model)
{
// Custom Status Game
if (model.Id.IsSpecified && model.Id.Value == "custom")
{
return new CustomStatusGame()
{
Type = ActivityType.CustomStatus,
Name = model.Name,
State = model.State.IsSpecified ? model.State.Value : null,
Emote = model.Emoji.IsSpecified ? model.Emoji.Value.ToIEmote() : null,
CreatedAt = DateTimeOffset.FromUnixTimeMilliseconds(model.CreatedAt.Value),
};
}

// Spotify Game
if (model.SyncId.IsSpecified)
{
@@ -23,6 +38,8 @@ namespace Discord.WebSocket
AlbumTitle = albumText,
TrackTitle = model.Details.GetValueOrDefault(),
Artists = model.State.GetValueOrDefault()?.Split(';').Select(x=>x?.Trim()).ToImmutableArray(),
StartedAt = timestamps?.Start,
EndsAt = timestamps?.End,
Duration = timestamps?.End - timestamps?.Start,
AlbumArtUrl = albumArtId != null ? CDN.GetSpotifyAlbumArtUrl(albumArtId) : null,
Type = ActivityType.Listening,


+ 22
- 0
src/Discord.Net.WebSocket/GatewayReconnectException.cs View File

@@ -0,0 +1,22 @@
using System;

namespace Discord.WebSocket
{
/// <summary>
/// An exception thrown when the gateway client has been requested to
/// reconnect.
/// </summary>
public class GatewayReconnectException : Exception
{
/// <summary>
/// Creates a new instance of the
/// <see cref="GatewayReconnectException"/> type.
/// </summary>
/// <param name="message">
/// The reason why the gateway has been requested to reconnect.
/// </param>
public GatewayReconnectException(string message)
: base(message)
{ }
}
}

+ 7
- 6
src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs View File

@@ -44,7 +44,7 @@ namespace Discord.Net.WebSockets
{
if (disposing)
{
DisconnectInternalAsync(true).GetAwaiter().GetResult();
DisconnectInternalAsync(isDisposing: true).GetAwaiter().GetResult();
_disconnectTokenSource?.Dispose();
_cancelTokenSource?.Dispose();
_lock?.Dispose();
@@ -94,19 +94,19 @@ namespace Discord.Net.WebSockets
_task = RunAsync(_cancelToken);
}

public async Task DisconnectAsync()
public async Task DisconnectAsync(int closeCode = 1000)
{
await _lock.WaitAsync().ConfigureAwait(false);
try
{
await DisconnectInternalAsync().ConfigureAwait(false);
await DisconnectInternalAsync(closeCode: closeCode).ConfigureAwait(false);
}
finally
{
_lock.Release();
}
}
private async Task DisconnectInternalAsync(bool isDisposing = false)
private async Task DisconnectInternalAsync(int closeCode = 1000, bool isDisposing = false)
{
try { _disconnectTokenSource.Cancel(false); }
catch { }
@@ -117,7 +117,8 @@ namespace Discord.Net.WebSockets
{
if (!isDisposing)
{
try { await _client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", new CancellationToken()); }
var status = (WebSocketCloseStatus)closeCode;
try { await _client.CloseOutputAsync(status, "", new CancellationToken()); }
catch { }
}
try { _client.Dispose(); }
@@ -141,7 +142,7 @@ namespace Discord.Net.WebSockets
await _lock.WaitAsync().ConfigureAwait(false);
try
{
await DisconnectInternalAsync(false);
await DisconnectInternalAsync(isDisposing: false);
}
finally
{


+ 1
- 1
src/Discord.Net.Webhook/DiscordWebhookClient.cs View File

@@ -85,7 +85,7 @@ namespace Discord.Webhook
}
private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config)
=> new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent);
/// <summary> Sends a message using to the channel for this webhook. </summary>
/// <summary> Sends a message to the channel for this webhook. </summary>
/// <returns> Returns the ID of the created message. </returns>
public Task<ulong> SendMessageAsync(string text = null, bool isTTS = false, IEnumerable<Embed> embeds = null,
string username = null, string avatarUrl = null, RequestOptions options = null)


+ 17
- 17
src/Discord.Net/Discord.Net.nuspec View File

@@ -2,10 +2,10 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Discord.Net</id>
<version>2.2.0-dev$suffix$</version>
<version>2.3.0-dev$suffix$</version>
<title>Discord.Net</title>
<authors>Discord.Net Contributors</authors>
<owners>RogueException</owners>
<owners>foxbot</owners>
<description>An asynchronous API wrapper for Discord. This metapackage includes all of the optional Discord.Net components.</description>
<tags>discord;discordapp</tags>
<projectUrl>https://github.com/RogueException/Discord.Net</projectUrl>
@@ -14,25 +14,25 @@
<iconUrl>https://github.com/RogueException/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png</iconUrl>
<dependencies>
<group targetFramework="net461">
<dependency id="Discord.Net.Core" version="2.2.0-dev$suffix$" />
<dependency id="Discord.Net.Rest" version="2.2.0-dev$suffix$" />
<dependency id="Discord.Net.WebSocket" version="2.2.0-dev$suffix$" />
<dependency id="Discord.Net.Commands" version="2.2.0-dev$suffix$" />
<dependency id="Discord.Net.Webhook" version="2.2.0-dev$suffix$" />
<dependency id="Discord.Net.Core" version="2.3.0-dev$suffix$" />
<dependency id="Discord.Net.Rest" version="2.3.0-dev$suffix$" />
<dependency id="Discord.Net.WebSocket" version="2.3.0-dev$suffix$" />
<dependency id="Discord.Net.Commands" version="2.3.0-dev$suffix$" />
<dependency id="Discord.Net.Webhook" version="2.3.0-dev$suffix$" />
</group>
<group targetFramework="netstandard2.0">
<dependency id="Discord.Net.Core" version="2.2.0-dev$suffix$" />
<dependency id="Discord.Net.Rest" version="2.2.0-dev$suffix$" />
<dependency id="Discord.Net.WebSocket" version="2.2.0-dev$suffix$" />
<dependency id="Discord.Net.Commands" version="2.2.0-dev$suffix$" />
<dependency id="Discord.Net.Webhook" version="2.2.0-dev$suffix$" />
<dependency id="Discord.Net.Core" version="2.3.0-dev$suffix$" />
<dependency id="Discord.Net.Rest" version="2.3.0-dev$suffix$" />
<dependency id="Discord.Net.WebSocket" version="2.3.0-dev$suffix$" />
<dependency id="Discord.Net.Commands" version="2.3.0-dev$suffix$" />
<dependency id="Discord.Net.Webhook" version="2.3.0-dev$suffix$" />
</group>
<group targetFramework="netstandard2.1">
<dependency id="Discord.Net.Core" version="2.2.0-dev$suffix$" />
<dependency id="Discord.Net.Rest" version="2.2.0-dev$suffix$" />
<dependency id="Discord.Net.WebSocket" version="2.2.0-dev$suffix$" />
<dependency id="Discord.Net.Commands" version="2.2.0-dev$suffix$" />
<dependency id="Discord.Net.Webhook" version="2.2.0-dev$suffix$" />
<dependency id="Discord.Net.Core" version="2.3.0-dev$suffix$" />
<dependency id="Discord.Net.Rest" version="2.3.0-dev$suffix$" />
<dependency id="Discord.Net.WebSocket" version="2.3.0-dev$suffix$" />
<dependency id="Discord.Net.Commands" version="2.3.0-dev$suffix$" />
<dependency id="Discord.Net.Webhook" version="2.3.0-dev$suffix$" />
</group>
</dependencies>
</metadata>


+ 1
- 1
test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs View File

@@ -83,7 +83,7 @@ namespace Discord
throw new NotImplementedException();
}

public Task<IUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null)
public Task<IUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null)
{
throw new NotImplementedException();
}


+ 1
- 1
test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs View File

@@ -91,7 +91,7 @@ namespace Discord
throw new NotImplementedException();
}

public Task<IUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null)
public Task<IUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null)
{
throw new NotImplementedException();
}


+ 1
- 1
test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs View File

@@ -177,7 +177,7 @@ namespace Discord
throw new NotImplementedException();
}

public Task<IUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null)
public Task<IUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null)
{
throw new NotImplementedException();
}


Loading…
Cancel
Save