diff --git a/README.md b/README.md index e56886c2b..537c8c2d2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Discord.Net v0.7.0-beta1 +# Discord.Net v0.7.0 An unofficial .Net API Wrapper for the Discord client (http://discordapp.com). [Join the discussion](https://discord.gg/0SBTUU1wZTVjAMPx) on Discord. @@ -7,11 +7,13 @@ An unofficial .Net API Wrapper for the Discord client (http://discordapp.com). The Discord API is still in active development, meaning this library may break at any time without notice. Discord.Net itself is also in alpha so several functions may be unstable or not work at all. -### Features -- Server Management (Servers, Channels, Messages, Invites) -- User Moderation (Kick/Ban/Unban/Mute/Unmute/Deafen/Undeafen) -- Alpha Voice Support (Outgoing only currently) +### Current Features +- Using Discord API version 3 - Supports .Net 4.5 and DNX 4.5.1 +- Server Management (Servers, Channels, Messages, Invites, Roles, Users) +- Send/Receieve Messages (Including mentions and formatting) +- Basic Voice Support (Outgoing only, Unencrypted only) +- Command extension library (Supports permission levels) ### NuGet Packages - [Discord.Net](https://www.nuget.org/packages/Discord.Net/) @@ -26,7 +28,6 @@ client.MessageCreated += async (s, e) => await client.SendMessage(e.Message.ChannelId, e.Message.Text); }; await client.Connect("discordtest@email.com", "Password123"); -await client.AcceptInvite("channel-invite-code"); ``` ### Example (Command Client) @@ -48,11 +49,9 @@ client.CreateCommand("acceptinvite") } }); await client.Connect("discordtest@email.com", "Password123"); -await client.AcceptInvite("channel-invite-code"); - ``` ### Known Issues - Due to current Discord restrictions, private messages are blocked unless both the sender and recipient are members of the same server. -- Caches do not currently clean up when their entries are no longer referenced, and there is no cap to the message cache. For now, disconencting and reconnecting will clear all caches. +- The Message caches does not currently clean up when their entries are no longer referenced, and there is currently no cap to it. For now, disconnecting and reconnecting will clear all caches. - DNX Core 5.0 is experiencing several network-related issues and support has been temporarily dropped. \ No newline at end of file diff --git a/src/Discord.Net.Commands.Net45/Properties/AssemblyInfo.cs b/src/Discord.Net.Commands.Net45/Properties/AssemblyInfo.cs index c99664f41..21df14995 100644 --- a/src/Discord.Net.Commands.Net45/Properties/AssemblyInfo.cs +++ b/src/Discord.Net.Commands.Net45/Properties/AssemblyInfo.cs @@ -13,5 +13,5 @@ using System.Runtime.InteropServices; [assembly: ComVisible(false)] [assembly: Guid("76ea00e6-ea24-41e1-acb2-639c0313fa80")] -[assembly: AssemblyVersion("0.6.1.2")] -[assembly: AssemblyFileVersion("0.6.1.2")] +[assembly: AssemblyVersion("0.7.0.0")] +[assembly: AssemblyFileVersion("0.7.0.0")] diff --git a/src/Discord.Net.Commands/project.json b/src/Discord.Net.Commands/project.json index 9b1c708e5..239e00ee2 100644 --- a/src/Discord.Net.Commands/project.json +++ b/src/Discord.Net.Commands/project.json @@ -1,5 +1,5 @@ { - "version": "0.7.0-beta1", + "version": "0.7.0", "description": "A small Discord.Net extension to make bot creation easier.", "authors": [ "RogueException" ], "tags": [ "discord", "discordapp" ], @@ -13,7 +13,7 @@ "warningsAsErrors": true }, "dependencies": { - "Discord.Net": "0.7.0-beta1" + "Discord.Net": "0.7.0" }, "frameworks": { "net45": { }, diff --git a/src/Discord.Net.Net45/Properties/AssemblyInfo.cs b/src/Discord.Net.Net45/Properties/AssemblyInfo.cs index 5222f5d02..24dc6a56e 100644 --- a/src/Discord.Net.Net45/Properties/AssemblyInfo.cs +++ b/src/Discord.Net.Net45/Properties/AssemblyInfo.cs @@ -13,5 +13,5 @@ using System.Runtime.InteropServices; [assembly: ComVisible(false)] [assembly: Guid("76ea00e6-ea24-41e1-acb2-639c0313fa80")] -[assembly: AssemblyVersion("0.6.1.2")] -[assembly: AssemblyFileVersion("0.6.1.2")] +[assembly: AssemblyVersion("0.7.0.0")] +[assembly: AssemblyFileVersion("0.7.0.0")] diff --git a/src/Discord.Net/Collections/AsyncCollection.cs b/src/Discord.Net/Collections/AsyncCollection.cs index 9b57d7015..c6acb6b63 100644 --- a/src/Discord.Net/Collections/AsyncCollection.cs +++ b/src/Discord.Net/Collections/AsyncCollection.cs @@ -9,7 +9,7 @@ namespace Discord.Collections public abstract class AsyncCollection : IEnumerable where TValue : class { - private static readonly object _writerLock = new object(); + private readonly object _writerLock; internal class CollectionItemEventArgs : EventArgs { @@ -53,9 +53,10 @@ namespace Discord.Collections protected readonly DiscordClient _client; protected readonly ConcurrentDictionary _dictionary; - protected AsyncCollection(DiscordClient client) + protected AsyncCollection(DiscordClient client, object writerLock) { _client = client; + _writerLock = writerLock; _dictionary = new ConcurrentDictionary(); } diff --git a/src/Discord.Net/Collections/Channels.cs b/src/Discord.Net/Collections/Channels.cs index ae0e4060c..5207abb36 100644 --- a/src/Discord.Net/Collections/Channels.cs +++ b/src/Discord.Net/Collections/Channels.cs @@ -6,8 +6,8 @@ namespace Discord.Collections { public sealed class Channels : AsyncCollection { - internal Channels(DiscordClient client) - : base(client) { } + internal Channels(DiscordClient client, object writerLock) + : base(client, writerLock) { } internal Channel GetOrAdd(string id, string serverId, string recipientId = null) => GetOrAdd(id, () => new Channel(_client, id, serverId, recipientId)); internal new Channel TryRemove(string id) => base.TryRemove(id); diff --git a/src/Discord.Net/Collections/Members.cs b/src/Discord.Net/Collections/Members.cs index 2ccc502f7..282915353 100644 --- a/src/Discord.Net/Collections/Members.cs +++ b/src/Discord.Net/Collections/Members.cs @@ -6,8 +6,8 @@ namespace Discord.Collections { public sealed class Members : AsyncCollection { - internal Members(DiscordClient client) - : base(client) { } + internal Members(DiscordClient client, object writerLock) + : base(client, writerLock) { } private string GetKey(string userId, string serverId) => serverId + '_' + userId; diff --git a/src/Discord.Net/Collections/Messages.cs b/src/Discord.Net/Collections/Messages.cs index 6c59a9233..2c353322b 100644 --- a/src/Discord.Net/Collections/Messages.cs +++ b/src/Discord.Net/Collections/Messages.cs @@ -5,8 +5,8 @@ namespace Discord.Collections public sealed class Messages : AsyncCollection { private readonly MessageCleaner _msgCleaner; - internal Messages(DiscordClient client) - : base(client) + internal Messages(DiscordClient client, object writerLock) + : base(client, writerLock) { _msgCleaner = new MessageCleaner(client); } diff --git a/src/Discord.Net/Collections/Roles.cs b/src/Discord.Net/Collections/Roles.cs index 8c63a2200..10557e616 100644 --- a/src/Discord.Net/Collections/Roles.cs +++ b/src/Discord.Net/Collections/Roles.cs @@ -6,8 +6,8 @@ namespace Discord.Collections { public sealed class Roles : AsyncCollection { - internal Roles(DiscordClient client) - : base(client) { } + internal Roles(DiscordClient client, object writerLock) + : base(client, writerLock) { } internal Role GetOrAdd(string id, string serverId) => GetOrAdd(id, () => new Role(_client, id, serverId)); internal new Role TryRemove(string id) => base.TryRemove(id); diff --git a/src/Discord.Net/Collections/Servers.cs b/src/Discord.Net/Collections/Servers.cs index 85e243c6e..d1f6b9245 100644 --- a/src/Discord.Net/Collections/Servers.cs +++ b/src/Discord.Net/Collections/Servers.cs @@ -6,8 +6,8 @@ namespace Discord.Collections { public sealed class Servers : AsyncCollection { - internal Servers(DiscordClient client) - : base(client) { } + internal Servers(DiscordClient client, object writerLock) + : base(client, writerLock) { } internal Server GetOrAdd(string id) => base.GetOrAdd(id, () => new Server(_client, id)); internal new Server TryRemove(string id) => base.TryRemove(id); diff --git a/src/Discord.Net/Collections/Users.cs b/src/Discord.Net/Collections/Users.cs index 56ffb060c..11f301aaf 100644 --- a/src/Discord.Net/Collections/Users.cs +++ b/src/Discord.Net/Collections/Users.cs @@ -6,8 +6,8 @@ namespace Discord.Collections { public sealed class Users : AsyncCollection { - internal Users(DiscordClient client) - : base(client) { } + internal Users(DiscordClient client, object writerLock) + : base(client, writerLock) { } internal User GetOrAdd(string id) => GetOrAdd(id, () => new User(_client, id)); internal new User TryRemove(string id) => base.TryRemove(id); diff --git a/src/Discord.Net/DiscordClient.API.cs b/src/Discord.Net/DiscordClient.API.cs index 71b6a4e48..a949c38a2 100644 --- a/src/Discord.Net/DiscordClient.API.cs +++ b/src/Discord.Net/DiscordClient.API.cs @@ -282,7 +282,8 @@ namespace Discord var msg = _messages.GetOrAdd(model.Id, channel.Id, model.Author.Id); msg.Update(model); RaiseMessageSent(msg); - } + result[i] = msg; + } await Task.Delay(1000).ConfigureAwait(false); } return result; diff --git a/src/Discord.Net/DiscordClient.Voice.cs b/src/Discord.Net/DiscordClient.Voice.cs index 1d6fa39df..3a434a99e 100644 --- a/src/Discord.Net/DiscordClient.Voice.cs +++ b/src/Discord.Net/DiscordClient.Voice.cs @@ -6,28 +6,44 @@ namespace Discord { public partial class DiscordClient { - public Task JoinVoiceServer(string channelId) - => JoinVoiceServer(_channels[channelId]); - public async Task JoinVoiceServer(Channel channel) + public Task JoinVoiceServer(Channel channel) + => JoinVoiceServer(channel.ServerId, channel.Id); + public async Task JoinVoiceServer(string serverId, string channelId) { CheckReady(checkVoice: true); - if (channel == null) throw new ArgumentNullException(nameof(channel)); + if (serverId == null) throw new ArgumentNullException(nameof(serverId)); + if (channelId == null) throw new ArgumentNullException(nameof(channelId)); await LeaveVoiceServer().ConfigureAwait(false); - _dataSocket.SendJoinVoice(channel); - //await _voiceSocket.WaitForConnection().ConfigureAwait(false); - //TODO: Add another ManualResetSlim to wait on here, base it off of DiscordClient's setup - } + try + { + await Task.Run(() => + { + _voiceSocket.SetServer(serverId); + _dataSocket.SendJoinVoice(serverId, channelId); + _voiceSocket.WaitForConnection(); + }) + .Timeout(_config.ConnectionTimeout) + .ConfigureAwait(false); + } + catch (TaskCanceledException) + { + await LeaveVoiceServer().ConfigureAwait(false); + } + } public async Task LeaveVoiceServer() { CheckReady(checkVoice: true); - if (_voiceSocket.CurrentVoiceServerId != null) + if (_voiceSocket.State != Net.WebSockets.WebSocketState.Disconnected) { - await _voiceSocket.Disconnect().ConfigureAwait(false); - await TaskHelper.CompletedTask.ConfigureAwait(false); - _dataSocket.SendLeaveVoice(); + var serverId = _voiceSocket.CurrentVoiceServerId; + if (serverId != null) + { + await _voiceSocket.Disconnect().ConfigureAwait(false); + _dataSocket.SendLeaveVoice(serverId); + } } } @@ -43,7 +59,6 @@ namespace Discord _voiceSocket.SendPCMFrames(data, count); } - /// Clears the PCM buffer. public void ClearVoicePCM() { @@ -57,7 +72,7 @@ namespace Discord { CheckReady(checkVoice: true); - _voiceSocket.Wait(); + _voiceSocket.WaitForQueue(); await TaskHelper.CompletedTask.ConfigureAwait(false); } } diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index daf01cd45..90f5a4f64 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -32,11 +32,12 @@ namespace Discord private readonly ManualResetEvent _disconnectedEvent; private readonly ManualResetEventSlim _connectedEvent; private readonly JsonSerializer _serializer; - protected ExceptionDispatchInfo _disconnectReason; private Task _runTask; - private bool _wasDisconnectUnexpected; private string _token; + protected ExceptionDispatchInfo _disconnectReason; + private bool _wasDisconnectUnexpected; + /// Returns the id of the current logged-in user. public string CurrentUserId => _currentUserId; private string _currentUserId; @@ -95,7 +96,12 @@ namespace Discord _api = new DiscordAPIClient(_config.LogLevel, _config.APITimeout); _dataSocket = new DataWebSocket(this); _dataSocket.Connected += (s, e) => { if (_state == (int)DiscordClientState.Connecting) CompleteConnect(); }; - _dataSocket.Disconnected += async (s, e) => { RaiseDisconnected(e); if (e.WasUnexpected) await _dataSocket.Login(_token); }; + _dataSocket.Disconnected += async (s, e) => + { + RaiseDisconnected(e); + if (e.WasUnexpected) + await _dataSocket.Reconnect(_token); + }; if (_config.EnableVoice) { _voiceSocket = new VoiceWebSocket(this); @@ -116,7 +122,7 @@ namespace Discord }; _voiceSocket.IsSpeaking += (s, e) => { - if (_voiceSocket.CurrentVoiceServerId != null) + if (_voiceSocket.State == WebSocketState.Connected) { var member = _members[e.UserId, _voiceSocket.CurrentVoiceServerId]; bool value = e.IsSpeaking; @@ -131,12 +137,13 @@ namespace Discord }; } - _channels = new Channels(this); - _members = new Members(this); - _messages = new Messages(this); - _roles = new Roles(this); - _servers = new Servers(this); - _users = new Users(this); + object cacheLock = new object(); + _channels = new Channels(this, cacheLock); + _members = new Members(this, cacheLock); + _messages = new Messages(this, cacheLock); + _roles = new Roles(this, cacheLock); + _servers = new Servers(this, cacheLock); + _users = new Users(this, cacheLock); _dataSocket.LogMessage += (s, e) => RaiseOnLog(e.Severity, LogMessageSource.DataWebSocket, e.Message); if (_config.EnableVoice) @@ -581,11 +588,14 @@ namespace Discord case "VOICE_SERVER_UPDATE": { var data = e.Payload.ToObject(_serializer); - var server = _servers[data.GuildId]; - if (_config.EnableVoice) + if (data.GuildId == _voiceSocket.CurrentVoiceServerId) { - _voiceSocket.Host = "wss://" + data.Endpoint.Split(':')[0]; - await _voiceSocket.Login(data.GuildId, _currentUserId, _dataSocket.SessionId, data.Token, _cancelToken).ConfigureAwait(false); + var server = _servers[data.GuildId]; + if (_config.EnableVoice) + { + _voiceSocket.Host = "wss://" + data.Endpoint.Split(':')[0]; + await _voiceSocket.Login(_currentUserId, _dataSocket.SessionId, data.Token, _cancelToken).ConfigureAwait(false); + } } } break; @@ -637,9 +647,6 @@ namespace Discord string token; try { - var cancelToken = new CancellationTokenSource(); - cancelToken.CancelAfter(5000); - _api.CancelToken = cancelToken.Token; var response = await _api.Login(email, password).ConfigureAwait(false); token = response.Token; if (_config.LogLevel >= LogMessageSeverity.Verbose) @@ -748,14 +755,14 @@ namespace Discord //When the first task ends, make sure the rest do too await DisconnectInternal(skipAwait: true); - bool wasUnexpected = _wasDisconnectUnexpected; - _wasDisconnectUnexpected = false; - - await Cleanup(wasUnexpected).ConfigureAwait(false); + await Cleanup().ConfigureAwait(false); _runTask = null; } - private async Task Cleanup(bool wasUnexpected) + private async Task Cleanup() { + var wasDisconnectUnexpected = _wasDisconnectUnexpected; + _wasDisconnectUnexpected = false; + await _dataSocket.Disconnect().ConfigureAwait(false); if (_config.EnableVoice) await _voiceSocket.Disconnect().ConfigureAwait(false); @@ -777,7 +784,7 @@ namespace Discord _currentUserId = null; _token = null; - if (!wasUnexpected) + if (!wasDisconnectUnexpected) { _state = (int)DiscordClientState.Disconnected; _disconnectedEvent.Set(); diff --git a/src/Discord.Net/DiscordClientConfig.cs b/src/Discord.Net/DiscordClientConfig.cs index 17bc947a3..7c623ae3e 100644 --- a/src/Discord.Net/DiscordClientConfig.cs +++ b/src/Discord.Net/DiscordClientConfig.cs @@ -29,7 +29,7 @@ namespace Discord private int _messageQueueInterval = 100; /// Gets or sets the max buffer length (in milliseconds) for outgoing voice packets. This value is the target maximum but is not guaranteed, the buffer will often go slightly above this value. public int VoiceBufferLength { get { return _voiceBufferLength; } set { SetValue(ref _voiceBufferLength, value); } } - private int _voiceBufferLength = 3000; + private int _voiceBufferLength = 1000; //Experimental Features #if !DNXCORE50 diff --git a/src/Discord.Net/Net/WebSockets/Commands.cs b/src/Discord.Net/Net/WebSockets/Commands.cs index 2fe053c92..00a725a6c 100644 --- a/src/Discord.Net/Net/WebSockets/Commands.cs +++ b/src/Discord.Net/Net/WebSockets/Commands.cs @@ -14,9 +14,8 @@ namespace Discord.Net.WebSockets public sealed class KeepAlive : WebSocketMessage { public KeepAlive() : base(1, GetTimestamp()) { } - private static DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - private static ulong GetTimestamp() - => (ulong)(DateTime.UtcNow - epoch).TotalMilliseconds; + private static readonly DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + private static ulong GetTimestamp() => (ulong)(DateTime.UtcNow - epoch).TotalMilliseconds; } public sealed class Login : WebSocketMessage { diff --git a/src/Discord.Net/Net/WebSockets/DataWebSocket.cs b/src/Discord.Net/Net/WebSockets/DataWebSocket.cs index 49da7bdb0..2f5effb9c 100644 --- a/src/Discord.Net/Net/WebSockets/DataWebSocket.cs +++ b/src/Discord.Net/Net/WebSockets/DataWebSocket.cs @@ -20,7 +20,7 @@ namespace Discord.Net.WebSockets public async Task Login(string token) { - await Connect(); + await Connect().ConfigureAwait(false); Commands.Login msg = new Commands.Login(); msg.Payload.Token = token; @@ -29,14 +29,38 @@ namespace Discord.Net.WebSockets } private async Task Redirect(string server) { - await DisconnectInternal(isUnexpected: false); - await Connect(); + await DisconnectInternal(isUnexpected: false).ConfigureAwait(false); + await Connect().ConfigureAwait(false); var resumeMsg = new Commands.Resume(); resumeMsg.Payload.SessionId = _sessionId; resumeMsg.Payload.Sequence = _lastSeq; QueueMessage(resumeMsg); } + public async Task Reconnect(string token) + { + try + { + var cancelToken = ParentCancelToken; + await Task.Delay(_client.Config.ReconnectDelay, cancelToken).ConfigureAwait(false); + while (!cancelToken.IsCancellationRequested) + { + try + { + await Login(token).ConfigureAwait(false); + break; + } + catch (OperationCanceledException) { throw; } + catch (Exception ex) + { + RaiseOnLog(LogMessageSeverity.Error, $"Reconnect failed: {ex.GetBaseException().Message}"); + //Net is down? We can keep trying to reconnect until the user runs Disconnect() + await Task.Delay(_client.Config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); + } + } + } + catch (OperationCanceledException) { } + } protected override async Task ProcessMessage(string json) { @@ -88,16 +112,17 @@ namespace Discord.Net.WebSockets return new Commands.KeepAlive(); } - public void SendJoinVoice(Channel channel) + public void SendJoinVoice(string serverId, string channelId) { var joinVoice = new Commands.JoinVoice(); - joinVoice.Payload.ServerId = channel.ServerId; - joinVoice.Payload.ChannelId = channel.Id; + joinVoice.Payload.ServerId = serverId; + joinVoice.Payload.ChannelId = channelId; QueueMessage(joinVoice); } - public void SendLeaveVoice() + public void SendLeaveVoice(string serverId) { var leaveVoice = new Commands.JoinVoice(); + leaveVoice.Payload.ServerId = serverId; QueueMessage(leaveVoice); } } diff --git a/src/Discord.Net/Net/WebSockets/VoiceWebSocket.cs b/src/Discord.Net/Net/WebSockets/VoiceWebSocket.cs index 51b3c8679..52f76a3c2 100644 --- a/src/Discord.Net/Net/WebSockets/VoiceWebSocket.cs +++ b/src/Discord.Net/Net/WebSockets/VoiceWebSocket.cs @@ -53,21 +53,24 @@ namespace Discord.Net.WebSockets _targetAudioBufferLength = client.Config.VoiceBufferLength / 20; //20 ms frames } - public async Task Login(string serverId, string userId, string sessionId, string token, CancellationToken cancelToken) + public void SetServer(string serverId) { - if (_serverId == serverId && _userId == userId && _sessionId == sessionId && _token == token) + _serverId = serverId; + } + public async Task Login(string userId, string sessionId, string token, CancellationToken cancelToken) + { + if ((WebSocketState)_state != WebSocketState.Disconnected) { //Adjust the host and tell the system to reconnect await DisconnectInternal(new Exception("Server transfer occurred."), isUnexpected: false); return; } - _serverId = serverId; _userId = userId; _sessionId = sessionId; _token = token; - await Connect(); + await Connect().ConfigureAwait(false); } public async Task Reconnect() { @@ -85,7 +88,7 @@ namespace Discord.Net.WebSockets catch (OperationCanceledException) { throw; } catch (Exception ex) { - RaiseOnLog(LogMessageSeverity.Error, $"DataSocket reconnect failed: {ex.GetBaseException().Message}"); + RaiseOnLog(LogMessageSeverity.Error, $"Reconnect failed: {ex.GetBaseException().Message}"); //Net is down? We can keep trying to reconnect until the user runs Disconnect() await Task.Delay(_client.Config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); } @@ -125,7 +128,7 @@ namespace Discord.Net.WebSockets #endif }.Concat(base.Run()).ToArray(); } - protected override Task Cleanup(bool wasUnexpected) + protected override Task Cleanup() { #if USE_THREAD _sendThread.Join(); @@ -133,16 +136,15 @@ namespace Discord.Net.WebSockets #endif ClearPCMFrames(); - if (!wasUnexpected) + if (!_wasDisconnectUnexpected) { - _serverId = null; _userId = null; _sessionId = null; _token = null; } _udp = null; - return base.Cleanup(wasUnexpected); + return base.Cleanup(); } private async Task ReceiveVoiceAsync() @@ -512,9 +514,13 @@ namespace Discord.Net.WebSockets return new VoiceCommands.KeepAlive(); } - public void Wait() + public void WaitForQueue() + { + _sendQueueEmptyWait.Wait(_cancelToken); + } + public void WaitForConnection() { - _sendQueueEmptyWait.Wait(); + _connectedEvent.Wait(); } } } diff --git a/src/Discord.Net/Net/WebSockets/WebSocket.cs b/src/Discord.Net/Net/WebSockets/WebSocket.cs index 6fc5e1731..aa12279c8 100644 --- a/src/Discord.Net/Net/WebSockets/WebSocket.cs +++ b/src/Discord.Net/Net/WebSockets/WebSocket.cs @@ -37,30 +37,33 @@ namespace Discord.Net.WebSockets protected readonly IWebSocketEngine _engine; protected readonly DiscordClient _client; protected readonly LogMessageSeverity _logLevel; + protected readonly ManualResetEventSlim _connectedEvent; - public string Host { get; set; } + protected ExceptionDispatchInfo _disconnectReason; + protected bool _wasDisconnectUnexpected; + protected WebSocketState _disconnectState; protected int _loginTimeout, _heartbeatInterval; private DateTime _lastHeartbeat; private Task _runTask; - public WebSocketState State => (WebSocketState)_state; - protected int _state; - - protected ExceptionDispatchInfo _disconnectReason; - private bool _wasDisconnectUnexpected; - public CancellationToken ParentCancelToken { get; set; } public CancellationToken CancelToken => _cancelToken; private CancellationTokenSource _cancelTokenSource; protected CancellationToken _cancelToken; + public string Host { get; set; } + + public WebSocketState State => (WebSocketState)_state; + protected int _state; + public WebSocket(DiscordClient client) { _client = client; _logLevel = client.Config.LogLevel; _loginTimeout = client.Config.ConnectionTimeout; _cancelToken = new CancellationToken(true); + _connectedEvent = new ManualResetEventSlim(false); _engine = new BuiltInWebSocketEngine(client.Config.WebSocketInterval); _engine.ProcessMessage += async (s, e) => @@ -78,9 +81,7 @@ namespace Discord.Net.WebSockets try { - await Disconnect().ConfigureAwait(false); - - _state = (int)WebSocketState.Connecting; + await Disconnect().ConfigureAwait(false); _cancelTokenSource = new CancellationTokenSource(); if (ParentCancelToken != null) @@ -91,50 +92,59 @@ namespace Discord.Net.WebSockets await _engine.Connect(Host, _cancelToken).ConfigureAwait(false); _lastHeartbeat = DateTime.UtcNow; + _state = (int)WebSocketState.Connecting; _runTask = RunTasks(); } - catch + catch (Exception ex) { - await Disconnect().ConfigureAwait(false); - throw; + await DisconnectInternal(ex, isUnexpected: false).ConfigureAwait(false); + throw; //Dont handle this exception internally, send up it upwards } } protected void CompleteConnect() { _state = (int)WebSocketState.Connected; + _connectedEvent.Set(); RaiseConnected(); } /*public Task Reconnect(CancellationToken cancelToken) => Connect(_host, _cancelToken);*/ public Task Disconnect() => DisconnectInternal(new Exception("Disconnect was requested by user."), isUnexpected: false); - protected Task DisconnectInternal(Exception ex = null, bool isUnexpected = true, bool skipAwait = false) + protected async Task DisconnectInternal(Exception ex = null, bool isUnexpected = true, bool skipAwait = false) { int oldState; bool hasWriterLock; //If in either connecting or connected state, get a lock by being the first to switch to disconnecting oldState = Interlocked.CompareExchange(ref _state, (int)WebSocketState.Disconnecting, (int)WebSocketState.Connecting); - if (oldState == (int)WebSocketState.Disconnected) return TaskHelper.CompletedTask; //Already disconnected + if (oldState == (int)WebSocketState.Disconnected) return; //Already disconnected hasWriterLock = oldState == (int)WebSocketState.Connecting; //Caused state change if (!hasWriterLock) { oldState = Interlocked.CompareExchange(ref _state, (int)WebSocketState.Disconnecting, (int)WebSocketState.Connected); - if (oldState == (int)WebSocketState.Disconnected) return TaskHelper.CompletedTask; //Already disconnected + if (oldState == (int)WebSocketState.Disconnected) return; //Already disconnected hasWriterLock = oldState == (int)WebSocketState.Connected; //Caused state change } if (hasWriterLock) { _wasDisconnectUnexpected = isUnexpected; + _disconnectState = (WebSocketState)oldState; _disconnectReason = ex != null ? ExceptionDispatchInfo.Capture(ex) : null; + + if (_disconnectState == WebSocketState.Connecting) //_runTask was never made + await Cleanup(); _cancelTokenSource.Cancel(); } if (!skipAwait) - return _runTask ?? TaskHelper.CompletedTask; + { + Task task = _runTask ?? TaskHelper.CompletedTask; + await task; + } else - return TaskHelper.CompletedTask; + await TaskHelper.CompletedTask; } protected virtual async Task RunTasks() @@ -143,19 +153,19 @@ namespace Discord.Net.WebSockets Task firstTask = Task.WhenAny(tasks); Task allTasks = Task.WhenAll(tasks); + //Wait until the first task ends/errors and capture the error try { await firstTask.ConfigureAwait(false); } catch (Exception ex) { await DisconnectInternal(ex: ex, skipAwait: true).ConfigureAwait(false); } - //When the first task ends, make sure the rest do too + //Ensure all other tasks are signaled to end. await DisconnectInternal(skipAwait: true); + + //Wait for the remaining tasks to complete try { await allTasks.ConfigureAwait(false); } catch { } - - bool wasUnexpected = _wasDisconnectUnexpected; - _wasDisconnectUnexpected = false; - await Cleanup(wasUnexpected).ConfigureAwait(false); - _runTask = null; + //Clean up state variables and raise disconnect event + await Cleanup().ConfigureAwait(false); } protected virtual Task[] Run() { @@ -164,12 +174,23 @@ namespace Discord.Net.WebSockets .Concat(new Task[] { HeartbeatAsync(cancelToken) }) .ToArray(); } - protected virtual Task Cleanup(bool wasUnexpected) + protected virtual async Task Cleanup() { + var disconnectState = _disconnectState; + _disconnectState = WebSocketState.Disconnected; + var wasDisconnectUnexpected = _wasDisconnectUnexpected; + _wasDisconnectUnexpected = false; + //Dont reset disconnectReason, we may called ThrowError() later + + await _engine.Disconnect(); _cancelTokenSource = null; - _state = (int)WebSocketState.Disconnected; - RaiseDisconnected(wasUnexpected, _disconnectReason?.SourceException); - return _engine.Disconnect(); + var oldState = _state; + _state = (int)WebSocketState.Disconnected; + _runTask = null; + _connectedEvent.Reset(); + + if (disconnectState == WebSocketState.Connected) + RaiseDisconnected(wasDisconnectUnexpected, _disconnectReason?.SourceException); } protected abstract Task ProcessMessage(string json); diff --git a/src/Discord.Net/project.json b/src/Discord.Net/project.json index 919a264e6..69ab0527e 100644 --- a/src/Discord.Net/project.json +++ b/src/Discord.Net/project.json @@ -1,5 +1,5 @@ { - "version": "0.7.0-beta1", + "version": "0.7.0", "description": "An unofficial .Net API wrapper for the Discord client.", "authors": [ "RogueException" ], "tags": [ "discord", "discordapp" ],