Browse Source

Merge branch 'dev' into patch-1

pull/1569/head
AraHaan GitHub 4 years ago
parent
commit
b906cff408
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
76 changed files with 841 additions and 307 deletions
  1. +2
    -2
      README.md
  2. +3
    -8
      azure-pipelines.yml
  3. +6
    -1
      azure/build.yml
  4. +3
    -3
      docs/_overwrites/Common/EmbedObjectBuilder.Inclusion.md
  5. +1
    -1
      docs/faq/basics/client-basics.md
  6. +2
    -2
      docs/faq/misc/glossary.md
  7. +5
    -5
      docs/guides/getting_started/first-bot.md
  8. +2
    -2
      docs/index.md
  9. +1
    -1
      samples/01_basic_ping_bot/01_basic_ping_bot.csproj
  10. +1
    -1
      samples/02_commands_framework/02_commands_framework.csproj
  11. +1
    -1
      samples/03_sharded_client/03_sharded_client.csproj
  12. +4
    -4
      samples/04_webhook_client/Program.cs
  13. +2
    -1
      src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs
  14. +2
    -2
      src/Discord.Net.Core/DiscordConfig.cs
  15. +7
    -1
      src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs
  16. +10
    -2
      src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs
  17. +7
    -0
      src/Discord.Net.Core/Entities/Messages/IMessage.cs
  18. +17
    -5
      src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs
  19. +1
    -1
      src/Discord.Net.Core/Entities/Users/IGuildUser.cs
  20. +4
    -0
      src/Discord.Net.Core/Entities/Users/IPresence.cs
  21. +1
    -1
      src/Discord.Net.Core/Extensions/MessageExtensions.cs
  22. +43
    -0
      src/Discord.Net.Core/GatewayIntents.cs
  23. +118
    -0
      src/Discord.Net.Core/Net/BucketId.cs
  24. +2
    -2
      src/Discord.Net.Core/Net/HttpException.cs
  25. +1
    -1
      src/Discord.Net.Core/Net/WebSocketClosedException.cs
  26. +2
    -1
      src/Discord.Net.Core/RequestOptions.cs
  27. +5
    -0
      src/Discord.Net.Rest/API/Common/Presence.cs
  28. +4
    -0
      src/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs
  29. +3
    -1
      src/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs
  30. +3
    -0
      src/Discord.Net.Rest/API/Rest/UploadFileParams.cs
  31. +3
    -0
      src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs
  32. +2
    -2
      src/Discord.Net.Rest/BaseDiscordClient.cs
  33. +58
    -37
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  34. +9
    -5
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs
  35. +9
    -5
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs
  36. +64
    -10
      src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs
  37. +14
    -4
      src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs
  38. +8
    -8
      src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs
  39. +8
    -8
      src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs
  40. +10
    -9
      src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs
  41. +32
    -4
      src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs
  42. +3
    -0
      src/Discord.Net.Rest/Entities/Messages/RestMessage.cs
  43. +11
    -6
      src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs
  44. +2
    -0
      src/Discord.Net.Rest/Entities/Users/RestUser.cs
  45. +7
    -7
      src/Discord.Net.Rest/Net/Queue/ClientBucket.cs
  46. +31
    -8
      src/Discord.Net.Rest/Net/Queue/RequestQueue.cs
  47. +57
    -17
      src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs
  48. +5
    -3
      src/Discord.Net.Rest/Net/RateLimitInfo.cs
  49. +3
    -1
      src/Discord.Net.WebSocket/API/Gateway/IdentifyParams.cs
  50. +12
    -1
      src/Discord.Net.WebSocket/ConnectionManager.cs
  51. +7
    -3
      src/Discord.Net.WebSocket/DiscordSocketApiClient.cs
  52. +7
    -1
      src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs
  53. +20
    -8
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  54. +37
    -1
      src/Discord.Net.WebSocket/DiscordSocketConfig.cs
  55. +12
    -4
      src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs
  56. +24
    -19
      src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs
  57. +8
    -8
      src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs
  58. +8
    -8
      src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs
  59. +0
    -20
      src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs
  60. +8
    -8
      src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs
  61. +1
    -1
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
  62. +13
    -1
      src/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs
  63. +2
    -0
      src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs
  64. +18
    -9
      src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs
  65. +2
    -0
      src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs
  66. +27
    -4
      src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs
  67. +1
    -1
      src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs
  68. +2
    -0
      src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs
  69. +1
    -1
      src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs
  70. +11
    -9
      src/Discord.Net.Webhook/DiscordWebhookClient.cs
  71. +8
    -4
      src/Discord.Net.Webhook/WebhookClientHelper.cs
  72. +2
    -2
      test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs
  73. +2
    -2
      test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs
  74. +2
    -2
      test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs
  75. +1
    -1
      test/Discord.Net.Tests.Unit/TokenUtilsTests.cs
  76. +6
    -6
      test/Discord.Net.Tests/Tests.DiscordWebhookClient.cs

+ 2
- 2
README.md View File

