Moved logging to its own service, more audio isolation prep
This commit is contained in:
@@ -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 enum LogSeverity : byte
|
||||
{
|
||||
Error = 1,
|
||||
Warning = 2,
|
||||
Info = 3,
|
||||
Verbose = 4,
|
||||
Debug = 5
|
||||
}
|
||||
|
||||
public class DiscordAPIClientConfig
|
||||
{
|
||||
internal static readonly string UserAgent = $"Discord.Net/{DiscordClient.Version} (https://github.com/RogueException/Discord.Net)";
|
||||
|
||||
/// <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
|
||||
{
|
||||
/// <summary> Provides a connection to the DiscordApp service. </summary>
|
||||
public sealed partial class DiscordClient : DiscordWSClient
|
||||
public enum DiscordClientState : byte
|
||||
{
|
||||
public static readonly string Version = typeof(DiscordClientConfig).GetTypeInfo().Assembly.GetName().Version.ToString(3);
|
||||
Disconnected,
|
||||
Connecting,
|
||||
Connected,
|
||||
Disconnecting
|
||||
}
|
||||
|
||||
private readonly DiscordAPIClient _api;
|
||||
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 partial class DiscordClient
|
||||
{
|
||||
public static readonly string Version = typeof(DiscordClient).GetTypeInfo().Assembly.GetName().Version.ToString(3);
|
||||
|
||||
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();
|
||||
_state = (int)DiscordClientState.Disconnected;
|
||||
_status = UserStatus.Online;
|
||||
|
||||
//Services
|
||||
_singletons = new Dictionary<Type, object>();
|
||||
_log = AddService(new LogService());
|
||||
CreateMainLogger();
|
||||
|
||||
//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>();
|
||||
|
||||
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);
|
||||
_singletons = new Dictionary<Type, object>();
|
||||
|
||||
_status = UserStatus.Online;
|
||||
|
||||
this.Connected += async (s, e) =>
|
||||
{
|
||||
_api.CancelToken = _cancelToken;
|
||||
await SendStatus().ConfigureAwait(false);
|
||||
};
|
||||
|
||||
if (_config.LogLevel >= LogMessageSeverity.Info)
|
||||
{
|
||||
JoinedServer += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
|
||||
$"Server Created: {e.Server?.Name ?? "[Private]"}");
|
||||
LeftServer += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
|
||||
$"Server Destroyed: {e.Server?.Name ?? "[Private]"}");
|
||||
ServerUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
|
||||
$"Server Updated: {e.Server?.Name ?? "[Private]"}");
|
||||
ServerAvailable += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
|
||||
$"Server Available: {e.Server?.Name ?? "[Private]"}");
|
||||
ServerUnavailable += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
|
||||
$"Server Unavailable: {e.Server?.Name ?? "[Private]"}");
|
||||
ChannelCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
|
||||
$"Channel Created: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}");
|
||||
ChannelDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
|
||||
$"Channel Destroyed: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}");
|
||||
ChannelUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
|
||||
$"Channel Updated: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}");
|
||||
MessageReceived += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
|
||||
$"Message Received: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}");
|
||||
MessageDeleted += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
|
||||
$"Message Deleted: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}");
|
||||
MessageUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
|
||||
$"Message Update: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}");
|
||||
RoleCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
|
||||
$"Role Created: {e.Server?.Name ?? "[Private]"}/{e.Role?.Name}");
|
||||
RoleUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
|
||||
$"Role Updated: {e.Server?.Name ?? "[Private]"}/{e.Role?.Name}");
|
||||
RoleDeleted += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
|
||||
$"Role Deleted: {e.Server?.Name ?? "[Private]"}/{e.Role?.Name}");
|
||||
UserBanned += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
|
||||
$"Banned User: {e.Server?.Name ?? "[Private]" }/{e.UserId}");
|
||||
UserUnbanned += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
|
||||
$"Unbanned User: {e.Server?.Name ?? "[Private]"}/{e.UserId}");
|
||||
UserJoined += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
|
||||
$"User Joined: {e.Server?.Name ?? "[Private]"}/{e.User.Name}");
|
||||
UserLeft += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
|
||||
$"User Left: {e.Server?.Name ?? "[Private]"}/{e.User.Name}");
|
||||
UserUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
|
||||
$"User Updated: {e.Server?.Name ?? "[Private]"}/{e.User.Name}");
|
||||
UserVoiceStateUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
|
||||
$"User Updated (Voice State): {e.Server?.Name ?? "[Private]"}/{e.User.Name}");
|
||||
ProfileUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
|
||||
"Profile Updated");
|
||||
}
|
||||
if (_config.LogLevel >= LogMessageSeverity.Verbose)
|
||||
{
|
||||
UserIsTypingUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client,
|
||||
$"User Updated (Is Typing): {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.User?.Name}");
|
||||
MessageReadRemotely += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client,
|
||||
$"Read Message (Remotely): {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}");
|
||||
MessageSent += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client,
|
||||
$"Sent Message: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}");
|
||||
UserPresenceUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client,
|
||||
$"User Updated (Presence): {e.Server?.Name ?? "[Private]"}/{e.User?.Name}");
|
||||
|
||||
_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})");
|
||||
else
|
||||
RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Rest, $"{e.Method} {e.Path}: {Math.Round(e.ElapsedMilliseconds, 2)} ms");
|
||||
};
|
||||
}
|
||||
if (_config.LogLevel >= LogMessageSeverity.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");
|
||||
}
|
||||
CreateRestLogger();
|
||||
|
||||
if (Config.UseMessageQueue)
|
||||
_pendingMessages = new ConcurrentQueue<Message>();
|
||||
|
||||
//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) => _logger.Log(LogSeverity.Info,
|
||||
$"Server Created: {e.Server?.Name ?? "[Private]"}");
|
||||
LeftServer += (s, e) => _logger.Log(LogSeverity.Info,
|
||||
$"Server Destroyed: {e.Server?.Name ?? "[Private]"}");
|
||||
ServerUpdated += (s, e) => _logger.Log(LogSeverity.Info,
|
||||
$"Server Updated: {e.Server?.Name ?? "[Private]"}");
|
||||
ServerAvailable += (s, e) => _logger.Log(LogSeverity.Info,
|
||||
$"Server Available: {e.Server?.Name ?? "[Private]"}");
|
||||
ServerUnavailable += (s, e) => _logger.Log(LogSeverity.Info,
|
||||
$"Server Unavailable: {e.Server?.Name ?? "[Private]"}");
|
||||
ChannelCreated += (s, e) => _logger.Log(LogSeverity.Info,
|
||||
$"Channel Created: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}");
|
||||
ChannelDestroyed += (s, e) => _logger.Log(LogSeverity.Info,
|
||||
$"Channel Destroyed: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}");
|
||||
ChannelUpdated += (s, e) => _logger.Log(LogSeverity.Info,
|
||||
$"Channel Updated: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}");
|
||||
MessageReceived += (s, e) => _logger.Log(LogSeverity.Info,
|
||||
$"Message Received: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}");
|
||||
MessageDeleted += (s, e) => _logger.Log(LogSeverity.Info,
|
||||
$"Message Deleted: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}");
|
||||
MessageUpdated += (s, e) => _logger.Log(LogSeverity.Info,
|
||||
$"Message Update: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}");
|
||||
RoleCreated += (s, e) => _logger.Log(LogSeverity.Info,
|
||||
$"Role Created: {e.Server?.Name ?? "[Private]"}/{e.Role?.Name}");
|
||||
RoleUpdated += (s, e) => _logger.Log(LogSeverity.Info,
|
||||
$"Role Updated: {e.Server?.Name ?? "[Private]"}/{e.Role?.Name}");
|
||||
RoleDeleted += (s, e) => _logger.Log(LogSeverity.Info,
|
||||
$"Role Deleted: {e.Server?.Name ?? "[Private]"}/{e.Role?.Name}");
|
||||
UserBanned += (s, e) => _logger.Log(LogSeverity.Info,
|
||||
$"Banned User: {e.Server?.Name ?? "[Private]" }/{e.UserId}");
|
||||
UserUnbanned += (s, e) => _logger.Log(LogSeverity.Info,
|
||||
$"Unbanned User: {e.Server?.Name ?? "[Private]"}/{e.UserId}");
|
||||
UserJoined += (s, e) => _logger.Log(LogSeverity.Info,
|
||||
$"User Joined: {e.Server?.Name ?? "[Private]"}/{e.User.Name}");
|
||||
UserLeft += (s, e) => _logger.Log(LogSeverity.Info,
|
||||
$"User Left: {e.Server?.Name ?? "[Private]"}/{e.User.Name}");
|
||||
UserUpdated += (s, e) => _logger.Log(LogSeverity.Info,
|
||||
$"User Updated: {e.Server?.Name ?? "[Private]"}/{e.User.Name}");
|
||||
UserVoiceStateUpdated += (s, e) => _logger.Log(LogSeverity.Info,
|
||||
$"User Updated (Voice State): {e.Server?.Name ?? "[Private]"}/{e.User.Name}");
|
||||
ProfileUpdated += (s, e) => _logger.Log(LogSeverity.Info,
|
||||
"Profile Updated");
|
||||
}
|
||||
if (_log.Level >= LogSeverity.Verbose)
|
||||
{
|
||||
UserIsTypingUpdated += (s, e) => _logger.Log(LogSeverity.Verbose,
|
||||
$"User Updated (Is Typing): {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.User?.Name}");
|
||||
MessageReadRemotely += (s, e) => _logger.Log(LogSeverity.Verbose,
|
||||
$"Read Message (Remotely): {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}");
|
||||
MessageSent += (s, e) => _logger.Log(LogSeverity.Verbose,
|
||||
$"Sent Message: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}");
|
||||
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)
|
||||
_restLogger.Log(LogSeverity.Verbose, $"{e.Method} {e.Path}: {Math.Round(e.ElapsedMilliseconds, 2)} ms ({e.Payload})");
|
||||
else
|
||||
_restLogger.Log(LogSeverity.Verbose, $"{e.Method} {e.Path}: {Math.Round(e.ElapsedMilliseconds, 2)} ms");
|
||||
};
|
||||
}
|
||||
}
|
||||
private void CreateCacheLogger()
|
||||
{
|
||||
_cacheLogger = _log.CreateLogger("Cache");
|
||||
if (_log.Level >= LogSeverity.Debug)
|
||||
{
|
||||
_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");
|
||||
}
|
||||
}
|
||||
|
||||
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}");
|
||||
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}");
|
||||
|
||||
await base.Connect(gateway, token)
|
||||
.Timeout(_config.ConnectionTimeout)
|
||||
.ConfigureAwait(false);
|
||||
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)
|
||||
{
|
||||
|
||||
61
src/Discord.Net/LogService.cs
Normal file
61
src/Discord.Net/LogService.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
|
||||
namespace Discord
|
||||
{
|
||||
public class LogService : IService
|
||||
{
|
||||
public DiscordClient Client => _client;
|
||||
private DiscordClient _client;
|
||||
|
||||
public LogSeverity Level => _level;
|
||||
private LogSeverity _level;
|
||||
|
||||
public event EventHandler<LogMessageEventArgs> LogMessage;
|
||||
internal void RaiseLogMessage(LogMessageEventArgs e)
|
||||
{
|
||||
if (LogMessage != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
LogMessage(this, e);
|
||||
}
|
||||
catch { } //We dont want to log on log errors
|
||||
}
|
||||
}
|
||||
|
||||
void IService.Install(DiscordClient client)
|
||||
{
|
||||
_client = client;
|
||||
_level = client.Config.LogLevel;
|
||||
}
|
||||
|
||||
public Logger CreateLogger(string source)
|
||||
{
|
||||
return new Logger(this, source);
|
||||
}
|
||||
}
|
||||
|
||||
public class Logger
|
||||
{
|
||||
private LogService _service;
|
||||
|
||||
public LogSeverity Level => _level;
|
||||
private LogSeverity _level;
|
||||
|
||||
public string Source => _source;
|
||||
private string _source;
|
||||
|
||||
internal Logger(LogService service, string source)
|
||||
{
|
||||
_service = service;
|
||||
_level = service.Level;
|
||||
_source = source;
|
||||
}
|
||||
|
||||
public void Log(LogSeverity severity, string message, Exception exception = null)
|
||||
{
|
||||
if (severity >= _service.Level)
|
||||
_service.RaiseLogMessage(new LogMessageEventArgs(severity, _source, message, exception));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.")
|
||||
{
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user