Refactoring and fixed a few stylecop errors

This commit is contained in:
RogueException
2015-12-09 00:56:09 -04:00
parent a44e08bba1
commit 82746e9207
25 changed files with 574 additions and 505 deletions

View File

@@ -67,18 +67,21 @@
<Compile Include="..\Discord.Net.Audio\Net\WebSockets\VoiceWebSocket.Events.cs"> <Compile Include="..\Discord.Net.Audio\Net\WebSockets\VoiceWebSocket.Events.cs">
<Link>Net\WebSockets\VoiceWebSocket.Events.cs</Link> <Link>Net\WebSockets\VoiceWebSocket.Events.cs</Link>
</Compile> </Compile>
<Compile Include="..\Discord.Net.Audio\Opus.cs"> <Compile Include="..\Discord.Net.Audio\Opus\Enums.cs">
<Link>Opus.cs</Link> <Link>Opus\Enums.cs</Link>
</Compile> </Compile>
<Compile Include="..\Discord.Net.Audio\OpusDecoder.cs"> <Compile Include="..\Discord.Net.Audio\Opus\OpusDecoder.cs">
<Link>OpusDecoder.cs</Link> <Link>Opus\OpusDecoder.cs</Link>
</Compile> </Compile>
<Compile Include="..\Discord.Net.Audio\OpusEncoder.cs"> <Compile Include="..\Discord.Net.Audio\Opus\OpusEncoder.cs">
<Link>OpusEncoder.cs</Link> <Link>Opus\OpusEncoder.cs</Link>
</Compile> </Compile>
<Compile Include="..\Discord.Net.Audio\Sodium.cs"> <Compile Include="..\Discord.Net.Audio\Sodium.cs">
<Link>Sodium.cs</Link> <Link>Sodium.cs</Link>
</Compile> </Compile>
<Compile Include="..\Discord.Net.Audio\Sodium\SecretBox.cs">
<Link>Sodium\SecretBox.cs</Link>
</Compile>
<Compile Include="..\Discord.Net.Audio\VoiceBuffer.cs"> <Compile Include="..\Discord.Net.Audio\VoiceBuffer.cs">
<Link>VoiceBuffer.cs</Link> <Link>VoiceBuffer.cs</Link>
</Compile> </Compile>

View File

@@ -145,27 +145,27 @@ namespace Discord.Audio
return Task.FromResult(_defaultClient); return Task.FromResult(_defaultClient);
} }
var client = _voiceClients.GetOrAdd(server.Id, _ => var client = _voiceClients.GetOrAdd(server.Id, (Func<long, DiscordAudioClient>)(_ =>
{ {
int id = unchecked(++_nextClientId); int id = unchecked(++_nextClientId);
var logger = Client.Log().CreateLogger($"Voice #{id}"); var logger = Client.Log().CreateLogger($"Voice #{id}");
GatewayWebSocket gatewaySocket = null; Net.WebSockets.GatewaySocket gatewaySocket = null;
var voiceSocket = new VoiceWebSocket(Client.Config, _config, logger); var voiceSocket = new VoiceWebSocket(Client.Config, _config, logger);
var voiceClient = new DiscordAudioClient(this, id, logger, gatewaySocket, voiceSocket); var voiceClient = new DiscordAudioClient((AudioService)(this), (int)id, (Logger)logger, (Net.WebSockets.GatewaySocket)gatewaySocket, (VoiceWebSocket)voiceSocket);
voiceClient.SetServerId(server.Id); voiceClient.SetServerId(server.Id);
voiceSocket.OnPacket += (s, e) => voiceSocket.OnPacket += (s, e) =>
{ {
RaiseOnPacket(e); RaiseOnPacket(e);
}; };
voiceSocket.IsSpeaking += (s, e) => voiceSocket.IsSpeaking += (s, e) =>
{ {
var user = Client.GetUser(server, e.UserId); var user = Client.GetUser(server, e.UserId);
RaiseUserIsSpeakingUpdated(user, e.IsSpeaking); RaiseUserIsSpeakingUpdated(user, e.IsSpeaking);
}; };
return voiceClient; return voiceClient;
}); }));
//await client.Connect(gatewaySocket.Host, _client.Token).ConfigureAwait(false); //await client.Connect(gatewaySocket.Host, _client.Token).ConfigureAwait(false);
return Task.FromResult(client); return Task.FromResult(client);
} }

View File

