@@ -409,6 +409,10 @@ namespace Discord.API | |||||
}; | }; | ||||
await SendGatewayAsync(GatewayOpCode.VoiceStateUpdate, payload, options: options).ConfigureAwait(false); | await SendGatewayAsync(GatewayOpCode.VoiceStateUpdate, payload, options: options).ConfigureAwait(false); | ||||
} | } | ||||
public async Task SendGuildSyncAsync(IEnumerable<ulong> guildIds, RequestOptions options = null) | |||||
{ | |||||
await SendGatewayAsync(GatewayOpCode.GuildSync, guildIds, options: options).ConfigureAwait(false); | |||||
} | |||||
//Channels | //Channels | ||||
public async Task<Channel> GetChannelAsync(ulong channelId, RequestOptions options = null) | public async Task<Channel> GetChannelAsync(ulong channelId, RequestOptions options = null) | ||||
@@ -18,13 +18,15 @@ | |||||
Resume = 6, | Resume = 6, | ||||
/// <summary> C←S - Used to notify a client that they must reconnect to another gateway. </summary> | /// <summary> C←S - Used to notify a client that they must reconnect to another gateway. </summary> | ||||
Reconnect = 7, | Reconnect = 7, | ||||
/// <summary> C→S - Used to request all members that were withheld by large_threshold </summary> | |||||
/// <summary> C→S - Used to request members that were withheld by large_threshold </summary> | |||||
RequestGuildMembers = 8, | RequestGuildMembers = 8, | ||||
/// <summary> C←S - Used to notify the client that their session has expired and cannot be resumed. </summary> | /// <summary> C←S - Used to notify the client that their session has expired and cannot be resumed. </summary> | ||||
InvalidSession = 9, | InvalidSession = 9, | ||||
/// <summary> C←S - Used to provide information to the client immediately on connection. </summary> | /// <summary> C←S - Used to provide information to the client immediately on connection. </summary> | ||||
Hello = 10, | Hello = 10, | ||||
/// <summary> C←S - Used to reply to a client's heartbeat. </summary> | /// <summary> C←S - Used to reply to a client's heartbeat. </summary> | ||||
HeartbeatAck = 11 | |||||
HeartbeatAck = 11, | |||||
/// <summary> C→S - Used to request presence updates from particular guilds. </summary> | |||||
GuildSync = 12 | |||||
} | } | ||||
} | } |
@@ -0,0 +1,17 @@ | |||||
using Newtonsoft.Json; | |||||
namespace Discord.API.Gateway | |||||
{ | |||||
public class GuildSyncEvent | |||||
{ | |||||
[JsonProperty("id")] | |||||
public ulong Id { get; set; } | |||||
[JsonProperty("large")] | |||||
public bool Large { get; set; } | |||||
[JsonProperty("presences")] | |||||
public Presence[] Presences { get; set; } | |||||
[JsonProperty("members")] | |||||
public GuildMember[] Members { get; set; } | |||||
} | |||||
} |
@@ -29,6 +29,8 @@ namespace Discord | |||||
private int _lastSeq; | private int _lastSeq; | ||||
private ImmutableDictionary<string, VoiceRegion> _voiceRegions; | private ImmutableDictionary<string, VoiceRegion> _voiceRegions; | ||||
private TaskCompletionSource<bool> _connectTask; | private TaskCompletionSource<bool> _connectTask; | ||||
private ConcurrentHashSet<ulong> _syncedGuilds; | |||||
private SemaphoreSlim _syncedGuildsLock; | |||||
private CancellationTokenSource _cancelToken; | private CancellationTokenSource _cancelToken; | ||||
private Task _heartbeatTask, _guildDownloadTask, _reconnectTask; | private Task _heartbeatTask, _guildDownloadTask, _reconnectTask; | ||||
private long _heartbeatTime; | private long _heartbeatTime; | ||||
@@ -102,6 +104,8 @@ namespace Discord | |||||
_voiceRegions = ImmutableDictionary.Create<string, VoiceRegion>(); | _voiceRegions = ImmutableDictionary.Create<string, VoiceRegion>(); | ||||
_largeGuilds = new ConcurrentQueue<ulong>(); | _largeGuilds = new ConcurrentQueue<ulong>(); | ||||
_syncedGuilds = new ConcurrentHashSet<ulong>(); | |||||
_syncedGuildsLock = new SemaphoreSlim(1, 1); | |||||
} | } | ||||
protected override async Task OnLoginAsync() | protected override async Task OnLoginAsync() | ||||
@@ -295,7 +299,7 @@ namespace Discord | |||||
{ | { | ||||
return Task.FromResult<IReadOnlyCollection<IUserGuild>>(Guilds); | return Task.FromResult<IReadOnlyCollection<IUserGuild>>(Guilds); | ||||
} | } | ||||
internal CachedGuild AddGuild(API.Gateway.ExtendedGuild model, DataStore dataStore) | |||||
internal CachedGuild AddGuild(ExtendedGuild model, DataStore dataStore) | |||||
{ | { | ||||
var guild = new CachedGuild(this, model, dataStore); | var guild = new CachedGuild(this, model, dataStore); | ||||
dataStore.AddGuild(guild); | dataStore.AddGuild(guild); | ||||
@@ -305,6 +309,7 @@ namespace Discord | |||||
} | } | ||||
internal CachedGuild RemoveGuild(ulong id) | internal CachedGuild RemoveGuild(ulong id) | ||||
{ | { | ||||
_syncedGuilds.TryRemove(id); | |||||
var guild = DataStore.RemoveGuild(id); | var guild = DataStore.RemoveGuild(id); | ||||
foreach (var channel in guild.Channels) | foreach (var channel in guild.Channels) | ||||
guild.RemoveChannel(channel.Id); | guild.RemoveChannel(channel.Id); | ||||
@@ -363,18 +368,47 @@ namespace Discord | |||||
} | } | ||||
/// <summary> Downloads the users list for all large guilds. </summary> | /// <summary> Downloads the users list for all large guilds. </summary> | ||||
public Task DownloadAllUsersAsync() | |||||
public Task DownloadAllUsersAsync() | |||||
=> DownloadUsersAsync(DataStore.Guilds.Where(x => !x.HasAllMembers)); | => DownloadUsersAsync(DataStore.Guilds.Where(x => !x.HasAllMembers)); | ||||
/// <summary> Downloads the users list for the provided guilds, if they don't have a complete list. </summary> | /// <summary> Downloads the users list for the provided guilds, if they don't have a complete list. </summary> | ||||
public async Task DownloadUsersAsync(IEnumerable<IGuild> guilds) | |||||
public Task DownloadUsersAsync(IEnumerable<IGuild> guilds) | |||||
=> DownloadUsersAsync(guilds.Select(x => x as CachedGuild).Where(x => x != null)); | |||||
public Task DownloadUsersAsync(params IGuild[] guilds) | |||||
=> DownloadUsersAsync(guilds.Select(x => x as CachedGuild).Where(x => x != null)); | |||||
private async Task DownloadUsersAsync(IEnumerable<CachedGuild> guilds) | |||||
{ | { | ||||
var cachedGuilds = guilds.ToArray(); | |||||
if (cachedGuilds.Length == 0) return; | |||||
//Sync guilds | |||||
if (ApiClient.AuthTokenType == TokenType.User) | |||||
{ | |||||
await _syncedGuildsLock.WaitAsync().ConfigureAwait(false); | |||||
try | |||||
{ | |||||
foreach (var guild in cachedGuilds) | |||||
_syncedGuilds.TryAdd(guild.Id); | |||||
await ApiClient.SendGuildSyncAsync(_syncedGuilds).ConfigureAwait(false); | |||||
await Task.WhenAll(cachedGuilds.Select(x => x.SyncPromise)); | |||||
//Reduce the list only to those with members left to download | |||||
cachedGuilds = cachedGuilds.Where(x => !x.HasAllMembers).ToArray(); | |||||
if (cachedGuilds.Length == 0) return; | |||||
} | |||||
finally | |||||
{ | |||||
_syncedGuildsLock.Release(); | |||||
} | |||||
} | |||||
//Download offline members | |||||
const short batchSize = 50; | const short batchSize = 50; | ||||
var cachedGuilds = guilds.Select(x => x as CachedGuild).ToArray(); | |||||
if (cachedGuilds.Length == 0) | |||||
return; | |||||
else if (cachedGuilds.Length == 1) | |||||
if (cachedGuilds.Length == 1) | |||||
{ | { | ||||
await cachedGuilds[0].DownloadUsersAsync().ConfigureAwait(false); | |||||
if (!cachedGuilds[0].HasAllMembers) | |||||
await ApiClient.SendRequestMembersAsync(new ulong[] { cachedGuilds[0].Id }).ConfigureAwait(false); | |||||
await cachedGuilds[0].DownloaderPromise.ConfigureAwait(false); | |||||
return; | return; | ||||
} | } | ||||
@@ -502,6 +536,15 @@ namespace Discord | |||||
_currentUser = currentUser; | _currentUser = currentUser; | ||||
_unavailableGuilds = unavailableGuilds; | _unavailableGuilds = unavailableGuilds; | ||||
_lastGuildAvailableTime = Environment.TickCount; | _lastGuildAvailableTime = Environment.TickCount; | ||||
await _syncedGuildsLock.WaitAsync().ConfigureAwait(false); | |||||
try | |||||
{ | |||||
_syncedGuilds = new ConcurrentHashSet<ulong>(); | |||||
} | |||||
finally | |||||
{ | |||||
_syncedGuildsLock.Release(); | |||||
} | |||||
DataStore = dataStore; | DataStore = dataStore; | ||||
_guildDownloadTask = WaitForGuildsAsync(_cancelToken.Token); | _guildDownloadTask = WaitForGuildsAsync(_cancelToken.Token); | ||||
@@ -513,9 +556,11 @@ namespace Discord | |||||
} | } | ||||
break; | break; | ||||
case "RESUMED": | case "RESUMED": | ||||
await _gatewayLogger.DebugAsync("Received Dispatch (RESUMED)").ConfigureAwait(false); | |||||
{ | |||||
await _gatewayLogger.DebugAsync("Received Dispatch (RESUMED)").ConfigureAwait(false); | |||||
await _gatewayLogger.InfoAsync("Resume").ConfigureAwait(false); | |||||
await _gatewayLogger.InfoAsync("Resumed previous session").ConfigureAwait(false); | |||||
} | |||||
return; | return; | ||||
//Guilds | //Guilds | ||||
@@ -579,9 +624,9 @@ namespace Discord | |||||
} | } | ||||
} | } | ||||
break; | break; | ||||
case "GUILD_EMOJI_UPDATE": //TODO: Add | |||||
case "GUILD_EMOJIS_UPDATE": //TODO: Add | |||||
{ | { | ||||
await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_EMOJI_UPDATE)").ConfigureAwait(false); | |||||
await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_EMOJIS_UPDATE)").ConfigureAwait(false); | |||||
var data = (payload as JToken).ToObject<API.Gateway.GuildEmojiUpdateEvent>(_serializer); | var data = (payload as JToken).ToObject<API.Gateway.GuildEmojiUpdateEvent>(_serializer); | ||||
var guild = DataStore.GetGuild(data.GuildId); | var guild = DataStore.GetGuild(data.GuildId); | ||||
@@ -593,13 +638,33 @@ namespace Discord | |||||
} | } | ||||
else | else | ||||
{ | { | ||||
await _gatewayLogger.WarningAsync("GUILD_EMOJI_UPDATE referenced an unknown guild.").ConfigureAwait(false); | |||||
await _gatewayLogger.WarningAsync("GUILD_EMOJIS_UPDATE referenced an unknown guild.").ConfigureAwait(false); | |||||
return; | return; | ||||
} | } | ||||
} | } | ||||
return; | return; | ||||
case "GUILD_INTEGRATIONS_UPDATE": | case "GUILD_INTEGRATIONS_UPDATE": | ||||
await _gatewayLogger.DebugAsync("Ignored Dispatch (GUILD_INTEGRATIONS_UPDATE)").ConfigureAwait(false); | |||||
{ | |||||
await _gatewayLogger.DebugAsync("Ignored Dispatch (GUILD_INTEGRATIONS_UPDATE)").ConfigureAwait(false); | |||||
} | |||||
return; | |||||
case "GUILD_SYNC": | |||||
{ | |||||
await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_SYNC)").ConfigureAwait(false); | |||||
var data = (payload as JToken).ToObject<GuildSyncEvent>(_serializer); | |||||
var guild = DataStore.GetGuild(data.Id); | |||||
if (guild != null) | |||||
{ | |||||
var before = guild.Clone(); | |||||
guild.Update(data, UpdateSource.WebSocket, DataStore); | |||||
await _guildUpdatedEvent.InvokeAsync(before, guild).ConfigureAwait(false); | |||||
} | |||||
else | |||||
{ | |||||
await _gatewayLogger.WarningAsync("GUILD_SYNC referenced an unknown guild.").ConfigureAwait(false); | |||||
return; | |||||
} | |||||
} | |||||
return; | return; | ||||
case "GUILD_DELETE": | case "GUILD_DELETE": | ||||
{ | { | ||||
@@ -10,6 +10,7 @@ using System.Threading.Tasks; | |||||
using ChannelModel = Discord.API.Channel; | using ChannelModel = Discord.API.Channel; | ||||
using EmojiUpdateModel = Discord.API.Gateway.GuildEmojiUpdateEvent; | using EmojiUpdateModel = Discord.API.Gateway.GuildEmojiUpdateEvent; | ||||
using ExtendedModel = Discord.API.Gateway.ExtendedGuild; | using ExtendedModel = Discord.API.Gateway.ExtendedGuild; | ||||
using GuildSyncModel = Discord.API.Gateway.GuildSyncEvent; | |||||
using MemberModel = Discord.API.GuildMember; | using MemberModel = Discord.API.GuildMember; | ||||
using Model = Discord.API.Guild; | using Model = Discord.API.Guild; | ||||
using PresenceModel = Discord.API.Presence; | using PresenceModel = Discord.API.Presence; | ||||
@@ -18,10 +19,16 @@ using VoiceStateModel = Discord.API.VoiceState; | |||||
namespace Discord | namespace Discord | ||||
{ | { | ||||
internal enum MemberDownloadState | |||||
{ | |||||
Incomplete, | |||||
Synced, | |||||
Complete | |||||
} | |||||
internal class CachedGuild : Guild, ICachedEntity<ulong>, IGuild, IUserGuild | internal class CachedGuild : Guild, ICachedEntity<ulong>, IGuild, IUserGuild | ||||
{ | { | ||||
private readonly SemaphoreSlim _audioLock; | private readonly SemaphoreSlim _audioLock; | ||||
private TaskCompletionSource<bool> _downloaderPromise; | |||||
private TaskCompletionSource<bool> _syncPromise, _downloaderPromise; | |||||
private ConcurrentHashSet<ulong> _channels; | private ConcurrentHashSet<ulong> _channels; | ||||
private ConcurrentDictionary<ulong, CachedGuildUser> _members; | private ConcurrentDictionary<ulong, CachedGuildUser> _members; | ||||
private ConcurrentDictionary<ulong, VoiceState> _voiceStates; | private ConcurrentDictionary<ulong, VoiceState> _voiceStates; | ||||
@@ -29,9 +36,11 @@ namespace Discord | |||||
public bool Available { get; private set; } | public bool Available { get; private set; } | ||||
public int MemberCount { get; private set; } | public int MemberCount { get; private set; } | ||||
public int DownloadedMemberCount { get; private set; } | public int DownloadedMemberCount { get; private set; } | ||||
public AudioClient AudioClient { get; private set; } | |||||
public AudioClient AudioClient { get; private set; } | |||||
public MemberDownloadState MemberDownloadState { get; private set; } | |||||
public bool HasAllMembers => _downloaderPromise.Task.IsCompleted; | public bool HasAllMembers => _downloaderPromise.Task.IsCompleted; | ||||
public Task SyncPromise => _syncPromise.Task; | |||||
public Task DownloaderPromise => _downloaderPromise.Task; | public Task DownloaderPromise => _downloaderPromise.Task; | ||||
public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; | public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; | ||||
@@ -51,6 +60,7 @@ namespace Discord | |||||
public CachedGuild(DiscordSocketClient discord, ExtendedModel model, DataStore dataStore) : base(discord, model) | public CachedGuild(DiscordSocketClient discord, ExtendedModel model, DataStore dataStore) : base(discord, model) | ||||
{ | { | ||||
_audioLock = new SemaphoreSlim(1, 1); | _audioLock = new SemaphoreSlim(1, 1); | ||||
_syncPromise = new TaskCompletionSource<bool>(); | |||||
_downloaderPromise = new TaskCompletionSource<bool>(); | _downloaderPromise = new TaskCompletionSource<bool>(); | ||||
Update(model, UpdateSource.Creation, dataStore); | Update(model, UpdateSource.Creation, dataStore); | ||||
} | } | ||||
@@ -91,9 +101,12 @@ namespace Discord | |||||
DownloadedMemberCount = 0; | DownloadedMemberCount = 0; | ||||
for (int i = 0; i < model.Members.Length; i++) | for (int i = 0; i < model.Members.Length; i++) | ||||
AddUser(model.Members[i], dataStore, members); | AddUser(model.Members[i], dataStore, members); | ||||
_downloaderPromise = new TaskCompletionSource<bool>(); | |||||
if (!model.Large) | |||||
_downloaderPromise.SetResult(true); | |||||
if (Discord.ApiClient.AuthTokenType != TokenType.User) | |||||
{ | |||||
_syncPromise.TrySetResult(true); | |||||
if (!model.Large) | |||||
_downloaderPromise.TrySetResult(true); | |||||
} | |||||
for (int i = 0; i < model.Presences.Length; i++) | for (int i = 0; i < model.Presences.Length; i++) | ||||
AddOrUpdateUser(model.Presences[i], dataStore, members); | AddOrUpdateUser(model.Presences[i], dataStore, members); | ||||
@@ -107,6 +120,24 @@ namespace Discord | |||||
} | } | ||||
_voiceStates = voiceStates; | _voiceStates = voiceStates; | ||||
} | } | ||||
public void Update(GuildSyncModel model, UpdateSource source, DataStore dataStore) | |||||
{ | |||||
if (source == UpdateSource.Rest && IsAttached) return; | |||||
var members = new ConcurrentDictionary<ulong, CachedGuildUser>(1, (int)(model.Presences.Length * 1.05)); | |||||
{ | |||||
DownloadedMemberCount = 0; | |||||
for (int i = 0; i < model.Members.Length; i++) | |||||
AddUser(model.Members[i], dataStore, members); | |||||
_syncPromise.TrySetResult(true); | |||||
if (!model.Large) | |||||
_downloaderPromise.TrySetResult(true); | |||||
for (int i = 0; i < model.Presences.Length; i++) | |||||
AddOrUpdateUser(model.Presences[i], dataStore, members); | |||||
} | |||||
_members = members; | |||||
} | |||||
public void Update(EmojiUpdateModel model, UpdateSource source) | public void Update(EmojiUpdateModel model, UpdateSource source) | ||||
{ | { | ||||
@@ -208,9 +239,7 @@ namespace Discord | |||||
} | } | ||||
public override async Task DownloadUsersAsync() | public override async Task DownloadUsersAsync() | ||||
{ | { | ||||
if (!HasAllMembers) | |||||
await Discord.ApiClient.SendRequestMembersAsync(new ulong[] { Id }).ConfigureAwait(false); | |||||
await _downloaderPromise.Task.ConfigureAwait(false); | |||||
await Discord.DownloadUsersAsync(new [] { this }); | |||||
} | } | ||||
public void CompleteDownloadMembers() | public void CompleteDownloadMembers() | ||||
{ | { | ||||