Added support for GUILD_SYNC
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
17
src/Discord.Net/API/Gateway/GuildSyncEvent.cs
Normal file
17
src/Discord.Net/API/Gateway/GuildSyncEvent.cs
Normal file
@@ -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);
|
||||||
@@ -366,15 +371,44 @@ namespace Discord
|
|||||||
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)
|
||||||
{
|
{
|
||||||
const short batchSize = 50;
|
var cachedGuilds = guilds.ToArray();
|
||||||
var cachedGuilds = guilds.Select(x => x as CachedGuild).ToArray();
|
if (cachedGuilds.Length == 0) return;
|
||||||
if (cachedGuilds.Length == 0)
|
|
||||||
return;
|
//Sync guilds
|
||||||
else if (cachedGuilds.Length == 1)
|
if (ApiClient.AuthTokenType == TokenType.User)
|
||||||
{
|
{
|
||||||
await cachedGuilds[0].DownloadUsersAsync().ConfigureAwait(false);
|
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;
|
||||||
|
|
||||||
|
if (cachedGuilds.Length == 1)
|
||||||
|
{
|
||||||
|
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 (Discord.ApiClient.AuthTokenType != TokenType.User)
|
||||||
if (!model.Large)
|
{
|
||||||
_downloaderPromise.SetResult(true);
|
_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.DownloadUsersAsync(new [] { this });
|
||||||
await Discord.ApiClient.SendRequestMembersAsync(new ulong[] { Id }).ConfigureAwait(false);
|
|
||||||
await _downloaderPromise.Task.ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
public void CompleteDownloadMembers()
|
public void CompleteDownloadMembers()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user