diff --git a/src/Discord.Net.Audio.Net5/Discord.Net.Audio.csproj b/src/Discord.Net.Audio.Net5/Discord.Net.Audio.csproj index 326fb88ae..ce88c0890 100644 --- a/src/Discord.Net.Audio.Net5/Discord.Net.Audio.csproj +++ b/src/Discord.Net.Audio.Net5/Discord.Net.Audio.csproj @@ -67,18 +67,21 @@ Net\WebSockets\VoiceWebSocket.Events.cs - - Opus.cs + + Opus\Enums.cs - - OpusDecoder.cs + + Opus\OpusDecoder.cs - - OpusEncoder.cs + + Opus\OpusEncoder.cs Sodium.cs + + Sodium\SecretBox.cs + VoiceBuffer.cs diff --git a/src/Discord.Net.Audio/AudioService.cs b/src/Discord.Net.Audio/AudioService.cs index 320bc4dee..74bc67c33 100644 --- a/src/Discord.Net.Audio/AudioService.cs +++ b/src/Discord.Net.Audio/AudioService.cs @@ -145,27 +145,27 @@ namespace Discord.Audio return Task.FromResult(_defaultClient); } - var client = _voiceClients.GetOrAdd(server.Id, _ => + var client = _voiceClients.GetOrAdd(server.Id, (Func)(_ => { 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); } diff --git a/src/Discord.Net.Audio/AudioServiceConfig.cs b/src/Discord.Net.Audio/AudioServiceConfig.cs index 421c28672..69c1a95b4 100644 --- a/src/Discord.Net.Audio/AudioServiceConfig.cs +++ b/src/Discord.Net.Audio/AudioServiceConfig.cs @@ -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; + /// Gets or sets the number of channels (1 or 2) used for outgoing audio. + 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(ref T storage, T value) { diff --git a/src/Discord.Net.Audio/DiscordAudioClient.cs b/src/Discord.Net.Audio/DiscordAudioClient.cs index 791321188..eecbe0749 100644 --- a/src/Discord.Net.Audio/DiscordAudioClient.cs +++ b/src/Discord.Net.Audio/DiscordAudioClient.cs @@ -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; diff --git a/src/Discord.Net.Audio/Net/WebSockets/VoiceWebSocket.cs b/src/Discord.Net.Audio/Net/WebSockets/VoiceWebSocket.cs index 93c16572c..1ccf3f8db 100644 --- a/src/Discord.Net.Audio/Net/WebSockets/VoiceWebSocket.cs +++ b/src/Discord.Net.Audio/Net/WebSockets/VoiceWebSocket.cs @@ -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(); - _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; diff --git a/src/Discord.Net.Audio/Opus.cs b/src/Discord.Net.Audio/Opus.cs deleted file mode 100644 index 917047d05..000000000 --- a/src/Discord.Net.Audio/Opus.cs +++ /dev/null @@ -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 - } - - /// Supported coding modes. - public enum Application : int - { - /// - /// 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. - /// - Voip = 2048, - /// - /// 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. - /// - Audio = 2049, - /// Low-delay mode that disables the speech-optimized mode in exchange for slightly reduced delay. - Restricted_LowLatency = 2051 - } - - public enum Error : int - { - /// No error. - OK = 0, - /// One or more invalid/out of range arguments. - BadArg = -1, - /// The mode struct passed is invalid. - BufferToSmall = -2, - /// An internal error was detected. - InternalError = -3, - /// The compressed data passed is corrupted. - InvalidPacket = -4, - /// Invalid/unsupported request number. - Unimplemented = -5, - /// An encoder or decoder structure is invalid or already freed. - InvalidState = -6, - /// Memory allocation has failed. - AllocFail = -7 - } - } -} diff --git a/src/Discord.Net.Audio/Opus/Enums.cs b/src/Discord.Net.Audio/Opus/Enums.cs new file mode 100644 index 000000000..2a705f746 --- /dev/null +++ b/src/Discord.Net.Audio/Opus/Enums.cs @@ -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 + } + + /// Supported coding modes. + internal enum OpusApplication : int + { + /// + /// 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. + /// + Voip = 2048, + /// + /// 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. + /// + Audio = 2049, + /// Low-delay mode that disables the speech-optimized mode in exchange for slightly reduced delay. + Restricted_LowLatency = 2051 + } + + internal enum OpusError : int + { + /// No error. + OK = 0, + /// One or more invalid/out of range arguments. + BadArg = -1, + /// The mode struct passed is invalid. + BufferToSmall = -2, + /// An internal error was detected. + InternalError = -3, + /// The compressed data passed is corrupted. + InvalidPacket = -4, + /// Invalid/unsupported request number. + Unimplemented = -5, + /// An encoder or decoder structure is invalid or already freed. + InvalidState = -6, + /// Memory allocation has failed. + AllocFail = -7 + } + +} diff --git a/src/Discord.Net.Audio/OpusDecoder.cs b/src/Discord.Net.Audio/Opus/OpusDecoder.cs similarity index 57% rename from src/Discord.Net.Audio/OpusDecoder.cs rename to src/Discord.Net.Audio/Opus/OpusDecoder.cs index e6c9cd400..fb5483327 100644 --- a/src/Discord.Net.Audio/OpusDecoder.cs +++ b/src/Discord.Net.Audio/Opus/OpusDecoder.cs @@ -1,11 +1,26 @@ using System; +using System.Runtime.InteropServices; +using System.Security; -namespace Discord.Audio +namespace Discord.Audio.Opus { /// Opus codec wrapper. 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; /// Gets the bit rate of the encoder. public const int BitRate = 16; @@ -22,7 +37,7 @@ namespace Discord.Audio /// Gets the bytes per frame. public int FrameSize { get; private set; } - /// Creates a new Opus encoder. + /// Creates a new Opus decoder. /// Sampling rate of the input signal (Hz). Supported Values: 8000, 12000, 16000, 24000, or 48000. /// Number of channels (1 or 2) in input signal. /// Length, in milliseconds, that each frame takes. Supported Values: 2.5, 5, 10, 20, 40, 60 @@ -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); } - /// Produces Opus encoded audio from PCM samples. - /// PCM samples to encode. - /// Offset of the frame in pcmSamples. - /// Buffer to store the encoded frame. - /// Length of the frame contained in outputBuffer. - public unsafe int DecodeFrame(byte[] input, int inputOffset, byte[] output) + /// Produces PCM samples from Opus-encoded audio. + /// PCM samples to decode. + /// Offset of the frame in input. + /// Buffer to store the decoded frame. + /// Length of the frame contained in output. + 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; } - /// Gets or sets whether Forward Error Correction is enabled. - 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 } } \ No newline at end of file diff --git a/src/Discord.Net.Audio/OpusEncoder.cs b/src/Discord.Net.Audio/Opus/OpusEncoder.cs similarity index 67% rename from src/Discord.Net.Audio/OpusEncoder.cs rename to src/Discord.Net.Audio/Opus/OpusEncoder.cs index fbb6a0729..a7f1b4ac1 100644 --- a/src/Discord.Net.Audio/OpusEncoder.cs +++ b/src/Discord.Net.Audio/Opus/OpusEncoder.cs @@ -1,11 +1,28 @@ using System; +using System.Runtime.InteropServices; +using System.Security; -namespace Discord.Audio +namespace Discord.Audio.Opus { /// Opus codec wrapper. 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; /// Gets the bit rate of the encoder. public const int BitsPerSample = 16; @@ -24,7 +41,7 @@ namespace Discord.Audio /// Gets the bit rate in kbit/s. public int? BitRate { get; private set; } /// Gets the coding mode of the encoder. - public Opus.Application Application { get; private set; } + public OpusApplication Application { get; private set; } /// Creates a new Opus encoder. /// Sampling rate of the input signal (Hz). Supported Values: 8000, 12000, 16000, 24000, or 48000. @@ -33,7 +50,7 @@ namespace Discord.Audio /// Bitrate (kbit/s) used for this encoder. Supported Values: 1-512. Null will use the recommended bitrate. /// Coding mode. /// A new OpusEncoder - 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()); } /// Gets or sets whether Forward Error Correction is enabled. @@ -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; } diff --git a/src/Discord.Net.Audio/Sodium.cs b/src/Discord.Net.Audio/Sodium.cs index 9d9e6c297..7c98dffba 100644 --- a/src/Discord.Net.Audio/Sodium.cs +++ b/src/Discord.Net.Audio/Sodium.cs @@ -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); - } - } } diff --git a/src/Discord.Net.Audio/Sodium/SecretBox.cs b/src/Discord.Net.Audio/Sodium/SecretBox.cs new file mode 100644 index 000000000..da2287d8b --- /dev/null +++ b/src/Discord.Net.Audio/Sodium/SecretBox.cs @@ -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); + } + } +} diff --git a/src/Discord.Net.Commands/CommandService.Events.cs b/src/Discord.Net.Commands/CommandService.Events.cs index f70223f14..c5ad7747b 100644 --- a/src/Discord.Net.Commands/CommandService.Events.cs +++ b/src/Discord.Net.Commands/CommandService.Events.cs @@ -2,7 +2,7 @@ namespace Discord.Commands { - public class CommandEventArgs + public class CommandEventArgs : EventArgs { private readonly string[] _args; diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index b39c55d47..c856fb3b0 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; namespace Discord.Commands { /// A Discord.Net client with extensions for handling common bot operations like text commands. - public partial class CommandService : IService + public sealed partial class CommandService : IService { private const string DefaultPermissionError = "You do not have permission to access this command."; diff --git a/src/Discord.Net.Net45/Discord.Net.csproj b/src/Discord.Net.Net45/Discord.Net.csproj index d5e5639f8..61f89a7a4 100644 --- a/src/Discord.Net.Net45/Discord.Net.csproj +++ b/src/Discord.Net.Net45/Discord.Net.csproj @@ -224,11 +224,11 @@ Net\TimeoutException.cs - - Net\WebSockets\GatewayWebSocket.cs + + Net\WebSockets\GatewaySocket.cs - - Net\WebSockets\GatewayWebSockets.Events.cs + + Net\WebSockets\GatewaySocket.Events.cs Net\WebSockets\IWebSocketEngine.cs diff --git a/src/Discord.Net/API/Messages/GatewaySocket.cs b/src/Discord.Net/API/Messages/GatewaySocket.cs index d84fbc761..f890db0e9 100644 --- a/src/Discord.Net/API/Messages/GatewaySocket.cs +++ b/src/Discord.Net/API/Messages/GatewaySocket.cs @@ -9,165 +9,165 @@ using System.Collections.Generic; namespace Discord.API { - public enum GatewayOpCodes : byte - { - /// Client <-- Server - Used to send most events. - Dispatch = 0, - /// Client <-> Server - Used to keep the connection alive and measure latency. - Heartbeat = 1, - /// Client --> Server - Used to associate a connection with a token and specify configuration. - Identify = 2, - /// Client --> Server - Used to update client's status and current game id. - StatusUpdate = 3, - /// Client --> Server - Used to join a particular voice channel. - VoiceStateUpdate = 4, - /// Client --> Server - Used to ensure the server's voice server is alive. Only send this if voice connection fails or suddenly drops. - VoiceServerPing = 5, - /// Client --> Server - Used to resume a connection after a redirect occurs. - Resume = 6, - /// Client <-- Server - Used to notify a client that they must reconnect to another gateway. - Redirect = 7, - /// Client --> Server - Used to request all members that were withheld by large_threshold - RequestGuildMembers = 8 - } + public enum GatewayOpCodes : byte + { + /// Client <-- Server - Used to send most events. + Dispatch = 0, + /// Client <-> Server - Used to keep the connection alive and measure latency. + Heartbeat = 1, + /// Client --> Server - Used to associate a connection with a token and specify configuration. + Identify = 2, + /// Client --> Server - Used to update client's status and current game id. + StatusUpdate = 3, + /// Client --> Server - Used to join a particular voice channel. + VoiceStateUpdate = 4, + /// Client --> Server - Used to ensure the server's voice server is alive. Only send this if voice connection fails or suddenly drops. + VoiceServerPing = 5, + /// Client --> Server - Used to resume a connection after a redirect occurs. + Resume = 6, + /// Client <-- Server - Used to notify a client that they must reconnect to another gateway. + Redirect = 7, + /// Client --> Server - Used to request all members that were withheld by large_threshold + 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 : 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 : 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(); - 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(); + return (T)base.Payload; + } + set { base.Payload = value; } + } + } - //Commands - internal sealed class HeartbeatCommand : WebSocketMessage - { - public HeartbeatCommand() : base((int)GatewayOpCodes.Heartbeat, EpochTime.GetMilliseconds()) { } - } - internal sealed class IdentifyCommand : WebSocketMessage - { - public IdentifyCommand() : base((int)GatewayOpCodes.Identify) { } - public class Data - { - [JsonProperty("token")] - public string Token; - [JsonProperty("v")] - public int Version = 3; - [JsonProperty("properties")] - public Dictionary Properties = new Dictionary(); - [JsonProperty("large_threshold", NullValueHandling = NullValueHandling.Ignore)] - public int? LargeThreshold; - [JsonProperty("compress", NullValueHandling = NullValueHandling.Ignore)] - public bool? Compress; + //Commands + internal sealed class HeartbeatCommand : WebSocketMessage + { + public HeartbeatCommand() : base((int)GatewayOpCodes.Heartbeat, EpochTime.GetMilliseconds()) { } + } + internal sealed class IdentifyCommand : WebSocketMessage + { + public IdentifyCommand() : base((int)GatewayOpCodes.Identify) { } + public class Data + { + [JsonProperty("token")] + public string Token; + [JsonProperty("v")] + public int Version = 3; + [JsonProperty("properties")] + public Dictionary Properties = new Dictionary(); + [JsonProperty("large_threshold", NullValueHandling = NullValueHandling.Ignore)] + public int? LargeThreshold; + [JsonProperty("compress", NullValueHandling = NullValueHandling.Ignore)] + public bool? Compress; } - } + } - internal sealed class StatusUpdateCommand : WebSocketMessage - { - 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 + { + 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 - { - 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 + { + 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 - { - 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 + { + 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; + } } diff --git a/src/Discord.Net/DiscordClient.Users.cs b/src/Discord.Net/DiscordClient.Users.cs index fbe7a572f..f6d4ed30b 100644 --- a/src/Discord.Net/DiscordClient.Users.cs +++ b/src/Discord.Net/DiscordClient.Users.cs @@ -58,7 +58,7 @@ namespace Discord } } - public partial class DiscordClient + public partial class DiscordClient : IDisposable { public event EventHandler 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; } - } + } } \ No newline at end of file diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index 4c4c82866..f8a782ebe 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -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; /// Returns the internal websocket object. - 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(); - 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; - } - /// Connects to the Discord server with the provided email and password. /// Returns a token for future connections. public async Task 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; + } /// Connects to the Discord server with the provided 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; _connectedEvent.Set(); @@ -359,8 +353,8 @@ namespace Discord } /// Disconnects from the Discord server, canceling any pending requests. - 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 obj) - where T : class - { - _singletons.Add(typeof(T), obj); - return obj; - } - public T GetSingleton(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 obj) - where T : class, IService - { - AddSingleton(obj); - obj.Install(this); - return obj; - } - public T GetService(bool required = true) - where T : class, IService - => GetSingleton(required); private async Task OnReceivedEvent(WebSocketEventEventArgs e) { @@ -851,45 +816,99 @@ namespace Discord _sentInitialLog = true; } + #region Async Wrapper + /// Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications. + public void Run(Func asyncAction) + { + try + { + asyncAction().GetAwaiter().GetResult(); //Avoids creating AggregateExceptions + } + catch (TaskCanceledException) { } + _disconnectedEvent.WaitOne(); + } + /// Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications. + public void Run() + { + _disconnectedEvent.WaitOne(); + } + #endregion + + #region Services + public T AddSingleton(T obj) + where T : class + { + _singletons.Add(typeof(T), obj); + return obj; + } + public T GetSingleton(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 obj) + where T : class, IService + { + AddSingleton(obj); + obj.Install(this); + return obj; + } + public T GetService(bool required = true) + where T : class, IService + => GetSingleton(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 - /// Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications. - public void Run(Func asyncAction) - { - try - { - asyncAction().GetAwaiter().GetResult(); //Avoids creating AggregateExceptions - } - catch (TaskCanceledException) { } - _disconnectedEvent.WaitOne(); - } - /// Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications. - 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; + } + } } \ No newline at end of file diff --git a/src/Discord.Net/Models/User.cs b/src/Discord.Net/Models/User.cs index 80e7a19eb..924166529 100644 --- a/src/Discord.Net/Models/User.cs +++ b/src/Discord.Net/Models/User.cs @@ -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 { diff --git a/src/Discord.Net/Net/HttpException.cs b/src/Discord.Net/Net/HttpException.cs index 84ae66530..192136c1a 100644 --- a/src/Discord.Net/Net/HttpException.cs +++ b/src/Discord.Net/Net/HttpException.cs @@ -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 + } } diff --git a/src/Discord.Net/Net/TimeoutException.cs b/src/Discord.Net/Net/TimeoutException.cs index 090d1e5d4..c0385015a 100644 --- a/src/Discord.Net/Net/TimeoutException.cs +++ b/src/Discord.Net/Net/TimeoutException.cs @@ -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.") diff --git a/src/Discord.Net/Net/WebSockets/GatewayWebSockets.Events.cs b/src/Discord.Net/Net/WebSockets/GatewaySocket.Events.cs similarity index 93% rename from src/Discord.Net/Net/WebSockets/GatewayWebSockets.Events.cs rename to src/Discord.Net/Net/WebSockets/GatewaySocket.Events.cs index 78f3c1ac5..a47f8231c 100644 --- a/src/Discord.Net/Net/WebSockets/GatewayWebSockets.Events.cs +++ b/src/Discord.Net/Net/WebSockets/GatewaySocket.Events.cs @@ -14,7 +14,7 @@ namespace Discord.Net.WebSockets } } - public partial class GatewayWebSocket + public partial class GatewaySocket { public event EventHandler ReceivedDispatch; private void RaiseReceivedDispatch(string type, JToken payload) diff --git a/src/Discord.Net/Net/WebSockets/GatewayWebSocket.cs b/src/Discord.Net/Net/WebSockets/GatewaySocket.cs similarity index 90% rename from src/Discord.Net/Net/WebSockets/GatewayWebSocket.cs rename to src/Discord.Net/Net/WebSockets/GatewaySocket.cs index 441de8f40..aaa79a0aa 100644 --- a/src/Discord.Net/Net/WebSockets/GatewayWebSocket.cs +++ b/src/Discord.Net/Net/WebSockets/GatewaySocket.cs @@ -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; } diff --git a/src/Discord.Net/Net/WebSockets/WebSocket.cs b/src/Discord.Net/Net/WebSockets/WebSocket.cs index b0352c067..ea76037b2 100644 --- a/src/Discord.Net/Net/WebSockets/WebSocket.cs +++ b/src/Discord.Net/Net/WebSockets/WebSocket.cs @@ -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) diff --git a/src/Discord.Net/Settings.StyleCop b/src/Discord.Net/Settings.StyleCop new file mode 100644 index 000000000..bb05f99bc --- /dev/null +++ b/src/Discord.Net/Settings.StyleCop @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Discord.Net/project.json b/src/Discord.Net/project.json index 59c5573aa..b0d08bf04 100644 --- a/src/Discord.Net/project.json +++ b/src/Discord.Net/project.json @@ -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" + } } } } \ No newline at end of file