| @@ -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.") | ||||
| { | { | ||||
| } | } | ||||