Browse Source

Update branch

pull/1537/head
Paulo 4 years ago
parent
commit
df4ac9cf64
69 changed files with 1042 additions and 270 deletions
  1. +3
    -8
      azure-pipelines.yml
  2. +6
    -1
      azure/build.yml
  3. +1
    -1
      samples/01_basic_ping_bot/01_basic_ping_bot.csproj
  4. +2
    -2
      samples/02_commands_framework/02_commands_framework.csproj
  5. +2
    -2
      samples/03_sharded_client/03_sharded_client.csproj
  6. +2
    -1
      src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs
  7. +6
    -0
      src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs
  8. +10
    -2
      src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs
  9. +19
    -0
      src/Discord.Net.Core/Entities/Guilds/IGuild.cs
  10. +13
    -3
      src/Discord.Net.Core/Entities/Messages/AllowedMentionTypes.cs
  11. +2
    -2
      src/Discord.Net.Core/Entities/Messages/AllowedMentions.cs
  12. +9
    -0
      src/Discord.Net.Core/Entities/Messages/IMessage.cs
  13. +15
    -0
      src/Discord.Net.Core/Entities/Messages/IUserMessage.cs
  14. +1
    -1
      src/Discord.Net.Core/Entities/Users/IGuildUser.cs
  15. +4
    -0
      src/Discord.Net.Core/Entities/Users/IPresence.cs
  16. +43
    -0
      src/Discord.Net.Core/GatewayIntents.cs
  17. +118
    -0
      src/Discord.Net.Core/Net/BucketId.cs
  18. +2
    -1
      src/Discord.Net.Core/RequestOptions.cs
  19. +7
    -10
      src/Discord.Net.Core/Utils/Comparers.cs
  20. +5
    -0
      src/Discord.Net.Rest/API/Common/Presence.cs
  21. +4
    -0
      src/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs
  22. +9
    -0
      src/Discord.Net.Rest/API/Rest/SearchGuildMembersParams.cs
  23. +3
    -0
      src/Discord.Net.Rest/API/Rest/UploadFileParams.cs
  24. +2
    -2
      src/Discord.Net.Rest/BaseDiscordClient.cs
  25. +91
    -37
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  26. +9
    -5
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs
  27. +9
    -5
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs
  28. +44
    -8
      src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs
  29. +14
    -4
      src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs
  30. +8
    -8
      src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs
  31. +8
    -8
      src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs
  32. +10
    -9
      src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs
  33. +52
    -14
      src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs
  34. +25
    -0
      src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
  35. +20
    -0
      src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs
  36. +4
    -1
      src/Discord.Net.Rest/Entities/Messages/RestMessage.cs
  37. +12
    -0
      src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs
  38. +2
    -0
      src/Discord.Net.Rest/Entities/Users/RestUser.cs
  39. +7
    -7
      src/Discord.Net.Rest/Net/Queue/ClientBucket.cs
  40. +30
    -7
      src/Discord.Net.Rest/Net/Queue/RequestQueue.cs
  41. +61
    -18
      src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs
  42. +5
    -3
      src/Discord.Net.Rest/Net/RateLimitInfo.cs
  43. +3
    -1
      src/Discord.Net.WebSocket/API/Gateway/IdentifyParams.cs
  44. +16
    -0
      src/Discord.Net.WebSocket/API/Gateway/RemoveAllReactionsForEmoteEvent.cs
  45. +22
    -0
      src/Discord.Net.WebSocket/BaseSocketClient.Events.cs
  46. +10
    -1
      src/Discord.Net.WebSocket/ConnectionManager.cs
  47. +1
    -0
      src/Discord.Net.WebSocket/DiscordShardedClient.cs
  48. +8
    -3
      src/Discord.Net.WebSocket/DiscordSocketApiClient.cs
  49. +7
    -1
      src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs
  50. +42
    -19
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  51. +35
    -0
      src/Discord.Net.WebSocket/DiscordSocketConfig.cs
  52. +12
    -4
      src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs
  53. +24
    -19
      src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs
  54. +8
    -8
      src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs
  55. +8
    -8
      src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs
  56. +8
    -8
      src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs
  57. +54
    -4
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
  58. +13
    -1
      src/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs
  59. +8
    -1
      src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs
  60. +14
    -2
      src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs
  61. +2
    -0
      src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs
  62. +27
    -4
      src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs
  63. +1
    -1
      src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs
  64. +6
    -1
      src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs
  65. +1
    -1
      src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs
  66. +7
    -7
      src/Discord.Net.Webhook/DiscordWebhookClient.cs
  67. +2
    -2
      test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs
  68. +2
    -2
      test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs
  69. +2
    -2
      test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs

+ 3
- 8
azure-pipelines.yml View File

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

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

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


+ 6
- 1
azure/build.yml View File

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

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


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

@@ -2,7 +2,7 @@

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

<ItemGroup>


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

@@ -2,11 +2,11 @@

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

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.0" />
</ItemGroup>

<ItemGroup>


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

@@ -2,12 +2,12 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace>_03_sharded_client</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.0" />
</ItemGroup>

<ItemGroup>


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

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

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

foreach (var method in validCommands)
{


+ 6
- 0
src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs View File

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

namespace Discord
{
/// <summary>
@@ -30,5 +32,9 @@ namespace Discord
/// 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; }
}
}

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

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

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


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

