From 74c0bb5b511c6bbab0f6318a87af9233d10857f1 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 5 Dec 2015 05:32:09 -0400 Subject: [PATCH] Moved logging to its own service, more audio isolation prep --- .../API/Converters/LongCollectionConverter.cs | 2 +- .../API/Converters/LongStringConverter.cs | 4 +- .../API/Converters/StringEnumConverter.cs | 11 - src/Discord.Net/API/WebSockets.cs | 9 +- src/Discord.Net/DiscordAPIClientConfig.cs | 19 +- src/Discord.Net/DiscordClient.Channels.cs | 6 +- src/Discord.Net/DiscordClient.Messages.cs | 10 +- src/Discord.Net/DiscordClient.Roles.cs | 6 +- src/Discord.Net/DiscordClient.Servers.cs | 10 +- src/Discord.Net/DiscordClient.Users.cs | 26 +- src/Discord.Net/DiscordClient.Voice.cs | 81 --- src/Discord.Net/DiscordClient.cs | 536 +++++++++++++----- src/Discord.Net/DiscordClientConfig.cs | 27 +- src/Discord.Net/DiscordWSClient.Events.cs | 89 --- src/Discord.Net/DiscordWSClient.cs | 306 ---------- src/Discord.Net/DiscordWSClientConfig.cs | 33 -- src/Discord.Net/Helpers/Mention.cs | 4 +- .../Helpers/{ => Shared}/EpochTime.cs | 0 .../Helpers/Shared/TaskExtensions.cs | 2 +- src/Discord.Net/LogService.cs | 61 ++ src/Discord.Net/Models/Message.cs | 2 +- src/Discord.Net/Models/Server.cs | 2 +- src/Discord.Net/Models/User.cs | 4 +- src/Discord.Net/Net/Rest/RestClient.cs | 12 +- .../Net/WebSockets/DataWebSocket.cs | 20 +- .../Net/WebSockets/WebSocket.Events.cs | 27 - src/Discord.Net/Net/WebSockets/WebSocket.cs | 103 ++-- .../Net/WebSockets/WebSocketSharpEngine.cs | 10 +- src/Discord.Net/TimeoutException.cs | 2 +- 29 files changed, 630 insertions(+), 794 deletions(-) delete mode 100644 src/Discord.Net/API/Converters/StringEnumConverter.cs delete mode 100644 src/Discord.Net/DiscordClient.Voice.cs delete mode 100644 src/Discord.Net/DiscordWSClient.Events.cs delete mode 100644 src/Discord.Net/DiscordWSClient.cs delete mode 100644 src/Discord.Net/DiscordWSClientConfig.cs rename src/Discord.Net/Helpers/{ => Shared}/EpochTime.cs (100%) create mode 100644 src/Discord.Net/LogService.cs delete mode 100644 src/Discord.Net/Net/WebSockets/WebSocket.Events.cs diff --git a/src/Discord.Net/API/Converters/LongCollectionConverter.cs b/src/Discord.Net/API/Converters/LongCollectionConverter.cs index 81b2f0df9..0d7d7bc83 100644 --- a/src/Discord.Net/API/Converters/LongCollectionConverter.cs +++ b/src/Discord.Net/API/Converters/LongCollectionConverter.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; namespace Discord.API.Converters { - internal class EnumerableLongStringConverter : JsonConverter + public class EnumerableLongStringConverter : JsonConverter { public override bool CanConvert(Type objectType) { diff --git a/src/Discord.Net/API/Converters/LongStringConverter.cs b/src/Discord.Net/API/Converters/LongStringConverter.cs index 9b320c210..ede01bb68 100644 --- a/src/Discord.Net/API/Converters/LongStringConverter.cs +++ b/src/Discord.Net/API/Converters/LongStringConverter.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Converters { - internal class LongStringConverter : JsonConverter + public class LongStringConverter : JsonConverter { public override bool CanConvert(Type objectType) { @@ -19,7 +19,7 @@ namespace Discord.API.Converters } } - internal class NullableLongStringConverter : JsonConverter + public class NullableLongStringConverter : JsonConverter { public override bool CanConvert(Type objectType) { diff --git a/src/Discord.Net/API/Converters/StringEnumConverter.cs b/src/Discord.Net/API/Converters/StringEnumConverter.cs deleted file mode 100644 index 251c5ff05..000000000 --- a/src/Discord.Net/API/Converters/StringEnumConverter.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Discord.API.Converters -{ - public class StringEnumConverter - { - } -} diff --git a/src/Discord.Net/API/WebSockets.cs b/src/Discord.Net/API/WebSockets.cs index 8ae222c97..805254b15 100644 --- a/src/Discord.Net/API/WebSockets.cs +++ b/src/Discord.Net/API/WebSockets.cs @@ -11,6 +11,9 @@ namespace Discord.API //Common public class WebSocketMessage { + public WebSocketMessage() { } + public WebSocketMessage(int op) { Operation = op; } + [JsonProperty("op")] public int Operation; [JsonProperty("d")] @@ -20,12 +23,12 @@ namespace Discord.API [JsonProperty("s", NullValueHandling = NullValueHandling.Ignore)] public int? Sequence; } - internal abstract class WebSocketMessage : WebSocketMessage + public abstract class WebSocketMessage : WebSocketMessage where T : new() { public WebSocketMessage() { Payload = new T(); } - public WebSocketMessage(int op) { Operation = op; Payload = new T(); } - public WebSocketMessage(int op, T payload) { Operation = op; Payload = payload; } + public WebSocketMessage(int op) : base(op) { Payload = new T(); } + public WebSocketMessage(int op, T payload) : base(op) { Payload = payload; } [JsonIgnore] public new T Payload diff --git a/src/Discord.Net/DiscordAPIClientConfig.cs b/src/Discord.Net/DiscordAPIClientConfig.cs index 9fb036f7d..107a99f92 100644 --- a/src/Discord.Net/DiscordAPIClientConfig.cs +++ b/src/Discord.Net/DiscordAPIClientConfig.cs @@ -1,16 +1,22 @@ using System; using System.Net; -using System.Reflection; namespace Discord { - public class DiscordAPIClientConfig + public enum LogSeverity : byte { - internal static readonly string UserAgent = $"Discord.Net/{DiscordClient.Version} (https://github.com/RogueException/Discord.Net)"; + Error = 1, + Warning = 2, + Info = 3, + Verbose = 4, + Debug = 5 + } + public class DiscordAPIClientConfig + { /// Specifies the minimum log level severity that will be sent to the LogMessage event. Warning: setting this to debug will really hurt performance but should help investigate any internal issues. - public LogMessageSeverity LogLevel { get { return _logLevel; } set { SetValue(ref _logLevel, value); } } - private LogMessageSeverity _logLevel = LogMessageSeverity.Info; + public LogSeverity LogLevel { get { return _logLevel; } set { SetValue(ref _logLevel, value); } } + private LogSeverity _logLevel = LogSeverity.Info; /// Max time (in milliseconds) to wait for an API request to complete. public int APITimeout { get { return _apiTimeout; } set { SetValue(ref _apiTimeout, value); } } @@ -23,6 +29,9 @@ namespace Discord public NetworkCredential ProxyCredentials { get { return _proxyCredentials; } set { SetValue(ref _proxyCredentials, value); } } private NetworkCredential _proxyCredentials = null; + //Internals + internal static readonly string UserAgent = $"Discord.Net/{DiscordClient.Version} (https://github.com/RogueException/Discord.Net)"; + //Lock protected bool _isLocked; internal void Lock() { _isLocked = true; } diff --git a/src/Discord.Net/DiscordClient.Channels.cs b/src/Discord.Net/DiscordClient.Channels.cs index 9a7cc6f78..5d95272f8 100644 --- a/src/Discord.Net/DiscordClient.Channels.cs +++ b/src/Discord.Net/DiscordClient.Channels.cs @@ -50,19 +50,19 @@ namespace Discord private void RaiseChannelCreated(Channel channel) { if (ChannelCreated != null) - RaiseEvent(nameof(ChannelCreated), () => ChannelCreated(this, new ChannelEventArgs(channel))); + EventHelper.Raise(_logger, nameof(ChannelCreated), () => ChannelCreated(this, new ChannelEventArgs(channel))); } public event EventHandler ChannelDestroyed; private void RaiseChannelDestroyed(Channel channel) { if (ChannelDestroyed != null) - RaiseEvent(nameof(ChannelDestroyed), () => ChannelDestroyed(this, new ChannelEventArgs(channel))); + EventHelper.Raise(_logger, nameof(ChannelDestroyed), () => ChannelDestroyed(this, new ChannelEventArgs(channel))); } public event EventHandler ChannelUpdated; private void RaiseChannelUpdated(Channel channel) { if (ChannelUpdated != null) - RaiseEvent(nameof(ChannelUpdated), () => ChannelUpdated(this, new ChannelEventArgs(channel))); + EventHelper.Raise(_logger, nameof(ChannelUpdated), () => ChannelUpdated(this, new ChannelEventArgs(channel))); } /// Returns a collection of all servers this client is a member of. diff --git a/src/Discord.Net/DiscordClient.Messages.cs b/src/Discord.Net/DiscordClient.Messages.cs index 63c998a36..727c9c755 100644 --- a/src/Discord.Net/DiscordClient.Messages.cs +++ b/src/Discord.Net/DiscordClient.Messages.cs @@ -54,31 +54,31 @@ namespace Discord private void RaiseMessageReceived(Message msg) { if (MessageReceived != null) - RaiseEvent(nameof(MessageReceived), () => MessageReceived(this, new MessageEventArgs(msg))); + EventHelper.Raise(_logger, nameof(MessageReceived), () => MessageReceived(this, new MessageEventArgs(msg))); } public event EventHandler MessageSent; private void RaiseMessageSent(Message msg) { if (MessageSent != null) - RaiseEvent(nameof(MessageSent), () => MessageSent(this, new MessageEventArgs(msg))); + EventHelper.Raise(_logger, nameof(MessageSent), () => MessageSent(this, new MessageEventArgs(msg))); } public event EventHandler MessageDeleted; private void RaiseMessageDeleted(Message msg) { if (MessageDeleted != null) - RaiseEvent(nameof(MessageDeleted), () => MessageDeleted(this, new MessageEventArgs(msg))); + EventHelper.Raise(_logger, nameof(MessageDeleted), () => MessageDeleted(this, new MessageEventArgs(msg))); } public event EventHandler MessageUpdated; private void RaiseMessageUpdated(Message msg) { if (MessageUpdated != null) - RaiseEvent(nameof(MessageUpdated), () => MessageUpdated(this, new MessageEventArgs(msg))); + EventHelper.Raise(_logger, nameof(MessageUpdated), () => MessageUpdated(this, new MessageEventArgs(msg))); } public event EventHandler MessageReadRemotely; private void RaiseMessageReadRemotely(Message msg) { if (MessageReadRemotely != null) - RaiseEvent(nameof(MessageReadRemotely), () => MessageReadRemotely(this, new MessageEventArgs(msg))); + EventHelper.Raise(_logger, nameof(MessageReadRemotely), () => MessageReadRemotely(this, new MessageEventArgs(msg))); } internal Messages Messages => _messages; diff --git a/src/Discord.Net/DiscordClient.Roles.cs b/src/Discord.Net/DiscordClient.Roles.cs index 028f38b66..03d884e66 100644 --- a/src/Discord.Net/DiscordClient.Roles.cs +++ b/src/Discord.Net/DiscordClient.Roles.cs @@ -29,19 +29,19 @@ namespace Discord private void RaiseRoleCreated(Role role) { if (RoleCreated != null) - RaiseEvent(nameof(RoleCreated), () => RoleCreated(this, new RoleEventArgs(role))); + EventHelper.Raise(_logger, nameof(RoleCreated), () => RoleCreated(this, new RoleEventArgs(role))); } public event EventHandler RoleUpdated; private void RaiseRoleDeleted(Role role) { if (RoleDeleted != null) - RaiseEvent(nameof(RoleDeleted), () => RoleDeleted(this, new RoleEventArgs(role))); + EventHelper.Raise(_logger, nameof(RoleDeleted), () => RoleDeleted(this, new RoleEventArgs(role))); } public event EventHandler RoleDeleted; private void RaiseRoleUpdated(Role role) { if (RoleUpdated != null) - RaiseEvent(nameof(RoleUpdated), () => RoleUpdated(this, new RoleEventArgs(role))); + EventHelper.Raise(_logger, nameof(RoleUpdated), () => RoleUpdated(this, new RoleEventArgs(role))); } internal Roles Roles => _roles; diff --git a/src/Discord.Net/DiscordClient.Servers.cs b/src/Discord.Net/DiscordClient.Servers.cs index 355055121..14960bc44 100644 --- a/src/Discord.Net/DiscordClient.Servers.cs +++ b/src/Discord.Net/DiscordClient.Servers.cs @@ -29,31 +29,31 @@ namespace Discord private void RaiseJoinedServer(Server server) { if (JoinedServer != null) - RaiseEvent(nameof(JoinedServer), () => JoinedServer(this, new ServerEventArgs(server))); + EventHelper.Raise(_logger, nameof(JoinedServer), () => JoinedServer(this, new ServerEventArgs(server))); } public event EventHandler LeftServer; private void RaiseLeftServer(Server server) { if (LeftServer != null) - RaiseEvent(nameof(LeftServer), () => LeftServer(this, new ServerEventArgs(server))); + EventHelper.Raise(_logger, nameof(LeftServer), () => LeftServer(this, new ServerEventArgs(server))); } public event EventHandler ServerUpdated; private void RaiseServerUpdated(Server server) { if (ServerUpdated != null) - RaiseEvent(nameof(ServerUpdated), () => ServerUpdated(this, new ServerEventArgs(server))); + EventHelper.Raise(_logger, nameof(ServerUpdated), () => ServerUpdated(this, new ServerEventArgs(server))); } public event EventHandler ServerUnavailable; private void RaiseServerUnavailable(Server server) { if (ServerUnavailable != null) - RaiseEvent(nameof(ServerUnavailable), () => ServerUnavailable(this, new ServerEventArgs(server))); + EventHelper.Raise(_logger, nameof(ServerUnavailable), () => ServerUnavailable(this, new ServerEventArgs(server))); } public event EventHandler ServerAvailable; private void RaiseServerAvailable(Server server) { if (ServerAvailable != null) - RaiseEvent(nameof(ServerAvailable), () => ServerAvailable(this, new ServerEventArgs(server))); + EventHelper.Raise(_logger, nameof(ServerAvailable), () => ServerAvailable(this, new ServerEventArgs(server))); } /// Returns a collection of all servers this client is a member of. diff --git a/src/Discord.Net/DiscordClient.Users.cs b/src/Discord.Net/DiscordClient.Users.cs index b8897dbbf..034853f0f 100644 --- a/src/Discord.Net/DiscordClient.Users.cs +++ b/src/Discord.Net/DiscordClient.Users.cs @@ -73,63 +73,63 @@ namespace Discord private void RaiseUserJoined(User user) { if (UserJoined != null) - RaiseEvent(nameof(UserJoined), () => UserJoined(this, new UserEventArgs(user))); + EventHelper.Raise(_logger, nameof(UserJoined), () => UserJoined(this, new UserEventArgs(user))); } public event EventHandler UserLeft; private void RaiseUserLeft(User user) { if (UserLeft != null) - RaiseEvent(nameof(UserLeft), () => UserLeft(this, new UserEventArgs(user))); + EventHelper.Raise(_logger, nameof(UserLeft), () => UserLeft(this, new UserEventArgs(user))); } public event EventHandler UserUpdated; private void RaiseUserUpdated(User user) { if (UserUpdated != null) - RaiseEvent(nameof(UserUpdated), () => UserUpdated(this, new UserEventArgs(user))); + EventHelper.Raise(_logger, nameof(UserUpdated), () => UserUpdated(this, new UserEventArgs(user))); } public event EventHandler UserPresenceUpdated; private void RaiseUserPresenceUpdated(User user) { if (UserPresenceUpdated != null) - RaiseEvent(nameof(UserPresenceUpdated), () => UserPresenceUpdated(this, new UserEventArgs(user))); + EventHelper.Raise(_logger, nameof(UserPresenceUpdated), () => UserPresenceUpdated(this, new UserEventArgs(user))); } public event EventHandler UserVoiceStateUpdated; private void RaiseUserVoiceStateUpdated(User user) { if (UserVoiceStateUpdated != null) - RaiseEvent(nameof(UserVoiceStateUpdated), () => UserVoiceStateUpdated(this, new UserEventArgs(user))); + EventHelper.Raise(_logger, nameof(UserVoiceStateUpdated), () => UserVoiceStateUpdated(this, new UserEventArgs(user))); } public event EventHandler UserIsTypingUpdated; private void RaiseUserIsTyping(User user, Channel channel) { if (UserIsTypingUpdated != null) - RaiseEvent(nameof(UserIsTypingUpdated), () => UserIsTypingUpdated(this, new UserChannelEventArgs(user, channel))); + EventHelper.Raise(_logger, nameof(UserIsTypingUpdated), () => UserIsTypingUpdated(this, new UserChannelEventArgs(user, channel))); } public event EventHandler ProfileUpdated; private void RaiseProfileUpdated() { if (ProfileUpdated != null) - RaiseEvent(nameof(ProfileUpdated), () => ProfileUpdated(this, EventArgs.Empty)); + EventHelper.Raise(_logger, nameof(ProfileUpdated), () => ProfileUpdated(this, EventArgs.Empty)); } public event EventHandler UserBanned; private void RaiseUserBanned(long userId, Server server) { if (UserBanned != null) - RaiseEvent(nameof(UserBanned), () => UserBanned(this, new BanEventArgs(userId, server))); + EventHelper.Raise(_logger, nameof(UserBanned), () => UserBanned(this, new BanEventArgs(userId, server))); } public event EventHandler UserUnbanned; private void RaiseUserUnbanned(long userId, Server server) { if (UserUnbanned != null) - RaiseEvent(nameof(UserUnbanned), () => UserUnbanned(this, new BanEventArgs(userId, server))); + EventHelper.Raise(_logger, nameof(UserUnbanned), () => UserUnbanned(this, new BanEventArgs(userId, server))); } - /// Returns the current logged-in user in a private channel. + /// Returns the current logged-in user used in private channels. internal User PrivateUser => _privateUser; private User _privateUser; /// Returns information about the currently logged-in account. - public GlobalUser CurrentUser { get { CheckReady(); return _privateUser.Global; } } + public GlobalUser CurrentUser => _privateUser?.Global; /// Returns a collection of all unique users this client can currently see. public IEnumerable AllUsers { get { CheckReady(); return _globalUsers; } } @@ -272,7 +272,7 @@ namespace Discord { if (server == null) throw new ArgumentNullException(nameof(server)); - _dataSocket.SendRequestUsers(server.Id); + _webSocket.SendRequestUsers(server.Id); } public async Task EditProfile(string currentPassword = "", @@ -312,7 +312,7 @@ namespace Discord } private Task SendStatus() { - _dataSocket.SendStatus(_status == UserStatus.Idle ? EpochTime.GetMilliseconds() - (10 * 60 * 1000) : (long?)null, _gameId); + _webSocket.SendStatus(_status == UserStatus.Idle ? EpochTime.GetMilliseconds() - (10 * 60 * 1000) : (long?)null, _gameId); return TaskHelper.CompletedTask; } } diff --git a/src/Discord.Net/DiscordClient.Voice.cs b/src/Discord.Net/DiscordClient.Voice.cs deleted file mode 100644 index 26d4d8558..000000000 --- a/src/Discord.Net/DiscordClient.Voice.cs +++ /dev/null @@ -1,81 +0,0 @@ -using Discord.Audio; -using System; -using System.Threading.Tasks; - -namespace Discord -{ - public partial class DiscordClient - { - public IDiscordVoiceClient GetVoiceClient(Server server) - { - if (server.Id <= 0) throw new ArgumentOutOfRangeException(nameof(server.Id)); - - if (!Config.EnableVoiceMultiserver) - { - if (server.Id == _voiceServerId) - return this; - else - return null; - } - - DiscordWSClient client; - if (_voiceClients.TryGetValue(server.Id, out client)) - return client; - else - return null; - } - private async Task CreateVoiceClient(Server server) - { - if (!Config.EnableVoiceMultiserver) - { - _voiceServerId = server.Id; - return this; - } - - var client = _voiceClients.GetOrAdd(server.Id, _ => - { - var config = _config.Clone(); - config.LogLevel = _config.LogLevel;// (LogMessageSeverity)Math.Min((int)_config.LogLevel, (int)LogMessageSeverity.Warning); - config.VoiceOnly = true; - config.VoiceClientId = unchecked(++_nextVoiceClientId); - return new DiscordWSClient(config, server.Id); - }); - client.LogMessage += (s, e) => - { - if (e.Source != LogMessageSource.DataWebSocket) - RaiseOnLog(e.Severity, e.Source, $"(#{client.Config.VoiceClientId}) {e.Message}", e.Exception); - }; - await client.Connect(_gateway, _token).ConfigureAwait(false); - return client; - } - - public async Task JoinVoiceServer(Channel channel) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - CheckReady(true); //checkVoice is done inside the voice client - - var client = await CreateVoiceClient(channel.Server).ConfigureAwait(false); - await client.JoinChannel(channel.Id).ConfigureAwait(false); - return client; - } - - public async Task LeaveVoiceServer(Server server) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - - if (Config.EnableVoiceMultiserver) - { - //client.CheckReady(); - DiscordWSClient client; - if (_voiceClients.TryRemove(server.Id, out client)) - await client.Disconnect().ConfigureAwait(false); - } - else - { - CheckReady(checkVoice: true); - await _voiceSocket.Disconnect().ConfigureAwait(false); - _dataSocket.SendLeaveVoice(server.Id); - } - } - } -} diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index b63131c14..b50635e0d 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -6,153 +6,279 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Threading; using System.Threading.Tasks; namespace Discord { + public enum DiscordClientState : byte + { + Disconnected, + Connecting, + Connected, + Disconnecting + } + + public class DisconnectedEventArgs : EventArgs + { + public readonly bool WasUnexpected; + public readonly Exception Error; + + public DisconnectedEventArgs(bool wasUnexpected, Exception error) + { + WasUnexpected = wasUnexpected; + Error = error; + } + } + public sealed class LogMessageEventArgs : EventArgs + { + public LogSeverity Severity { get; } + public string Source { get; } + public string Message { get; } + public Exception Exception { get; } + + public LogMessageEventArgs(LogSeverity severity, string source, string msg, Exception exception) + { + Severity = severity; + Source = source; + Message = msg; + Exception = exception; + } + } + /// Provides a connection to the DiscordApp service. - public sealed partial class DiscordClient : DiscordWSClient + public partial class DiscordClient { - public static readonly string Version = typeof(DiscordClientConfig).GetTypeInfo().Assembly.GetName().Version.ToString(3); + public static readonly string Version = typeof(DiscordClient).GetTypeInfo().Assembly.GetName().Version.ToString(3); - private readonly DiscordAPIClient _api; + private readonly ManualResetEvent _disconnectedEvent; + private readonly ManualResetEventSlim _connectedEvent; private readonly Random _rand; private readonly JsonSerializer _messageImporter; private readonly ConcurrentQueue _pendingMessages; private readonly Dictionary _singletons; + private readonly LogService _log; + private readonly object _cacheLock; + private Logger _logger, _restLogger, _cacheLogger; private bool _sentInitialLog; + private long? _userId; private UserStatus _status; private int? _gameId; + private Task _runTask; + private ExceptionDispatchInfo _disconnectReason; + private bool _wasDisconnectUnexpected; /// 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. - public new DiscordClientConfig Config => _config as DiscordClientConfig; + public DiscordClientConfig Config => _config; + private readonly DiscordClientConfig _config; + + /// Returns the current connection state of this client. + public DiscordClientState State => (DiscordClientState)_state; + private int _state; /// Gives direct access to the underlying DiscordAPIClient. This can be used to modify objects not in cache. - public DiscordAPIClient API => _api; - + public DiscordAPIClient APIClient => _api; + private readonly DiscordAPIClient _api; + + /// Returns the internal websocket object. + public DataWebSocket WebSocket => _webSocket; + private readonly DataWebSocket _webSocket; + + public string GatewayUrl => _gateway; + private string _gateway; + + public string Token => _token; + private string _token; + + /// Returns a cancellation token that triggers when the client is manually disconnected. + public CancellationToken CancelToken => _cancelToken; + private CancellationTokenSource _cancelTokenSource; + private CancellationToken _cancelToken; + + public event EventHandler Connected; + private void RaiseConnected() + { + if (Connected != null) + EventHelper.Raise(_logger, nameof(Connected), () => Connected(this, EventArgs.Empty)); + } + public event EventHandler Disconnected; + private void RaiseDisconnected(DisconnectedEventArgs e) + { + if (Disconnected != null) + EventHelper.Raise(_logger, nameof(Disconnected), () => Disconnected(this, e)); + } + /// Initializes a new instance of the DiscordClient class. public DiscordClient(DiscordClientConfig config = null) - : base(config ?? new DiscordClientConfig()) { + _config = config ?? new DiscordClientConfig(); + _config.Lock(); + _rand = new Random(); - _api = new DiscordAPIClient(_config); - if (Config.UseMessageQueue) - _pendingMessages = new ConcurrentQueue(); + _state = (int)DiscordClientState.Disconnected; + _status = UserStatus.Online; - object cacheLock = new object(); - _channels = new Channels(this, cacheLock); - _users = new Users(this, cacheLock); - _messages = new Messages(this, cacheLock, Config.MessageCacheLength > 0); - _roles = new Roles(this, cacheLock); - _servers = new Servers(this, cacheLock); - _globalUsers = new GlobalUsers(this, cacheLock); + //Services _singletons = new Dictionary(); + _log = AddService(new LogService()); + CreateMainLogger(); - _status = UserStatus.Online; - + //Async + _cancelToken = new CancellationToken(true); + _disconnectedEvent = new ManualResetEvent(true); + _connectedEvent = new ManualResetEventSlim(false); + + //Cache + _cacheLock = new object(); + _channels = new Channels(this, _cacheLock); + _users = new Users(this, _cacheLock); + _messages = new Messages(this, _cacheLock, Config.MessageCacheLength > 0); + _roles = new Roles(this, _cacheLock); + _servers = new Servers(this, _cacheLock); + _globalUsers = new GlobalUsers(this, _cacheLock); + CreateCacheLogger(); + + //Networking + _webSocket = CreateWebSocket(); + _api = new DiscordAPIClient(_config); + if (Config.UseMessageQueue) + _pendingMessages = new ConcurrentQueue(); this.Connected += async (s, e) => { _api.CancelToken = _cancelToken; await SendStatus().ConfigureAwait(false); }; - - if (_config.LogLevel >= LogMessageSeverity.Info) + CreateRestLogger(); + + //Import/Export + _messageImporter = new JsonSerializer(); + _messageImporter.ContractResolver = new Message.ImportResolver(); + } + + private void CreateMainLogger() + { + _logger = _log.CreateLogger("Client"); + if (_logger.Level >= LogSeverity.Info) { - JoinedServer += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, + JoinedServer += (s, e) => _logger.Log(LogSeverity.Info, $"Server Created: {e.Server?.Name ?? "[Private]"}"); - LeftServer += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, + LeftServer += (s, e) => _logger.Log(LogSeverity.Info, $"Server Destroyed: {e.Server?.Name ?? "[Private]"}"); - ServerUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, + ServerUpdated += (s, e) => _logger.Log(LogSeverity.Info, $"Server Updated: {e.Server?.Name ?? "[Private]"}"); - ServerAvailable += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, + ServerAvailable += (s, e) => _logger.Log(LogSeverity.Info, $"Server Available: {e.Server?.Name ?? "[Private]"}"); - ServerUnavailable += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, + ServerUnavailable += (s, e) => _logger.Log(LogSeverity.Info, $"Server Unavailable: {e.Server?.Name ?? "[Private]"}"); - ChannelCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, + ChannelCreated += (s, e) => _logger.Log(LogSeverity.Info, $"Channel Created: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}"); - ChannelDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, + ChannelDestroyed += (s, e) => _logger.Log(LogSeverity.Info, $"Channel Destroyed: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}"); - ChannelUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, + ChannelUpdated += (s, e) => _logger.Log(LogSeverity.Info, $"Channel Updated: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}"); - MessageReceived += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, + MessageReceived += (s, e) => _logger.Log(LogSeverity.Info, $"Message Received: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}"); - MessageDeleted += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, + MessageDeleted += (s, e) => _logger.Log(LogSeverity.Info, $"Message Deleted: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}"); - MessageUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, + MessageUpdated += (s, e) => _logger.Log(LogSeverity.Info, $"Message Update: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}"); - RoleCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, + RoleCreated += (s, e) => _logger.Log(LogSeverity.Info, $"Role Created: {e.Server?.Name ?? "[Private]"}/{e.Role?.Name}"); - RoleUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, + RoleUpdated += (s, e) => _logger.Log(LogSeverity.Info, $"Role Updated: {e.Server?.Name ?? "[Private]"}/{e.Role?.Name}"); - RoleDeleted += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, + RoleDeleted += (s, e) => _logger.Log(LogSeverity.Info, $"Role Deleted: {e.Server?.Name ?? "[Private]"}/{e.Role?.Name}"); - UserBanned += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, + UserBanned += (s, e) => _logger.Log(LogSeverity.Info, $"Banned User: {e.Server?.Name ?? "[Private]" }/{e.UserId}"); - UserUnbanned += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, + UserUnbanned += (s, e) => _logger.Log(LogSeverity.Info, $"Unbanned User: {e.Server?.Name ?? "[Private]"}/{e.UserId}"); - UserJoined += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, + UserJoined += (s, e) => _logger.Log(LogSeverity.Info, $"User Joined: {e.Server?.Name ?? "[Private]"}/{e.User.Name}"); - UserLeft += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, + UserLeft += (s, e) => _logger.Log(LogSeverity.Info, $"User Left: {e.Server?.Name ?? "[Private]"}/{e.User.Name}"); - UserUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, + UserUpdated += (s, e) => _logger.Log(LogSeverity.Info, $"User Updated: {e.Server?.Name ?? "[Private]"}/{e.User.Name}"); - UserVoiceStateUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, + UserVoiceStateUpdated += (s, e) => _logger.Log(LogSeverity.Info, $"User Updated (Voice State): {e.Server?.Name ?? "[Private]"}/{e.User.Name}"); - ProfileUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, + ProfileUpdated += (s, e) => _logger.Log(LogSeverity.Info, "Profile Updated"); } - if (_config.LogLevel >= LogMessageSeverity.Verbose) + if (_log.Level >= LogSeverity.Verbose) { - UserIsTypingUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, + UserIsTypingUpdated += (s, e) => _logger.Log(LogSeverity.Verbose, $"User Updated (Is Typing): {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.User?.Name}"); - MessageReadRemotely += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, + MessageReadRemotely += (s, e) => _logger.Log(LogSeverity.Verbose, $"Read Message (Remotely): {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}"); - MessageSent += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, + MessageSent += (s, e) => _logger.Log(LogSeverity.Verbose, $"Sent Message: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}"); - UserPresenceUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, + UserPresenceUpdated += (s, e) => _logger.Log(LogSeverity.Verbose, $"User Updated (Presence): {e.Server?.Name ?? "[Private]"}/{e.User?.Name}"); - + } + } + private void CreateRestLogger() + { + _restLogger = _log.CreateLogger("Rest"); + if (_log.Level >= LogSeverity.Verbose) + { _api.RestClient.OnRequest += (s, e) => { - if (e.Payload != null) - RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Rest, $"{e.Method} {e.Path}: {Math.Round(e.ElapsedMilliseconds, 2)} ms ({e.Payload})"); + if (e.Payload != null) + _restLogger.Log(LogSeverity.Verbose, $"{e.Method} {e.Path}: {Math.Round(e.ElapsedMilliseconds, 2)} ms ({e.Payload})"); else - RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Rest, $"{e.Method} {e.Path}: {Math.Round(e.ElapsedMilliseconds, 2)} ms"); + _restLogger.Log(LogSeverity.Verbose, $"{e.Method} {e.Path}: {Math.Round(e.ElapsedMilliseconds, 2)} ms"); }; } - if (_config.LogLevel >= LogMessageSeverity.Debug) + } + private void CreateCacheLogger() + { + _cacheLogger = _log.CreateLogger("Cache"); + if (_log.Level >= LogSeverity.Debug) { - _channels.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Created Channel {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); - _channels.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Destroyed Channel {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); - _channels.Cleared += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Cleared Channels"); - _users.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Created User {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); - _users.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Destroyed User {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); - _users.Cleared += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Cleared Users"); - _messages.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Created Message {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Channel.Id}/{e.Item.Id}"); - _messages.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Destroyed Message {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Channel.Id}/{e.Item.Id}"); - _messages.ItemRemapped += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Remapped Message {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Channel.Id}/[{e.OldId} -> {e.NewId}]"); - _messages.Cleared += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Cleared Messages"); - _roles.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Created Role {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); - _roles.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Destroyed Role {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); - _roles.Cleared += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Cleared Roles"); - _servers.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Created Server {e.Item.Id}"); - _servers.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Destroyed Server {e.Item.Id}"); - _servers.Cleared += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Cleared Servers"); - _globalUsers.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Created User {e.Item.Id}"); - _globalUsers.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Destroyed User {e.Item.Id}"); - _globalUsers.Cleared += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Cleared Users"); + _channels.ItemCreated += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Created Channel {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); + _channels.ItemDestroyed += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Destroyed Channel {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); + _channels.Cleared += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Cleared Channels"); + _users.ItemCreated += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Created User {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); + _users.ItemDestroyed += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Destroyed User {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); + _users.Cleared += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Cleared Users"); + _messages.ItemCreated += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Created Message {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Channel.Id}/{e.Item.Id}"); + _messages.ItemDestroyed += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Destroyed Message {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Channel.Id}/{e.Item.Id}"); + _messages.ItemRemapped += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Remapped Message {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Channel.Id}/[{e.OldId} -> {e.NewId}]"); + _messages.Cleared += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Cleared Messages"); + _roles.ItemCreated += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Created Role {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); + _roles.ItemDestroyed += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Destroyed Role {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); + _roles.Cleared += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Cleared Roles"); + _servers.ItemCreated += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Created Server {e.Item.Id}"); + _servers.ItemDestroyed += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Destroyed Server {e.Item.Id}"); + _servers.Cleared += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Cleared Servers"); + _globalUsers.ItemCreated += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Created User {e.Item.Id}"); + _globalUsers.ItemDestroyed += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Destroyed User {e.Item.Id}"); + _globalUsers.Cleared += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Cleared Users"); } + } - if (Config.UseMessageQueue) - _pendingMessages = new ConcurrentQueue(); - - _messageImporter = new JsonSerializer(); - _messageImporter.ContractResolver = new Message.ImportResolver(); - } + private DataWebSocket CreateWebSocket() + { + var socket = new DataWebSocket(this, _log.CreateLogger("WebSocket")); + socket.Connected += (s, e) => + { + if (_state == (int)DiscordClientState.Connecting) + CompleteConnect(); + }; + socket.Disconnected += async (s, e) => + { + RaiseDisconnected(e); + if (e.WasUnexpected) + await socket.Reconnect(_token).ConfigureAwait(false); + }; + + socket.ReceivedEvent += async (s, e) => await OnReceivedEvent(e).ConfigureAwait(false); + return socket; + } /// Connects to the Discord server with the provided email and password. /// Returns a token for future connections. - public new async Task Connect(string email, string password) + public async Task Connect(string email, string password) { if (!_sentInitialLog) SendInitialLog(); @@ -167,13 +293,13 @@ namespace Discord .Timeout(_config.APITimeout) .ConfigureAwait(false); token = response.Token; - if (_config.LogLevel >= LogMessageSeverity.Verbose) - RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, "Login successful, got token."); + if (_config.LogLevel >= LogSeverity.Verbose) + _logger.Log(LogSeverity.Verbose, "Login successful, got token."); + + await Connect(token); + return token; } catch (TaskCanceledException) { throw new TimeoutException(); } - - await Connect(token).ConfigureAwait(false); - return token; } /// Connects to the Discord server with the provided token. public async Task Connect(string token) @@ -185,22 +311,133 @@ namespace Discord await Disconnect().ConfigureAwait(false); _api.Token = token; - string gateway = (await _api.Gateway() - .Timeout(_config.APITimeout) - .ConfigureAwait(false) - ).Url; - if (_config.LogLevel >= LogMessageSeverity.Verbose) - RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, $"Websocket endpoint: {gateway}"); - - await base.Connect(gateway, token) - .Timeout(_config.ConnectionTimeout) - .ConfigureAwait(false); + var gatewayResponse = await _api.Gateway().Timeout(_config.APITimeout).ConfigureAwait(false); + string gateway = gatewayResponse.Url; + if (_config.LogLevel >= LogSeverity.Verbose) + _logger.Log(LogSeverity.Verbose, $"Websocket endpoint: {gateway}"); + + try + { + _state = (int)DiscordClientState.Connecting; + _disconnectedEvent.Reset(); + + _gateway = gateway; + _token = token; + + _cancelTokenSource = new CancellationTokenSource(); + _cancelToken = _cancelTokenSource.Token; + + _webSocket.Host = gateway; + _webSocket.ParentCancelToken = _cancelToken; + await _webSocket.Login(token).ConfigureAwait(false); + + _runTask = RunTasks(); + + 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; + } + + //_state = (int)DiscordClientState.Connected; + } + catch + { + await Disconnect().ConfigureAwait(false); + throw; + } + } + private void CompleteConnect() + { + _state = (int)DiscordClientState.Connected; + _connectedEvent.Set(); + RaiseConnected(); } - protected override async Task Cleanup() + /// Disconnects from the Discord server, canceling any pending requests. + public Task Disconnect() => DisconnectInternal(new Exception("Disconnect was requested by user."), isUnexpected: false); + private async Task DisconnectInternal(Exception ex = null, bool isUnexpected = true, bool skipAwait = false) { - await base.Cleanup().ConfigureAwait(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 (!skipAwait) + { + Task task = _runTask; + if (_runTask != null) + await task.ConfigureAwait(false); + } + } + + private async Task RunTasks() + { + List tasks = new List(); + tasks.Add(_cancelToken.Wait()); + if (Config.UseMessageQueue) + tasks.Add(MessageQueueLoop()); + + 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 DisconnectInternal(ex: ex, skipAwait: true).ConfigureAwait(false); } + + //Ensure all other tasks are signaled to end. + await DisconnectInternal(skipAwait: 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.Disconnect().ConfigureAwait(false); + + _userId = null; + _gateway = null; + _token = null; + + if (!wasDisconnectUnexpected) + { + _state = (int)DiscordClientState.Disconnected; + _disconnectedEvent.Set(); + } + _connectedEvent.Reset(); + _runTask = null; + } + private async Task Stop() + { if (Config.UseMessageQueue) { Message ignored; @@ -247,16 +484,8 @@ namespace Discord public T GetService(bool required = true) where T : class, IService => GetSingleton(required); - - protected override IEnumerable GetTasks() - { - if (Config.UseMessageQueue) - return base.GetTasks().Concat(new Task[] { MessageQueueLoop() }); - else - return base.GetTasks(); - } - protected override async Task OnReceivedEvent(WebSocketEventEventArgs e) + private async Task OnReceivedEvent(WebSocketEventEventArgs e) { try { @@ -265,8 +494,7 @@ namespace Discord //Global case "READY": //Resync { - base.OnReceivedEvent(e).Wait(); //This cannot be an await, or we'll get later messages before we're ready - var data = e.Payload.ToObject(_dataSocketSerializer); + var data = e.Payload.ToObject(_webSocket.Serializer); _privateUser = _users.GetOrAdd(data.User.Id, null); _privateUser.Update(data.User); _privateUser.Global.Update(data.User); @@ -291,7 +519,7 @@ namespace Discord //Servers case "GUILD_CREATE": { - var data = e.Payload.ToObject(_dataSocketSerializer); + var data = e.Payload.ToObject(_webSocket.Serializer); if (data.Unavailable != true) { var server = _servers.GetOrAdd(data.Id); @@ -305,7 +533,7 @@ namespace Discord break; case "GUILD_UPDATE": { - var data = e.Payload.ToObject(_dataSocketSerializer); + var data = e.Payload.ToObject(_webSocket.Serializer); var server = _servers[data.Id]; if (server != null) { @@ -316,7 +544,7 @@ namespace Discord break; case "GUILD_DELETE": { - var data = e.Payload.ToObject(_dataSocketSerializer); + var data = e.Payload.ToObject(_webSocket.Serializer); var server = _servers.TryRemove(data.Id); if (server != null) { @@ -331,7 +559,7 @@ namespace Discord //Channels case "CHANNEL_CREATE": { - var data = e.Payload.ToObject(_dataSocketSerializer); + var data = e.Payload.ToObject(_webSocket.Serializer); Channel channel; if (data.IsPrivate) { @@ -347,7 +575,7 @@ namespace Discord break; case "CHANNEL_UPDATE": { - var data = e.Payload.ToObject(_dataSocketSerializer); + var data = e.Payload.ToObject(_webSocket.Serializer); var channel = _channels[data.Id]; if (channel != null) { @@ -358,7 +586,7 @@ namespace Discord break; case "CHANNEL_DELETE": { - var data = e.Payload.ToObject(_dataSocketSerializer); + var data = e.Payload.ToObject(_webSocket.Serializer); var channel = _channels.TryRemove(data.Id); if (channel != null) RaiseChannelDestroyed(channel); @@ -368,7 +596,7 @@ namespace Discord //Members case "GUILD_MEMBER_ADD": { - var data = e.Payload.ToObject(_dataSocketSerializer); + var data = e.Payload.ToObject(_webSocket.Serializer); var user = _users.GetOrAdd(data.User.Id, data.GuildId); user.Update(data); if (Config.TrackActivity) @@ -378,7 +606,7 @@ namespace Discord break; case "GUILD_MEMBER_UPDATE": { - var data = e.Payload.ToObject(_dataSocketSerializer); + var data = e.Payload.ToObject(_webSocket.Serializer); var user = _users[data.User.Id, data.GuildId]; if (user != null) { @@ -389,7 +617,7 @@ namespace Discord break; case "GUILD_MEMBER_REMOVE": { - var data = e.Payload.ToObject(_dataSocketSerializer); + var data = e.Payload.ToObject(_webSocket.Serializer); var user = _users.TryRemove(data.UserId, data.GuildId); if (user != null) RaiseUserLeft(user); @@ -397,7 +625,7 @@ namespace Discord break; case "GUILD_MEMBERS_CHUNK": { - var data = e.Payload.ToObject(_dataSocketSerializer); + var data = e.Payload.ToObject(_webSocket.Serializer); foreach (var memberData in data.Members) { var user = _users.GetOrAdd(memberData.User.Id, memberData.GuildId); @@ -410,7 +638,7 @@ namespace Discord //Roles case "GUILD_ROLE_CREATE": { - var data = e.Payload.ToObject(_dataSocketSerializer); + var data = e.Payload.ToObject(_webSocket.Serializer); var role = _roles.GetOrAdd(data.Data.Id, data.GuildId); role.Update(data.Data); var server = _servers[data.GuildId]; @@ -421,7 +649,7 @@ namespace Discord break; case "GUILD_ROLE_UPDATE": { - var data = e.Payload.ToObject(_dataSocketSerializer); + var data = e.Payload.ToObject(_webSocket.Serializer); var role = _roles[data.Data.Id]; if (role != null) { @@ -432,7 +660,7 @@ namespace Discord break; case "GUILD_ROLE_DELETE": { - var data = e.Payload.ToObject(_dataSocketSerializer); + var data = e.Payload.ToObject(_webSocket.Serializer); var role = _roles.TryRemove(data.RoleId); if (role != null) { @@ -447,7 +675,7 @@ namespace Discord //Bans case "GUILD_BAN_ADD": { - var data = e.Payload.ToObject(_dataSocketSerializer); + var data = e.Payload.ToObject(_webSocket.Serializer); var server = _servers[data.GuildId]; if (server != null) { @@ -459,7 +687,7 @@ namespace Discord break; case "GUILD_BAN_REMOVE": { - var data = e.Payload.ToObject(_dataSocketSerializer); + var data = e.Payload.ToObject(_webSocket.Serializer); var server = _servers[data.GuildId]; if (server != null) { @@ -473,7 +701,7 @@ namespace Discord //Messages case "MESSAGE_CREATE": { - var data = e.Payload.ToObject(_dataSocketSerializer); + var data = e.Payload.ToObject(_webSocket.Serializer); Message msg = null; bool isAuthor = data.Author.Id == _userId; @@ -500,7 +728,7 @@ namespace Discord break; case "MESSAGE_UPDATE": { - var data = e.Payload.ToObject(_dataSocketSerializer); + var data = e.Payload.ToObject(_webSocket.Serializer); var msg = _messages[data.Id]; if (msg != null) { @@ -511,7 +739,7 @@ namespace Discord break; case "MESSAGE_DELETE": { - var data = e.Payload.ToObject(_dataSocketSerializer); + var data = e.Payload.ToObject(_webSocket.Serializer); var msg = _messages.TryRemove(data.Id); if (msg != null) RaiseMessageDeleted(msg); @@ -519,7 +747,7 @@ namespace Discord break; case "MESSAGE_ACK": { - var data = e.Payload.ToObject(_dataSocketSerializer); + var data = e.Payload.ToObject(_webSocket.Serializer); var msg = GetMessage(data.MessageId); if (msg != null) RaiseMessageReadRemotely(msg); @@ -529,7 +757,7 @@ namespace Discord //Statuses case "PRESENCE_UPDATE": { - var data = e.Payload.ToObject(_dataSocketSerializer); + var data = e.Payload.ToObject(_webSocket.Serializer); var user = _users.GetOrAdd(data.User.Id, data.GuildId); if (user != null) { @@ -540,7 +768,7 @@ namespace Discord break; case "TYPING_START": { - var data = e.Payload.ToObject(_dataSocketSerializer); + var data = e.Payload.ToObject(_webSocket.Serializer); var channel = _channels[data.ChannelId]; if (channel != null) { @@ -566,7 +794,7 @@ namespace Discord //Voice case "VOICE_STATE_UPDATE": { - var data = e.Payload.ToObject(_dataSocketSerializer); + var data = e.Payload.ToObject(_webSocket.Serializer); var user = _users[data.UserId, data.GuildId]; if (user != null) { @@ -585,7 +813,7 @@ namespace Discord //Settings case "USER_UPDATE": { - var data = e.Payload.ToObject(_dataSocketSerializer); + var data = e.Payload.ToObject(_webSocket.Serializer); var user = _globalUsers[data.Id]; if (user != null) { @@ -598,35 +826,61 @@ namespace Discord //Ignored case "USER_SETTINGS_UPDATE": case "GUILD_INTEGRATIONS_UPDATE": - break; - - //Internal (handled in DataWebSocket) - case "RESUMED": - break; - - //Pass to DiscordWSClient case "VOICE_SERVER_UPDATE": - await base.OnReceivedEvent(e).ConfigureAwait(false); + break; + + case "RESUMED": //Handled in DataWebSocket break; //Others default: - RaiseOnLog(LogMessageSeverity.Warning, LogMessageSource.DataWebSocket, $"Unknown message type: {e.Type}"); + _webSocket.Logger.Log(LogSeverity.Warning, $"Unknown message type: {e.Type}"); break; } } catch (Exception ex) { - RaiseOnLog(LogMessageSeverity.Error, LogMessageSource.Client, $"Error handling {e.Type} event: {ex.GetBaseException().Message}"); + _logger.Log(LogSeverity.Error, $"Error handling {e.Type} event", ex); } } private void SendInitialLog() { - if (_config.LogLevel >= LogMessageSeverity.Verbose) - RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, $"Config: {JsonConvert.SerializeObject(_config)}"); + if (_config.LogLevel >= LogSeverity.Verbose) + _logger.Log(LogSeverity.Verbose, $"Config: {JsonConvert.SerializeObject(_config)}"); _sentInitialLog = true; } + + + //Helpers + /// Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications. + public void Run(Func asyncAction) + { + try + { + asyncAction().GetAwaiter().GetResult(); //Avoids creating AggregateExceptions + } + catch (TaskCanceledException) { } + _disconnectedEvent.WaitOne(); + } + /// Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications. + public void Run() + { + _disconnectedEvent.WaitOne(); + } + + private void CheckReady() + { + switch (_state) + { + case (int)DiscordClientState.Disconnecting: + throw new InvalidOperationException("The client is disconnecting."); + case (int)DiscordClientState.Disconnected: + throw new InvalidOperationException("The client is not connected to Discord"); + case (int)DiscordClientState.Connecting: + throw new InvalidOperationException("The client is connecting."); + } + } public void GetCacheStats(out int serverCount, out int channelCount, out int userCount, out int uniqueUserCount, out int messageCount, out int roleCount) { diff --git a/src/Discord.Net/DiscordClientConfig.cs b/src/Discord.Net/DiscordClientConfig.cs index fe136803f..32483fefc 100644 --- a/src/Discord.Net/DiscordClientConfig.cs +++ b/src/Discord.Net/DiscordClientConfig.cs @@ -1,18 +1,35 @@ namespace Discord { - public class DiscordClientConfig : DiscordWSClientConfig - { - /// Gets or sets the time (in milliseconds) to wait when the message queue is empty before checking again. - public int MessageQueueInterval { get { return _messageQueueInterval; } set { SetValue(ref _messageQueueInterval, value); } } - private int _messageQueueInterval = 100; + public class DiscordClientConfig : DiscordAPIClientConfig + { + /// Max time in milliseconds to wait for DiscordClient to connect and initialize. + public int ConnectionTimeout { get { return _connectionTimeout; } set { SetValue(ref _connectionTimeout, value); } } + private int _connectionTimeout = 30000; + /// Gets or sets the time (in milliseconds) to wait after an unexpected disconnect before reconnecting. + public int ReconnectDelay { get { return _reconnectDelay; } set { SetValue(ref _reconnectDelay, value); } } + private int _reconnectDelay = 1000; + /// Gets or sets the time (in milliseconds) to wait after an reconnect fails before retrying. + public int FailedReconnectDelay { get { return _failedReconnectDelay; } set { SetValue(ref _failedReconnectDelay, value); } } + private int _failedReconnectDelay = 10000; + /// Gets or sets the time (in milliseconds) to wait when the websocket's message queue is empty before checking again. + public int WebSocketInterval { get { return _webSocketInterval; } set { SetValue(ref _webSocketInterval, value); } } + private int _webSocketInterval = 100; /// Gets or sets the number of messages per channel that should be kept in cache. Setting this to zero disables the message cache entirely. public int MessageCacheLength { get { return _messageCacheLength; } set { SetValue(ref _messageCacheLength, value); } } private int _messageCacheLength = 100; + //Experimental Features + /// (Experimental) Instructs Discord to not send send information about offline users, for servers with more than 50 users. + public bool UseLargeThreshold { get { return _useLargeThreshold; } set { SetValue(ref _useLargeThreshold, value); } } + private bool _useLargeThreshold = false; + //Experimental Features /// (Experimental) Enables or disables the internal message queue. This will allow SendMessage to return immediately and handle messages internally. Messages will set the IsQueued and HasFailed properties to show their progress. public bool UseMessageQueue { get { return _useMessageQueue; } set { SetValue(ref _useMessageQueue, value); } } private bool _useMessageQueue = false; + /// Gets or sets the time (in milliseconds) to wait when the message queue is empty before checking again. + public int MessageQueueInterval { get { return _messageQueueInterval; } set { SetValue(ref _messageQueueInterval, value); } } + private int _messageQueueInterval = 100; /// (Experimental) Maintains the LastActivity property for users, showing when they last made an action (sent message, joined server, typed, etc). public bool TrackActivity { get { return _trackActivity; } set { SetValue(ref _trackActivity, value); } } private bool _trackActivity = true; diff --git a/src/Discord.Net/DiscordWSClient.Events.cs b/src/Discord.Net/DiscordWSClient.Events.cs deleted file mode 100644 index c33f41430..000000000 --- a/src/Discord.Net/DiscordWSClient.Events.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; - -namespace Discord -{ - public enum LogMessageSeverity : byte - { - Error = 1, - Warning = 2, - Info = 3, - Verbose = 4, - Debug = 5 - } - public enum LogMessageSource : byte - { - Unknown = 0, - Cache, - Client, - DataWebSocket, - MessageQueue, - Rest, - VoiceWebSocket, - } - - public class DisconnectedEventArgs : EventArgs - { - public readonly bool WasUnexpected; - public readonly Exception Error; - - public DisconnectedEventArgs(bool wasUnexpected, Exception error) - { - WasUnexpected = wasUnexpected; - Error = error; - } - } - public sealed class LogMessageEventArgs : EventArgs - { - public LogMessageSeverity Severity { get; } - public LogMessageSource Source { get; } - public string Message { get; } - public Exception Exception { get; } - - public LogMessageEventArgs(LogMessageSeverity severity, LogMessageSource source, string msg, Exception exception) - { - Severity = severity; - Source = source; - Message = msg; - Exception = exception; - } - } - - public sealed class VoicePacketEventArgs - { - public long UserId { get; } - public long ChannelId { get; } - public byte[] Buffer { get; } - public int Offset { get; } - public int Count { get; } - - public VoicePacketEventArgs(long userId, long channelId, byte[] buffer, int offset, int count) - { - UserId = userId; - Buffer = buffer; - Offset = offset; - Count = count; - } - } - - public partial class DiscordWSClient - { - public event EventHandler Connected; - private void RaiseConnected() - { - if (Connected != null) - RaiseEvent(nameof(Connected), () => Connected(this, EventArgs.Empty)); - } - public event EventHandler Disconnected; - private void RaiseDisconnected(DisconnectedEventArgs e) - { - if (Disconnected != null) - RaiseEvent(nameof(Disconnected), () => Disconnected(this, e)); - } - public event EventHandler LogMessage; - protected void RaiseOnLog(LogMessageSeverity severity, LogMessageSource source, string message, Exception exception = null) - { - if (LogMessage != null) - RaiseEvent(nameof(LogMessage), () => LogMessage(this, new LogMessageEventArgs(severity, source, message, exception))); - } - } -} diff --git a/src/Discord.Net/DiscordWSClient.cs b/src/Discord.Net/DiscordWSClient.cs deleted file mode 100644 index 2713d9cfc..000000000 --- a/src/Discord.Net/DiscordWSClient.cs +++ /dev/null @@ -1,306 +0,0 @@ -using Discord.Net.WebSockets; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.ExceptionServices; -using System.Threading; -using System.Threading.Tasks; - -namespace Discord -{ - public enum DiscordClientState : byte - { - Disconnected, - Connecting, - Connected, - Disconnecting - } - - /// Provides a minimalistic websocket connection to the Discord service. - public partial class DiscordWSClient - { - protected readonly DiscordWSClientConfig _config; - protected readonly ManualResetEvent _disconnectedEvent; - protected readonly ManualResetEventSlim _connectedEvent; - protected ExceptionDispatchInfo _disconnectReason; - protected readonly DataWebSocket _dataSocket; - protected string _gateway, _token; - protected long? _userId; - private Task _runTask; - private bool _wasDisconnectUnexpected; - - public long CurrentUserId => _userId.Value; - - /// 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. - public DiscordWSClientConfig Config => _config; - - /// Returns the current connection state of this client. - public DiscordClientState State => (DiscordClientState)_state; - private int _state; - - public CancellationToken CancelToken => _cancelToken; - private CancellationTokenSource _cancelTokenSource; - protected CancellationToken _cancelToken; - - internal JsonSerializer DataSocketSerializer => _dataSocketSerializer; - internal JsonSerializer VoiceSocketSerializer => _voiceSocketSerializer; - protected readonly JsonSerializer _dataSocketSerializer, _voiceSocketSerializer; - - /// Initializes a new instance of the DiscordClient class. - public DiscordWSClient(DiscordWSClientConfig config = null) - { - _config = config ?? new DiscordWSClientConfig(); - _config.Lock(); - - _state = (int)DiscordClientState.Disconnected; - _cancelToken = new CancellationToken(true); - _disconnectedEvent = new ManualResetEvent(true); - _connectedEvent = new ManualResetEventSlim(false); - - _dataSocketSerializer = new JsonSerializer(); - _dataSocketSerializer.DateTimeZoneHandling = DateTimeZoneHandling.Utc; -#if TEST_RESPONSES - _dataSocketSerializer.CheckAdditionalContent = true; - _dataSocketSerializer.MissingMemberHandling = MissingMemberHandling.Error; -#else - _dataSocketSerializer.Error += (s, e) => - { - e.ErrorContext.Handled = true; - RaiseOnLog(LogMessageSeverity.Error, LogMessageSource.DataWebSocket, "Serialization Failed", e.ErrorContext.Error); - }; -#endif - - _voiceSocketSerializer = new JsonSerializer(); - _voiceSocketSerializer.DateTimeZoneHandling = DateTimeZoneHandling.Utc; -#if TEST_RESPONSES - _voiceSocketSerializer.CheckAdditionalContent = true; - _voiceSocketSerializer.MissingMemberHandling = MissingMemberHandling.Error; -#else - _voiceSocketSerializer.Error += (s, e) => - { - e.ErrorContext.Handled = true; - RaiseOnLog(LogMessageSeverity.Error, LogMessageSource.VoiceWebSocket, "Serialization Failed", e.ErrorContext.Error); - }; -#endif - - _dataSocket = CreateDataSocket(); - } - - internal virtual DataWebSocket CreateDataSocket() - { - var socket = new DataWebSocket(this); - socket.Connected += (s, e) => - { - if (_state == (int)DiscordClientState.Connecting) - CompleteConnect(); } - ; - socket.Disconnected += async (s, e) => - { - RaiseDisconnected(e); - if (e.WasUnexpected) - await socket.Reconnect(_token).ConfigureAwait(false); - }; - - socket.LogMessage += (s, e) => RaiseOnLog(e.Severity, LogMessageSource.DataWebSocket, e.Message, e.Exception); - if (_config.LogLevel >= LogMessageSeverity.Info) - { - socket.Connected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.DataWebSocket, "Connected"); - socket.Disconnected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.DataWebSocket, "Disconnected"); - } - - socket.ReceivedEvent += async (s, e) => await OnReceivedEvent(e).ConfigureAwait(false); - return socket; - } - - //Connection - public async Task Connect(string gateway, string token) - { - if (gateway == null) throw new ArgumentNullException(nameof(gateway)); - if (token == null) throw new ArgumentNullException(nameof(token)); - - try - { - _state = (int)DiscordClientState.Connecting; - _disconnectedEvent.Reset(); - - _gateway = gateway; - _token = token; - - _cancelTokenSource = new CancellationTokenSource(); - _cancelToken = _cancelTokenSource.Token; - - _dataSocket.Host = gateway; - _dataSocket.ParentCancelToken = _cancelToken; - await _dataSocket.Login(token).ConfigureAwait(false); - - _runTask = RunTasks(); - - try - { - //Cancel if either Disconnect is called, data socket errors or timeout is reached - var cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken, _dataSocket.CancelToken).Token; - _connectedEvent.Wait(cancelToken); - } - catch (OperationCanceledException) - { - _dataSocket.ThrowError(); //Throws data socket's internal error if any occured - throw; - } - - //_state = (int)DiscordClientState.Connected; - return token; - } - catch - { - await Disconnect().ConfigureAwait(false); - throw; - } - } - protected void CompleteConnect() - { - _state = (int)DiscordClientState.Connected; - _connectedEvent.Set(); - RaiseConnected(); - } - - /// Disconnects from the Discord server, canceling any pending requests. - public Task Disconnect() => DisconnectInternal(new Exception("Disconnect was requested by user."), isUnexpected: false); - protected async Task DisconnectInternal(Exception ex = null, bool isUnexpected = true, bool skipAwait = 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 (!skipAwait) - { - Task task = _runTask; - if (_runTask != null) - await task.ConfigureAwait(false); - } - } - - private async Task RunTasks() - { - Task[] tasks = GetTasks().ToArray(); - Task firstTask = Task.WhenAny(tasks); - Task allTasks = Task.WhenAll(tasks); - - //Wait until the first task ends/errors and capture the error - try { await firstTask.ConfigureAwait(false); } - catch (Exception ex) { await DisconnectInternal(ex: ex, skipAwait: true).ConfigureAwait(false); } - - //Ensure all other tasks are signaled to end. - await DisconnectInternal(skipAwait: true).ConfigureAwait(false); - - //Wait for the remaining tasks to complete - try { await allTasks.ConfigureAwait(false); } - catch { } - - //Start cleanup - var wasDisconnectUnexpected = _wasDisconnectUnexpected; - _wasDisconnectUnexpected = false; - - await Cleanup().ConfigureAwait(false); - - if (!wasDisconnectUnexpected) - { - _state = (int)DiscordClientState.Disconnected; - _disconnectedEvent.Set(); - } - _connectedEvent.Reset(); - _runTask = null; - } - protected virtual IEnumerable GetTasks() - { - return new Task[] { _cancelToken.Wait() }; - } - - protected virtual async Task Cleanup() - { - await _dataSocket.Disconnect().ConfigureAwait(false); - - _userId = null; - _gateway = null; - _token = null; - } - - //Helpers - /// Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications. - public void Run(Func asyncAction) - { - try - { - asyncAction().GetAwaiter().GetResult(); //Avoids creating AggregateExceptions - } - catch (TaskCanceledException) { } - _disconnectedEvent.WaitOne(); - } - /// Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications. - public void Run() - { - _disconnectedEvent.WaitOne(); - } - - protected void CheckReady(bool checkVoice = false) - { - switch (_state) - { - case (int)DiscordClientState.Disconnecting: - throw new InvalidOperationException("The client is disconnecting."); - case (int)DiscordClientState.Disconnected: - throw new InvalidOperationException("The client is not connected to Discord"); - case (int)DiscordClientState.Connecting: - throw new InvalidOperationException("The client is connecting."); - } - } - protected void RaiseEvent(string name, Action action) - { - try { action(); } - catch (Exception ex) - { - var ex2 = ex.GetBaseException(); - RaiseOnLog(LogMessageSeverity.Error, LogMessageSource.Client, - $"{name}'s handler raised {ex2.GetType().Name}: ${ex2.Message}", ex); - } - } - - protected virtual Task OnReceivedEvent(WebSocketEventEventArgs e) - { - try - { - switch (e.Type) - { - case "READY": - _userId = IdConvert.ToLong(e.Payload["user"].Value("id")); - break; - } - } - catch (Exception ex) - { - RaiseOnLog(LogMessageSeverity.Error, LogMessageSource.Client, $"Error handling {e.Type} event: {ex.GetBaseException().Message}"); - } - return TaskHelper.CompletedTask; - } - } -} \ No newline at end of file diff --git a/src/Discord.Net/DiscordWSClientConfig.cs b/src/Discord.Net/DiscordWSClientConfig.cs deleted file mode 100644 index d4cd53d6c..000000000 --- a/src/Discord.Net/DiscordWSClientConfig.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Reflection; - -namespace Discord -{ - public class DiscordWSClientConfig : DiscordAPIClientConfig - { - /// Max time in milliseconds to wait for DiscordClient to connect and initialize. - public int ConnectionTimeout { get { return _connectionTimeout; } set { SetValue(ref _connectionTimeout, value); } } - private int _connectionTimeout = 30000; - /// Gets or sets the time (in milliseconds) to wait after an unexpected disconnect before reconnecting. - public int ReconnectDelay { get { return _reconnectDelay; } set { SetValue(ref _reconnectDelay, value); } } - private int _reconnectDelay = 1000; - /// Gets or sets the time (in milliseconds) to wait after an reconnect fails before retrying. - public int FailedReconnectDelay { get { return _failedReconnectDelay; } set { SetValue(ref _failedReconnectDelay, value); } } - private int _failedReconnectDelay = 10000; - /// Gets or sets the time (in milliseconds) to wait when the websocket's message queue is empty before checking again. - public int WebSocketInterval { get { return _webSocketInterval; } set { SetValue(ref _webSocketInterval, value); } } - private int _webSocketInterval = 100; - - //Experimental Features - /// (Experimental) Instructs Discord to not send send information about offline users, for servers with more than 50 users. - public bool UseLargeThreshold { get { return _useLargeThreshold; } set { SetValue(ref _useLargeThreshold, value); } } - private bool _useLargeThreshold = false; - - public new DiscordWSClientConfig Clone() - { - var config = MemberwiseClone() as DiscordWSClientConfig; - config._isLocked = false; - return config; - } - } -} \ No newline at end of file diff --git a/src/Discord.Net/Helpers/Mention.cs b/src/Discord.Net/Helpers/Mention.cs index c16eef8b9..12037f4ce 100644 --- a/src/Discord.Net/Helpers/Mention.cs +++ b/src/Discord.Net/Helpers/Mention.cs @@ -6,8 +6,8 @@ namespace Discord { public static class Mention { - private static readonly Regex _userRegex = new Regex(@"<@([0-9]+?)>", RegexOptions.Compiled); - private static readonly Regex _channelRegex = new Regex(@"<#([0-9]+?)>", RegexOptions.Compiled); + private static readonly Regex _userRegex = new Regex(@"<@([0-9]+)>", RegexOptions.Compiled); + private static readonly Regex _channelRegex = new Regex(@"<#([0-9]+)>", RegexOptions.Compiled); private static readonly Regex _roleRegex = new Regex(@"@everyone", RegexOptions.Compiled); /// Returns the string used to create a user mention. diff --git a/src/Discord.Net/Helpers/EpochTime.cs b/src/Discord.Net/Helpers/Shared/EpochTime.cs similarity index 100% rename from src/Discord.Net/Helpers/EpochTime.cs rename to src/Discord.Net/Helpers/Shared/EpochTime.cs diff --git a/src/Discord.Net/Helpers/Shared/TaskExtensions.cs b/src/Discord.Net/Helpers/Shared/TaskExtensions.cs index df7c02ade..9d58ce9ea 100644 --- a/src/Discord.Net/Helpers/Shared/TaskExtensions.cs +++ b/src/Discord.Net/Helpers/Shared/TaskExtensions.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; namespace Discord { - public static class TaskExtensions + internal static class TaskExtensions { public static async Task Timeout(this Task task, int milliseconds) { diff --git a/src/Discord.Net/LogService.cs b/src/Discord.Net/LogService.cs new file mode 100644 index 000000000..6397ead5b --- /dev/null +++ b/src/Discord.Net/LogService.cs @@ -0,0 +1,61 @@ +using System; + +namespace Discord +{ + public class LogService : IService + { + public DiscordClient Client => _client; + private DiscordClient _client; + + public LogSeverity Level => _level; + private LogSeverity _level; + + public event EventHandler LogMessage; + internal void RaiseLogMessage(LogMessageEventArgs e) + { + if (LogMessage != null) + { + try + { + LogMessage(this, e); + } + catch { } //We dont want to log on log errors + } + } + + void IService.Install(DiscordClient client) + { + _client = client; + _level = client.Config.LogLevel; + } + + public Logger CreateLogger(string source) + { + return new Logger(this, source); + } + } + + public class Logger + { + private LogService _service; + + public LogSeverity Level => _level; + private LogSeverity _level; + + public string Source => _source; + private string _source; + + internal Logger(LogService service, string source) + { + _service = service; + _level = service.Level; + _source = source; + } + + public void Log(LogSeverity severity, string message, Exception exception = null) + { + if (severity >= _service.Level) + _service.RaiseLogMessage(new LogMessageEventArgs(severity, _source, message, exception)); + } + } +} diff --git a/src/Discord.Net/Models/Message.cs b/src/Discord.Net/Models/Message.cs index 2c1c0731c..42c0a740d 100644 --- a/src/Discord.Net/Models/Message.cs +++ b/src/Discord.Net/Models/Message.cs @@ -94,7 +94,7 @@ namespace Discord /// This is not set to true if the user was mentioned with @everyone (see IsMentioningEverone). public bool IsMentioningMe { get; private set; } /// Returns true if the current user created this message. - public bool IsAuthor => _client.CurrentUserId == _user.Id; + public bool IsAuthor => _client.CurrentUser.Id == _user.Id; /// Returns true if the message was sent as text-to-speech by someone with permissions to do so. public bool IsTTS { get; private set; } /// Returns the state of this message. Only useful if UseMessageQueue is true. diff --git a/src/Discord.Net/Models/Server.cs b/src/Discord.Net/Models/Server.cs index 38e189ab5..ee109e2f5 100644 --- a/src/Discord.Net/Models/Server.cs +++ b/src/Discord.Net/Models/Server.cs @@ -39,7 +39,7 @@ namespace Discord public string IconUrl => IconId != null ? Endpoints.ServerIcon(Id, IconId) : null; /// Returns true if the current user created this server. - public bool IsOwner => _client.CurrentUserId == _owner.Id; + public bool IsOwner => _client.CurrentUser.Id == _owner.Id; /// Returns the user that first created this server. [JsonIgnore] diff --git a/src/Discord.Net/Models/User.cs b/src/Discord.Net/Models/User.cs index 0e3f82b44..80e7a19eb 100644 --- a/src/Discord.Net/Models/User.cs +++ b/src/Discord.Net/Models/User.cs @@ -131,13 +131,13 @@ namespace Discord x => { x.AddMember(this); - if (Id == _client.CurrentUserId) + if (Id == _client.CurrentUser.Id) x.CurrentUser = this; }, x => { x.RemoveMember(this); - if (Id == _client.CurrentUserId) + if (Id == _client.CurrentUser.Id) x.CurrentUser = null; }); _voiceChannel = new Reference(x => _client.Channels[x]); diff --git a/src/Discord.Net/Net/Rest/RestClient.cs b/src/Discord.Net/Net/Rest/RestClient.cs index 68b220241..28ca466d2 100644 --- a/src/Discord.Net/Net/Rest/RestClient.cs +++ b/src/Discord.Net/Net/Rest/RestClient.cs @@ -91,7 +91,7 @@ namespace Discord.Net.Rest if (content != null) requestJson = JsonConvert.SerializeObject(content); - if (_config.LogLevel >= LogMessageSeverity.Verbose) + if (_config.LogLevel >= LogSeverity.Verbose) stopwatch = Stopwatch.StartNew(); string responseJson = await _engine.Send(method, path, requestJson, _cancelToken).ConfigureAwait(false); @@ -101,10 +101,10 @@ namespace Discord.Net.Rest throw new Exception("API check failed: Response is not empty."); #endif - if (_config.LogLevel >= LogMessageSeverity.Verbose) + if (_config.LogLevel >= LogSeverity.Verbose) { stopwatch.Stop(); - if (content != null && _config.LogLevel >= LogMessageSeverity.Debug) + if (content != null && _config.LogLevel >= LogSeverity.Debug) { if (path.StartsWith(Endpoints.Auth)) RaiseOnRequest(method, path, "[Hidden]", stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond); @@ -130,7 +130,7 @@ namespace Discord.Net.Rest { Stopwatch stopwatch = null; - if (_config.LogLevel >= LogMessageSeverity.Verbose) + if (_config.LogLevel >= LogSeverity.Verbose) stopwatch = Stopwatch.StartNew(); string responseJson = await _engine.SendFile(method, path, filename, stream, _cancelToken).ConfigureAwait(false); @@ -140,10 +140,10 @@ namespace Discord.Net.Rest throw new Exception("API check failed: Response is not empty."); #endif - if (_config.LogLevel >= LogMessageSeverity.Verbose) + if (_config.LogLevel >= LogSeverity.Verbose) { stopwatch.Stop(); - if (_config.LogLevel >= LogMessageSeverity.Debug) + if (_config.LogLevel >= LogSeverity.Debug) RaiseOnRequest(method, path, filename, stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond); else RaiseOnRequest(method, path, null, stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond); diff --git a/src/Discord.Net/Net/WebSockets/DataWebSocket.cs b/src/Discord.Net/Net/WebSockets/DataWebSocket.cs index 253d13299..225de42ba 100644 --- a/src/Discord.Net/Net/WebSockets/DataWebSocket.cs +++ b/src/Discord.Net/Net/WebSockets/DataWebSocket.cs @@ -26,8 +26,8 @@ namespace Discord.Net.WebSockets public string SessionId => _sessionId; private string _sessionId; - public DataWebSocket(DiscordWSClient client) - : base(client) + public DataWebSocket(DiscordClient client, Logger logger) + : base(client, logger) { } @@ -72,7 +72,7 @@ namespace Discord.Net.WebSockets catch (OperationCanceledException) { throw; } catch (Exception ex) { - RaiseOnLog(LogMessageSeverity.Error, $"Reconnect failed: {ex.GetBaseException().Message}"); + _logger.Log(LogSeverity.Error, $"Reconnect failed", ex); //Net is down? We can keep trying to reconnect until the user runs Disconnect() await Task.Delay(_client.Config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); } @@ -96,13 +96,13 @@ namespace Discord.Net.WebSockets JToken token = msg.Payload as JToken; if (msg.Type == "READY") { - var payload = token.ToObject(_client.DataSocketSerializer); + var payload = token.ToObject(_serializer); _sessionId = payload.SessionId; _heartbeatInterval = payload.HeartbeatInterval; } else if (msg.Type == "RESUMED") { - var payload = token.ToObject(_client.DataSocketSerializer); + var payload = token.ToObject(_serializer); _heartbeatInterval = payload.HeartbeatInterval; } RaiseReceivedEvent(msg.Type, token); @@ -112,19 +112,19 @@ namespace Discord.Net.WebSockets break; case OpCodes.Redirect: { - var payload = (msg.Payload as JToken).ToObject(_client.DataSocketSerializer); + var payload = (msg.Payload as JToken).ToObject(_serializer); if (payload.Url != null) { Host = payload.Url; - if (_logLevel >= LogMessageSeverity.Info) - RaiseOnLog(LogMessageSeverity.Info, "Redirected to " + payload.Url); + if (_logger.Level >= LogSeverity.Info) + _logger.Log(LogSeverity.Info, "Redirected to " + payload.Url); await Redirect(payload.Url).ConfigureAwait(false); } } break; default: - if (_logLevel >= LogMessageSeverity.Warning) - RaiseOnLog(LogMessageSeverity.Warning, $"Unknown Opcode: {opCode}"); + if (_logger.Level >= LogSeverity.Warning) + _logger.Log(LogSeverity.Warning, $"Unknown Opcode: {opCode}"); break; } } diff --git a/src/Discord.Net/Net/WebSockets/WebSocket.Events.cs b/src/Discord.Net/Net/WebSockets/WebSocket.Events.cs deleted file mode 100644 index 5fc538b89..000000000 --- a/src/Discord.Net/Net/WebSockets/WebSocket.Events.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; - -namespace Discord.Net.WebSockets -{ - public abstract partial class WebSocket - { - public event EventHandler Connected; - private void RaiseConnected() - { - if (Connected != null) - Connected(this, EventArgs.Empty); - } - public event EventHandler Disconnected; - private void RaiseDisconnected(bool wasUnexpected, Exception error) - { - if (Disconnected != null) - Disconnected(this, new DisconnectedEventArgs(wasUnexpected, error)); - } - - public event EventHandler LogMessage; - internal void RaiseOnLog(LogMessageSeverity severity, string message, Exception exception = null) - { - if (LogMessage != null) - LogMessage(this, new LogMessageEventArgs(severity, LogMessageSource.Unknown, message, exception)); - } - } -} diff --git a/src/Discord.Net/Net/WebSockets/WebSocket.cs b/src/Discord.Net/Net/WebSockets/WebSocket.cs index 9b7ba328c..8562837da 100644 --- a/src/Discord.Net/Net/WebSockets/WebSocket.cs +++ b/src/Discord.Net/Net/WebSockets/WebSocket.cs @@ -21,8 +21,7 @@ namespace Discord.Net.WebSockets public abstract partial class WebSocket { protected readonly IWebSocketEngine _engine; - protected readonly DiscordWSClient _client; - protected readonly LogMessageSeverity _logLevel; + protected readonly DiscordClient _client; protected readonly ManualResetEventSlim _connectedEvent; protected ExceptionDispatchInfo _disconnectReason; @@ -38,24 +37,48 @@ namespace Discord.Net.WebSockets private CancellationTokenSource _cancelTokenSource; protected CancellationToken _cancelToken; - public string Host { get; set; } + internal JsonSerializer Serializer => _serializer; + protected JsonSerializer _serializer; + + public Logger Logger => _logger; + protected readonly Logger _logger; + + public string Host { get { return _host; } set { _host = value; } } + private string _host; public WebSocketState State => (WebSocketState)_state; protected int _state; - public WebSocket(DiscordWSClient client) + public event EventHandler Connected; + private void RaiseConnected() + { + if (_logger.Level >= LogSeverity.Info) + _logger.Log(LogSeverity.Info, "Connected"); + if (Connected != null) + Connected(this, EventArgs.Empty); + } + public event EventHandler Disconnected; + private void RaiseDisconnected(bool wasUnexpected, Exception error) + { + if (_logger.Level >= LogSeverity.Info) + _logger.Log(LogSeverity.Info, "Disconnected"); + if (Disconnected != null) + Disconnected(this, new DisconnectedEventArgs(wasUnexpected, error)); + } + + public WebSocket(DiscordClient client, Logger logger) { _client = client; - _logLevel = client.Config.LogLevel; + _logger = logger; _loginTimeout = client.Config.ConnectionTimeout; _cancelToken = new CancellationToken(true); _connectedEvent = new ManualResetEventSlim(false); #if !DOTNET5_4 - _engine = new WebSocketSharpEngine(this, client.Config); + _engine = new WebSocketSharpEngine(this, client.Config, _logger); #else - //_engine = new BuiltInWebSocketEngine(this, client.Config); + //_engine = new BuiltInWebSocketEngine(this, client.Config, _logger); #endif _engine.BinaryMessage += (s, e) => { @@ -73,6 +96,19 @@ namespace Discord.Net.WebSockets { /*await*/ ProcessMessage(e.Message).Wait(); }; + + _serializer = new JsonSerializer(); + _serializer.DateTimeZoneHandling = DateTimeZoneHandling.Utc; +#if TEST_RESPONSES + _serializer.CheckAdditionalContent = true; + _serializer.MissingMemberHandling = MissingMemberHandling.Error; +#else + _serializer.Error += (s, e) => + { + e.ErrorContext.Handled = true; + _logger.Log(LogSeverity.Error, "Serialization Failed", e.ErrorContext.Error); + }; +#endif } protected async Task BeginConnect() @@ -94,25 +130,6 @@ namespace Discord.Net.WebSockets throw; } } - - protected virtual async Task Start() - { - try - { - 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 = RunTasks(); - } - catch (Exception ex) - { - await DisconnectInternal(ex, isUnexpected: false).ConfigureAwait(false); - throw; - } - } protected void EndConnect() { _state = (int)WebSocketState.Connected; @@ -145,7 +162,7 @@ namespace Discord.Net.WebSockets _cancelTokenSource.Cancel(); if (_disconnectState == WebSocketState.Connecting) //_runTask was never made - await Cleanup().ConfigureAwait(false); + await Stop().ConfigureAwait(false); } if (!skipAwait) @@ -156,6 +173,25 @@ namespace Discord.Net.WebSockets } } + protected virtual async Task Start() + { + try + { + 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 = RunTasks(); + } + catch (Exception ex) + { + await DisconnectInternal(ex, isUnexpected: false).ConfigureAwait(false); + throw; + } + } + protected virtual async Task RunTasks() { Task[] tasks = GetTasks().ToArray(); @@ -174,7 +210,7 @@ namespace Discord.Net.WebSockets catch { } //Start cleanup - await Cleanup().ConfigureAwait(false); + await Stop().ConfigureAwait(false); } protected virtual IEnumerable GetTasks() { @@ -182,7 +218,8 @@ namespace Discord.Net.WebSockets return _engine.GetTasks(cancelToken) .Concat(new Task[] { HeartbeatAsync(cancelToken) }); } - protected virtual async Task Cleanup() + + protected virtual async Task Stop() { var disconnectState = _disconnectState; _disconnectState = WebSocketState.Disconnected; @@ -203,8 +240,8 @@ namespace Discord.Net.WebSockets protected virtual Task ProcessMessage(string json) { - if (_logLevel >= LogMessageSeverity.Debug) - RaiseOnLog(LogMessageSeverity.Debug, $"In: {json}"); + if (_logger.Level >= LogSeverity.Debug) + _logger.Log(LogSeverity.Debug, $"In: {json}"); return TaskHelper.CompletedTask; } protected abstract object GetKeepAlive(); @@ -212,8 +249,8 @@ namespace Discord.Net.WebSockets protected void QueueMessage(object message) { string json = JsonConvert.SerializeObject(message); - if (_logLevel >= LogMessageSeverity.Debug) - RaiseOnLog(LogMessageSeverity.Debug, $"Out: " + json); + if (_logger.Level >= LogSeverity.Debug) + _logger.Log(LogSeverity.Debug, $"Out: " + json); _engine.QueueMessage(json); } diff --git a/src/Discord.Net/Net/WebSockets/WebSocketSharpEngine.cs b/src/Discord.Net/Net/WebSockets/WebSocketSharpEngine.cs index 9cce04d92..11b41172c 100644 --- a/src/Discord.Net/Net/WebSockets/WebSocketSharpEngine.cs +++ b/src/Discord.Net/Net/WebSockets/WebSocketSharpEngine.cs @@ -10,7 +10,8 @@ namespace Discord.Net.WebSockets { internal class WebSocketSharpEngine : IWebSocketEngine { - private readonly DiscordWSClientConfig _config; + private readonly DiscordClientConfig _config; + private readonly Logger _logger; private readonly ConcurrentQueue _sendQueue; private readonly WebSocket _parent; private WSSharpWebSocket _webSocket; @@ -28,10 +29,11 @@ namespace Discord.Net.WebSockets TextMessage(this, new WebSocketTextMessageEventArgs(msg)); } - internal WebSocketSharpEngine(WebSocket parent, DiscordWSClientConfig config) + internal WebSocketSharpEngine(WebSocket parent, DiscordClientConfig config, Logger logger) { _parent = parent; _config = config; + _logger = logger; _sendQueue = new ConcurrentQueue(); } @@ -51,7 +53,7 @@ namespace Discord.Net.WebSockets }; _webSocket.OnError += async (s, e) => { - _parent.RaiseOnLog(LogMessageSeverity.Error, e.Exception?.GetBaseException()?.Message ?? e.Message); + _logger.Log(LogSeverity.Error, "WebSocket Error", e.Exception); await _parent.DisconnectInternal(e.Exception, skipAwait: true).ConfigureAwait(false); }; _webSocket.OnClose += async (s, e) => @@ -61,7 +63,7 @@ namespace Discord.Net.WebSockets Exception ex = new Exception($"Got Close Message ({code}): {reason}"); await _parent.DisconnectInternal(ex, skipAwait: true).ConfigureAwait(false); }; - _webSocket.Log.Output = (e, m) => { }; //Dont let websocket-sharp print to console + _webSocket.Log.Output = (e, m) => { }; //Dont let websocket-sharp print to console directly _webSocket.Connect(); return TaskHelper.CompletedTask; } diff --git a/src/Discord.Net/TimeoutException.cs b/src/Discord.Net/TimeoutException.cs index 34778d1c8..1c831946f 100644 --- a/src/Discord.Net/TimeoutException.cs +++ b/src/Discord.Net/TimeoutException.cs @@ -4,7 +4,7 @@ namespace Discord { public sealed class TimeoutException : OperationCanceledException { - internal TimeoutException() + public TimeoutException() : base("An operation has timed out.") { }