diff --git a/src/Discord.Net.Commands.Net45/Discord.Net.Commands.csproj b/src/Discord.Net.Commands.Net45/Discord.Net.Commands.csproj index 7798e056b..04f5487bc 100644 --- a/src/Discord.Net.Commands.Net45/Discord.Net.Commands.csproj +++ b/src/Discord.Net.Commands.Net45/Discord.Net.Commands.csproj @@ -7,7 +7,7 @@ {1B5603B4-6F8F-4289-B945-7BAAE523D740} Library Properties - Discord.Net + Discord Discord.Net.Commands 512 v4.5 diff --git a/src/Discord.Net.Net45/Discord.Net.csproj b/src/Discord.Net.Net45/Discord.Net.csproj index 4bbf8e44a..ffcc5fd0f 100644 --- a/src/Discord.Net.Net45/Discord.Net.csproj +++ b/src/Discord.Net.Net45/Discord.Net.csproj @@ -7,7 +7,7 @@ {8D71A857-879A-4A10-859E-5FF824ED6688} Library Properties - Discord.Net + Discord Discord.Net 512 v4.5 @@ -159,6 +159,9 @@ Net\API\Endpoints.cs + + Net\API\HttpException.cs + Net\API\Requests.cs @@ -177,9 +180,6 @@ Net\API\RestClient.SharpRest.cs - - Net\HttpException.cs - Net\WebSockets\Commands.cs @@ -187,7 +187,7 @@ Net\WebSockets\DataWebSocket.cs - Net\DataWebSockets.Events.cs + Net\WebSockets\DataWebSockets.Events.cs Net\WebSockets\Events.cs @@ -216,6 +216,9 @@ Net\WebSockets\WebSocketMessage.cs + + TimeoutException.cs + diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index bdd0a13e0..9e0ad48d3 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -95,7 +95,7 @@ namespace Discord _api = new DiscordAPIClient(_config.LogLevel); _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 Reconnect(_token); }; + _dataSocket.Disconnected += async (s, e) => { RaiseDisconnected(e); if (e.WasUnexpected) await _dataSocket.Login(_token); }; if (_config.EnableVoice) { _voiceSocket = new VoiceWebSocket(this); @@ -112,7 +112,7 @@ namespace Discord } RaiseVoiceDisconnected(e); if (e.WasUnexpected) - await _voiceSocket.Reconnect(_cancelToken); + await _voiceSocket.Reconnect(); }; _voiceSocket.IsSpeaking += (s, e) => { @@ -292,6 +292,8 @@ namespace Discord } } break; + case "RESUMED": + break; //Servers case "GUILD_CREATE": @@ -358,7 +360,7 @@ namespace Discord var user = _users.GetOrAdd(data.User.Id); user.Update(data.User); if (_config.TrackActivity) - user.UpdateActivity(DateTime.UtcNow); + user.UpdateActivity(); var member = _members.GetOrAdd(data.User.Id, data.GuildId); member.Update(data); RaiseUserAdded(member); @@ -536,7 +538,7 @@ namespace Discord if (user != null) { if (_config.TrackActivity) - user.UpdateActivity(DateTime.UtcNow); + user.UpdateActivity(); if (channel != null) RaiseUserIsTyping(user, channel); } @@ -550,8 +552,8 @@ namespace Discord var server = _servers[data.GuildId]; if (_config.EnableVoice) { - string host = "wss://" + data.Endpoint.Split(':')[0]; - await _voiceSocket.Login(host, data.GuildId, _currentUserId, _dataSocket.SessionId, data.Token, _cancelToken).ConfigureAwait(false); + _voiceSocket.Host = "wss://" + data.Endpoint.Split(':')[0]; + await _voiceSocket.Login(data.GuildId, _currentUserId, _dataSocket.SessionId, data.Token, _cancelToken).ConfigureAwait(false); } } break; @@ -582,11 +584,6 @@ namespace Discord }; } - private void _dataSocket_Connected(object sender, EventArgs e) - { - throw new NotImplementedException(); - } - //Connection /// Connects to the Discord server with the provided token. public async Task Connect(string token) @@ -594,46 +591,54 @@ namespace Discord if (_state != (int)DiscordClientState.Disconnected) await Disconnect().ConfigureAwait(false); - if (_config.LogLevel >= LogMessageSeverity.Verbose) - RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Authentication, $"Using cached token."); - - await ConnectInternal(token).ConfigureAwait(false); - } + await ConnectInternal(token) + .Timeout(_config.ConnectionTimeout) + .ConfigureAwait(false); + } /// Connects to the Discord server with the provided email and password. /// Returns a token for future connections. public async Task Connect(string email, string password) { if (_state != (int)DiscordClientState.Disconnected) await Disconnect().ConfigureAwait(false); - - var response = await _api.Login(email, password).ConfigureAwait(false); - if (_config.LogLevel >= LogMessageSeverity.Verbose) - RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Authentication, "Login successful, got token."); - - return await ConnectInternal(response.Token).ConfigureAwait(false); - } - private Task Reconnect(string token) - { - if (_config.LogLevel >= LogMessageSeverity.Verbose) - RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Authentication, $"Using cached token."); - return ConnectInternal(token); + 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) + RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Authentication, "Login successful, got token."); + } + catch (TaskCanceledException) { throw new TimeoutException(); } + + return await ConnectInternal(token) + .Timeout(_config.ConnectionTimeout) + .ConfigureAwait(false); } private async Task ConnectInternal(string token) { - try + try { _disconnectedEvent.Reset(); _cancelTokenSource = new CancellationTokenSource(); _cancelToken = _cancelTokenSource.Token; - _state = (int)DiscordClientState.Connecting; - _api.Token = token; + _api.CancelToken = _cancelToken; + _token = token; + _state = (int)DiscordClientState.Connecting; + string url = (await _api.GetWebSocketEndpoint().ConfigureAwait(false)).Url; + url = "wss://gateway-besaid.discord.gg/"; if (_config.LogLevel >= LogMessageSeverity.Verbose) RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Authentication, $"Websocket endpoint: {url}"); - - await _dataSocket.Login(url, token, _cancelToken).ConfigureAwait(false); + + _dataSocket.Host = url; + _dataSocket.ParentCancelToken = _cancelToken; + await _dataSocket.Login(token).ConfigureAwait(false); _runTask = RunTasks(); @@ -641,8 +646,7 @@ namespace Discord { //Cancel if either Disconnect is called, data socket errors or timeout is reached var cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken, _dataSocket.CancelToken).Token; - if (!_connectedEvent.Wait(_config.ConnectionTimeout, cancelToken)) - throw new Exception("Operation timed out."); + _connectedEvent.Wait(cancelToken); } catch (OperationCanceledException) { @@ -656,6 +660,7 @@ namespace Discord } catch { + await Disconnect().ConfigureAwait(false); throw; } diff --git a/src/Discord.Net/Helpers/TaskHelper.cs b/src/Discord.Net/Helpers/TaskHelper.cs index 9c2fd2c77..27ff1c9ef 100644 --- a/src/Discord.Net/Helpers/TaskHelper.cs +++ b/src/Discord.Net/Helpers/TaskHelper.cs @@ -13,5 +13,26 @@ namespace Discord.Helpers CompletedTask = Task.Delay(0); #endif } + + public static async Task Timeout(this Task self, int milliseconds) + { + Task timeoutTask = Task.Delay(milliseconds); + Task finishedTask = await Task.WhenAny(self, timeoutTask); + if (finishedTask == timeoutTask) + { + throw new TimeoutException(); + } + else + await self; + } + public static async Task Timeout(this Task self, int milliseconds) + { + Task timeoutTask = Task.Delay(milliseconds); + Task finishedTask = await Task.WhenAny(self, timeoutTask).ConfigureAwait(false); + if (finishedTask == timeoutTask) + throw new TimeoutException(); + else + return await self.ConfigureAwait(false); + } } } diff --git a/src/Discord.Net/Net/API/DiscordAPIClient.cs b/src/Discord.Net/Net/API/DiscordAPIClient.cs index 426505634..b1e4b2c4b 100644 --- a/src/Discord.Net/Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/Net/API/DiscordAPIClient.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; namespace Discord.Net.API @@ -21,6 +22,12 @@ namespace Discord.Net.API get { return _token; } set { _token = value; _rest.SetToken(value); } } + private CancellationToken _cancelToken; + public CancellationToken CancelToken + { + get { return _cancelToken; } + set { _cancelToken = value; _rest.SetCancelToken(value); } + } //Auth public Task GetWebSocketEndpoint() @@ -191,5 +198,15 @@ namespace Discord.Net.API var request = new Requests.ChangeAvatar { Avatar = $"data:{type},/9j/{base64}", CurrentEmail = currentEmail, CurrentPassword = currentPassword }; return _rest.Patch(Endpoints.UserMe, request); } + + //Other + /*public Task GetUnresolvedIncidents() + { + return _rest.Get(Endpoints.StatusUnresolvedMaintenance); + } + public Task GetActiveIncidents() + { + return _rest.Get(Endpoints.StatusActiveMaintenance); + }*/ } } diff --git a/src/Discord.Net/Net/API/Endpoints.cs b/src/Discord.Net/Net/API/Endpoints.cs index 36306af68..048b75c98 100644 --- a/src/Discord.Net/Net/API/Endpoints.cs +++ b/src/Discord.Net/Net/API/Endpoints.cs @@ -1,7 +1,8 @@ namespace Discord.Net.API { internal static class Endpoints - { + { + public const string BaseStatusApi = "https://status.discordapp.com/api/v2/"; public const string BaseApi = "https://discordapp.com/api/"; //public const string Track = "track"; public const string Gateway = "gateway"; @@ -41,5 +42,8 @@ public const string Voice = "voice"; public const string VoiceRegions = "voice/regions"; public const string VoiceIce = "voice/ice"; - } + + public const string StatusActiveMaintenance = "scheduled-maintenances/active.json"; + public const string StatusUnresolvedMaintenance = "scheduled-maintenances/unresolved.json"; + } } diff --git a/src/Discord.Net/Net/HttpException.cs b/src/Discord.Net/Net/API/HttpException.cs similarity index 91% rename from src/Discord.Net/Net/HttpException.cs rename to src/Discord.Net/Net/API/HttpException.cs index 88bbcee3d..6530b109f 100644 --- a/src/Discord.Net/Net/HttpException.cs +++ b/src/Discord.Net/Net/API/HttpException.cs @@ -1,7 +1,7 @@ using System; using System.Net; -namespace Discord.Net +namespace Discord.Net.API { public class HttpException : Exception { diff --git a/src/Discord.Net/Net/WebSockets/DataWebSocket.cs b/src/Discord.Net/Net/WebSockets/DataWebSocket.cs index 883401dcb..49da7bdb0 100644 --- a/src/Discord.Net/Net/WebSockets/DataWebSocket.cs +++ b/src/Discord.Net/Net/WebSockets/DataWebSocket.cs @@ -7,8 +7,7 @@ using System.Threading.Tasks; namespace Discord.Net.WebSockets { internal partial class DataWebSocket : WebSocket - { - private string _redirectServer; + { private int _lastSeq; public string SessionId => _sessionId; @@ -18,29 +17,25 @@ namespace Discord.Net.WebSockets : base(client) { } - - public async Task Login(string host, string token, CancellationToken cancelToken) + + public async Task Login(string token) { - await base.Connect(host, cancelToken); + await Connect(); Commands.Login msg = new Commands.Login(); msg.Payload.Token = token; msg.Payload.Properties["$device"] = "Discord.Net"; QueueMessage(msg); } - - protected override Task[] Run() + private async Task Redirect(string server) { - //Send resume session if we were transferred - if (_redirectServer != null) - { - var resumeMsg = new Commands.Resume(); - resumeMsg.Payload.SessionId = _sessionId; - resumeMsg.Payload.Sequence = _lastSeq; - QueueMessage(resumeMsg); - _redirectServer = null; - } - return base.Run(); + await DisconnectInternal(isUnexpected: false); + await Connect(); + + var resumeMsg = new Commands.Resume(); + resumeMsg.Payload.SessionId = _sessionId; + resumeMsg.Payload.Sequence = _lastSeq; + QueueMessage(resumeMsg); } protected override async Task ProcessMessage(string json) @@ -54,27 +49,31 @@ namespace Discord.Net.WebSockets case 0: { JToken token = msg.Payload as JToken; - if (msg.Type == "READY") + if (msg.Type == "READY") { var payload = token.ToObject(); _sessionId = payload.SessionId; _heartbeatInterval = payload.HeartbeatInterval; QueueMessage(new Commands.UpdateStatus()); } + else if (msg.Type == "RESUMED") + { + var payload = token.ToObject(); + _heartbeatInterval = payload.HeartbeatInterval; + QueueMessage(new Commands.UpdateStatus()); + } RaiseReceivedEvent(msg.Type, token); - if (msg.Type == "READY") + if (msg.Type == "READY" || msg.Type == "RESUMED") CompleteConnect(); - /*if (_logLevel >= LogMessageSeverity.Info) - RaiseOnLog(LogMessageSeverity.Info, "Got Event: " + msg.Type);*/ } break; case 7: //Redirect { var payload = (msg.Payload as JToken).ToObject(); - _host = payload.Url; + Host = payload.Url; if (_logLevel >= LogMessageSeverity.Info) RaiseOnLog(LogMessageSeverity.Info, "Redirected to " + payload.Url); - await DisconnectInternal(new Exception("Server is redirecting."), true); + await Redirect(payload.Url); } break; default: diff --git a/src/Discord.Net/Net/WebSockets/Events.cs b/src/Discord.Net/Net/WebSockets/Events.cs index 98f52334d..e1cd54184 100644 --- a/src/Discord.Net/Net/WebSockets/Events.cs +++ b/src/Discord.Net/Net/WebSockets/Events.cs @@ -36,6 +36,11 @@ namespace Discord.Net.WebSockets [JsonProperty(PropertyName = "heartbeat_interval")] public int HeartbeatInterval; } + public sealed class Resumed + { + [JsonProperty(PropertyName = "heartbeat_interval")] + public int HeartbeatInterval; + } public sealed class Redirect { diff --git a/src/Discord.Net/Net/WebSockets/VoiceWebSocket.cs b/src/Discord.Net/Net/WebSockets/VoiceWebSocket.cs index 56bbcd83f..51b3c8679 100644 --- a/src/Discord.Net/Net/WebSockets/VoiceWebSocket.cs +++ b/src/Discord.Net/Net/WebSockets/VoiceWebSocket.cs @@ -53,13 +53,12 @@ namespace Discord.Net.WebSockets _targetAudioBufferLength = client.Config.VoiceBufferLength / 20; //20 ms frames } - public async Task Login(string host, string serverId, string userId, string sessionId, string token, CancellationToken cancelToken) + public async Task Login(string serverId, string userId, string sessionId, string token, CancellationToken cancelToken) { if (_serverId == serverId && _userId == userId && _sessionId == sessionId && _token == token) { //Adjust the host and tell the system to reconnect - _host = host; - await DisconnectInternal(new Exception("Server transfer occurred.")); + await DisconnectInternal(new Exception("Server transfer occurred."), isUnexpected: false); return; } @@ -68,18 +67,19 @@ namespace Discord.Net.WebSockets _sessionId = sessionId; _token = token; - await Connect(host, cancelToken); + await Connect(); } - public async Task Reconnect(CancellationToken cancelToken) + public async Task Reconnect() { try { - await Task.Delay(_client.Config.ReconnectDelay, cancelToken).ConfigureAwait(false); + var cancelToken = ParentCancelToken; + await Task.Delay(_client.Config.ReconnectDelay, cancelToken).ConfigureAwait(false); while (!cancelToken.IsCancellationRequested) { try { - await Connect(_host, cancelToken).ConfigureAwait(false); + await Connect().ConfigureAwait(false); break; } catch (OperationCanceledException) { throw; } @@ -295,7 +295,7 @@ namespace Discord.Net.WebSockets var payload = (msg.Payload as JToken).ToObject(); _heartbeatInterval = payload.HeartbeatInterval; _ssrc = payload.SSRC; - _endpoint = new IPEndPoint((await Dns.GetHostAddressesAsync(_host.Replace("wss://", "")).ConfigureAwait(false)).FirstOrDefault(), payload.Port); + _endpoint = new IPEndPoint((await Dns.GetHostAddressesAsync(Host.Replace("wss://", "")).ConfigureAwait(false)).FirstOrDefault(), payload.Port); //_mode = payload.Modes.LastOrDefault(); _isEncrypted = !payload.Modes.Contains("plain"); _udp.Connect(_endpoint); diff --git a/src/Discord.Net/Net/WebSockets/WebSocket.cs b/src/Discord.Net/Net/WebSockets/WebSocket.cs index 59acefdf5..6fc5e1731 100644 --- a/src/Discord.Net/Net/WebSockets/WebSocket.cs +++ b/src/Discord.Net/Net/WebSockets/WebSocket.cs @@ -38,7 +38,8 @@ namespace Discord.Net.WebSockets protected readonly DiscordClient _client; protected readonly LogMessageSeverity _logLevel; - protected string _host; + public string Host { get; set; } + protected int _loginTimeout, _heartbeatInterval; private DateTime _lastHeartbeat; private Task _runTask; @@ -49,6 +50,7 @@ namespace Discord.Net.WebSockets protected ExceptionDispatchInfo _disconnectReason; private bool _wasDisconnectUnexpected; + public CancellationToken ParentCancelToken { get; set; } public CancellationToken CancelToken => _cancelToken; private CancellationTokenSource _cancelTokenSource; protected CancellationToken _cancelToken; @@ -69,22 +71,24 @@ namespace Discord.Net.WebSockets }; } - protected virtual async Task Connect(string host, CancellationToken cancelToken) + protected virtual async Task Connect() { if (_state != (int)WebSocketState.Disconnected) throw new InvalidOperationException("Client is already connected or connecting to the server."); - try + try { await Disconnect().ConfigureAwait(false); _state = (int)WebSocketState.Connecting; _cancelTokenSource = new CancellationTokenSource(); - _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, cancelToken).Token; + if (ParentCancelToken != null) + _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, ParentCancelToken).Token; + else + _cancelToken = _cancelTokenSource.Token; - await _engine.Connect(host, _cancelToken).ConfigureAwait(false); - _host = host; + await _engine.Connect(Host, _cancelToken).ConfigureAwait(false); _lastHeartbeat = DateTime.UtcNow; _runTask = RunTasks(); diff --git a/src/Discord.Net/TimeoutException.cs b/src/Discord.Net/TimeoutException.cs new file mode 100644 index 000000000..5c35374c7 --- /dev/null +++ b/src/Discord.Net/TimeoutException.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public sealed class TimeoutException : Exception + { + internal TimeoutException() + : base("An operation has timed out.") + { + } + } +}