@@ -2,9 +2,9 @@
[![NuGet](https://img.shields.io/nuget/vpre/Discord.Net.svg?maxAge=2592000?style=plastic)](https://www.nuget.org/packages/Discord.Net) [![NuGet](https://img.shields.io/nuget/vpre/Discord.Net.svg?maxAge=2592000?style=plastic)](https://www.nuget.org/packages/Discord.Net)
[![MyGet](https://img.shields.io/myget/discord-net/vpre/Discord.Net.svg)](https://www.myget.org/feed/Packages/discord-net) [![MyGet](https://img.shields.io/myget/discord-net/vpre/Discord.Net.svg)](https://www.myget.org/feed/Packages/discord-net)
[![Build Status](https://dev.azure.com/discord-net/Discord.Net/_apis/build/status/discord-net.Discord.Net?branchName=dev)](https://dev.azure.com/discord-net/Discord.Net/_build/latest?definitionId=1&branchName=dev) [![Build Status](https://dev.azure.com/discord-net/Discord.Net/_apis/build/status/discord-net.Discord.Net?branchName=dev)](https://dev.azure.com/discord-net/Discord.Net/_build/latest?definitionId=1&branchName=dev)
[![Discord](https://discordapp.com/api/guilds/81384788765712384/widget.png)](https://discord.gg/jkrBmQR)
[![Discord](https://discord.com/api/guilds/81384788765712384/widget.png)](https://discord.gg/jkrBmQR)


An unofficial .NET API Wrapper for the Discord client (http://discordapp.com).
An unofficial .NET API Wrapper for the Discord client (https://discord.com).


Check out the [documentation](https://discord.foxbot.me/) or join the [Discord API Chat](https://discord.gg/jkrBmQR). Check out the [documentation](https://discord.foxbot.me/) or join the [Discord API Chat](https://discord.gg/jkrBmQR).




+ 3
- 8
azure-pipelines.yml View File

@@ -14,25 +14,20 @@ trigger:
jobs: jobs:
- job: Linux - job: Linux
pool: pool:
vmImage: 'ubuntu-16.04'
vmImage: 'ubuntu-latest'
steps: steps:
- task: UseDotNet@2
displayName: 'Use .NET Core sdk'
inputs:
packageType: 'sdk'
version: '3.x'
- template: azure/build.yml - template: azure/build.yml


- job: Windows_build - job: Windows_build
pool: pool:
vmImage: 'windows-2019'
vmImage: 'windows-latest'
condition: ne(variables['Build.SourceBranch'], 'refs/heads/dev') condition: ne(variables['Build.SourceBranch'], 'refs/heads/dev')
steps: steps:
- template: azure/build.yml - template: azure/build.yml


- job: Windows_deploy - job: Windows_deploy
pool: pool:
vmImage: 'windows-2019'
vmImage: 'windows-latest'
condition: | condition: |
and ( and (
succeeded(), succeeded(),


+ 6
- 1
azure/build.yml View File

@@ -1,5 +1,10 @@
steps: steps:
- script: dotnet restore --no-cache Discord.Net.sln
- task: DotNetCoreCLI@2
inputs:
command: 'restore'
projects: 'Discord.Net.sln'
feedsToUse: 'select'
verbosityRestore: 'Minimal'
displayName: Restore packages displayName: Restore packages


- script: dotnet build "Discord.Net.sln" --no-restore -v minimal -c $(buildConfiguration) /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag) - script: dotnet build "Discord.Net.sln" --no-restore -v minimal -c $(buildConfiguration) /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag)


+ 3
- 3
docs/_overwrites/Common/EmbedObjectBuilder.Inclusion.md View File

@@ -4,10 +4,10 @@ field, and 2 normal fields using an @Discord.EmbedBuilder:
```cs ```cs
var exampleAuthor = new EmbedAuthorBuilder() var exampleAuthor = new EmbedAuthorBuilder()
.WithName("I am a bot") .WithName("I am a bot")
.WithIconUrl("https://discordapp.com/assets/e05ead6e6ebc08df9291738d0aa6986d.png");
.WithIconUrl("https://discord.com/assets/e05ead6e6ebc08df9291738d0aa6986d.png");
var exampleFooter = new EmbedFooterBuilder() var exampleFooter = new EmbedFooterBuilder()
.WithText("I am a nice footer") .WithText("I am a nice footer")
.WithIconUrl("https://discordapp.com/assets/28174a34e77bb5e5310ced9f95cb480b.png");
.WithIconUrl("https://discord.com/assets/28174a34e77bb5e5310ced9f95cb480b.png");
var exampleField = new EmbedFieldBuilder() var exampleField = new EmbedFieldBuilder()
.WithName("Title of Another Field") .WithName("Title of Another Field")
.WithValue("I am an [example](https://example.com).") .WithValue("I am an [example](https://example.com).")
@@ -22,4 +22,4 @@ var embed = new EmbedBuilder()
.WithAuthor(exampleAuthor) .WithAuthor(exampleAuthor)
.WithFooter(exampleFooter) .WithFooter(exampleFooter)
.Build(); .Build();
```
```

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

@@ -30,7 +30,7 @@ There are few possible reasons why this may occur.
[TokenType]: xref:Discord.TokenType [TokenType]: xref:Discord.TokenType
[827]: https://github.com/RogueException/Discord.Net/issues/827 [827]: https://github.com/RogueException/Discord.Net/issues/827
[958]: https://github.com/RogueException/Discord.Net/issues/958 [958]: https://github.com/RogueException/Discord.Net/issues/958
[Discord API Terms of Service]: https://discordapp.com/developers/docs/legal
[Discord API Terms of Service]: https://discord.com/developers/docs/legal


## How do I do X, Y, Z when my bot connects/logs on? Why do I get a `NullReferenceException` upon calling any client methods after connect? ## How do I do X, Y, Z when my bot connects/logs on? Why do I get a `NullReferenceException` upon calling any client methods after connect?




+ 2
- 2
docs/faq/misc/glossary.md View File

@@ -19,7 +19,7 @@ channels, and are often referred to as "servers".
* A **Channel** ([IChannel]) represents a generic channel. * A **Channel** ([IChannel]) represents a generic channel.
- Example: #dotnet_discord-net - Example: #dotnet_discord-net
- See [Channel Types](#channel-types) - See [Channel Types](#channel-types)
[IGuild]: xref:Discord.IGuild [IGuild]: xref:Discord.IGuild
[IChannel]: xref:Discord.IChannel [IChannel]: xref:Discord.IChannel


@@ -79,4 +79,4 @@ activity for listening to a song on Spotify.
[RichGame]: xref:Discord.RichGame [RichGame]: xref:Discord.RichGame
[StreamingGame]: xref:Discord.StreamingGame [StreamingGame]: xref:Discord.StreamingGame
[SpotifyGame]: xref:Discord.SpotifyGame [SpotifyGame]: xref:Discord.SpotifyGame
[Rich Presence Intro]: https://discordapp.com/developers/docs/rich-presence/best-practices
[Rich Presence Intro]: https://discord.com/developers/docs/rich-presence/best-practices

+ 5
- 5
docs/guides/getting_started/first-bot.md View File

@@ -31,7 +31,7 @@ the Discord Applications Portal first.


![Step 7](images/intro-public-bot.png) ![Step 7](images/intro-public-bot.png)


[Discord Applications Portal]: https://discordapp.com/developers/applications/
[Discord Applications Portal]: https://discord.com/developers/applications/


## Adding your bot to a server ## Adding your bot to a server


@@ -165,11 +165,11 @@ or any other blocking method, such as reading from the console.
> the source code for your bot. > the source code for your bot.
> >
> In the following example, we retrieve the token from a pre-defined > In the following example, we retrieve the token from a pre-defined
> variable, which is **NOT** secure, especially if you plan on
> variable, which is **NOT** secure, especially if you plan on
> distributing the application in any shape or form. > distributing the application in any shape or form.
> >
> We recommend alternative storage such as
> [Environment Variables], an external configuration file, or a
> We recommend alternative storage such as
> [Environment Variables], an external configuration file, or a
> secrets manager for safe-handling of secrets. > secrets manager for safe-handling of secrets.
> >
> [Environment Variables]: https://en.wikipedia.org/wiki/Environment_variable > [Environment Variables]: https://en.wikipedia.org/wiki/Environment_variable
@@ -221,4 +221,4 @@ should be to separate...
2. the modules (handle commands) 2. the modules (handle commands)
3. the services (persistent storage, pure functions, data manipulation) 3. the services (persistent storage, pure functions, data manipulation)


[CommandService]: xref:Discord.Commands.CommandService
[CommandService]: xref:Discord.Commands.CommandService

+ 2
- 2
docs/index.md View File

@@ -11,12 +11,12 @@ title: Home
[![NuGet](https://img.shields.io/nuget/vpre/Discord.Net.svg?maxAge=2592000?style=plastic)](https://www.nuget.org/packages/Discord.Net) [![NuGet](https://img.shields.io/nuget/vpre/Discord.Net.svg?maxAge=2592000?style=plastic)](https://www.nuget.org/packages/Discord.Net)
[![MyGet](https://img.shields.io/myget/discord-net/vpre/Discord.Net.svg)](https://www.myget.org/feed/Packages/discord-net) [![MyGet](https://img.shields.io/myget/discord-net/vpre/Discord.Net.svg)](https://www.myget.org/feed/Packages/discord-net)
[![Build Status](https://dev.azure.com/discord-net/Discord.Net/_apis/build/status/discord-net.Discord.Net?branchName=dev)](https://dev.azure.com/discord-net/Discord.Net/_build/latest?definitionId=1&branchName=dev) [![Build Status](https://dev.azure.com/discord-net/Discord.Net/_apis/build/status/discord-net.Discord.Net?branchName=dev)](https://dev.azure.com/discord-net/Discord.Net/_build/latest?definitionId=1&branchName=dev)
[![Discord](https://discordapp.com/api/guilds/81384788765712384/widget.png)](https://discord.gg/jkrBmQR)
[![Discord](https://discord.com/api/guilds/81384788765712384/widget.png)](https://discord.gg/jkrBmQR)


## What is Discord.Net? ## What is Discord.Net?


Discord.Net is an asynchronous, multi-platform .NET Library used to Discord.Net is an asynchronous, multi-platform .NET Library used to
interface with the [Discord API](https://discordapp.com/).
interface with the [Discord API](https://discord.com/).


## Where to begin? ## Where to begin?




+ 1
- 1
samples/01_basic_ping_bot/01_basic_ping_bot.csproj View File

@@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<CheckEolTargetFramework>false</CheckEolTargetFramework> <CheckEolTargetFramework>false</CheckEolTargetFramework>
<TargetFramework>netcoreapp3.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup> </PropertyGroup>


<ItemGroup> <ItemGroup>


+ 1
- 1
samples/02_commands_framework/02_commands_framework.csproj View File

@@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<CheckEolTargetFramework>false</CheckEolTargetFramework> <CheckEolTargetFramework>false</CheckEolTargetFramework>
<TargetFramework>netcoreapp3.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup> </PropertyGroup>


<ItemGroup> <ItemGroup>


+ 1
- 1
samples/03_sharded_client/03_sharded_client.csproj View File

@@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<CheckEolTargetFramework>false</CheckEolTargetFramework> <CheckEolTargetFramework>false</CheckEolTargetFramework>
<TargetFramework>netcoreapp3.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace>_03_sharded_client</RootNamespace> <RootNamespace>_03_sharded_client</RootNamespace>
</PropertyGroup> </PropertyGroup>




+ 4
- 4
samples/04_webhook_client/Program.cs View File

@@ -14,10 +14,10 @@ namespace _04_webhook_client


public async Task MainAsync() public async Task MainAsync()
{ {
// The webhook url follows the format https://discordapp.com/api/webhooks/{id}/{token}
// The webhook url follows the format https://discord.com/api/webhooks/{id}/{token}
// Because anyone with the webhook URL can use your webhook // Because anyone with the webhook URL can use your webhook
// you should NOT hard code the URL or ID + token into your application.
using (var client = new DiscordWebhookClient("https://discordapp.com/api/webhooks/123/abc123"))
// you should NOT hard code the URL or ID + token into your application.
using (var client = new DiscordWebhookClient("https://discord.com/api/webhooks/123/abc123"))
{ {
var embed = new EmbedBuilder var embed = new EmbedBuilder
{ {
@@ -26,7 +26,7 @@ namespace _04_webhook_client
}; };


// Webhooks are able to send multiple embeds per message // Webhooks are able to send multiple embeds per message
// As such, your embeds must be passed as a collection.
// As such, your embeds must be passed as a collection.
await client.SendMessageAsync(text: "Send a message to this webhook!", embeds: new[] { embed.Build() }); await client.SendMessageAsync(text: "Send a message to this webhook!", embeds: new[] { embed.Build() });
} }
} }


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

@@ -135,7 +135,8 @@ namespace Discord.Commands
if (builder.Name == null) if (builder.Name == null)
builder.Name = typeInfo.Name; builder.Name = typeInfo.Name;


var validCommands = typeInfo.DeclaredMethods.Where(IsValidCommandDefinition);
// Get all methods (including from inherited members), that are valid commands
var validCommands = typeInfo.GetMethods().Where(IsValidCommandDefinition);


foreach (var method in validCommands) foreach (var method in validCommands)
{ {


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

@@ -13,7 +13,7 @@ namespace Discord
/// <returns> /// <returns>
/// An <see cref="int"/> representing the API version that Discord.Net uses to communicate with Discord. /// An <see cref="int"/> representing the API version that Discord.Net uses to communicate with Discord.
/// <para>A list of available API version can be seen on the official /// <para>A list of available API version can be seen on the official
/// <see href="https://discordapp.com/developers/docs/reference#api-versioning">Discord API documentation</see>
/// <see href="https://discord.com/developers/docs/reference#api-versioning">Discord API documentation</see>
/// .</para> /// .</para>
/// </returns> /// </returns>
public const int APIVersion = 6; public const int APIVersion = 6;
@@ -50,7 +50,7 @@ namespace Discord
/// <returns> /// <returns>
/// The Discord API URL using <see cref="APIVersion"/>. /// The Discord API URL using <see cref="APIVersion"/>.
/// </returns> /// </returns>
public static readonly string APIUrl = $"https://discordapp.com/api/v{APIVersion}/";
public static readonly string APIUrl = $"https://discord.com/api/v{APIVersion}/";
/// <summary> /// <summary>
/// Returns the base Discord CDN URL. /// Returns the base Discord CDN URL.
/// </summary> /// </summary>


+ 7
- 1
src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs View File

@@ -1,3 +1,5 @@
using System.Collections.Generic;

namespace Discord namespace Discord
{ {
/// <summary> /// <summary>
@@ -26,9 +28,13 @@ namespace Discord
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Setting this value to a category's snowflake identifier will change or set this channel's parent to the /// Setting this value to a category's snowflake identifier will change or set this channel's parent to the
/// specified channel; setting this value to <c>0</c> will detach this channel from its parent if one
/// specified channel; setting this value to <see langword="null"/> will detach this channel from its parent if one
/// is set. /// is set.
/// </remarks> /// </remarks>
public Optional<ulong?> CategoryId { get; set; } public Optional<ulong?> CategoryId { get; set; }
/// <summary>
/// Gets or sets the permission overwrites for this channel.
/// </summary>
public Optional<IEnumerable<Overwrite>> PermissionOverwrites { get; set; }
} }
} }

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

@@ -59,11 +59,15 @@ namespace Discord
/// <param name="embed">The <see cref="Discord.EmbedType.Rich" /> <see cref="Embed" /> to be sent.</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="options">The options to be used when sending the request.</param>
/// <param name="isSpoiler">Whether the message attachment should be hidden as a spoiler.</param> /// <param name="isSpoiler">Whether the message attachment should be hidden as a spoiler.</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> /// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result /// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message. /// contains the sent message.
/// </returns> /// </returns>
Task<IUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false);
Task<IUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null);
/// <summary> /// <summary>
/// Sends a file to this message channel with an optional caption. /// Sends a file to this message channel with an optional caption.
/// </summary> /// </summary>
@@ -88,11 +92,15 @@ namespace Discord
/// <param name="embed">The <see cref="Discord.EmbedType.Rich"/> <see cref="Embed"/> to be sent.</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="options">The options to be used when sending the request.</param>
/// <param name="isSpoiler">Whether the message attachment should be hidden as a spoiler.</param> /// <param name="isSpoiler">Whether the message attachment should be hidden as a spoiler.</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> /// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result /// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message. /// contains the sent message.
/// </returns> /// </returns>
Task<IUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false);
Task<IUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null);


/// <summary> /// <summary>
/// Gets a message from this message channel. /// Gets a message from this message channel.


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

@@ -39,6 +39,13 @@ namespace Discord
/// </returns> /// </returns>
bool IsSuppressed { get; } bool IsSuppressed { get; }
/// <summary> /// <summary>
/// Gets the value that indicates whether this message mentioned everyone.
/// </summary>
/// <returns>
/// <c>true</c> if this message mentioned everyone; otherwise <c>false</c>.
/// </returns>
bool MentionedEveryone { get; }
/// <summary>
/// Gets the content for this message. /// Gets the content for this message.
/// </summary> /// </summary>
/// <returns> /// <returns>


+ 17
- 5
src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs View File

@@ -76,6 +76,10 @@ namespace Discord
public PermValue MoveMembers => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.MoveMembers); public PermValue MoveMembers => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.MoveMembers);
/// <summary> If Allowed, a user may use voice-activity-detection rather than push-to-talk. </summary> /// <summary> If Allowed, a user may use voice-activity-detection rather than push-to-talk. </summary>
public PermValue UseVAD => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.UseVAD); public PermValue UseVAD => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.UseVAD);
/// <summary> If Allowed, a user may use priority speaker in a voice channel. </summary>
public PermValue PrioritySpeaker => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.PrioritySpeaker);
/// <summary> If Allowed, a user may go live in a voice channel. </summary>
public PermValue Stream => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.Stream);


/// <summary> If Allowed, a user may adjust role permissions. This also implicitly grants all other permissions. </summary> /// <summary> If Allowed, a user may adjust role permissions. This also implicitly grants all other permissions. </summary>
public PermValue ManageRoles => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ManageRoles); public PermValue ManageRoles => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ManageRoles);
@@ -109,7 +113,9 @@ namespace Discord
PermValue? moveMembers = null, PermValue? moveMembers = null,
PermValue? useVoiceActivation = null, PermValue? useVoiceActivation = null,
PermValue? manageRoles = null, PermValue? manageRoles = null,
PermValue? manageWebhooks = null)
PermValue? manageWebhooks = null,
PermValue? prioritySpeaker = null,
PermValue? stream = null)
{ {
Permissions.SetValue(ref allowValue, ref denyValue, createInstantInvite, ChannelPermission.CreateInstantInvite); Permissions.SetValue(ref allowValue, ref denyValue, createInstantInvite, ChannelPermission.CreateInstantInvite);
Permissions.SetValue(ref allowValue, ref denyValue, manageChannel, ChannelPermission.ManageChannels); Permissions.SetValue(ref allowValue, ref denyValue, manageChannel, ChannelPermission.ManageChannels);
@@ -129,6 +135,8 @@ namespace Discord
Permissions.SetValue(ref allowValue, ref denyValue, deafenMembers, ChannelPermission.DeafenMembers); Permissions.SetValue(ref allowValue, ref denyValue, deafenMembers, ChannelPermission.DeafenMembers);
Permissions.SetValue(ref allowValue, ref denyValue, moveMembers, ChannelPermission.MoveMembers); Permissions.SetValue(ref allowValue, ref denyValue, moveMembers, ChannelPermission.MoveMembers);
Permissions.SetValue(ref allowValue, ref denyValue, useVoiceActivation, ChannelPermission.UseVAD); Permissions.SetValue(ref allowValue, ref denyValue, useVoiceActivation, ChannelPermission.UseVAD);
Permissions.SetValue(ref allowValue, ref denyValue, prioritySpeaker, ChannelPermission.PrioritySpeaker);
Permissions.SetValue(ref allowValue, ref denyValue, stream, ChannelPermission.Stream);
Permissions.SetValue(ref allowValue, ref denyValue, manageRoles, ChannelPermission.ManageRoles); Permissions.SetValue(ref allowValue, ref denyValue, manageRoles, ChannelPermission.ManageRoles);
Permissions.SetValue(ref allowValue, ref denyValue, manageWebhooks, ChannelPermission.ManageWebhooks); Permissions.SetValue(ref allowValue, ref denyValue, manageWebhooks, ChannelPermission.ManageWebhooks);


@@ -159,10 +167,12 @@ namespace Discord
PermValue moveMembers = PermValue.Inherit, PermValue moveMembers = PermValue.Inherit,
PermValue useVoiceActivation = PermValue.Inherit, PermValue useVoiceActivation = PermValue.Inherit,
PermValue manageRoles = PermValue.Inherit, PermValue manageRoles = PermValue.Inherit,
PermValue manageWebhooks = PermValue.Inherit)
PermValue manageWebhooks = PermValue.Inherit,
PermValue prioritySpeaker = PermValue.Inherit,
PermValue stream = PermValue.Inherit)
: this(0, 0, createInstantInvite, manageChannel, addReactions, viewChannel, sendMessages, sendTTSMessages, manageMessages, : this(0, 0, createInstantInvite, manageChannel, addReactions, viewChannel, sendMessages, sendTTSMessages, manageMessages,
embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers,
moveMembers, useVoiceActivation, manageRoles, manageWebhooks) { }
moveMembers, useVoiceActivation, manageRoles, manageWebhooks, prioritySpeaker, stream) { }


/// <summary> /// <summary>
/// Initializes a new <see cref="OverwritePermissions" /> from the current one, changing the provided /// Initializes a new <see cref="OverwritePermissions" /> from the current one, changing the provided
@@ -188,10 +198,12 @@ namespace Discord
PermValue? moveMembers = null, PermValue? moveMembers = null,
PermValue? useVoiceActivation = null, PermValue? useVoiceActivation = null,
PermValue? manageRoles = null, PermValue? manageRoles = null,
PermValue? manageWebhooks = null)
PermValue? manageWebhooks = null,
PermValue? prioritySpeaker = null,
PermValue? stream = null)
=> new OverwritePermissions(AllowValue, DenyValue, createInstantInvite, manageChannel, addReactions, viewChannel, sendMessages, sendTTSMessages, manageMessages, => new OverwritePermissions(AllowValue, DenyValue, createInstantInvite, manageChannel, addReactions, viewChannel, sendMessages, sendTTSMessages, manageMessages,
embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers,
moveMembers, useVoiceActivation, manageRoles, manageWebhooks);
moveMembers, useVoiceActivation, manageRoles, manageWebhooks, prioritySpeaker, stream);


/// <summary> /// <summary>
/// Creates a <see cref="List{T}"/> of all the <see cref="ChannelPermission"/> values that are allowed. /// Creates a <see cref="List{T}"/> of all the <see cref="ChannelPermission"/> values that are allowed.


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

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


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

@@ -19,5 +19,9 @@ namespace Discord
/// Gets the set of clients where this user is currently active. /// Gets the set of clients where this user is currently active.
/// </summary> /// </summary>
IImmutableSet<ClientType> ActiveClients { get; } IImmutableSet<ClientType> ActiveClients { get; }
/// <summary>
/// Gets the list of activities that this user currently has available.
/// </summary>
IImmutableList<IActivity> Activities { get; }
} }
} }

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

@@ -17,7 +17,7 @@ namespace Discord
public static string GetJumpUrl(this IMessage msg) public static string GetJumpUrl(this IMessage msg)
{ {
var channel = msg.Channel; var channel = msg.Channel;
return $"https://discordapp.com/channels/{(channel is IDMChannel ? "@me" : $"{(channel as ITextChannel).GuildId}")}/{channel.Id}/{msg.Id}";
return $"https://discord.com/channels/{(channel is IDMChannel ? "@me" : $"{(channel as ITextChannel).GuildId}")}/{channel.Id}/{msg.Id}";
} }


/// <summary> /// <summary>


+ 43
- 0
src/Discord.Net.Core/GatewayIntents.cs View File

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

namespace Discord
{
[Flags]
public enum GatewayIntents
{
/// <summary> This intent includes no events </summary>
None = 0,
/// <summary> This intent includes GUILD_CREATE, GUILD_UPDATE, GUILD_DELETE, GUILD_ROLE_CREATE, GUILD_ROLE_UPDATE, GUILD_ROLE_DELETE, CHANNEL_CREATE, CHANNEL_UPDATE, CHANNEL_DELETE, CHANNEL_PINS_UPDATE </summary>
Guilds = 1 << 0,
/// <summary> This intent includes GUILD_MEMBER_ADD, GUILD_MEMBER_UPDATE, GUILD_MEMBER_REMOVE </summary>
/// <remarks> This is a privileged intent and must be enabled in the Developer Portal. </remarks>
GuildMembers = 1 << 1,
/// <summary> This intent includes GUILD_BAN_ADD, GUILD_BAN_REMOVE </summary>
GuildBans = 1 << 2,
/// <summary> This intent includes GUILD_EMOJIS_UPDATE </summary>
GuildEmojis = 1 << 3,
/// <summary> This intent includes GUILD_INTEGRATIONS_UPDATE </summary>
GuildIntegrations = 1 << 4,
/// <summary> This intent includes WEBHOOKS_UPDATE </summary>
GuildWebhooks = 1 << 5,
/// <summary> This intent includes INVITE_CREATE, INVITE_DELETE </summary>
GuildInvites = 1 << 6,
/// <summary> This intent includes VOICE_STATE_UPDATE </summary>
GuildVoiceStates = 1 << 7,
/// <summary> This intent includes PRESENCE_UPDATE </summary>
/// <remarks> This is a privileged intent and must be enabled in the Developer Portal. </remarks>
GuildPresences = 1 << 8,
/// <summary> This intent includes MESSAGE_CREATE, MESSAGE_UPDATE, MESSAGE_DELETE, MESSAGE_DELETE_BULK </summary>
GuildMessages = 1 << 9,
/// <summary> This intent includes MESSAGE_REACTION_ADD, MESSAGE_REACTION_REMOVE, MESSAGE_REACTION_REMOVE_ALL, MESSAGE_REACTION_REMOVE_EMOJI </summary>
GuildMessageReactions = 1 << 10,
/// <summary> This intent includes TYPING_START </summary>
GuildMessageTyping = 1 << 11,
/// <summary> This intent includes CHANNEL_CREATE, MESSAGE_CREATE, MESSAGE_UPDATE, MESSAGE_DELETE, CHANNEL_PINS_UPDATE </summary>
DirectMessages = 1 << 12,
/// <summary> This intent includes MESSAGE_REACTION_ADD, MESSAGE_REACTION_REMOVE, MESSAGE_REACTION_REMOVE_ALL, MESSAGE_REACTION_REMOVE_EMOJI </summary>
DirectMessageReactions = 1 << 13,
/// <summary> This intent includes TYPING_START </summary>
DirectMessageTyping = 1 << 14,
}
}

+ 118
- 0
src/Discord.Net.Core/Net/BucketId.cs View File

@@ -0,0 +1,118 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;

namespace Discord.Net
{
/// <summary>
/// Represents a ratelimit bucket.
/// </summary>
public class BucketId : IEquatable<BucketId>
{
/// <summary>
/// Gets the http method used to make the request if available.
/// </summary>
public string HttpMethod { get; }
/// <summary>
/// Gets the endpoint that is going to be requested if available.
/// </summary>
public string Endpoint { get; }
/// <summary>
/// Gets the major parameters of the route.
/// </summary>
public IOrderedEnumerable<KeyValuePair<string, string>> MajorParameters { get; }
/// <summary>
/// Gets the hash of this bucket.
/// </summary>
/// <remarks>
/// The hash is provided by Discord to group ratelimits.
/// </remarks>
public string BucketHash { get; }
/// <summary>
/// Gets if this bucket is a hash type.
/// </summary>
public bool IsHashBucket { get => BucketHash != null; }

private BucketId(string httpMethod, string endpoint, IEnumerable<KeyValuePair<string, string>> majorParameters, string bucketHash)
{
HttpMethod = httpMethod;
Endpoint = endpoint;
MajorParameters = majorParameters.OrderBy(x => x.Key);
BucketHash = bucketHash;
}

/// <summary>
/// Creates a new <see cref="BucketId"/> based on the
/// <see cref="HttpMethod"/> and <see cref="Endpoint"/>.
/// </summary>
/// <param name="httpMethod">Http method used to make the request.</param>
/// <param name="endpoint">Endpoint that is going to receive requests.</param>
/// <param name="majorParams">Major parameters of the route of this endpoint.</param>
/// <returns>
/// A <see cref="BucketId"/> based on the <see cref="HttpMethod"/>
/// and the <see cref="Endpoint"/> with the provided data.
/// </returns>
public static BucketId Create(string httpMethod, string endpoint, Dictionary<string, string> majorParams)
{
Preconditions.NotNullOrWhitespace(endpoint, nameof(endpoint));
majorParams ??= new Dictionary<string, string>();
return new BucketId(httpMethod, endpoint, majorParams, null);
}

/// <summary>
/// Creates a new <see cref="BucketId"/> based on a
/// <see cref="BucketHash"/> and a previous <see cref="BucketId"/>.
/// </summary>
/// <param name="hash">Bucket hash provided by Discord.</param>
/// <param name="oldBucket"><see cref="BucketId"/> that is going to be upgraded to a hash type.</param>
/// <returns>
/// A <see cref="BucketId"/> based on the <see cref="BucketHash"/>
/// and <see cref="MajorParameters"/>.
/// </returns>
public static BucketId Create(string hash, BucketId oldBucket)
{
Preconditions.NotNullOrWhitespace(hash, nameof(hash));
Preconditions.NotNull(oldBucket, nameof(oldBucket));
return new BucketId(null, null, oldBucket.MajorParameters, hash);
}

/// <summary>
/// Gets the string that will define this bucket as a hash based one.
/// </summary>
/// <returns>
/// A <see cref="string"/> that defines this bucket as a hash based one.
/// </returns>
public string GetBucketHash()
=> IsHashBucket ? $"{BucketHash}:{string.Join("/", MajorParameters.Select(x => x.Value))}" : null;

/// <summary>
/// Gets the string that will define this bucket as an endpoint based one.
/// </summary>
/// <returns>
/// A <see cref="string"/> that defines this bucket as an endpoint based one.
/// </returns>
public string GetUniqueEndpoint()
=> HttpMethod != null ? $"{HttpMethod} {Endpoint}" : Endpoint;

public override bool Equals(object obj)
=> Equals(obj as BucketId);

public override int GetHashCode()
=> IsHashBucket ? (BucketHash, string.Join("/", MajorParameters.Select(x => x.Value))).GetHashCode() : (HttpMethod, Endpoint).GetHashCode();

public override string ToString()
=> GetBucketHash() ?? GetUniqueEndpoint();

public bool Equals(BucketId other)
{
if (other is null)
return false;
if (ReferenceEquals(this, other))
return true;
if (GetType() != other.GetType())
return false;
return ToString() == other.ToString();
}
}
}

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

@@ -13,7 +13,7 @@ namespace Discord.Net
/// </summary> /// </summary>
/// <returns> /// <returns>
/// An /// An
/// <see href="https://discordapp.com/developers/docs/topics/opcodes-and-status-codes#http">HTTP status code</see>
/// <see href="https://discord.com/developers/docs/topics/opcodes-and-status-codes#http">HTTP status code</see>
/// from Discord. /// from Discord.
/// </returns> /// </returns>
public HttpStatusCode HttpCode { get; } public HttpStatusCode HttpCode { get; }
@@ -22,7 +22,7 @@ namespace Discord.Net
/// </summary> /// </summary>
/// <returns> /// <returns>
/// A /// A
/// <see href="https://discordapp.com/developers/docs/topics/opcodes-and-status-codes#json">JSON error code</see>
/// <see href="https://discord.com/developers/docs/topics/opcodes-and-status-codes#json">JSON error code</see>
/// from Discord, or <c>null</c> if none. /// from Discord, or <c>null</c> if none.
/// </returns> /// </returns>
public int? DiscordCode { get; } public int? DiscordCode { get; }


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

@@ -11,7 +11,7 @@ namespace Discord.Net
/// </summary> /// </summary>
/// <returns> /// <returns>
/// A /// A
/// <see href="https://discordapp.com/developers/docs/topics/opcodes-and-status-codes#gateway-gateway-close-event-codes">close code</see>
/// <see href="https://discord.com/developers/docs/topics/opcodes-and-status-codes#gateway-gateway-close-event-codes">close code</see>
/// from Discord. /// from Discord.
/// </returns> /// </returns>
public int CloseCode { get; } public int CloseCode { get; }


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

@@ -1,3 +1,4 @@
using Discord.Net;
using System.Threading; using System.Threading;


namespace Discord namespace Discord
@@ -57,7 +58,7 @@ namespace Discord
public bool? UseSystemClock { get; set; } public bool? UseSystemClock { get; set; }


internal bool IgnoreState { get; set; } internal bool IgnoreState { get; set; }
internal string BucketId { get; set; }
internal BucketId BucketId { get; set; }
internal bool IsClientBucket { get; set; } internal bool IsClientBucket { get; set; }
internal bool IsReactionBucket { get; set; } internal bool IsReactionBucket { get; set; }




+ 5
- 0
src/Discord.Net.Rest/API/Common/Presence.cs View File

@@ -1,5 +1,6 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using Newtonsoft.Json; using Newtonsoft.Json;
using System;
using System.Collections.Generic; using System.Collections.Generic;


namespace Discord.API namespace Discord.API
@@ -26,5 +27,9 @@ namespace Discord.API
// "client_status": { "desktop": "dnd", "mobile": "dnd" } // "client_status": { "desktop": "dnd", "mobile": "dnd" }
[JsonProperty("client_status")] [JsonProperty("client_status")]
public Optional<Dictionary<string, string>> ClientStatus { get; set; } public Optional<Dictionary<string, string>> ClientStatus { get; set; }
[JsonProperty("activities")]
public List<Game> Activities { get; set; }
[JsonProperty("premium_since")]
public Optional<DateTimeOffset?> PremiumSince { get; set; }
} }
} }

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

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


//Text channels //Text channels
[JsonProperty("topic")] [JsonProperty("topic")]
public Optional<string> Topic { get; set; } public Optional<string> Topic { get; set; }
[JsonProperty("nsfw")] [JsonProperty("nsfw")]
public Optional<bool> IsNsfw { get; set; } public Optional<bool> IsNsfw { get; set; }
[JsonProperty("rate_limit_per_user")]
public Optional<int> SlowModeInterval { get; set; }


//Voice channels //Voice channels
[JsonProperty("bitrate")] [JsonProperty("bitrate")]


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

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


namespace Discord.API.Rest namespace Discord.API.Rest
@@ -19,6 +19,8 @@ namespace Discord.API.Rest
public Optional<string> Username { get; set; } public Optional<string> Username { get; set; }
[JsonProperty("avatar_url")] [JsonProperty("avatar_url")]
public Optional<string> AvatarUrl { get; set; } public Optional<string> AvatarUrl { get; set; }
[JsonProperty("allowed_mentions")]
public Optional<AllowedMentions> AllowedMentions { get; set; }


public CreateWebhookMessageParams(string content) public CreateWebhookMessageParams(string content)
{ {


+ 3
- 0
src/Discord.Net.Rest/API/Rest/UploadFileParams.cs View File

@@ -19,6 +19,7 @@ namespace Discord.API.Rest
public Optional<string> Nonce { get; set; } public Optional<string> Nonce { get; set; }
public Optional<bool> IsTTS { get; set; } public Optional<bool> IsTTS { get; set; }
public Optional<Embed> Embed { get; set; } public Optional<Embed> Embed { get; set; }
public Optional<AllowedMentions> AllowedMentions { get; set; }
public bool IsSpoiler { get; set; } = false; public bool IsSpoiler { get; set; } = false;


public UploadFileParams(Stream file) public UploadFileParams(Stream file)
@@ -43,6 +44,8 @@ namespace Discord.API.Rest
payload["nonce"] = Nonce.Value; payload["nonce"] = Nonce.Value;
if (Embed.IsSpecified) if (Embed.IsSpecified)
payload["embed"] = Embed.Value; payload["embed"] = Embed.Value;
if (AllowedMentions.IsSpecified)
payload["allowed_mentions"] = AllowedMentions.Value;
if (IsSpoiler) if (IsSpoiler)
payload["hasSpoiler"] = IsSpoiler.ToString(); payload["hasSpoiler"] = IsSpoiler.ToString();




+ 3
- 0
src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs View File

@@ -21,6 +21,7 @@ namespace Discord.API.Rest
public Optional<string> Username { get; set; } public Optional<string> Username { get; set; }
public Optional<string> AvatarUrl { get; set; } public Optional<string> AvatarUrl { get; set; }
public Optional<Embed[]> Embeds { get; set; } public Optional<Embed[]> Embeds { get; set; }
public Optional<AllowedMentions> AllowedMentions { get; set; }


public bool IsSpoiler { get; set; } = false; public bool IsSpoiler { get; set; } = false;


@@ -51,6 +52,8 @@ namespace Discord.API.Rest
payload["avatar_url"] = AvatarUrl.Value; payload["avatar_url"] = AvatarUrl.Value;
if (Embeds.IsSpecified) if (Embeds.IsSpecified)
payload["embeds"] = Embeds.Value; payload["embeds"] = Embeds.Value;
if (AllowedMentions.IsSpecified)
payload["allowed_mentions"] = AllowedMentions.Value;


var json = new StringBuilder(); var json = new StringBuilder();
using (var text = new StringWriter(json)) using (var text = new StringWriter(json))


+ 2
- 2
src/Discord.Net.Rest/BaseDiscordClient.cs View File

@@ -49,9 +49,9 @@ namespace Discord.Rest
ApiClient.RequestQueue.RateLimitTriggered += async (id, info) => ApiClient.RequestQueue.RateLimitTriggered += async (id, info) =>
{ {
if (info == null) if (info == null)
await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {id ?? "null"}").ConfigureAwait(false);
await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {id?.ToString() ?? "null"}").ConfigureAwait(false);
else else
await _restLogger.WarningAsync($"Rate limit triggered: {id ?? "null"}").ConfigureAwait(false);
await _restLogger.WarningAsync($"Rate limit triggered: {id?.ToString() ?? "null"}").ConfigureAwait(false);
}; };
ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false); ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false);
} }


