| @@ -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"> | |||||
| <Link>Opus.cs</Link> | |||||
| <Compile Include="..\Discord.Net.Audio\Opus\Enums.cs"> | |||||
| <Link>Opus\Enums.cs</Link> | |||||
| </Compile> | </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> | ||||
| <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> | ||||
| <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> | ||||
| @@ -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); | ||||
| } | } | ||||
| @@ -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 | |||||
| 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; } | internal void Lock() { _isLocked = true; } | ||||
| protected void SetValue<T>(ref T storage, T value) | protected void SetValue<T>(ref T storage, T value) | ||||
| { | { | ||||
| @@ -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; | ||||
| @@ -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; | ||||
| @@ -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; | ||||
| 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; | |||||
| _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}"); | 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) | 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> | |||||
| 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; | 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 | |||||
| } | } | ||||
| } | } | ||||
| @@ -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; | |||||
| _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}"); | 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; | ||||
| } | } | ||||
| @@ -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); | |||||
| } | |||||
| } | |||||
| } | } | ||||
| @@ -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 | namespace Discord.Commands | ||||
| { | { | ||||
| public class CommandEventArgs | |||||
| public class CommandEventArgs : EventArgs | |||||
| { | { | ||||
| private readonly string[] _args; | private readonly string[] _args; | ||||
| @@ -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."; | ||||
| @@ -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"> | |||||
| <Link>Net\WebSockets\GatewayWebSocket.cs</Link> | |||||
| <Compile Include="..\Discord.Net\Net\WebSockets\GatewaySocket.cs"> | |||||
| <Link>Net\WebSockets\GatewaySocket.cs</Link> | |||||
| </Compile> | </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> | ||||
| <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> | ||||
| @@ -9,165 +9,165 @@ using System.Collections.Generic; | |||||
| namespace Discord.API | 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; | 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; | ||||
| } | } | ||||
| } | |||||
| } | |||||
| } | } | ||||
| @@ -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; | |||||
| private readonly GatewayWebSocket _webSocket; | |||||
| public GatewaySocket WebSocket => _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(); | |||||
| _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) | 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; | |||||
| 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> | /// <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) | |||||
| 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; | _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); | |||||
| 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; | 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 | |||||
| #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 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 | |||||
| .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 | else | ||||
| { | { | ||||
| @@ -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 | |||||
| } | |||||
| } | } | ||||
| @@ -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.") | ||||
| @@ -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) | ||||
| @@ -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; } | ||||
| @@ -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) | ||||
| @@ -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": { | "frameworks": { | ||||
| "net45": { | "net45": { | ||||
| @@ -40,22 +41,22 @@ | |||||
| } | } | ||||
| }, | }, | ||||
| "dotnet5.4": { | "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" | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||