@@ -683,6 +683,9 @@ namespace Discord
/// <summary>
/// Downloads all users for this guild if the current list is incomplete.
/// </summary>
/// <remarks>
/// This method downloads all users found within this guild throught the Gateway and caches them.
/// </remarks>
/// <returns>
/// A task that represents the asynchronous download operation.
/// </returns>
@@ -707,6 +710,22 @@ namespace Discord
/// be or has been removed from this guild.
/// </returns>
Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = 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"/>.
/// </summary>
/// <remarks>
/// The <paramref name="limit"/> can not be higher than <see cref="DiscordConfig.MaxUsersPerBatch"/>.
/// </remarks>
/// <param name="query">The partial name or nickname to search.</param>
/// <param name="limit">The maximum number of users to be gotten.</param>
/// <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 a collection of guild
/// users that the name or nickname starts with the provided <see cref="string"/> at <paramref name="query"/>.
/// </returns>
Task<IReadOnlyCollection<IGuildUser>> SearchUsersAsync(string query, int limit = DiscordConfig.MaxUsersPerBatch, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);

/// <summary>
/// Gets the specified number of audit log entries for this guild.


+ 13
- 3
src/Discord.Net.Core/Entities/Messages/AllowedMentionTypes.cs View File

@@ -8,17 +8,27 @@ namespace Discord
[Flags]
public enum AllowedMentionTypes
{
/// <summary>
/// No flag is set.
/// </summary>
/// <remarks>
/// This flag is not used to control mentions.
/// <note type="warning">
/// It will always be present and does not mean mentions will not be allowed.
/// </note>
/// </remarks>
None = 0,
/// <summary>
/// Controls role mentions.
/// </summary>
Roles,
Roles = 1,
/// <summary>
/// Controls user mentions.
/// </summary>
Users,
Users = 2,
/// <summary>
/// Controls <code>@everyone</code> and <code>@here</code> mentions.
/// </summary>
Everyone,
Everyone = 4,
}
}

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

@@ -39,7 +39,7 @@ namespace Discord
/// flag of the <see cref="AllowedTypes"/> property. If the flag is set, the value of this property
/// must be <c>null</c> or empty.
/// </summary>
public List<ulong> RoleIds { get; set; }
public List<ulong> RoleIds { get; set; } = new List<ulong>();

/// <summary>
/// Gets or sets the list of all user ids that will be mentioned.
@@ -47,7 +47,7 @@ namespace Discord
/// flag of the <see cref="AllowedTypes"/> property. If the flag is set, the value of this property
/// must be <c>null</c> or empty.
/// </summary>
public List<ulong> UserIds { get; set; }
public List<ulong> UserIds { get; set; } = new List<ulong>();

/// <summary>
/// Initializes a new instance of the <see cref="AllowedMentions"/> class.


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

@@ -215,6 +215,15 @@ namespace Discord
/// A task that represents the asynchronous removal operation.
/// </returns>
Task RemoveAllReactionsAsync(RequestOptions options = null);
/// <summary>
/// Removes all reactions with a specific emoji from this message.
/// </summary>
/// <param name="emote">The emoji used to react to this message.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous removal operation.
/// </returns>
Task RemoveAllReactionsForEmoteAsync(IEmote emote, RequestOptions options = null);

/// <summary>
/// Gets all users that reacted to a message with a given emote.


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

@@ -57,6 +57,21 @@ namespace Discord
/// </returns>
Task UnpinAsync(RequestOptions options = null);

/// <summary>
/// Publishes (crossposts) this message.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous operation for publishing this message.
/// </returns>
/// <remarks>
/// <note type="warning">
/// This call will throw an <see cref="InvalidOperationException"/> if attempted in a non-news channel.
/// </note>
/// This method will publish (crosspost) the message. Please note, publishing (crossposting), is only available in news channels.
/// </remarks>
Task CrosspostAsync(RequestOptions options = null);

/// <summary>
/// Transforms this message's text into a human-readable form by resolving its tags.
/// </summary>


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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

namespace Discord
@@ -57,7 +58,7 @@ 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; }


+ 7
- 10
src/Discord.Net.Core/Utils/Comparers.cs View File

@@ -41,16 +41,13 @@ namespace Discord
{
public override bool Equals(TEntity x, TEntity y)
{
bool xNull = x == null;
bool yNull = y == null;

if (xNull && yNull)
return true;

if (xNull ^ yNull)
return false;

return x.Id.Equals(y.Id);
return (x, y) switch
{
(null, null) => true,
(null, _) => false,
(_, null) => false,
var (l, r) => l.Id.Equals(r.Id)
};
}

public override int GetHashCode(TEntity obj)


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

+ 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")]


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

@@ -0,0 +1,9 @@
#pragma warning disable CS1591
namespace Discord.API.Rest
{
internal class SearchGuildMembersParams
{
public string Query { get; set; }
public Optional<int> Limit { get; set; }
}
}

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

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

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



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

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


+ 91
- 37
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)
{
@@ -660,6 +658,18 @@ namespace Discord.API

await SendAsync("DELETE", () => $"channels/{channelId}/messages/{messageId}/reactions", ids, options: options).ConfigureAwait(false);
}
public async Task RemoveAllReactionsForEmoteAsync(ulong channelId, ulong messageId, string emoji, RequestOptions options = null)
{
Preconditions.NotEqual(channelId, 0, nameof(channelId));
Preconditions.NotEqual(messageId, 0, nameof(messageId));
Preconditions.NotNullOrWhitespace(emoji, nameof(emoji));

options = RequestOptions.CreateOrClone(options);

var ids = new BucketIds(channelId: channelId);

await SendAsync("DELETE", () => $"channels/{channelId}/messages/{messageId}/reactions/{emoji}", ids, options: options).ConfigureAwait(false);
}
public async Task<IReadOnlyCollection<User>> GetReactionUsersAsync(ulong channelId, ulong messageId, string emoji, GetReactionUsersParams args, RequestOptions options = null)
{
Preconditions.NotEqual(channelId, 0, nameof(channelId));
@@ -695,6 +705,15 @@ namespace Discord.API
var ids = new BucketIds(channelId: channelId);
await SendAsync("POST", () => $"channels/{channelId}/typing", ids, options: options).ConfigureAwait(false);
}
public async Task CrosspostAsync(ulong channelId, ulong messageId, RequestOptions options = null)
{
Preconditions.NotEqual(channelId, 0, nameof(channelId));
Preconditions.NotEqual(messageId, 0, nameof(messageId));
options = RequestOptions.CreateOrClone(options);

var ids = new BucketIds(channelId: channelId);
await SendAsync("POST", () => $"channels/{channelId}/messages/{messageId}/crosspost", ids, options: options).ConfigureAwait(false);
}

