@@ -67,18 +67,21 @@ | |||
<Compile Include="..\Discord.Net.Audio\Net\WebSockets\VoiceWebSocket.Events.cs"> | |||
<Link>Net\WebSockets\VoiceWebSocket.Events.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net.Audio\Opus.cs"> | |||
<Link>Opus.cs</Link> | |||
<Compile Include="..\Discord.Net.Audio\Opus\Enums.cs"> | |||
<Link>Opus\Enums.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net.Audio\OpusDecoder.cs"> | |||
<Link>OpusDecoder.cs</Link> | |||
<Compile Include="..\Discord.Net.Audio\Opus\OpusDecoder.cs"> | |||
<Link>Opus\OpusDecoder.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net.Audio\OpusEncoder.cs"> | |||
<Link>OpusEncoder.cs</Link> | |||
<Compile Include="..\Discord.Net.Audio\Opus\OpusEncoder.cs"> | |||
<Link>Opus\OpusEncoder.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net.Audio\Sodium.cs"> | |||
<Link>Sodium.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net.Audio\Sodium\SecretBox.cs"> | |||
<Link>Sodium\SecretBox.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net.Audio\VoiceBuffer.cs"> | |||
<Link>VoiceBuffer.cs</Link> | |||
</Compile> | |||
@@ -145,27 +145,27 @@ namespace Discord.Audio | |||
return Task.FromResult(_defaultClient); | |||
} | |||
var client = _voiceClients.GetOrAdd(server.Id, _ => | |||
var client = _voiceClients.GetOrAdd(server.Id, (Func<long, DiscordAudioClient>)(_ => | |||
{ | |||
int id = unchecked(++_nextClientId); | |||
var logger = Client.Log().CreateLogger($"Voice #{id}"); | |||
GatewayWebSocket gatewaySocket = null; | |||
Net.WebSockets.GatewaySocket gatewaySocket = null; | |||
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); | |||
voiceSocket.OnPacket += (s, e) => | |||
{ | |||
RaiseOnPacket(e); | |||
RaiseOnPacket(e); | |||
}; | |||
voiceSocket.IsSpeaking += (s, e) => | |||
{ | |||
var user = Client.GetUser(server, e.UserId); | |||
RaiseUserIsSpeakingUpdated(user, e.IsSpeaking); | |||
RaiseUserIsSpeakingUpdated(user, e.IsSpeaking); | |||
}; | |||
return voiceClient; | |||
}); | |||
})); | |||
//await client.Connect(gatewaySocket.Host, _client.Token).ConfigureAwait(false); | |||
return Task.FromResult(client); | |||
} | |||
@@ -37,8 +37,12 @@ namespace Discord.Audio | |||
public int? Bitrate { get { return _bitrate; } set { SetValue(ref _bitrate, value); } } | |||
private int? _bitrate = null; | |||
//Lock | |||
protected bool _isLocked; | |||
/// <summary> Gets or sets the number of channels (1 or 2) used for outgoing audio. </summary> | |||
public int Channels { get { return _channels; } set { SetValue(ref _channels, value); } } | |||
private int _channels = 1; | |||
//Lock | |||
protected bool _isLocked; | |||
internal void Lock() { _isLocked = true; } | |||
protected void SetValue<T>(ref T storage, T value) | |||
{ | |||
@@ -10,14 +10,14 @@ namespace Discord.Audio | |||
public int Id => _id; | |||
private readonly AudioService _service; | |||
private readonly GatewayWebSocket _gatewaySocket; | |||
private readonly GatewaySocket _gatewaySocket; | |||
private readonly VoiceWebSocket _voiceSocket; | |||
private readonly Logger _logger; | |||
public long? ServerId => _voiceSocket.ServerId; | |||
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; | |||
_id = id; | |||
@@ -1,5 +1,7 @@ | |||
using Discord.API; | |||
using Discord.Audio; | |||
using Discord.Audio.Opus; | |||
using Discord.Audio.Sodium; | |||
using Newtonsoft.Json; | |||
using Newtonsoft.Json.Linq; | |||
using System; | |||
@@ -54,7 +56,7 @@ namespace Discord.Net.WebSockets | |||
_targetAudioBufferLength = _audioConfig.BufferLength / 20; //20 ms frames | |||
_encodingBuffer = new byte[MaxOpusSize]; | |||
_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); | |||
} | |||
@@ -223,7 +225,7 @@ namespace Discord.Net.WebSockets | |||
return; | |||
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) | |||
continue; | |||
result = decodingBuffer; | |||
@@ -294,7 +296,7 @@ namespace Discord.Net.WebSockets | |||
if (_isEncrypted) | |||
{ | |||
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) | |||
throw new InvalidOperationException("Failed to encrypt ping packet"); | |||
pingPacket = new byte[pingPacket.Length + 16]; | |||
@@ -333,7 +335,7 @@ namespace Discord.Net.WebSockets | |||
if (_isEncrypted) | |||
{ | |||
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) | |||
continue; | |||
rtpPacketLength = encodedLength + 12 + 16; | |||
@@ -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 | |||
} | |||
} | |||
} |
@@ -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 | |||
} | |||
} |
@@ -1,11 +1,26 @@ | |||
using System; | |||
using System.Runtime.InteropServices; | |||
using System.Security; | |||
namespace Discord.Audio | |||
namespace Discord.Audio.Opus | |||
{ | |||
/// <summary> Opus codec wrapper. </summary> | |||
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> | |||
public const int BitRate = 16; | |||
@@ -22,7 +37,7 @@ namespace Discord.Audio | |||
/// <summary> Gets the bytes per frame. </summary> | |||
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="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> | |||
@@ -44,45 +59,32 @@ namespace Discord.Audio | |||
SamplesPerFrame = samplingRate / 1000 * FrameLength; | |||
FrameSize = SamplesPerFrame * SampleSize; | |||
Opus.Error error; | |||
_ptr = Opus.CreateDecoder(samplingRate, channels, out error); | |||
if (error != Opus.Error.OK) | |||
OpusError error; | |||
_ptr = UnsafeNativeMethods.CreateDecoder(samplingRate, channels, out error); | |||
if (error != OpusError.OK) | |||
throw new InvalidOperationException($"Error occured while creating decoder: {error}"); | |||
SetForwardErrorCorrection(true); | |||
} | |||
/// <summary> Produces Opus encoded audio from PCM samples. </summary> | |||
/// <param name="input">PCM samples to encode.</param> | |||
/// <param name="inputOffset">Offset of the frame in pcmSamples.</param> | |||
/// <param name="output">Buffer to store the encoded frame.</param> | |||
/// <returns>Length of the frame contained in outputBuffer.</returns> | |||
public unsafe int DecodeFrame(byte[] input, int inputOffset, byte[] output) | |||
/// <summary> Produces PCM samples from Opus-encoded audio. </summary> | |||
/// <param name="input">PCM samples to decode.</param> | |||
/// <param name="inputOffset">Offset of the frame in input.</param> | |||
/// <param name="output">Buffer to store the decoded frame.</param> | |||
/// <returns>Length of the frame contained in output.</returns> | |||
public unsafe int DecodeFrame(byte[] input, int inputOffset, byte[] output) | |||
{ | |||
if (disposed) | |||
throw new ObjectDisposedException(nameof(OpusDecoder)); | |||
int result = 0; | |||
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) | |||
throw new Exception("Decoding failed: " + ((Opus.Error)result).ToString()); | |||
throw new Exception("Decoding failed: " + ((OpusError)result).ToString()); | |||
return result; | |||
} | |||
/// <summary> Gets or sets whether Forward Error Correction is enabled. </summary> | |||
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 | |||
#region IDisposable | |||
private bool disposed; | |||
public void Dispose() | |||
{ | |||
@@ -92,7 +94,7 @@ namespace Discord.Audio | |||
GC.SuppressFinalize(this); | |||
if (_ptr != IntPtr.Zero) | |||
Opus.DestroyEncoder(_ptr); | |||
UnsafeNativeMethods.DestroyDecoder(_ptr); | |||
disposed = true; | |||
} | |||
@@ -100,6 +102,6 @@ namespace Discord.Audio | |||
{ | |||
Dispose(); | |||
} | |||
#endregion | |||
#endregion | |||
} | |||
} |
@@ -1,11 +1,28 @@ | |||
using System; | |||
using System.Runtime.InteropServices; | |||
using System.Security; | |||
namespace Discord.Audio | |||
namespace Discord.Audio.Opus | |||
{ | |||
/// <summary> Opus codec wrapper. </summary> | |||
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> | |||
public const int BitsPerSample = 16; | |||
@@ -24,7 +41,7 @@ namespace Discord.Audio | |||
/// <summary> Gets the bit rate in kbit/s. </summary> | |||
public int? BitRate { get; private set; } | |||
/// <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> | |||
/// <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="application">Coding mode.</param> | |||
/// <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 && | |||
samplingRate != 16000 && samplingRate != 24000 && | |||
@@ -53,9 +70,9 @@ namespace Discord.Audio | |||
FrameSize = SamplesPerFrame * SampleSize; | |||
BitRate = bitrate; | |||
Opus.Error error; | |||
_ptr = Opus.CreateEncoder(samplingRate, channels, (int)application, out error); | |||
if (error != Opus.Error.OK) | |||
OpusError error; | |||
_ptr = UnsafeNativeMethods.CreateEncoder(samplingRate, channels, (int)application, out error); | |||
if (error != OpusError.OK) | |||
throw new InvalidOperationException($"Error occured while creating encoder: {error}"); | |||
SetForwardErrorCorrection(true); | |||
@@ -75,10 +92,10 @@ namespace Discord.Audio | |||
int result = 0; | |||
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) | |||
throw new Exception("Encoding failed: " + ((Opus.Error)result).ToString()); | |||
throw new Exception("Encoding failed: " + ((OpusError)result).ToString()); | |||
return result; | |||
} | |||
@@ -88,9 +105,9 @@ namespace Discord.Audio | |||
if (disposed) | |||
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) | |||
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> | |||
@@ -99,9 +116,9 @@ namespace Discord.Audio | |||
if (disposed) | |||
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) | |||
throw new Exception("Encoder error: " + ((Opus.Error)result).ToString()); | |||
throw new Exception("Encoder error: " + ((OpusError)result).ToString()); | |||
} | |||
#region IDisposable | |||
@@ -114,7 +131,7 @@ namespace Discord.Audio | |||
GC.SuppressFinalize(this); | |||
if (_ptr != IntPtr.Zero) | |||
Opus.DestroyEncoder(_ptr); | |||
UnsafeNativeMethods.DestroyEncoder(_ptr); | |||
disposed = true; | |||
} |
@@ -2,25 +2,4 @@ | |||
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); | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -2,7 +2,7 @@ | |||
namespace Discord.Commands | |||
{ | |||
public class CommandEventArgs | |||
public class CommandEventArgs : EventArgs | |||
{ | |||
private readonly string[] _args; | |||
@@ -7,7 +7,7 @@ using System.Threading.Tasks; | |||
namespace Discord.Commands | |||
{ | |||
/// <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."; | |||
@@ -224,11 +224,11 @@ | |||
<Compile Include="..\Discord.Net\Net\TimeoutException.cs"> | |||
<Link>Net\TimeoutException.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\Net\WebSockets\GatewayWebSocket.cs"> | |||
<Link>Net\WebSockets\GatewayWebSocket.cs</Link> | |||
<Compile Include="..\Discord.Net\Net\WebSockets\GatewaySocket.cs"> | |||
<Link>Net\WebSockets\GatewaySocket.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\Net\WebSockets\GatewayWebSockets.Events.cs"> | |||
<Link>Net\WebSockets\GatewayWebSockets.Events.cs</Link> | |||
<Compile Include="..\Discord.Net\Net\WebSockets\GatewaySocket.Events.cs"> | |||
<Link>Net\WebSockets\GatewaySocket.Events.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\Net\WebSockets\IWebSocketEngine.cs"> | |||
<Link>Net\WebSockets\IWebSocketEngine.cs</Link> | |||
@@ -9,165 +9,165 @@ using System.Collections.Generic; | |||
namespace Discord.API | |||
{ | |||
public enum GatewayOpCodes : byte | |||
{ | |||
/// <summary> Client <-- Server - Used to send most events. </summary> | |||
Dispatch = 0, | |||
/// <summary> Client <-> Server - Used to keep the connection alive and measure latency. </summary> | |||
Heartbeat = 1, | |||
/// <summary> Client --> Server - Used to associate a connection with a token and specify configuration. </summary> | |||
Identify = 2, | |||
/// <summary> Client --> Server - Used to update client's status and current game id. </summary> | |||
StatusUpdate = 3, | |||
/// <summary> Client --> Server - Used to join a particular voice channel. </summary> | |||
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> | |||
VoiceServerPing = 5, | |||
/// <summary> Client --> Server - Used to resume a connection after a redirect occurs. </summary> | |||
Resume = 6, | |||
/// <summary> Client <-- Server - Used to notify a client that they must reconnect to another gateway. </summary> | |||
Redirect = 7, | |||
/// <summary> Client --> Server - Used to request all members that were withheld by large_threshold </summary> | |||
RequestGuildMembers = 8 | |||
} | |||
public enum GatewayOpCodes : byte | |||
{ | |||
/// <summary> Client <-- Server - Used to send most events. </summary> | |||
Dispatch = 0, | |||
/// <summary> Client <-> Server - Used to keep the connection alive and measure latency. </summary> | |||
Heartbeat = 1, | |||
/// <summary> Client --> Server - Used to associate a connection with a token and specify configuration. </summary> | |||
Identify = 2, | |||
/// <summary> Client --> Server - Used to update client's status and current game id. </summary> | |||
StatusUpdate = 3, | |||
/// <summary> Client --> Server - Used to join a particular voice channel. </summary> | |||
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> | |||
VoiceServerPing = 5, | |||
/// <summary> Client --> Server - Used to resume a connection after a redirect occurs. </summary> | |||
Resume = 6, | |||
/// <summary> Client <-- Server - Used to notify a client that they must reconnect to another gateway. </summary> | |||
Redirect = 7, | |||
/// <summary> Client --> Server - Used to request all members that were withheld by large_threshold </summary> | |||
RequestGuildMembers = 8 | |||
} | |||
//Common | |||
public class WebSocketMessage | |||
{ | |||
public WebSocketMessage() { } | |||
public WebSocketMessage(int op) { Operation = op; } | |||
//Common | |||
public class WebSocketMessage | |||
{ | |||
public WebSocketMessage() { } | |||
public WebSocketMessage(int op) { Operation = op; } | |||
[JsonProperty("op")] | |||
public int Operation; | |||
[JsonProperty("d")] | |||
public object Payload; | |||
[JsonProperty("t", NullValueHandling = NullValueHandling.Ignore)] | |||
public string Type; | |||
[JsonProperty("s", NullValueHandling = NullValueHandling.Ignore)] | |||
public int? Sequence; | |||
} | |||
public abstract class WebSocketMessage<T> : WebSocketMessage | |||
where T : new() | |||
{ | |||
public WebSocketMessage() { Payload = new T(); } | |||
public WebSocketMessage(int op) : base(op) { Payload = new T(); } | |||
public WebSocketMessage(int op, T payload) : base(op) { Payload = payload; } | |||
[JsonProperty("op")] | |||
public int Operation; | |||
[JsonProperty("d")] | |||
public object Payload; | |||
[JsonProperty("t", NullValueHandling = NullValueHandling.Ignore)] | |||
public string Type; | |||
[JsonProperty("s", NullValueHandling = NullValueHandling.Ignore)] | |||
public int? Sequence; | |||
} | |||
public abstract class WebSocketMessage<T> : WebSocketMessage | |||
where T : new() | |||
{ | |||
public WebSocketMessage() { Payload = new T(); } | |||
public WebSocketMessage(int op) : base(op) { Payload = new T(); } | |||
public WebSocketMessage(int op, T payload) : base(op) { Payload = payload; } | |||
[JsonIgnore] | |||
public new T Payload | |||
{ | |||
get | |||
{ | |||
if (base.Payload is JToken) | |||
base.Payload = (base.Payload as JToken).ToObject<T>(); | |||
return (T)base.Payload; | |||
} | |||
set { base.Payload = value; } | |||
} | |||
} | |||
[JsonIgnore] | |||
public new T Payload | |||
{ | |||
get | |||
{ | |||
if (base.Payload is JToken) | |||
base.Payload = (base.Payload as JToken).ToObject<T>(); | |||
return (T)base.Payload; | |||
} | |||
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; | |||
//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> | |||
{ | |||
public StatusUpdateCommand() : base((int)GatewayOpCodes.StatusUpdate) { } | |||
public class Data | |||
{ | |||
[JsonProperty("idle_since")] | |||
public long? IdleSince; | |||
[JsonProperty("game_id")] | |||
public int? GameId; | |||
} | |||
} | |||
internal sealed class StatusUpdateCommand : WebSocketMessage<StatusUpdateCommand.Data> | |||
{ | |||
public StatusUpdateCommand() : base((int)GatewayOpCodes.StatusUpdate) { } | |||
public class Data | |||
{ | |||
[JsonProperty("idle_since")] | |||
public long? IdleSince; | |||
[JsonProperty("game_id")] | |||
public int? GameId; | |||
} | |||
} | |||
internal sealed class JoinVoiceCommand : WebSocketMessage<JoinVoiceCommand.Data> | |||
{ | |||
public JoinVoiceCommand() : base((int)GatewayOpCodes.VoiceStateUpdate) { } | |||
public class Data | |||
{ | |||
[JsonProperty("guild_id")] | |||
[JsonConverter(typeof(LongStringConverter))] | |||
public long ServerId; | |||
[JsonProperty("channel_id")] | |||
[JsonConverter(typeof(LongStringConverter))] | |||
public long ChannelId; | |||
[JsonProperty("self_mute")] | |||
public string SelfMute; | |||
[JsonProperty("self_deaf")] | |||
public string SelfDeaf; | |||
} | |||
} | |||
internal sealed class JoinVoiceCommand : WebSocketMessage<JoinVoiceCommand.Data> | |||
{ | |||
public JoinVoiceCommand() : base((int)GatewayOpCodes.VoiceStateUpdate) { } | |||
public class Data | |||
{ | |||
[JsonProperty("guild_id")] | |||
[JsonConverter(typeof(LongStringConverter))] | |||
public long ServerId; | |||
[JsonProperty("channel_id")] | |||
[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> | |||
{ | |||
public ResumeCommand() : base((int)GatewayOpCodes.Resume) { } | |||
public class Data | |||
{ | |||
[JsonProperty("session_id")] | |||
public string SessionId; | |||
[JsonProperty("seq")] | |||
public int Sequence; | |||
} | |||
} | |||
internal sealed class ResumeCommand : WebSocketMessage<ResumeCommand.Data> | |||
{ | |||
public ResumeCommand() : base((int)GatewayOpCodes.Resume) { } | |||
public class Data | |||
{ | |||
[JsonProperty("session_id")] | |||
public string SessionId; | |||
[JsonProperty("seq")] | |||
public int Sequence; | |||
} | |||
} | |||
//Events | |||
internal sealed class ReadyEvent | |||
{ | |||
public sealed class ReadStateInfo | |||
{ | |||
[JsonProperty("id")] | |||
public string ChannelId; | |||
[JsonProperty("mention_count")] | |||
public int MentionCount; | |||
[JsonProperty("last_message_id")] | |||
public string LastMessageId; | |||
} | |||
//Events | |||
internal sealed class ReadyEvent | |||
{ | |||
public sealed class ReadStateInfo | |||
{ | |||
[JsonProperty("id")] | |||
public string ChannelId; | |||
[JsonProperty("mention_count")] | |||
public int MentionCount; | |||
[JsonProperty("last_message_id")] | |||
public string LastMessageId; | |||
} | |||
[JsonProperty("v")] | |||
public int Version; | |||
[JsonProperty("user")] | |||
public UserInfo User; | |||
[JsonProperty("session_id")] | |||
public string SessionId; | |||
[JsonProperty("read_state")] | |||
public ReadStateInfo[] ReadState; | |||
[JsonProperty("guilds")] | |||
public ExtendedGuildInfo[] Guilds; | |||
[JsonProperty("private_channels")] | |||
public ChannelInfo[] PrivateChannels; | |||
[JsonProperty("heartbeat_interval")] | |||
public int HeartbeatInterval; | |||
} | |||
[JsonProperty("v")] | |||
public int Version; | |||
[JsonProperty("user")] | |||
public UserInfo User; | |||
[JsonProperty("session_id")] | |||
public string SessionId; | |||
[JsonProperty("read_state")] | |||
public ReadStateInfo[] ReadState; | |||
[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; | |||
} | |||
internal sealed class RedirectEvent | |||
{ | |||
[JsonProperty("url")] | |||
public string Url; | |||
} | |||
internal sealed class ResumeEvent | |||
{ | |||
[JsonProperty("heartbeat_interval")] | |||
public int HeartbeatInterval; | |||
} | |||
} |
@@ -58,7 +58,7 @@ namespace Discord | |||
} | |||
} | |||
public partial class DiscordClient | |||
public partial class DiscordClient : IDisposable | |||
{ | |||
public event EventHandler<UserEventArgs> UserJoined; | |||
private void RaiseUserJoined(User user) | |||
@@ -305,5 +305,5 @@ namespace Discord | |||
_webSocket.SendStatusUpdate(_status == UserStatus.Idle ? EpochTime.GetMilliseconds() - (10 * 60 * 1000) : (long?)null, _gameId); | |||
return TaskHelper.CompletedTask; | |||
} | |||
} | |||
} | |||
} |
@@ -4,7 +4,6 @@ using Newtonsoft.Json; | |||
using System; | |||
using System.Collections.Concurrent; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Reflection; | |||
using System.Runtime.ExceptionServices; | |||
using System.Threading; | |||
@@ -82,8 +81,8 @@ namespace Discord | |||
private readonly DiscordAPIClient _api; | |||
/// <summary> Returns the internal websocket object. </summary> | |||
public GatewayWebSocket WebSocket => _webSocket; | |||
private readonly GatewayWebSocket _webSocket; | |||
public GatewaySocket WebSocket => _webSocket; | |||
private readonly GatewaySocket _webSocket; | |||
public string GatewayUrl => _gateway; | |||
private string _gateway; | |||
@@ -140,11 +139,24 @@ namespace Discord | |||
CreateCacheLogger(); | |||
//Networking | |||
_webSocket = CreateWebSocket(); | |||
_api = new DiscordAPIClient(_config); | |||
_webSocket = new GatewaySocket(_config, _log.CreateLogger("WebSocket")); | |||
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) | |||
_pendingMessages = new ConcurrentQueue<Message>(); | |||
this.Connected += async (s, e) => | |||
Connected += async (s, e) => | |||
{ | |||
_api.CancelToken = _cancelToken; | |||
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> | |||
/// <returns> Returns a token for future connections. </returns> | |||
public async Task<string> Connect(string email, string password) | |||
@@ -285,73 +279,73 @@ namespace Discord | |||
if (State != DiscordClientState.Disconnected) | |||
await Disconnect().ConfigureAwait(false); | |||
string token; | |||
try | |||
{ | |||
var response = await _api.Login(email, password) | |||
.ConfigureAwait(false); | |||
token = response.Token; | |||
if (_config.LogLevel >= LogSeverity.Verbose) | |||
_logger.Log(LogSeverity.Verbose, "Login successful, got token."); | |||
await Connect(token); | |||
return token; | |||
} | |||
catch (TaskCanceledException) { throw new TimeoutException(); } | |||
} | |||
var response = await _api.Login(email, password) | |||
.ConfigureAwait(false); | |||
_token = response.Token; | |||
_api.Token = response.Token; | |||
if (_config.LogLevel >= LogSeverity.Verbose) | |||
_logger.Log(LogSeverity.Verbose, "Login successful, got token."); | |||
await BeginConnect(); | |||
return response.Token; | |||
} | |||
/// <summary> Connects to the Discord server with the provided token. </summary> | |||
public async Task Connect(string token) | |||
{ | |||
if (!_sentInitialLog) | |||
SendInitialLog(); | |||
if (State != (int)DiscordClientState.Disconnected) | |||
await Disconnect().ConfigureAwait(false); | |||
{ | |||
if (!_sentInitialLog) | |||
SendInitialLog(); | |||
_api.Token = token; | |||
var gatewayResponse = await _api.Gateway().ConfigureAwait(false); | |||
string gateway = gatewayResponse.Url; | |||
if (_config.LogLevel >= LogSeverity.Verbose) | |||
_logger.Log(LogSeverity.Verbose, $"Websocket endpoint: {gateway}"); | |||
try | |||
{ | |||
_state = (int)DiscordClientState.Connecting; | |||
_disconnectedEvent.Reset(); | |||
_gateway = gateway; | |||
_token = token; | |||
if (State != (int)DiscordClientState.Disconnected) | |||
await Disconnect().ConfigureAwait(false); | |||
_cancelTokenSource = new CancellationTokenSource(); | |||
_cancelToken = _cancelTokenSource.Token; | |||
_webSocket.Host = gateway; | |||
_webSocket.ParentCancelToken = _cancelToken; | |||
await _webSocket.Connect(token).ConfigureAwait(false); | |||
_runTask = RunTasks(); | |||
try | |||
{ | |||
//Cancel if either Disconnect is called, data socket errors or timeout is reached | |||
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; | |||
} | |||
_token = token; | |||
_api.Token = token; | |||
await BeginConnect(); | |||
} | |||
//_state = (int)DiscordClientState.Connected; | |||
} | |||
catch | |||
{ | |||
await Disconnect().ConfigureAwait(false); | |||
throw; | |||
} | |||
} | |||
private void CompleteConnect() | |||
private async Task BeginConnect() | |||
{ | |||
try | |||
{ | |||
_state = (int)DiscordClientState.Connecting; | |||
var gatewayResponse = await _api.Gateway().ConfigureAwait(false); | |||
string gateway = gatewayResponse.Url; | |||
if (_config.LogLevel >= LogSeverity.Verbose) | |||
_logger.Log(LogSeverity.Verbose, $"Websocket endpoint: {gateway}"); | |||
_disconnectedEvent.Reset(); | |||
_gateway = gateway; | |||
_cancelTokenSource = new CancellationTokenSource(); | |||
_cancelToken = _cancelTokenSource.Token; | |||
_webSocket.Host = gateway; | |||
_webSocket.ParentCancelToken = _cancelToken; | |||
await _webSocket.Connect(_token).ConfigureAwait(false); | |||
_runTask = RunTasks(); | |||
try | |||
{ | |||
//Cancel if either Disconnect is called, data socket errors or timeout is reached | |||
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; | |||
} | |||
} | |||
catch | |||
{ | |||
await Disconnect().ConfigureAwait(false); | |||
throw; | |||
} | |||
} | |||
private void EndConnect() | |||
{ | |||
_state = (int)DiscordClientState.Connected; | |||
_connectedEvent.Set(); | |||
@@ -359,8 +353,8 @@ namespace Discord | |||
} | |||
/// <summary> Disconnects from the Discord server, canceling any pending requests. </summary> | |||
public Task Disconnect() => DisconnectInternal(new Exception("Disconnect was requested by user."), isUnexpected: false); | |||
private async Task DisconnectInternal(Exception ex = null, bool isUnexpected = true, bool skipAwait = false) | |||
public Task Disconnect() => SignalDisconnect(new Exception("Disconnect was requested by user."), isUnexpected: false); | |||
private async Task SignalDisconnect(Exception ex = null, bool isUnexpected = true, bool wait = false) | |||
{ | |||
int oldState; | |||
bool hasWriterLock; | |||
@@ -386,7 +380,7 @@ namespace Discord | |||
await Cleanup().ConfigureAwait(false);*/ | |||
} | |||
if (!skipAwait) | |||
if (wait) | |||
{ | |||
Task task = _runTask; | |||
if (_runTask != null) | |||
@@ -407,10 +401,10 @@ namespace Discord | |||
//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); } | |||
catch (Exception ex) { await SignalDisconnect(ex: ex, wait: true).ConfigureAwait(false); } | |||
//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 | |||
try { await allTasks.ConfigureAwait(false); } | |||
@@ -453,35 +447,6 @@ namespace Discord | |||
_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) | |||
{ | |||
@@ -851,45 +816,99 @@ namespace Discord | |||
_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 | |||
#region Services | |||
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); | |||
#endregion | |||
#region IDisposable | |||
private bool _isDisposed = false; | |||
protected virtual void Dispose(bool isDisposing) | |||
{ | |||
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."); | |||
} | |||
} | |||
//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 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(); | |||
} | |||
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; | |||
} | |||
} | |||
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; | |||
} | |||
} | |||
} |
@@ -42,6 +42,7 @@ namespace Discord | |||
public bool IsServerDeafened { get; private set; } | |||
public bool IsServerSuppressed { get; private set; } | |||
public bool IsPrivate => _server.Id == null; | |||
public bool IsOwner => _server.Value.OwnerId == Id; | |||
public string SessionId { get; private set; } | |||
public string Token { get; private set; } | |||
@@ -101,9 +102,23 @@ namespace Discord | |||
{ | |||
if (_server.Id != null) | |||
{ | |||
return Server.Channels | |||
.Where(x => (x.Type == ChannelType.Text && x.GetPermissions(this).ReadMessages) || | |||
(x.Type == ChannelType.Voice && x.GetPermissions(this).Connect)); | |||
if (_client.Config.UsePermissionsCache) | |||
{ | |||
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 | |||
{ | |||
@@ -1,8 +1,12 @@ | |||
using System; | |||
using System.Net; | |||
using System.Runtime.Serialization; | |||
namespace Discord.Net | |||
{ | |||
#if NET45 | |||
[Serializable] | |||
#endif | |||
public class HttpException : Exception | |||
{ | |||
public HttpStatusCode StatusCode { get; } | |||
@@ -11,6 +15,10 @@ namespace Discord.Net | |||
: base($"The server responded with error {(int)statusCode} ({statusCode})") | |||
{ | |||
StatusCode = statusCode; | |||
} | |||
} | |||
} | |||
#if NET45 | |||
public override void GetObjectData(SerializationInfo info, StreamingContext context) | |||
=> base.GetObjectData(info, context); | |||
#endif | |||
} | |||
} |
@@ -2,7 +2,10 @@ | |||
namespace Discord.Net | |||
{ | |||
public sealed class TimeoutException : OperationCanceledException | |||
#if NET45 | |||
[Serializable] | |||
#endif | |||
public sealed class TimeoutException : OperationCanceledException | |||
{ | |||
public TimeoutException() | |||
: base("An operation has timed out.") | |||
@@ -14,7 +14,7 @@ namespace Discord.Net.WebSockets | |||
} | |||
} | |||
public partial class GatewayWebSocket | |||
public partial class GatewaySocket | |||
{ | |||
public event EventHandler<WebSocketEventEventArgs> ReceivedDispatch; | |||
private void RaiseReceivedDispatch(string type, JToken payload) |
@@ -6,7 +6,7 @@ using System.Threading.Tasks; | |||
namespace Discord.Net.WebSockets | |||
{ | |||
public partial class GatewayWebSocket : WebSocket | |||
public partial class GatewaySocket : WebSocket | |||
{ | |||
public int LastSequence => _lastSeq; | |||
private int _lastSeq; | |||
@@ -17,7 +17,7 @@ namespace Discord.Net.WebSockets | |||
public string SessionId => _sessionId; | |||
private string _sessionId; | |||
public GatewayWebSocket(DiscordConfig config, Logger logger) | |||
public GatewaySocket(DiscordConfig config, Logger logger) | |||
: base(config, logger) | |||
{ | |||
Disconnected += async (s, e) => | |||
@@ -28,14 +28,18 @@ namespace Discord.Net.WebSockets | |||
} | |||
public async Task Connect(string token) | |||
{ | |||
_token = token; | |||
{ | |||
await SignalDisconnect(wait: true).ConfigureAwait(false); | |||
_token = token; | |||
await BeginConnect().ConfigureAwait(false); | |||
SendIdentify(token); | |||
} | |||
private async Task Redirect(string server) | |||
{ | |||
await BeginConnect().ConfigureAwait(false); | |||
{ | |||
await SignalDisconnect(wait: true).ConfigureAwait(false); | |||
await BeginConnect().ConfigureAwait(false); | |||
SendResume(); | |||
} | |||
private async Task Reconnect() | |||
@@ -47,8 +51,8 @@ namespace Discord.Net.WebSockets | |||
while (!cancelToken.IsCancellationRequested) | |||
{ | |||
try | |||
{ | |||
await Connect(_token).ConfigureAwait(false); | |||
{ | |||
await Connect(_token).ConfigureAwait(false); | |||
break; | |||
} | |||
catch (OperationCanceledException) { throw; } |
@@ -114,7 +114,6 @@ namespace Discord.Net.WebSockets | |||
{ | |||
try | |||
{ | |||
await SignalDisconnect(wait: true).ConfigureAwait(false); | |||
_state = (int)WebSocketState.Connecting; | |||
if (ParentCancelToken == null) | |||
@@ -173,7 +172,7 @@ namespace Discord.Net.WebSockets | |||
await Cleanup().ConfigureAwait(false); | |||
} | |||
if (!wait) | |||
if (wait) | |||
{ | |||
Task task = _runTask; | |||
if (_runTask != null) | |||
@@ -0,0 +1 @@ | |||
<StyleCopSettings Version="105" /> |
@@ -28,9 +28,10 @@ | |||
} | |||
}, | |||
"dependencies": { | |||
"Newtonsoft.Json": "7.0.1" | |||
}, | |||
"dependencies": { | |||
"Newtonsoft.Json": "7.0.1", | |||
"StyleCop.Analyzers": "1.0.0-rc2" | |||
}, | |||
"frameworks": { | |||
"net45": { | |||
@@ -40,22 +41,22 @@ | |||
} | |||
}, | |||
"dotnet5.4": { | |||
"dependencies": { | |||
"System.Collections": "4.0.11-beta-23516", | |||
"System.Collections.Concurrent": "4.0.11-beta-23516", | |||
"System.Dynamic.Runtime": "4.0.11-beta-23516", | |||
"System.IO.FileSystem": "4.0.1-beta-23516", | |||
"System.IO.Compression": "4.1.0-beta-23516", | |||
"System.Linq": "4.0.1-beta-23516", | |||
"System.Net.NameResolution": "4.0.0-beta-23516", | |||
"System.Net.Sockets": "4.1.0-beta-23409", | |||
"System.Net.Requests": "4.0.11-beta-23516", | |||
"System.Net.WebSockets.Client": "4.0.0-beta-23516", | |||
"System.Runtime.InteropServices": "4.0.21-beta-23516", | |||
"System.Text.RegularExpressions": "4.0.11-beta-23516", | |||
"System.Threading": "4.0.11-beta-23516", | |||
"System.Threading.Thread": "4.0.0-beta-23516" | |||
} | |||
"dependencies": { | |||
"System.Collections": "4.0.11-beta-23516", | |||
"System.Collections.Concurrent": "4.0.11-beta-23516", | |||
"System.Dynamic.Runtime": "4.0.11-beta-23516", | |||
"System.IO.FileSystem": "4.0.1-beta-23516", | |||
"System.IO.Compression": "4.1.0-beta-23516", | |||
"System.Linq": "4.0.1-beta-23516", | |||
"System.Net.NameResolution": "4.0.0-beta-23516", | |||
"System.Net.Sockets": "4.1.0-beta-23409", | |||
"System.Net.Requests": "4.0.11-beta-23516", | |||
"System.Net.WebSockets.Client": "4.0.0-beta-23516", | |||
"System.Runtime.InteropServices": "4.0.21-beta-23516", | |||
"System.Text.RegularExpressions": "4.0.11-beta-23516", | |||
"System.Threading": "4.0.11-beta-23516", | |||
"System.Threading.Thread": "4.0.0-beta-23516" | |||
} | |||
} | |||
} | |||
} |