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