//Channel Permissions
public async Task ModifyChannelPermissionsAsync(ulong channelId, ulong targetId, ModifyChannelPermissionsParams args, RequestOptions options = null)
@@ -1127,6 +1146,22 @@ namespace Discord.API
await SendJsonAsync("PATCH", () => $"guilds/{guildId}/members/{userId}", args, ids, options: options).ConfigureAwait(false);
}
}
public async Task<IReadOnlyCollection<GuildMember>> SearchGuildMembersAsync(ulong guildId, SearchGuildMembersParams args, RequestOptions options = null)
{
Preconditions.NotEqual(guildId, 0, nameof(guildId));
Preconditions.NotNull(args, nameof(args));
Preconditions.GreaterThan(args.Limit, 0, nameof(args.Limit));
Preconditions.AtMost(args.Limit, DiscordConfig.MaxUsersPerBatch, nameof(args.Limit));
Preconditions.NotNullOrEmpty(args.Query, nameof(args.Query));
options = RequestOptions.CreateOrClone(options);

int limit = args.Limit.GetValueOrDefault(DiscordConfig.MaxUsersPerBatch);
string query = args.Query;

var ids = new BucketIds(guildId: guildId);
Expression<Func<string>> endpoint = () => $"guilds/{guildId}/members/search?limit={limit}&query={query}";
return await SendAsync<IReadOnlyCollection<GuildMember>>("GET", endpoint, ids, options: options).ConfigureAwait(false);
}

//Guild Roles
public async Task<IReadOnlyCollection<Role>> GetGuildRolesAsync(ulong guildId, RequestOptions options = null)
@@ -1137,13 +1172,13 @@ namespace Discord.API
var ids = new BucketIds(guildId: guildId);
return await SendAsync<IReadOnlyCollection<Role>>("GET", () => $"guilds/{guildId}/roles", ids, options: options).ConfigureAwait(false);
}
public async Task<Role> CreateGuildRoleAsync(ulong guildId, RequestOptions options = null)
public async Task<Role> CreateGuildRoleAsync(ulong guildId, Rest.ModifyGuildRoleParams args, RequestOptions options = null)
{
Preconditions.NotEqual(guildId, 0, nameof(guildId));
options = RequestOptions.CreateOrClone(options);

var ids = new BucketIds(guildId: guildId);
return await SendAsync<Role>("POST", () => $"guilds/{guildId}/roles", ids, options: options).ConfigureAwait(false);
return await SendJsonAsync<Role>("POST", () => $"guilds/{guildId}/roles", args, ids, options: options).ConfigureAwait(false);
}
public async Task DeleteGuildRoleAsync(ulong guildId, ulong roleId, RequestOptions options = null)
{
@@ -1433,21 +1468,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;
}
@@ -1458,18 +1511,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;
@@ -1506,7 +1560,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)
@@ -1519,7 +1573,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)
{


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


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

@@ -46,6 +46,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);
}
@@ -109,12 +118,19 @@ namespace Discord.Rest
public static IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(IMessageChannel channel, BaseDiscordClient client,
ulong? fromMessageId, Direction dir, int limit, RequestOptions options)
{
if (dir == Direction.Around)
throw new NotImplementedException(); //TODO: Impl

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

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

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

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

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

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

var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS, Embed = embed?.ToModel() ?? Optional<API.Embed>.Unspecified, AllowedMentions = allowedMentions?.ToModel() ?? Optional<API.AllowedMentions>.Unspecified, IsSpoiler = isSpoiler };
var model = await client.ApiClient.UploadFileAsync(channel.Id, args, options).ConfigureAwait(false);
return RestUserMessage.Create(client, channel, client.CurrentUser, model);
}
@@ -387,7 +422,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,


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

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

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


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

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

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


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

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

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

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

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

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


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

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

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

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

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

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

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


+ 52
- 14
src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs View File

