| @@ -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<string, string, double, Task> SentRequest { add { _sentRequestEvent.Add(value); } remove { _sentRequestEvent.Remove(value); } } | |||||
| private readonly AsyncEvent<Func<string, string, double, Task>> _sentRequestEvent = new AsyncEvent<Func<string, string, double, Task>>(); | |||||
| public event Func<int, Task> SentGatewayMessage { add { _sentGatewayMessageEvent.Add(value); } remove { _sentGatewayMessageEvent.Remove(value); } } | |||||
| private readonly AsyncEvent<Func<int, Task>> _sentGatewayMessageEvent = new AsyncEvent<Func<int, Task>>(); | |||||
| public event Func<VoiceOpCode, object, Task> ReceivedEvent { add { _receivedEvent.Add(value); } remove { _receivedEvent.Remove(value); } } | |||||
| private readonly AsyncEvent<Func<VoiceOpCode, object, Task>> _receivedEvent = new AsyncEvent<Func<VoiceOpCode, object, Task>>(); | |||||
| public event Func<Exception, Task> Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } | |||||
| private readonly AsyncEvent<Func<Exception, Task>> _disconnectedEvent = new AsyncEvent<Func<Exception, Task>>(); | |||||
| 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<WebSocketMessage>(reader.ReadToEnd()); | |||||
| await _receivedEvent.InvokeAsync((VoiceOpCode)msg.Operation, msg.Payload).ConfigureAwait(false); | |||||
| } | |||||
| } | |||||
| }; | |||||
| _gatewayClient.TextMessage += async text => | |||||
| { | |||||
| var msg = JsonConvert.DeserializeObject<WebSocketMessage>(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<T>(Stream jsonStream) | |||||
| { | |||||
| using (TextReader text = new StreamReader(jsonStream)) | |||||
| using (JsonReader reader = new JsonTextReader(text)) | |||||
| return _serializer.Deserialize<T>(reader); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -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.Converters; | ||||
| using Discord.Net.WebSockets; | |||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| using Newtonsoft.Json.Linq; | |||||
| using System; | using System; | ||||
| using System.Diagnostics; | |||||
| using System.Globalization; | |||||
| using System.IO; | |||||
| using System.Text; | |||||
| using System.Threading; | using System.Threading; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| @@ -15,60 +11,115 @@ namespace Discord.Audio | |||||
| { | { | ||||
| public class AudioClient | public class AudioClient | ||||
| { | { | ||||
| public const int MaxBitrate = 128; | |||||
| private const string Mode = "xsalsa20_poly1305"; | |||||
| public event Func<Task> Connected | |||||
| { | |||||
| add { _connectedEvent.Add(value); } | |||||
| remove { _connectedEvent.Remove(value); } | |||||
| } | |||||
| private readonly AsyncEvent<Func<Task>> _connectedEvent = new AsyncEvent<Func<Task>>(); | |||||
| public event Func<Task> Disconnected | |||||
| { | |||||
| add { _disconnectedEvent.Add(value); } | |||||
| remove { _disconnectedEvent.Remove(value); } | |||||
| } | |||||
| private readonly AsyncEvent<Func<Task>> _disconnectedEvent = new AsyncEvent<Func<Task>>(); | |||||
| public event Func<int, int, Task> LatencyUpdated | |||||
| { | |||||
| add { _latencyUpdatedEvent.Add(value); } | |||||
| remove { _latencyUpdatedEvent.Remove(value); } | |||||
| } | |||||
| private readonly AsyncEvent<Func<int, int, Task>> _latencyUpdatedEvent = new AsyncEvent<Func<int, int, Task>>(); | |||||
| private readonly ILogger _webSocketLogger; | |||||
| #if BENCHMARK | |||||
| private readonly ILogger _benchmarkLogger; | |||||
| #endif | |||||
| private readonly JsonSerializer _serializer; | 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<bool> _connectTask; | |||||
| private CancellationTokenSource _cancelToken; | |||||
| private Task _heartbeatTask, _reconnectTask; | |||||
| private long _heartbeatTime; | |||||
| private bool _isReconnecting; | |||||
| private string _url; | |||||
| public AudioAPIClient ApiClient { get; private set; } | |||||
| /// <summary> Gets the current connection state of this client. </summary> | |||||
| public ConnectionState ConnectionState { get; private set; } | public ConnectionState ConnectionState { get; private set; } | ||||
| /// <summary> Gets the estimated round-trip latency, in milliseconds, to the gateway server. </summary> | |||||
| public int Latency { get; private set; } | |||||
| internal AudioClient(WebSocketProvider provider, JsonSerializer serializer = null) | |||||
| /// <summary> Creates a new REST/WebSocket discord client. </summary> | |||||
| 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); | |||||
| }; | |||||
| } | |||||
| /// <inheritdoc /> | |||||
| public async Task ConnectAsync(string url) | public async Task ConnectAsync(string url) | ||||
| { | { | ||||
| await _connectionLock.WaitAsync().ConfigureAwait(false); | await _connectionLock.WaitAsync().ConfigureAwait(false); | ||||
| try | try | ||||
| { | { | ||||
| _isReconnecting = false; | |||||
| await ConnectInternalAsync(url).ConfigureAwait(false); | await ConnectInternalAsync(url).ConfigureAwait(false); | ||||
| } | } | ||||
| finally { _connectionLock.Release(); } | finally { _connectionLock.Release(); } | ||||
| } | } | ||||
| private async Task ConnectInternalAsync(string url) | private async Task ConnectInternalAsync(string url) | ||||
| { | { | ||||
| var state = ConnectionState; | |||||
| if (state == ConnectionState.Connecting || state == ConnectionState.Connected) | |||||
| await DisconnectInternalAsync().ConfigureAwait(false); | |||||
| ConnectionState = ConnectionState.Connecting; | ConnectionState = ConnectionState.Connecting; | ||||
| await _webSocketLogger.InfoAsync("Connecting").ConfigureAwait(false); | |||||
| try | try | ||||
| { | { | ||||
| _connectCancelToken = new CancellationTokenSource(); | |||||
| _gatewayClient.SetCancelToken(_connectCancelToken.Token); | |||||
| await _gatewayClient.ConnectAsync(url).ConfigureAwait(false); | |||||
| _url = url; | |||||
| _connectTask = new TaskCompletionSource<bool>(); | |||||
| _cancelToken = new CancellationTokenSource(); | |||||
| await ApiClient.ConnectAsync(url).ConfigureAwait(false); | |||||
| await _connectedEvent.InvokeAsync().ConfigureAwait(false); | |||||
| await _connectTask.Task.ConfigureAwait(false); | |||||
| ConnectionState = ConnectionState.Connected; | ConnectionState = ConnectionState.Connected; | ||||
| await _webSocketLogger.InfoAsync("Connected").ConfigureAwait(false); | |||||
| } | } | ||||
| catch (Exception) | catch (Exception) | ||||
| { | { | ||||
| @@ -76,12 +127,13 @@ namespace Discord.Audio | |||||
| throw; | throw; | ||||
| } | } | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public async Task DisconnectAsync() | public async Task DisconnectAsync() | ||||
| { | { | ||||
| await _connectionLock.WaitAsync().ConfigureAwait(false); | await _connectionLock.WaitAsync().ConfigureAwait(false); | ||||
| try | try | ||||
| { | { | ||||
| _isReconnecting = false; | |||||
| await DisconnectInternalAsync().ConfigureAwait(false); | await DisconnectInternalAsync().ConfigureAwait(false); | ||||
| } | } | ||||
| finally { _connectionLock.Release(); } | finally { _connectionLock.Release(); } | ||||
| @@ -90,30 +142,163 @@ namespace Discord.Audio | |||||
| { | { | ||||
| if (ConnectionState == ConnectionState.Disconnected) return; | if (ConnectionState == ConnectionState.Disconnected) return; | ||||
| ConnectionState = ConnectionState.Disconnecting; | 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; | 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<ReadyEvent>(_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<T>(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<T>(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) { } | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,17 @@ | |||||
| using Discord.Net.WebSockets; | |||||
| namespace Discord.Audio | |||||
| { | |||||
| public class AudioConfig | |||||
| { | |||||
| /// <summary> Gets or sets the time (in milliseconds) to wait for the websocket to connect and initialize. </summary> | |||||
| public int ConnectionTimeout { get; set; } = 30000; | |||||
| /// <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> | |||||
| public int FailedReconnectDelay { get; set; } = 15000; | |||||
| /// <summary> Gets or sets the provider used to generate new websocket connections. </summary> | |||||
| public WebSocketProvider WebSocketProvider { get; set; } = () => new DefaultWebSocketClient(); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,6 @@ | |||||
| namespace Discord.Audio | |||||
| { | |||||
| internal class Logger | |||||
| { | |||||
| } | |||||
| } | |||||
| @@ -36,7 +36,7 @@ namespace Discord.Audio.Opus | |||||
| { | { | ||||
| if (channels != 1 && channels != 2) | if (channels != 1 && channels != 2) | ||||
| throw new ArgumentOutOfRangeException(nameof(channels)); | 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)); | throw new ArgumentOutOfRangeException(nameof(bitrate)); | ||||
| OpusError error; | OpusError error; | ||||
| @@ -0,0 +1,74 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Collections.Immutable; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| public class AsyncEvent<T> | |||||
| { | |||||
| private readonly object _subLock = new object(); | |||||
| internal ImmutableArray<T> _subscriptions; | |||||
| public IReadOnlyList<T> Subscriptions => _subscriptions; | |||||
| public AsyncEvent() | |||||
| { | |||||
| _subscriptions = ImmutableArray.Create<T>(); | |||||
| } | |||||
| 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<Func<Task>> 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<T>(this AsyncEvent<Func<T, Task>> 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<T1, T2>(this AsyncEvent<Func<T1, T2, Task>> 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<T1, T2, T3>(this AsyncEvent<Func<T1, T2, T3, Task>> 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<T1, T2, T3, T4>(this AsyncEvent<Func<T1, T2, T3, T4, Task>> 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<T1, T2, T3, T4, T5>(this AsyncEvent<Func<T1, T2, T3, T4, T5, Task>> 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); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -2,7 +2,7 @@ | |||||
| { | { | ||||
| public enum GatewayOpCode : byte | public enum GatewayOpCode : byte | ||||
| { | { | ||||
| /// <summary> S→C - Used to send most events. </summary> | |||||
| /// <summary> C←S - Used to send most events. </summary> | |||||
| Dispatch = 0, | Dispatch = 0, | ||||
| /// <summary> C↔S - Used to keep the connection alive and measure latency. </summary> | /// <summary> C↔S - Used to keep the connection alive and measure latency. </summary> | ||||
| Heartbeat = 1, | Heartbeat = 1, | ||||
| @@ -16,15 +16,15 @@ | |||||
| VoiceServerPing = 5, | VoiceServerPing = 5, | ||||
| /// <summary> C→S - Used to resume a connection after a redirect occurs. </summary> | /// <summary> C→S - Used to resume a connection after a redirect occurs. </summary> | ||||
| Resume = 6, | Resume = 6, | ||||
| /// <summary> S→C - Used to notify a client that they must reconnect to another gateway. </summary> | |||||
| /// <summary> C←S - Used to notify a client that they must reconnect to another gateway. </summary> | |||||
| Reconnect = 7, | Reconnect = 7, | ||||
| /// <summary> C→S - Used to request all members that were withheld by large_threshold </summary> | /// <summary> C→S - Used to request all members that were withheld by large_threshold </summary> | ||||
| RequestGuildMembers = 8, | RequestGuildMembers = 8, | ||||
| /// <summary> S→C - Used to notify the client that their session has expired and cannot be resumed. </summary> | |||||
| /// <summary> C←S - Used to notify the client that their session has expired and cannot be resumed. </summary> | |||||
| InvalidSession = 9, | InvalidSession = 9, | ||||
| /// <summary> S→C - Used to provide information to the client immediately on connection. </summary> | |||||
| /// <summary> C←S - Used to provide information to the client immediately on connection. </summary> | |||||
| Hello = 10, | Hello = 10, | ||||
| /// <summary> S→C - Used to reply to a client's heartbeat. </summary> | |||||
| /// <summary> C←S - Used to reply to a client's heartbeat. </summary> | |||||
| HeartbeatAck = 11 | HeartbeatAck = 11 | ||||
| } | } | ||||
| } | } | ||||
| @@ -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; } | |||||
| } | |||||
| } | |||||
| @@ -8,8 +8,10 @@ | |||||
| SelectProtocol = 1, | SelectProtocol = 1, | ||||
| /// <summary> C←S - Used to notify that the voice connection was successful and informs the client of available protocols. </summary> | /// <summary> C←S - Used to notify that the voice connection was successful and informs the client of available protocols. </summary> | ||||
| Ready = 2, | Ready = 2, | ||||
| /// <summary> C↔S - Used to keep the connection alive and measure latency. </summary> | |||||
| /// <summary> C→S - Used to keep the connection alive and measure latency. </summary> | |||||
| Heartbeat = 3, | Heartbeat = 3, | ||||
| /// <summary> C←S - Used to reply to a client's heartbeat. </summary> | |||||
| HeartbeatAck = 3, | |||||
| /// <summary> C←S - Used to provide an encryption key to the client. </summary> | /// <summary> C←S - Used to provide an encryption key to the client. </summary> | ||||
| SessionDescription = 4, | SessionDescription = 4, | ||||
| /// <summary> C↔S - Used to inform that a certain user is speaking. </summary> | /// <summary> C↔S - Used to inform that a certain user is speaking. </summary> | ||||
| @@ -24,7 +24,7 @@ namespace Discord | |||||
| public event Func<Task> LoggedOut { add { _loggedOutEvent.Add(value); } remove { _loggedOutEvent.Remove(value); } } | public event Func<Task> LoggedOut { add { _loggedOutEvent.Add(value); } remove { _loggedOutEvent.Remove(value); } } | ||||
| private readonly AsyncEvent<Func<Task>> _loggedOutEvent = new AsyncEvent<Func<Task>>(); | private readonly AsyncEvent<Func<Task>> _loggedOutEvent = new AsyncEvent<Func<Task>>(); | ||||
| internal readonly Logger _discordLogger, _restLogger, _queueLogger; | |||||
| internal readonly ILogger _discordLogger, _restLogger, _queueLogger; | |||||
| internal readonly SemaphoreSlim _connectionLock; | internal readonly SemaphoreSlim _connectionLock; | ||||
| internal readonly LogManager _log; | internal readonly LogManager _log; | ||||
| internal readonly RequestQueue _requestQueue; | internal readonly RequestQueue _requestQueue; | ||||
| @@ -2,7 +2,6 @@ | |||||
| using Discord.Extensions; | using Discord.Extensions; | ||||
| using Discord.Logging; | using Discord.Logging; | ||||
| using Discord.Net.Converters; | using Discord.Net.Converters; | ||||
| using Discord.Net.WebSockets; | |||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| using Newtonsoft.Json.Linq; | using Newtonsoft.Json.Linq; | ||||
| using System; | using System; | ||||
| @@ -21,9 +20,9 @@ namespace Discord | |||||
| public partial class DiscordSocketClient : DiscordClient, IDiscordClient | public partial class DiscordSocketClient : DiscordClient, IDiscordClient | ||||
| { | { | ||||
| private readonly ConcurrentQueue<ulong> _largeGuilds; | private readonly ConcurrentQueue<ulong> _largeGuilds; | ||||
| private readonly Logger _gatewayLogger; | |||||
| private readonly ILogger _gatewayLogger; | |||||
| #if BENCHMARK | #if BENCHMARK | ||||
| private readonly Logger _benchmarkLogger; | |||||
| private readonly ILogger _benchmarkLogger; | |||||
| #endif | #endif | ||||
| private readonly JsonSerializer _serializer; | private readonly JsonSerializer _serializer; | ||||
| private readonly int _connectionTimeout, _reconnectDelay, _failedReconnectDelay; | private readonly int _connectionTimeout, _reconnectDelay, _failedReconnectDelay; | ||||
| @@ -150,6 +149,11 @@ namespace Discord | |||||
| await ApiClient.ConnectAsync().ConfigureAwait(false); | await ApiClient.ConnectAsync().ConfigureAwait(false); | ||||
| await _connectedEvent.InvokeAsync().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); | await _connectTask.Task.ConfigureAwait(false); | ||||
| ConnectionState = ConnectionState.Connected; | ConnectionState = ConnectionState.Connected; | ||||
| @@ -205,6 +209,7 @@ namespace Discord | |||||
| await _disconnectedEvent.InvokeAsync().ConfigureAwait(false); | await _disconnectedEvent.InvokeAsync().ConfigureAwait(false); | ||||
| } | } | ||||
| private async Task StartReconnectAsync() | private async Task StartReconnectAsync() | ||||
| { | { | ||||
| //TODO: Is this thread-safe? | //TODO: Is this thread-safe? | ||||
| @@ -416,10 +421,6 @@ namespace Discord | |||||
| await _gatewayLogger.DebugAsync("Received Hello").ConfigureAwait(false); | await _gatewayLogger.DebugAsync("Received Hello").ConfigureAwait(false); | ||||
| var data = (payload as JToken).ToObject<HelloEvent>(_serializer); | var data = (payload as JToken).ToObject<HelloEvent>(_serializer); | ||||
| if (_sessionId != null) | |||||
| await ApiClient.SendResumeAsync(_sessionId, _lastSeq).ConfigureAwait(false); | |||||
| else | |||||
| await ApiClient.SendIdentifyAsync().ConfigureAwait(false); | |||||
| _heartbeatTime = 0; | _heartbeatTime = 0; | ||||
| _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _cancelToken.Token); | _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _cancelToken.Token); | ||||
| } | } | ||||
| @@ -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); | |||||
| } | |||||
| } | |||||
| @@ -3,7 +3,7 @@ using System.Threading.Tasks; | |||||
| namespace Discord.Logging | namespace Discord.Logging | ||||
| { | { | ||||
| internal class LogManager : ILogger | |||||
| internal class LogManager : ILogManager, ILogger | |||||
| { | { | ||||
| public LogSeverity Level { get; } | public LogSeverity Level { get; } | ||||
| @@ -111,6 +111,6 @@ namespace Discord.Logging | |||||
| Task ILogger.DebugAsync(Exception ex) | Task ILogger.DebugAsync(Exception ex) | ||||
| => LogAsync(LogSeverity.Debug, "Discord", ex); | => LogAsync(LogSeverity.Debug, "Discord", ex); | ||||
| public Logger CreateLogger(string name) => new Logger(this, name); | |||||
| public ILogger CreateLogger(string name) => new Logger(this, name); | |||||
| } | } | ||||
| } | } | ||||