Browse Source

Merge pull request #2 from discord-net/dev

merge
pull/1690/head
vrachv GitHub 4 years ago
parent
commit
e831397ea9
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
100 changed files with 2198 additions and 339 deletions
  1. +2
    -2
      README.md
  2. +3
    -3
      docs/_overwrites/Common/EmbedObjectBuilder.Inclusion.md
  3. +1
    -1
      docs/faq/basics/client-basics.md
  4. +2
    -2
      docs/faq/misc/glossary.md
  5. +5
    -5
      docs/guides/getting_started/first-bot.md
  6. +2
    -2
      docs/index.md
  7. +4
    -4
      samples/04_webhook_client/Program.cs
  8. +5
    -1
      src/Discord.Net.Core/Audio/IAudioClient.cs
  9. +22
    -1
      src/Discord.Net.Core/CDN.cs
  10. +2
    -2
      src/Discord.Net.Core/DiscordConfig.cs
  11. +7
    -1
      src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs
  12. +9
    -0
      src/Discord.Net.Core/Entities/Channels/INewsChannel.cs
  13. +22
    -0
      src/Discord.Net.Core/Entities/Gateway/BotGateway.cs
  14. +38
    -0
      src/Discord.Net.Core/Entities/Gateway/SessionStartLimit.cs
  15. +21
    -0
      src/Discord.Net.Core/Entities/Guilds/GuildWidgetProperties.cs
  16. +165
    -37
      src/Discord.Net.Core/Entities/Guilds/IGuild.cs
  17. +12
    -0
      src/Discord.Net.Core/Entities/IApplication.cs
  18. +1
    -0
      src/Discord.Net.Core/Entities/IUpdateable.cs
  19. +14
    -0
      src/Discord.Net.Core/Entities/Invites/TargetUserType.cs
  20. +7
    -0
      src/Discord.Net.Core/Entities/Messages/IMessage.cs
  21. +1
    -1
      src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs
  22. +4
    -0
      src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs
  23. +9
    -2
      src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs
  24. +17
    -5
      src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs
  25. +27
    -0
      src/Discord.Net.Core/Entities/Teams/ITeam.cs
  26. +25
    -0
      src/Discord.Net.Core/Entities/Teams/ITeamMember.cs
  27. +11
    -0
      src/Discord.Net.Core/Entities/Teams/MembershipState.cs
  28. +4
    -0
      src/Discord.Net.Core/Entities/Users/IPresence.cs
  29. +1
    -1
      src/Discord.Net.Core/Extensions/MessageExtensions.cs
  30. +10
    -0
      src/Discord.Net.Core/IDiscordClient.cs
  31. +118
    -0
      src/Discord.Net.Core/Net/BucketId.cs
  32. +2
    -2
      src/Discord.Net.Core/Net/HttpException.cs
  33. +1
    -1
      src/Discord.Net.Core/Net/WebSocketClosedException.cs
  34. +3
    -1
      src/Discord.Net.Core/RequestOptions.cs
  35. +7
    -1
      src/Discord.Net.Rest/API/Common/Application.cs
  36. +22
    -2
      src/Discord.Net.Rest/API/Common/Guild.cs
  37. +2
    -2
      src/Discord.Net.Rest/API/Common/GuildEmbed.cs
  38. +13
    -0
      src/Discord.Net.Rest/API/Common/GuildWidget.cs
  39. +9
    -0
      src/Discord.Net.Rest/API/Common/MembershipState.cs
  40. +5
    -0
      src/Discord.Net.Rest/API/Common/Presence.cs
  41. +16
    -0
      src/Discord.Net.Rest/API/Common/SessionStartLimit.cs
  42. +17
    -0
      src/Discord.Net.Rest/API/Common/Team.cs
  43. +17
    -0
      src/Discord.Net.Rest/API/Common/TeamMember.cs
  44. +4
    -0
      src/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs
  45. +3
    -1
      src/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs
  46. +3
    -1
      src/Discord.Net.Rest/API/Rest/GetBotGatewayResponse.cs
  47. +5
    -1
      src/Discord.Net.Rest/API/Rest/GuildPruneParams.cs
  48. +14
    -0
      src/Discord.Net.Rest/API/Rest/ModifyGuildWidgetParams.cs
  49. +3
    -0
      src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs
  50. +7
    -3
      src/Discord.Net.Rest/BaseDiscordClient.cs
  51. +30
    -5
      src/Discord.Net.Rest/ClientHelper.cs
  52. +88
    -40
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  53. +14
    -6
      src/Discord.Net.Rest/DiscordRestClient.cs
  54. +9
    -5
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs
  55. +9
    -5
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs
  56. +31
    -3
      src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs
  57. +1
    -1
      src/Discord.Net.Rest/Entities/Channels/RestNewsChannel.cs
  58. +2
    -1
      src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs
  59. +59
    -9
      src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs
  60. +177
    -26
      src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
  61. +25
    -0
      src/Discord.Net.Rest/Entities/Guilds/RestGuildWidget.cs
  62. +12
    -4
      src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs
  63. +3
    -0
      src/Discord.Net.Rest/Entities/Messages/RestMessage.cs
  64. +13
    -8
      src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs
  65. +10
    -0
      src/Discord.Net.Rest/Entities/RestApplication.cs
  66. +37
    -0
      src/Discord.Net.Rest/Entities/Teams/RestTeam.cs
  67. +30
    -0
      src/Discord.Net.Rest/Entities/Teams/RestTeamMember.cs
  68. +2
    -0
      src/Discord.Net.Rest/Entities/Users/RestUser.cs
  69. +7
    -7
      src/Discord.Net.Rest/Net/Queue/ClientBucket.cs
  70. +53
    -0
      src/Discord.Net.Rest/Net/Queue/GatewayBucket.cs
  71. +65
    -12
      src/Discord.Net.Rest/Net/Queue/RequestQueue.cs
  72. +164
    -22
      src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs
  73. +3
    -3
      src/Discord.Net.Rest/Net/Queue/Requests/WebSocketRequest.cs
  74. +5
    -3
      src/Discord.Net.Rest/Net/RateLimitInfo.cs
  75. +31
    -0
      src/Discord.Net.WebSocket/API/Gateway/InviteCreateEvent.cs
  76. +14
    -0
      src/Discord.Net.WebSocket/API/Gateway/InviteDeleteEvent.cs
  77. +7
    -1
      src/Discord.Net.WebSocket/Audio/AudioClient.cs
  78. +3
    -0
      src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs
  79. +4
    -2
      src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs
  80. +5
    -3
      src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs
  81. +7
    -0
      src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs
  82. +8
    -1
      src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs
  83. +7
    -0
      src/Discord.Net.WebSocket/Audio/Streams/SodiumDecryptStream.cs
  84. +7
    -0
      src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs
  85. +42
    -0
      src/Discord.Net.WebSocket/BaseSocketClient.Events.cs
  86. +12
    -1
      src/Discord.Net.WebSocket/ConnectionManager.cs
  87. +46
    -7
      src/Discord.Net.WebSocket/DiscordShardedClient.cs
  88. +10
    -1
      src/Discord.Net.WebSocket/DiscordSocketApiClient.cs
  89. +89
    -15
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  90. +10
    -1
      src/Discord.Net.WebSocket/DiscordSocketConfig.cs
  91. +0
    -20
      src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs
  92. +1
    -1
      src/Discord.Net.WebSocket/Entities/Channels/SocketNewsChannel.cs
  93. +125
    -25
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
  94. +143
    -0
      src/Discord.Net.WebSocket/Entities/Invites/SocketInvite.cs
  95. +2
    -0
      src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs
  96. +20
    -11
      src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs
  97. +2
    -0
      src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs
  98. +27
    -4
      src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs
  99. +1
    -1
      src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs
  100. +2
    -0
      src/Discord.Net.WebSocket/Entities/Users/SocketUser.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)