+ 58
- 37
src/Discord.Net.Rest/DiscordRestApiClient.cs View File

@@ -24,7 +24,7 @@ namespace Discord.API
{ {
internal class DiscordRestApiClient : IDisposable internal class DiscordRestApiClient : IDisposable
{ {
private static readonly ConcurrentDictionary<string, Func<BucketIds, string>> _bucketIdGenerators = new ConcurrentDictionary<string, Func<BucketIds, string>>();
private static readonly ConcurrentDictionary<string, Func<BucketIds, BucketId>> _bucketIdGenerators = new ConcurrentDictionary<string, Func<BucketIds, BucketId>>();


public event Func<string, string, double, Task> SentRequest { add { _sentRequestEvent.Add(value); } remove { _sentRequestEvent.Remove(value); } } public event Func<string, string, double, Task> SentRequest { add { _sentRequestEvent.Add(value); } remove { _sentRequestEvent.Remove(value); } }
private readonly AsyncEvent<Func<string, string, double, Task>> _sentRequestEvent = new AsyncEvent<Func<string, string, double, Task>>(); private readonly AsyncEvent<Func<string, string, double, Task>> _sentRequestEvent = new AsyncEvent<Func<string, string, double, Task>>();
@@ -80,17 +80,13 @@ namespace Discord.API
/// <exception cref="ArgumentException">Unknown OAuth token type.</exception> /// <exception cref="ArgumentException">Unknown OAuth token type.</exception>
internal static string GetPrefixedToken(TokenType tokenType, string token) internal static string GetPrefixedToken(TokenType tokenType, string token)
{ {
switch (tokenType)
return tokenType switch
{ {
case default(TokenType):
return token;
case TokenType.Bot:
return $"Bot {token}";
case TokenType.Bearer:
return $"Bearer {token}";
default:
throw new ArgumentException(message: "Unknown OAuth token type.", paramName: nameof(tokenType));
}
default(TokenType) => token,
TokenType.Bot => $"Bot {token}",
TokenType.Bearer => $"Bearer {token}",
_ => throw new ArgumentException(message: "Unknown OAuth token type.", paramName: nameof(tokenType)),
};
} }
internal virtual void Dispose(bool disposing) internal virtual void Dispose(bool disposing)
{ {
@@ -133,7 +129,7 @@ namespace Discord.API
RestClient.SetCancelToken(_loginCancelToken.Token); RestClient.SetCancelToken(_loginCancelToken.Token);


AuthTokenType = tokenType; AuthTokenType = tokenType;
AuthToken = token;
AuthToken = token?.TrimEnd();
if (tokenType != TokenType.Webhook) if (tokenType != TokenType.Webhook)
RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken)); RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken));


@@ -180,9 +176,9 @@ namespace Discord.API
//Core //Core
internal Task SendAsync(string method, Expression<Func<string>> endpointExpr, BucketIds ids, internal Task SendAsync(string method, Expression<Func<string>> endpointExpr, BucketIds ids,
ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null)
=> SendAsync(method, GetEndpoint(endpointExpr), GetBucketId(ids, endpointExpr, funcName), clientBucket, options);
=> SendAsync(method, GetEndpoint(endpointExpr), GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options);
public async Task SendAsync(string method, string endpoint, public async Task SendAsync(string method, string endpoint,
string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null)
BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null)
{ {
options = options ?? new RequestOptions(); options = options ?? new RequestOptions();
options.HeaderOnly = true; options.HeaderOnly = true;
@@ -194,9 +190,9 @@ namespace Discord.API


internal Task SendJsonAsync(string method, Expression<Func<string>> endpointExpr, object payload, BucketIds ids, internal Task SendJsonAsync(string method, Expression<Func<string>> endpointExpr, object payload, BucketIds ids,
ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null)
=> SendJsonAsync(method, GetEndpoint(endpointExpr), payload, GetBucketId(ids, endpointExpr, funcName), clientBucket, options);
=> SendJsonAsync(method, GetEndpoint(endpointExpr), payload, GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options);
public async Task SendJsonAsync(string method, string endpoint, object payload, public async Task SendJsonAsync(string method, string endpoint, object payload,
string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null)
BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null)
{ {
options = options ?? new RequestOptions(); options = options ?? new RequestOptions();
options.HeaderOnly = true; options.HeaderOnly = true;
@@ -209,9 +205,9 @@ namespace Discord.API


internal Task SendMultipartAsync(string method, Expression<Func<string>> endpointExpr, IReadOnlyDictionary<string, object> multipartArgs, BucketIds ids, internal Task SendMultipartAsync(string method, Expression<Func<string>> endpointExpr, IReadOnlyDictionary<string, object> multipartArgs, BucketIds ids,
ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null)
=> SendMultipartAsync(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(ids, endpointExpr, funcName), clientBucket, options);
=> SendMultipartAsync(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options);
public async Task SendMultipartAsync(string method, string endpoint, IReadOnlyDictionary<string, object> multipartArgs, public async Task SendMultipartAsync(string method, string endpoint, IReadOnlyDictionary<string, object> multipartArgs,
string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null)
BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null)
{ {
options = options ?? new RequestOptions(); options = options ?? new RequestOptions();
options.HeaderOnly = true; options.HeaderOnly = true;
@@ -223,9 +219,9 @@ namespace Discord.API


internal Task<TResponse> SendAsync<TResponse>(string method, Expression<Func<string>> endpointExpr, BucketIds ids, internal Task<TResponse> SendAsync<TResponse>(string method, Expression<Func<string>> endpointExpr, BucketIds ids,
ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) where TResponse : class ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) where TResponse : class
=> SendAsync<TResponse>(method, GetEndpoint(endpointExpr), GetBucketId(ids, endpointExpr, funcName), clientBucket, options);
=> SendAsync<TResponse>(method, GetEndpoint(endpointExpr), GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options);
public async Task<TResponse> SendAsync<TResponse>(string method, string endpoint, public async Task<TResponse> SendAsync<TResponse>(string method, string endpoint,
string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) where TResponse : class
BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) where TResponse : class
{ {
options = options ?? new RequestOptions(); options = options ?? new RequestOptions();
options.BucketId = bucketId; options.BucketId = bucketId;
@@ -236,9 +232,9 @@ namespace Discord.API


internal Task<TResponse> SendJsonAsync<TResponse>(string method, Expression<Func<string>> endpointExpr, object payload, BucketIds ids, internal Task<TResponse> SendJsonAsync<TResponse>(string method, Expression<Func<string>> endpointExpr, object payload, BucketIds ids,
ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) where TResponse : class ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) where TResponse : class
=> SendJsonAsync<TResponse>(method, GetEndpoint(endpointExpr), payload, GetBucketId(ids, endpointExpr, funcName), clientBucket, options);
=> SendJsonAsync<TResponse>(method, GetEndpoint(endpointExpr), payload, GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options);
public async Task<TResponse> SendJsonAsync<TResponse>(string method, string endpoint, object payload, public async Task<TResponse> SendJsonAsync<TResponse>(string method, string endpoint, object payload,
string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) where TResponse : class
BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) where TResponse : class
{ {
options = options ?? new RequestOptions(); options = options ?? new RequestOptions();
options.BucketId = bucketId; options.BucketId = bucketId;
@@ -250,9 +246,9 @@ namespace Discord.API


internal Task<TResponse> SendMultipartAsync<TResponse>(string method, Expression<Func<string>> endpointExpr, IReadOnlyDictionary<string, object> multipartArgs, BucketIds ids, internal Task<TResponse> SendMultipartAsync<TResponse>(string method, Expression<Func<string>> endpointExpr, IReadOnlyDictionary<string, object> multipartArgs, BucketIds ids,
ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null)
=> SendMultipartAsync<TResponse>(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(ids, endpointExpr, funcName), clientBucket, options);
=> SendMultipartAsync<TResponse>(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options);
public async Task<TResponse> SendMultipartAsync<TResponse>(string method, string endpoint, IReadOnlyDictionary<string, object> multipartArgs, public async Task<TResponse> SendMultipartAsync<TResponse>(string method, string endpoint, IReadOnlyDictionary<string, object> multipartArgs,
string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null)
BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null)
{ {
options = options ?? new RequestOptions(); options = options ?? new RequestOptions();
options.BucketId = bucketId; options.BucketId = bucketId;
@@ -524,7 +520,8 @@ namespace Discord.API
throw new ArgumentException(message: $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", paramName: nameof(args.Content)); throw new ArgumentException(message: $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", paramName: nameof(args.Content));
options = RequestOptions.CreateOrClone(options); options = RequestOptions.CreateOrClone(options);


return await SendJsonAsync<Message>("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args, new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false);
var ids = new BucketIds(webhookId: webhookId);
return await SendJsonAsync<Message>("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false);
} }
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public async Task<Message> UploadFileAsync(ulong channelId, UploadFileParams args, RequestOptions options = null) public async Task<Message> UploadFileAsync(ulong channelId, UploadFileParams args, RequestOptions options = null)
@@ -563,7 +560,8 @@ namespace Discord.API
throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content));
} }


return await SendMultipartAsync<Message>("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args.ToDictionary(), new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false);
var ids = new BucketIds(webhookId: webhookId);
return await SendMultipartAsync<Message>("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false);
} }
public async Task DeleteMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) public async Task DeleteMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null)
{ {
@@ -876,8 +874,12 @@ namespace Discord.API
Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(guildId, 0, nameof(guildId));
options = RequestOptions.CreateOrClone(options); options = RequestOptions.CreateOrClone(options);


var ids = new BucketIds(guildId: guildId);
return await SendAsync<Ban>("GET", () => $"guilds/{guildId}/bans/{userId}", ids, options: options).ConfigureAwait(false);
try
{
var ids = new BucketIds(guildId: guildId);
return await SendAsync<Ban>("GET", () => $"guilds/{guildId}/bans/{userId}", ids, options: options).ConfigureAwait(false);
}
catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; }
} }
/// <exception cref="ArgumentException"> /// <exception cref="ArgumentException">
/// <paramref name="guildId"/> and <paramref name="userId"/> must not be equal to zero. /// <paramref name="guildId"/> and <paramref name="userId"/> must not be equal to zero.
@@ -1470,21 +1472,39 @@ namespace Discord.API
{ {
public ulong GuildId { get; internal set; } public ulong GuildId { get; internal set; }
public ulong ChannelId { get; internal set; } public ulong ChannelId { get; internal set; }
public ulong WebhookId { get; internal set; }
public string HttpMethod { get; internal set; }


internal BucketIds(ulong guildId = 0, ulong channelId = 0)
internal BucketIds(ulong guildId = 0, ulong channelId = 0, ulong webhookId = 0)
{ {
GuildId = guildId; GuildId = guildId;
ChannelId = channelId; ChannelId = channelId;
WebhookId = webhookId;
} }

internal object[] ToArray() internal object[] ToArray()
=> new object[] { GuildId, ChannelId };
=> new object[] { HttpMethod, GuildId, ChannelId, WebhookId };

internal Dictionary<string, string> ToMajorParametersDictionary()
{
var dict = new Dictionary<string, string>();
if (GuildId != 0)
dict["GuildId"] = GuildId.ToString();
if (ChannelId != 0)
dict["ChannelId"] = ChannelId.ToString();
if (WebhookId != 0)
dict["WebhookId"] = WebhookId.ToString();
return dict;
}


internal static int? GetIndex(string name) internal static int? GetIndex(string name)
{ {
switch (name) switch (name)
{ {
case "guildId": return 0;
case "channelId": return 1;
case "httpMethod": return 0;
case "guildId": return 1;
case "channelId": return 2;
case "webhookId": return 3;
default: default:
return null; return null;
} }
@@ -1495,18 +1515,19 @@ namespace Discord.API
{ {
return endpointExpr.Compile()(); return endpointExpr.Compile()();
} }
private static string GetBucketId(BucketIds ids, Expression<Func<string>> endpointExpr, string callingMethod)
private static BucketId GetBucketId(string httpMethod, BucketIds ids, Expression<Func<string>> endpointExpr, string callingMethod)
{ {
ids.HttpMethod ??= httpMethod;
return _bucketIdGenerators.GetOrAdd(callingMethod, x => CreateBucketId(endpointExpr))(ids); return _bucketIdGenerators.GetOrAdd(callingMethod, x => CreateBucketId(endpointExpr))(ids);
} }


private static Func<BucketIds, string> CreateBucketId(Expression<Func<string>> endpoint)
private static Func<BucketIds, BucketId> CreateBucketId(Expression<Func<string>> endpoint)
{ {
try try
{ {
//Is this a constant string? //Is this a constant string?
if (endpoint.Body.NodeType == ExpressionType.Constant) if (endpoint.Body.NodeType == ExpressionType.Constant)
return x => (endpoint.Body as ConstantExpression).Value.ToString();
return x => BucketId.Create(x.HttpMethod, (endpoint.Body as ConstantExpression).Value.ToString(), x.ToMajorParametersDictionary());


var builder = new StringBuilder(); var builder = new StringBuilder();
var methodCall = endpoint.Body as MethodCallExpression; var methodCall = endpoint.Body as MethodCallExpression;
@@ -1543,7 +1564,7 @@ namespace Discord.API


var mappedId = BucketIds.GetIndex(fieldName); var mappedId = BucketIds.GetIndex(fieldName);


if(!mappedId.HasValue && rightIndex != endIndex && format.Length > rightIndex + 1 && format[rightIndex + 1] == '/') //Ignore the next slash
if (!mappedId.HasValue && rightIndex != endIndex && format.Length > rightIndex + 1 && format[rightIndex + 1] == '/') //Ignore the next slash
rightIndex++; rightIndex++;


if (mappedId.HasValue) if (mappedId.HasValue)
@@ -1556,7 +1577,7 @@ namespace Discord.API


format = builder.ToString(); format = builder.ToString();


return x => string.Format(format, x.ToArray());
return x => BucketId.Create(x.HttpMethod, string.Format(format, x.ToArray()), x.ToMajorParametersDictionary());
} }
catch (Exception ex) catch (Exception ex)
{ {


+ 9
- 5
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs View File

@@ -36,13 +36,17 @@ namespace Discord.Rest
var maxAge = maxAgeModel.NewValue.ToObject<int>(discord.ApiClient.Serializer); var maxAge = maxAgeModel.NewValue.ToObject<int>(discord.ApiClient.Serializer);
var code = codeModel.NewValue.ToObject<string>(discord.ApiClient.Serializer); var code = codeModel.NewValue.ToObject<string>(discord.ApiClient.Serializer);
var temporary = temporaryModel.NewValue.ToObject<bool>(discord.ApiClient.Serializer); var temporary = temporaryModel.NewValue.ToObject<bool>(discord.ApiClient.Serializer);
var inviterId = inviterIdModel.NewValue.ToObject<ulong>(discord.ApiClient.Serializer);
var channelId = channelIdModel.NewValue.ToObject<ulong>(discord.ApiClient.Serializer); var channelId = channelIdModel.NewValue.ToObject<ulong>(discord.ApiClient.Serializer);
var uses = usesModel.NewValue.ToObject<int>(discord.ApiClient.Serializer); var uses = usesModel.NewValue.ToObject<int>(discord.ApiClient.Serializer);
var maxUses = maxUsesModel.NewValue.ToObject<int>(discord.ApiClient.Serializer); var maxUses = maxUsesModel.NewValue.ToObject<int>(discord.ApiClient.Serializer);


var inviterInfo = log.Users.FirstOrDefault(x => x.Id == inviterId);
var inviter = RestUser.Create(discord, inviterInfo);
RestUser inviter = null;
if (inviterIdModel != null)
{
var inviterId = inviterIdModel.NewValue.ToObject<ulong>(discord.ApiClient.Serializer);
var inviterInfo = log.Users.FirstOrDefault(x => x.Id == inviterId);
inviter = RestUser.Create(discord, inviterInfo);
}


return new InviteCreateAuditLogData(maxAge, code, temporary, inviter, channelId, uses, maxUses); return new InviteCreateAuditLogData(maxAge, code, temporary, inviter, channelId, uses, maxUses);
} }
@@ -70,10 +74,10 @@ namespace Discord.Rest
/// </returns> /// </returns>
public bool Temporary { get; } public bool Temporary { get; }
/// <summary> /// <summary>
/// Gets the user that created this invite.
/// Gets the user that created this invite if available.
/// </summary> /// </summary>
/// <returns> /// <returns>
/// A user that created this invite.
/// A user that created this invite or <see langword="null"/>.
/// </returns> /// </returns>
public IUser Creator { get; } public IUser Creator { get; }
/// <summary> /// <summary>


+ 9
- 5
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs View File

@@ -36,13 +36,17 @@ namespace Discord.Rest
var maxAge = maxAgeModel.OldValue.ToObject<int>(discord.ApiClient.Serializer); var maxAge = maxAgeModel.OldValue.ToObject<int>(discord.ApiClient.Serializer);
var code = codeModel.OldValue.ToObject<string>(discord.ApiClient.Serializer); var code = codeModel.OldValue.ToObject<string>(discord.ApiClient.Serializer);
var temporary = temporaryModel.OldValue.ToObject<bool>(discord.ApiClient.Serializer); var temporary = temporaryModel.OldValue.ToObject<bool>(discord.ApiClient.Serializer);
var inviterId = inviterIdModel.OldValue.ToObject<ulong>(discord.ApiClient.Serializer);
var channelId = channelIdModel.OldValue.ToObject<ulong>(discord.ApiClient.Serializer); var channelId = channelIdModel.OldValue.ToObject<ulong>(discord.ApiClient.Serializer);
var uses = usesModel.OldValue.ToObject<int>(discord.ApiClient.Serializer); var uses = usesModel.OldValue.ToObject<int>(discord.ApiClient.Serializer);
var maxUses = maxUsesModel.OldValue.ToObject<int>(discord.ApiClient.Serializer); var maxUses = maxUsesModel.OldValue.ToObject<int>(discord.ApiClient.Serializer);


var inviterInfo = log.Users.FirstOrDefault(x => x.Id == inviterId);
var inviter = RestUser.Create(discord, inviterInfo);
RestUser inviter = null;
if (inviterIdModel != null)
{
var inviterId = inviterIdModel.OldValue.ToObject<ulong>(discord.ApiClient.Serializer);
var inviterInfo = log.Users.FirstOrDefault(x => x.Id == inviterId);
inviter = RestUser.Create(discord, inviterInfo);
}


return new InviteDeleteAuditLogData(maxAge, code, temporary, inviter, channelId, uses, maxUses); return new InviteDeleteAuditLogData(maxAge, code, temporary, inviter, channelId, uses, maxUses);
} }
@@ -70,10 +74,10 @@ namespace Discord.Rest
/// </returns> /// </returns>
public bool Temporary { get; } public bool Temporary { get; }
/// <summary> /// <summary>
/// Gets the user that created this invite.
/// Gets the user that created this invite if available.
/// </summary> /// </summary>
/// <returns> /// <returns>
/// A user that created this invite.
/// A user that created this invite or <see langword="null"/>.
/// </returns> /// </returns>
public IUser Creator { get; } public IUser Creator { get; }
/// <summary> /// <summary>


+ 64
- 10
src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs View File

@@ -28,7 +28,16 @@ namespace Discord.Rest
{ {
Name = args.Name, Name = args.Name,
Position = args.Position, Position = args.Position,
CategoryId = args.CategoryId
CategoryId = args.CategoryId,
Overwrites = args.PermissionOverwrites.IsSpecified
? args.PermissionOverwrites.Value.Select(overwrite => new API.Overwrite
{
TargetId = overwrite.TargetId,
TargetType = overwrite.TargetType,
Allow = overwrite.Permissions.AllowValue,
Deny = overwrite.Permissions.DenyValue
}).ToArray()
: Optional.Create<API.Overwrite[]>(),
}; };
return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false);
} }
@@ -46,6 +55,15 @@ namespace Discord.Rest
Topic = args.Topic, Topic = args.Topic,
IsNsfw = args.IsNsfw, IsNsfw = args.IsNsfw,
SlowModeInterval = args.SlowModeInterval, SlowModeInterval = args.SlowModeInterval,
Overwrites = args.PermissionOverwrites.IsSpecified
? args.PermissionOverwrites.Value.Select(overwrite => new API.Overwrite
{
TargetId = overwrite.TargetId,
TargetType = overwrite.TargetType,
Allow = overwrite.Permissions.AllowValue,
Deny = overwrite.Permissions.DenyValue
}).ToArray()
: Optional.Create<API.Overwrite[]>(),
}; };
return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false);
} }
@@ -61,7 +79,16 @@ namespace Discord.Rest
Name = args.Name, Name = args.Name,
Position = args.Position, Position = args.Position,
CategoryId = args.CategoryId, CategoryId = args.CategoryId,
UserLimit = args.UserLimit.IsSpecified ? (args.UserLimit.Value ?? 0) : Optional.Create<int>()
UserLimit = args.UserLimit.IsSpecified ? (args.UserLimit.Value ?? 0) : Optional.Create<int>(),
Overwrites = args.PermissionOverwrites.IsSpecified
? args.PermissionOverwrites.Value.Select(overwrite => new API.Overwrite
{
TargetId = overwrite.TargetId,
TargetType = overwrite.TargetType,
Allow = overwrite.Permissions.AllowValue,
Deny = overwrite.Permissions.DenyValue
}).ToArray()
: Optional.Create<API.Overwrite[]>(),
}; };
return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false);
} }
@@ -109,12 +136,19 @@ namespace Discord.Rest
public static IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(IMessageChannel channel, BaseDiscordClient client, public static IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(IMessageChannel channel, BaseDiscordClient client,
ulong? fromMessageId, Direction dir, int limit, RequestOptions options) ulong? fromMessageId, Direction dir, int limit, RequestOptions options)
{ {
if (dir == Direction.Around)
throw new NotImplementedException(); //TODO: Impl

var guildId = (channel as IGuildChannel)?.GuildId; var guildId = (channel as IGuildChannel)?.GuildId;
var guild = guildId != null ? (client as IDiscordClient).GetGuildAsync(guildId.Value, CacheMode.CacheOnly).Result : null; var guild = guildId != null ? (client as IDiscordClient).GetGuildAsync(guildId.Value, CacheMode.CacheOnly).Result : null;


if (dir == Direction.Around && limit > DiscordConfig.MaxMessagesPerBatch)
{
int around = limit / 2;
if (fromMessageId.HasValue)
return GetMessagesAsync(channel, client, fromMessageId.Value + 1, Direction.Before, around + 1, options) //Need to include the message itself
.Concat(GetMessagesAsync(channel, client, fromMessageId, Direction.After, around, options));
else //Shouldn't happen since there's no public overload for ulong? and Direction
return GetMessagesAsync(channel, client, null, Direction.Before, around + 1, options);
}

return new PagedAsyncEnumerable<RestMessage>( return new PagedAsyncEnumerable<RestMessage>(
DiscordConfig.MaxMessagesPerBatch, DiscordConfig.MaxMessagesPerBatch,
async (info, ct) => async (info, ct) =>
@@ -218,18 +252,37 @@ namespace Discord.Rest
/// <exception cref="IOException">An I/O error occurred while opening the file.</exception> /// <exception cref="IOException">An I/O error occurred while opening the file.</exception>
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> /// <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> SendFileAsync(IMessageChannel channel, BaseDiscordClient client, public static async Task<RestUserMessage> SendFileAsync(IMessageChannel channel, BaseDiscordClient client,
string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler)
string filePath, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, RequestOptions options, bool isSpoiler)
{ {
string filename = Path.GetFileName(filePath); string filename = Path.GetFileName(filePath);
using (var file = File.OpenRead(filePath)) using (var file = File.OpenRead(filePath))
return await SendFileAsync(channel, client, file, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false);
return await SendFileAsync(channel, client, file, filename, text, isTTS, embed, allowedMentions, options, isSpoiler).ConfigureAwait(false);
} }


/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> /// <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> SendFileAsync(IMessageChannel channel, BaseDiscordClient client, public static async Task<RestUserMessage> SendFileAsync(IMessageChannel channel, BaseDiscordClient client,
Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler)
Stream stream, string filename, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, RequestOptions options, bool isSpoiler)
{ {
var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS, Embed = embed != null ? embed.ToModel() : Optional<API.Embed>.Unspecified, IsSpoiler = isSpoiler };
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 UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS, Embed = embed?.ToModel() ?? Optional<API.Embed>.Unspecified, AllowedMentions = allowedMentions?.ToModel() ?? Optional<API.AllowedMentions>.Unspecified, IsSpoiler = isSpoiler };
var model = await client.ApiClient.UploadFileAsync(channel.Id, args, options).ConfigureAwait(false); var model = await client.ApiClient.UploadFileAsync(channel.Id, args, options).ConfigureAwait(false);
return RestUserMessage.Create(client, channel, client.CurrentUser, model); return RestUserMessage.Create(client, channel, client.CurrentUser, model);
} }
@@ -387,7 +440,8 @@ namespace Discord.Rest
var apiArgs = new ModifyGuildChannelParams var apiArgs = new ModifyGuildChannelParams
{ {
Overwrites = category.PermissionOverwrites Overwrites = category.PermissionOverwrites
.Select(overwrite => new API.Overwrite{
.Select(overwrite => new API.Overwrite
{
TargetId = overwrite.TargetId, TargetId = overwrite.TargetId,
TargetType = overwrite.TargetType, TargetType = overwrite.TargetType,
Allow = overwrite.Permissions.AllowValue, Allow = overwrite.Permissions.AllowValue,


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

@@ -34,7 +34,7 @@ namespace Discord.Rest
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This method follows the same behavior as described in /// This method follows the same behavior as described in
/// <see cref="IMessageChannel.SendFileAsync(string, string, bool, Embed, RequestOptions, bool)"/>. Please visit
/// <see cref="IMessageChannel.SendFileAsync(string, string, bool, Embed, RequestOptions, bool, AllowedMentions)"/>. Please visit
/// its documentation for more details on this method. /// its documentation for more details on this method.
/// </remarks> /// </remarks>
/// <param name="filePath">The file path of the file.</param> /// <param name="filePath">The file path of the file.</param>
@@ -42,16 +42,21 @@ namespace Discord.Rest
/// <param name="isTTS">Whether the message should be read aloud by Discord or not.</param> /// <param name="isTTS">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="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="options">The options to be used when sending the request.</param>
/// <param name="isSpoiler">Whether the message attachment should be hidden as a spoiler.</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> /// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result /// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message. /// contains the sent message.
/// </returns> /// </returns>
new Task<RestUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false);
new Task<RestUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null);
/// <summary> /// <summary>
/// Sends a file to this message channel with an optional caption. /// Sends a file to this message channel with an optional caption.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This method follows the same behavior as described in <see cref="IMessageChannel.SendFileAsync(Stream, string, string, bool, Embed, RequestOptions, bool)"/>.
/// This method follows the same behavior as described in <see cref="IMessageChannel.SendFileAsync(Stream, string, string, bool, Embed, RequestOptions, bool, AllowedMentions)"/>.
/// Please visit its documentation for more details on this method. /// Please visit its documentation for more details on this method.
/// </remarks> /// </remarks>
/// <param name="stream">The <see cref="Stream" /> of the file to be sent.</param> /// <param name="stream">The <see cref="Stream" /> of the file to be sent.</param>
@@ -60,11 +65,16 @@ namespace Discord.Rest
/// <param name="isTTS">Whether the message should be read aloud by Discord or not.</param> /// <param name="isTTS">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="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="options">The options to be used when sending the request.</param>
/// <param name="isSpoiler">Whether the message attachment should be hidden as a spoiler.</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> /// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result /// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message. /// contains the sent message.
/// </returns> /// </returns>
new Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false);
new Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null);


/// <summary> /// <summary>
/// Gets a message from this message channel. /// Gets a message from this message channel.


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

@@ -121,12 +121,12 @@ namespace Discord.Rest
/// <exception cref="NotSupportedException"><paramref name="filePath" /> is in an invalid format.</exception> /// <exception cref="NotSupportedException"><paramref name="filePath" /> is in an invalid format.</exception>
/// <exception cref="IOException">An I/O error occurred while opening the file.</exception> /// <exception cref="IOException">An I/O error occurred while opening the file.</exception>
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false)
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler);
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, options, isSpoiler);
/// <inheritdoc /> /// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false)
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler);
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, options, isSpoiler);


