From c64bdb83b40f3b03bc173bc960ff7f191c39f52d Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 8 Jun 2016 23:29:50 -0300 Subject: [PATCH] Added support for more events, added benchmark --- src/Discord.Net/API/Common/GuildMember.cs | 4 +- src/Discord.Net/API/Common/Message.cs | 4 +- src/Discord.Net/API/Common/Presence.cs | 2 + src/Discord.Net/API/Common/VoiceState.cs | 4 +- .../API/Gateway/GuildMemberAddEvent.cs | 10 + .../API/Gateway/GuildMemberRemoveEvent.cs | 12 + .../API/Gateway/GuildMemberUpdateEvent.cs | 10 + .../API/Gateway/GuildRoleCreateEvent.cs | 2 +- .../API/Gateway/GuildRoleUpdateEvent.cs | 2 +- src/Discord.Net/DiscordSocketClient.cs | 832 +++++++++--------- src/Discord.Net/Entities/Roles/Role.cs | 2 + src/Discord.Net/Entities/Users/GuildUser.cs | 24 +- src/Discord.Net/Entities/Users/IGuildUser.cs | 4 +- src/Discord.Net/Entities/Users/IVoiceState.cs | 16 + src/Discord.Net/Entities/Users/SelfUser.cs | 2 +- src/Discord.Net/Entities/Users/User.cs | 2 +- .../Entities/WebSocket/CachedDMChannel.cs | 10 +- .../Entities/WebSocket/CachedGuild.cs | 68 +- .../Entities/WebSocket/CachedGuildUser.cs | 17 +- .../Entities/WebSocket/CachedPublicUser.cs | 18 +- .../Entities/WebSocket/CachedSelfUser.cs | 3 +- .../Entities/WebSocket/CachedTextChannel.cs | 14 +- .../WebSocket/ICachedMessageChannel.cs | 6 +- .../Entities/WebSocket/ICachedUser.cs | 7 + .../Entities/WebSocket/IVoiceState.cs.old | 62 -- .../Entities/WebSocket/VoiceState.cs | 42 + src/Discord.Net/project.json | 1 + 27 files changed, 678 insertions(+), 502 deletions(-) create mode 100644 src/Discord.Net/API/Gateway/GuildMemberAddEvent.cs create mode 100644 src/Discord.Net/API/Gateway/GuildMemberRemoveEvent.cs create mode 100644 src/Discord.Net/API/Gateway/GuildMemberUpdateEvent.cs create mode 100644 src/Discord.Net/Entities/Users/IVoiceState.cs create mode 100644 src/Discord.Net/Entities/WebSocket/ICachedUser.cs delete mode 100644 src/Discord.Net/Entities/WebSocket/IVoiceState.cs.old create mode 100644 src/Discord.Net/Entities/WebSocket/VoiceState.cs diff --git a/src/Discord.Net/API/Common/GuildMember.cs b/src/Discord.Net/API/Common/GuildMember.cs index 03da0d5bf..d54f8b3a6 100644 --- a/src/Discord.Net/API/Common/GuildMember.cs +++ b/src/Discord.Net/API/Common/GuildMember.cs @@ -14,8 +14,8 @@ namespace Discord.API [JsonProperty("joined_at")] public DateTime?JoinedAt { get; set; } [JsonProperty("deaf")] - public bool Deaf { get; set; } + public bool? Deaf { get; set; } [JsonProperty("mute")] - public bool Mute { get; set; } + public bool? Mute { get; set; } } } diff --git a/src/Discord.Net/API/Common/Message.cs b/src/Discord.Net/API/Common/Message.cs index ad8fc2bbe..f2ef47be3 100644 --- a/src/Discord.Net/API/Common/Message.cs +++ b/src/Discord.Net/API/Common/Message.cs @@ -27,7 +27,7 @@ namespace Discord.API public Attachment[] Attachments { get; set; } [JsonProperty("embeds")] public Embed[] Embeds { get; set; } - [JsonProperty("nonce")] - public uint? Nonce { get; set; } + /*[JsonProperty("nonce")] + public object Nonce { get; set; }*/ } } diff --git a/src/Discord.Net/API/Common/Presence.cs b/src/Discord.Net/API/Common/Presence.cs index 5f2b853e6..ce4edfb0f 100644 --- a/src/Discord.Net/API/Common/Presence.cs +++ b/src/Discord.Net/API/Common/Presence.cs @@ -6,6 +6,8 @@ namespace Discord.API { [JsonProperty("user")] public User User { get; set; } + [JsonProperty("guild_id")] + public ulong? GuildId { get; set; } [JsonProperty("status")] public UserStatus Status { get; set; } [JsonProperty("game")] diff --git a/src/Discord.Net/API/Common/VoiceState.cs b/src/Discord.Net/API/Common/VoiceState.cs index e848ea919..49a7409d0 100644 --- a/src/Discord.Net/API/Common/VoiceState.cs +++ b/src/Discord.Net/API/Common/VoiceState.cs @@ -4,8 +4,10 @@ namespace Discord.API { public class VoiceState { + [JsonProperty("guild_id")] + public ulong? GuildId { get; set; } [JsonProperty("channel_id")] - public ulong ChannelId { get; set; } + public ulong? ChannelId { get; set; } [JsonProperty("user_id")] public ulong UserId { get; set; } [JsonProperty("session_id")] diff --git a/src/Discord.Net/API/Gateway/GuildMemberAddEvent.cs b/src/Discord.Net/API/Gateway/GuildMemberAddEvent.cs new file mode 100644 index 000000000..3676439d4 --- /dev/null +++ b/src/Discord.Net/API/Gateway/GuildMemberAddEvent.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class GuildMemberAddEvent : GuildMember + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + } +} diff --git a/src/Discord.Net/API/Gateway/GuildMemberRemoveEvent.cs b/src/Discord.Net/API/Gateway/GuildMemberRemoveEvent.cs new file mode 100644 index 000000000..2916c5a91 --- /dev/null +++ b/src/Discord.Net/API/Gateway/GuildMemberRemoveEvent.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class GuildMemberRemoveEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("user")] + public User User { get; set; } + } +} diff --git a/src/Discord.Net/API/Gateway/GuildMemberUpdateEvent.cs b/src/Discord.Net/API/Gateway/GuildMemberUpdateEvent.cs new file mode 100644 index 000000000..8221b1199 --- /dev/null +++ b/src/Discord.Net/API/Gateway/GuildMemberUpdateEvent.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class GuildMemberUpdateEvent : GuildMember + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + } +} diff --git a/src/Discord.Net/API/Gateway/GuildRoleCreateEvent.cs b/src/Discord.Net/API/Gateway/GuildRoleCreateEvent.cs index f05543bf6..5753a638b 100644 --- a/src/Discord.Net/API/Gateway/GuildRoleCreateEvent.cs +++ b/src/Discord.Net/API/Gateway/GuildRoleCreateEvent.cs @@ -7,6 +7,6 @@ namespace Discord.API.Gateway [JsonProperty("guild_id")] public ulong GuildId { get; set; } [JsonProperty("role")] - public Role Data { get; set; } + public Role Role { get; set; } } } diff --git a/src/Discord.Net/API/Gateway/GuildRoleUpdateEvent.cs b/src/Discord.Net/API/Gateway/GuildRoleUpdateEvent.cs index 345154432..9e88b5de8 100644 --- a/src/Discord.Net/API/Gateway/GuildRoleUpdateEvent.cs +++ b/src/Discord.Net/API/Gateway/GuildRoleUpdateEvent.cs @@ -7,6 +7,6 @@ namespace Discord.API.Gateway [JsonProperty("guild_id")] public ulong GuildId { get; set; } [JsonProperty("role")] - public Role Data { get; set; } + public Role Role { get; set; } } } diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 177a44694..449817b1c 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -41,6 +41,9 @@ namespace Discord private readonly ConcurrentQueue _largeGuilds; private readonly Logger _gatewayLogger; +#if BENCHMARK + private readonly Logger _benchmarkLogger; +#endif private readonly DataStoreProvider _dataStoreProvider; private readonly JsonSerializer _serializer; private readonly int _connectionTimeout, _reconnectDelay, _failedReconnectDelay; @@ -106,7 +109,10 @@ namespace Discord _largeThreshold = config.LargeThreshold; _gatewayLogger = _log.CreateLogger("Gateway"); - +#if BENCHMARK + _benchmarkLogger = _log.CreateLogger("Benchmark"); +#endif + _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; ApiClient.SentGatewayMessage += async opCode => await _gatewayLogger.Debug($"Sent {(GatewayOpCode)opCode}"); @@ -207,7 +213,7 @@ namespace Discord { dataStore = dataStore ?? DataStore; - var guild = new CachedGuild(this, model); + var guild = new CachedGuild(this, model, dataStore); if (model.Unavailable != true) { for (int i = 0; i < model.Channels.Length; i++) @@ -247,7 +253,7 @@ namespace Discord { dataStore = dataStore ?? DataStore; - var recipient = AddCachedUser(model.Recipient, dataStore); + var recipient = GetOrAddCachedUser(model.Recipient, dataStore); var channel = recipient.AddDMChannel(model); dataStore.AddChannel(channel); return channel; @@ -287,7 +293,7 @@ namespace Discord { return Task.FromResult(DataStore.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault()); } - internal CachedPublicUser AddCachedUser(API.User model, DataStore dataStore = null) + internal CachedPublicUser GetOrAddCachedUser(API.User model, DataStore dataStore = null) { dataStore = dataStore ?? DataStore; @@ -299,8 +305,7 @@ namespace Discord { dataStore = dataStore ?? DataStore; - var user = dataStore.GetUser(id); - user.RemoveRef(); + var user = dataStore.RemoveUser(id); return user; } @@ -336,7 +341,7 @@ namespace Discord batchTasks[j] = guild.DownloaderPromise; } - ApiClient.SendRequestMembers(batchIds); + await ApiClient.SendRequestMembers(batchIds).ConfigureAwait(false); if (isLast && batchCount > 1) await Task.WhenAll(batchTasks.Take(count)).ConfigureAwait(false); @@ -347,474 +352,511 @@ namespace Discord private async Task ProcessMessage(GatewayOpCode opCode, int? seq, string type, object payload) { - if (seq != null) - _lastSeq = seq.Value; +#if BENCHMARK + Stopwatch stopwatch = Stopwatch.StartNew(); try { - switch (opCode) +#endif + if (seq != null) + _lastSeq = seq.Value; + try { - case GatewayOpCode.Hello: - { - var data = (payload as JToken).ToObject(_serializer); - - await ApiClient.SendIdentify().ConfigureAwait(false); - _heartbeatTask = RunHeartbeat(data.HeartbeatInterval, _heartbeatCancelToken.Token); - } - break; - case GatewayOpCode.HeartbeatAck: - { - var latency = (int)(Environment.TickCount - _heartbeatTime); - await _gatewayLogger.Debug($"Latency: {latency} ms").ConfigureAwait(false); - Latency = latency; - - await LatencyUpdated.Raise(latency).ConfigureAwait(false); - } - break; - case GatewayOpCode.Dispatch: - switch (type) - { - //Global - case "READY": - { - //TODO: Make downloading large guilds optional - var data = (payload as JToken).ToObject(_serializer); - var dataStore = _dataStoreProvider(ShardId, _totalShards, data.Guilds.Length, data.PrivateChannels.Length); - - _currentUser = new CachedSelfUser(this, data.User); - - for (int i = 0; i < data.Guilds.Length; i++) - AddCachedGuild(data.Guilds[i], dataStore); - for (int i = 0; i < data.PrivateChannels.Length; i++) - AddCachedDMChannel(data.PrivateChannels[i], dataStore); - - _sessionId = data.SessionId; - DataStore = dataStore; - - await Ready.Raise().ConfigureAwait(false); - - _connectTask.TrySetResult(true); //Signal the .Connect() call to complete - } - break; - - //Guilds - case "GUILD_CREATE": - { - var data = (payload as JToken).ToObject(_serializer); - var guild = new CachedGuild(this, data); - DataStore.AddGuild(guild); - - if (data.Unavailable == false) - type = "GUILD_AVAILABLE"; - else - await JoinedGuild.Raise(guild).ConfigureAwait(false); - - await GuildAvailable.Raise(guild); - } - break; - case "GUILD_UPDATE": - { - var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.Id); - if (guild != null) + switch (opCode) + { + case GatewayOpCode.Hello: + { + await _gatewayLogger.Debug($"Received Hello").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); + + await ApiClient.SendIdentify().ConfigureAwait(false); + _heartbeatTask = RunHeartbeat(data.HeartbeatInterval, _heartbeatCancelToken.Token); + } + break; + case GatewayOpCode.HeartbeatAck: + { + await _gatewayLogger.Debug($"Received HeartbeatAck").ConfigureAwait(false); + + var latency = (int)(Environment.TickCount - _heartbeatTime); + await _gatewayLogger.Debug($"Latency = {latency} ms").ConfigureAwait(false); + Latency = latency; + + await LatencyUpdated.Raise(latency).ConfigureAwait(false); + } + break; + case GatewayOpCode.Dispatch: + switch (type) + { + //Global + case "READY": { - var before = _enablePreUpdateEvents ? guild.Clone() : null; - guild.Update(data, UpdateSource.WebSocket); - await GuildUpdated.Raise(before, guild); + await _gatewayLogger.Debug($"Received Dispatch (READY)").ConfigureAwait(false); + + //TODO: Make downloading large guilds optional + var data = (payload as JToken).ToObject(_serializer); + var dataStore = _dataStoreProvider(ShardId, _totalShards, data.Guilds.Length, data.PrivateChannels.Length); + + _currentUser = new CachedSelfUser(this, data.User); + + for (int i = 0; i < data.Guilds.Length; i++) + AddCachedGuild(data.Guilds[i], dataStore); + for (int i = 0; i < data.PrivateChannels.Length; i++) + AddCachedDMChannel(data.PrivateChannels[i], dataStore); + + _sessionId = data.SessionId; + DataStore = dataStore; + + await Ready.Raise().ConfigureAwait(false); + + _connectTask.TrySetResult(true); //Signal the .Connect() call to complete } - else - await _gatewayLogger.Warning("GUILD_UPDATE referenced an unknown guild."); - } - break; - case "GUILD_DELETE": - { - var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.RemoveGuild(data.Id); - if (guild != null) + break; + + //Guilds + case "GUILD_CREATE": { - if (data.Unavailable == true) - type = "GUILD_UNAVAILABLE"; + var data = (payload as JToken).ToObject(_serializer); + var guild = new CachedGuild(this, data, DataStore); + DataStore.AddGuild(guild); + + if (data.Unavailable == false) + type = "GUILD_AVAILABLE"; + await _gatewayLogger.Debug($"Received Dispatch ({type})").ConfigureAwait(false); - await GuildUnavailable.Raise(guild); - if (data.Unavailable != true) - await LeftGuild.Raise(guild); + if (data.Unavailable != false) + await JoinedGuild.Raise(guild).ConfigureAwait(false); + + await GuildAvailable.Raise(guild).ConfigureAwait(false); } - else - await _gatewayLogger.Warning("GUILD_DELETE referenced an unknown guild."); - } - break; - - //Channels - case "CHANNEL_CREATE": - { - var data = (payload as JToken).ToObject(_serializer); - - ICachedChannel channel = null; - if (data.GuildId != null) + break; + case "GUILD_UPDATE": { - var guild = DataStore.GetGuild(data.GuildId.Value); + await _gatewayLogger.Debug($"Received Dispatch (GUILD_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.Id); if (guild != null) { - channel = guild.AddCachedChannel(data); - DataStore.AddChannel(channel); + var before = _enablePreUpdateEvents ? guild.Clone() : null; + guild.Update(data, UpdateSource.WebSocket); + await GuildUpdated.Raise(before, guild).ConfigureAwait(false); } else - await _gatewayLogger.Warning("CHANNEL_CREATE referenced an unknown guild."); + await _gatewayLogger.Warning("GUILD_UPDATE referenced an unknown guild."); } - else - channel = AddCachedDMChannel(data); - if (channel != null) - await ChannelCreated.Raise(channel); - } - break; - case "CHANNEL_UPDATE": - { - var data = (payload as JToken).ToObject(_serializer); - var channel = DataStore.GetChannel(data.Id); - if (channel != null) + break; + case "GUILD_DELETE": { - var before = _enablePreUpdateEvents ? channel.Clone() : null; - channel.Update(data, UpdateSource.WebSocket); - await ChannelUpdated.Raise(before, channel); + var data = (payload as JToken).ToObject(_serializer); + if (data.Unavailable == true) + type = "GUILD_UNAVAILABLE"; + await _gatewayLogger.Debug($"Received Dispatch ({type})").ConfigureAwait(false); + + var guild = DataStore.RemoveGuild(data.Id); + if (guild != null) + { + await GuildUnavailable.Raise(guild).ConfigureAwait(false); + if (data.Unavailable != true) + await LeftGuild.Raise(guild).ConfigureAwait(false); + foreach (var member in guild.Members) + member.User.RemoveRef(); + } + else + await _gatewayLogger.Warning($"{type} referenced an unknown guild.").ConfigureAwait(false); } - else - await _gatewayLogger.Warning("CHANNEL_UPDATE referenced an unknown channel."); - } - break; - case "CHANNEL_DELETE": - { - var data = (payload as JToken).ToObject(_serializer); - var channel = RemoveCachedChannel(data.Id); - if (channel != null) - await ChannelDestroyed.Raise(channel); - else - await _gatewayLogger.Warning("CHANNEL_DELETE referenced an unknown channel."); - } - break; - - //Members - /*case "GUILD_MEMBER_ADD": - { - var data = (payload as JToken).ToObject(_serializer); - var guild = GetGuild(data.GuildId.Value); - if (guild != null) + break; + + //Channels + case "CHANNEL_CREATE": { - var user = guild.AddCachedUser(data.User.Id, true, true); - user.Update(data); - user.UpdateActivity(); - UserJoined.Raise(user); + await _gatewayLogger.Debug($"Received Dispatch (CHANNEL_CREATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + ICachedChannel channel = null; + if (data.GuildId != null) + { + var guild = DataStore.GetGuild(data.GuildId.Value); + if (guild != null) + { + channel = guild.AddCachedChannel(data); + DataStore.AddChannel(channel); + } + else + await _gatewayLogger.Warning("CHANNEL_CREATE referenced an unknown guild."); + } + else + channel = AddCachedDMChannel(data); + if (channel != null) + await ChannelCreated.Raise(channel); } - else - await _gatewayLogger.Warning("GUILD_MEMBER_ADD referenced an unknown guild."); - } - break; - case "GUILD_MEMBER_UPDATE": - { - var data = (payload as JToken).ToObject(_serializer); - var guild = GetGuild(data.GuildId.Value); - if (guild != null) + break; + case "CHANNEL_UPDATE": { - var user = guild.GetCachedUser(data.User.Id); - if (user != null) + await _gatewayLogger.Debug($"Received Dispatch (CHANNEL_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = DataStore.GetChannel(data.Id); + if (channel != null) { - var before = _enablePreUpdateEvents ? user.Clone() : null; - user.Update(data); - await UserUpdated.Raise(before, user); + var before = _enablePreUpdateEvents ? channel.Clone() : null; + channel.Update(data, UpdateSource.WebSocket); + await ChannelUpdated.Raise(before, channel); } else - await _gatewayLogger.Warning("GUILD_MEMBER_UPDATE referenced an unknown user."); + await _gatewayLogger.Warning("CHANNEL_UPDATE referenced an unknown channel."); + } + break; + case "CHANNEL_DELETE": + { + await _gatewayLogger.Debug($"Received Dispatch (CHANNEL_DELETE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = RemoveCachedChannel(data.Id); + if (channel != null) + await ChannelDestroyed.Raise(channel); + else + await _gatewayLogger.Warning("CHANNEL_DELETE referenced an unknown channel."); } - else - await _gatewayLogger.Warning("GUILD_MEMBER_UPDATE referenced an unknown guild."); - } - break; - case "GUILD_MEMBER_REMOVE": - { - var data = (payload as JToken).ToObject(_serializer); - var guild = GetGuild(data.GuildId.Value); - if (guild != null) + break; + + //Members + case "GUILD_MEMBER_ADD": { - var user = guild.RemoveCachedUser(data.User.Id); - if (user != null) + await _gatewayLogger.Debug($"Received Dispatch (GUILD_MEMBER_ADD)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId); + if (guild != null) { - user.GlobalUser.RemoveGuild(); - if (user.GuildCount == 0 && user.DMChannel == null) - DataStore.RemoveUser(user.Id); - await UserLeft.Raise(user); + var user = guild.AddCachedUser(data); + await UserJoined.Raise(user).ConfigureAwait(false); } else - await _gatewayLogger.Warning("GUILD_MEMBER_REMOVE referenced an unknown user."); + await _gatewayLogger.Warning("GUILD_MEMBER_ADD referenced an unknown guild."); } - else - await _gatewayLogger.Warning("GUILD_MEMBER_REMOVE referenced an unknown guild."); - } - break; - case "GUILD_MEMBERS_CHUNK": - { - var data = (payload as JToken).ToObject(_serializer); - var guild = GetCachedGuild(data.GuildId); - if (guild != null) + break; + case "GUILD_MEMBER_UPDATE": { - foreach (var memberData in data.Members) + await _gatewayLogger.Debug($"Received Dispatch (GUILD_MEMBER_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId); + if (guild != null) { - var user = guild.AddCachedUser(memberData.User.Id, true, false); - user.Update(memberData); + var user = guild.GetCachedUser(data.User.Id); + if (user != null) + { + var before = _enablePreUpdateEvents ? user.Clone() : null; + user.Update(data, UpdateSource.WebSocket); + await UserUpdated.Raise(before, user); + } + else + await _gatewayLogger.Warning("GUILD_MEMBER_UPDATE referenced an unknown user."); } - - if (guild.CurrentUserCount >= guild.UserCount) //Finished downloading for there - await GuildAvailable.Raise(guild); + else + await _gatewayLogger.Warning("GUILD_MEMBER_UPDATE referenced an unknown guild."); } - else - await _gatewayLogger.Warning("GUILD_MEMBERS_CHUNK referenced an unknown guild."); - } - break; - - //Roles - /*case "GUILD_ROLE_CREATE": - { - var data = (payload as JToken).ToObject(_serializer); - var guild = GetCachedGuild(data.GuildId); - if (guild != null) + break; + case "GUILD_MEMBER_REMOVE": { - var role = guild.AddCachedRole(data.Data.Id); - role.Update(data.Data, false); - RoleCreated.Raise(role); + await _gatewayLogger.Debug($"Received Dispatch (GUILD_MEMBER_REMOVE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId); + if (guild != null) + { + var user = guild.RemoveCachedUser(data.User.Id); + if (user != null) + { + user.User.RemoveRef(); + await UserLeft.Raise(user); + } + else + await _gatewayLogger.Warning("GUILD_MEMBER_REMOVE referenced an unknown user."); + } + else + await _gatewayLogger.Warning("GUILD_MEMBER_REMOVE referenced an unknown guild."); } - else - await _gatewayLogger.Warning("GUILD_ROLE_CREATE referenced an unknown guild."); - } - break; - case "GUILD_ROLE_UPDATE": - { - var data = (payload as JToken).ToObject(_serializer); - var guild = GetCachedGuild(data.GuildId); - if (guild != null) + break; + case "GUILD_MEMBERS_CHUNK": { - var role = guild.GetRole(data.Data.Id); - if (role != null) + await _gatewayLogger.Debug($"Received Dispatch (GUILD_MEMBERS_CHUNK)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId); + if (guild != null) { - var before = _enablePreUpdateEvents ? role.Clone() : null; - role.Update(data.Data, true); - RoleUpdated.Raise(before, role); + foreach (var memberModel in data.Members) + guild.AddCachedUser(memberModel); + + if (guild.DownloadedMemberCount >= guild.MemberCount) //Finished downloading for there + { + guild.CompleteDownloadMembers(); + await GuildDownloadedMembers.Raise(guild).ConfigureAwait(false); + } } else - await _gatewayLogger.Warning("GUILD_ROLE_UPDATE referenced an unknown role."); + await _gatewayLogger.Warning("GUILD_MEMBERS_CHUNK referenced an unknown guild."); } - else - await _gatewayLogger.Warning("GUILD_ROLE_UPDATE referenced an unknown guild."); - } - break; - case "GUILD_ROLE_DELETE": - { - var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); - if (guild != null) + break; + + //Roles + case "GUILD_ROLE_CREATE": { - var role = guild.RemoveRole(data.RoleId); - if (role != null) - RoleDeleted.Raise(role); + await _gatewayLogger.Debug($"Received Dispatch (GUILD_ROLE_CREATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId); + if (guild != null) + { + var role = guild.AddCachedRole(data.Role); + await RoleCreated.Raise(role).ConfigureAwait(false); + } else - await _gatewayLogger.Warning("GUILD_ROLE_DELETE referenced an unknown role."); + await _gatewayLogger.Warning("GUILD_ROLE_CREATE referenced an unknown guild."); } - else - await _gatewayLogger.Warning("GUILD_ROLE_DELETE referenced an unknown guild."); - } - break; - - //Bans - case "GUILD_BAN_ADD": - { - var data = (payload as JToken).ToObject(_serializer); - var guild = GetCachedGuild(data.GuildId); - if (guild != null) - await UserBanned.Raise(new User(this, data)); - else - await _gatewayLogger.Warning("GUILD_BAN_ADD referenced an unknown guild."); - } - break; - case "GUILD_BAN_REMOVE": - { - var data = payload.ToObject(_serializer); - var guild = GetCachedGuild(data.GuildId); - if (guild != null) - await UserUnbanned.Raise(new User(this, data)); - else - await _gatewayLogger.Warning("GUILD_BAN_REMOVE referenced an unknown guild."); - } - break; - - //Messages - case "MESSAGE_CREATE": - { - var data = (payload as JToken).ToObject(_serializer); - var channel = DataStore.GetChannel(data.ChannelId); - if (channel != null) + break; + case "GUILD_ROLE_UPDATE": { - var user = channel.GetUser(data.Author.Id); + await _gatewayLogger.Debug($"Received Dispatch (GUILD_ROLE_UPDATE)").ConfigureAwait(false); - if (user != null) + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId); + if (guild != null) { - bool isAuthor = data.Author.Id == CurrentUser.Id; - var msg = channel.AddMessage(data.Id, user, data.Timestamp.Value); - - msg.Update(data); - - MessageReceived.Raise(msg); + var role = guild.GetRole(data.Role.Id); + if (role != null) + { + var before = _enablePreUpdateEvents ? role.Clone() : null; + role.Update(data.Role, UpdateSource.WebSocket); + await RoleUpdated.Raise(before, role).ConfigureAwait(false); + } + else + await _gatewayLogger.Warning("GUILD_ROLE_UPDATE referenced an unknown role."); } else - await _gatewayLogger.Warning("MESSAGE_CREATE referenced an unknown user."); + await _gatewayLogger.Warning("GUILD_ROLE_UPDATE referenced an unknown guild."); } - else - await _gatewayLogger.Warning("MESSAGE_CREATE referenced an unknown channel."); - } - break; - case "MESSAGE_UPDATE": - { - var data = (payload as JToken).ToObject(_serializer); - var channel = GetCachedChannel(data.ChannelId); - if (channel != null) + break; + case "GUILD_ROLE_DELETE": { - var msg = channel.GetMessage(data.Id, data.Author?.Id); - var before = _enablePreUpdateEvents ? msg.Clone() : null; - msg.Update(data); - MessageUpdated.Raise(before, msg); + await _gatewayLogger.Debug($"Received Dispatch (GUILD_ROLE_DELETE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId); + if (guild != null) + { + var role = guild.RemoveCachedRole(data.RoleId); + if (role != null) + await RoleDeleted.Raise(role).ConfigureAwait(false); + else + await _gatewayLogger.Warning("GUILD_ROLE_DELETE referenced an unknown role."); + } + else + await _gatewayLogger.Warning("GUILD_ROLE_DELETE referenced an unknown guild."); } - else - await _gatewayLogger.Warning("MESSAGE_UPDATE referenced an unknown channel."); - } - break; - case "MESSAGE_DELETE": - { - var data = (payload as JToken).ToObject(_serializer); - var channel = GetCachedChannel(data.ChannelId); - if (channel != null) + break; + + //Bans + case "GUILD_BAN_ADD": { - var msg = channel.RemoveMessage(data.Id); - MessageDeleted.Raise(msg); + await _gatewayLogger.Debug($"Received Dispatch (GUILD_BAN_ADD)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId); + if (guild != null) + await UserBanned.Raise(new User(this, data)); + else + await _gatewayLogger.Warning("GUILD_BAN_ADD referenced an unknown guild."); } - else - await _gatewayLogger.Warning("MESSAGE_DELETE referenced an unknown channel."); - } - break; - - //Statuses - case "PRESENCE_UPDATE": - { - var data = (payload as JToken).ToObject(_serializer); - User user; - Guild guild; - if (data.GuildId == null) + break; + case "GUILD_BAN_REMOVE": { - guild = null; - user = GetPrivateChannel(data.User.Id)?.Recipient; + await _gatewayLogger.Debug($"Received Dispatch (GUILD_BAN_REMOVE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId); + if (guild != null) + await UserUnbanned.Raise(new User(this, data)); + else + await _gatewayLogger.Warning("GUILD_BAN_REMOVE referenced an unknown guild."); } - else + break; + + //Messages + case "MESSAGE_CREATE": { - guild = GetGuild(data.GuildId.Value); - if (guild == null) + await _gatewayLogger.Debug($"Received Dispatch (MESSAGE_CREATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; + if (channel != null) { - await _gatewayLogger.Warning("PRESENCE_UPDATE referenced an unknown guild."); - break; + var author = channel.GetCachedUser(data.Author.Id); + + if (author != null) + { + var msg = channel.AddCachedMessage(author, data); + await MessageReceived.Raise(msg).ConfigureAwait(false); + } + else + await _gatewayLogger.Warning("MESSAGE_CREATE referenced an unknown user."); } else - user = guild.GetUser(data.User.Id); + await _gatewayLogger.Warning("MESSAGE_CREATE referenced an unknown channel."); } - - if (user != null) + break; + case "MESSAGE_UPDATE": { - var before = _enablePreUpdateEvents ? user.Clone() : null; - user.Update(data); - UserUpdated.Raise(before, user); + await _gatewayLogger.Debug($"Received Dispatch (MESSAGE_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; + if (channel != null) + { + var msg = channel.GetCachedMessage(data.Id); + var before = _enablePreUpdateEvents ? msg.Clone() : null; + msg.Update(data, UpdateSource.WebSocket); + await MessageUpdated.Raise(before, msg).ConfigureAwait(false); + } + else + await _gatewayLogger.Warning("MESSAGE_UPDATE referenced an unknown channel."); } - else + break; + case "MESSAGE_DELETE": { - //Occurs when a user leaves a guild - //await _gatewayLogger.Warning("PRESENCE_UPDATE referenced an unknown user."); + await _gatewayLogger.Debug($"Received Dispatch (MESSAGE_DELETE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; + if (channel != null) + { + var msg = channel.RemoveCachedMessage(data.Id); + await MessageDeleted.Raise(msg).ConfigureAwait(false); + } + else + await _gatewayLogger.Warning("MESSAGE_DELETE referenced an unknown channel."); } - } - break; - case "TYPING_START": - { - var data = (payload as JToken).ToObject(_serializer); - var channel = GetCachedChannel(data.ChannelId); - if (channel != null) + break; + + //Statuses + case "PRESENCE_UPDATE": { - var user = channel.GetUser(data.UserId); - if (user != null) + await _gatewayLogger.Debug($"Received Dispatch (PRESENCE_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + if (data.GuildId == null) + { + var user = DataStore.GetUser(data.User.Id); + if (user == null) + user.Update(data, UpdateSource.WebSocket); + } + else { - await UserIsTyping.Raise(channel, user); - user.UpdateActivity(); + var guild = DataStore.GetGuild(data.GuildId.Value); + if (guild == null) + { + await _gatewayLogger.Warning("PRESENCE_UPDATE referenced an unknown guild."); + break; + } + if (data.Status == UserStatus.Offline) + guild.RemoveCachedPresence(data.User.Id); + else + guild.AddOrUpdateCachedPresence(data); } } - else - await _gatewayLogger.Warning("TYPING_START referenced an unknown channel."); - } - break; - - //Voice - case "VOICE_STATE_UPDATE": - { - var data = (payload as JToken).ToObject(_serializer); - var guild = GetGuild(data.GuildId); - if (guild != null) + break; + case "TYPING_START": { - var user = guild.GetUser(data.UserId); - if (user != null) + await _gatewayLogger.Debug($"Received Dispatch (TYPING_START)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; + if (channel != null) { - var before = _enablePreUpdateEvents ? user.Clone() : null; - user.Update(data); - UserUpdated.Raise(before, user); + var user = channel.GetCachedUser(data.UserId); + if (user != null) + await UserIsTyping.Raise(channel, user).ConfigureAwait(false); } else + await _gatewayLogger.Warning("TYPING_START referenced an unknown channel.").ConfigureAwait(false); + } + break; + + //Voice + case "VOICE_STATE_UPDATE": + { + await _gatewayLogger.Debug($"Received Dispatch (VOICE_STATE_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + if (data.GuildId.HasValue) { - //Occurs when a user leaves a guild - //await _gatewayLogger.Warning("VOICE_STATE_UPDATE referenced an unknown user."); + var guild = DataStore.GetGuild(data.GuildId.Value); + if (guild != null) + { + if (data.ChannelId == null) + guild.RemoveCachedVoiceState(data.UserId); + else + guild.AddOrUpdateCachedVoiceState(data); + + var user = guild.GetCachedUser(data.UserId); + user.Update(data, UpdateSource.WebSocket); + } + else + await _gatewayLogger.Warning("VOICE_STATE_UPDATE referenced an unknown guild.").ConfigureAwait(false); } } - else - await _gatewayLogger.Warning("VOICE_STATE_UPDATE referenced an unknown guild."); - } - break; - - //Settings - case "USER_UPDATE": - { - var data = (payload as JToken).ToObject(_serializer); - if (data.Id == CurrentUser.Id) + break; + + //Settings + case "USER_UPDATE": { - var before = _enablePreUpdateEvents ? CurrentUser.Clone() : null; - CurrentUser.Update(data); - await CurrentUserUpdated.Raise(before, CurrentUser).ConfigureAwait(false); + await _gatewayLogger.Debug($"Received Dispatch (USER_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + if (data.Id == CurrentUser.Id) + { + var before = _enablePreUpdateEvents ? CurrentUser.Clone() : null; + CurrentUser.Update(data, UpdateSource.WebSocket); + await CurrentUserUpdated.Raise(before, CurrentUser).ConfigureAwait(false); + } } - } - break;*/ - - //Ignored - case "USER_SETTINGS_UPDATE": - case "MESSAGE_ACK": //TODO: Add (User only) - case "GUILD_EMOJIS_UPDATE": //TODO: Add - case "GUILD_INTEGRATIONS_UPDATE": //TODO: Add - case "VOICE_SERVER_UPDATE": //TODO: Add - case "RESUMED": //TODO: Add - await _gatewayLogger.Debug($"Ignored Dispatch ({type})").ConfigureAwait(false); - return; - - //Others - default: - await _gatewayLogger.Warning($"Unknown Dispatch ({type})").ConfigureAwait(false); - return; - } - break; - default: - await _gatewayLogger.Warning($"Unknown OpCode ({opCode})").ConfigureAwait(false); - return; + break; + + //Ignored + case "USER_SETTINGS_UPDATE": + case "MESSAGE_ACK": //TODO: Add (User only) + case "GUILD_EMOJIS_UPDATE": //TODO: Add + case "GUILD_INTEGRATIONS_UPDATE": //TODO: Add + case "VOICE_SERVER_UPDATE": //TODO: Add + case "RESUMED": //TODO: Add + await _gatewayLogger.Debug($"Ignored Dispatch ({type})").ConfigureAwait(false); + return; + + //Others + default: + await _gatewayLogger.Warning($"Unknown Dispatch ({type})").ConfigureAwait(false); + return; + } + break; + default: + await _gatewayLogger.Warning($"Unknown OpCode ({opCode})").ConfigureAwait(false); + return; + } } + catch (Exception ex) + { + await _gatewayLogger.Error($"Error handling {opCode}{(type != null ? $" ({type})" : "")}", ex).ConfigureAwait(false); + return; + } +#if BENCHMARK } - catch (Exception ex) + finally { - await _gatewayLogger.Error($"Error handling {opCode}{(type != null ? $" ({type})" : "")}", ex).ConfigureAwait(false); - return; + stopwatch.Stop(); + double millis = Math.Round(stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); + await _benchmarkLogger.Debug($"{millis} ms").ConfigureAwait(false); } - await _gatewayLogger.Debug($"Received {opCode}{(type != null ? $" ({type})" : "")}").ConfigureAwait(false); +#endif } private async Task RunHeartbeat(int intervalMillis, CancellationToken cancelToken) { diff --git a/src/Discord.Net/Entities/Roles/Role.cs b/src/Discord.Net/Entities/Roles/Role.cs index 578930b42..577a4c252 100644 --- a/src/Discord.Net/Entities/Roles/Role.cs +++ b/src/Discord.Net/Entities/Roles/Role.cs @@ -57,6 +57,8 @@ namespace Discord { await Discord.ApiClient.DeleteGuildRole(Guild.Id, Id).ConfigureAwait(false); } + + public Role Clone() => MemberwiseClone() as Role; public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id})"; diff --git a/src/Discord.Net/Entities/Users/GuildUser.cs b/src/Discord.Net/Entities/Users/GuildUser.cs index 7c72de216..dd879dd20 100644 --- a/src/Discord.Net/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Entities/Users/GuildUser.cs @@ -5,6 +5,7 @@ using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; using Model = Discord.API.GuildMember; +using VoiceStateModel = Discord.API.VoiceState; namespace Discord { @@ -24,12 +25,12 @@ namespace Discord public string AvatarUrl => User.AvatarUrl; public DateTime CreatedAt => User.CreatedAt; public string Discriminator => User.Discriminator; - public Game? Game => User.Game; public bool IsAttached => User.IsAttached; public bool IsBot => User.IsBot; public string Mention => User.Mention; - public UserStatus Status => User.Status; public string Username => User.Username; + public virtual UserStatus Status => User.Status; + public virtual Game? Game => User.Game; public DiscordClient Discord => Guild.Discord; @@ -43,8 +44,10 @@ namespace Discord { if (source == UpdateSource.Rest && IsAttached) return; - IsDeaf = model.Deaf; - IsMute = model.Mute; + if (model.Deaf.HasValue) + IsDeaf = model.Deaf.Value; + if (model.Mute.HasValue) + IsMute = model.Mute.Value; JoinedAt = model.JoinedAt.Value; Nickname = model.Nick; @@ -56,6 +59,13 @@ namespace Discord GuildPermissions = new GuildPermissions(Permissions.ResolveGuild(this)); } + public void Update(VoiceStateModel model, UpdateSource source) + { + if (source == UpdateSource.Rest && IsAttached) return; + + IsDeaf = model.Deaf; + IsMute = model.Mute; + } public async Task Update() { @@ -107,6 +117,10 @@ namespace Discord IGuild IGuildUser.Guild => Guild; IReadOnlyCollection IGuildUser.Roles => Roles; - IVoiceChannel IGuildUser.VoiceChannel => null; + bool IVoiceState.IsSelfDeafened => false; + bool IVoiceState.IsSelfMuted => false; + bool IVoiceState.IsSuppressed => false; + IVoiceChannel IVoiceState.VoiceChannel => null; + string IVoiceState.VoiceSessionId => null; } } diff --git a/src/Discord.Net/Entities/Users/IGuildUser.cs b/src/Discord.Net/Entities/Users/IGuildUser.cs index 63b7c12fb..5b68af6ca 100644 --- a/src/Discord.Net/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net/Entities/Users/IGuildUser.cs @@ -6,7 +6,7 @@ using Discord.API.Rest; namespace Discord { /// A Guild-User pairing. - public interface IGuildUser : IUpdateable, IUser + public interface IGuildUser : IUpdateable, IUser, IVoiceState { /// Returns true if the guild has deafened this user. bool IsDeaf { get; } @@ -23,8 +23,6 @@ namespace Discord IGuild Guild { get; } /// Returns a collection of the roles this user is a member of in this guild, including the guild's @everyone role. IReadOnlyCollection Roles { get; } - /// Gets the voice channel this user is currently in, if any. - IVoiceChannel VoiceChannel { get; } /// Gets the channel-level permissions granted to this user for a given channel. ChannelPermissions GetPermissions(IGuildChannel channel); diff --git a/src/Discord.Net/Entities/Users/IVoiceState.cs b/src/Discord.Net/Entities/Users/IVoiceState.cs new file mode 100644 index 000000000..8bdd7436c --- /dev/null +++ b/src/Discord.Net/Entities/Users/IVoiceState.cs @@ -0,0 +1,16 @@ +namespace Discord +{ + public interface IVoiceState + { + /// Returns true if this user has marked themselves as deafened. + bool IsSelfDeafened { get; } + /// Returns true if this user has marked themselves as muted. + bool IsSelfMuted { get; } + /// Returns true if the guild is temporarily blocking audio to/from this user. + bool IsSuppressed { get; } + /// Gets the voice channel this user is currently in, if any. + IVoiceChannel VoiceChannel { get; } + /// Gets the unique identifier for this user's voice session. + string VoiceSessionId { get; } + } +} diff --git a/src/Discord.Net/Entities/Users/SelfUser.cs b/src/Discord.Net/Entities/Users/SelfUser.cs index d650c29bf..1e0a621a1 100644 --- a/src/Discord.Net/Entities/Users/SelfUser.cs +++ b/src/Discord.Net/Entities/Users/SelfUser.cs @@ -30,7 +30,7 @@ namespace Discord var model = await Discord.ApiClient.GetCurrentUser().ConfigureAwait(false); Update(model, UpdateSource.Rest); - } + } public async Task Modify(Action func) { if (func != null) throw new NullReferenceException(nameof(func)); diff --git a/src/Discord.Net/Entities/Users/User.cs b/src/Discord.Net/Entities/Users/User.cs index 7efe5239f..6e1282933 100644 --- a/src/Discord.Net/Entities/Users/User.cs +++ b/src/Discord.Net/Entities/Users/User.cs @@ -17,9 +17,9 @@ namespace Discord public override DiscordClient Discord { get; } public string AvatarUrl => API.CDN.GetUserAvatarUrl(Id, _avatarId); - public virtual Game? Game => null; public string Mention => MentionUtils.Mention(this, false); public string NicknameMention => MentionUtils.Mention(this, true); + public virtual Game? Game => null; public virtual UserStatus Status => UserStatus.Unknown; public User(DiscordClient discord, Model model) diff --git a/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs index 6a8d4790a..1c0520a5a 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs @@ -13,7 +13,7 @@ namespace Discord public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; public new CachedPublicUser Recipient => base.Recipient as CachedPublicUser; - public IReadOnlyCollection Members => ImmutableArray.Create(Discord.CurrentUser, Recipient); + public IReadOnlyCollection Members => ImmutableArray.Create(Discord.CurrentUser, Recipient); public CachedDMChannel(DiscordSocketClient discord, CachedPublicUser recipient, Model model) : base(discord, recipient, model) @@ -21,11 +21,11 @@ namespace Discord _messages = new MessageCache(Discord, this); } - public override Task GetUser(ulong id) => Task.FromResult(GetCachedUser(id)); - public override Task> GetUsers() => Task.FromResult(Members); + public override Task GetUser(ulong id) => Task.FromResult(GetCachedUser(id)); + public override Task> GetUsers() => Task.FromResult>(Members); public override Task> GetUsers(int limit, int offset) => Task.FromResult>(Members.Skip(offset).Take(limit).ToImmutableArray()); - public IUser GetCachedUser(ulong id) + public ICachedUser GetCachedUser(ulong id) { var currentUser = Discord.CurrentUser; if (id == Recipient.Id) @@ -48,7 +48,7 @@ namespace Discord { return await _messages.Download(fromMessageId, dir, limit).ConfigureAwait(false); } - public CachedMessage AddCachedMessage(IUser author, MessageModel model) + public CachedMessage AddCachedMessage(ICachedUser author, MessageModel model) { var msg = new CachedMessage(this, author, model); _messages.Add(msg); diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs index 8890c8230..f8a4aa380 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs @@ -11,6 +11,8 @@ using ExtendedModel = Discord.API.Gateway.ExtendedGuild; using MemberModel = Discord.API.GuildMember; using Model = Discord.API.Guild; using PresenceModel = Discord.API.Presence; +using RoleModel = Discord.API.Role; +using VoiceStateModel = Discord.API.VoiceState; namespace Discord { @@ -20,9 +22,11 @@ namespace Discord private ConcurrentHashSet _channels; private ConcurrentDictionary _members; private ConcurrentDictionary _presences; - private int _userCount; + private ConcurrentDictionary _voiceStates; public bool Available { get; private set; } //TODO: Add to IGuild + public int MemberCount { get; private set; } + public int DownloadedMemberCount { get; private set; } public bool HasAllMembers => _downloaderPromise.Task.IsCompleted; public Task DownloaderPromise => _downloaderPromise.Task; @@ -32,9 +36,10 @@ namespace Discord public IReadOnlyCollection Channels => _channels.Select(x => GetCachedChannel(x)).ToReadOnlyCollection(_channels); public IReadOnlyCollection Members => _members.ToReadOnlyCollection(); - public CachedGuild(DiscordSocketClient discord, Model model) : base(discord, model) + public CachedGuild(DiscordSocketClient discord, ExtendedModel model, DataStore dataStore) : base(discord, model) { _downloaderPromise = new TaskCompletionSource(); + Update(model, UpdateSource.Creation, dataStore); } public void Update(ExtendedModel model, UpdateSource source, DataStore dataStore) @@ -52,6 +57,8 @@ namespace Discord _presences = new ConcurrentDictionary(); if (_roles == null) _roles = new ConcurrentDictionary(); + if (_voiceStates == null) + _voiceStates = new ConcurrentDictionary(); if (Emojis == null) Emojis = ImmutableArray.Create(); if (Features == null) @@ -61,7 +68,7 @@ namespace Discord base.Update(model as Model, source); - _userCount = model.MemberCount; + MemberCount = model.MemberCount; var channels = new ConcurrentHashSet(); if (model.Channels != null) @@ -75,7 +82,7 @@ namespace Discord if (model.Presences != null) { for (int i = 0; i < model.Presences.Length; i++) - AddCachedPresence(model.Presences[i], presences); + AddOrUpdateCachedPresence(model.Presences[i], presences); } _presences = presences; @@ -85,10 +92,19 @@ namespace Discord for (int i = 0; i < model.Members.Length; i++) AddCachedUser(model.Members[i], members, dataStore); _downloaderPromise = new TaskCompletionSource(); + DownloadedMemberCount = model.Members.Length; if (!model.Large) _downloaderPromise.SetResult(true); } _members = members; + + var voiceStates = new ConcurrentDictionary(); + if (model.VoiceStates != null) + { + for (int i = 0; i < model.VoiceStates.Length; i++) + AddOrUpdateCachedVoiceState(model.VoiceStates[i], _voiceStates); + } + _voiceStates = voiceStates; } public override Task GetChannel(ulong id) => Task.FromResult(GetCachedChannel(id)); @@ -108,7 +124,7 @@ namespace Discord (channels ?? _channels).TryRemove(id); } - public Presence AddCachedPresence(PresenceModel model, ConcurrentDictionary presences = null) + public Presence AddOrUpdateCachedPresence(PresenceModel model, ConcurrentDictionary presences = null) { var game = model.Game != null ? new Game(model.Game) : (Game?)null; var presence = new Presence(model.Status, game); @@ -130,6 +146,42 @@ namespace Discord return null; } + public Role AddCachedRole(RoleModel model, ConcurrentDictionary roles = null) + { + var role = new Role(this, model); + (roles ?? _roles)[model.Id] = role; + return role; + } + public Role RemoveCachedRole(ulong id) + { + Role role; + if (_roles.TryRemove(id, out role)) + return role; + return null; + } + + public VoiceState AddOrUpdateCachedVoiceState(VoiceStateModel model, ConcurrentDictionary voiceStates = null) + { + var voiceChannel = GetCachedChannel(model.ChannelId.Value) as CachedVoiceChannel; + var voiceState = new VoiceState(voiceChannel, model.SessionId, model.SelfMute, model.SelfDeaf, model.Suppress); + (voiceStates ?? _voiceStates)[model.UserId] = voiceState; + return voiceState; + } + public VoiceState? GetCachedVoiceState(ulong id) + { + VoiceState voiceState; + if (_voiceStates.TryGetValue(id, out voiceState)) + return voiceState; + return null; + } + public VoiceState? RemoveCachedVoiceState(ulong id) + { + VoiceState voiceState; + if (_voiceStates.TryRemove(id, out voiceState)) + return voiceState; + return null; + } + public override Task GetUser(ulong id) => Task.FromResult(GetCachedUser(id)); public override Task GetCurrentUser() => Task.FromResult(CurrentUser); @@ -140,10 +192,11 @@ namespace Discord => Task.FromResult>(Members.OrderBy(x => x.Id).Skip(offset).Take(limit).ToImmutableArray()); public CachedGuildUser AddCachedUser(MemberModel model, ConcurrentDictionary members = null, DataStore dataStore = null) { - var user = Discord.AddCachedUser(model.User); + var user = Discord.GetOrAddCachedUser(model.User); var member = new CachedGuildUser(this, user, model); (members ?? _members)[user.Id] = member; user.AddRef(); + DownloadedMemberCount++; return member; } public CachedGuildUser GetCachedUser(ulong id) @@ -160,7 +213,6 @@ namespace Discord return member; return null; } - public async Task DownloadMembers() { if (!HasAllMembers) @@ -169,7 +221,7 @@ namespace Discord } public void CompleteDownloadMembers() { - _downloaderPromise.SetResult(true); + _downloaderPromise.TrySetResult(true); } public CachedGuild Clone() => MemberwiseClone() as CachedGuild; diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs b/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs index 191fbe05f..8801d59d7 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs @@ -2,11 +2,21 @@ namespace Discord { - internal class CachedGuildUser : GuildUser, ICachedEntity + internal class CachedGuildUser : GuildUser, ICachedUser { - public VoiceChannel VoiceChannel { get; private set; } - public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; + public new CachedGuild Guild => base.Guild as CachedGuild; + public new CachedPublicUser User => base.User as CachedPublicUser; + + public Presence? Presence => Guild.GetCachedPresence(Id); + public override Game? Game => Presence?.Game; + public override UserStatus Status => Presence?.Status ?? UserStatus.Offline; + + public VoiceState? VoiceState => Guild.GetCachedVoiceState(Id); + public bool IsSelfDeafened => VoiceState?.IsSelfDeafened ?? false; + public bool IsSelfMuted => VoiceState?.IsSelfMuted ?? false; + public bool IsSuppressed => VoiceState?.IsSuppressed ?? false; + public CachedVoiceChannel VoiceChannel => VoiceState?.VoiceChannel; public CachedGuildUser(CachedGuild guild, CachedPublicUser user, Model model) : base(guild, user, model) @@ -14,5 +24,6 @@ namespace Discord } public CachedGuildUser Clone() => MemberwiseClone() as CachedGuildUser; + ICachedUser ICachedUser.Clone() => Clone(); } } diff --git a/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs b/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs index 04842ab8a..d1c67dac7 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs @@ -1,15 +1,20 @@ using ChannelModel = Discord.API.Channel; using Model = Discord.API.User; +using PresenceModel = Discord.API.Presence; namespace Discord { - internal class CachedPublicUser : User, ICachedEntity + internal class CachedPublicUser : User, ICachedUser { private int _references; + private Game? _game; + private UserStatus _status; public CachedDMChannel DMChannel { get; private set; } public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; + public override UserStatus Status => _status; + public override Game? Game => _game; public CachedPublicUser(DiscordSocketClient discord, Model model) : base(discord, model) @@ -39,6 +44,16 @@ namespace Discord } } + public void Update(PresenceModel model, UpdateSource source) + { + if (source == UpdateSource.Rest) return; + + var game = model.Game != null ? new Game(model.Game) : (Game?)null; + + _status = model.Status; + _game = game; + } + public void AddRef() { lock (this) @@ -54,5 +69,6 @@ namespace Discord } public CachedPublicUser Clone() => MemberwiseClone() as CachedPublicUser; + ICachedUser ICachedUser.Clone() => Clone(); } } diff --git a/src/Discord.Net/Entities/WebSocket/CachedSelfUser.cs b/src/Discord.Net/Entities/WebSocket/CachedSelfUser.cs index fe4a264c8..9b3543c11 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedSelfUser.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedSelfUser.cs @@ -2,7 +2,7 @@ namespace Discord { - internal class CachedSelfUser : SelfUser, ICachedEntity + internal class CachedSelfUser : SelfUser, ICachedUser { public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; @@ -12,5 +12,6 @@ namespace Discord } public CachedSelfUser Clone() => MemberwiseClone() as CachedSelfUser; + ICachedUser ICachedUser.Clone() => Clone(); } } diff --git a/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs index 96899a022..95c0ac375 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs @@ -14,7 +14,7 @@ namespace Discord public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; public new CachedGuild Guild => base.Guild as CachedGuild; - public IReadOnlyCollection Members + public IReadOnlyCollection Members => Guild.Members.Where(x => Permissions.GetValue(Permissions.ResolveChannel(x, this, x.GuildPermissions.RawValue), ChannelPermission.ReadMessages)).ToImmutableArray(); public CachedTextChannel(CachedGuild guild, Model model) @@ -23,11 +23,11 @@ namespace Discord _messages = new MessageCache(Discord, this); } - public override Task GetUser(ulong id) => Task.FromResult(GetCachedUser(id)); - public override Task> GetUsers() => Task.FromResult(Members); + public override Task GetUser(ulong id) => Task.FromResult(GetCachedUser(id)); + public override Task> GetUsers() => Task.FromResult>(Members); public override Task> GetUsers(int limit, int offset) => Task.FromResult>(Members.Skip(offset).Take(limit).ToImmutableArray()); - public IGuildUser GetCachedUser(ulong id) + public CachedGuildUser GetCachedUser(ulong id) { var user = Guild.GetCachedUser(id); if (user != null && Permissions.GetValue(Permissions.ResolveChannel(user, this, user.GuildPermissions.RawValue), ChannelPermission.ReadMessages)) @@ -48,7 +48,7 @@ namespace Discord return await _messages.Download(fromMessageId, dir, limit).ConfigureAwait(false); } - public CachedMessage AddCachedMessage(IUser author, MessageModel model) + public CachedMessage AddCachedMessage(ICachedUser author, MessageModel model) { var msg = new CachedMessage(this, author, model); _messages.Add(msg); @@ -65,10 +65,10 @@ namespace Discord public CachedTextChannel Clone() => MemberwiseClone() as CachedTextChannel; - IReadOnlyCollection ICachedMessageChannel.Members => Members; + IReadOnlyCollection ICachedMessageChannel.Members => Members; IMessage IMessageChannel.GetCachedMessage(ulong id) => GetCachedMessage(id); - IUser ICachedMessageChannel.GetCachedUser(ulong id) => GetCachedUser(id); + ICachedUser ICachedMessageChannel.GetCachedUser(ulong id) => GetCachedUser(id); ICachedChannel ICachedChannel.Clone() => Clone(); } } diff --git a/src/Discord.Net/Entities/WebSocket/ICachedMessageChannel.cs b/src/Discord.Net/Entities/WebSocket/ICachedMessageChannel.cs index 5db4a28a7..30ca49022 100644 --- a/src/Discord.Net/Entities/WebSocket/ICachedMessageChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/ICachedMessageChannel.cs @@ -5,12 +5,12 @@ namespace Discord { internal interface ICachedMessageChannel : ICachedChannel, IMessageChannel { - IReadOnlyCollection Members { get; } + IReadOnlyCollection Members { get; } - CachedMessage AddCachedMessage(IUser author, MessageModel model); + CachedMessage AddCachedMessage(ICachedUser author, MessageModel model); new CachedMessage GetCachedMessage(ulong id); CachedMessage RemoveCachedMessage(ulong id); - IUser GetCachedUser(ulong id); + ICachedUser GetCachedUser(ulong id); } } diff --git a/src/Discord.Net/Entities/WebSocket/ICachedUser.cs b/src/Discord.Net/Entities/WebSocket/ICachedUser.cs new file mode 100644 index 000000000..e9e7d2929 --- /dev/null +++ b/src/Discord.Net/Entities/WebSocket/ICachedUser.cs @@ -0,0 +1,7 @@ +namespace Discord +{ + internal interface ICachedUser : IUser, ICachedEntity + { + ICachedUser Clone(); + } +} diff --git a/src/Discord.Net/Entities/WebSocket/IVoiceState.cs.old b/src/Discord.Net/Entities/WebSocket/IVoiceState.cs.old deleted file mode 100644 index 0937f5049..000000000 --- a/src/Discord.Net/Entities/WebSocket/IVoiceState.cs.old +++ /dev/null @@ -1,62 +0,0 @@ -/*using System; -using Model = Discord.API.MemberVoiceState; - -namespace Discord.WebSocket -{ - internal class VoiceState : IVoiceState - { - [Flags] - private enum VoiceStates : byte - { - None = 0x0, - Muted = 0x01, - Deafened = 0x02, - Suppressed = 0x4, - SelfMuted = 0x10, - SelfDeafened = 0x20, - } - - private VoiceStates _voiceStates; - - public Guild Guild { get; } - public ulong UserId { get; } - - /// Gets this user's current voice channel. - public VoiceChannel VoiceChannel { get; set; } - - /// Returns true if this user has marked themselves as muted. - public bool IsSelfMuted => (_voiceStates & VoiceStates.SelfMuted) != 0; - /// Returns true if this user has marked themselves as deafened. - public bool IsSelfDeafened => (_voiceStates & VoiceStates.SelfDeafened) != 0; - /// Returns true if the guild is blocking audio from this user. - public bool IsMuted => (_voiceStates & VoiceStates.Muted) != 0; - /// Returns true if the guild is blocking audio to this user. - public bool IsDeafened => (_voiceStates & VoiceStates.Deafened) != 0; - /// Returns true if the guild is temporarily blocking audio to/from this user. - public bool IsSuppressed => (_voiceStates & VoiceStates.Suppressed) != 0; - - public VoiceState(ulong userId, Guild guild) - { - UserId = userId; - Guild = guild; - } - - private void Update(Model model, UpdateSource source) - { - if (model.IsMuted == true) - _voiceStates |= VoiceStates.Muted; - else if (model.IsMuted == false) - _voiceStates &= ~VoiceStates.Muted; - - if (model.IsDeafened == true) - _voiceStates |= VoiceStates.Deafened; - else if (model.IsDeafened == false) - _voiceStates &= ~VoiceStates.Deafened; - - if (model.IsSuppressed == true) - _voiceStates |= VoiceStates.Suppressed; - else if (model.IsSuppressed == false) - _voiceStates &= ~VoiceStates.Suppressed; - } - } -}*/ \ No newline at end of file diff --git a/src/Discord.Net/Entities/WebSocket/VoiceState.cs b/src/Discord.Net/Entities/WebSocket/VoiceState.cs new file mode 100644 index 000000000..fc183a520 --- /dev/null +++ b/src/Discord.Net/Entities/WebSocket/VoiceState.cs @@ -0,0 +1,42 @@ +using System; + +namespace Discord +{ + internal struct VoiceState : IVoiceState + { + [Flags] + private enum Flags : byte + { + None = 0x0, + Suppressed = 0x1, + SelfMuted = 0x2, + SelfDeafened = 0x4, + } + + private readonly Flags _voiceStates; + + public CachedVoiceChannel VoiceChannel { get; } + public string VoiceSessionId { get; } + + public bool IsSelfMuted => (_voiceStates & Flags.SelfMuted) != 0; + public bool IsSelfDeafened => (_voiceStates & Flags.SelfDeafened) != 0; + public bool IsSuppressed => (_voiceStates & Flags.Suppressed) != 0; + + public VoiceState(CachedVoiceChannel voiceChannel, string sessionId, bool isSelfMuted, bool isSelfDeafened, bool isSuppressed) + { + VoiceChannel = voiceChannel; + VoiceSessionId = sessionId; + + Flags voiceStates = Flags.None; + if (isSelfMuted) + voiceStates |= Flags.SelfMuted; + if (isSelfDeafened) + voiceStates |= Flags.SelfDeafened; + if (isSuppressed) + voiceStates |= Flags.Suppressed; + _voiceStates = voiceStates; + } + + IVoiceChannel IVoiceState.VoiceChannel => VoiceChannel; + } +} diff --git a/src/Discord.Net/project.json b/src/Discord.Net/project.json index a15ef430a..4da1b01d3 100644 --- a/src/Discord.Net/project.json +++ b/src/Discord.Net/project.json @@ -15,6 +15,7 @@ "buildOptions": { "allowUnsafe": true, + "define": [ "BENCHMARK" ], "warningsAsErrors": false },