@@ -37,8 +37,12 @@ namespace Discord.Audio
public int? Bitrate { get { return _bitrate; } set { SetValue(ref _bitrate, value); } } public int? Bitrate { get { return _bitrate; } set { SetValue(ref _bitrate, value); } }
private int? _bitrate = null; private int? _bitrate = null;
//Lock /// <summary> Gets or sets the number of channels (1 or 2) used for outgoing audio. </summary>
protected bool _isLocked; public int Channels { get { return _channels; } set { SetValue(ref _channels, value); } }
private int _channels = 1;
//Lock
protected bool _isLocked;
internal void Lock() { _isLocked = true; } internal void Lock() { _isLocked = true; }
protected void SetValue<T>(ref T storage, T value) protected void SetValue<T>(ref T storage, T value)
{ {

View File

@@ -10,14 +10,14 @@ namespace Discord.Audio
public int Id => _id; public int Id => _id;
private readonly AudioService _service; private readonly AudioService _service;
private readonly GatewayWebSocket _gatewaySocket; private readonly GatewaySocket _gatewaySocket;
private readonly VoiceWebSocket _voiceSocket; private readonly VoiceWebSocket _voiceSocket;
private readonly Logger _logger; private readonly Logger _logger;
public long? ServerId => _voiceSocket.ServerId; public long? ServerId => _voiceSocket.ServerId;
public long? ChannelId => _voiceSocket.ChannelId; public long? ChannelId => _voiceSocket.ChannelId;
public DiscordAudioClient(AudioService service, int id, Logger logger, GatewayWebSocket gatewaySocket, VoiceWebSocket voiceSocket) public DiscordAudioClient(AudioService service, int id, Logger logger, GatewaySocket gatewaySocket, VoiceWebSocket voiceSocket)
{ {
_service = service; _service = service;
_id = id; _id = id;

View File

@@ -1,5 +1,7 @@
using Discord.API; using Discord.API;
using Discord.Audio; using Discord.Audio;
using Discord.Audio.Opus;
using Discord.Audio.Sodium;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using System; using System;
@@ -54,7 +56,7 @@ namespace Discord.Net.WebSockets
_targetAudioBufferLength = _audioConfig.BufferLength / 20; //20 ms frames _targetAudioBufferLength = _audioConfig.BufferLength / 20; //20 ms frames
_encodingBuffer = new byte[MaxOpusSize]; _encodingBuffer = new byte[MaxOpusSize];
_ssrcMapping = new ConcurrentDictionary<uint, long>(); _ssrcMapping = new ConcurrentDictionary<uint, long>();
_encoder = new OpusEncoder(48000, 1, 20, _audioConfig.Bitrate, Opus.Application.Audio); _encoder = new OpusEncoder(48000, _audioConfig.Channels, 20, _audioConfig.Bitrate, OpusApplication.Audio);
_sendBuffer = new VoiceBuffer((int)Math.Ceiling(_audioConfig.BufferLength / (double)_encoder.FrameLength), _encoder.FrameSize); _sendBuffer = new VoiceBuffer((int)Math.Ceiling(_audioConfig.BufferLength / (double)_encoder.FrameLength), _encoder.FrameSize);
} }
@@ -223,7 +225,7 @@ namespace Discord.Net.WebSockets
return; return;
Buffer.BlockCopy(packet, 0, nonce, 0, 12); Buffer.BlockCopy(packet, 0, nonce, 0, 12);
int ret = Sodium.Decrypt(packet, 12, packetLength - 12, decodingBuffer, nonce, _secretKey); int ret = SecretBox.Decrypt(packet, 12, packetLength - 12, decodingBuffer, nonce, _secretKey);
if (ret != 0) if (ret != 0)
continue; continue;
result = decodingBuffer; result = decodingBuffer;
@@ -294,7 +296,7 @@ namespace Discord.Net.WebSockets
if (_isEncrypted) if (_isEncrypted)
{ {
Buffer.BlockCopy(pingPacket, 0, nonce, 0, 8); Buffer.BlockCopy(pingPacket, 0, nonce, 0, 8);
int ret = Sodium.Encrypt(pingPacket, 8, encodedFrame, 0, nonce, _secretKey); int ret = SecretBox.Encrypt(pingPacket, 8, encodedFrame, 0, nonce, _secretKey);
if (ret != 0) if (ret != 0)
throw new InvalidOperationException("Failed to encrypt ping packet"); throw new InvalidOperationException("Failed to encrypt ping packet");
pingPacket = new byte[pingPacket.Length + 16]; pingPacket = new byte[pingPacket.Length + 16];
@@ -333,7 +335,7 @@ namespace Discord.Net.WebSockets
if (_isEncrypted) if (_isEncrypted)
{ {
Buffer.BlockCopy(voicePacket, 2, nonce, 2, 6); //Update nonce Buffer.BlockCopy(voicePacket, 2, nonce, 2, 6); //Update nonce
int ret = Sodium.Encrypt(encodedFrame, encodedLength, voicePacket, 12, nonce, _secretKey); int ret = SecretBox.Encrypt(encodedFrame, encodedLength, voicePacket, 12, nonce, _secretKey);
if (ret != 0) if (ret != 0)
continue; continue;
rtpPacketLength = encodedLength + 12 + 16; rtpPacketLength = encodedLength + 12 + 16;

View File

@@ -1,71 +0,0 @@
using System;
using System.Runtime.InteropServices;
namespace Discord.Audio
{
internal unsafe static class Opus
{
[DllImport("opus", EntryPoint = "opus_encoder_create", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr CreateEncoder(int Fs, int channels, int application, out Error error);
[DllImport("opus", EntryPoint = "opus_encoder_destroy", CallingConvention = CallingConvention.Cdecl)]
public static extern void DestroyEncoder(IntPtr encoder);
[DllImport("opus", EntryPoint = "opus_encode", CallingConvention = CallingConvention.Cdecl)]
public static extern int Encode(IntPtr st, byte* pcm, int frame_size, byte[] data, int max_data_bytes);
[DllImport("opus", EntryPoint = "opus_decoder_create", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr CreateDecoder(int Fs, int channels, out Error error);
[DllImport("opus", EntryPoint = "opus_decoder_destroy", CallingConvention = CallingConvention.Cdecl)]
public static extern void DestroyDecoder(IntPtr decoder);
[DllImport("opus", EntryPoint = "opus_decode", CallingConvention = CallingConvention.Cdecl)]
public static extern int Decode(IntPtr st, byte* data, int len, byte[] pcm, int frame_size, int decode_fec);
[DllImport("opus", EntryPoint = "opus_encoder_ctl", CallingConvention = CallingConvention.Cdecl)]
public static extern int EncoderCtl(IntPtr st, Ctl request, int value);
public enum Ctl : int
{
SetBitrateRequest = 4002,
GetBitrateRequest = 4003,
SetInbandFECRequest = 4012,
GetInbandFECRequest = 4013
}
/// <summary>Supported coding modes.</summary>
public enum Application : int
{
/// <summary>
/// Gives best quality at a given bitrate for voice signals. It enhances the input signal by high-pass filtering and emphasizing formants and harmonics.
/// Optionally it includes in-band forward error correction to protect against packet loss. Use this mode for typical VoIP applications.
/// Because of the enhancement, even at high bitrates the output may sound different from the input.
/// </summary>
Voip = 2048,
/// <summary>
/// Gives best quality at a given bitrate for most non-voice signals like music.
/// Use this mode for music and mixed (music/voice) content, broadcast, and applications requiring less than 15 ms of coding delay.
/// </summary>
Audio = 2049,
/// <summary> Low-delay mode that disables the speech-optimized mode in exchange for slightly reduced delay. </summary>
Restricted_LowLatency = 2051
}
public enum Error : int
{
/// <summary> No error. </summary>
OK = 0,
/// <summary> One or more invalid/out of range arguments. </summary>
BadArg = -1,
/// <summary> The mode struct passed is invalid. </summary>
BufferToSmall = -2,
/// <summary> An internal error was detected. </summary>
InternalError = -3,
/// <summary> The compressed data passed is corrupted. </summary>
InvalidPacket = -4,
/// <summary> Invalid/unsupported request number. </summary>
Unimplemented = -5,
/// <summary> An encoder or decoder structure is invalid or already freed. </summary>
InvalidState = -6,
/// <summary> Memory allocation has failed. </summary>
AllocFail = -7
}
}
}

View File

@@ -0,0 +1,53 @@
using System;
using System.Runtime.InteropServices;
using System.Security;
namespace Discord.Audio.Opus
{
internal enum OpusCtl : int
{
SetBitrateRequest = 4002,
GetBitrateRequest = 4003,
SetInbandFECRequest = 4012,
GetInbandFECRequest = 4013
}
/// <summary>Supported coding modes.</summary>
internal enum OpusApplication : int
{
/// <summary>
/// Gives best quality at a given bitrate for voice signals. It enhances the input signal by high-pass filtering and emphasizing formants and harmonics.
/// Optionally it includes in-band forward error correction to protect against packet loss. Use this mode for typical VoIP applications.
/// Because of the enhancement, even at high bitrates the output may sound different from the input.
/// </summary>
Voip = 2048,
/// <summary>
/// Gives best quality at a given bitrate for most non-voice signals like music.
/// Use this mode for music and mixed (music/voice) content, broadcast, and applications requiring less than 15 ms of coding delay.
/// </summary>
Audio = 2049,
/// <summary> Low-delay mode that disables the speech-optimized mode in exchange for slightly reduced delay. </summary>
Restricted_LowLatency = 2051
}
internal enum OpusError : int
{
/// <summary> No error. </summary>
OK = 0,
/// <summary> One or more invalid/out of range arguments. </summary>
BadArg = -1,
/// <summary> The mode struct passed is invalid. </summary>
BufferToSmall = -2,
/// <summary> An internal error was detected. </summary>
InternalError = -3,
/// <summary> The compressed data passed is corrupted. </summary>
InvalidPacket = -4,
/// <summary> Invalid/unsupported request number. </summary>
Unimplemented = -5,
/// <summary> An encoder or decoder structure is invalid or already freed. </summary>
InvalidState = -6,
/// <summary> Memory allocation has failed. </summary>
AllocFail = -7
}
}

View File

@@ -1,11 +1,26 @@
using System; using System;
using System.Runtime.InteropServices;
using System.Security;
namespace Discord.Audio namespace Discord.Audio.Opus
{ {
/// <summary> Opus codec wrapper. </summary> /// <summary> Opus codec wrapper. </summary>
internal class OpusDecoder : IDisposable internal class OpusDecoder : IDisposable
{ {
private readonly IntPtr _ptr; #if NET45
[SuppressUnmanagedCodeSecurity]
#endif
private unsafe static class UnsafeNativeMethods
{
[DllImport("opus", EntryPoint = "opus_decoder_create", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr CreateDecoder(int Fs, int channels, out OpusError error);
[DllImport("opus", EntryPoint = "opus_decoder_destroy", CallingConvention = CallingConvention.Cdecl)]
public static extern void DestroyDecoder(IntPtr decoder);
[DllImport("opus", EntryPoint = "opus_decode", CallingConvention = CallingConvention.Cdecl)]
public static extern int Decode(IntPtr st, byte* data, int len, byte[] pcm, int frame_size, int decode_fec);
}
private readonly IntPtr _ptr;
/// <summary> Gets the bit rate of the encoder. </summary> /// <summary> Gets the bit rate of the encoder. </summary>
public const int BitRate = 16; public const int BitRate = 16;
@@ -22,7 +37,7 @@ namespace Discord.Audio
/// <summary> Gets the bytes per frame. </summary> /// <summary> Gets the bytes per frame. </summary>
public int FrameSize { get; private set; } public int FrameSize { get; private set; }
/// <summary> Creates a new Opus encoder. </summary> /// <summary> Creates a new Opus decoder. </summary>
/// <param name="samplingRate">Sampling rate of the input signal (Hz). Supported Values: 8000, 12000, 16000, 24000, or 48000.</param> /// <param name="samplingRate">Sampling rate of the input signal (Hz). Supported Values: 8000, 12000, 16000, 24000, or 48000.</param>
/// <param name="channels">Number of channels (1 or 2) in input signal.</param> /// <param name="channels">Number of channels (1 or 2) in input signal.</param>
/// <param name="frameLength">Length, in milliseconds, that each frame takes. Supported Values: 2.5, 5, 10, 20, 40, 60</param> /// <param name="frameLength">Length, in milliseconds, that each frame takes. Supported Values: 2.5, 5, 10, 20, 40, 60</param>
@@ -44,45 +59,32 @@ namespace Discord.Audio
SamplesPerFrame = samplingRate / 1000 * FrameLength; SamplesPerFrame = samplingRate / 1000 * FrameLength;
FrameSize = SamplesPerFrame * SampleSize; FrameSize = SamplesPerFrame * SampleSize;
Opus.Error error; OpusError error;
_ptr = Opus.CreateDecoder(samplingRate, channels, out error); _ptr = UnsafeNativeMethods.CreateDecoder(samplingRate, channels, out error);
if (error != Opus.Error.OK) if (error != OpusError.OK)
throw new InvalidOperationException($"Error occured while creating decoder: {error}"); throw new InvalidOperationException($"Error occured while creating decoder: {error}");
SetForwardErrorCorrection(true);
} }
/// <summary> Produces Opus encoded audio from PCM samples. </summary> /// <summary> Produces PCM samples from Opus-encoded audio. </summary>
/// <param name="input">PCM samples to encode.</param> /// <param name="input">PCM samples to decode.</param>
/// <param name="inputOffset">Offset of the frame in pcmSamples.</param> /// <param name="inputOffset">Offset of the frame in input.</param>
/// <param name="output">Buffer to store the encoded frame.</param> /// <param name="output">Buffer to store the decoded frame.</param>
/// <returns>Length of the frame contained in outputBuffer.</returns> /// <returns>Length of the frame contained in output.</returns>
public unsafe int DecodeFrame(byte[] input, int inputOffset, byte[] output) public unsafe int DecodeFrame(byte[] input, int inputOffset, byte[] output)
{ {
if (disposed) if (disposed)
throw new ObjectDisposedException(nameof(OpusDecoder)); throw new ObjectDisposedException(nameof(OpusDecoder));
int result = 0; int result = 0;
fixed (byte* inPtr = input) fixed (byte* inPtr = input)
result = Opus.Encode(_ptr, inPtr + inputOffset, SamplesPerFrame, output, output.Length); result = UnsafeNativeMethods.Decode(_ptr, inPtr + inputOffset, SamplesPerFrame, output, output.Length, 0);
if (result < 0) if (result < 0)
throw new Exception("Decoding failed: " + ((Opus.Error)result).ToString()); throw new Exception("Decoding failed: " + ((OpusError)result).ToString());
return result; return result;
} }
/// <summary> Gets or sets whether Forward Error Correction is enabled. </summary> #region IDisposable
public void SetForwardErrorCorrection(bool value)
{
if (disposed)
throw new ObjectDisposedException(nameof(OpusDecoder));
var result = Opus.EncoderCtl(_ptr, Opus.Ctl.SetInbandFECRequest, value ? 1 : 0);
if (result < 0)
throw new Exception("Decoder error: " + ((Opus.Error)result).ToString());
}
#region IDisposable
private bool disposed; private bool disposed;
public void Dispose() public void Dispose()
{ {
@@ -92,7 +94,7 @@ namespace Discord.Audio
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
if (_ptr != IntPtr.Zero) if (_ptr != IntPtr.Zero)
Opus.DestroyEncoder(_ptr); UnsafeNativeMethods.DestroyDecoder(_ptr);
disposed = true; disposed = true;
} }
@@ -100,6 +102,6 @@ namespace Discord.Audio
{ {
Dispose(); Dispose();
} }
#endregion #endregion
} }
} }

View File

@@ -1,11 +1,28 @@
using System; using System;
using System.Runtime.InteropServices;
using System.Security;
namespace Discord.Audio namespace Discord.Audio.Opus
{ {
/// <summary> Opus codec wrapper. </summary> /// <summary> Opus codec wrapper. </summary>
internal class OpusEncoder : IDisposable internal class OpusEncoder : IDisposable
{ {
private readonly IntPtr _ptr; #if NET45
[SuppressUnmanagedCodeSecurity]
#endif
private unsafe static class UnsafeNativeMethods
{
[DllImport("opus", EntryPoint = "opus_encoder_create", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr CreateEncoder(int Fs, int channels, int application, out OpusError error);
[DllImport("opus", EntryPoint = "opus_encoder_destroy", CallingConvention = CallingConvention.Cdecl)]
public static extern void DestroyEncoder(IntPtr encoder);
[DllImport("opus", EntryPoint = "opus_encode", CallingConvention = CallingConvention.Cdecl)]
public static extern int Encode(IntPtr st, byte* pcm, int frame_size, byte[] data, int max_data_bytes);
[DllImport("opus", EntryPoint = "opus_encoder_ctl", CallingConvention = CallingConvention.Cdecl)]
public static extern int EncoderCtl(IntPtr st, OpusCtl request, int value);
}
private readonly IntPtr _ptr;
/// <summary> Gets the bit rate of the encoder. </summary> /// <summary> Gets the bit rate of the encoder. </summary>
public const int BitsPerSample = 16; public const int BitsPerSample = 16;
@@ -24,7 +41,7 @@ namespace Discord.Audio
/// <summary> Gets the bit rate in kbit/s. </summary> /// <summary> Gets the bit rate in kbit/s. </summary>
public int? BitRate { get; private set; } public int? BitRate { get; private set; }
/// <summary> Gets the coding mode of the encoder. </summary> /// <summary> Gets the coding mode of the encoder. </summary>
public Opus.Application Application { get; private set; } public OpusApplication Application { get; private set; }
/// <summary> Creates a new Opus encoder. </summary> /// <summary> Creates a new Opus encoder. </summary>
/// <param name="samplingRate">Sampling rate of the input signal (Hz). Supported Values: 8000, 12000, 16000, 24000, or 48000.</param> /// <param name="samplingRate">Sampling rate of the input signal (Hz). Supported Values: 8000, 12000, 16000, 24000, or 48000.</param>
@@ -33,7 +50,7 @@ namespace Discord.Audio
/// <param name="bitrate">Bitrate (kbit/s) used for this encoder. Supported Values: 1-512. Null will use the recommended bitrate. </param> /// <param name="bitrate">Bitrate (kbit/s) used for this encoder. Supported Values: 1-512. Null will use the recommended bitrate. </param>
/// <param name="application">Coding mode.</param> /// <param name="application">Coding mode.</param>
/// <returns>A new <c>OpusEncoder</c></returns> /// <returns>A new <c>OpusEncoder</c></returns>
public OpusEncoder(int samplingRate, int channels, int frameLength, int? bitrate, Opus.Application application) public OpusEncoder(int samplingRate, int channels, int frameLength, int? bitrate, OpusApplication application)
{ {
if (samplingRate != 8000 && samplingRate != 12000 && if (samplingRate != 8000 && samplingRate != 12000 &&
samplingRate != 16000 && samplingRate != 24000 && samplingRate != 16000 && samplingRate != 24000 &&
@@ -53,9 +70,9 @@ namespace Discord.Audio
FrameSize = SamplesPerFrame * SampleSize; FrameSize = SamplesPerFrame * SampleSize;
BitRate = bitrate; BitRate = bitrate;
Opus.Error error; OpusError error;
_ptr = Opus.CreateEncoder(samplingRate, channels, (int)application, out error); _ptr = UnsafeNativeMethods.CreateEncoder(samplingRate, channels, (int)application, out error);
if (error != Opus.Error.OK) if (error != OpusError.OK)
throw new InvalidOperationException($"Error occured while creating encoder: {error}"); throw new InvalidOperationException($"Error occured while creating encoder: {error}");
SetForwardErrorCorrection(true); SetForwardErrorCorrection(true);
@@ -75,10 +92,10 @@ namespace Discord.Audio
int result = 0; int result = 0;
fixed (byte* inPtr = input) fixed (byte* inPtr = input)
result = Opus.Encode(_ptr, inPtr + inputOffset, SamplesPerFrame, output, output.Length); result = UnsafeNativeMethods.Encode(_ptr, inPtr + inputOffset, SamplesPerFrame, output, output.Length);
if (result < 0) if (result < 0)
throw new Exception("Encoding failed: " + ((Opus.Error)result).ToString()); throw new Exception("Encoding failed: " + ((OpusError)result).ToString());
return result; return result;
} }
@@ -88,9 +105,9 @@ namespace Discord.Audio
if (disposed) if (disposed)
throw new ObjectDisposedException(nameof(OpusEncoder)); throw new ObjectDisposedException(nameof(OpusEncoder));
var result = Opus.EncoderCtl(_ptr, Opus.Ctl.SetInbandFECRequest, value ? 1 : 0); var result = UnsafeNativeMethods.EncoderCtl(_ptr, OpusCtl.SetInbandFECRequest, value ? 1 : 0);
if (result < 0) if (result < 0)
throw new Exception("Encoder error: " + ((Opus.Error)result).ToString()); throw new Exception("Encoder error: " + ((OpusError)result).ToString());
} }
/// <summary> Gets or sets whether Forward Error Correction is enabled. </summary> /// <summary> Gets or sets whether Forward Error Correction is enabled. </summary>
@@ -99,9 +116,9 @@ namespace Discord.Audio
if (disposed) if (disposed)
throw new ObjectDisposedException(nameof(OpusEncoder)); throw new ObjectDisposedException(nameof(OpusEncoder));
var result = Opus.EncoderCtl(_ptr, Opus.Ctl.SetBitrateRequest, value * 1000); var result = UnsafeNativeMethods.EncoderCtl(_ptr, OpusCtl.SetBitrateRequest, value * 1000);
if (result < 0) if (result < 0)
throw new Exception("Encoder error: " + ((Opus.Error)result).ToString()); throw new Exception("Encoder error: " + ((OpusError)result).ToString());
} }
#region IDisposable #region IDisposable
@@ -114,7 +131,7 @@ namespace Discord.Audio
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
if (_ptr != IntPtr.Zero) if (_ptr != IntPtr.Zero)
Opus.DestroyEncoder(_ptr); UnsafeNativeMethods.DestroyEncoder(_ptr);
disposed = true; disposed = true;
} }

View File

@@ -2,25 +2,4 @@
namespace Discord.Audio namespace Discord.Audio
{ {
internal unsafe static class Sodium
{
[DllImport("libsodium", EntryPoint = "crypto_secretbox_easy", CallingConvention = CallingConvention.Cdecl)]
private static extern int SecretBoxEasy(byte* output, byte[] input, long inputLength, byte[] nonce, byte[] secret);
public static int Encrypt(byte[] input, long inputLength, byte[] output, int outputOffset, byte[] nonce, byte[] secret)
{
fixed (byte* outPtr = output)
return SecretBoxEasy(outPtr + outputOffset, input, inputLength, nonce, secret);
}
[DllImport("libsodium", EntryPoint = "crypto_secretbox_open_easy", CallingConvention = CallingConvention.Cdecl)]
private static extern int SecretBoxOpenEasy(byte[] output, byte* input, long inputLength, byte[] nonce, byte[] secret);
public static int Decrypt(byte[] input, int inputOffset, long inputLength, byte[] output, byte[] nonce, byte[] secret)
{
fixed (byte* inPtr = input)
return SecretBoxOpenEasy(output, inPtr + inputLength, inputLength, nonce, secret);
}
}
} }

View File

@@ -0,0 +1,30 @@
using System.Runtime.InteropServices;
using System.Security;
namespace Discord.Audio.Sodium
{
public unsafe static class SecretBox
{
#if NET45
[SuppressUnmanagedCodeSecurity]
#endif
private static class SafeNativeMethods
{
[DllImport("libsodium", EntryPoint = "crypto_secretbox_easy", CallingConvention = CallingConvention.Cdecl)]
public static extern int SecretBoxEasy(byte* output, byte[] input, long inputLength, byte[] nonce, byte[] secret);
[DllImport("libsodium", EntryPoint = "crypto_secretbox_open_easy", CallingConvention = CallingConvention.Cdecl)]
public static extern int SecretBoxOpenEasy(byte[] output, byte* input, long inputLength, byte[] nonce, byte[] secret);
}
public static int Encrypt(byte[] input, long inputLength, byte[] output, int outputOffset, byte[] nonce, byte[] secret)
{
fixed (byte* outPtr = output)
return SafeNativeMethods.SecretBoxEasy(outPtr + outputOffset, input, inputLength, nonce, secret);
}
public static int Decrypt(byte[] input, int inputOffset, long inputLength, byte[] output, byte[] nonce, byte[] secret)
{
fixed (byte* inPtr = input)
return SafeNativeMethods.SecretBoxOpenEasy(output, inPtr + inputLength, inputLength, nonce, secret);
}
}
}

View File

@@ -2,7 +2,7 @@
namespace Discord.Commands namespace Discord.Commands
{ {
public class CommandEventArgs public class CommandEventArgs : EventArgs
{ {
private readonly string[] _args; private readonly string[] _args;

View File

@@ -7,7 +7,7 @@ using System.Threading.Tasks;
namespace Discord.Commands namespace Discord.Commands
{ {
/// <summary> A Discord.Net client with extensions for handling common bot operations like text commands. </summary> /// <summary> A Discord.Net client with extensions for handling common bot operations like text commands. </summary>
public partial class CommandService : IService public sealed partial class CommandService : IService
{ {
private const string DefaultPermissionError = "You do not have permission to access this command."; private const string DefaultPermissionError = "You do not have permission to access this command.";

View File

@@ -224,11 +224,11 @@
<Compile Include="..\Discord.Net\Net\TimeoutException.cs"> <Compile Include="..\Discord.Net\Net\TimeoutException.cs">
<Link>Net\TimeoutException.cs</Link> <Link>Net\TimeoutException.cs</Link>
</Compile> </Compile>
<Compile Include="..\Discord.Net\Net\WebSockets\GatewayWebSocket.cs"> <Compile Include="..\Discord.Net\Net\WebSockets\GatewaySocket.cs">
<Link>Net\WebSockets\GatewayWebSocket.cs</Link> <Link>Net\WebSockets\GatewaySocket.cs</Link>
</Compile> </Compile>
<Compile Include="..\Discord.Net\Net\WebSockets\GatewayWebSockets.Events.cs"> <Compile Include="..\Discord.Net\Net\WebSockets\GatewaySocket.Events.cs">
<Link>Net\WebSockets\GatewayWebSockets.Events.cs</Link> <Link>Net\WebSockets\GatewaySocket.Events.cs</Link>
</Compile> </Compile>
<Compile Include="..\Discord.Net\Net\WebSockets\IWebSocketEngine.cs"> <Compile Include="..\Discord.Net\Net\WebSockets\IWebSocketEngine.cs">
<Link>Net\WebSockets\IWebSocketEngine.cs</Link> <Link>Net\WebSockets\IWebSocketEngine.cs</Link>

View File

@@ -9,165 +9,165 @@ using System.Collections.Generic;
namespace Discord.API namespace Discord.API
{ {
public enum GatewayOpCodes : byte public enum GatewayOpCodes : byte
{ {
/// <summary> Client <-- Server - Used to send most events. </summary> /// <summary> Client <-- Server - Used to send most events. </summary>
Dispatch = 0, Dispatch = 0,
/// <summary> Client <-> Server - Used to keep the connection alive and measure latency. </summary> /// <summary> Client <-> Server - Used to keep the connection alive and measure latency. </summary>
Heartbeat = 1, Heartbeat = 1,
/// <summary> Client --> Server - Used to associate a connection with a token and specify configuration. </summary> /// <summary> Client --> Server - Used to associate a connection with a token and specify configuration. </summary>
Identify = 2, Identify = 2,
/// <summary> Client --> Server - Used to update client's status and current game id. </summary> /// <summary> Client --> Server - Used to update client's status and current game id. </summary>
StatusUpdate = 3, StatusUpdate = 3,
/// <summary> Client --> Server - Used to join a particular voice channel. </summary> /// <summary> Client --> Server - Used to join a particular voice channel. </summary>
VoiceStateUpdate = 4, VoiceStateUpdate = 4,
/// <summary> Client --> Server - Used to ensure the server's voice server is alive. Only send this if voice connection fails or suddenly drops. </summary> /// <summary> Client --> Server - Used to ensure the server's voice server is alive. Only send this if voice connection fails or suddenly drops. </summary>
VoiceServerPing = 5, VoiceServerPing = 5,
/// <summary> Client --> Server - Used to resume a connection after a redirect occurs. </summary> /// <summary> Client --> Server - Used to resume a connection after a redirect occurs. </summary>
Resume = 6, Resume = 6,
/// <summary> Client <-- Server - Used to notify a client that they must reconnect to another gateway. </summary> /// <summary> Client <-- Server - Used to notify a client that they must reconnect to another gateway. </summary>
Redirect = 7, Redirect = 7,
/// <summary> Client --> Server - Used to request all members that were withheld by large_threshold </summary> /// <summary> Client --> Server - Used to request all members that were withheld by large_threshold </summary>
RequestGuildMembers = 8 RequestGuildMembers = 8
} }
//Common //Common
public class WebSocketMessage public class WebSocketMessage
{ {
public WebSocketMessage() { } public WebSocketMessage() { }
public WebSocketMessage(int op) { Operation = op; } public WebSocketMessage(int op) { Operation = op; }
[JsonProperty("op")] [JsonProperty("op")]
public int Operation; public int Operation;
[JsonProperty("d")] [JsonProperty("d")]
public object Payload; public object Payload;
[JsonProperty("t", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("t", NullValueHandling = NullValueHandling.Ignore)]
public string Type; public string Type;
[JsonProperty("s", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("s", NullValueHandling = NullValueHandling.Ignore)]
public int? Sequence; public int? Sequence;
} }
public abstract class WebSocketMessage<T> : WebSocketMessage public abstract class WebSocketMessage<T> : WebSocketMessage
where T : new() where T : new()
{ {
public WebSocketMessage() { Payload = new T(); } public WebSocketMessage() { Payload = new T(); }
public WebSocketMessage(int op) : base(op) { Payload = new T(); } public WebSocketMessage(int op) : base(op) { Payload = new T(); }
public WebSocketMessage(int op, T payload) : base(op) { Payload = payload; } public WebSocketMessage(int op, T payload) : base(op) { Payload = payload; }
[JsonIgnore] [JsonIgnore]
public new T Payload public new T Payload
{ {
get get
{ {
if (base.Payload is JToken) if (base.Payload is JToken)
base.Payload = (base.Payload as JToken).ToObject<T>(); base.Payload = (base.Payload as JToken).ToObject<T>();
return (T)base.Payload; return (T)base.Payload;
} }
set { base.Payload = value; } set { base.Payload = value; }
}
}
//Commands
internal sealed class HeartbeatCommand : WebSocketMessage<long>
{
public HeartbeatCommand() : base((int)GatewayOpCodes.Heartbeat, EpochTime.GetMilliseconds()) { }
}
internal sealed class IdentifyCommand : WebSocketMessage<IdentifyCommand.Data>
{
public IdentifyCommand() : base((int)GatewayOpCodes.Identify) { }
public class Data
{
[JsonProperty("token")]
public string Token;
[JsonProperty("v")]
public int Version = 3;
[JsonProperty("properties")]
public Dictionary<string, string> Properties = new Dictionary<string, string>();
[JsonProperty("large_threshold", NullValueHandling = NullValueHandling.Ignore)]
public int? LargeThreshold;
[JsonProperty("compress", NullValueHandling = NullValueHandling.Ignore)]
public bool? Compress;
} }
} }
internal sealed class StatusUpdateCommand : WebSocketMessage<StatusUpdateCommand.Data> //Commands
{ internal sealed class HeartbeatCommand : WebSocketMessage<long>
public StatusUpdateCommand() : base((int)GatewayOpCodes.StatusUpdate) { } {
public class Data public HeartbeatCommand() : base((int)GatewayOpCodes.Heartbeat, EpochTime.GetMilliseconds()) { }
{ }
[JsonProperty("idle_since")] internal sealed class IdentifyCommand : WebSocketMessage<IdentifyCommand.Data>
public long? IdleSince; {
[JsonProperty("game_id")] public IdentifyCommand() : base((int)GatewayOpCodes.Identify) { }
public int? GameId; public class Data
} {
} [JsonProperty("token")]
public string Token;
[JsonProperty("v")]
public int Version = 3;
[JsonProperty("properties")]
public Dictionary<string, string> Properties = new Dictionary<string, string>();
[JsonProperty("large_threshold", NullValueHandling = NullValueHandling.Ignore)]
public int? LargeThreshold;
[JsonProperty("compress", NullValueHandling = NullValueHandling.Ignore)]
public bool? Compress;
}
}
internal sealed class JoinVoiceCommand : WebSocketMessage<JoinVoiceCommand.Data> internal sealed class StatusUpdateCommand : WebSocketMessage<StatusUpdateCommand.Data>
{ {
public JoinVoiceCommand() : base((int)GatewayOpCodes.VoiceStateUpdate) { } public StatusUpdateCommand() : base((int)GatewayOpCodes.StatusUpdate) { }
public class Data public class Data
{ {
[JsonProperty("guild_id")] [JsonProperty("idle_since")]
[JsonConverter(typeof(LongStringConverter))] public long? IdleSince;
public long ServerId; [JsonProperty("game_id")]
[JsonProperty("channel_id")] public int? GameId;
[JsonConverter(typeof(LongStringConverter))] }
public long ChannelId; }
[JsonProperty("self_mute")]
public string SelfMute;
[JsonProperty("self_deaf")]
public string SelfDeaf;
}
}
internal sealed class ResumeCommand : WebSocketMessage<ResumeCommand.Data> internal sealed class JoinVoiceCommand : WebSocketMessage<JoinVoiceCommand.Data>
{ {
public ResumeCommand() : base((int)GatewayOpCodes.Resume) { } public JoinVoiceCommand() : base((int)GatewayOpCodes.VoiceStateUpdate) { }
public class Data public class Data
{ {
[JsonProperty("session_id")] [JsonProperty("guild_id")]
public string SessionId; [JsonConverter(typeof(LongStringConverter))]
[JsonProperty("seq")] public long ServerId;
public int Sequence; [JsonProperty("channel_id")]
} [JsonConverter(typeof(LongStringConverter))]
} public long ChannelId;
[JsonProperty("self_mute")]
public string SelfMute;
[JsonProperty("self_deaf")]
public string SelfDeaf;
}
}
//Events internal sealed class ResumeCommand : WebSocketMessage<ResumeCommand.Data>
internal sealed class ReadyEvent {
{ public ResumeCommand() : base((int)GatewayOpCodes.Resume) { }
public sealed class ReadStateInfo public class Data
{ {
[JsonProperty("id")] [JsonProperty("session_id")]
public string ChannelId; public string SessionId;
[JsonProperty("mention_count")] [JsonProperty("seq")]
public int MentionCount; public int Sequence;
[JsonProperty("last_message_id")] }
public string LastMessageId; }
}
[JsonProperty("v")] //Events
public int Version; internal sealed class ReadyEvent
[JsonProperty("user")] {
public UserInfo User; public sealed class ReadStateInfo
[JsonProperty("session_id")] {
public string SessionId; [JsonProperty("id")]
[JsonProperty("read_state")] public string ChannelId;
public ReadStateInfo[] ReadState; [JsonProperty("mention_count")]
[JsonProperty("guilds")] public int MentionCount;
public ExtendedGuildInfo[] Guilds; [JsonProperty("last_message_id")]
[JsonProperty("private_channels")] public string LastMessageId;
public ChannelInfo[] PrivateChannels; }
[JsonProperty("heartbeat_interval")]
public int HeartbeatInterval;
}
internal sealed class RedirectEvent [JsonProperty("v")]
{ public int Version;
[JsonProperty("url")] [JsonProperty("user")]
public string Url; public UserInfo User;
} [JsonProperty("session_id")]
internal sealed class ResumeEvent public string SessionId;
{ [JsonProperty("read_state")]
[JsonProperty("heartbeat_interval")] public ReadStateInfo[] ReadState;
public int HeartbeatInterval; [JsonProperty("guilds")]
} public ExtendedGuildInfo[] Guilds;
[JsonProperty("private_channels")]
public ChannelInfo[] PrivateChannels;
[JsonProperty("heartbeat_interval")]
public int HeartbeatInterval;
}
internal sealed class RedirectEvent
{
[JsonProperty("url")]
public string Url;
}
internal sealed class ResumeEvent
{
[JsonProperty("heartbeat_interval")]
public int HeartbeatInterval;
}
} }

View File

@@ -58,7 +58,7 @@ namespace Discord
} }
} }
public partial class DiscordClient public partial class DiscordClient : IDisposable
{ {
public event EventHandler<UserEventArgs> UserJoined; public event EventHandler<UserEventArgs> UserJoined;
private void RaiseUserJoined(User user) private void RaiseUserJoined(User user)
@@ -305,5 +305,5 @@ namespace Discord
_webSocket.SendStatusUpdate(_status == UserStatus.Idle ? EpochTime.GetMilliseconds() - (10 * 60 * 1000) : (long?)null, _gameId); _webSocket.SendStatusUpdate(_status == UserStatus.Idle ? EpochTime.GetMilliseconds() - (10 * 60 * 1000) : (long?)null, _gameId);
return TaskHelper.CompletedTask; return TaskHelper.CompletedTask;
} }
} }
} }

View File

@@ -4,7 +4,6 @@ using Newtonsoft.Json;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Reflection; using System.Reflection;
using System.Runtime.ExceptionServices; using System.Runtime.ExceptionServices;
using System.Threading; using System.Threading;
@@ -82,8 +81,8 @@ namespace Discord
private readonly DiscordAPIClient _api; private readonly DiscordAPIClient _api;
/// <summary> Returns the internal websocket object. </summary> /// <summary> Returns the internal websocket object. </summary>
public GatewayWebSocket WebSocket => _webSocket; public GatewaySocket WebSocket => _webSocket;
private readonly GatewayWebSocket _webSocket; private readonly GatewaySocket _webSocket;
public string GatewayUrl => _gateway; public string GatewayUrl => _gateway;
private string _gateway; private string _gateway;
@@ -140,11 +139,24 @@ namespace Discord
CreateCacheLogger(); CreateCacheLogger();
//Networking //Networking
_webSocket = CreateWebSocket(); _webSocket = new GatewaySocket(_config, _log.CreateLogger("WebSocket"));
_api = new DiscordAPIClient(_config); var settings = new JsonSerializerSettings();
_webSocket.Connected += (s, e) =>
{
if (_state == (int)DiscordClientState.Connecting)
EndConnect();
};
_webSocket.Disconnected += (s, e) =>
{
RaiseDisconnected(e);
};
_webSocket.ReceivedDispatch += async (s, e) => await OnReceivedEvent(e).ConfigureAwait(false);
_api = new DiscordAPIClient(_config);
if (Config.UseMessageQueue) if (Config.UseMessageQueue)
_pendingMessages = new ConcurrentQueue<Message>(); _pendingMessages = new ConcurrentQueue<Message>();
this.Connected += async (s, e) => Connected += async (s, e) =>
{ {
_api.CancelToken = _cancelToken; _api.CancelToken = _cancelToken;
await SendStatus().ConfigureAwait(false); await SendStatus().ConfigureAwait(false);
@@ -257,24 +269,6 @@ namespace Discord
} }
} }
private GatewayWebSocket CreateWebSocket()
{
var socket = new GatewayWebSocket(_config, _log.CreateLogger("WebSocket"));
var settings = new JsonSerializerSettings();
socket.Connected += (s, e) =>
{
if (_state == (int)DiscordClientState.Connecting)
CompleteConnect();
};
socket.Disconnected += (s, e) =>
{
RaiseDisconnected(e);
};
socket.ReceivedDispatch += async (s, e) => await OnReceivedEvent(e).ConfigureAwait(false);
return socket;
}
/// <summary> Connects to the Discord server with the provided email and password. </summary> /// <summary> Connects to the Discord server with the provided email and password. </summary>
/// <returns> Returns a token for future connections. </returns> /// <returns> Returns a token for future connections. </returns>
public async Task<string> Connect(string email, string password) public async Task<string> Connect(string email, string password)
@@ -285,73 +279,73 @@ namespace Discord
if (State != DiscordClientState.Disconnected) if (State != DiscordClientState.Disconnected)
await Disconnect().ConfigureAwait(false); await Disconnect().ConfigureAwait(false);
string token; var response = await _api.Login(email, password)
try .ConfigureAwait(false);
{ _token = response.Token;
var response = await _api.Login(email, password) _api.Token = response.Token;
.ConfigureAwait(false); if (_config.LogLevel >= LogSeverity.Verbose)
token = response.Token; _logger.Log(LogSeverity.Verbose, "Login successful, got token.");
if (_config.LogLevel >= LogSeverity.Verbose)
_logger.Log(LogSeverity.Verbose, "Login successful, got token.");
await Connect(token); await BeginConnect();
return token; return response.Token;
} }
catch (TaskCanceledException) { throw new TimeoutException(); }
}
/// <summary> Connects to the Discord server with the provided token. </summary> /// <summary> Connects to the Discord server with the provided token. </summary>
public async Task Connect(string token) public async Task Connect(string token)
{ {
if (!_sentInitialLog) if (!_sentInitialLog)
SendInitialLog(); SendInitialLog();
if (State != (int)DiscordClientState.Disconnected) if (State != (int)DiscordClientState.Disconnected)
await Disconnect().ConfigureAwait(false); await Disconnect().ConfigureAwait(false);
_api.Token = token; _token = token;
var gatewayResponse = await _api.Gateway().ConfigureAwait(false); _api.Token = token;
string gateway = gatewayResponse.Url; await BeginConnect();
if (_config.LogLevel >= LogSeverity.Verbose) }
_logger.Log(LogSeverity.Verbose, $"Websocket endpoint: {gateway}");
try private async Task BeginConnect()
{ {
_state = (int)DiscordClientState.Connecting; try
_disconnectedEvent.Reset(); {
_state = (int)DiscordClientState.Connecting;
_gateway = gateway; var gatewayResponse = await _api.Gateway().ConfigureAwait(false);
_token = token; string gateway = gatewayResponse.Url;
if (_config.LogLevel >= LogSeverity.Verbose)
_logger.Log(LogSeverity.Verbose, $"Websocket endpoint: {gateway}");
_cancelTokenSource = new CancellationTokenSource(); _disconnectedEvent.Reset();
_cancelToken = _cancelTokenSource.Token;
_webSocket.Host = gateway; _gateway = gateway;
_webSocket.ParentCancelToken = _cancelToken;
await _webSocket.Connect(token).ConfigureAwait(false);
_runTask = RunTasks(); _cancelTokenSource = new CancellationTokenSource();
_cancelToken = _cancelTokenSource.Token;
try _webSocket.Host = gateway;
{ _webSocket.ParentCancelToken = _cancelToken;
//Cancel if either Disconnect is called, data socket errors or timeout is reached await _webSocket.Connect(_token).ConfigureAwait(false);
var cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken, _webSocket.CancelToken).Token;
_connectedEvent.Wait(cancelToken);
}
catch (OperationCanceledException)
{
_webSocket.ThrowError(); //Throws data socket's internal error if any occured
throw;
}
//_state = (int)DiscordClientState.Connected; _runTask = RunTasks();
}
catch try
{ {
await Disconnect().ConfigureAwait(false); //Cancel if either Disconnect is called, data socket errors or timeout is reached
throw; var cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken, _webSocket.CancelToken).Token;
} _connectedEvent.Wait(cancelToken);
} }
private void CompleteConnect() catch (OperationCanceledException)
{
_webSocket.ThrowError(); //Throws data socket's internal error if any occured
throw;
}
}
catch
{
await Disconnect().ConfigureAwait(false);
throw;
}
}
private void EndConnect()
{ {
_state = (int)DiscordClientState.Connected; _state = (int)DiscordClientState.Connected;
_connectedEvent.Set(); _connectedEvent.Set();
@@ -359,8 +353,8 @@ namespace Discord
} }
/// <summary> Disconnects from the Discord server, canceling any pending requests. </summary> /// <summary> Disconnects from the Discord server, canceling any pending requests. </summary>
public Task Disconnect() => DisconnectInternal(new Exception("Disconnect was requested by user."), isUnexpected: false); public Task Disconnect() => SignalDisconnect(new Exception("Disconnect was requested by user."), isUnexpected: false);
private async Task DisconnectInternal(Exception ex = null, bool isUnexpected = true, bool skipAwait = false) private async Task SignalDisconnect(Exception ex = null, bool isUnexpected = true, bool wait = false)
{ {
int oldState; int oldState;
bool hasWriterLock; bool hasWriterLock;
@@ -386,7 +380,7 @@ namespace Discord
await Cleanup().ConfigureAwait(false);*/ await Cleanup().ConfigureAwait(false);*/
} }
if (!skipAwait) if (wait)
{ {
Task task = _runTask; Task task = _runTask;
if (_runTask != null) if (_runTask != null)
@@ -407,10 +401,10 @@ namespace Discord
//Wait until the first task ends/errors and capture the error //Wait until the first task ends/errors and capture the error
try { await firstTask.ConfigureAwait(false); } try { await firstTask.ConfigureAwait(false); }
catch (Exception ex) { await DisconnectInternal(ex: ex, skipAwait: true).ConfigureAwait(false); } catch (Exception ex) { await SignalDisconnect(ex: ex, wait: true).ConfigureAwait(false); }
//Ensure all other tasks are signaled to end. //Ensure all other tasks are signaled to end.
await DisconnectInternal(skipAwait: true).ConfigureAwait(false); await SignalDisconnect(wait: true).ConfigureAwait(false);
//Wait for the remaining tasks to complete //Wait for the remaining tasks to complete
try { await allTasks.ConfigureAwait(false); } try { await allTasks.ConfigureAwait(false); }
@@ -453,35 +447,6 @@ namespace Discord
_privateUser = null; _privateUser = null;
} }
public T AddSingleton<T>(T obj)
where T : class
{
_singletons.Add(typeof(T), obj);
return obj;
}
public T GetSingleton<T>(bool required = true)
where T : class
{
object singleton;
T singletonT = null;
if (_singletons.TryGetValue(typeof(T), out singleton))
singletonT = singleton as T;
if (singletonT == null && required)
throw new InvalidOperationException($"This operation requires {typeof(T).Name} to be added to {nameof(DiscordClient)}.");
return singletonT;
}
public T AddService<T>(T obj)
where T : class, IService
{
AddSingleton(obj);
obj.Install(this);
return obj;
}
public T GetService<T>(bool required = true)
where T : class, IService
=> GetSingleton<T>(required);
private async Task OnReceivedEvent(WebSocketEventEventArgs e) private async Task OnReceivedEvent(WebSocketEventEventArgs e)
{ {
@@ -851,45 +816,99 @@ namespace Discord
_sentInitialLog = true; _sentInitialLog = true;
} }
#region Async Wrapper
/// <summary> Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications. </summary>
public void Run(Func<Task> asyncAction)
{
try
{
asyncAction().GetAwaiter().GetResult(); //Avoids creating AggregateExceptions
}
catch (TaskCanceledException) { }
_disconnectedEvent.WaitOne();
}
/// <summary> Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications. </summary>
public void Run()
{
_disconnectedEvent.WaitOne();
}
#endregion
//Helpers #region Services
/// <summary> Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications. </summary> public T AddSingleton<T>(T obj)
public void Run(Func<Task> asyncAction) where T : class
{ {
try _singletons.Add(typeof(T), obj);
{ return obj;
asyncAction().GetAwaiter().GetResult(); //Avoids creating AggregateExceptions }
} public T GetSingleton<T>(bool required = true)
catch (TaskCanceledException) { } where T : class
_disconnectedEvent.WaitOne(); {
} object singleton;
/// <summary> Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications. </summary> T singletonT = null;
public void Run() if (_singletons.TryGetValue(typeof(T), out singleton))
{ singletonT = singleton as T;
_disconnectedEvent.WaitOne();
}
private void CheckReady() if (singletonT == null && required)
{ throw new InvalidOperationException($"This operation requires {typeof(T).Name} to be added to {nameof(DiscordClient)}.");
switch (_state) return singletonT;
{ }
case (int)DiscordClientState.Disconnecting: public T AddService<T>(T obj)
throw new InvalidOperationException("The client is disconnecting."); where T : class, IService
case (int)DiscordClientState.Disconnected: {
throw new InvalidOperationException("The client is not connected to Discord"); AddSingleton(obj);
case (int)DiscordClientState.Connecting: obj.Install(this);
throw new InvalidOperationException("The client is connecting."); return obj;
} }
} public T GetService<T>(bool required = true)
where T : class, IService
public void GetCacheStats(out int serverCount, out int channelCount, out int userCount, out int uniqueUserCount, out int messageCount, out int roleCount) => GetSingleton<T>(required);
{ #endregion
serverCount = _servers.Count;
channelCount = _channels.Count; #region IDisposable
userCount = _users.Count; private bool _isDisposed = false;
uniqueUserCount = _globalUsers.Count;
messageCount = _messages.Count; protected virtual void Dispose(bool isDisposing)
roleCount = _roles.Count; {
} if (!_isDisposed)
} {
if (isDisposing)
{
_disconnectedEvent.Dispose();
_connectedEvent.Dispose();
}
_isDisposed = true;
}
}
public void Dispose()
{
Dispose(true);
}
#endregion
//Helpers
private void CheckReady()
{
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.");
}
}
public void GetCacheStats(out int serverCount, out int channelCount, out int userCount, out int uniqueUserCount, out int messageCount, out int roleCount)
{
serverCount = _servers.Count;
channelCount = _channels.Count;
userCount = _users.Count;
uniqueUserCount = _globalUsers.Count;
messageCount = _messages.Count;
roleCount = _roles.Count;
}
}
} }

View File

@@ -42,6 +42,7 @@ namespace Discord
public bool IsServerDeafened { get; private set; } public bool IsServerDeafened { get; private set; }
public bool IsServerSuppressed { get; private set; } public bool IsServerSuppressed { get; private set; }
public bool IsPrivate => _server.Id == null; public bool IsPrivate => _server.Id == null;
public bool IsOwner => _server.Value.OwnerId == Id;
public string SessionId { get; private set; } public string SessionId { get; private set; }
public string Token { get; private set; } public string Token { get; private set; }
@@ -101,9 +102,23 @@ namespace Discord
{ {
if (_server.Id != null) if (_server.Id != null)
{ {
return Server.Channels if (_client.Config.UsePermissionsCache)
.Where(x => (x.Type == ChannelType.Text && x.GetPermissions(this).ReadMessages) || {
(x.Type == ChannelType.Voice && x.GetPermissions(this).Connect)); return Server.Channels
.Where(x => (x.Type == ChannelType.Text && x.GetPermissions(this).ReadMessages) ||
(x.Type == ChannelType.Voice && x.GetPermissions(this).Connect));
}
else
{
ChannelPermissions perms = new ChannelPermissions();
return Server.Channels
.Where(x =>
{
x.UpdatePermissions(this, perms);
return (x.Type == ChannelType.Text && perms.ReadMessages) ||
(x.Type == ChannelType.Voice && perms.Connect);
});
}
} }
else else
{ {

View File

@@ -1,8 +1,12 @@
using System; using System;
using System.Net; using System.Net;
using System.Runtime.Serialization;
namespace Discord.Net namespace Discord.Net
{ {
#if NET45
[Serializable]
#endif
public class HttpException : Exception public class HttpException : Exception
{ {
public HttpStatusCode StatusCode { get; } public HttpStatusCode StatusCode { get; }
@@ -11,6 +15,10 @@ namespace Discord.Net
: base($"The server responded with error {(int)statusCode} ({statusCode})") : base($"The server responded with error {(int)statusCode} ({statusCode})")
{ {
StatusCode = statusCode; StatusCode = statusCode;
} }
} #if NET45
public override void GetObjectData(SerializationInfo info, StreamingContext context)
=> base.GetObjectData(info, context);
#endif
}
} }

View File

@@ -2,7 +2,10 @@
namespace Discord.Net namespace Discord.Net
{ {
public sealed class TimeoutException : OperationCanceledException #if NET45
[Serializable]
#endif
public sealed class TimeoutException : OperationCanceledException
{ {
public TimeoutException() public TimeoutException()
: base("An operation has timed out.") : base("An operation has timed out.")

View File

@@ -14,7 +14,7 @@ namespace Discord.Net.WebSockets
} }
} }
public partial class GatewayWebSocket public partial class GatewaySocket
{ {
public event EventHandler<WebSocketEventEventArgs> ReceivedDispatch; public event EventHandler<WebSocketEventEventArgs> ReceivedDispatch;
private void RaiseReceivedDispatch(string type, JToken payload) private void RaiseReceivedDispatch(string type, JToken payload)

View File

@@ -6,7 +6,7 @@ using System.Threading.Tasks;
namespace Discord.Net.WebSockets namespace Discord.Net.WebSockets
{ {
public partial class GatewayWebSocket : WebSocket public partial class GatewaySocket : WebSocket
{ {
public int LastSequence => _lastSeq; public int LastSequence => _lastSeq;
private int _lastSeq; private int _lastSeq;
@@ -17,7 +17,7 @@ namespace Discord.Net.WebSockets
public string SessionId => _sessionId; public string SessionId => _sessionId;
private string _sessionId; private string _sessionId;
public GatewayWebSocket(DiscordConfig config, Logger logger) public GatewaySocket(DiscordConfig config, Logger logger)
: base(config, logger) : base(config, logger)
{ {
Disconnected += async (s, e) => Disconnected += async (s, e) =>
@@ -28,14 +28,18 @@ namespace Discord.Net.WebSockets
} }
public async Task Connect(string token) public async Task Connect(string token)
{ {
_token = token; await SignalDisconnect(wait: true).ConfigureAwait(false);
_token = token;
await BeginConnect().ConfigureAwait(false); await BeginConnect().ConfigureAwait(false);
SendIdentify(token); SendIdentify(token);
} }
private async Task Redirect(string server) private async Task Redirect(string server)
{ {
await BeginConnect().ConfigureAwait(false); await SignalDisconnect(wait: true).ConfigureAwait(false);
await BeginConnect().ConfigureAwait(false);
SendResume(); SendResume();
} }
private async Task Reconnect() private async Task Reconnect()
@@ -47,8 +51,8 @@ namespace Discord.Net.WebSockets
while (!cancelToken.IsCancellationRequested) while (!cancelToken.IsCancellationRequested)
{ {
try try
{ {
await Connect(_token).ConfigureAwait(false); await Connect(_token).ConfigureAwait(false);
break; break;
} }
catch (OperationCanceledException) { throw; } catch (OperationCanceledException) { throw; }

View File

@@ -114,7 +114,6 @@ namespace Discord.Net.WebSockets
{ {
try try
{ {
await SignalDisconnect(wait: true).ConfigureAwait(false);
_state = (int)WebSocketState.Connecting; _state = (int)WebSocketState.Connecting;
if (ParentCancelToken == null) if (ParentCancelToken == null)
@@ -173,7 +172,7 @@ namespace Discord.Net.WebSockets
await Cleanup().ConfigureAwait(false); await Cleanup().ConfigureAwait(false);
} }
if (!wait) if (wait)
{ {
Task task = _runTask; Task task = _runTask;
if (_runTask != null) if (_runTask != null)

View File

@@ -0,0 +1 @@
<StyleCopSettings Version="105" />

View File

@@ -28,9 +28,10 @@
} }
}, },
"dependencies": { "dependencies": {
"Newtonsoft.Json": "7.0.1" "Newtonsoft.Json": "7.0.1",
}, "StyleCop.Analyzers": "1.0.0-rc2"
},
"frameworks": { "frameworks": {
"net45": { "net45": {
@@ -40,22 +41,22 @@
} }
}, },
"dotnet5.4": { "dotnet5.4": {
"dependencies": { "dependencies": {
"System.Collections": "4.0.11-beta-23516", "System.Collections": "4.0.11-beta-23516",
"System.Collections.Concurrent": "4.0.11-beta-23516", "System.Collections.Concurrent": "4.0.11-beta-23516",
"System.Dynamic.Runtime": "4.0.11-beta-23516", "System.Dynamic.Runtime": "4.0.11-beta-23516",
"System.IO.FileSystem": "4.0.1-beta-23516", "System.IO.FileSystem": "4.0.1-beta-23516",
"System.IO.Compression": "4.1.0-beta-23516", "System.IO.Compression": "4.1.0-beta-23516",
"System.Linq": "4.0.1-beta-23516", "System.Linq": "4.0.1-beta-23516",
"System.Net.NameResolution": "4.0.0-beta-23516", "System.Net.NameResolution": "4.0.0-beta-23516",
"System.Net.Sockets": "4.1.0-beta-23409", "System.Net.Sockets": "4.1.0-beta-23409",
"System.Net.Requests": "4.0.11-beta-23516", "System.Net.Requests": "4.0.11-beta-23516",
"System.Net.WebSockets.Client": "4.0.0-beta-23516", "System.Net.WebSockets.Client": "4.0.0-beta-23516",
"System.Runtime.InteropServices": "4.0.21-beta-23516", "System.Runtime.InteropServices": "4.0.21-beta-23516",
"System.Text.RegularExpressions": "4.0.11-beta-23516", "System.Text.RegularExpressions": "4.0.11-beta-23516",
"System.Threading": "4.0.11-beta-23516", "System.Threading": "4.0.11-beta-23516",
"System.Threading.Thread": "4.0.0-beta-23516" "System.Threading.Thread": "4.0.0-beta-23516"
} }
} }
} }
} }