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 namespace Discord.API.Converters
{ {
internal class EnumerableLongStringConverter : JsonConverter
public class EnumerableLongStringConverter : JsonConverter
{ {
public override bool CanConvert(Type objectType) 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 namespace Discord.API.Converters
{ {
internal class LongStringConverter : JsonConverter
public class LongStringConverter : JsonConverter
{ {
public override bool CanConvert(Type objectType) 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) 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 //Common
public class WebSocketMessage public class WebSocketMessage
{ {
public WebSocketMessage() { }
public WebSocketMessage(int op) { Operation = op; }

[JsonProperty("op")] [JsonProperty("op")]
public int Operation; public int Operation;
[JsonProperty("d")] [JsonProperty("d")]
@@ -20,12 +23,12 @@ namespace Discord.API
[JsonProperty("s", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("s", NullValueHandling = NullValueHandling.Ignore)]
public int? Sequence; public int? Sequence;
} }
internal abstract class WebSocketMessage<T> : WebSocketMessage
public abstract class WebSocketMessage<T> : WebSocketMessage
where T : new() where T : new()
{ {
public WebSocketMessage() { Payload = new T(); } 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] [JsonIgnore]
public new T Payload public new T Payload


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

@@ -1,16 +1,22 @@
using System; using System;
using System.Net; using System.Net;
using System.Reflection;


namespace Discord 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> /// <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> /// <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); } } 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); } } public NetworkCredential ProxyCredentials { get { return _proxyCredentials; } set { SetValue(ref _proxyCredentials, value); } }
private NetworkCredential _proxyCredentials = null; private NetworkCredential _proxyCredentials = null;


//Internals
internal static readonly string UserAgent = $"Discord.Net/{DiscordClient.Version} (https://github.com/RogueException/Discord.Net)";

//Lock //Lock
protected bool _isLocked; protected bool _isLocked;
internal void Lock() { _isLocked = true; } 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) private void RaiseChannelCreated(Channel channel)
{ {
if (ChannelCreated != null) 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; public event EventHandler<ChannelEventArgs> ChannelDestroyed;
private void RaiseChannelDestroyed(Channel channel) private void RaiseChannelDestroyed(Channel channel)
{ {
if (ChannelDestroyed != null) 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; public event EventHandler<ChannelEventArgs> ChannelUpdated;
private void RaiseChannelUpdated(Channel channel) private void RaiseChannelUpdated(Channel channel)
{ {
if (ChannelUpdated != null) 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> /// <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) private void RaiseMessageReceived(Message msg)
{ {
if (MessageReceived != null) 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; public event EventHandler<MessageEventArgs> MessageSent;
private void RaiseMessageSent(Message msg) private void RaiseMessageSent(Message msg)
{ {
if (MessageSent != null) 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; public event EventHandler<MessageEventArgs> MessageDeleted;
private void RaiseMessageDeleted(Message msg) private void RaiseMessageDeleted(Message msg)
{ {
if (MessageDeleted != null) 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; public event EventHandler<MessageEventArgs> MessageUpdated;
private void RaiseMessageUpdated(Message msg) private void RaiseMessageUpdated(Message msg)
{ {
if (MessageUpdated != null) 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; public event EventHandler<MessageEventArgs> MessageReadRemotely;
private void RaiseMessageReadRemotely(Message msg) private void RaiseMessageReadRemotely(Message msg)
{ {
if (MessageReadRemotely != null) 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; 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) private void RaiseRoleCreated(Role role)
{ {
if (RoleCreated != null) 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; public event EventHandler<RoleEventArgs> RoleUpdated;
private void RaiseRoleDeleted(Role role) private void RaiseRoleDeleted(Role role)
{ {
if (RoleDeleted != null) 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; public event EventHandler<RoleEventArgs> RoleDeleted;
private void RaiseRoleUpdated(Role role) private void RaiseRoleUpdated(Role role)
{ {
if (RoleUpdated != null) 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; 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) private void RaiseJoinedServer(Server server)
{ {
if (JoinedServer != null) 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; public event EventHandler<ServerEventArgs> LeftServer;
private void RaiseLeftServer(Server server) private void RaiseLeftServer(Server server)
{ {
if (LeftServer != null) 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; public event EventHandler<ServerEventArgs> ServerUpdated;
private void RaiseServerUpdated(Server server) private void RaiseServerUpdated(Server server)
{ {
if (ServerUpdated != null) 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; public event EventHandler<ServerEventArgs> ServerUnavailable;
private void RaiseServerUnavailable(Server server) private void RaiseServerUnavailable(Server server)
{ {
if (ServerUnavailable != null) 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; public event EventHandler<ServerEventArgs> ServerAvailable;
private void RaiseServerAvailable(Server server) private void RaiseServerAvailable(Server server)
{ {
if (ServerAvailable != null) 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> /// <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) private void RaiseUserJoined(User user)
{ {
if (UserJoined != null) 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; public event EventHandler<UserEventArgs> UserLeft;
private void RaiseUserLeft(User user) private void RaiseUserLeft(User user)
{ {
if (UserLeft != null) 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; public event EventHandler<UserEventArgs> UserUpdated;
private void RaiseUserUpdated(User user) private void RaiseUserUpdated(User user)
{ {
if (UserUpdated != null) 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; public event EventHandler<UserEventArgs> UserPresenceUpdated;
private void RaiseUserPresenceUpdated(User user) private void RaiseUserPresenceUpdated(User user)
{ {
if (UserPresenceUpdated != null) 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; public event EventHandler<UserEventArgs> UserVoiceStateUpdated;
private void RaiseUserVoiceStateUpdated(User user) private void RaiseUserVoiceStateUpdated(User user)
{ {
if (UserVoiceStateUpdated != null) 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; public event EventHandler<UserChannelEventArgs> UserIsTypingUpdated;
private void RaiseUserIsTyping(User user, Channel channel) private void RaiseUserIsTyping(User user, Channel channel)
{ {
if (UserIsTypingUpdated != null) 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; public event EventHandler ProfileUpdated;
private void RaiseProfileUpdated() private void RaiseProfileUpdated()
{ {
if (ProfileUpdated != null) if (ProfileUpdated != null)
RaiseEvent(nameof(ProfileUpdated), () => ProfileUpdated(this, EventArgs.Empty));
EventHelper.Raise(_logger, nameof(ProfileUpdated), () => ProfileUpdated(this, EventArgs.Empty));
} }
public event EventHandler<BanEventArgs> UserBanned; public event EventHandler<BanEventArgs> UserBanned;
private void RaiseUserBanned(long userId, Server server) private void RaiseUserBanned(long userId, Server server)
{ {
if (UserBanned != null) 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; public event EventHandler<BanEventArgs> UserUnbanned;
private void RaiseUserUnbanned(long userId, Server server) private void RaiseUserUnbanned(long userId, Server server)
{ {
if (UserUnbanned != null) 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; internal User PrivateUser => _privateUser;
private User _privateUser; private User _privateUser;


/// <summary> Returns information about the currently logged-in account. </summary> /// <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> /// <summary> Returns a collection of all unique users this client can currently see. </summary>
public IEnumerable<GlobalUser> AllUsers { get { CheckReady(); return _globalUsers; } } public IEnumerable<GlobalUser> AllUsers { get { CheckReady(); return _globalUsers; } }
@@ -272,7 +272,7 @@ namespace Discord
{ {
if (server == null) throw new ArgumentNullException(nameof(server)); if (server == null) throw new ArgumentNullException(nameof(server));


_dataSocket.SendRequestUsers(server.Id);
_webSocket.SendRequestUsers(server.Id);
} }


public async Task EditProfile(string currentPassword = "", public async Task EditProfile(string currentPassword = "",
@@ -312,7 +312,7 @@ namespace Discord
} }
private Task SendStatus() 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; 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.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 >= 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}"); $"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 override 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>(_dataSocketSerializer);
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>(_dataSocketSerializer);
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>(_dataSocketSerializer);
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>(_dataSocketSerializer);
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>(_dataSocketSerializer);
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>(_dataSocketSerializer);
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>(_dataSocketSerializer);
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>(_dataSocketSerializer);
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>(_dataSocketSerializer);
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>(_dataSocketSerializer);
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>(_dataSocketSerializer);
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>(_dataSocketSerializer);
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>(_dataSocketSerializer);
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>(_dataSocketSerializer);
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>(_dataSocketSerializer);
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>(_dataSocketSerializer);
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>(_dataSocketSerializer);
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>(_dataSocketSerializer);
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>(_dataSocketSerializer);
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>(_dataSocketSerializer);
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>(_dataSocketSerializer);
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>(_dataSocketSerializer);
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>(_dataSocketSerializer);
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 >= 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; _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)
{ {


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

@@ -1,18 +1,35 @@
namespace Discord 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> /// <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); } } public int MessageCacheLength { get { return _messageCacheLength; } set { SetValue(ref _messageCacheLength, value); } }
private int _messageCacheLength = 100; 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 //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> /// <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); } } public bool UseMessageQueue { get { return _useMessageQueue; } set { SetValue(ref _useMessageQueue, value); } }
private bool _useMessageQueue = false; 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> /// <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); } } public bool TrackActivity { get { return _trackActivity; } set { SetValue(ref _trackActivity, value); } }
private bool _trackActivity = true; 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 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); private static readonly Regex _roleRegex = new Regex(@"@everyone", RegexOptions.Compiled);
/// <summary> Returns the string used to create a user mention. </summary> /// <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 namespace Discord
{ {
public static class TaskExtensions
internal static class TaskExtensions
{ {
public static async Task Timeout(this Task task, int milliseconds) 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> /// <remarks> This is not set to true if the user was mentioned with @everyone (see IsMentioningEverone). </remarks>
public bool IsMentioningMe { get; private set; } public bool IsMentioningMe { get; private set; }
/// <summary> Returns true if the current user created this message. </summary> /// <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> /// <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; } public bool IsTTS { get; private set; }
/// <summary> Returns the state of this message. Only useful if UseMessageQueue is true. </summary> /// <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; public string IconUrl => IconId != null ? Endpoints.ServerIcon(Id, IconId) : null;


/// <summary> Returns true if the current user created this server. </summary> /// <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> /// <summary> Returns the user that first created this server. </summary>
[JsonIgnore] [JsonIgnore]


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

@@ -131,13 +131,13 @@ namespace Discord
x => x =>
{ {
x.AddMember(this); x.AddMember(this);
if (Id == _client.CurrentUserId)
if (Id == _client.CurrentUser.Id)
x.CurrentUser = this; x.CurrentUser = this;
}, },
x => x =>
{ {
x.RemoveMember(this); x.RemoveMember(this);
if (Id == _client.CurrentUserId)
if (Id == _client.CurrentUser.Id)
x.CurrentUser = null; x.CurrentUser = null;
}); });
_voiceChannel = new Reference<Channel>(x => _client.Channels[x]); _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) if (content != null)
requestJson = JsonConvert.SerializeObject(content); requestJson = JsonConvert.SerializeObject(content);


if (_config.LogLevel >= LogMessageSeverity.Verbose)
if (_config.LogLevel >= LogSeverity.Verbose)
stopwatch = Stopwatch.StartNew(); stopwatch = Stopwatch.StartNew();
string responseJson = await _engine.Send(method, path, requestJson, _cancelToken).ConfigureAwait(false); 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."); throw new Exception("API check failed: Response is not empty.");
#endif #endif


if (_config.LogLevel >= LogMessageSeverity.Verbose)
if (_config.LogLevel >= LogSeverity.Verbose)
{ {
stopwatch.Stop(); stopwatch.Stop();
if (content != null && _config.LogLevel >= LogMessageSeverity.Debug)
if (content != null && _config.LogLevel >= LogSeverity.Debug)
{ {
if (path.StartsWith(Endpoints.Auth)) if (path.StartsWith(Endpoints.Auth))
RaiseOnRequest(method, path, "[Hidden]", stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond); RaiseOnRequest(method, path, "[Hidden]", stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond);
@@ -130,7 +130,7 @@ namespace Discord.Net.Rest
{ {
Stopwatch stopwatch = null; Stopwatch stopwatch = null;


if (_config.LogLevel >= LogMessageSeverity.Verbose)
if (_config.LogLevel >= LogSeverity.Verbose)
stopwatch = Stopwatch.StartNew(); stopwatch = Stopwatch.StartNew();
string responseJson = await _engine.SendFile(method, path, filename, stream, _cancelToken).ConfigureAwait(false); 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."); throw new Exception("API check failed: Response is not empty.");
#endif #endif


if (_config.LogLevel >= LogMessageSeverity.Verbose)
if (_config.LogLevel >= LogSeverity.Verbose)
{ {
stopwatch.Stop(); stopwatch.Stop();
if (_config.LogLevel >= LogMessageSeverity.Debug)
if (_config.LogLevel >= LogSeverity.Debug)
RaiseOnRequest(method, path, filename, stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond); RaiseOnRequest(method, path, filename, stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond);
else else
RaiseOnRequest(method, path, null, stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond); 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; public string SessionId => _sessionId;
private string _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 (OperationCanceledException) { throw; }
catch (Exception ex) 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() //Net is down? We can keep trying to reconnect until the user runs Disconnect()
await Task.Delay(_client.Config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); await Task.Delay(_client.Config.FailedReconnectDelay, cancelToken).ConfigureAwait(false);
} }
@@ -96,13 +96,13 @@ namespace Discord.Net.WebSockets
JToken token = msg.Payload as JToken; JToken token = msg.Payload as JToken;
if (msg.Type == "READY") if (msg.Type == "READY")
{ {
var payload = token.ToObject<ReadyEvent>(_client.DataSocketSerializer);
var payload = token.ToObject<ReadyEvent>(_serializer);
_sessionId = payload.SessionId; _sessionId = payload.SessionId;
_heartbeatInterval = payload.HeartbeatInterval; _heartbeatInterval = payload.HeartbeatInterval;
} }
else if (msg.Type == "RESUMED") else if (msg.Type == "RESUMED")
{ {
var payload = token.ToObject<ResumedEvent>(_client.DataSocketSerializer);
var payload = token.ToObject<ResumedEvent>(_serializer);
_heartbeatInterval = payload.HeartbeatInterval; _heartbeatInterval = payload.HeartbeatInterval;
} }
RaiseReceivedEvent(msg.Type, token); RaiseReceivedEvent(msg.Type, token);
@@ -112,19 +112,19 @@ namespace Discord.Net.WebSockets
break; break;
case OpCodes.Redirect: 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) if (payload.Url != null)
{ {
Host = payload.Url; 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); await Redirect(payload.Url).ConfigureAwait(false);
} }
} }
break; break;
default: default:
if (_logLevel >= LogMessageSeverity.Warning)
RaiseOnLog(LogMessageSeverity.Warning, $"Unknown Opcode: {opCode}");
if (_logger.Level >= LogSeverity.Warning)
_logger.Log(LogSeverity.Warning, $"Unknown Opcode: {opCode}");
break; 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 public abstract partial class WebSocket
{ {
protected readonly IWebSocketEngine _engine; protected readonly IWebSocketEngine _engine;
protected readonly DiscordWSClient _client;
protected readonly LogMessageSeverity _logLevel;
protected readonly DiscordClient _client;
protected readonly ManualResetEventSlim _connectedEvent; protected readonly ManualResetEventSlim _connectedEvent;


protected ExceptionDispatchInfo _disconnectReason; protected ExceptionDispatchInfo _disconnectReason;
@@ -38,24 +37,48 @@ namespace Discord.Net.WebSockets
private CancellationTokenSource _cancelTokenSource; private CancellationTokenSource _cancelTokenSource;
protected CancellationToken _cancelToken; 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; public WebSocketState State => (WebSocketState)_state;
protected int _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; _client = client;
_logLevel = client.Config.LogLevel;
_logger = logger;


_loginTimeout = client.Config.ConnectionTimeout; _loginTimeout = client.Config.ConnectionTimeout;
_cancelToken = new CancellationToken(true); _cancelToken = new CancellationToken(true);
_connectedEvent = new ManualResetEventSlim(false); _connectedEvent = new ManualResetEventSlim(false);


#if !DOTNET5_4 #if !DOTNET5_4
_engine = new WebSocketSharpEngine(this, client.Config);
_engine = new WebSocketSharpEngine(this, client.Config, _logger);
#else #else
//_engine = new BuiltInWebSocketEngine(this, client.Config);
//_engine = new BuiltInWebSocketEngine(this, client.Config, _logger);
#endif #endif
_engine.BinaryMessage += (s, e) => _engine.BinaryMessage += (s, e) =>
{ {
@@ -73,6 +96,19 @@ namespace Discord.Net.WebSockets
{ {
/*await*/ ProcessMessage(e.Message).Wait(); /*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() protected async Task BeginConnect()
@@ -94,25 +130,6 @@ namespace Discord.Net.WebSockets
throw; 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() protected void EndConnect()
{ {
_state = (int)WebSocketState.Connected; _state = (int)WebSocketState.Connected;
@@ -145,7 +162,7 @@ namespace Discord.Net.WebSockets


_cancelTokenSource.Cancel(); _cancelTokenSource.Cancel();
if (_disconnectState == WebSocketState.Connecting) //_runTask was never made if (_disconnectState == WebSocketState.Connecting) //_runTask was never made
await Cleanup().ConfigureAwait(false);
await Stop().ConfigureAwait(false);
} }


if (!skipAwait) 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() protected virtual async Task RunTasks()
{ {
Task[] tasks = GetTasks().ToArray(); Task[] tasks = GetTasks().ToArray();
@@ -174,7 +210,7 @@ namespace Discord.Net.WebSockets
catch { } catch { }


//Start cleanup //Start cleanup
await Cleanup().ConfigureAwait(false);
await Stop().ConfigureAwait(false);
} }
protected virtual IEnumerable<Task> GetTasks() protected virtual IEnumerable<Task> GetTasks()
{ {
@@ -182,7 +218,8 @@ namespace Discord.Net.WebSockets
return _engine.GetTasks(cancelToken) return _engine.GetTasks(cancelToken)
.Concat(new Task[] { HeartbeatAsync(cancelToken) }); .Concat(new Task[] { HeartbeatAsync(cancelToken) });
} }
protected virtual async Task Cleanup()

protected virtual async Task Stop()
{ {
var disconnectState = _disconnectState; var disconnectState = _disconnectState;
_disconnectState = WebSocketState.Disconnected; _disconnectState = WebSocketState.Disconnected;
@@ -203,8 +240,8 @@ namespace Discord.Net.WebSockets


protected virtual Task ProcessMessage(string json) 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; return TaskHelper.CompletedTask;
} }
protected abstract object GetKeepAlive(); protected abstract object GetKeepAlive();
@@ -212,8 +249,8 @@ namespace Discord.Net.WebSockets
protected void QueueMessage(object message) protected void QueueMessage(object message)
{ {
string json = JsonConvert.SerializeObject(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); _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 internal class WebSocketSharpEngine : IWebSocketEngine
{ {
private readonly DiscordWSClientConfig _config;
private readonly DiscordClientConfig _config;
private readonly Logger _logger;
private readonly ConcurrentQueue<string> _sendQueue; private readonly ConcurrentQueue<string> _sendQueue;
private readonly WebSocket _parent; private readonly WebSocket _parent;
private WSSharpWebSocket _webSocket; private WSSharpWebSocket _webSocket;
@@ -28,10 +29,11 @@ namespace Discord.Net.WebSockets
TextMessage(this, new WebSocketTextMessageEventArgs(msg)); TextMessage(this, new WebSocketTextMessageEventArgs(msg));
} }


internal WebSocketSharpEngine(WebSocket parent, DiscordWSClientConfig config)
internal WebSocketSharpEngine(WebSocket parent, DiscordClientConfig config, Logger logger)
{ {
_parent = parent; _parent = parent;
_config = config; _config = config;
_logger = logger;
_sendQueue = new ConcurrentQueue<string>(); _sendQueue = new ConcurrentQueue<string>();
} }


@@ -51,7 +53,7 @@ namespace Discord.Net.WebSockets
}; };
_webSocket.OnError += async (s, e) => _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); await _parent.DisconnectInternal(e.Exception, skipAwait: true).ConfigureAwait(false);
}; };
_webSocket.OnClose += async (s, e) => _webSocket.OnClose += async (s, e) =>
@@ -61,7 +63,7 @@ namespace Discord.Net.WebSockets
Exception ex = new Exception($"Got Close Message ({code}): {reason}"); Exception ex = new Exception($"Got Close Message ({code}): {reason}");
await _parent.DisconnectInternal(ex, skipAwait: true).ConfigureAwait(false); 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(); _webSocket.Connect();
return TaskHelper.CompletedTask; return TaskHelper.CompletedTask;
} }


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

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


Loading…
Cancel
Save