@@ -3,6 +3,7 @@ using Discord.Helpers;
using Discord.Net;
using Discord.Net.API;
using Discord.Net.WebSockets;
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Net;
@@ -30,6 +31,7 @@ namespace Discord
private readonly ConcurrentQueue<Message> _pendingMessages;
private readonly ManualResetEvent _disconnectedEvent;
private readonly ManualResetEventSlim _connectedEvent;
private readonly JsonSerializer _serializer;
private Task _runTask;
protected ExceptionDispatchInfo _disconnectReason;
private bool _wasDisconnectUnexpected;
@@ -78,7 +80,7 @@ namespace Discord
_dataSocket = new DataWebSocket(this);
_dataSocket.Connected += (s, e) => { if (_state == (int)DiscordClientState.Connecting) CompleteConnect(); };
_voiceSocket = new VoiceWebSocket(this);
_channels = new Channels(this);
_members = new Members(this);
_messages = new Messages(this);
@@ -87,47 +89,380 @@ namespace Discord
_users = new Users(this);
_dataSocket.LogMessage += (s, e) => RaiseOnLog(e.Severity, LogMessageSource.DataWebSocket, e.Message);
_voiceSocket.LogMessage += (s, e) => RaiseOnLog(e.Severity, LogMessageSource.Data WebSocket, e.Message);
_voiceSocket.LogMessage += (s, e) => RaiseOnLog(e.Severity, LogMessageSource.Voice WebSocket, e.Message);
if (_config.LogLevel >= LogMessageSeverity.Info)
{
_dataSocket.Connected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.DataWebSocket, "Connected");
_dataSocket.Disconnected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.DataWebSocket, "Disconnected");
_dataSocket.ReceievedEvent += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.DataWebSocket, $"Receieved {e.Type}");
_voiceSocket.Connected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.VoiceWebSocket, "Connected");
_voiceSocket.Disconnected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.VoiceWebSocket, "Disconnected");
}
if (_config.LogLevel >= LogMessageSeverity.Verbose)
{
Connected += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, "Connected");
Disconnected += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, "Disconnected");
ServerCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, $"Created Server: {e.Server.Name} ({e.Server.Id})");
ServerDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, $"Destroyed Server: {e.Server.Name} ({e.Server.Id})");
ServerUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, $"Updated Server: {e.Server.Name} ({e.Server.Id})");
UserUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, $"Updated User: {e.User.Name} ({e.UserId})");
ChannelCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, $"Created Channel: {e.Server.Name}/{e.Channel.Name} ({e.ServerId}/{e.ChannelId})");
ChannelDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, $"Destroyed Channel: {e.Server.Name}/{e.Channel.Name} ({e.ServerId}/{e.ChannelId})");
ChannelUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, $"Updated Channel: {e.Server.Name}/{e.Channel.Name} ({e.ServerId}/{e.ChannelId})");
MessageCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, $"Created Message: {e.Server.Name}/{e.Channel.Name}/{e.MessageId} ({e.ServerId}/{e.ChannelId}/{e.MessageId})");
MessageDeleted += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, $"Deleted Message: {e.Server.Name}/{e.Channel.Name}/{e.MessageId} ({e.ServerId}/{e.ChannelId}/{e.MessageId})");
MessageUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, $"Updated Message: {e.Server.Name}/{e.Channel.Name}/{e.MessageId} ({e.ServerId}/{e.ChannelId}/{e.MessageId})");
MessageReadRemotely += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, $"Read Message (Remotely): {e.Server.Name}/{e.Channel.Name}/{e.MessageId} ({e.ServerId}/{e.ChannelId}/{e.MessageId})");
MessageSent += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, $"Sent Message: {e.Server.Name}/{e.Channel.Name}/{e.MessageId} ({e.ServerId}/{e.ChannelId}/{e.MessageId})");
RoleCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, $"Created Role: {e.Server.Name}/{e.Role.Name} ({e.ServerId}/{e.RoleId}).");
RoleUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, $"Update Role: {e.Server.Name}/{e.Role.Name} ({e.ServerId}/{e.RoleId}).");
RoleDeleted += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, $"Deleted Role: {e.Server.Name}/{e.Role.Name} ({e.ServerId}/{e.RoleId}).");
BanAdded += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, $"Added Ban: {e.Server.Name}/{e.User?.Name ?? "Unknown"} ({e.ServerId}/{e.UserId}).");
BanRemoved += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, $"Removed Ban: {e.Server.Name}/{e.User?.Name ?? "Unknown"} ({e.ServerId}/{e.UserId}).");
MemberAdded += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, $"Added Member: {e.Server.Name}/{e.User.Name} ({e.ServerId}/{e.UserId}).");
MemberRemoved += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, $"Removed Member: {e.Server.Name}/{e.User.Name} ({e.ServerId}/{e.UserId}).");
MemberUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, $"Updated Member: {e.Server.Name}/{e.User.Name} ({e.ServerId}/{e.UserId}).");
MemberPresenceUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, $"Updated Member (Presence): {e.Server.Name}/{e.User.Name} ({e.ServerId}/{e.UserId})");
MemberVoiceStateUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, $"Updated Member (Voice State): {e.Server.Name}/{e.User.Name} ({e.ServerId}/{e.UserId})");
MemberIsTyping += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, $"Updated Member (Is Typing): {e.Server.Name}/{e.Channel.Name}/{e.User.Name} ({e.ServerId}/{e.ChannelId}/{e.UserId})");
VoiceConnected += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, $"Voice Connected");
VoiceDisconnected += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, $"Voice Disconnected");
_channels.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Created Channel {e.Item.ServerId}/{e.Item.Id}");
_channels.ItemUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Updated Channel {e.Item.ServerId}/{e.Item.Id}");
_channels.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Destroyed Channel {e.Item.ServerId}/{e.Item.Id}");
_channels.Cleared += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Cleared Channels");
_members.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Created Member {e.Item.ServerId}/{e.Item.UserId}");
_members.ItemUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Updated Member {e.Item.ServerId}/{e.Item.UserId}");
_members.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Destroyed Member {e.Item.ServerId}/{e.Item.UserId}");
_members.Cleared += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Cleared Members");
_messages.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Created Message {e.Item.ServerId}/{e.Item.ChannelId}/{e.Item.Id}");
_messages.ItemUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Updated Message {e.Item.ServerId}/{e.Item.ChannelId}/{e.Item.Id}");
_messages.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Destroyed Message {e.Item.ServerId}/{e.Item.ChannelId}/{e.Item.Id}");
_messages.ItemRemapped += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Remapped Message {e.Item.ServerId}/{e.Item.ChannelId}/[{e.OldId} -> {e.NewId}]");
_messages.Cleared += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Cleared Members");
_roles.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Created Role {e.Item.ServerId}/{e.Item.Id}");
_roles.ItemUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Updated Role {e.Item.ServerId}/{e.Item.Id}");
_roles.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Destroyed Role {e.Item.ServerId}/{e.Item.Id}");
_roles.Cleared += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Cleared Members");
_servers.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Created Server {e.Item.Id}");
_servers.ItemUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Updated Server {e.Item.Id}");
_servers.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Destroyed Server {e.Item.Id}");
_servers.Cleared += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Cleared Members");
_users.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Created User {e.Item.Id}");
_users.ItemUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Updated User {e.Item.Id}");
_users.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Destroyed User {e.Item.Id}");
_users.Cleared += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Cache, $"Cleared Members");
_api.RestClient.OnRequest += (s, e) =>
{
if (e.Payload != null)
RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Rest, $"{e.Method.Method} {e.Path}: {Math.Round(e.ElapsedMilliseconds, 2)} ({e.Payload})");
RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Rest, $"{e.Method.Method} {e.Path}: {Math.Round(e.ElapsedMilliseconds, 2)} ms ({e.Payload})");
else
RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Rest, $"{e.Method.Method} {e.Path}: {Math.Round(e.ElapsedMilliseconds, 2)}");
RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Rest, $"{e.Method.Method} {e.Path}: {Math.Round(e.ElapsedMilliseconds, 2)} ms ");
};
}
if (_config.UseMessageQueue)
_pendingMessages = new ConcurrentQueue<Message>();
}
_serializer = new JsonSerializer();
#if TEST_RESPONSES
_serializer.CheckAdditionalContent = true;
_serializer.MissingMemberHandling = MissingMemberHandling.Error;
#endif
_dataSocket.ReceievedEvent += async (s, e) =>
{
switch (e.Type)
{
//Global
case "READY": //Resync
{
var data = e.Payload.ToObject<Events.Ready>(_serializer);
_servers.Clear();
_channels.Clear();
_users.Clear();
_currentUserId = data.User.Id;
_currentUser = _users.GetOrAdd(data.User.Id);
_currentUser.Update(data.User);
foreach (var model in data.Guilds)
{
var server = _servers.GetOrAdd(model.Id);
server.Update(model);
}
foreach (var model in data.PrivateChannels)
{
var channel = _channels.GetOrAdd(model.Id, null, model.Recipient?.Id);
channel.Update(model);
}
}
break;
//Servers
case "GUILD_CREATE":
{
var model = e.Payload.ToObject<Events.GuildCreate>(_serializer);
var server = _servers.GetOrAdd(model.Id);
server.Update(model);
RaiseEvent(nameof(ServerCreated), () => RaiseServerCreated(server));
}
break;
case "GUILD_UPDATE":
{
var model = e.Payload.ToObject<Events.GuildUpdate>(_serializer);
var server = _servers.GetOrAdd(model.Id);
server.Update(model);
RaiseEvent(nameof(ServerUpdated), () => RaiseServerUpdated(server));
}
break;
case "GUILD_DELETE":
{
var data = e.Payload.ToObject<Events.GuildDelete>(_serializer);
var server = _servers.TryRemove(data.Id);
if (server != null)
RaiseEvent(nameof(ServerDestroyed), () => RaiseServerDestroyed(server));
}
break;
//Channels
case "CHANNEL_CREATE":
{
var data = e.Payload.ToObject<Events.ChannelCreate>(_serializer);
var channel = _channels.GetOrAdd(data.Id, data.GuildId, data.Recipient?.Id);
channel.Update(data);
RaiseEvent(nameof(ChannelCreated), () => RaiseChannelCreated(channel));
}
break;
case "CHANNEL_UPDATE":
{
var data = e.Payload.ToObject<Events.ChannelUpdate>(_serializer);
var channel = _channels.GetOrAdd(data.Id, data.GuildId, data.Recipient?.Id);
channel.Update(data);
RaiseEvent(nameof(ChannelUpdated), () => RaiseChannelUpdated(channel));
}
break;
case "CHANNEL_DELETE":
{
var data = e.Payload.ToObject<Events.ChannelDelete>(_serializer);
var channel = _channels.TryRemove(data.Id);
if (channel != null)
RaiseEvent(nameof(ChannelDestroyed), () => RaiseChannelDestroyed(channel));
}
break;
//Members
case "GUILD_MEMBER_ADD":
{
var data = e.Payload.ToObject<Events.GuildMemberAdd>(_serializer);
var member = _members.GetOrAdd(data.UserId, data.GuildId);
member.Update(data);
RaiseEvent(nameof(MemberAdded), () => RaiseMemberAdded(member));
}
break;
case "GUILD_MEMBER_UPDATE":
{
var data = e.Payload.ToObject<Events.GuildMemberUpdate>(_serializer);
var member = _members.GetOrAdd(data.UserId, data.GuildId);
member.Update(data);
RaiseEvent(nameof(MemberUpdated), () => RaiseMemberUpdated(member));
}
break;
case "GUILD_MEMBER_REMOVE":
{
var data = e.Payload.ToObject<Events.GuildMemberRemove>(_serializer);
var member = _members.TryRemove(data.UserId, data.GuildId);
if (member != null)
try { RaiseMemberRemoved(member); } catch { }
}
break;
//Roles
case "GUILD_ROLE_CREATE":
{
var data = e.Payload.ToObject<Events.GuildRoleCreate>(_serializer);
var role = _roles.GetOrAdd(data.Data.Id, data.GuildId);
role.Update(data.Data);
RaiseEvent(nameof(RoleUpdated), () => RaiseRoleUpdated(role));
}
break;
case "GUILD_ROLE_UPDATE":
{
var data = e.Payload.ToObject<Events.GuildRoleUpdate>(_serializer);
var role = _roles.GetOrAdd(data.Data.Id, data.GuildId);
role.Update(data.Data);
RaiseEvent(nameof(RoleUpdated), () => RaiseRoleUpdated(role));
}
break;
case "GUILD_ROLE_DELETE":
{
var data = e.Payload.ToObject<Events.GuildRoleDelete>(_serializer);
var role = _roles.TryRemove(data.RoleId);
if (role != null)
RaiseEvent(nameof(RoleDeleted), () => RaiseRoleDeleted(role));
}
break;
//Bans
case "GUILD_BAN_ADD":
{
var data = e.Payload.ToObject<Events.GuildBanAdd>(_serializer);
var server = _servers[data.GuildId];
if (server != null)
{
server.AddBan(data.UserId);
RaiseEvent(nameof(BanAdded), () => RaiseBanAdded(data.UserId, server));
}
}
break;
case "GUILD_BAN_REMOVE":
{
var data = e.Payload.ToObject<Events.GuildBanRemove>(_serializer);
var server = _servers[data.GuildId];
if (server != null && server.RemoveBan(data.UserId))
RaiseEvent(nameof(BanRemoved), () => RaiseBanRemoved(data.UserId, server));
}
break;
//Messages
case "MESSAGE_CREATE":
{
var data = e.Payload.ToObject<Events.MessageCreate>(_serializer);
Message msg = null;
bool wasLocal = _config.UseMessageQueue && data.Author.Id == _currentUserId && data.Nonce != null;
if (wasLocal)
{
msg = _messages.Remap("nonce" + data.Nonce, data.Id);
if (msg != null)
{
msg.IsQueued = false;
msg.Id = data.Id;
}
}
if (msg == null)
msg = _messages.GetOrAdd(data.Id, data.ChannelId);
msg.Update(data);
if (_config.TrackActivity)
msg.User.UpdateActivity(data.Timestamp);
if (wasLocal)
RaiseEvent(nameof(MessageSent), () => RaiseMessageSent(msg));
RaiseEvent(nameof(MessageCreated), () => RaiseMessageCreated(msg));
}
break;
case "MESSAGE_UPDATE":
{
var data = e.Payload.ToObject<Events.MessageUpdate>(_serializer);
var msg = _messages.GetOrAdd(data.Id, data.ChannelId);
msg.Update(data);
RaiseEvent(nameof(MessageUpdated), () => RaiseMessageUpdated(msg));
}
break;
case "MESSAGE_DELETE":
{
var data = e.Payload.ToObject<Events.MessageDelete>(_serializer);
var msg = _messages.TryRemove(data.Id);
if (msg != null)
RaiseEvent(nameof(MessageDeleted), () => RaiseMessageDeleted(msg));
}
break;
case "MESSAGE_ACK":
{
var data = e.Payload.ToObject<Events.MessageAck>(_serializer);
var msg = GetMessage(data.MessageId);
if (msg != null)
RaiseEvent(nameof(MessageReadRemotely), () => RaiseMessageReadRemotely(msg));
}
break;
//Statuses
case "PRESENCE_UPDATE":
{
var data = e.Payload.ToObject<Events.PresenceUpdate>(_serializer);
var member = _members[data.UserId, data.GuildId];
/*if (_config.TrackActivity)
{
var user = _users[data.User.Id];
if (user != null)
user.UpdateActivity(DateTime.UtcNow);
}*/
if (member != null)
{
member.Update(data);
RaiseEvent(nameof(MemberPresenceUpdated), () => RaiseMemberPresenceUpdated(member));
}
}
break;
case "VOICE_STATE_UPDATE":
{
var data = e.Payload.ToObject<Events.VoiceStateUpdate>(_serializer);
var member = _members[data.UserId, data.GuildId];
/*if (_config.TrackActivity)
{
var user = _users[data.User.Id];
if (user != null)
user.UpdateActivity(DateTime.UtcNow);
}*/
if (member != null)
{
member.Update(data);
RaiseEvent(nameof(MemberVoiceStateUpdated), () => RaiseMemberVoiceStateUpdated(member));
}
}
break;
case "TYPING_START":
{
var data = e.Payload.ToObject<Events.TypingStart>(_serializer);
var channel = _channels[data.ChannelId];
if (_config.TrackActivity)
{
var user = _users[data.UserId];
user.UpdateActivity(DateTime.UtcNow);
}
if (channel != null)
{
var member = _members[data.UserId, channel.ServerId];
if (member != null)
RaiseEvent(nameof(MemberIsTyping), () => RaiseMemberIsTyping(member, channel));
}
}
break;
//Voice
case "VOICE_SERVER_UPDATE":
{
var data = e.Payload.ToObject<Events.VoiceServerUpdate>(_serializer);
var server = _servers[data.GuildId];
if (_config.EnableVoice)
{
string host = "wss://" + data.Endpoint.Split(':')[0];
await _voiceSocket.Login(host, data.GuildId, _currentUserId, _dataSocket.SessionId, data.Token).ConfigureAwait(false);
}
}
break;
//Settings
case "USER_UPDATE":
{
var data = e.Payload.ToObject<Events.UserUpdate>(_serializer);
var user = _users[data.Id];
if (user != null)
{
user.Update(data);
try { RaiseUserUpdated(user); } catch { }
}
}
break;
case "USER_SETTINGS_UPDATE":
{
//TODO: Process this
}
break;
//Others
default:
RaiseOnLog(LogMessageSeverity.Warning, LogMessageSource.DataWebSocket, $"Unknown message type: {e.Type}");
break;
}
};
}
private void _dataSocket_Connected(object sender, EventArgs e)
{
@@ -262,10 +597,8 @@ namespace Discord
_disconnectedEvent.Set();
await _dataSocket.Disconnect().ConfigureAwait(false);
#if !DNXCORE50
if (_config.EnableVoice)
await _voiceSocket.Disconnect().ConfigureAwait(false);
#endif
Message ignored;
while (_pendingMessages.TryDequeue(out ignored)) { }
@@ -282,6 +615,12 @@ namespace Discord
}
//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 Block()
{
_disconnectedEvent.WaitOne();
}
private void CheckReady(bool checkVoice = false)
{
switch (_state)
@@ -301,10 +640,14 @@ namespace Discord
#endif
throw new InvalidOperationException("Voice is not enabled for this client.");
}
/// <summary> Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications. </summary>
public void Block()
private void RaiseEvent(string name, Action action)
{
_disconnectedEvent.WaitOne();
try { action(); }
catch (Exception ex)
{
RaiseOnLog(LogMessageSeverity.Error, LogMessageSource.Client,
$"{name} event handler raised an exception: ${ex.GetBaseException().Message}");
}
}
//Experimental