[![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)
[![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).



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

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



+ 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.
- Example: #dotnet_discord-net
- See [Channel Types](#channel-types)
[IGuild]: xref:Discord.IGuild
[IChannel]: xref:Discord.IChannel

@@ -79,4 +79,4 @@ activity for listening to a song on Spotify.
[RichGame]: xref:Discord.RichGame
[StreamingGame]: xref:Discord.StreamingGame
[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)

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

## 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.
>
> 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.
>
> 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.
>
> [Environment Variables]: https://en.wikipedia.org/wiki/Environment_variable
@@ -221,4 +221,4 @@ should be to separate...
2. the modules (handle commands)
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)
[![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)
[![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?

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?



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

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

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
// 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
{
@@ -26,7 +26,7 @@ namespace _04_webhook_client
};

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


+ 5
- 1
src/Discord.Net.Core/Audio/IAudioClient.cs View File

@@ -1,4 +1,5 @@
using System;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Discord.Audio
@@ -20,6 +21,9 @@ namespace Discord.Audio
/// <summary> Gets the estimated round-trip latency, in milliseconds, to the voice UDP server. </summary>
int UdpLatency { get; }

/// <summary>Gets the current audio streams.</summary>
IReadOnlyDictionary<ulong, AudioInStream> GetStreams();

Task StopAsync();
Task SetSpeakingAsync(bool value);



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

@@ -7,6 +7,17 @@ namespace Discord
/// </summary>
public static class CDN
{
/// <summary>
/// Returns a team icon URL.
/// </summary>
/// <param name="teamId">The team identifier.</param>
/// <param name="iconId">The icon identifier.</param>
/// <returns>
/// A URL pointing to the team's icon.
/// </returns>
public static string GetTeamIconUrl(ulong teamId, string iconId)
=> iconId != null ? $"{DiscordConfig.CDNUrl}team-icons/{teamId}/{iconId}.jpg" : null;

/// <summary>
/// Returns an application icon URL.
/// </summary>
@@ -62,11 +73,21 @@ namespace Discord
/// <param name="guildId">The guild snowflake identifier.</param>
/// <param name="splashId">The splash icon identifier.</param>
/// <returns>
/// A URL pointing to the guild's icon.
/// A URL pointing to the guild's splash.
/// </returns>
public static string GetGuildSplashUrl(ulong guildId, string splashId)
=> splashId != null ? $"{DiscordConfig.CDNUrl}splashes/{guildId}/{splashId}.jpg" : null;
/// <summary>
/// Returns a guild discovery splash URL.
/// </summary>
/// <param name="guildId">The guild snowflake identifier.</param>
/// <param name="discoverySplashId">The discovery splash icon identifier.</param>
/// <returns>
/// A URL pointing to the guild's discovery splash.
/// </returns>
public static string GetGuildDiscoverySplashUrl(ulong guildId, string discoverySplashId)
=> discoverySplashId != null ? $"{DiscordConfig.CDNUrl}discovery-splashes/{guildId}/{discoverySplashId}.jpg" : null;
/// <summary>
/// Returns a channel icon URL.
/// </summary>
/// <param name="channelId">The channel snowflake identifier.</param>


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

@@ -13,7 +13,7 @@ namespace Discord
/// <returns>
/// 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
/// <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>
/// </returns>
public const int APIVersion = 6;
@@ -50,7 +50,7 @@ namespace Discord
/// <returns>
/// The Discord API URL using <see cref="APIVersion"/>.
/// </returns>
public static readonly string APIUrl = $"https://discordapp.com/api/v{APIVersion}/";
public static readonly string APIUrl = $"https://discord.com/api/v{APIVersion}/";
/// <summary>
/// Returns the base Discord CDN URL.
/// </summary>


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

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

namespace Discord
{
/// <summary>
@@ -26,9 +28,13 @@ namespace Discord
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public Optional<ulong?> CategoryId { get; set; }
/// <summary>
/// Gets or sets the permission overwrites for this channel.
/// </summary>
public Optional<IEnumerable<Overwrite>> PermissionOverwrites { get; set; }
}
}

+ 9
- 0
src/Discord.Net.Core/Entities/Channels/INewsChannel.cs View File

@@ -0,0 +1,9 @@
namespace Discord
{
/// <summary>
/// Represents a generic news channel in a guild that can send and receive messages.
/// </summary>
public interface INewsChannel : ITextChannel
{
}
}

+ 22
- 0
src/Discord.Net.Core/Entities/Gateway/BotGateway.cs View File

@@ -0,0 +1,22 @@
namespace Discord
{
/// <summary>
/// Stores the gateway information related to the current bot.
/// </summary>
public class BotGateway
{
/// <summary>
/// Gets the WSS URL that can be used for connecting to the gateway.
/// </summary>
public string Url { get; internal set; }
/// <summary>
/// Gets the recommended number of shards to use when connecting.
/// </summary>
public int Shards { get; internal set; }
/// <summary>
/// Gets the <see cref="SessionStartLimit"/> that contains the information
/// about the current session start limit.
/// </summary>
public SessionStartLimit SessionStartLimit { get; internal set; }
}
}

+ 38
- 0
src/Discord.Net.Core/Entities/Gateway/SessionStartLimit.cs View File

@@ -0,0 +1,38 @@
namespace Discord
{
/// <summary>
/// Stores the information related to the gateway identify request.
/// </summary>
public class SessionStartLimit
{
/// <summary>
/// Gets the total number of session starts the current user is allowed.
/// </summary>
/// <returns>
/// The maximum amount of session starts the current user is allowed.
/// </returns>
public int Total { get; internal set; }
/// <summary>
/// Gets the remaining number of session starts the current user is allowed.
/// </summary>
/// <returns>
/// The remaining amount of session starts the current user is allowed.
/// </returns>
public int Remaining { get; internal set; }
/// <summary>
/// Gets the number of milliseconds after which the limit resets.
/// </summary>
/// <returns>
/// The milliseconds until the limit resets back to the <see cref="Total"/>.
/// </returns>
public int ResetAfter { get; internal set; }
/// <summary>
/// Gets the maximum concurrent identify requests in a time window.
/// </summary>
/// <returns>
/// The maximum concurrent identify requests in a time window,
/// limited to the same rate limit key.
/// </returns>
public int MaxConcurrency { get; internal set; }
}
}

+ 21
- 0
src/Discord.Net.Core/Entities/Guilds/GuildWidgetProperties.cs View File

@@ -0,0 +1,21 @@
namespace Discord
{
/// <summary>
/// Provides properties that are used to modify the widget of an <see cref="IGuild" /> with the specified changes.
/// </summary>
public class GuildWidgetProperties
{
/// <summary>
/// Sets whether the widget should be enabled.
/// </summary>
public Optional<bool> Enabled { get; set; }
/// <summary>
/// Sets the channel that the invite should place its users in, if not <see langword="null" />.
/// </summary>
public Optional<IChannel> Channel { get; set; }
/// <summary>
/// Sets the channel that the invite should place its users in, if not <see langword="null" />.
/// </summary>
public Optional<ulong?> ChannelId { get; set; }
}
}

+ 165
- 37
src/Discord.Net.Core/Entities/Guilds/IGuild.cs View File

@@ -23,7 +23,7 @@ namespace Discord
/// automatically moved to the AFK voice channel.
/// </summary>
/// <returns>
/// An <see cref="int"/> representing the amount of time in seconds for a user to be marked as inactive
/// An <see langword="int"/> representing the amount of time in seconds for a user to be marked as inactive
/// and moved into the AFK voice channel.
/// </returns>
int AFKTimeout { get; }
@@ -31,10 +31,17 @@ namespace Discord
/// Gets a value that indicates whether this guild is embeddable (i.e. can use widget).
/// </summary>
/// <returns>
/// <c>true</c> if this guild can be embedded via widgets; otherwise <c>false</c>.
/// <see langword="true" /> if this guild has a widget enabled; otherwise <see langword="false" />.
/// </returns>
bool IsEmbeddable { get; }
/// <summary>
/// Gets a value that indicates whether this guild has the widget enabled.
/// </summary>
/// <returns>
/// <see langword="true" /> if this guild has a widget enabled; otherwise <see langword="false" />.
/// </returns>
bool IsWidgetEnabled { get; }
/// <summary>
/// Gets the default message notifications for users who haven't explicitly set their notification settings.
/// </summary>
DefaultMessageNotifications DefaultMessageNotifications { get; }
@@ -64,31 +71,45 @@ namespace Discord
/// Gets the ID of this guild's icon.
/// </summary>
/// <returns>
/// An identifier for the splash image; <c>null</c> if none is set.
/// An identifier for the splash image; <see langword="null" /> if none is set.
/// </returns>
string IconId { get; }
/// <summary>
/// Gets the URL of this guild's icon.
/// </summary>
/// <returns>
/// A URL pointing to the guild's icon; <c>null</c> if none is set.
/// A URL pointing to the guild's icon; <see langword="null" /> if none is set.
/// </returns>
string IconUrl { get; }
/// <summary>
/// Gets the ID of this guild's splash image.
/// </summary>
/// <returns>
/// An identifier for the splash image; <c>null</c> if none is set.
/// An identifier for the splash image; <see langword="null" /> if none is set.
/// </returns>
string SplashId { get; }
/// <summary>
/// Gets the URL of this guild's splash image.
/// </summary>
/// <returns>
/// A URL pointing to the guild's splash image; <c>null</c> if none is set.
/// A URL pointing to the guild's splash image; <see langword="null" /> if none is set.
/// </returns>
string SplashUrl { get; }
/// <summary>
/// Gets the ID of this guild's discovery splash image.
/// </summary>
/// <returns>
/// An identifier for the discovery splash image; <see langword="null" /> if none is set.
/// </returns>
string DiscoverySplashId { get; }
/// <summary>
/// Gets the URL of this guild's discovery splash image.
/// </summary>
/// <returns>
/// A URL pointing to the guild's discovery splash image; <see langword="null" /> if none is set.
/// </returns>
string DiscoverySplashUrl { get; }
/// <summary>
/// Determines if this guild is currently connected and ready to be used.
/// </summary>
/// <remarks>
@@ -98,7 +119,7 @@ namespace Discord
/// This boolean is used to determine if the guild is currently connected to the WebSocket and is ready to be used/accessed.
/// </remarks>
/// <returns>
/// <c>true</c> if this guild is currently connected and ready to be used; otherwise <c>false</c>.
/// <c>true</c> if this guild is currently connected and ready to be used; otherwise <see langword="false"/>.
/// </returns>
bool Available { get; }

@@ -106,7 +127,7 @@ namespace Discord
/// Gets the ID of the AFK voice channel for this guild.
/// </summary>
/// <returns>
/// A <see cref="ulong"/> representing the snowflake identifier of the AFK voice channel; <c>null</c> if
/// A <see langword="ulong"/> representing the snowflake identifier of the AFK voice channel; <see langword="null" /> if
/// none is set.
/// </returns>
ulong? AFKChannelId { get; }
@@ -121,7 +142,7 @@ namespace Discord
/// </note>
/// </remarks>
/// <returns>
/// A <see cref="ulong"/> representing the snowflake identifier of the default text channel; <c>0</c> if
/// A <see langword="ulong"/> representing the snowflake identifier of the default text channel; <c>0</c> if
/// none can be found.
/// </returns>
ulong DefaultChannelId { get; }
@@ -129,30 +150,54 @@ namespace Discord
/// Gets the ID of the widget embed channel of this guild.
/// </summary>
/// <returns>
/// A <see cref="ulong"/> representing the snowflake identifier of the embedded channel found within the
/// widget settings of this guild; <c>null</c> if none is set.
/// A <see langword="ulong"/> representing the snowflake identifier of the embedded channel found within the
/// widget settings of this guild; <see langword="null" /> if none is set.
/// </returns>
ulong? EmbedChannelId { get; }
/// <summary>
/// Gets the ID of the channel assigned to the widget of this guild.
/// </summary>
/// <returns>
/// A <see langword="ulong"/> representing the snowflake identifier of the channel assigned to the widget found
/// within the widget settings of this guild; <see langword="null" /> if none is set.
/// </returns>
ulong? WidgetChannelId { get; }
/// <summary>
/// Gets the ID of the channel where randomized welcome messages are sent.
/// </summary>
/// <returns>
/// A <see cref="ulong"/> representing the snowflake identifier of the system channel where randomized
/// welcome messages are sent; <c>null</c> if none is set.
/// A <see langword="ulong"/> representing the snowflake identifier of the system channel where randomized
/// welcome messages are sent; <see langword="null" /> if none is set.
/// </returns>
ulong? SystemChannelId { get; }
/// <summary>
/// Gets the ID of the channel with the rules.
/// </summary>
/// <returns>
/// A <see langword="ulong"/> representing the snowflake identifier of the channel that contains the rules;
/// <see langword="null" /> if none is set.
/// </returns>
ulong? RulesChannelId { get; }
/// <summary>
/// Gets the ID of the channel where admins and moderators of Community guilds receive notices from Discord.
/// </summary>
/// <returns>
/// A <see langword="ulong"/> representing the snowflake identifier of the channel where admins and moderators
/// of Community guilds receive notices from Discord; <see langword="null" /> if none is set.
/// </returns>
ulong? PublicUpdatesChannelId { get; }
/// <summary>
/// Gets the ID of the user that owns this guild.
/// </summary>
/// <returns>
/// A <see cref="ulong"/> representing the snowflake identifier of the user that owns this guild.
/// A <see langword="ulong"/> representing the snowflake identifier of the user that owns this guild.
/// </returns>
ulong OwnerId { get; }
/// <summary>
/// Gets the application ID of the guild creator if it is bot-created.
/// </summary>
/// <returns>
/// A <see cref="ulong"/> representing the snowflake identifier of the application ID that created this guild, or <c>null</c> if it was not bot-created.
/// A <see langword="ulong"/> representing the snowflake identifier of the application ID that created this guild, or <see langword="null" /> if it was not bot-created.
/// </returns>
ulong? ApplicationId { get; }
/// <summary>
@@ -208,21 +253,21 @@ namespace Discord
/// Gets the identifier for this guilds banner image.
/// </summary>
/// <returns>
/// An identifier for the banner image; <c>null</c> if none is set.
/// An identifier for the banner image; <see langword="null" /> if none is set.
/// </returns>
string BannerId { get; }
/// <summary>
/// Gets the URL of this guild's banner image.
/// </summary>
/// <returns>
/// A URL pointing to the guild's banner image; <c>null</c> if none is set.
/// A URL pointing to the guild's banner image; <see langword="null" /> if none is set.
/// </returns>
string BannerUrl { get; }
/// <summary>
/// Gets the code for this guild's vanity invite URL.
/// </summary>
/// <returns>
/// A string containing the vanity invite code for this guild; <c>null</c> if none is set.
/// A string containing the vanity invite code for this guild; <see langword="null" /> if none is set.
/// </returns>
string VanityURLCode { get; }
/// <summary>
@@ -236,7 +281,7 @@ namespace Discord
/// Gets the description for the guild.
/// </summary>
/// <returns>
/// The description for the guild; <c>null</c> if none is set.
/// The description for the guild; <see langword="null" /> if none is set.
/// </returns>
string Description { get; }
/// <summary>
@@ -246,9 +291,50 @@ namespace Discord
/// This is the number of users who have boosted this guild.
/// </remarks>
/// <returns>
/// The number of premium subscribers of this guild.
/// The number of premium subscribers of this guild; <see langword="null" /> if not available.
/// </returns>
int PremiumSubscriptionCount { get; }
/// <summary>
/// Gets the maximum number of presences for the guild.
/// </summary>
/// <returns>
/// The maximum number of presences for the guild.
/// </returns>
int? MaxPresences { get; }
/// <summary>
/// Gets the maximum number of members for the guild.
/// </summary>
/// <returns>
/// The maximum number of members for the guild.
/// </returns>
int? MaxMembers { get; }
/// <summary>
/// Gets the maximum amount of users in a video channel.
/// </summary>
/// <returns>
/// The maximum amount of users in a video channel.
/// </returns>
int? MaxVideoChannelUsers { get; }
/// <summary>
/// Gets the approximate number of members in this guild.
/// </summary>
/// <remarks>
/// Only available when getting a guild via REST when `with_counts` is true.
/// </remarks>
/// <returns>
/// The approximate number of members in this guild.
/// </returns>
int? ApproximateMemberCount { get; }
/// <summary>
/// Gets the approximate number of non-offline members in this guild.
/// </summary>
/// <remarks>
/// Only available when getting a guild via REST when `with_counts` is true.
/// </remarks>
/// <returns>
/// The approximate number of non-offline members in this guild.
/// </returns>
int? ApproximatePresenceCount { get; }

/// <summary>
/// Gets the preferred locale of this guild in IETF BCP 47
@@ -285,8 +371,18 @@ namespace Discord
/// <returns>
/// A task that represents the asynchronous modification operation.
/// </returns>
[Obsolete("This endpoint is deprecated, use ModifyWidgetAsync instead.")]
Task ModifyEmbedAsync(Action<GuildEmbedProperties> func, RequestOptions options = null);
/// <summary>
/// Modifies this guild's widget.
/// </summary>
/// <param name="func">The delegate containing the properties to modify the guild widget with.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous modification operation.
/// </returns>
Task ModifyWidgetAsync(Action<GuildWidgetProperties> func, RequestOptions options = null);
/// <summary>
/// Bulk-modifies the order of channels in this guild.
/// </summary>
/// <param name="args">The properties used to modify the channel positions with.</param>
@@ -336,7 +432,7 @@ namespace Discord
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains a ban object, which
/// contains the user information and the reason for the ban; <c>null</c> if the ban entry cannot be found.
/// contains the user information and the reason for the ban; <see langword="null" /> if the ban entry cannot be found.
/// </returns>
Task<IBan> GetBanAsync(IUser user, RequestOptions options = null);
/// <summary>
@@ -346,7 +442,7 @@ namespace Discord
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains a ban object, which
/// contains the user information and the reason for the ban; <c>null</c> if the ban entry cannot be found.
/// contains the user information and the reason for the ban; <see langword="null" /> if the ban entry cannot be found.
/// </returns>
Task<IBan> GetBanAsync(ulong userId, RequestOptions options = null);
/// <summary>
@@ -410,7 +506,7 @@ namespace Discord
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the generic channel
/// associated with the specified <paramref name="id"/>; <c>null</c> if none is found.
/// associated with the specified <paramref name="id"/>; <see langword="null" /> if none is found.
/// </returns>
Task<IGuildChannel> GetChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
@@ -431,7 +527,7 @@ namespace Discord
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the text channel
/// associated with the specified <paramref name="id"/>; <c>null</c> if none is found.
/// associated with the specified <paramref name="id"/>; <see langword="null" /> if none is found.
/// </returns>
Task<ITextChannel> GetTextChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
@@ -462,7 +558,7 @@ namespace Discord
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the voice channel associated
/// with the specified <paramref name="id"/>; <c>null</c> if none is found.
/// with the specified <paramref name="id"/>; <see langword="null" /> if none is found.
/// </returns>
Task<IVoiceChannel> GetVoiceChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
@@ -472,7 +568,7 @@ namespace Discord
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the voice channel that the
/// AFK users will be moved to after they have idled for too long; <c>null</c> if none is set.
/// AFK users will be moved to after they have idled for too long; <see langword="null" /> if none is set.
/// </returns>
Task<IVoiceChannel> GetAFKChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
@@ -482,7 +578,7 @@ namespace Discord
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the text channel where
/// randomized welcome messages will be sent to; <c>null</c> if none is set.
/// randomized welcome messages will be sent to; <see langword="null" /> if none is set.
/// </returns>
Task<ITextChannel> GetSystemChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
@@ -492,7 +588,7 @@ namespace Discord
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the first viewable text
/// channel in this guild; <c>null</c> if none is found.
/// channel in this guild; <see langword="null" /> if none is found.
/// </returns>
Task<ITextChannel> GetDefaultChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
@@ -502,9 +598,40 @@ namespace Discord
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the embed channel set
/// within the server's widget settings; <c>null</c> if none is set.
/// within the server's widget settings; <see langword="null" /> if none is set.
/// </returns>
[Obsolete("This endpoint is deprecated, use GetWidgetChannelAsync instead.")]
Task<IGuildChannel> GetEmbedChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Gets the widget channel (i.e. the channel set in the guild's widget settings) in this guild.
/// </summary>
/// <param name="mode">The <see cref="CacheMode" /> that determines whether the object should be fetched from cache.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the widget channel set
/// within the server's widget settings; <see langword="null" /> if none is set.
/// </returns>
Task<IGuildChannel> GetWidgetChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Gets the text channel where Community guilds can display rules and/or guidelines.
/// </summary>
/// <param name="mode">The <see cref="CacheMode"/> that determines whether the object should be fetched from cache.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the text channel
/// where Community guilds can display rules and/or guidelines; <see langword="null" /> if none is set.
/// </returns>
Task<ITextChannel> GetRulesChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Gets the text channel channel where admins and moderators of Community guilds receive notices from Discord.
/// </summary>
/// <param name="mode">The <see cref="CacheMode"/> that determines whether the object should be fetched from cache.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the text channel channel where
/// admins and moderators of Community guilds receive notices from Discord; <see langword="null" /> if none is set.
/// </returns>
Task<ITextChannel> GetPublicUpdatesChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);

/// <summary>
/// Creates a new text channel in this guild.
@@ -573,7 +700,7 @@ namespace Discord
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the partial metadata of
/// the vanity invite found within this guild; <c>null</c> if none is found.
/// the vanity invite found within this guild; <see langword="null" /> if none is found.
/// </returns>
Task<IInviteMetadata> GetVanityInviteAsync(RequestOptions options = null);

@@ -582,7 +709,7 @@ namespace Discord
/// </summary>
/// <param name="id">The snowflake identifier for the role.</param>
/// <returns>
/// A role that is associated with the specified <paramref name="id"/>; <c>null</c> if none is found.
/// A role that is associated with the specified <paramref name="id"/>; <see langword="null" /> if none is found.
/// </returns>
IRole GetRole(ulong id);
/// <summary>
@@ -624,7 +751,7 @@ namespace Discord
/// <param name="accessToken">The OAuth2 access token for the user, requested with the guilds.join scope.</param>
/// <param name="func">The delegate containing the properties to be applied to the user upon being added to the guild.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>A guild user associated with the specified <paramref name="userId" />; <c>null</c> if the user is already in the guild.</returns>
/// <returns>A guild user associated with the specified <paramref name="userId" />; <see langword="null" /> if the user is already in the guild.</returns>
Task<IGuildUser> AddGuildUserAsync(ulong userId, string accessToken, Action<AddGuildUserProperties> func = null, RequestOptions options = null);
/// <summary>
/// Gets a collection of all users in this guild.
@@ -649,7 +776,7 @@ namespace Discord
/// <remarks>
/// This method retrieves a user found within this guild.
/// <note>
/// This may return <c>null</c> in the WebSocket implementation due to incomplete user collection in
/// This may return <see langword="null" /> in the WebSocket implementation due to incomplete user collection in
/// large guilds.
/// </note>
/// </remarks>
@@ -658,7 +785,7 @@ namespace Discord
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the guild user
/// associated with the specified <paramref name="id"/>; <c>null</c> if none is found.
/// associated with the specified <paramref name="id"/>; <see langword="null" /> if none is found.
/// </returns>
Task<IGuildUser> GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
@@ -705,11 +832,12 @@ namespace Discord
/// <param name="days">The number of days required for the users to be kicked.</param>
/// <param name="simulate">Whether this prune action is a simulation.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <param name="includeRoleIds">An array of role IDs to be included in the prune of users who do not have any additional roles.</param>
/// <returns>
/// A task that represents the asynchronous prune operation. The task result contains the number of users to
/// be or has been removed from this guild.
/// </returns>
Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null);
Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null, IEnumerable<ulong> includeRoleIds = null);
/// <summary>
/// Gets a collection of users in this guild that the name or nickname starts with the
/// provided <see cref="string"/> at <paramref name="query"/>.
@@ -751,7 +879,7 @@ namespace Discord
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the webhook with the
/// specified <paramref name="id"/>; <c>null</c> if none is found.
/// specified <paramref name="id"/>; <see langword="null" /> if none is found.
/// </returns>
Task<IWebhook> GetWebhookAsync(ulong id, RequestOptions options = null);
/// <summary>
@@ -771,7 +899,7 @@ namespace Discord
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the emote found with the
/// specified <paramref name="id"/>; <c>null</c> if none is found.
/// specified <paramref name="id"/>; <see langword="null" /> if none is found.
/// </returns>
Task<GuildEmote> GetEmoteAsync(ulong id, RequestOptions options = null);
/// <summary>


+ 12
- 0
src/Discord.Net.Core/Entities/IApplication.cs View File

@@ -22,6 +22,18 @@ namespace Discord
/// Gets the icon URL of the application.
/// </summary>
string IconUrl { get; }
/// <summary>
/// Gets if the bot is public.
/// </summary>
bool IsBotPublic { get; }
/// <summary>
/// Gets if the bot requires code grant.
/// </summary>
bool BotRequiresCodeGrant { get; }
/// <summary>
/// Gets the team associated with this application if there is one.
/// </summary>
ITeam Team { get; }

/// <summary>
/// Gets the partial user object containing info on the owner of the application.


+ 1
- 0
src/Discord.Net.Core/Entities/IUpdateable.cs View File

@@ -10,6 +10,7 @@ namespace Discord
/// <summary>
/// Updates this object's properties with its current state.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
Task UpdateAsync(RequestOptions options = null);
}
}

+ 14
- 0
src/Discord.Net.Core/Entities/Invites/TargetUserType.cs View File

@@ -0,0 +1,14 @@
namespace Discord
{
public enum TargetUserType
{
/// <summary>
/// The invite whose target user type is not defined.
/// </summary>
Undefined = 0,
/// <summary>
/// The invite is for a Go Live stream.
/// </summary>
Stream = 1
}
}

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

@@ -39,6 +39,13 @@ namespace Discord
/// </returns>
bool IsSuppressed { get; }
/// <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.
/// </summary>
/// <returns>


+ 1
- 1
src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs View File

@@ -17,7 +17,7 @@ namespace Discord
/// <summary> Gets a <see cref="ChannelPermissions"/> that grants all permissions for category channels. </summary>
public static readonly ChannelPermissions Category = new ChannelPermissions(0b01100_1111110_1111111110001_010001);
/// <summary> Gets a <see cref="ChannelPermissions"/> that grants all permissions for direct message channels. </summary>
public static readonly ChannelPermissions DM = new ChannelPermissions(0b00000_1000110_1011100110000_000000);
public static readonly ChannelPermissions DM = new ChannelPermissions(0b00000_1000110_1011100110001_000000);
/// <summary> Gets a <see cref="ChannelPermissions"/> that grants all permissions for group channels. </summary>
public static readonly ChannelPermissions Group = new ChannelPermissions(0b00000_1000110_0001101100000_000000);
/// <summary> Gets a <see cref="ChannelPermissions"/> that grants all permissions for a given channel type. </summary>


+ 4
- 0
src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs View File

@@ -51,6 +51,10 @@ namespace Discord
/// authentication when used on a guild that has server-wide 2FA enabled.
/// </remarks>
ManageGuild = 0x00_00_00_20,
/// <summary>
/// Allows for viewing of guild insights
/// </summary>
ViewGuildInsights = 0x00_08_00_00,

// Text
/// <summary>


+ 9
- 2
src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs View File

@@ -12,7 +12,7 @@ namespace Discord
/// <summary> Gets a <see cref="GuildPermissions"/> that grants all guild permissions for webhook users. </summary>
public static readonly GuildPermissions Webhook = new GuildPermissions(0b00000_0000000_0001101100000_000000);
/// <summary> Gets a <see cref="GuildPermissions"/> that grants all guild permissions. </summary>
public static readonly GuildPermissions All = new GuildPermissions(0b11111_1111110_1111111111111_111111);
public static readonly GuildPermissions All = new GuildPermissions(0b11111_1111111_1111111111111_111111);

/// <summary> Gets a packed value representing all the permissions in this <see cref="GuildPermissions"/>. </summary>
public ulong RawValue { get; }
@@ -34,6 +34,8 @@ namespace Discord
public bool AddReactions => Permissions.GetValue(RawValue, GuildPermission.AddReactions);
/// <summary> If <c>true</c>, a user may view the audit log. </summary>
public bool ViewAuditLog => Permissions.GetValue(RawValue, GuildPermission.ViewAuditLog);
/// <summary> If <c>true</c>, a user may view the guild insights. </summary>
public bool ViewGuildInsights => Permissions.GetValue(RawValue, GuildPermission.ViewGuildInsights);

/// <summary> If True, a user may join channels. </summary>
[Obsolete("Use ViewChannel instead.")]
@@ -97,6 +99,7 @@ namespace Discord
bool? manageGuild = null,
bool? addReactions = null,
bool? viewAuditLog = null,
bool? viewGuildInsights = null,
bool? viewChannel = null,
bool? sendMessages = null,
bool? sendTTSMessages = null,
@@ -130,6 +133,7 @@ namespace Discord
Permissions.SetValue(ref value, manageGuild, GuildPermission.ManageGuild);
Permissions.SetValue(ref value, addReactions, GuildPermission.AddReactions);
Permissions.SetValue(ref value, viewAuditLog, GuildPermission.ViewAuditLog);
Permissions.SetValue(ref value, viewGuildInsights, GuildPermission.ViewGuildInsights);
Permissions.SetValue(ref value, viewChannel, GuildPermission.ViewChannel);
Permissions.SetValue(ref value, sendMessages, GuildPermission.SendMessages);
Permissions.SetValue(ref value, sendTTSMessages, GuildPermission.SendTTSMessages);
@@ -166,6 +170,7 @@ namespace Discord
bool manageGuild = false,
bool addReactions = false,
bool viewAuditLog = false,
bool viewGuildInsights = false,
bool viewChannel = false,
bool sendMessages = false,
bool sendTTSMessages = false,
@@ -198,6 +203,7 @@ namespace Discord
manageGuild: manageGuild,
addReactions: addReactions,
viewAuditLog: viewAuditLog,
viewGuildInsights: viewGuildInsights,
viewChannel: viewChannel,
sendMessages: sendMessages,
sendTTSMessages: sendTTSMessages,
@@ -231,6 +237,7 @@ namespace Discord
bool? manageGuild = null,
bool? addReactions = null,
bool? viewAuditLog = null,
bool? viewGuildInsights = null,
bool? viewChannel = null,
bool? sendMessages = null,
bool? sendTTSMessages = null,
@@ -254,7 +261,7 @@ namespace Discord
bool? manageWebhooks = null,
bool? manageEmojis = null)
=> new GuildPermissions(RawValue, createInstantInvite, kickMembers, banMembers, administrator, manageChannels, manageGuild, addReactions,
viewAuditLog, viewChannel, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles,
viewAuditLog, viewGuildInsights, viewChannel, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles,
readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, moveMembers,
useVoiceActivation, prioritySpeaker, stream, changeNickname, manageNicknames, manageRoles, manageWebhooks, manageEmojis);



+ 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);
/// <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);
/// <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>
public PermValue ManageRoles => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ManageRoles);
@@ -109,7 +113,9 @@ namespace Discord
PermValue? moveMembers = null,
PermValue? useVoiceActivation = 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, manageChannel, ChannelPermission.ManageChannels);
@@ -129,6 +135,8 @@ namespace Discord
Permissions.SetValue(ref allowValue, ref denyValue, deafenMembers, ChannelPermission.DeafenMembers);
Permissions.SetValue(ref allowValue, ref denyValue, moveMembers, ChannelPermission.MoveMembers);
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, manageWebhooks, ChannelPermission.ManageWebhooks);