/// <inheritdoc /> /// <inheritdoc />
public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null)
@@ -200,11 +200,11 @@ namespace Discord.Rest
async Task<IReadOnlyCollection<IMessage>> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) async Task<IReadOnlyCollection<IMessage>> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options)
=> await GetPinnedMessagesAsync(options).ConfigureAwait(false); => await GetPinnedMessagesAsync(options).ConfigureAwait(false);
/// <inheritdoc /> /// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler)
=> await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false);
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions)
=> await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions).ConfigureAwait(false);
/// <inheritdoc /> /// <inheritdoc />
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.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions)
=> await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions).ConfigureAwait(false);
/// <inheritdoc /> /// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions)
=> await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false);


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

@@ -123,12 +123,12 @@ namespace Discord.Rest
/// <exception cref="NotSupportedException"><paramref name="filePath" /> is in an invalid format.</exception> /// <exception cref="NotSupportedException"><paramref name="filePath" /> is in an invalid format.</exception>
/// <exception cref="IOException">An I/O error occurred while opening the file.</exception> /// <exception cref="IOException">An I/O error occurred while opening the file.</exception>
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false)
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler);
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, options, isSpoiler);
/// <inheritdoc /> /// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false)
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler);
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, options, isSpoiler);


/// <inheritdoc /> /// <inheritdoc />
public Task TriggerTypingAsync(RequestOptions options = null) public Task TriggerTypingAsync(RequestOptions options = null)
@@ -178,11 +178,11 @@ namespace Discord.Rest
async Task<IReadOnlyCollection<IMessage>> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) async Task<IReadOnlyCollection<IMessage>> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options)
=> await GetPinnedMessagesAsync(options).ConfigureAwait(false); => await GetPinnedMessagesAsync(options).ConfigureAwait(false);


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


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.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions)
=> await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions).ConfigureAwait(false);


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


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

@@ -42,7 +42,8 @@ namespace Discord.Rest
base.Update(model); base.Update(model);
CategoryId = model.CategoryId; CategoryId = model.CategoryId;
Topic = model.Topic.Value; Topic = model.Topic.Value;
SlowModeInterval = model.SlowMode.Value;
if (model.SlowMode.IsSpecified)
SlowModeInterval = model.SlowMode.Value;
IsNsfw = model.Nsfw.GetValueOrDefault(); IsNsfw = model.Nsfw.GetValueOrDefault();
} }


@@ -129,13 +130,13 @@ namespace Discord.Rest
/// <exception cref="NotSupportedException"><paramref name="filePath" /> is in an invalid format.</exception> /// <exception cref="NotSupportedException"><paramref name="filePath" /> is in an invalid format.</exception>
/// <exception cref="IOException">An I/O error occurred while opening the file.</exception> /// <exception cref="IOException">An I/O error occurred while opening the file.</exception>
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false)
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler);
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, options, isSpoiler);


/// <inheritdoc /> /// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false)
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler);
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, options, isSpoiler);


/// <inheritdoc /> /// <inheritdoc />
public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null)
@@ -266,12 +267,12 @@ namespace Discord.Rest
=> await GetPinnedMessagesAsync(options).ConfigureAwait(false); => await GetPinnedMessagesAsync(options).ConfigureAwait(false);


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


/// <inheritdoc /> /// <inheritdoc />
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.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions)
=> await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions).ConfigureAwait(false);
/// <inheritdoc /> /// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions)
=> await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false);


+ 32
- 4
src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs View File

@@ -132,7 +132,7 @@ namespace Discord.Rest
public static async Task<RestBan> GetBanAsync(IGuild guild, BaseDiscordClient client, ulong userId, RequestOptions options) public static async Task<RestBan> GetBanAsync(IGuild guild, BaseDiscordClient client, ulong userId, RequestOptions options)
{ {
var model = await client.ApiClient.GetGuildBanAsync(guild.Id, userId, options).ConfigureAwait(false); var model = await client.ApiClient.GetGuildBanAsync(guild.Id, userId, options).ConfigureAwait(false);
return RestBan.Create(client, model);
return model == null ? null : RestBan.Create(client, model);
} }


