Browse Source

Moved logging to its own service, more audio isolation prep

tags/docs-0.9
RogueException 9 years ago
parent
commit
74c0bb5b51
29 changed files with 630 additions and 794 deletions
  1. +1
    -1
      src/Discord.Net/API/Converters/LongCollectionConverter.cs
  2. +2
    -2
      src/Discord.Net/API/Converters/LongStringConverter.cs
  3. +0
    -11
      src/Discord.Net/API/Converters/StringEnumConverter.cs
  4. +6
    -3
      src/Discord.Net/API/WebSockets.cs
  5. +14
    -5
      src/Discord.Net/DiscordAPIClientConfig.cs
  6. +3
    -3
      src/Discord.Net/DiscordClient.Channels.cs
  7. +5
    -5
      src/Discord.Net/DiscordClient.Messages.cs
  8. +3
    -3
      src/Discord.Net/DiscordClient.Roles.cs
  9. +5
    -5
      src/Discord.Net/DiscordClient.Servers.cs
  10. +13
    -13
      src/Discord.Net/DiscordClient.Users.cs
  11. +0
    -81
      src/Discord.Net/DiscordClient.Voice.cs
  12. +395
    -141
      src/Discord.Net/DiscordClient.cs
  13. +22
    -5
      src/Discord.Net/DiscordClientConfig.cs
  14. +0
    -89
      src/Discord.Net/DiscordWSClient.Events.cs
  15. +0
    -306
      src/Discord.Net/DiscordWSClient.cs
  16. +0
    -33
      src/Discord.Net/DiscordWSClientConfig.cs
  17. +2
    -2
      src/Discord.Net/Helpers/Mention.cs
  18. +0
    -0
      src/Discord.Net/Helpers/Shared/EpochTime.cs
  19. +1
    -1
      src/Discord.Net/Helpers/Shared/TaskExtensions.cs
  20. +61
    -0
      src/Discord.Net/LogService.cs
  21. +1
    -1
      src/Discord.Net/Models/Message.cs
  22. +1
    -1
      src/Discord.Net/Models/Server.cs
  23. +2
    -2
      src/Discord.Net/Models/User.cs
  24. +6
    -6
      src/Discord.Net/Net/Rest/RestClient.cs
  25. +10
    -10
      src/Discord.Net/Net/WebSockets/DataWebSocket.cs
  26. +0
    -27
      src/Discord.Net/Net/WebSockets/WebSocket.Events.cs
  27. +70
    -33
      src/Discord.Net/Net/WebSockets/WebSocket.cs
  28. +6
    -4
      src/Discord.Net/Net/WebSockets/WebSocketSharpEngine.cs
  29. +1
    -1
      src/Discord.Net/TimeoutException.cs

+ 1
- 1
src/Discord.Net/API/Converters/LongCollectionConverter.cs View File

@@ -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)
{


+ 2
- 2
src/Discord.Net/API/Converters/LongStringConverter.cs View File

@@ -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)
{


+ 0
- 11
src/Discord.Net/API/Converters/StringEnumConverter.cs View File

@@ -1,11 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Discord.API.Converters
{
public class StringEnumConverter
{
}
}

+ 6
- 3
src/Discord.Net/API/WebSockets.cs View File

@@ -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<T> : WebSocketMessage
public abstract class WebSocketMessage<T> : 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


+ 14
- 5
src/Discord.Net/DiscordAPIClientConfig.cs View File

@@ -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
{
/// <summary> 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. </summary>
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;
/// <summary> Max time (in milliseconds) to wait for an API request to complete. </summary>
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; }


+ 3
- 3
src/Discord.Net/DiscordClient.Channels.cs View File

@@ -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<ChannelEventArgs> 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<ChannelEventArgs> 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)));
}

/// <summary> Returns a collection of all servers this client is a member of. </summary>


+ 5
- 5
src/Discord.Net/DiscordClient.Messages.cs View File

