@@ -367,6 +367,10 @@ namespace Discord.API | |||
{ | |||
await SendGateway(GatewayOpCode.Heartbeat, lastSeq, options: options).ConfigureAwait(false); | |||
} | |||
public async Task SendRequestMembers(IEnumerable<ulong> guildIds, RequestOptions options = null) | |||
{ | |||
await SendGateway(GatewayOpCode.RequestGuildMembers, new RequestMembersParams { GuildIds = guildIds, Query = "", Limit = 0 }, options: options).ConfigureAwait(false); | |||
} | |||
//Channels | |||
public async Task<Channel> GetChannel(ulong channelId, RequestOptions options = null) | |||
@@ -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<ulong> GuildIds { get; set; } | |||
[JsonProperty("query")] | |||
public string Query { get; set; } | |||
[JsonProperty("limit")] | |||
@@ -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<IMessage, IMessage, Task> MessageUpdated; | |||
public event Func<IRole, Task> RoleCreated, RoleDeleted; | |||
public event Func<IRole, IRole, Task> RoleUpdated; | |||
public event Func<IGuild, Task> JoinedGuild, LeftGuild, GuildAvailable, GuildUnavailable; | |||
public event Func<IGuild, Task> JoinedGuild, LeftGuild, GuildAvailable, GuildUnavailable, GuildDownloadedMembers; | |||
public event Func<IGuild, IGuild, Task> GuildUpdated; | |||
public event Func<IUser, Task> UserJoined, UserLeft, UserBanned, UserUnbanned; | |||
public event Func<IUser, IUser, Task> UserUpdated; | |||
@@ -305,6 +304,47 @@ namespace Discord | |||
return user; | |||
} | |||
/// <summary> Downloads the members list for all large guilds. </summary> | |||
public Task DownloadAllMembers() | |||
=> DownloadMembers(DataStore.Guilds.Where(x => !x.HasAllMembers)); | |||
/// <summary> Downloads the members list for the provided guilds, if they don't have a complete list. </summary> | |||
public async Task DownloadMembers(IEnumerable<IGuild> 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) { } | |||
} | |||
} | |||
} |
@@ -306,6 +306,7 @@ namespace Discord | |||
IRole IGuild.EveryoneRole => EveryoneRole; | |||
IReadOnlyCollection<Emoji> IGuild.Emojis => Emojis; | |||
IReadOnlyCollection<string> IGuild.Features => Features; | |||
Task IGuild.DownloadUsers() { throw new NotSupportedException(); } | |||
IRole IGuild.GetRole(ulong id) => GetRole(id); | |||
} | |||
@@ -90,6 +90,9 @@ namespace Discord | |||
Task<IGuildUser> GetUser(ulong id); | |||
/// <summary> Gets the current user for this guild. </summary> | |||
Task<IGuildUser> GetCurrentUser(); | |||
/// <summary> Downloads all users for this guild if the current list is incomplete. </summary> | |||
Task DownloadUsers(); | |||
/// <summary> 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. </summary> | |||
Task<int> PruneUsers(int days = 30, bool simulate = false); | |||
} | |||
} |
@@ -16,6 +16,7 @@ namespace Discord | |||
{ | |||
internal class CachedGuild : Guild, ICachedEntity<ulong> | |||
{ | |||
private TaskCompletionSource<bool> _downloaderPromise; | |||
private ConcurrentHashSet<ulong> _channels; | |||
private ConcurrentDictionary<ulong, CachedGuildUser> _members; | |||
private ConcurrentDictionary<ulong, Presence> _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<ICachedGuildChannel> 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<bool>(); | |||
} | |||
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<bool>(); | |||
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) | |||