diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index bf94084ad..c6007d2dd 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -367,6 +367,10 @@ namespace Discord.API { await SendGateway(GatewayOpCode.Heartbeat, lastSeq, options: options).ConfigureAwait(false); } + public async Task SendRequestMembers(IEnumerable guildIds, RequestOptions options = null) + { + await SendGateway(GatewayOpCode.RequestGuildMembers, new RequestMembersParams { GuildIds = guildIds, Query = "", Limit = 0 }, options: options).ConfigureAwait(false); + } //Channels public async Task GetChannel(ulong channelId, RequestOptions options = null) diff --git a/src/Discord.Net/API/Gateway/RequestMembersParams.cs b/src/Discord.Net/API/Gateway/RequestMembersParams.cs index ed6edc6ef..f11be49b1 100644 --- a/src/Discord.Net/API/Gateway/RequestMembersParams.cs +++ b/src/Discord.Net/API/Gateway/RequestMembersParams.cs @@ -1,11 +1,12 @@ using Newtonsoft.Json; +using System.Collections.Generic; namespace Discord.API.Gateway { public class RequestMembersParams { [JsonProperty("guild_id")] - public ulong[] GuildId { get; set; } + public IEnumerable GuildIds { get; set; } [JsonProperty("query")] public string Query { get; set; } [JsonProperty("limit")] diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index fe33f223a..177a44694 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -17,9 +17,8 @@ using System.Threading.Tasks; namespace Discord { - //TODO: Remove unnecessary `as` casts //TODO: Add event docstrings - //TODO: Add reconnect logic (+ensure the heartbeat task shuts down) + //TODO: Add reconnect logic (+ensure the heartbeat task to shut down) //TODO: Add resume logic public class DiscordSocketClient : DiscordClient, IDiscordClient { @@ -32,7 +31,7 @@ namespace Discord public event Func MessageUpdated; public event Func RoleCreated, RoleDeleted; public event Func RoleUpdated; - public event Func JoinedGuild, LeftGuild, GuildAvailable, GuildUnavailable; + public event Func JoinedGuild, LeftGuild, GuildAvailable, GuildUnavailable, GuildDownloadedMembers; public event Func GuildUpdated; public event Func UserJoined, UserLeft, UserBanned, UserUnbanned; public event Func UserUpdated; @@ -305,6 +304,47 @@ namespace Discord return user; } + /// Downloads the members list for all large guilds. + public Task DownloadAllMembers() + => DownloadMembers(DataStore.Guilds.Where(x => !x.HasAllMembers)); + /// Downloads the members list for the provided guilds, if they don't have a complete list. + public async Task DownloadMembers(IEnumerable guilds) + { + const short batchSize = 50; + var cachedGuilds = guilds.Select(x => x as CachedGuild).ToArray(); + if (cachedGuilds.Length == 0) + return; + else if (cachedGuilds.Length == 1) + { + await cachedGuilds[0].DownloadMembers().ConfigureAwait(false); + return; + } + + ulong[] batchIds = new ulong[Math.Min(batchSize, cachedGuilds.Length)]; + Task[] batchTasks = new Task[batchIds.Length]; + int batchCount = (cachedGuilds.Length + (batchSize - 1)) / batchSize; + + for (int i = 0, k = 0; i < batchCount; i++) + { + bool isLast = i == batchCount - 1; + int count = isLast ? (batchIds.Length - (batchCount - 1) * batchSize) : batchSize; + + for (int j = 0; j < count; j++, k++) + { + var guild = cachedGuilds[k]; + batchIds[j] = guild.Id; + batchTasks[j] = guild.DownloaderPromise; + } + + ApiClient.SendRequestMembers(batchIds); + + if (isLast && batchCount > 1) + await Task.WhenAll(batchTasks.Take(count)).ConfigureAwait(false); + else + await Task.WhenAll(batchTasks).ConfigureAwait(false); + } + } + private async Task ProcessMessage(GatewayOpCode opCode, int? seq, string type, object payload) { if (seq != null) @@ -367,11 +407,8 @@ namespace Discord type = "GUILD_AVAILABLE"; else await JoinedGuild.Raise(guild).ConfigureAwait(false); - - if (!data.Large) - await GuildAvailable.Raise(guild); - else - _largeGuilds.Enqueue(data.Id); + + await GuildAvailable.Raise(guild); } break; case "GUILD_UPDATE": @@ -781,15 +818,19 @@ namespace Discord } private async Task RunHeartbeat(int intervalMillis, CancellationToken cancelToken) { - var state = ConnectionState; - while (state == ConnectionState.Connecting || state == ConnectionState.Connected) + try { - //if (_heartbeatTime != 0) //TODO: Connection lost, reconnect + var state = ConnectionState; + while (state == ConnectionState.Connecting || state == ConnectionState.Connected) + { + //if (_heartbeatTime != 0) //TODO: Connection lost, reconnect - _heartbeatTime = Environment.TickCount; - await ApiClient.SendHeartbeat(_lastSeq).ConfigureAwait(false); - await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false); + _heartbeatTime = Environment.TickCount; + await ApiClient.SendHeartbeat(_lastSeq).ConfigureAwait(false); + await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false); + } } + catch (OperationCanceledException) { } } } } diff --git a/src/Discord.Net/Entities/Guilds/Guild.cs b/src/Discord.Net/Entities/Guilds/Guild.cs index 2e3842bdb..65ce5ec71 100644 --- a/src/Discord.Net/Entities/Guilds/Guild.cs +++ b/src/Discord.Net/Entities/Guilds/Guild.cs @@ -306,6 +306,7 @@ namespace Discord IRole IGuild.EveryoneRole => EveryoneRole; IReadOnlyCollection IGuild.Emojis => Emojis; IReadOnlyCollection IGuild.Features => Features; + Task IGuild.DownloadUsers() { throw new NotSupportedException(); } IRole IGuild.GetRole(ulong id) => GetRole(id); } diff --git a/src/Discord.Net/Entities/Guilds/IGuild.cs b/src/Discord.Net/Entities/Guilds/IGuild.cs index 013265fd3..8d86dcd8b 100644 --- a/src/Discord.Net/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net/Entities/Guilds/IGuild.cs @@ -90,6 +90,9 @@ namespace Discord Task GetUser(ulong id); /// Gets the current user for this guild. Task GetCurrentUser(); + /// Downloads all users for this guild if the current list is incomplete. + Task DownloadUsers(); + /// Removes all users from this guild if they have not logged on in a provided number of days or, if simulate is true, returns the number of users that would be removed. Task PruneUsers(int days = 30, bool simulate = false); } } \ No newline at end of file diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs index 32b956e39..8890c8230 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs @@ -16,6 +16,7 @@ namespace Discord { internal class CachedGuild : Guild, ICachedEntity { + private TaskCompletionSource _downloaderPromise; private ConcurrentHashSet _channels; private ConcurrentDictionary _members; private ConcurrentDictionary _presences; @@ -23,6 +24,9 @@ namespace Discord public bool Available { get; private set; } //TODO: Add to IGuild + public bool HasAllMembers => _downloaderPromise.Task.IsCompleted; + public Task DownloaderPromise => _downloaderPromise.Task; + public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; public CachedGuildUser CurrentUser => GetCachedUser(Discord.CurrentUser.Id); public IReadOnlyCollection Channels => _channels.Select(x => GetCachedChannel(x)).ToReadOnlyCollection(_channels); @@ -30,6 +34,7 @@ namespace Discord public CachedGuild(DiscordSocketClient discord, Model model) : base(discord, model) { + _downloaderPromise = new TaskCompletionSource(); } public void Update(ExtendedModel model, UpdateSource source, DataStore dataStore) @@ -79,6 +84,9 @@ namespace Discord { for (int i = 0; i < model.Members.Length; i++) AddCachedUser(model.Members[i], members, dataStore); + _downloaderPromise = new TaskCompletionSource(); + if (!model.Large) + _downloaderPromise.SetResult(true); } _members = members; } @@ -153,6 +161,17 @@ namespace Discord return null; } + public async Task DownloadMembers() + { + if (!HasAllMembers) + await Discord.ApiClient.SendRequestMembers(new ulong[] { Id }).ConfigureAwait(false); + await _downloaderPromise.Task.ConfigureAwait(false); + } + public void CompleteDownloadMembers() + { + _downloaderPromise.SetResult(true); + } + public CachedGuild Clone() => MemberwiseClone() as CachedGuild; new internal ICachedGuildChannel ToChannel(ChannelModel model)