public static async Task AddBanAsync(IGuild guild, BaseDiscordClient client, public static async Task AddBanAsync(IGuild guild, BaseDiscordClient client,
@@ -176,7 +176,17 @@ namespace Discord.Rest
CategoryId = props.CategoryId, CategoryId = props.CategoryId,
Topic = props.Topic, Topic = props.Topic,
IsNsfw = props.IsNsfw, IsNsfw = props.IsNsfw,
Position = props.Position
Position = props.Position,
SlowModeInterval = props.SlowModeInterval,
Overwrites = props.PermissionOverwrites.IsSpecified
? props.PermissionOverwrites.Value.Select(overwrite => new API.Overwrite
{
TargetId = overwrite.TargetId,
TargetType = overwrite.TargetType,
Allow = overwrite.Permissions.AllowValue,
Deny = overwrite.Permissions.DenyValue
}).ToArray()
: Optional.Create<API.Overwrite[]>(),
}; };
var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false);
return RestTextChannel.Create(client, guild, model); return RestTextChannel.Create(client, guild, model);
@@ -195,7 +205,16 @@ namespace Discord.Rest
CategoryId = props.CategoryId, CategoryId = props.CategoryId,
Bitrate = props.Bitrate, Bitrate = props.Bitrate,
UserLimit = props.UserLimit, UserLimit = props.UserLimit,
Position = props.Position
Position = props.Position,
Overwrites = props.PermissionOverwrites.IsSpecified
? props.PermissionOverwrites.Value.Select(overwrite => new API.Overwrite
{
TargetId = overwrite.TargetId,
TargetType = overwrite.TargetType,
Allow = overwrite.Permissions.AllowValue,
Deny = overwrite.Permissions.DenyValue
}).ToArray()
: Optional.Create<API.Overwrite[]>(),
}; };
var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false);
return RestVoiceChannel.Create(client, guild, model); return RestVoiceChannel.Create(client, guild, model);
@@ -211,7 +230,16 @@ namespace Discord.Rest


var args = new CreateGuildChannelParams(name, ChannelType.Category) var args = new CreateGuildChannelParams(name, ChannelType.Category)
{ {
Position = props.Position
Position = props.Position,
Overwrites = props.PermissionOverwrites.IsSpecified
? props.PermissionOverwrites.Value.Select(overwrite => new API.Overwrite
{
TargetId = overwrite.TargetId,
TargetType = overwrite.TargetType,
Allow = overwrite.Permissions.AllowValue,
Deny = overwrite.Permissions.DenyValue
}).ToArray()
: Optional.Create<API.Overwrite[]>(),
}; };


var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false);


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

@@ -37,6 +37,9 @@ namespace Discord.Rest
public virtual bool IsSuppressed => false; public virtual bool IsSuppressed => false;
/// <inheritdoc /> /// <inheritdoc />
public virtual DateTimeOffset? EditedTimestamp => null; public virtual DateTimeOffset? EditedTimestamp => null;
/// <inheritdoc />
public virtual bool MentionedEveryone => false;

/// <summary> /// <summary>
/// Gets a collection of the <see cref="Attachment"/>'s on the message. /// Gets a collection of the <see cref="Attachment"/>'s on the message.
/// </summary> /// </summary>


+ 11
- 6
src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs View File

@@ -18,6 +18,8 @@ namespace Discord.Rest
private ImmutableArray<Attachment> _attachments = ImmutableArray.Create<Attachment>(); private ImmutableArray<Attachment> _attachments = ImmutableArray.Create<Attachment>();
private ImmutableArray<Embed> _embeds = ImmutableArray.Create<Embed>(); private ImmutableArray<Embed> _embeds = ImmutableArray.Create<Embed>();
private ImmutableArray<ITag> _tags = ImmutableArray.Create<ITag>(); private ImmutableArray<ITag> _tags = ImmutableArray.Create<ITag>();
private ImmutableArray<ulong> _roleMentionIds = ImmutableArray.Create<ulong>();
private ImmutableArray<RestUser> _userMentions = ImmutableArray.Create<RestUser>();


/// <inheritdoc /> /// <inheritdoc />
public override bool IsTTS => _isTTS; public override bool IsTTS => _isTTS;
@@ -28,15 +30,17 @@ namespace Discord.Rest
/// <inheritdoc /> /// <inheritdoc />
public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks); public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks);
/// <inheritdoc /> /// <inheritdoc />
public override bool MentionedEveryone => _isMentioningEveryone;
/// <inheritdoc />
public override IReadOnlyCollection<Attachment> Attachments => _attachments; public override IReadOnlyCollection<Attachment> Attachments => _attachments;
/// <inheritdoc /> /// <inheritdoc />
public override IReadOnlyCollection<Embed> Embeds => _embeds; public override IReadOnlyCollection<Embed> Embeds => _embeds;
/// <inheritdoc /> /// <inheritdoc />
public override IReadOnlyCollection<ulong> MentionedChannelIds => MessageHelper.FilterTagsByKey(TagType.ChannelMention, _tags); public override IReadOnlyCollection<ulong> MentionedChannelIds => MessageHelper.FilterTagsByKey(TagType.ChannelMention, _tags);
/// <inheritdoc /> /// <inheritdoc />
public override IReadOnlyCollection<ulong> MentionedRoleIds => MessageHelper.FilterTagsByKey(TagType.RoleMention, _tags);
public override IReadOnlyCollection<ulong> MentionedRoleIds => _roleMentionIds;
/// <inheritdoc /> /// <inheritdoc />
public override IReadOnlyCollection<RestUser> MentionedUsers => MessageHelper.FilterTagsByValue<RestUser>(TagType.UserMention, _tags);
public override IReadOnlyCollection<RestUser> MentionedUsers => _userMentions;
/// <inheritdoc /> /// <inheritdoc />
public override IReadOnlyCollection<ITag> Tags => _tags; public override IReadOnlyCollection<ITag> Tags => _tags;


@@ -67,6 +71,8 @@ namespace Discord.Rest
{ {
_isSuppressed = model.Flags.Value.HasFlag(API.MessageFlags.Suppressed); _isSuppressed = model.Flags.Value.HasFlag(API.MessageFlags.Suppressed);
} }
if (model.RoleMentions.IsSpecified)
_roleMentionIds = model.RoleMentions.Value.ToImmutableArray();


if (model.Attachments.IsSpecified) if (model.Attachments.IsSpecified)
{ {
@@ -96,20 +102,19 @@ namespace Discord.Rest
_embeds = ImmutableArray.Create<Embed>(); _embeds = ImmutableArray.Create<Embed>();
} }


ImmutableArray<IUser> mentions = ImmutableArray.Create<IUser>();
if (model.UserMentions.IsSpecified) if (model.UserMentions.IsSpecified)
{ {
var value = model.UserMentions.Value; var value = model.UserMentions.Value;
if (value.Length > 0) if (value.Length > 0)
{ {
var newMentions = ImmutableArray.CreateBuilder<IUser>(value.Length);
var newMentions = ImmutableArray.CreateBuilder<RestUser>(value.Length);
for (int i = 0; i < value.Length; i++) for (int i = 0; i < value.Length; i++)
{ {
var val = value[i]; var val = value[i];
if (val.Object != null) if (val.Object != null)
newMentions.Add(RestUser.Create(Discord, val.Object)); newMentions.Add(RestUser.Create(Discord, val.Object));
} }
mentions = newMentions.ToImmutable();
_userMentions = newMentions.ToImmutable();
} }
} }


@@ -118,7 +123,7 @@ namespace Discord.Rest
var text = model.Content.Value; var text = model.Content.Value;
var guildId = (Channel as IGuildChannel)?.GuildId; var guildId = (Channel as IGuildChannel)?.GuildId;
var guild = guildId != null ? (Discord as IDiscordClient).GetGuildAsync(guildId.Value, CacheMode.CacheOnly).Result : null; var guild = guildId != null ? (Discord as IDiscordClient).GetGuildAsync(guildId.Value, CacheMode.CacheOnly).Result : null;
_tags = MessageHelper.ParseTags(text, null, guild, mentions);
_tags = MessageHelper.ParseTags(text, null, guild, _userMentions);
model.Content = text; model.Content = text;
} }
} }


+ 2
- 0
src/Discord.Net.Rest/Entities/Users/RestUser.cs View File

@@ -35,6 +35,8 @@ namespace Discord.Rest
/// <inheritdoc /> /// <inheritdoc />
public virtual IImmutableSet<ClientType> ActiveClients => ImmutableHashSet<ClientType>.Empty; public virtual IImmutableSet<ClientType> ActiveClients => ImmutableHashSet<ClientType>.Empty;
/// <inheritdoc /> /// <inheritdoc />
public virtual IImmutableList<IActivity> Activities => ImmutableList<IActivity>.Empty;
/// <inheritdoc />
public virtual bool IsWebhook => false; public virtual bool IsWebhook => false;


internal RestUser(BaseDiscordClient discord, ulong id) internal RestUser(BaseDiscordClient discord, ulong id)


+ 7
- 7
src/Discord.Net.Rest/Net/Queue/ClientBucket.cs View File

@@ -10,14 +10,14 @@ namespace Discord.Net.Queue
internal struct ClientBucket internal struct ClientBucket
{ {
private static readonly ImmutableDictionary<ClientBucketType, ClientBucket> DefsByType; private static readonly ImmutableDictionary<ClientBucketType, ClientBucket> DefsByType;
private static readonly ImmutableDictionary<string, ClientBucket> DefsById;
private static readonly ImmutableDictionary<BucketId, ClientBucket> DefsById;


static ClientBucket() static ClientBucket()
{ {
var buckets = new[] var buckets = new[]
{ {
new ClientBucket(ClientBucketType.Unbucketed, "<unbucketed>", 10, 10),
new ClientBucket(ClientBucketType.SendEdit, "<send_edit>", 10, 10)
new ClientBucket(ClientBucketType.Unbucketed, BucketId.Create(null, "<unbucketed>", null), 10, 10),
new ClientBucket(ClientBucketType.SendEdit, BucketId.Create(null, "<send_edit>", null), 10, 10)
}; };


var builder = ImmutableDictionary.CreateBuilder<ClientBucketType, ClientBucket>(); var builder = ImmutableDictionary.CreateBuilder<ClientBucketType, ClientBucket>();
@@ -25,21 +25,21 @@ namespace Discord.Net.Queue
builder.Add(bucket.Type, bucket); builder.Add(bucket.Type, bucket);
DefsByType = builder.ToImmutable(); DefsByType = builder.ToImmutable();


var builder2 = ImmutableDictionary.CreateBuilder<string, ClientBucket>();
var builder2 = ImmutableDictionary.CreateBuilder<BucketId, ClientBucket>();
foreach (var bucket in buckets) foreach (var bucket in buckets)
builder2.Add(bucket.Id, bucket); builder2.Add(bucket.Id, bucket);
DefsById = builder2.ToImmutable(); DefsById = builder2.ToImmutable();
} }


public static ClientBucket Get(ClientBucketType type) => DefsByType[type]; public static ClientBucket Get(ClientBucketType type) => DefsByType[type];
public static ClientBucket Get(string id) => DefsById[id];
public static ClientBucket Get(BucketId id) => DefsById[id];
public ClientBucketType Type { get; } public ClientBucketType Type { get; }
public string Id { get; }
public BucketId Id { get; }
public int WindowCount { get; } public int WindowCount { get; }
public int WindowSeconds { get; } public int WindowSeconds { get; }


public ClientBucket(ClientBucketType type, string id, int count, int seconds)
public ClientBucket(ClientBucketType type, BucketId id, int count, int seconds)
{ {
Type = type; Type = type;
Id = id; Id = id;


+ 31
- 8
src/Discord.Net.Rest/Net/Queue/RequestQueue.cs View File

@@ -12,9 +12,9 @@ namespace Discord.Net.Queue
{ {
internal class RequestQueue : IDisposable internal class RequestQueue : IDisposable
{ {
public event Func<string, RateLimitInfo?, Task> RateLimitTriggered;
public event Func<BucketId, RateLimitInfo?, Task> RateLimitTriggered;


private readonly ConcurrentDictionary<string, RequestBucket> _buckets;
private readonly ConcurrentDictionary<BucketId, object> _buckets;
private readonly SemaphoreSlim _tokenLock; private readonly SemaphoreSlim _tokenLock;
private readonly CancellationTokenSource _cancelTokenSource; //Dispose token private readonly CancellationTokenSource _cancelTokenSource; //Dispose token
private CancellationTokenSource _clearToken; private CancellationTokenSource _clearToken;
@@ -34,7 +34,7 @@ namespace Discord.Net.Queue
_requestCancelToken = CancellationToken.None; _requestCancelToken = CancellationToken.None;
_parentToken = CancellationToken.None; _parentToken = CancellationToken.None;


_buckets = new ConcurrentDictionary<string, RequestBucket>();
_buckets = new ConcurrentDictionary<BucketId, object>();


_cleanupTask = RunCleanup(); _cleanupTask = RunCleanup();
} }
@@ -82,7 +82,7 @@ namespace Discord.Net.Queue
else else
request.Options.CancelToken = _requestCancelToken; request.Options.CancelToken = _requestCancelToken;


var bucket = GetOrCreateBucket(request.Options.BucketId, request);
var bucket = GetOrCreateBucket(request.Options, request);
var result = await bucket.SendAsync(request).ConfigureAwait(false); var result = await bucket.SendAsync(request).ConfigureAwait(false);
createdTokenSource?.Dispose(); createdTokenSource?.Dispose();
return result; return result;
@@ -110,14 +110,32 @@ namespace Discord.Net.Queue
_waitUntil = DateTimeOffset.UtcNow.AddMilliseconds(info.RetryAfter.Value + (info.Lag?.TotalMilliseconds ?? 0.0)); _waitUntil = DateTimeOffset.UtcNow.AddMilliseconds(info.RetryAfter.Value + (info.Lag?.TotalMilliseconds ?? 0.0));
} }


private RequestBucket GetOrCreateBucket(string id, RestRequest request)
private RequestBucket GetOrCreateBucket(RequestOptions options, RestRequest request)
{ {
return _buckets.GetOrAdd(id, x => new RequestBucket(this, request, x));
var bucketId = options.BucketId;
object obj = _buckets.GetOrAdd(bucketId, x => new RequestBucket(this, request, x));
if (obj is BucketId hashBucket)
{
options.BucketId = hashBucket;
return (RequestBucket)_buckets.GetOrAdd(hashBucket, x => new RequestBucket(this, request, x));
}
return (RequestBucket)obj;
} }
internal async Task RaiseRateLimitTriggered(string bucketId, RateLimitInfo? info)
internal async Task RaiseRateLimitTriggered(BucketId bucketId, RateLimitInfo? info)
{ {
await RateLimitTriggered(bucketId, info).ConfigureAwait(false); await RateLimitTriggered(bucketId, info).ConfigureAwait(false);
} }
internal (RequestBucket, BucketId) UpdateBucketHash(BucketId id, string discordHash)
{
if (!id.IsHashBucket)
{
var bucket = BucketId.Create(discordHash, id);
var hashReqQueue = (RequestBucket)_buckets.GetOrAdd(bucket, _buckets[id]);
_buckets.AddOrUpdate(id, bucket, (oldBucket, oldObj) => bucket);
return (hashReqQueue, bucket);
}
return (null, null);
}


private async Task RunCleanup() private async Task RunCleanup()
{ {
@@ -126,10 +144,15 @@ namespace Discord.Net.Queue
while (!_cancelTokenSource.IsCancellationRequested) while (!_cancelTokenSource.IsCancellationRequested)
{ {
var now = DateTimeOffset.UtcNow; var now = DateTimeOffset.UtcNow;
foreach (var bucket in _buckets.Select(x => x.Value))
foreach (var bucket in _buckets.Where(x => x.Value is RequestBucket).Select(x => (RequestBucket)x.Value))
{ {
if ((now - bucket.LastAttemptAt).TotalMinutes > 1.0) if ((now - bucket.LastAttemptAt).TotalMinutes > 1.0)
{
if (bucket.Id.IsHashBucket)
foreach (var redirectBucket in _buckets.Where(x => x.Value == bucket.Id).Select(x => (BucketId)x.Value))
_buckets.TryRemove(redirectBucket, out _); //remove redirections if hash bucket
_buckets.TryRemove(bucket.Id, out _); _buckets.TryRemove(bucket.Id, out _);
}
} }
await Task.Delay(60000, _cancelTokenSource.Token).ConfigureAwait(false); //Runs each minute await Task.Delay(60000, _cancelTokenSource.Token).ConfigureAwait(false); //Runs each minute
} }


+ 57
- 17
src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs View File

@@ -19,12 +19,13 @@ namespace Discord.Net.Queue
private readonly RequestQueue _queue; private readonly RequestQueue _queue;
private int _semaphore; private int _semaphore;
private DateTimeOffset? _resetTick; private DateTimeOffset? _resetTick;
private RequestBucket _redirectBucket;


public string Id { get; private set; }
public BucketId Id { get; private set; }
public int WindowCount { get; private set; } public int WindowCount { get; private set; }
public DateTimeOffset LastAttemptAt { get; private set; } public DateTimeOffset LastAttemptAt { get; private set; }


public RequestBucket(RequestQueue queue, RestRequest request, string id)
public RequestBucket(RequestQueue queue, RestRequest request, BucketId id)
{ {
_queue = queue; _queue = queue;
Id = id; Id = id;
@@ -32,7 +33,7 @@ namespace Discord.Net.Queue
_lock = new object(); _lock = new object();


if (request.Options.IsClientBucket) if (request.Options.IsClientBucket)
WindowCount = ClientBucket.Get(request.Options.BucketId).WindowCount;
WindowCount = ClientBucket.Get(Id).WindowCount;
else else
WindowCount = 1; //Only allow one request until we get a header back WindowCount = 1; //Only allow one request until we get a header back
_semaphore = WindowCount; _semaphore = WindowCount;
@@ -52,6 +53,8 @@ namespace Discord.Net.Queue
{ {
await _queue.EnterGlobalAsync(id, request).ConfigureAwait(false); await _queue.EnterGlobalAsync(id, request).ConfigureAwait(false);
await EnterAsync(id, request).ConfigureAwait(false); await EnterAsync(id, request).ConfigureAwait(false);
if (_redirectBucket != null)
return await _redirectBucket.SendAsync(request);


#if DEBUG_LIMITS #if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Sending..."); Debug.WriteLine($"[{id}] Sending...");
@@ -160,6 +163,9 @@ namespace Discord.Net.Queue


while (true) while (true)
{ {
if (_redirectBucket != null)
break;

if (DateTimeOffset.UtcNow > request.TimeoutAt || request.Options.CancelToken.IsCancellationRequested) if (DateTimeOffset.UtcNow > request.TimeoutAt || request.Options.CancelToken.IsCancellationRequested)
{ {
if (!isRateLimited) if (!isRateLimited)
@@ -175,7 +181,8 @@ namespace Discord.Net.Queue
} }


DateTimeOffset? timeoutAt = request.TimeoutAt; DateTimeOffset? timeoutAt = request.TimeoutAt;
if (windowCount > 0 && Interlocked.Decrement(ref _semaphore) < 0)
int semaphore = Interlocked.Decrement(ref _semaphore);
if (windowCount > 0 && semaphore < 0)
{ {
if (!isRateLimited) if (!isRateLimited)
{ {
@@ -210,20 +217,52 @@ namespace Discord.Net.Queue
} }
#if DEBUG_LIMITS #if DEBUG_LIMITS
else else
Debug.WriteLine($"[{id}] Entered Semaphore ({_semaphore}/{WindowCount} remaining)");
Debug.WriteLine($"[{id}] Entered Semaphore ({semaphore}/{WindowCount} remaining)");
#endif #endif
break; break;
} }
} }


private void UpdateRateLimit(int id, RestRequest request, RateLimitInfo info, bool is429)
private void UpdateRateLimit(int id, RestRequest request, RateLimitInfo info, bool is429, bool redirected = false)
{ {
if (WindowCount == 0) if (WindowCount == 0)
return; return;


lock (_lock) lock (_lock)
{ {
if (redirected)
{
Interlocked.Decrement(ref _semaphore); //we might still hit a real ratelimit if all tickets were already taken, can't do much about it since we didn't know they were the same
#if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Decrease Semaphore");
#endif
}
bool hasQueuedReset = _resetTick != null; bool hasQueuedReset = _resetTick != null;

if (info.Bucket != null && !redirected)
{
(RequestBucket, BucketId) hashBucket = _queue.UpdateBucketHash(Id, info.Bucket);
if (!(hashBucket.Item1 is null) && !(hashBucket.Item2 is null))
{
if (hashBucket.Item1 == this) //this bucket got promoted to a hash queue
{
Id = hashBucket.Item2;
#if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Promoted to Hash Bucket ({hashBucket.Item2})");
#endif
}
else
{
_redirectBucket = hashBucket.Item1; //this request should be part of another bucket, this bucket will be disabled, redirect everything
_redirectBucket.UpdateRateLimit(id, request, info, is429, redirected: true); //update the hash bucket ratelimit
#if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Redirected to {_redirectBucket.Id}");
#endif
return;
}
}
}

if (info.Limit.HasValue && WindowCount != info.Limit.Value) if (info.Limit.HasValue && WindowCount != info.Limit.Value)
{ {
WindowCount = info.Limit.Value; WindowCount = info.Limit.Value;
@@ -233,7 +272,6 @@ namespace Discord.Net.Queue
#endif #endif
} }


var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
DateTimeOffset? resetTick = null; DateTimeOffset? resetTick = null;


//Using X-RateLimit-Remaining causes a race condition //Using X-RateLimit-Remaining causes a race condition
@@ -250,16 +288,18 @@ namespace Discord.Net.Queue
Debug.WriteLine($"[{id}] Retry-After: {info.RetryAfter.Value} ({info.RetryAfter.Value} ms)"); Debug.WriteLine($"[{id}] Retry-After: {info.RetryAfter.Value} ({info.RetryAfter.Value} ms)");
#endif #endif
} }
else if (info.ResetAfter.HasValue && (request.Options.UseSystemClock.HasValue ? !request.Options.UseSystemClock.Value : false))
{
resetTick = DateTimeOffset.UtcNow.Add(info.ResetAfter.Value);
}
else if (info.ResetAfter.HasValue && (request.Options.UseSystemClock.HasValue ? !request.Options.UseSystemClock.Value : false))
{
resetTick = DateTimeOffset.UtcNow.Add(info.ResetAfter.Value);
#if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Reset-After: {info.ResetAfter.Value} ({info.ResetAfter?.TotalMilliseconds} ms)");
#endif
}
else if (info.Reset.HasValue) else if (info.Reset.HasValue)
{ {
resetTick = info.Reset.Value.AddSeconds(info.Lag?.TotalSeconds ?? 1.0); resetTick = info.Reset.Value.AddSeconds(info.Lag?.TotalSeconds ?? 1.0);


/* millisecond precision makes this unnecessary, retaining in case of regression

/* millisecond precision makes this unnecessary, retaining in case of regression
if (request.Options.IsReactionBucket) if (request.Options.IsReactionBucket)
resetTick = DateTimeOffset.Now.AddMilliseconds(250); resetTick = DateTimeOffset.Now.AddMilliseconds(250);
*/ */
@@ -269,17 +309,17 @@ namespace Discord.Net.Queue
Debug.WriteLine($"[{id}] X-RateLimit-Reset: {info.Reset.Value.ToUnixTimeSeconds()} ({diff} ms, {info.Lag?.TotalMilliseconds} ms lag)"); Debug.WriteLine($"[{id}] X-RateLimit-Reset: {info.Reset.Value.ToUnixTimeSeconds()} ({diff} ms, {info.Lag?.TotalMilliseconds} ms lag)");
#endif #endif
} }
else if (request.Options.IsClientBucket && request.Options.BucketId != null)
else if (request.Options.IsClientBucket && Id != null)
{ {
resetTick = DateTimeOffset.UtcNow.AddSeconds(ClientBucket.Get(request.Options.BucketId).WindowSeconds);
resetTick = DateTimeOffset.UtcNow.AddSeconds(ClientBucket.Get(Id).WindowSeconds);
#if DEBUG_LIMITS #if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Client Bucket ({ClientBucket.Get(request.Options.BucketId).WindowSeconds * 1000} ms)");
Debug.WriteLine($"[{id}] Client Bucket ({ClientBucket.Get(Id).WindowSeconds * 1000} ms)");
#endif #endif
} }


if (resetTick == null) if (resetTick == null)
{ {
WindowCount = 0; //No rate limit info, disable limits on this bucket (should only ever happen with a user token)
WindowCount = 0; //No rate limit info, disable limits on this bucket
#if DEBUG_LIMITS #if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Disabled Semaphore"); Debug.WriteLine($"[{id}] Disabled Semaphore");
#endif #endif


+ 5
- 3
src/Discord.Net.Rest/Net/RateLimitInfo.cs View File

@@ -11,7 +11,8 @@ namespace Discord.Net
public int? Remaining { get; } public int? Remaining { get; }
public int? RetryAfter { get; } public int? RetryAfter { get; }
public DateTimeOffset? Reset { get; } public DateTimeOffset? Reset { get; }
public TimeSpan? ResetAfter { get; }
public TimeSpan? ResetAfter { get; }
public string Bucket { get; }
public TimeSpan? Lag { get; } public TimeSpan? Lag { get; }


internal RateLimitInfo(Dictionary<string, string> headers) internal RateLimitInfo(Dictionary<string, string> headers)
@@ -26,8 +27,9 @@ namespace Discord.Net
double.TryParse(temp, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var reset) ? DateTimeOffset.FromUnixTimeMilliseconds((long)(reset * 1000)) : (DateTimeOffset?)null; double.TryParse(temp, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var reset) ? DateTimeOffset.FromUnixTimeMilliseconds((long)(reset * 1000)) : (DateTimeOffset?)null;
RetryAfter = headers.TryGetValue("Retry-After", out temp) && RetryAfter = headers.TryGetValue("Retry-After", out temp) &&
int.TryParse(temp, NumberStyles.None, CultureInfo.InvariantCulture, out var retryAfter) ? retryAfter : (int?)null; int.TryParse(temp, NumberStyles.None, CultureInfo.InvariantCulture, out var retryAfter) ? retryAfter : (int?)null;
ResetAfter = headers.TryGetValue("X-RateLimit-Reset-After", out temp) &&
float.TryParse(temp, out var resetAfter) ? TimeSpan.FromMilliseconds((long)(resetAfter * 1000)) : (TimeSpan?)null;
ResetAfter = headers.TryGetValue("X-RateLimit-Reset-After", out temp) &&
double.TryParse(temp, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var resetAfter) ? TimeSpan.FromMilliseconds((long)(resetAfter * 1000)) : (TimeSpan?)null;
Bucket = headers.TryGetValue("X-RateLimit-Bucket", out temp) ? temp : null;
Lag = headers.TryGetValue("Date", out temp) && Lag = headers.TryGetValue("Date", out temp) &&
DateTimeOffset.TryParse(temp, CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) ? DateTimeOffset.UtcNow - date : (TimeSpan?)null; DateTimeOffset.TryParse(temp, CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) ? DateTimeOffset.UtcNow - date : (TimeSpan?)null;
} }


+ 3
- 1
src/Discord.Net.WebSocket/API/Gateway/IdentifyParams.cs View File

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


@@ -17,5 +17,7 @@ namespace Discord.API.Gateway
public Optional<int[]> ShardingParams { get; set; } public Optional<int[]> ShardingParams { get; set; }
[JsonProperty("guild_subscriptions")] [JsonProperty("guild_subscriptions")]
public Optional<bool> GuildSubscriptions { get; set; } public Optional<bool> GuildSubscriptions { get; set; }
[JsonProperty("intents")]
public Optional<int> Intents { get; set; }
} }
} }

+ 12
- 1
src/Discord.Net.WebSocket/ConnectionManager.cs View File

@@ -44,6 +44,8 @@ namespace Discord
var ex2 = ex as WebSocketClosedException; var ex2 = ex as WebSocketClosedException;
if (ex2?.CloseCode == 4006) if (ex2?.CloseCode == 4006)
CriticalError(new Exception("WebSocket session expired", ex)); CriticalError(new Exception("WebSocket session expired", ex));
else if (ex2?.CloseCode == 4014)
CriticalError(new Exception("WebSocket connection was closed", ex));
else else
Error(new Exception("WebSocket connection was closed", ex)); Error(new Exception("WebSocket connection was closed", ex));
} }
@@ -141,7 +143,16 @@ namespace Discord
catch (OperationCanceledException) { } catch (OperationCanceledException) { }
}); });


await _onConnecting().ConfigureAwait(false);
try
{
await _onConnecting().ConfigureAwait(false);
}
catch (TaskCanceledException ex)
{
Exception innerEx = ex.InnerException ?? new OperationCanceledException("Failed to connect.");
Error(innerEx);
throw innerEx;
}


await _logger.InfoAsync("Connected").ConfigureAwait(false); await _logger.InfoAsync("Connected").ConfigureAwait(false);
State = ConnectionState.Connected; State = ConnectionState.Connected;


+ 7
- 3
src/Discord.Net.WebSocket/DiscordSocketApiClient.cs View File

@@ -209,7 +209,7 @@ namespace Discord.API
await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false); await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false);
} }


