From a8ca9946671c739bba57c8e50a21bd411e8b09d8 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 26 Sep 2015 23:26:27 -0300 Subject: [PATCH] Split DiscordClient into DiscordBaseClient and DiscordClient. Several fixes. --- src/Discord.Net.Net45/Discord.Net.csproj | 6 + src/Discord.Net/DiscordBaseClient.Events.cs | 107 +++ src/Discord.Net/DiscordBaseClient.cs | 272 ++++++ src/Discord.Net/DiscordClient.API.cs | 11 +- src/Discord.Net/DiscordClient.Events.cs | 101 -- src/Discord.Net/DiscordClient.Voice.cs | 32 +- src/Discord.Net/DiscordClient.cs | 1008 ++++++++------------ src/Discord.Net/Helpers/TaskHelper.cs | 28 +- src/Discord.Net/Models/Channel.cs | 10 - src/Discord.Net/WebSockets/Data/DataWebSocket.cs | 2 +- .../WebSockets/Data/DataWebSockets.Events.cs | 2 +- .../WebSockets/Voice/VoiceWebSocket.Events.cs | 2 +- src/Discord.Net/WebSockets/Voice/VoiceWebSocket.cs | 40 +- src/Discord.Net/WebSockets/WebSocket.Events.cs | 2 +- src/Discord.Net/WebSockets/WebSocket.cs | 12 +- 15 files changed, 868 insertions(+), 767 deletions(-) create mode 100644 src/Discord.Net/DiscordBaseClient.Events.cs create mode 100644 src/Discord.Net/DiscordBaseClient.cs 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()