@@ -176,7 +176,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 +205,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 +230,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);
@@ -264,19 +292,18 @@ namespace Discord.Rest
{
if (name == null) throw new ArgumentNullException(paramName: nameof(name));

var model = await client.ApiClient.CreateGuildRoleAsync(guild.Id, options).ConfigureAwait(false);
var role = RestRole.Create(client, guild, model);

await role.ModifyAsync(x =>
var createGuildRoleParams = new API.Rest.ModifyGuildRoleParams
{
x.Name = name;
x.Permissions = (permissions ?? role.Permissions);
x.Color = (color ?? Color.Default);
x.Hoist = isHoisted;
x.Mentionable = isMentionable;
}, options).ConfigureAwait(false);
Color = color?.RawValue ?? Optional.Create<uint>(),
Hoist = isHoisted,
Mentionable = isMentionable,
Name = name,
Permissions = permissions?.RawValue ?? Optional.Create<ulong>()
};

return role;
var model = await client.ApiClient.CreateGuildRoleAsync(guild.Id, createGuildRoleParams, options).ConfigureAwait(false);

return RestRole.Create(client, guild, model);
}

//Users
@@ -387,6 +414,17 @@ namespace Discord.Rest
model = await client.ApiClient.BeginGuildPruneAsync(guild.Id, args, options).ConfigureAwait(false);
return model.Pruned;
}
public static async Task<IReadOnlyCollection<RestGuildUser>> SearchUsersAsync(IGuild guild, BaseDiscordClient client,
string query, int? limit, RequestOptions options)
{
var apiArgs = new SearchGuildMembersParams
{
Query = query,
Limit = limit ?? Optional.Create<int>()
};
var models = await client.ApiClient.SearchGuildMembersAsync(guild.Id, apiArgs, options).ConfigureAwait(false);
return models.Select(x => RestGuildUser.Create(client, guild, x)).ToImmutableArray();
}

// Audit logs
public static IAsyncEnumerable<IReadOnlyCollection<RestAuditLogEntry>> GetAuditLogsAsync(IGuild guild, BaseDiscordClient client,


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

@@ -634,6 +634,23 @@ namespace Discord.Rest
public Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null)
=> GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options);

/// <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"/>.
/// </summary>
/// <remarks>
/// The <paramref name="limit"/> can not be higher than <see cref="DiscordConfig.MaxUsersPerBatch"/>.
/// </remarks>
/// <param name="query">The partial name or nickname to search.</param>
/// <param name="limit">The maximum number of users to be gotten.</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 a collection of guild
/// users that the name or nickname starts with the provided <see cref="string"/> at <paramref name="query"/>.
/// </returns>
public Task<IReadOnlyCollection<RestGuildUser>> SearchUsersAsync(string query, int limit = DiscordConfig.MaxUsersPerBatch, RequestOptions options = null)
=> GuildHelper.SearchUsersAsync(this, Discord, query, limit, options);

//Audit logs
/// <summary>
/// Gets the specified number of audit log entries for this guild.
@@ -884,6 +901,14 @@ namespace Discord.Rest
/// <exception cref="NotSupportedException">Downloading users is not supported for a REST-based guild.</exception>
Task IGuild.DownloadUsersAsync() =>
throw new NotSupportedException();
/// <inheritdoc />
async Task<IReadOnlyCollection<IGuildUser>> IGuild.SearchUsersAsync(string query, int limit, CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
return await SearchUsersAsync(query, limit, options).ConfigureAwait(false);
else
return ImmutableArray.Create<IGuildUser>();
}

async Task<IReadOnlyCollection<IAuditLogEntry>> IGuild.GetAuditLogsAsync(int limit, CacheMode cacheMode, RequestOptions options,
ulong? beforeId, ulong? userId, ActionType? actionType)


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

@@ -44,8 +44,10 @@ namespace Discord.Rest
};
return await client.ApiClient.ModifyMessageAsync(msg.Channel.Id, msg.Id, apiArgs, options).ConfigureAwait(false);
}

public static Task DeleteAsync(IMessage msg, BaseDiscordClient client, RequestOptions options)
=> DeleteAsync(msg.Channel.Id, msg.Id, client, options);

public static async Task DeleteAsync(ulong channelId, ulong msgId, BaseDiscordClient client,
RequestOptions options)
{
@@ -76,6 +78,11 @@ namespace Discord.Rest
await client.ApiClient.RemoveAllReactionsAsync(msg.Channel.Id, msg.Id, options).ConfigureAwait(false);
}

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

public static IAsyncEnumerable<IReadOnlyCollection<IUser>> GetReactionUsersAsync(IMessage msg, IEmote emote,
int? limit, BaseDiscordClient client, RequestOptions options)
{
@@ -115,6 +122,7 @@ namespace Discord.Rest
{
await client.ApiClient.AddPinAsync(msg.Channel.Id, msg.Id, options).ConfigureAwait(false);
}

public static async Task UnpinAsync(IMessage msg, BaseDiscordClient client,
RequestOptions options)
{
@@ -240,6 +248,7 @@ namespace Discord.Rest

return tags.ToImmutable();
}

private static int? FindIndex(IReadOnlyList<ITag> tags, int index)
{
int i = 0;
@@ -253,6 +262,7 @@ namespace Discord.Rest
return null; //Overlaps tag before this
return i;
}

public static ImmutableArray<ulong> FilterTagsByKey(TagType type, ImmutableArray<ITag> tags)
{
return tags
@@ -260,6 +270,7 @@ namespace Discord.Rest
.Select(x => x.Key)
.ToImmutableArray();
}

public static ImmutableArray<T> FilterTagsByValue<T>(TagType type, ImmutableArray<ITag> tags)
{
return tags
@@ -279,5 +290,14 @@ namespace Discord.Rest
return MessageSource.Bot;
return MessageSource.User;
}

public static Task CrosspostAsync(IMessage msg, BaseDiscordClient client, RequestOptions options)
=> CrosspostAsync(msg.Channel.Id, msg.Id, client, options);

public static async Task CrosspostAsync(ulong channelId, ulong msgId, BaseDiscordClient client,
RequestOptions options)
{
await client.ApiClient.CrosspostAsync(channelId, msgId, options).ConfigureAwait(false);
}
}
}