public async Task SendIdentifyAsync(int largeThreshold = 100, int shardID = 0, int totalShards = 1, bool guildSubscriptions = true, RequestOptions options = null)
public async Task SendIdentifyAsync(int largeThreshold = 100, int shardID = 0, int totalShards = 1, bool guildSubscriptions = true, GatewayIntents? gatewayIntents = null, RequestOptions options = null)
{ {
options = RequestOptions.CreateOrClone(options); options = RequestOptions.CreateOrClone(options);
var props = new Dictionary<string, string> var props = new Dictionary<string, string>
@@ -220,12 +220,16 @@ namespace Discord.API
{ {
Token = AuthToken, Token = AuthToken,
Properties = props, Properties = props,
LargeThreshold = largeThreshold,
GuildSubscriptions = guildSubscriptions
LargeThreshold = largeThreshold
}; };
if (totalShards > 1) if (totalShards > 1)
msg.ShardingParams = new int[] { shardID, totalShards }; msg.ShardingParams = new int[] { shardID, totalShards };


if (gatewayIntents.HasValue)
msg.Intents = (int)gatewayIntents.Value;
else
msg.GuildSubscriptions = guildSubscriptions;

await SendGatewayAsync(GatewayOpCode.Identify, msg, options: options).ConfigureAwait(false); await SendGatewayAsync(GatewayOpCode.Identify, msg, options: options).ConfigureAwait(false);
} }
public async Task SendResumeAsync(string sessionId, int lastSeq, RequestOptions options = null) public async Task SendResumeAsync(string sessionId, int lastSeq, RequestOptions options = null)


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

@@ -21,7 +21,13 @@ namespace Discord.WebSocket
remove { _disconnectedEvent.Remove(value); } remove { _disconnectedEvent.Remove(value); }
} }
private readonly AsyncEvent<Func<Exception, Task>> _disconnectedEvent = new AsyncEvent<Func<Exception, Task>>(); private readonly AsyncEvent<Func<Exception, Task>> _disconnectedEvent = new AsyncEvent<Func<Exception, Task>>();
/// <summary> Fired when guild data has finished downloading. </summary>
/// <summary>
/// Fired when guild data has finished downloading.
/// </summary>
/// <remarks>
/// It is possible that some guilds might be unsynced if <see cref="DiscordSocketConfig.MaxWaitBetweenGuildAvailablesBeforeReady" />
/// was not long enough to receive all GUILD_AVAILABLEs before READY.
/// </remarks>
public event Func<Task> Ready public event Func<Task> Ready
{ {
add { _readyEvent.Add(value); } add { _readyEvent.Add(value); }


+ 20
- 8
src/Discord.Net.WebSocket/DiscordSocketClient.cs View File

@@ -44,6 +44,7 @@ namespace Discord.WebSocket
private RestApplication _applicationInfo; private RestApplication _applicationInfo;
private bool _isDisposed; private bool _isDisposed;
private bool _guildSubscriptions; private bool _guildSubscriptions;
private GatewayIntents? _gatewayIntents;


/// <summary> /// <summary>
/// Provides access to a REST-only client with a shared state from this client. /// Provides access to a REST-only client with a shared state from this client.
@@ -137,6 +138,7 @@ namespace Discord.WebSocket
Rest = new DiscordSocketRestClient(config, ApiClient); Rest = new DiscordSocketRestClient(config, ApiClient);
_heartbeatTimes = new ConcurrentQueue<long>(); _heartbeatTimes = new ConcurrentQueue<long>();
_guildSubscriptions = config.GuildSubscriptions; _guildSubscriptions = config.GuildSubscriptions;
_gatewayIntents = config.GatewayIntents;


_stateLock = new SemaphoreSlim(1, 1); _stateLock = new SemaphoreSlim(1, 1);
_gatewayLogger = LogManager.CreateLogger(ShardId == 0 && TotalShards == 1 ? "Gateway" : $"Shard #{ShardId}"); _gatewayLogger = LogManager.CreateLogger(ShardId == 0 && TotalShards == 1 ? "Gateway" : $"Shard #{ShardId}");
@@ -167,7 +169,7 @@ namespace Discord.WebSocket


GuildAvailable += g => GuildAvailable += g =>
{ {
if (ConnectionState == ConnectionState.Connected && AlwaysDownloadUsers && !g.HasAllMembers)
if (_guildDownloadTask?.IsCompleted == true && ConnectionState == ConnectionState.Connected && AlwaysDownloadUsers && !g.HasAllMembers)
{ {
var _ = g.DownloadUsersAsync(); var _ = g.DownloadUsersAsync();
} }
@@ -242,7 +244,7 @@ namespace Discord.WebSocket
else else
{ {
await _gatewayLogger.DebugAsync("Identifying").ConfigureAwait(false); await _gatewayLogger.DebugAsync("Identifying").ConfigureAwait(false);
await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards, guildSubscriptions: _guildSubscriptions).ConfigureAwait(false);
await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards, guildSubscriptions: _guildSubscriptions, gatewayIntents: _gatewayIntents).ConfigureAwait(false);
} }


//Wait for READY //Wait for READY
@@ -340,7 +342,7 @@ namespace Discord.WebSocket
{ {
var user = SocketGlobalUser.Create(this, state, model); var user = SocketGlobalUser.Create(this, state, model);
user.GlobalUser.AddRef(); user.GlobalUser.AddRef();
user.Presence = new SocketPresence(UserStatus.Online, null, null);
user.Presence = new SocketPresence(UserStatus.Online, null, null, null);
return user; return user;
}); });
} }
@@ -368,7 +370,7 @@ namespace Discord.WebSocket
{ {
var cachedGuilds = guilds.ToImmutableArray(); var cachedGuilds = guilds.ToImmutableArray();


const short batchSize = 50;
int batchSize = _gatewayIntents.HasValue ? 1 : 100;
ulong[] batchIds = new ulong[Math.Min(batchSize, cachedGuilds.Length)]; ulong[] batchIds = new ulong[Math.Min(batchSize, cachedGuilds.Length)];
Task[] batchTasks = new Task[batchIds.Length]; Task[] batchTasks = new Task[batchIds.Length];
int batchCount = (cachedGuilds.Length + (batchSize - 1)) / batchSize; int batchCount = (cachedGuilds.Length + (batchSize - 1)) / batchSize;
@@ -376,7 +378,7 @@ namespace Discord.WebSocket
for (int i = 0, k = 0; i < batchCount; i++) for (int i = 0, k = 0; i < batchCount; i++)
{ {
bool isLast = i == batchCount - 1; bool isLast = i == batchCount - 1;
int count = isLast ? (batchIds.Length - (batchCount - 1) * batchSize) : batchSize;
int count = isLast ? (cachedGuilds.Length - (batchCount - 1) * batchSize) : batchSize;


for (int j = 0; j < count; j++, k++) for (int j = 0; j < count; j++, k++)
{ {
@@ -448,7 +450,7 @@ namespace Discord.WebSocket
return; return;
var status = Status; var status = Status;
var statusSince = _statusSince; var statusSince = _statusSince;
CurrentUser.Presence = new SocketPresence(status, Activity, null);
CurrentUser.Presence = new SocketPresence(status, Activity, null, null);


var gameModel = new GameModel(); var gameModel = new GameModel();
// Discord only accepts rich presence over RPC, don't even bother building a payload // Discord only accepts rich presence over RPC, don't even bother building a payload
@@ -517,7 +519,7 @@ namespace Discord.WebSocket
_sessionId = null; _sessionId = null;
_lastSeq = 0; _lastSeq = 0;


await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards).ConfigureAwait(false);
await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards, guildSubscriptions: _guildSubscriptions, gatewayIntents: _gatewayIntents).ConfigureAwait(false);
} }
break; break;
case GatewayOpCode.Reconnect: case GatewayOpCode.Reconnect:
@@ -576,6 +578,9 @@ namespace Discord.WebSocket
} }
else if (_connection.CancelToken.IsCancellationRequested) else if (_connection.CancelToken.IsCancellationRequested)
return; return;
if (BaseConfig.AlwaysDownloadUsers)
_ = DownloadUsersAsync(Guilds.Where(x => x.IsAvailable && !x.HasAllMembers));


await TimedInvokeAsync(_readyEvent, nameof(Ready)).ConfigureAwait(false); await TimedInvokeAsync(_readyEvent, nameof(Ready)).ConfigureAwait(false);
await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false); await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false);
@@ -898,6 +903,13 @@ namespace Discord.WebSocket


if (user != null) if (user != null)
{ {
var globalBefore = user.GlobalUser.Clone();
if (user.GlobalUser.Update(State, data.User))
{
//Global data was updated, trigger UserUpdated
await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false);
}

var before = user.Clone(); var before = user.Clone();
user.Update(State, data); user.Update(State, data);
await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), before, user).ConfigureAwait(false); await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), before, user).ConfigureAwait(false);
@@ -1770,7 +1782,7 @@ namespace Discord.WebSocket
try try
{ {
await logger.DebugAsync("GuildDownloader Started").ConfigureAwait(false); await logger.DebugAsync("GuildDownloader Started").ConfigureAwait(false);
while ((_unavailableGuildCount != 0) && (Environment.TickCount - _lastGuildAvailableTime < 2000))
while ((_unavailableGuildCount != 0) && (Environment.TickCount - _lastGuildAvailableTime < BaseConfig.MaxWaitBetweenGuildAvailablesBeforeReady))
await Task.Delay(500, cancelToken).ConfigureAwait(false); await Task.Delay(500, cancelToken).ConfigureAwait(false);
await logger.DebugAsync("GuildDownloader Stopped").ConfigureAwait(false); await logger.DebugAsync("GuildDownloader Stopped").ConfigureAwait(false);
} }


+ 37
- 1
src/Discord.Net.WebSocket/DiscordSocketConfig.cs View File