@@ -159,10 +167,12 @@ namespace Discord
PermValue moveMembers = PermValue.Inherit,
PermValue useVoiceActivation = 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,
embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers,
moveMembers, useVoiceActivation, manageRoles, manageWebhooks) { }
moveMembers, useVoiceActivation, manageRoles, manageWebhooks, prioritySpeaker, stream) { }

/// <summary>
/// Initializes a new <see cref="OverwritePermissions" /> from the current one, changing the provided
@@ -188,10 +198,12 @@ namespace Discord
PermValue? moveMembers = null,
PermValue? useVoiceActivation = 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,
embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers,
moveMembers, useVoiceActivation, manageRoles, manageWebhooks);
moveMembers, useVoiceActivation, manageRoles, manageWebhooks, prioritySpeaker, stream);

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


+ 27
- 0
src/Discord.Net.Core/Entities/Teams/ITeam.cs View File

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

namespace Discord
{
/// <summary>
/// Represents a Discord Team.
/// </summary>
public interface ITeam
{
/// <summary>
/// Gets the team icon url.
/// </summary>
string IconUrl { get; }
/// <summary>
/// Gets the team unique identifier.
/// </summary>
ulong Id { get; }
/// <summary>
/// Gets the members of this team.
/// </summary>
IReadOnlyList<ITeamMember> TeamMembers { get; }
/// <summary>
/// Gets the user identifier that owns this team.
/// </summary>
ulong OwnerUserId { get; }
}
}

+ 25
- 0
src/Discord.Net.Core/Entities/Teams/ITeamMember.cs View File

@@ -0,0 +1,25 @@
namespace Discord
{
/// <summary>
/// Represents a Discord Team member.
/// </summary>
public interface ITeamMember
{
/// <summary>
/// Gets the membership state of this team member.
/// </summary>
MembershipState MembershipState { get; }
/// <summary>
/// Gets the permissions of this team member.
/// </summary>
string[] Permissions { get; }
/// <summary>
/// Gets the team unique identifier for this team member.
/// </summary>
ulong TeamId { get; }
/// <summary>
/// Gets the Discord user of this team member.
/// </summary>
IUser User { get; }
}
}

+ 11
- 0
src/Discord.Net.Core/Entities/Teams/MembershipState.cs View File

@@ -0,0 +1,11 @@
namespace Discord
{
/// <summary>
/// Represents the membership state of a team member.
/// </summary>
public enum MembershipState
{
Invited,
Accepted,
}
}

+ 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.
/// </summary>
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)
{
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>


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

@@ -274,5 +274,15 @@ namespace Discord
/// that represents the number of shards that should be used with this account.
/// </returns>
Task<int> GetRecommendedShardCountAsync(RequestOptions options = null);

/// <summary>
/// Gets the gateway information related to the bot.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains a <see cref="BotGateway"/>
/// that represents the gateway information related to the bot.
/// </returns>
Task<BotGateway> GetBotGatewayAsync(RequestOptions options = null);
}
}

+ 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>
/// <returns>
/// 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.
/// </returns>
public HttpStatusCode HttpCode { get; }
@@ -22,7 +22,7 @@ namespace Discord.Net
/// </summary>
/// <returns>
/// 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.
/// </returns>
public int? DiscordCode { get; }


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

@@ -11,7 +11,7 @@ namespace Discord.Net
/// </summary>
/// <returns>
/// 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.
/// </returns>
public int CloseCode { get; }


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

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

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

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

internal static RequestOptions CreateOrClone(RequestOptions options)
{


+ 7
- 1
src/Discord.Net.Rest/API/Common/Application.cs View File

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

namespace Discord.API
@@ -15,6 +15,12 @@ namespace Discord.API
public ulong Id { get; set; }
[JsonProperty("icon")]
public string Icon { get; set; }
[JsonProperty("bot_public")]
public bool IsBotPublic { get; set; }
[JsonProperty("bot_require_code_grant")]
public bool BotRequiresCodeGrant { get; set; }
[JsonProperty("team")]
public Team Team { get; set; }

[JsonProperty("flags"), Int53]
public Optional<ulong> Flags { get; set; }


+ 22
- 2
src/Discord.Net.Rest/API/Common/Guild.cs View File

@@ -13,6 +13,8 @@ namespace Discord.API
public string Icon { get; set; }
[JsonProperty("splash")]
public string Splash { get; set; }
[JsonProperty("discovery_splash")]
public string DiscoverySplash { get; set; }
[JsonProperty("owner_id")]
public ulong OwnerId { get; set; }
[JsonProperty("region")]
@@ -22,9 +24,9 @@ namespace Discord.API
[JsonProperty("afk_timeout")]
public int AFKTimeout { get; set; }
[JsonProperty("embed_enabled")]
public bool EmbedEnabled { get; set; }
public Optional<bool> EmbedEnabled { get; set; }
[JsonProperty("embed_channel_id")]
public ulong? EmbedChannelId { get; set; }
public Optional<ulong?> EmbedChannelId { get; set; }
[JsonProperty("verification_level")]
public VerificationLevel VerificationLevel { get; set; }
[JsonProperty("default_message_notifications")]
@@ -43,6 +45,10 @@ namespace Discord.API
public MfaLevel MfaLevel { get; set; }
[JsonProperty("application_id")]
public ulong? ApplicationId { get; set; }
[JsonProperty("widget_enabled")]
public Optional<bool> WidgetEnabled { get; set; }
[JsonProperty("widget_channel_id")]
public Optional<ulong?> WidgetChannelId { get; set; }
[JsonProperty("system_channel_id")]
public ulong? SystemChannelId { get; set; }
[JsonProperty("premium_tier")]
@@ -56,9 +62,23 @@ namespace Discord.API
// this value is inverted, flags set will turn OFF features
[JsonProperty("system_channel_flags")]
public SystemChannelMessageDeny SystemChannelFlags { get; set; }
[JsonProperty("rules_channel_id")]
public ulong? RulesChannelId { get; set; }
[JsonProperty("max_presences")]
public Optional<int?> MaxPresences { get; set; }
[JsonProperty("max_members")]
public Optional<int> MaxMembers { get; set; }
[JsonProperty("premium_subscription_count")]
public int? PremiumSubscriptionCount { get; set; }
[JsonProperty("preferred_locale")]
public string PreferredLocale { get; set; }
[JsonProperty("public_updates_channel_id")]
public ulong? PublicUpdatesChannelId { get; set; }
[JsonProperty("max_video_channel_users")]
public Optional<int> MaxVideoChannelUsers { get; set; }
[JsonProperty("approximate_member_count")]
public Optional<int> ApproximateMemberCount { get; set; }
[JsonProperty("approximate_presence_count")]
public Optional<int> ApproximatePresenceCount { get; set; }
}
}

+ 2
- 2
src/Discord.Net.Rest/API/Common/GuildEmbed.cs View File

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

namespace Discord.API
@@ -8,6 +8,6 @@ namespace Discord.API
[JsonProperty("enabled")]
public bool Enabled { get; set; }
[JsonProperty("channel_id")]
public ulong ChannelId { get; set; }
public ulong? ChannelId { get; set; }
}
}

+ 13
- 0
src/Discord.Net.Rest/API/Common/GuildWidget.cs View File

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

namespace Discord.API
{
internal class GuildWidget
{
[JsonProperty("enabled")]
public bool Enabled { get; set; }
[JsonProperty("channel_id")]
public ulong? ChannelId { get; set; }
}
}

+ 9
- 0
src/Discord.Net.Rest/API/Common/MembershipState.cs View File

@@ -0,0 +1,9 @@
namespace Discord.API
{
internal enum MembershipState
{
None = 0,
Invited = 1,
Accepted = 2,
}
}

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

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

namespace Discord.API
@@ -26,5 +27,9 @@ namespace Discord.API
// "client_status": { "desktop": "dnd", "mobile": "dnd" }
[JsonProperty("client_status")]
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; }
}
}

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

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

namespace Discord.API.Rest
{
internal class SessionStartLimit
{
[JsonProperty("total")]
public int Total { get; set; }
[JsonProperty("remaining")]
public int Remaining { get; set; }
[JsonProperty("reset_after")]
public int ResetAfter { get; set; }
[JsonProperty("max_concurrency")]
public int MaxConcurrency { get; set; }
}
}

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

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

namespace Discord.API
{
internal class Team
{
[JsonProperty("icon")]
public Optional<string> Icon { get; set; }
[JsonProperty("id")]
public ulong Id { get; set; }
[JsonProperty("members")]
public TeamMember[] TeamMembers { get; set; }
[JsonProperty("owner_user_id")]
public ulong OwnerUserId { get; set; }
}
}

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

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

namespace Discord.API
{
internal class TeamMember
{
[JsonProperty("membership_state")]
public MembershipState MembershipState { get; set; }
[JsonProperty("permissions")]
public string[] Permissions { get; set; }
[JsonProperty("team_id")]
public ulong TeamId { get; set; }
[JsonProperty("user")]
public User User { 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; }
[JsonProperty("position")]
public Optional<int> Position { get; set; }
[JsonProperty("permission_overwrites")]
public Optional<Overwrite[]> Overwrites { get; set; }

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

//Voice channels
[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;

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

public CreateWebhookMessageParams(string content)
{


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

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

namespace Discord.API.Rest
@@ -9,5 +9,7 @@ namespace Discord.API.Rest
public string Url { get; set; }
[JsonProperty("shards")]
public int Shards { get; set; }
[JsonProperty("session_start_limit")]
public SessionStartLimit SessionStartLimit { get; set; }
}
}

+ 5
- 1
src/Discord.Net.Rest/API/Rest/GuildPruneParams.cs View File

@@ -9,9 +9,13 @@ namespace Discord.API.Rest
[JsonProperty("days")]
public int Days { get; }

public GuildPruneParams(int days)
[JsonProperty("include_roles")]
public ulong[] IncludeRoleIds { get; }

public GuildPruneParams(int days, ulong[] includeRoleIds)
{
Days = days;
IncludeRoleIds = includeRoleIds;
}
}
}

+ 14
- 0
src/Discord.Net.Rest/API/Rest/ModifyGuildWidgetParams.cs View File

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

namespace Discord.API.Rest
{
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
internal class ModifyGuildWidgetParams
{
[JsonProperty("enabled")]
public Optional<bool> Enabled { get; set; }
[JsonProperty("channel")]
public Optional<ulong?> ChannelId { get; set; }
}
}

+ 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> AvatarUrl { get; set; }
public Optional<Embed[]> Embeds { get; set; }
public Optional<AllowedMentions> AllowedMentions { get; set; }

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

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

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


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

@@ -46,12 +46,12 @@ namespace Discord.Rest
_restLogger = LogManager.CreateLogger("Rest");
_isFirstLogin = config.DisplayInitialLog;

ApiClient.RequestQueue.RateLimitTriggered += async (id, info) =>
ApiClient.RequestQueue.RateLimitTriggered += async (id, info, endpoint) =>
{
if (info == null)
await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {id ?? "null"}").ConfigureAwait(false);
await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {endpoint} {(id.IsHashBucket ? $"(Bucket: {id.BucketHash})" : "")}").ConfigureAwait(false);
else
await _restLogger.WarningAsync($"Rate limit triggered: {id ?? "null"}").ConfigureAwait(false);
await _restLogger.WarningAsync($"Rate limit triggered: {endpoint} {(id.IsHashBucket ? $"(Bucket: {id.BucketHash})" : "")}").ConfigureAwait(false);
};
ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false);
}
@@ -152,6 +152,10 @@ namespace Discord.Rest
public Task<int> GetRecommendedShardCountAsync(RequestOptions options = null)
=> ClientHelper.GetRecommendShardCountAsync(this, options);

/// <inheritdoc />
public Task<BotGateway> GetBotGatewayAsync(RequestOptions options = null)
=> ClientHelper.GetBotGatewayAsync(this, options);

//IDiscordClient
/// <inheritdoc />
ConnectionState IDiscordClient.ConnectionState => ConnectionState.Disconnected;


+ 30
- 5
src/Discord.Net.Rest/ClientHelper.cs View File

@@ -62,9 +62,9 @@ namespace Discord.Rest
}
public static async Task<RestGuild> GetGuildAsync(BaseDiscordClient client,
ulong id, RequestOptions options)
ulong id, bool withCounts, RequestOptions options)
{
var model = await client.ApiClient.GetGuildAsync(id, options).ConfigureAwait(false);
var model = await client.ApiClient.GetGuildAsync(id, withCounts, options).ConfigureAwait(false);
if (model != null)
return RestGuild.Create(client, model);
return null;
@@ -77,6 +77,14 @@ namespace Discord.Rest
return RestGuildEmbed.Create(model);
return null;
}
public static async Task<RestGuildWidget?> GetGuildWidgetAsync(BaseDiscordClient client,
ulong id, RequestOptions options)
{
var model = await client.ApiClient.GetGuildWidgetAsync(id, options).ConfigureAwait(false);
if (model != null)
return RestGuildWidget.Create(model);
return null;
}
public static IAsyncEnumerable<IReadOnlyCollection<RestUserGuild>> GetGuildSummariesAsync(BaseDiscordClient client,
ulong? fromGuildId, int? limit, RequestOptions options)
{
@@ -106,13 +114,13 @@ namespace Discord.Rest
count: limit
);
}
public static async Task<IReadOnlyCollection<RestGuild>> GetGuildsAsync(BaseDiscordClient client, RequestOptions options)
public static async Task<IReadOnlyCollection<RestGuild>> GetGuildsAsync(BaseDiscordClient client, bool withCounts, RequestOptions options)
{
var summaryModels = await GetGuildSummariesAsync(client, null, null, options).FlattenAsync().ConfigureAwait(false);
var guilds = ImmutableArray.CreateBuilder<RestGuild>();
foreach (var summaryModel in summaryModels)
{
var guildModel = await client.ApiClient.GetGuildAsync(summaryModel.Id).ConfigureAwait(false);
var guildModel = await client.ApiClient.GetGuildAsync(summaryModel.Id, withCounts).ConfigureAwait(false);
if (guildModel != null)
guilds.Add(RestGuild.Create(client, guildModel));
}
@@ -140,7 +148,7 @@ namespace Discord.Rest
public static async Task<RestGuildUser> GetGuildUserAsync(BaseDiscordClient client,
ulong guildId, ulong id, RequestOptions options)
{
var guild = await GetGuildAsync(client, guildId, options).ConfigureAwait(false);
var guild = await GetGuildAsync(client, guildId, false, options).ConfigureAwait(false);
if (guild == null)
return null;

@@ -176,5 +184,22 @@ namespace Discord.Rest
var response = await client.ApiClient.GetBotGatewayAsync(options).ConfigureAwait(false);
return response.Shards;
}

public static async Task<BotGateway> GetBotGatewayAsync(BaseDiscordClient client, RequestOptions options)
{
var response = await client.ApiClient.GetBotGatewayAsync(options).ConfigureAwait(false);
return new BotGateway
{
Url = response.Url,
Shards = response.Shards,
SessionStartLimit = new SessionStartLimit
{
Total = response.SessionStartLimit.Total,
Remaining = response.SessionStartLimit.Remaining,
ResetAfter = response.SessionStartLimit.ResetAfter,
MaxConcurrency = response.SessionStartLimit.MaxConcurrency
}
};
}
}
}

+ 88
- 40
src/Discord.Net.Rest/DiscordRestApiClient.cs View File

@@ -24,7 +24,7 @@ namespace Discord.API
{
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); } }
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>
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)
{
@@ -133,7 +129,7 @@ namespace Discord.API
RestClient.SetCancelToken(_loginCancelToken.Token);

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

@@ -180,9 +176,9 @@ namespace Discord.API
//Core
internal Task SendAsync(string method, Expression<Func<string>> endpointExpr, BucketIds ids,
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,
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.HeaderOnly = true;
@@ -194,9 +190,9 @@ namespace Discord.API

internal Task SendJsonAsync(string method, Expression<Func<string>> endpointExpr, object payload, BucketIds ids,
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,
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.HeaderOnly = true;
@@ -209,9 +205,9 @@ namespace Discord.API

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)
=> 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,
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.HeaderOnly = true;
@@ -223,9 +219,9 @@ namespace Discord.API

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
=> 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,
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.BucketId = bucketId;
@@ -236,9 +232,9 @@ namespace Discord.API

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
=> 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,
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.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,
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,
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.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));
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>
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));
}

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)
{
@@ -789,7 +787,7 @@ namespace Discord.API
}

//Guilds
public async Task<Guild> GetGuildAsync(ulong guildId, RequestOptions options = null)
public async Task<Guild> GetGuildAsync(ulong guildId, bool withCounts, RequestOptions options = null)
{
Preconditions.NotEqual(guildId, 0, nameof(guildId));
options = RequestOptions.CreateOrClone(options);
@@ -797,7 +795,7 @@ namespace Discord.API
try
{
var ids = new BucketIds(guildId: guildId);
return await SendAsync<Guild>("GET", () => $"guilds/{guildId}", ids, options: options).ConfigureAwait(false);
return await SendAsync<Guild>("GET", () => $"guilds/{guildId}?with_counts={(withCounts ? "true" : "false")}", ids, options: options).ConfigureAwait(false);
}
catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; }
}
@@ -855,10 +853,11 @@ namespace Discord.API
Preconditions.NotEqual(guildId, 0, nameof(guildId));
Preconditions.NotNull(args, nameof(args));
Preconditions.AtLeast(args.Days, 1, nameof(args.Days));
string endpointRoleIds = args.IncludeRoleIds?.Length > 0 ? $"&include_roles={string.Join(",", args.IncludeRoleIds)}" : "";
options = RequestOptions.CreateOrClone(options);

var ids = new BucketIds(guildId: guildId);
return await SendAsync<GetGuildPruneCountResponse>("GET", () => $"guilds/{guildId}/prune?days={args.Days}", ids, options: options).ConfigureAwait(false);
return await SendAsync<GetGuildPruneCountResponse>("GET", () => $"guilds/{guildId}/prune?days={args.Days}{endpointRoleIds}", ids, options: options).ConfigureAwait(false);
}

//Guild Bans
@@ -876,8 +875,12 @@ namespace Discord.API
Preconditions.NotEqual(guildId, 0, nameof(guildId));
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">
/// <paramref name="guildId"/> and <paramref name="userId"/> must not be equal to zero.
@@ -935,6 +938,32 @@ namespace Discord.API
return await SendJsonAsync<GuildEmbed>("PATCH", () => $"guilds/{guildId}/embed", args, ids, options: options).ConfigureAwait(false);
}

//Guild Widget
/// <exception cref="ArgumentException"><paramref name="guildId"/> must not be equal to zero.</exception>
public async Task<GuildWidget> GetGuildWidgetAsync(ulong guildId, RequestOptions options = null)
{
Preconditions.NotEqual(guildId, 0, nameof(guildId));
options = RequestOptions.CreateOrClone(options);

try
{
var ids = new BucketIds(guildId: guildId);
return await SendAsync<GuildWidget>("GET", () => $"guilds/{guildId}/widget", ids, options: options).ConfigureAwait(false);
}
catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; }
}
/// <exception cref="ArgumentException"><paramref name="guildId"/> must not be equal to zero.</exception>
/// <exception cref="ArgumentNullException"><paramref name="args"/> must not be <see langword="null"/>.</exception>
public async Task<GuildWidget> ModifyGuildWidgetAsync(ulong guildId, Rest.ModifyGuildWidgetParams args, RequestOptions options = null)
{
Preconditions.NotNull(args, nameof(args));
Preconditions.NotEqual(guildId, 0, nameof(guildId));
options = RequestOptions.CreateOrClone(options);

var ids = new BucketIds(guildId: guildId);
return await SendJsonAsync<GuildWidget>("PATCH", () => $"guilds/{guildId}/widget", args, ids, options: options).ConfigureAwait(false);
}

