@@ -4,7 +4,7 @@ using System.Collections.Generic; | |||
namespace Discord.API.Converters | |||
{ | |||
internal class EnumerableLongStringConverter : JsonConverter | |||
public class EnumerableLongStringConverter : JsonConverter | |||
{ | |||
public override bool CanConvert(Type objectType) | |||
{ | |||
@@ -3,7 +3,7 @@ using Newtonsoft.Json; | |||
namespace Discord.API.Converters | |||
{ | |||
internal class LongStringConverter : JsonConverter | |||
public class LongStringConverter : JsonConverter | |||
{ | |||
public override bool CanConvert(Type objectType) | |||
{ | |||
@@ -19,7 +19,7 @@ namespace Discord.API.Converters | |||
} | |||
} | |||
internal class NullableLongStringConverter : JsonConverter | |||
public class NullableLongStringConverter : JsonConverter | |||
{ | |||
public override bool CanConvert(Type objectType) | |||
{ | |||
@@ -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 | |||
public class WebSocketMessage | |||
{ | |||
public WebSocketMessage() { } | |||
public WebSocketMessage(int op) { Operation = op; } | |||
[JsonProperty("op")] | |||
public int Operation; | |||
[JsonProperty("d")] | |||
@@ -20,12 +23,12 @@ namespace Discord.API | |||
[JsonProperty("s", NullValueHandling = NullValueHandling.Ignore)] | |||
public int? Sequence; | |||
} | |||
internal abstract class WebSocketMessage<T> : WebSocketMessage | |||
public abstract class WebSocketMessage<T> : WebSocketMessage | |||
where T : new() | |||
{ | |||
public WebSocketMessage() { Payload = new T(); } | |||
public WebSocketMessage(int op) { Operation = op; Payload = new T(); } | |||
public WebSocketMessage(int op, T payload) { Operation = op; Payload = payload; } | |||
public WebSocketMessage(int op) : base(op) { Payload = new T(); } | |||
public WebSocketMessage(int op, T payload) : base(op) { Payload = payload; } | |||
[JsonIgnore] | |||
public new T Payload | |||
@@ -1,16 +1,22 @@ | |||
using System; | |||
using System.Net; | |||
using System.Reflection; | |||
namespace Discord | |||
{ | |||
public class DiscordAPIClientConfig | |||
public enum LogSeverity : byte | |||
{ | |||
internal static readonly string UserAgent = $"Discord.Net/{DiscordClient.Version} (https://github.com/RogueException/Discord.Net)"; | |||
Error = 1, | |||
Warning = 2, | |||
Info = 3, | |||
Verbose = 4, | |||
Debug = 5 | |||
} | |||
public class DiscordAPIClientConfig | |||
{ | |||
/// <summary> Specifies the minimum log level severity that will be sent to the LogMessage event. Warning: setting this to debug will really hurt performance but should help investigate any internal issues. </summary> | |||
public LogMessageSeverity LogLevel { get { return _logLevel; } set { SetValue(ref _logLevel, value); } } | |||
private LogMessageSeverity _logLevel = LogMessageSeverity.Info; | |||
public LogSeverity LogLevel { get { return _logLevel; } set { SetValue(ref _logLevel, value); } } | |||
private LogSeverity _logLevel = LogSeverity.Info; | |||
/// <summary> Max time (in milliseconds) to wait for an API request to complete. </summary> | |||
public int APITimeout { get { return _apiTimeout; } set { SetValue(ref _apiTimeout, value); } } | |||
@@ -23,6 +29,9 @@ namespace Discord | |||
public NetworkCredential ProxyCredentials { get { return _proxyCredentials; } set { SetValue(ref _proxyCredentials, value); } } | |||
private NetworkCredential _proxyCredentials = null; | |||
//Internals | |||
internal static readonly string UserAgent = $"Discord.Net/{DiscordClient.Version} (https://github.com/RogueException/Discord.Net)"; | |||
//Lock | |||
protected bool _isLocked; | |||
internal void Lock() { _isLocked = true; } | |||
@@ -50,19 +50,19 @@ namespace Discord | |||
private void RaiseChannelCreated(Channel channel) | |||
{ | |||
if (ChannelCreated != null) | |||
RaiseEvent(nameof(ChannelCreated), () => ChannelCreated(this, new ChannelEventArgs(channel))); | |||
EventHelper.Raise(_logger, nameof(ChannelCreated), () => ChannelCreated(this, new ChannelEventArgs(channel))); | |||
} | |||
public event EventHandler<ChannelEventArgs> ChannelDestroyed; | |||
private void RaiseChannelDestroyed(Channel channel) | |||
{ | |||
if (ChannelDestroyed != null) | |||
RaiseEvent(nameof(ChannelDestroyed), () => ChannelDestroyed(this, new ChannelEventArgs(channel))); | |||
EventHelper.Raise(_logger, nameof(ChannelDestroyed), () => ChannelDestroyed(this, new ChannelEventArgs(channel))); | |||
} | |||
public event EventHandler<ChannelEventArgs> ChannelUpdated; | |||
private void RaiseChannelUpdated(Channel channel) | |||
{ | |||
if (ChannelUpdated != null) | |||
RaiseEvent(nameof(ChannelUpdated), () => ChannelUpdated(this, new ChannelEventArgs(channel))); | |||
EventHelper.Raise(_logger, nameof(ChannelUpdated), () => ChannelUpdated(this, new ChannelEventArgs(channel))); | |||
} | |||
/// <summary> Returns a collection of all servers this client is a member of. </summary> | |||
@@ -54,31 +54,31 @@ namespace Discord | |||
private void RaiseMessageReceived(Message msg) | |||
{ | |||
if (MessageReceived != null) | |||
RaiseEvent(nameof(MessageReceived), () => MessageReceived(this, new MessageEventArgs(msg))); | |||
EventHelper.Raise(_logger, nameof(MessageReceived), () => MessageReceived(this, new MessageEventArgs(msg))); | |||
} | |||
public event EventHandler<MessageEventArgs> MessageSent; | |||
private void RaiseMessageSent(Message msg) | |||
{ | |||
if (MessageSent != null) | |||
RaiseEvent(nameof(MessageSent), () => MessageSent(this, new MessageEventArgs(msg))); | |||
EventHelper.Raise(_logger, nameof(MessageSent), () => MessageSent(this, new MessageEventArgs(msg))); | |||
} | |||
public event EventHandler<MessageEventArgs> MessageDeleted; | |||
private void RaiseMessageDeleted(Message msg) | |||
{ | |||
if (MessageDeleted != null) | |||
RaiseEvent(nameof(MessageDeleted), () => MessageDeleted(this, new MessageEventArgs(msg))); | |||
EventHelper.Raise(_logger, nameof(MessageDeleted), () => MessageDeleted(this, new MessageEventArgs(msg))); | |||
} | |||
public event EventHandler<MessageEventArgs> MessageUpdated; | |||
private void RaiseMessageUpdated(Message msg) | |||
{ | |||
if (MessageUpdated != null) | |||
RaiseEvent(nameof(MessageUpdated), () => MessageUpdated(this, new MessageEventArgs(msg))); | |||
EventHelper.Raise(_logger, nameof(MessageUpdated), () => MessageUpdated(this, new MessageEventArgs(msg))); | |||
} | |||
public event EventHandler<MessageEventArgs> MessageReadRemotely; | |||
private void RaiseMessageReadRemotely(Message msg) | |||
{ | |||
if (MessageReadRemotely != null) | |||
RaiseEvent(nameof(MessageReadRemotely), () => MessageReadRemotely(this, new MessageEventArgs(msg))); | |||
EventHelper.Raise(_logger, nameof(MessageReadRemotely), () => MessageReadRemotely(this, new MessageEventArgs(msg))); | |||
} | |||
internal Messages Messages => _messages; | |||
@@ -29,19 +29,19 @@ namespace Discord | |||
private void RaiseRoleCreated(Role role) | |||
{ | |||
if (RoleCreated != null) | |||
RaiseEvent(nameof(RoleCreated), () => RoleCreated(this, new RoleEventArgs(role))); | |||
EventHelper.Raise(_logger, nameof(RoleCreated), () => RoleCreated(this, new RoleEventArgs(role))); | |||
} | |||
public event EventHandler<RoleEventArgs> RoleUpdated; | |||
private void RaiseRoleDeleted(Role role) | |||
{ | |||
if (RoleDeleted != null) | |||
RaiseEvent(nameof(RoleDeleted), () => RoleDeleted(this, new RoleEventArgs(role))); | |||
EventHelper.Raise(_logger, nameof(RoleDeleted), () => RoleDeleted(this, new RoleEventArgs(role))); | |||
} | |||
public event EventHandler<RoleEventArgs> RoleDeleted; | |||
private void RaiseRoleUpdated(Role role) | |||
{ | |||
if (RoleUpdated != null) | |||
RaiseEvent(nameof(RoleUpdated), () => RoleUpdated(this, new RoleEventArgs(role))); | |||
EventHelper.Raise(_logger, nameof(RoleUpdated), () => RoleUpdated(this, new RoleEventArgs(role))); | |||
} | |||
internal Roles Roles => _roles; | |||
@@ -29,31 +29,31 @@ namespace Discord | |||
private void RaiseJoinedServer(Server server) | |||
{ | |||
if (JoinedServer != null) | |||
RaiseEvent(nameof(JoinedServer), () => JoinedServer(this, new ServerEventArgs(server))); | |||
EventHelper.Raise(_logger, nameof(JoinedServer), () => JoinedServer(this, new ServerEventArgs(server))); | |||
} | |||
public event EventHandler<ServerEventArgs> LeftServer; | |||
private void RaiseLeftServer(Server server) | |||
{ | |||
if (LeftServer != null) | |||
RaiseEvent(nameof(LeftServer), () => LeftServer(this, new ServerEventArgs(server))); | |||
EventHelper.Raise(_logger, nameof(LeftServer), () => LeftServer(this, new ServerEventArgs(server))); | |||
} | |||
public event EventHandler<ServerEventArgs> ServerUpdated; | |||
private void RaiseServerUpdated(Server server) | |||
{ | |||
if (ServerUpdated != null) | |||
RaiseEvent(nameof(ServerUpdated), () => ServerUpdated(this, new ServerEventArgs(server))); | |||
EventHelper.Raise(_logger, nameof(ServerUpdated), () => ServerUpdated(this, new ServerEventArgs(server))); | |||
} | |||
public event EventHandler<ServerEventArgs> ServerUnavailable; | |||
private void RaiseServerUnavailable(Server server) | |||
{ | |||
if (ServerUnavailable != null) | |||
RaiseEvent(nameof(ServerUnavailable), () => ServerUnavailable(this, new ServerEventArgs(server))); | |||
EventHelper.Raise(_logger, nameof(ServerUnavailable), () => ServerUnavailable(this, new ServerEventArgs(server))); | |||
} | |||
public event EventHandler<ServerEventArgs> ServerAvailable; | |||
private void RaiseServerAvailable(Server server) | |||
{ | |||
if (ServerAvailable != null) | |||
RaiseEvent(nameof(ServerAvailable), () => ServerAvailable(this, new ServerEventArgs(server))); | |||
EventHelper.Raise(_logger, nameof(ServerAvailable), () => ServerAvailable(this, new ServerEventArgs(server))); | |||
} | |||
/// <summary> Returns a collection of all servers this client is a member of. </summary> | |||
@@ -73,63 +73,63 @@ namespace Discord | |||
private void RaiseUserJoined(User user) | |||
{ | |||
if (UserJoined != null) | |||
RaiseEvent(nameof(UserJoined), () => UserJoined(this, new UserEventArgs(user))); | |||
EventHelper.Raise(_logger, nameof(UserJoined), () => UserJoined(this, new UserEventArgs(user))); | |||
} | |||
public event EventHandler<UserEventArgs> UserLeft; | |||
private void RaiseUserLeft(User user) | |||
{ | |||
if (UserLeft != null) | |||
RaiseEvent(nameof(UserLeft), () => UserLeft(this, new UserEventArgs(user))); | |||
EventHelper.Raise(_logger, nameof(UserLeft), () => UserLeft(this, new UserEventArgs(user))); | |||
} | |||
public event EventHandler<UserEventArgs> UserUpdated; | |||
private void RaiseUserUpdated(User user) | |||
{ | |||
if (UserUpdated != null) | |||
RaiseEvent(nameof(UserUpdated), () => UserUpdated(this, new UserEventArgs(user))); | |||
EventHelper.Raise(_logger, nameof(UserUpdated), () => UserUpdated(this, new UserEventArgs(user))); | |||
} | |||
public event EventHandler<UserEventArgs> UserPresenceUpdated; | |||
private void RaiseUserPresenceUpdated(User user) | |||
{ | |||
if (UserPresenceUpdated != null) | |||
RaiseEvent(nameof(UserPresenceUpdated), () => UserPresenceUpdated(this, new UserEventArgs(user))); | |||
EventHelper.Raise(_logger, nameof(UserPresenceUpdated), () => UserPresenceUpdated(this, new UserEventArgs(user))); | |||
} | |||
public event EventHandler<UserEventArgs> UserVoiceStateUpdated; | |||
private void RaiseUserVoiceStateUpdated(User user) | |||
{ | |||
if (UserVoiceStateUpdated != null) | |||
RaiseEvent(nameof(UserVoiceStateUpdated), () => UserVoiceStateUpdated(this, new UserEventArgs(user))); | |||
EventHelper.Raise(_logger, nameof(UserVoiceStateUpdated), () => UserVoiceStateUpdated(this, new UserEventArgs(user))); | |||
} | |||
public event EventHandler<UserChannelEventArgs> UserIsTypingUpdated; | |||
private void RaiseUserIsTyping(User user, Channel channel) | |||
{ | |||
if (UserIsTypingUpdated != null) | |||
RaiseEvent(nameof(UserIsTypingUpdated), () => UserIsTypingUpdated(this, new UserChannelEventArgs(user, channel))); | |||
EventHelper.Raise(_logger, nameof(UserIsTypingUpdated), () => UserIsTypingUpdated(this, new UserChannelEventArgs(user, channel))); | |||
} | |||
public event EventHandler ProfileUpdated; | |||
private void RaiseProfileUpdated() | |||
{ | |||
if (ProfileUpdated != null) | |||
RaiseEvent(nameof(ProfileUpdated), () => ProfileUpdated(this, EventArgs.Empty)); | |||
EventHelper.Raise(_logger, nameof(ProfileUpdated), () => ProfileUpdated(this, EventArgs.Empty)); | |||
} | |||
public event EventHandler<BanEventArgs> UserBanned; | |||
private void RaiseUserBanned(long userId, Server server) | |||
{ | |||
if (UserBanned != null) | |||
RaiseEvent(nameof(UserBanned), () => UserBanned(this, new BanEventArgs(userId, server))); | |||
EventHelper.Raise(_logger, nameof(UserBanned), () => UserBanned(this, new BanEventArgs(userId, server))); | |||
} | |||
public event EventHandler<BanEventArgs> UserUnbanned; | |||
private void RaiseUserUnbanned(long userId, Server server) | |||
{ | |||
if (UserUnbanned != null) | |||
RaiseEvent(nameof(UserUnbanned), () => UserUnbanned(this, new BanEventArgs(userId, server))); | |||
EventHelper.Raise(_logger, nameof(UserUnbanned), () => UserUnbanned(this, new BanEventArgs(userId, server))); | |||
} | |||
/// <summary> Returns the current logged-in user in a private channel. </summary> | |||
/// <summary> Returns the current logged-in user used in private channels. </summary> | |||
internal User PrivateUser => _privateUser; | |||
private User _privateUser; | |||
/// <summary> Returns information about the currently logged-in account. </summary> | |||
public GlobalUser CurrentUser { get { CheckReady(); return _privateUser.Global; } } | |||
public GlobalUser CurrentUser => _privateUser?.Global; | |||
/// <summary> Returns a collection of all unique users this client can currently see. </summary> | |||
public IEnumerable<GlobalUser> AllUsers { get { CheckReady(); return _globalUsers; } } | |||
@@ -272,7 +272,7 @@ namespace Discord | |||
{ | |||
if (server == null) throw new ArgumentNullException(nameof(server)); | |||
_dataSocket.SendRequestUsers(server.Id); | |||
_webSocket.SendRequestUsers(server.Id); | |||
} | |||
public async Task EditProfile(string currentPassword = "", | |||
@@ -312,7 +312,7 @@ namespace Discord | |||
} | |||
private Task SendStatus() | |||
{ | |||
_dataSocket.SendStatus(_status == UserStatus.Idle ? EpochTime.GetMilliseconds() - (10 * 60 * 1000) : (long?)null, _gameId); | |||
_webSocket.SendStatus(_status == UserStatus.Idle ? EpochTime.GetMilliseconds() - (10 * 60 * 1000) : (long?)null, _gameId); | |||
return TaskHelper.CompletedTask; | |||
} | |||
} |
@@ -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.Linq; | |||
using System.Reflection; | |||
using System.Runtime.ExceptionServices; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
namespace Discord | |||
{ | |||
public enum DiscordClientState : byte | |||
{ | |||
Disconnected, | |||
Connecting, | |||
Connected, | |||
Disconnecting | |||
} | |||
public class DisconnectedEventArgs : EventArgs | |||
{ | |||
public readonly bool WasUnexpected; | |||
public readonly Exception Error; | |||
public DisconnectedEventArgs(bool wasUnexpected, Exception error) | |||
{ | |||
WasUnexpected = wasUnexpected; | |||
Error = error; | |||
} | |||
} | |||
public sealed class LogMessageEventArgs : EventArgs | |||
{ | |||
public LogSeverity Severity { get; } | |||
public string Source { get; } | |||
public string Message { get; } | |||
public Exception Exception { get; } | |||
public LogMessageEventArgs(LogSeverity severity, string source, string msg, Exception exception) | |||
{ | |||
Severity = severity; | |||
Source = source; | |||
Message = msg; | |||
Exception = exception; | |||
} | |||
} | |||
/// <summary> Provides a connection to the DiscordApp service. </summary> | |||
public sealed partial class DiscordClient : DiscordWSClient | |||
public partial class DiscordClient | |||
{ | |||
public static readonly string Version = typeof(DiscordClientConfig).GetTypeInfo().Assembly.GetName().Version.ToString(3); | |||
public static readonly string Version = typeof(DiscordClient).GetTypeInfo().Assembly.GetName().Version.ToString(3); | |||
private readonly DiscordAPIClient _api; | |||
private readonly ManualResetEvent _disconnectedEvent; | |||
private readonly ManualResetEventSlim _connectedEvent; | |||
private readonly Random _rand; | |||
private readonly JsonSerializer _messageImporter; | |||
private readonly ConcurrentQueue<Message> _pendingMessages; | |||
private readonly Dictionary<Type, object> _singletons; | |||
private readonly LogService _log; | |||
private readonly object _cacheLock; | |||
private Logger _logger, _restLogger, _cacheLogger; | |||
private bool _sentInitialLog; | |||
private long? _userId; | |||
private UserStatus _status; | |||
private int? _gameId; | |||
private Task _runTask; | |||
private ExceptionDispatchInfo _disconnectReason; | |||
private bool _wasDisconnectUnexpected; | |||
/// <summary> Returns the configuration object used to make this client. Note that this object cannot be edited directly - to change the configuration of this client, use the DiscordClient(DiscordClientConfig config) constructor. </summary> | |||
public new DiscordClientConfig Config => _config as DiscordClientConfig; | |||
public DiscordClientConfig Config => _config; | |||
private readonly DiscordClientConfig _config; | |||
/// <summary> Returns the current connection state of this client. </summary> | |||
public DiscordClientState State => (DiscordClientState)_state; | |||
private int _state; | |||
/// <summary> Gives direct access to the underlying DiscordAPIClient. This can be used to modify objects not in cache. </summary> | |||
public DiscordAPIClient API => _api; | |||
public DiscordAPIClient APIClient => _api; | |||
private readonly DiscordAPIClient _api; | |||
/// <summary> Returns the internal websocket object. </summary> | |||
public DataWebSocket WebSocket => _webSocket; | |||
private readonly DataWebSocket _webSocket; | |||
public string GatewayUrl => _gateway; | |||
private string _gateway; | |||
public string Token => _token; | |||
private string _token; | |||
/// <summary> Returns a cancellation token that triggers when the client is manually disconnected. </summary> | |||
public CancellationToken CancelToken => _cancelToken; | |||
private CancellationTokenSource _cancelTokenSource; | |||
private CancellationToken _cancelToken; | |||
public event EventHandler Connected; | |||
private void RaiseConnected() | |||
{ | |||
if (Connected != null) | |||
EventHelper.Raise(_logger, nameof(Connected), () => Connected(this, EventArgs.Empty)); | |||
} | |||
public event EventHandler<DisconnectedEventArgs> Disconnected; | |||
private void RaiseDisconnected(DisconnectedEventArgs e) | |||
{ | |||
if (Disconnected != null) | |||
EventHelper.Raise(_logger, nameof(Disconnected), () => Disconnected(this, e)); | |||
} | |||
/// <summary> Initializes a new instance of the DiscordClient class. </summary> | |||
public DiscordClient(DiscordClientConfig config = null) | |||
: base(config ?? new DiscordClientConfig()) | |||
{ | |||
_config = config ?? new DiscordClientConfig(); | |||
_config.Lock(); | |||
_rand = new Random(); | |||
_api = new DiscordAPIClient(_config); | |||
if (Config.UseMessageQueue) | |||
_pendingMessages = new ConcurrentQueue<Message>(); | |||
_state = (int)DiscordClientState.Disconnected; | |||
_status = UserStatus.Online; | |||
object cacheLock = new object(); | |||
_channels = new Channels(this, cacheLock); | |||
_users = new Users(this, cacheLock); | |||
_messages = new Messages(this, cacheLock, Config.MessageCacheLength > 0); | |||
_roles = new Roles(this, cacheLock); | |||
_servers = new Servers(this, cacheLock); | |||
_globalUsers = new GlobalUsers(this, cacheLock); | |||
//Services | |||
_singletons = new Dictionary<Type, object>(); | |||
_log = AddService(new LogService()); | |||
CreateMainLogger(); | |||
_status = UserStatus.Online; | |||
//Async | |||
_cancelToken = new CancellationToken(true); | |||
_disconnectedEvent = new ManualResetEvent(true); | |||
_connectedEvent = new ManualResetEventSlim(false); | |||
//Cache | |||
_cacheLock = new object(); | |||
_channels = new Channels(this, _cacheLock); | |||
_users = new Users(this, _cacheLock); | |||
_messages = new Messages(this, _cacheLock, Config.MessageCacheLength > 0); | |||
_roles = new Roles(this, _cacheLock); | |||
_servers = new Servers(this, _cacheLock); | |||
_globalUsers = new GlobalUsers(this, _cacheLock); | |||
CreateCacheLogger(); | |||
//Networking | |||
_webSocket = CreateWebSocket(); | |||
_api = new DiscordAPIClient(_config); | |||
if (Config.UseMessageQueue) | |||
_pendingMessages = new ConcurrentQueue<Message>(); | |||
this.Connected += async (s, e) => | |||
{ | |||
_api.CancelToken = _cancelToken; | |||
await SendStatus().ConfigureAwait(false); | |||
}; | |||
if (_config.LogLevel >= LogMessageSeverity.Info) | |||
CreateRestLogger(); | |||
//Import/Export | |||
_messageImporter = new JsonSerializer(); | |||
_messageImporter.ContractResolver = new Message.ImportResolver(); | |||
} | |||
private void CreateMainLogger() | |||
{ | |||
_logger = _log.CreateLogger("Client"); | |||
if (_logger.Level >= LogSeverity.Info) | |||
{ | |||
JoinedServer += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, | |||
JoinedServer += (s, e) => _logger.Log(LogSeverity.Info, | |||
$"Server Created: {e.Server?.Name ?? "[Private]"}"); | |||
LeftServer += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, | |||
LeftServer += (s, e) => _logger.Log(LogSeverity.Info, | |||
$"Server Destroyed: {e.Server?.Name ?? "[Private]"}"); | |||
ServerUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, | |||
ServerUpdated += (s, e) => _logger.Log(LogSeverity.Info, | |||
$"Server Updated: {e.Server?.Name ?? "[Private]"}"); | |||
ServerAvailable += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, | |||
ServerAvailable += (s, e) => _logger.Log(LogSeverity.Info, | |||
$"Server Available: {e.Server?.Name ?? "[Private]"}"); | |||
ServerUnavailable += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, | |||
ServerUnavailable += (s, e) => _logger.Log(LogSeverity.Info, | |||
$"Server Unavailable: {e.Server?.Name ?? "[Private]"}"); | |||
ChannelCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, | |||
ChannelCreated += (s, e) => _logger.Log(LogSeverity.Info, | |||
$"Channel Created: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}"); | |||
ChannelDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, | |||
ChannelDestroyed += (s, e) => _logger.Log(LogSeverity.Info, | |||
$"Channel Destroyed: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}"); | |||
ChannelUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, | |||
ChannelUpdated += (s, e) => _logger.Log(LogSeverity.Info, | |||
$"Channel Updated: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}"); | |||
MessageReceived += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, | |||
MessageReceived += (s, e) => _logger.Log(LogSeverity.Info, | |||
$"Message Received: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}"); | |||
MessageDeleted += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, | |||
MessageDeleted += (s, e) => _logger.Log(LogSeverity.Info, | |||
$"Message Deleted: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}"); | |||
MessageUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, | |||
MessageUpdated += (s, e) => _logger.Log(LogSeverity.Info, | |||
$"Message Update: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}"); | |||
RoleCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, | |||
RoleCreated += (s, e) => _logger.Log(LogSeverity.Info, | |||
$"Role Created: {e.Server?.Name ?? "[Private]"}/{e.Role?.Name}"); | |||
RoleUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, | |||
RoleUpdated += (s, e) => _logger.Log(LogSeverity.Info, | |||
$"Role Updated: {e.Server?.Name ?? "[Private]"}/{e.Role?.Name}"); | |||
RoleDeleted += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, | |||
RoleDeleted += (s, e) => _logger.Log(LogSeverity.Info, | |||
$"Role Deleted: {e.Server?.Name ?? "[Private]"}/{e.Role?.Name}"); | |||
UserBanned += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, | |||
UserBanned += (s, e) => _logger.Log(LogSeverity.Info, | |||
$"Banned User: {e.Server?.Name ?? "[Private]" }/{e.UserId}"); | |||
UserUnbanned += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, | |||
UserUnbanned += (s, e) => _logger.Log(LogSeverity.Info, | |||
$"Unbanned User: {e.Server?.Name ?? "[Private]"}/{e.UserId}"); | |||
UserJoined += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, | |||
UserJoined += (s, e) => _logger.Log(LogSeverity.Info, | |||
$"User Joined: {e.Server?.Name ?? "[Private]"}/{e.User.Name}"); | |||
UserLeft += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, | |||
UserLeft += (s, e) => _logger.Log(LogSeverity.Info, | |||
$"User Left: {e.Server?.Name ?? "[Private]"}/{e.User.Name}"); | |||
UserUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, | |||
UserUpdated += (s, e) => _logger.Log(LogSeverity.Info, | |||
$"User Updated: {e.Server?.Name ?? "[Private]"}/{e.User.Name}"); | |||
UserVoiceStateUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, | |||
UserVoiceStateUpdated += (s, e) => _logger.Log(LogSeverity.Info, | |||
$"User Updated (Voice State): {e.Server?.Name ?? "[Private]"}/{e.User.Name}"); | |||
ProfileUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, | |||
ProfileUpdated += (s, e) => _logger.Log(LogSeverity.Info, | |||
"Profile Updated"); | |||
} | |||
if (_config.LogLevel >= LogMessageSeverity.Verbose) | |||
if (_log.Level >= LogSeverity.Verbose) | |||
{ | |||
UserIsTypingUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, | |||
UserIsTypingUpdated += (s, e) => _logger.Log(LogSeverity.Verbose, | |||
$"User Updated (Is Typing): {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.User?.Name}"); | |||
MessageReadRemotely += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, | |||
MessageReadRemotely += (s, e) => _logger.Log(LogSeverity.Verbose, | |||
$"Read Message (Remotely): {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}"); | |||
MessageSent += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, | |||
MessageSent += (s, e) => _logger.Log(LogSeverity.Verbose, | |||
$"Sent Message: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}"); | |||
UserPresenceUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, | |||
UserPresenceUpdated += (s, e) => _logger.Log(LogSeverity.Verbose, | |||
$"User Updated (Presence): {e.Server?.Name ?? "[Private]"}/{e.User?.Name}"); | |||
} | |||
} | |||
private void CreateRestLogger() | |||
{ | |||
_restLogger = _log.CreateLogger("Rest"); | |||
if (_log.Level >= LogSeverity.Verbose) | |||
{ | |||
_api.RestClient.OnRequest += (s, e) => | |||
{ | |||
if (e.Payload != null) | |||
RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Rest, $"{e.Method} {e.Path}: {Math.Round(e.ElapsedMilliseconds, 2)} ms ({e.Payload})"); | |||
if (e.Payload != null) | |||
_restLogger.Log(LogSeverity.Verbose, $"{e.Method} {e.Path}: {Math.Round(e.ElapsedMilliseconds, 2)} ms ({e.Payload})"); | |||
else | |||
RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Rest, $"{e.Method} {e.Path}: {Math.Round(e.ElapsedMilliseconds, 2)} ms"); | |||
_restLogger.Log(LogSeverity.Verbose, $"{e.Method} {e.Path}: {Math.Round(e.ElapsedMilliseconds, 2)} ms"); | |||
}; | |||
} | |||
if (_config.LogLevel >= LogMessageSeverity.Debug) | |||
} | |||
private void CreateCacheLogger() | |||
{ | |||
_cacheLogger = _log.CreateLogger("Cache"); | |||
if (_log.Level >= LogSeverity.Debug) | |||
{ | |||
_channels.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Created Channel {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); | |||
_channels.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Destroyed Channel {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); | |||
_channels.Cleared += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Cleared Channels"); | |||
_users.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Created User {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); | |||
_users.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Destroyed User {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); | |||
_users.Cleared += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Cleared Users"); | |||
_messages.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Created Message {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Channel.Id}/{e.Item.Id}"); | |||
_messages.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Destroyed Message {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Channel.Id}/{e.Item.Id}"); | |||
_messages.ItemRemapped += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Remapped Message {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Channel.Id}/[{e.OldId} -> {e.NewId}]"); | |||
_messages.Cleared += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Cleared Messages"); | |||
_roles.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Created Role {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); | |||
_roles.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Destroyed Role {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); | |||
_roles.Cleared += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Cleared Roles"); | |||
_servers.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Created Server {e.Item.Id}"); | |||
_servers.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Destroyed Server {e.Item.Id}"); | |||
_servers.Cleared += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Cleared Servers"); | |||
_globalUsers.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Created User {e.Item.Id}"); | |||
_globalUsers.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Destroyed User {e.Item.Id}"); | |||
_globalUsers.Cleared += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Cleared Users"); | |||
_channels.ItemCreated += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Created Channel {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); | |||
_channels.ItemDestroyed += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Destroyed Channel {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); | |||
_channels.Cleared += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Cleared Channels"); | |||
_users.ItemCreated += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Created User {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); | |||
_users.ItemDestroyed += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Destroyed User {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); | |||
_users.Cleared += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Cleared Users"); | |||
_messages.ItemCreated += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Created Message {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Channel.Id}/{e.Item.Id}"); | |||
_messages.ItemDestroyed += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Destroyed Message {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Channel.Id}/{e.Item.Id}"); | |||
_messages.ItemRemapped += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Remapped Message {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Channel.Id}/[{e.OldId} -> {e.NewId}]"); | |||
_messages.Cleared += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Cleared Messages"); | |||
_roles.ItemCreated += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Created Role {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); | |||
_roles.ItemDestroyed += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Destroyed Role {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); | |||
_roles.Cleared += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Cleared Roles"); | |||
_servers.ItemCreated += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Created Server {e.Item.Id}"); | |||
_servers.ItemDestroyed += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Destroyed Server {e.Item.Id}"); | |||
_servers.Cleared += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Cleared Servers"); | |||
_globalUsers.ItemCreated += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Created User {e.Item.Id}"); | |||
_globalUsers.ItemDestroyed += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Destroyed User {e.Item.Id}"); | |||
_globalUsers.Cleared += (s, e) => _cacheLogger.Log(LogSeverity.Debug, $"Cleared Users"); | |||
} | |||
} | |||
if (Config.UseMessageQueue) | |||
_pendingMessages = new ConcurrentQueue<Message>(); | |||
_messageImporter = new JsonSerializer(); | |||
_messageImporter.ContractResolver = new Message.ImportResolver(); | |||
} | |||
private DataWebSocket CreateWebSocket() | |||
{ | |||
var socket = new DataWebSocket(this, _log.CreateLogger("WebSocket")); | |||
socket.Connected += (s, e) => | |||
{ | |||
if (_state == (int)DiscordClientState.Connecting) | |||
CompleteConnect(); | |||
}; | |||
socket.Disconnected += async (s, e) => | |||
{ | |||
RaiseDisconnected(e); | |||
if (e.WasUnexpected) | |||
await socket.Reconnect(_token).ConfigureAwait(false); | |||
}; | |||
socket.ReceivedEvent += async (s, e) => await OnReceivedEvent(e).ConfigureAwait(false); | |||
return socket; | |||
} | |||
/// <summary> Connects to the Discord server with the provided email and password. </summary> | |||
/// <returns> Returns a token for future connections. </returns> | |||
public new async Task<string> Connect(string email, string password) | |||
public async Task<string> Connect(string email, string password) | |||
{ | |||
if (!_sentInitialLog) | |||
SendInitialLog(); | |||
@@ -167,13 +293,13 @@ namespace Discord | |||
.Timeout(_config.APITimeout) | |||
.ConfigureAwait(false); | |||
token = response.Token; | |||
if (_config.LogLevel >= LogMessageSeverity.Verbose) | |||
RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, "Login successful, got token."); | |||
if (_config.LogLevel >= LogSeverity.Verbose) | |||
_logger.Log(LogSeverity.Verbose, "Login successful, got token."); | |||
await Connect(token); | |||
return token; | |||
} | |||
catch (TaskCanceledException) { throw new TimeoutException(); } | |||
await Connect(token).ConfigureAwait(false); | |||
return token; | |||
} | |||
/// <summary> Connects to the Discord server with the provided token. </summary> | |||
public async Task Connect(string token) | |||
@@ -185,22 +311,133 @@ namespace Discord | |||
await Disconnect().ConfigureAwait(false); | |||
_api.Token = token; | |||
string gateway = (await _api.Gateway() | |||
.Timeout(_config.APITimeout) | |||
.ConfigureAwait(false) | |||
).Url; | |||
if (_config.LogLevel >= LogMessageSeverity.Verbose) | |||
RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, $"Websocket endpoint: {gateway}"); | |||
await base.Connect(gateway, token) | |||
.Timeout(_config.ConnectionTimeout) | |||
.ConfigureAwait(false); | |||
var gatewayResponse = await _api.Gateway().Timeout(_config.APITimeout).ConfigureAwait(false); | |||
string gateway = gatewayResponse.Url; | |||
if (_config.LogLevel >= LogSeverity.Verbose) | |||
_logger.Log(LogSeverity.Verbose, $"Websocket endpoint: {gateway}"); | |||
try | |||
{ | |||
_state = (int)DiscordClientState.Connecting; | |||
_disconnectedEvent.Reset(); | |||
_gateway = gateway; | |||
_token = token; | |||
_cancelTokenSource = new CancellationTokenSource(); | |||
_cancelToken = _cancelTokenSource.Token; | |||
_webSocket.Host = gateway; | |||
_webSocket.ParentCancelToken = _cancelToken; | |||
await _webSocket.Login(token).ConfigureAwait(false); | |||
_runTask = RunTasks(); | |||
try | |||
{ | |||
//Cancel if either Disconnect is called, data socket errors or timeout is reached | |||
var cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken, _webSocket.CancelToken).Token; | |||
_connectedEvent.Wait(cancelToken); | |||
} | |||
catch (OperationCanceledException) | |||
{ | |||
_webSocket.ThrowError(); //Throws data socket's internal error if any occured | |||
throw; | |||
} | |||
//_state = (int)DiscordClientState.Connected; | |||
} | |||
catch | |||
{ | |||
await Disconnect().ConfigureAwait(false); | |||
throw; | |||
} | |||
} | |||
private void CompleteConnect() | |||
{ | |||
_state = (int)DiscordClientState.Connected; | |||
_connectedEvent.Set(); | |||
RaiseConnected(); | |||
} | |||
protected override async Task Cleanup() | |||
/// <summary> Disconnects from the Discord server, canceling any pending requests. </summary> | |||
public Task Disconnect() => DisconnectInternal(new Exception("Disconnect was requested by user."), isUnexpected: false); | |||
private async Task DisconnectInternal(Exception ex = null, bool isUnexpected = true, bool skipAwait = false) | |||
{ | |||
await base.Cleanup().ConfigureAwait(false); | |||
int oldState; | |||
bool hasWriterLock; | |||
//If in either connecting or connected state, get a lock by being the first to switch to disconnecting | |||
oldState = Interlocked.CompareExchange(ref _state, (int)DiscordClientState.Disconnecting, (int)DiscordClientState.Connecting); | |||
if (oldState == (int)DiscordClientState.Disconnected) return; //Already disconnected | |||
hasWriterLock = oldState == (int)DiscordClientState.Connecting; //Caused state change | |||
if (!hasWriterLock) | |||
{ | |||
oldState = Interlocked.CompareExchange(ref _state, (int)DiscordClientState.Disconnecting, (int)DiscordClientState.Connected); | |||
if (oldState == (int)DiscordClientState.Disconnected) return; //Already disconnected | |||
hasWriterLock = oldState == (int)DiscordClientState.Connected; //Caused state change | |||
} | |||
if (hasWriterLock) | |||
{ | |||
_wasDisconnectUnexpected = isUnexpected; | |||
_disconnectReason = ex != null ? ExceptionDispatchInfo.Capture(ex) : null; | |||
_cancelTokenSource.Cancel(); | |||
/*if (_disconnectState == DiscordClientState.Connecting) //_runTask was never made | |||
await Cleanup().ConfigureAwait(false);*/ | |||
} | |||
if (!skipAwait) | |||
{ | |||
Task task = _runTask; | |||
if (_runTask != null) | |||
await task.ConfigureAwait(false); | |||
} | |||
} | |||
private async Task RunTasks() | |||
{ | |||
List<Task> tasks = new List<Task>(); | |||
tasks.Add(_cancelToken.Wait()); | |||
if (Config.UseMessageQueue) | |||
tasks.Add(MessageQueueLoop()); | |||
Task[] tasksArray = tasks.ToArray(); | |||
Task firstTask = Task.WhenAny(tasksArray); | |||
Task allTasks = Task.WhenAll(tasksArray); | |||
//Wait until the first task ends/errors and capture the error | |||
try { await firstTask.ConfigureAwait(false); } | |||
catch (Exception ex) { await DisconnectInternal(ex: ex, skipAwait: true).ConfigureAwait(false); } | |||
//Ensure all other tasks are signaled to end. | |||
await DisconnectInternal(skipAwait: true).ConfigureAwait(false); | |||
//Wait for the remaining tasks to complete | |||
try { await allTasks.ConfigureAwait(false); } | |||
catch { } | |||
//Start cleanup | |||
var wasDisconnectUnexpected = _wasDisconnectUnexpected; | |||
_wasDisconnectUnexpected = false; | |||
await _webSocket.Disconnect().ConfigureAwait(false); | |||
_userId = null; | |||
_gateway = null; | |||
_token = null; | |||
if (!wasDisconnectUnexpected) | |||
{ | |||
_state = (int)DiscordClientState.Disconnected; | |||
_disconnectedEvent.Set(); | |||
} | |||
_connectedEvent.Reset(); | |||
_runTask = null; | |||
} | |||
private async Task Stop() | |||
{ | |||
if (Config.UseMessageQueue) | |||
{ | |||
Message ignored; | |||
@@ -247,16 +484,8 @@ namespace Discord | |||
public T GetService<T>(bool required = true) | |||
where T : class, IService | |||
=> GetSingleton<T>(required); | |||
protected override IEnumerable<Task> GetTasks() | |||
{ | |||
if (Config.UseMessageQueue) | |||
return base.GetTasks().Concat(new Task[] { MessageQueueLoop() }); | |||
else | |||
return base.GetTasks(); | |||
} | |||
protected override async Task OnReceivedEvent(WebSocketEventEventArgs e) | |||
private async Task OnReceivedEvent(WebSocketEventEventArgs e) | |||
{ | |||
try | |||
{ | |||
@@ -265,8 +494,7 @@ namespace Discord | |||
//Global | |||
case "READY": //Resync | |||
{ | |||
base.OnReceivedEvent(e).Wait(); //This cannot be an await, or we'll get later messages before we're ready | |||
var data = e.Payload.ToObject<ReadyEvent>(_dataSocketSerializer); | |||
var data = e.Payload.ToObject<ReadyEvent>(_webSocket.Serializer); | |||
_privateUser = _users.GetOrAdd(data.User.Id, null); | |||
_privateUser.Update(data.User); | |||
_privateUser.Global.Update(data.User); | |||
@@ -291,7 +519,7 @@ namespace Discord | |||
//Servers | |||
case "GUILD_CREATE": | |||
{ | |||
var data = e.Payload.ToObject<GuildCreateEvent>(_dataSocketSerializer); | |||
var data = e.Payload.ToObject<GuildCreateEvent>(_webSocket.Serializer); | |||
if (data.Unavailable != true) | |||
{ | |||
var server = _servers.GetOrAdd(data.Id); | |||
@@ -305,7 +533,7 @@ namespace Discord | |||
break; | |||
case "GUILD_UPDATE": | |||
{ | |||
var data = e.Payload.ToObject<GuildUpdateEvent>(_dataSocketSerializer); | |||
var data = e.Payload.ToObject<GuildUpdateEvent>(_webSocket.Serializer); | |||
var server = _servers[data.Id]; | |||
if (server != null) | |||
{ | |||
@@ -316,7 +544,7 @@ namespace Discord | |||
break; | |||
case "GUILD_DELETE": | |||
{ | |||
var data = e.Payload.ToObject<GuildDeleteEvent>(_dataSocketSerializer); | |||
var data = e.Payload.ToObject<GuildDeleteEvent>(_webSocket.Serializer); | |||
var server = _servers.TryRemove(data.Id); | |||
if (server != null) | |||
{ | |||
@@ -331,7 +559,7 @@ namespace Discord | |||
//Channels | |||
case "CHANNEL_CREATE": | |||
{ | |||
var data = e.Payload.ToObject<ChannelCreateEvent>(_dataSocketSerializer); | |||
var data = e.Payload.ToObject<ChannelCreateEvent>(_webSocket.Serializer); | |||
Channel channel; | |||
if (data.IsPrivate) | |||
{ | |||
@@ -347,7 +575,7 @@ namespace Discord | |||
break; | |||
case "CHANNEL_UPDATE": | |||
{ | |||
var data = e.Payload.ToObject<ChannelUpdateEvent>(_dataSocketSerializer); | |||
var data = e.Payload.ToObject<ChannelUpdateEvent>(_webSocket.Serializer); | |||
var channel = _channels[data.Id]; | |||
if (channel != null) | |||
{ | |||
@@ -358,7 +586,7 @@ namespace Discord | |||
break; | |||
case "CHANNEL_DELETE": | |||
{ | |||
var data = e.Payload.ToObject<ChannelDeleteEvent>(_dataSocketSerializer); | |||
var data = e.Payload.ToObject<ChannelDeleteEvent>(_webSocket.Serializer); | |||
var channel = _channels.TryRemove(data.Id); | |||
if (channel != null) | |||
RaiseChannelDestroyed(channel); | |||
@@ -368,7 +596,7 @@ namespace Discord | |||
//Members | |||
case "GUILD_MEMBER_ADD": | |||
{ | |||
var data = e.Payload.ToObject<MemberAddEvent>(_dataSocketSerializer); | |||
var data = e.Payload.ToObject<MemberAddEvent>(_webSocket.Serializer); | |||
var user = _users.GetOrAdd(data.User.Id, data.GuildId); | |||
user.Update(data); | |||
if (Config.TrackActivity) | |||
@@ -378,7 +606,7 @@ namespace Discord | |||
break; | |||
case "GUILD_MEMBER_UPDATE": | |||
{ | |||
var data = e.Payload.ToObject<MemberUpdateEvent>(_dataSocketSerializer); | |||
var data = e.Payload.ToObject<MemberUpdateEvent>(_webSocket.Serializer); | |||
var user = _users[data.User.Id, data.GuildId]; | |||
if (user != null) | |||
{ | |||
@@ -389,7 +617,7 @@ namespace Discord | |||
break; | |||
case "GUILD_MEMBER_REMOVE": | |||
{ | |||
var data = e.Payload.ToObject<MemberRemoveEvent>(_dataSocketSerializer); | |||
var data = e.Payload.ToObject<MemberRemoveEvent>(_webSocket.Serializer); | |||
var user = _users.TryRemove(data.UserId, data.GuildId); | |||
if (user != null) | |||
RaiseUserLeft(user); | |||
@@ -397,7 +625,7 @@ namespace Discord | |||
break; | |||
case "GUILD_MEMBERS_CHUNK": | |||
{ | |||
var data = e.Payload.ToObject<MembersChunkEvent>(_dataSocketSerializer); | |||
var data = e.Payload.ToObject<MembersChunkEvent>(_webSocket.Serializer); | |||
foreach (var memberData in data.Members) | |||
{ | |||
var user = _users.GetOrAdd(memberData.User.Id, memberData.GuildId); | |||
@@ -410,7 +638,7 @@ namespace Discord | |||
//Roles | |||
case "GUILD_ROLE_CREATE": | |||
{ | |||
var data = e.Payload.ToObject<RoleCreateEvent>(_dataSocketSerializer); | |||
var data = e.Payload.ToObject<RoleCreateEvent>(_webSocket.Serializer); | |||
var role = _roles.GetOrAdd(data.Data.Id, data.GuildId); | |||
role.Update(data.Data); | |||
var server = _servers[data.GuildId]; | |||
@@ -421,7 +649,7 @@ namespace Discord | |||
break; | |||
case "GUILD_ROLE_UPDATE": | |||
{ | |||
var data = e.Payload.ToObject<RoleUpdateEvent>(_dataSocketSerializer); | |||
var data = e.Payload.ToObject<RoleUpdateEvent>(_webSocket.Serializer); | |||
var role = _roles[data.Data.Id]; | |||
if (role != null) | |||
{ | |||
@@ -432,7 +660,7 @@ namespace Discord | |||
break; | |||
case "GUILD_ROLE_DELETE": | |||
{ | |||
var data = e.Payload.ToObject<RoleDeleteEvent>(_dataSocketSerializer); | |||
var data = e.Payload.ToObject<RoleDeleteEvent>(_webSocket.Serializer); | |||
var role = _roles.TryRemove(data.RoleId); | |||
if (role != null) | |||
{ | |||
@@ -447,7 +675,7 @@ namespace Discord | |||
//Bans | |||
case "GUILD_BAN_ADD": | |||
{ | |||
var data = e.Payload.ToObject<BanAddEvent>(_dataSocketSerializer); | |||
var data = e.Payload.ToObject<BanAddEvent>(_webSocket.Serializer); | |||
var server = _servers[data.GuildId]; | |||
if (server != null) | |||
{ | |||
@@ -459,7 +687,7 @@ namespace Discord | |||
break; | |||
case "GUILD_BAN_REMOVE": | |||
{ | |||
var data = e.Payload.ToObject<BanRemoveEvent>(_dataSocketSerializer); | |||
var data = e.Payload.ToObject<BanRemoveEvent>(_webSocket.Serializer); | |||
var server = _servers[data.GuildId]; | |||
if (server != null) | |||
{ | |||
@@ -473,7 +701,7 @@ namespace Discord | |||
//Messages | |||
case "MESSAGE_CREATE": | |||
{ | |||
var data = e.Payload.ToObject<MessageCreateEvent>(_dataSocketSerializer); | |||
var data = e.Payload.ToObject<MessageCreateEvent>(_webSocket.Serializer); | |||
Message msg = null; | |||
bool isAuthor = data.Author.Id == _userId; | |||
@@ -500,7 +728,7 @@ namespace Discord | |||
break; | |||
case "MESSAGE_UPDATE": | |||
{ | |||
var data = e.Payload.ToObject<MessageUpdateEvent>(_dataSocketSerializer); | |||
var data = e.Payload.ToObject<MessageUpdateEvent>(_webSocket.Serializer); | |||
var msg = _messages[data.Id]; | |||
if (msg != null) | |||
{ | |||
@@ -511,7 +739,7 @@ namespace Discord | |||
break; | |||
case "MESSAGE_DELETE": | |||
{ | |||
var data = e.Payload.ToObject<MessageDeleteEvent>(_dataSocketSerializer); | |||
var data = e.Payload.ToObject<MessageDeleteEvent>(_webSocket.Serializer); | |||
var msg = _messages.TryRemove(data.Id); | |||
if (msg != null) | |||
RaiseMessageDeleted(msg); | |||
@@ -519,7 +747,7 @@ namespace Discord | |||
break; | |||
case "MESSAGE_ACK": | |||
{ | |||
var data = e.Payload.ToObject<MessageAckEvent>(_dataSocketSerializer); | |||
var data = e.Payload.ToObject<MessageAckEvent>(_webSocket.Serializer); | |||
var msg = GetMessage(data.MessageId); | |||
if (msg != null) | |||
RaiseMessageReadRemotely(msg); | |||
@@ -529,7 +757,7 @@ namespace Discord | |||
//Statuses | |||
case "PRESENCE_UPDATE": | |||
{ | |||
var data = e.Payload.ToObject<PresenceUpdateEvent>(_dataSocketSerializer); | |||
var data = e.Payload.ToObject<PresenceUpdateEvent>(_webSocket.Serializer); | |||
var user = _users.GetOrAdd(data.User.Id, data.GuildId); | |||
if (user != null) | |||
{ | |||
@@ -540,7 +768,7 @@ namespace Discord | |||
break; | |||
case "TYPING_START": | |||
{ | |||
var data = e.Payload.ToObject<TypingStartEvent>(_dataSocketSerializer); | |||
var data = e.Payload.ToObject<TypingStartEvent>(_webSocket.Serializer); | |||
var channel = _channels[data.ChannelId]; | |||
if (channel != null) | |||
{ | |||
@@ -566,7 +794,7 @@ namespace Discord | |||
//Voice | |||
case "VOICE_STATE_UPDATE": | |||
{ | |||
var data = e.Payload.ToObject<MemberVoiceStateUpdateEvent>(_dataSocketSerializer); | |||
var data = e.Payload.ToObject<MemberVoiceStateUpdateEvent>(_webSocket.Serializer); | |||
var user = _users[data.UserId, data.GuildId]; | |||
if (user != null) | |||
{ | |||
@@ -585,7 +813,7 @@ namespace Discord | |||
//Settings | |||
case "USER_UPDATE": | |||
{ | |||
var data = e.Payload.ToObject<UserUpdateEvent>(_dataSocketSerializer); | |||
var data = e.Payload.ToObject<UserUpdateEvent>(_webSocket.Serializer); | |||
var user = _globalUsers[data.Id]; | |||
if (user != null) | |||
{ | |||
@@ -598,35 +826,61 @@ namespace Discord | |||
//Ignored | |||
case "USER_SETTINGS_UPDATE": | |||
case "GUILD_INTEGRATIONS_UPDATE": | |||
break; | |||
//Internal (handled in DataWebSocket) | |||
case "RESUMED": | |||
break; | |||
//Pass to DiscordWSClient | |||
case "VOICE_SERVER_UPDATE": | |||
await base.OnReceivedEvent(e).ConfigureAwait(false); | |||
break; | |||
case "RESUMED": //Handled in DataWebSocket | |||
break; | |||
//Others | |||
default: | |||
RaiseOnLog(LogMessageSeverity.Warning, LogMessageSource.DataWebSocket, $"Unknown message type: {e.Type}"); | |||
_webSocket.Logger.Log(LogSeverity.Warning, $"Unknown message type: {e.Type}"); | |||
break; | |||
} | |||
} | |||
catch (Exception ex) | |||
{ | |||
RaiseOnLog(LogMessageSeverity.Error, LogMessageSource.Client, $"Error handling {e.Type} event: {ex.GetBaseException().Message}"); | |||
_logger.Log(LogSeverity.Error, $"Error handling {e.Type} event", ex); | |||
} | |||
} | |||
private void SendInitialLog() | |||
{ | |||
if (_config.LogLevel >= LogMessageSeverity.Verbose) | |||
RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, $"Config: {JsonConvert.SerializeObject(_config)}"); | |||
if (_config.LogLevel >= LogSeverity.Verbose) | |||
_logger.Log(LogSeverity.Verbose, $"Config: {JsonConvert.SerializeObject(_config)}"); | |||
_sentInitialLog = true; | |||
} | |||
//Helpers | |||
/// <summary> Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications. </summary> | |||
public void Run(Func<Task> asyncAction) | |||
{ | |||
try | |||
{ | |||
asyncAction().GetAwaiter().GetResult(); //Avoids creating AggregateExceptions | |||
} | |||
catch (TaskCanceledException) { } | |||
_disconnectedEvent.WaitOne(); | |||
} | |||
/// <summary> Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications. </summary> | |||
public void Run() | |||
{ | |||
_disconnectedEvent.WaitOne(); | |||
} | |||
private void CheckReady() | |||
{ | |||
switch (_state) | |||
{ | |||
case (int)DiscordClientState.Disconnecting: | |||
throw new InvalidOperationException("The client is disconnecting."); | |||
case (int)DiscordClientState.Disconnected: | |||
throw new InvalidOperationException("The client is not connected to Discord"); | |||
case (int)DiscordClientState.Connecting: | |||
throw new InvalidOperationException("The client is connecting."); | |||
} | |||
} | |||
public void GetCacheStats(out int serverCount, out int channelCount, out int userCount, out int uniqueUserCount, out int messageCount, out int roleCount) | |||
{ | |||
@@ -1,18 +1,35 @@ | |||
namespace Discord | |||
{ | |||
public class DiscordClientConfig : DiscordWSClientConfig | |||
{ | |||
/// <summary> Gets or sets the time (in milliseconds) to wait when the message queue is empty before checking again. </summary> | |||
public int MessageQueueInterval { get { return _messageQueueInterval; } set { SetValue(ref _messageQueueInterval, value); } } | |||
private int _messageQueueInterval = 100; | |||
public class DiscordClientConfig : DiscordAPIClientConfig | |||
{ | |||
/// <summary> Max time in milliseconds to wait for DiscordClient to connect and initialize. </summary> | |||
public int ConnectionTimeout { get { return _connectionTimeout; } set { SetValue(ref _connectionTimeout, value); } } | |||
private int _connectionTimeout = 30000; | |||
/// <summary> Gets or sets the time (in milliseconds) to wait after an unexpected disconnect before reconnecting. </summary> | |||
public int ReconnectDelay { get { return _reconnectDelay; } set { SetValue(ref _reconnectDelay, value); } } | |||
private int _reconnectDelay = 1000; | |||
/// <summary> Gets or sets the time (in milliseconds) to wait after an reconnect fails before retrying. </summary> | |||
public int FailedReconnectDelay { get { return _failedReconnectDelay; } set { SetValue(ref _failedReconnectDelay, value); } } | |||
private int _failedReconnectDelay = 10000; | |||
/// <summary> Gets or sets the time (in milliseconds) to wait when the websocket's message queue is empty before checking again. </summary> | |||
public int WebSocketInterval { get { return _webSocketInterval; } set { SetValue(ref _webSocketInterval, value); } } | |||
private int _webSocketInterval = 100; | |||
/// <summary> Gets or sets the number of messages per channel that should be kept in cache. Setting this to zero disables the message cache entirely. </summary> | |||
public int MessageCacheLength { get { return _messageCacheLength; } set { SetValue(ref _messageCacheLength, value); } } | |||
private int _messageCacheLength = 100; | |||
//Experimental Features | |||
/// <summary> (Experimental) Instructs Discord to not send send information about offline users, for servers with more than 50 users. </summary> | |||
public bool UseLargeThreshold { get { return _useLargeThreshold; } set { SetValue(ref _useLargeThreshold, value); } } | |||
private bool _useLargeThreshold = false; | |||
//Experimental Features | |||
/// <summary> (Experimental) Enables or disables the internal message queue. This will allow SendMessage to return immediately and handle messages internally. Messages will set the IsQueued and HasFailed properties to show their progress. </summary> | |||
public bool UseMessageQueue { get { return _useMessageQueue; } set { SetValue(ref _useMessageQueue, value); } } | |||
private bool _useMessageQueue = false; | |||
/// <summary> Gets or sets the time (in milliseconds) to wait when the message queue is empty before checking again. </summary> | |||
public int MessageQueueInterval { get { return _messageQueueInterval; } set { SetValue(ref _messageQueueInterval, value); } } | |||
private int _messageQueueInterval = 100; | |||
/// <summary> (Experimental) Maintains the LastActivity property for users, showing when they last made an action (sent message, joined server, typed, etc). </summary> | |||
public bool TrackActivity { get { return _trackActivity; } set { SetValue(ref _trackActivity, value); } } | |||
private bool _trackActivity = true; | |||
@@ -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 | |||
{ | |||
private static readonly Regex _userRegex = new Regex(@"<@([0-9]+?)>", RegexOptions.Compiled); | |||
private static readonly Regex _channelRegex = new Regex(@"<#([0-9]+?)>", RegexOptions.Compiled); | |||
private static readonly Regex _userRegex = new Regex(@"<@([0-9]+)>", RegexOptions.Compiled); | |||
private static readonly Regex _channelRegex = new Regex(@"<#([0-9]+)>", RegexOptions.Compiled); | |||
private static readonly Regex _roleRegex = new Regex(@"@everyone", RegexOptions.Compiled); | |||
/// <summary> Returns the string used to create a user mention. </summary> | |||
@@ -4,7 +4,7 @@ using System.Threading.Tasks; | |||
namespace Discord | |||
{ | |||
public static class TaskExtensions | |||
internal static class TaskExtensions | |||
{ | |||
public static async Task Timeout(this Task task, int milliseconds) | |||
{ | |||
@@ -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> | |||
public bool IsMentioningMe { get; private set; } | |||
/// <summary> Returns true if the current user created this message. </summary> | |||
public bool IsAuthor => _client.CurrentUserId == _user.Id; | |||
public bool IsAuthor => _client.CurrentUser.Id == _user.Id; | |||
/// <summary> Returns true if the message was sent as text-to-speech by someone with permissions to do so. </summary> | |||
public bool IsTTS { get; private set; } | |||
/// <summary> Returns the state of this message. Only useful if UseMessageQueue is true. </summary> | |||
@@ -39,7 +39,7 @@ namespace Discord | |||
public string IconUrl => IconId != null ? Endpoints.ServerIcon(Id, IconId) : null; | |||
/// <summary> Returns true if the current user created this server. </summary> | |||
public bool IsOwner => _client.CurrentUserId == _owner.Id; | |||
public bool IsOwner => _client.CurrentUser.Id == _owner.Id; | |||
/// <summary> Returns the user that first created this server. </summary> | |||
[JsonIgnore] | |||
@@ -131,13 +131,13 @@ namespace Discord | |||
x => | |||
{ | |||
x.AddMember(this); | |||
if (Id == _client.CurrentUserId) | |||
if (Id == _client.CurrentUser.Id) | |||
x.CurrentUser = this; | |||
}, | |||
x => | |||
{ | |||
x.RemoveMember(this); | |||
if (Id == _client.CurrentUserId) | |||
if (Id == _client.CurrentUser.Id) | |||
x.CurrentUser = null; | |||
}); | |||
_voiceChannel = new Reference<Channel>(x => _client.Channels[x]); | |||
@@ -91,7 +91,7 @@ namespace Discord.Net.Rest | |||
if (content != null) | |||
requestJson = JsonConvert.SerializeObject(content); | |||
if (_config.LogLevel >= LogMessageSeverity.Verbose) | |||
if (_config.LogLevel >= LogSeverity.Verbose) | |||
stopwatch = Stopwatch.StartNew(); | |||
string responseJson = await _engine.Send(method, path, requestJson, _cancelToken).ConfigureAwait(false); | |||
@@ -101,10 +101,10 @@ namespace Discord.Net.Rest | |||
throw new Exception("API check failed: Response is not empty."); | |||
#endif | |||
if (_config.LogLevel >= LogMessageSeverity.Verbose) | |||
if (_config.LogLevel >= LogSeverity.Verbose) | |||
{ | |||
stopwatch.Stop(); | |||
if (content != null && _config.LogLevel >= LogMessageSeverity.Debug) | |||
if (content != null && _config.LogLevel >= LogSeverity.Debug) | |||
{ | |||
if (path.StartsWith(Endpoints.Auth)) | |||
RaiseOnRequest(method, path, "[Hidden]", stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond); | |||
@@ -130,7 +130,7 @@ namespace Discord.Net.Rest | |||
{ | |||
Stopwatch stopwatch = null; | |||
if (_config.LogLevel >= LogMessageSeverity.Verbose) | |||
if (_config.LogLevel >= LogSeverity.Verbose) | |||
stopwatch = Stopwatch.StartNew(); | |||
string responseJson = await _engine.SendFile(method, path, filename, stream, _cancelToken).ConfigureAwait(false); | |||
@@ -140,10 +140,10 @@ namespace Discord.Net.Rest | |||
throw new Exception("API check failed: Response is not empty."); | |||
#endif | |||
if (_config.LogLevel >= LogMessageSeverity.Verbose) | |||
if (_config.LogLevel >= LogSeverity.Verbose) | |||
{ | |||
stopwatch.Stop(); | |||
if (_config.LogLevel >= LogMessageSeverity.Debug) | |||
if (_config.LogLevel >= LogSeverity.Debug) | |||
RaiseOnRequest(method, path, filename, stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond); | |||
else | |||
RaiseOnRequest(method, path, null, stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond); | |||
@@ -26,8 +26,8 @@ namespace Discord.Net.WebSockets | |||
public string SessionId => _sessionId; | |||
private string _sessionId; | |||
public DataWebSocket(DiscordWSClient client) | |||
: base(client) | |||
public DataWebSocket(DiscordClient client, Logger logger) | |||
: base(client, logger) | |||
{ | |||
} | |||
@@ -72,7 +72,7 @@ namespace Discord.Net.WebSockets | |||
catch (OperationCanceledException) { throw; } | |||
catch (Exception ex) | |||
{ | |||
RaiseOnLog(LogMessageSeverity.Error, $"Reconnect failed: {ex.GetBaseException().Message}"); | |||
_logger.Log(LogSeverity.Error, $"Reconnect failed", ex); | |||
//Net is down? We can keep trying to reconnect until the user runs Disconnect() | |||
await Task.Delay(_client.Config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); | |||
} | |||
@@ -96,13 +96,13 @@ namespace Discord.Net.WebSockets | |||
JToken token = msg.Payload as JToken; | |||
if (msg.Type == "READY") | |||
{ | |||
var payload = token.ToObject<ReadyEvent>(_client.DataSocketSerializer); | |||
var payload = token.ToObject<ReadyEvent>(_serializer); | |||
_sessionId = payload.SessionId; | |||
_heartbeatInterval = payload.HeartbeatInterval; | |||
} | |||
else if (msg.Type == "RESUMED") | |||
{ | |||
var payload = token.ToObject<ResumedEvent>(_client.DataSocketSerializer); | |||
var payload = token.ToObject<ResumedEvent>(_serializer); | |||
_heartbeatInterval = payload.HeartbeatInterval; | |||
} | |||
RaiseReceivedEvent(msg.Type, token); | |||
@@ -112,19 +112,19 @@ namespace Discord.Net.WebSockets | |||
break; | |||
case OpCodes.Redirect: | |||
{ | |||
var payload = (msg.Payload as JToken).ToObject<RedirectEvent>(_client.DataSocketSerializer); | |||
var payload = (msg.Payload as JToken).ToObject<RedirectEvent>(_serializer); | |||
if (payload.Url != null) | |||
{ | |||
Host = payload.Url; | |||
if (_logLevel >= LogMessageSeverity.Info) | |||
RaiseOnLog(LogMessageSeverity.Info, "Redirected to " + payload.Url); | |||
if (_logger.Level >= LogSeverity.Info) | |||
_logger.Log(LogSeverity.Info, "Redirected to " + payload.Url); | |||
await Redirect(payload.Url).ConfigureAwait(false); | |||
} | |||
} | |||
break; | |||
default: | |||
if (_logLevel >= LogMessageSeverity.Warning) | |||
RaiseOnLog(LogMessageSeverity.Warning, $"Unknown Opcode: {opCode}"); | |||
if (_logger.Level >= LogSeverity.Warning) | |||
_logger.Log(LogSeverity.Warning, $"Unknown Opcode: {opCode}"); | |||
break; | |||
} | |||
} | |||
@@ -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 | |||
{ | |||
protected readonly IWebSocketEngine _engine; | |||
protected readonly DiscordWSClient _client; | |||
protected readonly LogMessageSeverity _logLevel; | |||
protected readonly DiscordClient _client; | |||
protected readonly ManualResetEventSlim _connectedEvent; | |||
protected ExceptionDispatchInfo _disconnectReason; | |||
@@ -38,24 +37,48 @@ namespace Discord.Net.WebSockets | |||
private CancellationTokenSource _cancelTokenSource; | |||
protected CancellationToken _cancelToken; | |||
public string Host { get; set; } | |||
internal JsonSerializer Serializer => _serializer; | |||
protected JsonSerializer _serializer; | |||
public Logger Logger => _logger; | |||
protected readonly Logger _logger; | |||
public string Host { get { return _host; } set { _host = value; } } | |||
private string _host; | |||
public WebSocketState State => (WebSocketState)_state; | |||
protected int _state; | |||
public WebSocket(DiscordWSClient client) | |||
public event EventHandler Connected; | |||
private void RaiseConnected() | |||
{ | |||
if (_logger.Level >= LogSeverity.Info) | |||
_logger.Log(LogSeverity.Info, "Connected"); | |||
if (Connected != null) | |||
Connected(this, EventArgs.Empty); | |||
} | |||
public event EventHandler<DisconnectedEventArgs> Disconnected; | |||
private void RaiseDisconnected(bool wasUnexpected, Exception error) | |||
{ | |||
if (_logger.Level >= LogSeverity.Info) | |||
_logger.Log(LogSeverity.Info, "Disconnected"); | |||
if (Disconnected != null) | |||
Disconnected(this, new DisconnectedEventArgs(wasUnexpected, error)); | |||
} | |||
public WebSocket(DiscordClient client, Logger logger) | |||
{ | |||
_client = client; | |||
_logLevel = client.Config.LogLevel; | |||
_logger = logger; | |||
_loginTimeout = client.Config.ConnectionTimeout; | |||
_cancelToken = new CancellationToken(true); | |||
_connectedEvent = new ManualResetEventSlim(false); | |||
#if !DOTNET5_4 | |||
_engine = new WebSocketSharpEngine(this, client.Config); | |||
_engine = new WebSocketSharpEngine(this, client.Config, _logger); | |||
#else | |||
//_engine = new BuiltInWebSocketEngine(this, client.Config); | |||
//_engine = new BuiltInWebSocketEngine(this, client.Config, _logger); | |||
#endif | |||
_engine.BinaryMessage += (s, e) => | |||
{ | |||
@@ -73,6 +96,19 @@ namespace Discord.Net.WebSockets | |||
{ | |||
/*await*/ ProcessMessage(e.Message).Wait(); | |||
}; | |||
_serializer = new JsonSerializer(); | |||
_serializer.DateTimeZoneHandling = DateTimeZoneHandling.Utc; | |||
#if TEST_RESPONSES | |||
_serializer.CheckAdditionalContent = true; | |||
_serializer.MissingMemberHandling = MissingMemberHandling.Error; | |||
#else | |||
_serializer.Error += (s, e) => | |||
{ | |||
e.ErrorContext.Handled = true; | |||
_logger.Log(LogSeverity.Error, "Serialization Failed", e.ErrorContext.Error); | |||
}; | |||
#endif | |||
} | |||
protected async Task BeginConnect() | |||
@@ -94,25 +130,6 @@ namespace Discord.Net.WebSockets | |||
throw; | |||
} | |||
} | |||
protected virtual async Task Start() | |||
{ | |||
try | |||
{ | |||
if (_state != (int)WebSocketState.Connecting) | |||
throw new InvalidOperationException("Socket is in the wrong state."); | |||
_lastHeartbeat = DateTime.UtcNow; | |||
await _engine.Connect(Host, _cancelToken).ConfigureAwait(false); | |||
_runTask = RunTasks(); | |||
} | |||
catch (Exception ex) | |||
{ | |||
await DisconnectInternal(ex, isUnexpected: false).ConfigureAwait(false); | |||
throw; | |||
} | |||
} | |||
protected void EndConnect() | |||
{ | |||
_state = (int)WebSocketState.Connected; | |||
@@ -145,7 +162,7 @@ namespace Discord.Net.WebSockets | |||
_cancelTokenSource.Cancel(); | |||
if (_disconnectState == WebSocketState.Connecting) //_runTask was never made | |||
await Cleanup().ConfigureAwait(false); | |||
await Stop().ConfigureAwait(false); | |||
} | |||
if (!skipAwait) | |||
@@ -156,6 +173,25 @@ namespace Discord.Net.WebSockets | |||
} | |||
} | |||
protected virtual async Task Start() | |||
{ | |||
try | |||
{ | |||
if (_state != (int)WebSocketState.Connecting) | |||
throw new InvalidOperationException("Socket is in the wrong state."); | |||
_lastHeartbeat = DateTime.UtcNow; | |||
await _engine.Connect(Host, _cancelToken).ConfigureAwait(false); | |||
_runTask = RunTasks(); | |||
} | |||
catch (Exception ex) | |||
{ | |||
await DisconnectInternal(ex, isUnexpected: false).ConfigureAwait(false); | |||
throw; | |||
} | |||
} | |||
protected virtual async Task RunTasks() | |||
{ | |||
Task[] tasks = GetTasks().ToArray(); | |||
@@ -174,7 +210,7 @@ namespace Discord.Net.WebSockets | |||
catch { } | |||
//Start cleanup | |||
await Cleanup().ConfigureAwait(false); | |||
await Stop().ConfigureAwait(false); | |||
} | |||
protected virtual IEnumerable<Task> GetTasks() | |||
{ | |||
@@ -182,7 +218,8 @@ namespace Discord.Net.WebSockets | |||
return _engine.GetTasks(cancelToken) | |||
.Concat(new Task[] { HeartbeatAsync(cancelToken) }); | |||
} | |||
protected virtual async Task Cleanup() | |||
protected virtual async Task Stop() | |||
{ | |||
var disconnectState = _disconnectState; | |||
_disconnectState = WebSocketState.Disconnected; | |||
@@ -203,8 +240,8 @@ namespace Discord.Net.WebSockets | |||
protected virtual Task ProcessMessage(string json) | |||
{ | |||
if (_logLevel >= LogMessageSeverity.Debug) | |||
RaiseOnLog(LogMessageSeverity.Debug, $"In: {json}"); | |||
if (_logger.Level >= LogSeverity.Debug) | |||
_logger.Log(LogSeverity.Debug, $"In: {json}"); | |||
return TaskHelper.CompletedTask; | |||
} | |||
protected abstract object GetKeepAlive(); | |||
@@ -212,8 +249,8 @@ namespace Discord.Net.WebSockets | |||
protected void QueueMessage(object message) | |||
{ | |||
string json = JsonConvert.SerializeObject(message); | |||
if (_logLevel >= LogMessageSeverity.Debug) | |||
RaiseOnLog(LogMessageSeverity.Debug, $"Out: " + json); | |||
if (_logger.Level >= LogSeverity.Debug) | |||
_logger.Log(LogSeverity.Debug, $"Out: " + json); | |||
_engine.QueueMessage(json); | |||
} | |||
@@ -10,7 +10,8 @@ namespace Discord.Net.WebSockets | |||
{ | |||
internal class WebSocketSharpEngine : IWebSocketEngine | |||
{ | |||
private readonly DiscordWSClientConfig _config; | |||
private readonly DiscordClientConfig _config; | |||
private readonly Logger _logger; | |||
private readonly ConcurrentQueue<string> _sendQueue; | |||
private readonly WebSocket _parent; | |||
private WSSharpWebSocket _webSocket; | |||
@@ -28,10 +29,11 @@ namespace Discord.Net.WebSockets | |||
TextMessage(this, new WebSocketTextMessageEventArgs(msg)); | |||
} | |||
internal WebSocketSharpEngine(WebSocket parent, DiscordWSClientConfig config) | |||
internal WebSocketSharpEngine(WebSocket parent, DiscordClientConfig config, Logger logger) | |||
{ | |||
_parent = parent; | |||
_config = config; | |||
_logger = logger; | |||
_sendQueue = new ConcurrentQueue<string>(); | |||
} | |||
@@ -51,7 +53,7 @@ namespace Discord.Net.WebSockets | |||
}; | |||
_webSocket.OnError += async (s, e) => | |||
{ | |||
_parent.RaiseOnLog(LogMessageSeverity.Error, e.Exception?.GetBaseException()?.Message ?? e.Message); | |||
_logger.Log(LogSeverity.Error, "WebSocket Error", e.Exception); | |||
await _parent.DisconnectInternal(e.Exception, skipAwait: true).ConfigureAwait(false); | |||
}; | |||
_webSocket.OnClose += async (s, e) => | |||
@@ -61,7 +63,7 @@ namespace Discord.Net.WebSockets | |||
Exception ex = new Exception($"Got Close Message ({code}): {reason}"); | |||
await _parent.DisconnectInternal(ex, skipAwait: true).ConfigureAwait(false); | |||
}; | |||
_webSocket.Log.Output = (e, m) => { }; //Dont let websocket-sharp print to console | |||
_webSocket.Log.Output = (e, m) => { }; //Dont let websocket-sharp print to console directly | |||
_webSocket.Connect(); | |||
return TaskHelper.CompletedTask; | |||
} | |||
@@ -4,7 +4,7 @@ namespace Discord | |||
{ | |||
public sealed class TimeoutException : OperationCanceledException | |||
{ | |||
internal TimeoutException() | |||
public TimeoutException() | |||
: base("An operation has timed out.") | |||
{ | |||
} | |||