+ 4
- 1
src/Discord.Net.Rest/Entities/Messages/RestMessage.cs View File

@@ -165,7 +165,7 @@ namespace Discord.Rest
IReadOnlyCollection<IEmbed> IMessage.Embeds => Embeds;
/// <inheritdoc />
IReadOnlyCollection<ulong> IMessage.MentionedUserIds => MentionedUsers.Select(x => x.Id).ToImmutableArray();
/// <inheritdoc />
public IReadOnlyDictionary<IEmote, ReactionMetadata> Reactions => _reactions.ToDictionary(x => x.Emote, x => new ReactionMetadata { ReactionCount = x.Count, IsMe = x.Me });

@@ -182,6 +182,9 @@ namespace Discord.Rest
public Task RemoveAllReactionsAsync(RequestOptions options = null)
=> MessageHelper.RemoveAllReactionsAsync(this, Discord, options);
/// <inheritdoc />
public Task RemoveAllReactionsForEmoteAsync(IEmote emote, RequestOptions options = null)
=> MessageHelper.RemoveAllReactionsForEmoteAsync(this, emote, Discord, options);
/// <inheritdoc />
public IAsyncEnumerable<IReadOnlyCollection<IUser>> GetReactionUsersAsync(IEmote emote, int limit, RequestOptions options = null)
=> MessageHelper.GetReactionUsersAsync(this, emote, limit, Discord, options);
}


+ 12
- 0
src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs View File

@@ -148,6 +148,18 @@ namespace Discord.Rest
TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name)
=> 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>
public async Task CrosspostAsync(RequestOptions options = null)
{
if (!(Channel is RestNewsChannel))
{
throw new InvalidOperationException("Publishing (crossposting) is only valid in news channels.");
}

await MessageHelper.CrosspostAsync(this, Discord, options);
}

private string DebuggerDisplay => $"{Author}: {Content} ({Id}{(Attachments.Count > 0 ? $", {Attachments.Count} Attachments" : "")})";
}
}

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


+ 30
- 7
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?, 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;
@@ -38,7 +38,7 @@ namespace Discord.Net.Queue
_requestCancelToken = CancellationToken.None;
_parentToken = CancellationToken.None;

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

_cleanupTask = RunCleanup();
}
@@ -94,7 +94,7 @@ 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;
@@ -181,12 +181,30 @@ namespace Discord.Net.Queue

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

private async Task RunCleanup()
{
@@ -195,10 +213,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
}


+ 61
- 18
src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs View File

@@ -13,12 +13,15 @@ namespace Discord.Net.Queue
{
internal class RequestBucket
{
private const int MinimumSleepTimeMs = 750;

private readonly object _lock;
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; }

@@ -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...");
@@ -220,6 +225,9 @@ namespace Discord.Net.Queue

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

if (DateTimeOffset.UtcNow > request.TimeoutAt || request.Options.CancelToken.IsCancellationRequested)
{
if (!isRateLimited)
@@ -235,7 +243,8 @@ 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)
{
@@ -245,10 +254,11 @@ namespace Discord.Net.Queue

ThrowRetryLimit(request);

if (resetAt.HasValue)
if (resetAt.HasValue && resetAt > DateTimeOffset.UtcNow)
{
if (resetAt > timeoutAt)
ThrowRetryLimit(request);

int millis = (int)Math.Ceiling((resetAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds);
#if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Sleeping {millis} ms (Pre-emptive)");
@@ -258,18 +268,18 @@ namespace Discord.Net.Queue
}
else
{
if ((timeoutAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds < 500.0)
if ((timeoutAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds < MinimumSleepTimeMs)
ThrowRetryLimit(request);
#if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Sleeping 500* ms (Pre-emptive)");
Debug.WriteLine($"[{id}] Sleeping {MinimumSleepTimeMs}* ms (Pre-emptive)");
#endif
await Task.Delay(500, request.Options.CancelToken).ConfigureAwait(false);
await Task.Delay(MinimumSleepTimeMs, request.Options.CancelToken).ConfigureAwait(false);
}
continue;
}
#if DEBUG_LIMITS
else
Debug.WriteLine($"[{id}] Entered Semaphore ({_semaphore}/{WindowCount} remaining)");
Debug.WriteLine($"[{id}] Entered Semaphore ({semaphore}/{WindowCount} remaining)");
#endif
break;
}
@@ -282,7 +292,39 @@ namespace Discord.Net.Queue

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;
@@ -292,7 +334,6 @@ namespace Discord.Net.Queue
#endif
}

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

//Using X-RateLimit-Remaining causes a race condition
@@ -309,16 +350,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.Now.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);
*/
@@ -328,11 +371,11 @@ 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(request.Options.BucketId).WindowSeconds);
resetTick = DateTimeOffset.UtcNow.AddSeconds(ClientBucket.Get(Id).WindowSeconds);
#if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Client Bucket ({ClientBucket.Get(request.Options.BucketId).WindowSeconds * 1000} ms)");
Debug.WriteLine($"[{id}] Client Bucket ({ClientBucket.Get(Id).WindowSeconds * 1000} ms)");
#endif
}
else if (request.Options.IsGatewayBucket && request.Options.BucketId != null)
@@ -355,7 +398,7 @@ namespace Discord.Net.Queue

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


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


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

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

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