//Guild Integrations
/// <exception cref="ArgumentException"><paramref name="guildId"/> must not be equal to zero.</exception>
public async Task<IReadOnlyCollection<Integration>> GetGuildIntegrationsAsync(ulong guildId, RequestOptions options = null)
@@ -1470,21 +1499,39 @@ namespace Discord.API
{
public ulong GuildId { 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;
ChannelId = channelId;
WebhookId = webhookId;
}

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)
{
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:
return null;
}
@@ -1495,18 +1542,19 @@ namespace Discord.API
{
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);
}

private static Func<BucketIds, string> CreateBucketId(Expression<Func<string>> endpoint)
private static Func<BucketIds, BucketId> CreateBucketId(Expression<Func<string>> endpoint)
{
try
{
//Is this a constant string?
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 methodCall = endpoint.Body as MethodCallExpression;
@@ -1543,7 +1591,7 @@ namespace Discord.API

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

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

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


+ 14
- 6
src/Discord.Net.Rest/DiscordRestClient.cs View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
@@ -28,10 +29,10 @@ namespace Discord.Rest
internal DiscordRestClient(DiscordRestConfig config, API.DiscordRestApiClient api) : base(config, api) { }

private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config)
=> new API.DiscordRestApiClient(config.RestClientProvider,
DiscordRestConfig.UserAgent,
rateLimitPrecision: config.RateLimitPrecision,
useSystemClock: config.UseSystemClock);
=> new API.DiscordRestApiClient(config.RestClientProvider,
DiscordRestConfig.UserAgent,
rateLimitPrecision: config.RateLimitPrecision,
useSystemClock: config.UseSystemClock);

internal override void Dispose(bool disposing)
{
@@ -76,15 +77,22 @@ namespace Discord.Rest
=> ClientHelper.GetInviteAsync(this, inviteId, options);

public Task<RestGuild> GetGuildAsync(ulong id, RequestOptions options = null)
=> ClientHelper.GetGuildAsync(this, id, options);
=> ClientHelper.GetGuildAsync(this, id, false, options);
public Task<RestGuild> GetGuildAsync(ulong id, bool withCounts, RequestOptions options = null)
=> ClientHelper.GetGuildAsync(this, id, withCounts, options);
[Obsolete("This endpoint is deprecated, use GetGuildWidgetAsync instead.")]
public Task<RestGuildEmbed?> GetGuildEmbedAsync(ulong id, RequestOptions options = null)
=> ClientHelper.GetGuildEmbedAsync(this, id, options);
public Task<RestGuildWidget?> GetGuildWidgetAsync(ulong id, RequestOptions options = null)
=> ClientHelper.GetGuildWidgetAsync(this, id, options);
public IAsyncEnumerable<IReadOnlyCollection<RestUserGuild>> GetGuildSummariesAsync(RequestOptions options = null)
=> ClientHelper.GetGuildSummariesAsync(this, null, null, options);
public IAsyncEnumerable<IReadOnlyCollection<RestUserGuild>> GetGuildSummariesAsync(ulong fromGuildId, int limit, RequestOptions options = null)
=> ClientHelper.GetGuildSummariesAsync(this, fromGuildId, limit, options);
public Task<IReadOnlyCollection<RestGuild>> GetGuildsAsync(RequestOptions options = null)
=> ClientHelper.GetGuildsAsync(this, options);
=> ClientHelper.GetGuildsAsync(this, false, options);
public Task<IReadOnlyCollection<RestGuild>> GetGuildsAsync(bool withCounts, RequestOptions options = null)
=> ClientHelper.GetGuildsAsync(this, withCounts, options);
public Task<RestGuild> CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null, RequestOptions options = null)
=> ClientHelper.CreateGuildAsync(this, name, region, jpegIcon, options);



+ 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 code = codeModel.NewValue.ToObject<string>(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 uses = usesModel.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);
}
@@ -70,10 +74,10 @@ namespace Discord.Rest
/// </returns>
public bool Temporary { get; }
/// <summary>
/// Gets the user that created this invite.
/// Gets the user that created this invite if available.
/// </summary>
/// <returns>
/// A user that created this invite.
/// A user that created this invite or <see langword="null"/>.
/// </returns>
public IUser Creator { get; }
/// <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 code = codeModel.OldValue.ToObject<string>(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 uses = usesModel.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);
}
@@ -70,10 +74,10 @@ namespace Discord.Rest
/// </returns>
public bool Temporary { get; }
/// <summary>
/// Gets the user that created this invite.
/// Gets the user that created this invite if available.
/// </summary>
/// <returns>
/// A user that created this invite.
/// A user that created this invite or <see langword="null"/>.
/// </returns>
public IUser Creator { get; }
/// <summary>


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

@@ -28,7 +28,16 @@ namespace Discord.Rest
{
Name = args.Name,
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);
}
@@ -46,6 +55,15 @@ namespace Discord.Rest
Topic = args.Topic,
IsNsfw = args.IsNsfw,
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);
}
@@ -61,7 +79,16 @@ namespace Discord.Rest
Name = args.Name,
Position = args.Position,
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);
}
@@ -413,7 +440,8 @@ namespace Discord.Rest
var apiArgs = new ModifyGuildChannelParams
{
Overwrites = category.PermissionOverwrites
.Select(overwrite => new API.Overwrite{
.Select(overwrite => new API.Overwrite
{
TargetId = overwrite.TargetId,
TargetType = overwrite.TargetType,
Allow = overwrite.Permissions.AllowValue,


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

@@ -12,7 +12,7 @@ namespace Discord.Rest
/// Represents a REST-based news channel in a guild that has the same properties as a <see cref="RestTextChannel"/>.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class RestNewsChannel : RestTextChannel
public class RestNewsChannel : RestTextChannel, INewsChannel
{
internal RestNewsChannel(BaseDiscordClient discord, IGuild guild, ulong id)
:base(discord, guild, id)


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

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



+ 59
- 9
src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs View File

@@ -5,6 +5,7 @@ using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using EmbedModel = Discord.API.GuildEmbed;
using WidgetModel = Discord.API.GuildWidget;
using Model = Discord.API.Guild;
using RoleModel = Discord.API.Role;
using ImageModel = Discord.API.Image;
@@ -99,6 +100,27 @@ namespace Discord.Rest

return await client.ApiClient.ModifyGuildEmbedAsync(guild.Id, apiArgs, options).ConfigureAwait(false);
}
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <c>null</c>.</exception>
public static async Task<WidgetModel> ModifyWidgetAsync(IGuild guild, BaseDiscordClient client,
Action<GuildWidgetProperties> func, RequestOptions options)
{
if (func == null)
throw new ArgumentNullException(nameof(func));

var args = new GuildWidgetProperties();
func(args);
var apiArgs = new API.Rest.ModifyGuildWidgetParams
{
Enabled = args.Enabled
};

if (args.Channel.IsSpecified)
apiArgs.ChannelId = args.Channel.Value?.Id;
else if (args.ChannelId.IsSpecified)
apiArgs.ChannelId = args.ChannelId.Value;

return await client.ApiClient.ModifyGuildWidgetAsync(guild.Id, apiArgs, options).ConfigureAwait(false);
}
public static async Task ReorderChannelsAsync(IGuild guild, BaseDiscordClient client,
IEnumerable<ReorderChannelProperties> args, RequestOptions options)
{
@@ -132,7 +154,7 @@ namespace Discord.Rest
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);
return RestBan.Create(client, model);
return model == null ? null : RestBan.Create(client, model);
}

public static async Task AddBanAsync(IGuild guild, BaseDiscordClient client,
@@ -176,7 +198,17 @@ namespace Discord.Rest
CategoryId = props.CategoryId,
Topic = props.Topic,
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);
return RestTextChannel.Create(client, guild, model);
@@ -195,7 +227,16 @@ namespace Discord.Rest
CategoryId = props.CategoryId,
Bitrate = props.Bitrate,
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);
return RestVoiceChannel.Create(client, guild, model);
@@ -211,7 +252,16 @@ namespace Discord.Rest

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);
@@ -376,9 +426,9 @@ namespace Discord.Rest
);
}
public static async Task<int> PruneUsersAsync(IGuild guild, BaseDiscordClient client,
int days, bool simulate, RequestOptions options)
int days, bool simulate, RequestOptions options, IEnumerable<ulong> includeRoleIds)
{
var args = new GuildPruneParams(days);
var args = new GuildPruneParams(days, includeRoleIds?.ToArray());
GetGuildPruneCountResponse model;
if (simulate)
model = await client.ApiClient.GetGuildPruneCountAsync(guild.Id, args, options).ConfigureAwait(false);
@@ -451,7 +501,7 @@ namespace Discord.Rest
var emote = await client.ApiClient.GetGuildEmoteAsync(guild.Id, id, options).ConfigureAwait(false);
return emote.ToEntity();
}
public static async Task<GuildEmote> CreateEmoteAsync(IGuild guild, BaseDiscordClient client, string name, Image image, Optional<IEnumerable<IRole>> roles,
public static async Task<GuildEmote> CreateEmoteAsync(IGuild guild, BaseDiscordClient client, string name, Image image, Optional<IEnumerable<IRole>> roles,
RequestOptions options)
{
var apiargs = new CreateGuildEmoteParams
@@ -466,7 +516,7 @@ namespace Discord.Rest
return emote.ToEntity();
}
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <c>null</c>.</exception>
public static async Task<GuildEmote> ModifyEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, Action<EmoteProperties> func,
public static async Task<GuildEmote> ModifyEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, Action<EmoteProperties> func,
RequestOptions options)
{
if (func == null) throw new ArgumentNullException(paramName: nameof(func));
@@ -484,7 +534,7 @@ namespace Discord.Rest
var emote = await client.ApiClient.ModifyGuildEmoteAsync(guild.Id, id, apiargs, options).ConfigureAwait(false);
return emote.ToEntity();
}
public static Task DeleteEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options)
public static Task DeleteEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options)
=> client.ApiClient.DeleteGuildEmoteAsync(guild.Id, id, options);
}
}

+ 177
- 26
src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs View File

@@ -7,6 +7,7 @@ using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using EmbedModel = Discord.API.GuildEmbed;
using WidgetModel = Discord.API.GuildWidget;
using Model = Discord.API.Guild;

namespace Discord.Rest
@@ -28,6 +29,8 @@ namespace Discord.Rest
/// <inheritdoc />
public bool IsEmbeddable { get; private set; }
/// <inheritdoc />
public bool IsWidgetEnabled { get; private set; }
/// <inheritdoc />
public VerificationLevel VerificationLevel { get; private set; }
/// <inheritdoc />
public MfaLevel MfaLevel { get; private set; }
@@ -41,8 +44,14 @@ namespace Discord.Rest
/// <inheritdoc />
public ulong? EmbedChannelId { get; private set; }
/// <inheritdoc />
public ulong? WidgetChannelId { get; private set; }
/// <inheritdoc />
public ulong? SystemChannelId { get; private set; }
/// <inheritdoc />
public ulong? RulesChannelId { get; private set; }
/// <inheritdoc />
public ulong? PublicUpdatesChannelId { get; private set; }
/// <inheritdoc />
public ulong OwnerId { get; private set; }
/// <inheritdoc />
public string VoiceRegionId { get; private set; }
@@ -50,6 +59,8 @@ namespace Discord.Rest
public string IconId { get; private set; }
/// <inheritdoc />
public string SplashId { get; private set; }
/// <inheritdoc />
public string DiscoverySplashId { get; private set; }
internal bool Available { get; private set; }
/// <inheritdoc />
public ulong? ApplicationId { get; private set; }
@@ -67,6 +78,16 @@ namespace Discord.Rest
public int PremiumSubscriptionCount { get; private set; }
/// <inheritdoc />
public string PreferredLocale { get; private set; }
/// <inheritdoc />
public int? MaxPresences { get; private set; }
/// <inheritdoc />
public int? MaxMembers { get; private set; }
/// <inheritdoc />
public int? MaxVideoChannelUsers { get; private set; }
/// <inheritdoc />
public int? ApproximateMemberCount { get; private set; }
/// <inheritdoc />
public int? ApproximatePresenceCount { get; private set; }

/// <inheritdoc />
public CultureInfo PreferredCulture { get; private set; }
@@ -81,6 +102,8 @@ namespace Discord.Rest
/// <inheritdoc />
public string SplashUrl => CDN.GetGuildSplashUrl(Id, SplashId);
/// <inheritdoc />
public string DiscoverySplashUrl => CDN.GetGuildDiscoverySplashUrl(Id, DiscoverySplashId);
/// <inheritdoc />
public string BannerUrl => CDN.GetGuildBannerUrl(Id, BannerId);

/// <summary>
@@ -110,15 +133,24 @@ namespace Discord.Rest
internal void Update(Model model)
{
AFKChannelId = model.AFKChannelId;
EmbedChannelId = model.EmbedChannelId;
if (model.EmbedChannelId.IsSpecified)
EmbedChannelId = model.EmbedChannelId.Value;
if (model.WidgetChannelId.IsSpecified)
WidgetChannelId = model.WidgetChannelId.Value;
SystemChannelId = model.SystemChannelId;
RulesChannelId = model.RulesChannelId;
PublicUpdatesChannelId = model.PublicUpdatesChannelId;
AFKTimeout = model.AFKTimeout;
IsEmbeddable = model.EmbedEnabled;
if (model.EmbedEnabled.IsSpecified)
IsEmbeddable = model.EmbedEnabled.Value;
if (model.WidgetEnabled.IsSpecified)
IsWidgetEnabled = model.WidgetEnabled.Value;
IconId = model.Icon;
Name = model.Name;
OwnerId = model.OwnerId;
VoiceRegionId = model.Region;
SplashId = model.Splash;
DiscoverySplashId = model.DiscoverySplash;
VerificationLevel = model.VerificationLevel;
MfaLevel = model.MfaLevel;
DefaultMessageNotifications = model.DefaultMessageNotifications;
@@ -130,8 +162,18 @@ namespace Discord.Rest
SystemChannelFlags = model.SystemChannelFlags;
Description = model.Description;
PremiumSubscriptionCount = model.PremiumSubscriptionCount.GetValueOrDefault();
if (model.MaxPresences.IsSpecified)
MaxPresences = model.MaxPresences.Value ?? 25000;
if (model.MaxMembers.IsSpecified)
MaxMembers = model.MaxMembers.Value;
if (model.MaxVideoChannelUsers.IsSpecified)
MaxVideoChannelUsers = model.MaxVideoChannelUsers.Value;
PreferredLocale = model.PreferredLocale;
PreferredCulture = new CultureInfo(PreferredLocale);
if (model.ApproximateMemberCount.IsSpecified)
ApproximateMemberCount = model.ApproximateMemberCount.Value;
if (model.ApproximatePresenceCount.IsSpecified)
ApproximatePresenceCount = model.ApproximatePresenceCount.Value;

if (model.Emojis != null)
{
@@ -163,17 +205,36 @@ namespace Discord.Rest
EmbedChannelId = model.ChannelId;
IsEmbeddable = model.Enabled;
}
internal void Update(WidgetModel model)
{
WidgetChannelId = model.ChannelId;
IsWidgetEnabled = model.Enabled;
}

//General
/// <inheritdoc />
public async Task UpdateAsync(RequestOptions options = null)
=> Update(await Discord.ApiClient.GetGuildAsync(Id, options).ConfigureAwait(false));
=> Update(await Discord.ApiClient.GetGuildAsync(Id, false, options).ConfigureAwait(false));
/// <summary>
/// Updates this object's properties with its current state.
/// </summary>
/// <param name="withCounts">
/// If true, <see cref="ApproximateMemberCount"/> and <see cref="ApproximatePresenceCount"/>
/// will be updated as well.
/// </param>
/// <param name="options">The options to be used when sending the request.</param>
/// <remarks>
/// If <paramref name="withCounts"/> is true, <see cref="ApproximateMemberCount"/> and
/// <see cref="ApproximatePresenceCount"/> will be updated as well.
/// </remarks>
public async Task UpdateAsync(bool withCounts, RequestOptions options = null)
=> Update(await Discord.ApiClient.GetGuildAsync(Id, withCounts, options).ConfigureAwait(false));
/// <inheritdoc />
public Task DeleteAsync(RequestOptions options = null)
=> GuildHelper.DeleteAsync(this, Discord, options);

/// <inheritdoc />
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <see langword="null"/>.</exception>
public async Task ModifyAsync(Action<GuildProperties> func, RequestOptions options = null)
{
var model = await GuildHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false);
@@ -181,7 +242,8 @@ namespace Discord.Rest
}

/// <inheritdoc />
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <see langword="null"/>.</exception>
[Obsolete("This endpoint is deprecated, use ModifyWidgetAsync instead.")]
public async Task ModifyEmbedAsync(Action<GuildEmbedProperties> func, RequestOptions options = null)
{
var model = await GuildHelper.ModifyEmbedAsync(this, Discord, func, options).ConfigureAwait(false);
@@ -189,7 +251,15 @@ namespace Discord.Rest
}

/// <inheritdoc />
/// <exception cref="ArgumentNullException"><paramref name="args" /> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <see langword="null"/>.</exception>
public async Task ModifyWidgetAsync(Action<GuildWidgetProperties> func, RequestOptions options = null)
{
var model = await GuildHelper.ModifyWidgetAsync(this, Discord, func, options).ConfigureAwait(false);
Update(model);
}

