| @@ -5,7 +5,7 @@ | |||||
| using Discord.API.Converters; | using Discord.API.Converters; | ||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| namespace Discord.Audio.API | |||||
| namespace Discord.API | |||||
| { | { | ||||
| internal sealed class VoiceServerUpdateEvent | internal sealed class VoiceServerUpdateEvent | ||||
| { | { | ||||
| @@ -95,8 +95,7 @@ namespace Discord.Audio | |||||
| else | else | ||||
| { | { | ||||
| var logger = Client.Log().CreateLogger("Voice"); | var logger = Client.Log().CreateLogger("Voice"); | ||||
| var voiceSocket = new VoiceWebSocket(Client.Config, _config, logger); | |||||
| _defaultClient = new DiscordAudioClient(this, 0, logger, _client.WebSocket, voiceSocket); | |||||
| _defaultClient = new DiscordAudioClient(this, 0, logger, _client.WebSocket); | |||||
| } | } | ||||
| _talkingUsers = new ConcurrentDictionary<User, bool>(); | _talkingUsers = new ConcurrentDictionary<User, bool>(); | ||||
| @@ -145,27 +144,26 @@ namespace Discord.Audio | |||||
| return Task.FromResult(_defaultClient); | return Task.FromResult(_defaultClient); | ||||
| } | } | ||||
| var client = _voiceClients.GetOrAdd(server.Id, (Func<long, DiscordAudioClient>)(_ => | |||||
| var client = _voiceClients.GetOrAdd(server.Id, _ => | |||||
| { | { | ||||
| int id = unchecked(++_nextClientId); | int id = unchecked(++_nextClientId); | ||||
| var logger = Client.Log().CreateLogger($"Voice #{id}"); | var logger = Client.Log().CreateLogger($"Voice #{id}"); | ||||
| Net.WebSockets.GatewaySocket gatewaySocket = null; | |||||
| var voiceSocket = new VoiceWebSocket(Client.Config, _config, logger); | |||||
| var voiceClient = new DiscordAudioClient((AudioService)(this), (int)id, (Logger)logger, (Net.WebSockets.GatewaySocket)gatewaySocket, (VoiceWebSocket)voiceSocket); | |||||
| GatewaySocket gatewaySocket = null; | |||||
| var voiceClient = new DiscordAudioClient(this, id, logger, gatewaySocket); | |||||
| voiceClient.SetServerId(server.Id); | voiceClient.SetServerId(server.Id); | ||||
| voiceSocket.OnPacket += (s, e) => | |||||
| voiceClient.VoiceSocket.OnPacket += (s, e) => | |||||
| { | { | ||||
| RaiseOnPacket(e); | RaiseOnPacket(e); | ||||
| }; | }; | ||||
| voiceSocket.IsSpeaking += (s, e) => | |||||
| voiceClient.VoiceSocket.IsSpeaking += (s, e) => | |||||
| { | { | ||||
| var user = Client.GetUser(server, e.UserId); | var user = Client.GetUser(server, e.UserId); | ||||
| RaiseUserIsSpeakingUpdated(user, e.IsSpeaking); | RaiseUserIsSpeakingUpdated(user, e.IsSpeaking); | ||||
| }; | }; | ||||
| return voiceClient; | return voiceClient; | ||||
| })); | |||||
| }); | |||||
| //await client.Connect(gatewaySocket.Host, _client.Token).ConfigureAwait(false); | //await client.Connect(gatewaySocket.Host, _client.Token).ConfigureAwait(false); | ||||
| return Task.FromResult(client); | return Task.FromResult(client); | ||||
| } | } | ||||
| @@ -1,4 +1,5 @@ | |||||
| using Discord.Net.WebSockets; | |||||
| using Discord.API; | |||||
| using Discord.Net.WebSockets; | |||||
| using System; | using System; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| @@ -10,22 +11,29 @@ namespace Discord.Audio | |||||
| public int Id => _id; | public int Id => _id; | ||||
| private readonly AudioService _service; | private readonly AudioService _service; | ||||
| private readonly GatewaySocket _gatewaySocket; | |||||
| private readonly VoiceWebSocket _voiceSocket; | |||||
| private readonly Logger _logger; | private readonly Logger _logger; | ||||
| public long? ServerId => _voiceSocket.ServerId; | |||||
| public long? ChannelId => _voiceSocket.ChannelId; | |||||
| public GatewaySocket GatewaySocket => _gatewaySocket; | |||||
| private readonly GatewaySocket _gatewaySocket; | |||||
| public DiscordAudioClient(AudioService service, int id, Logger logger, GatewaySocket gatewaySocket, VoiceWebSocket voiceSocket) | |||||
| public VoiceWebSocket VoiceSocket => _voiceSocket; | |||||
| private readonly VoiceWebSocket _voiceSocket; | |||||
| public string Token => _token; | |||||
| private string _token; | |||||
| public long? ServerId => _voiceSocket.ServerId; | |||||
| public long? ChannelId => _voiceSocket.ChannelId; | |||||
| public DiscordAudioClient(AudioService service, int id, Logger logger, GatewaySocket gatewaySocket) | |||||
| { | { | ||||
| _service = service; | _service = service; | ||||
| _id = id; | _id = id; | ||||
| _logger = logger; | _logger = logger; | ||||
| _gatewaySocket = gatewaySocket; | _gatewaySocket = gatewaySocket; | ||||
| _voiceSocket = voiceSocket; | |||||
| _voiceSocket = new VoiceWebSocket(service.Client, this, logger); | |||||
| /*_voiceSocket.Connected += (s, e) => RaiseVoiceConnected(); | |||||
| /*_voiceSocket.Connected += (s, e) => RaiseVoiceConnected(); | |||||
| _voiceSocket.Disconnected += async (s, e) => | _voiceSocket.Disconnected += async (s, e) => | ||||
| { | { | ||||
| _voiceSocket.CurrentServerId; | _voiceSocket.CurrentServerId; | ||||
| @@ -37,7 +45,7 @@ namespace Discord.Audio | |||||
| await socket.Reconnect().ConfigureAwait(false); | await socket.Reconnect().ConfigureAwait(false); | ||||
| };*/ | };*/ | ||||
| /*_voiceSocket.IsSpeaking += (s, e) => | |||||
| /*_voiceSocket.IsSpeaking += (s, e) => | |||||
| { | { | ||||
| if (_voiceSocket.State == WebSocketState.Connected) | if (_voiceSocket.State == WebSocketState.Connected) | ||||
| { | { | ||||
| @@ -54,27 +62,28 @@ namespace Discord.Audio | |||||
| } | } | ||||
| };*/ | };*/ | ||||
| /*this.Connected += (s, e) => | |||||
| /*this.Connected += (s, e) => | |||||
| { | { | ||||
| _voiceSocket.ParentCancelToken = _cancelToken; | _voiceSocket.ParentCancelToken = _cancelToken; | ||||
| };*/ | };*/ | ||||
| _gatewaySocket.ReceivedDispatch += async (s, e) => | |||||
| _gatewaySocket.ReceivedDispatch += async (s, e) => | |||||
| { | { | ||||
| try | try | ||||
| { | { | ||||
| switch (e.Type) | switch (e.Type) | ||||
| { | { | ||||
| case "VOICE_SERVER_UPDATE": | case "VOICE_SERVER_UPDATE": | ||||
| { | |||||
| long serverId = IdConvert.ToLong(e.Payload.Value<string>("guild_id")); | |||||
| { | |||||
| var data = e.Payload.ToObject<VoiceServerUpdateEvent>(_gatewaySocket.Serializer); | |||||
| long serverId = data.ServerId; | |||||
| if (serverId == ServerId) | if (serverId == ServerId) | ||||
| { | { | ||||
| var client = _service.Client; | var client = _service.Client; | ||||
| string token = e.Payload.Value<string>("token"); | |||||
| _token = data.Token; | |||||
| _voiceSocket.Host = "wss://" + e.Payload.Value<string>("endpoint").Split(':')[0]; | _voiceSocket.Host = "wss://" + e.Payload.Value<string>("endpoint").Split(':')[0]; | ||||
| await _voiceSocket.Connect(client.CurrentUser.Id, _gatewaySocket.SessionId, token/*, client.CancelToken*/).ConfigureAwait(false); | |||||
| await _voiceSocket.Connect().ConfigureAwait(false); | |||||
| } | } | ||||
| } | } | ||||
| break; | break; | ||||
| @@ -26,7 +26,8 @@ namespace Discord.Net.WebSockets | |||||
| //private readonly Random _rand; | //private readonly Random _rand; | ||||
| private readonly int _targetAudioBufferLength; | private readonly int _targetAudioBufferLength; | ||||
| private readonly ConcurrentDictionary<uint, OpusDecoder> _decoders; | private readonly ConcurrentDictionary<uint, OpusDecoder> _decoders; | ||||
| private readonly AudioServiceConfig _audioConfig; | |||||
| private readonly DiscordAudioClient _audioClient; | |||||
| private readonly AudioServiceConfig _config; | |||||
| private OpusEncoder _encoder; | private OpusEncoder _encoder; | ||||
| private uint _ssrc; | private uint _ssrc; | ||||
| private ConcurrentDictionary<uint, long> _ssrcMapping; | private ConcurrentDictionary<uint, long> _ssrcMapping; | ||||
| @@ -37,8 +38,8 @@ namespace Discord.Net.WebSockets | |||||
| private bool _isEncrypted; | private bool _isEncrypted; | ||||
| private byte[] _secretKey, _encodingBuffer; | private byte[] _secretKey, _encodingBuffer; | ||||
| private ushort _sequence; | private ushort _sequence; | ||||
| private long? _serverId, _channelId, _userId; | |||||
| private string _sessionId, _token, _encryptionMode; | |||||
| private long? _serverId, _channelId; | |||||
| private string _encryptionMode; | |||||
| private int _ping; | private int _ping; | ||||
| private Thread _sendThread, _receiveThread; | private Thread _sendThread, _receiveThread; | ||||
| @@ -48,24 +49,21 @@ namespace Discord.Net.WebSockets | |||||
| public int Ping => _ping; | public int Ping => _ping; | ||||
| internal VoiceBuffer OutputBuffer => _sendBuffer; | internal VoiceBuffer OutputBuffer => _sendBuffer; | ||||
| public VoiceWebSocket(DiscordConfig config, AudioServiceConfig audioConfig, Logger logger) | |||||
| : base(config, logger) | |||||
| public VoiceWebSocket(DiscordClient client, DiscordAudioClient audioClient, Logger logger) | |||||
| : base(client, logger) | |||||
| { | { | ||||
| _audioConfig = audioConfig; | |||||
| _decoders = new ConcurrentDictionary<uint, OpusDecoder>(); | |||||
| _targetAudioBufferLength = _audioConfig.BufferLength / 20; //20 ms frames | |||||
| _audioClient = audioClient; | |||||
| _config = client.Audio().Config; | |||||
| _decoders = new ConcurrentDictionary<uint, OpusDecoder>(); | |||||
| _targetAudioBufferLength = _config.BufferLength / 20; //20 ms frames | |||||
| _encodingBuffer = new byte[MaxOpusSize]; | _encodingBuffer = new byte[MaxOpusSize]; | ||||
| _ssrcMapping = new ConcurrentDictionary<uint, long>(); | _ssrcMapping = new ConcurrentDictionary<uint, long>(); | ||||
| _encoder = new OpusEncoder(48000, _audioConfig.Channels, 20, _audioConfig.Bitrate, OpusApplication.Audio); | |||||
| _sendBuffer = new VoiceBuffer((int)Math.Ceiling(_audioConfig.BufferLength / (double)_encoder.FrameLength), _encoder.FrameSize); | |||||
| _encoder = new OpusEncoder(48000, _config.Channels, 20, _config.Bitrate, OpusApplication.Audio); | |||||
| _sendBuffer = new VoiceBuffer((int)Math.Ceiling(_config.BufferLength / (double)_encoder.FrameLength), _encoder.FrameSize); | |||||
| } | } | ||||
| public async Task Connect(long userId, string sessionId, string token) | |||||
| { | |||||
| _userId = userId; | |||||
| _sessionId = sessionId; | |||||
| _token = token; | |||||
| public async Task Connect() | |||||
| { | |||||
| await BeginConnect().ConfigureAwait(false); | await BeginConnect().ConfigureAwait(false); | ||||
| } | } | ||||
| public async Task Reconnect() | public async Task Reconnect() | ||||
| @@ -73,12 +71,12 @@ namespace Discord.Net.WebSockets | |||||
| try | try | ||||
| { | { | ||||
| var cancelToken = ParentCancelToken.Value; | var cancelToken = ParentCancelToken.Value; | ||||
| await Task.Delay(_config.ReconnectDelay, cancelToken).ConfigureAwait(false); | |||||
| await Task.Delay(_client.Config.ReconnectDelay, cancelToken).ConfigureAwait(false); | |||||
| while (!cancelToken.IsCancellationRequested) | while (!cancelToken.IsCancellationRequested) | ||||
| { | { | ||||
| try | try | ||||
| { | { | ||||
| await Connect(_userId.Value, _sessionId, _token).ConfigureAwait(false); | |||||
| await Connect().ConfigureAwait(false); | |||||
| break; | break; | ||||
| } | } | ||||
| catch (OperationCanceledException) { throw; } | catch (OperationCanceledException) { throw; } | ||||
| @@ -86,29 +84,26 @@ namespace Discord.Net.WebSockets | |||||
| { | { | ||||
| _logger.Error("Reconnect failed", ex); | _logger.Error("Reconnect failed", ex); | ||||
| //Net is down? We can keep trying to reconnect until the user runs Disconnect() | //Net is down? We can keep trying to reconnect until the user runs Disconnect() | ||||
| await Task.Delay(_config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); | |||||
| await Task.Delay(_client.Config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| catch (OperationCanceledException) { } | catch (OperationCanceledException) { } | ||||
| } | } | ||||
| public Task Disconnect() | |||||
| { | |||||
| return SignalDisconnect(wait: true); | |||||
| } | |||||
| public Task Disconnect() => _taskManager.Stop(); | |||||
| protected override async Task Run() | protected override async Task Run() | ||||
| { | { | ||||
| _udp = new UdpClient(new IPEndPoint(IPAddress.Any, 0)); | _udp = new UdpClient(new IPEndPoint(IPAddress.Any, 0)); | ||||
| List<Task> tasks = new List<Task>(); | List<Task> tasks = new List<Task>(); | ||||
| if ((_audioConfig.Mode & AudioMode.Outgoing) != 0) | |||||
| if ((_config.Mode & AudioMode.Outgoing) != 0) | |||||
| { | { | ||||
| _sendThread = new Thread(new ThreadStart(() => SendVoiceAsync(_cancelToken))); | _sendThread = new Thread(new ThreadStart(() => SendVoiceAsync(_cancelToken))); | ||||
| _sendThread.IsBackground = true; | _sendThread.IsBackground = true; | ||||
| _sendThread.Start(); | _sendThread.Start(); | ||||
| } | } | ||||
| if ((_audioConfig.Mode & AudioMode.Incoming) != 0) | |||||
| if ((_config.Mode & AudioMode.Incoming) != 0) | |||||
| { | { | ||||
| _receiveThread = new Thread(new ThreadStart(() => ReceiveVoiceAsync(_cancelToken))); | _receiveThread = new Thread(new ThreadStart(() => ReceiveVoiceAsync(_cancelToken))); | ||||
| _receiveThread.IsBackground = true; | _receiveThread.IsBackground = true; | ||||
| @@ -120,9 +115,9 @@ namespace Discord.Net.WebSockets | |||||
| #if !DOTNET5_4 | #if !DOTNET5_4 | ||||
| tasks.Add(WatcherAsync()); | tasks.Add(WatcherAsync()); | ||||
| #endif | #endif | ||||
| await RunTasks(tasks.ToArray()); | |||||
| await Cleanup(); | |||||
| tasks.AddRange(_engine.GetTasks(_cancelToken)); | |||||
| tasks.Add(HeartbeatAsync(_cancelToken)); | |||||
| await _taskManager.Start(tasks, _cancelTokenSource).ConfigureAwait(false); | |||||
| } | } | ||||
| protected override Task Cleanup() | protected override Task Cleanup() | ||||
| { | { | ||||
| @@ -141,12 +136,6 @@ namespace Discord.Net.WebSockets | |||||
| } | } | ||||
| ClearPCMFrames(); | ClearPCMFrames(); | ||||
| if (!_wasDisconnectUnexpected) | |||||
| { | |||||
| _userId = null; | |||||
| _sessionId = null; | |||||
| _token = null; | |||||
| } | |||||
| _udp = null; | _udp = null; | ||||
| return base.Cleanup(); | return base.Cleanup(); | ||||
| @@ -161,7 +150,7 @@ namespace Discord.Net.WebSockets | |||||
| int packetLength, resultOffset, resultLength; | int packetLength, resultOffset, resultLength; | ||||
| IPEndPoint endpoint = new IPEndPoint(IPAddress.Any, 0); | IPEndPoint endpoint = new IPEndPoint(IPAddress.Any, 0); | ||||
| if ((_audioConfig.Mode & AudioMode.Incoming) != 0) | |||||
| if ((_config.Mode & AudioMode.Incoming) != 0) | |||||
| { | { | ||||
| decodingBuffer = new byte[MaxOpusSize]; | decodingBuffer = new byte[MaxOpusSize]; | ||||
| nonce = new byte[24]; | nonce = new byte[24]; | ||||
| @@ -188,7 +177,7 @@ namespace Discord.Net.WebSockets | |||||
| if (packetLength > 0 && endpoint.Equals(_endpoint)) | if (packetLength > 0 && endpoint.Equals(_endpoint)) | ||||
| { | { | ||||
| if (_state != (int)WebSocketState.Connected) | |||||
| if (_state != (int)ConnectionState.Connected) | |||||
| { | { | ||||
| if (packetLength != 70) | if (packetLength != 70) | ||||
| return; | return; | ||||
| @@ -197,8 +186,8 @@ namespace Discord.Net.WebSockets | |||||
| int port = packet[68] | packet[69] << 8; | int port = packet[68] | packet[69] << 8; | ||||
| SendSelectProtocol(ip, port); | SendSelectProtocol(ip, port); | ||||
| if ((_audioConfig.Mode & AudioMode.Incoming) == 0) | |||||
| return; | |||||
| if ((_config.Mode & AudioMode.Incoming) == 0) | |||||
| return; //We dont need this thread anymore | |||||
| } | } | ||||
| else | else | ||||
| { | { | ||||
| @@ -258,7 +247,7 @@ namespace Discord.Net.WebSockets | |||||
| { | { | ||||
| try | try | ||||
| { | { | ||||
| while (!cancelToken.IsCancellationRequested && _state != (int)WebSocketState.Connected) | |||||
| while (!cancelToken.IsCancellationRequested && _state != (int)ConnectionState.Connected) | |||||
| Thread.Sleep(1); | Thread.Sleep(1); | ||||
| if (cancelToken.IsCancellationRequested) | if (cancelToken.IsCancellationRequested) | ||||
| @@ -410,14 +399,15 @@ namespace Discord.Net.WebSockets | |||||
| { | { | ||||
| case VoiceOpCodes.Ready: | case VoiceOpCodes.Ready: | ||||
| { | { | ||||
| if (_state != (int)WebSocketState.Connected) | |||||
| if (_state != (int)ConnectionState.Connected) | |||||
| { | { | ||||
| var payload = (msg.Payload as JToken).ToObject<VoiceReadyEvent>(_serializer); | var payload = (msg.Payload as JToken).ToObject<VoiceReadyEvent>(_serializer); | ||||
| _heartbeatInterval = payload.HeartbeatInterval; | _heartbeatInterval = payload.HeartbeatInterval; | ||||
| _ssrc = payload.SSRC; | _ssrc = payload.SSRC; | ||||
| _endpoint = new IPEndPoint((await Dns.GetHostAddressesAsync(Host.Replace("wss://", "")).ConfigureAwait(false)).FirstOrDefault(), payload.Port); | |||||
| var address = (await Dns.GetHostAddressesAsync(Host.Replace("wss://", "")).ConfigureAwait(false)).FirstOrDefault(); | |||||
| _endpoint = new IPEndPoint(address, payload.Port); | |||||
| if (_audioConfig.EnableEncryption) | |||||
| if (_config.EnableEncryption) | |||||
| { | { | ||||
| if (payload.Modes.Contains(EncryptedMode)) | if (payload.Modes.Contains(EncryptedMode)) | ||||
| { | { | ||||
| @@ -458,7 +448,7 @@ namespace Discord.Net.WebSockets | |||||
| var payload = (msg.Payload as JToken).ToObject<JoinServerEvent>(_serializer); | var payload = (msg.Payload as JToken).ToObject<JoinServerEvent>(_serializer); | ||||
| _secretKey = payload.SecretKey; | _secretKey = payload.SecretKey; | ||||
| SendIsTalking(true); | SendIsTalking(true); | ||||
| await EndConnect(); | |||||
| EndConnect(); | |||||
| } | } | ||||
| break; | break; | ||||
| case VoiceOpCodes.Speaking: | case VoiceOpCodes.Speaking: | ||||
| @@ -507,9 +497,9 @@ namespace Discord.Net.WebSockets | |||||
| { | { | ||||
| var msg = new IdentifyCommand(); | var msg = new IdentifyCommand(); | ||||
| msg.Payload.ServerId = _serverId.Value; | msg.Payload.ServerId = _serverId.Value; | ||||
| msg.Payload.SessionId = _sessionId; | |||||
| msg.Payload.Token = _token; | |||||
| msg.Payload.UserId = _userId.Value; | |||||
| msg.Payload.SessionId = _client.SessionId; | |||||
| msg.Payload.Token = _audioClient.Token; | |||||
| msg.Payload.UserId = _client.UserId.Value; | |||||
| QueueMessage(msg); | QueueMessage(msg); | ||||
| } | } | ||||
| @@ -93,7 +93,7 @@ namespace Discord.Commands | |||||
| } | } | ||||
| internal void SetRunFunc(Action<CommandEventArgs> func) | internal void SetRunFunc(Action<CommandEventArgs> func) | ||||
| { | { | ||||
| _runFunc = e => { func(e); return TaskHelper.CompletedTask; }; | |||||
| _runFunc = TaskHelper.ToAsync(func); | |||||
| } | } | ||||
| internal Task Run(CommandEventArgs args) | internal Task Run(CommandEventArgs args) | ||||
| { | { | ||||
| @@ -68,8 +68,8 @@ | |||||
| </Reference> | </Reference> | ||||
| </ItemGroup> | </ItemGroup> | ||||
| <ItemGroup> | <ItemGroup> | ||||
| <Compile Include="..\Discord.Net\API\Converters\LongCollectionConverter.cs"> | |||||
| <Link>API\Converters\LongCollectionConverter.cs</Link> | |||||
| <Compile Include="..\Discord.Net\API\Converters\LongStringCollectionConverter.cs"> | |||||
| <Link>API\Converters\LongStringCollectionConverter.cs</Link> | |||||
| </Compile> | </Compile> | ||||
| <Compile Include="..\Discord.Net\API\Converters\LongStringConverter.cs"> | <Compile Include="..\Discord.Net\API\Converters\LongStringConverter.cs"> | ||||
| <Link>API\Converters\LongStringConverter.cs</Link> | <Link>API\Converters\LongStringConverter.cs</Link> | ||||
| @@ -77,12 +77,12 @@ | |||||
| <Compile Include="..\Discord.Net\API\Endpoints.cs"> | <Compile Include="..\Discord.Net\API\Endpoints.cs"> | ||||
| <Link>API\Endpoints.cs</Link> | <Link>API\Endpoints.cs</Link> | ||||
| </Compile> | </Compile> | ||||
| <Compile Include="..\Discord.Net\API\Enums\AvatarImageType.cs"> | |||||
| <Link>API\Enums\AvatarImageType.cs</Link> | |||||
| </Compile> | |||||
| <Compile Include="..\Discord.Net\API\Enums\ChannelType.cs"> | <Compile Include="..\Discord.Net\API\Enums\ChannelType.cs"> | ||||
| <Link>API\Enums\ChannelType.cs</Link> | <Link>API\Enums\ChannelType.cs</Link> | ||||
| </Compile> | </Compile> | ||||
| <Compile Include="..\Discord.Net\API\Enums\ImageType.cs"> | |||||
| <Link>API\Enums\ImageType.cs</Link> | |||||
| </Compile> | |||||
| <Compile Include="..\Discord.Net\API\Enums\PermissionTarget.cs"> | <Compile Include="..\Discord.Net\API\Enums\PermissionTarget.cs"> | ||||
| <Link>API\Enums\PermissionTarget.cs</Link> | <Link>API\Enums\PermissionTarget.cs</Link> | ||||
| </Compile> | </Compile> | ||||
| @@ -176,6 +176,9 @@ | |||||
| <Compile Include="..\Discord.Net\Helpers\Reference.cs"> | <Compile Include="..\Discord.Net\Helpers\Reference.cs"> | ||||
| <Link>Helpers\Reference.cs</Link> | <Link>Helpers\Reference.cs</Link> | ||||
| </Compile> | </Compile> | ||||
| <Compile Include="..\Discord.Net\Helpers\TaskManager.cs"> | |||||
| <Link>Helpers\TaskManager.cs</Link> | |||||
| </Compile> | |||||
| <Compile Include="..\Discord.Net\Models\Channel.cs"> | <Compile Include="..\Discord.Net\Models\Channel.cs"> | ||||
| <Link>Models\Channel.cs</Link> | <Link>Models\Channel.cs</Link> | ||||
| </Compile> | </Compile> | ||||
| @@ -1,4 +1,5 @@ | |||||
| using System.Threading.Tasks; | |||||
| using System; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| @@ -7,7 +8,26 @@ namespace Discord | |||||
| public static Task CompletedTask { get; } | public static Task CompletedTask { get; } | ||||
| static TaskHelper() | static TaskHelper() | ||||
| { | { | ||||
| #if DOTNET54 | |||||
| CompletedTask = Task.CompletedTask; | |||||
| #else | |||||
| CompletedTask = Task.Delay(0); | CompletedTask = Task.Delay(0); | ||||
| #endif | |||||
| } | |||||
| public static Func<Task> ToAsync(Action action) | |||||
| { | |||||
| return () => | |||||
| { | |||||
| action(); return CompletedTask; | |||||
| }; | |||||
| } | |||||
| public static Func<T, Task> ToAsync<T>(Action<T> action) | |||||
| { | |||||
| return x => | |||||
| { | |||||
| action(x); return CompletedTask; | |||||
| }; | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -4,7 +4,7 @@ using System.Collections.Generic; | |||||
| namespace Discord.API.Converters | namespace Discord.API.Converters | ||||
| { | { | ||||
| public class EnumerableLongStringConverter : JsonConverter | |||||
| public class LongStringEnumerableConverter : JsonConverter | |||||
| { | { | ||||
| public override bool CanConvert(Type objectType) | public override bool CanConvert(Type objectType) | ||||
| { | { | ||||
| @@ -38,7 +38,7 @@ namespace Discord.API.Converters | |||||
| } | } | ||||
| } | } | ||||
| internal class LongArrayStringConverter : JsonConverter | |||||
| internal class LongStringArrayConverter : JsonConverter | |||||
| { | { | ||||
| public override bool CanConvert(Type objectType) | public override bool CanConvert(Type objectType) | ||||
| { | { | ||||
| @@ -36,7 +36,7 @@ namespace Discord.API | |||||
| [JsonProperty("joined_at")] | [JsonProperty("joined_at")] | ||||
| public DateTime? JoinedAt; | public DateTime? JoinedAt; | ||||
| [JsonProperty("roles")] | [JsonProperty("roles")] | ||||
| [JsonConverter(typeof(LongArrayStringConverter))] | |||||
| [JsonConverter(typeof(LongStringArrayConverter))] | |||||
| public long[] Roles; | public long[] Roles; | ||||
| } | } | ||||
| public class ExtendedMemberInfo : MemberInfo | public class ExtendedMemberInfo : MemberInfo | ||||
| @@ -53,7 +53,7 @@ namespace Discord.API | |||||
| [JsonProperty("status")] | [JsonProperty("status")] | ||||
| public string Status; | public string Status; | ||||
| [JsonProperty("roles")] //TODO: Might be temporary | [JsonProperty("roles")] //TODO: Might be temporary | ||||
| [JsonConverter(typeof(LongArrayStringConverter))] | |||||
| [JsonConverter(typeof(LongStringArrayConverter))] | |||||
| public long[] Roles; | public long[] Roles; | ||||
| } | } | ||||
| public class VoiceMemberInfo : MemberReference | public class VoiceMemberInfo : MemberReference | ||||
| @@ -88,7 +88,7 @@ namespace Discord.API | |||||
| [JsonConverter(typeof(NullableLongStringConverter))] | [JsonConverter(typeof(NullableLongStringConverter))] | ||||
| public long? ChannelId; | public long? ChannelId; | ||||
| [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] | [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] | ||||
| [JsonConverter(typeof(EnumerableLongStringConverter))] | |||||
| [JsonConverter(typeof(LongStringEnumerableConverter))] | |||||
| public IEnumerable<long> Roles; | public IEnumerable<long> Roles; | ||||
| } | } | ||||
| @@ -108,7 +108,7 @@ namespace Discord.API | |||||
| [JsonProperty("content")] | [JsonProperty("content")] | ||||
| public string Content; | public string Content; | ||||
| [JsonProperty("mentions")] | [JsonProperty("mentions")] | ||||
| [JsonConverter(typeof(EnumerableLongStringConverter))] | |||||
| [JsonConverter(typeof(LongStringEnumerableConverter))] | |||||
| public IEnumerable<long> Mentions; | public IEnumerable<long> Mentions; | ||||
| [JsonProperty("nonce", NullValueHandling = NullValueHandling.Ignore)] | [JsonProperty("nonce", NullValueHandling = NullValueHandling.Ignore)] | ||||
| public string Nonce; | public string Nonce; | ||||
| @@ -123,7 +123,7 @@ namespace Discord.API | |||||
| [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] | [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] | ||||
| public string Content; | public string Content; | ||||
| [JsonProperty("mentions", NullValueHandling = NullValueHandling.Ignore)] | [JsonProperty("mentions", NullValueHandling = NullValueHandling.Ignore)] | ||||
| [JsonConverter(typeof(EnumerableLongStringConverter))] | |||||
| [JsonConverter(typeof(LongStringEnumerableConverter))] | |||||
| public IEnumerable<long> Mentions; | public IEnumerable<long> Mentions; | ||||
| } | } | ||||
| public sealed class EditMessageResponse : MessageInfo { } | public sealed class EditMessageResponse : MessageInfo { } | ||||
| @@ -380,10 +380,11 @@ namespace Discord | |||||
| else | else | ||||
| { | { | ||||
| await _api.EditMessage( | await _api.EditMessage( | ||||
| msg.Id, | |||||
| msg.Channel.Id, | |||||
| queuedMessage.Text, | |||||
| queuedMessage.MentionedUsers); | |||||
| msg.Id, | |||||
| msg.Channel.Id, | |||||
| queuedMessage.Text, | |||||
| queuedMessage.MentionedUsers) | |||||
| .ConfigureAwait(false); | |||||
| } | } | ||||
| } | } | ||||
| catch (WebException) { break; } | catch (WebException) { break; } | ||||
| @@ -140,7 +140,7 @@ namespace Discord | |||||
| if (role == null) throw new ArgumentNullException(nameof(role)); | if (role == null) throw new ArgumentNullException(nameof(role)); | ||||
| CheckReady(); | CheckReady(); | ||||
| try { await _api.DeleteRole(role.Server.Id, role.Id); } | |||||
| try { await _api.DeleteRole(role.Server.Id, role.Id).ConfigureAwait(false); } | |||||
| catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } | catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } | ||||
| } | } | ||||
| @@ -118,7 +118,8 @@ namespace Discord | |||||
| { | { | ||||
| CheckReady(); | CheckReady(); | ||||
| return (await _api.GetVoiceRegions()).Select(x => new Region { Id = x.Id, Name = x.Name, Hostname = x.Hostname, Port = x.Port }); | |||||
| var regions = await _api.GetVoiceRegions().ConfigureAwait(false); | |||||
| return regions.Select(x => new Region { Id = x.Id, Name = x.Name, Hostname = x.Hostname, Port = x.Port }); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -254,7 +254,7 @@ namespace Discord | |||||
| if (days <= 0) throw new ArgumentOutOfRangeException(nameof(days)); | if (days <= 0) throw new ArgumentOutOfRangeException(nameof(days)); | ||||
| CheckReady(); | CheckReady(); | ||||
| var response = await _api.PruneUsers(server.Id, days, simulate); | |||||
| var response = await _api.PruneUsers(server.Id, days, simulate).ConfigureAwait(false); | |||||
| return response.Pruned ?? 0; | return response.Pruned ?? 0; | ||||
| } | } | ||||
| @@ -275,11 +275,11 @@ namespace Discord | |||||
| await _api.EditProfile(currentPassword: currentPassword, | await _api.EditProfile(currentPassword: currentPassword, | ||||
| username: username ?? _privateUser?.Name, email: email ?? _privateUser?.Global.Email, password: password, | username: username ?? _privateUser?.Name, email: email ?? _privateUser?.Global.Email, password: password, | ||||
| avatar: avatar, avatarType: avatarType, existingAvatar: _privateUser?.AvatarId); | |||||
| avatar: avatar, avatarType: avatarType, existingAvatar: _privateUser?.AvatarId).ConfigureAwait(false); | |||||
| if (password != null) | if (password != null) | ||||
| { | { | ||||
| var loginResponse = await _api.Login(_privateUser.Global.Email, password); | |||||
| var loginResponse = await _api.Login(_privateUser.Global.Email, password).ConfigureAwait(false); | |||||
| _api.Token = loginResponse.Token; | _api.Token = loginResponse.Token; | ||||
| } | } | ||||
| } | } | ||||
| @@ -11,15 +11,15 @@ using System.Threading.Tasks; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| public enum DiscordClientState : byte | |||||
| { | |||||
| Disconnected, | |||||
| Connecting, | |||||
| Connected, | |||||
| Disconnecting | |||||
| } | |||||
| public enum ConnectionState : byte | |||||
| { | |||||
| Disconnected, | |||||
| Connecting, | |||||
| Connected, | |||||
| Disconnecting | |||||
| } | |||||
| public class DisconnectedEventArgs : EventArgs | |||||
| public class DisconnectedEventArgs : EventArgs | |||||
| { | { | ||||
| public readonly bool WasUnexpected; | public readonly bool WasUnexpected; | ||||
| public readonly Exception Error; | public readonly Exception Error; | ||||
| @@ -51,25 +51,24 @@ namespace Discord | |||||
| { | { | ||||
| public static readonly string Version = typeof(DiscordClient).GetTypeInfo().Assembly.GetName().Version.ToString(3); | public static readonly string Version = typeof(DiscordClient).GetTypeInfo().Assembly.GetName().Version.ToString(3); | ||||
| private readonly ManualResetEvent _disconnectedEvent; | |||||
| private readonly LogService _log; | |||||
| private readonly Logger _logger, _restLogger, _cacheLogger; | |||||
| private readonly Dictionary<Type, object> _singletons; | |||||
| private readonly object _cacheLock; | |||||
| private readonly Semaphore _lock; | |||||
| private readonly ManualResetEvent _disconnectedEvent; | |||||
| private readonly ManualResetEventSlim _connectedEvent; | private readonly ManualResetEventSlim _connectedEvent; | ||||
| private readonly Dictionary<Type, object> _singletons; | |||||
| private readonly LogService _log; | |||||
| private readonly object _cacheLock; | |||||
| private Logger _logger, _restLogger, _cacheLogger; | |||||
| private readonly TaskManager _taskManager; | |||||
| private bool _sentInitialLog; | private bool _sentInitialLog; | ||||
| private UserStatus _status; | private UserStatus _status; | ||||
| private int? _gameId; | private int? _gameId; | ||||
| private Task _runTask; | |||||
| private ExceptionDispatchInfo _disconnectReason; | |||||
| private bool _wasDisconnectUnexpected; | |||||
| /// <summary> Returns the configuration object used to make this client. Note that this object cannot be edited directly - to change the configuration of this client, use the DiscordClient(DiscordClientConfig config) constructor. </summary> | /// <summary> Returns the configuration object used to make this client. Note that this object cannot be edited directly - to change the configuration of this client, use the DiscordClient(DiscordClientConfig config) constructor. </summary> | ||||
| public DiscordConfig Config => _config; | public DiscordConfig Config => _config; | ||||
| private readonly DiscordConfig _config; | private readonly DiscordConfig _config; | ||||
| /// <summary> Returns the current connection state of this client. </summary> | /// <summary> Returns the current connection state of this client. </summary> | ||||
| public DiscordClientState State => (DiscordClientState)_state; | |||||
| public ConnectionState State => (ConnectionState)_state; | |||||
| private int _state; | private int _state; | ||||
| /// <summary> Gives direct access to the underlying DiscordAPIClient. This can be used to modify objects not in cache. </summary> | /// <summary> Gives direct access to the underlying DiscordAPIClient. This can be used to modify objects not in cache. </summary> | ||||
| @@ -82,12 +81,17 @@ namespace Discord | |||||
| public string GatewayUrl => _gateway; | public string GatewayUrl => _gateway; | ||||
| private string _gateway; | private string _gateway; | ||||
| public string Token => _token; | public string Token => _token; | ||||
| private string _token; | private string _token; | ||||
| /// <summary> Returns a cancellation token that triggers when the client is manually disconnected. </summary> | |||||
| public CancellationToken CancelToken => _cancelToken; | |||||
| public string SessionId => _sessionId; | |||||
| private string _sessionId; | |||||
| public long? UserId => _privateUser?.Id; | |||||
| /// <summary> Returns a cancellation token that triggers when the client is manually disconnected. </summary> | |||||
| public CancellationToken CancelToken => _cancelToken; | |||||
| private CancellationTokenSource _cancelTokenSource; | private CancellationTokenSource _cancelTokenSource; | ||||
| private CancellationToken _cancelToken; | private CancellationToken _cancelToken; | ||||
| @@ -111,35 +115,37 @@ namespace Discord | |||||
| _config.Lock(); | _config.Lock(); | ||||
| _nonceRand = new Random(); | _nonceRand = new Random(); | ||||
| _state = (int)DiscordClientState.Disconnected; | |||||
| _state = (int)ConnectionState.Disconnected; | |||||
| _status = UserStatus.Online; | _status = UserStatus.Online; | ||||
| //Services | //Services | ||||
| _singletons = new Dictionary<Type, object>(); | _singletons = new Dictionary<Type, object>(); | ||||
| _log = AddService(new LogService()); | _log = AddService(new LogService()); | ||||
| CreateMainLogger(); | |||||
| _logger = CreateMainLogger(); | |||||
| //Async | |||||
| //Async | |||||
| _lock = new Semaphore(1, 1); | |||||
| _taskManager = new TaskManager(Cleanup); | |||||
| _cancelToken = new CancellationToken(true); | _cancelToken = new CancellationToken(true); | ||||
| _disconnectedEvent = new ManualResetEvent(true); | _disconnectedEvent = new ManualResetEvent(true); | ||||
| _connectedEvent = new ManualResetEventSlim(false); | _connectedEvent = new ManualResetEventSlim(false); | ||||
| //Cache | |||||
| _cacheLock = new object(); | |||||
| //Cache | |||||
| _cacheLock = new object(); | |||||
| _channels = new Channels(this, _cacheLock); | _channels = new Channels(this, _cacheLock); | ||||
| _users = new Users(this, _cacheLock); | _users = new Users(this, _cacheLock); | ||||
| _messages = new Messages(this, _cacheLock, Config.MessageCacheSize > 0); | _messages = new Messages(this, _cacheLock, Config.MessageCacheSize > 0); | ||||
| _roles = new Roles(this, _cacheLock); | _roles = new Roles(this, _cacheLock); | ||||
| _servers = new Servers(this, _cacheLock); | _servers = new Servers(this, _cacheLock); | ||||
| _globalUsers = new GlobalUsers(this, _cacheLock); | _globalUsers = new GlobalUsers(this, _cacheLock); | ||||
| CreateCacheLogger(); | |||||
| _cacheLogger = CreateCacheLogger(); | |||||
| //Networking | |||||
| _webSocket = new GatewaySocket(_config, _log.CreateLogger("WebSocket")); | |||||
| //Networking | |||||
| _webSocket = new GatewaySocket(this, _log.CreateLogger("WebSocket")); | |||||
| var settings = new JsonSerializerSettings(); | var settings = new JsonSerializerSettings(); | ||||
| _webSocket.Connected += (s, e) => | _webSocket.Connected += (s, e) => | ||||
| { | { | ||||
| if (_state == (int)DiscordClientState.Connecting) | |||||
| if (_state == (int)ConnectionState.Connecting) | |||||
| EndConnect(); | EndConnect(); | ||||
| }; | }; | ||||
| _webSocket.Disconnected += (s, e) => | _webSocket.Disconnected += (s, e) => | ||||
| @@ -157,88 +163,94 @@ namespace Discord | |||||
| _api.CancelToken = _cancelToken; | _api.CancelToken = _cancelToken; | ||||
| await SendStatus().ConfigureAwait(false); | await SendStatus().ConfigureAwait(false); | ||||
| }; | }; | ||||
| CreateRestLogger(); | |||||
| _restLogger = CreateRestLogger(); | |||||
| //Import/Export | //Import/Export | ||||
| _messageImporter = new JsonSerializer(); | _messageImporter = new JsonSerializer(); | ||||
| _messageImporter.ContractResolver = new Message.ImportResolver(); | _messageImporter.ContractResolver = new Message.ImportResolver(); | ||||
| } | } | ||||
| private void CreateMainLogger() | |||||
| { | |||||
| _logger = _log.CreateLogger("Client"); | |||||
| if (_logger.Level >= LogSeverity.Info) | |||||
| { | |||||
| JoinedServer += (s, e) => _logger.Info($"Server Created: {e.Server?.Name ?? "[Private]"}"); | |||||
| LeftServer += (s, e) => _logger.Info($"Server Destroyed: {e.Server?.Name ?? "[Private]"}"); | |||||
| ServerUpdated += (s, e) => _logger.Info($"Server Updated: {e.Server?.Name ?? "[Private]"}"); | |||||
| ServerAvailable += (s, e) => _logger.Info($"Server Available: {e.Server?.Name ?? "[Private]"}"); | |||||
| ServerUnavailable += (s, e) => _logger.Info($"Server Unavailable: {e.Server?.Name ?? "[Private]"}"); | |||||
| ChannelCreated += (s, e) => _logger.Info($"Channel Created: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}"); | |||||
| ChannelDestroyed += (s, e) => _logger.Info($"Channel Destroyed: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}"); | |||||
| ChannelUpdated += (s, e) => _logger.Info($"Channel Updated: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}"); | |||||
| MessageReceived += (s, e) => _logger.Info($"Message Received: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}"); | |||||
| MessageDeleted += (s, e) => _logger.Info($"Message Deleted: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}"); | |||||
| MessageUpdated += (s, e) => _logger.Info($"Message Update: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}"); | |||||
| RoleCreated += (s, e) => _logger.Info($"Role Created: {e.Server?.Name ?? "[Private]"}/{e.Role?.Name}"); | |||||
| RoleUpdated += (s, e) => _logger.Info($"Role Updated: {e.Server?.Name ?? "[Private]"}/{e.Role?.Name}"); | |||||
| RoleDeleted += (s, e) => _logger.Info($"Role Deleted: {e.Server?.Name ?? "[Private]"}/{e.Role?.Name}"); | |||||
| UserBanned += (s, e) => _logger.Info($"Banned User: {e.Server?.Name ?? "[Private]" }/{e.UserId}"); | |||||
| UserUnbanned += (s, e) => _logger.Info($"Unbanned User: {e.Server?.Name ?? "[Private]"}/{e.UserId}"); | |||||
| UserJoined += (s, e) => _logger.Info($"User Joined: {e.Server?.Name ?? "[Private]"}/{e.User.Name}"); | |||||
| UserLeft += (s, e) => _logger.Info($"User Left: {e.Server?.Name ?? "[Private]"}/{e.User.Name}"); | |||||
| UserUpdated += (s, e) => _logger.Info($"User Updated: {e.Server?.Name ?? "[Private]"}/{e.User.Name}"); | |||||
| UserVoiceStateUpdated += (s, e) => _logger.Info($"Voice Updated: {e.Server?.Name ?? "[Private]"}/{e.User.Name}"); | |||||
| ProfileUpdated += (s, e) => _logger.Info("Profile Updated"); | |||||
| private Logger CreateMainLogger() | |||||
| { | |||||
| Logger logger = null; | |||||
| if (_log.Level >= LogSeverity.Info) | |||||
| { | |||||
| logger = _log.CreateLogger("Client"); | |||||
| JoinedServer += (s, e) => logger.Info($"Server Created: {e.Server?.Name ?? "[Private]"}"); | |||||
| LeftServer += (s, e) => logger.Info($"Server Destroyed: {e.Server?.Name ?? "[Private]"}"); | |||||
| ServerUpdated += (s, e) => logger.Info($"Server Updated: {e.Server?.Name ?? "[Private]"}"); | |||||
| ServerAvailable += (s, e) => logger.Info($"Server Available: {e.Server?.Name ?? "[Private]"}"); | |||||
| ServerUnavailable += (s, e) => logger.Info($"Server Unavailable: {e.Server?.Name ?? "[Private]"}"); | |||||
| ChannelCreated += (s, e) => logger.Info($"Channel Created: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}"); | |||||
| ChannelDestroyed += (s, e) => logger.Info($"Channel Destroyed: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}"); | |||||
| ChannelUpdated += (s, e) => logger.Info($"Channel Updated: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}"); | |||||
| MessageReceived += (s, e) => logger.Info($"Message Received: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}"); | |||||
| MessageDeleted += (s, e) => logger.Info($"Message Deleted: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}"); | |||||
| MessageUpdated += (s, e) => logger.Info($"Message Update: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}"); | |||||
| RoleCreated += (s, e) => logger.Info($"Role Created: {e.Server?.Name ?? "[Private]"}/{e.Role?.Name}"); | |||||
| RoleUpdated += (s, e) => logger.Info($"Role Updated: {e.Server?.Name ?? "[Private]"}/{e.Role?.Name}"); | |||||
| RoleDeleted += (s, e) => logger.Info($"Role Deleted: {e.Server?.Name ?? "[Private]"}/{e.Role?.Name}"); | |||||
| UserBanned += (s, e) => logger.Info($"Banned User: {e.Server?.Name ?? "[Private]" }/{e.UserId}"); | |||||
| UserUnbanned += (s, e) => logger.Info($"Unbanned User: {e.Server?.Name ?? "[Private]"}/{e.UserId}"); | |||||
| UserJoined += (s, e) => logger.Info($"User Joined: {e.Server?.Name ?? "[Private]"}/{e.User.Name}"); | |||||
| UserLeft += (s, e) => logger.Info($"User Left: {e.Server?.Name ?? "[Private]"}/{e.User.Name}"); | |||||
| UserUpdated += (s, e) => logger.Info($"User Updated: {e.Server?.Name ?? "[Private]"}/{e.User.Name}"); | |||||
| UserVoiceStateUpdated += (s, e) => logger.Info($"Voice Updated: {e.Server?.Name ?? "[Private]"}/{e.User.Name}"); | |||||
| ProfileUpdated += (s, e) => logger.Info("Profile Updated"); | |||||
| } | } | ||||
| if (_log.Level >= LogSeverity.Verbose) | if (_log.Level >= LogSeverity.Verbose) | ||||
| { | { | ||||
| UserIsTypingUpdated += (s, e) => _logger.Verbose($"Is Typing: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.User?.Name}"); | |||||
| MessageAcknowledged += (s, e) => _logger.Verbose($"Ack Message: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}"); | |||||
| MessageSent += (s, e) => _logger.Verbose($"Sent Message: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}"); | |||||
| UserPresenceUpdated += (s, e) => _logger.Verbose($"Presence Updated: {e.Server?.Name ?? "[Private]"}/{e.User?.Name}"); | |||||
| UserIsTypingUpdated += (s, e) => logger.Verbose($"Is Typing: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.User?.Name}"); | |||||
| MessageAcknowledged += (s, e) => logger.Verbose($"Ack Message: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}"); | |||||
| MessageSent += (s, e) => logger.Verbose($"Sent Message: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}"); | |||||
| UserPresenceUpdated += (s, e) => logger.Verbose($"Presence Updated: {e.Server?.Name ?? "[Private]"}/{e.User?.Name}"); | |||||
| } | } | ||||
| return logger; | |||||
| } | } | ||||
| private void CreateRestLogger() | |||||
| { | |||||
| _restLogger = _log.CreateLogger("Rest"); | |||||
| if (_log.Level >= LogSeverity.Verbose) | |||||
| { | |||||
| _api.RestClient.OnRequest += (s, e) => | |||||
| private Logger CreateRestLogger() | |||||
| { | |||||
| Logger logger = null; | |||||
| if (_log.Level >= LogSeverity.Verbose) | |||||
| { | |||||
| logger = _log.CreateLogger("Rest"); | |||||
| _api.RestClient.OnRequest += (s, e) => | |||||
| { | { | ||||
| if (e.Payload != null) | if (e.Payload != null) | ||||
| _restLogger.Verbose( $"{e.Method} {e.Path}: {Math.Round(e.ElapsedMilliseconds, 2)} ms ({e.Payload})"); | |||||
| logger.Verbose( $"{e.Method} {e.Path}: {Math.Round(e.ElapsedMilliseconds, 2)} ms ({e.Payload})"); | |||||
| else | else | ||||
| _restLogger.Verbose( $"{e.Method} {e.Path}: {Math.Round(e.ElapsedMilliseconds, 2)} ms"); | |||||
| logger.Verbose( $"{e.Method} {e.Path}: {Math.Round(e.ElapsedMilliseconds, 2)} ms"); | |||||
| }; | }; | ||||
| } | |||||
| } | |||||
| private void CreateCacheLogger() | |||||
| { | |||||
| _cacheLogger = _log.CreateLogger("Cache"); | |||||
| if (_log.Level >= LogSeverity.Debug) | |||||
| { | |||||
| _channels.ItemCreated += (s, e) => _cacheLogger.Debug( $"Created Channel {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); | |||||
| _channels.ItemDestroyed += (s, e) => _cacheLogger.Debug( $"Destroyed Channel {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); | |||||
| _channels.Cleared += (s, e) => _cacheLogger.Debug( $"Cleared Channels"); | |||||
| _users.ItemCreated += (s, e) => _cacheLogger.Debug( $"Created User {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); | |||||
| _users.ItemDestroyed += (s, e) => _cacheLogger.Debug( $"Destroyed User {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); | |||||
| _users.Cleared += (s, e) => _cacheLogger.Debug( $"Cleared Users"); | |||||
| _messages.ItemCreated += (s, e) => _cacheLogger.Debug( $"Created Message {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Channel.Id}/{e.Item.Id}"); | |||||
| _messages.ItemDestroyed += (s, e) => _cacheLogger.Debug( $"Destroyed Message {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Channel.Id}/{e.Item.Id}"); | |||||
| _messages.ItemRemapped += (s, e) => _cacheLogger.Debug( $"Remapped Message {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Channel.Id}/[{e.OldId} -> {e.NewId}]"); | |||||
| _messages.Cleared += (s, e) => _cacheLogger.Debug( $"Cleared Messages"); | |||||
| _roles.ItemCreated += (s, e) => _cacheLogger.Debug( $"Created Role {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); | |||||
| _roles.ItemDestroyed += (s, e) => _cacheLogger.Debug( $"Destroyed Role {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); | |||||
| _roles.Cleared += (s, e) => _cacheLogger.Debug( $"Cleared Roles"); | |||||
| _servers.ItemCreated += (s, e) => _cacheLogger.Debug( $"Created Server {e.Item.Id}"); | |||||
| _servers.ItemDestroyed += (s, e) => _cacheLogger.Debug( $"Destroyed Server {e.Item.Id}"); | |||||
| _servers.Cleared += (s, e) => _cacheLogger.Debug( $"Cleared Servers"); | |||||
| _globalUsers.ItemCreated += (s, e) => _cacheLogger.Debug( $"Created User {e.Item.Id}"); | |||||
| _globalUsers.ItemDestroyed += (s, e) => _cacheLogger.Debug( $"Destroyed User {e.Item.Id}"); | |||||
| _globalUsers.Cleared += (s, e) => _cacheLogger.Debug( $"Cleared Users"); | |||||
| } | |||||
| } | |||||
| } | |||||
| return logger; | |||||
| } | |||||
| private Logger CreateCacheLogger() | |||||
| { | |||||
| Logger logger = null; | |||||
| if (_log.Level >= LogSeverity.Debug) | |||||
| { | |||||
| logger = _log.CreateLogger("Cache"); | |||||
| _channels.ItemCreated += (s, e) => logger.Debug( $"Created Channel {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); | |||||
| _channels.ItemDestroyed += (s, e) => logger.Debug( $"Destroyed Channel {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); | |||||
| _channels.Cleared += (s, e) => logger.Debug( $"Cleared Channels"); | |||||
| _users.ItemCreated += (s, e) => logger.Debug( $"Created User {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); | |||||
| _users.ItemDestroyed += (s, e) => logger.Debug( $"Destroyed User {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); | |||||
| _users.Cleared += (s, e) => logger.Debug( $"Cleared Users"); | |||||
| _messages.ItemCreated += (s, e) => logger.Debug( $"Created Message {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Channel.Id}/{e.Item.Id}"); | |||||
| _messages.ItemDestroyed += (s, e) => logger.Debug( $"Destroyed Message {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Channel.Id}/{e.Item.Id}"); | |||||
| _messages.ItemRemapped += (s, e) => logger.Debug( $"Remapped Message {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Channel.Id}/[{e.OldId} -> {e.NewId}]"); | |||||
| _messages.Cleared += (s, e) => logger.Debug( $"Cleared Messages"); | |||||
| _roles.ItemCreated += (s, e) => logger.Debug( $"Created Role {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); | |||||
| _roles.ItemDestroyed += (s, e) => logger.Debug( $"Destroyed Role {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); | |||||
| _roles.Cleared += (s, e) => logger.Debug( $"Cleared Roles"); | |||||
| _servers.ItemCreated += (s, e) => logger.Debug( $"Created Server {e.Item.Id}"); | |||||
| _servers.ItemDestroyed += (s, e) => logger.Debug( $"Destroyed Server {e.Item.Id}"); | |||||
| _servers.Cleared += (s, e) => logger.Debug( $"Cleared Servers"); | |||||
| _globalUsers.ItemCreated += (s, e) => logger.Debug( $"Created User {e.Item.Id}"); | |||||
| _globalUsers.ItemDestroyed += (s, e) => logger.Debug( $"Destroyed User {e.Item.Id}"); | |||||
| _globalUsers.Cleared += (s, e) => logger.Debug( $"Cleared Users"); | |||||
| } | |||||
| return logger; | |||||
| } | |||||
| /// <summary> Connects to the Discord server with the provided email and password. </summary> | /// <summary> Connects to the Discord server with the provided email and password. </summary> | ||||
| /// <returns> Returns a token for future connections. </returns> | /// <returns> Returns a token for future connections. </returns> | ||||
| @@ -247,17 +259,16 @@ namespace Discord | |||||
| if (!_sentInitialLog) | if (!_sentInitialLog) | ||||
| SendInitialLog(); | SendInitialLog(); | ||||
| if (State != DiscordClientState.Disconnected) | |||||
| if (State != ConnectionState.Disconnected) | |||||
| await Disconnect().ConfigureAwait(false); | await Disconnect().ConfigureAwait(false); | ||||
| var response = await _api.Login(email, password) | |||||
| .ConfigureAwait(false); | |||||
| var response = await _api.Login(email, password).ConfigureAwait(false); | |||||
| _token = response.Token; | _token = response.Token; | ||||
| _api.Token = response.Token; | _api.Token = response.Token; | ||||
| if (_config.LogLevel >= LogSeverity.Verbose) | if (_config.LogLevel >= LogSeverity.Verbose) | ||||
| _logger.Verbose( "Login successful, got token."); | _logger.Verbose( "Login successful, got token."); | ||||
| await BeginConnect(); | |||||
| await BeginConnect().ConfigureAwait(false); | |||||
| return response.Token; | return response.Token; | ||||
| } | } | ||||
| /// <summary> Connects to the Discord server with the provided token. </summary> | /// <summary> Connects to the Discord server with the provided token. </summary> | ||||
| @@ -266,48 +277,62 @@ namespace Discord | |||||
| if (!_sentInitialLog) | if (!_sentInitialLog) | ||||
| SendInitialLog(); | SendInitialLog(); | ||||
| if (State != (int)DiscordClientState.Disconnected) | |||||
| if (State != (int)ConnectionState.Disconnected) | |||||
| await Disconnect().ConfigureAwait(false); | await Disconnect().ConfigureAwait(false); | ||||
| _token = token; | _token = token; | ||||
| _api.Token = token; | _api.Token = token; | ||||
| await BeginConnect(); | |||||
| await BeginConnect().ConfigureAwait(false); | |||||
| } | } | ||||
| private async Task BeginConnect() | private async Task BeginConnect() | ||||
| { | { | ||||
| try | try | ||||
| { | { | ||||
| _state = (int)DiscordClientState.Connecting; | |||||
| var gatewayResponse = await _api.Gateway().ConfigureAwait(false); | |||||
| string gateway = gatewayResponse.Url; | |||||
| if (_config.LogLevel >= LogSeverity.Verbose) | |||||
| _logger.Verbose( $"Websocket endpoint: {gateway}"); | |||||
| _disconnectedEvent.Reset(); | |||||
| _gateway = gateway; | |||||
| _cancelTokenSource = new CancellationTokenSource(); | |||||
| _cancelToken = _cancelTokenSource.Token; | |||||
| _webSocket.Host = gateway; | |||||
| _webSocket.ParentCancelToken = _cancelToken; | |||||
| await _webSocket.Connect(_token).ConfigureAwait(false); | |||||
| _runTask = RunTasks(); | |||||
| _lock.WaitOne(); | |||||
| try | try | ||||
| { | { | ||||
| //Cancel if either Disconnect is called, data socket errors or timeout is reached | |||||
| var cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken, _webSocket.CancelToken).Token; | |||||
| _connectedEvent.Wait(cancelToken); | |||||
| await _taskManager.Stop().ConfigureAwait(false); | |||||
| _state = (int)ConnectionState.Connecting; | |||||
| var gatewayResponse = await _api.Gateway().ConfigureAwait(false); | |||||
| string gateway = gatewayResponse.Url; | |||||
| if (_config.LogLevel >= LogSeverity.Verbose) | |||||
| _logger.Verbose( $"Websocket endpoint: {gateway}"); | |||||
| _disconnectedEvent.Reset(); | |||||
| _gateway = gateway; | |||||
| _cancelTokenSource = new CancellationTokenSource(); | |||||
| _cancelToken = _cancelTokenSource.Token; | |||||
| _webSocket.Host = gateway; | |||||
| _webSocket.ParentCancelToken = _cancelToken; | |||||
| await _webSocket.Connect().ConfigureAwait(false); | |||||
| List<Task> tasks = new List<Task>(); | |||||
| tasks.Add(_cancelToken.Wait()); | |||||
| if (_config.UseMessageQueue) | |||||
| tasks.Add(MessageQueueAsync()); | |||||
| await _taskManager.Start(tasks, _cancelTokenSource).ConfigureAwait(false); | |||||
| try | |||||
| { | |||||
| //Cancel if either Disconnect is called, data socket errors or timeout is reached | |||||
| var cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken, _webSocket.CancelToken).Token; | |||||
| _connectedEvent.Wait(cancelToken); | |||||
| } | |||||
| catch (OperationCanceledException) | |||||
| { | |||||
| _webSocket.ThrowError(); //Throws data socket's internal error if any occured | |||||
| throw; | |||||
| } | |||||
| } | } | ||||
| catch (OperationCanceledException) | |||||
| finally | |||||
| { | { | ||||
| _webSocket.ThrowError(); //Throws data socket's internal error if any occured | |||||
| throw; | |||||
| _lock.Release(); | |||||
| } | } | ||||
| } | } | ||||
| catch | catch | ||||
| @@ -318,88 +343,15 @@ namespace Discord | |||||
| } | } | ||||
| private void EndConnect() | private void EndConnect() | ||||
| { | { | ||||
| _state = (int)DiscordClientState.Connected; | |||||
| _state = (int)ConnectionState.Connected; | |||||
| _connectedEvent.Set(); | _connectedEvent.Set(); | ||||
| RaiseConnected(); | RaiseConnected(); | ||||
| } | } | ||||
| /// <summary> Disconnects from the Discord server, canceling any pending requests. </summary> | |||||
| public Task Disconnect() => SignalDisconnect(new Exception("Disconnect was requested by user."), isUnexpected: false); | |||||
| private async Task SignalDisconnect(Exception ex = null, bool isUnexpected = true, bool wait = false) | |||||
| { | |||||
| int oldState; | |||||
| bool hasWriterLock; | |||||
| //If in either connecting or connected state, get a lock by being the first to switch to disconnecting | |||||
| oldState = Interlocked.CompareExchange(ref _state, (int)DiscordClientState.Disconnecting, (int)DiscordClientState.Connecting); | |||||
| if (oldState == (int)DiscordClientState.Disconnected) return; //Already disconnected | |||||
| hasWriterLock = oldState == (int)DiscordClientState.Connecting; //Caused state change | |||||
| if (!hasWriterLock) | |||||
| { | |||||
| oldState = Interlocked.CompareExchange(ref _state, (int)DiscordClientState.Disconnecting, (int)DiscordClientState.Connected); | |||||
| if (oldState == (int)DiscordClientState.Disconnected) return; //Already disconnected | |||||
| hasWriterLock = oldState == (int)DiscordClientState.Connected; //Caused state change | |||||
| } | |||||
| if (hasWriterLock) | |||||
| { | |||||
| _wasDisconnectUnexpected = isUnexpected; | |||||
| _disconnectReason = ex != null ? ExceptionDispatchInfo.Capture(ex) : null; | |||||
| _cancelTokenSource.Cancel(); | |||||
| /*if (_disconnectState == DiscordClientState.Connecting) //_runTask was never made | |||||
| await Cleanup().ConfigureAwait(false);*/ | |||||
| } | |||||
| if (wait) | |||||
| { | |||||
| Task task = _runTask; | |||||
| if (_runTask != null) | |||||
| await task.ConfigureAwait(false); | |||||
| } | |||||
| } | |||||
| private async Task RunTasks() | |||||
| { | |||||
| List<Task> tasks = new List<Task>(); | |||||
| tasks.Add(_cancelToken.Wait()); | |||||
| if (_config.UseMessageQueue) | |||||
| tasks.Add(MessageQueueAsync()); | |||||
| Task[] tasksArray = tasks.ToArray(); | |||||
| Task firstTask = Task.WhenAny(tasksArray); | |||||
| Task allTasks = Task.WhenAll(tasksArray); | |||||
| //Wait until the first task ends/errors and capture the error | |||||
| try { await firstTask.ConfigureAwait(false); } | |||||
| catch (Exception ex) { await SignalDisconnect(ex: ex, wait: true).ConfigureAwait(false); } | |||||
| //Ensure all other tasks are signaled to end. | |||||
| await SignalDisconnect(wait: true).ConfigureAwait(false); | |||||
| //Wait for the remaining tasks to complete | |||||
| try { await allTasks.ConfigureAwait(false); } | |||||
| catch { } | |||||
| //Start cleanup | |||||
| var wasDisconnectUnexpected = _wasDisconnectUnexpected; | |||||
| _wasDisconnectUnexpected = false; | |||||
| await _webSocket.SignalDisconnect().ConfigureAwait(false); | |||||
| _privateUser = null; | |||||
| _gateway = null; | |||||
| _token = null; | |||||
| if (!wasDisconnectUnexpected) | |||||
| { | |||||
| _state = (int)DiscordClientState.Disconnected; | |||||
| _disconnectedEvent.Set(); | |||||
| } | |||||
| _connectedEvent.Reset(); | |||||
| _runTask = null; | |||||
| } | |||||
| private async Task Stop() | |||||
| /// <summary> Disconnects from the Discord server, canceling any pending requests. </summary> | |||||
| public Task Disconnect() => _taskManager.Stop(); | |||||
| private async Task Cleanup() | |||||
| { | { | ||||
| if (Config.UseMessageQueue) | if (Config.UseMessageQueue) | ||||
| { | { | ||||
| @@ -417,7 +369,13 @@ namespace Discord | |||||
| _globalUsers.Clear(); | _globalUsers.Clear(); | ||||
| _privateUser = null; | _privateUser = null; | ||||
| } | |||||
| _gateway = null; | |||||
| _token = null; | |||||
| _state = (int)ConnectionState.Disconnected; | |||||
| _disconnectedEvent.Set(); | |||||
| _connectedEvent.Reset(); | |||||
| } | |||||
| private void OnReceivedEvent(WebSocketEventEventArgs e) | private void OnReceivedEvent(WebSocketEventEventArgs e) | ||||
| { | { | ||||
| @@ -429,7 +387,8 @@ namespace Discord | |||||
| case "READY": //Resync | case "READY": //Resync | ||||
| { | { | ||||
| var data = e.Payload.ToObject<ReadyEvent>(_webSocket.Serializer); | var data = e.Payload.ToObject<ReadyEvent>(_webSocket.Serializer); | ||||
| _privateUser = _users.GetOrAdd(data.User.Id, null); | |||||
| _sessionId = data.SessionId; | |||||
| _privateUser = _users.GetOrAdd(data.User.Id, null); | |||||
| _privateUser.Update(data.User); | _privateUser.Update(data.User); | ||||
| _privateUser.Global.Update(data.User); | _privateUser.Global.Update(data.User); | ||||
| foreach (var model in data.Guilds) | foreach (var model in data.Guilds) | ||||
| @@ -863,11 +822,11 @@ namespace Discord | |||||
| { | { | ||||
| switch (_state) | switch (_state) | ||||
| { | { | ||||
| case (int)DiscordClientState.Disconnecting: | |||||
| case (int)ConnectionState.Disconnecting: | |||||
| throw new InvalidOperationException("The client is disconnecting."); | throw new InvalidOperationException("The client is disconnecting."); | ||||
| case (int)DiscordClientState.Disconnected: | |||||
| case (int)ConnectionState.Disconnected: | |||||
| throw new InvalidOperationException("The client is not connected to Discord"); | throw new InvalidOperationException("The client is not connected to Discord"); | ||||
| case (int)DiscordClientState.Connecting: | |||||
| case (int)ConnectionState.Connecting: | |||||
| throw new InvalidOperationException("The client is connecting."); | throw new InvalidOperationException("The client is connecting."); | ||||
| } | } | ||||
| } | } | ||||
| @@ -23,7 +23,7 @@ namespace Discord | |||||
| public static string Channel(Channel channel) | public static string Channel(Channel channel) | ||||
| => $"<#{channel.Id}>"; | => $"<#{channel.Id}>"; | ||||
| /// <summary> Returns the string used to create a mention to everyone in a channel. </summary> | /// <summary> Returns the string used to create a mention to everyone in a channel. </summary> | ||||
| [Obsolete("Use Role.Mention instead")] | |||||
| [Obsolete("Use Server.EveryoneRole.Mention instead")] | |||||
| public static string Everyone() | public static string Everyone() | ||||
| => $"@everyone"; | => $"@everyone"; | ||||
| @@ -0,0 +1,148 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Runtime.ExceptionServices; | |||||
| using System.Threading; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| /// <summary> Helper class used to manage several tasks and keep them in sync. If any single task errors or stops, all other tasks will also be stopped. </summary> | |||||
| public class TaskManager | |||||
| { | |||||
| private readonly object _lock; | |||||
| private readonly Func<Task> _stopAction; | |||||
| private CancellationTokenSource _cancelSource; | |||||
| private Task _task; | |||||
| public bool WasUnexpected => _wasUnexpected; | |||||
| private bool _wasUnexpected; | |||||
| public Exception Exception => _stopReason.SourceException; | |||||
| private ExceptionDispatchInfo _stopReason; | |||||
| public TaskManager() | |||||
| { | |||||
| _lock = new object(); | |||||
| } | |||||
| public TaskManager(Action stopAction) | |||||
| : this() | |||||
| { | |||||
| _stopAction = TaskHelper.ToAsync(stopAction); | |||||
| } | |||||
| public TaskManager(Func<Task> stopAction) | |||||
| : this() | |||||
| { | |||||
| _stopAction = stopAction; | |||||
| } | |||||
| public async Task Start(IEnumerable<Task> tasks, CancellationTokenSource cancelSource) | |||||
| { | |||||
| while (true) | |||||
| { | |||||
| var task = _task; | |||||
| if (task != null) | |||||
| await Stop().ConfigureAwait(false); | |||||
| lock (_lock) | |||||
| { | |||||
| _cancelSource = new CancellationTokenSource(); | |||||
| if (_task != null) | |||||
| continue; //Another thread sneaked in and started this manager before we got a lock, loop and try again | |||||
| _stopReason = null; | |||||
| _wasUnexpected = false; | |||||
| Task[] tasksArray = tasks.ToArray(); | |||||
| Task<Task> anyTask = Task.WhenAny(tasksArray); | |||||
| Task allTasks = Task.WhenAll(tasksArray); | |||||
| _task = Task.Run(async () => | |||||
| { | |||||
| //Wait for the first task to stop or error | |||||
| Task firstTask = await anyTask.ConfigureAwait(false); | |||||
| //Signal the rest of the tasks to stop | |||||
| if (firstTask.Exception != null) | |||||
| SignalError(firstTask.Exception.GetBaseException(), true); | |||||
| else | |||||
| SignalStop(); | |||||
| //Wait for the other tasks; | |||||
| await allTasks.ConfigureAwait(false); | |||||
| //Run the cleanup function within our lock | |||||
| await _stopAction().ConfigureAwait(false); | |||||
| _task = null; | |||||
| }); | |||||
| return; | |||||
| } | |||||
| } | |||||
| } | |||||
| public void SignalStop() | |||||
| { | |||||
| lock (_lock) | |||||
| { | |||||
| if (_task == null) return; //Are we running? | |||||
| if (_cancelSource.IsCancellationRequested) return; | |||||
| _cancelSource.Cancel(); | |||||
| } | |||||
| } | |||||
| public Task Stop() | |||||
| { | |||||
| Task task; | |||||
| lock (_lock) | |||||
| { | |||||
| //Cache the task so we still have something to await if Cleanup is run really quickly | |||||
| task = _task; | |||||
| if (task == null) return TaskHelper.CompletedTask; //Are we running? | |||||
| if (_cancelSource.IsCancellationRequested) return task; | |||||
| _cancelSource.Cancel(); | |||||
| } | |||||
| return task; | |||||
| } | |||||
| public void SignalError(Exception ex, bool isUnexpected = true) | |||||
| { | |||||
| lock (_lock) | |||||
| { | |||||
| if (_task == null) return; //Are we running? | |||||
| _cancelSource.Cancel(); | |||||
| _stopReason = ExceptionDispatchInfo.Capture(ex); | |||||
| _wasUnexpected = isUnexpected; | |||||
| } | |||||
| } | |||||
| public Task Error(Exception ex, bool isUnexpected = true) | |||||
| { | |||||
| Task task; | |||||
| lock (_lock) | |||||
| { | |||||
| //Cache the task so we still have something to await if Cleanup is run really quickly | |||||
| task = _task; | |||||
| if (task == null) return TaskHelper.CompletedTask; //Are we running? | |||||
| if (_cancelSource.IsCancellationRequested) return task; | |||||
| _cancelSource.Cancel(); | |||||
| _stopReason = ExceptionDispatchInfo.Capture(ex); | |||||
| _wasUnexpected = isUnexpected; | |||||
| } | |||||
| return task; | |||||
| } | |||||
| /// <summary> Throws an exception if one was captured. </summary> | |||||
| public void Throw() | |||||
| { | |||||
| lock (_lock) | |||||
| { | |||||
| if (_stopReason != null) | |||||
| _stopReason.Throw(); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -77,7 +77,7 @@ namespace Discord.Net.Rest | |||||
| .FirstOrDefault(x => x.Name.Equals("Retry-After", StringComparison.OrdinalIgnoreCase)); | .FirstOrDefault(x => x.Name.Equals("Retry-After", StringComparison.OrdinalIgnoreCase)); | ||||
| if (retryAfter != null) | if (retryAfter != null) | ||||
| { | { | ||||
| await Task.Delay((int)retryAfter.Value); | |||||
| await Task.Delay((int)retryAfter.Value).ConfigureAwait(false); | |||||
| continue; | continue; | ||||
| } | } | ||||
| throw new HttpException(response.StatusCode); | throw new HttpException(response.StatusCode); | ||||
| @@ -2,6 +2,7 @@ | |||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| using Newtonsoft.Json.Linq; | using Newtonsoft.Json.Linq; | ||||
| using System; | using System; | ||||
| using System.Collections.Generic; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| namespace Discord.Net.WebSockets | namespace Discord.Net.WebSockets | ||||
| @@ -11,14 +12,11 @@ namespace Discord.Net.WebSockets | |||||
| public int LastSequence => _lastSeq; | public int LastSequence => _lastSeq; | ||||
| private int _lastSeq; | private int _lastSeq; | ||||
| public string Token => _token; | |||||
| private string _token; | |||||
| public string SessionId => _sessionId; | public string SessionId => _sessionId; | ||||
| private string _sessionId; | private string _sessionId; | ||||
| public GatewaySocket(DiscordConfig config, Logger logger) | |||||
| : base(config, logger) | |||||
| public GatewaySocket(DiscordClient client, Logger logger) | |||||
| : base(client, logger) | |||||
| { | { | ||||
| Disconnected += async (s, e) => | Disconnected += async (s, e) => | ||||
| { | { | ||||
| @@ -27,18 +25,13 @@ namespace Discord.Net.WebSockets | |||||
| }; | }; | ||||
| } | } | ||||
| public async Task Connect(string token) | |||||
| public async Task Connect() | |||||
| { | { | ||||
| await SignalDisconnect(wait: true).ConfigureAwait(false); | |||||
| _token = token; | |||||
| await BeginConnect().ConfigureAwait(false); | await BeginConnect().ConfigureAwait(false); | ||||
| SendIdentify(token); | |||||
| SendIdentify(); | |||||
| } | } | ||||
| private async Task Redirect(string server) | |||||
| private async Task Redirect() | |||||
| { | { | ||||
| await SignalDisconnect(wait: true).ConfigureAwait(false); | |||||
| await BeginConnect().ConfigureAwait(false); | await BeginConnect().ConfigureAwait(false); | ||||
| SendResume(); | SendResume(); | ||||
| } | } | ||||
| @@ -47,12 +40,12 @@ namespace Discord.Net.WebSockets | |||||
| try | try | ||||
| { | { | ||||
| var cancelToken = ParentCancelToken.Value; | var cancelToken = ParentCancelToken.Value; | ||||
| await Task.Delay(_config.ReconnectDelay, cancelToken).ConfigureAwait(false); | |||||
| await Task.Delay(_client.Config.ReconnectDelay, cancelToken).ConfigureAwait(false); | |||||
| while (!cancelToken.IsCancellationRequested) | while (!cancelToken.IsCancellationRequested) | ||||
| { | { | ||||
| try | try | ||||
| { | { | ||||
| await Connect(_token).ConfigureAwait(false); | |||||
| await Connect().ConfigureAwait(false); | |||||
| break; | break; | ||||
| } | } | ||||
| catch (OperationCanceledException) { throw; } | catch (OperationCanceledException) { throw; } | ||||
| @@ -60,21 +53,21 @@ namespace Discord.Net.WebSockets | |||||
| { | { | ||||
| _logger.Log(LogSeverity.Error, $"Reconnect failed", ex); | _logger.Log(LogSeverity.Error, $"Reconnect failed", ex); | ||||
| //Net is down? We can keep trying to reconnect until the user runs Disconnect() | //Net is down? We can keep trying to reconnect until the user runs Disconnect() | ||||
| await Task.Delay(_config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); | |||||
| await Task.Delay(_client.Config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| catch (OperationCanceledException) { } | catch (OperationCanceledException) { } | ||||
| } | } | ||||
| public Task Disconnect() | |||||
| { | |||||
| return SignalDisconnect(wait: true); | |||||
| } | |||||
| public Task Disconnect() => TaskManager.Stop(); | |||||
| protected override async Task Run() | protected override async Task Run() | ||||
| { | |||||
| await RunTasks(); | |||||
| } | |||||
| { | |||||
| List<Task> tasks = new List<Task>(); | |||||
| tasks.AddRange(_engine.GetTasks(_cancelToken)); | |||||
| tasks.Add(HeartbeatAsync(_cancelToken)); | |||||
| await _taskManager.Start(tasks, _cancelTokenSource).ConfigureAwait(false); | |||||
| } | |||||
| protected override async Task ProcessMessage(string json) | protected override async Task ProcessMessage(string json) | ||||
| { | { | ||||
| @@ -102,7 +95,7 @@ namespace Discord.Net.WebSockets | |||||
| } | } | ||||
| RaiseReceivedDispatch(msg.Type, token); | RaiseReceivedDispatch(msg.Type, token); | ||||
| if (msg.Type == "READY" || msg.Type == "RESUMED") | if (msg.Type == "READY" || msg.Type == "RESUMED") | ||||
| await EndConnect(); //Complete the connect | |||||
| EndConnect(); //Complete the connect | |||||
| } | } | ||||
| break; | break; | ||||
| case GatewayOpCodes.Redirect: | case GatewayOpCodes.Redirect: | ||||
| @@ -113,7 +106,7 @@ namespace Discord.Net.WebSockets | |||||
| Host = payload.Url; | Host = payload.Url; | ||||
| if (_logger.Level >= LogSeverity.Info) | if (_logger.Level >= LogSeverity.Info) | ||||
| _logger.Info("Redirected to " + payload.Url); | _logger.Info("Redirected to " + payload.Url); | ||||
| await Redirect(payload.Url).ConfigureAwait(false); | |||||
| await Redirect().ConfigureAwait(false); | |||||
| } | } | ||||
| } | } | ||||
| break; | break; | ||||
| @@ -124,12 +117,12 @@ namespace Discord.Net.WebSockets | |||||
| } | } | ||||
| } | } | ||||
| public void SendIdentify(string token) | |||||
| public void SendIdentify() | |||||
| { | { | ||||
| var msg = new IdentifyCommand(); | var msg = new IdentifyCommand(); | ||||
| msg.Payload.Token = token; | |||||
| msg.Payload.Token = _client.Token; | |||||
| msg.Payload.Properties["$device"] = "Discord.Net"; | msg.Payload.Properties["$device"] = "Discord.Net"; | ||||
| if (_config.UseLargeThreshold) | |||||
| if (_client.Config.UseLargeThreshold) | |||||
| msg.Payload.LargeThreshold = 100; | msg.Payload.LargeThreshold = 100; | ||||
| msg.Payload.Compress = true; | msg.Payload.Compress = true; | ||||
| QueueMessage(msg); | QueueMessage(msg); | ||||
| @@ -46,7 +46,7 @@ namespace Discord.Net.WebSockets | |||||
| _webSocket = new WS4NetWebSocket(host); | _webSocket = new WS4NetWebSocket(host); | ||||
| _webSocket.EnableAutoSendPing = false; | _webSocket.EnableAutoSendPing = false; | ||||
| _webSocket.NoDelay = true; | _webSocket.NoDelay = true; | ||||
| _webSocket.Proxy = null; //Disable | |||||
| _webSocket.Proxy = null; | |||||
| _webSocket.DataReceived += OnWebSocketBinary; | _webSocket.DataReceived += OnWebSocketBinary; | ||||
| _webSocket.MessageReceived += OnWebSocketText; | _webSocket.MessageReceived += OnWebSocketText; | ||||
| @@ -81,15 +81,15 @@ namespace Discord.Net.WebSockets | |||||
| return TaskHelper.CompletedTask; | return TaskHelper.CompletedTask; | ||||
| } | } | ||||
| private async void OnWebSocketError(object sender, ErrorEventArgs e) | |||||
| private void OnWebSocketError(object sender, ErrorEventArgs e) | |||||
| { | { | ||||
| await _parent.SignalDisconnect(e.Exception, isUnexpected: true).ConfigureAwait(false); | |||||
| _parent.TaskManager.SignalError(e.Exception); | |||||
| _waitUntilConnect.Set(); | _waitUntilConnect.Set(); | ||||
| } | } | ||||
| private async void OnWebSocketClosed(object sender, EventArgs e) | |||||
| private void OnWebSocketClosed(object sender, EventArgs e) | |||||
| { | { | ||||
| var ex = new Exception($"Connection lost or close message received."); | var ex = new Exception($"Connection lost or close message received."); | ||||
| await _parent.SignalDisconnect(ex, isUnexpected: false/*true*/).ConfigureAwait(false); | |||||
| _parent.TaskManager.SignalError(ex, isUnexpected: true); | |||||
| _waitUntilConnect.Set(); | _waitUntilConnect.Set(); | ||||
| } | } | ||||
| private void OnWebSocketOpened(object sender, EventArgs e) | private void OnWebSocketOpened(object sender, EventArgs e) | ||||
| @@ -1,52 +1,40 @@ | |||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| using System; | using System; | ||||
| using System.Collections.Generic; | |||||
| using System.IO; | using System.IO; | ||||
| using System.IO.Compression; | using System.IO.Compression; | ||||
| using System.Linq; | |||||
| using System.Runtime.ExceptionServices; | |||||
| using System.Threading; | using System.Threading; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| namespace Discord.Net.WebSockets | namespace Discord.Net.WebSockets | ||||
| { | { | ||||
| public enum WebSocketState : byte | |||||
| { | |||||
| Disconnected, | |||||
| Connecting, | |||||
| Connected, | |||||
| Disconnecting | |||||
| } | |||||
| public abstract partial class WebSocket | public abstract partial class WebSocket | ||||
| { | |||||
| protected readonly IWebSocketEngine _engine; | |||||
| protected readonly DiscordConfig _config; | |||||
| { | |||||
| private readonly Semaphore _lock; | |||||
| protected readonly IWebSocketEngine _engine; | |||||
| protected readonly DiscordClient _client; | |||||
| protected readonly ManualResetEventSlim _connectedEvent; | protected readonly ManualResetEventSlim _connectedEvent; | ||||
| protected ExceptionDispatchInfo _disconnectReason; | |||||
| protected bool _wasDisconnectUnexpected; | |||||
| protected WebSocketState _disconnectState; | |||||
| protected int _heartbeatInterval; | protected int _heartbeatInterval; | ||||
| private DateTime _lastHeartbeat; | private DateTime _lastHeartbeat; | ||||
| private Task _runTask; | |||||
| public CancellationToken? ParentCancelToken { get; set; } | public CancellationToken? ParentCancelToken { get; set; } | ||||
| public CancellationToken CancelToken => _cancelToken; | public CancellationToken CancelToken => _cancelToken; | ||||
| private CancellationTokenSource _cancelTokenSource; | |||||
| protected CancellationTokenSource _cancelTokenSource; | |||||
| protected CancellationToken _cancelToken; | protected CancellationToken _cancelToken; | ||||
| internal JsonSerializer Serializer => _serializer; | |||||
| public JsonSerializer Serializer => _serializer; | |||||
| protected JsonSerializer _serializer; | protected JsonSerializer _serializer; | ||||
| public Logger Logger => _logger; | |||||
| internal TaskManager TaskManager => _taskManager; | |||||
| protected readonly TaskManager _taskManager; | |||||
| public Logger Logger => _logger; | |||||
| protected readonly Logger _logger; | protected readonly Logger _logger; | ||||
| public string Host { get { return _host; } set { _host = value; } } | public string Host { get { return _host; } set { _host = value; } } | ||||
| private string _host; | private string _host; | ||||
| public WebSocketState State => (WebSocketState)_state; | |||||
| public ConnectionState State => (ConnectionState)_state; | |||||
| protected int _state; | protected int _state; | ||||
| public event EventHandler Connected; | public event EventHandler Connected; | ||||
| @@ -66,20 +54,22 @@ namespace Discord.Net.WebSockets | |||||
| Disconnected(this, new DisconnectedEventArgs(wasUnexpected, error)); | Disconnected(this, new DisconnectedEventArgs(wasUnexpected, error)); | ||||
| } | } | ||||
| public WebSocket(DiscordConfig config, Logger logger) | |||||
| public WebSocket(DiscordClient client, Logger logger) | |||||
| { | { | ||||
| _config = config; | |||||
| _client = client; | |||||
| _logger = logger; | _logger = logger; | ||||
| _cancelToken = new CancellationToken(true); | |||||
| _lock = new Semaphore(1, 1); | |||||
| _taskManager = new TaskManager(Cleanup); | |||||
| _cancelToken = new CancellationToken(true); | |||||
| _connectedEvent = new ManualResetEventSlim(false); | _connectedEvent = new ManualResetEventSlim(false); | ||||
| #if !DOTNET5_4 | #if !DOTNET5_4 | ||||
| _engine = new WS4NetEngine(this, _config, _logger); | |||||
| _engine = new WS4NetEngine(this, client.Config, _logger); | |||||
| #else | #else | ||||
| //_engine = new BuiltInWebSocketEngine(this, _config, _logger); | |||||
| //_engine = new BuiltInWebSocketEngine(this, client.Config, _logger); | |||||
| #endif | #endif | ||||
| _engine.BinaryMessage += (s, e) => | |||||
| _engine.BinaryMessage += (s, e) => | |||||
| { | { | ||||
| using (var compressed = new MemoryStream(e.Data, 2, e.Data.Length - 2)) | using (var compressed = new MemoryStream(e.Data, 2, e.Data.Length - 2)) | ||||
| using (var decompressed = new MemoryStream()) | using (var decompressed = new MemoryStream()) | ||||
| @@ -91,10 +81,7 @@ namespace Discord.Net.WebSockets | |||||
| ProcessMessage(reader.ReadToEnd()).Wait(); | ProcessMessage(reader.ReadToEnd()).Wait(); | ||||
| } | } | ||||
| }; | }; | ||||
| _engine.TextMessage += (s, e) => | |||||
| { | |||||
| /*await*/ ProcessMessage(e.Message).Wait(); | |||||
| }; | |||||
| _engine.TextMessage += (s, e) => ProcessMessage(e.Message).Wait(); | |||||
| _serializer = new JsonSerializer(); | _serializer = new JsonSerializer(); | ||||
| _serializer.DateTimeZoneHandling = DateTimeZoneHandling.Utc; | _serializer.DateTimeZoneHandling = DateTimeZoneHandling.Utc; | ||||
| @@ -112,127 +99,59 @@ namespace Discord.Net.WebSockets | |||||
| protected async Task BeginConnect() | protected async Task BeginConnect() | ||||
| { | { | ||||
| try | |||||
| { | |||||
| _state = (int)WebSocketState.Connecting; | |||||
| if (ParentCancelToken == null) | |||||
| throw new InvalidOperationException("Parent cancel token was never set."); | |||||
| _cancelTokenSource = new CancellationTokenSource(); | |||||
| _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, ParentCancelToken.Value).Token; | |||||
| if (_state != (int)WebSocketState.Connecting) | |||||
| throw new InvalidOperationException("Socket is in the wrong state."); | |||||
| _lastHeartbeat = DateTime.UtcNow; | |||||
| await _engine.Connect(Host, _cancelToken).ConfigureAwait(false); | |||||
| _runTask = Run(); | |||||
| try | |||||
| { | |||||
| _lock.WaitOne(); | |||||
| try | |||||
| { | |||||
| await _taskManager.Stop().ConfigureAwait(false); | |||||
| _state = (int)ConnectionState.Connecting; | |||||
| _cancelTokenSource = new CancellationTokenSource(); | |||||
| _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, ParentCancelToken.Value).Token; | |||||
| _lastHeartbeat = DateTime.UtcNow; | |||||
| await _engine.Connect(Host, _cancelToken).ConfigureAwait(false); | |||||
| await Run().ConfigureAwait(false); | |||||
| } | |||||
| finally | |||||
| { | |||||
| _lock.Release(); | |||||
| } | |||||
| } | } | ||||
| catch (Exception ex) | catch (Exception ex) | ||||
| { | { | ||||
| await SignalDisconnect(ex, isUnexpected: false).ConfigureAwait(false); | |||||
| throw; | |||||
| _taskManager.SignalError(ex, true); | |||||
| } | } | ||||
| } | } | ||||
| protected async Task EndConnect() | |||||
| protected void EndConnect() | |||||
| { | { | ||||
| try | try | ||||
| { | |||||
| _state = (int)WebSocketState.Connected; | |||||
| { | |||||
| _state = (int)ConnectionState.Connected; | |||||
| _connectedEvent.Set(); | _connectedEvent.Set(); | ||||
| RaiseConnected(); | RaiseConnected(); | ||||
| } | } | ||||
| catch (Exception ex) | catch (Exception ex) | ||||
| { | |||||
| await SignalDisconnect(ex, isUnexpected: false).ConfigureAwait(false); | |||||
| throw; | |||||
| } | |||||
| } | |||||
| protected internal async Task SignalDisconnect(Exception ex = null, bool isUnexpected = false, bool wait = false) | |||||
| { | |||||
| //If in either connecting or connected state, get a lock by being the first to switch to disconnecting | |||||
| int oldState = Interlocked.CompareExchange(ref _state, (int)WebSocketState.Disconnecting, (int)WebSocketState.Connecting); | |||||
| if (oldState == (int)WebSocketState.Disconnected) return; //Already disconnected | |||||
| bool hasWriterLock = oldState == (int)WebSocketState.Connecting; //Caused state change | |||||
| if (!hasWriterLock) | |||||
| { | |||||
| oldState = Interlocked.CompareExchange(ref _state, (int)WebSocketState.Disconnecting, (int)WebSocketState.Connected); | |||||
| if (oldState == (int)WebSocketState.Disconnected) return; //Already disconnected | |||||
| hasWriterLock = oldState == (int)WebSocketState.Connected; //Caused state change | |||||
| } | |||||
| if (hasWriterLock) | |||||
| { | { | ||||
| if (ex != null) | |||||
| _logger.Log(LogSeverity.Error, "Error", ex); | |||||
| CaptureError(ex ?? new Exception("Disconnect was requested."), isUnexpected); | |||||
| _cancelTokenSource.Cancel(); | |||||
| if (_disconnectState == WebSocketState.Connecting) //_runTask was never made | |||||
| await Cleanup().ConfigureAwait(false); | |||||
| } | |||||
| if (wait) | |||||
| { | |||||
| Task task = _runTask; | |||||
| if (_runTask != null) | |||||
| await task.ConfigureAwait(false); | |||||
| } | |||||
| } | |||||
| private void CaptureError(Exception ex, bool isUnexpected) | |||||
| { | |||||
| _disconnectReason = ExceptionDispatchInfo.Capture(ex); | |||||
| _wasDisconnectUnexpected = isUnexpected; | |||||
| _taskManager.SignalError(ex, true); | |||||
| } | |||||
| } | } | ||||
| protected abstract Task Run(); | protected abstract Task Run(); | ||||
| protected async Task RunTasks(params Task[] tasks) | |||||
| { | |||||
| //Get all async tasks | |||||
| tasks = tasks | |||||
| .Concat(_engine.GetTasks(_cancelToken)) | |||||
| .Concat(new Task[] { HeartbeatAsync(_cancelToken) }) | |||||
| .ToArray(); | |||||
| //Create group tasks | |||||
| Task firstTask = Task.WhenAny(tasks); | |||||
| Task allTasks = Task.WhenAll(tasks); | |||||
| //Wait until the first task ends/errors and capture the error | |||||
| Exception ex = null; | |||||
| try { await firstTask.ConfigureAwait(false); } | |||||
| catch (Exception ex2) { ex = ex2; } | |||||
| //Ensure all other tasks are signaled to end. | |||||
| await SignalDisconnect(ex, ex != null, true).ConfigureAwait(false); | |||||
| //Wait for the remaining tasks to complete | |||||
| try { await allTasks.ConfigureAwait(false); } | |||||
| catch { } | |||||
| //Start cleanup | |||||
| await Cleanup().ConfigureAwait(false); | |||||
| } | |||||
| protected virtual async Task Cleanup() | protected virtual async Task Cleanup() | ||||
| { | { | ||||
| var disconnectState = _disconnectState; | |||||
| _disconnectState = WebSocketState.Disconnected; | |||||
| var wasDisconnectUnexpected = _wasDisconnectUnexpected; | |||||
| _wasDisconnectUnexpected = false; | |||||
| //Dont reset disconnectReason, we may called ThrowError() later | |||||
| await _engine.Disconnect().ConfigureAwait(false); | await _engine.Disconnect().ConfigureAwait(false); | ||||
| _cancelTokenSource = null; | _cancelTokenSource = null; | ||||
| var oldState = _state; | var oldState = _state; | ||||
| _state = (int)WebSocketState.Disconnected; | |||||
| _runTask = null; | |||||
| _connectedEvent.Reset(); | _connectedEvent.Reset(); | ||||
| if (disconnectState == WebSocketState.Connected) | |||||
| RaiseDisconnected(wasDisconnectUnexpected, _disconnectReason?.SourceException); | |||||
| if (oldState == (int)ConnectionState.Connected) | |||||
| { | |||||
| _state = (int)ConnectionState.Disconnected; | |||||
| RaiseDisconnected(_taskManager.WasUnexpected, _taskManager.Exception); | |||||
| } | |||||
| } | } | ||||
| protected virtual Task ProcessMessage(string json) | protected virtual Task ProcessMessage(string json) | ||||
| @@ -240,8 +159,7 @@ namespace Discord.Net.WebSockets | |||||
| if (_logger.Level >= LogSeverity.Debug) | if (_logger.Level >= LogSeverity.Debug) | ||||
| _logger.Debug( $"In: {json}"); | _logger.Debug( $"In: {json}"); | ||||
| return TaskHelper.CompletedTask; | return TaskHelper.CompletedTask; | ||||
| } | |||||
| } | |||||
| protected void QueueMessage(object message) | protected void QueueMessage(object message) | ||||
| { | { | ||||
| string json = JsonConvert.SerializeObject(message); | string json = JsonConvert.SerializeObject(message); | ||||
| @@ -250,7 +168,7 @@ namespace Discord.Net.WebSockets | |||||
| _engine.QueueMessage(json); | _engine.QueueMessage(json); | ||||
| } | } | ||||
| private Task HeartbeatAsync(CancellationToken cancelToken) | |||||
| protected Task HeartbeatAsync(CancellationToken cancelToken) | |||||
| { | { | ||||
| return Task.Run(async () => | return Task.Run(async () => | ||||
| { | { | ||||
| @@ -258,7 +176,7 @@ namespace Discord.Net.WebSockets | |||||
| { | { | ||||
| while (!cancelToken.IsCancellationRequested) | while (!cancelToken.IsCancellationRequested) | ||||
| { | { | ||||
| if (_state == (int)WebSocketState.Connected) | |||||
| if (_state == (int)ConnectionState.Connected) | |||||
| { | { | ||||
| SendHeartbeat(); | SendHeartbeat(); | ||||
| await Task.Delay(_heartbeatInterval, cancelToken).ConfigureAwait(false); | await Task.Delay(_heartbeatInterval, cancelToken).ConfigureAwait(false); | ||||
| @@ -269,18 +187,12 @@ namespace Discord.Net.WebSockets | |||||
| } | } | ||||
| catch (OperationCanceledException) { } | catch (OperationCanceledException) { } | ||||
| }); | }); | ||||
| } | |||||
| } | |||||
| public abstract void SendHeartbeat(); | |||||
| protected internal void ThrowError() | |||||
| protected internal void ThrowError() | |||||
| { | { | ||||
| if (_wasDisconnectUnexpected) | |||||
| { | |||||
| var reason = _disconnectReason; | |||||
| _disconnectReason = null; | |||||
| reason.Throw(); | |||||
| } | |||||
| _taskManager.Throw(); | |||||
| } | } | ||||
| public abstract void SendHeartbeat(); | |||||
| } | } | ||||
| } | } | ||||
| @@ -112,9 +112,9 @@ namespace Discord.Tests | |||||
| public static void Cleanup() | public static void Cleanup() | ||||
| { | { | ||||
| WaitMany( | WaitMany( | ||||
| _hostClient.State == DiscordClientState.Connected ? _hostClient.AllServers.Select(x => _hostClient.LeaveServer(x)) : null, | |||||
| _targetBot.State == DiscordClientState.Connected ? _targetBot.AllServers.Select(x => _targetBot.LeaveServer(x)) : null, | |||||
| _observerBot.State == DiscordClientState.Connected ? _observerBot.AllServers.Select(x => _observerBot.LeaveServer(x)) : null); | |||||
| _hostClient.State == ConnectionState.Connected ? _hostClient.AllServers.Select(x => _hostClient.LeaveServer(x)) : null, | |||||
| _targetBot.State == ConnectionState.Connected ? _targetBot.AllServers.Select(x => _targetBot.LeaveServer(x)) : null, | |||||
| _observerBot.State == ConnectionState.Connected ? _observerBot.AllServers.Select(x => _observerBot.LeaveServer(x)) : null); | |||||
| WaitAll( | WaitAll( | ||||
| _hostClient.Disconnect(), | _hostClient.Disconnect(), | ||||