+ 16
- 0
src/Discord.Net.WebSocket/API/Gateway/RemoveAllReactionsForEmoteEvent.cs View File

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

namespace Discord.API.Gateway
{
internal class RemoveAllReactionsForEmoteEvent
{
[JsonProperty("channel_id")]
public ulong ChannelId { get; set; }
[JsonProperty("guild_id")]
public Optional<ulong> GuildId { get; set; }
[JsonProperty("message_id")]
public ulong MessageId { get; set; }
[JsonProperty("emoji")]
public Emoji Emoji { get; set; }
}
}

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

@@ -234,6 +234,28 @@ namespace Discord.WebSocket
remove { _reactionsClearedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<Cacheable<IUserMessage, ulong>, ISocketMessageChannel, Task>> _reactionsClearedEvent = new AsyncEvent<Func<Cacheable<IUserMessage, ulong>, ISocketMessageChannel, Task>>();
/// <summary>
/// Fired when all reactions to a message with a specific emote are removed.
/// </summary>
/// <remarks>
/// <para>
/// This event is fired when all reactions to a message with a specific emote are removed.
/// The event handler must return a <see cref="Task"/> and accept a <see cref="ISocketMessageChannel"/> and
/// a <see cref="IEmote"/> as its parameters.
/// </para>
/// <para>
/// The channel where this message was sent will be passed into the <see cref="ISocketMessageChannel"/> parameter.
/// </para>
/// <para>
/// The emoji that all reactions had and were removed will be passed into the <see cref="IEmote"/> parameter.
/// </para>
/// </remarks>
public event Func<Cacheable<IUserMessage, ulong>, ISocketMessageChannel, IEmote, Task> ReactionsRemovedForEmote
{
add { _reactionsRemovedForEmoteEvent.Add(value); }
remove { _reactionsRemovedForEmoteEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<Cacheable<IUserMessage, ulong>, ISocketMessageChannel, IEmote, Task>> _reactionsRemovedForEmoteEvent = new AsyncEvent<Func<Cacheable<IUserMessage, ulong>, ISocketMessageChannel, IEmote, Task>>();

//Roles
/// <summary> Fired when a role is created. </summary>


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

@@ -141,7 +141,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;


+ 1
- 0
src/Discord.Net.WebSocket/DiscordShardedClient.cs View File

@@ -333,6 +333,7 @@ namespace Discord.WebSocket
client.ReactionAdded += (cache, channel, reaction) => _reactionAddedEvent.InvokeAsync(cache, channel, reaction);
client.ReactionRemoved += (cache, channel, reaction) => _reactionRemovedEvent.InvokeAsync(cache, channel, reaction);
client.ReactionsCleared += (cache, channel) => _reactionsClearedEvent.InvokeAsync(cache, channel);
client.ReactionsRemovedForEmote += (cache, channel, emote) => _reactionsRemovedForEmoteEvent.InvokeAsync(cache, channel, emote);

client.RoleCreated += (role) => _roleCreatedEvent.InvokeAsync(role);
client.RoleDeleted += (role) => _roleDeletedEvent.InvokeAsync(role);


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

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

public async Task SendIdentifyAsync(int largeThreshold = 100, int shardID = 0, int totalShards = 1, bool guildSubscriptions = true, RequestOptions options = null)
public async Task SendIdentifyAsync(int largeThreshold = 100, int shardID = 0, int totalShards = 1, bool guildSubscriptions = true, GatewayIntents? gatewayIntents = null, RequestOptions options = null)
{
options = RequestOptions.CreateOrClone(options);
var props = new Dictionary<string, string>
@@ -225,13 +225,18 @@ namespace Discord.API
{
Token = AuthToken,
Properties = props,
LargeThreshold = largeThreshold,
GuildSubscriptions = guildSubscriptions
LargeThreshold = largeThreshold
};
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
msg.GuildSubscriptions = guildSubscriptions;

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


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

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


+ 42
- 19
src/Discord.Net.WebSocket/DiscordSocketClient.cs View File

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

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

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

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

//Wait for READY
@@ -338,7 +340,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;
});
}
@@ -366,7 +368,7 @@ namespace Discord.WebSocket
{
var cachedGuilds = guilds.ToImmutableArray();

const short batchSize = 50;
const short batchSize = 100; //TODO: Gateway Intents will limit to a maximum of 1 guild_id
ulong[] batchIds = new ulong[Math.Min(batchSize, cachedGuilds.Length)];
Task[] batchTasks = new Task[batchIds.Length];
int batchCount = (cachedGuilds.Length + (batchSize - 1)) / batchSize;
@@ -374,7 +376,7 @@ namespace Discord.WebSocket
for (int i = 0, k = 0; i < batchCount; i++)
{
bool isLast = i == batchCount - 1;
int count = isLast ? (batchIds.Length - (batchCount - 1) * batchSize) : batchSize;
int count = isLast ? (cachedGuilds.Length - (batchCount - 1) * batchSize) : batchSize;

for (int j = 0; j < count; j++, k++)
{
@@ -446,7 +448,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
@@ -515,7 +517,7 @@ namespace Discord.WebSocket
_sessionId = null;
_lastSeq = 0;

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

await TimedInvokeAsync(_readyEvent, nameof(Ready)).ConfigureAwait(false);
await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false);
@@ -1389,6 +1394,34 @@ namespace Discord.WebSocket
}
}
break;
case "MESSAGE_REACTION_REMOVE_EMOJI":
{
await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE_EMOJI)").ConfigureAwait(false);

var data = (payload as JToken).ToObject<API.Gateway.RemoveAllReactionsForEmoteEvent>(_serializer);
if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel)
{
var cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage;
bool isCached = cachedMsg != null;

var optionalMsg = !isCached
? Optional.Create<SocketUserMessage>()
: Optional.Create(cachedMsg);

var cacheable = new Cacheable<IUserMessage, ulong>(cachedMsg, data.MessageId, isCached, async () => await channel.GetMessageAsync(data.MessageId).ConfigureAwait(false) as IUserMessage);
var emote = data.Emoji.ToIEmote();

cachedMsg?.RemoveAllReactionsForEmoteAsync(emote);

await TimedInvokeAsync(_reactionsRemovedForEmoteEvent, nameof(ReactionsRemovedForEmote), cacheable, channel, emote).ConfigureAwait(false);
}
else
{
await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false);
return;
}
}
break;
case "MESSAGE_DELETE_BULK":
{
await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE_BULK)").ConfigureAwait(false);
@@ -1740,7 +1773,7 @@ namespace Discord.WebSocket
try
{
await logger.DebugAsync("GuildDownloader Started").ConfigureAwait(false);
while ((_unavailableGuildCount != 0) && (Environment.TickCount - _lastGuildAvailableTime < 2000))
while ((_unavailableGuildCount != 0) && (Environment.TickCount - _lastGuildAvailableTime < BaseConfig.MaxWaitBetweenGuildAvailablesBeforeReady))
await Task.Delay(500, cancelToken).ConfigureAwait(false);
await logger.DebugAsync("GuildDownloader Stopped").ConfigureAwait(false);
}
@@ -1769,17 +1802,7 @@ namespace Discord.WebSocket
return guild;
}
internal SocketGuild RemoveGuild(ulong id)
{
var guild = State.RemoveGuild(id);
if (guild != null)
{
foreach (var _ in guild.Channels)
State.RemoveChannel(id);
foreach (var user in guild.Users)
user.GlobalUser.RemoveRef(this);
}
return guild;
}
=> State.RemoveGuild(id);