/// <inheritdoc />
/// <exception cref="ArgumentNullException"><paramref name="args" /> is <see langword="null"/>.</exception>
public async Task ReorderChannelsAsync(IEnumerable<ReorderChannelProperties> args, RequestOptions options = null)
{
var arr = args.ToArray();
@@ -205,7 +275,7 @@ namespace Discord.Rest
role?.Update(model);
}
}
/// <inheritdoc />
public Task LeaveAsync(RequestOptions options = null)
=> GuildHelper.LeaveAsync(this, Discord, options);
@@ -230,7 +300,7 @@ namespace Discord.Rest
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains a ban object, which
/// contains the user information and the reason for the ban; <c>null</c> if the ban entry cannot be found.
/// contains the user information and the reason for the ban; <see langword="null"/> if the ban entry cannot be found.
/// </returns>
public Task<RestBan> GetBanAsync(IUser user, RequestOptions options = null)
=> GuildHelper.GetBanAsync(this, Discord, user.Id, options);
@@ -241,7 +311,7 @@ namespace Discord.Rest
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains a ban object, which
/// contains the user information and the reason for the ban; <c>null</c> if the ban entry cannot be found.
/// contains the user information and the reason for the ban; <see langword="null"/> if the ban entry cannot be found.
/// </returns>
public Task<RestBan> GetBanAsync(ulong userId, RequestOptions options = null)
=> GuildHelper.GetBanAsync(this, Discord, userId, options);
@@ -279,7 +349,7 @@ namespace Discord.Rest
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the generic channel
/// associated with the specified <paramref name="id"/>; <c>null</c> if none is found.
/// associated with the specified <paramref name="id"/>; <see langword="null"/> if none is found.
/// </returns>
public Task<RestGuildChannel> GetChannelAsync(ulong id, RequestOptions options = null)
=> GuildHelper.GetChannelAsync(this, Discord, id, options);
@@ -291,7 +361,7 @@ namespace Discord.Rest
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the text channel
/// associated with the specified <paramref name="id"/>; <c>null</c> if none is found.
/// associated with the specified <paramref name="id"/>; <see langword="null"/> if none is found.
/// </returns>
public async Task<RestTextChannel> GetTextChannelAsync(ulong id, RequestOptions options = null)
{
@@ -320,7 +390,7 @@ namespace Discord.Rest
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the voice channel associated
/// with the specified <paramref name="id"/>; <c>null</c> if none is found.
/// with the specified <paramref name="id"/>; <see langword="null"/> if none is found.
/// </returns>
public async Task<RestVoiceChannel> GetVoiceChannelAsync(ulong id, RequestOptions options = null)
{
@@ -362,7 +432,7 @@ namespace Discord.Rest
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the voice channel that the
/// AFK users will be moved to after they have idled for too long; <c>null</c> if none is set.
/// AFK users will be moved to after they have idled for too long; <see langword="null"/> if none is set.
/// </returns>
public async Task<RestVoiceChannel> GetAFKChannelAsync(RequestOptions options = null)
{
@@ -381,7 +451,7 @@ namespace Discord.Rest
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the first viewable text
/// channel in this guild; <c>null</c> if none is found.
/// channel in this guild; <see langword="null"/> if none is found.
/// </returns>
public async Task<RestTextChannel> GetDefaultChannelAsync(RequestOptions options = null)
{
@@ -399,8 +469,9 @@ namespace Discord.Rest
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the embed channel set
/// within the server's widget settings; <c>null</c> if none is set.
/// within the server's widget settings; <see langword="null"/> if none is set.
/// </returns>
[Obsolete("This endpoint is deprecated, use GetWidgetChannelAsync instead.")]
public async Task<RestGuildChannel> GetEmbedChannelAsync(RequestOptions options = null)
{
var embedId = EmbedChannelId;
@@ -410,12 +481,28 @@ namespace Discord.Rest
}

/// <summary>
/// Gets the first viewable text channel in this guild.
/// Gets the widget channel (i.e. the channel set in the guild's widget settings) in this guild.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the first viewable text
/// channel in this guild; <c>null</c> if none is found.
/// A task that represents the asynchronous get operation. The task result contains the widget channel set
/// within the server's widget settings; <see langword="null"/> if none is set.
/// </returns>
public async Task<RestGuildChannel> GetWidgetChannelAsync(RequestOptions options = null)
{
var widgetChannelId = WidgetChannelId;
if (widgetChannelId.HasValue)
return await GuildHelper.GetChannelAsync(this, Discord, widgetChannelId.Value, options).ConfigureAwait(false);
return null;
}

/// <summary>
/// Gets the text channel where guild notices such as welcome messages and boost events are posted.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the text channel
/// where guild notices such as welcome messages and boost events are poste; <see langword="null"/> if none is found.
/// </returns>
public async Task<RestTextChannel> GetSystemChannelAsync(RequestOptions options = null)
{
@@ -427,6 +514,45 @@ namespace Discord.Rest
}
return null;
}

/// <summary>
/// Gets the text channel where Community guilds can display rules and/or guidelines.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the text channel
/// where Community guilds can display rules and/or guidelines; <see langword="null"/> if none is set.
/// </returns>
public async Task<RestTextChannel> GetRulesChannelAsync(RequestOptions options = null)
{
var rulesChannelId = RulesChannelId;
if (rulesChannelId.HasValue)
{
var channel = await GuildHelper.GetChannelAsync(this, Discord, rulesChannelId.Value, options).ConfigureAwait(false);
return channel as RestTextChannel;
}
return null;
}

/// <summary>
/// Gets the text channel channel where admins and moderators of Community guilds receive notices from Discord.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the text channel channel where
/// admins and moderators of Community guilds receive notices from Discord; <see langword="null"/> if none is set.
/// </returns>
public async Task<RestTextChannel> GetPublicUpdatesChannelAsync(RequestOptions options = null)
{
var publicUpdatesChannelId = PublicUpdatesChannelId;
if (publicUpdatesChannelId.HasValue)
{
var channel = await GuildHelper.GetChannelAsync(this, Discord, publicUpdatesChannelId.Value, options).ConfigureAwait(false);
return channel as RestTextChannel;
}
return null;
}

/// <summary>
/// Creates a new text channel in this guild.
/// </summary>
@@ -458,7 +584,7 @@ namespace Discord.Rest
/// <param name="name">The name of the new channel.</param>
/// <param name="func">The delegate containing the properties to be applied to the channel upon its creation.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <exception cref="ArgumentNullException"><paramref name="name" /> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="name" /> is <see langword="null"/>.</exception>
/// <returns>
/// The created voice channel.
/// </returns>
@@ -470,7 +596,7 @@ namespace Discord.Rest
/// <param name="name">The name of the new channel.</param>
/// <param name="func">The delegate containing the properties to be applied to the channel upon its creation.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <exception cref="ArgumentNullException"><paramref name="name" /> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="name" /> is <see langword="null"/>.</exception>
/// <returns>
/// The created category channel.
/// </returns>
@@ -521,7 +647,7 @@ namespace Discord.Rest
/// </summary>
/// <param name="id">The snowflake identifier for the role.</param>
/// <returns>
/// A role that is associated with the specified <paramref name="id"/>; <c>null</c> if none is found.
/// A role that is associated with the specified <paramref name="id"/>; <see langword="null"/> if none is found.
/// </returns>
public RestRole GetRole(ulong id)
{
@@ -585,7 +711,7 @@ namespace Discord.Rest
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the guild user
/// associated with the specified <paramref name="id"/>; <c>null</c> if none is found.
/// associated with the specified <paramref name="id"/>; <see langword="null"/> if none is found.
/// </returns>
public Task<RestGuildUser> GetUserAsync(ulong id, RequestOptions options = null)
=> GuildHelper.GetUserAsync(this, Discord, id, options);
@@ -631,8 +757,8 @@ namespace Discord.Rest
/// A task that represents the asynchronous prune operation. The task result contains the number of users to
/// be or has been removed from this guild.
/// </returns>
public Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null)
=> GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options);
public Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null, IEnumerable<ulong> includeRoleIds = null)
=> GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options, includeRoleIds);

/// <summary>
/// Gets a collection of users in this guild that the name or nickname starts with the
@@ -675,7 +801,7 @@ namespace Discord.Rest
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the webhook with the
/// specified <paramref name="id"/>; <c>null</c> if none is found.
/// specified <paramref name="id"/>; <see langword="null"/> if none is found.
/// </returns>
public Task<RestWebhook> GetWebhookAsync(ulong id, RequestOptions options = null)
=> GuildHelper.GetWebhookAsync(this, Discord, id, options);
@@ -708,7 +834,7 @@ namespace Discord.Rest
public Task<GuildEmote> CreateEmoteAsync(string name, Image image, Optional<IEnumerable<IRole>> roles = default(Optional<IEnumerable<IRole>>), RequestOptions options = null)
=> GuildHelper.CreateEmoteAsync(this, Discord, name, image, roles, options);
/// <inheritdoc />
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <see langword="null"/>.</exception>
public Task<GuildEmote> ModifyEmoteAsync(GuildEmote emote, Action<EmoteProperties> func, RequestOptions options = null)
=> GuildHelper.ModifyEmoteAsync(this, Discord, emote.Id, func, options);
/// <inheritdoc />
@@ -808,6 +934,7 @@ namespace Discord.Rest
return null;
}
/// <inheritdoc />
[Obsolete("This endpoint is deprecated, use GetWidgetChannelAsync instead.")]
async Task<IGuildChannel> IGuild.GetEmbedChannelAsync(CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
@@ -816,6 +943,14 @@ namespace Discord.Rest
return null;
}
/// <inheritdoc />
async Task<IGuildChannel> IGuild.GetWidgetChannelAsync(CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
return await GetWidgetChannelAsync(options).ConfigureAwait(false);
else
return null;
}
/// <inheritdoc />
async Task<ITextChannel> IGuild.GetSystemChannelAsync(CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
@@ -824,6 +959,22 @@ namespace Discord.Rest
return null;
}
/// <inheritdoc />
async Task<ITextChannel> IGuild.GetRulesChannelAsync(CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
return await GetRulesChannelAsync(options).ConfigureAwait(false);
else
return null;
}
/// <inheritdoc />
async Task<ITextChannel> IGuild.GetPublicUpdatesChannelAsync(CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
return await GetPublicUpdatesChannelAsync(options).ConfigureAwait(false);
else
return null;
}
/// <inheritdoc />
async Task<ITextChannel> IGuild.CreateTextChannelAsync(string name, Action<TextChannelProperties> func, RequestOptions options)
=> await CreateTextChannelAsync(name, func, options).ConfigureAwait(false);
/// <inheritdoc />


+ 25
- 0
src/Discord.Net.Rest/Entities/Guilds/RestGuildWidget.cs View File

@@ -0,0 +1,25 @@
using System.Diagnostics;
using Model = Discord.API.GuildWidget;

namespace Discord.Rest
{
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public struct RestGuildWidget
{
public bool IsEnabled { get; private set; }
public ulong? ChannelId { get; private set; }

internal RestGuildWidget(bool isEnabled, ulong? channelId)
{
ChannelId = channelId;
IsEnabled = isEnabled;
}
internal static RestGuildWidget Create(Model model)
{
return new RestGuildWidget(model.Enabled, model.ChannelId);
}

public override string ToString() => ChannelId?.ToString() ?? "Unknown";
private string DebuggerDisplay => $"{ChannelId} ({(IsEnabled ? "Enabled" : "Disabled")})";
}
}

+ 12
- 4
src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs View File

@@ -65,12 +65,12 @@ namespace Discord.Rest

public static async Task AddReactionAsync(IMessage msg, IEmote emote, BaseDiscordClient client, RequestOptions options)
{
await client.ApiClient.AddReactionAsync(msg.Channel.Id, msg.Id, emote is Emote e ? $"{e.Name}:{e.Id}" : emote.Name, options).ConfigureAwait(false);
await client.ApiClient.AddReactionAsync(msg.Channel.Id, msg.Id, emote is Emote e ? $"{e.Name}:{e.Id}" : UrlEncode(emote.Name), options).ConfigureAwait(false);
}

public static async Task RemoveReactionAsync(IMessage msg, ulong userId, IEmote emote, BaseDiscordClient client, RequestOptions options)
{
await client.ApiClient.RemoveReactionAsync(msg.Channel.Id, msg.Id, userId, emote is Emote e ? $"{e.Name}:{e.Id}" : emote.Name, options).ConfigureAwait(false);
await client.ApiClient.RemoveReactionAsync(msg.Channel.Id, msg.Id, userId, emote is Emote e ? $"{e.Name}:{e.Id}" : UrlEncode(emote.Name), options).ConfigureAwait(false);
}

public static async Task RemoveAllReactionsAsync(IMessage msg, BaseDiscordClient client, RequestOptions options)
@@ -80,14 +80,14 @@ namespace Discord.Rest

public static async Task RemoveAllReactionsForEmoteAsync(IMessage msg, IEmote emote, BaseDiscordClient client, RequestOptions options)
{
await client.ApiClient.RemoveAllReactionsForEmoteAsync(msg.Channel.Id, msg.Id, emote is Emote e ? $"{e.Name}:{e.Id}" : emote.Name, options).ConfigureAwait(false);
await client.ApiClient.RemoveAllReactionsForEmoteAsync(msg.Channel.Id, msg.Id, emote is Emote e ? $"{e.Name}:{e.Id}" : UrlEncode(emote.Name), options).ConfigureAwait(false);
}

public static IAsyncEnumerable<IReadOnlyCollection<IUser>> GetReactionUsersAsync(IMessage msg, IEmote emote,
int? limit, BaseDiscordClient client, RequestOptions options)
{
Preconditions.NotNull(emote, nameof(emote));
var emoji = (emote is Emote e ? $"{e.Name}:{e.Id}" : emote.Name);
var emoji = (emote is Emote e ? $"{e.Name}:{e.Id}" : UrlEncode(emote.Name));

return new PagedAsyncEnumerable<IUser>(
DiscordConfig.MaxUserReactionsPerBatch,
@@ -114,7 +114,15 @@ namespace Discord.Rest
},
count: limit
);
}

private static string UrlEncode(string text)
{
#if NET461
return System.Net.WebUtility.UrlEncode(text);
#else
return System.Web.HttpUtility.UrlEncode(text);
#endif
}

public static async Task PinAsync(IMessage msg, BaseDiscordClient client,


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

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

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


+ 13
- 8
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<Embed> _embeds = ImmutableArray.Create<Embed>();
private ImmutableArray<ITag> _tags = ImmutableArray.Create<ITag>();
private ImmutableArray<ulong> _roleMentionIds = ImmutableArray.Create<ulong>();
private ImmutableArray<RestUser> _userMentions = ImmutableArray.Create<RestUser>();

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

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

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

ImmutableArray<IUser> mentions = ImmutableArray.Create<IUser>();
if (model.UserMentions.IsSpecified)
{
var value = model.UserMentions.Value;
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++)
{
var val = value[i];
if (val.Object != null)
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 guildId = (Channel as IGuildChannel)?.GuildId;
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;
}
}
@@ -149,10 +154,10 @@ namespace Discord.Rest
=> MentionUtils.Resolve(this, 0, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling);

/// <inheritdoc />
/// <exception cref="InvalidOperationException">This operation may only be called on a <see cref="RestNewsChannel"/> channel.</exception>
/// <exception cref="InvalidOperationException">This operation may only be called on a <see cref="INewsChannel"/> channel.</exception>
public async Task CrosspostAsync(RequestOptions options = null)
{
if (!(Channel is RestNewsChannel))
if (!(Channel is INewsChannel))
{
throw new InvalidOperationException("Publishing (crossposting) is only valid in news channels.");
}


+ 10
- 0
src/Discord.Net.Rest/Entities/RestApplication.cs View File

@@ -21,6 +21,12 @@ namespace Discord.Rest
public string[] RPCOrigins { get; private set; }
/// <inheritdoc />
public ulong Flags { get; private set; }
/// <inheritdoc />
public bool IsBotPublic { get; private set; }
/// <inheritdoc />
public bool BotRequiresCodeGrant { get; private set; }
/// <inheritdoc />
public ITeam Team { get; private set; }

/// <inheritdoc />
public IUser Owner { get; private set; }
@@ -46,11 +52,15 @@ namespace Discord.Rest
RPCOrigins = model.RPCOrigins;
Name = model.Name;
_iconId = model.Icon;
IsBotPublic = model.IsBotPublic;
BotRequiresCodeGrant = model.BotRequiresCodeGrant;

if (model.Flags.IsSpecified)
Flags = model.Flags.Value; //TODO: Do we still need this?
if (model.Owner.IsSpecified)
Owner = RestUser.Create(Discord, model.Owner.Value);
if (model.Team != null)
Team = RestTeam.Create(Discord, model.Team);
}

/// <exception cref="InvalidOperationException">Unable to update this object from a different application token.</exception>


+ 37
- 0
src/Discord.Net.Rest/Entities/Teams/RestTeam.cs View File

@@ -0,0 +1,37 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Model = Discord.API.Team;

namespace Discord.Rest
{
public class RestTeam : RestEntity<ulong>, ITeam
{
/// <inheritdoc />
public string IconUrl => _iconId != null ? CDN.GetTeamIconUrl(Id, _iconId) : null;
/// <inheritdoc />
public IReadOnlyList<ITeamMember> TeamMembers { get; private set; }
/// <inheritdoc />
public ulong OwnerUserId { get; private set; }

private string _iconId;

internal RestTeam(BaseDiscordClient discord, ulong id)
: base(discord, id)
{
}
internal static RestTeam Create(BaseDiscordClient discord, Model model)
{
var entity = new RestTeam(discord, model.Id);
entity.Update(model);
return entity;
}
internal virtual void Update(Model model)
{
if (model.Icon.IsSpecified)
_iconId = model.Icon.Value;
OwnerUserId = model.OwnerUserId;
TeamMembers = model.TeamMembers.Select(x => new RestTeamMember(Discord, x)).ToImmutableArray();
}
}
}

+ 30
- 0
src/Discord.Net.Rest/Entities/Teams/RestTeamMember.cs View File

@@ -0,0 +1,30 @@
using System;
using Model = Discord.API.TeamMember;

namespace Discord.Rest
{
public class RestTeamMember : ITeamMember
{
/// <inheritdoc />
public MembershipState MembershipState { get; }
/// <inheritdoc />
public string[] Permissions { get; }
/// <inheritdoc />
public ulong TeamId { get; }
/// <inheritdoc />
public IUser User { get; }

internal RestTeamMember(BaseDiscordClient discord, Model model)
{
MembershipState = model.MembershipState switch
{
API.MembershipState.Invited => MembershipState.Invited,
API.MembershipState.Accepted => MembershipState.Accepted,
_ => throw new InvalidOperationException("Invalid membership state"),
};
Permissions = model.Permissions;
TeamId = model.TeamId;
User = RestUser.Create(discord, model.User);
}
}
}

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

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

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
{
private static readonly ImmutableDictionary<ClientBucketType, ClientBucket> DefsByType;
private static readonly ImmutableDictionary<string, ClientBucket> DefsById;
private static readonly ImmutableDictionary<BucketId, ClientBucket> DefsById;

static ClientBucket()
{
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>();
@@ -25,21 +25,21 @@ namespace Discord.Net.Queue
builder.Add(bucket.Type, bucket);
DefsByType = builder.ToImmutable();

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

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 string Id { get; }
public BucketId Id { get; }
public int WindowCount { 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;
Id = id;


+ 53
- 0
src/Discord.Net.Rest/Net/Queue/GatewayBucket.cs View File

@@ -0,0 +1,53 @@
using System.Collections.Immutable;

namespace Discord.Net.Queue
{
public enum GatewayBucketType
{
Unbucketed = 0,
Identify = 1,
PresenceUpdate = 2,
}
internal struct GatewayBucket
{
private static readonly ImmutableDictionary<GatewayBucketType, GatewayBucket> DefsByType;
private static readonly ImmutableDictionary<BucketId, GatewayBucket> DefsById;

static GatewayBucket()
{
var buckets = new[]
{
// Limit is 120/60s, but 3 will be reserved for heartbeats (2 for possible heartbeats in the same timeframe and a possible failure)
new GatewayBucket(GatewayBucketType.Unbucketed, BucketId.Create(null, "<gateway-unbucketed>", null), 117, 60),
new GatewayBucket(GatewayBucketType.Identify, BucketId.Create(null, "<gateway-identify>", null), 1, 5),
new GatewayBucket(GatewayBucketType.PresenceUpdate, BucketId.Create(null, "<gateway-presenceupdate>", null), 5, 60),
};

var builder = ImmutableDictionary.CreateBuilder<GatewayBucketType, GatewayBucket>();
foreach (var bucket in buckets)
builder.Add(bucket.Type, bucket);
DefsByType = builder.ToImmutable();

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

public static GatewayBucket Get(GatewayBucketType type) => DefsByType[type];
public static GatewayBucket Get(BucketId id) => DefsById[id];

public GatewayBucketType Type { get; }
public BucketId Id { get; }
public int WindowCount { get; set; }
public int WindowSeconds { get; set; }

public GatewayBucket(GatewayBucketType type, BucketId id, int count, int seconds)
{
Type = type;
Id = id;
WindowCount = count;
WindowSeconds = seconds;
}
}
}

+ 65
- 12
src/Discord.Net.Rest/Net/Queue/RequestQueue.cs View File

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

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

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

_cleanupTask = RunCleanup();
}
@@ -82,16 +82,25 @@ namespace Discord.Net.Queue
else
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);
createdTokenSource?.Dispose();
return result;
}
public async Task SendAsync(WebSocketRequest request)
{
//TODO: Re-impl websocket buckets
request.CancelToken = _requestCancelToken;
await request.SendAsync().ConfigureAwait(false);
CancellationTokenSource createdTokenSource = null;
if (request.Options.CancelToken.CanBeCanceled)
{
createdTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_requestCancelToken, request.Options.CancelToken);
request.Options.CancelToken = createdTokenSource.Token;
}
else
request.Options.CancelToken = _requestCancelToken;

var bucket = GetOrCreateBucket(request.Options, request);
await bucket.SendAsync(request).ConfigureAwait(false);
createdTokenSource?.Dispose();
}

