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