| @@ -3,6 +3,6 @@ | |||
| "sdk": { | |||
| "version": "1.0.0-beta6", | |||
| "architecture": "x64", | |||
| "runtime": "coreclr" | |||
| "runtime": "clr" | |||
| } | |||
| } | |||
| @@ -11,6 +11,8 @@ | |||
| <AssemblyName>Discord.Net</AssemblyName> | |||
| <FileAlignment>512</FileAlignment> | |||
| <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> | |||
| <NuGetPackageImportStamp> | |||
| </NuGetPackageImportStamp> | |||
| </PropertyGroup> | |||
| <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> | |||
| <DebugSymbols>true</DebugSymbols> | |||
| @@ -36,6 +38,10 @@ | |||
| <HintPath>..\..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll</HintPath> | |||
| <Private>True</Private> | |||
| </Reference> | |||
| <Reference Include="Sodium, Version=0.8.0.0, Culture=neutral, processorArchitecture=MSIL"> | |||
| <HintPath>..\..\packages\libsodium-net.0.8.0\lib\Net40\Sodium.dll</HintPath> | |||
| <Private>True</Private> | |||
| </Reference> | |||
| <Reference Include="System" /> | |||
| <Reference Include="System.Net.Http" /> | |||
| </ItemGroup> | |||
| @@ -91,8 +97,8 @@ | |||
| <Compile Include="..\Discord.Net\DiscordTextWebSocket.Events.cs"> | |||
| <Link>DiscordTextWebSocket.Events.cs</Link> | |||
| </Compile> | |||
| <Compile Include="..\Discord.Net\DiscordVoiceWebSocket.cs"> | |||
| <Link>DiscordVoiceWebSocket.cs</Link> | |||
| <Compile Include="..\Discord.Net\DiscordVoiceSocket.cs"> | |||
| <Link>DiscordVoiceSocket.cs</Link> | |||
| </Compile> | |||
| <Compile Include="..\Discord.Net\DiscordWebSocket.cs"> | |||
| <Link>DiscordWebSocket.cs</Link> | |||
| @@ -136,6 +142,13 @@ | |||
| <Compile Include="Properties\AssemblyInfo.cs" /> | |||
| </ItemGroup> | |||
| <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> | |||
| <Import Project="..\..\packages\Baseclass.Contrib.Nuget.Output.1.0.0\build\net40\Baseclass.Contrib.Nuget.Output.targets" Condition="Exists('..\..\packages\Baseclass.Contrib.Nuget.Output.1.0.0\build\net40\Baseclass.Contrib.Nuget.Output.targets')" /> | |||
| <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild"> | |||
| <PropertyGroup> | |||
| <ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText> | |||
| </PropertyGroup> | |||
| <Error Condition="!Exists('..\..\packages\Baseclass.Contrib.Nuget.Output.1.0.0\build\net40\Baseclass.Contrib.Nuget.Output.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Baseclass.Contrib.Nuget.Output.1.0.0\build\net40\Baseclass.Contrib.Nuget.Output.targets'))" /> | |||
| </Target> | |||
| <!-- To modify your build process, add your task inside one of the targets below and uncomment it. | |||
| Other similar extension points exist, see Microsoft.Common.targets. | |||
| <Target Name="BeforeBuild"> | |||
| @@ -1,4 +1,6 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | |||
| <packages> | |||
| <package id="Baseclass.Contrib.Nuget.Output" version="1.0.0" targetFramework="net45" /> | |||
| <package id="libsodium-net" version="0.8.0" targetFramework="net45" /> | |||
| <package id="Newtonsoft.Json" version="7.0.1" targetFramework="net45" /> | |||
| </packages> | |||
| @@ -50,8 +50,7 @@ | |||
| public static readonly string VoiceIce = $"{Voice}/ice"; | |||
| //Web Sockets | |||
| public static readonly string BaseWss = "wss://" + BaseUrl; | |||
| public static readonly string WebSocket_Hub = $"{BaseWss}/hub"; | |||
| public static readonly string WebSocket_Hub = $"{BaseUrl}/hub"; | |||
| //Website | |||
| public static string InviteUrl(string code) => $"{BaseShortHttps}/{code}"; | |||
| @@ -11,9 +11,9 @@ namespace Discord.API.Models | |||
| public sealed class Ready | |||
| { | |||
| [JsonProperty(PropertyName = "ssrc")] | |||
| public int SSRC; | |||
| public uint SSRC; | |||
| [JsonProperty(PropertyName = "port")] | |||
| public int Port; | |||
| public ushort Port; | |||
| [JsonProperty(PropertyName = "modes")] | |||
| public string[] Modes; | |||
| [JsonProperty(PropertyName = "heartbeat_interval")] | |||
| @@ -19,7 +19,7 @@ namespace Discord | |||
| { | |||
| private readonly DiscordClientConfig _config; | |||
| private readonly DiscordTextWebSocket _webSocket; | |||
| private readonly DiscordVoiceWebSocket _voiceWebSocket; | |||
| private readonly DiscordVoiceSocket _voiceWebSocket; | |||
| private readonly ManualResetEventSlim _blockEvent; | |||
| private readonly Regex _userRegex, _channelRegex; | |||
| private readonly MatchEvaluator _userRegexEvaluator, _channelRegexEvaluator; | |||
| @@ -287,7 +287,7 @@ namespace Discord | |||
| user => { } | |||
| ); | |||
| _webSocket = new DiscordTextWebSocket(_config.WebSocketInterval); | |||
| _webSocket = new DiscordTextWebSocket(_config.ConnectionTimeout, _config.WebSocketInterval); | |||
| _webSocket.Connected += (s, e) => RaiseConnected(); | |||
| _webSocket.Disconnected += async (s, e) => | |||
| { | |||
| @@ -312,7 +312,7 @@ namespace Discord | |||
| }; | |||
| _webSocket.OnDebugMessage += (s, e) => RaiseOnDebugMessage(e.Message); | |||
| _voiceWebSocket = new DiscordVoiceWebSocket(_config.WebSocketInterval); | |||
| _voiceWebSocket = new DiscordVoiceSocket(_config.VoiceConnectionTimeout, _config.WebSocketInterval); | |||
| _voiceWebSocket.Connected += (s, e) => RaiseVoiceConnected(); | |||
| _voiceWebSocket.Disconnected += (s, e) => | |||
| { | |||
| @@ -578,7 +578,7 @@ namespace Discord | |||
| { | |||
| _currentVoiceEndpoint = data.Endpoint.Split(':')[0]; | |||
| _currentVoiceToken = data.Token; | |||
| await _voiceWebSocket.ConnectAsync("wss://" + _currentVoiceEndpoint); | |||
| await _voiceWebSocket.ConnectAsync(_currentVoiceEndpoint); | |||
| await _voiceWebSocket.Login(_currentVoiceServerId, UserId, SessionId, _currentVoiceToken); | |||
| } | |||
| } | |||
| @@ -2,6 +2,10 @@ | |||
| { | |||
| public class DiscordClientConfig | |||
| { | |||
| /// <summary> Max time in milliseconds to wait for the web socket to connect. </summary> | |||
| public int ConnectionTimeout { get; set; } = 5000; | |||
| /// <summary> Max time in milliseconds to wait for the voice web socket to connect. </summary> | |||
| public int VoiceConnectionTimeout { get; set; } = 10000; | |||
| /// <summary> Gets or sets the time (in milliseconds) to wait after an unexpected disconnect before reconnecting. </summary> | |||
| public int ReconnectDelay { get; set; } = 1000; | |||
| /// <summary> Gets or sets the time (in milliseconds) to wait after an reconnect fails before retrying. </summary> | |||
| @@ -11,12 +11,10 @@ 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) | |||
| public DiscordTextWebSocket(int timeout, int interval) | |||
| : base(timeout, interval) | |||
| { | |||
| _connectWaitOnLogin = new ManualResetEventSlim(false); | |||
| _connectWaitOnLogin2 = new ManualResetEventSlim(false); | |||
| @@ -40,7 +38,7 @@ namespace Discord | |||
| try | |||
| { | |||
| if (!_connectWaitOnLogin.Wait(ReadyTimeout, cancelToken)) //Waiting on READY message | |||
| if (!_connectWaitOnLogin.Wait(_timeout, cancelToken)) //Waiting on READY message | |||
| throw new Exception("No reply from Discord server"); | |||
| } | |||
| catch (OperationCanceledException) | |||
| @@ -53,7 +51,7 @@ namespace Discord | |||
| SetConnected(); | |||
| } | |||
| protected override void ProcessMessage(string json) | |||
| protected override Task ProcessMessage(string json) | |||
| { | |||
| var msg = JsonConvert.DeserializeObject<WebSocketMessage>(json); | |||
| switch (msg.Operation) | |||
| @@ -65,7 +63,7 @@ namespace Discord | |||
| var payload = (msg.Payload as JToken).ToObject<TextWebSocketEvents.Ready>(); | |||
| _heartbeatInterval = payload.HeartbeatInterval; | |||
| QueueMessage(new TextWebSocketCommands.UpdateStatus()); | |||
| QueueMessage(GetKeepAlive()); | |||
| //QueueMessage(GetKeepAlive()); | |||
| _connectWaitOnLogin.Set(); //Pre-Event | |||
| } | |||
| RaiseGotEvent(msg.Type, msg.Payload as JToken); | |||
| @@ -77,6 +75,11 @@ namespace Discord | |||
| RaiseOnDebugMessage("Unknown WebSocket operation ID: " + msg.Operation); | |||
| break; | |||
| } | |||
| #if DNXCORE | |||
| return Task.CompletedTask | |||
| #else | |||
| return Task.Delay(0); | |||
| #endif | |||
| } | |||
| protected override object GetKeepAlive() | |||
| @@ -0,0 +1,240 @@ | |||
| using Discord.API.Models; | |||
| using Discord.Helpers; | |||
| using Newtonsoft.Json; | |||
| using Newtonsoft.Json.Linq; | |||
| using System; | |||
| using System.Collections.Concurrent; | |||
| using System.Linq; | |||
| using System.Net; | |||
| using System.Net.Sockets; | |||
| using System.Threading; | |||
| using System.Threading.Tasks; | |||
| using WebSocketMessage = Discord.API.Models.VoiceWebSocketCommands.WebSocketMessage; | |||
| namespace Discord | |||
| { | |||
| internal sealed partial class DiscordVoiceSocket : DiscordWebSocket | |||
| { | |||
| private ManualResetEventSlim _connectWaitOnLogin; | |||
| private UdpClient _udp; | |||
| private ConcurrentQueue<byte[]> _sendQueue; | |||
| private string _myIp; | |||
| private IPEndPoint _endpoint; | |||
| private byte[] _secretKey; | |||
| private string _mode; | |||
| private bool _isFirst; | |||
| public DiscordVoiceSocket(int timeout, int interval) | |||
| : base(timeout, interval) | |||
| { | |||
| _connectWaitOnLogin = new ManualResetEventSlim(false); | |||
| _sendQueue = new ConcurrentQueue<byte[]>(); | |||
| } | |||
| protected override void OnConnect() | |||
| { | |||
| _udp = new UdpClient(new IPEndPoint(IPAddress.Any, 0)); | |||
| _udp.AllowNatTraversal(true); | |||
| _isFirst = true; | |||
| } | |||
| protected override void OnDisconnect() | |||
| { | |||
| _udp = null; | |||
| } | |||
| 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, string token) | |||
| { | |||
| var cancelToken = _disconnectToken.Token; | |||
| _connectWaitOnLogin.Reset(); | |||
| _myIp = (await Http.Get("http://ipinfo.io/ip")).Trim(); | |||
| VoiceWebSocketCommands.Login msg = new VoiceWebSocketCommands.Login(); | |||
| msg.Payload.ServerId = serverId; | |||
| msg.Payload.SessionId = sessionId; | |||
| msg.Payload.Token = token; | |||
| msg.Payload.UserId = userId; | |||
| await SendMessage(msg, cancelToken); | |||
| try | |||
| { | |||
| if (!_connectWaitOnLogin.Wait(_timeout, cancelToken)) //Waiting on JoinServer message | |||
| throw new Exception("No reply from Discord server"); | |||
| } | |||
| catch (OperationCanceledException) | |||
| { | |||
| throw new InvalidOperationException("Bad Token"); | |||
| } | |||
| 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 _udp.SendAsync(bytes, bytes.Length); | |||
| await Task.Delay(_sendInterval); | |||
| } | |||
| } | |||
| catch { } | |||
| finally { _disconnectToken.Cancel(); } | |||
| } | |||
| private async Task WatcherAsync() | |||
| { | |||
| try | |||
| { | |||
| await Task.Delay(-1, _disconnectToken.Token); | |||
| } | |||
| catch (TaskCanceledException) { } | |||
| #if DNXCORE50 | |||
| finally { _udp.Dispose(); } | |||
| #else | |||
| finally { _udp.Close(); } | |||
| #endif | |||
| } | |||
| protected override async Task ProcessMessage(string json) | |||
| { | |||
| var msg = JsonConvert.DeserializeObject<WebSocketMessage>(json); | |||
| switch (msg.Operation) | |||
| { | |||
| case 2: //READY | |||
| { | |||
| var payload = (msg.Payload as JToken).ToObject<VoiceWebSocketEvents.Ready>(); | |||
| _heartbeatInterval = payload.HeartbeatInterval; | |||
| _endpoint = new IPEndPoint((await Dns.GetHostAddressesAsync(_host)).FirstOrDefault(), payload.Port); | |||
| //_mode = payload.Modes.LastOrDefault(); | |||
| _mode = "plain"; | |||
| _udp.Connect(_endpoint); | |||
| var ssrc = payload.SSRC; | |||
| _sendQueue.Enqueue(new byte[70] { | |||
| (byte)((ssrc >> 24) & 0xFF), | |||
| (byte)((ssrc >> 16) & 0xFF), | |||
| (byte)((ssrc >> 8) & 0xFF), | |||
| (byte)((ssrc >> 0) & 0xFF), | |||
| 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, | |||
| 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, | |||
| 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, | |||
| 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, | |||
| 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 | |||
| }); | |||
| } | |||
| break; | |||
| case 4: //SESSION_DESCRIPTION | |||
| { | |||
| var payload = (msg.Payload as JToken).ToObject<VoiceWebSocketEvents.JoinServer>(); | |||
| _secretKey = payload.SecretKey; | |||
| _connectWaitOnLogin.Set(); | |||
| } | |||
| break; | |||
| default: | |||
| RaiseOnDebugMessage("Unknown WebSocket operation ID: " + msg.Operation); | |||
| break; | |||
| } | |||
| } | |||
| private void ProcessUdpMessage(UdpReceiveResult msg) | |||
| { | |||
| if (msg.Buffer.Length > 0 && msg.RemoteEndPoint.Equals(_endpoint)) | |||
| { | |||
| byte[] buffer = msg.Buffer; | |||
| int length = msg.Buffer.Length; | |||
| if (_isFirst) | |||
| { | |||
| _isFirst = false; | |||
| if (length != 70) | |||
| throw new Exception($"Unexpected message length. Expected 70, got {length}."); | |||
| int port = buffer[68] | buffer[69] << 8; | |||
| var login2 = new VoiceWebSocketCommands.Login2(); | |||
| login2.Payload.Protocol = "udp"; | |||
| login2.Payload.SocketData.Address = _myIp; | |||
| login2.Payload.SocketData.Mode = _mode; | |||
| login2.Payload.SocketData.Port = port; | |||
| QueueMessage(login2); | |||
| } | |||
| else | |||
| { | |||
| //Parse RTP Data | |||
| if (length < 12) | |||
| throw new Exception($"Unexpected message length. Expected >= 12, got {length}."); | |||
| byte flags = buffer[0]; | |||
| if (flags != 0x80) | |||
| throw new Exception("Unexpected Flags"); | |||
| byte payloadType = buffer[1]; | |||
| if (payloadType != 0x78) | |||
| throw new Exception("Unexpected Payload Type"); | |||
| ushort sequenceNumber = (ushort)((buffer[2] << 8) | buffer[3]); | |||
| uint timestamp = (uint)((buffer[4] << 24) | (buffer[5] << 16) | | |||
| (buffer[6] << 8) | (buffer[7] << 0)); | |||
| uint ssrc = (uint)((buffer[8] << 24) | (buffer[9] << 16) | | |||
| (buffer[10] << 8) | (buffer[11] << 0)); | |||
| //Decrypt | |||
| if (_mode == "xsalsa20_poly1305") | |||
| { | |||
| if (length < 36) //12 + 24 | |||
| throw new Exception($"Unexpected message length. Expected >= 36, got {length}."); | |||
| #if !DNXCORE50 | |||
| byte[] nonce = new byte[24]; //16 bytes static, 8 bytes incrementing? | |||
| Buffer.BlockCopy(buffer, 12, nonce, 0, 24); | |||
| byte[] cipherText = new byte[buffer.Length - 36]; | |||
| Buffer.BlockCopy(buffer, 36, cipherText, 0, cipherText.Length); | |||
| Sodium.SecretBox.Open(cipherText, nonce, _secretKey); | |||
| #endif | |||
| } | |||
| else //Plain | |||
| { | |||
| byte[] newBuffer = new byte[buffer.Length - 12]; | |||
| Buffer.BlockCopy(buffer, 12, newBuffer, 0, newBuffer.Length); | |||
| buffer = newBuffer; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| protected override object GetKeepAlive() | |||
| { | |||
| return new VoiceWebSocketCommands.KeepAlive(); | |||
| } | |||
| } | |||
| } | |||
| @@ -1,165 +0,0 @@ | |||
| using Discord.API.Models; | |||
| using Discord.Helpers; | |||
| using Newtonsoft.Json; | |||
| using Newtonsoft.Json.Linq; | |||
| using System; | |||
| using System.Collections.Concurrent; | |||
| using System.Linq; | |||
| using System.Net; | |||
| using System.Net.Sockets; | |||
| using System.Threading; | |||
| using System.Threading.Tasks; | |||
| using WebSocketMessage = Discord.API.Models.VoiceWebSocketCommands.WebSocketMessage; | |||
| 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; | |||
| private UdpClient _udp; | |||
| private ConcurrentQueue<byte[]> _sendQueue; | |||
| private string _ip; | |||
| public DiscordVoiceWebSocket(int interval) | |||
| : base(interval) | |||
| { | |||
| _connectWaitOnLogin = new ManualResetEventSlim(false); | |||
| _sendQueue = new ConcurrentQueue<byte[]>(); | |||
| } | |||
| protected override void OnConnect() | |||
| { | |||
| _udp = new UdpClient(0); | |||
| } | |||
| protected override void OnDisconnect() | |||
| { | |||
| _udp = null; | |||
| } | |||
| 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, string token) | |||
| { | |||
| var cancelToken = _disconnectToken.Token; | |||
| _connectWaitOnLogin.Reset(); | |||
| _ip = (await Http.Get("http://ipinfo.io/ip")).Trim(); | |||
| VoiceWebSocketCommands.Login msg = new VoiceWebSocketCommands.Login(); | |||
| msg.Payload.ServerId = serverId; | |||
| msg.Payload.SessionId = sessionId; | |||
| msg.Payload.Token = token; | |||
| msg.Payload.UserId = userId; | |||
| await SendMessage(msg, cancelToken); | |||
| try | |||
| { | |||
| if (!_connectWaitOnLogin.Wait(ReadyTimeout, cancelToken)) //Waiting on JoinServer message | |||
| throw new Exception("No reply from Discord server"); | |||
| } | |||
| catch (OperationCanceledException) | |||
| { | |||
| throw new InvalidOperationException("Bad Token"); | |||
| } | |||
| 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 (TaskCanceledException) { } | |||
| #if DNXCORE50 | |||
| finally { _udp.Dispose(); } | |||
| #else | |||
| finally { _udp.Close(); } | |||
| #endif | |||
| } | |||
| protected override void ProcessMessage(string json) | |||
| { | |||
| var msg = JsonConvert.DeserializeObject<WebSocketMessage>(json); | |||
| switch (msg.Operation) | |||
| { | |||
| case 2: | |||
| { | |||
| var payload = (msg.Payload as JToken).ToObject<VoiceWebSocketEvents.Ready>(); | |||
| _heartbeatInterval = payload.HeartbeatInterval; | |||
| var login2 = new VoiceWebSocketCommands.Login2(); | |||
| login2.Payload.Protocol = "udp"; | |||
| login2.Payload.SocketData.Address = _ip; | |||
| login2.Payload.SocketData.Mode = payload.Modes.Last(); | |||
| login2.Payload.SocketData.Port = (_udp.Client.LocalEndPoint as IPEndPoint).Port; | |||
| QueueMessage(login2); | |||
| } | |||
| break; | |||
| case 4: | |||
| { | |||
| var payload = (msg.Payload as JToken).ToObject<VoiceWebSocketEvents.JoinServer>(); | |||
| QueueMessage(GetKeepAlive()); | |||
| _connectWaitOnLogin.Set(); | |||
| } | |||
| break; | |||
| default: | |||
| RaiseOnDebugMessage("Unknown WebSocket operation ID: " + msg.Operation); | |||
| break; | |||
| } | |||
| } | |||
| private void ProcessUdpMessage(UdpReceiveResult msg) | |||
| { | |||
| System.Diagnostics.Debug.WriteLine($"Got {msg.Buffer.Length} bytes from {msg.RemoteEndPoint}."); | |||
| } | |||
| protected override object GetKeepAlive() | |||
| { | |||
| return new VoiceWebSocketCommands.KeepAlive(); | |||
| } | |||
| } | |||
| } | |||
| @@ -7,6 +7,8 @@ using System.Net.WebSockets; | |||
| using System.Text; | |||
| using System.Threading; | |||
| using System.Threading.Tasks; | |||
| using System.Net; | |||
| using System.Linq; | |||
| namespace Discord | |||
| { | |||
| @@ -16,8 +18,9 @@ namespace Discord | |||
| private const int SendChunkSize = 4096; | |||
| protected volatile CancellationTokenSource _disconnectToken; | |||
| protected int _heartbeatInterval; | |||
| protected int _timeout, _heartbeatInterval; | |||
| protected readonly int _sendInterval; | |||
| protected string _host; | |||
| private volatile ClientWebSocket _webSocket; | |||
| private volatile Task _tasks; | |||
| @@ -25,8 +28,9 @@ namespace Discord | |||
| private DateTime _lastHeartbeat; | |||
| private bool _isConnected; | |||
| public DiscordWebSocket(int interval) | |||
| public DiscordWebSocket(int timeout, int interval) | |||
| { | |||
| _timeout = timeout; | |||
| _sendInterval = interval; | |||
| _sendQueue = new ConcurrentQueue<byte[]>(); | |||
| @@ -41,10 +45,12 @@ namespace Discord | |||
| _webSocket = new ClientWebSocket(); | |||
| _webSocket.Options.KeepAliveInterval = TimeSpan.Zero; | |||
| await _webSocket.ConnectAsync(new Uri(url), cancelToken); | |||
| await _webSocket.ConnectAsync(new Uri("wss://" + url), cancelToken); | |||
| _host = url; | |||
| OnConnect(); | |||
| _lastHeartbeat = DateTime.UtcNow; | |||
| _tasks = Task.WhenAll(CreateTasks(cancelToken)) | |||
| .ContinueWith(x => | |||
| { | |||
| @@ -123,7 +129,10 @@ namespace Discord | |||
| } | |||
| while (!result.EndOfMessage); | |||
| ProcessMessage(builder.ToString()); | |||
| //TODO: Remove this | |||
| if (this is DiscordVoiceSocket) | |||
| System.Diagnostics.Debug.WriteLine(">>> " + builder.ToString()); | |||
| await ProcessMessage(builder.ToString()); | |||
| builder.Clear(); | |||
| } | |||
| @@ -157,16 +166,24 @@ namespace Discord | |||
| finally { _disconnectToken.Cancel(); } | |||
| } | |||
| protected abstract void ProcessMessage(string json); | |||
| protected abstract Task ProcessMessage(string json); | |||
| protected abstract object GetKeepAlive(); | |||
| protected void QueueMessage(object message) | |||
| { | |||
| var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)); | |||
| //TODO: Remove this | |||
| if (this is DiscordVoiceSocket) | |||
| System.Diagnostics.Debug.WriteLine("<<< " + JsonConvert.SerializeObject(message)); | |||
| var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)); | |||
| _sendQueue.Enqueue(bytes); | |||
| } | |||
| protected Task SendMessage(object message, CancellationToken cancelToken) | |||
| => SendMessage(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)), cancelToken); | |||
| { | |||
| //TODO: Remove this | |||
| if (this is DiscordVoiceSocket) | |||
| System.Diagnostics.Debug.WriteLine("<<< " + JsonConvert.SerializeObject(message)); | |||
| return SendMessage(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)), cancelToken); | |||
| } | |||
| protected async Task SendMessage(byte[] message, CancellationToken cancelToken) | |||
| { | |||
| var frameCount = (int)Math.Ceiling((double)message.Length / SendChunkSize); | |||
| @@ -1,49 +1,54 @@ | |||
| { | |||
| "version": "0.5.0-*", | |||
| "description": "An unofficial .Net API wrapper for the Discord client.", | |||
| "authors": [ "RogueException" ], | |||
| "tags": [ "discord", "discordapp" ], | |||
| "projectUrl": "https://github.com/RogueException/Discord.Net", | |||
| "licenseUrl": "http://opensource.org/licenses/MIT", | |||
| "repository": { | |||
| "type": "git", | |||
| "url": "git://github.com/RogueException/Discord.Net" | |||
| }, | |||
| "configurations": { | |||
| "FullDebug": { | |||
| "compilationOptions": { | |||
| "define": ["DEBUG","TRACE","TEST_RESPONSES"] | |||
| } | |||
| } | |||
| }, | |||
| { | |||
| "version": "0.5.0-*", | |||
| "description": "An unofficial .Net API wrapper for the Discord client.", | |||
| "authors": [ "RogueException" ], | |||
| "tags": [ "discord", "discordapp" ], | |||
| "projectUrl": "https://github.com/RogueException/Discord.Net", | |||
| "licenseUrl": "http://opensource.org/licenses/MIT", | |||
| "repository": { | |||
| "type": "git", | |||
| "url": "git://github.com/RogueException/Discord.Net" | |||
| }, | |||
| "configurations": { | |||
| "FullDebug": { | |||
| "compilationOptions": { | |||
| "define": [ "DEBUG", "TRACE", "TEST_RESPONSES" ] | |||
| } | |||
| } | |||
| }, | |||
| "dependencies": { | |||
| "Newtonsoft.Json": "7.0.1" | |||
| }, | |||
| "dependencies": { | |||
| "Newtonsoft.Json": "7.0.1" | |||
| }, | |||
| "frameworks": { | |||
| "net45": { | |||
| "dependencies": { | |||
| "Microsoft.Net.Http": "2.2.22" | |||
| } | |||
| }, | |||
| "dnx451": { | |||
| "dependencies": { | |||
| "Microsoft.Net.Http": "2.2.22" | |||
| } | |||
| }, | |||
| "dnxcore50": { | |||
| "dependencies": { | |||
| "System.Collections.Concurrent": "4.0.10", | |||
| "System.Diagnostics.Debug": "4.0.10", | |||
| "System.IO.Compression": "4.0.0", | |||
| "System.Linq": "4.0.0", | |||
| "System.Net.Requests": "4.0.10", | |||
| "System.Net.Sockets": "4.0.10-beta-23019", | |||
| "System.Net.WebSockets.Client": "4.0.0-beta-23123", | |||
| "System.Runtime": "4.0.20", | |||
| "System.Text.RegularExpressions": "4.0.10" | |||
| } | |||
| } | |||
| } | |||
| "frameworks": { | |||
| "net45": { | |||
| "dependencies": { | |||
| "Microsoft.Net.Http": "2.2.22", | |||
| "libsodium-net": "0.8.0", | |||
| "Baseclass.Contrib.Nuget.Output": "2.1.0" | |||
| } | |||
| }, | |||
| "dnx451": { | |||
| "dependencies": { | |||
| "Microsoft.Net.Http": "2.2.22", | |||
| "libsodium-net": "0.8.0", | |||
| "Baseclass.Contrib.Nuget.Output": "2.1.0" | |||
| } | |||
| }, | |||
| "dnxcore50": { | |||
| "dependencies": { | |||
| "System.Collections.Concurrent": "4.0.10", | |||
| "System.Diagnostics.Debug": "4.0.10", | |||
| "System.IO.Compression": "4.0.0", | |||
| "System.Linq": "4.0.0", | |||
| "System.Net.Requests": "4.0.10", | |||
| "System.Net.Sockets": "4.0.10-beta-23019", | |||
| "System.Net.WebSockets.Client": "4.0.0-beta-23123", | |||
| "System.Runtime": "4.0.20", | |||
| "System.Text.RegularExpressions": "4.0.10", | |||
| "System.Net.NameResolution": "4.0.0-beta-23019" | |||
| } | |||
| } | |||
| } | |||
| } | |||