@@ -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() | |||