@@ -87,7 +87,7 @@ namespace Discord.WebSocket
/// </para> /// </para>
/// <para> /// <para>
/// For more information, please see /// For more information, please see
/// <see href="https://discordapp.com/developers/docs/topics/gateway#request-guild-members">Request Guild Members</see>
/// <see href="https://discord.com/developers/docs/topics/gateway#request-guild-members">Request Guild Members</see>
/// on the official Discord API documentation. /// on the official Discord API documentation.
/// </para> /// </para>
/// <note> /// <note>
@@ -121,9 +121,45 @@ namespace Discord.WebSocket


/// <summary> /// <summary>
/// Gets or sets enabling dispatching of guild subscription events e.g. presence and typing events. /// Gets or sets enabling dispatching of guild subscription events e.g. presence and typing events.
/// This is not used if <see cref="GatewayIntents"/> are provided.
/// </summary> /// </summary>
public bool GuildSubscriptions { get; set; } = true; public bool GuildSubscriptions { get; set; } = true;


/// <summary>
/// Gets or sets the maximum wait time in milliseconds between GUILD_AVAILABLE events before firing READY.
///
/// If zero, READY will fire as soon as it is received and all guilds will be unavailable.
/// </summary>
/// <remarks>
/// <para>This property is measured in milliseconds, negative values will throw an exception.</para>
/// <para>If a guild is not received before READY, it will be unavailable.</para>
/// </remarks>
/// <returns>
/// The maximum wait time in milliseconds between GUILD_AVAILABLE events before firing READY.
/// </returns>
/// <exception cref="System.ArgumentException">Value must be at least 0.</exception>
public int MaxWaitBetweenGuildAvailablesBeforeReady {
get
{
return _maxWaitForGuildAvailable;
}
set
{
Preconditions.AtLeast(value, 0, nameof(MaxWaitBetweenGuildAvailablesBeforeReady));
_maxWaitForGuildAvailable = value;
}
}
private int _maxWaitForGuildAvailable = 10000;
/// Gets or sets gateway intents to limit what events are sent from Discord. Allows for more granular control than the <see cref="GuildSubscriptions"/> property.
/// </summary>
/// <remarks>
/// For more information, please see
/// <see href="https://discord.com/developers/docs/topics/gateway#gateway-intents">GatewayIntents</see>
/// on the official Discord API documentation.
/// </remarks>
public GatewayIntents? GatewayIntents { get; set; }

/// <summary> /// <summary>
/// Initializes a default configuration. /// Initializes a default configuration.
/// </summary> /// </summary>


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

@@ -42,7 +42,7 @@ namespace Discord.WebSocket
/// Sends a file to this message channel with an optional caption. /// Sends a file to this message channel with an optional caption.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This method follows the same behavior as described in <see cref="IMessageChannel.SendFileAsync(string, string, bool, Embed, RequestOptions, bool)"/>.
/// This method follows the same behavior as described in <see cref="IMessageChannel.SendFileAsync(string, string, bool, Embed, RequestOptions, bool, AllowedMentions)"/>.
/// Please visit its documentation for more details on this method. /// Please visit its documentation for more details on this method.
/// </remarks> /// </remarks>
/// <param name="filePath">The file path of the file.</param> /// <param name="filePath">The file path of the file.</param>
@@ -51,16 +51,20 @@ namespace Discord.WebSocket
/// <param name="embed">The <see cref="Discord.EmbedType.Rich" /> <see cref="Embed" /> to be sent.</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="options">The options to be used when sending the request.</param>
/// <param name="isSpoiler">Whether the message attachment should be hidden as a spoiler.</param> /// <param name="isSpoiler">Whether the message attachment should be hidden as a spoiler.</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> /// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result /// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message. /// contains the sent message.
/// </returns> /// </returns>
new Task<RestUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false);
new Task<RestUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null);
/// <summary> /// <summary>
/// Sends a file to this message channel with an optional caption. /// Sends a file to this message channel with an optional caption.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This method follows the same behavior as described in <see cref="IMessageChannel.SendFileAsync(Stream, string, string, bool, Embed, RequestOptions, bool)"/>.
/// This method follows the same behavior as described in <see cref="IMessageChannel.SendFileAsync(Stream, string, string, bool, Embed, RequestOptions, bool, AllowedMentions)"/>.
/// Please visit its documentation for more details on this method. /// Please visit its documentation for more details on this method.
/// </remarks> /// </remarks>
/// <param name="stream">The <see cref="Stream" /> of the file to be sent.</param> /// <param name="stream">The <see cref="Stream" /> of the file to be sent.</param>
@@ -70,11 +74,15 @@ namespace Discord.WebSocket
/// <param name="embed">The <see cref="Discord.EmbedType.Rich"/> <see cref="Embed"/> to be sent.</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="options">The options to be used when sending the request.</param>
/// <param name="isSpoiler">Whether the message attachment should be hidden as a spoiler.</param> /// <param name="isSpoiler">Whether the message attachment should be hidden as a spoiler.</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> /// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result /// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message. /// contains the sent message.
/// </returns> /// </returns>
new Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false);
new Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null);


/// <summary> /// <summary>
/// Gets a cached message from this channel. /// Gets a cached message from this channel.


+ 24
- 19
src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs View File

@@ -11,23 +11,11 @@ namespace Discord.WebSocket
public static IAsyncEnumerable<IReadOnlyCollection<IMessage>> GetMessagesAsync(ISocketMessageChannel channel, DiscordSocketClient discord, MessageCache messages, public static IAsyncEnumerable<IReadOnlyCollection<IMessage>> GetMessagesAsync(ISocketMessageChannel channel, DiscordSocketClient discord, MessageCache messages,
ulong? fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) ulong? fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options)
{ {
if (dir == Direction.Around)
throw new NotImplementedException(); //TODO: Impl

IReadOnlyCollection<SocketMessage> cachedMessages = null;
IAsyncEnumerable<IReadOnlyCollection<IMessage>> result = null;
if (dir == Direction.After && fromMessageId == null) if (dir == Direction.After && fromMessageId == null)
return AsyncEnumerable.Empty<IReadOnlyCollection<IMessage>>(); return AsyncEnumerable.Empty<IReadOnlyCollection<IMessage>>();


if (dir == Direction.Before || mode == CacheMode.CacheOnly)
{
if (messages != null) //Cache enabled
cachedMessages = messages.GetMany(fromMessageId, dir, limit);
else
cachedMessages = ImmutableArray.Create<SocketMessage>();
result = ImmutableArray.Create(cachedMessages).ToAsyncEnumerable<IReadOnlyCollection<IMessage>>();
}
var cachedMessages = GetCachedMessages(channel, discord, messages, fromMessageId, dir, limit);
var result = ImmutableArray.Create(cachedMessages).ToAsyncEnumerable<IReadOnlyCollection<IMessage>>();


if (dir == Direction.Before) if (dir == Direction.Before)
{ {
@@ -38,18 +26,35 @@ namespace Discord.WebSocket
//Download remaining messages //Download remaining messages
ulong? minId = cachedMessages.Count > 0 ? cachedMessages.Min(x => x.Id) : fromMessageId; ulong? minId = cachedMessages.Count > 0 ? cachedMessages.Min(x => x.Id) : fromMessageId;
var downloadedMessages = ChannelHelper.GetMessagesAsync(channel, discord, minId, dir, limit, options); var downloadedMessages = ChannelHelper.GetMessagesAsync(channel, discord, minId, dir, limit, options);
return result.Concat(downloadedMessages);
if (cachedMessages.Count != 0)
return result.Concat(downloadedMessages);
else
return downloadedMessages;
} }
else
else if (dir == Direction.After)
{
limit -= cachedMessages.Count;
if (mode == CacheMode.CacheOnly || limit <= 0)
return result;

//Download remaining messages
ulong maxId = cachedMessages.Count > 0 ? cachedMessages.Max(x => x.Id) : fromMessageId.Value;
var downloadedMessages = ChannelHelper.GetMessagesAsync(channel, discord, maxId, dir, limit, options);
if (cachedMessages.Count != 0)
return result.Concat(downloadedMessages);
else
return downloadedMessages;
}
else //Direction.Around
{ {
if (mode == CacheMode.CacheOnly)
if (mode == CacheMode.CacheOnly || limit <= cachedMessages.Count)
return result; return result;


//Dont use cache in this case
//Cache isn't useful here since Discord will send them anyways
return ChannelHelper.GetMessagesAsync(channel, discord, fromMessageId, dir, limit, options); return ChannelHelper.GetMessagesAsync(channel, discord, fromMessageId, dir, limit, options);
} }
} }
public static IReadOnlyCollection<SocketMessage> GetCachedMessages(SocketChannel channel, DiscordSocketClient discord, MessageCache messages,
public static IReadOnlyCollection<SocketMessage> GetCachedMessages(ISocketMessageChannel channel, DiscordSocketClient discord, MessageCache messages,
ulong? fromMessageId, Direction dir, int limit) ulong? fromMessageId, Direction dir, int limit)
{ {
if (messages != null) //Cache enabled if (messages != null) //Cache enabled


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

@@ -139,12 +139,12 @@ namespace Discord.WebSocket
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options); => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options);


/// <inheritdoc /> /// <inheritdoc />
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false)
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler);
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, options, isSpoiler);
/// <inheritdoc /> /// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false)
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler);
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, options, isSpoiler);
/// <inheritdoc /> /// <inheritdoc />
public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null)
=> ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options); => ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options);
@@ -229,11 +229,11 @@ namespace Discord.WebSocket
async Task<IReadOnlyCollection<IMessage>> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) async Task<IReadOnlyCollection<IMessage>> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options)
=> await GetPinnedMessagesAsync(options).ConfigureAwait(false); => await GetPinnedMessagesAsync(options).ConfigureAwait(false);
/// <inheritdoc /> /// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler)
=> await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false);
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions)
=> await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions).ConfigureAwait(false);
/// <inheritdoc /> /// <inheritdoc />
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.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions)
=> await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions).ConfigureAwait(false);
/// <inheritdoc /> /// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions)
=> await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false);


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

@@ -167,11 +167,11 @@ namespace Discord.WebSocket
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options); => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options);


/// <inheritdoc /> /// <inheritdoc />
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false)
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler);
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, options, isSpoiler);
/// <inheritdoc /> /// <inheritdoc />
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false)
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler);
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, options, isSpoiler);


/// <inheritdoc /> /// <inheritdoc />
public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null)
@@ -293,11 +293,11 @@ namespace Discord.WebSocket
=> await GetPinnedMessagesAsync(options).ConfigureAwait(false); => await GetPinnedMessagesAsync(options).ConfigureAwait(false);


/// <inheritdoc /> /// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler)
=> await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false);
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions)
=> await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions).ConfigureAwait(false);
/// <inheritdoc /> /// <inheritdoc />
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.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions)
=> await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions).ConfigureAwait(false);
/// <inheritdoc /> /// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions)
=> await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false);


+ 0
- 20
src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs View File

@@ -125,7 +125,6 @@ namespace Discord.WebSocket
public virtual async Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null) public virtual async Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null)
{ {
await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, user, permissions, options).ConfigureAwait(false); await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, user, permissions, options).ConfigureAwait(false);
_overwrites = _overwrites.Add(new Overwrite(user.Id, PermissionTarget.User, new OverwritePermissions(permissions.AllowValue, permissions.DenyValue)));
} }


/// <summary> /// <summary>
@@ -140,7 +139,6 @@ namespace Discord.WebSocket
public virtual async Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) public virtual async Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null)
{ {
await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, role, permissions, options).ConfigureAwait(false); await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, role, permissions, options).ConfigureAwait(false);
_overwrites = _overwrites.Add(new Overwrite(role.Id, PermissionTarget.Role, new OverwritePermissions(permissions.AllowValue, permissions.DenyValue)));
} }
/// <summary> /// <summary>
/// Removes the permission overwrite for the given user, if one exists. /// Removes the permission overwrite for the given user, if one exists.
@@ -153,15 +151,6 @@ namespace Discord.WebSocket
public virtual async Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null) public virtual async Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null)
{ {
await ChannelHelper.RemovePermissionOverwriteAsync(this, Discord, user, options).ConfigureAwait(false); await ChannelHelper.RemovePermissionOverwriteAsync(this, Discord, user, options).ConfigureAwait(false);

for (int i = 0; i < _overwrites.Length; i++)
{
if (_overwrites[i].TargetId == user.Id)
{
_overwrites = _overwrites.RemoveAt(i);
return;
}
}
} }
/// <summary> /// <summary>
/// Removes the permission overwrite for the given role, if one exists. /// Removes the permission overwrite for the given role, if one exists.
@@ -174,15 +163,6 @@ namespace Discord.WebSocket
public virtual async Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) public virtual async Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null)
{ {
await ChannelHelper.RemovePermissionOverwriteAsync(this, Discord, role, options).ConfigureAwait(false); await ChannelHelper.RemovePermissionOverwriteAsync(this, Discord, role, options).ConfigureAwait(false);

for (int i = 0; i < _overwrites.Length; i++)
{
if (_overwrites[i].TargetId == role.Id)
{
_overwrites = _overwrites.RemoveAt(i);
return;
}
}
} }


public new virtual SocketGuildUser GetUser(ulong id) => null; public new virtual SocketGuildUser GetUser(ulong id) => null;


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

@@ -165,13 +165,13 @@ namespace Discord.WebSocket
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options); => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options);


/// <inheritdoc /> /// <inheritdoc />
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false)
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler);
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, options, isSpoiler);


/// <inheritdoc /> /// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false)
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler);
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, options, isSpoiler);


/// <inheritdoc /> /// <inheritdoc />
public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null) public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null)
@@ -302,11 +302,11 @@ namespace Discord.WebSocket
=> await GetPinnedMessagesAsync(options).ConfigureAwait(false); => await GetPinnedMessagesAsync(options).ConfigureAwait(false);


/// <inheritdoc /> /// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler)
=> await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false);
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions)
=> await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions).ConfigureAwait(false);
/// <inheritdoc /> /// <inheritdoc />
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.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions)
=> await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions).ConfigureAwait(false);
/// <inheritdoc /> /// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions)
=> await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false);


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

@@ -381,7 +381,7 @@ namespace Discord.WebSocket
Description = model.Description; Description = model.Description;
PremiumSubscriptionCount = model.PremiumSubscriptionCount.GetValueOrDefault(); PremiumSubscriptionCount = model.PremiumSubscriptionCount.GetValueOrDefault();
PreferredLocale = model.PreferredLocale; PreferredLocale = model.PreferredLocale;
PreferredCulture = new CultureInfo(PreferredLocale);
PreferredCulture = PreferredLocale == null ? null : new CultureInfo(PreferredLocale);


if (model.Emojis != null) if (model.Emojis != null)
{ {


+ 13
- 1
src/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs View File

@@ -56,11 +56,23 @@ namespace Discord.WebSocket
cachedMessageIds = _orderedMessages; cachedMessageIds = _orderedMessages;
else if (dir == Direction.Before) else if (dir == Direction.Before)
cachedMessageIds = _orderedMessages.Where(x => x < fromMessageId.Value); cachedMessageIds = _orderedMessages.Where(x => x < fromMessageId.Value);
else
else if (dir == Direction.After)
cachedMessageIds = _orderedMessages.Where(x => x > fromMessageId.Value); cachedMessageIds = _orderedMessages.Where(x => x > fromMessageId.Value);
else //Direction.Around
{
if (!_messages.TryGetValue(fromMessageId.Value, out SocketMessage msg))
return ImmutableArray<SocketMessage>.Empty;
int around = limit / 2;
var before = GetMany(fromMessageId, Direction.Before, around);
var after = GetMany(fromMessageId, Direction.After, around).Reverse();

return after.Concat(new SocketMessage[] { msg }).Concat(before).ToImmutableArray();
}


if (dir == Direction.Before) if (dir == Direction.Before)
cachedMessageIds = cachedMessageIds.Reverse(); cachedMessageIds = cachedMessageIds.Reverse();
if (dir == Direction.Around) //Only happens if fromMessageId is null, should only get "around" and itself (+1)
limit = limit / 2 + 1;


return cachedMessageIds return cachedMessageIds
.Select(x => .Select(x =>


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

@@ -46,6 +46,8 @@ namespace Discord.WebSocket
public virtual bool IsSuppressed => false; public virtual bool IsSuppressed => false;
/// <inheritdoc /> /// <inheritdoc />
public virtual DateTimeOffset? EditedTimestamp => null; public virtual DateTimeOffset? EditedTimestamp => null;
/// <inheritdoc />
public virtual bool MentionedEveryone => false;


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


+ 18
- 9
src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs View File

@@ -20,7 +20,9 @@ namespace Discord.WebSocket
private ImmutableArray<Attachment> _attachments = ImmutableArray.Create<Attachment>(); private ImmutableArray<Attachment> _attachments = ImmutableArray.Create<Attachment>();
private ImmutableArray<Embed> _embeds = ImmutableArray.Create<Embed>(); private ImmutableArray<Embed> _embeds = ImmutableArray.Create<Embed>();
private ImmutableArray<ITag> _tags = ImmutableArray.Create<ITag>(); private ImmutableArray<ITag> _tags = ImmutableArray.Create<ITag>();
private ImmutableArray<SocketRole> _roleMentions = ImmutableArray.Create<SocketRole>();
private ImmutableArray<SocketUser> _userMentions = ImmutableArray.Create<SocketUser>();

/// <inheritdoc /> /// <inheritdoc />
public override bool IsTTS => _isTTS; public override bool IsTTS => _isTTS;
/// <inheritdoc /> /// <inheritdoc />
@@ -30,6 +32,8 @@ namespace Discord.WebSocket
/// <inheritdoc /> /// <inheritdoc />
public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks); public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks);
/// <inheritdoc /> /// <inheritdoc />
public override bool MentionedEveryone => _isMentioningEveryone;
/// <inheritdoc />
public override IReadOnlyCollection<Attachment> Attachments => _attachments; public override IReadOnlyCollection<Attachment> Attachments => _attachments;
/// <inheritdoc /> /// <inheritdoc />
public override IReadOnlyCollection<Embed> Embeds => _embeds; public override IReadOnlyCollection<Embed> Embeds => _embeds;
@@ -38,9 +42,9 @@ namespace Discord.WebSocket
/// <inheritdoc /> /// <inheritdoc />
public override IReadOnlyCollection<SocketGuildChannel> MentionedChannels => MessageHelper.FilterTagsByValue<SocketGuildChannel>(TagType.ChannelMention, _tags); public override IReadOnlyCollection<SocketGuildChannel> MentionedChannels => MessageHelper.FilterTagsByValue<SocketGuildChannel>(TagType.ChannelMention, _tags);
/// <inheritdoc /> /// <inheritdoc />
public override IReadOnlyCollection<SocketRole> MentionedRoles => MessageHelper.FilterTagsByValue<SocketRole>(TagType.RoleMention, _tags);
public override IReadOnlyCollection<SocketRole> MentionedRoles => _roleMentions;
/// <inheritdoc /> /// <inheritdoc />
public override IReadOnlyCollection<SocketUser> MentionedUsers => MessageHelper.FilterTagsByValue<SocketUser>(TagType.UserMention, _tags);
public override IReadOnlyCollection<SocketUser> MentionedUsers => _userMentions;


internal SocketUserMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author, MessageSource source) internal SocketUserMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author, MessageSource source)
: base(discord, id, channel, author, source) : base(discord, id, channel, author, source)
@@ -57,6 +61,8 @@ namespace Discord.WebSocket
{ {
base.Update(state, model); base.Update(state, model);


SocketGuild guild = (Channel as SocketGuildChannel)?.Guild;

if (model.IsTextToSpeech.IsSpecified) if (model.IsTextToSpeech.IsSpecified)
_isTTS = model.IsTextToSpeech.Value; _isTTS = model.IsTextToSpeech.Value;
if (model.Pinned.IsSpecified) if (model.Pinned.IsSpecified)
@@ -69,6 +75,8 @@ namespace Discord.WebSocket
{ {
_isSuppressed = model.Flags.Value.HasFlag(API.MessageFlags.Suppressed); _isSuppressed = model.Flags.Value.HasFlag(API.MessageFlags.Suppressed);
} }
if (model.RoleMentions.IsSpecified)
_roleMentions = model.RoleMentions.Value.Select(x => guild.GetRole(x)).ToImmutableArray();


if (model.Attachments.IsSpecified) if (model.Attachments.IsSpecified)
{ {
@@ -98,28 +106,29 @@ namespace Discord.WebSocket
_embeds = ImmutableArray.Create<Embed>(); _embeds = ImmutableArray.Create<Embed>();
} }


IReadOnlyCollection<IUser> mentions = ImmutableArray.Create<SocketUnknownUser>(); //Is passed to ParseTags to get real mention collection
if (model.UserMentions.IsSpecified) if (model.UserMentions.IsSpecified)
{ {
var value = model.UserMentions.Value; var value = model.UserMentions.Value;
if (value.Length > 0) if (value.Length > 0)
{ {
var newMentions = ImmutableArray.CreateBuilder<SocketUnknownUser>(value.Length);
var newMentions = ImmutableArray.CreateBuilder<SocketUser>(value.Length);
for (int i = 0; i < value.Length; i++) for (int i = 0; i < value.Length; i++)
{ {
var val = value[i]; var val = value[i];
if (val.Object != null)
var guildUser = guild.GetUser(val.Id);
if (guildUser != null)
newMentions.Add(guildUser);
else if (val.Object != null)
newMentions.Add(SocketUnknownUser.Create(Discord, state, val.Object)); newMentions.Add(SocketUnknownUser.Create(Discord, state, val.Object));
} }
mentions = newMentions.ToImmutable();
_userMentions = newMentions.ToImmutable();
} }
} }


if (model.Content.IsSpecified) if (model.Content.IsSpecified)
{ {
var text = model.Content.Value; var text = model.Content.Value;
var guild = (Channel as SocketGuildChannel)?.Guild;
_tags = MessageHelper.ParseTags(text, Channel, guild, mentions);
_tags = MessageHelper.ParseTags(text, Channel, guild, _userMentions);
model.Content = text; model.Content = text;
} }
} }


