diff --git a/src/Discord.Net.Net45/Discord.Net.csproj b/src/Discord.Net.Net45/Discord.Net.csproj
index 388383b57..b99426e03 100644
--- a/src/Discord.Net.Net45/Discord.Net.csproj
+++ b/src/Discord.Net.Net45/Discord.Net.csproj
@@ -136,6 +136,12 @@
DiscordAPIClient.cs
+
+ DiscordBaseClient.cs
+
+
+ DiscordBaseClient.Events.cs
+
DiscordClient.API.cs
diff --git a/src/Discord.Net/DiscordBaseClient.Events.cs b/src/Discord.Net/DiscordBaseClient.Events.cs
new file mode 100644
index 000000000..ca1c01f65
--- /dev/null
+++ b/src/Discord.Net/DiscordBaseClient.Events.cs
@@ -0,0 +1,107 @@
+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;
+
+ internal 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; }
+
+ internal LogMessageEventArgs(LogMessageSeverity severity, LogMessageSource source, string msg)
+ {
+ Severity = severity;
+ Source = source;
+ Message = msg;
+ }
+ }
+
+ public sealed class VoicePacketEventArgs
+ {
+ public string UserId { get; }
+ public string ChannelId { get; }
+ public byte[] Buffer { get; }
+ public int Offset { get; }
+ public int Count { get; }
+
+ internal VoicePacketEventArgs(string userId, string channelId, byte[] buffer, int offset, int count)
+ {
+ UserId = userId;
+ Buffer = buffer;
+ Offset = offset;
+ Count = count;
+ }
+ }
+
+ public abstract partial class DiscordBaseClient
+ {
+ public event EventHandler Connected;
+ private void RaiseConnected()
+ {
+ if (Connected != null)
+ RaiseEvent(nameof(Connected), () => Connected(this, EventArgs.Empty));
+ }
+ public event EventHandler Disconnected;
+ private void RaiseDisconnected(DisconnectedEventArgs e)
+ {
+ if (Disconnected != null)
+ RaiseEvent(nameof(Disconnected), () => Disconnected(this, e));
+ }
+ public event EventHandler LogMessage;
+ internal void RaiseOnLog(LogMessageSeverity severity, LogMessageSource source, string message)
+ {
+ if (LogMessage != null)
+ RaiseEvent(nameof(LogMessage), () => LogMessage(this, new LogMessageEventArgs(severity, source, message)));
+ }
+
+ public event EventHandler VoiceConnected;
+ private void RaiseVoiceConnected()
+ {
+ if (VoiceConnected != null)
+ RaiseEvent(nameof(VoiceConnected), () => VoiceConnected(this, EventArgs.Empty));
+ }
+ public event EventHandler VoiceDisconnected;
+ private void RaiseVoiceDisconnected(DisconnectedEventArgs e)
+ {
+ if (VoiceDisconnected != null)
+ RaiseEvent(nameof(VoiceDisconnected), () => VoiceDisconnected(this, e));
+ }
+
+ public event EventHandler OnVoicePacket;
+ internal void RaiseOnVoicePacket(VoicePacketEventArgs e)
+ {
+ if (OnVoicePacket != null)
+ OnVoicePacket(this, e);
+ }
+ }
+}
diff --git a/src/Discord.Net/DiscordBaseClient.cs b/src/Discord.Net/DiscordBaseClient.cs
new file mode 100644
index 000000000..936634eb6
--- /dev/null
+++ b/src/Discord.Net/DiscordBaseClient.cs
@@ -0,0 +1,272 @@
+using Discord.API;
+using Discord.Collections;
+using Discord.Helpers;
+using Discord.WebSockets.Data;
+using System;
+using System.Collections.Concurrent;
+using System.Net;
+using System.Runtime.ExceptionServices;
+using System.Threading;
+using System.Threading.Tasks;
+using VoiceWebSocket = Discord.WebSockets.Voice.VoiceWebSocket;
+
+namespace Discord
+{
+ public enum DiscordClientState : byte
+ {
+ Disconnected,
+ Connecting,
+ Connected,
+ Disconnecting
+ }
+
+ /// Provides a barebones connection to the Discord service
+ public partial class DiscordBaseClient
+ {
+ internal readonly DataWebSocket _dataSocket;
+ internal readonly VoiceWebSocket _voiceSocket;
+ protected readonly ManualResetEvent _disconnectedEvent;
+ protected readonly ManualResetEventSlim _connectedEvent;
+ private Task _runTask;
+ private string _gateway, _token;
+
+ protected ExceptionDispatchInfo _disconnectReason;
+ private bool _wasDisconnectUnexpected;
+
+ /// Returns the id of the current logged-in user.
+ public string CurrentUserId => _currentUserId;
+ private string _currentUserId;
+ /*/// Returns the server this user is currently connected to for voice.
+ public string CurrentVoiceServerId => _voiceSocket.CurrentServerId;*/
+
+ /// Returns the current connection state of this client.
+ public DiscordClientState State => (DiscordClientState)_state;
+ private int _state;
+
+ /// 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.
+ public DiscordClientConfig Config => _config;
+ protected readonly DiscordClientConfig _config;
+
+ public CancellationToken CancelToken => _cancelToken;
+ private CancellationTokenSource _cancelTokenSource;
+ private CancellationToken _cancelToken;
+
+ /// Initializes a new instance of the DiscordClient class.
+ public DiscordBaseClient(DiscordClientConfig config = null)
+ {
+ _config = config ?? new DiscordClientConfig();
+ _config.Lock();
+
+ _state = (int)DiscordClientState.Disconnected;
+ _cancelToken = new CancellationToken(true);
+ _disconnectedEvent = new ManualResetEvent(true);
+ _connectedEvent = new ManualResetEventSlim(false);
+
+ _dataSocket = new DataWebSocket(this);
+ _dataSocket.Connected += (s, e) => { if (_state == (int)DiscordClientState.Connecting) CompleteConnect(); };
+ _dataSocket.Disconnected += async (s, e) =>
+ {
+ RaiseDisconnected(e);
+ if (e.WasUnexpected)
+ await _dataSocket.Reconnect(_token);
+ };
+ if (Config.VoiceMode != DiscordVoiceMode.Disabled)
+ {
+ _voiceSocket = new VoiceWebSocket(this);
+ _voiceSocket.Connected += (s, e) => RaiseVoiceConnected();
+ _voiceSocket.Disconnected += async (s, e) =>
+ {
+ RaiseVoiceDisconnected(e);
+ if (e.WasUnexpected)
+ await _voiceSocket.Reconnect();
+ };
+ }
+
+ _dataSocket.LogMessage += (s, e) => RaiseOnLog(e.Severity, LogMessageSource.DataWebSocket, e.Message);
+ if (_config.VoiceMode != DiscordVoiceMode.Disabled)
+ _voiceSocket.LogMessage += (s, e) => RaiseOnLog(e.Severity, LogMessageSource.VoiceWebSocket, 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");
+ if (_config.VoiceMode != DiscordVoiceMode.Disabled)
+ {
+ _voiceSocket.Connected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.VoiceWebSocket, "Connected");
+ _voiceSocket.Disconnected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.VoiceWebSocket, "Disconnected");
+ }
+ }
+
+ _dataSocket.ReceivedEvent += (s, e) => OnReceivedEvent(e);
+ }
+
+ //Connection
+ protected async Task Connect(string gateway, string 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;
+ _token = token;
+ return token;
+ }
+ catch
+ {
+
+ await Disconnect().ConfigureAwait(false);
+ throw;
+ }
+ }
+ protected void CompleteConnect()
+ {
+ _state = (int)DiscordClientState.Connected;
+ _connectedEvent.Set();
+ RaiseConnected();
+ }
+
+ /// Disconnects from the Discord server, canceling any pending requests.
+ public Task Disconnect() => DisconnectInternal(new Exception("Disconnect was requested by user."), isUnexpected: false);
+ protected 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 TaskHelper.CompletedTask; //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 TaskHelper.CompletedTask; //Already disconnected
+ hasWriterLock = oldState == (int)DiscordClientState.Connected; //Caused state change
+ }
+
+ if (hasWriterLock)
+ {
+ _wasDisconnectUnexpected = isUnexpected;
+ _disconnectReason = ex != null ? ExceptionDispatchInfo.Capture(ex) : null;
+
+ _cancelTokenSource.Cancel();
+ /*if (_state == DiscordClientState.Connecting) //_runTask was never made
+ await Cleanup().ConfigureAwait(false);*/
+ }
+
+ if (!skipAwait)
+ return _runTask ?? TaskHelper.CompletedTask;
+ else
+ return TaskHelper.CompletedTask;
+ }
+
+ private async Task RunTasks()
+ {
+ Task[] tasks = Run();
+ 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);
+
+ //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 Task[] Run()
+ {
+ return new Task[] { _cancelToken.Wait() };
+ }
+
+ protected virtual async Task Cleanup()
+ {
+ await _dataSocket.Disconnect().ConfigureAwait(false);
+ if (_config.VoiceMode != DiscordVoiceMode.Disabled)
+ await _voiceSocket.Disconnect().ConfigureAwait(false);
+
+ _currentUserId = null;
+ _gateway = null;
+ _token = null;
+ }
+
+ //Helpers
+ /// Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications.
+ public void Block()
+ {
+ _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.");
+ }
+
+ if (checkVoice && _config.VoiceMode == DiscordVoiceMode.Disabled)
+ throw new InvalidOperationException("Voice is not enabled for this client.");
+ }
+ protected void RaiseEvent(string name, Action action)
+ {
+ try { action(); }
+ catch (Exception ex)
+ {
+ RaiseOnLog(LogMessageSeverity.Error, LogMessageSource.Client,
+ $"{name} event handler raised an exception: ${ex.GetBaseException().Message}");
+ }
+ }
+
+ internal virtual Task OnReceivedEvent(WebSocketEventEventArgs e)
+ {
+ if (e.Type == "READY")
+ _currentUserId = e.Payload["user"].Value("id");
+ return TaskHelper.CompletedTask;
+ }
+ }
+}
diff --git a/src/Discord.Net/DiscordClient.API.cs b/src/Discord.Net/DiscordClient.API.cs
index b249b784c..3cf3c354f 100644
--- a/src/Discord.Net/DiscordClient.API.cs
+++ b/src/Discord.Net/DiscordClient.API.cs
@@ -1,5 +1,4 @@
using Discord.API;
-using Discord.Helpers;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -98,7 +97,7 @@ namespace Discord
channel = user.PrivateChannel;
if (channel == null)
{
- var response = await _api.CreatePMChannel(_currentUserId, userId).ConfigureAwait(false);
+ var response = await _api.CreatePMChannel(CurrentUserId, userId).ConfigureAwait(false);
channel = _channels.GetOrAdd(response.Id, response.GuildId, response.Recipient?.Id);
channel.Update(response);
}
@@ -266,13 +265,13 @@ namespace Discord
var nonce = GenerateNonce();
if (_config.UseMessageQueue)
{
- var msg = _messages.GetOrAdd("nonce_" + nonce, channel.Id, _currentUserId);
+ var msg = _messages.GetOrAdd("nonce_" + nonce, channel.Id, CurrentUserId);
var currentMember = _members[msg.UserId, channel.ServerId];
msg.Update(new API.Message
{
Content = blockText,
Timestamp = DateTime.UtcNow,
- Author = new UserReference { Avatar = currentMember.AvatarId, Discriminator = currentMember.Discriminator, Id = _currentUserId, Username = currentMember.Name },
+ Author = new UserReference { Avatar = currentMember.AvatarId, Discriminator = currentMember.Discriminator, Id = CurrentUserId, Username = currentMember.Name },
ChannelId = channel.Id,
IsTextToSpeech = isTextToSpeech
});
@@ -513,13 +512,13 @@ namespace Discord
}
//Profile
- public Task EditProfile(string currentPassword,
+ public Task EditProfile(string currentPassword = "",
string username = null, string email = null, string password = null,
AvatarImageType avatarType = AvatarImageType.Png, byte[] avatar = null)
{
if (currentPassword == null) throw new ArgumentNullException(nameof(currentPassword));
- return _api.EditProfile(currentPassword, username: username, email: email, password: password,
+ return _api.EditProfile(currentPassword: currentPassword, username: username, email: email, password: password,
avatarType: avatarType, avatar: avatar);
}
diff --git a/src/Discord.Net/DiscordClient.Events.cs b/src/Discord.Net/DiscordClient.Events.cs
index eddd000ce..25841b9cc 100644
--- a/src/Discord.Net/DiscordClient.Events.cs
+++ b/src/Discord.Net/DiscordClient.Events.cs
@@ -2,50 +2,6 @@
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;
-
- internal 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; }
-
- internal LogMessageEventArgs(LogMessageSeverity severity, LogMessageSource source, string msg)
- {
- Severity = severity;
- Source = source;
- Message = msg;
- }
- }
-
public sealed class ServerEventArgs : EventArgs
{
public Server Server { get; }
@@ -148,45 +104,9 @@ namespace Discord
IsSpeaking = isSpeaking;
}
}
- public sealed class VoicePacketEventArgs
- {
- public string UserId { get; }
- public string ChannelId { get; }
- public byte[] Buffer { get; }
- public int Offset { get; }
- public int Count { get; }
-
- internal VoicePacketEventArgs(string userId, string channelId, byte[] buffer, int offset, int count)
- {
- UserId = userId;
- Buffer = buffer;
- Offset = offset;
- Count = count;
- }
- }
public partial class DiscordClient
{
- //General
- public event EventHandler Connected;
- private void RaiseConnected()
- {
- if (Connected != null)
- RaiseEvent(nameof(Connected), () => Connected(this, EventArgs.Empty));
- }
- public event EventHandler Disconnected;
- private void RaiseDisconnected(DisconnectedEventArgs e)
- {
- if (Disconnected != null)
- RaiseEvent(nameof(Disconnected), () => Disconnected(this, e));
- }
- public event EventHandler LogMessage;
- internal void RaiseOnLog(LogMessageSeverity severity, LogMessageSource source, string message)
- {
- if (LogMessage != null)
- RaiseEvent(nameof(LogMessage), () => LogMessage(this, new LogMessageEventArgs(severity, source, message)));
- }
-
//Server
public event EventHandler ServerCreated;
private void RaiseServerCreated(Server server)
@@ -342,26 +262,5 @@ namespace Discord
if (UserIsSpeaking != null)
RaiseEvent(nameof(UserIsSpeaking), () => UserIsSpeaking(this, new UserIsSpeakingEventArgs(member, isSpeaking)));
}
-
- //Voice
- public event EventHandler VoiceConnected;
- private void RaiseVoiceConnected()
- {
- if (VoiceConnected != null)
- RaiseEvent(nameof(UserIsSpeaking), () => VoiceConnected(this, EventArgs.Empty));
- }
- public event EventHandler VoiceDisconnected;
- private void RaiseVoiceDisconnected(DisconnectedEventArgs e)
- {
- if (VoiceDisconnected != null)
- RaiseEvent(nameof(UserIsSpeaking), () => VoiceDisconnected(this, e));
- }
-
- public event EventHandler OnVoicePacket;
- internal void RaiseOnVoicePacket(VoicePacketEventArgs e)
- {
- if (OnVoicePacket != null)
- OnVoicePacket(this, e);
- }
}
}
diff --git a/src/Discord.Net/DiscordClient.Voice.cs b/src/Discord.Net/DiscordClient.Voice.cs
index 0bc054cbd..d2e3fc141 100644
--- a/src/Discord.Net/DiscordClient.Voice.cs
+++ b/src/Discord.Net/DiscordClient.Voice.cs
@@ -1,6 +1,7 @@
using Discord.Helpers;
using Discord.WebSockets;
using System;
+using System.Threading;
using System.Threading.Tasks;
namespace Discord
@@ -8,30 +9,31 @@ namespace Discord
public partial class DiscordClient
{
public Task JoinVoiceServer(Channel channel)
- => JoinVoiceServer(channel?.Server, channel);
- public Task JoinVoiceServer(string serverId, string channelId)
- => JoinVoiceServer(_servers[serverId], _channels[channelId]);
+ => JoinVoiceServer(channel?.ServerId, channel?.Id);
public Task JoinVoiceServer(Server server, string channelId)
- => JoinVoiceServer(server, _channels[channelId]);
- private async Task JoinVoiceServer(Server server, Channel channel)
+ => JoinVoiceServer(server?.Id, channelId);
+ public async Task JoinVoiceServer(string serverId, string channelId)
{
CheckReady(checkVoice: true);
- if (server == null) throw new ArgumentNullException(nameof(server));
- if (channel == null) throw new ArgumentNullException(nameof(channel));
+ if (serverId == null) throw new ArgumentNullException(nameof(serverId));
+ if (channelId == null) throw new ArgumentNullException(nameof(channelId));
await LeaveVoiceServer().ConfigureAwait(false);
- _voiceSocket.SetChannel(server, channel);
- _dataSocket.SendJoinVoice(server.Id, channel.Id);
+ _voiceSocket.SetChannel(serverId, channelId);
+ _dataSocket.SendJoinVoice(serverId, channelId);
+ CancellationTokenSource tokenSource = new CancellationTokenSource();
try
{
- await Task.Run(() => _voiceSocket.WaitForConnection())
- .Timeout(_config.ConnectionTimeout)
+ await Task.Run(() => _voiceSocket.WaitForConnection(tokenSource.Token))
+ .Timeout(_config.ConnectionTimeout, tokenSource)
.ConfigureAwait(false);
}
- catch (TaskCanceledException)
+ catch (TimeoutException)
{
+ tokenSource.Cancel();
await LeaveVoiceServer().ConfigureAwait(false);
+ throw;
}
}
public async Task LeaveVoiceServer()
@@ -40,11 +42,11 @@ namespace Discord
if (_voiceSocket.State != WebSocketState.Disconnected)
{
- var server = _voiceSocket.CurrentVoiceServer;
- if (server != null)
+ var serverId = _voiceSocket.CurrentServerId;
+ if (serverId != null)
{
await _voiceSocket.Disconnect().ConfigureAwait(false);
- _dataSocket.SendLeaveVoice(server.Id);
+ _dataSocket.SendLeaveVoice(serverId);
}
}
}
diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs
index 3445dd761..318743f2d 100644
--- a/src/Discord.Net/DiscordClient.cs
+++ b/src/Discord.Net/DiscordClient.cs
@@ -7,54 +7,22 @@ using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Net;
-using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
-using VoiceWebSocket = Discord.WebSockets.Voice.VoiceWebSocket;
namespace Discord
{
- public enum DiscordClientState : byte
- {
- Disconnected,
- Connecting,
- Connected,
- Disconnecting
- }
-
/// Provides a connection to the DiscordApp service.
- public partial class DiscordClient
+ public partial class DiscordClient : DiscordBaseClient
{
+ protected readonly DiscordAPIClient _api;
private readonly Random _rand;
- private readonly DiscordAPIClient _api;
- private readonly DataWebSocket _dataSocket;
- private readonly VoiceWebSocket _voiceSocket;
- private readonly ConcurrentQueue _pendingMessages;
- private readonly ManualResetEvent _disconnectedEvent;
- private readonly ManualResetEventSlim _connectedEvent;
private readonly JsonSerializer _serializer;
- private Task _runTask;
- private string _token;
-
- protected ExceptionDispatchInfo _disconnectReason;
- private bool _wasDisconnectUnexpected;
+ private readonly ConcurrentQueue _pendingMessages;
- /// Returns the id of the current logged-in user.
- public string CurrentUserId => _currentUserId;
- private string _currentUserId;
/// Returns the current logged-in user.
public User CurrentUser => _currentUser;
private User _currentUser;
- /// Returns the server this user is currently connected to for voice.
- public Server CurrentVoiceServer => _voiceSocket.CurrentVoiceServer;
-
- /// Returns the current connection state of this client.
- public DiscordClientState State => (DiscordClientState)_state;
- private int _state;
-
- /// 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.
- public DiscordClientConfig Config => _config;
- private readonly DiscordClientConfig _config;
/// Returns a collection of all channels this client is a member of.
public Channels Channels => _channels;
@@ -76,36 +44,26 @@ namespace Discord
public Users Users => _users;
private readonly Users _users;
- public CancellationToken CancelToken => _cancelToken;
- private CancellationTokenSource _cancelTokenSource;
- private CancellationToken _cancelToken;
-
/// Initializes a new instance of the DiscordClient class.
public DiscordClient(DiscordClientConfig config = null)
+ : base(config)
{
- _config = config ?? new DiscordClientConfig();
- _config.Lock();
-
- _state = (int)DiscordClientState.Disconnected;
- _cancelToken = new CancellationToken(true);
- _disconnectedEvent = new ManualResetEvent(true);
- _connectedEvent = new ManualResetEventSlim(false);
_rand = new Random();
-
_api = new DiscordAPIClient(_config.LogLevel, _config.APITimeout);
- _dataSocket = new DataWebSocket(this);
- _dataSocket.Connected += (s, e) => { if (_state == (int)DiscordClientState.Connecting) CompleteConnect(); };
- _dataSocket.Disconnected += async (s, e) =>
- {
- RaiseDisconnected(e);
- if (e.WasUnexpected)
- await _dataSocket.Reconnect(_token);
- };
- if (_config.VoiceMode != DiscordVoiceMode.Disabled)
+ if (_config.UseMessageQueue)
+ _pendingMessages = new ConcurrentQueue();
+
+ object cacheLock = new object();
+ _channels = new Channels(this, cacheLock);
+ _members = new Members(this, cacheLock);
+ _messages = new Messages(this, cacheLock);
+ _roles = new Roles(this, cacheLock);
+ _servers = new Servers(this, cacheLock);
+ _users = new Users(this, cacheLock);
+
+ if (Config.VoiceMode != DiscordVoiceMode.Disabled)
{
- _voiceSocket = new VoiceWebSocket(this);
- _voiceSocket.Connected += (s, e) => RaiseVoiceConnected();
- _voiceSocket.Disconnected += async (s, e) =>
+ this.VoiceDisconnected += (s, e) =>
{
foreach (var member in _members)
{
@@ -115,49 +73,26 @@ namespace Discord
RaiseUserIsSpeaking(member, false);
}
}
- RaiseVoiceDisconnected(e);
- if (e.WasUnexpected)
- await _voiceSocket.Reconnect();
};
_voiceSocket.IsSpeaking += (s, e) =>
{
if (_voiceSocket.State == WebSocketState.Connected)
{
- var member = _members[e.UserId, _voiceSocket.CurrentVoiceServer.Id];
+ var member = _members[e.UserId, _voiceSocket.CurrentServerId];
bool value = e.IsSpeaking;
if (member.IsSpeaking != value)
{
member.IsSpeaking = value;
RaiseUserIsSpeaking(member, value);
- if (_config.TrackActivity)
+ if (Config.TrackActivity)
member.UpdateActivity();
}
}
};
}
- object cacheLock = new object();
- _channels = new Channels(this, cacheLock);
- _members = new Members(this, cacheLock);
- _messages = new Messages(this, cacheLock);
- _roles = new Roles(this, cacheLock);
- _servers = new Servers(this, cacheLock);
- _users = new Users(this, cacheLock);
-
- _dataSocket.LogMessage += (s, e) => RaiseOnLog(e.Severity, LogMessageSource.DataWebSocket, e.Message);
- if (_config.VoiceMode != DiscordVoiceMode.Disabled)
- _voiceSocket.LogMessage += (s, e) => RaiseOnLog(e.Severity, LogMessageSource.VoiceWebSocket, 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.ReceivedEvent += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.DataWebSocket, $"Received {e.Type}");
- if (_config.VoiceMode != DiscordVoiceMode.Disabled)
- {
- _voiceSocket.Connected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.VoiceWebSocket, "Connected");
- _voiceSocket.Disconnected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.VoiceWebSocket, "Disconnected");
- }
- }
+ this.Connected += (s,e) => _api.CancelToken = CancelToken;
+
if (_config.LogLevel >= LogMessageSeverity.Verbose)
{
bool isDebug = _config.LogLevel >= LogMessageSeverity.Debug;
@@ -270,513 +205,56 @@ namespace Discord
_serializer.CheckAdditionalContent = true;
_serializer.MissingMemberHandling = MissingMemberHandling.Error;
#endif
-
- _dataSocket.ReceivedEvent += async (s, e) =>
- {
- switch (e.Type)
- {
- //Global
- case "READY": //Resync
- {
- var data = e.Payload.ToObject(_serializer);
- _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 user = _users.GetOrAdd(model.Recipient.Id);
- user.Update(model.Recipient);
- var channel = _channels.GetOrAdd(model.Id, null, user.Id);
- channel.Update(model);
- }
- }
- break;
- case "RESUMED":
- break;
-
- //Servers
- case "GUILD_CREATE":
- {
- var model = e.Payload.ToObject(_serializer);
- var server = _servers.GetOrAdd(model.Id);
- server.Update(model);
- RaiseServerCreated(server);
- }
- break;
- case "GUILD_UPDATE":
- {
- var model = e.Payload.ToObject(_serializer);
- var server = _servers[model.Id];
- if (server != null)
- {
- server.Update(model);
- RaiseServerUpdated(server);
- }
- }
- break;
- case "GUILD_DELETE":
- {
- var data = e.Payload.ToObject(_serializer);
- var server = _servers.TryRemove(data.Id);
- if (server != null)
- RaiseServerDestroyed(server);
- }
- break;
-
- //Channels
- case "CHANNEL_CREATE":
- {
- var data = e.Payload.ToObject(_serializer);
- Channel channel;
- if (data.IsPrivate)
- {
- var user = _users.GetOrAdd(data.Recipient.Id);
- user.Update(data.Recipient);
- channel = _channels.GetOrAdd(data.Id, null, user.Id);
- }
- else
- channel = _channels.GetOrAdd(data.Id, data.GuildId, null);
- channel.Update(data);
- RaiseChannelCreated(channel);
- }
- break;
- case "CHANNEL_UPDATE":
- {
- var data = e.Payload.ToObject(_serializer);
- var channel = _channels[data.Id];
- if (channel != null)
- {
- channel.Update(data);
- RaiseChannelUpdated(channel);
- }
- }
- break;
- case "CHANNEL_DELETE":
- {
- var data = e.Payload.ToObject(_serializer);
- var channel = _channels.TryRemove(data.Id);
- if (channel != null)
- RaiseChannelDestroyed(channel);
- }
- break;
-
- //Members
- case "GUILD_MEMBER_ADD":
- {
- var data = e.Payload.ToObject(_serializer);
- var user = _users.GetOrAdd(data.User.Id);
- var member = _members.GetOrAdd(data.User.Id, data.GuildId);
- user.Update(data.User);
- member.Update(data);
- if (_config.TrackActivity)
- member.UpdateActivity();
- RaiseUserAdded(member);
- }
- break;
- case "GUILD_MEMBER_UPDATE":
- {
- var data = e.Payload.ToObject(_serializer);
- var member = _members[data.User.Id, data.GuildId];
- if (member != null)
- {
- member.Update(data);
- RaiseMemberUpdated(member);
- }
- }
- break;
- case "GUILD_MEMBER_REMOVE":
- {
- var data = e.Payload.ToObject(_serializer);
- var member = _members.TryRemove(data.UserId, data.GuildId);
- if (member != null)
- RaiseUserRemoved(member);
- }
- break;
-
- //Roles
- case "GUILD_ROLE_CREATE":
- {
- var data = e.Payload.ToObject(_serializer);
- var role = _roles.GetOrAdd(data.Data.Id, data.GuildId);
- role.Update(data.Data);
- RaiseRoleUpdated(role);
- }
- break;
- case "GUILD_ROLE_UPDATE":
- {
- var data = e.Payload.ToObject(_serializer);
- var role = _roles[data.Data.Id];
- if (role != null)
- role.Update(data.Data);
- RaiseRoleUpdated(role);
- }
- break;
- case "GUILD_ROLE_DELETE":
- {
- var data = e.Payload.ToObject(_serializer);
- var role = _roles.TryRemove(data.RoleId);
- if (role != null)
- RaiseRoleDeleted(role);
- }
- break;
-
- //Bans
- case "GUILD_BAN_ADD":
- {
- var data = e.Payload.ToObject(_serializer);
- var server = _servers[data.GuildId];
- if (server != null)
- {
- server.AddBan(data.UserId);
- RaiseBanAdded(data.UserId, server);
- }
- }
- break;
- case "GUILD_BAN_REMOVE":
- {
- var data = e.Payload.ToObject(_serializer);
- var server = _servers[data.GuildId];
- if (server != null && server.RemoveBan(data.UserId))
- RaiseBanRemoved(data.UserId, server);
- }
- break;
-
- //Messages
- case "MESSAGE_CREATE":
- {
- var data = e.Payload.ToObject(_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, data.Author.Id);
- msg.Update(data);
- if (_config.TrackActivity)
- {
- var channel = msg.Channel;
- if (channel == null || channel.IsPrivate)
- {
- var user = msg.User;
- if (user != null)
- user.UpdateActivity(data.Timestamp);
- }
- else
- {
- var member = msg.Member;
- if (member != null)
- member.UpdateActivity(data.Timestamp);
- }
- }
- if (wasLocal)
- RaiseMessageSent(msg);
- RaiseMessageCreated(msg);
- }
- break;
- case "MESSAGE_UPDATE":
- {
- var data = e.Payload.ToObject(_serializer);
- var msg = _messages[data.Id];
- if (msg != null)
- {
- msg.Update(data);
- RaiseMessageUpdated(msg);
- }
- }
- break;
- case "MESSAGE_DELETE":
- {
- var data = e.Payload.ToObject(_serializer);
- var msg = _messages.TryRemove(data.Id);
- if (msg != null)
- RaiseMessageDeleted(msg);
- }
- break;
- case "MESSAGE_ACK":
- {
- var data = e.Payload.ToObject(_serializer);
- var msg = GetMessage(data.MessageId);
- if (msg != null)
- RaiseMessageReadRemotely(msg);
- }
- break;
-
- //Statuses
- case "PRESENCE_UPDATE":
- {
- var data = e.Payload.ToObject(_serializer);
- var member = _members[data.User.Id, data.GuildId];
- /*if (_config.TrackActivity)
- {
- var user = _users[data.User.Id];
- if (user != null)
- user.UpdateActivity(DateTime.UtcNow);
- }*/
- if (member != null)
- {
- member.Update(data);
- RaiseUserPresenceUpdated(member);
- }
- }
- break;
- case "TYPING_START":
- {
- var data = e.Payload.ToObject(_serializer);
- var channel = _channels[data.ChannelId];
- var user = _users[data.UserId];
-
- if (user != null)
- {
- if (channel != null)
- RaiseUserIsTyping(user, channel);
- }
- if (_config.TrackActivity)
- {
- if (channel.IsPrivate)
- {
- if (user != null)
- user.UpdateActivity();
- }
- else
- {
- var member = _members[data.UserId, channel.ServerId];
- if (member != null)
- member.UpdateActivity();
- }
- }
- }
- break;
-
- //Voice
- case "VOICE_STATE_UPDATE":
- {
- var data = e.Payload.ToObject(_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);
- if (member.IsSpeaking)
- {
- member.IsSpeaking = false;
- RaiseUserIsSpeaking(member, false);
- }
- RaiseUserVoiceStateUpdated(member);
- }
- }
- break;
- case "VOICE_SERVER_UPDATE":
- {
- var data = e.Payload.ToObject(_serializer);
- if (data.GuildId == _voiceSocket.CurrentVoiceServer.Id)
- {
- var server = _servers[data.GuildId];
- if (_config.VoiceMode != DiscordVoiceMode.Disabled)
- {
- _voiceSocket.Host = "wss://" + data.Endpoint.Split(':')[0];
- await _voiceSocket.Login(_currentUserId, _dataSocket.SessionId, data.Token, _cancelToken).ConfigureAwait(false);
- }
- }
- }
- break;
-
- //Settings
- case "USER_UPDATE":
- {
- var data = e.Payload.ToObject(_serializer);
- var user = _users[data.Id];
- if (user != null)
- {
- user.Update(data);
- RaiseUserUpdated(user);
- }
- }
- break;
- case "USER_SETTINGS_UPDATE":
- {
- //TODO: Process this
- }
- break;
-
- //Others
- default:
- RaiseOnLog(LogMessageSeverity.Warning, LogMessageSource.DataWebSocket, $"Unknown message type: {e.Type}");
- break;
- }
- };
}
- //Connection
- /// Connects to the Discord server with the provided token.
- public async Task Connect(string token)
- {
- if (_state != (int)DiscordClientState.Disconnected)
- await Disconnect().ConfigureAwait(false);
-
- _cancelTokenSource = new CancellationTokenSource();
- _cancelToken = _cancelTokenSource.Token;
- _api.CancelToken = _cancelToken;
-
- await ConnectInternal(token)
- .Timeout(_config.ConnectionTimeout)
- .ConfigureAwait(false);
- }
/// Connects to the Discord server with the provided email and password.
/// Returns a token for future connections.
- public async Task Connect(string email, string password)
+ public new async Task Connect(string email, string password)
{
- if (_state != (int)DiscordClientState.Disconnected)
+ if (State != DiscordClientState.Disconnected)
await Disconnect().ConfigureAwait(false);
- _cancelTokenSource = new CancellationTokenSource();
- _cancelToken = _cancelTokenSource.Token;
- _api.CancelToken = _cancelToken;
-
string token;
try
{
- var response = await _api.Login(email, password).ConfigureAwait(false);
+ var response = await _api.Login(email, password)
+ .Timeout(5000);
token = response.Token;
- if (_config.LogLevel >= LogMessageSeverity.Verbose)
+ if (_config.LogLevel >= LogMessageSeverity.Verbose)
RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, "Login successful, got token.");
}
catch (TaskCanceledException) { throw new TimeoutException(); }
- return await ConnectInternal(token)
- .Timeout(_config.ConnectionTimeout)
- .ConfigureAwait(false);
- }
- private async Task ConnectInternal(string token)
- {
- try
- {
- _disconnectedEvent.Reset();
- _api.Token = token;
- _token = token;
- _state = (int)DiscordClientState.Connecting;
-
- string url = (await _api.Gateway().ConfigureAwait(false)).Url;
- if (_config.LogLevel >= LogMessageSeverity.Verbose)
- RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, $"Websocket endpoint: {url}");
-
- _dataSocket.Host = url;
- _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;
- _token = token;
- return token;
- }
- catch
- {
-
- await Disconnect().ConfigureAwait(false);
- throw;
- }
- }
- protected void CompleteConnect()
- {
- _state = (int)DiscordClientState.Connected;
- _connectedEvent.Set();
- RaiseConnected();
+ await Connect(token);
+ return token;
}
- /// Disconnects from the Discord server, canceling any pending requests.
- public Task Disconnect() => DisconnectInternal(new Exception("Disconnect was requested by user."), isUnexpected: false);
- protected Task DisconnectInternal(Exception ex = null, bool isUnexpected = true, bool skipAwait = false)
+ /// Connects to the Discord server with the provided token.
+ public async Task Connect(string token)
{
- 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 TaskHelper.CompletedTask; //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 TaskHelper.CompletedTask; //Already disconnected
- hasWriterLock = oldState == (int)DiscordClientState.Connected; //Caused state change
- }
+ if (State != (int)DiscordClientState.Disconnected)
+ await Disconnect().ConfigureAwait(false);
- if (hasWriterLock)
- {
- _wasDisconnectUnexpected = isUnexpected;
- _disconnectReason = ex != null ? ExceptionDispatchInfo.Capture(ex) : null;
- _cancelTokenSource.Cancel();
- }
+ _api.Token = token;
+ string gateway = (await _api.Gateway().ConfigureAwait(false)).Url;
+ if (_config.LogLevel >= LogMessageSeverity.Verbose)
+ RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, $"Websocket endpoint: {gateway}");
- if (!skipAwait)
- return _runTask ?? TaskHelper.CompletedTask;
- else
- return TaskHelper.CompletedTask;
+ await base.Connect(gateway, token)
+ .Timeout(_config.ConnectionTimeout)
+ .ConfigureAwait(false);
}
- private async Task RunTasks()
- {
- Task task;
- if (_config.UseMessageQueue)
- task = MessageQueueLoop();
- else
- task = _cancelToken.Wait();
-
- try { await task.ConfigureAwait(false); }
- catch (Exception ex) { await DisconnectInternal(ex, skipAwait: true).ConfigureAwait(false); }
-
- //When the first task ends, make sure the rest do too
- await DisconnectInternal(skipAwait: true);
-
- await Cleanup().ConfigureAwait(false);
- _runTask = null;
- }
- private async Task Cleanup()
+ protected override async Task Cleanup()
{
- var wasDisconnectUnexpected = _wasDisconnectUnexpected;
- _wasDisconnectUnexpected = false;
-
- await _dataSocket.Disconnect().ConfigureAwait(false);
- if (_config.VoiceMode != DiscordVoiceMode.Disabled)
- await _voiceSocket.Disconnect().ConfigureAwait(false);
+ await base.Cleanup().ConfigureAwait(false);
if (_config.UseMessageQueue)
{
Message ignored;
while (_pendingMessages.TryDequeue(out ignored)) { }
}
-
+
_channels.Clear();
_members.Clear();
_messages.Clear();
@@ -785,53 +263,12 @@ namespace Discord
_users.Clear();
_currentUser = null;
- _currentUserId = null;
- _token = null;
-
- if (!wasDisconnectUnexpected)
- {
- _state = (int)DiscordClientState.Disconnected;
- _disconnectedEvent.Set();
- }
- _connectedEvent.Reset();
- }
-
- //Helpers
- /// Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications.
- public void Block()
- {
- _disconnectedEvent.WaitOne();
- }
-
- private 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.");
- }
-
- if (checkVoice && _config.VoiceMode == DiscordVoiceMode.Disabled)
- throw new InvalidOperationException("Voice is not enabled for this client.");
- }
- private void RaiseEvent(string name, Action action)
- {
- try { action(); }
- catch (Exception ex)
- {
- RaiseOnLog(LogMessageSeverity.Error, LogMessageSource.Client,
- $"{name} event handler raised an exception: ${ex.GetBaseException().Message}");
- }
}
//Experimental
private Task MessageQueueLoop()
{
- var cancelToken = _cancelToken;
+ var cancelToken = CancelToken;
int interval = _config.MessageQueueInterval;
return Task.Run(async () =>
@@ -859,7 +296,7 @@ namespace Discord
msg.IsQueued = false;
msg.HasFailed = hasFailed;
RaiseMessageSent(msg);
- }
+ }
await Task.Delay(interval).ConfigureAwait(false);
}
});
@@ -869,5 +306,362 @@ namespace Discord
lock (_rand)
return _rand.Next().ToString();
}
+
+ internal override async Task OnReceivedEvent(WebSocketEventEventArgs e)
+ {
+ await base.OnReceivedEvent(e);
+
+ switch (e.Type)
+ {
+ //Global
+ case "READY": //Resync
+ {
+ var data = e.Payload.ToObject(_serializer);
+ _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 user = _users.GetOrAdd(model.Recipient.Id);
+ user.Update(model.Recipient);
+ var channel = _channels.GetOrAdd(model.Id, null, user.Id);
+ channel.Update(model);
+ }
+
+ /*foreach (var server in _servers)
+ _dataSocket.SendJoinVoice(server.Id, System.Linq.Enumerable.First(server.ChannelIds));*/
+ }
+ break;
+ case "RESUMED":
+ break;
+
+ //Servers
+ case "GUILD_CREATE":
+ {
+ var model = e.Payload.ToObject(_serializer);
+ var server = _servers.GetOrAdd(model.Id);
+ server.Update(model);
+ RaiseServerCreated(server);
+ }
+ break;
+ case "GUILD_UPDATE":
+ {
+ var model = e.Payload.ToObject(_serializer);
+ var server = _servers[model.Id];
+ if (server != null)
+ {
+ server.Update(model);
+ RaiseServerUpdated(server);
+ }
+ }
+ break;
+ case "GUILD_DELETE":
+ {
+ var data = e.Payload.ToObject(_serializer);
+ var server = _servers.TryRemove(data.Id);
+ if (server != null)
+ RaiseServerDestroyed(server);
+ }
+ break;
+
+ //Channels
+ case "CHANNEL_CREATE":
+ {
+ var data = e.Payload.ToObject(_serializer);
+ Channel channel;
+ if (data.IsPrivate)
+ {
+ var user = _users.GetOrAdd(data.Recipient.Id);
+ user.Update(data.Recipient);
+ channel = _channels.GetOrAdd(data.Id, null, user.Id);
+ }
+ else
+ channel = _channels.GetOrAdd(data.Id, data.GuildId, null);
+ channel.Update(data);
+ RaiseChannelCreated(channel);
+ }
+ break;
+ case "CHANNEL_UPDATE":
+ {
+ var data = e.Payload.ToObject(_serializer);
+ var channel = _channels[data.Id];
+ if (channel != null)
+ {
+ channel.Update(data);
+ RaiseChannelUpdated(channel);
+ }
+ }
+ break;
+ case "CHANNEL_DELETE":
+ {
+ var data = e.Payload.ToObject(_serializer);
+ var channel = _channels.TryRemove(data.Id);
+ if (channel != null)
+ RaiseChannelDestroyed(channel);
+ }
+ break;
+
+ //Members
+ case "GUILD_MEMBER_ADD":
+ {
+ var data = e.Payload.ToObject(_serializer);
+ var user = _users.GetOrAdd(data.User.Id);
+ var member = _members.GetOrAdd(data.User.Id, data.GuildId);
+ user.Update(data.User);
+ member.Update(data);
+ if (_config.TrackActivity)
+ member.UpdateActivity();
+ RaiseUserAdded(member);
+ }
+ break;
+ case "GUILD_MEMBER_UPDATE":
+ {
+ var data = e.Payload.ToObject(_serializer);
+ var member = _members[data.User.Id, data.GuildId];
+ if (member != null)
+ {
+ member.Update(data);
+ RaiseMemberUpdated(member);
+ }
+ }
+ break;
+ case "GUILD_MEMBER_REMOVE":
+ {
+ var data = e.Payload.ToObject(_serializer);
+ var member = _members.TryRemove(data.UserId, data.GuildId);
+ if (member != null)
+ RaiseUserRemoved(member);
+ }
+ break;
+
+ //Roles
+ case "GUILD_ROLE_CREATE":
+ {
+ var data = e.Payload.ToObject(_serializer);
+ var role = _roles.GetOrAdd(data.Data.Id, data.GuildId);
+ role.Update(data.Data);
+ RaiseRoleUpdated(role);
+ }
+ break;
+ case "GUILD_ROLE_UPDATE":
+ {
+ var data = e.Payload.ToObject(_serializer);
+ var role = _roles[data.Data.Id];
+ if (role != null)
+ role.Update(data.Data);
+ RaiseRoleUpdated(role);
+ }
+ break;
+ case "GUILD_ROLE_DELETE":
+ {
+ var data = e.Payload.ToObject(_serializer);
+ var role = _roles.TryRemove(data.RoleId);
+ if (role != null)
+ RaiseRoleDeleted(role);
+ }
+ break;
+
+ //Bans
+ case "GUILD_BAN_ADD":
+ {
+ var data = e.Payload.ToObject(_serializer);
+ var server = _servers[data.GuildId];
+ if (server != null)
+ {
+ server.AddBan(data.UserId);
+ RaiseBanAdded(data.UserId, server);
+ }
+ }
+ break;
+ case "GUILD_BAN_REMOVE":
+ {
+ var data = e.Payload.ToObject(_serializer);
+ var server = _servers[data.GuildId];
+ if (server != null && server.RemoveBan(data.UserId))
+ RaiseBanRemoved(data.UserId, server);
+ }
+ break;
+
+ //Messages
+ case "MESSAGE_CREATE":
+ {
+ var data = e.Payload.ToObject(_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, data.Author.Id);
+ msg.Update(data);
+ if (_config.TrackActivity)
+ {
+ var channel = msg.Channel;
+ if (channel == null || channel.IsPrivate)
+ {
+ var user = msg.User;
+ if (user != null)
+ user.UpdateActivity(data.Timestamp);
+ }
+ else
+ {
+ var member = msg.Member;
+ if (member != null)
+ member.UpdateActivity(data.Timestamp);
+ }
+ }
+ if (wasLocal)
+ RaiseMessageSent(msg);
+ RaiseMessageCreated(msg);
+ }
+ break;
+ case "MESSAGE_UPDATE":
+ {
+ var data = e.Payload.ToObject(_serializer);
+ var msg = _messages[data.Id];
+ if (msg != null)
+ {
+ msg.Update(data);
+ RaiseMessageUpdated(msg);
+ }
+ }
+ break;
+ case "MESSAGE_DELETE":
+ {
+ var data = e.Payload.ToObject(_serializer);
+ var msg = _messages.TryRemove(data.Id);
+ if (msg != null)
+ RaiseMessageDeleted(msg);
+ }
+ break;
+ case "MESSAGE_ACK":
+ {
+ var data = e.Payload.ToObject(_serializer);
+ var msg = GetMessage(data.MessageId);
+ if (msg != null)
+ RaiseMessageReadRemotely(msg);
+ }
+ break;
+
+ //Statuses
+ case "PRESENCE_UPDATE":
+ {
+ var data = e.Payload.ToObject(_serializer);
+ var member = _members[data.User.Id, data.GuildId];
+ /*if (_config.TrackActivity)
+ {
+ var user = _users[data.User.Id];
+ if (user != null)
+ user.UpdateActivity(DateTime.UtcNow);
+ }*/
+ if (member != null)
+ {
+ member.Update(data);
+ RaiseUserPresenceUpdated(member);
+ }
+ }
+ break;
+ case "TYPING_START":
+ {
+ var data = e.Payload.ToObject(_serializer);
+ var channel = _channels[data.ChannelId];
+ var user = _users[data.UserId];
+
+ if (user != null)
+ {
+ if (channel != null)
+ RaiseUserIsTyping(user, channel);
+ }
+ if (_config.TrackActivity)
+ {
+ if (channel.IsPrivate)
+ {
+ if (user != null)
+ user.UpdateActivity();
+ }
+ else
+ {
+ var member = _members[data.UserId, channel.ServerId];
+ if (member != null)
+ member.UpdateActivity();
+ }
+ }
+ }
+ break;
+
+ //Voice
+ case "VOICE_STATE_UPDATE":
+ {
+ var data = e.Payload.ToObject(_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);
+ if (member.IsSpeaking)
+ {
+ member.IsSpeaking = false;
+ RaiseUserIsSpeaking(member, false);
+ }
+ RaiseUserVoiceStateUpdated(member);
+ }
+ }
+ break;
+ case "VOICE_SERVER_UPDATE":
+ {
+ var data = e.Payload.ToObject(_serializer);
+ if (data.GuildId == _voiceSocket.CurrentServerId)
+ {
+ var server = _servers[data.GuildId];
+ if (_config.VoiceMode != DiscordVoiceMode.Disabled)
+ {
+ _voiceSocket.Host = "wss://" + data.Endpoint.Split(':')[0];
+ await _voiceSocket.Login(CurrentUserId, _dataSocket.SessionId, data.Token, CancelToken).ConfigureAwait(false);
+ }
+ }
+ }
+ break;
+
+ //Settings
+ case "USER_UPDATE":
+ {
+ var data = e.Payload.ToObject(_serializer);
+ var user = _users[data.Id];
+ if (user != null)
+ {
+ user.Update(data);
+ RaiseUserUpdated(user);
+ }
+ }
+ break;
+ case "USER_SETTINGS_UPDATE":
+ {
+ //TODO: Process this
+ }
+ break;
+
+ //Others
+ default:
+ RaiseOnLog(LogMessageSeverity.Warning, LogMessageSource.DataWebSocket, $"Unknown message type: {e.Type}");
+ break;
+ }
+ }
}
}
diff --git a/src/Discord.Net/Helpers/TaskHelper.cs b/src/Discord.Net/Helpers/TaskHelper.cs
index bce2b81cf..baae67be1 100644
--- a/src/Discord.Net/Helpers/TaskHelper.cs
+++ b/src/Discord.Net/Helpers/TaskHelper.cs
@@ -1,4 +1,6 @@
-using System.Threading.Tasks;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
namespace Discord.Helpers
{
@@ -32,5 +34,29 @@ namespace Discord.Helpers
else
return await self.ConfigureAwait(false);
}
+ public static async Task Timeout(this Task self, int milliseconds, CancellationTokenSource cancelToken)
+ {
+ try
+ {
+ cancelToken.CancelAfter(milliseconds);
+ await self;
+ }
+ catch (OperationCanceledException)
+ {
+ throw new TimeoutException();
+ }
+ }
+ public static async Task Timeout(this Task self, int milliseconds, CancellationTokenSource cancelToken)
+ {
+ try
+ {
+ cancelToken.CancelAfter(milliseconds);
+ return await self;
+ }
+ catch (OperationCanceledException)
+ {
+ throw new TimeoutException();
+ }
+ }
}
}
diff --git a/src/Discord.Net/Models/Channel.cs b/src/Discord.Net/Models/Channel.cs
index c9df8a4cc..22532c770 100644
--- a/src/Discord.Net/Models/Channel.cs
+++ b/src/Discord.Net/Models/Channel.cs
@@ -17,7 +17,6 @@ namespace Discord
private readonly DiscordClient _client;
private ConcurrentDictionary _messages;
- private ConcurrentDictionary _ssrcMapping;
/// Returns the unique identifier for this channel.
public string Id { get; }
@@ -70,8 +69,6 @@ namespace Discord
{
Name = model.Name;
Type = model.Type;
- if (Type == ChannelTypes.Voice && _ssrcMapping == null)
- _ssrcMapping = new ConcurrentDictionary();
}
internal void Update(API.ChannelInfo model)
{
@@ -104,12 +101,5 @@ namespace Discord
bool ignored;
return _messages.TryRemove(messageId, out ignored);
}
-
- internal string GetUserId(uint ssrc)
- {
- string userId = null;
- _ssrcMapping.TryGetValue(ssrc, out userId);
- return userId;
- }
}
}
diff --git a/src/Discord.Net/WebSockets/Data/DataWebSocket.cs b/src/Discord.Net/WebSockets/Data/DataWebSocket.cs
index 659d61e37..54578d9aa 100644
--- a/src/Discord.Net/WebSockets/Data/DataWebSocket.cs
+++ b/src/Discord.Net/WebSockets/Data/DataWebSocket.cs
@@ -12,7 +12,7 @@ namespace Discord.WebSockets.Data
public string SessionId => _sessionId;
private string _sessionId;
- public DataWebSocket(DiscordClient client)
+ public DataWebSocket(DiscordBaseClient client)
: base(client)
{
}
diff --git a/src/Discord.Net/WebSockets/Data/DataWebSockets.Events.cs b/src/Discord.Net/WebSockets/Data/DataWebSockets.Events.cs
index 0212fbce3..410829069 100644
--- a/src/Discord.Net/WebSockets/Data/DataWebSockets.Events.cs
+++ b/src/Discord.Net/WebSockets/Data/DataWebSockets.Events.cs
@@ -16,7 +16,7 @@ namespace Discord.WebSockets.Data
internal partial class DataWebSocket
{
- public event EventHandler ReceivedEvent;
+ internal event EventHandler ReceivedEvent;
private void RaiseReceivedEvent(string type, JToken payload)
{
if (ReceivedEvent != null)
diff --git a/src/Discord.Net/WebSockets/Voice/VoiceWebSocket.Events.cs b/src/Discord.Net/WebSockets/Voice/VoiceWebSocket.Events.cs
index 2de2aa918..e2e676986 100644
--- a/src/Discord.Net/WebSockets/Voice/VoiceWebSocket.Events.cs
+++ b/src/Discord.Net/WebSockets/Voice/VoiceWebSocket.Events.cs
@@ -2,7 +2,7 @@
namespace Discord.WebSockets.Voice
{
- public sealed class IsTalkingEventArgs : EventArgs
+ internal sealed class IsTalkingEventArgs : EventArgs
{
public readonly string UserId;
public readonly bool IsSpeaking;
diff --git a/src/Discord.Net/WebSockets/Voice/VoiceWebSocket.cs b/src/Discord.Net/WebSockets/Voice/VoiceWebSocket.cs
index b0a4c2ce5..03ec26ad5 100644
--- a/src/Discord.Net/WebSockets/Voice/VoiceWebSocket.cs
+++ b/src/Discord.Net/WebSockets/Voice/VoiceWebSocket.cs
@@ -15,7 +15,7 @@ using System.Threading.Tasks;
namespace Discord.WebSockets.Voice
{
- internal partial class VoiceWebSocket : WebSocket
+ internal partial class VoiceWebSocket : WebSocket
{
private const int MaxOpusSize = 4000;
private const string EncryptedMode = "xsalsa20_poly1305";
@@ -27,6 +27,7 @@ namespace Discord.WebSockets.Voice
private readonly ConcurrentDictionary _decoders;
private ManualResetEventSlim _connectWaitOnLogin;
private uint _ssrc;
+ private ConcurrentDictionary _ssrcMapping;
private ConcurrentQueue _sendQueue;
private ManualResetEventSlim _sendQueueWait, _sendQueueEmptyWait;
@@ -35,17 +36,16 @@ namespace Discord.WebSockets.Voice
private bool _isClearing, _isEncrypted;
private byte[] _secretKey, _encodingBuffer;
private ushort _sequence;
- private string _userId, _sessionId, _token, _encryptionMode;
- private Server _server;
- private Channel _channel;
+ private string _serverId, _channelId, _userId, _sessionId, _token, _encryptionMode;
#if USE_THREAD
- private Thread _sendThread;
+ private Thread _sendThread, _receiveThread;
#endif
- public Server CurrentVoiceServer => _server;
+ public string CurrentServerId => _serverId;
+ public string CurrentChannelId => _channelId;
- public VoiceWebSocket(DiscordClient client)
+ public VoiceWebSocket(DiscordBaseClient client)
: base(client)
{
_rand = new Random();
@@ -56,12 +56,14 @@ namespace Discord.WebSockets.Voice
_sendQueueEmptyWait = new ManualResetEventSlim(true);
_targetAudioBufferLength = client.Config.VoiceBufferLength / 20; //20 ms frames
_encodingBuffer = new byte[MaxOpusSize];
+ _ssrcMapping = new ConcurrentDictionary();
+ _encoder = new OpusEncoder(48000, 1, 20, Opus.Application.Audio);
}
- public void SetChannel(Server server, Channel channel)
+ public void SetChannel(string serverId, string channelId)
{
- _server = server;
- _channel = channel;
+ _serverId = serverId;
+ _channelId = channelId;
}
public async Task Login(string userId, string sessionId, string token, CancellationToken cancelToken)
{
@@ -113,7 +115,7 @@ namespace Discord.WebSockets.Voice
#endif
LoginCommand msg = new LoginCommand();
- msg.Payload.ServerId = _server.Id;
+ msg.Payload.ServerId = _serverId;
msg.Payload.SessionId = _sessionId;
msg.Payload.Token = _token;
msg.Payload.UserId = _userId;
@@ -122,6 +124,8 @@ namespace Discord.WebSockets.Voice
#if USE_THREAD
_sendThread = new Thread(new ThreadStart(() => SendVoiceAsync(_cancelToken)));
_sendThread.Start();
+ _receiveThread = new Thread(new ThreadStart(() => ReceiveVoiceAsync(_cancelToken)));
+ _receiveThread.Start();
#if !DNXCORE50
return new Task[] { WatcherAsync() }.Concat(base.Run()).ToArray();
#else
@@ -141,9 +145,11 @@ namespace Discord.WebSockets.Voice
{
#if USE_THREAD
_sendThread.Join();
+ _receiveThread.Join();
_sendThread = null;
+ _receiveThread = null;
#endif
-
+
OpusDecoder decoder;
foreach (var pair in _decoders)
{
@@ -274,9 +280,9 @@ namespace Discord.WebSockets.Voice
/*if (_logLevel >= LogMessageSeverity.Debug)
RaiseOnLog(LogMessageSeverity.Debug, $"Received {buffer.Length - 12} bytes.");*/
- string userId = _channel.GetUserId(ssrc);
- if (userId != null)
- RaiseOnPacket(userId, _channel.Id, result, resultOffset, resultLength);
+ string userId;
+ if (_ssrcMapping.TryGetValue(ssrc, out userId))
+ RaiseOnPacket(userId, _channelId, result, resultOffset, resultLength);
}
}
#if USE_THREAD || DNXCORE50
@@ -568,9 +574,9 @@ namespace Discord.WebSockets.Voice
{
_sendQueueEmptyWait.Wait(_cancelToken);
}
- public void WaitForConnection()
+ public void WaitForConnection(CancellationToken cancelToken)
{
- _connectedEvent.Wait();
+ _connectedEvent.Wait(cancelToken);
}
}
}
diff --git a/src/Discord.Net/WebSockets/WebSocket.Events.cs b/src/Discord.Net/WebSockets/WebSocket.Events.cs
index c2ef3e79e..9824fbff8 100644
--- a/src/Discord.Net/WebSockets/WebSocket.Events.cs
+++ b/src/Discord.Net/WebSockets/WebSocket.Events.cs
@@ -2,7 +2,7 @@
namespace Discord.WebSockets
{
- internal partial class WebSocket
+ internal abstract partial class WebSocket
{
public event EventHandler Connected;
private void RaiseConnected()
diff --git a/src/Discord.Net/WebSockets/WebSocket.cs b/src/Discord.Net/WebSockets/WebSocket.cs
index c9cc27a62..a1bb06462 100644
--- a/src/Discord.Net/WebSockets/WebSocket.cs
+++ b/src/Discord.Net/WebSockets/WebSocket.cs
@@ -35,7 +35,7 @@ namespace Discord.WebSockets
internal abstract partial class WebSocket
{
protected readonly IWebSocketEngine _engine;
- protected readonly DiscordClient _client;
+ protected readonly DiscordBaseClient _client;
protected readonly LogMessageSeverity _logLevel;
protected readonly ManualResetEventSlim _connectedEvent;
@@ -57,7 +57,7 @@ namespace Discord.WebSockets
public WebSocketState State => (WebSocketState)_state;
protected int _state;
- public WebSocket(DiscordClient client)
+ public WebSocket(DiscordBaseClient client)
{
_client = client;
_logLevel = client.Config.LogLevel;
@@ -131,9 +131,9 @@ namespace Discord.WebSockets
_disconnectState = (WebSocketState)oldState;
_disconnectReason = ex != null ? ExceptionDispatchInfo.Capture(ex) : null;
- if (_disconnectState == WebSocketState.Connecting) //_runTask was never made
- await Cleanup();
_cancelTokenSource.Cancel();
+ if (_disconnectState == WebSocketState.Connecting) //_runTask was never made
+ await Cleanup().ConfigureAwait(false);
}
if (!skipAwait)
@@ -161,8 +161,8 @@ namespace Discord.WebSockets
//Wait for the remaining tasks to complete
try { await allTasks.ConfigureAwait(false); }
catch { }
-
- //Clean up state variables and raise disconnect event
+
+ //Start cleanup
await Cleanup().ConfigureAwait(false);
}
protected virtual Task[] Run()