internal async Task EnterGlobalAsync(int id, RestRequest request)
@@ -109,14 +118,53 @@ namespace Discord.Net.Queue
{
_waitUntil = DateTimeOffset.UtcNow.AddMilliseconds(info.RetryAfter.Value + (info.Lag?.TotalMilliseconds ?? 0.0));
}
internal async Task EnterGlobalAsync(int id, WebSocketRequest request)
{
//If this is a global request (unbucketed), it'll be dealt in EnterAsync
var requestBucket = GatewayBucket.Get(request.Options.BucketId);
if (requestBucket.Type == GatewayBucketType.Unbucketed)
return;

//It's not a global request, so need to remove one from global (per-session)
var globalBucketType = GatewayBucket.Get(GatewayBucketType.Unbucketed);
var options = RequestOptions.CreateOrClone(request.Options);
options.BucketId = globalBucketType.Id;
var globalRequest = new WebSocketRequest(null, null, false, false, options);
var globalBucket = GetOrCreateBucket(options, globalRequest);
await globalBucket.TriggerAsync(id, globalRequest);
}

private RequestBucket GetOrCreateBucket(string id, RestRequest request)
private RequestBucket GetOrCreateBucket(RequestOptions options, IRequest request)
{
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(BucketId bucketId, RateLimitInfo? info, string endpoint)
{
return _buckets.GetOrAdd(id, x => new RequestBucket(this, request, x));
await RateLimitTriggered(bucketId, info, endpoint).ConfigureAwait(false);
}
internal async Task RaiseRateLimitTriggered(string bucketId, RateLimitInfo? info)
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);
}

public void ClearGatewayBuckets()
{
await RateLimitTriggered(bucketId, info).ConfigureAwait(false);
foreach (var gwBucket in (GatewayBucketType[])Enum.GetValues(typeof(GatewayBucketType)))
_buckets.TryRemove(GatewayBucket.Get(gwBucket).Id, out _);
}

private async Task RunCleanup()
@@ -126,10 +174,15 @@ namespace Discord.Net.Queue
while (!_cancelTokenSource.IsCancellationRequested)
{
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 (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 _);
}
}
await Task.Delay(60000, _cancelTokenSource.Token).ConfigureAwait(false); //Runs each minute
}


+ 164
- 22
src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs View File

@@ -19,12 +19,13 @@ namespace Discord.Net.Queue
private readonly RequestQueue _queue;
private int _semaphore;
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 DateTimeOffset LastAttemptAt { get; private set; }

public RequestBucket(RequestQueue queue, RestRequest request, string id)
public RequestBucket(RequestQueue queue, IRequest request, BucketId id)
{
_queue = queue;
Id = id;
@@ -33,6 +34,8 @@ namespace Discord.Net.Queue

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

#if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Sending...");
@@ -81,7 +86,7 @@ namespace Discord.Net.Queue
#endif
UpdateRateLimit(id, request, info, true);
}
await _queue.RaiseRateLimitTriggered(Id, info).ConfigureAwait(false);
await _queue.RaiseRateLimitTriggered(Id, info, $"{request.Method} {request.Endpoint}").ConfigureAwait(false);
continue; //Retry
case HttpStatusCode.BadGateway: //502
#if DEBUG_LIMITS
@@ -151,8 +156,68 @@ namespace Discord.Net.Queue
}
}
}
public async Task SendAsync(WebSocketRequest request)
{
int id = Interlocked.Increment(ref nextId);
#if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Start");
#endif
LastAttemptAt = DateTimeOffset.UtcNow;
while (true)
{
await _queue.EnterGlobalAsync(id, request).ConfigureAwait(false);
await EnterAsync(id, request).ConfigureAwait(false);

#if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Sending...");
#endif
try
{
await request.SendAsync().ConfigureAwait(false);
return;
}
catch (TimeoutException)
{
#if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Timeout");
#endif
if ((request.Options.RetryMode & RetryMode.RetryTimeouts) == 0)
throw;

private async Task EnterAsync(int id, RestRequest request)
await Task.Delay(500).ConfigureAwait(false);
continue; //Retry
}
/*catch (Exception)
{
#if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Error");
#endif
if ((request.Options.RetryMode & RetryMode.RetryErrors) == 0)
throw;

await Task.Delay(500);
continue; //Retry
}*/
finally
{
UpdateRateLimit(id, request, default(RateLimitInfo), false);
#if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Stop");
#endif
}
}
}

internal async Task TriggerAsync(int id, IRequest request)
{
#if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Trigger Bucket");
#endif
await EnterAsync(id, request).ConfigureAwait(false);
UpdateRateLimit(id, request, default(RateLimitInfo), false);
}

private async Task EnterAsync(int id, IRequest request)
{
int windowCount;
DateTimeOffset? resetAt;
@@ -160,6 +225,9 @@ namespace Discord.Net.Queue

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

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

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)
{
bool ignoreRatelimit = false;
isRateLimited = true;
await _queue.RaiseRateLimitTriggered(Id, null).ConfigureAwait(false);
switch (request)
{
case RestRequest restRequest:
await _queue.RaiseRateLimitTriggered(Id, null, $"{restRequest.Method} {restRequest.Endpoint}").ConfigureAwait(false);
break;
case WebSocketRequest webSocketRequest:
if (webSocketRequest.IgnoreLimit)
{
ignoreRatelimit = true;
break;
}
await _queue.RaiseRateLimitTriggered(Id, null, Id.Endpoint).ConfigureAwait(false);
break;
default:
throw new InvalidOperationException("Unknown request type");
}
if (ignoreRatelimit)
{
#if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Ignoring ratelimit");
#endif
break;
}
}

ThrowRetryLimit(request);
@@ -210,20 +302,52 @@ namespace Discord.Net.Queue
}
#if DEBUG_LIMITS
else
Debug.WriteLine($"[{id}] Entered Semaphore ({_semaphore}/{WindowCount} remaining)");
Debug.WriteLine($"[{id}] Entered Semaphore ({semaphore}/{WindowCount} remaining)");
#endif
break;
}
}

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

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;

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)
{
WindowCount = info.Limit.Value;
@@ -233,7 +357,6 @@ namespace Discord.Net.Queue
#endif
}

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

//Using X-RateLimit-Remaining causes a race condition
@@ -250,16 +373,18 @@ namespace Discord.Net.Queue
Debug.WriteLine($"[{id}] Retry-After: {info.RetryAfter.Value} ({info.RetryAfter.Value} ms)");
#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)
{
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)
resetTick = DateTimeOffset.Now.AddMilliseconds(250);
*/
@@ -269,17 +394,34 @@ namespace Discord.Net.Queue
Debug.WriteLine($"[{id}] X-RateLimit-Reset: {info.Reset.Value.ToUnixTimeSeconds()} ({diff} ms, {info.Lag?.TotalMilliseconds} ms lag)");
#endif
}
else if (request.Options.IsClientBucket && request.Options.BucketId != null)
else if (request.Options.IsClientBucket && Id != null)
{
resetTick = DateTimeOffset.UtcNow.AddSeconds(ClientBucket.Get(Id).WindowSeconds);
#if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Client Bucket ({ClientBucket.Get(Id).WindowSeconds * 1000} ms)");
#endif
}
else if (request.Options.IsGatewayBucket && request.Options.BucketId != null)
{
resetTick = DateTimeOffset.UtcNow.AddSeconds(ClientBucket.Get(request.Options.BucketId).WindowSeconds);
resetTick = DateTimeOffset.UtcNow.AddSeconds(GatewayBucket.Get(request.Options.BucketId).WindowSeconds);
#if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Gateway Bucket ({GatewayBucket.Get(request.Options.BucketId).WindowSeconds * 1000} ms)");
#endif
if (!hasQueuedReset)
{
_resetTick = resetTick;
LastAttemptAt = resetTick.Value;
#if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Client Bucket ({ClientBucket.Get(request.Options.BucketId).WindowSeconds * 1000} ms)");
Debug.WriteLine($"[{id}] Reset in {(int)Math.Ceiling((resetTick - DateTimeOffset.UtcNow).Value.TotalMilliseconds)} ms");
#endif
var _ = QueueReset(id, (int)Math.Ceiling((_resetTick.Value - DateTimeOffset.UtcNow).TotalMilliseconds), request);
}
return;
}

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
Debug.WriteLine($"[{id}] Disabled Semaphore");
#endif
@@ -296,12 +438,12 @@ namespace Discord.Net.Queue

if (!hasQueuedReset)
{
var _ = QueueReset(id, (int)Math.Ceiling((_resetTick.Value - DateTimeOffset.UtcNow).TotalMilliseconds));
var _ = QueueReset(id, (int)Math.Ceiling((_resetTick.Value - DateTimeOffset.UtcNow).TotalMilliseconds), request);
}
}
}
}
private async Task QueueReset(int id, int millis)
private async Task QueueReset(int id, int millis, IRequest request)
{
while (true)
{
@@ -323,7 +465,7 @@ namespace Discord.Net.Queue
}
}

private void ThrowRetryLimit(RestRequest request)
private void ThrowRetryLimit(IRequest request)
{
if ((request.Options.RetryMode & RetryMode.RetryRatelimit) == 0)
throw new RateLimitedException(request);


+ 3
- 3
src/Discord.Net.Rest/Net/Queue/Requests/WebSocketRequest.cs View File

@@ -9,22 +9,22 @@ namespace Discord.Net.Queue
public class WebSocketRequest : IRequest
{
public IWebSocketClient Client { get; }
public string BucketId { get; }
public byte[] Data { get; }
public bool IsText { get; }
public bool IgnoreLimit { get; }
public DateTimeOffset? TimeoutAt { get; }
public TaskCompletionSource<Stream> Promise { get; }
public RequestOptions Options { get; }
public CancellationToken CancelToken { get; internal set; }

public WebSocketRequest(IWebSocketClient client, string bucketId, byte[] data, bool isText, RequestOptions options)
public WebSocketRequest(IWebSocketClient client, byte[] data, bool isText, bool ignoreLimit, RequestOptions options)
{
Preconditions.NotNull(options, nameof(options));

Client = client;
BucketId = bucketId;
Data = data;
IsText = isText;
IgnoreLimit = ignoreLimit;
Options = options;
TimeoutAt = options.Timeout.HasValue ? DateTimeOffset.UtcNow.AddMilliseconds(options.Timeout.Value) : (DateTimeOffset?)null;
Promise = new TaskCompletionSource<Stream>();


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

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

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;
RetryAfter = headers.TryGetValue("Retry-After", out temp) &&
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) &&
DateTimeOffset.TryParse(temp, CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) ? DateTimeOffset.UtcNow - date : (TimeSpan?)null;
}


+ 31
- 0
src/Discord.Net.WebSocket/API/Gateway/InviteCreateEvent.cs View File

@@ -0,0 +1,31 @@
using Newtonsoft.Json;
using System;

namespace Discord.API.Gateway
{
internal class InviteCreateEvent
{
[JsonProperty("channel_id")]
public ulong ChannelId { get; set; }
[JsonProperty("code")]
public string Code { get; set; }
[JsonProperty("created_at")]
public DateTimeOffset CreatedAt { get; set; }
[JsonProperty("guild_id")]
public Optional<ulong> GuildId { get; set; }
[JsonProperty("inviter")]
public Optional<User> Inviter { get; set; }
[JsonProperty("max_age")]
public int MaxAge { get; set; }
[JsonProperty("max_uses")]
public int MaxUses { get; set; }
[JsonProperty("target_user")]
public Optional<User> TargetUser { get; set; }
[JsonProperty("target_user_type")]
public Optional<TargetUserType> TargetUserType { get; set; }
[JsonProperty("temporary")]
public bool Temporary { get; set; }
[JsonProperty("uses")]
public int Uses { get; set; }
}
}

+ 14
- 0
src/Discord.Net.WebSocket/API/Gateway/InviteDeleteEvent.cs View File

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

namespace Discord.API.Gateway
{
internal class InviteDeleteEvent
{
[JsonProperty("channel_id")]
public ulong ChannelId { get; set; }
[JsonProperty("code")]
public string Code { get; set; }
[JsonProperty("guild_id")]
public Optional<ulong> GuildId { get; set; }
}
}

+ 7
- 1
src/Discord.Net.WebSocket/Audio/AudioClient.cs View File

@@ -99,6 +99,12 @@ namespace Discord.Audio
_token = token;
await _connection.StartAsync().ConfigureAwait(false);
}

public IReadOnlyDictionary<ulong, AudioInStream> GetStreams()
{
return _streams.ToDictionary(pair => pair.Key, pair => pair.Value.Reader);
}

public async Task StopAsync()
{
await _connection.StopAsync().ConfigureAwait(false);
@@ -379,7 +385,7 @@ namespace Discord.Audio

private async Task RunHeartbeatAsync(int intervalMillis, CancellationToken cancelToken)
{
//TODO: Clean this up when Discord's session patch is live
// TODO: Clean this up when Discord's session patch is live
try
{
await _audioLogger.DebugAsync("Heartbeat Started").ConfigureAwait(false);


+ 3
- 0
src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs View File

@@ -61,14 +61,17 @@ namespace Discord.Audio.Streams

_task = Run();
}

protected override void Dispose(bool disposing)
{
if (disposing)
{
_disposeTokenSource?.Cancel();
_disposeTokenSource?.Dispose();
_cancelTokenSource?.Cancel();
_cancelTokenSource?.Dispose();
_queueLock?.Dispose();
_next.Dispose();
}
base.Dispose(disposing);
}


+ 4
- 2
src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs View File

@@ -68,10 +68,12 @@ namespace Discord.Audio.Streams

protected override void Dispose(bool disposing)
{
base.Dispose(disposing);

if (disposing)
{
_decoder.Dispose();
_next.Dispose();
}
base.Dispose(disposing);
}
}
}

+ 5
- 3
src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Threading;
using System.Threading.Tasks;

@@ -89,10 +89,12 @@ namespace Discord.Audio.Streams

protected override void Dispose(bool disposing)
{
base.Dispose(disposing);

if (disposing)
{
_encoder.Dispose();
_next.Dispose();
}
base.Dispose(disposing);
}
}
}

+ 7
- 0
src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs View File

@@ -76,5 +76,12 @@ namespace Discord.Audio.Streams
(buffer[extensionOffset + 3]);
return extensionOffset + 4 + (extensionLength * 4);
}

protected override void Dispose(bool disposing)
{
if (disposing)
_next.Dispose();
base.Dispose(disposing);
}
}
}

+ 8
- 1
src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Threading;
using System.Threading.Tasks;

@@ -69,5 +69,12 @@ namespace Discord.Audio.Streams
{
await _next.ClearAsync(cancelToken).ConfigureAwait(false);
}

protected override void Dispose(bool disposing)
{
if (disposing)
_next.Dispose();
base.Dispose(disposing);
}
}
}

+ 7
- 0
src/Discord.Net.WebSocket/Audio/Streams/SodiumDecryptStream.cs View File

@@ -44,5 +44,12 @@ namespace Discord.Audio.Streams
{
await _next.ClearAsync(cancelToken).ConfigureAwait(false);
}

protected override void Dispose(bool disposing)
{
if (disposing)
_next.Dispose();
base.Dispose(disposing);
}
}
}

+ 7
- 0
src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs View File

@@ -60,5 +60,12 @@ namespace Discord.Audio.Streams
{
await _next.ClearAsync(cancelToken).ConfigureAwait(false);
}

protected override void Dispose(bool disposing)
{
if (disposing)
_next.Dispose();
base.Dispose(disposing);
}
}
}

+ 42
- 0
src/Discord.Net.WebSocket/BaseSocketClient.Events.cs View File

@@ -389,5 +389,47 @@ namespace Discord.WebSocket
remove { _recipientRemovedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketGroupUser, Task>> _recipientRemovedEvent = new AsyncEvent<Func<SocketGroupUser, Task>>();

//Invites
/// <summary>
/// Fired when an invite is created.
/// </summary>
/// <remarks>
/// <para>
/// This event is fired when an invite is created. The event handler must return a
/// <see cref="Task"/> and accept a <see cref="SocketInvite"/> as its parameter.
/// </para>
/// <para>
/// The invite created will be passed into the <see cref="SocketInvite"/> parameter.
/// </para>
/// </remarks>
public event Func<SocketInvite, Task> InviteCreated
{
add { _inviteCreatedEvent.Add(value); }
remove { _inviteCreatedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketInvite, Task>> _inviteCreatedEvent = new AsyncEvent<Func<SocketInvite, Task>>();
/// <summary>
/// Fired when an invite is deleted.
/// </summary>
/// <remarks>
/// <para>
/// This event is fired when an invite is deleted. The event handler must return
/// a <see cref="Task"/> and accept a <see cref="SocketGuildChannel"/> and
/// <see cref="string"/> as its parameter.
/// </para>
/// <para>
/// The channel where this invite was created will be passed into the <see cref="SocketGuildChannel"/> parameter.
/// </para>
/// <para>
/// The code of the deleted invite will be passed into the <see cref="string"/> parameter.
/// </para>
/// </remarks>
public event Func<SocketGuildChannel, string, Task> InviteDeleted
{
add { _inviteDeletedEvent.Add(value); }
remove { _inviteDeletedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketGuildChannel, string, Task>> _inviteDeletedEvent = new AsyncEvent<Func<SocketGuildChannel, string, Task>>();
}
}

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

@@ -44,6 +44,8 @@ namespace Discord
var ex2 = ex as WebSocketClosedException;
if (ex2?.CloseCode == 4006)
CriticalError(new Exception("WebSocket session expired", ex));
else if (ex2?.CloseCode == 4014)
CriticalError(new Exception("WebSocket connection was closed", ex));
else
Error(new Exception("WebSocket connection was closed", ex));
}
@@ -141,7 +143,16 @@ namespace Discord
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);
State = ConnectionState.Connected;


+ 46
- 7
src/Discord.Net.WebSocket/DiscordShardedClient.cs View File

@@ -12,12 +12,14 @@ namespace Discord.WebSocket
public partial class DiscordShardedClient : BaseSocketClient, IDiscordClient
{
private readonly DiscordSocketConfig _baseConfig;
private readonly SemaphoreSlim _connectionGroupLock;
private readonly Dictionary<int, int> _shardIdsToIndex;
private readonly bool _automaticShards;
private int[] _shardIds;
private DiscordSocketClient[] _shards;
private int _totalShards;
private SemaphoreSlim[] _identifySemaphores;
private object _semaphoreResetLock;
private Task _semaphoreResetTask;

private bool _isDisposed;

@@ -62,10 +64,10 @@ namespace Discord.WebSocket
if (ids != null && config.TotalShards == null)
throw new ArgumentException($"Custom ids are not supported when {nameof(config.TotalShards)} is not specified.");

_semaphoreResetLock = new object();
_shardIdsToIndex = new Dictionary<int, int>();
config.DisplayInitialLog = false;
_baseConfig = config;
_connectionGroupLock = new SemaphoreSlim(1, 1);

if (config.TotalShards == null)
_automaticShards = true;
@@ -74,12 +76,15 @@ namespace Discord.WebSocket
_totalShards = config.TotalShards.Value;
_shardIds = ids ?? Enumerable.Range(0, _totalShards).ToArray();
_shards = new DiscordSocketClient[_shardIds.Length];
_identifySemaphores = new SemaphoreSlim[config.IdentifyMaxConcurrency];
for (int i = 0; i < config.IdentifyMaxConcurrency; i++)
_identifySemaphores[i] = new SemaphoreSlim(1, 1);
for (int i = 0; i < _shardIds.Length; i++)
{
_shardIdsToIndex.Add(_shardIds[i], i);
var newConfig = config.Clone();
newConfig.ShardId = _shardIds[i];
_shards[i] = new DiscordSocketClient(newConfig, _connectionGroupLock, i != 0 ? _shards[0] : null);
_shards[i] = new DiscordSocketClient(newConfig, this, i != 0 ? _shards[0] : null);
RegisterEvents(_shards[i], i == 0);
}
}
@@ -88,21 +93,53 @@ namespace Discord.WebSocket
=> new API.DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent,
rateLimitPrecision: config.RateLimitPrecision);

internal async Task AcquireIdentifyLockAsync(int shardId, CancellationToken token)
{
int semaphoreIdx = shardId % _baseConfig.IdentifyMaxConcurrency;
await _identifySemaphores[semaphoreIdx].WaitAsync(token).ConfigureAwait(false);
}

