Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow specifying list of user ids in SocketGuild.DownloadUsersAsync #2676

Open
wants to merge 6 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Discord.Net.Core/DiscordConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -232,5 +232,10 @@ public class DiscordConfig
/// Returns the max length of an application description.
/// </summary>
public const int MaxApplicationDescriptionLength = 400;

/// <summary>
/// Returns the max number of user IDs that can be requested in a Request Guild Members chunk.
/// </summary>
public const int MaxRequestedUserIdsPerRequestGuildMembersChunk = 100;
}
}
28 changes: 28 additions & 0 deletions src/Discord.Net.Core/Entities/Guilds/IGuild.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace Discord
Expand Down Expand Up @@ -958,6 +959,33 @@ public interface IGuild : IDeletable, ISnowflakeEntity
/// A task that represents the asynchronous download operation.
/// </returns>
Task DownloadUsersAsync();

/// <summary>
/// Downloads specific users for this guild with the default request timeout.
/// </summary>
/// <remarks>
/// This method downloads all users specified in <paramref name="userIds" /> through the Gateway and caches them.
/// Consider using <see cref="DownloadUsersAsync(IEnumerable{ulong}, CancellationToken)"/> when downloading a large number of users.
/// </remarks>
/// <param name="userIds">The list of Discord user IDs to download.</param>
/// <returns>
/// A task that represents the asynchronous download operation.
/// </returns>
/// <exception cref="OperationCanceledException">The timeout has elapsed.</exception>
Task DownloadUsersAsync(IEnumerable<ulong> userIds);
Comment on lines +962 to +975
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of defining 2 methods with the only difference being a cancel token, why not make the token parameter optional?


