From ec2a85745563a9af270df73514510adeca9af1f1 Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 27 Jun 2016 22:23:32 -0300 Subject: [PATCH] Did more groundwork for AudioClient, added ILogManager --- src/Discord.Net.Audio/AudioAPIClient.cs | 176 +++++++++++ src/Discord.Net.Audio/AudioClient.cs | 287 ++++++++++++++---- src/Discord.Net.Audio/AudioConfig.cs | 17 ++ src/Discord.Net.Audio/Logger.cs | 6 + src/Discord.Net.Audio/Opus/OpusEncoder.cs | 2 +- src/Discord.Net.Audio/Utilities/AsyncEvent.cs | 74 +++++ src/Discord.Net/API/Gateway/GatewayOpCode.cs | 10 +- src/Discord.Net/API/Voice/IdentifyParams.cs | 16 + src/Discord.Net/API/Voice/VoiceOpCode.cs | 4 +- src/Discord.Net/DiscordClient.cs | 2 +- src/Discord.Net/DiscordSocketClient.cs | 15 +- src/Discord.Net/Logging/ILogManager.cs | 36 +++ src/Discord.Net/Logging/LogManager.cs | 4 +- 13 files changed, 581 insertions(+), 68 deletions(-) create mode 100644 src/Discord.Net.Audio/AudioAPIClient.cs create mode 100644 src/Discord.Net.Audio/AudioConfig.cs create mode 100644 src/Discord.Net.Audio/Logger.cs create mode 100644 src/Discord.Net.Audio/Utilities/AsyncEvent.cs create mode 100644 src/Discord.Net/API/Voice/IdentifyParams.cs create mode 100644 src/Discord.Net/Logging/ILogManager.cs diff --git a/src/Discord.Net.Audio/AudioAPIClient.cs b/src/Discord.Net.Audio/AudioAPIClient.cs new file mode 100644 index 000000000..db3418610 --- /dev/null +++ b/src/Discord.Net.Audio/AudioAPIClient.cs @@ -0,0 +1,176 @@ +using Discord.API; +using Discord.API.Voice; +using Discord.Net.Converters; +using Discord.Net.WebSockets; +using Newtonsoft.Json; +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Audio +{ + public class AudioAPIClient + { + public const int MaxBitrate = 128; + private const string Mode = "xsalsa20_poly1305"; + + public event Func SentRequest { add { _sentRequestEvent.Add(value); } remove { _sentRequestEvent.Remove(value); } } + private readonly AsyncEvent> _sentRequestEvent = new AsyncEvent>(); + public event Func SentGatewayMessage { add { _sentGatewayMessageEvent.Add(value); } remove { _sentGatewayMessageEvent.Remove(value); } } + private readonly AsyncEvent> _sentGatewayMessageEvent = new AsyncEvent>(); + + public event Func ReceivedEvent { add { _receivedEvent.Add(value); } remove { _receivedEvent.Remove(value); } } + private readonly AsyncEvent> _receivedEvent = new AsyncEvent>(); + public event Func Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } + private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); + + private readonly ulong _userId; + private readonly string _token; + private readonly JsonSerializer _serializer; + private readonly IWebSocketClient _gatewayClient; + private readonly SemaphoreSlim _connectionLock; + private CancellationTokenSource _connectCancelToken; + + public ulong GuildId { get; } + public string SessionId { get; } + public ConnectionState ConnectionState { get; private set; } + + internal AudioAPIClient(ulong guildId, ulong userId, string sessionId, string token, WebSocketProvider webSocketProvider, JsonSerializer serializer = null) + { + GuildId = guildId; + _userId = userId; + SessionId = sessionId; + _token = token; + _connectionLock = new SemaphoreSlim(1, 1); + + _gatewayClient = webSocketProvider(); + //_gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .Net 4.6+) + _gatewayClient.BinaryMessage += async (data, index, count) => + { + using (var compressed = new MemoryStream(data, index + 2, count - 2)) + using (var decompressed = new MemoryStream()) + { + using (var zlib = new DeflateStream(compressed, CompressionMode.Decompress)) + zlib.CopyTo(decompressed); + decompressed.Position = 0; + using (var reader = new StreamReader(decompressed)) + { + var msg = JsonConvert.DeserializeObject(reader.ReadToEnd()); + await _receivedEvent.InvokeAsync((VoiceOpCode)msg.Operation, msg.Payload).ConfigureAwait(false); + } + } + }; + _gatewayClient.TextMessage += async text => + { + var msg = JsonConvert.DeserializeObject(text); + await _receivedEvent.InvokeAsync((VoiceOpCode)msg.Operation, msg.Payload).ConfigureAwait(false); + }; + _gatewayClient.Closed += async ex => + { + await DisconnectAsync().ConfigureAwait(false); + await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); + }; + + _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; + } + + public Task SendAsync(VoiceOpCode opCode, object payload, RequestOptions options = null) + { + byte[] bytes = null; + payload = new WebSocketMessage { Operation = (int)opCode, Payload = payload }; + if (payload != null) + bytes = Encoding.UTF8.GetBytes(SerializeJson(payload)); + //TODO: Send + return Task.CompletedTask; + } + + //WebSocket + public async Task SendHeartbeatAsync(RequestOptions options = null) + { + await SendAsync(VoiceOpCode.Heartbeat, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), options: options).ConfigureAwait(false); + } + public async Task SendIdentityAsync(ulong guildId, ulong userId, string sessionId, string token) + { + await SendAsync(VoiceOpCode.Identify, new IdentifyParams + { + GuildId = guildId, + UserId = userId, + SessionId = sessionId, + Token = token + }); + } + + public async Task ConnectAsync(string url) + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await ConnectInternalAsync(url).ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + private async Task ConnectInternalAsync(string url) + { + ConnectionState = ConnectionState.Connecting; + try + { + _connectCancelToken = new CancellationTokenSource(); + _gatewayClient.SetCancelToken(_connectCancelToken.Token); + await _gatewayClient.ConnectAsync(url).ConfigureAwait(false); + + await SendIdentityAsync(GuildId, _userId, SessionId, _token).ConfigureAwait(false); + + ConnectionState = ConnectionState.Connected; + } + catch (Exception) + { + await DisconnectInternalAsync().ConfigureAwait(false); + throw; + } + } + + public async Task DisconnectAsync() + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await DisconnectInternalAsync().ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + private async Task DisconnectInternalAsync() + { + if (ConnectionState == ConnectionState.Disconnected) return; + ConnectionState = ConnectionState.Disconnecting; + + try { _connectCancelToken?.Cancel(false); } + catch { } + + await _gatewayClient.DisconnectAsync().ConfigureAwait(false); + + ConnectionState = ConnectionState.Disconnected; + } + + //Helpers + private static double ToMilliseconds(Stopwatch stopwatch) => Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); + private string SerializeJson(object value) + { + var sb = new StringBuilder(256); + using (TextWriter text = new StringWriter(sb, CultureInfo.InvariantCulture)) + using (JsonWriter writer = new JsonTextWriter(text)) + _serializer.Serialize(writer, value); + return sb.ToString(); + } + private T DeserializeJson(Stream jsonStream) + { + using (TextReader text = new StreamReader(jsonStream)) + using (JsonReader reader = new JsonTextReader(text)) + return _serializer.Deserialize(reader); + } + } +} diff --git a/src/Discord.Net.Audio/AudioClient.cs b/src/Discord.Net.Audio/AudioClient.cs index 65173db55..7f9298ed0 100644 --- a/src/Discord.Net.Audio/AudioClient.cs +++ b/src/Discord.Net.Audio/AudioClient.cs @@ -1,13 +1,9 @@ -using Discord.API; -using Discord.API.Voice; +using Discord.API.Voice; +using Discord.Logging; using Discord.Net.Converters; -using Discord.Net.WebSockets; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using System; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.Text; using System.Threading; using System.Threading.Tasks; @@ -15,60 +11,115 @@ namespace Discord.Audio { public class AudioClient { - public const int MaxBitrate = 128; - - private const string Mode = "xsalsa20_poly1305"; + public event Func Connected + { + add { _connectedEvent.Add(value); } + remove { _connectedEvent.Remove(value); } + } + private readonly AsyncEvent> _connectedEvent = new AsyncEvent>(); + public event Func Disconnected + { + add { _disconnectedEvent.Add(value); } + remove { _disconnectedEvent.Remove(value); } + } + private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); + public event Func LatencyUpdated + { + add { _latencyUpdatedEvent.Add(value); } + remove { _latencyUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _latencyUpdatedEvent = new AsyncEvent>(); + private readonly ILogger _webSocketLogger; +#if BENCHMARK + private readonly ILogger _benchmarkLogger; +#endif private readonly JsonSerializer _serializer; - private readonly IWebSocketClient _gatewayClient; - private readonly SemaphoreSlim _connectionLock; - private CancellationTokenSource _connectCancelToken; + private readonly int _connectionTimeout, _reconnectDelay, _failedReconnectDelay; + internal readonly SemaphoreSlim _connectionLock; + + private TaskCompletionSource _connectTask; + private CancellationTokenSource _cancelToken; + private Task _heartbeatTask, _reconnectTask; + private long _heartbeatTime; + private bool _isReconnecting; + private string _url; + public AudioAPIClient ApiClient { get; private set; } + /// Gets the current connection state of this client. public ConnectionState ConnectionState { get; private set; } + /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. + public int Latency { get; private set; } - internal AudioClient(WebSocketProvider provider, JsonSerializer serializer = null) + /// Creates a new REST/WebSocket discord client. + internal AudioClient(ulong guildId, ulong userId, string sessionId, string token, AudioConfig config, ILogManager logManager) { - _connectionLock = new SemaphoreSlim(1, 1); + _connectionTimeout = config.ConnectionTimeout; + _reconnectDelay = config.ReconnectDelay; + _failedReconnectDelay = config.FailedReconnectDelay; - _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; - } + _webSocketLogger = logManager.CreateLogger("AudioWS"); +#if BENCHMARK + _benchmarkLogger = logManager.CreateLogger("Benchmark"); +#endif - public Task SendAsync(VoiceOpCode opCode, object payload, RequestOptions options = null) - { - byte[] bytes = null; - payload = new WebSocketMessage { Operation = (int)opCode, Payload = payload }; - if (payload != null) - bytes = Encoding.UTF8.GetBytes(SerializeJson(payload)); - //TODO: Send - return Task.CompletedTask; - } + _connectionLock = new SemaphoreSlim(1, 1); - //Gateway - public async Task SendHeartbeatAsync(int lastSeq, RequestOptions options = null) - { - await SendAsync(VoiceOpCode.Heartbeat, lastSeq, options: options).ConfigureAwait(false); - } + _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; + _serializer.Error += (s, e) => + { + _webSocketLogger.WarningAsync(e.ErrorContext.Error).GetAwaiter().GetResult(); + e.ErrorContext.Handled = true; + }; + + var webSocketProvider = config.WebSocketProvider; //TODO: Clean this check + ApiClient = new AudioAPIClient(guildId, userId, sessionId, token, config.WebSocketProvider); + ApiClient.SentGatewayMessage += async opCode => await _webSocketLogger.DebugAsync($"Sent {(VoiceOpCode)opCode}").ConfigureAwait(false); + ApiClient.ReceivedEvent += ProcessMessageAsync; + ApiClient.Disconnected += async ex => + { + if (ex != null) + { + await _webSocketLogger.WarningAsync($"Connection Closed: {ex.Message}").ConfigureAwait(false); + await StartReconnectAsync().ConfigureAwait(false); + } + else + await _webSocketLogger.WarningAsync($"Connection Closed").ConfigureAwait(false); + }; + } + /// public async Task ConnectAsync(string url) { await _connectionLock.WaitAsync().ConfigureAwait(false); try { + _isReconnecting = false; await ConnectInternalAsync(url).ConfigureAwait(false); } finally { _connectionLock.Release(); } } private async Task ConnectInternalAsync(string url) { + var state = ConnectionState; + if (state == ConnectionState.Connecting || state == ConnectionState.Connected) + await DisconnectInternalAsync().ConfigureAwait(false); + ConnectionState = ConnectionState.Connecting; + await _webSocketLogger.InfoAsync("Connecting").ConfigureAwait(false); try { - _connectCancelToken = new CancellationTokenSource(); - _gatewayClient.SetCancelToken(_connectCancelToken.Token); - await _gatewayClient.ConnectAsync(url).ConfigureAwait(false); + _url = url; + _connectTask = new TaskCompletionSource(); + _cancelToken = new CancellationTokenSource(); + await ApiClient.ConnectAsync(url).ConfigureAwait(false); + await _connectedEvent.InvokeAsync().ConfigureAwait(false); + + await _connectTask.Task.ConfigureAwait(false); ConnectionState = ConnectionState.Connected; + await _webSocketLogger.InfoAsync("Connected").ConfigureAwait(false); } catch (Exception) { @@ -76,12 +127,13 @@ namespace Discord.Audio throw; } } - + /// public async Task DisconnectAsync() { await _connectionLock.WaitAsync().ConfigureAwait(false); try { + _isReconnecting = false; await DisconnectInternalAsync().ConfigureAwait(false); } finally { _connectionLock.Release(); } @@ -90,30 +142,163 @@ namespace Discord.Audio { if (ConnectionState == ConnectionState.Disconnected) return; ConnectionState = ConnectionState.Disconnecting; - - try { _connectCancelToken?.Cancel(false); } - catch { } + await _webSocketLogger.InfoAsync("Disconnecting").ConfigureAwait(false); - await _gatewayClient.DisconnectAsync().ConfigureAwait(false); + //Signal tasks to complete + try { _cancelToken.Cancel(); } catch { } + + //Disconnect from server + await ApiClient.DisconnectAsync().ConfigureAwait(false); + + //Wait for tasks to complete + var heartbeatTask = _heartbeatTask; + if (heartbeatTask != null) + await heartbeatTask.ConfigureAwait(false); + _heartbeatTask = null; ConnectionState = ConnectionState.Disconnected; + await _webSocketLogger.InfoAsync("Disconnected").ConfigureAwait(false); + + await _disconnectedEvent.InvokeAsync().ConfigureAwait(false); + } + + private async Task StartReconnectAsync() + { + //TODO: Is this thread-safe? + if (_reconnectTask != null) return; + + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + if (_reconnectTask != null) return; + _isReconnecting = true; + _reconnectTask = ReconnectInternalAsync(); + } + finally { _connectionLock.Release(); } + } + private async Task ReconnectInternalAsync() + { + try + { + int nextReconnectDelay = 1000; + while (_isReconnecting) + { + try + { + await Task.Delay(nextReconnectDelay).ConfigureAwait(false); + nextReconnectDelay *= 2; + if (nextReconnectDelay > 30000) + nextReconnectDelay = 30000; + + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await ConnectInternalAsync(_url).ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + return; + } + catch (Exception ex) + { + await _webSocketLogger.WarningAsync("Reconnect failed", ex).ConfigureAwait(false); + } + } + } + finally + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + _isReconnecting = false; + _reconnectTask = null; + } + finally { _connectionLock.Release(); } + } } - //Helpers - private static double ToMilliseconds(Stopwatch stopwatch) => Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); - private string SerializeJson(object value) + private async Task ProcessMessageAsync(VoiceOpCode opCode, object payload) { - var sb = new StringBuilder(256); - using (TextWriter text = new StringWriter(sb, CultureInfo.InvariantCulture)) - using (JsonWriter writer = new JsonTextWriter(text)) - _serializer.Serialize(writer, value); - return sb.ToString(); +#if BENCHMARK + Stopwatch stopwatch = Stopwatch.StartNew(); + try + { +#endif + try + { + switch (opCode) + { + /*case VoiceOpCode.Ready: + { + await _webSocketLogger.DebugAsync("Received Ready").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); + + _heartbeatTime = 0; + _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _cancelToken.Token); + } + break;*/ + case VoiceOpCode.HeartbeatAck: + { + await _webSocketLogger.DebugAsync("Received HeartbeatAck").ConfigureAwait(false); + + var heartbeatTime = _heartbeatTime; + if (heartbeatTime != 0) + { + int latency = (int)(Environment.TickCount - _heartbeatTime); + _heartbeatTime = 0; + await _webSocketLogger.VerboseAsync($"Latency = {latency} ms").ConfigureAwait(false); + + int before = Latency; + Latency = latency; + + await _latencyUpdatedEvent.InvokeAsync(before, latency).ConfigureAwait(false); + } + } + break; + default: + await _webSocketLogger.WarningAsync($"Unknown OpCode ({opCode})").ConfigureAwait(false); + return; + } + } + catch (Exception ex) + { + await _webSocketLogger.ErrorAsync($"Error handling {opCode}", ex).ConfigureAwait(false); + return; + } +#if BENCHMARK + } + finally + { + stopwatch.Stop(); + double millis = Math.Round(stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); + await _benchmarkLogger.DebugAsync($"{millis} ms").ConfigureAwait(false); + } +#endif } - private T DeserializeJson(Stream jsonStream) + + private async Task RunHeartbeatAsync(int intervalMillis, CancellationToken cancelToken) { - using (TextReader text = new StreamReader(jsonStream)) - using (JsonReader reader = new JsonTextReader(text)) - return _serializer.Deserialize(reader); + //Clean this up when Discord's session patch is live + try + { + while (!cancelToken.IsCancellationRequested) + { + await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false); + + if (_heartbeatTime != 0) //Server never responded to our last heartbeat + { + if (ConnectionState == ConnectionState.Connected) + { + await _webSocketLogger.WarningAsync("Server missed last heartbeat").ConfigureAwait(false); + await StartReconnectAsync().ConfigureAwait(false); + return; + } + } + else + _heartbeatTime = Environment.TickCount; + await ApiClient.SendHeartbeatAsync().ConfigureAwait(false); + } + } + catch (OperationCanceledException) { } } } } diff --git a/src/Discord.Net.Audio/AudioConfig.cs b/src/Discord.Net.Audio/AudioConfig.cs new file mode 100644 index 000000000..43db99020 --- /dev/null +++ b/src/Discord.Net.Audio/AudioConfig.cs @@ -0,0 +1,17 @@ +using Discord.Net.WebSockets; + +namespace Discord.Audio +{ + public class AudioConfig + { + /// Gets or sets the time (in milliseconds) to wait for the websocket to connect and initialize. + public int ConnectionTimeout { get; set; } = 30000; + /// Gets or sets the time (in milliseconds) to wait after an unexpected disconnect before reconnecting. + public int ReconnectDelay { get; set; } = 1000; + /// Gets or sets the time (in milliseconds) to wait after an reconnect fails before retrying. + public int FailedReconnectDelay { get; set; } = 15000; + + /// Gets or sets the provider used to generate new websocket connections. + public WebSocketProvider WebSocketProvider { get; set; } = () => new DefaultWebSocketClient(); + } +} diff --git a/src/Discord.Net.Audio/Logger.cs b/src/Discord.Net.Audio/Logger.cs new file mode 100644 index 000000000..cdb3b5fa6 --- /dev/null +++ b/src/Discord.Net.Audio/Logger.cs @@ -0,0 +1,6 @@ +namespace Discord.Audio +{ + internal class Logger + { + } +} \ No newline at end of file diff --git a/src/Discord.Net.Audio/Opus/OpusEncoder.cs b/src/Discord.Net.Audio/Opus/OpusEncoder.cs index 92ac33317..2e1d4d861 100644 --- a/src/Discord.Net.Audio/Opus/OpusEncoder.cs +++ b/src/Discord.Net.Audio/Opus/OpusEncoder.cs @@ -36,7 +36,7 @@ namespace Discord.Audio.Opus { if (channels != 1 && channels != 2) throw new ArgumentOutOfRangeException(nameof(channels)); - if (bitrate != null && (bitrate < 1 || bitrate > AudioClient.MaxBitrate)) + if (bitrate != null && (bitrate < 1 || bitrate > AudioAPIClient.MaxBitrate)) throw new ArgumentOutOfRangeException(nameof(bitrate)); OpusError error; diff --git a/src/Discord.Net.Audio/Utilities/AsyncEvent.cs b/src/Discord.Net.Audio/Utilities/AsyncEvent.cs new file mode 100644 index 000000000..e94b1d892 --- /dev/null +++ b/src/Discord.Net.Audio/Utilities/AsyncEvent.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading.Tasks; + +namespace Discord +{ + public class AsyncEvent + { + private readonly object _subLock = new object(); + internal ImmutableArray _subscriptions; + + public IReadOnlyList Subscriptions => _subscriptions; + + public AsyncEvent() + { + _subscriptions = ImmutableArray.Create(); + } + + public void Add(T subscriber) + { + lock (_subLock) + _subscriptions = _subscriptions.Add(subscriber); + } + public void Remove(T subscriber) + { + lock (_subLock) + _subscriptions = _subscriptions.Remove(subscriber); + } + } + + public static class EventExtensions + { + public static async Task InvokeAsync(this AsyncEvent> eventHandler) + { + var subscribers = eventHandler.Subscriptions; + if (subscribers.Count > 0) + { + for (int i = 0; i < subscribers.Count; i++) + await subscribers[i].Invoke().ConfigureAwait(false); + } + } + public static async Task InvokeAsync(this AsyncEvent> eventHandler, T arg) + { + var subscribers = eventHandler.Subscriptions; + for (int i = 0; i < subscribers.Count; i++) + await subscribers[i].Invoke(arg).ConfigureAwait(false); + } + public static async Task InvokeAsync(this AsyncEvent> eventHandler, T1 arg1, T2 arg2) + { + var subscribers = eventHandler.Subscriptions; + for (int i = 0; i < subscribers.Count; i++) + await subscribers[i].Invoke(arg1, arg2).ConfigureAwait(false); + } + public static async Task InvokeAsync(this AsyncEvent> eventHandler, T1 arg1, T2 arg2, T3 arg3) + { + var subscribers = eventHandler.Subscriptions; + for (int i = 0; i < subscribers.Count; i++) + await subscribers[i].Invoke(arg1, arg2, arg3).ConfigureAwait(false); + } + public static async Task InvokeAsync(this AsyncEvent> eventHandler, T1 arg1, T2 arg2, T3 arg3, T4 arg4) + { + var subscribers = eventHandler.Subscriptions; + for (int i = 0; i < subscribers.Count; i++) + await subscribers[i].Invoke(arg1, arg2, arg3, arg4).ConfigureAwait(false); + } + public static async Task InvokeAsync(this AsyncEvent> eventHandler, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) + { + var subscribers = eventHandler.Subscriptions; + for (int i = 0; i < subscribers.Count; i++) + await subscribers[i].Invoke(arg1, arg2, arg3, arg4, arg5).ConfigureAwait(false); + } + } +} diff --git a/src/Discord.Net/API/Gateway/GatewayOpCode.cs b/src/Discord.Net/API/Gateway/GatewayOpCode.cs index 8b983383f..dc69d073c 100644 --- a/src/Discord.Net/API/Gateway/GatewayOpCode.cs +++ b/src/Discord.Net/API/Gateway/GatewayOpCode.cs @@ -2,7 +2,7 @@ { public enum GatewayOpCode : byte { - /// S→C - Used to send most events. + /// C←S - Used to send most events. Dispatch = 0, /// C↔S - Used to keep the connection alive and measure latency. Heartbeat = 1, @@ -16,15 +16,15 @@ VoiceServerPing = 5, /// C→S - Used to resume a connection after a redirect occurs. Resume = 6, - /// S→C - Used to notify a client that they must reconnect to another gateway. + /// C←S - Used to notify a client that they must reconnect to another gateway. Reconnect = 7, /// C→S - Used to request all members that were withheld by large_threshold RequestGuildMembers = 8, - /// S→C - Used to notify the client that their session has expired and cannot be resumed. + /// C←S - Used to notify the client that their session has expired and cannot be resumed. InvalidSession = 9, - /// S→C - Used to provide information to the client immediately on connection. + /// C←S - Used to provide information to the client immediately on connection. Hello = 10, - /// S→C - Used to reply to a client's heartbeat. + /// C←S - Used to reply to a client's heartbeat. HeartbeatAck = 11 } } diff --git a/src/Discord.Net/API/Voice/IdentifyParams.cs b/src/Discord.Net/API/Voice/IdentifyParams.cs new file mode 100644 index 000000000..39889bfad --- /dev/null +++ b/src/Discord.Net/API/Voice/IdentifyParams.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API.Voice +{ + public class IdentifyParams + { + [JsonProperty("server_id")] + public ulong GuildId { get; set; } + [JsonProperty("user_id")] + public ulong UserId { get; set; } + [JsonProperty("session_id")] + public string SessionId { get; set; } + [JsonProperty("token")] + public string Token { get; set; } + } +} diff --git a/src/Discord.Net/API/Voice/VoiceOpCode.cs b/src/Discord.Net/API/Voice/VoiceOpCode.cs index b1526b463..b571f2d96 100644 --- a/src/Discord.Net/API/Voice/VoiceOpCode.cs +++ b/src/Discord.Net/API/Voice/VoiceOpCode.cs @@ -8,8 +8,10 @@ SelectProtocol = 1, /// C←S - Used to notify that the voice connection was successful and informs the client of available protocols. Ready = 2, - /// C↔S - Used to keep the connection alive and measure latency. + /// C→S - Used to keep the connection alive and measure latency. Heartbeat = 3, + /// C←S - Used to reply to a client's heartbeat. + HeartbeatAck = 3, /// C←S - Used to provide an encryption key to the client. SessionDescription = 4, /// C↔S - Used to inform that a certain user is speaking. diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index 3e87dee1e..49681702b 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -24,7 +24,7 @@ namespace Discord public event Func LoggedOut { add { _loggedOutEvent.Add(value); } remove { _loggedOutEvent.Remove(value); } } private readonly AsyncEvent> _loggedOutEvent = new AsyncEvent>(); - internal readonly Logger _discordLogger, _restLogger, _queueLogger; + internal readonly ILogger _discordLogger, _restLogger, _queueLogger; internal readonly SemaphoreSlim _connectionLock; internal readonly LogManager _log; internal readonly RequestQueue _requestQueue; diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index f27a26657..01a9ae88c 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -2,7 +2,6 @@ using Discord.Extensions; using Discord.Logging; using Discord.Net.Converters; -using Discord.Net.WebSockets; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; @@ -21,9 +20,9 @@ namespace Discord public partial class DiscordSocketClient : DiscordClient, IDiscordClient { private readonly ConcurrentQueue _largeGuilds; - private readonly Logger _gatewayLogger; + private readonly ILogger _gatewayLogger; #if BENCHMARK - private readonly Logger _benchmarkLogger; + private readonly ILogger _benchmarkLogger; #endif private readonly JsonSerializer _serializer; private readonly int _connectionTimeout, _reconnectDelay, _failedReconnectDelay; @@ -150,6 +149,11 @@ namespace Discord await ApiClient.ConnectAsync().ConfigureAwait(false); await _connectedEvent.InvokeAsync().ConfigureAwait(false); + if (_sessionId != null) + await ApiClient.SendResumeAsync(_sessionId, _lastSeq).ConfigureAwait(false); + else + await ApiClient.SendIdentifyAsync().ConfigureAwait(false); + await _connectTask.Task.ConfigureAwait(false); ConnectionState = ConnectionState.Connected; @@ -205,6 +209,7 @@ namespace Discord await _disconnectedEvent.InvokeAsync().ConfigureAwait(false); } + private async Task StartReconnectAsync() { //TODO: Is this thread-safe? @@ -416,10 +421,6 @@ namespace Discord await _gatewayLogger.DebugAsync("Received Hello").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - if (_sessionId != null) - await ApiClient.SendResumeAsync(_sessionId, _lastSeq).ConfigureAwait(false); - else - await ApiClient.SendIdentifyAsync().ConfigureAwait(false); _heartbeatTime = 0; _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _cancelToken.Token); } diff --git a/src/Discord.Net/Logging/ILogManager.cs b/src/Discord.Net/Logging/ILogManager.cs new file mode 100644 index 000000000..b244419b9 --- /dev/null +++ b/src/Discord.Net/Logging/ILogManager.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Logging +{ + public interface ILogManager + { + LogSeverity Level { get; } + + Task LogAsync(LogSeverity severity, string source, string message, Exception ex = null); + Task LogAsync(LogSeverity severity, string source, FormattableString message, Exception ex = null); + Task LogAsync(LogSeverity severity, string source, Exception ex); + + Task ErrorAsync(string source, string message, Exception ex = null); + Task ErrorAsync(string source, FormattableString message, Exception ex = null); + Task ErrorAsync(string source, Exception ex); + + Task WarningAsync(string source, string message, Exception ex = null); + Task WarningAsync(string source, FormattableString message, Exception ex = null); + Task WarningAsync(string source, Exception ex); + + Task InfoAsync(string source, string message, Exception ex = null); + Task InfoAsync(string source, FormattableString message, Exception ex = null); + Task InfoAsync(string source, Exception ex); + + Task VerboseAsync(string source, string message, Exception ex = null); + Task VerboseAsync(string source, FormattableString message, Exception ex = null); + Task VerboseAsync(string source, Exception ex); + + Task DebugAsync(string source, string message, Exception ex = null); + Task DebugAsync(string source, FormattableString message, Exception ex = null); + Task DebugAsync(string source, Exception ex); + + ILogger CreateLogger(string name); + } +} diff --git a/src/Discord.Net/Logging/LogManager.cs b/src/Discord.Net/Logging/LogManager.cs index d428ae59f..e37b2bce6 100644 --- a/src/Discord.Net/Logging/LogManager.cs +++ b/src/Discord.Net/Logging/LogManager.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; namespace Discord.Logging { - internal class LogManager : ILogger + internal class LogManager : ILogManager, ILogger { public LogSeverity Level { get; } @@ -111,6 +111,6 @@ namespace Discord.Logging Task ILogger.DebugAsync(Exception ex) => LogAsync(LogSeverity.Debug, "Discord", ex); - public Logger CreateLogger(string name) => new Logger(this, name); + public ILogger CreateLogger(string name) => new Logger(this, name); } }