internal void ReleaseIdentifyLock()
{
lock (_semaphoreResetLock)
{
if (_semaphoreResetTask == null)
_semaphoreResetTask = ResetSemaphoresAsync();
}
}

private async Task ResetSemaphoresAsync()
{
await Task.Delay(5000).ConfigureAwait(false);
lock (_semaphoreResetLock)
{
foreach (var semaphore in _identifySemaphores)
if (semaphore.CurrentCount == 0)
semaphore.Release();
_semaphoreResetTask = null;
}
}

internal override async Task OnLoginAsync(TokenType tokenType, string token)
{
if (_automaticShards)
{
var shardCount = await GetRecommendedShardCountAsync().ConfigureAwait(false);
_shardIds = Enumerable.Range(0, shardCount).ToArray();
var botGateway = await GetBotGatewayAsync().ConfigureAwait(false);
_shardIds = Enumerable.Range(0, botGateway.Shards).ToArray();
_totalShards = _shardIds.Length;
_shards = new DiscordSocketClient[_shardIds.Length];
int maxConcurrency = botGateway.SessionStartLimit.MaxConcurrency;
_baseConfig.IdentifyMaxConcurrency = maxConcurrency;
_identifySemaphores = new SemaphoreSlim[maxConcurrency];
for (int i = 0; i < maxConcurrency; i++)
_identifySemaphores[i] = new SemaphoreSlim(1, 1);
for (int i = 0; i < _shardIds.Length; i++)
{
_shardIdsToIndex.Add(_shardIds[i], i);
var newConfig = _baseConfig.Clone();
newConfig.ShardId = _shardIds[i];
newConfig.TotalShards = _totalShards;
_shards[i] = new DiscordSocketClient(newConfig, _connectionGroupLock, i != 0 ? _shards[0] : null);
_shards[i] = new DiscordSocketClient(newConfig, this, i != 0 ? _shards[0] : null);
RegisterEvents(_shards[i], i == 0);
}
}
@@ -338,6 +375,9 @@ namespace Discord.WebSocket
client.UserIsTyping += (oldUser, newUser) => _userIsTypingEvent.InvokeAsync(oldUser, newUser);
client.RecipientAdded += (user) => _recipientAddedEvent.InvokeAsync(user);
client.RecipientRemoved += (user) => _recipientRemovedEvent.InvokeAsync(user);

client.InviteCreated += (invite) => _inviteCreatedEvent.InvokeAsync(invite);
client.InviteDeleted += (channel, invite) => _inviteDeletedEvent.InvokeAsync(channel, invite);
}

//IDiscordClient
@@ -395,7 +435,6 @@ namespace Discord.WebSocket
foreach (var client in _shards)
client?.Dispose();
}
_connectionGroupLock?.Dispose();
}

_isDisposed = true;


+ 10
- 1
src/Discord.Net.WebSocket/DiscordSocketApiClient.cs View File

@@ -132,6 +132,8 @@ namespace Discord.API
if (WebSocketClient == null)
throw new NotSupportedException("This client is not configured with WebSocket support.");

RequestQueue.ClearGatewayBuckets();

//Re-create streams to reset the zlib state
_compressed?.Dispose();
_decompressor?.Dispose();
@@ -205,7 +207,11 @@ namespace Discord.API
payload = new SocketFrame { Operation = (int)opCode, Payload = payload };
if (payload != null)
bytes = Encoding.UTF8.GetBytes(SerializeJson(payload));
await RequestQueue.SendAsync(new WebSocketRequest(WebSocketClient, null, bytes, true, options)).ConfigureAwait(false);

options.IsGatewayBucket = true;
if (options.BucketId == null)
options.BucketId = GatewayBucket.Get(GatewayBucketType.Unbucketed).Id;
await RequestQueue.SendAsync(new WebSocketRequest(WebSocketClient, bytes, true, opCode == GatewayOpCode.Heartbeat, options)).ConfigureAwait(false);
await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false);
}

@@ -225,6 +231,8 @@ namespace Discord.API
if (totalShards > 1)
msg.ShardingParams = new int[] { shardID, totalShards };

options.BucketId = GatewayBucket.Get(GatewayBucketType.Identify).Id;

if (gatewayIntents.HasValue)
msg.Intents = (int)gatewayIntents.Value;
else
@@ -258,6 +266,7 @@ namespace Discord.API
IsAFK = isAFK,
Game = game
};
options.BucketId = GatewayBucket.Get(GatewayBucketType.PresenceUpdate).Id;
await SendGatewayAsync(GatewayOpCode.StatusUpdate, args, options: options).ConfigureAwait(false);
}
public async Task SendRequestMembersAsync(IEnumerable<ulong> guildIds, RequestOptions options = null)


+ 89
- 15
src/Discord.Net.WebSocket/DiscordSocketClient.cs View File

@@ -26,7 +26,7 @@ namespace Discord.WebSocket
{
private readonly ConcurrentQueue<ulong> _largeGuilds;
private readonly JsonSerializer _serializer;
private readonly SemaphoreSlim _connectionGroupLock;
private readonly DiscordShardedClient _shardedClient;
private readonly DiscordSocketClient _parentClient;
private readonly ConcurrentQueue<long> _heartbeatTimes;
private readonly ConnectionManager _connection;
@@ -120,9 +120,9 @@ namespace Discord.WebSocket
/// <param name="config">The configuration to be used with the client.</param>
#pragma warning disable IDISP004
public DiscordSocketClient(DiscordSocketConfig config) : this(config, CreateApiClient(config), null, null) { }
internal DiscordSocketClient(DiscordSocketConfig config, SemaphoreSlim groupLock, DiscordSocketClient parentClient) : this(config, CreateApiClient(config), groupLock, parentClient) { }
internal DiscordSocketClient(DiscordSocketConfig config, DiscordShardedClient shardedClient, DiscordSocketClient parentClient) : this(config, CreateApiClient(config), shardedClient, parentClient) { }
#pragma warning restore IDISP004
private DiscordSocketClient(DiscordSocketConfig config, API.DiscordSocketApiClient client, SemaphoreSlim groupLock, DiscordSocketClient parentClient)
private DiscordSocketClient(DiscordSocketConfig config, API.DiscordSocketApiClient client, DiscordShardedClient shardedClient, DiscordSocketClient parentClient)
: base(config, client)
{
ShardId = config.ShardId ?? 0;
@@ -148,7 +148,7 @@ namespace Discord.WebSocket
_connection.Disconnected += (ex, recon) => TimedInvokeAsync(_disconnectedEvent, nameof(Disconnected), ex);

_nextAudioId = 1;
_connectionGroupLock = groupLock;
_shardedClient = shardedClient;
_parentClient = parentClient;

_serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() };
@@ -229,8 +229,12 @@ namespace Discord.WebSocket

private async Task OnConnectingAsync()
{
if (_connectionGroupLock != null)
await _connectionGroupLock.WaitAsync(_connection.CancelToken).ConfigureAwait(false);
bool locked = false;
if (_shardedClient != null && _sessionId == null)
{
await _shardedClient.AcquireIdentifyLockAsync(ShardId, _connection.CancelToken).ConfigureAwait(false);
locked = true;
}
try
{
await _gatewayLogger.DebugAsync("Connecting ApiClient").ConfigureAwait(false);
@@ -255,11 +259,8 @@ namespace Discord.WebSocket
}
finally
{
if (_connectionGroupLock != null)
{
await Task.Delay(5000).ConfigureAwait(false);
_connectionGroupLock.Release();
}
if (locked)
_shardedClient.ReleaseIdentifyLock();
}
}
private async Task OnDisconnectingAsync(Exception ex)
@@ -342,7 +343,7 @@ namespace Discord.WebSocket
{
var user = SocketGlobalUser.Create(this, state, model);
user.GlobalUser.AddRef();
user.Presence = new SocketPresence(UserStatus.Online, null, null);
user.Presence = new SocketPresence(UserStatus.Online, null, null, null);
return user;
});
}
@@ -370,7 +371,7 @@ namespace Discord.WebSocket
{
var cachedGuilds = guilds.ToImmutableArray();

const short batchSize = 100; //TODO: Gateway Intents will limit to a maximum of 1 guild_id
const short batchSize = 1;
ulong[] batchIds = new ulong[Math.Min(batchSize, cachedGuilds.Length)];
Task[] batchTasks = new Task[batchIds.Length];
int batchCount = (cachedGuilds.Length + (batchSize - 1)) / batchSize;
@@ -450,7 +451,7 @@ namespace Discord.WebSocket
return;
var status = Status;
var statusSince = _statusSince;
CurrentUser.Presence = new SocketPresence(status, Activity, null);
CurrentUser.Presence = new SocketPresence(status, Activity, null, null);

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

await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards, guildSubscriptions: _guildSubscriptions, gatewayIntents: _gatewayIntents).ConfigureAwait(false);
await _shardedClient.AcquireIdentifyLockAsync(ShardId, _connection.CancelToken).ConfigureAwait(false);
try
{
await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards, guildSubscriptions: _guildSubscriptions, gatewayIntents: _gatewayIntents).ConfigureAwait(false);
}
finally
{
_shardedClient.ReleaseIdentifyLock();
}
}
break;
case GatewayOpCode.Reconnect:
@@ -903,6 +912,13 @@ namespace Discord.WebSocket

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();
user.Update(State, data);
await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), before, user).ConfigureAwait(false);
@@ -1681,6 +1697,64 @@ namespace Discord.WebSocket
}
break;

//Invites
case "INVITE_CREATE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (INVITE_CREATE)").ConfigureAwait(false);

var data = (payload as JToken).ToObject<API.Gateway.InviteCreateEvent>(_serializer);
if (State.GetChannel(data.ChannelId) is SocketGuildChannel channel)
{
var guild = channel.Guild;
if (!guild.IsSynced)
{
await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false);
return;
}

SocketGuildUser inviter = data.Inviter.IsSpecified
? (guild.GetUser(data.Inviter.Value.Id) ?? guild.AddOrUpdateUser(data.Inviter.Value))
: null;

SocketUser target = data.TargetUser.IsSpecified
? (guild.GetUser(data.TargetUser.Value.Id) ?? (SocketUser)SocketUnknownUser.Create(this, State, data.TargetUser.Value))
: null;

var invite = SocketInvite.Create(this, guild, channel, inviter, target, data);

await TimedInvokeAsync(_inviteCreatedEvent, nameof(InviteCreated), invite).ConfigureAwait(false);
}
else
{
await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false);
return;
}
}
break;
case "INVITE_DELETE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (INVITE_DELETE)").ConfigureAwait(false);

var data = (payload as JToken).ToObject<API.Gateway.InviteDeleteEvent>(_serializer);
if (State.GetChannel(data.ChannelId) is SocketGuildChannel channel)
{
var guild = channel.Guild;
if (!guild.IsSynced)
{
await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false);
return;
}

await TimedInvokeAsync(_inviteDeletedEvent, nameof(InviteDeleted), channel, data.Code).ConfigureAwait(false);
}
else
{
await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false);
return;
}
}
break;

//Ignored (User only)
case "CHANNEL_PINS_ACK":
await _gatewayLogger.DebugAsync("Ignored Dispatch (CHANNEL_PINS_ACK)").ConfigureAwait(false);


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

@@ -87,7 +87,7 @@ namespace Discord.WebSocket
/// </para>
/// <para>
/// 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.
/// </para>
/// <note>
@@ -126,6 +126,14 @@ namespace Discord.WebSocket
public bool GuildSubscriptions { get; set; } = true;

/// <summary>
/// Gets or sets the maximum identify concurrency.
/// </summary>
/// <remarks>
/// This information is provided by Discord.
/// It is only used when using a <see cref="DiscordShardedClient"/> and auto-sharding is disabled.
/// </remarks>
public int IdentifyMaxConcurrency { get; set; } = 1;

/// 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.
@@ -151,6 +159,7 @@ namespace Discord.WebSocket
}
private int _maxWaitForGuildAvailable = 10000;
/// <summary>
/// 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>


