@@ -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.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<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 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; } | |||
/// <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) | |||
{ | |||
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<bool>(); | |||
_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; | |||
} | |||
} | |||
/// <inheritdoc /> | |||
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<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) | |||
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; | |||
@@ -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 | |||
{ | |||
/// <summary> S→C - Used to send most events. </summary> | |||
/// <summary> C←S - Used to send most events. </summary> | |||
Dispatch = 0, | |||
/// <summary> C↔S - Used to keep the connection alive and measure latency. </summary> | |||
Heartbeat = 1, | |||
@@ -16,15 +16,15 @@ | |||
VoiceServerPing = 5, | |||
/// <summary> C→S - Used to resume a connection after a redirect occurs. </summary> | |||
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, | |||
/// <summary> C→S - Used to request all members that were withheld by large_threshold </summary> | |||
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, | |||
/// <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, | |||
/// <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 | |||
} | |||
} |
@@ -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, | |||
/// <summary> C←S - Used to notify that the voice connection was successful and informs the client of available protocols. </summary> | |||
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, | |||
/// <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> | |||
SessionDescription = 4, | |||
/// <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); } } | |||
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 LogManager _log; | |||
internal readonly RequestQueue _requestQueue; | |||
@@ -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<ulong> _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<HelloEvent>(_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); | |||
} | |||
@@ -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 | |||
{ | |||
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); | |||
} | |||
} |