@@ -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<MessageEventArgs> 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<MessageEventArgs> 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<MessageEventArgs> 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<MessageEventArgs> 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;


+ 3
- 3
src/Discord.Net/DiscordClient.Roles.cs View File

@@ -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<RoleEventArgs> 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<RoleEventArgs> 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;


+ 5
- 5
src/Discord.Net/DiscordClient.Servers.cs View File

@@ -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<ServerEventArgs> 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<ServerEventArgs> 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<ServerEventArgs> 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<ServerEventArgs> 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)));
}

/// <summary> Returns a collection of all servers this client is a member of. </summary>


+ 13
- 13
src/Discord.Net/DiscordClient.Users.cs View File

@@ -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<UserEventArgs> 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<UserEventArgs> 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<UserEventArgs> 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<UserEventArgs> 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<UserChannelEventArgs> 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<BanEventArgs> 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<BanEventArgs> 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)));
}

/// <summary> Returns the current logged-in user in a private channel. </summary>
/// <summary> Returns the current logged-in user used in private channels. </summary>
internal User PrivateUser => _privateUser;
private User _privateUser;

/// <summary> Returns information about the currently logged-in account. </summary>
public GlobalUser CurrentUser { get { CheckReady(); return _privateUser.Global; } }
public GlobalUser CurrentUser => _privateUser?.Global;

/// <summary> Returns a collection of all unique users this client can currently see. </summary>
public IEnumerable<GlobalUser> 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;
}
}

+ 0
- 81
src/Discord.Net/DiscordClient.Voice.cs View File

@@ -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<IDiscordVoiceClient> 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<IDiscordVoiceClient> 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);
}
}
}
}

+ 395
- 141
src/Discord.Net/DiscordClient.cs View File

@@ -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;
}
}

/// <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 JsonSerializer _messageImporter;
private readonly ConcurrentQueue<Message> _pendingMessages;
private readonly Dictionary<Type, object> _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;