+ 2
- 0
src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs View File

@@ -154,6 +154,8 @@ namespace Discord.WebSocket
Nickname = model.Nick.Value; Nickname = model.Nick.Value;
if (model.Roles.IsSpecified) if (model.Roles.IsSpecified)
UpdateRoles(model.Roles.Value); UpdateRoles(model.Roles.Value);
if (model.PremiumSince.IsSpecified)
_premiumSinceTicks = model.PremiumSince.Value?.UtcTicks;
} }
private void UpdateRoles(ulong[] roleIds) private void UpdateRoles(ulong[] roleIds)
{ {


+ 27
- 4
src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs View File

@@ -18,16 +18,20 @@ namespace Discord.WebSocket
public IActivity Activity { get; } public IActivity Activity { get; }
/// <inheritdoc /> /// <inheritdoc />
public IImmutableSet<ClientType> ActiveClients { get; } public IImmutableSet<ClientType> ActiveClients { get; }
internal SocketPresence(UserStatus status, IActivity activity, IImmutableSet<ClientType> activeClients)
/// <inheritdoc />
public IImmutableList<IActivity> Activities { get; }
internal SocketPresence(UserStatus status, IActivity activity, IImmutableSet<ClientType> activeClients, IImmutableList<IActivity> activities)
{ {
Status = status; Status = status;
Activity= activity;
ActiveClients = activeClients;
Activity = activity;
ActiveClients = activeClients ?? ImmutableHashSet<ClientType>.Empty;
Activities = activities ?? ImmutableList<IActivity>.Empty;
} }
internal static SocketPresence Create(Model model) internal static SocketPresence Create(Model model)
{ {
var clients = ConvertClientTypesDict(model.ClientStatus.GetValueOrDefault()); var clients = ConvertClientTypesDict(model.ClientStatus.GetValueOrDefault());
return new SocketPresence(model.Status, model.Game?.ToEntity(), clients);
var activities = ConvertActivitiesList(model.Activities);
return new SocketPresence(model.Status, model.Game?.ToEntity(), clients, activities);
} }
/// <summary> /// <summary>
/// Creates a new <see cref="IReadOnlyCollection{T}"/> containing all of the client types /// Creates a new <see cref="IReadOnlyCollection{T}"/> containing all of the client types
@@ -53,6 +57,25 @@ namespace Discord.WebSocket
} }
return set.ToImmutableHashSet(); return set.ToImmutableHashSet();
} }
/// <summary>
/// Creates a new <see cref="IReadOnlyCollection{T}"/> containing all the activities
/// that a user has from the data supplied in the Presence update frame.
/// </summary>
/// <param name="activities">
/// A list of <see cref="API.Game"/>.
/// </param>
/// <returns>
/// A list of all <see cref="IActivity"/> that this user currently has available.
/// </returns>
private static IImmutableList<IActivity> ConvertActivitiesList(IList<API.Game> activities)
{
if (activities == null || activities.Count == 0)
return ImmutableList<IActivity>.Empty;
var list = new List<IActivity>();
foreach (var activity in activities)
list.Add(activity.ToEntity());
return list.ToImmutableList();
}


/// <summary> /// <summary>
/// Gets the status of the user. /// Gets the status of the user.


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

@@ -25,7 +25,7 @@ namespace Discord.WebSocket
/// <inheritdoc /> /// <inheritdoc />
public override bool IsWebhook => false; public override bool IsWebhook => false;
/// <inheritdoc /> /// <inheritdoc />
internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null, null); } set { } }
internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null, null, null); } set { } }
/// <inheritdoc /> /// <inheritdoc />
/// <exception cref="NotSupportedException">This field is not supported for an unknown user.</exception> /// <exception cref="NotSupportedException">This field is not supported for an unknown user.</exception>
internal override SocketGlobalUser GlobalUser => internal override SocketGlobalUser GlobalUser =>


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

@@ -41,6 +41,8 @@ namespace Discord.WebSocket
public UserStatus Status => Presence.Status; public UserStatus Status => Presence.Status;
/// <inheritdoc /> /// <inheritdoc />
public IImmutableSet<ClientType> ActiveClients => Presence.ActiveClients ?? ImmutableHashSet<ClientType>.Empty; public IImmutableSet<ClientType> ActiveClients => Presence.ActiveClients ?? ImmutableHashSet<ClientType>.Empty;
/// <inheritdoc />
public IImmutableList<IActivity> Activities => Presence.Activities ?? ImmutableList<IActivity>.Empty;
/// <summary> /// <summary>
/// Gets mutual guilds shared with this user. /// Gets mutual guilds shared with this user.
/// </summary> /// </summary>


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

@@ -30,7 +30,7 @@ namespace Discord.WebSocket
/// <inheritdoc /> /// <inheritdoc />
public override bool IsWebhook => true; public override bool IsWebhook => true;
/// <inheritdoc /> /// <inheritdoc />
internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null, null); } set { } }
internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null, null, null); } set { } }
internal override SocketGlobalUser GlobalUser => internal override SocketGlobalUser GlobalUser =>
throw new NotSupportedException(); throw new NotSupportedException();




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

@@ -33,7 +33,7 @@ namespace Discord.Webhook
: this(webhookUrl, new DiscordRestConfig()) { } : this(webhookUrl, new DiscordRestConfig()) { }


// regex pattern to match webhook urls // regex pattern to match webhook urls
private static Regex WebhookUrlRegex = new Regex(@"^.*(discord|discordapp)\.com\/api\/webhooks\/([\d]+)\/([a-z0-9_-]+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static Regex WebhookUrlRegex = new Regex(@"^.*(discord|discordapp)\.com\/api\/webhooks\/([\d]+)\/([a-z0-9_-]+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);


/// <summary> Creates a new Webhook Discord client. </summary> /// <summary> Creates a new Webhook Discord client. </summary>
public DiscordWebhookClient(ulong webhookId, string webhookToken, DiscordRestConfig config) public DiscordWebhookClient(ulong webhookId, string webhookToken, DiscordRestConfig config)
@@ -77,9 +77,9 @@ namespace Discord.Webhook
ApiClient.RequestQueue.RateLimitTriggered += async (id, info) => ApiClient.RequestQueue.RateLimitTriggered += async (id, info) =>
{ {
if (info == null) if (info == null)
await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {id ?? "null"}").ConfigureAwait(false);
await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {id?.ToString() ?? "null"}").ConfigureAwait(false);
else else
await _restLogger.WarningAsync($"Rate limit triggered: {id ?? "null"}").ConfigureAwait(false);
await _restLogger.WarningAsync($"Rate limit triggered: {id?.ToString() ?? "null"}").ConfigureAwait(false);
}; };
ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false); ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false);
} }
@@ -88,19 +88,21 @@ namespace Discord.Webhook
/// <summary> Sends a message 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> /// <returns> Returns the ID of the created message. </returns>
public Task<ulong> SendMessageAsync(string text = null, bool isTTS = false, IEnumerable<Embed> embeds = null, public Task<ulong> SendMessageAsync(string text = null, bool isTTS = false, IEnumerable<Embed> embeds = null,
string username = null, string avatarUrl = null, RequestOptions options = null)
=> WebhookClientHelper.SendMessageAsync(this, text, isTTS, embeds, username, avatarUrl, options);
string username = null, string avatarUrl = null, RequestOptions options = null, AllowedMentions allowedMentions = null)
=> WebhookClientHelper.SendMessageAsync(this, text, isTTS, embeds, username, avatarUrl, allowedMentions, options);


/// <summary> Sends a message to the channel for this webhook with an attachment. </summary> /// <summary> Sends a message to the channel for this webhook with an attachment. </summary>
/// <returns> Returns the ID of the created message. </returns> /// <returns> Returns the ID of the created message. </returns>
public Task<ulong> SendFileAsync(string filePath, string text, bool isTTS = false, public Task<ulong> SendFileAsync(string filePath, string text, bool isTTS = false,
IEnumerable<Embed> embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null, bool isSpoiler = false)
=> WebhookClientHelper.SendFileAsync(this, filePath, text, isTTS, embeds, username, avatarUrl, options, isSpoiler);
IEnumerable<Embed> embeds = null, string username = null, string avatarUrl = null,
RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null)
=> WebhookClientHelper.SendFileAsync(this, filePath, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, isSpoiler);
/// <summary> Sends a message to the channel for this webhook with an attachment. </summary> /// <summary> Sends a message to the channel for this webhook with an attachment. </summary>
/// <returns> Returns the ID of the created message. </returns> /// <returns> Returns the ID of the created message. </returns>
public Task<ulong> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, public Task<ulong> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false,
IEnumerable<Embed> embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null, bool isSpoiler = false)
=> WebhookClientHelper.SendFileAsync(this, stream, filename, text, isTTS, embeds, username, avatarUrl, options, isSpoiler);
IEnumerable<Embed> embeds = null, string username = null, string avatarUrl = null,
RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null)
=> WebhookClientHelper.SendFileAsync(this, stream, filename, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, isSpoiler);


/// <summary> Modifies the properties of this webhook. </summary> /// <summary> Modifies the properties of this webhook. </summary>
public Task ModifyWebhookAsync(Action<WebhookProperties> func, RequestOptions options = null) public Task ModifyWebhookAsync(Action<WebhookProperties> func, RequestOptions options = null)


+ 8
- 4
src/Discord.Net.Webhook/WebhookClientHelper.cs View File

@@ -21,7 +21,7 @@ namespace Discord.Webhook
return RestInternalWebhook.Create(client, model); return RestInternalWebhook.Create(client, model);
} }
public static async Task<ulong> SendMessageAsync(DiscordWebhookClient client, public static async Task<ulong> SendMessageAsync(DiscordWebhookClient client,
string text, bool isTTS, IEnumerable<Embed> embeds, string username, string avatarUrl, RequestOptions options)
string text, bool isTTS, IEnumerable<Embed> embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options)
{ {
var args = new CreateWebhookMessageParams(text) { IsTTS = isTTS }; var args = new CreateWebhookMessageParams(text) { IsTTS = isTTS };
if (embeds != null) if (embeds != null)
@@ -30,19 +30,21 @@ namespace Discord.Webhook
args.Username = username; args.Username = username;
if (avatarUrl != null) if (avatarUrl != null)
args.AvatarUrl = avatarUrl; args.AvatarUrl = avatarUrl;
if (allowedMentions != null)
args.AllowedMentions = allowedMentions.ToModel();


var model = await client.ApiClient.CreateWebhookMessageAsync(client.Webhook.Id, args, options: options).ConfigureAwait(false); var model = await client.ApiClient.CreateWebhookMessageAsync(client.Webhook.Id, args, options: options).ConfigureAwait(false);
return model.Id; return model.Id;
} }
public static async Task<ulong> SendFileAsync(DiscordWebhookClient client, string filePath, string text, bool isTTS, public static async Task<ulong> SendFileAsync(DiscordWebhookClient client, string filePath, string text, bool isTTS,
IEnumerable<Embed> embeds, string username, string avatarUrl, RequestOptions options, bool isSpoiler)
IEnumerable<Embed> embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, bool isSpoiler)
{ {
string filename = Path.GetFileName(filePath); string filename = Path.GetFileName(filePath);
using (var file = File.OpenRead(filePath)) using (var file = File.OpenRead(filePath))
return await SendFileAsync(client, file, filename, text, isTTS, embeds, username, avatarUrl, options, isSpoiler).ConfigureAwait(false);
return await SendFileAsync(client, file, filename, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, isSpoiler).ConfigureAwait(false);
} }
public static async Task<ulong> SendFileAsync(DiscordWebhookClient client, Stream stream, string filename, string text, bool isTTS, public static async Task<ulong> SendFileAsync(DiscordWebhookClient client, Stream stream, string filename, string text, bool isTTS,
IEnumerable<Embed> embeds, string username, string avatarUrl, RequestOptions options, bool isSpoiler)
IEnumerable<Embed> embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, bool isSpoiler)
{ {
var args = new UploadWebhookFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS, IsSpoiler = isSpoiler }; var args = new UploadWebhookFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS, IsSpoiler = isSpoiler };
if (username != null) if (username != null)
@@ -51,6 +53,8 @@ namespace Discord.Webhook
args.AvatarUrl = avatarUrl; args.AvatarUrl = avatarUrl;
if (embeds != null) if (embeds != null)
args.Embeds = embeds.Select(x => x.ToModel()).ToArray(); args.Embeds = embeds.Select(x => x.ToModel()).ToArray();
if(allowedMentions != null)
args.AllowedMentions = allowedMentions.ToModel();
var msg = await client.ApiClient.UploadWebhookFileAsync(client.Webhook.Id, args, options).ConfigureAwait(false); var msg = await client.ApiClient.UploadWebhookFileAsync(client.Webhook.Id, args, options).ConfigureAwait(false);
return msg.Id; return msg.Id;
} }


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

@@ -73,12 +73,12 @@ namespace Discord
throw new NotImplementedException(); throw new NotImplementedException();
} }


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


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


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

@@ -81,12 +81,12 @@ namespace Discord
throw new NotImplementedException(); throw new NotImplementedException();
} }


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


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


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

@@ -167,12 +167,12 @@ namespace Discord
throw new NotImplementedException(); throw new NotImplementedException();
} }


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


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


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

@@ -83,7 +83,7 @@ namespace Discord
public void BotTokenDoesNotThrowExceptions(string token) public void BotTokenDoesNotThrowExceptions(string token)
{ {
// This example token is pulled from the Discord Docs // This example token is pulled from the Discord Docs
// https://discordapp.com/developers/docs/reference#authentication-example-bot-token-authorization-header
// https://discord.com/developers/docs/reference#authentication-example-bot-token-authorization-header
// should not throw any exception // should not throw any exception
TokenUtils.ValidateToken(TokenType.Bot, token); TokenUtils.ValidateToken(TokenType.Bot, token);
} }


+ 6
- 6
test/Discord.Net.Tests/Tests.DiscordWebhookClient.cs View File

@@ -12,18 +12,18 @@ namespace Discord
public class DiscordWebhookClientTests public class DiscordWebhookClientTests
{ {
[Theory] [Theory]
[InlineData("https://discordapp.com/api/webhooks/123412347732897802/_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK",
[InlineData("https://discord.com/api/webhooks/123412347732897802/_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK",
123412347732897802, "_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK")] 123412347732897802, "_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK")]
// ptb, canary, etc will have slightly different urls // ptb, canary, etc will have slightly different urls
[InlineData("https://ptb.discordapp.com/api/webhooks/123412347732897802/_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK",
[InlineData("https://ptb.discord.com/api/webhooks/123412347732897802/_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK",
123412347732897802, "_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK")] 123412347732897802, "_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK")]
[InlineData("https://canary.discordapp.com/api/webhooks/123412347732897802/_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK",
[InlineData("https://canary.discord.com/api/webhooks/123412347732897802/_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK",
123412347732897802, "_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK")] 123412347732897802, "_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK")]
// don't care about https // don't care about https
[InlineData("http://canary.discordapp.com/api/webhooks/123412347732897802/_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK",
[InlineData("http://canary.discord.com/api/webhooks/123412347732897802/_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK",
123412347732897802, "_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK")] 123412347732897802, "_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK")]
// this is the minimum that the regex cares about // this is the minimum that the regex cares about
[InlineData("discordapp.com/api/webhooks/123412347732897802/_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK",
[InlineData("discord.com/api/webhooks/123412347732897802/_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK",
123412347732897802, "_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK")] 123412347732897802, "_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK")]
public void TestWebhook_Valid(string webhookurl, ulong expectedId, string expectedToken) public void TestWebhook_Valid(string webhookurl, ulong expectedId, string expectedToken)
{ {
@@ -48,7 +48,7 @@ namespace Discord
[Theory] [Theory]
[InlineData("123412347732897802/_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK")] [InlineData("123412347732897802/_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK")]
// trailing slash // trailing slash
[InlineData("https://discordapp.com/api/webhooks/123412347732897802/_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK/")]
[InlineData("https://discord.com/api/webhooks/123412347732897802/_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK/")]
public void TestWebhook_Invalid(string webhookurl) public void TestWebhook_Invalid(string webhookurl)
{ {
Assert.Throws<ArgumentException>(() => Assert.Throws<ArgumentException>(() =>


Loading…
Cancel
Save