| @@ -136,6 +136,12 @@ | |||
| <Compile Include="..\Discord.Net\DiscordAPIClient.cs"> | |||
| <Link>DiscordAPIClient.cs</Link> | |||
| </Compile> | |||
| <Compile Include="..\Discord.Net\DiscordBaseClient.cs"> | |||
| <Link>DiscordBaseClient.cs</Link> | |||
| </Compile> | |||
| <Compile Include="..\Discord.Net\DiscordBaseClient.Events.cs"> | |||
| <Link>DiscordBaseClient.Events.cs</Link> | |||
| </Compile> | |||
| <Compile Include="..\Discord.Net\DiscordClient.API.cs"> | |||
| <Link>DiscordClient.API.cs</Link> | |||
| </Compile> | |||
| @@ -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<DisconnectedEventArgs> Disconnected; | |||
| private void RaiseDisconnected(DisconnectedEventArgs e) | |||
| { | |||
| if (Disconnected != null) | |||
| RaiseEvent(nameof(Disconnected), () => Disconnected(this, e)); | |||
| } | |||
| public event EventHandler<LogMessageEventArgs> 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<DisconnectedEventArgs> VoiceDisconnected; | |||
| private void RaiseVoiceDisconnected(DisconnectedEventArgs e) | |||
| { | |||
| if (VoiceDisconnected != null) | |||
| RaiseEvent(nameof(VoiceDisconnected), () => VoiceDisconnected(this, e)); | |||
| } | |||
| public event EventHandler<VoicePacketEventArgs> OnVoicePacket; | |||
| internal void RaiseOnVoicePacket(VoicePacketEventArgs e) | |||
| { | |||
| if (OnVoicePacket != null) | |||
| OnVoicePacket(this, e); | |||
| } | |||
| } | |||
| } | |||
| @@ -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 | |||
| } | |||
| /// <summary> Provides a barebones connection to the Discord service </summary> | |||
| 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; | |||
| /// <summary> Returns the id of the current logged-in user. </summary> | |||
| public string CurrentUserId => _currentUserId; | |||
| private string _currentUserId; | |||
| /*/// <summary> Returns the server this user is currently connected to for voice. </summary> | |||
| public string CurrentVoiceServerId => _voiceSocket.CurrentServerId;*/ | |||
| /// <summary> Returns the current connection state of this client. </summary> | |||
| public DiscordClientState State => (DiscordClientState)_state; | |||
| private int _state; | |||
| /// <summary> Returns the configuration object used to make this client. Note that this object cannot be edited directly - to change the configuration of this client, use the DiscordClient(DiscordClientConfig config) constructor. </summary> | |||
| public DiscordClientConfig Config => _config; | |||
| protected readonly DiscordClientConfig _config; | |||
| public CancellationToken CancelToken => _cancelToken; | |||
| private CancellationTokenSource _cancelTokenSource; | |||
| private CancellationToken _cancelToken; | |||
| /// <summary> Initializes a new instance of the DiscordClient class. </summary> | |||
| 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<string> 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(); | |||
| } | |||
| /// <summary> Disconnects from the Discord server, canceling any pending requests. </summary> | |||
| public Task Disconnect() => DisconnectInternal(new Exception("Disconnect was requested by user."), isUnexpected: false); | |||
| protected 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 | |||
| /// <summary> Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications. </summary> | |||
| public void Block() | |||
| { | |||
| _disconnectedEvent.WaitOne(); | |||
| } | |||
| 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<string>("id"); | |||
| return TaskHelper.CompletedTask; | |||
| } | |||
| } | |||
| } | |||
| @@ -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<EditProfileResponse> EditProfile(string currentPassword, | |||
| public Task<EditProfileResponse> 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); | |||
| } | |||
| @@ -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<DisconnectedEventArgs> Disconnected; | |||
| private void RaiseDisconnected(DisconnectedEventArgs e) | |||
| { | |||
| if (Disconnected != null) | |||
| RaiseEvent(nameof(Disconnected), () => Disconnected(this, e)); | |||
| } | |||
| public event EventHandler<LogMessageEventArgs> 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<ServerEventArgs> 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<DisconnectedEventArgs> VoiceDisconnected; | |||
| private void RaiseVoiceDisconnected(DisconnectedEventArgs e) | |||
| { | |||
| if (VoiceDisconnected != null) | |||
| RaiseEvent(nameof(UserIsSpeaking), () => VoiceDisconnected(this, e)); | |||
| } | |||
| public event EventHandler<VoicePacketEventArgs> OnVoicePacket; | |||
| internal void RaiseOnVoicePacket(VoicePacketEventArgs e) | |||
| { | |||
| if (OnVoicePacket != null) | |||
| OnVoicePacket(this, e); | |||
| } | |||
| } | |||
| } | |||
| @@ -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); | |||
| } | |||
| } | |||
| } | |||
| @@ -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<T> Timeout<T>(this Task<T> self, int milliseconds, CancellationTokenSource cancelToken) | |||
| { | |||
| try | |||
| { | |||
| cancelToken.CancelAfter(milliseconds); | |||
| return await self; | |||
| } | |||
| catch (OperationCanceledException) | |||
| { | |||
| throw new TimeoutException(); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -17,7 +17,6 @@ namespace Discord | |||
| private readonly DiscordClient _client; | |||
| private ConcurrentDictionary<string, bool> _messages; | |||
| private ConcurrentDictionary<uint, string> _ssrcMapping; | |||
| /// <summary> Returns the unique identifier for this channel. </summary> | |||
| 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<uint, string>(); | |||
| } | |||
| 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; | |||
| } | |||
| } | |||
| } | |||
| @@ -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) | |||
| { | |||
| } | |||
| @@ -16,7 +16,7 @@ namespace Discord.WebSockets.Data | |||
| internal partial class DataWebSocket | |||
| { | |||
| public event EventHandler<WebSocketEventEventArgs> ReceivedEvent; | |||
| internal event EventHandler<WebSocketEventEventArgs> ReceivedEvent; | |||
| private void RaiseReceivedEvent(string type, JToken payload) | |||
| { | |||
| if (ReceivedEvent != null) | |||
| @@ -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; | |||
| @@ -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<uint, OpusDecoder> _decoders; | |||
| private ManualResetEventSlim _connectWaitOnLogin; | |||
| private uint _ssrc; | |||
| private ConcurrentDictionary<uint, string> _ssrcMapping; | |||
| private ConcurrentQueue<byte[]> _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<uint, string>(); | |||
| _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); | |||
| } | |||
| } | |||
| } | |||
| @@ -2,7 +2,7 @@ | |||
| namespace Discord.WebSockets | |||
| { | |||
| internal partial class WebSocket | |||
| internal abstract partial class WebSocket | |||
| { | |||
| public event EventHandler Connected; | |||
| private void RaiseConnected() | |||
| @@ -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() | |||