/// <exception cref="InvalidOperationException">Unexpected channel type is created.</exception>
internal ISocketPrivateChannel AddPrivateChannel(API.Channel model, ClientState state)


+ 35
- 0
src/Discord.Net.WebSocket/DiscordSocketConfig.cs View File

@@ -121,6 +121,7 @@ namespace Discord.WebSocket

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

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

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


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

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

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


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

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

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

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

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

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

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


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

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

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


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

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

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

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

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


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

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

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

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

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

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


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

@@ -809,17 +809,37 @@ namespace Discord.WebSocket
var members = Users;
var self = CurrentUser;
_members.Clear();
_members.TryAdd(self.Id, self);
if (self != null)
_members.TryAdd(self.Id, self);

DownloadedMemberCount = _members.Count;

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

/// <summary>
/// Gets a collection of all users in this guild.
/// </summary>
/// <remarks>
/// <para>This method retrieves all users found within this guild throught REST.</para>
/// <para>Users returned by this method are not cached.</para>
/// </remarks>
/// <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 collection of guild
/// users found within this guild.
/// </returns>
public IAsyncEnumerable<IReadOnlyCollection<IGuildUser>> GetUsersAsync(RequestOptions options = null)
{
if (HasAllMembers)
return ImmutableArray.Create(Users).ToAsyncEnumerable<IReadOnlyCollection<IGuildUser>>();
return GuildHelper.GetUsersAsync(this, Discord, null, null, options);
}

/// <inheritdoc />
public async Task DownloadUsersAsync()
{
@@ -830,6 +850,23 @@ namespace Discord.WebSocket
_downloaderPromise.TrySetResultAsync(true);
}

/// <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"/>.
/// </summary>
/// <remarks>
/// The <paramref name="limit"/> can not be higher than <see cref="DiscordConfig.MaxUsersPerBatch"/>.
/// </remarks>
/// <param name="query">The partial name or nickname to search.</param>
/// <param name="limit">The maximum number of users to be gotten.</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 a collection of guild
/// users that the name or nickname starts with the provided <see cref="string"/> at <paramref name="query"/>.
/// </returns>
public Task<IReadOnlyCollection<RestGuildUser>> SearchUsersAsync(string query, int limit = DiscordConfig.MaxUsersPerBatch, RequestOptions options = null)
=> GuildHelper.SearchUsersAsync(this, Discord, query, limit, options);

//Audit logs
/// <summary>
/// Gets the specified number of audit log entries for this guild.
@@ -1184,8 +1221,13 @@ namespace Discord.WebSocket
=> await CreateRoleAsync(name, permissions, color, isHoisted, isMentionable, options).ConfigureAwait(false);

/// <inheritdoc />
Task<IReadOnlyCollection<IGuildUser>> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options)
=> Task.FromResult<IReadOnlyCollection<IGuildUser>>(Users);
async Task<IReadOnlyCollection<IGuildUser>> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload && !HasAllMembers)
return (await GetUsersAsync(options).FlattenAsync().ConfigureAwait(false)).ToImmutableArray();
else
return Users;
}