/// <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>
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>
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<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>();
_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) =>
{
_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<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>
/// <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)
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;
}
/// <summary> Connects to the Discord server with the provided token. </summary>
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()
/// <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)
{
Message ignored;
@@ -247,16 +484,8 @@ namespace Discord
public T GetService<T>(bool required = true)
where T : class, IService
=> GetSingleton<T>(required);

protected override IEnumerable<Task> 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<ReadyEvent>(_dataSocketSerializer);
var data = e.Payload.ToObject<ReadyEvent>(_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<GuildCreateEvent>(_dataSocketSerializer);
var data = e.Payload.ToObject<GuildCreateEvent>(_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<GuildUpdateEvent>(_dataSocketSerializer);
var data = e.Payload.ToObject<GuildUpdateEvent>(_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<GuildDeleteEvent>(_dataSocketSerializer);
var data = e.Payload.ToObject<GuildDeleteEvent>(_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<ChannelCreateEvent>(_dataSocketSerializer);
var data = e.Payload.ToObject<ChannelCreateEvent>(_webSocket.Serializer);
Channel channel;
if (data.IsPrivate)
{
@@ -347,7 +575,7 @@ namespace Discord
break;
case "CHANNEL_UPDATE":
{
var data = e.Payload.ToObject<ChannelUpdateEvent>(_dataSocketSerializer);
var data = e.Payload.ToObject<ChannelUpdateEvent>(_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<ChannelDeleteEvent>(_dataSocketSerializer);
var data = e.Payload.ToObject<ChannelDeleteEvent>(_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<MemberAddEvent>(_dataSocketSerializer);
var data = e.Payload.ToObject<MemberAddEvent>(_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<MemberUpdateEvent>(_dataSocketSerializer);
var data = e.Payload.ToObject<MemberUpdateEvent>(_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<MemberRemoveEvent>(_dataSocketSerializer);
var data = e.Payload.ToObject<MemberRemoveEvent>(_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<MembersChunkEvent>(_dataSocketSerializer);
var data = e.Payload.ToObject<MembersChunkEvent>(_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<RoleCreateEvent>(_dataSocketSerializer);
var data = e.Payload.ToObject<RoleCreateEvent>(_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<RoleUpdateEvent>(_dataSocketSerializer);
var data = e.Payload.ToObject<RoleUpdateEvent>(_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<RoleDeleteEvent>(_dataSocketSerializer);
var data = e.Payload.ToObject<RoleDeleteEvent>(_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<BanAddEvent>(_dataSocketSerializer);
var data = e.Payload.ToObject<BanAddEvent>(_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<BanRemoveEvent>(_dataSocketSerializer);
var data = e.Payload.ToObject<BanRemoveEvent>(_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<MessageCreateEvent>(_dataSocketSerializer);
var data = e.Payload.ToObject<MessageCreateEvent>(_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<MessageUpdateEvent>(_dataSocketSerializer);
var data = e.Payload.ToObject<MessageUpdateEvent>(_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<MessageDeleteEvent>(_dataSocketSerializer);
var data = e.Payload.ToObject<MessageDeleteEvent>(_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<MessageAckEvent>(_dataSocketSerializer);
var data = e.Payload.ToObject<MessageAckEvent>(_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<PresenceUpdateEvent>(_dataSocketSerializer);
var data = e.Payload.ToObject<PresenceUpdateEvent>(_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<TypingStartEvent>(_dataSocketSerializer);
var data = e.Payload.ToObject<TypingStartEvent>(_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<MemberVoiceStateUpdateEvent>(_dataSocketSerializer);
var data = e.Payload.ToObject<MemberVoiceStateUpdateEvent>(_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<UserUpdateEvent>(_dataSocketSerializer);
var data = e.Payload.ToObject<UserUpdateEvent>(_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
/// <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)
{


+ 22
- 5
src/Discord.Net/DiscordClientConfig.cs View File

@@ -1,18 +1,35 @@
namespace Discord
{
public class DiscordClientConfig : DiscordWSClientConfig
{
/// <summary> Gets or sets the time (in milliseconds) to wait when the message queue is empty before checking again. </summary>
public int MessageQueueInterval { get { return _messageQueueInterval; } set { SetValue(ref _messageQueueInterval, value); } }
private int _messageQueueInterval = 100;
public class DiscordClientConfig : DiscordAPIClientConfig
{
/// <summary> Max time in milliseconds to wait for DiscordClient to connect and initialize. </summary>
public int ConnectionTimeout { get { return _connectionTimeout; } set { SetValue(ref _connectionTimeout, value); } }
private int _connectionTimeout = 30000;
/// <summary> Gets or sets the time (in milliseconds) to wait after an unexpected disconnect before reconnecting. </summary>
public int ReconnectDelay { get { return _reconnectDelay; } set { SetValue(ref _reconnectDelay, value); } }
private int _reconnectDelay = 1000;
/// <summary> Gets or sets the time (in milliseconds) to wait after an reconnect fails before retrying. </summary>
public int FailedReconnectDelay { get { return _failedReconnectDelay; } set { SetValue(ref _failedReconnectDelay, value); } }
private int _failedReconnectDelay = 10000;
/// <summary> Gets or sets the time (in milliseconds) to wait when the websocket's message queue is empty before checking again. </summary>
public int WebSocketInterval { get { return _webSocketInterval; } set { SetValue(ref _webSocketInterval, value); } }
private int _webSocketInterval = 100;
/// <summary> Gets or sets the number of messages per channel that should be kept in cache. Setting this to zero disables the message cache entirely. </summary>
public int MessageCacheLength { get { return _messageCacheLength; } set { SetValue(ref _messageCacheLength, value); } }
private int _messageCacheLength = 100;

//Experimental Features
/// <summary> (Experimental) Instructs Discord to not send send information about offline users, for servers with more than 50 users. </summary>
public bool UseLargeThreshold { get { return _useLargeThreshold; } set { SetValue(ref _useLargeThreshold, value); } }
private bool _useLargeThreshold = false;

//Experimental Features
/// <summary> (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. </summary>
public bool UseMessageQueue { get { return _useMessageQueue; } set { SetValue(ref _useMessageQueue, value); } }
private bool _useMessageQueue = false;
/// <summary> Gets or sets the time (in milliseconds) to wait when the message queue is empty before checking again. </summary>
public int MessageQueueInterval { get { return _messageQueueInterval; } set { SetValue(ref _messageQueueInterval, value); } }
private int _messageQueueInterval = 100;
/// <summary> (Experimental) Maintains the LastActivity property for users, showing when they last made an action (sent message, joined server, typed, etc). </summary>
public bool TrackActivity { get { return _trackActivity; } set { SetValue(ref _trackActivity, value); } }
private bool _trackActivity = true;


+ 0
- 89
src/Discord.Net/DiscordWSClient.Events.cs View File

@@ -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<DisconnectedEventArgs> Disconnected;
private void RaiseDisconnected(DisconnectedEventArgs e)
{
if (Disconnected != null)
RaiseEvent(nameof(Disconnected), () => Disconnected(this, e));
}
public event EventHandler<LogMessageEventArgs> 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)));
}
}
}

+ 0
- 306
src/Discord.Net/DiscordWSClient.cs View File

@@ -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
}

/// <summary> Provides a minimalistic websocket connection to the Discord service. </summary>
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;

/// <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 DiscordWSClientConfig Config => _config;

/// <summary> Returns the current connection state of this client. </summary>
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;

/// <summary> Initializes a new instance of the DiscordClient class. </summary>
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<string> 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();
}

/// <summary> Disconnects from the Discord server, canceling any pending requests. </summary>
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<Task> GetTasks()
{
return new Task[] { _cancelToken.Wait() };
}

protected virtual async Task Cleanup()
{
await _dataSocket.Disconnect().ConfigureAwait(false);

_userId = null;
_gateway = null;
_token = null;
}

//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();
}

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<string>("id"));
break;
}
}
catch (Exception ex)
{
RaiseOnLog(LogMessageSeverity.Error, LogMessageSource.Client, $"Error handling {e.Type} event: {ex.GetBaseException().Message}");
}
return TaskHelper.CompletedTask;
}
}
}

+ 0
- 33
src/Discord.Net/DiscordWSClientConfig.cs View File

@@ -1,33 +0,0 @@
using System;
using System.Reflection;

namespace Discord
{
public class DiscordWSClientConfig : DiscordAPIClientConfig
{
/// <summary> Max time in milliseconds to wait for DiscordClient to connect and initialize. </summary>
public int ConnectionTimeout { get { return _connectionTimeout; } set { SetValue(ref _connectionTimeout, value); } }
private int _connectionTimeout = 30000;
/// <summary> Gets or sets the time (in milliseconds) to wait after an unexpected disconnect before reconnecting. </summary>
public int ReconnectDelay { get { return _reconnectDelay; } set { SetValue(ref _reconnectDelay, value); } }
private int _reconnectDelay = 1000;
/// <summary> Gets or sets the time (in milliseconds) to wait after an reconnect fails before retrying. </summary>
public int FailedReconnectDelay { get { return _failedReconnectDelay; } set { SetValue(ref _failedReconnectDelay, value); } }
private int _failedReconnectDelay = 10000;
/// <summary> Gets or sets the time (in milliseconds) to wait when the websocket's message queue is empty before checking again. </summary>
public int WebSocketInterval { get { return _webSocketInterval; } set { SetValue(ref _webSocketInterval, value); } }
private int _webSocketInterval = 100;

//Experimental Features
/// <summary> (Experimental) Instructs Discord to not send send information about offline users, for servers with more than 50 users. </summary>
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;
}
}
}

+ 2
- 2
src/Discord.Net/Helpers/Mention.cs View File

@@ -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);
/// <summary> Returns the string used to create a user mention. </summary>


src/Discord.Net/Helpers/EpochTime.cs → src/Discord.Net/Helpers/Shared/EpochTime.cs View File


+ 1
- 1
src/Discord.Net/Helpers/Shared/TaskExtensions.cs View File

@@ -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)
{


+ 61
- 0
src/Discord.Net/LogService.cs View File

@@ -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<LogMessageEventArgs> 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));
}
}
}

+ 1
- 1
src/Discord.Net/Models/Message.cs View File

@@ -94,7 +94,7 @@ namespace Discord
/// <remarks> This is not set to true if the user was mentioned with @everyone (see IsMentioningEverone). </remarks>
public bool IsMentioningMe { get; private set; }
/// <summary> Returns true if the current user created this message. </summary>
public bool IsAuthor => _client.CurrentUserId == _user.Id;
public bool IsAuthor => _client.CurrentUser.Id == _user.Id;
/// <summary> Returns true if the message was sent as text-to-speech by someone with permissions to do so. </summary>
public bool IsTTS { get; private set; }
/// <summary> Returns the state of this message. Only useful if UseMessageQueue is true. </summary>


+ 1
- 1
src/Discord.Net/Models/Server.cs View File

@@ -39,7 +39,7 @@ namespace Discord
public string IconUrl => IconId != null ? Endpoints.ServerIcon(Id, IconId) : null;

/// <summary> Returns true if the current user created this server. </summary>
public bool IsOwner => _client.CurrentUserId == _owner.Id;
public bool IsOwner => _client.CurrentUser.Id == _owner.Id;

/// <summary> Returns the user that first created this server. </summary>
[JsonIgnore]


+ 2
- 2
src/Discord.Net/Models/User.cs View File

@@ -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<Channel>(x => _client.Channels[x]);


+ 6
- 6
src/Discord.Net/Net/Rest/RestClient.cs View File

@@ -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);


+ 10
- 10
src/Discord.Net/Net/WebSockets/DataWebSocket.cs View File

@@ -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<ReadyEvent>(_client.DataSocketSerializer);
var payload = token.ToObject<ReadyEvent>(_serializer);
_sessionId = payload.SessionId;
_heartbeatInterval = payload.HeartbeatInterval;
}
else if (msg.Type == "RESUMED")
{
var payload = token.ToObject<ResumedEvent>(_client.DataSocketSerializer);
var payload = token.ToObject<ResumedEvent>(_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<RedirectEvent>(_client.DataSocketSerializer);
var payload = (msg.Payload as JToken).ToObject<RedirectEvent>(_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;
}
}


+ 0
- 27
src/Discord.Net/Net/WebSockets/WebSocket.Events.cs View File

@@ -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<DisconnectedEventArgs> Disconnected;
private void RaiseDisconnected(bool wasUnexpected, Exception error)
{
if (Disconnected != null)
Disconnected(this, new DisconnectedEventArgs(wasUnexpected, error));
}

public event EventHandler<LogMessageEventArgs> LogMessage;
internal void RaiseOnLog(LogMessageSeverity severity, string message, Exception exception = null)
{
if (LogMessage != null)
LogMessage(this, new LogMessageEventArgs(severity, LogMessageSource.Unknown, message, exception));
}
}
}

+ 70
- 33
src/Discord.Net/Net/WebSockets/WebSocket.cs View File

@@ -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<DisconnectedEventArgs> 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<Task> 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);
}



+ 6
- 4
src/Discord.Net/Net/WebSockets/WebSocketSharpEngine.cs View File

@@ -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<string> _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<string>();
}

@@ -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;
}


+ 1
- 1
src/Discord.Net/TimeoutException.cs View File

@@ -4,7 +4,7 @@ namespace Discord
{
public sealed class TimeoutException : OperationCanceledException
{
internal TimeoutException()
public TimeoutException()
: base("An operation has timed out.")
{
}


Loading…
Cancel
Save