From a7c4bfa5957ec23351e004ce3318b71b37199935 Mon Sep 17 00:00:00 2001 From: Brandon Smith Date: Sun, 23 Aug 2015 06:27:21 -0300 Subject: [PATCH] Started adding support for voice --- global.json | 2 +- src/Discord.Net.Net45/Discord.Net.csproj | 27 +++- src/Discord.Net.Net45/HttpException.cs | 16 --- ...etCommands.cs => TextWebSocketCommands.cs} | 2 +- ...SocketEvents.cs => TextWebSocketEvents.cs} | 3 +- .../API/Models/VoiceWebSocketCommands.cs | 53 ++++++++ .../API/Models/VoiceWebSocketEvents.cs | 12 ++ src/Discord.Net/DiscordClient.Events.cs | 6 + src/Discord.Net/DiscordClient.cs | 113 ++++++++++------ .../DiscordTextWebSocket.Events.cs | 25 ++++ src/Discord.Net/DiscordTextWebSocket.cs | 82 ++++++++++++ src/Discord.Net/DiscordVoiceWebSocket.cs | 125 ++++++++++++++++++ src/Discord.Net/DiscordWebSocket.Events.cs | 24 +--- src/Discord.Net/DiscordWebSocket.cs | 103 +++++---------- 14 files changed, 438 insertions(+), 155 deletions(-) delete mode 100644 src/Discord.Net.Net45/HttpException.cs rename src/Discord.Net/API/Models/{WebSocketCommands.cs => TextWebSocketCommands.cs} (96%) rename src/Discord.Net/API/Models/{WebSocketEvents.cs => TextWebSocketEvents.cs} (98%) create mode 100644 src/Discord.Net/API/Models/VoiceWebSocketCommands.cs create mode 100644 src/Discord.Net/API/Models/VoiceWebSocketEvents.cs create mode 100644 src/Discord.Net/DiscordTextWebSocket.Events.cs create mode 100644 src/Discord.Net/DiscordTextWebSocket.cs create mode 100644 src/Discord.Net/DiscordVoiceWebSocket.cs diff --git a/global.json b/global.json index 4a056d884..e59a3a1b5 100644 --- a/global.json +++ b/global.json @@ -3,6 +3,6 @@ "sdk": { "version": "1.0.0-beta6", "architecture": "x64", - "runtime": "coreclr" + "runtime": "clr" } } \ No newline at end of file diff --git a/src/Discord.Net.Net45/Discord.Net.csproj b/src/Discord.Net.Net45/Discord.Net.csproj index ecb4ff3f4..dbd864736 100644 --- a/src/Discord.Net.Net45/Discord.Net.csproj +++ b/src/Discord.Net.Net45/Discord.Net.csproj @@ -58,11 +58,17 @@ API\Models\Common.cs - - API\Models\WebSocketCommands.cs + + API\Models\TextWebSocketCommands.cs - - API\Models\WebSocketEvents.cs + + API\Models\TextWebSocketEvents.cs + + + API\Models\VoiceWebSocketCommands.cs + + + API\Models\VoiceWebSocketEvents.cs Channel.cs @@ -79,6 +85,15 @@ DiscordClientConfig.cs + + DiscordTextWebSocket.cs + + + DiscordTextWebSocket.Events.cs + + + DiscordVoiceWebSocket.cs + DiscordWebSocket.cs @@ -91,6 +106,9 @@ Helpers\Http.cs + + HttpException.cs + Invite.cs @@ -115,7 +133,6 @@ User.cs - diff --git a/src/Discord.Net.Net45/HttpException.cs b/src/Discord.Net.Net45/HttpException.cs deleted file mode 100644 index 3f30fff95..000000000 --- a/src/Discord.Net.Net45/HttpException.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Net; - -namespace Discord -{ - public class HttpException : Exception - { - public HttpStatusCode StatusCode { get; } - - public HttpException(HttpStatusCode statusCode) - : base($"The server responded with error {statusCode}") - { - StatusCode = statusCode; - } - } -} diff --git a/src/Discord.Net/API/Models/WebSocketCommands.cs b/src/Discord.Net/API/Models/TextWebSocketCommands.cs similarity index 96% rename from src/Discord.Net/API/Models/WebSocketCommands.cs rename to src/Discord.Net/API/Models/TextWebSocketCommands.cs index 5fc64aa52..645646566 100644 --- a/src/Discord.Net/API/Models/WebSocketCommands.cs +++ b/src/Discord.Net/API/Models/TextWebSocketCommands.cs @@ -8,7 +8,7 @@ using System.Collections.Generic; namespace Discord.API.Models { - internal static class WebSocketCommands + internal static class TextWebSocketCommands { public sealed class KeepAlive : WebSocketMessage { diff --git a/src/Discord.Net/API/Models/WebSocketEvents.cs b/src/Discord.Net/API/Models/TextWebSocketEvents.cs similarity index 98% rename from src/Discord.Net/API/Models/WebSocketEvents.cs rename to src/Discord.Net/API/Models/TextWebSocketEvents.cs index 5fa12b017..52f3923ea 100644 --- a/src/Discord.Net/API/Models/WebSocketEvents.cs +++ b/src/Discord.Net/API/Models/TextWebSocketEvents.cs @@ -3,11 +3,10 @@ #pragma warning disable CS0169 using Newtonsoft.Json; -using System; namespace Discord.API.Models { - internal static class WebSocketEvents + internal static class TextWebSocketEvents { public sealed class Ready { diff --git a/src/Discord.Net/API/Models/VoiceWebSocketCommands.cs b/src/Discord.Net/API/Models/VoiceWebSocketCommands.cs new file mode 100644 index 000000000..d508d1c6c --- /dev/null +++ b/src/Discord.Net/API/Models/VoiceWebSocketCommands.cs @@ -0,0 +1,53 @@ +//Ignore unused/unassigned variable warnings +#pragma warning disable CS0649 +#pragma warning disable CS0169 + +using Newtonsoft.Json; +using System; +using System.Collections.Generic; + +namespace Discord.API.Models +{ + internal static class VoiceWebSocketCommands + { + public sealed class KeepAlive : WebSocketMessage + { + public KeepAlive() : base(3, null) { } + } + public sealed class Login : WebSocketMessage + { + public Login() : base(0) { } + public class Data + { + [JsonProperty(PropertyName = "server_id")] + public string ServerId; + [JsonProperty(PropertyName = "user_id")] + public string UserId; + [JsonProperty(PropertyName = "session_id")] + public string SessionId; + [JsonProperty(PropertyName = "token")] + public string Token; + } + } + public sealed class Login2 : WebSocketMessage + { + public Login2() : base(1) { } + public class Data + { + public class PCData + { + [JsonProperty(PropertyName = "address")] + public string Address; + [JsonProperty(PropertyName = "port")] + public int Port; + [JsonProperty(PropertyName = "mode")] + public string Mode = "xsalsa20_poly1305"; + } + [JsonProperty(PropertyName = "protocol")] + public string Protocol = "udp"; + [JsonProperty(PropertyName = "token")] + public string Token; + } + } + } +} diff --git a/src/Discord.Net/API/Models/VoiceWebSocketEvents.cs b/src/Discord.Net/API/Models/VoiceWebSocketEvents.cs new file mode 100644 index 000000000..2aa15e28d --- /dev/null +++ b/src/Discord.Net/API/Models/VoiceWebSocketEvents.cs @@ -0,0 +1,12 @@ +//Ignore unused/unassigned variable warnings +#pragma warning disable CS0649 +#pragma warning disable CS0169 + +using Newtonsoft.Json; + +namespace Discord.API.Models +{ + internal static class VoiceWebSocketEvents + { + } +} diff --git a/src/Discord.Net/DiscordClient.Events.cs b/src/Discord.Net/DiscordClient.Events.cs index 2a5745ddb..48d727b0c 100644 --- a/src/Discord.Net/DiscordClient.Events.cs +++ b/src/Discord.Net/DiscordClient.Events.cs @@ -16,6 +16,12 @@ namespace Discord if (DebugMessage != null) DebugMessage(this, new LogMessageEventArgs(message)); } + public event EventHandler VoiceDebugMessage; + private void RaiseOnVoiceDebugMessage(string message) + { + if (VoiceDebugMessage != null) + VoiceDebugMessage(this, new LogMessageEventArgs(message)); + } //General public event EventHandler Connected; diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index 49d560080..69c4c86df 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -17,16 +17,22 @@ namespace Discord /// Provides a connection to the DiscordApp service. public partial class DiscordClient { - private DiscordClientConfig _config; - private DiscordWebSocket _webSocket; - private ManualResetEventSlim _blockEvent; - private volatile CancellationTokenSource _disconnectToken; - private volatile Task _tasks; + private readonly DiscordClientConfig _config; + private readonly DiscordTextWebSocket _webSocket; + private readonly ManualResetEventSlim _blockEvent; private readonly Regex _userRegex, _channelRegex; private readonly MatchEvaluator _userRegexEvaluator, _channelRegexEvaluator; private readonly JsonSerializer _serializer; private readonly Random _rand; + private volatile CancellationTokenSource _disconnectToken; + private volatile Task _tasks; + +#if !DNXCORE50 + private readonly DiscordVoiceWebSocket _voiceWebSocket; + private string _currentVoiceServer; +#endif + /// Returns the User object for the current logged in user. public User User { get; private set; } /// Returns the id of the current logged in user. @@ -276,7 +282,7 @@ namespace Discord user => { } ); - _webSocket = new DiscordWebSocket(_config.WebSocketInterval); + _webSocket = new DiscordTextWebSocket(_config.WebSocketInterval); _webSocket.Connected += (s, e) => RaiseConnected(); _webSocket.Disconnected += async (s, e) => { @@ -287,7 +293,8 @@ namespace Discord try { await Task.Delay(_config.ReconnectDelay); - await _webSocket.ConnectAsync(Endpoints.WebSocket_Hub, true); + await _webSocket.ConnectAsync(Endpoints.WebSocket_Hub); + await _webSocket.Login(); break; } catch (Exception ex) @@ -298,6 +305,35 @@ namespace Discord } } }; + _webSocket.OnDebugMessage += (s, e) => RaiseOnDebugMessage(e.Message); + +#if !DNXCORE50 + _voiceWebSocket = new DiscordVoiceWebSocket(_config.WebSocketInterval); + _voiceWebSocket.Connected += (s, e) => RaiseConnected(); + _voiceWebSocket.Disconnected += async (s, e) => + { + //Reconnect if we didn't cause the disconnect + RaiseDisconnected(); + while (!_disconnectToken.IsCancellationRequested) + { + try + { + await Task.Delay(_config.ReconnectDelay); + await _voiceWebSocket.ConnectAsync(Endpoints.WebSocket_Hub); + await _voiceWebSocket.Login(_currentVoiceServer, UserId, SessionId); + break; + } + catch (Exception ex) + { + RaiseOnDebugMessage($"Reconnect Failed: {ex.Message}"); + //Net is down? We can keep trying to reconnect until the user runs Disconnect() + await Task.Delay(_config.FailedReconnectDelay); + } + } + }; + _voiceWebSocket.OnDebugMessage += (s, e) => RaiseOnVoiceDebugMessage(e.Message); +#endif + _webSocket.GotEvent += (s, e) => { switch (e.Type) @@ -305,7 +341,7 @@ namespace Discord //Global case "READY": //Resync { - var data = e.Event.ToObject(_serializer); + var data = e.Event.ToObject(_serializer); _servers.Clear(); _channels.Clear(); @@ -324,21 +360,21 @@ namespace Discord //Servers case "GUILD_CREATE": { - var data = e.Event.ToObject(_serializer); + var data = e.Event.ToObject(_serializer); var server = _servers.Update(data.Id, data); try { RaiseServerCreated(server); } catch { } } break; case "GUILD_UPDATE": { - var data = e.Event.ToObject(_serializer); + var data = e.Event.ToObject(_serializer); var server = _servers.Update(data.Id, data); try { RaiseServerUpdated(server); } catch { } } break; case "GUILD_DELETE": { - var data = e.Event.ToObject(_serializer); + var data = e.Event.ToObject(_serializer); var server = _servers.Remove(data.Id); if (server != null) try { RaiseServerDestroyed(server); } catch { } @@ -348,21 +384,21 @@ namespace Discord //Channels case "CHANNEL_CREATE": { - var data = e.Event.ToObject(_serializer); + var data = e.Event.ToObject(_serializer); var channel = _channels.Update(data.Id, data.GuildId, data); try { RaiseChannelCreated(channel); } catch { } } break; case "CHANNEL_UPDATE": { - var data = e.Event.ToObject(_serializer); + var data = e.Event.ToObject(_serializer); var channel = _channels.Update(data.Id, data.GuildId, data); try { RaiseChannelUpdated(channel); } catch { } } break; case "CHANNEL_DELETE": { - var data = e.Event.ToObject(_serializer); + var data = e.Event.ToObject(_serializer); var channel = _channels.Remove(data.Id); if (channel != null) try { RaiseChannelDestroyed(channel); } catch { } @@ -372,7 +408,7 @@ namespace Discord //Members case "GUILD_MEMBER_ADD": { - var data = e.Event.ToObject(_serializer); + var data = e.Event.ToObject(_serializer); var user = _users.Update(data.User.Id, data.User); var server = _servers[data.ServerId]; if (server != null) @@ -384,7 +420,7 @@ namespace Discord break; case "GUILD_MEMBER_UPDATE": { - var data = e.Event.ToObject(_serializer); + var data = e.Event.ToObject(_serializer); var user = _users.Update(data.User.Id, data.User); var server = _servers[data.ServerId]; if (server != null) @@ -396,7 +432,7 @@ namespace Discord break; case "GUILD_MEMBER_REMOVE": { - var data = e.Event.ToObject(_serializer); + var data = e.Event.ToObject(_serializer); var server = _servers[data.ServerId]; if (server != null) { @@ -410,21 +446,21 @@ namespace Discord //Roles case "GUILD_ROLE_CREATE": { - var data = e.Event.ToObject(_serializer); + var data = e.Event.ToObject(_serializer); var role = _roles.Update(data.Role.Id, data.ServerId, data.Role); try { RaiseRoleCreated(role); } catch { } } break; case "GUILD_ROLE_UPDATE": { - var data = e.Event.ToObject(_serializer); + var data = e.Event.ToObject(_serializer); var role = _roles.Update(data.Role.Id, data.ServerId, data.Role); try { RaiseRoleUpdated(role); } catch { } } break; case "GUILD_ROLE_DELETE": { - var data = e.Event.ToObject(_serializer); + var data = e.Event.ToObject(_serializer); var role = _roles.Remove(data.RoleId); if (role != null) try { RaiseRoleDeleted(role); } catch { } @@ -434,7 +470,7 @@ namespace Discord //Bans case "GUILD_BAN_ADD": { - var data = e.Event.ToObject(_serializer); + var data = e.Event.ToObject(_serializer); var user = _users.Update(data.User.Id, data.User); var server = _servers[data.ServerId]; try { RaiseBanAdded(user, server); } catch { } @@ -442,7 +478,7 @@ namespace Discord break; case "GUILD_BAN_REMOVE": { - var data = e.Event.ToObject(_serializer); + var data = e.Event.ToObject(_serializer); var user = _users.Update(data.User.Id, data.User); var server = _servers[data.ServerId]; if (server != null && server.RemoveBan(user.Id)) @@ -455,7 +491,7 @@ namespace Discord //Messages case "MESSAGE_CREATE": { - var data = e.Event.ToObject(_serializer); + var data = e.Event.ToObject(_serializer); Message msg = null; bool wasLocal = _config.UseMessageQueue && data.Author.Id == UserId && data.Nonce != null; if (wasLocal) @@ -478,14 +514,14 @@ namespace Discord break; case "MESSAGE_UPDATE": { - var data = e.Event.ToObject(_serializer); + var data = e.Event.ToObject(_serializer); var msg = _messages.Update(data.Id, data.ChannelId, data); try { RaiseMessageUpdated(msg); } catch { } } break; case "MESSAGE_DELETE": { - var data = e.Event.ToObject(_serializer); + var data = e.Event.ToObject(_serializer); var msg = GetMessage(data.MessageId); if (msg != null) { @@ -496,7 +532,7 @@ namespace Discord break; case "MESSAGE_ACK": { - var data = e.Event.ToObject(_serializer); + var data = e.Event.ToObject(_serializer); var msg = GetMessage(data.MessageId); if (msg != null) try { RaiseMessageRead(msg); } catch { } @@ -506,7 +542,7 @@ namespace Discord //Statuses case "PRESENCE_UPDATE": { - var data = e.Event.ToObject(_serializer); + var data = e.Event.ToObject(_serializer); var user = _users.Update(data.User.Id, data.User); var server = _servers[data.ServerId]; if (server != null) @@ -518,7 +554,7 @@ namespace Discord break; case "VOICE_STATE_UPDATE": { - var data = e.Event.ToObject(_serializer); + var data = e.Event.ToObject(_serializer); var server = _servers[data.ServerId]; if (server != null) { @@ -530,7 +566,7 @@ namespace Discord break; case "TYPING_START": { - var data = e.Event.ToObject(_serializer); + var data = e.Event.ToObject(_serializer); var channel = _channels[data.ChannelId]; var user = _users[data.UserId]; if (user != null) @@ -545,7 +581,7 @@ namespace Discord //Voice case "VOICE_SERVER_UPDATE": { - var data = e.Event.ToObject(_serializer); + var data = e.Event.ToObject(_serializer); var server = _servers[data.ServerId]; try { RaiseVoiceServerUpdated(server, data.Endpoint); } catch { } } @@ -554,7 +590,7 @@ namespace Discord //Settings case "USER_UPDATE": { - var data = e.Event.ToObject(_serializer); + var data = e.Event.ToObject(_serializer); var user = _users.Update(data.Id, data); try { RaiseUserUpdated(user); } catch { } } @@ -571,7 +607,6 @@ namespace Discord break; } }; - _webSocket.OnDebugMessage += (s, e) => RaiseOnDebugMessage(e.Message); } private async Task SendAsync() @@ -822,8 +857,9 @@ namespace Discord { try { + await _webSocket.ConnectAsync(Endpoints.WebSocket_Hub); Http.Token = token; - await _webSocket.ConnectAsync(Endpoints.WebSocket_Hub, true); + await _webSocket.Login(); success = true; } catch (InvalidOperationException) //Bad Token @@ -835,27 +871,27 @@ namespace Discord if (!success && password != null) //Email/Password login { //Open websocket while we wait for login response - Task socketTask = _webSocket.ConnectAsync(Endpoints.WebSocket_Hub, false); + Task socketTask = _webSocket.ConnectAsync(Endpoints.WebSocket_Hub); var response = await DiscordAPI.Login(emailOrUsername, password); await socketTask; //Wait for websocket to finish connecting, then send token token = response.Token; Http.Token = token; - _webSocket.Login(); + await _webSocket.Login(); success = true; } if (!success && password == null) //Anonymous login { //Open websocket while we wait for login response - Task socketTask = _webSocket.ConnectAsync(Endpoints.WebSocket_Hub, false); + Task socketTask = _webSocket.ConnectAsync(Endpoints.WebSocket_Hub); var response = await DiscordAPI.LoginAnonymous(emailOrUsername); await socketTask; //Wait for websocket to finish connecting, then send token token = response.Token; Http.Token = token; - _webSocket.Login(); + await _webSocket.Login(); success = true; } if (success) @@ -868,6 +904,9 @@ namespace Discord _tasks = _tasks.ContinueWith(async x => { await _webSocket.DisconnectAsync(); +#if !DNXCORE50 + await _voiceWebSocket.DisconnectAsync(); +#endif //Clear send queue Message ignored; diff --git a/src/Discord.Net/DiscordTextWebSocket.Events.cs b/src/Discord.Net/DiscordTextWebSocket.Events.cs new file mode 100644 index 000000000..d7cbb227d --- /dev/null +++ b/src/Discord.Net/DiscordTextWebSocket.Events.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json.Linq; +using System; + +namespace Discord +{ + internal partial class DiscordTextWebSocket + { + public event EventHandler GotEvent; + public sealed class MessageEventArgs : EventArgs + { + public readonly string Type; + public readonly JToken Event; + internal MessageEventArgs(string type, JToken data) + { + Type = type; + Event = data; + } + } + private void RaiseGotEvent(string type, JToken payload) + { + if (GotEvent != null) + GotEvent(this, new MessageEventArgs(type, payload)); + } + } +} diff --git a/src/Discord.Net/DiscordTextWebSocket.cs b/src/Discord.Net/DiscordTextWebSocket.cs new file mode 100644 index 000000000..5d9fd9434 --- /dev/null +++ b/src/Discord.Net/DiscordTextWebSocket.cs @@ -0,0 +1,82 @@ +using Discord.API.Models; +using Discord.Helpers; +using Newtonsoft.Json.Linq; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord +{ + internal sealed partial class DiscordTextWebSocket : DiscordWebSocket + { + private const int ReadyTimeout = 2500; //Max time in milliseconds between connecting to Discord and receiving a READY event + + private ManualResetEventSlim _connectWaitOnLogin, _connectWaitOnLogin2; + + public DiscordTextWebSocket(int interval) + : base(interval) + { + _connectWaitOnLogin = new ManualResetEventSlim(false); + _connectWaitOnLogin2 = new ManualResetEventSlim(false); + } + + public async Task Login() + { + var cancelToken = _disconnectToken.Token; + + _connectWaitOnLogin.Reset(); + _connectWaitOnLogin2.Reset(); + + TextWebSocketCommands.Login msg = new TextWebSocketCommands.Login(); + msg.Payload.Token = Http.Token; + msg.Payload.Properties["$os"] = ""; + msg.Payload.Properties["$browser"] = ""; + msg.Payload.Properties["$device"] = "Discord.Net"; + msg.Payload.Properties["$referrer"] = ""; + msg.Payload.Properties["$referring_domain"] = ""; + await SendMessage(msg, cancelToken); + + try + { + if (!_connectWaitOnLogin.Wait(ReadyTimeout, cancelToken)) //Waiting on READY message + throw new Exception("No reply from Discord server"); + } + catch (OperationCanceledException) + { + throw new InvalidOperationException("Bad Token"); + } + try { _connectWaitOnLogin2.Wait(cancelToken); } //Waiting on READY handler + catch (OperationCanceledException) { return; } + + SetConnected(); + } + + protected override void ProcessMessage(WebSocketMessage msg) + { + switch (msg.Operation) + { + case 0: + if (msg.Type == "READY") + { + var payload = (msg.Payload as JToken).ToObject(); + _heartbeatInterval = payload.HeartbeatInterval; + QueueMessage(new TextWebSocketCommands.UpdateStatus()); + QueueMessage(new TextWebSocketCommands.KeepAlive()); + _connectWaitOnLogin.Set(); //Pre-Event + } + RaiseGotEvent(msg.Type, msg.Payload as JToken); + if (msg.Type == "READY") + _connectWaitOnLogin2.Set(); //Post-Event + break; + default: + RaiseOnDebugMessage("Unknown WebSocket operation ID: " + msg.Operation); + break; + } + } + + protected override WebSocketMessage GetKeepAlive() + { + return new TextWebSocketCommands.KeepAlive(); + } + } +} diff --git a/src/Discord.Net/DiscordVoiceWebSocket.cs b/src/Discord.Net/DiscordVoiceWebSocket.cs new file mode 100644 index 000000000..be083f053 --- /dev/null +++ b/src/Discord.Net/DiscordVoiceWebSocket.cs @@ -0,0 +1,125 @@ +#if !DNXCORE50 +using Discord.API.Models; +using Discord.Helpers; +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord +{ + internal sealed partial class DiscordVoiceWebSocket : DiscordWebSocket + { + private const int ReadyTimeout = 2500; //Max time in milliseconds between connecting to Discord and receiving a READY event + + private ManualResetEventSlim _connectWaitOnLogin, _connectWaitOnLogin2; + private UdpClient _udp; + private ConcurrentQueue _sendQueue; + + public DiscordVoiceWebSocket(int interval) + : base(interval) + { + _connectWaitOnLogin = new ManualResetEventSlim(false); + _connectWaitOnLogin2 = new ManualResetEventSlim(false); + + _udp = new UdpClient(); + _sendQueue = new ConcurrentQueue(); + } + + protected override Task[] CreateTasks(CancellationToken cancelToken) + { + return new Task[] + { + Task.Factory.StartNew(ReceiveAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).Result, + Task.Factory.StartNew(SendAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).Result, + Task.Factory.StartNew(WatcherAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).Result + }.Concat(base.CreateTasks(cancelToken)).ToArray(); + } + public async Task Login(string serverId, string userId, string sessionId) + { + var cancelToken = _disconnectToken.Token; + + _connectWaitOnLogin.Reset(); + _connectWaitOnLogin2.Reset(); + + string ip = await Http.Get("http://ipinfo.io/ip"); + + VoiceWebSocketCommands.Login msg = new VoiceWebSocketCommands.Login(); + msg.Payload.Token = Http.Token; + msg.Payload.ServerId = serverId; + msg.Payload.UserId = userId; + msg.Payload.SessionId = sessionId; + await SendMessage(msg, cancelToken); + + try + { + if (!_connectWaitOnLogin.Wait(ReadyTimeout, cancelToken)) //Waiting on READY message + throw new Exception("No reply from Discord server"); + } + catch (OperationCanceledException) + { + throw new InvalidOperationException("Bad Token"); + } + try { _connectWaitOnLogin2.Wait(cancelToken); } //Waiting on READY handler + catch (OperationCanceledException) { return; } + + SetConnected(); + } + + private async Task ReceiveAsync() + { + var cancelToken = _disconnectToken.Token; + + try + { + while (!cancelToken.IsCancellationRequested) + { + var result = await _udp.ReceiveAsync(); + ProcessUdpMessage(result); + } + } + catch { } + finally { _disconnectToken.Cancel(); } + } + private async Task SendAsync() + { + var cancelToken = _disconnectToken.Token; + try + { + byte[] bytes; + while (!cancelToken.IsCancellationRequested) + { + while (_sendQueue.TryDequeue(out bytes)) + await SendMessage(bytes, cancelToken); + await Task.Delay(_sendInterval); + } + } + catch { } + finally { _disconnectToken.Cancel(); } + } + private async Task WatcherAsync() + { + try + { + await Task.Delay(-1, _disconnectToken.Token); + } + catch (OperationCanceledException) { } + _udp.Close(); + } + + protected override void ProcessMessage(WebSocketMessage msg) + { + } + private void ProcessUdpMessage(UdpReceiveResult msg) + { + } + + protected override WebSocketMessage GetKeepAlive() + { + return new VoiceWebSocketCommands.KeepAlive(); + } + } +} +#endif \ No newline at end of file diff --git a/src/Discord.Net/DiscordWebSocket.Events.cs b/src/Discord.Net/DiscordWebSocket.Events.cs index cb3c175bc..c6d14ef90 100644 --- a/src/Discord.Net/DiscordWebSocket.Events.cs +++ b/src/Discord.Net/DiscordWebSocket.Events.cs @@ -1,9 +1,8 @@ -using Newtonsoft.Json.Linq; -using System; +using System; namespace Discord { - internal partial class DiscordWebSocket + internal abstract partial class DiscordWebSocket { public event EventHandler Connected; private void RaiseConnected() @@ -19,25 +18,8 @@ namespace Discord Disconnected(this, EventArgs.Empty); } - public event EventHandler GotEvent; - public sealed class MessageEventArgs : EventArgs - { - public readonly string Type; - public readonly JToken Event; - internal MessageEventArgs(string type, JToken data) - { - Type = type; - Event = data; - } - } - private void RaiseGotEvent(string type, JToken payload) - { - if (GotEvent != null) - GotEvent(this, new MessageEventArgs(type, payload)); - } - public event EventHandler OnDebugMessage; - private void RaiseOnDebugMessage(string message) + protected void RaiseOnDebugMessage(string message) { if (OnDebugMessage != null) OnDebugMessage(this, new DiscordClient.LogMessageEventArgs(message)); diff --git a/src/Discord.Net/DiscordWebSocket.cs b/src/Discord.Net/DiscordWebSocket.cs index bbce2c0c3..0ec80e678 100644 --- a/src/Discord.Net/DiscordWebSocket.cs +++ b/src/Discord.Net/DiscordWebSocket.cs @@ -1,5 +1,4 @@ using Discord.API.Models; -using Discord.Helpers; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; @@ -11,31 +10,29 @@ using System.Threading.Tasks; namespace Discord { - internal sealed partial class DiscordWebSocket : IDisposable + internal abstract partial class DiscordWebSocket : IDisposable { private const int ReceiveChunkSize = 4096; private const int SendChunkSize = 4096; - private const int ReadyTimeout = 2500; //Max time in milliseconds between connecting to Discord and receiving a READY event + + protected volatile CancellationTokenSource _disconnectToken; + protected int _heartbeatInterval; + protected readonly int _sendInterval; private volatile ClientWebSocket _webSocket; - private volatile CancellationTokenSource _disconnectToken; private volatile Task _tasks; private ConcurrentQueue _sendQueue; - private int _heartbeatInterval, _sendInterval; private DateTime _lastHeartbeat; - private ManualResetEventSlim _connectWaitOnLogin, _connectWaitOnLogin2; private bool _isConnected; public DiscordWebSocket(int interval) { _sendInterval = interval; - _connectWaitOnLogin = new ManualResetEventSlim(false); - _connectWaitOnLogin2 = new ManualResetEventSlim(false); - + _sendQueue = new ConcurrentQueue(); } - public async Task ConnectAsync(string url, bool autoLogin) + public async Task ConnectAsync(string url) { await DisconnectAsync(); @@ -46,9 +43,7 @@ namespace Discord _webSocket.Options.KeepAliveInterval = TimeSpan.Zero; await _webSocket.ConnectAsync(new Uri(url), cancelToken); - _tasks = Task.WhenAll( - await Task.Factory.StartNew(ReceiveAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default), - await Task.Factory.StartNew(SendAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default)) + _tasks = Task.WhenAll(CreateTasks(cancelToken)) .ContinueWith(x => { //Do not clean up until both tasks have ended @@ -71,48 +66,29 @@ namespace Discord _tasks = null; }); - - if (autoLogin) - Login(); - } - public void Login() + } + public async Task DisconnectAsync() { - var cancelToken = _disconnectToken.Token; - - _connectWaitOnLogin.Reset(); - _connectWaitOnLogin2.Reset(); - - WebSocketCommands.Login msg = new WebSocketCommands.Login(); - msg.Payload.Token = Http.Token; - msg.Payload.Properties["$os"] = ""; - msg.Payload.Properties["$browser"] = ""; - msg.Payload.Properties["$device"] = "Discord.Net"; - msg.Payload.Properties["$referrer"] = ""; - msg.Payload.Properties["$referring_domain"] = ""; - SendMessage(msg, _disconnectToken.Token); - - try - { - if (!_connectWaitOnLogin.Wait(ReadyTimeout, cancelToken)) //Waiting on READY message - throw new Exception("No reply from Discord server"); - } - catch (OperationCanceledException) + if (_tasks != null) { - throw new InvalidOperationException("Bad Token"); + try { _disconnectToken.Cancel(); } catch (NullReferenceException) { } + try { await _tasks; } catch (NullReferenceException) { } } - try { _connectWaitOnLogin2.Wait(cancelToken); } //Waiting on READY handler - catch (OperationCanceledException) { return; } + } + protected void SetConnected() + { _isConnected = true; RaiseConnected(); } - public async Task DisconnectAsync() + + protected virtual Task[] CreateTasks(CancellationToken cancelToken) { - if (_tasks != null) + return new Task[] { - try { _disconnectToken.Cancel(); } catch (NullReferenceException) { } - try { await _tasks; } catch (NullReferenceException) { } - } + Task.Factory.StartNew(ReceiveAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).Result, + Task.Factory.StartNew(SendAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).Result + }; } private async Task ReceiveAsync() @@ -142,25 +118,7 @@ namespace Discord while (!result.EndOfMessage); var msg = JsonConvert.DeserializeObject(builder.ToString()); - switch (msg.Operation) - { - case 0: - if (msg.Type == "READY") - { - var payload = (msg.Payload as JToken).ToObject(); - _heartbeatInterval = payload.HeartbeatInterval; - QueueMessage(new WebSocketCommands.UpdateStatus()); - QueueMessage(new WebSocketCommands.KeepAlive()); - _connectWaitOnLogin.Set(); //Pre-Event - } - RaiseGotEvent(msg.Type, msg.Payload as JToken); - if (msg.Type == "READY") - _connectWaitOnLogin2.Set(); //Post-Event - break; - default: - RaiseOnDebugMessage("Unknown WebSocket operation ID: " + msg.Operation); - break; - } + ProcessMessage(msg); builder.Clear(); } @@ -168,11 +126,10 @@ namespace Discord catch { } finally { _disconnectToken.Cancel(); } } - private async Task SendAsync() { var cancelToken = _disconnectToken.Token; - try + try { byte[] bytes; while (_webSocket.State == WebSocketState.Open && !cancelToken.IsCancellationRequested) @@ -182,7 +139,7 @@ namespace Discord DateTime now = DateTime.UtcNow; if ((now - _lastHeartbeat).TotalMilliseconds > _heartbeatInterval) { - await SendMessage(new WebSocketCommands.KeepAlive(), cancelToken); + await SendMessage(GetKeepAlive(), cancelToken); _lastHeartbeat = now; } } @@ -195,15 +152,17 @@ namespace Discord finally { _disconnectToken.Cancel(); } } - private void QueueMessage(object message) + protected abstract void ProcessMessage(WebSocketMessage msg); + protected abstract WebSocketMessage GetKeepAlive(); + + protected void QueueMessage(object message) { var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)); _sendQueue.Enqueue(bytes); } - - private Task SendMessage(object message, CancellationToken cancelToken) + protected Task SendMessage(object message, CancellationToken cancelToken) => SendMessage(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)), cancelToken); - private async Task SendMessage(byte[] message, CancellationToken cancelToken) + protected async Task SendMessage(byte[] message, CancellationToken cancelToken) { var frameCount = (int)Math.Ceiling((double)message.Length / SendChunkSize);