From a831ae948466311f3742776c64fde19694642174 Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 8 Jun 2016 18:42:57 -0300 Subject: [PATCH] Added heartbeats, latency, guild events and channel events --- src/Discord.Net/API/DiscordAPIClient.cs | 10 +- src/Discord.Net/API/WebSocketMessage.cs | 2 +- src/Discord.Net/DiscordClient.cs | 20 ++ src/Discord.Net/DiscordSocketClient.cs | 183 +++++++++++------- .../Entities/Channels/DMChannel.cs | 2 +- .../Entities/Channels/GuildChannel.cs | 2 +- .../Entities/Channels/TextChannel.cs | 2 +- .../Entities/Channels/VoiceChannel.cs | 2 +- .../Entities/Guilds/GuildIntegration.cs | 4 +- src/Discord.Net/Entities/Guilds/UserGuild.cs | 2 +- src/Discord.Net/Entities/Invites/Invite.cs | 2 +- .../Entities/Invites/InviteMetadata.cs | 2 +- src/Discord.Net/Entities/Messages/Message.cs | 2 +- .../Entities/Permissions/Permissions.cs | 17 +- src/Discord.Net/Entities/Users/GuildUser.cs | 8 +- .../Entities/WebSocket/CachedDMChannel.cs | 3 +- .../Entities/WebSocket/CachedGuild.cs | 2 + .../Entities/WebSocket/CachedGuildUser.cs | 2 + .../Entities/WebSocket/CachedPublicUser.cs | 2 +- .../Entities/WebSocket/CachedTextChannel.cs | 1 + .../Entities/WebSocket/CachedVoiceChannel.cs | 2 + .../Entities/WebSocket/ICachedChannel.cs | 7 +- src/Discord.Net/Extensions/EventExtensions.cs | 10 + src/Discord.Net/Net/Rest/DefaultRestClient.cs | 21 +- .../Net/WebSockets/DefaultWebsocketClient.cs | 45 +++-- src/Discord.Net/Utilities/MessageCache.cs | 2 +- 26 files changed, 214 insertions(+), 143 deletions(-) diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index 39a6d25f7..bf94084ad 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -27,7 +27,7 @@ namespace Discord.API { public event Func SentRequest; public event Func SentGatewayMessage; - public event Func ReceivedGatewayEvent; + public event Func ReceivedGatewayEvent; private readonly RequestQueue _requestQueue; private readonly JsonSerializer _serializer; @@ -66,14 +66,14 @@ namespace Discord.API using (var reader = new StreamReader(decompressed)) { var msg = JsonConvert.DeserializeObject(reader.ReadToEnd()); - await ReceivedGatewayEvent.Raise((GatewayOpCode)msg.Operation, msg.Type, msg.Payload as JToken).ConfigureAwait(false); + await ReceivedGatewayEvent.Raise((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); } } }; _gatewayClient.TextMessage += async text => { var msg = JsonConvert.DeserializeObject(text); - await ReceivedGatewayEvent.Raise((GatewayOpCode)msg.Operation, msg.Type, msg.Payload as JToken).ConfigureAwait(false); + await ReceivedGatewayEvent.Raise((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); }; } @@ -363,6 +363,10 @@ namespace Discord.API }; await SendGateway(GatewayOpCode.Identify, msg, options: options).ConfigureAwait(false); } + public async Task SendHeartbeat(int lastSeq, RequestOptions options = null) + { + await SendGateway(GatewayOpCode.Heartbeat, lastSeq, options: options).ConfigureAwait(false); + } //Channels public async Task GetChannel(ulong channelId, RequestOptions options = null) diff --git a/src/Discord.Net/API/WebSocketMessage.cs b/src/Discord.Net/API/WebSocketMessage.cs index 19ec2ac41..3c7d11b70 100644 --- a/src/Discord.Net/API/WebSocketMessage.cs +++ b/src/Discord.Net/API/WebSocketMessage.cs @@ -9,7 +9,7 @@ namespace Discord.API [JsonProperty("t", NullValueHandling = NullValueHandling.Ignore)] public string Type { get; set; } [JsonProperty("s", NullValueHandling = NullValueHandling.Ignore)] - public uint? Sequence { get; set; } + public int? Sequence { get; set; } [JsonProperty("d")] public object Payload { get; set; } } diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index 719b5eba1..43829cca1 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -28,8 +28,10 @@ namespace Discord public LoginState LoginState { get; private set; } public API.DiscordApiClient ApiClient { get; private set; } + /// Creates a new discord client using only the REST API. public DiscordClient() : this(new DiscordConfig()) { } + /// Creates a new discord client using only the REST API. public DiscordClient(DiscordConfig config) { _log = new LogManager(config.LogLevel); @@ -40,10 +42,12 @@ namespace Discord _connectionLock = new SemaphoreSlim(1, 1); _requestQueue = new RequestQueue(); + //TODO: Is there any better way to do this WebSocketProvider access? ApiClient = new API.DiscordApiClient(config.RestClientProvider, (config as DiscordSocketConfig)?.WebSocketProvider, requestQueue: _requestQueue); ApiClient.SentRequest += async (method, endpoint, millis) => await _log.Verbose("Rest", $"{method} {endpoint}: {millis} ms").ConfigureAwait(false); } + /// public async Task Login(TokenType tokenType, string token, bool validateToken = true) { await _connectionLock.WaitAsync().ConfigureAwait(false); @@ -89,6 +93,7 @@ namespace Discord } protected virtual Task OnLogin() => Task.CompletedTask; + /// public async Task Logout() { await _connectionLock.WaitAsync().ConfigureAwait(false); @@ -115,12 +120,14 @@ namespace Discord } protected virtual Task OnLogout() => Task.CompletedTask; + /// public async Task> GetConnections() { var models = await ApiClient.GetCurrentUserConnections().ConfigureAwait(false); return models.Select(x => new Connection(x)).ToImmutableArray(); } + /// public virtual async Task GetChannel(ulong id) { var model = await ApiClient.GetChannel(id).ConfigureAwait(false); @@ -140,12 +147,14 @@ namespace Discord } return null; } + /// public virtual async Task> GetDMChannels() { var models = await ApiClient.GetCurrentUserDMs().ConfigureAwait(false); return models.Select(x => new DMChannel(this, new User(this, x.Recipient), x)).ToImmutableArray(); } + /// public virtual async Task GetInvite(string inviteIdOrXkcd) { var model = await ApiClient.GetInvite(inviteIdOrXkcd).ConfigureAwait(false); @@ -154,6 +163,7 @@ namespace Discord return null; } + /// public virtual async Task GetGuild(ulong id) { var model = await ApiClient.GetGuild(id).ConfigureAwait(false); @@ -161,6 +171,7 @@ namespace Discord return new Guild(this, model); return null; } + /// public virtual async Task GetGuildEmbed(ulong id) { var model = await ApiClient.GetGuildEmbed(id).ConfigureAwait(false); @@ -168,12 +179,14 @@ namespace Discord return new GuildEmbed(model); return null; } + /// public virtual async Task> GetGuilds() { var models = await ApiClient.GetCurrentUserGuilds().ConfigureAwait(false); return models.Select(x => new UserGuild(this, x)).ToImmutableArray(); } + /// public virtual async Task CreateGuild(string name, IVoiceRegion region, Stream jpegIcon = null) { var args = new CreateGuildParams(); @@ -181,6 +194,7 @@ namespace Discord return new Guild(this, model); } + /// public virtual async Task GetUser(ulong id) { var model = await ApiClient.GetUser(id).ConfigureAwait(false); @@ -188,6 +202,7 @@ namespace Discord return new User(this, model); return null; } + /// public virtual async Task GetUser(string username, string discriminator) { var model = await ApiClient.GetUser(username, discriminator).ConfigureAwait(false); @@ -195,6 +210,7 @@ namespace Discord return new User(this, model); return null; } + /// public virtual async Task GetCurrentUser() { var user = _currentUser; @@ -206,17 +222,20 @@ namespace Discord } return user; } + /// public virtual async Task> QueryUsers(string query, int limit) { var models = await ApiClient.QueryUsers(query, limit).ConfigureAwait(false); return models.Select(x => new User(this, x)).ToImmutableArray(); } + /// public virtual async Task> GetVoiceRegions() { var models = await ApiClient.GetVoiceRegions().ConfigureAwait(false); return models.Select(x => new VoiceRegion(x)).ToImmutableArray(); } + /// public virtual async Task GetVoiceRegion(string id) { var models = await ApiClient.GetVoiceRegions().ConfigureAwait(false); @@ -228,6 +247,7 @@ namespace Discord if (!_isDisposed) _isDisposed = true; } + /// public void Dispose() => Dispose(true); ConnectionState IDiscordClient.ConnectionState => ConnectionState.Disconnected; diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index a723d13d1..f6143cf1d 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -1,5 +1,4 @@ -using Discord.API; -using Discord.API.Gateway; +using Discord.API.Gateway; using Discord.Data; using Discord.Extensions; using Discord.Logging; @@ -11,19 +10,23 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace Discord { //TODO: Remove unnecessary `as` casts - //TODO: Add docstrings + //TODO: Add event docstrings + //TODO: Add reconnect logic (+ensure the heartbeat task shuts down) + //TODO: Add resume logic public class DiscordSocketClient : DiscordClient, IDiscordClient { public event Func Connected, Disconnected; public event Func Ready; //public event Func VoiceConnected, VoiceDisconnected; - /*public event Func ChannelCreated, ChannelDestroyed; + public event Func ChannelCreated, ChannelDestroyed; public event Func ChannelUpdated; public event Func MessageReceived, MessageDeleted; public event Func MessageUpdated; @@ -34,7 +37,8 @@ namespace Discord public event Func UserJoined, UserLeft, UserBanned, UserUnbanned; public event Func UserUpdated; public event Func CurrentUserUpdated; - public event Func UserIsTyping;*/ + public event Func UserIsTyping; + public event Func LatencyUpdated; private readonly ConcurrentQueue _largeGuilds; private readonly Logger _gatewayLogger; @@ -44,13 +48,21 @@ namespace Discord private readonly bool _enablePreUpdateEvents; private readonly int _largeThreshold; private readonly int _totalShards; - private ImmutableDictionary _voiceRegions; private string _sessionId; + private int _lastSeq; + private ImmutableDictionary _voiceRegions; private TaskCompletionSource _connectTask; + private CancellationTokenSource _heartbeatCancelToken; + private Task _heartbeatTask; + private long _heartbeatTime; + /// Gets the shard if of this client. public int ShardId { get; } + /// Gets the current connection state of this client. public ConnectionState ConnectionState { get; private set; } - public IWebSocketClient GatewaySocket { get; private set; } + /// Gets the estimated round-trip latency to the gateway server. + public int Latency { get; private set; } + internal IWebSocketClient GatewaySocket { get; private set; } internal int MessageCacheSize { get; private set; } //internal bool UsePermissionCache { get; private set; } internal DataStore DataStore { get; private set; } @@ -61,7 +73,7 @@ namespace Discord get { var guilds = DataStore.Guilds; - return guilds.Select(x => x as CachedGuild).ToReadOnlyCollection(guilds); + return guilds.ToReadOnlyCollection(guilds); } } internal IReadOnlyCollection DMChannels @@ -69,13 +81,15 @@ namespace Discord get { var users = DataStore.Users; - return users.Select(x => (x as CachedPublicUser).DMChannel).Where(x => x != null).ToReadOnlyCollection(users); + return users.Select(x => x.DMChannel).Where(x => x != null).ToReadOnlyCollection(users); } } internal IReadOnlyCollection VoiceRegions => _voiceRegions.ToReadOnlyCollection(); + /// Creates a new discord client using the REST and WebSocket APIs. public DiscordSocketClient() : this(new DiscordSocketConfig()) { } + /// Creates a new discord client using the REST and WebSocket APIs. public DiscordSocketClient(DiscordSocketConfig config) : base(config) { @@ -117,6 +131,7 @@ namespace Discord _voiceRegions = ImmutableDictionary.Create(); } + /// public async Task Connect() { await _connectionLock.WaitAsync().ConfigureAwait(false); @@ -135,6 +150,7 @@ namespace Discord try { _connectTask = new TaskCompletionSource(); + _heartbeatCancelToken = new CancellationTokenSource(); await ApiClient.Connect().ConfigureAwait(false); await _connectTask.Task.ConfigureAwait(false); @@ -148,6 +164,7 @@ namespace Discord await Connected.Raise().ConfigureAwait(false); } + /// public async Task Disconnect() { await _connectionLock.WaitAsync().ConfigureAwait(false); @@ -165,13 +182,15 @@ namespace Discord ConnectionState = ConnectionState.Disconnecting; await ApiClient.Disconnect().ConfigureAwait(false); + await _heartbeatTask.ConfigureAwait(false); while (_largeGuilds.TryDequeue(out guildId)) { } ConnectionState = ConnectionState.Disconnected; await Disconnected.Raise().ConfigureAwait(false); } - + + /// public override Task GetVoiceRegion(string id) { VoiceRegion region; @@ -180,6 +199,7 @@ namespace Discord return Task.FromResult(null); } + /// public override Task GetGuild(ulong id) { return Task.FromResult(DataStore.GetGuild(id)); @@ -192,7 +212,7 @@ namespace Discord if (model.Unavailable != true) { for (int i = 0; i < model.Channels.Length; i++) - AddCachedChannel(model.Channels[i], dataStore); + AddCachedChannel(guild, model.Channels[i], dataStore); } dataStore.AddGuild(guild); if (model.Large) @@ -203,7 +223,7 @@ namespace Discord { dataStore = dataStore ?? DataStore; - var guild = dataStore.RemoveGuild(id) as CachedGuild; + var guild = dataStore.RemoveGuild(id); foreach (var channel in guild.Channels) guild.RemoveCachedChannel(channel.Id); foreach (var user in guild.Members) @@ -211,25 +231,25 @@ namespace Discord return guild; } + /// public override Task GetChannel(ulong id) { return Task.FromResult(DataStore.GetChannel(id)); } - internal ICachedChannel AddCachedChannel(API.Channel model, DataStore dataStore = null) + internal ICachedGuildChannel AddCachedChannel(CachedGuild guild, API.Channel model, DataStore dataStore = null) { dataStore = dataStore ?? DataStore; - ICachedChannel channel; - if (model.IsPrivate) - { - var recipient = AddCachedUser(model.Recipient, dataStore); - channel = recipient.SetDMChannel(model); - } - else - { - var guild = dataStore.GetGuild(model.GuildId.Value); - channel = guild.AddCachedChannel(model); - } + var channel = guild.AddCachedChannel(model); + dataStore.AddChannel(channel); + return channel; + } + internal CachedDMChannel AddCachedDMChannel(API.Channel model, DataStore dataStore = null) + { + dataStore = dataStore ?? DataStore; + + var recipient = AddCachedUser(model.Recipient, dataStore); + var channel = recipient.AddDMChannel(model); dataStore.AddChannel(channel); return channel; } @@ -237,8 +257,8 @@ namespace Discord { dataStore = dataStore ?? DataStore; - //TODO: C#7 - var channel = DataStore.RemoveChannel(id) as ICachedChannel; + //TODO: C#7 Typeswitch Candidate + var channel = DataStore.RemoveChannel(id); var guildChannel = channel as ICachedGuildChannel; if (guildChannel != null) @@ -258,10 +278,12 @@ namespace Discord return null; } + /// public override Task GetUser(ulong id) { return Task.FromResult(DataStore.GetUser(id)); } + /// public override Task GetUser(string username, string discriminator) { return Task.FromResult(DataStore.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault()); @@ -270,7 +292,7 @@ namespace Discord { dataStore = dataStore ?? DataStore; - var user = dataStore.GetOrAddUser(model.Id, _ => new CachedPublicUser(this, model)) as CachedPublicUser; + var user = dataStore.GetOrAddUser(model.Id, _ => new CachedPublicUser(this, model)); user.AddRef(); return user; } @@ -278,22 +300,34 @@ namespace Discord { dataStore = dataStore ?? DataStore; - var user = dataStore.GetUser(id) as CachedPublicUser; + var user = dataStore.GetUser(id); user.RemoveRef(); return user; } - private async Task ProcessMessage(GatewayOpCode opCode, string type, JToken payload) + private async Task ProcessMessage(GatewayOpCode opCode, int? seq, string type, object payload) { + if (seq != null) + _lastSeq = seq.Value; try { switch (opCode) { case GatewayOpCode.Hello: { - var data = payload.ToObject(_serializer); + 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: @@ -303,15 +337,15 @@ namespace Discord case "READY": { //TODO: Make downloading large guilds optional - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); var dataStore = _dataStoreProvider(ShardId, _totalShards, data.Guilds.Length, data.PrivateChannels.Length); - _currentUser = new CachedSelfUser(this,data.User); + _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++) - AddCachedChannel(data.PrivateChannels[i], dataStore); + AddCachedDMChannel(data.PrivateChannels[i], dataStore); _sessionId = data.SessionId; DataStore = dataStore; @@ -323,9 +357,9 @@ namespace Discord break; //Guilds - /*case "GUILD_CREATE": + case "GUILD_CREATE": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); var guild = new CachedGuild(this, data); DataStore.AddGuild(guild); @@ -342,12 +376,12 @@ namespace Discord break; case "GUILD_UPDATE": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.GetGuild(data.Id); if (guild != null) { var before = _enablePreUpdateEvents ? guild.Clone() : null; - guild.Update(data); + guild.Update(data, UpdateSource.WebSocket); await GuildUpdated.Raise(before, guild); } else @@ -356,7 +390,7 @@ namespace Discord break; case "GUILD_DELETE": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.RemoveGuild(data.Id); if (guild != null) { @@ -375,34 +409,34 @@ namespace Discord //Channels case "CHANNEL_CREATE": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); - IChannel channel = null; + ICachedChannel channel = null; if (data.GuildId != null) { - var guild = GetCachedGuild(data.GuildId.Value); + var guild = DataStore.GetGuild(data.GuildId.Value); if (guild != null) - channel = guild.AddCachedChannel(data.Id, true); + { + channel = guild.AddCachedChannel(data); + DataStore.AddChannel(channel); + } else await _gatewayLogger.Warning("CHANNEL_CREATE referenced an unknown guild."); } else - channel = AddCachedPrivateChannel(data.Id, data.Recipient.Id); + channel = AddCachedDMChannel(data); if (channel != null) - { - channel.Update(data); await ChannelCreated.Raise(channel); - } } break; case "CHANNEL_UPDATE": { - var data = payload.ToObject(_serializer); - var channel = DataStore.GetChannel(data.Id) as Channel; + var data = (payload as JToken).ToObject(_serializer); + var channel = DataStore.GetChannel(data.Id); if (channel != null) { var before = _enablePreUpdateEvents ? channel.Clone() : null; - channel.Update(data); + channel.Update(data, UpdateSource.WebSocket); await ChannelUpdated.Raise(before, channel); } else @@ -411,7 +445,7 @@ namespace Discord break; case "CHANNEL_DELETE": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); var channel = RemoveCachedChannel(data.Id); if (channel != null) await ChannelDestroyed.Raise(channel); @@ -421,9 +455,9 @@ namespace Discord break; //Members - case "GUILD_MEMBER_ADD": + /*case "GUILD_MEMBER_ADD": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); var guild = GetGuild(data.GuildId.Value); if (guild != null) { @@ -438,7 +472,7 @@ namespace Discord break; case "GUILD_MEMBER_UPDATE": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); var guild = GetGuild(data.GuildId.Value); if (guild != null) { @@ -458,7 +492,7 @@ namespace Discord break; case "GUILD_MEMBER_REMOVE": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); var guild = GetGuild(data.GuildId.Value); if (guild != null) { @@ -479,7 +513,7 @@ namespace Discord break; case "GUILD_MEMBERS_CHUNK": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); var guild = GetCachedGuild(data.GuildId); if (guild != null) { @@ -498,9 +532,9 @@ namespace Discord break; //Roles - case "GUILD_ROLE_CREATE": + /*case "GUILD_ROLE_CREATE": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); var guild = GetCachedGuild(data.GuildId); if (guild != null) { @@ -514,7 +548,7 @@ namespace Discord break; case "GUILD_ROLE_UPDATE": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); var guild = GetCachedGuild(data.GuildId); if (guild != null) { @@ -534,8 +568,8 @@ namespace Discord break; case "GUILD_ROLE_DELETE": { - var data = payload.ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId) as CachedGuild; + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId); if (guild != null) { var role = guild.RemoveRole(data.RoleId); @@ -552,7 +586,7 @@ namespace Discord //Bans case "GUILD_BAN_ADD": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); var guild = GetCachedGuild(data.GuildId); if (guild != null) await UserBanned.Raise(new User(this, data)); @@ -574,8 +608,7 @@ namespace Discord //Messages case "MESSAGE_CREATE": { - var data = payload.ToObject(_serializer); - + var data = (payload as JToken).ToObject(_serializer); var channel = DataStore.GetChannel(data.ChannelId); if (channel != null) { @@ -599,7 +632,7 @@ namespace Discord break; case "MESSAGE_UPDATE": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); var channel = GetCachedChannel(data.ChannelId); if (channel != null) { @@ -614,7 +647,7 @@ namespace Discord break; case "MESSAGE_DELETE": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); var channel = GetCachedChannel(data.ChannelId); if (channel != null) { @@ -629,7 +662,7 @@ namespace Discord //Statuses case "PRESENCE_UPDATE": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); User user; Guild guild; if (data.GuildId == null) @@ -664,7 +697,7 @@ namespace Discord break; case "TYPING_START": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); var channel = GetCachedChannel(data.ChannelId); if (channel != null) { @@ -683,7 +716,7 @@ namespace Discord //Voice case "VOICE_STATE_UPDATE": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); var guild = GetGuild(data.GuildId); if (guild != null) { @@ -708,7 +741,7 @@ namespace Discord //Settings case "USER_UPDATE": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); if (data.Id == CurrentUser.Id) { var before = _enablePreUpdateEvents ? CurrentUser.Clone() : null; @@ -746,5 +779,17 @@ namespace Discord } await _gatewayLogger.Debug($"Received {opCode}{(type != null ? $" ({type})" : "")}").ConfigureAwait(false); } + private async Task RunHeartbeat(int intervalMillis, CancellationToken cancelToken) + { + var state = ConnectionState; + while (state == ConnectionState.Connecting || state == ConnectionState.Connected) + { + //if (_heartbeatTime != 0) //TODO: Connection lost, reconnect + + _heartbeatTime = Environment.TickCount; + await ApiClient.SendHeartbeat(_lastSeq).ConfigureAwait(false); + await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false); + } + } } } diff --git a/src/Discord.Net/Entities/Channels/DMChannel.cs b/src/Discord.Net/Entities/Channels/DMChannel.cs index eaaf3b8e6..ef8c08c19 100644 --- a/src/Discord.Net/Entities/Channels/DMChannel.cs +++ b/src/Discord.Net/Entities/Channels/DMChannel.cs @@ -26,7 +26,7 @@ namespace Discord Update(model, UpdateSource.Creation); } - protected void Update(Model model, UpdateSource source) + public void Update(Model model, UpdateSource source) { if (source == UpdateSource.Rest && IsAttached) return; diff --git a/src/Discord.Net/Entities/Channels/GuildChannel.cs b/src/Discord.Net/Entities/Channels/GuildChannel.cs index 0cdff457a..461f84076 100644 --- a/src/Discord.Net/Entities/Channels/GuildChannel.cs +++ b/src/Discord.Net/Entities/Channels/GuildChannel.cs @@ -30,7 +30,7 @@ namespace Discord Update(model, UpdateSource.Creation); } - protected virtual void Update(Model model, UpdateSource source) + public virtual void Update(Model model, UpdateSource source) { if (source == UpdateSource.Rest && IsAttached) return; diff --git a/src/Discord.Net/Entities/Channels/TextChannel.cs b/src/Discord.Net/Entities/Channels/TextChannel.cs index 17346d3e6..2c824ffa8 100644 --- a/src/Discord.Net/Entities/Channels/TextChannel.cs +++ b/src/Discord.Net/Entities/Channels/TextChannel.cs @@ -22,7 +22,7 @@ namespace Discord : base(guild, model) { } - protected override void Update(Model model, UpdateSource source) + public override void Update(Model model, UpdateSource source) { if (source == UpdateSource.Rest && IsAttached) return; diff --git a/src/Discord.Net/Entities/Channels/VoiceChannel.cs b/src/Discord.Net/Entities/Channels/VoiceChannel.cs index fd63ada44..8947c9672 100644 --- a/src/Discord.Net/Entities/Channels/VoiceChannel.cs +++ b/src/Discord.Net/Entities/Channels/VoiceChannel.cs @@ -17,7 +17,7 @@ namespace Discord : base(guild, model) { } - protected override void Update(Model model, UpdateSource source) + public override void Update(Model model, UpdateSource source) { if (source == UpdateSource.Rest && IsAttached) return; diff --git a/src/Discord.Net/Entities/Guilds/GuildIntegration.cs b/src/Discord.Net/Entities/Guilds/GuildIntegration.cs index 52d002f65..5dbdd6d47 100644 --- a/src/Discord.Net/Entities/Guilds/GuildIntegration.cs +++ b/src/Discord.Net/Entities/Guilds/GuildIntegration.cs @@ -31,7 +31,7 @@ namespace Discord Update(model, UpdateSource.Creation); } - private void Update(Model model, UpdateSource source) + public void Update(Model model, UpdateSource source) { if (source == UpdateSource.Rest && IsAttached) return; @@ -43,7 +43,7 @@ namespace Discord ExpireGracePeriod = model.ExpireGracePeriod; SyncedAt = model.SyncedAt; - Role = Guild.GetRole(model.RoleId) as Role; + Role = Guild.GetRole(model.RoleId); User = new User(Discord, model.User); } diff --git a/src/Discord.Net/Entities/Guilds/UserGuild.cs b/src/Discord.Net/Entities/Guilds/UserGuild.cs index 4eb45342d..a34b40d85 100644 --- a/src/Discord.Net/Entities/Guilds/UserGuild.cs +++ b/src/Discord.Net/Entities/Guilds/UserGuild.cs @@ -23,7 +23,7 @@ namespace Discord Discord = discord; Update(model, UpdateSource.Creation); } - private void Update(Model model, UpdateSource source) + public void Update(Model model, UpdateSource source) { if (source == UpdateSource.Rest && IsAttached) return; diff --git a/src/Discord.Net/Entities/Invites/Invite.cs b/src/Discord.Net/Entities/Invites/Invite.cs index c521370ed..d21b93331 100644 --- a/src/Discord.Net/Entities/Invites/Invite.cs +++ b/src/Discord.Net/Entities/Invites/Invite.cs @@ -26,7 +26,7 @@ namespace Discord Update(model, UpdateSource.Creation); } - protected void Update(Model model, UpdateSource source) + public void Update(Model model, UpdateSource source) { if (source == UpdateSource.Rest && IsAttached) return; diff --git a/src/Discord.Net/Entities/Invites/InviteMetadata.cs b/src/Discord.Net/Entities/Invites/InviteMetadata.cs index a4edc761f..8f3ad5a64 100644 --- a/src/Discord.Net/Entities/Invites/InviteMetadata.cs +++ b/src/Discord.Net/Entities/Invites/InviteMetadata.cs @@ -15,7 +15,7 @@ namespace Discord { Update(model, UpdateSource.Creation); } - private void Update(Model model, UpdateSource source) + public void Update(Model model, UpdateSource source) { if (source == UpdateSource.Rest && IsAttached) return; diff --git a/src/Discord.Net/Entities/Messages/Message.cs b/src/Discord.Net/Entities/Messages/Message.cs index e551616d2..4d05e409f 100644 --- a/src/Discord.Net/Entities/Messages/Message.cs +++ b/src/Discord.Net/Entities/Messages/Message.cs @@ -36,7 +36,7 @@ namespace Discord Update(model, UpdateSource.Creation); } - private void Update(Model model, UpdateSource source) + public void Update(Model model, UpdateSource source) { if (source == UpdateSource.Rest && IsAttached) return; diff --git a/src/Discord.Net/Entities/Permissions/Permissions.cs b/src/Discord.Net/Entities/Permissions/Permissions.cs index 4c920dd07..8a672489b 100644 --- a/src/Discord.Net/Entities/Permissions/Permissions.cs +++ b/src/Discord.Net/Entities/Permissions/Permissions.cs @@ -130,27 +130,14 @@ namespace Discord perms = channel.GetPermissionOverwrite(user); if (perms != null) resolvedPermissions = (resolvedPermissions & ~perms.Value.DenyValue) | perms.Value.AllowValue; - -#if CSHARP7 - switch (channel) - { - case ITextChannel _: - if (!GetValue(resolvedPermissions, ChannelPermission.ReadMessages)) - resolvedPermissions = 0; //No read permission on a text channel removes all other permissions - break; - case IVoiceChannel _: - if (!GetValue(resolvedPermissions, ChannelPermission.Connect)) - resolvedPermissions = 0; //No read permission on a text channel removes all other permissions - break; - } -#else + + //TODO: C# Typeswitch candidate var textChannel = channel as ITextChannel; var voiceChannel = channel as IVoiceChannel; if (textChannel != null && !GetValue(resolvedPermissions, ChannelPermission.ReadMessages)) resolvedPermissions = 0; //No read permission on a text channel removes all other permissions else if (voiceChannel != null && !GetValue(resolvedPermissions, ChannelPermission.Connect)) resolvedPermissions = 0; //No connect permission on a voice channel removes all other permissions -#endif resolvedPermissions &= mask; //Ensure we didnt get any permissions this channel doesnt support (from guildPerms, for example) } diff --git a/src/Discord.Net/Entities/Users/GuildUser.cs b/src/Discord.Net/Entities/Users/GuildUser.cs index fa070ca43..7c72de216 100644 --- a/src/Discord.Net/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Entities/Users/GuildUser.cs @@ -39,7 +39,7 @@ namespace Discord Update(model, UpdateSource.Creation); } - private void Update(Model model, UpdateSource source) + public void Update(Model model, UpdateSource source) { if (source == UpdateSource.Rest && IsAttached) return; @@ -49,9 +49,9 @@ namespace Discord Nickname = model.Nick; var roles = ImmutableArray.CreateBuilder(model.Roles.Length + 1); - roles.Add(Guild.EveryoneRole as Role); + roles.Add(Guild.EveryoneRole); for (int i = 0; i < model.Roles.Length; i++) - roles.Add(Guild.GetRole(model.Roles[i]) as Role); + roles.Add(Guild.GetRole(model.Roles[i])); Roles = roles.ToImmutable(); GuildPermissions = new GuildPermissions(Permissions.ResolveGuild(this)); @@ -89,7 +89,7 @@ namespace Discord if (args.Nickname.IsSpecified) Nickname = args.Nickname.Value ?? ""; if (args.Roles.IsSpecified) - Roles = args.Roles.Value.Select(x => Guild.GetRole(x) as Role).Where(x => x != null).ToImmutableArray(); + Roles = args.Roles.Value.Select(x => Guild.GetRole(x)).Where(x => x != null).ToImmutableArray(); } } public async Task Kick() diff --git a/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs index 33411ee17..6a8d4790a 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs @@ -65,6 +65,7 @@ namespace Discord public CachedDMChannel Clone() => MemberwiseClone() as CachedDMChannel; - IMessage IMessageChannel.GetCachedMessage(ulong id) => GetCachedMessage(id); + IMessage IMessageChannel.GetCachedMessage(ulong id) => GetCachedMessage(id); + ICachedChannel ICachedChannel.Clone() => Clone(); } } diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs index f2b032331..32b956e39 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs @@ -153,6 +153,8 @@ namespace Discord return null; } + public CachedGuild Clone() => MemberwiseClone() as CachedGuild; + new internal ICachedGuildChannel ToChannel(ChannelModel model) { switch (model.Type) diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs b/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs index f1bfa9d14..191fbe05f 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs @@ -12,5 +12,7 @@ namespace Discord : base(guild, user, model) { } + + public CachedGuildUser Clone() => MemberwiseClone() as CachedGuildUser; } } diff --git a/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs b/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs index 142cf15f9..04842ab8a 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs @@ -16,7 +16,7 @@ namespace Discord { } - public CachedDMChannel SetDMChannel(ChannelModel model) + public CachedDMChannel AddDMChannel(ChannelModel model) { lock (this) { diff --git a/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs index be3d59677..96899a022 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs @@ -69,5 +69,6 @@ namespace Discord IMessage IMessageChannel.GetCachedMessage(ulong id) => GetCachedMessage(id); IUser ICachedMessageChannel.GetCachedUser(ulong id) => GetCachedUser(id); + ICachedChannel ICachedChannel.Clone() => Clone(); } } diff --git a/src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs index 6d090d9fa..3b9f42bec 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs @@ -34,5 +34,7 @@ namespace Discord } public CachedVoiceChannel Clone() => MemberwiseClone() as CachedVoiceChannel; + + ICachedChannel ICachedChannel.Clone() => Clone(); } } diff --git a/src/Discord.Net/Entities/WebSocket/ICachedChannel.cs b/src/Discord.Net/Entities/WebSocket/ICachedChannel.cs index 933ce6226..caebf7c10 100644 --- a/src/Discord.Net/Entities/WebSocket/ICachedChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/ICachedChannel.cs @@ -1,6 +1,11 @@ -namespace Discord +using Model = Discord.API.Channel; + +namespace Discord { internal interface ICachedChannel : IChannel, ICachedEntity { + void Update(Model model, UpdateSource source); + + ICachedChannel Clone(); } } diff --git a/src/Discord.Net/Extensions/EventExtensions.cs b/src/Discord.Net/Extensions/EventExtensions.cs index 65ab6cebe..867d4d41d 100644 --- a/src/Discord.Net/Extensions/EventExtensions.cs +++ b/src/Discord.Net/Extensions/EventExtensions.cs @@ -6,6 +6,7 @@ namespace Discord.Extensions internal static class EventExtensions { //TODO: Optimize these for if there is only 1 subscriber (can we do this?) + //TODO: Could we maintain our own list instead of generating one on every invocation? public static async Task Raise(this Func eventHandler) { var subscriptions = eventHandler?.GetInvocationList(); @@ -42,5 +43,14 @@ namespace Discord.Extensions await (subscriptions[i] as Func).Invoke(arg1, arg2, arg3).ConfigureAwait(false); } } + public static async Task Raise(this Func eventHandler, T1 arg1, T2 arg2, T3 arg3, T4 arg4) + { + var subscriptions = eventHandler?.GetInvocationList(); + if (subscriptions != null) + { + for (int i = 0; i < subscriptions.Length; i++) + await (subscriptions[i] as Func).Invoke(arg1, arg2, arg3, arg4).ConfigureAwait(false); + } + } } } diff --git a/src/Discord.Net/Net/Rest/DefaultRestClient.cs b/src/Discord.Net/Net/Rest/DefaultRestClient.cs index 088c09f87..f870cf61e 100644 --- a/src/Discord.Net/Net/Rest/DefaultRestClient.cs +++ b/src/Discord.Net/Net/Rest/DefaultRestClient.cs @@ -92,25 +92,7 @@ namespace Discord.Net.Rest { foreach (var p in multipartParams) { -#if CSHARP7 - switch (p.Value) - { - case string value: - content.Add(new StringContent(value), p.Key); - break; - case byte[] value: - content.Add(new ByteArrayContent(value), p.Key); - break; - case Stream value: - content.Add(new StreamContent(value), p.Key); - break; - case MultipartFile value: - content.Add(new StreamContent(value.Stream), value.Filename, p.Key); - break; - default: - throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\""); - } -#else + //TODO: C# Typeswitch candidate var stringValue = p.Value as string; if (stringValue != null) { content.Add(new StringContent(stringValue), p.Key); continue; } var byteArrayValue = p.Value as byte[]; @@ -125,7 +107,6 @@ namespace Discord.Net.Rest } throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\""); -#endif } } restRequest.Content = content; diff --git a/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs b/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs index 03c965bf5..545a92d37 100644 --- a/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs +++ b/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs @@ -19,6 +19,7 @@ namespace Discord.Net.WebSockets public event Func TextMessage; private readonly ClientWebSocket _client; + private readonly SemaphoreSlim _sendLock; private Task _task; private CancellationTokenSource _cancelTokenSource; private CancellationToken _cancelToken, _parentToken; @@ -30,6 +31,7 @@ namespace Discord.Net.WebSockets _client.Options.Proxy = null; _client.Options.KeepAliveInterval = TimeSpan.Zero; + _sendLock = new SemaphoreSlim(1, 1); _cancelTokenSource = new CancellationTokenSource(); _cancelToken = CancellationToken.None; _parentToken = CancellationToken.None; @@ -82,28 +84,37 @@ namespace Discord.Net.WebSockets public async Task Send(byte[] data, int index, int count, bool isText) { - //TODO: If connection is temporarily down, retry? - int frameCount = (int)Math.Ceiling((double)count / SendChunkSize); - - for (int i = 0; i < frameCount; i++, index += SendChunkSize) + await _sendLock.WaitAsync(_cancelToken); + try { - bool isLast = i == (frameCount - 1); + //TODO: If connection is temporarily down, retry? + int frameCount = (int)Math.Ceiling((double)count / SendChunkSize); - int frameSize; - if (isLast) - frameSize = count - (i * SendChunkSize); - else - frameSize = SendChunkSize; - - try + for (int i = 0; i < frameCount; i++, index += SendChunkSize) { - await _client.SendAsync(new ArraySegment(data, index, count), isText ? WebSocketMessageType.Text : WebSocketMessageType.Binary, isLast, _cancelToken).ConfigureAwait(false); - } - catch (Win32Exception ex) when (ex.HResult == HR_TIMEOUT) - { - return; + bool isLast = i == (frameCount - 1); + + int frameSize; + if (isLast) + frameSize = count - (i * SendChunkSize); + else + frameSize = SendChunkSize; + + try + { + var type = isText ? WebSocketMessageType.Text : WebSocketMessageType.Binary; + await _client.SendAsync(new ArraySegment(data, index, count), type, isLast, _cancelToken).ConfigureAwait(false); + } + catch (Win32Exception ex) when (ex.HResult == HR_TIMEOUT) + { + return; + } } } + finally + { + _sendLock.Release(); + } } //TODO: Check this code diff --git a/src/Discord.Net/Utilities/MessageCache.cs b/src/Discord.Net/Utilities/MessageCache.cs index 4b1a35d08..991dde11f 100644 --- a/src/Discord.Net/Utilities/MessageCache.cs +++ b/src/Discord.Net/Utilities/MessageCache.cs @@ -74,7 +74,7 @@ namespace Discord { CachedMessage msg; if (_messages.TryGetValue(x, out msg)) - return msg as CachedMessage; + return msg; return null; }) .Where(x => x != null)