/// <summary>
/// Downloads specific users for this guild.
/// </summary>
/// <remarks>
/// This method downloads all users specified in <paramref name="userIds" /> through the Gateway and caches them.
/// </remarks>
/// <param name="userIds">The list of Discord user IDs to download.</param>
/// <param name="cancelToken">The cancellation token used to cancel the task.</param>
/// <returns>
/// A task that represents the asynchronous download operation.
/// </returns>
Task DownloadUsersAsync(IEnumerable<ulong> userIds, CancellationToken cancelToken);
/// <summary>
/// Prunes inactive users.
/// </summary>
Expand Down
103 changes: 103 additions & 0 deletions src/Discord.Net.Core/Extensions/EnumerableExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Based on https://github.com/dotnet/runtime/blob/main/src/libraries/System.Linq/src/System/Linq/Chunk.cs (only available on .NET 6+)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace Discord
{
internal static class EnumerableExtensions
{
/// <summary>
/// Split the elements of a sequence into chunks of size at most <paramref name="size"/>.
/// </summary>
/// <remarks>
/// Every chunk except the last will be of size <paramref name="size"/>.
/// The last chunk will contain the remaining elements and may be of a smaller size.
/// </remarks>
/// <param name="source">
/// An <see cref="IEnumerable{T}"/> whose elements to chunk.
/// </param>
/// <param name="size">
/// Maximum size of each chunk.
/// </param>
/// <typeparam name="TSource">
/// The type of the elements of source.
/// </typeparam>
/// <returns>
/// An <see cref="IEnumerable{T}"/> that contains the elements the input sequence split into chunks of size <paramref name="size"/>.
/// </returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="source"/> is null.
/// </exception>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="size"/> is below 1.
/// </exception>
public static IEnumerable<TSource[]> Chunk<TSource>(this IEnumerable<TSource> source, int size)
{
Preconditions.NotNull(source, nameof(source));
Preconditions.GreaterThan(size, 0, nameof(size));

return ChunkIterator(source, size);
}

private static IEnumerable<TSource[]> ChunkIterator<TSource>(IEnumerable<TSource> source, int size)
{
using IEnumerator<TSource> e = source.GetEnumerator();

// Before allocating anything, make sure there's at least one element.
if (e.MoveNext())
{
// Now that we know we have at least one item, allocate an initial storage array. This is not
// the array we'll yield. It starts out small in order to avoid significantly overallocating
// when the source has many fewer elements than the chunk size.
int arraySize = Math.Min(size, 4);
int i;
do
{
var array = new TSource[arraySize];

// Store the first item.
array[0] = e.Current;
i = 1;

if (size != array.Length)
{
// This is the first chunk. As we fill the array, grow it as needed.
for (; i < size && e.MoveNext(); i++)
{
if (i >= array.Length)
{
arraySize = (int)Math.Min((uint)size, 2 * (uint)array.Length);
Array.Resize(ref array, arraySize);
}

array[i] = e.Current;
}
}
else
{
// For all but the first chunk, the array will already be correctly sized.
// We can just store into it until either it's full or MoveNext returns false.
TSource[] local = array; // avoid bounds checks by using cached local (`array` is lifted to iterator object as a field)
Debug.Assert(local.Length == size);
for (; (uint)i < (uint)local.Length && e.MoveNext(); i++)
{
local[i] = e.Current;
}
}

if (i != array.Length)
{
Array.Resize(ref array, i);
}

yield return array;
}
while (i >= size && e.MoveNext());
}
}
}
}
11 changes: 10 additions & 1 deletion src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Model = Discord.API.Guild;
using WidgetModel = Discord.API.GuildWidget;
Expand Down Expand Up @@ -1512,6 +1513,14 @@ async Task<IReadOnlyCollection<IGuildUser>> IGuild.GetUsersAsync(CacheMode mode,
Task IGuild.DownloadUsersAsync() =>
throw new NotSupportedException();
/// <inheritdoc />
/// <exception cref="NotSupportedException">Downloading users is not supported for a REST-based guild.</exception>
Task IGuild.DownloadUsersAsync(IEnumerable<ulong> userIds) =>
throw new NotSupportedException();
/// <inheritdoc />
/// <exception cref="NotSupportedException">Downloading users is not supported for a REST-based guild.</exception>
Task IGuild.DownloadUsersAsync(IEnumerable<ulong> userIds, CancellationToken cancelToken) =>
throw new NotSupportedException();
/// <inheritdoc />
async Task<IReadOnlyCollection<IGuildUser>> IGuild.SearchUsersAsync(string query, int limit, CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
Expand Down Expand Up @@ -1604,7 +1613,7 @@ async Task<IAutoModRule[]> IGuild.GetAutoModRulesAsync(RequestOptions options)
/// <inheritdoc/>
async Task<IAutoModRule> IGuild.CreateAutoModRuleAsync(Action<AutoModRuleProperties> props, RequestOptions options)
=> await CreateAutoModRuleAsync(props, options).ConfigureAwait(false);

/// <inheritdoc/>
async Task<IGuildOnboarding> IGuild.GetOnboardingAsync(RequestOptions options)
=> await GetOnboardingAsync(options);
Expand Down
11 changes: 11 additions & 0 deletions src/Discord.Net.WebSocket/API/Gateway/GuildMembersChunkEvent.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Newtonsoft.Json;
using System.Collections.Generic;

namespace Discord.API.Gateway
{
Expand All @@ -8,5 +9,15 @@ internal class GuildMembersChunkEvent
public ulong GuildId { get; set; }
[JsonProperty("members")]
public GuildMember[] Members { get; set; }
[JsonProperty("chunk_index")]
public int ChunkIndex { get; set; }
[JsonProperty("chunk_count")]
public int ChunkCount { get; set; }
[JsonProperty("not_found")]
public Optional<IEnumerable<ulong>> NotFound { get; set; }
[JsonProperty("presences")]
public Optional<IEnumerable<Presence>> Presences { get; set; }
[JsonProperty("nonce")]
public Optional<string> Nonce { get; set; }
}
}
13 changes: 9 additions & 4 deletions src/Discord.Net.WebSocket/API/Gateway/RequestMembersParams.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@ namespace Discord.API.Gateway
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
internal class RequestMembersParams
{
[JsonProperty("guild_id")]
public ulong GuildId { get; set; }
[JsonProperty("query")]
public string Query { get; set; }
public Optional<string> Query { get; set; }
[JsonProperty("limit")]
public int Limit { get; set; }

[JsonProperty("guild_id")]
public IEnumerable<ulong> GuildIds { get; set; }
[JsonProperty("presences")]
public Optional<bool> Presences { get; set; }
[JsonProperty("user_ids")]
public Optional<IEnumerable<ulong>> UserIds { get; set; }
[JsonProperty("nonce")]
public Optional<string> Nonce { get; set; }
}
}
12 changes: 12 additions & 0 deletions src/Discord.Net.WebSocket/BaseSocketClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace Discord.WebSocket
Expand Down Expand Up @@ -236,6 +237,17 @@ private static DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config
/// </returns>
public abstract Task DownloadUsersAsync(IEnumerable<IGuild> guilds);