+ 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)
{
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>
@@ -140,7 +139,6 @@ namespace Discord.WebSocket
public virtual async Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null)
{
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>
/// 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)
{
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>
/// 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)
{
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;


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

@@ -15,7 +15,7 @@ namespace Discord.WebSocket
/// </note>
/// </remarks>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketNewsChannel : SocketTextChannel
public class SocketNewsChannel : SocketTextChannel, INewsChannel
{
internal SocketNewsChannel(DiscordSocketClient discord, ulong id, SocketGuild guild)
:base(discord, id, guild)


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

@@ -48,6 +48,8 @@ namespace Discord.WebSocket
/// <inheritdoc />
public bool IsEmbeddable { get; private set; }
/// <inheritdoc />
public bool IsWidgetEnabled { get; private set; }
/// <inheritdoc />
public VerificationLevel VerificationLevel { get; private set; }
/// <inheritdoc />
public MfaLevel MfaLevel { get; private set; }
@@ -83,7 +85,10 @@ namespace Discord.WebSocket

internal ulong? AFKChannelId { get; private set; }
internal ulong? EmbedChannelId { get; private set; }
internal ulong? WidgetChannelId { get; private set; }
internal ulong? SystemChannelId { get; private set; }
internal ulong? RulesChannelId { get; private set; }
internal ulong? PublicUpdatesChannelId { get; private set; }
/// <inheritdoc />
public ulong OwnerId { get; private set; }
/// <summary> Gets the user that owns this guild. </summary>
@@ -95,6 +100,8 @@ namespace Discord.WebSocket
/// <inheritdoc />
public string SplashId { get; private set; }
/// <inheritdoc />
public string DiscoverySplashId { get; private set; }
/// <inheritdoc />
public PremiumTier PremiumTier { get; private set; }
/// <inheritdoc />
public string BannerId { get; private set; }
@@ -108,6 +115,12 @@ namespace Discord.WebSocket
public int PremiumSubscriptionCount { get; private set; }
/// <inheritdoc />
public string PreferredLocale { get; private set; }
/// <inheritdoc />
public int? MaxPresences { get; private set; }
/// <inheritdoc />
public int? MaxMembers { get; private set; }
/// <inheritdoc />
public int? MaxVideoChannelUsers { get; private set; }

/// <inheritdoc />
public CultureInfo PreferredCulture { get; private set; }
@@ -119,6 +132,8 @@ namespace Discord.WebSocket
/// <inheritdoc />
public string SplashUrl => CDN.GetGuildSplashUrl(Id, SplashId);
/// <inheritdoc />
public string DiscoverySplashUrl => CDN.GetGuildDiscoverySplashUrl(Id, DiscoverySplashId);
/// <inheritdoc />
public string BannerUrl => CDN.GetGuildBannerUrl(Id, BannerId);
/// <summary> Indicates whether the client has all the members downloaded to the local guild cache. </summary>
public bool HasAllMembers => MemberCount == DownloadedMemberCount;// _downloaderPromise.Task.IsCompleted;
@@ -152,7 +167,7 @@ namespace Discord.WebSocket
/// </summary>
/// <returns>
/// A <see cref="SocketVoiceChannel" /> that the AFK users will be moved to after they have idled for too
/// long; <c>null</c> if none is set.
/// long; <see langword="null"/> if none is set.
/// </returns>
public SocketVoiceChannel AFKChannel
{
@@ -166,8 +181,9 @@ namespace Discord.WebSocket
/// Gets the embed channel (i.e. the channel set in the guild's widget settings) in this guild.
/// </summary>
/// <returns>
/// A channel set within the server's widget settings; <c>null</c> if none is set.
/// A channel set within the server's widget settings; <see langword="null"/> if none is set.
/// </returns>
[Obsolete("This property is deprecated, use WidgetChannel instead.")]
public SocketGuildChannel EmbedChannel
{
get
@@ -177,10 +193,24 @@ namespace Discord.WebSocket
}
}
/// <summary>
/// Gets the widget channel (i.e. the channel set in the guild's widget settings) in this guild.
/// </summary>
/// <returns>
/// A channel set within the server's widget settings; <see langword="null"/> if none is set.
/// </returns>
public SocketGuildChannel WidgetChannel
{
get
{
var id = WidgetChannelId;
return id.HasValue ? GetChannel(id.Value) : null;
}
}
/// <summary>
/// Gets the system channel where randomized welcome messages are sent in this guild.
/// </summary>
/// <returns>
/// A text channel where randomized welcome messages will be sent to; <c>null</c> if none is set.
/// A text channel where randomized welcome messages will be sent to; <see langword="null"/> if none is set.
/// </returns>
public SocketTextChannel SystemChannel
{
@@ -191,6 +221,36 @@ namespace Discord.WebSocket
}
}
/// <summary>
/// Gets the channel with the guild rules.
/// </summary>
/// <returns>
/// A text channel with the guild rules; <see langword="null"/> if none is set.
/// </returns>
public SocketTextChannel RulesChannel
{
get
{
var id = RulesChannelId;
return id.HasValue ? GetTextChannel(id.Value) : null;
}
}
/// <summary>
/// Gets the channel where admins and moderators of Community guilds receive
/// notices from Discord.
/// </summary>
/// <returns>
/// A text channel where admins and moderators of Community guilds receive
/// notices from Discord; <see langword="null"/> if none is set.
/// </returns>
public SocketTextChannel PublicUpdatesChannel
{
get
{
var id = PublicUpdatesChannelId;
return id.HasValue ? GetTextChannel(id.Value) : null;
}
}
/// <summary>
/// Gets a collection of all text channels in this guild.
/// </summary>
/// <returns>
@@ -360,15 +420,24 @@ namespace Discord.WebSocket
internal void Update(ClientState state, Model model)
{
AFKChannelId = model.AFKChannelId;
EmbedChannelId = model.EmbedChannelId;
if (model.EmbedChannelId.IsSpecified)
EmbedChannelId = model.EmbedChannelId.Value;
if (model.WidgetChannelId.IsSpecified)
WidgetChannelId = model.WidgetChannelId.Value;
SystemChannelId = model.SystemChannelId;
RulesChannelId = model.RulesChannelId;
PublicUpdatesChannelId = model.PublicUpdatesChannelId;
AFKTimeout = model.AFKTimeout;
IsEmbeddable = model.EmbedEnabled;
if (model.EmbedEnabled.IsSpecified)
IsEmbeddable = model.EmbedEnabled.Value;
if (model.WidgetEnabled.IsSpecified)
IsWidgetEnabled = model.WidgetEnabled.Value;
IconId = model.Icon;
Name = model.Name;
OwnerId = model.OwnerId;
VoiceRegionId = model.Region;
SplashId = model.Splash;
DiscoverySplashId = model.DiscoverySplash;
VerificationLevel = model.VerificationLevel;
MfaLevel = model.MfaLevel;
DefaultMessageNotifications = model.DefaultMessageNotifications;
@@ -380,8 +449,14 @@ namespace Discord.WebSocket
SystemChannelFlags = model.SystemChannelFlags;
Description = model.Description;
PremiumSubscriptionCount = model.PremiumSubscriptionCount.GetValueOrDefault();
if (model.MaxPresences.IsSpecified)
MaxPresences = model.MaxPresences.Value ?? 25000;
if (model.MaxMembers.IsSpecified)
MaxMembers = model.MaxMembers.Value;
if (model.MaxVideoChannelUsers.IsSpecified)
MaxVideoChannelUsers = model.MaxVideoChannelUsers.Value;
PreferredLocale = model.PreferredLocale;
PreferredCulture = new CultureInfo(PreferredLocale);
PreferredCulture = PreferredLocale == null ? null : new CultureInfo(PreferredLocale);

if (model.Emojis != null)
{
@@ -447,15 +522,20 @@ namespace Discord.WebSocket
=> GuildHelper.DeleteAsync(this, Discord, options);

/// <inheritdoc />
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <see langword="null"/>.</exception>
public Task ModifyAsync(Action<GuildProperties> func, RequestOptions options = null)
=> GuildHelper.ModifyAsync(this, Discord, func, options);

/// <inheritdoc />
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <see langword="null"/>.</exception>
[Obsolete("This endpoint is deprecated, use ModifyWidgetAsync instead.")]
public Task ModifyEmbedAsync(Action<GuildEmbedProperties> func, RequestOptions options = null)
=> GuildHelper.ModifyEmbedAsync(this, Discord, func, options);
/// <inheritdoc />
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <see langword="null"/>.</exception>
public Task ModifyWidgetAsync(Action<GuildWidgetProperties> func, RequestOptions options = null)
=> GuildHelper.ModifyWidgetAsync(this, Discord, func, options);
/// <inheritdoc />
public Task ReorderChannelsAsync(IEnumerable<ReorderChannelProperties> args, RequestOptions options = null)
=> GuildHelper.ReorderChannelsAsync(this, Discord, args, options);
/// <inheritdoc />
@@ -485,7 +565,7 @@ namespace Discord.WebSocket
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains a ban object, which
/// contains the user information and the reason for the ban; <c>null</c> if the ban entry cannot be found.
/// contains the user information and the reason for the ban; <see langword="null"/> if the ban entry cannot be found.
/// </returns>
public Task<RestBan> GetBanAsync(IUser user, RequestOptions options = null)
=> GuildHelper.GetBanAsync(this, Discord, user.Id, options);
@@ -496,7 +576,7 @@ namespace Discord.WebSocket
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains a ban object, which
/// contains the user information and the reason for the ban; <c>null</c> if the ban entry cannot be found.
/// contains the user information and the reason for the ban; <see langword="null"/> if the ban entry cannot be found.
/// </returns>
public Task<RestBan> GetBanAsync(ulong userId, RequestOptions options = null)
=> GuildHelper.GetBanAsync(this, Discord, userId, options);
@@ -521,7 +601,7 @@ namespace Discord.WebSocket
/// </summary>
/// <param name="id">The snowflake identifier for the channel.</param>
/// <returns>
/// A generic channel associated with the specified <paramref name="id" />; <c>null</c> if none is found.
/// A generic channel associated with the specified <paramref name="id" />; <see langword="null"/> if none is found.
/// </returns>
public SocketGuildChannel GetChannel(ulong id)
{
@@ -535,7 +615,7 @@ namespace Discord.WebSocket
/// </summary>
/// <param name="id">The snowflake identifier for the text channel.</param>
/// <returns>
/// A text channel associated with the specified <paramref name="id" />; <c>null</c> if none is found.
/// A text channel associated with the specified <paramref name="id" />; <see langword="null"/> if none is found.
/// </returns>
public SocketTextChannel GetTextChannel(ulong id)
=> GetChannel(id) as SocketTextChannel;
@@ -544,7 +624,7 @@ namespace Discord.WebSocket
/// </summary>
/// <param name="id">The snowflake identifier for the voice channel.</param>
/// <returns>
/// A voice channel associated with the specified <paramref name="id" />; <c>null</c> if none is found.
/// A voice channel associated with the specified <paramref name="id" />; <see langword="null"/> if none is found.
/// </returns>
public SocketVoiceChannel GetVoiceChannel(ulong id)
=> GetChannel(id) as SocketVoiceChannel;
@@ -553,7 +633,7 @@ namespace Discord.WebSocket
/// </summary>
/// <param name="id">The snowflake identifier for the category channel.</param>
/// <returns>
/// A category channel associated with the specified <paramref name="id" />; <c>null</c> if none is found.
/// A category channel associated with the specified <paramref name="id" />; <see langword="null"/> if none is found.
/// </returns>
public SocketCategoryChannel GetCategoryChannel(ulong id)
=> GetChannel(id) as SocketCategoryChannel;
@@ -589,7 +669,7 @@ namespace Discord.WebSocket
/// <param name="name">The new name for the voice channel.</param>
/// <param name="func">The delegate containing the properties to be applied to the channel upon its creation.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <exception cref="ArgumentNullException"><paramref name="name"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="name"/> is <see langword="null"/>.</exception>
/// <returns>
/// A task that represents the asynchronous creation operation. The task result contains the newly created
/// voice channel.
@@ -602,7 +682,7 @@ namespace Discord.WebSocket
/// <param name="name">The new name for the category.</param>
/// <param name="func">The delegate containing the properties to be applied to the channel upon its creation.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <exception cref="ArgumentNullException"><paramref name="name"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="name"/> is <see langword="null"/>.</exception>
/// <returns>
/// A task that represents the asynchronous creation operation. The task result contains the newly created
/// category channel.
@@ -666,7 +746,7 @@ namespace Discord.WebSocket
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the partial metadata of
/// the vanity invite found within this guild; <c>null</c> if none is found.
/// the vanity invite found within this guild; <see langword="null"/> if none is found.
/// </returns>
public Task<RestInviteMetadata> GetVanityInviteAsync(RequestOptions options = null)
=> GuildHelper.GetVanityInviteAsync(this, Discord, options);
@@ -677,7 +757,7 @@ namespace Discord.WebSocket
/// </summary>
/// <param name="id">The snowflake identifier for the role.</param>
/// <returns>
/// A role that is associated with the specified <paramref name="id"/>; <c>null</c> if none is found.
/// A role that is associated with the specified <paramref name="id"/>; <see langword="null"/> if none is found.
/// </returns>
public SocketRole GetRole(ulong id)
{
@@ -699,7 +779,7 @@ namespace Discord.WebSocket
/// <param name="isHoisted">Whether the role is separated from others on the sidebar.</param>
/// <param name="isMentionable">Whether the role can be mentioned.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <exception cref="ArgumentNullException"><paramref name="name"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="name"/> is <see langword="null"/>.</exception>
/// <returns>
/// A task that represents the asynchronous creation operation. The task result contains the newly created
/// role.
@@ -731,13 +811,13 @@ namespace Discord.WebSocket
/// <remarks>
/// This method retrieves a user found within this guild.
/// <note>
/// This may return <c>null</c> in the WebSocket implementation due to incomplete user collection in
/// This may return <see langword="null"/> in the WebSocket implementation due to incomplete user collection in
/// large guilds.
/// </note>
/// </remarks>
/// <param name="id">The snowflake identifier of the user.</param>
/// <returns>
/// A guild user associated with the specified <paramref name="id"/>; <c>null</c> if none is found.
/// A guild user associated with the specified <paramref name="id"/>; <see langword="null"/> if none is found.
/// </returns>
public SocketGuildUser GetUser(ulong id)
{
@@ -746,8 +826,8 @@ namespace Discord.WebSocket
return null;
}
/// <inheritdoc />
public Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null)
=> GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options);
public Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null, IEnumerable<ulong> includeRoleIds = null)
=> GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options, includeRoleIds);

internal SocketGuildUser AddOrUpdateUser(UserModel model)
{
@@ -891,7 +971,7 @@ namespace Discord.WebSocket
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the webhook with the
/// specified <paramref name="id"/>; <c>null</c> if none is found.
/// specified <paramref name="id"/>; <see langword="null"/> if none is found.
/// </returns>
public Task<RestWebhook> GetWebhookAsync(ulong id, RequestOptions options = null)
=> GuildHelper.GetWebhookAsync(this, Discord, id, options);
@@ -914,7 +994,7 @@ namespace Discord.WebSocket
public Task<GuildEmote> CreateEmoteAsync(string name, Image image, Optional<IEnumerable<IRole>> roles = default(Optional<IEnumerable<IRole>>), RequestOptions options = null)
=> GuildHelper.CreateEmoteAsync(this, Discord, name, image, roles, options);
/// <inheritdoc />
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <see langword="null"/>.</exception>
public Task<GuildEmote> ModifyEmoteAsync(GuildEmote emote, Action<EmoteProperties> func, RequestOptions options = null)
=> GuildHelper.ModifyEmoteAsync(this, Discord, emote.Id, func, options);
/// <inheritdoc />
@@ -1133,11 +1213,21 @@ namespace Discord.WebSocket
/// <inheritdoc />
ulong? IGuild.EmbedChannelId => EmbedChannelId;
/// <inheritdoc />
ulong? IGuild.WidgetChannelId => WidgetChannelId;
/// <inheritdoc />
ulong? IGuild.SystemChannelId => SystemChannelId;
/// <inheritdoc />
ulong? IGuild.RulesChannelId => RulesChannelId;
/// <inheritdoc />
ulong? IGuild.PublicUpdatesChannelId => PublicUpdatesChannelId;
/// <inheritdoc />
IRole IGuild.EveryoneRole => EveryoneRole;
/// <inheritdoc />
IReadOnlyCollection<IRole> IGuild.Roles => Roles;
/// <inheritdoc />
int? IGuild.ApproximateMemberCount => null;
/// <inheritdoc />
int? IGuild.ApproximatePresenceCount => null;

/// <inheritdoc />
async Task<IReadOnlyCollection<IBan>> IGuild.GetBansAsync(RequestOptions options)
@@ -1177,12 +1267,22 @@ namespace Discord.WebSocket
Task<ITextChannel> IGuild.GetDefaultChannelAsync(CacheMode mode, RequestOptions options)
=> Task.FromResult<ITextChannel>(DefaultChannel);
/// <inheritdoc />
[Obsolete("This method is deprecated, use GetWidgetChannelAsync instead.")]
Task<IGuildChannel> IGuild.GetEmbedChannelAsync(CacheMode mode, RequestOptions options)
=> Task.FromResult<IGuildChannel>(EmbedChannel);
/// <inheritdoc />
Task<IGuildChannel> IGuild.GetWidgetChannelAsync(CacheMode mode, RequestOptions options)
=> Task.FromResult<IGuildChannel>(WidgetChannel);
/// <inheritdoc />
Task<ITextChannel> IGuild.GetSystemChannelAsync(CacheMode mode, RequestOptions options)
=> Task.FromResult<ITextChannel>(SystemChannel);
/// <inheritdoc />
Task<ITextChannel> IGuild.GetRulesChannelAsync(CacheMode mode, RequestOptions options)
=> Task.FromResult<ITextChannel>(RulesChannel);
/// <inheritdoc />
Task<ITextChannel> IGuild.GetPublicUpdatesChannelAsync(CacheMode mode, RequestOptions options)
=> Task.FromResult<ITextChannel>(PublicUpdatesChannel);
/// <inheritdoc />
async Task<ITextChannel> IGuild.CreateTextChannelAsync(string name, Action<TextChannelProperties> func, RequestOptions options)
=> await CreateTextChannelAsync(name, func, options).ConfigureAwait(false);
/// <inheritdoc />


+ 143
- 0
src/Discord.Net.WebSocket/Entities/Invites/SocketInvite.cs View File

@@ -0,0 +1,143 @@
using Discord.Rest;
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Model = Discord.API.Gateway.InviteCreateEvent;

namespace Discord.WebSocket
{
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketInvite : SocketEntity<string>, IInviteMetadata
{
private long _createdAtTicks;

/// <inheritdoc />
public ulong ChannelId { get; private set; }
/// <summary>
/// Gets the channel where this invite was created.
/// </summary>
public SocketGuildChannel Channel { get; private set; }
/// <inheritdoc />
public ulong? GuildId { get; private set; }
/// <summary>
/// Gets the guild where this invite was created.
/// </summary>
public SocketGuild Guild { get; private set; }
/// <inheritdoc />
ChannelType IInvite.ChannelType
{
get
{
switch (Channel)
{
case IVoiceChannel voiceChannel: return ChannelType.Voice;
case ICategoryChannel categoryChannel: return ChannelType.Category;
case IDMChannel dmChannel: return ChannelType.DM;
case IGroupChannel groupChannel: return ChannelType.Group;
case INewsChannel newsChannel: return ChannelType.News;
case ITextChannel textChannel: return ChannelType.Text;
default: throw new InvalidOperationException("Invalid channel type.");
}
}
}
/// <inheritdoc />
string IInvite.ChannelName => Channel.Name;
/// <inheritdoc />
string IInvite.GuildName => Guild.Name;
/// <inheritdoc />
int? IInvite.PresenceCount => throw new NotImplementedException();
/// <inheritdoc />
int? IInvite.MemberCount => throw new NotImplementedException();
/// <inheritdoc />
bool IInviteMetadata.IsRevoked => throw new NotImplementedException();
/// <inheritdoc />
public bool IsTemporary { get; private set; }
/// <inheritdoc />
int? IInviteMetadata.MaxAge { get => MaxAge; }
/// <inheritdoc />
int? IInviteMetadata.MaxUses { get => MaxUses; }
/// <inheritdoc />
int? IInviteMetadata.Uses { get => Uses; }
/// <summary>
/// Gets the time (in seconds) until the invite expires.
/// </summary>
public int MaxAge { get; private set; }
/// <summary>
/// Gets the max number of uses this invite may have.
/// </summary>
public int MaxUses { get; private set; }
/// <summary>
/// Gets the number of times this invite has been used.
/// </summary>
public int Uses { get; private set; }
/// <summary>
/// Gets the user that created this invite if available.
/// </summary>
public SocketGuildUser Inviter { get; private set; }
/// <inheritdoc />
DateTimeOffset? IInviteMetadata.CreatedAt => DateTimeUtils.FromTicks(_createdAtTicks);
/// <summary>
/// Gets when this invite was created.
/// </summary>
public DateTimeOffset CreatedAt => DateTimeUtils.FromTicks(_createdAtTicks);
/// <summary>
/// Gets the user targeted by this invite if available.
/// </summary>
public SocketUser TargetUser { get; private set; }
/// <summary>
/// Gets the type of the user targeted by this invite.
/// </summary>
public TargetUserType TargetUserType { get; private set; }

/// <inheritdoc />
public string Code => Id;
/// <inheritdoc />
public string Url => $"{DiscordConfig.InviteUrl}{Code}";

internal SocketInvite(DiscordSocketClient discord, SocketGuild guild, SocketGuildChannel channel, SocketGuildUser inviter, SocketUser target, string id)
: base(discord, id)
{
Guild = guild;
Channel = channel;
Inviter = inviter;
TargetUser = target;
}
internal static SocketInvite Create(DiscordSocketClient discord, SocketGuild guild, SocketGuildChannel channel, SocketGuildUser inviter, SocketUser target, Model model)
{
var entity = new SocketInvite(discord, guild, channel, inviter, target, model.Code);
entity.Update(model);
return entity;
}
internal void Update(Model model)
{
ChannelId = model.ChannelId;
GuildId = model.GuildId.IsSpecified ? model.GuildId.Value : Guild.Id;
IsTemporary = model.Temporary;
MaxAge = model.MaxAge;
MaxUses = model.MaxUses;
Uses = model.Uses;
_createdAtTicks = model.CreatedAt.UtcTicks;
TargetUserType = model.TargetUserType.IsSpecified ? model.TargetUserType.Value : TargetUserType.Undefined;
}

/// <inheritdoc />
public Task DeleteAsync(RequestOptions options = null)
=> InviteHelper.DeleteAsync(this, Discord, options);

/// <summary>
/// Gets the URL of the invite.
/// </summary>
/// <returns>
/// A string that resolves to the Url of the invite.
/// </returns>
public override string ToString() => Url;
private string DebuggerDisplay => $"{Url} ({Guild?.Name} / {Channel.Name})";

/// <inheritdoc />
IGuild IInvite.Guild => Guild;
/// <inheritdoc />
IChannel IInvite.Channel => Channel;
/// <inheritdoc />
IUser IInviteMetadata.Inviter => Inviter;
}
}

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

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

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


+ 20
- 11
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<Embed> _embeds = ImmutableArray.Create<Embed>();
private ImmutableArray<ITag> _tags = ImmutableArray.Create<ITag>();
private ImmutableArray<SocketRole> _roleMentions = ImmutableArray.Create<SocketRole>();
private ImmutableArray<SocketUser> _userMentions = ImmutableArray.Create<SocketUser>();

/// <inheritdoc />
public override bool IsTTS => _isTTS;
/// <inheritdoc />
@@ -30,6 +32,8 @@ namespace Discord.WebSocket
/// <inheritdoc />
public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks);
/// <inheritdoc />
public override bool MentionedEveryone => _isMentioningEveryone;
/// <inheritdoc />
public override IReadOnlyCollection<Attachment> Attachments => _attachments;
/// <inheritdoc />
public override IReadOnlyCollection<Embed> Embeds => _embeds;
@@ -38,9 +42,9 @@ namespace Discord.WebSocket
/// <inheritdoc />
public override IReadOnlyCollection<SocketGuildChannel> MentionedChannels => MessageHelper.FilterTagsByValue<SocketGuildChannel>(TagType.ChannelMention, _tags);
/// <inheritdoc />
public override IReadOnlyCollection<SocketRole> MentionedRoles => MessageHelper.FilterTagsByValue<SocketRole>(TagType.RoleMention, _tags);
public override IReadOnlyCollection<SocketRole> MentionedRoles => _roleMentions;
/// <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)
: base(discord, id, channel, author, source)
@@ -57,6 +61,8 @@ namespace Discord.WebSocket
{
base.Update(state, model);

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

if (model.IsTextToSpeech.IsSpecified)
_isTTS = model.IsTextToSpeech.Value;
if (model.Pinned.IsSpecified)
@@ -69,6 +75,8 @@ namespace Discord.WebSocket
{
_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)
{
@@ -98,28 +106,29 @@ namespace Discord.WebSocket
_embeds = ImmutableArray.Create<Embed>();
}

IReadOnlyCollection<IUser> mentions = ImmutableArray.Create<SocketUnknownUser>(); //Is passed to ParseTags to get real mention collection
if (model.UserMentions.IsSpecified)
{
var value = model.UserMentions.Value;
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++)
{
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));
}
mentions = newMentions.ToImmutable();
_userMentions = newMentions.ToImmutable();
}
}

if (model.Content.IsSpecified)
{
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;
}
}
@@ -149,10 +158,10 @@ namespace Discord.WebSocket
=> MentionUtils.Resolve(this, 0, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling);

/// <inheritdoc />
/// <exception cref="InvalidOperationException">This operation may only be called on a <see cref="SocketNewsChannel"/> channel.</exception>
/// <exception cref="InvalidOperationException">This operation may only be called on a <see cref="INewsChannel"/> channel.</exception>
public async Task CrosspostAsync(RequestOptions options = null)
{
if (!(Channel is SocketNewsChannel))
if (!(Channel is INewsChannel))
{
throw new InvalidOperationException("Publishing (crossposting) is only valid in news channels.");
}


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

@@ -154,6 +154,8 @@ namespace Discord.WebSocket
Nickname = model.Nick.Value;
if (model.Roles.IsSpecified)
UpdateRoles(model.Roles.Value);
if (model.PremiumSince.IsSpecified)
_premiumSinceTicks = model.PremiumSince.Value?.UtcTicks;
}
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; }
/// <inheritdoc />
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;
Activity= activity;
ActiveClients = activeClients;
Activity = activity;
ActiveClients = activeClients ?? ImmutableHashSet<ClientType>.Empty;
Activities = activities ?? ImmutableList<IActivity>.Empty;
}
internal static SocketPresence Create(Model model)
{
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>
/// Creates a new <see cref="IReadOnlyCollection{T}"/> containing all of the client types
@@ -53,6 +57,25 @@ namespace Discord.WebSocket
}
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>
/// 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 />
public override bool IsWebhook => false;
/// <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 />
/// <exception cref="NotSupportedException">This field is not supported for an unknown user.</exception>
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;
/// <inheritdoc />
public IImmutableSet<ClientType> ActiveClients => Presence.ActiveClients ?? ImmutableHashSet<ClientType>.Empty;
/// <inheritdoc />
public IImmutableList<IActivity> Activities => Presence.Activities ?? ImmutableList<IActivity>.Empty;
/// <summary>
/// Gets mutual guilds shared with this user.
/// </summary>


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save