@@ -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) | ||||
{ | { | ||||
@@ -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) | ||||
{ | { | ||||
@@ -1,11 +0,0 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.API.Converters | |||||
{ | |||||
public class StringEnumConverter | |||||
{ | |||||
} | |||||
} |
@@ -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 | ||||
@@ -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; } | ||||
@@ -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> | ||||
@@ -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; | ||||
@@ -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; | ||||
@@ -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> | ||||
@@ -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; | ||||
} | } | ||||
} | } |
@@ -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); | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -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) | ||||
{ | { | ||||
@@ -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; | ||||
@@ -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))); | |||||
} | |||||
} | |||||
} |
@@ -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; | |||||
} | |||||
} | |||||
} |
@@ -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; | |||||
} | |||||
} | |||||
} |
@@ -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> | ||||
@@ -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) | ||||
{ | { | ||||
@@ -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)); | |||||
} | |||||
} | |||||
} |
@@ -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> | ||||
@@ -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] | ||||
@@ -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]); | ||||
@@ -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); | ||||
@@ -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; | ||||
} | } | ||||
} | } | ||||
@@ -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)); | |||||
} | |||||
} | |||||
} |
@@ -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); | ||||
} | } | ||||
@@ -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; | ||||
} | } | ||||
@@ -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.") | ||||
{ | { | ||||
} | } | ||||