@@ -367,6 +367,10 @@ namespace Discord.API | |||||
{ | { | ||||
await SendGateway(GatewayOpCode.Heartbeat, lastSeq, options: options).ConfigureAwait(false); | 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 | //Channels | ||||
public async Task<Channel> GetChannel(ulong channelId, RequestOptions options = null) | public async Task<Channel> GetChannel(ulong channelId, RequestOptions options = null) | ||||
@@ -1,11 +1,12 @@ | |||||
using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
using System.Collections.Generic; | |||||
namespace Discord.API.Gateway | namespace Discord.API.Gateway | ||||
{ | { | ||||
public class RequestMembersParams | public class RequestMembersParams | ||||
{ | { | ||||
[JsonProperty("guild_id")] | [JsonProperty("guild_id")] | ||||
public ulong[] GuildId { get; set; } | |||||
public IEnumerable<ulong> GuildIds { get; set; } | |||||
[JsonProperty("query")] | [JsonProperty("query")] | ||||
public string Query { get; set; } | public string Query { get; set; } | ||||
[JsonProperty("limit")] | [JsonProperty("limit")] | ||||
@@ -17,9 +17,8 @@ using System.Threading.Tasks; | |||||
namespace Discord | namespace Discord | ||||
{ | { | ||||
//TODO: Remove unnecessary `as` casts | |||||
//TODO: Add event docstrings | //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 | //TODO: Add resume logic | ||||
public class DiscordSocketClient : DiscordClient, IDiscordClient | public class DiscordSocketClient : DiscordClient, IDiscordClient | ||||
{ | { | ||||
@@ -32,7 +31,7 @@ namespace Discord | |||||
public event Func<IMessage, IMessage, Task> MessageUpdated; | public event Func<IMessage, IMessage, Task> MessageUpdated; | ||||
public event Func<IRole, Task> RoleCreated, RoleDeleted; | public event Func<IRole, Task> RoleCreated, RoleDeleted; | ||||
public event Func<IRole, IRole, Task> RoleUpdated; | 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<IGuild, IGuild, Task> GuildUpdated; | ||||
public event Func<IUser, Task> UserJoined, UserLeft, UserBanned, UserUnbanned; | public event Func<IUser, Task> UserJoined, UserLeft, UserBanned, UserUnbanned; | ||||
public event Func<IUser, IUser, Task> UserUpdated; | public event Func<IUser, IUser, Task> UserUpdated; | ||||
@@ -305,6 +304,47 @@ namespace Discord | |||||
return user; | 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) | private async Task ProcessMessage(GatewayOpCode opCode, int? seq, string type, object payload) | ||||
{ | { | ||||
if (seq != null) | if (seq != null) | ||||
@@ -367,11 +407,8 @@ namespace Discord | |||||
type = "GUILD_AVAILABLE"; | type = "GUILD_AVAILABLE"; | ||||
else | else | ||||
await JoinedGuild.Raise(guild).ConfigureAwait(false); | await JoinedGuild.Raise(guild).ConfigureAwait(false); | ||||
if (!data.Large) | |||||
await GuildAvailable.Raise(guild); | |||||
else | |||||
_largeGuilds.Enqueue(data.Id); | |||||
await GuildAvailable.Raise(guild); | |||||
} | } | ||||
break; | break; | ||||
case "GUILD_UPDATE": | case "GUILD_UPDATE": | ||||
@@ -781,15 +818,19 @@ namespace Discord | |||||
} | } | ||||
private async Task RunHeartbeat(int intervalMillis, CancellationToken cancelToken) | 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; | IRole IGuild.EveryoneRole => EveryoneRole; | ||||
IReadOnlyCollection<Emoji> IGuild.Emojis => Emojis; | IReadOnlyCollection<Emoji> IGuild.Emojis => Emojis; | ||||
IReadOnlyCollection<string> IGuild.Features => Features; | IReadOnlyCollection<string> IGuild.Features => Features; | ||||
Task IGuild.DownloadUsers() { throw new NotSupportedException(); } | |||||
IRole IGuild.GetRole(ulong id) => GetRole(id); | IRole IGuild.GetRole(ulong id) => GetRole(id); | ||||
} | } | ||||
@@ -90,6 +90,9 @@ namespace Discord | |||||
Task<IGuildUser> GetUser(ulong id); | Task<IGuildUser> GetUser(ulong id); | ||||
/// <summary> Gets the current user for this guild. </summary> | /// <summary> Gets the current user for this guild. </summary> | ||||
Task<IGuildUser> GetCurrentUser(); | 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); | Task<int> PruneUsers(int days = 30, bool simulate = false); | ||||
} | } | ||||
} | } |
@@ -16,6 +16,7 @@ namespace Discord | |||||
{ | { | ||||
internal class CachedGuild : Guild, ICachedEntity<ulong> | internal class CachedGuild : Guild, ICachedEntity<ulong> | ||||
{ | { | ||||
private TaskCompletionSource<bool> _downloaderPromise; | |||||
private ConcurrentHashSet<ulong> _channels; | private ConcurrentHashSet<ulong> _channels; | ||||
private ConcurrentDictionary<ulong, CachedGuildUser> _members; | private ConcurrentDictionary<ulong, CachedGuildUser> _members; | ||||
private ConcurrentDictionary<ulong, Presence> _presences; | private ConcurrentDictionary<ulong, Presence> _presences; | ||||
@@ -23,6 +24,9 @@ namespace Discord | |||||
public bool Available { get; private set; } //TODO: Add to IGuild | 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 new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; | ||||
public CachedGuildUser CurrentUser => GetCachedUser(Discord.CurrentUser.Id); | public CachedGuildUser CurrentUser => GetCachedUser(Discord.CurrentUser.Id); | ||||
public IReadOnlyCollection<ICachedGuildChannel> Channels => _channels.Select(x => GetCachedChannel(x)).ToReadOnlyCollection(_channels); | 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) | public CachedGuild(DiscordSocketClient discord, Model model) : base(discord, model) | ||||
{ | { | ||||
_downloaderPromise = new TaskCompletionSource<bool>(); | |||||
} | } | ||||
public void Update(ExtendedModel model, UpdateSource source, DataStore dataStore) | public void Update(ExtendedModel model, UpdateSource source, DataStore dataStore) | ||||
@@ -79,6 +84,9 @@ namespace Discord | |||||
{ | { | ||||
for (int i = 0; i < model.Members.Length; i++) | for (int i = 0; i < model.Members.Length; i++) | ||||
AddCachedUser(model.Members[i], members, dataStore); | AddCachedUser(model.Members[i], members, dataStore); | ||||
_downloaderPromise = new TaskCompletionSource<bool>(); | |||||
if (!model.Large) | |||||
_downloaderPromise.SetResult(true); | |||||
} | } | ||||
_members = members; | _members = members; | ||||
} | } | ||||
@@ -153,6 +161,17 @@ namespace Discord | |||||
return null; | 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; | public CachedGuild Clone() => MemberwiseClone() as CachedGuild; | ||||
new internal ICachedGuildChannel ToChannel(ChannelModel model) | new internal ICachedGuildChannel ToChannel(ChannelModel model) | ||||