@@ -3,6 +3,6 @@ | |||||
"sdk": { | "sdk": { | ||||
"version": "1.0.0-beta6", | "version": "1.0.0-beta6", | ||||
"architecture": "x64", | "architecture": "x64", | ||||
"runtime": "coreclr" | |||||
"runtime": "clr" | |||||
} | } | ||||
} | } |
@@ -11,6 +11,8 @@ | |||||
<AssemblyName>Discord.Net</AssemblyName> | <AssemblyName>Discord.Net</AssemblyName> | ||||
<FileAlignment>512</FileAlignment> | <FileAlignment>512</FileAlignment> | ||||
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion> | <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> | ||||
<NuGetPackageImportStamp> | |||||
</NuGetPackageImportStamp> | |||||
</PropertyGroup> | </PropertyGroup> | ||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> | <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> | ||||
<DebugSymbols>true</DebugSymbols> | <DebugSymbols>true</DebugSymbols> | ||||
@@ -36,6 +38,10 @@ | |||||
<HintPath>..\..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll</HintPath> | <HintPath>..\..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll</HintPath> | ||||
<Private>True</Private> | <Private>True</Private> | ||||
</Reference> | </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" /> | ||||
<Reference Include="System.Net.Http" /> | <Reference Include="System.Net.Http" /> | ||||
</ItemGroup> | </ItemGroup> | ||||
@@ -91,8 +97,8 @@ | |||||
<Compile Include="..\Discord.Net\DiscordTextWebSocket.Events.cs"> | <Compile Include="..\Discord.Net\DiscordTextWebSocket.Events.cs"> | ||||
<Link>DiscordTextWebSocket.Events.cs</Link> | <Link>DiscordTextWebSocket.Events.cs</Link> | ||||
</Compile> | </Compile> | ||||
<Compile Include="..\Discord.Net\DiscordVoiceWebSocket.cs"> | |||||
<Link>DiscordVoiceWebSocket.cs</Link> | |||||
<Compile Include="..\Discord.Net\DiscordVoiceSocket.cs"> | |||||
<Link>DiscordVoiceSocket.cs</Link> | |||||
</Compile> | </Compile> | ||||
<Compile Include="..\Discord.Net\DiscordWebSocket.cs"> | <Compile Include="..\Discord.Net\DiscordWebSocket.cs"> | ||||
<Link>DiscordWebSocket.cs</Link> | <Link>DiscordWebSocket.cs</Link> | ||||
@@ -136,6 +142,13 @@ | |||||
<Compile Include="Properties\AssemblyInfo.cs" /> | <Compile Include="Properties\AssemblyInfo.cs" /> | ||||
</ItemGroup> | </ItemGroup> | ||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> | <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. | <!-- 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. | Other similar extension points exist, see Microsoft.Common.targets. | ||||
<Target Name="BeforeBuild"> | <Target Name="BeforeBuild"> | ||||
@@ -1,4 +1,6 @@ | |||||
<?xml version="1.0" encoding="utf-8"?> | <?xml version="1.0" encoding="utf-8"?> | ||||
<packages> | <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" /> | <package id="Newtonsoft.Json" version="7.0.1" targetFramework="net45" /> | ||||
</packages> | </packages> |
@@ -50,8 +50,7 @@ | |||||
public static readonly string VoiceIce = $"{Voice}/ice"; | public static readonly string VoiceIce = $"{Voice}/ice"; | ||||
//Web Sockets | //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 | //Website | ||||
public static string InviteUrl(string code) => $"{BaseShortHttps}/{code}"; | public static string InviteUrl(string code) => $"{BaseShortHttps}/{code}"; | ||||
@@ -11,9 +11,9 @@ namespace Discord.API.Models | |||||
public sealed class Ready | public sealed class Ready | ||||
{ | { | ||||
[JsonProperty(PropertyName = "ssrc")] | [JsonProperty(PropertyName = "ssrc")] | ||||
public int SSRC; | |||||
public uint SSRC; | |||||
[JsonProperty(PropertyName = "port")] | [JsonProperty(PropertyName = "port")] | ||||
public int Port; | |||||
public ushort Port; | |||||
[JsonProperty(PropertyName = "modes")] | [JsonProperty(PropertyName = "modes")] | ||||
public string[] Modes; | public string[] Modes; | ||||
[JsonProperty(PropertyName = "heartbeat_interval")] | [JsonProperty(PropertyName = "heartbeat_interval")] | ||||
@@ -19,7 +19,7 @@ namespace Discord | |||||
{ | { | ||||
private readonly DiscordClientConfig _config; | private readonly DiscordClientConfig _config; | ||||
private readonly DiscordTextWebSocket _webSocket; | private readonly DiscordTextWebSocket _webSocket; | ||||
private readonly DiscordVoiceWebSocket _voiceWebSocket; | |||||
private readonly DiscordVoiceSocket _voiceWebSocket; | |||||
private readonly ManualResetEventSlim _blockEvent; | private readonly ManualResetEventSlim _blockEvent; | ||||
private readonly Regex _userRegex, _channelRegex; | private readonly Regex _userRegex, _channelRegex; | ||||
private readonly MatchEvaluator _userRegexEvaluator, _channelRegexEvaluator; | private readonly MatchEvaluator _userRegexEvaluator, _channelRegexEvaluator; | ||||
@@ -287,7 +287,7 @@ namespace Discord | |||||
user => { } | user => { } | ||||
); | ); | ||||
_webSocket = new DiscordTextWebSocket(_config.WebSocketInterval); | |||||
_webSocket = new DiscordTextWebSocket(_config.ConnectionTimeout, _config.WebSocketInterval); | |||||
_webSocket.Connected += (s, e) => RaiseConnected(); | _webSocket.Connected += (s, e) => RaiseConnected(); | ||||
_webSocket.Disconnected += async (s, e) => | _webSocket.Disconnected += async (s, e) => | ||||
{ | { | ||||
@@ -312,7 +312,7 @@ namespace Discord | |||||
}; | }; | ||||
_webSocket.OnDebugMessage += (s, e) => RaiseOnDebugMessage(e.Message); | _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.Connected += (s, e) => RaiseVoiceConnected(); | ||||
_voiceWebSocket.Disconnected += (s, e) => | _voiceWebSocket.Disconnected += (s, e) => | ||||
{ | { | ||||
@@ -578,7 +578,7 @@ namespace Discord | |||||
{ | { | ||||
_currentVoiceEndpoint = data.Endpoint.Split(':')[0]; | _currentVoiceEndpoint = data.Endpoint.Split(':')[0]; | ||||
_currentVoiceToken = data.Token; | _currentVoiceToken = data.Token; | ||||
await _voiceWebSocket.ConnectAsync("wss://" + _currentVoiceEndpoint); | |||||
await _voiceWebSocket.ConnectAsync(_currentVoiceEndpoint); | |||||
await _voiceWebSocket.Login(_currentVoiceServerId, UserId, SessionId, _currentVoiceToken); | await _voiceWebSocket.Login(_currentVoiceServerId, UserId, SessionId, _currentVoiceToken); | ||||
} | } | ||||
} | } | ||||
@@ -2,6 +2,10 @@ | |||||
{ | { | ||||
public class DiscordClientConfig | 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> | /// <summary> Gets or sets the time (in milliseconds) to wait after an unexpected disconnect before reconnecting. </summary> | ||||
public int ReconnectDelay { get; set; } = 1000; | public int ReconnectDelay { get; set; } = 1000; | ||||
/// <summary> Gets or sets the time (in milliseconds) to wait after an reconnect fails before retrying. </summary> | /// <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 | 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; | private ManualResetEventSlim _connectWaitOnLogin, _connectWaitOnLogin2; | ||||
public DiscordTextWebSocket(int interval) | |||||
: base(interval) | |||||
public DiscordTextWebSocket(int timeout, int interval) | |||||
: base(timeout, interval) | |||||
{ | { | ||||
_connectWaitOnLogin = new ManualResetEventSlim(false); | _connectWaitOnLogin = new ManualResetEventSlim(false); | ||||
_connectWaitOnLogin2 = new ManualResetEventSlim(false); | _connectWaitOnLogin2 = new ManualResetEventSlim(false); | ||||
@@ -40,7 +38,7 @@ namespace Discord | |||||
try | 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"); | throw new Exception("No reply from Discord server"); | ||||
} | } | ||||
catch (OperationCanceledException) | catch (OperationCanceledException) | ||||
@@ -53,7 +51,7 @@ namespace Discord | |||||
SetConnected(); | SetConnected(); | ||||
} | } | ||||
protected override void ProcessMessage(string json) | |||||
protected override Task ProcessMessage(string json) | |||||
{ | { | ||||
var msg = JsonConvert.DeserializeObject<WebSocketMessage>(json); | var msg = JsonConvert.DeserializeObject<WebSocketMessage>(json); | ||||
switch (msg.Operation) | switch (msg.Operation) | ||||
@@ -65,7 +63,7 @@ namespace Discord | |||||
var payload = (msg.Payload as JToken).ToObject<TextWebSocketEvents.Ready>(); | var payload = (msg.Payload as JToken).ToObject<TextWebSocketEvents.Ready>(); | ||||
_heartbeatInterval = payload.HeartbeatInterval; | _heartbeatInterval = payload.HeartbeatInterval; | ||||
QueueMessage(new TextWebSocketCommands.UpdateStatus()); | QueueMessage(new TextWebSocketCommands.UpdateStatus()); | ||||
QueueMessage(GetKeepAlive()); | |||||
//QueueMessage(GetKeepAlive()); | |||||
_connectWaitOnLogin.Set(); //Pre-Event | _connectWaitOnLogin.Set(); //Pre-Event | ||||
} | } | ||||
RaiseGotEvent(msg.Type, msg.Payload as JToken); | RaiseGotEvent(msg.Type, msg.Payload as JToken); | ||||
@@ -77,6 +75,11 @@ namespace Discord | |||||
RaiseOnDebugMessage("Unknown WebSocket operation ID: " + msg.Operation); | RaiseOnDebugMessage("Unknown WebSocket operation ID: " + msg.Operation); | ||||
break; | break; | ||||
} | } | ||||
#if DNXCORE | |||||
return Task.CompletedTask | |||||
#else | |||||
return Task.Delay(0); | |||||
#endif | |||||
} | } | ||||
protected override object GetKeepAlive() | 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.Text; | ||||
using System.Threading; | using System.Threading; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
using System.Net; | |||||
using System.Linq; | |||||
namespace Discord | namespace Discord | ||||
{ | { | ||||
@@ -16,8 +18,9 @@ namespace Discord | |||||
private const int SendChunkSize = 4096; | private const int SendChunkSize = 4096; | ||||
protected volatile CancellationTokenSource _disconnectToken; | protected volatile CancellationTokenSource _disconnectToken; | ||||
protected int _heartbeatInterval; | |||||
protected int _timeout, _heartbeatInterval; | |||||
protected readonly int _sendInterval; | protected readonly int _sendInterval; | ||||
protected string _host; | |||||
private volatile ClientWebSocket _webSocket; | private volatile ClientWebSocket _webSocket; | ||||
private volatile Task _tasks; | private volatile Task _tasks; | ||||
@@ -25,8 +28,9 @@ namespace Discord | |||||
private DateTime _lastHeartbeat; | private DateTime _lastHeartbeat; | ||||
private bool _isConnected; | private bool _isConnected; | ||||
public DiscordWebSocket(int interval) | |||||
public DiscordWebSocket(int timeout, int interval) | |||||
{ | { | ||||
_timeout = timeout; | |||||
_sendInterval = interval; | _sendInterval = interval; | ||||
_sendQueue = new ConcurrentQueue<byte[]>(); | _sendQueue = new ConcurrentQueue<byte[]>(); | ||||
@@ -41,10 +45,12 @@ namespace Discord | |||||
_webSocket = new ClientWebSocket(); | _webSocket = new ClientWebSocket(); | ||||
_webSocket.Options.KeepAliveInterval = TimeSpan.Zero; | _webSocket.Options.KeepAliveInterval = TimeSpan.Zero; | ||||
await _webSocket.ConnectAsync(new Uri(url), cancelToken); | |||||
await _webSocket.ConnectAsync(new Uri("wss://" + url), cancelToken); | |||||
_host = url; | |||||
OnConnect(); | OnConnect(); | ||||
_lastHeartbeat = DateTime.UtcNow; | |||||
_tasks = Task.WhenAll(CreateTasks(cancelToken)) | _tasks = Task.WhenAll(CreateTasks(cancelToken)) | ||||
.ContinueWith(x => | .ContinueWith(x => | ||||
{ | { | ||||
@@ -123,7 +129,10 @@ namespace Discord | |||||
} | } | ||||
while (!result.EndOfMessage); | 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(); | builder.Clear(); | ||||
} | } | ||||
@@ -157,16 +166,24 @@ namespace Discord | |||||
finally { _disconnectToken.Cancel(); } | finally { _disconnectToken.Cancel(); } | ||||
} | } | ||||
protected abstract void ProcessMessage(string json); | |||||
protected abstract Task ProcessMessage(string json); | |||||
protected abstract object GetKeepAlive(); | protected abstract object GetKeepAlive(); | ||||
protected void QueueMessage(object message) | 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); | _sendQueue.Enqueue(bytes); | ||||
} | } | ||||
protected Task SendMessage(object message, CancellationToken cancelToken) | 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) | protected async Task SendMessage(byte[] message, CancellationToken cancelToken) | ||||
{ | { | ||||
var frameCount = (int)Math.Ceiling((double)message.Length / SendChunkSize); | 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" | |||||
} | |||||
} | |||||
} | |||||
} | } |