|
|
@@ -15,51 +15,59 @@ namespace Discord |
|
|
|
{ |
|
|
|
private const int ReceiveChunkSize = 4096; |
|
|
|
private const int SendChunkSize = 4096; |
|
|
|
private const int ReadyTimeout = 5000; //Max time in milliseconds between connecting to Discord and receiving a READY event |
|
|
|
private const int ReadyTimeout = 2500; //Max time in milliseconds between connecting to Discord and receiving a READY event |
|
|
|
|
|
|
|
private volatile ClientWebSocket _webSocket; |
|
|
|
private volatile CancellationTokenSource _cancelToken; |
|
|
|
private volatile CancellationTokenSource _disconnectToken; |
|
|
|
private volatile Task _tasks; |
|
|
|
private ConcurrentQueue<byte[]> _sendQueue; |
|
|
|
private int _heartbeatInterval; |
|
|
|
private DateTime _lastHeartbeat; |
|
|
|
private AutoResetEvent _connectWaitOnLogin, _connectWaitOnLogin2; |
|
|
|
private ManualResetEventSlim _connectWaitOnLogin, _connectWaitOnLogin2; |
|
|
|
private bool _isConnected; |
|
|
|
|
|
|
|
public DiscordWebSocket() |
|
|
|
{ |
|
|
|
_connectWaitOnLogin = new ManualResetEventSlim(false); |
|
|
|
_connectWaitOnLogin2 = new ManualResetEventSlim(false); |
|
|
|
|
|
|
|
_sendQueue = new ConcurrentQueue<byte[]>(); |
|
|
|
} |
|
|
|
|
|
|
|
public async Task ConnectAsync(string url, bool autoLogin) |
|
|
|
{ |
|
|
|
await DisconnectAsync(); |
|
|
|
|
|
|
|
_connectWaitOnLogin = new AutoResetEvent(false); |
|
|
|
_connectWaitOnLogin2 = new AutoResetEvent(false); |
|
|
|
_sendQueue = new ConcurrentQueue<byte[]>(); |
|
|
|
_disconnectToken = new CancellationTokenSource(); |
|
|
|
var cancelToken = _disconnectToken.Token; |
|
|
|
|
|
|
|
_webSocket = new ClientWebSocket(); |
|
|
|
_webSocket.Options.KeepAliveInterval = TimeSpan.Zero; |
|
|
|
|
|
|
|
_cancelToken = new CancellationTokenSource(); |
|
|
|
var cancelToken = _cancelToken.Token; |
|
|
|
|
|
|
|
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) |
|
|
|
).ContinueWith(x => |
|
|
|
await Task.Factory.StartNew(SendAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default)) |
|
|
|
.ContinueWith(x => |
|
|
|
{ |
|
|
|
//Do not clean up until both tasks have ended |
|
|
|
_heartbeatInterval = 0; |
|
|
|
_lastHeartbeat = DateTime.MinValue; |
|
|
|
_webSocket.Dispose(); |
|
|
|
_webSocket = null; |
|
|
|
_cancelToken.Dispose(); |
|
|
|
_cancelToken = null; |
|
|
|
_disconnectToken.Dispose(); |
|
|
|
_disconnectToken = null; |
|
|
|
_tasks = null; |
|
|
|
|
|
|
|
//Clear send queue |
|
|
|
byte[] ignored; |
|
|
|
while (_sendQueue.TryDequeue(out ignored)) { } |
|
|
|
|
|
|
|
if (_isConnected) |
|
|
|
{ |
|
|
|
_isConnected = false; |
|
|
|
RaiseDisconnected(); |
|
|
|
} |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
if (autoLogin) |
|
|
@@ -67,6 +75,11 @@ namespace Discord |
|
|
|
} |
|
|
|
public void Login() |
|
|
|
{ |
|
|
|
var cancelToken = _disconnectToken.Token; |
|
|
|
|
|
|
|
_connectWaitOnLogin.Reset(); |
|
|
|
_connectWaitOnLogin2.Reset(); |
|
|
|
|
|
|
|
WebSocketCommands.Login msg = new WebSocketCommands.Login(); |
|
|
|
msg.Payload.Token = Http.Token; |
|
|
|
msg.Payload.Properties["$os"] = ""; |
|
|
@@ -74,11 +87,18 @@ namespace Discord |
|
|
|
msg.Payload.Properties["$device"] = "Discord.Net"; |
|
|
|
msg.Payload.Properties["$referrer"] = ""; |
|
|
|
msg.Payload.Properties["$referring_domain"] = ""; |
|
|
|
SendMessage(msg, _cancelToken.Token); |
|
|
|
SendMessage(msg, _disconnectToken.Token); |
|
|
|
|
|
|
|
if (!_connectWaitOnLogin.WaitOne(ReadyTimeout)) //Pre-Event |
|
|
|
throw new Exception("No reply from Discord server"); |
|
|
|
_connectWaitOnLogin2.WaitOne(); //Post-Event |
|
|
|
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"); |
|
|
|
} |
|
|
|
_connectWaitOnLogin2.Wait(cancelToken); //Waiting on READY handler |
|
|
|
|
|
|
|
_isConnected = true; |
|
|
|
RaiseConnected(); |
|
|
@@ -87,14 +107,14 @@ namespace Discord |
|
|
|
{ |
|
|
|
if (_tasks != null) |
|
|
|
{ |
|
|
|
_cancelToken.Cancel(); |
|
|
|
_disconnectToken.Cancel(); |
|
|
|
await _tasks; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private async Task ReceiveAsync() |
|
|
|
{ |
|
|
|
var cancelToken = _cancelToken.Token; |
|
|
|
var cancelToken = _disconnectToken.Token; |
|
|
|
var buffer = new byte[ReceiveChunkSize]; |
|
|
|
var builder = new StringBuilder(); |
|
|
|
|
|
|
@@ -105,7 +125,7 @@ namespace Discord |
|
|
|
WebSocketReceiveResult result; |
|
|
|
do |
|
|
|
{ |
|
|
|
result = await _webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), _cancelToken.Token); |
|
|
|
result = await _webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), _disconnectToken.Token); |
|
|
|
|
|
|
|
if (result.MessageType == WebSocketMessageType.Close) |
|
|
|
{ |
|
|
@@ -126,8 +146,8 @@ namespace Discord |
|
|
|
{ |
|
|
|
var payload = (msg.Payload as JToken).ToObject<WebSocketEvents.Ready>(); |
|
|
|
_heartbeatInterval = payload.HeartbeatInterval; |
|
|
|
SendMessage(new WebSocketCommands.UpdateStatus(), cancelToken); |
|
|
|
SendMessage(new WebSocketCommands.KeepAlive(), cancelToken); |
|
|
|
QueueMessage(new WebSocketCommands.UpdateStatus()); |
|
|
|
QueueMessage(new WebSocketCommands.KeepAlive()); |
|
|
|
_connectWaitOnLogin.Set(); //Pre-Event |
|
|
|
} |
|
|
|
RaiseGotEvent(msg.Type, msg.Payload as JToken); |
|
|
@@ -143,12 +163,12 @@ namespace Discord |
|
|
|
} |
|
|
|
} |
|
|
|
catch { } |
|
|
|
finally { _cancelToken.Cancel(); } |
|
|
|
finally { _disconnectToken.Cancel(); } |
|
|
|
} |
|
|
|
|
|
|
|
private async Task SendAsync() |
|
|
|
{ |
|
|
|
var cancelToken = _cancelToken.Token; |
|
|
|
var cancelToken = _disconnectToken.Token; |
|
|
|
try |
|
|
|
{ |
|
|
|
byte[] bytes; |
|
|
@@ -159,41 +179,46 @@ namespace Discord |
|
|
|
DateTime now = DateTime.UtcNow; |
|
|
|
if ((now - _lastHeartbeat).TotalMilliseconds > _heartbeatInterval) |
|
|
|
{ |
|
|
|
SendMessage(new WebSocketCommands.KeepAlive(), cancelToken); |
|
|
|
await SendMessage(new WebSocketCommands.KeepAlive(), cancelToken); |
|
|
|
_lastHeartbeat = now; |
|
|
|
} |
|
|
|
} |
|
|
|
while (_sendQueue.TryDequeue(out bytes)) |
|
|
|
{ |
|
|
|
var frameCount = (int)Math.Ceiling((double)bytes.Length / SendChunkSize); |
|
|
|
|
|
|
|
int offset = 0; |
|
|
|
for (var i = 0; i < frameCount; i++, offset += SendChunkSize) |
|
|
|
{ |
|
|
|
bool isLast = i == (frameCount - 1); |
|
|
|
|
|
|
|
int count; |
|
|
|
if (isLast) |
|
|
|
count = bytes.Length - (i * SendChunkSize); |
|
|
|
else |
|
|
|
count = SendChunkSize; |
|
|
|
|
|
|
|
await _webSocket.SendAsync(new ArraySegment<byte>(bytes, offset, count), WebSocketMessageType.Text, isLast, cancelToken); |
|
|
|
} |
|
|
|
} |
|
|
|
await SendMessage(bytes, cancelToken); |
|
|
|
await Task.Delay(100); |
|
|
|
} |
|
|
|
} |
|
|
|
catch { } |
|
|
|
finally { _cancelToken.Cancel(); } |
|
|
|
finally { _disconnectToken.Cancel(); } |
|
|
|
} |
|
|
|
|
|
|
|
private void SendMessage(object frame, CancellationToken token) |
|
|
|
private void QueueMessage(object message) |
|
|
|
{ |
|
|
|
var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(frame)); |
|
|
|
var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)); |
|
|
|
_sendQueue.Enqueue(bytes); |
|
|
|
} |
|
|
|
|
|
|
|
private Task SendMessage(object message, CancellationToken cancelToken) |
|
|
|
=> SendMessage(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)), cancelToken); |
|
|
|
private async Task SendMessage(byte[] message, CancellationToken cancelToken) |
|
|
|
{ |
|
|
|
var frameCount = (int)Math.Ceiling((double)message.Length / SendChunkSize); |
|
|
|
|
|
|
|
int offset = 0; |
|
|
|
for (var i = 0; i < frameCount; i++, offset += SendChunkSize) |
|
|
|
{ |
|
|
|
bool isLast = i == (frameCount - 1); |
|
|
|
|
|
|
|
int count; |
|
|
|
if (isLast) |
|
|
|
count = message.Length - (i * SendChunkSize); |
|
|
|
else |
|
|
|
count = SendChunkSize; |
|
|
|
|
|
|
|
await _webSocket.SendAsync(new ArraySegment<byte>(message, offset, count), WebSocketMessageType.Text, isLast, cancelToken); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
#region IDisposable Support |
|
|
|
private bool _isDisposed = false; |
|
|
|
|
|
|
|