@@ -28,24 +28,19 @@ namespace Discord.Audio | |||||
private readonly AsyncEvent<Func<VoiceOpCode, object, Task>> _receivedEvent = new AsyncEvent<Func<VoiceOpCode, object, Task>>(); | 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); } } | 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 AsyncEvent<Func<Exception, Task>> _disconnectedEvent = new AsyncEvent<Func<Exception, Task>>(); | ||||
private readonly ulong _userId; | |||||
private readonly string _token; | |||||
private readonly JsonSerializer _serializer; | private readonly JsonSerializer _serializer; | ||||
private readonly IWebSocketClient _gatewayClient; | private readonly IWebSocketClient _gatewayClient; | ||||
private readonly SemaphoreSlim _connectionLock; | private readonly SemaphoreSlim _connectionLock; | ||||
private CancellationTokenSource _connectCancelToken; | private CancellationTokenSource _connectCancelToken; | ||||
private bool _isDisposed; | |||||
public ulong GuildId { get; } | |||||
public string SessionId { get; } | |||||
public ulong GuildId { get; } | |||||
public ConnectionState ConnectionState { get; private set; } | public ConnectionState ConnectionState { get; private set; } | ||||
internal DiscordVoiceAPIClient(ulong guildId, ulong userId, string sessionId, string token, WebSocketProvider webSocketProvider, JsonSerializer serializer = null) | |||||
internal DiscordVoiceAPIClient(ulong guildId, WebSocketProvider webSocketProvider, JsonSerializer serializer = null) | |||||
{ | { | ||||
GuildId = guildId; | GuildId = guildId; | ||||
_userId = userId; | |||||
SessionId = sessionId; | |||||
_token = token; | |||||
_connectionLock = new SemaphoreSlim(1, 1); | _connectionLock = new SemaphoreSlim(1, 1); | ||||
_gatewayClient = webSocketProvider(); | _gatewayClient = webSocketProvider(); | ||||
@@ -78,6 +73,19 @@ namespace Discord.Audio | |||||
_serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; | _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; | ||||
} | } | ||||
void Dispose(bool disposing) | |||||
{ | |||||
if (!_isDisposed) | |||||
{ | |||||
if (disposing) | |||||
{ | |||||
_connectCancelToken?.Dispose(); | |||||
(_gatewayClient as IDisposable)?.Dispose(); | |||||
} | |||||
_isDisposed = true; | |||||
} | |||||
} | |||||
public void Dispose() => Dispose(true); | |||||
public Task SendAsync(VoiceOpCode opCode, object payload, RequestOptions options = null) | public Task SendAsync(VoiceOpCode opCode, object payload, RequestOptions options = null) | ||||
{ | { | ||||
@@ -105,16 +113,16 @@ namespace Discord.Audio | |||||
}); | }); | ||||
} | } | ||||
public async Task ConnectAsync(string url) | |||||
public async Task ConnectAsync(string url, ulong userId, string sessionId, string token) | |||||
{ | { | ||||
await _connectionLock.WaitAsync().ConfigureAwait(false); | await _connectionLock.WaitAsync().ConfigureAwait(false); | ||||
try | try | ||||
{ | { | ||||
await ConnectInternalAsync(url).ConfigureAwait(false); | |||||
await ConnectInternalAsync(url, userId, sessionId, token).ConfigureAwait(false); | |||||
} | } | ||||
finally { _connectionLock.Release(); } | finally { _connectionLock.Release(); } | ||||
} | } | ||||
private async Task ConnectInternalAsync(string url) | |||||
private async Task ConnectInternalAsync(string url, ulong userId, string sessionId, string token) | |||||
{ | { | ||||
ConnectionState = ConnectionState.Connecting; | ConnectionState = ConnectionState.Connecting; | ||||
try | try | ||||
@@ -123,7 +131,7 @@ namespace Discord.Audio | |||||
_gatewayClient.SetCancelToken(_connectCancelToken.Token); | _gatewayClient.SetCancelToken(_connectCancelToken.Token); | ||||
await _gatewayClient.ConnectAsync(url).ConfigureAwait(false); | await _gatewayClient.ConnectAsync(url).ConfigureAwait(false); | ||||
await SendIdentityAsync(GuildId, _userId, SessionId, _token).ConfigureAwait(false); | |||||
await SendIdentityAsync(GuildId, userId, sessionId, token).ConfigureAwait(false); | |||||
ConnectionState = ConnectionState.Connected; | ConnectionState = ConnectionState.Connected; | ||||
} | } | ||||
@@ -1,7 +1,6 @@ | |||||
using Discord.API.Voice; | using Discord.API.Voice; | ||||
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 System; | using System; | ||||
using System.Threading; | using System.Threading; | ||||
@@ -9,7 +8,7 @@ using System.Threading.Tasks; | |||||
namespace Discord.Audio | namespace Discord.Audio | ||||
{ | { | ||||
internal class AudioClient : IAudioClient | |||||
internal class AudioClient : IAudioClient, IDisposable | |||||
{ | { | ||||
public event Func<Task> Connected | public event Func<Task> Connected | ||||
{ | { | ||||
@@ -17,12 +16,12 @@ namespace Discord.Audio | |||||
remove { _connectedEvent.Remove(value); } | remove { _connectedEvent.Remove(value); } | ||||
} | } | ||||
private readonly AsyncEvent<Func<Task>> _connectedEvent = new AsyncEvent<Func<Task>>(); | private readonly AsyncEvent<Func<Task>> _connectedEvent = new AsyncEvent<Func<Task>>(); | ||||
public event Func<Task> Disconnected | |||||
public event Func<Exception, Task> Disconnected | |||||
{ | { | ||||
add { _disconnectedEvent.Add(value); } | add { _disconnectedEvent.Add(value); } | ||||
remove { _disconnectedEvent.Remove(value); } | remove { _disconnectedEvent.Remove(value); } | ||||
} | } | ||||
private readonly AsyncEvent<Func<Task>> _disconnectedEvent = new AsyncEvent<Func<Task>>(); | |||||
private readonly AsyncEvent<Func<Exception, Task>> _disconnectedEvent = new AsyncEvent<Func<Exception, Task>>(); | |||||
public event Func<int, int, Task> LatencyUpdated | public event Func<int, int, Task> LatencyUpdated | ||||
{ | { | ||||
add { _latencyUpdatedEvent.Add(value); } | add { _latencyUpdatedEvent.Add(value); } | ||||
@@ -34,28 +33,30 @@ namespace Discord.Audio | |||||
#if BENCHMARK | #if BENCHMARK | ||||
private readonly ILogger _benchmarkLogger; | private readonly ILogger _benchmarkLogger; | ||||
#endif | #endif | ||||
private readonly JsonSerializer _serializer; | |||||
internal readonly SemaphoreSlim _connectionLock; | internal readonly SemaphoreSlim _connectionLock; | ||||
private readonly JsonSerializer _serializer; | |||||
private TaskCompletionSource<bool> _connectTask; | private TaskCompletionSource<bool> _connectTask; | ||||
private CancellationTokenSource _cancelToken; | private CancellationTokenSource _cancelToken; | ||||
private Task _heartbeatTask, _reconnectTask; | |||||
private Task _heartbeatTask; | |||||
private long _heartbeatTime; | private long _heartbeatTime; | ||||
private bool _isReconnecting; | |||||
private string _url; | private string _url; | ||||
private bool _isDisposed; | |||||
private DiscordSocketClient Discord { get; } | |||||
public CachedGuild Guild { get; } | |||||
public DiscordVoiceAPIClient ApiClient { get; private set; } | public DiscordVoiceAPIClient ApiClient { get; private set; } | ||||
public ConnectionState ConnectionState { get; private set; } | public ConnectionState ConnectionState { get; private set; } | ||||
public int Latency { get; private set; } | public int Latency { get; private set; } | ||||
private DiscordSocketClient Discord => Guild.Discord; | |||||
/// <summary> Creates a new REST/WebSocket discord client. </summary> | /// <summary> Creates a new REST/WebSocket discord client. </summary> | ||||
internal AudioClient(DiscordSocketClient discord, ulong guildId, ulong userId, string sessionId, string token, WebSocketProvider webSocketProvider, ILogManager logManager) | |||||
internal AudioClient(CachedGuild guild) | |||||
{ | { | ||||
Discord = discord; | |||||
Guild = guild; | |||||
_webSocketLogger = logManager.CreateLogger("Audio"); | |||||
_udpLogger = logManager.CreateLogger("AudioUDP"); | |||||
_webSocketLogger = Discord.LogManager.CreateLogger("Audio"); | |||||
_udpLogger = Discord.LogManager.CreateLogger("AudioUDP"); | |||||
#if BENCHMARK | #if BENCHMARK | ||||
_benchmarkLogger = logManager.CreateLogger("Benchmark"); | _benchmarkLogger = logManager.CreateLogger("Benchmark"); | ||||
#endif | #endif | ||||
@@ -69,38 +70,34 @@ namespace Discord.Audio | |||||
e.ErrorContext.Handled = true; | e.ErrorContext.Handled = true; | ||||
}; | }; | ||||
ApiClient = new DiscordVoiceAPIClient(guildId, userId, sessionId, token, webSocketProvider); | |||||
ApiClient = new DiscordVoiceAPIClient(guild.Id, Discord.WebSocketProvider); | |||||
ApiClient.SentGatewayMessage += async opCode => await _webSocketLogger.DebugAsync($"Sent {(VoiceOpCode)opCode}").ConfigureAwait(false); | ApiClient.SentGatewayMessage += async opCode => await _webSocketLogger.DebugAsync($"Sent {(VoiceOpCode)opCode}").ConfigureAwait(false); | ||||
ApiClient.ReceivedEvent += ProcessMessageAsync; | ApiClient.ReceivedEvent += ProcessMessageAsync; | ||||
ApiClient.Disconnected += async ex => | ApiClient.Disconnected += async ex => | ||||
{ | { | ||||
if (ex != null) | if (ex != null) | ||||
{ | |||||
await _webSocketLogger.WarningAsync($"Connection Closed: {ex.Message}").ConfigureAwait(false); | await _webSocketLogger.WarningAsync($"Connection Closed: {ex.Message}").ConfigureAwait(false); | ||||
await StartReconnectAsync().ConfigureAwait(false); | |||||
} | |||||
else | else | ||||
await _webSocketLogger.WarningAsync($"Connection Closed").ConfigureAwait(false); | await _webSocketLogger.WarningAsync($"Connection Closed").ConfigureAwait(false); | ||||
}; | }; | ||||
} | } | ||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public async Task ConnectAsync(string url) | |||||
public async Task ConnectAsync(string url, ulong userId, string sessionId, string token) | |||||
{ | { | ||||
await _connectionLock.WaitAsync().ConfigureAwait(false); | await _connectionLock.WaitAsync().ConfigureAwait(false); | ||||
try | try | ||||
{ | { | ||||
_isReconnecting = false; | |||||
await ConnectInternalAsync(url).ConfigureAwait(false); | |||||
await ConnectInternalAsync(url, userId, sessionId, token).ConfigureAwait(false); | |||||
} | } | ||||
finally { _connectionLock.Release(); } | finally { _connectionLock.Release(); } | ||||
} | } | ||||
private async Task ConnectInternalAsync(string url) | |||||
private async Task ConnectInternalAsync(string url, ulong userId, string sessionId, string token) | |||||
{ | { | ||||
var state = ConnectionState; | var state = ConnectionState; | ||||
if (state == ConnectionState.Connecting || state == ConnectionState.Connected) | if (state == ConnectionState.Connecting || state == ConnectionState.Connected) | ||||
await DisconnectInternalAsync().ConfigureAwait(false); | |||||
await DisconnectInternalAsync(null).ConfigureAwait(false); | |||||
ConnectionState = ConnectionState.Connecting; | ConnectionState = ConnectionState.Connecting; | ||||
await _webSocketLogger.InfoAsync("Connecting").ConfigureAwait(false); | await _webSocketLogger.InfoAsync("Connecting").ConfigureAwait(false); | ||||
@@ -109,7 +106,7 @@ namespace Discord.Audio | |||||
_url = url; | _url = url; | ||||
_connectTask = new TaskCompletionSource<bool>(); | _connectTask = new TaskCompletionSource<bool>(); | ||||
_cancelToken = new CancellationTokenSource(); | _cancelToken = new CancellationTokenSource(); | ||||
await ApiClient.ConnectAsync(url).ConfigureAwait(false); | |||||
await ApiClient.ConnectAsync(url, userId, sessionId, token).ConfigureAwait(false); | |||||
await _connectedEvent.InvokeAsync().ConfigureAwait(false); | await _connectedEvent.InvokeAsync().ConfigureAwait(false); | ||||
await _connectTask.Task.ConfigureAwait(false); | await _connectTask.Task.ConfigureAwait(false); | ||||
@@ -119,7 +116,7 @@ namespace Discord.Audio | |||||
} | } | ||||
catch (Exception) | catch (Exception) | ||||
{ | { | ||||
await DisconnectInternalAsync().ConfigureAwait(false); | |||||
await DisconnectInternalAsync(null).ConfigureAwait(false); | |||||
throw; | throw; | ||||
} | } | ||||
} | } | ||||
@@ -129,12 +126,20 @@ namespace Discord.Audio | |||||
await _connectionLock.WaitAsync().ConfigureAwait(false); | await _connectionLock.WaitAsync().ConfigureAwait(false); | ||||
try | try | ||||
{ | { | ||||
_isReconnecting = false; | |||||
await DisconnectInternalAsync().ConfigureAwait(false); | |||||
await DisconnectInternalAsync(null).ConfigureAwait(false); | |||||
} | |||||
finally { _connectionLock.Release(); } | |||||
} | |||||
private async Task DisconnectAsync(Exception ex) | |||||
{ | |||||
await _connectionLock.WaitAsync().ConfigureAwait(false); | |||||
try | |||||
{ | |||||
await DisconnectInternalAsync(ex).ConfigureAwait(false); | |||||
} | } | ||||
finally { _connectionLock.Release(); } | finally { _connectionLock.Release(); } | ||||
} | } | ||||
private async Task DisconnectInternalAsync() | |||||
private async Task DisconnectInternalAsync(Exception ex) | |||||
{ | { | ||||
if (ConnectionState == ConnectionState.Disconnected) return; | if (ConnectionState == ConnectionState.Disconnected) return; | ||||
ConnectionState = ConnectionState.Disconnecting; | ConnectionState = ConnectionState.Disconnecting; | ||||
@@ -155,61 +160,7 @@ namespace Discord.Audio | |||||
ConnectionState = ConnectionState.Disconnected; | ConnectionState = ConnectionState.Disconnected; | ||||
await _webSocketLogger.InfoAsync("Disconnected").ConfigureAwait(false); | 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(); } | |||||
} | |||||
await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); | |||||
} | } | ||||
private async Task ProcessMessageAsync(VoiceOpCode opCode, object payload) | private async Task ProcessMessageAsync(VoiceOpCode opCode, object payload) | ||||
@@ -285,7 +236,7 @@ namespace Discord.Audio | |||||
if (ConnectionState == ConnectionState.Connected) | if (ConnectionState == ConnectionState.Connected) | ||||
{ | { | ||||
await _webSocketLogger.WarningAsync("Server missed last heartbeat").ConfigureAwait(false); | await _webSocketLogger.WarningAsync("Server missed last heartbeat").ConfigureAwait(false); | ||||
await StartReconnectAsync().ConfigureAwait(false); | |||||
await DisconnectInternalAsync(new Exception("Server missed last heartbeat")).ConfigureAwait(false); | |||||
return; | return; | ||||
} | } | ||||
} | } | ||||
@@ -296,5 +247,14 @@ namespace Discord.Audio | |||||
} | } | ||||
catch (OperationCanceledException) { } | catch (OperationCanceledException) { } | ||||
} | } | ||||
internal virtual void Dispose(bool disposing) | |||||
{ | |||||
if (!_isDisposed) | |||||
_isDisposed = true; | |||||
ApiClient.Dispose(); | |||||
} | |||||
/// <inheritdoc /> | |||||
public void Dispose() => Dispose(true); | |||||
} | } | ||||
} | } |
@@ -6,7 +6,7 @@ namespace Discord.Audio | |||||
public interface IAudioClient | public interface IAudioClient | ||||
{ | { | ||||
event Func<Task> Connected; | event Func<Task> Connected; | ||||
event Func<Task> Disconnected; | |||||
event Func<Exception, Task> Disconnected; | |||||
event Func<int, int, Task> LatencyUpdated; | event Func<int, int, Task> LatencyUpdated; | ||||
DiscordVoiceAPIClient ApiClient { get; } | DiscordVoiceAPIClient ApiClient { get; } | ||||
@@ -26,13 +26,13 @@ namespace Discord | |||||
internal readonly ILogger _discordLogger, _restLogger, _queueLogger; | internal readonly ILogger _discordLogger, _restLogger, _queueLogger; | ||||
internal readonly SemaphoreSlim _connectionLock; | internal readonly SemaphoreSlim _connectionLock; | ||||
internal readonly LogManager _log; | |||||
internal readonly RequestQueue _requestQueue; | internal readonly RequestQueue _requestQueue; | ||||
internal bool _isDisposed; | internal bool _isDisposed; | ||||
internal SelfUser _currentUser; | internal SelfUser _currentUser; | ||||
public API.DiscordApiClient ApiClient { get; } | |||||
internal LogManager LogManager { get; } | |||||
public LoginState LoginState { get; private set; } | public LoginState LoginState { get; private set; } | ||||
public API.DiscordApiClient ApiClient { get; private set; } | |||||
/// <summary> Creates a new REST-only discord client. </summary> | /// <summary> Creates a new REST-only discord client. </summary> | ||||
public DiscordClient() | public DiscordClient() | ||||
@@ -40,11 +40,11 @@ namespace Discord | |||||
/// <summary> Creates a new REST-only discord client. </summary> | /// <summary> Creates a new REST-only discord client. </summary> | ||||
public DiscordClient(DiscordConfig config) | public DiscordClient(DiscordConfig config) | ||||
{ | { | ||||
_log = new LogManager(config.LogLevel); | |||||
_log.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); | |||||
_discordLogger = _log.CreateLogger("Discord"); | |||||
_restLogger = _log.CreateLogger("Rest"); | |||||
_queueLogger = _log.CreateLogger("Queue"); | |||||
LogManager = new LogManager(config.LogLevel); | |||||
LogManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); | |||||
_discordLogger = LogManager.CreateLogger("Discord"); | |||||
_restLogger = LogManager.CreateLogger("Rest"); | |||||
_queueLogger = LogManager.CreateLogger("Queue"); | |||||
_connectionLock = new SemaphoreSlim(1, 1); | _connectionLock = new SemaphoreSlim(1, 1); | ||||
@@ -267,6 +267,8 @@ namespace Discord | |||||
public void Dispose() => Dispose(true); | public void Dispose() => Dispose(true); | ||||
ConnectionState IDiscordClient.ConnectionState => ConnectionState.Disconnected; | ConnectionState IDiscordClient.ConnectionState => ConnectionState.Disconnected; | ||||
ILogManager IDiscordClient.LogManager => LogManager; | |||||
Task IDiscordClient.ConnectAsync() { throw new NotSupportedException(); } | Task IDiscordClient.ConnectAsync() { throw new NotSupportedException(); } | ||||
Task IDiscordClient.DisconnectAsync() { throw new NotSupportedException(); } | Task IDiscordClient.DisconnectAsync() { throw new NotSupportedException(); } | ||||
} | } | ||||
@@ -13,12 +13,12 @@ namespace Discord | |||||
remove { _connectedEvent.Remove(value); } | remove { _connectedEvent.Remove(value); } | ||||
} | } | ||||
private readonly AsyncEvent<Func<Task>> _connectedEvent = new AsyncEvent<Func<Task>>(); | private readonly AsyncEvent<Func<Task>> _connectedEvent = new AsyncEvent<Func<Task>>(); | ||||
public event Func<Task> Disconnected | |||||
public event Func<Exception, Task> Disconnected | |||||
{ | { | ||||
add { _disconnectedEvent.Add(value); } | add { _disconnectedEvent.Add(value); } | ||||
remove { _disconnectedEvent.Remove(value); } | remove { _disconnectedEvent.Remove(value); } | ||||
} | } | ||||
private readonly AsyncEvent<Func<Task>> _disconnectedEvent = new AsyncEvent<Func<Task>>(); | |||||
private readonly AsyncEvent<Func<Exception, Task>> _disconnectedEvent = new AsyncEvent<Func<Exception, Task>>(); | |||||
public event Func<Task> Ready | public event Func<Task> Ready | ||||
{ | { | ||||
add { _readyEvent.Add(value); } | add { _readyEvent.Add(value); } | ||||
@@ -75,7 +75,7 @@ namespace Discord | |||||
AudioMode = config.AudioMode; | AudioMode = config.AudioMode; | ||||
WebSocketProvider = config.WebSocketProvider; | WebSocketProvider = config.WebSocketProvider; | ||||
_gatewayLogger = _log.CreateLogger("Gateway"); | |||||
_gatewayLogger = LogManager.CreateLogger("Gateway"); | |||||
#if BENCHMARK | #if BENCHMARK | ||||
_benchmarkLogger = _log.CreateLogger("Benchmark"); | _benchmarkLogger = _log.CreateLogger("Benchmark"); | ||||
#endif | #endif | ||||
@@ -94,7 +94,7 @@ namespace Discord | |||||
if (ex != null) | if (ex != null) | ||||
{ | { | ||||
await _gatewayLogger.WarningAsync($"Connection Closed: {ex.Message}").ConfigureAwait(false); | await _gatewayLogger.WarningAsync($"Connection Closed: {ex.Message}").ConfigureAwait(false); | ||||
await StartReconnectAsync().ConfigureAwait(false); | |||||
await StartReconnectAsync(ex).ConfigureAwait(false); | |||||
} | } | ||||
else | else | ||||
await _gatewayLogger.WarningAsync($"Connection Closed").ConfigureAwait(false); | await _gatewayLogger.WarningAsync($"Connection Closed").ConfigureAwait(false); | ||||
@@ -112,7 +112,7 @@ namespace Discord | |||||
protected override async Task OnLogoutAsync() | protected override async Task OnLogoutAsync() | ||||
{ | { | ||||
if (ConnectionState != ConnectionState.Disconnected) | if (ConnectionState != ConnectionState.Disconnected) | ||||
await DisconnectInternalAsync().ConfigureAwait(false); | |||||
await DisconnectInternalAsync(null).ConfigureAwait(false); | |||||
_voiceRegions = ImmutableDictionary.Create<string, VoiceRegion>(); | _voiceRegions = ImmutableDictionary.Create<string, VoiceRegion>(); | ||||
} | } | ||||
@@ -142,7 +142,7 @@ namespace Discord | |||||
var state = ConnectionState; | var state = ConnectionState; | ||||
if (state == ConnectionState.Connecting || state == ConnectionState.Connected) | if (state == ConnectionState.Connecting || state == ConnectionState.Connected) | ||||
await DisconnectInternalAsync().ConfigureAwait(false); | |||||
await DisconnectInternalAsync(null).ConfigureAwait(false); | |||||
ConnectionState = ConnectionState.Connecting; | ConnectionState = ConnectionState.Connecting; | ||||
await _gatewayLogger.InfoAsync("Connecting").ConfigureAwait(false); | await _gatewayLogger.InfoAsync("Connecting").ConfigureAwait(false); | ||||
@@ -165,7 +165,7 @@ namespace Discord | |||||
} | } | ||||
catch (Exception) | catch (Exception) | ||||
{ | { | ||||
await DisconnectInternalAsync().ConfigureAwait(false); | |||||
await DisconnectInternalAsync(null).ConfigureAwait(false); | |||||
throw; | throw; | ||||
} | } | ||||
} | } | ||||
@@ -176,11 +176,11 @@ namespace Discord | |||||
try | try | ||||
{ | { | ||||
_isReconnecting = false; | _isReconnecting = false; | ||||
await DisconnectInternalAsync().ConfigureAwait(false); | |||||
await DisconnectInternalAsync(null).ConfigureAwait(false); | |||||
} | } | ||||
finally { _connectionLock.Release(); } | finally { _connectionLock.Release(); } | ||||
} | } | ||||
private async Task DisconnectInternalAsync() | |||||
private async Task DisconnectInternalAsync(Exception ex) | |||||
{ | { | ||||
ulong guildId; | ulong guildId; | ||||
@@ -211,10 +211,10 @@ namespace Discord | |||||
ConnectionState = ConnectionState.Disconnected; | ConnectionState = ConnectionState.Disconnected; | ||||
await _gatewayLogger.InfoAsync("Disconnected").ConfigureAwait(false); | await _gatewayLogger.InfoAsync("Disconnected").ConfigureAwait(false); | ||||
await _disconnectedEvent.InvokeAsync().ConfigureAwait(false); | |||||
await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); | |||||
} | } | ||||
private async Task StartReconnectAsync() | |||||
private async Task StartReconnectAsync(Exception ex) | |||||
{ | { | ||||
//TODO: Is this thread-safe? | //TODO: Is this thread-safe? | ||||
if (_reconnectTask != null) return; | if (_reconnectTask != null) return; | ||||
@@ -222,6 +222,7 @@ namespace Discord | |||||
await _connectionLock.WaitAsync().ConfigureAwait(false); | await _connectionLock.WaitAsync().ConfigureAwait(false); | ||||
try | try | ||||
{ | { | ||||
await DisconnectInternalAsync(ex).ConfigureAwait(false); | |||||
if (_reconnectTask != null) return; | if (_reconnectTask != null) return; | ||||
_isReconnecting = true; | _isReconnecting = true; | ||||
_reconnectTask = ReconnectInternalAsync(); | _reconnectTask = ReconnectInternalAsync(); | ||||
@@ -469,7 +470,7 @@ namespace Discord | |||||
await _gatewayLogger.DebugAsync("Received Reconnect").ConfigureAwait(false); | await _gatewayLogger.DebugAsync("Received Reconnect").ConfigureAwait(false); | ||||
await _gatewayLogger.WarningAsync("Server requested a reconnect").ConfigureAwait(false); | await _gatewayLogger.WarningAsync("Server requested a reconnect").ConfigureAwait(false); | ||||
await StartReconnectAsync().ConfigureAwait(false); | |||||
await StartReconnectAsync(new Exception("Server requested a reconnect")).ConfigureAwait(false); | |||||
} | } | ||||
break; | break; | ||||
case GatewayOpCode.Dispatch: | case GatewayOpCode.Dispatch: | ||||
@@ -1113,9 +1114,7 @@ namespace Discord | |||||
var user = guild.GetUser(data.UserId); | var user = guild.GetUser(data.UserId); | ||||
if (user != null) | if (user != null) | ||||
{ | |||||
await _userVoiceStateUpdatedEvent.InvokeAsync(user, before, after).ConfigureAwait(false); | await _userVoiceStateUpdatedEvent.InvokeAsync(user, before, after).ConfigureAwait(false); | ||||
} | |||||
else | else | ||||
{ | { | ||||
await _gatewayLogger.WarningAsync("VOICE_STATE_UPDATE referenced an unknown user.").ConfigureAwait(false); | await _gatewayLogger.WarningAsync("VOICE_STATE_UPDATE referenced an unknown user.").ConfigureAwait(false); | ||||
@@ -1131,7 +1130,21 @@ namespace Discord | |||||
} | } | ||||
break; | break; | ||||
case "VOICE_SERVER_UPDATE": | case "VOICE_SERVER_UPDATE": | ||||
await _gatewayLogger.DebugAsync("Ignored Dispatch (VOICE_SERVER_UPDATE)").ConfigureAwait(false); | |||||
await _gatewayLogger.DebugAsync("Received Dispatch (VOICE_SERVER_UPDATE)").ConfigureAwait(false); | |||||
if (AudioMode != AudioMode.Disabled) | |||||
{ | |||||
var data = (payload as JToken).ToObject<VoiceServerUpdateEvent>(_serializer); | |||||
var guild = DataStore.GetGuild(data.GuildId); | |||||
if (guild != null) | |||||
await guild.ConnectAudio("wss://" + data.Endpoint, data.Token).ConfigureAwait(false); | |||||
else | |||||
{ | |||||
await _gatewayLogger.WarningAsync("VOICE_SERVER_UPDATE referenced an unknown guild.").ConfigureAwait(false); | |||||
return; | |||||
} | |||||
} | |||||
return; | return; | ||||
//Ignored (User only) | //Ignored (User only) | ||||
@@ -1183,7 +1196,7 @@ namespace Discord | |||||
if (ConnectionState == ConnectionState.Connected && (_guildDownloadTask?.IsCompleted ?? false)) | if (ConnectionState == ConnectionState.Connected && (_guildDownloadTask?.IsCompleted ?? false)) | ||||
{ | { | ||||
await _gatewayLogger.WarningAsync("Server missed last heartbeat").ConfigureAwait(false); | await _gatewayLogger.WarningAsync("Server missed last heartbeat").ConfigureAwait(false); | ||||
await StartReconnectAsync().ConfigureAwait(false); | |||||
await StartReconnectAsync(new Exception("Server missed last heartbeat")).ConfigureAwait(false); | |||||
return; | return; | ||||
} | } | ||||
} | } | ||||
@@ -5,6 +5,7 @@ using System.Collections.Concurrent; | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
using System.Linq; | using System.Linq; | ||||
using System.Threading; | |||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
using ChannelModel = Discord.API.Channel; | using ChannelModel = Discord.API.Channel; | ||||
using EmojiUpdateModel = Discord.API.Gateway.GuildEmojiUpdateEvent; | using EmojiUpdateModel = Discord.API.Gateway.GuildEmojiUpdateEvent; | ||||
@@ -17,8 +18,9 @@ using VoiceStateModel = Discord.API.VoiceState; | |||||
namespace Discord | namespace Discord | ||||
{ | { | ||||
internal class CachedGuild : Guild, IUserGuild, ICachedEntity<ulong> | |||||
internal class CachedGuild : Guild, ICachedEntity<ulong>, IGuild, IUserGuild | |||||
{ | { | ||||
private readonly SemaphoreSlim _audioLock; | |||||
private TaskCompletionSource<bool> _downloaderPromise; | private TaskCompletionSource<bool> _downloaderPromise; | ||||
private ConcurrentHashSet<ulong> _channels; | private ConcurrentHashSet<ulong> _channels; | ||||
private ConcurrentDictionary<ulong, CachedGuildUser> _members; | private ConcurrentDictionary<ulong, CachedGuildUser> _members; | ||||
@@ -27,7 +29,7 @@ namespace Discord | |||||
public bool Available { get; private set; } | public bool Available { get; private set; } | ||||
public int MemberCount { get; private set; } | public int MemberCount { get; private set; } | ||||
public int DownloadedMemberCount { get; private set; } | public int DownloadedMemberCount { get; private set; } | ||||
public IAudioClient AudioClient { get; private set; } | |||||
public AudioClient AudioClient { get; private set; } | |||||
public bool HasAllMembers => _downloaderPromise.Task.IsCompleted; | public bool HasAllMembers => _downloaderPromise.Task.IsCompleted; | ||||
public Task DownloaderPromise => _downloaderPromise.Task; | public Task DownloaderPromise => _downloaderPromise.Task; | ||||
@@ -48,6 +50,7 @@ namespace Discord | |||||
public CachedGuild(DiscordSocketClient discord, ExtendedModel model, DataStore dataStore) : base(discord, model) | public CachedGuild(DiscordSocketClient discord, ExtendedModel model, DataStore dataStore) : base(discord, model) | ||||
{ | { | ||||
_audioLock = new SemaphoreSlim(1, 1); | |||||
_downloaderPromise = new TaskCompletionSource<bool>(); | _downloaderPromise = new TaskCompletionSource<bool>(); | ||||
Update(model, UpdateSource.Creation, dataStore); | Update(model, UpdateSource.Creation, dataStore); | ||||
} | } | ||||
@@ -236,6 +239,55 @@ namespace Discord | |||||
return null; | return null; | ||||
} | } | ||||
public async Task ConnectAudio(string url, string token) | |||||
{ | |||||
AudioClient audioClient; | |||||
await _audioLock.WaitAsync().ConfigureAwait(false); | |||||
var voiceState = GetVoiceState(CurrentUser.Id).Value; | |||||
try | |||||
{ | |||||
audioClient = AudioClient; | |||||
if (audioClient == null) | |||||
{ | |||||
audioClient = new AudioClient(this); | |||||
audioClient.Disconnected += async ex => | |||||
{ | |||||
await _audioLock.WaitAsync().ConfigureAwait(false); | |||||
try | |||||
{ | |||||
if (ex != null) | |||||
{ | |||||
//Reconnect if we still have channel info. | |||||
//TODO: Is this threadsafe? Could channel data be deleted before we access it? | |||||
var voiceState2 = GetVoiceState(CurrentUser.Id); | |||||
if (voiceState2.HasValue) | |||||
{ | |||||
var voiceChannelId = voiceState2.Value.VoiceChannel?.Id; | |||||
if (voiceChannelId != null) | |||||
await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, voiceChannelId, voiceState2.Value.IsSelfDeafened, voiceState2.Value.IsSelfMuted); | |||||
} | |||||
} | |||||
else | |||||
{ | |||||
try { AudioClient.Dispose(); } catch { } | |||||
AudioClient = null; | |||||
} | |||||
} | |||||
finally | |||||
{ | |||||
_audioLock.Release(); | |||||
} | |||||
}; | |||||
AudioClient = audioClient; | |||||
} | |||||
} | |||||
finally | |||||
{ | |||||
_audioLock.Release(); | |||||
} | |||||
await audioClient.ConnectAsync(url, CurrentUser.Id, voiceState.VoiceSessionId, token).ConfigureAwait(false); | |||||
} | |||||
public CachedGuild Clone() => MemberwiseClone() as CachedGuild; | public CachedGuild Clone() => MemberwiseClone() as CachedGuild; | ||||
new internal ICachedGuildChannel ToChannel(ChannelModel model) | new internal ICachedGuildChannel ToChannel(ChannelModel model) | ||||
@@ -253,5 +305,6 @@ namespace Discord | |||||
bool IUserGuild.IsOwner => OwnerId == Discord.CurrentUser.Id; | bool IUserGuild.IsOwner => OwnerId == Discord.CurrentUser.Id; | ||||
GuildPermissions IUserGuild.Permissions => CurrentUser.GuildPermissions; | GuildPermissions IUserGuild.Permissions => CurrentUser.GuildPermissions; | ||||
IAudioClient IGuild.AudioClient => AudioClient; | |||||
} | } | ||||
} | } |
@@ -1,4 +1,5 @@ | |||||
using Discord.API; | using Discord.API; | ||||
using Discord.Logging; | |||||
using System; | using System; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.IO; | using System.IO; | ||||
@@ -13,6 +14,7 @@ namespace Discord | |||||
ConnectionState ConnectionState { get; } | ConnectionState ConnectionState { get; } | ||||
DiscordApiClient ApiClient { get; } | DiscordApiClient ApiClient { get; } | ||||
ILogManager LogManager { get; } | |||||
Task LoginAsync(TokenType tokenType, string token, bool validateToken = true); | Task LoginAsync(TokenType tokenType, string token, bool validateToken = true); | ||||
Task LogoutAsync(); | Task LogoutAsync(); | ||||