/// <inheritdoc />
async Task<IGuildUser> IGuild.AddGuildUserAsync(ulong userId, string accessToken, Action<AddGuildUserProperties> func, RequestOptions options)
@@ -1199,6 +1241,14 @@ namespace Discord.WebSocket
/// <inheritdoc />
Task<IGuildUser> IGuild.GetOwnerAsync(CacheMode mode, RequestOptions options)
=> Task.FromResult<IGuildUser>(Owner);
/// <inheritdoc />
async Task<IReadOnlyCollection<IGuildUser>> IGuild.SearchUsersAsync(string query, int limit, CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
return await SearchUsersAsync(query, limit, options).ConfigureAwait(false);
else
return ImmutableArray.Create<IGuildUser>();
}

/// <inheritdoc />
async Task<IReadOnlyCollection<IAuditLogEntry>> IGuild.GetAuditLogsAsync(int limit, CacheMode cacheMode, RequestOptions options,


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

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

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

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

return cachedMessageIds
.Select(x =>


+ 8
- 1
src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs View File

@@ -140,7 +140,7 @@ namespace Discord.WebSocket
Activity = new MessageActivity()
{
Type = model.Activity.Value.Type.Value,
PartyId = model.Activity.Value.PartyId.Value
PartyId = model.Activity.Value.PartyId.GetValueOrDefault()
};
}

@@ -200,6 +200,10 @@ namespace Discord.WebSocket
{
_reactions.Clear();
}
internal void RemoveReactionsForEmote(IEmote emote)
{
_reactions.RemoveAll(x => x.Emote.Equals(emote));
}

/// <inheritdoc />
public Task AddReactionAsync(IEmote emote, RequestOptions options = null)
@@ -214,6 +218,9 @@ namespace Discord.WebSocket
public Task RemoveAllReactionsAsync(RequestOptions options = null)
=> MessageHelper.RemoveAllReactionsAsync(this, Discord, options);
/// <inheritdoc />
public Task RemoveAllReactionsForEmoteAsync(IEmote emote, RequestOptions options = null)
=> MessageHelper.RemoveAllReactionsForEmoteAsync(this, emote, Discord, options);
/// <inheritdoc />
public IAsyncEnumerable<IReadOnlyCollection<IUser>> GetReactionUsersAsync(IEmote emote, int limit, RequestOptions options = null)
=> MessageHelper.GetReactionUsersAsync(this, emote, limit, Discord, options);
}


+ 14
- 2
src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs View File

@@ -123,7 +123,7 @@ namespace Discord.WebSocket
model.Content = text;
}
}
/// <inheritdoc />
/// <exception cref="InvalidOperationException">Only the author of a message may modify the message.</exception>
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
@@ -147,7 +147,19 @@ namespace Discord.WebSocket
public string Resolve(TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name,
TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name)
=> 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>
public async Task CrosspostAsync(RequestOptions options = null)
{
if (!(Channel is SocketNewsChannel))
{
throw new InvalidOperationException("Publishing (crossposting) is only valid in news channels.");
}

await MessageHelper.CrosspostAsync(this, Discord, options);
}

private string DebuggerDisplay => $"{Author}: {Content} ({Id}{(Attachments.Count > 0 ? $", {Attachments.Count} Attachments" : "")})";
internal new SocketUserMessage Clone() => MemberwiseClone() as SocketUserMessage;
}


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


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

@@ -41,11 +41,16 @@ 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>
/// <remarks>
/// This property will only include guilds in the same <see cref="DiscordSocketClient"/>.
/// </remarks>
public IReadOnlyCollection<SocketGuild> MutualGuilds
=> Discord.Guilds.Where(g => g.Users.Any(u => u.Id == Id)).ToImmutableArray();
=> Discord.Guilds.Where(g => g.GetUser(Id) != null).ToImmutableArray();

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


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

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



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

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

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

/// <summary> Creates a new Webhook Discord client. </summary>
public DiscordWebhookClient(ulong webhookId, string webhookToken, DiscordRestConfig config)
@@ -77,9 +77,9 @@ namespace Discord.Webhook
ApiClient.RequestQueue.RateLimitTriggered += async (id, info) =>
{
if (info == null)
await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {id ?? "null"}").ConfigureAwait(false);
await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {id?.ToString() ?? "null"}").ConfigureAwait(false);
else
await _restLogger.WarningAsync($"Rate limit triggered: {id ?? "null"}").ConfigureAwait(false);
await _restLogger.WarningAsync($"Rate limit triggered: {id?.ToString() ?? "null"}").ConfigureAwait(false);
};
ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false);
}
@@ -132,13 +132,13 @@ namespace Discord.Webhook
if (match != null)
{
// ensure that the first group is a ulong, set the _webhookId
// 0th group is always the entire match, so start at index 1
if (!(match.Groups[1].Success && ulong.TryParse(match.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out webhookId)))
// 0th group is always the entire match, and 1 is the domain; so start at index 2
if (!(match.Groups[2].Success && ulong.TryParse(match.Groups[2].Value, NumberStyles.None, CultureInfo.InvariantCulture, out webhookId)))
throw ex("The webhook Id could not be parsed.");

if (!match.Groups[2].Success)
if (!match.Groups[3].Success)
throw ex("The webhook token could not be parsed.");
webhookToken = match.Groups[2].Value;
webhookToken = match.Groups[3].Value;
}
else
throw ex();


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

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

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

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


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

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

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

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


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

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

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

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


Loading…
Cancel
Save