| @@ -136,6 +136,12 @@ | |||||
| <Compile Include="..\Discord.Net\DiscordAPIClient.cs"> | <Compile Include="..\Discord.Net\DiscordAPIClient.cs"> | ||||
| <Link>DiscordAPIClient.cs</Link> | <Link>DiscordAPIClient.cs</Link> | ||||
| </Compile> | </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"> | <Compile Include="..\Discord.Net\DiscordClient.API.cs"> | ||||
| <Link>DiscordClient.API.cs</Link> | <Link>DiscordClient.API.cs</Link> | ||||
| </Compile> | </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.API; | ||||
| using Discord.Helpers; | |||||
| using System; | using System; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Linq; | using System.Linq; | ||||
| @@ -98,7 +97,7 @@ namespace Discord | |||||
| channel = user.PrivateChannel; | channel = user.PrivateChannel; | ||||
| if (channel == null) | 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 = _channels.GetOrAdd(response.Id, response.GuildId, response.Recipient?.Id); | ||||
| channel.Update(response); | channel.Update(response); | ||||
| } | } | ||||
| @@ -266,13 +265,13 @@ namespace Discord | |||||
| var nonce = GenerateNonce(); | var nonce = GenerateNonce(); | ||||
| if (_config.UseMessageQueue) | 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]; | var currentMember = _members[msg.UserId, channel.ServerId]; | ||||
| msg.Update(new API.Message | msg.Update(new API.Message | ||||
| { | { | ||||
| Content = blockText, | Content = blockText, | ||||
| Timestamp = DateTime.UtcNow, | 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, | ChannelId = channel.Id, | ||||
| IsTextToSpeech = isTextToSpeech | IsTextToSpeech = isTextToSpeech | ||||
| }); | }); | ||||
| @@ -513,13 +512,13 @@ namespace Discord | |||||
| } | } | ||||
| //Profile | //Profile | ||||
| public Task<EditProfileResponse> EditProfile(string currentPassword, | |||||
| public Task<EditProfileResponse> EditProfile(string currentPassword = "", | |||||
| string username = null, string email = null, string password = null, | string username = null, string email = null, string password = null, | ||||
| AvatarImageType avatarType = AvatarImageType.Png, byte[] avatar = null) | AvatarImageType avatarType = AvatarImageType.Png, byte[] avatar = null) | ||||
| { | { | ||||
| if (currentPassword == null) throw new ArgumentNullException(nameof(currentPassword)); | 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); | avatarType: avatarType, avatar: avatar); | ||||
| } | } | ||||
| @@ -2,50 +2,6 @@ | |||||
| namespace Discord | 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 sealed class ServerEventArgs : EventArgs | ||||
| { | { | ||||
| public Server Server { get; } | public Server Server { get; } | ||||
| @@ -148,45 +104,9 @@ namespace Discord | |||||
| IsSpeaking = isSpeaking; | 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 | 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 | //Server | ||||
| public event EventHandler<ServerEventArgs> ServerCreated; | public event EventHandler<ServerEventArgs> ServerCreated; | ||||
| private void RaiseServerCreated(Server server) | private void RaiseServerCreated(Server server) | ||||
| @@ -342,26 +262,5 @@ namespace Discord | |||||
| if (UserIsSpeaking != null) | if (UserIsSpeaking != null) | ||||
| RaiseEvent(nameof(UserIsSpeaking), () => UserIsSpeaking(this, new UserIsSpeakingEventArgs(member, isSpeaking))); | 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.Helpers; | ||||
| using Discord.WebSockets; | using Discord.WebSockets; | ||||
| using System; | using System; | ||||
| using System.Threading; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| namespace Discord | namespace Discord | ||||
| @@ -8,30 +9,31 @@ namespace Discord | |||||
| public partial class DiscordClient | public partial class DiscordClient | ||||
| { | { | ||||
| public Task JoinVoiceServer(Channel channel) | 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) | 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); | 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); | 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 | try | ||||
| { | { | ||||
| await Task.Run(() => _voiceSocket.WaitForConnection()) | |||||
| .Timeout(_config.ConnectionTimeout) | |||||
| await Task.Run(() => _voiceSocket.WaitForConnection(tokenSource.Token)) | |||||
| .Timeout(_config.ConnectionTimeout, tokenSource) | |||||
| .ConfigureAwait(false); | .ConfigureAwait(false); | ||||
| } | } | ||||
| catch (TaskCanceledException) | |||||
| catch (TimeoutException) | |||||
| { | { | ||||
| tokenSource.Cancel(); | |||||
| await LeaveVoiceServer().ConfigureAwait(false); | await LeaveVoiceServer().ConfigureAwait(false); | ||||
| throw; | |||||
| } | } | ||||
| } | } | ||||
| public async Task LeaveVoiceServer() | public async Task LeaveVoiceServer() | ||||
| @@ -40,11 +42,11 @@ namespace Discord | |||||
| if (_voiceSocket.State != WebSocketState.Disconnected) | if (_voiceSocket.State != WebSocketState.Disconnected) | ||||
| { | { | ||||
| var server = _voiceSocket.CurrentVoiceServer; | |||||
| if (server != null) | |||||
| var serverId = _voiceSocket.CurrentServerId; | |||||
| if (serverId != null) | |||||
| { | { | ||||
| await _voiceSocket.Disconnect().ConfigureAwait(false); | 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 | namespace Discord.Helpers | ||||
| { | { | ||||
| @@ -32,5 +34,29 @@ namespace Discord.Helpers | |||||
| else | else | ||||
| return await self.ConfigureAwait(false); | 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 readonly DiscordClient _client; | ||||
| private ConcurrentDictionary<string, bool> _messages; | private ConcurrentDictionary<string, bool> _messages; | ||||
| private ConcurrentDictionary<uint, string> _ssrcMapping; | |||||
| /// <summary> Returns the unique identifier for this channel. </summary> | /// <summary> Returns the unique identifier for this channel. </summary> | ||||
| public string Id { get; } | public string Id { get; } | ||||
| @@ -70,8 +69,6 @@ namespace Discord | |||||
| { | { | ||||
| Name = model.Name; | Name = model.Name; | ||||
| Type = model.Type; | Type = model.Type; | ||||
| if (Type == ChannelTypes.Voice && _ssrcMapping == null) | |||||
| _ssrcMapping = new ConcurrentDictionary<uint, string>(); | |||||
| } | } | ||||
| internal void Update(API.ChannelInfo model) | internal void Update(API.ChannelInfo model) | ||||
| { | { | ||||
| @@ -104,12 +101,5 @@ namespace Discord | |||||
| bool ignored; | bool ignored; | ||||
| return _messages.TryRemove(messageId, out 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; | public string SessionId => _sessionId; | ||||
| private string _sessionId; | private string _sessionId; | ||||
| public DataWebSocket(DiscordClient client) | |||||
| public DataWebSocket(DiscordBaseClient client) | |||||
| : base(client) | : base(client) | ||||
| { | { | ||||
| } | } | ||||
| @@ -16,7 +16,7 @@ namespace Discord.WebSockets.Data | |||||
| internal partial class DataWebSocket | internal partial class DataWebSocket | ||||
| { | { | ||||
| public event EventHandler<WebSocketEventEventArgs> ReceivedEvent; | |||||
| internal event EventHandler<WebSocketEventEventArgs> ReceivedEvent; | |||||
| private void RaiseReceivedEvent(string type, JToken payload) | private void RaiseReceivedEvent(string type, JToken payload) | ||||
| { | { | ||||
| if (ReceivedEvent != null) | if (ReceivedEvent != null) | ||||
| @@ -2,7 +2,7 @@ | |||||
| namespace Discord.WebSockets.Voice | namespace Discord.WebSockets.Voice | ||||
| { | { | ||||
| public sealed class IsTalkingEventArgs : EventArgs | |||||
| internal sealed class IsTalkingEventArgs : EventArgs | |||||
| { | { | ||||
| public readonly string UserId; | public readonly string UserId; | ||||
| public readonly bool IsSpeaking; | public readonly bool IsSpeaking; | ||||
| @@ -15,7 +15,7 @@ using System.Threading.Tasks; | |||||
| namespace Discord.WebSockets.Voice | namespace Discord.WebSockets.Voice | ||||
| { | { | ||||
| internal partial class VoiceWebSocket : WebSocket | |||||
| internal partial class VoiceWebSocket : WebSocket | |||||
| { | { | ||||
| private const int MaxOpusSize = 4000; | private const int MaxOpusSize = 4000; | ||||
| private const string EncryptedMode = "xsalsa20_poly1305"; | private const string EncryptedMode = "xsalsa20_poly1305"; | ||||
| @@ -27,6 +27,7 @@ namespace Discord.WebSockets.Voice | |||||
| private readonly ConcurrentDictionary<uint, OpusDecoder> _decoders; | private readonly ConcurrentDictionary<uint, OpusDecoder> _decoders; | ||||
| private ManualResetEventSlim _connectWaitOnLogin; | private ManualResetEventSlim _connectWaitOnLogin; | ||||
| private uint _ssrc; | private uint _ssrc; | ||||
| private ConcurrentDictionary<uint, string> _ssrcMapping; | |||||
| private ConcurrentQueue<byte[]> _sendQueue; | private ConcurrentQueue<byte[]> _sendQueue; | ||||
| private ManualResetEventSlim _sendQueueWait, _sendQueueEmptyWait; | private ManualResetEventSlim _sendQueueWait, _sendQueueEmptyWait; | ||||
| @@ -35,17 +36,16 @@ namespace Discord.WebSockets.Voice | |||||
| private bool _isClearing, _isEncrypted; | private bool _isClearing, _isEncrypted; | ||||
| private byte[] _secretKey, _encodingBuffer; | private byte[] _secretKey, _encodingBuffer; | ||||
| private ushort _sequence; | 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 | #if USE_THREAD | ||||
| private Thread _sendThread; | |||||
| private Thread _sendThread, _receiveThread; | |||||
| #endif | #endif | ||||
| public Server CurrentVoiceServer => _server; | |||||
| public string CurrentServerId => _serverId; | |||||
| public string CurrentChannelId => _channelId; | |||||
| public VoiceWebSocket(DiscordClient client) | |||||
| public VoiceWebSocket(DiscordBaseClient client) | |||||
| : base(client) | : base(client) | ||||
| { | { | ||||
| _rand = new Random(); | _rand = new Random(); | ||||
| @@ -56,12 +56,14 @@ namespace Discord.WebSockets.Voice | |||||
| _sendQueueEmptyWait = new ManualResetEventSlim(true); | _sendQueueEmptyWait = new ManualResetEventSlim(true); | ||||
| _targetAudioBufferLength = client.Config.VoiceBufferLength / 20; //20 ms frames | _targetAudioBufferLength = client.Config.VoiceBufferLength / 20; //20 ms frames | ||||
| _encodingBuffer = new byte[MaxOpusSize]; | _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) | public async Task Login(string userId, string sessionId, string token, CancellationToken cancelToken) | ||||
| { | { | ||||
| @@ -113,7 +115,7 @@ namespace Discord.WebSockets.Voice | |||||
| #endif | #endif | ||||
| LoginCommand msg = new LoginCommand(); | LoginCommand msg = new LoginCommand(); | ||||
| msg.Payload.ServerId = _server.Id; | |||||
| msg.Payload.ServerId = _serverId; | |||||
| msg.Payload.SessionId = _sessionId; | msg.Payload.SessionId = _sessionId; | ||||
| msg.Payload.Token = _token; | msg.Payload.Token = _token; | ||||
| msg.Payload.UserId = _userId; | msg.Payload.UserId = _userId; | ||||
| @@ -122,6 +124,8 @@ namespace Discord.WebSockets.Voice | |||||
| #if USE_THREAD | #if USE_THREAD | ||||
| _sendThread = new Thread(new ThreadStart(() => SendVoiceAsync(_cancelToken))); | _sendThread = new Thread(new ThreadStart(() => SendVoiceAsync(_cancelToken))); | ||||
| _sendThread.Start(); | _sendThread.Start(); | ||||
| _receiveThread = new Thread(new ThreadStart(() => ReceiveVoiceAsync(_cancelToken))); | |||||
| _receiveThread.Start(); | |||||
| #if !DNXCORE50 | #if !DNXCORE50 | ||||
| return new Task[] { WatcherAsync() }.Concat(base.Run()).ToArray(); | return new Task[] { WatcherAsync() }.Concat(base.Run()).ToArray(); | ||||
| #else | #else | ||||
| @@ -141,9 +145,11 @@ namespace Discord.WebSockets.Voice | |||||
| { | { | ||||
| #if USE_THREAD | #if USE_THREAD | ||||
| _sendThread.Join(); | _sendThread.Join(); | ||||
| _receiveThread.Join(); | |||||
| _sendThread = null; | _sendThread = null; | ||||
| _receiveThread = null; | |||||
| #endif | #endif | ||||
| OpusDecoder decoder; | OpusDecoder decoder; | ||||
| foreach (var pair in _decoders) | foreach (var pair in _decoders) | ||||
| { | { | ||||
| @@ -274,9 +280,9 @@ namespace Discord.WebSockets.Voice | |||||
| /*if (_logLevel >= LogMessageSeverity.Debug) | /*if (_logLevel >= LogMessageSeverity.Debug) | ||||
| RaiseOnLog(LogMessageSeverity.Debug, $"Received {buffer.Length - 12} bytes.");*/ | 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 | #if USE_THREAD || DNXCORE50 | ||||
| @@ -568,9 +574,9 @@ namespace Discord.WebSockets.Voice | |||||
| { | { | ||||
| _sendQueueEmptyWait.Wait(_cancelToken); | _sendQueueEmptyWait.Wait(_cancelToken); | ||||
| } | } | ||||
| public void WaitForConnection() | |||||
| public void WaitForConnection(CancellationToken cancelToken) | |||||
| { | { | ||||
| _connectedEvent.Wait(); | |||||
| _connectedEvent.Wait(cancelToken); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -2,7 +2,7 @@ | |||||
| namespace Discord.WebSockets | namespace Discord.WebSockets | ||||
| { | { | ||||
| internal partial class WebSocket | |||||
| internal abstract partial class WebSocket | |||||
| { | { | ||||
| public event EventHandler Connected; | public event EventHandler Connected; | ||||
| private void RaiseConnected() | private void RaiseConnected() | ||||
| @@ -35,7 +35,7 @@ namespace Discord.WebSockets | |||||
| internal abstract partial class WebSocket | internal abstract partial class WebSocket | ||||
| { | { | ||||
| protected readonly IWebSocketEngine _engine; | protected readonly IWebSocketEngine _engine; | ||||
| protected readonly DiscordClient _client; | |||||
| protected readonly DiscordBaseClient _client; | |||||
| protected readonly LogMessageSeverity _logLevel; | protected readonly LogMessageSeverity _logLevel; | ||||
| protected readonly ManualResetEventSlim _connectedEvent; | protected readonly ManualResetEventSlim _connectedEvent; | ||||
| @@ -57,7 +57,7 @@ namespace Discord.WebSockets | |||||
| public WebSocketState State => (WebSocketState)_state; | public WebSocketState State => (WebSocketState)_state; | ||||
| protected int _state; | protected int _state; | ||||
| public WebSocket(DiscordClient client) | |||||
| public WebSocket(DiscordBaseClient client) | |||||
| { | { | ||||
| _client = client; | _client = client; | ||||
| _logLevel = client.Config.LogLevel; | _logLevel = client.Config.LogLevel; | ||||
| @@ -131,9 +131,9 @@ namespace Discord.WebSockets | |||||
| _disconnectState = (WebSocketState)oldState; | _disconnectState = (WebSocketState)oldState; | ||||
| _disconnectReason = ex != null ? ExceptionDispatchInfo.Capture(ex) : null; | _disconnectReason = ex != null ? ExceptionDispatchInfo.Capture(ex) : null; | ||||
| if (_disconnectState == WebSocketState.Connecting) //_runTask was never made | |||||
| await Cleanup(); | |||||
| _cancelTokenSource.Cancel(); | _cancelTokenSource.Cancel(); | ||||
| if (_disconnectState == WebSocketState.Connecting) //_runTask was never made | |||||
| await Cleanup().ConfigureAwait(false); | |||||
| } | } | ||||
| if (!skipAwait) | if (!skipAwait) | ||||
| @@ -161,8 +161,8 @@ namespace Discord.WebSockets | |||||
| //Wait for the remaining tasks to complete | //Wait for the remaining tasks to complete | ||||
| try { await allTasks.ConfigureAwait(false); } | try { await allTasks.ConfigureAwait(false); } | ||||
| catch { } | catch { } | ||||
| //Clean up state variables and raise disconnect event | |||||
| //Start cleanup | |||||
| await Cleanup().ConfigureAwait(false); | await Cleanup().ConfigureAwait(false); | ||||
| } | } | ||||
| protected virtual Task[] Run() | protected virtual Task[] Run() | ||||