| @@ -110,6 +110,9 @@ | |||||
| <Compile Include="..\Discord.Net\Helpers\JsonHttpClient.cs"> | <Compile Include="..\Discord.Net\Helpers\JsonHttpClient.cs"> | ||||
| <Link>Helpers\JsonHttpClient.cs</Link> | <Link>Helpers\JsonHttpClient.cs</Link> | ||||
| </Compile> | </Compile> | ||||
| <Compile Include="..\Discord.Net\Helpers\JsonHttpClient.Events.cs"> | |||||
| <Link>Helpers\JsonHttpClient.Events.cs</Link> | |||||
| </Compile> | |||||
| <Compile Include="..\Discord.Net\HttpException.cs"> | <Compile Include="..\Discord.Net\HttpException.cs"> | ||||
| <Link>HttpException.cs</Link> | <Link>HttpException.cs</Link> | ||||
| </Compile> | </Compile> | ||||
| @@ -10,7 +10,10 @@ namespace Discord | |||||
| WebSocketRawInput, //TODO: Make Http instanced and add a rawoutput event | WebSocketRawInput, //TODO: Make Http instanced and add a rawoutput event | ||||
| WebSocketUnknownOpCode, | WebSocketUnknownOpCode, | ||||
| WebSocketUnknownEvent, | WebSocketUnknownEvent, | ||||
| VoiceOutput | |||||
| XHRRawOutput, | |||||
| XHRTiming, | |||||
| VoiceInput, | |||||
| VoiceOutput, | |||||
| } | } | ||||
| public sealed class LogMessageEventArgs : EventArgs | public sealed class LogMessageEventArgs : EventArgs | ||||
| { | { | ||||
| @@ -90,8 +90,6 @@ namespace Discord | |||||
| _blockEvent = new ManualResetEventSlim(true); | _blockEvent = new ManualResetEventSlim(true); | ||||
| _config = config ?? new DiscordClientConfig(); | _config = config ?? new DiscordClientConfig(); | ||||
| _rand = new Random(); | _rand = new Random(); | ||||
| _http = new JsonHttpClient(); | |||||
| _api = new DiscordAPI(_http); | |||||
| _serializer = new JsonSerializer(); | _serializer = new JsonSerializer(); | ||||
| #if TEST_RESPONSES | #if TEST_RESPONSES | ||||
| @@ -367,7 +365,12 @@ namespace Discord | |||||
| } | } | ||||
| ); | ); | ||||
| _webSocket = new DiscordDataSocket(this, _config.ConnectionTimeout, _config.WebSocketInterval); | |||||
| _http = new JsonHttpClient(config.EnableDebug); | |||||
| _api = new DiscordAPI(_http); | |||||
| if (_config.EnableDebug) | |||||
| _http.OnDebugMessage += (s, e) => RaiseOnDebugMessage(e.Type, e.Message); | |||||
| _webSocket = new DiscordDataSocket(this, config.ConnectionTimeout, config.WebSocketInterval, config.EnableDebug); | |||||
| _webSocket.Connected += (s, e) => RaiseConnected(); | _webSocket.Connected += (s, e) => RaiseConnected(); | ||||
| _webSocket.Disconnected += async (s, e) => | _webSocket.Disconnected += async (s, e) => | ||||
| { | { | ||||
| @@ -406,7 +409,7 @@ namespace Discord | |||||
| #if !DNXCORE50 | #if !DNXCORE50 | ||||
| if (_config.EnableVoice) | if (_config.EnableVoice) | ||||
| { | { | ||||
| _voiceWebSocket = new DiscordVoiceSocket(this, _config.VoiceConnectionTimeout, _config.WebSocketInterval); | |||||
| _voiceWebSocket = new DiscordVoiceSocket(this, _config.VoiceConnectionTimeout, _config.WebSocketInterval, config.EnableDebug); | |||||
| _voiceWebSocket.Connected += (s, e) => RaiseVoiceConnected(); | _voiceWebSocket.Connected += (s, e) => RaiseVoiceConnected(); | ||||
| _voiceWebSocket.Disconnected += async (s, e) => | _voiceWebSocket.Disconnected += async (s, e) => | ||||
| { | { | ||||
| @@ -1,5 +1,4 @@ | |||||
| using Discord.API.Models; | using Discord.API.Models; | ||||
| using Discord.Helpers; | |||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| using Newtonsoft.Json.Linq; | using Newtonsoft.Json.Linq; | ||||
| using System; | using System; | ||||
| @@ -13,8 +12,8 @@ namespace Discord | |||||
| { | { | ||||
| private readonly ManualResetEventSlim _connectWaitOnLogin, _connectWaitOnLogin2; | private readonly ManualResetEventSlim _connectWaitOnLogin, _connectWaitOnLogin2; | ||||
| public DiscordDataSocket(DiscordClient client, int timeout, int interval) | |||||
| : base(client, timeout, interval) | |||||
| public DiscordDataSocket(DiscordClient client, int timeout, int interval, bool isDebug) | |||||
| : base(client, timeout, interval, isDebug) | |||||
| { | { | ||||
| _connectWaitOnLogin = new ManualResetEventSlim(false); | _connectWaitOnLogin = new ManualResetEventSlim(false); | ||||
| _connectWaitOnLogin2 = new ManualResetEventSlim(false); | _connectWaitOnLogin2 = new ManualResetEventSlim(false); | ||||
| @@ -72,7 +71,8 @@ namespace Discord | |||||
| } | } | ||||
| break; | break; | ||||
| default: | default: | ||||
| RaiseOnDebugMessage(DebugMessageType.WebSocketUnknownOpCode, "Unknown DataSocket op: " + msg.Operation); | |||||
| if (_isDebug) | |||||
| RaiseOnDebugMessage(DebugMessageType.WebSocketUnknownOpCode, "Unknown DataSocket op: " + msg.Operation); | |||||
| break; | break; | ||||
| } | } | ||||
| #if DNXCORE | #if DNXCORE | ||||
| @@ -49,8 +49,8 @@ namespace Discord | |||||
| #endif | #endif | ||||
| #endif | #endif | ||||
| public DiscordVoiceSocket(DiscordClient client, int timeout, int interval) | |||||
| : base(client, timeout, interval) | |||||
| public DiscordVoiceSocket(DiscordClient client, int timeout, int interval, bool isDebug) | |||||
| : base(client, timeout, interval, isDebug) | |||||
| { | { | ||||
| _connectWaitOnLogin = new ManualResetEventSlim(false); | _connectWaitOnLogin = new ManualResetEventSlim(false); | ||||
| #if !DNXCORE50 | #if !DNXCORE50 | ||||
| @@ -287,7 +287,8 @@ namespace Discord | |||||
| break; | break; | ||||
| #endif | #endif | ||||
| default: | default: | ||||
| RaiseOnDebugMessage(DebugMessageType.WebSocketUnknownOpCode, "Unknown VoiceSocket op: " + msg.Operation); | |||||
| if (_isDebug) | |||||
| RaiseOnDebugMessage(DebugMessageType.WebSocketUnknownOpCode, "Unknown VoiceSocket op: " + msg.Operation); | |||||
| break; | break; | ||||
| } | } | ||||
| #if DNXCORE50 | #if DNXCORE50 | ||||
| @@ -322,25 +323,42 @@ namespace Discord | |||||
| else | else | ||||
| { | { | ||||
| //Parse RTP Data | //Parse RTP Data | ||||
| /*if (length < 12) | |||||
| throw new Exception($"Unexpected message length. Expected >= 12, got {length}."); | |||||
| if (length < 12) | |||||
| { | |||||
| if (_isDebug) | |||||
| RaiseOnDebugMessage(DebugMessageType.VoiceInput, $"Unexpected message length. Expected >= 12, got {length}."); | |||||
| return; | |||||
| } | |||||
| byte flags = buffer[0]; | byte flags = buffer[0]; | ||||
| if (flags != 0x80) | if (flags != 0x80) | ||||
| throw new Exception("Unexpected Flags"); | |||||
| { | |||||
| if (_isDebug) | |||||
| RaiseOnDebugMessage(DebugMessageType.VoiceInput, $"Unexpected Flags: {flags}"); | |||||
| return; | |||||
| } | |||||
| byte payloadType = buffer[1]; | byte payloadType = buffer[1]; | ||||
| if (payloadType != 0x78) | if (payloadType != 0x78) | ||||
| throw new Exception("Unexpected Payload Type"); | |||||
| { | |||||
| if (_isDebug) | |||||
| RaiseOnDebugMessage(DebugMessageType.VoiceInput, $"Unexpected Payload Type: {flags}"); | |||||
| return; | |||||
| } | |||||
| 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)); | |||||
| ushort sequenceNumber = (ushort)((buffer[2] << 8) | | |||||
| buffer[3] << 0); | |||||
| 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 | //Decrypt | ||||
| if (_mode == "xsalsa20_poly1305") | |||||
| /*if (_mode == "xsalsa20_poly1305") | |||||
| { | { | ||||
| if (length < 36) //12 + 24 | if (length < 36) //12 + 24 | ||||
| throw new Exception($"Unexpected message length. Expected >= 36, got {length}."); | throw new Exception($"Unexpected message length. Expected >= 36, got {length}."); | ||||
| @@ -362,6 +380,8 @@ namespace Discord | |||||
| buffer = newBuffer; | buffer = newBuffer; | ||||
| }*/ | }*/ | ||||
| if (_isDebug) | |||||
| RaiseOnDebugMessage(DebugMessageType.VoiceInput, $"Received {buffer.Length - 12} bytes."); | |||||
| //TODO: Use Voice Data | //TODO: Use Voice Data | ||||
| } | } | ||||
| } | } | ||||
| @@ -14,22 +14,24 @@ namespace Discord | |||||
| private const int SendChunkSize = 4096; | private const int SendChunkSize = 4096; | ||||
| protected readonly DiscordClient _client; | protected readonly DiscordClient _client; | ||||
| protected volatile CancellationTokenSource _disconnectToken; | |||||
| protected int _timeout, _heartbeatInterval; | |||||
| protected readonly int _sendInterval; | protected readonly int _sendInterval; | ||||
| protected string _host; | |||||
| protected readonly bool _isDebug; | |||||
| private readonly ConcurrentQueue<byte[]> _sendQueue; | |||||
| protected volatile CancellationTokenSource _disconnectToken; | |||||
| private volatile ClientWebSocket _webSocket; | private volatile ClientWebSocket _webSocket; | ||||
| private volatile Task _tasks; | private volatile Task _tasks; | ||||
| private ConcurrentQueue<byte[]> _sendQueue; | |||||
| protected string _host; | |||||
| protected int _timeout, _heartbeatInterval; | |||||
| private DateTime _lastHeartbeat; | private DateTime _lastHeartbeat; | ||||
| private bool _isConnected; | private bool _isConnected; | ||||
| public DiscordWebSocket(DiscordClient client, int timeout, int interval) | |||||
| public DiscordWebSocket(DiscordClient client, int timeout, int interval, bool isDebug) | |||||
| { | { | ||||
| _client = client; | _client = client; | ||||
| _timeout = timeout; | _timeout = timeout; | ||||
| _sendInterval = interval; | _sendInterval = interval; | ||||
| _isDebug = isDebug; | |||||
| _sendQueue = new ConcurrentQueue<byte[]>(); | _sendQueue = new ConcurrentQueue<byte[]>(); | ||||
| } | } | ||||
| @@ -0,0 +1,14 @@ | |||||
| using System; | |||||
| namespace Discord.Helpers | |||||
| { | |||||
| internal partial class JsonHttpClient | |||||
| { | |||||
| public event EventHandler<LogMessageEventArgs> OnDebugMessage; | |||||
| protected void RaiseOnDebugMessage(DebugMessageType type, string message) | |||||
| { | |||||
| if (OnDebugMessage != null) | |||||
| OnDebugMessage(this, new LogMessageEventArgs(type, message)); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -1,7 +1,6 @@ | |||||
| using Discord.API; | using Discord.API; | ||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| using System; | using System; | ||||
| using System.Diagnostics; | |||||
| using System.IO; | using System.IO; | ||||
| using System.Globalization; | using System.Globalization; | ||||
| using System.Net.Http; | using System.Net.Http; | ||||
| @@ -9,24 +8,22 @@ using System.Net; | |||||
| using System.Reflection; | using System.Reflection; | ||||
| using System.Text; | using System.Text; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using System.Diagnostics; | |||||
| namespace Discord.Helpers | namespace Discord.Helpers | ||||
| { | { | ||||
| internal class JsonHttpClient | |||||
| internal partial class JsonHttpClient | |||||
| { | { | ||||
| #if TEST_RESPONSES | |||||
| private const bool _isDebug = true; | |||||
| #else | |||||
| private const bool _isDebug = false; | |||||
| #endif | |||||
| private bool _isDebug; | |||||
| private readonly HttpClient _client; | private readonly HttpClient _client; | ||||
| private readonly HttpMethod _patch; | private readonly HttpMethod _patch; | ||||
| #if TEST_RESPONSES | #if TEST_RESPONSES | ||||
| private readonly JsonSerializerSettings _settings; | private readonly JsonSerializerSettings _settings; | ||||
| #endif | #endif | ||||
| public JsonHttpClient() | |||||
| public JsonHttpClient(bool isDebug) | |||||
| { | { | ||||
| _isDebug = isDebug; | |||||
| _patch = new HttpMethod("PATCH"); //Not sure why this isn't a default... | _patch = new HttpMethod("PATCH"); //Not sure why this isn't a default... | ||||
| _client = new HttpClient(new HttpClientHandler | _client = new HttpClient(new HttpClientHandler | ||||
| @@ -131,8 +128,8 @@ namespace Discord.Helpers | |||||
| private async Task<string> Send(HttpMethod method, string path, HttpContent content) | private async Task<string> Send(HttpMethod method, string path, HttpContent content) | ||||
| { | { | ||||
| string responseJson = await SendRequest(method, path, content, true); | string responseJson = await SendRequest(method, path, content, true); | ||||
| if (path.StartsWith(Endpoints.BaseApi)) | |||||
| CheckEmptyResponse(responseJson); | |||||
| if (path.StartsWith(Endpoints.BaseApi) && !string.IsNullOrEmpty(responseJson)) | |||||
| throw new Exception("API check failed: Response is not empty."); | |||||
| return responseJson; | return responseJson; | ||||
| } | } | ||||
| #else | #else | ||||
| @@ -142,9 +139,25 @@ namespace Discord.Helpers | |||||
| private async Task<string> SendRequest(HttpMethod method, string path, HttpContent content, bool hasResponse) | private async Task<string> SendRequest(HttpMethod method, string path, HttpContent content, bool hasResponse) | ||||
| { | { | ||||
| #if TEST_RESPONSES | |||||
| Stopwatch stopwatch = Stopwatch.StartNew(); | |||||
| #endif | |||||
| Stopwatch stopwatch = null; | |||||
| if (_isDebug) | |||||
| { | |||||
| if (content != null) | |||||
| { | |||||
| if (content is StringContent) | |||||
| { | |||||
| string json = await (content as StringContent).ReadAsStringAsync(); | |||||
| RaiseOnDebugMessage(DebugMessageType.XHRRawOutput, $"{method} {path}: {json}"); | |||||
| } | |||||
| else | |||||
| { | |||||
| byte[] bytes = await content.ReadAsByteArrayAsync(); | |||||
| RaiseOnDebugMessage(DebugMessageType.XHRRawOutput, $"{method} {path}: {bytes.Length} bytes"); | |||||
| } | |||||
| } | |||||
| stopwatch = Stopwatch.StartNew(); | |||||
| } | |||||
| string result; | string result; | ||||
| using (HttpRequestMessage msg = new HttpRequestMessage(method, path)) | using (HttpRequestMessage msg = new HttpRequestMessage(method, path)) | ||||
| { | { | ||||
| @@ -168,21 +181,14 @@ namespace Discord.Helpers | |||||
| } | } | ||||
| } | } | ||||
| #if TEST_RESPONSES | |||||
| stopwatch.Stop(); | |||||
| Debug.WriteLine($"{method} {path}: {Math.Round(stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond, 2)}ms"); | |||||
| #endif | |||||
| if (_isDebug) | |||||
| { | |||||
| stopwatch.Stop(); | |||||
| RaiseOnDebugMessage(DebugMessageType.XHRTiming, $"{method} {path}: {Math.Round(stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond, 2)}ms"); | |||||
| } | |||||
| return result; | return result; | ||||
| } | } | ||||
| #if TEST_RESPONSES | |||||
| private void CheckEmptyResponse(string json) | |||||
| { | |||||
| if (!string.IsNullOrEmpty(json)) | |||||
| throw new Exception("API check failed: Response is not empty."); | |||||
| } | |||||
| #endif | |||||
| private StringContent AsJson(object obj) | private StringContent AsJson(object obj) | ||||
| { | { | ||||
| return new StringContent(JsonConvert.SerializeObject(obj), Encoding.UTF8, "application/json"); | return new StringContent(JsonConvert.SerializeObject(obj), Encoding.UTF8, "application/json"); | ||||