@@ -6,153 +6,279 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Linq;
using System.Reflection;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks;
namespace Discord
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;
}
}
/// <summary> Provides a connection to the DiscordApp service. </summary>
/// <summary> Provides a connection to the DiscordApp service. </summary>
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 Random _rand;
private readonly JsonSerializer _messageImporter;
private readonly JsonSerializer _messageImporter;
private readonly ConcurrentQueue<Message> _pendingMessages;
private readonly ConcurrentQueue<Message> _pendingMessages;
private readonly Dictionary<Type, object> _singletons;
private readonly Dictionary<Type, object> _singletons;
private readonly LogService _log;
private readonly object _cacheLock;
private Logger _logger, _restLogger, _cacheLogger;
private bool _sentInitialLog;
private bool _sentInitialLog;
private long? _userId;
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 new DiscordClientConfig Config => _config as DiscordClientConfig;
public DiscordClientConfig Config => _config;
private readonly DiscordClientConfig _config;
/// <summary> Returns the current connection state of this client. </summary>
public DiscordClientState State => (DiscordClientState)_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>
public DiscordAPIClient API => _api;
public DiscordAPIClient APIClient => _api;
private readonly DiscordAPIClient _api;
/// <summary> Returns the internal websocket object. </summary>
public DataWebSocket WebSocket => _webSocket;
private readonly DataWebSocket _webSocket;
public string GatewayUrl => _gateway;
private string _gateway;
public string Token => _token;
private string _token;
/// <summary> Returns a cancellation token that triggers when the client is manually disconnected. </summary>
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<DisconnectedEventArgs> Disconnected;
private void RaiseDisconnected(DisconnectedEventArgs e)
{
if (Disconnected != null)
EventHelper.Raise(_logger, nameof(Disconnected), () => Disconnected(this, e));
}
/// <summary> Initializes a new instance of the DiscordClient class. </summary>
/// <summary> Initializes a new instance of the DiscordClient class. </summary>
public DiscordClient(DiscordClientConfig config = null)
public DiscordClient(DiscordClientConfig config = null)
: base(config ?? new DiscordClientConfig())
{
{
_config = config ?? new DiscordClientConfig();
_config.Lock();
_rand = new Random();
_rand = new Random();
_api = new DiscordAPIClient(_config);
if (Config.UseMessageQueue)
_pendingMessages = new ConcurrentQueue<Message>();
_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<Type, object>();
_singletons = new Dictionary<Type, object>();
_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<Message>();
this.Connected += async (s, e) =>
this.Connected += async (s, e) =>
{
{
_api.CancelToken = _cancelToken;
_api.CancelToken = _cancelToken;
await SendStatus().ConfigureAwait(false);
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]"}");
$"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]"}");
$"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]"}");
$"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]"}");
$"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]"}");
$"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}");
$"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}");
$"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}");
$"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}");
$"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}");
$"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}");
$"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}");
$"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}");
$"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}");
$"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}");
$"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}");
$"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}");
$"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}");
$"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}");
$"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}");
$"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");
"Profile Updated");
}
}
if (_config.LogLevel >= LogMessage Severity.Verbose)
if (_log.Level >= Log Severity.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}");
$"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}");
$"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}");
$"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}");
$"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) =>
_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
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<Message>();
_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;
}
/// <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>
public new async Task<string> Connect(string email, string password)
public async Task<string> Connect(string email, string password)
{
{
if (!_sentInitialLog)
if (!_sentInitialLog)
SendInitialLog();
SendInitialLog();
@@ -167,13 +293,13 @@ namespace Discord
.Timeout(_config.APITimeout)
.Timeout(_config.APITimeout)
.ConfigureAwait(false);
.ConfigureAwait(false);
token = response.Token;
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(); }
catch (TaskCanceledException) { throw new TimeoutException(); }
await Connect(token).ConfigureAwait(false);
return token;
}
}
/// <summary> Connects to the Discord server with the provided token. </summary>
/// <summary> Connects to the Discord server with the provided token. </summary>
public async Task Connect(string token)
public async Task Connect(string token)
@@ -185,22 +311,133 @@ namespace Discord
await Disconnect().ConfigureAwait(false);
await Disconnect().ConfigureAwait(false);
_api.Token = token;
_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()
/// <summary> Disconnects from the Discord server, canceling any pending requests. </summary>
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<Task> tasks = new List<Task>();
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)
if (Config.UseMessageQueue)
{
{
Message ignored;
Message ignored;
@@ -247,16 +484,8 @@ namespace Discord
public T GetService<T>(bool required = true)
public T GetService<T>(bool required = true)
where T : class, IService
where T : class, IService
=> GetSingleton<T>(required);
=> GetSingleton<T>(required);
protected override IEnumerable<Task> GetTasks()
{
if (Config.UseMessageQueue)
return base.GetTasks().Concat(new Task[] { MessageQueueLoop() });
else
return base.GetTasks();
}
protected overrid e async Task OnReceivedEvent(WebSocketEventEventArgs e)
private async Task OnReceivedEvent(WebSocketEventEventArgs e)
{
{
try
try
{
{
@@ -265,8 +494,7 @@ namespace Discord
//Global
//Global
case "READY": //Resync
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<ReadyEvent>(_dataSocketSerializer);
var data = e.Payload.ToObject<ReadyEvent>(_webSocket.Serializer);
_privateUser = _users.GetOrAdd(data.User.Id, null);
_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);
@@ -291,7 +519,7 @@ namespace Discord
//Servers
//Servers
case "GUILD_CREATE":
case "GUILD_CREATE":
{
{
var data = e.Payload.ToObject<GuildCreateEvent>(_dataSocket Serializer);
var data = e.Payload.ToObject<GuildCreateEvent>(_webSocket. Serializer);
if (data.Unavailable != true)
if (data.Unavailable != true)
{
{
var server = _servers.GetOrAdd(data.Id);
var server = _servers.GetOrAdd(data.Id);
@@ -305,7 +533,7 @@ namespace Discord
break;
break;
case "GUILD_UPDATE":
case "GUILD_UPDATE":
{
{
var data = e.Payload.ToObject<GuildUpdateEvent>(_dataSocket Serializer);
var data = e.Payload.ToObject<GuildUpdateEvent>(_webSocket. Serializer);
var server = _servers[data.Id];
var server = _servers[data.Id];
if (server != null)
if (server != null)
{
{
@@ -316,7 +544,7 @@ namespace Discord
break;
break;
case "GUILD_DELETE":
case "GUILD_DELETE":
{
{
var data = e.Payload.ToObject<GuildDeleteEvent>(_dataSocket Serializer);
var data = e.Payload.ToObject<GuildDeleteEvent>(_webSocket. Serializer);
var server = _servers.TryRemove(data.Id);
var server = _servers.TryRemove(data.Id);
if (server != null)
if (server != null)
{
{
@@ -331,7 +559,7 @@ namespace Discord
//Channels
//Channels
case "CHANNEL_CREATE":
case "CHANNEL_CREATE":
{
{
var data = e.Payload.ToObject<ChannelCreateEvent>(_dataSocket Serializer);
var data = e.Payload.ToObject<ChannelCreateEvent>(_webSocket. Serializer);
Channel channel;
Channel channel;
if (data.IsPrivate)
if (data.IsPrivate)
{
{
@@ -347,7 +575,7 @@ namespace Discord
break;
break;
case "CHANNEL_UPDATE":
case "CHANNEL_UPDATE":
{
{
var data = e.Payload.ToObject<ChannelUpdateEvent>(_dataSocket Serializer);
var data = e.Payload.ToObject<ChannelUpdateEvent>(_webSocket. Serializer);
var channel = _channels[data.Id];
var channel = _channels[data.Id];
if (channel != null)
if (channel != null)
{
{
@@ -358,7 +586,7 @@ namespace Discord
break;
break;
case "CHANNEL_DELETE":
case "CHANNEL_DELETE":
{
{
var data = e.Payload.ToObject<ChannelDeleteEvent>(_dataSocket Serializer);
var data = e.Payload.ToObject<ChannelDeleteEvent>(_webSocket. Serializer);
var channel = _channels.TryRemove(data.Id);
var channel = _channels.TryRemove(data.Id);
if (channel != null)
if (channel != null)
RaiseChannelDestroyed(channel);
RaiseChannelDestroyed(channel);
@@ -368,7 +596,7 @@ namespace Discord
//Members
//Members
case "GUILD_MEMBER_ADD":
case "GUILD_MEMBER_ADD":
{
{
var data = e.Payload.ToObject<MemberAddEvent>(_dataSocket Serializer);
var data = e.Payload.ToObject<MemberAddEvent>(_webSocket. Serializer);
var user = _users.GetOrAdd(data.User.Id, data.GuildId);
var user = _users.GetOrAdd(data.User.Id, data.GuildId);
user.Update(data);
user.Update(data);
if (Config.TrackActivity)
if (Config.TrackActivity)
@@ -378,7 +606,7 @@ namespace Discord
break;
break;
case "GUILD_MEMBER_UPDATE":
case "GUILD_MEMBER_UPDATE":
{
{
var data = e.Payload.ToObject<MemberUpdateEvent>(_dataSocket Serializer);
var data = e.Payload.ToObject<MemberUpdateEvent>(_webSocket. Serializer);
var user = _users[data.User.Id, data.GuildId];
var user = _users[data.User.Id, data.GuildId];
if (user != null)
if (user != null)
{
{
@@ -389,7 +617,7 @@ namespace Discord
break;
break;
case "GUILD_MEMBER_REMOVE":
case "GUILD_MEMBER_REMOVE":
{
{
var data = e.Payload.ToObject<MemberRemoveEvent>(_dataSocket Serializer);
var data = e.Payload.ToObject<MemberRemoveEvent>(_webSocket. Serializer);
var user = _users.TryRemove(data.UserId, data.GuildId);
var user = _users.TryRemove(data.UserId, data.GuildId);
if (user != null)
if (user != null)
RaiseUserLeft(user);
RaiseUserLeft(user);
@@ -397,7 +625,7 @@ namespace Discord
break;
break;
case "GUILD_MEMBERS_CHUNK":
case "GUILD_MEMBERS_CHUNK":
{
{
var data = e.Payload.ToObject<MembersChunkEvent>(_dataSocket Serializer);
var data = e.Payload.ToObject<MembersChunkEvent>(_webSocket. Serializer);
foreach (var memberData in data.Members)
foreach (var memberData in data.Members)
{
{
var user = _users.GetOrAdd(memberData.User.Id, memberData.GuildId);
var user = _users.GetOrAdd(memberData.User.Id, memberData.GuildId);
@@ -410,7 +638,7 @@ namespace Discord
//Roles
//Roles
case "GUILD_ROLE_CREATE":
case "GUILD_ROLE_CREATE":
{
{
var data = e.Payload.ToObject<RoleCreateEvent>(_dataSocket Serializer);
var data = e.Payload.ToObject<RoleCreateEvent>(_webSocket. Serializer);
var role = _roles.GetOrAdd(data.Data.Id, data.GuildId);
var role = _roles.GetOrAdd(data.Data.Id, data.GuildId);
role.Update(data.Data);
role.Update(data.Data);
var server = _servers[data.GuildId];
var server = _servers[data.GuildId];
@@ -421,7 +649,7 @@ namespace Discord
break;
break;
case "GUILD_ROLE_UPDATE":
case "GUILD_ROLE_UPDATE":
{
{
var data = e.Payload.ToObject<RoleUpdateEvent>(_dataSocket Serializer);
var data = e.Payload.ToObject<RoleUpdateEvent>(_webSocket. Serializer);
var role = _roles[data.Data.Id];
var role = _roles[data.Data.Id];
if (role != null)
if (role != null)
{
{
@@ -432,7 +660,7 @@ namespace Discord
break;
break;
case "GUILD_ROLE_DELETE":
case "GUILD_ROLE_DELETE":
{
{
var data = e.Payload.ToObject<RoleDeleteEvent>(_dataSocket Serializer);
var data = e.Payload.ToObject<RoleDeleteEvent>(_webSocket. Serializer);
var role = _roles.TryRemove(data.RoleId);
var role = _roles.TryRemove(data.RoleId);
if (role != null)
if (role != null)
{
{
@@ -447,7 +675,7 @@ namespace Discord
//Bans
//Bans
case "GUILD_BAN_ADD":
case "GUILD_BAN_ADD":
{
{
var data = e.Payload.ToObject<BanAddEvent>(_dataSocket Serializer);
var data = e.Payload.ToObject<BanAddEvent>(_webSocket. Serializer);
var server = _servers[data.GuildId];
var server = _servers[data.GuildId];
if (server != null)
if (server != null)
{
{
@@ -459,7 +687,7 @@ namespace Discord
break;
break;
case "GUILD_BAN_REMOVE":
case "GUILD_BAN_REMOVE":
{
{
var data = e.Payload.ToObject<BanRemoveEvent>(_dataSocket Serializer);
var data = e.Payload.ToObject<BanRemoveEvent>(_webSocket. Serializer);
var server = _servers[data.GuildId];
var server = _servers[data.GuildId];
if (server != null)
if (server != null)
{
{
@@ -473,7 +701,7 @@ namespace Discord
//Messages
//Messages
case "MESSAGE_CREATE":
case "MESSAGE_CREATE":
{
{
var data = e.Payload.ToObject<MessageCreateEvent>(_dataSocket Serializer);
var data = e.Payload.ToObject<MessageCreateEvent>(_webSocket. Serializer);
Message msg = null;
Message msg = null;
bool isAuthor = data.Author.Id == _userId;
bool isAuthor = data.Author.Id == _userId;
@@ -500,7 +728,7 @@ namespace Discord
break;
break;
case "MESSAGE_UPDATE":
case "MESSAGE_UPDATE":
{
{
var data = e.Payload.ToObject<MessageUpdateEvent>(_dataSocket Serializer);
var data = e.Payload.ToObject<MessageUpdateEvent>(_webSocket. Serializer);
var msg = _messages[data.Id];
var msg = _messages[data.Id];
if (msg != null)
if (msg != null)
{
{
@@ -511,7 +739,7 @@ namespace Discord
break;
break;
case "MESSAGE_DELETE":
case "MESSAGE_DELETE":
{
{
var data = e.Payload.ToObject<MessageDeleteEvent>(_dataSocket Serializer);
var data = e.Payload.ToObject<MessageDeleteEvent>(_webSocket. Serializer);
var msg = _messages.TryRemove(data.Id);
var msg = _messages.TryRemove(data.Id);
if (msg != null)
if (msg != null)
RaiseMessageDeleted(msg);
RaiseMessageDeleted(msg);
@@ -519,7 +747,7 @@ namespace Discord
break;
break;
case "MESSAGE_ACK":
case "MESSAGE_ACK":
{
{
var data = e.Payload.ToObject<MessageAckEvent>(_dataSocket Serializer);
var data = e.Payload.ToObject<MessageAckEvent>(_webSocket. Serializer);
var msg = GetMessage(data.MessageId);
var msg = GetMessage(data.MessageId);
if (msg != null)
if (msg != null)
RaiseMessageReadRemotely(msg);
RaiseMessageReadRemotely(msg);
@@ -529,7 +757,7 @@ namespace Discord
//Statuses
//Statuses
case "PRESENCE_UPDATE":
case "PRESENCE_UPDATE":
{
{
var data = e.Payload.ToObject<PresenceUpdateEvent>(_dataSocket Serializer);
var data = e.Payload.ToObject<PresenceUpdateEvent>(_webSocket. Serializer);
var user = _users.GetOrAdd(data.User.Id, data.GuildId);
var user = _users.GetOrAdd(data.User.Id, data.GuildId);
if (user != null)
if (user != null)
{
{
@@ -540,7 +768,7 @@ namespace Discord
break;
break;
case "TYPING_START":
case "TYPING_START":
{
{
var data = e.Payload.ToObject<TypingStartEvent>(_dataSocket Serializer);
var data = e.Payload.ToObject<TypingStartEvent>(_webSocket. Serializer);
var channel = _channels[data.ChannelId];
var channel = _channels[data.ChannelId];
if (channel != null)
if (channel != null)
{
{
@@ -566,7 +794,7 @@ namespace Discord
//Voice
//Voice
case "VOICE_STATE_UPDATE":
case "VOICE_STATE_UPDATE":
{
{
var data = e.Payload.ToObject<MemberVoiceStateUpdateEvent>(_dataSocket Serializer);
var data = e.Payload.ToObject<MemberVoiceStateUpdateEvent>(_webSocket. Serializer);
var user = _users[data.UserId, data.GuildId];
var user = _users[data.UserId, data.GuildId];
if (user != null)
if (user != null)
{
{
@@ -585,7 +813,7 @@ namespace Discord
//Settings
//Settings
case "USER_UPDATE":
case "USER_UPDATE":
{
{
var data = e.Payload.ToObject<UserUpdateEvent>(_dataSocket Serializer);
var data = e.Payload.ToObject<UserUpdateEvent>(_webSocket. Serializer);
var user = _globalUsers[data.Id];
var user = _globalUsers[data.Id];
if (user != null)
if (user != null)
{
{
@@ -598,35 +826,61 @@ namespace Discord
//Ignored
//Ignored
case "USER_SETTINGS_UPDATE":
case "USER_SETTINGS_UPDATE":
case "GUILD_INTEGRATIONS_UPDATE":
case "GUILD_INTEGRATIONS_UPDATE":
break;
//Internal (handled in DataWebSocket)
case "RESUMED":
break;
//Pass to DiscordWSClient
case "VOICE_SERVER_UPDATE":
case "VOICE_SERVER_UPDATE":
await base.OnReceivedEvent(e).ConfigureAwait(false);
break;
case "RESUMED": //Handled in DataWebSocket
break;
break;
//Others
//Others
default:
default:
RaiseOnLog(LogMessageSeverity.Warning, LogMessageSource.DataWebSocket , $"Unknown message type: {e.Type}");
_webSocket.Logger.Log(LogSeverity.Warning , $"Unknown message type: {e.Type}");
break;
break;
}
}
}
}
catch (Exception ex)
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()
private void SendInitialLog()
{
{
if (_config.LogLevel >= LogMessage Severity.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;
_sentInitialLog = true;
}
}
//Helpers
/// <summary> Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications. </summary>
public void Run(Func<Task> asyncAction)
{
try
{
asyncAction().GetAwaiter().GetResult(); //Avoids creating AggregateExceptions
}
catch (TaskCanceledException) { }
_disconnectedEvent.WaitOne();
}
/// <summary> Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications. </summary>
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)
public void GetCacheStats(out int serverCount, out int channelCount, out int userCount, out int uniqueUserCount, out int messageCount, out int roleCount)
{
{