/// <summary>
/// Attempts to download specific users into the user cache for the selected guild.
/// </summary>
/// <param name="guild">The guild to download the members from.</param>
/// <param name="userIds">The list of Discord user IDs to download.</param>
/// <param name="cancelToken">The cancellation token used to cancel the task.</param>
/// <returns>
/// A task that represents the asynchronous download operation.
/// </returns>
public abstract Task DownloadUsersAsync(IGuild guild, IEnumerable<ulong> userIds, CancellationToken cancelToken = default);

/// <summary>
/// Creates a guild for the logged-in user who is in less than 10 active guilds.
/// </summary>
Expand Down
17 changes: 17 additions & 0 deletions src/Discord.Net.WebSocket/DiscordShardedClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,23 @@ public override async Task DownloadUsersAsync(IEnumerable<IGuild> guilds)
}
}

/// <inheritdoc />
/// <exception cref="ArgumentNullException"><paramref name="guild"/> is <see langword="null"/></exception>
public override async Task DownloadUsersAsync(IGuild guild, IEnumerable<ulong> userIds, CancellationToken cancelToken = default)
{
Preconditions.NotNull(guild, nameof(guild));

for (int i = 0; i < _shards.Length; i++)
{
int id = _shardIds[i];
if (GetShardIdFor(guild) == id)
{
await _shards[i].DownloadUsersAsync(guild, userIds, cancelToken).ConfigureAwait(false);
break;
}
}
}

private int GetLatency()
{
int total = 0;
Expand Down
17 changes: 15 additions & 2 deletions src/Discord.Net.WebSocket/DiscordSocketApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -379,11 +379,24 @@ public async Task SendPresenceUpdateAsync(UserStatus status, bool isAFK, long? s
options.BucketId = GatewayBucket.Get(GatewayBucketType.PresenceUpdate).Id;
await SendGatewayAsync(GatewayOpCode.PresenceUpdate, args, options: options).ConfigureAwait(false);
}
public async Task SendRequestMembersAsync(IEnumerable<ulong> guildIds, RequestOptions options = null)
public async Task SendRequestMembersAsync(ulong guildId, RequestOptions options = null)
{
options = RequestOptions.CreateOrClone(options);
await SendGatewayAsync(GatewayOpCode.RequestGuildMembers, new RequestMembersParams { GuildIds = guildIds, Query = "", Limit = 0 }, options: options).ConfigureAwait(false);
await SendGatewayAsync(GatewayOpCode.RequestGuildMembers, new RequestMembersParams { GuildId = guildId, Query = "", Limit = 0 }, options: options).ConfigureAwait(false);
}
public async Task SendRequestMembersAsync(ulong guildId, IEnumerable<ulong> userIds, string nonce, RequestOptions options = null)
{
var payload = new RequestMembersParams
{
GuildId = guildId,
Limit = 0,
UserIds = new Optional<IEnumerable<ulong>>(userIds),
Nonce = nonce
};
options = RequestOptions.CreateOrClone(options);
await SendGatewayAsync(GatewayOpCode.RequestGuildMembers, payload, options: options).ConfigureAwait(false);
}

public async Task SendVoiceStateUpdateAsync(ulong guildId, ulong? channelId, bool selfDeaf, bool selfMute, RequestOptions options = null)
{
var payload = new VoiceStateUpdateParams
Expand Down
Loading